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
379 changes: 265 additions & 114 deletions src/nativeInstallation.ts

Large diffs are not rendered by default.

180 changes: 130 additions & 50 deletions src/patches/agentsMd.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,38 @@
// 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 => {
// CC ≥ 2.1.87 ships with native AGENTS.md / alternative MD file support.
// Detect the fallback loop: endsWith("...CLAUDE.md")...for(let ... of [
if (/CLAUDE\.md.{0,100}for\(let \w+ of \["AGENTS\.md"/.test(file)) {
console.log(
'patch: agentsMd: alternative MD file support already present natively — skipping'
);
return file;
}

// 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 +41,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 +120,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;
};
12 changes: 12 additions & 0 deletions src/patches/fixLspSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ const getOpenDocumentLocation = (oldFile: string): LocationResult | null => {
};

export const writeFixLspSupport = (oldFile: string): string | null => {
// CC ≥ 2.1.87 ships with native LSP didOpen support and removed the
// validation throws — skip if the feature is already present.
if (
oldFile.includes('textDocument/didOpen') &&
!oldFile.includes('restartOnCrash is not yet implemented')
) {
console.log(
'patch: fixLspSupport: LSP fixes already present natively — skipping'
);
return oldFile;
}

// Patch 1: Comment out the validation by replacing with nothing
const validationPattern1 =
/if\([$\w]+\.restartOnCrash!==void 0\)throw Error\(`LSP server '\$\{[$\w]+\}': restartOnCrash is not yet implemented\. Remove this field from the configuration\.`\);/g;
Expand Down
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
10 changes: 10 additions & 0 deletions src/patches/modelSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ const findCustomModelListInsertionPoint = (
};

export const writeModelCustomizations = (oldFile: string): string | null => {
// Skip if custom models are already injected (e.g. from a previous
// tweakcc run baked into the backup, or future native support).
// The JSON.stringify format uses quoted keys: {"value":"claude-opus-4-6",...}
if (oldFile.includes('"value":"claude-opus-4-6"')) {
console.log(
'patch: modelCustomizations: custom models already present — skipping'
);
return oldFile;
}

const found = findCustomModelListInsertionPoint(oldFile);
if (!found) return null;

Expand Down
8 changes: 8 additions & 0 deletions src/patches/opusplan1m.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,14 @@ const patchAlwaysShowInModelSelector = (oldFile: string): string | null => {
* Main entry point: Apply all opusplan[1m] patches
*/
export const writeOpusplan1m = (oldFile: string): string | null => {
// CC ≥ 2.1.87 ships opusplan[1m] natively — skip if already present.
if (oldFile.includes('"opusplan[1m]"')) {
console.log(
'patch: opusplan1m: opusplan[1m] already supported natively — skipping'
);
return oldFile;
}

let newFile = oldFile;

// Patch 1: Mode switching function
Expand Down
Loading