@devilsdev/rag-pipeline-utils 2.5.0 → 2.6.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 (66) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +1 -1
  3. package/dist/index.cjs +1484 -147
  4. package/dist/index.d.ts +49 -0
  5. package/dist/index.mjs +1 -1
  6. package/dist/testing.cjs +2 -0
  7. package/dist/testing.d.ts +1 -0
  8. package/dist/testing.mjs +5 -0
  9. package/dist/types/adapters/ai-sdk.d.ts +53 -0
  10. package/dist/types/cache/component-keys.d.ts +7 -0
  11. package/dist/types/cache/memory-cache.d.ts +54 -0
  12. package/dist/types/cache/pipeline-cache.d.ts +66 -0
  13. package/dist/types/cache/redis-cache.d.ts +46 -0
  14. package/dist/types/connectors/anthropic-connector.d.ts +7 -2
  15. package/dist/types/connectors/cohere-connector.d.ts +7 -2
  16. package/dist/types/connectors/local-embedder.d.ts +9 -0
  17. package/dist/types/connectors/memory-retriever.d.ts +8 -0
  18. package/dist/types/connectors/ollama-connector.d.ts +7 -2
  19. package/dist/types/connectors/openai-connector.d.ts +16 -1
  20. package/dist/types/core/create-pipeline.d.ts +8 -0
  21. package/dist/types/core/plugin-registry.d.ts +136 -0
  22. package/dist/types/core/stage-contracts.d.ts +36 -0
  23. package/dist/types/core/validate-plugin.d.ts +45 -0
  24. package/dist/types/evaluate/semantic-similarity.d.ts +14 -0
  25. package/dist/types/grounding/check-answer.d.ts +101 -0
  26. package/dist/types/retrieval/file-vector-store.d.ts +12 -0
  27. package/dist/types/testing.d.ts +168 -0
  28. package/package.json +22 -7
  29. package/src/adapters/ai-sdk.js +215 -0
  30. package/src/cache/component-keys.js +38 -0
  31. package/src/cache/memory-cache.js +87 -0
  32. package/src/cache/pipeline-cache.js +260 -0
  33. package/src/cache/redis-cache.js +83 -0
  34. package/src/cli/commands/ai.js +17 -12
  35. package/src/cli/commands/plugin-hub.js +18 -13
  36. package/src/cli/enhanced-cli-commands.js +45 -29
  37. package/src/cli/handlers/query-handler.js +174 -11
  38. package/src/cli/services/instrument.js +83 -0
  39. package/src/cli/services/rag-service.js +51 -12
  40. package/src/cli/wizard/init-wizard.js +2 -17
  41. package/src/config/enhanced-ragrc-schema.js +0 -2
  42. package/src/config/validate-schema.js +4 -1
  43. package/src/connectors/anthropic-connector.js +22 -2
  44. package/src/connectors/cohere-connector.js +20 -2
  45. package/src/connectors/local-embedder.js +20 -0
  46. package/src/connectors/memory-retriever.js +18 -0
  47. package/src/connectors/ollama-connector.js +29 -2
  48. package/src/connectors/openai-connector.js +49 -4
  49. package/src/core/create-pipeline.js +112 -15
  50. package/src/core/performance/parallel-processor.js +62 -1
  51. package/src/core/plugin-registry.js +42 -19
  52. package/src/core/stage-contracts.js +36 -0
  53. package/src/core/validate-plugin.js +85 -0
  54. package/src/debug/execution-tracer.js +14 -4
  55. package/src/dev/dev-server.js +8 -1
  56. package/src/dev/hot-reload.js +4 -3
  57. package/src/enterprise/sso-integration.js +4 -3
  58. package/src/evaluate/semantic-similarity.js +79 -0
  59. package/src/grounding/check-answer.js +173 -0
  60. package/src/index.js +22 -0
  61. package/src/loader/csv-loader.js +3 -2
  62. package/src/loader/html-loader.js +3 -1
  63. package/src/loader/markdown-loader.js +3 -2
  64. package/src/retrieval/file-vector-store.js +30 -1
  65. package/src/security/jwt-validator.js +5 -4
  66. package/src/testing.js +254 -0
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * In-process cache with least-recently-used eviction and per-entry time to
5
+ * live. Implements the CacheBackend contract:
6
+ *
7
+ * get(key) -> Promise<value | undefined>
8
+ * set(key, value, { ttlSeconds }) -> Promise<void>
9
+ * delete(key) -> Promise<void>
10
+ * stats() -> Promise<{ hits, misses, size }>
11
+ *
12
+ * Values are stored as JSON copies, so later changes to a returned value do
13
+ * not change the cache (the same behaviour as RedisCache).
14
+ */
15
+ class MemoryCache {
16
+ /**
17
+ * @param {Object} [options]
18
+ * @param {number} [options.maxEntries=1000] - Entries kept before the least
19
+ * recently used is evicted
20
+ * @param {number} [options.ttlSeconds] - Default time to live; none means
21
+ * entries never expire on their own
22
+ */
23
+ constructor({ maxEntries = 1000, ttlSeconds } = {}) {
24
+ if (!Number.isInteger(maxEntries) || maxEntries < 1) {
25
+ throw new Error('maxEntries must be a positive integer');
26
+ }
27
+ this.maxEntries = maxEntries;
28
+ this.ttlSeconds = ttlSeconds;
29
+ this._entries = new Map(); // insertion order = recency order
30
+ this._hits = 0;
31
+ this._misses = 0;
32
+ }
33
+
34
+ /**
35
+ * @param {string} key
36
+ * @returns {Promise<unknown|undefined>}
37
+ */
38
+ async get(key) {
39
+ const entry = this._entries.get(key);
40
+ if (!entry || (entry.expiresAt !== null && entry.expiresAt <= Date.now())) {
41
+ if (entry) this._entries.delete(key);
42
+ this._misses += 1;
43
+ return undefined;
44
+ }
45
+ // Move to the most recently used position.
46
+ this._entries.delete(key);
47
+ this._entries.set(key, entry);
48
+ this._hits += 1;
49
+ return JSON.parse(entry.json);
50
+ }
51
+
52
+ /**
53
+ * @param {string} key
54
+ * @param {unknown} value - JSON-serialisable value
55
+ * @param {{ ttlSeconds?: number }} [options]
56
+ * @returns {Promise<void>}
57
+ */
58
+ async set(key, value, { ttlSeconds = this.ttlSeconds } = {}) {
59
+ const json = JSON.stringify(value);
60
+ if (json === undefined) return;
61
+ this._entries.delete(key);
62
+ this._entries.set(key, {
63
+ json,
64
+ expiresAt: ttlSeconds > 0 ? Date.now() + ttlSeconds * 1000 : null,
65
+ });
66
+ while (this._entries.size > this.maxEntries) {
67
+ this._entries.delete(this._entries.keys().next().value);
68
+ }
69
+ }
70
+
71
+ /** @param {string} key */
72
+ async delete(key) {
73
+ this._entries.delete(key);
74
+ }
75
+
76
+ /** Remove every entry. */
77
+ async clear() {
78
+ this._entries.clear();
79
+ }
80
+
81
+ /** @returns {Promise<{ hits: number, misses: number, size: number }>} */
82
+ async stats() {
83
+ return { hits: this._hits, misses: this._misses, size: this._entries.size };
84
+ }
85
+ }
86
+
87
+ module.exports = { MemoryCache };
@@ -0,0 +1,260 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Caching for createRagPipeline({ cache }): three opt-in layers.
5
+ *
6
+ * - embeddings: the query vector, keyed by the embedder's identity and the
7
+ * query text.
8
+ * - retrieval: the retriever's raw results, keyed by the retriever's state,
9
+ * the query and topK. Cached before `filterResults` (guardrail ACLs) and
10
+ * the reranker, so those still run on every request and one user's
11
+ * permissions can never shape another user's results.
12
+ * - generation: the answer, keyed by the llm's identity, the query, the
13
+ * exact documents it was given and the generation options. Skipped when
14
+ * the temperature is above 0.
15
+ *
16
+ * A layer caches only when its component identifies itself through
17
+ * `getCacheKey(role)` or a `cacheKey` string (the built-ins do), so a cache
18
+ * can never return results for a different model, index or configuration.
19
+ * Only successful, completed work is stored: nothing from an aborted or
20
+ * timed-out run.
21
+ */
22
+
23
+ const crypto = require('crypto');
24
+ const { MemoryCache } = require('./memory-cache');
25
+
26
+ const LAYERS = ['embeddings', 'retrieval', 'generation'];
27
+ const DEFAULT_TTL_SECONDS = {
28
+ embeddings: 30 * 24 * 3600,
29
+ retrieval: 3600,
30
+ generation: 24 * 3600,
31
+ };
32
+ const ROLE = {
33
+ embeddings: 'embedder',
34
+ retrieval: 'retriever',
35
+ generation: 'llm',
36
+ };
37
+
38
+ /** Generation options that change the answer and so belong in its key. */
39
+ const GENERATION_OPTIONS = [
40
+ 'temperature',
41
+ 'maxTokens',
42
+ 'maxOutputTokens',
43
+ 'topP',
44
+ 'model',
45
+ 'systemPrompt',
46
+ 'seed',
47
+ 'stopSequences',
48
+ ];
49
+
50
+ function hash(value) {
51
+ return crypto
52
+ .createHash('sha256')
53
+ .update(JSON.stringify(value))
54
+ .digest('hex');
55
+ }
56
+
57
+ function isBackend(value) {
58
+ return (
59
+ value && typeof value.get === 'function' && typeof value.set === 'function'
60
+ );
61
+ }
62
+
63
+ /** A component's identity for a role, or null when it has none. */
64
+ function componentKey(component, role) {
65
+ if (!component) return null;
66
+ if (typeof component.getCacheKey === 'function') {
67
+ const key = component.getCacheKey(role);
68
+ return typeof key === 'string' && key ? key : null;
69
+ }
70
+ return typeof component.cacheKey === 'string' && component.cacheKey
71
+ ? component.cacheKey
72
+ : null;
73
+ }
74
+
75
+ const warned = new Set();
76
+ function warnOnce(code, message) {
77
+ if (warned.has(code)) return;
78
+ warned.add(code);
79
+ process.emitWarning(message, { type: 'RagCacheWarning', code });
80
+ }
81
+
82
+ /**
83
+ * Normalise the `cache` option of createRagPipeline.
84
+ *
85
+ * - `true`: all three layers in one in-memory cache
86
+ * - a CacheBackend: all three layers in it
87
+ * - `{ embeddings, retrieval, generation, ttlSeconds }`: per layer, each
88
+ * `true` (in-memory), `false`/absent (off) or a CacheBackend;
89
+ * `ttlSeconds: { embeddings, retrieval, generation }` overrides the
90
+ * defaults (30 days, 1 hour, 24 hours)
91
+ *
92
+ * @returns {Object|null} Layer config, or null when caching is off
93
+ */
94
+ function normalizeCacheOption(option) {
95
+ if (!option) return null;
96
+ const shared =
97
+ option === true ? new MemoryCache() : isBackend(option) ? option : null;
98
+ const layers = {};
99
+ for (const layer of LAYERS) {
100
+ let backend = null;
101
+ if (shared) {
102
+ backend = shared;
103
+ } else if (option[layer] === true) {
104
+ backend = new MemoryCache();
105
+ } else if (isBackend(option[layer])) {
106
+ backend = option[layer];
107
+ }
108
+ if (backend) {
109
+ layers[layer] = {
110
+ backend,
111
+ ttlSeconds:
112
+ (option.ttlSeconds && option.ttlSeconds[layer]) ??
113
+ DEFAULT_TTL_SECONDS[layer],
114
+ hits: 0,
115
+ misses: 0,
116
+ skipped: 0,
117
+ };
118
+ }
119
+ }
120
+ return Object.keys(layers).length ? layers : null;
121
+ }
122
+
123
+ /**
124
+ * Per-pipeline cache. Every method returns undefined and does nothing for a
125
+ * layer that is off or whose component has no cache key.
126
+ */
127
+ class PipelineCache {
128
+ /**
129
+ * @param {Object} layers - normalizeCacheOption() output
130
+ * @param {{ embedder?: Object, retriever?: Object, llm?: Object }} components
131
+ */
132
+ constructor(layers, components) {
133
+ this.layers = layers;
134
+ this.components = components;
135
+ }
136
+
137
+ /** @private */
138
+ _key(layer, parts) {
139
+ const config = this.layers[layer];
140
+ if (!config) return null;
141
+ const component = this.components[ROLE[layer]];
142
+ const id = componentKey(component, ROLE[layer]);
143
+ if (!id) {
144
+ config.skipped += 1;
145
+ warnOnce(
146
+ `RAG_CACHE_NO_KEY_${layer.toUpperCase()}`,
147
+ `The ${layer} cache is off for this ${ROLE[layer]}: it has no getCacheKey(role) method or cacheKey string, so cached results could not be told apart from another configuration's. Add one to enable it.`,
148
+ );
149
+ return null;
150
+ }
151
+ return `${layer}:${hash([id, parts])}`;
152
+ }
153
+
154
+ /** @private */
155
+ async _get(layer, key, status) {
156
+ if (!key) {
157
+ status[layer] = 'skip';
158
+ return undefined;
159
+ }
160
+ const config = this.layers[layer];
161
+ let value;
162
+ try {
163
+ value = await config.backend.get(key);
164
+ } catch (error) {
165
+ // A cache outage must not fail the run.
166
+ warnOnce('RAG_CACHE_READ_FAILED', `Cache read failed: ${error.message}`);
167
+ value = undefined;
168
+ }
169
+ if (value === undefined) {
170
+ config.misses += 1;
171
+ status[layer] = 'miss';
172
+ } else {
173
+ config.hits += 1;
174
+ status[layer] = 'hit';
175
+ }
176
+ return value;
177
+ }
178
+
179
+ /** @private */
180
+ async _set(layer, key, value) {
181
+ if (!key || value === undefined) return;
182
+ const config = this.layers[layer];
183
+ try {
184
+ await config.backend.set(key, value, { ttlSeconds: config.ttlSeconds });
185
+ } catch (error) {
186
+ warnOnce(
187
+ 'RAG_CACHE_WRITE_FAILED',
188
+ `Cache write failed: ${error.message}`,
189
+ );
190
+ }
191
+ }
192
+
193
+ embeddingKey(query) {
194
+ return this._key('embeddings', { query });
195
+ }
196
+
197
+ retrievalKey({ query, queryVector, topK }) {
198
+ // Keyed on the query vector, not just the text, so a different embedder
199
+ // (or a retrained one) never gets another embedder's results; the text
200
+ // stays in the key for keyword and hybrid retrievers.
201
+ return this._key('retrieval', {
202
+ query: query ?? null,
203
+ vector: queryVector ? hash(queryVector) : null,
204
+ topK,
205
+ });
206
+ }
207
+
208
+ /**
209
+ * @returns {string|null} The key, or null to skip (temperature above 0,
210
+ * or no llm cache key)
211
+ */
212
+ generationKey({ query, docs, options }) {
213
+ if (!this.layers.generation) return null;
214
+ const llm = this.components.llm;
215
+ const temperature =
216
+ options.temperature ?? (llm && llm.config && llm.config.temperature);
217
+ if (typeof temperature === 'number' && temperature > 0) {
218
+ this.layers.generation.skipped += 1;
219
+ return null;
220
+ }
221
+ const generationOptions = {};
222
+ for (const name of GENERATION_OPTIONS) {
223
+ if (options[name] !== undefined) generationOptions[name] = options[name];
224
+ }
225
+ const context = (docs || []).map((d) =>
226
+ typeof d === 'string' ? d : [d.id ?? null, d.content ?? d.text ?? ''],
227
+ );
228
+ return this._key('generation', {
229
+ query,
230
+ context: hash(context),
231
+ options: generationOptions,
232
+ });
233
+ }
234
+
235
+ get(layer, key, status) {
236
+ if (!this.layers[layer]) return Promise.resolve(undefined);
237
+ return this._get(layer, key, status);
238
+ }
239
+
240
+ set(layer, key, value) {
241
+ if (!this.layers[layer]) return Promise.resolve();
242
+ return this._set(layer, key, value);
243
+ }
244
+
245
+ /** Hits, misses and skips per layer since the pipeline was created. */
246
+ stats() {
247
+ const out = {};
248
+ for (const [layer, c] of Object.entries(this.layers)) {
249
+ out[layer] = { hits: c.hits, misses: c.misses, skipped: c.skipped };
250
+ }
251
+ return out;
252
+ }
253
+ }
254
+
255
+ module.exports = {
256
+ normalizeCacheOption,
257
+ PipelineCache,
258
+ componentKey,
259
+ DEFAULT_TTL_SECONDS,
260
+ };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CacheBackend on Redis. Pass a connected client from `redis` (node-redis 4
5
+ * and later) or `ioredis`; neither is a dependency of this package.
6
+ *
7
+ * Values are stored as JSON under `prefix + key`, with a millisecond expiry
8
+ * when a time to live applies.
9
+ */
10
+ class RedisCache {
11
+ /**
12
+ * @param {Object} options
13
+ * @param {Object} options.client - Connected node-redis or ioredis client
14
+ * @param {string} [options.prefix='rag:'] - Key prefix
15
+ * @param {number} [options.ttlSeconds] - Default time to live
16
+ */
17
+ constructor({ client, prefix = 'rag:', ttlSeconds } = {}) {
18
+ if (
19
+ !client ||
20
+ typeof client.get !== 'function' ||
21
+ typeof client.set !== 'function'
22
+ ) {
23
+ throw new Error(
24
+ 'RedisCache needs a connected client from "redis" or "ioredis"',
25
+ );
26
+ }
27
+ this.client = client;
28
+ this.prefix = prefix;
29
+ this.ttlSeconds = ttlSeconds;
30
+ // node-redis 4+ exposes isOpen and takes set(key, value, { PX }); ioredis
31
+ // takes set(key, value, 'PX', ms).
32
+ this._nodeRedis = typeof client.isOpen === 'boolean';
33
+ this._hits = 0;
34
+ this._misses = 0;
35
+ }
36
+
37
+ /**
38
+ * @param {string} key
39
+ * @returns {Promise<unknown|undefined>}
40
+ */
41
+ async get(key) {
42
+ const raw = await this.client.get(this.prefix + key);
43
+ if (raw === null || raw === undefined) {
44
+ this._misses += 1;
45
+ return undefined;
46
+ }
47
+ this._hits += 1;
48
+ return JSON.parse(raw);
49
+ }
50
+
51
+ /**
52
+ * @param {string} key
53
+ * @param {unknown} value - JSON-serialisable value
54
+ * @param {{ ttlSeconds?: number }} [options]
55
+ */
56
+ async set(key, value, { ttlSeconds = this.ttlSeconds } = {}) {
57
+ const json = JSON.stringify(value);
58
+ if (json === undefined) return;
59
+ const k = this.prefix + key;
60
+ const ms = ttlSeconds > 0 ? Math.round(ttlSeconds * 1000) : 0;
61
+ if (!ms) {
62
+ await this.client.set(k, json);
63
+ } else if (this._nodeRedis) {
64
+ await this.client.set(k, json, { PX: ms });
65
+ } else {
66
+ await this.client.set(k, json, 'PX', ms);
67
+ }
68
+ }
69
+
70
+ /** @param {string} key */
71
+ async delete(key) {
72
+ const del = this.client.del || this.client.unlink;
73
+ if (typeof del === 'function')
74
+ await del.call(this.client, this.prefix + key);
75
+ }
76
+
77
+ /** Hits and misses seen by this instance (size is not tracked). */
78
+ async stats() {
79
+ return { hits: this._hits, misses: this._misses };
80
+ }
81
+ }
82
+
83
+ module.exports = { RedisCache };
@@ -7,13 +7,9 @@
7
7
 
8
8
  const { Command, Option } = require("commander");
9
9
  const { parsePositiveInteger } = require("../shared/cli-utils.js");
10
- const {
11
- handleTrainEmbedding,
12
- handleBenchmark,
13
- handleFeedback,
14
- handleSearch,
15
- handleMultimodalProcess,
16
- } = require("../handlers/ai-handler.js");
10
+ // The handler (document loaders: jsdom, marked, csv-parse) loads when an
11
+ // ai command runs, not when the CLI starts.
12
+ const aiHandler = () => require("../handlers/ai-handler.js"); // eslint-disable-line global-require
17
13
 
18
14
  /**
19
15
  * @param {() => object} getGlobalOptions - Returns program.opts()
@@ -37,7 +33,7 @@ function createAiCommand(getGlobalOptions) {
37
33
  "Train on documents from this file or directory instead",
38
34
  )
39
35
  .action(async (inputPath) => {
40
- await handleTrainEmbedding(getGlobalOptions(), inputPath);
36
+ await aiHandler().handleTrainEmbedding(getGlobalOptions(), inputPath);
41
37
  });
42
38
 
43
39
  ai.command("benchmark")
@@ -52,7 +48,7 @@ function createAiCommand(getGlobalOptions) {
52
48
  .option("--top-k <n>", "Passages to retrieve", parsePositiveInteger, 3)
53
49
  .option("--json", "Print the measurements as JSON")
54
50
  .action(async (options) => {
55
- await handleBenchmark(getGlobalOptions(), options);
51
+ await aiHandler().handleBenchmark(getGlobalOptions(), options);
56
52
  });
57
53
 
58
54
  ai.command("feedback")
@@ -67,7 +63,12 @@ function createAiCommand(getGlobalOptions) {
67
63
  .makeOptionMandatory(),
68
64
  )
69
65
  .action(async (question, docId, options) => {
70
- await handleFeedback(getGlobalOptions(), question, docId, options);
66
+ await aiHandler().handleFeedback(
67
+ getGlobalOptions(),
68
+ question,
69
+ docId,
70
+ options,
71
+ );
71
72
  });
72
73
 
73
74
  ai.command("search")
@@ -78,7 +79,7 @@ function createAiCommand(getGlobalOptions) {
78
79
  .option("--top-k <n>", "Passages to return", parsePositiveInteger, 3)
79
80
  .option("--json", "Print the results as JSON")
80
81
  .action(async (question, options) => {
81
- await handleSearch(getGlobalOptions(), question, options);
82
+ await aiHandler().handleSearch(getGlobalOptions(), question, options);
82
83
  });
83
84
 
84
85
  const multimodal = ai
@@ -92,7 +93,11 @@ function createAiCommand(getGlobalOptions) {
92
93
  .argument("<path>", "File or directory")
93
94
  .option("--json", "Print the report as JSON")
94
95
  .action(async (inputPath, options) => {
95
- await handleMultimodalProcess(getGlobalOptions(), inputPath, options);
96
+ await aiHandler().handleMultimodalProcess(
97
+ getGlobalOptions(),
98
+ inputPath,
99
+ options,
100
+ );
96
101
  });
97
102
 
98
103
  return ai;
@@ -7,9 +7,14 @@ const { Command } = require("commander");
7
7
  const { PluginHub } = require("../../ecosystem/plugin-hub");
8
8
  const { PluginCertification } = require("../../ecosystem/plugin-certification");
9
9
 
10
- const hubCommands = require("./plugin-hub/hub-commands");
11
- const certificationCommands = require("./plugin-hub/certification-commands");
12
- const reviewCommands = require("./plugin-hub/review-commands");
10
+ // Command modules (and inquirer, ora, cli-table3) load when a hub command
11
+ // runs, not when the CLI starts.
12
+ /* eslint-disable global-require */
13
+ const hubCommands = () => require("./plugin-hub/hub-commands");
14
+ const certificationCommands = () =>
15
+ require("./plugin-hub/certification-commands");
16
+ const reviewCommands = () => require("./plugin-hub/review-commands");
17
+ /* eslint-enable global-require */
13
18
  const { setupEventListeners } = require("./plugin-hub/event-handlers");
