n2-qln 3.2.0 → 3.3.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.
- package/index.js +14 -0
- package/lib/config.js +6 -0
- package/lib/provider-loader.js +126 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
7
|
|
|
8
8
|
// Core
|
|
9
|
+
const path = require('path');
|
|
9
10
|
const { loadConfig } = require('./lib/config');
|
|
10
11
|
const { Store } = require('./lib/store');
|
|
11
12
|
const { Embedding } = require('./lib/embedding');
|
|
@@ -13,6 +14,7 @@ const { Registry } = require('./lib/registry');
|
|
|
13
14
|
const { VectorIndex } = require('./lib/vector-index');
|
|
14
15
|
const { Router } = require('./lib/router');
|
|
15
16
|
const { Executor } = require('./lib/executor');
|
|
17
|
+
const { loadProviders } = require('./lib/provider-loader');
|
|
16
18
|
|
|
17
19
|
// MCP Tool (unified)
|
|
18
20
|
const { registerQlnCall } = require('./tools/qln-call');
|
|
@@ -31,6 +33,18 @@ async function main() {
|
|
|
31
33
|
const registry = new Registry(store, embedding);
|
|
32
34
|
registry.load();
|
|
33
35
|
|
|
36
|
+
// 1.5. Provider auto-indexing (providers/*.json → registry)
|
|
37
|
+
if (config.providers?.enabled !== false) {
|
|
38
|
+
const provDir = config.providers?.dir || path.join(__dirname, 'providers');
|
|
39
|
+
const provResult = loadProviders(provDir, registry);
|
|
40
|
+
if (provResult.loaded > 0) {
|
|
41
|
+
console.error(`[QLN] Providers: ${provResult.loaded} tools from ${provResult.details.filter(d => d.status === 'loaded').length} files`);
|
|
42
|
+
}
|
|
43
|
+
if (provResult.failed > 0) {
|
|
44
|
+
console.error(`[QLN] Provider warnings: ${provResult.failed} files failed to load`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
const vectorIndex = new VectorIndex();
|
|
35
49
|
const router = new Router(registry, vectorIndex, embedding);
|
|
36
50
|
const executor = new Executor(config.executor || {});
|
package/lib/config.js
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// QLN — Provider manifest loader (providers/*.json → registry auto-registration)
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { inferCategory } = require('./schema');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Required fields for a valid provider manifest.
|
|
8
|
+
* @type {string[]}
|
|
9
|
+
*/
|
|
10
|
+
const REQUIRED_MANIFEST_FIELDS = ['provider', 'tools'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Required fields for each tool entry within a manifest.
|
|
14
|
+
* @type {string[]}
|
|
15
|
+
*/
|
|
16
|
+
const REQUIRED_TOOL_FIELDS = ['name', 'description'];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load all provider manifests from a directory and register their tools.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} providersDir - Absolute path to providers/ directory
|
|
22
|
+
* @param {import('./registry').Registry} registry - QLN registry instance
|
|
23
|
+
* @returns {{ loaded: number, skipped: number, failed: number, details: object[] }}
|
|
24
|
+
*/
|
|
25
|
+
function loadProviders(providersDir, registry) {
|
|
26
|
+
const result = { loaded: 0, skipped: 0, failed: 0, details: [] };
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(providersDir)) return result;
|
|
29
|
+
|
|
30
|
+
/** @type {string[]} */
|
|
31
|
+
const files = fs.readdirSync(providersDir)
|
|
32
|
+
.filter(f => f.endsWith('.json'));
|
|
33
|
+
|
|
34
|
+
if (files.length === 0) return result;
|
|
35
|
+
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const filePath = path.join(providersDir, file);
|
|
38
|
+
try {
|
|
39
|
+
const manifest = _parseManifest(filePath);
|
|
40
|
+
if (!manifest) {
|
|
41
|
+
result.skipped++;
|
|
42
|
+
result.details.push({ file, status: 'skipped', reason: 'invalid manifest' });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const tools = _normalizeTools(manifest);
|
|
47
|
+
if (tools.length === 0) {
|
|
48
|
+
result.skipped++;
|
|
49
|
+
result.details.push({ file, status: 'skipped', reason: 'no valid tools' });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Idempotent: purge old entries from this provider before re-registering
|
|
54
|
+
registry.purgeBySource(`provider:${manifest.provider}`);
|
|
55
|
+
|
|
56
|
+
const count = registry.registerBatch(tools);
|
|
57
|
+
result.loaded += count;
|
|
58
|
+
result.details.push({
|
|
59
|
+
file,
|
|
60
|
+
status: 'loaded',
|
|
61
|
+
provider: manifest.provider,
|
|
62
|
+
toolCount: count,
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
result.failed++;
|
|
66
|
+
result.details.push({ file, status: 'failed', reason: err.message });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse and validate a manifest JSON file.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} filePath - Absolute path to JSON file
|
|
77
|
+
* @returns {object|null} Parsed manifest or null if invalid
|
|
78
|
+
*/
|
|
79
|
+
function _parseManifest(filePath) {
|
|
80
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
81
|
+
const manifest = JSON.parse(raw);
|
|
82
|
+
|
|
83
|
+
// Validate required fields
|
|
84
|
+
for (const field of REQUIRED_MANIFEST_FIELDS) {
|
|
85
|
+
if (!manifest[field]) return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// tools must be a non-empty array
|
|
89
|
+
if (!Array.isArray(manifest.tools)) return null;
|
|
90
|
+
|
|
91
|
+
return manifest;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Normalize tool entries from a manifest for registry registration.
|
|
96
|
+
* Injects provider metadata and assigns source = "provider:{name}".
|
|
97
|
+
*
|
|
98
|
+
* @param {object} manifest - Validated manifest object
|
|
99
|
+
* @returns {object[]} Array of normalized tool entries ready for registerBatch()
|
|
100
|
+
*/
|
|
101
|
+
function _normalizeTools(manifest) {
|
|
102
|
+
const providerName = manifest.provider;
|
|
103
|
+
const tools = [];
|
|
104
|
+
|
|
105
|
+
for (const raw of manifest.tools) {
|
|
106
|
+
// Skip tools missing required fields
|
|
107
|
+
if (!raw.name || !raw.description) continue;
|
|
108
|
+
|
|
109
|
+
tools.push({
|
|
110
|
+
name: raw.name,
|
|
111
|
+
description: raw.description,
|
|
112
|
+
source: `provider:${providerName}`,
|
|
113
|
+
category: raw.category || inferCategory(raw.name, 'provider'),
|
|
114
|
+
provider: providerName,
|
|
115
|
+
inputSchema: raw.inputSchema || null,
|
|
116
|
+
triggers: raw.triggers || undefined, // let schema.js extract
|
|
117
|
+
tags: raw.tags || [],
|
|
118
|
+
examples: raw.examples || [],
|
|
119
|
+
endpoint: raw.endpoint || '',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return tools;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { loadProviders };
|