Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 121 additions & 50 deletions src/patches/agentsMd.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,29 @@
// Please see the note about writing patches in ./index

import { showDiff } from './index';
import { escapeIdent, showDiff } from './index';

/**
* Patches the CLAUDE.md file reading function to also check for alternative
* filenames (e.g., AGENTS.md) when CLAUDE.md doesn't exist.
*
* This finds the function that reads CLAUDE.md files and modifies it to:
* 1. Add a `didReroute` parameter to the function
* 2. At the early `return null` (when the file doesn't exist), check if the
* path ends with CLAUDE.md and try alternative names (unless didReroute
* is true)
* 3. Recursive calls pass didReroute=true to avoid infinite loops
* Supports two code shapes:
*
* CC 2.1.62 (approx. by Claude):
* ```diff
* -function _t7(A, q) {
* +function _t7(A, q, didReroute) {
* try {
* let K = x1();
* - if (!K.existsSync(A) || !K.statSync(A).isFile()) return null;
* + if (!K.existsSync(A) || !K.statSync(A).isFile()) {
* + if (!didReroute && (A.endsWith("/CLAUDE.md") || A.endsWith("\\CLAUDE.md"))) {
* + for (let alt of ["AGENTS.md", "GEMINI.md", "QWEN.md"]) {
* + let altPath = A.slice(0, -9) + alt;
* + if (K.existsSync(altPath) && K.statSync(altPath).isFile())
* + return _t7(altPath, q, true);
* + }
* + }
* + return null;
* + }
* let Y = UL9(A).toLowerCase();
* if (Y && !dL9.has(Y))
* return (I(`Skipping non-text file in @include: ${A}`), null);
* let z = K.readFileSync(A, { encoding: "utf-8" }),
* { content: w, paths: H } = cL9(z);
* return { path: A, type: q, content: w, globs: H };
* } catch (K) {
* if (K instanceof Error && K.message.includes("EACCES"))
* n("tengu_claude_md_permission_error", {
* is_access_error: 1,
* has_home_dir: A.includes(_8()) ? 1 : 0,
* });
* }
* return null;
* }
* ```
* **Sync (CC ≤ 2.1.84):** A single function reads, checks existence, and
* processes the file. We add a `didReroute` parameter and inject the fallback
* at the early `return null`.
*
* **Async (CC ≥ 2.1.85):** The function was split into three:
* - content processor (has "Skipping non-text file" but no fs ops)
* - async reader (calls `readFile`, then the content processor)
* - error handler (ENOENT / EISDIR / EACCES)
* We patch the *async reader* instead: add `didReroute` and inject the
* fallback in its `catch` block.
*/
export const writeAgentsMd = (
file: string,
altNames: string[]
): string | null => {
// Step 1: Locate the content-processing function via the "Skipping" anchor.
const funcPattern =
/(function ([$\w]+)\(([$\w]+),([^)]+?))\)(?:.|\n){0,500}Skipping non-text file in @include/;

Expand All @@ -59,29 +32,48 @@ export const writeAgentsMd = (
console.error('patch: agentsMd: failed to find CLAUDE.md reading function');
return null;
}

// Step 2: Decide which code shape we're dealing with.
const fsPattern = /([$\w]+(?:\(\))?)\.(?:readFileSync|existsSync|statSync)/;
const fsMatch = funcMatch[0].match(fsPattern);

if (fsMatch) {
// Sync single-function pattern (CC ≤ 2.1.84)
return writeAgentsMdSync(
file,
funcMatch as RegExpMatchArray & { index: number },
fsMatch[1],
altNames
);
}

// Async split-function pattern (CC ≥ 2.1.85)
return writeAgentsMdAsync(file, funcMatch[2], altNames);
};

// ─── Sync strategy (unchanged logic, extracted) ──────────────────────────────

