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.
-
+
---
@@ -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