claude-code-router-config 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,607 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const chalk = require('chalk');
7
+
8
+ class PluginManager {
9
+ constructor(options = {}) {
10
+ this.pluginsDir = options.pluginsDir || path.join(os.homedir(), '.claude-code-router', 'plugins');
11
+ this.plugins = new Map();
12
+ this.hooks = new Map();
13
+ this.middleware = [];
14
+
15
+ this.initPluginDirectory();
16
+ }
17
+
18
+ initPluginDirectory() {
19
+ if (!fs.existsSync(this.pluginsDir)) {
20
+ fs.mkdirSync(this.pluginsDir, { recursive: true });
21
+ }
22
+ }
23
+
24
+ // Plugin structure validation
25
+ validatePlugin(plugin) {
26
+ const required = ['name', 'version', 'description', 'main'];
27
+ const missing = required.filter(field => !plugin[field]);
28
+
29
+ if (missing.length > 0) {
30
+ throw new Error(`Missing required fields: ${missing.join(', ')}`);
31
+ }
32
+
33
+ // Validate plugin structure
34
+ if (!plugin.provider && !plugin.hooks && !plugin.middleware) {
35
+ throw new Error('Plugin must define at least provider, hooks, or middleware');
36
+ }
37
+
38
+ return true;
39
+ }
40
+
41
+ // Load a plugin from directory or file
42
+ async loadPlugin(pluginPath) {
43
+ try {
44
+ const fullPath = path.isAbsolute(pluginPath)
45
+ ? pluginPath
46
+ : path.join(this.pluginsDir, pluginPath);
47
+
48
+ const pluginJsonPath = path.join(fullPath, 'plugin.json');
49
+
50
+ if (!fs.existsSync(pluginJsonPath)) {
51
+ throw new Error(`Plugin configuration not found: ${pluginJsonPath}`);
52
+ }
53
+
54
+ const pluginConfig = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
55
+ this.validatePlugin(pluginConfig);
56
+
57
+ // Load plugin main file
58
+ const mainPath = path.join(fullPath, pluginConfig.main);
59
+ if (!fs.existsSync(mainPath)) {
60
+ throw new Error(`Plugin main file not found: ${mainPath}`);
61
+ }
62
+
63
+ const PluginClass = require(mainPath);
64
+ const plugin = new PluginClass(pluginConfig);
65
+
66
+ // Initialize plugin
67
+ if (typeof plugin.initialize === 'function') {
68
+ await plugin.initialize();
69
+ }
70
+
71
+ // Register plugin
72
+ this.plugins.set(pluginConfig.name, {
73
+ instance: plugin,
74
+ config: pluginConfig,
75
+ path: fullPath
76
+ });
77
+
78
+ // Register hooks
79
+ if (plugin.hooks) {
80
+ Object.entries(plugin.hooks).forEach(([hookName, handler]) => {
81
+ this.registerHook(hookName, handler, pluginConfig.name);
82
+ });
83
+ }
84
+
85
+ // Register middleware
86
+ if (plugin.middleware) {
87
+ this.middleware.push({
88
+ name: pluginConfig.name,
89
+ handler: plugin.middleware
90
+ });
91
+ }
92
+
93
+ console.log(chalk.green(`✅ Plugin loaded: ${pluginConfig.name} v${pluginConfig.version}`));
94
+ return plugin;
95
+
96
+ } catch (error) {
97
+ console.error(chalk.red(`❌ Failed to load plugin ${pluginPath}:`), error.message);
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ // Load all plugins from directory
103
+ async loadAllPlugins() {
104
+ if (!fs.existsSync(this.pluginsDir)) {
105
+ console.log(chalk.yellow('⚠️ No plugins directory found'));
106
+ return;
107
+ }
108
+
109
+ const pluginDirs = fs.readdirSync(this.pluginsDir)
110
+ .filter(item => {
111
+ const itemPath = path.join(this.pluginsDir, item);
112
+ return fs.statSync(itemPath).isDirectory();
113
+ });
114
+
115
+ console.log(chalk.blue(`Loading ${pluginDirs.length} plugins...`));
116
+
117
+ const results = [];
118
+ for (const dir of pluginDirs) {
119
+ try {
120
+ const plugin = await this.loadPlugin(dir);
121
+ results.push({ success: true, plugin: plugin.config.name });
122
+ } catch (error) {
123
+ results.push({ success: false, plugin: dir, error: error.message });
124
+ }
125
+ }
126
+
127
+ console.log(chalk.green(`\n✅ Successfully loaded ${results.filter(r => r.success).length} plugins`));
128
+ const failed = results.filter(r => !r.success);
129
+ if (failed.length > 0) {
130
+ console.log(chalk.red(`❌ Failed to load ${failed.length} plugins:`));
131
+ failed.forEach(f => {
132
+ console.log(chalk.red(` - ${f.plugin}: ${f.error}`));
133
+ });
134
+ }
135
+
136
+ return results;
137
+ }
138
+
139
+ // Unload a plugin
140
+ async unloadPlugin(pluginName) {
141
+ if (!this.plugins.has(pluginName)) {
142
+ throw new Error(`Plugin not found: ${pluginName}`);
143
+ }
144
+
145
+ const pluginData = this.plugins.get(pluginName);
146
+ const plugin = pluginData.instance;
147
+
148
+ // Cleanup plugin
149
+ if (typeof plugin.cleanup === 'function') {
150
+ await plugin.cleanup();
151
+ }
152
+
153
+ // Remove hooks
154
+ this.removeHooksByPlugin(pluginName);
155
+
156
+ // Remove middleware
157
+ this.middleware = this.middleware.filter(mw => mw.name !== pluginName);
158
+
159
+ // Remove from plugins
160
+ this.plugins.delete(pluginName);
161
+
162
+ console.log(chalk.yellow(`⏏️ Plugin unloaded: ${pluginName}`));
163
+ }
164
+
165
+ // Reload a plugin
166
+ async reloadPlugin(pluginName) {
167
+ if (!this.plugins.has(pluginName)) {
168
+ throw new Error(`Plugin not found: ${pluginName}`);
169
+ }
170
+
171
+ const pluginPath = this.plugins.get(pluginName).path;
172
+ await this.unloadPlugin(pluginName);
173
+ await this.loadPlugin(pluginPath);
174
+ }
175
+
176
+ // Get loaded plugins
177
+ getLoadedPlugins() {
178
+ return Array.from(this.plugins.entries()).map(([name, data]) => ({
179
+ name,
180
+ config: data.config,
181
+ loaded: true
182
+ }));
183
+ }
184
+
185
+ // Get plugin by name
186
+ getPlugin(pluginName) {
187
+ const pluginData = this.plugins.get(pluginName);
188
+ return pluginData ? pluginData.instance : null;
189
+ }
190
+
191
+ // Register hook
192
+ registerHook(hookName, handler, pluginName) {
193
+ if (!this.hooks.has(hookName)) {
194
+ this.hooks.set(hookName, []);
195
+ }
196
+
197
+ this.hooks.get(hookName).push({
198
+ handler,
199
+ plugin: pluginName
200
+ });
201
+ }
202
+
203
+ // Remove hooks by plugin
204
+ removeHooksByPlugin(pluginName) {
205
+ for (const [hookName, hooks] of this.hooks.entries()) {
206
+ this.hooks.set(hookName, hooks.filter(hook => hook.plugin !== pluginName));
207
+ }
208
+ }
209
+
210
+ // Execute hook
211
+ async executeHook(hookName, ...args) {
212
+ if (!this.hooks.has(hookName)) {
213
+ return [];
214
+ }
215
+
216
+ const hooks = this.hooks.get(hookName);
217
+ const results = [];
218
+
219
+ for (const hook of hooks) {
220
+ try {
221
+ const result = await hook.handler(...args);
222
+ results.push({ plugin: hook.plugin, result });
223
+ } catch (error) {
224
+ console.error(chalk.red(`Hook error in ${hookName} (${hook.plugin}):`), error.message);
225
+ results.push({ plugin: hook.plugin, error: error.message });
226
+ }
227
+ }
228
+
229
+ return results;
230
+ }
231
+
232
+ // Apply middleware to request
233
+ async applyMiddleware(req, res, next) {
234
+ let index = 0;
235
+
236
+ const runNext = async () => {
237
+ if (index >= this.middleware.length) {
238
+ return next();
239
+ }
240
+
241
+ const middleware = this.middleware[index++];
242
+ try {
243
+ await middleware.handler(req, res, runNext);
244
+ } catch (error) {
245
+ console.error(chalk.red(`Middleware error (${middleware.name}):`), error.message);
246
+ next(error);
247
+ }
248
+ };
249
+
250
+ await runNext();
251
+ }
252
+
253
+ // Create plugin scaffold
254
+ createPlugin(pluginName, options = {}) {
255
+ const {
256
+ type = 'provider', // provider, hooks, middleware
257
+ author = 'Anonymous',
258
+ description = ''
259
+ } = options;
260
+
261
+ const pluginDir = path.join(this.pluginsDir, pluginName);
262
+
263
+ if (fs.existsSync(pluginDir)) {
264
+ throw new Error(`Plugin directory already exists: ${pluginName}`);
265
+ }
266
+
267
+ fs.mkdirSync(pluginDir, { recursive: true });
268
+
269
+ // Create plugin.json
270
+ const pluginConfig = {
271
+ name: pluginName,
272
+ version: '1.0.0',
273
+ description,
274
+ author,
275
+ type,
276
+ main: 'index.js',
277
+ keywords: [],
278
+ dependencies: {},
279
+ created: new Date().toISOString()
280
+ };
281
+
282
+ fs.writeFileSync(
283
+ path.join(pluginDir, 'plugin.json'),
284
+ JSON.stringify(pluginConfig, null, 2)
285
+ );
286
+
287
+ // Create main file based on type
288
+ let mainContent = '';
289
+
290
+ switch (type) {
291
+ case 'provider':
292
+ mainContent = this.getProviderTemplate(pluginName);
293
+ break;
294
+ case 'hooks':
295
+ mainContent = this.getHooksTemplate(pluginName);
296
+ break;
297
+ case 'middleware':
298
+ mainContent = this.getMiddlewareTemplate(pluginName);
299
+ break;
300
+ }
301
+
302
+ fs.writeFileSync(path.join(pluginDir, 'index.js'), mainContent);
303
+
304
+ // Create README
305
+ const readmeContent = this.getReadmeTemplate(pluginName, pluginConfig);
306
+ fs.writeFileSync(path.join(pluginDir, 'README.md'), readmeContent);
307
+
308
+ console.log(chalk.green(`✅ Plugin scaffold created: ${pluginName}`));
309
+ console.log(chalk.blue(`📁 Location: ${pluginDir}`));
310
+
311
+ return pluginDir;
312
+ }
313
+
314
+ // Plugin templates
315
+ getProviderTemplate(pluginName) {
316
+ return `class ${pluginName}Provider {
317
+ constructor(config) {
318
+ this.name = config.name;
319
+ this.apiBase = config.apiBase || 'https://api.example.com/v1';
320
+ this.models = config.models || ['model-1', 'model-2'];
321
+ this.pricing = config.pricing || { input: 0.01, output: 0.02 };
322
+ }
323
+
324
+ async initialize() {
325
+ console.log(\`Initializing \${this.name} plugin...\`);
326
+ // Initialize connections, validate API keys, etc.
327
+ }
328
+
329
+ async cleanup() {
330
+ console.log(\`Cleaning up \${this.name} plugin...\`);
331
+ // Close connections, cleanup resources
332
+ }
333
+
334
+ // Required methods for provider plugins
335
+ async createRequest(prompt, options = {}) {
336
+ const model = options.model || this.models[0];
337
+ return {
338
+ model,
339
+ messages: [{ role: 'user', content: prompt }],
340
+ max_tokens: options.maxTokens || 1000,
341
+ temperature: options.temperature || 0.7
342
+ };
343
+ }
344
+
345
+ async parseResponse(response) {
346
+ return response;
347
+ }
348
+
349
+ // Optional: Custom methods
350
+ async checkHealth() {
351
+ // Check if the provider is accessible
352
+ return true;
353
+ }
354
+
355
+ getCapabilities() {
356
+ return {
357
+ chat: true,
358
+ tools: false,
359
+ vision: false
360
+ };
361
+ }
362
+ }
363
+
364
+ module.exports = ${pluginName}Provider;
365
+ `;
366
+ }
367
+
368
+ getHooksTemplate(pluginName) {
369
+ return `class ${pluginName}Hooks {
370
+ constructor(config) {
371
+ this.name = config.name;
372
+ }
373
+
374
+ // Plugin initialization
375
+ async initialize() {
376
+ console.log(\`Initializing \${this.name} hooks...\`);
377
+ }
378
+
379
+ // Plugin cleanup
380
+ async cleanup() {
381
+ console.log(\`Cleaning up \${this.name} hooks...\`);
382
+ }
383
+
384
+ // Define hooks
385
+ get hooks() {
386
+ return {
387
+ // Called before request
388
+ 'beforeRequest': async (req, config) => {
389
+ console.log(\`Before request hook: \${req.method || 'Unknown'}\`);
390
+ return req;
391
+ },
392
+
393
+ // Called after request
394
+ 'afterRequest': async (req, response, latency) => {
395
+ console.log(\`After request hook: \${latency}ms\`);
396
+ return { req, response, latency };
397
+ },
398
+
399
+ // Called on error
400
+ 'onError': async (req, error) => {
401
+ console.error(\`Error hook: \${error.message}\`);
402
+ return error;
403
+ }
404
+ };
405
+ }
406
+ }
407
+
408
+ module.exports = ${pluginName}Hooks;
409
+ `;
410
+ }
411
+
412
+ getMiddlewareTemplate(pluginName) {
413
+ return `class ${pluginName}Middleware {
414
+ constructor(config) {
415
+ this.name = config.name;
416
+ this.options = config.options || {};
417
+ }
418
+
419
+ async initialize() {
420
+ console.log(\`Initializing \${this.name} middleware...\`);
421
+ }
422
+
423
+ async cleanup() {
424
+ console.log(\`Cleaning up \${this.name} middleware...\`);
425
+ }
426
+
427
+ // Middleware function
428
+ async middleware(req, res, next) {
429
+ // Add custom headers, logging, rate limiting, etc.
430
+ console.log(\`\${this.name} middleware processing request\`);
431
+
432
+ // Continue to next middleware
433
+ await next();
434
+ }
435
+ }
436
+
437
+ module.exports = ${pluginName}Middleware;
438
+ `;
439
+ }
440
+
441
+ getReadmeTemplate(pluginName, config) {
442
+ return `# ${pluginName} Plugin
443
+
444
+ ${config.description}
445
+
446
+ ## Installation
447
+
448
+ 1. Place this plugin in your Claude Code Router plugins directory
449
+ 2. Restart Claude Code Router
450
+ 3. The plugin will be automatically loaded
451
+
452
+ ## Configuration
453
+
454
+ Edit \`plugin.json\` to customize the plugin settings.
455
+
456
+ ## Usage
457
+
458
+ This plugin provides:
459
+ - Type: ${config.type}
460
+ - Version: ${config.version}
461
+ - Author: ${config.author}
462
+
463
+ ## Development
464
+
465
+ Modify \`index.js\` to extend the plugin functionality.
466
+
467
+ ## Support
468
+
469
+ For issues or questions, contact: ${config.author}
470
+ `;
471
+ }
472
+ }
473
+
474
+ // CLI interface
475
+ async function main() {
476
+ const args = process.argv.slice(2);
477
+ const command = args[0];
478
+
479
+ const manager = new PluginManager();
480
+
481
+ switch (command) {
482
+ case 'list':
483
+ const plugins = manager.getLoadedPlugins();
484
+ if (plugins.length === 0) {
485
+ console.log(chalk.yellow('No plugins loaded'));
486
+ } else {
487
+ console.log(chalk.blue('\nLoaded Plugins:'));
488
+ plugins.forEach(plugin => {
489
+ console.log(` 📦 ${plugin.name} v${plugin.config.version}`);
490
+ console.log(` ${plugin.config.description}`);
491
+ });
492
+ }
493
+ break;
494
+
495
+ case 'load':
496
+ const pluginName = args[1];
497
+ if (!pluginName) {
498
+ console.error(chalk.red('Please specify plugin name'));
499
+ process.exit(1);
500
+ }
501
+ try {
502
+ await manager.loadPlugin(pluginName);
503
+ } catch (error) {
504
+ console.error(chalk.red('Failed to load plugin:'), error.message);
505
+ process.exit(1);
506
+ }
507
+ break;
508
+
509
+ case 'unload':
510
+ const unloadName = args[1];
511
+ if (!unloadName) {
512
+ console.error(chalk.red('Please specify plugin name'));
513
+ process.exit(1);
514
+ }
515
+ try {
516
+ await manager.unloadPlugin(unloadName);
517
+ } catch (error) {
518
+ console.error(chalk.red('Failed to unload plugin:'), error.message);
519
+ process.exit(1);
520
+ }
521
+ break;
522
+
523
+ case 'reload':
524
+ const reloadName = args[1];
525
+ if (!reloadName) {
526
+ console.error(chalk.red('Please specify plugin name'));
527
+ process.exit(1);
528
+ }
529
+ try {
530
+ await manager.reloadPlugin(reloadName);
531
+ } catch (error) {
532
+ console.error(chalk.red('Failed to reload plugin:'), error.message);
533
+ process.exit(1);
534
+ }
535
+ break;
536
+
537
+ case 'create':
538
+ const createName = args[1];
539
+ const options = {};
540
+
541
+ // Parse options
542
+ const typeIndex = args.indexOf('--type');
543
+ if (typeIndex !== -1) {
544
+ options.type = args[typeIndex + 1];
545
+ }
546
+
547
+ const authorIndex = args.indexOf('--author');
548
+ if (authorIndex !== -1) {
549
+ options.author = args[authorIndex + 1];
550
+ }
551
+
552
+ const descIndex = args.indexOf('--description');
553
+ if (descIndex !== -1) {
554
+ options.description = args.slice(descIndex + 1).join(' ');
555
+ }
556
+
557
+ if (!createName) {
558
+ console.error(chalk.red('Please specify plugin name'));
559
+ console.log(chalk.blue('\nUsage: ccr plugin create <name> [--type provider|hooks|middleware] [--author <author>] [--description <description>]'));
560
+ process.exit(1);
561
+ }
562
+
563
+ try {
564
+ manager.createPlugin(createName, options);
565
+ } catch (error) {
566
+ console.error(chalk.red('Failed to create plugin:'), error.message);
567
+ process.exit(1);
568
+ }
569
+ break;
570
+
571
+ case 'load-all':
572
+ await manager.loadAllPlugins();
573
+ break;
574
+
575
+ default:
576
+ console.log(chalk.blue('Claude Code Router - Plugin Manager'));
577
+ console.log(chalk.gray('─'.repeat(40)));
578
+ console.log(chalk.yellow('Available commands:'));
579
+ console.log('');
580
+ console.log('Plugin Management:');
581
+ console.log(' ccr plugin list - List loaded plugins');
582
+ console.log(' ccr plugin load <name> - Load a plugin');
583
+ console.log(' ccr plugin unload <name> - Unload a plugin');
584
+ console.log(' ccr plugin reload <name> - Reload a plugin');
585
+ console.log(' ccr plugin create <name> [options] - Create plugin scaffold');
586
+ console.log(' ccr plugin load-all - Load all plugins');
587
+ console.log('');
588
+ console.log('Plugin Creation Options:');
589
+ console.log(' --type <provider|hooks|middleware> Plugin type');
590
+ console.log(' --author <name> Author name');
591
+ console.log(' --description <text> Plugin description');
592
+ console.log('');
593
+ console.log('Examples:');
594
+ console.log(' ccr plugin create my-provider --type provider');
595
+ console.log(' ccr plugin create logger --type middleware --author "John Doe"');
596
+ }
597
+ }
598
+
599
+ // Export for use in other modules
600
+ module.exports = {
601
+ PluginManager
602
+ };
603
+
604
+ // Run CLI if called directly
605
+ if (require.main === module) {
606
+ main().catch(console.error);
607
+ }