Skip to content

Instantly share code, notes, and snippets.

@Zamua
Last active January 27, 2026 18:02
Show Gist options
  • Select an option

  • Save Zamua/f7ca58ce5dd9ba61279ea195a01b190c to your computer and use it in GitHub Desktop.

Select an option

Save Zamua/f7ca58ce5dd9ba61279ea195a01b190c to your computer and use it in GitHub Desktop.
script to patch claude-code 2.0.76 to fix lsp plugin
#!/bin/bash
#
# Claude Code LSP Fix
# ====================
# Fixes the LSP plugin bug: https://github.com/anthropics/claude-code/issues/13952
#
# THE BUG:
# Claude Code's LSP manager has an empty initialize() function that should
# load and register LSP servers from plugins, but instead does nothing.
# This causes "No LSP server available for file type" errors.
#
# THE FIX:
# This script patches the empty initialize() function to actually:
# 1. Load LSP server configs from enabled plugins
# 2. Create server instances for each config
# 3. Register them so Claude Code can use them
#
# HOW IT WORKS:
# Uses the acorn JavaScript parser to find the right functions by their
# structure and string contents (not minified names, which vary between builds).
# This makes the fix reliable across different installations.
#
# USAGE:
# ./apply-claude-code-2.0.76-lsp-fix.sh # Apply the fix
# ./apply-claude-code-2.0.76-lsp-fix.sh --check # Check if fix is needed
# ./apply-claude-code-2.0.76-lsp-fix.sh --restore # Restore from backup
# ./apply-claude-code-2.0.76-lsp-fix.sh --fix-plugins # Fix plugin configs only
# ./apply-claude-code-2.0.76-lsp-fix.sh --help # Show this help
#
# REQUIREMENTS:
# - Node.js (already installed if you have Claude Code)
# - Internet connection (downloads acorn parser on first run)
# - npm-based Claude Code installation (not the standalone binary)
# The official install script creates a Mach-O binary that can't be patched.
# Install via npm instead: npm install -g @anthropic-ai/claude-code
#
# NOTE:
# This patch will be overwritten when Claude Code updates.
# Re-run this script after updates if LSP stops working.
#
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() { echo -e "${GREEN}βœ“${NC} $1"; }
print_warning() { echo -e "${YELLOW}!${NC} $1"; }
print_error() { echo -e "${RED}βœ—${NC} $1"; }
print_info() { echo -e "${BLUE}β†’${NC} $1"; }
# Fix plugin marketplace configs (removes unsupported fields like startupTimeout)
fix_plugin_configs() {
local marketplace_json="$HOME/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json"
if [ ! -f "$marketplace_json" ]; then
return 0
fi
# Use Node.js for proper JSON manipulation
node -e "
const fs = require('fs');
const path = '$marketplace_json';
let data;
try {
data = JSON.parse(fs.readFileSync(path, 'utf8'));
} catch (e) {
console.log('\x1b[33m!\x1b[0m Plugin config JSON is invalid, skipping');
process.exit(0);
}
let fixes = [];
const unsupportedFields = ['startupTimeout', 'shutdownTimeout'];
if (data.plugins) {
for (const plugin of data.plugins) {
if (plugin.lspServers) {
for (const [serverName, config] of Object.entries(plugin.lspServers)) {
for (const field of unsupportedFields) {
if (config[field] !== undefined) {
delete config[field];
fixes.push({ plugin: plugin.name, server: serverName, field });
}
}
}
}
}
}
if (fixes.length > 0) {
// Backup and write
fs.copyFileSync(path, path + '.backup');
fs.writeFileSync(path, JSON.stringify(data, null, 2));
for (const fix of fixes) {
console.log('\x1b[32mβœ“\x1b[0m Fixed ' + fix.plugin + ': removed unsupported \"' + fix.field + '\" from ' + fix.server);
}
} else {
console.log('\x1b[32mβœ“\x1b[0m Plugin configs already clean');
}
"
}
# Show help
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
sed -n '3,39p' "$0" | sed 's/^#//' | sed 's/^ //'
exit 0
fi
# Handle --fix-plugins (standalone)
if [ "$1" = "--fix-plugins" ]; then
echo "Fixing plugin configs..."
fix_plugin_configs
exit 0
fi
# Find Claude Code cli.js in common installation locations
find_cli_path() {
local locations=(
"$HOME/.claude/local/node_modules/@anthropic-ai/claude-code/cli.js"
"/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js"
"/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js"
"$(npm root -g 2>/dev/null)/@anthropic-ai/claude-code/cli.js"
)
for path in "${locations[@]}"; do
if [ -f "$path" ]; then
echo "$path"
return 0
fi
done
return 1
}
CLI_PATH=$(find_cli_path) || true
if [ -z "$CLI_PATH" ]; then
print_error "Claude Code cli.js not found"
echo ""
echo "Searched:"
echo " ~/.claude/local/node_modules/@anthropic-ai/claude-code/cli.js"
echo " /usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js"
echo " /usr/lib/node_modules/@anthropic-ai/claude-code/cli.js"
echo " \$(npm root -g)/@anthropic-ai/claude-code/cli.js"
echo ""
echo "This script only works with npm-based installations."
echo "If you installed via the official install script (standalone binary),"
echo "reinstall using: npm install -g @anthropic-ai/claude-code"
exit 1
fi
# Handle --restore
if [ "$1" = "--restore" ]; then
restored=0
# Restore cli.js
LATEST_BACKUP=$(ls -t "${CLI_PATH}.backup-"* 2>/dev/null | head -1)
if [ -n "$LATEST_BACKUP" ]; then
cp "$LATEST_BACKUP" "$CLI_PATH"
print_status "Restored cli.js from: $LATEST_BACKUP"
restored=1
fi
# Restore plugin config
MARKETPLACE_JSON="$HOME/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json"
if [ -f "${MARKETPLACE_JSON}.backup" ]; then
cp "${MARKETPLACE_JSON}.backup" "$MARKETPLACE_JSON"
print_status "Restored plugin config from backup"
restored=1
fi
if [ $restored -eq 0 ]; then
print_error "No backups found"
exit 1
fi
exit 0
fi
print_info "Found Claude Code at: $CLI_PATH"
echo ""
# Download acorn JS parser if needed (cached in /tmp)
ACORN_PATH="/tmp/acorn-claude-fix.js"
if [ ! -f "$ACORN_PATH" ]; then
print_info "Downloading acorn parser..."
if ! curl -sf https://unpkg.com/acorn@8.14.0/dist/acorn.js -o "$ACORN_PATH"; then
print_error "Failed to download acorn parser"
exit 1
fi
fi
# Create the Node.js patch script
PATCH_SCRIPT="/tmp/claude-lsp-patch-$$.js"
cat > "$PATCH_SCRIPT" << 'NODESCRIPT'
const fs = require('fs');
const acorn = require('/tmp/acorn-claude-fix.js');
const cliPath = process.argv[2];
const checkOnly = process.argv[3] === '--check';
let code = fs.readFileSync(cliPath, 'utf-8');
// Strip shebang for parsing (will restore later)
let shebang = '';
if (code.startsWith('#!')) {
const idx = code.indexOf('\n');
shebang = code.slice(0, idx + 1);
code = code.slice(idx + 1);
}
// Check if already patched (look for our unique variable names)
if (code.includes('let{servers:_S}=await') && code.includes('.set(_N,_I)')) {
console.log('\x1b[32mβœ“\x1b[0m Already patched');
process.exit(2); // Exit 2 = already patched, no changes
}
// Parse the JavaScript
let ast;
try {
ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: 'module' });
} catch (e) {
console.error('\x1b[31mβœ—\x1b[0m Failed to parse cli.js:', e.message);
process.exit(1);
}
// --- AST Helpers ---
// Get source text for a node
const src = (node) => code.slice(node.start, node.end);
// Recursively find all nodes matching a predicate
function findNodes(node, predicate, results = []) {
if (!node || typeof node !== 'object') return results;
if (predicate(node)) results.push(node);
for (const key in node) {
if (node[key] && typeof node[key] === 'object') {
if (Array.isArray(node[key])) {
node[key].forEach(child => findNodes(child, predicate, results));
} else {
findNodes(node[key], predicate, results);
}
}
}
return results;
}
// Check if node contains a string literal matching text
function containsString(node, text) {
const strings = findNodes(node, n => n.type === 'Literal' && typeof n.value === 'string');
return strings.some(s => s.value.includes(text));
}
// Check if node contains a template literal matching text
function containsTemplate(node, text) {
const templates = findNodes(node, n => n.type === 'TemplateLiteral');
return templates.some(t => t.quasis.map(q => q.value.raw).join('').includes(text));
}
// --- Find the functions we need to patch ---
const allFunctions = findNodes(ast, n =>
n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression'
);
// 1. Find createLspServer() - contains "Starting LSP server instance"
let createServerFunc = null;
for (const fn of allFunctions) {
if (containsString(fn, 'Starting LSP server instance') || containsTemplate(fn, 'Starting LSP server instance')) {
createServerFunc = fn;
break;
}
}
if (!createServerFunc) {
console.error('\x1b[31mβœ—\x1b[0m Could not find createLspServer function');
console.error(' (looking for function containing "Starting LSP server instance")');
process.exit(1);
}
const createServerName = createServerFunc.id?.name;
console.log('\x1b[34m→\x1b[0m Found createLspServer:', createServerName);
// 2. Find loadLspServersFromPlugins() - contains "Loaded" + "LSP server" log
let loadServersFunc = null;
for (const fn of allFunctions) {
const hasLoaded = containsString(fn, 'Loaded') || containsTemplate(fn, 'Loaded');
const hasLsp = containsString(fn, 'LSP server') || containsTemplate(fn, 'LSP server');
if (hasLoaded && hasLsp) {
loadServersFunc = fn;
break;
}
}
if (!loadServersFunc) {
console.error('\x1b[31mβœ—\x1b[0m Could not find loadLspServersFromPlugins function');
console.error(' (looking for function containing "Loaded" + "LSP server")');
process.exit(1);
}
const loadServersName = loadServersFunc.id?.name;
console.log('\x1b[34m→\x1b[0m Found loadLspServersFromPlugins:', loadServersName);
// 3. Find LSP manager factory - has 3 Maps and an empty async initialize()
let lspManagerFunc = null;
let emptyInitFunc = null;
let mapVars = [];
for (const fn of allFunctions) {
// Look for "new Map()" variable declarations
const varDecls = findNodes(fn, n => n.type === 'VariableDeclaration');
const mapInits = [];
for (const decl of varDecls) {
for (const d of decl.declarations) {
if (d.init?.type === 'NewExpression' && d.init.callee?.name === 'Map') {
mapInits.push(d.id.name);
}
}
}
// LSP manager has 3+ Maps
if (mapInits.length >= 3) {
// Find empty async function inside (the buggy initialize)
const asyncFuncs = findNodes(fn, n => n.type === 'FunctionDeclaration' && n.async);
for (const inner of asyncFuncs) {
const body = inner.body?.body;
// Empty body or just "return" with no value
if (body?.length === 0 ||
(body?.length === 1 && body[0].type === 'ReturnStatement' && !body[0].argument)) {
lspManagerFunc = fn;
emptyInitFunc = inner;
mapVars = mapInits;
break;
}
}
}
if (lspManagerFunc) break;
}
if (!lspManagerFunc || !emptyInitFunc) {
console.error('\x1b[31mβœ—\x1b[0m Could not find LSP manager with empty initialize()');
console.error(' (looking for function with 3 Maps + empty async function)');
process.exit(1);
}
const initFuncName = emptyInitFunc.id?.name;
const serverMap = mapVars[0]; // First map stores servers
const extMap = mapVars[1]; // Second map stores extension->server mappings
console.log('\x1b[34m→\x1b[0m Found empty initialize():', initFuncName);
console.log('\x1b[34m→\x1b[0m Server registry map:', serverMap);
console.log('\x1b[34m→\x1b[0m Extension map:', extMap);
if (checkOnly) {
console.log('');
console.log('\x1b[33m!\x1b[0m Patch needed - run without --check to apply');
process.exit(1);
}
// --- Build and apply the patch ---
// The fix: make initialize() actually load and register LSP servers
const newInitBody = `async function ${initFuncName}(){` +
`let{servers:_S}=await ${loadServersName}();` +
`for(let[_N,_C]of Object.entries(_S)){` +
`let _I=${createServerName}(_N,_C);` +
`${serverMap}.set(_N,_I);` +
`for(let[_E,_L]of Object.entries(_C.extensionToLanguage||{})){` +
`let _M=${extMap}.get(_E)||[];` +
`_M.push(_N);` +
`${extMap}.set(_E,_M)` +
`}` +
`}` +
`}`;
// Apply patch (preserve shebang)
const newCode = shebang + code.slice(0, emptyInitFunc.start) + newInitBody + code.slice(emptyInitFunc.end);
// Backup original
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const backupPath = cliPath + '.backup-' + timestamp;
fs.copyFileSync(cliPath, backupPath);
console.log('');
console.log('Backup:', backupPath);
// Write patched file
fs.writeFileSync(cliPath, newCode);
// Verify
if (fs.readFileSync(cliPath, 'utf-8').includes(newInitBody)) {
console.log('');
console.log('\x1b[32mβœ“\x1b[0m Fix applied successfully!');
} else {
console.error('\x1b[31mβœ—\x1b[0m Verification failed, restoring backup...');
fs.copyFileSync(backupPath, cliPath);
process.exit(1);
}
NODESCRIPT
# Run the patch script
node "$PATCH_SCRIPT" "$CLI_PATH" "$1"
EXIT_CODE=$?
# Cleanup temp script
rm -f "$PATCH_SCRIPT"
# Exit codes: 0 = patched, 1 = error/check-needed, 2 = already patched
if [ $EXIT_CODE -eq 0 ] && [ "$1" != "--check" ]; then
# Newly patched - fix plugins and show restart message
echo ""
fix_plugin_configs
echo ""
print_warning "Restart Claude Code for changes to take effect"
elif [ $EXIT_CODE -eq 2 ]; then
# Already patched - just exit cleanly
exit 0
fi
exit $EXIT_CODE
@mkreyman
Copy link
Copy Markdown

