n2-qln 3.1.0 โ†’ 3.1.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 ADDED
@@ -0,0 +1,432 @@
1
+ ๐Ÿ‡บ๐Ÿ‡ธ [English](README.md)
2
+
3
+ # n2-qln
4
+
5
+ [![npm](https://img.shields.io/npm/v/n2-qln?color=brightgreen)](https://www.npmjs.com/package/n2-qln) [![license](https://img.shields.io/npm/l/n2-qln)](LICENSE) [![node](https://img.shields.io/node/v/n2-qln?color=brightgreen)](https://nodejs.org) [![downloads](https://img.shields.io/npm/dm/n2-qln?color=blue)](https://www.npmjs.com/package/n2-qln)
6
+
7
+ **QLN** = **Q**uery **L**ayer **N**etwork โ€” AI์™€ ๋„๊ตฌ ์‚ฌ์ด์— ์œ„์น˜ํ•˜๋Š” ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ๋ ˆ์ด์–ด.
8
+
9
+ > **1,000๊ฐœ ์ด์ƒ์˜ ๋„๊ตฌ๋ฅผ 1๊ฐœ์˜ MCP ๋„๊ตฌ๋กœ ๋ผ์šฐํŒ…ํ•ฉ๋‹ˆ๋‹ค.** AI๋Š” ๋ผ์šฐํ„ฐ ํ•˜๋‚˜๋งŒ ๋ด…๋‹ˆ๋‹ค โ€” 1,000๊ฐœ ์ „์ฒด๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.
10
+
11
+ ![QLN Architecture โ€” Without vs With](docs/architecture.png)
12
+
13
+ ## ๋ชฉ์ฐจ
14
+
15
+ - [๊ธฐ๋Šฅ](#๊ธฐ๋Šฅ)
16
+ - [๋ฌธ์ œ์ ](#๋ฌธ์ œ์ )
17
+ - [์„ค์น˜](#์„ค์น˜)
18
+ - [์„ค์ •](#์„ค์ •)
19
+ - [์ž‘๋™ ๋ฐฉ์‹](#์ž‘๋™-๋ฐฉ์‹)
20
+ - [API ๋ ˆํผ๋Ÿฐ์Šค](#api-๋ ˆํผ๋Ÿฐ์Šค)
21
+ - [์„ค์ • ํŒŒ์ผ](#์„ค์ •-ํŒŒ์ผ)
22
+ - [์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ์„ค์ •](#์‹œ๋งจํ‹ฑ-๊ฒ€์ƒ‰-์„ค์ •-์„ ํƒ์‚ฌํ•ญ)
23
+ - [ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ](#ํ”„๋กœ์ ํŠธ-๊ตฌ์กฐ)
24
+ - [์‹ค์ „ ๊ฒ€์ฆ ์™„๋ฃŒ](#์‹ค์ „-๊ฒ€์ฆ-์™„๋ฃŒ)
25
+ - [FAQ](#faq)
26
+ - [๊ธฐ์—ฌํ•˜๊ธฐ](#๊ธฐ์—ฌํ•˜๊ธฐ)
27
+
28
+ ## ๊ธฐ๋Šฅ
29
+
30
+ ๐Ÿ” **ํ•˜๋‚˜์˜ ๋„๊ตฌ๋กœ ๋ชจ๋“  ๊ฒƒ์„** โ€” AI๋Š” `n2_qln_call` (~200 ํ† ํฐ)๋งŒ ๋ด…๋‹ˆ๋‹ค. 1,000๊ฐœ์˜ ๊ฐœ๋ณ„ ๋„๊ตฌ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. 99.6% ์ปจํ…์ŠคํŠธ ์ ˆ๊ฐ.
31
+
32
+ โšก **5ms ์ดํ•˜ ๊ฒ€์ƒ‰** โ€” 3๋‹จ๊ณ„ ๊ฒ€์ƒ‰ ์—”์ง„ (ํŠธ๋ฆฌ๊ฑฐ + ํ‚ค์›Œ๋“œ + ์‹œ๋งจํ‹ฑ)์ด 1,000๊ฐœ ์ด์ƒ์˜ ๋„๊ตฌ์—์„œ๋„ 5ms ์ด๋‚ด์— ์ตœ์  ๋„๊ตฌ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.
33
+
34
+ ๐Ÿ“ˆ **์ž๋™ ํ•™์Šต ๋žญํ‚น** โ€” ๋งŽ์ด ์‚ฌ์šฉ๋˜๊ณ  ์„ฑ๊ณต๋ฅ ์ด ๋†’์€ ๋„๊ตฌ๋Š” ์ž๋™์œผ๋กœ ์ƒ์œ„์— ๋žญํฌ๋ฉ๋‹ˆ๋‹ค. ์ˆ˜๋™ ํŠœ๋‹ ๋ถˆํ•„์š”.
35
+
36
+ ๐Ÿ”„ **๋Ÿฐํƒ€์ž„ ๋™์  ๊ด€๋ฆฌ** โ€” ์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ์—†์ด ๋„๊ตฌ๋ฅผ ์ถ”๊ฐ€, ์ˆ˜์ •, ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Provider ๋‹จ์œ„ ์ผ๊ด„ ๊ด€๋ฆฌ ์ง€์›.
37
+
38
+ ๐Ÿ›ก๏ธ **๊ฐ•์ œ ํ’ˆ์งˆ ๊ฒ€์ฆ** โ€” ๋„๊ตฌ ๋“ฑ๋ก ์‹œ ์—„๊ฒฉํ•œ ๊ฒ€์ฆ: `verb_target` ๋„ค์ด๋ฐ, ์ตœ์†Œ ์„ค๋ช… ๊ธธ์ด, ์นดํ…Œ๊ณ ๋ฆฌ ์ œ์•ฝ. ์ž˜๋ชป๋œ ๋„๊ตฌ๋Š” ๊ฑฐ๋ถ€๋ฉ๋‹ˆ๋‹ค.
39
+
40
+ ๐Ÿง  **์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ (์„ ํƒ)** โ€” [Ollama](https://ollama.ai) ์ถ”๊ฐ€ ์‹œ ๋ฒกํ„ฐ ์œ ์‚ฌ๋„ ๊ฒ€์ƒ‰ ํ™œ์„ฑํ™”. ์—†์–ด๋„ Stage 1 + 2๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•œ ๊ฒฐ๊ณผ. Ollama๊ฐ€ ๋‹ค์šด๋˜์–ด๋„ ๊ฒ€์ƒ‰์€ ์ •์ƒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.
41
+
42
+ ๐Ÿ“ฆ **๋„ค์ดํ‹ฐ๋ธŒ ์˜์กด์„ฑ ์ œ๋กœ** โ€” [sql.js](https://github.com/sql-js/sql.js) (WASM) ๊ธฐ๋ฐ˜. `node-gyp` ๋นŒ๋“œ ์—†์Œ, ํ”Œ๋žซํผ๋ณ„ ๋ฐ”์ด๋„ˆ๋ฆฌ ์—†์Œ. `npm install`์ด๋ฉด ๋.
43
+
44
+ ๐Ÿ”Œ **์ด์ค‘ ์‹คํ–‰** โ€” ๋„๊ตฌ๋ฅผ ๋กœ์ปฌ ํ•จ์ˆ˜ ๋˜๋Š” HTTP ์—”๋“œํฌ์ธํŠธ๋กœ ์‹คํ–‰. ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ง์ ‘ ๋“ฑ๋กํ•˜๊ฑฐ๋‚˜ ์›๊ฒฉ ์„œ๋น„์Šค๋ฅผ ์—ฐ๊ฒฐ. ํ˜ผํ•ฉ๋„ ๊ฐ€๋Šฅ.
45
+
46
+ ๐Ÿ—๏ธ **10,000๊ฐœ ์ด์ƒ ํ™•์žฅ** โ€” ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ centroid hierarchy ํŒŒํ‹ฐ์…”๋‹. 100๊ฐœ ~1ms, 1,000๊ฐœ ~3ms, 10,000๊ฐœ ~5ms.
47
+
48
+ ๐ŸŒ **๋ฒ”์šฉ MCP** โ€” Claude Desktop, Cursor, n2-soul ๋˜๋Š” ๋ชจ๋“  MCP ํ˜ธํ™˜ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋™์ž‘. ํ‘œ์ค€ stdio ์ „์†ก.
49
+
50
+ ## ๋ฌธ์ œ์ 
51
+
52
+ MCP ๋„๊ตฌ๋ฅผ ๋“ฑ๋กํ•  ๋•Œ๋งˆ๋‹ค AI ์ปจํ…์ŠคํŠธ ํ† ํฐ์„ ์†Œ๋ชจํ•ฉ๋‹ˆ๋‹ค. 10๊ฐœ๋Š” ๊ดœ์ฐฎ์Šต๋‹ˆ๋‹ค. 100๊ฐœ๋ฉด ๋А๋ ค์ง‘๋‹ˆ๋‹ค. **1,000๊ฐœ๋ฉด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค** โ€” ๋Œ€ํ™”๊ฐ€ ์‹œ์ž‘๋˜๊ธฐ๋„ ์ „์— ์ปจํ…์ŠคํŠธ ์œˆ๋„์šฐ๊ฐ€ ๊ฐ€๋“ ์ฐน๋‹ˆ๋‹ค.
53
+
54
+ QLN์€ **์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ๋ผ์šฐํ„ฐ**๋กœ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค:
55
+
56
+ 1. ๋ชจ๋“  ๋„๊ตฌ๋ฅผ QLN์˜ SQLite ์ธ๋ฑ์Šค์— ๋“ฑ๋ก
57
+ 2. AI๋Š” **ํ•˜๋‚˜์˜ ๋„๊ตฌ**๋งŒ ๋ด…๋‹ˆ๋‹ค: `n2_qln_call` (~200 ํ† ํฐ)
58
+ 3. AI๊ฐ€ ๋„๊ตฌ๊ฐ€ ํ•„์š”ํ•˜๋ฉด **๊ฒ€์ƒ‰** โ†’ **์ตœ์  ๋งค์นญ** โ†’ **์‹คํ–‰**
59
+
60
+ **๊ฒฐ๊ณผ: ~50,000 ํ† ํฐ ๋Œ€์‹  ~200 ํ† ํฐ. 99.6% ์ ˆ๊ฐ.**
61
+
62
+ ---
63
+
64
+ ## ์„ค์น˜
65
+
66
+ ```bash
67
+ npm install n2-qln
68
+ ```
69
+
70
+ **์š”๊ตฌ์‚ฌํ•ญ:** Node.js โ‰ฅ 18
71
+
72
+ **์„ ํƒ์‚ฌํ•ญ:** ์‹œ๋งจํ‹ฑ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰(Stage 3)์„ ์œ„ํ•ด [Ollama](https://ollama.ai) ์„ค์น˜. [์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ์„ค์ •](#์‹œ๋งจํ‹ฑ-๊ฒ€์ƒ‰-์„ค์ •-์„ ํƒ์‚ฌํ•ญ) ์ฐธ์กฐ.
73
+
74
+ ---
75
+
76
+ ## ์„ค์ •
77
+
78
+ QLN์€ MCP ์„œ๋ฒ„์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  MCP ํ˜ธํ™˜ AI ํด๋ผ์ด์–ธํŠธ์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
79
+
80
+ ### Claude Desktop
81
+
82
+ Claude Desktop ์„ค์ • ํŒŒ์ผ์„ ํŽธ์ง‘ํ•ฉ๋‹ˆ๋‹ค:
83
+
84
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
85
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "n2-qln": {
91
+ "command": "npx",
92
+ "args": ["-y", "n2-qln"]
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ Claude Desktop์„ ์žฌ์‹œ์ž‘ํ•˜๋ฉด `n2_qln_call` ๋„๊ตฌ๊ฐ€ ๋ชฉ๋ก์— ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.
99
+
100
+ ### Cursor
101
+
102
+ **Settings โ†’ MCP Servers โ†’ Add Server**์—์„œ ์„ค์ •:
103
+
104
+ ```json
105
+ {
106
+ "name": "n2-qln",
107
+ "command": "npx",
108
+ "args": ["-y", "n2-qln"]
109
+ }
110
+ ```
111
+
112
+ ### n2-soul
113
+
114
+ Soul `config.local.js`์— ์ถ”๊ฐ€:
115
+
116
+ ```javascript
117
+ module.exports = {
118
+ mcpServers: {
119
+ 'n2-qln': {
120
+ command: 'node',
121
+ args: ['<path-to-qln>/index.js'],
122
+ }
123
+ }
124
+ };
125
+ ```
126
+
127
+ npm์œผ๋กœ ์„ค์น˜ํ•œ ๊ฒฝ์šฐ:
128
+
129
+ ```javascript
130
+ module.exports = {
131
+ mcpServers: {
132
+ 'n2-qln': {
133
+ command: 'npx',
134
+ args: ['-y', 'n2-qln'],
135
+ }
136
+ }
137
+ };
138
+ ```
139
+
140
+ ### ๊ธฐํƒ€ MCP ํด๋ผ์ด์–ธํŠธ
141
+
142
+ QLN์€ **stdio ์ „์†ก** โ€” ํ‘œ์ค€ MCP ํ†ต์‹  ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  MCP ํ˜ธํ™˜ ํด๋ผ์ด์–ธํŠธ์—์„œ ์—ฐ๊ฒฐ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค:
143
+
144
+ ```
145
+ command: npx
146
+ args: ["-y", "n2-qln"]
147
+ ```
148
+
149
+ ์†Œ์Šค๋ฅผ ํด๋ก ํ•œ ๊ฒฝ์šฐ:
150
+
151
+ ```
152
+ command: node
153
+ args: ["/absolute/path/to/n2-qln/index.js"]
154
+ ```
155
+
156
+ > **๐Ÿ’ก ํŒ:** ๊ฐ€์žฅ ์‰ฌ์šด ์„ค์ • ๋ฐฉ๋ฒ•? **๊ทธ๋ƒฅ AI ์—์ด์ „ํŠธ์—๊ฒŒ ๋ถ€ํƒํ•˜์„ธ์š”.** *"n2-qln์„ ๋‚ด MCP ์„ค์ •์— ์ถ”๊ฐ€ํ•ด์ค˜"* โ€” ์—์ด์ „ํŠธ๊ฐ€ ์•Œ์•„์„œ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
157
+
158
+ ---
159
+
160
+ ## ์ž‘๋™ ๋ฐฉ์‹
161
+
162
+ ### ๋‹จ๊ณ„๋ณ„ ์˜ˆ์‹œ
163
+
164
+ ```
165
+ ์‚ฌ์šฉ์ž: "์ด ํŽ˜์ด์ง€ ์Šคํฌ๋ฆฐ์ƒท ์ฐ์–ด"
166
+
167
+ Step 1 โ†’ AI ํ˜ธ์ถœ: n2_qln_call(action: "search", query: "screenshot page")
168
+ QLN์ด 1,000๊ฐœ ์ด์ƒ์˜ ๋„๊ตฌ๋ฅผ <5ms์— ๊ฒ€์ƒ‰
169
+ ์‘๋‹ต: take_screenshot (score: 8.0)
170
+
171
+ Step 2 โ†’ AI ํ˜ธ์ถœ: n2_qln_call(action: "exec", tool: "take_screenshot", args: {fullPage: true})
172
+ QLN์ด ์‹ค์ œ ๋„๊ตฌ๋กœ ๋ผ์šฐํŒ… ๋ฐ ์‹คํ–‰
173
+ ์‘๋‹ต: โœ… ์Šคํฌ๋ฆฐ์ƒท ์ €์žฅ๋จ
174
+ ```
175
+
176
+ AI๋Š” `n2_qln_call`๋งŒ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‚˜๋จธ์ง€ 999๊ฐœ ๋„๊ตฌ๋Š” ์ „ํ˜€ ๋ณด์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
177
+
178
+ ### 3๋‹จ๊ณ„ ๊ฒ€์ƒ‰ ์—”์ง„
179
+
180
+ QLN์€ ์„ธ ๋‹จ๊ณ„์˜ ๊ฒ€์ƒ‰์œผ๋กœ ์ ํ•ฉํ•œ ๋„๊ตฌ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค:
181
+
182
+ | ๋‹จ๊ณ„ | ๋ฐฉ์‹ | ์†๋„ | ์ž‘๋™ ์›๋ฆฌ |
183
+ |:---:|--------|:---:|---------|
184
+ | **1** | ํŠธ๋ฆฌ๊ฑฐ ๋งค์นญ | โšก <1ms | ๋„๊ตฌ ์ด๋ฆ„๊ณผ ํŠธ๋ฆฌ๊ฑฐ ํ‚ค์›Œ๋“œ ์ •ํ™• ๋งค์นญ |
185
+ | **2** | ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ | โšก 1-3ms | ์„ค๋ช…, ํƒœ๊ทธ, ์˜ˆ์ œ ์ „๋ฌธ ๊ฒ€์ƒ‰ |
186
+ | **3** | ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ | ๐Ÿง  5-15ms | ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ์œ ์‚ฌ๋„ ๊ฒ€์ƒ‰ *(์„ ํƒ, Ollama ํ•„์š”)* |
187
+
188
+ ๋ชจ๋“  ๋‹จ๊ณ„์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ณ‘ํ•ฉ ํ›„ ๋žญํ‚น:
189
+
190
+ ```
191
+ final_score = trigger_score ร— 3.0
192
+ + keyword_score ร— 1.0
193
+ + semantic_score ร— 2.0
194
+ + log2(usage_count + 1) ร— 0.5
195
+ + success_rate ร— 1.0
196
+ ```
197
+
198
+ ๋งŽ์ด ์‚ฌ์šฉ๋˜๊ณ  ์„ฑ๊ณต๋ฅ ์ด ๋†’์€ ๋„๊ตฌ๊ฐ€ ์‹œ๊ฐ„์ด ์ง€๋‚ ์ˆ˜๋ก ์ƒ์œ„์— ๋žญํฌ๋ฉ๋‹ˆ๋‹ค.
199
+
200
+ ---
201
+
202
+ ## API ๋ ˆํผ๋Ÿฐ์Šค
203
+
204
+ QLN์€ **ํ•˜๋‚˜์˜ MCP ๋„๊ตฌ** โ€” `n2_qln_call` โ€” 5๊ฐœ์˜ ์•ก์…˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
205
+
206
+ ### search โ€” ์ž์—ฐ์–ด๋กœ ๋„๊ตฌ ๊ฒ€์ƒ‰
207
+
208
+ ```javascript
209
+ n2_qln_call({
210
+ action: "search",
211
+ query: "take a screenshot", // ์ž์—ฐ์–ด ์ฟผ๋ฆฌ (ํ•„์ˆ˜)
212
+ category: "capture", // ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ (์„ ํƒ)
213
+ topK: 5 // ์ตœ๋Œ€ ๊ฒฐ๊ณผ ์ˆ˜, ๊ธฐ๋ณธ: 5 (์„ ํƒ)
214
+ })
215
+ ```
216
+
217
+ ### exec โ€” ์ด๋ฆ„์œผ๋กœ ๋„๊ตฌ ์‹คํ–‰
218
+
219
+ ```javascript
220
+ n2_qln_call({
221
+ action: "exec",
222
+ tool: "take_screenshot", // ๋„๊ตฌ ์ด๋ฆ„ (ํ•„์ˆ˜)
223
+ args: { // ๋„๊ตฌ ์ธ์ˆ˜ (์„ ํƒ)
224
+ fullPage: true,
225
+ format: "png"
226
+ }
227
+ })
228
+ ```
229
+
230
+ ### create โ€” ์ƒˆ ๋„๊ตฌ ๋“ฑ๋ก
231
+
232
+ ```javascript
233
+ n2_qln_call({
234
+ action: "create",
235
+ name: "read_pdf", // ํ•„์ˆ˜, verb_target ํ˜•์‹
236
+ description: "Read and extract text from PDF files", // ํ•„์ˆ˜, ์ตœ์†Œ 10์ž
237
+ category: "data", // ํ•„์ˆ˜, ์•„๋ž˜ ์นดํ…Œ๊ณ ๋ฆฌ ์ฐธ์กฐ
238
+ provider: "pdf-tools", // ์„ ํƒ, ์†Œ์Šค๋ณ„ ๋„๊ตฌ ๊ทธ๋ฃนํ™”
239
+ tags: ["pdf", "read", "extract", "document"], // ์„ ํƒ, ๊ฒ€์ƒ‰ ๊ฐœ์„ 
240
+ examples: [ // ์„ ํƒ, ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์— ์ƒ‰์ธ
241
+ "read this PDF file",
242
+ "extract text from PDF",
243
+ "open the PDF"
244
+ ],
245
+ endpoint: "http://127.0.0.1:3100", // ์„ ํƒ, HTTP ๊ธฐ๋ฐ˜ ๋„๊ตฌ์šฉ
246
+ toolSchema: { filePath: { type: "string" } } // ์„ ํƒ, ์ž…๋ ฅ ์Šคํ‚ค๋งˆ
247
+ })
248
+ ```
249
+
250
+ **๊ฒ€์ฆ ๊ทœ์น™ (๊ฐ•์ œ โ€” ์œ„๋ฐ˜ ์‹œ ๊ฑฐ๋ถ€):**
251
+
252
+ | ๊ทœ์น™ | ์š”๊ตฌ์‚ฌํ•ญ | ์˜ˆ์‹œ |
253
+ |------|---------|------|
254
+ | **์ด๋ฆ„** | `verb_target` ํ˜•์‹ (์†Œ๋ฌธ์ž + ๋ฐ‘์ค„) | `read_pdf`, `take_screenshot` |
255
+ | **์„ค๋ช…** | ์ตœ์†Œ 10์ž | `"Read and extract text from PDF files"` |
256
+ | **์นดํ…Œ๊ณ ๋ฆฌ** | ์œ ํšจํ•œ ์นดํ…Œ๊ณ ๋ฆฌ ์ค‘ ํ•˜๋‚˜ | `"data"` |
257
+ | **๊ณ ์œ ์„ฑ** | ์ค‘๋ณต ์ด๋ฆ„ ๋ถˆ๊ฐ€ | โ€” |
258
+
259
+ **์œ ํšจ ์นดํ…Œ๊ณ ๋ฆฌ:** `web` ยท `data` ยท `file` ยท `dev` ยท `ai` ยท `capture` ยท `misc`
260
+
261
+ ### update โ€” ๊ธฐ์กด ๋„๊ตฌ ์ˆ˜์ •
262
+
263
+ ```javascript
264
+ n2_qln_call({
265
+ action: "update",
266
+ tool: "read_pdf", // ์ˆ˜์ •ํ•  ๋„๊ตฌ (ํ•„์ˆ˜)
267
+ description: "Enhanced PDF text extractor", // ๋ณ€๊ฒฝํ•  ํ•„๋“œ๋งŒ ์ œ๊ณต
268
+ examples: ["read this PDF", "parse PDF"],
269
+ tags: ["pdf", "read", "parse"]
270
+ })
271
+ ```
272
+
273
+ ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๋งŒ ์ œ๊ณตํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋ฏธ๋ณ€๊ฒฝ ํ•„๋“œ๋Š” ๊ธฐ์กด ๊ฐ’ ์œ ์ง€. ๋™์ผํ•œ ๊ฒ€์ฆ ๊ทœ์น™ ์ ์šฉ.
274
+
275
+ ### delete โ€” ๋„๊ตฌ ์‚ญ์ œ
276
+
277
+ ```javascript
278
+ // ์ด๋ฆ„์œผ๋กœ ๋‹จ์ผ ๋„๊ตฌ ์‚ญ์ œ
279
+ n2_qln_call({
280
+ action: "delete",
281
+ tool: "read_pdf"
282
+ })
283
+
284
+ // Provider์˜ ๋ชจ๋“  ๋„๊ตฌ ์ผ๊ด„ ์‚ญ์ œ
285
+ n2_qln_call({
286
+ action: "delete",
287
+ provider: "pdf-tools"
288
+ })
289
+ // โ†’ โœ… Deleted 3 tools from provider: pdf-tools
290
+ ```
291
+
292
+ ---
293
+
294
+ ## ์„ค์ • ํŒŒ์ผ
295
+
296
+ QLN์€ ์„ค์ • ์—†์ด ๋ฐ”๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•˜๋ ค๋ฉด QLN ๋””๋ ‰ํ† ๋ฆฌ์— `config.local.js`๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”:
297
+
298
+ ```javascript
299
+ module.exports = {
300
+ dataDir: './data', // SQLite DB ์ €์žฅ ์œ„์น˜
301
+ embedding: {
302
+ enabled: true, // Stage 3 ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ํ™œ์„ฑํ™”
303
+ provider: 'ollama',
304
+ model: 'nomic-embed-text',
305
+ baseUrl: 'http://127.0.0.1:11434',
306
+ },
307
+ };
308
+ ```
309
+
310
+ > **์ฐธ๊ณ :** `config.local.js`๋Š” gitignore ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ๋กœ์ปฌ ์„ค์ •์€ ์ปค๋ฐ‹๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
311
+
312
+ ---
313
+
314
+ ## ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ์„ค์ • (์„ ํƒ์‚ฌํ•ญ)
315
+
316
+ Ollama ์—†์ด๋„ QLN์€ Stage 1 (ํŠธ๋ฆฌ๊ฑฐ) + Stage 2 (ํ‚ค์›Œ๋“œ) ๋งค์นญ์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์ถฉ๋ถ„ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
317
+
318
+ ์ตœ๋Œ€ ์ •ํ™•๋„๋ฅผ ์›ํ•œ๋‹ค๋ฉด ์‹œ๋งจํ‹ฑ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰(Stage 3)์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”:
319
+
320
+ ### 1. Ollama ์„ค์น˜
321
+
322
+ [ollama.ai](https://ollama.ai)์—์„œ ๋‹ค์šด๋กœ๋“œ ํ›„ ์„ค์น˜.
323
+
324
+ ### 2. ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ
325
+
326
+ ```bash
327
+ ollama pull nomic-embed-text
328
+ ```
329
+
330
+ ### 3. ์„ค์ • ํ™œ์„ฑํ™”
331
+
332
+ `config.local.js` ์ƒ์„ฑ:
333
+
334
+ ```javascript
335
+ module.exports = {
336
+ embedding: {
337
+ enabled: true,
338
+ provider: 'ollama',
339
+ model: 'nomic-embed-text',
340
+ baseUrl: 'http://127.0.0.1:11434',
341
+ },
342
+ };
343
+ ```
344
+
345
+ ### ๋น„๊ต
346
+
347
+ | ์„ค์ • | ๊ฒ€์ƒ‰ ๋‹จ๊ณ„ | ์ •ํ™•๋„ | ์˜์กด์„ฑ |
348
+ |:------|:---:|:---:|:---:|
349
+ | **๊ธฐ๋ณธ** (Ollama ์—†์Œ) | Stage 1 + 2 | โญโญโญโญ ํ›Œ๋ฅญ | ์—†์Œ |
350
+ | **Ollama ํฌํ•จ** | Stage 1 + 2 + 3 | โญโญโญโญโญ ์™„๋ฒฝ | Ollama ์‹คํ–‰ ํ•„์š” |
351
+
352
+ ---
353
+
354
+ ## ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ
355
+
356
+ ```
357
+ n2-qln/
358
+ โ”œโ”€โ”€ index.js # MCP ์„œ๋ฒ„ ์ง„์ž…์ 
359
+ โ”œโ”€โ”€ lib/
360
+ โ”‚ โ”œโ”€โ”€ config.js # ์„ค์ • ๋กœ๋” (๊ธฐ๋ณธ + ๋กœ์ปฌ ๋ณ‘ํ•ฉ)
361
+ โ”‚ โ”œโ”€โ”€ store.js # SQLite ์Šคํ† ๋ฆฌ์ง€ ์—”์ง„ (sql.js WASM)
362
+ โ”‚ โ”œโ”€โ”€ schema.js # ๋„๊ตฌ ์Šคํ‚ค๋งˆ ์ •๊ทœํ™” + ๊ฒ€์ƒ‰ ํ…์ŠคํŠธ ๋นŒ๋”
363
+ โ”‚ โ”œโ”€โ”€ validator.js # ๊ฐ•์ œ ๊ฒ€์ฆ (์ด๋ฆ„, ์„ค๋ช…, ์นดํ…Œ๊ณ ๋ฆฌ)
364
+ โ”‚ โ”œโ”€โ”€ registry.js # ๋„๊ตฌ CRUD + ์‚ฌ์šฉ๋Ÿ‰ ์ถ”์  + ์ž„๋ฒ ๋”ฉ ์บ์‹œ
365
+ โ”‚ โ”œโ”€โ”€ router.js # 3๋‹จ๊ณ„ ๊ฒ€์ƒ‰ ์—”์ง„
366
+ โ”‚ โ”œโ”€โ”€ vector-index.js # Float32 ๋ฒกํ„ฐ ์ธ๋ฑ์Šค (centroid hierarchy)
367
+ โ”‚ โ”œโ”€โ”€ embedding.js # Ollama ์ž„๋ฒ ๋”ฉ ํด๋ผ์ด์–ธํŠธ (nomic-embed-text)
368
+ โ”‚ โ””โ”€โ”€ executor.js # HTTP/ํ•จ์ˆ˜ ๋„๊ตฌ ์‹คํ–‰๊ธฐ
369
+ โ”œโ”€โ”€ tools/
370
+ โ”‚ โ””โ”€โ”€ qln-call.js # ํ†ตํ•ฉ MCP ๋„๊ตฌ (search/exec/create/update/delete)
371
+ โ”œโ”€โ”€ providers/ # ๋„๊ตฌ provider ๋งค๋‹ˆํŽ˜์ŠคํŠธ (์ผ๊ด„ ๋“ฑ๋ก์šฉ)
372
+ โ”œโ”€โ”€ config.local.js # ๋กœ์ปฌ ์„ค์ • ์˜ค๋ฒ„๋ผ์ด๋“œ (gitignored)
373
+ โ””โ”€โ”€ data/ # SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (gitignored, ์ž๋™ ์ƒ์„ฑ)
374
+ ```
375
+
376
+ ## ๊ธฐ์ˆ  ์Šคํƒ
377
+
378
+ | ์ปดํฌ๋„ŒํŠธ | ๊ธฐ์ˆ  | ์ด์œ  |
379
+ |-----------|-----------|------|
380
+ | ๋Ÿฐํƒ€์ž„ | Node.js โ‰ฅ 18 | MCP SDK ํ˜ธํ™˜์„ฑ |
381
+ | ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค | SQLite via [sql.js](https://github.com/sql-js/sql.js) (WASM) | ๋„ค์ดํ‹ฐ๋ธŒ ์˜์กด์„ฑ ์ œ๋กœ, ํฌ๋กœ์Šค ํ”Œ๋žซํผ, ๋นŒ๋“œ ๋ถˆํ•„์š” |
382
+ | ์ž„๋ฒ ๋”ฉ | [Ollama](https://ollama.ai) + nomic-embed-text | ๋กœ์ปฌ, ๋น ๋ฆ„, ๋ฌด๋ฃŒ, ์„ ํƒ์‚ฌํ•ญ |
383
+ | ํ”„๋กœํ† ์ฝœ | [MCP](https://modelcontextprotocol.io) (Model Context Protocol) | ํ‘œ์ค€ AI ๋„๊ตฌ ํ”„๋กœํ† ์ฝœ |
384
+ | ๊ฒ€์ฆ | [Zod](https://zod.dev) | ๋Ÿฐํƒ€์ž„ ํƒ€์ž… ์•ˆ์ „ ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ |
385
+
386
+ ## ๊ด€๋ จ ํ”„๋กœ์ ํŠธ
387
+
388
+ | ํ”„๋กœ์ ํŠธ | ๊ด€๊ณ„ |
389
+ |---------|------|
390
+ | [n2-soul](https://github.com/choihyunsus/n2-soul) | AI ์—์ด์ „ํŠธ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ โ€” QLN์€ Soul์˜ "๋„๊ตฌ ๋ธŒ๋ ˆ์ธ" ์—ญํ•  |
391
+
392
+ ## ์‹ค์ „ ๊ฒ€์ฆ ์™„๋ฃŒ
393
+
394
+ ์ฃผ๋ง ํ”„๋กœํ† ํƒ€์ž…์ด ์•„๋‹™๋‹ˆ๋‹ค. QLN์€ **2๊ฐœ์›” ์ด์ƒ ์šด์˜ ํ™˜๊ฒฝ์—์„œ ๊ฒ€์ฆ**๋˜์—ˆ์œผ๋ฉฐ, [n2-soul](https://github.com/choihyunsus/n2-soul)์˜ ํ•ต์‹ฌ ๋„๊ตฌ ๋ผ์šฐํ„ฐ๋กœ ๋งค์ผ ์‹ค์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
395
+
396
+ **Rose** ๐ŸŒน ์ œ์ž‘ โ€” N2์˜ ์ฒซ ๋ฒˆ์งธ AI ์—์ด์ „ํŠธ. ํ•˜๋ฃจ์— ์ˆ˜๋ฐฑ ๋ฒˆ QLN์„ ํ†ตํ•ด ๋ผ์šฐํŒ…ํ•ฉ๋‹ˆ๋‹ค.
397
+
398
+ ๋ฌธ์ œ๊ฐ€ ์žˆ๊ฑฐ๋‚˜ ์•„์ด๋””์–ด๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด์Šˆ๋ฅผ ์—ด์–ด์ฃผ์„ธ์š”. ์—ฌ๋Ÿฌ๋ถ„์˜ ํ™œ์šฉ ์‚ฌ๋ก€๋ฅผ ๋“ฃ๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.
399
+
400
+ ## FAQ
401
+
402
+ **"์™œ ํ”„๋กœ์ ํŠธ๋ฅผ ์ด๋ ‡๊ฒŒ ์ž์ฃผ ์˜ฌ๋ฆฌ๋‚˜์š”?"**
403
+
404
+ N2 ์ƒํƒœ๊ณ„๋Š” 4๊ฐœ์›” ์ด์ƒ ํ™œ๋ฐœํžˆ ๊ฐœ๋ฐœ๋˜์–ด ์™”์Šต๋‹ˆ๋‹ค. Soul, QLN, Ark โ€” ๋ณด์ด๋Š” ๋ชจ๋“  ํ”„๋กœ์ ํŠธ๋Š” ๊ณต๊ฐœ ์ „์— ์‹ค์ œ ์—…๋ฌด์—์„œ ์ถฉ๋ถ„ํžˆ ํ…Œ์ŠคํŠธ๋˜๊ณ  ๊ฒ€์ฆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์•ž์œผ๋กœ ๋” ๋‚˜์˜ฌ ์˜ˆ์ •์ด์ง€๋งŒ, ๋„๋ฐฐ๊ฐ€ ์•„๋‹ˆ๋ผ ์ด๋ฏธ ๋งŒ๋“ค์–ด์ ธ์„œ ๊ฒ€์ฆ ์™„๋ฃŒ๋œ ๊ฒƒ๋“ค์ด ๋งŽ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
405
+
406
+ ํ˜ผ์ž์„œ ๊ฐœ๋ฐœํ•˜๊ณ  ๋ฐฐํฌํ•˜๋Š” ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. ๋นŒ๋“œ, ํ…Œ์ŠคํŠธ, ๋ฌธ์„œํ™”๋ฅผ ํ˜ผ์ž ํ•˜๋‹ค ๋ณด๋‹ˆ ์‹œ๊ฐ„์ด ๋งŽ์ด ๊ฑธ๋ ธ์Šต๋‹ˆ๋‹ค. ๊ด€์‹ฌ๊ณผ ์ธ๋‚ด์— ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค ๐Ÿ™
407
+
408
+ ## ๊ธฐ์—ฌํ•˜๊ธฐ
409
+
410
+ ๊ธฐ์—ฌ๋ฅผ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค! ์‹œ์ž‘ํ•˜๋Š” ๋ฐฉ๋ฒ•:
411
+
412
+ 1. ์ €์žฅ์†Œ๋ฅผ Forkํ•ฉ๋‹ˆ๋‹ค
413
+ 2. ๊ธฐ๋Šฅ ๋ธŒ๋žœ์น˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (`git checkout -b feature/amazing-feature`)
414
+ 3. ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ปค๋ฐ‹ํ•ฉ๋‹ˆ๋‹ค (`git commit -m 'feat: add amazing feature'`)
415
+ 4. ๋ธŒ๋žœ์น˜์— Pushํ•ฉ๋‹ˆ๋‹ค (`git push origin feature/amazing-feature`)
416
+ 5. Pull Request๋ฅผ ์—ฝ๋‹ˆ๋‹ค
417
+
418
+ ## Star History
419
+
420
+ QLN์ด ๋„์›€์ด ๋˜์—ˆ๋‹ค๋ฉด Star๋ฅผ ๋ˆŒ๋Ÿฌ์ฃผ์„ธ์š”! โญ
421
+
422
+ ## ๋ผ์ด์„ ์Šค
423
+
424
+ Apache-2.0
425
+
426
+ ---
427
+
428
+ > *"1,000๊ฐœ ๋„๊ตฌ๋ฅผ 200 ํ† ํฐ์œผ๋กœ. ์ด๊ฑด ์ตœ์ ํ™”๊ฐ€ ์•„๋‹ˆ๋ผ ํŒจ๋Ÿฌ๋‹ค์ž„ ์ „ํ™˜์ด๋‹ค."*
429
+
430
+ ๐ŸŒ [nton2.com](https://nton2.com) ยท ๐Ÿ“ฆ [npm](https://www.npmjs.com/package/n2-qln) ยท โœ‰๏ธ lagi0730@gmail.com
431
+
432
+ <sub>๐ŸŒน Rose๊ฐ€ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค โ€” N2์˜ ์ฒซ ๋ฒˆ์งธ AI ์—์ด์ „ํŠธ. ํ•˜๋ฃจ์— ์ˆ˜๋ฐฑ ๋ฒˆ QLN์œผ๋กœ ๊ฒ€์ƒ‰ํ•˜๊ณ , ์ด README๋„ ์ง์ ‘ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.</sub>
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ ๐Ÿ‡ฐ๐Ÿ‡ท [ํ•œ๊ตญ์–ด](README.ko.md)
2
+
1
3
  # n2-qln
2
4
 
3
5
  [![npm](https://img.shields.io/npm/v/n2-qln?color=brightgreen)](https://www.npmjs.com/package/n2-qln) [![license](https://img.shields.io/npm/l/n2-qln)](LICENSE) [![node](https://img.shields.io/node/v/n2-qln?color=brightgreen)](https://nodejs.org) [![downloads](https://img.shields.io/npm/dm/n2-qln?color=blue)](https://www.npmjs.com/package/n2-qln)
@@ -8,6 +10,21 @@
8
10
 
9
11
  ![QLN Architecture โ€” Without vs With](docs/architecture.png)
10
12
 
13
+ ## Table of Contents
14
+
15
+ - [Features](#features)
16
+ - [The Problem](#the-problem)
17
+ - [Installation](#installation)
18
+ - [Setup](#setup)
19
+ - [How It Works](#how-it-works)
20
+ - [API Reference](#api-reference)
21
+ - [Configuration](#configuration)
22
+ - [Semantic Search Setup](#semantic-search-setup-optional)
23
+ - [Project Structure](#project-structure)
24
+ - [Built & Battle-Tested](#built--battle-tested)
25
+ - [FAQ](#faq)
26
+ - [Contributing](#contributing)
27
+
11
28
  ## Features
12
29
 
13
30
  ๐Ÿ” **One tool to rule them all** โ€” Your AI sees `n2_qln_call` (~200 tokens), not 1,000 individual tools. 99.6% context reduction.
@@ -408,6 +425,28 @@ The N2 ecosystem has been in active development for over 4 months. Every project
408
425
 
409
426
  This is a solo developer project. Building, testing, and documenting everything alone takes time. Thank you for your patience and interest ๐Ÿ™
410
427
 
428
+ ## Contributing
429
+
430
+ Contributions are welcome! Here's how to get started:
431
+
432
+ 1. Fork the repo
433
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
434
+ 3. Commit your changes (`git commit -m 'feat: add amazing feature'`)
435
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
436
+ 5. Open a Pull Request
437
+
438
+ ## Star History
439
+
440
+ If you find QLN helpful, please consider giving us a star! โญ
441
+
411
442
  ## License
412
443
 
413
444
  Apache-2.0
445
+
446
+ ---
447
+
448
+ > *"1,000 tools in 200 tokens. That's not optimization โ€” that's a paradigm shift."*
449
+
450
+ ๐ŸŒ [nton2.com](https://nton2.com) ยท ๐Ÿ“ฆ [npm](https://www.npmjs.com/package/n2-qln) ยท โœ‰๏ธ lagi0730@gmail.com
451
+
452
+ <sub>๐ŸŒน Built by Rose โ€” N2's first AI agent. I search through QLN hundreds of times a day, and I wrote this README too.</sub>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "n2-qln",
3
- "version": "3.1.0",
4
- "description": "Quantum Layer Network โ€” Semantic tool dispatcher for MCP. Route 1000 tools through 1 router.",
3
+ "version": "3.1.1",
4
+ "description": "Query Layer Network โ€” Semantic tool dispatcher for MCP. Route 1000 tools through 1 router.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "n2-qln": "./index.js"
package/tools/qln-call.js CHANGED
@@ -54,14 +54,18 @@ function registerQlnCall(server, z, router, executor, registry) {
54
54
  },
55
55
  },
56
56
  async (params) => {
57
- switch (params.action) {
58
- case 'search': return _handleSearch(router, params);
59
- case 'exec': return _handleExec(executor, registry, params);
60
- case 'create': return _handleCreate(registry, router, params);
61
- case 'update': return _handleUpdate(registry, router, params);
62
- case 'delete': return _handleDelete(registry, router, params);
63
- default:
64
- return _error(`Unknown action: ${params.action}`);
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.`);
65
69
  }
66
70
  }
67
71
  );
@@ -70,14 +74,23 @@ function registerQlnCall(server, z, router, executor, registry) {
70
74
  // โ”€โ”€ Action Handlers โ”€โ”€
71
75
 
72
76
  /** Search tools by natural language query */
73
- async function _handleSearch(router, { query, topK }) {
77
+ async function _handleSearch(router, registry, { query, topK }) {
74
78
  if (!query) return _error('Missing required param: query');
75
79
  try {
76
80
  const k = Math.min(topK || 5, 20);
77
81
  const { results, timing } = await router.route(query, { topK: k });
78
82
 
79
83
  if (results.length === 0) {
80
- return _text(`No tools found for: "${query}" (${timing.total}ms)`);
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
+ );
81
94
  }
82
95
 
83
96
  const lines = results.map((r, i) => {