Compare commits

...
3 changed files with 125 additions and 51 deletions
+44
View File
@@ -11,6 +11,7 @@ import os from "os"
import * as path from "path"
import { HostProvider } from "@/hosts/host-provider"
import { ExtensionRegistryInfo } from "@/registry"
import { FeatureFlagsAndPayloads } from "@/services/feature-flags/providers/IFeatureFlagsProvider"
import { telemetryService } from "@/services/telemetry"
import { McpMarketplaceCatalog } from "@/shared/mcp"
import { Logger } from "@/shared/services/Logger"
@@ -67,6 +68,7 @@ export const GlobalFileNames = {
taskMetadata: "task_metadata.json",
mcpMarketplaceCatalog: "mcp_marketplace_catalog.json",
remoteConfig: (orgId: string) => `remote_config_${orgId}.json`,
featureFlagsCache: "feature_flags_cache.json",
}
export async function getDocumentsPath(): Promise<string> {
@@ -118,6 +120,13 @@ export function getClineHomePath(): string {
return path.join(os.homedir(), ".cline")
}
async function getExistingClineHomeDir(): Promise<string> {
const clineHomeDir = getClineHomePath()
await fs.mkdir(clineHomeDir, { recursive: true })
return clineHomeDir
}
export async function ensureTaskDirectoryExists(taskId: string): Promise<string> {
return getGlobalStorageDir("tasks", taskId)
}
@@ -499,6 +508,41 @@ export async function deleteRemoteConfigFromCache(organizationId: string): Promi
}
}
export interface FeatureFlagsCacheData {
updateTime: number
userId: string | null
cachedFlags: string[]
flagsPayload?: FeatureFlagsAndPayloads
}
export async function readFeatureFlagsCacheFromDisk(): Promise<FeatureFlagsCacheData | undefined> {
try {
const clineHomeDir = await getExistingClineHomeDir()
const cacheFilePath = path.join(clineHomeDir, GlobalFileNames.featureFlagsCache)
const fileExists = await fileExistsAtPath(cacheFilePath)
if (!fileExists) {
return undefined
}
const fileContents = await fs.readFile(cacheFilePath, "utf8")
return JSON.parse(fileContents)
} catch (error) {
Logger.error("Failed to read feature flags cache from disk:", error)
return undefined
}
}
export async function writeFeatureFlagsCacheToDisk(cache: FeatureFlagsCacheData): Promise<void> {
try {
const clineHomeDir = await getExistingClineHomeDir()
const cacheFilePath = path.join(clineHomeDir, GlobalFileNames.featureFlagsCache)
await fs.writeFile(cacheFilePath, JSON.stringify(cache, null, 2))
} catch (error) {
Logger.error("Failed to write feature flags cache to disk:", error)
}
}
/**
* Gets the path to the global hooks directory if it exists.
* Returns undefined if the directory doesn't exist.
+9 -6
View File
@@ -318,19 +318,22 @@ export class AuthService {
await this.sendAuthStatusUpdate()
} else {
Logger.warn("No user found after restoring auth token")
this._authenticated = false
this._clineAuthInfo = null
telemetryService.captureAuthLoggedOut(this._provider.name, LogoutReason.ERROR_RECOVERY)
await this.handleNoUserFound()
}
} catch (error) {
Logger.error("Error restoring auth token:", error)
this._authenticated = false
this._clineAuthInfo = null
telemetryService.captureAuthLoggedOut(this._provider.name, LogoutReason.ERROR_RECOVERY)
await this.handleNoUserFound()
return
}
}
private async handleNoUserFound() {
this._authenticated = false
this._clineAuthInfo = null
telemetryService.captureAuthLoggedOut(this._provider.name, LogoutReason.ERROR_RECOVERY)
await featureFlagsService.poll(null)
}
private async retrieveAuthInfo(): Promise<ClineAuthInfo | null> {
// If a refresh is already in progress, wait for it to complete
if (this._refreshPromise) {
@@ -1,4 +1,5 @@
import { clearOnboardingModelsCache, getClineOnboardingModels } from "@/core/controller/models/getClineOnboardingModels"
import { type FeatureFlagsCacheData, readFeatureFlagsCacheFromDisk, writeFeatureFlagsCacheToDisk } from "@/core/storage/disk"
import type { OnboardingModel } from "@/shared/proto/cline/state"
import { FEATURE_FLAGS, FeatureFlag, FeatureFlagDefaultValue } from "@/shared/services/feature-flags/feature-flags"
import { Logger } from "@/shared/services/Logger"
@@ -14,6 +15,14 @@ type CacheInfo = {
flagsPayload?: FeatureFlagsAndPayloads
}
function areAllFlagsCached(cachedFlagKeys: string[]): boolean {
if (cachedFlagKeys.length !== FEATURE_FLAGS.length) {
return false
}
const cachedSet = new Set(cachedFlagKeys)
return FEATURE_FLAGS.every((flag) => cachedSet.has(flag))
}
/**
* FeatureFlagsService provides feature flag functionality that works independently
* of telemetry settings. Feature flags are always available to ensure proper
@@ -28,32 +37,36 @@ export class FeatureFlagsService {
public constructor(private provider: IFeatureFlagsProvider) {}
private cache: Map<FeatureFlag, FeatureFlagPayload> = new Map()
/**
* Tracks cache update time and user ID for cache validity
*/
private cacheInfo: CacheInfo = { updateTime: 0, userId: null }
/**
* Poll all known feature flags to update their cached values
*/
public async poll(userId: string | null): Promise<void> {
// Do not update cache if last update was less than an hour ago
const timesNow = Date.now()
if (timesNow - this.cacheInfo.updateTime < DEFAULT_CACHE_TTL && this.cache.size) {
// Check if memory cache is still valid
if (timesNow - this.cacheInfo.updateTime < DEFAULT_CACHE_TTL) {
// Skip fetch if within TTL and user context is unchanged
if (this.cacheInfo.userId === userId) {
return
}
}
// Only update timestamp after successfully populating cache
this.cacheInfo = { updateTime: timesNow, userId: userId || null }
this.cacheInfo.updateTime = timesNow
this.cacheInfo.userId = userId || null
try {
const values = await this.provider.getAllFlagsAndPayloads({
flagKeys: FEATURE_FLAGS,
})
this.cacheInfo.flagsPayload = values
const isCacheValid = await this.loadFromDiskCache(userId, timesNow)
if (!isCacheValid) {
const values = await this.provider.getAllFlagsAndPayloads({
flagKeys: FEATURE_FLAGS,
})
this.cacheInfo.flagsPayload = values
await this.writeToDiskCache()
}
for (const flag of FEATURE_FLAGS) {
const payload = await this.getFeatureFlag(flag).catch(() => false)
@@ -68,6 +81,53 @@ export class FeatureFlagsService {
getClineOnboardingModels() // Refresh onboarding models cache if relevant flag changed
}
private async loadFromDiskCache(userId: string | null, currentTime: number): Promise<boolean> {
try {
const diskCache = await readFeatureFlagsCacheFromDisk()
if (!diskCache) {
return false
}
if (!diskCache.cachedFlags || !areAllFlagsCached(diskCache.cachedFlags)) {
return false
}
if (currentTime - diskCache.updateTime >= DEFAULT_CACHE_TTL) {
return false
}
if (diskCache.userId !== userId) {
return false
}
this.cacheInfo = {
updateTime: diskCache.updateTime,
userId: diskCache.userId,
flagsPayload: diskCache.flagsPayload,
}
return true
} catch (error) {
Logger.error("Failed to load feature flags from disk cache:", error)
return false
}
}
private async writeToDiskCache(): Promise<void> {
try {
const cacheData: FeatureFlagsCacheData = {
updateTime: this.cacheInfo.updateTime,
userId: this.cacheInfo.userId,
cachedFlags: [...FEATURE_FLAGS],
flagsPayload: this.cacheInfo.flagsPayload,
}
await writeFeatureFlagsCacheToDisk(cacheData)
} catch (error) {
Logger.error("Failed to write feature flags to disk cache:", error)
}
}
private async getFeatureFlag(flagName: FeatureFlag): Promise<FeatureFlagPayload | undefined> {
try {
const payload = this.cacheInfo.flagsPayload?.featureFlagPayloads?.[flagName]
@@ -98,7 +158,7 @@ export class FeatureFlagsService {
* Cache is updated periodically via poll(), and is generated on extension startup,
* and whenever the user logs in.
*/
public getBooleanFlagEnabled(flagName: FeatureFlag): boolean {
getBooleanFlagEnabled(flagName: FeatureFlag): boolean {
return this.cache.get(flagName) === true
}
@@ -127,39 +187,6 @@ export class FeatureFlagsService {
return undefined
}
/**
* Get the feature flags provider instance
* @returns The current feature flags provider
*/
public getProvider(): IFeatureFlagsProvider {
return this.provider
}
/**
* Check if feature flags are currently enabled
* @returns Boolean indicating whether feature flags are enabled
*/
public isEnabled(): boolean {
return this.provider.isEnabled()
}
/**
* Get current feature flags settings
* @returns Current feature flags settings
*/
public getSettings() {
return this.provider.getSettings()
}
/**
* For testing: directly set a feature flag in the cache
*/
public test(flagName: FeatureFlag, value: boolean) {
if (process.env.NODE_ENV === "true") {
this.cache.set(flagName, value)
}
}
/**
* Clean up resources when the service is disposed
*/