@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.
Files changed (76) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +238 -0
  3. package/dist/dashboard/public/assets/index-B-Y-3-0l.js +2 -0
  4. package/dist/dashboard/public/assets/index-B5YhpIl3.js +2 -0
  5. package/dist/dashboard/public/assets/index-BnFclxvN.css +1 -0
  6. package/dist/dashboard/public/assets/index-BtA9rnyI.js +2 -0
  7. package/dist/dashboard/public/assets/index-BzLTzidY.js +2 -0
  8. package/dist/dashboard/public/assets/index-C6PkDB7y.css +1 -0
  9. package/dist/dashboard/public/assets/index-D8yUCdOQ.js +2 -0
  10. package/dist/dashboard/public/assets/index-fQywB2df.js +2 -0
  11. package/dist/dashboard/public/assets/index-kZdrdi3K.css +1 -0
  12. package/dist/dashboard/public/assets/index-kgZrboBN.js +4 -0
  13. package/dist/dashboard/public/favicon.svg +1 -0
  14. package/dist/dashboard/public/icons.svg +24 -0
  15. package/dist/dashboard/public/index.html +16 -0
  16. package/dist/dashboard/server.d.ts +6 -0
  17. package/dist/dashboard/server.d.ts.map +1 -0
  18. package/dist/dashboard/server.js +385 -0
  19. package/dist/data-movement.d.ts +5 -0
  20. package/dist/data-movement.d.ts.map +1 -0
  21. package/dist/data-movement.js +257 -0
  22. package/dist/doctor.d.ts +3 -0
  23. package/dist/doctor.d.ts.map +1 -0
  24. package/dist/doctor.js +109 -0
  25. package/dist/index.d.ts +42 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +1015 -0
  28. package/dist/install.d.ts +3 -0
  29. package/dist/install.d.ts.map +1 -0
  30. package/dist/install.js +311 -0
  31. package/dist/interactive.d.ts +2 -0
  32. package/dist/interactive.d.ts.map +1 -0
  33. package/dist/interactive.js +1592 -0
  34. package/dist/logo.d.ts +3 -0
  35. package/dist/logo.d.ts.map +1 -0
  36. package/dist/logo.js +8 -0
  37. package/dist/mcp/audit.d.ts +27 -0
  38. package/dist/mcp/audit.d.ts.map +1 -0
  39. package/dist/mcp/audit.js +36 -0
  40. package/dist/mcp/cluster.d.ts +68 -0
  41. package/dist/mcp/cluster.d.ts.map +1 -0
  42. package/dist/mcp/cluster.js +303 -0
  43. package/dist/mcp/config.d.ts +14 -0
  44. package/dist/mcp/config.d.ts.map +1 -0
  45. package/dist/mcp/config.js +67 -0
  46. package/dist/mcp/http.d.ts +25 -0
  47. package/dist/mcp/http.d.ts.map +1 -0
  48. package/dist/mcp/http.js +588 -0
  49. package/dist/mcp/index.d.ts +5 -0
  50. package/dist/mcp/index.d.ts.map +1 -0
  51. package/dist/mcp/index.js +3 -0
  52. package/dist/mcp/result.d.ts +3 -0
  53. package/dist/mcp/result.d.ts.map +1 -0
  54. package/dist/mcp/result.js +10 -0
  55. package/dist/mcp/server.d.ts +19 -0
  56. package/dist/mcp/server.d.ts.map +1 -0
  57. package/dist/mcp/server.js +51 -0
  58. package/dist/mcp/tools.d.ts +10 -0
  59. package/dist/mcp/tools.d.ts.map +1 -0
  60. package/dist/mcp/tools.js +568 -0
  61. package/dist/mcp-http.d.ts +3 -0
  62. package/dist/mcp-http.d.ts.map +1 -0
  63. package/dist/mcp-http.js +42 -0
  64. package/dist/mcp.d.ts +3 -0
  65. package/dist/mcp.d.ts.map +1 -0
  66. package/dist/mcp.js +22 -0
  67. package/dist/paths.d.ts +4 -0
  68. package/dist/paths.d.ts.map +1 -0
  69. package/dist/paths.js +14 -0
  70. package/dist/rest/helpers.d.ts +17 -0
  71. package/dist/rest/helpers.d.ts.map +1 -0
  72. package/dist/rest/helpers.js +55 -0
  73. package/dist/rest/server.d.ts +4 -0
  74. package/dist/rest/server.d.ts.map +1 -0
  75. package/dist/rest/server.js +317 -0
  76. package/package.json +57 -0
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { type CliContext } from "./index.js";
2
+ export declare function runMcp(context: CliContext): Promise<void>;
3
+ //# sourceMappingURL=mcp.d.ts.map
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export declare function defaultThingdDir(): string;
2
+ export declare function defaultThingdDbPath(): string;
3
+ export declare function ensureThingdDir(): void;
4
+ //# sourceMappingURL=paths.d.ts.map
@@ -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
+ }