mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Telemetry refactor (#4021)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
parent
1e5bf74136
commit
1384077495
29 changed files with 2429 additions and 1603 deletions
37
packages/types/src/codebase-index.ts
Normal file
37
packages/types/src/codebase-index.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* CodebaseIndexConfig
|
||||
*/
|
||||
|
||||
export const codebaseIndexConfigSchema = z.object({
|
||||
codebaseIndexEnabled: z.boolean().optional(),
|
||||
codebaseIndexQdrantUrl: z.string().optional(),
|
||||
codebaseIndexEmbedderProvider: z.enum(["openai", "ollama"]).optional(),
|
||||
codebaseIndexEmbedderBaseUrl: z.string().optional(),
|
||||
codebaseIndexEmbedderModelId: z.string().optional(),
|
||||
})
|
||||
|
||||
export type CodebaseIndexConfig = z.infer<typeof codebaseIndexConfigSchema>
|
||||
|
||||
/**
|
||||
* CodebaseIndexModels
|
||||
*/
|
||||
|
||||
export const codebaseIndexModelsSchema = z.object({
|
||||
openai: z.record(z.string(), z.object({ dimension: z.number() })).optional(),
|
||||
ollama: z.record(z.string(), z.object({ dimension: z.number() })).optional(),
|
||||
})
|
||||
|
||||
export type CodebaseIndexModels = z.infer<typeof codebaseIndexModelsSchema>
|
||||
|
||||
/**
|
||||
* CdebaseIndexProvider
|
||||
*/
|
||||
|
||||
export const codebaseIndexProviderSchema = z.object({
|
||||
codeIndexOpenAiKey: z.string().optional(),
|
||||
codeIndexQdrantApiKey: z.string().optional(),
|
||||
})
|
||||
|
||||
export type CodebaseIndexProvider = z.infer<typeof codebaseIndexProviderSchema>
|
||||
26
packages/types/src/experiment.ts
Normal file
26
packages/types/src/experiment.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { z } from "zod"
|
||||
|
||||
import type { Keys, Equals, AssertEqual } from "./type-fu.js"
|
||||
|
||||
/**
|
||||
* ExperimentId
|
||||
*/
|
||||
|
||||
export const experimentIds = ["autoCondenseContext", "powerSteering"] as const
|
||||
|
||||
export const experimentIdsSchema = z.enum(experimentIds)
|
||||
|
||||
export type ExperimentId = z.infer<typeof experimentIdsSchema>
|
||||
|
||||
/**
|
||||
* Experiments
|
||||
*/
|
||||
|
||||
export const experimentsSchema = z.object({
|
||||
autoCondenseContext: z.boolean(),
|
||||
powerSteering: z.boolean(),
|
||||
})
|
||||
|
||||
export type Experiments = z.infer<typeof experimentsSchema>
|
||||
|
||||
type _AssertExperiments = AssertEqual<Equals<ExperimentId, Keys<Experiments>>>
|
||||
269
packages/types/src/global-settings.ts
Normal file
269
packages/types/src/global-settings.ts
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
import { z } from "zod"
|
||||
|
||||
import type { Keys } from "./type-fu.js"
|
||||
import {
|
||||
type ProviderSettings,
|
||||
PROVIDER_SETTINGS_KEYS,
|
||||
providerSettingsEntrySchema,
|
||||
providerSettingsSchema,
|
||||
} from "./provider-settings.js"
|
||||
import { historyItemSchema } from "./history.js"
|
||||
import { codebaseIndexModelsSchema, codebaseIndexConfigSchema } from "./codebase-index.js"
|
||||
import { experimentsSchema } from "./experiment.js"
|
||||
import { telemetrySettingsSchema } from "./telemetry.js"
|
||||
import { modeConfigSchema } from "./mode.js"
|
||||
import { customModePromptsSchema, customSupportPromptsSchema } from "./mode.js"
|
||||
import { languagesSchema } from "./vscode.js"
|
||||
|
||||
/**
|
||||
* GlobalSettings
|
||||
*/
|
||||
|
||||
export const globalSettingsSchema = z.object({
|
||||
currentApiConfigName: z.string().optional(),
|
||||
listApiConfigMeta: z.array(providerSettingsEntrySchema).optional(),
|
||||
pinnedApiConfigs: z.record(z.string(), z.boolean()).optional(),
|
||||
|
||||
lastShownAnnouncementId: z.string().optional(),
|
||||
customInstructions: z.string().optional(),
|
||||
taskHistory: z.array(historyItemSchema).optional(),
|
||||
|
||||
condensingApiConfigId: z.string().optional(),
|
||||
customCondensingPrompt: z.string().optional(),
|
||||
|
||||
autoApprovalEnabled: z.boolean().optional(),
|
||||
alwaysAllowReadOnly: z.boolean().optional(),
|
||||
alwaysAllowReadOnlyOutsideWorkspace: z.boolean().optional(),
|
||||
codebaseIndexModels: codebaseIndexModelsSchema.optional(),
|
||||
codebaseIndexConfig: codebaseIndexConfigSchema.optional(),
|
||||
alwaysAllowWrite: z.boolean().optional(),
|
||||
alwaysAllowWriteOutsideWorkspace: z.boolean().optional(),
|
||||
writeDelayMs: z.number().optional(),
|
||||
alwaysAllowBrowser: z.boolean().optional(),
|
||||
alwaysApproveResubmit: z.boolean().optional(),
|
||||
requestDelaySeconds: z.number().optional(),
|
||||
alwaysAllowMcp: z.boolean().optional(),
|
||||
alwaysAllowModeSwitch: z.boolean().optional(),
|
||||
alwaysAllowSubtasks: z.boolean().optional(),
|
||||
alwaysAllowExecute: z.boolean().optional(),
|
||||
allowedCommands: z.array(z.string()).optional(),
|
||||
allowedMaxRequests: z.number().nullish(),
|
||||
autoCondenseContextPercent: z.number().optional(),
|
||||
|
||||
browserToolEnabled: z.boolean().optional(),
|
||||
browserViewportSize: z.string().optional(),
|
||||
screenshotQuality: z.number().optional(),
|
||||
remoteBrowserEnabled: z.boolean().optional(),
|
||||
remoteBrowserHost: z.string().optional(),
|
||||
cachedChromeHostUrl: z.string().optional(),
|
||||
|
||||
enableCheckpoints: z.boolean().optional(),
|
||||
|
||||
ttsEnabled: z.boolean().optional(),
|
||||
ttsSpeed: z.number().optional(),
|
||||
soundEnabled: z.boolean().optional(),
|
||||
soundVolume: z.number().optional(),
|
||||
|
||||
maxOpenTabsContext: z.number().optional(),
|
||||
maxWorkspaceFiles: z.number().optional(),
|
||||
showRooIgnoredFiles: z.boolean().optional(),
|
||||
maxReadFileLine: z.number().optional(),
|
||||
|
||||
terminalOutputLineLimit: z.number().optional(),
|
||||
terminalShellIntegrationTimeout: z.number().optional(),
|
||||
terminalShellIntegrationDisabled: z.boolean().optional(),
|
||||
terminalCommandDelay: z.number().optional(),
|
||||
terminalPowershellCounter: z.boolean().optional(),
|
||||
terminalZshClearEolMark: z.boolean().optional(),
|
||||
terminalZshOhMy: z.boolean().optional(),
|
||||
terminalZshP10k: z.boolean().optional(),
|
||||
terminalZdotdir: z.boolean().optional(),
|
||||
terminalCompressProgressBar: z.boolean().optional(),
|
||||
|
||||
rateLimitSeconds: z.number().optional(),
|
||||
diffEnabled: z.boolean().optional(),
|
||||
fuzzyMatchThreshold: z.number().optional(),
|
||||
experiments: experimentsSchema.optional(),
|
||||
|
||||
language: languagesSchema.optional(),
|
||||
|
||||
telemetrySetting: telemetrySettingsSchema.optional(),
|
||||
|
||||
mcpEnabled: z.boolean().optional(),
|
||||
enableMcpServerCreation: z.boolean().optional(),
|
||||
|
||||
mode: z.string().optional(),
|
||||
modeApiConfigs: z.record(z.string(), z.string()).optional(),
|
||||
customModes: z.array(modeConfigSchema).optional(),
|
||||
customModePrompts: customModePromptsSchema.optional(),
|
||||
customSupportPrompts: customSupportPromptsSchema.optional(),
|
||||
enhancementApiConfigId: z.string().optional(),
|
||||
historyPreviewCollapsed: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type GlobalSettings = z.infer<typeof globalSettingsSchema>
|
||||
|
||||
type GlobalSettingsRecord = Record<Keys<GlobalSettings>, undefined>
|
||||
|
||||
const globalSettingsRecord: GlobalSettingsRecord = {
|
||||
codebaseIndexModels: undefined,
|
||||
codebaseIndexConfig: undefined,
|
||||
currentApiConfigName: undefined,
|
||||
listApiConfigMeta: undefined,
|
||||
pinnedApiConfigs: undefined,
|
||||
|
||||
lastShownAnnouncementId: undefined,
|
||||
customInstructions: undefined,
|
||||
taskHistory: undefined,
|
||||
|
||||
condensingApiConfigId: undefined,
|
||||
customCondensingPrompt: undefined,
|
||||
|
||||
autoApprovalEnabled: undefined,
|
||||
alwaysAllowReadOnly: undefined,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: undefined,
|
||||
alwaysAllowWrite: undefined,
|
||||
alwaysAllowWriteOutsideWorkspace: undefined,
|
||||
writeDelayMs: undefined,
|
||||
alwaysAllowBrowser: undefined,
|
||||
alwaysApproveResubmit: undefined,
|
||||
requestDelaySeconds: undefined,
|
||||
alwaysAllowMcp: undefined,
|
||||
alwaysAllowModeSwitch: undefined,
|
||||
alwaysAllowSubtasks: undefined,
|
||||
alwaysAllowExecute: undefined,
|
||||
allowedCommands: undefined,
|
||||
allowedMaxRequests: undefined,
|
||||
autoCondenseContextPercent: undefined,
|
||||
|
||||
browserToolEnabled: undefined,
|
||||
browserViewportSize: undefined,
|
||||
screenshotQuality: undefined,
|
||||
remoteBrowserEnabled: undefined,
|
||||
remoteBrowserHost: undefined,
|
||||
|
||||
enableCheckpoints: undefined,
|
||||
|
||||
ttsEnabled: undefined,
|
||||
ttsSpeed: undefined,
|
||||
soundEnabled: undefined,
|
||||
soundVolume: undefined,
|
||||
|
||||
maxOpenTabsContext: undefined,
|
||||
maxWorkspaceFiles: undefined,
|
||||
showRooIgnoredFiles: undefined,
|
||||
maxReadFileLine: undefined,
|
||||
|
||||
terminalOutputLineLimit: undefined,
|
||||
terminalShellIntegrationTimeout: undefined,
|
||||
terminalShellIntegrationDisabled: undefined,
|
||||
terminalCommandDelay: undefined,
|
||||
terminalPowershellCounter: undefined,
|
||||
terminalZshClearEolMark: undefined,
|
||||
terminalZshOhMy: undefined,
|
||||
terminalZshP10k: undefined,
|
||||
terminalZdotdir: undefined,
|
||||
terminalCompressProgressBar: undefined,
|
||||
|
||||
rateLimitSeconds: undefined,
|
||||
diffEnabled: undefined,
|
||||
fuzzyMatchThreshold: undefined,
|
||||
experiments: undefined,
|
||||
|
||||
language: undefined,
|
||||
|
||||
telemetrySetting: undefined,
|
||||
|
||||
mcpEnabled: undefined,
|
||||
enableMcpServerCreation: undefined,
|
||||
|
||||
mode: undefined,
|
||||
modeApiConfigs: undefined,
|
||||
customModes: undefined,
|
||||
customModePrompts: undefined,
|
||||
customSupportPrompts: undefined,
|
||||
enhancementApiConfigId: undefined,
|
||||
cachedChromeHostUrl: undefined,
|
||||
historyPreviewCollapsed: undefined,
|
||||
}
|
||||
|
||||
export const GLOBAL_SETTINGS_KEYS = Object.keys(globalSettingsRecord) as Keys<GlobalSettings>[]
|
||||
|
||||
/**
|
||||
* RooCodeSettings
|
||||
*/
|
||||
|
||||
export const rooCodeSettingsSchema = providerSettingsSchema.merge(globalSettingsSchema)
|
||||
|
||||
export type RooCodeSettings = GlobalSettings & ProviderSettings
|
||||
|
||||
/**
|
||||
* SecretState
|
||||
*/
|
||||
|
||||
export type SecretState = Pick<
|
||||
ProviderSettings,
|
||||
| "apiKey"
|
||||
| "glamaApiKey"
|
||||
| "openRouterApiKey"
|
||||
| "awsAccessKey"
|
||||
| "awsSecretKey"
|
||||
| "awsSessionToken"
|
||||
| "openAiApiKey"
|
||||
| "geminiApiKey"
|
||||
| "openAiNativeApiKey"
|
||||
| "deepSeekApiKey"
|
||||
| "mistralApiKey"
|
||||
| "unboundApiKey"
|
||||
| "requestyApiKey"
|
||||
| "xaiApiKey"
|
||||
| "groqApiKey"
|
||||
| "chutesApiKey"
|
||||
| "litellmApiKey"
|
||||
| "codeIndexOpenAiKey"
|
||||
| "codeIndexQdrantApiKey"
|
||||
>
|
||||
|
||||
export type CodeIndexSecrets = "codeIndexOpenAiKey" | "codeIndexQdrantApiKey"
|
||||
|
||||
type SecretStateRecord = Record<Keys<SecretState>, undefined>
|
||||
|
||||
const secretStateRecord: SecretStateRecord = {
|
||||
apiKey: undefined,
|
||||
glamaApiKey: undefined,
|
||||
openRouterApiKey: undefined,
|
||||
awsAccessKey: undefined,
|
||||
awsSecretKey: undefined,
|
||||
awsSessionToken: undefined,
|
||||
openAiApiKey: undefined,
|
||||
geminiApiKey: undefined,
|
||||
openAiNativeApiKey: undefined,
|
||||
deepSeekApiKey: undefined,
|
||||
mistralApiKey: undefined,
|
||||
unboundApiKey: undefined,
|
||||
requestyApiKey: undefined,
|
||||
xaiApiKey: undefined,
|
||||
groqApiKey: undefined,
|
||||
chutesApiKey: undefined,
|
||||
litellmApiKey: undefined,
|
||||
codeIndexOpenAiKey: undefined,
|
||||
codeIndexQdrantApiKey: undefined,
|
||||
}
|
||||
|
||||
export const SECRET_STATE_KEYS = Object.keys(secretStateRecord) as Keys<SecretState>[]
|
||||
|
||||
export const isSecretStateKey = (key: string): key is Keys<SecretState> =>
|
||||
SECRET_STATE_KEYS.includes(key as Keys<SecretState>)
|
||||
|
||||
/**
|
||||
* GlobalState
|
||||
*/
|
||||
|
||||
export type GlobalState = Omit<RooCodeSettings, Keys<SecretState>>
|
||||
|
||||
export const GLOBAL_STATE_KEYS = [...GLOBAL_SETTINGS_KEYS, ...PROVIDER_SETTINGS_KEYS].filter(
|
||||
(key: Keys<RooCodeSettings>) => !SECRET_STATE_KEYS.includes(key as Keys<SecretState>),
|
||||
) as Keys<GlobalState>[]
|
||||
|
||||
export const isGlobalStateKey = (key: string): key is Keys<GlobalState> =>
|
||||
GLOBAL_STATE_KEYS.includes(key as Keys<GlobalState>)
|
||||
21
packages/types/src/history.ts
Normal file
21
packages/types/src/history.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* HistoryItem
|
||||
*/
|
||||
|
||||
export const historyItemSchema = z.object({
|
||||
id: z.string(),
|
||||
number: z.number(),
|
||||
ts: z.number(),
|
||||
task: z.string(),
|
||||
tokensIn: z.number(),
|
||||
tokensOut: z.number(),
|
||||
cacheWrites: z.number().optional(),
|
||||
cacheReads: z.number().optional(),
|
||||
totalCost: z.number(),
|
||||
size: z.number().optional(),
|
||||
workspace: z.string().optional(),
|
||||
})
|
||||
|
||||
export type HistoryItem = z.infer<typeof historyItemSchema>
|
||||
|
|
@ -1,2 +1,15 @@
|
|||
export * from "./types.js"
|
||||
export * from "./api.js"
|
||||
export * from "./codebase-index.js"
|
||||
export * from "./experiment.js"
|
||||
export * from "./global-settings.js"
|
||||
export * from "./history.js"
|
||||
export * from "./ipc.js"
|
||||
export * from "./message.js"
|
||||
export * from "./mode.js"
|
||||
export * from "./model.js"
|
||||
export * from "./provider-settings.js"
|
||||
export * from "./telemetry.js"
|
||||
export * from "./terminal.js"
|
||||
export * from "./tool.js"
|
||||
export * from "./type-fu.js"
|
||||
export * from "./vscode.js"
|
||||
|
|
|
|||
183
packages/types/src/ipc.ts
Normal file
183
packages/types/src/ipc.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
import { z } from "zod"
|
||||
|
||||
import { clineMessageSchema, tokenUsageSchema } from "./message.js"
|
||||
import { toolNamesSchema, toolUsageSchema } from "./tool.js"
|
||||
import { rooCodeSettingsSchema } from "./global-settings.js"
|
||||
|
||||
/**
|
||||
* RooCodeEvent
|
||||
*/
|
||||
|
||||
export enum RooCodeEventName {
|
||||
Message = "message",
|
||||
TaskCreated = "taskCreated",
|
||||
TaskStarted = "taskStarted",
|
||||
TaskModeSwitched = "taskModeSwitched",
|
||||
TaskPaused = "taskPaused",
|
||||
TaskUnpaused = "taskUnpaused",
|
||||
TaskAskResponded = "taskAskResponded",
|
||||
TaskAborted = "taskAborted",
|
||||
TaskSpawned = "taskSpawned",
|
||||
TaskCompleted = "taskCompleted",
|
||||
TaskTokenUsageUpdated = "taskTokenUsageUpdated",
|
||||
TaskToolFailed = "taskToolFailed",
|
||||
}
|
||||
|
||||
export const rooCodeEventsSchema = z.object({
|
||||
[RooCodeEventName.Message]: z.tuple([
|
||||
z.object({
|
||||
taskId: z.string(),
|
||||
action: z.union([z.literal("created"), z.literal("updated")]),
|
||||
message: clineMessageSchema,
|
||||
}),
|
||||
]),
|
||||
[RooCodeEventName.TaskCreated]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskStarted]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskModeSwitched]: z.tuple([z.string(), z.string()]),
|
||||
[RooCodeEventName.TaskPaused]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskUnpaused]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskAborted]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskSpawned]: z.tuple([z.string(), z.string()]),
|
||||
[RooCodeEventName.TaskCompleted]: z.tuple([z.string(), tokenUsageSchema, toolUsageSchema]),
|
||||
[RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema]),
|
||||
[RooCodeEventName.TaskToolFailed]: z.tuple([z.string(), toolNamesSchema, z.string()]),
|
||||
})
|
||||
|
||||
export type RooCodeEvents = z.infer<typeof rooCodeEventsSchema>
|
||||
|
||||
/**
|
||||
* Ack
|
||||
*/
|
||||
|
||||
export const ackSchema = z.object({
|
||||
clientId: z.string(),
|
||||
pid: z.number(),
|
||||
ppid: z.number(),
|
||||
})
|
||||
|
||||
export type Ack = z.infer<typeof ackSchema>
|
||||
|
||||
/**
|
||||
* TaskCommand
|
||||
*/
|
||||
|
||||
export enum TaskCommandName {
|
||||
StartNewTask = "StartNewTask",
|
||||
CancelTask = "CancelTask",
|
||||
CloseTask = "CloseTask",
|
||||
}
|
||||
|
||||
export const taskCommandSchema = z.discriminatedUnion("commandName", [
|
||||
z.object({
|
||||
commandName: z.literal(TaskCommandName.StartNewTask),
|
||||
data: z.object({
|
||||
configuration: rooCodeSettingsSchema,
|
||||
text: z.string(),
|
||||
images: z.array(z.string()).optional(),
|
||||
newTab: z.boolean().optional(),
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
commandName: z.literal(TaskCommandName.CancelTask),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
commandName: z.literal(TaskCommandName.CloseTask),
|
||||
data: z.string(),
|
||||
}),
|
||||
])
|
||||
|
||||
export type TaskCommand = z.infer<typeof taskCommandSchema>
|
||||
|
||||
/**
|
||||
* TaskEvent
|
||||
*/
|
||||
|
||||
export const taskEventSchema = z.discriminatedUnion("eventName", [
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.Message),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.Message],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskCreated),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCreated],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskStarted),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskStarted],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskModeSwitched),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskModeSwitched],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskPaused),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskPaused],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskUnpaused),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskUnpaused],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskAskResponded),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAskResponded],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskAborted),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAborted],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskSpawned),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskSpawned],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskCompleted),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCompleted],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskTokenUsageUpdated),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskTokenUsageUpdated],
|
||||
}),
|
||||
])
|
||||
|
||||
export type TaskEvent = z.infer<typeof taskEventSchema>
|
||||
|
||||
/**
|
||||
* IpcMessage
|
||||
*/
|
||||
|
||||
export enum IpcMessageType {
|
||||
Connect = "Connect",
|
||||
Disconnect = "Disconnect",
|
||||
Ack = "Ack",
|
||||
TaskCommand = "TaskCommand",
|
||||
TaskEvent = "TaskEvent",
|
||||
}
|
||||
|
||||
export enum IpcOrigin {
|
||||
Client = "client",
|
||||
Server = "server",
|
||||
}
|
||||
|
||||
export const ipcMessageSchema = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal(IpcMessageType.Ack),
|
||||
origin: z.literal(IpcOrigin.Server),
|
||||
data: ackSchema,
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(IpcMessageType.TaskCommand),
|
||||
origin: z.literal(IpcOrigin.Client),
|
||||
clientId: z.string(),
|
||||
data: taskCommandSchema,
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(IpcMessageType.TaskEvent),
|
||||
origin: z.literal(IpcOrigin.Server),
|
||||
relayClientId: z.string().optional(),
|
||||
data: taskEventSchema,
|
||||
}),
|
||||
])
|
||||
|
||||
export type IpcMessage = z.infer<typeof ipcMessageSchema>
|
||||
118
packages/types/src/message.ts
Normal file
118
packages/types/src/message.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* ClineAsk
|
||||
*/
|
||||
|
||||
export const clineAsks = [
|
||||
"followup",
|
||||
"command",
|
||||
"command_output",
|
||||
"completion_result",
|
||||
"tool",
|
||||
"api_req_failed",
|
||||
"resume_task",
|
||||
"resume_completed_task",
|
||||
"mistake_limit_reached",
|
||||
"browser_action_launch",
|
||||
"use_mcp_server",
|
||||
"auto_approval_max_req_reached",
|
||||
] as const
|
||||
|
||||
export const clineAskSchema = z.enum(clineAsks)
|
||||
|
||||
export type ClineAsk = z.infer<typeof clineAskSchema>
|
||||
|
||||
/**
|
||||
* ClineSay
|
||||
*/
|
||||
|
||||
export const clineSays = [
|
||||
"error",
|
||||
"api_req_started",
|
||||
"api_req_finished",
|
||||
"api_req_retried",
|
||||
"api_req_retry_delayed",
|
||||
"api_req_deleted",
|
||||
"text",
|
||||
"reasoning",
|
||||
"completion_result",
|
||||
"user_feedback",
|
||||
"user_feedback_diff",
|
||||
"command_output",
|
||||
"shell_integration_warning",
|
||||
"browser_action",
|
||||
"browser_action_result",
|
||||
"mcp_server_request_started",
|
||||
"mcp_server_response",
|
||||
"subtask_result",
|
||||
"checkpoint_saved",
|
||||
"rooignore_error",
|
||||
"diff_error",
|
||||
"condense_context",
|
||||
"codebase_search_result",
|
||||
] as const
|
||||
|
||||
export const clineSaySchema = z.enum(clineSays)
|
||||
|
||||
export type ClineSay = z.infer<typeof clineSaySchema>
|
||||
|
||||
/**
|
||||
* ToolProgressStatus
|
||||
*/
|
||||
|
||||
export const toolProgressStatusSchema = z.object({
|
||||
icon: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
})
|
||||
|
||||
export type ToolProgressStatus = z.infer<typeof toolProgressStatusSchema>
|
||||
|
||||
/**
|
||||
* ContextCondense
|
||||
*/
|
||||
|
||||
export const contextCondenseSchema = z.object({
|
||||
cost: z.number(),
|
||||
prevContextTokens: z.number(),
|
||||
newContextTokens: z.number(),
|
||||
summary: z.string(),
|
||||
})
|
||||
|
||||
export type ContextCondense = z.infer<typeof contextCondenseSchema>
|
||||
|
||||
/**
|
||||
* ClineMessage
|
||||
*/
|
||||
|
||||
export const clineMessageSchema = z.object({
|
||||
ts: z.number(),
|
||||
type: z.union([z.literal("ask"), z.literal("say")]),
|
||||
ask: clineAskSchema.optional(),
|
||||
say: clineSaySchema.optional(),
|
||||
text: z.string().optional(),
|
||||
images: z.array(z.string()).optional(),
|
||||
partial: z.boolean().optional(),
|
||||
reasoning: z.string().optional(),
|
||||
conversationHistoryIndex: z.number().optional(),
|
||||
checkpoint: z.record(z.string(), z.unknown()).optional(),
|
||||
progressStatus: toolProgressStatusSchema.optional(),
|
||||
contextCondense: contextCondenseSchema.optional(),
|
||||
})
|
||||
|
||||
export type ClineMessage = z.infer<typeof clineMessageSchema>
|
||||
|
||||
/**
|
||||
* TokenUsage
|
||||
*/
|
||||
|
||||
export const tokenUsageSchema = z.object({
|
||||
totalTokensIn: z.number(),
|
||||
totalTokensOut: z.number(),
|
||||
totalCacheWrites: z.number().optional(),
|
||||
totalCacheReads: z.number().optional(),
|
||||
totalCost: z.number(),
|
||||
contextTokens: z.number(),
|
||||
})
|
||||
|
||||
export type TokenUsage = z.infer<typeof tokenUsageSchema>
|
||||
128
packages/types/src/mode.ts
Normal file
128
packages/types/src/mode.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { z } from "zod"
|
||||
|
||||
import { toolGroupsSchema } from "./tool.js"
|
||||
|
||||
/**
|
||||
* GroupOptions
|
||||
*/
|
||||
|
||||
export const groupOptionsSchema = z.object({
|
||||
fileRegex: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(pattern) => {
|
||||
if (!pattern) {
|
||||
return true // Optional, so empty is valid.
|
||||
}
|
||||
|
||||
try {
|
||||
new RegExp(pattern)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
{ message: "Invalid regular expression pattern" },
|
||||
),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export type GroupOptions = z.infer<typeof groupOptionsSchema>
|
||||
|
||||
/**
|
||||
* GroupEntry
|
||||
*/
|
||||
|
||||
export const groupEntrySchema = z.union([toolGroupsSchema, z.tuple([toolGroupsSchema, groupOptionsSchema])])
|
||||
|
||||
export type GroupEntry = z.infer<typeof groupEntrySchema>
|
||||
|
||||
/**
|
||||
* ModeConfig
|
||||
*/
|
||||
|
||||
const groupEntryArraySchema = z.array(groupEntrySchema).refine(
|
||||
(groups) => {
|
||||
const seen = new Set()
|
||||
|
||||
return groups.every((group) => {
|
||||
// For tuples, check the group name (first element).
|
||||
const groupName = Array.isArray(group) ? group[0] : group
|
||||
|
||||
if (seen.has(groupName)) {
|
||||
return false
|
||||
}
|
||||
|
||||
seen.add(groupName)
|
||||
return true
|
||||
})
|
||||
},
|
||||
{ message: "Duplicate groups are not allowed" },
|
||||
)
|
||||
|
||||
export const modeConfigSchema = z.object({
|
||||
slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
roleDefinition: z.string().min(1, "Role definition is required"),
|
||||
whenToUse: z.string().optional(),
|
||||
customInstructions: z.string().optional(),
|
||||
groups: groupEntryArraySchema,
|
||||
source: z.enum(["global", "project"]).optional(),
|
||||
})
|
||||
|
||||
export type ModeConfig = z.infer<typeof modeConfigSchema>
|
||||
|
||||
/**
|
||||
* CustomModesSettings
|
||||
*/
|
||||
|
||||
export const customModesSettingsSchema = z.object({
|
||||
customModes: z.array(modeConfigSchema).refine(
|
||||
(modes) => {
|
||||
const slugs = new Set()
|
||||
|
||||
return modes.every((mode) => {
|
||||
if (slugs.has(mode.slug)) {
|
||||
return false
|
||||
}
|
||||
|
||||
slugs.add(mode.slug)
|
||||
return true
|
||||
})
|
||||
},
|
||||
{
|
||||
message: "Duplicate mode slugs are not allowed",
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
export type CustomModesSettings = z.infer<typeof customModesSettingsSchema>
|
||||
|
||||
/**
|
||||
* PromptComponent
|
||||
*/
|
||||
|
||||
export const promptComponentSchema = z.object({
|
||||
roleDefinition: z.string().optional(),
|
||||
whenToUse: z.string().optional(),
|
||||
customInstructions: z.string().optional(),
|
||||
})
|
||||
|
||||
export type PromptComponent = z.infer<typeof promptComponentSchema>
|
||||
|
||||
/**
|
||||
* CustomModePrompts
|
||||
*/
|
||||
|
||||
export const customModePromptsSchema = z.record(z.string(), promptComponentSchema.optional())
|
||||
|
||||
export type CustomModePrompts = z.infer<typeof customModePromptsSchema>
|
||||
|
||||
/**
|
||||
* CustomSupportPrompts
|
||||
*/
|
||||
|
||||
export const customSupportPromptsSchema = z.record(z.string(), z.string().optional())
|
||||
|
||||
export type CustomSupportPrompts = z.infer<typeof customSupportPromptsSchema>
|
||||
63
packages/types/src/model.ts
Normal file
63
packages/types/src/model.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* ReasoningEffort
|
||||
*/
|
||||
|
||||
export const reasoningEfforts = ["low", "medium", "high"] as const
|
||||
|
||||
export const reasoningEffortsSchema = z.enum(reasoningEfforts)
|
||||
|
||||
export type ReasoningEffort = z.infer<typeof reasoningEffortsSchema>
|
||||
|
||||
/**
|
||||
* ModelParameter
|
||||
*/
|
||||
|
||||
export const modelParameters = ["max_tokens", "temperature", "reasoning", "include_reasoning"] as const
|
||||
|
||||
export const modelParametersSchema = z.enum(modelParameters)
|
||||
|
||||
export type ModelParameter = z.infer<typeof modelParametersSchema>
|
||||
|
||||
export const isModelParameter = (value: string): value is ModelParameter =>
|
||||
modelParameters.includes(value as ModelParameter)
|
||||
|
||||
/**
|
||||
* ModelInfo
|
||||
*/
|
||||
|
||||
export const modelInfoSchema = z.object({
|
||||
maxTokens: z.number().nullish(),
|
||||
maxThinkingTokens: z.number().nullish(),
|
||||
contextWindow: z.number(),
|
||||
supportsImages: z.boolean().optional(),
|
||||
supportsComputerUse: z.boolean().optional(),
|
||||
supportsPromptCache: z.boolean(),
|
||||
supportsReasoningBudget: z.boolean().optional(),
|
||||
requiredReasoningBudget: z.boolean().optional(),
|
||||
supportsReasoningEffort: z.boolean().optional(),
|
||||
supportedParameters: z.array(modelParametersSchema).optional(),
|
||||
inputPrice: z.number().optional(),
|
||||
outputPrice: z.number().optional(),
|
||||
cacheWritesPrice: z.number().optional(),
|
||||
cacheReadsPrice: z.number().optional(),
|
||||
description: z.string().optional(),
|
||||
reasoningEffort: reasoningEffortsSchema.optional(),
|
||||
minTokensPerCachePoint: z.number().optional(),
|
||||
maxCachePoints: z.number().optional(),
|
||||
cachableFields: z.array(z.string()).optional(),
|
||||
tiers: z
|
||||
.array(
|
||||
z.object({
|
||||
contextWindow: z.number(),
|
||||
inputPrice: z.number().optional(),
|
||||
outputPrice: z.number().optional(),
|
||||
cacheWritesPrice: z.number().optional(),
|
||||
cacheReadsPrice: z.number().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type ModelInfo = z.infer<typeof modelInfoSchema>
|
||||
360
packages/types/src/provider-settings.ts
Normal file
360
packages/types/src/provider-settings.ts
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
import { z } from "zod"
|
||||
|
||||
import type { Keys } from "./type-fu.js"
|
||||
import { reasoningEffortsSchema, modelInfoSchema } from "./model.js"
|
||||
import { codebaseIndexProviderSchema } from "./codebase-index.js"
|
||||
|
||||
/**
|
||||
* ProviderName
|
||||
*/
|
||||
|
||||
export const providerNames = [
|
||||
"anthropic",
|
||||
"glama",
|
||||
"openrouter",
|
||||
"bedrock",
|
||||
"vertex",
|
||||
"openai",
|
||||
"ollama",
|
||||
"vscode-lm",
|
||||
"lmstudio",
|
||||
"gemini",
|
||||
"openai-native",
|
||||
"mistral",
|
||||
"deepseek",
|
||||
"unbound",
|
||||
"requesty",
|
||||
"human-relay",
|
||||
"fake-ai",
|
||||
"xai",
|
||||
"groq",
|
||||
"chutes",
|
||||
"litellm",
|
||||
] as const
|
||||
|
||||
export const providerNamesSchema = z.enum(providerNames)
|
||||
|
||||
export type ProviderName = z.infer<typeof providerNamesSchema>
|
||||
|
||||
/**
|
||||
* ProviderSettingsEntry
|
||||
*/
|
||||
|
||||
export const providerSettingsEntrySchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
apiProvider: providerNamesSchema.optional(),
|
||||
})
|
||||
|
||||
export type ProviderSettingsEntry = z.infer<typeof providerSettingsEntrySchema>
|
||||
|
||||
/**
|
||||
* ProviderSettings
|
||||
*/
|
||||
|
||||
const baseProviderSettingsSchema = z.object({
|
||||
includeMaxTokens: z.boolean().optional(),
|
||||
diffEnabled: z.boolean().optional(),
|
||||
fuzzyMatchThreshold: z.number().optional(),
|
||||
modelTemperature: z.number().nullish(),
|
||||
rateLimitSeconds: z.number().optional(),
|
||||
|
||||
// Model reasoning.
|
||||
enableReasoningEffort: z.boolean().optional(),
|
||||
reasoningEffort: reasoningEffortsSchema.optional(),
|
||||
modelMaxTokens: z.number().optional(),
|
||||
modelMaxThinkingTokens: z.number().optional(),
|
||||
})
|
||||
|
||||
// Several of the providers share common model config properties.
|
||||
const apiModelIdProviderModelSchema = baseProviderSettingsSchema.extend({
|
||||
apiModelId: z.string().optional(),
|
||||
})
|
||||
|
||||
const anthropicSchema = apiModelIdProviderModelSchema.extend({
|
||||
apiKey: z.string().optional(),
|
||||
anthropicBaseUrl: z.string().optional(),
|
||||
anthropicUseAuthToken: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const glamaSchema = baseProviderSettingsSchema.extend({
|
||||
glamaModelId: z.string().optional(),
|
||||
glamaApiKey: z.string().optional(),
|
||||
})
|
||||
|
||||
const openRouterSchema = baseProviderSettingsSchema.extend({
|
||||
openRouterApiKey: z.string().optional(),
|
||||
openRouterModelId: z.string().optional(),
|
||||
openRouterBaseUrl: z.string().optional(),
|
||||
openRouterSpecificProvider: z.string().optional(),
|
||||
openRouterUseMiddleOutTransform: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const bedrockSchema = apiModelIdProviderModelSchema.extend({
|
||||
awsAccessKey: z.string().optional(),
|
||||
awsSecretKey: z.string().optional(),
|
||||
awsSessionToken: z.string().optional(),
|
||||
awsRegion: z.string().optional(),
|
||||
awsUseCrossRegionInference: z.boolean().optional(),
|
||||
awsUsePromptCache: z.boolean().optional(),
|
||||
awsProfile: z.string().optional(),
|
||||
awsUseProfile: z.boolean().optional(),
|
||||
awsCustomArn: z.string().optional(),
|
||||
})
|
||||
|
||||
const vertexSchema = apiModelIdProviderModelSchema.extend({
|
||||
vertexKeyFile: z.string().optional(),
|
||||
vertexJsonCredentials: z.string().optional(),
|
||||
vertexProjectId: z.string().optional(),
|
||||
vertexRegion: z.string().optional(),
|
||||
})
|
||||
|
||||
const openAiSchema = baseProviderSettingsSchema.extend({
|
||||
openAiBaseUrl: z.string().optional(),
|
||||
openAiApiKey: z.string().optional(),
|
||||
openAiLegacyFormat: z.boolean().optional(),
|
||||
openAiR1FormatEnabled: z.boolean().optional(),
|
||||
openAiModelId: z.string().optional(),
|
||||
openAiCustomModelInfo: modelInfoSchema.nullish(),
|
||||
openAiUseAzure: z.boolean().optional(),
|
||||
azureApiVersion: z.string().optional(),
|
||||
openAiStreamingEnabled: z.boolean().optional(),
|
||||
openAiHostHeader: z.string().optional(), // Keep temporarily for backward compatibility during migration.
|
||||
openAiHeaders: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
|
||||
const ollamaSchema = baseProviderSettingsSchema.extend({
|
||||
ollamaModelId: z.string().optional(),
|
||||
ollamaBaseUrl: z.string().optional(),
|
||||
})
|
||||
|
||||
const vsCodeLmSchema = baseProviderSettingsSchema.extend({
|
||||
vsCodeLmModelSelector: z
|
||||
.object({
|
||||
vendor: z.string().optional(),
|
||||
family: z.string().optional(),
|
||||
version: z.string().optional(),
|
||||
id: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const lmStudioSchema = baseProviderSettingsSchema.extend({
|
||||
lmStudioModelId: z.string().optional(),
|
||||
lmStudioBaseUrl: z.string().optional(),
|
||||
lmStudioDraftModelId: z.string().optional(),
|
||||
lmStudioSpeculativeDecodingEnabled: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const geminiSchema = apiModelIdProviderModelSchema.extend({
|
||||
geminiApiKey: z.string().optional(),
|
||||
googleGeminiBaseUrl: z.string().optional(),
|
||||
})
|
||||
|
||||
const openAiNativeSchema = apiModelIdProviderModelSchema.extend({
|
||||
openAiNativeApiKey: z.string().optional(),
|
||||
openAiNativeBaseUrl: z.string().optional(),
|
||||
})
|
||||
|
||||
const mistralSchema = apiModelIdProviderModelSchema.extend({
|
||||
mistralApiKey: z.string().optional(),
|
||||
mistralCodestralUrl: z.string().optional(),
|
||||
})
|
||||
|
||||
const deepSeekSchema = apiModelIdProviderModelSchema.extend({
|
||||
deepSeekBaseUrl: z.string().optional(),
|
||||
deepSeekApiKey: z.string().optional(),
|
||||
})
|
||||
|
||||
const unboundSchema = baseProviderSettingsSchema.extend({
|
||||
unboundApiKey: z.string().optional(),
|
||||
unboundModelId: z.string().optional(),
|
||||
})
|
||||
|
||||
const requestySchema = baseProviderSettingsSchema.extend({
|
||||
requestyApiKey: z.string().optional(),
|
||||
requestyModelId: z.string().optional(),
|
||||
})
|
||||
|
||||
const humanRelaySchema = baseProviderSettingsSchema
|
||||
|
||||
const fakeAiSchema = baseProviderSettingsSchema.extend({
|
||||
fakeAi: z.unknown().optional(),
|
||||
})
|
||||
|
||||
const xaiSchema = apiModelIdProviderModelSchema.extend({
|
||||
xaiApiKey: z.string().optional(),
|
||||
})
|
||||
|
||||
const groqSchema = apiModelIdProviderModelSchema.extend({
|
||||
groqApiKey: z.string().optional(),
|
||||
})
|
||||
|
||||
const chutesSchema = apiModelIdProviderModelSchema.extend({
|
||||
chutesApiKey: z.string().optional(),
|
||||
})
|
||||
|
||||
const litellmSchema = baseProviderSettingsSchema.extend({
|
||||
litellmBaseUrl: z.string().optional(),
|
||||
litellmApiKey: z.string().optional(),
|
||||
litellmModelId: z.string().optional(),
|
||||
})
|
||||
|
||||
const defaultSchema = z.object({
|
||||
apiProvider: z.undefined(),
|
||||
})
|
||||
|
||||
export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [
|
||||
anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })),
|
||||
glamaSchema.merge(z.object({ apiProvider: z.literal("glama") })),
|
||||
openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
|
||||
bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
|
||||
vertexSchema.merge(z.object({ apiProvider: z.literal("vertex") })),
|
||||
openAiSchema.merge(z.object({ apiProvider: z.literal("openai") })),
|
||||
ollamaSchema.merge(z.object({ apiProvider: z.literal("ollama") })),
|
||||
vsCodeLmSchema.merge(z.object({ apiProvider: z.literal("vscode-lm") })),
|
||||
lmStudioSchema.merge(z.object({ apiProvider: z.literal("lmstudio") })),
|
||||
geminiSchema.merge(z.object({ apiProvider: z.literal("gemini") })),
|
||||
openAiNativeSchema.merge(z.object({ apiProvider: z.literal("openai-native") })),
|
||||
mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })),
|
||||
deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })),
|
||||
unboundSchema.merge(z.object({ apiProvider: z.literal("unbound") })),
|
||||
requestySchema.merge(z.object({ apiProvider: z.literal("requesty") })),
|
||||
humanRelaySchema.merge(z.object({ apiProvider: z.literal("human-relay") })),
|
||||
fakeAiSchema.merge(z.object({ apiProvider: z.literal("fake-ai") })),
|
||||
xaiSchema.merge(z.object({ apiProvider: z.literal("xai") })),
|
||||
groqSchema.merge(z.object({ apiProvider: z.literal("groq") })),
|
||||
chutesSchema.merge(z.object({ apiProvider: z.literal("chutes") })),
|
||||
litellmSchema.merge(z.object({ apiProvider: z.literal("litellm") })),
|
||||
defaultSchema,
|
||||
])
|
||||
|
||||
export const providerSettingsSchema = z.object({
|
||||
apiProvider: providerNamesSchema.optional(),
|
||||
...anthropicSchema.shape,
|
||||
...glamaSchema.shape,
|
||||
...openRouterSchema.shape,
|
||||
...bedrockSchema.shape,
|
||||
...vertexSchema.shape,
|
||||
...openAiSchema.shape,
|
||||
...ollamaSchema.shape,
|
||||
...vsCodeLmSchema.shape,
|
||||
...lmStudioSchema.shape,
|
||||
...geminiSchema.shape,
|
||||
...openAiNativeSchema.shape,
|
||||
...mistralSchema.shape,
|
||||
...deepSeekSchema.shape,
|
||||
...unboundSchema.shape,
|
||||
...requestySchema.shape,
|
||||
...humanRelaySchema.shape,
|
||||
...fakeAiSchema.shape,
|
||||
...xaiSchema.shape,
|
||||
...groqSchema.shape,
|
||||
...chutesSchema.shape,
|
||||
...litellmSchema.shape,
|
||||
...codebaseIndexProviderSchema.shape,
|
||||
})
|
||||
|
||||
export type ProviderSettings = z.infer<typeof providerSettingsSchema>
|
||||
|
||||
type ProviderSettingsRecord = Record<Keys<ProviderSettings>, undefined>
|
||||
|
||||
const providerSettingsRecord: ProviderSettingsRecord = {
|
||||
apiProvider: undefined,
|
||||
// Anthropic
|
||||
apiModelId: undefined,
|
||||
apiKey: undefined,
|
||||
anthropicBaseUrl: undefined,
|
||||
anthropicUseAuthToken: undefined,
|
||||
// Glama
|
||||
glamaModelId: undefined,
|
||||
glamaApiKey: undefined,
|
||||
// OpenRouter
|
||||
openRouterApiKey: undefined,
|
||||
openRouterModelId: undefined,
|
||||
openRouterBaseUrl: undefined,
|
||||
openRouterSpecificProvider: undefined,
|
||||
openRouterUseMiddleOutTransform: undefined,
|
||||
// Amazon Bedrock
|
||||
awsAccessKey: undefined,
|
||||
awsSecretKey: undefined,
|
||||
awsSessionToken: undefined,
|
||||
awsRegion: undefined,
|
||||
awsUseCrossRegionInference: undefined,
|
||||
awsUsePromptCache: undefined,
|
||||
awsProfile: undefined,
|
||||
awsUseProfile: undefined,
|
||||
awsCustomArn: undefined,
|
||||
// Google Vertex
|
||||
vertexKeyFile: undefined,
|
||||
vertexJsonCredentials: undefined,
|
||||
vertexProjectId: undefined,
|
||||
vertexRegion: undefined,
|
||||
// OpenAI
|
||||
openAiBaseUrl: undefined,
|
||||
openAiApiKey: undefined,
|
||||
openAiLegacyFormat: undefined,
|
||||
openAiR1FormatEnabled: undefined,
|
||||
openAiModelId: undefined,
|
||||
openAiCustomModelInfo: undefined,
|
||||
openAiUseAzure: undefined,
|
||||
azureApiVersion: undefined,
|
||||
openAiStreamingEnabled: undefined,
|
||||
openAiHostHeader: undefined, // Keep temporarily for backward compatibility during migration
|
||||
openAiHeaders: undefined,
|
||||
// Ollama
|
||||
ollamaModelId: undefined,
|
||||
ollamaBaseUrl: undefined,
|
||||
// VS Code LM
|
||||
vsCodeLmModelSelector: undefined,
|
||||
lmStudioModelId: undefined,
|
||||
lmStudioBaseUrl: undefined,
|
||||
lmStudioDraftModelId: undefined,
|
||||
lmStudioSpeculativeDecodingEnabled: undefined,
|
||||
// Gemini
|
||||
geminiApiKey: undefined,
|
||||
googleGeminiBaseUrl: undefined,
|
||||
// OpenAI Native
|
||||
openAiNativeApiKey: undefined,
|
||||
openAiNativeBaseUrl: undefined,
|
||||
// Mistral
|
||||
mistralApiKey: undefined,
|
||||
mistralCodestralUrl: undefined,
|
||||
// DeepSeek
|
||||
deepSeekBaseUrl: undefined,
|
||||
deepSeekApiKey: undefined,
|
||||
// Unbound
|
||||
unboundApiKey: undefined,
|
||||
unboundModelId: undefined,
|
||||
// Requesty
|
||||
requestyApiKey: undefined,
|
||||
requestyModelId: undefined,
|
||||
// Code Index
|
||||
codeIndexOpenAiKey: undefined,
|
||||
codeIndexQdrantApiKey: undefined,
|
||||
// Reasoning
|
||||
enableReasoningEffort: undefined,
|
||||
reasoningEffort: undefined,
|
||||
modelMaxTokens: undefined,
|
||||
modelMaxThinkingTokens: undefined,
|
||||
// Generic
|
||||
includeMaxTokens: undefined,
|
||||
diffEnabled: undefined,
|
||||
fuzzyMatchThreshold: undefined,
|
||||
modelTemperature: undefined,
|
||||
rateLimitSeconds: undefined,
|
||||
// Fake AI
|
||||
fakeAi: undefined,
|
||||
// X.AI (Grok)
|
||||
xaiApiKey: undefined,
|
||||
// Groq
|
||||
groqApiKey: undefined,
|
||||
// Chutes AI
|
||||
chutesApiKey: undefined,
|
||||
// LiteLLM
|
||||
litellmBaseUrl: undefined,
|
||||
litellmApiKey: undefined,
|
||||
litellmModelId: undefined,
|
||||
}
|
||||
|
||||
export const PROVIDER_SETTINGS_KEYS = Object.keys(providerSettingsRecord) as Keys<ProviderSettings>[]
|
||||
134
packages/types/src/telemetry.ts
Normal file
134
packages/types/src/telemetry.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { z } from "zod"
|
||||
|
||||
import { providerNames } from "./provider-settings.js"
|
||||
|
||||
/**
|
||||
* TelemetrySetting
|
||||
*/
|
||||
|
||||
export const telemetrySettings = ["unset", "enabled", "disabled"] as const
|
||||
|
||||
export const telemetrySettingsSchema = z.enum(telemetrySettings)
|
||||
|
||||
export type TelemetrySetting = z.infer<typeof telemetrySettingsSchema>
|
||||
|
||||
/**
|
||||
* TelemetryEventName
|
||||
*/
|
||||
|
||||
export enum TelemetryEventName {
|
||||
TASK_CREATED = "Task Created",
|
||||
TASK_RESTARTED = "Task Reopened",
|
||||
TASK_COMPLETED = "Task Completed",
|
||||
TASK_CONVERSATION_MESSAGE = "Conversation Message",
|
||||
LLM_COMPLETION = "LLM Completion",
|
||||
MODE_SWITCH = "Mode Switched",
|
||||
TOOL_USED = "Tool Used",
|
||||
|
||||
CHECKPOINT_CREATED = "Checkpoint Created",
|
||||
CHECKPOINT_RESTORED = "Checkpoint Restored",
|
||||
CHECKPOINT_DIFFED = "Checkpoint Diffed",
|
||||
|
||||
CONTEXT_CONDENSED = "Context Condensed",
|
||||
SLIDING_WINDOW_TRUNCATION = "Sliding Window Truncation",
|
||||
|
||||
CODE_ACTION_USED = "Code Action Used",
|
||||
PROMPT_ENHANCED = "Prompt Enhanced",
|
||||
|
||||
TITLE_BUTTON_CLICKED = "Title Button Clicked",
|
||||
|
||||
AUTHENTICATION_INITIATED = "Authentication Initiated",
|
||||
|
||||
SCHEMA_VALIDATION_ERROR = "Schema Validation Error",
|
||||
DIFF_APPLICATION_ERROR = "Diff Application Error",
|
||||
SHELL_INTEGRATION_ERROR = "Shell Integration Error",
|
||||
CONSECUTIVE_MISTAKE_ERROR = "Consecutive Mistake Error",
|
||||
}
|
||||
|
||||
/**
|
||||
* TelemetryProperties
|
||||
*/
|
||||
|
||||
export const appPropertiesSchema = z.object({
|
||||
appVersion: z.string(),
|
||||
vscodeVersion: z.string(),
|
||||
platform: z.string(),
|
||||
editorName: z.string(),
|
||||
language: z.string(),
|
||||
mode: z.string(),
|
||||
})
|
||||
|
||||
export const taskPropertiesSchema = z.object({
|
||||
taskId: z.string().optional(),
|
||||
apiProvider: z.enum(providerNames).optional(),
|
||||
modelId: z.string().optional(),
|
||||
diffStrategy: z.string().optional(),
|
||||
isSubtask: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const telemetryPropertiesSchema = z.object({
|
||||
...appPropertiesSchema.shape,
|
||||
...taskPropertiesSchema.shape,
|
||||
})
|
||||
|
||||
export type TelemetryProperties = z.infer<typeof telemetryPropertiesSchema>
|
||||
|
||||
/**
|
||||
* TelemetryEvent
|
||||
*/
|
||||
|
||||
export type TelemetryEvent = {
|
||||
event: TelemetryEventName
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
properties?: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* RooCodeTelemetryEvent
|
||||
*/
|
||||
|
||||
const completionPropertiesSchema = z.object({
|
||||
inputTokens: z.number(),
|
||||
outputTokens: z.number(),
|
||||
cacheReadTokens: z.number().optional(),
|
||||
cacheWriteTokens: z.number().optional(),
|
||||
cost: z.number().optional(),
|
||||
})
|
||||
|
||||
export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.enum([
|
||||
TelemetryEventName.TASK_CREATED,
|
||||
TelemetryEventName.TASK_RESTARTED,
|
||||
TelemetryEventName.TASK_COMPLETED,
|
||||
TelemetryEventName.TASK_CONVERSATION_MESSAGE,
|
||||
TelemetryEventName.MODE_SWITCH,
|
||||
TelemetryEventName.TOOL_USED,
|
||||
TelemetryEventName.CHECKPOINT_CREATED,
|
||||
TelemetryEventName.CHECKPOINT_RESTORED,
|
||||
TelemetryEventName.CHECKPOINT_DIFFED,
|
||||
TelemetryEventName.CODE_ACTION_USED,
|
||||
TelemetryEventName.PROMPT_ENHANCED,
|
||||
TelemetryEventName.TITLE_BUTTON_CLICKED,
|
||||
TelemetryEventName.AUTHENTICATION_INITIATED,
|
||||
TelemetryEventName.SCHEMA_VALIDATION_ERROR,
|
||||
TelemetryEventName.DIFF_APPLICATION_ERROR,
|
||||
TelemetryEventName.SHELL_INTEGRATION_ERROR,
|
||||
TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR,
|
||||
]),
|
||||
properties: z.object({
|
||||
...appPropertiesSchema.shape,
|
||||
...taskPropertiesSchema.shape,
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(TelemetryEventName.LLM_COMPLETION),
|
||||
properties: z.object({
|
||||
...appPropertiesSchema.shape,
|
||||
...taskPropertiesSchema.shape,
|
||||
...completionPropertiesSchema.shape,
|
||||
}),
|
||||
}),
|
||||
])
|
||||
|
||||
export type RooCodeTelemetryEvent = z.infer<typeof rooCodeTelemetryEventSchema>
|
||||
30
packages/types/src/terminal.ts
Normal file
30
packages/types/src/terminal.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* CommandExecutionStatus
|
||||
*/
|
||||
|
||||
export const commandExecutionStatusSchema = z.discriminatedUnion("status", [
|
||||
z.object({
|
||||
executionId: z.string(),
|
||||
status: z.literal("started"),
|
||||
pid: z.number().optional(),
|
||||
command: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
executionId: z.string(),
|
||||
status: z.literal("output"),
|
||||
output: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
executionId: z.string(),
|
||||
status: z.literal("exited"),
|
||||
exitCode: z.number().optional(),
|
||||
}),
|
||||
z.object({
|
||||
executionId: z.string(),
|
||||
status: z.literal("fallback"),
|
||||
}),
|
||||
])
|
||||
|
||||
export type CommandExecutionStatus = z.infer<typeof commandExecutionStatusSchema>
|
||||
54
packages/types/src/tool.ts
Normal file
54
packages/types/src/tool.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* ToolGroup
|
||||
*/
|
||||
|
||||
export const toolGroups = ["read", "edit", "browser", "command", "mcp", "modes"] as const
|
||||
|
||||
export const toolGroupsSchema = z.enum(toolGroups)
|
||||
|
||||
export type ToolGroup = z.infer<typeof toolGroupsSchema>
|
||||
|
||||
/**
|
||||
* ToolName
|
||||
*/
|
||||
|
||||
export const toolNames = [
|
||||
"execute_command",
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"apply_diff",
|
||||
"insert_content",
|
||||
"search_and_replace",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_code_definition_names",
|
||||
"browser_action",
|
||||
"use_mcp_tool",
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
"switch_mode",
|
||||
"new_task",
|
||||
"fetch_instructions",
|
||||
"codebase_search",
|
||||
] as const
|
||||
|
||||
export const toolNamesSchema = z.enum(toolNames)
|
||||
|
||||
export type ToolName = z.infer<typeof toolNamesSchema>
|
||||
|
||||
/**
|
||||
* ToolUsage
|
||||
*/
|
||||
|
||||
export const toolUsageSchema = z.record(
|
||||
toolNamesSchema,
|
||||
z.object({
|
||||
attempts: z.number(),
|
||||
failures: z.number(),
|
||||
}),
|
||||
)
|
||||
|
||||
export type ToolUsage = z.infer<typeof toolUsageSchema>
|
||||
11
packages/types/src/type-fu.ts
Normal file
11
packages/types/src/type-fu.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* TS
|
||||
*/
|
||||
|
||||
export type Keys<T> = keyof T
|
||||
|
||||
export type Values<T> = T[keyof T]
|
||||
|
||||
export type Equals<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false
|
||||
|
||||
export type AssertEqual<T extends true> = T
|
||||
File diff suppressed because it is too large
Load diff
84
packages/types/src/vscode.ts
Normal file
84
packages/types/src/vscode.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* CodeAction
|
||||
*/
|
||||
|
||||
export const codeActionIds = ["explainCode", "fixCode", "improveCode", "addToContext", "newTask"] as const
|
||||
|
||||
export type CodeActionId = (typeof codeActionIds)[number]
|
||||
|
||||
export type CodeActionName = "EXPLAIN" | "FIX" | "IMPROVE" | "ADD_TO_CONTEXT" | "NEW_TASK"
|
||||
|
||||
/**
|
||||
* TerminalAction
|
||||
*/
|
||||
|
||||
export const terminalActionIds = ["terminalAddToContext", "terminalFixCommand", "terminalExplainCommand"] as const
|
||||
|
||||
export type TerminalActionId = (typeof terminalActionIds)[number]
|
||||
|
||||
export type TerminalActionName = "ADD_TO_CONTEXT" | "FIX" | "EXPLAIN"
|
||||
|
||||
export type TerminalActionPromptType = `TERMINAL_${TerminalActionName}`
|
||||
|
||||
/**
|
||||
* Command
|
||||
*/
|
||||
|
||||
export const commandIds = [
|
||||
"activationCompleted",
|
||||
|
||||
"plusButtonClicked",
|
||||
"promptsButtonClicked",
|
||||
"mcpButtonClicked",
|
||||
"historyButtonClicked",
|
||||
"popoutButtonClicked",
|
||||
"settingsButtonClicked",
|
||||
|
||||
"openInNewTab",
|
||||
|
||||
"showHumanRelayDialog",
|
||||
"registerHumanRelayCallback",
|
||||
"unregisterHumanRelayCallback",
|
||||
"handleHumanRelayResponse",
|
||||
|
||||
"newTask",
|
||||
|
||||
"setCustomStoragePath",
|
||||
|
||||
"focusInput",
|
||||
"acceptInput",
|
||||
] as const
|
||||
|
||||
export type CommandId = (typeof commandIds)[number]
|
||||
|
||||
/**
|
||||
* Language
|
||||
*/
|
||||
|
||||
export const languages = [
|
||||
"ca",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"hi",
|
||||
"it",
|
||||
"ja",
|
||||
"ko",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt-BR",
|
||||
"ru",
|
||||
"tr",
|
||||
"vi",
|
||||
"zh-CN",
|
||||
"zh-TW",
|
||||
] as const
|
||||
|
||||
export const languagesSchema = z.enum(languages)
|
||||
|
||||
export type Language = z.infer<typeof languagesSchema>
|
||||
|
||||
export const isLanguage = (value: string): value is Language => languages.includes(value as Language)
|
||||
|
|
@ -1323,6 +1323,21 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
} finally {
|
||||
this.isStreaming = false
|
||||
}
|
||||
if (
|
||||
inputTokens > 0 ||
|
||||
outputTokens > 0 ||
|
||||
cacheWriteTokens > 0 ||
|
||||
cacheReadTokens > 0 ||
|
||||
typeof totalCost !== "undefined"
|
||||
) {
|
||||
telemetryService.captureLlmCompletion(this.taskId, {
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
cost: totalCost,
|
||||
})
|
||||
}
|
||||
|
||||
// Need to call here in case the stream was aborted.
|
||||
if (this.abort || this.abandoned) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import type {
|
|||
ProviderSettings,
|
||||
RooCodeSettings,
|
||||
ProviderSettingsEntry,
|
||||
TelemetryProperties,
|
||||
CodeActionId,
|
||||
CodeActionName,
|
||||
TerminalActionId,
|
||||
|
|
@ -52,7 +53,7 @@ import { Task, TaskOptions } from "../task/Task"
|
|||
import { getNonce } from "./getNonce"
|
||||
import { getUri } from "./getUri"
|
||||
import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
|
||||
import { telemetryService } from "../../services/telemetry/TelemetryService"
|
||||
import { TelemetryPropertiesProvider, telemetryService } from "../../services/telemetry"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
import { webviewMessageHandler } from "./webviewMessageHandler"
|
||||
import { WebviewMessage } from "../../shared/WebviewMessage"
|
||||
|
|
@ -67,7 +68,10 @@ export type ClineProviderEvents = {
|
|||
clineCreated: [cline: Task]
|
||||
}
|
||||
|
||||
export class ClineProvider extends EventEmitter<ClineProviderEvents> implements vscode.WebviewViewProvider {
|
||||
export class ClineProvider
|
||||
extends EventEmitter<ClineProviderEvents>
|
||||
implements vscode.WebviewViewProvider, TelemetryPropertiesProvider
|
||||
{
|
||||
// Used in package.json as the view's id. This value cannot be changed due
|
||||
// to how VSCode caches views based on their id, and updating the id would
|
||||
// break existing instances of the extension.
|
||||
|
|
@ -1566,59 +1570,21 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
* This method is called by the telemetry service to get context information
|
||||
* like the current mode, API provider, etc.
|
||||
*/
|
||||
public async getTelemetryProperties(): Promise<Record<string, any>> {
|
||||
public async getTelemetryProperties(): Promise<TelemetryProperties> {
|
||||
const { mode, apiConfiguration, language } = await this.getState()
|
||||
const appVersion = this.context.extension?.packageJSON?.version
|
||||
const vscodeVersion = vscode.version
|
||||
const platform = process.platform
|
||||
const editorName = vscode.env.appName // Get the editor name (VS Code, Cursor, etc.)
|
||||
const task = this.getCurrentCline()
|
||||
|
||||
const properties: Record<string, any> = {
|
||||
vscodeVersion,
|
||||
platform,
|
||||
editorName,
|
||||
return {
|
||||
appVersion: this.context.extension?.packageJSON?.version,
|
||||
vscodeVersion: vscode.version,
|
||||
platform: process.platform,
|
||||
editorName: vscode.env.appName,
|
||||
language,
|
||||
mode,
|
||||
apiProvider: apiConfiguration?.apiProvider,
|
||||
modelId: task?.api?.getModel().id,
|
||||
diffStrategy: task?.diffStrategy?.getName(),
|
||||
isSubtask: task ? !!task.parentTask : undefined,
|
||||
}
|
||||
|
||||
// Add extension version
|
||||
if (appVersion) {
|
||||
properties.appVersion = appVersion
|
||||
}
|
||||
|
||||
// Add language
|
||||
if (language) {
|
||||
properties.language = language
|
||||
}
|
||||
|
||||
// Add current mode
|
||||
if (mode) {
|
||||
properties.mode = mode
|
||||
}
|
||||
|
||||
// Add API provider
|
||||
if (apiConfiguration?.apiProvider) {
|
||||
properties.apiProvider = apiConfiguration.apiProvider
|
||||
}
|
||||
|
||||
// Add model ID if available
|
||||
const currentCline = this.getCurrentCline()
|
||||
|
||||
if (currentCline?.api) {
|
||||
const { id: modelId } = currentCline.api.getModel()
|
||||
|
||||
if (modelId) {
|
||||
properties.modelId = modelId
|
||||
}
|
||||
}
|
||||
|
||||
if (currentCline?.diffStrategy) {
|
||||
properties.diffStrategy = currentCline.diffStrategy.getName()
|
||||
}
|
||||
|
||||
// Add isSubtask property that indicates whether this task is a subtask
|
||||
if (currentCline) {
|
||||
properties.isSubtask = !!currentCline.parentTask
|
||||
}
|
||||
|
||||
return properties
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import { getOllamaModels } from "../../api/providers/ollama"
|
|||
import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
|
||||
import { getLmStudioModels } from "../../api/providers/lmstudio"
|
||||
import { openMention } from "../mentions"
|
||||
import { telemetryService } from "../../services/telemetry/TelemetryService"
|
||||
import { telemetryService } from "../../services/telemetry"
|
||||
import { TelemetrySetting } from "../../shared/TelemetrySetting"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
import { Mode, defaultModeSlug } from "../../shared/modes"
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
await migrateSettings(context, outputChannel)
|
||||
|
||||
// Initialize telemetry service after environment variables are loaded.
|
||||
telemetryService.initialize()
|
||||
telemetryService.initialize(context)
|
||||
|
||||
// Initialize i18n for internationalization support
|
||||
initializeI18n(context.globalState.get("language") ?? formatLanguage(vscode.env.language))
|
||||
|
|
|
|||
|
|
@ -1,150 +0,0 @@
|
|||
import { PostHog } from "posthog-node"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
import { logger } from "../../utils/logging"
|
||||
|
||||
// This forward declaration is needed to avoid circular dependencies
|
||||
export interface ClineProviderInterface {
|
||||
// Gets telemetry properties to attach to every event
|
||||
getTelemetryProperties(): Promise<Record<string, any>>
|
||||
}
|
||||
|
||||
/**
|
||||
* PostHogClient handles telemetry event tracking for the Roo Code extension
|
||||
* Uses PostHog analytics to track user interactions and system events
|
||||
* Respects user privacy settings and VSCode's global telemetry configuration
|
||||
*/
|
||||
export class PostHogClient {
|
||||
public static readonly EVENTS = {
|
||||
TASK: {
|
||||
CREATED: "Task Created",
|
||||
RESTARTED: "Task Reopened",
|
||||
COMPLETED: "Task Completed",
|
||||
CONVERSATION_MESSAGE: "Conversation Message",
|
||||
MODE_SWITCH: "Mode Switched",
|
||||
TOOL_USED: "Tool Used",
|
||||
CHECKPOINT_CREATED: "Checkpoint Created",
|
||||
CHECKPOINT_RESTORED: "Checkpoint Restored",
|
||||
CHECKPOINT_DIFFED: "Checkpoint Diffed",
|
||||
CODE_ACTION_USED: "Code Action Used",
|
||||
PROMPT_ENHANCED: "Prompt Enhanced",
|
||||
CONTEXT_CONDENSED: "Context Condensed",
|
||||
SLIDING_WINDOW_TRUNCATION: "Sliding Window Truncation",
|
||||
},
|
||||
ERRORS: {
|
||||
SCHEMA_VALIDATION_ERROR: "Schema Validation Error",
|
||||
DIFF_APPLICATION_ERROR: "Diff Application Error",
|
||||
SHELL_INTEGRATION_ERROR: "Shell Integration Error",
|
||||
CONSECUTIVE_MISTAKE_ERROR: "Consecutive Mistake Error",
|
||||
},
|
||||
}
|
||||
|
||||
private static instance: PostHogClient
|
||||
private client: PostHog
|
||||
private distinctId: string = vscode.env.machineId
|
||||
private telemetryEnabled: boolean = false
|
||||
private providerRef: WeakRef<ClineProviderInterface> | null = null
|
||||
|
||||
private constructor() {
|
||||
this.client = new PostHog(process.env.POSTHOG_API_KEY || "", { host: "https://us.i.posthog.com" })
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the telemetry state based on user preferences and VSCode settings
|
||||
* Only enables telemetry if both VSCode global telemetry is enabled and user has opted in
|
||||
* @param didUserOptIn Whether the user has explicitly opted into telemetry
|
||||
*/
|
||||
public updateTelemetryState(didUserOptIn: boolean): void {
|
||||
this.telemetryEnabled = false
|
||||
|
||||
// First check global telemetry level - telemetry should only be enabled when level is "all"
|
||||
const telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
|
||||
const globalTelemetryEnabled = telemetryLevel === "all"
|
||||
|
||||
// We only enable telemetry if global vscode telemetry is enabled
|
||||
if (globalTelemetryEnabled) {
|
||||
this.telemetryEnabled = didUserOptIn
|
||||
}
|
||||
|
||||
// Update PostHog client state based on telemetry preference
|
||||
if (this.telemetryEnabled) {
|
||||
this.client.optIn()
|
||||
} else {
|
||||
this.client.optOut()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates the singleton instance of PostHogClient
|
||||
* @returns The PostHogClient instance
|
||||
*/
|
||||
public static getInstance(): PostHogClient {
|
||||
if (!PostHogClient.instance) {
|
||||
PostHogClient.instance = new PostHogClient()
|
||||
}
|
||||
|
||||
return PostHogClient.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ClineProvider reference to use for global properties
|
||||
* @param provider A ClineProvider instance to use
|
||||
*/
|
||||
public setProvider(provider: ClineProviderInterface): void {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
logger.debug("PostHogClient: ClineProvider reference set")
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a telemetry event if telemetry is enabled
|
||||
* @param event The event to capture with its properties
|
||||
*/
|
||||
public async capture(event: { event: string; properties?: any }): Promise<void> {
|
||||
// Only send events if telemetry is enabled
|
||||
if (this.telemetryEnabled) {
|
||||
// Get global properties from ClineProvider if available
|
||||
let globalProperties: Record<string, any> = {}
|
||||
const provider = this.providerRef?.deref()
|
||||
|
||||
if (provider) {
|
||||
try {
|
||||
// Get the telemetry properties directly from the provider
|
||||
globalProperties = await provider.getTelemetryProperties()
|
||||
} catch (error) {
|
||||
// Log error but continue with capturing the event
|
||||
logger.error(
|
||||
`Error getting telemetry properties: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge global properties with event-specific properties
|
||||
// Event properties take precedence in case of conflicts
|
||||
const mergedProperties = {
|
||||
...globalProperties,
|
||||
...(event.properties || {}),
|
||||
}
|
||||
|
||||
this.client.capture({
|
||||
distinctId: this.distinctId,
|
||||
event: event.event,
|
||||
properties: mergedProperties,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if telemetry is currently enabled
|
||||
* @returns Whether telemetry is enabled
|
||||
*/
|
||||
public isTelemetryEnabled(): boolean {
|
||||
return this.telemetryEnabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the PostHog client
|
||||
*/
|
||||
public async shutdown(): Promise<void> {
|
||||
await this.client.shutdown()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,35 @@
|
|||
import * as vscode from "vscode"
|
||||
import { ZodError } from "zod"
|
||||
|
||||
import { TelemetryEventName } from "@roo-code/types"
|
||||
|
||||
import { logger } from "../../utils/logging"
|
||||
import { PostHogClient, ClineProviderInterface } from "./PostHogClient"
|
||||
|
||||
import { PostHogTelemetryClient } from "./clients/PostHogTelemetryClient"
|
||||
import { type TelemetryClient, type TelemetryPropertiesProvider } from "./types"
|
||||
|
||||
/**
|
||||
* TelemetryService wrapper class that defers PostHogClient initialization
|
||||
* This ensures that we only create the PostHogClient after environment variables are loaded
|
||||
* TelemetryService wrapper class that defers initialization.
|
||||
* This ensures that we only create the various clients after environment
|
||||
* variables are loaded.
|
||||
*/
|
||||
class TelemetryService {
|
||||
private client: PostHogClient | null = null
|
||||
private clients: TelemetryClient[] = []
|
||||
private initialized = false
|
||||
|
||||
/**
|
||||
* Initialize the telemetry service with the PostHog client
|
||||
* This should be called after environment variables are loaded
|
||||
* Initialize the telemetry client. This should be called after environment
|
||||
* variables are loaded.
|
||||
*/
|
||||
public initialize(): void {
|
||||
public async initialize(context: vscode.ExtensionContext): Promise<void> {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
this.initialized = true
|
||||
|
||||
try {
|
||||
this.client = PostHogClient.getInstance()
|
||||
this.initialized = true
|
||||
this.clients.push(PostHogTelemetryClient.getInstance())
|
||||
} catch (error) {
|
||||
console.warn("Failed to initialize telemetry service:", error)
|
||||
}
|
||||
|
|
@ -32,10 +39,10 @@ class TelemetryService {
|
|||
* Sets the ClineProvider reference to use for global properties
|
||||
* @param provider A ClineProvider instance to use
|
||||
*/
|
||||
public setProvider(provider: ClineProviderInterface): void {
|
||||
// If client is initialized, pass the provider reference
|
||||
public setProvider(provider: TelemetryPropertiesProvider): void {
|
||||
// If client is initialized, pass the provider reference.
|
||||
if (this.isReady) {
|
||||
this.client!.setProvider(provider)
|
||||
this.clients.forEach((client) => client.setProvider(provider))
|
||||
}
|
||||
|
||||
logger.debug("TelemetryService: ClineProvider reference set")
|
||||
|
|
@ -47,7 +54,7 @@ class TelemetryService {
|
|||
* @returns Whether the service is ready to use
|
||||
*/
|
||||
private get isReady(): boolean {
|
||||
return this.initialized && this.client !== null
|
||||
return this.initialized && this.clients.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,19 +66,7 @@ class TelemetryService {
|
|||
return
|
||||
}
|
||||
|
||||
this.client!.updateTelemetryState(didUserOptIn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a telemetry event if telemetry is enabled
|
||||
* @param event The event to capture with its properties
|
||||
*/
|
||||
public capture(event: { event: string; properties?: any }): void {
|
||||
if (!this.isReady) {
|
||||
return
|
||||
}
|
||||
|
||||
this.client!.capture(event)
|
||||
this.clients.forEach((client) => client.updateTelemetryState(didUserOptIn))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -79,45 +74,61 @@ class TelemetryService {
|
|||
* @param eventName The event name to capture
|
||||
* @param properties The event properties
|
||||
*/
|
||||
public captureEvent(eventName: string, properties?: any): void {
|
||||
this.capture({ event: eventName, properties })
|
||||
public captureEvent(eventName: TelemetryEventName, properties?: any): void {
|
||||
if (!this.isReady) {
|
||||
return
|
||||
}
|
||||
|
||||
this.clients.forEach((client) => client.capture({ event: eventName, properties }))
|
||||
}
|
||||
|
||||
// Task events convenience methods
|
||||
public captureTaskCreated(taskId: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.CREATED, { taskId })
|
||||
this.captureEvent(TelemetryEventName.TASK_CREATED, { taskId })
|
||||
}
|
||||
|
||||
public captureTaskRestarted(taskId: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.RESTARTED, { taskId })
|
||||
this.captureEvent(TelemetryEventName.TASK_RESTARTED, { taskId })
|
||||
}
|
||||
|
||||
public captureTaskCompleted(taskId: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.COMPLETED, { taskId })
|
||||
this.captureEvent(TelemetryEventName.TASK_COMPLETED, { taskId })
|
||||
}
|
||||
|
||||
public captureConversationMessage(taskId: string, source: "user" | "assistant"): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.CONVERSATION_MESSAGE, { taskId, source })
|
||||
this.captureEvent(TelemetryEventName.TASK_CONVERSATION_MESSAGE, { taskId, source })
|
||||
}
|
||||
|
||||
public captureLlmCompletion(
|
||||
taskId: string,
|
||||
properties: {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheWriteTokens: number
|
||||
cacheReadTokens: number
|
||||
cost?: number
|
||||
},
|
||||
): void {
|
||||
this.captureEvent(TelemetryEventName.LLM_COMPLETION, { taskId, ...properties })
|
||||
}
|
||||
|
||||
public captureModeSwitch(taskId: string, newMode: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.MODE_SWITCH, { taskId, newMode })
|
||||
this.captureEvent(TelemetryEventName.MODE_SWITCH, { taskId, newMode })
|
||||
}
|
||||
|
||||
public captureToolUsage(taskId: string, tool: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.TOOL_USED, { taskId, tool })
|
||||
this.captureEvent(TelemetryEventName.TOOL_USED, { taskId, tool })
|
||||
}
|
||||
|
||||
public captureCheckpointCreated(taskId: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_CREATED, { taskId })
|
||||
this.captureEvent(TelemetryEventName.CHECKPOINT_CREATED, { taskId })
|
||||
}
|
||||
|
||||
public captureCheckpointDiffed(taskId: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_DIFFED, { taskId })
|
||||
this.captureEvent(TelemetryEventName.CHECKPOINT_DIFFED, { taskId })
|
||||
}
|
||||
|
||||
public captureCheckpointRestored(taskId: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_RESTORED, { taskId })
|
||||
this.captureEvent(TelemetryEventName.CHECKPOINT_RESTORED, { taskId })
|
||||
}
|
||||
|
||||
public captureContextCondensed(
|
||||
|
|
@ -126,7 +137,7 @@ class TelemetryService {
|
|||
usedCustomPrompt?: boolean,
|
||||
usedCustomApiHandler?: boolean,
|
||||
): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.CONTEXT_CONDENSED, {
|
||||
this.captureEvent(TelemetryEventName.CONTEXT_CONDENSED, {
|
||||
taskId,
|
||||
isAutomaticTrigger,
|
||||
...(usedCustomPrompt !== undefined && { usedCustomPrompt }),
|
||||
|
|
@ -135,32 +146,32 @@ class TelemetryService {
|
|||
}
|
||||
|
||||
public captureSlidingWindowTruncation(taskId: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.SLIDING_WINDOW_TRUNCATION, { taskId })
|
||||
this.captureEvent(TelemetryEventName.SLIDING_WINDOW_TRUNCATION, { taskId })
|
||||
}
|
||||
|
||||
public captureCodeActionUsed(actionType: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.CODE_ACTION_USED, { actionType })
|
||||
this.captureEvent(TelemetryEventName.CODE_ACTION_USED, { actionType })
|
||||
}
|
||||
|
||||
public capturePromptEnhanced(taskId?: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.PROMPT_ENHANCED, { ...(taskId && { taskId }) })
|
||||
this.captureEvent(TelemetryEventName.PROMPT_ENHANCED, { ...(taskId && { taskId }) })
|
||||
}
|
||||
|
||||
public captureSchemaValidationError({ schemaName, error }: { schemaName: string; error: ZodError }): void {
|
||||
// https://zod.dev/ERROR_HANDLING?id=formatting-errors
|
||||
this.captureEvent(PostHogClient.EVENTS.ERRORS.SCHEMA_VALIDATION_ERROR, { schemaName, error: error.format() })
|
||||
this.captureEvent(TelemetryEventName.SCHEMA_VALIDATION_ERROR, { schemaName, error: error.format() })
|
||||
}
|
||||
|
||||
public captureDiffApplicationError(taskId: string, consecutiveMistakeCount: number): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.ERRORS.DIFF_APPLICATION_ERROR, { taskId, consecutiveMistakeCount })
|
||||
this.captureEvent(TelemetryEventName.DIFF_APPLICATION_ERROR, { taskId, consecutiveMistakeCount })
|
||||
}
|
||||
|
||||
public captureShellIntegrationError(taskId: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.ERRORS.SHELL_INTEGRATION_ERROR, { taskId })
|
||||
this.captureEvent(TelemetryEventName.SHELL_INTEGRATION_ERROR, { taskId })
|
||||
}
|
||||
|
||||
public captureConsecutiveMistakeError(taskId: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.ERRORS.CONSECUTIVE_MISTAKE_ERROR, { taskId })
|
||||
this.captureEvent(TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR, { taskId })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -168,7 +179,7 @@ class TelemetryService {
|
|||
* @param button The button that was clicked
|
||||
*/
|
||||
public captureTitleButtonClicked(button: string): void {
|
||||
this.captureEvent("Title Button Clicked", { button })
|
||||
this.captureEvent(TelemetryEventName.TITLE_BUTTON_CLICKED, { button })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -176,20 +187,16 @@ class TelemetryService {
|
|||
* @returns Whether telemetry is enabled
|
||||
*/
|
||||
public isTelemetryEnabled(): boolean {
|
||||
return this.isReady && this.client!.isTelemetryEnabled()
|
||||
return this.isReady && this.clients.some((client) => client.isTelemetryEnabled())
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the PostHog client
|
||||
*/
|
||||
public async shutdown(): Promise<void> {
|
||||
if (!this.isReady) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.client!.shutdown()
|
||||
this.clients.forEach((client) => client.shutdown())
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance of the telemetry service wrapper
|
||||
export const telemetryService = new TelemetryService()
|
||||
|
|
|
|||
58
src/services/telemetry/clients/BaseTelemetryClient.ts
Normal file
58
src/services/telemetry/clients/BaseTelemetryClient.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { TelemetryEvent, TelemetryEventName } from "@roo-code/types"
|
||||
|
||||
import { TelemetryClient, TelemetryPropertiesProvider, TelemetryEventSubscription } from "../types"
|
||||
|
||||
export 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)
|
||||
}
|
||||
|
||||
protected async getEventProperties(event: TelemetryEvent): Promise<TelemetryEvent["properties"]> {
|
||||
let providerProperties: TelemetryEvent["properties"] = {}
|
||||
const provider = this.providerRef?.deref()
|
||||
|
||||
if (provider) {
|
||||
try {
|
||||
// Get the telemetry properties directly 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.
|
||||
return { ...providerProperties, ...(event.properties || {}) }
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
88
src/services/telemetry/clients/PostHogTelemetryClient.ts
Normal file
88
src/services/telemetry/clients/PostHogTelemetryClient.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { PostHog } from "posthog-node"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
import { TelemetryEventName, type TelemetryEvent } from "@roo-code/types"
|
||||
|
||||
import { BaseTelemetryClient } from "./BaseTelemetryClient"
|
||||
|
||||
/**
|
||||
* PostHogTelemetryClient handles telemetry event tracking for the Roo Code extension.
|
||||
* Uses PostHog analytics to track user interactions and system events.
|
||||
* Respects user privacy settings and VSCode's global telemetry configuration.
|
||||
*/
|
||||
export class PostHogTelemetryClient extends BaseTelemetryClient {
|
||||
private client: PostHog
|
||||
private distinctId: string = vscode.env.machineId
|
||||
|
||||
private constructor(debug = false) {
|
||||
super(
|
||||
{
|
||||
type: "exclude",
|
||||
events: [TelemetryEventName.LLM_COMPLETION],
|
||||
},
|
||||
debug,
|
||||
)
|
||||
|
||||
this.client = new PostHog(process.env.POSTHOG_API_KEY || "", { host: "https://us.i.posthog.com" })
|
||||
}
|
||||
|
||||
public override async capture(event: TelemetryEvent): Promise<void> {
|
||||
if (!this.isTelemetryEnabled() || !this.isEventCapturable(event.event)) {
|
||||
if (this.debug) {
|
||||
console.info(`[PostHogTelemetryClient#capture] Skipping event: ${event.event}`)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
console.info(`[PostHogTelemetryClient#capture] ${event.event}`)
|
||||
}
|
||||
|
||||
this.client.capture({
|
||||
distinctId: this.distinctId,
|
||||
event: event.event,
|
||||
properties: await this.getEventProperties(event),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the telemetry state based on user preferences and VSCode settings.
|
||||
* Only enables telemetry if both VSCode global telemetry is enabled and
|
||||
* user has opted in.
|
||||
* @param didUserOptIn Whether the user has explicitly opted into telemetry
|
||||
*/
|
||||
public override updateTelemetryState(didUserOptIn: boolean): void {
|
||||
this.telemetryEnabled = false
|
||||
|
||||
// First check global telemetry level - telemetry should only be enabled when level is "all".
|
||||
const telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
|
||||
const globalTelemetryEnabled = telemetryLevel === "all"
|
||||
|
||||
// We only enable telemetry if global vscode telemetry is enabled.
|
||||
if (globalTelemetryEnabled) {
|
||||
this.telemetryEnabled = didUserOptIn
|
||||
}
|
||||
|
||||
// Update PostHog client state based on telemetry preference.
|
||||
if (this.telemetryEnabled) {
|
||||
this.client.optIn()
|
||||
} else {
|
||||
this.client.optOut()
|
||||
}
|
||||
}
|
||||
|
||||
public override async shutdown(): Promise<void> {
|
||||
await this.client.shutdown()
|
||||
}
|
||||
|
||||
private static _instance: PostHogTelemetryClient | null = null
|
||||
|
||||
public static getInstance(): PostHogTelemetryClient {
|
||||
if (!PostHogTelemetryClient._instance) {
|
||||
PostHogTelemetryClient._instance = new PostHogTelemetryClient()
|
||||
}
|
||||
|
||||
return PostHogTelemetryClient._instance
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
// npx jest src/services/telemetry/clients/__tests__/PostHogTelemetryClient.test.ts
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import { PostHog } from "posthog-node"
|
||||
|
||||
import { TelemetryEventName } from "@roo-code/types"
|
||||
|
||||
import { TelemetryPropertiesProvider } from "../../types"
|
||||
import { PostHogTelemetryClient } from "../PostHogTelemetryClient"
|
||||
|
||||
jest.mock("posthog-node")
|
||||
|
||||
jest.mock("vscode", () => ({
|
||||
env: {
|
||||
machineId: "test-machine-id",
|
||||
},
|
||||
workspace: {
|
||||
getConfiguration: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe("PostHogTelemetryClient", () => {
|
||||
const getPrivateProperty = <T>(instance: any, propertyName: string): T => {
|
||||
return instance[propertyName]
|
||||
}
|
||||
|
||||
let mockPostHogClient: jest.Mocked<PostHog>
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
mockPostHogClient = {
|
||||
capture: jest.fn(),
|
||||
optIn: jest.fn(),
|
||||
optOut: jest.fn(),
|
||||
shutdown: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as jest.Mocked<PostHog>
|
||||
;(PostHog as unknown as jest.Mock).mockImplementation(() => mockPostHogClient)
|
||||
|
||||
// @ts-ignore - Accessing private static property for testing
|
||||
PostHogTelemetryClient._instance = undefined
|
||||
;(vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue("all"),
|
||||
})
|
||||
})
|
||||
|
||||
describe("getInstance", () => {
|
||||
it("should return the same instance when called multiple times", () => {
|
||||
const instance1 = PostHogTelemetryClient.getInstance()
|
||||
const instance2 = PostHogTelemetryClient.getInstance()
|
||||
expect(instance1).toBe(instance2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isEventCapturable", () => {
|
||||
it("should return true for events not in exclude list", () => {
|
||||
const client = PostHogTelemetryClient.getInstance()
|
||||
|
||||
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
|
||||
client,
|
||||
"isEventCapturable",
|
||||
).bind(client)
|
||||
|
||||
expect(isEventCapturable(TelemetryEventName.TASK_CREATED)).toBe(true)
|
||||
expect(isEventCapturable(TelemetryEventName.MODE_SWITCH)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for events in exclude list", () => {
|
||||
const client = PostHogTelemetryClient.getInstance()
|
||||
|
||||
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
|
||||
client,
|
||||
"isEventCapturable",
|
||||
).bind(client)
|
||||
|
||||
expect(isEventCapturable(TelemetryEventName.LLM_COMPLETION)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getEventProperties", () => {
|
||||
it("should merge provider properties with event properties", async () => {
|
||||
const client = PostHogTelemetryClient.getInstance()
|
||||
|
||||
const mockProvider: TelemetryPropertiesProvider = {
|
||||
getTelemetryProperties: jest.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 = PostHogTelemetryClient.getInstance()
|
||||
|
||||
const mockProvider: TelemetryPropertiesProvider = {
|
||||
getTelemetryProperties: jest.fn().mockRejectedValue(new Error("Provider error")),
|
||||
}
|
||||
|
||||
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation()
|
||||
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"),
|
||||
)
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should return event properties when no provider is set", async () => {
|
||||
const client = PostHogTelemetryClient.getInstance()
|
||||
|
||||
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 when telemetry is disabled", async () => {
|
||||
const client = PostHogTelemetryClient.getInstance()
|
||||
client.updateTelemetryState(false)
|
||||
|
||||
await client.capture({
|
||||
event: TelemetryEventName.TASK_CREATED,
|
||||
properties: { test: "value" },
|
||||
})
|
||||
|
||||
expect(mockPostHogClient.capture).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not capture events that are not capturable", async () => {
|
||||
const client = PostHogTelemetryClient.getInstance()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
await client.capture({
|
||||
event: TelemetryEventName.LLM_COMPLETION, // This is in the exclude list.
|
||||
properties: { test: "value" },
|
||||
})
|
||||
|
||||
expect(mockPostHogClient.capture).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should capture events when telemetry is enabled and event is capturable", async () => {
|
||||
const client = PostHogTelemetryClient.getInstance()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
const mockProvider: TelemetryPropertiesProvider = {
|
||||
getTelemetryProperties: jest.fn().mockResolvedValue({
|
||||
appVersion: "1.0.0",
|
||||
vscodeVersion: "1.60.0",
|
||||
platform: "darwin",
|
||||
editorName: "vscode",
|
||||
language: "en",
|
||||
mode: "code",
|
||||
}),
|
||||
}
|
||||
|
||||
client.setProvider(mockProvider)
|
||||
|
||||
await client.capture({
|
||||
event: TelemetryEventName.TASK_CREATED,
|
||||
properties: { test: "value" },
|
||||
})
|
||||
|
||||
expect(mockPostHogClient.capture).toHaveBeenCalledWith({
|
||||
distinctId: "test-machine-id",
|
||||
event: TelemetryEventName.TASK_CREATED,
|
||||
properties: expect.objectContaining({
|
||||
appVersion: "1.0.0",
|
||||
test: "value",
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateTelemetryState", () => {
|
||||
it("should enable telemetry when user opts in and global telemetry is enabled", () => {
|
||||
const client = PostHogTelemetryClient.getInstance()
|
||||
|
||||
;(vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue("all"),
|
||||
})
|
||||
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
expect(client.isTelemetryEnabled()).toBe(true)
|
||||
expect(mockPostHogClient.optIn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should disable telemetry when user opts out", () => {
|
||||
const client = PostHogTelemetryClient.getInstance()
|
||||
|
||||
;(vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue("all"),
|
||||
})
|
||||
|
||||
client.updateTelemetryState(false)
|
||||
|
||||
expect(client.isTelemetryEnabled()).toBe(false)
|
||||
expect(mockPostHogClient.optOut).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should disable telemetry when global telemetry is disabled, regardless of user opt-in", () => {
|
||||
const client = PostHogTelemetryClient.getInstance()
|
||||
|
||||
;(vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue("off"),
|
||||
})
|
||||
|
||||
client.updateTelemetryState(true)
|
||||
expect(client.isTelemetryEnabled()).toBe(false)
|
||||
expect(mockPostHogClient.optOut).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("shutdown", () => {
|
||||
it("should call shutdown on the PostHog client", async () => {
|
||||
const client = PostHogTelemetryClient.getInstance()
|
||||
await client.shutdown()
|
||||
expect(mockPostHogClient.shutdown).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
2
src/services/telemetry/index.ts
Normal file
2
src/services/telemetry/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./TelemetryService"
|
||||
export * from "./types"
|
||||
19
src/services/telemetry/types.ts
Normal file
19
src/services/telemetry/types.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { TelemetryEventName, type TelemetryProperties, type TelemetryEvent } from "@roo-code/types"
|
||||
|
||||
export type TelemetryEventSubscription =
|
||||
| { type: "include"; events: TelemetryEventName[] }
|
||||
| { type: "exclude"; events: TelemetryEventName[] }
|
||||
|
||||
export interface TelemetryPropertiesProvider {
|
||||
getTelemetryProperties(): Promise<TelemetryProperties>
|
||||
}
|
||||
|
||||
export interface TelemetryClient {
|
||||
subscription?: TelemetryEventSubscription
|
||||
|
||||
setProvider(provider: TelemetryPropertiesProvider): void
|
||||
capture(options: TelemetryEvent): Promise<void>
|
||||
updateTelemetryState(didUserOptIn: boolean): void
|
||||
isTelemetryEnabled(): boolean
|
||||
shutdown(): Promise<void>
|
||||
}
|
||||
210
src/utils/__tests__/refresh-timer.test.ts
Normal file
210
src/utils/__tests__/refresh-timer.test.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
import { RefreshTimer } from "../refresh-timer"
|
||||
|
||||
// Mock timers
|
||||
jest.useFakeTimers()
|
||||
|
||||
describe("RefreshTimer", () => {
|
||||
let mockCallback: jest.Mock
|
||||
let refreshTimer: RefreshTimer
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks before each test
|
||||
mockCallback = jest.fn()
|
||||
|
||||
// Default mock implementation returns success
|
||||
mockCallback.mockResolvedValue(true)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up after each test
|
||||
if (refreshTimer) {
|
||||
refreshTimer.stop()
|
||||
}
|
||||
jest.clearAllTimers()
|
||||
jest.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
|
||||
jest.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
|
||||
jest.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
|
||||
jest.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
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
await Promise.resolve() // Second attempt (backoff = 2000ms)
|
||||
jest.advanceTimersByTime(2000)
|
||||
|
||||
await Promise.resolve() // Third attempt (backoff = 4000ms)
|
||||
jest.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
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
// Second attempt (succeeds)
|
||||
await Promise.resolve()
|
||||
|
||||
// Fast-forward 5 seconds
|
||||
jest.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
|
||||
jest.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()
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
await Promise.resolve()
|
||||
jest.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)
|
||||
})
|
||||
})
|
||||
154
src/utils/refresh-timer.ts
Normal file
154
src/utils/refresh-timer.ts
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue