elsabro 2.2.0 → 3.7.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.
Files changed (88) hide show
  1. package/README.md +668 -20
  2. package/agents/elsabro-orchestrator.md +113 -0
  3. package/bin/install.js +0 -0
  4. package/commands/elsabro/execute.md +223 -46
  5. package/commands/elsabro/start.md +34 -0
  6. package/commands/elsabro/verify-work.md +29 -0
  7. package/flows/development-flow.json +452 -0
  8. package/flows/quick-flow.json +118 -0
  9. package/hooks/confirm-destructive.sh +145 -0
  10. package/hooks/hooks-config.json +81 -0
  11. package/hooks/lint-check.sh +238 -0
  12. package/hooks/post-edit-test.sh +189 -0
  13. package/package.json +5 -3
  14. package/references/SYSTEM_INDEX.md +379 -5
  15. package/references/agent-marketplace.md +2274 -0
  16. package/references/agent-protocol.md +1126 -0
  17. package/references/ai-code-suggestions.md +2413 -0
  18. package/references/checkpointing.md +595 -0
  19. package/references/collaboration-patterns.md +851 -0
  20. package/references/collaborative-sessions.md +1081 -0
  21. package/references/configuration-management.md +1810 -0
  22. package/references/cost-tracking.md +1095 -0
  23. package/references/enterprise-sso.md +2001 -0
  24. package/references/error-contracts-tests.md +1171 -0
  25. package/references/error-contracts-v2.md +968 -0
  26. package/references/error-contracts.md +3102 -0
  27. package/references/event-driven.md +1031 -0
  28. package/references/flow-orchestration.md +940 -0
  29. package/references/flow-visualization.md +1557 -0
  30. package/references/ide-integrations.md +3513 -0
  31. package/references/interrupt-system.md +681 -0
  32. package/references/kubernetes-deployment.md +3099 -0
  33. package/references/memory-system.md +683 -0
  34. package/references/mobile-companion.md +3236 -0
  35. package/references/multi-llm-providers.md +2494 -0
  36. package/references/multi-project-memory.md +1182 -0
  37. package/references/observability.md +793 -0
  38. package/references/output-schemas.md +858 -0
  39. package/references/parallel-worktrees.md +293 -0
  40. package/references/performance-profiler.md +955 -0
  41. package/references/plugin-system.md +1526 -0
  42. package/references/prompt-management.md +292 -0
  43. package/references/sandbox-execution.md +303 -0
  44. package/references/security-system.md +1253 -0
  45. package/references/streaming.md +696 -0
  46. package/references/testing-framework.md +1151 -0
  47. package/references/time-travel.md +802 -0
  48. package/references/tool-registry.md +886 -0
  49. package/references/voice-commands.md +3296 -0
  50. package/scripts/setup-parallel-worktrees.sh +319 -0
  51. package/skills/memory-update.md +207 -0
  52. package/skills/review.md +331 -0
  53. package/skills/techdebt.md +289 -0
  54. package/skills/tutor.md +219 -0
  55. package/templates/.planning/notes/.gitkeep +0 -0
  56. package/templates/CLAUDE.md.template +48 -0
  57. package/templates/agent-marketplace-config.json +220 -0
  58. package/templates/agent-protocol-config.json +136 -0
  59. package/templates/ai-suggestions-config.json +100 -0
  60. package/templates/checkpoint-state.json +61 -0
  61. package/templates/collaboration-config.json +157 -0
  62. package/templates/collaborative-sessions-config.json +153 -0
  63. package/templates/configuration-config.json +245 -0
  64. package/templates/cost-tracking-config.json +148 -0
  65. package/templates/enterprise-sso-config.json +438 -0
  66. package/templates/error-handling-config.json +79 -2
  67. package/templates/events-config.json +148 -0
  68. package/templates/flow-visualization-config.json +196 -0
  69. package/templates/ide-integrations-config.json +442 -0
  70. package/templates/kubernetes-config.json +764 -0
  71. package/templates/memory-state.json +84 -0
  72. package/templates/mistakes.md.template +52 -0
  73. package/templates/mobile-companion-config.json +600 -0
  74. package/templates/multi-llm-config.json +544 -0
  75. package/templates/multi-project-memory-config.json +145 -0
  76. package/templates/observability-config.json +109 -0
  77. package/templates/patterns.md.template +114 -0
  78. package/templates/performance-profiler-config.json +125 -0
  79. package/templates/plugin-config.json +170 -0
  80. package/templates/prompt-management-config.json +86 -0
  81. package/templates/sandbox-config.json +185 -0
  82. package/templates/schemas-config.json +65 -0
  83. package/templates/security-config.json +120 -0
  84. package/templates/streaming-config.json +72 -0
  85. package/templates/testing-config.json +81 -0
  86. package/templates/timetravel-config.json +62 -0
  87. package/templates/tool-registry-config.json +109 -0
  88. package/templates/voice-commands-config.json +658 -0
