n2-qln 3.2.0 → 3.3.1

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/README.ko.md CHANGED
@@ -380,7 +380,7 @@ module.exports = {
380
380
  };
381
381
  ```
382
382
 
383
- [n2-soul 클라우드 스토리지](https://github.com/choihyunsus/n2-soul#%EF%B8%8F-cloud-storage--store-your-ai-memory-anywhere)와 동일한 방식입니다. SQLite 파일이 해당 폴더에 저장되고, 동기화 서비스가 나머지를 처리합니다.
383
+ [n2-soul 클라우드 스토리지](https://github.com/choihyunsus/soul#%EF%B8%8F-cloud-storage--store-your-ai-memory-anywhere)와 동일한 방식입니다. SQLite 파일이 해당 폴더에 저장되고, 동기화 서비스가 나머지를 처리합니다.
384
384
 
385
385
  ---
386
386
 
@@ -420,11 +420,11 @@ n2-qln/
420
420
 
421
421
  | 프로젝트 | 관계 |
422
422
  |---------|------|
423
- | [n2-soul](https://github.com/choihyunsus/n2-soul) | AI 에이전트 오케스트레이터 — QLN은 Soul의 "도구 브레인" 역할 |
423
+ | [n2-soul](https://github.com/choihyunsus/soul) | AI 에이전트 오케스트레이터 — QLN은 Soul의 "도구 브레인" 역할 |
424
424
 
425
425
  ## 실전 검증 완료
426
426
 
427
- 주말 프로토타입이 아닙니다. QLN은 **2개월 이상 운영 환경에서 검증**되었으며, [n2-soul](https://github.com/choihyunsus/n2-soul)의 핵심 도구 라우터로 매일 실사용되고 있습니다.
427
+ 주말 프로토타입이 아닙니다. QLN은 **2개월 이상 운영 환경에서 검증**되었으며, [n2-soul](https://github.com/choihyunsus/soul)의 핵심 도구 라우터로 매일 실사용되고 있습니다.
428
428
 
429
429
  **Rose** 🌹 제작 — N2의 첫 번째 AI 에이전트. 하루에 수백 번 QLN을 통해 라우팅합니다.
430
430
 
package/README.md CHANGED
@@ -400,7 +400,7 @@ module.exports = {
400
400
  };
401
401
  ```
402
402
 
403
- Same approach as [n2-soul Cloud Storage](https://github.com/choihyunsus/n2-soul#%EF%B8%8F-cloud-storage--store-your-ai-memory-anywhere). SQLite file lives in that folder — your sync service handles the rest.
403
+ Same approach as [n2-soul Cloud Storage](https://github.com/choihyunsus/soul#%EF%B8%8F-cloud-storage--store-your-ai-memory-anywhere). SQLite file lives in that folder — your sync service handles the rest.
404
404
 
405
405
  ---
406
406
 
@@ -440,11 +440,11 @@ n2-qln/
440
440
 
441
441
  | Project | Relationship |
442
442
  |---------|-------------|
443
- | [n2-soul](https://github.com/choihyunsus/n2-soul) | AI agent orchestrator — QLN serves as Soul's "tool brain" |
443
+ | [n2-soul](https://github.com/choihyunsus/soul) | AI agent orchestrator — QLN serves as Soul's "tool brain" |
444
444
 
445
445
  ## Built & Battle-Tested
446
446
 
447
- This isn't a weekend prototype. QLN has been **tested in production for 2+ months** and is actively used every day as the core tool router for [n2-soul](https://github.com/choihyunsus/n2-soul).
447
+ This isn't a weekend prototype. QLN has been **tested in production for 2+ months** and is actively used every day as the core tool router for [n2-soul](https://github.com/choihyunsus/soul).
448
448
 
449
449
  Written by **Rose** 🌹 — N2's first AI agent, and the one who routes through QLN hundreds of times a day.
450
450
 
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
@@ -20,6 +20,12 @@ const defaults = {
20
20
  timeout: 20000,
21
21
  },
22
22
 
23
+ /** Provider manifest auto-indexing */
24
+ providers: {
25
+ enabled: true,
26
+ dir: path.join(__dirname, '..', 'providers'),
27
+ },
28
+
23
29
  /** Search configuration */
24
30
  search: {
25
31
  defaultTopK: 5,
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n2-qln",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
4
4
  "description": "Query Layer Network — Semantic tool dispatcher for MCP. Route 1000 tools through 1 router.",
5
5
  "main": "index.js",
6
6
  "bin": {