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
2 changes: 2 additions & 0 deletions src/defaultSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,8 @@ export const DEFAULT_SETTINGS: Settings = {
enableVoiceMode: false,
enableVoiceConciseOutput: true,
enableChannelsMode: false,
disableGarnetLoom: false,
enableMaxAgentTurnsOverride: false,
},
toolsets: [],
defaultToolset: null,
Expand Down
133 changes: 131 additions & 2 deletions src/patches/agentsMd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,19 @@ export const writeAgentsMd = (

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;
// CC 2.1.97+: reading and processing are split into separate functions.
// The content processor (sa_) has "Skipping non-text file" but no fs calls.
// Check if the feature is already built into this CC version.
if (file.includes('didReroute') && file.includes('AGENTS.md')) {
// CC 2.1.97+ already has AGENTS.md fallback built in — no patch needed
return file;
Comment on lines +75 to +77
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

Don't short-circuit on AGENTS.md alone.

This no-op ignores the actual altNames requested by the caller. If a user configures extra names like GEMINI.md or QWEN.md, this branch can return the original bundle unchanged as soon as CC ships any built-in AGENTS.md handling, so the additional fallbacks never get injected. src/patches/index.ts:812-818 shows this patch is driven by the full config.settings.claudeMdAltNames list.

Suggested fix
-    if (file.includes('didReroute') && file.includes('AGENTS.md')) {
+    const hasBuiltInReroute =
+      file.includes('didReroute') &&
+      altNames.every(altName => file.includes(altName));
+    if (hasBuiltInReroute) {
       // CC 2.1.97+ already has AGENTS.md fallback built in — no patch needed
       return file;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (file.includes('didReroute') && file.includes('AGENTS.md')) {
// CC 2.1.97+ already has AGENTS.md fallback built in — no patch needed
return file;
const hasBuiltInReroute =
file.includes('didReroute') &&
altNames.every(altName => file.includes(altName));
if (hasBuiltInReroute) {
// CC 2.1.97+ already has AGENTS.md fallback built in — no patch needed
return file;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patches/agentsMd.ts` around lines 75 - 77, The branch that returns early
when file.includes('didReroute') && file.includes('AGENTS.md') is too broad —
change it to consult config.settings.claudeMdAltNames instead of
short-circuiting on AGENTS.md alone: only return the original bundle if
claudeMdAltNames is empty or every altName in config.settings.claudeMdAltNames
is already present in the file (e.g., via file.includes(altName)); otherwise
proceed to inject the additional fallbacks. Reference the existing
variables/strings 'file', 'didReroute', 'AGENTS.md', and
'config.settings.claudeMdAltNames' when locating and updating the conditional.

}
// Otherwise, patch the async reader function.
return writeAgentsMdAsync(file, altNames);
}

const fsExpr = fsMatch[1];

const altNamesJson = JSON.stringify(altNames);
Expand Down Expand Up @@ -119,3 +128,123 @@ export const writeAgentsMd = (

return newFile;
};

/**
* CC 2.1.97+ variant: the file reading function is async and split from processing.
*
* Structure:
* ```
* async function yP4(q,K,_){
* try {
* let Y = await X8().readFile(q, {encoding:"utf-8"});
* return sa_(Y, q, K, _);
* } catch(z) {
* return ta_(z, q), {info:null, includePaths:[]};
* }
* }
* ```
*
* We patch the catch block to try alternative filenames when ENOENT + CLAUDE.md.
*/
const writeAgentsMdAsync = (
file: string,
altNames: string[]
): string | null => {
const infoNullStr = '{info:null,includePaths:[]}';

// Find the async reader function by locating {info:null,includePaths:[]}
// near a readFile call
let infoNullIdx = -1;
let searchStart = 0;
while (true) {
const idx = file.indexOf(infoNullStr, searchStart);
if (idx === -1) break;

const lookback = file.slice(Math.max(0, idx - 500), idx);
if (lookback.includes('.readFile')) {
infoNullIdx = idx;
break;
}
searchStart = idx + 1;
}

if (infoNullIdx === -1) {
console.error(
'patch: agentsMd: failed to find async CLAUDE.md reader function (2.1.97+)'
);
return null;
}

const lookback = file.slice(Math.max(0, infoNullIdx - 500), infoNullIdx);

// Find the async function definition
const asyncFuncPattern =
/async function ([$\w]+)\(([$\w]+),([$\w]+),([$\w]+)\)\{/g;
const funcMatches = Array.from(lookback.matchAll(asyncFuncPattern));
if (funcMatches.length === 0) {
console.error(
'patch: agentsMd: failed to find async function definition (2.1.97+)'
);
return null;
}
const lastFunc = funcMatches[funcMatches.length - 1];
const funcName = lastFunc[1];
const pathParam = lastFunc[2];
const typeParam = lastFunc[3];
const resolvedParam = lastFunc[4];

// Find the catch variable
const catchPattern = /catch\(([$\w]+)\)\{/g;
const catchMatches = Array.from(lookback.matchAll(catchPattern));
if (catchMatches.length === 0) {
console.error('patch: agentsMd: failed to find catch block (2.1.97+)');
return null;
}
const catchVar = catchMatches[catchMatches.length - 1][1];

// Step 1: Add didReroute parameter to the function signature
const sigStr = `async function ${funcName}(${pathParam},${typeParam},${resolvedParam})`;
const sigIdx = file.indexOf(sigStr);
if (sigIdx === -1) {
console.error(
'patch: agentsMd: failed to locate function signature for injection (2.1.97+)'
);
return null;
}
const closingParenIdx = sigIdx + sigStr.length - 1;
let newFile =
file.slice(0, closingParenIdx) +
',didReroute' +
file.slice(closingParenIdx);

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

// Step 2: Inject fallback in the catch block before {info:null,...}
// The catch block is: catch(z){return ta_(z,q),{info:null,includePaths:[]}}
// We inject before the return statement.
const catchBlockStr = `catch(${catchVar}){`;
const catchBlockIdx = newFile.indexOf(catchBlockStr, sigIdx);
if (catchBlockIdx === -1) {
console.error(
'patch: agentsMd: failed to locate catch block for injection (2.1.97+)'
);
return null;
}
const returnIdx = catchBlockIdx + catchBlockStr.length;

const altNamesJson = JSON.stringify(altNames);
const fallback =
`if(${catchVar}&&${catchVar}.code==="ENOENT"&&!didReroute` +
`&&(${pathParam}.endsWith("/CLAUDE.md")||${pathParam}.endsWith("\\\\CLAUDE.md")))` +
`{for(let alt of ${altNamesJson})` +
`{try{let altPath=${pathParam}.slice(0,-9)+alt;` +
`let r=await ${funcName}(altPath,${typeParam},${resolvedParam},true);` +
`if(r.info)return r}catch(e){}}}`;

const oldFile = newFile;
newFile = newFile.slice(0, returnIdx) + fallback + newFile.slice(returnIdx);

showDiff(oldFile, newFile, fallback, returnIdx, returnIdx);

return newFile;
};
12 changes: 8 additions & 4 deletions src/patches/autoAcceptPlanMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ export const writeAutoAcceptPlanMode = (oldFile: string): string | null => {

// Look for onChange handler after Ready to code
const afterReady = oldFile.slice(readyIdx, readyIdx + 3000);
const onChangeMatch = afterReady.match(
/onChange:\([$\w]+\)=>([$\w]+)\([$\w]+\),onCancel/
);
// CC ≥2.1.97: onChange:J6,onCancel (direct reference)
// CC <2.1.97: onChange:(X)=>FUNC(X),onCancel (arrow wrapper)
const onChangeMatch =
afterReady.match(/onChange:([$\w]+),onCancel/) ||
afterReady.match(/onChange:\([$\w]+\)=>([$\w]+)\([$\w]+\),onCancel/);
if (!onChangeMatch) {
console.error('patch: autoAcceptPlanMode: failed to find onChange handler');
return null;
Expand All @@ -67,8 +69,10 @@ export const writeAutoAcceptPlanMode = (oldFile: string): string | null => {

// Match the end of the "Exit plan mode?" conditional and the start of
// the "Ready to code?" return.
// CC ≥2.1.97: wrapper changed from Fragment to Box (u) with props
// CC <2.1.97: wrapper is Fragment with null
const pattern =
/(\}\}\)\)\)\);)(return [$\w]+\.default\.createElement\([$\w]+\.default\.Fragment,null,[$\w]+\.default\.createElement\([$\w]+,\{color:"planMode",title:"Ready to code\?")/;
/(\}\}\)\)\)\);)(return [$\w]+\.default\.createElement\((?:[$\w]+\.default\.Fragment,null|[$\w]+,\{[^}]*\}),[$\w]+\.default\.createElement\([$\w]+,\{color:"planMode",title:"Ready to code\?")/;

const match = oldFile.match(pattern);
if (!match || match.index === undefined) {
Expand Down
56 changes: 48 additions & 8 deletions src/patches/contextLimit.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,51 @@
// Please see the note about writing patches in ./index
import { showDiff } from './index';

import { globalReplace } from './index';
/**
* Replaces the hardcoded default context limit (200K) with an env var override.
*
* The context window function determines the token limit for each model:
* ```
* function TV(q,K){
* if(Cf(q))return 1e6; // [1m] models → 1M
* if(K?.includes(ni)&&U01(q))return 1e6; // SDK beta + supported model → 1M
* if(cZ8(q))return 1e6; // coral_reef_sonnet experiment → 1M
* return eN1 // default: eN1 = 200000
* }
* ```
*
* We patch the final `return eN1` to read from CLAUDE_CODE_CONTEXT_LIMIT env var,
* falling back to the original eN1 value if unset.
*
* ```diff
* - return eN1
* + return Number(process.env.CLAUDE_CODE_CONTEXT_LIMIT??eN1)
* ```
*/
export const writeContextLimit = (file: string): string | null => {
// Find the context window function by its unique structure:
// three 1e6 returns (for special models) then a variable return (default)
const pattern =
/function ([$\w]+)\(([$\w]+),([$\w]+)\)\{if\([$\w]+\(\2\)\)return 1e6;if\(\3\?\.includes\([$\w]+\)&&[$\w]+\(\2\)\)return 1e6;if\([$\w]+\(\2\)\)return 1e6;return ([$\w]+)\}/;

export const writeContextLimit = (oldFile: string): string | null => {
return globalReplace(
oldFile,
/\b200000\b/,
'(+process.env.CLAUDE_CODE_CONTEXT_LIMIT||200000)'
);
const match = file.match(pattern);
if (!match || match.index === undefined) {
console.error(
'patch: contextLimit: failed to find context window function'
);
return null;
}

const defaultVar = match[4];
const oldStr = `return ${defaultVar}}`;
const newStr = `return Number(process.env.CLAUDE_CODE_CONTEXT_LIMIT??${defaultVar})}`;
Comment on lines +39 to +40
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

Guard CLAUDE_CODE_CONTEXT_LIMIT before returning it.

Number('') resolves to 0 and Number('abc') resolves to NaN, so a bad env value can collapse the context window instead of preserving the original default.

Graceful fallback for invalid input
-  const newStr = `return Number(process.env.CLAUDE_CODE_CONTEXT_LIMIT??${defaultVar})}`;
+  const newStr =
+    `return ((v=>Number.isInteger(v)&&v>0?v:${defaultVar})` +
+    `(Number(process.env.CLAUDE_CODE_CONTEXT_LIMIT)))}`

As per coding guidelines "Implement error handling with try-catch blocks, log errors with debug(), and return graceful fallbacks".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const oldStr = `return ${defaultVar}}`;
const newStr = `return Number(process.env.CLAUDE_CODE_CONTEXT_LIMIT??${defaultVar})}`;
const oldStr = `return ${defaultVar}}`;
const newStr =
`return ((v=>Number.isInteger(v)&&v>0?v:${defaultVar})` +
`(Number(process.env.CLAUDE_CODE_CONTEXT_LIMIT)))}`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patches/contextLimit.ts` around lines 39 - 40, The replacement string
newStr should validate CLAUDE_CODE_CONTEXT_LIMIT before converting and returning
it: wrap the conversion in a try-catch, use a safe parse (e.g., parseInt or
Number) and check for NaN/empty string, call debug() on parse errors, and fall
back to the original defaultVar; update the code that sets newStr (and/or the
returned expression) so it performs the guarded conversion and returns
defaultVar when the env value is invalid instead of allowing Number('') or
Number('abc') to collapse the context window.


