n2-qln 3.1.1 → 3.2.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/README.ko.md CHANGED
@@ -349,6 +349,39 @@ module.exports = {
349
349
  | **기본** (Ollama 없음) | Stage 1 + 2 | ⭐⭐⭐⭐ 훌륭 | 없음 |
350
350
  | **Ollama 포함** | Stage 1 + 2 + 3 | ⭐⭐⭐⭐⭐ 완벽 | Ollama 실행 필요 |
351
351
 
352
+ ### 다국어 사용자
353
+
354
+ `nomic-embed-text`는 영어에 최적화되어 있습니다. **한국어, 일본어, 중국어** 등 다른 언어를 사용한다면 다국어 모델로 교체하세요:
355
+
356
+ ```bash
357
+ ollama pull bge-m3
358
+ ```
359
+
360
+ ```javascript
361
+ // config.local.js
362
+ module.exports = {
363
+ embedding: {
364
+ enabled: true,
365
+ model: 'bge-m3', // 다국어 지원 (100개 이상 언어)
366
+ },
367
+ };
368
+ ```
369
+
370
+ 코드 수정 없이 config의 모델명만 바꾸면 됩니다.
371
+
372
+ ### 클라우드 동기화
373
+
374
+ 도구 인덱스를 여러 기기에서 동기화하고 싶다면 `dataDir`을 클라우드 폴더로 지정하세요:
375
+
376
+ ```javascript
377
+ // config.local.js
378
+ module.exports = {
379
+ dataDir: 'G:/My Drive/n2-qln', // Google Drive, OneDrive, Dropbox, NAS...
380
+ };
381
+ ```
382
+
383
+ [n2-soul 클라우드 스토리지](https://github.com/choihyunsus/n2-soul#%EF%B8%8F-cloud-storage--store-your-ai-memory-anywhere)와 동일한 방식입니다. SQLite 파일이 해당 폴더에 저장되고, 동기화 서비스가 나머지를 처리합니다.
384
+
352
385
  ---
353
386
 
354
387
  ## 프로젝트 구조
package/README.md CHANGED
@@ -369,6 +369,39 @@ module.exports = {
369
369
  | **Default** (no Ollama) | Stage 1 + 2 | ⭐⭐⭐⭐ Great | None |
370
370
  | **With Ollama** | Stage 1 + 2 + 3 | ⭐⭐⭐⭐⭐ Perfect | Ollama running |
371
371
 
372
+ ### Multilingual Users
373
+
374
+ `nomic-embed-text` is optimized for English. For **Korean, Japanese, Chinese**, or other languages, swap to a multilingual model:
375
+
376
+ ```bash
377
+ ollama pull bge-m3
378
+ ```
379
+
380
+ ```javascript
381
+ // config.local.js
382
+ module.exports = {
383
+ embedding: {
384
+ enabled: true,
385
+ model: 'bge-m3', // multilingual (100+ languages)
386
+ },
387
+ };
388
+ ```
389
+
390
+ No code changes needed — just swap the model name in config.
391
+
392
+ ### Cloud Sync
393
+
394
+ Want your tool index synced across machines? Point `dataDir` to a cloud folder:
395
+
396
+ ```javascript
397
+ // config.local.js
398
+ module.exports = {
399
+ dataDir: 'G:/My Drive/n2-qln', // Google Drive, OneDrive, Dropbox, NAS...
400
+ };
401
+ ```
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.
404
+
372
405
  ---
373
406
 
374
407
  ## Project Structure
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // QLN — Quantum Layer Network MCP server entry point
2
+ // QLN — Query Layer Network MCP server entry point
3
3
  // Semantic tool dispatcher: route 1000 tools through 1 router
4
4
  const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
5
5
  const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
@@ -46,9 +46,10 @@ async function main() {
46
46
  }
47
47
 
48
48
  // 3. Create MCP server
49
+ const pkg = require('./package.json');
49
50
  const server = new McpServer({
50
51
  name: 'n2-qln',
51
- version: '3.1.0',
52
+ version: pkg.version,
52
53
  });
53
54
 
54
55
  // 4. Register unified MCP tool (1 tool, 5 actions)
package/lib/registry.js CHANGED
@@ -156,6 +156,7 @@ class Registry {
156
156
  const entry = this._cache.get(name);
157
157
  if (!entry) return;
158
158
  entry.usageCount++;
159
+ entry.lastUsedAt = new Date().toISOString();
159
160
  const alpha = 0.1;
160
161
  entry.successRate = entry.successRate * (1 - alpha) + (success ? 1 : 0) * alpha;
161
162
  entry.updatedAt = new Date().toISOString();
@@ -203,6 +204,7 @@ class Registry {
203
204
  embedding: _parseJson(row.embedding, null),
204
205
  usageCount: row.usage_count || 0,
205
206
  successRate: row.success_rate ?? 1.0,
207
+ lastUsedAt: row.last_used_at || null,
206
208
  registeredAt: row.registered_at,
207
209
  updatedAt: row.updated_at,
208
210
  };
package/lib/router.js CHANGED
@@ -104,13 +104,20 @@ class Router {
104
104
  } catch { /* graceful degradation */ }
105
105
  }
106
106
 
107
- /** Merge all stage results + usage/success bonus → ranking */
107
+ /** Merge all stage results + usage/success bonus + recency decay → ranking */
108
108
  _mergeAndRank(scores, topK, threshold) {
109
109
  const results = [];
110
110
  for (const [name, s] of scores) {
111
111
  const tool = this._registry.get(name);
112
112
  if (!tool) continue;
113
- const usageBonus = Math.log2((tool.usageCount || 0) + 1) * 0.5;
113
+
114
+ // Recency Decay: usage bonus fades over 30-day half-life
115
+ const daysSinceUse = tool.lastUsedAt
116
+ ? (Date.now() - new Date(tool.lastUsedAt).getTime()) / 86400000
117
+ : 0;
118
+ const recencyFactor = tool.lastUsedAt ? Math.exp(-daysSinceUse / 30) : 1.0;
119
+ const usageBonus = Math.log2((tool.usageCount || 0) + 1) * 0.5 * recencyFactor;
120
+
114
121
  const successBonus = (tool.successRate ?? 1.0) * 1.0;
115
122
  const finalScore = (s.stage1 || 0) + (s.stage2 || 0) + (s.stage3 || 0)
116
123
  + usageBonus + successBonus;
@@ -124,6 +131,7 @@ class Router {
124
131
  semantic: s.stage3 || 0,
125
132
  usage: Math.round(usageBonus * 100) / 100,
126
133
  success: Math.round(successBonus * 100) / 100,
134
+ recencyFactor: Math.round(recencyFactor * 1000) / 1000,
127
135
  },
128
136
  description: tool.description,
129
137
  source: tool.source,
@@ -133,7 +141,30 @@ class Router {
133
141
  }
134
142
  }
135
143
  results.sort((a, b) => b.score - a.score);
136
- return results.slice(0, topK);
144
+ const ranked = results.slice(0, topK);
145
+
146
+ // 5% Explorer: inject least-used tool into last slot
147
+ if (ranked.length >= topK && topK >= 2) {
148
+ const resultNames = new Set(ranked.map(r => r.name));
149
+ const allTools = this._registry.getAll()
150
+ .filter(t => !resultNames.has(t.name))
151
+ .sort((a, b) => (a.usageCount || 0) - (b.usageCount || 0));
152
+ if (allTools.length > 0) {
153
+ const explorer = allTools[0];
154
+ ranked[ranked.length - 1] = {
155
+ name: explorer.name,
156
+ score: 0,
157
+ stages: { trigger: 0, keyword: 0, semantic: 0, usage: 0, success: 0, recencyFactor: 0 },
158
+ description: explorer.description,
159
+ source: explorer.source,
160
+ category: explorer.category,
161
+ inputSchema: explorer.inputSchema,
162
+ explorer: true,
163
+ };
164
+ }
165
+ }
166
+
167
+ return ranked;
137
168
  }
138
169
 
139
170
  /** Build vector index */
package/lib/schema.js CHANGED
@@ -21,6 +21,7 @@ function createToolEntry(raw) {
21
21
  searchText: '',
22
22
  usageCount: raw.usageCount || 0,
23
23
  successRate: typeof raw.successRate === 'number' ? raw.successRate : 1.0,
24
+ lastUsedAt: raw.lastUsedAt || null,
24
25
  embedding: raw.embedding || null,
25
26
  registeredAt: raw.registeredAt || new Date().toISOString(),
26
27
  updatedAt: new Date().toISOString(),
package/lib/store.js CHANGED
@@ -104,6 +104,7 @@ class Store {
104
104
  addCol('tools', 'provider', 'TEXT', "''");
105
105
  addCol('tools', 'examples', 'TEXT', "'[]'");
106
106
  addCol('tools', 'endpoint', 'TEXT', "''");
107
+ addCol('tools', 'last_used_at', 'TEXT', 'NULL');
107
108
 
108
109
  // Copy plugin_name → provider (if old schema has plugin_name)
109
110
  try {
@@ -130,10 +131,10 @@ class Store {
130
131
  this._db.run(`
131
132
  INSERT INTO tools (name, description, source, category, provider,
132
133
  input_schema, triggers, tags, examples, endpoint, search_text, embedding,
133
- usage_count, success_rate, updated_at)
134
+ usage_count, success_rate, last_used_at, updated_at)
134
135
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
135
136
  ?, COALESCE(?, (SELECT embedding FROM tools WHERE name = ?)),
136
- ?, ?, datetime('now'))
137
+ ?, ?, ?, datetime('now'))
137
138
  ON CONFLICT(name) DO UPDATE SET
138
139
  description = excluded.description,
139
140
  source = excluded.source,
@@ -148,6 +149,7 @@ class Store {
148
149
  embedding = COALESCE(excluded.embedding, tools.embedding),
149
150
  usage_count = excluded.usage_count,
150
151
  success_rate = excluded.success_rate,
152
+ last_used_at = excluded.last_used_at,
151
153
  updated_at = excluded.updated_at
152
154
  `, [
153
155
  entry.name, entry.description, entry.source, entry.category, entry.provider,
@@ -155,7 +157,7 @@ class Store {
155
157
  JSON.stringify(entry.tags), JSON.stringify(entry.examples), entry.endpoint || '',
156
158
  entry.searchText,
157
159
  entry.embedding ? JSON.stringify(entry.embedding) : null, entry.name,
158
- entry.usageCount, entry.successRate,
160
+ entry.usageCount, entry.successRate, entry.lastUsedAt || null,
159
161
  ]);
160
162
  this._persist();
161
163
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n2-qln",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
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": {
@@ -15,7 +15,13 @@
15
15
  "sql.js": "^1.12.0",
16
16
  "zod": "~3.24.1"
17
17
  },
18
- "keywords": ["mcp", "ai", "tool-routing", "semantic-search", "llm"],
18
+ "keywords": [
19
+ "mcp",
20
+ "ai",
21
+ "tool-routing",
22
+ "semantic-search",
23
+ "llm"
24
+ ],
19
25
  "author": "N2",
20
26
  "license": "Apache-2.0",
21
27
  "repository": {