From 74e202fa480f1c8a9e1600f992442e7a19c2a5db Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Tue, 12 May 2026 20:27:19 +0000 Subject: [PATCH 1/2] fix: install Claude Code AI plugin in both user and project scopes The wizard previously ran `claude plugin install posthog`, which defaults to user scope only. Now also install with `--scope project` so the plugin is recorded in `.claude/settings.json` for collaborators on the repo, in addition to the existing per-user install. Overall success is reported if at least one scope succeeds, so a stricter project scope failure (e.g. write-protected `.claude/`) does not regress the existing user-scope behavior. Generated-By: PostHog Code Task-Id: 66273d08-6eb4-42cf-96b7-ddfe48159e2b --- .../clients/__tests__/claude-code.test.ts | 59 ++++++++++++++++--- .../clients/claude-code.ts | 34 ++++++++--- 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts index 1aa2e3b2..b023ba93 100644 --- a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts +++ b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts @@ -79,13 +79,29 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { }); describe('installPlugin', () => { - it('returns success on exit 0', async () => { - execSyncMock.mockImplementation(() => Buffer.from('')); + it('installs into both user and project scopes and returns success', async () => { + const installCalls: string[] = []; + execSyncMock.mockImplementation((cmd: string) => { + if (String(cmd).includes('plugin install')) { + installCalls.push(String(cmd)); + } + return Buffer.from(''); + }); const client = new ClaudeCodeMCPClient(); - await expect(client.installPlugin()).resolves.toEqual({ success: true }); + await expect(client.installPlugin()).resolves.toEqual({ + success: true, + alreadyInstalled: false, + }); + expect(installCalls).toEqual( + expect.arrayContaining([ + expect.stringContaining('--scope user'), + expect.stringContaining('--scope project'), + ]), + ); + expect(installCalls).toHaveLength(2); }); - it('returns success with alreadyInstalled when stderr contains "already installed"', async () => { + it('returns success with alreadyInstalled when both scopes report already installed', async () => { execSyncMock.mockImplementation((cmd: string) => { if (String(cmd).includes('plugin install')) { throw new Error('already installed'); @@ -99,7 +115,7 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { }); }); - it('returns success with alreadyInstalled when stderr contains "already exists"', async () => { + it('returns success with alreadyInstalled when both scopes report already exists', async () => { execSyncMock.mockImplementation((cmd: string) => { if (String(cmd).includes('plugin install')) { throw new Error('already exists'); @@ -113,7 +129,26 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { }); }); - it('returns failure and captures exception on unexpected error', async () => { + it('returns success when only one scope fails', async () => { + execSyncMock.mockImplementation((cmd: string) => { + if (String(cmd).includes('--scope project')) { + throw new Error('network timeout'); + } + return Buffer.from(''); + }); + const client = new ClaudeCodeMCPClient(); + await expect(client.installPlugin()).resolves.toEqual({ + success: true, + alreadyInstalled: false, + }); + expect(analytics.captureException).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining('scope=project'), + }), + ); + }); + + it('returns failure when both scopes fail with unexpected error', async () => { execSyncMock.mockImplementation((cmd: string) => { if (String(cmd).includes('plugin install')) { throw new Error('network timeout'); @@ -121,10 +156,18 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { return Buffer.from(''); }); const client = new ClaudeCodeMCPClient(); - await expect(client.installPlugin()).resolves.toEqual({ success: false }); + await expect(client.installPlugin()).resolves.toEqual({ + success: false, + alreadyInstalled: false, + }); + expect(analytics.captureException).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining('scope=user'), + }), + ); expect(analytics.captureException).toHaveBeenCalledWith( expect.objectContaining({ - message: expect.stringContaining('network timeout'), + message: expect.stringContaining('scope=project'), }), ); }); diff --git a/src/steps/add-mcp-server-to-clients/clients/claude-code.ts b/src/steps/add-mcp-server-to-clients/clients/claude-code.ts index 19c88b92..b557d4fc 100644 --- a/src/steps/add-mcp-server-to-clients/clients/claude-code.ts +++ b/src/steps/add-mcp-server-to-clients/clients/claude-code.ts @@ -140,21 +140,39 @@ export class ClaudeCodeMCPClient } } - installPlugin(): Promise { - const binary = this.findClaudeBinary(); - if (!binary) return Promise.resolve({ success: false }); + private installPluginInScope( + binary: string, + scope: 'user' | 'project', + ): PluginInstallResult { try { - execSync(`${binary} plugin install posthog`, { stdio: 'pipe' }); - return Promise.resolve({ success: true }); + execSync(`${binary} plugin install posthog --scope ${scope}`, { + stdio: 'pipe', + }); + return { success: true }; } catch (error) { const msg = error instanceof Error ? error.message : String(error); if (msg.includes('already installed') || msg.includes('already exists')) { - return Promise.resolve({ success: true, alreadyInstalled: true }); + return { success: true, alreadyInstalled: true }; } analytics.captureException( - new Error(`Claude Code plugin install failed: ${msg}`), + new Error(`Claude Code plugin install failed (scope=${scope}): ${msg}`), ); - return Promise.resolve({ success: false }); + return { success: false }; } } + + installPlugin(): Promise { + const binary = this.findClaudeBinary(); + if (!binary) return Promise.resolve({ success: false }); + + const userResult = this.installPluginInScope(binary, 'user'); + const projectResult = this.installPluginInScope(binary, 'project'); + + return Promise.resolve({ + success: userResult.success || projectResult.success, + alreadyInstalled: Boolean( + userResult.alreadyInstalled && projectResult.alreadyInstalled, + ), + }); + } } From 1b00a561ad332a4251bac3f74c2fb7bd44bb2633 Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Tue, 12 May 2026 20:42:01 +0000 Subject: [PATCH 2/2] feat: let users pick the AI plugin install scope (user/project/both) Adds a `PluginScope` parameter ('user' | 'project' | 'both') to the plugin install path and a scope picker in `McpScreen` shown after feature selection, when at least one selected client supports plugins and `.git` exists in the working directory. User scope is the default (matches the previous behavior); project scope writes the plugin to `.claude/settings.json` so collaborators pick it up; "both" installs to both places. When no `.git` is present, the picker is skipped and the user-scope default is used, since project scope would not be meaningful. - plugin-client: new PluginScope type + DEFAULT_PLUGIN_SCOPE='user' - claude-code: installPlugin(scope) runs `claude plugin install posthog --scope ` for each requested scope; 'both' aggregates results (success if either succeeds, alreadyInstalled if both report so) - codex: accepts the param but ignores it (no scope concept) - mcp-installer: new optional scope arg forwarded to the orchestrator and into analytics - McpScreen: new PluginScopeSelect phase shown only when meaningful - Tests updated for the new defaults and scope plumbing Generated-By: PostHog Code Task-Id: 66273d08-6eb4-42cf-96b7-ddfe48159e2b --- .../clients/__tests__/claude-code.test.ts | 52 ++++++++++-- .../clients/claude-code.ts | 23 +++-- .../clients/codex.ts | 8 +- src/steps/add-mcp-server-to-clients/index.ts | 10 ++- .../plugin-client.ts | 6 +- src/ui/tui/__tests__/mcp-installer.test.ts | 25 +++++- src/ui/tui/screens/McpScreen.tsx | 84 +++++++++++++++++-- src/ui/tui/services/mcp-installer.ts | 16 +++- 8 files changed, 190 insertions(+), 34 deletions(-) diff --git a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts index b023ba93..54f84d7f 100644 --- a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts +++ b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts @@ -79,7 +79,7 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { }); describe('installPlugin', () => { - it('installs into both user and project scopes and returns success', async () => { + it('defaults to user scope and returns success', async () => { const installCalls: string[] = []; execSyncMock.mockImplementation((cmd: string) => { if (String(cmd).includes('plugin install')) { @@ -92,6 +92,40 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { success: true, alreadyInstalled: false, }); + expect(installCalls).toHaveLength(1); + expect(installCalls[0]).toContain('--scope user'); + }); + + it('installs in project scope only when scope=project', async () => { + const installCalls: string[] = []; + execSyncMock.mockImplementation((cmd: string) => { + if (String(cmd).includes('plugin install')) { + installCalls.push(String(cmd)); + } + return Buffer.from(''); + }); + const client = new ClaudeCodeMCPClient(); + await expect(client.installPlugin('project')).resolves.toEqual({ + success: true, + alreadyInstalled: false, + }); + expect(installCalls).toHaveLength(1); + expect(installCalls[0]).toContain('--scope project'); + }); + + it('installs in both scopes when scope=both', async () => { + const installCalls: string[] = []; + execSyncMock.mockImplementation((cmd: string) => { + if (String(cmd).includes('plugin install')) { + installCalls.push(String(cmd)); + } + return Buffer.from(''); + }); + const client = new ClaudeCodeMCPClient(); + await expect(client.installPlugin('both')).resolves.toEqual({ + success: true, + alreadyInstalled: false, + }); expect(installCalls).toEqual( expect.arrayContaining([ expect.stringContaining('--scope user'), @@ -101,7 +135,7 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { expect(installCalls).toHaveLength(2); }); - it('returns success with alreadyInstalled when both scopes report already installed', async () => { + it('returns alreadyInstalled when scope=both and both scopes already have the plugin', async () => { execSyncMock.mockImplementation((cmd: string) => { if (String(cmd).includes('plugin install')) { throw new Error('already installed'); @@ -109,13 +143,13 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { return Buffer.from(''); }); const client = new ClaudeCodeMCPClient(); - await expect(client.installPlugin()).resolves.toEqual({ + await expect(client.installPlugin('both')).resolves.toEqual({ success: true, alreadyInstalled: true, }); }); - it('returns success with alreadyInstalled when both scopes report already exists', async () => { + it('returns alreadyInstalled when single scope reports already exists', async () => { execSyncMock.mockImplementation((cmd: string) => { if (String(cmd).includes('plugin install')) { throw new Error('already exists'); @@ -123,13 +157,13 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { return Buffer.from(''); }); const client = new ClaudeCodeMCPClient(); - await expect(client.installPlugin()).resolves.toEqual({ + await expect(client.installPlugin('user')).resolves.toEqual({ success: true, alreadyInstalled: true, }); }); - it('returns success when only one scope fails', async () => { + it('scope=both still succeeds when only one scope fails', async () => { execSyncMock.mockImplementation((cmd: string) => { if (String(cmd).includes('--scope project')) { throw new Error('network timeout'); @@ -137,7 +171,7 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { return Buffer.from(''); }); const client = new ClaudeCodeMCPClient(); - await expect(client.installPlugin()).resolves.toEqual({ + await expect(client.installPlugin('both')).resolves.toEqual({ success: true, alreadyInstalled: false, }); @@ -148,7 +182,7 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { ); }); - it('returns failure when both scopes fail with unexpected error', async () => { + it('scope=both fails when both scopes fail with unexpected errors', async () => { execSyncMock.mockImplementation((cmd: string) => { if (String(cmd).includes('plugin install')) { throw new Error('network timeout'); @@ -156,7 +190,7 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { return Buffer.from(''); }); const client = new ClaudeCodeMCPClient(); - await expect(client.installPlugin()).resolves.toEqual({ + await expect(client.installPlugin('both')).resolves.toEqual({ success: false, alreadyInstalled: false, }); diff --git a/src/steps/add-mcp-server-to-clients/clients/claude-code.ts b/src/steps/add-mcp-server-to-clients/clients/claude-code.ts index b557d4fc..b8cb08bb 100644 --- a/src/steps/add-mcp-server-to-clients/clients/claude-code.ts +++ b/src/steps/add-mcp-server-to-clients/clients/claude-code.ts @@ -1,6 +1,11 @@ import { DefaultMCPClient } from '../MCPClient'; import { DefaultMCPClientConfig } from '../defaults'; -import { PluginCapable, PluginInstallResult } from '../plugin-client'; +import { + DEFAULT_PLUGIN_SCOPE, + PluginCapable, + PluginInstallResult, + PluginScope, +} from '../plugin-client'; import { z } from 'zod'; import { execSync } from 'child_process'; import { analytics } from '../../../utils/analytics'; @@ -161,18 +166,20 @@ export class ClaudeCodeMCPClient } } - installPlugin(): Promise { + installPlugin( + scope: PluginScope = DEFAULT_PLUGIN_SCOPE, + ): Promise { const binary = this.findClaudeBinary(); if (!binary) return Promise.resolve({ success: false }); - const userResult = this.installPluginInScope(binary, 'user'); - const projectResult = this.installPluginInScope(binary, 'project'); + const scopes: Array<'user' | 'project'> = + scope === 'both' ? ['user', 'project'] : [scope]; + + const results = scopes.map((s) => this.installPluginInScope(binary, s)); return Promise.resolve({ - success: userResult.success || projectResult.success, - alreadyInstalled: Boolean( - userResult.alreadyInstalled && projectResult.alreadyInstalled, - ), + success: results.some((r) => r.success), + alreadyInstalled: results.every((r) => r.alreadyInstalled === true), }); } } diff --git a/src/steps/add-mcp-server-to-clients/clients/codex.ts b/src/steps/add-mcp-server-to-clients/clients/codex.ts index 937e75b1..9451b673 100644 --- a/src/steps/add-mcp-server-to-clients/clients/codex.ts +++ b/src/steps/add-mcp-server-to-clients/clients/codex.ts @@ -6,7 +6,11 @@ import * as path from 'node:path'; import { DefaultMCPClient } from '../MCPClient'; import { DefaultMCPClientConfig } from '../defaults'; -import { PluginCapable, PluginInstallResult } from '../plugin-client'; +import { + PluginCapable, + PluginInstallResult, + PluginScope, +} from '../plugin-client'; import { analytics } from '../../../utils/analytics'; @@ -90,7 +94,7 @@ export class CodexMCPClient extends DefaultMCPClient implements PluginCapable { } } - installPlugin(): Promise { + installPlugin(_scope?: PluginScope): Promise { const binary = this.findCodexBinary(); if (!binary) return Promise.resolve({ success: false }); diff --git a/src/steps/add-mcp-server-to-clients/index.ts b/src/steps/add-mcp-server-to-clients/index.ts index 94a75018..aea47620 100644 --- a/src/steps/add-mcp-server-to-clients/index.ts +++ b/src/steps/add-mcp-server-to-clients/index.ts @@ -11,7 +11,12 @@ import { ZedClient } from './clients/zed'; import { CodexMCPClient } from './clients/codex'; import { ALL_FEATURE_VALUES } from './defaults'; import { debug } from '../../utils/debug'; -import { isPluginCapable, PluginCapable } from './plugin-client'; +import { + DEFAULT_PLUGIN_SCOPE, + isPluginCapable, + PluginCapable, + PluginScope, +} from './plugin-client'; export const getSupportedClients = async (): Promise => { const allClients = [ @@ -162,11 +167,12 @@ export const getSupportedPluginClients = ( export const installPlugins = async ( clients: Array, + scope: PluginScope = DEFAULT_PLUGIN_SCOPE, ): Promise => { const installed: string[] = []; for (const client of clients) { try { - const result = await client.installPlugin(); + const result = await client.installPlugin(scope); if (result.success) installed.push(client.name); } catch (err) { debug(`[installPlugins] installPlugin threw for ${client.name}: ${err}`); diff --git a/src/steps/add-mcp-server-to-clients/plugin-client.ts b/src/steps/add-mcp-server-to-clients/plugin-client.ts index 3d05bf7a..3f96dd32 100644 --- a/src/steps/add-mcp-server-to-clients/plugin-client.ts +++ b/src/steps/add-mcp-server-to-clients/plugin-client.ts @@ -1,3 +1,7 @@ +export type PluginScope = 'user' | 'project' | 'both'; + +export const DEFAULT_PLUGIN_SCOPE: PluginScope = 'user'; + export interface PluginInstallResult { success: boolean; alreadyInstalled?: boolean; @@ -6,7 +10,7 @@ export interface PluginInstallResult { export interface PluginCapable { supportsPlugin(): boolean; isPluginInstalled(): Promise; - installPlugin(): Promise; + installPlugin(scope?: PluginScope): Promise; } export function isPluginCapable(client: T): client is T & PluginCapable { diff --git a/src/ui/tui/__tests__/mcp-installer.test.ts b/src/ui/tui/__tests__/mcp-installer.test.ts index 50f218e8..7744986b 100644 --- a/src/ui/tui/__tests__/mcp-installer.test.ts +++ b/src/ui/tui/__tests__/mcp-installer.test.ts @@ -49,23 +49,41 @@ describe('createMcpInstaller — installPlugins', () => { mockClaudeClient, mockCursorClient, ]); - expect(mcpModule.installPlugins).toHaveBeenCalledWith([mockClaudeClient]); + expect(mcpModule.installPlugins).toHaveBeenCalledWith( + [mockClaudeClient], + 'user', + ); expect(result).toEqual(['Claude Code']); }); - it('emits mcp plugins installed analytics with clients and attempted', async () => { + it('forwards the chosen plugin scope to the orchestrator', async () => { + mcpModule.getSupportedPluginClients.mockReturnValue([mockClaudeClient]); + mcpModule.installPlugins.mockResolvedValue(['Claude Code']); + + const installer = createMcpInstaller(); + await installer.detectClients(); + await installer.installPlugins(['Claude Code'], 'both'); + + expect(mcpModule.installPlugins).toHaveBeenCalledWith( + [mockClaudeClient], + 'both', + ); + }); + + it('emits mcp plugins installed analytics with clients, attempted, and scope', async () => { mcpModule.getSupportedPluginClients.mockReturnValue([mockClaudeClient]); mcpModule.installPlugins.mockResolvedValue(['Claude Code']); const installer = createMcpInstaller(); await installer.detectClients(); - await installer.installPlugins(['Claude Code']); + await installer.installPlugins(['Claude Code'], 'project'); expect(analytics.wizardCapture).toHaveBeenCalledWith( 'mcp plugins installed', { clients: ['Claude Code'], attempted: ['Claude Code'], + scope: 'project', }, ); }); @@ -84,6 +102,7 @@ describe('createMcpInstaller — installPlugins', () => { { clients: [], attempted: [], + scope: 'user', }, ); }); diff --git a/src/ui/tui/screens/McpScreen.tsx b/src/ui/tui/screens/McpScreen.tsx index f536cf55..7a201401 100644 --- a/src/ui/tui/screens/McpScreen.tsx +++ b/src/ui/tui/screens/McpScreen.tsx @@ -14,6 +14,8 @@ import { Box, Text, useInput } from 'ink'; import { useState, useEffect } from 'react'; import { useSyncExternalStore } from 'react'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; import { type WizardStore, McpOutcome } from '../store.js'; import { ConfirmationInput, @@ -26,9 +28,21 @@ import { AVAILABLE_FEATURES, ALL_FEATURE_VALUES, } from '../../../steps/add-mcp-server-to-clients/defaults.js'; +import { + DEFAULT_PLUGIN_SCOPE, + PluginScope, +} from '../../../steps/add-mcp-server-to-clients/plugin-client.js'; export type McpMode = 'install' | 'remove'; +const hasGitDirectory = (): boolean => { + try { + return fs.existsSync(path.join(process.cwd(), '.git')); + } catch { + return false; + } +}; + interface McpScreenProps { store: WizardStore; installer: McpInstaller; @@ -40,6 +54,7 @@ enum Phase { Ask = 'ask', Pick = 'pick', FeatureSelect = 'feature-select', + PluginScopeSelect = 'plugin-scope-select', Working = 'working', Done = 'done', None = 'none', @@ -72,8 +87,12 @@ export const McpScreen = ({ const [phase, setPhase] = useState(Phase.Detecting); const [clients, setClients] = useState([]); const [selectedClientNames, setSelectedClientNames] = useState([]); + const [selectedFeatures, setSelectedFeatures] = useState< + string[] | undefined + >(undefined); const [resultClients, setResultClients] = useState([]); const [pluginClients, setPluginClients] = useState([]); + const gitRepoDetected = hasGitDirectory(); useEffect(() => { void (async () => { @@ -93,11 +112,27 @@ export const McpScreen = ({ })(); }, [installer]); // eslint-disable-line + const anyClientSupportsPlugin = (names: string[]): boolean => + clients.some((c) => names.includes(c.name) && c.supportsPlugin); + + const proceedAfterFeatures = (names: string[], features: string[]) => { + setSelectedClientNames(names); + setSelectedFeatures(features); + // Only ask about plugin scope when there is a real choice: + // at least one selected client supports the plugin AND we're in a git repo. + // Otherwise the only sensible option is the user-scope default. + if (anyClientSupportsPlugin(names) && gitRepoDetected) { + setPhase(Phase.PluginScopeSelect); + } else { + void doInstall(names, features, DEFAULT_PLUGIN_SCOPE); + } + }; + const proceedToFeatureSelectOrInstall = (clientNames: string[]) => { setSelectedClientNames(clientNames); // Skip feature picker if CLI already specified features if (store.session.mcpFeatures) { - void doInstall(clientNames, store.session.mcpFeatures); + proceedAfterFeatures(clientNames, store.session.mcpFeatures); } else { setPhase(Phase.FeatureSelect); } @@ -117,7 +152,11 @@ export const McpScreen = ({ markDone(store, McpOutcome.Skipped); }; - const doInstall = async (names: string[], features?: string[]) => { + const doInstall = async ( + names: string[], + features?: string[], + pluginScope: PluginScope = DEFAULT_PLUGIN_SCOPE, + ) => { setPhase(Phase.Working); let mcpResult: string[] = []; let pluginResult: string[] = []; @@ -131,7 +170,7 @@ export const McpScreen = ({ // mcpResult stays [] } try { - pluginResult = await installer.installPlugins(names); + pluginResult = await installer.installPlugins(names, pluginScope); } catch { // best-effort — plugin failure does not affect MCP outcome } @@ -218,7 +257,40 @@ export const McpScreen = ({ groups={AVAILABLE_FEATURES} initialSelected={[...ALL_FEATURE_VALUES]} onSelect={(features) => { - void doInstall(selectedClientNames, features); + proceedAfterFeatures(selectedClientNames, features); + }} + /> + )} + + {phase === Phase.PluginScopeSelect && ( + + message="Where should the PostHog plugin be installed?" + options={[ + { + label: 'User (global, default)', + value: 'user', + hint: 'Available across all your projects', + }, + { + label: 'Project (shared)', + value: 'project', + hint: 'Committed via .claude/settings.json so your team gets it', + }, + { + label: 'Both', + value: 'both', + hint: 'Install globally and share with the project', + }, + ]} + onSelect={(value) => { + const scope = (Array.isArray(value) ? value[0] : value) as + | PluginScope + | undefined; + void doInstall( + selectedClientNames, + selectedFeatures, + scope ?? DEFAULT_PLUGIN_SCOPE, + ); }} /> )} @@ -235,7 +307,9 @@ export const McpScreen = ({ <> {'\u2714'} MCP server - {!isRemove && pluginClients.length > 0 ? ' and plugin' : ''}{' '} + {!isRemove && pluginClients.length > 0 + ? ' and plugin' + : ''}{' '} {isRemove ? 'removed from' : 'installed for'}: {resultClients.map((name, i) => ( diff --git a/src/ui/tui/services/mcp-installer.ts b/src/ui/tui/services/mcp-installer.ts index 4fd39aa4..4a35b44c 100644 --- a/src/ui/tui/services/mcp-installer.ts +++ b/src/ui/tui/services/mcp-installer.ts @@ -13,7 +13,11 @@ import { installPlugins as runPluginInstall, } from '../../../steps/add-mcp-server-to-clients/index.js'; import { ALL_FEATURE_VALUES } from '../../../steps/add-mcp-server-to-clients/defaults.js'; -import { isPluginCapable } from '../../../steps/add-mcp-server-to-clients/plugin-client.js'; +import { + DEFAULT_PLUGIN_SCOPE, + isPluginCapable, + PluginScope, +} from '../../../steps/add-mcp-server-to-clients/plugin-client.js'; import { logToFile } from '../../../utils/debug.js'; import { analytics } from '../../../utils/analytics.js'; @@ -37,7 +41,7 @@ export interface McpInstaller { remove(): Promise; /** Install the PostHog AI plugin to supported clients. Best-effort: failures do not affect MCP outcome. */ - installPlugins(clientNames: string[]): Promise; + installPlugins(clientNames: string[], scope?: PluginScope): Promise; } /** @@ -110,18 +114,22 @@ export function createMcpInstaller(): McpInstaller { return installed.map((c) => c.name); }, - async installPlugins(clientNames: string[]): Promise { + async installPlugins( + clientNames: string[], + scope: PluginScope = DEFAULT_PLUGIN_SCOPE, + ): Promise { const rawClients = cachedClients .filter((c) => clientNames.includes(c.name)) // eslint-disable-next-line @typescript-eslint/no-explicit-any .map((c) => c.raw as any); const pluginClients = getSupportedPluginClients(rawClients); - const installed = await runPluginInstall(pluginClients); + const installed = await runPluginInstall(pluginClients, scope); analytics.wizardCapture('mcp plugins installed', { clients: installed, attempted: pluginClients.map((c) => c.name), + scope, }); return installed;