@thingd/cli 0.31.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.
- package/LICENSE +201 -0
- package/README.md +238 -0
- package/dist/dashboard/public/assets/index-B-Y-3-0l.js +2 -0
- package/dist/dashboard/public/assets/index-B5YhpIl3.js +2 -0
- package/dist/dashboard/public/assets/index-BnFclxvN.css +1 -0
- package/dist/dashboard/public/assets/index-BtA9rnyI.js +2 -0
- package/dist/dashboard/public/assets/index-BzLTzidY.js +2 -0
- package/dist/dashboard/public/assets/index-C6PkDB7y.css +1 -0
- package/dist/dashboard/public/assets/index-D8yUCdOQ.js +2 -0
- package/dist/dashboard/public/assets/index-fQywB2df.js +2 -0
- package/dist/dashboard/public/assets/index-kZdrdi3K.css +1 -0
- package/dist/dashboard/public/assets/index-kgZrboBN.js +4 -0
- package/dist/dashboard/public/favicon.svg +1 -0
- package/dist/dashboard/public/icons.svg +24 -0
- package/dist/dashboard/public/index.html +16 -0
- package/dist/dashboard/server.d.ts +6 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +385 -0
- package/dist/data-movement.d.ts +5 -0
- package/dist/data-movement.d.ts.map +1 -0
- package/dist/data-movement.js +257 -0
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +109 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1015 -0
- package/dist/install.d.ts +3 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +311 -0
- package/dist/interactive.d.ts +2 -0
- package/dist/interactive.d.ts.map +1 -0
- package/dist/interactive.js +1592 -0
- package/dist/logo.d.ts +3 -0
- package/dist/logo.d.ts.map +1 -0
- package/dist/logo.js +8 -0
- package/dist/mcp/audit.d.ts +27 -0
- package/dist/mcp/audit.d.ts.map +1 -0
- package/dist/mcp/audit.js +36 -0
- package/dist/mcp/cluster.d.ts +68 -0
- package/dist/mcp/cluster.d.ts.map +1 -0
- package/dist/mcp/cluster.js +303 -0
- package/dist/mcp/config.d.ts +14 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +67 -0
- package/dist/mcp/http.d.ts +25 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.js +588 -0
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/result.d.ts +3 -0
- package/dist/mcp/result.d.ts.map +1 -0
- package/dist/mcp/result.js +10 -0
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +51 -0
- package/dist/mcp/tools.d.ts +10 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +568 -0
- package/dist/mcp-http.d.ts +3 -0
- package/dist/mcp-http.d.ts.map +1 -0
- package/dist/mcp-http.js +42 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +22 -0
- package/dist/paths.d.ts +4 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +14 -0
- package/dist/rest/helpers.d.ts +17 -0
- package/dist/rest/helpers.d.ts.map +1 -0
- package/dist/rest/helpers.js +55 -0
- package/dist/rest/server.d.ts +4 -0
- package/dist/rest/server.d.ts.map +1 -0
- package/dist/rest/server.js +317 -0
- package/package.json +57 -0
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,UAAU,EAA6B,MAAM,YAAY,CAAC;AAGxE,wBAAsB,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB/D"}
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createThingdMcpServer, readMcpHardeningOptionsFromEnv } from "@thingd/node";
|
|
3
|
+
import { resolveConnection, withDb } from "./index.js";
|
|
4
|
+
import { readMcpAuditOptionsFromEnv } from "./mcp/config.js";
|
|
5
|
+
export async function runMcp(context) {
|
|
6
|
+
const connection = resolveConnection(context);
|
|
7
|
+
await withDb(context, async (db) => {
|
|
8
|
+
const server = createThingdMcpServer(db, {
|
|
9
|
+
audit: readMcpAuditOptionsFromEnv(context.env),
|
|
10
|
+
hardening: readMcpHardeningOptionsFromEnv(context.env),
|
|
11
|
+
});
|
|
12
|
+
const transport = new StdioServerTransport();
|
|
13
|
+
context.stderr.write(`\nthingd stdio MCP server started successfully.\n`);
|
|
14
|
+
context.stderr.write(` ✓ Database: ${connection.path}\n`);
|
|
15
|
+
context.stderr.write(` ✓ Driver: ${connection.driver ?? "memory"}\n`);
|
|
16
|
+
context.stderr.write(` ✓ Transport: Stdio (listening silently on stdin)\n\n`);
|
|
17
|
+
await server.connect(transport);
|
|
18
|
+
// Keep the process alive and the database connection open
|
|
19
|
+
// so the MCP server can continue to receive messages over stdio.
|
|
20
|
+
return new Promise(() => { });
|
|
21
|
+
});
|
|
22
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAOA,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const THINGD_DIR_NAME = ".thingd";
|
|
5
|
+
const THINGD_DB_NAME = "data.db";
|
|
6
|
+
export function defaultThingdDir() {
|
|
7
|
+
return join(homedir(), THINGD_DIR_NAME);
|
|
8
|
+
}
|
|
9
|
+
export function defaultThingdDbPath() {
|
|
10
|
+
return join(defaultThingdDir(), THINGD_DB_NAME);
|
|
11
|
+
}
|
|
12
|
+
export function ensureThingdDir() {
|
|
13
|
+
mkdirSync(defaultThingdDir(), { recursive: true });
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
type SortField = "id" | "collection" | "created_at" | "updated_at" | "version";
|
|
3
|
+
type SortDirection = "asc" | "desc";
|
|
4
|
+
type LocalSortBy = {
|
|
5
|
+
field: SortField;
|
|
6
|
+
direction?: SortDirection;
|
|
7
|
+
};
|
|
8
|
+
export declare function readBody(req: IncomingMessage): Promise<string>;
|
|
9
|
+
export declare function sendJson(res: ServerResponse, status: number, data: unknown): void;
|
|
10
|
+
export declare function sendData(res: ServerResponse, data: unknown): void;
|
|
11
|
+
export declare function sendDataList(res: ServerResponse, data: unknown[], total?: number): void;
|
|
12
|
+
export declare function sendError(res: ServerResponse, status: number, code: string, message: string): void;
|
|
13
|
+
export declare function parseSortBy(params: URLSearchParams): LocalSortBy | undefined;
|
|
14
|
+
export declare function parseFilter(params: URLSearchParams): Record<string, unknown> | undefined;
|
|
15
|
+
export declare function parseIntParam(value: string | null): number | undefined;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/rest/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,KAAK,SAAS,GAAG,IAAI,GAAG,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;AAC/E,KAAK,aAAa,GAAG,KAAK,GAAG,MAAM,CAAC;AAEpC,KAAK,WAAW,GAAG;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B,CAAC;AAEF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAS9D;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAGjF;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAEjE;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAEvF;AAED,wBAAgB,SAAS,CACvB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,IAAI,CAEN;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,GAAG,SAAS,CAY5E;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAWxF;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAMtE"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function readBody(req) {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
let body = "";
|
|
4
|
+
req.on("data", (chunk) => {
|
|
5
|
+
body += chunk.toString();
|
|
6
|
+
});
|
|
7
|
+
req.on("end", () => resolve(body));
|
|
8
|
+
req.on("error", reject);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export function sendJson(res, status, data) {
|
|
12
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
13
|
+
res.end(JSON.stringify(data));
|
|
14
|
+
}
|
|
15
|
+
export function sendData(res, data) {
|
|
16
|
+
sendJson(res, 200, { data });
|
|
17
|
+
}
|
|
18
|
+
export function sendDataList(res, data, total) {
|
|
19
|
+
sendJson(res, 200, { data, ...(total !== undefined ? { total } : {}) });
|
|
20
|
+
}
|
|
21
|
+
export function sendError(res, status, code, message) {
|
|
22
|
+
sendJson(res, status, { error: { code, message } });
|
|
23
|
+
}
|
|
24
|
+
export function parseSortBy(params) {
|
|
25
|
+
const sort = params.get("sortBy");
|
|
26
|
+
if (!sort) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const parts = sort.split(":");
|
|
30
|
+
const field = parts[0];
|
|
31
|
+
const dir = parts[1] ?? "asc";
|
|
32
|
+
if (!field) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return { field: field, direction: dir };
|
|
36
|
+
}
|
|
37
|
+
export function parseFilter(params) {
|
|
38
|
+
const filter = {};
|
|
39
|
+
let hasFilter = false;
|
|
40
|
+
params.forEach((value, key) => {
|
|
41
|
+
if (key.startsWith("filter.")) {
|
|
42
|
+
const field = key.slice(7);
|
|
43
|
+
filter[field] = value;
|
|
44
|
+
hasFilter = true;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return hasFilter ? filter : undefined;
|
|
48
|
+
}
|
|
49
|
+
export function parseIntParam(value) {
|
|
50
|
+
if (!value) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const n = Number(value);
|
|
54
|
+
return Number.isNaN(n) ? undefined : n;
|
|
55
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { ThingD } from "thingd";
|
|
3
|
+
export declare function handleRestRequest(db: ThingD, req: IncomingMessage, res: ServerResponse, pathname: string): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/rest/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAwCrC,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CA2Uf"}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { parseFilter, parseIntParam, parseSortBy, readBody, sendData, sendDataList, sendError, } from "./helpers.js";
|
|
2
|
+
function matchRoute(pathname, pattern) {
|
|
3
|
+
const patternParts = pattern.split("/");
|
|
4
|
+
const pathParts = pathname.split("/");
|
|
5
|
+
if (patternParts.length !== pathParts.length) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const match = {};
|
|
9
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
10
|
+
const pp = patternParts[i] ?? "";
|
|
11
|
+
const xp = pathParts[i] ?? "";
|
|
12
|
+
if (pp.startsWith(":")) {
|
|
13
|
+
const key = pp.slice(1);
|
|
14
|
+
match[key] = xp;
|
|
15
|
+
}
|
|
16
|
+
else if (pp !== xp) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return match;
|
|
21
|
+
}
|
|
22
|
+
export async function handleRestRequest(db, req, res, pathname) {
|
|
23
|
+
const method = req.method ?? "GET";
|
|
24
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
25
|
+
try {
|
|
26
|
+
// ─── Health ──────────────────────────────────────────────────
|
|
27
|
+
if (pathname === "/v1/health" && method === "GET") {
|
|
28
|
+
const [objects, events, links, queues, collections, streams] = await Promise.all([
|
|
29
|
+
db.countObjects(),
|
|
30
|
+
db.countEvents(),
|
|
31
|
+
db.countLinks(),
|
|
32
|
+
db.listQueues(),
|
|
33
|
+
db.listCollections(),
|
|
34
|
+
db.listStreams(),
|
|
35
|
+
]);
|
|
36
|
+
sendData(res, {
|
|
37
|
+
status: "ok",
|
|
38
|
+
version: "0.31.0",
|
|
39
|
+
counts: {
|
|
40
|
+
objects,
|
|
41
|
+
events,
|
|
42
|
+
links,
|
|
43
|
+
queues: queues.length,
|
|
44
|
+
collections: collections.length,
|
|
45
|
+
streams: streams.length,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// ─── Counts ──────────────────────────────────────────────────
|
|
51
|
+
if (pathname === "/v1/counts/objects" && method === "GET") {
|
|
52
|
+
sendData(res, { count: await db.countObjects() });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (pathname === "/v1/counts/events" && method === "GET") {
|
|
56
|
+
sendData(res, { count: await db.countEvents() });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (pathname === "/v1/counts/links" && method === "GET") {
|
|
60
|
+
sendData(res, { count: await db.countLinks() });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// ─── Collections / Streams / Queues ──────────────────────────
|
|
64
|
+
if (pathname === "/v1/collections" && method === "GET") {
|
|
65
|
+
sendDataList(res, await db.listCollections());
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (pathname === "/v1/streams" && method === "GET") {
|
|
69
|
+
sendDataList(res, await db.listStreams());
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (pathname === "/v1/queues" && method === "GET") {
|
|
73
|
+
sendDataList(res, await db.listQueues());
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// ─── Objects ─────────────────────────────────────────────────
|
|
77
|
+
// GET /v1/objects?collection=...&filter.x=...&sortBy=...&limit=...&offset=...
|
|
78
|
+
if (pathname === "/v1/objects" && method === "GET") {
|
|
79
|
+
const collection = url.searchParams.get("collection");
|
|
80
|
+
if (!collection) {
|
|
81
|
+
sendError(res, 400, "bad_request", "Query parameter 'collection' is required");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const filter = parseFilter(url.searchParams);
|
|
85
|
+
const sortBy = parseSortBy(url.searchParams);
|
|
86
|
+
const limit = parseIntParam(url.searchParams.get("limit"));
|
|
87
|
+
const offset = parseIntParam(url.searchParams.get("offset"));
|
|
88
|
+
const objects = await db.listObjects(collection, { filter, sortBy, limit, offset });
|
|
89
|
+
sendDataList(res, objects);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// PUT /v1/objects/:collection/:id
|
|
93
|
+
const objMatch = matchRoute(pathname, "/v1/objects/:collection/:id");
|
|
94
|
+
if (objMatch?.collection && objMatch?.id && method === "PUT") {
|
|
95
|
+
const body = JSON.parse(await readBody(req));
|
|
96
|
+
body.id = objMatch.id;
|
|
97
|
+
const result = await db.put(objMatch.collection, body);
|
|
98
|
+
sendData(res, result);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// GET /v1/objects/:collection/:id
|
|
102
|
+
if (objMatch?.collection && objMatch?.id && method === "GET") {
|
|
103
|
+
const result = await db.get(objMatch.collection, objMatch.id);
|
|
104
|
+
if (!result) {
|
|
105
|
+
sendError(res, 404, "not_found", `Object '${objMatch.id}' not found in collection '${objMatch.collection}'`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
sendData(res, result);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// DELETE /v1/objects/:collection/:id
|
|
112
|
+
if (objMatch?.collection && objMatch?.id && method === "DELETE") {
|
|
113
|
+
const result = await db.delete(objMatch.collection, objMatch.id);
|
|
114
|
+
sendData(res, result);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// PUT /v1/objects/batch?collection=...
|
|
118
|
+
if (pathname === "/v1/objects/batch" && method === "PUT") {
|
|
119
|
+
const collection = url.searchParams.get("collection");
|
|
120
|
+
if (!collection) {
|
|
121
|
+
sendError(res, 400, "bad_request", "Query parameter 'collection' is required");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const body = JSON.parse(await readBody(req));
|
|
125
|
+
const objects = Array.isArray(body) ? body : body.objects;
|
|
126
|
+
if (!Array.isArray(objects)) {
|
|
127
|
+
sendError(res, 400, "bad_request", "Body must be an array or { objects: [...] }");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const result = await db.putBatch(collection, objects);
|
|
131
|
+
sendData(res, result);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// DELETE /v1/objects/batch?collection=...
|
|
135
|
+
if (pathname === "/v1/objects/batch" && method === "DELETE") {
|
|
136
|
+
const collection = url.searchParams.get("collection");
|
|
137
|
+
if (!collection) {
|
|
138
|
+
sendError(res, 400, "bad_request", "Query parameter 'collection' is required");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const body = JSON.parse(await readBody(req));
|
|
142
|
+
const ids = Array.isArray(body) ? body : body.ids;
|
|
143
|
+
if (!Array.isArray(ids)) {
|
|
144
|
+
sendError(res, 400, "bad_request", "Body must be an array or { ids: [...] }");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const count = await db.deleteBatch(collection, ids);
|
|
148
|
+
sendData(res, { deleted: count });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// ─── Search ──────────────────────────────────────────────────
|
|
152
|
+
if (pathname === "/v1/search" && method === "POST") {
|
|
153
|
+
const body = JSON.parse(await readBody(req));
|
|
154
|
+
if (!body.query) {
|
|
155
|
+
sendError(res, 400, "bad_request", "Field 'query' is required");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const results = await db.search(body.query, {
|
|
159
|
+
collections: body.collections,
|
|
160
|
+
limit: body.limit,
|
|
161
|
+
filter: body.filter,
|
|
162
|
+
});
|
|
163
|
+
sendData(res, results);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// ─── Events ──────────────────────────────────────────────────
|
|
167
|
+
// POST /v1/events/:stream
|
|
168
|
+
const streamMatch = matchRoute(pathname, "/v1/events/:stream");
|
|
169
|
+
if (streamMatch?.stream && method === "POST") {
|
|
170
|
+
const body = JSON.parse(await readBody(req));
|
|
171
|
+
if (!body.type) {
|
|
172
|
+
sendError(res, 400, "bad_request", "Field 'type' is required");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const event = await db.events.append(streamMatch.stream, body);
|
|
176
|
+
sendData(res, event);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// GET /v1/events?stream=...&fromSequence=...&limit=...
|
|
180
|
+
if (pathname === "/v1/events" && method === "GET") {
|
|
181
|
+
const stream = url.searchParams.get("stream") ?? undefined;
|
|
182
|
+
const fromSequence = parseIntParam(url.searchParams.get("fromSequence"));
|
|
183
|
+
const limit = parseIntParam(url.searchParams.get("limit"));
|
|
184
|
+
const events = await db.events.list(stream, { fromSequence, limit });
|
|
185
|
+
sendDataList(res, events);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// ─── Queues ──────────────────────────────────────────────────
|
|
189
|
+
// POST /v1/queues/:queue/push
|
|
190
|
+
const pushMatch = matchRoute(pathname, "/v1/queues/:queue/push");
|
|
191
|
+
if (pushMatch?.queue && method === "POST") {
|
|
192
|
+
const body = JSON.parse(await readBody(req));
|
|
193
|
+
const job = await db.queue(pushMatch.queue).push(body.payload ?? body, {
|
|
194
|
+
idempotencyKey: body.idempotencyKey,
|
|
195
|
+
maxAttempts: body.maxAttempts,
|
|
196
|
+
delayMs: body.delayMs,
|
|
197
|
+
});
|
|
198
|
+
sendData(res, job);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// POST /v1/queues/:queue/claim
|
|
202
|
+
const claimMatch = matchRoute(pathname, "/v1/queues/:queue/claim");
|
|
203
|
+
if (claimMatch?.queue && method === "POST") {
|
|
204
|
+
const body = JSON.parse(await readBody(req));
|
|
205
|
+
const job = await db.queue(claimMatch.queue).claim({
|
|
206
|
+
leaseMs: body.leaseMs,
|
|
207
|
+
});
|
|
208
|
+
if (!job) {
|
|
209
|
+
sendData(res, null);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
sendData(res, job);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// POST /v1/queues/:queue/ack
|
|
216
|
+
const ackMatch = matchRoute(pathname, "/v1/queues/:queue/ack");
|
|
217
|
+
if (ackMatch?.queue && method === "POST") {
|
|
218
|
+
const body = JSON.parse(await readBody(req));
|
|
219
|
+
const result = await db.queue(ackMatch.queue).ack(body.jobId);
|
|
220
|
+
if (!result.ok) {
|
|
221
|
+
sendError(res, 400, result.reason, `Ack failed: ${result.reason}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
sendData(res, result.job);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// POST /v1/queues/:queue/nack
|
|
228
|
+
const nackMatch = matchRoute(pathname, "/v1/queues/:queue/nack");
|
|
229
|
+
if (nackMatch?.queue && method === "POST") {
|
|
230
|
+
const body = JSON.parse(await readBody(req));
|
|
231
|
+
const result = await db.queue(nackMatch.queue).nack(body.jobId, {
|
|
232
|
+
delayMs: body.delayMs,
|
|
233
|
+
error: body.error,
|
|
234
|
+
});
|
|
235
|
+
if (!result.ok) {
|
|
236
|
+
sendError(res, 400, result.reason, `Nack failed: ${result.reason}`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
sendData(res, result.job);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// GET /v1/queues/:queue/jobs
|
|
243
|
+
const jobsMatch = matchRoute(pathname, "/v1/queues/:queue/jobs");
|
|
244
|
+
if (jobsMatch?.queue && method === "GET") {
|
|
245
|
+
const jobs = await db.queue(jobsMatch.queue).list();
|
|
246
|
+
sendDataList(res, jobs);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// GET /v1/queues/:queue/dead
|
|
250
|
+
const deadMatch = matchRoute(pathname, "/v1/queues/:queue/dead");
|
|
251
|
+
if (deadMatch?.queue && method === "GET") {
|
|
252
|
+
const jobs = await db.queue(deadMatch.queue).dead();
|
|
253
|
+
sendDataList(res, jobs);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// ─── Links ───────────────────────────────────────────────────
|
|
257
|
+
// POST /v1/links
|
|
258
|
+
if (pathname === "/v1/links" && method === "POST") {
|
|
259
|
+
const body = JSON.parse(await readBody(req));
|
|
260
|
+
if (!body.fromRef || !body.linkType || !body.toRef) {
|
|
261
|
+
sendError(res, 400, "bad_request", "Fields 'fromRef', 'linkType', 'toRef' are required");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const link = await db.links.create(body.fromRef, body.linkType, body.toRef, body.weight, body.metadataJson);
|
|
265
|
+
sendData(res, link);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// GET /v1/links?id=...
|
|
269
|
+
if (pathname === "/v1/links" && method === "GET") {
|
|
270
|
+
const id = url.searchParams.get("id");
|
|
271
|
+
if (id) {
|
|
272
|
+
const link = await db.links.get(id);
|
|
273
|
+
if (!link) {
|
|
274
|
+
sendError(res, 404, "not_found", `Link '${id}' not found`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
sendData(res, link);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// Neighbors query
|
|
281
|
+
const reference = url.searchParams.get("reference");
|
|
282
|
+
if (reference) {
|
|
283
|
+
const direction = url.searchParams.get("direction") ?? "Both";
|
|
284
|
+
const linkType = url.searchParams.get("linkType") ?? undefined;
|
|
285
|
+
const limit = parseIntParam(url.searchParams.get("limit"));
|
|
286
|
+
const neighbors = await db.links.neighbors(reference, direction, { linkType, limit });
|
|
287
|
+
sendDataList(res, neighbors);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
sendError(res, 400, "bad_request", "Query parameter 'id' or 'reference' is required");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// DELETE /v1/links/:id
|
|
294
|
+
const linkDeleteMatch = matchRoute(pathname, "/v1/links/:id");
|
|
295
|
+
if (linkDeleteMatch?.id && method === "DELETE") {
|
|
296
|
+
const deleted = await db.links.delete(linkDeleteMatch.id);
|
|
297
|
+
sendData(res, { deleted });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// GET /v1/links/:id
|
|
301
|
+
if (linkDeleteMatch?.id && method === "GET") {
|
|
302
|
+
const link = await db.links.get(linkDeleteMatch.id);
|
|
303
|
+
if (!link) {
|
|
304
|
+
sendError(res, 404, "not_found", `Link '${linkDeleteMatch.id}' not found`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
sendData(res, link);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// ─── 404 ─────────────────────────────────────────────────────
|
|
311
|
+
sendError(res, 404, "not_found", `No route for ${method} ${pathname}`);
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
315
|
+
sendError(res, 500, "internal_error", message);
|
|
316
|
+
}
|
|
317
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thingd/cli",
|
|
3
|
+
"version": "0.31.0",
|
|
4
|
+
"description": "CLI, Interactive TUI Dashboard, and MCP server for thingd — a fast object-first data engine for applications and AI agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "Sayan Mohsin",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"thingd",
|
|
10
|
+
"cli",
|
|
11
|
+
"mcp",
|
|
12
|
+
"mcp-server",
|
|
13
|
+
"ai-agents",
|
|
14
|
+
"dashboard",
|
|
15
|
+
"tui",
|
|
16
|
+
"data-engine",
|
|
17
|
+
"queue",
|
|
18
|
+
"local-first"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"provenance": true
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"thingd": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/sayanmohsin/thingd.git"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
45
|
+
"cli-table3": "^0.6.5",
|
|
46
|
+
"picocolors": "^1.1.1",
|
|
47
|
+
"zod": "^4.4.3",
|
|
48
|
+
"@thingd/node": "0.31.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=24.0.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "pnpm --filter frontend build && tsc -p tsconfig.json && node -e \"fs.mkdirSync('dist/dashboard', { recursive: true }); fs.cpSync('src/dashboard/public', 'dist/dashboard/public', { recursive: true })\"",
|
|
55
|
+
"test": "pnpm build && node --test test/*.test.mjs"
|
|
56
|
+
}
|
|
57
|
+
}
|