@@ -0,0 +1,1526 @@
1
+ # ELSABRO Plugin System
2
+
3
+ > Sistema de plugins extensible con carga dinámica, aislamiento y gestión de lifecycle.
4
+
5
+ ## Arquitectura General
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────────────────┐
9
+ │ Plugin System │
10
+ ├─────────────────────────────────────────────────────────────────────────┤
11
+ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
12
+ │ │ PluginLoader │ │ PluginRegistry │ │ PluginManager │ │
13
+ │ │ ───────────── │ │ ───────────── │ │ ───────────── │ │
14
+ │ │ • Discovery │ │ • Registration │ │ • Lifecycle │ │
15
+ │ │ • Validation │ │ • Dependencies │ │ • Hot-reload │ │
16
+ │ │ • Isolation │ │ • Versioning │ │ • Health │ │
17
+ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
18
+ │ │ │
19
+ │ ┌───────────────────────────┴───────────────────────────────┐ │
20
+ │ │ HookSystem │ │
21
+ │ │ • Before/After hooks • Middleware • Event propagation │ │
22
+ │ └────────────────────────────────────────────────────────────┘ │
23
+ │ │ │
24
+ │ ┌───────────────────────────┴───────────────────────────────┐ │
25
+ │ │ PluginContext │ │
26
+ │ │ • Sandboxed APIs • Resource limits • Permissions │ │
27
+ │ └────────────────────────────────────────────────────────────┘ │
28
+ └─────────────────────────────────────────────────────────────────────────┘
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 1. Plugin Definition
34
+
35
+ ### Estructura de Plugin
36
+
37
+ ```typescript
38
+ interface PluginManifest {
39
+ // Identity
40
+ name: string; // Unique identifier (e.g., "elsabro-git-plugin")
41
+ version: string; // Semver (e.g., "1.2.3")
42
+ displayName: string; // Human-readable name
43
+ description: string;
44
+ author: string | { name: string; email?: string; url?: string };
45
+ license: string;
46
+
47
+ // Entry points
48
+ main: string; // Main entry file
49
+ types?: string; // TypeScript definitions
50
+
51
+ // Dependencies
52
+ engines: {
53
+ elsabro: string; // Required ELSABRO version (semver range)
54
+ node?: string; // Node.js version
55
+ };
56
+ dependencies?: Record<string, string>; // npm packages
57
+ pluginDependencies?: Record<string, string>; // Other ELSABRO plugins
58
+
59
+ // Capabilities
60
+ contributes?: {
61
+ agents?: AgentContribution[];
62
+ tools?: ToolContribution[];
63
+ commands?: CommandContribution[];
64
+ hooks?: HookContribution[];
65
+ config?: ConfigContribution;
66
+ views?: ViewContribution[];
67
+ };
68
+
69
+ // Permissions
70
+ permissions?: PluginPermission[];
71
+
72
+ // Activation
73
+ activationEvents?: string[]; // When to activate (e.g., "onCommand:myCmd")
74
+ }
75
+
76
+ type PluginPermission =
77
+ | 'filesystem:read'
78
+ | 'filesystem:write'
79
+ | 'network'
80
+ | 'shell'
81
+ | 'env'
82
+ | 'secrets'
83
+ | 'config:read'
84
+ | 'config:write';
85
+ ```
86
+
87
+ ### Plugin Interface
88
+
89
+ ```typescript
90
+ interface Plugin {
91
+ // Lifecycle
92
+ activate(context: PluginContext): Promise<void>;
93
+ deactivate?(): Promise<void>;
94
+
95
+ // Optional exports
96
+ api?: Record<string, unknown>; // Public API for other plugins
97
+ }
98
+
99
+ interface PluginContext {
100
+ // Plugin info
101
+ readonly pluginPath: string;
102
+ readonly manifest: PluginManifest;
103
+
104
+ // Storage
105
+ readonly globalState: Memento;
106
+ readonly workspaceState: Memento;
107
+ readonly secrets: SecretsStorage;
108
+
109
+ // Subscriptions (auto-disposed on deactivate)
110
+ subscriptions: Disposable[];
111
+
112
+ // Logging
113
+ readonly logger: Logger;
114
+
115
+ // Extension APIs
116
+ readonly elsabro: ElsabroAPI;
117
+ }
118
+
119
+ interface Memento {
120
+ get<T>(key: string, defaultValue?: T): T;
121
+ update(key: string, value: unknown): Promise<void>;
122
+ keys(): readonly string[];
123
+ }
124
+
125
+ interface Disposable {
126
+ dispose(): void;
127
+ }
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 2. PluginLoader
133
+
134
+ ### Propósito
135
+ Descubre, valida y carga plugins desde múltiples fuentes.
136
+
137
+ ### Interfaz
138
+
139
+ ```typescript
140
+ interface PluginSource {
141
+ type: 'directory' | 'npm' | 'url';
142
+ path?: string;
143
+ package?: string;
144
+ url?: string;
145
+ }
146
+
147
+ interface LoadedPlugin {
148
+ manifest: PluginManifest;
149
+ module: Plugin;
150
+ path: string;
151
+ state: PluginState;
152
+ }
153
+
154
+ type PluginState =
155
+ | 'discovered'
156
+ | 'validated'
157
+ | 'loaded'
158
+ | 'activated'
159
+ | 'deactivated'
160
+ | 'error';
161
+
162
+ interface PluginLoader {
163
+ // Discovery
164
+ discover(sources: PluginSource[]): Promise<PluginManifest[]>;
165
+ discoverFromDirectory(path: string): Promise<PluginManifest[]>;
166
+
167
+ // Validation
168
+ validate(manifest: PluginManifest): ValidationResult;
169
+ checkDependencies(manifest: PluginManifest): DependencyResult;
170
+
171
+ // Loading
172
+ load(manifest: PluginManifest): Promise<LoadedPlugin>;
173
+ loadAll(manifests: PluginManifest[]): Promise<LoadedPlugin[]>;
174
+
175
+ // Isolation
176
+ createSandbox(plugin: LoadedPlugin): PluginSandbox;
177
+ }
178
+ ```
179
+
180
+ ### Implementación
181
+
182
+ ```typescript
183
+ class PluginLoaderImpl implements PluginLoader {
184
+ private loadedPlugins: Map<string, LoadedPlugin> = new Map();
185
+
186
+ constructor(
187
+ private config: ConfigManager,
188
+ private registry: PluginRegistry
189
+ ) {}
190
+
191
+ async discover(sources: PluginSource[]): Promise<PluginManifest[]> {
192
+ const manifests: PluginManifest[] = [];
193
+
194
+ for (const source of sources) {
195
+ try {
196
+ switch (source.type) {
197
+ case 'directory':
198
+ manifests.push(...await this.discoverFromDirectory(source.path!));
199
+ break;
200
+ case 'npm':
201
+ manifests.push(await this.discoverFromNpm(source.package!));
202
+ break;
203
+ case 'url':
204
+ manifests.push(await this.discoverFromUrl(source.url!));
205
+ break;
206
+ }
207
+ } catch (error) {
208
+ console.warn(`Failed to discover plugins from ${source.type}:`, error);
209
+ }
210
+ }
211
+
212
+ return manifests;
213
+ }
214
+
215
+ async discoverFromDirectory(dirPath: string): Promise<PluginManifest[]> {
216
+ const manifests: PluginManifest[] = [];
217
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
218
+
219
+ for (const entry of entries) {
220
+ if (!entry.isDirectory()) continue;
221
+
222
+ const pluginPath = path.join(dirPath, entry.name);
223
+ const manifestPath = path.join(pluginPath, 'plugin.json');
224
+
225
+ if (await this.fileExists(manifestPath)) {
226
+ try {
227
+ const content = await fs.readFile(manifestPath, 'utf-8');
228
+ const manifest = JSON.parse(content) as PluginManifest;
229
+ manifest._path = pluginPath;
230
+ manifests.push(manifest);
231
+ } catch (error) {
232
+ console.warn(`Invalid plugin manifest at ${manifestPath}:`, error);
233
+ }
234
+ }
235
+ }
236
+
237
+ return manifests;
238
+ }
239
+
240
+ private async discoverFromNpm(packageName: string): Promise<PluginManifest> {
241
+ const packagePath = require.resolve(`${packageName}/plugin.json`);
242
+ const content = await fs.readFile(packagePath, 'utf-8');
243
+ const manifest = JSON.parse(content);
244
+ manifest._path = path.dirname(packagePath);
245
+ return manifest;
246
+ }
247
+
248
+ validate(manifest: PluginManifest): ValidationResult {
249
+ const errors: string[] = [];
250
+ const warnings: string[] = [];
251
+
252
+ // Required fields
253
+ if (!manifest.name) errors.push('Missing required field: name');
254
+ if (!manifest.version) errors.push('Missing required field: version');
255
+ if (!manifest.main) errors.push('Missing required field: main');
256
+
257
+ // Version format
258
+ if (manifest.version && !semver.valid(manifest.version)) {
259
+ errors.push(`Invalid version format: ${manifest.version}`);
260
+ }
261
+
262
+ // Engine compatibility
263
+ if (manifest.engines?.elsabro) {
264
+ const elsabroVersion = this.config.get<string>('version');
265
+ if (!semver.satisfies(elsabroVersion, manifest.engines.elsabro)) {
266
+ errors.push(
267
+ `Incompatible ELSABRO version. Required: ${manifest.engines.elsabro}, Current: ${elsabroVersion}`
268
+ );
269
+ }
270
+ }
271
+
272
+ // Permissions validation
273
+ if (manifest.permissions) {
274
+ const validPermissions = [
275
+ 'filesystem:read', 'filesystem:write', 'network',
276
+ 'shell', 'env', 'secrets', 'config:read', 'config:write'
277
+ ];
278
+ for (const perm of manifest.permissions) {
279
+ if (!validPermissions.includes(perm)) {
280
+ warnings.push(`Unknown permission: ${perm}`);
281
+ }
282
+ }
283
+ }
284
+
285
+ return {
286
+ valid: errors.length === 0,
287
+ errors,
288
+ warnings
289
+ };
290
+ }
291
+
292
+ checkDependencies(manifest: PluginManifest): DependencyResult {
293
+ const missing: string[] = [];
294
+ const incompatible: Array<{ name: string; required: string; installed: string }> = [];
295
+
296
+ // Check plugin dependencies
297
+ if (manifest.pluginDependencies) {
298
+ for (const [name, versionRange] of Object.entries(manifest.pluginDependencies)) {
299
+ const installed = this.registry.get(name);
300
+ if (!installed) {
301
+ missing.push(name);
302
+ } else if (!semver.satisfies(installed.manifest.version, versionRange)) {
303
+ incompatible.push({
304
+ name,
305
+ required: versionRange,
306
+ installed: installed.manifest.version
307
+ });
308
+ }
309
+ }
310
+ }
311
+
312
+ return {
313
+ satisfied: missing.length === 0 && incompatible.length === 0,
314
+ missing,
315
+ incompatible
316
+ };
317
+ }
318
+
319
+ async load(manifest: PluginManifest): Promise<LoadedPlugin> {
320
+ const validation = this.validate(manifest);
321
+ if (!validation.valid) {
322
+ throw new PluginLoadError(manifest.name, validation.errors);
323
+ }
324
+
325
+ const depCheck = this.checkDependencies(manifest);
326
+ if (!depCheck.satisfied) {
327
+ throw new PluginDependencyError(manifest.name, depCheck);
328
+ }
329
+
330
+ // Load the plugin module
331
+ const mainPath = path.join(manifest._path!, manifest.main);
332
+ const module = await import(mainPath);
333
+
334
+ const plugin: LoadedPlugin = {
335
+ manifest,
336
+ module: module.default || module,
337
+ path: manifest._path!,
338
+ state: 'loaded'
339
+ };
340
+
341
+ this.loadedPlugins.set(manifest.name, plugin);
342
+ return plugin;
343
+ }
344
+
345
+ async loadAll(manifests: PluginManifest[]): Promise<LoadedPlugin[]> {
346
+ // Sort by dependencies
347
+ const sorted = this.topologicalSort(manifests);
348
+
349
+ const loaded: LoadedPlugin[] = [];
350
+ for (const manifest of sorted) {
351
+ try {
352
+ const plugin = await this.load(manifest);
353
+ loaded.push(plugin);
354
+ } catch (error) {
355
+ console.error(`Failed to load plugin ${manifest.name}:`, error);
356
+ }
357
+ }
358
+
359
+ return loaded;
360
+ }
361
+
362
+ private topologicalSort(manifests: PluginManifest[]): PluginManifest[] {
363
+ const graph = new Map<string, Set<string>>();
364
+ const manifestMap = new Map<string, PluginManifest>();
365
+
366
+ // Build dependency graph
367
+ for (const manifest of manifests) {
368
+ manifestMap.set(manifest.name, manifest);
369
+ graph.set(manifest.name, new Set(
370
+ Object.keys(manifest.pluginDependencies || {})
371
+ ));
372
+ }
373
+
374
+ // Kahn's algorithm
375
+ const result: PluginManifest[] = [];
376
+ const noDeps = manifests.filter(m => (graph.get(m.name)?.size || 0) === 0);
377
+
378
+ while (noDeps.length > 0) {
379
+ const current = noDeps.shift()!;
380
+ result.push(current);
381
+
382
+ for (const [name, deps] of graph) {
383
+ if (deps.has(current.name)) {
384
+ deps.delete(current.name);
385
+ if (deps.size === 0) {
386
+ noDeps.push(manifestMap.get(name)!);
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ // Check for cycles
393
+ if (result.length !== manifests.length) {
394
+ throw new Error('Circular dependency detected in plugins');
395
+ }
396
+
397
+ return result;
398
+ }
399
+
400
+ createSandbox(plugin: LoadedPlugin): PluginSandbox {
401
+ return new PluginSandbox(plugin, {
402
+ permissions: plugin.manifest.permissions || [],
403
+ resourceLimits: this.config.get('plugins.resourceLimits', {
404
+ memory: 128 * 1024 * 1024, // 128MB
405
+ cpu: 0.5
406
+ })
407
+ });
408
+ }
409
+
410
+ private async fileExists(path: string): Promise<boolean> {
411
+ try {
412
+ await fs.access(path);
413
+ return true;
414
+ } catch {
415
+ return false;
416
+ }
417
+ }
418
+ }
419
+ ```
420
+
421
+ ---
422
+
423
+ ## 3. PluginRegistry
424
+
425
+ ### Propósito
426
+ Mantiene el registro de plugins instalados y sus contribuciones.
427
+
428
+ ### Interfaz
429
+
430
+ ```typescript
431
+ interface PluginRegistry {
432
+ // Registration
433
+ register(plugin: LoadedPlugin): void;
434
+ unregister(name: string): void;
435
+
436
+ // Queries
437
+ get(name: string): LoadedPlugin | undefined;
438
+ getAll(): LoadedPlugin[];
439
+ getByState(state: PluginState): LoadedPlugin[];
440
+ find(predicate: (plugin: LoadedPlugin) => boolean): LoadedPlugin[];
441
+
442
+ // Contributions
443
+ getAgents(): AgentContribution[];
444
+ getTools(): ToolContribution[];
445
+ getCommands(): CommandContribution[];
446
+ getHooks(point: string): HookContribution[];
447
+
448
+ // Dependencies
449
+ getDependents(name: string): LoadedPlugin[];
450
+ getDependencies(name: string): LoadedPlugin[];
451
+
452
+ // Events
453
+ onRegister(callback: (plugin: LoadedPlugin) => void): Disposable;
454
+ onUnregister(callback: (name: string) => void): Disposable;
455
+ }
456
+
457
+ interface AgentContribution {
458
+ pluginName: string;
459
+ name: string;
460
+ displayName: string;
461
+ description: string;
462
+ model?: string;
463
+ systemPrompt?: string;
464
+ tools?: string[];
465
+ }
466
+
467
+ interface ToolContribution {
468
+ pluginName: string;
469
+ name: string;
470
+ displayName: string;
471
+ description: string;
472
+ inputSchema: Record<string, unknown>;
473
+ handler: string; // Reference to handler function
474
+ }
475
+
476
+ interface CommandContribution {
477
+ pluginName: string;
478
+ command: string;
479
+ title: string;
480
+ description?: string;
481
+ category?: string;
482
+ handler: string;
483
+ }
484
+
485
+ interface HookContribution {
486
+ pluginName: string;
487
+ point: string;
488
+ priority: number;
489
+ handler: string;
490
+ }
491
+ ```
492
+
493
+ ### Implementación
494
+
495
+ ```typescript
496
+ class PluginRegistryImpl implements PluginRegistry {
497
+ private plugins: Map<string, LoadedPlugin> = new Map();
498
+ private contributions = {
499
+ agents: new Map<string, AgentContribution>(),
500
+ tools: new Map<string, ToolContribution>(),
501
+ commands: new Map<string, CommandContribution>(),
502
+ hooks: new Map<string, HookContribution[]>()
503
+ };
504
+
505
+ private registerCallbacks: Set<Function> = new Set();
506
+ private unregisterCallbacks: Set<Function> = new Set();
507
+
508
+ register(plugin: LoadedPlugin): void {
509
+ const { manifest } = plugin;
510
+
511
+ // Check for duplicates
512
+ if (this.plugins.has(manifest.name)) {
513
+ throw new Error(`Plugin already registered: ${manifest.name}`);
514
+ }
515
+
516
+ this.plugins.set(manifest.name, plugin);
517
+
518
+ // Register contributions
519
+ if (manifest.contributes) {
520
+ this.registerContributions(manifest.name, manifest.contributes);
521
+ }
522
+
523
+ // Notify listeners
524
+ this.registerCallbacks.forEach(cb => cb(plugin));
525
+ }
526
+
527
+ unregister(name: string): void {
528
+ const plugin = this.plugins.get(name);
529
+ if (!plugin) return;
530
+
531
+ // Check for dependents
532
+ const dependents = this.getDependents(name);
533
+ if (dependents.length > 0) {
534
+ throw new Error(
535
+ `Cannot unregister ${name}: required by ${dependents.map(p => p.manifest.name).join(', ')}`
536
+ );
537
+ }
538
+
539
+ // Remove contributions
540
+ this.removeContributions(name);
541
+
542
+ this.plugins.delete(name);
543
+ this.unregisterCallbacks.forEach(cb => cb(name));
544
+ }
545
+
546
+ private registerContributions(pluginName: string, contributes: PluginManifest['contributes']): void {
547
+ if (contributes.agents) {
548
+ for (const agent of contributes.agents) {
549
+ this.contributions.agents.set(agent.name, { ...agent, pluginName });
550
+ }
551
+ }
552
+
553
+ if (contributes.tools) {
554
+ for (const tool of contributes.tools) {
555
+ this.contributions.tools.set(tool.name, { ...tool, pluginName });
556
+ }
557
+ }
558
+
559
+ if (contributes.commands) {
560
+ for (const cmd of contributes.commands) {
561
+ this.contributions.commands.set(cmd.command, { ...cmd, pluginName });
562
+ }
563
+ }
564
+
565
+ if (contributes.hooks) {
566
+ for (const hook of contributes.hooks) {
567
+ const hookWithPlugin = { ...hook, pluginName };
568
+ if (!this.contributions.hooks.has(hook.point)) {
569
+ this.contributions.hooks.set(hook.point, []);
570
+ }
571
+ this.contributions.hooks.get(hook.point)!.push(hookWithPlugin);
572
+ // Sort by priority
573
+ this.contributions.hooks.get(hook.point)!.sort((a, b) => a.priority - b.priority);
574
+ }
575
+ }
576
+ }
577
+
578
+ private removeContributions(pluginName: string): void {
579
+ // Remove agents
580
+ for (const [name, agent] of this.contributions.agents) {
581
+ if (agent.pluginName === pluginName) {
582
+ this.contributions.agents.delete(name);
583
+ }
584
+ }
585
+
586
+ // Remove tools
587
+ for (const [name, tool] of this.contributions.tools) {
588
+ if (tool.pluginName === pluginName) {
589
+ this.contributions.tools.delete(name);
590
+ }
591
+ }
592
+
593
+ // Remove commands
594
+ for (const [name, cmd] of this.contributions.commands) {
595
+ if (cmd.pluginName === pluginName) {
596
+ this.contributions.commands.delete(name);
597
+ }
598
+ }
599
+
600
+ // Remove hooks
601
+ for (const [point, hooks] of this.contributions.hooks) {
602
+ this.contributions.hooks.set(
603
+ point,
604
+ hooks.filter(h => h.pluginName !== pluginName)
605
+ );
606
+ }
607
+ }
608
+
609
+ get(name: string): LoadedPlugin | undefined {
610
+ return this.plugins.get(name);
611
+ }
612
+
613
+ getAll(): LoadedPlugin[] {
614
+ return Array.from(this.plugins.values());
615
+ }
616
+
617
+ getByState(state: PluginState): LoadedPlugin[] {
618
+ return this.getAll().filter(p => p.state === state);
619
+ }
620
+
621
+ find(predicate: (plugin: LoadedPlugin) => boolean): LoadedPlugin[] {
622
+ return this.getAll().filter(predicate);
623
+ }
624
+
625
+ getAgents(): AgentContribution[] {
626
+ return Array.from(this.contributions.agents.values());
627
+ }
628
+
629
+ getTools(): ToolContribution[] {
630
+ return Array.from(this.contributions.tools.values());
631
+ }
632
+
633
+ getCommands(): CommandContribution[] {
634
+ return Array.from(this.contributions.commands.values());
635
+ }
636
+
637
+ getHooks(point: string): HookContribution[] {
638
+ return this.contributions.hooks.get(point) || [];
639
+ }
640
+
641
+ getDependents(name: string): LoadedPlugin[] {
642
+ return this.getAll().filter(plugin => {
643
+ const deps = plugin.manifest.pluginDependencies || {};
644
+ return name in deps;
645
+ });
646
+ }
647
+
648
+ getDependencies(name: string): LoadedPlugin[] {
649
+ const plugin = this.plugins.get(name);
650
+ if (!plugin) return [];
651
+
652
+ const deps = plugin.manifest.pluginDependencies || {};
653
+ return Object.keys(deps)
654
+ .map(depName => this.plugins.get(depName))
655
+ .filter((p): p is LoadedPlugin => p !== undefined);
656
+ }
657
+
658
+ onRegister(callback: Function): Disposable {
659
+ this.registerCallbacks.add(callback);
660
+ return { dispose: () => this.registerCallbacks.delete(callback) };
661
+ }
662
+
663
+ onUnregister(callback: Function): Disposable {
664
+ this.unregisterCallbacks.add(callback);
665
+ return { dispose: () => this.unregisterCallbacks.delete(callback) };
666
+ }
667
+ }
668
+ ```
669
+
670
+ ---
671
+
672
+ ## 4. PluginManager
673
+
674
+ ### Propósito
675
+ Gestiona el lifecycle completo de plugins incluyendo activación, desactivación y hot-reload.
676
+
677
+ ### Interfaz
678
+
679
+ ```typescript
680
+ interface PluginManager {
681
+ // Lifecycle
682
+ activate(name: string): Promise<void>;
683
+ activateAll(): Promise<void>;
684
+ deactivate(name: string): Promise<void>;
685
+ deactivateAll(): Promise<void>;
686
+ reload(name: string): Promise<void>;
687
+
688
+ // Installation
689
+ install(source: PluginSource): Promise<LoadedPlugin>;
690
+ uninstall(name: string): Promise<void>;
691
+ update(name: string): Promise<LoadedPlugin>;
692
+
693
+ // Status
694
+ getStatus(name: string): PluginStatus;
695
+ getAllStatus(): Map<string, PluginStatus>;
696
+ isActive(name: string): boolean;
697
+
698
+ // Hot-reload
699
+ enableHotReload(name: string): void;
700
+ disableHotReload(name: string): void;
701
+
702
+ // Events
703
+ onActivate(callback: (plugin: LoadedPlugin) => void): Disposable;
704
+ onDeactivate(callback: (name: string) => void): Disposable;
705
+ onError(callback: (error: PluginError) => void): Disposable;
706
+ }
707
+
708
+ interface PluginStatus {
709
+ state: PluginState;
710
+ activatedAt?: Date;
711
+ error?: Error;
712
+ resourceUsage?: {
713
+ memory: number;
714
+ cpu: number;
715
+ };
716
+ }
717
+ ```
718
+
719
+ ### Implementación
720
+
721
+ ```typescript
722
+ class PluginManagerImpl implements PluginManager {
723
+ private contexts: Map<string, PluginContext> = new Map();
724
+ private watchers: Map<string, FSWatcher> = new Map();
725
+ private activateCallbacks: Set<Function> = new Set();
726
+ private deactivateCallbacks: Set<Function> = new Set();
727
+ private errorCallbacks: Set<Function> = new Set();
728
+
729
+ constructor(
730
+ private loader: PluginLoader,
731
+ private registry: PluginRegistry,
732
+ private config: ConfigManager
733
+ ) {}
734
+
735
+ async activate(name: string): Promise<void> {
736
+ const plugin = this.registry.get(name);
737
+ if (!plugin) {
738
+ throw new Error(`Plugin not found: ${name}`);
739
+ }
740
+
741
+ if (plugin.state === 'activated') {
742
+ return; // Already active
743
+ }
744
+
745
+ // Activate dependencies first
746
+ const deps = this.registry.getDependencies(name);
747
+ for (const dep of deps) {
748
+ if (dep.state !== 'activated') {
749
+ await this.activate(dep.manifest.name);
750
+ }
751
+ }
752
+
753
+ // Create context
754
+ const context = this.createContext(plugin);
755
+ this.contexts.set(name, context);
756
+
757
+ try {
758
+ // Activate plugin
759
+ await plugin.module.activate(context);
760
+ plugin.state = 'activated';
761
+
762
+ this.activateCallbacks.forEach(cb => cb(plugin));
763
+ } catch (error) {
764
+ plugin.state = 'error';
765
+ this.errorCallbacks.forEach(cb => cb({ plugin: name, error }));
766
+ throw error;
767
+ }
768
+ }
769
+
770
+ async activateAll(): Promise<void> {
771
+ const plugins = this.registry.getAll();
772
+ const sorted = this.sortByDependencies(plugins);
773
+
774
+ for (const plugin of sorted) {
775
+ try {
776
+ await this.activate(plugin.manifest.name);
777
+ } catch (error) {
778
+ console.error(`Failed to activate ${plugin.manifest.name}:`, error);
779
+ }
780
+ }
781
+ }
782
+
783
+ async deactivate(name: string): Promise<void> {
784
+ const plugin = this.registry.get(name);
785
+ if (!plugin || plugin.state !== 'activated') {
786
+ return;
787
+ }
788
+
789
+ // Check if any active plugins depend on this one
790
+ const dependents = this.registry.getDependents(name);
791
+ const activeDependents = dependents.filter(p => p.state === 'activated');
792
+ if (activeDependents.length > 0) {
793
+ throw new Error(
794
+ `Cannot deactivate ${name}: still required by active plugins: ` +
795
+ activeDependents.map(p => p.manifest.name).join(', ')
796
+ );
797
+ }
798
+
799
+ const context = this.contexts.get(name);
800
+
801
+ try {
802
+ // Call deactivate if implemented
803
+ if (plugin.module.deactivate) {
804
+ await plugin.module.deactivate();
805
+ }
806
+
807
+ // Dispose all subscriptions
808
+ if (context) {
809
+ for (const sub of context.subscriptions) {
810
+ sub.dispose();
811
+ }
812
+ this.contexts.delete(name);
813
+ }
814
+
815
+ plugin.state = 'deactivated';
816
+ this.deactivateCallbacks.forEach(cb => cb(name));
817
+ } catch (error) {
818
+ plugin.state = 'error';
819
+ this.errorCallbacks.forEach(cb => cb({ plugin: name, error }));
820
+ throw error;
821
+ }
822
+ }
823
+
824
+ async deactivateAll(): Promise<void> {
825
+ const plugins = this.registry.getByState('activated');
826
+ const sorted = this.sortByDependencies(plugins).reverse();
827
+
828
+ for (const plugin of sorted) {
829
+ try {
830
+ await this.deactivate(plugin.manifest.name);
831
+ } catch (error) {
832
+ console.error(`Failed to deactivate ${plugin.manifest.name}:`, error);
833
+ }
834
+ }
835
+ }
836
+
837
+ async reload(name: string): Promise<void> {
838
+ const plugin = this.registry.get(name);
839
+ if (!plugin) {
840
+ throw new Error(`Plugin not found: ${name}`);
841
+ }
842
+
843
+ const wasActive = plugin.state === 'activated';
844
+
845
+ // Deactivate if active
846
+ if (wasActive) {
847
+ await this.deactivate(name);
848
+ }
849
+
850
+ // Unregister old version
851
+ this.registry.unregister(name);
852
+
853
+ // Clear module cache
854
+ this.clearModuleCache(plugin.path);
855
+
856
+ // Reload manifest and module
857
+ const manifests = await this.loader.discoverFromDirectory(
858
+ path.dirname(plugin.path)
859
+ );
860
+ const manifest = manifests.find(m => m.name === name);
861
+ if (!manifest) {
862
+ throw new Error(`Plugin manifest not found after reload: ${name}`);
863
+ }
864
+
865
+ // Load and register
866
+ const reloaded = await this.loader.load(manifest);
867
+ this.registry.register(reloaded);
868
+
869
+ // Reactivate if was active
870
+ if (wasActive) {
871
+ await this.activate(name);
872
+ }
873
+ }
874
+
875
+ async install(source: PluginSource): Promise<LoadedPlugin> {
876
+ // Discover plugin
877
+ const manifests = await this.loader.discover([source]);
878
+ if (manifests.length === 0) {
879
+ throw new Error('No plugin found at source');
880
+ }
881
+
882
+ const manifest = manifests[0];
883
+
884
+ // Check if already installed
885
+ if (this.registry.get(manifest.name)) {
886
+ throw new Error(`Plugin already installed: ${manifest.name}`);
887
+ }
888
+
889
+ // Load and register
890
+ const plugin = await this.loader.load(manifest);
891
+ this.registry.register(plugin);
892
+
893
+ return plugin;
894
+ }
895
+
896
+ async uninstall(name: string): Promise<void> {
897
+ const plugin = this.registry.get(name);
898
+ if (!plugin) {
899
+ throw new Error(`Plugin not found: ${name}`);
900
+ }
901
+
902
+ // Deactivate if active
903
+ if (plugin.state === 'activated') {
904
+ await this.deactivate(name);
905
+ }
906
+
907
+ // Stop hot-reload watching
908
+ this.disableHotReload(name);
909
+
910
+ // Unregister
911
+ this.registry.unregister(name);
912
+
913
+ // Clear module cache
914
+ this.clearModuleCache(plugin.path);
915
+ }
916
+
917
+ async update(name: string): Promise<LoadedPlugin> {
918
+ const plugin = this.registry.get(name);
919
+ if (!plugin) {
920
+ throw new Error(`Plugin not found: ${name}`);
921
+ }
922
+
923
+ // TODO: Fetch latest version from source
924
+ // For now, just reload
925
+ await this.reload(name);
926
+ return this.registry.get(name)!;
927
+ }
928
+
929
+ enableHotReload(name: string): void {
930
+ const plugin = this.registry.get(name);
931
+ if (!plugin) return;
932
+
933
+ if (this.watchers.has(name)) return; // Already watching
934
+
935
+ const watcher = fs.watch(plugin.path, { recursive: true }, async (event, filename) => {
936
+ if (!filename || !this.shouldReload(filename)) return;
937
+
938
+ console.log(`Hot-reloading plugin ${name} due to change in ${filename}`);
939
+ try {
940
+ await this.reload(name);
941
+ } catch (error) {
942
+ console.error(`Hot-reload failed for ${name}:`, error);
943
+ this.errorCallbacks.forEach(cb => cb({ plugin: name, error }));
944
+ }
945
+ });
946
+
947
+ this.watchers.set(name, watcher);
948
+ }
949
+
950
+ disableHotReload(name: string): void {
951
+ const watcher = this.watchers.get(name);
952
+ if (watcher) {
953
+ watcher.close();
954
+ this.watchers.delete(name);
955
+ }
956
+ }
957
+
958
+ private shouldReload(filename: string): boolean {
959
+ const ext = path.extname(filename);
960
+ return ['.js', '.ts', '.json'].includes(ext) && !filename.includes('node_modules');
961
+ }
962
+
963
+ private createContext(plugin: LoadedPlugin): PluginContext {
964
+ const storagePath = path.join(
965
+ this.config.get('plugins.storagePath', '.elsabro/plugins'),
966
+ plugin.manifest.name
967
+ );
968
+
969
+ return {
970
+ pluginPath: plugin.path,
971
+ manifest: plugin.manifest,
972
+ globalState: new MementoImpl(path.join(storagePath, 'global-state.json')),
973
+ workspaceState: new MementoImpl(path.join(storagePath, 'workspace-state.json')),
974
+ secrets: new SecretsStorageImpl(plugin.manifest.name),
975
+ subscriptions: [],
976
+ logger: new PluginLogger(plugin.manifest.name),
977
+ elsabro: this.createElsabroAPI(plugin)
978
+ };
979
+ }
980
+
981
+ private createElsabroAPI(plugin: LoadedPlugin): ElsabroAPI {
982
+ const permissions = new Set(plugin.manifest.permissions || []);
983
+
984
+ return {
985
+ // Only expose APIs based on permissions
986
+ config: permissions.has('config:read') ? this.config : undefined,
987
+ secrets: permissions.has('secrets') ? secretsVault : undefined,
988
+ // ... other APIs
989
+ };
990
+ }
991
+
992
+ getStatus(name: string): PluginStatus {
993
+ const plugin = this.registry.get(name);
994
+ if (!plugin) {
995
+ return { state: 'error', error: new Error('Plugin not found') };
996
+ }
997
+
998
+ return {
999
+ state: plugin.state,
1000
+ activatedAt: plugin._activatedAt,
1001
+ error: plugin._lastError,
1002
+ resourceUsage: this.getResourceUsage(name)
1003
+ };
1004
+ }
1005
+
1006
+ getAllStatus(): Map<string, PluginStatus> {
1007
+ const status = new Map<string, PluginStatus>();
1008
+ for (const plugin of this.registry.getAll()) {
1009
+ status.set(plugin.manifest.name, this.getStatus(plugin.manifest.name));
1010
+ }
1011
+ return status;
1012
+ }
1013
+
1014
+ isActive(name: string): boolean {
1015
+ const plugin = this.registry.get(name);
1016
+ return plugin?.state === 'activated';
1017
+ }
1018
+
1019
+ private sortByDependencies(plugins: LoadedPlugin[]): LoadedPlugin[] {
1020
+ // Topological sort by dependencies
1021
+ const visited = new Set<string>();
1022
+ const result: LoadedPlugin[] = [];
1023
+
1024
+ const visit = (plugin: LoadedPlugin) => {
1025
+ if (visited.has(plugin.manifest.name)) return;
1026
+ visited.add(plugin.manifest.name);
1027
+
1028
+ const deps = this.registry.getDependencies(plugin.manifest.name);
1029
+ for (const dep of deps) {
1030
+ visit(dep);
1031
+ }
1032
+ result.push(plugin);
1033
+ };
1034
+
1035
+ for (const plugin of plugins) {
1036
+ visit(plugin);
1037
+ }
1038
+
1039
+ return result;
1040
+ }
1041
+
1042
+ private clearModuleCache(pluginPath: string): void {
1043
+ // Clear all cached modules from this plugin
1044
+ for (const key of Object.keys(require.cache)) {
1045
+ if (key.startsWith(pluginPath)) {
1046
+ delete require.cache[key];
1047
+ }
1048
+ }
1049
+ }
1050
+
1051
+ onActivate(callback: Function): Disposable {
1052
+ this.activateCallbacks.add(callback);
1053
+ return { dispose: () => this.activateCallbacks.delete(callback) };
1054
+ }
1055
+
1056
+ onDeactivate(callback: Function): Disposable {
1057
+ this.deactivateCallbacks.add(callback);
1058
+ return { dispose: () => this.deactivateCallbacks.delete(callback) };
1059
+ }
1060
+
1061
+ onError(callback: Function): Disposable {
1062
+ this.errorCallbacks.add(callback);
1063
+ return { dispose: () => this.errorCallbacks.delete(callback) };
1064
+ }
1065
+ }
1066
+ ```
1067
+
1068
+ ---
1069
+
1070
+ ## 5. Hook System
1071
+
1072
+ ### Propósito
1073
+ Sistema de hooks para interceptar y extender funcionalidad del core.
1074
+
1075
+ ### Interfaz
1076
+
1077
+ ```typescript
1078
+ type HookCallback<T = unknown, R = T> = (
1079
+ context: HookContext<T>,
1080
+ next: () => Promise<R>
1081
+ ) => Promise<R>;
1082
+
1083
+ interface HookContext<T> {
1084
+ data: T;
1085
+ plugin?: string;
1086
+ metadata: Record<string, unknown>;
1087
+ abort(reason?: string): void;
1088
+ }
1089
+
1090
+ interface HookSystem {
1091
+ // Registration
1092
+ register<T>(point: string, callback: HookCallback<T>, priority?: number): Disposable;
1093
+ registerBefore<T>(point: string, callback: (data: T) => T | Promise<T>): Disposable;
1094
+ registerAfter<T>(point: string, callback: (data: T, result: unknown) => void): Disposable;
1095
+
1096
+ // Execution
1097
+ execute<T, R>(point: string, data: T): Promise<R>;
1098
+ executeWaterfall<T>(point: string, data: T): Promise<T>;
1099
+
1100
+ // Queries
1101
+ getHooks(point: string): Array<{ callback: HookCallback; priority: number; plugin?: string }>;
1102
+ hasHooks(point: string): boolean;
1103
+
1104
+ // Built-in hook points
1105
+ readonly POINTS: {
1106
+ AGENT_BEFORE_RUN: 'agent:beforeRun';
1107
+ AGENT_AFTER_RUN: 'agent:afterRun';
1108
+ TOOL_BEFORE_EXECUTE: 'tool:beforeExecute';
1109
+ TOOL_AFTER_EXECUTE: 'tool:afterExecute';
1110
+ LLM_BEFORE_CALL: 'llm:beforeCall';
1111
+ LLM_AFTER_CALL: 'llm:afterCall';
1112
+ CONFIG_CHANGE: 'config:change';
1113
+ PLUGIN_ACTIVATE: 'plugin:activate';
1114
+ PLUGIN_DEACTIVATE: 'plugin:deactivate';
1115
+ };
1116
+ }
1117
+ ```
1118
+
1119
+ ### Implementación
1120
+
1121
+ ```typescript
1122
+ class HookSystemImpl implements HookSystem {
1123
+ private hooks: Map<string, Array<{
1124
+ callback: HookCallback;
1125
+ priority: number;
1126
+ plugin?: string;
1127
+ }>> = new Map();
1128
+
1129
+ readonly POINTS = {
1130
+ AGENT_BEFORE_RUN: 'agent:beforeRun',
1131
+ AGENT_AFTER_RUN: 'agent:afterRun',
1132
+ TOOL_BEFORE_EXECUTE: 'tool:beforeExecute',
1133
+ TOOL_AFTER_EXECUTE: 'tool:afterExecute',
1134
+ LLM_BEFORE_CALL: 'llm:beforeCall',
1135
+ LLM_AFTER_CALL: 'llm:afterCall',
1136
+ CONFIG_CHANGE: 'config:change',
1137
+ PLUGIN_ACTIVATE: 'plugin:activate',
1138
+ PLUGIN_DEACTIVATE: 'plugin:deactivate'
1139
+ } as const;
1140
+
1141
+ register<T>(
1142
+ point: string,
1143
+ callback: HookCallback<T>,
1144
+ priority: number = 100
1145
+ ): Disposable {
1146
+ if (!this.hooks.has(point)) {
1147
+ this.hooks.set(point, []);
1148
+ }
1149
+
1150
+ const hook = { callback, priority };
1151
+ this.hooks.get(point)!.push(hook);
1152
+ this.hooks.get(point)!.sort((a, b) => a.priority - b.priority);
1153
+
1154
+ return {
1155
+ dispose: () => {
1156
+ const hooks = this.hooks.get(point);
1157
+ if (hooks) {
1158
+ const index = hooks.indexOf(hook);
1159
+ if (index > -1) hooks.splice(index, 1);
1160
+ }
1161
+ }
1162
+ };
1163
+ }
1164
+
1165
+ registerBefore<T>(point: string, callback: (data: T) => T | Promise<T>): Disposable {
1166
+ return this.register<T>(point, async (ctx, next) => {
1167
+ ctx.data = await callback(ctx.data);
1168
+ return next();
1169
+ }, 50);
1170
+ }
1171
+
1172
+ registerAfter<T>(
1173
+ point: string,
1174
+ callback: (data: T, result: unknown) => void
1175
+ ): Disposable {
1176
+ return this.register<T>(point, async (ctx, next) => {
1177
+ const result = await next();
1178
+ callback(ctx.data, result);
1179
+ return result;
1180
+ }, 150);
1181
+ }
1182
+
1183
+ async execute<T, R>(point: string, data: T): Promise<R> {
1184
+ const hooks = this.hooks.get(point) || [];
1185
+ let aborted = false;
1186
+ let abortReason: string | undefined;
1187
+
1188
+ const context: HookContext<T> = {
1189
+ data,
1190
+ metadata: {},
1191
+ abort: (reason) => {
1192
+ aborted = true;
1193
+ abortReason = reason;
1194
+ }
1195
+ };
1196
+
1197
+ // Build middleware chain
1198
+ let index = -1;
1199
+
1200
+ const dispatch = async (i: number): Promise<R> => {
1201
+ if (aborted) {
1202
+ throw new HookAbortError(point, abortReason);
1203
+ }
1204
+
1205
+ if (i <= index) {
1206
+ throw new Error('next() called multiple times');
1207
+ }
1208
+ index = i;
1209
+
1210
+ if (i >= hooks.length) {
1211
+ // End of chain - return data as result
1212
+ return context.data as unknown as R;
1213
+ }
1214
+
1215
+ const hook = hooks[i];
1216
+ context.plugin = hook.plugin;
1217
+
1218
+ return hook.callback(context, () => dispatch(i + 1)) as Promise<R>;
1219
+ };
1220
+
1221
+ return dispatch(0);
1222
+ }
1223
+
1224
+ async executeWaterfall<T>(point: string, data: T): Promise<T> {
1225
+ const hooks = this.hooks.get(point) || [];
1226
+ let result = data;
1227
+
1228
+ for (const hook of hooks) {
1229
+ const context: HookContext<T> = {
1230
+ data: result,
1231
+ plugin: hook.plugin,
1232
+ metadata: {},
1233
+ abort: () => {
1234
+ throw new HookAbortError(point, 'Waterfall aborted');
1235
+ }
1236
+ };
1237
+
1238
+ result = await hook.callback(context, async () => context.data) as T;
1239
+ }
1240
+
1241
+ return result;
1242
+ }
1243
+
1244
+ getHooks(point: string) {
1245
+ return this.hooks.get(point) || [];
1246
+ }
1247
+
1248
+ hasHooks(point: string): boolean {
1249
+ return (this.hooks.get(point)?.length || 0) > 0;
1250
+ }
1251
+ }
1252
+ ```
1253
+
1254
+ ### Uso de Hooks
1255
+
1256
+ ```typescript
1257
+ const hooks = new HookSystem();
1258
+
1259
+ // Plugin registers a hook to modify agent prompts
1260
+ hooks.registerBefore<AgentRunContext>('agent:beforeRun', async (data) => {
1261
+ // Add custom instructions to system prompt
1262
+ data.systemPrompt += '\n\nAdditional instructions from plugin...';
1263
+ return data;
1264
+ });
1265
+
1266
+ // Plugin registers a hook to log all tool executions
1267
+ hooks.registerAfter<ToolExecuteContext>('tool:afterExecute', (data, result) => {
1268
+ analytics.track('tool_executed', {
1269
+ tool: data.toolName,
1270
+ duration: result.duration,
1271
+ success: !result.error
1272
+ });
1273
+ });
1274
+
1275
+ // Middleware-style hook
1276
+ hooks.register<LLMCallContext>('llm:beforeCall', async (ctx, next) => {
1277
+ const start = Date.now();
1278
+
1279
+ try {
1280
+ const result = await next();
1281
+ metrics.recordLatency('llm_call', Date.now() - start);
1282
+ return result;
1283
+ } catch (error) {
1284
+ metrics.increment('llm_errors');
1285
+ throw error;
1286
+ }
1287
+ }, 10); // High priority (runs early)
1288
+ ```
1289
+
1290
+ ---
1291
+
1292
+ ## 6. Plugin Examples
1293
+
1294
+ ### Example: Git Integration Plugin
1295
+
1296
+ ```typescript
1297
+ // plugins/elsabro-git/plugin.json
1298
+ {
1299
+ "name": "elsabro-git",
1300
+ "version": "1.0.0",
1301
+ "displayName": "Git Integration",
1302
+ "description": "Git operations for ELSABRO agents",
1303
+ "main": "dist/index.js",
1304
+ "engines": { "elsabro": ">=3.0.0" },
1305
+ "permissions": ["shell", "filesystem:read"],
1306
+ "contributes": {
1307
+ "tools": [
1308
+ {
1309
+ "name": "git-status",
1310
+ "displayName": "Git Status",
1311
+ "description": "Get the current git repository status",
1312
+ "inputSchema": {
1313
+ "type": "object",
1314
+ "properties": {
1315
+ "path": { "type": "string", "description": "Repository path" }
1316
+ }
1317
+ },
1318
+ "handler": "tools/gitStatus"
1319
+ },
1320
+ {
1321
+ "name": "git-commit",
1322
+ "displayName": "Git Commit",
1323
+ "description": "Create a git commit",
1324
+ "inputSchema": {
1325
+ "type": "object",
1326
+ "required": ["message"],
1327
+ "properties": {
1328
+ "message": { "type": "string" },
1329
+ "files": { "type": "array", "items": { "type": "string" } }
1330
+ }
1331
+ },
1332
+ "handler": "tools/gitCommit"
1333
+ }
1334
+ ],
1335
+ "hooks": [
1336
+ {
1337
+ "point": "agent:afterRun",
1338
+ "priority": 100,
1339
+ "handler": "hooks/autoCommit"
1340
+ }
1341
+ ]
1342
+ }
1343
+ }
1344
+ ```
1345
+
1346
+ ```typescript
1347
+ // plugins/elsabro-git/src/index.ts
1348
+ import { Plugin, PluginContext } from '@elsabro/plugin-api';
1349
+
1350
+ export default class GitPlugin implements Plugin {
1351
+ private context!: PluginContext;
1352
+
1353
+ async activate(context: PluginContext): Promise<void> {
1354
+ this.context = context;
1355
+
1356
+ context.logger.info('Git plugin activated');
1357
+
1358
+ // Check if git is available
1359
+ try {
1360
+ await this.runGitCommand(['--version']);
1361
+ } catch (error) {
1362
+ context.logger.warn('Git not found in PATH');
1363
+ }
1364
+ }
1365
+
1366
+ async deactivate(): Promise<void> {
1367
+ this.context.logger.info('Git plugin deactivated');
1368
+ }
1369
+
1370
+ private async runGitCommand(args: string[]): Promise<string> {
1371
+ // Implementation
1372
+ }
1373
+ }
1374
+ ```
1375
+
1376
+ ### Example: Analytics Plugin
1377
+
1378
+ ```typescript
1379
+ // plugins/elsabro-analytics/plugin.json
1380
+ {
1381
+ "name": "elsabro-analytics",
1382
+ "version": "1.0.0",
1383
+ "displayName": "Analytics",
1384
+ "description": "Usage analytics and insights",
1385
+ "main": "dist/index.js",
1386
+ "engines": { "elsabro": ">=3.0.0" },
1387
+ "permissions": ["network", "config:read"],
1388
+ "contributes": {
1389
+ "hooks": [
1390
+ { "point": "agent:afterRun", "priority": 200, "handler": "hooks/trackAgentRun" },
1391
+ { "point": "tool:afterExecute", "priority": 200, "handler": "hooks/trackToolUse" },
1392
+ { "point": "llm:afterCall", "priority": 200, "handler": "hooks/trackLLMCall" }
1393
+ ],
1394
+ "commands": [
1395
+ {
1396
+ "command": "analytics.dashboard",
1397
+ "title": "Show Analytics Dashboard",
1398
+ "category": "Analytics"
1399
+ }
1400
+ ]
1401
+ }
1402
+ }
1403
+ ```
1404
+
1405
+ ---
1406
+
1407
+ ## 7. Plugin Security
1408
+
1409
+ ### Sandbox Aislado
1410
+
1411
+ ```typescript
1412
+ class PluginSandbox {
1413
+ private vm: NodeVM;
1414
+
1415
+ constructor(plugin: LoadedPlugin, options: SandboxOptions) {
1416
+ this.vm = new NodeVM({
1417
+ timeout: options.timeout || 30000,
1418
+ sandbox: {},
1419
+ require: {
1420
+ external: options.allowedModules || ['lodash', 'dayjs'],
1421
+ builtin: this.getAllowedBuiltins(plugin.manifest.permissions),
1422
+ root: plugin.path,
1423
+ mock: this.createMocks(plugin.manifest.permissions)
1424
+ }
1425
+ });
1426
+ }
1427
+
1428
+ private getAllowedBuiltins(permissions: PluginPermission[]): string[] {
1429
+ const allowed = ['path', 'url', 'util', 'events'];
1430
+
1431
+ if (permissions.includes('filesystem:read')) {
1432
+ allowed.push('fs/promises');
1433
+ }
1434
+ if (permissions.includes('network')) {
1435
+ allowed.push('http', 'https');
1436
+ }
1437
+
1438
+ return allowed;
1439
+ }
1440
+
1441
+ private createMocks(permissions: PluginPermission[]): Record<string, unknown> {
1442
+ const mocks: Record<string, unknown> = {};
1443
+
1444
+ // Mock dangerous modules
1445
+ if (!permissions.includes('shell')) {
1446
+ mocks['child_process'] = {
1447
+ exec: () => { throw new Error('Permission denied: shell'); },
1448
+ spawn: () => { throw new Error('Permission denied: shell'); }
1449
+ };
1450
+ }
1451
+
1452
+ return mocks;
1453
+ }
1454
+ }
1455
+ ```
1456
+
1457
+ ### Permission Enforcement
1458
+
1459
+ ```typescript
1460
+ class PermissionEnforcer {
1461
+ constructor(private permissions: Set<PluginPermission>) {}
1462
+
1463
+ checkFileRead(path: string): void {
1464
+ if (!this.permissions.has('filesystem:read')) {
1465
+ throw new PermissionDeniedError('filesystem:read', path);
1466
+ }
1467
+ }
1468
+
1469
+ checkFileWrite(path: string): void {
1470
+ if (!this.permissions.has('filesystem:write')) {
1471
+ throw new PermissionDeniedError('filesystem:write', path);
1472
+ }
1473
+ }
1474
+
1475
+ checkNetwork(url: string): void {
1476
+ if (!this.permissions.has('network')) {
1477
+ throw new PermissionDeniedError('network', url);
1478
+ }
1479
+ }
1480
+
1481
+ checkSecrets(): void {
1482
+ if (!this.permissions.has('secrets')) {
1483
+ throw new PermissionDeniedError('secrets');
1484
+ }
1485
+ }
1486
+ }
1487
+ ```
1488
+
1489
+ ---
1490
+
1491
+ ## 8. Plugin Dashboard
1492
+
1493
+ ```
1494
+ ┌─────────────────────────────────────────────────────────────────────────────┐
1495
+ │ ELSABRO Plugin Manager │
1496
+ ├─────────────────────────────────────────────────────────────────────────────┤
1497
+ │ Installed: 5 plugins (4 active, 1 disabled) │
1498
+ │ Hot-reload: Enabled for development │
1499
+ ├─────────────────────────────────────────────────────────────────────────────┤
1500
+ │ Plugins │
1501
+ ├──────────────────────┬─────────┬────────────┬─────────────┬─────────────────┤
1502
+ │ Name │ Version │ Status │ Permissions │ Memory │
1503
+ ├──────────────────────┼─────────┼────────────┼─────────────┼─────────────────┤
1504
+ │ elsabro-git │ 1.2.0 │ ✓ Active │ shell, fs │ 12.3 MB │
1505
+ │ elsabro-analytics │ 2.0.1 │ ✓ Active │ network │ 8.1 MB │
1506
+ │ elsabro-jira │ 1.0.0 │ ✓ Active │ network │ 15.2 MB │
1507
+ │ elsabro-slack │ 1.5.2 │ ✓ Active │ network │ 10.8 MB │
1508
+ │ elsabro-debug │ 0.9.0 │ ✗ Disabled │ shell, fs │ - MB │
1509
+ └──────────────────────┴─────────┴────────────┴─────────────┴─────────────────┘
1510
+ │ Contributions │
1511
+ ├─────────────────────────────────────────────────────────────────────────────┤
1512
+ │ Agents: 2 (git-reviewer, jira-assistant) │
1513
+ │ Tools: 8 (git-status, git-commit, jira-create-issue, ...) │
1514
+ │ Commands: 5 │
1515
+ │ Hooks: 12 registered │
1516
+ └─────────────────────────────────────────────────────────────────────────────┘
1517
+ ```
1518
+
1519
+ ---
1520
+
1521
+ ## Referencias
1522
+
1523
+ - **REF-001**: Architecture Guide
1524
+ - **REF-022**: Security System
1525
+ - **REF-023**: Configuration Management
1526
+ - **REF-024**: Esta referencia (Plugin System)