Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ jest.mock('../../../../utils/analytics', () => ({

jest.mock('../../../../utils/debug', () => ({
debug: jest.fn(),
logToFile: jest.fn(),
}));

const warnMock = jest.fn();
jest.mock('../../../../ui', () => ({
getUI: () => ({
log: {
info: jest.fn(),
warn: warnMock,
error: jest.fn(),
success: jest.fn(),
step: jest.fn(),
},
}),
}));

describe('ClaudeCodeMCPClient — plugin methods', () => {
Expand Down Expand Up @@ -129,6 +143,55 @@ describe('ClaudeCodeMCPClient — plugin methods', () => {
);
});

it('returns failure without captureException when settings.json is malformed', async () => {
execSyncMock.mockImplementation((cmd: string) => {
if (String(cmd).includes('plugin install')) {
throw new Error(
'✘ Failed to install plugin "posthog": Failed to update settings: Invalid JSON syntax in settings file at /home/u/.claude/settings.json',
);
}
return Buffer.from('');
});
const client = new ClaudeCodeMCPClient();
await expect(client.installPlugin()).resolves.toEqual({ success: false });
expect(analytics.captureException).not.toHaveBeenCalled();
expect(warnMock).toHaveBeenCalledWith(
expect.stringContaining('settings.json'),
);
});

it('returns failure without captureException when marketplace is missing', async () => {
execSyncMock.mockImplementation((cmd: string) => {
if (String(cmd).includes('plugin install')) {
throw new Error(
'Plugin posthog not found in any configured marketplace',
);
}
return Buffer.from('');
});
const client = new ClaudeCodeMCPClient();
await expect(client.installPlugin()).resolves.toEqual({ success: false });
expect(analytics.captureException).not.toHaveBeenCalled();
expect(warnMock).toHaveBeenCalledWith(
expect.stringContaining('marketplace'),
);
});

it('returns failure without captureException when Claude Code is too old', async () => {
execSyncMock.mockImplementation((cmd: string) => {
if (String(cmd).includes('plugin install')) {
throw new Error("unknown command 'plugin'");
}
return Buffer.from('');
});
const client = new ClaudeCodeMCPClient();
await expect(client.installPlugin()).resolves.toEqual({ success: false });
expect(analytics.captureException).not.toHaveBeenCalled();
expect(warnMock).toHaveBeenCalledWith(
expect.stringContaining('Upgrade Claude Code'),
);
});

it('returns failure when no binary is found', async () => {
execSyncMock.mockImplementation(() => {
throw new Error('not found');
Expand Down
43 changes: 42 additions & 1 deletion src/steps/add-mcp-server-to-clients/clients/claude-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,46 @@ import { PluginCapable, PluginInstallResult } from '../plugin-client';
import { z } from 'zod';
import { execSync } from 'child_process';
import { analytics } from '../../../utils/analytics';
import { debug } from '../../../utils/debug';
import { debug, logToFile } from '../../../utils/debug';
import { getUI } from '../../../ui';
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';

// Stderr signatures that indicate the user's local Claude Code environment is
// the problem, not the wizard. These shouldn't pollute error tracking — we
// surface them to the user and move on.
const USER_ENV_PLUGIN_INSTALL_ERRORS: Array<{
pattern: RegExp;
userMessage: string;
}> = [
{
pattern: /Invalid JSON syntax in settings file/i,
userMessage:
'Your Claude Code settings.json has invalid JSON. Fix the syntax there and re-run the wizard to install the plugin.',
},
{
pattern: /not found in any configured marketplace/i,
userMessage:
'The PostHog marketplace is not configured in your Claude Code install, so the plugin could not be installed.',
},
{
pattern:
/unknown command|unrecognized command|not a claude command|invalid (sub)?command/i,
userMessage:
'Your Claude Code CLI is too old to support `plugin install`. Upgrade Claude Code and re-run the wizard to install the plugin.',
},
];

function classifyPluginInstallError(
msg: string,
): { userMessage: string } | null {
for (const { pattern, userMessage } of USER_ENV_PLUGIN_INSTALL_ERRORS) {
if (pattern.test(msg)) return { userMessage };
}
return null;
}

export const ClaudeCodeMCPConfig = DefaultMCPClientConfig;

export type ClaudeCodeMCPConfig = z.infer<typeof DefaultMCPClientConfig>;
Expand Down Expand Up @@ -151,6 +186,12 @@ export class ClaudeCodeMCPClient
if (msg.includes('already installed') || msg.includes('already exists')) {
return Promise.resolve({ success: true, alreadyInstalled: true });
}
const classified = classifyPluginInstallError(msg);
if (classified) {
getUI().log.warn(classified.userMessage);
logToFile(`[ClaudeCodeMCPClient] plugin install soft-failed: ${msg}`);
return Promise.resolve({ success: false });
}
analytics.captureException(
new Error(`Claude Code plugin install failed: ${msg}`),
);
Expand Down
Loading