@thingd/cli 0.70.0 → 0.72.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.
Files changed (62) hide show
  1. package/package.json +2 -2
  2. package/dist/commands/cloud.d.ts +0 -3
  3. package/dist/commands/cloud.d.ts.map +0 -1
  4. package/dist/commands/cloud.js +0 -648
  5. package/dist/commands/mcp-connect.d.ts +0 -3
  6. package/dist/commands/mcp-connect.d.ts.map +0 -1
  7. package/dist/commands/mcp-connect.js +0 -154
  8. package/dist/dashboard/public/assets/favicon-CgGFvG_0.svg +0 -4
  9. package/dist/dashboard/public/assets/index-C6PkDB7y.css +0 -1
  10. package/dist/dashboard/public/assets/index-CsLTLPTP.js +0 -4
  11. package/dist/dashboard/public/index.html +0 -19
  12. package/dist/dashboard/server.d.ts +0 -6
  13. package/dist/dashboard/server.d.ts.map +0 -1
  14. package/dist/dashboard/server.js +0 -663
  15. package/dist/data-movement.d.ts +0 -6
  16. package/dist/data-movement.d.ts.map +0 -1
  17. package/dist/data-movement.js +0 -475
  18. package/dist/doctor.d.ts +0 -3
  19. package/dist/doctor.d.ts.map +0 -1
  20. package/dist/doctor.js +0 -108
  21. package/dist/index.d.ts +0 -47
  22. package/dist/index.d.ts.map +0 -1
  23. package/dist/index.js +0 -1221
  24. package/dist/install.d.ts +0 -3
  25. package/dist/install.d.ts.map +0 -1
  26. package/dist/install.js +0 -229
  27. package/dist/interactive.d.ts +0 -2
  28. package/dist/interactive.d.ts.map +0 -1
  29. package/dist/interactive.js +0 -3036
  30. package/dist/lib/cloud-api.d.ts +0 -143
  31. package/dist/lib/cloud-api.d.ts.map +0 -1
  32. package/dist/lib/cloud-api.js +0 -207
  33. package/dist/lib/cloud-config.d.ts +0 -33
  34. package/dist/lib/cloud-config.d.ts.map +0 -1
  35. package/dist/lib/cloud-config.js +0 -42
  36. package/dist/lib/mcp-config-writer.d.ts +0 -26
  37. package/dist/lib/mcp-config-writer.d.ts.map +0 -1
  38. package/dist/lib/mcp-config-writer.js +0 -86
  39. package/dist/logo.d.ts +0 -3
  40. package/dist/logo.d.ts.map +0 -1
  41. package/dist/logo.js +0 -8
  42. package/dist/mcp/cluster.d.ts +0 -69
  43. package/dist/mcp/cluster.d.ts.map +0 -1
  44. package/dist/mcp/cluster.js +0 -304
  45. package/dist/mcp/config.d.ts +0 -14
  46. package/dist/mcp/config.d.ts.map +0 -1
  47. package/dist/mcp/config.js +0 -67
  48. package/dist/mcp/http.d.ts +0 -25
  49. package/dist/mcp/http.d.ts.map +0 -1
  50. package/dist/mcp/http.js +0 -597
  51. package/dist/mcp/index.d.ts +0 -5
  52. package/dist/mcp/index.d.ts.map +0 -1
  53. package/dist/mcp/index.js +0 -3
  54. package/dist/mcp-http.d.ts +0 -3
  55. package/dist/mcp-http.d.ts.map +0 -1
  56. package/dist/mcp-http.js +0 -42
  57. package/dist/mcp.d.ts +0 -3
  58. package/dist/mcp.d.ts.map +0 -1
  59. package/dist/mcp.js +0 -28
  60. package/dist/paths.d.ts +0 -4
  61. package/dist/paths.d.ts.map +0 -1
  62. package/dist/paths.js +0 -14