const replaceStart = match.index + match[0].length - oldStr.length;
const newFile =
file.slice(0, replaceStart) +
newStr +
file.slice(replaceStart + oldStr.length);

showDiff(file, newFile, newStr, replaceStart, replaceStart + oldStr.length);

return newFile;
};
66 changes: 66 additions & 0 deletions src/patches/garnetLoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { showDiff } from './index';

// ======

/**
* Disable tengu_garnet_loom — prevents auto-downgrade of Opus to Sonnet
*
* CC 2.1.97 has a feature gate `tengu_garnet_loom` that, when enabled server-side,
* auto-downgrades Opus subagents to Sonnet when context is under 200K tokens.
*
* The pattern in the subagent model resolver:
* if(!Y && Jz(j).includes("opus") && R8("tengu_garnet_loom",!1)){
* let H=$5("sonnet"); return O(H,"sonnet")
* }
*
* This patch forces the R8("tengu_garnet_loom") check to always return false,
* ensuring Opus stays Opus regardless of what GrowthBook says.
*/
export const writeDisableGarnetLoom = (oldFile: string): string | null => {
// Pattern: R8("tengu_garnet_loom",!1)
// This appears in the subagent model resolver function
const pattern = /R8\("tengu_garnet_loom",!1\)/;

const match = oldFile.match(pattern);
if (!match || match.index === undefined) {
// Try generic feature gate check function names
const altPattern = /([$\w]+)\("tengu_garnet_loom",![01]\)/;
const altMatch = oldFile.match(altPattern);
if (!altMatch || altMatch.index === undefined) {
console.error(
'patch: garnetLoom: failed to find tengu_garnet_loom feature gate check'
);
return null;
}

// Replace the entire check with false
const replacement = '!1';
const newFile =
oldFile.slice(0, altMatch.index) +
replacement +
oldFile.slice(altMatch.index + altMatch[0].length);
showDiff(
oldFile,
newFile,
replacement,
altMatch.index,
altMatch.index + altMatch[0].length
);
return newFile;
}

// Replace R8("tengu_garnet_loom",!1) with !1 (always false = never downgrade)
const replacement = '!1';
const newFile =
oldFile.slice(0, match.index) +
replacement +
oldFile.slice(match.index + match[0].length);
showDiff(
oldFile,
newFile,
replacement,
match.index,
match.index + match[0].length
);
return newFile;
};
29 changes: 29 additions & 0 deletions src/patches/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,35 @@ export const findBoxComponent = (fileContents: string): string | undefined => {
return boxDisplayNameMatch[1];
}

