Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
138 changes: 102 additions & 36 deletions src/patches/agentsMd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,108 @@ import { 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 patterns across CC versions:
*
* CC 2.1.62 (approx. by Claude):
* CC <=2.1.69 (sync): Function uses readFileSync/existsSync/statSync directly
* CC >=2.1.83 (async): File reading is split into jh1 (async reader) and XB9 (processor)
* The async reader catches ENOENT/EISDIR errors and returns {info:null,includePaths:[]}
*
* CC <=2.1.69:
* ```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);
* + }
* + }
* + if (!didReroute && (A.endsWith("/CLAUDE.md") || ...)) { ... }
* + 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;
* }
* ```
*
* CC >=2.1.83:
* ```diff
* -async function jh1(A, q, K) {
* +async function jh1(A, q, K, didReroute) {
* try {
* let z = await j8().readFile(A, {encoding:"utf-8"});
* return XB9(z, A, q, K)
* - } catch(_) { return DB9(_, A), {info:null,includePaths:[]} }
* + } catch(_) {
* + DB9(_, A);
* + if (!didReroute && (A.endsWith("/CLAUDE.md") || ...)) {
* + for (let alt of ["AGENTS.md",...]) {
* + let altPath = A.slice(0,-9) + alt;
* + try { let r = await jh1(altPath, q, K, true); if (r.info) return r; } catch {}
* + }
* + }
* + return {info:null,includePaths:[]}
* + }
* ```
*/
export const writeAgentsMd = (
file: string,
altNames: string[]
): string | null => {
// Try the new async pattern first (CC >=2.1.83)
const asyncResult = writeAgentsMdAsync(file, altNames);
if (asyncResult) return asyncResult;

// Fall back to the old sync pattern (CC <=2.1.69)
return writeAgentsMdSync(file, altNames);
};

const writeAgentsMdAsync = (
file: string,
altNames: string[]
): string | null => {
// Match the async reader function that:
// 1. Contains readFile (async)
// 2. Has a catch block that calls a function with error code checks (ENOENT/EISDIR)
// 3. Returns {info:null,includePaths:[]}
const funcPattern =
/(async function ([$\w]+)\(([$\w]+),([$\w]+),([$\w]+))\)\{try\{let ([$\w]+)=await ([$\w]+)\(\)\.readFile\(\3,\{encoding:"utf-8"\}\);return ([$\w]+)\(\6,\3,\4,\5\)\}catch\(([$\w]+)\)\{return ([$\w]+)\(\9,\3\),\{info:null,includePaths:\[\]\}\}\}/;

const funcMatch = file.match(funcPattern);
if (!funcMatch || funcMatch.index === undefined) {
return null;
}

const fullMatch = funcMatch[0];
const funcSig = funcMatch[1]; // async function NAME(A,q,K
const funcName = funcMatch[2]; // jh1
const pathParam = funcMatch[3]; // A
const typeParam = funcMatch[4]; // q
const thirdParam = funcMatch[5]; // K
const readVar = funcMatch[6]; // z
const fsGetter = funcMatch[7]; // j8
const processorFunc = funcMatch[8]; // XB9
const catchVar = funcMatch[9]; // _
const errorHandler = funcMatch[10]; // DB9

const altNamesJson = JSON.stringify(altNames);

const replacement =
`${funcSig},didReroute){try{let ${readVar}=await ${fsGetter}().readFile(${pathParam},{encoding:"utf-8"});return ${processorFunc}(${readVar},${pathParam},${typeParam},${thirdParam})}catch(${catchVar}){${errorHandler}(${catchVar},${pathParam});` +
`if(!didReroute&&(${pathParam}.endsWith("/CLAUDE.md")||${pathParam}.endsWith("\\\\CLAUDE.md"))){` +
`for(let alt of ${altNamesJson}){` +
`let altPath=${pathParam}.slice(0,-9)+alt;` +
`try{let r=await ${funcName}(altPath,${typeParam},${thirdParam},true);if(r.info)return r}catch{}` +
`}}` +
`return{info:null,includePaths:[]}}}`;

const startIndex = funcMatch.index;
const endIndex = startIndex + fullMatch.length;

const newFile =
file.slice(0, startIndex) + replacement + file.slice(endIndex);

showDiff(file, newFile, replacement, startIndex, endIndex);

return newFile;
};

const writeAgentsMdSync = (file: string, altNames: string[]): string | null => {
const funcPattern =
/(function ([$\w]+)\(([$\w]+),([^)]+?))\)(?:.|\n){0,500}Skipping non-text file in @include/;

Expand All @@ -67,26 +124,35 @@ export const writeAgentsMd = (

const fsPattern = /([$\w]+(?:\(\))?)\.(?:readFileSync|existsSync|statSync)/;
const fsMatch = funcMatch[0].match(fsPattern);
let callerFsMatch: RegExpMatchArray | null = null;
if (!fsMatch) {
console.error('patch: agentsMd: failed to find fs expression in function');
return null;
// Try the caller function for fs expression
const callerSearch = file.slice(Math.max(0, funcStart - 5000), funcStart);
callerFsMatch = callerSearch.match(fsPattern);
if (!callerFsMatch) {
console.error(
'patch: agentsMd: failed to find fs expression in function or caller'
);
return null;
}
Comment on lines +127 to +137
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and locate the file
find . -name "agentsMd.ts" -type f

Repository: Piebald-AI/tweakcc

Length of output: 87


🏁 Script executed:

# Read the relevant section and surrounding context
head -n 160 src/patches/agentsMd.ts | tail -n 60

Repository: Piebald-AI/tweakcc

Length of output: 2029


🏁 Script executed:

# Search for fsPattern definition in the file
rg "fsPattern" src/patches/agentsMd.ts -B 3 -A 3

Repository: Piebald-AI/tweakcc

Length of output: 637


🏁 Script executed:

# Get the full context of the function containing lines 127-137
sed -n '100,160p' src/patches/agentsMd.ts

Repository: Piebald-AI/tweakcc

Length of output: 2079


Replace first-match lookup with nearest-match strategy for caller fs accessor.

The 5000-character lookback window uses callerSearch.match(fsPattern), which captures the first fs-like accessor in the entire prefix, not the one nearest to the target function. If that window contains any earlier helper or wrapper with readFileSync, existsSync, or statSync, the injected reroute will reference the wrong object and generate incorrect fallback behavior.

Consider walking backward from funcStart to find the nearest match, or use a smaller, more contextually relevant lookback window.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patches/agentsMd.ts` around lines 127 - 137, The current lookup uses
callerSearch.match(fsPattern) which returns the first fs-like accessor in the
5000-char window and can pick an earlier helper; change the logic in agentsMd to
search backwards from funcStart for the nearest fs accessor instead: instead of
callerSearch.match(fsPattern), scan the substring (file.slice(Math.max(0,
funcStart - N), funcStart)) from end to start and use fsPattern.exec in a loop
or repeated matches to pick the last match (nearest to funcStart), updating
callerFsMatch accordingly; reference the variables callerFsMatch, fsPattern,
callerSearch, funcStart and replace the single .match call with a
reverse/last-match strategy (or reduce N) so the injected reroute targets the
correct fs object.

}
const fsExpr = fsMatch[1];

const fsExpr = fsMatch
? fsMatch[1]
: callerFsMatch
? callerFsMatch[1]
: 'require("fs")';

const altNamesJson = JSON.stringify(altNames);

// Step 1: 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)
const funcBody = newFile.slice(funcStart);

// CC ≤2.1.62: existsSync/isFile check before reading
const oldEarlyReturnPattern = /\.isFile\(\)\)return null/;
// CC ≥2.1.69: try/catch with ENOENT/EISDIR error codes
const newEarlyReturnPattern = /==="EISDIR"\)return null/;

const earlyReturnMatch =
Expand Down
9 changes: 4 additions & 5 deletions src/patches/allowCustomAgentModels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,10 @@ describe('allowCustomAgentModels', () => {
expect(result).not.toContain('hWH.includes(E)');
});

it('should return null when no patterns found', () => {
const result = writeAllowCustomAgentModels(
'totally unrelated code with no patterns'
);
expect(result).toBeNull();
it('should return file unchanged when no patterns found (CC >=2.1.83)', () => {
const input = 'totally unrelated code with no patterns';
const result = writeAllowCustomAgentModels(input);
expect(result).toBe(input);
});

it('should return null when only Zod pattern found', () => {
Expand Down
7 changes: 7 additions & 0 deletions src/patches/allowCustomAgentModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export const writeAllowCustomAgentModels = (file: string): string | null => {

const zodMatch = newFile.match(zodPattern);
if (!zodMatch || zodMatch.index === undefined) {
// CC >=2.1.83 already uses z.string().optional() for agent models.
// Check if validation flag still exists; if not, patch is not needed.
const validPatternAny =
/let\s+[$\w]+\s*=\s*([$\w]+)\s*&&\s*typeof\s+\1\s*===\s*"string"\s*&&\s*[$\w]+\.includes\(\1\)/;
if (!newFile.match(validPatternAny)) {
return newFile;
}
console.error(
'patch: allowCustomAgentModels: failed to find Zod enum pattern'
);
Expand Down
142 changes: 87 additions & 55 deletions src/patches/autoAcceptPlanMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,14 @@
// This patch automatically selects "Yes, clear context and auto-accept edits"
// without requiring user interaction.
//
// The accept handler function name varies between minified versions (e.g., "e"
// in 2.1.31, "t" in 2.1.22), so we detect it dynamically from the onChange prop.
//
// CC 2.1.22:
// ```diff
// if(Q)return F5.default.createElement(Sw,{...title:"Exit plan mode?"...});
// +t("yes-accept-edits");return null;
// return F5.default.createElement(F5.default.Fragment,null,
// F5.default.createElement(Sw,{color:"planMode",title:"Ready to code?",...
// ```
//
// CC 2.1.31:
// ```diff
// if(Q)return R8.default.createElement(fq,{...title:"Exit plan mode?"...});
// +e("yes-accept-edits");return null;
// return R8.default.createElement(R8.default.Fragment,null,
// R8.default.createElement(fq,{color:"planMode",title:"Ready to code?",...
// ```
// Supports multiple CC versions:
// - CC <=2.1.69: onChange:(X)=>FUNC(X),onCancel pattern
// - CC >=2.1.83: onChange:a or onChange:(X)=>void REF.current(X) pattern
// where 'a' is the async handler defined earlier in the component

import { showDiff } from './index';

/**
* Patch the plan approval component to auto-accept.
*
* Finds the "Ready to code?" return statement and inserts an early
* call to the accept handler function, bypassing the approval UI.
*/
export const writeAutoAcceptPlanMode = (oldFile: string): string | null => {
// First, find the accept handler function name by looking at the onChange handler
// near "Ready to code?". The pattern is: onChange:(X)=>FUNC(X),onCancel
const readyIdx = oldFile.indexOf('title:"Ready to code?"');
if (readyIdx === -1) {
console.error(
Expand All @@ -45,52 +23,106 @@ export const writeAutoAcceptPlanMode = (oldFile: string): string | null => {
return null;
}

// Check if already patched
const alreadyPatchedPattern =
/[$\w]+\("yes-accept-edits"\);return null;return/;
if (alreadyPatchedPattern.test(oldFile)) {
return oldFile;
}

// Look for onChange handler after Ready to code
const afterReady = oldFile.slice(readyIdx, readyIdx + 3000);
const onChangeMatch = afterReady.match(

// Try legacy pattern first: onChange:(X)=>FUNC(X),onCancel
const legacyOnChange = afterReady.match(
/onChange:\([$\w]+\)=>([$\w]+)\([$\w]+\),onCancel/
);
if (!onChangeMatch) {

// Try new pattern: onChange:FUNC, where FUNC is a direct reference
const directOnChange = afterReady.match(/onChange:([$\w]+),onCancel/);

// Try ref pattern: onChange:(X)=>void REF.current(X),onCancel
const refOnChange = afterReady.match(
/onChange:\([$\w]+\)=>void ([$\w]+)\.current\([$\w]+\),onCancel/
);

let acceptFuncName: string;

if (legacyOnChange) {
acceptFuncName = legacyOnChange[1];
} else if (directOnChange) {
acceptFuncName = directOnChange[1];
} else if (refOnChange) {
// The ref pattern uses REF.current which holds the actual handler
// We need to call REF.current("yes-accept-edits") or find the actual function
acceptFuncName = `${refOnChange[1]}.current`;
} else {
console.error('patch: autoAcceptPlanMode: failed to find onChange handler');
return null;
}

const acceptFuncName = onChangeMatch[1];
// Find the injection point: just before the return that renders "Ready to code?"
// Look for the return statement with createElement containing title:"Ready to code?"
//
// CC <=2.1.69: }}}))))});return React.createElement(Fragment,null,React.createElement(COMP,{color:"planMode",title:"Ready to code?"
// CC >=2.1.83: return React.createElement(Box,{...},React.createElement(COMP,{color:"planMode",title:"Ready to code?"

// Check if already patched (with any function name)
const alreadyPatchedPattern = new RegExp(
`[$\\w]+\\("yes-accept-edits"\\);return null;return`
);
if (alreadyPatchedPattern.test(oldFile)) {
return oldFile;
// Try the legacy pattern (after Exit plan mode conditional)
const legacyReturnPattern =
/(\}\}\)\)\)\);)(return [$\w]+\.default\.createElement\([$\w]+\.default\.Fragment,null,[$\w]+\.default\.createElement\([$\w]+,\{color:"planMode",title:"Ready to code\?")/;

const legacyMatch = oldFile.match(legacyReturnPattern);

if (legacyMatch && legacyMatch.index !== undefined) {
const insertion = `${acceptFuncName}("yes-accept-edits");return null;`;
const replacement = legacyMatch[1] + insertion + legacyMatch[2];
const startIndex = legacyMatch.index;
const endIndex = startIndex + legacyMatch[0].length;

const newFile =
oldFile.slice(0, startIndex) + replacement + oldFile.slice(endIndex);

showDiff(oldFile, newFile, replacement, startIndex, endIndex);
return newFile;
}

// Match the end of the "Exit plan mode?" conditional and the start of
// the "Ready to code?" return.
const pattern =
/(\}\}\)\)\)\);)(return [$\w]+\.default\.createElement\([$\w]+\.default\.Fragment,null,[$\w]+\.default\.createElement\([$\w]+,\{color:"planMode",title:"Ready to code\?")/;
// CC >=2.1.83: Find "return React.createElement(Box,{...title:"Ready to code?"
// The return is preceded by various patterns, find it by searching backwards from readyIdx
const beforeReady = oldFile.slice(Math.max(0, readyIdx - 500), readyIdx);

const match = oldFile.match(pattern);
if (!match || match.index === undefined) {
console.error(
'patch: autoAcceptPlanMode: failed to find "Ready to code?" return pattern'
);
return null;
// Look for the return statement start
const returnMatch = beforeReady.match(
/(return [$\w]+\.default\.createElement\([$\w]+,\{flexDirection:"column",tabIndex:0,autoFocus:!0.{0,200}[$\w]+\.default\.createElement\([$\w]+,\{color:"planMode",title:")$/
);

if (!returnMatch) {
// Simpler approach: find "return" before "Ready to code?" that starts the component tree
const simpleReturnIdx = beforeReady.lastIndexOf('return ');
if (simpleReturnIdx === -1) {
console.error(
'patch: autoAcceptPlanMode: failed to find return before "Ready to code?"'
);
return null;
}

const absoluteReturnIdx = Math.max(0, readyIdx - 500) + simpleReturnIdx;
const insertion = `${acceptFuncName}("yes-accept-edits");return null;`;

const newFile =
oldFile.slice(0, absoluteReturnIdx) +
insertion +
oldFile.slice(absoluteReturnIdx);

showDiff(oldFile, newFile, insertion, absoluteReturnIdx, absoluteReturnIdx);
return newFile;
}

// Insert auto-accept call between the if(Q) block and the return
// The accept function triggers the accept flow with "yes-accept-edits"
// return null prevents rendering the UI (component will unmount after state change)
const absoluteStart = Math.max(0, readyIdx - 500) + returnMatch.index!;
const insertion = `${acceptFuncName}("yes-accept-edits");return null;`;
const replacement = match[1] + insertion + match[2];

const startIndex = match.index;
const endIndex = startIndex + match[0].length;

const newFile =
oldFile.slice(0, startIndex) + replacement + oldFile.slice(endIndex);

showDiff(oldFile, newFile, replacement, startIndex, endIndex);
oldFile.slice(0, absoluteStart) + insertion + oldFile.slice(absoluteStart);

showDiff(oldFile, newFile, insertion, absoluteStart, absoluteStart);
return newFile;
};
Loading
Loading