Tested the updated AST-based script on 2.0.76. The parsing works but it fails to find createLspServer:

Claude Code: ~/.asdf/installs/nodejs/22.14.0/lib/node_modules/@anthropic-ai/claude-code/cli.js

βœ— Could not find createLspServer function
(looking for function containing "restartOnCrash")

Debug investigation shows the issue:

  • Parsed OK: 12341 top-level nodes, 30260 functions
  • String literals containing "restartOnCrash": 0
  • Template literals containing "restartOnCrash": 0

The string exists in the file but as an object property name in a schema definition, not as a quoted string:

...ful shutdown (milliseconds)"),restartOnCrash:m.boolean().optional().descri...

So containsString() and containsTemplate() won't find it since it's an identifier/property, not a string literal.

Maybe searching for a different unique string that IS quoted would work? Or matching against property access patterns in the AST?

@Zamua
Copy link
Copy Markdown
Author

Zamua commented Dec 31, 2025

do you mind sharing your entire cli.js file with me? you can share it however you like (gist, pastebin, etc)

@mkreyman
Copy link
Copy Markdown

Here's my cli.js (2.0.76, installed via asdf/npm):
https://gist.github.com/mkreyman/e2b026059891b98e60429060dc8023e4

@Zamua
Copy link
Copy Markdown
Author

Zamua commented Dec 31, 2025

thanks! i updated the script; do you mind trying again @mkreyman ?

@mkreyman
Copy link
Copy Markdown

Updated script works! πŸŽ‰

Found all functions and patched successfully:

  • createLspServer: T52
  • loadLspServersFromPlugins: v52
  • empty initialize(): G
  • Server registry map: A
  • Extension map: Q

After restart, the elixir-lsp plugin is now functional - LSP documentSymbol returns proper results. The skills from the elixir plugin are also available.

Thanks for the quick iteration!

@Zamua
Copy link
Copy Markdown
Author

Zamua commented Dec 31, 2025

πŸ₯³

@52617365
Copy link
Copy Markdown

52617365 commented Jan 1, 2026

Is this related to the script or some Claude code issue?
LSP server 'plugin:jdtls-lsp:jdtls': startupTimeout is not yet implemented. Remove this field from the configuration.

@Zamua
Copy link
Copy Markdown
Author

Zamua commented Jan 1, 2026

@52617365 hmmm that error message comes from claude code. it seems to be a bug because the startupTimeout config is only present for the jdtls plugin, but the claude code cli has explicit validation for not supporting this config with the message you saw. i was able to reproduce it locally

i got it fixed by updating my config for that plugin. can you try updating ~/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json (or whatever path yours is at) to remove startupTimeout": 120000? this is the patch

diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index c11e644..0a4755d 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -204,7 +204,7 @@
           "extensionToLanguage": {
             ".java": "java"
           },
-          "startupTimeout": 120000
+
         }
       }
     },

i can see about updating the script to auto-fix this as well

@52617365
Copy link
Copy Markdown

52617365 commented Jan 1, 2026

That fixed it, thank you a lot!

@Zamua
Copy link
Copy Markdown
Author

Zamua commented Jan 1, 2026

πŸ₯³

@saksham-vardaam
Copy link
Copy Markdown

saksham-vardaam commented Jan 2, 2026

I’m sorry to ask but how do I use this?

I’ve installed PHP LSP via the /plugin command.

@Zamua
Copy link
Copy Markdown
Author

Zamua commented Jan 2, 2026

I’m sorry to ask but how do I use this?

I’ve installed PHP LSP via the /plugin command.

not a problem, copy the script either by copy/paste or git clone, give it execute permissions, run it. here's an example on my computer

❯ git clone https://gist.github.com/Zamua/f7ca58ce5dd9ba61279ea195a01b190c
Cloning into 'f7ca58ce5dd9ba61279ea195a01b190c'...
remote: Enumerating objects: 21, done.
remote: Counting objects: 100% (21/21), done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 21 (delta 5), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (21/21), 8.51 KiB | 8.51 MiB/s, done.
Resolving deltas: 100% (5/5), done.

❯ cd f7ca58ce5dd9ba61279ea195a01b190c

❯ chmod +x apply-claude-code-2.0.76-lsp-fix.sh

❯ ./apply-claude-code-2.0.76-lsp-fix.sh
β†’ Found Claude Code at: /Users/zamua/.claude/local/node_modules/@anthropic-ai/claude-code/cli.js

βœ“ Already patched

@sxhmilyoyo
Copy link
Copy Markdown

Thanks for your fix!

I got this issue, not sure if anyone else got the similar one

> I started another session and got this when I run doctor command
 Plugin Errors
 β”” 1 plugin error(s) detected:
   β”” lsp-manager: LSP server 'plugin:jdtls-lsp:jdtls': startupTimeout is not yet
 implemented. Remove this field from the configuration.