const writeAgentsMdSync = (
file: string,
funcMatch: RegExpMatchArray & { index: number },
fsExpr: string,
altNames: string[]
): string | null => {
const upToFuncParamsClosingParen = funcMatch[1];
const functionName = funcMatch[2];
const firstParam = funcMatch[3];
const restParams = funcMatch[4];
const funcStart = funcMatch.index;

const fsPattern = /([$\w]+(?:\(\))?)\.(?:readFileSync|existsSync|statSync)/;
const fsMatch = funcMatch[0].match(fsPattern);
if (!fsMatch) {
console.error('patch: agentsMd: failed to find fs expression in function');
return null;
}
const fsExpr = fsMatch[1];

const altNamesJson = JSON.stringify(altNames);

// Step 1: Add didReroute parameter to function signature
// Add didReroute parameter to function signature
const sigIndex = funcStart + upToFuncParamsClosingParen.length;
let newFile = file.slice(0, sigIndex) + ',didReroute' + file.slice(sigIndex);

showDiff(file, newFile, ',didReroute', sigIndex, sigIndex);

// Step 2: Inject fallback at the early return null (when file doesn't exist)
// Inject fallback at the early return null (when file doesn't exist)
const funcBody = newFile.slice(funcStart);

// CC ≤2.1.62: existsSync/isFile check before reading
Expand Down Expand Up @@ -119,3 +111,82 @@ export const writeAgentsMd = (

return newFile;
};

// ─── Async strategy (CC ≥ 2.1.85) ───────────────────────────────────────────

const writeAgentsMdAsync = (
file: string,
contentProcessorName: string,
altNames: string[]
): string | null => {
// Find the async reader function:
// async function Fb8(H,$,q){
// try{ let _=await FS.readFile(H,...); return CONTENT_PROC(_,...) }
// catch(K){ return ERR_HANDLER(K,H),{info:null,includePaths:[]} }
// }
const readerPattern = new RegExp(
`(async function ([$\\w]+)\\(([$\\w]+),([^)]+))\\)\\{try\\{` +
`[^}]{0,200}\\.readFile\\(\\3,.{0,100}${escapeIdent(contentProcessorName)}\\(`
);
const readerMatch = file.match(readerPattern);
if (!readerMatch || readerMatch.index === undefined) {
console.error(
'patch: agentsMd: failed to find async CLAUDE.md reader function'
);
return null;
}

const readerSig = readerMatch[1]; // e.g. "async function Fb8(H,$,q"
const readerFuncName = readerMatch[2]; // e.g. "Fb8"
const pathParam = readerMatch[3]; // e.g. "H"
const restParams = readerMatch[4]; // e.g. "$,q"

const altNamesJson = JSON.stringify(altNames);

// Step 1: Add didReroute parameter to the reader's signature.
const sigIndex = readerMatch.index + readerSig.length;
let newFile = file.slice(0, sigIndex) + ',didReroute' + file.slice(sigIndex);

showDiff(file, newFile, ',didReroute', sigIndex, sigIndex);

// Step 2: Replace the catch block's return statement with fallback logic.
// Before: return al4(K,H),{info:null,includePaths:[]}
// After: al4(K,H); if(!didReroute && ...) { try alts } return{info:null,...}
const catchReturnPattern = new RegExp(
`return ([$\\w]+)\\(([$\\w]+),${escapeIdent(pathParam)}\\),\\{info:null,includePaths:\\[\\]\\}`
);

// Search in the vicinity of the async function (in the already-modified file).
const searchStart = readerMatch.index;
const searchSlice = newFile.slice(searchStart, searchStart + 1000);
const catchMatch = searchSlice.match(catchReturnPattern);

if (!catchMatch || catchMatch.index === undefined) {
console.error(
'patch: agentsMd: failed to find catch return in async reader'
);
return null;
}

const errorHandlerName = catchMatch[1]; // e.g. "al4"
const catchVar = catchMatch[2]; // e.g. "K"

const replacement =
`${errorHandlerName}(${catchVar},${pathParam});` +
`if(!didReroute&&(${pathParam}.endsWith("/CLAUDE.md")||${pathParam}.endsWith("\\\\CLAUDE.md"))){` +
`for(let alt of ${altNamesJson}){let altPath=${pathParam}.slice(0,-9)+alt;` +
`let r=await ${readerFuncName}(altPath,${restParams},true);if(r.info)return r}}` +
`return{info:null,includePaths:[]}`;

const catchReturnStart = searchStart + catchMatch.index;
const catchReturnEnd = catchReturnStart + catchMatch[0].length;

newFile =
newFile.slice(0, catchReturnStart) +
replacement +
newFile.slice(catchReturnEnd);

showDiff(file, newFile, replacement, catchReturnStart, catchReturnEnd);

return newFile;
};
6 changes: 3 additions & 3 deletions src/patches/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ export const findTextComponent = (fileContents: string): string | undefined => {
// The minified Text component has this signature:
// function X({color:A,backgroundColor:B,dimColor:C=!1,bold:D=!1,...})
const textComponentPattern =
/\bfunction ([$\w]+).{0,20}color:[$\w]+,backgroundColor:[$\w]+,dimColor:[$\w]+(?:=![01])?,bold:[$\w]+(?:=![01])?/;
/\bfunction ([$\w]+).{0,80}color:[$\w]+,backgroundColor:[$\w]+,dimColor:[$\w]+(?:=![01])?,bold:[$\w]+(?:=![01])?/;
const match = fileContents.match(textComponentPattern);
if (!match) {
console.log('patch: findTextComponent: failed to find text component');
Expand All @@ -311,7 +311,7 @@ export const findTextComponent = (fileContents: string): string | undefined => {
export const findBoxComponent = (fileContents: string): string | undefined => {
// Method 1: Find Box by ink-box createElement with local variable (CC ~2.0.x)
const inkBoxPattern =
/function ([$\w]+)\(.{0,2000}[^$\w]([$\w]+)=[$\w]+(?:\.default)?\.createElement\("ink-box".{0,200}?return \2/;
/function ([$\w]+)\(.{0,2000}[^$\w]([$\w]+)=[$\w]+(?:\.default)?\.createElement\("ink-box".{0,500}?return \2/;
const inkBoxMatch = fileContents.match(inkBoxPattern);
if (inkBoxMatch) {
return inkBoxMatch[1];
Expand All @@ -320,7 +320,7 @@ export const findBoxComponent = (fileContents: string): string | undefined => {
// Method 2: Find Box by direct return of createElement("ink-box"...) (CC 2.1.20+)
// Pattern: function NAME({children:T,...}){...createElement("ink-box",...),T)}
const directReturnPattern =
/function ([$\w]+)\(\{children:[$\w]+,flexWrap:[$\w]+.{0,2000}?\.createElement\("ink-box"/;
/function ([$\w]+)\(.{0,200}children:[$\w]+,flexWrap:[$\w]+.{0,2000}?\.createElement\("ink-box"/;
const directReturnMatch = fileContents.match(directReturnPattern);
if (directReturnMatch) {
return directReturnMatch[1];
Expand Down
5 changes: 4 additions & 1 deletion src/patches/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -838,7 +838,10 @@ export const applyCustomization = async (
},
'mcp-non-blocking': {
fn: c => writeMcpNonBlocking(c),
condition: !!config.settings.misc?.mcpConnectionNonBlocking,
condition:
!!config.settings.misc?.mcpConnectionNonBlocking &&
(ccInstInfo.version == null ||
compareVersions(ccInstInfo.version, '2.1.85') < 0),
},
'mcp-batch-size': {
fn: c => writeMcpBatchSize(c, config.settings.misc!.mcpServerBatchSize!),
Expand Down
53 changes: 42 additions & 11 deletions src/patches/patchesAppliedIndication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,43 @@ export const findVersionOutputLocation = (
};
};

/**
* Find the Claude Code version display pattern.
*
* Supports two shapes:
* - **Inline (pre-React Compiler):**
* `createElement(TEXT,{bold:!0},"Claude Code")," ",createElement(TEXT,{dimColor:!0},"v",VER)`
* - **Memoized (React Compiler, CC ≥ 2.1.85):**
* The bold "Claude Code" is cached in a variable, then referenced in
* `createElement(TEXT,null,CACHED," ",createElement(TEXT,{dimColor:!0},"v",VER))`
*/
const findVersionDisplay = (fileContents: string): RegExpMatchArray | null => {
// Strategy 1: inline pattern
const inlinePattern =
/[^$\w]([$\w]+)\.createElement\(([$\w]+),\{bold:!0\},"Claude Code"\)," ",([$\w]+)\.createElement\(([$\w]+),\{dimColor:!0\},"v",[$\w]+\)/;
const inlineMatch = fileContents.match(inlinePattern);
if (inlineMatch) return inlineMatch;

// Strategy 2: React Compiler memoized
const memoPattern =
/([$\w]+)=([$\w]+)\.createElement\(([$\w]+),\{bold:!0\},"Claude Code"\)/;
const memoMatch = fileContents.match(memoPattern);
if (!memoMatch) return null;

const cachedVar = memoMatch[1];
const versionPattern = new RegExp(
`[^$\\w][$\\w]+\\.createElement\\([$\\w]+,null,${escapeIdent(cachedVar)}," ",[$\\w]+\\.createElement\\([$\\w]+,\\{dimColor:!0\\},"v",[$\\w]+\\)`
);
return fileContents.match(versionPattern);
};

/**
* PATCH 2: Finds the location to insert tweakcc version in the header
*/
const findTweakccVersionLocation = (
fileContents: string
): LocationResult | null => {
// Find Claude Code version display
const pattern =
/[^$\w]([$\w]+)\.createElement\(([$\w]+),\{bold:!0\},"Claude Code"\)," ",([$\w]+)\.createElement\(([$\w]+),\{dimColor:!0\},"v",[$\w]+\)/;
const match = fileContents.match(pattern);
const match = findVersionDisplay(fileContents);
if (!match || match.index === undefined) {
console.error(
'patch: patchesAppliedIndication: failed to find Claude Code version pattern'
Expand Down Expand Up @@ -246,10 +273,8 @@ const applyIndicatorPatchesListPatch = (
const findPatchesListLocation = (
fileContents: string
): LocationResult | null => {
// 1. Find the same regex as patch 2
const pattern =
/[^$\w]([$\w]+)\.createElement\(([$\w]+),\{bold:!0\},"Claude Code"\)," ",([$\w]+)\.createElement\(([$\w]+),\{dimColor:!0\},"v",[$\w]+\)/;
const match = fileContents.match(pattern);
// 1. Find the version display (same helper as patch 2)
const match = findVersionDisplay(fileContents);
if (!match || match.index === undefined) {
console.error(
'patch: patchesAppliedIndication: failed to find Claude Code version pattern for patch 3'
Expand Down Expand Up @@ -475,10 +500,16 @@ export const writePatchesAppliedIndication = (
patchesApplies
);
if (!finalContent) {
console.error('patch: patchesAppliedIndication: patch 5 failed');
return null;
// PATCH 5 can fail on React Compiler builds (CC ≥ 2.1.85) where the
// component tree is flattened into variable assignments, making the
// paren-counting stack machine invalid. Gracefully skip — patches 1-4
// still provide the tweakcc version and header list.
console.warn(
'patch: patchesAppliedIndication: patch 5 skipped (indicator view patches list)'
);
} else {
content = finalContent;
}
content = finalContent;
}

return content;
Expand Down
2 changes: 1 addition & 1 deletion src/patches/userMessageDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export const writeUserMessageDisplay = (
// that renders the ">" in older versions so that we can silently drop it in the replacement,
// removing it in versions where it's present and not failing on versions where it's not.
const pattern =
/(No content found in user prompt message.{0,150}?\b)([$\w]+(?:\.default)?\.createElement.{0,30}\b[$\w]+(?:\.default)?\.createElement.{0,40}">.+?)?(([$\w]+(?:\.default)?\.createElement).{0,100})(\([$\w]+,(?:\{[^{}]+wrap:"wrap"\},([$\w]+)(?:\.trim\(\))?\)\)|\{text:([$\w]+)(?:,thinkingMetadata:[$\w]+)?\}\)\)?))/;
/(No content found in user prompt message.{0,250}?\b)([$\w]+(?:\.default)?\.createElement.{0,30}\b[$\w]+(?:\.default)?\.createElement.{0,40}">.+?)?(([$\w]+(?:\.default)?\.createElement).{0,200})(\([$\w]+,(?:\{[^{}]+wrap:"wrap"\},([$\w]+)(?:\.trim\(\))?\)\)|\{text:([$\w]+)[^}]*\}\)\)?))/;

const match = oldFile.match(pattern);

Expand Down