diff --git a/README.md b/README.md index a97b4960..978bbcfa 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -# free-code +# Claudium **The free build of Claude Code.** All telemetry stripped. All injected security-prompt guardrails removed. All experimental features unlocked. One binary, zero callbacks home. ```bash -curl -fsSL https://raw.githubusercontent.com/paoloanzn/free-code/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/DdogezD/claudium/main/install.sh | bash ``` -> Checks your system, installs Bun if needed, clones, builds with all features enabled, and puts `free-code` on your PATH. Then just `export ANTHROPIC_API_KEY="sk-ant-..."` and run `free-code`. +> Checks your system, installs Bun if needed, clones, builds with all features enabled, and puts `claudium` on your PATH. See [API Configuration](#api-configuration) for API setup.

- free-code screenshot + claudium screenshot

--- @@ -20,17 +20,22 @@ curl -fsSL https://raw.githubusercontent.com/paoloanzn/free-code/main/install.sh This is a clean, buildable fork of Anthropic's [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI -- the terminal-native AI coding agent. The upstream source became publicly available on March 31, 2026 through a source map exposure in the npm distribution. -This fork applies three categories of changes on top of that snapshot: +This fork applies four categories of changes on top of that snapshot: -### 1. Telemetry removed +### 1. Privacy-First -The upstream binary phones home through OpenTelemetry/gRPC, GrowthBook analytics, Sentry error reporting, and custom event logging. In this build: +Eliminates all tracking and remote-control mechanisms present in the original Claude Code: -- All outbound telemetry endpoints are dead-code-eliminated or stubbed -- GrowthBook feature flag evaluation still works locally (needed for runtime feature gates) but does not report back -- No crash reports, no usage analytics, no session fingerprinting +- No telemetry – No unnecessary data is transmitted to Anthropic servers +- No analytics – No usage tracking or event logging +- No fingerprinting – No user or environment identification +- No auto-updates – No remote version control or forced updates -### 2. Security-prompt guardrails removed +### 2. OpenAI-compatible API support + +Added an API shim layer (`src/services/api/openaiShim.ts`) transparently translates between Anthropic message format and OpenAI-compatible APIs. It supports both Chat Completions and the newer Responses API, so all Claude Code tools (bash, file read/write, grep, glob, agents, MCP, etc.) keep working while you swap in a different backend LLM. + +### 3. Security-prompt guardrails removed Anthropic injects system-level instructions into every conversation that constrain Claude's behavior beyond what the model itself enforces. These include: @@ -38,9 +43,9 @@ Anthropic injects system-level instructions into every conversation that constra - Injected "cyber risk" instruction blocks - Managed-settings security overlays pushed from Anthropic's servers -This build strips those injections. The model's own safety training still applies -- this just removes the extra layer of prompt-level restrictions that the CLI wraps around it. +This build strips those injections. The model's own safety training still applies —— this just removes the extra layer of prompt-level restrictions that the CLI wraps around it. -### 3. Experimental features enabled +### 4. Experimental features enabled Claude Code ships with dozens of feature flags gated behind `bun:bundle` compile-time switches. Most are disabled in the public npm release. This build unlocks all 45+ flags that compile cleanly, including: @@ -70,16 +75,10 @@ See [FEATURES.md](FEATURES.md) for the full audit of all 88 flags and their stat ## Quick install ```bash -curl -fsSL https://raw.githubusercontent.com/paoloanzn/free-code/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/DdogezD/claudium/main/install.sh | bash ``` -This will check your system, install Bun if needed, clone the repo, build the binary with all experimental features enabled, and symlink it as `free-code` on your PATH. - -After install, just run: -```bash -export ANTHROPIC_API_KEY="sk-ant-..." -free-code -``` +This will check your system, install Bun if needed, clone the repo, build the binary `cli-dev` (with all experimental features enabled), and put it as `claudium` on your PATH. --- @@ -87,7 +86,7 @@ free-code - [Bun](https://bun.sh) >= 1.3.11 - macOS or Linux (Windows via WSL) -- An Anthropic API key (set `ANTHROPIC_API_KEY` in your environment) +- An API key ([Anthropic Messages](#anthropic-messages-api) or [OpenAI-compatible APIs](#openai-compatible-apis)) ```bash # Install Bun if you don't have it @@ -100,8 +99,8 @@ curl -fsSL https://bun.sh/install | bash ```bash # Clone the repo -git clone https://github.com/paoloanzn/claude-code.git -cd claude-code +git clone https://github.com/DdogezD/claudium.git +cd claudium # Install dependencies bun install @@ -145,7 +144,11 @@ bun run ./scripts/build.ts --dev --feature=BRIDGE_MODE ## Run ```bash -# Run the built binary directly + +# Run the installed binary +claudium + +# Or the built binary ./cli # Or the dev binary @@ -154,24 +157,23 @@ bun run ./scripts/build.ts --dev --feature=BRIDGE_MODE # Or run from source without compiling (slower startup) bun run dev -# Set your API key -export ANTHROPIC_API_KEY="sk-ant-..." +# See [API Configuration](#api-configuration) for API setup. # Or use Claude.ai OAuth -./cli /login +claudium /login ``` ### Quick test ```bash # One-shot mode -./cli -p "what files are in this directory?" +claudium -p "what files are in this directory?" # Interactive REPL (default) -./cli +claudium # With specific model -./cli --model claude-sonnet-4-6-20250514 +claudium --model claude-sonnet-4-6-20250514 ``` --- @@ -216,7 +218,130 @@ src/ | Schema validation | Zod v4 | | Code search | ripgrep (bundled) | | Protocols | MCP, LSP | -| API | Anthropic Messages API | +| API | Anthropic Messages API / OpenAI-compatible APIs | + +--- + +## API Configuration + +Claudium supports both **Anthropic Messages API** (natively) and **OpenAI-compatible APIs**. The shim can use either Chat Completions or the newer Responses API depending on the provider and model you configure. + +### Anthropic Messages API + +Use the official Anthropic Messages API with your Anthropic API key. + +```bash +export ANTHROPIC_API_KEY="sk-ant-..." +``` + +### OpenAI-compatible APIs + +Enable OpenAI-compatible mode and configure your preferred provider: + +```bash +export CLAUDE_CODE_USE_OPENAI=1 +``` + +#### Environment Variables + +| Variable | Description | +|---|---| +| `OPENAI_API_KEY` | API key (required for cloud APIs, optional for local models) | +| `OPENAI_BASE_URL` | API base URL (default: `https://api.openai.com/v1`) | +| `OPENAI_MODEL` | Model ID (default: `gpt-4o`) | +| `OPENAI_API_MODE` | Force transport selection: `chat_completions` or `responses` | +| `ANTHROPIC_DEFAULT_OPUS_MODEL` | Override which concrete model the `opus` alias resolves to | +| `ANTHROPIC_DEFAULT_SONNET_MODEL` | Override which concrete model the `sonnet` alias resolves to | +| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | Override which concrete model the `haiku` alias resolves to | +| `CLAUDE_CODE_MAX_CONTEXT_TOKENS` | Override max context window size | +| `CLAUDE_CODE_SUMMARY_OUTPUT_TOKENS` | Override token limit for summarized context | +| `CLAUDE_CODE_AUTO_COMPACT_BUFFER_TOKENS` | Override auto-compact buffer size | + +**Autocompact buffer** = `CLAUDE_CODE_SUMMARY_OUTPUT_TOKENS` + `CLAUDE_CODE_AUTO_COMPACT_BUFFER_TOKENS` + +Use `OPENAI_MODEL` when you want to pin the whole session to one exact model. Use `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, and `ANTHROPIC_DEFAULT_HAIKU_MODEL` when you want aliases such as `opus`, `sonnet`, and `haiku` to resolve to your own provider-specific model IDs. + +#### Transport selection + +- Codex aliases such as `codexplan` and `codexspark` also use the unified Responses transport, but through the ChatGPT Codex auth/backend path. +- Other OpenAI-compatible backends stay on Chat Completions by default for maximum compatibility unless you explicitly select Responses. +- Official OpenAI requests that include reasoning still use the Responses API. +- Set `OPENAI_API_MODE=responses` to force `/responses` on providers that support it, or `OPENAI_API_MODE=chat_completions` to force the legacy path. + +#### Provider Examples + +**OpenAI (Responses API, explicit):** +```bash +export CLAUDE_CODE_USE_OPENAI=1 +export OPENAI_API_KEY=sk-... +export OPENAI_MODEL=gpt-5.4 +export OPENAI_API_MODE=responses +``` + +**OpenAI ChatGPT ** +```bash +export CLAUDE_CODE_USE_OPENAI=1 +export OPENAI_API_KEY=sk-... +export OPENAI_MODEL=gpt-4o +export OPENAI_API_MODE=chat_completions +``` + +**OpenAI ChatGPT Codex:** +```bash +export CLAUDE_CODE_USE_OPENAI=1 +export CODEX_API_KEY=eyJ... +export CHATGPT_ACCOUNT_ID=account_... +export OPENAI_MODEL=codexplan +``` + +**OpenRouter:** +```bash +export CLAUDE_CODE_USE_OPENAI=1 +export OPENAI_API_KEY=sk-or-v1-... +export OPENAI_BASE_URL=https://openrouter.ai/api/v1 +export OPENAI_MODEL=openai/gpt-5.4 +``` + +**DeepSeek:** +```bash +export CLAUDE_CODE_USE_OPENAI=1 +export OPENAI_API_KEY=sk-... +export OPENAI_BASE_URL=https://api.deepseek.com/v1 +export OPENAI_MODEL=deepseek-chat +``` + +**LLaMA.CPP (local):** +```bash +export CLAUDE_CODE_USE_OPENAI=1 +export OPENAI_BASE_URL=http://localhost:1234/v1 +export OPENAI_MODEL=your-model-name +export CLAUDE_CODE_MAX_CONTEXT_TOKENS=65536 +export CLAUDE_CODE_SUMMARY_OUTPUT_TOKENS=12000 +export CLAUDE_CODE_AUTO_COMPACT_BUFFER_TOKENS=4000 +``` + +**Ollama (local):** +```bash +export CLAUDE_CODE_USE_OPENAI=1 +export OPENAI_BASE_URL=http://localhost:1234/v1 +export OPENAI_MODEL=llama3.3 +``` + +**LM Studio (local):** +```bash +export CLAUDE_CODE_USE_OPENAI=1 +export OPENAI_BASE_URL=http://localhost:1234/v1 +export OPENAI_MODEL=your-model-name +``` + +**Azure OpenAI:** +```bash +export CLAUDE_CODE_USE_OPENAI=1 +export OPENAI_API_KEY=your-azure-key +export OPENAI_BASE_URL=https://your-resource.openai.azure.com/openai/deployments/your-deployment +export OPENAI_MODEL=gpt-4o +export AZURE_OPENAI_API_VERSION=2024-12-01-preview +``` --- diff --git a/assets/screenshot.png b/assets/screenshot.png index 22ffc10d..cfdc10ef 100644 Binary files a/assets/screenshot.png and b/assets/screenshot.png differ diff --git a/install.sh b/install.sh index 3a632e88..3547e964 100755 --- a/install.sh +++ b/install.sh @@ -1,19 +1,21 @@ #!/usr/bin/env bash set -euo pipefail -# free-code installer -# Usage: curl -fsSL https://raw.githubusercontent.com/paoloanzn/free-code/main/install.sh | bash +# claudium installer +# Usage: curl -fsSL https://raw.githubusercontent.com/DdogezD/claudium/main/install.sh | bash RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +ORANGE='\033[38;5;208m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' -REPO="https://github.com/paoloanzn/free-code.git" -INSTALL_DIR="$HOME/free-code" +REPO="https://github.com/DdogezD/claudium.git" +BUILD_DIR="$HOME/.cache/claudium" +INSTALL_DIR="$HOME/.local/bin" BUN_MIN_VERSION="1.3.11" info() { printf "${CYAN}[*]${RESET} %s\n" "$*"; } @@ -23,13 +25,15 @@ fail() { printf "${RED}[x]${RESET} %s\n" "$*"; exit 1; } header() { echo "" - printf "${BOLD}${CYAN}" + printf "${BOLD}${ORANGE}" cat << 'ART' - ___ _ - / _|_ __ ___ ___ ___ __| | ___ - | |_| '__/ _ \/ _ \_____ / __/ _` |/ _ \ - | _| | | __/ __/_____| (_| (_| | __/ - |_| |_| \___|\___| \___\__,_|\___| + +_________ .__ .___ .__ +\_ ___ \ | | _____ __ __ __| _/ |__| __ __ _____ +/ \ \/ | | \__ \ | | \ / __ | | | | | \ / \ +\ \____ | |__ / __ \_ | | / / /_/ | | | | | / | Y Y \ + \______ / |____/ (____ / |____/ \____ | |__| |____/ |__|_| / + \/ \/ \/ \/ ART printf "${RESET}" @@ -61,7 +65,9 @@ check_git() { # Compare semver: returns 0 if $1 >= $2 version_gte() { - [ "$(printf '%s\n' "$1" "$2" | sort -V | head -1)" = "$2" ] + local newer + newer="$(printf '%s\n' "$1" "$2" | sort -V | tail -1)" + [ "$newer" = "$1" ] } check_bun() { @@ -97,44 +103,48 @@ install_bun() { # ------------------------------------------------------------------- clone_repo() { - if [ -d "$INSTALL_DIR" ]; then - warn "$INSTALL_DIR already exists" - if [ -d "$INSTALL_DIR/.git" ]; then + if [ -d "$BUILD_DIR" ]; then + warn "$BUILD_DIR already exists" + if [ -d "$BUILD_DIR/.git" ]; then info "Pulling latest changes..." - git -C "$INSTALL_DIR" pull --ff-only origin main 2>/dev/null || { + git -C "$BUILD_DIR" pull --ff-only origin main 2>/dev/null || { warn "Pull failed, continuing with existing copy" } fi else - info "Cloning repository..." - git clone --depth 1 "$REPO" "$INSTALL_DIR" + info "Cloning repository to cache..." + git clone --depth 1 "$REPO" "$BUILD_DIR" fi - ok "Source: $INSTALL_DIR" + ok "Source cache: $BUILD_DIR" } install_deps() { info "Installing dependencies..." - cd "$INSTALL_DIR" + cd "$BUILD_DIR" || fail "Cannot enter $BUILD_DIR" bun install --frozen-lockfile 2>/dev/null || bun install ok "Dependencies installed" } build_binary() { - info "Building free-code (all experimental features enabled)..." - cd "$INSTALL_DIR" - bun run build:dev:full - ok "Binary built: $INSTALL_DIR/cli-dev" + info "Building claudium..." + cd "$BUILD_DIR" || fail "Cannot enter $BUILD_DIR" + bun run build:dev:claudium + local binary="$BUILD_DIR/cli-dev" + ok "Binary built: $binary" } -link_binary() { - local link_dir="$HOME/.local/bin" - mkdir -p "$link_dir" +install_binary() { + mkdir -p "$(dirname "$INSTALL_DIR")" + + cp "$BUILD_DIR/cli-dev" "$INSTALL_DIR/claudium" + chmod +x "$INSTALL_DIR/claudium" + ok "Installed: $INSTALL_DIR/claudium" - ln -sf "$INSTALL_DIR/cli-dev" "$link_dir/free-code" - ok "Symlinked: $link_dir/free-code" + rm -rf "$BUILD_DIR" + ok "Build cache cleaned" - if ! echo "$PATH" | tr ':' '\n' | grep -qx "$link_dir"; then - warn "$link_dir is not on your PATH" + if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then + warn "$INSTALL_DIR is not on your PATH" echo "" printf "${YELLOW} Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):${RESET}\n" printf "${BOLD} export PATH=\"\$HOME/.local/bin:\$PATH\"${RESET}\n" @@ -158,22 +168,24 @@ echo "" clone_repo install_deps build_binary -link_binary +install_binary echo "" printf "${GREEN}${BOLD} Installation complete!${RESET}\n" echo "" printf " ${BOLD}Run it:${RESET}\n" -printf " ${CYAN}free-code${RESET} # interactive REPL\n" -printf " ${CYAN}free-code -p \"your prompt\"${RESET} # one-shot mode\n" +printf " ${CYAN}claudium${RESET} # interactive REPL\n" +printf " ${CYAN}claudium -p \"your prompt\"${RESET} # one-shot mode\n" echo "" -printf " ${BOLD}Set your API key:${RESET}\n" +printf " ${BOLD}Set your Anthropic Messages API key:${RESET}\n" printf " ${CYAN}export ANTHROPIC_API_KEY=\"sk-ant-...\"${RESET}\n" echo "" printf " ${BOLD}Or log in with Claude.ai:${RESET}\n" -printf " ${CYAN}free-code /login${RESET}\n" +printf " ${CYAN}claudium /login${RESET}\n" +echo "" +printf " ${BOLD}Also support OpenAI Chat Completions APIs:${RESET}\n" +printf " ${CYAN}export CLAUDE_CODE_USE_OPENAI=1${RESET}\n" +printf " ${CYAN}export OPENAI_BASE_URL=http://.../v1${RESET}\n" echo "" -printf " ${DIM}Source: $INSTALL_DIR${RESET}\n" -printf " ${DIM}Binary: $INSTALL_DIR/cli-dev${RESET}\n" -printf " ${DIM}Link: ~/.local/bin/free-code${RESET}\n" +printf " ${BOLD}See README.md for full configs.${RESET}\n" echo "" diff --git a/package.json b/package.json index 425d56d7..b3541a57 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "type": "module", "packageManager": "bun@1.3.11", "bin": { - "claude": "./cli", - "claude-source": "./cli" + "claudium": "./cli", + "claudium-source": "./cli" }, "engines": { "bun": ">=1.3.11" @@ -16,6 +16,7 @@ "build": "bun run ./scripts/build.ts", "build:dev": "bun run ./scripts/build.ts --dev", "build:dev:full": "bun run ./scripts/build.ts --dev --feature-set=dev-full", + "build:dev:claudium": "bun run ./scripts/build.ts --dev --feature-set=claudium", "compile": "bun run ./scripts/build.ts --compile", "dev": "bun run ./src/entrypoints/cli.tsx" }, diff --git a/scripts/build.ts b/scripts/build.ts index 262a6e9d..303fa010 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -88,6 +88,41 @@ for (let i = 0; i < args.length; i += 1) { for (const feature of fullExperimentalFeatures) { featureSet.add(feature) } + } else if (args[i + 1] === 'claudium') { + const claudiumFeatures = [ + 'AGENT_MEMORY_SNAPSHOT', + 'AGENT_TRIGGERS', + 'AGENT_TRIGGERS_REMOTE', + 'AWAY_SUMMARY', + 'BASH_CLASSIFIER', + 'BUILTIN_EXPLORE_PLAN_AGENTS', + 'CACHED_MICROCOMPACT', + 'COMPACTION_REMINDERS', + 'EXTRACT_MEMORIES', + 'HISTORY_PICKER', + 'HOOK_PROMPTS', + 'KAIROS_BRIEF', + 'KAIROS_CHANNELS', + 'LODESTONE', + 'MCP_RICH_OUTPUT', + 'MESSAGE_ACTIONS', + 'NATIVE_CLIPBOARD_IMAGE', + 'NEW_INIT', + 'POWERSHELL_AUTO_MODE', + 'PROMPT_CACHE_BREAK_DETECTION', + 'QUICK_SEARCH', + 'SHOT_STATS', + 'TEAMMEM', + 'TOKEN_BUDGET', + 'TREE_SITTER_BASH', + 'TREE_SITTER_BASH_SHADOW', + 'UNATTENDED_RETRY', + 'VERIFICATION_AGENT', + 'VOICE_MODE', + ] + for (const feature of claudiumFeatures) { + featureSet.add(feature) + } } i += 1 continue @@ -151,7 +186,7 @@ const defines = { 'This reconstructed source snapshot does not include Anthropic internal issue routing.', ), 'MACRO.VERSION_CHANGELOG': JSON.stringify( - dev ? getVersionChangelog() : 'https://github.com/paoloanzn/claude-code', + dev ? getVersionChangelog() : 'https://github.com/DdogezD/claudium', ), } as const diff --git a/src/bridge/bridgeEnabled.ts b/src/bridge/bridgeEnabled.ts index b6eec416..a8a11ad3 100644 --- a/src/bridge/bridgeEnabled.ts +++ b/src/bridge/bridgeEnabled.ts @@ -3,7 +3,7 @@ import { checkGate_CACHED_OR_BLOCKING, getDynamicConfig_CACHED_MAY_BE_STALE, getFeatureValue_CACHED_MAY_BE_STALE, -} from '../services/analytics/growthbook.js' +} from '../services/analytics-stub.js' // Namespace import breaks the bridgeEnabled → auth → config → bridgeEnabled // cycle — authModule.foo is a live binding, so by the time the helpers below // call it, auth.js is fully loaded. Previously used require() for the same diff --git a/src/bridge/bridgeMain.ts b/src/bridge/bridgeMain.ts index 7aeacafc..6f11153f 100644 --- a/src/bridge/bridgeMain.ts +++ b/src/bridge/bridgeMain.ts @@ -3,14 +3,14 @@ import { randomUUID } from 'crypto' import { hostname, tmpdir } from 'os' import { basename, join, resolve } from 'path' import { getRemoteSessionUrl } from '../constants/product.js' -import { shutdownDatadog } from '../services/analytics/datadog.js' -import { shutdown1PEventLogging } from '../services/analytics/firstPartyEventLogger.js' -import { checkGate_CACHED_OR_BLOCKING } from '../services/analytics/growthbook.js' +import { shutdownDatadog } from '../services/analytics-stub.js' +import { shutdown1PEventLogging } from '../services/analytics-stub.js' +import { checkGate_CACHED_OR_BLOCKING } from '../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, logEventAsync, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { isInBundledMode } from '../utils/bundledMode.js' import { logForDebugging } from '../utils/debug.js' import { logForDiagnosticsNoPII } from '../utils/diagLogs.js' @@ -1529,7 +1529,7 @@ export async function runBridgeLoop( !fatalExit ) { logger.logStatus( - `Resume this session by running \`claude remote-control --continue\``, + `Resume this session by running \`claudium remote-control --continue\``, ) logForDebugging( `[bridge:shutdown] Skipping archive+deregister to allow resume of session ${initialSessionId}`, diff --git a/src/bridge/bridgeMessaging.ts b/src/bridge/bridgeMessaging.ts index 98ece039..538f5c56 100644 --- a/src/bridge/bridgeMessaging.ts +++ b/src/bridge/bridgeMessaging.ts @@ -17,7 +17,7 @@ import type { SDKControlResponse, } from '../entrypoints/sdk/controlTypes.js' import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js' -import { logEvent } from '../services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { EMPTY_USAGE } from '../services/api/emptyUsage.js' import type { Message } from '../types/message.js' import { normalizeControlMessageKeys } from '../utils/controlMessageCompat.js' diff --git a/src/bridge/debugUtils.ts b/src/bridge/debugUtils.ts index e9f7293a..39b14203 100644 --- a/src/bridge/debugUtils.ts +++ b/src/bridge/debugUtils.ts @@ -1,7 +1,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { logForDebugging } from '../utils/debug.js' import { errorMessage } from '../utils/errors.js' import { jsonStringify } from '../utils/slowOperations.js' diff --git a/src/bridge/envLessBridgeConfig.ts b/src/bridge/envLessBridgeConfig.ts index de0cb5e1..58f147e1 100644 --- a/src/bridge/envLessBridgeConfig.ts +++ b/src/bridge/envLessBridgeConfig.ts @@ -1,5 +1,5 @@ import { z } from 'zod/v4' -import { getFeatureValue_DEPRECATED } from '../services/analytics/growthbook.js' +import { getFeatureValue_DEPRECATED } from '../services/analytics-stub.js' import { lazySchema } from '../utils/lazySchema.js' import { lt } from '../utils/semver.js' import { isEnvLessBridgeEnabled } from './bridgeEnabled.js' diff --git a/src/bridge/initReplBridge.ts b/src/bridge/initReplBridge.ts index 85e403d4..5c03c05e 100644 --- a/src/bridge/initReplBridge.ts +++ b/src/bridge/initReplBridge.ts @@ -18,7 +18,7 @@ import { hostname } from 'os' import { getOriginalCwd, getSessionId } from '../bootstrap/state.js' import type { SDKMessage } from '../entrypoints/agentSdkTypes.js' import type { SDKControlResponse } from '../entrypoints/sdk/controlTypes.js' -import { getFeatureValue_CACHED_WITH_REFRESH } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_WITH_REFRESH } from '../services/analytics-stub.js' import { getOrganizationUUID } from '../services/oauth/client.js' import { isPolicyAllowed, diff --git a/src/bridge/jwtUtils.ts b/src/bridge/jwtUtils.ts index 030c001f..2f178733 100644 --- a/src/bridge/jwtUtils.ts +++ b/src/bridge/jwtUtils.ts @@ -1,4 +1,4 @@ -import { logEvent } from '../services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { logForDebugging } from '../utils/debug.js' import { logForDiagnosticsNoPII } from '../utils/diagLogs.js' import { errorMessage } from '../utils/errors.js' diff --git a/src/bridge/pollConfig.ts b/src/bridge/pollConfig.ts index 024b4761..fa783ba5 100644 --- a/src/bridge/pollConfig.ts +++ b/src/bridge/pollConfig.ts @@ -1,5 +1,5 @@ import { z } from 'zod/v4' -import { getFeatureValue_CACHED_WITH_REFRESH } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_WITH_REFRESH } from '../services/analytics-stub.js' import { lazySchema } from '../utils/lazySchema.js' import { DEFAULT_POLL_CONFIG, diff --git a/src/bridge/remoteBridgeCore.ts b/src/bridge/remoteBridgeCore.ts index 76545f6e..2f27598d 100644 --- a/src/bridge/remoteBridgeCore.ts +++ b/src/bridge/remoteBridgeCore.ts @@ -61,7 +61,7 @@ import { registerCleanup } from '../utils/cleanupRegistry.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import type { ReplBridgeHandle, BridgeState } from './replBridge.js' import type { Message } from '../types/message.js' import type { SDKMessage } from '../entrypoints/agentSdkTypes.js' diff --git a/src/bridge/replBridge.ts b/src/bridge/replBridge.ts index 7d7ac6a3..6506db09 100644 --- a/src/bridge/replBridge.ts +++ b/src/bridge/replBridge.ts @@ -12,7 +12,7 @@ import { logForDiagnosticsNoPII } from '../utils/diagLogs.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { registerCleanup } from '../utils/cleanupRegistry.js' import { handleIngressMessage, diff --git a/src/bridge/trustedDevice.ts b/src/bridge/trustedDevice.ts index a4bcf35b..74decc90 100644 --- a/src/bridge/trustedDevice.ts +++ b/src/bridge/trustedDevice.ts @@ -5,7 +5,7 @@ import { getOauthConfig } from '../constants/oauth.js' import { checkGate_CACHED_OR_BLOCKING, getFeatureValue_CACHED_MAY_BE_STALE, -} from '../services/analytics/growthbook.js' +} from '../services/analytics-stub.js' import { logForDebugging } from '../utils/debug.js' import { errorMessage } from '../utils/errors.js' import { isEssentialTrafficOnly } from '../utils/privacyLevel.js' diff --git a/src/cli/handlers/auth.ts b/src/cli/handlers/auth.ts index c4cba5d0..be9c32de 100644 --- a/src/cli/handlers/auth.ts +++ b/src/cli/handlers/auth.ts @@ -7,7 +7,7 @@ import { import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { getSSLErrorHint } from '../../services/api/errorUtils.js' import { fetchAndStoreClaudeCodeFirstTokenDate } from '../../services/api/firstTokenDate.js' import { diff --git a/src/cli/handlers/mcp.tsx b/src/cli/handlers/mcp.tsx index e530c268..cae41a53 100644 --- a/src/cli/handlers/mcp.tsx +++ b/src/cli/handlers/mcp.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopImportDialog.js'; import { render } from '../../ink.js'; import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { clearMcpClientConfig, clearServerTokensFromLocalStorage, getMcpClientConfig, readClientSecret, saveMcpClientSecret } from '../../services/mcp/auth.js'; import { connectToServer, getMcpServerConnectionBatchSize } from '../../services/mcp/client.js'; import { addMcpConfig, getAllMcpConfigs, getMcpConfigByName, getMcpConfigsByScope, removeMcpConfig } from '../../services/mcp/config.js'; diff --git a/src/cli/handlers/plugins.ts b/src/cli/handlers/plugins.ts index 9236abe0..bbbb3301 100644 --- a/src/cli/handlers/plugins.ts +++ b/src/cli/handlers/plugins.ts @@ -10,7 +10,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { disableAllPlugins, disablePlugin, diff --git a/src/cli/handlers/util.tsx b/src/cli/handlers/util.tsx index 03ff3cd7..47180bde 100644 --- a/src/cli/handlers/util.tsx +++ b/src/cli/handlers/util.tsx @@ -12,7 +12,7 @@ import { useManagePlugins } from '../../hooks/useManagePlugins.js'; import type { Root } from '../../ink.js'; import { Box, Text } from '../../ink.js'; import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { MCPConnectionManager } from '../../services/mcp/MCPConnectionManager.js'; import { AppStateProvider } from '../../state/AppState.js'; import { onChangeAppState } from '../../state/onChangeAppState.js'; diff --git a/src/cli/print.ts b/src/cli/print.ts index 60472575..bcfe291d 100644 --- a/src/cli/print.ts +++ b/src/cli/print.ts @@ -25,8 +25,8 @@ import { mergeAndFilterTools } from 'src/utils/toolPool.js' import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, -} from 'src/services/analytics/index.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' +} from '../services/analytics-stub.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { logForDebugging } from 'src/utils/debug.js' import { logForDiagnosticsNoPII, @@ -107,6 +107,7 @@ import { } from 'src/utils/gracefulShutdown.js' import { registerCleanup } from 'src/utils/cleanupRegistry.js' import { createIdleTimeoutManager } from 'src/utils/idleTimeout.js' +import { startMemoryPressureAdvisor } from 'src/utils/memoryPressureAdvisor.js' import type { SDKStatus, ModelInfo, @@ -348,7 +349,7 @@ import { getRunningTasks } from '../utils/task/framework.js' import { isBackgroundTask } from '../tasks/types.js' import { stopTask } from '../tasks/stopTask.js' import { drainSdkEvents } from '../utils/sdkEventQueue.js' -import { initializeGrowthBook } from '../services/analytics/growthbook.js' +import { initializeGrowthBook } from '../services/analytics-stub.js' import { errorMessage, toError } from '../utils/errors.js' import { sleep } from '../utils/sleep.js' import { isExtractModeActive } from '../memdir/paths.js' @@ -544,10 +545,11 @@ export async function runHeadless( proactiveModule.activateProactive('command') } - // Periodically force a full GC to keep memory usage in check + // Adaptive memory pressure monitoring + GC + // Replaces blind fixed-interval GC with RSS-based state machine + // to prevent GC death spiral (madvise/mmap/munmap storms) if (typeof Bun !== 'undefined') { - const gcTimer = setInterval(Bun.gc, 1000) - gcTimer.unref() + startMemoryPressureAdvisor() } // Start headless profiler for first turn diff --git a/src/cli/transports/WebSocketTransport.ts b/src/cli/transports/WebSocketTransport.ts index f8e27ace..e5312d6a 100644 --- a/src/cli/transports/WebSocketTransport.ts +++ b/src/cli/transports/WebSocketTransport.ts @@ -1,6 +1,6 @@ import type { StdoutMessage } from 'src/entrypoints/sdk/controlTypes.js' import type WsWebSocket from 'ws' -import { logEvent } from '../../services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import { CircularBuffer } from '../../utils/CircularBuffer.js' import { logForDebugging } from '../../utils/debug.js' import { logForDiagnosticsNoPII } from '../../utils/diagLogs.js' diff --git a/src/cli/update.ts b/src/cli/update.ts index a0cd35f5..ef080902 100644 --- a/src/cli/update.ts +++ b/src/cli/update.ts @@ -1,5 +1,5 @@ import chalk from 'chalk' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { getLatestVersion, type InstallStatus, diff --git a/src/commands.ts b/src/commands.ts index 9e4bf810..6ec9601e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -148,7 +148,6 @@ import antTrace from './commands/ant-trace/index.js' import perfIssue from './commands/perf-issue/index.js' import sandboxToggle from './commands/sandbox-toggle/index.js' import chrome from './commands/chrome/index.js' -import stickers from './commands/stickers/index.js' import advisor from './commands/advisor.js' import { logError } from './utils/log.js' import { toError } from './utils/errors.js' @@ -300,7 +299,6 @@ const COMMANDS = memoize((): Command[] => [ stats, status, statusline, - stickers, tag, theme, feedback, @@ -632,7 +630,6 @@ export const REMOTE_SAFE_COMMANDS: Set = new Set([ plan, // Plan mode toggle keybindings, // Keybinding management statusline, // Status line toggle - stickers, // Stickers mobile, // Mobile QR code ]) diff --git a/src/commands/branch/branch.ts b/src/commands/branch/branch.ts index 4a7c2779..1e88f9a3 100644 --- a/src/commands/branch/branch.ts +++ b/src/commands/branch/branch.ts @@ -2,7 +2,7 @@ import { randomUUID, type UUID } from 'crypto' import { mkdir, readFile, writeFile } from 'fs/promises' import { getOriginalCwd, getSessionId } from '../../bootstrap/state.js' import type { LocalJSXCommandContext } from '../../commands.js' -import { logEvent } from '../../services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import type { LocalJSXCommandOnDone } from '../../types/command.js' import type { ContentReplacementEntry, diff --git a/src/commands/bridge/bridge.tsx b/src/commands/bridge/bridge.tsx index 02ca16b4..476c0e21 100644 --- a/src/commands/bridge/bridge.tsx +++ b/src/commands/bridge/bridge.tsx @@ -13,7 +13,7 @@ import { shouldShowRemoteCallout } from '../../components/RemoteCallout.js'; import { useRegisterOverlay } from '../../context/overlayContext.js'; import { Box, Text } from '../../ink.js'; import { useKeybindings } from '../../keybindings/useKeybinding.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { useAppState, useSetAppState } from '../../state/AppState.js'; import type { ToolUseContext } from '../../Tool.js'; import type { LocalJSXCommandContext, LocalJSXCommandOnDone } from '../../types/command.js'; diff --git a/src/commands/brief.ts b/src/commands/brief.ts index d37ffd0a..d31fc76f 100644 --- a/src/commands/brief.ts +++ b/src/commands/brief.ts @@ -1,11 +1,11 @@ import { feature } from 'bun:bundle' import { z } from 'zod/v4' import { getKairosActive, setUserMsgOptIn } from '../bootstrap/state.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import type { ToolUseContext } from '../Tool.js' import { isBriefEntitled } from '../tools/BriefTool/BriefTool.js' import { BRIEF_TOOL_NAME } from '../tools/BriefTool/prompt.js' diff --git a/src/commands/clear/conversation.ts b/src/commands/clear/conversation.ts index e528b56e..acc68671 100644 --- a/src/commands/clear/conversation.ts +++ b/src/commands/clear/conversation.ts @@ -13,7 +13,7 @@ import { import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import type { AppState } from '../../state/AppState.js' import { isInProcessTeammateTask } from '../../tasks/InProcessTeammateTask/types.js' import { diff --git a/src/commands/copy/copy.tsx b/src/commands/copy/copy.tsx index 5853d165..2fb374d4 100644 --- a/src/commands/copy/copy.tsx +++ b/src/commands/copy/copy.tsx @@ -14,7 +14,7 @@ import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; import { stringWidth } from '../../ink/stringWidth.js'; import { setClipboard } from '../../ink/termio/osc.js'; import { Box, Text } from '../../ink.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import type { LocalJSXCommandCall } from '../../types/command.js'; import type { AssistantMessage, Message } from '../../types/message.js'; import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; diff --git a/src/commands/effort/effort.tsx b/src/commands/effort/effort.tsx index 41dd0d85..ef1a6cea 100644 --- a/src/commands/effort/effort.tsx +++ b/src/commands/effort/effort.tsx @@ -1,7 +1,7 @@ import { c as _c } from "react/compiler-runtime"; import * as React from 'react'; import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { useAppState, useSetAppState } from '../../state/AppState.js'; import type { LocalJSXCommandOnDone } from '../../types/command.js'; import { type EffortValue, getDisplayedEffortLevel, getEffortEnvOverride, getEffortValueDescription, isEffortLevel, toPersistableEffort } from '../../utils/effort.js'; diff --git a/src/commands/fast/fast.tsx b/src/commands/fast/fast.tsx index 398c3c34..b37b8ec1 100644 --- a/src/commands/fast/fast.tsx +++ b/src/commands/fast/fast.tsx @@ -6,7 +6,7 @@ import { Dialog } from '../../components/design-system/Dialog.js'; import { FastIcon, getFastIconString } from '../../components/FastIcon.js'; import { Box, Link, Text } from '../../ink.js'; import { useKeybindings } from '../../keybindings/useKeybinding.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { type AppState, useAppState, useSetAppState } from '../../state/AppState.js'; import type { LocalJSXCommandOnDone } from '../../types/command.js'; import { clearFastModeCooldown, FAST_MODE_MODEL_DISPLAY, getFastModeModel, getFastModeRuntimeState, getFastModeUnavailableReason, isFastModeEnabled, isFastModeSupportedByModel, prefetchFastModeStatus } from '../../utils/fastMode.js'; diff --git a/src/commands/hooks/hooks.tsx b/src/commands/hooks/hooks.tsx index c3994543..97a99782 100644 --- a/src/commands/hooks/hooks.tsx +++ b/src/commands/hooks/hooks.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { HooksConfigMenu } from '../../components/hooks/HooksConfigMenu.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { getTools } from '../../tools.js'; import type { LocalJSXCommandCall } from '../../types/command.js'; export const call: LocalJSXCommandCall = async (onDone, context) => { diff --git a/src/commands/ide/ide.tsx b/src/commands/ide/ide.tsx index 0a41b977..cf1ad3ef 100644 --- a/src/commands/ide/ide.tsx +++ b/src/commands/ide/ide.tsx @@ -2,7 +2,7 @@ import { c as _c } from "react/compiler-runtime"; import chalk from 'chalk'; import * as path from 'path'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import type { CommandResultDisplay, LocalJSXCommandContext } from '../../commands.js'; import { Select } from '../../components/CustomSelect/index.js'; import { Dialog } from '../../components/design-system/Dialog.js'; diff --git a/src/commands/install-github-app/OAuthFlowStep.tsx b/src/commands/install-github-app/OAuthFlowStep.tsx index cbefa7bb..b185583b 100644 --- a/src/commands/install-github-app/OAuthFlowStep.tsx +++ b/src/commands/install-github-app/OAuthFlowStep.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'; import { Spinner } from '../../components/Spinner.js'; import TextInput from '../../components/TextInput.js'; diff --git a/src/commands/install-github-app/install-github-app.tsx b/src/commands/install-github-app/install-github-app.tsx index a4faf95d..8e664151 100644 --- a/src/commands/install-github-app/install-github-app.tsx +++ b/src/commands/install-github-app/install-github-app.tsx @@ -1,6 +1,6 @@ import { execa } from 'execa'; import React, { useCallback, useState } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { WorkflowMultiselectDialog } from '../../components/WorkflowMultiselectDialog.js'; import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'; import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; diff --git a/src/commands/install-github-app/setupGitHubActions.ts b/src/commands/install-github-app/setupGitHubActions.ts index 09b24539..87147438 100644 --- a/src/commands/install-github-app/setupGitHubActions.ts +++ b/src/commands/install-github-app/setupGitHubActions.ts @@ -1,7 +1,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../../services/analytics-stub.js' import { saveGlobalConfig } from 'src/utils/config.js' import { CODE_REVIEW_PLUGIN_WORKFLOW_CONTENT, diff --git a/src/commands/install-slack-app/install-slack-app.ts b/src/commands/install-slack-app/install-slack-app.ts index 6980cede..95fabc06 100644 --- a/src/commands/install-slack-app/install-slack-app.ts +++ b/src/commands/install-slack-app/install-slack-app.ts @@ -1,5 +1,5 @@ import type { LocalCommandResult } from '../../commands.js' -import { logEvent } from '../../services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import { openBrowser } from '../../utils/browser.js' import { saveGlobalConfig } from '../../utils/config.js' diff --git a/src/commands/install.tsx b/src/commands/install.tsx index cba6949c..38165f85 100644 --- a/src/commands/install.tsx +++ b/src/commands/install.tsx @@ -3,7 +3,7 @@ import { homedir } from 'node:os'; import { join } from 'node:path'; import React, { useEffect, useState } from 'react'; import type { CommandResultDisplay } from 'src/commands.js'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import { StatusIcon } from '../components/design-system/StatusIcon.js'; import { Box, render, Text } from '../ink.js'; import { logForDebugging } from '../utils/debug.js'; diff --git a/src/commands/login/login.tsx b/src/commands/login/login.tsx index b532534d..280f742b 100644 --- a/src/commands/login/login.tsx +++ b/src/commands/login/login.tsx @@ -9,7 +9,7 @@ import { ConsoleOAuthFlow } from '../../components/ConsoleOAuthFlow.js'; import { Dialog } from '../../components/design-system/Dialog.js'; import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; import { Text } from '../../ink.js'; -import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'; +import { refreshGrowthBookAfterAuthChange } from '../../services/analytics-stub.js'; import { refreshPolicyLimits } from '../../services/policyLimits/index.js'; import { refreshRemoteManagedSettings } from '../../services/remoteManagedSettings/index.js'; import type { LocalJSXCommandOnDone } from '../../types/command.js'; diff --git a/src/commands/logout/logout.tsx b/src/commands/logout/logout.tsx index ddff29a6..0bd24af1 100644 --- a/src/commands/logout/logout.tsx +++ b/src/commands/logout/logout.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { clearTrustedDeviceTokenCache } from '../../bridge/trustedDevice.js'; import { Text } from '../../ink.js'; -import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'; +import { refreshGrowthBookAfterAuthChange } from '../../services/analytics-stub.js'; import { getGroveNoticeConfig, getGroveSettings } from '../../services/api/grove.js'; import { clearPolicyLimitsCache } from '../../services/policyLimits/index.js'; // flushTelemetry is loaded lazily to avoid pulling in ~1.1MB of OpenTelemetry at startup @@ -19,7 +19,7 @@ export async function performLogout({ // Flush telemetry BEFORE clearing credentials to prevent org data leakage const { flushTelemetry - } = await import('../../utils/telemetry/instrumentation.js'); + } = await import('../../utils/telemetry-stub.js'); await flushTelemetry(); await removeApiKey(); diff --git a/src/commands/mcp/addCommand.ts b/src/commands/mcp/addCommand.ts index aeb3bc05..51c10c5f 100644 --- a/src/commands/mcp/addCommand.ts +++ b/src/commands/mcp/addCommand.ts @@ -8,7 +8,7 @@ import { cliError, cliOk } from '../../cli/exit.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { readClientSecret, saveMcpClientSecret, diff --git a/src/commands/model/model.tsx b/src/commands/model/model.tsx index 65c084d9..6b900916 100644 --- a/src/commands/model/model.tsx +++ b/src/commands/model/model.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import type { CommandResultDisplay } from '../../commands.js'; import { ModelPicker } from '../../components/ModelPicker.js'; import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { useAppState, useSetAppState } from '../../state/AppState.js'; import type { LocalJSXCommandCall } from '../../types/command.js'; import type { EffortLevel } from '../../utils/effort.js'; diff --git a/src/commands/passes/passes.tsx b/src/commands/passes/passes.tsx index c17cb594..42444800 100644 --- a/src/commands/passes/passes.tsx +++ b/src/commands/passes/passes.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Passes } from '../../components/Passes/Passes.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { getCachedRemainingPasses } from '../../services/api/referral.js'; import type { LocalJSXCommandOnDone } from '../../types/command.js'; import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; diff --git a/src/commands/plugin/AddMarketplace.tsx b/src/commands/plugin/AddMarketplace.tsx index 79b8188d..9bd0cfc0 100644 --- a/src/commands/plugin/AddMarketplace.tsx +++ b/src/commands/plugin/AddMarketplace.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'; import { Byline } from '../../components/design-system/Byline.js'; import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'; diff --git a/src/commands/plugin/ManageMarketplaces.tsx b/src/commands/plugin/ManageMarketplaces.tsx index 0e5d3ccf..efdd8559 100644 --- a/src/commands/plugin/ManageMarketplaces.tsx +++ b/src/commands/plugin/ManageMarketplaces.tsx @@ -2,7 +2,7 @@ import { c as _c } from "react/compiler-runtime"; import figures from 'figures'; import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'; import { Byline } from '../../components/design-system/Byline.js'; import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'; diff --git a/src/commands/privacy-settings/privacy-settings.tsx b/src/commands/privacy-settings/privacy-settings.tsx index 4e4acf67..9413bc16 100644 --- a/src/commands/privacy-settings/privacy-settings.tsx +++ b/src/commands/privacy-settings/privacy-settings.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { type GroveDecision, GroveDialog, PrivacySettingsDialog } from '../../components/grove/Grove.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { getGroveNoticeConfig, getGroveSettings, isQualifiedForGrove } from '../../services/api/grove.js'; import type { LocalJSXCommandOnDone } from '../../types/command.js'; const FALLBACK_MESSAGE = 'Review and manage your privacy settings at https://claude.ai/settings/data-privacy-controls'; diff --git a/src/commands/rate-limit-options/rate-limit-options.tsx b/src/commands/rate-limit-options/rate-limit-options.tsx index 296ca4b8..055075b2 100644 --- a/src/commands/rate-limit-options/rate-limit-options.tsx +++ b/src/commands/rate-limit-options/rate-limit-options.tsx @@ -3,8 +3,8 @@ import React, { useMemo, useState } from 'react'; import type { CommandResultDisplay, LocalJSXCommandContext } from '../../commands.js'; import { type OptionWithDescription, Select } from '../../components/CustomSelect/select.js'; import { Dialog } from '../../components/design-system/Dialog.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'; import type { ToolUseContext } from '../../Tool.js'; import type { LocalJSXCommandOnDone } from '../../types/command.js'; diff --git a/src/commands/remote-setup/index.ts b/src/commands/remote-setup/index.ts index 7b291df1..bc368ab9 100644 --- a/src/commands/remote-setup/index.ts +++ b/src/commands/remote-setup/index.ts @@ -1,5 +1,5 @@ import type { Command } from '../../commands.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { isPolicyAllowed } from '../../services/policyLimits/index.js' const web = { diff --git a/src/commands/remote-setup/remote-setup.tsx b/src/commands/remote-setup/remote-setup.tsx index 00928795..9f821679 100644 --- a/src/commands/remote-setup/remote-setup.tsx +++ b/src/commands/remote-setup/remote-setup.tsx @@ -5,7 +5,7 @@ import { Select } from '../../components/CustomSelect/index.js'; import { Dialog } from '../../components/design-system/Dialog.js'; import { LoadingState } from '../../components/design-system/LoadingState.js'; import { Box, Text } from '../../ink.js'; -import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString } from '../../services/analytics/index.js'; +import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString } from '../../services/analytics-stub.js'; import type { LocalJSXCommandOnDone } from '../../types/command.js'; import { openBrowser } from '../../utils/browser.js'; import { getGhAuthStatus } from '../../utils/github/ghAuthStatus.js'; diff --git a/src/commands/review/reviewRemote.ts b/src/commands/review/reviewRemote.ts index 0b80e339..b523c3e2 100644 --- a/src/commands/review/reviewRemote.ts +++ b/src/commands/review/reviewRemote.ts @@ -10,11 +10,11 @@ */ import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { fetchUltrareviewQuota } from '../../services/api/ultrareviewQuota.js' import { fetchUtilization } from '../../services/api/usage.js' import type { ToolUseContext } from '../../Tool.js' diff --git a/src/commands/review/ultrareviewEnabled.ts b/src/commands/review/ultrareviewEnabled.ts index d10e5f5f..4b172fdc 100644 --- a/src/commands/review/ultrareviewEnabled.ts +++ b/src/commands/review/ultrareviewEnabled.ts @@ -1,4 +1,4 @@ -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' /** * Runtime gate for /ultrareview. GB config's `enabled` field controls diff --git a/src/commands/stickers/index.ts b/src/commands/stickers/index.ts deleted file mode 100644 index ebca4530..00000000 --- a/src/commands/stickers/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Command } from '../../commands.js' - -const stickers = { - type: 'local', - name: 'stickers', - description: 'Order Claude Code stickers', - supportsNonInteractive: false, - load: () => import('./stickers.js'), -} satisfies Command - -export default stickers diff --git a/src/commands/stickers/stickers.ts b/src/commands/stickers/stickers.ts deleted file mode 100644 index ede51931..00000000 --- a/src/commands/stickers/stickers.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { LocalCommandResult } from '../../types/command.js' -import { openBrowser } from '../../utils/browser.js' - -export async function call(): Promise { - const url = 'https://www.stickermule.com/claudecode' - const success = await openBrowser(url) - - if (success) { - return { type: 'text', value: 'Opening sticker page in browser…' } - } else { - return { - type: 'text', - value: `Failed to open browser. Visit: ${url}`, - } - } -} diff --git a/src/commands/tag/tag.tsx b/src/commands/tag/tag.tsx index 3809a995..606e749d 100644 --- a/src/commands/tag/tag.tsx +++ b/src/commands/tag/tag.tsx @@ -8,7 +8,7 @@ import { Select } from '../../components/CustomSelect/select.js'; import { Dialog } from '../../components/design-system/Dialog.js'; import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'; import { Box, Text } from '../../ink.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import type { LocalJSXCommandOnDone } from '../../types/command.js'; import { recursivelySanitizeUnicode } from '../../utils/sanitization.js'; import { getCurrentSessionTag, getTranscriptPath, saveTag } from '../../utils/sessionStorage.js'; diff --git a/src/commands/thinkback-play/index.ts b/src/commands/thinkback-play/index.ts index efa4bf7e..83837d1a 100644 --- a/src/commands/thinkback-play/index.ts +++ b/src/commands/thinkback-play/index.ts @@ -1,5 +1,5 @@ import type { Command } from '../../commands.js' -import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' // Hidden command that just plays the animation // Called by the thinkback skill after generation is complete diff --git a/src/commands/thinkback/index.ts b/src/commands/thinkback/index.ts index eceddde8..907c0420 100644 --- a/src/commands/thinkback/index.ts +++ b/src/commands/thinkback/index.ts @@ -1,5 +1,5 @@ import type { Command } from '../../commands.js' -import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' const thinkback = { type: 'local-jsx', diff --git a/src/commands/ultraplan.tsx b/src/commands/ultraplan.tsx index e492c10b..6cac8d3f 100644 --- a/src/commands/ultraplan.tsx +++ b/src/commands/ultraplan.tsx @@ -3,8 +3,8 @@ import { REMOTE_CONTROL_DISCONNECTED_MSG } from '../bridge/types.js'; import type { Command } from '../commands.js'; import { DIAMOND_OPEN } from '../constants/figures.js'; import { getRemoteSessionUrl } from '../constants/product.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics/index.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; import type { AppState } from '../state/AppStateStore.js'; import { checkRemoteAgentEligibility, formatPreconditionError, RemoteAgentTask, type RemoteAgentTaskState, registerRemoteAgentTask } from '../tasks/RemoteAgentTask/RemoteAgentTask.js'; import type { LocalJSXCommandCall } from '../types/command.js'; diff --git a/src/commands/vim/vim.ts b/src/commands/vim/vim.ts index de5fd99e..a250da11 100644 --- a/src/commands/vim/vim.ts +++ b/src/commands/vim/vim.ts @@ -1,7 +1,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import type { LocalCommandCall } from '../../types/command.js' import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' diff --git a/src/commands/voice/voice.ts b/src/commands/voice/voice.ts index f369891b..7096881f 100644 --- a/src/commands/voice/voice.ts +++ b/src/commands/voice/voice.ts @@ -1,6 +1,6 @@ import { normalizeLanguageForSTT } from '../../hooks/useVoice.js' import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js' -import { logEvent } from '../../services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import type { LocalCommandCall } from '../../types/command.js' import { isAnthropicAuthEnabled } from '../../utils/auth.js' import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' diff --git a/src/components/AutoModeOptInDialog.tsx b/src/components/AutoModeOptInDialog.tsx index 07fbf03f..8c45d6c8 100644 --- a/src/components/AutoModeOptInDialog.tsx +++ b/src/components/AutoModeOptInDialog.tsx @@ -1,6 +1,6 @@ import { c as _c } from "react/compiler-runtime"; import React from 'react'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import { Box, Link, Text } from '../ink.js'; import { updateSettingsForSource } from '../utils/settings/settings.js'; import { Select } from './CustomSelect/index.js'; diff --git a/src/components/AutoUpdater.tsx b/src/components/AutoUpdater.tsx index 144e2c8a..59ac268c 100644 --- a/src/components/AutoUpdater.tsx +++ b/src/components/AutoUpdater.tsx @@ -1,198 +1,20 @@ +/** + * AutoUpdater stub for claude-code-clean + * All auto-update functionality disabled + */ + import * as React from 'react'; -import { useEffect, useRef, useState } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; -import { useInterval } from 'usehooks-ts'; -import { useUpdateNotification } from '../hooks/useUpdateNotification.js'; -import { Box, Text } from '../ink.js'; -import { type AutoUpdaterResult, getLatestVersion, getMaxVersion, type InstallStatus, installGlobalPackage, shouldSkipVersion } from '../utils/autoUpdater.js'; -import { getGlobalConfig, isAutoUpdaterDisabled } from '../utils/config.js'; -import { logForDebugging } from '../utils/debug.js'; -import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js'; -import { installOrUpdateClaudePackage, localInstallationExists } from '../utils/localInstaller.js'; -import { removeInstalledSymlink } from '../utils/nativeInstaller/index.js'; -import { gt, gte } from '../utils/semver.js'; -import { getInitialSettings } from '../utils/settings/settings.js'; + type Props = { isUpdating: boolean; onChangeIsUpdating: (isUpdating: boolean) => void; - onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void; - autoUpdaterResult: AutoUpdaterResult | null; + onAutoUpdaterResult: (autoUpdaterResult: any) => void; + autoUpdaterResult: any | null; showSuccessMessage: boolean; verbose: boolean; }; -export function AutoUpdater({ - isUpdating, - onChangeIsUpdating, - onAutoUpdaterResult, - autoUpdaterResult, - showSuccessMessage, - verbose -}: Props): React.ReactNode { - const [versions, setVersions] = useState<{ - global?: string | null; - latest?: string | null; - }>({}); - const [hasLocalInstall, setHasLocalInstall] = useState(false); - const updateSemver = useUpdateNotification(autoUpdaterResult?.version); - useEffect(() => { - void localInstallationExists().then(setHasLocalInstall); - }, []); - - // Track latest isUpdating value in a ref so the memoized checkForUpdates - // callback always sees the current value. Without this, the 30-minute - // interval fires with a stale closure where isUpdating is false, allowing - // a concurrent installGlobalPackage() to run while one is already in - // progress. - const isUpdatingRef = useRef(isUpdating); - isUpdatingRef.current = isUpdating; - const checkForUpdates = React.useCallback(async () => { - if (isUpdatingRef.current) { - return; - } - if ("production" === 'test' || "production" === 'development') { - logForDebugging('AutoUpdater: Skipping update check in test/dev environment'); - return; - } - const currentVersion = MACRO.VERSION; - const channel = getInitialSettings()?.autoUpdatesChannel ?? 'latest'; - let latestVersion = await getLatestVersion(channel); - const isDisabled = isAutoUpdaterDisabled(); - - // Check if max version is set (server-side kill switch for auto-updates) - const maxVersion = await getMaxVersion(); - if (maxVersion && latestVersion && gt(latestVersion, maxVersion)) { - logForDebugging(`AutoUpdater: maxVersion ${maxVersion} is set, capping update from ${latestVersion} to ${maxVersion}`); - if (gte(currentVersion, maxVersion)) { - logForDebugging(`AutoUpdater: current version ${currentVersion} is already at or above maxVersion ${maxVersion}, skipping update`); - setVersions({ - global: currentVersion, - latest: latestVersion - }); - return; - } - latestVersion = maxVersion; - } - setVersions({ - global: currentVersion, - latest: latestVersion - }); - - // Check if update needed and perform update - if (!isDisabled && currentVersion && latestVersion && !gte(currentVersion, latestVersion) && !shouldSkipVersion(latestVersion)) { - const startTime = Date.now(); - onChangeIsUpdating(true); - - // Remove native installer symlink since we're using JS-based updates - // But only if user hasn't migrated to native installation - const config = getGlobalConfig(); - if (config.installMethod !== 'native') { - await removeInstalledSymlink(); - } - - // Detect actual running installation type - const installationType = await getCurrentInstallationType(); - logForDebugging(`AutoUpdater: Detected installation type: ${installationType}`); - - // Skip update for development builds - if (installationType === 'development') { - logForDebugging('AutoUpdater: Cannot auto-update development build'); - onChangeIsUpdating(false); - return; - } - - // Choose the appropriate update method based on what's actually running - let installStatus: InstallStatus; - let updateMethod: 'local' | 'global'; - if (installationType === 'npm-local') { - // Use local update for local installations - logForDebugging('AutoUpdater: Using local update method'); - updateMethod = 'local'; - installStatus = await installOrUpdateClaudePackage(channel); - } else if (installationType === 'npm-global') { - // Use global update for global installations - logForDebugging('AutoUpdater: Using global update method'); - updateMethod = 'global'; - installStatus = await installGlobalPackage(); - } else if (installationType === 'native') { - // This shouldn't happen - native should use NativeAutoUpdater - logForDebugging('AutoUpdater: Unexpected native installation in non-native updater'); - onChangeIsUpdating(false); - return; - } else { - // Fallback to config-based detection for unknown types - logForDebugging(`AutoUpdater: Unknown installation type, falling back to config`); - const isMigrated = config.installMethod === 'local'; - updateMethod = isMigrated ? 'local' : 'global'; - if (isMigrated) { - installStatus = await installOrUpdateClaudePackage(channel); - } else { - installStatus = await installGlobalPackage(); - } - } - onChangeIsUpdating(false); - if (installStatus === 'success') { - logEvent('tengu_auto_updater_success', { - fromVersion: currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - toVersion: latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - durationMs: Date.now() - startTime, - wasMigrated: updateMethod === 'local', - installationType: installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS - }); - } else { - logEvent('tengu_auto_updater_fail', { - fromVersion: currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - attemptedVersion: latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - status: installStatus as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - durationMs: Date.now() - startTime, - wasMigrated: updateMethod === 'local', - installationType: installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS - }); - } - onAutoUpdaterResult({ - version: latestVersion, - status: installStatus - }); - } - // isUpdating intentionally omitted from deps; we read isUpdatingRef - // instead so the guard is always current without changing callback - // identity (which would re-trigger the initial-check useEffect below). - // eslint-disable-next-line react-hooks/exhaustive-deps - // biome-ignore lint/correctness/useExhaustiveDependencies: isUpdating read via ref - }, [onAutoUpdaterResult]); - - // Initial check - useEffect(() => { - void checkForUpdates(); - }, [checkForUpdates]); - // Check every 30 minutes - useInterval(checkForUpdates, 30 * 60 * 1000); - if (!autoUpdaterResult?.version && (!versions.global || !versions.latest)) { - return null; - } - if (!autoUpdaterResult?.version && !isUpdating) { - return null; - } - return - {verbose && - globalVersion: {versions.global} · latestVersion:{' '} - {versions.latest} - } - {isUpdating ? <> - - - Auto-updating… - - - : autoUpdaterResult?.status === 'success' && showSuccessMessage && updateSemver && - ✓ Update installed · Restart to apply - } - {(autoUpdaterResult?.status === 'install_failed' || autoUpdaterResult?.status === 'no_permissions') && - ✗ Auto-update failed · Try claude doctor or{' '} - - {hasLocalInstall ? `cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}` : `npm i -g ${MACRO.PACKAGE_URL}`} - - } - ; +export function AutoUpdater(props: Props): React.ReactNode { + // No-op: Auto-updates disabled in claude-code-clean + return null; } -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZUVmZmVjdCIsInVzZVJlZiIsInVzZVN0YXRlIiwiQW5hbHl0aWNzTWV0YWRhdGFfSV9WRVJJRklFRF9USElTX0lTX05PVF9DT0RFX09SX0ZJTEVQQVRIUyIsImxvZ0V2ZW50IiwidXNlSW50ZXJ2YWwiLCJ1c2VVcGRhdGVOb3RpZmljYXRpb24iLCJCb3giLCJUZXh0IiwiQXV0b1VwZGF0ZXJSZXN1bHQiLCJnZXRMYXRlc3RWZXJzaW9uIiwiZ2V0TWF4VmVyc2lvbiIsIkluc3RhbGxTdGF0dXMiLCJpbnN0YWxsR2xvYmFsUGFja2FnZSIsInNob3VsZFNraXBWZXJzaW9uIiwiZ2V0R2xvYmFsQ29uZmlnIiwiaXNBdXRvVXBkYXRlckRpc2FibGVkIiwibG9nRm9yRGVidWdnaW5nIiwiZ2V0Q3VycmVudEluc3RhbGxhdGlvblR5cGUiLCJpbnN0YWxsT3JVcGRhdGVDbGF1ZGVQYWNrYWdlIiwibG9jYWxJbnN0YWxsYXRpb25FeGlzdHMiLCJyZW1vdmVJbnN0YWxsZWRTeW1saW5rIiwiZ3QiLCJndGUiLCJnZXRJbml0aWFsU2V0dGluZ3MiLCJQcm9wcyIsImlzVXBkYXRpbmciLCJvbkNoYW5nZUlzVXBkYXRpbmciLCJvbkF1dG9VcGRhdGVyUmVzdWx0IiwiYXV0b1VwZGF0ZXJSZXN1bHQiLCJzaG93U3VjY2Vzc01lc3NhZ2UiLCJ2ZXJib3NlIiwiQXV0b1VwZGF0ZXIiLCJSZWFjdE5vZGUiLCJ2ZXJzaW9ucyIsInNldFZlcnNpb25zIiwiZ2xvYmFsIiwibGF0ZXN0IiwiaGFzTG9jYWxJbnN0YWxsIiwic2V0SGFzTG9jYWxJbnN0YWxsIiwidXBkYXRlU2VtdmVyIiwidmVyc2lvbiIsInRoZW4iLCJpc1VwZGF0aW5nUmVmIiwiY3VycmVudCIsImNoZWNrRm9yVXBkYXRlcyIsInVzZUNhbGxiYWNrIiwiY3VycmVudFZlcnNpb24iLCJNQUNSTyIsIlZFUlNJT04iLCJjaGFubmVsIiwiYXV0b1VwZGF0ZXNDaGFubmVsIiwibGF0ZXN0VmVyc2lvbiIsImlzRGlzYWJsZWQiLCJtYXhWZXJzaW9uIiwic3RhcnRUaW1lIiwiRGF0ZSIsIm5vdyIsImNvbmZpZyIsImluc3RhbGxNZXRob2QiLCJpbnN0YWxsYXRpb25UeXBlIiwiaW5zdGFsbFN0YXR1cyIsInVwZGF0ZU1ldGhvZCIsImlzTWlncmF0ZWQiLCJmcm9tVmVyc2lvbiIsInRvVmVyc2lvbiIsImR1cmF0aW9uTXMiLCJ3YXNNaWdyYXRlZCIsImF0dGVtcHRlZFZlcnNpb24iLCJzdGF0dXMiLCJQQUNLQUdFX1VSTCJdLCJzb3VyY2VzIjpbIkF1dG9VcGRhdGVyLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZUVmZmVjdCwgdXNlUmVmLCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHtcbiAgdHlwZSBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxuICBsb2dFdmVudCxcbn0gZnJvbSAnc3JjL3NlcnZpY2VzL2FuYWx5dGljcy9pbmRleC5qcydcbmltcG9ydCB7IHVzZUludGVydmFsIH0gZnJvbSAndXNlaG9va3MtdHMnXG5pbXBvcnQgeyB1c2VVcGRhdGVOb3RpZmljYXRpb24gfSBmcm9tICcuLi9ob29rcy91c2VVcGRhdGVOb3RpZmljYXRpb24uanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi9pbmsuanMnXG5pbXBvcnQge1xuICB0eXBlIEF1dG9VcGRhdGVyUmVzdWx0LFxuICBnZXRMYXRlc3RWZXJzaW9uLFxuICBnZXRNYXhWZXJzaW9uLFxuICB0eXBlIEluc3RhbGxTdGF0dXMsXG4gIGluc3RhbGxHbG9iYWxQYWNrYWdlLFxuICBzaG91bGRTa2lwVmVyc2lvbixcbn0gZnJvbSAnLi4vdXRpbHMvYXV0b1VwZGF0ZXIuanMnXG5pbXBvcnQgeyBnZXRHbG9iYWxDb25maWcsIGlzQXV0b1VwZGF0ZXJEaXNhYmxlZCB9IGZyb20gJy4uL3V0aWxzL2NvbmZpZy5qcydcbmltcG9ydCB7IGxvZ0ZvckRlYnVnZ2luZyB9IGZyb20gJy4uL3V0aWxzL2RlYnVnLmpzJ1xuaW1wb3J0IHsgZ2V0Q3VycmVudEluc3RhbGxhdGlvblR5cGUgfSBmcm9tICcuLi91dGlscy9kb2N0b3JEaWFnbm9zdGljLmpzJ1xuaW1wb3J0IHtcbiAgaW5zdGFsbE9yVXBkYXRlQ2xhdWRlUGFja2FnZSxcbiAgbG9jYWxJbnN0YWxsYXRpb25FeGlzdHMsXG59IGZyb20gJy4uL3V0aWxzL2xvY2FsSW5zdGFsbGVyLmpzJ1xuaW1wb3J0IHsgcmVtb3ZlSW5zdGFsbGVkU3ltbGluayB9IGZyb20gJy4uL3V0aWxzL25hdGl2ZUluc3RhbGxlci9pbmRleC5qcydcbmltcG9ydCB7IGd0LCBndGUgfSBmcm9tICcuLi91dGlscy9zZW12ZXIuanMnXG5pbXBvcnQgeyBnZXRJbml0aWFsU2V0dGluZ3MgfSBmcm9tICcuLi91dGlscy9zZXR0aW5ncy9zZXR0aW5ncy5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgaXNVcGRhdGluZzogYm9vbGVhblxuICBvbkNoYW5nZUlzVXBkYXRpbmc6IChpc1VwZGF0aW5nOiBib29sZWFuKSA9PiB2b2lkXG4gIG9uQXV0b1VwZGF0ZXJSZXN1bHQ6IChhdXRvVXBkYXRlclJlc3VsdDogQXV0b1VwZGF0ZXJSZXN1bHQpID0+IHZvaWRcbiAgYXV0b1VwZGF0ZXJSZXN1bHQ6IEF1dG9VcGRhdGVyUmVzdWx0IHwgbnVsbFxuICBzaG93U3VjY2Vzc01lc3NhZ2U6IGJvb2xlYW5cbiAgdmVyYm9zZTogYm9vbGVhblxufVxuXG5leHBvcnQgZnVuY3Rpb24gQXV0b1VwZGF0ZXIoe1xuICBpc1VwZGF0aW5nLFxuICBvbkNoYW5nZUlzVXBkYXRpbmcsXG4gIG9uQXV0b1VwZGF0ZXJSZXN1bHQsXG4gIGF1dG9VcGRhdGVyUmVzdWx0LFxuICBzaG93U3VjY2Vzc01lc3NhZ2UsXG4gIHZlcmJvc2UsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IFt2ZXJzaW9ucywgc2V0VmVyc2lvbnNdID0gdXNlU3RhdGU8e1xuICAgIGdsb2JhbD86IHN0cmluZyB8IG51bGxcbiAgICBsYXRlc3Q/OiBzdHJpbmcgfCBudWxsXG4gIH0+KHt9KVxuICBjb25zdCBbaGFzTG9jYWxJbnN0YWxsLCBzZXRIYXNMb2NhbEluc3RhbGxdID0gdXNlU3RhdGUoZmFsc2UpXG4gIGNvbnN0IHVwZGF0ZVNlbXZlciA9IHVzZVVwZGF0ZU5vdGlmaWNhdGlvbihhdXRvVXBkYXRlclJlc3VsdD8udmVyc2lvbilcblxuICB1c2VFZmZlY3QoKCkgPT4ge1xuICAgIHZvaWQgbG9jYWxJbnN0YWxsYXRpb25FeGlzdHMoKS50aGVuKHNldEhhc0xvY2FsSW5zdGFsbClcbiAgfSwgW10pXG5cbiAgLy8gVHJhY2sgbGF0ZXN0IGlzVXBkYXRpbmcgdmFsdWUgaW4gYSByZWYgc28gdGhlIG1lbW9pemVkIGNoZWNrRm9yVXBkYXRlc1xuICAvLyBjYWxsYmFjayBhbHdheXMgc2VlcyB0aGUgY3VycmVudCB2YWx1ZS4gV2l0aG91dCB0aGlzLCB0aGUgMzAtbWludXRlXG4gIC8vIGludGVydmFsIGZpcmVzIHdpdGggYSBzdGFsZSBjbG9zdXJlIHdoZXJlIGlzVXBkYXRpbmcgaXMgZmFsc2UsIGFsbG93aW5nXG4gIC8vIGEgY29uY3VycmVudCBpbnN0YWxsR2xvYmFsUGFja2FnZSgpIHRvIHJ1biB3aGlsZSBvbmUgaXMgYWxyZWFkeSBpblxuICAvLyBwcm9ncmVzcy5cbiAgY29uc3QgaXNVcGRhdGluZ1JlZiA9IHVzZVJlZihpc1VwZGF0aW5nKVxuICBpc1VwZGF0aW5nUmVmLmN1cnJlbnQgPSBpc1VwZGF0aW5nXG5cbiAgY29uc3QgY2hlY2tGb3JVcGRhdGVzID0gUmVhY3QudXNlQ2FsbGJhY2soYXN5bmMgKCkgPT4ge1xuICAgIGlmIChpc1VwZGF0aW5nUmVmLmN1cnJlbnQpIHtcbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIGlmIChcbiAgICAgIFwicHJvZHVjdGlvblwiID09PSAndGVzdCcgfHxcbiAgICAgIFwicHJvZHVjdGlvblwiID09PSAnZGV2ZWxvcG1lbnQnXG4gICAgKSB7XG4gICAgICBsb2dGb3JEZWJ1Z2dpbmcoXG4gICAgICAgICdBdXRvVXBkYXRlcjogU2tpcHBpbmcgdXBkYXRlIGNoZWNrIGluIHRlc3QvZGV2IGVudmlyb25tZW50JyxcbiAgICAgIClcbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIGNvbnN0IGN1cnJlbnRWZXJzaW9uID0gTUFDUk8uVkVSU0lPTlxuICAgIGNvbnN0IGNoYW5uZWwgPSBnZXRJbml0aWFsU2V0dGluZ3MoKT8uYXV0b1VwZGF0ZXNDaGFubmVsID8/ICdsYXRlc3QnXG4gICAgbGV0IGxhdGVzdFZlcnNpb24gPSBhd2FpdCBnZXRMYXRlc3RWZXJzaW9uKGNoYW5uZWwpXG4gICAgY29uc3QgaXNEaXNhYmxlZCA9IGlzQXV0b1VwZGF0ZXJEaXNhYmxlZCgpXG5cbiAgICAvLyBDaGVjayBpZiBtYXggdmVyc2lvbiBpcyBzZXQgKHNlcnZlci1zaWRlIGtpbGwgc3dpdGNoIGZvciBhdXRvLXVwZGF0ZXMpXG4gICAgY29uc3QgbWF4VmVyc2lvbiA9IGF3YWl0IGdldE1heFZlcnNpb24oKVxuICAgIGlmIChtYXhWZXJzaW9uICYmIGxhdGVzdFZlcnNpb24gJiYgZ3QobGF0ZXN0VmVyc2lvbiwgbWF4VmVyc2lvbikpIHtcbiAgICAgIGxvZ0ZvckRlYnVnZ2luZyhcbiAgICAgICAgYEF1dG9VcGRhdGVyOiBtYXhWZXJzaW9uICR7bWF4VmVyc2lvbn0gaXMgc2V0LCBjYXBwaW5nIHVwZGF0ZSBmcm9tICR7bGF0ZXN0VmVyc2lvbn0gdG8gJHttYXhWZXJzaW9ufWAsXG4gICAgICApXG4gICAgICBpZiAoZ3RlKGN1cnJlbnRWZXJzaW9uLCBtYXhWZXJzaW9uKSkge1xuICAgICAgICBsb2dGb3JEZWJ1Z2dpbmcoXG4gICAgICAgICAgYEF1dG9VcGRhdGVyOiBjdXJyZW50IHZlcnNpb24gJHtjdXJyZW50VmVyc2lvbn0gaXMgYWxyZWFkeSBhdCBvciBhYm92ZSBtYXhWZXJzaW9uICR7bWF4VmVyc2lvbn0sIHNraXBwaW5nIHVwZGF0ZWAsXG4gICAgICAgIClcbiAgICAgICAgc2V0VmVyc2lvbnMoeyBnbG9iYWw6IGN1cnJlbnRWZXJzaW9uLCBsYXRlc3Q6IGxhdGVzdFZlcnNpb24gfSlcbiAgICAgICAgcmV0dXJuXG4gICAgICB9XG4gICAgICBsYXRlc3RWZXJzaW9uID0gbWF4VmVyc2lvblxuICAgIH1cblxuICAgIHNldFZlcnNpb25zKHsgZ2xvYmFsOiBjdXJyZW50VmVyc2lvbiwgbGF0ZXN0OiBsYXRlc3RWZXJzaW9uIH0pXG5cbiAgICAvLyBDaGVjayBpZiB1cGRhdGUgbmVlZGVkIGFuZCBwZXJmb3JtIHVwZGF0ZVxuICAgIGlmIChcbiAgICAgICFpc0Rpc2FibGVkICYmXG4gICAgICBjdXJyZW50VmVyc2lvbiAmJlxuICAgICAgbGF0ZXN0VmVyc2lvbiAmJlxuICAgICAgIWd0ZShjdXJyZW50VmVyc2lvbiwgbGF0ZXN0VmVyc2lvbikgJiZcbiAgICAgICFzaG91bGRTa2lwVmVyc2lvbihsYXRlc3RWZXJzaW9uKVxuICAgICkge1xuICAgICAgY29uc3Qgc3RhcnRUaW1lID0gRGF0ZS5ub3coKVxuICAgICAgb25DaGFuZ2VJc1VwZGF0aW5nKHRydWUpXG5cbiAgICAgIC8vIFJlbW92ZSBuYXRpdmUgaW5zdGFsbGVyIHN5bWxpbmsgc2luY2Ugd2UncmUgdXNpbmcgSlMtYmFzZWQgdXBkYXRlc1xuICAgICAgLy8gQnV0IG9ubHkgaWYgdXNlciBoYXNuJ3QgbWlncmF0ZWQgdG8gbmF0aXZlIGluc3RhbGxhdGlvblxuICAgICAgY29uc3QgY29uZmlnID0gZ2V0R2xvYmFsQ29uZmlnKClcbiAgICAgIGlmIChjb25maWcuaW5zdGFsbE1ldGhvZCAhPT0gJ25hdGl2ZScpIHtcbiAgICAgICAgYXdhaXQgcmVtb3ZlSW5zdGFsbGVkU3ltbGluaygpXG4gICAgICB9XG5cbiAgICAgIC8vIERldGVjdCBhY3R1YWwgcnVubmluZyBpbnN0YWxsYXRpb24gdHlwZVxuICAgICAgY29uc3QgaW5zdGFsbGF0aW9uVHlwZSA9IGF3YWl0IGdldEN1cnJlbnRJbnN0YWxsYXRpb25UeXBlKClcbiAgICAgIGxvZ0ZvckRlYnVnZ2luZyhcbiAgICAgICAgYEF1dG9VcGRhdGVyOiBEZXRlY3RlZCBpbnN0YWxsYXRpb24gdHlwZTogJHtpbnN0YWxsYXRpb25UeXBlfWAsXG4gICAgICApXG5cbiAgICAgIC8vIFNraXAgdXBkYXRlIGZvciBkZXZlbG9wbWVudCBidWlsZHNcbiAgICAgIGlmIChpbnN0YWxsYXRpb25UeXBlID09PSAnZGV2ZWxvcG1lbnQnKSB7XG4gICAgICAgIGxvZ0ZvckRlYnVnZ2luZygnQXV0b1VwZGF0ZXI6IENhbm5vdCBhdXRvLXVwZGF0ZSBkZXZlbG9wbWVudCBidWlsZCcpXG4gICAgICAgIG9uQ2hhbmdlSXNVcGRhdGluZyhmYWxzZSlcbiAgICAgICAgcmV0dXJuXG4gICAgICB9XG5cbiAgICAgIC8vIENob29zZSB0aGUgYXBwcm9wcmlhdGUgdXBkYXRlIG1ldGhvZCBiYXNlZCBvbiB3aGF0J3MgYWN0dWFsbHkgcnVubmluZ1xuICAgICAgbGV0IGluc3RhbGxTdGF0dXM6IEluc3RhbGxTdGF0dXNcbiAgICAgIGxldCB1cGRhdGVNZXRob2Q6ICdsb2NhbCcgfCAnZ2xvYmFsJ1xuXG4gICAgICBpZiAoaW5zdGFsbGF0aW9uVHlwZSA9PT0gJ25wbS1sb2NhbCcpIHtcbiAgICAgICAgLy8gVXNlIGxvY2FsIHVwZGF0ZSBmb3IgbG9jYWwgaW5zdGFsbGF0aW9uc1xuICAgICAgICBsb2dGb3JEZWJ1Z2dpbmcoJ0F1dG9VcGRhdGVyOiBVc2luZyBsb2NhbCB1cGRhdGUgbWV0aG9kJylcbiAgICAgICAgdXBkYXRlTWV0aG9kID0gJ2xvY2FsJ1xuICAgICAgICBpbnN0YWxsU3RhdHVzID0gYXdhaXQgaW5zdGFsbE9yVXBkYXRlQ2xhdWRlUGFja2FnZShjaGFubmVsKVxuICAgICAgfSBlbHNlIGlmIChpbnN0YWxsYXRpb25UeXBlID09PSAnbnBtLWdsb2JhbCcpIHtcbiAgICAgICAgLy8gVXNlIGdsb2JhbCB1cGRhdGUgZm9yIGdsb2JhbCBpbnN0YWxsYXRpb25zXG4gICAgICAgIGxvZ0ZvckRlYnVnZ2luZygnQXV0b1VwZGF0ZXI6IFVzaW5nIGdsb2JhbCB1cGRhdGUgbWV0aG9kJylcbiAgICAgICAgdXBkYXRlTWV0aG9kID0gJ2dsb2JhbCdcbiAgICAgICAgaW5zdGFsbFN0YXR1cyA9IGF3YWl0IGluc3RhbGxHbG9iYWxQYWNrYWdlKClcbiAgICAgIH0gZWxzZSBpZiAoaW5zdGFsbGF0aW9uVHlwZSA9PT0gJ25hdGl2ZScpIHtcbiAgICAgICAgLy8gVGhpcyBzaG91bGRuJ3QgaGFwcGVuIC0gbmF0aXZlIHNob3VsZCB1c2UgTmF0aXZlQXV0b1VwZGF0ZXJcbiAgICAgICAgbG9nRm9yRGVidWdnaW5nKFxuICAgICAgICAgICdBdXRvVXBkYXRlcjogVW5leHBlY3RlZCBuYXRpdmUgaW5zdGFsbGF0aW9uIGluIG5vbi1uYXRpdmUgdXBkYXRlcicsXG4gICAgICAgIClcbiAgICAgICAgb25DaGFuZ2VJc1VwZGF0aW5nKGZhbHNlKVxuICAgICAgICByZXR1cm5cbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIC8vIEZhbGxiYWNrIHRvIGNvbmZpZy1iYXNlZCBkZXRlY3Rpb24gZm9yIHVua25vd24gdHlwZXNcbiAgICAgICAgbG9nRm9yRGVidWdnaW5nKFxuICAgICAgICAgIGBBdXRvVXBkYXRlcjogVW5rbm93biBpbnN0YWxsYXRpb24gdHlwZSwgZmFsbGluZyBiYWNrIHRvIGNvbmZpZ2AsXG4gICAgICAgIClcbiAgICAgICAgY29uc3QgaXNNaWdyYXRlZCA9IGNvbmZpZy5pbnN0YWxsTWV0aG9kID09PSAnbG9jYWwnXG4gICAgICAgIHVwZGF0ZU1ldGhvZCA9IGlzTWlncmF0ZWQgPyAnbG9jYWwnIDogJ2dsb2JhbCdcblxuICAgICAgICBpZiAoaXNNaWdyYXRlZCkge1xuICAgICAgICAgIGluc3RhbGxTdGF0dXMgPSBhd2FpdCBpbnN0YWxsT3JVcGRhdGVDbGF1ZGVQYWNrYWdlKGNoYW5uZWwpXG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgaW5zdGFsbFN0YXR1cyA9IGF3YWl0IGluc3RhbGxHbG9iYWxQYWNrYWdlKClcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBvbkNoYW5nZUlzVXBkYXRpbmcoZmFsc2UpXG5cbiAgICAgIGlmIChpbnN0YWxsU3RhdHVzID09PSAnc3VjY2VzcycpIHtcbiAgICAgICAgbG9nRXZlbnQoJ3Rlbmd1X2F1dG9fdXBkYXRlcl9zdWNjZXNzJywge1xuICAgICAgICAgIGZyb21WZXJzaW9uOlxuICAgICAgICAgICAgY3VycmVudFZlcnNpb24gYXMgQW5hbHl0aWNzTWV0YWRhdGFfSV9WRVJJRklFRF9USElTX0lTX05PVF9DT0RFX09SX0ZJTEVQQVRIUyxcbiAgICAgICAgICB0b1ZlcnNpb246XG4gICAgICAgICAgICBsYXRlc3RWZXJzaW9uIGFzIEFuYWx5dGljc01ldGFkYXRhX0lfVkVSSUZJRURfVEhJU19JU19OT1RfQ09ERV9PUl9GSUxFUEFUSFMsXG4gICAgICAgICAgZHVyYXRpb25NczogRGF0ZS5ub3coKSAtIHN0YXJ0VGltZSxcbiAgICAgICAgICB3YXNNaWdyYXRlZDogdXBkYXRlTWV0aG9kID09PSAnbG9jYWwnLFxuICAgICAgICAgIGluc3RhbGxhdGlvblR5cGU6XG4gICAgICAgICAgICBpbnN0YWxsYXRpb25UeXBlIGFzIEFuYWx5dGljc01ldGFkYXRhX0lfVkVSSUZJRURfVEhJU19JU19OT1RfQ09ERV9PUl9GSUxFUEFUSFMsXG4gICAgICAgIH0pXG4gICAgICB9IGVsc2Uge1xuICAgICAgICBsb2dFdmVudCgndGVuZ3VfYXV0b191cGRhdGVyX2ZhaWwnLCB7XG4gICAgICAgICAgZnJvbVZlcnNpb246XG4gICAgICAgICAgICBjdXJyZW50VmVyc2lvbiBhcyBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxuICAgICAgICAgIGF0dGVtcHRlZFZlcnNpb246XG4gICAgICAgICAgICBsYXRlc3RWZXJzaW9uIGFzIEFuYWx5dGljc01ldGFkYXRhX0lfVkVSSUZJRURfVEhJU19JU19OT1RfQ09ERV9PUl9GSUxFUEFUSFMsXG4gICAgICAgICAgc3RhdHVzOlxuICAgICAgICAgICAgaW5zdGFsbFN0YXR1cyBhcyBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxuICAgICAgICAgIGR1cmF0aW9uTXM6IERhdGUubm93KCkgLSBzdGFydFRpbWUsXG4gICAgICAgICAgd2FzTWlncmF0ZWQ6IHVwZGF0ZU1ldGhvZCA9PT0gJ2xvY2FsJyxcbiAgICAgICAgICBpbnN0YWxsYXRpb25UeXBlOlxuICAgICAgICAgICAgaW5zdGFsbGF0aW9uVHlwZSBhcyBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxuICAgICAgICB9KVxuICAgICAgfVxuXG4gICAgICBvbkF1dG9VcGRhdGVyUmVzdWx0KHtcbiAgICAgICAgdmVyc2lvbjogbGF0ZXN0VmVyc2lvbixcbiAgICAgICAgc3RhdHVzOiBpbnN0YWxsU3RhdHVzLFxuICAgICAgfSlcbiAgICB9XG4gICAgLy8gaXNVcGRhdGluZyBpbnRlbnRpb25hbGx5IG9taXR0ZWQgZnJvbSBkZXBzOyB3ZSByZWFkIGlzVXBkYXRpbmdSZWZcbiAgICAvLyBpbnN0ZWFkIHNvIHRoZSBndWFyZCBpcyBhbHdheXMgY3VycmVudCB3aXRob3V0IGNoYW5naW5nIGNhbGxiYWNrXG4gICAgLy8gaWRlbnRpdHkgKHdoaWNoIHdvdWxkIHJlLXRyaWdnZXIgdGhlIGluaXRpYWwtY2hlY2sgdXNlRWZmZWN0IGJlbG93KS5cbiAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgcmVhY3QtaG9va3MvZXhoYXVzdGl2ZS1kZXBzXG4gICAgLy8gYmlvbWUtaWdub3JlIGxpbnQvY29ycmVjdG5lc3MvdXNlRXhoYXVzdGl2ZURlcGVuZGVuY2llczogaXNVcGRhdGluZyByZWFkIHZpYSByZWZcbiAgfSwgW29uQXV0b1VwZGF0ZXJSZXN1bHRdKVxuXG4gIC8vIEluaXRpYWwgY2hlY2tcbiAgdXNlRWZmZWN0KCgpID0+IHtcbiAgICB2b2lkIGNoZWNrRm9yVXBkYXRlcygpXG4gIH0sIFtjaGVja0ZvclVwZGF0ZXNdKVxuXG4gIC8vIENoZWNrIGV2ZXJ5IDMwIG1pbnV0ZXNcbiAgdXNlSW50ZXJ2YWwoY2hlY2tGb3JVcGRhdGVzLCAzMCAqIDYwICogMTAwMClcblxuICBpZiAoIWF1dG9VcGRhdGVyUmVzdWx0Py52ZXJzaW9uICYmICghdmVyc2lvbnMuZ2xvYmFsIHx8ICF2ZXJzaW9ucy5sYXRlc3QpKSB7XG4gICAgcmV0dXJuIG51bGxcbiAgfVxuXG4gIGlmICghYXV0b1VwZGF0ZXJSZXN1bHQ/LnZlcnNpb24gJiYgIWlzVXBkYXRpbmcpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG5cbiAgcmV0dXJuIChcbiAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJyb3dcIiBnYXA9ezF9PlxuICAgICAge3ZlcmJvc2UgJiYgKFxuICAgICAgICA8VGV4dCBkaW1Db2xvciB3cmFwPVwidHJ1bmNhdGVcIj5cbiAgICAgICAgICBnbG9iYWxWZXJzaW9uOiB7dmVyc2lvbnMuZ2xvYmFsfSAmbWlkZG90OyBsYXRlc3RWZXJzaW9uOnsnICd9XG4gICAgICAgICAge3ZlcnNpb25zLmxhdGVzdH1cbiAgICAgICAgPC9UZXh0PlxuICAgICAgKX1cbiAgICAgIHtpc1VwZGF0aW5nID8gKFxuICAgICAgICA8PlxuICAgICAgICAgIDxCb3g+XG4gICAgICAgICAgICA8VGV4dCBjb2xvcj1cInRleHRcIiBkaW1Db2xvciB3cmFwPVwidHJ1bmNhdGVcIj5cbiAgICAgICAgICAgICAgQXV0by11cGRhdGluZ+KAplxuICAgICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgIDwvQm94PlxuICAgICAgICA8Lz5cbiAgICAgICkgOiAoXG4gICAgICAgIGF1dG9VcGRhdGVyUmVzdWx0Py5zdGF0dXMgPT09ICdzdWNjZXNzJyAmJlxuICAgICAgICBzaG93U3VjY2Vzc01lc3NhZ2UgJiZcbiAgICAgICAgdXBkYXRlU2VtdmVyICYmIChcbiAgICAgICAgICA8VGV4dCBjb2xvcj1cInN1Y2Nlc3NcIiB3cmFwPVwidHJ1bmNhdGVcIj5cbiAgICAgICAgICAgIOKckyBVcGRhdGUgaW5zdGFsbGVkIMK3IFJlc3RhcnQgdG8gYXBwbHlcbiAgICAgICAgICA8L1RleHQ+XG4gICAgICAgIClcbiAgICAgICl9XG4gICAgICB7KGF1dG9VcGRhdGVyUmVzdWx0Py5zdGF0dXMgPT09ICdpbnN0YWxsX2ZhaWxlZCcgfHxcbiAgICAgICAgYXV0b1VwZGF0ZXJSZXN1bHQ/LnN0YXR1cyA9PT0gJ25vX3Blcm1pc3Npb25zJykgJiYgKFxuICAgICAgICA8VGV4dCBjb2xvcj1cImVycm9yXCIgd3JhcD1cInRydW5jYXRlXCI+XG4gICAgICAgICAg4pyXIEF1dG8tdXBkYXRlIGZhaWxlZCAmbWlkZG90OyBUcnkgPFRleHQgYm9sZD5jbGF1ZGUgZG9jdG9yPC9UZXh0PiBvcnsnICd9XG4gICAgICAgICAgPFRleHQgYm9sZD5cbiAgICAgICAgICAgIHtoYXNMb2NhbEluc3RhbGxcbiAgICAgICAgICAgICAgPyBgY2Qgfi8uY2xhdWRlL2xvY2FsICYmIG5wbSB1cGRhdGUgJHtNQUNSTy5QQUNLQUdFX1VSTH1gXG4gICAgICAgICAgICAgIDogYG5wbSBpIC1nICR7TUFDUk8uUEFDS0FHRV9VUkx9YH1cbiAgICAgICAgICA8L1RleHQ+XG4gICAgICAgIDwvVGV4dD5cbiAgICAgICl9XG4gICAgPC9Cb3g+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxTQUFTLEVBQUVDLE1BQU0sRUFBRUMsUUFBUSxRQUFRLE9BQU87QUFDbkQsU0FDRSxLQUFLQywwREFBMEQsRUFDL0RDLFFBQVEsUUFDSCxpQ0FBaUM7QUFDeEMsU0FBU0MsV0FBVyxRQUFRLGFBQWE7QUFDekMsU0FBU0MscUJBQXFCLFFBQVEsbUNBQW1DO0FBQ3pFLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxRQUFRLFdBQVc7QUFDckMsU0FDRSxLQUFLQyxpQkFBaUIsRUFDdEJDLGdCQUFnQixFQUNoQkMsYUFBYSxFQUNiLEtBQUtDLGFBQWEsRUFDbEJDLG9CQUFvQixFQUNwQkMsaUJBQWlCLFFBQ1oseUJBQXlCO0FBQ2hDLFNBQVNDLGVBQWUsRUFBRUMscUJBQXFCLFFBQVEsb0JBQW9CO0FBQzNFLFNBQVNDLGVBQWUsUUFBUSxtQkFBbUI7QUFDbkQsU0FBU0MsMEJBQTBCLFFBQVEsOEJBQThCO0FBQ3pFLFNBQ0VDLDRCQUE0QixFQUM1QkMsdUJBQXVCLFFBQ2xCLDRCQUE0QjtBQUNuQyxTQUFTQyxzQkFBc0IsUUFBUSxtQ0FBbUM7QUFDMUUsU0FBU0MsRUFBRSxFQUFFQyxHQUFHLFFBQVEsb0JBQW9CO0FBQzVDLFNBQVNDLGtCQUFrQixRQUFRLCtCQUErQjtBQUVsRSxLQUFLQyxLQUFLLEdBQUc7RUFDWEMsVUFBVSxFQUFFLE9BQU87RUFDbkJDLGtCQUFrQixFQUFFLENBQUNELFVBQVUsRUFBRSxPQUFPLEVBQUUsR0FBRyxJQUFJO0VBQ2pERSxtQkFBbUIsRUFBRSxDQUFDQyxpQkFBaUIsRUFBRXBCLGlCQUFpQixFQUFFLEdBQUcsSUFBSTtFQUNuRW9CLGlCQUFpQixFQUFFcEIsaUJBQWlCLEdBQUcsSUFBSTtFQUMzQ3FCLGtCQUFrQixFQUFFLE9BQU87RUFDM0JDLE9BQU8sRUFBRSxPQUFPO0FBQ2xCLENBQUM7QUFFRCxPQUFPLFNBQVNDLFdBQVdBLENBQUM7RUFDMUJOLFVBQVU7RUFDVkMsa0JBQWtCO0VBQ2xCQyxtQkFBbUI7RUFDbkJDLGlCQUFpQjtFQUNqQkMsa0JBQWtCO0VBQ2xCQztBQUNLLENBQU4sRUFBRU4sS0FBSyxDQUFDLEVBQUUxQixLQUFLLENBQUNrQyxTQUFTLENBQUM7RUFDekIsTUFBTSxDQUFDQyxRQUFRLEVBQUVDLFdBQVcsQ0FBQyxHQUFHakMsUUFBUSxDQUFDO0lBQ3ZDa0MsTUFBTSxDQUFDLEVBQUUsTUFBTSxHQUFHLElBQUk7SUFDdEJDLE1BQU0sQ0FBQyxFQUFFLE1BQU0sR0FBRyxJQUFJO0VBQ3hCLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0VBQ04sTUFBTSxDQUFDQyxlQUFlLEVBQUVDLGtCQUFrQixDQUFDLEdBQUdyQyxRQUFRLENBQUMsS0FBSyxDQUFDO0VBQzdELE1BQU1zQyxZQUFZLEdBQUdsQyxxQkFBcUIsQ0FBQ3VCLGlCQUFpQixFQUFFWSxPQUFPLENBQUM7RUFFdEV6QyxTQUFTLENBQUMsTUFBTTtJQUNkLEtBQUtvQix1QkFBdUIsQ0FBQyxDQUFDLENBQUNzQixJQUFJLENBQUNILGtCQUFrQixDQUFDO0VBQ3pELENBQUMsRUFBRSxFQUFFLENBQUM7O0VBRU47RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBLE1BQU1JLGFBQWEsR0FBRzFDLE1BQU0sQ0FBQ3lCLFVBQVUsQ0FBQztFQUN4Q2lCLGFBQWEsQ0FBQ0MsT0FBTyxHQUFHbEIsVUFBVTtFQUVsQyxNQUFNbUIsZUFBZSxHQUFHOUMsS0FBSyxDQUFDK0MsV0FBVyxDQUFDLFlBQVk7SUFDcEQsSUFBSUgsYUFBYSxDQUFDQyxPQUFPLEVBQUU7TUFDekI7SUFDRjtJQUVBLElBQ0UsWUFBWSxLQUFLLE1BQU0sSUFDdkIsWUFBWSxLQUFLLGFBQWEsRUFDOUI7TUFDQTNCLGVBQWUsQ0FDYiw0REFDRixDQUFDO01BQ0Q7SUFDRjtJQUVBLE1BQU04QixjQUFjLEdBQUdDLEtBQUssQ0FBQ0MsT0FBTztJQUNwQyxNQUFNQyxPQUFPLEdBQUcxQixrQkFBa0IsQ0FBQyxDQUFDLEVBQUUyQixrQkFBa0IsSUFBSSxRQUFRO0lBQ3BFLElBQUlDLGFBQWEsR0FBRyxNQUFNMUMsZ0JBQWdCLENBQUN3QyxPQUFPLENBQUM7SUFDbkQsTUFBTUcsVUFBVSxHQUFHckMscUJBQXFCLENBQUMsQ0FBQzs7SUFFMUM7SUFDQSxNQUFNc0MsVUFBVSxHQUFHLE1BQU0zQyxhQUFhLENBQUMsQ0FBQztJQUN4QyxJQUFJMkMsVUFBVSxJQUFJRixhQUFhLElBQUk5QixFQUFFLENBQUM4QixhQUFhLEVBQUVFLFVBQVUsQ0FBQyxFQUFFO01BQ2hFckMsZUFBZSxDQUNiLDJCQUEyQnFDLFVBQVUsZ0NBQWdDRixhQUFhLE9BQU9FLFVBQVUsRUFDckcsQ0FBQztNQUNELElBQUkvQixHQUFHLENBQUN3QixjQUFjLEVBQUVPLFVBQVUsQ0FBQyxFQUFFO1FBQ25DckMsZUFBZSxDQUNiLGdDQUFnQzhCLGNBQWMsc0NBQXNDTyxVQUFVLG1CQUNoRyxDQUFDO1FBQ0RuQixXQUFXLENBQUM7VUFBRUMsTUFBTSxFQUFFVyxjQUFjO1VBQUVWLE1BQU0sRUFBRWU7UUFBYyxDQUFDLENBQUM7UUFDOUQ7TUFDRjtNQUNBQSxhQUFhLEdBQUdFLFVBQVU7SUFDNUI7SUFFQW5CLFdBQVcsQ0FBQztNQUFFQyxNQUFNLEVBQUVXLGNBQWM7TUFBRVYsTUFBTSxFQUFFZTtJQUFjLENBQUMsQ0FBQzs7SUFFOUQ7SUFDQSxJQUNFLENBQUNDLFVBQVUsSUFDWE4sY0FBYyxJQUNkSyxhQUFhLElBQ2IsQ0FBQzdCLEdBQUcsQ0FBQ3dCLGNBQWMsRUFBRUssYUFBYSxDQUFDLElBQ25DLENBQUN0QyxpQkFBaUIsQ0FBQ3NDLGFBQWEsQ0FBQyxFQUNqQztNQUNBLE1BQU1HLFNBQVMsR0FBR0MsSUFBSSxDQUFDQyxHQUFHLENBQUMsQ0FBQztNQUM1QjlCLGtCQUFrQixDQUFDLElBQUksQ0FBQzs7TUFFeEI7TUFDQTtNQUNBLE1BQU0rQixNQUFNLEdBQUczQyxlQUFlLENBQUMsQ0FBQztNQUNoQyxJQUFJMkMsTUFBTSxDQUFDQyxhQUFhLEtBQUssUUFBUSxFQUFFO1FBQ3JDLE1BQU10QyxzQkFBc0IsQ0FBQyxDQUFDO01BQ2hDOztNQUVBO01BQ0EsTUFBTXVDLGdCQUFnQixHQUFHLE1BQU0xQywwQkFBMEIsQ0FBQyxDQUFDO01BQzNERCxlQUFlLENBQ2IsNENBQTRDMkMsZ0JBQWdCLEVBQzlELENBQUM7O01BRUQ7TUFDQSxJQUFJQSxnQkFBZ0IsS0FBSyxhQUFhLEVBQUU7UUFDdEMzQyxlQUFlLENBQUMsbURBQW1ELENBQUM7UUFDcEVVLGtCQUFrQixDQUFDLEtBQUssQ0FBQztRQUN6QjtNQUNGOztNQUVBO01BQ0EsSUFBSWtDLGFBQWEsRUFBRWpELGFBQWE7TUFDaEMsSUFBSWtELFlBQVksRUFBRSxPQUFPLEdBQUcsUUFBUTtNQUVwQyxJQUFJRixnQkFBZ0IsS0FBSyxXQUFXLEVBQUU7UUFDcEM7UUFDQTNDLGVBQWUsQ0FBQyx3Q0FBd0MsQ0FBQztRQUN6RDZDLFlBQVksR0FBRyxPQUFPO1FBQ3RCRCxhQUFhLEdBQUcsTUFBTTFDLDRCQUE0QixDQUFDK0IsT0FBTyxDQUFDO01BQzdELENBQUMsTUFBTSxJQUFJVSxnQkFBZ0IsS0FBSyxZQUFZLEVBQUU7UUFDNUM7UUFDQTNDLGVBQWUsQ0FBQyx5Q0FBeUMsQ0FBQztRQUMxRDZDLFlBQVksR0FBRyxRQUFRO1FBQ3ZCRCxhQUFhLEdBQUcsTUFBTWhELG9CQUFvQixDQUFDLENBQUM7TUFDOUMsQ0FBQyxNQUFNLElBQUkrQyxnQkFBZ0IsS0FBSyxRQUFRLEVBQUU7UUFDeEM7UUFDQTNDLGVBQWUsQ0FDYixtRUFDRixDQUFDO1FBQ0RVLGtCQUFrQixDQUFDLEtBQUssQ0FBQztRQUN6QjtNQUNGLENBQUMsTUFBTTtRQUNMO1FBQ0FWLGVBQWUsQ0FDYixnRUFDRixDQUFDO1FBQ0QsTUFBTThDLFVBQVUsR0FBR0wsTUFBTSxDQUFDQyxhQUFhLEtBQUssT0FBTztRQUNuREcsWUFBWSxHQUFHQyxVQUFVLEdBQUcsT0FBTyxHQUFHLFFBQVE7UUFFOUMsSUFBSUEsVUFBVSxFQUFFO1VBQ2RGLGFBQWEsR0FBRyxNQUFNMUMsNEJBQTRCLENBQUMrQixPQUFPLENBQUM7UUFDN0QsQ0FBQyxNQUFNO1VBQ0xXLGFBQWEsR0FBRyxNQUFNaEQsb0JBQW9CLENBQUMsQ0FBQztRQUM5QztNQUNGO01BRUFjLGtCQUFrQixDQUFDLEtBQUssQ0FBQztNQUV6QixJQUFJa0MsYUFBYSxLQUFLLFNBQVMsRUFBRTtRQUMvQnpELFFBQVEsQ0FBQyw0QkFBNEIsRUFBRTtVQUNyQzRELFdBQVcsRUFDVGpCLGNBQWMsSUFBSTVDLDBEQUEwRDtVQUM5RThELFNBQVMsRUFDUGIsYUFBYSxJQUFJakQsMERBQTBEO1VBQzdFK0QsVUFBVSxFQUFFVixJQUFJLENBQUNDLEdBQUcsQ0FBQyxDQUFDLEdBQUdGLFNBQVM7VUFDbENZLFdBQVcsRUFBRUwsWUFBWSxLQUFLLE9BQU87VUFDckNGLGdCQUFnQixFQUNkQSxnQkFBZ0IsSUFBSXpEO1FBQ3hCLENBQUMsQ0FBQztNQUNKLENBQUMsTUFBTTtRQUNMQyxRQUFRLENBQUMseUJBQXlCLEVBQUU7VUFDbEM0RCxXQUFXLEVBQ1RqQixjQUFjLElBQUk1QywwREFBMEQ7VUFDOUVpRSxnQkFBZ0IsRUFDZGhCLGFBQWEsSUFBSWpELDBEQUEwRDtVQUM3RWtFLE1BQU0sRUFDSlIsYUFBYSxJQUFJMUQsMERBQTBEO1VBQzdFK0QsVUFBVSxFQUFFVixJQUFJLENBQUNDLEdBQUcsQ0FBQyxDQUFDLEdBQUdGLFNBQVM7VUFDbENZLFdBQVcsRUFBRUwsWUFBWSxLQUFLLE9BQU87VUFDckNGLGdCQUFnQixFQUNkQSxnQkFBZ0IsSUFBSXpEO1FBQ3hCLENBQUMsQ0FBQztNQUNKO01BRUF5QixtQkFBbUIsQ0FBQztRQUNsQmEsT0FBTyxFQUFFVyxhQUFhO1FBQ3RCaUIsTUFBTSxFQUFFUjtNQUNWLENBQUMsQ0FBQztJQUNKO0lBQ0E7SUFDQTtJQUNBO0lBQ0E7SUFDQTtFQUNGLENBQUMsRUFBRSxDQUFDakMsbUJBQW1CLENBQUMsQ0FBQzs7RUFFekI7RUFDQTVCLFNBQVMsQ0FBQyxNQUFNO0lBQ2QsS0FBSzZDLGVBQWUsQ0FBQyxDQUFDO0VBQ3hCLENBQUMsRUFBRSxDQUFDQSxlQUFlLENBQUMsQ0FBQzs7RUFFckI7RUFDQXhDLFdBQVcsQ0FBQ3dDLGVBQWUsRUFBRSxFQUFFLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQztFQUU1QyxJQUFJLENBQUNoQixpQkFBaUIsRUFBRVksT0FBTyxLQUFLLENBQUNQLFFBQVEsQ0FBQ0UsTUFBTSxJQUFJLENBQUNGLFFBQVEsQ0FBQ0csTUFBTSxDQUFDLEVBQUU7SUFDekUsT0FBTyxJQUFJO0VBQ2I7RUFFQSxJQUFJLENBQUNSLGlCQUFpQixFQUFFWSxPQUFPLElBQUksQ0FBQ2YsVUFBVSxFQUFFO0lBQzlDLE9BQU8sSUFBSTtFQUNiO0VBRUEsT0FDRSxDQUFDLEdBQUcsQ0FBQyxhQUFhLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUNwQyxNQUFNLENBQUNLLE9BQU8sSUFDTixDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLFVBQVU7QUFDdEMseUJBQXlCLENBQUNHLFFBQVEsQ0FBQ0UsTUFBTSxDQUFDLHdCQUF3QixDQUFDLEdBQUc7QUFDdEUsVUFBVSxDQUFDRixRQUFRLENBQUNHLE1BQU07QUFDMUIsUUFBUSxFQUFFLElBQUksQ0FDUDtBQUNQLE1BQU0sQ0FBQ1gsVUFBVSxHQUNUO0FBQ1IsVUFBVSxDQUFDLEdBQUc7QUFDZCxZQUFZLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxVQUFVO0FBQ3ZEO0FBQ0EsWUFBWSxFQUFFLElBQUk7QUFDbEIsVUFBVSxFQUFFLEdBQUc7QUFDZixRQUFRLEdBQUcsR0FFSEcsaUJBQWlCLEVBQUV3QyxNQUFNLEtBQUssU0FBUyxJQUN2Q3ZDLGtCQUFrQixJQUNsQlUsWUFBWSxJQUNWLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLFVBQVU7QUFDL0M7QUFDQSxVQUFVLEVBQUUsSUFBSSxDQUVUO0FBQ1AsTUFBTSxDQUFDLENBQUNYLGlCQUFpQixFQUFFd0MsTUFBTSxLQUFLLGdCQUFnQixJQUM5Q3hDLGlCQUFpQixFQUFFd0MsTUFBTSxLQUFLLGdCQUFnQixLQUM5QyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxVQUFVO0FBQzNDLDRDQUE0QyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsYUFBYSxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRztBQUNsRixVQUFVLENBQUMsSUFBSSxDQUFDLElBQUk7QUFDcEIsWUFBWSxDQUFDL0IsZUFBZSxHQUNaLG9DQUFvQ1UsS0FBSyxDQUFDc0IsV0FBVyxFQUFFLEdBQ3ZELFlBQVl0QixLQUFLLENBQUNzQixXQUFXLEVBQUU7QUFDL0MsVUFBVSxFQUFFLElBQUk7QUFDaEIsUUFBUSxFQUFFLElBQUksQ0FDUDtBQUNQLElBQUksRUFBRSxHQUFHLENBQUM7QUFFViIsImlnbm9yZUxpc3QiOltdfQ== \ No newline at end of file diff --git a/src/components/AutoUpdaterWrapper.tsx b/src/components/AutoUpdaterWrapper.tsx index d812ec36..20532a1f 100644 --- a/src/components/AutoUpdaterWrapper.tsx +++ b/src/components/AutoUpdaterWrapper.tsx @@ -1,91 +1,10 @@ -import { c as _c } from "react/compiler-runtime"; -import { feature } from 'bun:bundle'; +/** + * AutoUpdaterWrapper stub for claudium + * All auto-update functionality disabled + */ + import * as React from 'react'; -import type { AutoUpdaterResult } from '../utils/autoUpdater.js'; -import { isAutoUpdaterDisabled } from '../utils/config.js'; -import { logForDebugging } from '../utils/debug.js'; -import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js'; -import { AutoUpdater } from './AutoUpdater.js'; -import { NativeAutoUpdater } from './NativeAutoUpdater.js'; -import { PackageManagerAutoUpdater } from './PackageManagerAutoUpdater.js'; -type Props = { - isUpdating: boolean; - onChangeIsUpdating: (isUpdating: boolean) => void; - onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void; - autoUpdaterResult: AutoUpdaterResult | null; - showSuccessMessage: boolean; - verbose: boolean; -}; -export function AutoUpdaterWrapper(t0) { - const $ = _c(17); - const { - isUpdating, - onChangeIsUpdating, - onAutoUpdaterResult, - autoUpdaterResult, - showSuccessMessage, - verbose - } = t0; - const [useNativeInstaller, setUseNativeInstaller] = React.useState(null); - const [isPackageManager, setIsPackageManager] = React.useState(null); - let t1; - let t2; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = () => { - const checkInstallation = async function checkInstallation() { - if (feature("SKIP_DETECTION_WHEN_AUTOUPDATES_DISABLED") && isAutoUpdaterDisabled()) { - logForDebugging("AutoUpdaterWrapper: Skipping detection, auto-updates disabled"); - return; - } - const installationType = await getCurrentInstallationType(); - logForDebugging(`AutoUpdaterWrapper: Installation type: ${installationType}`); - setUseNativeInstaller(installationType === "native"); - setIsPackageManager(installationType === "package-manager"); - }; - checkInstallation(); - }; - t2 = []; - $[0] = t1; - $[1] = t2; - } else { - t1 = $[0]; - t2 = $[1]; - } - React.useEffect(t1, t2); - if (useNativeInstaller === null || isPackageManager === null) { - return null; - } - if (isPackageManager) { - let t3; - if ($[2] !== autoUpdaterResult || $[3] !== isUpdating || $[4] !== onAutoUpdaterResult || $[5] !== onChangeIsUpdating || $[6] !== showSuccessMessage || $[7] !== verbose) { - t3 = ; - $[2] = autoUpdaterResult; - $[3] = isUpdating; - $[4] = onAutoUpdaterResult; - $[5] = onChangeIsUpdating; - $[6] = showSuccessMessage; - $[7] = verbose; - $[8] = t3; - } else { - t3 = $[8]; - } - return t3; - } - const Updater = useNativeInstaller ? NativeAutoUpdater : AutoUpdater; - let t3; - if ($[9] !== Updater || $[10] !== autoUpdaterResult || $[11] !== isUpdating || $[12] !== onAutoUpdaterResult || $[13] !== onChangeIsUpdating || $[14] !== showSuccessMessage || $[15] !== verbose) { - t3 = ; - $[9] = Updater; - $[10] = autoUpdaterResult; - $[11] = isUpdating; - $[12] = onAutoUpdaterResult; - $[13] = onChangeIsUpdating; - $[14] = showSuccessMessage; - $[15] = verbose; - $[16] = t3; - } else { - t3 = $[16]; - } - return t3; -} -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmZWF0dXJlIiwiUmVhY3QiLCJBdXRvVXBkYXRlclJlc3VsdCIsImlzQXV0b1VwZGF0ZXJEaXNhYmxlZCIsImxvZ0ZvckRlYnVnZ2luZyIsImdldEN1cnJlbnRJbnN0YWxsYXRpb25UeXBlIiwiQXV0b1VwZGF0ZXIiLCJOYXRpdmVBdXRvVXBkYXRlciIsIlBhY2thZ2VNYW5hZ2VyQXV0b1VwZGF0ZXIiLCJQcm9wcyIsImlzVXBkYXRpbmciLCJvbkNoYW5nZUlzVXBkYXRpbmciLCJvbkF1dG9VcGRhdGVyUmVzdWx0IiwiYXV0b1VwZGF0ZXJSZXN1bHQiLCJzaG93U3VjY2Vzc01lc3NhZ2UiLCJ2ZXJib3NlIiwiQXV0b1VwZGF0ZXJXcmFwcGVyIiwidDAiLCIkIiwiX2MiLCJ1c2VOYXRpdmVJbnN0YWxsZXIiLCJzZXRVc2VOYXRpdmVJbnN0YWxsZXIiLCJ1c2VTdGF0ZSIsImlzUGFja2FnZU1hbmFnZXIiLCJzZXRJc1BhY2thZ2VNYW5hZ2VyIiwidDEiLCJ0MiIsIlN5bWJvbCIsImZvciIsImNoZWNrSW5zdGFsbGF0aW9uIiwiaW5zdGFsbGF0aW9uVHlwZSIsInVzZUVmZmVjdCIsInQzIiwiVXBkYXRlciJdLCJzb3VyY2VzIjpbIkF1dG9VcGRhdGVyV3JhcHBlci50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgZmVhdHVyZSB9IGZyb20gJ2J1bjpidW5kbGUnXG5pbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB0eXBlIHsgQXV0b1VwZGF0ZXJSZXN1bHQgfSBmcm9tICcuLi91dGlscy9hdXRvVXBkYXRlci5qcydcbmltcG9ydCB7IGlzQXV0b1VwZGF0ZXJEaXNhYmxlZCB9IGZyb20gJy4uL3V0aWxzL2NvbmZpZy5qcydcbmltcG9ydCB7IGxvZ0ZvckRlYnVnZ2luZyB9IGZyb20gJy4uL3V0aWxzL2RlYnVnLmpzJ1xuaW1wb3J0IHsgZ2V0Q3VycmVudEluc3RhbGxhdGlvblR5cGUgfSBmcm9tICcuLi91dGlscy9kb2N0b3JEaWFnbm9zdGljLmpzJ1xuaW1wb3J0IHsgQXV0b1VwZGF0ZXIgfSBmcm9tICcuL0F1dG9VcGRhdGVyLmpzJ1xuaW1wb3J0IHsgTmF0aXZlQXV0b1VwZGF0ZXIgfSBmcm9tICcuL05hdGl2ZUF1dG9VcGRhdGVyLmpzJ1xuaW1wb3J0IHsgUGFja2FnZU1hbmFnZXJBdXRvVXBkYXRlciB9IGZyb20gJy4vUGFja2FnZU1hbmFnZXJBdXRvVXBkYXRlci5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgaXNVcGRhdGluZzogYm9vbGVhblxuICBvbkNoYW5nZUlzVXBkYXRpbmc6IChpc1VwZGF0aW5nOiBib29sZWFuKSA9PiB2b2lkXG4gIG9uQXV0b1VwZGF0ZXJSZXN1bHQ6IChhdXRvVXBkYXRlclJlc3VsdDogQXV0b1VwZGF0ZXJSZXN1bHQpID0+IHZvaWRcbiAgYXV0b1VwZGF0ZXJSZXN1bHQ6IEF1dG9VcGRhdGVyUmVzdWx0IHwgbnVsbFxuICBzaG93U3VjY2Vzc01lc3NhZ2U6IGJvb2xlYW5cbiAgdmVyYm9zZTogYm9vbGVhblxufVxuXG5leHBvcnQgZnVuY3Rpb24gQXV0b1VwZGF0ZXJXcmFwcGVyKHtcbiAgaXNVcGRhdGluZyxcbiAgb25DaGFuZ2VJc1VwZGF0aW5nLFxuICBvbkF1dG9VcGRhdGVyUmVzdWx0LFxuICBhdXRvVXBkYXRlclJlc3VsdCxcbiAgc2hvd1N1Y2Nlc3NNZXNzYWdlLFxuICB2ZXJib3NlLFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBbdXNlTmF0aXZlSW5zdGFsbGVyLCBzZXRVc2VOYXRpdmVJbnN0YWxsZXJdID0gUmVhY3QudXNlU3RhdGU8XG4gICAgYm9vbGVhbiB8IG51bGxcbiAgPihudWxsKVxuICBjb25zdCBbaXNQYWNrYWdlTWFuYWdlciwgc2V0SXNQYWNrYWdlTWFuYWdlcl0gPSBSZWFjdC51c2VTdGF0ZTxcbiAgICBib29sZWFuIHwgbnVsbFxuICA+KG51bGwpXG5cbiAgUmVhY3QudXNlRWZmZWN0KCgpID0+IHtcbiAgICBhc3luYyBmdW5jdGlvbiBjaGVja0luc3RhbGxhdGlvbigpIHtcbiAgICAgIC8vIFNraXAgaW5zdGFsbGF0aW9uIHR5cGUgZGV0ZWN0aW9uIGlmIGF1dG8tdXBkYXRlcyBhcmUgZGlzYWJsZWQgKGFudC1vbmx5KVxuICAgICAgLy8gVGhpcyBhdm9pZHMgcG90ZW50aWFsbHkgc2xvdyBwYWNrYWdlIG1hbmFnZXIgZGV0ZWN0aW9uIChzcGF3blN5bmMgY2FsbHMpXG4gICAgICBpZiAoXG4gICAgICAgIGZlYXR1cmUoJ1NLSVBfREVURUNUSU9OX1dIRU5fQVVUT1VQREFURVNfRElTQUJMRUQnKSAmJlxuICAgICAgICBpc0F1dG9VcGRhdGVyRGlzYWJsZWQoKVxuICAgICAgKSB7XG4gICAgICAgIGxvZ0ZvckRlYnVnZ2luZyhcbiAgICAgICAgICAnQXV0b1VwZGF0ZXJXcmFwcGVyOiBTa2lwcGluZyBkZXRlY3Rpb24sIGF1dG8tdXBkYXRlcyBkaXNhYmxlZCcsXG4gICAgICAgIClcbiAgICAgICAgcmV0dXJuXG4gICAgICB9XG5cbiAgICAgIGNvbnN0IGluc3RhbGxhdGlvblR5cGUgPSBhd2FpdCBnZXRDdXJyZW50SW5zdGFsbGF0aW9uVHlwZSgpXG4gICAgICBsb2dGb3JEZWJ1Z2dpbmcoXG4gICAgICAgIGBBdXRvVXBkYXRlcldyYXBwZXI6IEluc3RhbGxhdGlvbiB0eXBlOiAke2luc3RhbGxhdGlvblR5cGV9YCxcbiAgICAgIClcbiAgICAgIHNldFVzZU5hdGl2ZUluc3RhbGxlcihpbnN0YWxsYXRpb25UeXBlID09PSAnbmF0aXZlJylcbiAgICAgIHNldElzUGFja2FnZU1hbmFnZXIoaW5zdGFsbGF0aW9uVHlwZSA9PT0gJ3BhY2thZ2UtbWFuYWdlcicpXG4gICAgfVxuXG4gICAgdm9pZCBjaGVja0luc3RhbGxhdGlvbigpXG4gIH0sIFtdKVxuXG4gIC8vIERvbid0IHJlbmRlciB1bnRpbCB3ZSBrbm93IHRoZSBpbnN0YWxsYXRpb24gdHlwZVxuICBpZiAodXNlTmF0aXZlSW5zdGFsbGVyID09PSBudWxsIHx8IGlzUGFja2FnZU1hbmFnZXIgPT09IG51bGwpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG5cbiAgaWYgKGlzUGFja2FnZU1hbmFnZXIpIHtcbiAgICByZXR1cm4gKFxuICAgICAgPFBhY2thZ2VNYW5hZ2VyQXV0b1VwZGF0ZXJcbiAgICAgICAgdmVyYm9zZT17dmVyYm9zZX1cbiAgICAgICAgb25BdXRvVXBkYXRlclJlc3VsdD17b25BdXRvVXBkYXRlclJlc3VsdH1cbiAgICAgICAgYXV0b1VwZGF0ZXJSZXN1bHQ9e2F1dG9VcGRhdGVyUmVzdWx0fVxuICAgICAgICBpc1VwZGF0aW5nPXtpc1VwZGF0aW5nfVxuICAgICAgICBvbkNoYW5nZUlzVXBkYXRpbmc9e29uQ2hhbmdlSXNVcGRhdGluZ31cbiAgICAgICAgc2hvd1N1Y2Nlc3NNZXNzYWdlPXtzaG93U3VjY2Vzc01lc3NhZ2V9XG4gICAgICAvPlxuICAgIClcbiAgfVxuXG4gIGNvbnN0IFVwZGF0ZXIgPSB1c2VOYXRpdmVJbnN0YWxsZXIgPyBOYXRpdmVBdXRvVXBkYXRlciA6IEF1dG9VcGRhdGVyXG5cbiAgcmV0dXJuIChcbiAgICA8VXBkYXRlclxuICAgICAgdmVyYm9zZT17dmVyYm9zZX1cbiAgICAgIG9uQXV0b1VwZGF0ZXJSZXN1bHQ9e29uQXV0b1VwZGF0ZXJSZXN1bHR9XG4gICAgICBhdXRvVXBkYXRlclJlc3VsdD17YXV0b1VwZGF0ZXJSZXN1bHR9XG4gICAgICBpc1VwZGF0aW5nPXtpc1VwZGF0aW5nfVxuICAgICAgb25DaGFuZ2VJc1VwZGF0aW5nPXtvbkNoYW5nZUlzVXBkYXRpbmd9XG4gICAgICBzaG93U3VjY2Vzc01lc3NhZ2U9e3Nob3dTdWNjZXNzTWVzc2FnZX1cbiAgICAvPlxuICApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxTQUFTQSxPQUFPLFFBQVEsWUFBWTtBQUNwQyxPQUFPLEtBQUtDLEtBQUssTUFBTSxPQUFPO0FBQzlCLGNBQWNDLGlCQUFpQixRQUFRLHlCQUF5QjtBQUNoRSxTQUFTQyxxQkFBcUIsUUFBUSxvQkFBb0I7QUFDMUQsU0FBU0MsZUFBZSxRQUFRLG1CQUFtQjtBQUNuRCxTQUFTQywwQkFBMEIsUUFBUSw4QkFBOEI7QUFDekUsU0FBU0MsV0FBVyxRQUFRLGtCQUFrQjtBQUM5QyxTQUFTQyxpQkFBaUIsUUFBUSx3QkFBd0I7QUFDMUQsU0FBU0MseUJBQXlCLFFBQVEsZ0NBQWdDO0FBRTFFLEtBQUtDLEtBQUssR0FBRztFQUNYQyxVQUFVLEVBQUUsT0FBTztFQUNuQkMsa0JBQWtCLEVBQUUsQ0FBQ0QsVUFBVSxFQUFFLE9BQU8sRUFBRSxHQUFHLElBQUk7RUFDakRFLG1CQUFtQixFQUFFLENBQUNDLGlCQUFpQixFQUFFWCxpQkFBaUIsRUFBRSxHQUFHLElBQUk7RUFDbkVXLGlCQUFpQixFQUFFWCxpQkFBaUIsR0FBRyxJQUFJO0VBQzNDWSxrQkFBa0IsRUFBRSxPQUFPO0VBQzNCQyxPQUFPLEVBQUUsT0FBTztBQUNsQixDQUFDO0FBRUQsT0FBTyxTQUFBQyxtQkFBQUMsRUFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUE0QjtJQUFBVCxVQUFBO0lBQUFDLGtCQUFBO0lBQUFDLG1CQUFBO0lBQUFDLGlCQUFBO0lBQUFDLGtCQUFBO0lBQUFDO0VBQUEsSUFBQUUsRUFPM0I7RUFDTixPQUFBRyxrQkFBQSxFQUFBQyxxQkFBQSxJQUFvRHBCLEtBQUssQ0FBQXFCLFFBQVMsQ0FFaEUsSUFBSSxDQUFDO0VBQ1AsT0FBQUMsZ0JBQUEsRUFBQUMsbUJBQUEsSUFBZ0R2QixLQUFLLENBQUFxQixRQUFTLENBRTVELElBQUksQ0FBQztFQUFBLElBQUFHLEVBQUE7RUFBQSxJQUFBQyxFQUFBO0VBQUEsSUFBQVIsQ0FBQSxRQUFBUyxNQUFBLENBQUFDLEdBQUE7SUFFU0gsRUFBQSxHQUFBQSxDQUFBO01BQ2QsTUFBQUksaUJBQUEsa0JBQUFBLGtCQUFBO1FBR0UsSUFDRTdCLE9BQU8sQ0FBQywwQ0FDYyxDQUFDLElBQXZCRyxxQkFBcUIsQ0FBQyxDQUFDO1VBRXZCQyxlQUFlLENBQ2IsK0RBQ0YsQ0FBQztVQUFBO1FBQUE7UUFJSCxNQUFBMEIsZ0JBQUEsR0FBeUIsTUFBTXpCLDBCQUEwQixDQUFDLENBQUM7UUFDM0RELGVBQWUsQ0FDYiwwQ0FBMEMwQixnQkFBZ0IsRUFDNUQsQ0FBQztRQUNEVCxxQkFBcUIsQ0FBQ1MsZ0JBQWdCLEtBQUssUUFBUSxDQUFDO1FBQ3BETixtQkFBbUIsQ0FBQ00sZ0JBQWdCLEtBQUssaUJBQWlCLENBQUM7TUFBQSxDQUM1RDtNQUVJRCxpQkFBaUIsQ0FBQyxDQUFDO0lBQUEsQ0FDekI7SUFBRUgsRUFBQSxLQUFFO0lBQUFSLENBQUEsTUFBQU8sRUFBQTtJQUFBUCxDQUFBLE1BQUFRLEVBQUE7RUFBQTtJQUFBRCxFQUFBLEdBQUFQLENBQUE7SUFBQVEsRUFBQSxHQUFBUixDQUFBO0VBQUE7RUF2QkxqQixLQUFLLENBQUE4QixTQUFVLENBQUNOLEVBdUJmLEVBQUVDLEVBQUUsQ0FBQztFQUdOLElBQUlOLGtCQUFrQixLQUFLLElBQWlDLElBQXpCRyxnQkFBZ0IsS0FBSyxJQUFJO0lBQUEsT0FDbkQsSUFBSTtFQUFBO0VBR2IsSUFBSUEsZ0JBQWdCO0lBQUEsSUFBQVMsRUFBQTtJQUFBLElBQUFkLENBQUEsUUFBQUwsaUJBQUEsSUFBQUssQ0FBQSxRQUFBUixVQUFBLElBQUFRLENBQUEsUUFBQU4sbUJBQUEsSUFBQU0sQ0FBQSxRQUFBUCxrQkFBQSxJQUFBTyxDQUFBLFFBQUFKLGtCQUFBLElBQUFJLENBQUEsUUFBQUgsT0FBQTtNQUVoQmlCLEVBQUEsSUFBQyx5QkFBeUIsQ0FDZmpCLE9BQU8sQ0FBUEEsUUFBTSxDQUFDLENBQ0tILG1CQUFtQixDQUFuQkEsb0JBQWtCLENBQUMsQ0FDckJDLGlCQUFpQixDQUFqQkEsa0JBQWdCLENBQUMsQ0FDeEJILFVBQVUsQ0FBVkEsV0FBUyxDQUFDLENBQ0ZDLGtCQUFrQixDQUFsQkEsbUJBQWlCLENBQUMsQ0FDbEJHLGtCQUFrQixDQUFsQkEsbUJBQWlCLENBQUMsR0FDdEM7TUFBQUksQ0FBQSxNQUFBTCxpQkFBQTtNQUFBSyxDQUFBLE1BQUFSLFVBQUE7TUFBQVEsQ0FBQSxNQUFBTixtQkFBQTtNQUFBTSxDQUFBLE1BQUFQLGtCQUFBO01BQUFPLENBQUEsTUFBQUosa0JBQUE7TUFBQUksQ0FBQSxNQUFBSCxPQUFBO01BQUFHLENBQUEsTUFBQWMsRUFBQTtJQUFBO01BQUFBLEVBQUEsR0FBQWQsQ0FBQTtJQUFBO0lBQUEsT0FQRmMsRUFPRTtFQUFBO0VBSU4sTUFBQUMsT0FBQSxHQUFnQmIsa0JBQWtCLEdBQWxCYixpQkFBb0QsR0FBcERELFdBQW9EO0VBQUEsSUFBQTBCLEVBQUE7RUFBQSxJQUFBZCxDQUFBLFFBQUFlLE9BQUEsSUFBQWYsQ0FBQSxTQUFBTCxpQkFBQSxJQUFBSyxDQUFBLFNBQUFSLFVBQUEsSUFBQVEsQ0FBQSxTQUFBTixtQkFBQSxJQUFBTSxDQUFBLFNBQUFQLGtCQUFBLElBQUFPLENBQUEsU0FBQUosa0JBQUEsSUFBQUksQ0FBQSxTQUFBSCxPQUFBO0lBR2xFaUIsRUFBQSxJQUFDLE9BQU8sQ0FDR2pCLE9BQU8sQ0FBUEEsUUFBTSxDQUFDLENBQ0tILG1CQUFtQixDQUFuQkEsb0JBQWtCLENBQUMsQ0FDckJDLGlCQUFpQixDQUFqQkEsa0JBQWdCLENBQUMsQ0FDeEJILFVBQVUsQ0FBVkEsV0FBUyxDQUFDLENBQ0ZDLGtCQUFrQixDQUFsQkEsbUJBQWlCLENBQUMsQ0FDbEJHLGtCQUFrQixDQUFsQkEsbUJBQWlCLENBQUMsR0FDdEM7SUFBQUksQ0FBQSxNQUFBZSxPQUFBO0lBQUFmLENBQUEsT0FBQUwsaUJBQUE7SUFBQUssQ0FBQSxPQUFBUixVQUFBO0lBQUFRLENBQUEsT0FBQU4sbUJBQUE7SUFBQU0sQ0FBQSxPQUFBUCxrQkFBQTtJQUFBTyxDQUFBLE9BQUFKLGtCQUFBO0lBQUFJLENBQUEsT0FBQUgsT0FBQTtJQUFBRyxDQUFBLE9BQUFjLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFkLENBQUE7RUFBQTtFQUFBLE9BUEZjLEVBT0U7QUFBQSIsImlnbm9yZUxpc3QiOltdfQ== \ No newline at end of file + +export function AutoUpdaterWrapper(): React.ReactNode { + return null; +} \ No newline at end of file diff --git a/src/components/BypassPermissionsModeDialog.tsx b/src/components/BypassPermissionsModeDialog.tsx index ed09416c..602a54c7 100644 --- a/src/components/BypassPermissionsModeDialog.tsx +++ b/src/components/BypassPermissionsModeDialog.tsx @@ -1,6 +1,6 @@ import { c as _c } from "react/compiler-runtime"; import React, { useCallback } from 'react'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import { Box, Link, Newline, Text } from '../ink.js'; import { gracefulShutdownSync } from '../utils/gracefulShutdown.js'; import { updateSettingsForSource } from '../utils/settings/settings.js'; diff --git a/src/components/ClaudeInChromeOnboarding.tsx b/src/components/ClaudeInChromeOnboarding.tsx index 10fb1823..b35a244e 100644 --- a/src/components/ClaudeInChromeOnboarding.tsx +++ b/src/components/ClaudeInChromeOnboarding.tsx @@ -1,6 +1,6 @@ import { c as _c } from "react/compiler-runtime"; import React from 'react'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; // eslint-disable-next-line custom-rules/prefer-use-keybindings -- enter to continue import { Box, Link, Newline, Text, useInput } from '../ink.js'; import { isChromeExtensionInstalled } from '../utils/claudeInChrome/setup.js'; diff --git a/src/components/ClaudeMdExternalIncludesDialog.tsx b/src/components/ClaudeMdExternalIncludesDialog.tsx index 2428e89f..bf9f700c 100644 --- a/src/components/ClaudeMdExternalIncludesDialog.tsx +++ b/src/components/ClaudeMdExternalIncludesDialog.tsx @@ -1,6 +1,6 @@ import { c as _c } from "react/compiler-runtime"; import React, { useCallback } from 'react'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import { Box, Link, Text } from '../ink.js'; import type { ExternalClaudeMdInclude } from '../utils/claudemd.js'; import { saveCurrentProjectConfig } from '../utils/config.js'; diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 717697f6..671569d1 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -1,6 +1,6 @@ import { c as _c } from "react/compiler-runtime"; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; import { installOAuthTokens } from '../cli/handlers/auth.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { setClipboard } from '../ink/termio/osc.js'; diff --git a/src/components/DesktopUpsell/DesktopUpsellStartup.tsx b/src/components/DesktopUpsell/DesktopUpsellStartup.tsx index e919039d..a371eb07 100644 --- a/src/components/DesktopUpsell/DesktopUpsellStartup.tsx +++ b/src/components/DesktopUpsell/DesktopUpsellStartup.tsx @@ -2,8 +2,8 @@ import { c as _c } from "react/compiler-runtime"; import * as React from 'react'; import { useEffect, useState } from 'react'; import { Box, Text } from '../../ink.js'; -import { getDynamicConfig_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { getDynamicConfig_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; import { Select } from '../CustomSelect/select.js'; import { DesktopHandoff } from '../DesktopHandoff.js'; diff --git a/src/components/Feedback.tsx b/src/components/Feedback.tsx index 8f2fbb1b..96fe4ed4 100644 --- a/src/components/Feedback.tsx +++ b/src/components/Feedback.tsx @@ -3,8 +3,8 @@ import { readFile, stat } from 'fs/promises'; import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { getLastAPIRequest } from 'src/bootstrap/state.js'; -import { logEventTo1P } from 'src/services/analytics/firstPartyEventLogger.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { logEventTo1P } from '../services/analytics-stub.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; import { getLastAssistantMessage, normalizeMessagesForAPI } from 'src/utils/messages.js'; import type { CommandResultDisplay } from '../commands.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; diff --git a/src/components/FeedbackSurvey/FeedbackSurvey.tsx b/src/components/FeedbackSurvey/FeedbackSurvey.tsx index dc3e7120..e27bb17c 100644 --- a/src/components/FeedbackSurvey/FeedbackSurvey.tsx +++ b/src/components/FeedbackSurvey/FeedbackSurvey.tsx @@ -1,6 +1,6 @@ import { c as _c } from "react/compiler-runtime"; import React from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { Box, Text } from '../../ink.js'; import { FeedbackSurveyView, isValidResponseInput } from './FeedbackSurveyView.js'; import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'; diff --git a/src/components/FeedbackSurvey/useFeedbackSurvey.tsx b/src/components/FeedbackSurvey/useFeedbackSurvey.tsx index 20bab3d7..ef91eecd 100644 --- a/src/components/FeedbackSurvey/useFeedbackSurvey.tsx +++ b/src/components/FeedbackSurvey/useFeedbackSurvey.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDynamicConfig } from 'src/hooks/useDynamicConfig.js'; -import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { isFeedbackSurveyDisabled } from '../../services/analytics-stub.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { isPolicyAllowed } from '../../services/policyLimits/index.js'; import type { Message } from '../../types/message.js'; import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; @@ -9,7 +9,7 @@ import { isEnvTruthy } from '../../utils/envUtils.js'; import { getLastAssistantMessage } from '../../utils/messages.js'; import { getMainLoopModel } from '../../utils/model/model.js'; import { getInitialSettings } from '../../utils/settings/settings.js'; -import { logOTelEvent } from '../../utils/telemetry/events.js'; +import { logOTelEvent } from '../../utils/telemetry-stub.js'; import { submitTranscriptShare, type TranscriptShareTrigger } from './submitTranscriptShare.js'; import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'; import { useSurveyState } from './useSurveyState.js'; diff --git a/src/components/FeedbackSurvey/useMemorySurvey.tsx b/src/components/FeedbackSurvey/useMemorySurvey.tsx index e7d9b18b..c0a076dc 100644 --- a/src/components/FeedbackSurvey/useMemorySurvey.tsx +++ b/src/components/FeedbackSurvey/useMemorySurvey.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { isFeedbackSurveyDisabled } from '../../services/analytics-stub.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { isAutoMemoryEnabled } from '../../memdir/paths.js'; import { isPolicyAllowed } from '../../services/policyLimits/index.js'; import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js'; @@ -10,7 +10,7 @@ import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; import { isEnvTruthy } from '../../utils/envUtils.js'; import { isAutoManagedMemoryFile } from '../../utils/memoryFileDetection.js'; import { extractTextContent, getLastAssistantMessage } from '../../utils/messages.js'; -import { logOTelEvent } from '../../utils/telemetry/events.js'; +import { logOTelEvent } from '../../utils/telemetry-stub.js'; import { submitTranscriptShare } from './submitTranscriptShare.js'; import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'; import { useSurveyState } from './useSurveyState.js'; diff --git a/src/components/FeedbackSurvey/usePostCompactSurvey.tsx b/src/components/FeedbackSurvey/usePostCompactSurvey.tsx index b33281a1..a34e611f 100644 --- a/src/components/FeedbackSurvey/usePostCompactSurvey.tsx +++ b/src/components/FeedbackSurvey/usePostCompactSurvey.tsx @@ -1,13 +1,13 @@ import { c as _c } from "react/compiler-runtime"; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'; -import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { isFeedbackSurveyDisabled } from '../../services/analytics-stub.js'; +import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { shouldUseSessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js'; import type { Message } from '../../types/message.js'; import { isEnvTruthy } from '../../utils/envUtils.js'; import { isCompactBoundaryMessage } from '../../utils/messages.js'; -import { logOTelEvent } from '../../utils/telemetry/events.js'; +import { logOTelEvent } from '../../utils/telemetry-stub.js'; import { useSurveyState } from './useSurveyState.js'; import type { FeedbackSurveyResponse } from './utils.js'; const HIDE_THANKS_AFTER_MS = 3000; diff --git a/src/components/GlobalSearchDialog.tsx b/src/components/GlobalSearchDialog.tsx index b4551e22..4477fac8 100644 --- a/src/components/GlobalSearchDialog.tsx +++ b/src/components/GlobalSearchDialog.tsx @@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from 'react'; import { useRegisterOverlay } from '../context/overlayContext.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { Text } from '../ink.js'; -import { logEvent } from '../services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import { getCwd } from '../utils/cwd.js'; import { openFileInExternalEditor } from '../utils/editor.js'; import { truncatePathMiddle, truncateToWidth } from '../utils/format.js'; diff --git a/src/components/HistorySearchDialog.tsx b/src/components/HistorySearchDialog.tsx index 639787ef..4ae70e98 100644 --- a/src/components/HistorySearchDialog.tsx +++ b/src/components/HistorySearchDialog.tsx @@ -6,7 +6,7 @@ import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { stringWidth } from '../ink/stringWidth.js'; import { wrapAnsi } from '../ink/wrapAnsi.js'; import { Box, Text } from '../ink.js'; -import { logEvent } from '../services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import type { HistoryEntry } from '../utils/config.js'; import { formatRelativeTimeAgo, truncateToWidth } from '../utils/format.js'; import { FuzzyPicker } from './design-system/FuzzyPicker.js'; diff --git a/src/components/LogSelector.tsx b/src/components/LogSelector.tsx index 8b4ba2dc..5f59a930 100644 --- a/src/components/LogSelector.tsx +++ b/src/components/LogSelector.tsx @@ -11,7 +11,7 @@ import { applyColor } from '../ink/colorize.js'; import type { Color } from '../ink/styles.js'; import { Box, Text, useInput, useTerminalFocus, useTheme } from '../ink.js'; import { useKeybinding } from '../keybindings/useKeybinding.js'; -import { logEvent } from '../services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import type { LogOption, SerializedMessage } from '../types/logs.js'; import { formatLogMetadata, truncateToWidth } from '../utils/format.js'; import { getWorktreePaths } from '../utils/getWorktreePaths.js'; diff --git a/src/components/LogoV2/ChannelsNotice.tsx b/src/components/LogoV2/ChannelsNotice.tsx index 865ccd7e..da4135fc 100644 --- a/src/components/LogoV2/ChannelsNotice.tsx +++ b/src/components/LogoV2/ChannelsNotice.tsx @@ -135,7 +135,7 @@ export function ChannelsNotice() { } let t2; if ($[24] !== flag) { - t2 = Experimental · inbound messages will be pushed into this session, this carries prompt injection risks. Restart Free Code without {flag} to disable.; + t2 = Experimental · inbound messages will be pushed into this session, this carries prompt injection risks. Restart Claudium without {flag} to disable.; $[24] = flag; $[25] = t2; } else { diff --git a/src/components/LogoV2/CondensedLogo.tsx b/src/components/LogoV2/CondensedLogo.tsx index 91b940c5..5ed4076a 100644 --- a/src/components/LogoV2/CondensedLogo.tsx +++ b/src/components/LogoV2/CondensedLogo.tsx @@ -88,7 +88,7 @@ export function CondensedLogo() { } let t5; if ($[8] === Symbol.for("react.memo_cache_sentinel")) { - t5 = Free Code; + t5 = Claudium; $[8] = t5; } else { t5 = $[8]; diff --git a/src/components/LogoV2/EmergencyTip.tsx b/src/components/LogoV2/EmergencyTip.tsx index ff6a83e3..820db26c 100644 --- a/src/components/LogoV2/EmergencyTip.tsx +++ b/src/components/LogoV2/EmergencyTip.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useEffect, useMemo } from 'react'; import { Box, Text } from 'src/ink.js'; -import { getDynamicConfig_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'; +import { getDynamicConfig_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js'; import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js'; const CONFIG_NAME = 'tengu-top-of-feed-tip'; export function EmergencyTip(): React.ReactNode { diff --git a/src/components/LogoV2/GuestPassesUpsell.tsx b/src/components/LogoV2/GuestPassesUpsell.tsx index e5d5fb63..81cc6eb7 100644 --- a/src/components/LogoV2/GuestPassesUpsell.tsx +++ b/src/components/LogoV2/GuestPassesUpsell.tsx @@ -2,7 +2,7 @@ import { c as _c } from "react/compiler-runtime"; import * as React from 'react'; import { useState } from 'react'; import { Text } from '../../ink.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { checkCachedPassesEligibility, formatCreditAmount, getCachedReferrerReward, getCachedRemainingPasses } from '../../services/api/referral.js'; import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; function resetIfPassesRefreshed(): void { @@ -60,7 +60,7 @@ export function GuestPassesUpsell() { let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { const reward = getCachedReferrerReward(); - t0 = [✻] [✻]{" "}[✻] ·{" "}{reward ? `Share Free Code and earn ${formatCreditAmount(reward)} of extra usage · /passes` : "3 guest passes at /passes"}; + t0 = [✻] [✻]{" "}[✻] ·{" "}{reward ? `Share Claudium and earn ${formatCreditAmount(reward)} of extra usage · /passes` : "3 guest passes at /passes"}; $[0] = t0; } else { t0 = $[0]; diff --git a/src/components/LogoV2/LogoV2.tsx b/src/components/LogoV2/LogoV2.tsx index bc54168c..3199964d 100644 --- a/src/components/LogoV2/LogoV2.tsx +++ b/src/components/LogoV2/LogoV2.tsx @@ -248,8 +248,8 @@ export function LogoV2() { } const layoutMode = getLayoutMode(columns); const userTheme = resolveThemeSetting(getGlobalConfig().theme); - const borderTitle = ` ${color("startupAccent", userTheme)("Free Code")} ${color("inactive", userTheme)(`v${version}`)} `; - const compactBorderTitle = color("startupAccent", userTheme)(" Free Code "); + const borderTitle = ` ${color("startupAccent", userTheme)("Claudium")} ${color("inactive", userTheme)(`v${version}`)} `; + const compactBorderTitle = color("startupAccent", userTheme)(" Claudium "); if (layoutMode === "compact") { let welcomeMessage = formatWelcomeMessage(username); if (stringWidth(welcomeMessage) > columns - 4) { diff --git a/src/components/LogoV2/OverageCreditUpsell.tsx b/src/components/LogoV2/OverageCreditUpsell.tsx index 8c4a19f7..7bf80db4 100644 --- a/src/components/LogoV2/OverageCreditUpsell.tsx +++ b/src/components/LogoV2/OverageCreditUpsell.tsx @@ -2,7 +2,7 @@ import { c as _c } from "react/compiler-runtime"; import * as React from 'react'; import { useState } from 'react'; import { Text } from '../../ink.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { formatGrantAmount, getCachedOverageCreditGrant, refreshOverageCreditGrantCache } from '../../services/api/overageCreditGrant.js'; import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; import { truncate } from '../../utils/format.js'; diff --git a/src/components/LogoV2/WelcomeV2.tsx b/src/components/LogoV2/WelcomeV2.tsx index 296c777d..381b6aba 100644 --- a/src/components/LogoV2/WelcomeV2.tsx +++ b/src/components/LogoV2/WelcomeV2.tsx @@ -9,7 +9,7 @@ export function WelcomeV2() { if (env.terminal === "Apple_Terminal") { let t0; if ($[0] !== theme) { - t0 = ; + t0 = ; $[0] = theme; $[1] = t0; } else { @@ -28,7 +28,7 @@ export function WelcomeV2() { let t7; let t8; if ($[2] === Symbol.for("react.memo_cache_sentinel")) { - t0 = {"Welcome to Free Code"} v{MACRO.VERSION} ; + t0 = {"Welcome to Claudium"} v{MACRO.VERSION} ; t1 = {"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}; t2 = {" "}; t3 = {" "}; @@ -113,7 +113,7 @@ export function WelcomeV2() { let t5; let t6; if ($[18] === Symbol.for("react.memo_cache_sentinel")) { - t0 = {"Welcome to Free Code"} v{MACRO.VERSION} ; + t0 = {"Welcome to Claudium"} v{MACRO.VERSION} ; t1 = {"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}; t2 = {" "}; t3 = {" * \u2588\u2588\u2588\u2588\u2588\u2593\u2593\u2591 "}; diff --git a/src/components/LogoV2/feedConfigs.tsx b/src/components/LogoV2/feedConfigs.tsx index 44854154..a5ae0ff6 100644 --- a/src/components/LogoV2/feedConfigs.tsx +++ b/src/components/LogoV2/feedConfigs.tsx @@ -39,7 +39,7 @@ export function createWhatsNewFeed(releaseNotes: string[]): FeedConfig { text: note }; }); - const emptyMessage = "external" === 'ant' ? 'Unable to fetch latest claude-cli-internal commits' : 'Check the Free Code changelog for updates'; + const emptyMessage = "external" === 'ant' ? 'Unable to fetch latest claude-cli-internal commits' : 'Check the Claudium changelog for updates'; return { title: "external" === 'ant' ? "What's new [ANT-ONLY: Latest CC commits]" : "What's new", lines, @@ -73,7 +73,7 @@ export function createProjectOnboardingFeed(steps: Step[]): FeedConfig { } export function createGuestPassesFeed(): FeedConfig { const reward = getCachedReferrerReward(); - const subtitle = reward ? `Share Free Code and earn ${formatCreditAmount(reward)} of extra usage` : 'Share Free Code with friends'; + const subtitle = reward ? `Share Claudium and earn ${formatCreditAmount(reward)} of extra usage` : 'Share Claudium with friends'; return { title: '3 guest passes', lines: [], diff --git a/src/components/MCPServerApprovalDialog.tsx b/src/components/MCPServerApprovalDialog.tsx index f75abcce..709cc3da 100644 --- a/src/components/MCPServerApprovalDialog.tsx +++ b/src/components/MCPServerApprovalDialog.tsx @@ -1,6 +1,6 @@ import { c as _c } from "react/compiler-runtime"; import React from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js'; import { Select } from './CustomSelect/index.js'; import { Dialog } from './design-system/Dialog.js'; diff --git a/src/components/MCPServerMultiselectDialog.tsx b/src/components/MCPServerMultiselectDialog.tsx index b5325ba4..26ba3e1f 100644 --- a/src/components/MCPServerMultiselectDialog.tsx +++ b/src/components/MCPServerMultiselectDialog.tsx @@ -1,7 +1,7 @@ import { c as _c } from "react/compiler-runtime"; import partition from 'lodash-es/partition.js'; import React, { useCallback } from 'react'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import { Box, Text } from '../ink.js'; import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js'; import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'; diff --git a/src/components/MessageSelector.tsx b/src/components/MessageSelector.tsx index 7df5b566..f751b3d0 100644 --- a/src/components/MessageSelector.tsx +++ b/src/components/MessageSelector.tsx @@ -4,7 +4,7 @@ import { randomUUID, type UUID } from 'crypto'; import figures from 'figures'; import * as React from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; import { useAppState } from 'src/state/AppState.js'; import { type DiffStats, fileHistoryCanRestore, fileHistoryEnabled, fileHistoryGetDiffStats } from 'src/utils/fileHistory.js'; import { logError } from 'src/utils/log.js'; diff --git a/src/components/ModelPicker.tsx b/src/components/ModelPicker.tsx index 4bf18bc5..9fe6354b 100644 --- a/src/components/ModelPicker.tsx +++ b/src/components/ModelPicker.tsx @@ -3,7 +3,7 @@ import capitalize from 'lodash-es/capitalize.js'; import * as React from 'react'; import { useCallback, useMemo, useState } from 'react'; import { useExitOnCtrlCDWithKeybindings } from 'src/hooks/useExitOnCtrlCDWithKeybindings.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; import { FAST_MODE_MODEL_DISPLAY, isFastModeAvailable, isFastModeCooldown, isFastModeEnabled } from 'src/utils/fastMode.js'; import { Box, Text } from '../ink.js'; import { useKeybindings } from '../keybindings/useKeybinding.js'; diff --git a/src/components/NativeAutoUpdater.tsx b/src/components/NativeAutoUpdater.tsx index a71244dd..70611789 100644 --- a/src/components/NativeAutoUpdater.tsx +++ b/src/components/NativeAutoUpdater.tsx @@ -1,193 +1,11 @@ -import * as React from 'react'; -import { useEffect, useRef, useState } from 'react'; -import { logEvent } from 'src/services/analytics/index.js'; -import { logForDebugging } from 'src/utils/debug.js'; -import { logError } from 'src/utils/log.js'; -import { useInterval } from 'usehooks-ts'; -import { useUpdateNotification } from '../hooks/useUpdateNotification.js'; -import { Box, Text } from '../ink.js'; -import type { AutoUpdaterResult } from '../utils/autoUpdater.js'; -import { getMaxVersion, getMaxVersionMessage } from '../utils/autoUpdater.js'; -import { isAutoUpdaterDisabled } from '../utils/config.js'; -import { installLatest } from '../utils/nativeInstaller/index.js'; -import { gt } from '../utils/semver.js'; -import { getInitialSettings } from '../utils/settings/settings.js'; - /** - * Categorize error messages for analytics + * NativeAutoUpdater stub for claude-code-clean + * All auto-update functionality disabled */ -function getErrorType(errorMessage: string): string { - if (errorMessage.includes('timeout')) { - return 'timeout'; - } - if (errorMessage.includes('Checksum mismatch')) { - return 'checksum_mismatch'; - } - if (errorMessage.includes('ENOENT') || errorMessage.includes('not found')) { - return 'not_found'; - } - if (errorMessage.includes('EACCES') || errorMessage.includes('permission')) { - return 'permission_denied'; - } - if (errorMessage.includes('ENOSPC')) { - return 'disk_full'; - } - if (errorMessage.includes('npm')) { - return 'npm_error'; - } - if (errorMessage.includes('network') || errorMessage.includes('ECONNREFUSED') || errorMessage.includes('ENOTFOUND')) { - return 'network_error'; - } - return 'unknown'; -} -type Props = { - isUpdating: boolean; - onChangeIsUpdating: (isUpdating: boolean) => void; - onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void; - autoUpdaterResult: AutoUpdaterResult | null; - showSuccessMessage: boolean; - verbose: boolean; -}; -export function NativeAutoUpdater({ - isUpdating, - onChangeIsUpdating, - onAutoUpdaterResult, - autoUpdaterResult, - showSuccessMessage, - verbose -}: Props): React.ReactNode { - const [versions, setVersions] = useState<{ - current?: string | null; - latest?: string | null; - }>({}); - const [maxVersionIssue, setMaxVersionIssue] = useState(null); - const updateSemver = useUpdateNotification(autoUpdaterResult?.version); - const channel = getInitialSettings()?.autoUpdatesChannel ?? 'latest'; - - // Track latest isUpdating value in a ref so the memoized checkForUpdates - // callback always sees the current value without changing callback identity - // (which would re-trigger the initial-check useEffect below and cause - // repeated downloads on remount — the upstream trigger for #22413). - const isUpdatingRef = useRef(isUpdating); - isUpdatingRef.current = isUpdating; - const checkForUpdates = React.useCallback(async () => { - if (isUpdatingRef.current) { - return; - } - if ("production" === 'test' || "production" === 'development') { - logForDebugging('NativeAutoUpdater: Skipping update check in test/dev environment'); - return; - } - if (isAutoUpdaterDisabled()) { - return; - } - onChangeIsUpdating(true); - const startTime = Date.now(); - // Log the start of an auto-update check for funnel analysis - logEvent('tengu_native_auto_updater_start', {}); - try { - // Check if current version is above the max allowed version - const maxVersion = await getMaxVersion(); - if (maxVersion && gt(MACRO.VERSION, maxVersion)) { - const msg = await getMaxVersionMessage(); - setMaxVersionIssue(msg ?? 'affects your version'); - } - const result = await installLatest(channel); - const currentVersion = MACRO.VERSION; - const latencyMs = Date.now() - startTime; - - // Handle lock contention gracefully - just return without treating as error - if (result.lockFailed) { - logEvent('tengu_native_auto_updater_lock_contention', { - latency_ms: latencyMs - }); - return; // Silently skip this update check, will try again later - } - - // Update versions for display - setVersions({ - current: currentVersion, - latest: result.latestVersion - }); - if (result.wasUpdated) { - logEvent('tengu_native_auto_updater_success', { - latency_ms: latencyMs - }); - onAutoUpdaterResult({ - version: result.latestVersion, - status: 'success' - }); - } else { - // Already up to date - logEvent('tengu_native_auto_updater_up_to_date', { - latency_ms: latencyMs - }); - } - } catch (error) { - const latencyMs = Date.now() - startTime; - const errorMessage = error instanceof Error ? error.message : String(error); - logError(error); - const errorType = getErrorType(errorMessage); - logEvent('tengu_native_auto_updater_fail', { - latency_ms: latencyMs, - error_timeout: errorType === 'timeout', - error_checksum: errorType === 'checksum_mismatch', - error_not_found: errorType === 'not_found', - error_permission: errorType === 'permission_denied', - error_disk_full: errorType === 'disk_full', - error_npm: errorType === 'npm_error', - error_network: errorType === 'network_error' - }); - onAutoUpdaterResult({ - version: null, - status: 'install_failed' - }); - } finally { - onChangeIsUpdating(false); - } - // isUpdating intentionally omitted from deps; we read isUpdatingRef - // instead so the guard is always current without changing callback - // identity (which would re-trigger the initial-check useEffect below). - // eslint-disable-next-line react-hooks/exhaustive-deps - // biome-ignore lint/correctness/useExhaustiveDependencies: isUpdating read via ref - }, [onAutoUpdaterResult, channel]); - - // Initial check - useEffect(() => { - void checkForUpdates(); - }, [checkForUpdates]); +import * as React from 'react'; - // Check every 30 minutes - useInterval(checkForUpdates, 30 * 60 * 1000); - const hasUpdateResult = !!autoUpdaterResult?.version; - const hasVersionInfo = !!versions.current && !!versions.latest; - // Show the component when: - // - warning banner needed (above max version), or - // - there's an update result to display (success/error), or - // - actively checking and we have version info to show - const shouldRender = !!maxVersionIssue || hasUpdateResult || isUpdating && hasVersionInfo; - if (!shouldRender) { - return null; - } - return - {verbose && - current: {versions.current} · {channel}: {versions.latest} - } - {isUpdating ? - - Checking for updates - - : autoUpdaterResult?.status === 'success' && showSuccessMessage && updateSemver && - ✓ Update installed · Restart to update - } - {autoUpdaterResult?.status === 'install_failed' && - ✗ Auto-update failed · Try /status - } - {maxVersionIssue && "external" === 'ant' && - ⚠ Known issue: {maxVersionIssue} · Run{' '} - claude rollback --safe to downgrade - } - ; +export function NativeAutoUpdater(): React.ReactNode { + // No-op: Auto-updates disabled in claude-code-clean + return null; } -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZUVmZmVjdCIsInVzZVJlZiIsInVzZVN0YXRlIiwibG9nRXZlbnQiLCJsb2dGb3JEZWJ1Z2dpbmciLCJsb2dFcnJvciIsInVzZUludGVydmFsIiwidXNlVXBkYXRlTm90aWZpY2F0aW9uIiwiQm94IiwiVGV4dCIsIkF1dG9VcGRhdGVyUmVzdWx0IiwiZ2V0TWF4VmVyc2lvbiIsImdldE1heFZlcnNpb25NZXNzYWdlIiwiaXNBdXRvVXBkYXRlckRpc2FibGVkIiwiaW5zdGFsbExhdGVzdCIsImd0IiwiZ2V0SW5pdGlhbFNldHRpbmdzIiwiZ2V0RXJyb3JUeXBlIiwiZXJyb3JNZXNzYWdlIiwiaW5jbHVkZXMiLCJQcm9wcyIsImlzVXBkYXRpbmciLCJvbkNoYW5nZUlzVXBkYXRpbmciLCJvbkF1dG9VcGRhdGVyUmVzdWx0IiwiYXV0b1VwZGF0ZXJSZXN1bHQiLCJzaG93U3VjY2Vzc01lc3NhZ2UiLCJ2ZXJib3NlIiwiTmF0aXZlQXV0b1VwZGF0ZXIiLCJSZWFjdE5vZGUiLCJ2ZXJzaW9ucyIsInNldFZlcnNpb25zIiwiY3VycmVudCIsImxhdGVzdCIsIm1heFZlcnNpb25Jc3N1ZSIsInNldE1heFZlcnNpb25Jc3N1ZSIsInVwZGF0ZVNlbXZlciIsInZlcnNpb24iLCJjaGFubmVsIiwiYXV0b1VwZGF0ZXNDaGFubmVsIiwiaXNVcGRhdGluZ1JlZiIsImNoZWNrRm9yVXBkYXRlcyIsInVzZUNhbGxiYWNrIiwic3RhcnRUaW1lIiwiRGF0ZSIsIm5vdyIsIm1heFZlcnNpb24iLCJNQUNSTyIsIlZFUlNJT04iLCJtc2ciLCJyZXN1bHQiLCJjdXJyZW50VmVyc2lvbiIsImxhdGVuY3lNcyIsImxvY2tGYWlsZWQiLCJsYXRlbmN5X21zIiwibGF0ZXN0VmVyc2lvbiIsIndhc1VwZGF0ZWQiLCJzdGF0dXMiLCJlcnJvciIsIkVycm9yIiwibWVzc2FnZSIsIlN0cmluZyIsImVycm9yVHlwZSIsImVycm9yX3RpbWVvdXQiLCJlcnJvcl9jaGVja3N1bSIsImVycm9yX25vdF9mb3VuZCIsImVycm9yX3Blcm1pc3Npb24iLCJlcnJvcl9kaXNrX2Z1bGwiLCJlcnJvcl9ucG0iLCJlcnJvcl9uZXR3b3JrIiwiaGFzVXBkYXRlUmVzdWx0IiwiaGFzVmVyc2lvbkluZm8iLCJzaG91bGRSZW5kZXIiXSwic291cmNlcyI6WyJOYXRpdmVBdXRvVXBkYXRlci50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VFZmZlY3QsIHVzZVJlZiwgdXNlU3RhdGUgfSBmcm9tICdyZWFjdCdcbmltcG9ydCB7IGxvZ0V2ZW50IH0gZnJvbSAnc3JjL3NlcnZpY2VzL2FuYWx5dGljcy9pbmRleC5qcydcbmltcG9ydCB7IGxvZ0ZvckRlYnVnZ2luZyB9IGZyb20gJ3NyYy91dGlscy9kZWJ1Zy5qcydcbmltcG9ydCB7IGxvZ0Vycm9yIH0gZnJvbSAnc3JjL3V0aWxzL2xvZy5qcydcbmltcG9ydCB7IHVzZUludGVydmFsIH0gZnJvbSAndXNlaG9va3MtdHMnXG5pbXBvcnQgeyB1c2VVcGRhdGVOb3RpZmljYXRpb24gfSBmcm9tICcuLi9ob29rcy91c2VVcGRhdGVOb3RpZmljYXRpb24uanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi9pbmsuanMnXG5pbXBvcnQgdHlwZSB7IEF1dG9VcGRhdGVyUmVzdWx0IH0gZnJvbSAnLi4vdXRpbHMvYXV0b1VwZGF0ZXIuanMnXG5pbXBvcnQgeyBnZXRNYXhWZXJzaW9uLCBnZXRNYXhWZXJzaW9uTWVzc2FnZSB9IGZyb20gJy4uL3V0aWxzL2F1dG9VcGRhdGVyLmpzJ1xuaW1wb3J0IHsgaXNBdXRvVXBkYXRlckRpc2FibGVkIH0gZnJvbSAnLi4vdXRpbHMvY29uZmlnLmpzJ1xuaW1wb3J0IHsgaW5zdGFsbExhdGVzdCB9IGZyb20gJy4uL3V0aWxzL25hdGl2ZUluc3RhbGxlci9pbmRleC5qcydcbmltcG9ydCB7IGd0IH0gZnJvbSAnLi4vdXRpbHMvc2VtdmVyLmpzJ1xuaW1wb3J0IHsgZ2V0SW5pdGlhbFNldHRpbmdzIH0gZnJvbSAnLi4vdXRpbHMvc2V0dGluZ3Mvc2V0dGluZ3MuanMnXG5cbi8qKlxuICogQ2F0ZWdvcml6ZSBlcnJvciBtZXNzYWdlcyBmb3IgYW5hbHl0aWNzXG4gKi9cbmZ1bmN0aW9uIGdldEVycm9yVHlwZShlcnJvck1lc3NhZ2U6IHN0cmluZyk6IHN0cmluZyB7XG4gIGlmIChlcnJvck1lc3NhZ2UuaW5jbHVkZXMoJ3RpbWVvdXQnKSkge1xuICAgIHJldHVybiAndGltZW91dCdcbiAgfVxuICBpZiAoZXJyb3JNZXNzYWdlLmluY2x1ZGVzKCdDaGVja3N1bSBtaXNtYXRjaCcpKSB7XG4gICAgcmV0dXJuICdjaGVja3N1bV9taXNtYXRjaCdcbiAgfVxuICBpZiAoZXJyb3JNZXNzYWdlLmluY2x1ZGVzKCdFTk9FTlQnKSB8fCBlcnJvck1lc3NhZ2UuaW5jbHVkZXMoJ25vdCBmb3VuZCcpKSB7XG4gICAgcmV0dXJuICdub3RfZm91bmQnXG4gIH1cbiAgaWYgKGVycm9yTWVzc2FnZS5pbmNsdWRlcygnRUFDQ0VTJykgfHwgZXJyb3JNZXNzYWdlLmluY2x1ZGVzKCdwZXJtaXNzaW9uJykpIHtcbiAgICByZXR1cm4gJ3Blcm1pc3Npb25fZGVuaWVkJ1xuICB9XG4gIGlmIChlcnJvck1lc3NhZ2UuaW5jbHVkZXMoJ0VOT1NQQycpKSB7XG4gICAgcmV0dXJuICdkaXNrX2Z1bGwnXG4gIH1cbiAgaWYgKGVycm9yTWVzc2FnZS5pbmNsdWRlcygnbnBtJykpIHtcbiAgICByZXR1cm4gJ25wbV9lcnJvcidcbiAgfVxuICBpZiAoXG4gICAgZXJyb3JNZXNzYWdlLmluY2x1ZGVzKCduZXR3b3JrJykgfHxcbiAgICBlcnJvck1lc3NhZ2UuaW5jbHVkZXMoJ0VDT05OUkVGVVNFRCcpIHx8XG4gICAgZXJyb3JNZXNzYWdlLmluY2x1ZGVzKCdFTk9URk9VTkQnKVxuICApIHtcbiAgICByZXR1cm4gJ25ldHdvcmtfZXJyb3InXG4gIH1cbiAgcmV0dXJuICd1bmtub3duJ1xufVxuXG50eXBlIFByb3BzID0ge1xuICBpc1VwZGF0aW5nOiBib29sZWFuXG4gIG9uQ2hhbmdlSXNVcGRhdGluZzogKGlzVXBkYXRpbmc6IGJvb2xlYW4pID0+IHZvaWRcbiAgb25BdXRvVXBkYXRlclJlc3VsdDogKGF1dG9VcGRhdGVyUmVzdWx0OiBBdXRvVXBkYXRlclJlc3VsdCkgPT4gdm9pZFxuICBhdXRvVXBkYXRlclJlc3VsdDogQXV0b1VwZGF0ZXJSZXN1bHQgfCBudWxsXG4gIHNob3dTdWNjZXNzTWVzc2FnZTogYm9vbGVhblxuICB2ZXJib3NlOiBib29sZWFuXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBOYXRpdmVBdXRvVXBkYXRlcih7XG4gIGlzVXBkYXRpbmcsXG4gIG9uQ2hhbmdlSXNVcGRhdGluZyxcbiAgb25BdXRvVXBkYXRlclJlc3VsdCxcbiAgYXV0b1VwZGF0ZXJSZXN1bHQsXG4gIHNob3dTdWNjZXNzTWVzc2FnZSxcbiAgdmVyYm9zZSxcbn06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgW3ZlcnNpb25zLCBzZXRWZXJzaW9uc10gPSB1c2VTdGF0ZTx7XG4gICAgY3VycmVudD86IHN0cmluZyB8IG51bGxcbiAgICBsYXRlc3Q/OiBzdHJpbmcgfCBudWxsXG4gIH0+KHt9KVxuICBjb25zdCBbbWF4VmVyc2lvbklzc3VlLCBzZXRNYXhWZXJzaW9uSXNzdWVdID0gdXNlU3RhdGU8c3RyaW5nIHwgbnVsbD4obnVsbClcbiAgY29uc3QgdXBkYXRlU2VtdmVyID0gdXNlVXBkYXRlTm90aWZpY2F0aW9uKGF1dG9VcGRhdGVyUmVzdWx0Py52ZXJzaW9uKVxuICBjb25zdCBjaGFubmVsID0gZ2V0SW5pdGlhbFNldHRpbmdzKCk/LmF1dG9VcGRhdGVzQ2hhbm5lbCA/PyAnbGF0ZXN0J1xuXG4gIC8vIFRyYWNrIGxhdGVzdCBpc1VwZGF0aW5nIHZhbHVlIGluIGEgcmVmIHNvIHRoZSBtZW1vaXplZCBjaGVja0ZvclVwZGF0ZXNcbiAgLy8gY2FsbGJhY2sgYWx3YXlzIHNlZXMgdGhlIGN1cnJlbnQgdmFsdWUgd2l0aG91dCBjaGFuZ2luZyBjYWxsYmFjayBpZGVudGl0eVxuICAvLyAod2hpY2ggd291bGQgcmUtdHJpZ2dlciB0aGUgaW5pdGlhbC1jaGVjayB1c2VFZmZlY3QgYmVsb3cgYW5kIGNhdXNlXG4gIC8vIHJlcGVhdGVkIGRvd25sb2FkcyBvbiByZW1vdW50IOKAlCB0aGUgdXBzdHJlYW0gdHJpZ2dlciBmb3IgIzIyNDEzKS5cbiAgY29uc3QgaXNVcGRhdGluZ1JlZiA9IHVzZVJlZihpc1VwZGF0aW5nKVxuICBpc1VwZGF0aW5nUmVmLmN1cnJlbnQgPSBpc1VwZGF0aW5nXG5cbiAgY29uc3QgY2hlY2tGb3JVcGRhdGVzID0gUmVhY3QudXNlQ2FsbGJhY2soYXN5bmMgKCkgPT4ge1xuICAgIGlmIChpc1VwZGF0aW5nUmVmLmN1cnJlbnQpIHtcbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIGlmIChcbiAgICAgIFwicHJvZHVjdGlvblwiID09PSAndGVzdCcgfHxcbiAgICAgIFwicHJvZHVjdGlvblwiID09PSAnZGV2ZWxvcG1lbnQnXG4gICAgKSB7XG4gICAgICBsb2dGb3JEZWJ1Z2dpbmcoXG4gICAgICAgICdOYXRpdmVBdXRvVXBkYXRlcjogU2tpcHBpbmcgdXBkYXRlIGNoZWNrIGluIHRlc3QvZGV2IGVudmlyb25tZW50JyxcbiAgICAgIClcbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIGlmIChpc0F1dG9VcGRhdGVyRGlzYWJsZWQoKSkge1xuICAgICAgcmV0dXJuXG4gICAgfVxuXG4gICAgb25DaGFuZ2VJc1VwZGF0aW5nKHRydWUpXG4gICAgY29uc3Qgc3RhcnRUaW1lID0gRGF0ZS5ub3coKVxuXG4gICAgLy8gTG9nIHRoZSBzdGFydCBvZiBhbiBhdXRvLXVwZGF0ZSBjaGVjayBmb3IgZnVubmVsIGFuYWx5c2lzXG4gICAgbG9nRXZlbnQoJ3Rlbmd1X25hdGl2ZV9hdXRvX3VwZGF0ZXJfc3RhcnQnLCB7fSlcblxuICAgIHRyeSB7XG4gICAgICAvLyBDaGVjayBpZiBjdXJyZW50IHZlcnNpb24gaXMgYWJvdmUgdGhlIG1heCBhbGxvd2VkIHZlcnNpb25cbiAgICAgIGNvbnN0IG1heFZlcnNpb24gPSBhd2FpdCBnZXRNYXhWZXJzaW9uKClcbiAgICAgIGlmIChtYXhWZXJzaW9uICYmIGd0KE1BQ1JPLlZFUlNJT04sIG1heFZlcnNpb24pKSB7XG4gICAgICAgIGNvbnN0IG1zZyA9IGF3YWl0IGdldE1heFZlcnNpb25NZXNzYWdlKClcbiAgICAgICAgc2V0TWF4VmVyc2lvbklzc3VlKG1zZyA/PyAnYWZmZWN0cyB5b3VyIHZlcnNpb24nKVxuICAgICAgfVxuXG4gICAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBpbnN0YWxsTGF0ZXN0KGNoYW5uZWwpXG4gICAgICBjb25zdCBjdXJyZW50VmVyc2lvbiA9IE1BQ1JPLlZFUlNJT05cbiAgICAgIGNvbnN0IGxhdGVuY3lNcyA9IERhdGUubm93KCkgLSBzdGFydFRpbWVcblxuICAgICAgLy8gSGFuZGxlIGxvY2sgY29udGVudGlvbiBncmFjZWZ1bGx5IC0ganVzdCByZXR1cm4gd2l0aG91dCB0cmVhdGluZyBhcyBlcnJvclxuICAgICAgaWYgKHJlc3VsdC5sb2NrRmFpbGVkKSB7XG4gICAgICAgIGxvZ0V2ZW50KCd0ZW5ndV9uYXRpdmVfYXV0b191cGRhdGVyX2xvY2tfY29udGVudGlvbicsIHtcbiAgICAgICAgICBsYXRlbmN5X21zOiBsYXRlbmN5TXMsXG4gICAgICAgIH0pXG4gICAgICAgIHJldHVybiAvLyBTaWxlbnRseSBza2lwIHRoaXMgdXBkYXRlIGNoZWNrLCB3aWxsIHRyeSBhZ2FpbiBsYXRlclxuICAgICAgfVxuXG4gICAgICAvLyBVcGRhdGUgdmVyc2lvbnMgZm9yIGRpc3BsYXlcbiAgICAgIHNldFZlcnNpb25zKHsgY3VycmVudDogY3VycmVudFZlcnNpb24sIGxhdGVzdDogcmVzdWx0LmxhdGVzdFZlcnNpb24gfSlcblxuICAgICAgaWYgKHJlc3VsdC53YXNVcGRhdGVkKSB7XG4gICAgICAgIGxvZ0V2ZW50KCd0ZW5ndV9uYXRpdmVfYXV0b191cGRhdGVyX3N1Y2Nlc3MnLCB7XG4gICAgICAgICAgbGF0ZW5jeV9tczogbGF0ZW5jeU1zLFxuICAgICAgICB9KVxuXG4gICAgICAgIG9uQXV0b1VwZGF0ZXJSZXN1bHQoe1xuICAgICAgICAgIHZlcnNpb246IHJlc3VsdC5sYXRlc3RWZXJzaW9uLFxuICAgICAgICAgIHN0YXR1czogJ3N1Y2Nlc3MnLFxuICAgICAgICB9KVxuICAgICAgfSBlbHNlIHtcbiAgICAgICAgLy8gQWxyZWFkeSB1cCB0byBkYXRlXG4gICAgICAgIGxvZ0V2ZW50KCd0ZW5ndV9uYXRpdmVfYXV0b191cGRhdGVyX3VwX3RvX2RhdGUnLCB7XG4gICAgICAgICAgbGF0ZW5jeV9tczogbGF0ZW5jeU1zLFxuICAgICAgICB9KVxuICAgICAgfVxuICAgIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgICBjb25zdCBsYXRlbmN5TXMgPSBEYXRlLm5vdygpIC0gc3RhcnRUaW1lXG4gICAgICBjb25zdCBlcnJvck1lc3NhZ2UgPVxuICAgICAgICBlcnJvciBpbnN0YW5jZW9mIEVycm9yID8gZXJyb3IubWVzc2FnZSA6IFN0cmluZyhlcnJvcilcbiAgICAgIGxvZ0Vycm9yKGVycm9yKVxuXG4gICAgICBjb25zdCBlcnJvclR5cGUgPSBnZXRFcnJvclR5cGUoZXJyb3JNZXNzYWdlKVxuICAgICAgbG9nRXZlbnQoJ3Rlbmd1X25hdGl2ZV9hdXRvX3VwZGF0ZXJfZmFpbCcsIHtcbiAgICAgICAgbGF0ZW5jeV9tczogbGF0ZW5jeU1zLFxuICAgICAgICBlcnJvcl90aW1lb3V0OiBlcnJvclR5cGUgPT09ICd0aW1lb3V0JyxcbiAgICAgICAgZXJyb3JfY2hlY2tzdW06IGVycm9yVHlwZSA9PT0gJ2NoZWNrc3VtX21pc21hdGNoJyxcbiAgICAgICAgZXJyb3Jfbm90X2ZvdW5kOiBlcnJvclR5cGUgPT09ICdub3RfZm91bmQnLFxuICAgICAgICBlcnJvcl9wZXJtaXNzaW9uOiBlcnJvclR5cGUgPT09ICdwZXJtaXNzaW9uX2RlbmllZCcsXG4gICAgICAgIGVycm9yX2Rpc2tfZnVsbDogZXJyb3JUeXBlID09PSAnZGlza19mdWxsJyxcbiAgICAgICAgZXJyb3JfbnBtOiBlcnJvclR5cGUgPT09ICducG1fZXJyb3InLFxuICAgICAgICBlcnJvcl9uZXR3b3JrOiBlcnJvclR5cGUgPT09ICduZXR3b3JrX2Vycm9yJyxcbiAgICAgIH0pXG5cbiAgICAgIG9uQXV0b1VwZGF0ZXJSZXN1bHQoe1xuICAgICAgICB2ZXJzaW9uOiBudWxsLFxuICAgICAgICBzdGF0dXM6ICdpbnN0YWxsX2ZhaWxlZCcsXG4gICAgICB9KVxuICAgIH0gZmluYWxseSB7XG4gICAgICBvbkNoYW5nZUlzVXBkYXRpbmcoZmFsc2UpXG4gICAgfVxuICAgIC8vIGlzVXBkYXRpbmcgaW50ZW50aW9uYWxseSBvbWl0dGVkIGZyb20gZGVwczsgd2UgcmVhZCBpc1VwZGF0aW5nUmVmXG4gICAgLy8gaW5zdGVhZCBzbyB0aGUgZ3VhcmQgaXMgYWx3YXlzIGN1cnJlbnQgd2l0aG91dCBjaGFuZ2luZyBjYWxsYmFja1xuICAgIC8vIGlkZW50aXR5ICh3aGljaCB3b3VsZCByZS10cmlnZ2VyIHRoZSBpbml0aWFsLWNoZWNrIHVzZUVmZmVjdCBiZWxvdykuXG4gICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIHJlYWN0LWhvb2tzL2V4aGF1c3RpdmUtZGVwc1xuICAgIC8vIGJpb21lLWlnbm9yZSBsaW50L2NvcnJlY3RuZXNzL3VzZUV4aGF1c3RpdmVEZXBlbmRlbmNpZXM6IGlzVXBkYXRpbmcgcmVhZCB2aWEgcmVmXG4gIH0sIFtvbkF1dG9VcGRhdGVyUmVzdWx0LCBjaGFubmVsXSlcblxuICAvLyBJbml0aWFsIGNoZWNrXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgdm9pZCBjaGVja0ZvclVwZGF0ZXMoKVxuICB9LCBbY2hlY2tGb3JVcGRhdGVzXSlcblxuICAvLyBDaGVjayBldmVyeSAzMCBtaW51dGVzXG4gIHVzZUludGVydmFsKGNoZWNrRm9yVXBkYXRlcywgMzAgKiA2MCAqIDEwMDApXG5cbiAgY29uc3QgaGFzVXBkYXRlUmVzdWx0ID0gISFhdXRvVXBkYXRlclJlc3VsdD8udmVyc2lvblxuICBjb25zdCBoYXNWZXJzaW9uSW5mbyA9ICEhdmVyc2lvbnMuY3VycmVudCAmJiAhIXZlcnNpb25zLmxhdGVzdFxuICAvLyBTaG93IHRoZSBjb21wb25lbnQgd2hlbjpcbiAgLy8gLSB3YXJuaW5nIGJhbm5lciBuZWVkZWQgKGFib3ZlIG1heCB2ZXJzaW9uKSwgb3JcbiAgLy8gLSB0aGVyZSdzIGFuIHVwZGF0ZSByZXN1bHQgdG8gZGlzcGxheSAoc3VjY2Vzcy9lcnJvciksIG9yXG4gIC8vIC0gYWN0aXZlbHkgY2hlY2tpbmcgYW5kIHdlIGhhdmUgdmVyc2lvbiBpbmZvIHRvIHNob3dcbiAgY29uc3Qgc2hvdWxkUmVuZGVyID1cbiAgICAhIW1heFZlcnNpb25Jc3N1ZSB8fCBoYXNVcGRhdGVSZXN1bHQgfHwgKGlzVXBkYXRpbmcgJiYgaGFzVmVyc2lvbkluZm8pXG5cbiAgaWYgKCFzaG91bGRSZW5kZXIpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG5cbiAgcmV0dXJuIChcbiAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJyb3dcIiBnYXA9ezF9PlxuICAgICAge3ZlcmJvc2UgJiYgKFxuICAgICAgICA8VGV4dCBkaW1Db2xvciB3cmFwPVwidHJ1bmNhdGVcIj5cbiAgICAgICAgICBjdXJyZW50OiB7dmVyc2lvbnMuY3VycmVudH0gJm1pZGRvdDsge2NoYW5uZWx9OiB7dmVyc2lvbnMubGF0ZXN0fVxuICAgICAgICA8L1RleHQ+XG4gICAgICApfVxuICAgICAge2lzVXBkYXRpbmcgPyAoXG4gICAgICAgIDxCb3g+XG4gICAgICAgICAgPFRleHQgZGltQ29sb3Igd3JhcD1cInRydW5jYXRlXCI+XG4gICAgICAgICAgICBDaGVja2luZyBmb3IgdXBkYXRlc1xuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICApIDogKFxuICAgICAgICBhdXRvVXBkYXRlclJlc3VsdD8uc3RhdHVzID09PSAnc3VjY2VzcycgJiZcbiAgICAgICAgc2hvd1N1Y2Nlc3NNZXNzYWdlICYmXG4gICAgICAgIHVwZGF0ZVNlbXZlciAmJiAoXG4gICAgICAgICAgPFRleHQgY29sb3I9XCJzdWNjZXNzXCIgd3JhcD1cInRydW5jYXRlXCI+XG4gICAgICAgICAgICDinJMgVXBkYXRlIGluc3RhbGxlZCDCtyBSZXN0YXJ0IHRvIHVwZGF0ZVxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgKVxuICAgICAgKX1cbiAgICAgIHthdXRvVXBkYXRlclJlc3VsdD8uc3RhdHVzID09PSAnaW5zdGFsbF9mYWlsZWQnICYmIChcbiAgICAgICAgPFRleHQgY29sb3I9XCJlcnJvclwiIHdyYXA9XCJ0cnVuY2F0ZVwiPlxuICAgICAgICAgIOKclyBBdXRvLXVwZGF0ZSBmYWlsZWQgJm1pZGRvdDsgVHJ5IDxUZXh0IGJvbGQ+L3N0YXR1czwvVGV4dD5cbiAgICAgICAgPC9UZXh0PlxuICAgICAgKX1cbiAgICAgIHttYXhWZXJzaW9uSXNzdWUgJiYgXCJleHRlcm5hbFwiID09PSAnYW50JyAmJiAoXG4gICAgICAgIDxUZXh0IGNvbG9yPVwid2FybmluZ1wiPlxuICAgICAgICAgIOKaoCBLbm93biBpc3N1ZToge21heFZlcnNpb25Jc3N1ZX0gJm1pZGRvdDsgUnVueycgJ31cbiAgICAgICAgICA8VGV4dCBib2xkPmNsYXVkZSByb2xsYmFjayAtLXNhZmU8L1RleHQ+IHRvIGRvd25ncmFkZVxuICAgICAgICA8L1RleHQ+XG4gICAgICApfVxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsU0FBUyxFQUFFQyxNQUFNLEVBQUVDLFFBQVEsUUFBUSxPQUFPO0FBQ25ELFNBQVNDLFFBQVEsUUFBUSxpQ0FBaUM7QUFDMUQsU0FBU0MsZUFBZSxRQUFRLG9CQUFvQjtBQUNwRCxTQUFTQyxRQUFRLFFBQVEsa0JBQWtCO0FBQzNDLFNBQVNDLFdBQVcsUUFBUSxhQUFhO0FBQ3pDLFNBQVNDLHFCQUFxQixRQUFRLG1DQUFtQztBQUN6RSxTQUFTQyxHQUFHLEVBQUVDLElBQUksUUFBUSxXQUFXO0FBQ3JDLGNBQWNDLGlCQUFpQixRQUFRLHlCQUF5QjtBQUNoRSxTQUFTQyxhQUFhLEVBQUVDLG9CQUFvQixRQUFRLHlCQUF5QjtBQUM3RSxTQUFTQyxxQkFBcUIsUUFBUSxvQkFBb0I7QUFDMUQsU0FBU0MsYUFBYSxRQUFRLG1DQUFtQztBQUNqRSxTQUFTQyxFQUFFLFFBQVEsb0JBQW9CO0FBQ3ZDLFNBQVNDLGtCQUFrQixRQUFRLCtCQUErQjs7QUFFbEU7QUFDQTtBQUNBO0FBQ0EsU0FBU0MsWUFBWUEsQ0FBQ0MsWUFBWSxFQUFFLE1BQU0sQ0FBQyxFQUFFLE1BQU0sQ0FBQztFQUNsRCxJQUFJQSxZQUFZLENBQUNDLFFBQVEsQ0FBQyxTQUFTLENBQUMsRUFBRTtJQUNwQyxPQUFPLFNBQVM7RUFDbEI7RUFDQSxJQUFJRCxZQUFZLENBQUNDLFFBQVEsQ0FBQyxtQkFBbUIsQ0FBQyxFQUFFO0lBQzlDLE9BQU8sbUJBQW1CO0VBQzVCO0VBQ0EsSUFBSUQsWUFBWSxDQUFDQyxRQUFRLENBQUMsUUFBUSxDQUFDLElBQUlELFlBQVksQ0FBQ0MsUUFBUSxDQUFDLFdBQVcsQ0FBQyxFQUFFO0lBQ3pFLE9BQU8sV0FBVztFQUNwQjtFQUNBLElBQUlELFlBQVksQ0FBQ0MsUUFBUSxDQUFDLFFBQVEsQ0FBQyxJQUFJRCxZQUFZLENBQUNDLFFBQVEsQ0FBQyxZQUFZLENBQUMsRUFBRTtJQUMxRSxPQUFPLG1CQUFtQjtFQUM1QjtFQUNBLElBQUlELFlBQVksQ0FBQ0MsUUFBUSxDQUFDLFFBQVEsQ0FBQyxFQUFFO0lBQ25DLE9BQU8sV0FBVztFQUNwQjtFQUNBLElBQUlELFlBQVksQ0FBQ0MsUUFBUSxDQUFDLEtBQUssQ0FBQyxFQUFFO0lBQ2hDLE9BQU8sV0FBVztFQUNwQjtFQUNBLElBQ0VELFlBQVksQ0FBQ0MsUUFBUSxDQUFDLFNBQVMsQ0FBQyxJQUNoQ0QsWUFBWSxDQUFDQyxRQUFRLENBQUMsY0FBYyxDQUFDLElBQ3JDRCxZQUFZLENBQUNDLFFBQVEsQ0FBQyxXQUFXLENBQUMsRUFDbEM7SUFDQSxPQUFPLGVBQWU7RUFDeEI7RUFDQSxPQUFPLFNBQVM7QUFDbEI7QUFFQSxLQUFLQyxLQUFLLEdBQUc7RUFDWEMsVUFBVSxFQUFFLE9BQU87RUFDbkJDLGtCQUFrQixFQUFFLENBQUNELFVBQVUsRUFBRSxPQUFPLEVBQUUsR0FBRyxJQUFJO0VBQ2pERSxtQkFBbUIsRUFBRSxDQUFDQyxpQkFBaUIsRUFBRWQsaUJBQWlCLEVBQUUsR0FBRyxJQUFJO0VBQ25FYyxpQkFBaUIsRUFBRWQsaUJBQWlCLEdBQUcsSUFBSTtFQUMzQ2Usa0JBQWtCLEVBQUUsT0FBTztFQUMzQkMsT0FBTyxFQUFFLE9BQU87QUFDbEIsQ0FBQztBQUVELE9BQU8sU0FBU0MsaUJBQWlCQSxDQUFDO0VBQ2hDTixVQUFVO0VBQ1ZDLGtCQUFrQjtFQUNsQkMsbUJBQW1CO0VBQ25CQyxpQkFBaUI7RUFDakJDLGtCQUFrQjtFQUNsQkM7QUFDSyxDQUFOLEVBQUVOLEtBQUssQ0FBQyxFQUFFckIsS0FBSyxDQUFDNkIsU0FBUyxDQUFDO0VBQ3pCLE1BQU0sQ0FBQ0MsUUFBUSxFQUFFQyxXQUFXLENBQUMsR0FBRzVCLFFBQVEsQ0FBQztJQUN2QzZCLE9BQU8sQ0FBQyxFQUFFLE1BQU0sR0FBRyxJQUFJO0lBQ3ZCQyxNQUFNLENBQUMsRUFBRSxNQUFNLEdBQUcsSUFBSTtFQUN4QixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztFQUNOLE1BQU0sQ0FBQ0MsZUFBZSxFQUFFQyxrQkFBa0IsQ0FBQyxHQUFHaEMsUUFBUSxDQUFDLE1BQU0sR0FBRyxJQUFJLENBQUMsQ0FBQyxJQUFJLENBQUM7RUFDM0UsTUFBTWlDLFlBQVksR0FBRzVCLHFCQUFxQixDQUFDaUIsaUJBQWlCLEVBQUVZLE9BQU8sQ0FBQztFQUN0RSxNQUFNQyxPQUFPLEdBQUdyQixrQkFBa0IsQ0FBQyxDQUFDLEVBQUVzQixrQkFBa0IsSUFBSSxRQUFROztFQUVwRTtFQUNBO0VBQ0E7RUFDQTtFQUNBLE1BQU1DLGFBQWEsR0FBR3RDLE1BQU0sQ0FBQ29CLFVBQVUsQ0FBQztFQUN4Q2tCLGFBQWEsQ0FBQ1IsT0FBTyxHQUFHVixVQUFVO0VBRWxDLE1BQU1tQixlQUFlLEdBQUd6QyxLQUFLLENBQUMwQyxXQUFXLENBQUMsWUFBWTtJQUNwRCxJQUFJRixhQUFhLENBQUNSLE9BQU8sRUFBRTtNQUN6QjtJQUNGO0lBRUEsSUFDRSxZQUFZLEtBQUssTUFBTSxJQUN2QixZQUFZLEtBQUssYUFBYSxFQUM5QjtNQUNBM0IsZUFBZSxDQUNiLGtFQUNGLENBQUM7TUFDRDtJQUNGO0lBRUEsSUFBSVMscUJBQXFCLENBQUMsQ0FBQyxFQUFFO01BQzNCO0lBQ0Y7SUFFQVMsa0JBQWtCLENBQUMsSUFBSSxDQUFDO0lBQ3hCLE1BQU1vQixTQUFTLEdBQUdDLElBQUksQ0FBQ0MsR0FBRyxDQUFDLENBQUM7O0lBRTVCO0lBQ0F6QyxRQUFRLENBQUMsaUNBQWlDLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFFL0MsSUFBSTtNQUNGO01BQ0EsTUFBTTBDLFVBQVUsR0FBRyxNQUFNbEMsYUFBYSxDQUFDLENBQUM7TUFDeEMsSUFBSWtDLFVBQVUsSUFBSTlCLEVBQUUsQ0FBQytCLEtBQUssQ0FBQ0MsT0FBTyxFQUFFRixVQUFVLENBQUMsRUFBRTtRQUMvQyxNQUFNRyxHQUFHLEdBQUcsTUFBTXBDLG9CQUFvQixDQUFDLENBQUM7UUFDeENzQixrQkFBa0IsQ0FBQ2MsR0FBRyxJQUFJLHNCQUFzQixDQUFDO01BQ25EO01BRUEsTUFBTUMsTUFBTSxHQUFHLE1BQU1uQyxhQUFhLENBQUN1QixPQUFPLENBQUM7TUFDM0MsTUFBTWEsY0FBYyxHQUFHSixLQUFLLENBQUNDLE9BQU87TUFDcEMsTUFBTUksU0FBUyxHQUFHUixJQUFJLENBQUNDLEdBQUcsQ0FBQyxDQUFDLEdBQUdGLFNBQVM7O01BRXhDO01BQ0EsSUFBSU8sTUFBTSxDQUFDRyxVQUFVLEVBQUU7UUFDckJqRCxRQUFRLENBQUMsMkNBQTJDLEVBQUU7VUFDcERrRCxVQUFVLEVBQUVGO1FBQ2QsQ0FBQyxDQUFDO1FBQ0YsT0FBTSxDQUFDO01BQ1Q7O01BRUE7TUFDQXJCLFdBQVcsQ0FBQztRQUFFQyxPQUFPLEVBQUVtQixjQUFjO1FBQUVsQixNQUFNLEVBQUVpQixNQUFNLENBQUNLO01BQWMsQ0FBQyxDQUFDO01BRXRFLElBQUlMLE1BQU0sQ0FBQ00sVUFBVSxFQUFFO1FBQ3JCcEQsUUFBUSxDQUFDLG1DQUFtQyxFQUFFO1VBQzVDa0QsVUFBVSxFQUFFRjtRQUNkLENBQUMsQ0FBQztRQUVGNUIsbUJBQW1CLENBQUM7VUFDbEJhLE9BQU8sRUFBRWEsTUFBTSxDQUFDSyxhQUFhO1VBQzdCRSxNQUFNLEVBQUU7UUFDVixDQUFDLENBQUM7TUFDSixDQUFDLE1BQU07UUFDTDtRQUNBckQsUUFBUSxDQUFDLHNDQUFzQyxFQUFFO1VBQy9Da0QsVUFBVSxFQUFFRjtRQUNkLENBQUMsQ0FBQztNQUNKO0lBQ0YsQ0FBQyxDQUFDLE9BQU9NLEtBQUssRUFBRTtNQUNkLE1BQU1OLFNBQVMsR0FBR1IsSUFBSSxDQUFDQyxHQUFHLENBQUMsQ0FBQyxHQUFHRixTQUFTO01BQ3hDLE1BQU14QixZQUFZLEdBQ2hCdUMsS0FBSyxZQUFZQyxLQUFLLEdBQUdELEtBQUssQ0FBQ0UsT0FBTyxHQUFHQyxNQUFNLENBQUNILEtBQUssQ0FBQztNQUN4RHBELFFBQVEsQ0FBQ29ELEtBQUssQ0FBQztNQUVmLE1BQU1JLFNBQVMsR0FBRzVDLFlBQVksQ0FBQ0MsWUFBWSxDQUFDO01BQzVDZixRQUFRLENBQUMsZ0NBQWdDLEVBQUU7UUFDekNrRCxVQUFVLEVBQUVGLFNBQVM7UUFDckJXLGFBQWEsRUFBRUQsU0FBUyxLQUFLLFNBQVM7UUFDdENFLGNBQWMsRUFBRUYsU0FBUyxLQUFLLG1CQUFtQjtRQUNqREcsZUFBZSxFQUFFSCxTQUFTLEtBQUssV0FBVztRQUMxQ0ksZ0JBQWdCLEVBQUVKLFNBQVMsS0FBSyxtQkFBbUI7UUFDbkRLLGVBQWUsRUFBRUwsU0FBUyxLQUFLLFdBQVc7UUFDMUNNLFNBQVMsRUFBRU4sU0FBUyxLQUFLLFdBQVc7UUFDcENPLGFBQWEsRUFBRVAsU0FBUyxLQUFLO01BQy9CLENBQUMsQ0FBQztNQUVGdEMsbUJBQW1CLENBQUM7UUFDbEJhLE9BQU8sRUFBRSxJQUFJO1FBQ2JvQixNQUFNLEVBQUU7TUFDVixDQUFDLENBQUM7SUFDSixDQUFDLFNBQVM7TUFDUmxDLGtCQUFrQixDQUFDLEtBQUssQ0FBQztJQUMzQjtJQUNBO0lBQ0E7SUFDQTtJQUNBO0lBQ0E7RUFDRixDQUFDLEVBQUUsQ0FBQ0MsbUJBQW1CLEVBQUVjLE9BQU8sQ0FBQyxDQUFDOztFQUVsQztFQUNBckMsU0FBUyxDQUFDLE1BQU07SUFDZCxLQUFLd0MsZUFBZSxDQUFDLENBQUM7RUFDeEIsQ0FBQyxFQUFFLENBQUNBLGVBQWUsQ0FBQyxDQUFDOztFQUVyQjtFQUNBbEMsV0FBVyxDQUFDa0MsZUFBZSxFQUFFLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDO0VBRTVDLE1BQU02QixlQUFlLEdBQUcsQ0FBQyxDQUFDN0MsaUJBQWlCLEVBQUVZLE9BQU87RUFDcEQsTUFBTWtDLGNBQWMsR0FBRyxDQUFDLENBQUN6QyxRQUFRLENBQUNFLE9BQU8sSUFBSSxDQUFDLENBQUNGLFFBQVEsQ0FBQ0csTUFBTTtFQUM5RDtFQUNBO0VBQ0E7RUFDQTtFQUNBLE1BQU11QyxZQUFZLEdBQ2hCLENBQUMsQ0FBQ3RDLGVBQWUsSUFBSW9DLGVBQWUsSUFBS2hELFVBQVUsSUFBSWlELGNBQWU7RUFFeEUsSUFBSSxDQUFDQyxZQUFZLEVBQUU7SUFDakIsT0FBTyxJQUFJO0VBQ2I7RUFFQSxPQUNFLENBQUMsR0FBRyxDQUFDLGFBQWEsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ3BDLE1BQU0sQ0FBQzdDLE9BQU8sSUFDTixDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLFVBQVU7QUFDdEMsbUJBQW1CLENBQUNHLFFBQVEsQ0FBQ0UsT0FBTyxDQUFDLFVBQVUsQ0FBQ00sT0FBTyxDQUFDLEVBQUUsQ0FBQ1IsUUFBUSxDQUFDRyxNQUFNO0FBQzFFLFFBQVEsRUFBRSxJQUFJLENBQ1A7QUFDUCxNQUFNLENBQUNYLFVBQVUsR0FDVCxDQUFDLEdBQUc7QUFDWixVQUFVLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsVUFBVTtBQUN4QztBQUNBLFVBQVUsRUFBRSxJQUFJO0FBQ2hCLFFBQVEsRUFBRSxHQUFHLENBQUMsR0FFTkcsaUJBQWlCLEVBQUVnQyxNQUFNLEtBQUssU0FBUyxJQUN2Qy9CLGtCQUFrQixJQUNsQlUsWUFBWSxJQUNWLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLFVBQVU7QUFDL0M7QUFDQSxVQUFVLEVBQUUsSUFBSSxDQUVUO0FBQ1AsTUFBTSxDQUFDWCxpQkFBaUIsRUFBRWdDLE1BQU0sS0FBSyxnQkFBZ0IsSUFDN0MsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsVUFBVTtBQUMzQyw0Q0FBNEMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxJQUFJO0FBQ3BFLFFBQVEsRUFBRSxJQUFJLENBQ1A7QUFDUCxNQUFNLENBQUN2QixlQUFlLElBQUksVUFBVSxLQUFLLEtBQUssSUFDdEMsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLFNBQVM7QUFDN0IseUJBQXlCLENBQUNBLGVBQWUsQ0FBQyxhQUFhLENBQUMsR0FBRztBQUMzRCxVQUFVLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxzQkFBc0IsRUFBRSxJQUFJLENBQUM7QUFDbEQsUUFBUSxFQUFFLElBQUksQ0FDUDtBQUNQLElBQUksRUFBRSxHQUFHLENBQUM7QUFFViIsImlnbm9yZUxpc3QiOltdfQ== \ No newline at end of file diff --git a/src/components/Onboarding.tsx b/src/components/Onboarding.tsx index d4b62663..c705821a 100644 --- a/src/components/Onboarding.tsx +++ b/src/components/Onboarding.tsx @@ -1,6 +1,6 @@ import { c as _c } from "react/compiler-runtime"; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; import { setupTerminal, shouldOfferTerminalSetup } from '../commands/terminalSetup/terminalSetup.js'; import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js'; import { Box, Link, Newline, Text, useTheme } from '../ink.js'; @@ -71,9 +71,9 @@ export function Onboarding({ */} - Claude can make mistakes + AI can make mistakes - You should always review Claude's responses, especially when + You should always review AI's responses, especially when running code. diff --git a/src/components/PackageManagerAutoUpdater.tsx b/src/components/PackageManagerAutoUpdater.tsx index a8681abf..da82a5cf 100644 --- a/src/components/PackageManagerAutoUpdater.tsx +++ b/src/components/PackageManagerAutoUpdater.tsx @@ -1,104 +1,11 @@ -import { c as _c } from "react/compiler-runtime"; +/** + * PackageManagerAutoUpdater stub for claude-code-clean + * All auto-update functionality disabled + */ + import * as React from 'react'; -import { useState } from 'react'; -import { useInterval } from 'usehooks-ts'; -import { Text } from '../ink.js'; -import { type AutoUpdaterResult, getLatestVersionFromGcs, getMaxVersion, shouldSkipVersion } from '../utils/autoUpdater.js'; -import { isAutoUpdaterDisabled } from '../utils/config.js'; -import { logForDebugging } from '../utils/debug.js'; -import { getPackageManager, type PackageManager } from '../utils/nativeInstaller/packageManagers.js'; -import { gt, gte } from '../utils/semver.js'; -import { getInitialSettings } from '../utils/settings/settings.js'; -type Props = { - isUpdating: boolean; - onChangeIsUpdating: (isUpdating: boolean) => void; - onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void; - autoUpdaterResult: AutoUpdaterResult | null; - showSuccessMessage: boolean; - verbose: boolean; -}; -export function PackageManagerAutoUpdater(t0) { - const $ = _c(10); - const { - verbose - } = t0; - const [updateAvailable, setUpdateAvailable] = useState(false); - const [packageManager, setPackageManager] = useState("unknown"); - let t1; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = async () => { - false || false; - if (isAutoUpdaterDisabled()) { - return; - } - const [channel, pm] = await Promise.all([Promise.resolve(getInitialSettings()?.autoUpdatesChannel ?? "latest"), getPackageManager()]); - setPackageManager(pm); - let latest = await getLatestVersionFromGcs(channel); - const maxVersion = await getMaxVersion(); - if (maxVersion && latest && gt(latest, maxVersion)) { - logForDebugging(`PackageManagerAutoUpdater: maxVersion ${maxVersion} is set, capping update from ${latest} to ${maxVersion}`); - if (gte(MACRO.VERSION, maxVersion)) { - logForDebugging(`PackageManagerAutoUpdater: current version ${MACRO.VERSION} is already at or above maxVersion ${maxVersion}, skipping update`); - setUpdateAvailable(false); - return; - } - latest = maxVersion; - } - const hasUpdate = latest && !gte(MACRO.VERSION, latest) && !shouldSkipVersion(latest); - setUpdateAvailable(!!hasUpdate); - if (hasUpdate) { - logForDebugging(`PackageManagerAutoUpdater: Update available ${MACRO.VERSION} -> ${latest}`); - } - }; - $[0] = t1; - } else { - t1 = $[0]; - } - const checkForUpdates = t1; - let t2; - let t3; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t2 = () => { - checkForUpdates(); - }; - t3 = [checkForUpdates]; - $[1] = t2; - $[2] = t3; - } else { - t2 = $[1]; - t3 = $[2]; - } - React.useEffect(t2, t3); - useInterval(checkForUpdates, 1800000); - if (!updateAvailable) { - return null; - } - const updateCommand = packageManager === "homebrew" ? "brew upgrade claude-code" : packageManager === "winget" ? "winget upgrade Anthropic.ClaudeCode" : packageManager === "apk" ? "apk upgrade claude-code" : "your package manager update command"; - let t4; - if ($[3] !== verbose) { - t4 = verbose && currentVersion: {MACRO.VERSION}; - $[3] = verbose; - $[4] = t4; - } else { - t4 = $[4]; - } - let t5; - if ($[5] !== updateCommand) { - t5 = Update available! Run: {updateCommand}; - $[5] = updateCommand; - $[6] = t5; - } else { - t5 = $[6]; - } - let t6; - if ($[7] !== t4 || $[8] !== t5) { - t6 = <>{t4}{t5}; - $[7] = t4; - $[8] = t5; - $[9] = t6; - } else { - t6 = $[9]; - } - return t6; + +export function PackageManagerAutoUpdater(): React.ReactNode { + // No-op: Auto-updates disabled in claude-code-clean + return null; } -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZVN0YXRlIiwidXNlSW50ZXJ2YWwiLCJUZXh0IiwiQXV0b1VwZGF0ZXJSZXN1bHQiLCJnZXRMYXRlc3RWZXJzaW9uRnJvbUdjcyIsImdldE1heFZlcnNpb24iLCJzaG91bGRTa2lwVmVyc2lvbiIsImlzQXV0b1VwZGF0ZXJEaXNhYmxlZCIsImxvZ0ZvckRlYnVnZ2luZyIsImdldFBhY2thZ2VNYW5hZ2VyIiwiUGFja2FnZU1hbmFnZXIiLCJndCIsImd0ZSIsImdldEluaXRpYWxTZXR0aW5ncyIsIlByb3BzIiwiaXNVcGRhdGluZyIsIm9uQ2hhbmdlSXNVcGRhdGluZyIsIm9uQXV0b1VwZGF0ZXJSZXN1bHQiLCJhdXRvVXBkYXRlclJlc3VsdCIsInNob3dTdWNjZXNzTWVzc2FnZSIsInZlcmJvc2UiLCJQYWNrYWdlTWFuYWdlckF1dG9VcGRhdGVyIiwidDAiLCIkIiwiX2MiLCJ1cGRhdGVBdmFpbGFibGUiLCJzZXRVcGRhdGVBdmFpbGFibGUiLCJwYWNrYWdlTWFuYWdlciIsInNldFBhY2thZ2VNYW5hZ2VyIiwidDEiLCJTeW1ib2wiLCJmb3IiLCJjaGFubmVsIiwicG0iLCJQcm9taXNlIiwiYWxsIiwicmVzb2x2ZSIsImF1dG9VcGRhdGVzQ2hhbm5lbCIsImxhdGVzdCIsIm1heFZlcnNpb24iLCJNQUNSTyIsIlZFUlNJT04iLCJoYXNVcGRhdGUiLCJjaGVja0ZvclVwZGF0ZXMiLCJ0MiIsInQzIiwidXNlRWZmZWN0IiwidXBkYXRlQ29tbWFuZCIsInQ0IiwidDUiLCJ0NiJdLCJzb3VyY2VzIjpbIlBhY2thZ2VNYW5hZ2VyQXV0b1VwZGF0ZXIudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgdXNlU3RhdGUgfSBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZUludGVydmFsIH0gZnJvbSAndXNlaG9va3MtdHMnXG5pbXBvcnQgeyBUZXh0IH0gZnJvbSAnLi4vaW5rLmpzJ1xuaW1wb3J0IHtcbiAgdHlwZSBBdXRvVXBkYXRlclJlc3VsdCxcbiAgZ2V0TGF0ZXN0VmVyc2lvbkZyb21HY3MsXG4gIGdldE1heFZlcnNpb24sXG4gIHNob3VsZFNraXBWZXJzaW9uLFxufSBmcm9tICcuLi91dGlscy9hdXRvVXBkYXRlci5qcydcbmltcG9ydCB7IGlzQXV0b1VwZGF0ZXJEaXNhYmxlZCB9IGZyb20gJy4uL3V0aWxzL2NvbmZpZy5qcydcbmltcG9ydCB7IGxvZ0ZvckRlYnVnZ2luZyB9IGZyb20gJy4uL3V0aWxzL2RlYnVnLmpzJ1xuaW1wb3J0IHtcbiAgZ2V0UGFja2FnZU1hbmFnZXIsXG4gIHR5cGUgUGFja2FnZU1hbmFnZXIsXG59IGZyb20gJy4uL3V0aWxzL25hdGl2ZUluc3RhbGxlci9wYWNrYWdlTWFuYWdlcnMuanMnXG5pbXBvcnQgeyBndCwgZ3RlIH0gZnJvbSAnLi4vdXRpbHMvc2VtdmVyLmpzJ1xuaW1wb3J0IHsgZ2V0SW5pdGlhbFNldHRpbmdzIH0gZnJvbSAnLi4vdXRpbHMvc2V0dGluZ3Mvc2V0dGluZ3MuanMnXG5cbnR5cGUgUHJvcHMgPSB7XG4gIGlzVXBkYXRpbmc6IGJvb2xlYW5cbiAgb25DaGFuZ2VJc1VwZGF0aW5nOiAoaXNVcGRhdGluZzogYm9vbGVhbikgPT4gdm9pZFxuICBvbkF1dG9VcGRhdGVyUmVzdWx0OiAoYXV0b1VwZGF0ZXJSZXN1bHQ6IEF1dG9VcGRhdGVyUmVzdWx0KSA9PiB2b2lkXG4gIGF1dG9VcGRhdGVyUmVzdWx0OiBBdXRvVXBkYXRlclJlc3VsdCB8IG51bGxcbiAgc2hvd1N1Y2Nlc3NNZXNzYWdlOiBib29sZWFuXG4gIHZlcmJvc2U6IGJvb2xlYW5cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFBhY2thZ2VNYW5hZ2VyQXV0b1VwZGF0ZXIoeyB2ZXJib3NlIH06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgW3VwZGF0ZUF2YWlsYWJsZSwgc2V0VXBkYXRlQXZhaWxhYmxlXSA9IHVzZVN0YXRlKGZhbHNlKVxuICBjb25zdCBbcGFja2FnZU1hbmFnZXIsIHNldFBhY2thZ2VNYW5hZ2VyXSA9XG4gICAgdXNlU3RhdGU8UGFja2FnZU1hbmFnZXI+KCd1bmtub3duJylcblxuICBjb25zdCBjaGVja0ZvclVwZGF0ZXMgPSBSZWFjdC51c2VDYWxsYmFjayhhc3luYyAoKSA9PiB7XG4gICAgaWYgKFxuICAgICAgXCJwcm9kdWN0aW9uXCIgPT09ICd0ZXN0JyB8fFxuICAgICAgXCJwcm9kdWN0aW9uXCIgPT09ICdkZXZlbG9wbWVudCdcbiAgICApIHtcbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIGlmIChpc0F1dG9VcGRhdGVyRGlzYWJsZWQoKSkge1xuICAgICAgcmV0dXJuXG4gICAgfVxuXG4gICAgY29uc3QgW2NoYW5uZWwsIHBtXSA9IGF3YWl0IFByb21pc2UuYWxsKFtcbiAgICAgIFByb21pc2UucmVzb2x2ZShnZXRJbml0aWFsU2V0dGluZ3MoKT8uYXV0b1VwZGF0ZXNDaGFubmVsID8/ICdsYXRlc3QnKSxcbiAgICAgIGdldFBhY2thZ2VNYW5hZ2VyKCksXG4gICAgXSlcbiAgICBzZXRQYWNrYWdlTWFuYWdlcihwbSlcblxuICAgIGxldCBsYXRlc3QgPSBhd2FpdCBnZXRMYXRlc3RWZXJzaW9uRnJvbUdjcyhjaGFubmVsKVxuXG4gICAgLy8gQ2hlY2sgaWYgbWF4IHZlcnNpb24gaXMgc2V0IChzZXJ2ZXItc2lkZSBraWxsIHN3aXRjaCBmb3IgYXV0by11cGRhdGVzKVxuICAgIGNvbnN0IG1heFZlcnNpb24gPSBhd2FpdCBnZXRNYXhWZXJzaW9uKClcblxuICAgIGlmIChtYXhWZXJzaW9uICYmIGxhdGVzdCAmJiBndChsYXRlc3QsIG1heFZlcnNpb24pKSB7XG4gICAgICBsb2dGb3JEZWJ1Z2dpbmcoXG4gICAgICAgIGBQYWNrYWdlTWFuYWdlckF1dG9VcGRhdGVyOiBtYXhWZXJzaW9uICR7bWF4VmVyc2lvbn0gaXMgc2V0LCBjYXBwaW5nIHVwZGF0ZSBmcm9tICR7bGF0ZXN0fSB0byAke21heFZlcnNpb259YCxcbiAgICAgIClcbiAgICAgIGlmIChndGUoTUFDUk8uVkVSU0lPTiwgbWF4VmVyc2lvbikpIHtcbiAgICAgICAgbG9nRm9yRGVidWdnaW5nKFxuICAgICAgICAgIGBQYWNrYWdlTWFuYWdlckF1dG9VcGRhdGVyOiBjdXJyZW50IHZlcnNpb24gJHtNQUNSTy5WRVJTSU9OfSBpcyBhbHJlYWR5IGF0IG9yIGFib3ZlIG1heFZlcnNpb24gJHttYXhWZXJzaW9ufSwgc2tpcHBpbmcgdXBkYXRlYCxcbiAgICAgICAgKVxuICAgICAgICBzZXRVcGRhdGVBdmFpbGFibGUoZmFsc2UpXG4gICAgICAgIHJldHVyblxuICAgICAgfVxuICAgICAgbGF0ZXN0ID0gbWF4VmVyc2lvblxuICAgIH1cblxuICAgIGNvbnN0IGhhc1VwZGF0ZSA9XG4gICAgICBsYXRlc3QgJiYgIWd0ZShNQUNSTy5WRVJTSU9OLCBsYXRlc3QpICYmICFzaG91bGRTa2lwVmVyc2lvbihsYXRlc3QpXG5cbiAgICBzZXRVcGRhdGVBdmFpbGFibGUoISFoYXNVcGRhdGUpXG5cbiAgICBpZiAoaGFzVXBkYXRlKSB7XG4gICAgICBsb2dGb3JEZWJ1Z2dpbmcoXG4gICAgICAgIGBQYWNrYWdlTWFuYWdlckF1dG9VcGRhdGVyOiBVcGRhdGUgYXZhaWxhYmxlICR7TUFDUk8uVkVSU0lPTn0gLT4gJHtsYXRlc3R9YCxcbiAgICAgIClcbiAgICB9XG4gIH0sIFtdKVxuXG4gIC8vIEluaXRpYWwgY2hlY2tcbiAgUmVhY3QudXNlRWZmZWN0KCgpID0+IHtcbiAgICB2b2lkIGNoZWNrRm9yVXBkYXRlcygpXG4gIH0sIFtjaGVja0ZvclVwZGF0ZXNdKVxuXG4gIC8vIENoZWNrIGV2ZXJ5IDMwIG1pbnV0ZXNcbiAgdXNlSW50ZXJ2YWwoY2hlY2tGb3JVcGRhdGVzLCAzMCAqIDYwICogMTAwMClcblxuICBpZiAoIXVwZGF0ZUF2YWlsYWJsZSkge1xuICAgIHJldHVybiBudWxsXG4gIH1cblxuICAvLyBwYWNtYW4sIGRlYiwgYW5kIHJwbSBkb24ndCBnZXQgc3BlY2lmaWMgY29tbWFuZHMgYmVjYXVzZSB0aGV5IGVhY2ggaGF2ZVxuICAvLyBtdWx0aXBsZSBmcm9udGVuZHMgKHBhY21hbjogeWF5L3BhcnUvbWFrZXBrZywgZGViOiBhcHQvYXB0LWdldC9hcHRpdHVkZS9uYWxhLFxuICAvLyBycG06IGRuZi95dW0venlwcGVyKVxuICBjb25zdCB1cGRhdGVDb21tYW5kID1cbiAgICBwYWNrYWdlTWFuYWdlciA9PT0gJ2hvbWVicmV3J1xuICAgICAgPyAnYnJldyB1cGdyYWRlIGNsYXVkZS1jb2RlJ1xuICAgICAgOiBwYWNrYWdlTWFuYWdlciA9PT0gJ3dpbmdldCdcbiAgICAgICAgPyAnd2luZ2V0IHVwZ3JhZGUgQW50aHJvcGljLkNsYXVkZUNvZGUnXG4gICAgICAgIDogcGFja2FnZU1hbmFnZXIgPT09ICdhcGsnXG4gICAgICAgICAgPyAnYXBrIHVwZ3JhZGUgY2xhdWRlLWNvZGUnXG4gICAgICAgICAgOiAneW91ciBwYWNrYWdlIG1hbmFnZXIgdXBkYXRlIGNvbW1hbmQnXG5cbiAgcmV0dXJuIChcbiAgICA8PlxuICAgICAge3ZlcmJvc2UgJiYgKFxuICAgICAgICA8VGV4dCBkaW1Db2xvciB3cmFwPVwidHJ1bmNhdGVcIj5cbiAgICAgICAgICBjdXJyZW50VmVyc2lvbjoge01BQ1JPLlZFUlNJT059XG4gICAgICAgIDwvVGV4dD5cbiAgICAgICl9XG4gICAgICA8VGV4dCBjb2xvcj1cIndhcm5pbmdcIiB3cmFwPVwidHJ1bmNhdGVcIj5cbiAgICAgICAgVXBkYXRlIGF2YWlsYWJsZSEgUnVuOiA8VGV4dCBib2xkPnt1cGRhdGVDb21tYW5kfTwvVGV4dD5cbiAgICAgIDwvVGV4dD5cbiAgICA8Lz5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxRQUFRLFFBQVEsT0FBTztBQUNoQyxTQUFTQyxXQUFXLFFBQVEsYUFBYTtBQUN6QyxTQUFTQyxJQUFJLFFBQVEsV0FBVztBQUNoQyxTQUNFLEtBQUtDLGlCQUFpQixFQUN0QkMsdUJBQXVCLEVBQ3ZCQyxhQUFhLEVBQ2JDLGlCQUFpQixRQUNaLHlCQUF5QjtBQUNoQyxTQUFTQyxxQkFBcUIsUUFBUSxvQkFBb0I7QUFDMUQsU0FBU0MsZUFBZSxRQUFRLG1CQUFtQjtBQUNuRCxTQUNFQyxpQkFBaUIsRUFDakIsS0FBS0MsY0FBYyxRQUNkLDZDQUE2QztBQUNwRCxTQUFTQyxFQUFFLEVBQUVDLEdBQUcsUUFBUSxvQkFBb0I7QUFDNUMsU0FBU0Msa0JBQWtCLFFBQVEsK0JBQStCO0FBRWxFLEtBQUtDLEtBQUssR0FBRztFQUNYQyxVQUFVLEVBQUUsT0FBTztFQUNuQkMsa0JBQWtCLEVBQUUsQ0FBQ0QsVUFBVSxFQUFFLE9BQU8sRUFBRSxHQUFHLElBQUk7RUFDakRFLG1CQUFtQixFQUFFLENBQUNDLGlCQUFpQixFQUFFZixpQkFBaUIsRUFBRSxHQUFHLElBQUk7RUFDbkVlLGlCQUFpQixFQUFFZixpQkFBaUIsR0FBRyxJQUFJO0VBQzNDZ0Isa0JBQWtCLEVBQUUsT0FBTztFQUMzQkMsT0FBTyxFQUFFLE9BQU87QUFDbEIsQ0FBQztBQUVELE9BQU8sU0FBQUMsMEJBQUFDLEVBQUE7RUFBQSxNQUFBQyxDQUFBLEdBQUFDLEVBQUE7RUFBbUM7SUFBQUo7RUFBQSxJQUFBRSxFQUFrQjtFQUMxRCxPQUFBRyxlQUFBLEVBQUFDLGtCQUFBLElBQThDMUIsUUFBUSxDQUFDLEtBQUssQ0FBQztFQUM3RCxPQUFBMkIsY0FBQSxFQUFBQyxpQkFBQSxJQUNFNUIsUUFBUSxDQUFpQixTQUFTLENBQUM7RUFBQSxJQUFBNkIsRUFBQTtFQUFBLElBQUFOLENBQUEsUUFBQU8sTUFBQSxDQUFBQyxHQUFBO0lBRUtGLEVBQUEsU0FBQUEsQ0FBQTtNQUV0QyxLQUM4QixJQUQ5QixLQUM4QjtNQUtoQyxJQUFJdEIscUJBQXFCLENBQUMsQ0FBQztRQUFBO01BQUE7TUFJM0IsT0FBQXlCLE9BQUEsRUFBQUMsRUFBQSxJQUFzQixNQUFNQyxPQUFPLENBQUFDLEdBQUksQ0FBQyxDQUN0Q0QsT0FBTyxDQUFBRSxPQUFRLENBQUN2QixrQkFBa0IsQ0FBcUIsQ0FBQyxFQUFBd0Isa0JBQVksSUFBcEQsUUFBb0QsQ0FBQyxFQUNyRTVCLGlCQUFpQixDQUFDLENBQUMsQ0FDcEIsQ0FBQztNQUNGbUIsaUJBQWlCLENBQUNLLEVBQUUsQ0FBQztNQUVyQixJQUFBSyxNQUFBLEdBQWEsTUFBTWxDLHVCQUF1QixDQUFDNEIsT0FBTyxDQUFDO01BR25ELE1BQUFPLFVBQUEsR0FBbUIsTUFBTWxDLGFBQWEsQ0FBQyxDQUFDO01BRXhDLElBQUlrQyxVQUFvQixJQUFwQkQsTUFBOEMsSUFBdEIzQixFQUFFLENBQUMyQixNQUFNLEVBQUVDLFVBQVUsQ0FBQztRQUNoRC9CLGVBQWUsQ0FDYix5Q0FBeUMrQixVQUFVLGdDQUFnQ0QsTUFBTSxPQUFPQyxVQUFVLEVBQzVHLENBQUM7UUFDRCxJQUFJM0IsR0FBRyxDQUFDNEIsS0FBSyxDQUFBQyxPQUFRLEVBQUVGLFVBQVUsQ0FBQztVQUNoQy9CLGVBQWUsQ0FDYiw4Q0FBOENnQyxLQUFLLENBQUFDLE9BQVEsc0NBQXNDRixVQUFVLG1CQUM3RyxDQUFDO1VBQ0RiLGtCQUFrQixDQUFDLEtBQUssQ0FBQztVQUFBO1FBQUE7UUFHM0JZLE1BQUEsQ0FBQUEsQ0FBQSxDQUFTQyxVQUFVO01BQWI7TUFHUixNQUFBRyxTQUFBLEdBQ0VKLE1BQXFDLElBQXJDLENBQVcxQixHQUFHLENBQUM0QixLQUFLLENBQUFDLE9BQVEsRUFBRUgsTUFBTSxDQUErQixJQUFuRSxDQUEwQ2hDLGlCQUFpQixDQUFDZ0MsTUFBTSxDQUFDO01BRXJFWixrQkFBa0IsQ0FBQyxDQUFDLENBQUNnQixTQUFTLENBQUM7TUFFL0IsSUFBSUEsU0FBUztRQUNYbEMsZUFBZSxDQUNiLCtDQUErQ2dDLEtBQUssQ0FBQUMsT0FBUSxPQUFPSCxNQUFNLEVBQzNFLENBQUM7TUFBQTtJQUNGLENBQ0Y7SUFBQWYsQ0FBQSxNQUFBTSxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBTixDQUFBO0VBQUE7RUEvQ0QsTUFBQW9CLGVBQUEsR0FBd0JkLEVBK0NsQjtFQUFBLElBQUFlLEVBQUE7RUFBQSxJQUFBQyxFQUFBO0VBQUEsSUFBQXRCLENBQUEsUUFBQU8sTUFBQSxDQUFBQyxHQUFBO0lBR1VhLEVBQUEsR0FBQUEsQ0FBQTtNQUNURCxlQUFlLENBQUMsQ0FBQztJQUFBLENBQ3ZCO0lBQUVFLEVBQUEsSUFBQ0YsZUFBZSxDQUFDO0lBQUFwQixDQUFBLE1BQUFxQixFQUFBO0lBQUFyQixDQUFBLE1BQUFzQixFQUFBO0VBQUE7SUFBQUQsRUFBQSxHQUFBckIsQ0FBQTtJQUFBc0IsRUFBQSxHQUFBdEIsQ0FBQTtFQUFBO0VBRnBCeEIsS0FBSyxDQUFBK0MsU0FBVSxDQUFDRixFQUVmLEVBQUVDLEVBQWlCLENBQUM7RUFHckI1QyxXQUFXLENBQUMwQyxlQUFlLEVBQUUsT0FBYyxDQUFDO0VBRTVDLElBQUksQ0FBQ2xCLGVBQWU7SUFBQSxPQUNYLElBQUk7RUFBQTtFQU1iLE1BQUFzQixhQUFBLEdBQ0VwQixjQUFjLEtBQUssVUFNMEIsR0FON0MsMEJBTTZDLEdBSnpDQSxjQUFjLEtBQUssUUFJc0IsR0FKekMscUNBSXlDLEdBRnZDQSxjQUFjLEtBQUssS0FFb0IsR0FGdkMseUJBRXVDLEdBRnZDLHFDQUV1QztFQUFBLElBQUFxQixFQUFBO0VBQUEsSUFBQXpCLENBQUEsUUFBQUgsT0FBQTtJQUkxQzRCLEVBQUEsR0FBQTVCLE9BSUEsSUFIQyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQVIsS0FBTyxDQUFDLENBQU0sSUFBVSxDQUFWLFVBQVUsQ0FBQyxnQkFDWixDQUFBb0IsS0FBSyxDQUFBQyxPQUFPLENBQy9CLEVBRkMsSUFBSSxDQUdOO0lBQUFsQixDQUFBLE1BQUFILE9BQUE7SUFBQUcsQ0FBQSxNQUFBeUIsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQXpCLENBQUE7RUFBQTtFQUFBLElBQUEwQixFQUFBO0VBQUEsSUFBQTFCLENBQUEsUUFBQXdCLGFBQUE7SUFDREUsRUFBQSxJQUFDLElBQUksQ0FBTyxLQUFTLENBQVQsU0FBUyxDQUFNLElBQVUsQ0FBVixVQUFVLENBQUMsdUJBQ2IsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFKLEtBQUcsQ0FBQyxDQUFFRixjQUFZLENBQUUsRUFBekIsSUFBSSxDQUM5QixFQUZDLElBQUksQ0FFRTtJQUFBeEIsQ0FBQSxNQUFBd0IsYUFBQTtJQUFBeEIsQ0FBQSxNQUFBMEIsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQTFCLENBQUE7RUFBQTtFQUFBLElBQUEyQixFQUFBO0VBQUEsSUFBQTNCLENBQUEsUUFBQXlCLEVBQUEsSUFBQXpCLENBQUEsUUFBQTBCLEVBQUE7SUFSVEMsRUFBQSxLQUNHLENBQUFGLEVBSUQsQ0FDQSxDQUFBQyxFQUVNLENBQUMsR0FDTjtJQUFBMUIsQ0FBQSxNQUFBeUIsRUFBQTtJQUFBekIsQ0FBQSxNQUFBMEIsRUFBQTtJQUFBMUIsQ0FBQSxNQUFBMkIsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQTNCLENBQUE7RUFBQTtFQUFBLE9BVEgyQixFQVNHO0FBQUEiLCJpZ25vcmVMaXN0IjpbXX0= \ No newline at end of file diff --git a/src/components/Passes/Passes.tsx b/src/components/Passes/Passes.tsx index fe47f9db..82f19efd 100644 --- a/src/components/Passes/Passes.tsx +++ b/src/components/Passes/Passes.tsx @@ -7,7 +7,7 @@ import { setClipboard } from '../../ink/termio/osc.js'; // eslint-disable-next-line custom-rules/prefer-use-keybindings -- enter to copy link import { Box, Link, Text, useInput } from '../../ink.js'; import { useKeybinding } from '../../keybindings/useKeybinding.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { fetchReferralRedemptions, formatCreditAmount, getCachedOrFetchPassesEligibility } from '../../services/api/referral.js'; import type { ReferralRedemptionsResponse, ReferrerRewardInfo } from '../../services/oauth/types.js'; import { count } from '../../utils/array.js'; diff --git a/src/components/PromptInput/Notifications.tsx b/src/components/PromptInput/Notifications.tsx index 9b263cf2..79087370 100644 --- a/src/components/PromptInput/Notifications.tsx +++ b/src/components/PromptInput/Notifications.tsx @@ -3,7 +3,7 @@ import { feature } from 'bun:bundle'; import * as React from 'react'; import { type ReactNode, useEffect, useMemo, useState } from 'react'; import { type Notification, useNotifications } from 'src/context/notifications.js'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { useAppState } from 'src/state/AppState.js'; import { useVoiceState } from '../../context/voice.js'; import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'; diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx index 128e73cb..247fe9dd 100644 --- a/src/components/PromptInput/PromptInput.tsx +++ b/src/components/PromptInput/PromptInput.tsx @@ -6,7 +6,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore import { useNotifications } from 'src/context/notifications.js'; import { useCommandQueue } from 'src/hooks/useCommandQueue.js'; import { type IDEAtMentioned, useIdeAtMentioned } from 'src/hooks/useIdeAtMentioned.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { type AppState, useAppState, useAppStateStore, useSetAppState } from 'src/state/AppState.js'; import type { FooterItem } from 'src/state/AppStateStore.js'; import { getCwd } from 'src/utils/cwd.js'; diff --git a/src/components/PromptInput/PromptInputHelpMenu.tsx b/src/components/PromptInput/PromptInputHelpMenu.tsx index 53fdcc98..b4307346 100644 --- a/src/components/PromptInput/PromptInputHelpMenu.tsx +++ b/src/components/PromptInput/PromptInputHelpMenu.tsx @@ -5,7 +5,7 @@ import { Box, Text } from 'src/ink.js'; import { getPlatform } from 'src/utils/platform.js'; import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'; import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js'; import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js'; import { getNewlineInstructions } from './utils.js'; diff --git a/src/components/QuickOpenDialog.tsx b/src/components/QuickOpenDialog.tsx index 23c12af9..4b6b706f 100644 --- a/src/components/QuickOpenDialog.tsx +++ b/src/components/QuickOpenDialog.tsx @@ -6,7 +6,7 @@ import { useRegisterOverlay } from '../context/overlayContext.js'; import { generateFileSuggestions } from '../hooks/fileSuggestions.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { Text } from '../ink.js'; -import { logEvent } from '../services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import { getCwd } from '../utils/cwd.js'; import { openFileInExternalEditor } from '../utils/editor.js'; import { truncatePathMiddle, truncateToWidth } from '../utils/format.js'; diff --git a/src/components/Settings/Config.tsx b/src/components/Settings/Config.tsx index 37ee93c9..7db97e0f 100644 --- a/src/components/Settings/Config.tsx +++ b/src/components/Settings/Config.tsx @@ -14,7 +14,7 @@ import chalk from 'chalk'; import { permissionModeTitle, permissionModeFromString, toExternalPermissionMode, isExternalPermissionMode, EXTERNAL_PERMISSION_MODES, PERMISSION_MODES, type ExternalPermissionMode, type PermissionMode } from '../../utils/permissions/PermissionMode.js'; import { getAutoModeEnabledState, hasAutoModeOptInAnySource, transitionPlanAutoMode } from '../../utils/permissions/permissionSetup.js'; import { logError } from '../../utils/log.js'; -import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/index.js'; +import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics-stub.js'; import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'; import { ThemePicker } from '../ThemePicker.js'; import { useAppState, useSetAppState, useAppStateStore } from '../../state/AppState.js'; @@ -40,7 +40,7 @@ import { getUserMsgOptIn, setUserMsgOptIn } from '../../bootstrap/state.js'; import { DEFAULT_OUTPUT_STYLE_NAME } from 'src/constants/outputStyles.js'; import { isEnvTruthy, isRunningOnHomespace } from 'src/utils/envUtils.js'; import type { LocalJSXCommandContext, CommandResultDisplay } from '../../commands.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js'; import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'; import { getCliTeammateModeOverride, clearCliTeammateModeOverride } from '../../utils/swarm/backends/teammateModeSnapshot.js'; import { getHardcodedTeammateModelFallback } from '../../utils/swarm/teammateModel.js'; diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index c170c86b..86cb4e9f 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -6,7 +6,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { computeGlimmerIndex, computeShimmerSegments, SHIMMER_INTERVAL_MS } from '../bridge/bridgeStatusUtil.js'; import { feature } from 'bun:bundle'; import { getKairosActive, getUserMsgOptIn } from '../bootstrap/state.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js'; import { isEnvTruthy } from '../utils/envUtils.js'; import { count } from '../utils/array.js'; import sample from 'lodash-es/sample.js'; diff --git a/src/components/StatusLine.tsx b/src/components/StatusLine.tsx index eafb49ad..115ef8ab 100644 --- a/src/components/StatusLine.tsx +++ b/src/components/StatusLine.tsx @@ -1,7 +1,7 @@ import { feature } from 'bun:bundle'; import * as React from 'react'; import { memo, useCallback, useEffect, useRef } from 'react'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import { useAppState, useSetAppState } from 'src/state/AppState.js'; import type { PermissionMode } from 'src/utils/permissions/PermissionMode.js'; import { getIsRemoteMode, getKairosActive, getMainThreadAgentType, getOriginalCwd, getSdkBetas, getSessionId } from '../bootstrap/state.js'; diff --git a/src/components/TeleportResumeWrapper.tsx b/src/components/TeleportResumeWrapper.tsx index ead5fdb9..d40678a9 100644 --- a/src/components/TeleportResumeWrapper.tsx +++ b/src/components/TeleportResumeWrapper.tsx @@ -1,6 +1,6 @@ import { c as _c } from "react/compiler-runtime"; import React, { useEffect } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; import type { TeleportRemoteResponse } from 'src/utils/conversationRecovery.js'; import type { CodeSession } from 'src/utils/teleport/api.js'; import { type TeleportSource, useTeleportResume } from '../hooks/useTeleportResume.js'; diff --git a/src/components/TokenWarning.tsx b/src/components/TokenWarning.tsx index 8a5c2004..df549896 100644 --- a/src/components/TokenWarning.tsx +++ b/src/components/TokenWarning.tsx @@ -3,7 +3,7 @@ import { feature } from 'bun:bundle'; import * as React from 'react'; import { useSyncExternalStore } from 'react'; import { Box, Text } from '../ink.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js'; import { calculateTokenWarningState, getEffectiveContextWindowSize, isAutoCompactEnabled } from '../services/compact/autoCompact.js'; import { useCompactWarningSuppression } from '../services/compact/compactWarningHook.js'; import { getUpgradeMessage } from '../utils/model/contextWindowUpgradeCheck.js'; diff --git a/src/components/TrustDialog/TrustDialog.tsx b/src/components/TrustDialog/TrustDialog.tsx index 3bfcc22c..c3d66869 100644 --- a/src/components/TrustDialog/TrustDialog.tsx +++ b/src/components/TrustDialog/TrustDialog.tsx @@ -1,7 +1,7 @@ import { c as _c } from "react/compiler-runtime"; import { homedir } from 'os'; import React from 'react'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { setSessionTrustAccepted } from '../../bootstrap/state.js'; import type { Command } from '../../commands.js'; import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; @@ -206,7 +206,7 @@ export function TrustDialog(t0) { if ($[20] === Symbol.for("react.memo_cache_sentinel")) { t16 = {getFsImplementation().cwd()}; t17 = Quick safety check: Is this a project you created or one you trust? (Like your own code, a well-known open source project, or work from your team). If not, take a moment to review what{"'"}s in this folder first.; - t18 = Claude Code{"'"}ll be able to read, edit, and execute files here.; + t18 = Claudium{"'"}ll be able to read, edit, and execute files here.; $[20] = t16; $[21] = t17; $[22] = t18; diff --git a/src/components/WorktreeExitDialog.tsx b/src/components/WorktreeExitDialog.tsx index c51c9390..e74fb69f 100644 --- a/src/components/WorktreeExitDialog.tsx +++ b/src/components/WorktreeExitDialog.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import type { CommandResultDisplay } from 'src/commands.js'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import { logForDebugging } from 'src/utils/debug.js'; import { Box, Text } from '../ink.js'; import { execFileNoThrow } from '../utils/execFileNoThrow.js'; diff --git a/src/components/agents/generateAgent.ts b/src/components/agents/generateAgent.ts index 04fd6245..7f88ba3a 100644 --- a/src/components/agents/generateAgent.ts +++ b/src/components/agents/generateAgent.ts @@ -13,7 +13,7 @@ import { isAutoMemoryEnabled } from '../../memdir/paths.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { jsonParse } from '../../utils/slowOperations.js' import { asSystemPrompt } from '../../utils/systemPromptType.js' diff --git a/src/components/agents/new-agent-creation/wizard-steps/ConfirmStepWrapper.tsx b/src/components/agents/new-agent-creation/wizard-steps/ConfirmStepWrapper.tsx index 343eca21..10dbbcaa 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/ConfirmStepWrapper.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/ConfirmStepWrapper.tsx @@ -1,6 +1,6 @@ import chalk from 'chalk'; import React, { type ReactNode, useCallback, useState } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../../services/analytics-stub.js'; import { useSetAppState } from 'src/state/AppState.js'; import type { Tools } from '../../../../Tool.js'; import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'; diff --git a/src/components/grove/Grove.tsx b/src/components/grove/Grove.tsx index be5505bc..e242e271 100644 --- a/src/components/grove/Grove.tsx +++ b/src/components/grove/Grove.tsx @@ -1,6 +1,6 @@ import { c as _c } from "react/compiler-runtime"; import React, { useEffect, useState } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { Box, Link, Text, useInput } from '../../ink.js'; import { type AccountSettings, calculateShouldShowGrove, type GroveConfig, getGroveNoticeConfig, getGroveSettings, markGroveNoticeViewed, updateGroveSettings } from '../../services/api/grove.js'; import { Select } from '../CustomSelect/index.js'; diff --git a/src/components/mcp/MCPRemoteServerMenu.tsx b/src/components/mcp/MCPRemoteServerMenu.tsx index 954c6f30..abcf38ba 100644 --- a/src/components/mcp/MCPRemoteServerMenu.tsx +++ b/src/components/mcp/MCPRemoteServerMenu.tsx @@ -1,6 +1,6 @@ import figures from 'figures'; import React, { useEffect, useRef, useState } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import type { CommandResultDisplay } from '../../commands.js'; import { getOauthConfig } from '../../constants/oauth.js'; import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; diff --git a/src/components/memory/MemoryFileSelector.tsx b/src/components/memory/MemoryFileSelector.tsx index 0c207efe..c938803a 100644 --- a/src/components/memory/MemoryFileSelector.tsx +++ b/src/components/memory/MemoryFileSelector.tsx @@ -10,7 +10,7 @@ import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithK import { Box, Text } from '../../ink.js'; import { useKeybinding } from '../../keybindings/useKeybinding.js'; import { getAutoMemPath, isAutoMemoryEnabled } from '../../memdir/paths.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { isAutoDreamEnabled } from '../../services/autoDream/config.js'; import { readLastConsolidatedAt } from '../../services/autoDream/consolidationLock.js'; import { useAppState } from '../../state/AppState.js'; diff --git a/src/components/messageActions.tsx b/src/components/messageActions.tsx index 982eca0b..5712e667 100644 --- a/src/components/messageActions.tsx +++ b/src/components/messageActions.tsx @@ -4,7 +4,7 @@ import type { RefObject } from 'react'; import React, { useCallback, useMemo, useRef } from 'react'; import { Box, Text } from '../ink.js'; import { useKeybindings } from '../keybindings/useKeybinding.js'; -import { logEvent } from '../services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import type { NormalizedUserMessage, RenderableMessage } from '../types/message.js'; import { isEmptyMessageText, SYNTHETIC_MESSAGES } from '../utils/messages.js'; const NAVIGABLE_TYPES = ['user', 'assistant', 'grouped_tool_use', 'collapsed_read_search', 'system', 'attachment'] as const; diff --git a/src/components/messages/UserPromptMessage.tsx b/src/components/messages/UserPromptMessage.tsx index e2fa3d7e..5afa73a8 100644 --- a/src/components/messages/UserPromptMessage.tsx +++ b/src/components/messages/UserPromptMessage.tsx @@ -3,7 +3,7 @@ import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; import React, { useContext, useMemo } from 'react'; import { getKairosActive, getUserMsgOptIn } from '../../bootstrap/state.js'; import { Box } from '../../ink.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js'; import { useAppState } from '../../state/AppState.js'; import { isEnvTruthy } from '../../utils/envUtils.js'; import { logError } from '../../utils/log.js'; diff --git a/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx b/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx index 38719156..e9791f2e 100644 --- a/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx +++ b/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx @@ -6,7 +6,7 @@ import { useTerminalSize } from '../../../hooks/useTerminalSize.js'; import { stringWidth } from '../../../ink/stringWidth.js'; import { useTheme } from '../../../ink.js'; import { useKeybindings } from '../../../keybindings/useKeybinding.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics-stub.js'; import { useAppState } from '../../../state/AppState.js'; import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'; import { AskUserQuestionTool } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'; diff --git a/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx b/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx index bb193726..2ea1e5dc 100644 --- a/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +++ b/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx @@ -4,9 +4,9 @@ import figures from 'figures'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Box, Text, useTheme } from '../../../ink.js'; import { useKeybinding } from '../../../keybindings/useKeybinding.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js'; -import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics-stub.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics-stub.js'; +import { sanitizeToolNameForAnalytics } from '../../../services/analytics-stub.js'; import { useAppState } from '../../../state/AppState.js'; import { BashTool } from '../../../tools/BashTool/BashTool.js'; import { getFirstWordPrefix, getSimpleCommandPrefix } from '../../../tools/BashTool/bashPermissions.js'; diff --git a/src/components/permissions/EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.tsx b/src/components/permissions/EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.tsx index d680f0c5..2c6631f0 100644 --- a/src/components/permissions/EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.tsx +++ b/src/components/permissions/EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.tsx @@ -2,7 +2,7 @@ import { c as _c } from "react/compiler-runtime"; import React from 'react'; import { handlePlanModeTransition } from '../../../bootstrap/state.js'; import { Box, Text } from '../../../ink.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics-stub.js'; import { useAppState } from '../../../state/AppState.js'; import { isPlanModeInterviewPhaseEnabled } from '../../../utils/planModeV2.js'; import { Select } from '../../CustomSelect/index.js'; diff --git a/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx b/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx index 8e0ca6c5..bfaadc11 100644 --- a/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx +++ b/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx @@ -3,7 +3,7 @@ import type { UUID } from 'crypto'; import figures from 'figures'; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useNotifications } from 'src/context/notifications.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics-stub.js'; import { useAppState, useAppStateStore, useSetAppState } from 'src/state/AppState.js'; import { getSdkBetas, getSessionId, isSessionPersistenceDisabled, setHasExitedPlanMode, setNeedsAutoModeExitAttachment, setNeedsPlanModeExitAttachment } from '../../../bootstrap/state.js'; import { generateSessionName } from '../../../commands/rename/generateSessionName.js'; diff --git a/src/components/permissions/FallbackPermissionRequest.tsx b/src/components/permissions/FallbackPermissionRequest.tsx index 2e889785..fcc9c39a 100644 --- a/src/components/permissions/FallbackPermissionRequest.tsx +++ b/src/components/permissions/FallbackPermissionRequest.tsx @@ -2,7 +2,7 @@ import { c as _c } from "react/compiler-runtime"; import React, { useCallback, useMemo } from 'react'; import { getOriginalCwd } from '../../bootstrap/state.js'; import { Box, Text, useTheme } from '../../ink.js'; -import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js'; +import { sanitizeToolNameForAnalytics } from '../../services/analytics-stub.js'; import { env } from '../../utils/env.js'; import { shouldShowAlwaysAllowOptions } from '../../utils/permissions/permissionsLoader.js'; import { truncateToLines } from '../../utils/stringUtils.js'; diff --git a/src/components/permissions/FilePermissionDialog/useFilePermissionDialog.ts b/src/components/permissions/FilePermissionDialog/useFilePermissionDialog.ts index 3272c967..63f7285c 100644 --- a/src/components/permissions/FilePermissionDialog/useFilePermissionDialog.ts +++ b/src/components/permissions/FilePermissionDialog/useFilePermissionDialog.ts @@ -4,8 +4,8 @@ import { useKeybindings } from '../../../keybindings/useKeybinding.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../../services/analytics/index.js' -import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js' +} from '../../../services/analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../../../services/analytics-stub.js' import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js' import type { CompletionType } from '../../../utils/unaryLogging.js' import type { ToolUseConfirm } from '../PermissionRequest.js' diff --git a/src/components/permissions/FilePermissionDialog/usePermissionHandler.ts b/src/components/permissions/FilePermissionDialog/usePermissionHandler.ts index 85821cbc..e0a4cd53 100644 --- a/src/components/permissions/FilePermissionDialog/usePermissionHandler.ts +++ b/src/components/permissions/FilePermissionDialog/usePermissionHandler.ts @@ -1,8 +1,8 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../../services/analytics/index.js' -import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js' +} from '../../../services/analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../../../services/analytics-stub.js' import type { ToolPermissionContext } from '../../../Tool.js' import { CLAUDE_FOLDER_PERMISSION_PATTERN, diff --git a/src/components/permissions/PermissionExplanation.tsx b/src/components/permissions/PermissionExplanation.tsx index b7fd36b2..d0c10052 100644 --- a/src/components/permissions/PermissionExplanation.tsx +++ b/src/components/permissions/PermissionExplanation.tsx @@ -2,7 +2,7 @@ import { c as _c } from "react/compiler-runtime"; import React, { Suspense, use, useState } from 'react'; import { Box, Text } from '../../ink.js'; import { useKeybinding } from '../../keybindings/useKeybinding.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import type { Message } from '../../types/message.js'; import { generatePermissionExplanation, isPermissionExplainerEnabled, type PermissionExplanation as PermissionExplanationType, type RiskLevel } from '../../utils/permissions/permissionExplainer.js'; import { ShimmerChar } from '../Spinner/ShimmerChar.js'; diff --git a/src/components/permissions/PermissionPrompt.tsx b/src/components/permissions/PermissionPrompt.tsx index cc04708a..86718804 100644 --- a/src/components/permissions/PermissionPrompt.tsx +++ b/src/components/permissions/PermissionPrompt.tsx @@ -3,7 +3,7 @@ import React, { type ReactNode, useCallback, useMemo, useState } from 'react'; import { Box, Text } from '../../ink.js'; import type { KeybindingAction } from '../../keybindings/types.js'; import { useKeybindings } from '../../keybindings/useKeybinding.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { useSetAppState } from '../../state/AppState.js'; import { type OptionWithDescription, Select } from '../CustomSelect/select.js'; export type FeedbackType = 'accept' | 'reject'; diff --git a/src/components/permissions/PowerShellPermissionRequest/PowerShellPermissionRequest.tsx b/src/components/permissions/PowerShellPermissionRequest/PowerShellPermissionRequest.tsx index 2a7cd38e..1956d37e 100644 --- a/src/components/permissions/PowerShellPermissionRequest/PowerShellPermissionRequest.tsx +++ b/src/components/permissions/PowerShellPermissionRequest/PowerShellPermissionRequest.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Box, Text, useTheme } from '../../../ink.js'; import { useKeybinding } from '../../../keybindings/useKeybinding.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js'; -import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics-stub.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics-stub.js'; +import { sanitizeToolNameForAnalytics } from '../../../services/analytics-stub.js'; import { getDestructiveCommandWarning } from '../../../tools/PowerShellTool/destructiveCommandWarning.js'; import { PowerShellTool } from '../../../tools/PowerShellTool/PowerShellTool.js'; import { isAllowlistedCommand } from '../../../tools/PowerShellTool/readOnlyValidation.js'; diff --git a/src/components/permissions/SandboxPermissionRequest.tsx b/src/components/permissions/SandboxPermissionRequest.tsx index bd994212..484e758d 100644 --- a/src/components/permissions/SandboxPermissionRequest.tsx +++ b/src/components/permissions/SandboxPermissionRequest.tsx @@ -2,7 +2,7 @@ import { c as _c } from "react/compiler-runtime"; import * as React from 'react'; import { Box, Text } from 'src/ink.js'; import { type NetworkHostPattern, shouldAllowManagedSandboxDomainsOnly } from 'src/utils/sandbox/sandbox-adapter.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { Select } from '../CustomSelect/select.js'; import { PermissionDialog } from './PermissionDialog.js'; export type SandboxPermissionRequestProps = { diff --git a/src/components/permissions/SkillPermissionRequest/SkillPermissionRequest.tsx b/src/components/permissions/SkillPermissionRequest/SkillPermissionRequest.tsx index 346f846b..4582fc7c 100644 --- a/src/components/permissions/SkillPermissionRequest/SkillPermissionRequest.tsx +++ b/src/components/permissions/SkillPermissionRequest/SkillPermissionRequest.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useMemo } from 'react'; import { logError } from 'src/utils/log.js'; import { getOriginalCwd } from '../../../bootstrap/state.js'; import { Box, Text } from '../../../ink.js'; -import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'; +import { sanitizeToolNameForAnalytics } from '../../../services/analytics-stub.js'; import { SKILL_TOOL_NAME } from '../../../tools/SkillTool/constants.js'; import { SkillTool } from '../../../tools/SkillTool/SkillTool.js'; import { env } from '../../../utils/env.js'; diff --git a/src/components/permissions/hooks.ts b/src/components/permissions/hooks.ts index 7cba31b7..631419e7 100644 --- a/src/components/permissions/hooks.ts +++ b/src/components/permissions/hooks.ts @@ -3,8 +3,8 @@ import { useEffect, useRef } from 'react' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' -import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js' +} from '../../services/analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../../services/analytics-stub.js' import { BashTool } from 'src/tools/BashTool/BashTool.js' import { splitCommand_DEPRECATED } from 'src/utils/bash/commands.js' import type { diff --git a/src/components/permissions/useShellPermissionFeedback.ts b/src/components/permissions/useShellPermissionFeedback.ts index 58abbbda..ddc6098c 100644 --- a/src/components/permissions/useShellPermissionFeedback.ts +++ b/src/components/permissions/useShellPermissionFeedback.ts @@ -2,8 +2,8 @@ import { useState } from 'react' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' -import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js' +} from '../../services/analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../../services/analytics-stub.js' import { useSetAppState } from '../../state/AppState.js' import type { ToolUseConfirm } from './PermissionRequest.js' import { logUnaryPermissionEvent } from './utils.js' diff --git a/src/constants/prompts.ts b/src/constants/prompts.ts index 9eb49b36..8c3bc353 100644 --- a/src/constants/prompts.ts +++ b/src/constants/prompts.ts @@ -46,7 +46,7 @@ import { import { isEnvTruthy } from '../utils/envUtils.js' import { isReplModeEnabled } from '../tools/REPLTool/constants.js' import { feature } from 'bun:bundle' -import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { shouldUseGlobalCacheScope } from '../utils/betas.js' import { isForkSubagentEnabled } from '../tools/AgentTool/forkSubagent.js' import { @@ -712,6 +712,11 @@ export async function computeSimpleEnvInfo( // @[MODEL LAUNCH]: Add a knowledge cutoff date for the new model. function getKnowledgeCutoff(modelId: string): string | null { const canonical = getCanonicalName(modelId) + // Allow environment variable override: CLAUDE_CODE_PROMPTS_KNOWLEDGE_DATE + const customDate = process.env.CLAUDE_CODE_PROMPTS_KNOWLEDGE_DATE?.trim() + if (customDate) { + return customDate + } if (canonical.includes('claude-sonnet-4-6')) { return 'August 2025' } else if (canonical.includes('claude-opus-4-6')) { diff --git a/src/constants/system.ts b/src/constants/system.ts index 0cd2e765..ed88fb36 100644 --- a/src/constants/system.ts +++ b/src/constants/system.ts @@ -1,7 +1,7 @@ // Critical system constants extracted to break circular dependencies import { feature } from 'bun:bundle' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { logForDebugging } from '../utils/debug.js' import { isEnvDefinedFalsy } from '../utils/envUtils.js' import { getAPIProvider } from '../utils/model/providers.js' diff --git a/src/coordinator/coordinatorMode.ts b/src/coordinator/coordinatorMode.ts index fc5dc4eb..2193b735 100644 --- a/src/coordinator/coordinatorMode.ts +++ b/src/coordinator/coordinatorMode.ts @@ -1,10 +1,10 @@ import { feature } from 'bun:bundle' import { ASYNC_AGENT_ALLOWED_TOOLS } from '../constants/tools.js' -import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { AGENT_TOOL_NAME } from '../tools/AgentTool/constants.js' import { BASH_TOOL_NAME } from '../tools/BashTool/toolName.js' import { FILE_EDIT_TOOL_NAME } from '../tools/FileEditTool/constants.js' diff --git a/src/cost-tracker.ts b/src/cost-tracker.ts index b03184c6..c5a54881 100644 --- a/src/cost-tracker.ts +++ b/src/cost-tracker.ts @@ -31,7 +31,7 @@ import type { ModelUsage } from './entrypoints/agentSdkTypes.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from './services/analytics/index.js' +} from './services/analytics-stub.js' import { getAdvisorUsage } from './utils/advisor.js' import { getCurrentProjectConfig, diff --git a/src/entrypoints/init.ts b/src/entrypoints/init.ts index 69eb01d2..6f76f77f 100644 --- a/src/entrypoints/init.ts +++ b/src/entrypoints/init.ts @@ -45,8 +45,8 @@ import { // ~400KB of OpenTelemetry + protobuf modules until telemetry is actually initialized. // gRPC exporters (~700KB via @grpc/grpc-js) are further lazy-loaded within instrumentation.ts. import { configureGlobalAgents } from '../utils/proxy.js' -import { isBetaTracingEnabled } from '../utils/telemetry/betaSessionTracing.js' -import { getTelemetryAttributes } from '../utils/telemetryAttributes.js' +import { isBetaTracingEnabled } from '../utils/telemetry-stub.js' +import { getTelemetryAttributes } from '../utils/telemetryAttributes-stub.js' import { setShellIfWindows } from '../utils/windowsPaths.js' // initialize1PEventLogging is dynamically imported to defer OpenTelemetry sdk-logs/resources @@ -87,22 +87,23 @@ export const init = memoize(async (): Promise => { setupGracefulShutdown() profileCheckpoint('init_after_graceful_shutdown') + // REMOVED: 1P event logging (privacy-focused fork) // Initialize 1P event logging (no security concerns, but deferred to avoid // loading OpenTelemetry sdk-logs at startup). growthbook.js is already in // the module cache by this point (firstPartyEventLogger imports it), so the // second dynamic import adds no load cost. - void Promise.all([ - import('../services/analytics/firstPartyEventLogger.js'), - import('../services/analytics/growthbook.js'), - ]).then(([fp, gb]) => { - fp.initialize1PEventLogging() - // Rebuild the logger provider if tengu_1p_event_batch_config changes - // mid-session. Change detection (isEqual) is inside the handler so - // unchanged refreshes are no-ops. - gb.onGrowthBookRefresh(() => { - void fp.reinitialize1PEventLoggingIfConfigChanged() - }) - }) + // void Promise.all([ + // import('../services/analytics/firstPartyEventLogger.js'), + // import('../services/analytics/growthbook.js'), + // ]).then(([fp, gb]) => { + // fp.initialize1PEventLogging() + // // Rebuild the logger provider if tengu_1p_event_batch_config changes + // // mid-session. Change detection (isEqual) is inside the handler so + // // unchanged refreshes are no-ops. + // gb.onGrowthBookRefresh(() => { + // void fp.reinitialize1PEventLoggingIfConfigChanged() + // }) + // }) profileCheckpoint('init_after_1p_event_logging') // Populate OAuth account info if it is not already cached in config. This is needed since the @@ -244,71 +245,71 @@ export const init = memoize(async (): Promise => { * For non-eligible users, initializes telemetry immediately. * This should only be called once, after the trust dialog has been accepted. */ -export function initializeTelemetryAfterTrust(): void { - if (isEligibleForRemoteManagedSettings()) { - // For SDK/headless mode with beta tracing, initialize eagerly first - // to ensure the tracer is ready before the first query runs. - // The async path below will still run but doInitializeTelemetry() guards against double init. - if (getIsNonInteractiveSession() && isBetaTracingEnabled()) { - void doInitializeTelemetry().catch(error => { - logForDebugging( - `[3P telemetry] Eager telemetry init failed (beta tracing): ${errorMessage(error)}`, - { level: 'error' }, - ) - }) - } - logForDebugging( - '[3P telemetry] Waiting for remote managed settings before telemetry init', - ) - void waitForRemoteManagedSettingsToLoad() - .then(async () => { - logForDebugging( - '[3P telemetry] Remote managed settings loaded, initializing telemetry', - ) +// export function initializeTelemetryAfterTrust(): void { +// if (isEligibleForRemoteManagedSettings()) { +// // For SDK/headless mode with beta tracing, initialize eagerly first +// // to ensure the tracer is ready before the first query runs. +// // The async path below will still run but doInitializeTelemetry() guards against double init. +// if (getIsNonInteractiveSession() && isBetaTracingEnabled()) { +// void doInitializeTelemetry().catch(error => { +// logForDebugging( +// `[3P telemetry] Eager telemetry init failed (beta tracing): ${errorMessage(error)}`, +// { level: 'error' }, +// ) +// }) +// } +// logForDebugging( +// '[3P telemetry] Waiting for remote managed settings before telemetry init', +// ) +// void waitForRemoteManagedSettingsToLoad() +// .then(async () => { +// logForDebugging( +// '[3P telemetry] Remote managed settings loaded, initializing telemetry', +// ) // Re-apply env vars to pick up remote settings before initializing telemetry. - applyConfigEnvironmentVariables() - await doInitializeTelemetry() - }) - .catch(error => { - logForDebugging( - `[3P telemetry] Telemetry init failed (remote settings path): ${errorMessage(error)}`, - { level: 'error' }, - ) - }) - } else { - void doInitializeTelemetry().catch(error => { - logForDebugging( - `[3P telemetry] Telemetry init failed: ${errorMessage(error)}`, - { level: 'error' }, - ) - }) - } -} +// applyConfigEnvironmentVariables() +// await doInitializeTelemetry() +// }) +// .catch(error => { +// logForDebugging( +// `[3P telemetry] Telemetry init failed (remote settings path): ${errorMessage(error)}`, +// { level: 'error' }, +// ) +// }) +// } else { +// void doInitializeTelemetry().catch(error => { +// logForDebugging( +// `[3P telemetry] Telemetry init failed: ${errorMessage(error)}`, +// { level: 'error' }, +// ) +// }) +// } +// } -async function doInitializeTelemetry(): Promise { - if (telemetryInitialized) { +// async function doInitializeTelemetry(): Promise { +// if (telemetryInitialized) { // Already initialized, nothing to do - return - } +// return +// } // Set flag before init to prevent double initialization - telemetryInitialized = true - try { - await setMeterState() - } catch (error) { +// telemetryInitialized = true +// try { +// await setMeterState() +// } catch (error) { // Reset flag on failure so subsequent calls can retry - telemetryInitialized = false - throw error - } -} +// telemetryInitialized = false +// throw error +// } +// } async function setMeterState(): Promise { // Lazy-load instrumentation to defer ~400KB of OpenTelemetry + protobuf - const { initializeTelemetry } = await import( - '../utils/telemetry/instrumentation.js' - ) +// const { initializeTelemetry } = await import( +// '../utils/telemetry/instrumentation.js' +// ) // Initialize customer OTLP telemetry (metrics, logs, traces) - const meter = await initializeTelemetry() +// const meter = await initializeTelemetry() if (meter) { // Create factory function for attributed counters const createAttributedCounter = ( diff --git a/src/hooks/fileSuggestions.ts b/src/hooks/fileSuggestions.ts index 95403108..24143b65 100644 --- a/src/hooks/fileSuggestions.ts +++ b/src/hooks/fileSuggestions.ts @@ -11,7 +11,7 @@ import { FileIndex, yieldToEventLoop, } from '../native-ts/file-index/index.js' -import { logEvent } from '../services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import type { FileSuggestionCommandInput } from '../types/fileSuggestion.js' import { getGlobalConfig } from '../utils/config.js' import { getCwd } from '../utils/cwd.js' diff --git a/src/hooks/notifs/useCanSwitchToExistingSubscription.tsx b/src/hooks/notifs/useCanSwitchToExistingSubscription.tsx index 8884477d..7986d6af 100644 --- a/src/hooks/notifs/useCanSwitchToExistingSubscription.tsx +++ b/src/hooks/notifs/useCanSwitchToExistingSubscription.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { getOauthProfileFromApiKey } from 'src/services/oauth/getOauthProfile.js'; import { isClaudeAISubscriber } from 'src/utils/auth.js'; import { Text } from '../../ink.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; import { useStartupNotification } from './useStartupNotification.js'; const MAX_SHOW_COUNT = 3; diff --git a/src/hooks/notifs/useNpmDeprecationNotification.tsx b/src/hooks/notifs/useNpmDeprecationNotification.tsx index f0e9d292..781e066c 100644 --- a/src/hooks/notifs/useNpmDeprecationNotification.tsx +++ b/src/hooks/notifs/useNpmDeprecationNotification.tsx @@ -4,22 +4,7 @@ import { isEnvTruthy } from 'src/utils/envUtils.js'; import { useStartupNotification } from './useStartupNotification.js'; const NPM_DEPRECATION_MESSAGE = 'Claude Code has switched from npm to native installer. Run `claude install` or see https://docs.anthropic.com/en/docs/claude-code/getting-started for more options.'; export function useNpmDeprecationNotification() { - useStartupNotification(_temp); -} -async function _temp() { - if (isInBundledMode() || isEnvTruthy(process.env.DISABLE_INSTALLATION_CHECKS)) { - return null; - } - const installationType = await getCurrentInstallationType(); - if (installationType === "development") { - return null; - } - return { - timeoutMs: 15000, - key: "npm-deprecation-warning", - text: NPM_DEPRECATION_MESSAGE, - color: "warning", - priority: "high" - }; + // Disabled in claude-code-clean - no update notifications + useStartupNotification(async () => null); } //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJpc0luQnVuZGxlZE1vZGUiLCJnZXRDdXJyZW50SW5zdGFsbGF0aW9uVHlwZSIsImlzRW52VHJ1dGh5IiwidXNlU3RhcnR1cE5vdGlmaWNhdGlvbiIsIk5QTV9ERVBSRUNBVElPTl9NRVNTQUdFIiwidXNlTnBtRGVwcmVjYXRpb25Ob3RpZmljYXRpb24iLCJfdGVtcCIsInByb2Nlc3MiLCJlbnYiLCJESVNBQkxFX0lOU1RBTExBVElPTl9DSEVDS1MiLCJpbnN0YWxsYXRpb25UeXBlIiwidGltZW91dE1zIiwia2V5IiwidGV4dCIsImNvbG9yIiwicHJpb3JpdHkiXSwic291cmNlcyI6WyJ1c2VOcG1EZXByZWNhdGlvbk5vdGlmaWNhdGlvbi50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgaXNJbkJ1bmRsZWRNb2RlIH0gZnJvbSAnc3JjL3V0aWxzL2J1bmRsZWRNb2RlLmpzJ1xuaW1wb3J0IHsgZ2V0Q3VycmVudEluc3RhbGxhdGlvblR5cGUgfSBmcm9tICdzcmMvdXRpbHMvZG9jdG9yRGlhZ25vc3RpYy5qcydcbmltcG9ydCB7IGlzRW52VHJ1dGh5IH0gZnJvbSAnc3JjL3V0aWxzL2VudlV0aWxzLmpzJ1xuaW1wb3J0IHsgdXNlU3RhcnR1cE5vdGlmaWNhdGlvbiB9IGZyb20gJy4vdXNlU3RhcnR1cE5vdGlmaWNhdGlvbi5qcydcblxuY29uc3QgTlBNX0RFUFJFQ0FUSU9OX01FU1NBR0UgPVxuICAnQ2xhdWRlIENvZGUgaGFzIHN3aXRjaGVkIGZyb20gbnBtIHRvIG5hdGl2ZSBpbnN0YWxsZXIuIFJ1biBgY2xhdWRlIGluc3RhbGxgIG9yIHNlZSBodHRwczovL2RvY3MuYW50aHJvcGljLmNvbS9lbi9kb2NzL2NsYXVkZS1jb2RlL2dldHRpbmctc3RhcnRlZCBmb3IgbW9yZSBvcHRpb25zLidcblxuZXhwb3J0IGZ1bmN0aW9uIHVzZU5wbURlcHJlY2F0aW9uTm90aWZpY2F0aW9uKCk6IHZvaWQge1xuICB1c2VTdGFydHVwTm90aWZpY2F0aW9uKGFzeW5jICgpID0+IHtcbiAgICBpZiAoXG4gICAgICBpc0luQnVuZGxlZE1vZGUoKSB8fFxuICAgICAgaXNFbnZUcnV0aHkocHJvY2Vzcy5lbnYuRElTQUJMRV9JTlNUQUxMQVRJT05fQ0hFQ0tTKVxuICAgICkge1xuICAgICAgcmV0dXJuIG51bGxcbiAgICB9XG4gICAgY29uc3QgaW5zdGFsbGF0aW9uVHlwZSA9IGF3YWl0IGdldEN1cnJlbnRJbnN0YWxsYXRpb25UeXBlKClcbiAgICBpZiAoaW5zdGFsbGF0aW9uVHlwZSA9PT0gJ2RldmVsb3BtZW50JykgcmV0dXJuIG51bGxcbiAgICByZXR1cm4ge1xuICAgICAgdGltZW91dE1zOiAxNTAwMCxcbiAgICAgIGtleTogJ25wbS1kZXByZWNhdGlvbi13YXJuaW5nJyxcbiAgICAgIHRleHQ6IE5QTV9ERVBSRUNBVElPTl9NRVNTQUdFLFxuICAgICAgY29sb3I6ICd3YXJuaW5nJyxcbiAgICAgIHByaW9yaXR5OiAnaGlnaCcsXG4gICAgfVxuICB9KVxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxTQUFTQSxlQUFlLFFBQVEsMEJBQTBCO0FBQzFELFNBQVNDLDBCQUEwQixRQUFRLCtCQUErQjtBQUMxRSxTQUFTQyxXQUFXLFFBQVEsdUJBQXVCO0FBQ25ELFNBQVNDLHNCQUFzQixRQUFRLDZCQUE2QjtBQUVwRSxNQUFNQyx1QkFBdUIsR0FDM0IscUtBQXFLO0FBRXZLLE9BQU8sU0FBQUMsOEJBQUE7RUFDTEYsc0JBQXNCLENBQUNHLEtBZ0J0QixDQUFDO0FBQUE7QUFqQkcsZUFBQUEsTUFBQTtFQUVILElBQ0VOLGVBQWUsQ0FDb0MsQ0FBQyxJQUFwREUsV0FBVyxDQUFDSyxPQUFPLENBQUFDLEdBQUksQ0FBQUMsMkJBQTRCLENBQUM7SUFBQSxPQUU3QyxJQUFJO0VBQUE7RUFFYixNQUFBQyxnQkFBQSxHQUF5QixNQUFNVCwwQkFBMEIsQ0FBQyxDQUFDO0VBQzNELElBQUlTLGdCQUFnQixLQUFLLGFBQWE7SUFBQSxPQUFTLElBQUk7RUFBQTtFQUFBLE9BQzVDO0lBQUFDLFNBQUEsRUFDTSxLQUFLO0lBQUFDLEdBQUEsRUFDWCx5QkFBeUI7SUFBQUMsSUFBQSxFQUN4QlQsdUJBQXVCO0lBQUFVLEtBQUEsRUFDdEIsU0FBUztJQUFBQyxRQUFBLEVBQ047RUFDWixDQUFDO0FBQUEiLCJpZ25vcmVMaXN0IjpbXX0= \ No newline at end of file diff --git a/src/hooks/toolPermission/PermissionContext.ts b/src/hooks/toolPermission/PermissionContext.ts index 43ae7e81..87f9247a 100644 --- a/src/hooks/toolPermission/PermissionContext.ts +++ b/src/hooks/toolPermission/PermissionContext.ts @@ -3,8 +3,8 @@ import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' -import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js' +} from '../../services/analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../../services/analytics-stub.js' import type { ToolUseConfirm } from '../../components/permissions/PermissionRequest.js' import type { ToolPermissionContext, diff --git a/src/hooks/toolPermission/permissionLogging.ts b/src/hooks/toolPermission/permissionLogging.ts index fa0dc5aa..8ebdd121 100644 --- a/src/hooks/toolPermission/permissionLogging.ts +++ b/src/hooks/toolPermission/permissionLogging.ts @@ -5,13 +5,13 @@ import { feature } from 'bun:bundle' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' -import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js' +} from '../../services/analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../../services/analytics-stub.js' import { getCodeEditToolDecisionCounter } from '../../bootstrap/state.js' import type { Tool as ToolType, ToolUseContext } from '../../Tool.js' import { getLanguageName } from '../../utils/cliHighlight.js' import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js' -import { logOTelEvent } from '../../utils/telemetry/events.js' +import { logOTelEvent } from '../../utils/telemetry-stub.js' import type { PermissionApprovalSource, PermissionRejectionSource, diff --git a/src/hooks/useAwaySummary.ts b/src/hooks/useAwaySummary.ts index 6fb23810..4001c9a9 100644 --- a/src/hooks/useAwaySummary.ts +++ b/src/hooks/useAwaySummary.ts @@ -4,7 +4,7 @@ import { getTerminalFocusState, subscribeTerminalFocus, } from '../ink/terminal-focus-state.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { generateAwaySummary } from '../services/awaySummary.js' import type { Message } from '../types/message.js' import { createAwaySummaryMessage } from '../utils/messages.js' diff --git a/src/hooks/useCanUseTool.tsx b/src/hooks/useCanUseTool.tsx index 1eb77d56..b0ab0f4a 100644 --- a/src/hooks/useCanUseTool.tsx +++ b/src/hooks/useCanUseTool.tsx @@ -3,8 +3,8 @@ import { feature } from 'bun:bundle'; import { APIUserAbortError } from '@anthropic-ai/sdk'; import * as React from 'react'; import { useCallback } from 'react'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; -import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; +import { sanitizeToolNameForAnalytics } from '../services/analytics-stub.js'; import type { ToolUseConfirm } from '../components/permissions/PermissionRequest.js'; import { Text } from '../ink.js'; import type { ToolPermissionContext, Tool as ToolType, ToolUseContext } from '../Tool.js'; diff --git a/src/hooks/useCancelRequest.ts b/src/hooks/useCancelRequest.ts index 4382e276..c530dc80 100644 --- a/src/hooks/useCancelRequest.ts +++ b/src/hooks/useCancelRequest.ts @@ -5,8 +5,8 @@ * This component renders nothing - it just registers the cancel keybinding handler. */ import { useCallback, useRef } from 'react' -import { logEvent } from 'src/services/analytics/index.js' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/metadata.js' +import { logEvent } from '../services/analytics-stub.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics-stub.js' import { useAppState, useAppStateStore, diff --git a/src/hooks/useClaudeCodeHintRecommendation.tsx b/src/hooks/useClaudeCodeHintRecommendation.tsx index 390185b3..dd664c41 100644 --- a/src/hooks/useClaudeCodeHintRecommendation.tsx +++ b/src/hooks/useClaudeCodeHintRecommendation.tsx @@ -11,7 +11,7 @@ import { c as _c } from "react/compiler-runtime"; import * as React from 'react'; import { useNotifications } from '../context/notifications.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent } from '../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent } from '../services/analytics-stub.js'; import { clearPendingHint, getPendingHintSnapshot, markShownThisSession, subscribeToPendingHint } from '../utils/claudeCodeHints.js'; import { logForDebugging } from '../utils/debug.js'; import { disableHintRecommendations, markHintPluginShown, type PluginHintRecommendation, resolvePluginHint } from '../utils/plugins/hintRecommendation.js'; diff --git a/src/hooks/useDiffInIDE.ts b/src/hooks/useDiffInIDE.ts index 8fb0d106..53f2785c 100644 --- a/src/hooks/useDiffInIDE.ts +++ b/src/hooks/useDiffInIDE.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'crypto' import { basename } from 'path' import { useEffect, useMemo, useRef, useState } from 'react' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { readFileSync } from 'src/utils/fileRead.js' import { expandPath } from 'src/utils/path.js' import type { PermissionOption } from '../components/permissions/FilePermissionDialog/permissionOptions.js' diff --git a/src/hooks/useDynamicConfig.ts b/src/hooks/useDynamicConfig.ts index 7edd5bb7..7f252f3e 100644 --- a/src/hooks/useDynamicConfig.ts +++ b/src/hooks/useDynamicConfig.ts @@ -1,5 +1,5 @@ import React from 'react' -import { getDynamicConfig_BLOCKS_ON_INIT } from '../services/analytics/growthbook.js' +import { getDynamicConfig_BLOCKS_ON_INIT } from '../services/analytics-stub.js' /** * React hook for dynamic config values. diff --git a/src/hooks/useGlobalKeybindings.tsx b/src/hooks/useGlobalKeybindings.tsx index 5f1c39b4..1dc95fcd 100644 --- a/src/hooks/useGlobalKeybindings.tsx +++ b/src/hooks/useGlobalKeybindings.tsx @@ -9,8 +9,8 @@ import { useCallback } from 'react'; import instances from '../ink/instances.js'; import { useKeybinding } from '../keybindings/useKeybinding.js'; import type { Screen } from '../screens/REPL.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics/index.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; import { useAppState, useSetAppState } from '../state/AppState.js'; import { count } from '../utils/array.js'; import { getTerminalPanel } from '../utils/terminalPanel.js'; diff --git a/src/hooks/useIdeLogging.ts b/src/hooks/useIdeLogging.ts index e73c2301..aab88d7a 100644 --- a/src/hooks/useIdeLogging.ts +++ b/src/hooks/useIdeLogging.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { z } from 'zod/v4' import type { MCPServerConnection } from '../services/mcp/types.js' import { getConnectedIdeClient } from '../utils/ide.js' diff --git a/src/hooks/useMainLoopModel.ts b/src/hooks/useMainLoopModel.ts index ceb54813..9d5df00d 100644 --- a/src/hooks/useMainLoopModel.ts +++ b/src/hooks/useMainLoopModel.ts @@ -1,5 +1,5 @@ import { useEffect, useReducer } from 'react' -import { onGrowthBookRefresh } from '../services/analytics/growthbook.js' +import { onGrowthBookRefresh } from '../services/analytics-stub.js' import { useAppState } from '../state/AppState.js' import { getDefaultMainLoopModelSetting, diff --git a/src/hooks/useManagePlugins.ts b/src/hooks/useManagePlugins.ts index 7efe1d55..b9ca477c 100644 --- a/src/hooks/useManagePlugins.ts +++ b/src/hooks/useManagePlugins.ts @@ -4,7 +4,7 @@ import { useNotifications } from '../context/notifications.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { reinitializeLspServerManager } from '../services/lsp/manager.js' import { useAppState, useSetAppState } from '../state/AppState.js' import type { AgentDefinition } from '../tools/AgentTool/loadAgentsDir.js' diff --git a/src/hooks/usePromptSuggestion.ts b/src/hooks/usePromptSuggestion.ts index 0a0a35f9..90c2108a 100644 --- a/src/hooks/usePromptSuggestion.ts +++ b/src/hooks/usePromptSuggestion.ts @@ -3,7 +3,7 @@ import { useTerminalFocus } from '../ink/hooks/use-terminal-focus.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { abortSpeculation } from '../services/PromptSuggestion/speculation.js' import { useAppState, useSetAppState } from '../state/AppState.js' diff --git a/src/hooks/useReplBridge.tsx b/src/hooks/useReplBridge.tsx index 7c10ac6b..0d08bd92 100644 --- a/src/hooks/useReplBridge.tsx +++ b/src/hooks/useReplBridge.tsx @@ -13,7 +13,7 @@ import { useNotifications } from '../context/notifications.js'; import type { PermissionMode, SDKMessage } from '../entrypoints/agentSdkTypes.js'; import type { SDKControlResponse } from '../entrypoints/sdk/controlTypes.js'; import { Text } from '../ink.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js'; import { useAppState, useAppStateStore, useSetAppState } from '../state/AppState.js'; import type { Message } from '../types/message.js'; import { getCwd } from '../utils/cwd.js'; diff --git a/src/hooks/useSkillImprovementSurvey.ts b/src/hooks/useSkillImprovementSurvey.ts index 29f27256..e7b0ca62 100644 --- a/src/hooks/useSkillImprovementSurvey.ts +++ b/src/hooks/useSkillImprovementSurvey.ts @@ -4,7 +4,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { useAppState, useSetAppState } from '../state/AppState.js' import type { Message } from '../types/message.js' import type { SkillUpdate } from '../utils/hooks/skillImprovement.js' diff --git a/src/hooks/useSkillsChange.ts b/src/hooks/useSkillsChange.ts index 198675de..58697543 100644 --- a/src/hooks/useSkillsChange.ts +++ b/src/hooks/useSkillsChange.ts @@ -5,7 +5,7 @@ import { clearCommandsCache, getCommands, } from '../commands.js' -import { onGrowthBookRefresh } from '../services/analytics/growthbook.js' +import { onGrowthBookRefresh } from '../services/analytics-stub.js' import { logError } from '../utils/log.js' import { skillChangeDetector } from '../utils/skills/skillChangeDetector.js' diff --git a/src/hooks/useTeleportResume.tsx b/src/hooks/useTeleportResume.tsx index 9b459aab..8759a48a 100644 --- a/src/hooks/useTeleportResume.tsx +++ b/src/hooks/useTeleportResume.tsx @@ -1,7 +1,7 @@ import { c as _c } from "react/compiler-runtime"; import { useCallback, useState } from 'react'; import { setTeleportedSessionInfo } from 'src/bootstrap/state.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; import type { TeleportRemoteResponse } from 'src/utils/conversationRecovery.js'; import type { CodeSession } from 'src/utils/teleport/api.js'; import { errorMessage, TeleportOperationError } from '../utils/errors.js'; diff --git a/src/hooks/useTypeahead.tsx b/src/hooks/useTypeahead.tsx index a269902b..44ca08e4 100644 --- a/src/hooks/useTypeahead.tsx +++ b/src/hooks/useTypeahead.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNotifications } from 'src/context/notifications.js'; import { Text } from 'src/ink.js'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import { useDebounceCallback } from 'usehooks-ts'; import { type Command, getCommandName } from '../commands.js'; import { getModeFromInput, getValueFromInput } from '../components/PromptInput/inputModes.js'; diff --git a/src/hooks/useVoice.ts b/src/hooks/useVoice.ts index 30c09917..0d03e5f0 100644 --- a/src/hooks/useVoice.ts +++ b/src/hooks/useVoice.ts @@ -12,7 +12,7 @@ import { useTerminalFocus } from '../ink/hooks/use-terminal-focus.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { getVoiceKeyterms } from '../services/voiceKeyterms.js' import { connectVoiceStream, diff --git a/src/interactiveHelpers.tsx b/src/interactiveHelpers.tsx index b88dbbf1..544e140e 100644 --- a/src/interactiveHelpers.tsx +++ b/src/interactiveHelpers.tsx @@ -1,18 +1,18 @@ import { feature } from 'bun:bundle'; import { appendFileSync } from 'fs'; import React from 'react'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from './services/analytics-stub.js'; import { gracefulShutdown, gracefulShutdownSync } from 'src/utils/gracefulShutdown.js'; import { type ChannelEntry, getAllowedChannels, setAllowedChannels, setHasDevChannels, setSessionTrustAccepted, setStatsStore } from './bootstrap/state.js'; import type { Command } from './commands.js'; import { createStatsStore, type StatsStore } from './context/stats.js'; import { getSystemContext } from './context.js'; -import { initializeTelemetryAfterTrust } from './entrypoints/init.js'; +// import { initializeTelemetryAfterTrust } from './entrypoints/init.js'; import { isSynchronizedOutputSupported } from './ink/terminal.js'; import type { RenderOptions, Root, TextProps } from './ink.js'; import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js'; import { startDeferredPrefetches } from './main.js'; -import { checkGate_CACHED_OR_BLOCKING, initializeGrowthBook, resetGrowthBook } from './services/analytics/growthbook.js'; +import { checkGate_CACHED_OR_BLOCKING, initializeGrowthBook, resetGrowthBook } from './services/analytics-stub.js'; import { isQualifiedForGrove } from './services/api/grove.js'; import { handleMcpjsonServerApprovals } from './services/mcpServerApproval.js'; import { AppStateProvider } from './state/AppState.js'; @@ -187,7 +187,7 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod // otelHeadersHelper (which requires trust to execute) are available. // Defer to next tick so the OTel dynamic import resolves after first render // instead of during the pre-render microtask queue. - setImmediate(() => initializeTelemetryAfterTrust()); +// setImmediate(() => initializeTelemetryAfterTrust()); if (await isQualifiedForGrove()) { const { GroveDialog diff --git a/src/keybindings/loadUserBindings.ts b/src/keybindings/loadUserBindings.ts index 416abe7a..c06611b3 100644 --- a/src/keybindings/loadUserBindings.ts +++ b/src/keybindings/loadUserBindings.ts @@ -13,8 +13,8 @@ import chokidar, { type FSWatcher } from 'chokidar' import { readFileSync } from 'fs' import { readFile, stat } from 'fs/promises' import { dirname, join } from 'path' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' -import { logEvent } from '../services/analytics/index.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' +import { logEvent } from '../services/analytics-stub.js' import { registerCleanup } from '../utils/cleanupRegistry.js' import { logForDebugging } from '../utils/debug.js' import { getClaudeConfigHomeDir } from '../utils/envUtils.js' diff --git a/src/keybindings/shortcutFormat.ts b/src/keybindings/shortcutFormat.ts index 45db3b0c..3de486b9 100644 --- a/src/keybindings/shortcutFormat.ts +++ b/src/keybindings/shortcutFormat.ts @@ -1,7 +1,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { loadKeybindingsSync } from './loadUserBindings.js' import { getBindingDisplayText } from './resolver.js' import type { KeybindingContextName } from './types.js' diff --git a/src/keybindings/useShortcutDisplay.ts b/src/keybindings/useShortcutDisplay.ts index d8217488..ec46d3d3 100644 --- a/src/keybindings/useShortcutDisplay.ts +++ b/src/keybindings/useShortcutDisplay.ts @@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { useOptionalKeybindingContext } from './KeybindingContext.js' import type { KeybindingContextName } from './types.js' diff --git a/src/main.tsx b/src/main.tsx index 016fe8c8..8f2f4bfc 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -29,11 +29,11 @@ import React from 'react'; import { getOauthConfig } from './constants/oauth.js'; import { getRemoteSessionUrl } from './constants/product.js'; import { getSystemContext, getUserContext } from './context.js'; -import { init, initializeTelemetryAfterTrust } from './entrypoints/init.js'; +import { init } from './entrypoints/init.js'; import { addToHistory } from './history.js'; import type { Root } from './ink.js'; import { launchRepl } from './replLauncher.js'; -import { hasGrowthBookEnvOverride, initializeGrowthBook, refreshGrowthBookAfterAuthChange } from './services/analytics/growthbook.js'; +import { hasGrowthBookEnvOverride, initializeGrowthBook, refreshGrowthBookAfterAuthChange } from './services/analytics-stub.js'; import { fetchBootstrapData } from './services/api/bootstrap.js'; import { type DownloadResult, downloadSessionFiles, type FilesApiConfig, parseFileSpecs } from './services/api/filesApi.js'; import { prefetchPassesEligibility } from './services/api/referral.js'; @@ -80,10 +80,10 @@ const coordinatorModeModule = feature('COORDINATOR_MODE') ? require('./coordinat const assistantModule = feature('KAIROS') ? require('./assistant/index.js') as typeof import('./assistant/index.js') : null; const kairosGate = feature('KAIROS') ? require('./assistant/gate.js') as typeof import('./assistant/gate.js') : null; import { relative, resolve } from 'path'; -import { isAnalyticsDisabled } from 'src/services/analytics/config.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; -import { initializeAnalyticsGates } from 'src/services/analytics/sink.js'; +import { isAnalyticsDisabled } from './services/analytics-stub.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from './services/analytics-stub.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from './services/analytics-stub.js'; +import { initializeAnalyticsGates } from './services/analytics-stub.js'; import { getOriginalCwd, setAdditionalDirectoriesForClaudeMd, setIsRemoteMode, setMainLoopModelOverride, setMainThreadAgentType, setTeleportedSessionInfo } from './bootstrap/state.js'; import { filterCommandsForRemoteMode, getCommands } from './commands.js'; import type { StatsStore } from './context/stats.js'; @@ -132,8 +132,8 @@ import { getInitialSettings, getManagedSettingsKeysForLogging, getSettingsForSou import { resetSettingsCache } from './utils/settings/settingsCache.js'; import type { ValidationError } from './utils/settings/validation.js'; import { DEFAULT_TASKS_MODE_TASK_LIST_ID, TASK_STATUSES } from './utils/tasks.js'; -import { logPluginLoadErrors, logPluginsEnabledForSession } from './utils/telemetry/pluginTelemetry.js'; -import { logSkillsLoaded } from './utils/telemetry/skillLoadedEvent.js'; +import { logPluginLoadErrors, logPluginsEnabledForSession } from './utils/telemetry-stub.js'; +import { logSkillsLoaded } from './utils/telemetry-stub.js'; import { generateTempFilePath } from './utils/tempfile.js'; import { validateUuid } from './utils/uuid.js'; // Plugin startup checks are now handled non-blockingly in REPL.tsx diff --git a/src/memdir/memdir.ts b/src/memdir/memdir.ts index 1e7e68b5..636f072b 100644 --- a/src/memdir/memdir.ts +++ b/src/memdir/memdir.ts @@ -9,12 +9,12 @@ const teamMemPaths = feature('TEAMMEM') : null import { getKairosActive, getOriginalCwd } from '../bootstrap/state.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' /* eslint-enable @typescript-eslint/no-require-imports */ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { GREP_TOOL_NAME } from '../tools/GrepTool/prompt.js' import { isReplModeEnabled } from '../tools/REPLTool/constants.js' import { logForDebugging } from '../utils/debug.js' diff --git a/src/memdir/paths.ts b/src/memdir/paths.ts index 68a6baf0..59feb17d 100644 --- a/src/memdir/paths.ts +++ b/src/memdir/paths.ts @@ -5,7 +5,7 @@ import { getIsNonInteractiveSession, getProjectRoot, } from '../bootstrap/state.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { getClaudeConfigHomeDir, isEnvDefinedFalsy, diff --git a/src/memdir/teamMemPaths.ts b/src/memdir/teamMemPaths.ts index 1a13ae7e..b7296e72 100644 --- a/src/memdir/teamMemPaths.ts +++ b/src/memdir/teamMemPaths.ts @@ -1,6 +1,6 @@ import { lstat, realpath } from 'fs/promises' import { dirname, join, resolve, sep } from 'path' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { getErrnoCode } from '../utils/errors.js' import { getAutoMemPath, isAutoMemoryEnabled } from './paths.js' diff --git a/src/migrations/migrateAutoUpdatesToSettings.ts b/src/migrations/migrateAutoUpdatesToSettings.ts index c541713b..e8816db0 100644 --- a/src/migrations/migrateAutoUpdatesToSettings.ts +++ b/src/migrations/migrateAutoUpdatesToSettings.ts @@ -1,4 +1,4 @@ -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js' import { logError } from '../utils/log.js' import { diff --git a/src/migrations/migrateBypassPermissionsAcceptedToSettings.ts b/src/migrations/migrateBypassPermissionsAcceptedToSettings.ts index e36407f5..ec642134 100644 --- a/src/migrations/migrateBypassPermissionsAcceptedToSettings.ts +++ b/src/migrations/migrateBypassPermissionsAcceptedToSettings.ts @@ -1,4 +1,4 @@ -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js' import { logError } from '../utils/log.js' import { diff --git a/src/migrations/migrateEnableAllProjectMcpServersToSettings.ts b/src/migrations/migrateEnableAllProjectMcpServersToSettings.ts index 42d1bc20..bf6285c0 100644 --- a/src/migrations/migrateEnableAllProjectMcpServersToSettings.ts +++ b/src/migrations/migrateEnableAllProjectMcpServersToSettings.ts @@ -1,4 +1,4 @@ -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { getCurrentProjectConfig, saveCurrentProjectConfig, diff --git a/src/migrations/migrateLegacyOpusToCurrent.ts b/src/migrations/migrateLegacyOpusToCurrent.ts index bdca4aad..616c9e20 100644 --- a/src/migrations/migrateLegacyOpusToCurrent.ts +++ b/src/migrations/migrateLegacyOpusToCurrent.ts @@ -1,7 +1,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { saveGlobalConfig } from '../utils/config.js' import { isLegacyModelRemapEnabled } from '../utils/model/model.js' import { getAPIProvider } from '../utils/model/providers.js' diff --git a/src/migrations/migrateOpusToOpus1m.ts b/src/migrations/migrateOpusToOpus1m.ts index e065e198..b403ec2d 100644 --- a/src/migrations/migrateOpusToOpus1m.ts +++ b/src/migrations/migrateOpusToOpus1m.ts @@ -1,4 +1,4 @@ -import { logEvent } from '../services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { getDefaultMainLoopModelSetting, isOpus1mMergeEnabled, diff --git a/src/migrations/migrateSonnet45ToSonnet46.ts b/src/migrations/migrateSonnet45ToSonnet46.ts index bfbfcefe..d45cd603 100644 --- a/src/migrations/migrateSonnet45ToSonnet46.ts +++ b/src/migrations/migrateSonnet45ToSonnet46.ts @@ -1,7 +1,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { isMaxSubscriber, isProSubscriber, diff --git a/src/migrations/resetAutoModeOptInForDefaultOffer.ts b/src/migrations/resetAutoModeOptInForDefaultOffer.ts index bc0c78a4..ff5b0e3f 100644 --- a/src/migrations/resetAutoModeOptInForDefaultOffer.ts +++ b/src/migrations/resetAutoModeOptInForDefaultOffer.ts @@ -1,5 +1,5 @@ import { feature } from 'bun:bundle' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js' import { logError } from '../utils/log.js' import { getAutoModeEnabledState } from '../utils/permissions/permissionSetup.js' diff --git a/src/migrations/resetProToOpusDefault.ts b/src/migrations/resetProToOpusDefault.ts index 601872f7..4409fb8e 100644 --- a/src/migrations/resetProToOpusDefault.ts +++ b/src/migrations/resetProToOpusDefault.ts @@ -1,4 +1,4 @@ -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { isProSubscriber } from '../utils/auth.js' import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js' import { getAPIProvider } from '../utils/model/providers.js' diff --git a/src/query.ts b/src/query.ts index 07e8b6fa..6c2640c0 100644 --- a/src/query.ts +++ b/src/query.ts @@ -22,7 +22,7 @@ const contextCollapse = feature('CONTEXT_COLLAPSE') import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, -} from 'src/services/analytics/index.js' +} from './services/analytics-stub.js' import { ImageSizeError } from './utils/imageValidation.js' import { ImageResizeError } from './utils/imageResizer.js' import { findToolByName, type ToolUseContext } from './Tool.js' @@ -87,7 +87,7 @@ import { tokenCountWithEstimation, } from './utils/tokens.js' import { ESCALATED_MAX_TOKENS } from './utils/context.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from './services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from './services/analytics-stub.js' import { SLEEP_TOOL_NAME } from './tools/SleepTool/prompt.js' import { executePostSamplingHooks } from './utils/hooks/postSamplingHooks.js' import { executeStopFailureHooks } from './utils/hooks.js' diff --git a/src/query/config.ts b/src/query/config.ts index 83261e6a..e2494f62 100644 --- a/src/query/config.ts +++ b/src/query/config.ts @@ -1,5 +1,5 @@ import { getSessionId } from '../bootstrap/state.js' -import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import type { SessionId } from '../types/ids.js' import { isEnvTruthy } from '../utils/envUtils.js' diff --git a/src/query/stopHooks.ts b/src/query/stopHooks.ts index 1118086c..56a687b6 100644 --- a/src/query/stopHooks.ts +++ b/src/query/stopHooks.ts @@ -4,7 +4,7 @@ import { isExtractModeActive } from '../memdir/paths.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import type { ToolUseContext } from '../Tool.js' import type { HookProgress } from '../types/hooks.js' import type { diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index 54085283..123a050e 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -44,7 +44,7 @@ import { WorkerPendingPermission } from '../components/permissions/WorkerPending import { injectUserMessageToTeammate, getAllInProcessTeammateTasks } from '../tasks/InProcessTeammateTask/InProcessTeammateTask.js'; import { isLocalAgentTask, queuePendingMessage, appendMessageToLocalAgent, type LocalAgentTaskState } from '../tasks/LocalAgentTask/LocalAgentTask.js'; import { registerLeaderToolUseConfirmQueue, unregisterLeaderToolUseConfirmQueue, registerLeaderSetToolPermissionContext, unregisterLeaderSetToolPermissionContext } from '../utils/swarm/leaderPermissionBridge.js'; -import { endInteractionSpan } from '../utils/telemetry/sessionTracing.js'; +import { endInteractionSpan } from '../utils/telemetry-stub.js'; import { useLogMessages } from '../hooks/useLogMessages.js'; import { useReplBridge } from '../hooks/useReplBridge.js'; import { type Command, type CommandResultDisplay, type ResumeEntrypoint, getCommandName, isCommandEnabled } from '../commands.js'; @@ -130,8 +130,8 @@ import { clearSpeculativeChecks } from '../tools/BashTool/bashPermissions.js'; import type { AutoUpdaterResult } from '../utils/autoUpdater.js'; import { getGlobalConfig, saveGlobalConfig, getGlobalConfigWriteCount } from '../utils/config.js'; import { hasConsoleBillingAccess } from '../utils/billing.js'; -import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/index.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'; +import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics-stub.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js'; import { textForResubmit, handleMessageFromStream, type StreamingToolUse, type StreamingThinking, isCompactBoundaryMessage, getMessagesAfterCompactBoundary, getContentText, createUserMessage, createAssistantMessage, createTurnDurationMessage, createAgentsKilledMessage, createApiMetricsMessage, createSystemMessage, createCommandInputMessage, formatCommandInputTags } from '../utils/messages.js'; import { generateSessionTitle } from '../utils/sessionTitle.js'; import { BASH_INPUT_TAG, COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG, LOCAL_COMMAND_STDOUT_TAG } from '../constants/xml.js'; @@ -1135,7 +1135,7 @@ export function REPL({ // session from mid-conversation context. const haikuTitleAttemptedRef = useRef((initialMessages?.length ?? 0) > 0); const agentTitle = mainThreadAgentDefinition?.agentType; - const terminalTitle = sessionTitle ?? agentTitle ?? haikuTitle ?? 'Free Code'; + const terminalTitle = sessionTitle ?? agentTitle ?? haikuTitle ?? 'Claudium'; const isWaitingForApproval = toolUseConfirmQueue.length > 0 || promptQueue.length > 0 || pendingWorkerRequest || pendingSandboxRequest; // Local-jsx commands (like /plugin, /config) show user-facing dialogs that // wait for input. Require jsx != null — if the flag is stuck true but jsx diff --git a/src/screens/ResumeConversation.tsx b/src/screens/ResumeConversation.tsx index f97fa946..28ef8150 100644 --- a/src/screens/ResumeConversation.tsx +++ b/src/screens/ResumeConversation.tsx @@ -11,7 +11,7 @@ import { restoreCostStateForSession } from '../cost-tracker.js'; import { setClipboard } from '../ink/termio/osc.js'; import { Box, Text } from '../ink.js'; import { useKeybinding } from '../keybindings/useKeybinding.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; import type { MCPServerConnection, ScopedMcpServerConfig } from '../services/mcp/types.js'; import { useAppState, useSetAppState } from '../state/AppState.js'; import type { Tool } from '../Tool.js'; diff --git a/src/services/PromptSuggestion/promptSuggestion.ts b/src/services/PromptSuggestion/promptSuggestion.ts index dc68563f..f3236e7f 100644 --- a/src/services/PromptSuggestion/promptSuggestion.ts +++ b/src/services/PromptSuggestion/promptSuggestion.ts @@ -18,11 +18,11 @@ import { } from '../../utils/messages.js' import { getInitialSettings } from '../../utils/settings/settings.js' import { isTeammate } from '../../utils/teammate.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' import { currentLimits } from '../claudeAiLimits.js' import { isSpeculationEnabled, startSpeculation } from './speculation.js' diff --git a/src/services/PromptSuggestion/speculation.ts b/src/services/PromptSuggestion/speculation.ts index 0d96557f..fd44cd6f 100644 --- a/src/services/PromptSuggestion/speculation.ts +++ b/src/services/PromptSuggestion/speculation.ts @@ -46,7 +46,7 @@ import { jsonStringify } from '../../utils/slowOperations.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' import { generateSuggestion, getPromptVariant, diff --git a/src/services/SessionMemory/sessionMemory.ts b/src/services/SessionMemory/sessionMemory.ts index b0c67fba..a9fce0e5 100644 --- a/src/services/SessionMemory/sessionMemory.ts +++ b/src/services/SessionMemory/sessionMemory.ts @@ -39,7 +39,7 @@ import { import { sequential } from '../../utils/sequential.js' import { asSystemPrompt } from '../../utils/systemPromptType.js' import { getTokenUsage, tokenCountWithEstimation } from '../../utils/tokens.js' -import { logEvent } from '../analytics/index.js' +import { logEvent } from '../analytics-stub.js' import { isAutoCompactEnabled } from '../compact/autoCompact.js' import { buildSessionMemoryUpdatePrompt, @@ -71,7 +71,7 @@ import { errorMessage, getErrnoCode } from '../../utils/errors.js' import { getDynamicConfig_CACHED_MAY_BE_STALE, getFeatureValue_CACHED_MAY_BE_STALE, -} from '../analytics/growthbook.js' +} from '../analytics-stub.js' /** * Check if session memory feature is enabled. diff --git a/src/services/SessionMemory/sessionMemoryUtils.ts b/src/services/SessionMemory/sessionMemoryUtils.ts index ee4ec460..62526546 100644 --- a/src/services/SessionMemory/sessionMemoryUtils.ts +++ b/src/services/SessionMemory/sessionMemoryUtils.ts @@ -7,7 +7,7 @@ import { isFsInaccessible } from '../../utils/errors.js' import { getFsImplementation } from '../../utils/fsOperations.js' import { getSessionMemoryPath } from '../../utils/permissions/filesystem.js' import { sleep } from '../../utils/sleep.js' -import { logEvent } from '../analytics/index.js' +import { logEvent } from '../analytics-stub.js' const EXTRACTION_WAIT_TIMEOUT_MS = 15000 const EXTRACTION_STALE_THRESHOLD_MS = 60000 // 1 minute diff --git a/src/services/analytics-stub.ts b/src/services/analytics-stub.ts new file mode 100644 index 00000000..f7853db0 --- /dev/null +++ b/src/services/analytics-stub.ts @@ -0,0 +1,219 @@ +/** + * Stub file for analytics functionality + * All functions are no-ops to allow code to compile without analytics infrastructure + */ + +// Type stubs +export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = Record; +export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = Record; + +// No-op analytics functions +export function logEvent(...args: any[]): void { + // No-op +} + +export function initializeGrowthBook(...args: any[]): Promise { + return Promise.resolve(); +} + +export function refreshGrowthBookAfterAuthChange(...args: any[]): Promise { + return Promise.resolve(); +} + +export function hasGrowthBookEnvOverride(): boolean { + return false; +} + +export function isAnalyticsDisabled(): boolean { + return true; +} + +export function getFeatureValue_CACHED_MAY_BE_STALE(key: string, defaultValue?: any): any { + return defaultValue; +} + +export function getFeatureValue_CACHED_WITH_REFRESH(key: string, defaultValue?: any): any { + return defaultValue; +} + +export function getFeatureValue_DEPRECATED(key: string, defaultValue?: any): any { + return defaultValue; +} + +export function checkStatsigFeatureGate_CACHED_MAY_BE_STALE(gate: string): boolean { + return false; +} + +export function checkGate_CACHED_OR_BLOCKING(gate: string): Promise { + return Promise.resolve(false); +} + +export function initializeAnalyticsGates(...args: any[]): void { + // No-op +} + +export function initializeAnalyticsSink(...args: any[]): void { + // No-op +} + +export function shutdownDatadog(...args: any[]): Promise { + return Promise.resolve(); +} + +export function shutdown1PEventLogging(...args: any[]): Promise { + return Promise.resolve(); +} + +export function sanitizeToolNameForAnalytics(toolName: string): string { + return toolName; +} + +export function getDynamicConfig_BLOCKS_ON_INIT(key: string, defaultValue?: any): Promise { + return Promise.resolve(defaultValue); +} + +export function onGrowthBookRefresh(callback: () => void): void { + // No-op +} + +export function getDynamicConfig_CACHED_MAY_BE_STALE(key: string, defaultValue?: any): any { + return defaultValue; +} + +export function logEventTo1P(...args: any[]): void { + // No-op +} + +export function isFeedbackSurveyDisabled(): boolean { + return true; +} + +export function resetGrowthBook(...args: any[]): Promise { + return Promise.resolve(); +} + +export function getFileExtensionForAnalytics(path: string): string { + return path.split('.').pop() || ''; +} + +export function isToolDetailsLoggingEnabled(): boolean { + return false; +} + +export function isUserPromptsLoggingEnabled(): boolean { + return false; +} + +// Additional analytics functions +export function extractToolInputForTelemetry(...args: any[]): any { + return {}; +} + +export function extractToolResponseForTelemetry(...args: any[]): any { + return {}; +} + +export function buildMCPToolTelemetryFieldsAnonymized(...args: any[]): Record { + return {}; +} + +export function getQueryIdForLogging(): string { + return 'stub-query-id'; +} + +// Complete set of analytics stubs from original +export function mcpToolDetailsForAnalytics(...args: any[]): Record { + return {}; +} + +export function extractMcpToolDetails(...args: any[]): Record { + return {}; +} + +export function extractSkillName(...args: any[]): string { + return ''; +} + +export function getAllGrowthBookFeatures(): Record { + return {}; +} + +export function getApiBaseUrlHost(): string { + return ''; +} + +export function getEventMetadata(): Record { + return {}; +} + +export function getEventSamplingConfig(...args: any[]): Record { + return {}; +} + +export function getFileExtensionsFromBashCommand(...args: any[]): string[] { + return []; +} + +export function getGrowthBookConfigOverrides(): Record { + return {}; +} + +export function is1PEventLoggingEnabled(): boolean { + return false; +} + +export function isAnalyticsToolDetailsLoggingEnabled(): boolean { + return false; +} + +export function isSinkKilled(): boolean { + return true; +} + +export function logEventAsync(...args: any[]): Promise { + return Promise.resolve(); +} + +export function logGrowthBookExperimentTo1P(...args: any[]): void { + // No-op +} + +export function refreshGrowthBookFeatures(): Promise { + return Promise.resolve(); +} + +export function reinitialize1PEventLoggingIfConfigChanged(): Promise { + return Promise.resolve(); +} + +export function setGrowthBookConfigOverride(...args: any[]): void { + // No-op +} + +export function setupPeriodicGrowthBookRefresh(...args: any[]): void { + // No-op +} + +export function shouldSampleEvent(...args: any[]): boolean { + return false; +} + +export function checkSecurityRestrictionGate(...args: any[]): boolean { + return false; +} + +export function clearGrowthBookConfigOverrides(): void { + // No-op +} + +export function attachAnalyticsSink(...args: any[]): void { + // No-op +} + +export function initializeDatadog(...args: any[]): void { + // No-op +} + +export function initialize1PEventLogging(...args: any[]): void { + // No-op +} diff --git a/src/services/analytics/config.ts b/src/services/analytics/config.ts deleted file mode 100644 index 9e80601b..00000000 --- a/src/services/analytics/config.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Shared analytics configuration - * - * Common logic for determining when analytics should be disabled - * across all analytics systems (Datadog, 1P) - */ - -import { isEnvTruthy } from '../../utils/envUtils.js' -import { isTelemetryDisabled } from '../../utils/privacyLevel.js' - -/** - * Check if analytics operations should be disabled - * - * Analytics is disabled in the following cases: - * - Test environment (NODE_ENV === 'test') - * - Third-party cloud providers (Bedrock/Vertex) - * - Privacy level is no-telemetry or essential-traffic - */ -export function isAnalyticsDisabled(): boolean { - return ( - process.env.NODE_ENV === 'test' || - isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) || - isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) || - isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) || - isTelemetryDisabled() - ) -} - -/** - * Check if the feedback survey should be suppressed. - * - * Unlike isAnalyticsDisabled(), this does NOT block on 3P providers - * (Bedrock/Vertex/Foundry). The survey is a local UI prompt with no - * transcript data — enterprise customers capture responses via OTEL. - */ -export function isFeedbackSurveyDisabled(): boolean { - return process.env.NODE_ENV === 'test' || isTelemetryDisabled() -} diff --git a/src/services/analytics/datadog.ts b/src/services/analytics/datadog.ts deleted file mode 100644 index 2f8bdf31..00000000 --- a/src/services/analytics/datadog.ts +++ /dev/null @@ -1,307 +0,0 @@ -import axios from 'axios' -import { createHash } from 'crypto' -import memoize from 'lodash-es/memoize.js' -import { getOrCreateUserID } from '../../utils/config.js' -import { logError } from '../../utils/log.js' -import { getCanonicalName } from '../../utils/model/model.js' -import { getAPIProvider } from '../../utils/model/providers.js' -import { MODEL_COSTS } from '../../utils/modelCost.js' -import { isAnalyticsDisabled } from './config.js' -import { getEventMetadata } from './metadata.js' - -const DATADOG_LOGS_ENDPOINT = - 'https://http-intake.logs.us5.datadoghq.com/api/v2/logs' -const DATADOG_CLIENT_TOKEN = 'pubbbf48e6d78dae54bceaa4acf463299bf' -const DEFAULT_FLUSH_INTERVAL_MS = 15000 -const MAX_BATCH_SIZE = 100 -const NETWORK_TIMEOUT_MS = 5000 - -const DATADOG_ALLOWED_EVENTS = new Set([ - 'chrome_bridge_connection_succeeded', - 'chrome_bridge_connection_failed', - 'chrome_bridge_disconnected', - 'chrome_bridge_tool_call_completed', - 'chrome_bridge_tool_call_error', - 'chrome_bridge_tool_call_started', - 'chrome_bridge_tool_call_timeout', - 'tengu_api_error', - 'tengu_api_success', - 'tengu_brief_mode_enabled', - 'tengu_brief_mode_toggled', - 'tengu_brief_send', - 'tengu_cancel', - 'tengu_compact_failed', - 'tengu_exit', - 'tengu_flicker', - 'tengu_init', - 'tengu_model_fallback_triggered', - 'tengu_oauth_error', - 'tengu_oauth_success', - 'tengu_oauth_token_refresh_failure', - 'tengu_oauth_token_refresh_success', - 'tengu_oauth_token_refresh_lock_acquiring', - 'tengu_oauth_token_refresh_lock_acquired', - 'tengu_oauth_token_refresh_starting', - 'tengu_oauth_token_refresh_completed', - 'tengu_oauth_token_refresh_lock_releasing', - 'tengu_oauth_token_refresh_lock_released', - 'tengu_query_error', - 'tengu_session_file_read', - 'tengu_started', - 'tengu_tool_use_error', - 'tengu_tool_use_granted_in_prompt_permanent', - 'tengu_tool_use_granted_in_prompt_temporary', - 'tengu_tool_use_rejected_in_prompt', - 'tengu_tool_use_success', - 'tengu_uncaught_exception', - 'tengu_unhandled_rejection', - 'tengu_voice_recording_started', - 'tengu_voice_toggled', - 'tengu_team_mem_sync_pull', - 'tengu_team_mem_sync_push', - 'tengu_team_mem_sync_started', - 'tengu_team_mem_entries_capped', -]) - -const TAG_FIELDS = [ - 'arch', - 'clientType', - 'errorType', - 'http_status_range', - 'http_status', - 'kairosActive', - 'model', - 'platform', - 'provider', - 'skillMode', - 'subscriptionType', - 'toolName', - 'userBucket', - 'userType', - 'version', - 'versionBase', -] - -function camelToSnakeCase(str: string): string { - return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`) -} - -type DatadogLog = { - ddsource: string - ddtags: string - message: string - service: string - hostname: string - [key: string]: unknown -} - -let logBatch: DatadogLog[] = [] -let flushTimer: NodeJS.Timeout | null = null -let datadogInitialized: boolean | null = null - -async function flushLogs(): Promise { - if (logBatch.length === 0) return - - const logsToSend = logBatch - logBatch = [] - - try { - await axios.post(DATADOG_LOGS_ENDPOINT, logsToSend, { - headers: { - 'Content-Type': 'application/json', - 'DD-API-KEY': DATADOG_CLIENT_TOKEN, - }, - timeout: NETWORK_TIMEOUT_MS, - }) - } catch (error) { - logError(error) - } -} - -function scheduleFlush(): void { - if (flushTimer) return - - flushTimer = setTimeout(() => { - flushTimer = null - void flushLogs() - }, getFlushIntervalMs()).unref() -} - -export const initializeDatadog = memoize(async (): Promise => { - if (isAnalyticsDisabled()) { - datadogInitialized = false - return false - } - - try { - datadogInitialized = true - return true - } catch (error) { - logError(error) - datadogInitialized = false - return false - } -}) - -/** - * Flush remaining Datadog logs and shut down. - * Called from gracefulShutdown() before process.exit() since - * forceExit() prevents the beforeExit handler from firing. - */ -export async function shutdownDatadog(): Promise { - if (flushTimer) { - clearTimeout(flushTimer) - flushTimer = null - } - await flushLogs() -} - -// NOTE: use via src/services/analytics/index.ts > logEvent -export async function trackDatadogEvent( - eventName: string, - properties: { [key: string]: boolean | number | undefined }, -): Promise { - if (process.env.NODE_ENV !== 'production') { - return - } - - // Don't send events for 3P providers (Bedrock, Vertex, Foundry) - if (getAPIProvider() !== 'firstParty') { - return - } - - // Fast path: use cached result if available to avoid await overhead - let initialized = datadogInitialized - if (initialized === null) { - initialized = await initializeDatadog() - } - if (!initialized || !DATADOG_ALLOWED_EVENTS.has(eventName)) { - return - } - - try { - const metadata = await getEventMetadata({ - model: properties.model, - betas: properties.betas, - }) - // Destructure to avoid duplicate envContext (once nested, once flattened) - const { envContext, ...restMetadata } = metadata - const allData: Record = { - ...restMetadata, - ...envContext, - ...properties, - userBucket: getUserBucket(), - } - - // Normalize MCP tool names to "mcp" for cardinality reduction - if ( - typeof allData.toolName === 'string' && - allData.toolName.startsWith('mcp__') - ) { - allData.toolName = 'mcp' - } - - // Normalize model names for cardinality reduction (external users only) - if (process.env.USER_TYPE !== 'ant' && typeof allData.model === 'string') { - const shortName = getCanonicalName(allData.model.replace(/\[1m]$/i, '')) - allData.model = shortName in MODEL_COSTS ? shortName : 'other' - } - - // Truncate dev version to base + date (remove timestamp and sha for cardinality reduction) - // e.g. "2.0.53-dev.20251124.t173302.sha526cc6a" -> "2.0.53-dev.20251124" - if (typeof allData.version === 'string') { - allData.version = allData.version.replace( - /^(\d+\.\d+\.\d+-dev\.\d{8})\.t\d+\.sha[a-f0-9]+$/, - '$1', - ) - } - - // Transform status to http_status and http_status_range to avoid Datadog reserved field - if (allData.status !== undefined && allData.status !== null) { - const statusCode = String(allData.status) - allData.http_status = statusCode - - // Determine status range (1xx, 2xx, 3xx, 4xx, 5xx) - const firstDigit = statusCode.charAt(0) - if (firstDigit >= '1' && firstDigit <= '5') { - allData.http_status_range = `${firstDigit}xx` - } - - // Remove original status field to avoid conflict with Datadog's reserved field - delete allData.status - } - - // Build ddtags with high-cardinality fields for filtering. - // event: is prepended so the event name is searchable via the - // log search API — the `message` field (where eventName also lives) - // is a DD reserved field and is NOT queryable from dashboard widget - // queries or the aggregation API. See scripts/release/MONITORING.md. - const allDataRecord = allData - const tags = [ - `event:${eventName}`, - ...TAG_FIELDS.filter( - field => - allDataRecord[field] !== undefined && allDataRecord[field] !== null, - ).map(field => `${camelToSnakeCase(field)}:${allDataRecord[field]}`), - ] - - const log: DatadogLog = { - ddsource: 'nodejs', - ddtags: tags.join(','), - message: eventName, - service: 'claude-code', - hostname: 'claude-code', - env: process.env.USER_TYPE, - } - - // Add all fields as searchable attributes (not duplicated in tags) - for (const [key, value] of Object.entries(allData)) { - if (value !== undefined && value !== null) { - log[camelToSnakeCase(key)] = value - } - } - - logBatch.push(log) - - // Flush immediately if batch is full, otherwise schedule - if (logBatch.length >= MAX_BATCH_SIZE) { - if (flushTimer) { - clearTimeout(flushTimer) - flushTimer = null - } - void flushLogs() - } else { - scheduleFlush() - } - } catch (error) { - logError(error) - } -} - -const NUM_USER_BUCKETS = 30 - -/** - * Gets a 'bucket' that the user ID falls into. - * - * For alerting purposes, we want to alert on the number of users impacted - * by an issue, rather than the number of events- often a small number of users - * can generate a large number of events (e.g. due to retries). To approximate - * this without ruining cardinality by counting user IDs directly, we hash the user ID - * and assign it to one of a fixed number of buckets. - * - * This allows us to estimate the number of unique users by counting unique buckets, - * while preserving user privacy and reducing cardinality. - */ -const getUserBucket = memoize((): number => { - const userId = getOrCreateUserID() - const hash = createHash('sha256').update(userId).digest('hex') - return parseInt(hash.slice(0, 8), 16) % NUM_USER_BUCKETS -}) - -function getFlushIntervalMs(): number { - // Allow tests to override to not block on the default flush interval. - return ( - parseInt(process.env.CLAUDE_CODE_DATADOG_FLUSH_INTERVAL_MS || '', 10) || - DEFAULT_FLUSH_INTERVAL_MS - ) -} diff --git a/src/services/analytics/firstPartyEventLogger.ts b/src/services/analytics/firstPartyEventLogger.ts deleted file mode 100644 index e3a501d7..00000000 --- a/src/services/analytics/firstPartyEventLogger.ts +++ /dev/null @@ -1,449 +0,0 @@ -import type { AnyValueMap, Logger, logs } from '@opentelemetry/api-logs' -import { resourceFromAttributes } from '@opentelemetry/resources' -import { - BatchLogRecordProcessor, - LoggerProvider, -} from '@opentelemetry/sdk-logs' -import { - ATTR_SERVICE_NAME, - ATTR_SERVICE_VERSION, -} from '@opentelemetry/semantic-conventions' -import { randomUUID } from 'crypto' -import { isEqual } from 'lodash-es' -import { getOrCreateUserID } from '../../utils/config.js' -import { logForDebugging } from '../../utils/debug.js' -import { logError } from '../../utils/log.js' -import { getPlatform, getWslVersion } from '../../utils/platform.js' -import { jsonStringify } from '../../utils/slowOperations.js' -import { profileCheckpoint } from '../../utils/startupProfiler.js' -import { getCoreUserData } from '../../utils/user.js' -import { isAnalyticsDisabled } from './config.js' -import { FirstPartyEventLoggingExporter } from './firstPartyEventLoggingExporter.js' -import type { GrowthBookUserAttributes } from './growthbook.js' -import { getDynamicConfig_CACHED_MAY_BE_STALE } from './growthbook.js' -import { getEventMetadata } from './metadata.js' -import { isSinkKilled } from './sinkKillswitch.js' - -/** - * Configuration for sampling individual event types. - * Each event name maps to an object containing sample_rate (0-1). - * Events not in the config are logged at 100% rate. - */ -export type EventSamplingConfig = { - [eventName: string]: { - sample_rate: number - } -} - -const EVENT_SAMPLING_CONFIG_NAME = 'tengu_event_sampling_config' -/** - * Get the event sampling configuration from GrowthBook. - * Uses cached value if available, updates cache in background. - */ -export function getEventSamplingConfig(): EventSamplingConfig { - return getDynamicConfig_CACHED_MAY_BE_STALE( - EVENT_SAMPLING_CONFIG_NAME, - {}, - ) -} - -/** - * Determine if an event should be sampled based on its sample rate. - * Returns the sample rate if sampled, null if not sampled. - * - * @param eventName - Name of the event to check - * @returns The sample_rate if event should be logged, null if it should be dropped - */ -export function shouldSampleEvent(eventName: string): number | null { - const config = getEventSamplingConfig() - const eventConfig = config[eventName] - - // If no config for this event, log at 100% rate (no sampling) - if (!eventConfig) { - return null - } - - const sampleRate = eventConfig.sample_rate - - // Validate sample rate is in valid range - if (typeof sampleRate !== 'number' || sampleRate < 0 || sampleRate > 1) { - return null - } - - // Sample rate of 1 means log everything (no need to add metadata) - if (sampleRate >= 1) { - return null - } - - // Sample rate of 0 means drop everything - if (sampleRate <= 0) { - return 0 - } - - // Randomly decide whether to sample this event - return Math.random() < sampleRate ? sampleRate : 0 -} - -const BATCH_CONFIG_NAME = 'tengu_1p_event_batch_config' -type BatchConfig = { - scheduledDelayMillis?: number - maxExportBatchSize?: number - maxQueueSize?: number - skipAuth?: boolean - maxAttempts?: number - path?: string - baseUrl?: string -} -function getBatchConfig(): BatchConfig { - return getDynamicConfig_CACHED_MAY_BE_STALE( - BATCH_CONFIG_NAME, - {}, - ) -} - -// Module-local state for event logging (not exposed globally) -let firstPartyEventLogger: ReturnType | null = null -let firstPartyEventLoggerProvider: LoggerProvider | null = null -// Last batch config used to construct the provider — used by -// reinitialize1PEventLoggingIfConfigChanged to decide whether a rebuild is -// needed when GrowthBook refreshes. -let lastBatchConfig: BatchConfig | null = null -/** - * Flush and shutdown the 1P event logger. - * This should be called as the final step before process exit to ensure - * all events (including late ones from API responses) are exported. - */ -export async function shutdown1PEventLogging(): Promise { - if (!firstPartyEventLoggerProvider) { - return - } - try { - await firstPartyEventLoggerProvider.shutdown() - if (process.env.USER_TYPE === 'ant') { - logForDebugging('1P event logging: final shutdown complete') - } - } catch { - // Ignore shutdown errors - } -} - -/** - * Check if 1P event logging is enabled. - * Respects the same opt-outs as other analytics sinks: - * - Test environment - * - Third-party cloud providers (Bedrock/Vertex) - * - Global telemetry opt-outs - * - Non-essential traffic disabled - * - * Note: Unlike BigQuery metrics, event logging does NOT check organization-level - * metrics opt-out via API. It follows the same pattern as Statsig event logging. - */ -export function is1PEventLoggingEnabled(): boolean { - // Respect standard analytics opt-outs - return !isAnalyticsDisabled() -} - -/** - * Log a 1st-party event for internal analytics (async version). - * Events are batched and exported to /api/event_logging/batch - * - * This enriches the event with core metadata (model, session, env context, etc.) - * at log time, similar to logEventToStatsig. - * - * @param eventName - Name of the event (e.g., 'tengu_api_query') - * @param metadata - Additional metadata for the event (intentionally no strings, to avoid accidentally logging code/filepaths) - */ -async function logEventTo1PAsync( - firstPartyEventLogger: Logger, - eventName: string, - metadata: Record = {}, -): Promise { - try { - // Enrich with core metadata at log time (similar to Statsig pattern) - const coreMetadata = await getEventMetadata({ - model: metadata.model, - betas: metadata.betas, - }) - - // Build attributes - OTel supports nested objects natively via AnyValueMap - // Cast through unknown since our nested objects are structurally compatible - // with AnyValue but TS doesn't recognize it due to missing index signatures - const attributes = { - event_name: eventName, - event_id: randomUUID(), - // Pass objects directly - no JSON serialization needed - core_metadata: coreMetadata, - user_metadata: getCoreUserData(true), - event_metadata: metadata, - } as unknown as AnyValueMap - - // Add user_id if available - const userId = getOrCreateUserID() - if (userId) { - attributes.user_id = userId - } - - // Debug logging when debug mode is enabled - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `[ANT-ONLY] 1P event: ${eventName} ${jsonStringify(metadata, null, 0)}`, - ) - } - - // Emit log record - firstPartyEventLogger.emit({ - body: eventName, - attributes, - }) - } catch (e) { - if (process.env.NODE_ENV === 'development') { - throw e - } - if (process.env.USER_TYPE === 'ant') { - logError(e as Error) - } - // swallow - } -} - -/** - * Log a 1st-party event for internal analytics. - * Events are batched and exported to /api/event_logging/batch - * - * @param eventName - Name of the event (e.g., 'tengu_api_query') - * @param metadata - Additional metadata for the event (intentionally no strings, to avoid accidentally logging code/filepaths) - */ -export function logEventTo1P( - eventName: string, - metadata: Record = {}, -): void { - if (!is1PEventLoggingEnabled()) { - return - } - - if (!firstPartyEventLogger || isSinkKilled('firstParty')) { - return - } - - // Fire and forget - don't block on metadata enrichment - void logEventTo1PAsync(firstPartyEventLogger, eventName, metadata) -} - -/** - * GrowthBook experiment event data for logging - */ -export type GrowthBookExperimentData = { - experimentId: string - variationId: number - userAttributes?: GrowthBookUserAttributes - experimentMetadata?: Record -} - -// api.anthropic.com only serves the "production" GrowthBook environment -// (see starling/starling/cli/cli.py DEFAULT_ENVIRONMENTS). Staging and -// development environments are not exported to the prod API. -function getEnvironmentForGrowthBook(): string { - return 'production' -} - -/** - * Log a GrowthBook experiment assignment event to 1P. - * Events are batched and exported to /api/event_logging/batch - * - * @param data - GrowthBook experiment assignment data - */ -export function logGrowthBookExperimentTo1P( - data: GrowthBookExperimentData, -): void { - if (!is1PEventLoggingEnabled()) { - return - } - - if (!firstPartyEventLogger || isSinkKilled('firstParty')) { - return - } - - const userId = getOrCreateUserID() - const { accountUuid, organizationUuid } = getCoreUserData(true) - - // Build attributes for GrowthbookExperimentEvent - const attributes = { - event_type: 'GrowthbookExperimentEvent', - event_id: randomUUID(), - experiment_id: data.experimentId, - variation_id: data.variationId, - ...(userId && { device_id: userId }), - ...(accountUuid && { account_uuid: accountUuid }), - ...(organizationUuid && { organization_uuid: organizationUuid }), - ...(data.userAttributes && { - session_id: data.userAttributes.sessionId, - user_attributes: jsonStringify(data.userAttributes), - }), - ...(data.experimentMetadata && { - experiment_metadata: jsonStringify(data.experimentMetadata), - }), - environment: getEnvironmentForGrowthBook(), - } - - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `[ANT-ONLY] 1P GrowthBook experiment: ${data.experimentId} variation=${data.variationId}`, - ) - } - - firstPartyEventLogger.emit({ - body: 'growthbook_experiment', - attributes, - }) -} - -const DEFAULT_LOGS_EXPORT_INTERVAL_MS = 10000 -const DEFAULT_MAX_EXPORT_BATCH_SIZE = 200 -const DEFAULT_MAX_QUEUE_SIZE = 8192 - -/** - * Initialize 1P event logging infrastructure. - * This creates a separate LoggerProvider for internal event logging, - * independent of customer OTLP telemetry. - * - * This uses its own minimal resource configuration with just the attributes - * we need for internal analytics (service name, version, platform info). - */ -export function initialize1PEventLogging(): void { - profileCheckpoint('1p_event_logging_start') - const enabled = is1PEventLoggingEnabled() - - if (!enabled) { - if (process.env.USER_TYPE === 'ant') { - logForDebugging('1P event logging not enabled') - } - return - } - - // Fetch batch processor configuration from GrowthBook dynamic config - // Uses cached value if available, refreshes in background - const batchConfig = getBatchConfig() - lastBatchConfig = batchConfig - profileCheckpoint('1p_event_after_growthbook_config') - - const scheduledDelayMillis = - batchConfig.scheduledDelayMillis || - parseInt( - process.env.OTEL_LOGS_EXPORT_INTERVAL || - DEFAULT_LOGS_EXPORT_INTERVAL_MS.toString(), - ) - - const maxExportBatchSize = - batchConfig.maxExportBatchSize || DEFAULT_MAX_EXPORT_BATCH_SIZE - - const maxQueueSize = batchConfig.maxQueueSize || DEFAULT_MAX_QUEUE_SIZE - - // Build our own resource for 1P event logging with minimal attributes - const platform = getPlatform() - const attributes: Record = { - [ATTR_SERVICE_NAME]: 'claude-code', - [ATTR_SERVICE_VERSION]: MACRO.VERSION, - } - - // Add WSL-specific attributes if running on WSL - if (platform === 'wsl') { - const wslVersion = getWslVersion() - if (wslVersion) { - attributes['wsl.version'] = wslVersion - } - } - - const resource = resourceFromAttributes(attributes) - - // Create a new LoggerProvider with the EventLoggingExporter - // NOTE: This is kept separate from customer telemetry logs to ensure - // internal events don't leak to customer endpoints and vice versa. - // We don't register this globally - it's only used for internal event logging. - const eventLoggingExporter = new FirstPartyEventLoggingExporter({ - maxBatchSize: maxExportBatchSize, - skipAuth: batchConfig.skipAuth, - maxAttempts: batchConfig.maxAttempts, - path: batchConfig.path, - baseUrl: batchConfig.baseUrl, - isKilled: () => isSinkKilled('firstParty'), - }) - firstPartyEventLoggerProvider = new LoggerProvider({ - resource, - processors: [ - new BatchLogRecordProcessor(eventLoggingExporter, { - scheduledDelayMillis, - maxExportBatchSize, - maxQueueSize, - }), - ], - }) - - // Initialize event logger from our internal provider (NOT from global API) - // IMPORTANT: We must get the logger from our local provider, not logs.getLogger() - // because logs.getLogger() returns a logger from the global provider, which is - // separate and used for customer telemetry. - firstPartyEventLogger = firstPartyEventLoggerProvider.getLogger( - 'com.anthropic.claude_code.events', - MACRO.VERSION, - ) -} - -/** - * Rebuild the 1P event logging pipeline if the batch config changed. - * Register this with onGrowthBookRefresh so long-running sessions pick up - * changes to batch size, delay, endpoint, etc. - * - * Event-loss safety: - * 1. Null the logger first — concurrent logEventTo1P() calls hit the - * !firstPartyEventLogger guard and bail during the swap window. This drops - * a handful of events but prevents emitting to a draining provider. - * 2. forceFlush() drains the old BatchLogRecordProcessor buffer to the - * exporter. Export failures go to disk at getCurrentBatchFilePath() which - * is keyed by module-level BATCH_UUID + sessionId — unchanged across - * reinit — so the NEW exporter's disk-backed retry picks them up. - * 3. Swap to new provider/logger; old provider shutdown runs in background - * (buffer already drained, just cleanup). - */ -export async function reinitialize1PEventLoggingIfConfigChanged(): Promise { - if (!is1PEventLoggingEnabled() || !firstPartyEventLoggerProvider) { - return - } - - const newConfig = getBatchConfig() - - if (isEqual(newConfig, lastBatchConfig)) { - return - } - - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `1P event logging: ${BATCH_CONFIG_NAME} changed, reinitializing`, - ) - } - - const oldProvider = firstPartyEventLoggerProvider - const oldLogger = firstPartyEventLogger - firstPartyEventLogger = null - - try { - await oldProvider.forceFlush() - } catch { - // Export failures are already on disk; new exporter will retry them. - } - - firstPartyEventLoggerProvider = null - try { - initialize1PEventLogging() - } catch (e) { - // Restore so the next GrowthBook refresh can retry. oldProvider was - // only forceFlush()'d, not shut down — it's still functional. Without - // this, both stay null and the !firstPartyEventLoggerProvider gate at - // the top makes recovery impossible. - firstPartyEventLoggerProvider = oldProvider - firstPartyEventLogger = oldLogger - logError(e) - return - } - - void oldProvider.shutdown().catch(() => {}) -} diff --git a/src/services/analytics/firstPartyEventLoggingExporter.ts b/src/services/analytics/firstPartyEventLoggingExporter.ts deleted file mode 100644 index aefb22cb..00000000 --- a/src/services/analytics/firstPartyEventLoggingExporter.ts +++ /dev/null @@ -1,806 +0,0 @@ -import type { HrTime } from '@opentelemetry/api' -import { type ExportResult, ExportResultCode } from '@opentelemetry/core' -import type { - LogRecordExporter, - ReadableLogRecord, -} from '@opentelemetry/sdk-logs' -import axios from 'axios' -import { randomUUID } from 'crypto' -import { appendFile, mkdir, readdir, unlink, writeFile } from 'fs/promises' -import * as path from 'path' -import type { CoreUserData } from 'src/utils/user.js' -import { - getIsNonInteractiveSession, - getSessionId, -} from '../../bootstrap/state.js' -import { ClaudeCodeInternalEvent } from '../../types/generated/events_mono/claude_code/v1/claude_code_internal_event.js' -import { GrowthbookExperimentEvent } from '../../types/generated/events_mono/growthbook/v1/growthbook_experiment_event.js' -import { - getClaudeAIOAuthTokens, - hasProfileScope, - isClaudeAISubscriber, -} from '../../utils/auth.js' -import { checkHasTrustDialogAccepted } from '../../utils/config.js' -import { logForDebugging } from '../../utils/debug.js' -import { getClaudeConfigHomeDir } from '../../utils/envUtils.js' -import { errorMessage, isFsInaccessible, toError } from '../../utils/errors.js' -import { getAuthHeaders } from '../../utils/http.js' -import { readJSONLFile } from '../../utils/json.js' -import { logError } from '../../utils/log.js' -import { sleep } from '../../utils/sleep.js' -import { jsonStringify } from '../../utils/slowOperations.js' -import { getClaudeCodeUserAgent } from '../../utils/userAgent.js' -import { isOAuthTokenExpired } from '../oauth/client.js' -import { stripProtoFields } from './index.js' -import { type EventMetadata, to1PEventFormat } from './metadata.js' - -// Unique ID for this process run - used to isolate failed event files between runs -const BATCH_UUID = randomUUID() - -// File prefix for failed event storage -const FILE_PREFIX = '1p_failed_events.' - -// Storage directory for failed events - evaluated at runtime to respect CLAUDE_CONFIG_DIR in tests -function getStorageDir(): string { - return path.join(getClaudeConfigHomeDir(), 'telemetry') -} - -// API envelope - event_data is the JSON output from proto toJSON() -type FirstPartyEventLoggingEvent = { - event_type: 'ClaudeCodeInternalEvent' | 'GrowthbookExperimentEvent' - event_data: unknown -} - -type FirstPartyEventLoggingPayload = { - events: FirstPartyEventLoggingEvent[] -} - -/** - * Exporter for 1st-party event logging to /api/event_logging/batch. - * - * Export cycles are controlled by OpenTelemetry's BatchLogRecordProcessor, which - * triggers export() when either: - * - Time interval elapses (default: 5 seconds via scheduledDelayMillis) - * - Batch size is reached (default: 200 events via maxExportBatchSize) - * - * This exporter adds resilience on top: - * - Append-only log for failed events (concurrency-safe) - * - Quadratic backoff retry for failed events, dropped after maxAttempts - * - Immediate retry of queued events when any export succeeds (endpoint is healthy) - * - Chunking large event sets into smaller batches - * - Auth fallback: retries without auth on 401 errors - */ -export class FirstPartyEventLoggingExporter implements LogRecordExporter { - private readonly endpoint: string - private readonly timeout: number - private readonly maxBatchSize: number - private readonly skipAuth: boolean - private readonly batchDelayMs: number - private readonly baseBackoffDelayMs: number - private readonly maxBackoffDelayMs: number - private readonly maxAttempts: number - private readonly isKilled: () => boolean - private pendingExports: Promise[] = [] - private isShutdown = false - private readonly schedule: ( - fn: () => Promise, - delayMs: number, - ) => () => void - private cancelBackoff: (() => void) | null = null - private attempts = 0 - private isRetrying = false - private lastExportErrorContext: string | undefined - - constructor( - options: { - timeout?: number - maxBatchSize?: number - skipAuth?: boolean - batchDelayMs?: number - baseBackoffDelayMs?: number - maxBackoffDelayMs?: number - maxAttempts?: number - path?: string - baseUrl?: string - // Injected killswitch probe. Checked per-POST so that disabling the - // firstParty sink also stops backoff retries (not just new emits). - // Passed in rather than imported to avoid a cycle with firstPartyEventLogger.ts. - isKilled?: () => boolean - schedule?: (fn: () => Promise, delayMs: number) => () => void - } = {}, - ) { - // Default: prod, except when ANTHROPIC_BASE_URL is explicitly staging. - // Overridable via tengu_1p_event_batch_config.baseUrl. - const baseUrl = - options.baseUrl || - (process.env.ANTHROPIC_BASE_URL === 'https://api-staging.anthropic.com' - ? 'https://api-staging.anthropic.com' - : 'https://api.anthropic.com') - - this.endpoint = `${baseUrl}${options.path || '/api/event_logging/batch'}` - - this.timeout = options.timeout || 10000 - this.maxBatchSize = options.maxBatchSize || 200 - this.skipAuth = options.skipAuth ?? false - this.batchDelayMs = options.batchDelayMs || 100 - this.baseBackoffDelayMs = options.baseBackoffDelayMs || 500 - this.maxBackoffDelayMs = options.maxBackoffDelayMs || 30000 - this.maxAttempts = options.maxAttempts ?? 8 - this.isKilled = options.isKilled ?? (() => false) - this.schedule = - options.schedule ?? - ((fn, ms) => { - const t = setTimeout(fn, ms) - return () => clearTimeout(t) - }) - - // Retry any failed events from previous runs of this session (in background) - void this.retryPreviousBatches() - } - - // Expose for testing - async getQueuedEventCount(): Promise { - return (await this.loadEventsFromCurrentBatch()).length - } - - // --- Storage helpers --- - - private getCurrentBatchFilePath(): string { - return path.join( - getStorageDir(), - `${FILE_PREFIX}${getSessionId()}.${BATCH_UUID}.json`, - ) - } - - private async loadEventsFromFile( - filePath: string, - ): Promise { - try { - return await readJSONLFile(filePath) - } catch { - return [] - } - } - - private async loadEventsFromCurrentBatch(): Promise< - FirstPartyEventLoggingEvent[] - > { - return this.loadEventsFromFile(this.getCurrentBatchFilePath()) - } - - private async saveEventsToFile( - filePath: string, - events: FirstPartyEventLoggingEvent[], - ): Promise { - try { - if (events.length === 0) { - try { - await unlink(filePath) - } catch { - // File doesn't exist, nothing to delete - } - } else { - // Ensure storage directory exists - await mkdir(getStorageDir(), { recursive: true }) - // Write as JSON lines (one event per line) - const content = events.map(e => jsonStringify(e)).join('\n') + '\n' - await writeFile(filePath, content, 'utf8') - } - } catch (error) { - logError(error) - } - } - - private async appendEventsToFile( - filePath: string, - events: FirstPartyEventLoggingEvent[], - ): Promise { - if (events.length === 0) return - try { - // Ensure storage directory exists - await mkdir(getStorageDir(), { recursive: true }) - // Append as JSON lines (one event per line) - atomic on most filesystems - const content = events.map(e => jsonStringify(e)).join('\n') + '\n' - await appendFile(filePath, content, 'utf8') - } catch (error) { - logError(error) - } - } - - private async deleteFile(filePath: string): Promise { - try { - await unlink(filePath) - } catch { - // File doesn't exist or can't be deleted, ignore - } - } - - // --- Previous batch retry (startup) --- - - private async retryPreviousBatches(): Promise { - try { - const prefix = `${FILE_PREFIX}${getSessionId()}.` - let files: string[] - try { - files = (await readdir(getStorageDir())) - .filter((f: string) => f.startsWith(prefix) && f.endsWith('.json')) - .filter((f: string) => !f.includes(BATCH_UUID)) // Exclude current batch - } catch (e) { - if (isFsInaccessible(e)) return - throw e - } - - for (const file of files) { - const filePath = path.join(getStorageDir(), file) - void this.retryFileInBackground(filePath) - } - } catch (error) { - logError(error) - } - } - - private async retryFileInBackground(filePath: string): Promise { - if (this.attempts >= this.maxAttempts) { - await this.deleteFile(filePath) - return - } - - const events = await this.loadEventsFromFile(filePath) - if (events.length === 0) { - await this.deleteFile(filePath) - return - } - - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `1P event logging: retrying ${events.length} events from previous batch`, - ) - } - - const failedEvents = await this.sendEventsInBatches(events) - if (failedEvents.length === 0) { - await this.deleteFile(filePath) - if (process.env.USER_TYPE === 'ant') { - logForDebugging('1P event logging: previous batch retry succeeded') - } - } else { - // Save only the failed events back (not all original events) - await this.saveEventsToFile(filePath, failedEvents) - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `1P event logging: previous batch retry failed, ${failedEvents.length} events remain`, - ) - } - } - } - - async export( - logs: ReadableLogRecord[], - resultCallback: (result: ExportResult) => void, - ): Promise { - if (this.isShutdown) { - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - '1P event logging export failed: Exporter has been shutdown', - ) - } - resultCallback({ - code: ExportResultCode.FAILED, - error: new Error('Exporter has been shutdown'), - }) - return - } - - const exportPromise = this.doExport(logs, resultCallback) - this.pendingExports.push(exportPromise) - - // Clean up completed exports - void exportPromise.finally(() => { - const index = this.pendingExports.indexOf(exportPromise) - if (index > -1) { - void this.pendingExports.splice(index, 1) - } - }) - } - - private async doExport( - logs: ReadableLogRecord[], - resultCallback: (result: ExportResult) => void, - ): Promise { - try { - // Filter for event logs only (by scope name) - const eventLogs = logs.filter( - log => - log.instrumentationScope?.name === 'com.anthropic.claude_code.events', - ) - - if (eventLogs.length === 0) { - resultCallback({ code: ExportResultCode.SUCCESS }) - return - } - - // Transform new logs (failed events are retried independently via backoff) - const events = this.transformLogsToEvents(eventLogs).events - - if (events.length === 0) { - resultCallback({ code: ExportResultCode.SUCCESS }) - return - } - - if (this.attempts >= this.maxAttempts) { - resultCallback({ - code: ExportResultCode.FAILED, - error: new Error( - `Dropped ${events.length} events: max attempts (${this.maxAttempts}) reached`, - ), - }) - return - } - - // Send events - const failedEvents = await this.sendEventsInBatches(events) - this.attempts++ - - if (failedEvents.length > 0) { - await this.queueFailedEvents(failedEvents) - this.scheduleBackoffRetry() - const context = this.lastExportErrorContext - ? ` (${this.lastExportErrorContext})` - : '' - resultCallback({ - code: ExportResultCode.FAILED, - error: new Error( - `Failed to export ${failedEvents.length} events${context}`, - ), - }) - return - } - - // Success - reset backoff and immediately retry any queued events - this.resetBackoff() - if ((await this.getQueuedEventCount()) > 0 && !this.isRetrying) { - void this.retryFailedEvents() - } - resultCallback({ code: ExportResultCode.SUCCESS }) - } catch (error) { - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `1P event logging export failed: ${errorMessage(error)}`, - ) - } - logError(error) - resultCallback({ - code: ExportResultCode.FAILED, - error: toError(error), - }) - } - } - - private async sendEventsInBatches( - events: FirstPartyEventLoggingEvent[], - ): Promise { - // Chunk events into batches - const batches: FirstPartyEventLoggingEvent[][] = [] - for (let i = 0; i < events.length; i += this.maxBatchSize) { - batches.push(events.slice(i, i + this.maxBatchSize)) - } - - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `1P event logging: exporting ${events.length} events in ${batches.length} batch(es)`, - ) - } - - // Send each batch with delay between them. On first failure, assume the - // endpoint is down and short-circuit: queue the failed batch plus all - // remaining unsent batches without POSTing them. The backoff retry will - // probe again with a single batch next tick. - const failedBatchEvents: FirstPartyEventLoggingEvent[] = [] - let lastErrorContext: string | undefined - for (let i = 0; i < batches.length; i++) { - const batch = batches[i]! - try { - await this.sendBatchWithRetry({ events: batch }) - } catch (error) { - lastErrorContext = getAxiosErrorContext(error) - for (let j = i; j < batches.length; j++) { - failedBatchEvents.push(...batches[j]!) - } - if (process.env.USER_TYPE === 'ant') { - const skipped = batches.length - 1 - i - logForDebugging( - `1P event logging: batch ${i + 1}/${batches.length} failed (${lastErrorContext}); short-circuiting ${skipped} remaining batch(es)`, - ) - } - break - } - - if (i < batches.length - 1 && this.batchDelayMs > 0) { - await sleep(this.batchDelayMs) - } - } - - if (failedBatchEvents.length > 0 && lastErrorContext) { - this.lastExportErrorContext = lastErrorContext - } - - return failedBatchEvents - } - - private async queueFailedEvents( - events: FirstPartyEventLoggingEvent[], - ): Promise { - const filePath = this.getCurrentBatchFilePath() - - // Append-only: just add new events to file (atomic on most filesystems) - await this.appendEventsToFile(filePath, events) - - const context = this.lastExportErrorContext - ? ` (${this.lastExportErrorContext})` - : '' - const message = `1P event logging: ${events.length} events failed to export${context}` - logError(new Error(message)) - } - - private scheduleBackoffRetry(): void { - // Don't schedule if already retrying or shutdown - if (this.cancelBackoff || this.isRetrying || this.isShutdown) { - return - } - - // Quadratic backoff (matching Statsig SDK): base * attempts² - const delay = Math.min( - this.baseBackoffDelayMs * this.attempts * this.attempts, - this.maxBackoffDelayMs, - ) - - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `1P event logging: scheduling backoff retry in ${delay}ms (attempt ${this.attempts})`, - ) - } - - this.cancelBackoff = this.schedule(async () => { - this.cancelBackoff = null - await this.retryFailedEvents() - }, delay) - } - - private async retryFailedEvents(): Promise { - const filePath = this.getCurrentBatchFilePath() - - // Keep retrying while there are events and endpoint is healthy - while (!this.isShutdown) { - const events = await this.loadEventsFromFile(filePath) - if (events.length === 0) break - - if (this.attempts >= this.maxAttempts) { - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `1P event logging: max attempts (${this.maxAttempts}) reached, dropping ${events.length} events`, - ) - } - await this.deleteFile(filePath) - this.resetBackoff() - return - } - - this.isRetrying = true - - // Clear file before retry (we have events in memory now) - await this.deleteFile(filePath) - - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `1P event logging: retrying ${events.length} failed events (attempt ${this.attempts + 1})`, - ) - } - - const failedEvents = await this.sendEventsInBatches(events) - this.attempts++ - - this.isRetrying = false - - if (failedEvents.length > 0) { - // Write failures back to disk - await this.saveEventsToFile(filePath, failedEvents) - this.scheduleBackoffRetry() - return // Failed - wait for backoff - } - - // Success - reset backoff and continue loop to drain any newly queued events - this.resetBackoff() - if (process.env.USER_TYPE === 'ant') { - logForDebugging('1P event logging: backoff retry succeeded') - } - } - } - - private resetBackoff(): void { - this.attempts = 0 - if (this.cancelBackoff) { - this.cancelBackoff() - this.cancelBackoff = null - } - } - - private async sendBatchWithRetry( - payload: FirstPartyEventLoggingPayload, - ): Promise { - if (this.isKilled()) { - // Throw so the caller short-circuits remaining batches and queues - // everything to disk. Zero network traffic while killed; the backoff - // timer keeps ticking and will resume POSTs as soon as the GrowthBook - // cache picks up the cleared flag. - throw new Error('firstParty sink killswitch active') - } - - const baseHeaders: Record = { - 'Content-Type': 'application/json', - 'User-Agent': getClaudeCodeUserAgent(), - 'x-service-name': 'claude-code', - } - - // Skip auth if trust hasn't been established yet - // This prevents executing apiKeyHelper commands before the trust dialog - // Non-interactive sessions implicitly have workspace trust - const hasTrust = - checkHasTrustDialogAccepted() || getIsNonInteractiveSession() - if (process.env.USER_TYPE === 'ant' && !hasTrust) { - logForDebugging('1P event logging: Trust not accepted') - } - - // Skip auth when the OAuth token is expired or lacks user:profile - // scope (service key sessions). Falls through to unauthenticated send. - let shouldSkipAuth = this.skipAuth || !hasTrust - if (!shouldSkipAuth && isClaudeAISubscriber()) { - const tokens = getClaudeAIOAuthTokens() - if (!hasProfileScope()) { - shouldSkipAuth = true - } else if (tokens && isOAuthTokenExpired(tokens.expiresAt)) { - shouldSkipAuth = true - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - '1P event logging: OAuth token expired, skipping auth to avoid 401', - ) - } - } - } - - // Try with auth headers first (unless trust not established or token is known to be expired) - const authResult = shouldSkipAuth - ? { headers: {}, error: 'trust not established or Oauth token expired' } - : getAuthHeaders() - const useAuth = !authResult.error - - if (!useAuth && process.env.USER_TYPE === 'ant') { - logForDebugging( - `1P event logging: auth not available, sending without auth`, - ) - } - - const headers = useAuth - ? { ...baseHeaders, ...authResult.headers } - : baseHeaders - - try { - const response = await axios.post(this.endpoint, payload, { - timeout: this.timeout, - headers, - }) - this.logSuccess(payload.events.length, useAuth, response.data) - return - } catch (error) { - // Handle 401 by retrying without auth - if ( - useAuth && - axios.isAxiosError(error) && - error.response?.status === 401 - ) { - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - '1P event logging: 401 auth error, retrying without auth', - ) - } - const response = await axios.post(this.endpoint, payload, { - timeout: this.timeout, - headers: baseHeaders, - }) - this.logSuccess(payload.events.length, false, response.data) - return - } - - throw error - } - } - - private logSuccess( - eventCount: number, - withAuth: boolean, - responseData: unknown, - ): void { - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `1P event logging: ${eventCount} events exported successfully${withAuth ? ' (with auth)' : ' (without auth)'}`, - ) - logForDebugging(`API Response: ${jsonStringify(responseData, null, 2)}`) - } - } - - private hrTimeToDate(hrTime: HrTime): Date { - const [seconds, nanoseconds] = hrTime - return new Date(seconds * 1000 + nanoseconds / 1000000) - } - - private transformLogsToEvents( - logs: ReadableLogRecord[], - ): FirstPartyEventLoggingPayload { - const events: FirstPartyEventLoggingEvent[] = [] - - for (const log of logs) { - const attributes = log.attributes || {} - - // Check if this is a GrowthBook experiment event - if (attributes.event_type === 'GrowthbookExperimentEvent') { - const timestamp = this.hrTimeToDate(log.hrTime) - const account_uuid = attributes.account_uuid as string | undefined - const organization_uuid = attributes.organization_uuid as - | string - | undefined - events.push({ - event_type: 'GrowthbookExperimentEvent', - event_data: GrowthbookExperimentEvent.toJSON({ - event_id: attributes.event_id as string, - timestamp, - experiment_id: attributes.experiment_id as string, - variation_id: attributes.variation_id as number, - environment: attributes.environment as string, - user_attributes: attributes.user_attributes as string, - experiment_metadata: attributes.experiment_metadata as string, - device_id: attributes.device_id as string, - session_id: attributes.session_id as string, - auth: - account_uuid || organization_uuid - ? { account_uuid, organization_uuid } - : undefined, - }), - }) - continue - } - - // Extract event name - const eventName = - (attributes.event_name as string) || (log.body as string) || 'unknown' - - // Extract metadata objects directly (no JSON parsing needed) - const coreMetadata = attributes.core_metadata as EventMetadata | undefined - const userMetadata = attributes.user_metadata as CoreUserData - const eventMetadata = (attributes.event_metadata || {}) as Record< - string, - unknown - > - - if (!coreMetadata) { - // Emit partial event if core metadata is missing - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `1P event logging: core_metadata missing for event ${eventName}`, - ) - } - events.push({ - event_type: 'ClaudeCodeInternalEvent', - event_data: ClaudeCodeInternalEvent.toJSON({ - event_id: attributes.event_id as string | undefined, - event_name: eventName, - client_timestamp: this.hrTimeToDate(log.hrTime), - session_id: getSessionId(), - additional_metadata: Buffer.from( - jsonStringify({ - transform_error: 'core_metadata attribute is missing', - }), - ).toString('base64'), - }), - }) - continue - } - - // Transform to 1P format - const formatted = to1PEventFormat( - coreMetadata, - userMetadata, - eventMetadata, - ) - - // _PROTO_* keys are PII-tagged values meant only for privileged BQ - // columns. Hoist known keys to proto fields, then defensively strip any - // remaining _PROTO_* so an unrecognized future key can't silently land - // in the general-access additional_metadata blob. sink.ts applies the - // same strip before Datadog; this closes the 1P side. - const { - _PROTO_skill_name, - _PROTO_plugin_name, - _PROTO_marketplace_name, - ...rest - } = formatted.additional - const additionalMetadata = stripProtoFields(rest) - - events.push({ - event_type: 'ClaudeCodeInternalEvent', - event_data: ClaudeCodeInternalEvent.toJSON({ - event_id: attributes.event_id as string | undefined, - event_name: eventName, - client_timestamp: this.hrTimeToDate(log.hrTime), - device_id: attributes.user_id as string | undefined, - email: userMetadata?.email, - auth: formatted.auth, - ...formatted.core, - env: formatted.env, - process: formatted.process, - skill_name: - typeof _PROTO_skill_name === 'string' - ? _PROTO_skill_name - : undefined, - plugin_name: - typeof _PROTO_plugin_name === 'string' - ? _PROTO_plugin_name - : undefined, - marketplace_name: - typeof _PROTO_marketplace_name === 'string' - ? _PROTO_marketplace_name - : undefined, - additional_metadata: - Object.keys(additionalMetadata).length > 0 - ? Buffer.from(jsonStringify(additionalMetadata)).toString( - 'base64', - ) - : undefined, - }), - }) - } - - return { events } - } - - async shutdown(): Promise { - this.isShutdown = true - this.resetBackoff() - await this.forceFlush() - if (process.env.USER_TYPE === 'ant') { - logForDebugging('1P event logging exporter shutdown complete') - } - } - - async forceFlush(): Promise { - await Promise.all(this.pendingExports) - if (process.env.USER_TYPE === 'ant') { - logForDebugging('1P event logging exporter flush complete') - } - } -} - -function getAxiosErrorContext(error: unknown): string { - if (!axios.isAxiosError(error)) { - return errorMessage(error) - } - - const parts: string[] = [] - - const requestId = error.response?.headers?.['request-id'] - if (requestId) { - parts.push(`request-id=${requestId}`) - } - - if (error.response?.status) { - parts.push(`status=${error.response.status}`) - } - - if (error.code) { - parts.push(`code=${error.code}`) - } - - if (error.message) { - parts.push(error.message) - } - - return parts.join(', ') -} diff --git a/src/services/analytics/growthbook.ts b/src/services/analytics/growthbook.ts deleted file mode 100644 index c71bba84..00000000 --- a/src/services/analytics/growthbook.ts +++ /dev/null @@ -1,1155 +0,0 @@ -import { GrowthBook } from '@growthbook/growthbook' -import { isEqual, memoize } from 'lodash-es' -import { - getIsNonInteractiveSession, - getSessionTrustAccepted, -} from '../../bootstrap/state.js' -import { getGrowthBookClientKey } from '../../constants/keys.js' -import { - checkHasTrustDialogAccepted, - getGlobalConfig, - saveGlobalConfig, -} from '../../utils/config.js' -import { logForDebugging } from '../../utils/debug.js' -import { toError } from '../../utils/errors.js' -import { getAuthHeaders } from '../../utils/http.js' -import { logError } from '../../utils/log.js' -import { createSignal } from '../../utils/signal.js' -import { jsonStringify } from '../../utils/slowOperations.js' -import { - type GitHubActionsMetadata, - getUserForGrowthBook, -} from '../../utils/user.js' -import { - is1PEventLoggingEnabled, - logGrowthBookExperimentTo1P, -} from './firstPartyEventLogger.js' - -/** - * User attributes sent to GrowthBook for targeting. - * Uses UUID suffix (not Uuid) to align with GrowthBook conventions. - */ -export type GrowthBookUserAttributes = { - id: string - sessionId: string - deviceID: string - platform: 'win32' | 'darwin' | 'linux' - apiBaseUrlHost?: string - organizationUUID?: string - accountUUID?: string - userType?: string - subscriptionType?: string - rateLimitTier?: string - firstTokenTime?: number - email?: string - appVersion?: string - github?: GitHubActionsMetadata -} - -/** - * Malformed feature response from API that uses "value" instead of "defaultValue". - * This is a workaround until the API is fixed. - */ -type MalformedFeatureDefinition = { - value?: unknown - defaultValue?: unknown - [key: string]: unknown -} - -let client: GrowthBook | null = null - -// Named handler refs so resetGrowthBook can remove them to prevent accumulation -let currentBeforeExitHandler: (() => void) | null = null -let currentExitHandler: (() => void) | null = null - -// Track whether auth was available when the client was created -// This allows us to detect when we need to recreate with fresh auth headers -let clientCreatedWithAuth = false - -// Store experiment data from payload for logging exposures later -type StoredExperimentData = { - experimentId: string - variationId: number - inExperiment?: boolean - hashAttribute?: string - hashValue?: string -} -const experimentDataByFeature = new Map() - -// Cache for remote eval feature values - workaround for SDK not respecting remoteEval response -// The SDK's setForcedFeatures also doesn't work reliably with remoteEval -const remoteEvalFeatureValues = new Map() - -// Track features accessed before init that need exposure logging -const pendingExposures = new Set() - -// Track features that have already had their exposure logged this session (dedup) -// This prevents firing duplicate exposure events when getFeatureValue_CACHED_MAY_BE_STALE -// is called repeatedly in hot paths (e.g., isAutoMemoryEnabled in render loops) -const loggedExposures = new Set() - -// Track re-initialization promise for security gate checks -// When GrowthBook is re-initializing (e.g., after auth change), security gate checks -// should wait for init to complete to avoid returning stale values -let reinitializingPromise: Promise | null = null - -// Listeners notified when GrowthBook feature values refresh (initial init or -// periodic refresh). Use for systems that bake feature values into long-lived -// objects at construction time (e.g. firstPartyEventLogger reads -// tengu_1p_event_batch_config once and builds a LoggerProvider with it) and -// need to rebuild when config changes. Per-call readers like -// getEventSamplingConfig / isSinkKilled don't need this — they're already -// reactive. -// -// NOT cleared by resetGrowthBook — subscribers register once (typically in -// init.ts) and must survive auth-change resets. -type GrowthBookRefreshListener = () => void | Promise -const refreshed = createSignal() - -/** Call a listener with sync-throw and async-rejection both routed to logError. */ -function callSafe(listener: GrowthBookRefreshListener): void { - try { - // Promise.resolve() normalizes sync returns and Promises so both - // sync throws (caught by outer try) and async rejections (caught - // by .catch) hit logError. Without the .catch, an async listener - // that rejects becomes an unhandled rejection — the try/catch - // only sees the Promise, not its eventual rejection. - void Promise.resolve(listener()).catch(e => { - logError(e) - }) - } catch (e) { - logError(e) - } -} - -/** - * Register a callback to fire when GrowthBook feature values refresh. - * Returns an unsubscribe function. - * - * If init has already completed with features by the time this is called - * (remoteEvalFeatureValues is populated), the listener fires once on the - * next microtask. This catch-up handles the race where GB's network response - * lands before the REPL's useEffect commits — on external builds with fast - * networks and MCP-heavy configs, init can finish in ~100ms while REPL mount - * takes ~600ms (see #20951 external-build trace at 30.540 vs 31.046). - * - * Change detection is on the subscriber: the callback fires on every refresh; - * use isEqual against your last-seen config to decide whether to act. - */ -export function onGrowthBookRefresh( - listener: GrowthBookRefreshListener, -): () => void { - let subscribed = true - const unsubscribe = refreshed.subscribe(() => callSafe(listener)) - if (remoteEvalFeatureValues.size > 0) { - queueMicrotask(() => { - // Re-check: listener may have been removed, or resetGrowthBook may have - // cleared the Map, between registration and this microtask running. - if (subscribed && remoteEvalFeatureValues.size > 0) { - callSafe(listener) - } - }) - } - return () => { - subscribed = false - unsubscribe() - } -} - -/** - * Parse env var overrides for GrowthBook features. - * Set CLAUDE_INTERNAL_FC_OVERRIDES to a JSON object mapping feature keys to values - * to bypass remote eval and disk cache. Useful for eval harnesses that need to - * test specific feature flag configurations. Only active when USER_TYPE is 'ant'. - * - * Example: CLAUDE_INTERNAL_FC_OVERRIDES='{"my_feature": true, "my_config": {"key": "val"}}' - */ -let envOverrides: Record | null = null -let envOverridesParsed = false - -function getEnvOverrides(): Record | null { - if (!envOverridesParsed) { - envOverridesParsed = true - if (process.env.USER_TYPE === 'ant') { - const raw = process.env.CLAUDE_INTERNAL_FC_OVERRIDES - if (raw) { - try { - envOverrides = JSON.parse(raw) as Record - logForDebugging( - `GrowthBook: Using env var overrides for ${Object.keys(envOverrides!).length} features: ${Object.keys(envOverrides!).join(', ')}`, - ) - } catch { - logError( - new Error( - `GrowthBook: Failed to parse CLAUDE_INTERNAL_FC_OVERRIDES: ${raw}`, - ), - ) - } - } - } - } - return envOverrides -} - -/** - * Check if a feature has an env-var override (CLAUDE_INTERNAL_FC_OVERRIDES). - * When true, _CACHED_MAY_BE_STALE will return the override without touching - * disk or network — callers can skip awaiting init for that feature. - */ -export function hasGrowthBookEnvOverride(feature: string): boolean { - const overrides = getEnvOverrides() - return overrides !== null && feature in overrides -} - -/** - * Local config overrides set via /config Gates tab (ant-only). Checked after - * env-var overrides — env wins so eval harnesses remain deterministic. Unlike - * getEnvOverrides this is not memoized: the user can change overrides at - * runtime, and getGlobalConfig() is already memory-cached (pointer-chase) - * until the next saveGlobalConfig() invalidates it. - */ -function getConfigOverrides(): Record | undefined { - if (process.env.USER_TYPE !== 'ant') return undefined - try { - return getGlobalConfig().growthBookOverrides - } catch { - // getGlobalConfig() throws before configReadingAllowed is set (early - // main.tsx startup path). Same degrade as the disk-cache fallback below. - return undefined - } -} - -/** - * Enumerate all known GrowthBook features and their current resolved values - * (not including overrides). In-memory payload first, disk cache fallback — - * same priority as the getters. Used by the /config Gates tab. - */ -export function getAllGrowthBookFeatures(): Record { - if (remoteEvalFeatureValues.size > 0) { - return Object.fromEntries(remoteEvalFeatureValues) - } - return getGlobalConfig().cachedGrowthBookFeatures ?? {} -} - -export function getGrowthBookConfigOverrides(): Record { - return getConfigOverrides() ?? {} -} - -/** - * Set or clear a single config override. Pass undefined to clear. - * Fires onGrowthBookRefresh listeners so systems that bake gate values into - * long-lived objects (useMainLoopModel, useSkillsChange, etc.) rebuild — - * otherwise overriding e.g. tengu_ant_model_override wouldn't actually - * change the model until the next periodic refresh. - */ -export function setGrowthBookConfigOverride( - feature: string, - value: unknown, -): void { - if (process.env.USER_TYPE !== 'ant') return - try { - saveGlobalConfig(c => { - const current = c.growthBookOverrides ?? {} - if (value === undefined) { - if (!(feature in current)) return c - const { [feature]: _, ...rest } = current - if (Object.keys(rest).length === 0) { - const { growthBookOverrides: __, ...configWithout } = c - return configWithout - } - return { ...c, growthBookOverrides: rest } - } - if (isEqual(current[feature], value)) return c - return { ...c, growthBookOverrides: { ...current, [feature]: value } } - }) - // Subscribers do their own change detection (see onGrowthBookRefresh docs), - // so firing on a no-op write is fine. - refreshed.emit() - } catch (e) { - logError(e) - } -} - -export function clearGrowthBookConfigOverrides(): void { - if (process.env.USER_TYPE !== 'ant') return - try { - saveGlobalConfig(c => { - if ( - !c.growthBookOverrides || - Object.keys(c.growthBookOverrides).length === 0 - ) { - return c - } - const { growthBookOverrides: _, ...rest } = c - return rest - }) - refreshed.emit() - } catch (e) { - logError(e) - } -} - -/** - * Log experiment exposure for a feature if it has experiment data. - * Deduplicates within a session - each feature is logged at most once. - */ -function logExposureForFeature(feature: string): void { - // Skip if already logged this session (dedup) - if (loggedExposures.has(feature)) { - return - } - - const expData = experimentDataByFeature.get(feature) - if (expData) { - loggedExposures.add(feature) - logGrowthBookExperimentTo1P({ - experimentId: expData.experimentId, - variationId: expData.variationId, - userAttributes: getUserAttributes(), - experimentMetadata: { - feature_id: feature, - }, - }) - } -} - -/** - * Process a remote eval payload from the GrowthBook server and populate - * local caches. Called after both initial client.init() and after - * client.refreshFeatures() so that _BLOCKS_ON_INIT callers see fresh values - * across the process lifetime, not just init-time snapshots. - * - * Without this running on refresh, remoteEvalFeatureValues freezes at its - * init-time snapshot and getDynamicConfig_BLOCKS_ON_INIT returns stale values - * for the entire process lifetime — which broke the tengu_max_version_config - * kill switch for long-running sessions. - */ -async function processRemoteEvalPayload( - gbClient: GrowthBook, -): Promise { - // WORKAROUND: Transform remote eval response format - // The API returns { "value": ... } but SDK expects { "defaultValue": ... } - // TODO: Remove this once the API is fixed to return correct format - const payload = gbClient.getPayload() - // Empty object is truthy — without the length check, `{features: {}}` - // (transient server bug, truncated response) would pass, clear the maps - // below, return true, and syncRemoteEvalToDisk would wholesale-write `{}` - // to disk: total flag blackout for every process sharing ~/.claude.json. - if (!payload?.features || Object.keys(payload.features).length === 0) { - return false - } - - // Clear before rebuild so features removed between refreshes don't - // leave stale ghost entries that short-circuit getFeatureValueInternal. - experimentDataByFeature.clear() - - const transformedFeatures: Record = {} - for (const [key, feature] of Object.entries(payload.features)) { - const f = feature as MalformedFeatureDefinition - if ('value' in f && !('defaultValue' in f)) { - transformedFeatures[key] = { - ...f, - defaultValue: f.value, - } - } else { - transformedFeatures[key] = f - } - - // Store experiment data for later logging when feature is accessed - if (f.source === 'experiment' && f.experimentResult) { - const expResult = f.experimentResult as { - variationId?: number - } - const exp = f.experiment as { key?: string } | undefined - if (exp?.key && expResult.variationId !== undefined) { - experimentDataByFeature.set(key, { - experimentId: exp.key, - variationId: expResult.variationId, - }) - } - } - } - // Re-set the payload with transformed features - await gbClient.setPayload({ - ...payload, - features: transformedFeatures, - }) - - // WORKAROUND: Cache the evaluated values directly from remote eval response. - // The SDK's evalFeature() tries to re-evaluate rules locally, ignoring the - // pre-evaluated 'value' from remoteEval. setForcedFeatures also doesn't work - // reliably. So we cache values ourselves and use them in getFeatureValueInternal. - remoteEvalFeatureValues.clear() - for (const [key, feature] of Object.entries(transformedFeatures)) { - // Under remoteEval:true the server pre-evaluates. Whether the answer - // lands in `value` (current API) or `defaultValue` (post-TODO API shape), - // it's the authoritative value for this user. Guarding on both keeps - // syncRemoteEvalToDisk correct across a partial or full API migration. - const v = 'value' in feature ? feature.value : feature.defaultValue - if (v !== undefined) { - remoteEvalFeatureValues.set(key, v) - } - } - return true -} - -/** - * Write the complete remoteEvalFeatureValues map to disk. Called exactly - * once per successful processRemoteEvalPayload — never from a failure path, - * so init-timeout poisoning is structurally impossible (the .catch() at init - * never reaches here). - * - * Wholesale replace (not merge): features deleted server-side are dropped - * from disk on the next successful payload. Ant builds ⊇ external, so - * switching builds is safe — the write is always a complete answer for this - * process's SDK key. - */ -function syncRemoteEvalToDisk(): void { - const fresh = Object.fromEntries(remoteEvalFeatureValues) - const config = getGlobalConfig() - if (isEqual(config.cachedGrowthBookFeatures, fresh)) { - return - } - saveGlobalConfig(current => ({ - ...current, - cachedGrowthBookFeatures: fresh, - })) -} - -/** - * Check if GrowthBook operations should be enabled - */ -function isGrowthBookEnabled(): boolean { - // GrowthBook depends on 1P event logging. - return is1PEventLoggingEnabled() -} - -/** - * Hostname of ANTHROPIC_BASE_URL when it points at a non-Anthropic proxy. - * - * Enterprise-proxy deployments (Epic, Marble, etc.) typically use - * apiKeyHelper auth, which means isAnthropicAuthEnabled() returns false and - * organizationUUID/accountUUID/email are all absent from GrowthBook - * attributes. Without this, there's no stable attribute to target them on - * — only per-device IDs. See src/utils/auth.ts isAnthropicAuthEnabled(). - * - * Returns undefined for unset/default (api.anthropic.com) so the attribute - * is absent for direct-API users. Hostname only — no path/query/creds. - */ -export function getApiBaseUrlHost(): string | undefined { - const baseUrl = process.env.ANTHROPIC_BASE_URL - if (!baseUrl) return undefined - try { - const host = new URL(baseUrl).host - if (host === 'api.anthropic.com') return undefined - return host - } catch { - return undefined - } -} - -/** - * Get user attributes for GrowthBook from CoreUserData - */ -function getUserAttributes(): GrowthBookUserAttributes { - const user = getUserForGrowthBook() - - // For ants, always try to include email from OAuth config even if ANTHROPIC_API_KEY is set. - // This ensures GrowthBook targeting by email works regardless of auth method. - let email = user.email - if (!email && process.env.USER_TYPE === 'ant') { - email = getGlobalConfig().oauthAccount?.emailAddress - } - - const apiBaseUrlHost = getApiBaseUrlHost() - - const attributes = { - id: user.deviceId, - sessionId: user.sessionId, - deviceID: user.deviceId, - platform: user.platform, - ...(apiBaseUrlHost && { apiBaseUrlHost }), - ...(user.organizationUuid && { organizationUUID: user.organizationUuid }), - ...(user.accountUuid && { accountUUID: user.accountUuid }), - ...(user.userType && { userType: user.userType }), - ...(user.subscriptionType && { subscriptionType: user.subscriptionType }), - ...(user.rateLimitTier && { rateLimitTier: user.rateLimitTier }), - ...(user.firstTokenTime && { firstTokenTime: user.firstTokenTime }), - ...(email && { email }), - ...(user.appVersion && { appVersion: user.appVersion }), - ...(user.githubActionsMetadata && { - githubActionsMetadata: user.githubActionsMetadata, - }), - } - return attributes -} - -/** - * Get or create the GrowthBook client instance - */ -const getGrowthBookClient = memoize( - (): { client: GrowthBook; initialized: Promise } | null => { - if (!isGrowthBookEnabled()) { - return null - } - - const attributes = getUserAttributes() - const clientKey = getGrowthBookClientKey() - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `GrowthBook: Creating client with clientKey=${clientKey}, attributes: ${jsonStringify(attributes)}`, - ) - } - const baseUrl = - process.env.USER_TYPE === 'ant' - ? process.env.CLAUDE_CODE_GB_BASE_URL || 'https://api.anthropic.com/' - : 'https://api.anthropic.com/' - - // Skip auth if trust hasn't been established yet - // This prevents executing apiKeyHelper commands before the trust dialog - // Non-interactive sessions implicitly have workspace trust - // getSessionTrustAccepted() covers the case where the TrustDialog auto-resolved - // without persisting trust for the specific CWD (e.g., home directory) — - // showSetupScreens() sets this after the trust dialog flow completes. - const hasTrust = - checkHasTrustDialogAccepted() || - getSessionTrustAccepted() || - getIsNonInteractiveSession() - const authHeaders = hasTrust - ? getAuthHeaders() - : { headers: {}, error: 'trust not established' } - const hasAuth = !authHeaders.error - clientCreatedWithAuth = hasAuth - - // Capture in local variable so the init callback operates on THIS client, - // not a later client if reinitialization happens before init completes - const thisClient = new GrowthBook({ - apiHost: baseUrl, - clientKey, - attributes, - remoteEval: true, - // Re-fetch when user ID or org changes (org change = login to different org) - cacheKeyAttributes: ['id', 'organizationUUID'], - // Add auth headers if available - ...(authHeaders.error - ? {} - : { apiHostRequestHeaders: authHeaders.headers }), - // Debug logging for Ants - ...(process.env.USER_TYPE === 'ant' - ? { - log: (msg: string, ctx: Record) => { - logForDebugging(`GrowthBook: ${msg} ${jsonStringify(ctx)}`) - }, - } - : {}), - }) - client = thisClient - - if (!hasAuth) { - // No auth available yet — skip HTTP init, rely on disk-cached values. - // initializeGrowthBook() will reset and re-create with auth when available. - return { client: thisClient, initialized: Promise.resolve() } - } - - const initialized = thisClient - .init({ timeout: 5000 }) - .then(async result => { - // Guard: if this client was replaced by a newer one, skip processing - if (client !== thisClient) { - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - 'GrowthBook: Skipping init callback for replaced client', - ) - } - return - } - - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `GrowthBook initialized successfully, source: ${result.source}, success: ${result.success}`, - ) - } - - const hadFeatures = await processRemoteEvalPayload(thisClient) - // Re-check: processRemoteEvalPayload yields at `await setPayload`. - // Microtask-only today (no encryption, no sticky-bucket service), but - // the guard at the top of this callback runs before that await; - // this runs after. - if (client !== thisClient) return - - if (hadFeatures) { - for (const feature of pendingExposures) { - logExposureForFeature(feature) - } - pendingExposures.clear() - syncRemoteEvalToDisk() - // Notify subscribers: remoteEvalFeatureValues is populated and - // disk is freshly synced. _CACHED_MAY_BE_STALE reads memory first - // (#22295), so subscribers see fresh values immediately. - refreshed.emit() - } - - // Log what features were loaded - if (process.env.USER_TYPE === 'ant') { - const features = thisClient.getFeatures() - if (features) { - const featureKeys = Object.keys(features) - logForDebugging( - `GrowthBook loaded ${featureKeys.length} features: ${featureKeys.slice(0, 10).join(', ')}${featureKeys.length > 10 ? '...' : ''}`, - ) - } - } - }) - .catch(error => { - if (process.env.USER_TYPE === 'ant') { - logError(toError(error)) - } - }) - - // Register cleanup handlers for graceful shutdown (named refs so resetGrowthBook can remove them) - currentBeforeExitHandler = () => client?.destroy() - currentExitHandler = () => client?.destroy() - process.on('beforeExit', currentBeforeExitHandler) - process.on('exit', currentExitHandler) - - return { client: thisClient, initialized } - }, -) - -/** - * Initialize GrowthBook client (blocks until ready) - */ -export const initializeGrowthBook = memoize( - async (): Promise => { - let clientWrapper = getGrowthBookClient() - if (!clientWrapper) { - return null - } - - // Check if auth has become available since the client was created - // If so, we need to recreate the client with fresh auth headers - // Only check if trust is established to avoid triggering apiKeyHelper before trust dialog - if (!clientCreatedWithAuth) { - const hasTrust = - checkHasTrustDialogAccepted() || - getSessionTrustAccepted() || - getIsNonInteractiveSession() - if (hasTrust) { - const currentAuth = getAuthHeaders() - if (!currentAuth.error) { - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - 'GrowthBook: Auth became available after client creation, reinitializing', - ) - } - // Use resetGrowthBook to properly destroy old client and stop periodic refresh - // This prevents double-init where old client's init promise continues running - resetGrowthBook() - clientWrapper = getGrowthBookClient() - if (!clientWrapper) { - return null - } - } - } - } - - await clientWrapper.initialized - - // Set up periodic refresh after successful initialization - // This is called here (not separately) so it's always re-established after any reinit - setupPeriodicGrowthBookRefresh() - - return clientWrapper.client - }, -) - -/** - * Get a feature value with a default fallback - blocks until initialized. - * @internal Used by both deprecated and cached functions. - */ -async function getFeatureValueInternal( - feature: string, - defaultValue: T, - logExposure: boolean, -): Promise { - // Check env var overrides first (for eval harnesses) - const overrides = getEnvOverrides() - if (overrides && feature in overrides) { - return overrides[feature] as T - } - const configOverrides = getConfigOverrides() - if (configOverrides && feature in configOverrides) { - return configOverrides[feature] as T - } - - if (!isGrowthBookEnabled()) { - return defaultValue - } - - const growthBookClient = await initializeGrowthBook() - if (!growthBookClient) { - return defaultValue - } - - // Use cached remote eval values if available (workaround for SDK bug) - let result: T - if (remoteEvalFeatureValues.has(feature)) { - result = remoteEvalFeatureValues.get(feature) as T - } else { - result = growthBookClient.getFeatureValue(feature, defaultValue) as T - } - - // Log experiment exposure using stored experiment data - if (logExposure) { - logExposureForFeature(feature) - } - - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - `GrowthBook: getFeatureValue("${feature}") = ${jsonStringify(result)}`, - ) - } - return result -} - -/** - * @deprecated Use getFeatureValue_CACHED_MAY_BE_STALE instead, which is non-blocking. - * This function blocks on GrowthBook initialization which can slow down startup. - */ -export async function getFeatureValue_DEPRECATED( - feature: string, - defaultValue: T, -): Promise { - return getFeatureValueInternal(feature, defaultValue, true) -} - -/** - * Get a feature value from disk cache immediately. Pure read — disk is - * populated by syncRemoteEvalToDisk on every successful payload (init + - * periodic refresh), not by this function. - * - * This is the preferred method for startup-critical paths and sync contexts. - * The value may be stale if the cache was written by a previous process. - */ -export function getFeatureValue_CACHED_MAY_BE_STALE( - feature: string, - defaultValue: T, -): T { - // Check env var overrides first (for eval harnesses) - const overrides = getEnvOverrides() - if (overrides && feature in overrides) { - return overrides[feature] as T - } - const configOverrides = getConfigOverrides() - if (configOverrides && feature in configOverrides) { - return configOverrides[feature] as T - } - - if (!isGrowthBookEnabled()) { - return defaultValue - } - - // Log experiment exposure if data is available, otherwise defer until after init - if (experimentDataByFeature.has(feature)) { - logExposureForFeature(feature) - } else { - pendingExposures.add(feature) - } - - // In-memory payload is authoritative once processRemoteEvalPayload has run. - // Disk is also fresh by then (syncRemoteEvalToDisk runs synchronously inside - // init), so this is correctness-equivalent to the disk read below — but it - // skips the config JSON parse and is what onGrowthBookRefresh subscribers - // depend on to read fresh values the instant they're notified. - if (remoteEvalFeatureValues.has(feature)) { - return remoteEvalFeatureValues.get(feature) as T - } - - // Fall back to disk cache (survives across process restarts) - try { - const cached = getGlobalConfig().cachedGrowthBookFeatures?.[feature] - return cached !== undefined ? (cached as T) : defaultValue - } catch { - return defaultValue - } -} - -/** - * @deprecated Disk cache is now synced on every successful payload load - * (init + 20min/6h periodic refresh). The per-feature TTL never fetched - * fresh data from the server — it only re-wrote in-memory state to disk, - * which is now redundant. Use getFeatureValue_CACHED_MAY_BE_STALE directly. - */ -export function getFeatureValue_CACHED_WITH_REFRESH( - feature: string, - defaultValue: T, - _refreshIntervalMs: number, -): T { - return getFeatureValue_CACHED_MAY_BE_STALE(feature, defaultValue) -} - -/** - * Check a Statsig feature gate value via GrowthBook, with fallback to Statsig cache. - * - * **MIGRATION ONLY**: This function is for migrating existing Statsig gates to GrowthBook. - * For new features, use `getFeatureValue_CACHED_MAY_BE_STALE()` instead. - * - * - Checks GrowthBook disk cache first - * - Falls back to Statsig's cachedStatsigGates during migration - * - The value may be stale if the cache hasn't been updated recently - * - * @deprecated Use getFeatureValue_CACHED_MAY_BE_STALE() for new code. This function - * exists only to support migration of existing Statsig gates. - */ -export function checkStatsigFeatureGate_CACHED_MAY_BE_STALE( - gate: string, -): boolean { - // Check env var overrides first (for eval harnesses) - const overrides = getEnvOverrides() - if (overrides && gate in overrides) { - return Boolean(overrides[gate]) - } - const configOverrides = getConfigOverrides() - if (configOverrides && gate in configOverrides) { - return Boolean(configOverrides[gate]) - } - - if (!isGrowthBookEnabled()) { - return false - } - - // Log experiment exposure if data is available, otherwise defer until after init - if (experimentDataByFeature.has(gate)) { - logExposureForFeature(gate) - } else { - pendingExposures.add(gate) - } - - // Return cached value immediately from disk - // First check GrowthBook cache, then fall back to Statsig cache for migration - const config = getGlobalConfig() - const gbCached = config.cachedGrowthBookFeatures?.[gate] - if (gbCached !== undefined) { - return Boolean(gbCached) - } - // Fallback to Statsig cache for migration period - return config.cachedStatsigGates?.[gate] ?? false -} - -/** - * Check a security restriction gate, waiting for re-init if in progress. - * - * Use this for security-critical gates where we need fresh values after auth changes. - * - * Behavior: - * - If GrowthBook is re-initializing (e.g., after login), waits for it to complete - * - Otherwise, returns cached value immediately (Statsig cache first, then GrowthBook) - * - * Statsig cache is checked first as a safety measure for security-related checks: - * if the Statsig cache indicates the gate is enabled, we honor it. - */ -export async function checkSecurityRestrictionGate( - gate: string, -): Promise { - // Check env var overrides first (for eval harnesses) - const overrides = getEnvOverrides() - if (overrides && gate in overrides) { - return Boolean(overrides[gate]) - } - const configOverrides = getConfigOverrides() - if (configOverrides && gate in configOverrides) { - return Boolean(configOverrides[gate]) - } - - if (!isGrowthBookEnabled()) { - return false - } - - // If re-initialization is in progress, wait for it to complete - // This ensures we get fresh values after auth changes - if (reinitializingPromise) { - await reinitializingPromise - } - - // Check Statsig cache first - it may have correct value from previous logged-in session - const config = getGlobalConfig() - const statsigCached = config.cachedStatsigGates?.[gate] - if (statsigCached !== undefined) { - return Boolean(statsigCached) - } - - // Then check GrowthBook cache - const gbCached = config.cachedGrowthBookFeatures?.[gate] - if (gbCached !== undefined) { - return Boolean(gbCached) - } - - // No cache - return false (don't block on init for uncached gates) - return false -} - -/** - * Check a boolean entitlement gate with fallback-to-blocking semantics. - * - * Fast path: if the disk cache already says `true`, return it immediately. - * Slow path: if disk says `false`/missing, await GrowthBook init and fetch the - * fresh server value (max ~5s). Disk is populated by syncRemoteEvalToDisk - * inside init, so by the time the slow path returns, disk already has the - * fresh value — no write needed here. - * - * Use for user-invoked features (e.g. /remote-control) that are gated on - * subscription/org, where a stale `false` would unfairly block access but a - * stale `true` is acceptable (the server is the real gatekeeper). - */ -export async function checkGate_CACHED_OR_BLOCKING( - gate: string, -): Promise { - // Check env var overrides first (for eval harnesses) - const overrides = getEnvOverrides() - if (overrides && gate in overrides) { - return Boolean(overrides[gate]) - } - const configOverrides = getConfigOverrides() - if (configOverrides && gate in configOverrides) { - return Boolean(configOverrides[gate]) - } - - if (!isGrowthBookEnabled()) { - return false - } - - // Fast path: disk cache already says true — trust it - const cached = getGlobalConfig().cachedGrowthBookFeatures?.[gate] - if (cached === true) { - // Log experiment exposure if data is available, otherwise defer - if (experimentDataByFeature.has(gate)) { - logExposureForFeature(gate) - } else { - pendingExposures.add(gate) - } - return true - } - - // Slow path: disk says false/missing — may be stale, fetch fresh - return getFeatureValueInternal(gate, false, true) -} - -/** - * Refresh GrowthBook after auth changes (login/logout). - * - * NOTE: This must destroy and recreate the client because GrowthBook's - * apiHostRequestHeaders cannot be updated after client creation. - */ -export function refreshGrowthBookAfterAuthChange(): void { - if (!isGrowthBookEnabled()) { - return - } - - try { - // Reset the client completely to get fresh auth headers - // This is necessary because apiHostRequestHeaders can't be updated after creation - resetGrowthBook() - - // resetGrowthBook cleared remoteEvalFeatureValues. If re-init below - // times out (hadFeatures=false) or short-circuits on !hasAuth (logout), - // the init-callback notify never fires — subscribers stay synced to the - // previous account's memoized state. Notify here so they re-read now - // (falls to disk cache). If re-init succeeds, they'll notify again with - // fresh values; if not, at least they're synced to the post-reset state. - refreshed.emit() - - // Reinitialize with fresh auth headers and attributes - // Track this promise so security gate checks can wait for it. - // .catch before .finally: initializeGrowthBook can reject if its sync - // helpers throw (getGrowthBookClient, getAuthHeaders, resetGrowthBook — - // clientWrapper.initialized itself has its own .catch so never rejects), - // and .finally re-settles with the original rejection — the sync - // try/catch below cannot catch async rejections. - reinitializingPromise = initializeGrowthBook() - .catch(error => { - logError(toError(error)) - return null - }) - .finally(() => { - reinitializingPromise = null - }) - } catch (error) { - if (process.env.NODE_ENV === 'development') { - throw error - } - logError(toError(error)) - } -} - -/** - * Reset GrowthBook client state (primarily for testing) - */ -export function resetGrowthBook(): void { - stopPeriodicGrowthBookRefresh() - // Remove process handlers before destroying client to prevent accumulation - if (currentBeforeExitHandler) { - process.off('beforeExit', currentBeforeExitHandler) - currentBeforeExitHandler = null - } - if (currentExitHandler) { - process.off('exit', currentExitHandler) - currentExitHandler = null - } - client?.destroy() - client = null - clientCreatedWithAuth = false - reinitializingPromise = null - experimentDataByFeature.clear() - pendingExposures.clear() - loggedExposures.clear() - remoteEvalFeatureValues.clear() - getGrowthBookClient.cache?.clear?.() - initializeGrowthBook.cache?.clear?.() - envOverrides = null - envOverridesParsed = false -} - -// Periodic refresh interval (matches Statsig's 6-hour interval) -const GROWTHBOOK_REFRESH_INTERVAL_MS = - process.env.USER_TYPE !== 'ant' - ? 6 * 60 * 60 * 1000 // 6 hours - : 20 * 60 * 1000 // 20 min (for ants) -let refreshInterval: ReturnType | null = null -let beforeExitListener: (() => void) | null = null - -/** - * Light refresh - re-fetch features from server without recreating client. - * Use this for periodic refresh when auth headers haven't changed. - * - * Unlike refreshGrowthBookAfterAuthChange() which destroys and recreates the client, - * this preserves client state and just fetches fresh feature values. - */ -export async function refreshGrowthBookFeatures(): Promise { - if (!isGrowthBookEnabled()) { - return - } - - try { - const growthBookClient = await initializeGrowthBook() - if (!growthBookClient) { - return - } - - await growthBookClient.refreshFeatures() - - // Guard: if this client was replaced during the in-flight refresh - // (e.g. refreshGrowthBookAfterAuthChange ran), skip processing the - // stale payload. Mirrors the init-callback guard above. - if (growthBookClient !== client) { - if (process.env.USER_TYPE === 'ant') { - logForDebugging( - 'GrowthBook: Skipping refresh processing for replaced client', - ) - } - return - } - - // Rebuild remoteEvalFeatureValues from the refreshed payload so that - // _BLOCKS_ON_INIT callers (e.g. getMaxVersion for the auto-update kill - // switch) see fresh values, not the stale init-time snapshot. - const hadFeatures = await processRemoteEvalPayload(growthBookClient) - // Same re-check as init path: covers the setPayload yield inside - // processRemoteEvalPayload (the guard above only covers refreshFeatures). - if (growthBookClient !== client) return - - if (process.env.USER_TYPE === 'ant') { - logForDebugging('GrowthBook: Light refresh completed') - } - - // Gate on hadFeatures: if the payload was empty/malformed, - // remoteEvalFeatureValues wasn't rebuilt — skip both the no-op disk - // write and the spurious subscriber churn (clearCommandMemoizationCaches - // + getCommands + 4× model re-renders). - if (hadFeatures) { - syncRemoteEvalToDisk() - refreshed.emit() - } - } catch (error) { - if (process.env.NODE_ENV === 'development') { - throw error - } - logError(toError(error)) - } -} - -/** - * Set up periodic refresh of GrowthBook features. - * Uses light refresh (refreshGrowthBookFeatures) to re-fetch without recreating client. - * - * Call this after initialization for long-running sessions to ensure - * feature values stay fresh. Matches Statsig's 6-hour refresh interval. - */ -export function setupPeriodicGrowthBookRefresh(): void { - if (!isGrowthBookEnabled()) { - return - } - - // Clear any existing interval to avoid duplicates - if (refreshInterval) { - clearInterval(refreshInterval) - } - - refreshInterval = setInterval(() => { - void refreshGrowthBookFeatures() - }, GROWTHBOOK_REFRESH_INTERVAL_MS) - // Allow process to exit naturally - this timer shouldn't keep the process alive - refreshInterval.unref?.() - - // Register cleanup listener only once - if (!beforeExitListener) { - beforeExitListener = () => { - stopPeriodicGrowthBookRefresh() - } - process.once('beforeExit', beforeExitListener) - } -} - -/** - * Stop periodic refresh (for testing or cleanup) - */ -export function stopPeriodicGrowthBookRefresh(): void { - if (refreshInterval) { - clearInterval(refreshInterval) - refreshInterval = null - } - if (beforeExitListener) { - process.removeListener('beforeExit', beforeExitListener) - beforeExitListener = null - } -} - -// ============================================================================ -// Dynamic Config Functions -// These are semantic wrappers around feature functions for Statsig API parity. -// In GrowthBook, dynamic configs are just features with object values. -// ============================================================================ - -/** - * Get a dynamic config value - blocks until GrowthBook is initialized. - * Prefer getFeatureValue_CACHED_MAY_BE_STALE for startup-critical paths. - */ -export async function getDynamicConfig_BLOCKS_ON_INIT( - configName: string, - defaultValue: T, -): Promise { - return getFeatureValue_DEPRECATED(configName, defaultValue) -} - -/** - * Get a dynamic config value from disk cache immediately. Pure read — see - * getFeatureValue_CACHED_MAY_BE_STALE. - * This is the preferred method for startup-critical paths and sync contexts. - * - * In GrowthBook, dynamic configs are just features with object values. - */ -export function getDynamicConfig_CACHED_MAY_BE_STALE( - configName: string, - defaultValue: T, -): T { - return getFeatureValue_CACHED_MAY_BE_STALE(configName, defaultValue) -} diff --git a/src/services/analytics/index.ts b/src/services/analytics/index.ts deleted file mode 100644 index 30d2e592..00000000 --- a/src/services/analytics/index.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Analytics service - public API for event logging - * - * This module serves as the main entry point for analytics events in Claude CLI. - * - * DESIGN: This module has NO dependencies to avoid import cycles. - * Events are queued until attachAnalyticsSink() is called during app initialization. - * The sink handles routing to Datadog and 1P event logging. - */ - -/** - * Marker type for verifying analytics metadata doesn't contain sensitive data - * - * This type forces explicit verification that string values being logged - * don't contain code snippets, file paths, or other sensitive information. - * - * Usage: `myString as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS` - */ -export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never - -/** - * Marker type for values routed to PII-tagged proto columns via `_PROTO_*` - * payload keys. The destination BQ column has privileged access controls, - * so unredacted values are acceptable — unlike general-access backends. - * - * sink.ts strips `_PROTO_*` keys before Datadog fanout; only the 1P - * exporter (firstPartyEventLoggingExporter) sees them and hoists them to the - * top-level proto field. A single stripProtoFields call guards all non-1P - * sinks — no per-sink filtering to forget. - * - * Usage: `rawName as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED` - */ -export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = never - -/** - * Strip `_PROTO_*` keys from a payload destined for general-access storage. - * Used by: - * - sink.ts: before Datadog fanout (never sees PII-tagged values) - * - firstPartyEventLoggingExporter: defensive strip of additional_metadata - * after hoisting known _PROTO_* keys to proto fields — prevents a future - * unrecognized _PROTO_foo from silently landing in the BQ JSON blob. - * - * Returns the input unchanged (same reference) when no _PROTO_ keys present. - */ -export function stripProtoFields( - metadata: Record, -): Record { - let result: Record | undefined - for (const key in metadata) { - if (key.startsWith('_PROTO_')) { - if (result === undefined) { - result = { ...metadata } - } - delete result[key] - } - } - return result ?? metadata -} - -// Internal type for logEvent metadata - different from the enriched EventMetadata in metadata.ts -type LogEventMetadata = { [key: string]: boolean | number | undefined } - -type QueuedEvent = { - eventName: string - metadata: LogEventMetadata - async: boolean -} - -/** - * Sink interface for the analytics backend - */ -export type AnalyticsSink = { - logEvent: (eventName: string, metadata: LogEventMetadata) => void - logEventAsync: ( - eventName: string, - metadata: LogEventMetadata, - ) => Promise -} - -// Event queue for events logged before sink is attached -const eventQueue: QueuedEvent[] = [] - -// Sink - initialized during app startup -let sink: AnalyticsSink | null = null - -/** - * Attach the analytics sink that will receive all events. - * Queued events are drained asynchronously via queueMicrotask to avoid - * adding latency to the startup path. - * - * Idempotent: if a sink is already attached, this is a no-op. This allows - * calling from both the preAction hook (for subcommands) and setup() (for - * the default command) without coordination. - */ -export function attachAnalyticsSink(newSink: AnalyticsSink): void { - if (sink !== null) { - return - } - sink = newSink - - // Drain the queue asynchronously to avoid blocking startup - if (eventQueue.length > 0) { - const queuedEvents = [...eventQueue] - eventQueue.length = 0 - - // Log queue size for ants to help debug analytics initialization timing - if (process.env.USER_TYPE === 'ant') { - sink.logEvent('analytics_sink_attached', { - queued_event_count: queuedEvents.length, - }) - } - - queueMicrotask(() => { - for (const event of queuedEvents) { - if (event.async) { - void sink!.logEventAsync(event.eventName, event.metadata) - } else { - sink!.logEvent(event.eventName, event.metadata) - } - } - }) - } -} - -/** - * Log an event to analytics backends (synchronous) - * - * Events may be sampled based on the 'tengu_event_sampling_config' dynamic config. - * When sampled, the sample_rate is added to the event metadata. - * - * If no sink is attached, events are queued and drained when the sink attaches. - */ -export function logEvent( - eventName: string, - // intentionally no strings unless AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - // to avoid accidentally logging code/filepaths - metadata: LogEventMetadata, -): void { - if (sink === null) { - eventQueue.push({ eventName, metadata, async: false }) - return - } - sink.logEvent(eventName, metadata) -} - -/** - * Log an event to analytics backends (asynchronous) - * - * Events may be sampled based on the 'tengu_event_sampling_config' dynamic config. - * When sampled, the sample_rate is added to the event metadata. - * - * If no sink is attached, events are queued and drained when the sink attaches. - */ -export async function logEventAsync( - eventName: string, - // intentionally no strings, to avoid accidentally logging code/filepaths - metadata: LogEventMetadata, -): Promise { - if (sink === null) { - eventQueue.push({ eventName, metadata, async: true }) - return - } - await sink.logEventAsync(eventName, metadata) -} - -/** - * Reset analytics state for testing purposes only. - * @internal - */ -export function _resetForTesting(): void { - sink = null - eventQueue.length = 0 -} diff --git a/src/services/analytics/sink.ts b/src/services/analytics/sink.ts deleted file mode 100644 index a7b70212..00000000 --- a/src/services/analytics/sink.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Analytics sink implementation - * - * This module contains the actual analytics routing logic and should be - * initialized during app startup. It routes events to Datadog and 1P event - * logging. - * - * Usage: Call initializeAnalyticsSink() during app startup to attach the sink. - */ - -import { trackDatadogEvent } from './datadog.js' -import { logEventTo1P, shouldSampleEvent } from './firstPartyEventLogger.js' -import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from './growthbook.js' -import { attachAnalyticsSink, stripProtoFields } from './index.js' -import { isSinkKilled } from './sinkKillswitch.js' - -// Local type matching the logEvent metadata signature -type LogEventMetadata = { [key: string]: boolean | number | undefined } - -const DATADOG_GATE_NAME = 'tengu_log_datadog_events' - -// Module-level gate state - starts undefined, initialized during startup -let isDatadogGateEnabled: boolean | undefined = undefined - -/** - * Check if Datadog tracking is enabled. - * Falls back to cached value from previous session if not yet initialized. - */ -function shouldTrackDatadog(): boolean { - if (isSinkKilled('datadog')) { - return false - } - if (isDatadogGateEnabled !== undefined) { - return isDatadogGateEnabled - } - - // Fallback to cached value from previous session - try { - return checkStatsigFeatureGate_CACHED_MAY_BE_STALE(DATADOG_GATE_NAME) - } catch { - return false - } -} - -/** - * Log an event (synchronous implementation) - */ -function logEventImpl(eventName: string, metadata: LogEventMetadata): void { - // Check if this event should be sampled - const sampleResult = shouldSampleEvent(eventName) - - // If sample result is 0, the event was not selected for logging - if (sampleResult === 0) { - return - } - - // If sample result is a positive number, add it to metadata - const metadataWithSampleRate = - sampleResult !== null - ? { ...metadata, sample_rate: sampleResult } - : metadata - - if (shouldTrackDatadog()) { - // Datadog is a general-access backend — strip _PROTO_* keys - // (unredacted PII-tagged values meant only for the 1P privileged column). - void trackDatadogEvent(eventName, stripProtoFields(metadataWithSampleRate)) - } - - // 1P receives the full payload including _PROTO_* — the exporter - // destructures and routes those keys to proto fields itself. - logEventTo1P(eventName, metadataWithSampleRate) -} - -/** - * Log an event (asynchronous implementation) - * - * With Segment removed the two remaining sinks are fire-and-forget, so this - * just wraps the sync impl — kept to preserve the sink interface contract. - */ -function logEventAsyncImpl( - eventName: string, - metadata: LogEventMetadata, -): Promise { - logEventImpl(eventName, metadata) - return Promise.resolve() -} - -/** - * Initialize analytics gates during startup. - * - * Updates gate values from server. Early events use cached values from previous - * session to avoid data loss during initialization. - * - * Called from main.tsx during setupBackend(). - */ -export function initializeAnalyticsGates(): void { - isDatadogGateEnabled = - checkStatsigFeatureGate_CACHED_MAY_BE_STALE(DATADOG_GATE_NAME) -} - -/** - * Initialize the analytics sink. - * - * Call this during app startup to attach the analytics backend. - * Any events logged before this is called will be queued and drained. - * - * Idempotent: safe to call multiple times (subsequent calls are no-ops). - */ -export function initializeAnalyticsSink(): void { - attachAnalyticsSink({ - logEvent: logEventImpl, - logEventAsync: logEventAsyncImpl, - }) -} diff --git a/src/services/analytics/sinkKillswitch.ts b/src/services/analytics/sinkKillswitch.ts deleted file mode 100644 index 88757580..00000000 --- a/src/services/analytics/sinkKillswitch.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { getDynamicConfig_CACHED_MAY_BE_STALE } from './growthbook.js' - -// Mangled name: per-sink analytics killswitch -const SINK_KILLSWITCH_CONFIG_NAME = 'tengu_frond_boric' - -export type SinkName = 'datadog' | 'firstParty' - -/** - * GrowthBook JSON config that disables individual analytics sinks. - * Shape: { datadog?: boolean, firstParty?: boolean } - * A value of true for a key stops all dispatch to that sink. - * Default {} (nothing killed). Fail-open: missing/malformed config = sink stays on. - * - * NOTE: Must NOT be called from inside is1PEventLoggingEnabled() - - * growthbook.ts:isGrowthBookEnabled() calls that, so a lookup here would recurse. - * Call at per-event dispatch sites instead. - */ -export function isSinkKilled(sink: SinkName): boolean { - const config = getDynamicConfig_CACHED_MAY_BE_STALE< - Partial> - >(SINK_KILLSWITCH_CONFIG_NAME, {}) - // getFeatureValue_CACHED_MAY_BE_STALE guards on `!== undefined`, so a - // cached JSON null leaks through instead of falling back to {}. - return config?.[sink] === true -} diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts index 89a6e661..94513e20 100644 --- a/src/services/api/claude.ts +++ b/src/services/api/claude.ts @@ -70,7 +70,7 @@ import { import { resolveAppliedEffort } from '../../utils/effort.js' import { isEnvTruthy } from '../../utils/envUtils.js' import { errorMessage } from '../../utils/errors.js' -import { computeFingerprintFromMessages } from '../../utils/fingerprint.js' +import { computeFingerprintFromMessages } from '../../utils/fingerprint-stub.js' import { captureAPIRequest, logError } from '../../utils/log.js' import { createAssistantAPIErrorMessage, @@ -93,7 +93,7 @@ import { type SystemPrompt, } from '../../utils/systemPromptType.js' import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js' -import { getDynamicConfig_BLOCKS_ON_INIT } from '../analytics/growthbook.js' +import { getDynamicConfig_BLOCKS_ON_INIT } from '../analytics-stub.js' import { currentLimits, extractQuotaStatusFromError, @@ -144,7 +144,7 @@ import { import type { QuerySource } from 'src/constants/querySource.js' import type { Notification } from 'src/context/notifications.js' import { addToTotalSessionCost } from 'src/cost-tracker.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' import type { AgentId } from 'src/types/ids.js' import { ADVISOR_TOOL_INSTRUCTIONS, @@ -213,12 +213,12 @@ import { isBetaTracingEnabled, type LLMRequestNewContext, startLLMRequestSpan, -} from '../../utils/telemetry/sessionTracing.js' +} from '../../utils/telemetry-stub.js' /* eslint-enable @typescript-eslint/no-require-imports */ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' import { consumePendingCacheEdits, getPinnedCacheEdits, diff --git a/src/services/api/client.ts b/src/services/api/client.ts index 8c1feb69..1a022f08 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -150,6 +150,14 @@ export async function getAnthropicClient({ fetch: resolvedFetch, }), } + if (isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)) { + const { createOpenAIShimClient } = await import('./openaiShim.js') + return createOpenAIShimClient({ + defaultHeaders, + maxRetries, + timeout: parseInt(process.env.API_TIMEOUT_MS || String(600 * 1000), 10), + }) as unknown as Anthropic + } if (isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)) { const { AnthropicBedrock } = await import('@anthropic-ai/bedrock-sdk') // Use region override for small fast model if specified diff --git a/src/services/api/codexShim.ts b/src/services/api/codexShim.ts new file mode 100644 index 00000000..208127b0 --- /dev/null +++ b/src/services/api/codexShim.ts @@ -0,0 +1,822 @@ +import type { + ResolvedCodexCredentials, + ResolvedProviderRequest, +} from './providerConfig.js' + +export interface AnthropicUsage { + input_tokens: number + output_tokens: number + cache_creation_input_tokens: number + cache_read_input_tokens: number +} + +export interface AnthropicStreamEvent { + type: string + message?: Record + index?: number + content_block?: Record + delta?: Record + usage?: Partial +} + +export interface ShimCreateParams { + model: string + messages: Array> + system?: unknown + tools?: Array> + max_tokens: number + stream?: boolean + temperature?: number + top_p?: number + tool_choice?: unknown + metadata?: unknown + [key: string]: unknown +} + +type ResponsesInputPart = + | { type: 'input_text'; text: string } + | { type: 'output_text'; text: string } + | { type: 'input_image'; image_url: string } + +type ResponsesInputItem = + | { + type: 'message' + role: 'user' | 'assistant' + content: ResponsesInputPart[] + } + | { + type: 'function_call' + id: string + call_id: string + name: string + arguments: string + } + | { + type: 'function_call_output' + call_id: string + output: string + } + +type ResponsesTool = { + type: 'function' + name: string + description: string + parameters: Record + strict?: boolean +} + +type CodexSseEvent = { + event: string + data: Record +} + +function makeUsage(usage?: { + input_tokens?: number + output_tokens?: number +}): AnthropicUsage { + return { + input_tokens: usage?.input_tokens ?? 0, + output_tokens: usage?.output_tokens ?? 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + } +} + +function makeMessageId(): string { + return `msg_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}` +} + +function normalizeToolUseId(toolUseId: string | undefined): { + id: string + callId: string +} { + const value = (toolUseId || '').trim() + if (!value) { + return { + id: 'fc_unknown', + callId: 'call_unknown', + } + } + if (value.startsWith('call_')) { + return { + id: `fc_${value.slice('call_'.length)}`, + callId: value, + } + } + if (value.startsWith('fc_')) { + return { + id: value, + callId: `call_${value.slice('fc_'.length)}`, + } + } + return { + id: `fc_${value}`, + callId: value, + } +} + +function convertSystemPrompt(system: unknown): string { + if (!system) return '' + if (typeof system === 'string') return system + if (Array.isArray(system)) { + return system + .map((block: { type?: string; text?: string }) => + block.type === 'text' ? (block.text ?? '') : '', + ) + .join('\n\n') + } + return String(system) +} + +function convertToolResultToText(content: unknown): string { + if (typeof content === 'string') return content + if (!Array.isArray(content)) return JSON.stringify(content ?? '') + + const chunks: string[] = [] + for (const block of content) { + if (block?.type === 'text' && typeof block.text === 'string') { + chunks.push(block.text) + continue + } + + if (block?.type === 'image') { + const src = block.source + if (src?.type === 'url' && src.url) { + chunks.push(`[Image](${src.url})`) + } + continue + } + + if (typeof block?.text === 'string') { + chunks.push(block.text) + } + } + + return chunks.join('\n') +} + +function convertContentBlocksToResponsesParts( + content: unknown, + role: 'user' | 'assistant', +): ResponsesInputPart[] { + const textType = role === 'assistant' ? 'output_text' : 'input_text' + if (typeof content === 'string') { + return [{ type: textType, text: content }] + } + if (!Array.isArray(content)) { + return [{ type: textType, text: String(content ?? '') }] + } + + const parts: ResponsesInputPart[] = [] + for (const block of content) { + switch (block?.type) { + case 'text': + parts.push({ type: textType, text: block.text ?? '' }) + break + case 'image': { + if (role === 'assistant') break + const source = block.source + if (source?.type === 'base64') { + parts.push({ + type: 'input_image', + image_url: `data:${source.media_type};base64,${source.data}`, + }) + } else if (source?.type === 'url' && source.url) { + parts.push({ + type: 'input_image', + image_url: source.url, + }) + } + break + } + case 'thinking': + if (block.thinking) { + parts.push({ + type: textType, + text: `${block.thinking}`, + }) + } + break + case 'tool_use': + case 'tool_result': + break + default: + if (typeof block?.text === 'string') { + parts.push({ type: textType, text: block.text }) + } + } + } + + return parts +} + +export function convertAnthropicMessagesToResponsesInput( + messages: Array<{ role?: string; message?: { role?: string; content?: unknown }; content?: unknown }>, +): ResponsesInputItem[] { + const items: ResponsesInputItem[] = [] + + for (const message of messages) { + const inner = message.message ?? message + const role = (inner as { role?: string }).role ?? message.role + const content = (inner as { content?: unknown }).content + + if (role === 'user') { + if (Array.isArray(content)) { + const toolResults = content.filter( + (block: { type?: string }) => block.type === 'tool_result', + ) + const otherContent = content.filter( + (block: { type?: string }) => block.type !== 'tool_result', + ) + + for (const toolResult of toolResults) { + const { callId } = normalizeToolUseId(toolResult.tool_use_id) + items.push({ + type: 'function_call_output', + call_id: callId, + output: convertToolResultToText(toolResult.content), + }) + } + + const parts = convertContentBlocksToResponsesParts(otherContent, 'user') + if (parts.length > 0) { + items.push({ + type: 'message', + role: 'user', + content: parts, + }) + } + continue + } + + items.push({ + type: 'message', + role: 'user', + content: convertContentBlocksToResponsesParts(content, 'user'), + }) + continue + } + + if (role === 'assistant') { + const textBlocks = Array.isArray(content) + ? content.filter((block: { type?: string }) => block.type !== 'tool_use') + : content + const parts = convertContentBlocksToResponsesParts(textBlocks, 'assistant') + if (parts.length > 0) { + items.push({ + type: 'message', + role: 'assistant', + content: parts, + }) + } + + if (Array.isArray(content)) { + for (const toolUse of content.filter( + (block: { type?: string }) => block.type === 'tool_use', + )) { + const normalized = normalizeToolUseId(toolUse.id) + items.push({ + type: 'function_call', + id: normalized.id, + call_id: normalized.callId, + name: toolUse.name ?? 'tool', + arguments: + typeof toolUse.input === 'string' + ? toolUse.input + : JSON.stringify(toolUse.input ?? {}), + }) + } + } + } + } + + return items.filter(item => + item.type !== 'message' || item.content.length > 0, + ) +} + +/** + * Recursively enforces Codex strict-mode constraints on a JSON schema: + * - Every `object` type gets `additionalProperties: false` + * - All property keys are listed in `required` + * - Nested schemas (properties, items, anyOf/oneOf/allOf) are processed too + */ +function enforceStrictSchema(schema: unknown): Record { + if (!schema || typeof schema !== 'object' || Array.isArray(schema)) { + return (schema ?? {}) as Record + } + + const record = { ...(schema as Record) } + + // Codex API strict schemas reject these JSON schema keywords + delete record.$schema + delete record.propertyNames + + if (record.type === 'object') { + // OpenAI structured outputs completely forbid dynamic additionalProperties. + // They must be set to false unconditionally. + record.additionalProperties = false + + if ( + record.properties && + typeof record.properties === 'object' && + !Array.isArray(record.properties) + ) { + const props = record.properties as Record + + const enforcedProps: Record = {} + for (const [key, value] of Object.entries(props)) { + const strictValue = enforceStrictSchema(value) + if ( + strictValue && + typeof strictValue === 'object' && + strictValue.type === 'object' && + strictValue.additionalProperties === false && + (!strictValue.properties || Object.keys(strictValue.properties).length === 0) + ) { + continue + } + enforcedProps[key] = strictValue + } + record.properties = enforcedProps + record.required = Object.keys(enforcedProps) + } else { + // No properties — empty required array + record.required = [] + } + } + + // Recurse into array items + if ('items' in record) { + if (Array.isArray(record.items)) { + record.items = (record.items as unknown[]).map(item => enforceStrictSchema(item)) + } else { + record.items = enforceStrictSchema(record.items) + } + } + + // Recurse into combinators + for (const key of ['anyOf', 'oneOf', 'allOf'] as const) { + if (key in record && Array.isArray(record[key])) { + record[key] = (record[key] as unknown[]).map(item => enforceStrictSchema(item)) + } + } + + return record +} + +export function convertToolsToResponsesTools( + tools: Array<{ name?: string; description?: string; input_schema?: Record }>, +): ResponsesTool[] { + return tools + .filter(tool => tool.name && tool.name !== 'ToolSearchTool') + .map(tool => { + const rawParameters = tool.input_schema ?? { type: 'object', properties: {} } + const parameters = enforceStrictSchema(rawParameters) + + return { + type: 'function', + name: tool.name ?? 'tool', + description: tool.description ?? '', + parameters, + strict: true, + } + }) +} + +function convertToolChoice(toolChoice: unknown): unknown { + const choice = toolChoice as { type?: string; name?: string } | undefined + if (!choice?.type) return undefined + if (choice.type === 'auto') return 'auto' + if (choice.type === 'any') return 'required' + if (choice.type === 'tool' && choice.name) { + return { + type: 'function', + name: choice.name, + } + } + return undefined +} + +export async function performCodexRequest(options: { + request: ResolvedProviderRequest + credentials: ResolvedCodexCredentials + params: ShimCreateParams + defaultHeaders: Record + signal?: AbortSignal +}): Promise { + const input = convertAnthropicMessagesToResponsesInput( + options.params.messages as Array<{ + role?: string + message?: { role?: string; content?: unknown } + content?: unknown + }>, + ) + const body: Record = { + model: options.request.resolvedModel, + input: input.length > 0 + ? input + : [ + { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: '' }], + }, + ], + store: false, + stream: true, + } + + const instructions = convertSystemPrompt(options.params.system) + if (instructions) { + body.instructions = instructions + } + + const toolChoice = convertToolChoice(options.params.tool_choice) + if (toolChoice) { + body.tool_choice = toolChoice + } + + if (options.params.tools && options.params.tools.length > 0) { + const convertedTools = convertToolsToResponsesTools( + options.params.tools as Array<{ + name?: string + description?: string + input_schema?: Record + }>, + ) + if (convertedTools.length > 0) { + body.tools = convertedTools + body.parallel_tool_calls = true + body.tool_choice ??= 'auto' + } + } + + if (options.request.reasoning) { + body.reasoning = options.request.reasoning + } + + const isTargetModel = + options.request.resolvedModel?.toLowerCase().includes('gpt') || + options.request.resolvedModel?.toLowerCase().includes('codex') + + // Only pass temperature and top_p if it's not a GPT/Codex model that rejects them + if (!isTargetModel) { + if (options.params.temperature !== undefined) { + body.temperature = options.params.temperature + } + if (options.params.top_p !== undefined) { + body.top_p = options.params.top_p + } + } + + const headers: Record = { + 'Content-Type': 'application/json', + ...options.defaultHeaders, + Authorization: `Bearer ${options.credentials.apiKey}`, + } + if (options.credentials.accountId) { + headers['chatgpt-account-id'] = options.credentials.accountId + } + headers.originator ??= 'openclaude' + + const response = await fetch(`${options.request.baseUrl}/responses`, { + method: 'POST', + headers, + body: JSON.stringify(body), + signal: options.signal, + }) + + if (!response.ok) { + const errorBody = await response.text().catch(() => 'unknown error') + throw new Error(`Codex API error ${response.status}: ${errorBody}`) + } + + return response +} + +async function* readSseEvents(response: Response): AsyncGenerator { + const reader = response.body?.getReader() + if (!reader) return + + const decoder = new TextDecoder() + let buffer = '' + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const chunks = buffer.split('\n\n') + buffer = chunks.pop() ?? '' + + for (const chunk of chunks) { + const lines = chunk + .split('\n') + .map(line => line.trim()) + .filter(Boolean) + if (lines.length === 0) continue + + const eventLine = lines.find(line => line.startsWith('event: ')) + const dataLines = lines.filter(line => line.startsWith('data: ')) + if (!eventLine || dataLines.length === 0) continue + + const event = eventLine.slice(7).trim() + const rawData = dataLines.map(line => line.slice(6)).join('\n') + if (rawData === '[DONE]') continue + + let data: Record + try { + const parsed = JSON.parse(rawData) + if (!parsed || typeof parsed !== 'object') continue + data = parsed as Record + } catch { + continue + } + + yield { event, data } + } + } +} + +function determineStopReason( + response: Record | undefined, + sawToolUse: boolean, +): 'end_turn' | 'tool_use' | 'max_tokens' { + const output = Array.isArray(response?.output) ? response.output : [] + if ( + sawToolUse || + output.some((item: { type?: string }) => item?.type === 'function_call') + ) { + return 'tool_use' + } + + const incompleteReason = response?.incomplete_details?.reason + if ( + typeof incompleteReason === 'string' && + incompleteReason.includes('max_output_tokens') + ) { + return 'max_tokens' + } + + return 'end_turn' +} + +export async function collectCodexCompletedResponse( + response: Response, +): Promise> { + let completedResponse: Record | undefined + + for await (const event of readSseEvents(response)) { + if (event.event === 'response.failed') { + throw new Error( + event.data?.response?.error?.message ?? + event.data?.error?.message ?? + 'Codex response failed', + ) + } + + if ( + event.event === 'response.completed' || + event.event === 'response.incomplete' + ) { + completedResponse = event.data?.response + break + } + } + + if (!completedResponse) { + throw new Error('Codex response ended without a completed payload') + } + + return completedResponse +} + +export async function* codexStreamToAnthropic( + response: Response, + model: string, +): AsyncGenerator { + const messageId = makeMessageId() + const toolBlocksByItemId = new Map< + string, + { index: number; toolUseId: string } + >() + let activeTextBlockIndex: number | null = null + let nextContentBlockIndex = 0 + let sawToolUse = false + let finalResponse: Record | undefined + + const closeActiveTextBlock = async function* () { + if (activeTextBlockIndex === null) return + yield { + type: 'content_block_stop', + index: activeTextBlockIndex, + } + activeTextBlockIndex = null + } + + const startTextBlockIfNeeded = async function* () { + if (activeTextBlockIndex !== null) return + activeTextBlockIndex = nextContentBlockIndex++ + yield { + type: 'content_block_start', + index: activeTextBlockIndex, + content_block: { type: 'text', text: '' }, + } + } + + yield { + type: 'message_start', + message: { + id: messageId, + type: 'message', + role: 'assistant', + content: [], + model, + stop_reason: null, + stop_sequence: null, + usage: makeUsage(), + }, + } + + for await (const event of readSseEvents(response)) { + const payload = event.data + + if (event.event === 'response.output_item.added') { + const item = payload.item + if (item?.type === 'function_call') { + yield* closeActiveTextBlock() + const blockIndex = nextContentBlockIndex++ + const toolUseId = item.call_id ?? item.id ?? `call_${blockIndex}` + toolBlocksByItemId.set(String(item.id ?? toolUseId), { + index: blockIndex, + toolUseId, + }) + sawToolUse = true + + yield { + type: 'content_block_start', + index: blockIndex, + content_block: { + type: 'tool_use', + id: toolUseId, + name: item.name ?? 'tool', + input: {}, + }, + } + + if (item.arguments) { + yield { + type: 'content_block_delta', + index: blockIndex, + delta: { + type: 'input_json_delta', + partial_json: item.arguments, + }, + } + } + } + continue + } + + if (event.event === 'response.content_part.added') { + if (payload.part?.type === 'output_text') { + yield* startTextBlockIfNeeded() + } + continue + } + + if (event.event === 'response.output_text.delta') { + yield* startTextBlockIfNeeded() + if (activeTextBlockIndex !== null) { + yield { + type: 'content_block_delta', + index: activeTextBlockIndex, + delta: { + type: 'text_delta', + text: payload.delta ?? '', + }, + } + } + continue + } + + if (event.event === 'response.function_call_arguments.delta') { + const toolBlock = toolBlocksByItemId.get(String(payload.item_id ?? '')) + if (toolBlock) { + yield { + type: 'content_block_delta', + index: toolBlock.index, + delta: { + type: 'input_json_delta', + partial_json: payload.delta ?? '', + }, + } + } + continue + } + + if (event.event === 'response.output_item.done') { + const item = payload.item + if (item?.type === 'function_call') { + const toolBlock = toolBlocksByItemId.get(String(item.id ?? '')) + if (toolBlock) { + yield { + type: 'content_block_stop', + index: toolBlock.index, + } + toolBlocksByItemId.delete(String(item.id)) + } + } else if (item?.type === 'message') { + yield* closeActiveTextBlock() + } + continue + } + + if ( + event.event === 'response.completed' || + event.event === 'response.incomplete' + ) { + finalResponse = payload.response + break + } + + if (event.event === 'response.failed') { + throw new Error( + payload?.response?.error?.message ?? + payload?.error?.message ?? + 'Codex response failed', + ) + } + } + + yield* closeActiveTextBlock() + for (const toolBlock of toolBlocksByItemId.values()) { + yield { + type: 'content_block_stop', + index: toolBlock.index, + } + } + + yield { + type: 'message_delta', + delta: { + stop_reason: determineStopReason(finalResponse, sawToolUse), + stop_sequence: null, + }, + usage: { + input_tokens: finalResponse?.usage?.input_tokens ?? 0, + output_tokens: finalResponse?.usage?.output_tokens ?? 0, + }, + } + yield { type: 'message_stop' } +} + +export function convertCodexResponseToAnthropicMessage( + data: Record, + model: string, +): Record { + const content: Array> = [] + const output = Array.isArray(data.output) ? data.output : [] + + for (const item of output) { + if (item?.type === 'message' && Array.isArray(item.content)) { + for (const part of item.content) { + if (part?.type === 'output_text') { + content.push({ + type: 'text', + text: part.text ?? '', + }) + } + } + continue + } + + if (item?.type === 'function_call') { + let input: unknown + try { + input = JSON.parse(item.arguments ?? '{}') + } catch { + input = { raw: item.arguments ?? '' } + } + + content.push({ + type: 'tool_use', + id: item.call_id ?? item.id ?? makeMessageId(), + name: item.name ?? 'tool', + input, + }) + } + } + + return { + id: data.id ?? makeMessageId(), + type: 'message', + role: 'assistant', + content, + model: data.model ?? model, + stop_reason: determineStopReason(data, content.some(item => item.type === 'tool_use')), + stop_sequence: null, + usage: makeUsage(data.usage), + } +} diff --git a/src/services/api/errors.ts b/src/services/api/errors.ts index 1a7edc52..ffd3f9f8 100644 --- a/src/services/api/errors.ts +++ b/src/services/api/errors.ts @@ -42,7 +42,7 @@ import { ImageSizeError } from '../../utils/imageValidation.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' import { type ClaudeAILimits, getRateLimitErrorMessage, diff --git a/src/services/api/filesApi.ts b/src/services/api/filesApi.ts index cb9a03b9..99a302eb 100644 --- a/src/services/api/filesApi.ts +++ b/src/services/api/filesApi.ts @@ -20,7 +20,7 @@ import { sleep } from '../../utils/sleep.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' // Files API is currently in beta. oauth-2025-04-20 enables Bearer OAuth // on public-api routes (auth.py: "oauth_auth" not in beta_versions → 404). diff --git a/src/services/api/grove.ts b/src/services/api/grove.ts index f8af7897..663beed8 100644 --- a/src/services/api/grove.ts +++ b/src/services/api/grove.ts @@ -3,7 +3,7 @@ import memoize from 'lodash-es/memoize.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../analytics-stub.js' import { getOauthAccountInfo, isConsumerSubscriber } from 'src/utils/auth.js' import { logForDebugging } from 'src/utils/debug.js' import { gracefulShutdown } from 'src/utils/gracefulShutdown.js' diff --git a/src/services/api/logging.ts b/src/services/api/logging.ts index a411c12c..9a1e05fd 100644 --- a/src/services/api/logging.ts +++ b/src/services/api/logging.ts @@ -22,19 +22,19 @@ import { logError } from 'src/utils/log.js' import { getAPIProviderForStatsig } from 'src/utils/model/providers.js' import type { PermissionMode } from 'src/utils/permissions/PermissionMode.js' import { jsonStringify } from 'src/utils/slowOperations.js' -import { logOTelEvent } from 'src/utils/telemetry/events.js' +import { logOTelEvent } from '../../utils/telemetry-stub.js' import { endLLMRequestSpan, isBetaTracingEnabled, type Span, -} from 'src/utils/telemetry/sessionTracing.js' +} from '../../utils/telemetry-stub.js' import type { NonNullableUsage } from '../../entrypoints/sdk/sdkUtilityTypes.js' import { consumeInvokingRequestId } from '../../utils/agentContext.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' -import { sanitizeToolNameForAnalytics } from '../analytics/metadata.js' +} from '../analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../analytics-stub.js' import { EMPTY_USAGE } from './emptyUsage.js' import { classifyAPIError } from './errors.js' import { extractConnectionErrorDetails } from './errorUtils.js' diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts new file mode 100644 index 00000000..00757649 --- /dev/null +++ b/src/services/api/openaiShim.ts @@ -0,0 +1,1028 @@ +/** + * OpenAI-compatible API shim for Claude Code. + * + * Translates Anthropic SDK calls (anthropic.beta.messages.create) into + * OpenAI-compatible requests and streams back events in the Anthropic + * streaming format so the rest of the codebase is unaware. + * + * Supports both Chat Completions and the newer Responses API for OpenAI, + * Azure OpenAI, Ollama, LM Studio, OpenRouter, Together, Groq, Fireworks, + * DeepSeek, Mistral, ChatGPT Codex aliases, and other OpenAI-compatible APIs. + * + * Environment variables: + * CLAUDE_CODE_USE_OPENAI=1 — enable this provider + * OPENAI_API_KEY=sk-... — API key (optional for local models) + * OPENAI_BASE_URL=http://... — base URL (default: https://api.openai.com/v1) + * OPENAI_MODEL=gpt-4o — default model override + * OPENAI_API_MODE=chat_completions|responses — force the transport mode + * CODEX_API_KEY / ~/.codex/auth.json — Codex auth for codexplan/codexspark + */ + +import { + codexStreamToAnthropic, + collectCodexCompletedResponse, + convertAnthropicMessagesToResponsesInput, + convertCodexResponseToAnthropicMessage, + convertToolsToResponsesTools, + performCodexRequest, + type AnthropicStreamEvent, + type AnthropicUsage, + type ShimCreateParams, +} from './codexShim.js' +import { + resolveCodexApiCredentials, + resolveProviderRequest, +} from './providerConfig.js' + +// --------------------------------------------------------------------------- +// Types — minimal subset of Anthropic SDK types we need to produce +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Message format conversion: Anthropic → OpenAI +// --------------------------------------------------------------------------- + +interface OpenAIMessage { + role: 'system' | 'user' | 'assistant' | 'tool' + content?: string | Array<{ type: string; text?: string; image_url?: { url: string } }> + tool_calls?: Array<{ + id: string + type: 'function' + function: { name: string; arguments: string } + extra_content?: Record + }> + tool_call_id?: string + name?: string +} + +interface OpenAITool { + type: 'function' + function: { + name: string + description: string + parameters: Record + strict?: boolean + } +} + +type ResponsesToolChoice = + | 'auto' + | 'required' + | 'none' + | { + type: 'function' + name: string + } + +function convertSystemPrompt( + system: unknown, +): string { + if (!system) return '' + if (typeof system === 'string') return system + if (Array.isArray(system)) { + return system + .map((block: { type?: string; text?: string }) => + block.type === 'text' ? block.text ?? '' : '', + ) + .join('\n\n') + } + return String(system) +} + +function convertContentBlocks( + content: unknown, +): string | Array<{ type: string; text?: string; image_url?: { url: string } }> { + if (typeof content === 'string') return content + if (!Array.isArray(content)) return String(content ?? '') + + const parts: Array<{ type: string; text?: string; image_url?: { url: string } }> = [] + for (const block of content) { + switch (block.type) { + case 'text': + parts.push({ type: 'text', text: block.text ?? '' }) + break + case 'image': { + const src = block.source + if (src?.type === 'base64') { + parts.push({ + type: 'image_url', + image_url: { + url: `data:${src.media_type};base64,${src.data}`, + }, + }) + } else if (src?.type === 'url') { + parts.push({ type: 'image_url', image_url: { url: src.url } }) + } + break + } + case 'tool_use': + // handled separately + break + case 'tool_result': + // handled separately + break + case 'thinking': + // Append thinking as text with a marker for models that support reasoning + if (block.thinking) { + parts.push({ type: 'text', text: `${block.thinking}` }) + } + break + default: + if (block.text) { + parts.push({ type: 'text', text: block.text }) + } + } + } + + if (parts.length === 0) return '' + if (parts.length === 1 && parts[0].type === 'text') return parts[0].text ?? '' + return parts +} + +function convertMessages( + messages: Array<{ role: string; message?: { role?: string; content?: unknown }; content?: unknown }>, + system: unknown, +): OpenAIMessage[] { + const result: OpenAIMessage[] = [] + + // System message first + const sysText = convertSystemPrompt(system) + if (sysText) { + result.push({ role: 'system', content: sysText }) + } + + for (const msg of messages) { + // Claude Code wraps messages in { role, message: { role, content } } + const inner = msg.message ?? msg + const role = (inner as { role?: string }).role ?? msg.role + const content = (inner as { content?: unknown }).content + + if (role === 'user') { + // Check for tool_result blocks in user messages + if (Array.isArray(content)) { + const toolResults = content.filter((b: { type?: string }) => b.type === 'tool_result') + const otherContent = content.filter((b: { type?: string }) => b.type !== 'tool_result') + + // Emit tool results as tool messages + for (const tr of toolResults) { + const trContent = Array.isArray(tr.content) + ? tr.content.map((c: { text?: string }) => c.text ?? '').join('\n') + : typeof tr.content === 'string' + ? tr.content + : JSON.stringify(tr.content ?? '') + result.push({ + role: 'tool', + tool_call_id: tr.tool_use_id ?? 'unknown', + content: tr.is_error ? `Error: ${trContent}` : trContent, + }) + } + + // Emit remaining user content + if (otherContent.length > 0) { + result.push({ + role: 'user', + content: convertContentBlocks(otherContent), + }) + } + } else { + result.push({ + role: 'user', + content: convertContentBlocks(content), + }) + } + } else if (role === 'assistant') { + // Check for tool_use blocks + if (Array.isArray(content)) { + const toolUses = content.filter((b: { type?: string }) => b.type === 'tool_use') + const textContent = content.filter( + (b: { type?: string }) => b.type !== 'tool_use' && b.type !== 'thinking', + ) + + const assistantMsg: OpenAIMessage = { + role: 'assistant', + content: convertContentBlocks(textContent) as string, + } + + if (toolUses.length > 0) { + assistantMsg.tool_calls = toolUses.map( + (tu: { + id?: string + name?: string + input?: unknown + extra_content?: Record + }) => ({ + id: tu.id ?? `call_${Math.random().toString(36).slice(2)}`, + type: 'function' as const, + function: { + name: tu.name ?? 'unknown', + arguments: + typeof tu.input === 'string' + ? tu.input + : JSON.stringify(tu.input ?? {}), + }, + ...(tu.extra_content ? { extra_content: tu.extra_content } : {}), + }), + ) + } + + result.push(assistantMsg) + } else { + result.push({ + role: 'assistant', + content: convertContentBlocks(content) as string, + }) + } + } + } + + return result +} + +/** + * OpenAI requires every key in `properties` to also appear in `required`. + * Anthropic schemas often mark fields as optional (omitted from `required`), + * which causes 400 errors on OpenAI/Codex endpoints. This normalizes the + * schema by ensuring `required` is a superset of `properties` keys. + */ +function normalizeSchemaForOpenAI( + schema: Record, + strict = true, +): Record { + if (schema.type !== 'object' || !schema.properties) return schema + const properties = schema.properties as Record + const existingRequired = Array.isArray(schema.required) ? schema.required as string[] : [] + // OpenAI strict mode requires every property to be listed in required[]. + // Gemini rejects schemas where required[] contains keys absent from properties, + // so only promote keys that actually exist in properties. + if (strict) { + const allKeys = Object.keys(properties) + const required = Array.from(new Set([...existingRequired, ...allKeys])) + return { ...schema, required } + } + // For Gemini: keep only existing required keys that are present in properties + const required = existingRequired.filter(k => k in properties) + return { ...schema, required } +} + +function convertTools( + tools: Array<{ name: string; description?: string; input_schema?: Record }>, +): OpenAITool[] { + return tools + .filter(t => t.name !== 'ToolSearchTool') // Not relevant for OpenAI + .map(t => { + const schema = { ...(t.input_schema ?? { type: 'object', properties: {} }) } as Record + + // For Codex/OpenAI: promote known Agent sub-fields into required[] only if + // they actually exist in properties (Gemini rejects required keys absent from properties). + if (t.name === 'Agent' && schema.properties) { + const props = schema.properties as Record + if (!Array.isArray(schema.required)) schema.required = [] + const req = schema.required as string[] + for (const key of ['message', 'subagent_type']) { + if (key in props && !req.includes(key)) req.push(key) + } + } + + return { + type: 'function' as const, + function: { + name: t.name, + description: t.description ?? '', + parameters: normalizeSchemaForOpenAI(schema), + }, + } + }) +} + +function convertResponsesToolChoice( + toolChoice: unknown, +): ResponsesToolChoice | undefined { + const choice = toolChoice as { type?: string; name?: string } | undefined + if (!choice?.type) return undefined + if (choice.type === 'auto') return 'auto' + if (choice.type === 'any') return 'required' + if (choice.type === 'none') return 'none' + if (choice.type === 'tool' && choice.name) { + return { + type: 'function', + name: choice.name, + } + } + return undefined +} + +function shouldOmitSamplingParamsForResponses(model: string): boolean { + const normalized = model.toLowerCase() + return ( + normalized.includes('gpt') || + normalized.includes('codex') || + /^o\d/.test(normalized) + ) +} + +function buildResponsesRequestBody( + request: ReturnType, + params: ShimCreateParams, +): Record { + const input = convertAnthropicMessagesToResponsesInput( + params.messages as Array<{ + role?: string + message?: { role?: string; content?: unknown } + content?: unknown + }>, + ) + const body: Record = { + model: request.resolvedModel, + input: + input.length > 0 + ? input + : [ + { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: '' }], + }, + ], + store: false, + stream: true, + } + + const instructions = convertSystemPrompt(params.system) + if (instructions) { + body.instructions = instructions + } + + if (params.max_tokens > 0) { + body.max_output_tokens = params.max_tokens + } + + const toolChoice = convertResponsesToolChoice(params.tool_choice) + if (toolChoice) { + body.tool_choice = toolChoice + } + + if (params.tools && params.tools.length > 0) { + const convertedTools = convertToolsToResponsesTools( + params.tools as Array<{ + name?: string + description?: string + input_schema?: Record + }>, + ) + if (convertedTools.length > 0) { + body.tools = convertedTools + body.parallel_tool_calls = true + body.tool_choice ??= 'auto' + } + } + + if (request.reasoning) { + body.reasoning = request.reasoning + } + + if (params.metadata !== undefined) { + body.metadata = params.metadata + } + + if (!shouldOmitSamplingParamsForResponses(request.resolvedModel)) { + if (params.temperature !== undefined) { + body.temperature = params.temperature + } + if (params.top_p !== undefined) { + body.top_p = params.top_p + } + } + + return body +} + +// --------------------------------------------------------------------------- +// Streaming: OpenAI SSE → Anthropic stream events +// --------------------------------------------------------------------------- + +interface OpenAIStreamChunk { + id: string + object: string + model: string + choices: Array<{ + index: number + delta: { + role?: string + content?: string | null + tool_calls?: Array<{ + index: number + id?: string + type?: string + function?: { name?: string; arguments?: string } + extra_content?: Record + }> + } + finish_reason: string | null + }> + usage?: { + prompt_tokens?: number + completion_tokens?: number + total_tokens?: number + } +} + +function makeMessageId(): string { + return `msg_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}` +} + +function convertChunkUsage( + usage: OpenAIStreamChunk['usage'] | undefined, +): Partial | undefined { + if (!usage) return undefined + + return { + input_tokens: usage.prompt_tokens ?? 0, + output_tokens: usage.completion_tokens ?? 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + } +} + +/** + * Async generator that transforms an OpenAI SSE stream into + * Anthropic-format BetaRawMessageStreamEvent objects. + */ +async function* openaiStreamToAnthropic( + response: Response, + model: string, +): AsyncGenerator { + const messageId = makeMessageId() + let contentBlockIndex = 0 + const activeToolCalls = new Map() + let hasEmittedContentStart = false + let lastStopReason: 'tool_use' | 'max_tokens' | 'end_turn' | null = null + let hasEmittedFinalUsage = false + let hasProcessedFinishReason = false + + // Emit message_start + yield { + type: 'message_start', + message: { + id: messageId, + type: 'message', + role: 'assistant', + content: [], + model, + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + }, + } + + const reader = response.body?.getReader() + if (!reader) return + + const decoder = new TextDecoder() + let buffer = '' + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() ?? '' + + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed || trimmed === 'data: [DONE]') continue + if (!trimmed.startsWith('data: ')) continue + + let chunk: OpenAIStreamChunk + try { + chunk = JSON.parse(trimmed.slice(6)) + } catch { + continue + } + + const chunkUsage = convertChunkUsage(chunk.usage) + + for (const choice of chunk.choices ?? []) { + const delta = choice.delta + + // Text content — use != null to distinguish absent field from empty string, + // some providers send "" as first delta to signal streaming start + if (delta.content != null) { + if (!hasEmittedContentStart) { + yield { + type: 'content_block_start', + index: contentBlockIndex, + content_block: { type: 'text', text: '' }, + } + hasEmittedContentStart = true + } + yield { + type: 'content_block_delta', + index: contentBlockIndex, + delta: { type: 'text_delta', text: delta.content }, + } + } + + // Tool calls + if (delta.tool_calls) { + for (const tc of delta.tool_calls) { + if (tc.id && tc.function?.name) { + // New tool call starting + if (hasEmittedContentStart) { + yield { + type: 'content_block_stop', + index: contentBlockIndex, + } + contentBlockIndex++ + hasEmittedContentStart = false + } + + const toolBlockIndex = contentBlockIndex + activeToolCalls.set(tc.index, { + id: tc.id, + name: tc.function.name, + index: toolBlockIndex, + }) + + yield { + type: 'content_block_start', + index: toolBlockIndex, + content_block: { + type: 'tool_use', + id: tc.id, + name: tc.function.name, + input: {}, + ...(tc.extra_content ? { extra_content: tc.extra_content } : {}), + }, + } + contentBlockIndex++ + + // Emit any initial arguments + if (tc.function.arguments) { + yield { + type: 'content_block_delta', + index: toolBlockIndex, + delta: { + type: 'input_json_delta', + partial_json: tc.function.arguments, + }, + } + } + } else if (tc.function?.arguments) { + // Continuation of existing tool call + const active = activeToolCalls.get(tc.index) + if (active) { + yield { + type: 'content_block_delta', + index: active.index, + delta: { + type: 'input_json_delta', + partial_json: tc.function.arguments, + }, + } + } + } + } + } + + // Finish — guard ensures we only process finish_reason once even if + // multiple chunks arrive with finish_reason set (some providers do this) + if (choice.finish_reason && !hasProcessedFinishReason) { + hasProcessedFinishReason = true + + // Close any open content blocks + if (hasEmittedContentStart) { + yield { + type: 'content_block_stop', + index: contentBlockIndex, + } + } + // Close active tool calls + for (const [, tc] of activeToolCalls) { + yield { type: 'content_block_stop', index: tc.index } + } + + const stopReason = + choice.finish_reason === 'tool_calls' + ? 'tool_use' + : choice.finish_reason === 'length' + ? 'max_tokens' + : 'end_turn' + lastStopReason = stopReason + + yield { + type: 'message_delta', + delta: { stop_reason: stopReason, stop_sequence: null }, + ...(chunkUsage ? { usage: chunkUsage } : {}), + } + if (chunkUsage) { + hasEmittedFinalUsage = true + } + } + } + + if ( + !hasEmittedFinalUsage && + chunkUsage && + (chunk.choices?.length ?? 0) === 0 + ) { + yield { + type: 'message_delta', + delta: { stop_reason: lastStopReason, stop_sequence: null }, + usage: chunkUsage, + } + hasEmittedFinalUsage = true + } + } + } + + yield { type: 'message_stop' } +} + +// --------------------------------------------------------------------------- +// The shim client — duck-types as Anthropic SDK +// --------------------------------------------------------------------------- + +class OpenAIShimStream { + private generator: AsyncGenerator + // The controller property is checked by claude.ts to distinguish streams from error messages + controller = new AbortController() + + constructor(generator: AsyncGenerator) { + this.generator = generator + } + + async *[Symbol.asyncIterator]() { + yield* this.generator + } +} + +class OpenAIShimMessages { + private defaultHeaders: Record + + constructor(defaultHeaders: Record) { + this.defaultHeaders = defaultHeaders + } + + create( + params: ShimCreateParams, + options?: { signal?: AbortSignal; headers?: Record }, + ) { + const self = this + + const promise = (async () => { + const request = resolveProviderRequest({ model: params.model }) + const response = await self._doRequest(request, params, options) + const usesResponsesTransport = request.transport === 'responses' + + if (params.stream) { + return new OpenAIShimStream( + usesResponsesTransport + ? codexStreamToAnthropic(response, request.resolvedModel) + : openaiStreamToAnthropic(response, request.resolvedModel), + ) + } + + if (usesResponsesTransport) { + const data = await collectCodexCompletedResponse(response) + return convertCodexResponseToAnthropicMessage( + data, + request.resolvedModel, + ) + } + + const data = await response.json() + return self._convertNonStreamingResponse(data, request.resolvedModel) + })() + + ; (promise as unknown as Record).withResponse = + async () => { + const data = await promise + return { + data, + response: new Response(), + request_id: makeMessageId(), + } + } + + return promise + } + + private async _doRequest( + request: ReturnType, + params: ShimCreateParams, + options?: { signal?: AbortSignal; headers?: Record }, + ): Promise { + if (request.transport === 'responses') { + if (request.backend === 'codex') { + const credentials = resolveCodexApiCredentials() + if (!credentials.apiKey) { + const authHint = credentials.authPath + ? ` or place a Codex auth.json at ${credentials.authPath}` + : '' + throw new Error( + `Codex auth is required for ${request.requestedModel}. Set CODEX_API_KEY${authHint}.`, + ) + } + if (!credentials.accountId) { + throw new Error( + 'Codex auth is missing chatgpt_account_id. Re-login with the Codex CLI or set CHATGPT_ACCOUNT_ID/CODEX_ACCOUNT_ID.', + ) + } + + return performCodexRequest({ + request, + credentials, + params, + defaultHeaders: { + ...this.defaultHeaders, + ...(options?.headers ?? {}), + }, + signal: options?.signal, + }) + } + + return this._doResponsesRequest(request, params, options) + } + + return this._doOpenAIRequest(request, params, options) + } + + private async _doResponsesRequest( + request: ReturnType, + params: ShimCreateParams, + options?: { signal?: AbortSignal; headers?: Record }, + ): Promise { + const body = buildResponsesRequestBody(request, params) + const headers: Record = { + 'Content-Type': 'application/json', + ...this.defaultHeaders, + ...(options?.headers ?? {}), + } + + const apiKey = process.env.OPENAI_API_KEY ?? '' + const isAzure = /cognitiveservices\.azure\.com|openai\.azure\.com/.test(request.baseUrl) + + if (apiKey) { + if (isAzure) { + headers['api-key'] = apiKey + } else { + headers.Authorization = `Bearer ${apiKey}` + } + } + + let responsesUrl: string + if (isAzure) { + const apiVersion = process.env.AZURE_OPENAI_API_VERSION ?? '2025-03-01-preview' + const deployment = request.resolvedModel ?? process.env.OPENAI_MODEL ?? 'gpt-4o' + if (/\/deployments\//i.test(request.baseUrl)) { + const base = request.baseUrl.replace(/\/+$/, '') + responsesUrl = `${base}/responses?api-version=${apiVersion}` + } else { + const base = request.baseUrl.replace(/\/(openai\/)?v1\/?$/, '').replace(/\/+$/, '') + responsesUrl = `${base}/openai/deployments/${deployment}/responses?api-version=${apiVersion}` + } + } else { + responsesUrl = `${request.baseUrl}/responses` + } + + const response = await fetch(responsesUrl, { + method: 'POST', + headers, + body: JSON.stringify(body), + signal: options?.signal, + }) + + if (!response.ok) { + const errorBody = await response.text().catch(() => 'unknown error') + throw new Error(`OpenAI Responses API error ${response.status}: ${errorBody}`) + } + + return response + } + + private async _doOpenAIRequest( + request: ReturnType, + params: ShimCreateParams, + options?: { signal?: AbortSignal; headers?: Record }, + ): Promise { + const openaiMessages = convertMessages( + params.messages as Array<{ + role: string + message?: { role?: string; content?: unknown } + content?: unknown + }>, + params.system, + ) + + const body: Record = { + model: request.resolvedModel, + messages: openaiMessages, + stream: params.stream ?? false, + } + // Convert max_tokens to max_completion_tokens for OpenAI API compatibility. + // Azure OpenAI requires max_completion_tokens and does not accept max_tokens. + // Ensure max_tokens is a valid positive number before using it. + const maxTokensValue = typeof params.max_tokens === 'number' && params.max_tokens > 0 + ? params.max_tokens + : undefined + const maxCompletionTokensValue = typeof (params as Record).max_completion_tokens === 'number' + ? (params as Record).max_completion_tokens as number + : undefined + + if (maxTokensValue !== undefined) { + body.max_completion_tokens = maxTokensValue + } else if (maxCompletionTokensValue !== undefined) { + body.max_completion_tokens = maxCompletionTokensValue + } + + if (params.stream) { + body.stream_options = { include_usage: true } + } + + if (params.temperature !== undefined) body.temperature = params.temperature + if (params.top_p !== undefined) body.top_p = params.top_p + + if (params.tools && params.tools.length > 0) { + const converted = convertTools( + params.tools as Array<{ + name: string + description?: string + input_schema?: Record + }>, + ) + if (converted.length > 0) { + body.tools = converted + if (params.tool_choice) { + const tc = params.tool_choice as { type?: string; name?: string } + if (tc.type === 'auto') { + body.tool_choice = 'auto' + } else if (tc.type === 'tool' && tc.name) { + body.tool_choice = { + type: 'function', + function: { name: tc.name }, + } + } else if (tc.type === 'any') { + body.tool_choice = 'required' + } else if (tc.type === 'none') { + body.tool_choice = 'none' + } + } + } + } + + const headers: Record = { + 'Content-Type': 'application/json', + ...this.defaultHeaders, + ...(options?.headers ?? {}), + } + + const apiKey = process.env.OPENAI_API_KEY ?? '' + const isAzure = /cognitiveservices\.azure\.com|openai\.azure\.com/.test(request.baseUrl) + + if (apiKey) { + if (isAzure) { + // Azure uses api-key header instead of Bearer token + headers['api-key'] = apiKey + } else { + headers.Authorization = `Bearer ${apiKey}` + } + } + + // Build the chat completions URL + // Azure Cognitive Services / Azure OpenAI require a deployment-specific path + // and an api-version query parameter. + // Standard format: {base}/openai/deployments/{model}/chat/completions?api-version={version} + // Non-Azure: {base}/chat/completions + let chatCompletionsUrl: string + if (isAzure) { + const apiVersion = process.env.AZURE_OPENAI_API_VERSION ?? '2024-12-01-preview' + const deployment = request.resolvedModel ?? process.env.OPENAI_MODEL ?? 'gpt-4o' + // If base URL already contains /deployments/, use it as-is with api-version + if (/\/deployments\//i.test(request.baseUrl)) { + const base = request.baseUrl.replace(/\/+$/, '') + chatCompletionsUrl = `${base}/chat/completions?api-version=${apiVersion}` + } else { + // Strip trailing /v1 or /openai/v1 if present, then build Azure path + const base = request.baseUrl.replace(/\/(openai\/)?v1\/?$/, '').replace(/\/+$/, '') + chatCompletionsUrl = `${base}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}` + } + } else { + chatCompletionsUrl = `${request.baseUrl}/chat/completions` + } + + const response = await fetch(chatCompletionsUrl, { + method: 'POST', + headers, + body: JSON.stringify(body), + signal: options?.signal, + }) + + if (!response.ok) { + const errorBody = await response.text().catch(() => 'unknown error') + throw new Error(`OpenAI API error ${response.status}: ${errorBody}`) + } + + return response + } + + private _convertNonStreamingResponse( + data: { + id?: string + model?: string + choices?: Array<{ + message?: { + role?: string + content?: string | null + tool_calls?: Array<{ + id: string + function: { name: string; arguments: string } + extra_content?: Record + }> + } + finish_reason?: string + }> + usage?: { + prompt_tokens?: number + completion_tokens?: number + } + }, + model: string, + ) { + const choice = data.choices?.[0] + const content: Array> = [] + + if (choice?.message?.content) { + content.push({ type: 'text', text: choice.message.content }) + } + + if (choice?.message?.tool_calls) { + for (const tc of choice.message.tool_calls) { + let input: unknown + try { + input = JSON.parse(tc.function.arguments) + } catch { + input = { raw: tc.function.arguments } + } + content.push({ + type: 'tool_use', + id: tc.id, + name: tc.function.name, + input, + ...(tc.extra_content ? { extra_content: tc.extra_content } : {}), + }) + } + } + + const stopReason = + choice?.finish_reason === 'tool_calls' + ? 'tool_use' + : choice?.finish_reason === 'length' + ? 'max_tokens' + : 'end_turn' + + return { + id: data.id ?? makeMessageId(), + type: 'message', + role: 'assistant', + content, + model: data.model ?? model, + stop_reason: stopReason, + stop_sequence: null, + usage: { + input_tokens: data.usage?.prompt_tokens ?? 0, + output_tokens: data.usage?.completion_tokens ?? 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + } + } +} + +class OpenAIShimBeta { + messages: OpenAIShimMessages + + constructor(defaultHeaders: Record) { + this.messages = new OpenAIShimMessages(defaultHeaders) + } +} + +export function createOpenAIShimClient(options: { + defaultHeaders?: Record + maxRetries?: number + timeout?: number +}): unknown { + const beta = new OpenAIShimBeta({ + ...(options.defaultHeaders ?? {}), + }) + + return { + beta, + messages: beta.messages, + } +} diff --git a/src/services/api/promptCacheBreakDetection.ts b/src/services/api/promptCacheBreakDetection.ts index 1599d537..3858ea10 100644 --- a/src/services/api/promptCacheBreakDetection.ts +++ b/src/services/api/promptCacheBreakDetection.ts @@ -14,7 +14,7 @@ import type { QuerySource } from '../../constants/querySource.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' function getCacheBreakDiffPath(): string { const chars = 'abcdefghijklmnopqrstuvwxyz0123456789' diff --git a/src/services/api/providerConfig.ts b/src/services/api/providerConfig.ts new file mode 100644 index 00000000..d1036f9b --- /dev/null +++ b/src/services/api/providerConfig.ts @@ -0,0 +1,367 @@ +import { existsSync, readFileSync } from 'node:fs' +import { homedir } from 'node:os' +import { join } from 'node:path' + +export const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1' +export const DEFAULT_CODEX_BASE_URL = 'https://chatgpt.com/backend-api/codex' + +const CODEX_ALIAS_MODELS: Record< + string, + { + model: string + reasoningEffort?: ReasoningEffort + } +> = { + codexplan: { + model: 'gpt-5.4', + reasoningEffort: 'high', + }, + codexspark: { + model: 'gpt-5.3-codex-spark', + }, +} as const + +type CodexAlias = keyof typeof CODEX_ALIAS_MODELS +type ReasoningEffort = 'low' | 'medium' | 'high' + +export type ProviderTransport = 'chat_completions' | 'responses' + +export type ProviderBackend = 'openai' | 'codex' + +export type ResolvedProviderRequest = { + transport: ProviderTransport + backend: ProviderBackend + requestedModel: string + resolvedModel: string + baseUrl: string + reasoning?: { + effort: ReasoningEffort + } +} + +export type ResolvedCodexCredentials = { + apiKey: string + accountId?: string + authPath?: string + source: 'env' | 'auth.json' | 'none' +} + +type ModelDescriptor = { + raw: string + baseModel: string + reasoning?: { + effort: ReasoningEffort + } +} + +const LOCALHOST_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']) + +function asTrimmedString(value: unknown): string | undefined { + return typeof value === 'string' && value.trim() ? value.trim() : undefined +} + +function readNestedString( + value: unknown, + paths: string[][], +): string | undefined { + for (const path of paths) { + let current = value + let valid = true + for (const key of path) { + if (!current || typeof current !== 'object' || !(key in current)) { + valid = false + break + } + current = (current as Record)[key] + } + if (!valid) continue + const stringValue = asTrimmedString(current) + if (stringValue) return stringValue + } + return undefined +} + +function decodeJwtPayload(token: string): Record | undefined { + const parts = token.split('.') + if (parts.length < 2) return undefined + + try { + const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/') + const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4) + const json = Buffer.from(padded, 'base64').toString('utf8') + const parsed = JSON.parse(json) + return parsed && typeof parsed === 'object' + ? (parsed as Record) + : undefined + } catch { + return undefined + } +} + +function parseReasoningEffort(value: string | undefined): ReasoningEffort | undefined { + if (!value) return undefined + const normalized = value.trim().toLowerCase() + if (normalized === 'low' || normalized === 'medium' || normalized === 'high') { + return normalized + } + return undefined +} + +function parseModelDescriptor(model: string): ModelDescriptor { + const trimmed = model.trim() + const queryIndex = trimmed.indexOf('?') + if (queryIndex === -1) { + const alias = trimmed.toLowerCase() as CodexAlias + const aliasConfig = CODEX_ALIAS_MODELS[alias] + if (aliasConfig) { + return { + raw: trimmed, + baseModel: aliasConfig.model, + reasoning: aliasConfig.reasoningEffort + ? { effort: aliasConfig.reasoningEffort } + : undefined, + } + } + return { + raw: trimmed, + baseModel: trimmed, + } + } + + const baseModel = trimmed.slice(0, queryIndex).trim() + const params = new URLSearchParams(trimmed.slice(queryIndex + 1)) + const alias = baseModel.toLowerCase() as CodexAlias + const aliasConfig = CODEX_ALIAS_MODELS[alias] + const resolvedBaseModel = aliasConfig?.model ?? baseModel + const reasoning = + parseReasoningEffort(params.get('reasoning') ?? undefined) ?? + (aliasConfig?.reasoningEffort + ? { effort: aliasConfig.reasoningEffort } + : undefined) + + return { + raw: trimmed, + baseModel: resolvedBaseModel, + reasoning: typeof reasoning === 'string' ? { effort: reasoning } : reasoning, + } +} + +function isCodexAlias(model: string): boolean { + const normalized = model.trim().toLowerCase() + const base = normalized.split('?', 1)[0] ?? normalized + return base in CODEX_ALIAS_MODELS +} + +export function isLocalProviderUrl(baseUrl: string | undefined): boolean { + if (!baseUrl) return false + try { + return LOCALHOST_HOSTNAMES.has(new URL(baseUrl).hostname) + } catch { + return false + } +} + +export function isCodexBaseUrl(baseUrl: string | undefined): boolean { + if (!baseUrl) return false + try { + const parsed = new URL(baseUrl) + return ( + parsed.hostname === 'chatgpt.com' && + parsed.pathname.replace(/\/+$/, '') === '/backend-api/codex' + ) + } catch { + return false + } +} + +function isOfficialOpenAIBaseUrl(baseUrl: string | undefined): boolean { + const candidate = baseUrl ?? DEFAULT_OPENAI_BASE_URL + try { + return new URL(candidate).hostname === 'api.openai.com' + } catch { + return false + } +} + +function getConfiguredOpenAITransport( + env: NodeJS.ProcessEnv = process.env, +): Extract | undefined { + const explicitMode = asTrimmedString(env.OPENAI_API_MODE)?.toLowerCase() + if (explicitMode === 'chat_completions' || explicitMode === 'responses') { + return explicitMode + } + + return undefined +} + +function shouldUseOpenAIResponsesApi(options: { + baseUrl?: string + descriptor: ModelDescriptor + env?: NodeJS.ProcessEnv +}): boolean { + const configuredTransport = getConfiguredOpenAITransport(options.env) + if (configuredTransport) { + return configuredTransport === 'responses' + } + + if (!isOfficialOpenAIBaseUrl(options.baseUrl)) { + return false + } + + if (options.descriptor.reasoning) { + return true + } + + return false +} + +export function resolveProviderRequest(options?: { + model?: string + baseUrl?: string + fallbackModel?: string +}): ResolvedProviderRequest { + const requestedModel = + options?.model?.trim() || + process.env.OPENAI_MODEL?.trim() || + options?.fallbackModel?.trim() || + 'gpt-4o' + const descriptor = parseModelDescriptor(requestedModel) + const rawBaseUrl = + options?.baseUrl ?? + process.env.OPENAI_BASE_URL ?? + process.env.OPENAI_API_BASE ?? + undefined + const backend: ProviderBackend = + isCodexAlias(requestedModel) || isCodexBaseUrl(rawBaseUrl) + ? 'codex' + : 'openai' + const transport: ProviderTransport = + backend === 'codex' + ? 'responses' + : shouldUseOpenAIResponsesApi({ + baseUrl: rawBaseUrl, + descriptor, + }) + ? 'responses' + : 'chat_completions' + + return { + transport, + backend, + requestedModel, + resolvedModel: descriptor.baseModel, + baseUrl: + (rawBaseUrl ?? + (backend === 'codex' + ? DEFAULT_CODEX_BASE_URL + : DEFAULT_OPENAI_BASE_URL) + ).replace(/\/+$/, ''), + reasoning: descriptor.reasoning, + } +} + +export function resolveCodexAuthPath( + env: NodeJS.ProcessEnv = process.env, +): string { + const explicit = asTrimmedString(env.CODEX_AUTH_JSON_PATH) + if (explicit) return explicit + + const codexHome = asTrimmedString(env.CODEX_HOME) + if (codexHome) return join(codexHome, 'auth.json') + + return join(homedir(), '.codex', 'auth.json') +} + +export function parseChatgptAccountId( + token: string | undefined, +): string | undefined { + if (!token) return undefined + const payload = decodeJwtPayload(token) + const fromClaim = asTrimmedString( + payload?.['https://api.openai.com/auth.chatgpt_account_id'], + ) + if (fromClaim) return fromClaim + return asTrimmedString(payload?.chatgpt_account_id) +} + +function loadCodexAuthJson( + authPath: string, +): Record | undefined { + if (!existsSync(authPath)) return undefined + try { + const raw = readFileSync(authPath, 'utf8') + const parsed = JSON.parse(raw) + return parsed && typeof parsed === 'object' + ? (parsed as Record) + : undefined + } catch { + return undefined + } +} + +export function resolveCodexApiCredentials( + env: NodeJS.ProcessEnv = process.env, +): ResolvedCodexCredentials { + const envApiKey = asTrimmedString(env.CODEX_API_KEY) + const envAccountId = + asTrimmedString(env.CODEX_ACCOUNT_ID) ?? + asTrimmedString(env.CHATGPT_ACCOUNT_ID) + + if (envApiKey) { + return { + apiKey: envApiKey, + accountId: envAccountId ?? parseChatgptAccountId(envApiKey), + source: 'env', + } + } + + const authPath = resolveCodexAuthPath(env) + const authJson = loadCodexAuthJson(authPath) + if (!authJson) { + return { + apiKey: '', + authPath, + source: 'none', + } + } + + const apiKey = readNestedString(authJson, [ + ['access_token'], + ['accessToken'], + ['tokens', 'access_token'], + ['tokens', 'accessToken'], + ['auth', 'access_token'], + ['auth', 'accessToken'], + ['token', 'access_token'], + ['token', 'accessToken'], + ['tokens', 'id_token'], + ['tokens', 'idToken'], + ]) + const accountId = + envAccountId ?? + readNestedString(authJson, [ + ['account_id'], + ['accountId'], + ['tokens', 'account_id'], + ['tokens', 'accountId'], + ['auth', 'account_id'], + ['auth', 'accountId'], + ]) ?? + parseChatgptAccountId(apiKey) + + if (!apiKey) { + return { + apiKey: '', + accountId, + authPath, + source: 'none', + } + } + + return { + apiKey, + accountId, + authPath, + source: 'auth.json', + } +} diff --git a/src/services/api/withRetry.ts b/src/services/api/withRetry.ts index 5ec9ad08..c2a8f292 100644 --- a/src/services/api/withRetry.ts +++ b/src/services/api/withRetry.ts @@ -35,11 +35,11 @@ import { isNonCustomOpusModel } from '../../utils/model/model.js' import { disableKeepAlive } from '../../utils/proxy.js' import { sleep } from '../../utils/sleep.js' import type { ThinkingConfig } from '../../utils/thinking.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' import { checkMockRateLimitError, isMockRateLimitError, diff --git a/src/services/autoDream/autoDream.ts b/src/services/autoDream/autoDream.ts index d387d9f6..9a0d24a6 100644 --- a/src/services/autoDream/autoDream.ts +++ b/src/services/autoDream/autoDream.ts @@ -22,8 +22,8 @@ import { import type { Message } from '../../types/message.js' import { logForDebugging } from '../../utils/debug.js' import type { ToolUseContext } from '../../Tool.js' -import { logEvent } from '../analytics/index.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' +import { logEvent } from '../analytics-stub.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' import { isAutoMemoryEnabled, getAutoMemPath } from '../../memdir/paths.js' import { isAutoDreamEnabled } from './config.js' import { getProjectDir } from '../../utils/sessionStorage.js' diff --git a/src/services/autoDream/config.ts b/src/services/autoDream/config.ts index 3ed70ef3..16c55116 100644 --- a/src/services/autoDream/config.ts +++ b/src/services/autoDream/config.ts @@ -3,7 +3,7 @@ // agent / task registry / message builder chain that autoDream.ts pulls in. import { getInitialSettings } from '../../utils/settings/settings.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' /** * Whether background memory consolidation should run. User setting diff --git a/src/services/claudeAiLimits.ts b/src/services/claudeAiLimits.ts index 979f4f72..8a768527 100644 --- a/src/services/claudeAiLimits.ts +++ b/src/services/claudeAiLimits.ts @@ -8,8 +8,8 @@ import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js' import { logError } from '../utils/log.js' import { getSmallFastModel } from '../utils/model/model.js' import { isEssentialTrafficOnly } from '../utils/privacyLevel.js' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from './analytics/index.js' -import { logEvent } from './analytics/index.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from './analytics-stub.js' +import { logEvent } from './analytics-stub.js' import { getAPIMetadata } from './api/claude.js' import { getAnthropicClient } from './api/client.js' import { diff --git a/src/services/compact/autoCompact.ts b/src/services/compact/autoCompact.ts index 4025897c..45f03d5a 100644 --- a/src/services/compact/autoCompact.ts +++ b/src/services/compact/autoCompact.ts @@ -12,7 +12,7 @@ import { hasExactErrorMessage } from '../../utils/errors.js' import type { CacheSafeParams } from '../../utils/forkedAgent.js' import { logError } from '../../utils/log.js' import { tokenCountWithEstimation } from '../../utils/tokens.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' import { getMaxOutputTokensForModel } from '../api/claude.js' import { notifyCompaction } from '../api/promptCacheBreakDetection.js' import { setLastSummarizedMessageId } from '../SessionMemory/sessionMemoryUtils.js' @@ -27,14 +27,23 @@ import { trySessionMemoryCompaction } from './sessionMemoryCompact.js' // Reserve this many tokens for output during compaction // Based on p99.99 of compact summary output being 17,387 tokens. -const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000 +export const DEFAULT_MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000 // Returns the context window size minus the max output tokens for the model export function getEffectiveContextWindowSize(model: string): number { - const reservedTokensForSummary = Math.min( - getMaxOutputTokensForModel(model), - MAX_OUTPUT_TOKENS_FOR_SUMMARY, - ) + // Allow overriding max output tokens for summary via environment variable. + // If the env var is defined, use the provided value directly. + const envSummaryTokens = process.env.CLAUDE_CODE_SUMMARY_OUTPUT_TOKENS + const parsedSummaryTokens = envSummaryTokens + ? parseInt(envSummaryTokens, 10) + : DEFAULT_MAX_OUTPUT_TOKENS_FOR_SUMMARY + const reservedTokensForSummary = envSummaryTokens !== undefined && + !Number.isNaN(parsedSummaryTokens) + ? parsedSummaryTokens + : Math.min( + getMaxOutputTokensForModel(model), + DEFAULT_MAX_OUTPUT_TOKENS_FOR_SUMMARY, + ) let contextWindow = getContextWindowForModel(model, getSdkBetas()) const autoCompactWindow = process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW @@ -59,11 +68,22 @@ export type AutoCompactTrackingState = { consecutiveFailures?: number } -export const AUTOCOMPACT_BUFFER_TOKENS = 13_000 +export const DEFAULT_AUTOCOMPACT_BUFFER_TOKENS = 13_000 export const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000 export const ERROR_THRESHOLD_BUFFER_TOKENS = 20_000 export const MANUAL_COMPACT_BUFFER_TOKENS = 3_000 +export function getAutoCompactBufferTokens(): number { + // Allow overriding autocompact buffer size via environment variable. + // If the env var is defined, use the provided value directly. + const envBuffer = process.env.CLAUDE_CODE_AUTO_COMPACT_BUFFER_TOKENS + const parsed = envBuffer !== undefined ? parseInt(envBuffer, 10) : NaN + if (envBuffer !== undefined && !Number.isNaN(parsed)) { + return parsed + } + return DEFAULT_AUTOCOMPACT_BUFFER_TOKENS +} + // Stop trying autocompact after this many consecutive failures. // BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures (up to 3,272) // in a single session, wasting ~250K API calls/day globally. @@ -72,8 +92,9 @@ const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 export function getAutoCompactThreshold(model: string): number { const effectiveContextWindow = getEffectiveContextWindowSize(model) + const autocompactBufferTokens = getAutoCompactBufferTokens() const autocompactThreshold = - effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS + effectiveContextWindow - autocompactBufferTokens // Override for easier testing of autocompact const envPercent = process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE diff --git a/src/services/compact/compact.ts b/src/services/compact/compact.ts index f8f86eaf..095303d4 100644 --- a/src/services/compact/compact.ts +++ b/src/services/compact/compact.ts @@ -91,11 +91,11 @@ import { extractDiscoveredToolNames, isToolSearchEnabled, } from '../../utils/toolSearch.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' import { getMaxOutputTokensForModel, queryModelWithStreaming, diff --git a/src/services/compact/microCompact.ts b/src/services/compact/microCompact.ts index 5e13587f..f1c3a57e 100644 --- a/src/services/compact/microCompact.ts +++ b/src/services/compact/microCompact.ts @@ -17,7 +17,7 @@ import { jsonStringify } from '../../utils/slowOperations.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' import { notifyCacheDeletion } from '../api/promptCacheBreakDetection.js' import { roughTokenCountEstimation } from '../tokenEstimation.js' import { diff --git a/src/services/compact/postCompactCleanup.ts b/src/services/compact/postCompactCleanup.ts index 2b214728..e42611a5 100644 --- a/src/services/compact/postCompactCleanup.ts +++ b/src/services/compact/postCompactCleanup.ts @@ -6,7 +6,7 @@ import { clearSpeculativeChecks } from '../../tools/BashTool/bashPermissions.js' import { clearClassifierApprovals } from '../../utils/classifierApprovals.js' import { resetGetMemoryFilesCache } from '../../utils/claudemd.js' import { clearSessionMessagesCache } from '../../utils/sessionStorage.js' -import { clearBetaTracingState } from '../../utils/telemetry/betaSessionTracing.js' +import { clearBetaTracingState } from '../../utils/telemetry-stub.js' import { resetMicrocompactState } from './microCompact.js' /** diff --git a/src/services/compact/sessionMemoryCompact.ts b/src/services/compact/sessionMemoryCompact.ts index 9a5f8d25..1e7b4ad9 100644 --- a/src/services/compact/sessionMemoryCompact.ts +++ b/src/services/compact/sessionMemoryCompact.ts @@ -21,8 +21,8 @@ import { extractDiscoveredToolNames } from '../../utils/toolSearch.js' import { getDynamicConfig_BLOCKS_ON_INIT, getFeatureValue_CACHED_MAY_BE_STALE, -} from '../analytics/growthbook.js' -import { logEvent } from '../analytics/index.js' +} from '../analytics-stub.js' +import { logEvent } from '../analytics-stub.js' import { isSessionMemoryEmpty, truncateSessionMemoryForCompact, diff --git a/src/services/compact/timeBasedMCConfig.ts b/src/services/compact/timeBasedMCConfig.ts index 19ae9497..2573e72d 100644 --- a/src/services/compact/timeBasedMCConfig.ts +++ b/src/services/compact/timeBasedMCConfig.ts @@ -1,4 +1,4 @@ -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' /** * GrowthBook config for time-based microcompact. diff --git a/src/services/extractMemories/extractMemories.ts b/src/services/extractMemories/extractMemories.ts index 708d47d7..45bbb7db 100644 --- a/src/services/extractMemories/extractMemories.ts +++ b/src/services/extractMemories/extractMemories.ts @@ -53,9 +53,9 @@ import { createMemorySavedMessage, createUserMessage, } from '../../utils/messages.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' -import { logEvent } from '../analytics/index.js' -import { sanitizeToolNameForAnalytics } from '../analytics/metadata.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' +import { logEvent } from '../analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../analytics-stub.js' import { buildExtractAutoOnlyPrompt, buildExtractCombinedPrompt, diff --git a/src/services/internalLogging.ts b/src/services/internalLogging.ts index c70cad73..895ce92a 100644 --- a/src/services/internalLogging.ts +++ b/src/services/internalLogging.ts @@ -5,7 +5,7 @@ import { jsonStringify } from '../utils/slowOperations.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from './analytics/index.js' +} from '../services/analytics-stub.js' /** * Get the current Kubernetes namespace: diff --git a/src/services/mcp/auth.ts b/src/services/mcp/auth.ts index 61c94636..58d18078 100644 --- a/src/services/mcp/auth.ts +++ b/src/services/mcp/auth.ts @@ -43,8 +43,8 @@ import { clearKeychainCache } from '../../utils/secureStorage/macOsKeychainHelpe import type { SecureStorageData } from '../../utils/secureStorage/types.js' import { sleep } from '../../utils/sleep.js' import { jsonParse, jsonStringify } from '../../utils/slowOperations.js' -import { logEvent } from '../analytics/index.js' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../analytics/metadata.js' +import { logEvent } from '../analytics-stub.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../analytics-stub.js' import { buildRedirectUri, findAvailablePort } from './oauthPort.js' import type { McpHTTPServerConfig, McpSSEServerConfig } from './types.js' import { getLoggingSafeMcpBaseUrl } from './utils.js' diff --git a/src/services/mcp/channelAllowlist.ts b/src/services/mcp/channelAllowlist.ts index bf511e59..73c25197 100644 --- a/src/services/mcp/channelAllowlist.ts +++ b/src/services/mcp/channelAllowlist.ts @@ -18,7 +18,7 @@ import { z } from 'zod/v4' import { lazySchema } from '../../utils/lazySchema.js' import { parsePluginIdentifier } from '../../utils/plugins/pluginIdentifier.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' export type ChannelAllowlistEntry = { marketplace: string diff --git a/src/services/mcp/channelPermissions.ts b/src/services/mcp/channelPermissions.ts index 1a2a65f0..8cd465b2 100644 --- a/src/services/mcp/channelPermissions.ts +++ b/src/services/mcp/channelPermissions.ts @@ -24,7 +24,7 @@ */ import { jsonStringify } from '../../utils/slowOperations.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' /** * GrowthBook runtime gate — separate from the channels gate (tengu_harbor) diff --git a/src/services/mcp/claudeai.ts b/src/services/mcp/claudeai.ts index 132990a2..5be28fe4 100644 --- a/src/services/mcp/claudeai.ts +++ b/src/services/mcp/claudeai.ts @@ -4,7 +4,7 @@ import { getOauthConfig } from 'src/constants/oauth.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../analytics-stub.js' import { getClaudeAIOAuthTokens } from 'src/utils/auth.js' import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js' import { logForDebugging } from 'src/utils/debug.js' diff --git a/src/services/mcp/client.ts b/src/services/mcp/client.ts index bcd06616..c24bd38f 100644 --- a/src/services/mcp/client.ts +++ b/src/services/mcp/client.ts @@ -103,7 +103,7 @@ import { import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' import { type ElicitationWaitingState, runElicitationHooks, diff --git a/src/services/mcp/config.ts b/src/services/mcp/config.ts index c5cee9cd..713cc095 100644 --- a/src/services/mcp/config.ts +++ b/src/services/mcp/config.ts @@ -39,7 +39,7 @@ import { jsonStringify } from '../../utils/slowOperations.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' import { fetchClaudeAIMcpConfigsIfEligible } from './claudeai.js' import { expandEnvVarsInString } from './envExpansion.js' import { diff --git a/src/services/mcp/elicitationHandler.ts b/src/services/mcp/elicitationHandler.ts index 5f71f7b6..6ad04f25 100644 --- a/src/services/mcp/elicitationHandler.ts +++ b/src/services/mcp/elicitationHandler.ts @@ -16,7 +16,7 @@ import { jsonStringify } from '../../utils/slowOperations.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' /** Configuration for the waiting state shown after the user opens a URL. */ export type ElicitationWaitingState = { diff --git a/src/services/mcp/headersHelper.ts b/src/services/mcp/headersHelper.ts index 3ae0e3d9..20b0d5f5 100644 --- a/src/services/mcp/headersHelper.ts +++ b/src/services/mcp/headersHelper.ts @@ -5,7 +5,7 @@ import { errorMessage } from '../../utils/errors.js' import { execFileNoThrowWithCwd } from '../../utils/execFileNoThrow.js' import { logError, logMCPDebug, logMCPError } from '../../utils/log.js' import { jsonParse } from '../../utils/slowOperations.js' -import { logEvent } from '../analytics/index.js' +import { logEvent } from '../analytics-stub.js' import type { McpHTTPServerConfig, McpSSEServerConfig, diff --git a/src/services/mcp/useManageMCPConnections.ts b/src/services/mcp/useManageMCPConnections.ts index 9b550992..38e5041b 100644 --- a/src/services/mcp/useManageMCPConnections.ts +++ b/src/services/mcp/useManageMCPConnections.ts @@ -40,7 +40,7 @@ import reject from 'lodash-es/reject.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../analytics-stub.js' import { dedupClaudeAiMcpServers, doesEnterpriseMcpConfigExist, diff --git a/src/services/mcp/vscodeSdkMcp.ts b/src/services/mcp/vscodeSdkMcp.ts index a15329cf..87a450a2 100644 --- a/src/services/mcp/vscodeSdkMcp.ts +++ b/src/services/mcp/vscodeSdkMcp.ts @@ -4,8 +4,8 @@ import { lazySchema } from '../../utils/lazySchema.js' import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE, getFeatureValue_CACHED_MAY_BE_STALE, -} from '../analytics/growthbook.js' -import { logEvent } from '../analytics/index.js' +} from '../analytics-stub.js' +import { logEvent } from '../analytics-stub.js' import type { ConnectedMCPServer, MCPServerConnection } from './types.js' // Mirror of AutoModeEnabledState in permissionSetup.ts — inlined because that diff --git a/src/services/notifier.ts b/src/services/notifier.ts index 330e16a0..abb5e128 100644 --- a/src/services/notifier.ts +++ b/src/services/notifier.ts @@ -7,7 +7,7 @@ import { logError } from '../utils/log.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from './analytics/index.js' +} from './analytics-stub.js' export type NotificationOptions = { message: string diff --git a/src/services/oauth/auth-code-listener.ts b/src/services/oauth/auth-code-listener.ts index a46c33ec..a566a0a9 100644 --- a/src/services/oauth/auth-code-listener.ts +++ b/src/services/oauth/auth-code-listener.ts @@ -1,7 +1,7 @@ import type { IncomingMessage, ServerResponse } from 'http' import { createServer, type Server } from 'http' import type { AddressInfo } from 'net' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../analytics-stub.js' import { getOauthConfig } from '../../constants/oauth.js' import { logError } from '../../utils/log.js' import { shouldUseClaudeAIAuth } from './client.js' diff --git a/src/services/oauth/client.ts b/src/services/oauth/client.ts index 76bfdce1..3ae80f54 100644 --- a/src/services/oauth/client.ts +++ b/src/services/oauth/client.ts @@ -3,7 +3,7 @@ import axios from 'axios' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../analytics-stub.js' import { ALL_OAUTH_SCOPES, CLAUDE_AI_INFERENCE_SCOPE, diff --git a/src/services/oauth/index.ts b/src/services/oauth/index.ts index fdd8e5f7..b64fc1a5 100644 --- a/src/services/oauth/index.ts +++ b/src/services/oauth/index.ts @@ -1,4 +1,4 @@ -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../analytics-stub.js' import { openBrowser } from '../../utils/browser.js' import { AuthCodeListener } from './auth-code-listener.js' import * as client from './client.js' diff --git a/src/services/plugins/PluginInstallationManager.ts b/src/services/plugins/PluginInstallationManager.ts index 67b0c1e1..0dbf7eff 100644 --- a/src/services/plugins/PluginInstallationManager.ts +++ b/src/services/plugins/PluginInstallationManager.ts @@ -20,7 +20,7 @@ import { reconcileMarketplaces, } from '../../utils/plugins/reconciler.js' import { refreshActivePlugins } from '../../utils/plugins/refresh.js' -import { logEvent } from '../analytics/index.js' +import { logEvent } from '../analytics-stub.js' type SetAppState = (f: (prevState: AppState) => AppState) => void diff --git a/src/services/plugins/pluginCliCommands.ts b/src/services/plugins/pluginCliCommands.ts index 514a4143..8558d5bf 100644 --- a/src/services/plugins/pluginCliCommands.ts +++ b/src/services/plugins/pluginCliCommands.ts @@ -17,12 +17,12 @@ import { writeToStdout } from '../../utils/process.js' import { buildPluginTelemetryFields, classifyPluginCommandError, -} from '../../utils/telemetry/pluginTelemetry.js' +} from '../../utils/telemetry-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' import { disableAllPluginsOp, disablePluginOp, diff --git a/src/services/remoteManagedSettings/securityCheck.tsx b/src/services/remoteManagedSettings/securityCheck.tsx index 0ec8aeb0..8403b64b 100644 --- a/src/services/remoteManagedSettings/securityCheck.tsx +++ b/src/services/remoteManagedSettings/securityCheck.tsx @@ -8,7 +8,7 @@ import { AppStateProvider } from '../../state/AppState.js'; import { gracefulShutdownSync } from '../../utils/gracefulShutdown.js'; import { getBaseRenderOptions } from '../../utils/renderOptions.js'; import type { SettingsJson } from '../../utils/settings/types.js'; -import { logEvent } from '../analytics/index.js'; +import { logEvent } from '../analytics-stub.js'; export type SecurityCheckResult = 'approved' | 'rejected' | 'no_check_needed'; /** diff --git a/src/services/settingsSync/index.ts b/src/services/settingsSync/index.ts index 2d392b75..fcf99789 100644 --- a/src/services/settingsSync/index.ts +++ b/src/services/settingsSync/index.ts @@ -38,8 +38,8 @@ import { getSettingsFilePathForSource } from '../../utils/settings/settings.js' import { resetSettingsCache } from '../../utils/settings/settingsCache.js' import { sleep } from '../../utils/sleep.js' import { getClaudeCodeUserAgent } from '../../utils/userAgent.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' -import { logEvent } from '../analytics/index.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' +import { logEvent } from '../analytics-stub.js' import { getRetryDelay } from '../api/withRetry.js' import { type SettingsSyncFetchResult, diff --git a/src/services/teamMemorySync/index.ts b/src/services/teamMemorySync/index.ts index ef66940d..61fbee56 100644 --- a/src/services/teamMemorySync/index.ts +++ b/src/services/teamMemorySync/index.ts @@ -54,8 +54,8 @@ import { import { sleep } from '../../utils/sleep.js' import { jsonStringify } from '../../utils/slowOperations.js' import { getClaudeCodeUserAgent } from '../../utils/userAgent.js' -import { logEvent } from '../analytics/index.js' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../analytics/metadata.js' +import { logEvent } from '../analytics-stub.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../analytics-stub.js' import { getRetryDelay } from '../api/withRetry.js' import { scanForSecrets } from './secretScanner.js' import { diff --git a/src/services/teamMemorySync/watcher.ts b/src/services/teamMemorySync/watcher.ts index 0d9874e6..0645f376 100644 --- a/src/services/teamMemorySync/watcher.ts +++ b/src/services/teamMemorySync/watcher.ts @@ -22,7 +22,7 @@ import { getGithubRepo } from '../../utils/git.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' import { createSyncState, isTeamMemorySyncAvailable, diff --git a/src/services/tips/tipRegistry.ts b/src/services/tips/tipRegistry.ts index c99edfdc..7177389b 100644 --- a/src/services/tips/tipRegistry.ts +++ b/src/services/tips/tipRegistry.ts @@ -43,7 +43,7 @@ import { getCurrentSessionAgentColor, isCustomTitleEnabled, } from '../../utils/sessionStorage.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics-stub.js' import { formatGrantAmount, getCachedOverageCreditGrant, @@ -143,7 +143,7 @@ const externalTips: Tip[] = [ { id: 'git-worktrees', content: async () => - 'Use git worktrees to run multiple Claude sessions in parallel.', + 'Use git worktrees to run multiple Claudium sessions in parallel.', cooldownSessions: 10, isRelevant: async () => { try { @@ -158,7 +158,7 @@ const externalTips: Tip[] = [ { id: 'color-when-multi-clauding', content: async () => - 'Running multiple Claude sessions? Use /color and /rename to tell them apart at a glance.', + 'Running multiple Claudium sessions? Use /color and /rename to tell them apart at a glance.', cooldownSessions: 10, isRelevant: async () => { if (getCurrentSessionAgentColor()) return false @@ -216,7 +216,7 @@ const externalTips: Tip[] = [ }, { id: 'memory-command', - content: async () => 'Use /memory to view and manage Claude memory', + content: async () => 'Use /memory to view and manage Claudium memory', cooldownSessions: 15, async isRelevant() { const config = getGlobalConfig() @@ -255,7 +255,7 @@ const externalTips: Tip[] = [ { id: 'prompt-queue', content: async () => - 'Hit Enter to queue up additional messages while Claude is working.', + 'Hit Enter to queue up additional messages while Claudium is working.', cooldownSessions: 5, async isRelevant() { const config = getGlobalConfig() @@ -265,14 +265,14 @@ const externalTips: Tip[] = [ { id: 'enter-to-steer-in-relatime', content: async () => - 'Send messages to Claude while it works to steer Claude in real-time', + 'Send messages to Claudium while it works to steer Claudium in real-time', cooldownSessions: 20, isRelevant: async () => true, }, { id: 'todo-list', content: async () => - 'Ask Claude to create a todo list when working on complex tasks to track progress and remain on track', + 'Ask Claudium to create a todo list when working on complex tasks to track progress and remain on track', cooldownSessions: 20, isRelevant: async () => true, }, @@ -376,7 +376,7 @@ const externalTips: Tip[] = [ { id: 'continue', content: async () => - 'Run claude --continue or claude --resume to resume a conversation', + 'Run claudium --continue or claudium --resume to resume a conversation', cooldownSessions: 10, isRelevant: async () => true, }, @@ -521,7 +521,7 @@ const externalTips: Tip[] = [ 'off' | 'copy_a' | 'copy_b' >('tengu_tide_elm', 'off') return variant === 'copy_b' - ? `Use ${cmd} for better one-shot answers. Claude thinks it through first.` + ? `Use ${cmd} for better one-shot answers. Claudium thinks it through first.` : `Working on something tricky? ${cmd} gives better first answers` }, cooldownSessions: 3, @@ -550,8 +550,8 @@ const externalTips: Tip[] = [ 'off' | 'copy_a' | 'copy_b' >('tengu_tern_alloy', 'off') return variant === 'copy_b' - ? `For big tasks, tell Claude to ${blue('use subagents')}. They work in parallel and keep your main thread clean.` - : `Say ${blue('"fan out subagents"')} and Claude sends a team. Each one digs deep so nothing gets missed.` + ? `For big tasks, tell Claudium to ${blue('use subagents')}. They work in parallel and keep your main thread clean.` + : `Say ${blue('"fan out subagents"')} and Claudium sends a team. Each one digs deep so nothing gets missed.` }, cooldownSessions: 3, isRelevant: async () => { diff --git a/src/services/tips/tipScheduler.ts b/src/services/tips/tipScheduler.ts index 2924f601..21009100 100644 --- a/src/services/tips/tipScheduler.ts +++ b/src/services/tips/tipScheduler.ts @@ -2,7 +2,7 @@ import { getSettings_DEPRECATED } from '../../utils/settings/settings.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../analytics/index.js' +} from '../analytics-stub.js' import { getSessionsSinceLastShown, recordTipShown } from './tipHistory.js' import { getRelevantTips } from './tipRegistry.js' import type { Tip, TipContext } from './types.js' diff --git a/src/services/tools/toolExecution.ts b/src/services/tools/toolExecution.ts index 40ef38ec..24d53ecb 100644 --- a/src/services/tools/toolExecution.ts +++ b/src/services/tools/toolExecution.ts @@ -7,7 +7,7 @@ import type { import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../analytics-stub.js' import { extractMcpToolDetails, extractSkillName, @@ -17,7 +17,7 @@ import { isToolDetailsLoggingEnabled, mcpToolDetailsForAnalytics, sanitizeToolNameForAnalytics, -} from 'src/services/analytics/metadata.js' +} from '../analytics-stub.js' import { addToToolDuration, getCodeEditToolDecisionCounter, @@ -87,7 +87,7 @@ import { } from '../../utils/sessionActivity.js' import { jsonStringify } from '../../utils/slowOperations.js' import { Stream } from '../../utils/stream.js' -import { logOTelEvent } from '../../utils/telemetry/events.js' +import { logOTelEvent } from '../../utils/telemetry-stub.js' import { addToolContentEvent, endToolBlockedOnUserSpan, @@ -97,7 +97,7 @@ import { startToolBlockedOnUserSpan, startToolExecutionSpan, startToolSpan, -} from '../../utils/telemetry/sessionTracing.js' +} from '../../utils/telemetry-stub.js' import { formatError, formatZodValidationError, diff --git a/src/services/tools/toolHooks.ts b/src/services/tools/toolHooks.ts index cb2ef8fa..c964316a 100644 --- a/src/services/tools/toolHooks.ts +++ b/src/services/tools/toolHooks.ts @@ -1,8 +1,8 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' -import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js' +} from '../analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../analytics-stub.js' import type z from 'zod/v4' import type { CanUseToolFn } from '../../hooks/useCanUseTool.js' import type { AnyObject, Tool, ToolUseContext } from '../../Tool.js' diff --git a/src/services/voiceStreamSTT.ts b/src/services/voiceStreamSTT.ts index e1c86f78..daedb207 100644 --- a/src/services/voiceStreamSTT.ts +++ b/src/services/voiceStreamSTT.ts @@ -29,7 +29,7 @@ import { jsonParse, jsonStringify } from '../utils/slowOperations.js' const KEEPALIVE_MSG = '{"type":"KeepAlive"}' const CLOSE_STREAM_MSG = '{"type":"CloseStream"}' -import { getFeatureValue_CACHED_MAY_BE_STALE } from './analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from './analytics-stub.js' // ─── Constants ─────────────────────────────────────────────────────── diff --git a/src/setup.ts b/src/setup.ts index 985e8577..ea189608 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -5,7 +5,7 @@ import chalk from 'chalk' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from './services/analytics-stub.js' import { getCwd } from 'src/utils/cwd.js' import { checkForReleaseNotes } from 'src/utils/releaseNotes.js' import { setCwd } from 'src/utils/Shell.js' diff --git a/src/skills/bundled/scheduleRemoteAgents.ts b/src/skills/bundled/scheduleRemoteAgents.ts index 21b2ac69..99b91634 100644 --- a/src/skills/bundled/scheduleRemoteAgents.ts +++ b/src/skills/bundled/scheduleRemoteAgents.ts @@ -1,4 +1,4 @@ -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import type { MCPServerConnection } from '../../services/mcp/types.js' import { isPolicyAllowed } from '../../services/policyLimits/index.js' import type { ToolUseContext } from '../../Tool.js' diff --git a/src/skills/loadSkillsDir.ts b/src/skills/loadSkillsDir.ts index b1138eed..3296cb79 100644 --- a/src/skills/loadSkillsDir.ts +++ b/src/skills/loadSkillsDir.ts @@ -16,7 +16,7 @@ import { import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { roughTokenCountEstimation } from '../services/tokenEstimation.js' import type { Command, PromptCommand } from '../types/command.js' import { diff --git a/src/state/teammateViewHelpers.ts b/src/state/teammateViewHelpers.ts index 2c32f7b1..e157198a 100644 --- a/src/state/teammateViewHelpers.ts +++ b/src/state/teammateViewHelpers.ts @@ -1,4 +1,4 @@ -import { logEvent } from '../services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { isTerminalTaskStatus } from '../Task.js' import type { LocalAgentTaskState } from '../tasks/LocalAgentTask/LocalAgentTask.js' diff --git a/src/tools/AgentTool/AgentTool.tsx b/src/tools/AgentTool/AgentTool.tsx index ee378b7b..8fad9669 100644 --- a/src/tools/AgentTool/AgentTool.tsx +++ b/src/tools/AgentTool/AgentTool.tsx @@ -8,8 +8,8 @@ import { clearInvokedSkillsForAgent, getSdkAgentProgressSummariesEnabled } from import { enhanceSystemPromptWithEnvDetails, getSystemPrompt } from '../../constants/prompts.js'; import { isCoordinatorMode } from '../../coordinator/coordinatorMode.js'; import { startAgentSummarization } from '../../services/AgentSummary/agentSummary.js'; -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { clearDumpState } from '../../services/api/dumpPrompts.js'; import { completeAgentTask as completeAsyncAgent, createActivityDescriptionResolver, createProgressTracker, enqueueAgentNotification, failAgentTask as failAsyncAgent, getProgressUpdate, getTokenCountFromTracker, isLocalAgentTask, killAsyncAgent, registerAgentForeground, registerAsyncAgent, unregisterAgentForeground, updateAgentProgress as updateAsyncAgentProgress, updateProgressFromMessage } from '../../tasks/LocalAgentTask/LocalAgentTask.js'; import { checkRemoteAgentEligibility, formatPreconditionError, getRemoteTaskSessionUrl, registerRemoteAgentTask } from '../../tasks/RemoteAgentTask/RemoteAgentTask.js'; diff --git a/src/tools/AgentTool/agentToolUtils.ts b/src/tools/AgentTool/agentToolUtils.ts index a566ac31..d0b233c1 100644 --- a/src/tools/AgentTool/agentToolUtils.ts +++ b/src/tools/AgentTool/agentToolUtils.ts @@ -11,7 +11,7 @@ import { startAgentSummarization } from '../../services/AgentSummary/agentSummar import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { clearDumpState } from '../../services/api/dumpPrompts.js' import type { AppState } from '../../state/AppState.js' import type { diff --git a/src/tools/AgentTool/builtInAgents.ts b/src/tools/AgentTool/builtInAgents.ts index 721dff96..a9df8c6b 100644 --- a/src/tools/AgentTool/builtInAgents.ts +++ b/src/tools/AgentTool/builtInAgents.ts @@ -1,6 +1,6 @@ import { feature } from 'bun:bundle' import { getIsNonInteractiveSession } from '../../bootstrap/state.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { isEnvTruthy } from '../../utils/envUtils.js' import { CLAUDE_CODE_GUIDE_AGENT } from './built-in/claudeCodeGuideAgent.js' import { EXPLORE_AGENT } from './built-in/exploreAgent.js' diff --git a/src/tools/AgentTool/loadAgentsDir.ts b/src/tools/AgentTool/loadAgentsDir.ts index cb4dc35e..98d6efc9 100644 --- a/src/tools/AgentTool/loadAgentsDir.ts +++ b/src/tools/AgentTool/loadAgentsDir.ts @@ -7,7 +7,7 @@ import { isAutoMemoryEnabled } from '../../memdir/paths.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { type McpServerConfig, McpServerConfigSchema, diff --git a/src/tools/AgentTool/prompt.ts b/src/tools/AgentTool/prompt.ts index 2a051768..6d5569bb 100644 --- a/src/tools/AgentTool/prompt.ts +++ b/src/tools/AgentTool/prompt.ts @@ -1,4 +1,4 @@ -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { getSubscriptionType } from '../../utils/auth.js' import { hasEmbeddedSearchTools } from '../../utils/embeddedTools.js' import { isEnvDefinedFalsy, isEnvTruthy } from '../../utils/envUtils.js' diff --git a/src/tools/AgentTool/runAgent.ts b/src/tools/AgentTool/runAgent.ts index e2cb942e..03c36af6 100644 --- a/src/tools/AgentTool/runAgent.ts +++ b/src/tools/AgentTool/runAgent.ts @@ -13,7 +13,7 @@ import type { QuerySource } from '../../constants/querySource.js' import { getSystemContext, getUserContext } from '../../context.js' import type { CanUseToolFn } from '../../hooks/useCanUseTool.js' import { query } from '../../query.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { getDumpPromptsPath } from '../../services/api/dumpPrompts.js' import { cleanupAgentTracking } from '../../services/api/promptCacheBreakDetection.js' import { @@ -76,7 +76,7 @@ import { isPerfettoTracingEnabled, registerAgent as registerPerfettoAgent, unregisterAgent as unregisterPerfettoAgent, -} from '../../utils/telemetry/perfettoTracing.js' +} from '../../utils/telemetry-stub.js' import type { ContentReplacementState } from '../../utils/toolResultStorage.js' import { createAgentId } from '../../utils/uuid.js' import { resolveAgentTools } from './agentToolUtils.js' diff --git a/src/tools/BashTool/BashTool.tsx b/src/tools/BashTool/BashTool.tsx index 3fcc8876..e7a8f8cd 100644 --- a/src/tools/BashTool/BashTool.tsx +++ b/src/tools/BashTool/BashTool.tsx @@ -7,7 +7,7 @@ import type { AppState } from 'src/state/AppState.js'; import { z } from 'zod/v4'; import { getKairosActive } from '../../bootstrap/state.js'; import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import { notifyVscodeFileUpdated } from '../../services/mcp/vscodeSdkMcp.js'; import type { SetToolJSXFn, ToolCallProgress, ToolUseContext, ValidationResult } from '../../Tool.js'; import { buildTool, type ToolDef } from '../../Tool.js'; diff --git a/src/tools/BashTool/bashPermissions.ts b/src/tools/BashTool/bashPermissions.ts index 83b3c633..b151512b 100644 --- a/src/tools/BashTool/bashPermissions.ts +++ b/src/tools/BashTool/bashPermissions.ts @@ -1,11 +1,11 @@ import { feature } from 'bun:bundle' import { APIUserAbortError } from '@anthropic-ai/sdk' import type { z } from 'zod/v4' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import type { ToolPermissionContext, ToolUseContext } from '../../Tool.js' import type { PendingClassifierCheck } from '../../types/permissions.js' import { count } from '../../utils/array.js' diff --git a/src/tools/BashTool/bashSecurity.ts b/src/tools/BashTool/bashSecurity.ts index a7213111..ff8e76fe 100644 --- a/src/tools/BashTool/bashSecurity.ts +++ b/src/tools/BashTool/bashSecurity.ts @@ -1,4 +1,4 @@ -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import { extractHeredocs } from '../../utils/bash/heredoc.js' import { ParsedCommand } from '../../utils/bash/ParsedCommand.js' import { diff --git a/src/tools/BashTool/shouldUseSandbox.ts b/src/tools/BashTool/shouldUseSandbox.ts index 92032afd..e621819c 100644 --- a/src/tools/BashTool/shouldUseSandbox.ts +++ b/src/tools/BashTool/shouldUseSandbox.ts @@ -1,4 +1,4 @@ -import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js' import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js' import { getSettings_DEPRECATED } from '../../utils/settings/settings.js' diff --git a/src/tools/BashTool/utils.ts b/src/tools/BashTool/utils.ts index 9edee875..96920956 100644 --- a/src/tools/BashTool/utils.ts +++ b/src/tools/BashTool/utils.ts @@ -5,7 +5,7 @@ import type { } from '@anthropic-ai/sdk/resources/index.mjs' import { readFile, stat } from 'fs/promises' import { getOriginalCwd } from 'src/bootstrap/state.js' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import type { ToolPermissionContext } from 'src/Tool.js' import { getCwd } from 'src/utils/cwd.js' import { pathInAllowedWorkingPath } from 'src/utils/permissions/filesystem.js' diff --git a/src/tools/BriefTool/BriefTool.ts b/src/tools/BriefTool/BriefTool.ts index 31f28296..7b938497 100644 --- a/src/tools/BriefTool/BriefTool.ts +++ b/src/tools/BriefTool/BriefTool.ts @@ -1,8 +1,8 @@ import { feature } from 'bun:bundle' import { z } from 'zod/v4' import { getKairosActive, getUserMsgOptIn } from '../../bootstrap/state.js' -import { getFeatureValue_CACHED_WITH_REFRESH } from '../../services/analytics/growthbook.js' -import { logEvent } from '../../services/analytics/index.js' +import { getFeatureValue_CACHED_WITH_REFRESH } from '../../services/analytics-stub.js' +import { logEvent } from '../../services/analytics-stub.js' import type { ValidationResult } from '../../Tool.js' import { buildTool, type ToolDef } from '../../Tool.js' import { isEnvTruthy } from '../../utils/envUtils.js' diff --git a/src/tools/ConfigTool/ConfigTool.ts b/src/tools/ConfigTool/ConfigTool.ts index ad8f0f99..11c5dc2f 100644 --- a/src/tools/ConfigTool/ConfigTool.ts +++ b/src/tools/ConfigTool/ConfigTool.ts @@ -3,7 +3,7 @@ import { z } from 'zod/v4' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { buildTool, type ToolDef } from '../../Tool.js' import { type GlobalConfig, diff --git a/src/tools/EnterWorktreeTool/EnterWorktreeTool.ts b/src/tools/EnterWorktreeTool/EnterWorktreeTool.ts index eb2e6dbf..30ac9a50 100644 --- a/src/tools/EnterWorktreeTool/EnterWorktreeTool.ts +++ b/src/tools/EnterWorktreeTool/EnterWorktreeTool.ts @@ -1,7 +1,7 @@ import { z } from 'zod/v4' import { getSessionId, setOriginalCwd } from '../../bootstrap/state.js' import { clearSystemPromptSections } from '../../constants/systemPromptSections.js' -import { logEvent } from '../../services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import type { Tool } from '../../Tool.js' import { buildTool, type ToolDef } from '../../Tool.js' import { clearMemoryFileCaches } from '../../utils/claudemd.js' diff --git a/src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts b/src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts index 5aa83c12..9cc36aad 100644 --- a/src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts +++ b/src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts @@ -8,8 +8,8 @@ import { setNeedsAutoModeExitAttachment, setNeedsPlanModeExitAttachment, } from '../../bootstrap/state.js' -import { logEvent } from '../../services/analytics/index.js' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js' +import { logEvent } from '../../services/analytics-stub.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics-stub.js' import { buildTool, type Tool, diff --git a/src/tools/ExitWorktreeTool/ExitWorktreeTool.ts b/src/tools/ExitWorktreeTool/ExitWorktreeTool.ts index 2d735c07..672e94eb 100644 --- a/src/tools/ExitWorktreeTool/ExitWorktreeTool.ts +++ b/src/tools/ExitWorktreeTool/ExitWorktreeTool.ts @@ -6,7 +6,7 @@ import { setProjectRoot, } from '../../bootstrap/state.js' import { clearSystemPromptSections } from '../../constants/systemPromptSections.js' -import { logEvent } from '../../services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import type { Tool } from '../../Tool.js' import { buildTool, type ToolDef } from '../../Tool.js' import { count } from '../../utils/array.js' diff --git a/src/tools/FileEditTool/FileEditTool.ts b/src/tools/FileEditTool/FileEditTool.ts index 85399420..b0c4e194 100644 --- a/src/tools/FileEditTool/FileEditTool.ts +++ b/src/tools/FileEditTool/FileEditTool.ts @@ -1,6 +1,6 @@ import { dirname, isAbsolute, sep } from 'path' -import { logEvent } from 'src/services/analytics/index.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { logEvent } from '../../services/analytics-stub.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { diagnosticTracker } from '../../services/diagnosticTracking.js' import { clearDeliveredDiagnosticsForFile } from '../../services/lsp/LSPDiagnosticRegistry.js' import { getLspServerManager } from '../../services/lsp/manager.js' diff --git a/src/tools/FileReadTool/FileReadTool.ts b/src/tools/FileReadTool/FileReadTool.ts index 1ceed46d..ccb877b3 100644 --- a/src/tools/FileReadTool/FileReadTool.ts +++ b/src/tools/FileReadTool/FileReadTool.ts @@ -10,12 +10,12 @@ import { } from '../../constants/apiLimits.js' import { hasBinaryExtension } from '../../constants/files.js' import { memoryFreshnessNote } from '../../memdir/memoryAge.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' -import { logEvent } from '../../services/analytics/index.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' +import { logEvent } from '../../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, getFileExtensionForAnalytics, -} from '../../services/analytics/metadata.js' +} from '../../services/analytics-stub.js' import { countTokensWithAPI, roughTokenCountEstimationForFileType, diff --git a/src/tools/FileReadTool/limits.ts b/src/tools/FileReadTool/limits.ts index 3e0247f6..b04af643 100644 --- a/src/tools/FileReadTool/limits.ts +++ b/src/tools/FileReadTool/limits.ts @@ -13,7 +13,7 @@ * tool-result while truncation yields ~25K tokens of content at the cap. */ import memoize from 'lodash-es/memoize.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { MAX_OUTPUT_SIZE } from 'src/utils/file.js' export const DEFAULT_MAX_OUTPUT_TOKENS = 25000 diff --git a/src/tools/FileWriteTool/FileWriteTool.ts b/src/tools/FileWriteTool/FileWriteTool.ts index 6da0afc8..a087b23d 100644 --- a/src/tools/FileWriteTool/FileWriteTool.ts +++ b/src/tools/FileWriteTool/FileWriteTool.ts @@ -1,7 +1,7 @@ import { dirname, sep } from 'path' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import { z } from 'zod/v4' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { diagnosticTracker } from '../../services/diagnosticTracking.js' import { clearDeliveredDiagnosticsForFile } from '../../services/lsp/LSPDiagnosticRegistry.js' import { getLspServerManager } from '../../services/lsp/manager.js' diff --git a/src/tools/PowerShellTool/PowerShellTool.tsx b/src/tools/PowerShellTool/PowerShellTool.tsx index db35d2aa..1681e689 100644 --- a/src/tools/PowerShellTool/PowerShellTool.tsx +++ b/src/tools/PowerShellTool/PowerShellTool.tsx @@ -7,7 +7,7 @@ import type { AppState } from 'src/state/AppState.js'; import { z } from 'zod/v4'; import { getKairosActive } from '../../bootstrap/state.js'; import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics-stub.js'; import type { SetToolJSXFn, Tool, ToolCallProgress, ValidationResult } from '../../Tool.js'; import { buildTool, type ToolDef } from '../../Tool.js'; import { backgroundExistingForegroundTask, markTaskNotified, registerForeground, spawnShellTask, unregisterForeground } from '../../tasks/LocalShellTask/LocalShellTask.js'; diff --git a/src/tools/RemoteTriggerTool/RemoteTriggerTool.ts b/src/tools/RemoteTriggerTool/RemoteTriggerTool.ts index 8e623a15..9a6eb831 100644 --- a/src/tools/RemoteTriggerTool/RemoteTriggerTool.ts +++ b/src/tools/RemoteTriggerTool/RemoteTriggerTool.ts @@ -1,7 +1,7 @@ import axios from 'axios' import { z } from 'zod/v4' import { getOauthConfig } from '../../constants/oauth.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { getOrganizationUUID } from '../../services/oauth/client.js' import { isPolicyAllowed } from '../../services/policyLimits/index.js' import type { ToolUseContext } from '../../Tool.js' diff --git a/src/tools/ScheduleCronTool/prompt.ts b/src/tools/ScheduleCronTool/prompt.ts index 33ff2a20..b1522637 100644 --- a/src/tools/ScheduleCronTool/prompt.ts +++ b/src/tools/ScheduleCronTool/prompt.ts @@ -1,5 +1,5 @@ import { feature } from 'bun:bundle' -import { getFeatureValue_CACHED_WITH_REFRESH } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_WITH_REFRESH } from '../../services/analytics-stub.js' import { DEFAULT_CRON_JITTER_CONFIG } from '../../utils/cronTasks.js' import { isEnvTruthy } from '../../utils/envUtils.js' diff --git a/src/tools/SkillTool/SkillTool.ts b/src/tools/SkillTool/SkillTool.ts index 85b68fad..bd7a6aca 100644 --- a/src/tools/SkillTool/SkillTool.ts +++ b/src/tools/SkillTool/SkillTool.ts @@ -32,7 +32,7 @@ import { isOfficialMarketplaceName, parsePluginIdentifier, } from 'src/utils/plugins/pluginIdentifier.js' -import { buildPluginCommandTelemetryFields } from 'src/utils/telemetry/pluginTelemetry.js' +import { buildPluginCommandTelemetryFields } from '../../utils/telemetry-stub.js' import { z } from 'zod/v4' import { addInvokedSkill, @@ -45,7 +45,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { getAgentContext } from '../../utils/agentContext.js' import { errorMessage } from '../../utils/errors.js' import { @@ -109,7 +109,7 @@ const remoteSkillModules = feature('EXPERIMENTAL_SKILL_SEARCH') ? { ...(require('../../services/skillSearch/remoteSkillState.js') as typeof import('../../services/skillSearch/remoteSkillState.js')), ...(require('../../services/skillSearch/remoteSkillLoader.js') as typeof import('../../services/skillSearch/remoteSkillLoader.js')), - ...(require('../../services/skillSearch/telemetry.js') as typeof import('../../services/skillSearch/telemetry.js')), + ...(require('../../services/analytics-stub.js') as typeof import('../../services/skillSearch/telemetry.js')), ...(require('../../services/skillSearch/featureCheck.js') as typeof import('../../services/skillSearch/featureCheck.js')), } : null diff --git a/src/tools/SkillTool/prompt.ts b/src/tools/SkillTool/prompt.ts index b05a58f3..39cff651 100644 --- a/src/tools/SkillTool/prompt.ts +++ b/src/tools/SkillTool/prompt.ts @@ -10,7 +10,7 @@ import { stringWidth } from '../../ink/stringWidth.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { count } from '../../utils/array.js' import { logForDebugging } from '../../utils/debug.js' import { toError } from '../../utils/errors.js' diff --git a/src/tools/TaskUpdateTool/TaskUpdateTool.ts b/src/tools/TaskUpdateTool/TaskUpdateTool.ts index 427831bf..4c249698 100644 --- a/src/tools/TaskUpdateTool/TaskUpdateTool.ts +++ b/src/tools/TaskUpdateTool/TaskUpdateTool.ts @@ -1,6 +1,6 @@ import { feature } from 'bun:bundle' import { z } from 'zod/v4' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { buildTool, type ToolDef } from '../../Tool.js' import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js' import { diff --git a/src/tools/TeamCreateTool/TeamCreateTool.ts b/src/tools/TeamCreateTool/TeamCreateTool.ts index 64a80186..919d7bce 100644 --- a/src/tools/TeamCreateTool/TeamCreateTool.ts +++ b/src/tools/TeamCreateTool/TeamCreateTool.ts @@ -1,7 +1,7 @@ import { z } from 'zod/v4' import { getSessionId } from '../../bootstrap/state.js' -import { logEvent } from '../../services/analytics/index.js' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js' +import { logEvent } from '../../services/analytics-stub.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics-stub.js' import type { Tool } from '../../Tool.js' import { buildTool, type ToolDef } from '../../Tool.js' import { formatAgentId } from '../../utils/agentId.js' diff --git a/src/tools/TeamDeleteTool/TeamDeleteTool.ts b/src/tools/TeamDeleteTool/TeamDeleteTool.ts index f09f78c3..5ef2bd95 100644 --- a/src/tools/TeamDeleteTool/TeamDeleteTool.ts +++ b/src/tools/TeamDeleteTool/TeamDeleteTool.ts @@ -1,6 +1,6 @@ import { z } from 'zod/v4' -import { logEvent } from '../../services/analytics/index.js' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js' +import { logEvent } from '../../services/analytics-stub.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics-stub.js' import type { Tool } from '../../Tool.js' import { buildTool, type ToolDef } from '../../Tool.js' import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js' diff --git a/src/tools/TodoWriteTool/TodoWriteTool.ts b/src/tools/TodoWriteTool/TodoWriteTool.ts index f0eafbad..3b4573b3 100644 --- a/src/tools/TodoWriteTool/TodoWriteTool.ts +++ b/src/tools/TodoWriteTool/TodoWriteTool.ts @@ -1,7 +1,7 @@ import { feature } from 'bun:bundle' import { z } from 'zod/v4' import { getSessionId } from '../../bootstrap/state.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { buildTool, type ToolDef } from '../../Tool.js' import { lazySchema } from '../../utils/lazySchema.js' import { isTodoV2Enabled } from '../../utils/tasks.js' diff --git a/src/tools/ToolSearchTool/ToolSearchTool.ts b/src/tools/ToolSearchTool/ToolSearchTool.ts index ee43b668..5ef1a9f0 100644 --- a/src/tools/ToolSearchTool/ToolSearchTool.ts +++ b/src/tools/ToolSearchTool/ToolSearchTool.ts @@ -4,7 +4,7 @@ import { z } from 'zod/v4' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { buildTool, findToolByName, diff --git a/src/tools/ToolSearchTool/prompt.ts b/src/tools/ToolSearchTool/prompt.ts index ca3e6b07..cd4da889 100644 --- a/src/tools/ToolSearchTool/prompt.ts +++ b/src/tools/ToolSearchTool/prompt.ts @@ -1,6 +1,6 @@ import { feature } from 'bun:bundle' import { isReplBridgeActive } from '../../bootstrap/state.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import type { Tool } from '../../Tool.js' import { AGENT_TOOL_NAME } from '../AgentTool/constants.js' diff --git a/src/tools/WebFetchTool/utils.ts b/src/tools/WebFetchTool/utils.ts index 6d55f70b..7f2a067a 100644 --- a/src/tools/WebFetchTool/utils.ts +++ b/src/tools/WebFetchTool/utils.ts @@ -3,7 +3,7 @@ import { LRUCache } from 'lru-cache' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { queryHaiku } from '../../services/api/claude.js' import { AbortError } from '../../utils/errors.js' import { getWebFetchUserAgent } from '../../utils/http.js' diff --git a/src/tools/WebSearchTool/WebSearchTool.ts b/src/tools/WebSearchTool/WebSearchTool.ts index bbe12eb7..e7995aa7 100644 --- a/src/tools/WebSearchTool/WebSearchTool.ts +++ b/src/tools/WebSearchTool/WebSearchTool.ts @@ -5,7 +5,7 @@ import type { import { getAPIProvider } from 'src/utils/model/providers.js' import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js' import { z } from 'zod/v4' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { queryModelWithStreaming } from '../../services/api/claude.js' import { buildTool, type ToolDef } from '../../Tool.js' import { lazySchema } from '../../utils/lazySchema.js' diff --git a/src/tools/shared/gitOperationTracking.ts b/src/tools/shared/gitOperationTracking.ts index 970a53f8..0fe6108d 100644 --- a/src/tools/shared/gitOperationTracking.ts +++ b/src/tools/shared/gitOperationTracking.ts @@ -12,7 +12,7 @@ import { getCommitCounter, getPrCounter } from '../../bootstrap/state.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' /** * Build a regex that matches `git ` while tolerating git's global diff --git a/src/utils/Shell.ts b/src/utils/Shell.ts index f2e5fcf4..4966f4f8 100644 --- a/src/utils/Shell.ts +++ b/src/utils/Shell.ts @@ -4,7 +4,7 @@ import { type FileHandle, mkdir, open, realpath } from 'fs/promises' import memoize from 'lodash-es/memoize.js' import { isAbsolute, resolve } from 'path' import { join as posixJoin } from 'path/posix' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { getOriginalCwd, getSessionId, diff --git a/src/utils/advisor.ts b/src/utils/advisor.ts index 54a2dfb3..e916b028 100644 --- a/src/utils/advisor.ts +++ b/src/utils/advisor.ts @@ -1,5 +1,5 @@ import type { BetaUsage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { shouldIncludeFirstPartyOnlyBetas } from './betas.js' import { isEnvTruthy } from './envUtils.js' import { getInitialSettings } from './settings/settings.js' diff --git a/src/utils/agentContext.ts b/src/utils/agentContext.ts index 821a4aab..e296f050 100644 --- a/src/utils/agentContext.ts +++ b/src/utils/agentContext.ts @@ -22,7 +22,7 @@ */ import { AsyncLocalStorage } from 'async_hooks' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics/index.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics-stub.js' import { isAgentSwarmsEnabled } from './agentSwarmsEnabled.js' /** diff --git a/src/utils/agentSwarmsEnabled.ts b/src/utils/agentSwarmsEnabled.ts index fac5404c..d1a3bdee 100644 --- a/src/utils/agentSwarmsEnabled.ts +++ b/src/utils/agentSwarmsEnabled.ts @@ -1,4 +1,4 @@ -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { isEnvTruthy } from './envUtils.js' /** diff --git a/src/utils/analyzeContext.ts b/src/utils/analyzeContext.ts index 87d4dd55..24ecc376 100644 --- a/src/utils/analyzeContext.ts +++ b/src/utils/analyzeContext.ts @@ -8,9 +8,9 @@ import { microcompactMessages } from 'src/services/compact/microCompact.js' import { getSdkBetas } from '../bootstrap/state.js' import { getCommandName } from '../commands.js' import { getSystemContext } from '../context.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { - AUTOCOMPACT_BUFFER_TOKENS, + getAutoCompactBufferTokens, getEffectiveContextWindowSize, isAutoCompactEnabled, MANUAL_COMPACT_BUFFER_TOKENS, @@ -1001,7 +1001,7 @@ export async function analyzeContextUsage( // Check if autocompact is enabled and calculate threshold const isAutoCompact = isAutoCompactEnabled() const autoCompactThreshold = isAutoCompact - ? getEffectiveContextWindowSize(model) - AUTOCOMPACT_BUFFER_TOKENS + ? getEffectiveContextWindowSize(model) - getAutoCompactBufferTokens() : undefined // Create categories diff --git a/src/utils/api.ts b/src/utils/api.ts index 9b66fd79..1a6ed413 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -6,15 +6,15 @@ import type { import { createHash } from 'crypto' import { SYSTEM_PROMPT_DYNAMIC_BOUNDARY } from 'src/constants/prompts.js' import { getSystemContext, getUserContext } from 'src/context.js' -import { isAnalyticsDisabled } from 'src/services/analytics/config.js' +import { isAnalyticsDisabled } from '../services/analytics-stub.js' import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE, getFeatureValue_CACHED_MAY_BE_STALE, -} from 'src/services/analytics/growthbook.js' +} from '../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../services/analytics-stub.js' import { prefetchAllMcpResources } from 'src/services/mcp/client.js' import type { ScopedMcpServerConfig } from 'src/services/mcp/types.js' import { BashTool } from 'src/tools/BashTool/BashTool.js' diff --git a/src/utils/attachments.ts b/src/utils/attachments.ts index 8a1612a4..1c803825 100644 --- a/src/utils/attachments.ts +++ b/src/utils/attachments.ts @@ -2,7 +2,7 @@ import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, -} from 'src/services/analytics/index.js' +} from '../services/analytics-stub.js' import { toolMatchesName, type Tools, @@ -216,7 +216,7 @@ import { getEffectiveContextWindowSize, isAutoCompactEnabled, } from '../services/compact/autoCompact.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { hasInstructionsLoadedHook, executeInstructionsLoadedHooks, diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 64a61808..6c62ad8e 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -8,7 +8,7 @@ import { CLAUDE_AI_PROFILE_SCOPE } from 'src/constants/oauth.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../services/analytics-stub.js' import { getModelStrings } from 'src/utils/model/modelStrings.js' import { getAPIProvider } from 'src/utils/model/providers.js' import { @@ -115,7 +115,8 @@ export function isAnthropicAuthEnabled(): boolean { const is3P = isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) || isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) || - isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) + isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) || + isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) // Check if user has configured an external API key source // This allows externally-provided API keys to work (without requiring proxy configuration) @@ -1728,12 +1729,13 @@ export function getSubscriptionName(): string { } } -/** Check if using third-party services (Bedrock or Vertex or Foundry) */ +/** Check if using third-party services (Bedrock or Vertex or Foundry or OpenAI-compatible) */ export function isUsing3PServices(): boolean { return !!( isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) || isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) || - isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) + isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) || + isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ) } diff --git a/src/utils/autoUpdater.ts b/src/utils/autoUpdater.ts index 2a5fc6f9..790c75a8 100644 --- a/src/utils/autoUpdater.ts +++ b/src/utils/autoUpdater.ts @@ -1,34 +1,9 @@ -import axios from 'axios' -import { constants as fsConstants } from 'fs' -import { access, writeFile } from 'fs/promises' -import { homedir } from 'os' -import { join } from 'path' -import { getDynamicConfig_BLOCKS_ON_INIT } from 'src/services/analytics/growthbook.js' -import { - type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - logEvent, -} from 'src/services/analytics/index.js' -import { type ReleaseChannel, saveGlobalConfig } from './config.js' -import { logForDebugging } from './debug.js' -import { env } from './env.js' -import { getClaudeConfigHomeDir } from './envUtils.js' -import { ClaudeError, getErrnoCode, isENOENT } from './errors.js' -import { execFileNoThrowWithCwd } from './execFileNoThrow.js' -import { getFsImplementation } from './fsOperations.js' -import { gracefulShutdownSync } from './gracefulShutdown.js' -import { logError } from './log.js' -import { gte, lt } from './semver.js' -import { getInitialSettings } from './settings/settings.js' -import { - filterClaudeAliases, - getShellConfigPaths, - readFileLines, - writeFileLines, -} from './shellConfig.js' -import { jsonParse } from './slowOperations.js' +/** + * Auto-updater stub for claude-code-clean + * All auto-update functionality has been disabled for privacy and independence + */ -const GCS_BUCKET_URL = - 'https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases' +import { ClaudeError } from './errors.js' class AutoUpdaterError extends ClaudeError {} @@ -51,511 +26,111 @@ export type MaxVersionConfig = { ant_message?: string } -/** - * Checks if the current version meets the minimum required version from Statsig config - * Terminates the process with an error message if the version is too old - * - * NOTE ON SHA-BASED VERSIONING: - * We use SemVer-compliant versioning with build metadata format (X.X.X+SHA) for continuous deployment. - * According to SemVer specs, build metadata (the +SHA part) is ignored when comparing versions. - * - * Versioning approach: - * 1. For version requirements/compatibility (assertMinVersion), we use semver comparison that ignores build metadata - * 2. For updates ('claude update'), we use exact string comparison to detect any change, including SHA - * - This ensures users always get the latest build, even when only the SHA changes - * - The UI clearly shows both versions including build metadata - * - * This approach keeps version comparison logic simple while maintaining traceability via the SHA. - */ +// Disabled: No version checks from remote servers export async function assertMinVersion(): Promise { - if (process.env.NODE_ENV === 'test') { - return - } - - try { - const versionConfig = await getDynamicConfig_BLOCKS_ON_INIT<{ - minVersion: string - }>('tengu_version_config', { minVersion: '0.0.0' }) - - if ( - versionConfig.minVersion && - lt(MACRO.VERSION, versionConfig.minVersion) - ) { - // biome-ignore lint/suspicious/noConsole:: intentional console output - console.error(` -It looks like your version of Claude Code (${MACRO.VERSION}) needs an update. -A newer version (${versionConfig.minVersion} or higher) is required to continue. - -To update, please run: - claude update - -This will ensure you have access to the latest features and improvements. -`) - gracefulShutdownSync(1) - } - } catch (error) { - logError(error as Error) - } + // No-op: claude-code-clean does not enforce minimum versions + return } -/** - * Returns the maximum allowed version for the current user type. - * For ants, returns the `ant` field (dev version format). - * For external users, returns the `external` field (clean semver). - * This is used as a server-side kill switch to pause auto-updates during incidents. - * Returns undefined if no cap is configured. - */ +// Disabled: No maximum version caps export async function getMaxVersion(): Promise { - const config = await getMaxVersionConfig() - if (process.env.USER_TYPE === 'ant') { - return config.ant || undefined - } - return config.external || undefined + return undefined } -/** - * Returns the server-driven message explaining the known issue, if configured. - * Shown in the warning banner when the current version exceeds the max allowed version. - */ +// Disabled: No version messages from server export async function getMaxVersionMessage(): Promise { - const config = await getMaxVersionConfig() - if (process.env.USER_TYPE === 'ant') { - return config.ant_message || undefined - } - return config.external_message || undefined + return undefined } -async function getMaxVersionConfig(): Promise { - try { - return await getDynamicConfig_BLOCKS_ON_INIT( - 'tengu_max_version_config', - {}, - ) - } catch (error) { - logError(error as Error) - return {} - } -} - -/** - * Checks if a target version should be skipped due to user's minimumVersion setting. - * This is used when switching to stable channel - the user can choose to stay on their - * current version until stable catches up, preventing downgrades. - */ +// Disabled: No version skipping export function shouldSkipVersion(targetVersion: string): boolean { - const settings = getInitialSettings() - const minimumVersion = settings?.minimumVersion - if (!minimumVersion) { - return false - } - // Skip if target version is less than minimum - const shouldSkip = !gte(targetVersion, minimumVersion) - if (shouldSkip) { - logForDebugging( - `Skipping update to ${targetVersion} - below minimumVersion ${minimumVersion}`, - ) - } - return shouldSkip + return false } -// Lock file for auto-updater to prevent concurrent updates -const LOCK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minute timeout for locks - -/** - * Get the path to the lock file - * This is a function to ensure it's evaluated at runtime after test setup - */ +// Disabled: No update locks needed export function getLockFilePath(): string { - return join(getClaudeConfigHomeDir(), '.update.lock') + return '' } -/** - * Attempts to acquire a lock for auto-updater - * @returns true if lock was acquired, false if another process holds the lock - */ -async function acquireLock(): Promise { - const fs = getFsImplementation() - const lockPath = getLockFilePath() - - // Check for existing lock: 1 stat() on the happy path (fresh lock or ENOENT), - // 2 on stale-lock recovery (re-verify staleness immediately before unlink). - try { - const stats = await fs.stat(lockPath) - const age = Date.now() - stats.mtimeMs - if (age < LOCK_TIMEOUT_MS) { - return false - } - // Lock is stale, remove it before taking over. Re-verify staleness - // immediately before unlinking to close a TOCTOU race: if two processes - // both observe the stale lock, A unlinks + writes a fresh lock, then B - // would unlink A's fresh lock and both believe they hold it. A fresh - // lock has a recent mtime, so re-checking staleness makes B back off. - try { - const recheck = await fs.stat(lockPath) - if (Date.now() - recheck.mtimeMs < LOCK_TIMEOUT_MS) { - return false - } - await fs.unlink(lockPath) - } catch (err) { - if (!isENOENT(err)) { - logError(err as Error) - return false - } - } - } catch (err) { - if (!isENOENT(err)) { - logError(err as Error) - return false - } - // ENOENT: no lock file, proceed to create one - } +// Disabled: No updates from GCS +export async function getLatestVersionFromGcs(channel: string): Promise { + return null +} - // Create lock file atomically with O_EXCL (flag: 'wx'). If another process - // wins the race and creates it first, we get EEXIST and back off. - // Lazy-mkdir the config dir on ENOENT. - try { - await writeFile(lockPath, `${process.pid}`, { - encoding: 'utf8', - flag: 'wx', - }) - return true - } catch (err) { - const code = getErrnoCode(err) - if (code === 'EEXIST') { - return false - } - if (code === 'ENOENT') { - try { - // fs.mkdir from getFsImplementation() is always recursive:true and - // swallows EEXIST internally, so a dir-creation race cannot reach the - // catch below — only writeFile's EEXIST (true lock contention) can. - await fs.mkdir(getClaudeConfigHomeDir()) - await writeFile(lockPath, `${process.pid}`, { - encoding: 'utf8', - flag: 'wx', - }) - return true - } catch (mkdirErr) { - if (getErrnoCode(mkdirErr) === 'EEXIST') { - return false - } - logError(mkdirErr as Error) - return false - } - } - logError(err as Error) - return false +// Disabled: No auto-updates +export async function checkForUpdates(): Promise { + return { + version: null, + status: 'success', + notifications: ['Auto-updates disabled in claude-code-clean'] } } -/** - * Releases the update lock if it's held by this process - */ -async function releaseLock(): Promise { - const fs = getFsImplementation() - const lockPath = getLockFilePath() - try { - const lockData = await fs.readFile(lockPath, { encoding: 'utf8' }) - if (lockData === `${process.pid}`) { - await fs.unlink(lockPath) - } - } catch (err) { - if (isENOENT(err)) { - return - } - logError(err as Error) +// Disabled: No installations +export async function installUpdate(version: string): Promise { + return { + version: null, + status: 'success', + notifications: ['Auto-updates disabled in claude-code-clean'] } } -async function getInstallationPrefix(): Promise { - // Run from home directory to avoid reading project-level .npmrc/.bunfig.toml - const isBun = env.isRunningWithBun() - let prefixResult = null - if (isBun) { - prefixResult = await execFileNoThrowWithCwd('bun', ['pm', 'bin', '-g'], { - cwd: homedir(), - }) - } else { - prefixResult = await execFileNoThrowWithCwd( - 'npm', - ['-g', 'config', 'get', 'prefix'], - { cwd: homedir() }, - ) - } - if (prefixResult.code !== 0) { - logError(new Error(`Failed to check ${isBun ? 'bun' : 'npm'} permissions`)) - return null - } - return prefixResult.stdout.trim() +// Export any other functions that might be imported elsewhere as no-ops +export async function checkAndInstallUpdate(): Promise { + // No-op } -export async function checkGlobalInstallPermissions(): Promise<{ - hasPermissions: boolean - npmPrefix: string | null -}> { - try { - const prefix = await getInstallationPrefix() - if (!prefix) { - return { hasPermissions: false, npmPrefix: null } - } +export async function getUpdateStatus(): Promise { + return 'disabled' +} - try { - await access(prefix, fsConstants.W_OK) - return { hasPermissions: true, npmPrefix: prefix } - } catch { - logError( - new AutoUpdaterError( - 'Insufficient permissions for global npm install.', - ), - ) - return { hasPermissions: false, npmPrefix: prefix } - } - } catch (error) { - logError(error as Error) - return { hasPermissions: false, npmPrefix: null } - } +export function isAutoUpdateEnabled(): boolean { + return false } -export async function getLatestVersion( - channel: ReleaseChannel, -): Promise { - const npmTag = channel === 'stable' ? 'stable' : 'latest' +// Additional stub exports +export async function checkGlobalInstallPermissions(): Promise<{ canInstall: boolean; reason?: string }> { + return { canInstall: false, reason: 'Auto-updates disabled in claude-code-clean' } +} - // Run from home directory to avoid reading project-level .npmrc - // which could be maliciously crafted to redirect to an attacker's registry - const result = await execFileNoThrowWithCwd( - 'npm', - ['view', `${MACRO.PACKAGE_URL}@${npmTag}`, 'version', '--prefer-online'], - { abortSignal: AbortSignal.timeout(5000), cwd: homedir() }, - ) - if (result.code !== 0) { - logForDebugging(`npm view failed with code ${result.code}`) - if (result.stderr) { - logForDebugging(`npm stderr: ${result.stderr.trim()}`) - } else { - logForDebugging('npm stderr: (empty)') - } - if (result.stdout) { - logForDebugging(`npm stdout: ${result.stdout.trim()}`) - } - return null - } - return result.stdout.trim() +export async function getLatestVersion(channel: string): Promise { + return null } export type NpmDistTags = { - latest: string | null - stable: string | null + latest?: string + beta?: string + stable?: string } -/** - * Get npm dist-tags (latest and stable versions) from the registry. - * This is used by the doctor command to show users what versions are available. - */ export async function getNpmDistTags(): Promise { - // Run from home directory to avoid reading project-level .npmrc - const result = await execFileNoThrowWithCwd( - 'npm', - ['view', MACRO.PACKAGE_URL, 'dist-tags', '--json', '--prefer-online'], - { abortSignal: AbortSignal.timeout(5000), cwd: homedir() }, - ) - - if (result.code !== 0) { - logForDebugging(`npm view dist-tags failed with code ${result.code}`) - return { latest: null, stable: null } - } - - try { - const parsed = jsonParse(result.stdout.trim()) as Record - return { - latest: typeof parsed.latest === 'string' ? parsed.latest : null, - stable: typeof parsed.stable === 'string' ? parsed.stable : null, - } - } catch (error) { - logForDebugging(`Failed to parse dist-tags: ${error}`) - return { latest: null, stable: null } - } + return {} } -/** - * Get the latest version from GCS bucket for a given release channel. - * This is used by installations that don't have npm (e.g. package manager installs). - */ -export async function getLatestVersionFromGcs( - channel: ReleaseChannel, -): Promise { - try { - const response = await axios.get(`${GCS_BUCKET_URL}/${channel}`, { - timeout: 5000, - responseType: 'text', - }) - return response.data.trim() - } catch (error) { - logForDebugging(`Failed to fetch ${channel} from GCS: ${error}`) - return null - } -} - -/** - * Get available versions from GCS bucket (for native installations). - * Fetches both latest and stable channel pointers. - */ export async function getGcsDistTags(): Promise { - const [latest, stable] = await Promise.all([ - getLatestVersionFromGcs('latest'), - getLatestVersionFromGcs('stable'), - ]) - - return { latest, stable } + return {} } -/** - * Get version history from npm registry (ant-only feature) - * Returns versions sorted newest-first, limited to the specified count - * - * Uses NATIVE_PACKAGE_URL when available because: - * 1. Native installation is the primary installation method for ant users - * 2. Not all JS package versions have corresponding native packages - * 3. This prevents rollback from listing versions that don't have native binaries - */ export async function getVersionHistory(limit: number): Promise { - if (process.env.USER_TYPE !== 'ant') { - return [] - } - - // Use native package URL when available to ensure we only show versions - // that have native binaries (not all JS package versions have native builds) - const packageUrl = MACRO.NATIVE_PACKAGE_URL ?? MACRO.PACKAGE_URL - - // Run from home directory to avoid reading project-level .npmrc - const result = await execFileNoThrowWithCwd( - 'npm', - ['view', packageUrl, 'versions', '--json', '--prefer-online'], - // Longer timeout for version list - { abortSignal: AbortSignal.timeout(30000), cwd: homedir() }, - ) - - if (result.code !== 0) { - logForDebugging(`npm view versions failed with code ${result.code}`) - if (result.stderr) { - logForDebugging(`npm stderr: ${result.stderr.trim()}`) - } - return [] - } - - try { - const versions = jsonParse(result.stdout.trim()) as string[] - // Take last N versions, then reverse to get newest first - return versions.slice(-limit).reverse() - } catch (error) { - logForDebugging(`Failed to parse version history: ${error}`) - return [] - } + return [] } -export async function installGlobalPackage( - specificVersion?: string | null, -): Promise { - if (!(await acquireLock())) { - logError( - new AutoUpdaterError('Another process is currently installing an update'), - ) - // Log the lock contention - logEvent('tengu_auto_updater_lock_contention', { - pid: process.pid, - currentVersion: - MACRO.VERSION as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - }) - return 'in_progress' - } - - try { - await removeClaudeAliasesFromShellConfigs() - // Check if we're using npm from Windows path in WSL - if (!env.isRunningWithBun() && env.isNpmFromWindowsPath()) { - logError(new Error('Windows NPM detected in WSL environment')) - logEvent('tengu_auto_updater_windows_npm_in_wsl', { - currentVersion: - MACRO.VERSION as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - }) - // biome-ignore lint/suspicious/noConsole:: intentional console output - console.error(` -Error: Windows NPM detected in WSL - -You're running Claude Code in WSL but using the Windows NPM installation from /mnt/c/. -This configuration is not supported for updates. - -To fix this issue: - 1. Install Node.js within your Linux distribution: e.g. sudo apt install nodejs npm - 2. Make sure Linux NPM is in your PATH before the Windows version - 3. Try updating again with 'claude update' -`) - return 'install_failed' - } - - const { hasPermissions } = await checkGlobalInstallPermissions() - if (!hasPermissions) { - return 'no_permissions' - } - - // Use specific version if provided, otherwise use latest - const packageSpec = specificVersion - ? `${MACRO.PACKAGE_URL}@${specificVersion}` - : MACRO.PACKAGE_URL - - // Run from home directory to avoid reading project-level .npmrc/.bunfig.toml - // which could be maliciously crafted to redirect to an attacker's registry - const packageManager = env.isRunningWithBun() ? 'bun' : 'npm' - const installResult = await execFileNoThrowWithCwd( - packageManager, - ['install', '-g', packageSpec], - { cwd: homedir() }, - ) - if (installResult.code !== 0) { - const error = new AutoUpdaterError( - `Failed to install new version of claude: ${installResult.stdout} ${installResult.stderr}`, - ) - logError(error) - return 'install_failed' - } - - // Set installMethod to 'global' to track npm global installations - saveGlobalConfig(current => ({ - ...current, - installMethod: 'global', - })) - - return 'success' - } finally { - // Ensure we always release the lock - await releaseLock() +export async function installGlobalPackage(version: string): Promise { + return { + version: null, + status: 'success', + notifications: ['Auto-updates disabled in claude-code-clean'] } } -/** - * Remove claude aliases from shell configuration files - * This helps clean up old installation methods when switching to native or npm global - */ -async function removeClaudeAliasesFromShellConfigs(): Promise { - const configMap = getShellConfigPaths() - - // Process each shell config file - for (const [, configFile] of Object.entries(configMap)) { - try { - const lines = await readFileLines(configFile) - if (!lines) continue +export async function removeShellAliases(): Promise { + // No-op +} - const { filtered, hadAlias } = filterClaudeAliases(lines) +export async function addShellAliases(): Promise { + // No-op +} - if (hadAlias) { - await writeFileLines(configFile, filtered) - logForDebugging(`Removed claude alias from ${configFile}`) - } - } catch (error) { - // Don't fail the whole operation if one file can't be processed - logForDebugging(`Failed to remove alias from ${configFile}: ${error}`, { - level: 'error', - }) - } - } +export function isUpdateAvailable(current: string, latest: string): boolean { + return false } diff --git a/src/utils/background/remote/preconditions.ts b/src/utils/background/remote/preconditions.ts index a7b229b4..ebdb296d 100644 --- a/src/utils/background/remote/preconditions.ts +++ b/src/utils/background/remote/preconditions.ts @@ -1,7 +1,7 @@ import axios from 'axios' import { getOauthConfig } from 'src/constants/oauth.js' import { getOrganizationUUID } from 'src/services/oauth/client.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics-stub.js' import { checkAndRefreshOAuthTokenIfNeeded, getClaudeAIOAuthTokens, diff --git a/src/utils/background/remote/remoteSession.ts b/src/utils/background/remote/remoteSession.ts index a054921d..ca21246e 100644 --- a/src/utils/background/remote/remoteSession.ts +++ b/src/utils/background/remote/remoteSession.ts @@ -1,5 +1,5 @@ import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.js' -import { checkGate_CACHED_OR_BLOCKING } from '../../../services/analytics/growthbook.js' +import { checkGate_CACHED_OR_BLOCKING } from '../../../services/analytics-stub.js' import { isPolicyAllowed } from '../../../services/policyLimits/index.js' import { detectCurrentRepositoryWithHost } from '../../detectRepository.js' import { isEnvTruthy } from '../../envUtils.js' diff --git a/src/utils/bash/ShellSnapshot.ts b/src/utils/bash/ShellSnapshot.ts index d26f052c..29d77d23 100644 --- a/src/utils/bash/ShellSnapshot.ts +++ b/src/utils/bash/ShellSnapshot.ts @@ -3,7 +3,7 @@ import { execa } from 'execa' import { mkdir, stat } from 'fs/promises' import * as os from 'os' import { join } from 'path' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import { registerCleanup } from '../cleanupRegistry.js' import { getCwd } from '../cwd.js' import { logForDebugging } from '../debug.js' diff --git a/src/utils/bash/parser.ts b/src/utils/bash/parser.ts index c6851f1c..015b8a6e 100644 --- a/src/utils/bash/parser.ts +++ b/src/utils/bash/parser.ts @@ -1,5 +1,5 @@ import { feature } from 'bun:bundle' -import { logEvent } from '../../services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import { logForDebugging } from '../debug.js' import { ensureParserInitialized, diff --git a/src/utils/betas.ts b/src/utils/betas.ts index fcd7b971..e315a59e 100644 --- a/src/utils/betas.ts +++ b/src/utils/betas.ts @@ -3,7 +3,7 @@ import memoize from 'lodash-es/memoize.js' import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE, getFeatureValue_CACHED_MAY_BE_STALE, -} from 'src/services/analytics/growthbook.js' +} from '../services/analytics-stub.js' import { getIsNonInteractiveSession, getSdkBetas } from '../bootstrap/state.js' import { BEDROCK_EXTRA_PARAMS_HEADERS, diff --git a/src/utils/claudeInChrome/mcpServer.ts b/src/utils/claudeInChrome/mcpServer.ts index bbe0e2b1..42f9fabb 100644 --- a/src/utils/claudeInChrome/mcpServer.ts +++ b/src/utils/claudeInChrome/mcpServer.ts @@ -1,13 +1,13 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { format } from 'util' -import { shutdownDatadog } from '../../services/analytics/datadog.js' -import { shutdown1PEventLogging } from '../../services/analytics/firstPartyEventLogger.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { shutdownDatadog } from '../../services/analytics-stub.js' +import { shutdown1PEventLogging } from '../../services/analytics-stub.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' -import { initializeAnalyticsSink } from '../../services/analytics/sink.js' +} from '../../services/analytics-stub.js' +import { initializeAnalyticsSink } from '../../services/analytics-stub.js' import { getClaudeAIOAuthTokens } from '../auth.js' import { enableConfigs, getGlobalConfig, saveGlobalConfig } from '../config.js' import { logForDebugging } from '../debug.js' diff --git a/src/utils/claudeInChrome/setup.ts b/src/utils/claudeInChrome/setup.ts index 77697023..15f48c0e 100644 --- a/src/utils/claudeInChrome/setup.ts +++ b/src/utils/claudeInChrome/setup.ts @@ -7,7 +7,7 @@ import { getIsNonInteractiveSession, getSessionBypassPermissionsMode, } from '../../bootstrap/state.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import type { ScopedMcpServerConfig } from '../../services/mcp/types.js' import { isInBundledMode } from '../bundledMode.js' import { getGlobalConfig, saveGlobalConfig } from '../config.js' diff --git a/src/utils/claudemd.ts b/src/utils/claudemd.ts index 5ea8ab6d..1f44bd73 100644 --- a/src/utils/claudemd.ts +++ b/src/utils/claudemd.ts @@ -40,14 +40,14 @@ import { sep, } from 'path' import picomatch from 'picomatch' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { getAdditionalDirectoriesForClaudeMd, getOriginalCwd, } from '../bootstrap/state.js' import { truncateEntrypointContent } from '../memdir/memdir.js' import { getAutoMemEntrypoint, isAutoMemoryEnabled } from '../memdir/paths.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { getCurrentProjectConfig, getManagedClaudeRulesDir, diff --git a/src/utils/cleanup.ts b/src/utils/cleanup.ts index 294ad2f7..dc838af6 100644 --- a/src/utils/cleanup.ts +++ b/src/utils/cleanup.ts @@ -1,7 +1,7 @@ import * as fs from 'fs/promises' import { homedir } from 'os' import { join } from 'path' -import { logEvent } from '../services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { CACHE_PATHS } from './cachePaths.js' import { logForDebugging } from './debug.js' import { getClaudeConfigHomeDir } from './envUtils.js' diff --git a/src/utils/computerUse/gates.ts b/src/utils/computerUse/gates.ts index 6563a480..8fd8c640 100644 --- a/src/utils/computerUse/gates.ts +++ b/src/utils/computerUse/gates.ts @@ -1,6 +1,6 @@ import type { CoordinateMode, CuSubGates } from '@ant/computer-use-mcp/types' -import { getDynamicConfig_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getDynamicConfig_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { getSubscriptionType } from '../auth.js' import { isEnvTruthy } from '../envUtils.js' diff --git a/src/utils/computerUse/mcpServer.ts b/src/utils/computerUse/mcpServer.ts index d51d80ab..ac2561e3 100644 --- a/src/utils/computerUse/mcpServer.ts +++ b/src/utils/computerUse/mcpServer.ts @@ -6,9 +6,9 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' import { homedir } from 'os' -import { shutdownDatadog } from '../../services/analytics/datadog.js' -import { shutdown1PEventLogging } from '../../services/analytics/firstPartyEventLogger.js' -import { initializeAnalyticsSink } from '../../services/analytics/sink.js' +import { shutdownDatadog } from '../../services/analytics-stub.js' +import { shutdown1PEventLogging } from '../../services/analytics-stub.js' +import { initializeAnalyticsSink } from '../../services/analytics-stub.js' import { enableConfigs } from '../config.js' import { logForDebugging } from '../debug.js' import { filterAppsForDescription } from './appNames.js' diff --git a/src/utils/config.ts b/src/utils/config.ts index eecbf0c6..ea0189e6 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -6,7 +6,7 @@ import pickBy from 'lodash-es/pickBy.js' import { basename, dirname, join, resolve } from 'path' import { getOriginalCwd, getSessionTrustAccepted } from '../bootstrap/state.js' import { getAutoMemEntrypoint } from '../memdir/paths.js' -import { logEvent } from '../services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import type { McpServerConfig } from '../services/mcp/types.js' import type { BillingType, diff --git a/src/utils/context.ts b/src/utils/context.ts index d9714de9..ee5d09e6 100644 --- a/src/utils/context.ts +++ b/src/utils/context.ts @@ -52,14 +52,11 @@ export function getContextWindowForModel( model: string, betas?: string[], ): number { - // Allow override via environment variable (ant-only) + // Allow override via environment variable // This takes precedence over all other context window resolution, including 1M detection, // so users can cap the effective context window for local decisions (auto-compact, etc.) // while still using a 1M-capable endpoint. - if ( - process.env.USER_TYPE === 'ant' && - process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS - ) { + if (process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS) { const override = parseInt(process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS, 10) if (!isNaN(override) && override > 0) { return override diff --git a/src/utils/cronJitterConfig.ts b/src/utils/cronJitterConfig.ts index 9cab46f8..4ae74f24 100644 --- a/src/utils/cronJitterConfig.ts +++ b/src/utils/cronJitterConfig.ts @@ -9,7 +9,7 @@ // Daemon/SDK: omit getJitterConfig → DEFAULT_CRON_JITTER_CONFIG applies. import { z } from 'zod/v4' -import { getFeatureValue_CACHED_WITH_REFRESH } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_WITH_REFRESH } from '../services/analytics-stub.js' import { type CronJitterConfig, DEFAULT_CRON_JITTER_CONFIG, diff --git a/src/utils/cronScheduler.ts b/src/utils/cronScheduler.ts index 56b36271..58a6d172 100644 --- a/src/utils/cronScheduler.ts +++ b/src/utils/cronScheduler.ts @@ -16,7 +16,7 @@ import { import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { cronToHuman } from './cron.js' import { type CronJitterConfig, diff --git a/src/utils/deepLink/registerProtocol.ts b/src/utils/deepLink/registerProtocol.ts index 0e630ee6..41971e4b 100644 --- a/src/utils/deepLink/registerProtocol.ts +++ b/src/utils/deepLink/registerProtocol.ts @@ -16,11 +16,11 @@ import { promises as fs } from 'fs' import * as os from 'os' import * as path from 'path' -import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../../services/analytics-stub.js' import { logForDebugging } from '../debug.js' import { getClaudeConfigHomeDir } from '../envUtils.js' import { getErrnoCode } from '../errors.js' diff --git a/src/utils/diff.ts b/src/utils/diff.ts index 38e7c1bf..998739de 100644 --- a/src/utils/diff.ts +++ b/src/utils/diff.ts @@ -1,5 +1,5 @@ import { type StructuredPatchHunk, structuredPatch } from 'diff' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { getLocCounter } from '../bootstrap/state.js' import { addToTotalLinesChanged } from '../cost-tracker.js' import type { FileEdit } from '../tools/FileEditTool/types.js' diff --git a/src/utils/effort.ts b/src/utils/effort.ts index cafcf3de..8e46efab 100644 --- a/src/utils/effort.ts +++ b/src/utils/effort.ts @@ -2,7 +2,7 @@ import { isUltrathinkEnabled } from './thinking.js' import { getInitialSettings } from './settings/settings.js' import { isProSubscriber, isMaxSubscriber, isTeamSubscriber } from './auth.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { getAPIProvider } from './model/providers.js' import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js' import { isEnvTruthy } from './envUtils.js' diff --git a/src/utils/fastMode.ts b/src/utils/fastMode.ts index 98de3ee6..f9a4e779 100644 --- a/src/utils/fastMode.ts +++ b/src/utils/fastMode.ts @@ -1,6 +1,6 @@ import axios from 'axios' import { getOauthConfig, OAUTH_BETA_HEADER } from 'src/constants/oauth.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { getIsNonInteractiveSession, getKairosActive, @@ -9,7 +9,7 @@ import { import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { getAnthropicApiKey, getClaudeAIOAuthTokens, diff --git a/src/utils/file.ts b/src/utils/file.ts index 773c3f41..7b8a972b 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -12,8 +12,8 @@ import { resolve, sep, } from 'path' -import { logEvent } from 'src/services/analytics/index.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { logEvent } from '../services/analytics-stub.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { getCwd } from '../utils/cwd.js' import { logForDebugging } from './debug.js' import { isENOENT, isFsInaccessible } from './errors.js' diff --git a/src/utils/fileHistory.ts b/src/utils/fileHistory.ts index 4e227d99..59003556 100644 --- a/src/utils/fileHistory.ts +++ b/src/utils/fileHistory.ts @@ -16,7 +16,7 @@ import { getOriginalCwd, getSessionId, } from 'src/bootstrap/state.js' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { notifyVscodeFileUpdated } from 'src/services/mcp/vscodeSdkMcp.js' import type { LogOption } from 'src/types/logs.js' import { inspect } from 'util' diff --git a/src/utils/fileOperationAnalytics.ts b/src/utils/fileOperationAnalytics.ts index 097c5a73..b2cacc4b 100644 --- a/src/utils/fileOperationAnalytics.ts +++ b/src/utils/fileOperationAnalytics.ts @@ -1,6 +1,6 @@ import { createHash } from 'crypto' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/index.js' -import { logEvent } from 'src/services/analytics/index.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics-stub.js' +import { logEvent } from '../services/analytics-stub.js' /** * Creates a truncated SHA256 hash (16 chars) for file paths diff --git a/src/utils/filePersistence/filePersistence.ts b/src/utils/filePersistence/filePersistence.ts index 9dabbab7..a939e702 100644 --- a/src/utils/filePersistence/filePersistence.ts +++ b/src/utils/filePersistence/filePersistence.ts @@ -12,7 +12,7 @@ import { join, relative } from 'path' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { type FilesApiConfig, uploadSessionFiles, diff --git a/src/utils/fingerprint-stub.ts b/src/utils/fingerprint-stub.ts new file mode 100644 index 00000000..ba1a83c5 --- /dev/null +++ b/src/utils/fingerprint-stub.ts @@ -0,0 +1,17 @@ +/** + * Stub file for fingerprint functionality + * All functions are no-ops to allow code to compile without fingerprint infrastructure + */ + +export function computeFingerprint(...args: any[]): string { + return 'no-fingerprint'; +} + +export function computeFingerprintFromMessages(...args: any[]): string { + // Return a dummy fingerprint + return 'no-fingerprint'; +} + +export function generateFingerprint(...args: any[]): string { + return 'no-fingerprint'; +} diff --git a/src/utils/fingerprint.ts b/src/utils/fingerprint.ts deleted file mode 100644 index 17906ebd..00000000 --- a/src/utils/fingerprint.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { createHash } from 'crypto' -import type { AssistantMessage, UserMessage } from '../types/message.js' - -/** - * Hardcoded salt from backend validation. - * Must match exactly for fingerprint validation to pass. - */ -export const FINGERPRINT_SALT = '59cf53e54c78' - -/** - * Extracts text content from the first user message. - * - * @param messages - Array of internal message types - * @returns First text content, or empty string if not found - */ -export function extractFirstMessageText( - messages: (UserMessage | AssistantMessage)[], -): string { - const firstUserMessage = messages.find(msg => msg.type === 'user') - if (!firstUserMessage) { - return '' - } - - const content = firstUserMessage.message.content - - if (typeof content === 'string') { - return content - } - - if (Array.isArray(content)) { - const textBlock = content.find(block => block.type === 'text') - if (textBlock && textBlock.type === 'text') { - return textBlock.text - } - } - - return '' -} - -/** - * Computes 3-character fingerprint for Claude Code attribution. - * Algorithm: SHA256(SALT + msg[4] + msg[7] + msg[20] + version)[:3] - * IMPORTANT: Do not change this method without careful coordination with - * 1P and 3P (Bedrock, Vertex, Azure) APIs. - * - * @param messageText - First user message text content - * @param version - Version string (from MACRO.VERSION) - * @returns 3-character hex fingerprint - */ -export function computeFingerprint( - messageText: string, - version: string, -): string { - // Extract chars at indices [4, 7, 20], use "0" if index not found - const indices = [4, 7, 20] - const chars = indices.map(i => messageText[i] || '0').join('') - - const fingerprintInput = `${FINGERPRINT_SALT}${chars}${version}` - - // SHA256 hash, return first 3 hex chars - const hash = createHash('sha256').update(fingerprintInput).digest('hex') - return hash.slice(0, 3) -} - -/** - * Computes fingerprint from the first user message. - * - * @param messages - Array of normalized messages - * @returns 3-character hex fingerprint - */ -export function computeFingerprintFromMessages( - messages: (UserMessage | AssistantMessage)[], -): string { - const firstMessageText = extractFirstMessageText(messages) - return computeFingerprint(firstMessageText, MACRO.VERSION) -} diff --git a/src/utils/forkedAgent.ts b/src/utils/forkedAgent.ts index eff95953..9b6ad9fd 100644 --- a/src/utils/forkedAgent.ts +++ b/src/utils/forkedAgent.ts @@ -17,7 +17,7 @@ import { query } from '../query.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { accumulateUsage, updateUsage } from '../services/api/claude.js' import { EMPTY_USAGE, type NonNullableUsage } from '../services/api/logging.js' import type { ToolUseContext } from '../Tool.js' diff --git a/src/utils/getWorktreePaths.ts b/src/utils/getWorktreePaths.ts index fdac0bcb..517b3a21 100644 --- a/src/utils/getWorktreePaths.ts +++ b/src/utils/getWorktreePaths.ts @@ -1,5 +1,5 @@ import { sep } from 'path' -import { logEvent } from '../services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { execFileNoThrowWithCwd } from './execFileNoThrow.js' import { gitExe } from './git.js' diff --git a/src/utils/gracefulShutdown.ts b/src/utils/gracefulShutdown.ts index 03e6233e..598b95fd 100644 --- a/src/utils/gracefulShutdown.ts +++ b/src/utils/gracefulShutdown.ts @@ -29,12 +29,12 @@ import { supportsTabStatus, wrapForMultiplexer, } from '../ink/termio/osc.js' -import { shutdownDatadog } from '../services/analytics/datadog.js' -import { shutdown1PEventLogging } from '../services/analytics/firstPartyEventLogger.js' +import { shutdownDatadog } from '../services/analytics-stub.js' +import { shutdown1PEventLogging } from '../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import type { AppState } from '../state/AppState.js' import { runCleanupFunctions } from './cleanupRegistry.js' import { logForDebugging } from './debug.js' @@ -173,7 +173,7 @@ function printResumeHint(): void { writeSync( 1, chalk.dim( - `\nResume this session with:\nclaude --resume ${resumeArg}\n`, + `\nResume this session with:\nclaudium --resume ${resumeArg}\n`, ), ) resumeHintPrinted = true diff --git a/src/utils/handlePromptSubmit.ts b/src/utils/handlePromptSubmit.ts index 4e514dd0..0be393d3 100644 --- a/src/utils/handlePromptSubmit.ts +++ b/src/utils/handlePromptSubmit.ts @@ -1,6 +1,6 @@ import type { UUID } from 'crypto' -import { logEvent } from 'src/services/analytics/index.js' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/metadata.js' +import { logEvent } from '../services/analytics-stub.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics-stub.js' import { type Command, getCommandName, isCommandEnabled } from '../commands.js' import { selectableUserMessagesFilter } from '../components/MessageSelector.js' import type { SpinnerMode } from '../components/Spinner/types.js' diff --git a/src/utils/headlessProfiler.ts b/src/utils/headlessProfiler.ts index 50aadaf2..629ed203 100644 --- a/src/utils/headlessProfiler.ts +++ b/src/utils/headlessProfiler.ts @@ -16,7 +16,7 @@ import { getIsNonInteractiveSession } from '../bootstrap/state.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { logForDebugging } from './debug.js' import { isEnvTruthy } from './envUtils.js' import { getPerformance } from './profilerBase.js' diff --git a/src/utils/heapDumpService.ts b/src/utils/heapDumpService.ts index c451d85a..9fa1c2eb 100644 --- a/src/utils/heapDumpService.ts +++ b/src/utils/heapDumpService.ts @@ -14,7 +14,7 @@ import { type HeapSpaceInfo, } from 'v8' import { getSessionId } from '../bootstrap/state.js' -import { logEvent } from '../services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { logForDebugging } from './debug.js' import { toError } from './errors.js' import { getDesktopPath } from './file.js' diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts index 41985744..1394b8c0 100644 --- a/src/utils/hooks.ts +++ b/src/utils/hooks.ts @@ -54,14 +54,14 @@ import { import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, -} from 'src/services/analytics/index.js' -import { logOTelEvent } from './telemetry/events.js' +} from '../services/analytics-stub.js' +import { logOTelEvent } from './telemetry-stub.js' import { ALLOWED_OFFICIAL_MARKETPLACE_NAMES } from './plugins/schemas.js' import { startHookSpan, endHookSpan, isBetaTracingEnabled, -} from './telemetry/sessionTracing.js' +} from './telemetry-stub.js' import { hookJSONOutputSchema, promptRequestSchema, diff --git a/src/utils/hooks/execAgentHook.ts b/src/utils/hooks/execAgentHook.ts index 3282670f..1ff22259 100644 --- a/src/utils/hooks/execAgentHook.ts +++ b/src/utils/hooks/execAgentHook.ts @@ -1,8 +1,8 @@ import { randomUUID } from 'crypto' import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js' import { query } from '../../query.js' -import { logEvent } from '../../services/analytics/index.js' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js' +import { logEvent } from '../../services/analytics-stub.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics-stub.js' import type { ToolUseContext } from '../../Tool.js' import { type Tool, toolMatchesName } from '../../Tool.js' import { SYNTHETIC_OUTPUT_TOOL_NAME } from '../../tools/SyntheticOutputTool/SyntheticOutputTool.js' diff --git a/src/utils/hooks/skillImprovement.ts b/src/utils/hooks/skillImprovement.ts index 564a3f89..af42e1a6 100644 --- a/src/utils/hooks/skillImprovement.ts +++ b/src/utils/hooks/skillImprovement.ts @@ -1,11 +1,11 @@ import { feature } from 'bun:bundle' import { getInvokedSkillsForAgent } from '../../bootstrap/state.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { queryModelWithoutStreaming } from '../../services/api/claude.js' import { getEmptyToolPermissionContext } from '../../Tool.js' import type { Message } from '../../types/message.js' diff --git a/src/utils/ide.ts b/src/utils/ide.ts index fe6d55d7..6a5a3fa8 100644 --- a/src/utils/ide.ts +++ b/src/utils/ide.ts @@ -6,7 +6,7 @@ import memoize from 'lodash-es/memoize.js' import { createConnection } from 'net' import * as os from 'os' import { basename, join, sep as pathSeparator, resolve } from 'path' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { getIsScrollDraining, getOriginalCwd } from '../bootstrap/state.js' import { callIdeRpc } from '../services/mcp/client.js' import type { diff --git a/src/utils/imagePaste.ts b/src/utils/imagePaste.ts index 9d3c2b04..c63793d2 100644 --- a/src/utils/imagePaste.ts +++ b/src/utils/imagePaste.ts @@ -7,7 +7,7 @@ import { IMAGE_MAX_WIDTH, IMAGE_TARGET_RAW_SIZE, } from '../constants/apiLimits.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { getImageProcessor } from '../tools/FileReadTool/imageProcessor.js' import { logForDebugging } from './debug.js' import { execFileNoThrowWithCwd } from './execFileNoThrow.js' diff --git a/src/utils/imageResizer.ts b/src/utils/imageResizer.ts index 149b4087..2c7fbab9 100644 --- a/src/utils/imageResizer.ts +++ b/src/utils/imageResizer.ts @@ -8,7 +8,7 @@ import { IMAGE_MAX_WIDTH, IMAGE_TARGET_RAW_SIZE, } from '../constants/apiLimits.js' -import { logEvent } from '../services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { getImageProcessor, type SharpFunction, diff --git a/src/utils/imageValidation.ts b/src/utils/imageValidation.ts index 401f50a2..c4eae17c 100644 --- a/src/utils/imageValidation.ts +++ b/src/utils/imageValidation.ts @@ -1,5 +1,5 @@ import { API_IMAGE_MAX_BASE64_SIZE } from '../constants/apiLimits.js' -import { logEvent } from '../services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { formatFileSize } from './format.js' /** diff --git a/src/utils/immediateCommand.ts b/src/utils/immediateCommand.ts index faa5733c..89b36840 100644 --- a/src/utils/immediateCommand.ts +++ b/src/utils/immediateCommand.ts @@ -1,4 +1,4 @@ -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' /** * Whether inference-config commands (/model, /fast, /effort) should execute diff --git a/src/utils/markdownConfigLoader.ts b/src/utils/markdownConfigLoader.ts index 68729976..f8197b02 100644 --- a/src/utils/markdownConfigLoader.ts +++ b/src/utils/markdownConfigLoader.ts @@ -7,7 +7,7 @@ import { dirname, join, resolve, sep } from 'path' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../services/analytics-stub.js' import { getProjectRoot } from '../bootstrap/state.js' import { logForDebugging } from './debug.js' import { getClaudeConfigHomeDir, isEnvTruthy } from './envUtils.js' diff --git a/src/utils/mcpInstructionsDelta.ts b/src/utils/mcpInstructionsDelta.ts index acac0c1e..71c57209 100644 --- a/src/utils/mcpInstructionsDelta.ts +++ b/src/utils/mcpInstructionsDelta.ts @@ -1,5 +1,5 @@ -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' -import { logEvent } from '../services/analytics/index.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' +import { logEvent } from '../services/analytics-stub.js' import type { ConnectedMCPServer, MCPServerConnection, diff --git a/src/utils/mcpOutputStorage.ts b/src/utils/mcpOutputStorage.ts index 530d185b..39ceed2a 100644 --- a/src/utils/mcpOutputStorage.ts +++ b/src/utils/mcpOutputStorage.ts @@ -3,7 +3,7 @@ import { join } from 'path' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import type { MCPResultType } from '../services/mcp/client.js' import { toError } from './errors.js' import { formatFileSize } from './format.js' diff --git a/src/utils/mcpValidation.ts b/src/utils/mcpValidation.ts index d2dcc7d5..4c4645fa 100644 --- a/src/utils/mcpValidation.ts +++ b/src/utils/mcpValidation.ts @@ -3,7 +3,7 @@ import type { ImageBlockParam, TextBlockParam, } from '@anthropic-ai/sdk/resources/index.mjs' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { countMessagesTokensWithAPI, roughTokenCountEstimation, diff --git a/src/utils/memoryPressureAdvisor.ts b/src/utils/memoryPressureAdvisor.ts new file mode 100644 index 00000000..031e4f7b --- /dev/null +++ b/src/utils/memoryPressureAdvisor.ts @@ -0,0 +1,234 @@ +/** + * Adaptive Memory Pressure Advisor + * + * Monitors RSS memory usage and adjusts GC frequency based on pressure level. + * Replaces the blind fixed-interval GC that could contribute to death spirals. + * + * Problem: At ~2000+ messages, React Fiber trees cause severe memory pressure. + * Bun's GC behavior triggers excessive mmap/munmap → madvise() storms → soft lockup. + * + * Solution: Adaptive monitoring with 3 states (NORMAL → ELEVATED → CRITICAL) + * that adjusts GC frequency and triggers more aggressive collection when needed. + */ + +import { logForDebugging } from './debug.js' + +/** Memory pressure states */ +export type MemoryPressureState = 'NORMAL' | 'ELEVATED' | 'CRITICAL' + +/** Memory pressure thresholds in MB */ +const RSS_THRESHOLD_ELEVATED_MB = 500 +const RSS_THRESHOLD_CRITICAL_MB = 1500 + +/** GC intervals based on state (milliseconds) */ +const GC_INTERVAL_NORMAL_MS = 5000 +const GC_INTERVAL_ELEVATED_MS = 2000 +const GC_INTERVAL_CRITICAL_MS = 500 + +/** Number of samples to keep for trend analysis */ +const TREND_WINDOW_SIZE = 5 + +/** Growth rate threshold for CRITICAL (MB per sample) */ +const GROWTH_RATE_CRITICAL_MB = 100 + +/** + * Tracks memory usage and RSS growth trend + */ +interface MemorySample { + rss: number + timestamp: number +} + +/** + * Adaptive memory pressure advisor with 3-state machine: + * - NORMAL: Memory usage is healthy, infrequent GC + * - ELEVATED: Memory pressure building, moderate GC frequency + * - CRITICAL: High memory pressure, frequent GC to prevent death spiral + */ +export class MemoryPressureAdvisor { + private samples: MemorySample[] = [] + private currentState: MemoryPressureState = 'NORMAL' + private gcTimer: ReturnType | null = null + private isRunning = false + + /** + * Get current RSS in MB + */ + private getRssMB(): number { + if (typeof Bun !== 'undefined') { + const mem = Bun.memory() + return mem.rss / (1024 * 1024) + } + // Fallback for non-Bun environment + const usage = process.memoryUsage() + return usage.rss / (1024 * 1024) + } + + /** + * Compute RSS growth rate in MB per sample interval + */ + private computeGrowthRateMB(): number { + if (this.samples.length < 2) return 0 + const oldest = this.samples[0]! + const newest = this.samples[this.samples.length - 1]! + const timeDeltaSec = (newest.timestamp - oldest.timestamp) / 1000 + if (timeDeltaSec === 0) return 0 + return (newest.rss - oldest.rss) / timeDeltaSec + } + + /** + * Determine pressure state based on RSS and growth trend + */ + private computeState(rssMB: number): MemoryPressureState { + // Check for critical growth rate first (predictive) + const growthRate = this.computeGrowthRateMB() + if (growthRate > GROWTH_RATE_CRITICAL_MB) { + return 'CRITICAL' + } + + // Check absolute thresholds + if (rssMB >= RSS_THRESHOLD_CRITICAL_MB) { + return 'CRITICAL' + } + if (rssMB >= RSS_THRESHOLD_ELEVATED_MB) { + return 'ELEVATED' + } + return 'NORMAL' + } + + /** + * Get GC interval for given state + */ + private getGcInterval(state: MemoryPressureState): number { + switch (state) { + case 'CRITICAL': + return GC_INTERVAL_CRITICAL_MS + case 'ELEVATED': + return GC_INTERVAL_ELEVATED_MS + case 'NORMAL': + default: + return GC_INTERVAL_NORMAL_MS + } + } + + /** + * Record a memory sample + */ + private recordSample(rss: number): void { + this.samples.push({ rss, timestamp: Date.now() }) + // Keep only recent samples for trend analysis + if (this.samples.length > TREND_WINDOW_SIZE) { + this.samples.shift() + } + } + + /** + * Perform adaptive GC based on current state + */ + private adaptiveGC(): void { + const rssMB = this.getRssMB() + this.recordSample(rssMB) + const newState = this.computeState(rssMB) + + // Log state transitions + if (newState !== this.currentState) { + const prevState = this.currentState + this.currentState = newState + logForDebugging( + `[MemoryPressure] State: ${prevState} → ${newState} (RSS: ${rssMB.toFixed(1)} MB)`, + ) + this.restartTimer() + } + + // Only trigger full GC when memory is elevated + if (newState !== 'NORMAL' && typeof Bun !== 'undefined') { + Bun.gc(true) + } + } + + /** + * Restart the GC timer with updated interval + */ + private restartTimer(): void { + if (this.gcTimer) { + clearInterval(this.gcTimer) + } + const interval = this.getGcInterval(this.currentState) + this.gcTimer = setInterval(() => this.adaptiveGC(), interval) + this.gcTimer.unref() + } + + /** + * Start the memory pressure advisor + */ + start(): void { + if (this.isRunning) return + this.isRunning = true + + // Initial sample + const initialRSS = this.getRssMB() + this.recordSample(initialRSS) + this.currentState = this.computeState(initialRSS) + + logForDebugging( + `[MemoryPressure] Started - Initial RSS: ${initialRSS.toFixed(1)} MB, State: ${this.currentState}`, + ) + + // Start the adaptive GC timer + this.restartTimer() + } + + /** + * Stop the memory pressure advisor + */ + stop(): void { + if (!this.isRunning) return + this.isRunning = false + + if (this.gcTimer) { + clearInterval(this.gcTimer) + this.gcTimer = null + } + this.samples = [] + this.currentState = 'NORMAL' + + logForDebugging('[MemoryPressure] Stopped') + } + + /** + * Get current memory pressure state + */ + getState(): MemoryPressureState { + return this.currentState + } +} + +// Singleton instance +let advisorInstance: MemoryPressureAdvisor | null = null + +/** + * Start the global memory pressure advisor + */ +export function startMemoryPressureAdvisor(): MemoryPressureAdvisor { + if (!advisorInstance) { + advisorInstance = new MemoryPressureAdvisor() + } + advisorInstance.start() + return advisorInstance +} + +/** + * Get the global memory pressure advisor instance + */ +export function getMemoryPressureAdvisor(): MemoryPressureAdvisor | null { + return advisorInstance +} + +/** + * Stop the global memory pressure advisor + */ +export function stopMemoryPressureAdvisor(): void { + if (advisorInstance) { + advisorInstance.stop() + } +} diff --git a/src/utils/messages.ts b/src/utils/messages.ts index 7d8db974..61dba5eb 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -18,8 +18,8 @@ import last from 'lodash-es/last.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' -import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js' +} from '../services/analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../services/analytics-stub.js' import type { AgentId } from 'src/types/ids.js' import { companionIntroText } from '../buddy/prompt.js' import { NO_CONTENT_MESSAGE } from '../constants/messages.js' @@ -28,7 +28,7 @@ import { isAutoMemoryEnabled } from '../memdir/paths.js' import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE, getFeatureValue_CACHED_MAY_BE_STALE, -} from '../services/analytics/growthbook.js' +} from '../services/analytics-stub.js' import { getImageTooLargeErrorMessage, getPdfInvalidErrorMessage, diff --git a/src/utils/model/antModels.ts b/src/utils/model/antModels.ts index bde34efd..6b22716f 100644 --- a/src/utils/model/antModels.ts +++ b/src/utils/model/antModels.ts @@ -1,4 +1,4 @@ -import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import type { EffortLevel } from '../effort.js' export type AntModel = { diff --git a/src/utils/model/configs.ts b/src/utils/model/configs.ts index 89f243d8..8587b276 100644 --- a/src/utils/model/configs.ts +++ b/src/utils/model/configs.ts @@ -3,6 +3,17 @@ import type { APIProvider } from './providers.js' export type ModelConfig = Record +// --------------------------------------------------------------------------- +// OpenAI-compatible model mappings +// Maps Claude model tiers to sensible defaults for popular providers. +// Override with OPENAI_MODEL, ANTHROPIC_MODEL, or settings.model +// --------------------------------------------------------------------------- +export const OPENAI_MODEL_DEFAULTS = { + opus: 'gpt-4o', // best reasoning + sonnet: 'gpt-4o-mini', // balanced + haiku: 'gpt-4o-mini', // fast & cheap +} as const + // @[MODEL LAUNCH]: Add a new CLAUDE_*_CONFIG constant here. Double check the correct model strings // here since the pattern may change. @@ -11,6 +22,7 @@ export const CLAUDE_3_7_SONNET_CONFIG = { bedrock: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', vertex: 'claude-3-7-sonnet@20250219', foundry: 'claude-3-7-sonnet', + openai: 'gpt-4o-mini', } as const satisfies ModelConfig export const CLAUDE_3_5_V2_SONNET_CONFIG = { @@ -18,6 +30,7 @@ export const CLAUDE_3_5_V2_SONNET_CONFIG = { bedrock: 'anthropic.claude-3-5-sonnet-20241022-v2:0', vertex: 'claude-3-5-sonnet-v2@20241022', foundry: 'claude-3-5-sonnet', + openai: 'gpt-4o-mini', } as const satisfies ModelConfig export const CLAUDE_3_5_HAIKU_CONFIG = { @@ -25,6 +38,7 @@ export const CLAUDE_3_5_HAIKU_CONFIG = { bedrock: 'us.anthropic.claude-3-5-haiku-20241022-v1:0', vertex: 'claude-3-5-haiku@20241022', foundry: 'claude-3-5-haiku', + openai: 'gpt-4o-mini', } as const satisfies ModelConfig export const CLAUDE_HAIKU_4_5_CONFIG = { @@ -32,6 +46,7 @@ export const CLAUDE_HAIKU_4_5_CONFIG = { bedrock: 'us.anthropic.claude-haiku-4-5-20251001-v1:0', vertex: 'claude-haiku-4-5@20251001', foundry: 'claude-haiku-4-5', + openai: 'gpt-4o-mini', } as const satisfies ModelConfig export const CLAUDE_SONNET_4_CONFIG = { @@ -39,6 +54,7 @@ export const CLAUDE_SONNET_4_CONFIG = { bedrock: 'us.anthropic.claude-sonnet-4-20250514-v1:0', vertex: 'claude-sonnet-4@20250514', foundry: 'claude-sonnet-4', + openai: 'gpt-4o-mini', } as const satisfies ModelConfig export const CLAUDE_SONNET_4_5_CONFIG = { @@ -46,6 +62,7 @@ export const CLAUDE_SONNET_4_5_CONFIG = { bedrock: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', vertex: 'claude-sonnet-4-5@20250929', foundry: 'claude-sonnet-4-5', + openai: 'gpt-4o', } as const satisfies ModelConfig export const CLAUDE_OPUS_4_CONFIG = { @@ -53,6 +70,7 @@ export const CLAUDE_OPUS_4_CONFIG = { bedrock: 'us.anthropic.claude-opus-4-20250514-v1:0', vertex: 'claude-opus-4@20250514', foundry: 'claude-opus-4', + openai: 'gpt-4o', } as const satisfies ModelConfig export const CLAUDE_OPUS_4_1_CONFIG = { @@ -60,6 +78,7 @@ export const CLAUDE_OPUS_4_1_CONFIG = { bedrock: 'us.anthropic.claude-opus-4-1-20250805-v1:0', vertex: 'claude-opus-4-1@20250805', foundry: 'claude-opus-4-1', + openai: 'gpt-4o', } as const satisfies ModelConfig export const CLAUDE_OPUS_4_5_CONFIG = { @@ -67,6 +86,7 @@ export const CLAUDE_OPUS_4_5_CONFIG = { bedrock: 'us.anthropic.claude-opus-4-5-20251101-v1:0', vertex: 'claude-opus-4-5@20251101', foundry: 'claude-opus-4-5', + openai: 'gpt-4o', } as const satisfies ModelConfig export const CLAUDE_OPUS_4_6_CONFIG = { @@ -74,6 +94,7 @@ export const CLAUDE_OPUS_4_6_CONFIG = { bedrock: 'us.anthropic.claude-opus-4-6-v1', vertex: 'claude-opus-4-6', foundry: 'claude-opus-4-6', + openai: 'gpt-4o', } as const satisfies ModelConfig export const CLAUDE_SONNET_4_6_CONFIG = { @@ -81,6 +102,7 @@ export const CLAUDE_SONNET_4_6_CONFIG = { bedrock: 'us.anthropic.claude-sonnet-4-6', vertex: 'claude-sonnet-4-6', foundry: 'claude-sonnet-4-6', + openai: 'gpt-4o-mini', } as const satisfies ModelConfig // @[MODEL LAUNCH]: Register the new config here. diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 85e369fa..8fe7a177 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -34,7 +34,13 @@ export type ModelName = string export type ModelSetting = ModelName | ModelAlias | null export function getSmallFastModel(): ModelName { - return process.env.ANTHROPIC_SMALL_FAST_MODEL || getDefaultHaikuModel() + if (process.env.ANTHROPIC_SMALL_FAST_MODEL) return process.env.ANTHROPIC_SMALL_FAST_MODEL + // For OpenAI provider, honor an explicit OPENAI_MODEL first; otherwise fall + // back to the Haiku default chain so ANTHROPIC_DEFAULT_HAIKU_MODEL also works. + if (getAPIProvider() === 'openai') { + return process.env.OPENAI_MODEL || getDefaultHaikuModel() + } + return getDefaultHaikuModel() } export function isNonCustomOpusModel(model: ModelName): boolean { @@ -66,7 +72,7 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined { specifiedModel = modelOverride } else { const settings = getSettings_DEPRECATED() || {} - specifiedModel = process.env.ANTHROPIC_MODEL || settings.model || undefined + specifiedModel = process.env.ANTHROPIC_MODEL || process.env.OPENAI_MODEL || settings.model || undefined } // Ignore the user-specified model if it's not in the availableModels allowlist. diff --git a/src/utils/model/providers.ts b/src/utils/model/providers.ts index aba9b7d7..b78e757a 100644 --- a/src/utils/model/providers.ts +++ b/src/utils/model/providers.ts @@ -1,16 +1,18 @@ -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/index.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics-stub.js' import { isEnvTruthy } from '../envUtils.js' -export type APIProvider = 'firstParty' | 'bedrock' | 'vertex' | 'foundry' +export type APIProvider = 'firstParty' | 'bedrock' | 'vertex' | 'foundry' | 'openai' export function getAPIProvider(): APIProvider { - return isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) - ? 'bedrock' - : isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) - ? 'vertex' - : isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) - ? 'foundry' - : 'firstParty' + return isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) + ? 'openai' + : isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) + ? 'bedrock' + : isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) + ? 'vertex' + : isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) + ? 'foundry' + : 'firstParty' } export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS { diff --git a/src/utils/modelCost.ts b/src/utils/modelCost.ts index b4867d4e..e8969238 100644 --- a/src/utils/modelCost.ts +++ b/src/utils/modelCost.ts @@ -1,6 +1,6 @@ import type { BetaUsage as Usage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/index.js' -import { logEvent } from 'src/services/analytics/index.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics-stub.js' +import { logEvent } from '../services/analytics-stub.js' import { setHasUnknownModelCost } from '../bootstrap/state.js' import { isFastModeEnabled } from './fastMode.js' import { diff --git a/src/utils/nativeInstaller/download.ts b/src/utils/nativeInstaller/download.ts index fa11e670..ea07cde7 100644 --- a/src/utils/nativeInstaller/download.ts +++ b/src/utils/nativeInstaller/download.ts @@ -11,7 +11,7 @@ import axios from 'axios' import { createHash } from 'crypto' import { chmod, writeFile } from 'fs/promises' import { join } from 'path' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import type { ReleaseChannel } from '../config.js' import { logForDebugging } from '../debug.js' import { toError } from '../errors.js' diff --git a/src/utils/nativeInstaller/installer.ts b/src/utils/nativeInstaller/installer.ts index 3a3b770b..9921c11a 100644 --- a/src/utils/nativeInstaller/installer.ts +++ b/src/utils/nativeInstaller/installer.ts @@ -33,7 +33,7 @@ import { basename, delimiter, dirname, join, resolve } from 'path' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../../services/analytics-stub.js' import { getMaxVersion, shouldSkipVersion } from '../autoUpdater.js' import { registerCleanup } from '../cleanupRegistry.js' import { getGlobalConfig, saveGlobalConfig } from '../config.js' diff --git a/src/utils/nativeInstaller/pidLock.ts b/src/utils/nativeInstaller/pidLock.ts index 5e820279..601ee47c 100644 --- a/src/utils/nativeInstaller/pidLock.ts +++ b/src/utils/nativeInstaller/pidLock.ts @@ -10,7 +10,7 @@ */ import { basename, join } from 'path' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { logForDebugging } from '../debug.js' import { isEnvDefinedFalsy, isEnvTruthy } from '../envUtils.js' import { isENOENT, toError } from '../errors.js' diff --git a/src/utils/permissions/filesystem.ts b/src/utils/permissions/filesystem.ts index 320b6235..30886c9f 100644 --- a/src/utils/permissions/filesystem.ts +++ b/src/utils/permissions/filesystem.ts @@ -13,7 +13,7 @@ import { } from 'src/tools/FileEditTool/constants.js' import type { z } from 'zod/v4' import { getOriginalCwd, getSessionId } from '../../bootstrap/state.js' -import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import type { AnyObject, Tool, ToolPermissionContext } from '../../Tool.js' import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js' import { getCwd } from '../cwd.js' diff --git a/src/utils/permissions/permissionExplainer.ts b/src/utils/permissions/permissionExplainer.ts index e2ef3ed0..bc5eaf79 100644 --- a/src/utils/permissions/permissionExplainer.ts +++ b/src/utils/permissions/permissionExplainer.ts @@ -1,6 +1,6 @@ import { z } from 'zod/v4' -import { logEvent } from '../../services/analytics/index.js' -import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js' +import { logEvent } from '../../services/analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../../services/analytics-stub.js' import type { AssistantMessage, Message } from '../../types/message.js' import { getGlobalConfig } from '../config.js' import { logForDebugging } from '../debug.js' diff --git a/src/utils/permissions/permissionSetup.ts b/src/utils/permissions/permissionSetup.ts index 8520da84..9c8dacb3 100644 --- a/src/utils/permissions/permissionSetup.ts +++ b/src/utils/permissions/permissionSetup.ts @@ -39,7 +39,7 @@ import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE, getDynamicConfig_BLOCKS_ON_INIT, getFeatureValue_CACHED_MAY_BE_STALE, -} from 'src/services/analytics/growthbook.js' +} from '../../services/analytics-stub.js' import { addDirHelpMessage, validateDirectoryForWorkspace, @@ -47,7 +47,7 @@ import { import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { AGENT_TOOL_NAME } from '../../tools/AgentTool/constants.js' import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js' /* eslint-enable @typescript-eslint/no-require-imports */ diff --git a/src/utils/permissions/permissions.ts b/src/utils/permissions/permissions.ts index 99db042d..24686a60 100644 --- a/src/utils/permissions/permissions.ts +++ b/src/utils/permissions/permissions.ts @@ -70,12 +70,12 @@ import { getTotalInputTokens, getTotalOutputTokens, } from '../../bootstrap/state.js' -import { getFeatureValue_CACHED_WITH_REFRESH } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_WITH_REFRESH } from '../../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' -import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js' +} from '../../services/analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../../services/analytics-stub.js' import { clearClassifierChecking, setClassifierChecking, diff --git a/src/utils/permissions/yoloClassifier.ts b/src/utils/permissions/yoloClassifier.ts index 1ec78b51..0ae5c110 100644 --- a/src/utils/permissions/yoloClassifier.ts +++ b/src/utils/permissions/yoloClassifier.ts @@ -10,9 +10,9 @@ import { getSessionId, setLastClassifierRequests, } from '../../bootstrap/state.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' -import { logEvent } from '../../services/analytics/index.js' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' +import { logEvent } from '../../services/analytics-stub.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics-stub.js' import { getCacheControl } from '../../services/api/claude.js' import { parsePromptTooLongTokenCounts } from '../../services/api/errors.js' import { getDefaultMaxRetries } from '../../services/api/withRetry.js' diff --git a/src/utils/planModeV2.ts b/src/utils/planModeV2.ts index ffacb46d..31639547 100644 --- a/src/utils/planModeV2.ts +++ b/src/utils/planModeV2.ts @@ -1,4 +1,4 @@ -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { getRateLimitTier, getSubscriptionType } from './auth.js' import { isEnvDefinedFalsy, isEnvTruthy } from './envUtils.js' diff --git a/src/utils/plugins/fetchTelemetry.ts b/src/utils/plugins/fetchTelemetry.ts index cf30af47..1755fd45 100644 --- a/src/utils/plugins/fetchTelemetry.ts +++ b/src/utils/plugins/fetchTelemetry.ts @@ -15,7 +15,7 @@ import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { OFFICIAL_MARKETPLACE_NAME } from './officialMarketplace.js' export type PluginFetchSource = diff --git a/src/utils/plugins/headlessPluginInstall.ts b/src/utils/plugins/headlessPluginInstall.ts index 2839f433..2f768e17 100644 --- a/src/utils/plugins/headlessPluginInstall.ts +++ b/src/utils/plugins/headlessPluginInstall.ts @@ -9,7 +9,7 @@ * ZIP creation on install and extraction on load transparently. */ -import { logEvent } from '../../services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import { registerCleanup } from '../cleanupRegistry.js' import { logForDebugging } from '../debug.js' import { withDiagnosticsTiming } from '../diagLogs.js' diff --git a/src/utils/plugins/hintRecommendation.ts b/src/utils/plugins/hintRecommendation.ts index 4c39e1ff..f0a1aa74 100644 --- a/src/utils/plugins/hintRecommendation.ts +++ b/src/utils/plugins/hintRecommendation.ts @@ -10,12 +10,12 @@ * marketplace filtering is hardcoded for v1. */ -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { type ClaudeCodeHint, hasShownHintThisSession, diff --git a/src/utils/plugins/marketplaceManager.ts b/src/utils/plugins/marketplaceManager.ts index c7c84b61..570c4973 100644 --- a/src/utils/plugins/marketplaceManager.ts +++ b/src/utils/plugins/marketplaceManager.ts @@ -23,7 +23,7 @@ import { writeFile } from 'fs/promises' import isEqual from 'lodash-es/isEqual.js' import memoize from 'lodash-es/memoize.js' import { basename, dirname, isAbsolute, join, resolve, sep } from 'path' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { logForDebugging } from '../debug.js' import { isEnvTruthy } from '../envUtils.js' import { diff --git a/src/utils/plugins/officialMarketplaceGcs.ts b/src/utils/plugins/officialMarketplaceGcs.ts index 987636b9..5fc3d773 100644 --- a/src/utils/plugins/officialMarketplaceGcs.ts +++ b/src/utils/plugins/officialMarketplaceGcs.ts @@ -12,8 +12,8 @@ import axios from 'axios' import { chmod, mkdir, readFile, rename, rm, writeFile } from 'fs/promises' import { dirname, join, resolve, sep } from 'path' import { waitForScrollIdle } from '../../bootstrap/state.js' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/index.js' -import { logEvent } from '../../services/analytics/index.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics-stub.js' +import { logEvent } from '../../services/analytics-stub.js' import { logForDebugging } from '../debug.js' import { parseZipModes, unzipFile } from '../dxt/zip.js' import { errorMessage, getErrnoCode } from '../errors.js' diff --git a/src/utils/plugins/officialMarketplaceStartupCheck.ts b/src/utils/plugins/officialMarketplaceStartupCheck.ts index d8fb2eba..d3e6542c 100644 --- a/src/utils/plugins/officialMarketplaceStartupCheck.ts +++ b/src/utils/plugins/officialMarketplaceStartupCheck.ts @@ -9,8 +9,8 @@ */ import { join } from 'path' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' -import { logEvent } from '../../services/analytics/index.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' +import { logEvent } from '../../services/analytics-stub.js' import { getGlobalConfig, saveGlobalConfig } from '../config.js' import { logForDebugging } from '../debug.js' import { isEnvTruthy } from '../envUtils.js' diff --git a/src/utils/plugins/pluginInstallationHelpers.ts b/src/utils/plugins/pluginInstallationHelpers.ts index 1e4c885f..afc33fdc 100644 --- a/src/utils/plugins/pluginInstallationHelpers.ts +++ b/src/utils/plugins/pluginInstallationHelpers.ts @@ -12,7 +12,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { getCwd } from '../cwd.js' import { toError } from '../errors.js' import { getFsImplementation } from '../fsOperations.js' @@ -21,7 +21,7 @@ import { getSettingsForSource, updateSettingsForSource, } from '../settings/settings.js' -import { buildPluginTelemetryFields } from '../telemetry/pluginTelemetry.js' +import { buildPluginTelemetryFields } from '../telemetry-stub.js' import { clearAllCaches } from './cacheUtils.js' import { formatDependencyCountSuffix, diff --git a/src/utils/preflightChecks.tsx b/src/utils/preflightChecks.tsx index ca0fe4c1..7f1c4e2e 100644 --- a/src/utils/preflightChecks.tsx +++ b/src/utils/preflightChecks.tsx @@ -1,7 +1,7 @@ import { c as _c } from "react/compiler-runtime"; import axios from 'axios'; import React, { useEffect, useState } from 'react'; -import { logEvent } from 'src/services/analytics/index.js'; +import { logEvent } from '../services/analytics-stub.js'; import { Spinner } from '../components/Spinner.js'; import { getOauthConfig } from '../constants/oauth.js'; import { useTimeout } from '../hooks/useTimeout.js'; diff --git a/src/utils/processUserInput/processBashCommand.tsx b/src/utils/processUserInput/processBashCommand.tsx index 8ab95850..37e62de1 100644 --- a/src/utils/processUserInput/processBashCommand.tsx +++ b/src/utils/processUserInput/processBashCommand.tsx @@ -6,7 +6,7 @@ import type { SetToolJSXFn } from 'src/Tool.js'; import { BashTool } from 'src/tools/BashTool/BashTool.js'; import type { AttachmentMessage, SystemMessage, UserMessage } from 'src/types/message.js'; import type { ShellProgress } from 'src/types/tools.js'; -import { logEvent } from '../../services/analytics/index.js'; +import { logEvent } from '../../services/analytics-stub.js'; import { errorMessage, ShellError } from '../errors.js'; import { createSyntheticUserCaveatMessage, createUserInterruptionMessage, createUserMessage, prepareUserContent } from '../messages.js'; import { resolveDefaultShell } from '../shell/resolveDefaultShell.js'; diff --git a/src/utils/processUserInput/processSlashCommand.tsx b/src/utils/processUserInput/processSlashCommand.tsx index a0bf451b..27ee9c5c 100644 --- a/src/utils/processUserInput/processSlashCommand.tsx +++ b/src/utils/processUserInput/processSlashCommand.tsx @@ -9,7 +9,7 @@ import type { AssistantMessage, AttachmentMessage, Message, NormalizedUserMessag import { addInvokedSkill, getSessionId } from '../../bootstrap/state.js'; import { COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG } from '../../constants/xml.js'; import type { CanUseToolFn } from '../../hooks/useCanUseTool.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent } from '../../services/analytics/index.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent } from '../../services/analytics-stub.js'; import { getDumpPromptsPath } from '../../services/api/dumpPrompts.js'; import { buildPostCompactMessages } from '../../services/compact/compact.js'; import { resetMicrocompactState } from '../../services/compact/microCompact.js'; @@ -40,8 +40,8 @@ import { isRestrictedToPluginOnly, isSourceAdminTrusted } from '../settings/plug import { parseSlashCommand } from '../slashCommandParsing.js'; import { sleep } from '../sleep.js'; import { recordSkillUsage } from '../suggestions/skillUsageTracking.js'; -import { logOTelEvent, redactIfDisabled } from '../telemetry/events.js'; -import { buildPluginCommandTelemetryFields } from '../telemetry/pluginTelemetry.js'; +import { logOTelEvent, redactIfDisabled } from '../telemetry-stub.js'; +import { buildPluginCommandTelemetryFields } from '../telemetry-stub.js'; import { getAssistantMessageContentLength } from '../tokens.js'; import { createAgentId } from '../uuid.js'; import { getWorkload } from '../workloadContext.js'; diff --git a/src/utils/processUserInput/processTextPrompt.ts b/src/utils/processUserInput/processTextPrompt.ts index cc91c5b8..ffcaf213 100644 --- a/src/utils/processUserInput/processTextPrompt.ts +++ b/src/utils/processUserInput/processTextPrompt.ts @@ -6,11 +6,11 @@ import type { SystemMessage, UserMessage, } from 'src/types/message.js' -import { logEvent } from '../../services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import type { PermissionMode } from '../../types/permissions.js' import { createUserMessage } from '../messages.js' -import { logOTelEvent, redactIfDisabled } from '../telemetry/events.js' -import { startInteractionSpan } from '../telemetry/sessionTracing.js' +import { logOTelEvent, redactIfDisabled } from '../telemetry-stub.js' +import { startInteractionSpan } from '../telemetry-stub.js' import { matchesKeepGoingKeyword, matchesNegativeKeyword, diff --git a/src/utils/processUserInput/processUserInput.ts b/src/utils/processUserInput/processUserInput.ts index d5a68c43..93b71e00 100644 --- a/src/utils/processUserInput/processUserInput.ts +++ b/src/utils/processUserInput/processUserInput.ts @@ -6,7 +6,7 @@ import type { } from '@anthropic-ai/sdk/resources/messages.mjs' import { randomUUID } from 'crypto' import type { QuerySource } from 'src/constants/querySource.js' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../../services/analytics-stub.js' import { getContentText } from 'src/utils/messages.js' import { findCommand, diff --git a/src/utils/ripgrep.ts b/src/utils/ripgrep.ts index 683da051..d762bf88 100644 --- a/src/utils/ripgrep.ts +++ b/src/utils/ripgrep.ts @@ -3,7 +3,7 @@ import { execFile, spawn } from 'child_process' import memoize from 'lodash-es/memoize.js' import { homedir } from 'os' import * as path from 'path' -import { logEvent } from 'src/services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { fileURLToPath } from 'url' import { isInBundledMode } from './bundledMode.js' import { logForDebugging } from './debug.js' diff --git a/src/utils/sessionFileAccessHooks.ts b/src/utils/sessionFileAccessHooks.ts index 08d4ed50..9432bfe8 100644 --- a/src/utils/sessionFileAccessHooks.ts +++ b/src/utils/sessionFileAccessHooks.ts @@ -9,7 +9,7 @@ import type { HookInput, HookJSONOutput } from '../entrypoints/agentSdkTypes.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { FILE_EDIT_TOOL_NAME } from '../tools/FileEditTool/constants.js' import { inputSchema as editInputSchema } from '../tools/FileEditTool/types.js' import { FileReadTool } from '../tools/FileReadTool/FileReadTool.js' diff --git a/src/utils/sessionStorage.ts b/src/utils/sessionStorage.ts index 6d775d6c..bfe7db73 100644 --- a/src/utils/sessionStorage.ts +++ b/src/utils/sessionStorage.ts @@ -20,7 +20,7 @@ import { basename, dirname, join } from 'path' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../services/analytics-stub.js' import { getOriginalCwd, getPlanSlugCache, @@ -32,7 +32,7 @@ import { } from '../bootstrap/state.js' import { builtInCommandNames } from '../commands.js' import { COMMAND_NAME_TAG, TICK_TAG } from '../constants/xml.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import * as sessionIngress from '../services/api/sessionIngress.js' import { REPL_TOOL_NAME } from '../tools/REPLTool/constants.js' import { diff --git a/src/utils/sessionTitle.ts b/src/utils/sessionTitle.ts index 5a722c88..c88c896b 100644 --- a/src/utils/sessionTitle.ts +++ b/src/utils/sessionTitle.ts @@ -14,7 +14,7 @@ import { z } from 'zod/v4' import { getIsNonInteractiveSession } from '../bootstrap/state.js' -import { logEvent } from '../services/analytics/index.js' +import { logEvent } from '../services/analytics-stub.js' import { queryHaiku } from '../services/api/claude.js' import type { Message } from '../types/message.js' import { logForDebugging } from './debug.js' diff --git a/src/utils/shell/prefix.ts b/src/utils/shell/prefix.ts index e97a37b7..55e8c355 100644 --- a/src/utils/shell/prefix.ts +++ b/src/utils/shell/prefix.ts @@ -9,11 +9,11 @@ import chalk from 'chalk' import type { QuerySource } from '../../constants/querySource.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { queryHaiku } from '../../services/api/claude.js' import { startsWithApiErrorPrefix } from '../../services/api/errors.js' import { memoizeWithLRU } from '../memoize.js' diff --git a/src/utils/sideQuery.ts b/src/utils/sideQuery.ts index 4e6d4d73..38def71b 100644 --- a/src/utils/sideQuery.ts +++ b/src/utils/sideQuery.ts @@ -10,12 +10,12 @@ import { getAttributionHeader, getCLISyspromptPrefix, } from '../constants/system.js' -import { logEvent } from '../services/analytics/index.js' -import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics/metadata.js' +import { logEvent } from '../services/analytics-stub.js' +import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics-stub.js' import { getAPIMetadata } from '../services/api/claude.js' import { getAnthropicClient } from '../services/api/client.js' import { getModelBetas, modelSupportsStructuredOutputs } from './betas.js' -import { computeFingerprint } from './fingerprint.js' +import { computeFingerprint } from './fingerprint-stub.js' import { normalizeModelStringForAPI } from './model/model.js' type MessageParam = Anthropic.MessageParam diff --git a/src/utils/sinks.ts b/src/utils/sinks.ts index 386ce6ae..fb26ca26 100644 --- a/src/utils/sinks.ts +++ b/src/utils/sinks.ts @@ -1,4 +1,4 @@ -import { initializeAnalyticsSink } from '../services/analytics/sink.js' +import { initializeAnalyticsSink } from '../services/analytics-stub.js' import { initializeErrorLogSink } from './errorLogSink.js' /** diff --git a/src/utils/skills/skillChangeDetector.ts b/src/utils/skills/skillChangeDetector.ts index a3acb5e2..7860dcab 100644 --- a/src/utils/skills/skillChangeDetector.ts +++ b/src/utils/skills/skillChangeDetector.ts @@ -8,7 +8,7 @@ import { import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { clearSkillCaches, getSkillsPath, diff --git a/src/utils/startupProfiler.ts b/src/utils/startupProfiler.ts index d16b094a..0e5714a9 100644 --- a/src/utils/startupProfiler.ts +++ b/src/utils/startupProfiler.ts @@ -14,7 +14,7 @@ import { getSessionId } from 'src/bootstrap/state.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import { logForDebugging } from './debug.js' import { getClaudeConfigHomeDir, isEnvTruthy } from './envUtils.js' import { getFsImplementation } from './fsOperations.js' diff --git a/src/utils/swarm/inProcessRunner.ts b/src/utils/swarm/inProcessRunner.ts index c85d959e..2c6b53ee 100644 --- a/src/utils/swarm/inProcessRunner.ts +++ b/src/utils/swarm/inProcessRunner.ts @@ -22,7 +22,7 @@ import { import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../../services/analytics/index.js' +} from '../../services/analytics-stub.js' import { getAutoCompactThreshold } from '../../services/compact/autoCompact.js' import { buildPostCompactMessages, @@ -96,7 +96,7 @@ import { readMailbox, writeToMailbox, } from '../teammateMailbox.js' -import { unregisterAgent as unregisterPerfettoAgent } from '../telemetry/perfettoTracing.js' +import { unregisterAgent as unregisterPerfettoAgent } from '../telemetry-stub.js' import { createContentReplacementState } from '../toolResultStorage.js' import { TEAM_LEAD_NAME } from './constants.js' import { diff --git a/src/utils/swarm/spawnInProcess.ts b/src/utils/swarm/spawnInProcess.ts index 7f548f28..9b0c36d8 100644 --- a/src/utils/swarm/spawnInProcess.ts +++ b/src/utils/swarm/spawnInProcess.ts @@ -39,7 +39,7 @@ import { isPerfettoTracingEnabled, registerAgent as registerPerfettoAgent, unregisterAgent as unregisterPerfettoAgent, -} from '../telemetry/perfettoTracing.js' +} from '../telemetry-stub.js' import { removeMemberByAgentId } from './teamHelpers.js' type SetAppStateFn = (updater: (prev: AppState) => AppState) => void diff --git a/src/utils/systemPrompt.ts b/src/utils/systemPrompt.ts index 2059c81b..104ed28b 100644 --- a/src/utils/systemPrompt.ts +++ b/src/utils/systemPrompt.ts @@ -2,7 +2,7 @@ import { feature } from 'bun:bundle' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import type { ToolUseContext } from '../Tool.js' import type { AgentDefinition } from '../tools/AgentTool/loadAgentsDir.js' import { isBuiltInAgent } from '../tools/AgentTool/loadAgentsDir.js' diff --git a/src/utils/telemetry-stub.ts b/src/utils/telemetry-stub.ts new file mode 100644 index 00000000..9b85f075 --- /dev/null +++ b/src/utils/telemetry-stub.ts @@ -0,0 +1,286 @@ +/** + * Stub file for telemetry functionality + * All functions are no-ops to allow code to compile without telemetry infrastructure + */ + +// No-op telemetry functions +export function logOTelEvent(...args: any[]): void { + // No-op +} + +export function redactIfDisabled(value: any): any { + return value; +} + +export function startInteractionSpan(...args: any[]): any { + return { end: () => {} }; +} + +export function endInteractionSpan(...args: any[]): void { + // No-op +} + +export function logPluginLoadErrors(...args: any[]): void { + // No-op +} + +export function logPluginsEnabledForSession(...args: any[]): void { + // No-op +} + +export function logSkillsLoaded(...args: any[]): void { + // No-op +} + +export function buildPluginCommandTelemetryFields(...args: any[]): Record { + return {}; +} + +export function buildPluginTelemetryFields(...args: any[]): Record { + return {}; +} + +export function isBetaTracingEnabled(): boolean { + return false; +} + +export function clearBetaTracingState(...args: any[]): void { + // No-op +} + +// Perfetto tracing stubs +export function isPerfettoTracingEnabled(): boolean { + return false; +} + +export function registerAgent(...args: any[]): void { + // No-op +} + +export function unregisterAgent(...args: any[]): void { + // No-op +} + +export function startToolSpan(...args: any[]): any { + return { end: () => {} }; +} + +export function endToolSpan(...args: any[]): void { + // No-op +} + +export function startApiSpan(...args: any[]): any { + return { end: () => {} }; +} + +export function endApiSpan(...args: any[]): void { + // No-op +} + +export function recordApiLatency(...args: any[]): void { + // No-op +} + +// Perfetto tracing stubs +export function registerAgent(...args: any[]): void { + // No-op +} + +export function unregisterAgent(...args: any[]): void { + // No-op +} + +export function beginPerfettoSpan(...args: any[]): any { + return { end: () => {} }; +} + +export function endPerfettoSpan(...args: any[]): void { + // No-op +} + +// Instrumentation stubs +export function shutdownTelemetry(...args: any[]): Promise { + return Promise.resolve(); +} + +export function flushTelemetry(...args: any[]): Promise { + return Promise.resolve(); +} + +// Additional telemetry span stubs +export function startToolBlockedOnUserSpan(...args: any[]): any { + return { end: () => {}, setAttribute: () => {} }; +} + +export function startToolSpan(...args: any[]): any { + return { end: () => {}, setAttribute: () => {} }; +} + +export function startCompactSpan(...args: any[]): any { + return { end: () => {}, setAttribute: () => {} }; +} + +export function startQuerySpan(...args: any[]): any { + return { end: () => {}, setAttribute: () => {} }; +} + +export function endSpan(...args: any[]): void { + // No-op +} + +export function setSpanAttribute(...args: any[]): void { + // No-op +} + +// Complete set of telemetry span stubs +export function endToolExecutionSpan(...args: any[]): void { + // No-op +} + +export function startToolExecutionSpan(...args: any[]): any { + return { end: () => {}, setAttribute: () => {} }; +} + +export function endToolBlockedOnUserSpan(...args: any[]): void { + // No-op +} + +export function endToolSpan(...args: any[]): void { + // No-op +} + +export function endHookSpan(...args: any[]): void { + // No-op +} + +export function startHookSpan(...args: any[]): any { + return { end: () => {}, setAttribute: () => {} }; +} + +export function startLLMRequestSpan(...args: any[]): any { + return { end: () => {}, setAttribute: () => {} }; +} + +export function endLLMRequestSpan(...args: any[]): void { + // No-op +} + +export function startUserInputPerfettoSpan(...args: any[]): any { + return { end: () => {} }; +} + +export function endUserInputPerfettoSpan(...args: any[]): void { + // No-op +} + +export function startInteractionPerfettoSpan(...args: any[]): any { + return { end: () => {} }; +} + +export function endInteractionPerfettoSpan(...args: any[]): void { + // No-op +} + +export function startLLMRequestPerfettoSpan(...args: any[]): any { + return { end: () => {} }; +} + +export function endLLMRequestPerfettoSpan(...args: any[]): void { + // No-op +} + +export function startToolPerfettoSpan(...args: any[]): any { + return { end: () => {} }; +} + +export function endToolPerfettoSpan(...args: any[]): void { + // No-op +} + +export function addBetaInteractionAttributes(...args: any[]): void { + // No-op +} + +export function addBetaLLMRequestAttributes(...args: any[]): void { + // No-op +} + +export function addBetaLLMResponseAttributes(...args: any[]): void { + // No-op +} + +export function addBetaToolInputAttributes(...args: any[]): void { + // No-op +} + +export function addBetaToolResultAttributes(...args: any[]): void { + // No-op +} + +export function addToolContentEvent(...args: any[]): void { + // No-op +} + +export function classifyPluginCommandError(...args: any[]): string { + return 'unknown'; +} + +export function emitPerfettoCounter(...args: any[]): void { + // No-op +} + +export function emitPerfettoInstant(...args: any[]): void { + // No-op +} + +export function executeInSpan(callback: () => T): T { + return callback(); +} + +export function getCurrentSpan(): any { + return { end: () => {}, setAttribute: () => {} }; +} + +export function getEnabledVia(): string { + return 'disabled'; +} + +export function getPerfettoEvents(): any[] { + return []; +} + +export function getTelemetryPluginScope(): string { + return ''; +} + +export function hashPluginId(...args: any[]): string { + return ''; +} + +export function initializePerfettoTracing(...args: any[]): void { + // No-op +} + +export function initializeTelemetry(...args: any[]): Promise { + return Promise.resolve(); +} + +export function isEnhancedTelemetryEnabled(): boolean { + return false; +} + +export function isTelemetryEnabled(): boolean { + return false; +} + +export function resetPerfettoTracer(): void { + // No-op +} + +export function truncateContent(...args: any[]): string { + return ''; +} + +export function bootstrapTelemetry(...args: any[]): void { + // No-op +} diff --git a/src/utils/telemetry/betaSessionTracing.ts b/src/utils/telemetry/betaSessionTracing.ts deleted file mode 100644 index 6699721e..00000000 --- a/src/utils/telemetry/betaSessionTracing.ts +++ /dev/null @@ -1,491 +0,0 @@ -/** - * Beta Session Tracing for Claude Code - * - * This module contains beta tracing features enabled when - * ENABLE_BETA_TRACING_DETAILED=1 and BETA_TRACING_ENDPOINT are set. - * - * For external users, tracing is enabled in SDK/headless mode, or in - * interactive mode when the org is allowlisted via the - * tengu_trace_lantern GrowthBook gate. - * For ant users, tracing is enabled in all modes. - * - * Visibility Rules: - * | Content | External | Ant | - * |------------------|----------|------| - * | System prompts | ✅ | ✅ | - * | Model output | ✅ | ✅ | - * | Thinking output | ❌ | ✅ | - * | Tools | ✅ | ✅ | - * | new_context | ✅ | ✅ | - * - * Features: - * - Per-agent message tracking with hash-based deduplication - * - System prompt logging (once per unique hash) - * - Hook execution spans - * - Detailed new_context attributes for LLM requests - */ - -import type { Span } from '@opentelemetry/api' -import { createHash } from 'crypto' -import { getIsNonInteractiveSession } from '../../bootstrap/state.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' -import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js' -import type { AssistantMessage, UserMessage } from '../../types/message.js' -import { isEnvTruthy } from '../envUtils.js' -import { jsonParse, jsonStringify } from '../slowOperations.js' -import { logOTelEvent } from './events.js' - -// Message type for API calls (UserMessage or AssistantMessage) -type APIMessage = UserMessage | AssistantMessage - -/** - * Track hashes we've already logged this session (system prompts, tools, etc). - * - * WHY: System prompts and tool schemas are large and rarely change within a session. - * Sending full content on every request would be wasteful. Instead, we hash and - * only log the full content once per unique hash. - */ -const seenHashes = new Set() - -/** - * Track the last reported message hash per querySource (agent) for incremental context. - * - * WHY: When debugging traces, we want to see what NEW information was added each turn, - * not the entire conversation history (which can be huge). By tracking the last message - * we reported per agent, we can compute and send only the delta (new messages since - * the last request). This is tracked per-agent (querySource) because different agents - * (main thread, subagents, warmup requests) have independent conversation contexts. - */ -const lastReportedMessageHash = new Map() - -/** - * Clear tracking state after compaction. - * Old hashes are irrelevant once messages have been replaced. - */ -export function clearBetaTracingState(): void { - seenHashes.clear() - lastReportedMessageHash.clear() -} - -const MAX_CONTENT_SIZE = 60 * 1024 // 60KB (Honeycomb limit is 64KB, staying safe) - -/** - * Check if beta detailed tracing is enabled. - * - Requires ENABLE_BETA_TRACING_DETAILED=1 and BETA_TRACING_ENDPOINT - * - For external users, enabled in SDK/headless mode OR when org is - * allowlisted via the tengu_trace_lantern GrowthBook gate - */ -export function isBetaTracingEnabled(): boolean { - const baseEnabled = - isEnvTruthy(process.env.ENABLE_BETA_TRACING_DETAILED) && - Boolean(process.env.BETA_TRACING_ENDPOINT) - - if (!baseEnabled) { - return false - } - - // For external users, enable in SDK/headless mode OR when org is allowlisted. - // Gate reads from disk cache, so first run after allowlisting returns false; - // works from second run onward (same behavior as enhanced_telemetry_beta). - if (process.env.USER_TYPE !== 'ant') { - return ( - getIsNonInteractiveSession() || - getFeatureValue_CACHED_MAY_BE_STALE('tengu_trace_lantern', false) - ) - } - - return true -} - -/** - * Truncate content to fit within Honeycomb limits. - */ -export function truncateContent( - content: string, - maxSize: number = MAX_CONTENT_SIZE, -): { content: string; truncated: boolean } { - if (content.length <= maxSize) { - return { content, truncated: false } - } - - return { - content: - content.slice(0, maxSize) + - '\n\n[TRUNCATED - Content exceeds 60KB limit]', - truncated: true, - } -} - -/** - * Generate a short hash (first 12 hex chars of SHA-256). - */ -function shortHash(content: string): string { - return createHash('sha256').update(content).digest('hex').slice(0, 12) -} - -/** - * Generate a hash for a system prompt. - */ -function hashSystemPrompt(systemPrompt: string): string { - return `sp_${shortHash(systemPrompt)}` -} - -/** - * Generate a hash for a message based on its content. - */ -function hashMessage(message: APIMessage): string { - const content = jsonStringify(message.message.content) - return `msg_${shortHash(content)}` -} - -// Regex to detect content wrapped in tags -const SYSTEM_REMINDER_REGEX = - /^\n?([\s\S]*?)\n?<\/system-reminder>$/ - -/** - * Check if text is entirely a system reminder (wrapped in tags). - * Returns the inner content if it is, null otherwise. - */ -function extractSystemReminderContent(text: string): string | null { - const match = text.trim().match(SYSTEM_REMINDER_REGEX) - return match && match[1] ? match[1].trim() : null -} - -/** - * Result of formatting messages - separates regular content from system reminders. - */ -interface FormattedMessages { - contextParts: string[] - systemReminders: string[] -} - -/** - * Format user messages for new_context display, separating system reminders. - * Only handles user messages (assistant messages are filtered out before this is called). - */ -function formatMessagesForContext(messages: UserMessage[]): FormattedMessages { - const contextParts: string[] = [] - const systemReminders: string[] = [] - - for (const message of messages) { - const content = message.message.content - if (typeof content === 'string') { - const reminderContent = extractSystemReminderContent(content) - if (reminderContent) { - systemReminders.push(reminderContent) - } else { - contextParts.push(`[USER]\n${content}`) - } - } else if (Array.isArray(content)) { - for (const block of content) { - if (block.type === 'text') { - const reminderContent = extractSystemReminderContent(block.text) - if (reminderContent) { - systemReminders.push(reminderContent) - } else { - contextParts.push(`[USER]\n${block.text}`) - } - } else if (block.type === 'tool_result') { - const resultContent = - typeof block.content === 'string' - ? block.content - : jsonStringify(block.content) - // Tool results can also contain system reminders (e.g., malware warning) - const reminderContent = extractSystemReminderContent(resultContent) - if (reminderContent) { - systemReminders.push(reminderContent) - } else { - contextParts.push( - `[TOOL RESULT: ${block.tool_use_id}]\n${resultContent}`, - ) - } - } - } - } - } - - return { contextParts, systemReminders } -} - -export interface LLMRequestNewContext { - /** System prompt (typically only on first request or if changed) */ - systemPrompt?: string - /** Query source identifying the agent/purpose (e.g., 'repl_main_thread', 'agent:builtin') */ - querySource?: string - /** Tool schemas sent with the request */ - tools?: string -} - -/** - * Add beta attributes to an interaction span. - * Adds new_context with the user prompt. - */ -export function addBetaInteractionAttributes( - span: Span, - userPrompt: string, -): void { - if (!isBetaTracingEnabled()) { - return - } - - const { content: truncatedPrompt, truncated } = truncateContent( - `[USER PROMPT]\n${userPrompt}`, - ) - span.setAttributes({ - new_context: truncatedPrompt, - ...(truncated && { - new_context_truncated: true, - new_context_original_length: userPrompt.length, - }), - }) -} - -/** - * Add beta attributes to an LLM request span. - * Handles system prompt logging and new_context computation. - */ -export function addBetaLLMRequestAttributes( - span: Span, - newContext?: LLMRequestNewContext, - messagesForAPI?: APIMessage[], -): void { - if (!isBetaTracingEnabled()) { - return - } - - // Add system prompt info to the span - if (newContext?.systemPrompt) { - const promptHash = hashSystemPrompt(newContext.systemPrompt) - const preview = newContext.systemPrompt.slice(0, 500) - - // Always add hash, preview, and length to the span - span.setAttribute('system_prompt_hash', promptHash) - span.setAttribute('system_prompt_preview', preview) - span.setAttribute('system_prompt_length', newContext.systemPrompt.length) - - // Log the full system prompt only once per unique hash this session - if (!seenHashes.has(promptHash)) { - seenHashes.add(promptHash) - - // Truncate for the log if needed - const { content: truncatedPrompt, truncated } = truncateContent( - newContext.systemPrompt, - ) - - void logOTelEvent('system_prompt', { - system_prompt_hash: promptHash, - system_prompt: truncatedPrompt, - system_prompt_length: String(newContext.systemPrompt.length), - ...(truncated && { system_prompt_truncated: 'true' }), - }) - } - } - - // Add tools info to the span - if (newContext?.tools) { - try { - const toolsArray = jsonParse(newContext.tools) as Record< - string, - unknown - >[] - - // Build array of {name, hash} for each tool - const toolsWithHashes = toolsArray.map(tool => { - const toolJson = jsonStringify(tool) - const toolHash = shortHash(toolJson) - return { - name: typeof tool.name === 'string' ? tool.name : 'unknown', - hash: toolHash, - json: toolJson, - } - }) - - // Set span attribute with array of name/hash pairs - span.setAttribute( - 'tools', - jsonStringify( - toolsWithHashes.map(({ name, hash }) => ({ name, hash })), - ), - ) - span.setAttribute('tools_count', toolsWithHashes.length) - - // Log each tool's full description once per unique hash - for (const { name, hash, json } of toolsWithHashes) { - if (!seenHashes.has(`tool_${hash}`)) { - seenHashes.add(`tool_${hash}`) - - const { content: truncatedTool, truncated } = truncateContent(json) - - void logOTelEvent('tool', { - tool_name: sanitizeToolNameForAnalytics(name), - tool_hash: hash, - tool: truncatedTool, - ...(truncated && { tool_truncated: 'true' }), - }) - } - } - } catch { - // If parsing fails, log the raw tools string - span.setAttribute('tools_parse_error', true) - } - } - - // Add new_context using hash-based tracking (visible to all users) - if (messagesForAPI && messagesForAPI.length > 0 && newContext?.querySource) { - const querySource = newContext.querySource - const lastHash = lastReportedMessageHash.get(querySource) - - // Find where the last reported message is in the array - let startIndex = 0 - if (lastHash) { - for (let i = 0; i < messagesForAPI.length; i++) { - const msg = messagesForAPI[i] - if (msg && hashMessage(msg) === lastHash) { - startIndex = i + 1 // Start after the last reported message - break - } - } - // If lastHash not found, startIndex stays 0 (send everything) - } - - // Get new messages (filter out assistant messages - we only want user input/tool results) - const newMessages = messagesForAPI - .slice(startIndex) - .filter((m): m is UserMessage => m.type === 'user') - - if (newMessages.length > 0) { - // Format new messages, separating system reminders from regular content - const { contextParts, systemReminders } = - formatMessagesForContext(newMessages) - - // Set new_context (regular user content and tool results) - if (contextParts.length > 0) { - const fullContext = contextParts.join('\n\n---\n\n') - const { content: truncatedContext, truncated } = - truncateContent(fullContext) - - span.setAttributes({ - new_context: truncatedContext, - new_context_message_count: newMessages.length, - ...(truncated && { - new_context_truncated: true, - new_context_original_length: fullContext.length, - }), - }) - } - - // Set system_reminders as a separate attribute - if (systemReminders.length > 0) { - const fullReminders = systemReminders.join('\n\n---\n\n') - const { content: truncatedReminders, truncated: remindersTruncated } = - truncateContent(fullReminders) - - span.setAttributes({ - system_reminders: truncatedReminders, - system_reminders_count: systemReminders.length, - ...(remindersTruncated && { - system_reminders_truncated: true, - system_reminders_original_length: fullReminders.length, - }), - }) - } - - // Update last reported hash to the last message in the array - const lastMessage = messagesForAPI[messagesForAPI.length - 1] - if (lastMessage) { - lastReportedMessageHash.set(querySource, hashMessage(lastMessage)) - } - } - } -} - -/** - * Add beta attributes to endLLMRequestSpan. - * Handles model_output and thinking_output truncation. - */ -export function addBetaLLMResponseAttributes( - endAttributes: Record, - metadata?: { - modelOutput?: string - thinkingOutput?: string - }, -): void { - if (!isBetaTracingEnabled() || !metadata) { - return - } - - // Add model_output (text content) - visible to all users - if (metadata.modelOutput !== undefined) { - const { content: modelOutput, truncated: outputTruncated } = - truncateContent(metadata.modelOutput) - endAttributes['response.model_output'] = modelOutput - if (outputTruncated) { - endAttributes['response.model_output_truncated'] = true - endAttributes['response.model_output_original_length'] = - metadata.modelOutput.length - } - } - - // Add thinking_output - ant-only - if ( - process.env.USER_TYPE === 'ant' && - metadata.thinkingOutput !== undefined - ) { - const { content: thinkingOutput, truncated: thinkingTruncated } = - truncateContent(metadata.thinkingOutput) - endAttributes['response.thinking_output'] = thinkingOutput - if (thinkingTruncated) { - endAttributes['response.thinking_output_truncated'] = true - endAttributes['response.thinking_output_original_length'] = - metadata.thinkingOutput.length - } - } -} - -/** - * Add beta attributes to startToolSpan. - * Adds tool_input with the serialized tool input. - */ -export function addBetaToolInputAttributes( - span: Span, - toolName: string, - toolInput: string, -): void { - if (!isBetaTracingEnabled()) { - return - } - - const { content: truncatedInput, truncated } = truncateContent( - `[TOOL INPUT: ${toolName}]\n${toolInput}`, - ) - span.setAttributes({ - tool_input: truncatedInput, - ...(truncated && { - tool_input_truncated: true, - tool_input_original_length: toolInput.length, - }), - }) -} - -/** - * Add beta attributes to endToolSpan. - * Adds new_context with the tool result. - */ -export function addBetaToolResultAttributes( - endAttributes: Record, - toolName: string | number | boolean, - toolResult: string, -): void { - if (!isBetaTracingEnabled()) { - return - } - - const { content: truncatedResult, truncated } = truncateContent( - `[TOOL RESULT: ${toolName}]\n${toolResult}`, - ) - endAttributes['new_context'] = truncatedResult - if (truncated) { - endAttributes['new_context_truncated'] = true - endAttributes['new_context_original_length'] = toolResult.length - } -} diff --git a/src/utils/telemetry/bigqueryExporter.ts b/src/utils/telemetry/bigqueryExporter.ts deleted file mode 100644 index 2f935c45..00000000 --- a/src/utils/telemetry/bigqueryExporter.ts +++ /dev/null @@ -1,252 +0,0 @@ -import type { Attributes, HrTime } from '@opentelemetry/api' -import { type ExportResult, ExportResultCode } from '@opentelemetry/core' -import { - AggregationTemporality, - type MetricData, - type DataPoint as OTelDataPoint, - type PushMetricExporter, - type ResourceMetrics, -} from '@opentelemetry/sdk-metrics' -import axios from 'axios' -import { checkMetricsEnabled } from 'src/services/api/metricsOptOut.js' -import { getIsNonInteractiveSession } from '../../bootstrap/state.js' -import { getSubscriptionType, isClaudeAISubscriber } from '../auth.js' -import { checkHasTrustDialogAccepted } from '../config.js' -import { logForDebugging } from '../debug.js' -import { errorMessage, toError } from '../errors.js' -import { getAuthHeaders } from '../http.js' -import { logError } from '../log.js' -import { jsonStringify } from '../slowOperations.js' -import { getClaudeCodeUserAgent } from '../userAgent.js' - -type DataPoint = { - attributes: Record - value: number - timestamp: string -} - -type Metric = { - name: string - description?: string - unit?: string - data_points: DataPoint[] -} - -type InternalMetricsPayload = { - resource_attributes: Record - metrics: Metric[] -} - -export class BigQueryMetricsExporter implements PushMetricExporter { - private readonly endpoint: string - private readonly timeout: number - private pendingExports: Promise[] = [] - private isShutdown = false - - constructor(options: { timeout?: number } = {}) { - const defaultEndpoint = 'https://api.anthropic.com/api/claude_code/metrics' - - if ( - process.env.USER_TYPE === 'ant' && - process.env.ANT_CLAUDE_CODE_METRICS_ENDPOINT - ) { - this.endpoint = - process.env.ANT_CLAUDE_CODE_METRICS_ENDPOINT + - '/api/claude_code/metrics' - } else { - this.endpoint = defaultEndpoint - } - - this.timeout = options.timeout || 5000 - } - - async export( - metrics: ResourceMetrics, - resultCallback: (result: ExportResult) => void, - ): Promise { - if (this.isShutdown) { - resultCallback({ - code: ExportResultCode.FAILED, - error: new Error('Exporter has been shutdown'), - }) - return - } - - const exportPromise = this.doExport(metrics, resultCallback) - this.pendingExports.push(exportPromise) - - // Clean up completed exports - void exportPromise.finally(() => { - const index = this.pendingExports.indexOf(exportPromise) - if (index > -1) { - void this.pendingExports.splice(index, 1) - } - }) - } - - private async doExport( - metrics: ResourceMetrics, - resultCallback: (result: ExportResult) => void, - ): Promise { - try { - // Skip if trust not established in interactive mode - // This prevents triggering apiKeyHelper before trust dialog - const hasTrust = - checkHasTrustDialogAccepted() || getIsNonInteractiveSession() - if (!hasTrust) { - logForDebugging( - 'BigQuery metrics export: trust not established, skipping', - ) - resultCallback({ code: ExportResultCode.SUCCESS }) - return - } - - // Check organization-level metrics opt-out - const metricsStatus = await checkMetricsEnabled() - if (!metricsStatus.enabled) { - logForDebugging('Metrics export disabled by organization setting') - resultCallback({ code: ExportResultCode.SUCCESS }) - return - } - - const payload = this.transformMetricsForInternal(metrics) - - const authResult = getAuthHeaders() - if (authResult.error) { - logForDebugging(`Metrics export failed: ${authResult.error}`) - resultCallback({ - code: ExportResultCode.FAILED, - error: new Error(authResult.error), - }) - return - } - - const headers: Record = { - 'Content-Type': 'application/json', - 'User-Agent': getClaudeCodeUserAgent(), - ...authResult.headers, - } - - const response = await axios.post(this.endpoint, payload, { - timeout: this.timeout, - headers, - }) - - logForDebugging('BigQuery metrics exported successfully') - logForDebugging( - `BigQuery API Response: ${jsonStringify(response.data, null, 2)}`, - ) - resultCallback({ code: ExportResultCode.SUCCESS }) - } catch (error) { - logForDebugging(`BigQuery metrics export failed: ${errorMessage(error)}`) - logError(error) - resultCallback({ - code: ExportResultCode.FAILED, - error: toError(error), - }) - } - } - - private transformMetricsForInternal( - metrics: ResourceMetrics, - ): InternalMetricsPayload { - const attrs = metrics.resource.attributes - - const resourceAttributes: Record = { - 'service.name': (attrs['service.name'] as string) || 'claude-code', - 'service.version': (attrs['service.version'] as string) || 'unknown', - 'os.type': (attrs['os.type'] as string) || 'unknown', - 'os.version': (attrs['os.version'] as string) || 'unknown', - 'host.arch': (attrs['host.arch'] as string) || 'unknown', - 'aggregation.temporality': - this.selectAggregationTemporality() === AggregationTemporality.DELTA - ? 'delta' - : 'cumulative', - } - - // Only add wsl.version if it exists (omit instead of default) - if (attrs['wsl.version']) { - resourceAttributes['wsl.version'] = attrs['wsl.version'] as string - } - - // Add customer type and subscription type - if (isClaudeAISubscriber()) { - resourceAttributes['user.customer_type'] = 'claude_ai' - const subscriptionType = getSubscriptionType() - if (subscriptionType) { - resourceAttributes['user.subscription_type'] = subscriptionType - } - } else { - resourceAttributes['user.customer_type'] = 'api' - } - - const transformed = { - resource_attributes: resourceAttributes, - metrics: metrics.scopeMetrics.flatMap(scopeMetric => - scopeMetric.metrics.map(metric => ({ - name: metric.descriptor.name, - description: metric.descriptor.description, - unit: metric.descriptor.unit, - data_points: this.extractDataPoints(metric), - })), - ), - } - - return transformed - } - - private extractDataPoints(metric: MetricData): DataPoint[] { - const dataPoints = metric.dataPoints || [] - - return dataPoints - .filter( - (point): point is OTelDataPoint => - typeof point.value === 'number', - ) - .map(point => ({ - attributes: this.convertAttributes(point.attributes), - value: point.value, - timestamp: this.hrTimeToISOString( - point.endTime || point.startTime || [Date.now() / 1000, 0], - ), - })) - } - - async shutdown(): Promise { - this.isShutdown = true - await this.forceFlush() - logForDebugging('BigQuery metrics exporter shutdown complete') - } - - async forceFlush(): Promise { - await Promise.all(this.pendingExports) - logForDebugging('BigQuery metrics exporter flush complete') - } - - private convertAttributes( - attributes: Attributes | undefined, - ): Record { - const result: Record = {} - if (attributes) { - for (const [key, value] of Object.entries(attributes)) { - if (value !== undefined && value !== null) { - result[key] = String(value) - } - } - } - return result - } - - private hrTimeToISOString(hrTime: HrTime): string { - const [seconds, nanoseconds] = hrTime - const date = new Date(seconds * 1000 + nanoseconds / 1000000) - return date.toISOString() - } - - selectAggregationTemporality(): AggregationTemporality { - // DO NOT CHANGE THIS TO CUMULATIVE - // It would mess up the aggregation of metrics - // for CC Productivity metrics dashboard - return AggregationTemporality.DELTA - } -} diff --git a/src/utils/telemetry/events.ts b/src/utils/telemetry/events.ts deleted file mode 100644 index f93b8aaa..00000000 --- a/src/utils/telemetry/events.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { Attributes } from '@opentelemetry/api' -import { getEventLogger, getPromptId } from 'src/bootstrap/state.js' -import { logForDebugging } from '../debug.js' -import { isEnvTruthy } from '../envUtils.js' -import { getTelemetryAttributes } from '../telemetryAttributes.js' - -// Monotonically increasing counter for ordering events within a session -let eventSequence = 0 - -// Track whether we've already warned about a null event logger to avoid spamming -let hasWarnedNoEventLogger = false - -function isUserPromptLoggingEnabled() { - return isEnvTruthy(process.env.OTEL_LOG_USER_PROMPTS) -} - -export function redactIfDisabled(content: string): string { - return isUserPromptLoggingEnabled() ? content : '' -} - -export async function logOTelEvent( - eventName: string, - metadata: { [key: string]: string | undefined } = {}, -): Promise { - const eventLogger = getEventLogger() - if (!eventLogger) { - if (!hasWarnedNoEventLogger) { - hasWarnedNoEventLogger = true - logForDebugging( - `[3P telemetry] Event dropped (no event logger initialized): ${eventName}`, - { level: 'warn' }, - ) - } - return - } - - // Skip logging in test environment - if (process.env.NODE_ENV === 'test') { - return - } - - const attributes: Attributes = { - ...getTelemetryAttributes(), - 'event.name': eventName, - 'event.timestamp': new Date().toISOString(), - 'event.sequence': eventSequence++, - } - - // Add prompt ID to events (but not metrics, where it would cause unbounded cardinality) - const promptId = getPromptId() - if (promptId) { - attributes['prompt.id'] = promptId - } - - // Workspace directory from the desktop app (host path). Events only — - // filesystem paths are too high-cardinality for metric dimensions, and - // the BQ metrics pipeline must never see them. - const workspaceDir = process.env.CLAUDE_CODE_WORKSPACE_HOST_PATHS - if (workspaceDir) { - attributes['workspace.host_paths'] = workspaceDir.split('|') - } - - // Add metadata as attributes - all values are already strings - for (const [key, value] of Object.entries(metadata)) { - if (value !== undefined) { - attributes[key] = value - } - } - - // Emit log record as an event - eventLogger.emit({ - body: `claude_code.${eventName}`, - attributes, - }) -} diff --git a/src/utils/telemetry/instrumentation.ts b/src/utils/telemetry/instrumentation.ts deleted file mode 100644 index 224f0481..00000000 --- a/src/utils/telemetry/instrumentation.ts +++ /dev/null @@ -1,825 +0,0 @@ -import { DiagLogLevel, diag, trace } from '@opentelemetry/api' -import { logs } from '@opentelemetry/api-logs' -// OTLP/Prometheus exporters are dynamically imported inside the protocol -// switch statements below. A process uses at most one protocol variant per -// signal, but static imports would load all 6 (~1.2MB) on every startup. -import { - envDetector, - hostDetector, - osDetector, - resourceFromAttributes, -} from '@opentelemetry/resources' -import { - BatchLogRecordProcessor, - ConsoleLogRecordExporter, - LoggerProvider, -} from '@opentelemetry/sdk-logs' -import { - ConsoleMetricExporter, - MeterProvider, - PeriodicExportingMetricReader, -} from '@opentelemetry/sdk-metrics' -import { - BasicTracerProvider, - BatchSpanProcessor, - ConsoleSpanExporter, -} from '@opentelemetry/sdk-trace-base' -import { - ATTR_SERVICE_NAME, - ATTR_SERVICE_VERSION, - SEMRESATTRS_HOST_ARCH, -} from '@opentelemetry/semantic-conventions' -import { HttpsProxyAgent } from 'https-proxy-agent' -import { - getLoggerProvider, - getMeterProvider, - getTracerProvider, - setEventLogger, - setLoggerProvider, - setMeterProvider, - setTracerProvider, -} from 'src/bootstrap/state.js' -import { - getOtelHeadersFromHelper, - getSubscriptionType, - is1PApiCustomer, - isClaudeAISubscriber, -} from 'src/utils/auth.js' -import { getPlatform, getWslVersion } from 'src/utils/platform.js' - -import { getCACertificates } from '../caCerts.js' -import { registerCleanup } from '../cleanupRegistry.js' -import { getHasFormattedOutput, logForDebugging } from '../debug.js' -import { isEnvTruthy } from '../envUtils.js' -import { errorMessage } from '../errors.js' -import { getMTLSConfig } from '../mtls.js' -import { getProxyUrl, shouldBypassProxy } from '../proxy.js' -import { getSettings_DEPRECATED } from '../settings/settings.js' -import { jsonStringify } from '../slowOperations.js' -import { profileCheckpoint } from '../startupProfiler.js' -import { isBetaTracingEnabled } from './betaSessionTracing.js' -import { BigQueryMetricsExporter } from './bigqueryExporter.js' -import { ClaudeCodeDiagLogger } from './logger.js' -import { initializePerfettoTracing } from './perfettoTracing.js' -import { - endInteractionSpan, - isEnhancedTelemetryEnabled, -} from './sessionTracing.js' - -const DEFAULT_METRICS_EXPORT_INTERVAL_MS = 60000 -const DEFAULT_LOGS_EXPORT_INTERVAL_MS = 5000 -const DEFAULT_TRACES_EXPORT_INTERVAL_MS = 5000 - -class TelemetryTimeoutError extends Error {} - -function telemetryTimeout(ms: number, message: string): Promise { - return new Promise((_, reject) => { - setTimeout( - (rej: (e: Error) => void, msg: string) => - rej(new TelemetryTimeoutError(msg)), - ms, - reject, - message, - ).unref() - }) -} - -export function bootstrapTelemetry() { - if (process.env.USER_TYPE === 'ant') { - // Read from ANT_ prefixed variables that are defined at build time - if (process.env.ANT_OTEL_METRICS_EXPORTER) { - process.env.OTEL_METRICS_EXPORTER = process.env.ANT_OTEL_METRICS_EXPORTER - } - if (process.env.ANT_OTEL_LOGS_EXPORTER) { - process.env.OTEL_LOGS_EXPORTER = process.env.ANT_OTEL_LOGS_EXPORTER - } - if (process.env.ANT_OTEL_TRACES_EXPORTER) { - process.env.OTEL_TRACES_EXPORTER = process.env.ANT_OTEL_TRACES_EXPORTER - } - if (process.env.ANT_OTEL_EXPORTER_OTLP_PROTOCOL) { - process.env.OTEL_EXPORTER_OTLP_PROTOCOL = - process.env.ANT_OTEL_EXPORTER_OTLP_PROTOCOL - } - if (process.env.ANT_OTEL_EXPORTER_OTLP_ENDPOINT) { - process.env.OTEL_EXPORTER_OTLP_ENDPOINT = - process.env.ANT_OTEL_EXPORTER_OTLP_ENDPOINT - } - if (process.env.ANT_OTEL_EXPORTER_OTLP_HEADERS) { - process.env.OTEL_EXPORTER_OTLP_HEADERS = - process.env.ANT_OTEL_EXPORTER_OTLP_HEADERS - } - } - - // Set default tempoality to 'delta' because it's the more sane default - if (!process.env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE) { - process.env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = 'delta' - } -} - -// Per OTEL spec, "none" means "no automatically configured exporter for this signal". -// https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#exporter-selection -export function parseExporterTypes(value: string | undefined): string[] { - return (value || '') - .trim() - .split(',') - .filter(Boolean) - .map(t => t.trim()) - .filter(t => t !== 'none') -} - -async function getOtlpReaders() { - const exporterTypes = parseExporterTypes(process.env.OTEL_METRICS_EXPORTER) - const exportInterval = parseInt( - process.env.OTEL_METRIC_EXPORT_INTERVAL || - DEFAULT_METRICS_EXPORT_INTERVAL_MS.toString(), - ) - - const exporters = [] - for (const exporterType of exporterTypes) { - if (exporterType === 'console') { - // Custom console exporter that shows resource attributes - const consoleExporter = new ConsoleMetricExporter() - const originalExport = consoleExporter.export.bind(consoleExporter) - - consoleExporter.export = (metrics, callback) => { - // Log resource attributes once at the start - if (metrics.resource && metrics.resource.attributes) { - // The console exporter is for debugging, so console output is intentional here - - logForDebugging('\n=== Resource Attributes ===') - logForDebugging(jsonStringify(metrics.resource.attributes)) - logForDebugging('===========================\n') - } - - return originalExport(metrics, callback) - } - - exporters.push(consoleExporter) - } else if (exporterType === 'otlp') { - const protocol = - process.env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL?.trim() || - process.env.OTEL_EXPORTER_OTLP_PROTOCOL?.trim() - - const httpConfig = getOTLPExporterConfig() - - switch (protocol) { - case 'grpc': { - // Lazy-import to keep @grpc/grpc-js (~700KB) out of the telemetry chunk - // when the protocol is http/protobuf (ant default) or http/json. - const { OTLPMetricExporter } = await import( - '@opentelemetry/exporter-metrics-otlp-grpc' - ) - exporters.push(new OTLPMetricExporter()) - break - } - case 'http/json': { - const { OTLPMetricExporter } = await import( - '@opentelemetry/exporter-metrics-otlp-http' - ) - exporters.push(new OTLPMetricExporter(httpConfig)) - break - } - case 'http/protobuf': { - const { OTLPMetricExporter } = await import( - '@opentelemetry/exporter-metrics-otlp-proto' - ) - exporters.push(new OTLPMetricExporter(httpConfig)) - break - } - default: - throw new Error( - `Unknown protocol set in OTEL_EXPORTER_OTLP_METRICS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${protocol}`, - ) - } - } else if (exporterType === 'prometheus') { - const { PrometheusExporter } = await import( - '@opentelemetry/exporter-prometheus' - ) - exporters.push(new PrometheusExporter()) - } else { - throw new Error( - `Unknown exporter type set in OTEL_EXPORTER_OTLP_METRICS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${exporterType}`, - ) - } - } - - return exporters.map(exporter => { - if ('export' in exporter) { - return new PeriodicExportingMetricReader({ - exporter, - exportIntervalMillis: exportInterval, - }) - } - return exporter - }) -} - -async function getOtlpLogExporters() { - const exporterTypes = parseExporterTypes(process.env.OTEL_LOGS_EXPORTER) - - const protocol = - process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL?.trim() || - process.env.OTEL_EXPORTER_OTLP_PROTOCOL?.trim() - const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT - - logForDebugging( - `[3P telemetry] getOtlpLogExporters: types=${jsonStringify(exporterTypes)}, protocol=${protocol}, endpoint=${endpoint}`, - ) - - const exporters = [] - for (const exporterType of exporterTypes) { - if (exporterType === 'console') { - exporters.push(new ConsoleLogRecordExporter()) - } else if (exporterType === 'otlp') { - const httpConfig = getOTLPExporterConfig() - - switch (protocol) { - case 'grpc': { - const { OTLPLogExporter } = await import( - '@opentelemetry/exporter-logs-otlp-grpc' - ) - exporters.push(new OTLPLogExporter()) - break - } - case 'http/json': { - const { OTLPLogExporter } = await import( - '@opentelemetry/exporter-logs-otlp-http' - ) - exporters.push(new OTLPLogExporter(httpConfig)) - break - } - case 'http/protobuf': { - const { OTLPLogExporter } = await import( - '@opentelemetry/exporter-logs-otlp-proto' - ) - exporters.push(new OTLPLogExporter(httpConfig)) - break - } - default: - throw new Error( - `Unknown protocol set in OTEL_EXPORTER_OTLP_LOGS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${protocol}`, - ) - } - } else { - throw new Error( - `Unknown exporter type set in OTEL_LOGS_EXPORTER env var: ${exporterType}`, - ) - } - } - - return exporters -} - -async function getOtlpTraceExporters() { - const exporterTypes = parseExporterTypes(process.env.OTEL_TRACES_EXPORTER) - - const exporters = [] - for (const exporterType of exporterTypes) { - if (exporterType === 'console') { - exporters.push(new ConsoleSpanExporter()) - } else if (exporterType === 'otlp') { - const protocol = - process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL?.trim() || - process.env.OTEL_EXPORTER_OTLP_PROTOCOL?.trim() - - const httpConfig = getOTLPExporterConfig() - - switch (protocol) { - case 'grpc': { - const { OTLPTraceExporter } = await import( - '@opentelemetry/exporter-trace-otlp-grpc' - ) - exporters.push(new OTLPTraceExporter()) - break - } - case 'http/json': { - const { OTLPTraceExporter } = await import( - '@opentelemetry/exporter-trace-otlp-http' - ) - exporters.push(new OTLPTraceExporter(httpConfig)) - break - } - case 'http/protobuf': { - const { OTLPTraceExporter } = await import( - '@opentelemetry/exporter-trace-otlp-proto' - ) - exporters.push(new OTLPTraceExporter(httpConfig)) - break - } - default: - throw new Error( - `Unknown protocol set in OTEL_EXPORTER_OTLP_TRACES_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${protocol}`, - ) - } - } else { - throw new Error( - `Unknown exporter type set in OTEL_TRACES_EXPORTER env var: ${exporterType}`, - ) - } - } - - return exporters -} - -export function isTelemetryEnabled() { - return isEnvTruthy(process.env.CLAUDE_CODE_ENABLE_TELEMETRY) -} - -function getBigQueryExportingReader() { - const bigqueryExporter = new BigQueryMetricsExporter() - return new PeriodicExportingMetricReader({ - exporter: bigqueryExporter, - exportIntervalMillis: 5 * 60 * 1000, // 5mins for BigQuery metrics exporter to reduce load - }) -} - -function isBigQueryMetricsEnabled() { - // BigQuery metrics are enabled for: - // 1. API customers (excluding Claude.ai subscribers and Bedrock/Vertex) - // 2. Claude for Enterprise (C4E) users - // 3. Claude for Teams users - const subscriptionType = getSubscriptionType() - const isC4EOrTeamUser = - isClaudeAISubscriber() && - (subscriptionType === 'enterprise' || subscriptionType === 'team') - - return is1PApiCustomer() || isC4EOrTeamUser -} - -/** - * Initialize beta tracing - a separate code path for detailed debugging. - * Uses BETA_TRACING_ENDPOINT instead of OTEL_EXPORTER_OTLP_ENDPOINT. - */ -async function initializeBetaTracing( - resource: ReturnType, -): Promise { - const endpoint = process.env.BETA_TRACING_ENDPOINT - if (!endpoint) { - return - } - - const [{ OTLPTraceExporter }, { OTLPLogExporter }] = await Promise.all([ - import('@opentelemetry/exporter-trace-otlp-http'), - import('@opentelemetry/exporter-logs-otlp-http'), - ]) - - const httpConfig = { - url: `${endpoint}/v1/traces`, - } - - const logHttpConfig = { - url: `${endpoint}/v1/logs`, - } - - // Initialize trace exporter - const traceExporter = new OTLPTraceExporter(httpConfig) - const spanProcessor = new BatchSpanProcessor(traceExporter, { - scheduledDelayMillis: DEFAULT_TRACES_EXPORT_INTERVAL_MS, - }) - - const tracerProvider = new BasicTracerProvider({ - resource, - spanProcessors: [spanProcessor], - }) - - trace.setGlobalTracerProvider(tracerProvider) - setTracerProvider(tracerProvider) - - // Initialize log exporter - const logExporter = new OTLPLogExporter(logHttpConfig) - const loggerProvider = new LoggerProvider({ - resource, - processors: [ - new BatchLogRecordProcessor(logExporter, { - scheduledDelayMillis: DEFAULT_LOGS_EXPORT_INTERVAL_MS, - }), - ], - }) - - logs.setGlobalLoggerProvider(loggerProvider) - setLoggerProvider(loggerProvider) - - // Initialize event logger - const eventLogger = logs.getLogger( - 'com.anthropic.claude_code.events', - MACRO.VERSION, - ) - setEventLogger(eventLogger) - - // Setup flush handlers - flush both logs AND traces - process.on('beforeExit', async () => { - await loggerProvider?.forceFlush() - await tracerProvider?.forceFlush() - }) - - process.on('exit', () => { - void loggerProvider?.forceFlush() - void tracerProvider?.forceFlush() - }) -} - -export async function initializeTelemetry() { - profileCheckpoint('telemetry_init_start') - bootstrapTelemetry() - - // Console exporters call console.dir on a timer (5s logs/traces, 60s - // metrics), writing pretty-printed objects to stdout. In stream-json - // mode stdout is the SDK message channel; the first line (`{`) breaks - // the SDK's line reader. Stripped here (not main.tsx) because init.ts - // re-runs applyConfigEnvironmentVariables() inside initializeTelemetry- - // AfterTrust for remote-managed-settings users, and bootstrapTelemetry - // above copies ANT_OTEL_* for ant users — both would undo an earlier strip. - if (getHasFormattedOutput()) { - for (const key of [ - 'OTEL_METRICS_EXPORTER', - 'OTEL_LOGS_EXPORTER', - 'OTEL_TRACES_EXPORTER', - ] as const) { - const v = process.env[key] - if (v?.includes('console')) { - process.env[key] = v - .split(',') - .map(s => s.trim()) - .filter(s => s !== 'console') - .join(',') - } - } - } - - diag.setLogger(new ClaudeCodeDiagLogger(), DiagLogLevel.ERROR) - - // Initialize Perfetto tracing (independent of OTEL) - // Enable via CLAUDE_CODE_PERFETTO_TRACE=1 or CLAUDE_CODE_PERFETTO_TRACE= - initializePerfettoTracing() - - const readers = [] - - // Add customer exporters (if enabled) - const telemetryEnabled = isTelemetryEnabled() - logForDebugging( - `[3P telemetry] isTelemetryEnabled=${telemetryEnabled} (CLAUDE_CODE_ENABLE_TELEMETRY=${process.env.CLAUDE_CODE_ENABLE_TELEMETRY})`, - ) - if (telemetryEnabled) { - readers.push(...(await getOtlpReaders())) - } - - // Add BigQuery exporter (for API customers, C4E users, and internal users) - if (isBigQueryMetricsEnabled()) { - readers.push(getBigQueryExportingReader()) - } - - // Create base resource with service attributes - const platform = getPlatform() - const baseAttributes: Record = { - [ATTR_SERVICE_NAME]: 'claude-code', - [ATTR_SERVICE_VERSION]: MACRO.VERSION, - } - - // Add WSL-specific attributes if running on WSL - if (platform === 'wsl') { - const wslVersion = getWslVersion() - if (wslVersion) { - baseAttributes['wsl.version'] = wslVersion - } - } - - const baseResource = resourceFromAttributes(baseAttributes) - - // Use OpenTelemetry detectors - const osResource = resourceFromAttributes( - osDetector.detect().attributes || {}, - ) - - // Extract only host.arch from hostDetector - const hostDetected = hostDetector.detect() - const hostArchAttributes = hostDetected.attributes?.[SEMRESATTRS_HOST_ARCH] - ? { - [SEMRESATTRS_HOST_ARCH]: hostDetected.attributes[SEMRESATTRS_HOST_ARCH], - } - : {} - const hostArchResource = resourceFromAttributes(hostArchAttributes) - - const envResource = resourceFromAttributes( - envDetector.detect().attributes || {}, - ) - - // Merge resources - later resources take precedence - const resource = baseResource - .merge(osResource) - .merge(hostArchResource) - .merge(envResource) - - // Check if beta tracing is enabled - this is a separate code path - // Available to all users who set ENABLE_BETA_TRACING_DETAILED=1 and BETA_TRACING_ENDPOINT - if (isBetaTracingEnabled()) { - void initializeBetaTracing(resource).catch(e => - logForDebugging(`Beta tracing init failed: ${e}`, { level: 'error' }), - ) - // Still set up meter provider for metrics (but skip regular logs/traces setup) - const meterProvider = new MeterProvider({ - resource, - views: [], - readers, - }) - setMeterProvider(meterProvider) - - // Register shutdown for beta tracing - const shutdownTelemetry = async () => { - const timeoutMs = parseInt( - process.env.CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS || '2000', - ) - try { - endInteractionSpan() - - // Force flush + shutdown together inside the timeout. Previously forceFlush - // was awaited unbounded BEFORE the race, blocking exit on slow OTLP endpoints. - // Each provider's flush→shutdown is chained independently so a slow logger - // flush doesn't delay meterProvider/tracerProvider shutdown (no waterfall). - const loggerProvider = getLoggerProvider() - const tracerProvider = getTracerProvider() - - const chains: Promise[] = [meterProvider.shutdown()] - if (loggerProvider) { - chains.push( - loggerProvider.forceFlush().then(() => loggerProvider.shutdown()), - ) - } - if (tracerProvider) { - chains.push( - tracerProvider.forceFlush().then(() => tracerProvider.shutdown()), - ) - } - - await Promise.race([ - Promise.all(chains), - telemetryTimeout(timeoutMs, 'OpenTelemetry shutdown timeout'), - ]) - } catch { - // Ignore shutdown errors - } - } - registerCleanup(shutdownTelemetry) - - return meterProvider.getMeter('com.anthropic.claude_code', MACRO.VERSION) - } - - const meterProvider = new MeterProvider({ - resource, - views: [], - readers, - }) - - // Store reference in state for flushing - setMeterProvider(meterProvider) - - // Initialize logs if telemetry is enabled - if (telemetryEnabled) { - const logExporters = await getOtlpLogExporters() - logForDebugging( - `[3P telemetry] Created ${logExporters.length} log exporter(s)`, - ) - - if (logExporters.length > 0) { - const loggerProvider = new LoggerProvider({ - resource, - // Add batch processors for each exporter - processors: logExporters.map( - exporter => - new BatchLogRecordProcessor(exporter, { - scheduledDelayMillis: parseInt( - process.env.OTEL_LOGS_EXPORT_INTERVAL || - DEFAULT_LOGS_EXPORT_INTERVAL_MS.toString(), - ), - }), - ), - }) - - // Register the logger provider globally - logs.setGlobalLoggerProvider(loggerProvider) - setLoggerProvider(loggerProvider) - - // Initialize event logger - const eventLogger = logs.getLogger( - 'com.anthropic.claude_code.events', - MACRO.VERSION, - ) - setEventLogger(eventLogger) - logForDebugging('[3P telemetry] Event logger set successfully') - - // 'beforeExit' is emitted when Node.js empties its event loop and has no additional work to schedule. - // Unlike 'exit', it allows us to perform async operations, so it works well for letting - // network requests complete before the process exits naturally. - process.on('beforeExit', async () => { - await loggerProvider?.forceFlush() - // Also flush traces - they use BatchSpanProcessor which needs explicit flush - const tracerProvider = getTracerProvider() - await tracerProvider?.forceFlush() - }) - - process.on('exit', () => { - // Final attempt to flush logs and traces - void loggerProvider?.forceFlush() - void getTracerProvider()?.forceFlush() - }) - } - } - - // Initialize tracing if enhanced telemetry is enabled (BETA) - if (telemetryEnabled && isEnhancedTelemetryEnabled()) { - const traceExporters = await getOtlpTraceExporters() - if (traceExporters.length > 0) { - // Create span processors for each exporter - const spanProcessors = traceExporters.map( - exporter => - new BatchSpanProcessor(exporter, { - scheduledDelayMillis: parseInt( - process.env.OTEL_TRACES_EXPORT_INTERVAL || - DEFAULT_TRACES_EXPORT_INTERVAL_MS.toString(), - ), - }), - ) - - const tracerProvider = new BasicTracerProvider({ - resource, - spanProcessors, - }) - - // Register the tracer provider globally - trace.setGlobalTracerProvider(tracerProvider) - setTracerProvider(tracerProvider) - } - } - - // Shutdown metrics and logs on exit (flushes and closes exporters) - const shutdownTelemetry = async () => { - const timeoutMs = parseInt( - process.env.CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS || '2000', - ) - - try { - // End any active interaction span before shutdown - endInteractionSpan() - - const shutdownPromises = [meterProvider.shutdown()] - const loggerProvider = getLoggerProvider() - if (loggerProvider) { - shutdownPromises.push(loggerProvider.shutdown()) - } - const tracerProvider = getTracerProvider() - if (tracerProvider) { - shutdownPromises.push(tracerProvider.shutdown()) - } - - await Promise.race([ - Promise.all(shutdownPromises), - telemetryTimeout(timeoutMs, 'OpenTelemetry shutdown timeout'), - ]) - } catch (error) { - if (error instanceof Error && error.message.includes('timeout')) { - logForDebugging( - ` -OpenTelemetry telemetry flush timed out after ${timeoutMs}ms - -To resolve this issue, you can: -1. Increase the timeout by setting CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS env var (e.g., 5000 for 5 seconds) -2. Check if your OpenTelemetry backend is experiencing scalability issues -3. Disable OpenTelemetry by unsetting CLAUDE_CODE_ENABLE_TELEMETRY env var - -Current timeout: ${timeoutMs}ms -`, - { level: 'error' }, - ) - } - throw error - } - } - - // Always register shutdown (internal metrics are always enabled) - registerCleanup(shutdownTelemetry) - - return meterProvider.getMeter('com.anthropic.claude_code', MACRO.VERSION) -} - -/** - * Flush all pending telemetry data immediately. - * This should be called before logout or org switching to prevent data leakage. - */ -export async function flushTelemetry(): Promise { - const meterProvider = getMeterProvider() - if (!meterProvider) { - return - } - - const timeoutMs = parseInt( - process.env.CLAUDE_CODE_OTEL_FLUSH_TIMEOUT_MS || '5000', - ) - - try { - const flushPromises = [meterProvider.forceFlush()] - const loggerProvider = getLoggerProvider() - if (loggerProvider) { - flushPromises.push(loggerProvider.forceFlush()) - } - const tracerProvider = getTracerProvider() - if (tracerProvider) { - flushPromises.push(tracerProvider.forceFlush()) - } - - await Promise.race([ - Promise.all(flushPromises), - telemetryTimeout(timeoutMs, 'OpenTelemetry flush timeout'), - ]) - - logForDebugging('Telemetry flushed successfully') - } catch (error) { - if (error instanceof TelemetryTimeoutError) { - logForDebugging( - `Telemetry flush timed out after ${timeoutMs}ms. Some metrics may not be exported.`, - { level: 'warn' }, - ) - } else { - logForDebugging(`Telemetry flush failed: ${errorMessage(error)}`, { - level: 'error', - }) - } - // Don't throw - allow logout to continue even if flush fails - } -} - -function parseOtelHeadersEnvVar(): Record { - const headers: Record = {} - const envHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS - if (envHeaders) { - for (const pair of envHeaders.split(',')) { - const [key, ...valueParts] = pair.split('=') - if (key && valueParts.length > 0) { - headers[key.trim()] = valueParts.join('=').trim() - } - } - } - return headers -} - -/** - * Get configuration for OTLP exporters including: - * - HTTP agent options (proxy, mTLS) - * - Dynamic headers via otelHeadersHelper or static headers from env var - */ -function getOTLPExporterConfig() { - const proxyUrl = getProxyUrl() - const mtlsConfig = getMTLSConfig() - const settings = getSettings_DEPRECATED() - - // Build base config - const config: Record = {} - - // Parse static headers from env var once (doesn't change at runtime) - const staticHeaders = parseOtelHeadersEnvVar() - - // If otelHeadersHelper is configured, use async headers function for dynamic refresh - // Otherwise just return static headers if any exist - if (settings?.otelHeadersHelper) { - config.headers = async (): Promise> => { - const dynamicHeaders = getOtelHeadersFromHelper() - return { ...staticHeaders, ...dynamicHeaders } - } - } else if (Object.keys(staticHeaders).length > 0) { - config.headers = async (): Promise> => staticHeaders - } - - // Check if we should bypass proxy for OTEL endpoint - const otelEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT - if (!proxyUrl || (otelEndpoint && shouldBypassProxy(otelEndpoint))) { - // No proxy configured or OTEL endpoint should bypass proxy - const caCerts = getCACertificates() - if (mtlsConfig || caCerts) { - config.httpAgentOptions = { - ...mtlsConfig, - ...(caCerts && { ca: caCerts }), - } - } - return config - } - - // Return an HttpAgentFactory function that creates our proxy agent - const caCerts = getCACertificates() - const agentFactory = (_protocol: string) => { - // Create and return the proxy agent with mTLS and CA cert config - const proxyAgent = - mtlsConfig || caCerts - ? new HttpsProxyAgent(proxyUrl, { - ...(mtlsConfig && { - cert: mtlsConfig.cert, - key: mtlsConfig.key, - passphrase: mtlsConfig.passphrase, - }), - ...(caCerts && { ca: caCerts }), - }) - : new HttpsProxyAgent(proxyUrl) - - return proxyAgent - } - - config.httpAgentOptions = agentFactory - return config -} diff --git a/src/utils/telemetry/logger.ts b/src/utils/telemetry/logger.ts deleted file mode 100644 index 0f080f58..00000000 --- a/src/utils/telemetry/logger.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { DiagLogger } from '@opentelemetry/api' -import { logForDebugging } from '../debug.js' -import { logError } from '../log.js' -export class ClaudeCodeDiagLogger implements DiagLogger { - error(message: string, ..._: unknown[]) { - logError(new Error(message)) - logForDebugging(`[3P telemetry] OTEL diag error: ${message}`, { - level: 'error', - }) - } - warn(message: string, ..._: unknown[]) { - logError(new Error(message)) - logForDebugging(`[3P telemetry] OTEL diag warn: ${message}`, { - level: 'warn', - }) - } - info(_message: string, ..._args: unknown[]) { - return - } - debug(_message: string, ..._args: unknown[]) { - return - } - verbose(_message: string, ..._args: unknown[]) { - return - } -} diff --git a/src/utils/telemetry/pluginTelemetry.ts b/src/utils/telemetry/pluginTelemetry.ts deleted file mode 100644 index 30600e74..00000000 --- a/src/utils/telemetry/pluginTelemetry.ts +++ /dev/null @@ -1,289 +0,0 @@ -/** - * Plugin telemetry helpers — shared field builders for plugin lifecycle events. - * - * Implements the twin-column privacy pattern: every user-defined-name field - * emits both a raw value (routed to PII-tagged _PROTO_* BQ columns) and a - * redacted twin (real name iff marketplace ∈ allowlist, else 'third-party'). - * - * plugin_id_hash provides an opaque per-plugin aggregation key with no privacy - * dependency — sha256(name@marketplace + FIXED_SALT) truncated to 16 chars. - * This answers distinct-count and per-plugin-trend questions that the - * redacted column can't, without exposing user-defined names. - */ - -import { createHash } from 'crypto' -import { sep } from 'path' -import { - type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, - logEvent, -} from '../../services/analytics/index.js' -import type { - LoadedPlugin, - PluginError, - PluginManifest, -} from '../../types/plugin.js' -import { - isOfficialMarketplaceName, - parsePluginIdentifier, -} from '../plugins/pluginIdentifier.js' - -// builtinPlugins.ts:BUILTIN_MARKETPLACE_NAME — inlined to avoid the cycle -// through commands.js. Marketplace schemas.ts enforces 'builtin' is reserved. -const BUILTIN_MARKETPLACE_NAME = 'builtin' - -// Fixed salt for plugin_id_hash. Same constant across all repos and emission -// sites. Not per-org, not rotated — per-org salt would defeat cross-org -// distinct-count, rotation would break trend lines. Customers can compute the -// same hash on their known plugin names to reverse-match their own telemetry. -const PLUGIN_ID_HASH_SALT = 'claude-plugin-telemetry-v1' - -/** - * Opaque per-plugin aggregation key. Input is the name@marketplace string as - * it appears in enabledPlugins keys, lowercased on the marketplace suffix for - * reproducibility. 16-char truncation keeps BQ GROUP BY cardinality manageable - * while making collisions negligible at projected 10k-plugin scale. Name case - * is preserved in both branches (enabledPlugins keys are case-sensitive). - */ -export function hashPluginId(name: string, marketplace?: string): string { - const key = marketplace ? `${name}@${marketplace.toLowerCase()}` : name - return createHash('sha256') - .update(key + PLUGIN_ID_HASH_SALT) - .digest('hex') - .slice(0, 16) -} - -/** - * 4-value scope enum for plugin origin. Distinct from PluginScope - * (managed/user/project/local) which is installation-target — this is - * marketplace-origin. - * - * - official: from an allowlisted Anthropic marketplace - * - default-bundle: ships with product (@builtin), auto-enabled - * - org: enterprise admin-pushed via managed settings (policySettings) - * - user-local: user added marketplace or local plugin - */ -export type TelemetryPluginScope = - | 'official' - | 'org' - | 'user-local' - | 'default-bundle' - -export function getTelemetryPluginScope( - name: string, - marketplace: string | undefined, - managedNames: Set | null, -): TelemetryPluginScope { - if (marketplace === BUILTIN_MARKETPLACE_NAME) return 'default-bundle' - if (isOfficialMarketplaceName(marketplace)) return 'official' - if (managedNames?.has(name)) return 'org' - return 'user-local' -} - -/** - * How a plugin arrived in the session. Splits self-selected from org-pushed - * — plugin_scope alone doesn't (an official plugin can be user-installed OR - * org-pushed; both are scope='official'). - */ -export type EnabledVia = - | 'user-install' - | 'org-policy' - | 'default-enable' - | 'seed-mount' - -/** How a skill/command invocation was triggered. */ -export type InvocationTrigger = - | 'user-slash' - | 'claude-proactive' - | 'nested-skill' - -/** Where a skill invocation executes. */ -export type SkillExecutionContext = 'fork' | 'inline' | 'remote' - -/** How a plugin install was initiated. */ -export type InstallSource = - | 'cli-explicit' - | 'ui-discover' - | 'ui-suggestion' - | 'deep-link' - -export function getEnabledVia( - plugin: LoadedPlugin, - managedNames: Set | null, - seedDirs: string[], -): EnabledVia { - if (plugin.isBuiltin) return 'default-enable' - if (managedNames?.has(plugin.name)) return 'org-policy' - // Trailing sep: /opt/plugins must not match /opt/plugins-extra - if ( - seedDirs.some(dir => - plugin.path.startsWith(dir.endsWith(sep) ? dir : dir + sep), - ) - ) { - return 'seed-mount' - } - return 'user-install' -} - -/** - * Common plugin telemetry fields keyed off name@marketplace. Returns the - * hash, scope enum, and the redacted-twin columns. Callers add the raw - * _PROTO_* fields separately (those require the PII-tagged marker type). - */ -export function buildPluginTelemetryFields( - name: string, - marketplace: string | undefined, - managedNames: Set | null = null, -): { - plugin_id_hash: AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS - plugin_scope: AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS - plugin_name_redacted: AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS - marketplace_name_redacted: AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS - is_official_plugin: boolean -} { - const scope = getTelemetryPluginScope(name, marketplace, managedNames) - // Both official marketplaces and builtin plugins are Anthropic-controlled - // — safe to expose real names in the redacted columns. - const isAnthropicControlled = - scope === 'official' || scope === 'default-bundle' - return { - plugin_id_hash: hashPluginId( - name, - marketplace, - ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - plugin_scope: - scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - plugin_name_redacted: (isAnthropicControlled - ? name - : 'third-party') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - marketplace_name_redacted: (isAnthropicControlled && marketplace - ? marketplace - : 'third-party') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - is_official_plugin: isAnthropicControlled, - } -} - -/** - * Per-invocation callers (SkillTool, processSlashCommand) pass - * managedNames=null — the session-level tengu_plugin_enabled_for_session - * event carries the authoritative plugin_scope, and per-invocation rows can - * join on plugin_id_hash to recover it. This keeps hot-path call sites free - * of the extra settings read. - */ -export function buildPluginCommandTelemetryFields( - pluginInfo: { pluginManifest: PluginManifest; repository: string }, - managedNames: Set | null = null, -): ReturnType { - const { marketplace } = parsePluginIdentifier(pluginInfo.repository) - return buildPluginTelemetryFields( - pluginInfo.pluginManifest.name, - marketplace, - managedNames, - ) -} - -/** - * Emit tengu_plugin_enabled_for_session once per enabled plugin at session - * start. Supplements tengu_skill_loaded (which still fires per-skill) — use - * this for plugin-level aggregates instead of DISTINCT-on-prefix hacks. - * A plugin with 5 skills emits 5 skill_loaded rows but 1 of these. - */ -export function logPluginsEnabledForSession( - plugins: LoadedPlugin[], - managedNames: Set | null, - seedDirs: string[], -): void { - for (const plugin of plugins) { - const { marketplace } = parsePluginIdentifier(plugin.repository) - - logEvent('tengu_plugin_enabled_for_session', { - _PROTO_plugin_name: - plugin.name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, - ...(marketplace && { - _PROTO_marketplace_name: - marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, - }), - ...buildPluginTelemetryFields(plugin.name, marketplace, managedNames), - enabled_via: getEnabledVia( - plugin, - managedNames, - seedDirs, - ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - skill_path_count: - (plugin.skillsPath ? 1 : 0) + (plugin.skillsPaths?.length ?? 0), - command_path_count: - (plugin.commandsPath ? 1 : 0) + (plugin.commandsPaths?.length ?? 0), - has_mcp: plugin.manifest.mcpServers !== undefined, - has_hooks: plugin.hooksConfig !== undefined, - ...(plugin.manifest.version && { - version: plugin.manifest - .version as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - }), - }) - } -} - -/** - * Bounded-cardinality error bucket for CLI plugin operation failures. - * Maps free-form error messages to 5 stable categories so dashboard - * GROUP BY stays tractable. - */ -export type PluginCommandErrorCategory = - | 'network' - | 'not-found' - | 'permission' - | 'validation' - | 'unknown' - -export function classifyPluginCommandError( - error: unknown, -): PluginCommandErrorCategory { - const msg = String((error as { message?: unknown })?.message ?? error) - if ( - /ENOTFOUND|ECONNREFUSED|EAI_AGAIN|ETIMEDOUT|ECONNRESET|network|Could not resolve|Connection refused|timed out/i.test( - msg, - ) - ) { - return 'network' - } - if (/\b404\b|not found|does not exist|no such plugin/i.test(msg)) { - return 'not-found' - } - if (/\b40[13]\b|EACCES|EPERM|permission denied|unauthorized/i.test(msg)) { - return 'permission' - } - if (/invalid|malformed|schema|validation|parse error/i.test(msg)) { - return 'validation' - } - return 'unknown' -} - -/** - * Emit tengu_plugin_load_failed once per error surfaced by session-start - * plugin loading. Pairs with tengu_plugin_enabled_for_session so dashboards - * can compute a load-success rate. PluginError.type is already a bounded - * enum — use it directly as error_category. - */ -export function logPluginLoadErrors( - errors: PluginError[], - managedNames: Set | null, -): void { - for (const err of errors) { - const { name, marketplace } = parsePluginIdentifier(err.source) - // Not all PluginError variants carry a plugin name (some have pluginId, - // some are marketplace-level). Use the 'plugin' property if present, - // fall back to the name parsed from err.source. - const pluginName = 'plugin' in err && err.plugin ? err.plugin : name - logEvent('tengu_plugin_load_failed', { - error_category: - err.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - _PROTO_plugin_name: - pluginName as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, - ...(marketplace && { - _PROTO_marketplace_name: - marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, - }), - ...buildPluginTelemetryFields(pluginName, marketplace, managedNames), - }) - } -} diff --git a/src/utils/telemetry/skillLoadedEvent.ts b/src/utils/telemetry/skillLoadedEvent.ts deleted file mode 100644 index a84a58bb..00000000 --- a/src/utils/telemetry/skillLoadedEvent.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { getSkillToolCommands } from '../../commands.js' -import { - type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, - logEvent, -} from '../../services/analytics/index.js' -import { getCharBudget } from '../../tools/SkillTool/prompt.js' - -/** - * Logs a tengu_skill_loaded event for each skill available at session startup. - * This enables analytics on which skills are available across sessions. - */ -export async function logSkillsLoaded( - cwd: string, - contextWindowTokens: number, -): Promise { - const skills = await getSkillToolCommands(cwd) - const skillBudget = getCharBudget(contextWindowTokens) - - for (const skill of skills) { - if (skill.type !== 'prompt') continue - - logEvent('tengu_skill_loaded', { - // _PROTO_skill_name routes to the privileged skill_name BQ column. - // Unredacted names don't go in additional_metadata. - _PROTO_skill_name: - skill.name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, - skill_source: - skill.source as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - skill_loaded_from: - skill.loadedFrom as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - skill_budget: skillBudget, - ...(skill.kind && { - skill_kind: - skill.kind as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - }), - }) - } -} diff --git a/src/utils/telemetryAttributes-stub.ts b/src/utils/telemetryAttributes-stub.ts new file mode 100644 index 00000000..2801c3d5 --- /dev/null +++ b/src/utils/telemetryAttributes-stub.ts @@ -0,0 +1,16 @@ +/** + * Stub file for telemetry attributes functionality + * All functions are no-ops to allow code to compile without telemetry attributes infrastructure + */ + +export function getTelemetryAttributes(...args: any[]): Record { + return {}; +} + +export function setTelemetryAttribute(...args: any[]): void { + // No-op +} + +export function getTelemetryAttribute(key: string): any { + return undefined; +} diff --git a/src/utils/telemetryAttributes.ts b/src/utils/telemetryAttributes.ts deleted file mode 100644 index 2038c104..00000000 --- a/src/utils/telemetryAttributes.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { Attributes } from '@opentelemetry/api' -import { getSessionId } from 'src/bootstrap/state.js' -import { getOauthAccountInfo } from './auth.js' -import { getOrCreateUserID } from './config.js' -import { envDynamic } from './envDynamic.js' -import { isEnvTruthy } from './envUtils.js' -import { toTaggedId } from './taggedId.js' - -// Default configuration for metrics cardinality -const METRICS_CARDINALITY_DEFAULTS = { - OTEL_METRICS_INCLUDE_SESSION_ID: true, - OTEL_METRICS_INCLUDE_VERSION: false, - OTEL_METRICS_INCLUDE_ACCOUNT_UUID: true, -} - -function shouldIncludeAttribute( - envVar: keyof typeof METRICS_CARDINALITY_DEFAULTS, -): boolean { - const defaultValue = METRICS_CARDINALITY_DEFAULTS[envVar] - const envValue = process.env[envVar] - - if (envValue === undefined) { - return defaultValue - } - - return isEnvTruthy(envValue) -} - -export function getTelemetryAttributes(): Attributes { - const userId = getOrCreateUserID() - const sessionId = getSessionId() - - const attributes: Attributes = { - 'user.id': userId, - } - - if (shouldIncludeAttribute('OTEL_METRICS_INCLUDE_SESSION_ID')) { - attributes['session.id'] = sessionId - } - if (shouldIncludeAttribute('OTEL_METRICS_INCLUDE_VERSION')) { - attributes['app.version'] = MACRO.VERSION - } - - // Only include OAuth account data when actively using OAuth authentication - const oauthAccount = getOauthAccountInfo() - if (oauthAccount) { - const orgId = oauthAccount.organizationUuid - const email = oauthAccount.emailAddress - const accountUuid = oauthAccount.accountUuid - - if (orgId) attributes['organization.id'] = orgId - if (email) attributes['user.email'] = email - - if ( - accountUuid && - shouldIncludeAttribute('OTEL_METRICS_INCLUDE_ACCOUNT_UUID') - ) { - attributes['user.account_uuid'] = accountUuid - attributes['user.account_id'] = - process.env.CLAUDE_CODE_ACCOUNT_TAGGED_ID || - toTaggedId('user', accountUuid) - } - } - - // Add terminal type if available - if (envDynamic.terminal) { - attributes['terminal.type'] = envDynamic.terminal - } - - return attributes -} diff --git a/src/utils/teleport.tsx b/src/utils/teleport.tsx index 4be98a0a..32422e72 100644 --- a/src/utils/teleport.tsx +++ b/src/utils/teleport.tsx @@ -3,8 +3,8 @@ import chalk from 'chalk'; import { randomUUID } from 'crypto'; import React from 'react'; import { getOriginalCwd, getSessionId } from 'src/bootstrap/state.js'; -import { checkGate_CACHED_OR_BLOCKING } from 'src/services/analytics/growthbook.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { checkGate_CACHED_OR_BLOCKING } from '../services/analytics-stub.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics-stub.js'; import { isPolicyAllowed } from 'src/services/policyLimits/index.js'; import { z } from 'zod/v4'; import { getTeleportErrors, TeleportError, type TeleportLocalErrorType } from '../components/TeleportError.js'; diff --git a/src/utils/teleport/gitBundle.ts b/src/utils/teleport/gitBundle.ts index 0e53c5dc..b6e52101 100644 --- a/src/utils/teleport/gitBundle.ts +++ b/src/utils/teleport/gitBundle.ts @@ -13,8 +13,8 @@ import { stat, unlink } from 'fs/promises' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' +} from '../../services/analytics-stub.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics-stub.js' import { type FilesApiConfig, uploadFile } from '../../services/api/filesApi.js' import { getCwd } from '../cwd.js' import { logForDebugging } from '../debug.js' diff --git a/src/utils/theme.ts b/src/utils/theme.ts index 0b0d1c2c..eccd0f2f 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -117,7 +117,7 @@ const lightTheme: Theme = { autoAccept: 'rgb(135,0,255)', // Electric violet bashBorder: 'rgb(255,0,135)', // Vibrant pink claude: 'rgb(215,119,87)', // Claude orange - startupAccent: 'rgb(124,176,133)', // Free Code pastel green + startupAccent: 'rgb(255,195,130)', // Claudium light orange claudeShimmer: 'rgb(245,149,117)', // Lighter claude orange for shimmer effect claudeBlue_FOR_SYSTEM_SPINNER: 'rgb(87,105,247)', // Medium blue for system spinner claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'rgb(117,135,255)', // Lighter blue for system spinner shimmer @@ -364,7 +364,7 @@ const lightDaltonizedTheme: Theme = { autoAccept: 'rgb(135,0,255)', // Electric violet bashBorder: 'rgb(0,102,204)', // Blue instead of pink claude: 'rgb(255,153,51)', // Orange adjusted for deuteranopia - startupAccent: 'rgb(124,176,133)', // Free Code pastel green + startupAccent: 'rgb(124,176,133)', // Claudium pastel green claudeShimmer: 'rgb(255,183,101)', // Lighter orange for shimmer effect claudeBlue_FOR_SYSTEM_SPINNER: 'rgb(51,102,255)', // Bright blue for system spinner claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'rgb(101,152,255)', // Lighter bright blue for system spinner shimmer @@ -446,7 +446,7 @@ const darkTheme: Theme = { autoAccept: 'rgb(175,135,255)', // Electric violet bashBorder: 'rgb(253,93,177)', // Bright pink claude: 'rgb(215,119,87)', // Claude orange - startupAccent: 'rgb(184,225,174)', // Free Code pastel green + startupAccent: 'rgb(184,225,174)', // Claudium pastel green claudeShimmer: 'rgb(235,159,127)', // Lighter claude orange for shimmer effect claudeBlue_FOR_SYSTEM_SPINNER: 'rgb(147,165,255)', // Blue for system spinner claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'rgb(177,195,255)', // Lighter blue for system spinner shimmer @@ -528,7 +528,7 @@ const darkDaltonizedTheme: Theme = { autoAccept: 'rgb(175,135,255)', // Electric violet bashBorder: 'rgb(51,153,255)', // Bright blue claude: 'rgb(255,153,51)', // Orange adjusted for deuteranopia - startupAccent: 'rgb(184,225,174)', // Free Code pastel green + startupAccent: 'rgb(184,225,174)', // Claudium pastel green claudeShimmer: 'rgb(255,183,101)', // Lighter orange for shimmer effect claudeBlue_FOR_SYSTEM_SPINNER: 'rgb(153,204,255)', // Light blue for system spinner claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'rgb(183,224,255)', // Lighter blue for system spinner shimmer diff --git a/src/utils/thinking.ts b/src/utils/thinking.ts index b37fe42d..04c374c6 100644 --- a/src/utils/thinking.ts +++ b/src/utils/thinking.ts @@ -1,7 +1,7 @@ // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered import type { Theme } from './theme.js' import { feature } from 'bun:bundle' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { getCanonicalName } from './model/model.js' import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js' import { getAPIProvider } from './model/providers.js' diff --git a/src/utils/toolResultStorage.ts b/src/utils/toolResultStorage.ts index f4dfef32..67245500 100644 --- a/src/utils/toolResultStorage.ts +++ b/src/utils/toolResultStorage.ts @@ -12,9 +12,9 @@ import { MAX_TOOL_RESULT_BYTES, MAX_TOOL_RESULTS_PER_MESSAGE_CHARS, } from '../constants/toolLimits.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' -import { logEvent } from '../services/analytics/index.js' -import { sanitizeToolNameForAnalytics } from '../services/analytics/metadata.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' +import { logEvent } from '../services/analytics-stub.js' +import { sanitizeToolNameForAnalytics } from '../services/analytics-stub.js' import type { Message } from '../types/message.js' import { logForDebugging } from './debug.js' import { getErrnoCode, toError } from './errors.js' diff --git a/src/utils/toolSearch.ts b/src/utils/toolSearch.ts index 5f9a001e..3393957e 100644 --- a/src/utils/toolSearch.ts +++ b/src/utils/toolSearch.ts @@ -7,11 +7,11 @@ */ import memoize from 'lodash-es/memoize.js' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from '../services/analytics/index.js' +} from '../services/analytics-stub.js' import type { Tool } from '../Tool.js' import { type ToolPermissionContext, diff --git a/src/utils/unaryLogging.ts b/src/utils/unaryLogging.ts index 2e375e62..811dfa15 100644 --- a/src/utils/unaryLogging.ts +++ b/src/utils/unaryLogging.ts @@ -1,7 +1,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../services/analytics-stub.js' export type CompletionType = | 'str_replace_single' diff --git a/src/utils/warningHandler.ts b/src/utils/warningHandler.ts index 3c9572e6..25610942 100644 --- a/src/utils/warningHandler.ts +++ b/src/utils/warningHandler.ts @@ -2,7 +2,7 @@ import { posix, win32 } from 'path' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, -} from 'src/services/analytics/index.js' +} from '../services/analytics-stub.js' import { logForDebugging } from './debug.js' import { isEnvTruthy } from './envUtils.js' import { getPlatform } from './platform.js' diff --git a/src/voice/voiceModeEnabled.ts b/src/voice/voiceModeEnabled.ts index 1d8867c3..e6a0260e 100644 --- a/src/voice/voiceModeEnabled.ts +++ b/src/voice/voiceModeEnabled.ts @@ -1,5 +1,5 @@ import { feature } from 'bun:bundle' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics-stub.js' import { getClaudeAIOAuthTokens, isAnthropicAuthEnabled, diff --git a/tests/verify-privacy.sh b/tests/verify-privacy.sh new file mode 100755 index 00000000..1da246a5 --- /dev/null +++ b/tests/verify-privacy.sh @@ -0,0 +1,276 @@ +#!/bin/bash +# Privacy Verification Test Suite +# This script verifies that all telemetry, analytics, and fingerprinting code has been removed + +set -e + +PROJECT_ROOT="$(pwd)" +cd "$PROJECT_ROOT" + +echo "==================================" +echo "Privacy Verification Test Suite" +echo "==================================" +echo "" + +# Color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +TESTS_PASSED=0 +TESTS_FAILED=0 + +# Test function +test_check() { + local test_name="$1" + local command="$2" + local expected_count="$3" + + echo -n "Testing: $test_name ... " + + result=$(eval "$command" 2>/dev/null | wc -l | tr -d ' ') + + if [ "$result" -eq "$expected_count" ]; then + echo -e "${GREEN}✓ PASS${NC} (found $result matches, expected $expected_count)" + ((TESTS_PASSED++)) + return 0 + else + echo -e "${RED}✗ FAIL${NC} (found $result matches, expected $expected_count)" + ((TESTS_FAILED++)) + return 1 + fi +} + +echo "=== 1. Verifying Deleted Directories ===" +echo "" + +test_check "Telemetry directory deleted" \ + "find src/utils/telemetry -type f 2>/dev/null || true" \ + "0" + +test_check "Analytics directory deleted" \ + "find src/services/analytics -type f 2>/dev/null || true" \ + "0" + +echo "" +echo "=== 2. Verifying Deleted Files ===" +echo "" + +test_check "fingerprint.ts deleted" \ + "find src -name 'fingerprint.ts' -not -name '*stub*' 2>/dev/null || true" \ + "0" + +test_check "telemetryAttributes.ts deleted" \ + "find src -name 'telemetryAttributes.ts' -not -name '*stub*' 2>/dev/null || true" \ + "0" + +echo "" +echo "=== 3. Verifying No Old Imports Remain ===" +echo "" + +test_check "No local telemetry imports (non-stub)" \ + "grep -r \"from.*['\\\"]\\.\\.*/telemetry/\" src/ --include='*.ts' --include='*.tsx' || true" \ + "0" + +test_check "No local analytics imports (non-stub)" \ + "grep -r \"from.*['\\\"]\\.\\.*/analytics/\" src/ --include='*.ts' --include='*.tsx' || true" \ + "0" + +test_check "No fingerprint imports (non-stub)" \ + "grep -r \"from.*['\\\"].*fingerprint[^-]\" src/ --include='*.ts' --include='*.tsx' --exclude='*stub*' || true" \ + "0" + +test_check "No telemetryAttributes imports (non-stub)" \ + "grep -r \"from.*['\\\"].*telemetryAttributes[^-]\" src/ --include='*.ts' --include='*.tsx' || true" \ + "0" + +echo "" +echo "=== 4. Verifying API Endpoints Removed ===" +echo "" + +test_check "No BigQuery metrics endpoint" \ + "grep -r 'api.anthropic.com/api/claude_code/metrics' src/ || true" \ + "0" + +test_check "No event logging endpoint" \ + "grep -r 'api.anthropic.com/api/event_logging/batch' src/ || true" \ + "0" + +echo "" +echo "=== 5. Verifying Tracking Code Patterns Removed ===" +echo "" + +# Allow computeFingerprint calls if they import from stub +echo -n "Testing: computeFingerprint calls use stub implementation ... " +fingerprint_calls=$(grep -r 'computeFingerprint(' src/ --include='*.ts' --include='*.tsx' --exclude='*stub*' 2>/dev/null | wc -l | tr -d ' ') +if [ "$fingerprint_calls" -eq "0" ]; then + echo -e "${GREEN}✓ PASS${NC} (no calls found)" + ((TESTS_PASSED++)) +else + # Check if the files importing it use the stub + files_with_calls=$(grep -r 'computeFingerprint(' src/ --include='*.ts' --include='*.tsx' --exclude='*stub*' -l 2>/dev/null || true) + all_use_stub=true + for file in $files_with_calls; do + if ! grep -q "from.*fingerprint-stub" "$file" 2>/dev/null; then + all_use_stub=false + break + fi + done + if [ "$all_use_stub" = true ]; then + echo -e "${GREEN}✓ PASS${NC} (all calls use stub implementation)" + ((TESTS_PASSED++)) + else + echo -e "${RED}✗ FAIL${NC} (found calls not using stub)" + ((TESTS_FAILED++)) + fi +fi + +test_check "No BigQueryExporter usage" \ + "grep -r 'BigQueryExporter' src/ --include='*.ts' --include='*.tsx' || true" \ + "0" + +test_check "No firstPartyEventLogger usage" \ + "grep -r 'firstPartyEventLogger' src/ --include='*.ts' --include='*.tsx' | grep -v '//.*firstPartyEventLogger' || true" \ + "0" + +# Check for actual GrowthBook method calls (not just comments/docs) +echo -n "Testing: No GrowthBook API calls ... " +gb_calls=$(grep -r 'growthbook\.\(getFeatureValue\|evalFeature\|run\|setAttributes\)' src/ --include='*.ts' --include='*.tsx' --exclude='*stub*' 2>/dev/null | wc -l | tr -d ' ') +if [ "$gb_calls" -eq "0" ]; then + echo -e "${GREEN}✓ PASS${NC} (no API calls found)" + ((TESTS_PASSED++)) +else + echo -e "${RED}✗ FAIL${NC} (found $gb_calls API calls)" + ((TESTS_FAILED++)) +fi + +echo "" +echo "=== 6. Verifying Stub Files Exist ===" +echo "" + +test_check "telemetry-stub.ts exists" \ + "find src -name 'telemetry-stub.ts' 2>/dev/null" \ + "1" + +test_check "analytics-stub.ts exists" \ + "find src -name 'analytics-stub.ts' 2>/dev/null" \ + "1" + +test_check "fingerprint-stub.ts exists" \ + "find src -name 'fingerprint-stub.ts' 2>/dev/null" \ + "1" + +test_check "telemetryAttributes-stub.ts exists" \ + "find src -name 'telemetryAttributes-stub.ts' 2>/dev/null" \ + "1" + +echo "" +echo "=== 7. Verifying Stub Implementations ===" +echo "" + +# Check that stubs return no-op or safe values +test_check "Fingerprint stub returns 'no-fingerprint'" \ + "grep -c \"return 'no-fingerprint'\" src/utils/fingerprint-stub.ts" \ + "1" + +echo -n "Testing: Analytics stub exports no-op functions ... " +analytics_exports=$(grep -E '^export (function|const)' src/services/analytics-stub.ts 2>/dev/null | wc -l | tr -d ' ') +if [ "$analytics_exports" -ge "10" ]; then + echo -e "${GREEN}✓ PASS${NC} (found $analytics_exports exports)" + ((TESTS_PASSED++)) +else + echo -e "${RED}✗ FAIL${NC} (found $analytics_exports exports, expected at least 10)" + ((TESTS_FAILED++)) +fi + +echo "" +echo "=== 8. Checking for Suspicious Patterns ===" +echo "" + +# Look for potential data collection patterns +echo -n "Checking for SHA256 hash operations... " +sha_count=$(grep -r "createHash('sha256')" src/ --include='*.ts' --include='*.tsx' --exclude='*stub*' --exclude-dir=node_modules 2>/dev/null | wc -l | tr -d ' ') +if [ "$sha_count" -eq "0" ]; then + echo -e "${GREEN}✓ None found${NC}" + ((TESTS_PASSED++)) +else + echo -e "${YELLOW}⚠ Found $sha_count instances${NC} (may be legitimate uses)" + # Not counting as failure since SHA256 can be used for non-tracking purposes +fi + +echo -n "Checking for user.id references... " +user_id_count=$(grep -r "user\.id" src/ --include='*.ts' --include='*.tsx' --exclude='*stub*' --exclude-dir=node_modules 2>/dev/null | wc -l | tr -d ' ') +if [ "$user_id_count" -eq "0" ]; then + echo -e "${GREEN}✓ None found${NC}" + ((TESTS_PASSED++)) +else + echo -e "${YELLOW}⚠ Found $user_id_count instances${NC}" +fi + +echo -n "Checking for session tracking... " +session_track=$(grep -r "sessionId\|session\.id" src/ --include='*.ts' --include='*.tsx' --exclude='*stub*' --exclude-dir=node_modules 2>/dev/null | grep -v "// " | wc -l | tr -d ' ') +if [ "$session_track" -eq "0" ]; then + echo -e "${GREEN}✓ None found${NC}" + ((TESTS_PASSED++)) +else + echo -e "${YELLOW}⚠ Found $session_track instances${NC} (checking if legitimate...)" +fi + +echo "" +echo "=== 9. Package.json Verification ===" +echo "" + +test_check "Package name changed to claude-code-clean" \ + "grep -c '\"name\": \"claude-code-clean\"' package.json" \ + "1" + +test_check "Version changed to clean variant" \ + "grep -c '\"version\": \"1.0.0-clean\"' package.json" \ + "1" + +test_check "Binary name changed to claude-clean" \ + "grep -c '\"claude-clean\"' package.json" \ + "1" + +echo "" +echo "=== 10. Essential Files Verification ===" +echo "" + +test_check "README.md exists" \ + "test -f README.md && echo 'exists'" \ + "1" + +test_check "LICENSE exists" \ + "test -f LICENSE && echo 'exists'" \ + "1" + +echo "" +echo "==================================" +echo "Test Results Summary" +echo "==================================" +echo -e "${GREEN}Tests Passed: $TESTS_PASSED${NC}" +if [ "$TESTS_FAILED" -gt "0" ]; then + echo -e "${RED}Tests Failed: $TESTS_FAILED${NC}" +else + echo -e "${GREEN}Tests Failed: 0${NC}" +fi +echo "" + +if [ "$TESTS_FAILED" -eq "0" ]; then + echo -e "${GREEN}✓✓✓ ALL PRIVACY CHECKS PASSED ✓✓✓${NC}" + echo "" + echo "This codebase has been verified to be free of:" + echo " ✓ Telemetry infrastructure" + echo " ✓ Analytics and event logging" + echo " ✓ User fingerprinting" + echo " ✓ Session tracking" + echo " ✓ Data collection endpoints" + echo "" + exit 0 +else + echo -e "${RED}✗✗✗ SOME TESTS FAILED ✗✗✗${NC}" + echo "" + echo "Please review the failed tests above." + exit 1 +fi