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.
@@ -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.1",
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": ["knowledgebase", "vector-search", "faiss", "fuzzy-search"],
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
- if (mode === 'vector') {
214
- const res = await fetch('/api/search/vector', {
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
- const data = await res.json()
220
- results = data.results
221
- } else {
222
- const res = await fetch(`/api/search/fuzzy?q=${encodeURIComponent(query)}`)
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(results) {
234
- if (!results || results.length === 0) {
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
- resultsDiv.innerHTML = results.map(r => `
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
- `).join('')
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
  }