source dump of claude code
at main 164 lines 5.4 kB view raw
1/** 2 * Plugin-hint recommendations. 3 * 4 * Companion to lspRecommendation.ts: where LSP recommendations are triggered 5 * by file edits, plugin hints are triggered by CLIs/SDKs emitting a 6 * `<claude-code-hint />` tag to stderr (detected by the Bash/PowerShell tools). 7 * 8 * State persists in GlobalConfig.claudeCodeHints — a show-once record per 9 * plugin and a disabled flag (user picked "don't show again"). Official- 10 * marketplace filtering is hardcoded for v1. 11 */ 12 13import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' 14import { 15 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 16 type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 17 logEvent, 18} from '../../services/analytics/index.js' 19import { 20 type ClaudeCodeHint, 21 hasShownHintThisSession, 22 setPendingHint, 23} from '../claudeCodeHints.js' 24import { getGlobalConfig, saveGlobalConfig } from '../config.js' 25import { logForDebugging } from '../debug.js' 26import { isPluginInstalled } from './installedPluginsManager.js' 27import { getPluginById } from './marketplaceManager.js' 28import { 29 isOfficialMarketplaceName, 30 parsePluginIdentifier, 31} from './pluginIdentifier.js' 32import { isPluginBlockedByPolicy } from './pluginPolicy.js' 33 34/** 35 * Hard cap on `claudeCodeHints.plugin[]` — bounds config growth. Each shown 36 * plugin appends one slug; past this point we stop prompting (and stop 37 * appending) rather than let the config grow without limit. 38 */ 39const MAX_SHOWN_PLUGINS = 100 40 41export type PluginHintRecommendation = { 42 pluginId: string 43 pluginName: string 44 marketplaceName: string 45 pluginDescription?: string 46 sourceCommand: string 47} 48 49/** 50 * Pre-store gate called by shell tools when a `type="plugin"` hint is detected. 51 * Drops the hint if: 52 * 53 * - a dialog has already been shown this session 54 * - user has disabled hints 55 * - the shown-plugins list has hit the config-growth cap 56 * - plugin slug doesn't parse as `name@marketplace` 57 * - marketplace isn't official (hardcoded for v1) 58 * - plugin is already installed 59 * - plugin was already shown in a prior session 60 * 61 * Synchronous on purpose — shell tools shouldn't await a marketplace lookup 62 * just to strip a stderr line. The async marketplace-cache check happens 63 * later in resolvePluginHint (hook side). 64 */ 65export function maybeRecordPluginHint(hint: ClaudeCodeHint): void { 66 if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_lapis_finch', false)) return 67 if (hasShownHintThisSession()) return 68 69 const state = getGlobalConfig().claudeCodeHints 70 if (state?.disabled) return 71 72 const shown = state?.plugin ?? [] 73 if (shown.length >= MAX_SHOWN_PLUGINS) return 74 75 const pluginId = hint.value 76 const { name, marketplace } = parsePluginIdentifier(pluginId) 77 if (!name || !marketplace) return 78 if (!isOfficialMarketplaceName(marketplace)) return 79 if (shown.includes(pluginId)) return 80 if (isPluginInstalled(pluginId)) return 81 if (isPluginBlockedByPolicy(pluginId)) return 82 83 // Bound repeat lookups on the same slug — a CLI that emits on every 84 // invocation shouldn't trigger N resolve cycles for the same plugin. 85 if (triedThisSession.has(pluginId)) return 86 triedThisSession.add(pluginId) 87 88 setPendingHint(hint) 89} 90 91const triedThisSession = new Set<string>() 92 93/** Test-only reset. */ 94export function _resetHintRecommendationForTesting(): void { 95 triedThisSession.clear() 96} 97 98/** 99 * Resolve the pending hint to a renderable recommendation. Runs the async 100 * marketplace lookup that the sync pre-store gate skipped. Returns null if 101 * the plugin isn't in the marketplace cache — the hint is discarded. 102 */ 103export async function resolvePluginHint( 104 hint: ClaudeCodeHint, 105): Promise<PluginHintRecommendation | null> { 106 const pluginId = hint.value 107 const { name, marketplace } = parsePluginIdentifier(pluginId) 108 109 const pluginData = await getPluginById(pluginId) 110 111 logEvent('tengu_plugin_hint_detected', { 112 _PROTO_plugin_name: (name ?? 113 '') as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 114 _PROTO_marketplace_name: (marketplace ?? 115 '') as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 116 result: (pluginData 117 ? 'passed' 118 : 'not_in_cache') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 119 }) 120 121 if (!pluginData) { 122 logForDebugging( 123 `[hintRecommendation] ${pluginId} not found in marketplace cache`, 124 ) 125 return null 126 } 127 128 return { 129 pluginId, 130 pluginName: pluginData.entry.name, 131 marketplaceName: marketplace ?? '', 132 pluginDescription: pluginData.entry.description, 133 sourceCommand: hint.sourceCommand, 134 } 135} 136 137/** 138 * Record that a prompt for this plugin was surfaced. Called regardless of 139 * the user's yes/no response — show-once semantics. 140 */ 141export function markHintPluginShown(pluginId: string): void { 142 saveGlobalConfig(current => { 143 const existing = current.claudeCodeHints?.plugin ?? [] 144 if (existing.includes(pluginId)) return current 145 return { 146 ...current, 147 claudeCodeHints: { 148 ...current.claudeCodeHints, 149 plugin: [...existing, pluginId], 150 }, 151 } 152 }) 153} 154 155/** Called when the user picks "don't show plugin installation hints again". */ 156export function disableHintRecommendations(): void { 157 saveGlobalConfig(current => { 158 if (current.claudeCodeHints?.disabled) return current 159 return { 160 ...current, 161 claudeCodeHints: { ...current.claudeCodeHints, disabled: true }, 162 } 163 }) 164}