⏺ The marketplace config was likely refreshed. Let me fix the startupTimeout field again:

⏺ Bash(/tmp/apply-claude-code-lsp-fix.sh --fix-plugins)
  ⎿  Fixing plugin configs...
     βœ“ Fixed jdtls-lsp: removed unsupported "startupTimeout" from jdtls

⏺ Fixed. The startupTimeout field was restored because the marketplace config gets refreshed periodically.

  β˜… Insight ─────────────────────────────────────
  Why this keeps happening:
  1. Claude Code refreshes marketplace configs from the remote repository
  2. The upstream marketplace.json still contains startupTimeout: 120000
  3. Each refresh overwrites the local fix

  Workaround options:
  - Run --fix-plugins after marketplace refreshes
  - Or add a hook to auto-fix on session start
  ─────────────────────────────────────────────────

  Would you like me to create a session-start hook that automatically removes unsupported LSP config fields?

@Zamua
Copy link
Copy Markdown
Author

Zamua commented Jan 4, 2026

hmmmmmm ok, so are you saying the script worked, but then it reverted back to being broken again after a while? i don't have much more info than claude does there. not sure what would cause it to recent the plugin config, but if re-running the script fixes it, i guess you'll need to just keep doing that when it breaks πŸ˜…

let me know if i'm misunderstanding your issue

@Viacol0916
Copy link
Copy Markdown

Hi @Zamua, thanks for this helpful script!

I encountered an issue: the script exits silently without any output on my machine.

After debugging with bash -x, I found the cause: I installed Claude Code via the official installation script (not npm), which installs a standalone Mach-O binary instead of a Node.js package.

$ which claude
/Users/xxx/.local/bin/claude

$ file /Users/xxx/.local/bin/claude
/Users/xxx/.local/bin/claude: Mach-O 64-bit executable arm64

Since there's no cli.js file in this installation method, the find_cli_path() function returns 1, and set -e causes the script to exit immediately before printing the error message.

Suggestions:

  1. Maybe add a note in the script header that this only works for npm installations
  2. Or change CLI_PATH=$(find_cli_path) to CLI_PATH=$(find_cli_path) || true so the friendly error message can be displayed

Hope this helps others who might encounter the same issue!

@Zamua
Copy link
Copy Markdown
Author

Zamua commented Jan 5, 2026

@Viacol0916 ahhh thanks for that info! i've updated the script

@sxhmilyoyo
Copy link
Copy Markdown

hmmmmmm ok, so are you saying the script worked, but then it reverted back to being broken again after a while? i don't have much more info than claude does there. not sure what would cause it to recent the plugin config, but if re-running the script fixes it, i guess you'll need to just keep doing that when it breaks πŸ˜…

let me know if i'm misunderstanding your issue

yeah, it works but have to run the script whenever I start a new CC session in the different folder.

@Not-Just-Coder
Copy link
Copy Markdown

i am in windows system , error message is as follows

$ sh apply-claude-code-2.0.76-lsp-fix.sh
β†’ Found Claude Code at: C:
vm4w
odejs
ode_modules/@anthropic-ai/claude-code/cli.js

β†’ Downloading acorn parser...
node:internal/modules/cjs/loader:1228
throw err;
^

Error: Cannot find module '/tmp/acorn-claude-fix.js'
Require stack:

  • C:\Users\SUNJIE1\AppData\Local\Temp\claude-lsp-patch-2250.js
    at Function._resolveFilename (node:internal/modules/cjs/loader:1225:15)
    at Function._load (node:internal/modules/cjs/loader:1055:27)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:220:24)
    at Module.require (node:internal/modules/cjs/loader:1311:12)
    at require (node:internal/modules/helpers:136:16)
    at Object. (C:\Users\SUNJIE
    1\AppData\Local\Temp\claude-lsp-patch-2250.js:2:15)
    at Module._compile (node:internal/modules/cjs/loader:1554:14)
    at Object..js (node:internal/modules/cjs/loader:1706:10)
    at Module.load (node:internal/modules/cjs/loader:1289:32) {
    code: 'MODULE_NOT_FOUND',
    requireStack: [
    'C:\Users\SUNJIE~1\AppData\Local\Temp\claude-lsp-patch-2250.js'
    ]
    }

Node.js v22.14.0

@genesiscz
Copy link
Copy Markdown

Here you go - script that works if you installed with bunjs

