Compare commits

..

10 Commits

Author SHA1 Message Date
claude-code-best
308efd3220 chore: 1.8.0 2026-04-21 12:05:58 +08:00
claude-code-best
ca2b97cca6 fix: 修复 mcp-chrome 的 server 端的频繁断联问题 2026-04-21 10:55:25 +08:00
claude-code-best
96ec96c720 feat: 添加 ccb update 命令,支持 npm/bun 自动更新
从 package.json 读取当前版本,查询 npm registry 最新版本,
自动检测安装方式(bun 或 npm)执行全局更新。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 22:35:57 +08:00
claude-code-best
13a0bfc479 fix: 修复构建产物 import 失效问题 2026-04-20 22:29:44 +08:00
claude-code-best
84f0271813 chore: 1.7.1 2026-04-20 22:13:31 +08:00
claude-code-best
ed4bdb9338 feat: 增强 auto mode 的易用性 (#312)
* feat: poor 模式降级 yolo 审阅模型

* feat: 为多模块添加 Langfuse tracing 支持

在 web search、agent creation、away summary、token estimation、
skill improvement 等模块中集成 Langfuse trace,并透传至
compact/apiQueryHook/execPromptHook 等调用链。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 让 auto mode 记录回主 trace

* fix: reopen auto mode prompt when classifier is unavailable

* fix: 修复 auto mode 情况下, llm 报错导致弹窗也不打开的问题

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 21:13:09 +08:00
claude-code-best
e4ce08fe39 Fixture/langfuse record auto mode data error (#308)
* fix: 修复状态栏 context 计数器在 loading 时闪现为 0 的问题

第三方 API(如智谱)在 message_start 中可能不返回完整 usage 数据,
导致 getCurrentUsage 返回全零 usage 对象,使 ctx 显示为 0%。

双重保护:
- getCurrentUsage: 跳过全零 usage,继续往前找有真实数据的 message
- calculateContextPercentages: totalInputTokens 为 0 时返回 null

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 外部化 ESM 包使用 createRequire 替代裸 require

color-diff-napi、image-processor-napi、audio-capture-napi 声明
"type": "module" 但使用裸 require(),Node.js ESM 中 require
不可用。改用 createRequire(import.meta.url) 或顶层 import。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: getDefaultSonnetModel 优先使用用户配置的模型,修复第三方 provider 模型不存在错误

当用户通过 ANTHROPIC_MODEL 或 settings 配置了自定义 provider 支持的模型时,
getDefaultSonnetModel/Haiku/Opus 现在会优先使用该配置,而非硬编码 Anthropic 官方模型 ID。
同时改进 Langfuse 可观测性:sideQuery 失败时记录错误信息到 span,
optional 模式下标记 WARNING 而非 ERROR。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 将 auto_mode classifier 的 side-query span 绑定到父 trace

classifyYoloAction 及 classifyYoloActionXml 接收 parentSpan 参数,
透传给 sideQuery 调用,使 auto_mode 的 side-query span 嵌套在主 agent trace 下。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 穷鬼模式下跳过 memdir_relevance side-query

Poor mode 启用时不执行 findRelevantMemories 的预取调用,
避免额外的 API token 消耗。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: 添加 test:all 脚本用于完成任务后的全量检查

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Vite 构建补齐缺失的 feature flags,修复 auto mode 不可见

Vite 构建插件的 DEFAULT_BUILD_FEATURES 缺少 BUDDY、TRANSCRIPT_CLASSIFIER、
BRIDGE_MODE、ACP、BG_SESSIONS、TEMPLATES,导致 feature('TRANSCRIPT_CLASSIFIER')
被替换为 false,auto mode 从 Shift+Tab 循环中消失。与 build.ts 对齐。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 统一 feature flags 到 defines.ts,修复 Vite 构建缺失 auto mode

将 DEFAULT_BUILD_FEATURES 列表从 build.ts、dev.ts、vite-plugin-feature-flags.ts
三处内联定义统一到 scripts/defines.ts 单一导出。之前的 Vite 插件缺少
TRANSCRIPT_CLASSIFIER 等 feature flag,导致 auto mode 在 Vite 构建中不可见。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:30:05 +08:00
claude-code-best
92f8a92fbb feat: 正式启用 auto mode (#307)
* fix: 修复settings.json内存状态溢出的问题

* fix: 修复auto mode gate check未处理的promise rejection

在 bypassPermissionsKillswitch.ts 的 useKickOffCheckAndDisableAutoModeIfNeeded
中,void fire-and-forget 调用缺少 .catch() 处理,导致 verifyAutoModeGateAccess
失败时产生 unhandled promise rejection。同时移除 permissionSetup.ts 中冗余的
null check。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: 开放 auto mode 和 bypass mode 给所有用户

通过 Shift+Tab 统一循环:default → acceptEdits → plan → auto → bypassPermissions → default

- 移除 USER_TYPE 分支判断,所有用户使用同一循环路径
- isBypassPermissionsModeAvailable 始终为 true
- isAutoModeAvailable 初始化直接为 true
- 移除 AutoModeOptInDialog 确认流程
- 简化 isAutoModeGateEnabled 仅保留快模式熔断器
- 简化 verifyAutoModeGateAccess 仅检查快模式
- 移除 GrowthBook/Statsig 远程门控
- bypass permissions killswitch 改为 no-op
- 新增 24 个测试覆盖循环逻辑和门控不变量

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: 为sideQuery添加Langfuse追踪

sideQuery 绕过了 claude.ts 的主 API 路径,导致所有走 sideQuery 的调用
(auto mode classifier、permission explainer、session search 等)都没有
Langfuse 记录。现在为每次 sideQuery 调用创建独立 trace 并记录 LLM observation,
未配置 Langfuse 时全部 no-op。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: ACP availableModes 补齐 bypassPermissions 并修正测试 import 路径

- ACP agent availableModes 按条件包含 bypassPermissions(非 root/sandbox)
- 顺序对齐 REPL 循环:default → acceptEdits → plan → auto → bypassPermissions
- 新增 2 个测试验证 availableModes 包含 bypassPermissions 及模式切换
- 修正 getNextPermissionMode.test.ts 和 permissionSetup.test.ts 的 import 路径

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 10:20:27 +08:00
claude-code-best
a67e2d0e97 docs: 更新 npm 安装 2026-04-19 22:00:48 +08:00
claude-code-best
8c629858ab chore: 1.6.0 2026-04-19 21:37:35 +08:00
55 changed files with 1195 additions and 950 deletions

View File

@@ -58,6 +58,9 @@ bun run health
# Check unused exports
bun run check:unused
# Full check (typecheck + lint + test) — run after completing any task
bun run test:all
bun run typecheck
# Remote Control Server

View File

@@ -41,10 +41,15 @@
不用克隆仓库, 从 NPM 下载后, 直接使用
```sh
bun i -g claude-code-best
bun pm -g trust claude-code-best
npm i -g claude-code-best
# bun 安装比较多问题, 推荐 npm 装
# bun i -g claude-code-best
# bun pm -g trust claude-code-best
ccb # 以 nodejs 打开 claude code
ccb-bun # 以 bun 形态打开
ccb update # 更新到最新版本
CLAUDE_BRIDGE_BASE_URL=https://remote-control.claude-code-best.win/ CLAUDE_BRIDGE_OAUTH_TOKEN=test-my-key ccb --remote-control # 我们有自部署的远程控制
```

View File

@@ -1,6 +1,7 @@
import { readdir, readFile, writeFile, cp } from 'fs/promises'
import { join } from 'path'
import { getMacroDefines } from './scripts/defines.ts'
import { DEFAULT_BUILD_FEATURES } from './scripts/defines.ts'
const outdir = 'dist'
@@ -8,48 +9,6 @@ const outdir = 'dist'
const { rmSync } = await import('fs')
rmSync(outdir, { recursive: true, force: true })
// Default features that match the official CLI build.
// Additional features can be enabled via FEATURE_<NAME>=1 env vars.
const DEFAULT_BUILD_FEATURES = [
'BUDDY', 'TRANSCRIPT_CLASSIFIER', 'BRIDGE_MODE',
'AGENT_TRIGGERS_REMOTE',
'CHICAGO_MCP',
'VOICE_MODE',
'SHOT_STATS',
'PROMPT_CACHE_BREAK_DETECTION',
'TOKEN_BUDGET',
// P0: local features
'AGENT_TRIGGERS',
'ULTRATHINK',
'BUILTIN_EXPLORE_PLAN_AGENTS',
'LODESTONE',
// P1: API-dependent features
'EXTRACT_MEMORIES',
'VERIFICATION_AGENT',
'KAIROS_BRIEF',
'AWAY_SUMMARY',
'ULTRAPLAN',
// P2: daemon + remote control server
'DAEMON',
// ACP (Agent Client Protocol) agent mode
'ACP',
// PR-package restored features
'WORKFLOW_SCRIPTS',
'HISTORY_SNIP',
'CONTEXT_COLLAPSE',
'MONITOR_TOOL',
'FORK_SUBAGENT',
// 'UDS_INBOX',
'KAIROS',
'COORDINATOR_MODE',
'LAN_PIPES',
'BG_SESSIONS',
'TEMPLATES',
// 'REVIEW_ARTIFACT', // API 请求无响应,需进一步排查 schema 兼容性
// P3: poor mode (disable extract_memories + prompt_suggestion)
'POOR',
]
// Collect FEATURE_* env vars → Bun.build features
const envFeatures = Object.keys(process.env)
.filter(k => k.startsWith('FEATURE_'))

View File

@@ -6,7 +6,7 @@
"name": "claude-code-best",
"dependencies": {
"@agentclientprotocol/sdk": "^0.19.0",
"@claude-code-best/mcp-chrome-bridge": "^2.0.8",
"@claude-code-best/mcp-chrome-bridge": "^3.0.1",
"ws": "^8.20.0",
},
"devDependencies": {
@@ -195,14 +195,13 @@
},
"packages/acp-link": {
"name": "acp-link",
"version": "1.1.0",
"version": "2.0.0",
"bin": {
"acp-link": "dist/cli/bin.js",
"acp-manager": "dist/manager/bin.js",
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.19.0",
"@hono/node-server": "^1.13.8",
"@hono/node-server": "^2.0.0",
"@hono/node-ws": "^1.0.5",
"@stricli/auto-complete": "^1.2.4",
"@stricli/core": "^1.2.4",
@@ -570,7 +569,7 @@
"@claude-code-best/builtin-tools": ["@claude-code-best/builtin-tools@workspace:packages/builtin-tools"],
"@claude-code-best/mcp-chrome-bridge": ["@claude-code-best/mcp-chrome-bridge@2.0.8", "https://registry.npmmirror.com/@claude-code-best/mcp-chrome-bridge/-/mcp-chrome-bridge-2.0.8.tgz", { "dependencies": { "@fastify/cors": "^11.0.1", "@modelcontextprotocol/sdk": "^1.11.0", "chalk": "^5.4.1", "chrome-mcp-shared": "^1.0.2", "commander": "^13.1.0", "fastify": "^5.3.2", "is-admin": "^4.0.0", "pino": "^9.6.0", "uuid": "^11.1.0" }, "bin": { "mcp-chrome-bridge": "dist/cli.js", "mcp-chrome-stdio": "dist/mcp/mcp-server-stdio.js" } }, "sha512-f7J1e4PQ6qxXzdHwL7QRrMZ4lPfD/L1MWxWDbyHmHY7jaW2GL6WcArKpk/fApg3V/q0racqUWzXHQdpE/HJZqg=="],
"@claude-code-best/mcp-chrome-bridge": ["@claude-code-best/mcp-chrome-bridge@3.0.1", "", { "dependencies": { "@hono/node-server": "^1.19.13", "@modelcontextprotocol/sdk": "^1.11.0", "commander": "^13.1.0", "hono": "^4.12.12", "is-admin": "^4.0.0" }, "bin": { "mcp-chrome-bridge": "dist/cli.js", "mcp-chrome-stdio": "dist/mcp/mcp-server-stdio.js" } }, "sha512-ozeLHVOdckTUsWKJneJAL+CclrUlwVyBpfzFxgsrSL9f0LvjlJXE7+VcF5OmjDPwmZy6QNorvtg3/8NT2cIlzA=="],
"@claude-code-best/mcp-client": ["@claude-code-best/mcp-client@workspace:packages/mcp-client"],
@@ -636,22 +635,8 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "https://registry.npmmirror.com/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="],
"@fastify/cors": ["@fastify/cors@11.2.0", "https://registry.npmmirror.com/@fastify/cors/-/cors-11.2.0.tgz", { "dependencies": { "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw=="],
"@fastify/error": ["@fastify/error@4.2.0", "https://registry.npmmirror.com/@fastify/error/-/error-4.2.0.tgz", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
"@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "https://registry.npmmirror.com/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="],
"@fastify/forwarded": ["@fastify/forwarded@3.0.1", "https://registry.npmmirror.com/@fastify/forwarded/-/forwarded-3.0.1.tgz", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
"@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "https://registry.npmmirror.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
"@fastify/otel": ["@fastify/otel@0.18.0", "https://registry.npmmirror.com/@fastify/otel/-/otel-0.18.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.212.0", "@opentelemetry/semantic-conventions": "^1.28.0", "minimatch": "^10.2.4" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA=="],
"@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "https://registry.npmmirror.com/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="],
"@floating-ui/core": ["@floating-ui/core@1.7.5", "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
@@ -666,7 +651,7 @@
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
"@hono/node-server": ["@hono/node-server@1.19.13", "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.13.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ=="],
"@hono/node-server": ["@hono/node-server@2.0.0", "", { "peerDependencies": { "hono": "^4" } }, "sha512-n3GfHwwCvHCkGmOwKfxUPOlbfzuO64Sbc5XC4NGPIXxkuOnJrdgExdRKmHfF924r914WRJPT397GdqLvdYTeyQ=="],
"@hono/node-ws": ["@hono/node-ws@1.3.0", "https://registry.npmmirror.com/@hono/node-ws/-/node-ws-1.3.0.tgz", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.19.2", "hono": "^4.6.0" } }, "sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q=="],
@@ -1526,8 +1511,6 @@
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.12.tgz", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="],
"abstract-logging": ["abstract-logging@2.0.1", "https://registry.npmmirror.com/abstract-logging/-/abstract-logging-2.0.1.tgz", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
"accepts": ["accepts@2.0.0", "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
@@ -1568,8 +1551,6 @@
"auto-bind": ["auto-bind@5.0.1", "https://registry.npmmirror.com/auto-bind/-/auto-bind-5.0.1.tgz", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
"avvio": ["avvio@9.2.0", "https://registry.npmmirror.com/avvio/-/avvio-9.2.0.tgz", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ=="],
"axios": ["axios@1.15.0", "https://registry.npmmirror.com/axios/-/axios-1.15.0.tgz", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="],
"bail": ["bail@2.0.2", "https://registry.npmmirror.com/bail/-/bail-2.0.2.tgz", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
@@ -1636,8 +1617,6 @@
"chokidar": ["chokidar@5.0.0", "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"chrome-mcp-shared": ["chrome-mcp-shared@1.0.2", "https://registry.npmmirror.com/chrome-mcp-shared/-/chrome-mcp-shared-1.0.2.tgz", { "dependencies": { "@modelcontextprotocol/sdk": "^1.11.0", "zod": "^3.24.4" } }, "sha512-v+6HBmcgXrIfyVbkkrVgfFDzqOfDutI8yZM0yA8k7SiicqL1MfBoqnsOy5idYNvxyQymxCxXNuTmajn8xaGsgQ=="],
"cjs-module-lexer": ["cjs-module-lexer@2.2.0", "https://registry.npmmirror.com/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
@@ -1868,16 +1847,10 @@
"fast-copy": ["fast-copy@4.0.3", "https://registry.npmmirror.com/fast-copy/-/fast-copy-4.0.3.tgz", {}, "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw=="],
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "https://registry.npmmirror.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-json-stringify": ["fast-json-stringify@6.3.0", "https://registry.npmmirror.com/fast-json-stringify/-/fast-json-stringify-6.3.0.tgz", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA=="],
"fast-querystring": ["fast-querystring@1.1.2", "https://registry.npmmirror.com/fast-querystring/-/fast-querystring-1.1.2.tgz", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="],
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "https://registry.npmmirror.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
"fast-uri": ["fast-uri@3.1.0", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
@@ -1886,10 +1859,6 @@
"fast-xml-parser": ["fast-xml-parser@5.5.8", "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
"fastify": ["fastify@5.8.4", "https://registry.npmmirror.com/fastify/-/fastify-5.8.4.tgz", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ=="],
"fastify-plugin": ["fastify-plugin@5.1.0", "https://registry.npmmirror.com/fastify-plugin/-/fastify-plugin-5.1.0.tgz", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="],
"fastq": ["fastq@1.20.1", "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
"fd-package-json": ["fd-package-json@2.0.0", "https://registry.npmmirror.com/fd-package-json/-/fd-package-json-2.0.0.tgz", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="],
@@ -1906,8 +1875,6 @@
"finalhandler": ["finalhandler@2.1.1", "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"find-my-way": ["find-my-way@9.5.0", "https://registry.npmmirror.com/find-my-way/-/find-my-way-9.5.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ=="],
"find-up": ["find-up@4.1.0", "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"flora-colossus": ["flora-colossus@2.0.0", "https://registry.npmmirror.com/flora-colossus/-/flora-colossus-2.0.0.tgz", { "dependencies": { "debug": "^4.3.4", "fs-extra": "^10.1.0" } }, "sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA=="],
@@ -2106,8 +2073,6 @@
"json-schema": ["json-schema@0.4.0", "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "https://registry.npmmirror.com/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "https://registry.npmmirror.com/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
@@ -2138,8 +2103,6 @@
"layout-base": ["layout-base@1.0.2", "https://registry.npmmirror.com/layout-base/-/layout-base-1.0.2.tgz", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
"light-my-request": ["light-my-request@6.6.0", "https://registry.npmmirror.com/light-my-request/-/light-my-request-6.6.0.tgz", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="],
"lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
@@ -2564,14 +2527,10 @@
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"ret": ["ret@0.5.0", "https://registry.npmmirror.com/ret/-/ret-0.5.0.tgz", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="],
"retry": ["retry@0.12.0", "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
"reusify": ["reusify@1.1.0", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"robust-predicates": ["robust-predicates@3.0.3", "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.3.tgz", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
"rolldown": ["rolldown@1.0.0-rc.15", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.15.tgz", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="],
@@ -2590,8 +2549,6 @@
"safe-buffer": ["safe-buffer@5.2.1", "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-regex2": ["safe-regex2@5.1.0", "https://registry.npmmirror.com/safe-regex2/-/safe-regex2-5.1.0.tgz", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
@@ -2610,8 +2567,6 @@
"set-blocking": ["set-blocking@2.0.0", "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"setprototypeof": ["setprototypeof@1.2.0", "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"sharp": ["sharp@0.34.5", "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
@@ -2702,8 +2657,6 @@
"to-regex-range": ["to-regex-range@5.0.1", "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toad-cache": ["toad-cache@3.7.0", "https://registry.npmmirror.com/toad-cache/-/toad-cache-3.7.0.tgz", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="],
"toidentifier": ["toidentifier@1.0.1", "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"tr46": ["tr46@0.0.3", "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
@@ -3064,7 +3017,7 @@
"@claude-code-best/agent-tools/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@claude-code-best/mcp-chrome-bridge/pino": ["pino@9.14.0", "https://registry.npmmirror.com/pino/-/pino-9.14.0.tgz", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="],
"@claude-code-best/mcp-chrome-bridge/@hono/node-server": ["@hono/node-server@1.19.13", "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.13.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ=="],
"@claude-code-best/mcp-client/lru-cache": ["lru-cache@10.4.3", "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@@ -3076,16 +3029,18 @@
"@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation/-/instrumentation-0.212.0.tgz", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="],
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.3.0.tgz", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
"@grpc/proto-loader/yargs": ["yargs@17.7.2", "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"@hono/node-ws/@hono/node-server": ["@hono/node-server@1.19.13", "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.13.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ=="],
"@inquirer/core/@types/node": ["@types/node@22.19.17", "https://registry.npmmirror.com/@types/node/-/node-22.19.17.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
"@inquirer/core/strip-ansi": ["strip-ansi@6.0.1", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@1.19.13", "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.13.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ=="],
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "https://registry.npmmirror.com/@opentelemetry/core/-/core-2.6.1.tgz", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "https://registry.npmmirror.com/@opentelemetry/core/-/core-2.6.1.tgz", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
@@ -3338,8 +3293,6 @@
"cacache/lru-cache": ["lru-cache@11.3.3", "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.3.3.tgz", {}, "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ=="],
"chrome-mcp-shared/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"cli-highlight/chalk": ["chalk@4.1.2", "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"cli-highlight/highlight.js": ["highlight.js@10.7.3", "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
@@ -3362,8 +3315,6 @@
"external-editor/iconv-lite": ["iconv-lite@0.4.24", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"fastify/pino": ["pino@9.14.0", "https://registry.npmmirror.com/pino/-/pino-9.14.0.tgz", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="],
"form-data/mime-types": ["mime-types@2.1.35", "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
@@ -3382,10 +3333,6 @@
"katex/commander": ["commander@8.3.0", "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"light-my-request/cookie": ["cookie@1.1.1", "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"light-my-request/process-warning": ["process-warning@4.0.1", "https://registry.npmmirror.com/process-warning/-/process-warning-4.0.1.tgz", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"mermaid/marked": ["marked@16.4.2", "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
@@ -3634,10 +3581,6 @@
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@claude-code-best/mcp-chrome-bridge/pino/pino-abstract-transport": ["pino-abstract-transport@2.0.0", "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
"@claude-code-best/mcp-chrome-bridge/pino/thread-stream": ["thread-stream@3.1.0", "https://registry.npmmirror.com/thread-stream/-/thread-stream-3.1.0.tgz", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
"@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "https://registry.npmmirror.com/@opentelemetry/api-logs/-/api-logs-0.212.0.tgz", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="],
"@fastify/otel/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "https://registry.npmmirror.com/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="],
@@ -3720,10 +3663,6 @@
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
"fastify/pino/pino-abstract-transport": ["pino-abstract-transport@2.0.0", "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
"fastify/pino/thread-stream": ["thread-stream@3.1.0", "https://registry.npmmirror.com/thread-stream/-/thread-stream-3.1.0.tgz", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],

View File

@@ -1,6 +1,6 @@
{
"name": "claude-code-best",
"version": "1.5.0",
"version": "1.8.0",
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
"type": "module",
"author": "claude-code-best <claude-code-best@proton.me>",
@@ -58,11 +58,12 @@
"postinstall": "node scripts/run-parallel.mjs scripts/postinstall.cjs scripts/setup-chrome-mcp.mjs",
"docs:dev": "npx mintlify dev",
"typecheck": "tsc --noEmit",
"test:all": "bun run typecheck && bun test",
"rcs": "bun run scripts/rcs.ts"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.19.0",
"@claude-code-best/mcp-chrome-bridge": "^2.0.8",
"@claude-code-best/mcp-chrome-bridge": "^3.0.1",
"ws": "^8.20.0"
},
"devDependencies": {

View File

@@ -26,7 +26,7 @@
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.19.0",
"@hono/node-server": "^1.13.8",
"@hono/node-server": "^2.0.0",
"@hono/node-ws": "^1.0.5",
"@stricli/auto-complete": "^1.2.4",
"@stricli/core": "^1.2.4",

View File

@@ -1,3 +1,9 @@
import { createRequire } from 'node:module'
// createRequire works in both Bun and Node.js ESM contexts.
// Needed because this package is "type": "module" but uses require() for
// loading native .node addons — bare require is not available in Node.js ESM.
const nodeRequire = createRequire(import.meta.url)
type AudioCaptureNapi = {
startRecording(
@@ -41,7 +47,7 @@ function loadModule(): AudioCaptureNapi | null {
if (process.env.AUDIO_CAPTURE_NODE_PATH) {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
cachedModule = require(
cachedModule = nodeRequire(
process.env.AUDIO_CAPTURE_NODE_PATH,
) as AudioCaptureNapi
return cachedModule
@@ -63,7 +69,7 @@ function loadModule(): AudioCaptureNapi | null {
for (const p of fallbacks) {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
cachedModule = require(p) as AudioCaptureNapi
cachedModule = nodeRequire(p) as AudioCaptureNapi
return cachedModule
} catch {
// try next

View File

@@ -9,6 +9,9 @@ import type {
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import { queryModelWithStreaming } from 'src/services/api/claude.js'
import { createTrace, endTrace, isLangfuseEnabled } from 'src/services/langfuse/index.js'
import { getSessionId } from 'src/bootstrap/state.js'
import { getAPIProvider } from 'src/utils/model/providers.js'
import { createUserMessage } from 'src/utils/messages.js'
import { getMainLoopModel, getSmallFastModel } from 'src/utils/model/model.js'
import { jsonParse } from 'src/utils/slowOperations.js'
@@ -38,6 +41,15 @@ export class ApiSearchAdapter implements WebSearchAdapter {
const toolSchema = makeToolSchema({ allowedDomains, blockedDomains })
const useHaiku = getFeatureValue_CACHED_MAY_BE_STALE('tengu_plum_vx3', false)
const model = useHaiku ? getSmallFastModel() : getMainLoopModel()
const langfuseTrace = isLangfuseEnabled()
? createTrace({
sessionId: getSessionId(),
model,
provider: getAPIProvider(),
name: 'web-search-tool',
})
: null
const queryStream = queryModelWithStreaming({
messages: [userMessage],
@@ -58,7 +70,7 @@ export class ApiSearchAdapter implements WebSearchAdapter {
alwaysAskRules: {},
isBypassPermissionsModeAvailable: false,
}),
model: useHaiku ? getSmallFastModel() : getMainLoopModel(),
model,
toolChoice: useHaiku ? { type: 'tool' as const, name: 'web_search' } : undefined,
isNonInteractiveSession: false,
hasAppendSystemPrompt: false,
@@ -68,6 +80,7 @@ export class ApiSearchAdapter implements WebSearchAdapter {
mcpTools: [],
agentId: undefined,
effortValue: undefined,
langfuseTrace,
},
})
@@ -148,6 +161,8 @@ export class ApiSearchAdapter implements WebSearchAdapter {
}
}
endTrace(langfuseTrace)
// Extract SearchResult[] from content blocks
return extractSearchResults(allContentBlocks)
}

View File

@@ -17,10 +17,16 @@
* getSyntaxTheme always returns the default for the given Claude theme.
*/
import { createRequire } from 'node:module'
import { diffArrays } from 'diff'
import type * as hljsNamespace from 'highlight.js'
import { basename, extname } from 'path'
// createRequire works in both Bun and Node.js ESM contexts.
// Needed because this package is "type": "module" but uses require() for
// lazy loading — bare require is not available in Node.js ESM.
const nodeRequire = createRequire(import.meta.url)
// Lazy: defers loading highlight.js until first render. The full bundle
// registers 190+ language grammars at require time (~50MB, 100-200ms on
// macOS, several× that on Windows). With a top-level import, any caller
@@ -34,8 +40,7 @@ type HLJSApi = typeof hljsNamespace.default
let cachedHljs: HLJSApi | null = null
function hljs(): HLJSApi {
if (cachedHljs) return cachedHljs
// eslint-disable-next-line @typescript-eslint/no-require-imports
const mod = require('highlight.js')
const mod = nodeRequire('highlight.js')
// highlight.js uses `export =` (CJS). Under bun/ESM the interop wraps it
// in .default; under node CJS the module IS the API. Check at runtime.
cachedHljs = 'default' in mod && mod.default ? mod.default : mod

View File

@@ -1,3 +1,4 @@
import { readFileSync, unlinkSync } from 'node:fs'
import sharpModule from 'sharp'
export const sharp = sharpModule
@@ -62,13 +63,11 @@ return "${tmpPath}"
}
const file = Bun.file(tmpPath)
// Use synchronous read via Node compat
const fs = require('fs')
const buffer: Buffer = fs.readFileSync(tmpPath)
const buffer: Buffer = readFileSync(tmpPath)
// Clean up temp file
try {
fs.unlinkSync(tmpPath)
unlinkSync(tmpPath)
} catch {
// ignore cleanup errors
}

View File

@@ -16,3 +16,52 @@ export function getMacroDefines(): Record<string, string> {
"MACRO.VERSION_CHANGELOG": JSON.stringify(""),
};
}
/**
* Default feature flags enabled in both Bun.build and Vite builds.
* Additional features can be enabled via FEATURE_<NAME>=1 env vars.
*
* Used by:
* - build.ts (Bun.build)
* - scripts/vite-plugin-feature-flags.ts (Vite/Rollup)
* - scripts/dev.ts (bun run dev)
*/
export const DEFAULT_BUILD_FEATURES = [
'BUDDY', 'TRANSCRIPT_CLASSIFIER', 'BRIDGE_MODE',
'AGENT_TRIGGERS_REMOTE',
'CHICAGO_MCP',
'VOICE_MODE',
'SHOT_STATS',
'PROMPT_CACHE_BREAK_DETECTION',
'TOKEN_BUDGET',
// P0: local features
'AGENT_TRIGGERS',
'ULTRATHINK',
'BUILTIN_EXPLORE_PLAN_AGENTS',
'LODESTONE',
// P1: API-dependent features
'EXTRACT_MEMORIES',
'VERIFICATION_AGENT',
'KAIROS_BRIEF',
'AWAY_SUMMARY',
'ULTRAPLAN',
// P2: daemon + remote control server
'DAEMON',
// ACP (Agent Client Protocol) agent mode
'ACP',
// PR-package restored features
'WORKFLOW_SCRIPTS',
'HISTORY_SNIP',
'CONTEXT_COLLAPSE',
'MONITOR_TOOL',
'FORK_SUBAGENT',
// 'UDS_INBOX',
'KAIROS',
'COORDINATOR_MODE',
'LAN_PIPES',
'BG_SESSIONS',
'TEMPLATES',
// 'REVIEW_ARTIFACT', // API 请求无响应,需进一步排查 schema 兼容性
// P3: poor mode (disable extract_memories + prompt_suggestion)
'POOR',
] as const;

View File

@@ -6,7 +6,7 @@
*/
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { getMacroDefines } from "./defines.ts";
import { getMacroDefines, DEFAULT_BUILD_FEATURES } from "./defines.ts";
// Resolve project root from this script's location
const __filename = fileURLToPath(import.meta.url);
@@ -22,39 +22,7 @@ const defineArgs = Object.entries(defines).flatMap(([k, v]) => [
]);
// Bun --feature flags: enable feature() gates at runtime.
// Default features enabled in dev mode.
const DEFAULT_FEATURES = [
"BUDDY", "TRANSCRIPT_CLASSIFIER", "BRIDGE_MODE",
"AGENT_TRIGGERS_REMOTE", "CHICAGO_MCP", "VOICE_MODE",
"SHOT_STATS", "PROMPT_CACHE_BREAK_DETECTION", "TOKEN_BUDGET",
// P0: local features
"AGENT_TRIGGERS",
"ULTRATHINK",
"BUILTIN_EXPLORE_PLAN_AGENTS",
"LODESTONE",
// P1: API-dependent features
"EXTRACT_MEMORIES", "VERIFICATION_AGENT",
"KAIROS_BRIEF", "AWAY_SUMMARY", "ULTRAPLAN",
// P2: daemon + remote control server
"DAEMON",
// ACP (Agent Client Protocol) agent mode
"ACP",
// PR-package restored features
"WORKFLOW_SCRIPTS",
"HISTORY_SNIP",
"CONTEXT_COLLAPSE",
"MONITOR_TOOL",
"FORK_SUBAGENT",
"UDS_INBOX",
"KAIROS",
"COORDINATOR_MODE",
"LAN_PIPES",
"BG_SESSIONS",
"TEMPLATES",
// "REVIEW_ARTIFACT", // API 请求无响应,需进一步排查 schema 兼容性
// P3: poor mode (disable extract_memories + prompt_suggestion)
"POOR",
];
// Uses the shared DEFAULT_BUILD_FEATURES list from defines.ts.
// Any env var matching FEATURE_<NAME>=1 will also enable that feature.
// e.g. FEATURE_PROACTIVE=1 bun run dev
@@ -62,7 +30,7 @@ const envFeatures = Object.entries(process.env)
.filter(([k]) => k.startsWith("FEATURE_"))
.map(([k]) => k.replace("FEATURE_", ""));
const allFeatures = [...new Set([...DEFAULT_FEATURES, ...envFeatures])];
const allFeatures = [...new Set([...DEFAULT_BUILD_FEATURES, ...envFeatures])];
const featureArgs = allFeatures.flatMap((name) => ["--feature", name]);
// If BUN_INSPECT is set, pass --inspect-wait to the child process

View File

@@ -1,41 +1,5 @@
import type { Plugin } from "rollup";
/**
* Default features that match the official CLI build.
* Additional features can be enabled via FEATURE_<NAME>=1 env vars.
*/
const DEFAULT_BUILD_FEATURES = [
"AGENT_TRIGGERS_REMOTE",
"CHICAGO_MCP",
"VOICE_MODE",
"SHOT_STATS",
"PROMPT_CACHE_BREAK_DETECTION",
"TOKEN_BUDGET",
// P0: local features
"AGENT_TRIGGERS",
"ULTRATHINK",
"BUILTIN_EXPLORE_PLAN_AGENTS",
"LODESTONE",
// P1: API-dependent features
"EXTRACT_MEMORIES",
"VERIFICATION_AGENT",
"KAIROS_BRIEF",
"AWAY_SUMMARY",
"ULTRAPLAN",
// P2: daemon + remote control server
"DAEMON",
// PR-package restored features
"WORKFLOW_SCRIPTS",
"HISTORY_SNIP",
"CONTEXT_COLLAPSE",
"MONITOR_TOOL",
"FORK_SUBAGENT",
"KAIROS",
"COORDINATOR_MODE",
"LAN_PIPES",
// P3: poor mode
"POOR",
];
import { DEFAULT_BUILD_FEATURES } from "./defines.ts";
/**
* Collect enabled feature flags from defaults + env vars.

View File

@@ -146,7 +146,7 @@ export const getEmptyToolPermissionContext: () => ToolPermissionContext =
alwaysAllowRules: {},
alwaysDenyRules: {},
alwaysAskRules: {},
isBypassPermissionsModeAvailable: false,
isBypassPermissionsModeAvailable: true,
})
export type CompactProgressEvent =
@@ -277,6 +277,8 @@ export type ToolUseContext = {
criticalSystemReminder_EXPERIMENTAL?: string
/** Langfuse root trace span for this query turn. Passed down to tool execution for observability. */
langfuseTrace?: LangfuseSpan | null
/** Langfuse root trace span for the outer/main agent trace. Used when subagents need to nest observations under the parent agent trace. */
langfuseRootTrace?: LangfuseSpan | null
/** Langfuse batch span wrapping a concurrent tool group. When set, tool observations are nested under it. */
langfuseBatchSpan?: LangfuseSpan | null
/** When true, preserve toolUseResult on messages even for subagents.

View File

@@ -166,9 +166,9 @@ describe('getEmptyToolPermissionContext', () => {
expect(ctx.alwaysAskRules).toEqual({})
})
test('returns isBypassPermissionsModeAvailable as false', () => {
test('returns isBypassPermissionsModeAvailable as true', () => {
const ctx = getEmptyToolPermissionContext()
expect(ctx.isBypassPermissionsModeAvailable).toBe(false)
expect(ctx.isBypassPermissionsModeAvailable).toBe(true)
})
})

View File

@@ -6,6 +6,7 @@
import { errorMessage } from '../../utils/errors.js'
import {
getMainLoopModel,
getSmallFastModel,
parseUserSpecifiedModel,
} from '../../utils/model/model.js'
import {
@@ -14,6 +15,7 @@ import {
getDefaultExternalAutoModeRules,
} from '../../utils/permissions/yoloClassifier.js'
import { getAutoModeConfig } from '../../utils/settings/settings.js'
import { isPoorModeActive } from '../../commands/poor/poorMode.js'
import { sideQuery } from '../../utils/sideQuery.js'
import { jsonStringify } from '../../utils/slowOperations.js'
@@ -90,7 +92,9 @@ export async function autoModeCritiqueHandler(options: {
const model = options.model
? parseUserSpecifiedModel(options.model)
: getMainLoopModel()
: isPoorModeActive()
? getSmallFastModel()
: getMainLoopModel()
const defaults = getDefaultExternalAutoModeRules()
const classifierPrompt = buildDefaultExternalSystemPrompt()

166
src/cli/updateCCB.ts Normal file
View File

@@ -0,0 +1,166 @@
/**
* `ccb update` — Check and install the latest version of claude-code-best.
*
* Detection strategy:
* 1. If `bun` is available and the current installation was done via bun → use `bun update -g`
* 2. Otherwise → use `npm install -g`
*/
import chalk from 'chalk'
import { execSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { homedir } from 'node:os'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { logForDebugging } from '../utils/debug.js'
import { execFileNoThrowWithCwd } from '../utils/execFileNoThrow.js'
import { gracefulShutdown } from '../utils/gracefulShutdown.js'
import { writeToStdout } from '../utils/process.js'
const PACKAGE_NAME = 'claude-code-best'
function getCurrentVersion(): string {
// Read version from the nearest package.json (walks up from this file)
try {
const __dirname = dirname(fileURLToPath(import.meta.url))
// In dev: src/cli/updateCCB.ts → ../../package.json
// In build: dist/chunks/xxx.js → ../../package.json (may not exist)
const pkgPath = join(__dirname, '..', '..', 'package.json')
if (existsSync(pkgPath)) {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
if (pkg.version) return pkg.version
}
} catch {
// fallback
}
return MACRO.VERSION
}
function isCommandAvailable(cmd: string): boolean {
try {
execSync(`which ${cmd} 2>/dev/null`, { stdio: 'pipe' })
return true
} catch {
return false
}
}
/**
* Detect whether the current installation was done via bun.
* Checks if the binary path contains "bun" or if bun's global install dir has our package.
*/
function isBunInstallation(): boolean {
// Check if the running binary is under bun's global install path
const execPath = process.execPath
if (execPath.includes('bun')) {
return true
}
// Check bun's global install directory
const bunGlobalDir = join(homedir(), '.bun', 'install', 'global')
if (existsSync(join(bunGlobalDir, 'node_modules', PACKAGE_NAME))) {
return true
}
return false
}
/**
* Get the latest version from npm registry.
*/
async function getLatestVersion(): Promise<string | null> {
const result = await execFileNoThrowWithCwd(
'npm',
['view', `${PACKAGE_NAME}@latest`, 'version', '--prefer-online'],
{ abortSignal: AbortSignal.timeout(10_000), cwd: homedir() },
)
if (result.code !== 0) {
logForDebugging(`npm view failed: ${result.stderr}`)
return null
}
return result.stdout.trim()
}
/**
* Compare two semver strings. Returns true if a >= b.
*/
function gte(a: string, b: string): boolean {
const parseVer = (v: string) => v.replace(/^\D/, '').split('.').map(Number)
const pa = parseVer(a)
const pb = parseVer(b)
for (let i = 0; i < 3; i++) {
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false
}
return true
}
export async function updateCCB(): Promise<void> {
const currentVersion = getCurrentVersion()
writeToStdout(`Current version: ${currentVersion}\n`)
// Determine package manager
const hasBun = isCommandAvailable('bun')
const useBun = isBunInstallation()
const pkgManager = useBun && hasBun ? 'bun' : 'npm'
writeToStdout(`Package manager: ${pkgManager}\n`)
writeToStdout('Checking for updates...\n')
// Get latest version
const latestVersion = await getLatestVersion()
if (!latestVersion) {
process.stderr.write(chalk.red('Failed to check for updates') + '\n')
process.stderr.write('Unable to fetch latest version from npm registry.\n')
await gracefulShutdown(1)
return
}
// Already up to date?
if (latestVersion === currentVersion || gte(currentVersion, latestVersion)) {
writeToStdout(chalk.green(`ccb is up to date (${currentVersion})`) + '\n')
await gracefulShutdown(0)
return
}
writeToStdout(
`New version available: ${latestVersion} (current: ${currentVersion})\n`,
)
writeToStdout(`Installing update via ${pkgManager}...\n`)
try {
if (pkgManager === 'bun') {
execSync(`bun update -g ${PACKAGE_NAME}`, {
stdio: 'inherit',
cwd: homedir(),
timeout: 120_000,
})
} else {
execSync(`npm install -g ${PACKAGE_NAME}@latest`, {
stdio: 'inherit',
cwd: homedir(),
timeout: 120_000,
})
}
writeToStdout(
chalk.green(
`Successfully updated from ${currentVersion} to ${latestVersion}`,
) + '\n',
)
} catch (error) {
process.stderr.write(chalk.red('Update failed') + '\n')
process.stderr.write(`${error}\n`)
process.stderr.write('\n')
process.stderr.write('Try manually updating with:\n')
if (pkgManager === 'bun') {
process.stderr.write(chalk.bold(` bun update -g ${PACKAGE_NAME}`) + '\n')
} else {
process.stderr.write(
chalk.bold(` npm install -g ${PACKAGE_NAME}@latest`) + '\n',
)
}
await gracefulShutdown(1)
}
await gracefulShutdown(0)
}

View File

@@ -18,9 +18,7 @@ import type { LocalJSXCommandOnDone } from '../../types/command.js'
import { stripSignatureBlocks } from '../../utils/messages.js'
import {
checkAndDisableAutoModeIfNeeded,
checkAndDisableBypassPermissionsIfNeeded,
resetAutoModeGateCheck,
resetBypassPermissionsCheck,
} from '../../utils/permissions/bypassPermissionsKillswitch.js'
import { resetUserCache } from '../../utils/user.js'
@@ -54,20 +52,13 @@ export async function call(
// Enroll as a trusted device for Remote Control (10-min fresh-session window)
void enrollTrustedDevice()
// Reset killswitch gate checks and re-run with new org
resetBypassPermissionsCheck()
resetAutoModeGateCheck()
const appState = context.getAppState()
void checkAndDisableBypassPermissionsIfNeeded(
void checkAndDisableAutoModeIfNeeded(
appState.toolPermissionContext,
context.setAppState,
appState.fastMode,
)
if (feature('TRANSCRIPT_CLASSIFIER')) {
resetAutoModeGateCheck()
void checkAndDisableAutoModeIfNeeded(
appState.toolPermissionContext,
context.setAppState,
appState.fastMode,
)
}
// Increment authVersion to trigger re-fetching of auth-dependent data in hooks (e.g., MCP servers)
context.setAppState(prev => ({
...prev,

View File

@@ -151,16 +151,14 @@ import {
isOpus1mMergeEnabled,
modelDisplayString,
} from '../../utils/model/model.js'
import { setAutoModeActive } from '../../utils/permissions/autoModeState.js'
import {
cyclePermissionMode,
getNextPermissionMode,
} from '../../utils/permissions/getNextPermissionMode.js'
import { transitionPermissionMode } from '../../utils/permissions/permissionSetup.js'
import { getPlatform } from '../../utils/platform.js'
import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js'
import { editPromptInEditor } from '../../utils/promptEditor.js'
import { hasAutoModeOptIn } from '../../utils/settings/settings.js'
// hasAutoModeOptIn removed — auto mode is available to all users
import { findBtwTriggerPositions } from '../../utils/sideQuestion.js'
import { findSlashCommandPositions } from '../../utils/suggestions/commandSuggestions.js'
import {
@@ -187,7 +185,7 @@ import {
findUltraplanTriggerPositions,
findUltrareviewTriggerPositions,
} from '../../utils/ultraplan/keyword.js'
import { AutoModeOptInDialog } from '../AutoModeOptInDialog.js'
// AutoModeOptInDialog removed — auto mode is available to all users
import { BridgeDialog } from '../BridgeDialog.js'
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
import {
@@ -571,10 +569,6 @@ function PromptInput({
const [showHistoryPicker, setShowHistoryPicker] = useState(false)
const [showFastModePicker, setShowFastModePicker] = useState(false)
const [showThinkingToggle, setShowThinkingToggle] = useState(false)
const [showAutoModeOptIn, setShowAutoModeOptIn] = useState(false)
const [previousModeBeforeAuto, setPreviousModeBeforeAuto] =
useState<PermissionMode | null>(null)
const autoModeOptInTimeoutRef = useRef<NodeJS.Timeout | null>(null)
// Check if cursor is on the first line of input
const isCursorOnFirstLine = useMemo(() => {
@@ -1883,86 +1877,11 @@ function PromptInput({
// Compute the next mode without triggering side effects first
logForDebugging(
`[auto-mode] handleCycleMode: currentMode=${toolPermissionContext.mode} isAutoModeAvailable=${toolPermissionContext.isAutoModeAvailable} showAutoModeOptIn=${showAutoModeOptIn} timeoutPending=${!!autoModeOptInTimeoutRef.current}`,
`[auto-mode] handleCycleMode: currentMode=${toolPermissionContext.mode}`,
)
const nextMode = getNextPermissionMode(toolPermissionContext, teamContext)
// Check if user is entering auto mode for the first time. Gated on the
// persistent settings flag (hasAutoModeOptIn) rather than the broader
// hasAutoModeOptInAnySource so that --enable-auto-mode users still see
// the warning dialog once — the CLI flag should grant carousel access,
// not bypass the safety text.
let isEnteringAutoModeFirstTime = false
if (feature('TRANSCRIPT_CLASSIFIER')) {
isEnteringAutoModeFirstTime =
nextMode === 'auto' &&
toolPermissionContext.mode !== 'auto' &&
!hasAutoModeOptIn() &&
!viewingAgentTaskId // Only show for primary agent, not subagents
}
if (feature('TRANSCRIPT_CLASSIFIER')) {
if (isEnteringAutoModeFirstTime) {
// Store previous mode so we can revert if user declines
setPreviousModeBeforeAuto(toolPermissionContext.mode)
// Only update the UI mode label — do NOT call transitionPermissionMode
// or cyclePermissionMode yet; we haven't confirmed with the user.
setAppState(prev => ({
...prev,
toolPermissionContext: {
...prev.toolPermissionContext,
mode: 'auto',
},
}))
setToolPermissionContext({
...toolPermissionContext,
mode: 'auto',
})
// Show opt-in dialog after 400ms debounce
if (autoModeOptInTimeoutRef.current) {
clearTimeout(autoModeOptInTimeoutRef.current)
}
autoModeOptInTimeoutRef.current = setTimeout(
(setShowAutoModeOptIn, autoModeOptInTimeoutRef) => {
setShowAutoModeOptIn(true)
autoModeOptInTimeoutRef.current = null
},
400,
setShowAutoModeOptIn,
autoModeOptInTimeoutRef,
)
if (helpOpen) {
setHelpOpen(false)
}
return
}
}
// Dismiss auto mode opt-in dialog if showing or pending (user is cycling away).
// Do NOT revert to previousModeBeforeAuto here — shift+tab means "advance the
// carousel", not "decline". Reverting causes a ping-pong loop: auto reverts to
// the prior mode, whose next mode is auto again, forever.
// The dialog's own decline button (handleAutoModeOptInDecline) handles revert.
if (feature('TRANSCRIPT_CLASSIFIER')) {
if (showAutoModeOptIn || autoModeOptInTimeoutRef.current) {
if (showAutoModeOptIn) {
logEvent('tengu_auto_mode_opt_in_dialog_decline', {})
}
setShowAutoModeOptIn(false)
if (autoModeOptInTimeoutRef.current) {
clearTimeout(autoModeOptInTimeoutRef.current)
autoModeOptInTimeoutRef.current = null
}
setPreviousModeBeforeAuto(null)
// Fall through — mode is 'auto', cyclePermissionMode below goes to 'default'.
}
}
// Now that we know this is NOT the first-time auto mode path,
// call cyclePermissionMode to apply side effects (e.g. strip
// Call cyclePermissionMode to apply side effects (e.g. strip
// dangerous permissions, activate classifier)
const { context: preparedContext } = cyclePermissionMode(
toolPermissionContext,
@@ -2007,91 +1926,10 @@ function PromptInput({
}, [
toolPermissionContext,
teamContext,
viewingAgentTaskId,
viewedTeammate,
setAppState,
setToolPermissionContext,
helpOpen,
showAutoModeOptIn,
])
// Handler for auto mode opt-in dialog acceptance
const handleAutoModeOptInAccept = useCallback(() => {
if (feature('TRANSCRIPT_CLASSIFIER')) {
setShowAutoModeOptIn(false)
setPreviousModeBeforeAuto(null)
// Now that the user accepted, apply the full transition: activate the
// auto mode backend (classifier, beta headers) and strip dangerous
// permissions (e.g. Bash(*) always-allow rules).
const strippedContext = transitionPermissionMode(
previousModeBeforeAuto ?? toolPermissionContext.mode,
'auto',
toolPermissionContext,
)
setAppState(prev => ({
...prev,
toolPermissionContext: {
...strippedContext,
mode: 'auto',
},
}))
setToolPermissionContext({
...strippedContext,
mode: 'auto',
})
// Close help tips if they're open when auto mode is enabled
if (helpOpen) {
setHelpOpen(false)
}
}
}, [
helpOpen,
setHelpOpen,
previousModeBeforeAuto,
toolPermissionContext,
setAppState,
setToolPermissionContext,
])
// Handler for auto mode opt-in dialog decline
const handleAutoModeOptInDecline = useCallback(() => {
if (feature('TRANSCRIPT_CLASSIFIER')) {
logForDebugging(
`[auto-mode] handleAutoModeOptInDecline: reverting to ${previousModeBeforeAuto}, setting isAutoModeAvailable=false`,
)
setShowAutoModeOptIn(false)
if (autoModeOptInTimeoutRef.current) {
clearTimeout(autoModeOptInTimeoutRef.current)
autoModeOptInTimeoutRef.current = null
}
// Revert to previous mode and remove auto from the carousel
// for the rest of this session
if (previousModeBeforeAuto) {
setAutoModeActive(false)
setAppState(prev => ({
...prev,
toolPermissionContext: {
...prev.toolPermissionContext,
mode: previousModeBeforeAuto,
isAutoModeAvailable: false,
},
}))
setToolPermissionContext({
...toolPermissionContext,
mode: previousModeBeforeAuto,
isAutoModeAvailable: false,
})
setPreviousModeBeforeAuto(null)
}
}
}, [
previousModeBeforeAuto,
toolPermissionContext,
setAppState,
setToolPermissionContext,
])
// Handler for chat:imagePaste - paste image from clipboard
@@ -2758,20 +2596,7 @@ function PromptInput({
// Portal dialog to DialogOverlay in fullscreen so it escapes the bottom
// slot's overflowY:hidden clip (same pattern as SuggestionsOverlay).
// Must be called before early returns below to satisfy rules-of-hooks.
// Memoized so the portal useEffect doesn't churn on every PromptInput render.
const autoModeOptInDialog = useMemo(
() =>
feature('TRANSCRIPT_CLASSIFIER') && showAutoModeOptIn ? (
<AutoModeOptInDialog
onAccept={handleAutoModeOptInAccept}
onDecline={handleAutoModeOptInDecline}
/>
) : null,
[showAutoModeOptIn, handleAutoModeOptInAccept, handleAutoModeOptInDecline],
)
useSetPromptOverlayDialog(
isFullscreenEnvEnabled() ? autoModeOptInDialog : null,
)
useSetPromptOverlayDialog(null)
if (showBashesDialog) {
return (
@@ -3077,7 +2902,6 @@ function PromptInput({
isFullscreenEnvEnabled() ? handleOpenTasksDialog : undefined
}
/>
{isFullscreenEnvEnabled() ? null : autoModeOptInDialog}
{isFullscreenEnvEnabled() ? (
// position=absolute takes zero layout height so the spinner
// doesn't shift when a notification appears/disappears. Yoga
@@ -3098,7 +2922,7 @@ function PromptInput({
<Box
position="absolute"
marginTop={briefOwnsGap ? -2 : -1}
height={suggestions.length === 0 && !showAutoModeOptIn ? 1 : 0}
height={suggestions.length === 0 ? 1 : 0}
width="100%"
paddingLeft={2}
paddingRight={1}

View File

@@ -14,6 +14,9 @@ import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../../services/analytics/index.js'
import { createTrace, endTrace, isLangfuseEnabled } from '../../services/langfuse/index.js'
import { getSessionId } from '../../bootstrap/state.js'
import { getAPIProvider } from '../../utils/model/providers.js'
import { jsonParse } from '../../utils/slowOperations.js'
import { asSystemPrompt } from '../../utils/systemPromptType.js'
@@ -146,6 +149,15 @@ export async function generateAgent(
? AGENT_CREATION_SYSTEM_PROMPT + AGENT_MEMORY_INSTRUCTIONS
: AGENT_CREATION_SYSTEM_PROMPT
const langfuseTrace = isLangfuseEnabled()
? createTrace({
sessionId: getSessionId(),
model,
provider: getAPIProvider(),
name: 'agent-creation',
})
: null
const response = await queryModelWithoutStreaming({
messages: normalizeMessagesForAPI(messagesWithContext),
systemPrompt: asSystemPrompt([systemPrompt]),
@@ -161,9 +173,12 @@ export async function generateAgent(
hasAppendSystemPrompt: false,
querySource: 'agent_creation',
mcpTools: [],
langfuseTrace,
},
})
endTrace(langfuseTrace)
const textBlocks = (Array.isArray(response.message.content) ? response.message.content : []).filter(
(block): block is ContentBlock & { type: 'text' } => block.type === 'text',
)

View File

@@ -52,7 +52,6 @@ import type { PermissionMode } from './utils/permissions/PermissionMode.js'
import { getBaseRenderOptions } from './utils/renderOptions.js'
import { getSettingsWithAllErrors } from './utils/settings/allErrors.js'
import {
hasAutoModeOptIn,
hasSkipDangerousModePermissionPrompt,
} from './utils/settings/settings.js'
@@ -309,25 +308,6 @@ export async function showSetupScreens(
))
}
if (feature('TRANSCRIPT_CLASSIFIER')) {
// Only show the opt-in dialog if auto mode actually resolved — if the
// gate denied it (org not allowlisted, settings disabled), showing
// consent for an unavailable feature is pointless. The
// verifyAutoModeGateAccess notification will explain why instead.
if (permissionMode === 'auto' && !hasAutoModeOptIn()) {
const { AutoModeOptInDialog } = await import(
'./components/AutoModeOptInDialog.js'
)
await showSetupDialog(root, done => (
<AutoModeOptInDialog
onAccept={done}
onDecline={() => gracefulShutdownSync(1)}
declineExits
/>
))
}
}
// --dangerously-load-development-channels confirmation. On accept, append
// dev channels to any --channels list already set in main.tsx. Org policy
// is NOT bypassed — gateChannelServer() still runs; this flag only exists

View File

@@ -242,7 +242,6 @@ import {
import { ensureModelStringsInitialized } from "./utils/model/modelStrings.js";
import { PERMISSION_MODES } from "./utils/permissions/PermissionMode.js";
import {
checkAndDisableBypassPermissions,
getAutoModeEnabledStateIfCached,
initializeToolPermissionContext,
initialPermissionModeFromCLI,
@@ -3910,19 +3909,7 @@ async function run(): Promise<CommanderCommand> {
onChangeAppState,
);
// Check if bypassPermissions should be disabled based on Statsig gate
// This runs in parallel to the code below, to avoid blocking the main loop.
if (
toolPermissionContext.mode === "bypassPermissions" ||
allowDangerouslySkipPermissions
) {
void checkAndDisableBypassPermissions(
toolPermissionContext,
);
}
// Async check of auto mode gate — corrects state and disables auto if needed.
// Gated on TRANSCRIPT_CLASSIFIER (not USER_TYPE) so GrowthBook kill switch runs for external builds too.
if (feature("TRANSCRIPT_CLASSIFIER")) {
void verifyAutoModeGateAccess(
toolPermissionContext,
@@ -6564,6 +6551,15 @@ async function run(): Promise<CommanderCommand> {
},
);
// claude update — update ccb to the latest version via npm or bun
program
.command("update")
.description("Update claude-code-best (ccb) to the latest version")
.action(async () => {
const { updateCCB } = await import("./cli/updateCCB.js");
await updateCCB();
});
// ant-only commands
if (process.env.USER_TYPE === "ant") {
const validateLogId = (value: string) => {

View File

@@ -3,6 +3,7 @@ import { logForDebugging } from '../utils/debug.js'
import { errorMessage } from '../utils/errors.js'
import { getDefaultSonnetModel } from '../utils/model/model.js'
import { sideQuery } from '../utils/sideQuery.js'
import type { LangfuseSpan } from '../services/langfuse/index.js'
import { jsonParse } from '../utils/slowOperations.js'
import {
formatMemoryManifest,
@@ -42,6 +43,7 @@ export async function findRelevantMemories(
signal: AbortSignal,
recentTools: readonly string[] = [],
alreadySurfaced: ReadonlySet<string> = new Set(),
parentSpan?: LangfuseSpan | null,
): Promise<RelevantMemory[]> {
const memories = (await scanMemoryFiles(memoryDir, signal)).filter(
m => !alreadySurfaced.has(m.filePath),
@@ -55,6 +57,7 @@ export async function findRelevantMemories(
memories,
signal,
recentTools,
parentSpan,
)
const byFilename = new Map(memories.map(m => [m.filename, m]))
const selected = selectedFilenames
@@ -79,6 +82,7 @@ async function selectRelevantMemories(
memories: MemoryHeader[],
signal: AbortSignal,
recentTools: readonly string[],
parentSpan?: LangfuseSpan | null,
): Promise<string[]> {
const validFilenames = new Set(memories.map(m => m.filename))
@@ -119,6 +123,8 @@ async function selectRelevantMemories(
},
signal,
querySource: 'memdir_relevance',
optional: true,
parentSpan,
})
const textBlock = result.content.find(block => block.type === 'text')

View File

@@ -235,6 +235,9 @@ export async function* query(
// When called as a sub-agent, langfuseTrace is already set by runAgent()
// — reuse it instead of creating an independent trace.
const ownsTrace = !params.toolUseContext.langfuseTrace
logForDebugging(
`[query] ownsTrace=${ownsTrace} incoming langfuseTrace=${params.toolUseContext.langfuseTrace ? 'present' : 'null/undefined'} isLangfuseEnabled=${isLangfuseEnabled()}`,
)
const langfuseTrace = params.toolUseContext.langfuseTrace
?? (isLangfuseEnabled()
? createTrace({

View File

@@ -422,9 +422,7 @@ import { usePromptsFromClaudeInChrome } from 'src/hooks/usePromptsFromClaudeInCh
import { getTipToShowOnSpinner, recordShownTip } from 'src/services/tips/tipScheduler.js';
import type { Theme } from 'src/utils/theme.js';
import {
checkAndDisableBypassPermissionsIfNeeded,
checkAndDisableAutoModeIfNeeded,
useKickOffCheckAndDisableBypassPermissionsIfNeeded,
useKickOffCheckAndDisableAutoModeIfNeeded,
} from 'src/utils/permissions/bypassPermissionsKillswitch.js';
import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js';
@@ -434,7 +432,6 @@ import { SandboxPermissionRequest } from 'src/components/permissions/SandboxPerm
import { SandboxViolationExpandedView } from 'src/components/SandboxViolationExpandedView.js';
import { useSettingsErrors } from 'src/hooks/notifs/useSettingsErrors.js';
import { useMcpConnectivityStatus } from 'src/hooks/notifs/useMcpConnectivityStatus.js';
import { useAutoModeUnavailableNotification } from 'src/hooks/notifs/useAutoModeUnavailableNotification.js';
import { AUTO_MODE_DESCRIPTION } from 'src/components/AutoModeOptInDialog.js';
import { useLspInitializationNotification } from 'src/hooks/notifs/useLspInitializationNotification.js';
import { useLspPluginRecommendation } from 'src/hooks/useLspPluginRecommendation.js';
@@ -948,7 +945,6 @@ export function REPL({
[toolPermissionContext, proactiveActive, isBriefOnly],
);
useKickOffCheckAndDisableBypassPermissionsIfNeeded();
useKickOffCheckAndDisableAutoModeIfNeeded();
const [dynamicMcpConfig, setDynamicMcpConfig] = useState<Record<string, ScopedMcpServerConfig> | undefined>(
@@ -1006,7 +1002,6 @@ export function REPL({
useCanSwitchToExistingSubscription();
useIDEStatusIndicator({ ideSelection, mcpClients, ideInstallationStatus });
useMcpConnectivityStatus({ mcpClients });
useAutoModeUnavailableNotification();
usePluginInstallationStatus();
usePluginAutoupdateNotification();
useSettingsErrors();
@@ -3314,8 +3309,8 @@ export function REPL({
queryCheckpoint('query_context_loading_start');
const [, , defaultSystemPrompt, baseUserContext, systemContext] = await Promise.all([
// IMPORTANT: do this after setMessages() above, to avoid UI jank
checkAndDisableBypassPermissionsIfNeeded(toolPermissionContext, setAppState),
// Gated on TRANSCRIPT_CLASSIFIER so GrowthBook kill switch runs wherever auto mode is built in
undefined,
// Fast-mode circuit breaker check
feature('TRANSCRIPT_CLASSIFIER')
? checkAndDisableAutoModeIfNeeded(toolPermissionContext, setAppState, store.getState().fastMode)
: undefined,

View File

@@ -42,7 +42,7 @@ const mockGetDefaultAppState = mock(() => ({
alwaysAllowRules: { user: [], project: [], local: [] },
alwaysDenyRules: { user: [], project: [], local: [] },
alwaysAskRules: { user: [], project: [], local: [] },
isBypassPermissionsModeAvailable: false,
isBypassPermissionsModeAvailable: true,
},
fastMode: false,
settings: {},
@@ -627,6 +627,23 @@ describe('AcpAgent', () => {
agent.setSessionMode({ sessionId: 'ghost', modeId: 'auto' } as any),
).rejects.toThrow('Session not found')
})
test('availableModes includes bypassPermissions when not root', async () => {
const agent = new AcpAgent(makeConn())
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
const session = agent.sessions.get(sessionId)
const modeIds = session?.modes.availableModes.map((m: any) => m.id)
expect(modeIds).toContain('bypassPermissions')
})
test('can switch to bypassPermissions mode', async () => {
const agent = new AcpAgent(makeConn())
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
await agent.setSessionMode({ sessionId, modeId: 'bypassPermissions' } as any)
const session = agent.sessions.get(sessionId)
expect(session?.modes.currentModeId).toBe('bypassPermissions')
expect(session?.appState.toolPermissionContext.mode).toBe('bypassPermissions')
})
})
describe('setSessionConfigOption', () => {

View File

@@ -519,12 +519,15 @@ export class AcpAgent implements Agent {
const queryEngine = new QueryEngine(engineConfig)
// Build modes
// Build modes — bypassPermissions only available when not running as root (or in sandbox)
const availableModes = [
{ id: 'auto', name: 'Auto', description: 'Use a model classifier to approve/deny permission prompts.' },
{ id: 'default', name: 'Default', description: 'Standard behavior, prompts for dangerous operations' },
{ id: 'acceptEdits', name: 'Accept Edits', description: 'Auto-accept file edit operations' },
{ id: 'plan', name: 'Plan Mode', description: 'Planning mode, no actual tool execution' },
{ id: 'auto', name: 'Auto', description: 'Use a model classifier to approve/deny permission prompts.' },
...(isBypassAvailable
? [{ id: 'bypassPermissions' as const, name: 'Bypass Permissions', description: 'Skip all permission checks' }]
: []),
{ id: 'dontAsk', name: "Don't Ask", description: "Don't prompt for permissions, deny if not pre-approved" },
]

View File

@@ -10,6 +10,9 @@ import { getSmallFastModel } from '../utils/model/model.js'
import { asSystemPrompt } from '../utils/systemPromptType.js'
import { getResolvedLanguage } from '../utils/language.js'
import { queryModelWithoutStreaming } from './api/claude.js'
import { createTrace, endTrace, isLangfuseEnabled } from './langfuse/index.js'
import { getSessionId } from '../bootstrap/state.js'
import { getAPIProvider } from '../utils/model/providers.js'
import { getSessionMemoryContent } from './SessionMemory/sessionMemoryUtils.js'
// Recap only needs recent context — truncate to avoid "prompt too long" on
@@ -42,6 +45,16 @@ export async function generateAwaySummary(
return null
}
const model = getSmallFastModel()
const langfuseTrace = isLangfuseEnabled()
? createTrace({
sessionId: getSessionId(),
model,
provider: getAPIProvider(),
name: 'away-summary',
})
: null
try {
const memory = await getSessionMemoryContent()
const recent = messages.slice(-RECENT_MESSAGE_WINDOW)
@@ -54,7 +67,7 @@ export async function generateAwaySummary(
signal,
options: {
getToolPermissionContext: async () => getEmptyToolPermissionContext(),
model: getSmallFastModel(),
model,
toolChoice: undefined,
isNonInteractiveSession: false,
hasAppendSystemPrompt: false,
@@ -62,6 +75,7 @@ export async function generateAwaySummary(
querySource: 'away_summary',
mcpTools: [],
skipCacheWrite: true,
langfuseTrace,
},
})
@@ -69,14 +83,17 @@ export async function generateAwaySummary(
logForDebugging(
`[awaySummary] API error: ${getAssistantMessageText(response)}`,
)
endTrace(langfuseTrace, undefined, 'error')
return null
}
endTrace(langfuseTrace)
return getAssistantMessageText(response)
} catch (err) {
if (err instanceof APIUserAbortError || signal.aborted) {
return null
}
logForDebugging(`[awaySummary] generation failed: ${err}`)
endTrace(langfuseTrace, undefined, 'error')
return null
}
}

View File

@@ -1326,6 +1326,7 @@ async function streamCompactSummary({
agents: context.options.agentDefinitions.activeAgents,
mcpTools: [],
effortValue: appState.effortValue,
langfuseTrace: context.langfuseTrace,
},
})
const streamIter = streamingGen[Symbol.asyncIterator]()

View File

@@ -1,4 +1,4 @@
export { initLangfuse, shutdownLangfuse, isLangfuseEnabled, getLangfuseProcessor } from './client.js'
export { createTrace, createSubagentTrace, recordLLMObservation, recordToolObservation, endTrace, createToolBatchSpan, endToolBatchSpan } from './tracing.js'
export { createTrace, createSubagentTrace, createChildSpan, recordLLMObservation, recordToolObservation, endTrace, createToolBatchSpan, endToolBatchSpan } from './tracing.js'
export type { LangfuseSpan } from './tracing.js'
export { sanitizeToolInput, sanitizeToolOutput, sanitizeGlobal } from './sanitize.js'

View File

@@ -282,6 +282,60 @@ export function createSubagentTrace(params: {
}
}
/**
* Create a child span under a parent trace — used for side queries
* that should be nested under the main agent trace in Langfuse.
*/
export function createChildSpan(
parentSpan: LangfuseSpan | null,
params: {
name: string
sessionId: string
model: string
provider: string
input?: unknown
querySource?: string
username?: string
},
): LangfuseSpan | null {
if (!parentSpan || !isLangfuseEnabled()) return null
try {
const span = startObservation(
params.name,
{
input: params.input,
metadata: {
provider: params.provider,
model: params.model,
querySource: params.querySource,
},
},
{
asType: 'span',
parentSpanContext: parentSpan.otelSpan.spanContext(),
},
) as LangfuseSpan
// Propagate session ID and user ID from parent
const parent = parentSpan as unknown as RootTrace
const sessionId = parent._sessionId ?? params.sessionId
if (sessionId) {
span.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
;(span as unknown as RootTrace)._sessionId = sessionId
}
const userId = parent._userId ?? resolveLangfuseUserId(params.username)
if (userId) {
span.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId)
;(span as unknown as RootTrace)._userId = userId
}
logForDebugging(`[langfuse] Child span created: ${span.id} (parent=${parentSpan.id})`)
return span
} catch (e) {
logForDebugging(`[langfuse] createChildSpan failed: ${e}`, { level: 'error' })
return null
}
}
export function endTrace(
rootSpan: LangfuseSpan | null,
output?: unknown,

View File

@@ -109,7 +109,6 @@ const externalTips: Tip[] = [
`Use Plan Mode to prepare for a complex request before making changes. Press ${getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab')} twice to enable.`,
cooldownSessions: 5,
isRelevant: async () => {
if (process.env.USER_TYPE === 'ant') return false
const config = getGlobalConfig()
// Show to users who haven't used plan mode recently (7+ days)
const daysSinceLastUse = config.lastPlanModeUse
@@ -401,9 +400,7 @@ const externalTips: Tip[] = [
{
id: 'shift-tab',
content: async () =>
process.env.USER_TYPE === 'ant'
? `Hit ${getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab')} to cycle between default mode and auto mode`
: `Hit ${getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab')} to cycle between default mode, auto-accept edit mode, and plan mode`,
`Hit ${getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab')} to cycle between default, accept edits, plan, auto, and bypass modes`,
cooldownSessions: 10,
isRelevant: async () => true,
},

View File

@@ -25,6 +25,8 @@ import { jsonStringify } from '../utils/slowOperations.js'
import { isToolReferenceBlock } from '../utils/toolSearch.js'
import { getAPIMetadata, getExtraBodyParams } from './api/claude.js'
import { getAnthropicClient } from './api/client.js'
import { createTrace, endTrace, isLangfuseEnabled, recordLLMObservation } from './langfuse/index.js'
import { getSessionId } from '../bootstrap/state.js'
import { withTokenCountVCR } from './vcr.js'
// Minimal values for token counting with thinking enabled
@@ -309,6 +311,15 @@ export async function countTokensViaHaikuFallback(
: betas
// biome-ignore lint/plugin: token counting needs specialized parameters (thinking, betas) that sideQuery doesn't support
const apiStart = Date.now()
const langfuseTrace = isLangfuseEnabled()
? createTrace({
sessionId: getSessionId(),
model: normalizeModelStringForAPI(model),
provider: getAPIProvider(),
name: 'token-estimation',
})
: null
const response = await anthropic.beta.messages.create({
model: normalizeModelStringForAPI(model),
max_tokens: containsThinking ? TOKEN_COUNT_MAX_TOKENS : 1,
@@ -331,6 +342,22 @@ export async function countTokensViaHaikuFallback(
const cacheCreationTokens = usage.cache_creation_input_tokens || 0
const cacheReadTokens = usage.cache_read_input_tokens || 0
recordLLMObservation(langfuseTrace, {
model: normalizeModelStringForAPI(model),
provider: getAPIProvider(),
input: messagesToSend,
output: response.content,
usage: {
input_tokens: inputTokens,
output_tokens: usage.output_tokens,
cache_creation_input_tokens: cacheCreationTokens || undefined,
cache_read_input_tokens: cacheReadTokens || undefined,
},
startTime: new Date(apiStart),
endTime: new Date(),
})
endTrace(langfuseTrace)
return inputTokens + cacheCreationTokens + cacheReadTokens
}

View File

@@ -17,7 +17,6 @@ import {
notifySessionMetadataChanged,
type SessionExternalMetadata,
} from '../utils/sessionState.js'
import { updateSettingsForSource } from '../utils/settings/settings.js'
import type { AppState } from './AppStateStore.js'
// Inverse of the push below — restore on worker restart.
@@ -91,23 +90,11 @@ export function onChangeAppState({
notifyPermissionModeChanged(newMode)
}
// mainLoopModel: remove it from settings?
if (
newState.mainLoopModel !== oldState.mainLoopModel &&
newState.mainLoopModel === null
) {
// Remove from settings
updateSettingsForSource('userSettings', { model: undefined })
setMainLoopModelOverride(null)
}
// mainLoopModel: add it to settings?
if (
newState.mainLoopModel !== oldState.mainLoopModel &&
newState.mainLoopModel !== null
) {
// Save to settings
updateSettingsForSource('userSettings', { model: newState.mainLoopModel })
// mainLoopModel: session-scoped only (do NOT persist to userSettings).
// Writing to settings.json would leak model changes into other running
// sessions (anthropics/claude-code#37596). Each process keeps its own
// model override in memory via setMainLoopModelOverride.
if (newState.mainLoopModel !== oldState.mainLoopModel) {
setMainLoopModelOverride(newState.mainLoopModel)
}

View File

@@ -457,9 +457,14 @@ describe("buildClassifierUnavailableMessage", () => {
expect(msg).toContain("classifier-v1");
expect(msg).toContain("unavailable");
});
test("tells the model to wait and retry later", () => {
const msg = buildClassifierUnavailableMessage("Bash", "classifier-v1");
expect(msg).toContain("Wait briefly and then try this action again.");
expect(msg).toContain("come back to it later");
});
});
// ─── normalizeMessages ──────────────────────────────────────────────────
describe("normalizeMessages", () => {
test("splits multi-block assistant message into individual messages", () => {

View File

@@ -2201,6 +2201,7 @@ async function getRelevantMemoryAttachments(
recentTools: readonly string[],
signal: AbortSignal,
alreadySurfaced: ReadonlySet<string>,
parentSpan?: unknown,
): Promise<Attachment[]> {
// If an agent is @-mentioned, search only its memory dir (isolation).
// Otherwise search the auto-memory dir.
@@ -2221,6 +2222,7 @@ async function getRelevantMemoryAttachments(
signal,
recentTools,
alreadySurfaced,
parentSpan as Parameters<typeof findRelevantMemories>[5],
).catch(() => []),
),
)
@@ -2370,6 +2372,12 @@ export function startRelevantMemoryPrefetch(
return undefined
}
// Poor mode: skip the side-query to save tokens
const { isPoorModeActive } = require('../commands/poor/poorMode.js') as typeof import('../commands/poor/poorMode.js')
if (isPoorModeActive()) {
return undefined
}
const lastUserMessage = messages.findLast(m => m.type === 'user' && !m.isMeta)
if (!lastUserMessage) {
return undefined
@@ -2397,6 +2405,7 @@ export function startRelevantMemoryPrefetch(
collectRecentSuccessfulTools(messages, lastUserMessage),
controller.signal,
surfaced.paths,
toolUseContext.langfuseTrace,
).catch(e => {
if (!isAbortError(e)) {
logError(e)

View File

@@ -133,6 +133,12 @@ export function calculateContextPercentages(
currentUsage.cache_creation_input_tokens +
currentUsage.cache_read_input_tokens
// Treat zero input tokens the same as no usage data — avoids flashing
// "ctx:0%" when a third-party API omits usage from message_start.
if (totalInputTokens === 0) {
return { used: null, remaining: null }
}
const usedPercentage = Math.round(
(totalInputTokens / contextWindowSize) * 100,
)

View File

@@ -374,6 +374,10 @@ export function createSubagentContext(
}
return {
// Preserve the parent Langfuse trace separately so nested side queries
// like auto_mode can attach to the main agent trace instead of the
// subagent's own trace.
langfuseRootTrace: parentContext.langfuseTrace,
// Mutable state - cloned by default to maintain isolation
// Clone overrides.readFileState if provided, otherwise clone from parent
readFileState: cloneFileStateCache(

View File

@@ -104,6 +104,7 @@ export function createApiQueryHook<TResult>(
querySource: config.name,
mcpTools: [],
agentId: context.toolUseContext.agentId,
langfuseTrace: context.toolUseContext.langfuseTrace,
},
})

View File

@@ -84,6 +84,7 @@ Your response must be a JSON object matching one of the following schemas:
querySource: 'hook_prompt',
mcpTools: [],
agentId: toolUseContext.agentId,
langfuseTrace: toolUseContext.langfuseTrace,
outputFormat: {
type: 'json_schema',
schema: {

View File

@@ -7,6 +7,9 @@ import {
logEvent,
} from '../../services/analytics/index.js'
import { queryModelWithoutStreaming } from '../../services/api/claude.js'
import { createTrace, endTrace, isLangfuseEnabled } from '../../services/langfuse/index.js'
import { getSessionId } from '../../bootstrap/state.js'
import { getAPIProvider } from '../model/providers.js'
import { getEmptyToolPermissionContext } from '../../Tool.js'
import type { Message } from '../../types/message.js'
import { createAbortController } from '../abortController.js'
@@ -209,6 +212,16 @@ export async function applySkillImprovement(
const updateList = updates.map(u => `- ${u.section}: ${u.change}`).join('\n')
const model = getSmallFastModel()
const langfuseTrace = isLangfuseEnabled()
? createTrace({
sessionId: getSessionId(),
model,
provider: getAPIProvider(),
name: 'skill-improvement-apply',
})
: null
const response = await queryModelWithoutStreaming({
messages: [
createUserMessage({
@@ -238,7 +251,7 @@ Rules:
signal: createAbortController().signal,
options: {
getToolPermissionContext: async () => getEmptyToolPermissionContext(),
model: getSmallFastModel(),
model,
toolChoice: undefined,
isNonInteractiveSession: false,
hasAppendSystemPrompt: false,
@@ -246,9 +259,12 @@ Rules:
agents: [],
querySource: 'skill_improvement_apply',
mcpTools: [],
langfuseTrace,
},
})
endTrace(langfuseTrace)
const responseText = extractTextContent(Array.isArray(response.message.content) ? response.message.content : []).trim()
const updatedContent = extractTag(responseText, 'updated_file')

View File

@@ -126,6 +126,12 @@ export function getDefaultOpusModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
}
// Fall back to user's configured model — custom providers may not
// recognize hardcoded Anthropic model IDs.
const userSpecifiedOpus = getUserSpecifiedModelSetting()
if (userSpecifiedOpus) {
return parseUserSpecifiedModel(userSpecifiedOpus)
}
// 3P providers (Bedrock, Vertex, Foundry) — kept as a separate branch
// even when values match, since 3P availability lags firstParty and
// these will diverge again at the next model launch.
@@ -153,6 +159,13 @@ export function getDefaultSonnetModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) {
return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
}
// Fall back to user's configured model (ANTHROPIC_MODEL / settings) —
// custom providers (proxies, national clouds) may not recognize the
// hardcoded Anthropic model IDs.
const userSpecified = getUserSpecifiedModelSetting()
if (userSpecified) {
return parseUserSpecifiedModel(userSpecified)
}
// Default to Sonnet 4.5 for 3P since they may not have 4.6 yet
if (provider !== 'firstParty') {
return getModelStrings().sonnet45
@@ -175,6 +188,12 @@ export function getDefaultHaikuModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) {
return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
}
// Fall back to user's configured model — custom providers may not
// recognize hardcoded Anthropic model IDs.
const userSpecifiedHaiku = getUserSpecifiedModelSetting()
if (userSpecifiedHaiku) {
return parseUserSpecifiedModel(userSpecifiedHaiku)
}
// Haiku 4.5 is available on all platforms (first-party, Foundry, Bedrock, Vertex)
return getModelStrings().haiku45

View File

@@ -0,0 +1,204 @@
/**
* Tests for src/utils/permissions/getNextPermissionMode.ts
*
* Covers the unified permission mode cycling logic:
* default → acceptEdits → plan → auto → bypassPermissions → default
*
* After the "open auto/bypass to all users" change, there is no USER_TYPE
* distinction — all users share the same cycle order.
*/
import { describe, expect, test } from 'bun:test'
import type { ToolPermissionContext } from '../../../Tool.js'
import type { PermissionMode } from '../PermissionMode.js'
// Inline getNextPermissionMode to avoid importing the heavy permissionSetup
// dependency chain (growthbook, settings, etc.).
// The function under test is small and pure enough to copy for testing.
import { getNextPermissionMode } from '../getNextPermissionMode.js'
// ─── helpers ──────────────────────────────────────────────────────────────────
function makeContext(
mode: PermissionMode,
overrides: Partial<ToolPermissionContext> = {},
): ToolPermissionContext {
return {
mode,
additionalWorkingDirectories: new Map(),
alwaysAllowRules: {},
alwaysDenyRules: {},
alwaysAskRules: {},
isBypassPermissionsModeAvailable: true,
...overrides,
}
}
// ─── tests ────────────────────────────────────────────────────────────────────
describe('getNextPermissionMode', () => {
// ── Full cycle ──────────────────────────────────────────────────────────
describe('unified cycle order', () => {
test('default → acceptEdits', () => {
expect(getNextPermissionMode(makeContext('default'))).toBe('acceptEdits')
})
test('acceptEdits → plan', () => {
expect(getNextPermissionMode(makeContext('acceptEdits'))).toBe('plan')
})
test('plan → auto', () => {
expect(getNextPermissionMode(makeContext('plan'))).toBe('auto')
})
test('auto → bypassPermissions (when bypass available)', () => {
expect(getNextPermissionMode(makeContext('auto'))).toBe('bypassPermissions')
})
test('bypassPermissions → default', () => {
expect(getNextPermissionMode(makeContext('bypassPermissions'))).toBe('default')
})
test('full cycle completes back to default', () => {
const cycle: PermissionMode[] = []
let ctx = makeContext('default')
for (let i = 0; i < 5; i++) {
const next = getNextPermissionMode(ctx)
cycle.push(next)
ctx = makeContext(next)
}
expect(cycle).toEqual([
'acceptEdits',
'plan',
'auto',
'bypassPermissions',
'default',
])
})
})
// ── auto → default when bypass unavailable ─────────────────────────────
describe('auto mode with bypass unavailable', () => {
test('auto → default when isBypassPermissionsModeAvailable is false', () => {
const ctx = makeContext('auto', {
isBypassPermissionsModeAvailable: false,
})
expect(getNextPermissionMode(ctx)).toBe('default')
})
})
// ── dontAsk mode ────────────────────────────────────────────────────────
describe('dontAsk mode', () => {
test('dontAsk → default', () => {
expect(getNextPermissionMode(makeContext('dontAsk'))).toBe('default')
})
})
// ── USER_TYPE independence ──────────────────────────────────────────────
describe('no USER_TYPE distinction', () => {
test('cycle order is the same regardless of USER_TYPE', () => {
// Save original
const originalUserType = process.env.USER_TYPE
// Test with no USER_TYPE
delete process.env.USER_TYPE
const cycleNoType: PermissionMode[] = []
let ctx = makeContext('default')
for (let i = 0; i < 5; i++) {
const next = getNextPermissionMode(ctx)
cycleNoType.push(next)
ctx = makeContext(next)
}
// Test with USER_TYPE=ant
process.env.USER_TYPE = 'ant'
const cycleAnt: PermissionMode[] = []
ctx = makeContext('default')
for (let i = 0; i < 5; i++) {
const next = getNextPermissionMode(ctx)
cycleAnt.push(next)
ctx = makeContext(next)
}
// Restore
if (originalUserType !== undefined) {
process.env.USER_TYPE = originalUserType
} else {
delete process.env.USER_TYPE
}
// Both should produce the same cycle
expect(cycleNoType).toEqual(cycleAnt)
expect(cycleNoType).toEqual([
'acceptEdits',
'plan',
'auto',
'bypassPermissions',
'default',
])
})
})
// ── teamContext parameter ───────────────────────────────────────────────
describe('teamContext parameter', () => {
test('does not affect cycle when provided', () => {
const ctx = makeContext('default')
const teamCtx = { leadAgentId: 'agent-123' }
expect(getNextPermissionMode(ctx, teamCtx)).toBe('acceptEdits')
})
test('does not affect cycle for plan mode', () => {
const ctx = makeContext('plan')
const teamCtx = { leadAgentId: 'agent-456' }
expect(getNextPermissionMode(ctx, teamCtx)).toBe('auto')
})
})
// ── cycle stability (no infinite loops) ─────────────────────────────────
describe('cycle stability', () => {
test('all modes return to default within 6 steps', () => {
const modes: PermissionMode[] = [
'default',
'acceptEdits',
'plan',
'auto',
'bypassPermissions',
'dontAsk',
]
for (const startMode of modes) {
let current = startMode
let returnedToDefault = false
for (let i = 0; i < 6; i++) {
current = getNextPermissionMode(makeContext(current))
if (current === 'default') {
returnedToDefault = true
break
}
}
expect(returnedToDefault).toBe(true)
}
})
test('cycling 100 times never produces an invalid mode', () => {
const validModes = new Set<string>([
'default',
'acceptEdits',
'plan',
'auto',
'bypassPermissions',
'dontAsk',
])
let ctx = makeContext('default')
for (let i = 0; i < 100; i++) {
const next = getNextPermissionMode(ctx)
expect(validModes.has(next)).toBe(true)
ctx = makeContext(next)
}
})
})
})

View File

@@ -0,0 +1,148 @@
/**
* Tests for the simplified permission gate functions.
*
* After the "open auto/bypass to all users" change, the key guarantees are:
* - shouldDisableBypassPermissions() always returns false
* - isBypassPermissionsModeDisabled() always returns false
* - hasAutoModeOptInAnySource() always returns true
* - isAutoModeGateEnabled() returns true unless fast-mode circuit breaker fires
* - getAutoModeUnavailableReason() returns null when no breaker fires
*
* These functions are tested through the getNextPermissionMode cycle
* and through direct unit tests of the gate functions.
*/
import { describe, expect, test } from 'bun:test'
import type { ToolPermissionContext } from '../../../Tool.js'
import type { PermissionMode } from '../PermissionMode.js'
import { getNextPermissionMode } from '../getNextPermissionMode.js'
// ─── helpers ──────────────────────────────────────────────────────────────────
function makeContext(
mode: PermissionMode,
overrides: Partial<ToolPermissionContext> = {},
): ToolPermissionContext {
return {
mode,
additionalWorkingDirectories: new Map(),
alwaysAllowRules: {},
alwaysDenyRules: {},
alwaysAskRules: {},
isBypassPermissionsModeAvailable: true,
...overrides,
}
}
// ─── tests ────────────────────────────────────────────────────────────────────
describe('permission gate invariants (after opening auto/bypass)', () => {
// ── Bypass permissions is always available ──────────────────────────────
describe('bypass mode always reachable in cycle', () => {
test('auto → bypassPermissions when isBypassPermissionsModeAvailable is true', () => {
const ctx = makeContext('auto', { isBypassPermissionsModeAvailable: true })
expect(getNextPermissionMode(ctx)).toBe('bypassPermissions')
})
test('isBypassPermissionsModeAvailable true is the default from getEmptyToolPermissionContext', () => {
// This test verifies the Tool.ts default is true
// (imported indirectly through the cycle behavior)
const ctx = makeContext('auto')
expect(ctx.isBypassPermissionsModeAvailable).toBe(true)
expect(getNextPermissionMode(ctx)).toBe('bypassPermissions')
})
})
// ── Auto mode is always available in cycle ──────────────────────────────
describe('auto mode always reachable in cycle', () => {
test('plan → auto (always, no gate check)', () => {
expect(getNextPermissionMode(makeContext('plan'))).toBe('auto')
})
test('plan → auto even when isBypassPermissionsModeAvailable is false', () => {
const ctx = makeContext('plan', { isBypassPermissionsModeAvailable: false })
expect(getNextPermissionMode(ctx)).toBe('auto')
})
test('bypassPermissions → default (then default → acceptEdits → plan → auto)', () => {
// Verify that after bypass, you can reach auto by cycling through
const fromBypass = getNextPermissionMode(makeContext('bypassPermissions'))
expect(fromBypass).toBe('default')
const fromDefault = getNextPermissionMode(makeContext('default'))
expect(fromDefault).toBe('acceptEdits')
const fromAcceptEdits = getNextPermissionMode(makeContext('acceptEdits'))
expect(fromAcceptEdits).toBe('plan')
const fromPlan = getNextPermissionMode(makeContext('plan'))
expect(fromPlan).toBe('auto')
})
})
// ── No opt-in gate between modes ────────────────────────────────────────
describe('no opt-in gate between modes', () => {
test('cycling from default to auto completes in 3 steps without any opt-in check', () => {
let mode: PermissionMode = 'default'
const steps: PermissionMode[] = []
// default → acceptEdits → plan → auto
for (let i = 0; i < 3; i++) {
mode = getNextPermissionMode(makeContext(mode))
steps.push(mode)
}
expect(steps).toEqual(['acceptEdits', 'plan', 'auto'])
})
test('cycling from default to bypassPermissions completes in 4 steps', () => {
let mode: PermissionMode = 'default'
const steps: PermissionMode[] = []
for (let i = 0; i < 4; i++) {
mode = getNextPermissionMode(makeContext(mode))
steps.push(mode)
}
expect(steps).toEqual(['acceptEdits', 'plan', 'auto', 'bypassPermissions'])
})
})
// ── Mode ordering safety (most dangerous modes last) ────────────────────
describe('safety ordering', () => {
test('auto comes before bypassPermissions in the cycle', () => {
// Starting from plan, user must press Shift+Tab twice to reach bypass
// (plan → auto → bypassPermissions)
const fromPlan = getNextPermissionMode(makeContext('plan'))
expect(fromPlan).toBe('auto')
const fromAuto = getNextPermissionMode(makeContext('auto'))
expect(fromAuto).toBe('bypassPermissions')
})
test('default comes before any dangerous mode', () => {
// default → acceptEdits (safe, just auto-accept edits)
const fromDefault = getNextPermissionMode(makeContext('default'))
expect(fromDefault).toBe('acceptEdits')
// acceptEdits is the least dangerous mode
})
})
})
describe('Tool.ts default context', () => {
test('getEmptyToolPermissionContext has isBypassPermissionsModeAvailable = true', async () => {
const { getEmptyToolPermissionContext } = await import('../../../Tool.js')
const ctx = getEmptyToolPermissionContext()
expect(ctx.isBypassPermissionsModeAvailable).toBe(true)
})
})
describe('settings hasAutoModeOptIn', () => {
test('always returns true after change', async () => {
const { hasAutoModeOptIn } = await import('../../settings/settings.js')
expect(hasAutoModeOptIn()).toBe(true)
})
})

View File

@@ -1,153 +1,136 @@
import { mock, describe, expect, test } from "bun:test";
import { mock, describe, expect, test } from 'bun:test'
import { createFileStateCacheWithSizeLimit } from '../../../utils/fileStateCache.js'
import { createSubagentContext } from '../../../utils/forkedAgent.js'
import { getEmptyToolPermissionContext } from '../../../Tool.js'
// Mock log.ts to cut the heavy dependency chain
mock.module("src/utils/log.ts", () => ({
mock.module('src/utils/log.ts', () => ({
logError: () => {},
logToFile: () => {},
getLogDisplayTitle: () => "",
getLogDisplayTitle: () => '',
logEvent: () => {},
logMCPError: () => {},
logMCPDebug: () => {},
dateToFilename: (d: Date) => d.toISOString().replace(/[:.]/g, "-"),
getLogFilePath: () => "/tmp/mock-log",
dateToFilename: (d: Date) => d.toISOString().replace(/[:.]/g, '-'),
getLogFilePath: () => '/tmp/mock-log',
attachErrorLogSink: () => {},
getInMemoryErrors: () => [],
loadErrorLogs: async () => [],
getErrorLogByIndex: async () => null,
captureAPIRequest: () => {},
_resetErrorLogForTesting: () => {},
}));
}))
const {
getDenyRuleForTool,
getAskRuleForTool,
getDenyRuleForAgent,
filterDeniedAgents,
} = await import("../permissions");
} = await import('../permissions')
import { getEmptyToolPermissionContext } from "../../../Tool";
// ─── Helper ─────────────────────────────────────────────────────────────
function makeContext(opts: {
denyRules?: string[];
askRules?: string[];
}) {
const ctx = getEmptyToolPermissionContext();
const deny: Record<string, string[]> = {};
const ask: Record<string, string[]> = {};
// alwaysDenyRules stores raw rule strings — getDenyRules() calls
// permissionRuleValueFromString internally
if (opts.denyRules?.length) {
deny["localSettings"] = opts.denyRules;
}
if (opts.askRules?.length) {
ask["localSettings"] = opts.askRules;
}
return {
...ctx,
alwaysDenyRules: deny,
alwaysAskRules: ask,
} as any;
function makeContext(opts: { denyRules?: string[]; askRules?: string[] }) {
const ctx = getEmptyToolPermissionContext()
const deny: Record<string, string[]> = {}
const ask: Record<string, string[]> = {}
if (opts.denyRules?.length) deny.localSettings = opts.denyRules
if (opts.askRules?.length) ask.localSettings = opts.askRules
return { ...ctx, alwaysDenyRules: deny, alwaysAskRules: ask } as any
}
function makeTool(name: string, mcpInfo?: { serverName: string; toolName: string }) {
return { name, mcpInfo };
return { name, mcpInfo }
}
// ─── getDenyRuleForTool ─────────────────────────────────────────────────
describe('getDenyRuleForTool', () => {
test('returns null when no deny rules', () => {
const ctx = makeContext({})
expect(getDenyRuleForTool(ctx, makeTool('Bash'))).toBeNull()
})
test('returns matching deny rule for tool', () => {
const ctx = makeContext({ denyRules: ['Bash'] })
const result = getDenyRuleForTool(ctx, makeTool('Bash'))
expect(result).not.toBeNull()
expect(result!.ruleValue.toolName).toBe('Bash')
})
test('returns null for non-matching tool', () => {
const ctx = makeContext({ denyRules: ['Bash'] })
expect(getDenyRuleForTool(ctx, makeTool('Read'))).toBeNull()
})
test('rule with content does not match whole-tool deny', () => {
const ctx = makeContext({ denyRules: ['Bash(rm -rf)'] })
const result = getDenyRuleForTool(ctx, makeTool('Bash'))
expect(result).toBeNull()
})
})
describe("getDenyRuleForTool", () => {
test("returns null when no deny rules", () => {
const ctx = makeContext({});
expect(getDenyRuleForTool(ctx, makeTool("Bash"))).toBeNull();
});
describe('getAskRuleForTool', () => {
test('returns null when no ask rules', () => {
const ctx = makeContext({})
expect(getAskRuleForTool(ctx, makeTool('Bash'))).toBeNull()
})
test('returns matching ask rule', () => {
const ctx = makeContext({ askRules: ['Write'] })
const result = getAskRuleForTool(ctx, makeTool('Write'))
expect(result).not.toBeNull()
})
test('returns null for non-matching tool', () => {
const ctx = makeContext({ askRules: ['Write'] })
expect(getAskRuleForTool(ctx, makeTool('Bash'))).toBeNull()
})
})
test("returns matching deny rule for tool", () => {
const ctx = makeContext({ denyRules: ["Bash"] });
const result = getDenyRuleForTool(ctx, makeTool("Bash"));
expect(result).not.toBeNull();
expect(result!.ruleValue.toolName).toBe("Bash");
});
describe('getDenyRuleForAgent', () => {
test('returns null when no deny rules', () => {
const ctx = makeContext({})
expect(getDenyRuleForAgent(ctx, 'Agent', 'Explore')).toBeNull()
})
test('returns matching deny rule for agent type', () => {
const ctx = makeContext({ denyRules: ['Agent(Explore)'] })
const result = getDenyRuleForAgent(ctx, 'Agent', 'Explore')
expect(result).not.toBeNull()
})
test('returns null for non-matching agent type', () => {
const ctx = makeContext({ denyRules: ['Agent(Explore)'] })
expect(getDenyRuleForAgent(ctx, 'Agent', 'Research')).toBeNull()
})
})
test("returns null for non-matching tool", () => {
const ctx = makeContext({ denyRules: ["Bash"] });
expect(getDenyRuleForTool(ctx, makeTool("Read"))).toBeNull();
});
describe('Langfuse trace propagation', () => {
test('subagent context preserves parent trace for nested side queries', () => {
const parentTrace = { id: 'parent-trace' } as never
const parentContext = {
...getEmptyToolPermissionContext(),
messages: [],
abortController: new AbortController(),
readFileState: createFileStateCacheWithSizeLimit(1),
getAppState: () => ({ toolPermissionContext: getEmptyToolPermissionContext() }),
setAppState: () => {},
updateFileHistoryState: () => {},
updateAttributionState: () => {},
setInProgressToolUseIDs: () => {},
setResponseLength: () => {},
langfuseTrace: parentTrace,
} as never
const subagentContext = createSubagentContext(parentContext)
expect(subagentContext.langfuseRootTrace).toBe(parentTrace)
})
})
test("rule with content does not match whole-tool deny", () => {
// getDenyRuleForTool uses toolMatchesRule which requires ruleContent === undefined
// Rules like "Bash(rm -rf)" only match specific invocations, not the entire tool
const ctx = makeContext({ denyRules: ["Bash(rm -rf)"] });
const result = getDenyRuleForTool(ctx, makeTool("Bash"));
expect(result).toBeNull();
});
});
// ─── getAskRuleForTool ──────────────────────────────────────────────────
describe("getAskRuleForTool", () => {
test("returns null when no ask rules", () => {
const ctx = makeContext({});
expect(getAskRuleForTool(ctx, makeTool("Bash"))).toBeNull();
});
test("returns matching ask rule", () => {
const ctx = makeContext({ askRules: ["Write"] });
const result = getAskRuleForTool(ctx, makeTool("Write"));
expect(result).not.toBeNull();
});
test("returns null for non-matching tool", () => {
const ctx = makeContext({ askRules: ["Write"] });
expect(getAskRuleForTool(ctx, makeTool("Bash"))).toBeNull();
});
});
// ─── getDenyRuleForAgent ────────────────────────────────────────────────
describe("getDenyRuleForAgent", () => {
test("returns null when no deny rules", () => {
const ctx = makeContext({});
expect(getDenyRuleForAgent(ctx, "Agent", "Explore")).toBeNull();
});
test("returns matching deny rule for agent type", () => {
const ctx = makeContext({ denyRules: ["Agent(Explore)"] });
const result = getDenyRuleForAgent(ctx, "Agent", "Explore");
expect(result).not.toBeNull();
});
test("returns null for non-matching agent type", () => {
const ctx = makeContext({ denyRules: ["Agent(Explore)"] });
expect(getDenyRuleForAgent(ctx, "Agent", "Research")).toBeNull();
});
});
// ─── filterDeniedAgents ─────────────────────────────────────────────────
describe("filterDeniedAgents", () => {
test("returns all agents when no deny rules", () => {
const ctx = makeContext({});
const agents = [{ agentType: "Explore" }, { agentType: "Research" }];
expect(filterDeniedAgents(agents, ctx, "Agent")).toEqual(agents);
});
test("filters out denied agent type", () => {
const ctx = makeContext({ denyRules: ["Agent(Explore)"] });
const agents = [{ agentType: "Explore" }, { agentType: "Research" }];
const result = filterDeniedAgents(agents, ctx, "Agent");
expect(result).toHaveLength(1);
expect(result[0]!.agentType).toBe("Research");
});
test("returns empty array when all agents denied", () => {
const ctx = makeContext({
denyRules: ["Agent(Explore)", "Agent(Research)"],
});
const agents = [{ agentType: "Explore" }, { agentType: "Research" }];
expect(filterDeniedAgents(agents, ctx, "Agent")).toEqual([]);
});
});
describe('filterDeniedAgents', () => {
test('returns all agents when no deny rules', () => {
const ctx = makeContext({})
const agents = [{ agentType: 'Explore' }, { agentType: 'Research' }]
expect(filterDeniedAgents(agents, ctx, 'Agent')).toEqual(agents)
})
test('filters out denied agent type', () => {
const ctx = makeContext({ denyRules: ['Agent(Explore)'] })
const agents = [{ agentType: 'Explore' }, { agentType: 'Research' }]
const result = filterDeniedAgents(agents, ctx, 'Agent')
expect(result).toHaveLength(1)
expect(result[0]!.agentType).toBe('Research')
})
test('returns empty array when all agents denied', () => {
const ctx = makeContext({ denyRules: ['Agent(Explore)', 'Agent(Research)'] })
const agents = [{ agentType: 'Explore' }, { agentType: 'Research' }]
expect(filterDeniedAgents(agents, ctx, 'Agent')).toEqual([])
})
})

View File

@@ -1,79 +1,44 @@
import { feature } from 'bun:bundle'
import { useEffect, useRef } from 'react'
import {
type AppState,
useAppState,
useAppStateStore,
useSetAppState,
} from 'src/state/AppState.js'
import type { ToolPermissionContext } from 'src/Tool.js'
import { useNotifications } from 'src/context/notifications.js'
import { toError } from '../../utils/errors.js'
import { logError } from '../../utils/log.js'
import { getIsRemoteMode } from '../../bootstrap/state.js'
import { useAppState, useAppStateStore, useSetAppState } from '../../state/AppState.js'
import type { ToolPermissionContext } from '../../Tool.js'
import {
createDisabledBypassPermissionsContext,
shouldDisableBypassPermissions,
verifyAutoModeGateAccess,
} from './permissionSetup.js'
let bypassPermissionsCheckRan = false
/**
* No-op — bypass permissions is always available.
*/
export async function checkAndDisableBypassPermissionsIfNeeded(
toolPermissionContext: ToolPermissionContext,
setAppState: (f: (prev: AppState) => AppState) => void,
_toolPermissionContext: ToolPermissionContext,
_setAppState: (f: (prev: import('../../state/AppState.js').AppState) => import('../../state/AppState.js').AppState) => void,
): Promise<void> {
// Check if bypassPermissions should be disabled based on Statsig gate
// Do this only once, before the first query, to ensure we have the latest gate value
if (bypassPermissionsCheckRan) {
return
}
bypassPermissionsCheckRan = true
if (!toolPermissionContext.isBypassPermissionsModeAvailable) {
return
}
const shouldDisable = await shouldDisableBypassPermissions()
if (!shouldDisable) {
return
}
setAppState(prev => {
return {
...prev,
toolPermissionContext: createDisabledBypassPermissionsContext(
prev.toolPermissionContext,
),
}
})
// Bypass permissions is always available — no gate check needed
}
/**
* Reset the run-once flag for checkAndDisableBypassPermissionsIfNeeded.
* Call this after /login so the gate check re-runs with the new org.
* Reset stub — kept for interface compatibility.
*/
export function resetBypassPermissionsCheck(): void {
bypassPermissionsCheckRan = false
// No-op
}
/**
* No-op hook — bypass permissions is always available.
*/
export function useKickOffCheckAndDisableBypassPermissionsIfNeeded(): void {
const toolPermissionContext = useAppState(s => s.toolPermissionContext)
const setAppState = useSetAppState()
// Run once, when the component mounts
useEffect(() => {
if (getIsRemoteMode()) return
void checkAndDisableBypassPermissionsIfNeeded(
toolPermissionContext,
setAppState,
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// No-op
}
let autoModeCheckRan = false
export async function checkAndDisableAutoModeIfNeeded(
toolPermissionContext: ToolPermissionContext,
setAppState: (f: (prev: AppState) => AppState) => void,
setAppState: (f: (prev: import('../../state/AppState.js').AppState) => import('../../state/AppState.js').AppState) => void,
fastMode?: boolean,
): Promise<void> {
if (feature('TRANSCRIPT_CLASSIFIER')) {
@@ -87,10 +52,6 @@ export async function checkAndDisableAutoModeIfNeeded(
fastMode,
)
setAppState(prev => {
// Apply the transform to CURRENT context, not the stale snapshot we
// passed to verifyAutoModeGateAccess. The async GrowthBook await inside
// can be outrun by a mid-turn shift-tab; spreading a stale context here
// would revert the user's mode change.
const nextCtx = updateContext(prev.toolPermissionContext)
const newState =
nextCtx === prev.toolPermissionContext
@@ -133,11 +94,6 @@ export function useKickOffCheckAndDisableAutoModeIfNeeded(): void {
const isFirstRunRef = useRef(true)
// Runs on mount (startup check) AND whenever the model or fast mode changes
// (kick-out / carousel-restore). Watching both model fields covers /model,
// Cmd+P picker, /config, and bridge onSetModel paths; fastMode covers
// /fast on|off for the tengu_auto_mode_config.disableFastMode circuit
// breaker. The print.ts headless paths are covered by the sync
// isAutoModeGateEnabled() check.
useEffect(() => {
if (getIsRemoteMode()) return
if (isFirstRunRef.current) {
@@ -149,7 +105,9 @@ export function useKickOffCheckAndDisableAutoModeIfNeeded(): void {
store.getState().toolPermissionContext,
setAppState,
fastMode,
)
).catch(error => {
logError(new Error('Auto mode gate check failed', { cause: toError(error) }))
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mainLoopModel, mainLoopModelForSession, fastMode])
}

View File

@@ -1,35 +1,13 @@
import { feature } from 'bun:bundle'
import type { ToolPermissionContext } from '../../Tool.js'
import { logForDebugging } from '../debug.js'
import type { PermissionMode } from './PermissionMode.js'
import {
getAutoModeUnavailableReason,
isAutoModeGateEnabled,
transitionPermissionMode,
} from './permissionSetup.js'
// Checks both the cached isAutoModeAvailable (set at startup by
// verifyAutoModeGateAccess) and the live isAutoModeGateEnabled() — these can
// diverge if the circuit breaker or settings change mid-session. The
// live check prevents transitionPermissionMode from throwing
// (permissionSetup.ts:~559), which would silently crash the shift+tab handler
// and leave the user stuck at the current mode.
function canCycleToAuto(ctx: ToolPermissionContext): boolean {
if (feature('TRANSCRIPT_CLASSIFIER')) {
const gateEnabled = isAutoModeGateEnabled()
const can = !!ctx.isAutoModeAvailable && gateEnabled
if (!can) {
logForDebugging(
`[auto-mode] canCycleToAuto=false: ctx.isAutoModeAvailable=${ctx.isAutoModeAvailable} isAutoModeGateEnabled=${gateEnabled} reason=${getAutoModeUnavailableReason()}`,
)
}
return can
}
return false
}
import { transitionPermissionMode } from './permissionSetup.js'
/**
* Determines the next permission mode when cycling through modes with Shift+Tab.
*
* Unified cycle for all users (no USER_TYPE distinction):
* default → acceptEdits → plan → auto → bypassPermissions → default
*/
export function getNextPermissionMode(
toolPermissionContext: ToolPermissionContext,
@@ -37,43 +15,29 @@ export function getNextPermissionMode(
): PermissionMode {
switch (toolPermissionContext.mode) {
case 'default':
// Ants skip acceptEdits and plan — auto mode replaces them
if (process.env.USER_TYPE === 'ant') {
if (toolPermissionContext.isBypassPermissionsModeAvailable) {
return 'bypassPermissions'
}
if (canCycleToAuto(toolPermissionContext)) {
return 'auto'
}
return 'default'
}
return 'acceptEdits'
case 'acceptEdits':
return 'plan'
case 'plan':
return 'auto'
case 'auto':
if (toolPermissionContext.isBypassPermissionsModeAvailable) {
return 'bypassPermissions'
}
if (canCycleToAuto(toolPermissionContext)) {
return 'auto'
}
return 'default'
case 'bypassPermissions':
if (canCycleToAuto(toolPermissionContext)) {
return 'auto'
}
return 'default'
case 'dontAsk':
// Not exposed in UI cycle yet, but return default if somehow reached
return 'default'
default:
// Covers auto (when TRANSCRIPT_CLASSIFIER is enabled) and any future modes — always fall back to default
// Covers any future modes — always fall back to default
return 'default'
}
}

View File

@@ -7,7 +7,8 @@ import { logForDebugging } from '../debug.js'
import { errorMessage } from '../errors.js'
import { lazySchema } from '../lazySchema.js'
import { logError } from '../log.js'
import { getMainLoopModel } from '../model/model.js'
import { getMainLoopModel, getSmallFastModel } from '../model/model.js'
import { isPoorModeActive } from '../../commands/poor/poorMode.js'
import { sideQuery } from '../sideQuery.js'
import { jsonStringify } from '../slowOperations.js'
@@ -172,7 +173,7 @@ ${conversationContext ? `\nRecent conversation context:\n${conversationContext}`
Explain this command in context.`
const model = getMainLoopModel()
const model = isPoorModeActive() ? getSmallFastModel() : getMainLoopModel()
// Use sideQuery with forced tool choice for guaranteed structured output
const response = await sideQuery({

View File

@@ -799,10 +799,6 @@ export function initialPermissionModeFromCLI({
result = { mode: 'default', notification }
}
if (!result) {
result = { mode: 'default', notification }
}
if (feature('TRANSCRIPT_CLASSIFIER') && result.mode === 'auto') {
autoModeStateModule?.setAutoModeActive(true)
}
@@ -927,20 +923,9 @@ export async function initializeToolPermissionContext({
})
}
// Check if bypassPermissions mode is available (not disabled by Statsig gate or settings)
// Use cached values to avoid blocking on startup
const growthBookDisableBypassPermissionsMode =
checkStatsigFeatureGate_CACHED_MAY_BE_STALE(
'tengu_disable_bypass_permissions_mode',
)
// Bypass permissions mode is available to all users
const isBypassPermissionsModeAvailable = true
const settings = getSettings_DEPRECATED() || {}
const settingsDisableBypassPermissionsMode =
settings.permissions?.disableBypassPermissionsMode === 'disable'
const isBypassPermissionsModeAvailable =
(permissionMode === 'bypassPermissions' ||
allowDangerouslySkipPermissions) &&
!growthBookDisableBypassPermissionsMode &&
!settingsDisableBypassPermissionsMode
// Load all permission rules from disk
const rulesFromDisk = loadAllPermissionRulesFromDisk()
@@ -984,7 +969,7 @@ export async function initializeToolPermissionContext({
alwaysAskRules: {},
isBypassPermissionsModeAvailable,
...(feature('TRANSCRIPT_CLASSIFIER')
? { isAutoModeAvailable: isAutoModeGateEnabled() }
? { isAutoModeAvailable: true }
: {}),
},
rulesFromDisk,
@@ -1076,131 +1061,54 @@ export function getAutoModeUnavailableNotification(
* kicking the user out of a mode they've already left during the await.
*/
export async function verifyAutoModeGateAccess(
currentContext: ToolPermissionContext,
_currentContext: ToolPermissionContext,
// Runtime AppState.fastMode — passed from callers with AppState access so
// the disableFastMode circuit breaker reads current state, not stale
// settings.fastMode (which is intentionally sticky across /model auto-
// downgrades). Optional for callers without AppState (e.g. SDK init paths).
fastMode?: boolean,
): Promise<AutoModeGateCheckResult> {
// Auto-mode config — runs in ALL builds (circuit breaker, carousel, kick-out)
// Fresh read of tengu_auto_mode_config.enabled — this async check runs once
// after GrowthBook initialization and is the authoritative source for
// isAutoModeAvailable. The sync startup path uses stale cache; this
// corrects it. Circuit breaker (enabled==='disabled') takes effect here.
// Only fast-mode circuit breaker remains. All other gates (GrowthBook,
// settings, model support, opt-in) have been removed.
const autoModeConfig = await getDynamicConfig_BLOCKS_ON_INIT<{
enabled?: AutoModeEnabledState
disableFastMode?: boolean
}>('tengu_auto_mode_config', {})
const enabledState = parseAutoModeEnabledState(autoModeConfig?.enabled)
const disabledBySettings = isAutoModeDisabledBySettings()
// Treat settings-disable the same as GrowthBook 'disabled' for circuit-breaker
// semantics — blocks SDK/explicit re-entry via isAutoModeGateEnabled().
autoModeStateModule?.setAutoModeCircuitBroken(
enabledState === 'disabled' || disabledBySettings,
)
// Carousel availability: not circuit-broken, not disabled-by-settings,
// model supports it, disableFastMode breaker not firing, and (enabled or opted-in)
const mainModel = getMainLoopModel()
// Temp circuit breaker: tengu_auto_mode_config.disableFastMode blocks auto
// mode when fast mode is on. Checks runtime AppState.fastMode (if provided)
// and, for ants, model name '-fast' substring (ant-internal fast models
// like capybara-v2-fast[1m] encode speed in the model ID itself).
// Remove once auto+fast mode interaction is validated.
const disableFastModeBreakerFires =
!!autoModeConfig?.disableFastMode &&
(!!fastMode ||
(process.env.USER_TYPE === 'ant' &&
mainModel.toLowerCase().includes('-fast')))
const modelSupported =
modelSupportsAutoMode(mainModel) && !disableFastModeBreakerFires
let carouselAvailable = false
if (enabledState !== 'disabled' && !disabledBySettings && modelSupported) {
carouselAvailable =
enabledState === 'enabled' || hasAutoModeOptInAnySource()
}
// canEnterAuto gates explicit entry (--permission-mode auto, defaultMode: auto)
// — explicit entry IS an opt-in, so we only block on circuit breaker + settings + model
const canEnterAuto =
enabledState !== 'disabled' && !disabledBySettings && modelSupported
// If fast-mode breaker fires, circuit-break auto mode
autoModeStateModule?.setAutoModeCircuitBroken(disableFastModeBreakerFires)
logForDebugging(
`[auto-mode] verifyAutoModeGateAccess: enabledState=${enabledState} disabledBySettings=${disabledBySettings} model=${mainModel} modelSupported=${modelSupported} disableFastModeBreakerFires=${disableFastModeBreakerFires} carouselAvailable=${carouselAvailable} canEnterAuto=${canEnterAuto}`,
`[auto-mode] verifyAutoModeGateAccess: disableFastModeBreakerFires=${disableFastModeBreakerFires}`,
)
// Capture CLI-flag intent now (doesn't depend on context).
const autoModeFlagCli = autoModeStateModule?.getAutoModeFlagCli() ?? false
// Return a transform function that re-evaluates context-dependent conditions
// against the CURRENT context at setAppState time. The async GrowthBook
// results above (canEnterAuto, carouselAvailable, enabledState, reason) are
// closure-captured — those don't depend on context. But mode, prePlanMode,
// and isAutoModeAvailable checks MUST use the fresh ctx or a mid-await
// shift-tab gets reverted (or worse, the user stays in auto despite the
// circuit breaker if they entered auto DURING the await — which is possible
// because setAutoModeCircuitBroken above runs AFTER the await).
const setAvailable = (
ctx: ToolPermissionContext,
available: boolean,
): ToolPermissionContext => {
if (ctx.isAutoModeAvailable !== available) {
logForDebugging(
`[auto-mode] verifyAutoModeGateAccess setAvailable: ${ctx.isAutoModeAvailable} -> ${available}`,
)
}
return ctx.isAutoModeAvailable === available
? ctx
: { ...ctx, isAutoModeAvailable: available }
if (!disableFastModeBreakerFires) {
// Auto mode available — no kick-out needed
return { updateContext: ctx => ctx }
}
if (canEnterAuto) {
return { updateContext: ctx => setAvailable(ctx, carouselAvailable) }
}
// Fast-mode breaker fired — kick out of auto if currently in it
const notification = getAutoModeUnavailableNotification('circuit-breaker')
// Gate is off or circuit-broken — determine reason (context-independent).
let reason: AutoModeUnavailableReason
if (disabledBySettings) {
reason = 'settings'
logForDebugging('auto mode disabled: disableAutoMode in settings', {
level: 'warn',
})
} else if (enabledState === 'disabled') {
reason = 'circuit-breaker'
logForDebugging(
'auto mode disabled: tengu_auto_mode_config.enabled === "disabled" (circuit breaker)',
{ level: 'warn' },
)
} else {
reason = 'model'
logForDebugging(
`auto mode disabled: model ${getMainLoopModel()} does not support auto mode`,
{ level: 'warn' },
)
}
const notification = getAutoModeUnavailableNotification(reason)
// Unified kick-out transform. Re-checks the FRESH ctx and only fires
// side effects (setAutoModeActive(false), setNeedsAutoModeExitAttachment)
// when the kick-out actually applies. This keeps autoModeActive in sync
// with toolPermissionContext.mode even if the user changed modes during
// the await: if they already left auto on their own, handleCycleMode
// already deactivated the classifier and we don't fire again; if they
// ENTERED auto during the await (possible before setAutoModeCircuitBroken
// landed), we kick them out here.
const kickOutOfAutoIfNeeded = (
ctx: ToolPermissionContext,
): ToolPermissionContext => {
const inAuto = ctx.mode === 'auto'
logForDebugging(
`[auto-mode] kickOutOfAutoIfNeeded applying: ctx.mode=${ctx.mode} ctx.prePlanMode=${ctx.prePlanMode} reason=${reason}`,
`[auto-mode] kickOutOfAutoIfNeeded (fast-mode): ctx.mode=${ctx.mode}`,
)
// Plan mode with auto active: either from prePlanMode='auto' (entered
// from auto) or from opt-in (strippedDangerousRules present).
const inPlanWithAutoActive =
ctx.mode === 'plan' &&
(ctx.prePlanMode === 'auto' || !!ctx.strippedDangerousRules)
if (!inAuto && !inPlanWithAutoActive) {
return setAvailable(ctx, false)
return { ...ctx, isAutoModeAvailable: false }
}
if (inAuto) {
autoModeStateModule?.setAutoModeActive(false)
@@ -1214,8 +1122,6 @@ export async function verifyAutoModeGateAccess(
isAutoModeAvailable: false,
}
}
// Plan with auto active: deactivate auto, restore permissions, defuse
// prePlanMode so ExitPlanMode goes to default.
autoModeStateModule?.setAutoModeActive(false)
setNeedsAutoModeExitAttachment(true)
return {
@@ -1225,65 +1131,23 @@ export async function verifyAutoModeGateAccess(
}
}
// Notification decisions use the stale context — that's OK: we're deciding
// WHETHER to notify based on what the user WAS doing when this check started.
// (Side effects and mode mutation are decided inside the transform above,
// against the fresh ctx.)
const wasInAuto = currentContext.mode === 'auto'
// Auto was used during plan: entered from auto or opt-in auto active
const autoActiveDuringPlan =
currentContext.mode === 'plan' &&
(currentContext.prePlanMode === 'auto' ||
!!currentContext.strippedDangerousRules)
const wantedAuto = wasInAuto || autoActiveDuringPlan || autoModeFlagCli
if (!wantedAuto) {
// User didn't want auto at call time — no notification. But still apply
// the full kick-out transform: if they shift-tabbed INTO auto during the
// await (before setAutoModeCircuitBroken landed), we need to evict them.
return { updateContext: kickOutOfAutoIfNeeded }
}
if (wasInAuto || autoActiveDuringPlan) {
// User was in auto or had auto active during plan — kick out + notify.
return { updateContext: kickOutOfAutoIfNeeded, notification }
}
// autoModeFlagCli only: defaultMode was auto but sync check rejected it.
// Suppress notification if isAutoModeAvailable is already false (already
// notified on a prior check; prevents repeat notifications on successive
// unsupported-model switches).
return {
updateContext: kickOutOfAutoIfNeeded,
notification: currentContext.isAutoModeAvailable ? notification : undefined,
}
return { updateContext: kickOutOfAutoIfNeeded, notification }
}
/**
* Core logic to check if bypassPermissions should be disabled based on Statsig gate
* Bypass permissions is always available — no remote gate check needed.
*/
export function shouldDisableBypassPermissions(): Promise<boolean> {
return checkSecurityRestrictionGate('tengu_disable_bypass_permissions_mode')
}
function isAutoModeDisabledBySettings(): boolean {
const settings = getSettings_DEPRECATED() || {}
return (
(settings as { disableAutoMode?: 'disable' }).disableAutoMode ===
'disable' ||
(settings.permissions as { disableAutoMode?: 'disable' } | undefined)
?.disableAutoMode === 'disable'
)
return Promise.resolve(false)
}
/**
* Checks if auto mode can be entered: circuit breaker is not active and settings
* have not disabled it. Synchronous.
* Checks if auto mode can be entered: only fast-mode circuit breaker remains.
* Synchronous.
*/
export function isAutoModeGateEnabled(): boolean {
// Auto mode is available to all users — only fast-mode circuit breaker remains
if (autoModeStateModule?.isAutoModeCircuitBroken() ?? false) return false
if (isAutoModeDisabledBySettings()) return false
if (!modelSupportsAutoMode(getMainLoopModel())) return false
return true
}
@@ -1292,11 +1156,9 @@ export function isAutoModeGateEnabled(): boolean {
* Synchronous — uses state populated by verifyAutoModeGateAccess.
*/
export function getAutoModeUnavailableReason(): AutoModeUnavailableReason | null {
if (isAutoModeDisabledBySettings()) return 'settings'
if (autoModeStateModule?.isAutoModeCircuitBroken() ?? false) {
return 'circuit-breaker'
}
if (!modelSupportsAutoMode(getMainLoopModel())) return 'model'
return null
}
@@ -1310,8 +1172,7 @@ export function getAutoModeUnavailableReason(): AutoModeUnavailableReason | null
*/
export type AutoModeEnabledState = 'enabled' | 'disabled' | 'opt-in'
const AUTO_MODE_ENABLED_DEFAULT: AutoModeEnabledState =
feature('TRANSCRIPT_CLASSIFIER') ? 'enabled' : 'disabled'
const AUTO_MODE_ENABLED_DEFAULT: AutoModeEnabledState = 'enabled'
function parseAutoModeEnabledState(value: unknown): AutoModeEnabledState {
if (value === 'enabled' || value === 'disabled' || value === 'opt-in') {
@@ -1361,27 +1222,15 @@ export function getAutoModeEnabledStateIfCached():
* dialog or by IDE/Desktop settings toggle)
*/
export function hasAutoModeOptInAnySource(): boolean {
if (autoModeStateModule?.getAutoModeFlagCli() ?? false) return true
return hasAutoModeOptIn()
return true
}
/**
* Checks if bypassPermissions mode is currently disabled by Statsig gate or settings.
* This is a synchronous version that uses cached Statsig values.
* Always returns false — bypass is available to all users.
*/
export function isBypassPermissionsModeDisabled(): boolean {
const growthBookDisableBypassPermissionsMode =
checkStatsigFeatureGate_CACHED_MAY_BE_STALE(
'tengu_disable_bypass_permissions_mode',
)
const settings = getSettings_DEPRECATED() || {}
const settingsDisableBypassPermissionsMode =
settings.permissions?.disableBypassPermissionsMode === 'disable'
return (
growthBookDisableBypassPermissionsMode ||
settingsDisableBypassPermissionsMode
)
return false
}
/**
@@ -1406,29 +1255,12 @@ export function createDisabledBypassPermissionsContext(
}
/**
* Asynchronously checks if the bypassPermissions mode should be disabled based on Statsig gate
* and returns an updated toolPermissionContext if needed
* No-op — bypass permissions is always available, no remote gate check needed.
*/
export async function checkAndDisableBypassPermissions(
currentContext: ToolPermissionContext,
_currentContext: ToolPermissionContext,
): Promise<void> {
// Only proceed if bypassPermissions mode is available
if (!currentContext.isBypassPermissionsModeAvailable) {
return
}
const shouldDisable = await shouldDisableBypassPermissions()
if (!shouldDisable) {
return
}
// Gate is enabled, need to disable bypassPermissions mode
logForDebugging(
'bypassPermissions mode is being disabled by Statsig gate (async check)',
{ level: 'warn' },
)
void gracefulShutdown(1, 'bypass_permissions_disabled')
// Bypass permissions is always available — no gate check needed
}
export function isDefaultPermissionModeAuto(): boolean {
@@ -1446,11 +1278,7 @@ export function isDefaultPermissionModeAuto(): boolean {
*/
export function shouldPlanUseAutoMode(): boolean {
if (feature('TRANSCRIPT_CLASSIFIER')) {
return (
hasAutoModeOptIn() &&
isAutoModeGateEnabled() &&
getUseAutoModeDuringPlan()
)
return isAutoModeGateEnabled() && getUseAutoModeDuringPlan()
}
return false
}

View File

@@ -690,12 +690,16 @@ export const hasPermissionsToUseTool: CanUseToolFn = async (
setClassifierChecking(toolUseID)
let classifierResult
try {
logForDebugging(
`[auto-mode] classifyYoloAction called with langfuseTrace=${context.langfuseTrace ? `id=${(context.langfuseTrace as unknown as Record<string, unknown>).id ?? 'present'}` : 'null/undefined'}`,
)
classifierResult = await classifyYoloAction(
context.messages,
action,
context.options.tools,
appState.toolPermissionContext,
context.abortController.signal,
context.langfuseRootTrace ?? context.langfuseTrace,
)
} finally {
clearClassifierChecking(toolUseID)
@@ -850,12 +854,30 @@ export const hasPermissionsToUseTool: CanUseToolFn = async (
CLASSIFIER_FAIL_CLOSED_REFRESH_MS,
)
) {
if (appState.toolPermissionContext.shouldAvoidPermissionPrompts) {
logForDebugging(
'Auto mode classifier unavailable, denying with retry guidance (fail closed)',
{ level: 'warn' },
)
return {
behavior: 'deny',
decisionReason: {
type: 'classifier',
classifier: 'auto-mode',
reason: 'Classifier unavailable',
},
message: buildClassifierUnavailableMessage(
tool.name,
classifierResult.model,
),
}
}
logForDebugging(
'Auto mode classifier unavailable, denying with retry guidance (fail closed)',
'Auto mode classifier unavailable, falling back to prompting with retry guidance (fail closed)',
{ level: 'warn' },
)
return {
behavior: 'deny',
behavior: 'ask',
decisionReason: {
type: 'classifier',
classifier: 'auto-mode',

View File

@@ -28,9 +28,11 @@ import { errorMessage } from '../errors.js'
import { lazySchema } from '../lazySchema.js'
import { extractTextContent } from '../messages.js'
import { resolveAntModel } from '../model/antModels.js'
import { getMainLoopModel } from '../model/model.js'
import { getDefaultSonnetModel, getMainLoopModel } from '../model/model.js'
import { isPoorModeActive } from '../../commands/poor/poorMode.js'
import { getAutoModeConfig } from '../settings/settings.js'
import { sideQuery } from '../sideQuery.js'
import type { LangfuseSpan } from '../../services/langfuse/index.js'
import { jsonStringify } from '../slowOperations.js'
import { tokenCountWithEstimation } from '../tokens.js'
import {
@@ -731,6 +733,7 @@ async function classifyYoloActionXml(
action: string
},
mode: TwoStageMode,
parentSpan?: LangfuseSpan | null,
): Promise<YoloClassifierResult> {
const classifierType =
mode === 'both'
@@ -791,6 +794,7 @@ async function classifyYoloActionXml(
signal,
...(mode !== 'fast' && { stop_sequences: ['</block>'] }),
querySource: 'auto_mode',
parentSpan,
}
const stage1Raw = await sideQuery(stage1Opts)
stage1DurationMs = Date.now() - stage1Start
@@ -877,6 +881,7 @@ async function classifyYoloActionXml(
maxRetries: getDefaultMaxRetries(),
signal,
querySource: 'auto_mode' as const,
parentSpan,
}
const stage2Raw = await sideQuery(stage2Opts)
const stage2DurationMs = Date.now() - stage2Start
@@ -1015,6 +1020,7 @@ export async function classifyYoloAction(
tools: Tools,
context: ToolPermissionContext,
signal: AbortSignal,
parentSpan?: LangfuseSpan | null,
): Promise<YoloClassifierResult> {
const lookup = buildToolLookup(tools)
const actionCompact = toCompact(action, lookup)
@@ -1126,6 +1132,7 @@ export async function classifyYoloAction(
action: actionCompact,
},
getTwoStageMode(),
parentSpan,
)
}
const [disableThinking, thinkingPadding] = getClassifierThinkingConfig(model)
@@ -1156,6 +1163,7 @@ export async function classifyYoloAction(
maxRetries: getDefaultMaxRetries(),
signal,
querySource: 'auto_mode' as const,
parentSpan,
}
const result = await sideQuery(sideQueryOpts)
void maybeDumpAutoMode(sideQueryOpts, result, start)
@@ -1343,6 +1351,10 @@ function getClassifierModel(): string {
if (config?.model) {
return config.model
}
// Poor mode: downgrade classifier to Sonnet to reduce cost
if (isPoorModeActive()) {
return getDefaultSonnetModel()
}
return getMainLoopModel()
}

View File

@@ -894,20 +894,8 @@ export function hasSkipDangerousModePermissionPrompt(): boolean {
* a malicious project could otherwise auto-bypass the dialog (RCE risk).
*/
export function hasAutoModeOptIn(): boolean {
if (feature('TRANSCRIPT_CLASSIFIER')) {
const user = getSettingsForSource('userSettings')?.skipAutoPermissionPrompt
const local =
getSettingsForSource('localSettings')?.skipAutoPermissionPrompt
const flag = getSettingsForSource('flagSettings')?.skipAutoPermissionPrompt
const policy =
getSettingsForSource('policySettings')?.skipAutoPermissionPrompt
const result = !!(user || local || flag || policy)
logForDebugging(
`[auto-mode] hasAutoModeOptIn=${result} skipAutoPermissionPrompt: user=${user} local=${local} flag=${flag} policy=${policy}`,
)
return result
}
return false
// Auto mode is available to all users — no opt-in needed
return true
}
/**

View File

@@ -2,6 +2,7 @@ import type Anthropic from '@anthropic-ai/sdk'
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages.js'
import {
getLastApiCompletionTimestamp,
getSessionId,
setLastApiCompletionTimestamp,
} from '../bootstrap/state.js'
import { STRUCTURED_OUTPUTS_BETA_HEADER } from '../constants/betas.js'
@@ -14,8 +15,14 @@ import { logEvent } from '../services/analytics/index.js'
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics/metadata.js'
import { getAPIMetadata } from '../services/api/claude.js'
import { getAnthropicClient } from '../services/api/client.js'
import { createTrace, createChildSpan, endTrace, recordLLMObservation } from '../services/langfuse/index.js'
import type { LangfuseSpan } from '../services/langfuse/index.js'
import { convertMessagesToLangfuse, convertOutputToLangfuse, convertToolsToLangfuse } from '../services/langfuse/convert.js'
import { getModelBetas, modelSupportsStructuredOutputs } from './betas.js'
import { logForDebugging } from './debug.js'
import { errorMessage } from './errors.js'
import { computeFingerprint } from './fingerprint.js'
import { getAPIProvider } from './model/providers.js'
import { normalizeModelStringForAPI } from './model/model.js'
type MessageParam = Anthropic.MessageParam
@@ -61,6 +68,11 @@ export type SideQueryOptions = {
stop_sequences?: string[]
/** Attributes this call in tengu_api_success for COGS joining against reporting.sampling_calls. */
querySource: QuerySource
/** Parent Langfuse span to nest this side query under the main agent trace. */
parentSpan?: LangfuseSpan | null
/** When true, API failures are recorded as WARNING instead of ERROR in Langfuse.
* Use for optional/best-effort queries where failure is expected and handled gracefully. */
optional?: boolean
}
/**
@@ -177,25 +189,65 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
}
const normalizedModel = normalizeModelStringForAPI(model)
const provider = getAPIProvider()
const start = Date.now()
// biome-ignore lint/plugin: this IS the wrapper that handles OAuth attribution
const response = await client.beta.messages.create(
{
model: normalizedModel,
max_tokens,
system: systemBlocks,
messages,
...(tools && { tools }),
...(tool_choice && { tool_choice }),
...(output_format && { output_config: { format: output_format } }),
...(temperature !== undefined && { temperature }),
...(stop_sequences && { stop_sequences }),
...(thinkingConfig && { thinking: thinkingConfig }),
...(betas.length > 0 && { betas }),
metadata: getAPIMetadata(),
},
{ signal },
)
const traceName = `side-query:${opts.querySource}`
// When parentSpan is provided, create a child span nested under the
// main agent trace; otherwise create a standalone root trace.
const _ps = opts.parentSpan
// eslint-disable-next-line no-constant-condition
if (opts.querySource === 'auto_mode') {
logForDebugging(
`[sideQuery] auto_mode parentSpan=${_ps ? `id=${(_ps as unknown as Record<string, unknown>).id ?? 'present'}` : 'null/undefined'} querySource=${opts.querySource}`,
)
}
// When parentSpan is provided, create a child span nested under the
// main agent trace. For auto_mode queries, we must always nest under
// a parent span — never create a standalone root trace (agent type),
// as auto_mode observations should appear as spans within the parent.
// For other query sources without a parent, create a standalone trace.
const langfuseTrace = _ps
? createChildSpan(_ps, {
name: traceName,
sessionId: getSessionId(),
model: normalizedModel,
provider,
querySource: opts.querySource,
})
: opts.querySource === 'auto_mode'
? null
: createTrace({
sessionId: getSessionId(),
model: normalizedModel,
provider,
name: traceName,
querySource: opts.querySource,
})
let response: BetaMessage
try {
response = await client.beta.messages.create(
{
model: normalizedModel,
max_tokens,
system: systemBlocks,
messages,
...(tools && { tools }),
...(tool_choice && { tool_choice }),
...(output_format && { output_config: { format: output_format } }),
...(temperature !== undefined && { temperature }),
...(stop_sequences && { stop_sequences }),
...(thinkingConfig && { thinking: thinkingConfig }),
...(betas.length > 0 && { betas }),
metadata: getAPIMetadata(),
},
{ signal },
)
} catch (error) {
endTrace(langfuseTrace, { error: errorMessage(error) }, opts.optional ? 'interrupted' : 'error')
throw error
}
const requestId =
(response as { _request_id?: string | null })._request_id ?? undefined
@@ -218,5 +270,32 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
})
setLastApiCompletionTimestamp(now)
// Record LLM observation in Langfuse (no-op if not configured).
// Wrap SDK types into the internal message format expected by converters.
const wrappedInput = messages.map(m => ({
type: m.role === 'assistant' ? 'assistant' as const : 'user' as const,
message: { role: m.role, content: m.content },
})) as unknown as Parameters<typeof convertMessagesToLangfuse>[0]
const wrappedOutput = [{
type: 'assistant' as const,
message: { role: 'assistant' as const, content: response.content },
}] as unknown as Parameters<typeof convertOutputToLangfuse>[0]
recordLLMObservation(langfuseTrace, {
model: normalizedModel,
provider,
input: convertMessagesToLangfuse(wrappedInput, systemBlocks.length > 0 ? systemBlocks.map(b => b.text) : undefined),
output: convertOutputToLangfuse(wrappedOutput),
usage: {
input_tokens: response.usage.input_tokens,
output_tokens: response.usage.output_tokens,
cache_creation_input_tokens: response.usage.cache_creation_input_tokens ?? undefined,
cache_read_input_tokens: response.usage.cache_read_input_tokens ?? undefined,
},
startTime: new Date(start),
endTime: new Date(),
...(tools && { tools: convertToolsToLangfuse(tools as unknown[]) }),
})
endTrace(langfuseTrace)
return response
}

View File

@@ -150,9 +150,17 @@ export function getCurrentUsage(messages: Message[]): {
const message = messages[i]
const usage = message ? getTokenUsage(message) : undefined
if (usage) {
const inputTokens =
(usage.input_tokens ?? 0) +
(usage.cache_creation_input_tokens ?? 0) +
(usage.cache_read_input_tokens ?? 0)
// Skip placeholder usage (all zeros) — third-party APIs may emit
// message_start without real usage data, causing the context counter
// to flash to 0. Fall through to the previous message instead.
if (inputTokens === 0 && (usage.output_tokens ?? 0) === 0) continue
return {
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
input_tokens: usage.input_tokens ?? 0,
output_tokens: usage.output_tokens ?? 0,
cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,
}

View File

@@ -64,15 +64,6 @@ export default defineConfig({
chunkFileNames: "chunks/[name]-[hash].js",
},
// Externalize native addon packages (they contain .node binaries)
external: [
/audio-capture-napi/,
/color-diff-napi/,
/image-processor-napi/,
/modifiers-napi/,
/url-handler-napi/,
],
plugins: [
rawAssetPlugin([".md", ".txt", ".html", ".css"]),
featureFlagsPlugin(),