14
19
  const {
15
20
  installFromSource,
@@ -102,7 +107,7 @@ class PluginHubCLI {
102
107
  "relevance",
103
108
  )
104
109
  .action(async (query, _options) => {
105
- await hubCommands.searchPlugins(this.hub, query, _options);
110
+ await hubCommands().searchPlugins(this.hub, query, _options);
106
111
  });
107
112
 
108
113
  program
@@ -128,7 +133,7 @@ class PluginHubCLI {
128
133
  .command("info <plugin>")
129
134
  .description("Get detailed information about a plugin")
130
135
  .action(async (plugin) => {
131
- await hubCommands.getPluginInfo(this.hub, plugin);
136
+ await hubCommands().getPluginInfo(this.hub, plugin);
132
137
  });
133
138
 
134
139
  // List installed command
@@ -148,7 +153,7 @@ class PluginHubCLI {
148
153
  .option("--dry-run", "Validate without publishing")
149
154
  .option("--tag <tag>", "Release tag")
150
155
  .action(async (pluginPath, _options) => {
151
- await hubCommands.publishPlugin(
156
+ await hubCommands().publishPlugin(
152
157
  this.hub,
153
158
  pluginPath || process.cwd(),
154
159
  _options,
@@ -161,7 +166,7 @@ class PluginHubCLI {
161
166
  .description("Rate a plugin (1-5 stars)")
162
167
  .option("-r, --review <review>", "Written review")
163
168
  .action(async (plugin, rating, _options) => {
164
- await reviewCommands.ratePlugin(
169
+ await reviewCommands().ratePlugin(
165
170
  this.hub,
166
171
  plugin,
167
172
  parseInt(rating),
@@ -176,7 +181,7 @@ class PluginHubCLI {
176
181
  .option("-l, --limit <limit>", "Number of reviews", parseInt, 10)
177
182
  .option("--sort <sort>", "Sort by: helpful, recent, rating", "helpful")
178
183
  .action(async (plugin, _options) => {
179
- await reviewCommands.getPluginReviews(this.hub, plugin, _options);
184
+ await reviewCommands().getPluginReviews(this.hub, plugin, _options);
180
185
  });
181
186
 
182
187
  // Trending command
@@ -187,7 +192,7 @@ class PluginHubCLI {
187
192
  .option("-c, --category <category>", "Filter by category")
188
193
  .option("-l, --limit <limit>", "Number of results", parseInt, 20)
189
194
  .action(async (_options) => {
190
- await hubCommands.getTrendingPlugins(this.hub, _options);
195
+ await hubCommands().getTrendingPlugins(this.hub, _options);
191
196
  });
192
197
 
193
198
  // Certification commands
@@ -210,7 +215,7 @@ class PluginHubCLI {
210
215
  .command("verify <plugin> <certificationId>")
211
216
  .description("Verify plugin certification")
212
217
  .action(async (plugin, certificationId) => {
213
- await certificationCommands.verifyCertification(
218
+ await certificationCommands().verifyCertification(
214
219
  this.certification,
215
220
  plugin,
216
221
  certificationId,
@@ -222,7 +227,7 @@ class PluginHubCLI {
222
227
  .description("Show certification requirements")
223
228
  .action(async (level) => {
224
229
  // Requirements are a static list; no registry is contacted.
225
- await certificationCommands.showCertificationRequirements(
230
+ await certificationCommands().showCertificationRequirements(
226
231
  new PluginCertification(),
227
232
  level,
228
233
  );
@@ -237,7 +242,7 @@ class PluginHubCLI {
237
242
  .command("status [publisherId]")
238
243
  .description("Check publisher verification status")
239
244
  .action(async (publisherId) => {
240
- await certificationCommands.getPublisherStatus(
245
+ await certificationCommands().getPublisherStatus(
241
246
  this.certification,
242
247
  publisherId,
243
248
  );
@@ -247,7 +252,7 @@ class PluginHubCLI {
247
252
  .command("apply")
248
253
  .description("Apply for publisher verification")
249
254
  .action(async () => {
250
- await certificationCommands.applyForPublisherVerification(
255
+ await certificationCommands().applyForPublisherVerification(
251
256
  this.certification,
252
257
  );
253
258
  });