Files
CherryHQ-cherry-studio/scripts/patch-claude-agent-sdk.ts
SuYao e72bde30eb refactor: replace static pnpm patch with postinstall script for claude-agent-sdk (#13139)
### What this PR does

Before this PR:
`claude-agent-sdk` is patched via a static pnpm `.patch` file that
replaces entire minified lines. This breaks every time the SDK is
upgraded because obfuscated variable names change with each
minification.

After this PR:
A Node.js postinstall script (`scripts/patch-claude-agent-sdk.mjs`) uses
semantic regex patterns to apply the same 3 patches. Since it matches
structural patterns (not variable names), it survives SDK version bumps
as long as the code structure remains the same.

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

The following tradeoffs were made:
- Regex-based patching is slightly less precise than a static `.patch`
file, but far more resilient to minified code changes.
- The script validates all 3 patches applied and exits with error if
patterns don't match, so SDK structure changes are caught immediately.

The following alternatives were considered:
- Keeping the pnpm patch approach — rejected because it requires manual
regeneration on every SDK upgrade.
- Using AST-based patching — rejected as overkill for 3 targeted
replacements in minified code.

### Breaking changes

None. The same 3 modifications are applied (spawn→fork, remove command
destructuring, IPC stdio), just via a different mechanism.

### Special notes for your reviewer

The 3 patches applied by the script:
1. `import{spawn as X}` → `import{fork as X}` — enables IPC channel
2. Remove `command:VAR,` from `spawnLocalProcess` destructuring
3. Rewrite spawn call to `fork(args[0], args.slice(1), ...)` with IPC
stdio, removing `windowsHide:!0`

43 unit tests cover: variable name variations, idempotency, partial
matches, and no-match detection.

### 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: Not required — internal build tooling change
- [x] Self-review: I have reviewed my own code before requesting review
from others

### Release note

```release-note
NONE
```

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:23:49 +08:00

133 lines
4.2 KiB
TypeScript

/**
* Postinstall script to patch @anthropic-ai/claude-agent-sdk
*
* The SDK is shipped as minified/obfuscated code, so we use semantic regex
* patterns (not variable names) to apply patches. This is more resilient
* to SDK version bumps than a static .patch file.
*
* Changes:
* 1. spawn → fork (child_process import) — enables IPC channel
* 2. Remove `command` from spawnLocalProcess destructuring
* 3. Rewrite spawn call to use fork(args[0], args.slice(1), ...) with IPC stdio
*/
import { readFileSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
interface PatchResult {
result: string
matched: boolean
}
interface ApplyAllResult {
result: string
patchCount: number
alreadyPatched: boolean
}
// 1. Replace `import{spawn as X}from"child_process"` with `import{fork as X}from"child_process"`
export function patchSpawnImport(content: string): PatchResult {
let matched = false
const result = content.replace(/import\{spawn as ([\w$]+)\}from"child_process"/, (_, alias) => {
matched = true
return `import{fork as ${alias}}from"child_process"`
})
return { result, matched }
}
// 2. Remove `command:X,` from spawnLocalProcess destructuring
// Before: spawnLocalProcess(Q){let{command:X,args:Y,cwd:$,env:W,signal:J}=Q
// After: spawnLocalProcess(Q){let{args:Y,cwd:$,env:W,signal:J}=Q
export function patchRemoveCommand(content: string): PatchResult {
let matched = false
const result = content.replace(
/spawnLocalProcess\(([\w$]+)\)\{let\{command:([\w$]+),args:([\w$]+)/,
(_, fnArg, _cmd, args) => {
matched = true
return `spawnLocalProcess(${fnArg}){let{args:${args}`
}
)
return { result, matched }
}
// 3. Rewrite the spawn/fork call:
// Before: =Sq(X,Y,{cwd:$,stdio:["pipe","pipe",G],signal:J,env:W,windowsHide:!0})
// After: =Sq(Y[0],Y.slice(1),{cwd:$,stdio:G==="pipe"?["pipe","pipe","pipe","ipc"]:["pipe","pipe","ignore","ipc"],signal:J,env:W})
export function patchSpawnCall(content: string): PatchResult {
let matched = false
const result = content.replace(
/([\w$]+)\(([\w$]+),([\w$]+),\{cwd:([\w$]+),stdio:\["pipe","pipe",([\w$]+)\],signal:([\w$]+),env:([\w$]+),windowsHide:!0\}/,
(_, fn, _cmd, args, cwd, stderr, signal, env) => {
matched = true
return `${fn}(${args}[0],${args}.slice(1),{cwd:${cwd},stdio:${stderr}==="pipe"?["pipe","pipe","pipe","ipc"]:["pipe","pipe","ignore","ipc"],signal:${signal},env:${env}}`
}
)
return { result, matched }
}
// Apply all patches and return summary
export function applyAllPatches(content: string): ApplyAllResult {
let patchCount = 0
const p1 = patchSpawnImport(content)
content = p1.result
if (p1.matched) patchCount++
const p2 = patchRemoveCommand(content)
content = p2.result
if (p2.matched) patchCount++
const p3 = patchSpawnCall(content)
content = p3.result
if (p3.matched) patchCount++
const alreadyPatched = patchCount === 0 && content.includes('import{fork as') && content.includes('"ipc"')
return { result: content, patchCount, alreadyPatched }
}
// --- CLI entry point (skipped when imported by tests) ---
function main() {
const require_ = createRequire(import.meta.url)
let sdkPath: string
try {
sdkPath = path.join(path.dirname(require_.resolve('@anthropic-ai/claude-agent-sdk')), 'sdk.mjs')
} catch {
console.log('[patch-claude-agent-sdk] Package not installed, skipping.')
process.exit(0)
}
let fileContent: string
try {
fileContent = readFileSync(sdkPath, 'utf-8')
} catch {
console.error(`[patch-claude-agent-sdk] Failed to read ${sdkPath}`)
process.exit(1)
}
const { result, patchCount, alreadyPatched } = applyAllPatches(fileContent)
if (patchCount === 0) {
if (alreadyPatched) {
console.log('[patch-claude-agent-sdk] Already patched, skipping.')
process.exit(0)
}
console.error('[patch-claude-agent-sdk] No patterns matched! The SDK structure may have changed.')
process.exit(1)
}
if (patchCount < 3) {
console.warn(`[patch-claude-agent-sdk] Warning: only ${patchCount}/3 patches applied.`)
}
writeFileSync(sdkPath, result, 'utf-8')
console.log(`[patch-claude-agent-sdk] Successfully applied ${patchCount}/3 patches to sdk.mjs`)
}
if (!process.env.VITEST) {
main()
}