#!/bin/bash
#
# Claude Code LSP Fix
# ====================
# Fixes the LSP plugin bug: https://github.com/anthropics/claude-code/issues/13952
#
# THE BUG:
#   Claude Code's LSP manager has an empty initialize() function that should
#   load and register LSP servers from plugins, but instead does nothing.
#   This causes "No LSP server available for file type" errors.
#
# THE FIX:
#   This script patches the empty initialize() function to actually:
#   1. Load LSP server configs from enabled plugins
#   2. Create server instances for each config
#   3. Register them so Claude Code can use them
#
# HOW IT WORKS:
#   Uses the acorn JavaScript parser to find the right functions by their
#   structure and string contents (not minified names, which vary between builds).
#   This makes the fix reliable across different installations.
#
# USAGE:
#   ./apply-claude-code-2.0.76-lsp-fix.sh               # Apply the fix
#   ./apply-claude-code-2.0.76-lsp-fix.sh --check       # Check if fix is needed
#   ./apply-claude-code-2.0.76-lsp-fix.sh --restore     # Restore from backup
#   ./apply-claude-code-2.0.76-lsp-fix.sh --fix-plugins # Fix plugin configs only
#   ./apply-claude-code-2.0.76-lsp-fix.sh --help        # Show this help
#
# REQUIREMENTS:
#   - Node.js (already installed if you have Claude Code)
#   - Internet connection (downloads acorn parser on first run)
#   - npm or bun-based Claude Code installation (not the standalone binary)
#     The official install script creates a Mach-O binary that can't be patched.
#     Install via bun instead: bun add -g @anthropic-ai/claude-code
#
# NOTE:
#   This patch will be overwritten when Claude Code updates.
#   Re-run this script after updates if LSP stops working.
#

set -e

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

print_status() { echo -e "${GREEN}βœ“${NC} $1"; }
print_warning() { echo -e "${YELLOW}!${NC} $1"; }
print_error() { echo -e "${RED}βœ—${NC} $1"; }
print_info() { echo -e "${BLUE}β†’${NC} $1"; }

# Fix plugin marketplace configs (removes unsupported fields like startupTimeout)
fix_plugin_configs() {
    local marketplace_json="$HOME/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json"

    if [ ! -f "$marketplace_json" ]; then
        return 0
    fi

    # Use Node.js for proper JSON manipulation
    node -e "
const fs = require('fs');
const path = '$marketplace_json';
let data;
try {
    data = JSON.parse(fs.readFileSync(path, 'utf8'));
} catch (e) {
    console.log('\x1b[33m!\x1b[0m Plugin config JSON is invalid, skipping');
    process.exit(0);
}

let fixes = [];
const unsupportedFields = ['startupTimeout', 'shutdownTimeout'];

if (data.plugins) {
    for (const plugin of data.plugins) {
        if (plugin.lspServers) {
            for (const [serverName, config] of Object.entries(plugin.lspServers)) {
                for (const field of unsupportedFields) {
                    if (config[field] !== undefined) {
                        delete config[field];
                        fixes.push({ plugin: plugin.name, server: serverName, field });
                    }
                }
            }
        }
    }
}

if (fixes.length > 0) {
    // Backup and write
    fs.copyFileSync(path, path + '.backup');
    fs.writeFileSync(path, JSON.stringify(data, null, 2));
    for (const fix of fixes) {
        console.log('\x1b[32mβœ“\x1b[0m Fixed ' + fix.plugin + ': removed unsupported \"' + fix.field + '\" from ' + fix.server);
    }
} else {
    console.log('\x1b[32mβœ“\x1b[0m Plugin configs already clean');
}
"
}

# Show help
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
    sed -n '3,39p' "$0" | sed 's/^#//' | sed 's/^ //'
    exit 0
fi

# Handle --fix-plugins (standalone)
if [ "$1" = "--fix-plugins" ]; then
    echo "Fixing plugin configs..."
    fix_plugin_configs
    exit 0
fi

# Find Claude Code cli.js in common installation locations
find_cli_path() {
    local locations=(
        "$HOME/.claude/local/node_modules/@anthropic-ai/claude-code/cli.js"
        "/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js"
        "/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js"
        "$(npm root -g 2>/dev/null)/@anthropic-ai/claude-code/cli.js"
        # Bun installation paths
        "$HOME/.bun/install/global/node_modules/@anthropic-ai/claude-code/cli.js"
    )
    for path in "${locations[@]}"; do
        if [ -f "$path" ]; then
            echo "$path"
            return 0
        fi
    done
    return 1
}

CLI_PATH=$(find_cli_path) || true
if [ -z "$CLI_PATH" ]; then
    print_error "Claude Code cli.js not found"
    echo ""
    echo "Searched:"
    echo "  ~/.claude/local/node_modules/@anthropic-ai/claude-code/cli.js"
    echo "  /usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js"
    echo "  /usr/lib/node_modules/@anthropic-ai/claude-code/cli.js"
    echo "  \$(npm root -g)/@anthropic-ai/claude-code/cli.js"
    echo "  ~/.bun/install/global/node_modules/@anthropic-ai/claude-code/cli.js"
    echo ""
    echo "This script only works with npm/bun-based installations."
    echo "If you installed via the official install script (standalone binary),"
    echo "reinstall using: bun add -g @anthropic-ai/claude-code"
    exit 1
