Compare commits

...
Author SHA1 Message Date
abeatrix 7741e3df0e add test 2025-09-17 00:56:48 -07:00
abeatrix cf183463b7 Use Logger & add filePath based storage 2025-09-11 17:57:10 -07:00
abeatrix 5dc1163a56 Merge branch 'main' into bee/cline-secrets-storage 2025-09-11 14:30:08 -07:00
abeatrix 44d142b21c Clean up 2025-09-11 12:00:24 -07:00
abeatrix 3d9b45ba49 Integrate credential storage
- Replace SecretStore with CredentialStorage in vscode-context
- Add fallback to SecretStore if CredentialStorage unavailable
2025-09-10 23:15:24 -07:00
abeatrix b2874bdfaf Add credential storage abstraction with platform-specific backends
- Create new credential storage system with support for macOS Keychain, Windows Credential Manager, and Linux secret-tool
- Add secrets storage abstraction layer to decouple from VS Code's secret storage
- Refactor StateManager to use new storage abstractions instead of direct VS Code context
- Add type definitions and utilities for storage system
- Enable credential storage for standalone environments
2025-09-10 23:08:56 -07:00
11 changed files with 745 additions and 40 deletions
+116
View File
@@ -0,0 +1,116 @@
import { Disposable } from "vscode"
import { Logger } from "@/services/logging/Logger"
import { StorageEventListener } from "./utils/types"
/**
* An abstract storage class that provides a template for storage operations.
* Subclasses must implement the protected abstract methods to define their storage logic.
* The public methods (get, store, delete) are final and cannot be overridden.
*/
export abstract class ClineStorage {
/**
* The name of the storage, used for logging purposes.
*/
protected name = "ClineStorage"
/**
* List of subscribers to storage change events.
*/
private readonly subscribers: Array<StorageEventListener> = []
/**
* Subscribe to storage change events.
*/
public onDidChange(callback: StorageEventListener): Disposable {
this.subscribers.push(callback)
return new Disposable(() => {
const callbackIndex = this.subscribers.indexOf(callback)
this.subscribers.splice(callbackIndex, 1)
})
}
/**
* Fire storage change event to all subscribers.
*/
protected async fire(key: string): Promise<void> {
Logger.info(`[${this.name}] onDidChange event fired for '${key}'`)
await Promise.all(this.subscribers.map((subscriber) => subscriber({ key })))
}
/**
* Get a value from storage. This method is final and cannot be overridden.
* Subclasses should implement _get() to define their storage retrieval logic.
*/
public async get(key: string): Promise<string | undefined> {
try {
return await this._get(key)
} catch (error) {
Logger.error(`[${this.name}] failed to get '${key}':`, error)
return undefined
}
}
/**
* Store a value in storage. This method is final and cannot be overridden.
* Subclasses should implement _store() to define their storage logic.
* This method automatically fires change events after storing.
*/
public async store(key: string, value: string): Promise<void> {
try {
await this._store(key, value)
await this.fire(key)
} catch (error) {
Logger.error(`[${this.name}] failed to store '${key}':`, error)
}
}
/**
* Delete a value from storage. This method is final and cannot be overridden.
* Subclasses should implement _delete() to define their deletion logic.
* This method automatically fires change events after deletion.
*/
public async delete(key: string): Promise<void> {
try {
await this._delete(key)
await this.fire(key)
} catch (error) {
Logger.error(`[${this.name}] failed to delete '${key}':`, error)
}
}
/**
* Abstract method that subclasses must implement to retrieve values from their storage.
*/
protected abstract _get(key: string): Promise<string | undefined>
/**
* Abstract method that subclasses must implement to store values in their storage.
*/
protected abstract _store(key: string, value: string): Promise<void>
/**
* Abstract method that subclasses must implement to delete values from their storage.
*/
protected abstract _delete(key: string): Promise<void>
}
/**
* A simple in-memory implementation of ClineStorage using a Map.
*/
export class InMemoryClineStorage extends ClineStorage {
/**
* A simple in-memory cache to store key-value pairs.
*/
private readonly _cache = new Map<string, string>()
protected async _get(key: string): Promise<string | undefined> {
return this._cache.get(key)
}
protected async _store(key: string, value: string): Promise<void> {
this._cache.set(key, value)
}
protected async _delete(key: string): Promise<void> {
this._cache.delete(key)
}
}
+10 -3
View File
@@ -3,6 +3,7 @@ import chokidar, { FSWatcher } from "chokidar"
import type { ExtensionContext } from "vscode"
import { getTaskHistoryStateFilePath, readTaskHistoryFromState, writeTaskHistoryToState } from "./disk"
import { STATE_MANAGER_NOT_INITIALIZED } from "./error-messages"
import { secretStorage } from "./secrets"
import { GlobalState, GlobalStateKey, LocalState, LocalStateKey, SecretKey, Secrets } from "./state-keys"
import { readGlobalStateFromDisk, readSecretsFromDisk, readWorkspaceStateFromDisk } from "./utils/state-helpers"
@@ -46,8 +47,8 @@ export class StateManager {
try {
// Load all extension state from disk
const globalState = await readGlobalStateFromDisk(this.context)
const secrets = await readSecretsFromDisk(this.context)
const workspaceState = await readWorkspaceStateFromDisk(this.context)
const secrets = await readSecretsFromDisk(secretStorage)
// Populate the cache with all extension state and secrets fields
// Use populate method to avoid triggering persistence during initialization
@@ -126,6 +127,12 @@ export class StateManager {
// Update cache immediately for all keys
Object.entries(updates).forEach(([key, value]) => {
// Skip unchanged values as we don't want to trigger unnecessary
// writes & incorrectly fire an onDidChange events.
const current = this.secretsCache[key as keyof Secrets]
if (current === value) {
return
}
this.secretsCache[key as keyof Secrets] = value
this.pendingSecrets.add(key as SecretKey)
})
@@ -653,9 +660,9 @@ export class StateManager {
Array.from(keys).map((key) => {
const value = this.secretsCache[key]
if (value) {
return this.context.secrets.store(key, value)
return secretStorage.store(key, value)
} else {
return this.context.secrets.delete(key)
return secretStorage.delete(key)
}
}),
)
+219
View File
@@ -0,0 +1,219 @@
import { spawnSync } from "node:child_process"
import * as os from "node:os"
import { expect } from "chai"
import * as sinon from "sinon"
import { ErrorService } from "@/services/error"
import { Logger } from "@/services/logging/Logger"
import { CredentialStorage } from "./credential"
const platform = os.platform()
function hasCommand(cmd: string): boolean {
if (process.platform === "win32") {
return true
}
const result = spawnSync("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" })
return result.status === 0
}
// Check if required OS credential tools are available
const shouldSkip = (platform === "darwin" && !hasCommand("security")) || (platform === "linux" && !hasCommand("secret-tool"))
describe("CredentialStorage", () => {
if (shouldSkip) {
console.warn("Skipping CredentialStorage tests: required OS credential tool not available")
return
}
let sandbox: sinon.SinonSandbox
let store: CredentialStorage
let testKey: string
beforeEach(async () => {
sandbox = sinon.createSandbox()
// Mock Logger methods to avoid HostProvider dependency
sandbox.stub(Logger, "info").returns()
sandbox.stub(Logger, "error").returns()
// Mock ErrorService to avoid telemetry dependency
const mockErrorService = {
logMessage: sandbox.stub(),
logException: sandbox.stub(),
toClineError: sandbox.stub(),
isEnabled: sandbox.stub().returns(false),
getSettings: sandbox.stub().returns({ enabled: false, hostEnabled: false }),
getProvider: sandbox.stub(),
dispose: sandbox.stub().resolves(),
}
sandbox.stub(ErrorService, "initialize").resolves(mockErrorService as any)
sandbox.stub(ErrorService, "get").returns(mockErrorService as any)
await ErrorService.initialize()
store = new CredentialStorage()
// Generate unique key for each test to avoid conflicts
testKey = `e2e_secret_${Date.now()}_${Math.random().toString(36).substring(7)}`
})
afterEach(async () => {
// Clean up any test credentials
try {
await store.delete(testKey)
} catch {
// Ignore errors during cleanup
}
sandbox.restore()
})
describe("Basic operations", () => {
it("should store and retrieve a credential", async () => {
const value = "test-secret"
// Store the credential
await store.store(testKey, value)
// Retrieve and verify
const fetched = await store.get(testKey)
expect(fetched).to.equal(value)
})
it("should delete a credential", async () => {
const value = "test-secret-to-delete"
// Store the credential
await store.store(testKey, value)
// Verify it exists
const beforeDelete = await store.get(testKey)
expect(beforeDelete).to.equal(value)
// Delete the credential
await store.delete(testKey)
// Verify it's deleted
const afterDelete = await store.get(testKey)
expect(afterDelete).to.be.undefined
})
it("should return undefined for non-existent keys", async () => {
const nonExistentKey = `non_existent_${Date.now()}`
const result = await store.get(nonExistentKey)
expect(result).to.be.undefined
})
it("should handle updating an existing credential", async () => {
const initialValue = "initial-secret"
const updatedValue = "updated-secret"
// Store initial value
await store.store(testKey, initialValue)
// Verify initial value
const initial = await store.get(testKey)
expect(initial).to.equal(initialValue)
// Delete the existing credential first (required on some platforms)
await store.delete(testKey)
// Store new value
await store.store(testKey, updatedValue)
// Verify updated value
const updated = await store.get(testKey)
expect(updated).to.equal(updatedValue)
})
})
describe("Edge cases", () => {
it("should handle empty string values", async () => {
const emptyValue = ""
await store.store(testKey, emptyValue)
const fetched = await store.get(testKey)
// Note: Some credential stores might treat empty strings differently
// This test documents the actual behavior
expect(fetched).to.satisfy((val: string | undefined) => val === emptyValue || val === undefined)
})
it("should handle special characters in values", async () => {
const specialValue = "test!@#$%^&*()_+-=[]{}|;':\",./<>?"
await store.store(testKey, specialValue)
const fetched = await store.get(testKey)
expect(fetched).to.equal(specialValue)
})
it("should handle long values", async () => {
const longValue = "a".repeat(1000)
await store.store(testKey, longValue)
const fetched = await store.get(testKey)
expect(fetched).to.equal(longValue)
})
it("should handle special characters in keys", async () => {
const specialKey = `test_key_with-special.chars_${Date.now()}`
const value = "test-value"
try {
await store.store(specialKey, value)
const fetched = await store.get(specialKey)
expect(fetched).to.equal(value)
} finally {
// Clean up
try {
await store.delete(specialKey)
} catch {
// Ignore cleanup errors
}
}
})
})
describe("Error handling", () => {
it("should handle delete of non-existent key gracefully", async () => {
const nonExistentKey = `non_existent_delete_${Date.now()}`
// Should not throw an error
try {
await store.delete(nonExistentKey)
// If we reach here, the operation succeeded without throwing
expect(true).to.be.true
} catch (error) {
// If an error is thrown, fail the test
expect.fail(`Expected delete to not throw, but got: ${error}`)
}
})
it("should handle concurrent operations", async () => {
const value1 = "value1"
const value2 = "value2"
const key1 = `${testKey}_1`
const key2 = `${testKey}_2`
try {
// Perform multiple operations concurrently
await Promise.all([store.store(key1, value1), store.store(key2, value2), store.get(key1), store.get(key2)])
// Verify stored values
const fetched1 = await store.get(key1)
const fetched2 = await store.get(key2)
expect(fetched1).to.equal(value1)
expect(fetched2).to.equal(value2)
} finally {
// Clean up
await Promise.all([store.delete(key1).catch(() => {}), store.delete(key2).catch(() => {})])
}
})
})
describe("Platform-specific behavior", () => {
it(`should work correctly on ${platform}`, async () => {
const platformSpecificValue = `${platform}-specific-value`
await store.store(testKey, platformSpecificValue)
const fetched = await store.get(testKey)
expect(fetched).to.equal(platformSpecificValue)
})
})
})
+183
View File
@@ -0,0 +1,183 @@
import { spawn } from "node:child_process"
import { getPlatformOS, PLATFORM_OS } from "@/utils/platform"
import { ClineStorage } from "./ClineStorage"
type CommandSpec = { command: string; args: string[]; stdin?: string }
interface CommandArgs {
service: string
account: string
target?: string
}
interface CommandStoreArgs extends CommandArgs {
value: string
}
interface PlatformCommand {
get: (options: CommandArgs) => CommandSpec
store: (options: CommandStoreArgs) => CommandSpec
delete: (options: CommandArgs) => CommandSpec
}
interface PlatformCommands {
[PLATFORM_OS.Win32]: PlatformCommand
[PLATFORM_OS.Linux]: PlatformCommand
[PLATFORM_OS.MacOS]: PlatformCommand
}
export class CredentialStorage extends ClineStorage {
override name = "CredentialStorage"
private readonly commands: PlatformCommand
constructor() {
super()
const platform = getPlatformOS()
const commands = PLATFORM_COMMANDS[platform]
if (!commands) {
throw new Error(`Unsupported platform: ${platform}`)
}
if (platform === PLATFORM_OS.Win32) {
this.initWindowsCredentialManager()
}
this.commands = commands
}
private getCredentialIdentifiers(key: string) {
const service = `Cline: ${key}`
const account = `cline_${key}`
const target = `${service}:${account}`.replaceAll('"', "_")
return { service, account, target }
}
protected async _get(key: string): Promise<string | undefined> {
const { service, account, target } = this.getCredentialIdentifiers(key)
try {
const result = await this.exec(this.commands.get({ service, account, target }))
return result || undefined
} catch (error) {
throw error
}
}
protected async _store(key: string, value: string): Promise<void> {
const { service, account, target } = this.getCredentialIdentifiers(key)
try {
await this.exec(this.commands.store({ service, account, target, value }))
} catch (error) {
throw error
}
}
protected async _delete(key: string): Promise<void> {
const { service, account, target } = this.getCredentialIdentifiers(key)
try {
await this.exec(this.commands.delete({ service, account, target }))
} catch {
// Ignore deletion errors (key might not exist)
}
}
private exec({ command, args, stdin }: CommandSpec): Promise<string> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, { stdio: "pipe" })
let stdout = ""
let stderr = ""
child.stdout.on("data", (data) => {
stdout += data
})
child.stderr.on("data", (data) => {
stderr += data
})
if (stdin !== undefined) {
child.stdin.write(stdin)
child.stdin.end()
}
child.once("close", (code) => {
code === 0 ? resolve(stdout.trim()) : reject(new Error(`${command} failed: ${stderr || stdout}`))
})
child.once("error", reject)
})
}
private initWindowsCredentialManager(): void {
this.exec({
command: "powershell.exe",
args: [
"-Command",
"if (-not (Get-Module -ListAvailable -Name CredentialManager)) { Install-Module -Name CredentialManager -Force -Scope CurrentUser }",
],
})
}
}
const PLATFORM_COMMANDS: PlatformCommands = {
[PLATFORM_OS.MacOS]: {
get: ({ service, account }) => ({
command: "security",
args: ["find-generic-password", "-s", service, "-a", account, "-w"],
}),
store: ({ service, account, value }) => ({
command: "security",
args: ["add-generic-password", "-s", service, "-a", account, "-w", value],
}),
delete: ({ service, account }) => ({
command: "security",
args: ["delete-generic-password", "-s", service, "-a", account],
}),
},
[PLATFORM_OS.Linux]: {
get: ({ service, account }) => ({
command: "secret-tool",
args: ["lookup", "service", service, "account", account],
}),
store: ({ service, account, value }) => ({
command: "secret-tool",
args: ["store", "--label", service, "service", service, "account", account],
stdin: value,
}),
delete: ({ service, account }) => ({
command: "secret-tool",
args: ["clear", "service", service, "account", account],
}),
},
[PLATFORM_OS.Win32]: {
get: ({ target }) => ({
command: "powershell.exe",
args: [
"-Command",
"param($Target); $cred = Get-StoredCredential -Target $Target; if ($cred) { $cred.GetNetworkCredential().Password } else { '' }",
"-Target",
target || "",
],
}),
store: ({ target, value }) => ({
command: "powershell.exe",
args: [
"-Command",
"param($Target, $Value); $pass = ConvertTo-SecureString $Value -AsPlainText -Force; New-StoredCredential -Target $Target -UserName 'Cline' -SecurePassword $pass -Persist LocalMachine",
"-Target",
target || "",
"-Value",
value,
],
}),
delete: ({ target }) => ({
command: "powershell.exe",
args: [
"-Command",
"param($Target); $cred = Get-StoredCredential -Target $Target; if ($cred) { Remove-StoredCredential -Target $Target }",
"-Target",
target || "",
],
}),
},
}
+75
View File
@@ -0,0 +1,75 @@
import * as fs from "node:fs"
import * as path from "node:path"
import { ClineStorage } from "./ClineStorage"
/**
* A storage implementation that uses the filesystem to store key-value pairs.
*/
export class FileBasedStorage extends ClineStorage {
override name = "FileBasedStorage"
private readonly cache = new Map<string, string>()
constructor(private fsPath: string) {
super()
this.read()
}
override async _get(key: string): Promise<string | undefined> {
try {
await this.read()
return this.cache.get(key) || undefined
} catch (error) {
throw error
}
}
override async _store(key: string, value: string): Promise<void> {
try {
await this.read()
this.cache.set(key, value)
await this.write()
} catch (error) {
throw error
}
}
override async _delete(key: string): Promise<void> {
try {
await this.read()
this.cache.delete(key)
await this.write()
} catch (error) {
console.error("FileBasedStorage", error)
}
}
private async read(): Promise<void> {
try {
const fileContent = await fs.promises.readFile(this.fsPath, "utf-8")
const json = JSON.parse(fileContent) as Record<string, string>
this.cache.clear() // Clear existing cache
for (const [key, value] of Object.entries(json)) {
if (key && value) {
this.cache.set(key, value)
}
}
} catch (error) {
throw error
}
}
private async write(): Promise<void> {
try {
// Ensure directory exists
const dir = path.dirname(this.fsPath)
await fs.promises.mkdir(dir, { recursive: true })
// Convert map to object and save
const json = Object.fromEntries(this.cache)
await fs.promises.writeFile(this.fsPath, JSON.stringify(json, null, 2), "utf-8")
} catch (error) {
console.error("FileBasedStorage", error)
}
}
}
+65
View File
@@ -0,0 +1,65 @@
import type { SecretStorage as VSCodeSecretStorage } from "vscode"
import { Logger } from "@/services/logging/Logger"
import { ClineStorage } from "./ClineStorage"
type SecretStores = VSCodeSecretStorage | ClineStorage
/**
* Wrapper around VSCode Secret Storage or any other storage type for managing secrets.
*/
export class ClineSecretStorage extends ClineStorage {
override readonly name = "ClineSecretStorage"
private static readonly store = new ClineSecretStorage()
static get instance(): ClineSecretStorage {
return ClineSecretStorage.store
}
private secretStorage: SecretStores | null = null
public get storage(): SecretStores {
if (!this.secretStorage) {
throw new Error("[ClineSecretStorage] init not called")
}
return this.secretStorage
}
public init(store: SecretStores) {
if (!this.secretStorage) {
this.secretStorage = store
Logger.info("[ClineSecretStorage] initialized")
}
return this.secretStorage
}
protected async _get(key: string): Promise<string | undefined> {
try {
return key ? await this.storage.get(key) : undefined
} catch (error) {
Logger.error("[ClineSecretStorage]", error)
return undefined
}
}
/**
* [SECURITY] Avoid logging secrets values.
*/
protected async _store(key: string, value: string): Promise<void> {
try {
if (value && value.length > 0) {
await this.storage.store(key, value)
}
} catch (error) {
Logger.error("[ClineSecretStorage]", error)
}
}
protected async _delete(key: string): Promise<void> {
Logger.info("[ClineSecretStorage] deleting " + key)
await this.storage.delete(key)
}
}
/**
* Singleton instance of ClineSecretStorage
*/
export const secretStorage = ClineSecretStorage.instance
+37 -36
View File
@@ -9,10 +9,11 @@ import { DEFAULT_MCP_DISPLAY_MODE, McpDisplayMode } from "@/shared/McpDisplayMod
import { Mode, OpenaiReasoningEffort } from "@/shared/storage/types"
import { TelemetrySetting } from "@/shared/TelemetrySetting"
import { UserInfo } from "@/shared/UserInfo"
import { ClineStorage } from "../ClineStorage"
import { readTaskHistoryFromState } from "../disk"
import { GlobalState, LocalState, SecretKey, Secrets } from "../state-keys"
export async function readSecretsFromDisk(context: ExtensionContext): Promise<Secrets> {
export async function readSecretsFromDisk(store: ClineStorage): Promise<Secrets> {
const [
apiKey,
openRouterApiKey,
@@ -50,41 +51,41 @@ export async function readSecretsFromDisk(context: ExtensionContext): Promise<Se
difyApiKey,
authNonce,
] = await Promise.all([
context.secrets.get("apiKey") as Promise<string | undefined>,
context.secrets.get("openRouterApiKey") as Promise<string | undefined>,
context.secrets.get("clineAccountId") as Promise<string | undefined>,
context.secrets.get("awsAccessKey") as Promise<string | undefined>,
context.secrets.get("awsSecretKey") as Promise<string | undefined>,
context.secrets.get("awsSessionToken") as Promise<string | undefined>,
context.secrets.get("awsBedrockApiKey") as Promise<string | undefined>,
context.secrets.get("openAiApiKey") as Promise<string | undefined>,
context.secrets.get("geminiApiKey") as Promise<string | undefined>,
context.secrets.get("openAiNativeApiKey") as Promise<string | undefined>,
context.secrets.get("deepSeekApiKey") as Promise<string | undefined>,
context.secrets.get("requestyApiKey") as Promise<string | undefined>,
context.secrets.get("togetherApiKey") as Promise<string | undefined>,
context.secrets.get("qwenApiKey") as Promise<string | undefined>,
context.secrets.get("doubaoApiKey") as Promise<string | undefined>,
context.secrets.get("mistralApiKey") as Promise<string | undefined>,
context.secrets.get("fireworksApiKey") as Promise<string | undefined>,
context.secrets.get("liteLlmApiKey") as Promise<string | undefined>,
context.secrets.get("asksageApiKey") as Promise<string | undefined>,
context.secrets.get("xaiApiKey") as Promise<string | undefined>,
context.secrets.get("sambanovaApiKey") as Promise<string | undefined>,
context.secrets.get("cerebrasApiKey") as Promise<string | undefined>,
context.secrets.get("groqApiKey") as Promise<string | undefined>,
context.secrets.get("moonshotApiKey") as Promise<string | undefined>,
context.secrets.get("nebiusApiKey") as Promise<string | undefined>,
context.secrets.get("huggingFaceApiKey") as Promise<string | undefined>,
context.secrets.get("sapAiCoreClientId") as Promise<string | undefined>,
context.secrets.get("sapAiCoreClientSecret") as Promise<string | undefined>,
context.secrets.get("huaweiCloudMaasApiKey") as Promise<string | undefined>,
context.secrets.get("basetenApiKey") as Promise<string | undefined>,
context.secrets.get("zaiApiKey") as Promise<string | undefined>,
context.secrets.get("ollamaApiKey") as Promise<string | undefined>,
context.secrets.get("vercelAiGatewayApiKey") as Promise<string | undefined>,
context.secrets.get("difyApiKey") as Promise<string | undefined>,
context.secrets.get("authNonce") as Promise<string | undefined>,
store.get("apiKey") as Promise<string | undefined>,
store.get("openRouterApiKey") as Promise<string | undefined>,
store.get("clineAccountId") as Promise<string | undefined>,
store.get("awsAccessKey") as Promise<string | undefined>,
store.get("awsSecretKey") as Promise<string | undefined>,
store.get("awsSessionToken") as Promise<string | undefined>,
store.get("awsBedrockApiKey") as Promise<string | undefined>,
store.get("openAiApiKey") as Promise<string | undefined>,
store.get("geminiApiKey") as Promise<string | undefined>,
store.get("openAiNativeApiKey") as Promise<string | undefined>,
store.get("deepSeekApiKey") as Promise<string | undefined>,
store.get("requestyApiKey") as Promise<string | undefined>,
store.get("togetherApiKey") as Promise<string | undefined>,
store.get("qwenApiKey") as Promise<string | undefined>,
store.get("doubaoApiKey") as Promise<string | undefined>,
store.get("mistralApiKey") as Promise<string | undefined>,
store.get("fireworksApiKey") as Promise<string | undefined>,
store.get("liteLlmApiKey") as Promise<string | undefined>,
store.get("asksageApiKey") as Promise<string | undefined>,
store.get("xaiApiKey") as Promise<string | undefined>,
store.get("sambanovaApiKey") as Promise<string | undefined>,
store.get("cerebrasApiKey") as Promise<string | undefined>,
store.get("groqApiKey") as Promise<string | undefined>,
store.get("moonshotApiKey") as Promise<string | undefined>,
store.get("nebiusApiKey") as Promise<string | undefined>,
store.get("huggingFaceApiKey") as Promise<string | undefined>,
store.get("sapAiCoreClientId") as Promise<string | undefined>,
store.get("sapAiCoreClientSecret") as Promise<string | undefined>,
store.get("huaweiCloudMaasApiKey") as Promise<string | undefined>,
store.get("basetenApiKey") as Promise<string | undefined>,
store.get("zaiApiKey") as Promise<string | undefined>,
store.get("ollamaApiKey") as Promise<string | undefined>,
store.get("vercelAiGatewayApiKey") as Promise<string | undefined>,
store.get("difyApiKey") as Promise<string | undefined>,
store.get("authNonce") as Promise<string | undefined>,
])
return {
+12
View File
@@ -0,0 +1,12 @@
import { type SecretStorage } from "vscode"
import { ClineStorage } from "../ClineStorage"
import { CredentialStorage } from "../credential"
import { ClineSecretStorage } from "../secrets"
export type ClineStorages = ClineStorage | ClineSecretStorage | CredentialStorage | SecretStorage
export interface ClineStorageChangeEvent {
readonly key: string
}
export type StorageEventListener = (event: ClineStorageChangeEvent) => Promise<void>
+3
View File
@@ -30,6 +30,7 @@ import { fixWithCline } from "./core/controller/commands/fixWithCline"
import { improveWithCline } from "./core/controller/commands/improveWithCline"
import { sendAddToInputEvent } from "./core/controller/ui/subscribeToAddToInput"
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
import { secretStorage } from "./core/storage/secrets"
import { workspaceResolver } from "./core/workspace"
import { focusChatInput, getContextForCommand } from "./hosts/vscode/commandUtils"
import { VscodeDiffViewProvider } from "./hosts/vscode/VscodeDiffViewProvider"
@@ -55,6 +56,8 @@ https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/framewo
export async function activate(context: vscode.ExtensionContext) {
setupHostProvider(context)
secretStorage.init(context.secrets)
const sidebarWebview = (await initialize(context)) as VscodeWebviewProvider
Logger.log("Cline extension activated")
+4 -1
View File
@@ -4,6 +4,7 @@ import path, { join } from "path"
import type { Extension, ExtensionContext } from "vscode"
import { ExtensionKind, ExtensionMode } from "vscode"
import { URI } from "vscode-uri"
import { CredentialStorage } from "@/core/storage/credential"
import { log } from "./utils"
import { EnvironmentVariableCollection, MementoStore, readJson, SecretStore } from "./vscode-context-utils"
@@ -37,7 +38,9 @@ const extensionContext: ExtensionContext = {
// Set up KV stores.
globalState: new MementoStore(path.join(DATA_DIR, "globalState.json")),
secrets: new SecretStore(path.join(DATA_DIR, "secrets.json")),
// Example using CredentialStorage with fallback to SecretStore
// TODO: Use storage based on host configurations. E.g. 'credential', 'stateless', 'file', 'client', etc.
secrets: new CredentialStorage() || new SecretStore(path.join(DATA_DIR, "secrets.json")),
// Set up URIs.
storageUri: URI.file(DATA_DIR),
+21
View File
@@ -0,0 +1,21 @@
import os from "node:os"
// Determine platform and architecture at runtime once.
const _platform = os.platform()
export enum PLATFORM_OS {
MacOS = "darwin",
Linux = "linux",
Win32 = "win32",
}
export function getPlatformOS(): PLATFORM_OS {
switch (_platform) {
case "darwin":
return PLATFORM_OS.MacOS
case "win32":
return PLATFORM_OS.Win32
default:
return PLATFORM_OS.Linux
}
}