Files
CherryHQ-cherry-studio/scripts/before-pack.js
LiuVaayne 4ef98318af feat: integrate rtk for reducing LLM token consumption on agent shell commands (#13615)
### What this PR does

Before this PR:
Agent Bash tool calls output raw, verbose shell command results that
consume excessive LLM tokens.

After this PR:
Bash commands are transparently rewritten via
[rtk](https://github.com/rtk-ai/rtk) to produce concise, LLM-friendly
output — reducing token consumption by 60-90% on common shell commands
(`cat`, `grep`, `find`, `ls`, etc.).

<img width="647" height="578" alt="image"
src="https://github.com/user-attachments/assets/438de126-c79d-4b69-bf3b-65a220671900"
/>



Fixes #13600

### Why we need it and why it was done in this way

rtk is a single Rust binary (MIT licensed, zero dependencies) that
rewrites shell commands into optimized versions. When a command has no
rtk equivalent, it passes through unchanged — zero risk of breaking
existing workflows.

The integration follows three layers:
1. **Build time**: `scripts/download-rtk-binaries.js` downloads
platform-specific rtk and jq binaries into `resources/binaries/`
(bundled via existing `asarUnpack: resources/**`)
2. **First run**: `extractRtkBinaries()` copies binaries from app
resources to `~/.cherrystudio/bin/` (follows existing binary
distribution pattern used by bun, uv, openclaw)
3. **Runtime**: A `PreToolUse` hook in the Claude Code service
intercepts Bash tool calls, runs `rtk rewrite "<command>"`, and
substitutes the optimized command if available

The following tradeoffs were made:
- Binaries are bundled in the app package (increases app size ~5MB)
rather than downloaded on demand — ensures rtk is always available
without network dependency
- jq is bundled alongside rtk for potential external hook script usage,
even though the TypeScript hook doesn't need it
- `rtkRewrite()` is async (non-blocking) to avoid stalling the main
process event loop

The following alternatives were considered:
- On-demand download (like bun/uv install scripts) — rejected because
rtk should "always be on" per requirements
- System PATH detection only — rejected because it requires users to
install rtk manually

### Breaking changes

None. The rtk rewrite is transparent and falls back gracefully when rtk
is unavailable or a command has no optimized version.

### Special notes for your reviewer

- The download script is non-fatal: if binary download fails during
build, the build continues without rtk
- The hook runs before the existing `preToolUseHook` (permission
handling), so commands are rewritten before permission checks
- Version guard requires rtk >= 0.23.0 (when `rtk rewrite` subcommand
was introduced)

### Checklist

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: Write code that humans can understand and Keep it simple
- [x] Refactor: You have left the code cleaner than you found it (Boy
Scout Rule)
- [ ] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [ ] Documentation: A user-guide update was considered and is present
(link) or not required
- [x] Self-review: I have reviewed my own code before requesting review
from others

### Release note

```release-note
Integrate rtk to automatically optimize agent shell commands for 60-90% token savings
```

---------

Signed-off-by: Vaayne <liu.vaayne@gmail.com>
2026-03-30 11:13:35 +08:00

150 lines
5.6 KiB
JavaScript

const { Arch } = require('electron-builder')
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')
const { parse, stringify } = require('yaml')
const workspaceConfigPath = path.join(__dirname, '..', 'pnpm-workspace.yaml')
// if you want to add new prebuild binaries packages with different architectures, you can add them here
// please add to allX64 and allArm64 from pnpm-lock.yaml
const packages = [
'@img/sharp-darwin-arm64',
'@img/sharp-darwin-x64',
'@img/sharp-libvips-darwin-arm64',
'@img/sharp-libvips-darwin-x64',
'@img/sharp-libvips-linux-arm64',
'@img/sharp-libvips-linuxmusl-arm64',
'@img/sharp-libvips-linux-x64',
'@img/sharp-libvips-linuxmusl-x64',
'@img/sharp-linux-arm64',
'@img/sharp-linux-x64',
'@img/sharp-linuxmusl-arm64',
'@img/sharp-linuxmusl-x64',
'@img/sharp-win32-arm64',
'@img/sharp-win32-x64',
'@libsql/darwin-arm64',
'@libsql/darwin-x64',
'@libsql/linux-arm64-gnu',
'@libsql/linux-x64-gnu',
'@libsql/linux-arm64-musl',
'@libsql/linux-x64-musl',
'@libsql/win32-x64-msvc',
'@napi-rs/system-ocr-darwin-arm64',
'@napi-rs/system-ocr-darwin-x64',
'@napi-rs/system-ocr-win32-arm64-msvc',
'@napi-rs/system-ocr-win32-x64-msvc',
'@napi-rs/canvas-linux-x64-gnu',
'@napi-rs/canvas-linux-x64-musl',
'@napi-rs/canvas-linux-arm64-gnu',
'@napi-rs/canvas-linux-arm64-musl',
'@napi-rs/canvas-darwin-x64',
'@napi-rs/canvas-darwin-arm64',
'@napi-rs/canvas-win32-x64-msvc',
'@napi-rs/canvas-win32-arm64-msvc',
'@strongtz/win32-arm64-msvc'
]
const platformToArch = {
mac: 'darwin',
windows: 'win32',
linux: 'linux',
linuxmusl: 'linuxmusl'
}
exports.default = async function (context) {
const arch = context.arch === Arch.arm64 ? 'arm64' : 'x64'
const platformName = context.packager.platform.name
const platform = platformToArch[platformName]
// Download rtk binary for the target platform
try {
console.log(`Downloading rtk binary for ${platform}-${arch}...`)
execSync(`node "${path.join(__dirname, 'download-rtk-binaries.js')}" ${platform} ${arch}`, { stdio: 'inherit' })
} catch (error) {
console.warn(`Warning: rtk binary download failed (non-fatal): ${error.message}`)
}
const downloadPackages = async () => {
// Skip if target platform and architecture match current system
if (platform === process.platform && arch === process.arch) {
console.log(`Skipping install: target (${platform}/${arch}) matches current system`)
return
}
console.log(`Installing packages for target platform=${platform} arch=${arch}...`)
// Backup and modify pnpm-workspace.yaml to add target platform support
const originalWorkspaceConfig = fs.readFileSync(workspaceConfigPath, 'utf-8')
const workspaceConfig = parse(originalWorkspaceConfig)
// Add target platform to supportedArchitectures.os
if (!workspaceConfig.supportedArchitectures.os.includes(platform)) {
workspaceConfig.supportedArchitectures.os.push(platform)
}
// Add target architecture to supportedArchitectures.cpu
if (!workspaceConfig.supportedArchitectures.cpu.includes(arch)) {
workspaceConfig.supportedArchitectures.cpu.push(arch)
}
const modifiedWorkspaceConfig = stringify(workspaceConfig)
console.log('Modified workspace config:', modifiedWorkspaceConfig)
fs.writeFileSync(workspaceConfigPath, modifiedWorkspaceConfig)
try {
execSync(`pnpm install`, { stdio: 'inherit' })
} finally {
// Restore original pnpm-workspace.yaml
fs.writeFileSync(workspaceConfigPath, originalWorkspaceConfig)
}
}
await downloadPackages()
const excludePackages = async (packagesToExclude) => {
// 从项目根目录的 electron-builder.yml 读取 files 配置,避免多次覆盖配置导致出错
const electronBuilderConfigPath = path.join(__dirname, '..', 'electron-builder.yml')
const electronBuilderConfig = parse(fs.readFileSync(electronBuilderConfigPath, 'utf-8'))
let filters = electronBuilderConfig.files
// add filters for other architectures (exclude them)
filters.push(...packagesToExclude)
context.packager.config.files[0].filter = filters
}
const arm64KeepPackages = packages.filter((p) => p.includes('arm64') && p.includes(platform))
const arm64ExcludePackages = packages
.filter((p) => !arm64KeepPackages.includes(p))
.map((p) => '!node_modules/' + p + '/**')
const x64KeepPackages = packages.filter((p) => p.includes('x64') && p.includes(platform))
const x64ExcludePackages = packages
.filter((p) => !x64KeepPackages.includes(p))
.map((p) => '!node_modules/' + p + '/**')
const excludeRipgrepFilters = ['arm64-darwin', 'arm64-linux', 'x64-darwin', 'x64-linux', 'x64-win32']
.filter((f) => {
// On Windows ARM64, also keep x64-win32 for emulation compatibility
if (platform === 'win32' && context.arch === Arch.arm64 && f === 'x64-win32') {
return false
}
return f !== `${arch}-${platform}`
})
.map((f) => '!node_modules/@anthropic-ai/claude-agent-sdk/vendor/ripgrep/' + f + '/**')
// Exclude rtk binaries for other platform-arch combinations
const currentPlatformKey = `${platform}-${arch}`
const allRtkPlatforms = ['darwin-arm64', 'darwin-x64', 'linux-x64', 'linux-arm64', 'win32-x64']
const excludeRtkFilters = allRtkPlatforms
.filter((p) => p !== currentPlatformKey)
.map((p) => '!resources/binaries/' + p + '/**')
if (context.arch === Arch.arm64) {
await excludePackages([...arm64ExcludePackages, ...excludeRipgrepFilters, ...excludeRtkFilters])
} else {
await excludePackages([...x64ExcludePackages, ...excludeRipgrepFilters, ...excludeRtkFilters])
}
}