Parcourir la source

Move @roo-code/cloud to the Roo-Code repo (#7503)

Chris Estreich il y a 4 mois
Parent
commit
cd9e92fa9b
100 fichiers modifiés avec 8640 ajouts et 515 suppressions
  1. 1 1
      apps/web-roo-code/package.json
  2. 2 4
      package.json
  3. 20 0
      packages/cloud/eslint.config.mjs
  4. 29 0
      packages/cloud/package.json
  5. 137 0
      packages/cloud/src/CloudAPI.ts
  6. 363 0
      packages/cloud/src/CloudService.ts
  7. 282 0
      packages/cloud/src/CloudSettingsService.ts
  8. 50 0
      packages/cloud/src/CloudShareService.ts
  9. 154 0
      packages/cloud/src/RefreshTimer.ts
  10. 78 0
      packages/cloud/src/StaticSettingsService.ts
  11. 93 0
      packages/cloud/src/StaticTokenAuthService.ts
  12. 246 0
      packages/cloud/src/TelemetryClient.ts
  13. 727 0
      packages/cloud/src/WebAuthService.ts
  14. 59 0
      packages/cloud/src/__mocks__/vscode.ts
  15. 147 0
      packages/cloud/src/__tests__/CloudService.integration.test.ts
  16. 600 0
      packages/cloud/src/__tests__/CloudService.test.ts
  17. 172 0
      packages/cloud/src/__tests__/CloudSettingsService.parsing.test.ts
  18. 535 0
      packages/cloud/src/__tests__/CloudSettingsService.test.ts
  19. 318 0
      packages/cloud/src/__tests__/CloudShareService.test.ts
  20. 210 0
      packages/cloud/src/__tests__/RefreshTimer.test.ts
  21. 102 0
      packages/cloud/src/__tests__/StaticSettingsService.test.ts
  22. 314 0
      packages/cloud/src/__tests__/StaticTokenAuthService.spec.ts
  23. 740 0
      packages/cloud/src/__tests__/TelemetryClient.test.ts
  24. 1196 0
      packages/cloud/src/__tests__/WebAuthService.spec.ts
  25. 290 0
      packages/cloud/src/bridge/ExtensionBridgeService.ts
  26. 297 0
      packages/cloud/src/bridge/ExtensionManager.ts
  27. 289 0
      packages/cloud/src/bridge/SocketConnectionManager.ts
  28. 279 0
      packages/cloud/src/bridge/TaskManager.ts
  29. 6 0
      packages/cloud/src/config.ts
  30. 42 0
      packages/cloud/src/errors.ts
  31. 49 0
      packages/cloud/src/importVscode.ts
  32. 5 0
      packages/cloud/src/index.ts
  33. 5 0
      packages/cloud/src/utils.ts
  34. 9 0
      packages/cloud/tsconfig.json
  35. 14 0
      packages/cloud/vitest.config.ts
  36. 2 1
      packages/config-typescript/base.json
  37. 1 1
      packages/telemetry/package.json
  38. 1 1
      packages/types/npm/package.metadata.json
  39. 0 1
      packages/types/src/__tests__/ipc.test.ts
  40. 0 1
      packages/types/src/__tests__/provider-settings.test.ts
  41. 618 0
      packages/types/src/cloud.ts
  42. 1 0
      packages/types/src/index.ts
  43. 0 1
      packages/types/src/providers/__tests__/claude-code.spec.ts
  44. 98 72
      pnpm-lock.yaml
  45. 0 340
      scripts/link-packages.ts
  46. 2 2
      src/__tests__/command-integration.spec.ts
  47. 0 1
      src/__tests__/command-mentions.spec.ts
  48. 0 1
      src/__tests__/commands.spec.ts
  49. 0 2
      src/api/providers/__tests__/bedrock-error-handling.spec.ts
  50. 0 2
      src/api/providers/__tests__/cerebras.spec.ts
  51. 2 2
      src/api/providers/__tests__/claude-code-caching.spec.ts
  52. 0 1
      src/api/providers/__tests__/claude-code.spec.ts
  53. 0 1
      src/api/providers/__tests__/constants.spec.ts
  54. 1 1
      src/api/providers/__tests__/gemini-handler.spec.ts
  55. 2 2
      src/api/providers/__tests__/io-intelligence.spec.ts
  56. 0 1
      src/api/providers/__tests__/lite-llm.spec.ts
  57. 0 1
      src/api/providers/__tests__/roo.spec.ts
  58. 4 3
      src/api/providers/fetchers/__tests__/lmstudio.test.ts
  59. 1 2
      src/api/providers/fetchers/__tests__/ollama.test.ts
  60. 1 0
      src/api/providers/roo.ts
  61. 0 2
      src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts
  62. 1 1
      src/core/context/context-management/__tests__/context-error-handling.test.ts
  63. 1 2
      src/core/mentions/__tests__/index.spec.ts
  64. 0 1
      src/core/mentions/__tests__/processUserContentMentions.spec.ts
  65. 2 2
      src/core/prompts/__tests__/get-prompt-component.spec.ts
  66. 0 1
      src/core/prompts/sections/__tests__/custom-instructions-global.spec.ts
  67. 0 2
      src/core/prompts/sections/__tests__/custom-instructions-path-detection.spec.ts
  68. 0 1
      src/core/prompts/tools/__tests__/fetch-instructions.spec.ts
  69. 0 1
      src/core/prompts/tools/__tests__/new-task.spec.ts
  70. 2 2
      src/core/task/__tests__/AutoApprovalHandler.spec.ts
  71. 2 2
      src/core/task/__tests__/Task.dispose.test.ts
  72. 0 1
      src/core/tools/__tests__/askFollowupQuestionTool.spec.ts
  73. 1 1
      src/core/tools/__tests__/attemptCompletionTool.spec.ts
  74. 3 2
      src/core/webview/ClineProvider.ts
  75. 1 2
      src/core/webview/__tests__/ClineProvider.spec.ts
  76. 2 2
      src/core/webview/__tests__/messageEnhancer.test.ts
  77. 1 0
      src/core/webview/webviewMessageHandler.ts
  78. 2 1
      src/extension.ts
  79. 2 2
      src/integrations/claude-code/__tests__/message-filter.spec.ts
  80. 0 2
      src/integrations/claude-code/__tests__/run.spec.ts
  81. 1 1
      src/integrations/misc/__tests__/extract-text-large-files.spec.ts
  82. 1 3
      src/integrations/misc/__tests__/open-file.spec.ts
  83. 3 3
      src/package.json
  84. 0 2
      src/services/browser/__tests__/BrowserSession.spec.ts
  85. 10 11
      src/services/browser/__tests__/UrlContentFetcher.spec.ts
  86. 0 2
      src/services/code-index/__tests__/config-manager.spec.ts
  87. 1 1
      src/services/code-index/embedders/__tests__/mistral.spec.ts
  88. 1 1
      src/services/code-index/embedders/__tests__/ollama.spec.ts
  89. 2 1
      src/services/code-index/embedders/__tests__/openai-compatible-rate-limit.spec.ts
  90. 2 2
      src/services/code-index/embedders/__tests__/openai.spec.ts
  91. 0 1
      src/services/code-index/embedders/__tests__/vercel-ai-gateway.spec.ts
  92. 0 2
      src/services/code-index/processors/__tests__/parser.vb.spec.ts
  93. 1 1
      src/services/code-index/shared/__tests__/get-relative-path.spec.ts
  94. 0 1
      src/services/code-index/shared/__tests__/validation-helpers.spec.ts
  95. 0 1
      src/services/command/__tests__/built-in-commands.spec.ts
  96. 1 1
      src/services/command/__tests__/frontmatter-commands.spec.ts
  97. 0 1
      src/services/glob/__tests__/gitignore-integration.spec.ts
  98. 0 1
      src/services/glob/__tests__/gitignore-test.spec.ts
  99. 1 2
      src/services/glob/__tests__/list-files.spec.ts
  100. 2 2
      src/services/marketplace/MarketplaceManager.ts

+ 1 - 1
apps/web-roo-code/package.json

@@ -22,7 +22,7 @@
 		"embla-carousel-auto-scroll": "^8.6.0",
 		"embla-carousel-auto-scroll": "^8.6.0",
 		"embla-carousel-autoplay": "^8.6.0",
 		"embla-carousel-autoplay": "^8.6.0",
 		"embla-carousel-react": "^8.6.0",
 		"embla-carousel-react": "^8.6.0",
-		"framer-motion": "^12.15.0",
+		"framer-motion": "12.15.0",
 		"lucide-react": "^0.518.0",
 		"lucide-react": "^0.518.0",
 		"next": "^15.2.5",
 		"next": "^15.2.5",
 		"next-themes": "^0.4.6",
 		"next-themes": "^0.4.6",

+ 2 - 4
package.json

@@ -24,9 +24,7 @@
 		"knip": "knip --include files",
 		"knip": "knip --include files",
 		"update-contributors": "node scripts/update-contributors.js",
 		"update-contributors": "node scripts/update-contributors.js",
 		"evals": "dotenvx run -f packages/evals/.env.development packages/evals/.env.local -- docker compose -f packages/evals/docker-compose.yml --profile server --profile runner up --build --scale runner=0",
 		"evals": "dotenvx run -f packages/evals/.env.development packages/evals/.env.local -- docker compose -f packages/evals/docker-compose.yml --profile server --profile runner up --build --scale runner=0",
-		"npm:publish:types": "pnpm --filter @roo-code/types npm:publish",
-		"link-workspace-packages": "tsx scripts/link-packages.ts",
-		"unlink-workspace-packages": "tsx scripts/link-packages.ts --unlink"
+		"npm:publish:types": "pnpm --filter @roo-code/types npm:publish"
 	},
 	},
 	"devDependencies": {
 	"devDependencies": {
 		"@changesets/cli": "^2.27.10",
 		"@changesets/cli": "^2.27.10",
@@ -47,7 +45,7 @@
 		"prettier": "^3.4.2",
 		"prettier": "^3.4.2",
 		"rimraf": "^6.0.1",
 		"rimraf": "^6.0.1",
 		"tsx": "^4.19.3",
 		"tsx": "^4.19.3",
-		"turbo": "^2.5.3",
+		"turbo": "^2.5.6",
 		"typescript": "^5.4.5"
 		"typescript": "^5.4.5"
 	},
 	},
 	"lint-staged": {
 	"lint-staged": {

+ 20 - 0
packages/cloud/eslint.config.mjs

@@ -0,0 +1,20 @@
+import { config } from "@roo-code/config-eslint/base"
+import globals from "globals"
+
+/** @type {import("eslint").Linter.Config} */
+export default [
+	...config,
+	{
+		files: ["**/*.cjs"],
+		languageOptions: {
+			globals: {
+				...globals.node,
+				...globals.commonjs,
+			},
+			sourceType: "commonjs",
+		},
+		rules: {
+			"@typescript-eslint/no-require-imports": "off",
+		},
+	},
+]

+ 29 - 0
packages/cloud/package.json

@@ -0,0 +1,29 @@
+{
+	"name": "@roo-code/cloud",
+	"description": "Roo Code Cloud services.",
+	"version": "0.0.0",
+	"type": "module",
+	"exports": "./src/index.ts",
+	"scripts": {
+		"lint": "eslint src --ext=ts --max-warnings=0",
+		"check-types": "tsc --noEmit",
+		"test": "vitest run",
+		"clean": "rimraf .turbo"
+	},
+	"dependencies": {
+		"@roo-code/types": "workspace:^",
+		"ioredis": "^5.6.1",
+		"jwt-decode": "^4.0.0",
+		"p-wait-for": "^5.0.2",
+		"socket.io-client": "^4.8.1",
+		"zod": "^3.25.76"
+	},
+	"devDependencies": {
+		"@roo-code/config-eslint": "workspace:^",
+		"@roo-code/config-typescript": "workspace:^",
+		"@types/node": "^24.1.0",
+		"@types/vscode": "^1.102.0",
+		"globals": "^16.3.0",
+		"vitest": "^3.2.4"
+	}
+}

+ 137 - 0
packages/cloud/src/CloudAPI.ts

@@ -0,0 +1,137 @@
+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),
+		})
+	}
+}

+ 363 - 0
packages/cloud/src/CloudService.ts

@@ -0,0 +1,363 @@
+import type { Disposable, ExtensionContext } from "vscode"
+import EventEmitter from "events"
+
+import type {
+	TelemetryEvent,
+	ClineMessage,
+	CloudServiceEvents,
+	AuthService,
+	SettingsService,
+	CloudUserInfo,
+	OrganizationAllowList,
+	OrganizationSettings,
+	ShareVisibility,
+	UserSettingsConfig,
+	UserSettingsData,
+	UserFeatures,
+} from "@roo-code/types"
+
+import { TaskNotFoundError } from "./errors.js"
+import { WebAuthService } from "./WebAuthService.js"
+import { StaticTokenAuthService } from "./StaticTokenAuthService.js"
+import { CloudSettingsService } from "./CloudSettingsService.js"
+import { StaticSettingsService } from "./StaticSettingsService.js"
+import { CloudTelemetryClient as TelemetryClient } from "./TelemetryClient.js"
+import { CloudShareService } from "./CloudShareService.js"
+import { CloudAPI } from "./CloudAPI.js"
+
+type AuthStateChangedPayload = CloudServiceEvents["auth-state-changed"][0]
+type AuthUserInfoPayload = CloudServiceEvents["user-info"][0]
+type SettingsPayload = CloudServiceEvents["settings-updated"][0]
+
+export class CloudService extends EventEmitter<CloudServiceEvents> implements Disposable {
+	private static _instance: CloudService | null = null
+
+	private context: ExtensionContext
+
+	private authStateListener: (data: AuthStateChangedPayload) => void
+	private authUserInfoListener: (data: AuthUserInfoPayload) => void
+	private settingsListener: (data: SettingsPayload) => void
+
+	private isInitialized = false
+	private log: (...args: unknown[]) => void
+
+	/**
+	 * Services
+	 */
+
+	private _authService: AuthService | null = null
+
+	public get authService() {
+		return this._authService
+	}
+
+	private _settingsService: SettingsService | null = null
+
+	public get settingsService() {
+		return this._settingsService
+	}
+
+	private _telemetryClient: TelemetryClient | null = null
+
+	public get telemetryClient() {
+		return this._telemetryClient
+	}
+
+	private _shareService: CloudShareService | null = null
+
+	public get shareService() {
+		return this._shareService
+	}
+
+	private _cloudAPI: CloudAPI | null = null
+
+	public get cloudAPI() {
+		return this._cloudAPI
+	}
+
+	private constructor(context: ExtensionContext, log?: (...args: unknown[]) => void) {
+		super()
+
+		this.context = context
+		this.log = log || console.log
+
+		this.authStateListener = (data: AuthStateChangedPayload) => {
+			this.emit("auth-state-changed", data)
+		}
+
+		this.authUserInfoListener = (data: AuthUserInfoPayload) => {
+			this.emit("user-info", data)
+		}
+
+		this.settingsListener = (data: SettingsPayload) => {
+			this.emit("settings-updated", data)
+		}
+	}
+
+	public async initialize(): Promise<void> {
+		if (this.isInitialized) {
+			return
+		}
+
+		try {
+			// For testing you can create a token with:
+			// `pnpm --filter @roo-code-cloud/roomote-cli development auth job-token --job-id 1 --user-id user_2xmBhejNeDTwanM8CgIOnMgVxzC --org-id org_2wbhchVXZMQl8OS1yt0mrDazCpW`
+			// The token will last for 1 hour.
+			const cloudToken = process.env.ROO_CODE_CLOUD_TOKEN
+
+			if (cloudToken && cloudToken.length > 0) {
+				this._authService = new StaticTokenAuthService(this.context, cloudToken, this.log)
+			} else {
+				this._authService = new WebAuthService(this.context, this.log)
+			}
+
+			this._authService.on("auth-state-changed", this.authStateListener)
+			this._authService.on("user-info", this.authUserInfoListener)
+			await this._authService.initialize()
+
+			// Check for static settings environment variable.
+			const staticOrgSettings = process.env.ROO_CODE_CLOUD_ORG_SETTINGS
+
+			if (staticOrgSettings && staticOrgSettings.length > 0) {
+				this._settingsService = new StaticSettingsService(staticOrgSettings, this.log)
+			} else {
+				const cloudSettingsService = new CloudSettingsService(this.context, this._authService, this.log)
+
+				cloudSettingsService.on("settings-updated", this.settingsListener)
+				await cloudSettingsService.initialize()
+
+				this._settingsService = cloudSettingsService
+			}
+
+			this._cloudAPI = new CloudAPI(this._authService, this.log)
+
+			this._telemetryClient = new TelemetryClient(this._authService, this._settingsService)
+
+			this._shareService = new CloudShareService(this._cloudAPI, this._settingsService, this.log)
+
+			this.isInitialized = true
+		} catch (error) {
+			this.log("[CloudService] Failed to initialize:", error)
+			throw new Error(`Failed to initialize CloudService: ${error}`)
+		}
+	}
+
+	// AuthService
+
+	public async login(): Promise<void> {
+		this.ensureInitialized()
+		return this.authService!.login()
+	}
+
+	public async logout(): Promise<void> {
+		this.ensureInitialized()
+		return this.authService!.logout()
+	}
+
+	public isAuthenticated(): boolean {
+		this.ensureInitialized()
+		return this.authService!.isAuthenticated()
+	}
+
+	public hasActiveSession(): boolean {
+		this.ensureInitialized()
+		return this.authService!.hasActiveSession()
+	}
+
+	public hasOrIsAcquiringActiveSession(): boolean {
+		this.ensureInitialized()
+		return this.authService!.hasOrIsAcquiringActiveSession()
+	}
+
+	public getUserInfo(): CloudUserInfo | null {
+		this.ensureInitialized()
+		return this.authService!.getUserInfo()
+	}
+
+	public getOrganizationId(): string | null {
+		this.ensureInitialized()
+		const userInfo = this.authService!.getUserInfo()
+		return userInfo?.organizationId || null
+	}
+
+	public getOrganizationName(): string | null {
+		this.ensureInitialized()
+		const userInfo = this.authService!.getUserInfo()
+		return userInfo?.organizationName || null
+	}
+
+	public getOrganizationRole(): string | null {
+		this.ensureInitialized()
+		const userInfo = this.authService!.getUserInfo()
+		return userInfo?.organizationRole || null
+	}
+
+	public hasStoredOrganizationId(): boolean {
+		this.ensureInitialized()
+		return this.authService!.getStoredOrganizationId() !== null
+	}
+
+	public getStoredOrganizationId(): string | null {
+		this.ensureInitialized()
+		return this.authService!.getStoredOrganizationId()
+	}
+
+	public getAuthState(): string {
+		this.ensureInitialized()
+		return this.authService!.getState()
+	}
+
+	public async handleAuthCallback(
+		code: string | null,
+		state: string | null,
+		organizationId?: string | null,
+	): Promise<void> {
+		this.ensureInitialized()
+		return this.authService!.handleCallback(code, state, organizationId)
+	}
+
+	// SettingsService
+
+	public getAllowList(): OrganizationAllowList {
+		this.ensureInitialized()
+		return this.settingsService!.getAllowList()
+	}
+
+	public getOrganizationSettings(): OrganizationSettings | undefined {
+		this.ensureInitialized()
+		return this.settingsService!.getSettings()
+	}
+
+	public getUserSettings(): UserSettingsData | undefined {
+		this.ensureInitialized()
+		return this.settingsService!.getUserSettings()
+	}
+
+	public getUserFeatures(): UserFeatures {
+		this.ensureInitialized()
+		return this.settingsService!.getUserFeatures()
+	}
+
+	public getUserSettingsConfig(): UserSettingsConfig {
+		this.ensureInitialized()
+		return this.settingsService!.getUserSettingsConfig()
+	}
+
+	public async updateUserSettings(settings: Partial<UserSettingsConfig>): Promise<boolean> {
+		this.ensureInitialized()
+		return this.settingsService!.updateUserSettings(settings)
+	}
+
+	// TelemetryClient
+
+	public captureEvent(event: TelemetryEvent): void {
+		this.ensureInitialized()
+		this.telemetryClient!.capture(event)
+	}
+
+	// ShareService
+
+	public async shareTask(
+		taskId: string,
+		visibility: ShareVisibility = "organization",
+		clineMessages?: ClineMessage[],
+	) {
+		this.ensureInitialized()
+
+		try {
+			return await this.shareService!.shareTask(taskId, visibility)
+		} catch (error) {
+			if (error instanceof TaskNotFoundError && clineMessages) {
+				// Backfill messages and retry.
+				await this.telemetryClient!.backfillMessages(clineMessages, taskId)
+				return await this.shareService!.shareTask(taskId, visibility)
+			}
+
+			throw error
+		}
+	}
+
+	public async canShareTask(): Promise<boolean> {
+		this.ensureInitialized()
+		return this.shareService!.canShareTask()
+	}
+
+	// Lifecycle
+
+	public dispose(): void {
+		if (this.authService) {
+			this.authService.off("auth-state-changed", this.authStateListener)
+			this.authService.off("user-info", this.authUserInfoListener)
+		}
+
+		if (this.settingsService) {
+			if (this.settingsService instanceof CloudSettingsService) {
+				this.settingsService.off("settings-updated", this.settingsListener)
+			}
+
+			this.settingsService.dispose()
+		}
+
+		this.isInitialized = false
+	}
+
+	private ensureInitialized(): void {
+		if (!this.isInitialized) {
+			throw new Error("CloudService not initialized.")
+		}
+	}
+
+	static get instance(): CloudService {
+		if (!this._instance) {
+			throw new Error("CloudService not initialized")
+		}
+
+		return this._instance
+	}
+
+	static async createInstance(
+		context: ExtensionContext,
+		log?: (...args: unknown[]) => void,
+		eventHandlers?: Partial<{
+			[K in keyof CloudServiceEvents]: (...args: CloudServiceEvents[K]) => void
+		}>,
+	): Promise<CloudService> {
+		if (this._instance) {
+			throw new Error("CloudService instance already created")
+		}
+
+		this._instance = new CloudService(context, log)
+
+		await this._instance.initialize()
+
+		if (eventHandlers) {
+			for (const [event, handler] of Object.entries(eventHandlers)) {
+				if (handler) {
+					this._instance.on(
+						event as keyof CloudServiceEvents,
+						handler as (...args: CloudServiceEvents[keyof CloudServiceEvents]) => void,
+					)
+				}
+			}
+		}
+
+		await this._instance.authService?.broadcast()
+
+		return this._instance
+	}
+
+	static hasInstance(): boolean {
+		return this._instance !== null && this._instance.isInitialized
+	}
+
+	static resetInstance(): void {
+		if (this._instance) {
+			this._instance.dispose()
+			this._instance = null
+		}
+	}
+
+	static isEnabled(): boolean {
+		return !!this._instance?.isAuthenticated()
+	}
+}

+ 282 - 0
packages/cloud/src/CloudSettingsService.ts

@@ -0,0 +1,282 @@
+import EventEmitter from "events"
+
+import type { ExtensionContext } from "vscode"
+
+import { z } from "zod"
+
+import {
+	type SettingsService,
+	type SettingsServiceEvents,
+	type AuthService,
+	type AuthState,
+	type UserFeatures,
+	type UserSettingsConfig,
+	type UserSettingsData,
+	OrganizationAllowList,
+	OrganizationSettings,
+	organizationSettingsSchema,
+	userSettingsDataSchema,
+	ORGANIZATION_ALLOW_ALL,
+} from "@roo-code/types"
+
+import { getRooCodeApiUrl } from "./config.js"
+import { RefreshTimer } from "./RefreshTimer.js"
+
+const ORGANIZATION_SETTINGS_CACHE_KEY = "organization-settings"
+const USER_SETTINGS_CACHE_KEY = "user-settings"
+
+const parseExtensionSettingsResponse = (data: unknown) => {
+	const shapeResult = z.object({ organization: z.unknown(), user: z.unknown() }).safeParse(data)
+
+	if (!shapeResult.success) {
+		return { success: false, error: shapeResult.error } as const
+	}
+
+	const orgResult = organizationSettingsSchema.safeParse(shapeResult.data.organization)
+
+	if (!orgResult.success) {
+		return { success: false, error: orgResult.error } as const
+	}
+
+	const userResult = userSettingsDataSchema.safeParse(shapeResult.data.user)
+
+	if (!userResult.success) {
+		return { success: false, error: userResult.error } as const
+	}
+
+	return {
+		success: true,
+		data: { organization: orgResult.data, user: userResult.data },
+	} as const
+}
+
+export class CloudSettingsService extends EventEmitter<SettingsServiceEvents> implements SettingsService {
+	private context: ExtensionContext
+	private authService: AuthService
+	private settings: OrganizationSettings | undefined = undefined
+	private userSettings: UserSettingsData | undefined = undefined
+	private timer: RefreshTimer
+	private log: (...args: unknown[]) => void
+
+	constructor(context: ExtensionContext, authService: AuthService, log?: (...args: unknown[]) => void) {
+		super()
+
+		this.context = context
+		this.authService = authService
+		this.log = log || console.log
+
+		this.timer = new RefreshTimer({
+			callback: async () => {
+				return await this.fetchSettings()
+			},
+			successInterval: 30000,
+			initialBackoffMs: 1000,
+			maxBackoffMs: 30000,
+		})
+	}
+
+	public async initialize(): Promise<void> {
+		this.loadCachedSettings()
+
+		// Clear cached settings if we have missed a log out.
+		if (this.authService.getState() == "logged-out" && (this.settings || this.userSettings)) {
+			await this.removeSettings()
+		}
+
+		this.authService.on("auth-state-changed", async (data: { state: AuthState; previousState: AuthState }) => {
+			try {
+				if (data.state === "active-session") {
+					this.timer.start()
+				} else if (data.previousState === "active-session") {
+					this.timer.stop()
+
+					if (data.state === "logged-out") {
+						await this.removeSettings()
+					}
+				}
+			} catch (error) {
+				this.log(`[cloud-settings] error processing auth-state-changed: ${error}`, error)
+			}
+		})
+
+		if (this.authService.hasActiveSession()) {
+			this.timer.start()
+		}
+	}
+
+	private async fetchSettings(): Promise<boolean> {
+		const token = this.authService.getSessionToken()
+
+		if (!token) {
+			return false
+		}
+
+		try {
+			const response = await fetch(`${getRooCodeApiUrl()}/api/extension-settings`, {
+				headers: {
+					Authorization: `Bearer ${token}`,
+				},
+			})
+
+			if (!response.ok) {
+				this.log("[cloud-settings] Failed to fetch extension settings:", response.status, response.statusText)
+				return false
+			}
+
+			const data = await response.json()
+			const result = parseExtensionSettingsResponse(data)
+
+			if (!result.success) {
+				this.log("[cloud-settings] Invalid extension settings format:", result.error)
+				return false
+			}
+
+			const { organization: newOrgSettings, user: newUserSettings } = result.data
+
+			let orgChanged = false
+			let userChanged = false
+
+			// Check for organization settings changes
+			if (!this.settings || this.settings.version !== newOrgSettings.version) {
+				this.settings = newOrgSettings
+				orgChanged = true
+			}
+
+			// Check for user settings changes
+			if (!this.userSettings || this.userSettings.version !== newUserSettings.version) {
+				this.userSettings = newUserSettings
+				userChanged = true
+			}
+
+			// Emit a single event if either settings changed
+			if (orgChanged || userChanged) {
+				this.emit("settings-updated", {} as Record<string, never>)
+			}
+
+			const hasChanges = orgChanged || userChanged
+
+			if (hasChanges) {
+				await this.cacheSettings()
+			}
+
+			return true
+		} catch (error) {
+			this.log("[cloud-settings] Error fetching extension settings:", error)
+			return false
+		}
+	}
+
+	private async cacheSettings(): Promise<void> {
+		// Store settings in separate globalState values
+		if (this.settings) {
+			await this.context.globalState.update(ORGANIZATION_SETTINGS_CACHE_KEY, this.settings)
+		}
+
+		if (this.userSettings) {
+			await this.context.globalState.update(USER_SETTINGS_CACHE_KEY, this.userSettings)
+		}
+	}
+
+	private loadCachedSettings(): void {
+		// Load settings from separate globalState values
+		this.settings = this.context.globalState.get<OrganizationSettings>(ORGANIZATION_SETTINGS_CACHE_KEY)
+		this.userSettings = this.context.globalState.get<UserSettingsData>(USER_SETTINGS_CACHE_KEY)
+	}
+
+	public getAllowList(): OrganizationAllowList {
+		return this.settings?.allowList || ORGANIZATION_ALLOW_ALL
+	}
+
+	public getSettings(): OrganizationSettings | undefined {
+		return this.settings
+	}
+
+	public getUserSettings(): UserSettingsData | undefined {
+		return this.userSettings
+	}
+
+	public getUserFeatures(): UserFeatures {
+		return this.userSettings?.features || {}
+	}
+
+	public getUserSettingsConfig(): UserSettingsConfig {
+		return this.userSettings?.settings || {}
+	}
+
+	public async updateUserSettings(settings: Partial<UserSettingsConfig>): Promise<boolean> {
+		const token = this.authService.getSessionToken()
+
+		if (!token) {
+			this.log("[cloud-settings] No session token available for updating user settings")
+			return false
+		}
+
+		try {
+			const currentVersion = this.userSettings?.version
+			const requestBody: {
+				settings: Partial<UserSettingsConfig>
+				version?: number
+			} = {
+				settings,
+			}
+
+			// Include current version for optimistic locking if we have cached settings
+			if (currentVersion !== undefined) {
+				requestBody.version = currentVersion
+			}
+
+			const response = await fetch(`${getRooCodeApiUrl()}/api/user-settings`, {
+				method: "PATCH",
+				headers: {
+					"Content-Type": "application/json",
+					Authorization: `Bearer ${token}`,
+				},
+				body: JSON.stringify(requestBody),
+			})
+
+			if (!response.ok) {
+				if (response.status === 409) {
+					this.log(
+						"[cloud-settings] Version conflict when updating user settings - settings may have been updated elsewhere",
+					)
+				} else {
+					this.log("[cloud-settings] Failed to update user settings:", response.status, response.statusText)
+				}
+				return false
+			}
+
+			const updatedUserSettings = await response.json()
+			const result = userSettingsDataSchema.safeParse(updatedUserSettings)
+
+			if (!result.success) {
+				this.log("[cloud-settings] Invalid user settings response format:", result.error)
+				return false
+			}
+
+			if (!this.userSettings || result.data.version > this.userSettings.version) {
+				this.userSettings = result.data
+				await this.cacheSettings()
+				this.emit("settings-updated", {} as Record<string, never>)
+			}
+
+			return true
+		} catch (error) {
+			this.log("[cloud-settings] Error updating user settings:", error)
+			return false
+		}
+	}
+
+	private async removeSettings(): Promise<void> {
+		this.settings = undefined
+		this.userSettings = undefined
+
+		// Clear both cache keys
+		await this.context.globalState.update(ORGANIZATION_SETTINGS_CACHE_KEY, undefined)
+		await this.context.globalState.update(USER_SETTINGS_CACHE_KEY, undefined)
+	}
+
+	public dispose(): void {
+		this.removeAllListeners()
+		this.timer.stop()
+	}
+}

+ 50 - 0
packages/cloud/src/CloudShareService.ts

@@ -0,0 +1,50 @@
+import type { SettingsService, ShareResponse, ShareVisibility } from "@roo-code/types"
+
+import { importVscode } from "./importVscode.js"
+import type { CloudAPI } from "./CloudAPI.js"
+
+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) {
+				const vscode = await importVscode()
+
+				if (vscode?.env?.clipboard?.writeText) {
+					try {
+						await vscode.env.clipboard.writeText(response.shareUrl)
+					} catch (copyErr) {
+						this.log("[ShareService] Clipboard write failed (non-fatal):", copyErr)
+					}
+				} else {
+					this.log("[ShareService] VS Code clipboard unavailable; running outside extension host.")
+				}
+			}
+
+			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
+		}
+	}
+}

+ 154 - 0
packages/cloud/src/RefreshTimer.ts

@@ -0,0 +1,154 @@
+/**
+ * RefreshTimer - A utility for executing a callback with configurable retry behavior
+ *
+ * This timer executes a callback function and schedules the next execution based on the result:
+ * - If the callback succeeds (returns true), it schedules the next attempt after a fixed interval
+ * - If the callback fails (returns false), it uses exponential backoff up to a maximum interval
+ */
+
+/**
+ * Configuration options for the RefreshTimer
+ */
+export interface RefreshTimerOptions {
+	/**
+	 * The callback function to execute
+	 * Should return a Promise that resolves to a boolean indicating success (true) or failure (false)
+	 */
+	callback: () => Promise<boolean>
+
+	/**
+	 * Time in milliseconds to wait before next attempt after success
+	 * @default 50000 (50 seconds)
+	 */
+	successInterval?: number
+
+	/**
+	 * Initial backoff time in milliseconds for the first failure
+	 * @default 1000 (1 second)
+	 */
+	initialBackoffMs?: number
+
+	/**
+	 * Maximum backoff time in milliseconds
+	 * @default 300000 (5 minutes)
+	 */
+	maxBackoffMs?: number
+}
+
+/**
+ * A timer utility that executes a callback with configurable retry behavior
+ */
+export class RefreshTimer {
+	private callback: () => Promise<boolean>
+	private successInterval: number
+	private initialBackoffMs: number
+	private maxBackoffMs: number
+	private currentBackoffMs: number
+	private attemptCount: number
+	private timerId: NodeJS.Timeout | null
+	private isRunning: boolean
+
+	/**
+	 * Creates a new RefreshTimer
+	 *
+	 * @param options Configuration options for the timer
+	 */
+	constructor(options: RefreshTimerOptions) {
+		this.callback = options.callback
+		this.successInterval = options.successInterval ?? 50000 // 50 seconds
+		this.initialBackoffMs = options.initialBackoffMs ?? 1000 // 1 second
+		this.maxBackoffMs = options.maxBackoffMs ?? 300000 // 5 minutes
+		this.currentBackoffMs = this.initialBackoffMs
+		this.attemptCount = 0
+		this.timerId = null
+		this.isRunning = false
+	}
+
+	/**
+	 * Starts the timer and executes the callback immediately
+	 */
+	public start(): void {
+		if (this.isRunning) {
+			return
+		}
+
+		this.isRunning = true
+
+		// Execute the callback immediately
+		this.executeCallback()
+	}
+
+	/**
+	 * Stops the timer and cancels any pending execution
+	 */
+	public stop(): void {
+		if (!this.isRunning) {
+			return
+		}
+
+		if (this.timerId) {
+			clearTimeout(this.timerId)
+			this.timerId = null
+		}
+
+		this.isRunning = false
+	}
+
+	/**
+	 * Resets the backoff state and attempt count
+	 * Does not affect whether the timer is running
+	 */
+	public reset(): void {
+		this.currentBackoffMs = this.initialBackoffMs
+		this.attemptCount = 0
+	}
+
+	/**
+	 * Schedules the next attempt based on the success/failure of the current attempt
+	 *
+	 * @param wasSuccessful Whether the current attempt was successful
+	 */
+	private scheduleNextAttempt(wasSuccessful: boolean): void {
+		if (!this.isRunning) {
+			return
+		}
+
+		if (wasSuccessful) {
+			// Reset backoff on success
+			this.currentBackoffMs = this.initialBackoffMs
+			this.attemptCount = 0
+
+			this.timerId = setTimeout(() => this.executeCallback(), this.successInterval)
+		} else {
+			// Increment attempt count
+			this.attemptCount++
+
+			// Calculate backoff time with exponential increase
+			// Formula: initialBackoff * 2^(attemptCount - 1)
+			this.currentBackoffMs = Math.min(
+				this.initialBackoffMs * Math.pow(2, this.attemptCount - 1),
+				this.maxBackoffMs,
+			)
+
+			this.timerId = setTimeout(() => this.executeCallback(), this.currentBackoffMs)
+		}
+	}
+
+	/**
+	 * Executes the callback and handles the result
+	 */
+	private async executeCallback(): Promise<void> {
+		if (!this.isRunning) {
+			return
+		}
+
+		try {
+			const result = await this.callback()
+
+			this.scheduleNextAttempt(result)
+		} catch (_error) {
+			// Treat errors as failed attempts
+			this.scheduleNextAttempt(false)
+		}
+	}
+}

+ 78 - 0
packages/cloud/src/StaticSettingsService.ts

@@ -0,0 +1,78 @@
+import {
+	type SettingsService,
+	type UserFeatures,
+	type UserSettingsConfig,
+	type UserSettingsData,
+	OrganizationAllowList,
+	OrganizationSettings,
+	organizationSettingsSchema,
+	ORGANIZATION_ALLOW_ALL,
+} from "@roo-code/types"
+
+export class StaticSettingsService implements SettingsService {
+	private settings: OrganizationSettings
+	private log: (...args: unknown[]) => void
+
+	constructor(envValue: string, log?: (...args: unknown[]) => void) {
+		this.log = log || console.log
+		this.settings = this.parseEnvironmentSettings(envValue)
+	}
+
+	private parseEnvironmentSettings(envValue: string): OrganizationSettings {
+		try {
+			const decodedValue = Buffer.from(envValue, "base64").toString("utf-8")
+			const parsedJson = JSON.parse(decodedValue)
+			return organizationSettingsSchema.parse(parsedJson)
+		} catch (error) {
+			this.log(
+				`[StaticSettingsService] failed to parse static settings: ${error instanceof Error ? error.message : String(error)}`,
+				error,
+			)
+
+			throw new Error("Failed to parse static settings", { cause: error })
+		}
+	}
+
+	public getAllowList(): OrganizationAllowList {
+		return this.settings?.allowList || ORGANIZATION_ALLOW_ALL
+	}
+
+	public getSettings(): OrganizationSettings | undefined {
+		return this.settings
+	}
+
+	/**
+	 * Returns static user settings with roomoteControlEnabled and extensionBridgeEnabled as true
+	 */
+	public getUserSettings(): UserSettingsData | undefined {
+		return {
+			features: {
+				roomoteControlEnabled: true,
+			},
+			settings: {
+				extensionBridgeEnabled: true,
+			},
+			version: 1,
+		}
+	}
+
+	public getUserFeatures(): UserFeatures {
+		return {
+			roomoteControlEnabled: true,
+		}
+	}
+
+	public getUserSettingsConfig(): UserSettingsConfig {
+		return {
+			extensionBridgeEnabled: true,
+		}
+	}
+
+	public async updateUserSettings(_settings: Partial<UserSettingsConfig>): Promise<boolean> {
+		throw new Error("User settings updates are not supported in static mode")
+	}
+
+	public dispose(): void {
+		// No resources to clean up for static settings.
+	}
+}

+ 93 - 0
packages/cloud/src/StaticTokenAuthService.ts

@@ -0,0 +1,93 @@
+import EventEmitter from "events"
+
+import { jwtDecode } from "jwt-decode"
+import type { ExtensionContext } from "vscode"
+
+import type { JWTPayload, CloudUserInfo, AuthService, AuthServiceEvents, AuthState } from "@roo-code/types"
+
+export class StaticTokenAuthService extends EventEmitter<AuthServiceEvents> implements AuthService {
+	private state: AuthState = "active-session"
+	private token: string
+	private log: (...args: unknown[]) => void
+	private userInfo: CloudUserInfo
+
+	constructor(context: ExtensionContext, token: string, log?: (...args: unknown[]) => void) {
+		super()
+
+		this.token = token
+		this.log = log || console.log
+
+		this.log("[auth] Using StaticTokenAuthService")
+
+		let payload
+
+		try {
+			payload = jwtDecode<JWTPayload>(token)
+		} catch (error) {
+			this.log("[auth] Failed to parse JWT:", error)
+		}
+
+		this.userInfo = {
+			id: payload?.r?.u || payload?.sub || undefined,
+			organizationId: payload?.r?.o || undefined,
+			extensionBridgeEnabled: true,
+		}
+	}
+
+	public async initialize(): Promise<void> {
+		this.state = "active-session"
+	}
+
+	public broadcast(): void {
+		this.emit("auth-state-changed", {
+			state: this.state,
+			previousState: "initializing",
+		})
+
+		this.emit("user-info", { userInfo: this.userInfo })
+	}
+
+	public async login(): Promise<void> {
+		throw new Error("Authentication methods are disabled in StaticTokenAuthService")
+	}
+
+	public async logout(): Promise<void> {
+		throw new Error("Authentication methods are disabled in StaticTokenAuthService")
+	}
+
+	public async handleCallback(
+		_code: string | null,
+		_state: string | null,
+		_organizationId?: string | null,
+	): Promise<void> {
+		throw new Error("Authentication methods are disabled in StaticTokenAuthService")
+	}
+
+	public getState(): AuthState {
+		return this.state
+	}
+
+	public getSessionToken(): string | undefined {
+		return this.token
+	}
+
+	public isAuthenticated(): boolean {
+		return true
+	}
+
+	public hasActiveSession(): boolean {
+		return true
+	}
+
+	public hasOrIsAcquiringActiveSession(): boolean {
+		return true
+	}
+
+	public getUserInfo(): CloudUserInfo | null {
+		return this.userInfo
+	}
+
+	public getStoredOrganizationId(): string | null {
+		return this.userInfo?.organizationId || null
+	}
+}

+ 246 - 0
packages/cloud/src/TelemetryClient.ts

@@ -0,0 +1,246 @@
+import {
+	type TelemetryClient,
+	type TelemetryEvent,
+	type ClineMessage,
+	type AuthService,
+	type SettingsService,
+	TelemetryEventName,
+	rooCodeTelemetryEventSchema,
+	TelemetryPropertiesProvider,
+	TelemetryEventSubscription,
+} from "@roo-code/types"
+
+import { getRooCodeApiUrl } from "./config.js"
+
+abstract class BaseTelemetryClient implements TelemetryClient {
+	protected providerRef: WeakRef<TelemetryPropertiesProvider> | null = null
+	protected telemetryEnabled: boolean = false
+
+	constructor(
+		public readonly subscription?: TelemetryEventSubscription,
+		protected readonly debug = false,
+	) {}
+
+	protected isEventCapturable(eventName: TelemetryEventName): boolean {
+		if (!this.subscription) {
+			return true
+		}
+
+		return this.subscription.type === "include"
+			? this.subscription.events.includes(eventName)
+			: !this.subscription.events.includes(eventName)
+	}
+
+	/**
+	 * Determines if a specific property should be included in telemetry events
+	 * Override in subclasses to filter specific properties
+	 */
+	protected isPropertyCapturable(_propertyName: string): boolean {
+		return true
+	}
+
+	protected async getEventProperties(event: TelemetryEvent): Promise<TelemetryEvent["properties"]> {
+		let providerProperties: TelemetryEvent["properties"] = {}
+		const provider = this.providerRef?.deref()
+
+		if (provider) {
+			try {
+				// Get properties from the provider
+				providerProperties = await provider.getTelemetryProperties()
+			} catch (error) {
+				// Log error but continue with capturing the event.
+				console.error(
+					`Error getting telemetry properties: ${error instanceof Error ? error.message : String(error)}`,
+				)
+			}
+		}
+
+		// Merge provider properties with event-specific properties.
+		// Event properties take precedence in case of conflicts.
+		const mergedProperties = {
+			...providerProperties,
+			...(event.properties || {}),
+		}
+
+		// Filter out properties that shouldn't be captured by this client
+		return Object.fromEntries(Object.entries(mergedProperties).filter(([key]) => this.isPropertyCapturable(key)))
+	}
+
+	public abstract capture(event: TelemetryEvent): Promise<void>
+
+	public setProvider(provider: TelemetryPropertiesProvider): void {
+		this.providerRef = new WeakRef(provider)
+	}
+
+	public abstract updateTelemetryState(didUserOptIn: boolean): void
+
+	public isTelemetryEnabled(): boolean {
+		return this.telemetryEnabled
+	}
+
+	public abstract shutdown(): Promise<void>
+}
+
+export class CloudTelemetryClient extends BaseTelemetryClient {
+	constructor(
+		private authService: AuthService,
+		private settingsService: SettingsService,
+		debug = false,
+	) {
+		super(
+			{
+				type: "exclude",
+				events: [TelemetryEventName.TASK_CONVERSATION_MESSAGE],
+			},
+			debug,
+		)
+	}
+
+	private async fetch(path: string, options: RequestInit) {
+		if (!this.authService.isAuthenticated()) {
+			return
+		}
+
+		const token = this.authService.getSessionToken()
+
+		if (!token) {
+			console.error(`[TelemetryClient#fetch] Unauthorized: No session token available.`)
+			return
+		}
+
+		const response = await fetch(`${getRooCodeApiUrl()}/api/${path}`, {
+			...options,
+			headers: {
+				Authorization: `Bearer ${token}`,
+				"Content-Type": "application/json",
+			},
+		})
+
+		if (!response.ok) {
+			console.error(
+				`[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`,
+			)
+		}
+	}
+
+	public override async capture(event: TelemetryEvent) {
+		if (!this.isTelemetryEnabled() || !this.isEventCapturable(event.event)) {
+			if (this.debug) {
+				console.info(`[TelemetryClient#capture] Skipping event: ${event.event}`)
+			}
+
+			return
+		}
+
+		const payload = {
+			type: event.event,
+			properties: await this.getEventProperties(event),
+		}
+
+		if (this.debug) {
+			console.info(`[TelemetryClient#capture] ${JSON.stringify(payload)}`)
+		}
+
+		const result = rooCodeTelemetryEventSchema.safeParse(payload)
+
+		if (!result.success) {
+			console.error(
+				`[TelemetryClient#capture] Invalid telemetry event: ${result.error.message} - ${JSON.stringify(payload)}`,
+			)
+
+			return
+		}
+
+		try {
+			await this.fetch(`events`, {
+				method: "POST",
+				body: JSON.stringify(result.data),
+			})
+		} catch (error) {
+			console.error(`[TelemetryClient#capture] Error sending telemetry event: ${error}`)
+		}
+	}
+
+	public async backfillMessages(messages: ClineMessage[], taskId: string): Promise<void> {
+		if (!this.authService.isAuthenticated()) {
+			if (this.debug) {
+				console.info(`[TelemetryClient#backfillMessages] Skipping: Not authenticated`)
+			}
+			return
+		}
+
+		const token = this.authService.getSessionToken()
+
+		if (!token) {
+			console.error(`[TelemetryClient#backfillMessages] Unauthorized: No session token available.`)
+			return
+		}
+
+		try {
+			const mergedProperties = await this.getEventProperties({
+				event: TelemetryEventName.TASK_MESSAGE,
+				properties: { taskId },
+			})
+
+			const formData = new FormData()
+			formData.append("taskId", taskId)
+			formData.append("properties", JSON.stringify(mergedProperties))
+
+			formData.append(
+				"file",
+				new File([JSON.stringify(messages)], "task.json", {
+					type: "application/json",
+				}),
+			)
+
+			if (this.debug) {
+				console.info(
+					`[TelemetryClient#backfillMessages] Uploading ${messages.length} messages for task ${taskId}`,
+				)
+			}
+
+			// Custom fetch for multipart - don't set Content-Type header (let browser set it)
+			const response = await fetch(`${getRooCodeApiUrl()}/api/events/backfill`, {
+				method: "POST",
+				headers: {
+					Authorization: `Bearer ${token}`,
+					// Note: No Content-Type header - browser will set multipart/form-data with boundary
+				},
+				body: formData,
+			})
+
+			if (!response.ok) {
+				console.error(
+					`[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`,
+				)
+			} else if (this.debug) {
+				console.info(`[TelemetryClient#backfillMessages] Successfully uploaded messages for task ${taskId}`)
+			}
+		} catch (error) {
+			console.error(`[TelemetryClient#backfillMessages] Error uploading messages: ${error}`)
+		}
+	}
+
+	public override updateTelemetryState(_didUserOptIn: boolean) {}
+
+	public override isTelemetryEnabled(): boolean {
+		return true
+	}
+
+	protected override isEventCapturable(eventName: TelemetryEventName): boolean {
+		// Ensure that this event type is supported by the telemetry client
+		if (!super.isEventCapturable(eventName)) {
+			return false
+		}
+
+		// Only record message telemetry if a cloud account is present and explicitly configured to record messages
+		if (eventName === TelemetryEventName.TASK_MESSAGE) {
+			return this.settingsService.getSettings()?.cloudSettings?.recordTaskMessages || false
+		}
+
+		// Other telemetry types are capturable at this point
+		return true
+	}
+
+	public override async shutdown() {}
+}

+ 727 - 0
packages/cloud/src/WebAuthService.ts

@@ -0,0 +1,727 @@
+import crypto from "crypto"
+import EventEmitter from "events"
+
+import type { ExtensionContext } from "vscode"
+import { z } from "zod"
+
+import type {
+	CloudUserInfo,
+	CloudOrganizationMembership,
+	AuthService,
+	AuthServiceEvents,
+	AuthState,
+} from "@roo-code/types"
+
+import { getClerkBaseUrl, getRooCodeApiUrl, PRODUCTION_CLERK_BASE_URL } from "./config.js"
+import { getUserAgent } from "./utils.js"
+import { importVscode } from "./importVscode.js"
+import { InvalidClientTokenError } from "./errors.js"
+import { RefreshTimer } from "./RefreshTimer.js"
+
+const AUTH_STATE_KEY = "clerk-auth-state"
+
+/**
+ * AuthCredentials
+ */
+
+const authCredentialsSchema = z.object({
+	clientToken: z.string().min(1, "Client token cannot be empty"),
+	sessionId: z.string().min(1, "Session ID cannot be empty"),
+	organizationId: z.string().nullable().optional(),
+})
+
+type AuthCredentials = z.infer<typeof authCredentialsSchema>
+
+/**
+ * Clerk Schemas
+ */
+
+const clerkSignInResponseSchema = z.object({
+	response: z.object({
+		created_session_id: z.string(),
+	}),
+})
+
+const clerkCreateSessionTokenResponseSchema = z.object({
+	jwt: z.string(),
+})
+
+const clerkMeResponseSchema = z.object({
+	response: z.object({
+		id: z.string().optional(),
+		first_name: z.string().nullish(),
+		last_name: z.string().nullish(),
+		image_url: z.string().optional(),
+		primary_email_address_id: z.string().optional(),
+		email_addresses: z
+			.array(
+				z.object({
+					id: z.string(),
+					email_address: z.string(),
+				}),
+			)
+			.optional(),
+		public_metadata: z.record(z.any()).optional(),
+	}),
+})
+
+const clerkOrganizationMembershipsSchema = z.object({
+	response: z.array(
+		z.object({
+			id: z.string(),
+			role: z.string(),
+			permissions: z.array(z.string()).optional(),
+			created_at: z.number().optional(),
+			updated_at: z.number().optional(),
+			organization: z.object({
+				id: z.string(),
+				name: z.string(),
+				slug: z.string().optional(),
+				image_url: z.string().optional(),
+				has_image: z.boolean().optional(),
+				created_at: z.number().optional(),
+				updated_at: z.number().optional(),
+			}),
+		}),
+	),
+})
+
+export class WebAuthService extends EventEmitter<AuthServiceEvents> implements AuthService {
+	private context: ExtensionContext
+	private timer: RefreshTimer
+	private state: AuthState = "initializing"
+	private log: (...args: unknown[]) => void
+	private readonly authCredentialsKey: string
+
+	private credentials: AuthCredentials | null = null
+	private sessionToken: string | null = null
+	private userInfo: CloudUserInfo | null = null
+	private isFirstRefreshAttempt: boolean = false
+
+	constructor(context: ExtensionContext, log?: (...args: unknown[]) => void) {
+		super()
+
+		this.context = context
+		this.log = log || console.log
+
+		this.log("[auth] Using WebAuthService")
+
+		// Calculate auth credentials key based on Clerk base URL.
+		const clerkBaseUrl = getClerkBaseUrl()
+
+		if (clerkBaseUrl !== PRODUCTION_CLERK_BASE_URL) {
+			this.authCredentialsKey = `clerk-auth-credentials-${clerkBaseUrl}`
+		} else {
+			this.authCredentialsKey = "clerk-auth-credentials"
+		}
+
+		this.timer = new RefreshTimer({
+			callback: async () => {
+				await this.refreshSession()
+				return true
+			},
+			successInterval: 50_000,
+			initialBackoffMs: 1_000,
+			maxBackoffMs: 300_000,
+		})
+	}
+
+	private changeState(newState: AuthState): void {
+		const previousState = this.state
+		this.state = newState
+		this.emit("auth-state-changed", { state: newState, previousState })
+	}
+
+	private async handleCredentialsChange(): Promise<void> {
+		try {
+			const credentials = await this.loadCredentials()
+
+			if (credentials) {
+				if (
+					this.credentials === null ||
+					this.credentials.clientToken !== credentials.clientToken ||
+					this.credentials.sessionId !== credentials.sessionId
+				) {
+					this.transitionToAttemptingSession(credentials)
+				}
+			} else {
+				if (this.state !== "logged-out") {
+					this.transitionToLoggedOut()
+				}
+			}
+		} catch (error) {
+			this.log("[auth] Error handling credentials change:", error)
+		}
+	}
+
+	private transitionToLoggedOut(): void {
+		this.timer.stop()
+
+		this.credentials = null
+		this.sessionToken = null
+		this.userInfo = null
+
+		this.changeState("logged-out")
+
+		this.log("[auth] Transitioned to logged-out state")
+	}
+
+	private transitionToAttemptingSession(credentials: AuthCredentials): void {
+		this.credentials = credentials
+
+		this.sessionToken = null
+		this.userInfo = null
+		this.isFirstRefreshAttempt = true
+
+		this.changeState("attempting-session")
+
+		this.timer.start()
+
+		this.log("[auth] Transitioned to attempting-session state")
+	}
+
+	private transitionToInactiveSession(): void {
+		this.sessionToken = null
+		this.userInfo = null
+
+		this.changeState("inactive-session")
+
+		this.log("[auth] Transitioned to inactive-session state")
+	}
+
+	/**
+	 * Initialize the auth state
+	 *
+	 * This method loads tokens from storage and determines the current auth state.
+	 * It also starts the refresh timer if we have an active session.
+	 */
+	public async initialize(): Promise<void> {
+		if (this.state !== "initializing") {
+			this.log("[auth] initialize() called after already initialized")
+			return
+		}
+
+		await this.handleCredentialsChange()
+
+		this.context.subscriptions.push(
+			this.context.secrets.onDidChange((e) => {
+				if (e.key === this.authCredentialsKey) {
+					this.handleCredentialsChange()
+				}
+			}),
+		)
+	}
+
+	public broadcast(): void {}
+
+	private async storeCredentials(credentials: AuthCredentials): Promise<void> {
+		await this.context.secrets.store(this.authCredentialsKey, JSON.stringify(credentials))
+	}
+
+	private async loadCredentials(): Promise<AuthCredentials | null> {
+		const credentialsJson = await this.context.secrets.get(this.authCredentialsKey)
+		if (!credentialsJson) return null
+
+		try {
+			const parsedJson = JSON.parse(credentialsJson)
+			const credentials = authCredentialsSchema.parse(parsedJson)
+
+			// Migration: If no organizationId but we have userInfo, add it
+			if (credentials.organizationId === undefined && this.userInfo?.organizationId) {
+				credentials.organizationId = this.userInfo.organizationId
+				await this.storeCredentials(credentials)
+				this.log("[auth] Migrated credentials with organizationId")
+			}
+
+			return credentials
+		} catch (error) {
+			if (error instanceof z.ZodError) {
+				this.log("[auth] Invalid credentials format:", error.errors)
+			} else {
+				this.log("[auth] Failed to parse stored credentials:", error)
+			}
+			return null
+		}
+	}
+
+	private async clearCredentials(): Promise<void> {
+		await this.context.secrets.delete(this.authCredentialsKey)
+	}
+
+	/**
+	 * Start the login process
+	 *
+	 * This method initiates the authentication flow by generating a state parameter
+	 * and opening the browser to the authorization URL.
+	 */
+	public async login(): Promise<void> {
+		try {
+			const vscode = await importVscode()
+
+			if (!vscode) {
+				throw new Error("VS Code API not available")
+			}
+
+			// Generate a cryptographically random state parameter.
+			const state = crypto.randomBytes(16).toString("hex")
+			await this.context.globalState.update(AUTH_STATE_KEY, state)
+			const packageJSON = this.context.extension?.packageJSON
+			const publisher = packageJSON?.publisher ?? "RooVeterinaryInc"
+			const name = packageJSON?.name ?? "roo-cline"
+			const params = new URLSearchParams({
+				state,
+				auth_redirect: `${vscode.env.uriScheme}://${publisher}.${name}`,
+			})
+			const url = `${getRooCodeApiUrl()}/extension/sign-in?${params.toString()}`
+			await vscode.env.openExternal(vscode.Uri.parse(url))
+		} catch (error) {
+			this.log(`[auth] Error initiating Roo Code Cloud auth: ${error}`)
+			throw new Error(`Failed to initiate Roo Code Cloud authentication: ${error}`)
+		}
+	}
+
+	/**
+	 * Handle the callback from Roo Code Cloud
+	 *
+	 * This method is called when the user is redirected back to the extension
+	 * after authenticating with Roo Code Cloud.
+	 *
+	 * @param code The authorization code from the callback
+	 * @param state The state parameter from the callback
+	 * @param organizationId The organization ID from the callback (null for personal accounts)
+	 */
+	public async handleCallback(
+		code: string | null,
+		state: string | null,
+		organizationId?: string | null,
+	): Promise<void> {
+		if (!code || !state) {
+			const vscode = await importVscode()
+
+			if (vscode) {
+				vscode.window.showInformationMessage("Invalid Roo Code Cloud sign in url")
+			}
+
+			return
+		}
+
+		try {
+			// Validate state parameter to prevent CSRF attacks.
+			const storedState = this.context.globalState.get(AUTH_STATE_KEY)
+
+			if (state !== storedState) {
+				this.log("[auth] State mismatch in callback")
+				throw new Error("Invalid state parameter. Authentication request may have been tampered with.")
+			}
+
+			const credentials = await this.clerkSignIn(code)
+
+			// Set organizationId (null for personal accounts)
+			credentials.organizationId = organizationId || null
+
+			await this.storeCredentials(credentials)
+
+			const vscode = await importVscode()
+
+			if (vscode) {
+				vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud")
+			}
+
+			this.log("[auth] Successfully authenticated with Roo Code Cloud")
+		} catch (error) {
+			this.log(`[auth] Error handling Roo Code Cloud callback: ${error}`)
+			this.changeState("logged-out")
+			throw new Error(`Failed to handle Roo Code Cloud callback: ${error}`)
+		}
+	}
+
+	/**
+	 * Log out
+	 *
+	 * This method removes all stored tokens and stops the refresh timer.
+	 */
+	public async logout(): Promise<void> {
+		const oldCredentials = this.credentials
+
+		try {
+			// Clear credentials from storage - onDidChange will handle state transitions
+			await this.clearCredentials()
+			await this.context.globalState.update(AUTH_STATE_KEY, undefined)
+
+			if (oldCredentials) {
+				try {
+					await this.clerkLogout(oldCredentials)
+				} catch (error) {
+					this.log("[auth] Error calling clerkLogout:", error)
+				}
+			}
+
+			const vscode = await importVscode()
+
+			if (vscode) {
+				vscode.window.showInformationMessage("Logged out from Roo Code Cloud")
+			}
+
+			this.log("[auth] Logged out from Roo Code Cloud")
+		} catch (error) {
+			this.log(`[auth] Error logging out from Roo Code Cloud: ${error}`)
+			throw new Error(`Failed to log out from Roo Code Cloud: ${error}`)
+		}
+	}
+
+	public getState(): AuthState {
+		return this.state
+	}
+
+	public getSessionToken(): string | undefined {
+		if (this.state === "active-session" && this.sessionToken) {
+			return this.sessionToken
+		}
+
+		return
+	}
+
+	/**
+	 * Check if the user is authenticated
+	 *
+	 * @returns True if the user is authenticated (has an active, attempting, or inactive session)
+	 */
+	public isAuthenticated(): boolean {
+		return (
+			this.state === "active-session" || this.state === "attempting-session" || this.state === "inactive-session"
+		)
+	}
+
+	public hasActiveSession(): boolean {
+		return this.state === "active-session"
+	}
+
+	/**
+	 * Check if the user has an active session or is currently attempting to acquire one
+	 *
+	 * @returns True if the user has an active session or is attempting to get one
+	 */
+	public hasOrIsAcquiringActiveSession(): boolean {
+		return this.state === "active-session" || this.state === "attempting-session"
+	}
+
+	/**
+	 * Refresh the session
+	 *
+	 * This method refreshes the session token using the client token.
+	 */
+	private async refreshSession(): Promise<void> {
+		if (!this.credentials) {
+			this.log("[auth] Cannot refresh session: missing credentials")
+			return
+		}
+
+		try {
+			const previousState = this.state
+			this.sessionToken = await this.clerkCreateSessionToken()
+
+			if (previousState !== "active-session") {
+				this.changeState("active-session")
+				this.log("[auth] Transitioned to active-session state")
+				this.fetchUserInfo()
+			} else {
+				this.state = "active-session"
+			}
+		} catch (error) {
+			if (error instanceof InvalidClientTokenError) {
+				this.log("[auth] Invalid/Expired client token: clearing credentials")
+				this.clearCredentials()
+			} else if (this.isFirstRefreshAttempt && this.state === "attempting-session") {
+				this.isFirstRefreshAttempt = false
+				this.transitionToInactiveSession()
+			}
+			this.log("[auth] Failed to refresh session", error)
+			throw error
+		}
+	}
+
+	private async fetchUserInfo(): Promise<void> {
+		if (!this.credentials) {
+			return
+		}
+
+		this.userInfo = await this.clerkMe()
+		this.emit("user-info", { userInfo: this.userInfo })
+	}
+
+	/**
+	 * Extract user information from the ID token
+	 *
+	 * @returns User information from ID token claims or null if no ID token available
+	 */
+	public getUserInfo(): CloudUserInfo | null {
+		return this.userInfo
+	}
+
+	/**
+	 * Get the stored organization ID from credentials
+	 *
+	 * @returns The stored organization ID, null for personal accounts or if no credentials exist
+	 */
+	public getStoredOrganizationId(): string | null {
+		return this.credentials?.organizationId || null
+	}
+
+	private async clerkSignIn(ticket: string): Promise<AuthCredentials> {
+		const formData = new URLSearchParams()
+		formData.append("strategy", "ticket")
+		formData.append("ticket", ticket)
+
+		const response = await fetch(`${getClerkBaseUrl()}/v1/client/sign_ins`, {
+			method: "POST",
+			headers: {
+				"Content-Type": "application/x-www-form-urlencoded",
+				"User-Agent": this.userAgent(),
+			},
+			body: formData.toString(),
+			signal: AbortSignal.timeout(10000),
+		})
+
+		if (!response.ok) {
+			throw new Error(`HTTP ${response.status}: ${response.statusText}`)
+		}
+
+		const {
+			response: { created_session_id: sessionId },
+		} = clerkSignInResponseSchema.parse(await response.json())
+
+		// 3. Extract the client token from the Authorization header.
+		const clientToken = response.headers.get("authorization")
+
+		if (!clientToken) {
+			throw new Error("No authorization header found in the response")
+		}
+
+		return authCredentialsSchema.parse({ clientToken, sessionId })
+	}
+
+	private async clerkCreateSessionToken(): Promise<string> {
+		const formData = new URLSearchParams()
+		formData.append("_is_native", "1")
+
+		// Handle 3 cases for organization_id:
+		// 1. Have an org id: organization_id=THE_ORG_ID
+		// 2. Have a personal account: organization_id= (empty string)
+		// 3. Don't know if you have an org id (old style credentials): don't send organization_id param at all
+		const organizationId = this.getStoredOrganizationId()
+		if (this.credentials?.organizationId !== undefined) {
+			// We have organization context info (either org id or personal account)
+			formData.append("organization_id", organizationId || "")
+		}
+		// If organizationId is undefined, don't send the param at all (old credentials)
+
+		const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${this.credentials!.sessionId}/tokens`, {
+			method: "POST",
+			headers: {
+				"Content-Type": "application/x-www-form-urlencoded",
+				Authorization: `Bearer ${this.credentials!.clientToken}`,
+				"User-Agent": this.userAgent(),
+			},
+			body: formData.toString(),
+			signal: AbortSignal.timeout(10000),
+		})
+
+		if (response.status === 401 || response.status === 404) {
+			throw new InvalidClientTokenError()
+		} else if (!response.ok) {
+			throw new Error(`HTTP ${response.status}: ${response.statusText}`)
+		}
+
+		const data = clerkCreateSessionTokenResponseSchema.parse(await response.json())
+
+		return data.jwt
+	}
+
+	private async clerkMe(): Promise<CloudUserInfo> {
+		const response = await fetch(`${getClerkBaseUrl()}/v1/me`, {
+			headers: {
+				Authorization: `Bearer ${this.credentials!.clientToken}`,
+				"User-Agent": this.userAgent(),
+			},
+			signal: AbortSignal.timeout(10000),
+		})
+
+		if (!response.ok) {
+			throw new Error(`HTTP ${response.status}: ${response.statusText}`)
+		}
+
+		const payload = await response.json()
+		const { response: userData } = clerkMeResponseSchema.parse(payload)
+
+		const userInfo: CloudUserInfo = {
+			id: userData.id,
+			picture: userData.image_url,
+		}
+
+		const names = [userData.first_name, userData.last_name].filter((name) => !!name)
+		userInfo.name = names.length > 0 ? names.join(" ") : undefined
+		const primaryEmailAddressId = userData.primary_email_address_id
+		const emailAddresses = userData.email_addresses
+
+		if (primaryEmailAddressId && emailAddresses) {
+			userInfo.email = emailAddresses.find(
+				(email: { id: string }) => primaryEmailAddressId === email.id,
+			)?.email_address
+		}
+
+		// Check for extension_bridge_enabled in user's public metadata
+		let extensionBridgeEnabled = false
+		if (userData.public_metadata?.extension_bridge_enabled === true) {
+			extensionBridgeEnabled = true
+		}
+
+		// Fetch organization info if user is in organization context
+		try {
+			const storedOrgId = this.getStoredOrganizationId()
+
+			if (this.credentials?.organizationId !== undefined) {
+				// We have organization context info
+				if (storedOrgId !== null) {
+					// User is in organization context - fetch user's memberships and filter
+					const orgMemberships = await this.clerkGetOrganizationMemberships()
+					const userMembership = this.findOrganizationMembership(orgMemberships, storedOrgId)
+
+					if (userMembership) {
+						this.setUserOrganizationInfo(userInfo, userMembership)
+
+						// Check organization public metadata for extension_bridge_enabled
+						// Organization setting takes precedence over user setting
+						if (await this.isExtensionBridgeEnabledForOrganization(storedOrgId)) {
+							extensionBridgeEnabled = true
+						}
+
+						this.log("[auth] User in organization context:", {
+							id: userMembership.organization.id,
+							name: userMembership.organization.name,
+							role: userMembership.role,
+						})
+					} else {
+						this.log("[auth] Warning: User not found in stored organization:", storedOrgId)
+					}
+				} else {
+					this.log("[auth] User in personal account context - not setting organization info")
+				}
+			} else {
+				// Old credentials without organization context - fetch organization info to determine context
+				const orgMemberships = await this.clerkGetOrganizationMemberships()
+				const primaryOrgMembership = this.findPrimaryOrganizationMembership(orgMemberships)
+
+				if (primaryOrgMembership) {
+					this.setUserOrganizationInfo(userInfo, primaryOrgMembership)
+
+					// Check organization public metadata for extension_bridge_enabled
+					if (await this.isExtensionBridgeEnabledForOrganization(primaryOrgMembership.organization.id)) {
+						extensionBridgeEnabled = true
+					}
+
+					this.log("[auth] Legacy credentials: Found organization membership:", {
+						id: primaryOrgMembership.organization.id,
+						name: primaryOrgMembership.organization.name,
+						role: primaryOrgMembership.role,
+					})
+				} else {
+					this.log("[auth] Legacy credentials: No organization memberships found")
+				}
+			}
+		} catch (error) {
+			this.log("[auth] Failed to fetch organization info:", error)
+			// Don't throw - organization info is optional
+		}
+
+		// Set the extension bridge enabled flag
+		userInfo.extensionBridgeEnabled = extensionBridgeEnabled
+
+		return userInfo
+	}
+
+	private findOrganizationMembership(
+		memberships: CloudOrganizationMembership[],
+		organizationId: string,
+	): CloudOrganizationMembership | undefined {
+		return memberships?.find((membership) => membership.organization.id === organizationId)
+	}
+
+	private findPrimaryOrganizationMembership(
+		memberships: CloudOrganizationMembership[],
+	): CloudOrganizationMembership | undefined {
+		return memberships && memberships.length > 0 ? memberships[0] : undefined
+	}
+
+	private setUserOrganizationInfo(userInfo: CloudUserInfo, membership: CloudOrganizationMembership): void {
+		userInfo.organizationId = membership.organization.id
+		userInfo.organizationName = membership.organization.name
+		userInfo.organizationRole = membership.role
+		userInfo.organizationImageUrl = membership.organization.image_url
+	}
+
+	private async clerkGetOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
+		const response = await fetch(`${getClerkBaseUrl()}/v1/me/organization_memberships`, {
+			headers: {
+				Authorization: `Bearer ${this.credentials!.clientToken}`,
+				"User-Agent": this.userAgent(),
+			},
+			signal: AbortSignal.timeout(10000),
+		})
+
+		return clerkOrganizationMembershipsSchema.parse(await response.json()).response
+	}
+
+	private async getOrganizationMetadata(
+		organizationId: string,
+	): Promise<{ public_metadata?: Record<string, unknown> } | null> {
+		try {
+			const response = await fetch(`${getClerkBaseUrl()}/v1/organizations/${organizationId}`, {
+				headers: {
+					Authorization: `Bearer ${this.credentials!.clientToken}`,
+					"User-Agent": this.userAgent(),
+				},
+				signal: AbortSignal.timeout(10000),
+			})
+
+			if (!response.ok) {
+				this.log(`[auth] Failed to fetch organization metadata: ${response.status} ${response.statusText}`)
+				return null
+			}
+
+			const data = await response.json()
+			return data.response || data
+		} catch (error) {
+			this.log("[auth] Error fetching organization metadata:", error)
+			return null
+		}
+	}
+
+	private async isExtensionBridgeEnabledForOrganization(organizationId: string): Promise<boolean> {
+		const orgMetadata = await this.getOrganizationMetadata(organizationId)
+		return orgMetadata?.public_metadata?.extension_bridge_enabled === true
+	}
+
+	private async clerkLogout(credentials: AuthCredentials): Promise<void> {
+		const formData = new URLSearchParams()
+		formData.append("_is_native", "1")
+
+		const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${credentials.sessionId}/remove`, {
+			method: "POST",
+			headers: {
+				"Content-Type": "application/x-www-form-urlencoded",
+				Authorization: `Bearer ${credentials.clientToken}`,
+				"User-Agent": this.userAgent(),
+			},
+			body: formData.toString(),
+			signal: AbortSignal.timeout(10000),
+		})
+
+		if (!response.ok) {
+			throw new Error(`HTTP ${response.status}: ${response.statusText}`)
+		}
+	}
+
+	private userAgent(): string {
+		return getUserAgent(this.context)
+	}
+}

+ 59 - 0
packages/cloud/src/__mocks__/vscode.ts

@@ -0,0 +1,59 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+export const window = {
+	showInformationMessage: vi.fn(),
+	showErrorMessage: vi.fn(),
+}
+
+export const env = {
+	openExternal: vi.fn(),
+}
+
+export const Uri = {
+	parse: vi.fn((uri: string) => ({ toString: () => uri })),
+}
+
+export interface ExtensionContext {
+	secrets: {
+		get: (key: string) => Promise<string | undefined>
+		store: (key: string, value: string) => Promise<void>
+		delete: (key: string) => Promise<void>
+		onDidChange: (listener: (e: { key: string }) => void) => {
+			dispose: () => void
+		}
+	}
+	globalState: {
+		get: <T>(key: string) => T | undefined
+		update: (key: string, value: any) => Promise<void>
+	}
+	subscriptions: any[]
+	extension?: {
+		packageJSON?: {
+			version?: string
+			publisher?: string
+			name?: string
+		}
+	}
+}
+
+// Mock implementation for tests
+export const mockExtensionContext: ExtensionContext = {
+	secrets: {
+		get: vi.fn().mockResolvedValue(undefined),
+		store: vi.fn().mockResolvedValue(undefined),
+		delete: vi.fn().mockResolvedValue(undefined),
+		onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
+	},
+	globalState: {
+		get: vi.fn().mockReturnValue(undefined),
+		update: vi.fn().mockResolvedValue(undefined),
+	},
+	subscriptions: [],
+	extension: {
+		packageJSON: {
+			version: "1.0.0",
+			publisher: "RooVeterinaryInc",
+			name: "roo-cline",
+		},
+	},
+}

+ 147 - 0
packages/cloud/src/__tests__/CloudService.integration.test.ts

@@ -0,0 +1,147 @@
+// npx vitest run src/__tests__/CloudService.integration.test.ts
+
+import type { ExtensionContext } from "vscode"
+
+import { CloudService } from "../CloudService.js"
+import { StaticSettingsService } from "../StaticSettingsService.js"
+import { CloudSettingsService } from "../CloudSettingsService.js"
+
+vi.mock("vscode", () => ({
+	ExtensionContext: vi.fn(),
+	window: {
+		showInformationMessage: vi.fn(),
+		showErrorMessage: vi.fn(),
+	},
+	env: {
+		openExternal: vi.fn(),
+	},
+	Uri: {
+		parse: vi.fn(),
+	},
+}))
+
+describe("CloudService Integration - Settings Service Selection", () => {
+	let mockContext: ExtensionContext
+
+	beforeEach(() => {
+		CloudService.resetInstance()
+
+		mockContext = {
+			subscriptions: [],
+			workspaceState: {
+				get: vi.fn(),
+				update: vi.fn(),
+				keys: vi.fn().mockReturnValue([]),
+			},
+			secrets: {
+				get: vi.fn(),
+				store: vi.fn(),
+				delete: vi.fn(),
+				onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
+			},
+			globalState: {
+				get: vi.fn(),
+				update: vi.fn(),
+				setKeysForSync: vi.fn(),
+				keys: vi.fn().mockReturnValue([]),
+			},
+			extensionUri: { scheme: "file", path: "/mock/path" },
+			extensionPath: "/mock/path",
+			extensionMode: 1,
+			asAbsolutePath: vi.fn((relativePath: string) => `/mock/path/${relativePath}`),
+			storageUri: { scheme: "file", path: "/mock/storage" },
+			extension: {
+				packageJSON: {
+					version: "1.0.0",
+				},
+			},
+		} as unknown as ExtensionContext
+	})
+
+	afterEach(() => {
+		CloudService.resetInstance()
+		delete process.env.ROO_CODE_CLOUD_ORG_SETTINGS
+		delete process.env.ROO_CODE_CLOUD_TOKEN
+	})
+
+	it("should use CloudSettingsService when no environment variable is set", async () => {
+		// Ensure no environment variables are set
+		delete process.env.ROO_CODE_CLOUD_ORG_SETTINGS
+		delete process.env.ROO_CODE_CLOUD_TOKEN
+
+		const cloudService = await CloudService.createInstance(mockContext)
+
+		// Access the private settingsService to check its type
+		const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService
+		expect(settingsService).toBeInstanceOf(CloudSettingsService)
+	})
+
+	it("should use StaticSettingsService when ROO_CODE_CLOUD_ORG_SETTINGS is set", async () => {
+		const validSettings = {
+			version: 1,
+			cloudSettings: {
+				recordTaskMessages: true,
+				enableTaskSharing: true,
+				taskShareExpirationDays: 30,
+			},
+			defaultSettings: {
+				enableCheckpoints: true,
+			},
+			allowList: {
+				allowAll: true,
+				providers: {},
+			},
+		}
+
+		// Set the environment variable
+		process.env.ROO_CODE_CLOUD_ORG_SETTINGS = Buffer.from(JSON.stringify(validSettings)).toString("base64")
+
+		const cloudService = await CloudService.createInstance(mockContext)
+
+		// Access the private settingsService to check its type
+		const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService
+		expect(settingsService).toBeInstanceOf(StaticSettingsService)
+
+		// Verify the settings are correctly loaded
+		expect(cloudService.getAllowList()).toEqual(validSettings.allowList)
+	})
+
+	it("should throw error when ROO_CODE_CLOUD_ORG_SETTINGS contains invalid data", async () => {
+		// Set invalid environment variable
+		process.env.ROO_CODE_CLOUD_ORG_SETTINGS = "invalid-base64-data"
+
+		await expect(CloudService.createInstance(mockContext)).rejects.toThrow("Failed to initialize CloudService")
+	})
+
+	it("should prioritize static token auth when both environment variables are set", async () => {
+		const validSettings = {
+			version: 1,
+			cloudSettings: {
+				recordTaskMessages: true,
+				enableTaskSharing: true,
+				taskShareExpirationDays: 30,
+			},
+			defaultSettings: {
+				enableCheckpoints: true,
+			},
+			allowList: {
+				allowAll: true,
+				providers: {},
+			},
+		}
+
+		// Set both environment variables
+		process.env.ROO_CODE_CLOUD_TOKEN = "test-token"
+		process.env.ROO_CODE_CLOUD_ORG_SETTINGS = Buffer.from(JSON.stringify(validSettings)).toString("base64")
+
+		const cloudService = await CloudService.createInstance(mockContext)
+
+		// Should use StaticSettingsService for settings
+		const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService
+		expect(settingsService).toBeInstanceOf(StaticSettingsService)
+
+		// Should use StaticTokenAuthService for auth (from the existing logic)
+		expect(cloudService.isAuthenticated()).toBe(true)
+		expect(cloudService.hasActiveSession()).toBe(true)
+	})
+})

+ 600 - 0
packages/cloud/src/__tests__/CloudService.test.ts

@@ -0,0 +1,600 @@
+// npx vitest run src/__tests__/CloudService.test.ts
+
+import * as vscode from "vscode"
+
+import type { ClineMessage } from "@roo-code/types"
+
+import { TaskNotFoundError } from "../errors.js"
+import { CloudService } from "../CloudService.js"
+import { WebAuthService } from "../WebAuthService.js"
+import { CloudSettingsService } from "../CloudSettingsService.js"
+import { CloudShareService } from "../CloudShareService.js"
+import { CloudTelemetryClient as TelemetryClient } from "../TelemetryClient.js"
+
+vi.mock("vscode", () => ({
+	ExtensionContext: vi.fn(),
+	window: {
+		showInformationMessage: vi.fn(),
+		showErrorMessage: vi.fn(),
+	},
+	env: {
+		openExternal: vi.fn(),
+	},
+	Uri: {
+		parse: vi.fn(),
+	},
+}))
+
+vi.mock("../WebAuthService")
+
+vi.mock("../CloudSettingsService")
+
+vi.mock("../CloudShareService")
+
+vi.mock("../TelemetryClient")
+
+describe("CloudService", () => {
+	let mockContext: vscode.ExtensionContext
+
+	let mockAuthService: {
+		initialize: ReturnType<typeof vi.fn>
+		broadcast: ReturnType<typeof vi.fn>
+		login: ReturnType<typeof vi.fn>
+		logout: ReturnType<typeof vi.fn>
+		isAuthenticated: ReturnType<typeof vi.fn>
+		hasActiveSession: ReturnType<typeof vi.fn>
+		hasOrIsAcquiringActiveSession: ReturnType<typeof vi.fn>
+		getUserInfo: ReturnType<typeof vi.fn>
+		getState: ReturnType<typeof vi.fn>
+		getSessionToken: ReturnType<typeof vi.fn>
+		handleCallback: ReturnType<typeof vi.fn>
+		getStoredOrganizationId: ReturnType<typeof vi.fn>
+		on: ReturnType<typeof vi.fn>
+		off: ReturnType<typeof vi.fn>
+		once: ReturnType<typeof vi.fn>
+		emit: ReturnType<typeof vi.fn>
+	}
+
+	let mockSettingsService: {
+		initialize: ReturnType<typeof vi.fn>
+		getSettings: ReturnType<typeof vi.fn>
+		getAllowList: ReturnType<typeof vi.fn>
+		dispose: ReturnType<typeof vi.fn>
+		on: ReturnType<typeof vi.fn>
+		off: ReturnType<typeof vi.fn>
+	}
+
+	let mockShareService: {
+		shareTask: ReturnType<typeof vi.fn>
+		canShareTask: ReturnType<typeof vi.fn>
+	}
+
+	let mockTelemetryClient: {
+		backfillMessages: ReturnType<typeof vi.fn>
+	}
+
+	beforeEach(() => {
+		CloudService.resetInstance()
+
+		mockContext = {
+			subscriptions: [],
+			workspaceState: {
+				get: vi.fn(),
+				update: vi.fn(),
+				keys: vi.fn().mockReturnValue([]),
+			},
+			secrets: {
+				get: vi.fn(),
+				store: vi.fn(),
+				delete: vi.fn(),
+				onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
+			},
+			globalState: {
+				get: vi.fn(),
+				update: vi.fn(),
+				setKeysForSync: vi.fn(),
+				keys: vi.fn().mockReturnValue([]),
+			},
+			extensionUri: { scheme: "file", path: "/mock/path" },
+			extensionPath: "/mock/path",
+			extensionMode: 1,
+			asAbsolutePath: vi.fn((relativePath: string) => `/mock/path/${relativePath}`),
+			storageUri: { scheme: "file", path: "/mock/storage" },
+			extension: {
+				packageJSON: {
+					version: "1.0.0",
+				},
+			},
+		} as unknown as vscode.ExtensionContext
+
+		mockAuthService = {
+			initialize: vi.fn().mockResolvedValue(undefined),
+			broadcast: vi.fn(),
+			login: vi.fn(),
+			logout: vi.fn(),
+			isAuthenticated: vi.fn().mockReturnValue(false),
+			hasActiveSession: vi.fn().mockReturnValue(false),
+			hasOrIsAcquiringActiveSession: vi.fn().mockReturnValue(false),
+			getUserInfo: vi.fn(),
+			getState: vi.fn().mockReturnValue("logged-out"),
+			getSessionToken: vi.fn(),
+			handleCallback: vi.fn(),
+			getStoredOrganizationId: vi.fn().mockReturnValue(null),
+			on: vi.fn(),
+			off: vi.fn(),
+			once: vi.fn(),
+			emit: vi.fn(),
+		}
+
+		mockSettingsService = {
+			initialize: vi.fn(),
+			getSettings: vi.fn(),
+			getAllowList: vi.fn(),
+			dispose: vi.fn(),
+			on: vi.fn(),
+			off: vi.fn(),
+		}
+
+		mockShareService = {
+			shareTask: vi.fn(),
+			canShareTask: vi.fn().mockResolvedValue(true),
+		}
+
+		mockTelemetryClient = {
+			backfillMessages: vi.fn().mockResolvedValue(undefined),
+		}
+
+		vi.mocked(WebAuthService).mockImplementation(() => mockAuthService as unknown as WebAuthService)
+
+		vi.mocked(CloudSettingsService).mockImplementation(() => mockSettingsService as unknown as CloudSettingsService)
+
+		vi.mocked(CloudShareService).mockImplementation(() => mockShareService as unknown as CloudShareService)
+
+		vi.mocked(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient)
+	})
+
+	afterEach(() => {
+		vi.clearAllMocks()
+		CloudService.resetInstance()
+	})
+
+	describe("createInstance", () => {
+		it("should create and initialize CloudService instance", async () => {
+			const mockLog = vi.fn()
+
+			const cloudService = await CloudService.createInstance(mockContext, mockLog)
+
+			expect(cloudService).toBeInstanceOf(CloudService)
+			expect(WebAuthService).toHaveBeenCalledWith(mockContext, expect.any(Function))
+			expect(CloudSettingsService).toHaveBeenCalledWith(mockContext, mockAuthService, expect.any(Function))
+		})
+
+		it("should set up event listeners for CloudSettingsService", async () => {
+			const mockLog = vi.fn()
+
+			await CloudService.createInstance(mockContext, mockLog)
+
+			expect(mockSettingsService.on).toHaveBeenCalledWith("settings-updated", expect.any(Function))
+		})
+
+		it("should throw error if instance already exists", async () => {
+			await CloudService.createInstance(mockContext)
+
+			await expect(CloudService.createInstance(mockContext)).rejects.toThrow(
+				"CloudService instance already created",
+			)
+		})
+	})
+
+	describe("authentication methods", () => {
+		let cloudService: CloudService
+
+		beforeEach(async () => {
+			cloudService = await CloudService.createInstance(mockContext)
+		})
+
+		it("should delegate login to AuthService", async () => {
+			await cloudService.login()
+			expect(mockAuthService.login).toHaveBeenCalled()
+		})
+
+		it("should delegate logout to AuthService", async () => {
+			await cloudService.logout()
+			expect(mockAuthService.logout).toHaveBeenCalled()
+		})
+
+		it("should delegate isAuthenticated to AuthService", () => {
+			const result = cloudService.isAuthenticated()
+			expect(mockAuthService.isAuthenticated).toHaveBeenCalled()
+			expect(result).toBe(false)
+		})
+
+		it("should delegate hasActiveSession to AuthService", () => {
+			const result = cloudService.hasActiveSession()
+			expect(mockAuthService.hasActiveSession).toHaveBeenCalled()
+			expect(result).toBe(false)
+		})
+
+		it("should delegate getUserInfo to AuthService", async () => {
+			await cloudService.getUserInfo()
+			expect(mockAuthService.getUserInfo).toHaveBeenCalled()
+		})
+
+		it("should return organization ID from user info", () => {
+			const mockUserInfo = {
+				name: "Test User",
+				email: "[email protected]",
+				organizationId: "org_123",
+				organizationName: "Test Org",
+				organizationRole: "admin",
+			}
+			mockAuthService.getUserInfo.mockReturnValue(mockUserInfo)
+
+			const result = cloudService.getOrganizationId()
+			expect(mockAuthService.getUserInfo).toHaveBeenCalled()
+			expect(result).toBe("org_123")
+		})
+
+		it("should return null when no organization ID available", () => {
+			mockAuthService.getUserInfo.mockReturnValue(null)
+
+			const result = cloudService.getOrganizationId()
+			expect(result).toBe(null)
+		})
+
+		it("should return organization name from user info", () => {
+			const mockUserInfo = {
+				name: "Test User",
+				email: "[email protected]",
+				organizationId: "org_123",
+				organizationName: "Test Org",
+				organizationRole: "admin",
+			}
+			mockAuthService.getUserInfo.mockReturnValue(mockUserInfo)
+
+			const result = cloudService.getOrganizationName()
+			expect(mockAuthService.getUserInfo).toHaveBeenCalled()
+			expect(result).toBe("Test Org")
+		})
+
+		it("should return null when no organization name available", () => {
+			mockAuthService.getUserInfo.mockReturnValue(null)
+
+			const result = cloudService.getOrganizationName()
+			expect(result).toBe(null)
+		})
+
+		it("should return organization role from user info", () => {
+			const mockUserInfo = {
+				name: "Test User",
+				email: "[email protected]",
+				organizationId: "org_123",
+				organizationName: "Test Org",
+				organizationRole: "admin",
+			}
+			mockAuthService.getUserInfo.mockReturnValue(mockUserInfo)
+
+			const result = cloudService.getOrganizationRole()
+			expect(mockAuthService.getUserInfo).toHaveBeenCalled()
+			expect(result).toBe("admin")
+		})
+
+		it("should return null when no organization role available", () => {
+			mockAuthService.getUserInfo.mockReturnValue(null)
+
+			const result = cloudService.getOrganizationRole()
+			expect(result).toBe(null)
+		})
+
+		it("should delegate getAuthState to AuthService", () => {
+			const result = cloudService.getAuthState()
+			expect(mockAuthService.getState).toHaveBeenCalled()
+			expect(result).toBe("logged-out")
+		})
+
+		it("should delegate handleAuthCallback to AuthService", async () => {
+			await cloudService.handleAuthCallback("code", "state")
+			expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state", undefined)
+		})
+
+		it("should delegate handleAuthCallback with organizationId to AuthService", async () => {
+			await cloudService.handleAuthCallback("code", "state", "org_123")
+			expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state", "org_123")
+		})
+
+		it("should return stored organization ID from AuthService", () => {
+			mockAuthService.getStoredOrganizationId.mockReturnValue("org_456")
+
+			const result = cloudService.getStoredOrganizationId()
+			expect(mockAuthService.getStoredOrganizationId).toHaveBeenCalled()
+			expect(result).toBe("org_456")
+		})
+
+		it("should return null when no stored organization ID available", () => {
+			mockAuthService.getStoredOrganizationId.mockReturnValue(null)
+
+			const result = cloudService.getStoredOrganizationId()
+			expect(result).toBe(null)
+		})
+
+		it("should return true when stored organization ID exists", () => {
+			mockAuthService.getStoredOrganizationId.mockReturnValue("org_789")
+
+			const result = cloudService.hasStoredOrganizationId()
+			expect(result).toBe(true)
+		})
+
+		it("should return false when no stored organization ID exists", () => {
+			mockAuthService.getStoredOrganizationId.mockReturnValue(null)
+
+			const result = cloudService.hasStoredOrganizationId()
+			expect(result).toBe(false)
+		})
+	})
+
+	describe("organization settings methods", () => {
+		let cloudService: CloudService
+
+		beforeEach(async () => {
+			cloudService = await CloudService.createInstance(mockContext)
+		})
+
+		it("should delegate getAllowList to SettingsService", () => {
+			cloudService.getAllowList()
+			expect(mockSettingsService.getAllowList).toHaveBeenCalled()
+		})
+	})
+
+	describe("error handling", () => {
+		it("should throw error when accessing methods before initialization", () => {
+			expect(() => CloudService.instance.login()).toThrow("CloudService not initialized")
+		})
+
+		it("should throw error when accessing instance before creation", () => {
+			expect(() => CloudService.instance).toThrow("CloudService not initialized")
+		})
+	})
+
+	describe("hasInstance", () => {
+		it("should return false when no instance exists", () => {
+			expect(CloudService.hasInstance()).toBe(false)
+		})
+
+		it("should return true when instance exists and is initialized", async () => {
+			await CloudService.createInstance(mockContext)
+			expect(CloudService.hasInstance()).toBe(true)
+		})
+	})
+
+	describe("dispose", () => {
+		it("should dispose of all services and clean up", async () => {
+			const cloudService = await CloudService.createInstance(mockContext)
+			cloudService.dispose()
+
+			expect(mockSettingsService.dispose).toHaveBeenCalled()
+		})
+
+		it("should remove event listeners from CloudSettingsService", async () => {
+			// Create a mock that will pass the instanceof check
+			const mockCloudSettingsService = Object.create(CloudSettingsService.prototype)
+			Object.assign(mockCloudSettingsService, {
+				initialize: vi.fn(),
+				getSettings: vi.fn(),
+				getAllowList: vi.fn(),
+				dispose: vi.fn(),
+				on: vi.fn(),
+				off: vi.fn(),
+			})
+
+			// Override the mock to return our properly typed instance
+			vi.mocked(CloudSettingsService).mockImplementation(() => mockCloudSettingsService)
+
+			const cloudService = await CloudService.createInstance(mockContext)
+
+			// Verify the listener was added
+			expect(mockCloudSettingsService.on).toHaveBeenCalledWith("settings-updated", expect.any(Function))
+
+			// Get the listener function that was registered
+			const registeredListener = mockCloudSettingsService.on.mock.calls.find(
+				(call: unknown[]) => call[0] === "settings-updated",
+			)?.[1]
+
+			cloudService.dispose()
+
+			// Verify the listener was removed with the same function
+			expect(mockCloudSettingsService.off).toHaveBeenCalledWith("settings-updated", registeredListener)
+		})
+
+		it("should handle disposal when using StaticSettingsService", async () => {
+			// Reset the instance first
+			CloudService.resetInstance()
+
+			// Mock a StaticSettingsService (which doesn't extend CloudSettingsService)
+			const mockStaticSettingsService = {
+				initialize: vi.fn(),
+				getSettings: vi.fn(),
+				getAllowList: vi.fn(),
+				dispose: vi.fn(),
+				on: vi.fn(), // Add on method to avoid initialization error
+				off: vi.fn(), // Add off method for disposal
+			}
+
+			// Override the mock to return a service that won't pass instanceof check
+			vi.mocked(CloudSettingsService).mockImplementation(
+				() => mockStaticSettingsService as unknown as CloudSettingsService,
+			)
+
+			// This should not throw even though the service doesn't pass instanceof check
+			const _cloudService = await CloudService.createInstance(mockContext)
+
+			// Should not throw when disposing
+			expect(() => _cloudService.dispose()).not.toThrow()
+
+			// Should still call dispose on the settings service
+			expect(mockStaticSettingsService.dispose).toHaveBeenCalled()
+			// Should NOT call off method since it's not a CloudSettingsService instance
+			expect(mockStaticSettingsService.off).not.toHaveBeenCalled()
+		})
+	})
+
+	describe("settings event handling", () => {
+		let _cloudService: CloudService
+
+		beforeEach(async () => {
+			_cloudService = await CloudService.createInstance(mockContext)
+		})
+
+		it("should emit settings-updated event when settings are updated", async () => {
+			const settingsListener = vi.fn()
+			_cloudService.on("settings-updated", settingsListener)
+
+			// Get the settings listener that was registered with the settings service
+			const serviceSettingsListener = mockSettingsService.on.mock.calls.find(
+				(call: string[]) => call[0] === "settings-updated",
+			)?.[1]
+
+			expect(serviceSettingsListener).toBeDefined()
+
+			// Simulate settings update event
+			const settingsData = {
+				settings: {
+					version: 2,
+					defaultSettings: {},
+					allowList: { allowAll: true, providers: {} },
+				},
+				previousSettings: {
+					version: 1,
+					defaultSettings: {},
+					allowList: { allowAll: true, providers: {} },
+				},
+			}
+			serviceSettingsListener(settingsData)
+
+			expect(settingsListener).toHaveBeenCalledWith(settingsData)
+		})
+	})
+
+	describe("shareTask with ClineMessage retry logic", () => {
+		let cloudService: CloudService
+
+		beforeEach(async () => {
+			// Reset mocks for shareTask tests
+			vi.clearAllMocks()
+
+			// Reset authentication state for shareTask tests
+			mockAuthService.isAuthenticated.mockReturnValue(true)
+			mockAuthService.hasActiveSession.mockReturnValue(true)
+			mockAuthService.hasOrIsAcquiringActiveSession.mockReturnValue(true)
+			mockAuthService.getState.mockReturnValue("active")
+
+			cloudService = await CloudService.createInstance(mockContext)
+		})
+
+		it("should call shareTask without retry when successful", async () => {
+			const taskId = "test-task-id"
+			const visibility = "organization"
+			const clineMessages: ClineMessage[] = [
+				{
+					ts: Date.now(),
+					type: "say",
+					say: "text",
+					text: "Hello world",
+				},
+			]
+
+			const expectedResult = {
+				success: true,
+				shareUrl: "https://example.com/share/123",
+			}
+			mockShareService.shareTask.mockResolvedValue(expectedResult)
+
+			const result = await cloudService.shareTask(taskId, visibility, clineMessages)
+
+			expect(mockShareService.shareTask).toHaveBeenCalledTimes(1)
+			expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, visibility)
+			expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled()
+			expect(result).toEqual(expectedResult)
+		})
+
+		it("should retry with backfill when TaskNotFoundError occurs", async () => {
+			const taskId = "test-task-id"
+			const visibility = "organization"
+			const clineMessages: ClineMessage[] = [
+				{
+					ts: Date.now(),
+					type: "say",
+					say: "text",
+					text: "Hello world",
+				},
+			]
+
+			const expectedResult = {
+				success: true,
+				shareUrl: "https://example.com/share/123",
+			}
+
+			// First call throws TaskNotFoundError, second call succeeds
+			mockShareService.shareTask
+				.mockRejectedValueOnce(new TaskNotFoundError(taskId))
+				.mockResolvedValueOnce(expectedResult)
+
+			const result = await cloudService.shareTask(taskId, visibility, clineMessages)
+
+			expect(mockShareService.shareTask).toHaveBeenCalledTimes(2)
+			expect(mockShareService.shareTask).toHaveBeenNthCalledWith(1, taskId, visibility)
+			expect(mockShareService.shareTask).toHaveBeenNthCalledWith(2, taskId, visibility)
+			expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledTimes(1)
+			expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledWith(clineMessages, taskId)
+			expect(result).toEqual(expectedResult)
+		})
+
+		it("should not retry when TaskNotFoundError occurs but no clineMessages provided", async () => {
+			const taskId = "test-task-id"
+			const visibility = "organization"
+
+			const taskNotFoundError = new TaskNotFoundError(taskId)
+			mockShareService.shareTask.mockRejectedValue(taskNotFoundError)
+
+			await expect(cloudService.shareTask(taskId, visibility)).rejects.toThrow(TaskNotFoundError)
+
+			expect(mockShareService.shareTask).toHaveBeenCalledTimes(1)
+			expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled()
+		})
+
+		it("should not retry when non-TaskNotFoundError occurs", async () => {
+			const taskId = "test-task-id"
+			const visibility = "organization"
+			const clineMessages: ClineMessage[] = [
+				{
+					ts: Date.now(),
+					type: "say",
+					say: "text",
+					text: "Hello world",
+				},
+			]
+
+			const genericError = new Error("Some other error")
+			mockShareService.shareTask.mockRejectedValue(genericError)
+
+			await expect(cloudService.shareTask(taskId, visibility, clineMessages)).rejects.toThrow(genericError)
+
+			expect(mockShareService.shareTask).toHaveBeenCalledTimes(1)
+			expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled()
+		})
+
+		it("should work with default parameters", async () => {
+			const taskId = "test-task-id"
+			const expectedResult = {
+				success: true,
+				shareUrl: "https://example.com/share/123",
+			}
+			mockShareService.shareTask.mockResolvedValue(expectedResult)
+
+			const result = await cloudService.shareTask(taskId)
+
+			expect(mockShareService.shareTask).toHaveBeenCalledTimes(1)
+			expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, "organization")
+			expect(result).toEqual(expectedResult)
+		})
+	})
+})

+ 172 - 0
packages/cloud/src/__tests__/CloudSettingsService.parsing.test.ts

@@ -0,0 +1,172 @@
+// pnpm test src/__tests__/CloudSettingsService.parsing.test.ts
+
+import type { ExtensionContext } from "vscode"
+
+import type { AuthService } from "@roo-code/types"
+
+import { CloudSettingsService } from "../CloudSettingsService.js"
+
+describe("CloudSettingsService - Response Parsing", () => {
+	let mockContext: ExtensionContext
+	let mockAuthService: AuthService
+	let service: CloudSettingsService
+
+	beforeEach(() => {
+		// Mock ExtensionContext
+		mockContext = {
+			globalState: {
+				get: vi.fn(),
+				update: vi.fn().mockResolvedValue(undefined),
+			},
+		} as unknown as ExtensionContext
+
+		// Mock AuthService with active session
+		mockAuthService = {
+			getState: vi.fn().mockReturnValue("active-session"),
+			hasActiveSession: vi.fn().mockReturnValue(true),
+			getSessionToken: vi.fn().mockReturnValue("test-token"),
+			on: vi.fn(),
+			removeListener: vi.fn(),
+		} as unknown as AuthService
+
+		service = new CloudSettingsService(mockContext, mockAuthService, vi.fn())
+	})
+
+	it("should successfully parse valid extension settings response", async () => {
+		// Mock fetch response with a valid settings structure
+		const mockResponse = {
+			organization: {
+				version: 1,
+				defaultSettings: {},
+				allowList: {
+					allowAll: true,
+					providers: {},
+				},
+			},
+			user: {
+				features: {},
+				settings: {},
+				version: 1,
+			},
+		}
+
+		global.fetch = vi.fn().mockResolvedValue({
+			ok: true,
+			json: vi.fn().mockResolvedValue(mockResponse),
+		})
+
+		// Initialize the service
+		await service.initialize()
+
+		// Wait for the fetch to be called (timer executes immediately but asynchronously)
+		await vi.waitFor(() => {
+			expect(global.fetch).toHaveBeenCalled()
+		})
+
+		// Wait a bit for the async processing to complete
+		await new Promise((resolve) => setTimeout(resolve, 10))
+
+		// Verify settings were parsed correctly
+		const orgSettings = service.getSettings()
+		const userSettings = service.getUserSettings()
+
+		expect(orgSettings).toEqual(mockResponse.organization)
+		expect(userSettings).toEqual(mockResponse.user)
+	})
+
+	it("should handle complex nested provider settings without type errors", async () => {
+		// Mock response with complex nested provider settings
+		const mockResponse = {
+			organization: {
+				version: 2,
+				defaultSettings: {
+					maxOpenTabsContext: 10,
+					maxReadFileLine: 1000,
+				},
+				allowList: {
+					allowAll: false,
+					providers: {
+						anthropic: {
+							allowAll: true,
+						},
+						openai: {
+							allowAll: false,
+							models: ["gpt-4", "gpt-3.5-turbo"],
+						},
+					},
+				},
+				providerProfiles: {
+					default: {
+						id: "default",
+						apiProvider: "anthropic",
+						apiModelId: "claude-3-opus-20240229",
+						apiKey: "test-key",
+						modelTemperature: 0.7,
+					},
+				},
+			},
+			user: {
+				features: {
+					roomoteControlEnabled: true,
+				},
+				settings: {
+					extensionBridgeEnabled: true,
+				},
+				version: 1,
+			},
+		}
+
+		global.fetch = vi.fn().mockResolvedValue({
+			ok: true,
+			json: vi.fn().mockResolvedValue(mockResponse),
+		})
+
+		// Initialize the service
+		await service.initialize()
+
+		// Wait for the fetch to be called (timer executes immediately but asynchronously)
+		await vi.waitFor(() => {
+			expect(global.fetch).toHaveBeenCalled()
+		})
+
+		// Wait a bit for the async processing to complete
+		await new Promise((resolve) => setTimeout(resolve, 10))
+
+		// Verify complex settings were parsed correctly
+		const orgSettings = service.getSettings()
+		const userSettings = service.getUserSettings()
+
+		expect(orgSettings).toEqual(mockResponse.organization)
+		expect(userSettings).toEqual(mockResponse.user)
+		expect(orgSettings?.providerProfiles?.default).toBeDefined()
+	})
+
+	it("should handle invalid response gracefully", async () => {
+		// Mock invalid response
+		const mockResponse = {
+			organization: {
+				// Missing required fields
+				version: 1,
+			},
+			user: {
+				// Missing required fields
+				version: 1,
+			},
+		}
+
+		global.fetch = vi.fn().mockResolvedValue({
+			ok: true,
+			json: vi.fn().mockResolvedValue(mockResponse),
+		})
+
+		// Initialize the service
+		await service.initialize()
+
+		// Settings should remain undefined due to validation failure
+		const orgSettings = service.getSettings()
+		const userSettings = service.getUserSettings()
+
+		expect(orgSettings).toBeUndefined()
+		expect(userSettings).toBeUndefined()
+	})
+})

+ 535 - 0
packages/cloud/src/__tests__/CloudSettingsService.test.ts

@@ -0,0 +1,535 @@
+import type { ExtensionContext } from "vscode"
+
+import type { OrganizationSettings, AuthService } from "@roo-code/types"
+
+import { CloudSettingsService } from "../CloudSettingsService.js"
+import { RefreshTimer } from "../RefreshTimer.js"
+
+vi.mock("../RefreshTimer")
+
+vi.mock("../config", () => ({
+	getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"),
+}))
+
+global.fetch = vi.fn()
+
+describe("CloudSettingsService", () => {
+	let mockContext: ExtensionContext
+	let mockAuthService: {
+		getState: ReturnType<typeof vi.fn>
+		getSessionToken: ReturnType<typeof vi.fn>
+		hasActiveSession: ReturnType<typeof vi.fn>
+		on: ReturnType<typeof vi.fn>
+	}
+	let mockRefreshTimer: {
+		start: ReturnType<typeof vi.fn>
+		stop: ReturnType<typeof vi.fn>
+	}
+	let cloudSettingsService: CloudSettingsService
+	let mockLog: ReturnType<typeof vi.fn>
+
+	const mockSettings: OrganizationSettings = {
+		version: 1,
+		defaultSettings: {},
+		allowList: {
+			allowAll: true,
+			providers: {},
+		},
+	}
+
+	const mockUserSettings = {
+		features: {},
+		settings: {},
+		version: 1,
+	}
+
+	const mockExtensionSettingsResponse = {
+		organization: mockSettings,
+		user: mockUserSettings,
+	}
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+
+		mockContext = {
+			globalState: {
+				get: vi.fn(),
+				update: vi.fn().mockResolvedValue(undefined),
+			},
+		} as unknown as ExtensionContext
+
+		mockAuthService = {
+			getState: vi.fn().mockReturnValue("logged-out"),
+			getSessionToken: vi.fn(),
+			hasActiveSession: vi.fn().mockReturnValue(false),
+			on: vi.fn(),
+		}
+
+		mockRefreshTimer = {
+			start: vi.fn(),
+			stop: vi.fn(),
+		}
+
+		mockLog = vi.fn()
+
+		// Mock RefreshTimer constructor
+		vi.mocked(RefreshTimer).mockImplementation(() => mockRefreshTimer as unknown as RefreshTimer)
+
+		cloudSettingsService = new CloudSettingsService(mockContext, mockAuthService as unknown as AuthService, mockLog)
+	})
+
+	afterEach(() => {
+		cloudSettingsService.dispose()
+	})
+
+	describe("constructor", () => {
+		it("should create CloudSettingsService with proper dependencies", () => {
+			expect(cloudSettingsService).toBeInstanceOf(CloudSettingsService)
+			expect(RefreshTimer).toHaveBeenCalledWith({
+				callback: expect.any(Function),
+				successInterval: 30000,
+				initialBackoffMs: 1000,
+				maxBackoffMs: 30000,
+			})
+		})
+
+		it("should use console.log as default logger when none provided", () => {
+			const service = new CloudSettingsService(mockContext, mockAuthService as unknown as AuthService)
+			expect(service).toBeInstanceOf(CloudSettingsService)
+		})
+	})
+
+	describe("initialize", () => {
+		it("should load cached settings on initialization", async () => {
+			const cachedSettings = {
+				version: 1,
+				defaultSettings: {},
+				allowList: { allowAll: true, providers: {} },
+			}
+
+			// Create a fresh mock context for this test
+			const testContext = {
+				globalState: {
+					get: vi.fn((key: string) => {
+						if (key === "organization-settings") return cachedSettings
+						if (key === "user-settings") return mockUserSettings
+						return undefined
+					}),
+					update: vi.fn().mockResolvedValue(undefined),
+				},
+			} as unknown as ExtensionContext
+
+			// Mock auth service to not be logged out
+			const testAuthService = {
+				getState: vi.fn().mockReturnValue("active"),
+				getSessionToken: vi.fn(),
+				hasActiveSession: vi.fn().mockReturnValue(false),
+				on: vi.fn(),
+			}
+
+			// Create a new instance to test initialization
+			const testService = new CloudSettingsService(
+				testContext,
+				testAuthService as unknown as AuthService,
+				mockLog,
+			)
+			await testService.initialize()
+
+			expect(testContext.globalState.get).toHaveBeenCalledWith("organization-settings")
+			expect(testContext.globalState.get).toHaveBeenCalledWith("user-settings")
+			expect(testService.getSettings()).toEqual(cachedSettings)
+
+			testService.dispose()
+		})
+
+		it("should clear cached settings if user is logged out", async () => {
+			const cachedSettings = {
+				version: 1,
+				defaultSettings: {},
+				allowList: { allowAll: true, providers: {} },
+			}
+			mockContext.globalState.get = vi.fn((key: string) => {
+				if (key === "organization-settings") return cachedSettings
+				if (key === "user-settings") return mockUserSettings
+				return undefined
+			})
+			mockAuthService.getState.mockReturnValue("logged-out")
+
+			await cloudSettingsService.initialize()
+
+			// Check that both cache keys are cleared
+			const updateCalls = vi.mocked(mockContext.globalState.update).mock.calls
+			const orgSettingsCall = updateCalls.find((call) => call[0] === "organization-settings")
+			const userSettingsCall = updateCalls.find((call) => call[0] === "user-settings")
+
+			expect(orgSettingsCall).toBeDefined()
+			expect(orgSettingsCall?.[1]).toBeUndefined()
+			expect(userSettingsCall).toBeDefined()
+			expect(userSettingsCall?.[1]).toBeUndefined()
+		})
+
+		it("should set up auth service event listeners", async () => {
+			await cloudSettingsService.initialize()
+
+			expect(mockAuthService.on).toHaveBeenCalledWith("auth-state-changed", expect.any(Function))
+		})
+
+		it("should start timer if user has active session", async () => {
+			mockAuthService.hasActiveSession.mockReturnValue(true)
+
+			await cloudSettingsService.initialize()
+
+			expect(mockRefreshTimer.start).toHaveBeenCalled()
+		})
+
+		it("should not start timer if user has no active session", async () => {
+			mockAuthService.hasActiveSession.mockReturnValue(false)
+
+			await cloudSettingsService.initialize()
+
+			expect(mockRefreshTimer.start).not.toHaveBeenCalled()
+		})
+	})
+
+	describe("event emission", () => {
+		beforeEach(async () => {
+			await cloudSettingsService.initialize()
+		})
+
+		it("should emit 'settings-updated' event when settings change", async () => {
+			const eventSpy = vi.fn()
+			cloudSettingsService.on("settings-updated", eventSpy)
+
+			mockAuthService.getSessionToken.mockReturnValue("valid-token")
+			vi.mocked(fetch).mockResolvedValue({
+				ok: true,
+				json: vi.fn().mockResolvedValue(mockExtensionSettingsResponse),
+			} as unknown as Response)
+
+			// Get the callback function passed to RefreshTimer
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+
+			await timerCallback?.()
+
+			expect(eventSpy).toHaveBeenCalledWith({})
+		})
+
+		it("should emit event when either org or user settings change", async () => {
+			const eventSpy = vi.fn()
+
+			const previousSettings = {
+				version: 1,
+				defaultSettings: {},
+				allowList: { allowAll: true, providers: {} },
+			}
+			const newSettings = {
+				version: 2,
+				defaultSettings: {},
+				allowList: { allowAll: true, providers: {} },
+			}
+
+			// Create a fresh mock context for this test
+			const testContext = {
+				globalState: {
+					get: vi.fn((key: string) => {
+						if (key === "organization-settings") return previousSettings
+						if (key === "user-settings") return mockUserSettings
+						return undefined
+					}),
+					update: vi.fn().mockResolvedValue(undefined),
+				},
+			} as unknown as ExtensionContext
+
+			// Mock auth service to not be logged out
+			const testAuthService = {
+				getState: vi.fn().mockReturnValue("active"),
+				getSessionToken: vi.fn().mockReturnValue("valid-token"),
+				hasActiveSession: vi.fn().mockReturnValue(false),
+				on: vi.fn(),
+			}
+
+			// Create a new service instance with cached settings
+			const testService = new CloudSettingsService(
+				testContext,
+				testAuthService as unknown as AuthService,
+				mockLog,
+			)
+			testService.on("settings-updated", eventSpy)
+			await testService.initialize()
+
+			vi.mocked(fetch).mockResolvedValue({
+				ok: true,
+				json: vi.fn().mockResolvedValue({
+					organization: newSettings,
+					user: mockUserSettings,
+				}),
+			} as unknown as Response)
+
+			// Get the callback function passed to RefreshTimer for this instance
+			const timerCallback =
+				vi.mocked(RefreshTimer).mock.calls[vi.mocked(RefreshTimer).mock.calls.length - 1]?.[0]?.callback
+
+			await timerCallback?.()
+
+			expect(eventSpy).toHaveBeenCalledWith({})
+
+			testService.dispose()
+		})
+
+		it("should not emit event when settings version is unchanged", async () => {
+			const eventSpy = vi.fn()
+
+			// Create a fresh mock context for this test
+			const testContext = {
+				globalState: {
+					get: vi.fn((key: string) => {
+						if (key === "organization-settings") return mockSettings
+						if (key === "user-settings") return mockUserSettings
+						return undefined
+					}),
+					update: vi.fn().mockResolvedValue(undefined),
+				},
+			} as unknown as ExtensionContext
+
+			// Mock auth service to not be logged out
+			const testAuthService = {
+				getState: vi.fn().mockReturnValue("active"),
+				getSessionToken: vi.fn().mockReturnValue("valid-token"),
+				hasActiveSession: vi.fn().mockReturnValue(false),
+				on: vi.fn(),
+			}
+
+			// Create a new service instance with cached settings
+			const testService = new CloudSettingsService(
+				testContext,
+				testAuthService as unknown as AuthService,
+				mockLog,
+			)
+			testService.on("settings-updated", eventSpy)
+			await testService.initialize()
+
+			vi.mocked(fetch).mockResolvedValue({
+				ok: true,
+				json: vi.fn().mockResolvedValue(mockExtensionSettingsResponse), // Same version
+			} as unknown as Response)
+
+			// Get the callback function passed to RefreshTimer for this instance
+			const timerCallback =
+				vi.mocked(RefreshTimer).mock.calls[vi.mocked(RefreshTimer).mock.calls.length - 1]?.[0]?.callback
+
+			await timerCallback?.()
+
+			expect(eventSpy).not.toHaveBeenCalled()
+
+			testService.dispose()
+		})
+
+		it("should not emit event when fetch fails", async () => {
+			const eventSpy = vi.fn()
+			cloudSettingsService.on("settings-updated", eventSpy)
+
+			mockAuthService.getSessionToken.mockReturnValue("valid-token")
+			vi.mocked(fetch).mockResolvedValue({
+				ok: false,
+				status: 500,
+				statusText: "Internal Server Error",
+			} as unknown as Response)
+
+			// Get the callback function passed to RefreshTimer
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+
+			await timerCallback?.()
+
+			expect(eventSpy).not.toHaveBeenCalled()
+		})
+
+		it("should not emit event when no auth token available", async () => {
+			const eventSpy = vi.fn()
+			cloudSettingsService.on("settings-updated", eventSpy)
+
+			mockAuthService.getSessionToken.mockReturnValue(null)
+
+			// Get the callback function passed to RefreshTimer
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+
+			await timerCallback?.()
+
+			expect(eventSpy).not.toHaveBeenCalled()
+			expect(fetch).not.toHaveBeenCalled()
+		})
+	})
+
+	describe("fetchSettings", () => {
+		beforeEach(async () => {
+			await cloudSettingsService.initialize()
+		})
+
+		it("should fetch and cache settings successfully", async () => {
+			mockAuthService.getSessionToken.mockReturnValue("valid-token")
+			vi.mocked(fetch).mockResolvedValue({
+				ok: true,
+				json: vi.fn().mockResolvedValue(mockExtensionSettingsResponse),
+			} as unknown as Response)
+
+			// Get the callback function passed to RefreshTimer
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+
+			const result = await timerCallback?.()
+
+			expect(result).toBe(true)
+
+			expect(fetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension-settings", {
+				headers: {
+					Authorization: "Bearer valid-token",
+				},
+			})
+
+			expect(mockContext.globalState.update).toHaveBeenCalledWith("organization-settings", mockSettings)
+			expect(mockContext.globalState.update).toHaveBeenCalledWith("user-settings", mockUserSettings)
+		})
+
+		it("should handle fetch errors gracefully", async () => {
+			mockAuthService.getSessionToken.mockReturnValue("valid-token")
+			vi.mocked(fetch).mockRejectedValue(new Error("Network error"))
+
+			// Get the callback function passed to RefreshTimer
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+
+			const result = await timerCallback?.()
+
+			expect(result).toBe(false)
+
+			expect(mockLog).toHaveBeenCalledWith(
+				"[cloud-settings] Error fetching extension settings:",
+				expect.any(Error),
+			)
+		})
+
+		it("should handle invalid response format", async () => {
+			mockAuthService.getSessionToken.mockReturnValue("valid-token")
+			vi.mocked(fetch).mockResolvedValue({
+				ok: true,
+				json: vi.fn().mockResolvedValue({ invalid: "data" }),
+			} as unknown as Response)
+
+			// Get the callback function passed to RefreshTimer
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+
+			const result = await timerCallback?.()
+
+			expect(result).toBe(false)
+
+			expect(mockLog).toHaveBeenCalledWith(
+				"[cloud-settings] Invalid extension settings format:",
+				expect.any(Object),
+			)
+		})
+	})
+
+	describe("getAllowList", () => {
+		it("should return settings allowList when available", async () => {
+			mockContext.globalState.get = vi.fn((key: string) => {
+				if (key === "organization-settings") return mockSettings
+				return undefined
+			})
+			await cloudSettingsService.initialize()
+
+			const allowList = cloudSettingsService.getAllowList()
+			expect(allowList).toEqual(mockSettings.allowList)
+		})
+
+		it("should return default allow all when no settings available", () => {
+			const allowList = cloudSettingsService.getAllowList()
+			expect(allowList).toEqual({ allowAll: true, providers: {} })
+		})
+	})
+
+	describe("getSettings", () => {
+		it("should return current settings", async () => {
+			// Create a fresh mock context for this test
+			const testContext = {
+				globalState: {
+					get: vi.fn((key: string) => {
+						if (key === "organization-settings") return mockSettings
+						return undefined
+					}),
+					update: vi.fn().mockResolvedValue(undefined),
+				},
+			} as unknown as ExtensionContext
+
+			// Mock auth service to not be logged out
+			const testAuthService = {
+				getState: vi.fn().mockReturnValue("active"),
+				getSessionToken: vi.fn(),
+				hasActiveSession: vi.fn().mockReturnValue(false),
+				on: vi.fn(),
+			}
+
+			const testService = new CloudSettingsService(
+				testContext,
+				testAuthService as unknown as AuthService,
+				mockLog,
+			)
+			await testService.initialize()
+
+			const settings = testService.getSettings()
+			expect(settings).toEqual(mockSettings)
+
+			testService.dispose()
+		})
+
+		it("should return undefined when no settings available", () => {
+			const settings = cloudSettingsService.getSettings()
+			expect(settings).toBeUndefined()
+		})
+	})
+
+	describe("dispose", () => {
+		it("should remove all listeners and stop timer", () => {
+			const removeAllListenersSpy = vi.spyOn(cloudSettingsService, "removeAllListeners")
+
+			cloudSettingsService.dispose()
+
+			expect(removeAllListenersSpy).toHaveBeenCalled()
+			expect(mockRefreshTimer.stop).toHaveBeenCalled()
+		})
+	})
+
+	describe("auth service event handlers", () => {
+		it("should start timer when auth-state-changed event is triggered with active-session", async () => {
+			await cloudSettingsService.initialize()
+
+			// Get the auth-state-changed handler
+			const authStateChangedHandler = mockAuthService.on.mock.calls.find(
+				(call: string[]) => call[0] === "auth-state-changed",
+			)?.[1]
+			expect(authStateChangedHandler).toBeDefined()
+
+			// Simulate active-session state change
+			authStateChangedHandler({
+				state: "active-session",
+				previousState: "attempting-session",
+			})
+			expect(mockRefreshTimer.start).toHaveBeenCalled()
+		})
+
+		it("should stop timer and remove settings when auth-state-changed event is triggered with logged-out", async () => {
+			await cloudSettingsService.initialize()
+
+			// Get the auth-state-changed handler
+			const authStateChangedHandler = mockAuthService.on.mock.calls.find(
+				(call: string[]) => call[0] === "auth-state-changed",
+			)?.[1]
+			expect(authStateChangedHandler).toBeDefined()
+
+			// Simulate logged-out state change from active-session
+			await authStateChangedHandler({
+				state: "logged-out",
+				previousState: "active-session",
+			})
+			expect(mockRefreshTimer.stop).toHaveBeenCalled()
+			expect(mockContext.globalState.update).toHaveBeenCalledWith("organization-settings", undefined)
+			expect(mockContext.globalState.update).toHaveBeenCalledWith("user-settings", undefined)
+		})
+	})
+})

+ 318 - 0
packages/cloud/src/__tests__/CloudShareService.test.ts

@@ -0,0 +1,318 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import type { MockedFunction } from "vitest"
+import * as vscode from "vscode"
+
+import type { SettingsService, AuthService } from "@roo-code/types"
+
+import { CloudAPI } from "../CloudAPI.js"
+import { CloudShareService } from "../CloudShareService.js"
+import { CloudAPIError, TaskNotFoundError } from "../errors.js"
+
+const mockFetch = vi.fn()
+global.fetch = mockFetch as any
+
+vi.mock("vscode", () => ({
+	window: {
+		showInformationMessage: vi.fn(),
+		showErrorMessage: vi.fn(),
+		showQuickPick: vi.fn(),
+	},
+	env: {
+		clipboard: {
+			writeText: vi.fn(),
+		},
+		openExternal: vi.fn(),
+	},
+	Uri: {
+		parse: vi.fn(),
+	},
+	extensions: {
+		getExtension: vi.fn(() => ({
+			packageJSON: { version: "1.0.0" },
+		})),
+	},
+}))
+
+vi.mock("../Config", () => ({
+	getRooCodeApiUrl: () => "https://app.roocode.com",
+}))
+
+vi.mock("../utils", () => ({
+	getUserAgent: () => "Roo-Code 1.0.0",
+}))
+
+describe("CloudShareService", () => {
+	let shareService: CloudShareService
+	let mockAuthService: AuthService
+	let mockSettingsService: SettingsService
+	let mockCloudAPI: CloudAPI
+	let mockLog: MockedFunction<(...args: unknown[]) => void>
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+		mockFetch.mockClear()
+
+		mockLog = vi.fn()
+		mockAuthService = {
+			hasActiveSession: vi.fn(),
+			getSessionToken: vi.fn(),
+			isAuthenticated: vi.fn(),
+		} as any
+
+		mockSettingsService = {
+			getSettings: vi.fn(),
+		} as any
+
+		mockCloudAPI = new CloudAPI(mockAuthService, mockLog)
+
+		shareService = new CloudShareService(mockCloudAPI, mockSettingsService, mockLog)
+	})
+
+	describe("shareTask", () => {
+		it("should share task with organization visibility and copy to clipboard", async () => {
+			const mockResponseData = {
+				success: true,
+				shareUrl: "https://app.roocode.com/share/abc123",
+			}
+
+			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
+
+			mockFetch.mockResolvedValue({
+				ok: true,
+				json: vi.fn().mockResolvedValue(mockResponseData),
+			})
+
+			const result = await shareService.shareTask("task-123", "organization")
+
+			expect(result.success).toBe(true)
+			expect(result.shareUrl).toBe("https://app.roocode.com/share/abc123")
+
+			expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", {
+				method: "POST",
+				headers: {
+					"Content-Type": "application/json",
+					Authorization: "Bearer session-token",
+					"User-Agent": "Roo-Code 1.0.0",
+				},
+				body: JSON.stringify({
+					taskId: "task-123",
+					visibility: "organization",
+				}),
+				signal: expect.any(AbortSignal),
+			})
+
+			expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith("https://app.roocode.com/share/abc123")
+		})
+
+		it("should share task with public visibility", async () => {
+			const mockResponseData = {
+				success: true,
+				shareUrl: "https://app.roocode.com/share/abc123",
+			}
+
+			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
+
+			mockFetch.mockResolvedValue({
+				ok: true,
+				json: vi.fn().mockResolvedValue(mockResponseData),
+			})
+
+			const result = await shareService.shareTask("task-123", "public")
+
+			expect(result.success).toBe(true)
+
+			expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", {
+				method: "POST",
+				headers: {
+					"Content-Type": "application/json",
+					Authorization: "Bearer session-token",
+					"User-Agent": "Roo-Code 1.0.0",
+				},
+				body: JSON.stringify({ taskId: "task-123", visibility: "public" }),
+				signal: expect.any(AbortSignal),
+			})
+		})
+
+		it("should default to organization visibility when not specified", async () => {
+			const mockResponseData = {
+				success: true,
+				shareUrl: "https://app.roocode.com/share/abc123",
+			}
+
+			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
+			mockFetch.mockResolvedValue({
+				ok: true,
+				json: vi.fn().mockResolvedValue(mockResponseData),
+			})
+
+			const result = await shareService.shareTask("task-123")
+
+			expect(result.success).toBe(true)
+			expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", {
+				method: "POST",
+				headers: {
+					"Content-Type": "application/json",
+					Authorization: "Bearer session-token",
+					"User-Agent": "Roo-Code 1.0.0",
+				},
+				body: JSON.stringify({
+					taskId: "task-123",
+					visibility: "organization",
+				}),
+				signal: expect.any(AbortSignal),
+			})
+		})
+
+		it("should handle API error response", async () => {
+			const mockResponseData = {
+				success: false,
+				error: "Task not found",
+			}
+
+			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
+			mockFetch.mockResolvedValue({
+				ok: true,
+				json: vi.fn().mockResolvedValue(mockResponseData),
+			})
+
+			const result = await shareService.shareTask("task-123", "organization")
+
+			expect(result.success).toBe(false)
+			expect(result.error).toBe("Task not found")
+		})
+
+		it("should handle authentication errors", async () => {
+			;(mockAuthService.getSessionToken as any).mockReturnValue(null)
+
+			await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Authentication required")
+		})
+
+		it("should handle unexpected errors", async () => {
+			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
+			mockFetch.mockRejectedValue(new Error("Network error"))
+
+			await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Network error")
+		})
+
+		it("should throw TaskNotFoundError for 404 responses", async () => {
+			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 404,
+				statusText: "Not Found",
+				json: vi.fn().mockRejectedValue(new Error("Invalid JSON")),
+				text: vi.fn().mockResolvedValue("Not Found"),
+			})
+
+			await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(TaskNotFoundError)
+			await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Task not found")
+		})
+
+		it("should throw generic Error for non-404 HTTP errors", async () => {
+			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 500,
+				statusText: "Internal Server Error",
+				json: vi.fn().mockRejectedValue(new Error("Invalid JSON")),
+				text: vi.fn().mockResolvedValue("Internal Server Error"),
+			})
+
+			await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(CloudAPIError)
+			await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(
+				"HTTP 500: Internal Server Error",
+			)
+		})
+
+		it("should create TaskNotFoundError with correct properties", async () => {
+			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 404,
+				statusText: "Not Found",
+				json: vi.fn().mockRejectedValue(new Error("Invalid JSON")),
+				text: vi.fn().mockResolvedValue("Not Found"),
+			})
+
+			try {
+				await shareService.shareTask("task-123", "organization")
+				expect.fail("Expected TaskNotFoundError to be thrown")
+			} catch (error) {
+				expect(error).toBeInstanceOf(TaskNotFoundError)
+				expect(error).toBeInstanceOf(Error)
+				expect((error as TaskNotFoundError).message).toBe("Task not found")
+			}
+		})
+	})
+
+	describe("canShareTask", () => {
+		it("should return true when authenticated and sharing is enabled", async () => {
+			;(mockAuthService.isAuthenticated as any).mockReturnValue(true)
+			;(mockSettingsService.getSettings as any).mockReturnValue({
+				cloudSettings: {
+					enableTaskSharing: true,
+				},
+			})
+
+			const result = await shareService.canShareTask()
+
+			expect(result).toBe(true)
+		})
+
+		it("should return false when authenticated but sharing is disabled", async () => {
+			;(mockAuthService.isAuthenticated as any).mockReturnValue(true)
+			;(mockSettingsService.getSettings as any).mockReturnValue({
+				cloudSettings: {
+					enableTaskSharing: false,
+				},
+			})
+
+			const result = await shareService.canShareTask()
+
+			expect(result).toBe(false)
+		})
+
+		it("should return false when authenticated and sharing setting is undefined (default)", async () => {
+			;(mockAuthService.isAuthenticated as any).mockReturnValue(true)
+			;(mockSettingsService.getSettings as any).mockReturnValue({
+				cloudSettings: {},
+			})
+
+			const result = await shareService.canShareTask()
+
+			expect(result).toBe(false)
+		})
+
+		it("should return false when authenticated and no settings available (default)", async () => {
+			;(mockAuthService.isAuthenticated as any).mockReturnValue(true)
+			;(mockSettingsService.getSettings as any).mockReturnValue(undefined)
+
+			const result = await shareService.canShareTask()
+
+			expect(result).toBe(false)
+		})
+
+		it("should return false when settings service returns undefined", async () => {
+			;(mockSettingsService.getSettings as any).mockReturnValue(undefined)
+
+			const result = await shareService.canShareTask()
+
+			expect(result).toBe(false)
+		})
+
+		it("should handle errors gracefully", async () => {
+			;(mockSettingsService.getSettings as any).mockImplementation(() => {
+				throw new Error("Settings error")
+			})
+
+			const result = await shareService.canShareTask()
+
+			expect(result).toBe(false)
+			expect(mockLog).toHaveBeenCalledWith(
+				"[ShareService] Error checking if task can be shared:",
+				expect.any(Error),
+			)
+		})
+	})
+})

+ 210 - 0
packages/cloud/src/__tests__/RefreshTimer.test.ts

@@ -0,0 +1,210 @@
+// npx vitest run src/__tests__/RefreshTimer.test.ts
+
+import type { Mock } from "vitest"
+
+import { RefreshTimer } from "../RefreshTimer.js"
+
+vi.useFakeTimers()
+
+describe("RefreshTimer", () => {
+	let mockCallback: Mock
+	let refreshTimer: RefreshTimer
+
+	beforeEach(() => {
+		mockCallback = vi.fn()
+		mockCallback.mockResolvedValue(true)
+	})
+
+	afterEach(() => {
+		if (refreshTimer) {
+			refreshTimer.stop()
+		}
+
+		vi.clearAllTimers()
+		vi.clearAllMocks()
+	})
+
+	it("should execute callback immediately when started", () => {
+		refreshTimer = new RefreshTimer({
+			callback: mockCallback,
+		})
+
+		refreshTimer.start()
+
+		expect(mockCallback).toHaveBeenCalledTimes(1)
+	})
+
+	it("should schedule next attempt after success interval when callback succeeds", async () => {
+		mockCallback.mockResolvedValue(true)
+
+		refreshTimer = new RefreshTimer({
+			callback: mockCallback,
+			successInterval: 50000, // 50 seconds
+		})
+
+		refreshTimer.start()
+
+		// Fast-forward to execute the first callback
+		await Promise.resolve()
+
+		expect(mockCallback).toHaveBeenCalledTimes(1)
+
+		// Fast-forward 50 seconds
+		vi.advanceTimersByTime(50000)
+
+		// Callback should be called again
+		expect(mockCallback).toHaveBeenCalledTimes(2)
+	})
+
+	it("should use exponential backoff when callback fails", async () => {
+		mockCallback.mockResolvedValue(false)
+
+		refreshTimer = new RefreshTimer({
+			callback: mockCallback,
+			initialBackoffMs: 1000, // 1 second
+		})
+
+		refreshTimer.start()
+
+		// Fast-forward to execute the first callback
+		await Promise.resolve()
+
+		expect(mockCallback).toHaveBeenCalledTimes(1)
+
+		// Fast-forward 1 second
+		vi.advanceTimersByTime(1000)
+
+		// Callback should be called again
+		expect(mockCallback).toHaveBeenCalledTimes(2)
+
+		// Fast-forward to execute the second callback
+		await Promise.resolve()
+
+		// Fast-forward 2 seconds
+		vi.advanceTimersByTime(2000)
+
+		// Callback should be called again
+		expect(mockCallback).toHaveBeenCalledTimes(3)
+
+		// Fast-forward to execute the third callback
+		await Promise.resolve()
+	})
+
+	it("should not exceed maximum backoff interval", async () => {
+		mockCallback.mockResolvedValue(false)
+
+		refreshTimer = new RefreshTimer({
+			callback: mockCallback,
+			initialBackoffMs: 1000, // 1 second
+			maxBackoffMs: 5000, // 5 seconds
+		})
+
+		refreshTimer.start()
+
+		// Fast-forward through multiple failures to reach max backoff
+		await Promise.resolve() // First attempt
+		vi.advanceTimersByTime(1000)
+
+		await Promise.resolve() // Second attempt (backoff = 2000ms)
+		vi.advanceTimersByTime(2000)
+
+		await Promise.resolve() // Third attempt (backoff = 4000ms)
+		vi.advanceTimersByTime(4000)
+
+		await Promise.resolve() // Fourth attempt (backoff would be 8000ms but max is 5000ms)
+
+		// Should be capped at maxBackoffMs (no way to verify without logger)
+	})
+
+	it("should reset backoff after a successful attempt", async () => {
+		// First call fails, second succeeds, third fails
+		mockCallback.mockResolvedValueOnce(false).mockResolvedValueOnce(true).mockResolvedValueOnce(false)
+
+		refreshTimer = new RefreshTimer({
+			callback: mockCallback,
+			initialBackoffMs: 1000,
+			successInterval: 5000,
+		})
+
+		refreshTimer.start()
+
+		// First attempt (fails)
+		await Promise.resolve()
+
+		// Fast-forward 1 second
+		vi.advanceTimersByTime(1000)
+
+		// Second attempt (succeeds)
+		await Promise.resolve()
+
+		// Fast-forward 5 seconds
+		vi.advanceTimersByTime(5000)
+
+		// Third attempt (fails)
+		await Promise.resolve()
+
+		// Backoff should be reset to initial value (no way to verify without logger)
+	})
+
+	it("should handle errors in callback as failures", async () => {
+		mockCallback.mockRejectedValue(new Error("Test error"))
+
+		refreshTimer = new RefreshTimer({
+			callback: mockCallback,
+			initialBackoffMs: 1000,
+		})
+
+		refreshTimer.start()
+
+		// Fast-forward to execute the callback
+		await Promise.resolve()
+
+		// Error should be treated as a failure (no way to verify without logger)
+	})
+
+	it("should stop the timer and cancel pending executions", () => {
+		refreshTimer = new RefreshTimer({
+			callback: mockCallback,
+		})
+
+		refreshTimer.start()
+
+		// Stop the timer
+		refreshTimer.stop()
+
+		// Fast-forward a long time
+		vi.advanceTimersByTime(1000000)
+
+		// Callback should only have been called once (the initial call)
+		expect(mockCallback).toHaveBeenCalledTimes(1)
+	})
+
+	it("should reset the backoff state", async () => {
+		mockCallback.mockResolvedValue(false)
+
+		refreshTimer = new RefreshTimer({
+			callback: mockCallback,
+			initialBackoffMs: 1000,
+		})
+
+		refreshTimer.start()
+
+		// Fast-forward through a few failures
+		await Promise.resolve()
+		vi.advanceTimersByTime(1000)
+
+		await Promise.resolve()
+		vi.advanceTimersByTime(2000)
+
+		// Reset the timer
+		refreshTimer.reset()
+
+		// Stop and restart to trigger a new execution
+		refreshTimer.stop()
+		refreshTimer.start()
+
+		await Promise.resolve()
+
+		// Backoff should be back to initial value (no way to verify without logger)
+	})
+})

+ 102 - 0
packages/cloud/src/__tests__/StaticSettingsService.test.ts

@@ -0,0 +1,102 @@
+// npx vitest run src/__tests__/StaticSettingsService.test.ts
+
+import { StaticSettingsService } from "../StaticSettingsService.js"
+
+describe("StaticSettingsService", () => {
+	const validSettings = {
+		version: 1,
+		cloudSettings: {
+			recordTaskMessages: true,
+			enableTaskSharing: true,
+			taskShareExpirationDays: 30,
+		},
+		defaultSettings: {
+			enableCheckpoints: true,
+			maxOpenTabsContext: 10,
+		},
+		allowList: {
+			allowAll: false,
+			providers: {
+				anthropic: {
+					allowAll: true,
+				},
+			},
+		},
+	}
+
+	const validBase64 = Buffer.from(JSON.stringify(validSettings)).toString("base64")
+
+	describe("constructor", () => {
+		it("should parse valid base64 encoded JSON settings", () => {
+			const service = new StaticSettingsService(validBase64)
+			expect(service.getSettings()).toEqual(validSettings)
+		})
+
+		it("should throw error for invalid base64", () => {
+			expect(() => new StaticSettingsService("invalid-base64!@#")).toThrow("Failed to parse static settings")
+		})
+
+		it("should throw error for invalid JSON", () => {
+			const invalidJson = Buffer.from("{ invalid json }").toString("base64")
+			expect(() => new StaticSettingsService(invalidJson)).toThrow("Failed to parse static settings")
+		})
+
+		it("should throw error for invalid schema", () => {
+			const invalidSettings = { invalid: "schema" }
+			const invalidBase64 = Buffer.from(JSON.stringify(invalidSettings)).toString("base64")
+			expect(() => new StaticSettingsService(invalidBase64)).toThrow("Failed to parse static settings")
+		})
+	})
+
+	describe("getAllowList", () => {
+		it("should return the allow list from settings", () => {
+			const service = new StaticSettingsService(validBase64)
+			expect(service.getAllowList()).toEqual(validSettings.allowList)
+		})
+	})
+
+	describe("getSettings", () => {
+		it("should return the parsed settings", () => {
+			const service = new StaticSettingsService(validBase64)
+			expect(service.getSettings()).toEqual(validSettings)
+		})
+	})
+
+	describe("dispose", () => {
+		it("should be a no-op for static settings", () => {
+			const service = new StaticSettingsService(validBase64)
+			expect(() => service.dispose()).not.toThrow()
+		})
+	})
+
+	describe("logging", () => {
+		it("should use provided logger for errors", () => {
+			const mockLog = vi.fn()
+			expect(() => new StaticSettingsService("invalid-base64!@#", mockLog)).toThrow()
+
+			expect(mockLog).toHaveBeenCalledWith(
+				expect.stringContaining("[StaticSettingsService] failed to parse static settings:"),
+				expect.any(Error),
+			)
+		})
+
+		it("should use console.log as default logger for errors", () => {
+			const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {})
+			expect(() => new StaticSettingsService("invalid-base64!@#")).toThrow()
+
+			expect(consoleSpy).toHaveBeenCalledWith(
+				expect.stringContaining("[StaticSettingsService] failed to parse static settings:"),
+				expect.any(Error),
+			)
+
+			consoleSpy.mockRestore()
+		})
+
+		it("should not log anything for successful parsing", () => {
+			const mockLog = vi.fn()
+			new StaticSettingsService(validBase64, mockLog)
+
+			expect(mockLog).not.toHaveBeenCalled()
+		})
+	})
+})

+ 314 - 0
packages/cloud/src/__tests__/StaticTokenAuthService.spec.ts

@@ -0,0 +1,314 @@
+import type { ExtensionContext } from "vscode"
+
+import { StaticTokenAuthService } from "../StaticTokenAuthService.js"
+
+vi.mock("vscode", () => ({
+	window: {
+		showInformationMessage: vi.fn(),
+	},
+	env: {
+		openExternal: vi.fn(),
+		uriScheme: "vscode",
+	},
+	Uri: {
+		parse: vi.fn(),
+	},
+}))
+
+describe("StaticTokenAuthService", () => {
+	let authService: StaticTokenAuthService
+	let mockContext: ExtensionContext
+	let mockLog: (...args: unknown[]) => void
+	const testToken = "test-static-token"
+
+	// Job token (t:'cj') with userId and orgId - sub is CloudJob ID
+	const jobTokenWithOrg =
+		"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJyY2MiLCJzdWIiOiIzIiwiZXhwIjoxNzU2Mjc5NzU0LCJpYXQiOjE3NTYyNzU4NTQsIm5iZiI6MTc1NjI3NTgyNCwidiI6MSwiciI6eyJ1IjoidXNlcl8yeG1CaGVqTmVEVHdhbk04Q2dJT25NZ1Z4ekMiLCJvIjoib3JnXzEyM2FiYyIsInQiOiJjaiJ9fQ.k6VgV0cZUbx75kdedaeAsVYSRT7PzxDOCseLowq6moX92B4QuqtNkPRLKtQX7pJCxjuqRwEjJxmfTeXtQ82Pyg"
+
+	// Job token without orgId (orgId was null during creation)
+	const jobTokenNoOrg =
+		"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJyY2MiLCJzdWIiOiI1IiwiZXhwIjoxNzU2Mjc5NzU0LCJpYXQiOjE3NTYyNzU4NTQsIm5iZiI6MTc1NjI3NTgyNCwidiI6MSwiciI6eyJ1IjoidXNlcl8yeG1CaGVqTmVEVHdhbk04Q2dJT25NZ1Z4ekMiLCJ0IjoiY2oifX0.signature"
+
+	// Auth token (t:'auth') - sub is User ID
+	const authToken =
+		"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJyY2MiLCJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTc1NjI3OTc1NCwiaWF0IjoxNzU2Mjc1ODU0LCJuYmYiOjE3NTYyNzU4MjQsInYiOjEsInIiOnsidSI6InVzZXJfMTIzIiwidCI6ImF1dGgifX0.signature"
+
+	// JWT without the 'r' field (legacy format)
+	const legacyJWT =
+		"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
+
+	beforeEach(() => {
+		mockLog = vi.fn()
+
+		// Create a minimal mock that satisfies the constructor requirements
+		const mockContextPartial = {
+			extension: {
+				packageJSON: {
+					publisher: "TestPublisher",
+					name: "test-extension",
+				},
+			},
+			globalState: {
+				get: vi.fn(),
+				update: vi.fn(),
+			},
+			secrets: {
+				get: vi.fn(),
+				store: vi.fn(),
+				delete: vi.fn(),
+				onDidChange: vi.fn(),
+			},
+			subscriptions: [],
+		}
+
+		// Use type assertion for test mocking
+		mockContext = mockContextPartial as unknown as ExtensionContext
+
+		authService = new StaticTokenAuthService(mockContext, testToken, mockLog)
+	})
+
+	afterEach(() => {
+		vi.clearAllMocks()
+	})
+
+	describe("constructor", () => {
+		it("should create instance and log static token mode", () => {
+			expect(authService).toBeInstanceOf(StaticTokenAuthService)
+			expect(mockLog).toHaveBeenCalledWith("[auth] Using StaticTokenAuthService")
+		})
+
+		it("should use console.log as default logger", () => {
+			const serviceWithoutLog = new StaticTokenAuthService(mockContext as unknown as ExtensionContext, testToken)
+			// Can't directly test console.log usage, but constructor should not throw
+			expect(serviceWithoutLog).toBeInstanceOf(StaticTokenAuthService)
+		})
+
+		it("should parse job token with orgId and extract userId from r.u", () => {
+			const serviceWithJWT = new StaticTokenAuthService(mockContext, jobTokenWithOrg, mockLog)
+
+			const userInfo = serviceWithJWT.getUserInfo()
+			expect(userInfo?.id).toBe("user_2xmBhejNeDTwanM8CgIOnMgVxzC")
+			expect(userInfo?.organizationId).toBe("org_123abc")
+			expect(userInfo?.extensionBridgeEnabled).toBe(true)
+		})
+
+		it("should parse job token without orgId (null orgId case)", () => {
+			const serviceWithJWT = new StaticTokenAuthService(mockContext, jobTokenNoOrg, mockLog)
+
+			const userInfo = serviceWithJWT.getUserInfo()
+			expect(userInfo?.id).toBe("user_2xmBhejNeDTwanM8CgIOnMgVxzC")
+			expect(userInfo?.organizationId).toBeUndefined()
+			expect(userInfo?.extensionBridgeEnabled).toBe(true)
+		})
+
+		it("should parse auth token and extract userId from r.u", () => {
+			const serviceWithAuthToken = new StaticTokenAuthService(mockContext, authToken, mockLog)
+
+			const userInfo = serviceWithAuthToken.getUserInfo()
+			expect(userInfo?.id).toBe("user_123")
+			expect(userInfo?.organizationId).toBeUndefined()
+			expect(userInfo?.extensionBridgeEnabled).toBe(true)
+		})
+
+		it("should handle legacy JWT format with sub field", () => {
+			const serviceWithLegacyJWT = new StaticTokenAuthService(mockContext, legacyJWT, mockLog)
+
+			const userInfo = serviceWithLegacyJWT.getUserInfo()
+			expect(userInfo?.id).toBe("user_123")
+			expect(userInfo?.organizationId).toBeUndefined()
+			expect(userInfo?.extensionBridgeEnabled).toBe(true)
+		})
+
+		it("should handle invalid JWT gracefully", () => {
+			const serviceWithInvalidJWT = new StaticTokenAuthService(mockContext, "invalid-jwt-token", mockLog)
+
+			const userInfo = serviceWithInvalidJWT.getUserInfo()
+			expect(userInfo?.id).toBeUndefined()
+			expect(userInfo?.organizationId).toBeUndefined()
+			expect(userInfo?.extensionBridgeEnabled).toBe(true)
+
+			expect(mockLog).toHaveBeenCalledWith("[auth] Failed to parse JWT:", expect.any(Error))
+		})
+
+		it("should handle malformed JWT payload", () => {
+			// JWT with invalid base64 in payload
+			const malformedJWT = "header.!!!invalid-base64!!!.signature"
+			const serviceWithMalformedJWT = new StaticTokenAuthService(mockContext, malformedJWT, mockLog)
+
+			const userInfo = serviceWithMalformedJWT.getUserInfo()
+			expect(userInfo?.id).toBeUndefined()
+			expect(userInfo?.organizationId).toBeUndefined()
+
+			expect(mockLog).toHaveBeenCalledWith("[auth] Failed to parse JWT:", expect.any(Error))
+		})
+	})
+
+	describe("initialize", () => {
+		it("should start in active-session state", async () => {
+			await authService.initialize()
+			expect(authService.getState()).toBe("active-session")
+		})
+
+		it("should not emit events on initialize", async () => {
+			const authStateChangedSpy = vi.fn()
+			const userInfoSpy = vi.fn()
+
+			authService.on("auth-state-changed", authStateChangedSpy)
+			authService.on("user-info", userInfoSpy)
+
+			await authService.initialize()
+
+			expect(authStateChangedSpy).not.toHaveBeenCalled()
+			expect(userInfoSpy).not.toHaveBeenCalled()
+		})
+	})
+
+	describe("broadcast", () => {
+		it("should emit auth-state-changed event", () => {
+			const spy = vi.fn()
+			authService.on("auth-state-changed", spy)
+
+			authService.broadcast()
+
+			expect(spy).toHaveBeenCalledWith({
+				state: "active-session",
+				previousState: "initializing",
+			})
+		})
+
+		it("should emit user-info event", () => {
+			const spy = vi.fn()
+			authService.on("user-info", spy)
+
+			authService.broadcast()
+
+			expect(spy).toHaveBeenCalledWith({
+				userInfo: expect.objectContaining({
+					extensionBridgeEnabled: true,
+				}),
+			})
+		})
+
+		it("should emit user-info with parsed JWT data", () => {
+			const serviceWithJWT = new StaticTokenAuthService(mockContext, jobTokenWithOrg, mockLog)
+
+			const spy = vi.fn()
+			serviceWithJWT.on("user-info", spy)
+
+			serviceWithJWT.broadcast()
+
+			expect(spy).toHaveBeenCalledWith({
+				userInfo: {
+					extensionBridgeEnabled: true,
+					id: "user_2xmBhejNeDTwanM8CgIOnMgVxzC",
+					organizationId: "org_123abc",
+				},
+			})
+		})
+	})
+
+	describe("getSessionToken", () => {
+		it("should return the provided token", () => {
+			expect(authService.getSessionToken()).toBe(testToken)
+		})
+
+		it("should return different token when constructed with different token", () => {
+			const differentToken = "different-token"
+			const differentService = new StaticTokenAuthService(mockContext, differentToken, mockLog)
+			expect(differentService.getSessionToken()).toBe(differentToken)
+		})
+	})
+
+	describe("getUserInfo", () => {
+		it("should return object with extensionBridgeEnabled flag", () => {
+			const userInfo = authService.getUserInfo()
+			expect(userInfo).toHaveProperty("extensionBridgeEnabled")
+			expect(userInfo?.extensionBridgeEnabled).toBe(true)
+		})
+	})
+
+	describe("getStoredOrganizationId", () => {
+		it("should return null for non-JWT token", () => {
+			expect(authService.getStoredOrganizationId()).toBeNull()
+		})
+
+		it("should return organizationId from parsed JWT", () => {
+			const serviceWithJWT = new StaticTokenAuthService(mockContext, jobTokenWithOrg, mockLog)
+
+			expect(serviceWithJWT.getStoredOrganizationId()).toBe("org_123abc")
+		})
+
+		it("should return null when JWT has no organizationId", () => {
+			const serviceWithNoOrg = new StaticTokenAuthService(mockContext, jobTokenNoOrg, mockLog)
+
+			expect(serviceWithNoOrg.getStoredOrganizationId()).toBeNull()
+		})
+
+		it("should return null for legacy JWT format", () => {
+			const serviceWithLegacyJWT = new StaticTokenAuthService(mockContext, legacyJWT, mockLog)
+
+			expect(serviceWithLegacyJWT.getStoredOrganizationId()).toBeNull()
+		})
+	})
+
+	describe("authentication state methods", () => {
+		it("should always return true for isAuthenticated", () => {
+			expect(authService.isAuthenticated()).toBe(true)
+		})
+
+		it("should always return true for hasActiveSession", () => {
+			expect(authService.hasActiveSession()).toBe(true)
+		})
+
+		it("should always return true for hasOrIsAcquiringActiveSession", () => {
+			expect(authService.hasOrIsAcquiringActiveSession()).toBe(true)
+		})
+
+		it("should return active-session for getState", () => {
+			expect(authService.getState()).toBe("active-session")
+		})
+	})
+
+	describe("disabled authentication methods", () => {
+		const expectedErrorMessage = "Authentication methods are disabled in StaticTokenAuthService"
+
+		it("should throw error for login", async () => {
+			await expect(authService.login()).rejects.toThrow(expectedErrorMessage)
+		})
+
+		it("should throw error for logout", async () => {
+			await expect(authService.logout()).rejects.toThrow(expectedErrorMessage)
+		})
+
+		it("should throw error for handleCallback", async () => {
+			await expect(authService.handleCallback("code", "state")).rejects.toThrow(expectedErrorMessage)
+		})
+
+		it("should throw error for handleCallback with organization", async () => {
+			await expect(authService.handleCallback("code", "state", "org_123")).rejects.toThrow(expectedErrorMessage)
+		})
+	})
+
+	describe("event emission", () => {
+		it("should be able to register and emit events via broadcast", () => {
+			const authStateChangedSpy = vi.fn()
+			const userInfoSpy = vi.fn()
+
+			authService.on("auth-state-changed", authStateChangedSpy)
+			authService.on("user-info", userInfoSpy)
+
+			authService.broadcast()
+
+			expect(authStateChangedSpy).toHaveBeenCalledWith({
+				state: "active-session",
+				previousState: "initializing",
+			})
+
+			expect(userInfoSpy).toHaveBeenCalledWith({
+				userInfo: expect.objectContaining({
+					extensionBridgeEnabled: true,
+				}),
+			})
+		})
+	})
+})

+ 740 - 0
packages/cloud/src/__tests__/TelemetryClient.test.ts

@@ -0,0 +1,740 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+// npx vitest run src/__tests__/TelemetryClient.test.ts
+
+import { type TelemetryPropertiesProvider, TelemetryEventName } from "@roo-code/types"
+
+import { CloudTelemetryClient as TelemetryClient } from "../TelemetryClient.js"
+
+const mockFetch = vi.fn()
+global.fetch = mockFetch as any
+
+describe("TelemetryClient", () => {
+	const getPrivateProperty = <T>(instance: any, propertyName: string): T => {
+		return instance[propertyName]
+	}
+
+	let mockAuthService: any
+	let mockSettingsService: any
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+
+		// Create a mock AuthService instead of using the singleton
+		mockAuthService = {
+			getSessionToken: vi.fn().mockReturnValue("mock-token"),
+			getState: vi.fn().mockReturnValue("active-session"),
+			isAuthenticated: vi.fn().mockReturnValue(true),
+			hasActiveSession: vi.fn().mockReturnValue(true),
+		}
+
+		// Create a mock SettingsService
+		mockSettingsService = {
+			getSettings: vi.fn().mockReturnValue({
+				cloudSettings: {
+					recordTaskMessages: true,
+				},
+			}),
+		}
+
+		mockFetch.mockResolvedValue({
+			ok: true,
+			json: vi.fn().mockResolvedValue({}),
+		})
+
+		vi.spyOn(console, "info").mockImplementation(() => {})
+		vi.spyOn(console, "error").mockImplementation(() => {})
+	})
+
+	afterEach(() => {
+		vi.restoreAllMocks()
+	})
+
+	describe("isEventCapturable", () => {
+		it("should return true for events not in exclude list", () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
+				client,
+				"isEventCapturable",
+			).bind(client)
+
+			expect(isEventCapturable(TelemetryEventName.TASK_CREATED)).toBe(true)
+			expect(isEventCapturable(TelemetryEventName.LLM_COMPLETION)).toBe(true)
+			expect(isEventCapturable(TelemetryEventName.MODE_SWITCH)).toBe(true)
+			expect(isEventCapturable(TelemetryEventName.TOOL_USED)).toBe(true)
+		})
+
+		it("should return false for events in exclude list", () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
+				client,
+				"isEventCapturable",
+			).bind(client)
+
+			expect(isEventCapturable(TelemetryEventName.TASK_CONVERSATION_MESSAGE)).toBe(false)
+		})
+
+		it("should return true for TASK_MESSAGE events when recordTaskMessages is true", () => {
+			mockSettingsService.getSettings.mockReturnValue({
+				cloudSettings: {
+					recordTaskMessages: true,
+				},
+			})
+
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
+				client,
+				"isEventCapturable",
+			).bind(client)
+
+			expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(true)
+		})
+
+		it("should return false for TASK_MESSAGE events when recordTaskMessages is false", () => {
+			mockSettingsService.getSettings.mockReturnValue({
+				cloudSettings: {
+					recordTaskMessages: false,
+				},
+			})
+
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
+				client,
+				"isEventCapturable",
+			).bind(client)
+
+			expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
+		})
+
+		it("should return false for TASK_MESSAGE events when recordTaskMessages is undefined", () => {
+			mockSettingsService.getSettings.mockReturnValue({
+				cloudSettings: {},
+			})
+
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
+				client,
+				"isEventCapturable",
+			).bind(client)
+
+			expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
+		})
+
+		it("should return false for TASK_MESSAGE events when cloudSettings is undefined", () => {
+			mockSettingsService.getSettings.mockReturnValue({})
+
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
+				client,
+				"isEventCapturable",
+			).bind(client)
+
+			expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
+		})
+
+		it("should return false for TASK_MESSAGE events when getSettings returns undefined", () => {
+			mockSettingsService.getSettings.mockReturnValue(undefined)
+
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
+				client,
+				"isEventCapturable",
+			).bind(client)
+
+			expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
+		})
+	})
+
+	describe("getEventProperties", () => {
+		it("should merge provider properties with event properties", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const mockProvider: TelemetryPropertiesProvider = {
+				getTelemetryProperties: vi.fn().mockResolvedValue({
+					appVersion: "1.0.0",
+					vscodeVersion: "1.60.0",
+					platform: "darwin",
+					editorName: "vscode",
+					language: "en",
+					mode: "code",
+				}),
+			}
+
+			client.setProvider(mockProvider)
+
+			const getEventProperties = getPrivateProperty<
+				(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
+			>(client, "getEventProperties").bind(client)
+
+			const result = await getEventProperties({
+				event: TelemetryEventName.TASK_CREATED,
+				properties: {
+					customProp: "value",
+					mode: "override", // This should override the provider's mode.
+				},
+			})
+
+			expect(result).toEqual({
+				appVersion: "1.0.0",
+				vscodeVersion: "1.60.0",
+				platform: "darwin",
+				editorName: "vscode",
+				language: "en",
+				mode: "override", // Event property takes precedence.
+				customProp: "value",
+			})
+
+			expect(mockProvider.getTelemetryProperties).toHaveBeenCalledTimes(1)
+		})
+
+		it("should handle errors from provider gracefully", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const mockProvider: TelemetryPropertiesProvider = {
+				getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
+			}
+
+			const consoleErrorSpy = vi.spyOn(console, "error")
+
+			client.setProvider(mockProvider)
+
+			const getEventProperties = getPrivateProperty<
+				(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
+			>(client, "getEventProperties").bind(client)
+
+			const result = await getEventProperties({
+				event: TelemetryEventName.TASK_CREATED,
+				properties: { customProp: "value" },
+			})
+
+			expect(result).toEqual({ customProp: "value" })
+			expect(consoleErrorSpy).toHaveBeenCalledWith(
+				expect.stringContaining("Error getting telemetry properties: Provider error"),
+			)
+		})
+
+		it("should return event properties when no provider is set", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const getEventProperties = getPrivateProperty<
+				(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
+			>(client, "getEventProperties").bind(client)
+
+			const result = await getEventProperties({
+				event: TelemetryEventName.TASK_CREATED,
+				properties: { customProp: "value" },
+			})
+
+			expect(result).toEqual({ customProp: "value" })
+		})
+	})
+
+	describe("capture", () => {
+		it("should not capture events that are not capturable", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			await client.capture({
+				event: TelemetryEventName.TASK_CONVERSATION_MESSAGE, // In exclude list.
+				properties: { test: "value" },
+			})
+
+			expect(mockFetch).not.toHaveBeenCalled()
+		})
+
+		it("should not capture TASK_MESSAGE events when recordTaskMessages is false", async () => {
+			mockSettingsService.getSettings.mockReturnValue({
+				cloudSettings: {
+					recordTaskMessages: false,
+				},
+			})
+
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			await client.capture({
+				event: TelemetryEventName.TASK_MESSAGE,
+				properties: {
+					taskId: "test-task-id",
+					message: {
+						ts: 1,
+						type: "say",
+						say: "text",
+						text: "test message",
+					},
+				},
+			})
+
+			expect(mockFetch).not.toHaveBeenCalled()
+		})
+
+		it("should not capture TASK_MESSAGE events when recordTaskMessages is undefined", async () => {
+			mockSettingsService.getSettings.mockReturnValue({
+				cloudSettings: {},
+			})
+
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			await client.capture({
+				event: TelemetryEventName.TASK_MESSAGE,
+				properties: {
+					taskId: "test-task-id",
+					message: {
+						ts: 1,
+						type: "say",
+						say: "text",
+						text: "test message",
+					},
+				},
+			})
+
+			expect(mockFetch).not.toHaveBeenCalled()
+		})
+
+		it("should not send request when schema validation fails", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			await client.capture({
+				event: TelemetryEventName.TASK_CREATED,
+				properties: { test: "value" },
+			})
+
+			expect(mockFetch).not.toHaveBeenCalled()
+			expect(console.error).toHaveBeenCalledWith(expect.stringContaining("Invalid telemetry event"))
+		})
+
+		it("should send request when event is capturable and validation passes", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const providerProperties = {
+				appName: "roo-code",
+				appVersion: "1.0.0",
+				vscodeVersion: "1.60.0",
+				platform: "darwin",
+				editorName: "vscode",
+				language: "en",
+				mode: "code",
+			}
+
+			const eventProperties = {
+				taskId: "test-task-id",
+			}
+
+			const mockValidatedData = {
+				type: TelemetryEventName.TASK_CREATED,
+				properties: {
+					...providerProperties,
+					taskId: "test-task-id",
+				},
+			}
+
+			const mockProvider: TelemetryPropertiesProvider = {
+				getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
+			}
+
+			client.setProvider(mockProvider)
+
+			await client.capture({
+				event: TelemetryEventName.TASK_CREATED,
+				properties: eventProperties,
+			})
+
+			expect(mockFetch).toHaveBeenCalledWith(
+				"https://app.roocode.com/api/events",
+				expect.objectContaining({
+					method: "POST",
+					body: JSON.stringify(mockValidatedData),
+				}),
+			)
+		})
+
+		it("should attempt to capture TASK_MESSAGE events when recordTaskMessages is true", async () => {
+			mockSettingsService.getSettings.mockReturnValue({
+				cloudSettings: {
+					recordTaskMessages: true,
+				},
+			})
+
+			const eventProperties = {
+				appName: "roo-code",
+				appVersion: "1.0.0",
+				vscodeVersion: "1.60.0",
+				platform: "darwin",
+				editorName: "vscode",
+				language: "en",
+				mode: "code",
+				taskId: "test-task-id",
+				message: {
+					ts: 1,
+					type: "say",
+					say: "text",
+					text: "test message",
+				},
+			}
+
+			const mockValidatedData = {
+				type: TelemetryEventName.TASK_MESSAGE,
+				properties: eventProperties,
+			}
+
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			await client.capture({
+				event: TelemetryEventName.TASK_MESSAGE,
+				properties: eventProperties,
+			})
+
+			expect(mockFetch).toHaveBeenCalledWith(
+				"https://app.roocode.com/api/events",
+				expect.objectContaining({
+					method: "POST",
+					body: JSON.stringify(mockValidatedData),
+				}),
+			)
+		})
+
+		it("should handle fetch errors gracefully", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			mockFetch.mockRejectedValue(new Error("Network error"))
+
+			await expect(
+				client.capture({
+					event: TelemetryEventName.TASK_CREATED,
+					properties: { test: "value" },
+				}),
+			).resolves.not.toThrow()
+		})
+	})
+
+	describe("telemetry state methods", () => {
+		it("should always return true for isTelemetryEnabled", () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+			expect(client.isTelemetryEnabled()).toBe(true)
+		})
+
+		it("should have empty implementations for updateTelemetryState and shutdown", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+			client.updateTelemetryState(true)
+			await client.shutdown()
+		})
+	})
+
+	describe("backfillMessages", () => {
+		it("should not send request when not authenticated", async () => {
+			mockAuthService.isAuthenticated.mockReturnValue(false)
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const messages = [
+				{
+					ts: 1,
+					type: "say" as const,
+					say: "text" as const,
+					text: "test message",
+				},
+			]
+
+			await client.backfillMessages(messages, "test-task-id")
+
+			expect(mockFetch).not.toHaveBeenCalled()
+		})
+
+		it("should not send request when no session token available", async () => {
+			mockAuthService.getSessionToken.mockReturnValue(null)
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const messages = [
+				{
+					ts: 1,
+					type: "say" as const,
+					say: "text" as const,
+					text: "test message",
+				},
+			]
+
+			await client.backfillMessages(messages, "test-task-id")
+
+			expect(mockFetch).not.toHaveBeenCalled()
+			expect(console.error).toHaveBeenCalledWith(
+				"[TelemetryClient#backfillMessages] Unauthorized: No session token available.",
+			)
+		})
+
+		it("should send FormData request with correct structure when authenticated", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const providerProperties = {
+				appName: "roo-code",
+				appVersion: "1.0.0",
+				vscodeVersion: "1.60.0",
+				platform: "darwin",
+				editorName: "vscode",
+				language: "en",
+				mode: "code",
+			}
+
+			const mockProvider: TelemetryPropertiesProvider = {
+				getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
+			}
+
+			client.setProvider(mockProvider)
+
+			const messages = [
+				{
+					ts: 1,
+					type: "say" as const,
+					say: "text" as const,
+					text: "test message 1",
+				},
+				{
+					ts: 2,
+					type: "ask" as const,
+					ask: "followup" as const,
+					text: "test question",
+				},
+			]
+
+			await client.backfillMessages(messages, "test-task-id")
+
+			expect(mockFetch).toHaveBeenCalledWith(
+				"https://app.roocode.com/api/events/backfill",
+				expect.objectContaining({
+					method: "POST",
+					headers: {
+						Authorization: "Bearer mock-token",
+					},
+					body: expect.any(FormData),
+				}),
+			)
+
+			// Verify FormData contents
+			const call = mockFetch.mock.calls[0]
+			const formData = call?.[1]?.body as FormData
+
+			expect(formData.get("taskId")).toBe("test-task-id")
+
+			// Parse and compare properties as objects since JSON.stringify order can vary
+			const propertiesJson = formData.get("properties") as string
+			const parsedProperties = JSON.parse(propertiesJson)
+			expect(parsedProperties).toEqual({
+				taskId: "test-task-id",
+				...providerProperties,
+			})
+			// The messages are stored as a File object under the "file" key
+			const fileField = formData.get("file") as File
+			expect(fileField).toBeInstanceOf(File)
+			expect(fileField.name).toBe("task.json")
+			expect(fileField.type).toBe("application/json")
+
+			// Read the file content to verify the messages
+			const fileContent = await fileField.text()
+			expect(fileContent).toBe(JSON.stringify(messages))
+		})
+
+		it("should handle provider errors gracefully", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const mockProvider: TelemetryPropertiesProvider = {
+				getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
+			}
+
+			client.setProvider(mockProvider)
+
+			const messages = [
+				{
+					ts: 1,
+					type: "say" as const,
+					say: "text" as const,
+					text: "test message",
+				},
+			]
+
+			await client.backfillMessages(messages, "test-task-id")
+
+			expect(mockFetch).toHaveBeenCalledWith(
+				"https://app.roocode.com/api/events/backfill",
+				expect.objectContaining({
+					method: "POST",
+					headers: {
+						Authorization: "Bearer mock-token",
+					},
+					body: expect.any(FormData),
+				}),
+			)
+
+			// Verify FormData contents - should still work with just taskId
+			const call = mockFetch.mock.calls[0]
+			const formData = call?.[1]?.body as FormData
+
+			expect(formData.get("taskId")).toBe("test-task-id")
+			expect(formData.get("properties")).toBe(
+				JSON.stringify({
+					taskId: "test-task-id",
+				}),
+			)
+
+			// The messages are stored as a File object under the "file" key
+			const fileField = formData.get("file") as File
+			expect(fileField).toBeInstanceOf(File)
+			expect(fileField.name).toBe("task.json")
+			expect(fileField.type).toBe("application/json")
+
+			// Read the file content to verify the messages
+			const fileContent = await fileField.text()
+			expect(fileContent).toBe(JSON.stringify(messages))
+		})
+
+		it("should work without provider set", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			const messages = [
+				{
+					ts: 1,
+					type: "say" as const,
+					say: "text" as const,
+					text: "test message",
+				},
+			]
+
+			await client.backfillMessages(messages, "test-task-id")
+
+			expect(mockFetch).toHaveBeenCalledWith(
+				"https://app.roocode.com/api/events/backfill",
+				expect.objectContaining({
+					method: "POST",
+					headers: {
+						Authorization: "Bearer mock-token",
+					},
+					body: expect.any(FormData),
+				}),
+			)
+
+			// Verify FormData contents - should work with just taskId
+			const call = mockFetch.mock.calls[0]
+			const formData = call?.[1]?.body as FormData
+
+			expect(formData.get("taskId")).toBe("test-task-id")
+			expect(formData.get("properties")).toBe(
+				JSON.stringify({
+					taskId: "test-task-id",
+				}),
+			)
+
+			// The messages are stored as a File object under the "file" key
+			const fileField = formData.get("file") as File
+			expect(fileField).toBeInstanceOf(File)
+			expect(fileField.name).toBe("task.json")
+			expect(fileField.type).toBe("application/json")
+
+			// Read the file content to verify the messages
+			const fileContent = await fileField.text()
+			expect(fileContent).toBe(JSON.stringify(messages))
+		})
+
+		it("should handle fetch errors gracefully", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			mockFetch.mockRejectedValue(new Error("Network error"))
+
+			const messages = [
+				{
+					ts: 1,
+					type: "say" as const,
+					say: "text" as const,
+					text: "test message",
+				},
+			]
+
+			await expect(client.backfillMessages(messages, "test-task-id")).resolves.not.toThrow()
+
+			expect(console.error).toHaveBeenCalledWith(
+				expect.stringContaining(
+					"[TelemetryClient#backfillMessages] Error uploading messages: Error: Network error",
+				),
+			)
+		})
+
+		it("should handle HTTP error responses", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 404,
+				statusText: "Not Found",
+			})
+
+			const messages = [
+				{
+					ts: 1,
+					type: "say" as const,
+					say: "text" as const,
+					text: "test message",
+				},
+			]
+
+			await client.backfillMessages(messages, "test-task-id")
+
+			expect(console.error).toHaveBeenCalledWith(
+				"[TelemetryClient#backfillMessages] POST events/backfill -> 404 Not Found",
+			)
+		})
+
+		it("should log debug information when debug is enabled", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService, true)
+
+			const messages = [
+				{
+					ts: 1,
+					type: "say" as const,
+					say: "text" as const,
+					text: "test message",
+				},
+			]
+
+			await client.backfillMessages(messages, "test-task-id")
+
+			expect(console.info).toHaveBeenCalledWith(
+				"[TelemetryClient#backfillMessages] Uploading 1 messages for task test-task-id",
+			)
+			expect(console.info).toHaveBeenCalledWith(
+				"[TelemetryClient#backfillMessages] Successfully uploaded messages for task test-task-id",
+			)
+		})
+
+		it("should handle empty messages array", async () => {
+			const client = new TelemetryClient(mockAuthService, mockSettingsService)
+
+			await client.backfillMessages([], "test-task-id")
+
+			expect(mockFetch).toHaveBeenCalledWith(
+				"https://app.roocode.com/api/events/backfill",
+				expect.objectContaining({
+					method: "POST",
+					headers: {
+						Authorization: "Bearer mock-token",
+					},
+					body: expect.any(FormData),
+				}),
+			)
+
+			// Verify FormData contents
+			const call = mockFetch.mock.calls[0]
+			const formData = call?.[1]?.body as FormData
+
+			// The messages are stored as a File object under the "file" key
+			const fileField = formData.get("file") as File
+			expect(fileField).toBeInstanceOf(File)
+			expect(fileField.name).toBe("task.json")
+			expect(fileField.type).toBe("application/json")
+
+			// Read the file content to verify the empty messages array
+			const fileContent = await fileField.text()
+			expect(fileContent).toBe("[]")
+		})
+	})
+})

+ 1196 - 0
packages/cloud/src/__tests__/WebAuthService.spec.ts

@@ -0,0 +1,1196 @@
+// npx vitest run src/__tests__/auth/WebAuthService.spec.ts
+
+import crypto from "crypto"
+
+import type { Mock } from "vitest"
+import type { ExtensionContext } from "vscode"
+
+import { WebAuthService } from "../WebAuthService.js"
+import { RefreshTimer } from "../RefreshTimer.js"
+import { getClerkBaseUrl, getRooCodeApiUrl } from "../config.js"
+import { getUserAgent } from "../utils.js"
+
+vi.mock("crypto")
+
+vi.mock("../RefreshTimer")
+vi.mock("../config")
+vi.mock("../utils")
+
+const mockFetch = vi.fn()
+global.fetch = mockFetch
+
+vi.mock("vscode", () => ({
+	window: {
+		showInformationMessage: vi.fn(),
+		showErrorMessage: vi.fn(),
+	},
+	env: {
+		openExternal: vi.fn(),
+		uriScheme: "vscode",
+	},
+	Uri: {
+		parse: vi.fn((uri: string) => ({ toString: () => uri })),
+	},
+}))
+
+describe("WebAuthService", () => {
+	let authService: WebAuthService
+	let mockTimer: {
+		start: Mock
+		stop: Mock
+		reset: Mock
+	}
+	let mockLog: Mock
+	let mockContext: {
+		subscriptions: { push: Mock }
+		secrets: {
+			get: Mock
+			store: Mock
+			delete: Mock
+			onDidChange: Mock
+		}
+		globalState: {
+			get: Mock
+			update: Mock
+		}
+		extension: {
+			packageJSON: {
+				version: string
+				publisher: string
+				name: string
+			}
+		}
+	}
+
+	beforeEach(() => {
+		// Reset all mocks
+		vi.clearAllMocks()
+
+		// Setup mock context with proper subscriptions array
+		mockContext = {
+			subscriptions: {
+				push: vi.fn(),
+			},
+			secrets: {
+				get: vi.fn().mockResolvedValue(undefined),
+				store: vi.fn().mockResolvedValue(undefined),
+				delete: vi.fn().mockResolvedValue(undefined),
+				onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
+			},
+			globalState: {
+				get: vi.fn().mockReturnValue(undefined),
+				update: vi.fn().mockResolvedValue(undefined),
+			},
+			extension: {
+				packageJSON: {
+					version: "1.0.0",
+					publisher: "RooVeterinaryInc",
+					name: "roo-cline",
+				},
+			},
+		}
+
+		// Setup timer mock
+		mockTimer = {
+			start: vi.fn(),
+			stop: vi.fn(),
+			reset: vi.fn(),
+		}
+		const MockedRefreshTimer = vi.mocked(RefreshTimer)
+		MockedRefreshTimer.mockImplementation(() => mockTimer as unknown as RefreshTimer)
+
+		// Setup config mocks - use production URL by default to maintain existing test behavior
+		vi.mocked(getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com")
+		vi.mocked(getRooCodeApiUrl).mockReturnValue("https://api.test.com")
+
+		// Setup utils mock
+		vi.mocked(getUserAgent).mockReturnValue("Roo-Code 1.0.0")
+
+		// Setup crypto mock
+		vi.mocked(crypto.randomBytes).mockReturnValue(Buffer.from("test-random-bytes") as never)
+
+		// Setup log mock
+		mockLog = vi.fn()
+
+		authService = new WebAuthService(mockContext as unknown as ExtensionContext, mockLog)
+	})
+
+	afterEach(() => {
+		vi.clearAllMocks()
+	})
+
+	describe("constructor", () => {
+		it("should initialize with correct default values", () => {
+			expect(authService.getState()).toBe("initializing")
+			expect(authService.isAuthenticated()).toBe(false)
+			expect(authService.hasActiveSession()).toBe(false)
+			expect(authService.getSessionToken()).toBeUndefined()
+			expect(authService.getUserInfo()).toBeNull()
+		})
+
+		it("should create RefreshTimer with correct configuration", () => {
+			expect(RefreshTimer).toHaveBeenCalledWith({
+				callback: expect.any(Function),
+				successInterval: 50_000,
+				initialBackoffMs: 1_000,
+				maxBackoffMs: 300_000,
+			})
+		})
+
+		it("should use console.log as default logger", () => {
+			const serviceWithoutLog = new WebAuthService(mockContext as unknown as ExtensionContext)
+			// Can't directly test console.log usage, but constructor should not throw
+			expect(serviceWithoutLog).toBeInstanceOf(WebAuthService)
+		})
+	})
+
+	describe("initialize", () => {
+		it("should handle credentials change and setup event listener", async () => {
+			await authService.initialize()
+
+			expect(mockContext.subscriptions.push).toHaveBeenCalled()
+			expect(mockContext.secrets.onDidChange).toHaveBeenCalled()
+		})
+
+		it("should not initialize twice", async () => {
+			await authService.initialize()
+			const firstCallCount = vi.mocked(mockContext.secrets.onDidChange).mock.calls.length
+
+			await authService.initialize()
+			expect(mockContext.secrets.onDidChange).toHaveBeenCalledTimes(firstCallCount)
+			expect(mockLog).toHaveBeenCalledWith("[auth] initialize() called after already initialized")
+		})
+
+		it("should transition to logged-out when no credentials exist", async () => {
+			mockContext.secrets.get.mockResolvedValue(undefined)
+
+			const authStateChangedSpy = vi.fn()
+			authService.on("auth-state-changed", authStateChangedSpy)
+
+			await authService.initialize()
+
+			expect(authService.getState()).toBe("logged-out")
+			expect(authStateChangedSpy).toHaveBeenCalledWith({
+				state: "logged-out",
+				previousState: "initializing",
+			})
+		})
+
+		it("should transition to attempting-session when valid credentials exist", async () => {
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+
+			const authStateChangedSpy = vi.fn()
+			authService.on("auth-state-changed", authStateChangedSpy)
+
+			await authService.initialize()
+
+			expect(authService.getState()).toBe("attempting-session")
+			expect(authStateChangedSpy).toHaveBeenCalledWith({
+				state: "attempting-session",
+				previousState: "initializing",
+			})
+			expect(mockTimer.start).toHaveBeenCalled()
+		})
+
+		it("should handle invalid credentials gracefully", async () => {
+			mockContext.secrets.get.mockResolvedValue("invalid-json")
+
+			const authStateChangedSpy = vi.fn()
+			authService.on("auth-state-changed", authStateChangedSpy)
+
+			await authService.initialize()
+
+			expect(authService.getState()).toBe("logged-out")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Failed to parse stored credentials:", expect.any(Error))
+		})
+
+		it("should handle credentials change events", async () => {
+			let onDidChangeCallback: (e: { key: string }) => void
+
+			mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => {
+				onDidChangeCallback = callback
+				return { dispose: vi.fn() }
+			})
+
+			await authService.initialize()
+
+			// Simulate credentials change event
+			const newCredentials = {
+				clientToken: "new-token",
+				sessionId: "new-session",
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(newCredentials))
+
+			const authStateChangedSpy = vi.fn()
+			authService.on("auth-state-changed", authStateChangedSpy)
+
+			onDidChangeCallback!({ key: "clerk-auth-credentials" })
+			await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling
+
+			expect(authStateChangedSpy).toHaveBeenCalled()
+		})
+	})
+
+	describe("login", () => {
+		beforeEach(async () => {
+			await authService.initialize()
+		})
+
+		it("should generate state and open external URL", async () => {
+			const mockOpenExternal = vi.fn()
+			const vscode = await import("vscode")
+			vi.mocked(vscode.env.openExternal).mockImplementation(mockOpenExternal)
+
+			await authService.login()
+
+			expect(crypto.randomBytes).toHaveBeenCalledWith(16)
+
+			expect(mockContext.globalState.update).toHaveBeenCalledWith(
+				"clerk-auth-state",
+				"746573742d72616e646f6d2d6279746573",
+			)
+
+			expect(mockOpenExternal).toHaveBeenCalledWith(
+				expect.objectContaining({
+					toString: expect.any(Function),
+				}),
+			)
+		})
+
+		it("should use package.json values for redirect URI", async () => {
+			const mockOpenExternal = vi.fn()
+			const vscode = await import("vscode")
+			vi.mocked(vscode.env.openExternal).mockImplementation(mockOpenExternal)
+
+			await authService.login()
+
+			const expectedUrl =
+				"https://api.test.com/extension/sign-in?state=746573742d72616e646f6d2d6279746573&auth_redirect=vscode%3A%2F%2FRooVeterinaryInc.roo-cline"
+			expect(mockOpenExternal).toHaveBeenCalledWith(
+				expect.objectContaining({
+					toString: expect.any(Function),
+				}),
+			)
+
+			// Verify the actual URL
+			const calledUri = mockOpenExternal.mock.calls[0]?.[0]
+			expect(calledUri.toString()).toBe(expectedUrl)
+		})
+
+		it("should handle errors during login", async () => {
+			vi.mocked(crypto.randomBytes).mockImplementation(() => {
+				throw new Error("Crypto error")
+			})
+
+			await expect(authService.login()).rejects.toThrow("Failed to initiate Roo Code Cloud authentication")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Error initiating Roo Code Cloud auth: Error: Crypto error")
+		})
+	})
+
+	describe("handleCallback", () => {
+		beforeEach(async () => {
+			await authService.initialize()
+		})
+
+		it("should handle invalid parameters", async () => {
+			const vscode = await import("vscode")
+			const mockShowInfo = vi.fn()
+			vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo)
+
+			await authService.handleCallback(null, "state")
+			expect(mockShowInfo).toHaveBeenCalledWith("Invalid Roo Code Cloud sign in url")
+
+			await authService.handleCallback("code", null)
+			expect(mockShowInfo).toHaveBeenCalledWith("Invalid Roo Code Cloud sign in url")
+		})
+
+		it("should validate state parameter", async () => {
+			mockContext.globalState.get.mockReturnValue("stored-state")
+
+			await expect(authService.handleCallback("code", "different-state")).rejects.toThrow(
+				"Failed to handle Roo Code Cloud callback",
+			)
+			expect(mockLog).toHaveBeenCalledWith("[auth] State mismatch in callback")
+		})
+
+		it("should successfully handle valid callback", async () => {
+			const storedState = "valid-state"
+			mockContext.globalState.get.mockReturnValue(storedState)
+
+			// Mock successful Clerk sign-in response
+			const mockResponse = {
+				ok: true,
+				json: () =>
+					Promise.resolve({
+						response: { created_session_id: "session-123" },
+					}),
+				headers: {
+					get: (header: string) => (header === "authorization" ? "Bearer token-123" : null),
+				},
+			}
+			mockFetch.mockResolvedValue(mockResponse)
+
+			const vscode = await import("vscode")
+			const mockShowInfo = vi.fn()
+			vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo)
+
+			await authService.handleCallback("auth-code", storedState)
+
+			expect(mockContext.secrets.store).toHaveBeenCalledWith(
+				"clerk-auth-credentials",
+				JSON.stringify({
+					clientToken: "Bearer token-123",
+					sessionId: "session-123",
+					organizationId: null,
+				}),
+			)
+			expect(mockShowInfo).toHaveBeenCalledWith("Successfully authenticated with Roo Code Cloud")
+		})
+
+		it("should handle Clerk API errors", async () => {
+			const storedState = "valid-state"
+			mockContext.globalState.get.mockReturnValue(storedState)
+
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 400,
+				statusText: "Bad Request",
+			})
+
+			const authStateChangedSpy = vi.fn()
+			authService.on("auth-state-changed", authStateChangedSpy)
+
+			await expect(authService.handleCallback("auth-code", storedState)).rejects.toThrow(
+				"Failed to handle Roo Code Cloud callback",
+			)
+			expect(authStateChangedSpy).toHaveBeenCalled()
+		})
+	})
+
+	describe("logout", () => {
+		beforeEach(async () => {
+			await authService.initialize()
+		})
+
+		it("should clear credentials and call Clerk logout", async () => {
+			// Set up credentials first by simulating a login state
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+
+			// Manually set the credentials in the service
+			authService["credentials"] = credentials
+
+			// Mock successful logout response
+			mockFetch.mockResolvedValue({ ok: true })
+
+			const vscode = await import("vscode")
+			const mockShowInfo = vi.fn()
+			vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo)
+
+			await authService.logout()
+
+			expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials")
+			expect(mockContext.globalState.update).toHaveBeenCalledWith("clerk-auth-state", undefined)
+			expect(mockFetch).toHaveBeenCalledWith(
+				"https://clerk.roocode.com/v1/client/sessions/test-session/remove",
+				expect.objectContaining({
+					method: "POST",
+					headers: expect.objectContaining({
+						Authorization: "Bearer test-token",
+					}),
+				}),
+			)
+			expect(mockShowInfo).toHaveBeenCalledWith("Logged out from Roo Code Cloud")
+		})
+
+		it("should handle logout without credentials", async () => {
+			const vscode = await import("vscode")
+			const mockShowInfo = vi.fn()
+			vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo)
+
+			await authService.logout()
+
+			expect(mockContext.secrets.delete).toHaveBeenCalled()
+			expect(mockFetch).not.toHaveBeenCalled()
+			expect(mockShowInfo).toHaveBeenCalledWith("Logged out from Roo Code Cloud")
+		})
+
+		it("should handle Clerk logout errors gracefully", async () => {
+			// Set up credentials first by simulating a login state
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+
+			// Manually set the credentials in the service
+			authService["credentials"] = credentials
+
+			// Mock failed logout response
+			mockFetch.mockRejectedValue(new Error("Network error"))
+
+			const vscode = await import("vscode")
+			const mockShowInfo = vi.fn()
+			vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo)
+
+			await authService.logout()
+
+			expect(mockLog).toHaveBeenCalledWith("[auth] Error calling clerkLogout:", expect.any(Error))
+			expect(mockShowInfo).toHaveBeenCalledWith("Logged out from Roo Code Cloud")
+		})
+	})
+
+	describe("state management", () => {
+		it("should return correct state", () => {
+			expect(authService.getState()).toBe("initializing")
+		})
+
+		it("should return correct authentication status", async () => {
+			await authService.initialize()
+			expect(authService.isAuthenticated()).toBe(false)
+
+			// Create a new service instance with credentials
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+
+			const authenticatedService = new WebAuthService(mockContext as unknown as ExtensionContext, mockLog)
+			await authenticatedService.initialize()
+
+			expect(authenticatedService.isAuthenticated()).toBe(true)
+			expect(authenticatedService.hasActiveSession()).toBe(false)
+		})
+
+		it("should return session token only for active sessions", () => {
+			expect(authService.getSessionToken()).toBeUndefined()
+
+			// Manually set state to active-session for testing
+			// This would normally happen through refreshSession
+			authService["state"] = "active-session"
+			authService["sessionToken"] = "test-jwt"
+
+			expect(authService.getSessionToken()).toBe("test-jwt")
+		})
+
+		it("should return correct values for new methods", async () => {
+			await authService.initialize()
+			expect(authService.hasOrIsAcquiringActiveSession()).toBe(false)
+
+			// Create a new service instance with credentials (attempting-session)
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+
+			const attemptingService = new WebAuthService(mockContext as unknown as ExtensionContext, mockLog)
+			await attemptingService.initialize()
+
+			expect(attemptingService.hasOrIsAcquiringActiveSession()).toBe(true)
+			expect(attemptingService.hasActiveSession()).toBe(false)
+
+			// Manually set state to active-session for testing
+			attemptingService["state"] = "active-session"
+			expect(attemptingService.hasOrIsAcquiringActiveSession()).toBe(true)
+			expect(attemptingService.hasActiveSession()).toBe(true)
+		})
+	})
+
+	describe("session refresh", () => {
+		beforeEach(async () => {
+			// Set up with credentials
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+			await authService.initialize()
+		})
+
+		it("should refresh session successfully", async () => {
+			// Mock successful token creation and user info fetch
+			mockFetch
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () => Promise.resolve({ jwt: "new-jwt-token" }),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: {
+								first_name: "John",
+								last_name: "Doe",
+								image_url: "https://example.com/avatar.jpg",
+								primary_email_address_id: "email-1",
+								email_addresses: [{ id: "email-1", email_address: "[email protected]" }],
+							},
+						}),
+				})
+
+			const authStateChangedSpy = vi.fn()
+			const userInfoSpy = vi.fn()
+			authService.on("auth-state-changed", authStateChangedSpy)
+			authService.on("user-info", userInfoSpy)
+
+			// Trigger refresh by calling the timer callback
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+			await timerCallback?.()
+
+			// Wait for async operations to complete
+			await new Promise((resolve) => setTimeout(resolve, 0))
+
+			expect(authService.getState()).toBe("active-session")
+			expect(authService.hasActiveSession()).toBe(true)
+			expect(authService.getSessionToken()).toBe("new-jwt-token")
+			expect(authStateChangedSpy).toHaveBeenCalledWith({
+				state: "active-session",
+				previousState: "attempting-session",
+			})
+			expect(userInfoSpy).toHaveBeenCalledWith({
+				userInfo: {
+					id: undefined,
+					name: "John Doe",
+					email: "[email protected]",
+					picture: "https://example.com/avatar.jpg",
+					extensionBridgeEnabled: false,
+				},
+			})
+		})
+
+		it("should handle invalid client token error", async () => {
+			// Mock 401 response (invalid token)
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 401,
+				statusText: "Unauthorized",
+			})
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+
+			await expect(timerCallback?.()).rejects.toThrow()
+			expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Invalid/Expired client token: clearing credentials")
+		})
+
+		it("should handle network errors during refresh", async () => {
+			mockFetch.mockRejectedValue(new Error("Network error"))
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+
+			await expect(timerCallback?.()).rejects.toThrow("Network error")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Failed to refresh session", expect.any(Error))
+		})
+
+		it("should transition to inactive-session on first attempt failure", async () => {
+			// Mock failed token creation response
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 500,
+				statusText: "Internal Server Error",
+			})
+
+			const authStateChangedSpy = vi.fn()
+			authService.on("auth-state-changed", authStateChangedSpy)
+
+			// Verify we start in attempting-session state
+			expect(authService.getState()).toBe("attempting-session")
+			expect(authService["isFirstRefreshAttempt"]).toBe(true)
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+
+			await expect(timerCallback?.()).rejects.toThrow()
+
+			// Should transition to inactive-session after first failure
+			expect(authService.getState()).toBe("inactive-session")
+			expect(authService["isFirstRefreshAttempt"]).toBe(false)
+			expect(authStateChangedSpy).toHaveBeenCalledWith({
+				state: "inactive-session",
+				previousState: "attempting-session",
+			})
+		})
+
+		it("should not transition to inactive-session on subsequent failures", async () => {
+			// First, transition to inactive-session by failing the first attempt
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 500,
+				statusText: "Internal Server Error",
+			})
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+			await expect(timerCallback?.()).rejects.toThrow()
+
+			// Verify we're now in inactive-session
+			expect(authService.getState()).toBe("inactive-session")
+			expect(authService["isFirstRefreshAttempt"]).toBe(false)
+
+			const authStateChangedSpy = vi.fn()
+			authService.on("auth-state-changed", authStateChangedSpy)
+
+			// Subsequent failure should not trigger another transition
+			await expect(timerCallback?.()).rejects.toThrow()
+
+			expect(authService.getState()).toBe("inactive-session")
+			expect(authStateChangedSpy).not.toHaveBeenCalled()
+		})
+
+		it("should clear credentials on 401 during first refresh attempt (bug fix)", async () => {
+			// Mock 401 response during first refresh attempt
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 401,
+				statusText: "Unauthorized",
+			})
+
+			const authStateChangedSpy = vi.fn()
+			authService.on("auth-state-changed", authStateChangedSpy)
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+			await expect(timerCallback?.()).rejects.toThrow()
+
+			// Should clear credentials (not just transition to inactive-session)
+			expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Invalid/Expired client token: clearing credentials")
+
+			// Simulate credentials cleared event
+			mockContext.secrets.get.mockResolvedValue(undefined)
+			await authService["handleCredentialsChange"]()
+
+			expect(authService.getState()).toBe("logged-out")
+			expect(authStateChangedSpy).toHaveBeenCalledWith({
+				state: "logged-out",
+				previousState: "attempting-session",
+			})
+		})
+	})
+
+	describe("user info", () => {
+		it("should return null initially", () => {
+			expect(authService.getUserInfo()).toBeNull()
+		})
+
+		it("should parse user info correctly for personal accounts", async () => {
+			// Set up with credentials for personal account (no organizationId)
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+				organizationId: null,
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+			await authService.initialize()
+
+			// Clear previous mock calls
+			mockFetch.mockClear()
+
+			// Mock successful responses
+			mockFetch
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () => Promise.resolve({ jwt: "jwt-token" }),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: {
+								first_name: "Jane",
+								last_name: "Smith",
+								image_url: "https://example.com/jane.jpg",
+								primary_email_address_id: "email-2",
+								email_addresses: [
+									{ id: "email-1", email_address: "[email protected]" },
+									{ id: "email-2", email_address: "[email protected]" },
+								],
+							},
+						}),
+				})
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+			await timerCallback?.()
+
+			// Wait for async operations to complete
+			await new Promise((resolve) => setTimeout(resolve, 0))
+
+			const userInfo = authService.getUserInfo()
+			expect(userInfo).toEqual({
+				id: undefined,
+				name: "Jane Smith",
+				email: "[email protected]",
+				picture: "https://example.com/jane.jpg",
+				extensionBridgeEnabled: false,
+			})
+		})
+
+		it("should parse user info correctly for organization accounts", async () => {
+			// Set up with credentials for organization account
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+				organizationId: "org_1",
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+			await authService.initialize()
+
+			// Clear previous mock calls
+			mockFetch.mockClear()
+
+			// Mock successful responses
+			mockFetch
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () => Promise.resolve({ jwt: "jwt-token" }),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: {
+								first_name: "Jane",
+								last_name: "Smith",
+								image_url: "https://example.com/jane.jpg",
+								primary_email_address_id: "email-2",
+								email_addresses: [
+									{ id: "email-1", email_address: "[email protected]" },
+									{ id: "email-2", email_address: "[email protected]" },
+								],
+							},
+						}),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: [
+								{
+									id: "org_member_id_1",
+									role: "member",
+									organization: {
+										id: "org_1",
+										name: "Org 1",
+									},
+								},
+							],
+						}),
+				})
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+			await timerCallback?.()
+
+			// Wait for async operations to complete
+			await new Promise((resolve) => setTimeout(resolve, 0))
+
+			const userInfo = authService.getUserInfo()
+			expect(userInfo).toEqual({
+				id: undefined,
+				name: "Jane Smith",
+				email: "[email protected]",
+				picture: "https://example.com/jane.jpg",
+				extensionBridgeEnabled: false,
+				organizationId: "org_1",
+				organizationName: "Org 1",
+				organizationRole: "member",
+				organizationImageUrl: undefined,
+			})
+		})
+
+		it("should handle missing user info fields", async () => {
+			// Set up with credentials for personal account (no organizationId)
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+				organizationId: null,
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+			await authService.initialize()
+
+			// Clear previous mock calls
+			mockFetch.mockClear()
+
+			// Mock responses with minimal data
+			mockFetch
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () => Promise.resolve({ jwt: "jwt-token" }),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: {
+								first_name: "John",
+								last_name: "Doe",
+							},
+						}),
+				})
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+
+			await timerCallback?.()
+
+			// Wait for async operations to complete
+			await new Promise((resolve) => setTimeout(resolve, 0))
+
+			const userInfo = authService.getUserInfo()
+			expect(userInfo).toEqual({
+				id: undefined,
+				name: "John Doe",
+				email: undefined,
+				picture: undefined,
+				extensionBridgeEnabled: false,
+			})
+		})
+	})
+
+	describe("event emissions", () => {
+		it("should emit auth-state-changed event for logged-out", async () => {
+			const authStateChangedSpy = vi.fn()
+			authService.on("auth-state-changed", authStateChangedSpy)
+
+			await authService.initialize()
+
+			expect(authStateChangedSpy).toHaveBeenCalledWith({
+				state: "logged-out",
+				previousState: "initializing",
+			})
+		})
+
+		it("should emit auth-state-changed event for attempting-session", async () => {
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+
+			const authStateChangedSpy = vi.fn()
+			authService.on("auth-state-changed", authStateChangedSpy)
+
+			await authService.initialize()
+
+			expect(authStateChangedSpy).toHaveBeenCalledWith({
+				state: "attempting-session",
+				previousState: "initializing",
+			})
+		})
+
+		it("should emit auth-state-changed event for active-session", async () => {
+			// Set up with credentials
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+			await authService.initialize()
+
+			// Clear previous mock calls
+			mockFetch.mockClear()
+
+			// Mock both the token creation and user info fetch
+			mockFetch
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () => Promise.resolve({ jwt: "jwt-token" }),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: {
+								first_name: "Test",
+								last_name: "User",
+							},
+						}),
+				})
+
+			const authStateChangedSpy = vi.fn()
+			authService.on("auth-state-changed", authStateChangedSpy)
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+
+			await timerCallback?.()
+
+			// Wait for async operations to complete
+			await new Promise((resolve) => setTimeout(resolve, 0))
+
+			expect(authStateChangedSpy).toHaveBeenCalledWith({
+				state: "active-session",
+				previousState: "attempting-session",
+			})
+		})
+
+		it("should emit user-info event", async () => {
+			// Set up with credentials
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+			await authService.initialize()
+
+			// Clear previous mock calls
+			mockFetch.mockClear()
+
+			mockFetch
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () => Promise.resolve({ jwt: "jwt-token" }),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: {
+								first_name: "Test",
+								last_name: "User",
+							},
+						}),
+				})
+
+			const userInfoSpy = vi.fn()
+			authService.on("user-info", userInfoSpy)
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0]?.[0]?.callback
+
+			await timerCallback?.()
+
+			// Wait for async operations to complete
+			await new Promise((resolve) => setTimeout(resolve, 0))
+
+			expect(userInfoSpy).toHaveBeenCalledWith({
+				userInfo: {
+					id: undefined,
+					name: "Test User",
+					email: undefined,
+					picture: undefined,
+					extensionBridgeEnabled: false,
+				},
+			})
+		})
+	})
+
+	describe("error handling", () => {
+		it("should handle credentials change errors", async () => {
+			mockContext.secrets.get.mockRejectedValue(new Error("Storage error"))
+
+			await authService.initialize()
+
+			expect(mockLog).toHaveBeenCalledWith("[auth] Error handling credentials change:", expect.any(Error))
+		})
+
+		it("should handle malformed JSON in credentials", async () => {
+			mockContext.secrets.get.mockResolvedValue("invalid-json{")
+
+			await authService.initialize()
+
+			expect(authService.getState()).toBe("logged-out")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Failed to parse stored credentials:", expect.any(Error))
+		})
+
+		it("should handle invalid credentials schema", async () => {
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify({ invalid: "data" }))
+
+			await authService.initialize()
+
+			expect(authService.getState()).toBe("logged-out")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Invalid credentials format:", expect.any(Array))
+		})
+
+		it("should handle missing authorization header in sign-in response", async () => {
+			const storedState = "valid-state"
+			mockContext.globalState.get.mockReturnValue(storedState)
+
+			mockFetch.mockResolvedValue({
+				ok: true,
+				json: () =>
+					Promise.resolve({
+						response: { created_session_id: "session-123" },
+					}),
+				headers: {
+					get: () => null, // No authorization header
+				},
+			})
+
+			await expect(authService.handleCallback("auth-code", storedState)).rejects.toThrow(
+				"Failed to handle Roo Code Cloud callback",
+			)
+		})
+	})
+
+	describe("timer integration", () => {
+		it("should stop timer on logged-out transition", async () => {
+			await authService.initialize()
+
+			expect(mockTimer.stop).toHaveBeenCalled()
+		})
+
+		it("should start timer on attempting-session transition", async () => {
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+
+			await authService.initialize()
+
+			expect(mockTimer.start).toHaveBeenCalled()
+		})
+	})
+
+	describe("auth credentials key scoping", () => {
+		it("should use default key when getClerkBaseUrl returns production URL", async () => {
+			// Mock getClerkBaseUrl to return production URL
+			vi.mocked(getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com")
+
+			const service = new WebAuthService(mockContext as unknown as ExtensionContext, mockLog)
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+
+			await service.initialize()
+			await service["storeCredentials"](credentials)
+
+			expect(mockContext.secrets.store).toHaveBeenCalledWith(
+				"clerk-auth-credentials",
+				JSON.stringify(credentials),
+			)
+		})
+
+		it("should use scoped key when getClerkBaseUrl returns custom URL", async () => {
+			const customUrl = "https://custom.clerk.com"
+			// Mock getClerkBaseUrl to return custom URL
+			vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl)
+
+			const service = new WebAuthService(mockContext as unknown as ExtensionContext, mockLog)
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+
+			await service.initialize()
+			await service["storeCredentials"](credentials)
+
+			expect(mockContext.secrets.store).toHaveBeenCalledWith(
+				`clerk-auth-credentials-${customUrl}`,
+				JSON.stringify(credentials),
+			)
+		})
+
+		it("should load credentials using scoped key", async () => {
+			const customUrl = "https://custom.clerk.com"
+			vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl)
+
+			const service = new WebAuthService(mockContext as unknown as ExtensionContext, mockLog)
+			const credentials = {
+				clientToken: "test-token",
+				sessionId: "test-session",
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+
+			await service.initialize()
+			const loadedCredentials = await service["loadCredentials"]()
+
+			expect(mockContext.secrets.get).toHaveBeenCalledWith(`clerk-auth-credentials-${customUrl}`)
+			expect(loadedCredentials).toEqual(credentials)
+		})
+
+		it("should clear credentials using scoped key", async () => {
+			const customUrl = "https://custom.clerk.com"
+			vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl)
+
+			const service = new WebAuthService(mockContext as unknown as ExtensionContext, mockLog)
+
+			await service.initialize()
+			await service["clearCredentials"]()
+
+			expect(mockContext.secrets.delete).toHaveBeenCalledWith(`clerk-auth-credentials-${customUrl}`)
+		})
+
+		it("should listen for changes on scoped key", async () => {
+			const customUrl = "https://custom.clerk.com"
+			vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl)
+
+			let onDidChangeCallback: (e: { key: string }) => void
+
+			mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => {
+				onDidChangeCallback = callback
+				return { dispose: vi.fn() }
+			})
+
+			const service = new WebAuthService(mockContext as unknown as ExtensionContext, mockLog)
+			await service.initialize()
+
+			// Simulate credentials change event with scoped key
+			const newCredentials = {
+				clientToken: "new-token",
+				sessionId: "new-session",
+			}
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(newCredentials))
+
+			const authStateChangedSpy = vi.fn()
+			service.on("auth-state-changed", authStateChangedSpy)
+
+			onDidChangeCallback!({ key: `clerk-auth-credentials-${customUrl}` })
+			await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling
+
+			expect(authStateChangedSpy).toHaveBeenCalled()
+		})
+
+		it("should not respond to changes on different scoped keys", async () => {
+			const customUrl = "https://custom.clerk.com"
+			vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl)
+
+			let onDidChangeCallback: (e: { key: string }) => void
+
+			mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => {
+				onDidChangeCallback = callback
+				return { dispose: vi.fn() }
+			})
+
+			const service = new WebAuthService(mockContext as unknown as ExtensionContext, mockLog)
+			await service.initialize()
+
+			const authStateChangedSpy = vi.fn()
+			service.on("auth-state-changed", authStateChangedSpy)
+
+			// Simulate credentials change event with different scoped key
+			onDidChangeCallback!({
+				key: "clerk-auth-credentials-https://other.clerk.com",
+			})
+			await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling
+
+			expect(authStateChangedSpy).not.toHaveBeenCalled()
+		})
+
+		it("should not respond to changes on default key when using scoped key", async () => {
+			const customUrl = "https://custom.clerk.com"
+			vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl)
+
+			let onDidChangeCallback: (e: { key: string }) => void
+
+			mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => {
+				onDidChangeCallback = callback
+				return { dispose: vi.fn() }
+			})
+
+			const service = new WebAuthService(mockContext as unknown as ExtensionContext, mockLog)
+			await service.initialize()
+
+			const authStateChangedSpy = vi.fn()
+			service.on("auth-state-changed", authStateChangedSpy)
+
+			// Simulate credentials change event with default key
+			onDidChangeCallback!({ key: "clerk-auth-credentials" })
+			await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling
+
+			expect(authStateChangedSpy).not.toHaveBeenCalled()
+		})
+	})
+})

+ 290 - 0
packages/cloud/src/bridge/ExtensionBridgeService.ts

@@ -0,0 +1,290 @@
+import crypto from "crypto"
+
+import {
+	type TaskProviderLike,
+	type TaskLike,
+	type CloudUserInfo,
+	type ExtensionBridgeCommand,
+	type TaskBridgeCommand,
+	ConnectionState,
+	ExtensionSocketEvents,
+	TaskSocketEvents,
+} from "@roo-code/types"
+
+import { SocketConnectionManager } from "./SocketConnectionManager.js"
+import { ExtensionManager } from "./ExtensionManager.js"
+import { TaskManager } from "./TaskManager.js"
+
+export interface ExtensionBridgeServiceOptions {
+	userId: string
+	socketBridgeUrl: string
+	token: string
+	provider: TaskProviderLike
+	sessionId?: string
+}
+
+export class ExtensionBridgeService {
+	private static instance: ExtensionBridgeService | null = null
+
+	// Core
+	private readonly userId: string
+	private readonly socketBridgeUrl: string
+	private readonly token: string
+	private readonly provider: TaskProviderLike
+	private readonly instanceId: string
+
+	// Managers
+	private connectionManager: SocketConnectionManager
+	private extensionManager: ExtensionManager
+	private taskManager: TaskManager
+
+	// Reconnection
+	private readonly MAX_RECONNECT_ATTEMPTS = Infinity
+	private readonly RECONNECT_DELAY = 1_000
+	private readonly RECONNECT_DELAY_MAX = 30_000
+
+	public static getInstance(): ExtensionBridgeService | null {
+		return ExtensionBridgeService.instance
+	}
+
+	public static async createInstance(options: ExtensionBridgeServiceOptions) {
+		console.log("[ExtensionBridgeService] createInstance")
+		ExtensionBridgeService.instance = new ExtensionBridgeService(options)
+		await ExtensionBridgeService.instance.initialize()
+		return ExtensionBridgeService.instance
+	}
+
+	public static resetInstance() {
+		if (ExtensionBridgeService.instance) {
+			console.log("[ExtensionBridgeService] resetInstance")
+			ExtensionBridgeService.instance.disconnect().catch(() => {})
+			ExtensionBridgeService.instance = null
+		}
+	}
+
+	public static async handleRemoteControlState(
+		userInfo: CloudUserInfo | null,
+		remoteControlEnabled: boolean | undefined,
+		options: ExtensionBridgeServiceOptions,
+		logger?: (message: string) => void,
+	) {
+		if (userInfo?.extensionBridgeEnabled && remoteControlEnabled) {
+			const existingService = ExtensionBridgeService.getInstance()
+
+			if (!existingService) {
+				try {
+					const service = await ExtensionBridgeService.createInstance(options)
+					const state = service.getConnectionState()
+
+					logger?.(`[ExtensionBridgeService#handleRemoteControlState] Instance created (state: ${state})`)
+
+					if (state !== ConnectionState.CONNECTED) {
+						logger?.(
+							`[ExtensionBridgeService#handleRemoteControlState] Service is not connected yet, will retry in background`,
+						)
+					}
+				} catch (error) {
+					const message = `[ExtensionBridgeService#handleRemoteControlState] Failed to create instance: ${
+						error instanceof Error ? error.message : String(error)
+					}`
+
+					logger?.(message)
+					console.error(message)
+				}
+			} else {
+				const state = existingService.getConnectionState()
+
+				if (state === ConnectionState.FAILED || state === ConnectionState.DISCONNECTED) {
+					logger?.(
+						`[ExtensionBridgeService#handleRemoteControlState] Existing service is ${state}, attempting reconnection`,
+					)
+
+					existingService.reconnect().catch((error) => {
+						const message = `[ExtensionBridgeService#handleRemoteControlState] Reconnection failed: ${
+							error instanceof Error ? error.message : String(error)
+						}`
+
+						logger?.(message)
+						console.error(message)
+					})
+				}
+			}
+		} else {
+			const existingService = ExtensionBridgeService.getInstance()
+
+			if (existingService) {
+				try {
+					await existingService.disconnect()
+					ExtensionBridgeService.resetInstance()
+
+					logger?.(`[ExtensionBridgeService#handleRemoteControlState] Service disconnected and reset`)
+				} catch (error) {
+					const message = `[ExtensionBridgeService#handleRemoteControlState] Failed to disconnect and reset instance: ${
+						error instanceof Error ? error.message : String(error)
+					}`
+
+					logger?.(message)
+					console.error(message)
+				}
+			}
+		}
+	}
+
+	private constructor(options: ExtensionBridgeServiceOptions) {
+		this.userId = options.userId
+		this.socketBridgeUrl = options.socketBridgeUrl
+		this.token = options.token
+		this.provider = options.provider
+		this.instanceId = options.sessionId || crypto.randomUUID()
+
+		this.connectionManager = new SocketConnectionManager({
+			url: this.socketBridgeUrl,
+			socketOptions: {
+				query: {
+					token: this.token,
+					clientType: "extension",
+					instanceId: this.instanceId,
+				},
+				transports: ["websocket", "polling"],
+				reconnection: true,
+				reconnectionAttempts: this.MAX_RECONNECT_ATTEMPTS,
+				reconnectionDelay: this.RECONNECT_DELAY,
+				reconnectionDelayMax: this.RECONNECT_DELAY_MAX,
+			},
+			onConnect: () => this.handleConnect(),
+			onDisconnect: () => this.handleDisconnect(),
+			onReconnect: () => this.handleReconnect(),
+		})
+
+		this.extensionManager = new ExtensionManager(this.instanceId, this.userId, this.provider)
+
+		this.taskManager = new TaskManager()
+	}
+
+	private async initialize() {
+		// Populate the app and git properties before registering the instance.
+		await this.provider.getTelemetryProperties()
+
+		await this.connectionManager.connect()
+		this.setupSocketListeners()
+	}
+
+	private setupSocketListeners() {
+		const socket = this.connectionManager.getSocket()
+
+		if (!socket) {
+			console.error("[ExtensionBridgeService] Socket not available")
+			return
+		}
+
+		// Remove any existing listeners first to prevent duplicates.
+		socket.off(ExtensionSocketEvents.RELAYED_COMMAND)
+		socket.off(TaskSocketEvents.RELAYED_COMMAND)
+		socket.off("connected")
+
+		socket.on(ExtensionSocketEvents.RELAYED_COMMAND, (message: ExtensionBridgeCommand) => {
+			console.log(
+				`[ExtensionBridgeService] on(${ExtensionSocketEvents.RELAYED_COMMAND}) -> ${message.type} for ${message.instanceId}`,
+			)
+
+			this.extensionManager?.handleExtensionCommand(message)
+		})
+
+		socket.on(TaskSocketEvents.RELAYED_COMMAND, (message: TaskBridgeCommand) => {
+			console.log(
+				`[ExtensionBridgeService] on(${TaskSocketEvents.RELAYED_COMMAND}) -> ${message.type} for ${message.taskId}`,
+			)
+
+			this.taskManager.handleTaskCommand(message)
+		})
+	}
+
+	private async handleConnect() {
+		const socket = this.connectionManager.getSocket()
+
+		if (!socket) {
+			console.error("[ExtensionBridgeService] Socket not available after connect")
+
+			return
+		}
+
+		await this.extensionManager.onConnect(socket)
+		await this.taskManager.onConnect(socket)
+	}
+
+	private handleDisconnect() {
+		this.extensionManager.onDisconnect()
+		this.taskManager.onDisconnect()
+	}
+
+	private async handleReconnect() {
+		const socket = this.connectionManager.getSocket()
+
+		if (!socket) {
+			console.error("[ExtensionBridgeService] Socket not available after reconnect")
+
+			return
+		}
+
+		// Re-setup socket listeners to ensure they're properly configured
+		// after automatic reconnection (Socket.IO's built-in reconnection)
+		// The socket.off() calls in setupSocketListeners prevent duplicates
+		this.setupSocketListeners()
+
+		await this.extensionManager.onReconnect(socket)
+		await this.taskManager.onReconnect(socket)
+	}
+
+	// Task API
+
+	public async subscribeToTask(task: TaskLike): Promise<void> {
+		const socket = this.connectionManager.getSocket()
+
+		if (!socket || !this.connectionManager.isConnected()) {
+			console.warn("[ExtensionBridgeService] Cannot subscribe to task: not connected. Will retry when connected.")
+
+			this.taskManager.addPendingTask(task)
+
+			const state = this.connectionManager.getConnectionState()
+
+			if (state === ConnectionState.DISCONNECTED || state === ConnectionState.FAILED) {
+				this.initialize()
+			}
+
+			return
+		}
+
+		await this.taskManager.subscribeToTask(task, socket)
+	}
+
+	public async unsubscribeFromTask(taskId: string): Promise<void> {
+		const socket = this.connectionManager.getSocket()
+
+		if (!socket) {
+			return
+		}
+
+		await this.taskManager.unsubscribeFromTask(taskId, socket)
+	}
+
+	// Shared API
+
+	public getConnectionState(): ConnectionState {
+		return this.connectionManager.getConnectionState()
+	}
+
+	public async disconnect(): Promise<void> {
+		await this.extensionManager.cleanup(this.connectionManager.getSocket())
+		await this.taskManager.cleanup(this.connectionManager.getSocket())
+		await this.connectionManager.disconnect()
+		ExtensionBridgeService.instance = null
+	}
+
+	public async reconnect(): Promise<void> {
+		await this.connectionManager.reconnect()
+
+		// After a manual reconnect, we have a new socket instance
+		// so we need to set up listeners again.
+		this.setupSocketListeners()
+	}
+}

+ 297 - 0
packages/cloud/src/bridge/ExtensionManager.ts

@@ -0,0 +1,297 @@
+import type { Socket } from "socket.io-client"
+
+import {
+	type TaskProviderLike,
+	type ExtensionInstance,
+	type ExtensionBridgeCommand,
+	type ExtensionBridgeEvent,
+	RooCodeEventName,
+	TaskStatus,
+	ExtensionBridgeCommandName,
+	ExtensionBridgeEventName,
+	ExtensionSocketEvents,
+	HEARTBEAT_INTERVAL_MS,
+} from "@roo-code/types"
+
+export class ExtensionManager {
+	private instanceId: string
+	private userId: string
+	private provider: TaskProviderLike
+	private extensionInstance: ExtensionInstance
+	private heartbeatInterval: NodeJS.Timeout | null = null
+	private socket: Socket | null = null
+
+	constructor(instanceId: string, userId: string, provider: TaskProviderLike) {
+		this.instanceId = instanceId
+		this.userId = userId
+		this.provider = provider
+
+		this.extensionInstance = {
+			instanceId: this.instanceId,
+			userId: this.userId,
+			workspacePath: this.provider.cwd,
+			appProperties: this.provider.appProperties,
+			gitProperties: this.provider.gitProperties,
+			lastHeartbeat: Date.now(),
+			task: {
+				taskId: "",
+				taskStatus: TaskStatus.None,
+			},
+			taskHistory: [],
+		}
+
+		this.setupListeners()
+	}
+
+	public async onConnect(socket: Socket): Promise<void> {
+		this.socket = socket
+		await this.registerInstance(socket)
+		this.startHeartbeat(socket)
+	}
+
+	public onDisconnect(): void {
+		this.stopHeartbeat()
+		this.socket = null
+	}
+
+	public async onReconnect(socket: Socket): Promise<void> {
+		this.socket = socket
+		await this.registerInstance(socket)
+		this.startHeartbeat(socket)
+	}
+
+	public async cleanup(socket: Socket | null): Promise<void> {
+		this.stopHeartbeat()
+
+		if (socket) {
+			await this.unregisterInstance(socket)
+		}
+
+		this.socket = null
+	}
+
+	public handleExtensionCommand(message: ExtensionBridgeCommand): void {
+		if (message.instanceId !== this.instanceId) {
+			console.log(`[ExtensionManager] command -> instance id mismatch | ${this.instanceId}`, {
+				messageInstanceId: message.instanceId,
+			})
+
+			return
+		}
+
+		switch (message.type) {
+			case ExtensionBridgeCommandName.StartTask: {
+				console.log(`[ExtensionManager] command -> createTask() | ${message.instanceId}`, {
+					text: message.payload.text?.substring(0, 100) + "...",
+					hasImages: !!message.payload.images,
+				})
+
+				this.provider.createTask(message.payload.text, message.payload.images)
+
+				break
+			}
+			case ExtensionBridgeCommandName.StopTask: {
+				const instance = this.updateInstance()
+
+				if (instance.task.taskStatus === TaskStatus.Running) {
+					console.log(`[ExtensionManager] command -> cancelTask() | ${message.instanceId}`)
+
+					this.provider.cancelTask()
+					this.provider.postStateToWebview()
+				} else if (instance.task.taskId) {
+					console.log(`[ExtensionManager] command -> clearTask() | ${message.instanceId}`)
+
+					this.provider.clearTask()
+					this.provider.postStateToWebview()
+				}
+
+				break
+			}
+			case ExtensionBridgeCommandName.ResumeTask: {
+				console.log(`[ExtensionManager] command -> resumeTask() | ${message.instanceId}`, {
+					taskId: message.payload.taskId,
+				})
+
+				// Resume the task from history by taskId
+				this.provider.resumeTask(message.payload.taskId)
+
+				this.provider.postStateToWebview()
+
+				break
+			}
+		}
+	}
+
+	private async registerInstance(socket: Socket): Promise<void> {
+		const instance = this.updateInstance()
+
+		try {
+			socket.emit(ExtensionSocketEvents.REGISTER, instance)
+
+			console.log(
+				`[ExtensionManager] emit() -> ${ExtensionSocketEvents.REGISTER}`,
+				// instance,
+			)
+		} catch (error) {
+			console.error(
+				`[ExtensionManager] emit() failed -> ${ExtensionSocketEvents.REGISTER}: ${
+					error instanceof Error ? error.message : String(error)
+				}`,
+			)
+
+			return
+		}
+	}
+
+	private async unregisterInstance(socket: Socket): Promise<void> {
+		const instance = this.updateInstance()
+
+		try {
+			socket.emit(ExtensionSocketEvents.UNREGISTER, instance)
+
+			console.log(
+				`[ExtensionManager] emit() -> ${ExtensionSocketEvents.UNREGISTER}`,
+				// instance,
+			)
+		} catch (error) {
+			console.error(
+				`[ExtensionManager] emit() failed -> ${ExtensionSocketEvents.UNREGISTER}: ${
+					error instanceof Error ? error.message : String(error)
+				}`,
+			)
+		}
+	}
+
+	private startHeartbeat(socket: Socket): void {
+		this.stopHeartbeat()
+
+		this.heartbeatInterval = setInterval(async () => {
+			const instance = this.updateInstance()
+
+			try {
+				socket.emit(ExtensionSocketEvents.HEARTBEAT, instance)
+
+				// console.log(
+				//   `[ExtensionManager] emit() -> ${ExtensionSocketEvents.HEARTBEAT}`,
+				//   instance,
+				// );
+			} catch (error) {
+				console.error(
+					`[ExtensionManager] emit() failed -> ${ExtensionSocketEvents.HEARTBEAT}: ${
+						error instanceof Error ? error.message : String(error)
+					}`,
+				)
+			}
+		}, HEARTBEAT_INTERVAL_MS)
+	}
+
+	private stopHeartbeat(): void {
+		if (this.heartbeatInterval) {
+			clearInterval(this.heartbeatInterval)
+			this.heartbeatInterval = null
+		}
+	}
+
+	private setupListeners(): void {
+		const eventMapping = [
+			{
+				from: RooCodeEventName.TaskCreated,
+				to: ExtensionBridgeEventName.TaskCreated,
+			},
+			{
+				from: RooCodeEventName.TaskStarted,
+				to: ExtensionBridgeEventName.TaskStarted,
+			},
+			{
+				from: RooCodeEventName.TaskCompleted,
+				to: ExtensionBridgeEventName.TaskCompleted,
+			},
+			{
+				from: RooCodeEventName.TaskAborted,
+				to: ExtensionBridgeEventName.TaskAborted,
+			},
+			{
+				from: RooCodeEventName.TaskFocused,
+				to: ExtensionBridgeEventName.TaskFocused,
+			},
+			{
+				from: RooCodeEventName.TaskUnfocused,
+				to: ExtensionBridgeEventName.TaskUnfocused,
+			},
+			{
+				from: RooCodeEventName.TaskActive,
+				to: ExtensionBridgeEventName.TaskActive,
+			},
+			{
+				from: RooCodeEventName.TaskInteractive,
+				to: ExtensionBridgeEventName.TaskInteractive,
+			},
+			{
+				from: RooCodeEventName.TaskResumable,
+				to: ExtensionBridgeEventName.TaskResumable,
+			},
+			{
+				from: RooCodeEventName.TaskIdle,
+				to: ExtensionBridgeEventName.TaskIdle,
+			},
+		] as const
+
+		const addListener =
+			(type: ExtensionBridgeEventName) =>
+			async (..._args: unknown[]) => {
+				this.publishEvent({
+					type,
+					instance: this.updateInstance(),
+					timestamp: Date.now(),
+				})
+			}
+
+		eventMapping.forEach(({ from, to }) => this.provider.on(from, addListener(to)))
+	}
+
+	private async publishEvent(message: ExtensionBridgeEvent): Promise<boolean> {
+		if (!this.socket) {
+			console.error("[ExtensionManager] publishEvent -> socket not available")
+			return false
+		}
+
+		try {
+			this.socket.emit(ExtensionSocketEvents.EVENT, message)
+
+			console.log(`[ExtensionManager] emit() -> ${ExtensionSocketEvents.EVENT} ${message.type}`, message)
+
+			return true
+		} catch (error) {
+			console.error(
+				`[ExtensionManager] emit() failed -> ${ExtensionSocketEvents.EVENT}: ${
+					error instanceof Error ? error.message : String(error)
+				}`,
+			)
+
+			return false
+		}
+	}
+
+	private updateInstance(): ExtensionInstance {
+		const task = this.provider?.getCurrentTask()
+		const taskHistory = this.provider?.getRecentTasks() ?? []
+
+		this.extensionInstance = {
+			...this.extensionInstance,
+			appProperties: this.extensionInstance.appProperties ?? this.provider.appProperties,
+			gitProperties: this.extensionInstance.gitProperties ?? this.provider.gitProperties,
+			lastHeartbeat: Date.now(),
+			task: task
+				? {
+						taskId: task.taskId,
+						taskStatus: task.taskStatus,
+						...task.metadata,
+					}
+				: { taskId: "", taskStatus: TaskStatus.None },
+			taskAsk: task?.taskAsk,
+			taskHistory,
+		}
+
+		return this.extensionInstance
+	}
+}

+ 289 - 0
packages/cloud/src/bridge/SocketConnectionManager.ts

@@ -0,0 +1,289 @@
+import { io, type Socket } from "socket.io-client"
+
+import { ConnectionState, type RetryConfig } from "@roo-code/types"
+
+export interface SocketConnectionOptions {
+	url: string
+	socketOptions: Record<string, unknown>
+	onConnect?: () => void | Promise<void>
+	onDisconnect?: (reason: string) => void
+	onReconnect?: (attemptNumber: number) => void | Promise<void>
+	onError?: (error: Error) => void
+	logger?: {
+		log: (message: string, ...args: unknown[]) => void
+		error: (message: string, ...args: unknown[]) => void
+		warn: (message: string, ...args: unknown[]) => void
+	}
+}
+
+export class SocketConnectionManager {
+	private socket: Socket | null = null
+	private connectionState: ConnectionState = ConnectionState.DISCONNECTED
+	private retryAttempt: number = 0
+	private retryTimeout: NodeJS.Timeout | null = null
+	private hasConnectedOnce: boolean = false
+
+	private readonly retryConfig: RetryConfig = {
+		maxInitialAttempts: 10,
+		initialDelay: 1_000,
+		maxDelay: 15_000,
+		backoffMultiplier: 2,
+	}
+
+	private readonly CONNECTION_TIMEOUT = 2_000
+	private readonly options: SocketConnectionOptions
+
+	constructor(options: SocketConnectionOptions, retryConfig?: Partial<RetryConfig>) {
+		this.options = options
+
+		if (retryConfig) {
+			this.retryConfig = { ...this.retryConfig, ...retryConfig }
+		}
+	}
+
+	public async connect(): Promise<void> {
+		if (this.connectionState === ConnectionState.CONNECTED) {
+			console.log(`[SocketConnectionManager] Already connected`)
+			return
+		}
+
+		if (this.connectionState === ConnectionState.CONNECTING || this.connectionState === ConnectionState.RETRYING) {
+			console.log(`[SocketConnectionManager] Connection attempt already in progress`)
+
+			return
+		}
+
+		// Start connection attempt without blocking.
+		this.startConnectionAttempt()
+	}
+
+	private async startConnectionAttempt() {
+		this.retryAttempt = 0
+
+		try {
+			await this.connectWithRetry()
+		} catch (error) {
+			console.error(`[SocketConnectionManager] Initial connection attempts failed:`, error)
+
+			// If we've never connected successfully, we've exhausted our retry attempts
+			// The user will need to manually retry or fix the issue
+			this.connectionState = ConnectionState.FAILED
+		}
+	}
+
+	private async connectWithRetry(): Promise<void> {
+		let delay = this.retryConfig.initialDelay
+
+		while (this.retryAttempt < this.retryConfig.maxInitialAttempts) {
+			try {
+				this.connectionState = this.retryAttempt === 0 ? ConnectionState.CONNECTING : ConnectionState.RETRYING
+
+				console.log(
+					`[SocketConnectionManager] Connection attempt ${this.retryAttempt + 1} / ${this.retryConfig.maxInitialAttempts}`,
+				)
+
+				await this.connectSocket()
+
+				console.log(`[SocketConnectionManager] Connected to ${this.options.url}`)
+
+				this.connectionState = ConnectionState.CONNECTED
+				this.retryAttempt = 0
+
+				this.clearRetryTimeouts()
+
+				if (this.options.onConnect) {
+					await this.options.onConnect()
+				}
+
+				return
+			} catch (error) {
+				this.retryAttempt++
+
+				console.error(`[SocketConnectionManager] Connection attempt ${this.retryAttempt} failed:`, error)
+
+				if (this.socket) {
+					this.socket.disconnect()
+					this.socket = null
+				}
+
+				if (this.retryAttempt >= this.retryConfig.maxInitialAttempts) {
+					this.connectionState = ConnectionState.FAILED
+
+					throw new Error(`Failed to connect after ${this.retryConfig.maxInitialAttempts} attempts`)
+				}
+
+				console.log(`[SocketConnectionManager] Waiting ${delay}ms before retry...`)
+
+				await this.delay(delay)
+
+				delay = Math.min(delay * this.retryConfig.backoffMultiplier, this.retryConfig.maxDelay)
+			}
+		}
+	}
+
+	private async connectSocket(): Promise<void> {
+		return new Promise((resolve, reject) => {
+			this.socket = io(this.options.url, this.options.socketOptions)
+
+			const connectionTimeout = setTimeout(() => {
+				console.error(`[SocketConnectionManager] Connection timeout`)
+
+				if (this.connectionState !== ConnectionState.CONNECTED) {
+					this.socket?.disconnect()
+					reject(new Error("Connection timeout"))
+				}
+			}, this.CONNECTION_TIMEOUT)
+
+			this.socket.on("connect", async () => {
+				clearTimeout(connectionTimeout)
+
+				const isReconnection = this.hasConnectedOnce
+
+				// If this is a reconnection (not the first connect), treat it as a
+				// reconnect.
+				// This handles server restarts where 'reconnect' event might not fire.
+				if (isReconnection) {
+					console.log(
+						`[SocketConnectionManager] Treating connect as reconnection (server may have restarted)`,
+					)
+
+					this.connectionState = ConnectionState.CONNECTED
+
+					if (this.options.onReconnect) {
+						// Call onReconnect to re-register instance.
+						await this.options.onReconnect(0)
+					}
+				}
+
+				this.hasConnectedOnce = true
+				resolve()
+			})
+
+			this.socket.on("disconnect", (reason: string) => {
+				console.log(`[SocketConnectionManager] Disconnected (reason: ${reason})`)
+
+				this.connectionState = ConnectionState.DISCONNECTED
+
+				if (this.options.onDisconnect) {
+					this.options.onDisconnect(reason)
+				}
+
+				// Don't attempt to reconnect if we're manually disconnecting.
+				const isManualDisconnect = reason === "io client disconnect"
+
+				if (!isManualDisconnect && this.hasConnectedOnce) {
+					// After successful initial connection, rely entirely on Socket.IO's
+					// reconnection.
+					console.log(`[SocketConnectionManager] Socket.IO will handle reconnection (reason: ${reason})`)
+				}
+			})
+
+			// Listen for reconnection attempts.
+			this.socket.on("reconnect_attempt", (attemptNumber: number) => {
+				console.log(`[SocketConnectionManager] Socket.IO reconnect attempt:`, {
+					attemptNumber,
+				})
+			})
+
+			this.socket.on("reconnect", (attemptNumber: number) => {
+				console.log(`[SocketConnectionManager] Socket reconnected (attempt: ${attemptNumber})`)
+
+				this.connectionState = ConnectionState.CONNECTED
+
+				if (this.options.onReconnect) {
+					this.options.onReconnect(attemptNumber)
+				}
+			})
+
+			this.socket.on("reconnect_error", (error: Error) => {
+				console.error(`[SocketConnectionManager] Socket.IO reconnect error:`, error)
+			})
+
+			this.socket.on("reconnect_failed", () => {
+				console.error(`[SocketConnectionManager] Socket.IO reconnection failed after all attempts`)
+
+				this.connectionState = ConnectionState.FAILED
+
+				// Socket.IO has exhausted its reconnection attempts
+				// The connection is now permanently failed until manual intervention
+			})
+
+			this.socket.on("error", (error) => {
+				console.error(`[SocketConnectionManager] Socket error:`, error)
+
+				if (this.connectionState !== ConnectionState.CONNECTED) {
+					clearTimeout(connectionTimeout)
+					reject(error)
+				}
+
+				if (this.options.onError) {
+					this.options.onError(error)
+				}
+			})
+
+			this.socket.on("auth_error", (error) => {
+				console.error(`[SocketConnectionManager] Authentication error:`, error)
+				clearTimeout(connectionTimeout)
+				reject(new Error(error.message || "Authentication failed"))
+			})
+		})
+	}
+
+	private delay(ms: number): Promise<void> {
+		return new Promise((resolve) => {
+			this.retryTimeout = setTimeout(resolve, ms)
+		})
+	}
+
+	// 1. Custom retry for initial connection attempts.
+	// 2. Socket.IO's built-in reconnection after successful initial connection.
+
+	private clearRetryTimeouts() {
+		if (this.retryTimeout) {
+			clearTimeout(this.retryTimeout)
+			this.retryTimeout = null
+		}
+	}
+
+	public async disconnect(): Promise<void> {
+		console.log(`[SocketConnectionManager] Disconnecting...`)
+
+		this.clearRetryTimeouts()
+
+		if (this.socket) {
+			this.socket.removeAllListeners()
+			this.socket.disconnect()
+			this.socket = null
+		}
+
+		this.connectionState = ConnectionState.DISCONNECTED
+
+		console.log(`[SocketConnectionManager] Disconnected`)
+	}
+
+	public getSocket(): Socket | null {
+		return this.socket
+	}
+
+	public getConnectionState(): ConnectionState {
+		return this.connectionState
+	}
+
+	public isConnected(): boolean {
+		return this.connectionState === ConnectionState.CONNECTED && this.socket?.connected === true
+	}
+
+	public async reconnect(): Promise<void> {
+		if (this.connectionState === ConnectionState.CONNECTED) {
+			console.log(`[SocketConnectionManager] Already connected`)
+			return
+		}
+
+		console.log(`[SocketConnectionManager] Manual reconnection requested`)
+
+		this.hasConnectedOnce = false
+
+		await this.disconnect()
+		await this.connect()
+	}
+}

+ 279 - 0
packages/cloud/src/bridge/TaskManager.ts

@@ -0,0 +1,279 @@
+import type { Socket } from "socket.io-client"
+
+import {
+	type ClineMessage,
+	type TaskEvents,
+	type TaskLike,
+	type TaskBridgeCommand,
+	type TaskBridgeEvent,
+	RooCodeEventName,
+	TaskBridgeEventName,
+	TaskBridgeCommandName,
+	TaskSocketEvents,
+} from "@roo-code/types"
+
+type TaskEventListener = {
+	[K in keyof TaskEvents]: (...args: TaskEvents[K]) => void | Promise<void>
+}[keyof TaskEvents]
+
+const TASK_EVENT_MAPPING: Record<TaskBridgeEventName, keyof TaskEvents> = {
+	[TaskBridgeEventName.Message]: RooCodeEventName.Message,
+	[TaskBridgeEventName.TaskModeSwitched]: RooCodeEventName.TaskModeSwitched,
+	[TaskBridgeEventName.TaskInteractive]: RooCodeEventName.TaskInteractive,
+}
+
+export class TaskManager {
+	private subscribedTasks: Map<string, TaskLike> = new Map()
+	private pendingTasks: Map<string, TaskLike> = new Map()
+	private socket: Socket | null = null
+
+	private taskListeners: Map<string, Map<TaskBridgeEventName, TaskEventListener>> = new Map()
+
+	constructor() {}
+
+	public async onConnect(socket: Socket): Promise<void> {
+		this.socket = socket
+
+		// Rejoin all subscribed tasks.
+		for (const taskId of this.subscribedTasks.keys()) {
+			try {
+				socket.emit(TaskSocketEvents.JOIN, { taskId })
+
+				console.log(`[TaskManager] emit() -> ${TaskSocketEvents.JOIN} ${taskId}`)
+			} catch (error) {
+				console.error(
+					`[TaskManager] emit() failed -> ${TaskSocketEvents.JOIN}: ${
+						error instanceof Error ? error.message : String(error)
+					}`,
+				)
+			}
+		}
+
+		// Subscribe to any pending tasks.
+		for (const task of this.pendingTasks.values()) {
+			await this.subscribeToTask(task, socket)
+		}
+
+		this.pendingTasks.clear()
+	}
+
+	public onDisconnect(): void {
+		this.socket = null
+	}
+
+	public async onReconnect(socket: Socket): Promise<void> {
+		this.socket = socket
+
+		// Rejoin all subscribed tasks.
+		for (const taskId of this.subscribedTasks.keys()) {
+			try {
+				socket.emit(TaskSocketEvents.JOIN, { taskId })
+
+				console.log(`[TaskManager] emit() -> ${TaskSocketEvents.JOIN} ${taskId}`)
+			} catch (error) {
+				console.error(
+					`[TaskManager] emit() failed -> ${TaskSocketEvents.JOIN}: ${
+						error instanceof Error ? error.message : String(error)
+					}`,
+				)
+			}
+		}
+	}
+
+	public async cleanup(socket: Socket | null): Promise<void> {
+		if (!socket) {
+			return
+		}
+
+		const unsubscribePromises = []
+
+		for (const taskId of this.subscribedTasks.keys()) {
+			unsubscribePromises.push(this.unsubscribeFromTask(taskId, socket))
+		}
+
+		await Promise.allSettled(unsubscribePromises)
+		this.subscribedTasks.clear()
+		this.taskListeners.clear()
+		this.pendingTasks.clear()
+		this.socket = null
+	}
+
+	public addPendingTask(task: TaskLike): void {
+		this.pendingTasks.set(task.taskId, task)
+	}
+
+	public async subscribeToTask(task: TaskLike, socket: Socket): Promise<void> {
+		const taskId = task.taskId
+		this.subscribedTasks.set(taskId, task)
+		this.setupListeners(task)
+
+		try {
+			socket.emit(TaskSocketEvents.JOIN, { taskId })
+			console.log(`[TaskManager] emit() -> ${TaskSocketEvents.JOIN} ${taskId}`)
+		} catch (error) {
+			console.error(
+				`[TaskManager] emit() failed -> ${TaskSocketEvents.JOIN}: ${
+					error instanceof Error ? error.message : String(error)
+				}`,
+			)
+		}
+	}
+
+	public async unsubscribeFromTask(taskId: string, socket: Socket): Promise<void> {
+		const task = this.subscribedTasks.get(taskId)
+
+		if (task) {
+			this.removeListeners(task)
+			this.subscribedTasks.delete(taskId)
+		}
+
+		try {
+			socket.emit(TaskSocketEvents.LEAVE, { taskId })
+
+			console.log(`[TaskManager] emit() -> ${TaskSocketEvents.LEAVE} ${taskId}`)
+		} catch (error) {
+			console.error(
+				`[TaskManager] emit() failed -> ${TaskSocketEvents.LEAVE}: ${
+					error instanceof Error ? error.message : String(error)
+				}`,
+			)
+		}
+	}
+
+	public handleTaskCommand(message: TaskBridgeCommand): void {
+		const task = this.subscribedTasks.get(message.taskId)
+
+		if (!task) {
+			console.error(`[TaskManager#handleTaskCommand] Unable to find task ${message.taskId}`)
+
+			return
+		}
+
+		switch (message.type) {
+			case TaskBridgeCommandName.Message:
+				console.log(
+					`[TaskManager#handleTaskCommand] ${TaskBridgeCommandName.Message} ${message.taskId} -> submitUserMessage()`,
+					message,
+				)
+
+				task.submitUserMessage(message.payload.text, message.payload.images)
+				break
+			case TaskBridgeCommandName.ApproveAsk:
+				console.log(
+					`[TaskManager#handleTaskCommand] ${TaskBridgeCommandName.ApproveAsk} ${message.taskId} -> approveAsk()`,
+					message,
+				)
+
+				task.approveAsk(message.payload)
+				break
+			case TaskBridgeCommandName.DenyAsk:
+				console.log(
+					`[TaskManager#handleTaskCommand] ${TaskBridgeCommandName.DenyAsk} ${message.taskId} -> denyAsk()`,
+					message,
+				)
+
+				task.denyAsk(message.payload)
+				break
+		}
+	}
+
+	private setupListeners(task: TaskLike): void {
+		if (this.taskListeners.has(task.taskId)) {
+			console.warn("[TaskManager] Listeners already exist for task, removing old listeners:", task.taskId)
+
+			this.removeListeners(task)
+		}
+
+		const listeners = new Map<TaskBridgeEventName, TaskEventListener>()
+
+		const onMessage = ({ action, message }: { action: string; message: ClineMessage }) => {
+			this.publishEvent({
+				type: TaskBridgeEventName.Message,
+				taskId: task.taskId,
+				action,
+				message,
+			})
+		}
+
+		task.on(RooCodeEventName.Message, onMessage)
+		listeners.set(TaskBridgeEventName.Message, onMessage)
+
+		const onTaskModeSwitched = (mode: string) => {
+			this.publishEvent({
+				type: TaskBridgeEventName.TaskModeSwitched,
+				taskId: task.taskId,
+				mode,
+			})
+		}
+
+		task.on(RooCodeEventName.TaskModeSwitched, onTaskModeSwitched)
+		listeners.set(TaskBridgeEventName.TaskModeSwitched, onTaskModeSwitched)
+
+		const onTaskInteractive = (_taskId: string) => {
+			this.publishEvent({
+				type: TaskBridgeEventName.TaskInteractive,
+				taskId: task.taskId,
+			})
+		}
+
+		task.on(RooCodeEventName.TaskInteractive, onTaskInteractive)
+
+		listeners.set(TaskBridgeEventName.TaskInteractive, onTaskInteractive)
+
+		this.taskListeners.set(task.taskId, listeners)
+
+		console.log("[TaskManager] Task listeners setup complete for:", task.taskId)
+	}
+
+	private removeListeners(task: TaskLike): void {
+		const listeners = this.taskListeners.get(task.taskId)
+
+		if (!listeners) {
+			return
+		}
+
+		console.log("[TaskManager] Removing task listeners for:", task.taskId)
+
+		listeners.forEach((listener, eventName) => {
+			try {
+				// eslint-disable-next-line @typescript-eslint/no-explicit-any
+				task.off(TASK_EVENT_MAPPING[eventName], listener as any)
+			} catch (error) {
+				console.error(
+					`[TaskManager] Error removing listener for ${String(eventName)} on task ${task.taskId}:`,
+					error,
+				)
+			}
+		})
+
+		this.taskListeners.delete(task.taskId)
+	}
+
+	private async publishEvent(message: TaskBridgeEvent): Promise<boolean> {
+		if (!this.socket) {
+			console.error("[TaskManager] publishEvent -> socket not available")
+			return false
+		}
+
+		try {
+			this.socket.emit(TaskSocketEvents.EVENT, message)
+
+			if (message.type !== TaskBridgeEventName.Message) {
+				console.log(
+					`[TaskManager] emit() -> ${TaskSocketEvents.EVENT} ${message.taskId} ${message.type}`,
+					message,
+				)
+			}
+
+			return true
+		} catch (error) {
+			console.error(
+				`[TaskManager] emit() failed -> ${TaskSocketEvents.EVENT}: ${
+					error instanceof Error ? error.message : String(error)
+				}`,
+			)
+
+			return false
+		}
+	}
+}

+ 6 - 0
packages/cloud/src/config.ts

@@ -0,0 +1,6 @@
+export const PRODUCTION_CLERK_BASE_URL = "https://clerk.roocode.com"
+export const PRODUCTION_ROO_CODE_API_URL = "https://app.roocode.com"
+
+export const getClerkBaseUrl = () => process.env.CLERK_BASE_URL || PRODUCTION_CLERK_BASE_URL
+
+export const getRooCodeApiUrl = () => process.env.ROO_CODE_API_URL || PRODUCTION_ROO_CODE_API_URL

+ 42 - 0
packages/cloud/src/errors.ts

@@ -0,0 +1,42 @@
+export class CloudAPIError extends Error {
+	constructor(
+		message: string,
+		public statusCode?: number,
+		public responseBody?: unknown,
+	) {
+		super(message)
+		this.name = "CloudAPIError"
+		Object.setPrototypeOf(this, CloudAPIError.prototype)
+	}
+}
+
+export class TaskNotFoundError extends CloudAPIError {
+	constructor(taskId?: string) {
+		super(taskId ? `Task '${taskId}' not found` : "Task not found", 404)
+		this.name = "TaskNotFoundError"
+		Object.setPrototypeOf(this, TaskNotFoundError.prototype)
+	}
+}
+
+export class AuthenticationError extends CloudAPIError {
+	constructor(message = "Authentication required") {
+		super(message, 401)
+		this.name = "AuthenticationError"
+		Object.setPrototypeOf(this, AuthenticationError.prototype)
+	}
+}
+
+export class NetworkError extends CloudAPIError {
+	constructor(message = "Network error occurred") {
+		super(message)
+		this.name = "NetworkError"
+		Object.setPrototypeOf(this, NetworkError.prototype)
+	}
+}
+
+export class InvalidClientTokenError extends Error {
+	constructor() {
+		super("Invalid/Expired client token")
+		Object.setPrototypeOf(this, InvalidClientTokenError.prototype)
+	}
+}

+ 49 - 0
packages/cloud/src/importVscode.ts

@@ -0,0 +1,49 @@
+/**
+ * Utility for lazy-loading the VS Code module in environments where it's available.
+ * This allows the SDK to be used in both VS Code extension and Node.js environments.
+ * Compatible with both VSCode and Cursor extension hosts.
+ */
+
+let vscodeModule: typeof import("vscode") | undefined
+
+/**
+ * Attempts to dynamically import the VS Code module.
+ * Returns undefined if not running in a VS Code/Cursor extension context.
+ */
+export async function importVscode(): Promise<typeof import("vscode") | undefined> {
+	// Check if already loaded
+	if (vscodeModule) {
+		return vscodeModule
+	}
+
+	try {
+		// Method 1: Check if vscode is available in global scope (common in extension hosts).
+		if (typeof globalThis !== "undefined" && "acquireVsCodeApi" in globalThis) {
+			// We're in a webview context, vscode module won't be available.
+			return undefined
+		}
+
+		// Method 2: Try to require the module (works in most extension contexts).
+		if (typeof require !== "undefined") {
+			try {
+				// eslint-disable-next-line @typescript-eslint/no-require-imports
+				vscodeModule = require("vscode")
+
+				if (vscodeModule) {
+					return vscodeModule
+				}
+			} catch (error) {
+				console.error("Error loading VS Code module:", error)
+				// Fall through to dynamic import.
+			}
+		}
+
+		// Method 3: Dynamic import (original approach, works in VSCode).
+		vscodeModule = await import("vscode")
+		return vscodeModule
+	} catch (error) {
+		// Log the original error for debugging.
+		console.warn("VS Code module not available in this environment:", error)
+		return undefined
+	}
+}

+ 5 - 0
packages/cloud/src/index.ts

@@ -0,0 +1,5 @@
+export * from "./config.js"
+
+export * from "./CloudAPI.js"
+export * from "./CloudService.js"
+export * from "./bridge/ExtensionBridgeService.js"

+ 5 - 0
packages/cloud/src/utils.ts

@@ -0,0 +1,5 @@
+import type { ExtensionContext } from "vscode"
+
+export function getUserAgent(context?: ExtensionContext): string {
+	return `Roo-Code ${context?.extension?.packageJSON?.version || "unknown"}`
+}

+ 9 - 0
packages/cloud/tsconfig.json

@@ -0,0 +1,9 @@
+{
+	"extends": "@roo-code/config-typescript/base.json",
+	"compilerOptions": {
+		"types": ["vitest/globals", "node"],
+		"outDir": "./dist"
+	},
+	"include": ["src", "scripts", "*.config.ts"],
+	"exclude": ["node_modules"]
+}

+ 14 - 0
packages/cloud/vitest.config.ts

@@ -0,0 +1,14 @@
+import { defineConfig } from "vitest/config"
+
+export default defineConfig({
+	test: {
+		globals: true,
+		environment: "node",
+		watch: false,
+	},
+	resolve: {
+		alias: {
+			vscode: new URL("./src/__mocks__/vscode.ts", import.meta.url).pathname,
+		},
+	},
+})

+ 2 - 1
packages/config-typescript/base.json

@@ -14,6 +14,7 @@
 		"resolveJsonModule": true,
 		"resolveJsonModule": true,
 		"skipLibCheck": true,
 		"skipLibCheck": true,
 		"strict": true,
 		"strict": true,
-		"target": "ES2022"
+		"target": "ES2022",
+		"types": ["node"]
 	}
 	}
 }
 }

+ 1 - 1
packages/telemetry/package.json

@@ -8,7 +8,7 @@
 		"lint": "eslint src --ext=ts --max-warnings=0",
 		"lint": "eslint src --ext=ts --max-warnings=0",
 		"check-types": "tsc --noEmit",
 		"check-types": "tsc --noEmit",
 		"test": "vitest run",
 		"test": "vitest run",
-		"clean": "rimraf dist .turbo"
+		"clean": "rimraf .turbo"
 	},
 	},
 	"dependencies": {
 	"dependencies": {
 		"@roo-code/types": "workspace:^",
 		"@roo-code/types": "workspace:^",

+ 1 - 1
packages/types/npm/package.metadata.json

@@ -1,6 +1,6 @@
 {
 {
 	"name": "@roo-code/types",
 	"name": "@roo-code/types",
-	"version": "1.63.0",
+	"version": "1.64.0",
 	"description": "TypeScript type definitions for Roo Code.",
 	"description": "TypeScript type definitions for Roo Code.",
 	"publishConfig": {
 	"publishConfig": {
 		"access": "public",
 		"access": "public",

+ 0 - 1
packages/types/src/__tests__/ipc.test.ts

@@ -1,4 +1,3 @@
-import { describe, it, expect } from "vitest"
 import { TaskCommandName, taskCommandSchema } from "../ipc.js"
 import { TaskCommandName, taskCommandSchema } from "../ipc.js"
 
 
 describe("IPC Types", () => {
 describe("IPC Types", () => {

+ 0 - 1
packages/types/src/__tests__/provider-settings.test.ts

@@ -1,4 +1,3 @@
-import { describe, it, expect } from "vitest"
 import { getApiProtocol } from "../provider-settings.js"
 import { getApiProtocol } from "../provider-settings.js"
 
 
 describe("getApiProtocol", () => {
 describe("getApiProtocol", () => {

+ 618 - 0
packages/types/src/cloud.ts

@@ -0,0 +1,618 @@
+import EventEmitter from "events"
+
+import { z } from "zod"
+
+import { RooCodeEventName } from "./events.js"
+import { TaskStatus, taskMetadataSchema } from "./task.js"
+import { globalSettingsSchema } from "./global-settings.js"
+import { providerSettingsWithIdSchema } from "./provider-settings.js"
+import { mcpMarketplaceItemSchema } from "./marketplace.js"
+import { clineMessageSchema } from "./message.js"
+import { staticAppPropertiesSchema, gitPropertiesSchema } from "./telemetry.js"
+
+/**
+ * JWTPayload
+ */
+
+export interface JWTPayload {
+	iss?: string // Issuer (should be 'rcc')
+	sub?: string // Subject - CloudJob ID for job tokens (t:'cj'), User ID for auth tokens (t:'auth')
+	exp?: number // Expiration time
+	iat?: number // Issued at time
+	nbf?: number // Not before time
+	v?: number // Version (should be 1)
+	r?: {
+		u?: string // User ID (always present in valid tokens)
+		o?: string // Organization ID (optional - undefined when orgId is null)
+		t?: string // Token type: 'cj' for job tokens, 'auth' for auth tokens
+	}
+}
+
+/**
+ * CloudUserInfo
+ */
+
+export interface CloudUserInfo {
+	id?: string
+	name?: string
+	email?: string
+	picture?: string
+	organizationId?: string
+	organizationName?: string
+	organizationRole?: string
+	organizationImageUrl?: string
+	extensionBridgeEnabled?: boolean
+}
+
+/**
+ * CloudOrganization
+ */
+
+export interface CloudOrganization {
+	id: string
+	name: string
+	slug?: string
+	image_url?: string
+	has_image?: boolean
+	created_at?: number
+	updated_at?: number
+}
+
+/**
+ * CloudOrganizationMembership
+ */
+
+export interface CloudOrganizationMembership {
+	id: string
+	organization: CloudOrganization
+	role: string
+	permissions?: string[]
+	created_at?: number
+	updated_at?: number
+}
+
+/**
+ * OrganizationAllowList
+ */
+
+export const organizationAllowListSchema = z.object({
+	allowAll: z.boolean(),
+	providers: z.record(
+		z.object({
+			allowAll: z.boolean(),
+			models: z.array(z.string()).optional(),
+		}),
+	),
+})
+
+export type OrganizationAllowList = z.infer<typeof organizationAllowListSchema>
+
+/**
+ * OrganizationDefaultSettings
+ */
+
+export const organizationDefaultSettingsSchema = globalSettingsSchema
+	.pick({
+		enableCheckpoints: true,
+		fuzzyMatchThreshold: true,
+		maxOpenTabsContext: true,
+		maxReadFileLine: true,
+		maxWorkspaceFiles: true,
+		showRooIgnoredFiles: true,
+		terminalCommandDelay: true,
+		terminalCompressProgressBar: true,
+		terminalOutputLineLimit: true,
+		terminalShellIntegrationDisabled: true,
+		terminalShellIntegrationTimeout: true,
+		terminalZshClearEolMark: true,
+	})
+	// Add stronger validations for some fields.
+	.merge(
+		z.object({
+			maxOpenTabsContext: z.number().int().nonnegative().optional(),
+			maxReadFileLine: z.number().int().gte(-1).optional(),
+			maxWorkspaceFiles: z.number().int().nonnegative().optional(),
+			terminalCommandDelay: z.number().int().nonnegative().optional(),
+			terminalOutputLineLimit: z.number().int().nonnegative().optional(),
+			terminalShellIntegrationTimeout: z.number().int().nonnegative().optional(),
+		}),
+	)
+
+export type OrganizationDefaultSettings = z.infer<typeof organizationDefaultSettingsSchema>
+
+/**
+ * OrganizationCloudSettings
+ */
+
+export const organizationCloudSettingsSchema = z.object({
+	recordTaskMessages: z.boolean().optional(),
+	enableTaskSharing: z.boolean().optional(),
+	taskShareExpirationDays: z.number().int().positive().optional(),
+	allowMembersViewAllTasks: z.boolean().optional(),
+})
+
+export type OrganizationCloudSettings = z.infer<typeof organizationCloudSettingsSchema>
+
+/**
+ * OrganizationSettings
+ */
+
+export const organizationSettingsSchema = z.object({
+	version: z.number(),
+	cloudSettings: organizationCloudSettingsSchema.optional(),
+	defaultSettings: organizationDefaultSettingsSchema,
+	allowList: organizationAllowListSchema,
+	hiddenMcps: z.array(z.string()).optional(),
+	hideMarketplaceMcps: z.boolean().optional(),
+	mcps: z.array(mcpMarketplaceItemSchema).optional(),
+	providerProfiles: z.record(z.string(), providerSettingsWithIdSchema).optional(),
+})
+
+export type OrganizationSettings = z.infer<typeof organizationSettingsSchema>
+
+/**
+ * User Settings Schemas
+ */
+
+export const userFeaturesSchema = z.object({
+	roomoteControlEnabled: z.boolean().optional(),
+})
+
+export type UserFeatures = z.infer<typeof userFeaturesSchema>
+
+export const userSettingsConfigSchema = z.object({
+	extensionBridgeEnabled: z.boolean().optional(),
+})
+
+export type UserSettingsConfig = z.infer<typeof userSettingsConfigSchema>
+
+export const userSettingsDataSchema = z.object({
+	features: userFeaturesSchema,
+	settings: userSettingsConfigSchema,
+	version: z.number(),
+})
+
+export type UserSettingsData = z.infer<typeof userSettingsDataSchema>
+
+/**
+ * Constants
+ */
+
+export const ORGANIZATION_ALLOW_ALL: OrganizationAllowList = {
+	allowAll: true,
+	providers: {},
+} as const
+
+export const ORGANIZATION_DEFAULT: OrganizationSettings = {
+	version: 0,
+	cloudSettings: {
+		recordTaskMessages: true,
+		enableTaskSharing: true,
+		taskShareExpirationDays: 30,
+		allowMembersViewAllTasks: true,
+	},
+	defaultSettings: {},
+	allowList: ORGANIZATION_ALLOW_ALL,
+} as const
+
+/**
+ * ShareVisibility
+ */
+
+export type ShareVisibility = "organization" | "public"
+
+/**
+ * ShareResponse
+ */
+
+export const shareResponseSchema = z.object({
+	success: z.boolean(),
+	shareUrl: z.string().optional(),
+	error: z.string().optional(),
+	isNewShare: z.boolean().optional(),
+	manageUrl: z.string().optional(),
+})
+
+export type ShareResponse = z.infer<typeof shareResponseSchema>
+
+/**
+ * AuthService
+ */
+
+export type AuthState = "initializing" | "logged-out" | "active-session" | "attempting-session" | "inactive-session"
+
+export interface AuthService extends EventEmitter<AuthServiceEvents> {
+	// Lifecycle
+	initialize(): Promise<void>
+	broadcast(): void
+
+	// Authentication methods
+	login(): Promise<void>
+	logout(): Promise<void>
+	handleCallback(code: string | null, state: string | null, organizationId?: string | null): Promise<void>
+
+	// State methods
+	getState(): AuthState
+	isAuthenticated(): boolean
+	hasActiveSession(): boolean
+	hasOrIsAcquiringActiveSession(): boolean
+
+	// Token and user info
+	getSessionToken(): string | undefined
+	getUserInfo(): CloudUserInfo | null
+	getStoredOrganizationId(): string | null
+}
+
+/**
+ * AuthServiceEvents
+ */
+
+export interface AuthServiceEvents {
+	"auth-state-changed": [
+		data: {
+			state: AuthState
+			previousState: AuthState
+		},
+	]
+	"user-info": [data: { userInfo: CloudUserInfo }]
+}
+
+/**
+ * SettingsService
+ */
+
+/**
+ * Interface for settings services that provide organization settings
+ */
+export interface SettingsService {
+	/**
+	 * Get the organization allow list
+	 * @returns The organization allow list or default if none available
+	 */
+	getAllowList(): OrganizationAllowList
+
+	/**
+	 * Get the current organization settings
+	 * @returns The organization settings or undefined if none available
+	 */
+	getSettings(): OrganizationSettings | undefined
+
+	/**
+	 * Get the current user settings
+	 * @returns The user settings data or undefined if none available
+	 */
+	getUserSettings(): UserSettingsData | undefined
+
+	/**
+	 * Get the current user features
+	 * @returns The user features or empty object if none available
+	 */
+	getUserFeatures(): UserFeatures
+
+	/**
+	 * Get the current user settings configuration
+	 * @returns The user settings configuration or empty object if none available
+	 */
+	getUserSettingsConfig(): UserSettingsConfig
+
+	/**
+	 * Update user settings with partial configuration
+	 * @param settings Partial user settings configuration to update
+	 * @returns Promise that resolves to true if successful, false otherwise
+	 */
+	updateUserSettings(settings: Partial<UserSettingsConfig>): Promise<boolean>
+
+	/**
+	 * Dispose of the settings service and clean up resources
+	 */
+	dispose(): void
+}
+
+/**
+ * SettingsServiceEvents
+ */
+
+export interface SettingsServiceEvents {
+	"settings-updated": [data: Record<string, never>]
+}
+
+/**
+ * CloudServiceEvents
+ */
+
+export type CloudServiceEvents = AuthServiceEvents & SettingsServiceEvents
+
+/**
+ * ConnectionState
+ */
+
+export enum ConnectionState {
+	DISCONNECTED = "disconnected",
+	CONNECTING = "connecting",
+	CONNECTED = "connected",
+	RETRYING = "retrying",
+	FAILED = "failed",
+}
+
+/**
+ * RetryConfig
+ */
+
+export interface RetryConfig {
+	maxInitialAttempts: number
+	initialDelay: number
+	maxDelay: number
+	backoffMultiplier: number
+}
+
+/**
+ * Constants
+ */
+
+export const HEARTBEAT_INTERVAL_MS = 20_000
+export const INSTANCE_TTL_SECONDS = 60
+
+/**
+ * ExtensionTask
+ */
+
+const extensionTaskSchema = z.object({
+	taskId: z.string(),
+	taskStatus: z.nativeEnum(TaskStatus),
+	...taskMetadataSchema.shape,
+})
+
+export type ExtensionTask = z.infer<typeof extensionTaskSchema>
+
+/**
+ * ExtensionInstance
+ */
+
+export const extensionInstanceSchema = z.object({
+	instanceId: z.string(),
+	userId: z.string(),
+	workspacePath: z.string(),
+	appProperties: staticAppPropertiesSchema,
+	gitProperties: gitPropertiesSchema.optional(),
+	lastHeartbeat: z.coerce.number(),
+	task: extensionTaskSchema,
+	taskAsk: clineMessageSchema.optional(),
+	taskHistory: z.array(z.string()),
+})
+
+export type ExtensionInstance = z.infer<typeof extensionInstanceSchema>
+
+/**
+ * ExtensionBridgeEvent
+ */
+
+export enum ExtensionBridgeEventName {
+	TaskCreated = RooCodeEventName.TaskCreated,
+	TaskStarted = RooCodeEventName.TaskStarted,
+	TaskCompleted = RooCodeEventName.TaskCompleted,
+	TaskAborted = RooCodeEventName.TaskAborted,
+	TaskFocused = RooCodeEventName.TaskFocused,
+	TaskUnfocused = RooCodeEventName.TaskUnfocused,
+	TaskActive = RooCodeEventName.TaskActive,
+	TaskInteractive = RooCodeEventName.TaskInteractive,
+	TaskResumable = RooCodeEventName.TaskResumable,
+	TaskIdle = RooCodeEventName.TaskIdle,
+
+	InstanceRegistered = "instance_registered",
+	InstanceUnregistered = "instance_unregistered",
+	HeartbeatUpdated = "heartbeat_updated",
+}
+
+export const extensionBridgeEventSchema = z.discriminatedUnion("type", [
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.TaskCreated),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.TaskStarted),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.TaskCompleted),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.TaskAborted),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.TaskFocused),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.TaskUnfocused),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.TaskActive),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.TaskInteractive),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.TaskResumable),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.TaskIdle),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.InstanceRegistered),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.InstanceUnregistered),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.HeartbeatUpdated),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+])
+
+export type ExtensionBridgeEvent = z.infer<typeof extensionBridgeEventSchema>
+
+/**
+ * ExtensionBridgeCommand
+ */
+
+export enum ExtensionBridgeCommandName {
+	StartTask = "start_task",
+	StopTask = "stop_task",
+	ResumeTask = "resume_task",
+}
+
+export const extensionBridgeCommandSchema = z.discriminatedUnion("type", [
+	z.object({
+		type: z.literal(ExtensionBridgeCommandName.StartTask),
+		instanceId: z.string(),
+		payload: z.object({
+			text: z.string(),
+			images: z.array(z.string()).optional(),
+		}),
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeCommandName.StopTask),
+		instanceId: z.string(),
+		payload: z.object({ taskId: z.string() }),
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeCommandName.ResumeTask),
+		instanceId: z.string(),
+		payload: z.object({
+			taskId: z.string(),
+		}),
+		timestamp: z.number(),
+	}),
+])
+
+export type ExtensionBridgeCommand = z.infer<typeof extensionBridgeCommandSchema>
+
+/**
+ * TaskBridgeEvent
+ */
+
+export enum TaskBridgeEventName {
+	Message = RooCodeEventName.Message,
+	TaskModeSwitched = RooCodeEventName.TaskModeSwitched,
+	TaskInteractive = RooCodeEventName.TaskInteractive,
+}
+
+export const taskBridgeEventSchema = z.discriminatedUnion("type", [
+	z.object({
+		type: z.literal(TaskBridgeEventName.Message),
+		taskId: z.string(),
+		action: z.string(),
+		message: clineMessageSchema,
+	}),
+	z.object({
+		type: z.literal(TaskBridgeEventName.TaskModeSwitched),
+		taskId: z.string(),
+		mode: z.string(),
+	}),
+	z.object({
+		type: z.literal(TaskBridgeEventName.TaskInteractive),
+		taskId: z.string(),
+	}),
+])
+
+export type TaskBridgeEvent = z.infer<typeof taskBridgeEventSchema>
+
+/**
+ * TaskBridgeCommand
+ */
+
+export enum TaskBridgeCommandName {
+	Message = "message",
+	ApproveAsk = "approve_ask",
+	DenyAsk = "deny_ask",
+}
+
+export const taskBridgeCommandSchema = z.discriminatedUnion("type", [
+	z.object({
+		type: z.literal(TaskBridgeCommandName.Message),
+		taskId: z.string(),
+		payload: z.object({
+			text: z.string(),
+			images: z.array(z.string()).optional(),
+		}),
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(TaskBridgeCommandName.ApproveAsk),
+		taskId: z.string(),
+		payload: z.object({
+			text: z.string().optional(),
+			images: z.array(z.string()).optional(),
+		}),
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(TaskBridgeCommandName.DenyAsk),
+		taskId: z.string(),
+		payload: z.object({
+			text: z.string().optional(),
+			images: z.array(z.string()).optional(),
+		}),
+		timestamp: z.number(),
+	}),
+])
+
+export type TaskBridgeCommand = z.infer<typeof taskBridgeCommandSchema>
+
+/**
+ * ExtensionSocketEvents
+ */
+
+export const ExtensionSocketEvents = {
+	CONNECTED: "extension:connected",
+
+	REGISTER: "extension:register",
+	UNREGISTER: "extension:unregister",
+
+	HEARTBEAT: "extension:heartbeat",
+
+	EVENT: "extension:event", // event from extension instance
+	RELAYED_EVENT: "extension:relayed_event", // relay from server
+
+	COMMAND: "extension:command", // command from user
+	RELAYED_COMMAND: "extension:relayed_command", // relay from server
+} as const
+
+/**
+ * TaskSocketEvents
+ */
+
+export const TaskSocketEvents = {
+	JOIN: "task:join",
+	LEAVE: "task:leave",
+
+	EVENT: "task:event", // event from extension task
+	RELAYED_EVENT: "task:relayed_event", // relay from server
+
+	COMMAND: "task:command", // command from user
+	RELAYED_COMMAND: "task:relayed_command", // relay from server
+} as const

+ 1 - 0
packages/types/src/index.ts

@@ -1,4 +1,5 @@
 export * from "./api.js"
 export * from "./api.js"
+export * from "./cloud.js"
 export * from "./codebase-index.js"
 export * from "./codebase-index.js"
 export * from "./events.js"
 export * from "./events.js"
 export * from "./experiment.js"
 export * from "./experiment.js"

+ 0 - 1
packages/types/src/providers/__tests__/claude-code.spec.ts

@@ -1,4 +1,3 @@
-import { describe, test, expect } from "vitest"
 import { convertModelNameForVertex, getClaudeCodeModelId } from "../claude-code.js"
 import { convertModelNameForVertex, getClaudeCodeModelId } from "../claude-code.js"
 
 
 describe("convertModelNameForVertex", () => {
 describe("convertModelNameForVertex", () => {

+ 98 - 72
pnpm-lock.yaml

@@ -71,8 +71,8 @@ importers:
         specifier: ^4.19.3
         specifier: ^4.19.3
         version: 4.19.4
         version: 4.19.4
       turbo:
       turbo:
-        specifier: ^2.5.3
-        version: 2.5.4
+        specifier: ^2.5.6
+        version: 2.5.6
       typescript:
       typescript:
         specifier: ^5.4.5
         specifier: ^5.4.5
         version: 5.8.3
         version: 5.8.3
@@ -285,8 +285,8 @@ importers:
         specifier: ^8.6.0
         specifier: ^8.6.0
         version: 8.6.0([email protected])
         version: 8.6.0([email protected])
       framer-motion:
       framer-motion:
-        specifier: ^12.15.0
-        version: 12.16.0(@emotion/[email protected])([email protected]([email protected]))([email protected])
+        specifier: 12.15.0
+        version: 12.15.0(@emotion/[email protected])([email protected]([email protected]))([email protected])
       lucide-react:
       lucide-react:
         specifier: ^0.518.0
         specifier: ^0.518.0
         version: 0.518.0([email protected])
         version: 0.518.0([email protected])
@@ -371,6 +371,46 @@ importers:
         specifier: ^3.2.3
         specifier: ^3.2.3
         version: 3.2.4(@types/[email protected])(@types/[email protected])(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
         version: 3.2.4(@types/[email protected])(@types/[email protected])(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
 
 
+  packages/cloud:
+    dependencies:
+      '@roo-code/types':
+        specifier: workspace:^
+        version: link:../types
+      ioredis:
+        specifier: ^5.6.1
+        version: 5.6.1
+      jwt-decode:
+        specifier: ^4.0.0
+        version: 4.0.0
+      p-wait-for:
+        specifier: ^5.0.2
+        version: 5.0.2
+      socket.io-client:
+        specifier: ^4.8.1
+        version: 4.8.1
+      zod:
+        specifier: ^3.25.76
+        version: 3.25.76
+    devDependencies:
+      '@roo-code/config-eslint':
+        specifier: workspace:^
+        version: link:../config-eslint
+      '@roo-code/config-typescript':
+        specifier: workspace:^
+        version: link:../config-typescript
+      '@types/node':
+        specifier: ^24.1.0
+        version: 24.2.1
+      '@types/vscode':
+        specifier: ^1.102.0
+        version: 1.103.0
+      globals:
+        specifier: ^16.3.0
+        version: 16.3.0
+      vitest:
+        specifier: ^3.2.4
+        version: 3.2.4(@types/[email protected])(@types/[email protected])(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
+
   packages/config-eslint:
   packages/config-eslint:
     devDependencies:
     devDependencies:
       '@eslint/js':
       '@eslint/js':
@@ -396,7 +436,7 @@ importers:
         version: 5.2.0([email protected]([email protected]))
         version: 5.2.0([email protected]([email protected]))
       eslint-plugin-turbo:
       eslint-plugin-turbo:
         specifier: ^2.4.4
         specifier: ^2.4.4
-        version: 2.5.3([email protected]([email protected]))([email protected].4)
+        version: 2.5.3([email protected]([email protected]))([email protected].6)
       globals:
       globals:
         specifier: ^16.0.0
         specifier: ^16.0.0
         version: 16.1.0
         version: 16.1.0
@@ -578,14 +618,14 @@ importers:
         specifier: ^1.9.18
         specifier: ^1.9.18
         version: 1.9.18([email protected])
         version: 1.9.18([email protected])
       '@modelcontextprotocol/sdk':
       '@modelcontextprotocol/sdk':
-        specifier: ^1.9.0
+        specifier: 1.12.0
         version: 1.12.0
         version: 1.12.0
       '@qdrant/js-client-rest':
       '@qdrant/js-client-rest':
         specifier: ^1.14.0
         specifier: ^1.14.0
         version: 1.14.0([email protected])
         version: 1.14.0([email protected])
       '@roo-code/cloud':
       '@roo-code/cloud':
-        specifier: ^0.29.0
-        version: 0.29.0
+        specifier: workspace:^
+        version: link:../packages/cloud
       '@roo-code/ipc':
       '@roo-code/ipc':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../packages/ipc
         version: link:../packages/ipc
@@ -595,9 +635,6 @@ importers:
       '@roo-code/types':
       '@roo-code/types':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../packages/types
         version: link:../packages/types
-      '@types/lodash.debounce':
-        specifier: ^4.0.9
-        version: 4.0.9
       '@vscode/codicons':
       '@vscode/codicons':
         specifier: ^0.0.36
         specifier: ^0.0.36
         version: 0.0.36
         version: 0.0.36
@@ -803,6 +840,9 @@ importers:
       '@types/glob':
       '@types/glob':
         specifier: ^8.1.0
         specifier: ^8.1.0
         version: 8.1.0
         version: 8.1.0
+      '@types/lodash.debounce':
+        specifier: ^4.0.9
+        version: 4.0.9
       '@types/mocha':
       '@types/mocha':
         specifier: ^10.0.10
         specifier: ^10.0.10
         version: 10.0.10
         version: 10.0.10
@@ -3346,12 +3386,6 @@ packages:
     cpu: [x64]
     cpu: [x64]
     os: [win32]
     os: [win32]
 
 
-  '@roo-code/[email protected]':
-    resolution: {integrity: sha512-fXN0mdkd5GezpVrCspe6atUkwvSk5D4wF80g+lc8E3aPVqEAozoI97kHNulRChGlBw7UIdd5xxbr1Z8Jtn+S/Q==}
-
-  '@roo-code/[email protected]':
-    resolution: {integrity: sha512-pX8ftkDq1CySBbkUTIW9/QEG52ttFT/kl0ID286l0L3W22wpGRUct6PCedNI9kLDM4s5sxaUeZx7b3rUChikkw==}
-
   '@sec-ant/[email protected]':
   '@sec-ant/[email protected]':
     resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
     resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
 
 
@@ -4225,6 +4259,9 @@ packages:
   '@types/[email protected]':
   '@types/[email protected]':
     resolution: {integrity: sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==}
     resolution: {integrity: sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==}
 
 
+  '@types/[email protected]':
+    resolution: {integrity: sha512-o4hanZAQdNfsKecexq9L3eHICd0AAvdbLk6hA60UzGXbGH/q8b/9xv2RgR7vV3ZcHuyKVq7b37IGd/+gM4Tu+Q==}
+
   '@types/[email protected]':
   '@types/[email protected]':
     resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
     resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
 
 
@@ -6129,8 +6166,8 @@ packages:
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
     resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
 
 
-  [email protected]6.0:
-    resolution: {integrity: sha512-xryrmD4jSBQrS2IkMdcTmiS4aSKckbS7kLDCuhUn9110SQKG1w3zlq1RTqCblewg+ZYe+m3sdtzQA6cRwo5g8Q==}
+  [email protected]5.0:
+    resolution: {integrity: sha512-XKg/LnKExdLGugZrDILV7jZjI599785lDIJZLxMiiIFidCsy0a4R2ZEf+Izm67zyOuJgQYTHOmodi7igQsw3vg==}
     peerDependencies:
     peerDependencies:
       '@emotion/is-prop-valid': '*'
       '@emotion/is-prop-valid': '*'
       react: ^18.0.0 || ^19.0.0
       react: ^18.0.0 || ^19.0.0
@@ -9470,38 +9507,38 @@ packages:
     resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
     resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
     engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
     engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
 
 
-  [email protected].4:
-    resolution: {integrity: sha512-ah6YnH2dErojhFooxEzmvsoZQTMImaruZhFPfMKPBq8sb+hALRdvBNLqfc8NWlZq576FkfRZ/MSi4SHvVFT9PQ==}
+  [email protected].6:
+    resolution: {integrity: sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A==}
     cpu: [x64]
     cpu: [x64]
     os: [darwin]
     os: [darwin]
 
 
-  [email protected].4:
-    resolution: {integrity: sha512-2+Nx6LAyuXw2MdXb7pxqle3MYignLvS7OwtsP9SgtSBaMlnNlxl9BovzqdYAgkUW3AsYiQMJ/wBRb7d+xemM5A==}
+  [email protected].6:
+    resolution: {integrity: sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA==}
     cpu: [arm64]
     cpu: [arm64]
     os: [darwin]
     os: [darwin]
 
 
-  [email protected].4:
-    resolution: {integrity: sha512-5May2kjWbc8w4XxswGAl74GZ5eM4Gr6IiroqdLhXeXyfvWEdm2mFYCSWOzz0/z5cAgqyGidF1jt1qzUR8hTmOA==}
+  [email protected].6:
+    resolution: {integrity: sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA==}
     cpu: [x64]
     cpu: [x64]
     os: [linux]
     os: [linux]
 
 
-  [email protected].4:
-    resolution: {integrity: sha512-/2yqFaS3TbfxV3P5yG2JUI79P7OUQKOUvAnx4MV9Bdz6jqHsHwc9WZPpO4QseQm+NvmgY6ICORnoVPODxGUiJg==}
+  [email protected].6:
+    resolution: {integrity: sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ==}
     cpu: [arm64]
     cpu: [arm64]
     os: [linux]
     os: [linux]
 
 
-  [email protected].4:
-    resolution: {integrity: sha512-EQUO4SmaCDhO6zYohxIjJpOKRN3wlfU7jMAj3CgcyTPvQR/UFLEKAYHqJOnJtymbQmiiM/ihX6c6W6Uq0yC7mA==}
+  [email protected].6:
+    resolution: {integrity: sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg==}
     cpu: [x64]
     cpu: [x64]
     os: [win32]
     os: [win32]
 
 
-  [email protected].4:
-    resolution: {integrity: sha512-oQ8RrK1VS8lrxkLriotFq+PiF7iiGgkZtfLKF4DDKsmdbPo0O9R2mQxm7jHLuXraRCuIQDWMIw6dpcr7Iykf4A==}
+  [email protected].6:
+    resolution: {integrity: sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q==}
     cpu: [arm64]
     cpu: [arm64]
     os: [win32]
     os: [win32]
 
 
-  [email protected].4:
-    resolution: {integrity: sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA==}
+  [email protected].6:
+    resolution: {integrity: sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w==}
     hasBin: true
     hasBin: true
 
 
   [email protected]:
   [email protected]:
@@ -11424,8 +11461,8 @@ snapshots:
       '@modelcontextprotocol/sdk': 1.12.0
       '@modelcontextprotocol/sdk': 1.12.0
       google-auth-library: 9.15.1
       google-auth-library: 9.15.1
       ws: 8.18.2
       ws: 8.18.2
-      zod: 3.25.61
-      zod-to-json-schema: 3.24.5([email protected]1)
+      zod: 3.25.76
+      zod-to-json-schema: 3.24.5([email protected].76)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - bufferutil
       - bufferutil
       - encoding
       - encoding
@@ -11684,8 +11721,8 @@ snapshots:
       '@lmstudio/lms-isomorphic': 0.4.5
       '@lmstudio/lms-isomorphic': 0.4.5
       chalk: 4.1.2
       chalk: 4.1.2
       jsonschema: 1.5.0
       jsonschema: 1.5.0
-      zod: 3.25.61
-      zod-to-json-schema: 3.24.5([email protected]1)
+      zod: 3.25.76
+      zod-to-json-schema: 3.24.5([email protected].76)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - bufferutil
       - bufferutil
       - utf-8-validate
       - utf-8-validate
@@ -11751,8 +11788,8 @@ snapshots:
       express-rate-limit: 7.5.0([email protected])
       express-rate-limit: 7.5.0([email protected])
       pkce-challenge: 5.0.0
       pkce-challenge: 5.0.0
       raw-body: 3.0.0
       raw-body: 3.0.0
-      zod: 3.25.61
-      zod-to-json-schema: 3.24.5([email protected]1)
+      zod: 3.25.76
+      zod-to-json-schema: 3.24.5([email protected].76)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
@@ -12732,23 +12769,6 @@ snapshots:
   '@rollup/[email protected]':
   '@rollup/[email protected]':
     optional: true
     optional: true
 
 
-  '@roo-code/[email protected]':
-    dependencies:
-      '@roo-code/types': 1.63.0
-      ioredis: 5.6.1
-      jwt-decode: 4.0.0
-      p-wait-for: 5.0.2
-      socket.io-client: 4.8.1
-      zod: 3.25.76
-    transitivePeerDependencies:
-      - bufferutil
-      - supports-color
-      - utf-8-validate
-
-  '@roo-code/[email protected]':
-    dependencies:
-      zod: 3.25.76
-
   '@sec-ant/[email protected]': {}
   '@sec-ant/[email protected]': {}
 
 
   '@sevinf/[email protected]': {}
   '@sevinf/[email protected]': {}
@@ -13799,6 +13819,8 @@ snapshots:
 
 
   '@types/[email protected]': {}
   '@types/[email protected]': {}
 
 
+  '@types/[email protected]': {}
+
   '@types/[email protected]':
   '@types/[email protected]':
     dependencies:
     dependencies:
       '@types/node': 24.2.1
       '@types/node': 24.2.1
@@ -15551,11 +15573,11 @@ snapshots:
       string.prototype.matchall: 4.0.12
       string.prototype.matchall: 4.0.12
       string.prototype.repeat: 1.0.0
       string.prototype.repeat: 1.0.0
 
 
-  [email protected]([email protected]([email protected]))([email protected].4):
+  [email protected]([email protected]([email protected]))([email protected].6):
     dependencies:
     dependencies:
       dotenv: 16.0.3
       dotenv: 16.0.3
       eslint: 9.27.0([email protected])
       eslint: 9.27.0([email protected])
-      turbo: 2.5.4
+      turbo: 2.5.6
 
 
   [email protected]:
   [email protected]:
     dependencies:
     dependencies:
@@ -16026,7 +16048,7 @@ snapshots:
 
 
   [email protected]: {}
   [email protected]: {}
 
 
-  [email protected]6.0(@emotion/[email protected])([email protected]([email protected]))([email protected]):
+  [email protected]5.0(@emotion/[email protected])([email protected]([email protected]))([email protected]):
     dependencies:
     dependencies:
       motion-dom: 12.16.0
       motion-dom: 12.16.0
       motion-utils: 12.12.1
       motion-utils: 12.12.1
@@ -19977,32 +19999,32 @@ snapshots:
 
 
   [email protected]: {}
   [email protected]: {}
 
 
-  [email protected].4:
+  [email protected].6:
     optional: true
     optional: true
 
 
-  [email protected].4:
+  [email protected].6:
     optional: true
     optional: true
 
 
-  [email protected].4:
+  [email protected].6:
     optional: true
     optional: true
 
 
-  [email protected].4:
+  [email protected].6:
     optional: true
     optional: true
 
 
-  [email protected].4:
+  [email protected].6:
     optional: true
     optional: true
 
 
-  [email protected].4:
+  [email protected].6:
     optional: true
     optional: true
 
 
-  [email protected].4:
+  [email protected].6:
     optionalDependencies:
     optionalDependencies:
-      turbo-darwin-64: 2.5.4
-      turbo-darwin-arm64: 2.5.4
-      turbo-linux-64: 2.5.4
-      turbo-linux-arm64: 2.5.4
-      turbo-windows-64: 2.5.4
-      turbo-windows-arm64: 2.5.4
+      turbo-darwin-64: 2.5.6
+      turbo-darwin-arm64: 2.5.6
+      turbo-linux-64: 2.5.6
+      turbo-linux-arm64: 2.5.6
+      turbo-windows-64: 2.5.6
+      turbo-windows-arm64: 2.5.6
 
 
   [email protected]:
   [email protected]:
     dependencies:
     dependencies:
@@ -20836,6 +20858,10 @@ snapshots:
     dependencies:
     dependencies:
       zod: 3.25.61
       zod: 3.25.61
 
 
+  [email protected]([email protected]):
+    dependencies:
+      zod: 3.25.76
+
   [email protected]([email protected])([email protected]):
   [email protected]([email protected])([email protected]):
     dependencies:
     dependencies:
       typescript: 5.8.3
       typescript: 5.8.3

+ 0 - 340
scripts/link-packages.ts

@@ -1,340 +0,0 @@
-import { spawn, execSync, type ChildProcess } from "child_process"
-import * as path from "path"
-import * as fs from "fs"
-import { fileURLToPath } from "url"
-import { glob } from "glob"
-
-// @ts-expect-error - TS1470: We only run this script with tsx so it will never
-// compile to CJS and it's safe to ignore this tsc error.
-const __filename = fileURLToPath(import.meta.url)
-const __dirname = path.dirname(__filename)
-
-interface PackageConfig {
-	readonly name: string
-	readonly sourcePath: string
-	readonly targetPaths: readonly string[]
-	readonly replacePath?: string
-	readonly npmPath: string
-	readonly watchCommand?: string
-	readonly watchOutput?: {
-		readonly start: string[]
-		readonly stop: string[]
-	}
-}
-
-interface Config {
-	readonly packages: readonly PackageConfig[]
-}
-
-interface WatcherResult {
-	child: ChildProcess
-}
-
-interface NpmPackage {
-	name?: string
-	version?: string
-	type: "module"
-	dependencies: Record<string, string>
-	main: string
-	module: string
-	types: string
-	exports: {
-		".": {
-			types: string
-			import: string
-			require: {
-				types: string
-				default: string
-			}
-		}
-	}
-	files: string[]
-}
-
-const config: Config = {
-	packages: [
-		{
-			name: "@roo-code/cloud",
-			sourcePath: "../Roo-Code-Cloud/packages/sdk",
-			targetPaths: ["src/node_modules/@roo-code/cloud"] as const,
-			replacePath: "node_modules/.pnpm/@roo-code+cloud*",
-			npmPath: "npm",
-			watchCommand: "pnpm build:development:watch",
-			watchOutput: {
-				start: ["CLI Building", "CLI Change detected"],
-				stop: ["DTS ⚡️ Build success"],
-			},
-		},
-	],
-} as const
-
-const args = process.argv.slice(2)
-const packageName = args.find((arg) => !arg.startsWith("--"))
-const watchMode = !args.includes("--no-watch")
-const unlink = args.includes("--unlink")
-
-const packages: readonly PackageConfig[] = packageName
-	? config.packages.filter((p) => p.name === packageName)
-	: config.packages
-
-if (!packages.length) {
-	console.error(`Package '${packageName}' not found`)
-	process.exit(1)
-}
-
-function pathExists(filePath: string): boolean {
-	try {
-		fs.accessSync(filePath)
-		return true
-	} catch {
-		return false
-	}
-}
-
-function copyRecursiveSync(src: string, dest: string): void {
-	const exists = pathExists(src)
-
-	if (!exists) {
-		return
-	}
-
-	const stats = fs.statSync(src)
-	const isDirectory = stats.isDirectory()
-
-	if (isDirectory) {
-		if (!pathExists(dest)) {
-			fs.mkdirSync(dest, { recursive: true })
-		}
-
-		const children = fs.readdirSync(src)
-
-		children.forEach((childItemName) => {
-			copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName))
-		})
-	} else {
-		fs.copyFileSync(src, dest)
-	}
-}
-
-function generateNpmPackageJson(sourcePath: string, npmPath: string): string {
-	const npmDir = path.join(sourcePath, npmPath)
-	const npmPackagePath = path.join(npmDir, "package.json")
-	const npmMetadataPath = path.join(npmDir, "package.metadata.json")
-	const monorepoPackagePath = path.join(sourcePath, "package.json")
-
-	if (pathExists(npmPackagePath)) {
-		return npmPackagePath
-	}
-
-	if (!pathExists(npmMetadataPath)) {
-		throw new Error(`No package.metadata.json found in ${npmDir}`)
-	}
-
-	const monorepoPackageContent = fs.readFileSync(monorepoPackagePath, "utf8")
-
-	const monorepoPackage = JSON.parse(monorepoPackageContent) as {
-		dependencies?: Record<string, string>
-	}
-
-	const npmMetadataContent = fs.readFileSync(npmMetadataPath, "utf8")
-	const npmMetadata = JSON.parse(npmMetadataContent) as Partial<NpmPackage>
-
-	const npmPackage: NpmPackage = {
-		...npmMetadata,
-		type: "module",
-		dependencies: monorepoPackage.dependencies || {},
-		main: "./dist/index.cjs",
-		module: "./dist/index.js",
-		types: "./dist/index.d.ts",
-		exports: {
-			".": {
-				types: "./dist/index.d.ts",
-				import: "./dist/index.js",
-				require: {
-					types: "./dist/index.d.cts",
-					default: "./dist/index.cjs",
-				},
-			},
-		},
-		files: ["dist"],
-	}
-
-	fs.writeFileSync(npmPackagePath, JSON.stringify(npmPackage, null, 2) + "\n")
-
-	return npmPackagePath
-}
-
-function linkPackage(pkg: PackageConfig): void {
-	const sourcePath = path.resolve(__dirname, "..", pkg.sourcePath)
-
-	if (!pathExists(sourcePath)) {
-		console.error(`❌ Source not found: ${sourcePath}`)
-		process.exit(1)
-	}
-
-	generateNpmPackageJson(sourcePath, pkg.npmPath)
-
-	for (const currentTargetPath of pkg.targetPaths) {
-		const targetPath = path.resolve(__dirname, "..", currentTargetPath)
-
-		if (pathExists(targetPath)) {
-			fs.rmSync(targetPath, { recursive: true, force: true })
-		}
-
-		const parentDir = path.dirname(targetPath)
-		fs.mkdirSync(parentDir, { recursive: true })
-
-		const linkSource = pkg.npmPath ? path.join(sourcePath, pkg.npmPath) : sourcePath
-		copyRecursiveSync(linkSource, targetPath)
-	}
-}
-
-function unlinkPackage(pkg: PackageConfig): void {
-	for (const currentTargetPath of pkg.targetPaths) {
-		const targetPath = path.resolve(__dirname, "..", currentTargetPath)
-
-		if (pathExists(targetPath)) {
-			fs.rmSync(targetPath, { recursive: true, force: true })
-			console.log(`🗑️  Removed ${pkg.name} from ${currentTargetPath}`)
-		}
-	}
-}
-
-function startWatch(pkg: PackageConfig): WatcherResult {
-	if (!pkg.watchCommand) {
-		throw new Error(`Package ${pkg.name} has no watch command configured`)
-	}
-
-	const commandParts = pkg.watchCommand.split(" ")
-	const [cmd, ...args] = commandParts
-
-	if (!cmd) {
-		throw new Error(`Invalid watch command for ${pkg.name}`)
-	}
-
-	console.log(`👀 Watching for changes to ${pkg.sourcePath} with ${cmd} ${args.join(" ")}`)
-
-	const child = spawn(cmd, args, {
-		cwd: path.resolve(__dirname, "..", pkg.sourcePath),
-		stdio: "pipe",
-		shell: true,
-	})
-
-	let debounceTimer: NodeJS.Timeout | null = null
-
-	const DEBOUNCE_DELAY = 500
-
-	if (child.stdout) {
-		child.stdout.on("data", (data: Buffer) => {
-			const output = data.toString()
-
-			const isStarting = pkg.watchOutput?.start.some((start) => output.includes(start))
-
-			const isDone = pkg.watchOutput?.stop.some((stop) => output.includes(stop))
-
-			if (isStarting) {
-				console.log(`🔨 Building ${pkg.name}...`)
-
-				if (debounceTimer) {
-					clearTimeout(debounceTimer)
-					debounceTimer = null
-				}
-			}
-
-			if (isDone) {
-				console.log(`✅ Built ${pkg.name}`)
-
-				if (debounceTimer) {
-					clearTimeout(debounceTimer)
-				}
-
-				debounceTimer = setTimeout(() => {
-					linkPackage(pkg)
-
-					console.log(`♻️ Copied ${pkg.name} to ${pkg.targetPaths.length} paths\n`)
-
-					debounceTimer = null
-				}, DEBOUNCE_DELAY)
-			}
-		})
-	}
-
-	if (child.stderr) {
-		child.stderr.on("data", (data: Buffer) => {
-			console.log(`❌ "${data.toString()}"`)
-		})
-	}
-
-	return { child }
-}
-
-function main(): void {
-	if (unlink) {
-		packages.forEach(unlinkPackage)
-
-		console.log("\n📦 Restoring npm packages...")
-
-		try {
-			execSync("pnpm install", { cwd: __dirname, stdio: "ignore" })
-			console.log("✅ npm packages restored")
-		} catch (error) {
-			console.error(`❌ Failed to restore packages: ${error instanceof Error ? error.message : String(error)}`)
-
-			console.log("   Run 'pnpm install' manually if needed")
-		}
-	} else {
-		packages.forEach((pkg) => {
-			linkPackage(pkg)
-
-			if (pkg.replacePath) {
-				const replacePattern = path.resolve(__dirname, "..", pkg.replacePath)
-
-				try {
-					const matchedPaths = glob.sync(replacePattern)
-
-					if (matchedPaths.length > 0) {
-						matchedPaths.forEach((matchedPath: string) => {
-							if (pathExists(matchedPath)) {
-								fs.rmSync(matchedPath, { recursive: true, force: true })
-								console.log(`🗑️  Removed ${pkg.name} from ${matchedPath}`)
-							}
-						})
-					} else {
-						if (pathExists(replacePattern)) {
-							fs.rmSync(replacePattern, { recursive: true, force: true })
-							console.log(`🗑️  Removed ${pkg.name} from ${replacePattern}`)
-						}
-					}
-				} catch (error) {
-					console.error(
-						`❌ Error processing replace path: ${error instanceof Error ? error.message : String(error)}`,
-					)
-				}
-			}
-		})
-
-		if (watchMode) {
-			const packagesWithWatch = packages.filter(
-				(pkg): pkg is PackageConfig & { watchCommand: string } => pkg.watchCommand !== undefined,
-			)
-
-			const watchers = packagesWithWatch.map(startWatch)
-
-			if (watchers.length > 0) {
-				process.on("SIGINT", () => {
-					console.log("\n👋 Stopping watchers...")
-
-					watchers.forEach((w) => {
-						if (w.child) {
-							w.child.kill()
-						}
-					})
-
-					process.exit(0)
-				})
-			}
-		}
-	}
-}
-
-main()

+ 2 - 2
src/__tests__/command-integration.spec.ts

@@ -1,7 +1,7 @@
-import { describe, it, expect } from "vitest"
-import { getCommands, getCommand, getCommandNames } from "../services/command/commands"
 import * as path from "path"
 import * as path from "path"
 
 
+import { getCommands, getCommand, getCommandNames } from "../services/command/commands"
+
 describe("Command Integration Tests", () => {
 describe("Command Integration Tests", () => {
 	const testWorkspaceDir = path.join(__dirname, "../../")
 	const testWorkspaceDir = path.join(__dirname, "../../")
 
 

+ 0 - 1
src/__tests__/command-mentions.spec.ts

@@ -1,4 +1,3 @@
-import { describe, it, expect, beforeEach, vi } from "vitest"
 import { parseMentions } from "../core/mentions"
 import { parseMentions } from "../core/mentions"
 import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
 import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
 import { getCommand } from "../services/command/commands"
 import { getCommand } from "../services/command/commands"

+ 0 - 1
src/__tests__/commands.spec.ts

@@ -1,4 +1,3 @@
-import { describe, it, expect } from "vitest"
 import {
 import {
 	getCommands,
 	getCommands,
 	getCommand,
 	getCommand,

+ 0 - 2
src/api/providers/__tests__/bedrock-error-handling.spec.ts

@@ -1,5 +1,3 @@
-import { vi } from "vitest"
-
 // Mock BedrockRuntimeClient and commands
 // Mock BedrockRuntimeClient and commands
 const mockSend = vi.fn()
 const mockSend = vi.fn()
 
 

+ 0 - 2
src/api/providers/__tests__/cerebras.spec.ts

@@ -1,5 +1,3 @@
-import { describe, it, expect, vi, beforeEach } from "vitest"
-
 // Mock i18n
 // Mock i18n
 vi.mock("../../i18n", () => ({
 vi.mock("../../i18n", () => ({
 	t: vi.fn((key: string, params?: Record<string, any>) => {
 	t: vi.fn((key: string, params?: Record<string, any>) => {

+ 2 - 2
src/api/providers/__tests__/claude-code-caching.spec.ts

@@ -1,10 +1,10 @@
-import { describe, it, expect, vi, beforeEach } from "vitest"
+import type { Anthropic } from "@anthropic-ai/sdk"
+
 import { ClaudeCodeHandler } from "../claude-code"
 import { ClaudeCodeHandler } from "../claude-code"
 import { runClaudeCode } from "../../../integrations/claude-code/run"
 import { runClaudeCode } from "../../../integrations/claude-code/run"
 import type { ApiHandlerOptions } from "../../../shared/api"
 import type { ApiHandlerOptions } from "../../../shared/api"
 import type { ClaudeCodeMessage } from "../../../integrations/claude-code/types"
 import type { ClaudeCodeMessage } from "../../../integrations/claude-code/types"
 import type { ApiStreamUsageChunk } from "../../transform/stream"
 import type { ApiStreamUsageChunk } from "../../transform/stream"
-import type { Anthropic } from "@anthropic-ai/sdk"
 
 
 // Mock the runClaudeCode function
 // Mock the runClaudeCode function
 vi.mock("../../../integrations/claude-code/run", () => ({
 vi.mock("../../../integrations/claude-code/run", () => ({

+ 0 - 1
src/api/providers/__tests__/claude-code.spec.ts

@@ -1,4 +1,3 @@
-import { describe, test, expect, vi, beforeEach } from "vitest"
 import { ClaudeCodeHandler } from "../claude-code"
 import { ClaudeCodeHandler } from "../claude-code"
 import { ApiHandlerOptions } from "../../../shared/api"
 import { ApiHandlerOptions } from "../../../shared/api"
 import { ClaudeCodeMessage } from "../../../integrations/claude-code/types"
 import { ClaudeCodeMessage } from "../../../integrations/claude-code/types"

+ 0 - 1
src/api/providers/__tests__/constants.spec.ts

@@ -1,6 +1,5 @@
 // npx vitest run src/api/providers/__tests__/constants.spec.ts
 // npx vitest run src/api/providers/__tests__/constants.spec.ts
 
 
-import { describe, it, expect } from "vitest"
 import { DEFAULT_HEADERS } from "../constants"
 import { DEFAULT_HEADERS } from "../constants"
 import { Package } from "../../../shared/package"
 import { Package } from "../../../shared/package"
 
 

+ 1 - 1
src/api/providers/__tests__/gemini-handler.spec.ts

@@ -1,5 +1,5 @@
-import { describe, it, expect, vi } from "vitest"
 import { t } from "i18next"
 import { t } from "i18next"
+
 import { GeminiHandler } from "../gemini"
 import { GeminiHandler } from "../gemini"
 import type { ApiHandlerOptions } from "../../../shared/api"
 import type { ApiHandlerOptions } from "../../../shared/api"
 
 

+ 2 - 2
src/api/providers/__tests__/io-intelligence.spec.ts

@@ -1,7 +1,7 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
+import { Anthropic } from "@anthropic-ai/sdk"
+
 import { IOIntelligenceHandler } from "../io-intelligence"
 import { IOIntelligenceHandler } from "../io-intelligence"
 import type { ApiHandlerOptions } from "../../../shared/api"
 import type { ApiHandlerOptions } from "../../../shared/api"
-import { Anthropic } from "@anthropic-ai/sdk"
 
 
 const mockCreate = vi.fn()
 const mockCreate = vi.fn()
 
 

+ 0 - 1
src/api/providers/__tests__/lite-llm.spec.ts

@@ -1,4 +1,3 @@
-import { describe, it, expect, vi, beforeEach } from "vitest"
 import OpenAI from "openai"
 import OpenAI from "openai"
 import { Anthropic } from "@anthropic-ai/sdk"
 import { Anthropic } from "@anthropic-ai/sdk"
 
 

+ 0 - 1
src/api/providers/__tests__/roo.spec.ts

@@ -100,7 +100,6 @@ vitest.mock("../../../i18n", () => ({
 // Import after mocks are set up
 // Import after mocks are set up
 import { RooHandler } from "../roo"
 import { RooHandler } from "../roo"
 import { CloudService } from "@roo-code/cloud"
 import { CloudService } from "@roo-code/cloud"
-import { t } from "../../../i18n"
 
 
 describe("RooHandler", () => {
 describe("RooHandler", () => {
 	let handler: RooHandler
 	let handler: RooHandler

+ 4 - 3
src/api/providers/fetchers/__tests__/lmstudio.test.ts

@@ -1,8 +1,9 @@
 import axios from "axios"
 import axios from "axios"
-import { vi, describe, it, expect, beforeEach } from "vitest"
-import { LMStudioClient, LLM, LLMInstanceInfo, LLMInfo } from "@lmstudio/sdk"
+import { LMStudioClient, LLMInstanceInfo, LLMInfo } from "@lmstudio/sdk"
+
+import { ModelInfo, lMStudioDefaultModelInfo } from "@roo-code/types"
+
 import { getLMStudioModels, parseLMStudioModel } from "../lmstudio"
 import { getLMStudioModels, parseLMStudioModel } from "../lmstudio"
-import { ModelInfo, lMStudioDefaultModelInfo } from "@roo-code/types" // ModelInfo is a type
 
 
 // Mock axios
 // Mock axios
 vi.mock("axios")
 vi.mock("axios")

+ 1 - 2
src/api/providers/fetchers/__tests__/ollama.test.ts

@@ -1,6 +1,5 @@
 import axios from "axios"
 import axios from "axios"
-import path from "path"
-import { vi, describe, it, expect, beforeEach } from "vitest"
+
 import { getOllamaModels, parseOllamaModel } from "../ollama"
 import { getOllamaModels, parseOllamaModel } from "../ollama"
 import ollamaModelsData from "./fixtures/ollama-model-details.json"
 import ollamaModelsData from "./fixtures/ollama-model-details.json"
 
 

+ 1 - 0
src/api/providers/roo.ts

@@ -1,4 +1,5 @@
 import { Anthropic } from "@anthropic-ai/sdk"
 import { Anthropic } from "@anthropic-ai/sdk"
+
 import { rooDefaultModelId, rooModels, type RooModelId } from "@roo-code/types"
 import { rooDefaultModelId, rooModels, type RooModelId } from "@roo-code/types"
 import { CloudService } from "@roo-code/cloud"
 import { CloudService } from "@roo-code/cloud"
 
 

+ 0 - 2
src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts

@@ -1,10 +1,8 @@
 // npx vitest src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts
 // npx vitest src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts
 
 
-import { describe, it, expect, beforeEach } from "vitest"
 import { AssistantMessageParser } from "../AssistantMessageParser"
 import { AssistantMessageParser } from "../AssistantMessageParser"
 import { AssistantMessageContent } from "../parseAssistantMessage"
 import { AssistantMessageContent } from "../parseAssistantMessage"
 import { TextContent, ToolUse } from "../../../shared/tools"
 import { TextContent, ToolUse } from "../../../shared/tools"
-import { toolNames } from "@roo-code/types"
 
 
 /**
 /**
  * Helper to filter out empty text content blocks.
  * Helper to filter out empty text content blocks.

+ 1 - 1
src/core/context/context-management/__tests__/context-error-handling.test.ts

@@ -1,5 +1,5 @@
-import { describe, it, expect, vi } from "vitest"
 import { APIError } from "openai"
 import { APIError } from "openai"
+
 import { checkContextWindowExceededError } from "../context-error-handling"
 import { checkContextWindowExceededError } from "../context-error-handling"
 
 
 describe("checkContextWindowExceededError", () => {
 describe("checkContextWindowExceededError", () => {

+ 1 - 2
src/core/mentions/__tests__/index.spec.ts

@@ -1,10 +1,9 @@
 // npx vitest core/mentions/__tests__/index.spec.ts
 // npx vitest core/mentions/__tests__/index.spec.ts
 
 
-import { describe, it, expect, vi, beforeEach } from "vitest"
 import * as vscode from "vscode"
 import * as vscode from "vscode"
+
 import { parseMentions } from "../index"
 import { parseMentions } from "../index"
 import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher"
 import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher"
-import { t } from "../../../i18n"
 
 
 // Mock vscode
 // Mock vscode
 vi.mock("vscode", () => ({
 vi.mock("vscode", () => ({

+ 0 - 1
src/core/mentions/__tests__/processUserContentMentions.spec.ts

@@ -1,6 +1,5 @@
 // npx vitest core/mentions/__tests__/processUserContentMentions.spec.ts
 // npx vitest core/mentions/__tests__/processUserContentMentions.spec.ts
 
 
-import { describe, it, expect, vi, beforeEach } from "vitest"
 import { processUserContentMentions } from "../processUserContentMentions"
 import { processUserContentMentions } from "../processUserContentMentions"
 import { parseMentions } from "../index"
 import { parseMentions } from "../index"
 import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher"
 import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher"

+ 2 - 2
src/core/prompts/__tests__/get-prompt-component.spec.ts

@@ -1,7 +1,7 @@
-import { describe, it, expect } from "vitest"
-import { getPromptComponent } from "../system"
 import type { CustomModePrompts } from "@roo-code/types"
 import type { CustomModePrompts } from "@roo-code/types"
 
 
+import { getPromptComponent } from "../system"
+
 describe("getPromptComponent", () => {
 describe("getPromptComponent", () => {
 	it("should return undefined for empty objects", () => {
 	it("should return undefined for empty objects", () => {
 		const customModePrompts: CustomModePrompts = {
 		const customModePrompts: CustomModePrompts = {

+ 0 - 1
src/core/prompts/sections/__tests__/custom-instructions-global.spec.ts

@@ -1,5 +1,4 @@
 import * as path from "path"
 import * as path from "path"
-import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
 
 
 // Use vi.hoisted to ensure mocks are available during hoisting
 // Use vi.hoisted to ensure mocks are available during hoisting
 const { mockHomedir, mockStat, mockReadFile, mockReaddir, mockGetRooDirectoriesForCwd, mockGetGlobalRooDirectory } =
 const { mockHomedir, mockStat, mockReadFile, mockReaddir, mockGetRooDirectoriesForCwd, mockGetGlobalRooDirectory } =

+ 0 - 2
src/core/prompts/sections/__tests__/custom-instructions-path-detection.spec.ts

@@ -1,5 +1,3 @@
-import { describe, it, expect, vi } from "vitest"
-import * as os from "os"
 import * as path from "path"
 import * as path from "path"
 
 
 describe("custom-instructions path detection", () => {
 describe("custom-instructions path detection", () => {

+ 0 - 1
src/core/prompts/tools/__tests__/fetch-instructions.spec.ts

@@ -1,4 +1,3 @@
-import { describe, it, expect } from "vitest"
 import { getFetchInstructionsDescription } from "../fetch-instructions"
 import { getFetchInstructionsDescription } from "../fetch-instructions"
 
 
 describe("getFetchInstructionsDescription", () => {
 describe("getFetchInstructionsDescription", () => {

+ 0 - 1
src/core/prompts/tools/__tests__/new-task.spec.ts

@@ -1,4 +1,3 @@
-import { describe, it, expect } from "vitest"
 import { getNewTaskDescription } from "../new-task"
 import { getNewTaskDescription } from "../new-task"
 import { ToolArgs } from "../types"
 import { ToolArgs } from "../types"
 
 

+ 2 - 2
src/core/task/__tests__/AutoApprovalHandler.spec.ts

@@ -1,7 +1,7 @@
-import { describe, it, expect, vi, beforeEach } from "vitest"
-import { AutoApprovalHandler } from "../AutoApprovalHandler"
 import { GlobalState, ClineMessage } from "@roo-code/types"
 import { GlobalState, ClineMessage } from "@roo-code/types"
 
 
+import { AutoApprovalHandler } from "../AutoApprovalHandler"
+
 // Mock getApiMetrics
 // Mock getApiMetrics
 vi.mock("../../../shared/getApiMetrics", () => ({
 vi.mock("../../../shared/getApiMetrics", () => ({
 	getApiMetrics: vi.fn(),
 	getApiMetrics: vi.fn(),

+ 2 - 2
src/core/task/__tests__/Task.dispose.test.ts

@@ -1,7 +1,7 @@
+import { ProviderSettings } from "@roo-code/types"
+
 import { Task } from "../Task"
 import { Task } from "../Task"
 import { ClineProvider } from "../../webview/ClineProvider"
 import { ClineProvider } from "../../webview/ClineProvider"
-import { ProviderSettings } from "@roo-code/types"
-import { vi, describe, test, expect, beforeEach, afterEach } from "vitest"
 
 
 // Mock dependencies
 // Mock dependencies
 vi.mock("../../webview/ClineProvider")
 vi.mock("../../webview/ClineProvider")

+ 0 - 1
src/core/tools/__tests__/askFollowupQuestionTool.spec.ts

@@ -1,4 +1,3 @@
-import { describe, it, expect, vi } from "vitest"
 import { askFollowupQuestionTool } from "../askFollowupQuestionTool"
 import { askFollowupQuestionTool } from "../askFollowupQuestionTool"
 import { ToolUse } from "../../../shared/tools"
 import { ToolUse } from "../../../shared/tools"
 
 

+ 1 - 1
src/core/tools/__tests__/attemptCompletionTool.spec.ts

@@ -1,5 +1,5 @@
-import { describe, it, expect, vi, beforeEach } from "vitest"
 import { TodoItem } from "@roo-code/types"
 import { TodoItem } from "@roo-code/types"
+
 import { AttemptCompletionToolUse } from "../../../shared/tools"
 import { AttemptCompletionToolUse } from "../../../shared/tools"
 
 
 // Mock the formatResponse module before importing the tool
 // Mock the formatResponse module before importing the tool

+ 3 - 2
src/core/webview/ClineProvider.ts

@@ -29,16 +29,17 @@ import {
 	type TerminalActionId,
 	type TerminalActionId,
 	type TerminalActionPromptType,
 	type TerminalActionPromptType,
 	type HistoryItem,
 	type HistoryItem,
-	type ClineAsk,
+	type CloudUserInfo,
 	RooCodeEventName,
 	RooCodeEventName,
 	requestyDefaultModelId,
 	requestyDefaultModelId,
 	openRouterDefaultModelId,
 	openRouterDefaultModelId,
 	glamaDefaultModelId,
 	glamaDefaultModelId,
 	DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
 	DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
 	DEFAULT_WRITE_DELAY_MS,
 	DEFAULT_WRITE_DELAY_MS,
+	ORGANIZATION_ALLOW_ALL,
 } from "@roo-code/types"
 } from "@roo-code/types"
 import { TelemetryService } from "@roo-code/telemetry"
 import { TelemetryService } from "@roo-code/telemetry"
-import { type CloudUserInfo, CloudService, ORGANIZATION_ALLOW_ALL, getRooCodeApiUrl } from "@roo-code/cloud"
+import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud"
 
 
 import { Package } from "../../shared/package"
 import { Package } from "../../shared/package"
 import { findLast } from "../../shared/array"
 import { findLast } from "../../shared/array"

+ 1 - 2
src/core/webview/__tests__/ClineProvider.spec.ts

@@ -4,9 +4,8 @@ import Anthropic from "@anthropic-ai/sdk"
 import * as vscode from "vscode"
 import * as vscode from "vscode"
 import axios from "axios"
 import axios from "axios"
 
 
-import { type ProviderSettingsEntry, type ClineMessage } from "@roo-code/types"
+import { type ProviderSettingsEntry, type ClineMessage, ORGANIZATION_ALLOW_ALL } from "@roo-code/types"
 import { TelemetryService } from "@roo-code/telemetry"
 import { TelemetryService } from "@roo-code/telemetry"
-import { ORGANIZATION_ALLOW_ALL } from "@roo-code/cloud"
 
 
 import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
 import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
 import { defaultModeSlug } from "../../../shared/modes"
 import { defaultModeSlug } from "../../../shared/modes"

+ 2 - 2
src/core/webview/__tests__/messageEnhancer.test.ts

@@ -1,7 +1,7 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
-import { MessageEnhancer } from "../messageEnhancer"
 import { ProviderSettings, ClineMessage } from "@roo-code/types"
 import { ProviderSettings, ClineMessage } from "@roo-code/types"
 import { TelemetryService } from "@roo-code/telemetry"
 import { TelemetryService } from "@roo-code/telemetry"
+
+import { MessageEnhancer } from "../messageEnhancer"
 import * as singleCompletionHandlerModule from "../../../utils/single-completion-handler"
 import * as singleCompletionHandlerModule from "../../../utils/single-completion-handler"
 import { ProviderSettingsManager } from "../../config/ProviderSettingsManager"
 import { ProviderSettingsManager } from "../../config/ProviderSettingsManager"
 
 

+ 1 - 0
src/core/webview/webviewMessageHandler.ts

@@ -14,6 +14,7 @@ import {
 } from "@roo-code/types"
 } from "@roo-code/types"
 import { CloudService } from "@roo-code/cloud"
 import { CloudService } from "@roo-code/cloud"
 import { TelemetryService } from "@roo-code/telemetry"
 import { TelemetryService } from "@roo-code/telemetry"
+
 import { type ApiMessage } from "../task-persistence/apiMessages"
 import { type ApiMessage } from "../task-persistence/apiMessages"
 
 
 import { ClineProvider } from "./ClineProvider"
 import { ClineProvider } from "./ClineProvider"

+ 2 - 1
src/extension.ts

@@ -12,7 +12,8 @@ try {
 	console.warn("Failed to load environment variables:", e)
 	console.warn("Failed to load environment variables:", e)
 }
 }
 
 
-import { CloudService, ExtensionBridgeService, type CloudUserInfo } from "@roo-code/cloud"
+import type { CloudUserInfo } from "@roo-code/types"
+import { CloudService, ExtensionBridgeService } from "@roo-code/cloud"
 import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry"
 import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry"
 
 
 import "./utils/path" // Necessary to have access to String.prototype.toPosix.
 import "./utils/path" // Necessary to have access to String.prototype.toPosix.

+ 2 - 2
src/integrations/claude-code/__tests__/message-filter.spec.ts

@@ -1,7 +1,7 @@
-import { describe, test, expect } from "vitest"
-import { filterMessagesForClaudeCode } from "../message-filter"
 import type { Anthropic } from "@anthropic-ai/sdk"
 import type { Anthropic } from "@anthropic-ai/sdk"
 
 
+import { filterMessagesForClaudeCode } from "../message-filter"
+
 describe("filterMessagesForClaudeCode", () => {
 describe("filterMessagesForClaudeCode", () => {
 	test("should pass through string messages unchanged", () => {
 	test("should pass through string messages unchanged", () => {
 		const messages: Anthropic.Messages.MessageParam[] = [
 		const messages: Anthropic.Messages.MessageParam[] = [

+ 0 - 2
src/integrations/claude-code/__tests__/run.spec.ts

@@ -1,5 +1,3 @@
-import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"
-
 // Mock i18n system
 // Mock i18n system
 vi.mock("../../i18n", () => ({
 vi.mock("../../i18n", () => ({
 	t: vi.fn((key: string, options?: Record<string, any>) => {
 	t: vi.fn((key: string, options?: Record<string, any>) => {

+ 1 - 1
src/integrations/misc/__tests__/extract-text-large-files.spec.ts

@@ -1,7 +1,7 @@
 // npx vitest run integrations/misc/__tests__/extract-text-large-files.spec.ts
 // npx vitest run integrations/misc/__tests__/extract-text-large-files.spec.ts
 
 
-import { describe, it, expect, vi, beforeEach, Mock } from "vitest"
 import * as fs from "fs/promises"
 import * as fs from "fs/promises"
+
 import { extractTextFromFile } from "../extract-text"
 import { extractTextFromFile } from "../extract-text"
 import { countFileLines } from "../line-counter"
 import { countFileLines } from "../line-counter"
 import { readLines } from "../read-lines"
 import { readLines } from "../read-lines"

+ 1 - 3
src/integrations/misc/__tests__/open-file.spec.ts

@@ -1,7 +1,5 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
 import * as vscode from "vscode"
 import * as vscode from "vscode"
-import * as path from "path"
-import * as os from "os"
+
 import { openFile } from "../open-file"
 import { openFile } from "../open-file"
 
 
 // Mock vscode module
 // Mock vscode module

+ 3 - 3
src/package.json

@@ -427,13 +427,12 @@
 		"@google/genai": "^1.0.0",
 		"@google/genai": "^1.0.0",
 		"@lmstudio/sdk": "^1.1.1",
 		"@lmstudio/sdk": "^1.1.1",
 		"@mistralai/mistralai": "^1.9.18",
 		"@mistralai/mistralai": "^1.9.18",
-		"@modelcontextprotocol/sdk": "^1.9.0",
+		"@modelcontextprotocol/sdk": "1.12.0",
 		"@qdrant/js-client-rest": "^1.14.0",
 		"@qdrant/js-client-rest": "^1.14.0",
-		"@roo-code/cloud": "^0.29.0",
+		"@roo-code/cloud": "workspace:^",
 		"@roo-code/ipc": "workspace:^",
 		"@roo-code/ipc": "workspace:^",
 		"@roo-code/telemetry": "workspace:^",
 		"@roo-code/telemetry": "workspace:^",
 		"@roo-code/types": "workspace:^",
 		"@roo-code/types": "workspace:^",
-		"@types/lodash.debounce": "^4.0.9",
 		"@vscode/codicons": "^0.0.36",
 		"@vscode/codicons": "^0.0.36",
 		"async-mutex": "^0.5.0",
 		"async-mutex": "^0.5.0",
 		"axios": "^1.7.4",
 		"axios": "^1.7.4",
@@ -504,6 +503,7 @@
 		"@types/diff": "^5.2.1",
 		"@types/diff": "^5.2.1",
 		"@types/diff-match-patch": "^1.0.36",
 		"@types/diff-match-patch": "^1.0.36",
 		"@types/glob": "^8.1.0",
 		"@types/glob": "^8.1.0",
+		"@types/lodash.debounce": "^4.0.9",
 		"@types/mocha": "^10.0.10",
 		"@types/mocha": "^10.0.10",
 		"@types/node": "20.x",
 		"@types/node": "20.x",
 		"@types/node-cache": "^4.1.3",
 		"@types/node-cache": "^4.1.3",

+ 0 - 2
src/services/browser/__tests__/BrowserSession.spec.ts

@@ -1,9 +1,7 @@
 // npx vitest services/browser/__tests__/BrowserSession.spec.ts
 // npx vitest services/browser/__tests__/BrowserSession.spec.ts
 
 
-import { describe, it, expect, vi, beforeEach } from "vitest"
 import { BrowserSession } from "../BrowserSession"
 import { BrowserSession } from "../BrowserSession"
 import { discoverChromeHostUrl, tryChromeHostUrl } from "../browserDiscovery"
 import { discoverChromeHostUrl, tryChromeHostUrl } from "../browserDiscovery"
-import { fileExistsAtPath } from "../../../utils/fs"
 
 
 // Mock dependencies
 // Mock dependencies
 vi.mock("vscode", () => ({
 vi.mock("vscode", () => ({

+ 10 - 11
src/services/browser/__tests__/UrlContentFetcher.spec.ts

@@ -1,10 +1,9 @@
 // npx vitest services/browser/__tests__/UrlContentFetcher.spec.ts
 // npx vitest services/browser/__tests__/UrlContentFetcher.spec.ts
 
 
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
-import { UrlContentFetcher } from "../UrlContentFetcher"
-import { fileExistsAtPath } from "../../../utils/fs"
 import * as path from "path"
 import * as path from "path"
 
 
+import { UrlContentFetcher } from "../UrlContentFetcher"
+
 // Mock dependencies
 // Mock dependencies
 vi.mock("vscode", () => ({
 vi.mock("vscode", () => ({
 	ExtensionContext: vi.fn(),
 	ExtensionContext: vi.fn(),
@@ -128,8 +127,8 @@ describe("UrlContentFetcher", () => {
 		it("should launch browser with correct arguments on non-Linux platforms", async () => {
 		it("should launch browser with correct arguments on non-Linux platforms", async () => {
 			// Ensure we're not on Linux for this test
 			// Ensure we're not on Linux for this test
 			const originalPlatform = process.platform
 			const originalPlatform = process.platform
-			Object.defineProperty(process, 'platform', {
-				value: 'darwin' // macOS
+			Object.defineProperty(process, "platform", {
+				value: "darwin", // macOS
 			})
 			})
 
 
 			try {
 			try {
@@ -153,8 +152,8 @@ describe("UrlContentFetcher", () => {
 				})
 				})
 			} finally {
 			} finally {
 				// Restore original platform
 				// Restore original platform
-				Object.defineProperty(process, 'platform', {
-					value: originalPlatform
+				Object.defineProperty(process, "platform", {
+					value: originalPlatform,
 				})
 				})
 			}
 			}
 		})
 		})
@@ -162,8 +161,8 @@ describe("UrlContentFetcher", () => {
 		it("should launch browser with Linux-specific arguments", async () => {
 		it("should launch browser with Linux-specific arguments", async () => {
 			// Mock process.platform to be linux
 			// Mock process.platform to be linux
 			const originalPlatform = process.platform
 			const originalPlatform = process.platform
-			Object.defineProperty(process, 'platform', {
-				value: 'linux'
+			Object.defineProperty(process, "platform", {
+				value: "linux",
 			})
 			})
 
 
 			try {
 			try {
@@ -190,8 +189,8 @@ describe("UrlContentFetcher", () => {
 				})
 				})
 			} finally {
 			} finally {
 				// Restore original platform
 				// Restore original platform
-				Object.defineProperty(process, 'platform', {
-					value: originalPlatform
+				Object.defineProperty(process, "platform", {
+					value: originalPlatform,
 				})
 				})
 			}
 			}
 		})
 		})

+ 0 - 2
src/services/code-index/__tests__/config-manager.spec.ts

@@ -1,8 +1,6 @@
 // npx vitest services/code-index/__tests__/config-manager.spec.ts
 // npx vitest services/code-index/__tests__/config-manager.spec.ts
 
 
-import { describe, it, expect, beforeEach, vi } from "vitest"
 import { CodeIndexConfigManager } from "../config-manager"
 import { CodeIndexConfigManager } from "../config-manager"
-import { ContextProxy } from "../../../core/config/ContextProxy"
 import { PreviousConfigSnapshot } from "../interfaces/config"
 import { PreviousConfigSnapshot } from "../interfaces/config"
 
 
 // Mock ContextProxy
 // Mock ContextProxy

+ 1 - 1
src/services/code-index/embedders/__tests__/mistral.spec.ts

@@ -1,5 +1,5 @@
-import { vitest, describe, it, expect, beforeEach } from "vitest"
 import type { MockedClass } from "vitest"
 import type { MockedClass } from "vitest"
+
 import { MistralEmbedder } from "../mistral"
 import { MistralEmbedder } from "../mistral"
 import { OpenAICompatibleEmbedder } from "../openai-compatible"
 import { OpenAICompatibleEmbedder } from "../openai-compatible"
 
 

+ 1 - 1
src/services/code-index/embedders/__tests__/ollama.spec.ts

@@ -1,5 +1,5 @@
-import { vitest, describe, it, expect, beforeEach, afterEach } from "vitest"
 import type { MockedFunction } from "vitest"
 import type { MockedFunction } from "vitest"
+
 import { CodeIndexOllamaEmbedder } from "../ollama"
 import { CodeIndexOllamaEmbedder } from "../ollama"
 
 
 // Mock fetch
 // Mock fetch

+ 2 - 1
src/services/code-index/embedders/__tests__/openai-compatible-rate-limit.spec.ts

@@ -1,5 +1,6 @@
-import { describe, it, expect, vi, beforeEach, afterEach, MockedClass, MockedFunction } from "vitest"
+import type { MockedClass, MockedFunction } from "vitest"
 import { OpenAI } from "openai"
 import { OpenAI } from "openai"
+
 import { OpenAICompatibleEmbedder } from "../openai-compatible"
 import { OpenAICompatibleEmbedder } from "../openai-compatible"
 
 
 // Mock the OpenAI SDK
 // Mock the OpenAI SDK

+ 2 - 2
src/services/code-index/embedders/__tests__/openai.spec.ts

@@ -1,8 +1,8 @@
-import { vitest, describe, it, expect, beforeEach, afterEach } from "vitest"
 import type { MockedClass, MockedFunction } from "vitest"
 import type { MockedClass, MockedFunction } from "vitest"
 import { OpenAI } from "openai"
 import { OpenAI } from "openai"
+
 import { OpenAiEmbedder } from "../openai"
 import { OpenAiEmbedder } from "../openai"
-import { MAX_BATCH_TOKENS, MAX_ITEM_TOKENS, MAX_BATCH_RETRIES, INITIAL_RETRY_DELAY_MS } from "../../constants"
+import { MAX_ITEM_TOKENS, INITIAL_RETRY_DELAY_MS } from "../../constants"
 
 
 // Mock the OpenAI SDK
 // Mock the OpenAI SDK
 vitest.mock("openai")
 vitest.mock("openai")

+ 0 - 1
src/services/code-index/embedders/__tests__/vercel-ai-gateway.spec.ts

@@ -1,6 +1,5 @@
 // npx vitest run src/services/code-index/embedders/__tests__/vercel-ai-gateway.spec.ts
 // npx vitest run src/services/code-index/embedders/__tests__/vercel-ai-gateway.spec.ts
 
 
-import { describe, it, expect, vi, beforeEach } from "vitest"
 import { VercelAiGatewayEmbedder } from "../vercel-ai-gateway"
 import { VercelAiGatewayEmbedder } from "../vercel-ai-gateway"
 import { OpenAICompatibleEmbedder } from "../openai-compatible"
 import { OpenAICompatibleEmbedder } from "../openai-compatible"
 
 

+ 0 - 2
src/services/code-index/processors/__tests__/parser.vb.spec.ts

@@ -1,6 +1,4 @@
-import { describe, it, expect, beforeEach, vi } from "vitest"
 import { CodeParser } from "../parser"
 import { CodeParser } from "../parser"
-import * as path from "path"
 
 
 // Mock TelemetryService
 // Mock TelemetryService
 vi.mock("../../../../../packages/telemetry/src/TelemetryService", () => ({
 vi.mock("../../../../../packages/telemetry/src/TelemetryService", () => ({

+ 1 - 1
src/services/code-index/shared/__tests__/get-relative-path.spec.ts

@@ -1,5 +1,5 @@
-import { describe, it, expect } from "vitest"
 import path from "path"
 import path from "path"
+
 import { generateNormalizedAbsolutePath, generateRelativeFilePath } from "../get-relative-path"
 import { generateNormalizedAbsolutePath, generateRelativeFilePath } from "../get-relative-path"
 
 
 describe("get-relative-path", () => {
 describe("get-relative-path", () => {

+ 0 - 1
src/services/code-index/shared/__tests__/validation-helpers.spec.ts

@@ -1,4 +1,3 @@
-import { describe, it, expect } from "vitest"
 import { sanitizeErrorMessage } from "../validation-helpers"
 import { sanitizeErrorMessage } from "../validation-helpers"
 
 
 describe("sanitizeErrorMessage", () => {
 describe("sanitizeErrorMessage", () => {

+ 0 - 1
src/services/command/__tests__/built-in-commands.spec.ts

@@ -1,4 +1,3 @@
-import { describe, it, expect } from "vitest"
 import { getBuiltInCommands, getBuiltInCommand, getBuiltInCommandNames } from "../built-in-commands"
 import { getBuiltInCommands, getBuiltInCommand, getBuiltInCommandNames } from "../built-in-commands"
 
 
 describe("Built-in Commands", () => {
 describe("Built-in Commands", () => {

+ 1 - 1
src/services/command/__tests__/frontmatter-commands.spec.ts

@@ -1,6 +1,6 @@
-import { describe, it, expect, beforeEach, vi } from "vitest"
 import fs from "fs/promises"
 import fs from "fs/promises"
 import * as path from "path"
 import * as path from "path"
+
 import { getCommand, getCommands } from "../commands"
 import { getCommand, getCommands } from "../commands"
 
 
 // Mock fs and path modules
 // Mock fs and path modules

+ 0 - 1
src/services/glob/__tests__/gitignore-integration.spec.ts

@@ -1,4 +1,3 @@
-import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"
 import * as path from "path"
 import * as path from "path"
 import * as fs from "fs"
 import * as fs from "fs"
 import * as os from "os"
 import * as os from "os"

+ 0 - 1
src/services/glob/__tests__/gitignore-test.spec.ts

@@ -1,4 +1,3 @@
-import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"
 import * as path from "path"
 import * as path from "path"
 import * as fs from "fs"
 import * as fs from "fs"
 import * as os from "os"
 import * as os from "os"

+ 1 - 2
src/services/glob/__tests__/list-files.spec.ts

@@ -1,7 +1,6 @@
-import { vi, describe, it, expect, beforeEach } from "vitest"
 import * as path from "path"
 import * as path from "path"
-import { listFiles } from "../list-files"
 import * as childProcess from "child_process"
 import * as childProcess from "child_process"
+import { listFiles } from "../list-files"
 
 
 vi.mock("child_process")
 vi.mock("child_process")
 vi.mock("fs")
 vi.mock("fs")

+ 2 - 2
src/services/marketplace/MarketplaceManager.ts

@@ -4,9 +4,9 @@ import * as path from "path"
 import * as vscode from "vscode"
 import * as vscode from "vscode"
 import * as yaml from "yaml"
 import * as yaml from "yaml"
 
 
-import type { MarketplaceItem, MarketplaceItemType, McpMarketplaceItem } from "@roo-code/types"
+import type { OrganizationSettings, MarketplaceItem, MarketplaceItemType, McpMarketplaceItem } from "@roo-code/types"
 import { TelemetryService } from "@roo-code/telemetry"
 import { TelemetryService } from "@roo-code/telemetry"
-import { type OrganizationSettings, CloudService } from "@roo-code/cloud"
+import { CloudService } from "@roo-code/cloud"
 
 
 import { GlobalFileNames } from "../../shared/globalFileNames"
 import { GlobalFileNames } from "../../shared/globalFileNames"
 import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
 import { ensureSettingsDirectoryExists } from "../../utils/globalContext"

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff