neuro-cli 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,887 @@
1
+ // ============================================================
2
+ // NeuroCLI - Plugin Bundle System
3
+ // Packages Skills + Hooks + MCP servers into installable units
4
+ // ============================================================
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, copyFileSync, statSync, } from 'fs';
6
+ import { join, basename, dirname, resolve, extname } from 'path';
7
+ import { homedir } from 'os';
8
+ import { createHash } from 'crypto';
9
+ import { execSync } from 'child_process';
10
+ import { pipeline as pipelineCallback } from 'stream';
11
+ import { promisify } from 'util';
12
+ const pipelineAsync = promisify(pipelineCallback);
13
+ // ---- Constants ----
14
+ const BUNDLES_DIR = join(homedir(), '.neuro', 'bundles');
15
+ const REGISTRY_FILE = join(homedir(), '.neuro', 'bundle-registry.json');
16
+ const MANIFEST_FILE = 'bundle.json';
17
+ const SKILLS_DIR = 'skills';
18
+ const HOOKS_DIR = 'hooks';
19
+ const MCP_DIR = 'mcp';
20
+ const TOOLS_DIR = 'tools';
21
+ // ---- Helper Functions ----
22
+ function ensureDir(dir) {
23
+ if (!existsSync(dir)) {
24
+ mkdirSync(dir, { recursive: true });
25
+ }
26
+ }
27
+ function computeChecksum(filePath) {
28
+ const content = readFileSync(filePath);
29
+ return createHash('sha256').update(content).digest('hex');
30
+ }
31
+ function computeDirectoryChecksum(dirPath) {
32
+ const hash = createHash('sha256');
33
+ function walk(dir) {
34
+ const entries = readdirSync(dir, { withFileTypes: true });
35
+ // Sort for deterministic ordering
36
+ entries.sort((a, b) => a.name.localeCompare(b.name));
37
+ for (const entry of entries) {
38
+ const fullPath = join(dir, entry.name);
39
+ if (entry.isDirectory()) {
40
+ hash.update(`dir:${entry.name}`);
41
+ walk(fullPath);
42
+ }
43
+ else if (entry.isFile()) {
44
+ const content = readFileSync(fullPath);
45
+ hash.update(`file:${entry.name}:${content.length}:`);
46
+ hash.update(content);
47
+ }
48
+ }
49
+ }
50
+ walk(dirPath);
51
+ return hash.digest('hex');
52
+ }
53
+ function readJSONFile(filePath) {
54
+ if (!existsSync(filePath))
55
+ return null;
56
+ try {
57
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function writeJSONFile(filePath, data) {
64
+ ensureDir(dirname(filePath));
65
+ writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
66
+ }
67
+ function copyRecursive(src, dest) {
68
+ ensureDir(dest);
69
+ const entries = readdirSync(src, { withFileTypes: true });
70
+ for (const entry of entries) {
71
+ const srcPath = join(src, entry.name);
72
+ const destPath = join(dest, entry.name);
73
+ if (entry.isDirectory()) {
74
+ copyRecursive(srcPath, destPath);
75
+ }
76
+ else {
77
+ copyFileSync(srcPath, destPath);
78
+ }
79
+ }
80
+ }
81
+ function execCommand(command, options) {
82
+ try {
83
+ const stdout = execSync(command, {
84
+ encoding: 'utf-8',
85
+ cwd: options?.cwd ?? process.cwd(),
86
+ timeout: options?.timeout ?? 30000,
87
+ maxBuffer: 10 * 1024 * 1024,
88
+ env: { ...process.env, NO_COLOR: '1' },
89
+ });
90
+ return { stdout: stdout.trim(), stderr: '', exitCode: 0 };
91
+ }
92
+ catch (error) {
93
+ return {
94
+ stdout: error.stdout?.toString().trim() ?? '',
95
+ stderr: error.stderr?.toString().trim() ?? '',
96
+ exitCode: error.status ?? 1,
97
+ };
98
+ }
99
+ }
100
+ // ---- PluginBundleManager Class ----
101
+ export class PluginBundleManager {
102
+ bundlesDir;
103
+ registryFile;
104
+ installedBundles = new Map();
105
+ constructor(bundlesDir) {
106
+ this.bundlesDir = bundlesDir ?? BUNDLES_DIR;
107
+ this.registryFile = REGISTRY_FILE;
108
+ ensureDir(this.bundlesDir);
109
+ this.loadInstalledBundles();
110
+ }
111
+ // ---- Installation ----
112
+ /**
113
+ * Install a plugin bundle from URL, path, or registry
114
+ */
115
+ async installBundle(source) {
116
+ const warnings = [];
117
+ // Determine source type
118
+ if (source.startsWith('http://') || source.startsWith('https://')) {
119
+ return this.installFromURL(source);
120
+ }
121
+ if (source.startsWith('registry:') || !source.includes('/') && !source.includes('\\')) {
122
+ const bundleId = source.replace(/^registry:/, '');
123
+ return this.installFromRegistry(bundleId);
124
+ }
125
+ // Local path
126
+ return this.installFromPath(source, warnings);
127
+ }
128
+ /**
129
+ * Uninstall a plugin bundle
130
+ */
131
+ uninstallBundle(bundleId) {
132
+ const bundle = this.installedBundles.get(bundleId);
133
+ if (!bundle) {
134
+ return { success: false, error: `Bundle "${bundleId}" is not installed` };
135
+ }
136
+ // Remove bundle directory
137
+ try {
138
+ if (existsSync(bundle.installPath)) {
139
+ rmSync(bundle.installPath, { recursive: true, force: true });
140
+ }
141
+ }
142
+ catch (error) {
143
+ return { success: false, error: `Failed to remove bundle files: ${error instanceof Error ? error.message : String(error)}` };
144
+ }
145
+ // Remove from installed bundles
146
+ this.installedBundles.delete(bundleId);
147
+ this.saveInstalledBundles();
148
+ // Run uninstall hooks if present
149
+ this.runLifecycleHook(bundle, 'uninstall');
150
+ return { success: true };
151
+ }
152
+ /**
153
+ * List all installed bundles
154
+ */
155
+ listBundles() {
156
+ return Array.from(this.installedBundles.values()).map(bundle => ({
157
+ id: bundle.manifest.id,
158
+ name: bundle.manifest.name,
159
+ version: bundle.manifest.version,
160
+ description: bundle.manifest.description,
161
+ author: bundle.manifest.author,
162
+ enabled: bundle.enabled,
163
+ source: bundle.source,
164
+ }));
165
+ }
166
+ /**
167
+ * Get bundle details
168
+ */
169
+ getBundleInfo(bundleId) {
170
+ return this.installedBundles.get(bundleId) ?? null;
171
+ }
172
+ /**
173
+ * Update a bundle to the latest version
174
+ */
175
+ async updateBundle(bundleId) {
176
+ const bundle = this.installedBundles.get(bundleId);
177
+ if (!bundle) {
178
+ return { success: false, error: `Bundle "${bundleId}" is not installed` };
179
+ }
180
+ const previousVersion = bundle.manifest.version;
181
+ // Re-install from the original source
182
+ const installResult = await this.installBundle(bundle.source);
183
+ if (!installResult.success) {
184
+ return {
185
+ success: false,
186
+ previousVersion,
187
+ error: installResult.error ?? 'Update failed',
188
+ };
189
+ }
190
+ const newBundle = this.installedBundles.get(bundleId);
191
+ const newVersion = newBundle?.manifest.version ?? previousVersion;
192
+ return {
193
+ success: true,
194
+ previousVersion,
195
+ newVersion,
196
+ };
197
+ }
198
+ /**
199
+ * Validate a bundle before install
200
+ */
201
+ validateBundle(bundlePath) {
202
+ const errors = [];
203
+ const warnings = [];
204
+ // Check path exists
205
+ if (!existsSync(bundlePath)) {
206
+ errors.push({
207
+ code: 'PATH_NOT_FOUND',
208
+ message: `Bundle path does not exist: ${bundlePath}`,
209
+ });
210
+ return { valid: false, errors, warnings };
211
+ }
212
+ // Check manifest exists
213
+ const manifestPath = join(bundlePath, MANIFEST_FILE);
214
+ if (!existsSync(manifestPath)) {
215
+ errors.push({
216
+ code: 'MANIFEST_MISSING',
217
+ message: `No ${MANIFEST_FILE} found in bundle`,
218
+ path: MANIFEST_FILE,
219
+ });
220
+ return { valid: false, errors, warnings };
221
+ }
222
+ // Parse manifest
223
+ let manifest;
224
+ try {
225
+ const raw = JSON.parse(readFileSync(manifestPath, 'utf-8'));
226
+ manifest = raw;
227
+ }
228
+ catch (error) {
229
+ errors.push({
230
+ code: 'MANIFEST_INVALID_JSON',
231
+ message: `Failed to parse ${MANIFEST_FILE}: ${error instanceof Error ? error.message : String(error)}`,
232
+ path: MANIFEST_FILE,
233
+ });
234
+ return { valid: false, errors, warnings };
235
+ }
236
+ // Validate manifest fields
237
+ if (!manifest.id || typeof manifest.id !== 'string') {
238
+ errors.push({
239
+ code: 'MANIFEST_MISSING_ID',
240
+ message: 'Bundle manifest must have a valid "id" field',
241
+ path: MANIFEST_FILE,
242
+ });
243
+ }
244
+ else if (!/^[a-zA-Z0-9_-]+$/.test(manifest.id)) {
245
+ errors.push({
246
+ code: 'MANIFEST_INVALID_ID',
247
+ message: 'Bundle ID must contain only alphanumeric characters, hyphens, and underscores',
248
+ path: MANIFEST_FILE,
249
+ });
250
+ }
251
+ if (!manifest.name || typeof manifest.name !== 'string') {
252
+ errors.push({
253
+ code: 'MANIFEST_MISSING_NAME',
254
+ message: 'Bundle manifest must have a "name" field',
255
+ path: MANIFEST_FILE,
256
+ });
257
+ }
258
+ if (!manifest.version || typeof manifest.version !== 'string') {
259
+ errors.push({
260
+ code: 'MANIFEST_MISSING_VERSION',
261
+ message: 'Bundle manifest must have a "version" field',
262
+ path: MANIFEST_FILE,
263
+ });
264
+ }
265
+ else if (!/^\d+\.\d+\.\d+/.test(manifest.version)) {
266
+ warnings.push({
267
+ code: 'MANIFEST_INVALID_SEMVER',
268
+ message: `Version "${manifest.version}" is not valid semver`,
269
+ path: MANIFEST_FILE,
270
+ });
271
+ }
272
+ if (!manifest.description || typeof manifest.description !== 'string') {
273
+ errors.push({
274
+ code: 'MANIFEST_MISSING_DESCRIPTION',
275
+ message: 'Bundle manifest must have a "description" field',
276
+ path: MANIFEST_FILE,
277
+ });
278
+ }
279
+ if (!manifest.author || typeof manifest.author !== 'string') {
280
+ errors.push({
281
+ code: 'MANIFEST_MISSING_AUTHOR',
282
+ message: 'Bundle manifest must have an "author" field',
283
+ path: MANIFEST_FILE,
284
+ });
285
+ }
286
+ if (!manifest.compatibility?.neuroVersion) {
287
+ warnings.push({
288
+ code: 'MANIFEST_MISSING_COMPATIBILITY',
289
+ message: 'No "compatibility.neuroVersion" specified - bundle may not work with all NeuroCLI versions',
290
+ path: MANIFEST_FILE,
291
+ });
292
+ }
293
+ // Validate referenced files exist
294
+ this.validateBundleReferences(bundlePath, manifest.skills, SKILLS_DIR, errors, warnings);
295
+ this.validateBundleReferences(bundlePath, manifest.hooks, HOOKS_DIR, errors, warnings);
296
+ this.validateBundleReferences(bundlePath, manifest.mcpServers, MCP_DIR, errors, warnings);
297
+ this.validateBundleReferences(bundlePath, manifest.tools, TOOLS_DIR, errors, warnings);
298
+ // Validate permissions
299
+ const validPermissions = [
300
+ 'file:read', 'file:write', 'file:delete',
301
+ 'bash:execute', 'network:access',
302
+ 'env:read', 'env:write',
303
+ 'git:access', 'mcp:connect',
304
+ ];
305
+ if (manifest.permissions) {
306
+ for (const perm of manifest.permissions) {
307
+ if (!validPermissions.includes(perm)) {
308
+ warnings.push({
309
+ code: 'UNKNOWN_PERMISSION',
310
+ message: `Unknown permission: "${perm}"`,
311
+ path: MANIFEST_FILE,
312
+ });
313
+ }
314
+ }
315
+ }
316
+ // Check for dangerous permissions
317
+ if (manifest.permissions?.includes('bash:execute')) {
318
+ warnings.push({
319
+ code: 'DANGEROUS_PERMISSION',
320
+ message: 'Bundle requests "bash:execute" permission - this allows arbitrary command execution',
321
+ path: MANIFEST_FILE,
322
+ });
323
+ }
324
+ if (manifest.permissions?.includes('network:access')) {
325
+ warnings.push({
326
+ code: 'SENSITIVE_PERMISSION',
327
+ message: 'Bundle requests "network:access" permission - this allows network connections',
328
+ path: MANIFEST_FILE,
329
+ });
330
+ }
331
+ // Check for hooks directory and executable scripts
332
+ const hooksDir = join(bundlePath, HOOKS_DIR);
333
+ if (existsSync(hooksDir)) {
334
+ const hookFiles = readdirSync(hooksDir);
335
+ for (const hookFile of hookFiles) {
336
+ const hookPath = join(hooksDir, hookFile);
337
+ const stat = statSync(hookPath);
338
+ if (stat.isFile() && !(stat.mode & 0o111)) {
339
+ warnings.push({
340
+ code: 'HOOK_NOT_EXECUTABLE',
341
+ message: `Hook script "${hookFile}" is not executable`,
342
+ path: join(HOOKS_DIR, hookFile),
343
+ });
344
+ }
345
+ }
346
+ }
347
+ // Check MCP configs are valid JSON
348
+ const mcpDir = join(bundlePath, MCP_DIR);
349
+ if (existsSync(mcpDir)) {
350
+ const mcpFiles = readdirSync(mcpDir).filter(f => f.endsWith('.json'));
351
+ for (const mcpFile of mcpFiles) {
352
+ const mcpPath = join(mcpDir, mcpFile);
353
+ try {
354
+ JSON.parse(readFileSync(mcpPath, 'utf-8'));
355
+ }
356
+ catch {
357
+ errors.push({
358
+ code: 'INVALID_MCP_CONFIG',
359
+ message: `MCP config "${mcpFile}" is not valid JSON`,
360
+ path: join(MCP_DIR, mcpFile),
361
+ });
362
+ }
363
+ }
364
+ }
365
+ return {
366
+ valid: errors.length === 0,
367
+ errors,
368
+ warnings,
369
+ manifest: errors.length === 0 ? manifest : undefined,
370
+ };
371
+ }
372
+ /**
373
+ * Create a new plugin bundle
374
+ */
375
+ createBundle(options) {
376
+ const { outputDir, manifest } = options;
377
+ const bundleDir = join(outputDir, manifest.id);
378
+ try {
379
+ // Create bundle structure
380
+ ensureDir(bundleDir);
381
+ ensureDir(join(bundleDir, SKILLS_DIR));
382
+ ensureDir(join(bundleDir, HOOKS_DIR));
383
+ ensureDir(join(bundleDir, MCP_DIR));
384
+ ensureDir(join(bundleDir, TOOLS_DIR));
385
+ // Write manifest
386
+ const fullManifest = {
387
+ ...manifest,
388
+ createdAt: new Date().toISOString(),
389
+ updatedAt: new Date().toISOString(),
390
+ };
391
+ writeJSONFile(join(bundleDir, MANIFEST_FILE), fullManifest);
392
+ // Write skills
393
+ if (options.skills) {
394
+ for (const [skillName, content] of Object.entries(options.skills)) {
395
+ const skillDir = join(bundleDir, SKILLS_DIR, skillName);
396
+ ensureDir(skillDir);
397
+ writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf-8');
398
+ }
399
+ }
400
+ // Write hooks
401
+ if (options.hooks) {
402
+ for (const [filename, content] of Object.entries(options.hooks)) {
403
+ const hookPath = join(bundleDir, HOOKS_DIR, filename);
404
+ writeFileSync(hookPath, content, 'utf-8');
405
+ // Make executable
406
+ try {
407
+ const stat = statSync(hookPath);
408
+ chmodSync(hookPath, stat.mode | 0o111);
409
+ }
410
+ catch {
411
+ // Best-effort chmod
412
+ }
413
+ }
414
+ }
415
+ // Write MCP server configs
416
+ if (options.mcpServers) {
417
+ for (const [filename, config] of Object.entries(options.mcpServers)) {
418
+ writeJSONFile(join(bundleDir, MCP_DIR, filename), config);
419
+ }
420
+ }
421
+ // Write tools
422
+ if (options.tools) {
423
+ for (const [filename, content] of Object.entries(options.tools)) {
424
+ writeFileSync(join(bundleDir, TOOLS_DIR, filename), content, 'utf-8');
425
+ }
426
+ }
427
+ return { success: true, path: bundleDir };
428
+ }
429
+ catch (error) {
430
+ return { success: false, error: `Failed to create bundle: ${error instanceof Error ? error.message : String(error)}` };
431
+ }
432
+ }
433
+ /**
434
+ * Publish a bundle to the registry
435
+ */
436
+ publishBundle(bundlePath) {
437
+ // Validate first
438
+ const validation = this.validateBundle(bundlePath);
439
+ if (!validation.valid) {
440
+ return {
441
+ success: false,
442
+ error: `Bundle validation failed: ${validation.errors.map(e => e.message).join('; ')}`,
443
+ };
444
+ }
445
+ const manifest = validation.manifest;
446
+ // Add to local registry
447
+ const registry = this.loadRegistry();
448
+ const checksum = computeDirectoryChecksum(bundlePath);
449
+ const entry = {
450
+ id: manifest.id,
451
+ name: manifest.name,
452
+ version: manifest.version,
453
+ description: manifest.description,
454
+ author: manifest.author,
455
+ url: `local://${bundlePath}`,
456
+ checksum,
457
+ keywords: manifest.keywords,
458
+ downloads: 0,
459
+ updatedAt: new Date().toISOString(),
460
+ };
461
+ // Check if already published
462
+ const existingIndex = registry.findIndex(e => e.id === manifest.id);
463
+ if (existingIndex >= 0) {
464
+ registry[existingIndex] = entry;
465
+ }
466
+ else {
467
+ registry.push(entry);
468
+ }
469
+ this.saveRegistry(registry);
470
+ return { success: true };
471
+ }
472
+ /**
473
+ * Search available bundles
474
+ */
475
+ searchBundles(query) {
476
+ const registry = this.loadRegistry();
477
+ const queryLower = query.toLowerCase();
478
+ const terms = queryLower.split(/\s+/);
479
+ return registry
480
+ .filter(entry => {
481
+ const searchText = [
482
+ entry.id,
483
+ entry.name,
484
+ entry.description,
485
+ entry.author,
486
+ ...(entry.keywords ?? []),
487
+ ]
488
+ .join(' ')
489
+ .toLowerCase();
490
+ return terms.every(term => searchText.includes(term));
491
+ })
492
+ .map(entry => ({
493
+ id: entry.id,
494
+ name: entry.name,
495
+ version: entry.version,
496
+ description: entry.description,
497
+ author: entry.author,
498
+ keywords: entry.keywords,
499
+ downloads: entry.downloads,
500
+ url: entry.url,
501
+ }))
502
+ .sort((a, b) => (b.downloads ?? 0) - (a.downloads ?? 0));
503
+ }
504
+ // ---- Enable / Disable ----
505
+ /**
506
+ * Enable a bundle
507
+ */
508
+ enableBundle(bundleId) {
509
+ const bundle = this.installedBundles.get(bundleId);
510
+ if (!bundle) {
511
+ return { success: false, error: `Bundle "${bundleId}" is not installed` };
512
+ }
513
+ bundle.enabled = true;
514
+ this.saveInstalledBundles();
515
+ // Run install hooks if present
516
+ this.runLifecycleHook(bundle, 'enable');
517
+ return { success: true };
518
+ }
519
+ /**
520
+ * Disable a bundle
521
+ */
522
+ disableBundle(bundleId) {
523
+ const bundle = this.installedBundles.get(bundleId);
524
+ if (!bundle) {
525
+ return { success: false, error: `Bundle "${bundleId}" is not installed` };
526
+ }
527
+ bundle.enabled = false;
528
+ this.saveInstalledBundles();
529
+ this.runLifecycleHook(bundle, 'disable');
530
+ return { success: true };
531
+ }
532
+ // ---- Skill / Hook / MCP Resolution ----
533
+ /**
534
+ * Get all skill paths from enabled bundles
535
+ */
536
+ getBundleSkillPaths() {
537
+ const results = [];
538
+ for (const [bundleId, bundle] of this.installedBundles) {
539
+ if (!bundle.enabled)
540
+ continue;
541
+ for (const skillRef of bundle.manifest.skills) {
542
+ const skillPath = join(bundle.installPath, skillRef);
543
+ if (existsSync(skillPath)) {
544
+ results.push({
545
+ bundleId,
546
+ skillPath,
547
+ skillName: basename(skillPath),
548
+ });
549
+ }
550
+ }
551
+ }
552
+ return results;
553
+ }
554
+ /**
555
+ * Get all hook scripts from enabled bundles
556
+ */
557
+ getBundleHooks() {
558
+ const results = [];
559
+ for (const [bundleId, bundle] of this.installedBundles) {
560
+ if (!bundle.enabled)
561
+ continue;
562
+ for (const hookRef of bundle.manifest.hooks) {
563
+ const hookPath = join(bundle.installPath, hookRef);
564
+ if (existsSync(hookPath)) {
565
+ results.push({
566
+ bundleId,
567
+ hookPath,
568
+ hookName: basename(hookRef),
569
+ });
570
+ }
571
+ }
572
+ }
573
+ return results;
574
+ }
575
+ /**
576
+ * Get all MCP server configs from enabled bundles
577
+ */
578
+ getBundleMCPServers() {
579
+ const results = [];
580
+ for (const [bundleId, bundle] of this.installedBundles) {
581
+ if (!bundle.enabled)
582
+ continue;
583
+ for (const mcpRef of bundle.manifest.mcpServers) {
584
+ const configPath = join(bundle.installPath, mcpRef);
585
+ if (existsSync(configPath)) {
586
+ const config = readJSONFile(configPath);
587
+ if (config) {
588
+ results.push({
589
+ bundleId,
590
+ configPath,
591
+ configName: basename(mcpRef),
592
+ config,
593
+ });
594
+ }
595
+ }
596
+ }
597
+ }
598
+ return results;
599
+ }
600
+ /**
601
+ * Get all tool scripts from enabled bundles
602
+ */
603
+ getBundleTools() {
604
+ const results = [];
605
+ for (const [bundleId, bundle] of this.installedBundles) {
606
+ if (!bundle.enabled)
607
+ continue;
608
+ for (const toolRef of bundle.manifest.tools) {
609
+ const toolPath = join(bundle.installPath, toolRef);
610
+ if (existsSync(toolPath)) {
611
+ results.push({
612
+ bundleId,
613
+ toolPath,
614
+ toolName: basename(toolRef, extname(toolRef)),
615
+ });
616
+ }
617
+ }
618
+ }
619
+ return results;
620
+ }
621
+ // ---- Private: Installation Methods ----
622
+ async installFromPath(sourcePath, warnings) {
623
+ const resolvedPath = resolve(sourcePath);
624
+ // Validate the bundle
625
+ const validation = this.validateBundle(resolvedPath);
626
+ if (!validation.valid) {
627
+ return {
628
+ success: false,
629
+ warnings: validation.warnings.map(w => w.message),
630
+ error: `Validation failed: ${validation.errors.map(e => e.message).join('; ')}`,
631
+ };
632
+ }
633
+ warnings.push(...validation.warnings.map(w => w.message));
634
+ const manifest = validation.manifest;
635
+ // Check if already installed
636
+ const existing = this.installedBundles.get(manifest.id);
637
+ if (existing) {
638
+ // Uninstall old version
639
+ this.uninstallBundle(manifest.id);
640
+ }
641
+ // Copy bundle to install directory
642
+ const installPath = join(this.bundlesDir, manifest.id);
643
+ try {
644
+ if (existsSync(installPath)) {
645
+ rmSync(installPath, { recursive: true, force: true });
646
+ }
647
+ copyRecursive(resolvedPath, installPath);
648
+ }
649
+ catch (error) {
650
+ return {
651
+ success: false,
652
+ warnings,
653
+ error: `Failed to copy bundle: ${error instanceof Error ? error.message : String(error)}`,
654
+ };
655
+ }
656
+ // Compute checksum
657
+ const checksum = computeDirectoryChecksum(installPath);
658
+ // Create bundle record
659
+ const bundle = {
660
+ manifest,
661
+ installPath,
662
+ installedAt: new Date().toISOString(),
663
+ source: resolvedPath,
664
+ checksum,
665
+ enabled: true,
666
+ };
667
+ // Run install hooks
668
+ this.runLifecycleHook(bundle, 'install');
669
+ // Save
670
+ this.installedBundles.set(manifest.id, bundle);
671
+ this.saveInstalledBundles();
672
+ return { success: true, bundle, warnings };
673
+ }
674
+ async installFromURL(url) {
675
+ // Download the bundle
676
+ const tempDir = join(this.bundlesDir, '.temp', `download_${Date.now()}`);
677
+ ensureDir(tempDir);
678
+ try {
679
+ // Use curl or wget to download
680
+ const isTarball = url.endsWith('.tar.gz') || url.endsWith('.tgz');
681
+ const isZip = url.endsWith('.zip');
682
+ const downloadPath = join(tempDir, isTarball ? 'bundle.tar.gz' : isZip ? 'bundle.zip' : 'bundle.tar.gz');
683
+ const downloadResult = execCommand(`curl -fsSL -o "${downloadPath}" "${url}"`, { timeout: 60000 });
684
+ if (downloadResult.exitCode !== 0) {
685
+ return {
686
+ success: false,
687
+ error: `Failed to download bundle from ${url}: ${downloadResult.stderr}`,
688
+ };
689
+ }
690
+ // Extract
691
+ const extractDir = join(tempDir, 'extracted');
692
+ ensureDir(extractDir);
693
+ if (isTarball) {
694
+ const extractResult = execCommand(`tar -xzf "${downloadPath}" -C "${extractDir}"`);
695
+ if (extractResult.exitCode !== 0) {
696
+ return { success: false, error: `Failed to extract tarball: ${extractResult.stderr}` };
697
+ }
698
+ }
699
+ else if (isZip) {
700
+ const extractResult = execCommand(`unzip -q -o "${downloadPath}" -d "${extractDir}"`);
701
+ if (extractResult.exitCode !== 0) {
702
+ return { success: false, error: `Failed to extract zip: ${extractResult.stderr}` };
703
+ }
704
+ }
705
+ else {
706
+ // Try tarball by default
707
+ const extractResult = execCommand(`tar -xzf "${downloadPath}" -C "${extractDir}"`);
708
+ if (extractResult.exitCode !== 0) {
709
+ return { success: false, error: `Failed to extract archive: ${extractResult.stderr}` };
710
+ }
711
+ }
712
+ // Find the bundle.json in the extracted directory
713
+ const bundlePath = this.findBundleRoot(extractDir);
714
+ if (!bundlePath) {
715
+ return { success: false, error: `No ${MANIFEST_FILE} found in downloaded archive` };
716
+ }
717
+ // Install from the extracted path
718
+ const result = await this.installFromPath(bundlePath, []);
719
+ // Cleanup temp
720
+ try {
721
+ rmSync(tempDir, { recursive: true, force: true });
722
+ }
723
+ catch {
724
+ // Best-effort cleanup
725
+ }
726
+ // Update source URL in installed bundle
727
+ if (result.success && result.bundle) {
728
+ result.bundle.source = url;
729
+ this.installedBundles.set(result.bundle.manifest.id, result.bundle);
730
+ this.saveInstalledBundles();
731
+ }
732
+ return result;
733
+ }
734
+ catch (error) {
735
+ // Cleanup temp
736
+ try {
737
+ rmSync(tempDir, { recursive: true, force: true });
738
+ }
739
+ catch { }
740
+ return {
741
+ success: false,
742
+ error: `Failed to install from URL: ${error instanceof Error ? error.message : String(error)}`,
743
+ };
744
+ }
745
+ }
746
+ async installFromRegistry(bundleId) {
747
+ const registry = this.loadRegistry();
748
+ const entry = registry.find(e => e.id === bundleId);
749
+ if (!entry) {
750
+ return { success: false, error: `Bundle "${bundleId}" not found in registry` };
751
+ }
752
+ // If it's a local path reference
753
+ if (entry.url.startsWith('local://')) {
754
+ const localPath = entry.url.replace('local://', '');
755
+ return this.installFromPath(localPath, []);
756
+ }
757
+ // Install from URL
758
+ return this.installFromURL(entry.url);
759
+ }
760
+ // ---- Private: Helper Methods ----
761
+ findBundleRoot(searchDir) {
762
+ // Check if bundle.json is at the root
763
+ if (existsSync(join(searchDir, MANIFEST_FILE))) {
764
+ return searchDir;
765
+ }
766
+ // Check one level down (common for GitHub archives)
767
+ try {
768
+ const entries = readdirSync(searchDir, { withFileTypes: true });
769
+ for (const entry of entries) {
770
+ if (entry.isDirectory()) {
771
+ const subDir = join(searchDir, entry.name);
772
+ if (existsSync(join(subDir, MANIFEST_FILE))) {
773
+ return subDir;
774
+ }
775
+ }
776
+ }
777
+ }
778
+ catch {
779
+ // Ignore
780
+ }
781
+ return null;
782
+ }
783
+ validateBundleReferences(bundlePath, references, expectedDir, errors, warnings) {
784
+ if (!references || references.length === 0)
785
+ return;
786
+ for (const ref of references) {
787
+ const fullPath = join(bundlePath, ref);
788
+ if (!existsSync(fullPath)) {
789
+ // Check if it exists in the expected directory
790
+ const altPath = join(bundlePath, expectedDir, basename(ref));
791
+ if (existsSync(altPath)) {
792
+ warnings.push({
793
+ code: 'REFERENCE_PATH_MISMATCH',
794
+ message: `Referenced file "${ref}" not found at declared path, but exists at "${expectedDir}/${basename(ref)}"`,
795
+ path: ref,
796
+ });
797
+ }
798
+ else {
799
+ errors.push({
800
+ code: 'REFERENCE_FILE_MISSING',
801
+ message: `Referenced file "${ref}" does not exist in bundle`,
802
+ path: ref,
803
+ });
804
+ }
805
+ }
806
+ }
807
+ }
808
+ runLifecycleHook(bundle, lifecycle) {
809
+ const hooksDir = join(bundle.installPath, HOOKS_DIR);
810
+ if (!existsSync(hooksDir))
811
+ return;
812
+ try {
813
+ const hookFiles = readdirSync(hooksDir);
814
+ for (const hookFile of hookFiles) {
815
+ // Check if the hook file name contains the lifecycle event
816
+ const hookName = hookFile.toLowerCase();
817
+ if (hookName.includes(lifecycle)) {
818
+ const hookPath = join(hooksDir, hookFile);
819
+ const stat = statSync(hookPath);
820
+ if (stat.isFile()) {
821
+ try {
822
+ execCommand(`"${hookPath}"`, {
823
+ cwd: bundle.installPath,
824
+ timeout: 30000,
825
+ });
826
+ }
827
+ catch {
828
+ // Hook execution failure should not block operations
829
+ }
830
+ }
831
+ }
832
+ }
833
+ }
834
+ catch {
835
+ // Ignore hook execution errors
836
+ }
837
+ }
838
+ // ---- Persistence ----
839
+ loadInstalledBundles() {
840
+ const indexFile = join(this.bundlesDir, 'installed.json');
841
+ const data = readJSONFile(indexFile);
842
+ if (!data)
843
+ return;
844
+ for (const entry of data) {
845
+ const manifestPath = join(this.bundlesDir, entry.id, MANIFEST_FILE);
846
+ const manifest = readJSONFile(manifestPath);
847
+ if (manifest) {
848
+ this.installedBundles.set(entry.id, {
849
+ manifest,
850
+ installPath: join(this.bundlesDir, entry.id),
851
+ installedAt: entry.installedAt,
852
+ source: entry.source,
853
+ checksum: entry.checksum,
854
+ enabled: entry.enabled,
855
+ });
856
+ }
857
+ }
858
+ }
859
+ saveInstalledBundles() {
860
+ const indexFile = join(this.bundlesDir, 'installed.json');
861
+ const data = Array.from(this.installedBundles.values()).map(bundle => ({
862
+ id: bundle.manifest.id,
863
+ source: bundle.source,
864
+ installedAt: bundle.installedAt,
865
+ checksum: bundle.checksum,
866
+ enabled: bundle.enabled,
867
+ }));
868
+ writeJSONFile(indexFile, data);
869
+ }
870
+ loadRegistry() {
871
+ return readJSONFile(this.registryFile) ?? [];
872
+ }
873
+ saveRegistry(registry) {
874
+ writeJSONFile(this.registryFile, registry);
875
+ }
876
+ }
877
+ // ---- chmodSync polyfill (Node.js built-in, but let's be safe) ----
878
+ function chmodSync(path, mode) {
879
+ try {
880
+ const { chmodSync: nodeChmod } = require('fs');
881
+ nodeChmod(path, mode);
882
+ }
883
+ catch {
884
+ // Ignore on platforms that don't support chmod
885
+ }
886
+ }
887
+ //# sourceMappingURL=plugin-bundle.js.map