context-vault 2.0.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/LICENSE +21 -0
- package/README.md +383 -0
- package/bin/cli.js +588 -0
- package/package.json +30 -0
- package/smithery.yaml +10 -0
- package/src/capture/README.md +23 -0
- package/src/capture/file-ops.js +75 -0
- package/src/capture/formatters.js +29 -0
- package/src/capture/index.js +91 -0
- package/src/core/README.md +20 -0
- package/src/core/categories.js +50 -0
- package/src/core/config.js +76 -0
- package/src/core/files.js +114 -0
- package/src/core/frontmatter.js +108 -0
- package/src/core/status.js +105 -0
- package/src/index/README.md +28 -0
- package/src/index/db.js +138 -0
- package/src/index/embed.js +56 -0
- package/src/index/index.js +258 -0
- package/src/retrieve/README.md +19 -0
- package/src/retrieve/index.js +173 -0
- package/src/server/README.md +44 -0
- package/src/server/helpers.js +29 -0
- package/src/server/index.js +82 -0
- package/src/server/tools.js +211 -0
- package/ui/Context.applescript +36 -0
- package/ui/index.html +1377 -0
- package/ui/serve.js +473 -0
package/ui/serve.js
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* serve.js — Lightweight HTTP server for context-mcp UI
|
|
4
|
+
*
|
|
5
|
+
* Structure-agnostic: discovers tables, columns, and directories at runtime.
|
|
6
|
+
* Works with any SQLite database and any vault directory layout.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node serve.js [--port 3141] [--db-path /path/to/vault.db] [--vault-dir /path/to/vault]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createServer } from "node:http";
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync, statSync, readdirSync } from "node:fs";
|
|
13
|
+
import { resolve, dirname, join, basename } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { resolveConfig } from "../src/config.js";
|
|
16
|
+
import { initDatabase } from "../src/db.js";
|
|
17
|
+
import { embed } from "../src/embed.js";
|
|
18
|
+
import { hybridSearch } from "../src/search.js";
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
// ─── Config & DB (mutable — can be reconnected) ────────────────────────────
|
|
23
|
+
|
|
24
|
+
let config = resolveConfig();
|
|
25
|
+
let db = initDatabase(config.dbPath);
|
|
26
|
+
|
|
27
|
+
function reconnectDb(dbPath) {
|
|
28
|
+
try { db.close(); } catch {}
|
|
29
|
+
db = initDatabase(dbPath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Load UI HTML once ──────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const htmlContent = readFileSync(resolve(__dirname, "index.html"), "utf-8");
|
|
35
|
+
|
|
36
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function jsonResponse(res, data, status = 200) {
|
|
39
|
+
const body = JSON.stringify(data);
|
|
40
|
+
res.writeHead(status, {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"Access-Control-Allow-Origin": "*",
|
|
43
|
+
"Access-Control-Allow-Methods": "GET, PUT, OPTIONS",
|
|
44
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
45
|
+
});
|
|
46
|
+
res.end(body);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function htmlResponse(res) {
|
|
50
|
+
res.writeHead(200, {
|
|
51
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
52
|
+
"Access-Control-Allow-Origin": "*",
|
|
53
|
+
});
|
|
54
|
+
res.end(htmlContent);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function errorResponse(res, message, status = 500) {
|
|
58
|
+
jsonResponse(res, { error: message }, status);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function safeTableName(name) {
|
|
62
|
+
// Only allow alphanumeric, underscore, hyphen
|
|
63
|
+
return /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(name) ? name : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Discovery ──────────────────────────────────────────────────────────────
|
|
67
|
+
// Returns everything the UI needs to build its sidebar and choose views.
|
|
68
|
+
|
|
69
|
+
function handleDiscover(res) {
|
|
70
|
+
// 1. Discover all tables with schemas and row counts
|
|
71
|
+
const rawTables = db.prepare(
|
|
72
|
+
"SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
73
|
+
).all();
|
|
74
|
+
|
|
75
|
+
const tables = rawTables.map((t) => {
|
|
76
|
+
try {
|
|
77
|
+
const count = db.prepare(`SELECT COUNT(*) as count FROM "${t.name}"`).get().count;
|
|
78
|
+
const cols = db.prepare(`PRAGMA table_info("${t.name}")`).all();
|
|
79
|
+
return {
|
|
80
|
+
name: t.name,
|
|
81
|
+
type: t.type,
|
|
82
|
+
count,
|
|
83
|
+
columns: cols.map((c) => ({ name: c.name, type: c.type, pk: !!c.pk })),
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
return { name: t.name, type: t.type, count: 0, columns: [] };
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 2. For tables with a "kind" or "type" or "category" column, get distinct values
|
|
91
|
+
const tableGroups = {};
|
|
92
|
+
for (const t of tables) {
|
|
93
|
+
const groupCol = t.columns.find((c) =>
|
|
94
|
+
["kind", "type", "category", "status", "group"].includes(c.name.toLowerCase())
|
|
95
|
+
);
|
|
96
|
+
if (groupCol && t.count > 0) {
|
|
97
|
+
try {
|
|
98
|
+
const groups = db.prepare(
|
|
99
|
+
`SELECT "${groupCol.name}" as grp, COUNT(*) as count FROM "${t.name}" WHERE "${groupCol.name}" IS NOT NULL GROUP BY "${groupCol.name}" ORDER BY count DESC`
|
|
100
|
+
).all();
|
|
101
|
+
tableGroups[t.name] = { column: groupCol.name, groups: groups.map((g) => ({ value: g.grp, count: g.count })) };
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Knowledge directory structure (top-level only)
|
|
107
|
+
let directories = [];
|
|
108
|
+
let vaultDirName = basename(config.vaultDir);
|
|
109
|
+
if (existsSync(config.vaultDir)) {
|
|
110
|
+
try {
|
|
111
|
+
const entries = readdirSync(config.vaultDir, { withFileTypes: true });
|
|
112
|
+
directories = entries
|
|
113
|
+
.filter((e) => !e.name.startsWith("."))
|
|
114
|
+
.map((e) => {
|
|
115
|
+
const fullPath = join(config.vaultDir, e.name);
|
|
116
|
+
const item = { name: e.name, type: e.isDirectory() ? "directory" : "file" };
|
|
117
|
+
if (e.isDirectory()) {
|
|
118
|
+
try {
|
|
119
|
+
// Count files recursively
|
|
120
|
+
const count = countFiles(fullPath);
|
|
121
|
+
item.fileCount = count;
|
|
122
|
+
} catch { item.fileCount = 0; }
|
|
123
|
+
}
|
|
124
|
+
if (e.isFile()) {
|
|
125
|
+
try { item.size = statSync(fullPath).size; } catch {}
|
|
126
|
+
}
|
|
127
|
+
return item;
|
|
128
|
+
})
|
|
129
|
+
.sort((a, b) => {
|
|
130
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
131
|
+
return a.name.localeCompare(b.name);
|
|
132
|
+
});
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 4. DB size
|
|
137
|
+
let dbSize = "unknown";
|
|
138
|
+
try {
|
|
139
|
+
const bytes = statSync(config.dbPath).size;
|
|
140
|
+
dbSize = bytes < 1024 * 1024
|
|
141
|
+
? `${(bytes / 1024).toFixed(1)}KB`
|
|
142
|
+
: `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
143
|
+
} catch {}
|
|
144
|
+
|
|
145
|
+
jsonResponse(res, {
|
|
146
|
+
tables,
|
|
147
|
+
tableGroups,
|
|
148
|
+
directories,
|
|
149
|
+
vaultDirName,
|
|
150
|
+
dbSize,
|
|
151
|
+
connection: {
|
|
152
|
+
dbPath: config.dbPath,
|
|
153
|
+
vaultDir: config.vaultDir,
|
|
154
|
+
dataDir: config.dataDir,
|
|
155
|
+
devDir: config.devDir,
|
|
156
|
+
vaultDirExists: existsSync(config.vaultDir),
|
|
157
|
+
dbExists: existsSync(config.dbPath),
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function countFiles(dir) {
|
|
163
|
+
let count = 0;
|
|
164
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
165
|
+
if (entry.name.startsWith(".")) continue;
|
|
166
|
+
if (entry.isFile()) count++;
|
|
167
|
+
else if (entry.isDirectory()) count += countFiles(join(dir, entry.name));
|
|
168
|
+
}
|
|
169
|
+
return count;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── Generic Table Data ─────────────────────────────────────────────────────
|
|
173
|
+
// Query any table with pagination, search, and optional group filtering.
|
|
174
|
+
|
|
175
|
+
function handleTableData(res, url) {
|
|
176
|
+
const params = url.searchParams;
|
|
177
|
+
const table = params.get("table");
|
|
178
|
+
if (!table || !safeTableName(table)) return errorResponse(res, "Invalid table name", 400);
|
|
179
|
+
|
|
180
|
+
// Verify table exists
|
|
181
|
+
const exists = db.prepare("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name = ?").get(table);
|
|
182
|
+
if (!exists) return errorResponse(res, "Table not found", 404);
|
|
183
|
+
|
|
184
|
+
const limit = Math.min(parseInt(params.get("limit") || "50", 10), 200);
|
|
185
|
+
const offset = parseInt(params.get("offset") || "0", 10);
|
|
186
|
+
const q = params.get("q") || "";
|
|
187
|
+
const groupCol = params.get("groupCol") || "";
|
|
188
|
+
const groupVal = params.get("groupVal") || "";
|
|
189
|
+
const sortCol = params.get("sort") || "";
|
|
190
|
+
const sortDir = params.get("dir") === "asc" ? "ASC" : "DESC";
|
|
191
|
+
|
|
192
|
+
// Get column info for this table
|
|
193
|
+
const colInfo = db.prepare(`PRAGMA table_info("${table}")`).all();
|
|
194
|
+
const colNames = colInfo.map((c) => c.name);
|
|
195
|
+
|
|
196
|
+
// Build WHERE clause
|
|
197
|
+
const conditions = [];
|
|
198
|
+
const binds = [];
|
|
199
|
+
|
|
200
|
+
// Group filter
|
|
201
|
+
if (groupCol && groupVal && colNames.includes(groupCol)) {
|
|
202
|
+
conditions.push(`"${groupCol}" = ?`);
|
|
203
|
+
binds.push(groupVal);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Search: LIKE across all text-looking columns
|
|
207
|
+
if (q) {
|
|
208
|
+
const textCols = colInfo.filter((c) =>
|
|
209
|
+
!c.type || c.type.toUpperCase().includes("TEXT") || c.type.toUpperCase().includes("VARCHAR") || c.type === ""
|
|
210
|
+
);
|
|
211
|
+
if (textCols.length > 0) {
|
|
212
|
+
const likeClauses = textCols.map((c) => `"${c.name}" LIKE ?`);
|
|
213
|
+
conditions.push(`(${likeClauses.join(" OR ")})`);
|
|
214
|
+
for (const _ of textCols) binds.push(`%${q}%`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const where = conditions.length ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
219
|
+
|
|
220
|
+
// Sort
|
|
221
|
+
let orderBy = "";
|
|
222
|
+
if (sortCol && colNames.includes(sortCol)) {
|
|
223
|
+
orderBy = ` ORDER BY "${sortCol}" ${sortDir}`;
|
|
224
|
+
} else {
|
|
225
|
+
// Auto-detect: prefer created_at, date, updated_at, id, or rowid
|
|
226
|
+
const dateCols = ["created_at", "updated_at", "date", "timestamp", "created", "modified"];
|
|
227
|
+
const autoSort = dateCols.find((c) => colNames.includes(c));
|
|
228
|
+
if (autoSort) {
|
|
229
|
+
orderBy = ` ORDER BY "${autoSort}" DESC`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const rows = db.prepare(`SELECT * FROM "${table}"${where}${orderBy} LIMIT ? OFFSET ?`).all(...binds, limit, offset);
|
|
235
|
+
const total = db.prepare(`SELECT COUNT(*) as count FROM "${table}"${where}`).get(...binds).count;
|
|
236
|
+
|
|
237
|
+
jsonResponse(res, {
|
|
238
|
+
table,
|
|
239
|
+
columns: colInfo.map((c) => ({ name: c.name, type: c.type, pk: !!c.pk })),
|
|
240
|
+
rows,
|
|
241
|
+
total,
|
|
242
|
+
});
|
|
243
|
+
} catch (e) {
|
|
244
|
+
errorResponse(res, "Query failed: " + e.message);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─── Knowledge Directory Browser ────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
function handleBrowse(res, url) {
|
|
251
|
+
const subpath = url.searchParams.get("path") || "";
|
|
252
|
+
const baseDir = config.vaultDir;
|
|
253
|
+
|
|
254
|
+
if (!existsSync(baseDir)) {
|
|
255
|
+
return jsonResponse(res, { exists: false, path: baseDir, baseName: basename(baseDir), items: [] });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const targetDir = subpath ? resolve(baseDir, subpath) : baseDir;
|
|
259
|
+
|
|
260
|
+
// Safety: prevent traversal outside knowledge dir
|
|
261
|
+
if (!targetDir.startsWith(baseDir)) {
|
|
262
|
+
return errorResponse(res, "Invalid path", 400);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!existsSync(targetDir)) {
|
|
266
|
+
return jsonResponse(res, { exists: false, path: targetDir, baseName: basename(baseDir), items: [] });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const entries = readdirSync(targetDir, { withFileTypes: true });
|
|
271
|
+
const items = entries
|
|
272
|
+
.filter((e) => !e.name.startsWith("."))
|
|
273
|
+
.map((e) => {
|
|
274
|
+
const fullPath = join(targetDir, e.name);
|
|
275
|
+
const item = {
|
|
276
|
+
name: e.name,
|
|
277
|
+
type: e.isDirectory() ? "directory" : "file",
|
|
278
|
+
path: subpath ? join(subpath, e.name) : e.name,
|
|
279
|
+
};
|
|
280
|
+
if (e.isFile()) {
|
|
281
|
+
try {
|
|
282
|
+
const st = statSync(fullPath);
|
|
283
|
+
item.size = st.size;
|
|
284
|
+
item.modified = st.mtime.toISOString();
|
|
285
|
+
} catch {}
|
|
286
|
+
}
|
|
287
|
+
if (e.isDirectory()) {
|
|
288
|
+
try {
|
|
289
|
+
const children = readdirSync(fullPath, { withFileTypes: true });
|
|
290
|
+
item.childCount = children.filter((c) => !c.name.startsWith(".")).length;
|
|
291
|
+
} catch { item.childCount = 0; }
|
|
292
|
+
}
|
|
293
|
+
return item;
|
|
294
|
+
})
|
|
295
|
+
.sort((a, b) => {
|
|
296
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
297
|
+
return a.name.localeCompare(b.name);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
jsonResponse(res, { exists: true, path: targetDir, baseName: basename(baseDir), relativePath: subpath, items });
|
|
301
|
+
} catch (e) {
|
|
302
|
+
errorResponse(res, "Failed to browse: " + e.message);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function handleFileContent(res, url) {
|
|
307
|
+
const filePath = url.searchParams.get("path") || "";
|
|
308
|
+
const baseDir = config.vaultDir;
|
|
309
|
+
|
|
310
|
+
if (!filePath || !existsSync(baseDir)) {
|
|
311
|
+
return errorResponse(res, "Invalid path", 400);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const fullPath = resolve(baseDir, filePath);
|
|
315
|
+
|
|
316
|
+
// Safety: prevent traversal
|
|
317
|
+
if (!fullPath.startsWith(baseDir)) {
|
|
318
|
+
return errorResponse(res, "Invalid path", 400);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!existsSync(fullPath)) {
|
|
322
|
+
return errorResponse(res, "File not found", 404);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
327
|
+
const stats = statSync(fullPath);
|
|
328
|
+
jsonResponse(res, {
|
|
329
|
+
path: filePath,
|
|
330
|
+
fullPath,
|
|
331
|
+
baseName: basename(baseDir),
|
|
332
|
+
name: basename(fullPath),
|
|
333
|
+
content,
|
|
334
|
+
size: stats.size,
|
|
335
|
+
modified: stats.mtime.toISOString(),
|
|
336
|
+
});
|
|
337
|
+
} catch (e) {
|
|
338
|
+
errorResponse(res, "Failed to read file: " + e.message);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ─── Semantic Search ─────────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
async function handleSearch(res, url) {
|
|
345
|
+
const query = url.searchParams.get("q") || "";
|
|
346
|
+
const kindParam = url.searchParams.get("kind") || url.searchParams.get("type") || "";
|
|
347
|
+
const kindFilter = kindParam ? kindParam.replace(/s$/, "") : null;
|
|
348
|
+
|
|
349
|
+
if (!query) return jsonResponse(res, { results: [] });
|
|
350
|
+
|
|
351
|
+
const sorted = await hybridSearch(db, embed, query, kindFilter);
|
|
352
|
+
jsonResponse(res, { query, results: sorted });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ─── Config Handlers ─────────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
const configFilePath = resolve(__dirname, "..", "config.json");
|
|
358
|
+
|
|
359
|
+
function handleGetConfig(res) {
|
|
360
|
+
try {
|
|
361
|
+
const raw = existsSync(configFilePath) ? readFileSync(configFilePath, "utf-8") : "{}";
|
|
362
|
+
const cfg = JSON.parse(raw);
|
|
363
|
+
jsonResponse(res, {
|
|
364
|
+
config: cfg,
|
|
365
|
+
path: configFilePath,
|
|
366
|
+
active: {
|
|
367
|
+
dbPath: config.dbPath,
|
|
368
|
+
vaultDir: config.vaultDir,
|
|
369
|
+
dataDir: config.dataDir,
|
|
370
|
+
devDir: config.devDir,
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
} catch (e) {
|
|
374
|
+
errorResponse(res, "Failed to read config: " + e.message);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const MAX_BODY = 1024 * 1024; // 1MB
|
|
379
|
+
|
|
380
|
+
function handlePutConfig(req, res) {
|
|
381
|
+
let body = "";
|
|
382
|
+
let size = 0;
|
|
383
|
+
req.on("data", (chunk) => {
|
|
384
|
+
size += chunk.length;
|
|
385
|
+
if (size > MAX_BODY) {
|
|
386
|
+
req.destroy();
|
|
387
|
+
return errorResponse(res, "Request body too large", 413);
|
|
388
|
+
}
|
|
389
|
+
body += chunk;
|
|
390
|
+
});
|
|
391
|
+
req.on("end", () => {
|
|
392
|
+
try {
|
|
393
|
+
const newConfig = JSON.parse(body);
|
|
394
|
+
const allowed = ["vaultDir", "dataDir", "dbPath", "devDir"];
|
|
395
|
+
const sanitized = {};
|
|
396
|
+
for (const key of allowed) {
|
|
397
|
+
if (newConfig[key] !== undefined) sanitized[key] = newConfig[key];
|
|
398
|
+
}
|
|
399
|
+
writeFileSync(configFilePath, JSON.stringify(sanitized, null, 2) + "\n");
|
|
400
|
+
|
|
401
|
+
// Reconnect DB if dbPath changed
|
|
402
|
+
const newDbPath = resolve(sanitized.dbPath || config.dbPath);
|
|
403
|
+
if (newDbPath !== config.dbPath && existsSync(newDbPath)) {
|
|
404
|
+
reconnectDb(newDbPath);
|
|
405
|
+
config.dbPath = newDbPath;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (sanitized.vaultDir) config.vaultDir = resolve(sanitized.vaultDir);
|
|
409
|
+
if (sanitized.dataDir) config.dataDir = resolve(sanitized.dataDir);
|
|
410
|
+
if (sanitized.devDir) config.devDir = resolve(sanitized.devDir);
|
|
411
|
+
config.vaultDirExists = existsSync(config.vaultDir);
|
|
412
|
+
|
|
413
|
+
jsonResponse(res, { ok: true, config: sanitized });
|
|
414
|
+
} catch (e) {
|
|
415
|
+
errorResponse(res, "Invalid JSON: " + e.message, 400);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ─── Server ─────────────────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
const port = (() => {
|
|
423
|
+
const portArg = process.argv.indexOf("--port");
|
|
424
|
+
if (portArg !== -1 && process.argv[portArg + 1]) return parseInt(process.argv[portArg + 1], 10);
|
|
425
|
+
if (process.env.PORT) return parseInt(process.env.PORT, 10);
|
|
426
|
+
return 3141;
|
|
427
|
+
})();
|
|
428
|
+
|
|
429
|
+
const server = createServer((req, res) => {
|
|
430
|
+
if (req.method === "OPTIONS") {
|
|
431
|
+
res.writeHead(204, {
|
|
432
|
+
"Access-Control-Allow-Origin": "*",
|
|
433
|
+
"Access-Control-Allow-Methods": "GET, PUT, OPTIONS",
|
|
434
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
435
|
+
});
|
|
436
|
+
return res.end();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
440
|
+
const path = url.pathname;
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
if (path === "/") return htmlResponse(res);
|
|
444
|
+
if (path === "/api/discover") return handleDiscover(res);
|
|
445
|
+
if (path === "/api/table-data") return handleTableData(res, url);
|
|
446
|
+
if (path === "/api/search") return handleSearch(res, url);
|
|
447
|
+
if (path === "/api/browse") return handleBrowse(res, url);
|
|
448
|
+
if (path === "/api/file") return handleFileContent(res, url);
|
|
449
|
+
if (path === "/api/config" && req.method === "GET") return handleGetConfig(res);
|
|
450
|
+
if (path === "/api/config" && req.method === "PUT") return handlePutConfig(req, res);
|
|
451
|
+
errorResponse(res, "Not found", 404);
|
|
452
|
+
} catch (e) {
|
|
453
|
+
console.error(e);
|
|
454
|
+
errorResponse(res, e.message);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
server.listen(port, () => {
|
|
459
|
+
console.log(`Vault UI → http://localhost:${port}`);
|
|
460
|
+
console.log(`DB: ${config.dbPath}`);
|
|
461
|
+
console.log(`Vault: ${config.vaultDir}`);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// ─── Graceful Shutdown ───────────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
function shutdown() {
|
|
467
|
+
server.close(() => {
|
|
468
|
+
try { db.close(); } catch {}
|
|
469
|
+
process.exit(0);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
process.on("SIGINT", shutdown);
|
|
473
|
+
process.on("SIGTERM", shutdown);
|