@devilsdev/rag-pipeline-utils 2.4.4 → 2.5.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 (225) hide show
  1. package/.ragrc.schema.json +83 -192
  2. package/CHANGELOG.md +345 -0
  3. package/README.md +418 -117
  4. package/SECURITY.md +5 -5
  5. package/dist/experimental.cjs +2 -0
  6. package/dist/experimental.d.ts +1 -0
  7. package/dist/experimental.mjs +5 -0
  8. package/dist/index.cjs +4992 -1242
  9. package/dist/index.d.ts +80 -2
  10. package/dist/index.mjs +1 -1
  11. package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
  12. package/dist/types/agentic/iterative-retriever.d.ts +85 -0
  13. package/dist/types/agentic/query-planner.d.ts +145 -0
  14. package/dist/types/agentic/self-critique.d.ts +45 -0
  15. package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
  16. package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
  17. package/dist/types/ai/multimodal-processing.d.ts +32 -0
  18. package/dist/types/ai/multimodal.d.ts +39 -0
  19. package/dist/types/ai/retrieval-engine.d.ts +37 -0
  20. package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
  21. package/dist/types/chunking/chunking-engine.d.ts +70 -0
  22. package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
  23. package/dist/types/chunking/strategies/recursive.d.ts +20 -0
  24. package/dist/types/chunking/strategies/semantic.d.ts +19 -0
  25. package/dist/types/chunking/strategies/sentence.d.ts +13 -0
  26. package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
  27. package/dist/types/citation/citation-tracker.d.ts +64 -0
  28. package/dist/types/citation/hallucination-detector.d.ts +40 -0
  29. package/dist/types/citation/source-mapper.d.ts +37 -0
  30. package/dist/types/connectors/anthropic-connector.d.ts +92 -0
  31. package/dist/types/connectors/base-connector.d.ts +87 -0
  32. package/dist/types/connectors/cohere-connector.d.ts +89 -0
  33. package/dist/types/connectors/connector-utils.d.ts +110 -0
  34. package/dist/types/connectors/local-embedder.d.ts +86 -0
  35. package/dist/types/connectors/memory-retriever.d.ts +91 -0
  36. package/dist/types/connectors/ollama-connector.d.ts +110 -0
  37. package/dist/types/connectors/openai-connector.d.ts +114 -0
  38. package/dist/types/connectors/rag-prompt.d.ts +40 -0
  39. package/dist/types/cost/cost-calculator.d.ts +152 -0
  40. package/dist/types/cost/cost-tracker.d.ts +126 -0
  41. package/dist/types/cost/token-budget.d.ts +110 -0
  42. package/dist/types/dag/core/error-context.d.ts +239 -0
  43. package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
  44. package/dist/types/dag/core/topology-validation.d.ts +226 -0
  45. package/dist/types/dag/dag-engine.d.ts +156 -0
  46. package/dist/types/dag/dag-node.d.ts +37 -0
  47. package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
  48. package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
  49. package/dist/types/debug/execution-tracer.d.ts +57 -0
  50. package/dist/types/debug/trace-inspector.d.ts +31 -0
  51. package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
  52. package/dist/types/enterprise/data-governance.d.ts +203 -0
  53. package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
  54. package/dist/types/enterprise/sso-integration.d.ts +420 -0
  55. package/dist/types/evaluate/context-metrics.d.ts +55 -0
  56. package/dist/types/evaluate/faithfulness.d.ts +41 -0
  57. package/dist/types/evaluate/groundedness.d.ts +60 -0
  58. package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
  59. package/dist/types/evaluate/relevance.d.ts +21 -0
  60. package/dist/types/evaluate/scoring.d.ts +123 -0
  61. package/dist/types/experimental.d.ts +15 -0
  62. package/dist/types/graph/entity-extractor.d.ts +84 -0
  63. package/dist/types/graph/graph-index.d.ts +49 -0
  64. package/dist/types/graph/graph-retriever.d.ts +81 -0
  65. package/dist/types/graph/knowledge-graph.d.ts +129 -0
  66. package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
  67. package/dist/types/guardrails/injection-detector.d.ts +47 -0
  68. package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
  69. package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
  70. package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
  71. package/dist/types/mcp/mcp-server.d.ts +85 -0
  72. package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
  73. package/dist/types/reranker/cascade-reranker.d.ts +40 -0
  74. package/dist/types/reranker/embedding-reranker.d.ts +77 -0
  75. package/dist/types/reranker/scoring-reranker.d.ts +41 -0
  76. package/dist/types/retrieval/bm25-search.d.ts +43 -0
  77. package/dist/types/retrieval/file-vector-store.d.ts +87 -0
  78. package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
  79. package/dist/types/retrieval/rank-fusion.d.ts +10 -0
  80. package/dist/types/security/jwt-config.d.ts +80 -0
  81. package/dist/types/security/jwt-validator.d.ts +195 -0
  82. package/dist/types/security/plugin-sandbox.d.ts +158 -0
  83. package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
  84. package/dist/types/security/validator.d.ts +5 -0
  85. package/dist/types/streaming/embedding-stream.d.ts +65 -0
  86. package/dist/types/streaming/sse-adapter.d.ts +59 -0
  87. package/dist/types/streaming/stream-router.d.ts +88 -0
  88. package/dist/types/streaming/streaming-embedder.d.ts +99 -0
  89. package/dist/types/streaming/websocket-adapter.d.ts +71 -0
  90. package/dist/types/templates/scaffold.d.ts +55 -0
  91. package/dist/types/templates/template-registry.d.ts +63 -0
  92. package/dist/types/utils/embed-query.d.ts +20 -0
  93. package/dist/types/utils/logger.d.ts +50 -0
  94. package/dist/types/utils/rate-limiter.d.ts +66 -0
  95. package/dist/types/utils/retry.d.ts +27 -0
  96. package/dist/types/utils/run-abort.d.ts +34 -0
  97. package/dist/types/utils/secure-logging.d.ts +51 -0
  98. package/dist/types/utils/structured-logger.d.ts +126 -0
  99. package/package.json +53 -29
  100. package/src/agentic/agentic-pipeline.js +75 -7
  101. package/src/agentic/iterative-retriever.js +47 -10
  102. package/src/agentic/query-planner.js +329 -78
  103. package/src/citation/citation-tracker.js +6 -4
  104. package/src/citation/hallucination-detector.js +2 -2
  105. package/src/citation/source-mapper.js +15 -28
  106. package/src/cli/commands/ai.js +101 -0
  107. package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
  108. package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
  109. package/src/cli/commands/plugin-hub/local-commands.js +143 -0
  110. package/src/cli/commands/plugin-hub.js +96 -36
  111. package/src/cli/diagnostics/config-checker.js +76 -63
  112. package/src/cli/diagnostics/environment-checker.js +62 -31
  113. package/src/cli/diagnostics/plugin-checker.js +36 -89
  114. package/src/cli/doctor-command.js +39 -5
  115. package/src/cli/enhanced-cli-commands.js +72 -217
  116. package/src/cli/handlers/ai-handler.js +364 -0
  117. package/src/cli/handlers/diagnostics-handler.js +78 -31
  118. package/src/cli/handlers/ingest-handler.js +28 -22
  119. package/src/cli/handlers/init-handler.js +148 -25
  120. package/src/cli/handlers/query-handler.js +41 -26
  121. package/src/cli/handlers/run-handler.js +16 -167
  122. package/src/cli/handlers/serve-handler.js +60 -0
  123. package/src/cli/services/bench-stats.js +54 -0
  124. package/src/cli/services/benchmark.js +161 -0
  125. package/src/cli/services/feedback.js +155 -0
  126. package/src/cli/services/rag-service.js +322 -0
  127. package/src/cli/shared/cli-utils.js +24 -7
  128. package/src/cli/shared/config-context.js +21 -0
  129. package/src/cli/wizard/config-builder.js +81 -0
  130. package/src/cli/wizard/init-wizard.js +617 -0
  131. package/src/config/component-factory.js +125 -0
  132. package/src/config/load-config.js +29 -10
  133. package/src/config/load-plugin-config.js +46 -2
  134. package/src/connectors/anthropic-connector.js +93 -55
  135. package/src/connectors/base-connector.js +74 -19
  136. package/src/connectors/cohere-connector.js +88 -32
  137. package/src/connectors/connector-utils.js +302 -0
  138. package/src/connectors/local-embedder.js +43 -5
  139. package/src/connectors/memory-retriever.js +108 -15
  140. package/src/connectors/ollama-connector.js +83 -49
  141. package/src/connectors/openai-connector.js +108 -44
  142. package/src/connectors/rag-prompt.js +88 -0
  143. package/src/core/create-pipeline.js +209 -60
  144. package/src/core/plugin-registry.js +87 -11
  145. package/src/cost/cost-calculator.js +205 -32
  146. package/src/dev/dev-server.js +166 -11
  147. package/src/dev/hot-reload.js +40 -1
  148. package/src/ecosystem/plugin-checks.js +181 -0
  149. package/src/ecosystem/plugin-hub.js +50 -125
  150. package/src/ecosystem/plugin-installer.js +298 -0
  151. package/src/ecosystem/plugin-probe.js +127 -0
  152. package/src/ecosystem/plugin-scanner.js +185 -0
  153. package/src/ecosystem/tar-extract.js +114 -0
  154. package/src/enterprise/audit-logging.js +158 -29
  155. package/src/enterprise/sso-integration.js +715 -228
  156. package/src/evaluate/context-metrics.js +28 -16
  157. package/src/evaluate/evaluator.js +3 -3
  158. package/src/evaluate/faithfulness.js +18 -14
  159. package/src/evaluate/groundedness.js +43 -12
  160. package/src/evaluate/pipeline-evaluator.js +35 -8
  161. package/src/evaluate/scoring.js +251 -3
  162. package/src/experimental.js +71 -0
  163. package/src/guardrails/guardrails-pipeline.js +69 -7
  164. package/src/guardrails/injection-detector.js +283 -0
  165. package/src/guardrails/post-generation-guard.js +19 -0
  166. package/src/guardrails/pre-retrieval-guard.js +36 -36
  167. package/src/guardrails/retrieval-guard.js +58 -8
  168. package/src/index.js +36 -1
  169. package/src/loader/document-reader.js +91 -0
  170. package/src/reranker/cascade-reranker.js +3 -0
  171. package/src/reranker/embedding-reranker.js +136 -21
  172. package/src/reranker/llm-reranker.js +79 -12
  173. package/src/reranker/scoring-reranker.js +3 -0
  174. package/src/retrieval/bm25-search.js +12 -6
  175. package/src/retrieval/file-vector-store.js +212 -0
  176. package/src/retrieval/hybrid-retriever.js +22 -19
  177. package/src/security/jwt-config.js +13 -2
  178. package/src/security/jwt-validator.js +134 -30
  179. package/src/security/plugin-sandbox.js +18 -1
  180. package/src/security/plugin-signature-verifier.js +245 -70
  181. package/src/utils/deprecate.js +69 -0
  182. package/src/utils/embed-query.js +33 -0
  183. package/src/utils/input-sanitizer.js +19 -4
  184. package/src/utils/logger.js +34 -4
  185. package/src/utils/run-abort.js +102 -0
  186. package/src/ai/federated-learning.js +0 -211
  187. package/src/ai/federation.js +0 -71
  188. package/src/ai/index.js +0 -22
  189. package/src/ai/model-trainer.js +0 -217
  190. package/src/ai/model-training.js +0 -202
  191. package/src/ai/multimodal/multi-modal-processor.js +0 -345
  192. package/src/ai/orchestrator.js +0 -65
  193. package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
  194. package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
  195. package/src/cli/commands/ai-ml/federated-commands.js +0 -207
  196. package/src/cli/commands/ai-ml/index.js +0 -17
  197. package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
  198. package/src/cli/commands/ai-ml/shared.js +0 -51
  199. package/src/cli/commands/ai-ml/training-commands.js +0 -228
  200. package/src/cli/commands/ai-ml.js +0 -386
  201. package/src/cli/commands/docs.js +0 -228
  202. package/src/cli/commands/dx.js +0 -538
  203. package/src/cli/commands/wizard.js +0 -627
  204. package/src/cli/input-sanitization-middleware.js +0 -356
  205. package/src/cli/interactive-wizard.js +0 -373
  206. package/src/cli/templates/development.template.json +0 -64
  207. package/src/cli/templates/minimal.template.json +0 -33
  208. package/src/cli/templates/production.template.json +0 -104
  209. package/src/cli/templates/testing.template.json +0 -45
  210. package/src/cli/wizard/configuration.js +0 -148
  211. package/src/cli/wizard/observability.js +0 -79
  212. package/src/cli/wizard/performance.js +0 -76
  213. package/src/cli/wizard/plugin-selection.js +0 -297
  214. package/src/cli/wizard/preview-save.js +0 -172
  215. package/src/cli/wizard/project-setup.js +0 -66
  216. package/src/core/pipeline-factory.js +0 -59
  217. package/src/dag/enhanced-dag-engine.js +0 -894
  218. package/src/dx/index.js +0 -40
  219. package/src/dx/integration-templates.js +0 -662
  220. package/src/dx/performance-profiler.js +0 -968
  221. package/src/dx/realtime-debugger.js +0 -755
  222. package/src/dx/visual-pipeline-builder.js +0 -578
  223. package/src/ingest.js +0 -59
  224. package/src/pipeline/pipeline.js +0 -18
  225. package/src/query.js +0 -117
