Compare commits

...

1 Commits

Author SHA1 Message Date
Igor Tceglevskii 125ef8127c VS Code plugin system 2025-10-19 23:08:03 -07:00
26 changed files with 3449 additions and 2 deletions
@@ -0,0 +1,398 @@
# Plugin System Architecture
## Overview
The Cline Plugin System is a parallel extension architecture that allows third-party VS Code extensions to register tools and capabilities with Cline through a JavaScript API. This system operates independently from the MCP (Model Context Protocol) infrastructure while following similar patterns for capability discovery and execution.
### Key Design Principles
1. **Parallel Architecture**: Plugins run alongside MCP servers without interference or data conversion overhead
2. **VS Code Native**: Direct integration with VS Code extension API for seamless discovery and activation
3. **Isolated Context**: Plugins receive limited execution context with safe service boundaries
4. **Dynamic Discovery**: Capabilities are discovered at runtime and included in LLM system prompts
5. **Graceful Errors**: Plugin failures are isolated and reported to the LLM without breaking the task flow
## Architecture Components
### Component Hierarchy
```
Extension.ts (Activation)
Controller
PluginHub (Service Layer)
Task → ToolExecutor
PluginToolHandler (Tool Coordinator)
Plugin Extension (External)
```
### Core Components
#### 1. PluginHub (`src/services/plugins/PluginHub.ts`)
**Responsibilities:**
- Discover compatible VS Code extensions during Cline activation
- Manage plugin registry and lifecycle
- Execute plugin capabilities with error handling
- Generate plugin sections for system prompts
**Key Methods:**
```typescript
class PluginHub {
// Discovery and registration
async discoverPlugins(): Promise<void>
async registerPlugin(plugin: ClinePlugin, extensionId: string): Promise<void>
async unregisterPlugin(pluginId: string): Promise<void>
// Execution
async executePluginCapability(
pluginId: string,
capabilityName: string,
parameters: Record<string, any>,
taskConfig: TaskConfig
): Promise<any>
// System prompt integration
getPluginPrompts(): string
getPluginCapabilities(): PluginCapability[]
}
```
**State Management:**
- Maintains `Map<string, RegisteredPlugin>` for active plugins
- Tracks capability mappings per plugin
- Stores last error state for debugging
#### 2. PluginContext (`src/services/plugins/PluginContext.ts`)
**Responsibilities:**
- Provide isolated execution context to plugins
- Expose safe services with appropriate boundaries
- Implement logging, storage, and HTTP capabilities
**Security Boundaries:**
- No direct access to VSCode API
- No access to internal Cline state (TaskState, MessageState)
- No file system access (prevents arbitrary file operations)
- Rate-limited HTTP client
- Scoped storage (plugin-specific only)
**Interface:**
```typescript
interface PluginContext {
// Read-only task information
taskId: string
taskMode: 'plan' | 'act'
workingDirectory: string
// Safe services
logger: PluginLogger
storage: PluginStorage
http: PluginHttpClient
// Communication
notify(message: string): void
requestInput(prompt: string): Promise<string>
}
```
#### 3. PluginToolHandler (`src/core/task/tools/handlers/PluginToolHandler.ts`)
**Responsibilities:**
- Integrate plugins into the tool coordinator pattern
- Handle tool execution requests from the LLM
- Format results and errors for LLM consumption
**Implementation Pattern:**
```typescript
export class PluginToolHandler implements IFullyManagedTool {
readonly name = ClineDefaultTool.PLUGIN_EXECUTE
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
const { plugin_id, capability_name, parameters } = block.params
try {
const result = await config.services.pluginHub.executePluginCapability(
plugin_id,
capability_name,
parameters,
config
)
return formatResponse.pluginSuccess(plugin_id, capability_name, result)
} catch (error) {
return formatResponse.pluginError(plugin_id, capability_name, error)
}
}
}
```
## Integration Points
### 1. Extension Activation (`src/extension.ts`)
Plugins are discovered during Cline's activation phase:
```typescript
export async function activate(context: vscode.ExtensionContext) {
// ... existing initialization
// Initialize plugin hub
const pluginHub = new PluginHub(context)
await pluginHub.discoverPlugins()
// Make available to controller
controller.pluginHub = pluginHub
// ... rest of activation
}
```
### 2. API Export (`src/exports/index.ts`)
Plugins register through the exported API:
```typescript
export function createClineAPI(controller: Controller): ClineAPI {
return {
// Existing API methods
startNewTask: async (task, images) => { ... },
sendMessage: async (message, images) => { ... },
// New plugin API
plugins: {
registerPlugin: async (plugin: ClinePlugin) => {
const extensionId = getCallingExtensionId()
await controller.pluginHub.registerPlugin(plugin, extensionId)
},
unregisterPlugin: async (pluginId: string) => {
await controller.pluginHub.unregisterPlugin(pluginId)
}
}
}
}
```
### 3. Tool Coordinator (`src/core/task/ToolExecutor.ts`)
Plugin handler is registered like other tools:
```typescript
private registerToolHandlers(): void {
// ... existing tool registrations
// Register plugin handler
this.coordinator.register(new PluginToolHandler())
}
```
### 4. System Prompt (`src/core/prompts/system-prompt/components/plugins.ts`)
Plugin capabilities are included in the system prompt:
```typescript
export function getPluginSection(context: SystemPromptContext): string {
const pluginHub = context.pluginHub
if (!pluginHub || pluginHub.getPluginCount() === 0) {
return ''
}
return `
# Plugin Extensions
The following plugin extensions are available:
${pluginHub.getPluginPrompts()}
Use the plugin_execute tool to call plugin capabilities.
`
}
```
## Data Flow
### Plugin Registration Flow
```
1. Plugin Extension activates
2. Extension calls Cline's exported API
3. API extracts calling extension ID
4. PluginHub.registerPlugin() called
5. Plugin.getCapabilities() retrieved
6. Capabilities stored in registry
7. Confirmation returned to plugin
```
### Plugin Execution Flow
```
1. LLM generates plugin_execute tool use
2. ToolExecutor routes to PluginToolHandler
3. PluginToolHandler validates parameters
4. PluginContext created from TaskConfig
5. PluginHub.executePluginCapability() called
6. Plugin.executeCapability() invoked
7. Result formatted and returned to LLM
```
## Error Handling Strategy
### Isolation Principles
1. **Try-Catch Boundaries**: All plugin calls wrapped in try-catch
2. **Timeout Protection**: Plugin execution has maximum time limit
3. **Error Propagation**: Errors formatted for LLM understanding
4. **State Preservation**: Plugin errors don't corrupt task state
### Error Types
```typescript
enum PluginErrorType {
REGISTRATION_FAILED = 'registration_failed',
CAPABILITY_NOT_FOUND = 'capability_not_found',
EXECUTION_TIMEOUT = 'execution_timeout',
EXECUTION_ERROR = 'execution_error',
PARAMETER_VALIDATION = 'parameter_validation'
}
```
### Error Reporting to LLM
```
Error executing plugin 'weather-plugin' capability 'getCurrentWeather':
Invalid parameter 'location' - must be a non-empty string.
Available parameters:
- location (string, required): City name or coordinates
- units (string, optional): Temperature units (celsius/fahrenheit)
```
## Comparison with MCP
| Aspect | MCP | Plugin System |
|--------|-----|---------------|
| **Protocol** | JSON-RPC 2.0 | Direct JS API |
| **Transport** | stdio/SSE/HTTP | In-process |
| **Discovery** | Settings file | VS Code extension API |
| **Configuration** | Per-server settings | Package.json metadata |
| **Permissions** | Per-tool approval | Extension-level trust |
| **State** | External process | In-process isolation |
| **Performance** | Protocol overhead | Direct function calls |
| **Use Case** | External tools/APIs | VS Code integration |
## Testing Strategy
### Unit Tests
- **PluginHub**: Registration, execution, error handling
- **PluginContext**: Service boundaries, isolation
- **PluginToolHandler**: Coordinator integration
### Integration Tests
- **End-to-end**: Plugin registration → execution → result
- **Error scenarios**: Timeouts, invalid parameters, plugin crashes
- **System prompt**: Capability inclusion and formatting
### Mock Plugin Pattern
```typescript
class MockWeatherPlugin implements ClinePlugin {
readonly id = 'mock-weather'
readonly name = 'Mock Weather'
readonly version = '1.0.0'
async getCapabilities() {
return [{
name: 'getWeather',
description: 'Get weather data',
parameters: [...]
}]
}
async executeCapability(name, params, context) {
return { temperature: 72, condition: 'sunny' }
}
}
```
## Performance Considerations
1. **Lazy Loading**: Plugins discovered at activation, not on every task
2. **Capability Caching**: Capabilities cached after first retrieval
3. **Async Execution**: All plugin calls are asynchronous
4. **Resource Limits**: HTTP client rate-limited, storage size-limited
## Future Extensibility
Potential enhancements:
1. **Plugin Marketplace**: Discover and install plugins from marketplace
2. **Capability Versioning**: Support multiple versions of same capability
3. **Plugin Dependencies**: Plugins that depend on other plugins
4. **Streaming Results**: Support for streaming responses from plugins
5. **UI Integration**: Plugin-provided UI panels and commands
6. **Resource Access**: Plugin-defined resources (like MCP resources)
## Migration Guide
For developers extending the plugin system:
### Adding New Safe Services to PluginContext
1. Define interface in `PluginContext`
2. Implement service in `PluginContext.ts`
3. Add security boundaries and rate limits
4. Update documentation
5. Add tests for new service
### Adding Plugin-Related Tools
Follow the standard tool handler pattern:
1. Create handler in `src/core/task/tools/handlers/`
2. Implement `IToolHandler` or `IFullyManagedTool`
3. Register in `ToolExecutor.registerToolHandlers()`
4. Add tool to system prompt
5. Update `ClineDefaultTool` enum
## Debugging
### Enable Plugin Logging
```typescript
// In plugin extension
context.logger.setLevel('debug')
context.logger.debug('Executing capability', { name, params })
```
### Inspect Plugin Registry
```typescript
// In Cline developer console
const pluginHub = controller.pluginHub
console.log('Registered plugins:', pluginHub.getPlugins())
console.log('Plugin capabilities:', pluginHub.getPluginCapabilities())
```
### Common Issues
1. **Plugin not discovered**: Check `extensionDependencies` in package.json
2. **Registration fails**: Ensure plugin implements ClinePlugin interface
3. **Execution timeout**: Check plugin execution time, add logging
4. **Context errors**: Verify plugin only uses provided context APIs
## Security Considerations
1. **Extension Trust**: Plugins run with extension permissions - users must trust installed extensions
2. **No Arbitrary Code**: Plugins cannot execute arbitrary code through Cline
3. **Scoped Storage**: Plugin storage isolated from other plugins and Cline
4. **Rate Limiting**: HTTP requests rate-limited to prevent abuse
5. **Error Isolation**: Plugin errors don't expose internal Cline state
## Conclusion
The Plugin System provides a clean, performant way for VS Code extensions to extend Cline's capabilities while maintaining security boundaries and error isolation. By following the patterns established by the internal tool system and MCP integration, plugins integrate seamlessly into Cline's workflow while remaining independent and maintainable.
@@ -0,0 +1,886 @@
# Creating Cline Plugin Extensions
## Overview
Cline plugins are VS Code extensions that extend Cline's capabilities by registering custom tools and functions. This guide shows you how to create plugins that integrate with other VS Code extensions' APIs, using Python environment intelligence as a practical example.
## Why Create Cline Plugins?
Cline plugins bridge the gap between VS Code extensions and Cline's AI capabilities. Common use cases:
- **Environment Intelligence**: Access runtime environment data (Python interpreters, Node versions, etc.)
- **Tool Integration**: Connect Cline to language servers, debuggers, test runners
- **External APIs**: Integrate third-party services (databases, cloud providers, etc.)
- **Custom Workflows**: Add domain-specific operations tailored to your team
## Quick Start
### 1. Prerequisites
- Node.js 18+ and npm
- VS Code 1.84+
- Basic TypeScript knowledge
- Cline extension installed
### 2. Create Your Extension
```bash
npm install -g yo generator-code
yo code
# Choose: New Extension (TypeScript)
# Extension name: cline-python-env
# Description: Python environment intelligence for Cline
# Initialize git: Yes
```
### 3. Add Cline as Dependency
Edit `package.json`:
```json
{
"name": "cline-python-env",
"displayName": "Cline Python Environment Plugin",
"version": "0.1.0",
"engines": {
"vscode": "^1.84.0"
},
"extensionDependencies": [
"saoudrizwan.claude-dev"
],
"dependencies": {
"@vscode/python-extension": "^1.0.5"
}
}
```
### 4. Install Dependencies
```bash
npm install @vscode/python-extension
```
## Plugin Structure
### Core Interface
Every Cline plugin must implement the `ClinePlugin` interface:
```typescript
interface ClinePlugin {
// Unique identifier (use your extension ID)
readonly id: string
// Display name
readonly name: string
// Semantic version
readonly version: string
// Optional description
readonly description?: string
// Return available capabilities/tools
getCapabilities(): Promise<PluginCapability[]>
// Execute a specific capability
executeCapability(
capabilityName: string,
parameters: Record<string, any>,
context: PluginContext
): Promise<any>
// Optional cleanup
dispose?(): Promise<void>
}
```
### Capability Definition
Each tool/function your plugin provides:
```typescript
interface PluginCapability {
// Unique capability name (within your plugin)
name: string
// Description for the LLM
description: string
// Parameter definitions
parameters: ParameterDefinition[]
// Optional return type description
returns?: string
// Optional usage guidance for the LLM
prompt?: string
// Optional usage examples
examples?: string[]
}
interface ParameterDefinition {
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
required: boolean
description?: string
defaultValue?: any
}
```
### Plugin Context
Your plugin receives a limited context for security:
```typescript
interface PluginContext {
// Current task information
taskId: string
taskMode: 'plan' | 'act'
workingDirectory: string
// Safe services
logger: PluginLogger // Scoped logging
storage: PluginStorage // Plugin-specific storage
http: PluginHttpClient // Rate-limited HTTP
// Communication methods
notify(message: string): void
requestInput(prompt: string): Promise<string>
}
```
## Complete Example: Python Environment Plugin
This plugin integrates with the VS Code Python extension to provide environment intelligence.
### src/plugin.ts
```typescript
import * as vscode from 'vscode'
import { PythonExtension } from '@vscode/python-extension'
import { ClinePlugin, PluginCapability, PluginContext } from './types'
export class PythonEnvPlugin implements ClinePlugin {
readonly id = 'cline-python-env'
readonly name = 'Python Environment Intelligence'
readonly version = '1.0.0'
readonly description = 'Provides Python environment and package information'
private pythonApi?: Awaited<ReturnType<typeof PythonExtension.api>>
async initialize() {
// Get Python extension API
this.pythonApi = await PythonExtension.api()
}
async getCapabilities(): Promise<PluginCapability[]> {
return [
{
name: 'getPythonEnvironment',
description: 'Get detailed information about the active Python environment including version, installed packages, and environment type',
parameters: [],
returns: 'Environment details including Python version, environment type, installed packages with versions, and environment path',
prompt: 'Use this to understand what Python packages are available before suggesting code. Check package versions to generate compatible code.',
examples: [
'Get the current Python environment to check if TensorFlow is installed',
'Check Python version before using version-specific syntax',
'Verify pandas version before generating DataFrame code'
]
},
{
name: 'getPythonVersion',
description: 'Get the Python version of the active environment',
parameters: [],
returns: 'Python version string (e.g., "3.11.2")'
},
{
name: 'checkPackageInstalled',
description: 'Check if a specific package is installed and get its version',
parameters: [
{
name: 'packageName',
type: 'string',
required: true,
description: 'Name of the package to check (e.g., "pandas", "tensorflow")'
}
],
returns: 'Package version if installed, null if not installed',
examples: [
'Check if numpy is installed before suggesting array operations',
'Verify Django version to generate compatible view code'
]
},
{
name: 'getInstallCommand',
description: 'Get the appropriate package installation command for the current environment type',
parameters: [
{
name: 'packageName',
type: 'string',
required: true,
description: 'Name of the package to install'
},
{
name: 'version',
type: 'string',
required: false,
description: 'Optional specific version (e.g., "2.0.0")'
}
],
returns: 'Installation command appropriate for the environment (pip, conda, poetry, etc.)',
prompt: 'Use this to provide correct installation commands. Different environments (venv, conda, poetry) require different commands.'
}
]
}
async executeCapability(
capabilityName: string,
parameters: Record<string, any>,
context: PluginContext
): Promise<any> {
if (!this.pythonApi) {
throw new Error('Python extension API not available')
}
context.logger.info(`Executing ${capabilityName}`, { parameters })
switch (capabilityName) {
case 'getPythonEnvironment':
return await this.getPythonEnvironment(context)
case 'getPythonVersion':
return await this.getPythonVersion(context)
case 'checkPackageInstalled':
return await this.checkPackageInstalled(
parameters.packageName as string,
context
)
case 'getInstallCommand':
return await this.getInstallCommand(
parameters.packageName as string,
parameters.version as string | undefined,
context
)
default:
throw new Error(`Unknown capability: ${capabilityName}`)
}
}
// Core implementation: resolveEnvironment()
private async getPythonEnvironment(context: PluginContext) {
try {
// Get active environment path
const envPath = this.pythonApi!.environments.getActiveEnvironmentPath()
context.logger.debug('Active environment path', { path: envPath.path })
// Resolve full environment details - THE KEY FUNCTION!
const envDetails = await this.pythonApi!.environments.resolveEnvironment(envPath)
if (!envDetails) {
return {
error: 'Could not resolve Python environment',
path: envPath.path
}
}
// Extract and format relevant information
const result = {
path: envPath.path,
version: envDetails.version?.major && envDetails.version?.minor && envDetails.version?.micro
? `${envDetails.version.major}.${envDetails.version.minor}.${envDetails.version.micro}`
: 'unknown',
environmentType: this.detectEnvironmentType(envPath.path),
packages: this.formatPackages(envDetails),
pythonExecutable: envDetails.executable?.uri?.fsPath || envPath.path
}
context.logger.info('Environment resolved successfully', {
version: result.version,
packageCount: result.packages.length
})
return result
} catch (error) {
context.logger.error('Failed to get Python environment', { error })
throw new Error(`Failed to resolve Python environment: ${error}`)
}
}
private async getPythonVersion(context: PluginContext) {
const env = await this.getPythonEnvironment(context)
return env.version
}
private async checkPackageInstalled(
packageName: string,
context: PluginContext
) {
const env = await this.getPythonEnvironment(context)
const pkg = env.packages.find(
p => p.name.toLowerCase() === packageName.toLowerCase()
)
if (pkg) {
context.logger.info(`Package ${packageName} found`, { version: pkg.version })
return pkg.version
}
context.logger.info(`Package ${packageName} not found`)
return null
}
private async getInstallCommand(
packageName: string,
version: string | undefined,
context: PluginContext
) {
const env = await this.getPythonEnvironment(context)
const packageSpec = version ? `${packageName}==${version}` : packageName
// Detect environment type and return appropriate command
switch (env.environmentType) {
case 'conda':
return `conda install ${packageSpec}`
case 'poetry':
return version
? `poetry add ${packageName}@${version}`
: `poetry add ${packageName}`
case 'pipenv':
return `pipenv install ${packageSpec}`
case 'venv':
case 'virtualenv':
return `pip install ${packageSpec}`
default:
// System Python - recommend user flag
context.notify('Using system Python - consider creating a virtual environment')
return `pip install --user ${packageSpec}`
}
}
// Helper methods
private detectEnvironmentType(path: string): string {
if (path.includes('conda') || path.includes('miniconda') || path.includes('anaconda')) {
return 'conda'
} else if (path.includes('poetry')) {
return 'poetry'
} else if (path.includes('pipenv')) {
return 'pipenv'
} else if (path.includes('.venv') || path.includes('venv')) {
return 'venv'
} else if (path.includes('virtualenv')) {
return 'virtualenv'
} else {
return 'system'
}
}
private formatPackages(envDetails: any): Array<{ name: string; version: string }> {
// Note: The actual structure depends on the Python extension API version
// This is a simplified example
const packages: Array<{ name: string; version: string }> = []
// Extract packages from environment details
// The exact property path may vary - check Python extension docs
if (envDetails.packages) {
for (const pkg of envDetails.packages) {
packages.push({
name: pkg.name || 'unknown',
version: pkg.version || 'unknown'
})
}
}
return packages
}
async dispose() {
// Cleanup if needed
this.pythonApi = undefined
}
}
```
### src/extension.ts
```typescript
import * as vscode from 'vscode'
import { PythonEnvPlugin } from './plugin'
export async function activate(context: vscode.ExtensionContext) {
console.log('Python Environment Plugin activating...')
// Get Cline API
const clineExtension = vscode.extensions.getExtension('saoudrizwan.claude-dev')
if (!clineExtension) {
vscode.window.showErrorMessage('Cline extension not found')
return
}
// Activate Cline if not already active
if (!clineExtension.isActive) {
await clineExtension.activate()
}
const clineApi = clineExtension.exports
if (!clineApi || !clineApi.plugins) {
vscode.window.showErrorMessage('Cline plugin API not available')
return
}
// Create and register plugin
const plugin = new PythonEnvPlugin()
await plugin.initialize()
try {
await clineApi.plugins.registerPlugin(plugin)
console.log('Python Environment Plugin registered successfully')
// Cleanup on deactivation
context.subscriptions.push({
dispose: async () => {
await plugin.dispose()
await clineApi.plugins.unregisterPlugin(plugin.id)
}
})
} catch (error) {
vscode.window.showErrorMessage(`Failed to register plugin: ${error}`)
}
}
export function deactivate() {
console.log('Python Environment Plugin deactivated')
}
```
### src/types.ts
```typescript
// Type definitions for Cline plugin interface
// (These would typically be provided by Cline or installed from npm)
export interface ClinePlugin {
readonly id: string
readonly name: string
readonly version: string
readonly description?: string
getCapabilities(): Promise<PluginCapability[]>
executeCapability(
capabilityName: string,
parameters: Record<string, any>,
context: PluginContext
): Promise<any>
dispose?(): Promise<void>
}
export interface PluginCapability {
name: string
description: string
parameters: ParameterDefinition[]
returns?: string
prompt?: string
examples?: string[]
}
export interface ParameterDefinition {
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
required: boolean
description?: string
defaultValue?: any
}
export interface PluginContext {
taskId: string
taskMode: 'plan' | 'act'
workingDirectory: string
logger: PluginLogger
storage: PluginStorage
http: PluginHttpClient
notify(message: string): void
requestInput(prompt: string): Promise<string>
}
export interface PluginLogger {
debug(message: string, data?: any): void
info(message: string, data?: any): void
warn(message: string, data?: any): void
error(message: string, data?: any): void
}
export interface PluginStorage {
get<T>(key: string): Promise<T | undefined>
set<T>(key: string, value: T): Promise<void>
delete(key: string): Promise<void>
clear(): Promise<void>
}
export interface PluginHttpClient {
get(url: string, options?: RequestOptions): Promise<HttpResponse>
post(url: string, data?: any, options?: RequestOptions): Promise<HttpResponse>
}
export interface RequestOptions {
headers?: Record<string, string>
timeout?: number
}
export interface HttpResponse {
status: number
data: any
headers: Record<string, string>
}
```
## How the Plugin Works
### 1. Registration Flow
```
Extension Activates
Get Cline Extension API
Create Plugin Instance
Call initialize() (get Python API)
Register with Cline
Plugin Available to LLM
```
### 2. Execution Flow
```
User asks: "Check if pandas is installed"
LLM generates: plugin_execute tool call
Cline routes to your plugin
executeCapability('checkPackageInstalled', {packageName: 'pandas'})
Your code calls resolveEnvironment()
Return result to LLM
LLM uses result in response
```
### 3. What the LLM Sees
When your plugin is registered, Cline adds it to the system prompt:
```
# Plugin Extensions
## Python Environment Intelligence (cline-python-env)
Provides Python environment and package information
### Available Capabilities:
**getPythonEnvironment**
Description: Get detailed information about the active Python environment including
version, installed packages, and environment type
Usage: Use this to understand what Python packages are available before suggesting code.
Check package versions to generate compatible code.
Examples:
- Get the current Python environment to check if TensorFlow is installed
- Check Python version before using version-specific syntax
- Verify pandas version before generating DataFrame code
Returns: Environment details including Python version, environment type, installed
packages with versions, and environment path
**checkPackageInstalled**
Description: Check if a specific package is installed and get its version
Parameters:
- packageName (string, required): Name of the package to check
Returns: Package version if installed, null if not installed
[... other capabilities ...]
```
## Best Practices
### 1. Error Handling
Always handle errors gracefully:
```typescript
async executeCapability(name: string, params: any, context: PluginContext) {
try {
// Your logic
const result = await this.doSomething(params)
return result
} catch (error) {
// Log the error
context.logger.error(`Failed to execute ${name}`, { error, params })
// Return user-friendly error
throw new Error(
`Failed to ${name}: ${error.message}. ` +
`Please check that the required extension is installed.`
)
}
}
```
### 2. Validate Parameters
```typescript
private validatePackageName(name: string) {
if (!name || typeof name !== 'string') {
throw new Error('Package name must be a non-empty string')
}
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
throw new Error('Invalid package name format')
}
}
```
### 3. Use Context Logging
```typescript
async getPythonEnvironment(context: PluginContext) {
context.logger.info('Fetching Python environment')
const start = Date.now()
const result = await this.pythonApi.environments.resolveEnvironment(path)
context.logger.debug('Environment resolved', {
duration: Date.now() - start,
packageCount: result.packages?.length
})
return result
}
```
### 4. Cache Expensive Operations
```typescript
private envCache?: {
path: string
data: any
timestamp: number
}
async getPythonEnvironment(context: PluginContext) {
const envPath = this.pythonApi!.environments.getActiveEnvironmentPath()
// Check cache (5 minute expiry)
if (this.envCache &&
this.envCache.path === envPath.path &&
Date.now() - this.envCache.timestamp < 300000) {
context.logger.debug('Using cached environment data')
return this.envCache.data
}
// Fetch fresh data
const data = await this.pythonApi!.environments.resolveEnvironment(envPath)
this.envCache = {
path: envPath.path,
data,
timestamp: Date.now()
}
return data
}
```
### 5. Provide Helpful Prompts
Guide the LLM on when and how to use your capabilities:
```typescript
{
name: 'checkPackageInstalled',
description: 'Check if a specific package is installed',
prompt: `
IMPORTANT: Always check if packages are installed before suggesting code that uses them.
Examples of when to use:
- Before generating import statements
- When user mentions a package name
- Before suggesting package-specific solutions
If package is not installed, suggest the installation command using getInstallCommand.
`,
parameters: [...]
}
```
## Testing Your Plugin
### 1. Unit Tests
```typescript
// test/plugin.test.ts
import { PythonEnvPlugin } from '../src/plugin'
import { PluginContext } from '../src/types'
describe('PythonEnvPlugin', () => {
let plugin: PythonEnvPlugin
let mockContext: PluginContext
beforeEach(() => {
plugin = new PythonEnvPlugin()
mockContext = createMockContext()
})
it('should detect conda environment', () => {
const path = '/Users/test/miniconda3/envs/myenv/bin/python'
const type = plugin['detectEnvironmentType'](path)
expect(type).toBe('conda')
})
it('should handle missing package', async () => {
// Mock Python API response
mockPythonApi.environments.resolveEnvironment.mockResolvedValue({
packages: []
})
const result = await plugin.executeCapability(
'checkPackageInstalled',
{ packageName: 'nonexistent' },
mockContext
)
expect(result).toBeNull()
})
})
```
### 2. Integration Testing
Test with Cline running:
1. Install your extension in development mode (`F5` in VS Code)
2. Ask Cline: "What Python packages do I have installed?"
3. Check Cline calls your plugin
4. Verify the response is useful
### 3. Debug Logging
Enable verbose logging in your plugin:
```typescript
if (process.env.DEBUG === 'true') {
context.logger.debug('Full environment details', { envDetails })
}
```
## Publishing Your Plugin
### 1. Prepare for Publication
Update `package.json`:
```json
{
"name": "cline-python-env",
"displayName": "Cline Python Environment Plugin",
"description": "Provides Python environment intelligence to Cline",
"version": "1.0.0",
"publisher": "your-username",
"repository": {
"type": "git",
"url": "https://github.com/your-username/cline-python-env"
},
"keywords": ["cline", "python", "environment", "plugin"],
"categories": ["Other"],
"icon": "icon.png"
}
```
### 2. Add Documentation
Create `README.md`:
```markdown
# Cline Python Environment Plugin
Provides Python environment intelligence to Cline, enabling it to understand
your Python setup and generate more accurate code.
## Features
- Detect Python version and environment type
- Check installed packages and versions
- Generate correct installation commands
- Provide environment-aware code suggestions
## Usage
Install this extension, then ask Cline questions like:
- "What Python packages do I have installed?"
- "Check if TensorFlow is installed"
- "What version of pandas am I using?"
Cline will automatically use this plugin to provide accurate information.
## Requirements
- Cline extension installed
- Python extension for VS Code installed
```
### 3. Publish
```bash
# Install vsce
npm install -g vsce
# Package extension
vsce package
# Publish to marketplace
vsce publish
```
## Troubleshooting
### Plugin Not Registering
**Problem**: Plugin doesn't appear in Cline
**Solutions**:
1. Check `extensionDependencies` includes Cline
2. Verify Cline is active before registration
3. Check console for error messages
4. Ensure plugin implements all required methods
### API Not Available
**Problem**: External extension API returns undefined
**Solutions**:
1. Check external extension is installed
2. Ensure external extension activated first
3. Add activation event: `"onLanguage:python"`
4. Wait for activation: `await extension.activate()`
### Execution Timeouts
**Problem**: Plugin operations take too long
**Solutions**:
1. Cache expensive operations
2. Make operations asynchronous
3. Add progress notifications
4. Implement timeouts with
+264
View File
@@ -0,0 +1,264 @@
# Implementation Plan
## Overview
Create a parallel VS Code extension plugin system for Cline that allows third-party extensions to register tools and capabilities through a JavaScript API, providing dynamic capability discovery similar to MCP but optimized for direct VS Code extension integration without protocol overhead.
This implementation will create a new plugin system alongside the existing MCP infrastructure, allowing VS Code extensions to declare Cline as a dependency and register tools during their activation. The system will provide limited context access for plugins while maintaining security boundaries, and ensure the LLM always has visibility into available plugin capabilities through system prompts.
### Reference Documentation
**This implementation MUST follow the architectural patterns and API specifications defined in:**
1. **Architecture Documentation**: `docs/development/plugin-system-architecture.md`
- Defines the complete system architecture and component hierarchy
- Specifies integration patterns with existing tool infrastructure
- Details security boundaries and isolation requirements
- Provides error handling and testing strategies
2. **Plugin Development Guide**: `docs/plugin-development/creating-cline-plugins.md`
- Defines the complete plugin interface that extensions must implement
- Shows practical integration patterns (e.g., Python environment example)
- Specifies the context API that plugins receive
- Documents expected behavior and best practices
**All code must be fully compatible with the interfaces and patterns documented in these guides.**
## Types
Define comprehensive TypeScript interfaces for plugin registration, tool definitions, and execution context.
```typescript
// Core plugin interface that extensions must implement
interface ClinePlugin {
readonly id: string
readonly name: string
readonly version: string
readonly description?: string
getCapabilities(): Promise<PluginCapability[]>
executeCapability(capabilityName: string, parameters: Record<string, any>, context: PluginContext): Promise<any>
dispose?(): Promise<void>
}
// Individual capability/tool definition
interface PluginCapability {
name: string
description: string
parameters: ParameterDefinition[]
returns?: string
prompt?: string
examples?: string[]
}
// Parameter schema definition
interface ParameterDefinition {
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
required: boolean
description?: string
defaultValue?: any
}
// Limited context provided to plugins
interface PluginContext {
taskId: string
taskMode: 'plan' | 'act'
workingDirectory: string
// Safe services
logger: PluginLogger
storage: PluginStorage
http: PluginHttpClient
// Communication methods
notify(message: string): void
requestInput(prompt: string): Promise<string>
}
// Plugin registration in Cline's exported API
interface ClinePluginAPI {
registerPlugin(plugin: ClinePlugin): Promise<void>
unregisterPlugin(pluginId: string): Promise<void>
}
// Internal plugin registry types
interface RegisteredPlugin {
plugin: ClinePlugin
extensionId: string
capabilities: Map<string, PluginCapability>
isActive: boolean
lastError?: string
}
```
## Files
Create new plugin system files and modify existing tool infrastructure for integration.
**New Files:**
- `src/services/plugins/PluginHub.ts` - Main plugin management service
- `src/services/plugins/PluginContext.ts` - Limited context implementation for plugins
- `src/services/plugins/types.ts` - Plugin type definitions
- `src/core/task/tools/handlers/PluginToolHandler.ts` - Tool handler for plugin capabilities
- `src/exports/plugin-api.ts` - API exported for plugin extensions
- `src/shared/plugins.ts` - Shared plugin enums and constants
**Modified Files:**
- `src/exports/index.ts` - Add plugin API to main export
- `src/extension.ts` - Initialize PluginHub service
- `src/core/controller/index.ts` - Add plugin hub reference
- `src/core/task/index.ts` - Pass plugin hub to task
- `src/core/task/ToolExecutor.ts` - Register plugin tool handlers
- `src/core/prompts/system-prompt/components/plugins.ts` - Plugin system prompt section
- `src/core/prompts/system-prompt/components/index.ts` - Include plugin section
- `src/shared/tools.ts` - Add plugin tool enum values
## Functions
Implement core plugin management and execution functions.
**New Functions:**
- `PluginHub.discoverPlugins()` - Scan VS Code extensions for Cline plugins
- `PluginHub.registerPlugin(plugin, extensionId)` - Register plugin and capabilities
- `PluginHub.executePluginCapability(pluginId, capabilityName, params)` - Execute plugin tool
- `PluginHub.getPluginPrompts()` - Get all plugin prompts for system prompt
- `PluginContext.createContext(taskConfig)` - Create limited plugin context
- `PluginToolHandler.execute()` - Handle plugin tool execution in coordinator
- `createPluginAPI(controller)` - Create plugin API for export
**Modified Functions:**
- `createClineAPI()` - Include plugin registration API
- `ToolExecutor.registerToolHandlers()` - Register plugin handlers
- `getSystemPrompt()` - Include plugin capabilities in prompt
## Classes
Define plugin management and execution classes.
**New Classes:**
- `PluginHub` - Central plugin registry and management
- `PluginContext` - Limited execution context for plugins
- `PluginLogger` - Scoped logging for plugins
- `PluginStorage` - Plugin-scoped storage interface
- `PluginHttpClient` - Rate-limited HTTP client for plugins
- `PluginToolHandler` - Tool handler implementing IFullyManagedTool
**Modified Classes:**
- `Controller` - Add pluginHub property and initialization
- `Task` - Pass plugin hub to tool executor
- `ToolExecutor` - Include plugin tool registration
## Dependencies
No new external dependencies required - leverages existing VS Code API and Cline infrastructure.
All functionality built on existing dependencies:
- VS Code Extension API for extension discovery and management
- Existing Cline tool infrastructure and coordinator pattern
- Current TypeScript and Zod validation patterns
- Existing error handling and logging systems
### Optional Dependencies for Plugin Developers
Plugin extensions may add their own dependencies to integrate with other VS Code extensions:
- `@vscode/python-extension` - For Python environment integration
- Other VS Code extension APIs as needed for specific integrations
## Testing
Create comprehensive test coverage for plugin system functionality.
**Test Files:**
- `src/core/task/tools/handlers/__tests__/PluginToolHandler.test.ts` - Plugin tool handler tests
- `src/services/plugins/__tests__/PluginHub.test.ts` - Plugin hub functionality tests
- `src/services/plugins/__tests__/PluginContext.test.ts` - Plugin context isolation tests
- `src/exports/__tests__/plugin-api.test.ts` - Plugin API export tests
**Test Coverage:**
- Plugin discovery and registration workflows
- Capability execution with error handling
- Context isolation and security boundaries
- System prompt integration
- Tool coordinator integration
## Implementation Order
Structured implementation sequence to minimize conflicts and enable incremental testing.
### Phase 1: Core Infrastructure (Steps 1-4)
1. **Core Types and Interfaces**
- Define all TypeScript interfaces in `src/services/plugins/types.ts`
- Must match interfaces in `docs/plugin-development/creating-cline-plugins.md`
- Include: `ClinePlugin`, `PluginCapability`, `PluginContext`, `PluginLogger`, `PluginStorage`, `PluginHttpClient`
2. **Plugin Context Implementation**
- Create `PluginContext.ts` with limited context and safe services
- Implement security boundaries as specified in `docs/development/plugin-system-architecture.md`
- Services: `PluginLogger` (scoped logging), `PluginStorage` (plugin-scoped), `PluginHttpClient` (rate-limited)
- Communication: `notify()` and `requestInput()` methods
3. **Plugin Hub Service**
- Implement `PluginHub.ts` with discovery, registration, and execution logic
- Follow architecture defined in `docs/development/plugin-system-architecture.md`
- Key methods:
- `discoverPlugins()` - Initial discovery during activation
- `registerPlugin(plugin, extensionId)` - Active registration
- `executePluginCapability(pluginId, capabilityName, params)` - Execution with error isolation
- `getPluginPrompts()` - Generate system prompt sections
- Maintain `Map<string, RegisteredPlugin>` for registry
4. **Plugin API Export**
- Create `src/exports/plugin-api.ts`
- Integrate into main exports in `src/exports/index.ts`
- API must match specification in plugin development guide:
- `registerPlugin(plugin: ClinePlugin): Promise<void>`
- `unregisterPlugin(pluginId: string): Promise<void>`
### Phase 2: Tool System Integration (Steps 5-6)
5. **Tool Handler Integration**
- Implement `PluginToolHandler.ts` following `IFullyManagedTool` pattern
- Register in `ToolExecutor.registerToolHandlers()`
- Handle tool execution with proper error boundaries
- Format results using `formatResponse.pluginSuccess()` and `formatResponse.pluginError()`
6. **System Prompt Integration**
- Create `src/core/prompts/system-prompt/components/plugins.ts`
- Follow prompt format shown in architecture documentation
- Include plugin capabilities with descriptions, parameters, prompts, and examples
- Add section to main system prompt generation
### Phase 3: Core Integration (Steps 7-8)
7. **Controller Integration**
- Add `pluginHub` property to Controller
- Initialize plugin hub in controller constructor
- Pass plugin hub reference to tasks
8. **Extension Integration**
- Update `extension.ts` activation to initialize plugin system
- Call `pluginHub.discoverPlugins()` during activation
- Ensure proper cleanup on deactivation
### Phase 4: Quality Assurance (Steps 9-10)
9. **Testing Implementation**
- Create comprehensive test suite matching architecture doc testing strategy
- Unit tests: PluginHub, PluginContext, PluginToolHandler
- Integration tests: End-to-end registration and execution
- Mock plugin pattern for testing
- Test error isolation and security boundaries
10. **Documentation Validation**
- Verify implementation matches both documentation files
- Ensure all interfaces are compatible with plugin development guide
- Validate architecture matches architecture documentation
- Create example plugin (Python environment integration recommended)
- **Note: Documentation already complete - validate implementation against it**
### Implementation Guidelines
**Critical Requirements:**
1. All interfaces MUST match `docs/plugin-development/creating-cline-plugins.md` exactly
2. Architecture MUST follow patterns in `docs/development/plugin-system-architecture.md`
3. Security boundaries MUST be enforced as documented
4. Error handling MUST follow isolation principles from architecture doc
5. System prompt format MUST match documented format
**Testing Checkpoints:**
- After Phase 1: Test plugin registration and context creation
- After Phase 2: Test tool execution through coordinator
- After Phase 3: Test end-to-end flow from extension activation
- After Phase 4: Validate against documentation and run full test suite
+288
View File
@@ -0,0 +1,288 @@
# Python Extension API Integration Guide
## Why Integrate with the VS Code Python Extension API?
The Python extension (`ms-python.python`) for VS Code exposes a powerful API that provides **computed intelligence about Python environments** - data that is expensive or impossible to obtain by simply reading source code files.
## The Gold Mine: Environment Intelligence
### What Makes This a Gold Mine?
When building AI-powered code generation tools, understanding the **runtime environment** is just as critical as understanding the code itself. The Python extension has spent years solving the complex problem of:
- **Discovering Python installations** across Windows, macOS, and Linux
- **Detecting virtual environments** (venv, conda, poetry, pipenv, virtualenv)
- **Tracking installed packages** and their versions
- **Managing environment activation** with correct paths and environment variables
- **Monitoring environment changes** in real-time
You get all of this intelligence for FREE through the API.
### The Problem: Reading Code Isn't Enough
Consider this simple Python file:
```python
import pandas as pd
import tensorflow as tf
import requests
df = pd.read_csv('data.csv')
model = tf.keras.Sequential([...])
```
**What you CAN see by reading the file:**
- ✅ The code imports `pandas`, `tensorflow`, and `requests`
- ✅ It uses pandas DataFrames and TensorFlow Keras API
**What you CANNOT see by reading the file:**
- ❌ Which Python interpreter will actually run this code?
- ❌ Are these packages actually installed?
- ❌ What versions are installed? (TensorFlow 1.x vs 2.x is drastically different!)
- ❌ Is this using a virtual environment or system Python?
- ❌ What Python version is being used? (affects available syntax features)
- ❌ What other packages are available for suggestions?
- ❌ Where are packages installed?
- ❌ Is CUDA/GPU support available?
### Why This Data is Valuable for Development
#### 1. **Accurate Code Generation**
Without environment knowledge, you're guessing. With it, you can:
- Generate code using the correct API version (TensorFlow 1.x vs 2.x)
- Use Python version-specific syntax (f-strings in 3.6+, walrus operator in 3.8+)
- Suggest only packages that are actually installed
- Generate environment-appropriate installation commands
#### 2. **Better Error Prevention**
- Warn about missing dependencies BEFORE code execution
- Suggest correct package versions for the Python version in use
- Detect incompatible package combinations
- Prevent suggesting code that won't work in the user's environment
#### 3. **Smart Autocomplete & Suggestions**
- Only suggest APIs from installed package versions
- Recommend packages that work with the current Python version
- Suggest compatible dependency versions
- Provide environment-specific code snippets
#### 4. **Proper Development Workflow**
- Know whether to use `pip`, `conda`, `poetry`, or `pipenv` for installations
- Generate correct activation commands for the environment type
- Understand project structure through environment location
- Respect virtual environment isolation
## Why Environment Data is Expensive to Obtain
### The Hidden Complexity
Getting accurate Python environment information is deceptively difficult:
#### 1. **Cross-Platform Differences**
- Windows: `C:\Python39\python.exe`, `%USERPROFILE%\.virtualenvs\`, registry entries
- macOS: `/usr/local/bin/python3`, homebrew paths, framework builds
- Linux: `/usr/bin/python3`, multiple system versions, various package managers
#### 2. **Environment Type Detection**
Different virtual environment tools have different structures:
- **venv**: `pyvenv.cfg` file
- **conda**: `conda-meta/` directory
- **poetry**: `poetry.lock` + `pyproject.toml`
- **pipenv**: `Pipfile` + `Pipfile.lock`
- **virtualenv**: Similar to venv but older structure
Each requires different detection logic!
#### 3. **Package Discovery**
Finding installed packages isn't trivial:
- Parse `site-packages/` directories
- Read `.dist-info` or `.egg-info` metadata
- Handle different package formats
- Deal with editable installs (`pip install -e`)
- Check multiple potential locations
#### 4. **Environment Activation**
Each environment type activates differently:
```bash
# venv
source .venv/bin/activate # Unix
.venv\Scripts\activate.bat # Windows
# conda
conda activate myenv
# poetry
poetry shell
```
#### 5. **Real-Time Monitoring**
Tracking when users:
- Create new environments
- Switch between environments
- Install/uninstall packages
- Change Python interpreter settings
### The Cost of DIY Implementation
If you tried to implement this yourself:
**Time Investment:**
- 2-4 weeks just for basic cross-platform environment discovery
- 1-2 weeks for package detection and parsing
- 1 week for activation script generation
- Ongoing maintenance for edge cases and new environment tools
**Complexity:**
- Handle all OS-specific quirks
- Parse various metadata formats
- Deal with symlinks and junction points
- Handle spaces and special characters in paths
- Support new environment tools as they emerge
**The Python extension has already done this!** Years of development, bug fixes, and edge case handling are available through a simple API.
## Getting Started: The First Function to Implement
### Recommended: `getActiveEnvironmentPath()`
Start with the simplest and most fundamental function:
```typescript
const pythonApi = await PythonExtension.api();
const envPath = pythonApi.environments.getActiveEnvironmentPath();
console.log(envPath.path);
// Output: "/Users/username/project/.venv/bin/python"
```
#### Why Start Here?
1. **Simple to integrate** - Just one function call
2. **Immediate value** - Tells you which Python the user is actually using
3. **Foundation for more** - Other functions build on this
4. **No complex parsing** - Returns a clean path string
#### What You Get
The active environment path tells you:
- **Environment type detection**: Is it in `.venv/`, `conda/`, or system location?
- **Project context**: Virtual env paths often reveal the project root
- **Isolation awareness**: Know if user is in isolated env (safe) vs system Python (careful!)
- **Interpreter location**: Exact binary that will execute the code
#### Example Usage in Code Generation
```typescript
async function generateInstallCommand(packageName: string) {
const pythonApi = await PythonExtension.api();
const envPath = pythonApi.environments.getActiveEnvironmentPath().path;
// Detect environment type from path
if (envPath.includes('conda')) {
return `conda install ${packageName}`;
} else if (envPath.includes('.venv') || envPath.includes('virtualenv')) {
return `pip install ${packageName}`;
} else if (envPath.includes('poetry')) {
return `poetry add ${packageName}`;
} else {
// System Python - be cautious!
return `pip install --user ${packageName}`;
}
}
```
### Next Level: `resolveEnvironment()`
Once you have the basic integration working, level up with:
```typescript
const envPath = pythonApi.environments.getActiveEnvironmentPath().path;
const details = await pythonApi.environments.resolveEnvironment(envPath);
```
#### What This Unlocks
This function returns **rich environment details**:
- **Python version**: "3.11.2" - Know what syntax features are available
- **Environment type**: "Venv", "Conda", "Poetry", etc.
- **Installed packages**: Complete list with versions
- **Environment variables**: Variables needed for activation
- **Package locations**: Where to find installed libraries
#### Powerful Example: Version-Aware Code Generation
```typescript
async function generateTensorFlowCode() {
const envPath = pythonApi.environments.getActiveEnvironmentPath().path;
const details = await pythonApi.environments.resolveEnvironment(envPath);
// Check TensorFlow version
const tfVersion = details.packages.find(p => p.name === 'tensorflow')?.version;
if (!tfVersion) {
return {
error: "TensorFlow not installed",
suggestion: "Run: pip install tensorflow"
};
}
if (tfVersion.startsWith('1.')) {
// Generate TensorFlow 1.x code
return `
import tensorflow as tf
session = tf.Session()
# TensorFlow 1.x style code
`.trim();
} else {
// Generate TensorFlow 2.x code
return `
import tensorflow as tf
# TensorFlow 2.x style - eager execution by default
model = tf.keras.Sequential([...])
`.trim();
}
}
```
## Implementation Strategy
### Phase 1: Basic Integration
1. ✅ Import `@vscode/python-extension` npm module
2. ✅ Get Python extension API instance
3. ✅ Call `getActiveEnvironmentPath()`
4. ✅ Display environment path in your UI
### Phase 2: Environment Intelligence
1. ✅ Call `resolveEnvironment()` with active path
2. ✅ Cache environment details
3. ✅ Use Python version for syntax decisions
4. ✅ Check installed packages before suggesting imports
### Phase 3: Real-Time Awareness
1. ✅ Subscribe to `onDidChangeActiveEnvironment`
2. ✅ Subscribe to `onDidEnvironmentsChanged`
3. ✅ Update your tool's state when environment changes
4. ✅ Invalidate caches appropriately
## Key Takeaways
🎯 **The Python extension API provides environment intelligence that is:**
-**Impossible to get cheaply** by reading source files
- 🚀 **Years of development** already done for you
- 🔄 **Real-time and accurate** through native integration
- 🌍 **Cross-platform and battle-tested** across millions of users
🎯 **Start simple with `getActiveEnvironmentPath()`** then expand to `resolveEnvironment()` for maximum value
🎯 **This data transforms your code generation** from guessing to knowing
## Resources
- [Python Extension API Documentation](https://github.com/microsoft/vscode-python/wiki/Python-Environment-APIs)
- [@vscode/python-extension NPM Module](https://www.npmjs.com/package/@vscode/python-extension)
- [Python Extension GitHub Repository](https://github.com/microsoft/vscode-python)
---
**Remember:** The Python extension has already solved the hard problems. Your job is to leverage that intelligence to build smarter tools!
+3
View File
@@ -39,6 +39,9 @@ export const toolParamNames = [
"needs_more_exploration",
"task_progress",
"timeout",
"plugin_id",
"capability_name",
"parameters",
] as const
export type ToolParamName = (typeof toolParamNames)[number]
+5
View File
@@ -8,6 +8,7 @@ import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMi
import { downloadTask } from "@integrations/misc/export-markdown"
import { ClineAccountService } from "@services/account/ClineAccountService"
import { McpHub } from "@services/mcp/McpHub"
import { PluginHub } from "@services/plugins/PluginHub"
import { ApiProvider, ModelInfo } from "@shared/api"
import { ChatContent } from "@shared/ChatContent"
import { ExtensionState, Platform } from "@shared/ExtensionMessage"
@@ -63,6 +64,7 @@ export class Controller {
task?: Task
mcpHub: McpHub
pluginHub: PluginHub
accountService: ClineAccountService
authService: AuthService
ocaAuthService: OcaAuthService
@@ -161,6 +163,9 @@ export class Controller {
telemetryService,
)
// Initialize Plugin Hub
this.pluginHub = new PluginHub(context)
// Clean up legacy checkpoints
cleanupLegacyCheckpoints().catch((error) => {
console.error("Failed to cleanup legacy checkpoints:", error)
+9
View File
@@ -56,6 +56,15 @@ Otherwise, if you have not completed the task and do not need additional informa
invalidMcpToolArgumentError: (serverName: string, toolName: string) =>
`Invalid JSON argument used with ${serverName} for ${toolName}. Please retry with a properly formatted JSON argument.`,
invalidPluginArgumentError: (pluginId: string, capabilityName: string) =>
`Invalid JSON parameter used with plugin '${pluginId}' for capability '${capabilityName}'. Please retry with a properly formatted JSON parameter.`,
pluginSuccess: (pluginId: string, capabilityName: string, result: string) =>
`Plugin '${pluginId}' capability '${capabilityName}' executed successfully.\n\nResult:\n${result}`,
pluginError: (pluginId: string, capabilityName: string, error: string) =>
`Plugin '${pluginId}' capability '${capabilityName}' failed with error:\n${error}`,
toolResult: (
text: string,
images?: string[],
@@ -8,6 +8,7 @@ import { getEditingFilesSection } from "./editing_files"
import { getFeedbackSection } from "./feedback"
import { getMcp } from "./mcp"
import { getObjectiveSection } from "./objective"
import { getPluginSection } from "./plugins"
import { getRulesSection } from "./rules"
import { getSystemInfo } from "./system_info"
import { getUpdatingTaskProgress } from "./task_progress"
@@ -24,6 +25,7 @@ export function getSystemPromptComponents() {
{ id: SystemPromptSection.AGENT_ROLE, fn: getAgentRoleSection },
{ id: SystemPromptSection.SYSTEM_INFO, fn: getSystemInfo },
{ id: SystemPromptSection.MCP, fn: getMcp },
{ id: SystemPromptSection.PLUGINS, fn: getPluginSection },
{ id: SystemPromptSection.TODO, fn: getTodoListSection },
{
id: SystemPromptSection.USER_INSTRUCTIONS,
@@ -0,0 +1,63 @@
/**
* Plugin System Prompt Component
*
* Generates the system prompt section describing available plugin capabilities.
* This allows the LLM to discover and use plugin tools dynamically.
*/
import type { PromptVariant, SystemPromptContext } from "../types"
/**
* Get the plugin section for the system prompt.
* Includes all registered plugins and their capabilities.
*
* @param variant - The prompt variant being generated
* @param context - The system prompt context containing pluginHub
* @returns Formatted plugin section string, or undefined if no plugins
*/
export async function getPluginSection(variant: PromptVariant, context: SystemPromptContext): Promise<string | undefined> {
// Return undefined if plugin hub not available or no plugins registered
const pluginHub = context.pluginHub
if (!pluginHub || pluginHub.getPluginCount() === 0) {
return undefined
}
const pluginPrompts = pluginHub.getPluginPrompts()
if (!pluginPrompts) {
return undefined
}
return `
# Plugin Extensions
The following plugin extensions are available to extend your capabilities:
${pluginPrompts}
## Using Plugin Capabilities
To use a plugin capability, use the plugin_execute tool:
<plugin_execute>
<plugin_id>The plugin ID (e.g., "cline-python-env")</plugin_id>
<capability_name>The capability name (e.g., "getPythonEnvironment")</capability_name>
<parameters>
{
"param1": "value1",
"param2": "value2"
}
</parameters>
</plugin_execute>
The <parameters> field should contain a JSON object with the required and optional parameters as defined by the capability. If no parameters are required, you can omit the <parameters> field or pass an empty object {}.
Plugin capabilities are particularly useful for:
- Accessing runtime environment information (e.g., Python/Node versions, installed packages)
- Integrating with other VS Code extensions' APIs
- Performing domain-specific operations not available through standard tools
- Querying external services and APIs
Always check the capability's parameter definitions and examples before using it to ensure correct usage.
`
}
@@ -3,6 +3,7 @@ export enum SystemPromptSection {
TOOL_USE = "TOOL_USE_SECTION",
TOOLS = "TOOLS_SECTION",
MCP = "MCP_SECTION",
PLUGINS = "PLUGINS_SECTION",
EDITING_FILES = "EDITING_FILES_SECTION",
ACT_VS_PLAN = "ACT_VS_PLAN_SECTION",
CLI_SUBAGENTS = "CLI_SUBAGENTS_SECTION",
+2
View File
@@ -4,6 +4,7 @@
import { ApiProviderInfo } from "@/core/api"
import type { McpHub } from "@/services/mcp/McpHub"
import type { PluginHub } from "@/services/plugins/PluginHub"
import type { BrowserSettings } from "@/shared/BrowserSettings"
import type { FocusChainSettings } from "@/shared/FocusChainSettings"
import { ModelFamily } from "@/shared/prompts"
@@ -94,6 +95,7 @@ export interface SystemPromptContext {
readonly ide: string
readonly supportsBrowserUse?: boolean
readonly mcpHub?: McpHub
readonly pluginHub?: PluginHub
readonly focusChainSettings?: FocusChainSettings
readonly globalClineRulesFileInstructions?: string
readonly localClineRulesFileInstructions?: string
+5
View File
@@ -6,6 +6,7 @@ import { BrowserSession } from "@services/browser/BrowserSession"
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
import { featureFlagsService } from "@services/feature-flags"
import { McpHub } from "@services/mcp/McpHub"
import { PluginHub } from "@services/plugins/PluginHub"
import { ClineAsk, ClineSay } from "@shared/ExtensionMessage"
import { ClineDefaultTool } from "@shared/tools"
import { ClineAskResponse } from "@shared/WebviewMessage"
@@ -32,6 +33,7 @@ import { ListFilesToolHandler } from "./tools/handlers/ListFilesToolHandler"
import { LoadMcpDocumentationHandler } from "./tools/handlers/LoadMcpDocumentationHandler"
import { NewTaskHandler } from "./tools/handlers/NewTaskHandler"
import { PlanModeRespondHandler } from "./tools/handlers/PlanModeRespondHandler"
import { PluginToolHandler } from "./tools/handlers/PluginToolHandler"
import { ReadFileToolHandler } from "./tools/handlers/ReadFileToolHandler"
import { ReportBugHandler } from "./tools/handlers/ReportBugHandler"
import { SearchFilesToolHandler } from "./tools/handlers/SearchFilesToolHandler"
@@ -72,6 +74,7 @@ export class ToolExecutor {
private browserSession: BrowserSession,
private diffViewProvider: DiffViewProvider,
private mcpHub: McpHub,
private pluginHub: PluginHub,
private fileContextTracker: FileContextTracker,
private clineIgnoreController: ClineIgnoreController,
private contextManager: ContextManager,
@@ -142,6 +145,7 @@ export class ToolExecutor {
focusChainSettings: this.stateManager.getGlobalSettingsKey("focusChainSettings"),
services: {
mcpHub: this.mcpHub,
pluginHub: this.pluginHub,
browserSession: this.browserSession,
urlContentFetcher: this.urlContentFetcher,
diffViewProvider: this.diffViewProvider,
@@ -201,6 +205,7 @@ export class ToolExecutor {
this.coordinator.register(new UseMcpToolHandler())
this.coordinator.register(new AccessMcpResourceHandler())
this.coordinator.register(new LoadMcpDocumentationHandler())
this.coordinator.register(new PluginToolHandler())
this.coordinator.register(new PlanModeRespondHandler())
this.coordinator.register(new NewTaskHandler())
this.coordinator.register(new AttemptCompletionHandler())
+2
View File
@@ -458,6 +458,7 @@ export class Task {
this.browserSession,
this.diffViewProvider,
this.mcpHub,
this.controller.pluginHub,
this.fileContextTracker,
this.clineIgnoreController,
this.contextManager,
@@ -1801,6 +1802,7 @@ export class Task {
providerInfo,
supportsBrowserUse,
mcpHub: this.mcpHub,
pluginHub: this.controller.pluginHub,
focusChainSettings: this.stateManager.getGlobalSettingsKey("focusChainSettings"),
globalClineRulesFileInstructions,
localClineRulesFileInstructions,
@@ -0,0 +1,191 @@
/**
* Plugin Tool Handler
*
* Handles execution of plugin capabilities through the tool coordinator.
* Integrates plugin system with Cline's tool execution infrastructure.
*/
import type { ToolUse } from "@core/assistant-message"
import { formatResponse } from "@core/prompts/responses"
import { ClineAsk } from "@shared/ExtensionMessage"
import type { PluginContextConfig } from "@/services/plugins/PluginContext"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
/**
* Message type for plugin execution (similar to ClineAskUseMcpServer)
*/
interface ClineAskUsePlugin {
type: "use_plugin"
pluginId: string
capabilityName: string
parameters: string // JSON string
}
export class PluginToolHandler implements IFullyManagedTool {
readonly name = ClineDefaultTool.PLUGIN_EXECUTE
getDescription(block: ToolUse): string {
return `[${block.name} for '${block.params.plugin_id}.${block.params.capability_name}']`
}
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
const plugin_id = block.params.plugin_id
const capability_name = block.params.capability_name
const parameters = block.params.parameters
const partialMessage = JSON.stringify({
type: "use_plugin",
pluginId: uiHelpers.removeClosingTag(block, "plugin_id", plugin_id),
capabilityName: uiHelpers.removeClosingTag(block, "capability_name", capability_name),
parameters: uiHelpers.removeClosingTag(block, "parameters", parameters),
} satisfies ClineAskUsePlugin)
// Check if tool should be auto-approved
const config = uiHelpers.getConfig()
const shouldAutoApprove = config.callbacks.shouldAutoApproveTool(block.name)
if (shouldAutoApprove) {
await uiHelpers.removeLastPartialMessageIfExistsWithType("ask", "use_plugin")
await uiHelpers.say("use_plugin" as any, partialMessage, undefined, undefined, block.partial)
} else {
await uiHelpers.removeLastPartialMessageIfExistsWithType("say", "use_plugin")
await uiHelpers.ask("use_plugin" as ClineAsk, partialMessage, block.partial).catch(() => {})
}
}
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
const plugin_id: string | undefined = block.params.plugin_id
const capability_name: string | undefined = block.params.capability_name
const parameters: string | undefined = block.params.parameters
// Validate required parameters
if (!plugin_id) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError(block.name, "plugin_id")
}
if (!capability_name) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError(block.name, "capability_name")
}
// Parse and validate parameters if provided
let parsedParameters: Record<string, any> = {}
if (parameters) {
try {
parsedParameters = JSON.parse(parameters)
} catch (_error) {
config.taskState.consecutiveMistakeCount++
await config.callbacks.say(
"error",
`Cline tried to use ${capability_name} with an invalid JSON parameter. Retrying...`,
)
return formatResponse.toolError(formatResponse.invalidPluginArgumentError(plugin_id, capability_name || ""))
}
}
config.taskState.consecutiveMistakeCount = 0
// Handle approval flow
const completeMessage = JSON.stringify({
type: "use_plugin",
pluginId: plugin_id,
capabilityName: capability_name,
parameters: parameters ?? "{}",
} satisfies ClineAskUsePlugin)
// Check if this specific plugin capability is auto-approved
// For now, plugins follow the general tool auto-approval setting
// In the future, we could add per-plugin capability auto-approval
if (config.callbacks.shouldAutoApproveTool(block.name)) {
// Auto-approval flow
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "use_plugin")
await config.callbacks.say("use_plugin", completeMessage, undefined, undefined, false)
if (!config.yoloModeToggled) {
config.taskState.consecutiveAutoApprovedRequestsCount++
}
// Capture telemetry
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
} else {
// Manual approval flow
const notificationMessage = `Cline wants to use ${capability_name || "unknown capability"} from ${plugin_id || "unknown plugin"}`
// Show notification
showNotificationForApprovalIfAutoApprovalEnabled(
notificationMessage,
config.autoApprovalSettings.enabled,
config.autoApprovalSettings.enableNotifications,
)
await config.callbacks.removeLastPartialMessageIfExistsWithType("say", "use_plugin")
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("use_plugin", completeMessage, config)
if (!didApprove) {
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, false)
return formatResponse.toolDenied()
} else {
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true)
}
}
// Show plugin request started message
await config.callbacks.say("plugin_request_started", `Executing ${plugin_id}.${capability_name}...`)
try {
// Check if plugin hub is available
if (!config.services.pluginHub) {
throw new Error("Plugin system is not initialized")
}
// Create plugin context configuration
const contextConfig: PluginContextConfig = {
pluginId: plugin_id,
taskId: config.ulid,
taskMode: config.mode,
workingDirectory: config.cwd,
extensionContext: config.context,
notifyCallback: (message: string) => {
config.callbacks.say("plugin_notification", `[${plugin_id}] ${message}`)
},
requestInputCallback: async (prompt: string) => {
const response = await config.callbacks.ask("followup", prompt)
return response.text || ""
},
}
// Execute the plugin capability
const result = await config.services.pluginHub.executePluginCapability(
plugin_id,
capability_name,
parsedParameters,
contextConfig,
)
// Check if execution was successful
if (!result.success) {
await config.callbacks.say("plugin_error", `Error: ${result.error}`)
return formatResponse.pluginError(plugin_id, capability_name, result.error || "Unknown error")
}
// Format the result for display
const resultText = typeof result.data === "string" ? result.data : JSON.stringify(result.data, null, 2)
await config.callbacks.say("plugin_response", resultText)
// Return formatted result
return formatResponse.pluginSuccess(plugin_id, capability_name, resultText)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
await config.callbacks.say("plugin_error", `Error: ${errorMessage}`)
return formatResponse.pluginError(plugin_id, capability_name, errorMessage)
}
}
}
+2
View File
@@ -5,6 +5,7 @@ import type { DiffViewProvider } from "@integrations/editor/DiffViewProvider"
import type { BrowserSession } from "@services/browser/BrowserSession"
import type { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
import type { McpHub } from "@services/mcp/McpHub"
import type { PluginHub } from "@services/plugins/PluginHub"
import type { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import type { BrowserSettings } from "@shared/BrowserSettings"
import type { ClineAsk, ClineSay } from "@shared/ExtensionMessage"
@@ -65,6 +66,7 @@ export interface TaskConfig {
*/
export interface TaskServices {
mcpHub: McpHub
pluginHub?: PluginHub
browserSession: BrowserSession
urlContentFetcher: UrlContentFetcher
diffViewProvider: DiffViewProvider
+8
View File
@@ -1,3 +1,5 @@
import type { ClinePluginAPI } from "../services/plugins/types"
export interface ClineAPI {
/**
* Starts a new task with an optional initial message and images.
@@ -22,4 +24,10 @@ export interface ClineAPI {
* Simulates pressing the secondary button in the chat interface.
*/
pressSecondaryButton(): Promise<void>
/**
* Plugin API for registering and managing Cline plugins.
* Third-party VS Code extensions can use this to extend Cline's capabilities.
*/
plugins: ClinePluginAPI
}
+4
View File
@@ -2,6 +2,7 @@ import { Controller } from "@core/controller"
import { sendChatButtonClickedEvent } from "@core/controller/ui/subscribeToChatButtonClicked"
import { HostProvider } from "@/hosts/host-provider"
import { ClineAPI } from "./cline"
import { createPluginAPI } from "./plugin-api"
export function createClineAPI(sidebarController: Controller): ClineAPI {
const api: ClineAPI = {
@@ -45,6 +46,9 @@ export function createClineAPI(sidebarController: Controller): ClineAPI {
HostProvider.get().logToChannel("No active task to press button for")
}
},
// Plugin API
plugins: createPluginAPI(sidebarController.pluginHub),
}
return api
+181
View File
@@ -0,0 +1,181 @@
/**
* Plugin API Export
*
* Public API for third-party VS Code extensions to register plugins with Cline.
* This API is exported through Cline's extension exports and accessed by plugin extensions.
*/
import * as vscode from "vscode"
import type { PluginHub } from "../services/plugins/PluginHub"
import type { ClinePlugin, ClinePluginAPI } from "../services/plugins/types"
/**
* Create the plugin API that will be exported by Cline.
* Plugin extensions access this through vscode.extensions.getExtension().exports.plugins
*
* @param pluginHub - The plugin hub instance from the controller
* @returns The plugin API object
*/
export function createPluginAPI(pluginHub: PluginHub): ClinePluginAPI {
return {
/**
* Register a plugin with Cline.
* This should be called during the plugin extension's activation.
*
* @param plugin - The plugin instance to register
* @throws Error if registration fails or plugin is invalid
*
* @example
* ```typescript
* // In plugin extension's activate() function
* const clineApi = vscode.extensions.getExtension('saoudrizwan.claude-dev')?.exports
* const myPlugin = new MyPlugin()
* await clineApi.plugins.registerPlugin(myPlugin)
* ```
*/
async registerPlugin(plugin: ClinePlugin): Promise<void> {
// Get the calling extension's ID
const extensionId = getCallingExtensionId()
if (!extensionId) {
throw new Error(
"Unable to determine calling extension ID. " +
"registerPlugin must be called from an activated VS Code extension.",
)
}
// Validate plugin before passing to hub
validatePlugin(plugin)
// Register with the hub
await pluginHub.registerPlugin(plugin, extensionId)
},
/**
* Unregister a previously registered plugin.
* This should be called during the plugin extension's deactivation.
*
* @param pluginId - ID of the plugin to unregister
*
* @example
* ```typescript
* // In plugin extension's deactivate() function
* const clineApi = vscode.extensions.getExtension('saoudrizwan.claude-dev')?.exports
* await clineApi.plugins.unregisterPlugin('my-plugin-id')
* ```
*/
async unregisterPlugin(pluginId: string): Promise<void> {
if (!pluginId || typeof pluginId !== "string") {
throw new Error("Plugin ID must be a non-empty string")
}
await pluginHub.unregisterPlugin(pluginId)
},
}
}
/**
* Get the ID of the extension that is calling this API.
* Uses the call stack to determine which extension made the call.
*
* @returns Extension ID or undefined if it cannot be determined
*/
function getCallingExtensionId(): string | undefined {
try {
// Get call stack
const stack = new Error().stack
if (!stack) {
return undefined
}
// Parse the stack to find the calling extension
// Stack traces typically contain file paths that include the extension ID
const lines = stack.split("\n")
for (const line of lines) {
// Look for patterns like:
// at /Users/username/.vscode/extensions/publisher.extension-name-version/...
// or C:\Users\username\.vscode\extensions\publisher.extension-name-version\...
const extensionMatch = line.match(/\.vscode[/\\]extensions[/\\]([^/\\]+)[/\\]/)
if (extensionMatch && extensionMatch[1]) {
// Extract publisher.extension-name from publisher.extension-name-version
const fullName = extensionMatch[1]
const versionMatch = fullName.match(/^(.+?)-\d+\.\d+\.\d+/)
return versionMatch ? versionMatch[1] : fullName
}
}
// Alternative approach: check all active extensions to find which one is in the stack
for (const ext of vscode.extensions.all) {
if (ext.isActive && ext.extensionPath && stack.includes(ext.extensionPath)) {
return ext.id
}
}
return undefined
} catch (error) {
console.error("[PluginAPI] Error determining calling extension ID:", error)
return undefined
}
}
/**
* Validate a plugin object before registration.
* Provides early validation and helpful error messages.
*
* @param plugin - The plugin to validate
* @throws Error if validation fails
*/
function validatePlugin(plugin: ClinePlugin): void {
if (!plugin || typeof plugin !== "object") {
throw new Error("Plugin must be a valid object")
}
// Validate required fields
if (!plugin.id || typeof plugin.id !== "string") {
throw new Error("Plugin must have a valid 'id' property (string). " + "Typically this should be your extension ID.")
}
if (plugin.id.trim() === "") {
throw new Error("Plugin ID cannot be empty")
}
if (!plugin.name || typeof plugin.name !== "string") {
throw new Error("Plugin must have a valid 'name' property (string)")
}
if (plugin.name.trim() === "") {
throw new Error("Plugin name cannot be empty")
}
if (!plugin.version || typeof plugin.version !== "string") {
throw new Error("Plugin must have a valid 'version' property (string). " + "Use semantic versioning (e.g., '1.0.0')")
}
// Validate version format (basic semver check)
const versionRegex = /^\d+\.\d+\.\d+/
if (!versionRegex.test(plugin.version)) {
throw new Error(`Plugin version '${plugin.version}' is not valid. ` + "Use semantic versioning format (e.g., '1.0.0')")
}
// Validate required methods
if (typeof plugin.getCapabilities !== "function") {
throw new Error("Plugin must implement 'getCapabilities()' method that returns Promise<PluginCapability[]>")
}
if (typeof plugin.executeCapability !== "function") {
throw new Error("Plugin must implement 'executeCapability(name, params, context)' method that returns Promise<any>")
}
// Validate optional fields
if (plugin.description !== undefined && typeof plugin.description !== "string") {
throw new Error("Plugin 'description' property must be a string if provided")
}
if (plugin.dispose !== undefined && typeof plugin.dispose !== "function") {
throw new Error("Plugin 'dispose' property must be a function if provided")
}
}
+4 -1
View File
@@ -13,7 +13,7 @@ import { WebviewProvider } from "./core/webview"
import { createClineAPI } from "./exports"
import { Logger } from "./services/logging/Logger"
import { cleanupTestMode, initializeTestMode } from "./services/test/TestMode"
import "./utils/path" // necessary to have access to String.prototype.toPosix
import "./utils/path"; // necessary to have access to String.prototype.toPosix
import path from "node:path"
import type { ExtensionContext } from "vscode"
@@ -393,6 +393,9 @@ export async function activate(context: vscode.ExtensionContext) {
}),
)
// Initialize plugin system - discover plugins that depend on Cline
await webview.controller.pluginHub.discoverPlugins()
return createClineAPI(webview.controller)
}
+251
View File
@@ -0,0 +1,251 @@
/**
* Plugin Context Implementation
*
* Provides isolated execution context for plugins with safe service boundaries.
* No direct access to VSCode API or internal Cline state.
*/
import axios, { AxiosError } from "axios"
import * as vscode from "vscode"
import { HttpResponse, PluginContext, PluginHttpClient, PluginLogger, PluginStorage, RequestOptions } from "./types"
/**
* Configuration for creating a plugin context
*/
export interface PluginContextConfig {
/** Plugin ID for scoping logs and storage */
pluginId: string
/** Current task ID */
taskId: string
/** Current task mode */
taskMode: "plan" | "act"
/** Working directory for the task */
workingDirectory: string
/** VS Code extension context for storage */
extensionContext: vscode.ExtensionContext
/** Callback to send notifications to user */
notifyCallback: (message: string) => void
/** Callback to request input from user */
requestInputCallback: (prompt: string) => Promise<string>
}
/**
* Rate limiter for HTTP requests
*/
class HttpRateLimiter {
private requestTimestamps: number[] = []
private readonly maxRequestsPerMinute = 60
private readonly windowMs = 60000 // 1 minute
canMakeRequest(): boolean {
const now = Date.now()
// Remove timestamps older than window
this.requestTimestamps = this.requestTimestamps.filter((ts) => now - ts < this.windowMs)
if (this.requestTimestamps.length >= this.maxRequestsPerMinute) {
return false
}
this.requestTimestamps.push(now)
return true
}
getRemainingRequests(): number {
const now = Date.now()
this.requestTimestamps = this.requestTimestamps.filter((ts) => now - ts < this.windowMs)
return Math.max(0, this.maxRequestsPerMinute - this.requestTimestamps.length)
}
}
/**
* Scoped logger implementation for plugins
*/
class PluginLoggerImpl implements PluginLogger {
constructor(
private pluginId: string,
private outputChannel: vscode.OutputChannel,
) {}
debug(message: string, data?: any): void {
this.log("DEBUG", message, data)
}
info(message: string, data?: any): void {
this.log("INFO", message, data)
}
warn(message: string, data?: any): void {
this.log("WARN", message, data)
}
error(message: string, data?: any): void {
this.log("ERROR", message, data)
}
private log(level: string, message: string, data?: any): void {
const timestamp = new Date().toISOString()
const prefix = `[${timestamp}] [${this.pluginId}] [${level}]`
if (data !== undefined) {
const dataStr = typeof data === "object" ? JSON.stringify(data, null, 2) : String(data)
this.outputChannel.appendLine(`${prefix} ${message}\n${dataStr}`)
} else {
this.outputChannel.appendLine(`${prefix} ${message}`)
}
}
}
/**
* Plugin-scoped storage implementation
*/
class PluginStorageImpl implements PluginStorage {
private readonly storageKeyPrefix: string
constructor(
private pluginId: string,
private globalState: vscode.Memento,
) {
this.storageKeyPrefix = `plugin_${pluginId}_`
}
async get<T>(key: string): Promise<T | undefined> {
const fullKey = this.getFullKey(key)
return this.globalState.get<T>(fullKey)
}
async set<T>(key: string, value: T): Promise<void> {
const fullKey = this.getFullKey(key)
await this.globalState.update(fullKey, value)
}
async delete(key: string): Promise<void> {
const fullKey = this.getFullKey(key)
await this.globalState.update(fullKey, undefined)
}
async clear(): Promise<void> {
const keys = this.globalState.keys()
const pluginKeys = keys.filter((k) => k.startsWith(this.storageKeyPrefix))
for (const key of pluginKeys) {
await this.globalState.update(key, undefined)
}
}
private getFullKey(key: string): string {
return `${this.storageKeyPrefix}${key}`
}
}
/**
* Rate-limited HTTP client implementation
*/
class PluginHttpClientImpl implements PluginHttpClient {
private rateLimiter = new HttpRateLimiter()
private readonly defaultTimeout = 30000 // 30 seconds
constructor(private pluginId: string) {}
async get(url: string, options?: RequestOptions): Promise<HttpResponse> {
return this.request("GET", url, undefined, options)
}
async post(url: string, data?: any, options?: RequestOptions): Promise<HttpResponse> {
return this.request("POST", url, data, options)
}
private async request(method: "GET" | "POST", url: string, data?: any, options?: RequestOptions): Promise<HttpResponse> {
// Check rate limit
if (!this.rateLimiter.canMakeRequest()) {
throw new Error(
`Rate limit exceeded for plugin '${this.pluginId}'. ` +
`Maximum 60 requests per minute allowed. ` +
`Try again in a few seconds.`,
)
}
// Validate URL
try {
new URL(url)
} catch (error) {
throw new Error(`Invalid URL: ${url}`)
}
// Ensure HTTPS for external requests
const urlObj = new URL(url)
if (urlObj.protocol === "http:" && !this.isLocalhost(urlObj.hostname)) {
throw new Error(`Plugin HTTP client requires HTTPS for external requests. ` + `Use https:// instead of http://`)
}
try {
const timeout = options?.timeout ?? this.defaultTimeout
const response = await axios({
method,
url,
data,
headers: options?.headers,
timeout,
validateStatus: () => true, // Don't throw on any status code
})
return {
status: response.status,
data: response.data,
headers: response.headers as Record<string, string>,
}
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError
if (axiosError.code === "ECONNABORTED") {
throw new Error(`Request timeout after ${options?.timeout ?? this.defaultTimeout}ms`)
}
throw new Error(`HTTP request failed: ${axiosError.message}`)
}
throw error
}
}
private isLocalhost(hostname: string): boolean {
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"
}
}
/**
* Create a plugin context with safe service boundaries
*/
export function createPluginContext(config: PluginContextConfig): PluginContext {
// Create output channel for plugin logs
const outputChannel = vscode.window.createOutputChannel(`Cline Plugin: ${config.pluginId}`)
// Create service implementations
const logger = new PluginLoggerImpl(config.pluginId, outputChannel)
const storage = new PluginStorageImpl(config.pluginId, config.extensionContext.globalState)
const http = new PluginHttpClientImpl(config.pluginId)
// Return context with security boundaries
return {
taskId: config.taskId,
taskMode: config.taskMode,
workingDirectory: config.workingDirectory,
logger,
storage,
http,
notify: config.notifyCallback,
requestInput: config.requestInputCallback,
}
}
/**
* Dispose of resources associated with a plugin context
*/
export function disposePluginContext(context: PluginContext): void {
// Clean up any resources
// Note: Output channels are managed by VS Code and don't need explicit disposal
// Storage is persisted in VS Code's global state
}
+492
View File
@@ -0,0 +1,492 @@
/**
* Plugin Hub Service
*
* Central registry and lifecycle management for Cline plugins.
* Handles discovery, registration, execution, and system prompt integration.
*/
import * as vscode from "vscode"
import { createPluginContext, PluginContextConfig } from "./PluginContext"
import { ClinePlugin, PluginCapability, PluginExecutionResult, RegisteredPlugin } from "./types"
/**
* Central hub for managing Cline plugins
*/
export class PluginHub {
private plugins: Map<string, RegisteredPlugin> = new Map()
private readonly extensionContext: vscode.ExtensionContext
constructor(extensionContext: vscode.ExtensionContext) {
this.extensionContext = extensionContext
}
/**
* Discover and register plugins from VS Code extensions during Cline activation.
* Scans all installed extensions that declare Cline as a dependency.
*/
async discoverPlugins(): Promise<void> {
const clineExtensionId = "saoudrizwan.claude-dev"
// Get all extensions
const allExtensions = vscode.extensions.all
// Filter extensions that depend on Cline
const dependentExtensions = allExtensions.filter((ext) => {
const deps = ext.packageJSON?.extensionDependencies as string[] | undefined
return deps?.includes(clineExtensionId)
})
console.log(`[PluginHub] Found ${dependentExtensions.length} extensions that depend on Cline`)
// Note: Actual plugin registration happens when extensions call registerPlugin()
// during their activation. This discovery phase just logs potential plugins.
for (const ext of dependentExtensions) {
if (!ext.isActive) {
console.log(`[PluginHub] Extension ${ext.id} not yet active, will register when activated`)
}
}
}
/**
* Register a plugin with the hub.
* Called by plugin extensions through the Cline API.
*
* @param plugin - Plugin instance to register
* @param extensionId - ID of the extension registering the plugin
* @throws Error if plugin ID conflicts or registration fails
*/
async registerPlugin(plugin: ClinePlugin, extensionId: string): Promise<void> {
// Validate plugin
if (!plugin.id || typeof plugin.id !== "string") {
throw new Error("Plugin must have a valid string ID")
}
if (!plugin.name || typeof plugin.name !== "string") {
throw new Error("Plugin must have a valid string name")
}
if (!plugin.version || typeof plugin.version !== "string") {
throw new Error("Plugin must have a valid string version")
}
if (typeof plugin.getCapabilities !== "function") {
throw new Error("Plugin must implement getCapabilities() method")
}
if (typeof plugin.executeCapability !== "function") {
throw new Error("Plugin must implement executeCapability() method")
}
// Check for ID conflicts
if (this.plugins.has(plugin.id)) {
throw new Error(`Plugin with ID '${plugin.id}' is already registered`)
}
console.log(`[PluginHub] Registering plugin: ${plugin.id} (${plugin.name} v${plugin.version})`)
try {
// Get capabilities from plugin
const capabilities = await Promise.race([
plugin.getCapabilities(),
new Promise<never>((_, reject) => setTimeout(() => reject(new Error("Capability retrieval timeout")), 10000)),
])
// Validate capabilities
this.validateCapabilities(capabilities, plugin.id)
// Create capabilities map
const capabilitiesMap = new Map<string, PluginCapability>()
for (const capability of capabilities) {
capabilitiesMap.set(capability.name, capability)
}
// Register plugin
const registeredPlugin: RegisteredPlugin = {
plugin,
extensionId,
capabilities: capabilitiesMap,
isActive: true,
}
this.plugins.set(plugin.id, registeredPlugin)
console.log(`[PluginHub] Successfully registered plugin '${plugin.id}' with ${capabilities.length} capabilities`)
} catch (error) {
console.error(`[PluginHub] Failed to register plugin '${plugin.id}':`, error)
throw new Error(`Plugin registration failed: ${error instanceof Error ? error.message : String(error)}`)
}
}
/**
* Unregister a plugin from the hub.
*
* @param pluginId - ID of plugin to unregister
*/
async unregisterPlugin(pluginId: string): Promise<void> {
const registered = this.plugins.get(pluginId)
if (!registered) {
console.warn(`[PluginHub] Attempted to unregister unknown plugin: ${pluginId}`)
return
}
console.log(`[PluginHub] Unregistering plugin: ${pluginId}`)
try {
// Call plugin's dispose method if it exists
if (typeof registered.plugin.dispose === "function") {
await Promise.race([
registered.plugin.dispose(),
new Promise<void>((_, reject) => setTimeout(() => reject(new Error("Dispose timeout")), 5000)),
])
}
} catch (error) {
console.error(`[PluginHub] Error disposing plugin '${pluginId}':`, error)
// Continue with unregistration even if dispose fails
}
this.plugins.delete(pluginId)
console.log(`[PluginHub] Plugin '${pluginId}' unregistered`)
}
/**
* Execute a plugin capability with the given parameters.
*
* @param pluginId - ID of the plugin
* @param capabilityName - Name of the capability to execute
* @param parameters - Parameters to pass to the capability
* @param contextConfig - Configuration for creating the plugin context
* @returns Promise resolving to the execution result
*/
async executePluginCapability(
pluginId: string,
capabilityName: string,
parameters: Record<string, any>,
contextConfig: PluginContextConfig,
): Promise<PluginExecutionResult> {
const startTime = Date.now()
try {
// Get registered plugin
const registered = this.plugins.get(pluginId)
if (!registered) {
return {
success: false,
error: `Plugin '${pluginId}' not found. Available plugins: ${Array.from(this.plugins.keys()).join(", ") || "none"}`,
duration: Date.now() - startTime,
pluginId,
capabilityName,
}
}
if (!registered.isActive) {
return {
success: false,
error: `Plugin '${pluginId}' is not active${registered.lastError ? `: ${registered.lastError}` : ""}`,
duration: Date.now() - startTime,
pluginId,
capabilityName,
}
}
// Check capability exists
const capability = registered.capabilities.get(capabilityName)
if (!capability) {
const availableCapabilities = Array.from(registered.capabilities.keys()).join(", ")
return {
success: false,
error: `Capability '${capabilityName}' not found in plugin '${pluginId}'. Available capabilities: ${availableCapabilities}`,
duration: Date.now() - startTime,
pluginId,
capabilityName,
}
}
// Validate parameters
const validationError = this.validateParameters(parameters, capability)
if (validationError) {
return {
success: false,
error: validationError,
duration: Date.now() - startTime,
pluginId,
capabilityName,
}
}
// Create plugin context with security boundaries
const context = createPluginContext(contextConfig)
console.log(`[PluginHub] Executing ${pluginId}.${capabilityName}`)
// Execute capability with timeout
const result = await Promise.race([
registered.plugin.executeCapability(capabilityName, parameters, context),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Execution timeout after 60 seconds")), 60000),
),
])
// Update last execution time
registered.lastExecutionTime = Date.now()
return {
success: true,
data: result,
duration: Date.now() - startTime,
pluginId,
capabilityName,
}
} catch (error) {
console.error(`[PluginHub] Error executing ${pluginId}.${capabilityName}:`, error)
// Mark plugin as having an error
const registered = this.plugins.get(pluginId)
if (registered) {
registered.lastError = error instanceof Error ? error.message : String(error)
}
return {
success: false,
error: error instanceof Error ? error.message : String(error),
duration: Date.now() - startTime,
pluginId,
capabilityName,
}
}
}
/**
* Get formatted plugin prompts for inclusion in system prompt.
* Returns a formatted string describing all available plugins and their capabilities.
*/
getPluginPrompts(): string {
if (this.plugins.size === 0) {
return ""
}
const sections: string[] = []
for (const [pluginId, registered] of this.plugins.entries()) {
if (!registered.isActive) {
continue
}
const plugin = registered.plugin
let section = `## ${plugin.name} (${pluginId})`
if (plugin.description) {
section += `\n${plugin.description}`
}
section += "\n\n### Available Capabilities:\n"
for (const [capName, capability] of registered.capabilities.entries()) {
section += `\n**${capName}**\n`
section += `Description: ${capability.description}\n`
if (capability.parameters.length > 0) {
section += "\nParameters:\n"
for (const param of capability.parameters) {
const requiredStr = param.required ? "required" : "optional"
const descStr = param.description ? `: ${param.description}` : ""
section += `- ${param.name} (${param.type}, ${requiredStr})${descStr}\n`
}
} else {
section += "\nNo parameters required.\n"
}
if (capability.returns) {
section += `\nReturns: ${capability.returns}\n`
}
if (capability.prompt) {
section += `\nUsage: ${capability.prompt}\n`
}
if (capability.examples && capability.examples.length > 0) {
section += "\nExamples:\n"
for (const example of capability.examples) {
section += `- ${example}\n`
}
}
}
sections.push(section)
}
return sections.join("\n\n")
}
/**
* Get all plugin capabilities as an array.
* Useful for programmatic access to capabilities.
*/
getPluginCapabilities(): Array<{ pluginId: string; capability: PluginCapability }> {
const capabilities: Array<{ pluginId: string; capability: PluginCapability }> = []
for (const [pluginId, registered] of this.plugins.entries()) {
if (!registered.isActive) {
continue
}
for (const capability of registered.capabilities.values()) {
capabilities.push({ pluginId, capability })
}
}
return capabilities
}
/**
* Get count of registered plugins.
*/
getPluginCount(): number {
return this.plugins.size
}
/**
* Get count of active plugins.
*/
getActivePluginCount(): number {
return Array.from(this.plugins.values()).filter((p) => p.isActive).length
}
/**
* Get information about all registered plugins.
*/
getPlugins(): Array<{
id: string
name: string
version: string
description?: string
extensionId: string
isActive: boolean
capabilityCount: number
lastError?: string
}> {
return Array.from(this.plugins.values()).map((registered) => ({
id: registered.plugin.id,
name: registered.plugin.name,
version: registered.plugin.version,
description: registered.plugin.description,
extensionId: registered.extensionId,
isActive: registered.isActive,
capabilityCount: registered.capabilities.size,
lastError: registered.lastError,
}))
}
/**
* Validate plugin capabilities array.
*/
private validateCapabilities(capabilities: PluginCapability[], pluginId: string): void {
if (!Array.isArray(capabilities)) {
throw new Error("getCapabilities() must return an array")
}
if (capabilities.length === 0) {
throw new Error("Plugin must provide at least one capability")
}
const capabilityNames = new Set<string>()
for (const capability of capabilities) {
// Validate capability structure
if (!capability.name || typeof capability.name !== "string") {
throw new Error("Each capability must have a valid string name")
}
if (!capability.description || typeof capability.description !== "string") {
throw new Error(`Capability '${capability.name}' must have a valid string description`)
}
if (!Array.isArray(capability.parameters)) {
throw new Error(`Capability '${capability.name}' must have a parameters array`)
}
// Check for duplicate capability names
if (capabilityNames.has(capability.name)) {
throw new Error(`Duplicate capability name '${capability.name}' in plugin '${pluginId}'`)
}
capabilityNames.add(capability.name)
// Validate parameters
for (const param of capability.parameters) {
if (!param.name || typeof param.name !== "string") {
throw new Error(
`Invalid parameter in capability '${capability.name}': parameter must have a valid string name`,
)
}
const validTypes = ["string", "number", "boolean", "object", "array"]
if (!validTypes.includes(param.type)) {
throw new Error(
`Invalid parameter type '${param.type}' for parameter '${param.name}' in capability '${capability.name}'`,
)
}
if (typeof param.required !== "boolean") {
throw new Error(
`Parameter '${param.name}' in capability '${capability.name}' must have a boolean 'required' field`,
)
}
}
}
}
/**
* Validate parameters against capability definition.
*/
private validateParameters(parameters: Record<string, any>, capability: PluginCapability): string | null {
// Check required parameters
for (const paramDef of capability.parameters) {
if (paramDef.required) {
if (!(paramDef.name in parameters)) {
return `Missing required parameter '${paramDef.name}'`
}
const value = parameters[paramDef.name]
// Basic type checking
const actualType = Array.isArray(value) ? "array" : typeof value
const expectedType = paramDef.type
if (actualType !== expectedType) {
return `Parameter '${paramDef.name}' must be of type '${expectedType}', got '${actualType}'`
}
}
}
// Check for unexpected parameters
const validParamNames = new Set(capability.parameters.map((p) => p.name))
for (const paramName in parameters) {
if (!validParamNames.has(paramName)) {
return `Unexpected parameter '${paramName}'. Valid parameters: ${Array.from(validParamNames).join(", ")}`
}
}
return null
}
/**
* Dispose of all plugins and clean up resources.
*/
async dispose(): Promise<void> {
console.log("[PluginHub] Disposing all plugins")
const pluginIds = Array.from(this.plugins.keys())
for (const pluginId of pluginIds) {
try {
await this.unregisterPlugin(pluginId)
} catch (error) {
console.error(`[PluginHub] Error unregistering plugin '${pluginId}':`, error)
}
}
this.plugins.clear()
}
}
+338
View File
@@ -0,0 +1,338 @@
/**
* Cline Plugin System Type Definitions
*
* These types define the contract between Cline and plugin extensions.
* All interfaces must remain stable for backward compatibility.
*/
/**
* Core plugin interface that all Cline plugin extensions must implement.
* Plugins are VS Code extensions that declare Cline as a dependency and
* register during their activation.
*/
export interface ClinePlugin {
/** Unique identifier for the plugin (typically the extension ID) */
readonly id: string
/** Human-readable display name */
readonly name: string
/** Semantic version string (e.g., "1.0.0") */
readonly version: string
/** Optional description of the plugin's purpose */
readonly description?: string
/**
* Get all capabilities/tools provided by this plugin.
* Called during registration and when capabilities need to be refreshed.
*
* @returns Promise resolving to array of capability definitions
*/
getCapabilities(): Promise<PluginCapability[]>
/**
* Execute a specific capability with the given parameters.
* Cline calls this when the LLM requests to use a plugin tool.
*
* @param capabilityName - Name of the capability to execute
* @param parameters - Parameters passed from the LLM
* @param context - Limited execution context with safe services
* @returns Promise resolving to the capability's result
* @throws Error if execution fails (will be caught and handled by Cline)
*/
executeCapability(
capabilityName: string,
parameters: Record<string, any>,
context: PluginContext
): Promise<any>
/**
* Optional cleanup method called when plugin is unregistered.
* Use this to dispose of resources, close connections, etc.
*/
dispose?(): Promise<void>
}
/**
* Definition of a single capability/tool provided by a plugin.
* Each capability becomes a tool that the LLM can use.
*/
export interface PluginCapability {
/** Unique name within this plugin (e.g., "getPythonEnvironment") */
name: string
/** Description of what this capability does (shown to LLM) */
description: string
/** Parameter definitions for this capability */
parameters: ParameterDefinition[]
/** Optional description of what this capability returns */
returns?: string
/**
* Optional guidance for the LLM on when and how to use this capability.
* Use this to provide context, examples, and best practices.
*/
prompt?: string
/**
* Optional usage examples to help the LLM understand when to use this capability.
* Each example should be a brief scenario description.
*/
examples?: string[]
}
/**
* Definition of a parameter for a plugin capability.
* Used to validate inputs and inform the LLM about expected parameters.
*/
export interface ParameterDefinition {
/** Parameter name */
name: string
/** Parameter type */
type: "string" | "number" | "boolean" | "object" | "array"
/** Whether this parameter is required */
required: boolean
/** Optional description of the parameter */
description?: string
/** Optional default value if parameter is not provided */
defaultValue?: any
}
/**
* Limited execution context provided to plugins.
* Provides safe, controlled access to services without exposing
* internal Cline state or VS Code APIs.
*/
export interface PluginContext {
/** Unique identifier for the current task */
taskId: string
/** Current task mode (plan or act) */
taskMode: "plan" | "act"
/** Current working directory for the task */
workingDirectory: string
/** Scoped logger for plugin output */
logger: PluginLogger
/** Plugin-specific key-value storage */
storage: PluginStorage
/** Rate-limited HTTP client for external requests */
http: PluginHttpClient
/**
* Send a notification message to the user.
* Use sparingly - messages appear in the chat interface.
*
* @param message - Message to display to the user
*/
notify(message: string): void
/**
* Request text input from the user.
* This will pause execution until the user responds.
*
* @param prompt - Prompt to show the user
* @returns Promise resolving to the user's input
*/
requestInput(prompt: string): Promise<string>
}
/**
* Scoped logging interface for plugins.
* Logs are associated with the plugin ID and included in task history.
*/
export interface PluginLogger {
/**
* Log debug information (verbose logging).
* Only shown when debug mode is enabled.
*/
debug(message: string, data?: any): void
/** Log informational messages */
info(message: string, data?: any): void
/** Log warning messages */
warn(message: string, data?: any): void
/** Log error messages */
error(message: string, data?: any): void
}
/**
* Plugin-scoped storage interface.
* Each plugin has isolated storage that persists across sessions.
* Storage is scoped by plugin ID.
*/
export interface PluginStorage {
/**
* Get a value from storage.
*
* @param key - Storage key
* @returns Promise resolving to the stored value or undefined
*/
get<T>(key: string): Promise<T | undefined>
/**
* Set a value in storage.
*
* @param key - Storage key
* @param value - Value to store (must be JSON-serializable)
*/
set<T>(key: string, value: T): Promise<void>
/**
* Delete a value from storage.
*
* @param key - Storage key
*/
delete(key: string): Promise<void>
/**
* Clear all storage for this plugin.
* Use with caution - this cannot be undone.
*/
clear(): Promise<void>
}
/**
* Rate-limited HTTP client for plugins.
* Prevents plugins from making excessive external requests.
*/
export interface PluginHttpClient {
/**
* Make a GET request.
*
* @param url - URL to request
* @param options - Optional request configuration
* @returns Promise resolving to the response
*/
get(url: string, options?: RequestOptions): Promise<HttpResponse>
/**
* Make a POST request.
*
* @param url - URL to request
* @param data - Data to send in request body
* @param options - Optional request configuration
* @returns Promise resolving to the response
*/
post(url: string, data?: any, options?: RequestOptions): Promise<HttpResponse>
}
/**
* Configuration options for HTTP requests.
*/
export interface RequestOptions {
/** HTTP headers to include in the request */
headers?: Record<string, string>
/** Request timeout in milliseconds (default: 30000) */
timeout?: number
}
/**
* HTTP response from the plugin HTTP client.
*/
export interface HttpResponse {
/** HTTP status code */
status: number
/** Response data (parsed JSON if applicable) */
data: any
/** Response headers */
headers: Record<string, string>
}
/**
* API exported by Cline for plugin registration.
* Plugin extensions access this via the Cline extension's exports.
*/
export interface ClinePluginAPI {
/**
* Register a plugin with Cline.
* Must be called during the plugin extension's activation.
*
* @param plugin - Plugin instance to register
* @throws Error if registration fails or plugin ID conflicts
*/
registerPlugin(plugin: ClinePlugin): Promise<void>
/**
* Unregister a previously registered plugin.
* Should be called during plugin extension's deactivation.
*
* @param pluginId - ID of the plugin to unregister
*/
unregisterPlugin(pluginId: string): Promise<void>
}
/**
* Internal type representing a registered plugin with metadata.
* Not exposed to plugin extensions.
*/
export interface RegisteredPlugin {
/** The plugin instance */
plugin: ClinePlugin
/** ID of the VS Code extension that registered this plugin */
extensionId: string
/** Cached capabilities map (name -> capability) */
capabilities: Map<string, PluginCapability>
/** Whether the plugin is currently active and usable */
isActive: boolean
/** Last error message if plugin has failed */
lastError?: string
/** Timestamp of last successful capability execution */
lastExecutionTime?: number
}
/**
* Options for executing a plugin capability.
* Internal type used by PluginHub.
*/
export interface PluginExecutionOptions {
/** Maximum execution time in milliseconds */
timeout?: number
/** Whether to retry on transient failures */
retryOnFailure?: boolean
}
/**
* Result of a plugin capability execution.
* Internal type used by PluginHub and ToolHandler.
*/
export interface PluginExecutionResult {
/** Whether execution succeeded */
success: boolean
/** Result data if successful */
data?: any
/** Error message if failed */
error?: string
/** Execution duration in milliseconds */
duration: number
/** Plugin ID that executed */
pluginId: string
/** Capability name that was executed */
capabilityName: string
}
+6
View File
@@ -131,6 +131,7 @@ export type ClineAsk =
| "auto_approval_max_req_reached"
| "browser_action_launch"
| "use_mcp_server"
| "use_plugin"
| "new_task"
| "condense"
| "summarize_task"
@@ -160,6 +161,11 @@ export type ClineSay =
| "mcp_server_response"
| "mcp_notification"
| "use_mcp_server"
| "plugin_request_started"
| "plugin_response"
| "plugin_notification"
| "plugin_error"
| "use_plugin"
| "diff_error"
| "deleted_api_reqs"
| "clineignore_error"
@@ -22,6 +22,7 @@ function convertClineAskToProtoEnum(ask: AppClineAsk | undefined): ClineAsk | un
auto_approval_max_req_reached: ClineAsk.AUTO_APPROVAL_MAX_REQ_REACHED,
browser_action_launch: ClineAsk.BROWSER_ACTION_LAUNCH,
use_mcp_server: ClineAsk.USE_MCP_SERVER,
use_plugin: ClineAsk.USE_MCP_SERVER, // Reuse MCP enum for now
new_task: ClineAsk.NEW_TASK,
condense: ClineAsk.CONDENSE,
summarize_task: ClineAsk.SUMMARIZE_TASK,
@@ -55,7 +56,7 @@ function convertProtoEnumToClineAsk(ask: ClineAsk): AppClineAsk | undefined {
[ClineAsk.MISTAKE_LIMIT_REACHED]: "mistake_limit_reached",
[ClineAsk.AUTO_APPROVAL_MAX_REQ_REACHED]: "auto_approval_max_req_reached",
[ClineAsk.BROWSER_ACTION_LAUNCH]: "browser_action_launch",
[ClineAsk.USE_MCP_SERVER]: "use_mcp_server",
[ClineAsk.USE_MCP_SERVER]: "use_mcp_server", // Note: use_plugin also maps here
[ClineAsk.NEW_TASK]: "new_task",
[ClineAsk.CONDENSE]: "condense",
[ClineAsk.SUMMARIZE_TASK]: "summarize_task",
@@ -94,6 +95,11 @@ function convertClineSayToProtoEnum(say: AppClineSay | undefined): ClineSay | un
mcp_server_response: ClineSay.MCP_SERVER_RESPONSE,
mcp_notification: ClineSay.MCP_NOTIFICATION,
use_mcp_server: ClineSay.USE_MCP_SERVER_SAY,
plugin_request_started: ClineSay.MCP_SERVER_REQUEST_STARTED, // Reuse MCP enum
plugin_response: ClineSay.MCP_SERVER_RESPONSE, // Reuse MCP enum
plugin_notification: ClineSay.MCP_NOTIFICATION, // Reuse MCP enum
plugin_error: ClineSay.ERROR, // Reuse ERROR enum
use_plugin: ClineSay.USE_MCP_SERVER_SAY, // Reuse MCP enum
diff_error: ClineSay.DIFF_ERROR,
deleted_api_reqs: ClineSay.DELETED_API_REQS,
clineignore_error: ClineSay.CLINEIGNORE_ERROR,
+1
View File
@@ -13,6 +13,7 @@ export enum ClineDefaultTool {
MCP_USE = "use_mcp_tool",
MCP_ACCESS = "access_mcp_resource",
MCP_DOCS = "load_mcp_documentation",
PLUGIN_EXECUTE = "plugin_execute",
NEW_TASK = "new_task",
PLAN_MODE = "plan_mode_respond",
TODO = "focus_chain",
+36
View File
@@ -0,0 +1,36 @@
# Extension-for-Extension Pattern in VS Code
## Core Principle
One extension (the "host") exports an API that other extensions (the "plugins") can import and use. This creates an extensibility ecosystem where your extension becomes a platform.
## Key Components
**1. API Export**
- The host extension's `activate()` function returns an object - this becomes its public API
- Any extension can access this API via `vscode.extensions.getExtension()` and reading the `.exports` property
- The returned API object should be a well-defined TypeScript interface
**2. Discovery Mechanism**
- **Explicit**: Plugin extensions declare `extensionDependencies` in their `package.json` to ensure the host loads first
- **Automatic**: Host scans `vscode.extensions.all` to find compatible plugins by checking their `package.json` metadata (typically in the `contributes` section)
**3. Registration**
- Plugins call a registration method on the host's API (e.g., `registerPlugin()`)
- Host maintains a registry of active plugins and can invoke their functionality as needed
**4. Shared Contract**
- Define TypeScript interfaces for the API in a shared npm package, or document them clearly
- Include version information to handle API evolution
## VS Code APIs Used
- `vscode.extensions.getExtension(id)` - retrieve another extension
- `extension.activate()` - ensure extension is loaded
- `extension.exports` - access the exported API
- `extension.packageJSON` - read metadata for discovery
- `vscode.extensions.onDidChange` - detect newly installed extensions
## Activation Timing
Host should use early activation events (`*` or `onStartupFinished`) so it's available when plugins activate.