fi

# Handle --restore
if [ "$1" = "--restore" ]; then
    restored=0

    # Restore cli.js
    LATEST_BACKUP=$(ls -t "${CLI_PATH}.backup-"* 2>/dev/null | head -1)
    if [ -n "$LATEST_BACKUP" ]; then
        cp "$LATEST_BACKUP" "$CLI_PATH"
        print_status "Restored cli.js from: $LATEST_BACKUP"
        restored=1
    fi

    # Restore plugin config
    MARKETPLACE_JSON="$HOME/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json"
    if [ -f "${MARKETPLACE_JSON}.backup" ]; then
        cp "${MARKETPLACE_JSON}.backup" "$MARKETPLACE_JSON"
        print_status "Restored plugin config from backup"
        restored=1
    fi

    if [ $restored -eq 0 ]; then
        print_error "No backups found"
        exit 1
    fi
    exit 0
fi

print_info "Found Claude Code at: $CLI_PATH"
echo ""

# Download acorn JS parser if needed (cached in /tmp)
ACORN_PATH="/tmp/acorn-claude-fix.js"
if [ ! -f "$ACORN_PATH" ]; then
    print_info "Downloading acorn parser..."
    if ! curl -sf https://unpkg.com/acorn@8.14.0/dist/acorn.js -o "$ACORN_PATH"; then
        print_error "Failed to download acorn parser"
        exit 1
    fi
fi

# Create the Node.js patch script
PATCH_SCRIPT="/tmp/claude-lsp-patch-$$.js"

cat > "$PATCH_SCRIPT" << 'NODESCRIPT'
const fs = require('fs');
const acorn = require('/tmp/acorn-claude-fix.js');

const cliPath = process.argv[2];
const checkOnly = process.argv[3] === '--check';

let code = fs.readFileSync(cliPath, 'utf-8');

// Strip shebang for parsing (will restore later)
let shebang = '';
if (code.startsWith('#!')) {
    const idx = code.indexOf('\n');
    shebang = code.slice(0, idx + 1);
    code = code.slice(idx + 1);
}

// Check if already patched (look for our unique variable names)
if (code.includes('let{servers:_S}=await') && code.includes('.set(_N,_I)')) {
    console.log('\x1b[32mβœ“\x1b[0m Already patched');
    process.exit(2);  // Exit 2 = already patched, no changes
}

// Parse the JavaScript
let ast;
try {
    ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: 'module' });
} catch (e) {
    console.error('\x1b[31mβœ—\x1b[0m Failed to parse cli.js:', e.message);
    process.exit(1);
}

// --- AST Helpers ---

// Get source text for a node
const src = (node) => code.slice(node.start, node.end);

// Recursively find all nodes matching a predicate
function findNodes(node, predicate, results = []) {
    if (!node || typeof node !== 'object') return results;
    if (predicate(node)) results.push(node);
    for (const key in node) {
        if (node[key] && typeof node[key] === 'object') {
            if (Array.isArray(node[key])) {
                node[key].forEach(child => findNodes(child, predicate, results));
            } else {
                findNodes(node[key], predicate, results);
            }
        }
    }
    return results;
}

// Check if node contains a string literal matching text
function containsString(node, text) {
    const strings = findNodes(node, n => n.type === 'Literal' && typeof n.value === 'string');
    return strings.some(s => s.value.includes(text));
}

// Check if node contains a template literal matching text
function containsTemplate(node, text) {
    const templates = findNodes(node, n => n.type === 'TemplateLiteral');
    return templates.some(t => t.quasis.map(q => q.value.raw).join('').includes(text));
}

// --- Find the functions we need to patch ---

const allFunctions = findNodes(ast, n =>
    n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression'
);

// 1. Find createLspServer() - contains "Starting LSP server instance"
let createServerFunc = null;
for (const fn of allFunctions) {
    if (containsString(fn, 'Starting LSP server instance') || containsTemplate(fn, 'Starting LSP server instance')) {
        createServerFunc = fn;
        break;
    }
}
if (!createServerFunc) {
    console.error('\x1b[31mβœ—\x1b[0m Could not find createLspServer function');
    console.error('   (looking for function containing "Starting LSP server instance")');
    process.exit(1);
}
const createServerName = createServerFunc.id?.name;
console.log('\x1b[34m→\x1b[0m Found createLspServer:', createServerName);

