diff --git a/src/main/java/com/claudecode/telemetry/FeatureFlagService.java b/src/main/java/com/claudecode/telemetry/FeatureFlagService.java
new file mode 100644
index 0000000..758f252
--- /dev/null
+++ b/src/main/java/com/claudecode/telemetry/FeatureFlagService.java
@@ -0,0 +1,186 @@
+package com.claudecode.telemetry;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.*;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Feature Flag 服务 —— 对应 claude-code 中 GrowthBook 的本地替代。
+ *
+ * 使用本地 JSON 文件管理 feature flags,支持:
+ *
+ * - 布尔型 flag(功能开关)
+ * - 字符串/数字型 flag(配置值)
+ * - 运行时重加载
+ * - 环境变量覆盖 (CLAUDE_CODE_FF_xxx)
+ *
+ *
+ * 配置文件位置: ~/.claude-code/feature-flags.json
+ */
+public class FeatureFlagService {
+
+ private static final Logger log = LoggerFactory.getLogger(FeatureFlagService.class);
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ /** flag 默认值 */
+ private static final Map DEFAULTS = Map.ofEntries(
+ Map.entry("DIRECT_CONNECT", true),
+ Map.entry("WORKTREE_MODE", true),
+ Map.entry("LSP_INTEGRATION", true),
+ Map.entry("SESSION_MEMORY", true),
+ Map.entry("COORDINATOR_MODE", true),
+ Map.entry("PLUGIN_MARKETPLACE", false),
+ Map.entry("ADVANCED_UI", false),
+ Map.entry("VOICE_INPUT", false),
+ Map.entry("AUTO_COMPACT", true),
+ Map.entry("METRICS_COLLECTION", false)
+ );
+
+ private final Path configFile;
+ private final ConcurrentHashMap flags = new ConcurrentHashMap<>();
+ private long lastLoadTime = 0;
+
+ public FeatureFlagService() {
+ this(Path.of(System.getProperty("user.home"), ".claude-code", "feature-flags.json"));
+ }
+
+ public FeatureFlagService(Path configFile) {
+ this.configFile = configFile;
+ loadFlags();
+ }
+
+ /**
+ * 获取布尔型 flag。
+ */
+ public boolean isEnabled(String flagName) {
+ // 环境变量覆盖(最高优先级)
+ String envKey = "CLAUDE_CODE_FF_" + flagName;
+ String envVal = System.getenv(envKey);
+ if (envVal != null) {
+ return "true".equalsIgnoreCase(envVal) || "1".equals(envVal);
+ }
+
+ Object value = flags.get(flagName);
+ if (value instanceof Boolean b) return b;
+ if (value != null) return Boolean.parseBoolean(value.toString());
+
+ // 默认值
+ Object def = DEFAULTS.get(flagName);
+ if (def instanceof Boolean b) return b;
+ return false;
+ }
+
+ /**
+ * 获取字符串型 flag。
+ */
+ public String getString(String flagName, String defaultValue) {
+ String envKey = "CLAUDE_CODE_FF_" + flagName;
+ String envVal = System.getenv(envKey);
+ if (envVal != null) return envVal;
+
+ Object value = flags.get(flagName);
+ if (value != null) return value.toString();
+
+ Object def = DEFAULTS.get(flagName);
+ if (def != null) return def.toString();
+
+ return defaultValue;
+ }
+
+ /**
+ * 获取数字型 flag。
+ */
+ public long getNumber(String flagName, long defaultValue) {
+ String envKey = "CLAUDE_CODE_FF_" + flagName;
+ String envVal = System.getenv(envKey);
+ if (envVal != null) {
+ try {
+ return Long.parseLong(envVal);
+ } catch (NumberFormatException ignored) {}
+ }
+
+ Object value = flags.get(flagName);
+ if (value instanceof Number n) return n.longValue();
+ if (value != null) {
+ try {
+ return Long.parseLong(value.toString());
+ } catch (NumberFormatException ignored) {}
+ }
+
+ Object def = DEFAULTS.get(flagName);
+ if (def instanceof Number n) return n.longValue();
+
+ return defaultValue;
+ }
+
+ /**
+ * 设置 flag 值(运行时)。
+ */
+ public void setFlag(String flagName, Object value) {
+ flags.put(flagName, value);
+ }
+
+ /**
+ * 获取所有 flag 及其当前值。
+ */
+ public Map getAllFlags() {
+ Map result = new ConcurrentHashMap<>(DEFAULTS);
+ result.putAll(flags);
+ return result;
+ }
+
+ /**
+ * 从配置文件加载 flag。
+ */
+ public void loadFlags() {
+ if (!Files.isRegularFile(configFile)) {
+ log.debug("Feature flag config not found: {}", configFile);
+ return;
+ }
+
+ try {
+ Map loaded = MAPPER.readValue(
+ configFile.toFile(), new TypeReference<>() {});
+ flags.putAll(loaded);
+ lastLoadTime = System.currentTimeMillis();
+ log.info("Loaded {} feature flags from {}", loaded.size(), configFile);
+ } catch (IOException e) {
+ log.warn("Failed to load feature flags: {}", e.getMessage());
+ }
+ }
+
+ /**
+ * 保存当前 flag 到配置文件。
+ */
+ public void saveFlags() {
+ try {
+ Files.createDirectories(configFile.getParent());
+ MAPPER.writerWithDefaultPrettyPrinter()
+ .writeValue(configFile.toFile(), flags);
+ log.info("Saved {} feature flags to {}", flags.size(), configFile);
+ } catch (IOException e) {
+ log.error("Failed to save feature flags: {}", e.getMessage());
+ }
+ }
+
+ /**
+ * 重新加载(如果文件有变化)。
+ */
+ public void reloadIfChanged() {
+ try {
+ if (!Files.isRegularFile(configFile)) return;
+ long mtime = Files.getLastModifiedTime(configFile).toMillis();
+ if (mtime > lastLoadTime) {
+ loadFlags();
+ }
+ } catch (IOException e) {
+ log.debug("Failed to check flag file mtime: {}", e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/claudecode/telemetry/FeatureGate.java b/src/main/java/com/claudecode/telemetry/FeatureGate.java
new file mode 100644
index 0000000..48c48c6
--- /dev/null
+++ b/src/main/java/com/claudecode/telemetry/FeatureGate.java
@@ -0,0 +1,137 @@
+package com.claudecode.telemetry;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+
+/**
+ * Feature Gate — 连接 FeatureFlagService 到具体功能/工具的开关控制。
+ *
+ * 对应 claude-code 中散布在各处的 featureFlag.isEnabled() 检查。
+ * 提供统一的 gate 注册和查询机制。
+ */
+public class FeatureGate {
+
+ private static final Logger log = LoggerFactory.getLogger(FeatureGate.class);
+
+ private final FeatureFlagService flagService;
+
+ /** tool name → flag name 映射 */
+ private final Map toolGates = new LinkedHashMap<>();
+
+ /** feature category → flag name 映射 */
+ private final Map featureGates = new LinkedHashMap<>();
+
+ public FeatureGate(FeatureFlagService flagService) {
+ this.flagService = flagService;
+ registerDefaults();
+ }
+
+ private void registerDefaults() {
+ // 工具级 gate
+ registerToolGate("enter_worktree", "WORKTREE_MODE");
+ registerToolGate("exit_worktree", "WORKTREE_MODE");
+
+ // 功能级 gate
+ registerFeatureGate("server_mode", "DIRECT_CONNECT");
+ registerFeatureGate("lsp", "LSP_INTEGRATION");
+ registerFeatureGate("session_memory", "SESSION_MEMORY");
+ registerFeatureGate("coordinator", "COORDINATOR_MODE");
+ registerFeatureGate("plugin_marketplace", "PLUGIN_MARKETPLACE");
+ registerFeatureGate("auto_compact", "AUTO_COMPACT");
+ registerFeatureGate("metrics", "METRICS_COLLECTION");
+ registerFeatureGate("voice", "VOICE_INPUT");
+ registerFeatureGate("advanced_ui", "ADVANCED_UI");
+ }
+
+ /**
+ * 注册工具级 gate。
+ */
+ public void registerToolGate(String toolName, String flagName) {
+ toolGates.put(toolName, flagName);
+ }
+
+ /**
+ * 注册功能级 gate。
+ */
+ public void registerFeatureGate(String featureName, String flagName) {
+ featureGates.put(featureName, flagName);
+ }
+
+ /**
+ * 检查工具是否启用。
+ */
+ public boolean isToolEnabled(String toolName) {
+ String flagName = toolGates.get(toolName);
+ if (flagName == null) return true; // 没注册 gate 的工具默认启用
+ return flagService.isEnabled(flagName);
+ }
+
+ /**
+ * 检查功能是否启用。
+ */
+ public boolean isFeatureEnabled(String featureName) {
+ String flagName = featureGates.get(featureName);
+ if (flagName == null) return true;
+ return flagService.isEnabled(flagName);
+ }
+
+ /**
+ * 获取所有已禁用的工具名称。
+ */
+ public Set getDisabledTools() {
+ Set disabled = new LinkedHashSet<>();
+ for (Map.Entry entry : toolGates.entrySet()) {
+ if (!flagService.isEnabled(entry.getValue())) {
+ disabled.add(entry.getKey());
+ }
+ }
+ return disabled;
+ }
+
+ /**
+ * 获取所有已禁用的功能名称。
+ */
+ public Set getDisabledFeatures() {
+ Set disabled = new LinkedHashSet<>();
+ for (Map.Entry entry : featureGates.entrySet()) {
+ if (!flagService.isEnabled(entry.getValue())) {
+ disabled.add(entry.getKey());
+ }
+ }
+ return disabled;
+ }
+
+ /**
+ * 生成人类可读的 gate 状态报告。
+ */
+ public String statusReport() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("=== Feature Gates ===\n");
+
+ if (!featureGates.isEmpty()) {
+ sb.append("\nFeatures:\n");
+ for (Map.Entry entry : featureGates.entrySet()) {
+ boolean enabled = flagService.isEnabled(entry.getValue());
+ sb.append(" ").append(enabled ? "✓" : "✗").append(" ")
+ .append(entry.getKey()).append(" (").append(entry.getValue()).append(")\n");
+ }
+ }
+
+ if (!toolGates.isEmpty()) {
+ sb.append("\nTools:\n");
+ for (Map.Entry entry : toolGates.entrySet()) {
+ boolean enabled = flagService.isEnabled(entry.getValue());
+ sb.append(" ").append(enabled ? "✓" : "✗").append(" ")
+ .append(entry.getKey()).append(" (").append(entry.getValue()).append(")\n");
+ }
+ }
+
+ return sb.toString();
+ }
+
+ public FeatureFlagService getFlagService() {
+ return flagService;
+ }
+}
diff --git a/src/main/java/com/claudecode/telemetry/MetricsCollector.java b/src/main/java/com/claudecode/telemetry/MetricsCollector.java
new file mode 100644
index 0000000..d135d1f
--- /dev/null
+++ b/src/main/java/com/claudecode/telemetry/MetricsCollector.java
@@ -0,0 +1,232 @@
+package com.claudecode.telemetry;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.*;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 基础指标收集器 —— 替代 claude-code 中的 Datadog/OpenTelemetry 全套。
+ *
+ * 只收集本地指标,不上报到任何远程服务。
+ *
+ * 收集的指标:
+ *
+ * - 会话时长
+ * - 工具使用次数(按工具名)
+ * - 命令使用次数(按命令名)
+ * - API 调用次数和 token 用量
+ * - 错误次数(按类型)
+ * - 自动压缩次数
+ *
+ *
+ * 存储位置: ~/.claude-code/metrics/{date}.json
+ */
+public class MetricsCollector {
+
+ private static final Logger log = LoggerFactory.getLogger(MetricsCollector.class);
+ private static final ObjectMapper MAPPER = new ObjectMapper()
+ .enable(SerializationFeature.INDENT_OUTPUT);
+
+ private final Path metricsDir;
+ private final String sessionId;
+ private final Instant sessionStart;
+
+ // ==================== 计数器 ====================
+
+ /** 工具使用次数: toolName → count */
+ private final ConcurrentHashMap toolUsage = new ConcurrentHashMap<>();
+
+ /** 命令使用次数: commandName → count */
+ private final ConcurrentHashMap commandUsage = new ConcurrentHashMap<>();
+
+ /** 错误次数: errorType → count */
+ private final ConcurrentHashMap errorCounts = new ConcurrentHashMap<>();
+
+ /** API 调用次数 */
+ private final AtomicLong apiCallCount = new AtomicLong(0);
+
+ /** 总 token 使用量 */
+ private final AtomicLong totalInputTokens = new AtomicLong(0);
+ private final AtomicLong totalOutputTokens = new AtomicLong(0);
+
+ /** 自动压缩次数 */
+ private final AtomicLong autoCompactCount = new AtomicLong(0);
+
+ /** 用户消息数 */
+ private final AtomicLong userMessageCount = new AtomicLong(0);
+
+ /** 助手消息数 */
+ private final AtomicLong assistantMessageCount = new AtomicLong(0);
+
+ public MetricsCollector() {
+ this(Path.of(System.getProperty("user.home"), ".claude-code", "metrics"),
+ UUID.randomUUID().toString().substring(0, 8));
+ }
+
+ public MetricsCollector(Path metricsDir, String sessionId) {
+ this.metricsDir = metricsDir;
+ this.sessionId = sessionId;
+ this.sessionStart = Instant.now();
+ }
+
+ // ==================== 记录方法 ====================
+
+ public void recordToolUse(String toolName) {
+ toolUsage.computeIfAbsent(toolName, k -> new AtomicLong(0)).incrementAndGet();
+ }
+
+ public void recordCommand(String commandName) {
+ commandUsage.computeIfAbsent(commandName, k -> new AtomicLong(0)).incrementAndGet();
+ }
+
+ public void recordError(String errorType) {
+ errorCounts.computeIfAbsent(errorType, k -> new AtomicLong(0)).incrementAndGet();
+ }
+
+ public void recordApiCall(long inputTokens, long outputTokens) {
+ apiCallCount.incrementAndGet();
+ totalInputTokens.addAndGet(inputTokens);
+ totalOutputTokens.addAndGet(outputTokens);
+ }
+
+ public void recordAutoCompact() {
+ autoCompactCount.incrementAndGet();
+ }
+
+ public void recordUserMessage() {
+ userMessageCount.incrementAndGet();
+ }
+
+ public void recordAssistantMessage() {
+ assistantMessageCount.incrementAndGet();
+ }
+
+ // ==================== 获取指标 ====================
+
+ public long getSessionDurationSeconds() {
+ return Instant.now().getEpochSecond() - sessionStart.getEpochSecond();
+ }
+
+ public Map getToolUsage() {
+ Map result = new TreeMap<>();
+ toolUsage.forEach((k, v) -> result.put(k, v.get()));
+ return result;
+ }
+
+ public Map getCommandUsage() {
+ Map result = new TreeMap<>();
+ commandUsage.forEach((k, v) -> result.put(k, v.get()));
+ return result;
+ }
+
+ public Map getErrorCounts() {
+ Map result = new TreeMap<>();
+ errorCounts.forEach((k, v) -> result.put(k, v.get()));
+ return result;
+ }
+
+ // ==================== 持久化 ====================
+
+ /**
+ * 将当前会话指标保存到按日期分割的 JSON 文件。
+ */
+ public void flush() {
+ try {
+ Files.createDirectories(metricsDir);
+
+ String date = LocalDate.now(ZoneId.systemDefault()).toString();
+ Path file = metricsDir.resolve(date + ".json");
+
+ // 读取已有数据
+ List