n2-qln 3.4.2 → 4.1.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 (57) hide show
  1. package/README.ko.md +459 -470
  2. package/README.md +459 -490
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +87 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/lib/config.d.ts +9 -0
  7. package/{lib → dist/lib}/config.js +23 -27
  8. package/dist/lib/config.js.map +1 -0
  9. package/dist/lib/embedding.d.ts +27 -0
  10. package/{lib → dist/lib}/embedding.js +39 -47
  11. package/dist/lib/embedding.js.map +1 -0
  12. package/dist/lib/executor.d.ts +57 -0
  13. package/dist/lib/executor.js +175 -0
  14. package/dist/lib/executor.js.map +1 -0
  15. package/dist/lib/mcp-discovery.d.ts +83 -0
  16. package/dist/lib/mcp-discovery.js +203 -0
  17. package/dist/lib/mcp-discovery.js.map +1 -0
  18. package/dist/lib/provider-loader.d.ts +13 -0
  19. package/dist/lib/provider-loader.js +146 -0
  20. package/dist/lib/provider-loader.js.map +1 -0
  21. package/dist/lib/registry.d.ts +38 -0
  22. package/{lib → dist/lib}/registry.js +82 -92
  23. package/dist/lib/registry.js.map +1 -0
  24. package/dist/lib/router.d.ts +63 -0
  25. package/{lib → dist/lib}/router.js +75 -117
  26. package/dist/lib/router.js.map +1 -0
  27. package/dist/lib/schema.d.ts +20 -0
  28. package/{lib → dist/lib}/schema.js +38 -30
  29. package/dist/lib/schema.js.map +1 -0
  30. package/dist/lib/store.d.ts +37 -0
  31. package/dist/lib/store.js +207 -0
  32. package/dist/lib/store.js.map +1 -0
  33. package/dist/lib/validator.d.ts +37 -0
  34. package/dist/lib/validator.js +114 -0
  35. package/dist/lib/validator.js.map +1 -0
  36. package/dist/lib/vector-index.d.ts +37 -0
  37. package/{lib → dist/lib}/vector-index.js +19 -36
  38. package/dist/lib/vector-index.js.map +1 -0
  39. package/dist/tools/qln-call.d.ts +41 -0
  40. package/dist/tools/qln-call.js +353 -0
  41. package/dist/tools/qln-call.js.map +1 -0
  42. package/dist/tools/qln-helpers.d.ts +55 -0
  43. package/dist/tools/qln-helpers.js +88 -0
  44. package/dist/tools/qln-helpers.js.map +1 -0
  45. package/dist/types.d.ts +243 -0
  46. package/dist/types.js +4 -0
  47. package/dist/types.js.map +1 -0
  48. package/index.js +3 -79
  49. package/package.json +11 -4
  50. package/.github/FUNDING.yml +0 -3
  51. package/docs/README.md +0 -2
  52. package/docs/architecture.png +0 -0
  53. package/lib/executor.js +0 -104
  54. package/lib/provider-loader.js +0 -126
  55. package/lib/store.js +0 -217
  56. package/lib/validator.js +0 -171
  57. package/tools/qln-call.js +0 -257
