underrow 2026.4.1 → 2026.4.2
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/.claude/settings.local.json +2 -1
- package/package.json +7 -2
- package/public/index.html +30 -34
- package/src/server.js +167 -0
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
"allow": [
|
|
4
4
|
"Bash(node:*)",
|
|
5
5
|
"Bash(npm link:*)",
|
|
6
|
-
"Bash(knowme --help)"
|
|
6
|
+
"Bash(knowme --help)",
|
|
7
|
+
"Bash(timeout 3 bash -c 'node index.js --port 3739 &>/dev/null & PID=$!; sleep 2; curl -s http://localhost:3737/openapi.json 2>/dev/null || curl -s http://localhost:3739/openapi.json; kill $PID 2>/dev/null')"
|
|
7
8
|
]
|
|
8
9
|
}
|
|
9
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "underrow",
|
|
3
|
-
"version": "2026.4.
|
|
3
|
+
"version": "2026.4.2",
|
|
4
4
|
"description": "KnowledgeBase driver - file watcher with vector and fuzzy search",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
"start": "node index.js",
|
|
12
12
|
"dev": "node --watch index.js"
|
|
13
13
|
},
|
|
14
|
-
"keywords": [
|
|
14
|
+
"keywords": [
|
|
15
|
+
"knowledgebase",
|
|
16
|
+
"vector-search",
|
|
17
|
+
"faiss",
|
|
18
|
+
"fuzzy-search"
|
|
19
|
+
],
|
|
15
20
|
"license": "MIT",
|
|
16
21
|
"dependencies": {
|
|
17
22
|
"chokidar": "^3.6.0",
|
package/public/index.html
CHANGED
|
@@ -64,16 +64,6 @@
|
|
|
64
64
|
|
|
65
65
|
.search-box input:focus { border-color: #58a6ff; }
|
|
66
66
|
|
|
67
|
-
.search-box select {
|
|
68
|
-
padding: 0.75rem;
|
|
69
|
-
background: #161b22;
|
|
70
|
-
border: 1px solid #30363d;
|
|
71
|
-
border-radius: 6px;
|
|
72
|
-
color: #e1e4e8;
|
|
73
|
-
font-size: 0.9rem;
|
|
74
|
-
cursor: pointer;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
67
|
.search-box button {
|
|
78
68
|
padding: 0.75rem 1.5rem;
|
|
79
69
|
background: #238636;
|
|
@@ -169,10 +159,6 @@
|
|
|
169
159
|
<div class="container">
|
|
170
160
|
<div class="search-box">
|
|
171
161
|
<input type="text" id="query" placeholder="Search your knowledge base..." autofocus />
|
|
172
|
-
<select id="mode">
|
|
173
|
-
<option value="vector">Vector</option>
|
|
174
|
-
<option value="fuzzy">Fuzzy</option>
|
|
175
|
-
</select>
|
|
176
162
|
<button onclick="doSearch()">Search</button>
|
|
177
163
|
</div>
|
|
178
164
|
|
|
@@ -185,7 +171,6 @@
|
|
|
185
171
|
|
|
186
172
|
<script>
|
|
187
173
|
const queryInput = document.getElementById('query')
|
|
188
|
-
const modeSelect = document.getElementById('mode')
|
|
189
174
|
const resultsDiv = document.getElementById('results')
|
|
190
175
|
|
|
191
176
|
queryInput.addEventListener('keydown', e => {
|
|
@@ -206,37 +191,48 @@
|
|
|
206
191
|
const query = queryInput.value.trim()
|
|
207
192
|
if (!query) return
|
|
208
193
|
|
|
209
|
-
const mode = modeSelect.value
|
|
210
|
-
let results
|
|
211
|
-
|
|
212
194
|
try {
|
|
213
|
-
|
|
214
|
-
|
|
195
|
+
const [vectorRes, fuzzyRes] = await Promise.all([
|
|
196
|
+
fetch('/api/search/vector', {
|
|
215
197
|
method: 'POST',
|
|
216
198
|
headers: { 'Content-Type': 'application/json' },
|
|
217
199
|
body: JSON.stringify({ query, k: 20 }),
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const data = await res.json()
|
|
224
|
-
results = data.results
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
renderResults(results)
|
|
200
|
+
}).then(r => r.json()),
|
|
201
|
+
fetch(`/api/search/fuzzy?q=${encodeURIComponent(query)}`).then(r => r.json()),
|
|
202
|
+
])
|
|
203
|
+
|
|
204
|
+
renderResults(vectorRes.results, fuzzyRes.results)
|
|
228
205
|
} catch (err) {
|
|
229
206
|
resultsDiv.innerHTML = `<div class="empty-state">Error: ${err.message}</div>`
|
|
230
207
|
}
|
|
231
208
|
}
|
|
232
209
|
|
|
233
|
-
function renderResults(
|
|
234
|
-
|
|
210
|
+
function renderResults(vectorResults, fuzzyResults) {
|
|
211
|
+
const hasVector = vectorResults && vectorResults.length > 0
|
|
212
|
+
const hasFuzzy = fuzzyResults && fuzzyResults.length > 0
|
|
213
|
+
|
|
214
|
+
if (!hasVector && !hasFuzzy) {
|
|
235
215
|
resultsDiv.innerHTML = '<div class="empty-state">No results found.</div>'
|
|
236
216
|
return
|
|
237
217
|
}
|
|
238
218
|
|
|
239
|
-
|
|
219
|
+
let html = ''
|
|
220
|
+
|
|
221
|
+
if (hasVector) {
|
|
222
|
+
html += '<h3 style="color:#58a6ff;margin-bottom:0.5rem">Vector Results</h3>'
|
|
223
|
+
html += vectorResults.map(renderCard).join('')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (hasFuzzy) {
|
|
227
|
+
html += `<h3 style="color:#58a6ff;margin:${hasVector ? '1.5rem' : '0'} 0 0.5rem">Fuzzy Results</h3>`
|
|
228
|
+
html += fuzzyResults.map(renderCard).join('')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
resultsDiv.innerHTML = html
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function renderCard(r) {
|
|
235
|
+
return `
|
|
240
236
|
<div class="result-card">
|
|
241
237
|
<div class="result-header">
|
|
242
238
|
<span class="result-path">${escapeHtml(r.filePath)}</span>
|
|
@@ -253,7 +249,7 @@
|
|
|
253
249
|
</div>
|
|
254
250
|
<div class="result-text">${escapeHtml(r.text)}</div>
|
|
255
251
|
</div>
|
|
256
|
-
`
|
|
252
|
+
`
|
|
257
253
|
}
|
|
258
254
|
|
|
259
255
|
function escapeHtml(str) {
|
package/src/server.js
CHANGED
|
@@ -11,6 +11,42 @@ export function createServer(store, embedder) {
|
|
|
11
11
|
app.use(express.json())
|
|
12
12
|
app.use(express.static(resolve(__dirname, '..', 'public')))
|
|
13
13
|
|
|
14
|
+
// Help
|
|
15
|
+
app.get('/help', (req, res) => {
|
|
16
|
+
res.type('text/plain').send(`Underrow - KnowledgeBase Driver
|
|
17
|
+
|
|
18
|
+
Underrow watches a directory for file changes, chunks the content,
|
|
19
|
+
computes vector embeddings and information density, and makes
|
|
20
|
+
everything searchable via a web dashboard and REST API.
|
|
21
|
+
|
|
22
|
+
Routes
|
|
23
|
+
------
|
|
24
|
+
|
|
25
|
+
GET / Web dashboard with search UI and live stats
|
|
26
|
+
GET /help This help text
|
|
27
|
+
GET /openapi.json OpenAPI 3.0 spec for the API
|
|
28
|
+
|
|
29
|
+
GET /api/stats
|
|
30
|
+
Returns indexing statistics: file count, chunk count, average density.
|
|
31
|
+
|
|
32
|
+
GET /api/documents
|
|
33
|
+
Returns metadata for every indexed chunk.
|
|
34
|
+
|
|
35
|
+
POST /api/search/vector
|
|
36
|
+
Body: { "query": "...", "k": 10 }
|
|
37
|
+
Embeds the query and returns the top-k chunks by cosine similarity.
|
|
38
|
+
|
|
39
|
+
GET /api/search/fuzzy?q=...&limit=20&threshold=0.4
|
|
40
|
+
Fuzzy text search across all indexed chunks.
|
|
41
|
+
|
|
42
|
+
Environment / CLI
|
|
43
|
+
-----------------
|
|
44
|
+
underrow [dir] Directory to watch (default: cwd)
|
|
45
|
+
--port, -p <number> Server port (default: 3737, env: KB_PORT)
|
|
46
|
+
--data, -d <path> Data storage dir (default: ./data, env: KB_DATA_DIR)
|
|
47
|
+
`)
|
|
48
|
+
})
|
|
49
|
+
|
|
14
50
|
// Vector search
|
|
15
51
|
app.post('/api/search/vector', async (req, res) => {
|
|
16
52
|
const { query, k = 10 } = req.body
|
|
@@ -53,5 +89,136 @@ export function createServer(store, embedder) {
|
|
|
53
89
|
res.json(store.getAllMetadata())
|
|
54
90
|
})
|
|
55
91
|
|
|
92
|
+
// OpenAPI spec
|
|
93
|
+
app.get('/openapi.json', (req, res) => {
|
|
94
|
+
res.json({
|
|
95
|
+
openapi: '3.0.3',
|
|
96
|
+
info: {
|
|
97
|
+
title: 'Underrow',
|
|
98
|
+
version: '2026.4.1',
|
|
99
|
+
description: 'KnowledgeBase driver - file watcher with vector and fuzzy search',
|
|
100
|
+
},
|
|
101
|
+
paths: {
|
|
102
|
+
'/api/search/vector': {
|
|
103
|
+
post: {
|
|
104
|
+
summary: 'Vector similarity search',
|
|
105
|
+
requestBody: {
|
|
106
|
+
required: true,
|
|
107
|
+
content: {
|
|
108
|
+
'application/json': {
|
|
109
|
+
schema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
required: ['query'],
|
|
112
|
+
properties: {
|
|
113
|
+
query: { type: 'string', description: 'Search text to embed and match against indexed chunks' },
|
|
114
|
+
k: { type: 'integer', default: 10, description: 'Number of results to return' },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
responses: {
|
|
121
|
+
200: {
|
|
122
|
+
description: 'Search results ranked by cosine similarity',
|
|
123
|
+
content: { 'application/json': { schema: { $ref: '#/components/schemas/SearchResults' } } },
|
|
124
|
+
},
|
|
125
|
+
400: { description: 'Missing query' },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
'/api/search/fuzzy': {
|
|
130
|
+
get: {
|
|
131
|
+
summary: 'Fuzzy text search',
|
|
132
|
+
parameters: [
|
|
133
|
+
{ name: 'q', in: 'query', required: true, schema: { type: 'string' }, description: 'Search query' },
|
|
134
|
+
{ name: 'limit', in: 'query', schema: { type: 'integer', default: 20 }, description: 'Max results' },
|
|
135
|
+
{ name: 'threshold', in: 'query', schema: { type: 'number', default: 0.4 }, description: 'Fuse.js match threshold (0 = exact, 1 = anything)' },
|
|
136
|
+
],
|
|
137
|
+
responses: {
|
|
138
|
+
200: {
|
|
139
|
+
description: 'Fuzzy-matched results',
|
|
140
|
+
content: { 'application/json': { schema: { $ref: '#/components/schemas/SearchResults' } } },
|
|
141
|
+
},
|
|
142
|
+
400: { description: 'Missing q parameter' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
'/api/stats': {
|
|
147
|
+
get: {
|
|
148
|
+
summary: 'Index statistics',
|
|
149
|
+
responses: {
|
|
150
|
+
200: {
|
|
151
|
+
description: 'Current indexing stats',
|
|
152
|
+
content: {
|
|
153
|
+
'application/json': {
|
|
154
|
+
schema: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
files: { type: 'integer' },
|
|
158
|
+
chunks: { type: 'integer' },
|
|
159
|
+
avgDensity: { type: 'number' },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
'/api/documents': {
|
|
169
|
+
get: {
|
|
170
|
+
summary: 'All indexed document metadata',
|
|
171
|
+
responses: {
|
|
172
|
+
200: {
|
|
173
|
+
description: 'Array of chunk metadata',
|
|
174
|
+
content: {
|
|
175
|
+
'application/json': {
|
|
176
|
+
schema: {
|
|
177
|
+
type: 'array',
|
|
178
|
+
items: { $ref: '#/components/schemas/ChunkMeta' },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
components: {
|
|
188
|
+
schemas: {
|
|
189
|
+
ChunkMeta: {
|
|
190
|
+
type: 'object',
|
|
191
|
+
properties: {
|
|
192
|
+
id: { type: 'integer' },
|
|
193
|
+
filePath: { type: 'string' },
|
|
194
|
+
chunkIndex: { type: 'integer' },
|
|
195
|
+
text: { type: 'string' },
|
|
196
|
+
density: { type: 'number' },
|
|
197
|
+
mtime: { type: 'number' },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
SearchResults: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
results: {
|
|
204
|
+
type: 'array',
|
|
205
|
+
items: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {
|
|
208
|
+
score: { type: 'number' },
|
|
209
|
+
filePath: { type: 'string' },
|
|
210
|
+
chunkIndex: { type: 'integer' },
|
|
211
|
+
text: { type: 'string' },
|
|
212
|
+
density: { type: 'number' },
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
56
223
|
return app
|
|
57
224
|
}
|