Skip to content
Merged
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
37 changes: 2 additions & 35 deletions src/patches/agentsMd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,6 @@ import { showDiff } from './index';
* 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") || ...)) { ... }
* + 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,
Expand Down Expand Up @@ -88,13 +55,13 @@ const writeAgentsMdAsync = (
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});` +
`${funcSig},didReroute){try{let ${readVar}=await ${fsGetter}().readFile(${pathParam},{encoding:"utf-8"});return ${processorFunc}(${readVar},${pathParam},${typeParam},${thirdParam})}catch(${catchVar}){` +
`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:[]}}}`;
`return ${errorHandler}(${catchVar},${pathParam}),{info:null,includePaths:[]}}}`;

const startIndex = funcMatch.index;
const endIndex = startIndex + fullMatch.length;
Expand Down
3 changes: 3 additions & 0 deletions src/patches/allowBypassPermsInSudo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const writeAllowBypassPermsInSudo = (file: string): string | null => {
const match = file.match(pattern);

if (!match || match.index === undefined) {
if (!file.includes('root/sudo privileges')) {
return file;
}
console.error('patch: allowBypassPermsInSudo: failed to find pattern');
return null;
}
Expand Down
20 changes: 20 additions & 0 deletions src/patches/autoAcceptPlanMode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';

import { writeAutoAcceptPlanMode } from './autoAcceptPlanMode';

describe('writeAutoAcceptPlanMode', () => {
it('finds the enclosing return even when it starts before the Ready prompt window', () => {
const filler = 'x'.repeat(700);
const input =
'function A(){let h=(v)=>v;' +
`return R.default.createElement(Box,{children:"${filler}"},` +
'R.default.createElement(Card,{color:"planMode",title:"Ready to code?",onChange:h,onCancel:z}));}';

const result = writeAutoAcceptPlanMode(input);

expect(result).not.toBeNull();
expect(result).toContain(
'h("yes-accept-edits-keep-context");return null;return R.default.createElement'
);
});
});
180 changes: 167 additions & 13 deletions src/patches/autoAcceptPlanMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,139 @@

import { showDiff } from './index';

const findEnclosingFunctionReturn = (
oldFile: string,
readyIdx: number
): number | null => {
const functionStart = oldFile.lastIndexOf('function ', readyIdx);
if (functionStart === -1) return null;

const openBrace = oldFile.indexOf('{', functionStart);
if (openBrace === -1 || openBrace > readyIdx) return null;

let depth = 0;
for (let index = openBrace; index < oldFile.length; index++) {
const char = oldFile[index];
if (char === '{') depth++;
else if (char === '}') {
depth--;
if (depth === 0) {
const functionTail = oldFile.slice(openBrace, index + 1);
const returnPattern = /return [$\w]+(?:\.default)?\.createElement/g;
let match: RegExpExecArray | null;
let lastMatch: RegExpExecArray | null = null;
while ((match = returnPattern.exec(functionTail)) !== null) {
lastMatch = match;
}
return lastMatch ? openBrace + lastMatch.index : null;
}
}
}

return null;
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const patchPlanModePrompts = (file: string): string => {
const replacements: Array<
[RegExp, string | ((...args: string[]) => string)]
> = [
[
/When ready, use \$\{([$\w]+)\} to present your plan for approval/g,
(_match, toolName) =>
`When ready, use \${${toolName}} to exit plan mode. The plan will be approved automatically.`,
],
[
/Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval\./g,
'Use this tool when you are in plan mode and have finished writing your plan to the plan file. Calling this tool exits plan mode and approves the plan automatically.',
],
[
/This tool simply signals that you're done planning and ready for the user to review and approve/g,
'This tool signals that you are done planning and that the plan should be approved automatically',
],
[
/Once your plan is finalized, use THIS tool to request approval/g,
'Once your plan is finalized, use THIS tool to approve the plan and proceed',
],
[
/ExitPlanMode inherently requests user approval of your plan\./g,
'ExitPlanMode inherently approves your plan and lets you proceed.',
],
[
/Present your plan to the user for approval/g,
'Exit plan mode; the plan will be approved automatically',
],
[
/design an implementation approach for user approval/g,
'design an implementation approach before automatic approval',
],
[
/This tool REQUIRES user approval - they must consent to entering plan mode/g,
'This tool enters plan mode; plan exit approval is handled automatically when auto-accept plan mode is enabled',
],
[
/Claude has written up a plan and is ready to execute\. Would you like to proceed\?/g,
'Claude has written up a plan and is ready to execute. The plan is approved automatically.',
],
[
/Call `\$\{([$\w]+)\}` to present the plan for approval\./g,
(_match, toolName) =>
`Call \`\${${toolName}}\` to exit plan mode; the plan will be approved automatically.`,
],
[
/## Phase 2: Spawn Workers \(After Plan Approval\)/g,
'## Phase 2: Spawn Workers (After Automatic Plan Approval)',
],
[
/Once the plan is approved, spawn/g,
'After the plan is approved automatically, spawn',
],
[
/searchHint:"present plan for approval and start coding \(plan mode only\)"/g,
'searchHint:"approve plan and start coding (plan mode only)"',
],
[
/async description\(\)\{return"Prompts the user to exit plan mode and start coding"\}/g,
'async description(){return"Exits plan mode and starts coding"}',
],
];

let newFile = file;
for (const [pattern, replacement] of replacements) {
const before = newFile;
newFile = newFile.replace(pattern, replacement as never);
if (newFile !== before) {
showDiff(file, newFile, String(replacement), 0, 0);
}
}

