/** * Legacy plugin metadata helpers shared by call sites that still assemble * analytics-compatible payload shapes. * * In this fork the downstream analytics sinks are disabled, so these helpers * only normalize/redact fields for local compatibility code; they do not * imply an active telemetry export path. */ import { createHash } from 'crypto' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, } from '../../services/analytics/index.js' import type { PluginManifest } from '../../types/plugin.js' import { isOfficialMarketplaceName, parsePluginIdentifier, } from '../plugins/pluginIdentifier.js' // builtinPlugins.ts:BUILTIN_MARKETPLACE_NAME — inlined to avoid the cycle // through commands.js. Marketplace schemas.ts enforces 'builtin' is reserved. const BUILTIN_MARKETPLACE_NAME = 'builtin' // Fixed salt for plugin_id_hash. Kept stable so legacy field shapes that still // use this helper continue to derive the same opaque key. const PLUGIN_ID_HASH_SALT = 'claude-plugin-telemetry-v1' /** * Opaque per-plugin compatibility key derived from the name@marketplace * string. The 16-char truncation keeps the identifier short while preserving * a stable grouping key for local compatibility code. */ export function hashPluginId(name: string, marketplace?: string): string { const key = marketplace ? `${name}@${marketplace.toLowerCase()}` : name return createHash('sha256') .update(key + PLUGIN_ID_HASH_SALT) .digest('hex') .slice(0, 16) } /** * 4-value scope enum for plugin origin. Distinct from PluginScope * (managed/user/project/local) which is installation-target — this is * marketplace-origin. * * - official: from an allowlisted Anthropic marketplace * - default-bundle: ships with product (@builtin), auto-enabled * - org: enterprise admin-pushed via managed settings (policySettings) * - user-local: user added marketplace or local plugin */ export type TelemetryPluginScope = | 'official' | 'org' | 'user-local' | 'default-bundle' export function getTelemetryPluginScope( name: string, marketplace: string | undefined, managedNames: Set | null, ): TelemetryPluginScope { if (marketplace === BUILTIN_MARKETPLACE_NAME) return 'default-bundle' if (isOfficialMarketplaceName(marketplace)) return 'official' if (managedNames?.has(name)) return 'org' return 'user-local' } /** How a skill/command invocation was triggered. */ export type InvocationTrigger = | 'user-slash' | 'claude-proactive' | 'nested-skill' /** Where a skill invocation executes. */ export type SkillExecutionContext = 'fork' | 'inline' | 'remote' /** How a plugin install was initiated. */ export type InstallSource = | 'cli-explicit' | 'ui-discover' | 'ui-suggestion' | 'deep-link' /** * Common plugin metadata fields keyed off name@marketplace. Keeps the legacy * field set in one place so no-op analytics compatibility callers do not have * to duplicate redaction logic. */ export function buildPluginTelemetryFields( name: string, marketplace: string | undefined, managedNames: Set | null = null, ): { plugin_id_hash: AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS plugin_scope: AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS plugin_name_redacted: AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS marketplace_name_redacted: AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS is_official_plugin: boolean } { const scope = getTelemetryPluginScope(name, marketplace, managedNames) // Both official marketplaces and builtin plugins are Anthropic-controlled // — safe to expose real names in the redacted columns. const isAnthropicControlled = scope === 'official' || scope === 'default-bundle' return { plugin_id_hash: hashPluginId( name, marketplace, ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, plugin_scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, plugin_name_redacted: (isAnthropicControlled ? name : 'third-party') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, marketplace_name_redacted: (isAnthropicControlled && marketplace ? marketplace : 'third-party') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, is_official_plugin: isAnthropicControlled, } } /** * Per-invocation callers (SkillTool, processSlashCommand) pass * managedNames=null to keep hot-path call sites free of the extra settings read. */ export function buildPluginCommandTelemetryFields( pluginInfo: { pluginManifest: PluginManifest; repository: string }, managedNames: Set | null = null, ): ReturnType { const { marketplace } = parsePluginIdentifier(pluginInfo.repository) return buildPluginTelemetryFields( pluginInfo.pluginManifest.name, marketplace, managedNames, ) } /** * Stable error buckets for CLI plugin operation failures. */ export type PluginCommandErrorCategory = | 'network' | 'not-found' | 'permission' | 'validation' | 'unknown' export function classifyPluginCommandError( error: unknown, ): PluginCommandErrorCategory { const msg = String((error as { message?: unknown })?.message ?? error) if ( /ENOTFOUND|ECONNREFUSED|EAI_AGAIN|ETIMEDOUT|ECONNRESET|network|Could not resolve|Connection refused|timed out/i.test( msg, ) ) { return 'network' } if (/\b404\b|not found|does not exist|no such plugin/i.test(msg)) { return 'not-found' } if (/\b40[13]\b|EACCES|EPERM|permission denied|unauthorized/i.test(msg)) { return 'permission' } if (/invalid|malformed|schema|validation|parse error/i.test(msg)) { return 'validation' } return 'unknown' }