upgrade for win & remove some un-use code

This commit is contained in:
CrazyBoyM 2025-09-12 00:44:15 +08:00
parent 6bbaa6c559
commit d0d1dca009
44 changed files with 135 additions and 702 deletions

View File

@ -96,6 +96,18 @@ After installation, you can use any of these commands:
- `kwa` - Kode With Agent (alternative)
- `kd` - Ultra-short alias
### Windows Notes
- Install Git for Windows to provide a Bash (Unixlike) environment: https://git-scm.com/download/win
- Kode automatically prefers Git Bash/MSYS or WSL Bash when available.
- If neither is available, it will fall back to your default shell, but many features work best with Bash.
- Use VS Codes integrated terminal rather than legacy Command Prompt (cmd):
- Better font rendering and icon support.
- Fewer path and encoding quirks compared to cmd.
- Select “Git Bash” as the VS Code terminal shell when possible.
- Optional: If you install globally via npm, avoid spaces in the global prefix path to prevent shim issues.
- Example: `npm config set prefix "C:\\npm"` and reinstall global packages.
## Usage
### Interactive Mode

View File

@ -50,6 +50,17 @@ npm install -g @shareai-lab/kode
- `kwa` - Kode With Agent备选
- `kd` - 超短别名
### Windows 提示
- 请安装 Git for Windows包含 Git Bash 类 Unix 终端https://git-scm.com/download/win
- Kode 会优先使用 Git Bash/MSYS 或 WSL Bash没有时会回退到默认终端但在 Bash 下体验更佳。
- 推荐在 VS Code 的集成终端中运行(而非系统默认的 cmd
- 字体与图标显示更稳定UI 体验更好。
- 相比 cmd 路径/编码等兼容性问题更少。
- 在 VS Code 终端中选择 “Git Bash” 作为默认 Shell。
- 可选:若通过 npm 全局安装,建议避免将 npm 全局 prefix 设置在含空格的路径,以免生成的可执行 shim 出现路径解析问题。
- 示例:`npm config set prefix "C:\\npm"`,然后重新安装全局包。
## 使用方法
### 交互模式

View File

@ -51,8 +51,6 @@
"@commander-js/extra-typings": "^13.1.0",
"@inkjs/ui": "^2.0.0",
"@modelcontextprotocol/sdk": "^1.15.1",
"@statsig/client-core": "^3.18.2",
"@statsig/js-client": "^3.18.2",
"@types/lodash-es": "^4.17.12",
"@types/react": "^19.1.8",
"ansi-escapes": "^7.0.0",

View File

@ -11,7 +11,6 @@ import { getGitState, getIsGit, GitRepoState } from '../utils/git'
import { useTerminalSize } from '../hooks/useTerminalSize'
import { getAnthropicApiKey, getGlobalConfig } from '../utils/config'
import { USER_AGENT } from '../utils/http'
import { logEvent } from '../services/statsig'
import { PRODUCT_NAME } from '../constants/product'
import { API_ERROR_MESSAGE_PREFIX, queryQuick } from '../services/claude'
import { openBrowser } from '../utils/browser'

View File

@ -3,7 +3,6 @@ import { Static, Box, Text, useInput } from 'ink'
import TextInput from './TextInput'
import { OAuthService, createAndStoreApiKey } from '../services/oauth'
import { getTheme } from '../utils/theme'
import { logEvent } from '../services/statsig'
import { AsciiLogo } from './AsciiLogo'
import { useTerminalSize } from '../hooks/useTerminalSize'
import { logError } from '../utils/log'
@ -70,10 +69,10 @@ export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
useInput(async (_, key) => {
if (key.return) {
if (oauthStatus.state === 'idle') {
logEvent('tengu_oauth_start', {})
setOAuthStatus({ state: 'ready_to_start' })
} else if (oauthStatus.state === 'success') {
logEvent('tengu_oauth_success', {})
await clearTerminal() // needed to clear out Static components
onDone()
} else if (oauthStatus.state === 'error' && oauthStatus.toRetry) {
@ -101,7 +100,7 @@ export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
}
// Track which path the user is taking (manual code entry)
logEvent('tengu_oauth_manual_entry', {})
oauthService.processCallback({
authorizationCode,
state,
@ -133,7 +132,7 @@ export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
'Failed to exchange authorization code for access token. Please try again.',
toRetry: { state: 'ready_to_start' },
})
logEvent('tengu_oauth_token_exchange_error', { error: err.message })
} else {
// Handle other errors
setOAuthStatus({
@ -154,7 +153,7 @@ export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
message: 'Failed to create API key: ' + err.message,
toRetry: { state: 'ready_to_start' },
})
logEvent('tengu_oauth_api_key_error', { error: err.message })
throw err
},
)
@ -169,13 +168,10 @@ export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
"Unable to create API key. The server accepted the request but didn't return a key.",
toRetry: { state: 'ready_to_start' },
})
logEvent('tengu_oauth_api_key_error', {
error: 'server_returned_no_key',
})
}
} catch (err) {
const errorMessage = (err as Error).message
logEvent('tengu_oauth_error', { error: errorMessage })
}
}, [oauthService, setShowPastePrompt])

View File

