init claude-code
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Plugin-hint recommendations.
|
||||
*
|
||||
* Companion to lspRecommendation.ts: where LSP recommendations are triggered
|
||||
* by file edits, plugin hints are triggered by CLIs/SDKs emitting a
|
||||
* `<claude-code-hint />` tag to stderr (detected by the Bash/PowerShell tools).
|
||||
*
|
||||
* State persists in GlobalConfig.claudeCodeHints — a show-once record per
|
||||
* plugin and a disabled flag (user picked "don't show again"). Official-
|
||||
* marketplace filtering is hardcoded for v1.
|
||||
*/
|
||||
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import {
|
||||
type ClaudeCodeHint,
|
||||
hasShownHintThisSession,
|
||||
setPendingHint,
|
||||
} from '../claudeCodeHints.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../config.js'
|
||||
import { logForDebugging } from '../debug.js'
|
||||
import { isPluginInstalled } from './installedPluginsManager.js'
|
||||
import { getPluginById } from './marketplaceManager.js'
|
||||
import {
|
||||
isOfficialMarketplaceName,
|
||||
parsePluginIdentifier,
|
||||
} from './pluginIdentifier.js'
|
||||
import { isPluginBlockedByPolicy } from './pluginPolicy.js'
|
||||
|
||||
/**
|
||||
* Hard cap on `claudeCodeHints.plugin[]` — bounds config growth. Each shown
|
||||
* plugin appends one slug; past this point we stop prompting (and stop
|
||||
* appending) rather than let the config grow without limit.
|
||||
*/
|
||||
const MAX_SHOWN_PLUGINS = 100
|
||||
|
||||
export type PluginHintRecommendation = {
|
||||
pluginId: string
|
||||
pluginName: string
|
||||
marketplaceName: string
|
||||
pluginDescription?: string
|
||||
sourceCommand: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-store gate called by shell tools when a `type="plugin"` hint is detected.
|
||||
* Drops the hint if:
|
||||
*
|
||||
* - a dialog has already been shown this session
|
||||
* - user has disabled hints
|
||||
* - the shown-plugins list has hit the config-growth cap
|
||||
* - plugin slug doesn't parse as `name@marketplace`
|
||||
* - marketplace isn't official (hardcoded for v1)
|
||||
* - plugin is already installed
|
||||
* - plugin was already shown in a prior session
|
||||
*
|
||||
* Synchronous on purpose — shell tools shouldn't await a marketplace lookup
|
||||
* just to strip a stderr line. The async marketplace-cache check happens
|
||||
* later in resolvePluginHint (hook side).
|
||||
*/
|
||||
export function maybeRecordPluginHint(hint: ClaudeCodeHint): void {
|
||||
if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_lapis_finch', false)) return
|
||||
if (hasShownHintThisSession()) return
|
||||
|
||||
const state = getGlobalConfig().claudeCodeHints
|
||||
if (state?.disabled) return
|
||||
|
||||
const shown = state?.plugin ?? []
|
||||
if (shown.length >= MAX_SHOWN_PLUGINS) return
|
||||
|
||||
const pluginId = hint.value
|
||||
const { name, marketplace } = parsePluginIdentifier(pluginId)
|
||||
if (!name || !marketplace) return
|
||||
if (!isOfficialMarketplaceName(marketplace)) return
|
||||
if (shown.includes(pluginId)) return
|
||||
if (isPluginInstalled(pluginId)) return
|
||||
if (isPluginBlockedByPolicy(pluginId)) return
|
||||
|
||||
// Bound repeat lookups on the same slug — a CLI that emits on every
|
||||
// invocation shouldn't trigger N resolve cycles for the same plugin.
|
||||
if (triedThisSession.has(pluginId)) return
|
||||
triedThisSession.add(pluginId)
|
||||
|
||||
setPendingHint(hint)
|
||||
}
|
||||
|
||||
const triedThisSession = new Set<string>()
|
||||
|
||||
/** Test-only reset. */
|
||||
export function _resetHintRecommendationForTesting(): void {
|
||||
triedThisSession.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the pending hint to a renderable recommendation. Runs the async
|
||||
* marketplace lookup that the sync pre-store gate skipped. Returns null if
|
||||
* the plugin isn't in the marketplace cache — the hint is discarded.
|
||||
*/
|
||||
export async function resolvePluginHint(
|
||||
hint: ClaudeCodeHint,
|
||||
): Promise<PluginHintRecommendation | null> {
|
||||
const pluginId = hint.value
|
||||
const { name, marketplace } = parsePluginIdentifier(pluginId)
|
||||
|
||||
const pluginData = await getPluginById(pluginId)
|
||||
|
||||
logEvent('tengu_plugin_hint_detected', {
|
||||
_PROTO_plugin_name: (name ??
|
||||
'') as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
|
||||
_PROTO_marketplace_name: (marketplace ??
|
||||
'') as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
|
||||
result: (pluginData
|
||||
? 'passed'
|
||||
: 'not_in_cache') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
if (!pluginData) {
|
||||
logForDebugging(
|
||||
`[hintRecommendation] ${pluginId} not found in marketplace cache`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
pluginId,
|
||||
pluginName: pluginData.entry.name,
|
||||
marketplaceName: marketplace ?? '',
|
||||
pluginDescription: pluginData.entry.description,
|
||||
sourceCommand: hint.sourceCommand,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that a prompt for this plugin was surfaced. Called regardless of
|
||||
* the user's yes/no response — show-once semantics.
|
||||
*/
|
||||
export function markHintPluginShown(pluginId: string): void {
|
||||
saveGlobalConfig(current => {
|
||||
const existing = current.claudeCodeHints?.plugin ?? []
|
||||
if (existing.includes(pluginId)) return current
|
||||
return {
|
||||
...current,
|
||||
claudeCodeHints: {
|
||||
...current.claudeCodeHints,
|
||||
plugin: [...existing, pluginId],
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Called when the user picks "don't show plugin installation hints again". */
|
||||
export function disableHintRecommendations(): void {
|
||||
saveGlobalConfig(current => {
|
||||
if (current.claudeCodeHints?.disabled) return current
|
||||
return {
|
||||
...current,
|
||||
claudeCodeHints: { ...current.claudeCodeHints, disabled: true },
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user