/ utils / plugins / pluginIdentifier.ts
pluginIdentifier.ts
  1  import type {
  2    EditableSettingSource,
  3    SettingSource,
  4  } from '../settings/constants.js'
  5  import {
  6    ALLOWED_OFFICIAL_MARKETPLACE_NAMES,
  7    type PluginScope,
  8  } from './schemas.js'
  9  
 10  /**
 11   * Extended scope type that includes 'flag' for session-only plugins.
 12   * 'flag' scope is NOT persisted to installed_plugins.json.
 13   */
 14  export type ExtendedPluginScope = PluginScope | 'flag'
 15  
 16  /**
 17   * Scopes that are persisted to installed_plugins.json.
 18   * Excludes 'flag' which is session-only.
 19   */
 20  export type PersistablePluginScope = Exclude<ExtendedPluginScope, 'flag'>
 21  
 22  /**
 23   * Map from SettingSource to plugin scope.
 24   * Note: flagSettings maps to 'flag' which is session-only and not persisted.
 25   */
 26  export const SETTING_SOURCE_TO_SCOPE = {
 27    policySettings: 'managed',
 28    userSettings: 'user',
 29    projectSettings: 'project',
 30    localSettings: 'local',
 31    flagSettings: 'flag',
 32  } as const satisfies Record<SettingSource, ExtendedPluginScope>
 33  
 34  /**
 35   * Parsed plugin identifier with name and optional marketplace
 36   */
 37  export type ParsedPluginIdentifier = {
 38    name: string
 39    marketplace?: string
 40  }
 41  
 42  /**
 43   * Parse a plugin identifier string into name and marketplace components
 44   * @param plugin The plugin identifier (name or name@marketplace)
 45   * @returns Parsed plugin name and optional marketplace
 46   *
 47   * Note: Only the first '@' is used as separator. If the input contains multiple '@' symbols
 48   * (e.g., "plugin@market@place"), everything after the second '@' is ignored.
 49   * This is intentional as marketplace names should not contain '@'.
 50   */
 51  export function parsePluginIdentifier(plugin: string): ParsedPluginIdentifier {
 52    if (plugin.includes('@')) {
 53      const parts = plugin.split('@')
 54      return { name: parts[0] || '', marketplace: parts[1] }
 55    }
 56    return { name: plugin }
 57  }
 58  
 59  /**
 60   * Build a plugin ID from name and marketplace
 61   * @param name The plugin name
 62   * @param marketplace Optional marketplace name
 63   * @returns Plugin ID in format "name" or "name@marketplace"
 64   */
 65  export function buildPluginId(name: string, marketplace?: string): string {
 66    return marketplace ? `${name}@${marketplace}` : name
 67  }
 68  
 69  /**
 70   * Check if a marketplace name is an official (Anthropic-controlled) marketplace.
 71   * Used for telemetry redaction — official plugin identifiers are safe to log to
 72   * general-access additional_metadata; third-party identifiers go only to the
 73   * PII-tagged _PROTO_* BQ columns.
 74   */
 75  export function isOfficialMarketplaceName(
 76    marketplace: string | undefined,
 77  ): boolean {
 78    return (
 79      marketplace !== undefined &&
 80      ALLOWED_OFFICIAL_MARKETPLACE_NAMES.has(marketplace.toLowerCase())
 81    )
 82  }
 83  
 84  /**
 85   * Map from installable plugin scope to editable setting source.
 86   * This is the inverse of SETTING_SOURCE_TO_SCOPE for editable scopes only.
 87   * Note: 'managed' scope cannot be installed to, so it's not included here.
 88   */
 89  const SCOPE_TO_EDITABLE_SOURCE: Record<
 90    Exclude<PluginScope, 'managed'>,
 91    EditableSettingSource
 92  > = {
 93    user: 'userSettings',
 94    project: 'projectSettings',
 95    local: 'localSettings',
 96  }
 97  
 98  /**
 99   * Convert a plugin scope to its corresponding editable setting source
100   * @param scope The plugin installation scope
101   * @returns The corresponding setting source for reading/writing settings
102   * @throws Error if scope is 'managed' (cannot install plugins to managed scope)
103   */
104  export function scopeToSettingSource(
105    scope: PluginScope,
106  ): EditableSettingSource {
107    if (scope === 'managed') {
108      throw new Error('Cannot install plugins to managed scope')
109    }
110    return SCOPE_TO_EDITABLE_SOURCE[scope]
111  }
112  
113  /**
114   * Convert an editable setting source to its corresponding plugin scope.
115   * Derived from SETTING_SOURCE_TO_SCOPE to maintain a single source of truth.
116   * @param source The setting source
117   * @returns The corresponding plugin scope
118   */
119  export function settingSourceToScope(
120    source: EditableSettingSource,
121  ): Exclude<PluginScope, 'managed'> {
122    return SETTING_SOURCE_TO_SCOPE[source] as Exclude<PluginScope, 'managed'>
123  }