package/lib/store.js DELETED
@@ -1,217 +0,0 @@
1
- // QLN — SQLite storage engine (pure JS via sql.js WASM). No native dependencies.
2
- const fs = require('fs');
3
- const path = require('path');
4
-
5
- /** @type {object|null} sql.js module singleton */
6
- let _SQL = null;
7
- /** @type {Promise<object>|null} sql.js init promise */
8
- let _initPromise = null;
9
-
10
- /**
11
- * Initialize sql.js WASM module (once per process).
12
- * @returns {Promise<object>} sql.js module with Database constructor
13
- */
14
- async function initSqlJs() {
15
- if (_SQL) return _SQL;
16
- if (_initPromise) return _initPromise;
17
- _initPromise = (async () => {
18
- const initFn = require('sql.js');
19
- _SQL = await initFn();
20
- return _SQL;
21
- })();
22
- return _initPromise;
23
- }
24
-
25
- /**
26
- * QLN SQLite store.
27
- * Dedicated tool index DB — separated from Soul KV-Cache.
28
- *
29
- * File: {dataDir}/qln-tools.sqlite
30
- */
31
- class Store {
32
- /**
33
- * @param {string} dataDir - Data directory path
34
- */
35
- constructor(dataDir) {
36
- this._dataDir = dataDir;
37
- this._db = null;
38
- this._dbPath = path.join(dataDir, 'qln-tools.sqlite');
39
- }
40
-
41
- /** Async init — load sql.js + open DB + create schema */
42
- async init() {
43
- await initSqlJs();
44
- if (!fs.existsSync(this._dataDir)) {
45
- fs.mkdirSync(this._dataDir, { recursive: true });
46
- }
47
- if (fs.existsSync(this._dbPath)) {
48
- const buffer = fs.readFileSync(this._dbPath);
49
- this._db = new _SQL.Database(buffer);
50
- } else {
51
- this._db = new _SQL.Database();
52
- }
53
- this._createSchema();
54
- }
55
-
56
- /** Create tools + providers table schema */
57
- _createSchema() {
58
- this._db.run(`
59
- CREATE TABLE IF NOT EXISTS tools (
60
- name TEXT PRIMARY KEY,
61
- description TEXT DEFAULT '',
62
- source TEXT DEFAULT 'unknown',
63
- category TEXT DEFAULT 'misc',
64
- provider TEXT DEFAULT '',
65
- input_schema TEXT DEFAULT '{}',
66
- triggers TEXT DEFAULT '[]',
67
- tags TEXT DEFAULT '[]',
68
- examples TEXT DEFAULT '[]',
69
- endpoint TEXT DEFAULT '',
70
- search_text TEXT DEFAULT '',
71
- embedding TEXT DEFAULT '',
72
- usage_count INTEGER DEFAULT 0,
73
- success_rate REAL DEFAULT 1.0,
74
- registered_at TEXT DEFAULT (datetime('now')),
75
- updated_at TEXT DEFAULT (datetime('now'))
76
- )
77
- `);
78
- this._db.run(`CREATE INDEX IF NOT EXISTS idx_tools_source ON tools(source)`);
79
- this._db.run(`CREATE INDEX IF NOT EXISTS idx_tools_category ON tools(category)`);
80
-
81
- this._db.run(`
82
- CREATE TABLE IF NOT EXISTS providers (
83
- name TEXT PRIMARY KEY,
84
- version TEXT DEFAULT '1.0.0',
85
- description TEXT DEFAULT '',
86
- endpoint TEXT DEFAULT '',
87
- tool_count INTEGER DEFAULT 0,
88
- registered_at TEXT DEFAULT (datetime('now')),
89
- updated_at TEXT DEFAULT (datetime('now'))
90
- )
91
- `);
92
-
93
- // Safe migration: add new columns if missing (for existing DBs)
94
- this._migrateSchema();
95
- }
96
-
97
- /** Schema migration — safe ADD COLUMN (ignores if already exists) */
98
- _migrateSchema() {
99
- const addCol = (table, col, type, dflt) => {
100
- try {
101
- this._db.run(`ALTER TABLE ${table} ADD COLUMN ${col} ${type} DEFAULT ${dflt}`);
102
- } catch { /* column already exists */ }
103
- };
104
- addCol('tools', 'provider', 'TEXT', "''");
105
- addCol('tools', 'examples', 'TEXT', "'[]'");
106
- addCol('tools', 'endpoint', 'TEXT', "''");
107
- addCol('tools', 'last_used_at', 'TEXT', 'NULL');
108
-
109
- // Copy plugin_name → provider (if old schema has plugin_name)
110
- try {
111
- const cols = this._db.exec("PRAGMA table_info(tools)");
112
- if (cols.length > 0) {
113
- const colNames = cols[0].values.map(r => r[1]);
114
- if (colNames.includes('plugin_name') && colNames.includes('provider')) {
115
- this._db.run("UPDATE tools SET provider = plugin_name WHERE provider = '' AND plugin_name != ''");
116
- }
117
- }
118
- } catch { /* table doesn't exist yet */ }
119
-
120
- // Create provider index (safe)
121
- try {
122
- this._db.run(`CREATE INDEX IF NOT EXISTS idx_tools_provider ON tools(provider)`);
123
- } catch { /* already exists */ }
124
- }
125
-
126
- /**
127
- * Upsert a tool entry.
128
- * @param {object} entry - Normalized tool entry
129
- */
130
- upsert(entry) {
131
- this._db.run(`
132
- INSERT INTO tools (name, description, source, category, provider,
133
- input_schema, triggers, tags, examples, endpoint, search_text, embedding,
134
- usage_count, success_rate, last_used_at, updated_at)
135
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
136
- ?, COALESCE(?, (SELECT embedding FROM tools WHERE name = ?)),
137
- ?, ?, ?, datetime('now'))
138
- ON CONFLICT(name) DO UPDATE SET
139
- description = excluded.description,
140
- source = excluded.source,
141
- category = excluded.category,
142
- provider = excluded.provider,
143
- input_schema = excluded.input_schema,
144
- triggers = excluded.triggers,
145
- tags = excluded.tags,
146
- examples = excluded.examples,
147
- endpoint = excluded.endpoint,
148
- search_text = excluded.search_text,
149
- embedding = COALESCE(excluded.embedding, tools.embedding),
150
- usage_count = excluded.usage_count,
151
- success_rate = excluded.success_rate,
152
- last_used_at = excluded.last_used_at,
153
- updated_at = excluded.updated_at
154
- `, [
155
- entry.name, entry.description, entry.source, entry.category, entry.provider,
156
- JSON.stringify(entry.inputSchema), JSON.stringify(entry.triggers),
157
- JSON.stringify(entry.tags), JSON.stringify(entry.examples), entry.endpoint || '',
158
- entry.searchText,
159
- entry.embedding ? JSON.stringify(entry.embedding) : null, entry.name,
160
- entry.usageCount, entry.successRate, entry.lastUsedAt || null,
161
- ]);
162
- this._persist();
163
- }
164
-
165
- /**
166
- * Remove a tool by name.
167
- * @param {string} name
168
- * @returns {boolean}
169
- */
170
- remove(name) {
171
- this._db.run('DELETE FROM tools WHERE name = ?', [name]);
172
- this._persist();
173
- return true;
174
- }
175
-
176
- /**
177
- * Purge all tools by source (for re-sync).
178
- * @param {string} source
179
- */
180
- purgeBySource(source) {
181
- this._db.run('DELETE FROM tools WHERE source = ?', [source]);
182
- this._persist();
183
- }
184
-
185
- /**
186
- * Load all tools from DB.
187
- * @returns {object[]} Array of SQLite rows
188
- */
189
- loadAll() {
190
- const results = this._db.exec('SELECT * FROM tools ORDER BY name');
191
- if (results.length === 0) return [];
192
- const cols = results[0].columns;
193
- return results[0].values.map(row => {
194
- const obj = {};
195
- cols.forEach((c, i) => { obj[c] = row[i]; });
196
- return obj;
197
- });
198
- }
199
-
200
- /** Persist DB to disk */
201
- _persist() {
202
- if (!this._db) return;
203
- const data = this._db.export();
204
- fs.writeFileSync(this._dbPath, Buffer.from(data));
205
- }
206
-
207
- /** Close connection */
208
- dispose() {
209
- if (this._db) {
210
- this._persist();
211
- this._db.close();
212
- this._db = null;
213
- }
214
- }
215
- }
216
-
217
- module.exports = { Store, initSqlJs };
package/lib/validator.js DELETED
@@ -1,171 +0,0 @@
1
- // QLN — Tool registration validator (validator.rs pattern)
2
- // Enforced validation: errors reject registration, warnings pass with message.
3
-
4
- /** Valid tool categories */
5
- const VALID_CATEGORIES = ['web', 'data', 'file', 'dev', 'ai', 'capture', 'misc'];
6
-
7
- /** Tool name pattern: verb_target (e.g. read_pdf, take_screenshot) */
8
- const NAME_PATTERN = /^[a-z]+_[a-z][a-z0-9_]*$/;
9
-
10
- /** Minimum description length */
11
- const MIN_DESCRIPTION_LENGTH = 10;
12
-
13
- /**
14
- * Validate tool entry for registration.
15
- * Pattern: accumulate errors → reject if any error exists.
16
- *
17
- * @param {object} params - Tool creation params
18
- * @param {import('./registry').Registry} registry - For duplicate check
19
- * @returns {{ valid: boolean, errors: ValidationError[] }}
20
- */
21
- function validateToolEntry(params, registry) {
22
- /** @type {ValidationError[]} */
23
- const errors = [];
24
-
25
- // Rule 1: name required + verb_target format enforced
26
- if (!params.name || params.name.trim() === '') {
27
- errors.push({
28
- field: 'name',
29
- message: 'name은 필수입니다',
30
- severity: 'error',
31
- });
32
- } else if (!NAME_PATTERN.test(params.name)) {
33
- errors.push({
34
- field: 'name',
35
- message: `'${params.name}' → 동사_대상 형식 필수 (예: read_pdf, take_screenshot)`,
36
- severity: 'error',
37
- });
38
- }
39
-
40
- // Rule 2: description required (= first-line comment) + min 10 chars
41
- if (!params.description || params.description.trim() === '') {
42
- errors.push({
43
- field: 'description',
44
- message: 'description은 필수입니다 (도구 용도 설명)',
45
- severity: 'error',
46
- });
47
- } else if (params.description.trim().length < MIN_DESCRIPTION_LENGTH) {
48
- errors.push({
49
- field: 'description',
50
- message: `최소 ${MIN_DESCRIPTION_LENGTH}자 이상 설명 필요 (현재: ${params.description.trim().length}자)`,
51
- severity: 'error',
52
- });
53
- }
54
-
55
- // Rule 3: category enum enforced
56
- if (params.category && !VALID_CATEGORIES.includes(params.category)) {
57
- errors.push({
58
- field: 'category',
59
- message: `'${params.category}' → ${VALID_CATEGORIES.join('|')} 중 하나`,
60
- severity: 'error',
61
- });
62
- }
63
-
64
- // Rule 4: duplicate name check
65
- if (params.name && registry && registry.get(params.name)) {
66
- errors.push({
67
- field: 'name',
68
- message: `'${params.name}' 이미 존재합니다. action: "update" 를 사용하세요`,
69
- severity: 'error',
70
- });
71
- }
72
-
73
- // Warning: no tags (search accuracy may be lower)
74
- if (!params.tags || params.tags.length === 0) {
75
- errors.push({
76
- field: 'tags',
77
- message: 'tags 미지정 — 검색 정확도가 낮아질 수 있습니다',
78
- severity: 'warning',
79
- });
80
- }
81
-
82
- const errorCount = errors.filter(e => e.severity === 'error').length;
83
- return { valid: errorCount === 0, errors };
84
- }
85
-
86
- /**
87
- * Validate tool update params.
88
- * Same rules as create, but no duplicate check (updating existing tool).
89
- *
90
- * @param {object} params - Update params (only changed fields)
91
- * @param {object} existing - Current tool entry from registry
92
- * @returns {{ valid: boolean, errors: ValidationError[] }}
93
- */
94
- function validateUpdateEntry(params, existing) {
95
- /** @type {ValidationError[]} */
96
- const errors = [];
97
-
98
- // Merge: use existing values as fallback
99
- const merged = {
100
- name: params.name || existing.name,
101
- description: params.description || existing.description,
102
- category: params.category || existing.category,
103
- };
104
-
105
- // Rule 1: name format (if changed)
106
- if (params.name && !NAME_PATTERN.test(params.name)) {
107
- errors.push({
108
- field: 'name',
109
- message: `'${params.name}' → 동사_대상 형식 필수 (예: read_pdf, take_screenshot)`,
110
- severity: 'error',
111
- });
112
- }
113
-
114
- // Rule 2: description min length (if changed)
115
- if (params.description && params.description.trim().length < MIN_DESCRIPTION_LENGTH) {
116
- errors.push({
117
- field: 'description',
118
- message: `최소 ${MIN_DESCRIPTION_LENGTH}자 이상 설명 필요 (현재: ${params.description.trim().length}자)`,
119
- severity: 'error',
120
- });
121
- }
122
-
123
- // Rule 3: category enum (if changed)
124
- if (params.category && !VALID_CATEGORIES.includes(params.category)) {
125
- errors.push({
126
- field: 'category',
127
- message: `'${params.category}' → ${VALID_CATEGORIES.join('|')} 중 하나`,
128
- severity: 'error',
129
- });
130
- }
131
-
132
- const errorCount = errors.filter(e => e.severity === 'error').length;
133
- return { valid: errorCount === 0, errors };
134
- }
135
-
136
- /**
137
- * Format validation errors as user-readable string.
138
- * @param {ValidationError[]} errors
139
- * @returns {string}
140
- */
141
- function formatValidationErrors(errors) {
142
- const errorCount = errors.filter(e => e.severity === 'error').length;
143
- const warnCount = errors.filter(e => e.severity === 'warning').length;
144
-
145
- const lines = errors.map(e => {
146
- const icon = e.severity === 'error' ? '❌' : '⚠️';
147
- return ` ${icon} [${e.field}] ${e.message}`;
148
- });
149
-
150
- const header = errorCount > 0
151
- ? `등록 거부 (${errorCount} error${errorCount > 1 ? 's' : ''}${warnCount > 0 ? `, ${warnCount} warning` : ''}):`
152
- : `등록 완료 (${warnCount} warning${warnCount > 1 ? 's' : ''}):`;
153
-
154
- return `${header}\n${lines.join('\n')}`;
155
- }
156
-
157
- /**
158
- * @typedef {object} ValidationError
159
- * @property {string} field - Field name
160
- * @property {string} message - Error message
161
- * @property {'error'|'warning'} severity - Severity level
162
- */
163
-
164
- module.exports = {
165
- validateToolEntry,
166
- validateUpdateEntry,
167
- formatValidationErrors,
168
- VALID_CATEGORIES,
169
- NAME_PATTERN,
170
- MIN_DESCRIPTION_LENGTH,
171
- };
package/tools/qln-call.js DELETED
@@ -1,257 +0,0 @@
1
- // QLN MCP tool — n2_qln_call (unified tool: search/exec/create/update/delete)
2
- // Single entry point for all QLN operations. ~200 tokens in AI context.
3
- const { validateToolEntry, validateUpdateEntry, formatValidationErrors } = require('../lib/validator');
4
-
5
- /**
6
- * Register the unified n2_qln_call MCP tool.
7
- * @param {object} server - MCP server
8
- * @param {object} z - Zod validator
9
- * @param {import('../lib/router').Router} router - L1 search engine
10
- * @param {import('../lib/executor').Executor} executor - L3 tool executor
11
- * @param {import('../lib/registry').Registry} registry - L2 tool index
12
- */
13
- function registerQlnCall(server, z, router, executor, registry) {
14
- server.tool(
15
- 'n2_qln_call',
16
- {
17
- title: 'QLN Call',
18
- description:
19
- 'Query Layer Network — unified tool dispatcher. ' +
20
- 'Search 1000+ tools, execute them, or manage the index. ' +
21
- 'Actions: search, exec, create, update, delete.',
22
- inputSchema: {
23
- action: z.enum(['search', 'exec', 'create', 'update', 'delete'])
24
- .describe('Action: search | exec | create | update | delete'),
25
- // search
26
- query: z.string().optional()
27
- .describe('[search] Natural language query (e.g. "take a screenshot")'),
28
- topK: z.number().optional()
29
- .describe('[search] Number of results (default: 5, max: 20)'),
30
- // exec
31
- tool: z.string().optional()
32
- .describe('[exec/update/delete] Tool name'),
33
- args: z.record(z.unknown()).optional()
34
- .describe('[exec] Tool arguments (JSON object)'),
35
- // create / update
36
- name: z.string().optional()
37
- .describe('[create/update] Tool name (unique identifier)'),
38
- description: z.string().optional()
39
- .describe('[create/update] Tool description (used for search matching)'),
40
- source: z.string().optional()
41
- .describe('[create/update] Source: mcp, plugin, local (default: local)'),
42
- category: z.string().optional()
43
- .describe('[create/update] Category: web, data, file, dev, ai, capture, misc'),
44
- toolSchema: z.record(z.unknown()).optional()
45
- .describe('[create/update] JSON Schema for tool input'),
46
- tags: z.array(z.string()).optional()
47
- .describe('[create/update] Additional search tags'),
48
- provider: z.string().optional()
49
- .describe('[create/update] Provider name (e.g. "n2-browser", "pdf-tools")'),
50
- examples: z.array(z.string()).optional()
51
- .describe('[create/update] Usage examples (e.g. ["read this PDF file", "extract text from PDF"])'),
52
- endpoint: z.string().optional()
53
- .describe('[create/update] Execution HTTP endpoint'),
54
- },
55
- },
56
- async (params) => {
57
- try {
58
- switch (params.action) {
59
- case 'search': return await _handleSearch(router, registry, params);
60
- case 'exec': return await _handleExec(executor, registry, params);
61
- case 'create': return await _handleCreate(registry, router, params);
62
- case 'update': return await _handleUpdate(registry, router, params);
63
- case 'delete': return await _handleDelete(registry, router, params);
64
- default:
65
- return _error(`Unknown action: ${params.action}`);
66
- }
67
- } catch (err) {
68
- return _error(`QLN internal error: ${err.message}. Try the action again or use raw tool calls as fallback.`);
69
- }
70
- }
71
- );
72
- }
73
-
74
- // ── Action Handlers ──
75
-
76
- /** Search tools by natural language query */
77
- async function _handleSearch(router, registry, { query, topK }) {
78
- if (!query) return _error('Missing required param: query');
79
- try {
80
- const k = Math.min(topK || 5, 20);
81
- const { results, timing } = await router.route(query, { topK: k });
82
-
83
- if (results.length === 0) {
84
- // Blind Spot prevention: show available categories so AI doesn't hallucinate
85
- const stats = registry.stats();
86
- const categories = Object.entries(stats.byCategory)
87
- .map(([cat, count]) => `${cat}(${count})`)
88
- .join(', ');
89
- return _text(
90
- `No tools found for: "${query}" (${timing.total}ms)\n\n` +
91
- `Available categories [${stats.total} tools]: ${categories || 'none'}\n` +
92
- `→ Try a different keyword, or check available categories above.`
93
- );
94
- }
95
-
96
- const lines = results.map((r, i) => {
97
- const schemaHint = r.inputSchema
98
- ? ` | args: ${JSON.stringify(Object.keys(r.inputSchema.properties || r.inputSchema || {}))}`
99
- : '';
100
- return `${i + 1}. **${r.name}** (${r.score}) [${r.source}/${r.category}]${schemaHint}\n ${r.description || '(no description)'}`;
101
- });
102
-
103
- const top = results[0];
104
- const hint = `\n→ Execute: n2_qln_call(action: "exec", tool: "${top.name}", args: {})`;
105
-
106
- return _text(
107
- `Route "${query}" (${timing.total}ms, stages: T${timing.stage1}+K${timing.stage2}+S${timing.stage3}ms):\n\n` +
108
- `${lines.join('\n\n')}${hint}`
109
- );
110
- } catch (err) {
111
- return _error(`Search failed: ${err.message}`);
112
- }
113
- }
114
-
115
- /** Execute a tool by name */
116
- async function _handleExec(executor, registry, { tool: toolName, args }) {
117
- if (!toolName) return _error('Missing required param: tool');
118
- try {
119
- const { result, source, elapsed } = await executor.exec(toolName, args || {});
120
- registry.recordUsage(toolName, true);
121
-
122
- const resultStr = typeof result === 'string'
123
- ? result
124
- : JSON.stringify(result, null, 2);
125
-
126
- const truncated = resultStr.length > 4000
127
- ? resultStr.substring(0, 4000) + '\n... (truncated)'
128
- : resultStr;
129
-
130
- return _text(`✅ [${toolName}] (${source}, ${elapsed}ms):\n${truncated}`);
131
- } catch (err) {
132
- registry.recordUsage(toolName, false);
133
- return { content: [{ type: 'text', text: `❌ [${toolName}] failed: ${err.message}` }], isError: true };
134
- }
135
- }
136
-
137
- /** Create (register) a new tool — with enforced validation */
138
- async function _handleCreate(registry, router, params) {
139
- // Step 1: Forced validation (validator.rs pattern)
140
- const validation = validateToolEntry(params, registry);
141
- if (!validation.valid) {
142
- return { content: [{ type: 'text', text: formatValidationErrors(validation.errors) }], isError: true };
143
- }
144
-
145
- try {
146
- // Step 2: Register with auto-enrichment
147
- const entry = registry.register({
148
- name: params.name,
149
- description: params.description,
150
- source: params.source || 'local',
151
- category: params.category || undefined,
152
- provider: params.provider || '',
153
- inputSchema: params.toolSchema,
154
- tags: params.tags,
155
- examples: params.examples || [],
156
- endpoint: params.endpoint || '',
157
- });
158
-
159
- // Step 3: Rebuild vector index
160
- const indexResult = router.buildIndex();
161
-
162
- // Step 4: Return with warnings if any
163
- const warnings = validation.errors.filter(e => e.severity === 'warning');
164
- const warnMsg = warnings.length > 0
165
- ? `\n${formatValidationErrors(warnings)}`
166
- : '';
167
-
168
- return _text(
169
- `✅ Created: ${entry.name} [${entry.source}/${entry.category}]\n` +
170
- `Provider: ${entry.provider || '(none)'}\n` +
171
- `Triggers: ${entry.triggers.join(', ')}\n` +
172
- `Examples: ${entry.examples.length}\n` +
173
- `Index: ${indexResult.indexed} tools, ${indexResult.categories} categories${warnMsg}`
174
- );
175
- } catch (err) {
176
- return _error(`Create failed: ${err.message}`);
177
- }
178
- }
179
-
180
- /** Update an existing tool — with validation */
181
- async function _handleUpdate(registry, router, params) {
182
- const toolName = params.tool || params.name;
183
- if (!toolName) return _error('Missing required param: tool (or name)');
184
-
185
- const existing = registry.get(toolName);
186
- if (!existing) return _error(`Tool not found: ${toolName}`);
187
-
188
- // Validate changed fields
189
- const validation = validateUpdateEntry(params, existing);
190
- if (!validation.valid) {
191
- return { content: [{ type: 'text', text: formatValidationErrors(validation.errors) }], isError: true };
192
- }
193
-
194
- try {
195
- const entry = registry.register({
196
- name: toolName,
197
- description: params.description || existing.description,
198
- source: params.source || existing.source,
199
- category: params.category || existing.category,
200
- provider: params.provider || existing.provider,
201
- inputSchema: params.toolSchema || existing.inputSchema,
202
- tags: params.tags || existing.tags,
203
- examples: params.examples || existing.examples,
204
- endpoint: params.endpoint || existing.endpoint,
205
- });
206
-
207
- const indexResult = router.buildIndex();
208
- return _text(
209
- `✅ Updated: ${entry.name} [${entry.source}/${entry.category}]\n` +
210
- `Provider: ${entry.provider || '(none)'}\n` +
211
- `Triggers: ${entry.triggers.join(', ')}\n` +
212
- `Index: ${indexResult.indexed} tools, ${indexResult.categories} categories`
213
- );
214
- } catch (err) {
215
- return _error(`Update failed: ${err.message}`);
216
- }
217
- }
218
-
219
- /** Delete tool(s) — by name or by provider */
220
- async function _handleDelete(registry, router, params) {
221
- const toolName = params.tool || params.name;
222
-
223
- // Provider-level delete: remove all tools of a provider
224
- if (params.provider && !toolName) {
225
- const count = registry.removeByProvider(params.provider);
226
- if (count === 0) return _error(`No tools found for provider: ${params.provider}`);
227
- const indexResult = router.buildIndex();
228
- return _text(
229
- `✅ Deleted ${count} tools from provider: ${params.provider}\n` +
230
- `Index: ${indexResult.indexed} tools, ${indexResult.categories} categories`
231
- );
232
- }
233
-
234
- // Single tool delete
235
- if (!toolName) return _error('Missing required param: tool (or name). For bulk delete, use provider param.');
236
-
237
- const removed = registry.remove(toolName);
238
- if (!removed) return _error(`Tool not found: ${toolName}`);
239
-
240
- const indexResult = router.buildIndex();
241
- return _text(
242
- `✅ Deleted: ${toolName}\n` +
243
- `Index: ${indexResult.indexed} tools, ${indexResult.categories} categories`
244
- );
245
- }
246
-
247
- // ── Response Helpers ──
248
-
249
- function _text(text) {
250
- return { content: [{ type: 'text', text }] };
251
- }
252
-
253
- function _error(message) {
254
- return { content: [{ type: 'text', text: `⚠️ ${message}` }], isError: true };
255
- }
256
-
257
- module.exports = { registerQlnCall };