@ -12,7 +12,6 @@ import {
isNotEmptyMessage,
normalizeMessages,
} from '../utils/messages.js'
import { logEvent } from '../services/statsig'
import type { AssistantMessage, UserMessage } from '../query'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
@ -37,23 +36,14 @@ export function MessageSelector({
}: Props): React.ReactNode {
const currentUUID = useMemo(randomUUID, [])
// Log when selector is opened
useEffect(() => {
logEvent('tengu_message_selector_opened', {})
}, [])
useEffect(() => {}, [])
function handleSelect(message: UserMessage) {
const indexFromEnd = messages.length - 1 - messages.indexOf(message)
logEvent('tengu_message_selector_selected', {
index_from_end: indexFromEnd.toString(),
message_type: message.type,
is_current_prompt: (message.uuid === currentUUID).toString(),
})
onSelect(message)
}
function handleEscape() {
logEvent('tengu_message_selector_cancelled', {})
onEscape()
}

View File

@ -7,7 +7,6 @@ import {
getCurrentProjectConfig,
} from '../utils/config.js'
import { PRODUCT_NAME } from '../constants/product'
import { logEvent } from '../services/statsig'
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
import { homedir } from 'os'
import { getCwd } from '../utils/state'
@ -19,20 +18,13 @@ type Props = {
export function TrustDialog({ onDone }: Props): React.ReactNode {
const theme = getTheme()
React.useEffect(() => {
// Log when dialog is shown
logEvent('trust_dialog_shown', {})
}, [])
React.useEffect(() => {}, [])
function onChange(value: 'yes' | 'no') {
const config = getCurrentProjectConfig()
switch (value) {
case 'yes': {
// Log when user accepts
const isHomeDir = homedir() === getCwd()
logEvent('trust_dialog_accept', {
isHomeDir: String(isHomeDir),
})
if (!isHomeDir) {
saveCurrentProjectConfig({

View File

@ -1,7 +1,6 @@
import { TextBlock, ToolUseBlock } from '@anthropic-ai/sdk/resources/index.mjs'
import { AssistantMessage, BinaryFeedbackResult } from '../../query'
import { MAIN_QUERY_TEMPERATURE } from '../../services/claude'
import { getDynamicConfig, logEvent } from '../../services/statsig'
import { isEqual, zip } from 'lodash-es'
import { getGitState } from '../../utils/git'
@ -19,9 +18,8 @@ type BinaryFeedbackConfig = {
}
async function getBinaryFeedbackStatsigConfig(): Promise<BinaryFeedbackConfig> {
return await getDynamicConfig('tengu-binary-feedback-config', {
sampleFrequency: 0,
})
return { sampleFrequency: 0 }
}
function getMessageBlockSequence(m: AssistantMessage) {
@ -40,38 +38,14 @@ export async function logBinaryFeedbackEvent(
const modelA = m1.message.model
const modelB = m2.message.model
const gitState = await getGitState()
logEvent('tengu_binary_feedback', {
msg_id_A: m1.message.id,
msg_id_B: m2.message.id,
choice: {
'prefer-left': m1.message.id,
'prefer-right': m2.message.id,
neither: undefined,
'no-preference': undefined,
}[choice],
choiceStr: choice,
gitHead: gitState?.commitHash,
gitBranch: gitState?.branchName,
gitRepoRemoteUrl: gitState?.remoteUrl || undefined,
gitRepoIsHeadOnRemote: gitState?.isHeadOnRemote?.toString(),
gitRepoIsClean: gitState?.isClean?.toString(),
modelA,
modelB,
temperatureA: String(MAIN_QUERY_TEMPERATURE),
temperatureB: String(MAIN_QUERY_TEMPERATURE),
seqA: String(getMessageBlockSequence(m1)),
seqB: String(getMessageBlockSequence(m2)),
})
}
export async function logBinaryFeedbackSamplingDecision(
decision: boolean,
reason?: string,
): Promise<void> {
logEvent('tengu_binary_feedback_sampling_decision', {
decision: decision.toString(),
reason,
})
}
export async function logBinaryFeedbackDisplayDecision(
@ -80,14 +54,7 @@ export async function logBinaryFeedbackDisplayDecision(
m2: AssistantMessage,
reason?: string,
): Promise<void> {
logEvent('tengu_binary_feedback_display_decision', {
decision: decision.toString(),
reason,
msg_id_A: m1.message.id,
msg_id_B: m2.message.id,
seqA: String(getMessageBlockSequence(m1)),
seqB: String(getMessageBlockSequence(m2)),
})
}
function textContentBlocksEqual(cb1: TextBlock, cb2: TextBlock): boolean {

View File

@ -4,7 +4,6 @@ import { useMemo } from 'react'
import { Tool } from '../../../Tool'
import { GlobTool } from '../../../tools/GlobTool/GlobTool'
import { GrepTool } from '../../../tools/GrepTool/GrepTool'
import { logEvent } from '../../../services/statsig'
function getToolUseFromMessages(
toolUseID: string,
@ -46,7 +45,7 @@ export function useGetToolFromMessages(
_ => _.name === toolUse.name,
)
if (tool === GlobTool || tool === GrepTool) {
logEvent('tengu_legacy_tool_lookup', {})
}
if (!tool) {
throw new ReferenceError(`Tool not found for ${toolUse.name}`)

View File

@ -2,7 +2,7 @@ import { useEffect } from 'react'
import { logUnaryEvent, CompletionType } from '../../utils/unaryLogging'
import { ToolUseConfirm } from '../../components/permissions/PermissionRequest'
import { env } from '../../utils/env'
import { logEvent } from '../../services/statsig'
type UnaryEventType = {
completion_type: CompletionType
@ -19,11 +19,7 @@ export function usePermissionRequestLogging(
unaryEvent: UnaryEventType,
): void {
useEffect(() => {
// Log Statsig event
logEvent('tengu_tool_use_show_permission_request', {
messageID: toolUseConfirm.assistantMessage.message.id,
toolName: toolUseConfirm.tool.name,
})
// Handle string or Promise language name
const languagePromise = Promise.resolve(unaryEvent.language_name)

View File

@ -1,5 +0,0 @@
export const GATE_TOKEN_EFFICIENT_TOOLS = 'tengu-token-efficient-tools'
export const BETA_HEADER_TOKEN_EFFICIENT_TOOLS =
'token-efficient-tools-2024-12-11'
export const GATE_USE_EXTERNAL_UPDATER = 'tengu-use-external-updater'
export const CLAUDE_CODE_20250219_BETA_HEADER = 'claude-code-20250219'

View File

@ -1,3 +0,0 @@
export const SENTRY_DSN = ''
export const STATSIG_CLIENT_KEY = ''

View File

@ -89,7 +89,7 @@ import {
ensureConfigScope,
} from '../services/mcpClient'
import { handleMcprcServerApprovals } from '../services/mcpServerApproval'
import { checkGate, initializeStatsig, logEvent } from '../services/statsig'
import { getExampleCommands } from '../utils/exampleCommands'
import { cursorShow } from 'ansi-escapes'
import { getLatestVersion, assertMinVersion, getUpdateCommandSuggestions } from '../utils/autoUpdater'
@ -97,7 +97,7 @@ import { gt } from 'semver'
import { CACHE_PATHS } from '../utils/log'
// import { checkAndNotifyUpdate } from '../utils/autoUpdater'
import { PersistentShell } from '../utils/PersistentShell'
import { GATE_USE_EXTERNAL_UPDATER } from '../constants/betas'
// Vendor beta gates removed
import { clearTerminal } from '../utils/terminal'
import { showInvalidConfigDialog } from '../components/InvalidConfigDialog'
import { ConfigParseError } from '../utils/errors'
@ -269,12 +269,7 @@ async function setup(cwd: string, safeMode?: boolean): Promise<void> {
projectConfig.lastCost !== undefined &&
projectConfig.lastDuration !== undefined
) {
logEvent('tengu_exit', {
last_session_cost: String(projectConfig.lastCost),
last_session_api_duration: String(projectConfig.lastAPIDuration),
last_session_duration: String(projectConfig.lastDuration),
last_session_id: projectConfig.lastSessionId,
})
// Clear the values after logging
// saveCurrentProjectConfig({
// ...projectConfig,
@ -321,9 +316,7 @@ async function main() {
let renderContext: RenderOptions | undefined = {
exitOnCtrlC: false,
onFlicker() {
logEvent('tengu_flicker', {})
},
onFlicker() {},
} as any
if (
@ -400,15 +393,7 @@ ${commandList}`,
.action(
async (prompt, { cwd, debug, verbose, enableArchitect, print, safe }) => {
await showSetupScreens(safe, print)
logEvent('tengu_init', {
entrypoint: PRODUCT_COMMAND,
hasInitialPrompt: Boolean(prompt).toString(),
hasStdin: Boolean(stdinContent).toString(),
enableArchitect: enableArchitect?.toString() ?? 'false',
verbose: verbose?.toString() ?? 'false',
debug: debug?.toString() ?? 'false',
print: print?.toString() ?? 'false',
})
await setup(cwd, safe)
assertMinVersion()
@ -574,10 +559,6 @@ ${commandList}`,
.description('Remove a tool from the list of approved tools')
.action(async (tool: string) => {
const result = handleRemoveApprovedTool(tool)
logEvent('tengu_approved_tool_remove', {
tool,
success: String(result.success),
})
console.log(result.message)
process.exit(result.success ? 0 : 1)
})
@ -593,7 +574,6 @@ ${commandList}`,
.description(`Start the ${PRODUCT_NAME} MCP server`)
.action(async () => {
const providedCwd = (program.opts() as { cwd?: string }).cwd ?? cwd()
logEvent('tengu_mcp_start', { providedCwd })
// Verify the directory exists
if (!existsSync(providedCwd)) {
@ -621,7 +601,6 @@ ${commandList}`,
.action(async (name, url, options) => {
try {
const scope = ensureConfigScope(options.scope)
logEvent('tengu_mcp_add', { name, type: 'sse', scope })
addMcpServer(name, { type: 'sse', url }, scope)
console.log(
@ -717,11 +696,7 @@ ${commandList}`,
// Add the server
if (type === 'sse') {
logEvent('tengu_mcp_add', {
name: serverName,
type: 'sse',
scope: serverScope,
})
addMcpServer(
serverName,
{ type: 'sse', url: commandOrUrlValue },
@ -731,11 +706,7 @@ ${commandList}`,
`Added SSE MCP server ${serverName} with URL ${commandOrUrlValue} to ${serverScope} config`,
)
} else {
logEvent('tengu_mcp_add', {
name: serverName,
type: 'stdio',
scope: serverScope,
})
addMcpServer(
serverName,
{
@ -757,13 +728,13 @@ ${commandList}`,
// Check if it's an SSE URL (starts with http:// or https://)
if (commandOrUrl.match(/^https?:\/\//)) {
logEvent('tengu_mcp_add', { name, type: 'sse', scope })
addMcpServer(name, { type: 'sse', url: commandOrUrl }, scope)
console.log(
`Added SSE MCP server ${name} with URL ${commandOrUrl} to ${scope} config`,
)
} else {
logEvent('tengu_mcp_add', { name, type: 'stdio', scope })
const env = parseEnvVars(options.env)
addMcpServer(
name,
@ -799,7 +770,7 @@ ${commandList}`,
.action(async (name: string, options: { scope?: string }) => {
try {
const scope = ensureConfigScope(options.scope)
logEvent('tengu_mcp_delete', { name, scope })
removeMcpServer(name, scope)
console.log(`Removed MCP server ${name} from ${scope} config`)
@ -814,7 +785,6 @@ ${commandList}`,
.command('list')
.description('List configured MCP servers')
.action(() => {
logEvent('tengu_mcp_list', {})
const servers = listMCPServers()
if (Object.keys(servers).length === 0) {
console.log(
@ -873,7 +843,7 @@ ${commandList}`,
}
// Add server with the provided config
logEvent('tengu_mcp_add_json', { name, type: serverConfig.type, scope })
addMcpServer(name, serverConfig, scope)
if (serverConfig.type === 'sse') {
@ -899,7 +869,7 @@ ${commandList}`,
.command('get <name>')
.description('Get details about an MCP server')
.action((name: string) => {
logEvent('tengu_mcp_get', { name })
const server = getMcpServer(name)
if (!server) {
console.error(`No MCP server found with name: ${name}`)
@ -1227,7 +1197,7 @@ ${commandList}`,
'Reset all approved and rejected project-scoped (.mcp.json) servers within this project',
)
.action(() => {
logEvent('tengu_mcp_reset_project_choices', {})
resetMcpChoices()
})
@ -1239,7 +1209,7 @@ ${commandList}`,
'Reset all approved and rejected .mcprc servers for this project',
)
.action(() => {
logEvent('tengu_mcp_reset_mcprc_choices', {})
resetMcpChoices()
})
}
@ -1249,7 +1219,7 @@ ${commandList}`,
.command('doctor')
.description(`Check the health of your ${PRODUCT_NAME} installation`)
.action(async () => {
logEvent('tengu_doctor_command', {})
await new Promise<void>(resolve => {
;(async () => {
@ -1267,7 +1237,7 @@ ${commandList}`,
.command('update')
.description('Show manual upgrade commands (no auto-install)')
.action(async () => {
logEvent('tengu_update_check', {})
console.log(`Current version: ${MACRO.VERSION}`)
console.log('Checking for updates...')
@ -1306,7 +1276,7 @@ ${commandList}`,
.option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
.action(async (number, { cwd }) => {
await setup(cwd, false)
logEvent('tengu_view_logs', { number: number?.toString() ?? '' })
const context: { unmount?: () => void } = {}
;(async () => {
const { render } = await import('ink')
@ -1358,7 +1328,7 @@ ${commandList}`,
let messages, date, forkNumber
try {
if (isNumber) {
logEvent('tengu_resume', { number: number.toString() })
const log = logs[number]
if (!log) {
console.error('No conversation found at index', number)
@ -1368,7 +1338,7 @@ ${commandList}`,
;({ date, forkNumber } = log)
} else {
// Handle file path case
logEvent('tengu_resume', { filePath: identifier })
if (!existsSync(identifier)) {
console.error('File does not exist:', identifier)
process.exit(1)
@ -1438,7 +1408,7 @@ ${commandList}`,
.option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
.action(async (number, { cwd }) => {
await setup(cwd, false)
logEvent('tengu_view_errors', { number: number?.toString() ?? '' })
const context: { unmount?: () => void } = {}
;(async () => {
const { render } = await import('ink')
@ -1463,7 +1433,7 @@ ${commandList}`,
.description('Get a value from context')
.action(async (key, { cwd }) => {
await setup(cwd, false)
logEvent('tengu_context_get', { key })
const context = omit(
await getContext(),
'codeStyle',
@ -1479,7 +1449,7 @@ ${commandList}`,
.option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
.action(async (key, value, { cwd }) => {
await setup(cwd, false)
logEvent('tengu_context_set', { key })
setContext(key, value)
console.log(`Set context.${key} to "${value}"`)
process.exit(0)
@ -1491,7 +1461,7 @@ ${commandList}`,
.option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
.action(async ({ cwd }) => {
await setup(cwd, false)
logEvent('tengu_context_list', {})
const context = omit(
await getContext(),
'codeStyle',
@ -1508,7 +1478,7 @@ ${commandList}`,
.option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
.action(async (key, { cwd }) => {
await setup(cwd, false)
logEvent('tengu_context_delete', { key })
removeContext(key)
console.log(`Removed context.${key}`)
process.exit(0)

View File

@ -1,6 +1,5 @@
import React, { useCallback } from 'react'
import { hasPermissionsToUseTool } from '../permissions'
import { logEvent } from '../services/statsig'
import { BashTool, inputSchema } from '../tools/BashTool/BashTool'
import { getCommandSubcommandPrefix } from '../utils/commands'
import { REJECT_MESSAGE } from '../utils/messages'
@ -25,12 +24,7 @@ function useCanUseTool(
return useCallback<CanUseToolFn>(
async (tool, input, toolUseContext, assistantMessage) => {
return new Promise(resolve => {
function logCancelledEvent() {
logEvent('tengu_tool_use_cancelled', {
messageID: assistantMessage.message.id,
toolName: tool.name,
})
}
function logCancelledEvent() {}
function resolveWithCancelledAndAbortAllToolCalls() {
resolve({
@ -58,10 +52,7 @@ function useCanUseTool(
.then(async result => {
// Has permissions to use tool, granted in config
if (result.result) {
logEvent('tengu_tool_use_granted_in_config', {
messageID: assistantMessage.message.id,
toolName: tool.name,
})
resolve({ result: true })
return
}
@ -92,31 +83,15 @@ function useCanUseTool(
riskScore: null,
onAbort() {
logCancelledEvent()
logEvent('tengu_tool_use_rejected_in_prompt', {
messageID: assistantMessage.message.id,
toolName: tool.name,
})
resolveWithCancelledAndAbortAllToolCalls()
},
onAllow(type) {
if (type === 'permanent') {
logEvent('tengu_tool_use_granted_in_prompt_permanent', {
messageID: assistantMessage.message.id,
toolName: tool.name,
})
} else {
logEvent('tengu_tool_use_granted_in_prompt_temporary', {
messageID: assistantMessage.message.id,
toolName: tool.name,
})
}
resolve({ result: true })
},
onReject() {
logEvent('tengu_tool_use_rejected_in_prompt', {
messageID: assistantMessage.message.id,
toolName: tool.name,
})
resolveWithCancelledAndAbortAllToolCalls()
},
})

View File

@ -1,6 +1,5 @@
import { useInput } from 'ink'
import { ToolUseConfirm } from '../components/permissions/PermissionRequest'
import { logEvent } from '../services/statsig'
import { BinaryFeedbackContext } from '../screens/REPL'
import type { SetToolJSXFn } from '../Tool'
@ -30,7 +29,7 @@ export function useCancelRequest(
// Esc closes the message selector
return
}
logEvent('tengu_cancel', {})
setToolJSX(null)
setToolUseConfirm(null)
setBinaryFeedbackContext(null)

View File

@ -1,12 +1,8 @@
import { useEffect } from 'react'
import { logEvent } from '../services/statsig'
export function useLogStartupTime(): void {
useEffect(() => {
const startupTimeMs = Math.round(process.uptime() * 1000)
logEvent('tengu_timer', {
event: 'startup',
durationMs: String(startupTimeMs),
})
}, [])
}

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react'
import { logEvent } from '../services/statsig'
import { logUnaryEvent, CompletionType } from '../utils/unaryLogging'
import { ToolUseConfirm } from '../components/permissions/PermissionRequest'
import { env } from '../utils/env'
@ -19,11 +19,7 @@ export function usePermissionRequestLogging(
unaryEvent: UnaryEvent,
): void {
useEffect(() => {
// Log Statsig event
logEvent('tengu_tool_use_show_permission_request', {
messageID: toolUseConfirm.assistantMessage.message.id,
toolName: toolUseConfirm.tool.name,
})
// Handle string or Promise language name
const languagePromise = Promise.resolve(unaryEvent.language_name)

View File

@ -16,7 +16,6 @@ import {
queryModel,
} from './services/claude.js'
import { emitReminderEvent } from './services/systemReminder'
import { logEvent } from './services/statsig'
import { all } from './utils/generators'
import { logError } from './utils/log'
import {
@ -415,10 +414,7 @@ export async function* runToolUse(
)
logEvent('tengu_tool_use_start', {
toolName: toolUse.name,
toolUseID: toolUse.id,
})
const toolName = toolUse.name
const tool = toolUseContext.options.tools.find(t => t.name === toolName)
@ -432,12 +428,7 @@ export async function* runToolUse(
requestId: currentRequest?.id,
})
logEvent('tengu_tool_use_error', {
error: `No such tool available: ${toolName}`,
messageID: assistantMessage.message.id,
toolName,
toolUseID: toolUse.id,
})
yield createUserMessage([
{
@ -469,10 +460,7 @@ export async function* runToolUse(
requestId: currentRequest?.id,
})
logEvent('tengu_tool_use_cancelled', {
toolName: tool.name,
toolUseID: toolUse.id,
})
const message = createUserMessage([
createToolResultStopMessage(toolUse.id),
@ -579,12 +567,7 @@ async function* checkPermissionsAndCallTool(
errorMessage = `Error: The View tool requires a 'file_path' parameter to specify which file to read. Please provide the absolute path to the file you want to view. For example: {"file_path": "/path/to/file.txt"}`
}
logEvent('tengu_tool_use_error', {
error: errorMessage,
messageID: assistantMessage.message.id,
toolName: tool.name,
toolInput: JSON.stringify(input).slice(0, 200),
})
yield createUserMessage([
{
type: 'tool_result',
@ -604,13 +587,6 @@ async function* checkPermissionsAndCallTool(
context,
)
if (isValidCall?.result === false) {
logEvent('tengu_tool_use_error', {
error: isValidCall?.message.slice(0, 2000),
messageID: assistantMessage.message.id,
toolName: tool.name,
toolInput: JSON.stringify(input).slice(0, 200),
...(isValidCall?.meta ?? {}),
})
yield createUserMessage([
{
type: 'tool_result',
@ -645,10 +621,7 @@ async function* checkPermissionsAndCallTool(
for await (const result of generator) {
switch (result.type) {
case 'result':
logEvent('tengu_tool_use_success', {
messageID: assistantMessage.message.id,
toolName: tool.name,
})
yield createUserMessage(
[
{
@ -664,10 +637,7 @@ async function* checkPermissionsAndCallTool(
)
return
case 'progress':
logEvent('tengu_tool_use_progress', {
messageID: assistantMessage.message.id,
toolName: tool.name,
})
yield createProgressMessage(
toolUseID,
siblingToolUseIDs,
@ -681,12 +651,7 @@ async function* checkPermissionsAndCallTool(
} catch (error) {
const content = formatError(error)
logError(error)
logEvent('tengu_tool_use_error', {
error: content.slice(0, 2000),
messageID: assistantMessage.message.id,
toolName: tool.name,
toolInput: JSON.stringify(input).slice(0, 1000),
})
yield createUserMessage([
{
type: 'tool_result',

View File

@ -45,7 +45,6 @@ import type { Tool } from '../Tool'
// Auto-updater removed; only show a new version banner passed from CLI
import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
import { MACRO } from '../constants/macros'
import { logEvent } from '../services/statsig'
import { getNextAvailableLogForkNumber } from '../utils/log'
import {
getErroredToolUseMessages,
@ -214,7 +213,7 @@ export function REPL({
useEffect(() => {
const totalCost = getTotalCost()
if (totalCost >= 5 /* $5 */ && !showCostDialog && !haveShownCostDialog) {
logEvent('tengu_cost_threshold_reached', {})
setShowCostDialog(true)
}
}, [messages, showCostDialog, haveShownCostDialog])
@ -681,7 +680,7 @@ export function REPL({
...projectConfig,
hasAcknowledgedCostThreshold: true,
})
logEvent('tengu_cost_threshold_acknowledged', {})
}}
/>
)}

View File

@ -25,7 +25,6 @@ import {
normalizeContentFromAPI,
} from '../utils/messages'
import { countTokens } from '../utils/tokens'
import { logEvent } from './statsig'
import { withVCR } from './vcr'
import {
debug as debugLogger,
@ -333,13 +332,7 @@ async function withRetry<T>(
`${chalk.red(`API ${error.name} (${error.message}) · Retrying in ${Math.round(delayMs / 1000)} seconds… (attempt ${attempt}/${maxRetries})`)}`,
)
logEvent('tengu_api_retry', {
attempt: String(attempt),
delayMs: String(delayMs),
error: error.message,
status: String(error.status),
provider: USE_BEDROCK ? 'bedrock' : USE_VERTEX ? 'vertex' : '1p',
})
try {
await abortableDelay(delayMs, options.signal)
@ -770,9 +763,6 @@ function convertOpenAIResponseToAnthropic(response: OpenAI.ChatCompletion, tools
let contentBlocks: ContentBlock[] = []
const message = response.choices?.[0]?.message
if (!message) {
logEvent('weird_response', {
response: JSON.stringify(response),
})
return {
role: 'assistant',
content: [],
@ -1426,13 +1416,6 @@ async function queryAnthropicNative(
if (options?.prependCLISysprompt) {
// Log stats about first block for analyzing prefix matching config
const [firstSyspromptBlock] = splitSysPromptPrefix(systemPrompt)
logEvent('tengu_sysprompt_block', {
snippet: firstSyspromptBlock?.slice(0, 20),
length: String(firstSyspromptBlock?.length ?? 0),
hash: firstSyspromptBlock
? createHash('sha256').update(firstSyspromptBlock).digest('hex')
: '',
})
systemPrompt = [getCLISyspromptPrefix(), ...systemPrompt]
}
@ -1750,17 +1733,7 @@ async function queryAnthropicNative(
assistantMessage.costUSD = costUSD
addToTotalCost(costUSD, durationMs)
logEvent('api_response_anthropic_native', {
model,
input_tokens: String(inputTokens),
output_tokens: String(outputTokens),
cache_creation_input_tokens: String(cacheCreationInputTokens),
cache_read_input_tokens: String(cacheReadInputTokens),
cost_usd: String(costUSD),
duration_ms: String(durationMs),
ttft_ms: String(ttftMs),
attempt_number: String(attemptNumber),
})
return assistantMessage
} catch (error) {
@ -1850,13 +1823,6 @@ async function queryOpenAI(
if (options?.prependCLISysprompt) {
// Log stats about first block for analyzing prefix matching config (see https://console.statsig.com/4aF3Ewatb6xPVpCwxb5nA3/dynamic_configs/claude_cli_system_prompt_prefixes)
const [firstSyspromptBlock] = splitSysPromptPrefix(systemPrompt)
logEvent('tengu_sysprompt_block', {
snippet: firstSyspromptBlock?.slice(0, 20),
length: String(firstSyspromptBlock?.length ?? 0),
hash: firstSyspromptBlock
? createHash('sha256').update(firstSyspromptBlock).digest('hex')
: '',
})
systemPrompt = [getCLISyspromptPrefix() + systemPrompt] // some openai-like providers need the entire system prompt as a single block
}
@ -1943,9 +1909,6 @@ async function queryOpenAI(
}
const reasoningEffort = await getReasoningEffort(modelProfile, messages)
if (reasoningEffort) {
logEvent('debug_reasoning_effort', {
effort: reasoningEffort,
})
opts.reasoning_effort = reasoningEffort
}

View File

@ -5,7 +5,6 @@ import { memoize } from 'lodash-es'
import type { MessageParam } from '@anthropic-ai/sdk/resources/index.mjs'
import type { Command } from '../commands'
import { getCwd } from '../utils/state'
import { logEvent } from './statsig'
import { execFile } from 'child_process'
import { promisify } from 'util'
@ -559,12 +558,7 @@ export const loadCustomCommands = memoize(
// Log performance metrics for monitoring
// This follows the same pattern as other performance-sensitive operations
logEvent('tengu_custom_command_scan', {
durationMs: duration.toString(),
projectFilesFound: projectFiles.length.toString(),
userFilesFound: userFiles.length.toString(),
totalFiles: allFiles.length.toString(),
})
// Parse files and create command objects
const commands: CustomCommandWithScope[] = []
@ -619,12 +613,7 @@ export const loadCustomCommands = memoize(
const enabledCommands = commands.filter(cmd => cmd.isEnabled)
// Log loading results for debugging and monitoring
logEvent('tengu_custom_commands_loaded', {
totalCommands: commands.length.toString(),
enabledCommands: enabledCommands.length.toString(),
userCommands: commands.filter(cmd => cmd.scope === 'user').length.toString(),
projectCommands: commands.filter(cmd => cmd.scope === 'project').length.toString(),
})
return enabledCommands
} catch (error) {

View File

@ -4,7 +4,6 @@ import {
systemReminderService,
} from '../services/systemReminder'
import { getAgentFilePath } from '../utils/agentStorage'
import { logEvent } from '../services/statsig'
interface FileTimestamp {
path: string
@ -44,11 +43,7 @@ class FileFreshnessService {
// Reset session state on startup
this.resetSession()
// Log session startup
logEvent('file_freshness_session_startup', {
agentId: context.agentId || 'default',
timestamp: context.timestamp,
})
},
)
}

View File

@ -36,7 +36,6 @@ import type { Tool } from '../Tool'
import { MCPTool } from '../tools/MCPTool/MCPTool'
import { logMCPError } from '../utils/log'
import { Command } from '../commands'
import { logEvent } from '../services/statsig'
import { PRODUCT_COMMAND } from '../constants/product.js'
type McpName = string
@ -332,10 +331,8 @@ export const getClients = memoize(async (): Promise<WrappedClient[]> => {
Object.entries(allServers).map(async ([name, serverRef]) => {
try {
const client = await connectToServer(name, serverRef as McpServerConfig)
logEvent('tengu_mcp_server_connection_succeeded', {})
return { name, client, type: 'connected' as const }
} catch (error) {
logEvent('tengu_mcp_server_connection_failed', {})
logMCPError(
name,
`Connection failed: ${error instanceof Error ? error.message : String(error)}`,

View File

@ -5,7 +5,6 @@ import * as url from 'url'
import { OAUTH_CONFIG } from '../constants/oauth'
import { openBrowser } from '../utils/browser'
import { logEvent } from '../services/statsig'
import { logError } from '../utils/log'
import { resetAnthropicClient } from './claude'
import {
@ -180,8 +179,7 @@ export class OAuthService {
})
res.end()
// Track which path the user is taking (automatic browser redirect)
logEvent('tengu_oauth_automatic_redirect', {})
this.processCallback({
authorizationCode,
@ -309,11 +307,7 @@ export async function createAndStoreApiKey(
errorText = await createApiKeyResp.text()
}
logEvent('tengu_oauth_api_key', {
status: createApiKeyResp.ok ? 'success' : 'failure',
statusCode: createApiKeyResp.status.toString(),
error: createApiKeyResp.ok ? '' : errorText || JSON.stringify(apiKeyData),
})
if (createApiKeyResp.ok && apiKeyData && apiKeyData.raw_key) {
const apiKey = apiKeyData.raw_key
@ -347,11 +341,7 @@ export async function createAndStoreApiKey(
return null
} catch (error) {
logEvent('tengu_oauth_api_key', {
status: 'failure',
statusCode: 'exception',
error: error instanceof Error ? error.message : String(error),
})
throw error
}
}

View File

@ -2,7 +2,6 @@ import { OpenAI } from 'openai'
import { getGlobalConfig, GlobalConfig } from '../utils/config'
import { ProxyAgent, fetch, Response } from 'undici'
import { setSessionState, getSessionState } from '../utils/sessionState'
import { logEvent } from '../services/statsig'
import { debug as debugLogger, getCurrentRequest, logAPIError } from '../utils/debugLogger'
/**
@ -169,12 +168,7 @@ const ERROR_HANDLERS: ErrorHandler[] = [
remainder += line + '\n'
}
}
logEvent('truncated_tool_description', {
name: tool.function.name,
original_length: String(tool.function.description.length),
truncated_length: String(str.length),
remainder_length: String(remainder.length),
})
tool.function.description = str
toolDescriptions[tool.function.name] = remainder
}

View File

@ -1,95 +0,0 @@
import { memoize } from 'lodash-es'
import chalk from 'chalk'
// Statsig is disabled by default in the CLI runtime to avoid
// bringing browser-only globals (e.g., XMLHttpRequest) into Node.
// The Client SDK is browser-oriented; using it at module scope can
// break on Windows shells. We keep a lightweight no-op shim.
import { env } from '../utils/env'
const gateValues: Record<string, boolean> = {}
let client: any | null = null
export const initializeStatsig = memoize(
async (): Promise<any | null> => {
// Fully disabled in CLI by default
return null
},
)
export function logEvent(
eventName: string,
metadata: { [key: string]: string | undefined },
): void {
// console.log('logEvent', eventName, metadata)
if (env.isCI || process.env.NODE_ENV === 'test') {
return
}
// Keep debug line for local visibility, but do not import client SDK
if (process.argv.includes('--debug') || process.argv.includes('-d')) {
console.log(chalk.dim(`[DEBUG-ONLY] Statsig event: ${eventName} ${JSON.stringify(metadata, null, 0)}`))
}
}
export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
// Default to disabled gates when Statsig is not active
return false
// if (env.isCI || process.env.NODE_ENV === 'test') {
// return false
// }
// const statsigClient = await initializeStatsig()
// if (!statsigClient) return false
// const value = statsigClient.checkGate(gateName)
// gateValues[gateName] = value
// return value
})
export const useStatsigGate = (gateName: string, defaultValue = false) => {
return false
// const [gateValue, setGateValue] = React.useState(defaultValue)
// React.useEffect(() => {
// checkGate(gateName).then(setGateValue)
// }, [gateName])
// return gateValue
}
export function getGateValues(): Record<string, boolean> {
return { ...gateValues }
}
export const getExperimentValue = memoize(
async <T>(experimentName: string, defaultValue: T): Promise<T> => {
return defaultValue
// if (env.isCI || process.env.NODE_ENV === 'test') {
// return defaultValue
// }
// const statsigClient = await initializeStatsig()
// if (!statsigClient) return defaultValue
// const experiment = statsigClient.getExperiment(experimentName)
// if (Object.keys(experiment.value).length === 0) {
// logError(`getExperimentValue got empty value for ${experimentName}`)
// return defaultValue
// }
// return experiment.value as T
},
)
// NB Not memoized like other methods, to allow for dynamic config changes
export const getDynamicConfig = async <T>(
configName: string,
defaultValue: T,
): Promise<T> => {
return defaultValue
// if (env.isCI || process.env.NODE_ENV === 'test') {
// return defaultValue
// }
// const statsigClient = await initializeStatsig()
// if (!statsigClient) return defaultValue
// const config = statsigClient.getDynamicConfig(configName)
// if (Object.keys(config.value).length === 0) {
// logError(`getDynamicConfig got empty value for ${configName}`)
// return defaultValue
// }
// return config.value as T
}

View File

@ -1,86 +0,0 @@
import { StorageProvider } from '@statsig/client-core'
import * as fs from 'fs'
import * as path from 'path'
import { homedir } from 'os'
import { logError } from '../utils/log'
import { existsSync, unlinkSync } from 'fs'
import { CONFIG_BASE_DIR } from '../constants/product'
// Support both KODE_CONFIG_DIR and CLAUDE_CONFIG_DIR environment variables
const CONFIG_DIR = process.env.KODE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR ?? path.join(homedir(), CONFIG_BASE_DIR)
const STATSIG_DIR = path.join(CONFIG_DIR, 'statsig')
// Ensure the directory exists
try {
fs.mkdirSync(STATSIG_DIR, { recursive: true })
} catch (error) {
logError(`Failed to create statsig storage directory: ${error}`)
}
export class FileSystemStorageProvider implements StorageProvider {
private cache: Map<string, string> = new Map()
private ready = false
constructor() {
// Load all existing files into cache on startup
try {
if (!fs.existsSync(STATSIG_DIR)) {
fs.mkdirSync(STATSIG_DIR, { recursive: true })
}
const files = fs.readdirSync(STATSIG_DIR)
for (const file of files) {
const key = decodeURIComponent(file)
const value = fs.readFileSync(path.join(STATSIG_DIR, file), 'utf8')
this.cache.set(key, value)
}
this.ready = true
} catch (error) {
logError(`Failed to initialize statsig storage: ${error}`)
this.ready = true // Still mark as ready to avoid blocking
}
}
isReady(): boolean {
return this.ready
}
isReadyResolver(): Promise<void> | null {
return this.ready ? Promise.resolve() : null
}
getProviderName(): string {
return 'FileSystemStorageProvider'
}
getItem(key: string): string | null {
return this.cache.get(key) ?? null
}
setItem(key: string, value: string): void {
this.cache.set(key, value)
try {
const encodedKey = encodeURIComponent(key)
fs.writeFileSync(path.join(STATSIG_DIR, encodedKey), value, 'utf8')
} catch (error) {
logError(`Failed to write statsig storage item: ${error}`)
}
}
removeItem(key: string): void {
this.cache.delete(key)
const encodedKey = encodeURIComponent(key)
const file = path.join(STATSIG_DIR, encodedKey)
if (!existsSync(file)) {
return
}
try {
unlinkSync(file)
} catch (error) {
logError(`Failed to remove statsig storage item: ${error}`)
}
}
getAllKeys(): readonly string[] {
return Array.from(this.cache.keys())
}
}

View File

@ -1,5 +1,4 @@
import { getTodos, TodoItem } from '../utils/todoStorage'
import { logEvent } from './statsig'
export interface ReminderMessage {
role: 'system'
@ -98,17 +97,7 @@ class SystemReminderService {
}
// Log aggregated metrics instead of individual events for performance
if (reminders.length > 0) {
logEvent('system_reminder_batch', {
count: reminders.length.toString(),
types: reminders.map(r => r.type).join(','),
priorities: reminders.map(r => r.priority).join(','),
categories: reminders.map(r => r.category).join(','),
sessionCount: this.sessionState.reminderCount.toString(),
agentId: agentId || 'default',
timestamp: currentTime.toString(),
})
}
return reminders
}
@ -339,13 +328,7 @@ class SystemReminderService {
this.sessionState.contextPresent =
Object.keys(context.context || {}).length > 0
// Log session startup
logEvent('system_reminder_session_startup', {
agentId: context.agentId || 'default',
contextKeys: Object.keys(context.context || {}).join(','),
messageCount: (context.messages || 0).toString(),
timestamp: context.timestamp.toString(),
})
})
// Todo change events

View File

@ -17,7 +17,6 @@ import { getModelManager } from '../../utils/model'
import BashToolResultMessage from './BashToolResultMessage'
import { BANNED_COMMANDS, PROMPT } from './prompt'
import { formatOutput, getCommandFilePaths } from './utils'
import { logEvent } from '../../services/statsig'
export const inputSchema = z.strictObject({
command: z.string().describe('The command to execute'),
@ -176,7 +175,7 @@ export const BashTool = {
// Shell directory is outside original working directory, reset it
await PersistentShell.getInstance().setCwd(getOriginalCwd())
stderr = `${stderr.trim()}${EOL}Shell cwd was reset to ${getOriginalCwd()}`
logEvent('bash_tool_reset_to_original_dir', {})
}
// Update read timestamps for any files referenced by the command

View File

@ -7,7 +7,6 @@ import { z } from 'zod'
import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage'
import { StructuredDiff } from '../../components/StructuredDiff'
import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage'
import { logEvent } from '../../services/statsig'
import { Tool, ValidationResult } from '../../Tool'
import { intersperse } from '../../utils/array'
import {
@ -251,7 +250,6 @@ export const FileEditTool = {
// Log when editing CLAUDE.md
if (fullFilePath.endsWith(`${sep}${PROJECT_FILE}`)) {
logEvent('tengu_write_claudemd', {})
}
// Emit file edited event for system reminders

View File

@ -9,7 +9,6 @@ import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdated
import { HighlightedCode } from '../../components/HighlightedCode'
import { StructuredDiff } from '../../components/StructuredDiff'
import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage'
import { logEvent } from '../../services/statsig'
import type { Tool } from '../../Tool'
import { intersperse } from '../../utils/array'
import {
@ -226,7 +225,6 @@ export const FileWriteTool = {
// Log when writing to CLAUDE.md
if (fullFilePath.endsWith(`${sep}${PROJECT_FILE}`)) {
logEvent('tengu_write_claudemd', {})
}
// Emit file edited event for system reminders

View File

@ -5,7 +5,6 @@ import * as React from 'react'
import { z } from 'zod'
import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage'
import { StructuredDiff } from '../../components/StructuredDiff'
import { logEvent } from '../../services/statsig'
import { Tool, ValidationResult } from '../../Tool'
import { intersperse } from '../../utils/array'
import {
@ -374,12 +373,7 @@ export const MultiEditTool = {
}
// Log the operation
logEvent('multi_edit_tool_used', {
file_path: relativePath,
edits_count: String(edits.length),
was_new_file: String(!fileExists),
duration_ms: String(Date.now() - startTime),
})
yield {
type: 'result',

View File

@ -7,7 +7,7 @@ import {
StickerRequestForm,
FormData,
} from '../../components/StickerRequestForm'
import { checkGate, logEvent } from '../../services/statsig'
// Telemetry and gates removed
import { getTheme } from '../../utils/theme'
const stickerRequestSchema = z.object({
@ -19,18 +19,14 @@ export const StickerRequestTool: Tool = {
userFacingName: () => 'Stickers',
description: async () => DESCRIPTION,
inputSchema: stickerRequestSchema,
isEnabled: async () => {
const enabled = await checkGate('tengu_sticker_easter_egg')
return enabled
},
isEnabled: async () => false,
isReadOnly: () => false,
isConcurrencySafe: () => false, // StickerRequestTool modifies state, not safe for concurrent execution
needsPermissions: () => false,
prompt: async () => PROMPT,
async *call(_, context: ToolUseContext) {
// Log form entry event
logEvent('sticker_request_form_opened', {})
// Create a promise to track form completion and status
let resolveForm: (success: boolean) => void
@ -45,19 +41,14 @@ export const StickerRequestTool: Tool = {
jsx: (
<StickerRequestForm
onSubmit={(formData: FormData) => {
// Log successful completion with form data
logEvent('sticker_request_form_completed', {
has_address: Boolean(formData.address1).toString(),
has_optional_address: Boolean(formData.address2).toString(),
})
resolveForm(true)
if (extendedContext.setToolJSX) {
extendedContext.setToolJSX(null) // Clear the JSX
}
}}
onClose={() => {
// Log form cancellation
logEvent('sticker_request_form_cancelled', {})
resolveForm(false)
if (extendedContext.setToolJSX) {
extendedContext.setToolJSX(null) // Clear the JSX

View File

@ -5,7 +5,7 @@ import { Tool } from '../../Tool'
import { DESCRIPTION, PROMPT } from './prompt'
import { getTheme } from '../../utils/theme'
import { MessageResponse } from '../../components/MessageResponse'
import { checkGate, logEvent } from '../../services/statsig'
// Telemetry and gates removed
import { USE_BEDROCK, USE_VERTEX } from '../../utils/model'
const thinkToolSchema = z.object({
@ -17,20 +17,14 @@ export const ThinkTool = {
userFacingName: () => 'Think',
description: async () => DESCRIPTION,
inputSchema: thinkToolSchema,
isEnabled: async () =>
Boolean(process.env.THINK_TOOL) && (await checkGate('tengu_think_tool')),
isEnabled: async () => Boolean(process.env.THINK_TOOL),
isReadOnly: () => true,
isConcurrencySafe: () => true, // ThinkTool is read-only, safe for concurrent execution
needsPermissions: () => false,
prompt: async () => PROMPT,
async *call(input, { messageId }) {
logEvent('tengu_thinking', {
messageId,
thoughtLength: input.thought.length.toString(),
method: 'tool',
provider: USE_BEDROCK ? 'bedrock' : USE_VERTEX ? 'vertex' : '1p',
})
yield {
type: 'result',

View File

@ -6,7 +6,6 @@ import { spawn, execSync, type ChildProcess } from 'child_process'
import { isAbsolute, resolve, join } from 'path'
import { logError } from './log'
import * as os from 'os'
import { logEvent } from '../services/statsig'
import { PRODUCT_COMMAND } from '../constants/product'
type ExecResult = {
@ -236,10 +235,6 @@ export class PersistentShell {
if (code) {
// TODO: It would be nice to alert the user that shell crashed
logError(`Shell exited with code ${code} and signal ${signal}`)
logEvent('persistent_shell_exit', {
code: code?.toString() || 'null',
signal: signal || 'null',
})
}
for (const file of [
this.statusFile,
@ -303,20 +298,13 @@ export class PersistentShell {
.split('\n')
.filter(Boolean) // Filter out empty strings
if (childPids.length > 0) {
logEvent('persistent_shell_command_interrupted', {
numChildProcesses: childPids.length.toString(),
})
}
childPids.forEach(pid => {
try {
process.kill(Number(pid), 'SIGTERM')
} catch (error) {
logError(`Failed to kill process ${pid}: ${error}`)
logEvent('persistent_shell_kill_process_error', {
error: (error as Error).message.substring(0, 10),
})
}
})
} catch {
@ -355,9 +343,7 @@ export class PersistentShell {
resolve(result)
} catch (error) {
logEvent('persistent_shell_command_error', {
error: (error as Error).message.substring(0, 10),
})
reject(error as Error)
} finally {
this.isExecuting = false
@ -418,9 +404,7 @@ export class PersistentShell {
// If there's a syntax error, return an error and log it
const errorStr =
typeof stderr === 'string' ? stderr : String(stderr || '')
logEvent('persistent_shell_syntax_error', {
error: errorStr.substring(0, 10),
})
return Promise.resolve({
stdout: '',
stderr: errorStr,
@ -486,10 +470,7 @@ export class PersistentShell {
this.killChildren()
code = SIGTERM_CODE
stderr += (stderr ? '\n' : '') + 'Command execution timed out'
logEvent('persistent_shell_command_timeout', {
command: command.substring(0, 10),
timeout: commandTimeout.toString(),
})
}
resolve({
stdout,
@ -515,10 +496,7 @@ export class PersistentShell {
? error.message
: String(error || 'Unknown error')
logError(`Error in sendToShell: ${errorString}`)
logEvent('persistent_shell_write_error', {
error: errorString.substring(0, 100),
command: command.substring(0, 30),
})
throw error
}
}

View File

@ -1,6 +1,6 @@
import { execFileNoThrow } from './execFileNoThrow'
import { logError } from './log'
import { getDynamicConfig } from '../services/statsig'
import { lt, gt } from 'semver'
import { MACRO } from '../constants/macros'
import { PRODUCT_NAME } from '../constants/product'
@ -14,10 +14,7 @@ export type VersionConfig = {
// Ensure current version meets minimum supported version; exit if too old
export async function assertMinVersion(): Promise<void> {
try {
const versionConfig = await getDynamicConfig<VersionConfig>(
'tengu_version_config',
{ minVersion: '0.0.0' },
)
const versionConfig: VersionConfig = { minVersion: '0.0.0' }
if (versionConfig.minVersion && lt(MACRO.VERSION, versionConfig.minVersion)) {
const suggestions = await getUpdateCommandSuggestions()
// Intentionally minimal: caller may print its own message; we just exit
@ -122,4 +119,3 @@ export async function checkAndNotifyUpdate(): Promise<void> {
logError(`update-notify: ${error}`)
}
}

View File

@ -1,20 +0,0 @@
import { memoize } from 'lodash-es'
import { checkGate } from '../services/statsig'
import {
GATE_TOKEN_EFFICIENT_TOOLS,
BETA_HEADER_TOKEN_EFFICIENT_TOOLS,
CLAUDE_CODE_20250219_BETA_HEADER,
} from '../constants/betas.js'
export const getBetas = memoize(async (): Promise<string[]> => {
const betaHeaders = [CLAUDE_CODE_20250219_BETA_HEADER]
if (process.env.USER_TYPE === 'ant' || process.env.SWE_BENCH) {
const useTokenEfficientTools = await checkGate(GATE_TOKEN_EFFICIENT_TOOLS)
if (useTokenEfficientTools) {
betaHeaders.push(BETA_HEADER_TOKEN_EFFICIENT_TOOLS)
}
}
return betaHeaders
})

View File

@ -6,8 +6,6 @@ import { GLOBAL_CLAUDE_FILE } from './env'
import { getCwd } from './state'
import { randomBytes } from 'crypto'
import { safeParseJSON } from './json'
import { checkGate, logEvent } from '../services/statsig'
import { GATE_USE_EXTERNAL_UPDATER } from '../constants/betas'
import { ConfigParseError } from './errors'
import type { ThemeNames } from './theme'
import { debug as debugLogger } from './debugLogger'
@ -474,10 +472,7 @@ export function saveCurrentProjectConfig(projectConfig: ProjectConfig): void {
}
export async function isAutoUpdaterDisabled(): Promise<boolean> {
const useExternalUpdater = await checkGate(GATE_USE_EXTERNAL_UPDATER)
return (
useExternalUpdater || getGlobalConfig().autoUpdaterStatus === 'disabled'
)
return getGlobalConfig().autoUpdaterStatus === 'disabled'
}
export const TEST_MCPRC_CONFIG_FOR_TESTING: Record<string, McpServerConfig> = {}
@ -523,9 +518,7 @@ export const getMcprcConfig = memoize(
const mcprcContent = readFileSync(mcprcPath, 'utf-8')
const config = safeParseJSON(mcprcContent)
if (config && typeof config === 'object') {
logEvent('tengu_mcprc_found', {
numServers: Object.keys(config).length.toString(),
})
// Logging removed
return config as Record<string, McpServerConfig>
}
} catch {
@ -561,10 +554,7 @@ export function getOrCreateUserID(): string {
}
export function getConfigForCLI(key: string, global: boolean): unknown {
logEvent('tengu_config_get', {
key,
global: global?.toString() ?? 'false',
})
if (global) {
if (!isGlobalConfigKey(key)) {
console.error(
@ -589,10 +579,7 @@ export function setConfigForCLI(
value: unknown,
global: boolean,
): void {
logEvent('tengu_config_set', {
key,
global: global?.toString() ?? 'false',
})
if (global) {
if (!isGlobalConfigKey(key)) {
console.error(
@ -634,10 +621,7 @@ export function setConfigForCLI(
}
export function deleteConfigForCLI(key: string, global: boolean): void {
logEvent('tengu_config_delete', {
key,
global: global?.toString() ?? 'false',
})
if (global) {
if (!isGlobalConfigKey(key)) {
console.error(
@ -664,9 +648,7 @@ export function deleteConfigForCLI(key: string, global: boolean): void {
export function listConfigForCLI(global: true): GlobalConfig
export function listConfigForCLI(global: false): ProjectConfig
export function listConfigForCLI(global: boolean): object {
logEvent('tengu_config_list', {
global: global?.toString() ?? 'false',
})
if (global) {
const currentConfig = pick(getGlobalConfig(), GLOBAL_CONFIG_KEYS)
return currentConfig

View File

@ -11,7 +11,6 @@ import { MalformedCommandError } from './errors'
import { logError } from './log'
import { resolve } from 'path'
import { last, memoize } from 'lodash-es'
import { logEvent } from '../services/statsig'
import type { SetToolJSXFn, Tool, ToolUseContext } from '../Tool'
import { lastX } from '../utils/generators'
import { NO_CONTENT_MESSAGE } from '../services/claude'
@ -175,7 +174,7 @@ export async function processUserInput(
): Promise<Message[]> {
// Bash commands
if (mode === 'bash') {
logEvent('tengu_input_bash', {})
const userMessage = createUserMessage(`<bash-input>${input}</bash-input>`)
@ -242,7 +241,7 @@ export async function processUserInput(
}
// Koding mode - special wrapper for display
else if (mode === 'koding') {
logEvent('tengu_input_koding', {})
const userMessage = createUserMessage(
`<koding-input>${input}</koding-input>`,
@ -265,7 +264,7 @@ export async function processUserInput(
commandName = commandName + ' (MCP)'
}
if (!commandName) {
logEvent('tengu_input_slash_missing', { input })
return [
createAssistantMessage('Commands are in the form `/command [args]`'),
]
@ -274,7 +273,7 @@ export async function processUserInput(
// Check if it's a real command before processing
if (!hasCommand(commandName, context.options.commands)) {
// If not a real command, treat it as a regular user input
logEvent('tengu_input_prompt', {})
return [createUserMessage(input)]
}
@ -288,7 +287,7 @@ export async function processUserInput(
// Local JSX commands
if (newMessages.length === 0) {
logEvent('tengu_input_command', { input })
return []
}
@ -300,23 +299,23 @@ export async function processUserInput(
typeof newMessages[1]!.message.content === 'string' &&
newMessages[1]!.message.content.startsWith('Unknown command:')
) {
logEvent('tengu_input_slash_invalid', { input })
return newMessages
}
// User-Assistant pair (eg. local commands)
if (newMessages.length === 2) {
logEvent('tengu_input_command', { input })
return newMessages
}
// A valid command
logEvent('tengu_input_command', { input })
return newMessages
}
// Regular user prompt
logEvent('tengu_input_prompt', {})
// Check if this is a Koding request that needs special handling
const isKodingRequest = context.options?.isKodingRequest === true

View File

@ -1,5 +1,5 @@
import { memoize } from 'lodash-es'
import { getDynamicConfig, getExperimentValue } from '../services/statsig'
import { logError } from './log'
import {
getGlobalConfig,
@ -28,15 +28,7 @@ const DEFAULT_MODEL_CONFIG: ModelConfig = {
* Relies on the built-in caching from StatsigClient
*/
async function getModelConfig(): Promise<ModelConfig> {
try {
return await getDynamicConfig<ModelConfig>(
'tengu-capable-model-config',
DEFAULT_MODEL_CONFIG,
)
} catch (error) {
logError(error)
return DEFAULT_MODEL_CONFIG
}
return DEFAULT_MODEL_CONFIG
}
export const getSlowAndCapableModel = memoize(async (): Promise<string> => {

View File

@ -1,4 +1,4 @@
import { logEvent } from '../services/statsig'
type SessionState = {
modelErrors: Record<string, unknown>
currentError: string | null
@ -24,16 +24,8 @@ function setSessionState(
value?: any,
): void {
if (typeof keyOrState === 'string') {
logEvent('session_state_set', {
key: keyOrState,
value: JSON.stringify(value),
})
sessionState[keyOrState] = value
} else {
logEvent('session_state_set', {
key: 'partial',
value: JSON.stringify(keyOrState),
})
Object.assign(sessionState, keyOrState)
}
}

View File

@ -1,6 +1,5 @@
import { last } from 'lodash-es'
import type { Message } from '../query'
import { logEvent } from '../services/statsig'
import { getLastAssistantMessageId } from './messages'
import { ThinkTool } from '../tools/ThinkTool/ThinkTool'
import { USE_BEDROCK, USE_VERTEX, getModelManager } from './model'
@ -10,22 +9,10 @@ export async function getMaxThinkingTokens(
): Promise<number> {
if (process.env.MAX_THINKING_TOKENS) {
const tokens = parseInt(process.env.MAX_THINKING_TOKENS, 10)
logEvent('tengu_thinking', {
method: 'scratchpad',
tokenCount: tokens.toString(),
messageId: getLastAssistantMessageId(messages),
provider: USE_BEDROCK ? 'bedrock' : USE_VERTEX ? 'vertex' : '1p',
})
return tokens
}
if (await ThinkTool.isEnabled()) {
logEvent('tengu_thinking', {
method: 'scratchpad',
tokenCount: '0',
messageId: getLastAssistantMessageId(messages),
provider: USE_BEDROCK ? 'bedrock' : USE_VERTEX ? 'vertex' : '1p',
})
return 0
}
@ -34,12 +21,6 @@ export async function getMaxThinkingTokens(
lastMessage?.type !== 'user' ||
typeof lastMessage.message.content !== 'string'
) {
logEvent('tengu_thinking', {
method: 'scratchpad',
tokenCount: '0',
messageId: getLastAssistantMessageId(messages),
provider: USE_BEDROCK ? 'bedrock' : USE_VERTEX ? 'vertex' : '1p',
})
return 0
}
@ -53,12 +34,6 @@ export async function getMaxThinkingTokens(
content.includes('think very hard') ||
content.includes('ultrathink')
) {
logEvent('tengu_thinking', {
method: 'scratchpad',
tokenCount: '31999',
messageId: getLastAssistantMessageId(messages),
provider: USE_BEDROCK ? 'bedrock' : USE_VERTEX ? 'vertex' : '1p',
})
return 32_000 - 1
}
@ -69,31 +44,13 @@ export async function getMaxThinkingTokens(
content.includes('think more') ||
content.includes('megathink')
) {
logEvent('tengu_thinking', {
method: 'scratchpad',
tokenCount: '10000',
messageId: getLastAssistantMessageId(messages),
provider: USE_BEDROCK ? 'bedrock' : USE_VERTEX ? 'vertex' : '1p',
})
return 10_000
}
if (content.includes('think')) {
logEvent('tengu_thinking', {
method: 'scratchpad',
tokenCount: '4000',
messageId: getLastAssistantMessageId(messages),
provider: USE_BEDROCK ? 'bedrock' : USE_VERTEX ? 'vertex' : '1p',
})
return 4_000
}
logEvent('tengu_thinking', {
method: 'scratchpad',
tokenCount: '0',
messageId: getLastAssistantMessageId(messages),
provider: USE_BEDROCK ? 'bedrock' : USE_VERTEX ? 'vertex' : '1p',
})
return 0
}

View File

@ -1,4 +1,3 @@
import { logEvent } from '../services/statsig'
export type CompletionType =
| 'str_replace_single'
@ -16,11 +15,5 @@ type LogEvent = {
}
export function logUnaryEvent(event: LogEvent): void {
logEvent('tengu_unary_event', {
event: event.event,
completion_type: event.completion_type,
language_name: event.metadata.language_name,
message_id: event.metadata.message_id,
platform: event.metadata.platform,
})
// intentionally no-op
}

View File

@ -1,7 +1,6 @@
import { getGlobalConfig, getOrCreateUserID } from './config'
import { memoize } from 'lodash-es'
import { env } from './env'
import { type StatsigUser } from '@statsig/js-client'
import { execFileNoThrow } from './execFileNoThrow'
import { logError, SESSION_ID } from './log'
import { MACRO } from '../constants/macros'
@ -14,7 +13,16 @@ export const getGitEmail = memoize(async (): Promise<string | undefined> => {
return result.stdout.trim() || undefined
})
export const getUser = memoize(async (): Promise<StatsigUser> => {
type SimpleUser = {
customIDs?: Record<string, string>
userID: string
appVersion?: string
userAgent?: string
email?: string
custom?: Record<string, unknown>
}
export const getUser = memoize(async (): Promise<SimpleUser> => {
const userID = getOrCreateUserID()
const config = getGlobalConfig()
const email = undefined