// 2. Find loadLspServersFromPlugins() - contains "Loaded" + "LSP server" log
let loadServersFunc = null;
for (const fn of allFunctions) {
    const hasLoaded = containsString(fn, 'Loaded') || containsTemplate(fn, 'Loaded');
    const hasLsp = containsString(fn, 'LSP server') || containsTemplate(fn, 'LSP server');
    if (hasLoaded && hasLsp) {
        loadServersFunc = fn;
        break;
    }
}
if (!loadServersFunc) {
    console.error('\x1b[31mβœ—\x1b[0m Could not find loadLspServersFromPlugins function');
    console.error('   (looking for function containing "Loaded" + "LSP server")');
    process.exit(1);
}
const loadServersName = loadServersFunc.id?.name;
console.log('\x1b[34m→\x1b[0m Found loadLspServersFromPlugins:', loadServersName);

// 3. Find LSP manager factory - has 3 Maps and an empty async initialize()
let lspManagerFunc = null;
let emptyInitFunc = null;
let mapVars = [];

for (const fn of allFunctions) {
    // Look for "new Map()" variable declarations
    const varDecls = findNodes(fn, n => n.type === 'VariableDeclaration');
    const mapInits = [];

    for (const decl of varDecls) {
        for (const d of decl.declarations) {
            if (d.init?.type === 'NewExpression' && d.init.callee?.name === 'Map') {
                mapInits.push(d.id.name);
            }
        }
    }

    // LSP manager has 3+ Maps
    if (mapInits.length >= 3) {
        // Find empty async function inside (the buggy initialize)
        const asyncFuncs = findNodes(fn, n => n.type === 'FunctionDeclaration' && n.async);

        for (const inner of asyncFuncs) {
            const body = inner.body?.body;
            // Empty body or just "return" with no value
            if (body?.length === 0 ||
                (body?.length === 1 && body[0].type === 'ReturnStatement' && !body[0].argument)) {
                lspManagerFunc = fn;
                emptyInitFunc = inner;
                mapVars = mapInits;
                break;
            }
        }
    }
    if (lspManagerFunc) break;
}

if (!lspManagerFunc || !emptyInitFunc) {
    console.error('\x1b[31mβœ—\x1b[0m Could not find LSP manager with empty initialize()');
    console.error('   (looking for function with 3 Maps + empty async function)');
    process.exit(1);
}

const initFuncName = emptyInitFunc.id?.name;
const serverMap = mapVars[0];  // First map stores servers
const extMap = mapVars[1];     // Second map stores extension->server mappings

console.log('\x1b[34m→\x1b[0m Found empty initialize():', initFuncName);
console.log('\x1b[34m→\x1b[0m Server registry map:', serverMap);
console.log('\x1b[34m→\x1b[0m Extension map:', extMap);

if (checkOnly) {
    console.log('');
    console.log('\x1b[33m!\x1b[0m Patch needed - run without --check to apply');
    process.exit(1);
}

// --- Build and apply the patch ---

// The fix: make initialize() actually load and register LSP servers
const newInitBody = `async function ${initFuncName}(){` +
    `let{servers:_S}=await ${loadServersName}();` +
    `for(let[_N,_C]of Object.entries(_S)){` +
        `let _I=${createServerName}(_N,_C);` +
        `${serverMap}.set(_N,_I);` +
        `for(let[_E,_L]of Object.entries(_C.extensionToLanguage||{})){` +
            `let _M=${extMap}.get(_E)||[];` +
            `_M.push(_N);` +
            `${extMap}.set(_E,_M)` +
        `}` +
    `}` +
`}`;

// Apply patch (preserve shebang)
const newCode = shebang + code.slice(0, emptyInitFunc.start) + newInitBody + code.slice(emptyInitFunc.end);

// Backup original
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const backupPath = cliPath + '.backup-' + timestamp;
fs.copyFileSync(cliPath, backupPath);
console.log('');
console.log('Backup:', backupPath);

// Write patched file
fs.writeFileSync(cliPath, newCode);

// Verify
if (fs.readFileSync(cliPath, 'utf-8').includes(newInitBody)) {
    console.log('');
    console.log('\x1b[32mβœ“\x1b[0m Fix applied successfully!');
} else {
    console.error('\x1b[31mβœ—\x1b[0m Verification failed, restoring backup...');
    fs.copyFileSync(backupPath, cliPath);
    process.exit(1);
}
NODESCRIPT

# Run the patch script
node "$PATCH_SCRIPT" "$CLI_PATH" "$1"
EXIT_CODE=$?

# Cleanup temp script
rm -f "$PATCH_SCRIPT"

# Exit codes: 0 = patched, 1 = error/check-needed, 2 = already patched
if [ $EXIT_CODE -eq 0 ] && [ "$1" != "--check" ]; then
    # Newly patched - fix plugins and show restart message
    echo ""
    fix_plugin_configs
    echo ""
    print_warning "Restart Claude Code for changes to take effect"
elif [ $EXIT_CODE -eq 2 ]; then
    # Already patched - just exit cleanly
    exit 0
fi

exit $EXIT_CODE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment