Skip to main content

ai_agent/types/
plugin.rs

1// Source: ~/claudecode/openclaudecode/src/types/plugin.ts
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Re-export of plugin author from schemas.
7pub type PluginAuthor = serde_json::Value;
8
9/// Re-export of plugin manifest from schemas.
10pub type PluginManifest = serde_json::Value;
11
12/// Re-export of command metadata from schemas.
13pub type CommandMetadata = serde_json::Value;
14
15/// Definition for a built-in plugin that ships with the CLI.
16pub struct BuiltinPluginDefinition {
17    /// Plugin name (used in `{name}@builtin` identifier)
18    pub name: String,
19    /// Description shown in the /plugin UI
20    pub description: String,
21    /// Optional version string
22    pub version: Option<String>,
23    /// Skills provided by this plugin
24    pub skills: Option<Vec<serde_json::Value>>,
25    /// Hooks provided by this plugin
26    pub hooks: Option<serde_json::Value>,
27    /// MCP servers provided by this plugin
28    pub mcp_servers: Option<HashMap<String, serde_json::Value>>,
29    /// Whether this plugin is available
30    pub is_available: Option<Box<dyn Fn() -> bool + Send + Sync>>,
31    /// Default enabled state before the user sets a preference
32    pub default_enabled: Option<bool>,
33}
34
35/// A plugin repository.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct PluginRepository {
38    pub url: String,
39    pub branch: String,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    #[serde(rename = "lastUpdated")]
42    pub last_updated: Option<String>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    #[serde(rename = "commitSha")]
45    pub commit_sha: Option<String>,
46}
47
48/// Plugin configuration with repositories.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct PluginConfig {
51    pub repositories: HashMap<String, PluginRepository>,
52}
53
54/// A loaded plugin.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct LoadedPlugin {
57    pub name: String,
58    pub manifest: PluginManifest,
59    pub path: String,
60    pub source: String,
61    /// Repository identifier
62    pub repository: String,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub enabled: Option<bool>,
65    /// True for built-in plugins that ship with the CLI
66    #[serde(skip_serializing_if = "Option::is_none")]
67    #[serde(rename = "isBuiltin")]
68    pub is_builtin: Option<bool>,
69    /// Git commit SHA for version pinning
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub sha: Option<String>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    #[serde(rename = "commandsPath")]
74    pub commands_path: Option<String>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    #[serde(rename = "commandsPaths")]
77    pub commands_paths: Option<Vec<String>>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    #[serde(rename = "commandsMetadata")]
80    pub commands_metadata: Option<HashMap<String, CommandMetadata>>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    #[serde(rename = "agentsPath")]
83    pub agents_path: Option<String>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    #[serde(rename = "agentsPaths")]
86    pub agents_paths: Option<Vec<String>>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    #[serde(rename = "skillsPath")]
89    pub skills_path: Option<String>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    #[serde(rename = "skillsPaths")]
92    pub skills_paths: Option<Vec<String>>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    #[serde(rename = "outputStylesPath")]
95    pub output_styles_path: Option<String>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    #[serde(rename = "outputStylesPaths")]
98    pub output_styles_paths: Option<Vec<String>>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    #[serde(rename = "hooksConfig")]
101    pub hooks_config: Option<serde_json::Value>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    #[serde(rename = "mcpServers")]
104    pub mcp_servers: Option<HashMap<String, serde_json::Value>>,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    #[serde(rename = "lspServers")]
107    pub lsp_servers: Option<HashMap<String, serde_json::Value>>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub settings: Option<HashMap<String, serde_json::Value>>,
110}
111
112/// Plugin component types.
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114#[serde(rename_all = "kebab-case")]
115pub enum PluginComponent {
116    Commands,
117    Agents,
118    Skills,
119    Hooks,
120    OutputStyles,
121}
122
123/// Discriminated union of plugin error types.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(tag = "type")]
126pub enum PluginError {
127    #[serde(rename = "path-not-found")]
128    PathNotFound {
129        source: String,
130        #[serde(skip_serializing_if = "Option::is_none")]
131        plugin: Option<String>,
132        path: String,
133        component: PluginComponent,
134    },
135    #[serde(rename = "git-auth-failed")]
136    GitAuthFailed {
137        source: String,
138        #[serde(skip_serializing_if = "Option::is_none")]
139        plugin: Option<String>,
140        #[serde(rename = "gitUrl")]
141        git_url: String,
142        #[serde(rename = "authType")]
143        auth_type: GitAuthType,
144    },
145    #[serde(rename = "git-timeout")]
146    GitTimeout {
147        source: String,
148        #[serde(skip_serializing_if = "Option::is_none")]
149        plugin: Option<String>,
150        #[serde(rename = "gitUrl")]
151        git_url: String,
152        operation: GitOperation,
153    },
154    #[serde(rename = "network-error")]
155    NetworkError {
156        source: String,
157        #[serde(skip_serializing_if = "Option::is_none")]
158        plugin: Option<String>,
159        url: String,
160        #[serde(skip_serializing_if = "Option::is_none")]
161        details: Option<String>,
162    },
163    #[serde(rename = "manifest-parse-error")]
164    ManifestParseError {
165        source: String,
166        #[serde(skip_serializing_if = "Option::is_none")]
167        plugin: Option<String>,
168        #[serde(rename = "manifestPath")]
169        manifest_path: String,
170        #[serde(rename = "parseError")]
171        parse_error: String,
172    },
173    #[serde(rename = "manifest-validation-error")]
174    ManifestValidationError {
175        source: String,
176        #[serde(skip_serializing_if = "Option::is_none")]
177        plugin: Option<String>,
178        #[serde(rename = "manifestPath")]
179        manifest_path: String,
180        #[serde(rename = "validationErrors")]
181        validation_errors: Vec<String>,
182    },
183    #[serde(rename = "plugin-not-found")]
184    PluginNotFound {
185        source: String,
186        #[serde(rename = "pluginId")]
187        plugin_id: String,
188        marketplace: String,
189    },
190    #[serde(rename = "marketplace-not-found")]
191    MarketplaceNotFound {
192        source: String,
193        marketplace: String,
194        #[serde(rename = "availableMarketplaces")]
195        available_marketplaces: Vec<String>,
196    },
197    #[serde(rename = "marketplace-load-failed")]
198    MarketplaceLoadFailed {
199        source: String,
200        marketplace: String,
201        reason: String,
202    },
203    #[serde(rename = "mcp-config-invalid")]
204    McpConfigInvalid {
205        source: String,
206        plugin: String,
207        #[serde(rename = "serverName")]
208        server_name: String,
209        #[serde(rename = "validationError")]
210        validation_error: String,
211    },
212    #[serde(rename = "mcp-server-suppressed-duplicate")]
213    McpServerSuppressedDuplicate {
214        source: String,
215        plugin: String,
216        #[serde(rename = "serverName")]
217        server_name: String,
218        #[serde(rename = "duplicateOf")]
219        duplicate_of: String,
220    },
221    #[serde(rename = "lsp-config-invalid")]
222    LspConfigInvalid {
223        source: String,
224        plugin: String,
225        #[serde(rename = "serverName")]
226        server_name: String,
227        #[serde(rename = "validationError")]
228        validation_error: String,
229    },
230    #[serde(rename = "hook-load-failed")]
231    HookLoadFailed {
232        source: String,
233        plugin: String,
234        #[serde(rename = "hookPath")]
235        hook_path: String,
236        reason: String,
237    },
238    #[serde(rename = "component-load-failed")]
239    ComponentLoadFailed {
240        source: String,
241        plugin: String,
242        component: PluginComponent,
243        path: String,
244        reason: String,
245    },
246    #[serde(rename = "mcpb-download-failed")]
247    McpbDownloadFailed {
248        source: String,
249        plugin: String,
250        url: String,
251        reason: String,
252    },
253    #[serde(rename = "mcpb-extract-failed")]
254    McpbExtractFailed {
255        source: String,
256        plugin: String,
257        #[serde(rename = "mcpbPath")]
258        mcpb_path: String,
259        reason: String,
260    },
261    #[serde(rename = "mcpb-invalid-manifest")]
262    McpbInvalidManifest {
263        source: String,
264        plugin: String,
265        #[serde(rename = "mcpbPath")]
266        mcpb_path: String,
267        #[serde(rename = "validationError")]
268        validation_error: String,
269    },
270    #[serde(rename = "lsp-server-start-failed")]
271    LspServerStartFailed {
272        source: String,
273        plugin: String,
274        #[serde(rename = "serverName")]
275        server_name: String,
276        reason: String,
277    },
278    #[serde(rename = "lsp-server-crashed")]
279    LspServerCrashed {
280        source: String,
281        plugin: String,
282        #[serde(rename = "serverName")]
283        server_name: String,
284        #[serde(rename = "exitCode")]
285        exit_code: Option<i32>,
286        #[serde(skip_serializing_if = "Option::is_none")]
287        signal: Option<String>,
288    },
289    #[serde(rename = "lsp-request-timeout")]
290    LspRequestTimeout {
291        source: String,
292        plugin: String,
293        #[serde(rename = "serverName")]
294        server_name: String,
295        method: String,
296        #[serde(rename = "timeoutMs")]
297        timeout_ms: u64,
298    },
299    #[serde(rename = "lsp-request-failed")]
300    LspRequestFailed {
301        source: String,
302        plugin: String,
303        #[serde(rename = "serverName")]
304        server_name: String,
305        method: String,
306        error: String,
307    },
308    #[serde(rename = "marketplace-blocked-by-policy")]
309    MarketplaceBlockedByPolicy {
310        source: String,
311        #[serde(skip_serializing_if = "Option::is_none")]
312        plugin: Option<String>,
313        marketplace: String,
314        #[serde(skip_serializing_if = "Option::is_none")]
315        #[serde(rename = "blockedByBlocklist")]
316        blocked_by_blocklist: Option<bool>,
317        #[serde(rename = "allowedSources")]
318        allowed_sources: Vec<String>,
319    },
320    #[serde(rename = "dependency-unsatisfied")]
321    DependencyUnsatisfied {
322        source: String,
323        plugin: String,
324        dependency: String,
325        reason: DependencyReason,
326    },
327    #[serde(rename = "plugin-cache-miss")]
328    PluginCacheMiss {
329        source: String,
330        plugin: String,
331        #[serde(rename = "installPath")]
332        install_path: String,
333    },
334    #[serde(rename = "generic-error")]
335    GenericError {
336        source: String,
337        #[serde(skip_serializing_if = "Option::is_none")]
338        plugin: Option<String>,
339        error: String,
340    },
341}
342
343/// Git authentication type.
344#[derive(Debug, Clone, Serialize, Deserialize)]
345#[serde(rename_all = "lowercase")]
346pub enum GitAuthType {
347    Ssh,
348    Https,
349}
350
351/// Git operation type.
352#[derive(Debug, Clone, Serialize, Deserialize)]
353#[serde(rename_all = "lowercase")]
354pub enum GitOperation {
355    Clone,
356    Pull,
357}
358
359/// Dependency reason.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361#[serde(rename_all = "kebab-case")]
362pub enum DependencyReason {
363    NotEnabled,
364    NotFound,
365}
366
367/// Plugin load result.
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct PluginLoadResult {
370    pub enabled: Vec<LoadedPlugin>,
371    pub disabled: Vec<LoadedPlugin>,
372    pub errors: Vec<PluginError>,
373}
374
375/// Helper function to get a display message from any PluginError.
376pub fn get_plugin_error_message(error: &PluginError) -> String {
377    match error {
378        PluginError::GenericError { error: msg, .. } => msg.clone(),
379        PluginError::PathNotFound {
380            path, component, ..
381        } => {
382            format!("Path not found: {} ({:?})", path, component)
383        }
384        PluginError::GitAuthFailed {
385            auth_type, git_url, ..
386        } => {
387            format!("Git authentication failed ({:?}): {}", auth_type, git_url)
388        }
389        PluginError::GitTimeout {
390            operation, git_url, ..
391        } => {
392            format!("Git {:?} timeout: {}", operation, git_url)
393        }
394        PluginError::NetworkError { url, details, .. } => {
395            if let Some(d) = details {
396                format!("Network error: {} - {}", url, d)
397            } else {
398                format!("Network error: {}", url)
399            }
400        }
401        PluginError::ManifestParseError { parse_error, .. } => {
402            format!("Manifest parse error: {}", parse_error)
403        }
404        PluginError::ManifestValidationError {
405            validation_errors, ..
406        } => {
407            format!(
408                "Manifest validation failed: {}",
409                validation_errors.join(", ")
410            )
411        }
412        PluginError::PluginNotFound {
413            plugin_id,
414            marketplace,
415            ..
416        } => {
417            format!(
418                "Plugin {} not found in marketplace {}",
419                plugin_id, marketplace
420            )
421        }
422        PluginError::MarketplaceNotFound { marketplace, .. } => {
423            format!("Marketplace {} not found", marketplace)
424        }
425        PluginError::MarketplaceLoadFailed {
426            marketplace,
427            reason,
428            ..
429        } => {
430            format!("Marketplace {} failed to load: {}", marketplace, reason)
431        }
432        PluginError::McpConfigInvalid {
433            server_name,
434            validation_error,
435            ..
436        } => {
437            format!("MCP server {} invalid: {}", server_name, validation_error)
438        }
439        PluginError::McpServerSuppressedDuplicate {
440            server_name,
441            duplicate_of,
442            ..
443        } => {
444            let dup = if duplicate_of.starts_with("plugin:") {
445                format!(
446                    "server provided by plugin \"{}\"",
447                    duplicate_of.split(':').nth(1).unwrap_or("?")
448                )
449            } else {
450                format!("already-configured \"{}\"", duplicate_of)
451            };
452            format!(
453                "MCP server \"{}\" skipped — same command/URL as {}",
454                server_name, dup
455            )
456        }
457        PluginError::HookLoadFailed { reason, .. } => {
458            format!("Hook load failed: {}", reason)
459        }
460        PluginError::ComponentLoadFailed {
461            component,
462            path,
463            reason,
464            ..
465        } => {
466            format!("{:?} load failed from {}: {}", component, path, reason)
467        }
468        PluginError::McpbDownloadFailed { url, reason, .. } => {
469            format!("Failed to download MCPB from {}: {}", url, reason)
470        }
471        PluginError::McpbExtractFailed {
472            mcpb_path, reason, ..
473        } => {
474            format!("Failed to extract MCPB {}: {}", mcpb_path, reason)
475        }
476        PluginError::McpbInvalidManifest {
477            mcpb_path,
478            validation_error,
479            ..
480        } => {
481            format!(
482                "MCPB manifest invalid at {}: {}",
483                mcpb_path, validation_error
484            )
485        }
486        PluginError::LspConfigInvalid {
487            plugin,
488            server_name,
489            validation_error,
490            ..
491        } => {
492            format!(
493                "Plugin \"{}\" has invalid LSP server config for \"{}\": {}",
494                plugin, server_name, validation_error
495            )
496        }
497        PluginError::LspServerStartFailed {
498            plugin,
499            server_name,
500            reason,
501            ..
502        } => {
503            format!(
504                "Plugin \"{}\" failed to start LSP server \"{}\": {}",
505                plugin, server_name, reason
506            )
507        }
508        PluginError::LspServerCrashed {
509            plugin,
510            server_name,
511            exit_code,
512            signal,
513            ..
514        } => {
515            if let Some(sig) = signal {
516                format!(
517                    "Plugin \"{}\" LSP server \"{}\" crashed with signal {}",
518                    plugin, server_name, sig
519                )
520            } else {
521                format!(
522                    "Plugin \"{}\" LSP server \"{}\" crashed with exit code {}",
523                    plugin,
524                    server_name,
525                    exit_code
526                        .map(|c| c.to_string())
527                        .unwrap_or_else(|| "unknown".to_string())
528                )
529            }
530        }
531        PluginError::LspRequestTimeout {
532            plugin,
533            server_name,
534            method,
535            timeout_ms,
536            ..
537        } => {
538            format!(
539                "Plugin \"{}\" LSP server \"{}\" timed out on {} request after {}ms",
540                plugin, server_name, method, timeout_ms
541            )
542        }
543        PluginError::LspRequestFailed {
544            plugin,
545            server_name,
546            method,
547            error,
548            ..
549        } => {
550            format!(
551                "Plugin \"{}\" LSP server \"{}\" {} request failed: {}",
552                plugin, server_name, method, error
553            )
554        }
555        PluginError::MarketplaceBlockedByPolicy {
556            marketplace,
557            blocked_by_blocklist,
558            ..
559        } => {
560            if blocked_by_blocklist == &Some(true) {
561                format!(
562                    "Marketplace '{}' is blocked by enterprise policy",
563                    marketplace
564                )
565            } else {
566                format!(
567                    "Marketplace '{}' is not in the allowed marketplace list",
568                    marketplace
569                )
570            }
571        }
572        PluginError::DependencyUnsatisfied {
573            dependency, reason, ..
574        } => {
575            let hint = match reason {
576                DependencyReason::NotEnabled => "disabled — enable it or remove the dependency",
577                DependencyReason::NotFound => "not found in any configured marketplace",
578            };
579            format!("Dependency \"{}\" is {}", dependency, hint)
580        }
581        PluginError::PluginCacheMiss {
582            plugin,
583            install_path,
584            ..
585        } => {
586            format!(
587                "Plugin \"{}\" not cached at {} — run /plugins to refresh",
588                plugin, install_path
589            )
590        }
591    }
592}