musonda 1.0.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 (196) hide show
  1. package/IMPLEMENTATION.md +432 -0
  2. package/README.md +119 -0
  3. package/dist/agent/harness.d.ts +25 -0
  4. package/dist/agent/harness.js +215 -0
  5. package/dist/agent/harness.js.map +1 -0
  6. package/dist/agent/openaiResponsesClient.d.ts +35 -0
  7. package/dist/agent/openaiResponsesClient.js +250 -0
  8. package/dist/agent/openaiResponsesClient.js.map +1 -0
  9. package/dist/agent/systemPrompt.d.ts +9 -0
  10. package/dist/agent/systemPrompt.js +100 -0
  11. package/dist/agent/systemPrompt.js.map +1 -0
  12. package/dist/agent/types.d.ts +53 -0
  13. package/dist/agent/types.js +2 -0
  14. package/dist/agent/types.js.map +1 -0
  15. package/dist/cli/commands/chat.d.ts +6 -0
  16. package/dist/cli/commands/chat.js +8 -0
  17. package/dist/cli/commands/chat.js.map +1 -0
  18. package/dist/cli/commands/doctor.d.ts +4 -0
  19. package/dist/cli/commands/doctor.js +336 -0
  20. package/dist/cli/commands/doctor.js.map +1 -0
  21. package/dist/cli/commands/gateway.d.ts +20 -0
  22. package/dist/cli/commands/gateway.js +135 -0
  23. package/dist/cli/commands/gateway.js.map +1 -0
  24. package/dist/cli/commands/help.d.ts +4 -0
  25. package/dist/cli/commands/help.js +159 -0
  26. package/dist/cli/commands/help.js.map +1 -0
  27. package/dist/cli/commands/plugins.d.ts +18 -0
  28. package/dist/cli/commands/plugins.js +250 -0
  29. package/dist/cli/commands/plugins.js.map +1 -0
  30. package/dist/cli/commands/service.d.ts +16 -0
  31. package/dist/cli/commands/service.js +136 -0
  32. package/dist/cli/commands/service.js.map +1 -0
  33. package/dist/cli/commands/setup.d.ts +4 -0
  34. package/dist/cli/commands/setup.js +8 -0
  35. package/dist/cli/commands/setup.js.map +1 -0
  36. package/dist/cli/commands/status.d.ts +4 -0
  37. package/dist/cli/commands/status.js +73 -0
  38. package/dist/cli/commands/status.js.map +1 -0
  39. package/dist/cli/commands/update.d.ts +22 -0
  40. package/dist/cli/commands/update.js +226 -0
  41. package/dist/cli/commands/update.js.map +1 -0
  42. package/dist/cli/daemon.d.ts +33 -0
  43. package/dist/cli/daemon.js +208 -0
  44. package/dist/cli/daemon.js.map +1 -0
  45. package/dist/cli/interactiveMenu.d.ts +5 -0
  46. package/dist/cli/interactiveMenu.js +225 -0
  47. package/dist/cli/interactiveMenu.js.map +1 -0
  48. package/dist/cli/musondaCli.d.ts +30 -0
  49. package/dist/cli/musondaCli.js +269 -0
  50. package/dist/cli/musondaCli.js.map +1 -0
  51. package/dist/cli/ui.d.ts +49 -0
  52. package/dist/cli/ui.js +90 -0
  53. package/dist/cli/ui.js.map +1 -0
  54. package/dist/cli.d.ts +9 -0
  55. package/dist/cli.js +315 -0
  56. package/dist/cli.js.map +1 -0
  57. package/dist/config/loader.d.ts +12 -0
  58. package/dist/config/loader.js +217 -0
  59. package/dist/config/loader.js.map +1 -0
  60. package/dist/config/paths.d.ts +11 -0
  61. package/dist/config/paths.js +18 -0
  62. package/dist/config/paths.js.map +1 -0
  63. package/dist/config/types.d.ts +246 -0
  64. package/dist/config/types.js +155 -0
  65. package/dist/config/types.js.map +1 -0
  66. package/dist/gateway/channels/cli.d.ts +37 -0
  67. package/dist/gateway/channels/cli.js +298 -0
  68. package/dist/gateway/channels/cli.js.map +1 -0
  69. package/dist/gateway/channels/discord.d.ts +18 -0
  70. package/dist/gateway/channels/discord.js +419 -0
  71. package/dist/gateway/channels/discord.js.map +1 -0
  72. package/dist/gateway/channels/slack.d.ts +26 -0
  73. package/dist/gateway/channels/slack.js +470 -0
  74. package/dist/gateway/channels/slack.js.map +1 -0
  75. package/dist/gateway/channels/webhook.d.ts +18 -0
  76. package/dist/gateway/channels/webhook.js +49 -0
  77. package/dist/gateway/channels/webhook.js.map +1 -0
  78. package/dist/gateway/http.d.ts +33 -0
  79. package/dist/gateway/http.js +605 -0
  80. package/dist/gateway/http.js.map +1 -0
  81. package/dist/gateway/server.d.ts +38 -0
  82. package/dist/gateway/server.js +233 -0
  83. package/dist/gateway/server.js.map +1 -0
  84. package/dist/gateway/types.d.ts +28 -0
  85. package/dist/gateway/types.js +2 -0
  86. package/dist/gateway/types.js.map +1 -0
  87. package/dist/index.d.ts +17 -0
  88. package/dist/index.js +32 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/memory/memoryManager.d.ts +22 -0
  91. package/dist/memory/memoryManager.js +200 -0
  92. package/dist/memory/memoryManager.js.map +1 -0
  93. package/dist/memory/storage.d.ts +24 -0
  94. package/dist/memory/storage.js +187 -0
  95. package/dist/memory/storage.js.map +1 -0
  96. package/dist/memory/types.d.ts +43 -0
  97. package/dist/memory/types.js +2 -0
  98. package/dist/memory/types.js.map +1 -0
  99. package/dist/plugins/manifest.d.ts +23 -0
  100. package/dist/plugins/manifest.js +185 -0
  101. package/dist/plugins/manifest.js.map +1 -0
  102. package/dist/plugins/mcp/mcpBridge.d.ts +31 -0
  103. package/dist/plugins/mcp/mcpBridge.js +115 -0
  104. package/dist/plugins/mcp/mcpBridge.js.map +1 -0
  105. package/dist/plugins/mcp/mcpClient.d.ts +43 -0
  106. package/dist/plugins/mcp/mcpClient.js +309 -0
  107. package/dist/plugins/mcp/mcpClient.js.map +1 -0
  108. package/dist/plugins/mcp/mcpConfig.d.ts +34 -0
  109. package/dist/plugins/mcp/mcpConfig.js +326 -0
  110. package/dist/plugins/mcp/mcpConfig.js.map +1 -0
  111. package/dist/plugins/mcp/types.d.ts +48 -0
  112. package/dist/plugins/mcp/types.js +2 -0
  113. package/dist/plugins/mcp/types.js.map +1 -0
  114. package/dist/plugins/pluginManager.d.ts +164 -0
  115. package/dist/plugins/pluginManager.js +1016 -0
  116. package/dist/plugins/pluginManager.js.map +1 -0
  117. package/dist/plugins/skills/frontmatterParser.d.ts +28 -0
  118. package/dist/plugins/skills/frontmatterParser.js +189 -0
  119. package/dist/plugins/skills/frontmatterParser.js.map +1 -0
  120. package/dist/plugins/skills/skillDiscovery.d.ts +15 -0
  121. package/dist/plugins/skills/skillDiscovery.js +148 -0
  122. package/dist/plugins/skills/skillDiscovery.js.map +1 -0
  123. package/dist/plugins/skills/skillTools.d.ts +8 -0
  124. package/dist/plugins/skills/skillTools.js +195 -0
  125. package/dist/plugins/skills/skillTools.js.map +1 -0
  126. package/dist/plugins/skills/types.d.ts +25 -0
  127. package/dist/plugins/skills/types.js +2 -0
  128. package/dist/plugins/skills/types.js.map +1 -0
  129. package/dist/plugins/types.d.ts +30 -0
  130. package/dist/plugins/types.js +2 -0
  131. package/dist/plugins/types.js.map +1 -0
  132. package/dist/scheduler/scheduler.d.ts +25 -0
  133. package/dist/scheduler/scheduler.js +239 -0
  134. package/dist/scheduler/scheduler.js.map +1 -0
  135. package/dist/scheduler/types.d.ts +39 -0
  136. package/dist/scheduler/types.js +2 -0
  137. package/dist/scheduler/types.js.map +1 -0
  138. package/dist/security/childProcessEnv.d.ts +5 -0
  139. package/dist/security/childProcessEnv.js +42 -0
  140. package/dist/security/childProcessEnv.js.map +1 -0
  141. package/dist/security/fileSandbox.d.ts +153 -0
  142. package/dist/security/fileSandbox.js +779 -0
  143. package/dist/security/fileSandbox.js.map +1 -0
  144. package/dist/service/index.d.ts +30 -0
  145. package/dist/service/index.js +41 -0
  146. package/dist/service/index.js.map +1 -0
  147. package/dist/service/systemd.d.ts +36 -0
  148. package/dist/service/systemd.js +409 -0
  149. package/dist/service/systemd.js.map +1 -0
  150. package/dist/service/types.d.ts +118 -0
  151. package/dist/service/types.js +6 -0
  152. package/dist/service/types.js.map +1 -0
  153. package/dist/tools/builtin/calcTools.d.ts +2 -0
  154. package/dist/tools/builtin/calcTools.js +51 -0
  155. package/dist/tools/builtin/calcTools.js.map +1 -0
  156. package/dist/tools/builtin/fileTools.d.ts +11 -0
  157. package/dist/tools/builtin/fileTools.js +352 -0
  158. package/dist/tools/builtin/fileTools.js.map +1 -0
  159. package/dist/tools/builtin/memoryTools.d.ts +4 -0
  160. package/dist/tools/builtin/memoryTools.js +123 -0
  161. package/dist/tools/builtin/memoryTools.js.map +1 -0
  162. package/dist/tools/builtin/noteTools.d.ts +4 -0
  163. package/dist/tools/builtin/noteTools.js +91 -0
  164. package/dist/tools/builtin/noteTools.js.map +1 -0
  165. package/dist/tools/builtin/pluginTools.d.ts +29 -0
  166. package/dist/tools/builtin/pluginTools.js +406 -0
  167. package/dist/tools/builtin/pluginTools.js.map +1 -0
  168. package/dist/tools/builtin/scheduleTools.d.ts +4 -0
  169. package/dist/tools/builtin/scheduleTools.js +156 -0
  170. package/dist/tools/builtin/scheduleTools.js.map +1 -0
  171. package/dist/tools/builtin/timeTools.d.ts +2 -0
  172. package/dist/tools/builtin/timeTools.js +41 -0
  173. package/dist/tools/builtin/timeTools.js.map +1 -0
  174. package/dist/tools/builtin/webTools.d.ts +12 -0
  175. package/dist/tools/builtin/webTools.js +325 -0
  176. package/dist/tools/builtin/webTools.js.map +1 -0
  177. package/dist/tools/registry.d.ts +15 -0
  178. package/dist/tools/registry.js +111 -0
  179. package/dist/tools/registry.js.map +1 -0
  180. package/dist/tools/types.d.ts +41 -0
  181. package/dist/tools/types.js +2 -0
  182. package/dist/tools/types.js.map +1 -0
  183. package/dist/utils/logger.d.ts +29 -0
  184. package/dist/utils/logger.js +124 -0
  185. package/dist/utils/logger.js.map +1 -0
  186. package/dist/wizard/onboarding.d.ts +3 -0
  187. package/dist/wizard/onboarding.js +408 -0
  188. package/dist/wizard/onboarding.js.map +1 -0
  189. package/package.json +69 -0
  190. package/plugins/example-calc/mcp.json +11 -0
  191. package/plugins/example-calc/plugin.json +12 -0
  192. package/plugins/example-calc/server.mjs +92 -0
  193. package/plugins/example-calc/skills/math-assistant/SKILL.md +15 -0
  194. package/plugins/plugin-builder/plugin.json +12 -0
  195. package/plugins/plugin-builder/skills/create-plugin/SKILL.md +149 -0
  196. package/plugins/plugin-builder/skills/manage-plugins/SKILL.md +157 -0
