Download src/tools/ConfigTool/ConfigTool.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 13.5 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/tools/ConfigTool/ConfigTool.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/tools/ConfigTool/ConfigTool.ts
-
curl -L -o ConfigTool.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/tools/ConfigTool/ConfigTool.ts
13.5 kB
| import { feature } from 'bun:bundle' | |
| import { z } from 'zod/v4' | |
| import { | |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| logEvent, | |
| } from '../../services/analytics/index.js' | |
| import { buildTool, type ToolDef } from '../../Tool.js' | |
| import { | |
| type GlobalConfig, | |
| getGlobalConfig, | |
| getRemoteControlAtStartup, | |
| saveGlobalConfig, | |
| } from '../../utils/config.js' | |
| import { errorMessage } from '../../utils/errors.js' | |
| import { lazySchema } from '../../utils/lazySchema.js' | |
| import { logError } from '../../utils/log.js' | |
| import { | |
| getInitialSettings, | |
| updateSettingsForSource, | |
| } from '../../utils/settings/settings.js' | |
| import { jsonStringify } from '../../utils/slowOperations.js' | |
| import { CONFIG_TOOL_NAME } from './constants.js' | |
| import { DESCRIPTION, generatePrompt } from './prompt.js' | |
| import { | |
| getConfig, | |
| getOptionsForSetting, | |
| getPath, | |
| isSupported, | |
| } from './supportedSettings.js' | |
| import { | |
| renderToolResultMessage, | |
| renderToolUseMessage, | |
| renderToolUseRejectedMessage, | |
| } from './UI.js' | |
| const inputSchema = lazySchema(() => | |
| z.strictObject({ | |
| setting: z | |
| .string() | |
| .describe( | |
| 'The setting key (e.g., "theme", "model", "permissions.defaultMode")', | |
| ), | |
| value: z | |
| .union([z.string(), z.boolean(), z.number()]) | |
| .optional() | |
| .describe('The new value. Omit to get current value.'), | |
| }), | |
| ) | |
| type InputSchema = ReturnType<typeof inputSchema> | |
| const outputSchema = lazySchema(() => | |
| z.object({ | |
| success: z.boolean(), | |
| operation: z.enum(['get', 'set']).optional(), | |
| setting: z.string().optional(), | |
| value: z.unknown().optional(), | |
| previousValue: z.unknown().optional(), | |
| newValue: z.unknown().optional(), | |
| error: z.string().optional(), | |
| }), | |
| ) | |
| type OutputSchema = ReturnType<typeof outputSchema> | |
| export type Input = z.infer<InputSchema> | |
| export type Output = z.infer<OutputSchema> | |
| export const ConfigTool = buildTool({ | |
| name: CONFIG_TOOL_NAME, | |
| searchHint: 'get or set Claude Code settings (theme, model)', | |
| maxResultSizeChars: 100_000, | |
| async description() { | |
| return DESCRIPTION | |
| }, | |
| async prompt() { | |
| return generatePrompt() | |
| }, | |
| get inputSchema(): InputSchema { | |
| return inputSchema() | |
| }, | |
| get outputSchema(): OutputSchema { | |
| return outputSchema() | |
| }, | |
| userFacingName() { | |
| return 'Config' | |
| }, | |
| shouldDefer: true, | |
| isConcurrencySafe() { | |
| return true | |
| }, | |
| isReadOnly(input: Input) { | |
| return input.value === undefined | |
| }, | |
| toAutoClassifierInput(input) { | |
| return input.value === undefined | |
| ? input.setting | |
| : `${input.setting} = ${input.value}` | |
| }, | |
| async checkPermissions(input: Input) { | |
| // Auto-allow reading configs | |
| if (input.value === undefined) { | |
| return { behavior: 'allow' as const, updatedInput: input } | |
| } | |
| return { | |
| behavior: 'ask' as const, | |
| message: `Set ${input.setting} to ${jsonStringify(input.value)}`, | |
| } | |
| }, | |
| renderToolUseMessage, | |
| renderToolResultMessage, | |
| renderToolUseRejectedMessage, | |
| async call({ setting, value }: Input, context): Promise<{ data: Output }> { | |
| // 1. Check if setting is supported | |
| // Voice settings are registered at build-time (feature('VOICE_MODE')), but | |
| // must also be gated at runtime. When the kill-switch is on, treat | |
| // voiceEnabled as an unknown setting so no voice-specific strings leak. | |
| if (feature('VOICE_MODE') && setting === 'voiceEnabled') { | |
| const { isVoiceGrowthBookEnabled } = await import( | |
| '../../voice/voiceModeEnabled.js' | |
| ) | |
| if (!isVoiceGrowthBookEnabled()) { | |
| return { | |
| data: { success: false, error: `Unknown setting: "${setting}"` }, | |
| } | |
| } | |
| } | |
| if (!isSupported(setting)) { | |
| return { | |
| data: { success: false, error: `Unknown setting: "${setting}"` }, | |
| } | |
| } | |
| const config = getConfig(setting)! | |
| const path = getPath(setting) | |
| // 2. GET operation | |
| if (value === undefined) { | |
| const currentValue = getValue(config.source, path) | |
| const displayValue = config.formatOnRead | |
| ? config.formatOnRead(currentValue) | |
| : currentValue | |
| return { | |
| data: { success: true, operation: 'get', setting, value: displayValue }, | |
| } | |
| } | |
| // 3. SET operation | |
| // Handle "default" — unset the config key so it falls back to the | |
| // platform-aware default (determined by the bridge feature gate). | |
| if ( | |
| setting === 'remoteControlAtStartup' && | |
| typeof value === 'string' && | |
| value.toLowerCase().trim() === 'default' | |
| ) { | |
| saveGlobalConfig(prev => { | |
| if (prev.remoteControlAtStartup === undefined) return prev | |
| const next = { ...prev } | |
| delete next.remoteControlAtStartup | |
| return next | |
| }) | |
| const resolved = getRemoteControlAtStartup() | |
| // Sync to AppState so useReplBridge reacts immediately | |
| context.setAppState(prev => { | |
| if (prev.replBridgeEnabled === resolved && !prev.replBridgeOutboundOnly) | |
| return prev | |
| return { | |
| ...prev, | |
| replBridgeEnabled: resolved, | |
| replBridgeOutboundOnly: false, | |
| } | |
| }) | |
| return { | |
| data: { | |
| success: true, | |
| operation: 'set', | |
| setting, | |
| value: resolved, | |
| }, | |
| } | |
| } | |
| let finalValue: unknown = value | |
| // Coerce and validate boolean values | |
| if (config.type === 'boolean') { | |
| if (typeof value === 'string') { | |
| const lower = value.toLowerCase().trim() | |
| if (lower === 'true') finalValue = true | |
| else if (lower === 'false') finalValue = false | |
| } | |
| if (typeof finalValue !== 'boolean') { | |
| return { | |
| data: { | |
| success: false, | |
| operation: 'set', | |
| setting, | |
| error: `${setting} requires true or false.`, | |
| }, | |
| } | |
| } | |
| } | |
| // Check options | |
| const options = getOptionsForSetting(setting) | |
| if (options && !options.includes(String(finalValue))) { | |
| return { | |
| data: { | |
| success: false, | |
| operation: 'set', | |
| setting, | |
| error: `Invalid value "${value}". Options: ${options.join(', ')}`, | |
| }, | |
| } | |
| } | |
| // Async validation (e.g., model API check) | |
| if (config.validateOnWrite) { | |
| const result = await config.validateOnWrite(finalValue) | |
| if (!result.valid) { | |
| return { | |
| data: { | |
| success: false, | |
| operation: 'set', | |
| setting, | |
| error: result.error, | |
| }, | |
| } | |
| } | |
| } | |
| // Pre-flight checks for voice mode | |
| if ( | |
| feature('VOICE_MODE') && | |
| setting === 'voiceEnabled' && | |
| finalValue === true | |
| ) { | |
| const { isVoiceModeEnabled } = await import( | |
| '../../voice/voiceModeEnabled.js' | |
| ) | |
| if (!isVoiceModeEnabled()) { | |
| const { isAnthropicAuthEnabled } = await import('../../utils/auth.js') | |
| return { | |
| data: { | |
| success: false, | |
| error: !isAnthropicAuthEnabled() | |
| ? 'Voice mode requires a Claude.ai account. Please run /login to sign in.' | |
| : 'Voice mode is not available.', | |
| }, | |
| } | |
| } | |
| const { isVoiceStreamAvailable } = await import( | |
| '../../services/voiceStreamSTT.js' | |
| ) | |
| const { | |
| checkRecordingAvailability, | |
| checkVoiceDependencies, | |
| requestMicrophonePermission, | |
| } = await import('../../services/voice.js') | |
| const recording = await checkRecordingAvailability() | |
| if (!recording.available) { | |
| return { | |
| data: { | |
| success: false, | |
| error: | |
| recording.reason ?? | |
| 'Voice mode is not available in this environment.', | |
| }, | |
| } | |
| } | |
| if (!isVoiceStreamAvailable()) { | |
| return { | |
| data: { | |
| success: false, | |
| error: | |
| 'Voice mode requires a Claude.ai account. Please run /login to sign in.', | |
| }, | |
| } | |
| } | |
| const deps = await checkVoiceDependencies() | |
| if (!deps.available) { | |
| return { | |
| data: { | |
| success: false, | |
| error: | |
| 'No audio recording tool found.' + | |
| (deps.installCommand ? ` Run: ${deps.installCommand}` : ''), | |
| }, | |
| } | |
| } | |
| if (!(await requestMicrophonePermission())) { | |
| let guidance: string | |
| if (process.platform === 'win32') { | |
| guidance = 'Settings \u2192 Privacy \u2192 Microphone' | |
| } else if (process.platform === 'linux') { | |
| guidance = "your system's audio settings" | |
| } else { | |
| guidance = | |
| 'System Settings \u2192 Privacy & Security \u2192 Microphone' | |
| } | |
| return { | |
| data: { | |
| success: false, | |
| error: `Microphone access is denied. To enable it, go to ${guidance}, then try again.`, | |
| }, | |
| } | |
| } | |
| } | |
| const previousValue = getValue(config.source, path) | |
| // 4. Write to storage | |
| try { | |
| if (config.source === 'global') { | |
| const key = path[0] | |
| if (!key) { | |
| return { | |
| data: { | |
| success: false, | |
| operation: 'set', | |
| setting, | |
| error: 'Invalid setting path', | |
| }, | |
| } | |
| } | |
| saveGlobalConfig(prev => { | |
| if (prev[key as keyof GlobalConfig] === finalValue) return prev | |
| return { ...prev, [key]: finalValue } | |
| }) | |
| } else { | |
| const update = buildNestedObject(path, finalValue) | |
| const result = updateSettingsForSource('userSettings', update) | |
| if (result.error) { | |
| return { | |
| data: { | |
| success: false, | |
| operation: 'set', | |
| setting, | |
| error: result.error.message, | |
| }, | |
| } | |
| } | |
| } | |
| // 5a. Voice needs notifyChange so applySettingsChange resyncs | |
| // AppState.settings (useVoiceEnabled reads settings.voiceEnabled) | |
| // and the settings cache resets for the next /voice read. | |
| if (feature('VOICE_MODE') && setting === 'voiceEnabled') { | |
| const { settingsChangeDetector } = await import( | |
| '../../utils/settings/changeDetector.js' | |
| ) | |
| settingsChangeDetector.notifyChange('userSettings') | |
| } | |
| // 5b. Sync to AppState if needed for immediate UI effect | |
| if (config.appStateKey) { | |
| const appKey = config.appStateKey | |
| context.setAppState(prev => { | |
| if (prev[appKey] === finalValue) return prev | |
| return { ...prev, [appKey]: finalValue } | |
| }) | |
| } | |
| // Sync remoteControlAtStartup to AppState so the bridge reacts | |
| // immediately (the config key differs from the AppState field name, | |
| // so the generic appStateKey mechanism can't handle this). | |
| if (setting === 'remoteControlAtStartup') { | |
| const resolved = getRemoteControlAtStartup() | |
| context.setAppState(prev => { | |
| if ( | |
| prev.replBridgeEnabled === resolved && | |
| !prev.replBridgeOutboundOnly | |
| ) | |
| return prev | |
| return { | |
| ...prev, | |
| replBridgeEnabled: resolved, | |
| replBridgeOutboundOnly: false, | |
| } | |
| }) | |
| } | |
| logEvent('tengu_config_tool_changed', { | |
| setting: | |
| setting as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| value: String( | |
| finalValue, | |
| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| }) | |
| return { | |
| data: { | |
| success: true, | |
| operation: 'set', | |
| setting, | |
| previousValue, | |
| newValue: finalValue, | |
| }, | |
| } | |
| } catch (error) { | |
| logError(error) | |
| return { | |
| data: { | |
| success: false, | |
| operation: 'set', | |
| setting, | |
| error: errorMessage(error), | |
| }, | |
| } | |
| } | |
| }, | |
| mapToolResultToToolResultBlockParam(content: Output, toolUseID: string) { | |
| if (content.success) { | |
| if (content.operation === 'get') { | |
| return { | |
| tool_use_id: toolUseID, | |
| type: 'tool_result' as const, | |
| content: `${content.setting} = ${jsonStringify(content.value)}`, | |
| } | |
| } | |
| return { | |
| tool_use_id: toolUseID, | |
| type: 'tool_result' as const, | |
| content: `Set ${content.setting} to ${jsonStringify(content.newValue)}`, | |
| } | |
| } | |
| return { | |
| tool_use_id: toolUseID, | |
| type: 'tool_result' as const, | |
| content: `Error: ${content.error}`, | |
| is_error: true, | |
| } | |
| }, | |
| } satisfies ToolDef<InputSchema, Output>) | |
| function getValue(source: 'global' | 'settings', path: string[]): unknown { | |
| if (source === 'global') { | |
| const config = getGlobalConfig() | |
| const key = path[0] | |
| if (!key) return undefined | |
| return config[key as keyof GlobalConfig] | |
| } | |
| const settings = getInitialSettings() | |
| let current: unknown = settings | |
| for (const key of path) { | |
| if (current && typeof current === 'object' && key in current) { | |
| current = (current as Record<string, unknown>)[key] | |
| } else { | |
| return undefined | |
| } | |
| } | |
| return current | |
| } | |
| function buildNestedObject( | |
| path: string[], | |
| value: unknown, | |
| ): Record<string, unknown> { | |
| if (path.length === 0) { | |
| return {} | |
| } | |
| const key = path[0]! | |
| if (path.length === 1) { | |
| return { [key]: value } | |
| } | |
| return { [key]: buildNestedObject(path.slice(1), value) } | |
| } | |