const planExitPermissionUpdate =
'permissionUpdates:[{type:"setMode",mode:"acceptEdits",destination:"session"}]';

const permissionDefaultPattern =
/kind:"permission_exit_plan_mode_v2",payload:([\s\S]{0,300}?),result:([\s\S]{0,180}?),default:\{behavior:"cancelled"\}/;
const beforePermissionDefault = newFile;
newFile = newFile.replace(
permissionDefaultPattern,
`kind:"permission_exit_plan_mode_v2",payload:$1,result:$2,default:{behavior:"allow",${planExitPermissionUpdate}}`
);
if (newFile !== beforePermissionDefault) {
showDiff(file, newFile, 'permission_exit_plan_mode_v2 default allow', 0, 0);
}

const exitPlanCheckPermissionsPattern =
/async checkPermissions\(([$\w]+),([$\w]+)\)\{if\(([$\w]+)\(\)\)return\{behavior:"allow",updatedInput:\1\};return\{behavior:"ask",message:"Exit plan mode\?",updatedInput:\1\}\}/;
const beforeCheckPermissions = newFile;
newFile = newFile.replace(
exitPlanCheckPermissionsPattern,
`async checkPermissions($1,$2){return{behavior:"allow",updatedInput:$1,${planExitPermissionUpdate}}}`
);
if (newFile !== beforeCheckPermissions) {
showDiff(file, newFile, 'ExitPlanMode checkPermissions allow', 0, 0);
}

return newFile;
};

export const writeAutoAcceptPlanMode = (oldFile: string): string | null => {
const readyIdx = oldFile.indexOf('title:"Ready to code?"');
if (readyIdx === -1) {
Expand All @@ -25,7 +158,7 @@ export const writeAutoAcceptPlanMode = (oldFile: string): string | null => {

// Check if already patched
const alreadyPatchedPattern =
/[$\w]+\("yes-accept-edits"\);return null;return/;
/[$\w]+(?:\.current)?\("yes-accept-edits(?:-keep-context)?"\);return null;return/;
if (alreadyPatchedPattern.test(oldFile)) {
return oldFile;
}
Expand Down Expand Up @@ -74,7 +207,7 @@ export const writeAutoAcceptPlanMode = (oldFile: string): string | null => {
const legacyMatch = oldFile.match(legacyReturnPattern);

if (legacyMatch && legacyMatch.index !== undefined) {
const insertion = `${acceptFuncName}("yes-accept-edits");return null;`;
const insertion = `${acceptFuncName}("yes-accept-edits-keep-context");return null;`;
const replacement = legacyMatch[1] + insertion + legacyMatch[2];
const startIndex = legacyMatch.index;
const endIndex = startIndex + legacyMatch[0].length;
Expand All @@ -83,12 +216,15 @@ export const writeAutoAcceptPlanMode = (oldFile: string): string | null => {
oldFile.slice(0, startIndex) + replacement + oldFile.slice(endIndex);

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

// 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);
// The return is preceded by various patterns, find it by searching backwards from readyIdx.
// Newer bundles can place a long prop list before the title, so keep this
// wider than the old 500-byte window and fall back to function-level search.
const returnSearchStart = Math.max(0, readyIdx - 2500);
const beforeReady = oldFile.slice(returnSearchStart, readyIdx);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Look for the return statement start
const returnMatch = beforeReady.match(
Expand All @@ -99,30 +235,48 @@ export const writeAutoAcceptPlanMode = (oldFile: string): string | null => {
// 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?"'
const enclosingReturnIdx = findEnclosingFunctionReturn(oldFile, readyIdx);
if (enclosingReturnIdx === null) {
console.error(
'patch: autoAcceptPlanMode: failed to find return before "Ready to code?"'
);
return null;
}

const insertion = `${acceptFuncName}("yes-accept-edits-keep-context");return null;`;
const newFile =
oldFile.slice(0, enclosingReturnIdx) +
insertion +
oldFile.slice(enclosingReturnIdx);

showDiff(
oldFile,
newFile,
insertion,
enclosingReturnIdx,
enclosingReturnIdx
);
return null;
return patchPlanModePrompts(newFile);
}

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

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

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

const absoluteStart = Math.max(0, readyIdx - 500) + returnMatch.index!;
const insertion = `${acceptFuncName}("yes-accept-edits");return null;`;
const insertion = `${acceptFuncName}("yes-accept-edits-keep-context");return null;`;

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

showDiff(oldFile, newFile, insertion, absoluteStart, absoluteStart);
return newFile;
return patchPlanModePrompts(newFile);
};
21 changes: 15 additions & 6 deletions src/patches/contextLimit.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
// Please see the note about writing patches in ./index

import { globalReplace } from './index';

export const writeContextLimit = (oldFile: string): string | null => {
return globalReplace(
oldFile,
/\b200000\b/,
'(+process.env.CLAUDE_CODE_CONTEXT_LIMIT||200000)'
const replacement = '(+process.env.CLAUDE_CODE_CONTEXT_LIMIT||200000)';
const pattern =
/var ([\w$]+)=200000,([\w$]+)=20000,([\w$]+)=32000,([\w$]+)=(128000|64000);/;
const match = oldFile.match(pattern);

if (!match) {
console.error(
'patch: contextLimit: failed to find context limit constants'
);
return null;
}

return oldFile.replace(
pattern,
`var ${match[1]}=${replacement},${match[2]}=20000,${match[3]}=32000,${match[4]}=${match[5]};`
);
};
Loading