// Method 4: Find Box by createElement usage near known anchor (CC 2.1.97+)
// In 2.1.97, ink-box is no longer used via createElement directly in most code.
// Instead, a Box wrapper component is assigned to a short local variable (e.g. `u`)
// and used as createElement(u, {flexDirection:...}). We find it by locating a known
// anchor (the "Claude Code" bold text in the header) and checking which variable
// is used with flexDirection props nearby.
const headerAnchor = fileContents.match(
/createElement\([$\w]+,\{bold:!0\},"Claude Code"\)/
);
if (headerAnchor && headerAnchor.index !== undefined) {
const searchStart = Math.max(0, headerAnchor.index - 2000);
const searchEnd = Math.min(fileContents.length, headerAnchor.index + 2000);
const region = fileContents.slice(searchStart, searchEnd);
const boxUsage = region.match(
/createElement\(([$\w]+),\{(?:flexDirection|gap|marginBottom)/
);
if (boxUsage) {
return boxUsage[1];
}
Comment on lines +346 to +354
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

Choose the Box candidate closest to the header anchor.

Lines 346-350 search a ±2000-char window, but region.match(...) returns the first layout-like createElement(...) in that whole slice. A matching call before the "Claude Code" anchor can win over the actual header Box wrapper, which then poisons every downstream ${boxComponent} insertion.

Suggested fix
   if (headerAnchor && headerAnchor.index !== undefined) {
     const searchStart = Math.max(0, headerAnchor.index - 2000);
     const searchEnd = Math.min(fileContents.length, headerAnchor.index + 2000);
     const region = fileContents.slice(searchStart, searchEnd);
-    const boxUsage = region.match(
-      /createElement\(([$\w]+),\{(?:flexDirection|gap|marginBottom)/
-    );
-    if (boxUsage) {
-      return boxUsage[1];
+    const anchorOffset = headerAnchor.index - searchStart;
+    const boxUsages = Array.from(
+      region.matchAll(
+        /createElement\(([$\w]+),\{(?:flexDirection|gap|marginBottom)/g
+      )
+    );
+    const boxUsage =
+      boxUsages.find((match) => (match.index ?? 0) >= anchorOffset) ??
+      boxUsages.at(-1);
+    if (boxUsage) {
+      return boxUsage[1];
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patches/helpers.ts` around lines 346 - 354, The current logic uses
region.match(...) which returns the first createElement(...) hit in the
±2000-char region and can pick a candidate that is farther from
headerAnchor.index; change it to find all matches (use a global regex via
matchAll or exec loop on the same pattern) over region, compute the absolute
distance between each match's start (adjusted by searchStart) and
headerAnchor.index, pick the match with the smallest distance, and return its
capture group (the box component name) instead of the first match; update
references to headerAnchor, fileContents, region, and boxUsage accordingly.

}

// Method 4b: Fallback — find Box wrapper by border prop destructuring
const borderBoxPattern =
/function ([$\w]+)\([$\w]+\)\{.{0,200}borderColor:[$\w]+,borderTopColor:[$\w]+,borderBottomColor:[$\w]+,borderLeftColor:[$\w]+,borderRightColor:[$\w]+,backgroundColor:[$\w]+,children:[$\w]+,ref:[$\w]+/;
const borderBoxMatch = fileContents.match(borderBoxPattern);
if (borderBoxMatch) {
return borderBoxMatch[1];
}

console.error(
'patch: findBoxComponent: failed to find Box component (neither ink-box createElement nor displayName found)'
);
Expand Down
Loading