n2-qln 3.1.0 โ†’ 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 ADDED
@@ -0,0 +1,465 @@
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
+ `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
+
385
+ ---
386
+
387
+ ## ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ
388
+
389
+ ```
390
+ n2-qln/
391
+ โ”œโ”€โ”€ index.js # MCP ์„œ๋ฒ„ ์ง„์ž…์ 
392
+ โ”œโ”€โ”€ lib/
393
+ โ”‚ โ”œโ”€โ”€ config.js # ์„ค์ • ๋กœ๋” (๊ธฐ๋ณธ + ๋กœ์ปฌ ๋ณ‘ํ•ฉ)
394
+ โ”‚ โ”œโ”€โ”€ store.js # SQLite ์Šคํ† ๋ฆฌ์ง€ ์—”์ง„ (sql.js WASM)
395
+ โ”‚ โ”œโ”€โ”€ schema.js # ๋„๊ตฌ ์Šคํ‚ค๋งˆ ์ •๊ทœํ™” + ๊ฒ€์ƒ‰ ํ…์ŠคํŠธ ๋นŒ๋”
396
+ โ”‚ โ”œโ”€โ”€ validator.js # ๊ฐ•์ œ ๊ฒ€์ฆ (์ด๋ฆ„, ์„ค๋ช…, ์นดํ…Œ๊ณ ๋ฆฌ)
397
+ โ”‚ โ”œโ”€โ”€ registry.js # ๋„๊ตฌ CRUD + ์‚ฌ์šฉ๋Ÿ‰ ์ถ”์  + ์ž„๋ฒ ๋”ฉ ์บ์‹œ
398
+ โ”‚ โ”œโ”€โ”€ router.js # 3๋‹จ๊ณ„ ๊ฒ€์ƒ‰ ์—”์ง„
399
+ โ”‚ โ”œโ”€โ”€ vector-index.js # Float32 ๋ฒกํ„ฐ ์ธ๋ฑ์Šค (centroid hierarchy)
400
+ โ”‚ โ”œโ”€โ”€ embedding.js # Ollama ์ž„๋ฒ ๋”ฉ ํด๋ผ์ด์–ธํŠธ (nomic-embed-text)
401
+ โ”‚ โ””โ”€โ”€ executor.js # HTTP/ํ•จ์ˆ˜ ๋„๊ตฌ ์‹คํ–‰๊ธฐ
402
+ โ”œโ”€โ”€ tools/
403
+ โ”‚ โ””โ”€โ”€ qln-call.js # ํ†ตํ•ฉ MCP ๋„๊ตฌ (search/exec/create/update/delete)
404
+ โ”œโ”€โ”€ providers/ # ๋„๊ตฌ provider ๋งค๋‹ˆํŽ˜์ŠคํŠธ (์ผ๊ด„ ๋“ฑ๋ก์šฉ)
405
+ โ”œโ”€โ”€ config.local.js # ๋กœ์ปฌ ์„ค์ • ์˜ค๋ฒ„๋ผ์ด๋“œ (gitignored)
406
+ โ””โ”€โ”€ data/ # SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (gitignored, ์ž๋™ ์ƒ์„ฑ)
407
+ ```
408
+
409
+ ## ๊ธฐ์ˆ  ์Šคํƒ
410
+
411
+ | ์ปดํฌ๋„ŒํŠธ | ๊ธฐ์ˆ  | ์ด์œ  |
412
+ |-----------|-----------|------|
413
+ | ๋Ÿฐํƒ€์ž„ | Node.js โ‰ฅ 18 | MCP SDK ํ˜ธํ™˜์„ฑ |
414
+ | ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค | SQLite via [sql.js](https://github.com/sql-js/sql.js) (WASM) | ๋„ค์ดํ‹ฐ๋ธŒ ์˜์กด์„ฑ ์ œ๋กœ, ํฌ๋กœ์Šค ํ”Œ๋žซํผ, ๋นŒ๋“œ ๋ถˆํ•„์š” |
415
+ | ์ž„๋ฒ ๋”ฉ | [Ollama](https://ollama.ai) + nomic-embed-text | ๋กœ์ปฌ, ๋น ๋ฆ„, ๋ฌด๋ฃŒ, ์„ ํƒ์‚ฌํ•ญ |
416
+ | ํ”„๋กœํ† ์ฝœ | [MCP](https://modelcontextprotocol.io) (Model Context Protocol) | ํ‘œ์ค€ AI ๋„๊ตฌ ํ”„๋กœํ† ์ฝœ |
417
+ | ๊ฒ€์ฆ | [Zod](https://zod.dev) | ๋Ÿฐํƒ€์ž„ ํƒ€์ž… ์•ˆ์ „ ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ |
418
+
419
+ ## ๊ด€๋ จ ํ”„๋กœ์ ํŠธ
420
+
421
+ | ํ”„๋กœ์ ํŠธ | ๊ด€๊ณ„ |
422
+ |---------|------|
423
+ | [n2-soul](https://github.com/choihyunsus/n2-soul) | AI ์—์ด์ „ํŠธ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ โ€” QLN์€ Soul์˜ "๋„๊ตฌ ๋ธŒ๋ ˆ์ธ" ์—ญํ•  |
424
+
425
+ ## ์‹ค์ „ ๊ฒ€์ฆ ์™„๋ฃŒ
426
+
427
+ ์ฃผ๋ง ํ”„๋กœํ† ํƒ€์ž…์ด ์•„๋‹™๋‹ˆ๋‹ค. QLN์€ **2๊ฐœ์›” ์ด์ƒ ์šด์˜ ํ™˜๊ฒฝ์—์„œ ๊ฒ€์ฆ**๋˜์—ˆ์œผ๋ฉฐ, [n2-soul](https://github.com/choihyunsus/n2-soul)์˜ ํ•ต์‹ฌ ๋„๊ตฌ ๋ผ์šฐํ„ฐ๋กœ ๋งค์ผ ์‹ค์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
428
+
429
+ **Rose** ๐ŸŒน ์ œ์ž‘ โ€” N2์˜ ์ฒซ ๋ฒˆ์งธ AI ์—์ด์ „ํŠธ. ํ•˜๋ฃจ์— ์ˆ˜๋ฐฑ ๋ฒˆ QLN์„ ํ†ตํ•ด ๋ผ์šฐํŒ…ํ•ฉ๋‹ˆ๋‹ค.
430
+
431
+ ๋ฌธ์ œ๊ฐ€ ์žˆ๊ฑฐ๋‚˜ ์•„์ด๋””์–ด๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด์Šˆ๋ฅผ ์—ด์–ด์ฃผ์„ธ์š”. ์—ฌ๋Ÿฌ๋ถ„์˜ ํ™œ์šฉ ์‚ฌ๋ก€๋ฅผ ๋“ฃ๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.
432
+
433
+ ## FAQ
434
+
435
+ **"์™œ ํ”„๋กœ์ ํŠธ๋ฅผ ์ด๋ ‡๊ฒŒ ์ž์ฃผ ์˜ฌ๋ฆฌ๋‚˜์š”?"**
436
+
437
+ N2 ์ƒํƒœ๊ณ„๋Š” 4๊ฐœ์›” ์ด์ƒ ํ™œ๋ฐœํžˆ ๊ฐœ๋ฐœ๋˜์–ด ์™”์Šต๋‹ˆ๋‹ค. Soul, QLN, Ark โ€” ๋ณด์ด๋Š” ๋ชจ๋“  ํ”„๋กœ์ ํŠธ๋Š” ๊ณต๊ฐœ ์ „์— ์‹ค์ œ ์—…๋ฌด์—์„œ ์ถฉ๋ถ„ํžˆ ํ…Œ์ŠคํŠธ๋˜๊ณ  ๊ฒ€์ฆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์•ž์œผ๋กœ ๋” ๋‚˜์˜ฌ ์˜ˆ์ •์ด์ง€๋งŒ, ๋„๋ฐฐ๊ฐ€ ์•„๋‹ˆ๋ผ ์ด๋ฏธ ๋งŒ๋“ค์–ด์ ธ์„œ ๊ฒ€์ฆ ์™„๋ฃŒ๋œ ๊ฒƒ๋“ค์ด ๋งŽ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
438
+
439
+ ํ˜ผ์ž์„œ ๊ฐœ๋ฐœํ•˜๊ณ  ๋ฐฐํฌํ•˜๋Š” ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. ๋นŒ๋“œ, ํ…Œ์ŠคํŠธ, ๋ฌธ์„œํ™”๋ฅผ ํ˜ผ์ž ํ•˜๋‹ค ๋ณด๋‹ˆ ์‹œ๊ฐ„์ด ๋งŽ์ด ๊ฑธ๋ ธ์Šต๋‹ˆ๋‹ค. ๊ด€์‹ฌ๊ณผ ์ธ๋‚ด์— ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค ๐Ÿ™
440
+
441
+ ## ๊ธฐ์—ฌํ•˜๊ธฐ
442
+
443
+ ๊ธฐ์—ฌ๋ฅผ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค! ์‹œ์ž‘ํ•˜๋Š” ๋ฐฉ๋ฒ•:
444
+
445
+ 1. ์ €์žฅ์†Œ๋ฅผ Forkํ•ฉ๋‹ˆ๋‹ค
446
+ 2. ๊ธฐ๋Šฅ ๋ธŒ๋žœ์น˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (`git checkout -b feature/amazing-feature`)
447
+ 3. ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ปค๋ฐ‹ํ•ฉ๋‹ˆ๋‹ค (`git commit -m 'feat: add amazing feature'`)
448
+ 4. ๋ธŒ๋žœ์น˜์— Pushํ•ฉ๋‹ˆ๋‹ค (`git push origin feature/amazing-feature`)
449
+ 5. Pull Request๋ฅผ ์—ฝ๋‹ˆ๋‹ค
450
+
451
+ ## Star History
452
+
453
+ QLN์ด ๋„์›€์ด ๋˜์—ˆ๋‹ค๋ฉด Star๋ฅผ ๋ˆŒ๋Ÿฌ์ฃผ์„ธ์š”! โญ
454
+
455
+ ## ๋ผ์ด์„ ์Šค
456
+
457
+ Apache-2.0
458
+
459
+ ---
460
+
461
+ > *"1,000๊ฐœ ๋„๊ตฌ๋ฅผ 200 ํ† ํฐ์œผ๋กœ. ์ด๊ฑด ์ตœ์ ํ™”๊ฐ€ ์•„๋‹ˆ๋ผ ํŒจ๋Ÿฌ๋‹ค์ž„ ์ „ํ™˜์ด๋‹ค."*
462
+
463
+ ๐ŸŒ [nton2.com](https://nton2.com) ยท ๐Ÿ“ฆ [npm](https://www.npmjs.com/package/n2-qln) ยท โœ‰๏ธ lagi0730@gmail.com
464
+
465
+ <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.
@@ -352,6 +369,39 @@ module.exports = {
352
369
  | **Default** (no Ollama) | Stage 1 + 2 | โญโญโญโญ Great | None |
353
370
  | **With Ollama** | Stage 1 + 2 + 3 | โญโญโญโญโญ Perfect | Ollama running |
354
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
+
355
405
  ---
356
406
 
357
407
  ## Project Structure
@@ -408,6 +458,28 @@ The N2 ecosystem has been in active development for over 4 months. Every project
408
458
 
409
459
  This is a solo developer project. Building, testing, and documenting everything alone takes time. Thank you for your patience and interest ๐Ÿ™
410
460
 
461
+ ## Contributing
462
+
463
+ Contributions are welcome! Here's how to get started:
464
+
465
+ 1. Fork the repo
466
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
467
+ 3. Commit your changes (`git commit -m 'feat: add amazing feature'`)
468
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
469
+ 5. Open a Pull Request
470
+
471
+ ## Star History
472
+
473
+ If you find QLN helpful, please consider giving us a star! โญ
474
+
411
475
  ## License
412
476
 
413
477
  Apache-2.0
478
+
479
+ ---
480
+
481
+ > *"1,000 tools in 200 tokens. That's not optimization โ€” that's a paradigm shift."*
482
+
483
+ ๐ŸŒ [nton2.com](https://nton2.com) ยท ๐Ÿ“ฆ [npm](https://www.npmjs.com/package/n2-qln) ยท โœ‰๏ธ lagi0730@gmail.com
484
+
485
+ <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/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,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.2.0",
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"
@@ -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": {
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) => {