@@ -1,663 +0,0 @@
1
- import { existsSync, promises as fs, statSync } from "node:fs";
2
- import { createServer } from "node:http";
3
- import { createRequire } from "node:module";
4
- import { dirname, extname, isAbsolute, join, relative } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { handleRestRequest, ThingD } from "@thingd/sdk";
7
- import { readCloudConfig } from "../lib/cloud-config.js";
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = dirname(__filename);
10
- const _require = createRequire(__filename);
11
- let pkgVersion = "0.0.0";
12
- try {
13
- const pkg = _require("../../package.json");
14
- pkgVersion = pkg.version || "0.0.0";
15
- }
16
- catch {
17
- // fallback — try relative to dist
18
- try {
19
- const pkg = _require("../package.json");
20
- pkgVersion = pkg.version || "0.0.0";
21
- }
22
- catch {
23
- // leave default
24
- }
25
- }
26
- // Candidate public folders to support both tsx dev and compiled dist packaging
27
- const publicDirCandidates = [
28
- join(__dirname, "public"),
29
- join(__dirname, "../public"),
30
- join(__dirname, "../../../src/dashboard/public"),
31
- join(__dirname, "../../src/dashboard/public"),
32
- ];
33
- let publicDir = "";
34
- for (const cand of publicDirCandidates) {
35
- if (existsSync(cand)) {
36
- publicDir = cand;
37
- break;
38
- }
39
- }
40
- const MIME_TYPES = {
41
- ".html": "text/html",
42
- ".css": "text/css",
43
- ".js": "application/javascript",
44
- ".json": "application/json",
45
- ".png": "image/png",
46
- ".ico": "image/x-icon",
47
- ".svg": "image/svg+xml",
48
- ".webmanifest": "application/manifest+json",
49
- };
50
- async function readBody(req) {
51
- return new Promise((resolvePromise, rejectPromise) => {
52
- let body = "";
53
- req.on("data", (chunk) => {
54
- body += chunk;
55
- });
56
- req.on("end", () => {
57
- resolvePromise(body);
58
- });
59
- req.on("error", (err) => {
60
- rejectPromise(err);
61
- });
62
- });
63
- }
64
- function sendError(res, status, message) {
65
- res.writeHead(status, { "Content-Type": "application/json" });
66
- res.end(JSON.stringify({ error: message }));
67
- }
68
- function isCloudPath(path) {
69
- return path.startsWith("http://") || path.startsWith("https://") || path.startsWith("thingd://");
70
- }
71
- export async function startDashboardServer(connectionOptions, port) {
72
- // 1. Maintain dynamic active database options
73
- let activeOptions = { ...connectionOptions };
74
- let db = await ThingD.open({
75
- path: activeOptions.path,
76
- url: activeOptions.cloud ? activeOptions.path : undefined,
77
- driver: activeOptions.driver,
78
- authToken: activeOptions.authToken,
79
- instanceSlug: activeOptions.instanceSlug,
80
- });
81
- // 2. Create HTTP Server
82
- const server = createServer(async (req, res) => {
83
- try {
84
- const url = new URL(req.url || "", `http://${req.headers.host || "localhost"}`);
85
- const pathname = url.pathname;
86
- // Handle CORS for ease of developer integrations
87
- const allowedOrigins = [
88
- "http://localhost:8757",
89
- "http://localhost:8758",
90
- "http://127.0.0.1:8757",
91
- "http://127.0.0.1:8758",
92
- ];
93
- const origin = req.headers.origin;
94
- if (origin && allowedOrigins.includes(origin)) {
95
- res.setHeader("Access-Control-Allow-Origin", origin);
96
- }
97
- else {
98
- res.setHeader("Access-Control-Allow-Origin", "*");
99
- }
100
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
101
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
102
- if (req.method === "OPTIONS") {
103
- res.writeHead(204);
104
- res.end();
105
- return;
106
- }
107
- // CSRF protection: state-changing requests must come from a known origin
108
- if (req.method !== "GET" && req.method !== "OPTIONS" && req.method !== "HEAD") {
109
- const requestOrigin = req.headers.origin;
110
- if (requestOrigin && !allowedOrigins.includes(requestOrigin)) {
111
- sendError(res, 403, "Cross-origin state-changing requests are not allowed");
112
- return;
113
- }
114
- }
115
- // Security Gate middleware for API endpoints
116
- const isApiRoute = pathname.startsWith("/api/");
117
- const isConnectRoute = pathname === "/api/connect";
118
- const isRestRoute = pathname.startsWith("/v1/");
119
- if ((isApiRoute || isRestRoute) && !isConnectRoute && activeOptions.authToken) {
120
- const authHeader = req.headers.authorization;
121
- const expectedHeader = `Bearer ${activeOptions.authToken}`;
122
- if (!authHeader || authHeader !== expectedHeader) {
123
- res.writeHead(401, { "Content-Type": "application/json" });
124
- res.end(JSON.stringify({ error: "Unauthorized. Valid auth token is required." }));
125
- return;
126
- }
127
- }
128
- // REST API Routes
129
- if (pathname.startsWith("/api/")) {
130
- // POST /api/connect (Dynamic connection swapping)
131
- if (pathname === "/api/connect" && req.method === "POST") {
132
- const bodyStr = await readBody(req);
133
- const { path, driver, authToken, instanceSlug } = JSON.parse(bodyStr);
134
- if (!path || !driver) {
135
- sendError(res, 400, "Fields 'path' and 'driver' are required.");
136
- return;
137
- }
138
- const cloudMode = isCloudPath(path);
139
- const resolvedToken = authToken || (cloudMode ? readCloudConfig()?.token : undefined);
140
- const resolvedInstanceSlug = instanceSlug || activeOptions.instanceSlug;
141
- // Safely shut down the old db instance
142
- await db.close();
143
- // Spawn new db connection dynamically
144
- db = await ThingD.open({
145
- path,
146
- url: cloudMode ? path : undefined,
147
- driver,
148
- authToken: resolvedToken,
149
- instanceSlug: resolvedInstanceSlug,
150
- });
151
- activeOptions = {
152
- path,
153
- driver,
154
- authToken: resolvedToken,
155
- cloud: cloudMode,
156
- instanceSlug: resolvedInstanceSlug,
157
- };
158
- res.writeHead(200, { "Content-Type": "application/json" });
159
- res.end(JSON.stringify({ success: true, path, driver }));
160
- return;
161
- }
162
- // GET /api/status
163
- if (pathname === "/api/status" && req.method === "GET") {
164
- const [objects, events, activeJobs, deadJobs] = await Promise.all([
165
- db.countObjects(),
166
- db.countEvents(),
167
- db.countActiveJobs(),
168
- db.countDeadJobs(),
169
- ]);
170
- let dbSize = "N/A (in-memory)";
171
- if (activeOptions.driver === "native" && existsSync(activeOptions.path)) {
172
- try {
173
- const stats = statSync(activeOptions.path);
174
- dbSize = `${(stats.size / 1024).toFixed(1)} KB`;
175
- }
176
- catch {
177
- dbSize = "N/A (error)";
178
- }
179
- }
180
- res.writeHead(200, { "Content-Type": "application/json" });
181
- res.end(JSON.stringify({
182
- version: pkgVersion,
183
- mode: activeOptions.cloud ? "cloud" : "local",
184
- driver: activeOptions.driver || "memory",
185
- path: activeOptions.path,
186
- metrics: { objects, events, activeJobs, deadJobs, dbSize },
187
- authRequired: !!activeOptions.authToken,
188
- }));
189
- return;
190
- }
191
- // GET /api/collections
192
- if (pathname === "/api/collections" && req.method === "GET") {
193
- const collections = await db.listCollections();
194
- res.writeHead(200, { "Content-Type": "application/json" });
195
- res.end(JSON.stringify(collections));
196
- return;
197
- }
198
- // GET/POST/DELETE /api/objects
199
- if (pathname === "/api/objects") {
200
- if (req.method === "GET") {
201
- const collection = url.searchParams.get("collection");
202
- if (!collection) {
203
- sendError(res, 400, "Query parameter 'collection' is required.");
204
- return;
205
- }
206
- const objects = await db.listObjects(collection);
207
- res.writeHead(200, { "Content-Type": "application/json" });
208
- res.end(JSON.stringify(objects));
209
- return;
210
- }
211
- if (req.method === "POST") {
212
- const bodyStr = await readBody(req);
213
- const { collection, id, text, data } = JSON.parse(bodyStr);
214
- if (!collection || !id) {
215
- sendError(res, 400, "Fields 'collection' and 'id' are required.");
216
- return;
217
- }
218
- const result = await db.put(collection, { id, text, ...data });
219
- res.writeHead(200, { "Content-Type": "application/json" });
220
- res.end(JSON.stringify(result));
221
- return;
222
- }
223
- if (req.method === "DELETE") {
224
- const collection = url.searchParams.get("collection");
225
- const id = url.searchParams.get("id");
226
- if (!collection || !id) {
227
- sendError(res, 400, "Query parameters 'collection' and 'id' are required.");
228
- return;
229
- }
230
- const result = await db.delete(collection, id);
231
- res.writeHead(200, { "Content-Type": "application/json" });
232
- res.end(JSON.stringify(result));
233
- return;
234
- }
235
- }
236
- // GET/POST /api/events
237
- if (pathname === "/api/events") {
238
- if (req.method === "GET") {
239
- const stream = url.searchParams.get("stream") || undefined;
240
- const limitVal = url.searchParams.get("limit");
241
- const limit = limitVal ? Number.parseInt(limitVal, 10) : undefined;
242
- const events = await db.events.list(stream);
243
- const sliced = limit ? events.slice(0, limit) : events;
244
- res.writeHead(200, { "Content-Type": "application/json" });
245
- res.end(JSON.stringify(sliced));
246
- return;
247
- }
248
- if (req.method === "POST") {
249
- const bodyStr = await readBody(req);
250
- const { stream, type, text, data } = JSON.parse(bodyStr);
251
- if (!stream || !type) {
252
- sendError(res, 400, "Fields 'stream' and 'type' are required.");
253
- return;
254
- }
255
- const result = await db.events.append(stream, { type, text, ...data });
256
- res.writeHead(200, { "Content-Type": "application/json" });
257
- res.end(JSON.stringify(result));
258
- return;
259
- }
260
- }
261
- // GET /api/events/streams
262
- if (pathname === "/api/events/streams" && req.method === "GET") {
263
- const streams = await db.listStreams();
264
- res.writeHead(200, { "Content-Type": "application/json" });
265
- res.end(JSON.stringify(streams));
266
- return;
267
- }
268
- // GET /api/queues
269
- if (pathname === "/api/queues" && req.method === "GET") {
270
- const queues = await db.listQueues();
271
- res.writeHead(200, { "Content-Type": "application/json" });
272
- res.end(JSON.stringify(queues));
273
- return;
274
- }
275
- // GET /api/queues/jobs
276
- if (pathname === "/api/queues/jobs" && req.method === "GET") {
277
- const queue = url.searchParams.get("queue");
278
- const status = url.searchParams.get("status");
279
- if (!queue) {
280
- sendError(res, 400, "Query parameter 'queue' is required.");
281
- return;
282
- }
283
- const q = db.queue(queue);
284
- const jobs = status === "dead" ? await q.dead() : await q.list();
285
- res.writeHead(200, { "Content-Type": "application/json" });
286
- res.end(JSON.stringify(jobs));
287
- return;
288
- }
289
- // GET /api/queues/stats
290
- if (pathname === "/api/queues/stats" && req.method === "GET") {
291
- const queue = url.searchParams.get("queue");
292
- if (!queue) {
293
- sendError(res, 400, "Query parameter 'queue' is required.");
294
- return;
295
- }
296
- const q = db.queue(queue);
297
- const [activeJobs, deadJobs] = await Promise.all([q.list(), q.dead()]);
298
- const leased = activeJobs.filter((j) => j.status === "leased").length;
299
- const ready = activeJobs.filter((j) => j.status === "ready").length;
300
- res.writeHead(200, { "Content-Type": "application/json" });
301
- res.end(JSON.stringify({
302
- queue,
303
- totalActive: activeJobs.length,
304
- ready,
305
- leased,
306
- dead: deadJobs.length,
307
- }));
308
- return;
309
- }
310
- // POST /api/queues/push
311
- if (pathname === "/api/queues/push" && req.method === "POST") {
312
- const bodyStr = await readBody(req);
313
- const { queue, payload, delayMs, maxAttempts, idempotencyKey } = JSON.parse(bodyStr);
314
- if (!queue || !payload) {
315
- sendError(res, 400, "Fields 'queue' and 'payload' are required.");
316
- return;
317
- }
318
- const q = db.queue(queue);
319
- const result = await q.push(payload, { delayMs, maxAttempts, idempotencyKey });
320
- res.writeHead(200, { "Content-Type": "application/json" });
321
- res.end(JSON.stringify(result));
322
- return;
323
- }
324
- // POST /api/queues/claim
325
- if (pathname === "/api/queues/claim" && req.method === "POST") {
326
- const bodyStr = await readBody(req);
327
- const { queue, leaseMs } = JSON.parse(bodyStr);
328
- if (!queue) {
329
- sendError(res, 400, "Field 'queue' is required.");
330
- return;
331
- }
332
- const q = db.queue(queue);
333
- const job = await q.claim({ leaseMs });
334
- res.writeHead(200, { "Content-Type": "application/json" });
335
- res.end(JSON.stringify(job || null));
336
- return;
337
- }
338
- // POST /api/queues/ack
339
- if (pathname === "/api/queues/ack" && req.method === "POST") {
340
- const bodyStr = await readBody(req);
341
- const { queue, jobId } = JSON.parse(bodyStr);
342
- if (!queue || !jobId) {
343
- sendError(res, 400, "Fields 'queue' and 'jobId' are required.");
344
- return;
345
- }
346
- const q = db.queue(queue);
347
- const result = await q.ack(jobId);
348
- res.writeHead(200, { "Content-Type": "application/json" });
349
- res.end(JSON.stringify(result));
350
- return;
351
- }
352
- // POST /api/queues/nack
353
- if (pathname === "/api/queues/nack" && req.method === "POST") {
354
- const bodyStr = await readBody(req);
355
- const { queue, jobId, error, delayMs } = JSON.parse(bodyStr);
356
- if (!queue || !jobId) {
357
- sendError(res, 400, "Fields 'queue' and 'jobId' are required.");
358
- return;
359
- }
360
- const q = db.queue(queue);
361
- const result = await q.nack(jobId, { error, delayMs });
362
- res.writeHead(200, { "Content-Type": "application/json" });
363
- res.end(JSON.stringify(result));
364
- return;
365
- }
366
- // GET /api/search
367
- if (pathname === "/api/search" && req.method === "GET") {
368
- const query = url.searchParams.get("query");
369
- const limitVal = url.searchParams.get("limit");
370
- const collectionsStr = url.searchParams.get("collections");
371
- const filterStr = url.searchParams.get("filter");
372
- if (!query) {
373
- sendError(res, 400, "Query parameter 'query' is required.");
374
- return;
375
- }
376
- const limit = limitVal ? Number.parseInt(limitVal, 10) : undefined;
377
- const collections = collectionsStr ? collectionsStr.split(",") : undefined;
378
- const filter = filterStr ? JSON.parse(filterStr) : undefined;
379
- const results = await db.search(query, { limit, collections, filter });
380
- res.writeHead(200, { "Content-Type": "application/json" });
381
- res.end(JSON.stringify(results));
382
- return;
383
- }
384
- // GET /api/schema
385
- if (pathname === "/api/schema" && req.method === "GET") {
386
- const collection = url.searchParams.get("collection") || undefined;
387
- const schemas = await db.schema(collection);
388
- res.writeHead(200, { "Content-Type": "application/json" });
389
- res.end(JSON.stringify(schemas));
390
- return;
391
- }
392
- // POST /api/nlq
393
- if (pathname === "/api/nlq" && req.method === "POST") {
394
- const bodyStr = await readBody(req);
395
- const { question, collection, model, endpoint, apiKey } = JSON.parse(bodyStr);
396
- if (!question) {
397
- sendError(res, 400, "Field 'question' is required.");
398
- return;
399
- }
400
- const llmModel = model || "llama3";
401
- const llmEndpoint = (endpoint || "http://localhost:11434/v1").replace(/\/+$/, "");
402
- const llmApiKey = apiKey || "";
403
- // Step 1: Reflect schema
404
- const schemas = await db.schema(collection || undefined);
405
- if (!schemas || schemas.length === 0) {
406
- sendError(res, 400, "No collections found. Add objects first or specify a valid collection.");
407
- return;
408
- }
409
- // Step 2: Build prompt and call LLM
410
- const systemPrompt = `You are a data analysis assistant. The user has a thingd database with these collections and inferred schemas:
411
-
412
- ${JSON.stringify(schemas, null, 2)}
413
-
414
- You can perform these operations on the data:
415
- - "aggregate": count/sum/avg/min/max with optional groupBy
416
- - "timeseries": time-bucketed aggregation by hour/day/week/month
417
- - "search": full-text search across objects
418
-
419
- Respond with ONLY a JSON object (no markdown, no explanation) matching this type:
420
- {
421
- "action": "aggregate" | "timeseries" | "search",
422
- "collection": "string (collection name)",
423
- "function": "count" | "sum" | "avg" | "min" | "max" (omit for search)",
424
- "field": "string (field name for sum/avg/min/max, omit for count)",
425
- "groupBy": "string (field name to group by, optional)",
426
- "bucket": "hour" | "day" | "week" | "month" (only for timeseries)",
427
- "query": "string (search query, only for search action)",
428
- "limit": number (optional, max 100)
429
- }}
430
-
431
- Example: { "action": "aggregate", "collection": "orders", "function": "sum", "field": "revenue", "groupBy": "region" }`;
432
- const llmResponse = await fetch(`${llmEndpoint}/chat/completions`, {
433
- method: "POST",
434
- headers: {
435
- "Content-Type": "application/json",
436
- ...(llmApiKey ? { Authorization: `Bearer ${llmApiKey}` } : {}),
437
- },
438
- body: JSON.stringify({
439
- model: llmModel,
440
- messages: [
441
- { role: "system", content: systemPrompt },
442
- { role: "user", content: question },
443
- ],
444
- max_tokens: 1024,
445
- temperature: 0.1,
446
- }),
447
- });
448
- if (!llmResponse.ok) {
449
- const errText = await llmResponse.text();
450
- sendError(res, 502, `LLM returned ${llmResponse.status}: ${errText}`);
451
- return;
452
- }
453
- const llmData = await llmResponse.json();
454
- const llmText = llmData.choices?.[0]?.message?.content;
455
- if (!llmText) {
456
- sendError(res, 502, "LLM returned no choices");
457
- return;
458
- }
459
- // Step 3: Parse intent
460
- const cleaned = llmText
461
- .trim()
462
- .replace(/^```(?:json)?\s*/, "")
463
- .replace(/\s*```$/, "")
464
- .trim();
465
- let intent;
466
- try {
467
- intent = JSON.parse(cleaned);
468
- }
469
- catch {
470
- sendError(res, 502, `Failed to parse LLM response as JSON: ${cleaned}`);
471
- return;
472
- }
473
- // Step 4: Execute
474
- let data;
475
- switch (intent.action) {
476
- case "aggregate": {
477
- const fn = intent.function || "count";
478
- const col = intent.collection;
479
- if (fn === "count") {
480
- data = await db.aggregate.count(col, {
481
- groupBy: intent.groupBy,
482
- });
483
- }
484
- else if (fn === "sum") {
485
- data = await db.aggregate.sum(col, intent.field, {
486
- groupBy: intent.groupBy,
487
- });
488
- }
489
- else if (fn === "avg") {
490
- data = await db.aggregate.avg(col, intent.field, {
491
- groupBy: intent.groupBy,
492
- });
493
- }
494
- else if (fn === "min") {
495
- data = await db.aggregate.min(col, intent.field, {
496
- groupBy: intent.groupBy,
497
- });
498
- }
499
- else {
500
- data = await db.aggregate.max(col, intent.field, {
501
- groupBy: intent.groupBy,
502
- });
503
- }
504
- break;
505
- }
506
- case "timeseries": {
507
- const bucket = (intent.bucket || "day");
508
- data = await db.timeseries(intent.collection, {
509
- function: (intent.function || "count"),
510
- bucket,
511
- field: intent.field,
512
- });
513
- break;
514
- }
515
- case "search": {
516
- data = await db.search(intent.query || question, {
517
- collections: [intent.collection],
518
- limit: intent.limit || 10,
519
- });
520
- break;
521
- }
522
- default:
523
- sendError(res, 400, `Unknown action: ${intent.action}`);
524
- return;
525
- }
526
- const result = {
527
- answer: formatAnswer(intent, data),
528
- data,
529
- intent,
530
- };
531
- res.writeHead(200, { "Content-Type": "application/json" });
532
- res.end(JSON.stringify(result));
533
- return;
534
- }
535
- // GET /api/db/checkpoint
536
- if (pathname === "/api/db/checkpoint" && req.method === "GET") {
537
- try {
538
- const result = db.walCheckpoint();
539
- res.writeHead(200, { "Content-Type": "application/json" });
540
- res.end(JSON.stringify(result));
541
- }
542
- catch (e) {
543
- sendError(res, 400, e instanceof Error ? e.message : String(e));
544
- }
545
- return;
546
- }
547
- // GET /api/db/integrity
548
- if (pathname === "/api/db/integrity" && req.method === "GET") {
549
- try {
550
- await db.countObjects();
551
- res.writeHead(200, { "Content-Type": "application/json" });
552
- res.end(JSON.stringify({ ok: true, message: "Database is accessible" }));
553
- }
554
- catch (e) {
555
- res.writeHead(200, { "Content-Type": "application/json" });
556
- res.end(JSON.stringify({ ok: false, message: e instanceof Error ? e.message : String(e) }));
557
- }
558
- return;
559
- }
560
- // POST /api/backup
561
- if (pathname === "/api/backup" && req.method === "POST") {
562
- try {
563
- let body = "";
564
- for await (const chunk of req) {
565
- body += chunk;
566
- }
567
- const { path: backupPath } = JSON.parse(body);
568
- if (!backupPath) {
569
- sendError(res, 400, "Missing 'path' in request body");
570
- return;
571
- }
572
- db.backupTo(backupPath);
573
- const { statSync } = await import("node:fs");
574
- const stats = statSync(backupPath);
575
- res.writeHead(200, { "Content-Type": "application/json" });
576
- res.end(JSON.stringify({ path: backupPath, sizeBytes: stats.size }));
577
- }
578
- catch (e) {
579
- sendError(res, 500, e instanceof Error ? e.message : String(e));
580
- }
581
- return;
582
- }
583
- // GET /api/config/error-mode
584
- if (pathname === "/api/config/error-mode" && req.method === "GET") {
585
- res.writeHead(200, { "Content-Type": "application/json" });
586
- res.end(JSON.stringify({ productionMode: false }));
587
- return;
588
- }
589
- sendError(res, 404, `Endpoint ${req.method} ${pathname} not found.`);
590
- return;
591
- }
592
- // REST API Routes (/v1/*)
593
- if (pathname.startsWith("/v1/")) {
594
- await handleRestRequest(db, req, res, pathname);
595
- return;
596
- }
597
- // Static File Server
598
- const targetFilePath = pathname === "/" ? "index.html" : pathname.replace(/^\//, "");
599
- const fullFilePath = join(publicDir, targetFilePath);
600
- // Security: ensure the resolved path is inside the public folder
601
- const relativePath = relative(publicDir, fullFilePath);
602
- if (isAbsolute(relativePath) || relativePath.startsWith("..")) {
603
- res.writeHead(403);
604
- res.end("Forbidden");
605
- return;
606
- }
607
- if (existsSync(fullFilePath)) {
608
- const fileContent = await fs.readFile(fullFilePath);
609
- const ext = extname(fullFilePath).toLowerCase();
610
- const contentType = MIME_TYPES[ext] || "application/octet-stream";
611
- res.writeHead(200, { "Content-Type": contentType });
612
- res.end(fileContent);
613
- }
614
- else {
615
- res.writeHead(404);
616
- res.end("Not Found");
617
- }
618
- }
619
- catch (err) {
620
- console.error("Dashboard server exception:", err);
621
- sendError(res, 500, err instanceof Error ? err.message : String(err));
622
- }
623
- });
624
- return new Promise((resolvePromise, rejectPromise) => {
625
- server.listen(port, () => {
626
- resolvePromise({
627
- server,
628
- close: async () => {
629
- await new Promise((closeRes) => server.close(() => closeRes()));
630
- await db.close();
631
- },
632
- });
633
- });
634
- server.on("error", (err) => {
635
- rejectPromise(err);
636
- });
637
- });
638
- }
639
- function formatAnswer(intent, data) {
640
- const fnName = intent.function || "count";
641
- const field = intent.field || "objects";
642
- switch (intent.action) {
643
- case "aggregate": {
644
- const d = data;
645
- const total = d?.total ?? 0;
646
- const groups = d?.groups?.length ?? 0;
647
- if (groups > 0) {
648
- return `${fnName} of ${field} = ${total}, grouped by ${intent.groupBy} into ${groups} groups`;
649
- }
650
- return `${fnName} of ${field} = ${total}`;
651
- }
652
- case "timeseries": {
653
- const d = data;
654
- return `Time series with ${d?.buckets?.length ?? 0} buckets`;
655
- }
656
- case "search": {
657
- const hits = data?.length ?? 0;
658
- return `Found ${hits} results`;
659
- }
660
- default:
661
- return "Query executed.";
662
- }
663
- }
@@ -1,6 +0,0 @@
1
- import { type CliContext } from "./index.js";
2
- export declare function runExport(context: CliContext): Promise<void>;
3
- export declare function runImport(context: CliContext): Promise<void>;
4
- export declare function runSnapshot(context: CliContext): Promise<void>;
5
- export declare function runBackup(context: CliContext): Promise<void>;
6
- //# sourceMappingURL=data-movement.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"data-movement.d.ts","sourceRoot":"","sources":["../src/data-movement.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,UAAU,EAAwD,MAAM,YAAY,CAAC;AAwEnG,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAyClE;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAsFlE;AAoND,wBAAsB,WAAW,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmHpE;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAyClE"}