@@ -145,6 +145,23 @@ class PluginHub extends EventEmitter {
145
145
  /**
146
146
  * Install plugin from the hub
147
147
  */
148
+ /**
149
+ * Download a plugin from the registry and install it into a project
150
+ * (see ecosystem/plugin-installer.js): sha256 verified against the
151
+ * registry, static security scan (high-risk findings block unless
152
+ * allowRisky), stage contract check, files kept inside
153
+ * <project>/.rag/plugins, and `plugins.<stage>.<name>` added to the config.
154
+ *
155
+ * @param {string} pluginId - Registry plugin id
156
+ * @param {string} [version='latest']
157
+ * @param {object} [_options]
158
+ * @param {string} [_options.configPath='.ragrc.json'] - Config to register the plugin in
159
+ * @param {boolean} [_options.requireCertified] - Refuse plugins the registry marks uncertified
160
+ * @param {string} [_options.stage] - Overrides package.json ragPipeline.stage
161
+ * @param {string} [_options.name] - Overrides package.json ragPipeline.name
162
+ * @param {boolean} [_options.force] - Replace an installed plugin
163
+ * @param {boolean} [_options.allowRisky] - Install despite high-risk findings
164
+ */
148
165
  async installPlugin(pluginId, version = 'latest', _options = {}) {
149
166
  const installId = crypto.randomUUID();
150
167
  const startedAt = Date.now();
@@ -152,69 +169,43 @@ class PluginHub extends EventEmitter {
152
169
  try {
153
170
  this.emit('install_start', { pluginId, version, installId });
154
171
 
155
- // Get plugin info and verify
156
- const pluginInfo = await this.getPluginInfo(pluginId);
157
- if (!pluginInfo) throw new Error(`Plugin ${pluginId} not found`);
158
-
159
- // Check certification if required
160
- if (_options.requireCertified && !pluginInfo.certified) {
161
- throw new Error(`Plugin ${pluginId} is not certified`);
162
- }
163
-
164
- // Security scan in sandbox
165
- if (_options.securityScan !== false) {
166
- this.emit('install_progress', { installId, stage: 'security_scan' });
167
- await this.sandbox.scanPlugin(pluginInfo);
172
+ if (_options.requireCertified) {
173
+ const info = await this.getPluginInfo(pluginId);
174
+ if (!info.certified)
175
+ throw new Error(`Plugin ${pluginId} is not certified`);
168
176
  }
169
177
 
170
- // Download plugin
171
- this.emit('install_progress', { installId, stage: 'downloading' });
172
- const downloadUrl = await this._getDownloadUrl(pluginId, version);
173
- const pluginData = await this._downloadPlugin(downloadUrl);
174
-
175
- // Verify integrity
176
- this.emit('install_progress', { installId, stage: 'verifying' });
177
- await this._verifyPluginIntegrity(pluginData, pluginInfo.checksums);
178
-
179
- // Install in sandbox first
180
- this.emit('install_progress', { installId, stage: 'sandbox_install' });
181
- const sandboxResult = await this.sandbox.installPlugin(pluginData, {
182
- timeout: _options.sandboxTimeout || 30000,
183
- memoryLimit: _options.memoryLimit || '512MB',
184
- networkAccess: _options.networkAccess || false,
178
+ const { installPlugin } = require('./plugin-installer');
179
+ const result = await installPlugin({
180
+ source: pluginId,
181
+ version,
182
+ hub: this,
183
+ configPath: _options.configPath || '.ragrc.json',
184
+ stage: _options.stage,
185
+ name: _options.name,
186
+ force: _options.force,
187
+ allowRisky: _options.allowRisky,
188
+ onProgress: (stage) =>
189
+ this.emit('install_progress', { installId, stage }),
185
190
  });
186
191
 
187
- if (!sandboxResult.success) {
188
- throw new Error(`Sandbox installation failed: ${sandboxResult.error}`);
189
- }
190
-
191
- // Install to main system
192
- this.emit('install_progress', { installId, stage: 'installing' });
193
- const installPath = await this._installPluginToSystem(
194
- pluginData,
195
- pluginInfo,
196
- );
197
-
198
- // Track installation analytics
199
192
  this.analytics.trackInstallation(pluginId, version, {
200
193
  success: true,
201
194
  installTime: Date.now() - startedAt,
202
195
  });
203
-
204
196
  this.emit('install_complete', {
205
197
  pluginId,
206
198
  version,
207
199
  installId,
208
- installPath,
209
- pluginInfo,
200
+ installPath: result.path,
210
201
  });
211
202
 
212
203
  return {
213
204
  success: true,
214
205
  pluginId,
215
206
  version,
216
- installPath,
217
- pluginInfo,
207
+ installPath: result.path,
208
+ ...result,
218
209
  };
219
210
  } catch (error) {
220
211
  this.analytics.trackInstallation(pluginId, version, {
@@ -344,46 +335,16 @@ class PluginHub extends EventEmitter {
344
335
  /**
345
336
  * Get user's installed plugins
346
337
  */
347
- async getInstalledPlugins() {
348
- try {
349
- const pluginsDir = path.join(this._config.localCacheDir, 'plugins');
350
- const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
351
-
352
- const installed = [];
353
-
354
- for (const entry of entries) {
355
- if (entry.isDirectory()) {
356
- const metadataPath = path.join(
357
- pluginsDir,
358
- entry.name,
359
- 'metadata.json',
360
- );
361
-
362
- try {
363
- const metadata = JSON.parse(
364
- await fs.readFile(metadataPath, 'utf8'),
365
- );
366
- installed.push({
367
- ...metadata,
368
- installPath: path.join(pluginsDir, entry.name),
369
- lastUsed: await this.analytics.getLastUsed(metadata.id),
370
- });
371
- } catch {
372
- // Skip invalid plugins
373
- continue; // eslint-disable-line no-continue
374
- }
375
- }
376
- }
377
-
378
- return installed;
379
- } catch {
380
- return [];
381
- }
338
+ /**
339
+ * Plugins registered in a project's config (see plugin-installer
340
+ * listInstalledPlugins).
341
+ * @param {string} [configPath='.ragrc.json']
342
+ */
343
+ async getInstalledPlugins(configPath = '.ragrc.json') {
344
+ const { listInstalledPlugins } = require('./plugin-installer');
345
+ return listInstalledPlugins(configPath);
382
346
  }
383
347
 
384
- // ------------------------------
385
- // Private methods
386
- // ------------------------------
387
348
  async _makeRequest(method, endpoint, params = {}) {
388
349
  const doFetch = pickFetch(this._config.fetch);
389
350
  if (!doFetch) throw new Error('fetch is not available in this environment');
@@ -540,35 +501,6 @@ class PluginHub extends EventEmitter {
540
501
  }
541
502
  }
542
503
 
543
- async _installPluginToSystem(pluginData, pluginInfo) {
544
- const installDir = path.join(
545
- this._config.localCacheDir,
546
- 'plugins',
547
- pluginInfo.id,
548
- );
549
-
550
- // Create installation directory
551
- await fs.mkdir(installDir, { recursive: true });
552
-
553
- // Simulated install: write metadata only
554
- const metadataPath = path.join(installDir, 'metadata.json');
555
- await fs.writeFile(
556
- metadataPath,
557
- JSON.stringify(
558
- {
559
- ...pluginInfo,
560
- installedAt: new Date().toISOString(),
561
- installPath: installDir,
562
- size: Buffer.isBuffer(pluginData) ? pluginData.length : 0,
563
- },
564
- null,
565
- 2,
566
- ),
567
- );
568
-
569
- return installDir;
570
- }
571
-
572
504
  async _validatePluginForPublish(pluginPath) {
573
505
  // Validate plugin structure and metadata
574
506
  const packageJsonPath = path.join(pluginPath, 'package.json');
@@ -608,19 +540,12 @@ class PluginHub extends EventEmitter {
608
540
  });
609
541
  }
610
542
 
611
- async _getInstalledVersion(pluginId) {
612
- try {
613
- const metadataPath = path.join(
614
- this._config.localCacheDir,
615
- 'plugins',
616
- pluginId,
617
- 'metadata.json',
618
- );
619
- const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf8'));
620
- return metadata.version;
621
- } catch {
622
- return null;
623
- }
543
+ async _getInstalledVersion(pluginId, configPath = '.ragrc.json') {
544
+ const { listInstalledPlugins } = require('./plugin-installer');
545
+ const match = listInstalledPlugins(configPath).find(
546
+ (p) => p.name === pluginId,
547
+ );
548
+ return match ? match.version : null;
624
549
  }
625
550
  }
626
551
 
@@ -0,0 +1,298 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Install a plugin into a project and register it in .ragrc.json.
5
+ *
6
+ * Sources: a registry plugin id (needs a PluginHub; the download is verified
7
+ * against the registry's sha256), a local directory, a .tgz / .tar.gz
8
+ * package, a single .js file, or an npm package ("npm:<spec>").
9
+ *
10
+ * Steps: stage the files inside <project>/.rag/plugins, statically scan
11
+ * them (high-risk findings block the install unless allowRisky), check the
12
+ * entry module exposes the stage's required methods (in a separate process),
13
+ * move into place and add `plugins.<stage>.<name>` to the config. Install
14
+ * scripts are never run. Installed plugins run with your privileges when the
15
+ * pipeline uses them; the scan informs that decision, it does not sandbox.
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+ const path = require('path');
21
+ const crypto = require('crypto');
22
+ const { execFile } = require('child_process');
23
+ const { promisify } = require('util');
24
+ const { extractTarGz, isWithin, relativeWithin } = require('./tar-extract');
25
+ const { scanPlugin } = require('./plugin-scanner');
26
+ const { probeContract, STAGE_METHODS } = require('./plugin-probe');
27
+
28
+ const NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
29
+
30
+ /** child is strictly inside parent (both resolved paths). */
31
+ function isInside(parent, child) {
32
+ return (
33
+ isWithin(parent, child) && path.resolve(parent) !== path.resolve(child)
34
+ );
35
+ }
36
+
37
+ /** Run npm without a shell (on Windows via node + npm-cli.js). */
38
+ async function runNpm(args, cwd) {
39
+ const run = promisify(execFile);
40
+ if (process.platform === 'win32') {
41
+ const npmCli = path.join(
42
+ path.dirname(process.execPath),
43
+ 'node_modules',
44
+ 'npm',
45
+ 'bin',
46
+ 'npm-cli.js',
47
+ );
48
+ return run(process.execPath, [npmCli, ...args], { cwd, windowsHide: true });
49
+ }
50
+ return run('npm', args, { cwd });
51
+ }
52
+
53
+ /** If extraction produced a single top-level directory (e.g. "package/"), use it. */
54
+ function packageRoot(dir) {
55
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
56
+ if (entries.length === 1 && entries[0].isDirectory()) {
57
+ return path.join(dir, entries[0].name);
58
+ }
59
+ return dir;
60
+ }
61
+
62
+ async function stageSource(source, stagingDir, options) {
63
+ const local = path.resolve(options.cwd || process.cwd(), source);
64
+
65
+ if (fs.existsSync(local)) {
66
+ const stat = fs.statSync(local);
67
+ if (stat.isDirectory()) {
68
+ fs.cpSync(local, stagingDir, {
69
+ recursive: true,
70
+ // Skip node_modules and .git at any depth (path is relative to local)
71
+ filter: (src) =>
72
+ !/(^|\/)(node_modules|\.git)(\/|$)/.test(
73
+ relativeWithin(local, src) || '',
74
+ ),
75
+ });
76
+ return { kind: 'directory', root: stagingDir };
77
+ }
78
+ if (/\.(tgz|tar\.gz)$/i.test(local)) {
79
+ extractTarGz(fs.readFileSync(local), stagingDir);
80
+ return { kind: 'tarball', root: packageRoot(stagingDir) };
81
+ }
82
+ if (/\.(c?js)$/i.test(local)) {
83
+ fs.copyFileSync(local, path.join(stagingDir, 'index.js'));
84
+ return { kind: 'file', root: stagingDir };
85
+ }
86
+ throw new Error(
87
+ `Unsupported plugin file: ${source} (expected a directory, .tgz or .js)`,
88
+ );
89
+ }
90
+
91
+ if (source.startsWith('npm:')) {
92
+ const spec = source.slice(4);
93
+ const packDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rag-npm-'));
94
+ try {
95
+ const { stdout } = await runNpm(
96
+ [
97
+ 'pack',
98
+ spec,
99
+ '--silent',
100
+ '--ignore-scripts',
101
+ '--pack-destination',
102
+ packDir,
103
+ ],
104
+ packDir,
105
+ );
106
+ const tarball = path.join(packDir, stdout.trim().split(/\r?\n/).pop());
107
+ extractTarGz(fs.readFileSync(tarball), stagingDir);
108
+ return { kind: 'npm', root: packageRoot(stagingDir) };
109
+ } finally {
110
+ fs.rmSync(packDir, { recursive: true, force: true });
111
+ }
112
+ }
113
+
114
+ if (!options.hub) {
115
+ throw new Error(
116
+ `"${source}" is not a local path or npm:<package>, and no plugin registry is configured.`,
117
+ );
118
+ }
119
+ const hub = options.hub;
120
+ const info = await hub.getPluginInfo(source);
121
+ const url = await hub._getDownloadUrl(source, options.version || 'latest');
122
+ const archive = await hub._downloadPlugin(url);
123
+ await hub._verifyPluginIntegrity(archive, info.checksums); // throws on mismatch
124
+ extractTarGz(archive, stagingDir);
125
+ return { kind: 'registry', root: packageRoot(stagingDir), info };
126
+ }
127
+
128
+ /**
129
+ * @param {object} params
130
+ * @param {string} params.source - Registry id, path, .tgz, .js or "npm:<spec>"
131
+ * @param {string} params.configPath - .ragrc.json to register the plugin in
132
+ * @param {string} [params.stage] - Overrides package.json "ragPipeline.stage"
133
+ * @param {string} [params.name] - Overrides package.json "ragPipeline.name"
134
+ * @param {boolean} [params.force] - Replace an installed plugin with the same stage/name
135
+ * @param {boolean} [params.allowRisky] - Install despite high-risk scan findings
136
+ * @param {object} [params.hub] - PluginHub, required for registry ids
137
+ * @param {string} [params.version] - Registry version (default "latest")
138
+ * @param {function} [params.onProgress] - Called with a stage label
139
+ * @returns {Promise<object>} Installation summary
140
+ */
141
+ async function installPlugin(params) {
142
+ const progress = params.onProgress || (() => {});
143
+ const configPath = path.resolve(params.configPath);
144
+ if (!fs.existsSync(configPath)) {
145
+ throw new Error(
146
+ `Config file not found: ${params.configPath}. Run "rag-pipeline init" first.`,
147
+ );
148
+ }
149
+ const configDir = path.dirname(configPath);
150
+ const pluginsDir = path.join(configDir, '.rag', 'plugins');
151
+ fs.mkdirSync(pluginsDir, { recursive: true });
152
+ const stagingDir = path.join(
153
+ pluginsDir,
154
+ `.staging-${crypto.randomBytes(6).toString('hex')}`,
155
+ );
156
+ fs.mkdirSync(stagingDir);
157
+
158
+ try {
159
+ progress('fetching');
160
+ const staged = await stageSource(params.source, stagingDir, {
161
+ hub: params.hub,
162
+ version: params.version,
163
+ cwd: configDir,
164
+ });
165
+
166
+ const manifestPath = path.join(staged.root, 'package.json');
167
+ const manifest = fs.existsSync(manifestPath)
168
+ ? JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
169
+ : {};
170
+ const stage = params.stage || manifest.ragPipeline?.stage;
171
+ const name = params.name || manifest.ragPipeline?.name;
172
+ if (!STAGE_METHODS[stage]) {
173
+ throw new Error(
174
+ `Unknown or missing stage. Pass --stage (${Object.keys(STAGE_METHODS).join(', ')}) or set "ragPipeline.stage" in package.json.`,
175
+ );
176
+ }
177
+ if (!name || !NAME_PATTERN.test(name)) {
178
+ throw new Error(
179
+ 'Missing or invalid plugin name. Pass --name (letters, digits, ".", "_", "-") or set "ragPipeline.name" in package.json.',
180
+ );
181
+ }
182
+
183
+ const entry = path.resolve(staged.root, manifest.main || 'index.js');
184
+ if (!isInside(staged.root, entry) || !fs.existsSync(entry)) {
185
+ throw new Error(
186
+ `Plugin entry file not found: ${manifest.main || 'index.js'}`,
187
+ );
188
+ }
189
+
190
+ progress('scanning');
191
+ const scan = scanPlugin(staged.root);
192
+ if (scan.counts.high > 0 && !params.allowRisky) {
193
+ const error = new Error(
194
+ `Security scan found ${scan.counts.high} high-risk finding(s); not installed. Review them and re-run with --allow-risky to install anyway.`,
195
+ );
196
+ error.scan = scan;
197
+ throw error;
198
+ }
199
+
200
+ progress('verifying');
201
+ const probe = await probeContract(entry, stage);
202
+ if (!probe.ok) {
203
+ const reason =
204
+ probe.error || `missing method(s): ${probe.missing.join(', ')}`;
205
+ throw new Error(
206
+ `Plugin does not implement the ${stage} contract: ${reason}`,
207
+ );
208
+ }
209
+
210
+ progress('installing');
211
+ const destination = path.join(pluginsDir, `${stage}-${name}`);
212
+ if (fs.existsSync(destination)) {
213
+ if (!params.force) {
214
+ throw new Error(
215
+ `${stage} plugin "${name}" is already installed. Use --force to replace it.`,
216
+ );
217
+ }
218
+ fs.rmSync(destination, { recursive: true, force: true });
219
+ }
220
+ fs.renameSync(staged.root, destination);
221
+ const installedEntry = path.join(
222
+ destination,
223
+ relativeWithin(staged.root, entry),
224
+ );
225
+
226
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
227
+ raw.plugins = raw.plugins || {};
228
+ raw.plugins[stage] = raw.plugins[stage] || {};
229
+ raw.plugins[stage][name] = `./${relativeWithin(configDir, installedEntry)}`;
230
+ fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`);
231
+
232
+ return {
233
+ stage,
234
+ name,
235
+ source: staged.kind,
236
+ version: manifest.version || staged.info?.version || null,
237
+ path: destination,
238
+ entry: raw.plugins[stage][name],
239
+ scan,
240
+ methods: probe.methods,
241
+ inPipeline: Array.isArray(raw.pipeline)
242
+ ? raw.pipeline.some((p) => p.stage === stage && p.name === name)
243
+ : false,
244
+ };
245
+ } finally {
246
+ fs.rmSync(stagingDir, { recursive: true, force: true });
247
+ }
248
+ }
249
+
250
+ module.exports = { installPlugin };
251
+
252
+ /**
253
+ * Plugins registered under `plugins` in a config, with the installed
254
+ * package's version when it lives in .rag/plugins.
255
+ *
256
+ * @param {string} configPath
257
+ * @returns {Array<{stage: string, name: string, entry: string, version: string|null, installed: boolean, inPipeline: boolean, exists: boolean}>}
258
+ */
259
+ function listInstalledPlugins(configPath) {
260
+ const absolute = path.resolve(configPath);
261
+ if (!fs.existsSync(absolute)) return [];
262
+ const config = JSON.parse(fs.readFileSync(absolute, 'utf8'));
263
+ const configDir = path.dirname(absolute);
264
+ const pluginsDir = path.join(configDir, '.rag', 'plugins');
265
+ const pipeline = Array.isArray(config.pipeline) ? config.pipeline : [];
266
+ const list = [];
267
+
268
+ for (const [stage, plugins] of Object.entries(config.plugins || {})) {
269
+ for (const [name, entry] of Object.entries(plugins || {})) {
270
+ if (typeof entry !== 'string') continue;
271
+ const entryPath = path.resolve(configDir, entry);
272
+ const installedDir = path.join(pluginsDir, `${stage}-${name}`);
273
+ const installed = isInside(installedDir, entryPath);
274
+ let version = null;
275
+ const manifest = path.join(installedDir, 'package.json');
276
+ if (installed && fs.existsSync(manifest)) {
277
+ try {
278
+ version =
279
+ JSON.parse(fs.readFileSync(manifest, 'utf8')).version || null;
280
+ } catch {
281
+ // unreadable manifest: version unknown
282
+ }
283
+ }
284
+ list.push({
285
+ stage,
286
+ name,
287
+ entry,
288
+ version,
289
+ installed,
290
+ exists: fs.existsSync(entryPath),
291
+ inPipeline: pipeline.some((p) => p.stage === stage && p.name === name),
292
+ });
293
+ }
294
+ }
295
+ return list;
296
+ }
297
+
298
+ module.exports.listInstalledPlugins = listInstalledPlugins;
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Check that a plugin module exposes the methods its stage requires
5
+ * (contracts/<stage>-contract.json). Loading a module runs its code, so the
6
+ * check happens in a separate Node process with a timeout; this keeps the
7
+ * calling process clean but is NOT a security sandbox.
8
+ */
9
+
10
+ const { execFile } = require('child_process');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const STAGE_METHODS = {
15
+ loader: ['load'],
16
+ embedder: ['embed'],
17
+ retriever: ['store', 'retrieve'],
18
+ llm: ['generate'],
19
+ reranker: ['rerank'],
20
+ };
21
+
22
+ // Runs in the child: load the module the way buildComponents() does and
23
+ // report which methods the resulting object has.
24
+ const MARKER = '@@rag-probe@@';
25
+ const PROBE_SCRIPT = `
26
+ const MARKER = '${MARKER}';
27
+ const mod = require(process.argv[1]);
28
+ const exported = mod && mod.default ? mod.default : mod;
29
+ let instance = exported;
30
+ if (typeof exported === 'function') {
31
+ instance = /^class\\s/.test(Function.prototype.toString.call(exported))
32
+ ? new exported({})
33
+ : exported({});
34
+ }
35
+ const methods = [];
36
+ for (let o = instance; o && o !== Object.prototype; o = Object.getPrototypeOf(o)) {
37
+ for (const key of Object.getOwnPropertyNames(o)) {
38
+ try { if (key !== 'constructor' && typeof instance[key] === 'function') methods.push(key); } catch {}
39
+ }
40
+ }
41
+ process.stdout.write(MARKER + JSON.stringify({ type: typeof instance, methods: [...new Set(methods)] }));
42
+ `;
43
+
44
+ /** The "Error: ..." line of a Node stack trace, else the first line. */
45
+ function firstErrorLine(text) {
46
+ const lines = String(text)
47
+ .split(/\r?\n/)
48
+ .map((line) => line.trim())
49
+ .filter(Boolean);
50
+ return (
51
+ lines.find((line) => /^\w*Error\b/.test(line)) ||
52
+ lines[0] ||
53
+ 'unknown error'
54
+ );
55
+ }
56
+
57
+ /**
58
+ * @param {string} modulePath - Plugin entry file
59
+ * @param {string} stage - loader | embedder | retriever | llm | reranker
60
+ * @param {{timeoutMs?: number}} [options]
61
+ * @returns {Promise<{ok: boolean, missing: string[], methods: string[], error?: string}>}
62
+ */
63
+ function probeContract(modulePath, stage, options = {}) {
64
+ const required = STAGE_METHODS[stage];
65
+ if (!required) {
66
+ return Promise.reject(
67
+ new Error(
68
+ `Unknown stage "${stage}". Expected: ${Object.keys(STAGE_METHODS).join(', ')}`,
69
+ ),
70
+ );
71
+ }
72
+ const absolute = path.resolve(modulePath);
73
+ if (!fs.existsSync(absolute)) {
74
+ return Promise.resolve({
75
+ ok: false,
76
+ missing: required,
77
+ methods: [],
78
+ error: `Not found: ${modulePath}`,
79
+ });
80
+ }
81
+
82
+ return new Promise((resolve) => {
83
+ execFile(
84
+ process.execPath,
85
+ ['-e', PROBE_SCRIPT, absolute],
86
+ {
87
+ timeout: options.timeoutMs || 10000,
88
+ cwd: path.dirname(absolute),
89
+ windowsHide: true,
90
+ },
91
+ (error, stdout, stderr) => {
92
+ if (error) {
93
+ const reason = error.killed
94
+ ? 'timed out while loading'
95
+ : firstErrorLine(stderr || error.message);
96
+ resolve({
97
+ ok: false,
98
+ missing: required,
99
+ methods: [],
100
+ error: `Failed to load: ${reason}`,
101
+ });
102
+ return;
103
+ }
104
+ let parsed;
105
+ try {
106
+ // The module may print while loading; the result follows the marker.
107
+ parsed = JSON.parse(
108
+ stdout.slice(stdout.lastIndexOf(MARKER) + MARKER.length),
109
+ );
110
+ } catch {
111
+ resolve({
112
+ ok: false,
113
+ missing: required,
114
+ methods: [],
115
+ error: 'Module printed unexpected output while loading',
116
+ });
117
+ return;
118
+ }
119
+ // An embedder may implement embedQuery only for queries; embed is required.
120
+ const missing = required.filter((m) => !parsed.methods.includes(m));
121
+ resolve({ ok: missing.length === 0, missing, methods: parsed.methods });
122
+ },
123
+ );
124
+ });
125
+ }
126
+
127
+ module.exports = { probeContract, STAGE_METHODS };