@@ -0,0 +1,1016 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { ManifestValidator } from './manifest.js';
5
+ import { SkillDiscovery } from './skills/skillDiscovery.js';
6
+ import { SkillToolFactory } from './skills/skillTools.js';
7
+ import { SkillFrontmatterParser } from './skills/frontmatterParser.js';
8
+ import { McpConfigValidator } from './mcp/mcpConfig.js';
9
+ import { McpBridge } from './mcp/mcpBridge.js';
10
+ import { logger } from '../utils/logger.js';
11
+ export class PluginManager {
12
+ config;
13
+ plugins = new Map();
14
+ skills = [];
15
+ mcpBridge = new McpBridge();
16
+ isLoaded = false;
17
+ constructor(config) {
18
+ this.config = config;
19
+ }
20
+ getAllSkills() {
21
+ return this.skills;
22
+ }
23
+ getAllPlugins() {
24
+ return Array.from(this.plugins.values());
25
+ }
26
+ getMcpBridge() {
27
+ return this.mcpBridge;
28
+ }
29
+ /**
30
+ * Discovers and loads all plugins from configured plugin directories.
31
+ * Registers skills and MCP tools into Musonda's ToolRegistry.
32
+ */
33
+ async loadAll(toolRegistry) {
34
+ const pluginsConfig = this.config.plugins;
35
+ if (!pluginsConfig || !pluginsConfig.enabled) {
36
+ logger.info('Agent Plugins subsystem is disabled.');
37
+ return;
38
+ }
39
+ logger.info('Initializing Agent Plugins subsystem...');
40
+ // Register built-in skill tools for progressive disclosure
41
+ const skillTools = SkillToolFactory.createTools(() => this.getAllSkills());
42
+ for (const tool of skillTools) {
43
+ toolRegistry.register(tool);
44
+ }
45
+ const baseDataDir = path.resolve(pluginsConfig.dataDir || './data/plugin_data');
46
+ if (!fs.existsSync(baseDataDir)) {
47
+ try {
48
+ fs.mkdirSync(baseDataDir, { recursive: true });
49
+ }
50
+ catch (err) {
51
+ logger.error(`Failed to create plugin data directory: ${err.message}`);
52
+ }
53
+ }
54
+ const disabledSet = new Set(pluginsConfig.disabledPlugins || []);
55
+ const searchDirs = pluginsConfig.directories || ['./data/plugins', './plugins'];
56
+ for (const searchDir of searchDirs) {
57
+ const resolvedSearchDir = path.resolve(searchDir);
58
+ if (!fs.existsSync(resolvedSearchDir)) {
59
+ continue;
60
+ }
61
+ let entries = [];
62
+ try {
63
+ entries = fs.readdirSync(resolvedSearchDir, { withFileTypes: true });
64
+ }
65
+ catch (err) {
66
+ logger.warn(`Could not read plugin search directory "${resolvedSearchDir}": ${err.message}`);
67
+ continue;
68
+ }
69
+ for (const entry of entries) {
70
+ if (!entry.isDirectory())
71
+ continue;
72
+ const candidateDir = path.join(resolvedSearchDir, entry.name);
73
+ await this.loadPluginFromDirectory(candidateDir, baseDataDir, disabledSet, toolRegistry);
74
+ }
75
+ }
76
+ this.isLoaded = true;
77
+ logger.info(`Agent Plugins loaded: ${this.plugins.size} plugin(s), ${this.skills.length} skill(s), ${this.mcpBridge.getActiveServers().length} active MCP server(s).`);
78
+ }
79
+ /**
80
+ * Loads a single plugin from a directory.
81
+ */
82
+ async loadPluginFromDirectory(pluginDir, baseDataDir, disabledSet, toolRegistry) {
83
+ const resolvedPluginDir = path.resolve(pluginDir);
84
+ const manifestPath = path.join(resolvedPluginDir, 'plugin.json');
85
+ if (!fs.existsSync(manifestPath)) {
86
+ return null;
87
+ }
88
+ // 1. Validate manifest
89
+ const manifestResult = ManifestValidator.loadFromDirectory(resolvedPluginDir);
90
+ if (!manifestResult.valid || !manifestResult.manifest) {
91
+ logger.error(`[Plugin Rejected] "${resolvedPluginDir}" has invalid plugin.json: ${manifestResult.errors.join('; ')}`);
92
+ return null;
93
+ }
94
+ const manifest = manifestResult.manifest;
95
+ const pluginName = manifest.name;
96
+ if (disabledSet.has(pluginName)) {
97
+ logger.info(`[Plugin: ${pluginName}] Disabled in configuration. Skipping.`);
98
+ return null;
99
+ }
100
+ if (this.plugins.has(pluginName)) {
101
+ logger.warn(`[Plugin: ${pluginName}] Already loaded from another location. Skipping duplicate.`);
102
+ return null;
103
+ }
104
+ // 2. Dedicated writable plugin data directory
105
+ const pluginDataDir = path.join(baseDataDir, pluginName);
106
+ if (!fs.existsSync(pluginDataDir)) {
107
+ try {
108
+ fs.mkdirSync(pluginDataDir, { recursive: true });
109
+ }
110
+ catch (err) {
111
+ logger.error(`Could not create data dir for plugin "${pluginName}": ${err.message}`);
112
+ }
113
+ }
114
+ logger.info(`[Plugin: ${pluginName}] Loading package (version: ${manifest.version || 'unspecified'})...`);
115
+ // 3. Discover skills
116
+ const skillResult = SkillDiscovery.discover(resolvedPluginDir, pluginName);
117
+ if (skillResult.skills.length > 0) {
118
+ this.skills.push(...skillResult.skills);
119
+ }
120
+ // 4. Discover and connect MCP servers
121
+ const mcpResult = McpConfigValidator.loadFromDirectory(resolvedPluginDir, pluginName, pluginDataDir);
122
+ if (mcpResult.valid && mcpResult.servers.length > 0) {
123
+ for (const server of mcpResult.servers) {
124
+ // Pass plugin root and data to server context
125
+ server.pluginRoot = resolvedPluginDir;
126
+ server.pluginData = pluginDataDir;
127
+ await this.mcpBridge.registerServer(server, toolRegistry);
128
+ }
129
+ }
130
+ const pkg = {
131
+ name: pluginName,
132
+ rootDir: resolvedPluginDir,
133
+ dataDir: pluginDataDir,
134
+ manifest,
135
+ };
136
+ this.plugins.set(pluginName, pkg);
137
+ return pkg;
138
+ }
139
+ /**
140
+ * Unloads a plugin from runtime: terminates its MCP servers, removes its bridged tools and skills.
141
+ */
142
+ async unloadPlugin(pluginName, toolRegistry) {
143
+ const pkg = this.plugins.get(pluginName);
144
+ if (!pkg) {
145
+ return false;
146
+ }
147
+ // 1. Unregister MCP servers and bridged tools
148
+ await this.mcpBridge.unregisterServersForPlugin(pluginName, toolRegistry);
149
+ // 2. Remove skills belonging to this plugin
150
+ this.skills = this.skills.filter((s) => s.pluginName !== pluginName);
151
+ // 3. Remove plugin from map
152
+ this.plugins.delete(pluginName);
153
+ logger.info(`[Plugin: ${pluginName}] Unloaded from runtime.`);
154
+ return true;
155
+ }
156
+ /**
157
+ * Reloads a specific plugin from disk into the live runtime.
158
+ */
159
+ async reloadPlugin(pluginName, toolRegistry) {
160
+ const existing = this.plugins.get(pluginName);
161
+ const rootDir = existing?.rootDir;
162
+ if (existing) {
163
+ await this.unloadPlugin(pluginName, toolRegistry);
164
+ }
165
+ const pluginsConfig = this.config.plugins;
166
+ const baseDataDir = path.resolve(pluginsConfig?.dataDir || './data/plugin_data');
167
+ const disabledSet = new Set(pluginsConfig?.disabledPlugins || []);
168
+ // If we already know the rootDir, reload from there
169
+ if (rootDir && fs.existsSync(rootDir)) {
170
+ return await this.loadPluginFromDirectory(rootDir, baseDataDir, disabledSet, toolRegistry);
171
+ }
172
+ // Otherwise search configured searchDirs
173
+ const searchDirs = pluginsConfig?.directories || ['./data/plugins', './plugins'];
174
+ for (const searchDir of searchDirs) {
175
+ const candidateDir = path.resolve(searchDir, pluginName);
176
+ if (fs.existsSync(candidateDir)) {
177
+ return await this.loadPluginFromDirectory(candidateDir, baseDataDir, disabledSet, toolRegistry);
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+ /**
183
+ * Reloads all plugins from configured directories without shutting down the gateway.
184
+ */
185
+ async reloadAll(toolRegistry) {
186
+ logger.info('Reloading all Agent Plugins...');
187
+ await this.mcpBridge.shutdown();
188
+ this.plugins.clear();
189
+ this.skills = [];
190
+ const pluginsConfig = this.config.plugins;
191
+ if (!pluginsConfig || !pluginsConfig.enabled) {
192
+ return { loaded: 0, skills: 0, mcpServers: 0 };
193
+ }
194
+ const baseDataDir = path.resolve(pluginsConfig.dataDir || './data/plugin_data');
195
+ const disabledSet = new Set(pluginsConfig.disabledPlugins || []);
196
+ const searchDirs = pluginsConfig.directories || ['./data/plugins', './plugins'];
197
+ for (const searchDir of searchDirs) {
198
+ const resolvedSearchDir = path.resolve(searchDir);
199
+ if (!fs.existsSync(resolvedSearchDir))
200
+ continue;
201
+ let entries = [];
202
+ try {
203
+ entries = fs.readdirSync(resolvedSearchDir, { withFileTypes: true });
204
+ }
205
+ catch {
206
+ continue;
207
+ }
208
+ for (const entry of entries) {
209
+ if (!entry.isDirectory())
210
+ continue;
211
+ const candidateDir = path.join(resolvedSearchDir, entry.name);
212
+ await this.loadPluginFromDirectory(candidateDir, baseDataDir, disabledSet, toolRegistry);
213
+ }
214
+ }
215
+ logger.info(`Agent Plugins reloaded: ${this.plugins.size} plugin(s), ${this.skills.length} skill(s), ${this.mcpBridge.getActiveServers().length} active MCP server(s).`);
216
+ return {
217
+ loaded: this.plugins.size,
218
+ skills: this.skills.length,
219
+ mcpServers: this.mcpBridge.getActiveServers().length,
220
+ };
221
+ }
222
+ /**
223
+ * Scaffolds a new plugin on disk with valid manifest, optional skills, and optional MCP servers,
224
+ * then immediately activates it in the live runtime.
225
+ */
226
+ async createAndLoadPlugin(params, toolRegistry) {
227
+ // 1. Validate plugin name
228
+ const nameValidation = ManifestValidator.validatePluginName(params.name);
229
+ if (!nameValidation.valid) {
230
+ return {
231
+ success: false,
232
+ error: `Invalid plugin name: ${nameValidation.error}`,
233
+ skillsLoaded: 0,
234
+ toolsRegistered: [],
235
+ };
236
+ }
237
+ const pluginName = params.name;
238
+ const version = params.version || '1.0.0';
239
+ const pluginsConfig = this.config.plugins;
240
+ const basePluginsDir = (pluginsConfig?.directories && pluginsConfig.directories[0]) || './plugins';
241
+ const pluginDir = path.resolve(params.targetDir || path.join(basePluginsDir, pluginName));
242
+ if (fs.existsSync(pluginDir)) {
243
+ const manifestPath = path.join(pluginDir, 'plugin.json');
244
+ if (fs.existsSync(manifestPath) && this.plugins.has(pluginName)) {
245
+ return {
246
+ success: false,
247
+ error: `Plugin "${pluginName}" already exists at "${pluginDir}". Use plugin_reload to refresh existing plugins.`,
248
+ skillsLoaded: 0,
249
+ toolsRegistered: [],
250
+ };
251
+ }
252
+ }
253
+ try {
254
+ fs.mkdirSync(pluginDir, { recursive: true });
255
+ // 2. Build manifest
256
+ const manifest = {
257
+ $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json',
258
+ name: pluginName,
259
+ version,
260
+ description: params.description,
261
+ author: params.author || { name: 'Musonda Agent' },
262
+ license: 'MIT',
263
+ };
264
+ fs.writeFileSync(path.join(pluginDir, 'plugin.json'), JSON.stringify(manifest, null, 2), 'utf-8');
265
+ // 3. Write skills if provided
266
+ if (params.skills && params.skills.length > 0) {
267
+ for (const skill of params.skills) {
268
+ const skillDir = path.join(pluginDir, 'skills', skill.name);
269
+ fs.mkdirSync(skillDir, { recursive: true });
270
+ // Ensure markdown has valid YAML frontmatter
271
+ let skillMd = skill.content.trim();
272
+ if (!skillMd.startsWith('---')) {
273
+ skillMd = `---\nname: ${skill.name}\ndescription: ${skill.description}\nlicense: MIT\n---\n\n${skillMd}`;
274
+ }
275
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillMd, 'utf-8');
276
+ if (skill.scripts && skill.scripts.length > 0) {
277
+ const scriptsDir = path.join(skillDir, 'scripts');
278
+ fs.mkdirSync(scriptsDir, { recursive: true });
279
+ for (const script of skill.scripts) {
280
+ fs.writeFileSync(path.join(scriptsDir, script.filename), script.content, 'utf-8');
281
+ }
282
+ }
283
+ }
284
+ }
285
+ // 4. Write MCP configuration if provided
286
+ if (params.mcp) {
287
+ const mcpPath = path.join(pluginDir, 'mcp.json');
288
+ const mcpData = {
289
+ $schema: 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json',
290
+ ...params.mcp,
291
+ };
292
+ fs.writeFileSync(mcpPath, JSON.stringify(mcpData, null, 2), 'utf-8');
293
+ }
294
+ // 5. Load into runtime
295
+ const baseDataDir = path.resolve(pluginsConfig?.dataDir || './data/plugin_data');
296
+ const disabledSet = new Set(pluginsConfig?.disabledPlugins || []);
297
+ const pkg = await this.loadPluginFromDirectory(pluginDir, baseDataDir, disabledSet, toolRegistry);
298
+ if (!pkg) {
299
+ return {
300
+ success: false,
301
+ error: `Plugin files were created at "${pluginDir}", but failed to load into runtime.`,
302
+ skillsLoaded: 0,
303
+ toolsRegistered: [],
304
+ };
305
+ }
306
+ const pluginSkills = this.skills.filter((s) => s.pluginName === pluginName);
307
+ const activeMcpServers = this.mcpBridge.getActiveServers().filter((s) => s.server.pluginName === pluginName);
308
+ const toolsRegistered = activeMcpServers.flatMap((s) => s.tools.map((t) => t.name));
309
+ return {
310
+ success: true,
311
+ plugin: pkg,
312
+ skillsLoaded: pluginSkills.length,
313
+ toolsRegistered,
314
+ };
315
+ }
316
+ catch (err) {
317
+ logger.error(`Failed to create plugin "${pluginName}":`, err);
318
+ return {
319
+ success: false,
320
+ error: `Plugin creation failed: ${err.message}`,
321
+ skillsLoaded: 0,
322
+ toolsRegistered: [],
323
+ };
324
+ }
325
+ }
326
+ /**
327
+ * Installs an Agent Plugin from a local directory path, Git repository URL (e.g. GitHub), or npm package,
328
+ * validates its specification conformance (https://agent-plugins.org), installs any package dependencies,
329
+ * and immediately activates it in the live Musonda Gateway runtime without restarting the server.
330
+ */
331
+ async installPlugin(params, toolRegistry) {
332
+ const rawSource = params.source?.trim();
333
+ if (!rawSource) {
334
+ return {
335
+ success: false,
336
+ error: 'Missing required "source" parameter. Provide a local directory path, Git repository URL, or package identifier.',
337
+ skillsLoaded: 0,
338
+ toolsRegistered: [],
339
+ };
340
+ }
341
+ const pluginsConfig = this.config.plugins;
342
+ const configuredDirs = pluginsConfig?.directories || ['./data/plugins', './plugins'];
343
+ const searchDirs = configuredDirs.map((d) => path.resolve(d));
344
+ const basePluginsDir = searchDirs[0] || path.resolve('./plugins');
345
+ if (!fs.existsSync(basePluginsDir)) {
346
+ try {
347
+ fs.mkdirSync(basePluginsDir, { recursive: true });
348
+ }
349
+ catch (err) {
350
+ logger.error(`Could not create base plugins directory "${basePluginsDir}": ${err.message}`);
351
+ }
352
+ }
353
+ const isInsideAllowedDir = (targetPath) => {
354
+ const resolvedTarget = path.resolve(targetPath);
355
+ return searchDirs.some((allowedDir) => {
356
+ const rel = path.relative(allowedDir, resolvedTarget);
357
+ return !rel.startsWith('..') && !path.isAbsolute(rel) && rel.length > 0;
358
+ });
359
+ };
360
+ let targetPluginDir;
361
+ let sourceIsLocal = false;
362
+ let resolvedSourcePath = '';
363
+ // Check if source is a local directory
364
+ const candidateLocal = path.resolve(rawSource);
365
+ if (fs.existsSync(candidateLocal) && fs.statSync(candidateLocal).isDirectory()) {
366
+ sourceIsLocal = true;
367
+ resolvedSourcePath = candidateLocal;
368
+ }
369
+ if (sourceIsLocal) {
370
+ const manifestPath = path.join(resolvedSourcePath, 'plugin.json');
371
+ if (!fs.existsSync(manifestPath)) {
372
+ return {
373
+ success: false,
374
+ error: `Source directory "${resolvedSourcePath}" is missing required "plugin.json" manifest (https://agent-plugins.org).`,
375
+ skillsLoaded: 0,
376
+ toolsRegistered: [],
377
+ };
378
+ }
379
+ let parsedManifest;
380
+ try {
381
+ parsedManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
382
+ }
383
+ catch (err) {
384
+ return {
385
+ success: false,
386
+ error: `Invalid JSON in manifest "${manifestPath}": ${err.message}`,
387
+ skillsLoaded: 0,
388
+ toolsRegistered: [],
389
+ };
390
+ }
391
+ const pluginName = params.name || parsedManifest.name;
392
+ const nameValidation = ManifestValidator.validatePluginName(pluginName);
393
+ if (!nameValidation.valid) {
394
+ return {
395
+ success: false,
396
+ error: `Invalid plugin name "${pluginName}": ${nameValidation.error}`,
397
+ skillsLoaded: 0,
398
+ toolsRegistered: [],
399
+ };
400
+ }
401
+ if (params.targetDir) {
402
+ const candidateTarget = path.resolve(params.targetDir);
403
+ if (!isInsideAllowedDir(candidateTarget)) {
404
+ return {
405
+ success: false,
406
+ error: `Security violation: targetDir "${params.targetDir}" must resolve strictly within a configured plugin directory (${searchDirs.join(', ')}).`,
407
+ skillsLoaded: 0,
408
+ toolsRegistered: [],
409
+ };
410
+ }
411
+ targetPluginDir = candidateTarget;
412
+ }
413
+ else {
414
+ targetPluginDir = path.join(basePluginsDir, pluginName);
415
+ }
416
+ // Copy directory if not already at target location
417
+ if (path.resolve(resolvedSourcePath) !== path.resolve(targetPluginDir)) {
418
+ if (this.plugins.has(pluginName)) {
419
+ await this.unloadPlugin(pluginName, toolRegistry);
420
+ }
421
+ if (fs.existsSync(targetPluginDir)) {
422
+ if (!isInsideAllowedDir(targetPluginDir)) {
423
+ return {
424
+ success: false,
425
+ error: `Security violation: Refusing to delete unsafe target directory "${targetPluginDir}".`,
426
+ skillsLoaded: 0,
427
+ toolsRegistered: [],
428
+ };
429
+ }
430
+ fs.rmSync(targetPluginDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
431
+ }
432
+ fs.mkdirSync(path.dirname(targetPluginDir), { recursive: true });
433
+ fs.cpSync(resolvedSourcePath, targetPluginDir, { recursive: true });
434
+ }
435
+ }
436
+ else {
437
+ // Treat as Git repository or remote URL
438
+ let gitUrl = rawSource;
439
+ if (gitUrl.startsWith('github:')) {
440
+ gitUrl = `https://github.com/${gitUrl.slice(7)}.git`;
441
+ }
442
+ else if (/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+$/.test(gitUrl)) {
443
+ gitUrl = `https://github.com/${gitUrl}.git`;
444
+ }
445
+ let derivedName = params.name;
446
+ if (!derivedName) {
447
+ const urlParts = gitUrl.replace(/\/+$/, '').split('/');
448
+ const lastPart = urlParts[urlParts.length - 1] || 'plugin';
449
+ derivedName = lastPart.replace(/\.git$/, '').toLowerCase().replace(/[^a-z0-9.-]/g, '-');
450
+ }
451
+ if (params.targetDir) {
452
+ const candidateTarget = path.resolve(params.targetDir);
453
+ if (!isInsideAllowedDir(candidateTarget)) {
454
+ return {
455
+ success: false,
456
+ error: `Security violation: targetDir "${params.targetDir}" must resolve strictly within a configured plugin directory (${searchDirs.join(', ')}).`,
457
+ skillsLoaded: 0,
458
+ toolsRegistered: [],
459
+ };
460
+ }
461
+ targetPluginDir = candidateTarget;
462
+ }
463
+ else {
464
+ targetPluginDir = path.join(basePluginsDir, derivedName);
465
+ }
466
+ if (this.plugins.has(derivedName)) {
467
+ await this.unloadPlugin(derivedName, toolRegistry);
468
+ }
469
+ if (fs.existsSync(targetPluginDir)) {
470
+ if (!isInsideAllowedDir(targetPluginDir)) {
471
+ return {
472
+ success: false,
473
+ error: `Security violation: Refusing to delete unsafe target directory "${targetPluginDir}".`,
474
+ skillsLoaded: 0,
475
+ toolsRegistered: [],
476
+ };
477
+ }
478
+ fs.rmSync(targetPluginDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
479
+ }
480
+ fs.mkdirSync(path.dirname(targetPluginDir), { recursive: true });
481
+ try {
482
+ logger.info(`Cloning Git repository "${gitUrl}" into "${targetPluginDir}"...`);
483
+ execFileSync('git', ['clone', '--depth', '1', gitUrl, targetPluginDir], {
484
+ stdio: 'pipe',
485
+ timeout: 60000,
486
+ });
487
+ }
488
+ catch (err) {
489
+ const stderrMsg = err.stderr?.toString() || err.message;
490
+ return {
491
+ success: false,
492
+ error: `Failed to clone Git repository from "${gitUrl}": ${stderrMsg}`,
493
+ skillsLoaded: 0,
494
+ toolsRegistered: [],
495
+ };
496
+ }
497
+ }
498
+ // Validate the plugin at targetPluginDir
499
+ const manifestResult = ManifestValidator.loadFromDirectory(targetPluginDir);
500
+ if (!manifestResult.valid || !manifestResult.manifest) {
501
+ if (!sourceIsLocal) {
502
+ if (isInsideAllowedDir(targetPluginDir)) {
503
+ fs.rmSync(targetPluginDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
504
+ }
505
+ }
506
+ return {
507
+ success: false,
508
+ error: `Plugin manifest validation failed for "${targetPluginDir}": ${manifestResult.errors.join('; ')}`,
509
+ skillsLoaded: 0,
510
+ toolsRegistered: [],
511
+ };
512
+ }
513
+ const pluginName = manifestResult.manifest.name;
514
+ // Install production dependencies if package.json has dependencies and node_modules is missing
515
+ // Security: always supply --ignore-scripts to avoid untrusted lifecycle code execution
516
+ if (params.installDependencies !== false) {
517
+ const packageJsonPath = path.join(targetPluginDir, 'package.json');
518
+ const nodeModulesPath = path.join(targetPluginDir, 'node_modules');
519
+ if (fs.existsSync(packageJsonPath) && !fs.existsSync(nodeModulesPath)) {
520
+ try {
521
+ const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
522
+ if (pkgJson.dependencies && Object.keys(pkgJson.dependencies).length > 0) {
523
+ logger.info(`Installing dependencies for plugin "${pluginName}"...`);
524
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
525
+ execFileSync(npmCmd, ['install', '--omit=dev', '--ignore-scripts'], {
526
+ cwd: targetPluginDir,
527
+ stdio: 'pipe',
528
+ shell: false,
529
+ timeout: 120000,
530
+ });
531
+ }
532
+ }
533
+ catch (err) {
534
+ logger.warn(`Could not install dependencies for plugin "${pluginName}": ${err.message}`);
535
+ }
536
+ }
537
+ }
538
+ // Load into runtime
539
+ const baseDataDir = path.resolve(pluginsConfig?.dataDir || './data/plugin_data');
540
+ const disabledSet = new Set(pluginsConfig?.disabledPlugins || []);
541
+ const pkg = await this.loadPluginFromDirectory(targetPluginDir, baseDataDir, disabledSet, toolRegistry);
542
+ if (!pkg) {
543
+ return {
544
+ success: false,
545
+ error: `Plugin was downloaded to "${targetPluginDir}", but failed to load into runtime. Check logs for details.`,
546
+ skillsLoaded: 0,
547
+ toolsRegistered: [],
548
+ };
549
+ }
550
+ const pluginSkills = this.skills.filter((s) => s.pluginName === pluginName);
551
+ const activeMcpServers = this.mcpBridge.getActiveServers().filter((s) => s.server.pluginName === pluginName);
552
+ const toolsRegistered = activeMcpServers.flatMap((s) => s.tools.map((t) => t.name));
553
+ return {
554
+ success: true,
555
+ plugin: pkg,
556
+ skillsLoaded: pluginSkills.length,
557
+ toolsRegistered,
558
+ installedPath: targetPluginDir,
559
+ };
560
+ }
561
+ /**
562
+ * Unloads and uninstalls an Agent Plugin: stops its MCP servers, removes bridged tools and skills
563
+ * from runtime, optionally deletes its files from disk, and optionally purges its persistent data directory.
564
+ */
565
+ async removePlugin(params, toolRegistry) {
566
+ const pluginName = params.pluginName?.trim();
567
+ if (!pluginName) {
568
+ return {
569
+ success: false,
570
+ error: 'Missing required "pluginName" parameter.',
571
+ pluginName: '',
572
+ unloaded: false,
573
+ deletedFiles: false,
574
+ purgedData: false,
575
+ skillsRemoved: [],
576
+ toolsUnregistered: [],
577
+ };
578
+ }
579
+ const existing = this.plugins.get(pluginName);
580
+ const skillsRemoved = this.skills.filter((s) => s.pluginName === pluginName).map((s) => s.name);
581
+ const activeMcpServers = this.mcpBridge.getActiveServers().filter((s) => s.server.pluginName === pluginName);
582
+ const toolsUnregistered = activeMcpServers.flatMap((s) => s.tools.map((t) => t.name));
583
+ // Determine root directory
584
+ let rootDir = existing?.rootDir;
585
+ const pluginsConfig = this.config.plugins;
586
+ const searchDirs = pluginsConfig?.directories || ['./data/plugins', './plugins'];
587
+ if (!rootDir) {
588
+ for (const searchDir of searchDirs) {
589
+ const candidateDir = path.resolve(searchDir, pluginName);
590
+ if (fs.existsSync(candidateDir)) {
591
+ rootDir = candidateDir;
592
+ break;
593
+ }
594
+ }
595
+ }
596
+ // Determine data directory
597
+ const baseDataDir = path.resolve(pluginsConfig?.dataDir || './data/plugin_data');
598
+ const dataDir = existing?.dataDir || path.join(baseDataDir, pluginName);
599
+ if (!existing && (!rootDir || !fs.existsSync(rootDir))) {
600
+ return {
601
+ success: false,
602
+ error: `Plugin "${pluginName}" not found in active runtime or on disk in configured directories.`,
603
+ pluginName,
604
+ unloaded: false,
605
+ deletedFiles: false,
606
+ purgedData: false,
607
+ skillsRemoved: [],
608
+ toolsUnregistered: [],
609
+ };
610
+ }
611
+ // 1. Unload from live runtime
612
+ let unloaded = false;
613
+ if (existing) {
614
+ unloaded = await this.unloadPlugin(pluginName, toolRegistry);
615
+ }
616
+ const allowedPluginDirs = searchDirs.map((d) => path.resolve(d));
617
+ const isInside = (candidate, allowedRoots) => {
618
+ const resolved = path.resolve(candidate);
619
+ return allowedRoots.some((root) => {
620
+ const rel = path.relative(root, resolved);
621
+ return !rel.startsWith('..') && !path.isAbsolute(rel) && rel.length > 0;
622
+ });
623
+ };
624
+ // 2. Delete plugin files from disk
625
+ let deletedFiles = false;
626
+ if (params.deleteFiles !== false && rootDir && fs.existsSync(rootDir)) {
627
+ if (!isInside(rootDir, allowedPluginDirs)) {
628
+ logger.error(`Security error: Refusing to delete plugin root directory "${rootDir}" outside allowed directories.`);
629
+ }
630
+ else {
631
+ for (let i = 0; i < 6; i++) {
632
+ try {
633
+ fs.rmSync(rootDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 150 });
634
+ deletedFiles = true;
635
+ logger.info(`[Plugin: ${pluginName}] Deleted package directory: "${rootDir}"`);
636
+ break;
637
+ }
638
+ catch (err) {
639
+ if (i === 5) {
640
+ logger.warn(`Could not delete directory "${rootDir}" for plugin "${pluginName}": ${err.message}`);
641
+ }
642
+ else {
643
+ await new Promise((r) => setTimeout(r, 250));
644
+ }
645
+ }
646
+ }
647
+ }
648
+ }
649
+ // 3. Purge persistent data directory if requested
650
+ let purgedData = false;
651
+ if (params.purgeData === true && dataDir && fs.existsSync(dataDir)) {
652
+ if (!isInside(dataDir, [baseDataDir])) {
653
+ logger.error(`Security error: Refusing to delete plugin data directory "${dataDir}" outside base data directory.`);
654
+ }
655
+ else {
656
+ for (let i = 0; i < 6; i++) {
657
+ try {
658
+ fs.rmSync(dataDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 150 });
659
+ purgedData = true;
660
+ logger.info(`[Plugin: ${pluginName}] Purged persistent data directory: "${dataDir}"`);
661
+ break;
662
+ }
663
+ catch (err) {
664
+ if (i === 5) {
665
+ logger.warn(`Could not purge data directory "${dataDir}" for plugin "${pluginName}": ${err.message}`);
666
+ }
667
+ else {
668
+ await new Promise((r) => setTimeout(r, 250));
669
+ }
670
+ }
671
+ }
672
+ }
673
+ }
674
+ return {
675
+ success: true,
676
+ pluginName,
677
+ unloaded,
678
+ deletedFiles,
679
+ purgedData,
680
+ skillsRemoved,
681
+ toolsUnregistered,
682
+ };
683
+ }
684
+ /**
685
+ * Discovers Agent Plugins locally on disk and across the ecosystem (official repositories, GitHub, npm).
686
+ */
687
+ async findPlugins(params = {}) {
688
+ const query = params.query?.trim().toLowerCase() || '';
689
+ const source = params.source || 'all';
690
+ const localResults = [];
691
+ const remoteResults = [];
692
+ // 1. Scan Local Plugins
693
+ if (source === 'all' || source === 'local') {
694
+ const pluginsConfig = this.config.plugins;
695
+ const searchDirs = pluginsConfig?.directories || ['./data/plugins', './plugins'];
696
+ const seenDirs = new Set();
697
+ for (const sDir of searchDirs) {
698
+ const resolvedDir = path.resolve(sDir);
699
+ if (!fs.existsSync(resolvedDir))
700
+ continue;
701
+ let entries = [];
702
+ try {
703
+ entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
704
+ }
705
+ catch {
706
+ continue;
707
+ }
708
+ for (const entry of entries) {
709
+ if (!entry.isDirectory())
710
+ continue;
711
+ const candidatePath = path.join(resolvedDir, entry.name);
712
+ if (seenDirs.has(candidatePath))
713
+ continue;
714
+ seenDirs.add(candidatePath);
715
+ const manifestPath = path.join(candidatePath, 'plugin.json');
716
+ if (!fs.existsSync(manifestPath))
717
+ continue;
718
+ try {
719
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
720
+ const pName = manifest.name || entry.name;
721
+ const pDesc = manifest.description || '';
722
+ const pKeywords = manifest.keywords || [];
723
+ if (query &&
724
+ !pName.toLowerCase().includes(query) &&
725
+ !pDesc.toLowerCase().includes(query) &&
726
+ !pKeywords.some((k) => k.toLowerCase().includes(query))) {
727
+ continue;
728
+ }
729
+ // Inspect skills in skills/
730
+ const skillDir = path.join(candidatePath, 'skills');
731
+ const skills = [];
732
+ if (fs.existsSync(skillDir)) {
733
+ try {
734
+ const sEntries = fs.readdirSync(skillDir, { withFileTypes: true });
735
+ for (const se of sEntries) {
736
+ if (se.isDirectory() && fs.existsSync(path.join(skillDir, se.name, 'SKILL.md'))) {
737
+ skills.push(se.name);
738
+ }
739
+ }
740
+ }
741
+ catch { }
742
+ }
743
+ // Inspect MCP servers in mcp.json
744
+ const mcpPath = path.join(candidatePath, 'mcp.json');
745
+ const mcpServers = [];
746
+ if (fs.existsSync(mcpPath)) {
747
+ try {
748
+ const mcpDoc = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
749
+ if (mcpDoc.mcpServers) {
750
+ mcpServers.push(...Object.keys(mcpDoc.mcpServers));
751
+ }
752
+ }
753
+ catch { }
754
+ }
755
+ localResults.push({
756
+ name: pName,
757
+ version: manifest.version || '1.0.0',
758
+ description: pDesc,
759
+ isLoaded: this.plugins.has(pName),
760
+ path: candidatePath,
761
+ skills,
762
+ mcpServers,
763
+ });
764
+ }
765
+ catch { }
766
+ }
767
+ }
768
+ }
769
+ // 2. Scan Remote & Curated Ecosystem Plugins
770
+ if (source === 'all' || source === 'remote') {
771
+ const CURATED_PLUGINS = [
772
+ {
773
+ name: 'agent-plugins-example',
774
+ description: 'Official Agent Plugins reference implementation featuring Agent Skills and stdio MCP servers.',
775
+ repository: 'https://github.com/agentplugins/agent-plugins-example',
776
+ url: 'https://agent-plugins.org/',
777
+ author: 'Agent Plugins Core Maintainers (Amazon, Cursor, Microsoft, OpenAI, Vercel)',
778
+ keywords: ['example', 'reference', 'skills', 'mcp', 'stdio', 'agent-plugins'],
779
+ official: true,
780
+ },
781
+ {
782
+ name: 'agent-plugins-spec',
783
+ description: 'The complete normative contract, JSON schemas, and technical specifications for Agent Plugins 1.0.0.',
784
+ repository: 'https://github.com/agentplugins/agent-plugins-spec',
785
+ url: 'https://agent-plugins.org/specification',
786
+ author: 'Agent Plugins Technical Steering Committee',
787
+ keywords: ['specification', 'spec', 'schemas', 'standards', 'agent-plugins'],
788
+ official: true,
789
+ },
790
+ {
791
+ name: 'agent-plugins-site',
792
+ description: 'Documentation, guides, and compatible client catalog for Agent Plugins.',
793
+ repository: 'https://github.com/agentplugins/agent-plugins-site',
794
+ url: 'https://agent-plugins.org/',
795
+ author: 'Agent Plugins Documentation Contributors',
796
+ keywords: ['docs', 'documentation', 'compatible-clients', 'agent-plugins'],
797
+ official: true,
798
+ },
799
+ {
800
+ name: 'example-calc',
801
+ description: 'Calculator and math service demonstrating Agent Skills and MCP stdio tools in Musonda.',
802
+ repository: 'https://github.com/agentplugins',
803
+ url: 'https://agent-plugins.org/',
804
+ author: 'Musonda Team',
805
+ keywords: ['calculator', 'math', 'stdio', 'mcp', 'tools'],
806
+ official: true,
807
+ },
808
+ {
809
+ name: 'plugin-builder',
810
+ description: 'Meta-plugin providing guidelines, schemas, and skills for authoring, scaffolding, and dynamically managing Agent Plugins.',
811
+ repository: 'https://github.com/agentplugins',
812
+ url: 'https://agent-plugins.org/',
813
+ author: 'Musonda Team',
814
+ keywords: ['builder', 'authoring', 'management', 'developer-tools', 'scaffolding'],
815
+ official: true,
816
+ },
817
+ ];
818
+ const seenRepos = new Set();
819
+ // Filter curated list
820
+ for (const p of CURATED_PLUGINS) {
821
+ seenRepos.add(p.repository.toLowerCase());
822
+ if (!query ||
823
+ p.name.toLowerCase().includes(query) ||
824
+ p.description.toLowerCase().includes(query) ||
825
+ p.keywords.some((k) => k.toLowerCase().includes(query))) {
826
+ remoteResults.push({
827
+ ...p,
828
+ isInstalled: this.plugins.has(p.name),
829
+ });
830
+ }
831
+ }
832
+ // Query GitHub Search API for topic:agent-plugin
833
+ if (query) {
834
+ try {
835
+ const ghRes = await fetch(`https://api.github.com/search/repositories?q=topic:agent-plugin+${encodeURIComponent(query)}&per_page=5`, {
836
+ headers: {
837
+ 'User-Agent': 'Musonda-Gateway/1.0.0',
838
+ Accept: 'application/vnd.github.v3+json',
839
+ },
840
+ signal: AbortSignal.timeout(3500),
841
+ });
842
+ if (ghRes.ok) {
843
+ const data = await ghRes.json();
844
+ if (data.items && Array.isArray(data.items)) {
845
+ for (const item of data.items) {
846
+ const repoUrl = (item.html_url || '').toLowerCase();
847
+ if (seenRepos.has(repoUrl))
848
+ continue;
849
+ seenRepos.add(repoUrl);
850
+ remoteResults.push({
851
+ name: item.name,
852
+ description: item.description || '',
853
+ repository: item.html_url,
854
+ url: item.homepage || item.html_url,
855
+ author: item.owner?.login || '',
856
+ keywords: item.topics || ['agent-plugin'],
857
+ official: item.owner?.login === 'agentplugins',
858
+ isInstalled: this.plugins.has(item.name),
859
+ });
860
+ }
861
+ }
862
+ }
863
+ }
864
+ catch {
865
+ // Graceful fallback if network is offline
866
+ }
867
+ }
868
+ }
869
+ return {
870
+ query: query || undefined,
871
+ totalFound: localResults.length + remoteResults.length,
872
+ local: localResults,
873
+ remote: remoteResults,
874
+ };
875
+ }
876
+ /**
877
+ * Returns detailed inspection information for a specific plugin.
878
+ */
879
+ getPluginInfo(pluginNameOrPath) {
880
+ const raw = pluginNameOrPath?.trim();
881
+ if (!raw) {
882
+ return {
883
+ found: false,
884
+ isLoaded: false,
885
+ skills: [],
886
+ mcpServers: [],
887
+ error: 'Missing plugin identifier or path.',
888
+ };
889
+ }
890
+ let rootDir;
891
+ const loaded = this.plugins.get(raw);
892
+ if (loaded) {
893
+ rootDir = loaded.rootDir;
894
+ }
895
+ else {
896
+ const candidateLocal = path.resolve(raw);
897
+ if (fs.existsSync(candidateLocal) && fs.statSync(candidateLocal).isDirectory()) {
898
+ rootDir = candidateLocal;
899
+ }
900
+ else {
901
+ const pluginsConfig = this.config.plugins;
902
+ const searchDirs = pluginsConfig?.directories || ['./data/plugins', './plugins'];
903
+ for (const sDir of searchDirs) {
904
+ const cand = path.resolve(sDir, raw);
905
+ if (fs.existsSync(cand)) {
906
+ rootDir = cand;
907
+ break;
908
+ }
909
+ }
910
+ }
911
+ }
912
+ if (!rootDir || !fs.existsSync(rootDir)) {
913
+ return {
914
+ found: false,
915
+ isLoaded: false,
916
+ skills: [],
917
+ mcpServers: [],
918
+ error: `Plugin "${raw}" not found in loaded plugins or on disk.`,
919
+ };
920
+ }
921
+ const manifestPath = path.join(rootDir, 'plugin.json');
922
+ if (!fs.existsSync(manifestPath)) {
923
+ return {
924
+ found: false,
925
+ isLoaded: false,
926
+ skills: [],
927
+ mcpServers: [],
928
+ error: `Directory "${rootDir}" does not contain a valid plugin.json manifest.`,
929
+ };
930
+ }
931
+ let manifest;
932
+ try {
933
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
934
+ }
935
+ catch (err) {
936
+ return {
937
+ found: false,
938
+ isLoaded: false,
939
+ skills: [],
940
+ mcpServers: [],
941
+ error: `Could not parse plugin.json: ${err.message}`,
942
+ };
943
+ }
944
+ const pluginName = manifest.name || path.basename(rootDir);
945
+ const isLoaded = this.plugins.has(pluginName);
946
+ // Read skills
947
+ const skillsList = [];
948
+ const skillsDir = path.join(rootDir, 'skills');
949
+ if (fs.existsSync(skillsDir)) {
950
+ try {
951
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
952
+ for (const entry of entries) {
953
+ if (!entry.isDirectory())
954
+ continue;
955
+ const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
956
+ if (fs.existsSync(skillFile)) {
957
+ try {
958
+ const content = fs.readFileSync(skillFile, 'utf-8');
959
+ const parsed = SkillFrontmatterParser.parse(content, entry.name);
960
+ const summary = parsed.body.slice(0, 150).replace(/\n+/g, ' ');
961
+ skillsList.push({
962
+ name: parsed.frontmatter.name || entry.name,
963
+ description: parsed.frontmatter.description || '',
964
+ instructionsSummary: summary ? summary + '...' : undefined,
965
+ });
966
+ }
967
+ catch { }
968
+ }
969
+ }
970
+ }
971
+ catch { }
972
+ }
973
+ // Read MCP servers
974
+ const mcpServersList = [];
975
+ const mcpPath = path.join(rootDir, 'mcp.json');
976
+ if (fs.existsSync(mcpPath)) {
977
+ try {
978
+ const mcpDoc = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
979
+ if (mcpDoc.mcpServers) {
980
+ for (const [sName, sCfg] of Object.entries(mcpDoc.mcpServers)) {
981
+ mcpServersList.push({
982
+ name: sName,
983
+ type: sCfg.type,
984
+ command: sCfg.command,
985
+ url: sCfg.url,
986
+ });
987
+ }
988
+ }
989
+ }
990
+ catch { }
991
+ }
992
+ // Active bridged tools if currently loaded
993
+ const activeServers = this.mcpBridge.getActiveServers().filter((s) => s.server.pluginName === pluginName);
994
+ const activeTools = activeServers.flatMap((s) => s.tools.map((t) => t.name));
995
+ return {
996
+ found: true,
997
+ isLoaded,
998
+ manifest,
999
+ rootDir,
1000
+ dataDir: loaded?.dataDir || path.resolve(this.config.plugins?.dataDir || './data/plugin_data', pluginName),
1001
+ skills: skillsList,
1002
+ mcpServers: mcpServersList,
1003
+ activeTools,
1004
+ };
1005
+ }
1006
+ /**
1007
+ * Shuts down all active plugin resources (MCP subprocesses).
1008
+ */
1009
+ async shutdownAll() {
1010
+ await this.mcpBridge.shutdown();
1011
+ this.plugins.clear();
1012
+ this.skills = [];
1013
+ this.isLoaded = false;
1014
+ }
1015
+ }
1016
+ //# sourceMappingURL=pluginManager.js.map