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
65 changes: 65 additions & 0 deletions src/patches/agentsMd.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, it, expect } from 'vitest';
import { writeAgentsMd } from './agentsMd';

const mockFunction =
'function _t7(A,q){try{let K=x1();' +
'if(!K.existsSync(A)||!K.statSync(A).isFile())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});' +
'}return null;}';

const altNames = ['AGENTS.md', 'GEMINI.md', 'QWEN.md'];

describe('agentsMd', () => {
describe('writeAgentsMd', () => {
it('should inject fallback at early return null when CLAUDE.md is missing', () => {
const result = writeAgentsMd(mockFunction, altNames);
expect(result).not.toBeNull();
expect(result).toContain('didReroute');
expect(result).toContain('endsWith("/CLAUDE.md")');
expect(result).toContain('AGENTS.md');
expect(result).toMatch(/\.isFile\(\)\)\{.*?return null;\}/);
});

it('should preserve CLAUDE.md content when present', () => {
const result = writeAgentsMd(mockFunction, altNames)!;
const returnIdx = result.indexOf('return{path:');
expect(returnIdx).toBeGreaterThan(-1);
const beforeReturn = result.slice(Math.max(0, returnIdx - 50), returnIdx);
expect(beforeReturn).not.toContain('didReroute');
});

it('should pass didReroute=true in recursive calls', () => {
const result = writeAgentsMd(mockFunction, altNames)!;
expect(result).toContain('return _t7(altPath,q,true)');
});

it('should return null when no alternatives are found', () => {
const result = writeAgentsMd(mockFunction, altNames)!;
expect(result).toMatch(/\}return null;\}/);
});

it('should add didReroute parameter to function signature', () => {
const result = writeAgentsMd(mockFunction, altNames)!;
expect(result).toContain('function _t7(A,q,didReroute)');
});

it('should use the correct fs expression', () => {
const result = writeAgentsMd(mockFunction, altNames)!;
expect(result).toContain('K.existsSync(altPath)');
expect(result).toContain('K.statSync(altPath)');
});

it('should return null when function pattern is not found', () => {
const result = writeAgentsMd('not a valid file', altNames);
expect(result).toBeNull();
});
});
});
55 changes: 31 additions & 24 deletions src/patches/agentsMd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { showDiff } from './index';

/**
* Patches the CLAUDE.md file reading function to also check for alternative
* filenames (e.g., AGENTS.md).
* 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. Right before the `return {` statement, check if the path ends with
* CLAUDE.md and try alternative names (unless didReroute is true)
* 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
*
* CC 2.1.62 (approx. by Claude):
Expand All @@ -18,19 +19,22 @@ import { showDiff } from './index';
* +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()) 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);
* + 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 { path: A, type: q, content: w, globs: H };
* } catch (K) {
* if (K instanceof Error && K.message.includes("EACCES"))
Expand All @@ -48,7 +52,7 @@ export const writeAgentsMd = (
altNames: string[]
): string | null => {
const funcPattern =
/(function ([$\w]+)\(([$\w]+),([^)]+?))\)(?:.|\n){0,500}Skipping non-text file in @include(?:.|\n){0,500}return\{path:[$\w]+,.{0,20}?content:[$\w]+/;
/(function ([$\w]+)\(([$\w]+),([^)]+?))\)(?:.|\n){0,500}Skipping non-text file in @include/;

const funcMatch = file.match(funcPattern);
if (!funcMatch || funcMatch.index === undefined) {
Expand All @@ -71,33 +75,36 @@ export const writeAgentsMd = (

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 rerouting code right before the `return {`
const returnPattern = /return\{path:[$\w]+,.{0,20}?content:[$\w]+/;
const returnMatch = newFile.slice(funcStart).match(returnPattern);
// Step 2: Inject fallback at the early return null (when file doesn't exist)
const earlyReturnPattern = /\.isFile\(\)\)return null/;
const funcBody = newFile.slice(funcStart);
const earlyReturnMatch = funcBody.match(earlyReturnPattern);

if (!returnMatch || returnMatch.index === undefined) {
if (!earlyReturnMatch || earlyReturnMatch.index === undefined) {
console.error(
'patch: agentsMd: failed to find return statement for injection'
'patch: agentsMd: failed to find early return null for injection'
);
return null;
}

const injection = `if(!didReroute&&(${firstParam}.endsWith("/CLAUDE.md")||${firstParam}.endsWith("\\\\CLAUDE.md"))){for(let alt of ${altNamesJson}){let altPath=${firstParam}.slice(0,-9)+alt;if(${fsExpr}.existsSync(altPath)&&${fsExpr}.statSync(altPath).isFile())return ${functionName}(altPath,${restParams},true);}}`;
const fallback = `if(!didReroute&&(${firstParam}.endsWith("/CLAUDE.md")||${firstParam}.endsWith("\\\\CLAUDE.md"))){for(let alt of ${altNamesJson}){let altPath=${firstParam}.slice(0,-9)+alt;if(${fsExpr}.existsSync(altPath)&&${fsExpr}.statSync(altPath).isFile())return ${functionName}(altPath,${restParams},true);}}`;

const returnStart = funcStart + returnMatch.index;
const replacement = injection + returnMatch[0];
const earlyReturnStart = funcStart + earlyReturnMatch.index;
const oldStr = earlyReturnMatch[0];
const newStr = `.isFile()){${fallback}return null;}`;

newFile =
newFile.slice(0, returnStart) +
replacement +
newFile.slice(returnStart + returnMatch[0].length);
newFile.slice(0, earlyReturnStart) +
newStr +
newFile.slice(earlyReturnStart + oldStr.length);

showDiff(file, newFile, replacement, returnStart, returnStart);
showDiff(file, newFile, newStr, earlyReturnStart, earlyReturnStart);

return newFile;
};