context-vault 2.8.15 → 2.8.17
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/app-dist/apple-touch-icon.png +0 -0
- package/app-dist/assets/index-CGFd_zQ8.js +371 -0
- package/app-dist/assets/index-b-qgfkBK.css +1 -0
- package/app-dist/favicon-16x16.png +0 -0
- package/app-dist/favicon-32x32.png +0 -0
- package/app-dist/favicon.ico +0 -0
- package/app-dist/index.html +18 -0
- package/bin/cli.js +42 -2
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/index/embed.js +2 -2
- package/node_modules/@context-vault/core/src/index/index.js +16 -13
- package/node_modules/@context-vault/core/src/server/helpers.js +1 -1
- package/node_modules/@context-vault/core/src/server/tools/get-context.js +4 -1
- package/package.json +3 -2
- package/scripts/prepack.js +17 -1
- package/src/local-server.js +296 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { join, dirname, extname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
const MIME_TYPES = {
|
|
9
|
+
".html": "text/html",
|
|
10
|
+
".js": "application/javascript",
|
|
11
|
+
".css": "text/css",
|
|
12
|
+
".svg": "image/svg+xml",
|
|
13
|
+
".ico": "image/x-icon",
|
|
14
|
+
".png": "image/png",
|
|
15
|
+
".woff": "font/woff",
|
|
16
|
+
".woff2": "font/woff2",
|
|
17
|
+
".json": "application/json",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const LOCAL_USER = {
|
|
21
|
+
id: "local",
|
|
22
|
+
email: "local@localhost",
|
|
23
|
+
tier: "local",
|
|
24
|
+
linkedAt: null,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function json(res, status, data) {
|
|
28
|
+
const body = JSON.stringify(data);
|
|
29
|
+
res.writeHead(status, {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"Content-Length": Buffer.byteLength(body),
|
|
32
|
+
});
|
|
33
|
+
res.end(body);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readBody(req) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const chunks = [];
|
|
39
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
40
|
+
req.on("end", () => {
|
|
41
|
+
try {
|
|
42
|
+
const raw = Buffer.concat(chunks).toString();
|
|
43
|
+
resolve(raw ? JSON.parse(raw) : {});
|
|
44
|
+
} catch {
|
|
45
|
+
resolve({});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
req.on("error", reject);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tryParse(str, fallback) {
|
|
53
|
+
if (!str) return fallback;
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(str);
|
|
56
|
+
} catch {
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mapRow(row) {
|
|
62
|
+
return {
|
|
63
|
+
id: row.id,
|
|
64
|
+
kind: row.kind,
|
|
65
|
+
category: row.category,
|
|
66
|
+
title: row.title || null,
|
|
67
|
+
body: row.body || null,
|
|
68
|
+
tags: tryParse(row.tags, []),
|
|
69
|
+
meta: tryParse(row.meta, {}),
|
|
70
|
+
source: row.source || null,
|
|
71
|
+
identity_key: row.identity_key || null,
|
|
72
|
+
expires_at: row.expires_at || null,
|
|
73
|
+
created_at: row.created_at,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mapCaptureResult(entry) {
|
|
78
|
+
return {
|
|
79
|
+
id: entry.id,
|
|
80
|
+
kind: entry.kind,
|
|
81
|
+
category: entry.category,
|
|
82
|
+
title: entry.title || null,
|
|
83
|
+
body: entry.body || null,
|
|
84
|
+
tags: entry.tags || [],
|
|
85
|
+
meta: entry.meta || {},
|
|
86
|
+
source: entry.source || null,
|
|
87
|
+
identity_key: entry.identity_key || null,
|
|
88
|
+
expires_at: entry.expires_at || null,
|
|
89
|
+
created_at: entry.createdAt,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function serveStatic(req, res, pathname, appDistDir) {
|
|
94
|
+
if (!existsSync(appDistDir)) {
|
|
95
|
+
const msg = "Web UI not bundled. Run `node scripts/prepack.js` first.";
|
|
96
|
+
res.writeHead(503, { "Content-Type": "text/plain" });
|
|
97
|
+
res.end(msg);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let filePath = join(appDistDir, pathname === "/" ? "index.html" : pathname);
|
|
102
|
+
|
|
103
|
+
// Security: prevent path traversal
|
|
104
|
+
if (!filePath.startsWith(appDistDir)) {
|
|
105
|
+
res.writeHead(403);
|
|
106
|
+
res.end("Forbidden");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!existsSync(filePath)) {
|
|
111
|
+
const ext = extname(pathname);
|
|
112
|
+
if (ext) {
|
|
113
|
+
res.writeHead(404);
|
|
114
|
+
res.end("Not found");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// SPA fallback for React Router routes
|
|
118
|
+
filePath = join(appDistDir, "index.html");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!existsSync(filePath)) {
|
|
122
|
+
res.writeHead(404);
|
|
123
|
+
res.end("Not found");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const ext = extname(filePath);
|
|
128
|
+
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
129
|
+
const content = readFileSync(filePath);
|
|
130
|
+
res.writeHead(200, {
|
|
131
|
+
"Content-Type": mime,
|
|
132
|
+
"Content-Length": content.length,
|
|
133
|
+
});
|
|
134
|
+
res.end(content);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function handleApi(
|
|
138
|
+
req,
|
|
139
|
+
res,
|
|
140
|
+
url,
|
|
141
|
+
ctx,
|
|
142
|
+
{ captureAndIndex, gatherVaultStatus },
|
|
143
|
+
) {
|
|
144
|
+
const { method } = req;
|
|
145
|
+
const pathname = url.pathname;
|
|
146
|
+
|
|
147
|
+
if (pathname === "/api/me" && method === "GET") {
|
|
148
|
+
return json(res, 200, LOCAL_USER);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (pathname === "/api/register" && method === "POST") {
|
|
152
|
+
return json(res, 200, LOCAL_USER);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (pathname === "/api/vault/status" && method === "GET") {
|
|
156
|
+
const status = gatherVaultStatus(ctx);
|
|
157
|
+
return json(res, 200, status);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (pathname === "/api/vault/entries" && method === "GET") {
|
|
161
|
+
const limit = Math.min(
|
|
162
|
+
parseInt(url.searchParams.get("limit") || "50", 10),
|
|
163
|
+
200,
|
|
164
|
+
);
|
|
165
|
+
const offset = parseInt(url.searchParams.get("offset") || "0", 10);
|
|
166
|
+
const kind = url.searchParams.get("kind") || null;
|
|
167
|
+
const category = url.searchParams.get("category") || null;
|
|
168
|
+
const q = url.searchParams.get("q") || null;
|
|
169
|
+
|
|
170
|
+
let query = `SELECT * FROM vault WHERE (expires_at IS NULL OR expires_at > datetime('now'))`;
|
|
171
|
+
const params = [];
|
|
172
|
+
|
|
173
|
+
if (kind) {
|
|
174
|
+
query += ` AND kind = ?`;
|
|
175
|
+
params.push(kind);
|
|
176
|
+
}
|
|
177
|
+
if (category) {
|
|
178
|
+
query += ` AND category = ?`;
|
|
179
|
+
params.push(category);
|
|
180
|
+
}
|
|
181
|
+
if (q) {
|
|
182
|
+
query += ` AND (title LIKE ? OR body LIKE ?)`;
|
|
183
|
+
params.push(`%${q}%`, `%${q}%`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const countQuery = query.replace("SELECT *", "SELECT COUNT(*) as c");
|
|
187
|
+
const total = ctx.db.prepare(countQuery).get(...params).c;
|
|
188
|
+
|
|
189
|
+
query += ` ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
|
190
|
+
params.push(limit, offset);
|
|
191
|
+
|
|
192
|
+
const rows = ctx.db.prepare(query).all(...params);
|
|
193
|
+
return json(res, 200, { entries: rows.map(mapRow), total, limit, offset });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (pathname === "/api/vault/entries" && method === "POST") {
|
|
197
|
+
const data = await readBody(req);
|
|
198
|
+
const entry = await captureAndIndex(ctx, data);
|
|
199
|
+
return json(res, 201, mapCaptureResult(entry));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const entryMatch = pathname.match(/^\/api\/vault\/entries\/([^/]+)$/);
|
|
203
|
+
if (entryMatch) {
|
|
204
|
+
const id = entryMatch[1];
|
|
205
|
+
|
|
206
|
+
if (method === "GET") {
|
|
207
|
+
const row = ctx.stmts.getEntryById.get(id);
|
|
208
|
+
if (!row) return json(res, 404, { error: "Entry not found" });
|
|
209
|
+
return json(res, 200, mapRow(row));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (method === "DELETE") {
|
|
213
|
+
const entry = ctx.stmts.getEntryById.get(id);
|
|
214
|
+
if (!entry) return json(res, 404, { error: "Entry not found" });
|
|
215
|
+
|
|
216
|
+
if (entry.file_path) {
|
|
217
|
+
try {
|
|
218
|
+
unlinkSync(entry.file_path);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
if (e.code !== "ENOENT") throw e;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const rowidResult = ctx.stmts.getRowid.get(id);
|
|
225
|
+
if (rowidResult?.rowid) {
|
|
226
|
+
try {
|
|
227
|
+
ctx.deleteVec(Number(rowidResult.rowid));
|
|
228
|
+
} catch {}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
ctx.stmts.deleteEntry.run(id);
|
|
232
|
+
return json(res, 200, { deleted: true });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return json(res, 404, { error: "Not available in local mode" });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function startLocalServer(port = 4422) {
|
|
240
|
+
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
241
|
+
const { initDatabase, prepareStatements, insertVec, deleteVec } =
|
|
242
|
+
await import("@context-vault/core/index/db");
|
|
243
|
+
const { embed } = await import("@context-vault/core/index/embed");
|
|
244
|
+
const { captureAndIndex } = await import("@context-vault/core/capture");
|
|
245
|
+
const { gatherVaultStatus } = await import("@context-vault/core/core/status");
|
|
246
|
+
|
|
247
|
+
const config = resolveConfig();
|
|
248
|
+
const db = await initDatabase(config.dbPath);
|
|
249
|
+
const stmts = prepareStatements(db);
|
|
250
|
+
|
|
251
|
+
const ctx = {
|
|
252
|
+
db,
|
|
253
|
+
config,
|
|
254
|
+
stmts,
|
|
255
|
+
embed,
|
|
256
|
+
insertVec: (r, e) => insertVec(stmts, r, e),
|
|
257
|
+
deleteVec: (r) => deleteVec(stmts, r),
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const appDistDir = join(__dirname, "..", "app-dist");
|
|
261
|
+
|
|
262
|
+
const server = http.createServer(async (req, res) => {
|
|
263
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
264
|
+
const pathname = url.pathname;
|
|
265
|
+
|
|
266
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
267
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
268
|
+
res.setHeader(
|
|
269
|
+
"Access-Control-Allow-Headers",
|
|
270
|
+
"Content-Type, Authorization",
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (req.method === "OPTIONS") {
|
|
274
|
+
res.writeHead(204);
|
|
275
|
+
res.end();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (pathname.startsWith("/api/")) {
|
|
280
|
+
try {
|
|
281
|
+
await handleApi(req, res, url, ctx, {
|
|
282
|
+
captureAndIndex,
|
|
283
|
+
gatherVaultStatus,
|
|
284
|
+
});
|
|
285
|
+
} catch (e) {
|
|
286
|
+
json(res, 500, { error: e.message });
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
serveStatic(req, res, pathname, appDistDir);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
server.listen(port);
|
|
295
|
+
return server;
|
|
296
|
+
}
|