@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/index.js ADDED
@@ -0,0 +1,1015 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { setTimeout as sleep } from "node:timers/promises";
5
+ import { pathToFileURL } from "node:url";
6
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
8
+ import { ThingD, } from "@thingd/node";
9
+ import pc from "picocolors";
10
+ import { runInteractiveCli } from "./interactive.js";
11
+ import { logoLine } from "./logo.js";
12
+ import { runMcp } from "./mcp.js";
13
+ import { defaultThingdDbPath, ensureThingdDir } from "./paths.js";
14
+ // ── Opencode-style log output ──────────────────────────────────────
15
+ function writeLog(target, data, header) {
16
+ const W = 60;
17
+ if (header) {
18
+ target.write(` ${pc.bold(header)}\n`);
19
+ target.write(` ${pc.dim("─".repeat(W))}\n`);
20
+ }
21
+ for (const { label, value } of data) {
22
+ target.write(` ${pc.cyan("●")} ${pc.dim(label.padEnd(14))} ${value}\n`);
23
+ }
24
+ target.write("\n");
25
+ }
26
+ function writeLogBullets(target, items, header) {
27
+ const W = 60;
28
+ if (header) {
29
+ target.write(` ${pc.bold(header)}\n`);
30
+ target.write(` ${pc.dim("─".repeat(W))}\n`);
31
+ }
32
+ for (const item of items) {
33
+ const icon = item.icon ?? pc.cyan("○");
34
+ const indent = " ".repeat(item.indent ?? 1);
35
+ target.write(`${indent}${icon} ${item.text}\n`);
36
+ }
37
+ target.write("\n");
38
+ }
39
+ const HELP_TEXT = `${logoLine()}Admin and operator CLI for thingd.
40
+
41
+ Usage:
42
+ thingd status [--url <url>]
43
+ thingd tools --url <url>
44
+ thingd install [--raw] [--claude] [--cursor] [--antigravity]
45
+ thingd doctor
46
+ thingd mcp [--path <path>] [--driver <driver>]
47
+ thingd mcp-http [--path <path>] [--driver <driver>] [--host <host>] [--port <port>] [--auth-token <tok>] [--allow-unauthenticated]
48
+ thingd search <query> [--collection <name>] [--limit <n>] [--filter <json>]
49
+ thingd objects list <collection> [--limit <n>] [--offset <n>] [--sort-by <field>] [--sort-dir <asc|desc>] [--filter <json>]
50
+ thingd objects get <collection> <id>
51
+ thingd objects put <collection> <id> --text <text>
52
+ thingd objects put <collection> <id> --data '{"field":"value"}'
53
+ thingd objects put-batch <collection> --file <path>
54
+ thingd objects delete <collection> <id>
55
+ thingd objects delete-batch <collection> <id1> [id2] ...
56
+ thingd events streams
57
+ thingd events append <stream> <type> [--text <text>] [--data '{"field":"value"}']
58
+ thingd events list [stream] [--limit <n>]
59
+ thingd collections list
60
+ thingd streams list
61
+ thingd queues list-all
62
+ thingd queues stats <queue>
63
+ thingd queues push <queue> --payload '{"key":"value"}'
64
+ thingd queues claim <queue> [--lease-ms <ms>]
65
+ thingd queues ack <queue> <jobId>
66
+ thingd queues nack <queue> <jobId> [--error <message>] [--delay-ms <ms>]
67
+ thingd queues list <queue> [--limit <n>]
68
+ thingd queues dead <queue> [--limit <n>]
69
+ thingd bench rust --smoke
70
+ thingd bench rust --count <n>
71
+ thingd metrics
72
+ thingd dashboard [--port <port>] [--path <path>] [--driver <driver>]
73
+ thingd export --collection <name> --out <path> [--redact [keys]]
74
+ thingd export --events [--stream <name>] --out <path> [--redact [keys]]
75
+ thingd import --collection <name> --in <path>
76
+ thingd snapshot create --out <path>
77
+ thingd snapshot restore --in <path>
78
+
79
+ Options:
80
+ --url <url> remote thingd URL. Defaults to THINGD_URL
81
+ --auth-token <tok> remote bearer token. Defaults to THINGD_AUTH_TOKEN
82
+ --path <path> local database path. Defaults to THINGD_PATH or ~/.thingd/data.db
83
+ --driver <driver> memory, native, or cloud
84
+ --pretty opencode-style log output (human-readable)
85
+ --limit <n> result limit for search and list commands
86
+ --filter <json> metadata key-value filter (e.g. '{"status":"active"}')
87
+ -h, --help show help
88
+ `;
89
+ const BOOLEAN_FLAGS = new Set([
90
+ "h",
91
+ "help",
92
+ "json",
93
+ "pretty",
94
+ "allow-unauthenticated",
95
+ "raw",
96
+ "claude",
97
+ "cursor",
98
+ "antigravity",
99
+ "smoke",
100
+ "events",
101
+ ]);
102
+ export async function runCli(args = process.argv.slice(2), options = {}) {
103
+ // Auto-detect native binary for global/local dev execution
104
+ if (!process.env.THINGD_NATIVE_PATH) {
105
+ try {
106
+ const { existsSync } = await import("node:fs");
107
+ const { join } = await import("node:path");
108
+ const cliDir = join(resolveCliPath(), "..", "..");
109
+ const platform = process.platform;
110
+ const arch = process.arch;
111
+ const candidates = [
112
+ // installed via pnpm/npm as transitive dependency of thingd
113
+ join(cliDir, "node_modules", "thingd-native", "dist", "thingd_native.node"),
114
+ join(cliDir, "node_modules", "thingd-native", "prebuilds", `${platform}-${arch}`, "thingd_native.node"),
115
+ // workspace sibling (local dev)
116
+ join(cliDir, "..", "thingd-native", "dist", "thingd_native.node"),
117
+ join(cliDir, "..", "thingd-native", "prebuilds", `${platform}-${arch}`, "thingd_native.node"),
118
+ ];
119
+ for (const candidate of candidates) {
120
+ if (existsSync(candidate)) {
121
+ process.env.THINGD_NATIVE_PATH = candidate;
122
+ break;
123
+ }
124
+ }
125
+ }
126
+ catch {
127
+ // Ignore detection errors
128
+ }
129
+ }
130
+ const parsed = parseArgs(args);
131
+ const context = {
132
+ parsed,
133
+ env: options.env ?? process.env,
134
+ stdout: options.stdout ?? process.stdout,
135
+ stderr: options.stderr ?? process.stderr,
136
+ pretty: hasFlag(parsed, "pretty"),
137
+ };
138
+ try {
139
+ if (hasFlag(parsed, "help") || hasFlag(parsed, "h")) {
140
+ writeText(context.stdout, HELP_TEXT);
141
+ return 0;
142
+ }
143
+ if (parsed.tokens.length === 0) {
144
+ process.stdout.write(logoLine());
145
+ await sleep(300);
146
+ await runInteractiveCli();
147
+ return 0;
148
+ }
149
+ await runCommand(context);
150
+ return 0;
151
+ }
152
+ catch (error) {
153
+ writeJson(context.stderr, {
154
+ error: error instanceof Error ? error.message : String(error),
155
+ }, context.pretty);
156
+ return 1;
157
+ }
158
+ }
159
+ async function runCommand(context) {
160
+ const command = requiredToken(context.parsed, 0, "command");
161
+ if (command === "status") {
162
+ await runStatus(context);
163
+ return;
164
+ }
165
+ if (command === "tools") {
166
+ await runTools(context);
167
+ return;
168
+ }
169
+ if (command === "search") {
170
+ await runSearch(context);
171
+ return;
172
+ }
173
+ if (command === "mcp") {
174
+ await runMcp(context);
175
+ return;
176
+ }
177
+ if (command === "mcp-http") {
178
+ const { runMcpHttp } = await import("./mcp-http.js");
179
+ await runMcpHttp(context);
180
+ return;
181
+ }
182
+ if (command === "install") {
183
+ const { runInstall } = await import("./install.js");
184
+ await runInstall(context);
185
+ return;
186
+ }
187
+ if (command === "doctor") {
188
+ const { runDoctor } = await import("./doctor.js");
189
+ await runDoctor(context);
190
+ return;
191
+ }
192
+ if (command === "bench") {
193
+ await runBench(context);
194
+ return;
195
+ }
196
+ if (command === "objects") {
197
+ await runObjects(context);
198
+ return;
199
+ }
200
+ if (command === "events") {
201
+ await runEvents(context);
202
+ return;
203
+ }
204
+ if (command === "collections") {
205
+ await runCollections(context);
206
+ return;
207
+ }
208
+ if (command === "streams") {
209
+ await runStreams(context);
210
+ return;
211
+ }
212
+ if (command === "queues") {
213
+ await runQueues(context);
214
+ return;
215
+ }
216
+ if (command === "links") {
217
+ await runLinks(context);
218
+ return;
219
+ }
220
+ if (command === "metrics") {
221
+ await runMetrics(context);
222
+ return;
223
+ }
224
+ if (command === "dashboard") {
225
+ await runDashboard(context);
226
+ return;
227
+ }
228
+ if (command === "export") {
229
+ const { runExport } = await import("./data-movement.js");
230
+ await runExport(context);
231
+ return;
232
+ }
233
+ if (command === "import") {
234
+ const { runImport } = await import("./data-movement.js");
235
+ await runImport(context);
236
+ return;
237
+ }
238
+ if (command === "snapshot") {
239
+ const { runSnapshot } = await import("./data-movement.js");
240
+ await runSnapshot(context);
241
+ return;
242
+ }
243
+ throw new Error(`Unknown command: ${command}`);
244
+ }
245
+ async function runBench(context) {
246
+ const target = requiredToken(context.parsed, 1, "benchmark target (rust)");
247
+ if (target !== "rust") {
248
+ throw new Error(`Unsupported benchmark target: ${target}`);
249
+ }
250
+ const isSmoke = hasFlag(context.parsed, "smoke");
251
+ const countStr = stringFlag(context.parsed, "count");
252
+ const count = countStr ? Number.parseInt(countStr, 10) : isSmoke ? 100 : undefined;
253
+ if (count === undefined) {
254
+ throw new Error("bench rust requires --smoke or --count <n>");
255
+ }
256
+ if (Number.isNaN(count) || count <= 0) {
257
+ throw new Error("--count must be a positive integer");
258
+ }
259
+ try {
260
+ const { execSync } = await import("node:child_process");
261
+ try {
262
+ execSync("cargo --version", { stdio: "ignore" });
263
+ }
264
+ catch {
265
+ throw new Error("Rust toolchain (cargo) is not installed or not in the PATH. Cannot run Rust benchmarks.");
266
+ }
267
+ context.stderr.write(`\n${pc.bold("Running Rust storage benchmark")} (Count: ${pc.cyan(count)})...\n\n`);
268
+ const { spawn } = await import("node:child_process");
269
+ const child = spawn("cargo", [
270
+ "run",
271
+ "--release",
272
+ "-p",
273
+ "thingd-core",
274
+ "--example",
275
+ "storage_bench",
276
+ "--features",
277
+ "sqlite",
278
+ "--",
279
+ String(count),
280
+ ], {
281
+ stdio: "inherit",
282
+ cwd: resolve(resolveCliPath(), "../../../.."),
283
+ });
284
+ return new Promise((resolvePromise, rejectPromise) => {
285
+ child.on("close", (code) => {
286
+ if (code === 0) {
287
+ resolvePromise();
288
+ }
289
+ else {
290
+ rejectPromise(new Error(`Benchmark failed with exit code: ${code}`));
291
+ }
292
+ });
293
+ child.on("error", (error) => {
294
+ rejectPromise(error);
295
+ });
296
+ });
297
+ }
298
+ catch (err) {
299
+ throw new Error(`Failed to run benchmark: ${err instanceof Error ? err.message : String(err)}`);
300
+ }
301
+ }
302
+ async function runStatus(context) {
303
+ const connection = resolveConnection(context);
304
+ if (!connection.cloud) {
305
+ if (context.pretty) {
306
+ writeLog(context.stdout, [
307
+ { label: "Driver", value: pc.cyan(connection.driver ?? "memory") },
308
+ { label: "Path", value: pc.dim(connection.path) },
309
+ ], "thingd status");
310
+ return;
311
+ }
312
+ writeJson(context.stdout, {
313
+ mode: "local",
314
+ driver: connection.driver ?? "memory",
315
+ path: connection.path,
316
+ }, context.pretty);
317
+ return;
318
+ }
319
+ const baseUrl = resolveCloudBaseUrl(connection.path);
320
+ const [health, cluster] = await Promise.all([
321
+ fetchJson(new URL("/healthz", baseUrl), connection.authToken),
322
+ fetchJson(new URL("/cluster/status", baseUrl), connection.authToken),
323
+ ]);
324
+ const castCluster = cluster;
325
+ const replication = castCluster?.replication;
326
+ const lastReplicatedSequence = replication && typeof replication === "object" && "lastReplicatedSequence" in replication
327
+ ? replication.lastReplicatedSequence
328
+ : undefined;
329
+ const replicationLag = replication && typeof replication === "object" && "lag" in replication
330
+ ? replication.lag
331
+ : undefined;
332
+ if (context.pretty) {
333
+ const items = [
334
+ { label: "Mode", value: pc.cyan("cloud") },
335
+ { label: "URL", value: pc.dim(resolveCloudMcpUrl(connection.path)) },
336
+ ];
337
+ if (lastReplicatedSequence !== undefined) {
338
+ items.push({ label: "Last Seq", value: String(lastReplicatedSequence) });
339
+ }
340
+ if (replicationLag !== undefined) {
341
+ items.push({ label: "Repl Lag", value: `${replicationLag}ms` });
342
+ }
343
+ writeLog(context.stdout, items, "thingd status");
344
+ return;
345
+ }
346
+ writeJson(context.stdout, {
347
+ mode: "cloud",
348
+ url: resolveCloudMcpUrl(connection.path),
349
+ health,
350
+ cluster,
351
+ leaderUrl: castCluster?.leaderUrl,
352
+ lastReplicatedSequence,
353
+ replicationLag,
354
+ }, context.pretty);
355
+ }
356
+ async function runTools(context) {
357
+ const connection = resolveConnection(context);
358
+ if (!connection.cloud) {
359
+ throw new Error("tools requires --url or THINGD_URL because tools are exposed by the MCP runtime");
360
+ }
361
+ const client = new Client({
362
+ name: "thingd-cli",
363
+ version: "0.1.0",
364
+ });
365
+ const transport = new StreamableHTTPClientTransport(new URL(resolveCloudMcpUrl(connection.path)), {
366
+ requestInit: connection.authToken
367
+ ? {
368
+ headers: {
369
+ Authorization: `Bearer ${connection.authToken}`,
370
+ },
371
+ }
372
+ : undefined,
373
+ });
374
+ try {
375
+ await client.connect(transport);
376
+ const result = await client.listTools();
377
+ writeJson(context.stdout, {
378
+ tools: result.tools.map((tool) => ({
379
+ name: tool.name,
380
+ description: tool.description,
381
+ })),
382
+ }, context.pretty);
383
+ }
384
+ finally {
385
+ await client.close();
386
+ }
387
+ }
388
+ async function runSearch(context) {
389
+ const query = context.parsed.tokens.slice(1).join(" ").trim();
390
+ if (!query) {
391
+ throw new Error("search requires a query");
392
+ }
393
+ await withDb(context, async (db) => {
394
+ const filterStr = stringFlag(context.parsed, "filter");
395
+ const options = {
396
+ collections: stringFlags(context.parsed, "collection"),
397
+ limit: optionalInt(context.parsed, "limit"),
398
+ filter: filterStr ? JSON.parse(filterStr) : undefined,
399
+ };
400
+ const results = await db.search(query, compactOptions(options));
401
+ if (context.pretty) {
402
+ const bullets = results.map((r) => {
403
+ const res = r;
404
+ const col = res.kind === "object" ? (res.collection ?? "") : (res.stream ?? "");
405
+ return `${pc.green(res.id)} ${pc.dim(col)}`;
406
+ });
407
+ writeLogBullets(context.stdout, bullets.map((t) => ({ text: t })), `thingd search ${query}`);
408
+ return;
409
+ }
410
+ writeJson(context.stdout, results, context.pretty);
411
+ });
412
+ }
413
+ async function runObjects(context) {
414
+ const action = requiredToken(context.parsed, 1, "objects action");
415
+ const collection = requiredToken(context.parsed, 2, "collection");
416
+ await withDb(context, async (db) => {
417
+ if (action === "list") {
418
+ const sortByField = stringFlag(context.parsed, "sort-by");
419
+ const sortByDir = stringFlag(context.parsed, "sort-dir");
420
+ const sortBy = sortByField
421
+ ? { field: sortByField, direction: sortByDir ?? "asc" }
422
+ : undefined;
423
+ const filterStr = stringFlag(context.parsed, "filter");
424
+ const options = {
425
+ filter: filterStr ? JSON.parse(filterStr) : undefined,
426
+ sortBy,
427
+ limit: optionalInt(context.parsed, "limit"),
428
+ offset: optionalInt(context.parsed, "offset"),
429
+ };
430
+ const objects = await db.listObjects(collection, compactOptions(options));
431
+ if (context.pretty) {
432
+ const bullets = objects.map((obj) => {
433
+ const { id, version, createdAt } = obj;
434
+ const meta = `${pc.dim(`v${version}`)} ${createdAt ? pc.dim(new Date(createdAt).toLocaleString()) : ""}`;
435
+ return `${pc.green(id)} ${meta}`;
436
+ });
437
+ writeLogBullets(context.stdout, bullets.map((t) => ({ text: t })), `thingd objects list ${collection}`);
438
+ }
439
+ else {
440
+ writeJson(context.stdout, objects, false);
441
+ }
442
+ return;
443
+ }
444
+ if (action === "put-batch") {
445
+ const filePath = stringFlag(context.parsed, "file");
446
+ if (!filePath) {
447
+ throw new Error("put-batch requires --file <path>");
448
+ }
449
+ const { existsSync, readFileSync } = await import("node:fs");
450
+ const { resolve } = await import("node:path");
451
+ const resolved = resolve(filePath);
452
+ if (!existsSync(resolved)) {
453
+ throw new Error(`File not found: ${resolved}`);
454
+ }
455
+ const raw = readFileSync(resolved, "utf8");
456
+ const parsed = JSON.parse(raw);
457
+ const objects = Array.isArray(parsed) ? parsed : parsed.objects;
458
+ if (!Array.isArray(objects)) {
459
+ throw new Error("File must contain a JSON array or { objects: [...] }");
460
+ }
461
+ const result = await db.putBatch(collection, objects);
462
+ if (context.pretty) {
463
+ writeLog(context.stdout, [{ label: "Created", value: `${result.length} object(s)` }], `thingd objects put-batch ${collection}`);
464
+ }
465
+ else {
466
+ writeJson(context.stdout, result, false);
467
+ }
468
+ return;
469
+ }
470
+ if (action === "delete-batch") {
471
+ const ids = context.parsed.tokens.slice(3);
472
+ if (ids.length === 0) {
473
+ throw new Error("delete-batch requires at least one object id");
474
+ }
475
+ const count = await db.deleteBatch(collection, ids);
476
+ if (context.pretty) {
477
+ writeLog(context.stdout, [{ label: "Deleted", value: `${count} object(s)` }], `thingd objects delete-batch ${collection}`);
478
+ }
479
+ else {
480
+ writeJson(context.stdout, { deleted: count }, false);
481
+ }
482
+ return;
483
+ }
484
+ const id = requiredToken(context.parsed, 3, "object id");
485
+ if (action === "get") {
486
+ writeJson(context.stdout, await db.get(collection, id), context.pretty);
487
+ return;
488
+ }
489
+ if (action === "put") {
490
+ const object = buildMemoryObject(context.parsed, id);
491
+ writeJson(context.stdout, await db.put(collection, object), context.pretty);
492
+ return;
493
+ }
494
+ if (action === "delete") {
495
+ writeJson(context.stdout, await db.delete(collection, id), context.pretty);
496
+ return;
497
+ }
498
+ throw new Error(`Unknown objects action: ${action}`);
499
+ });
500
+ }
501
+ async function runEvents(context) {
502
+ const action = requiredToken(context.parsed, 1, "events action");
503
+ await withDb(context, async (db) => {
504
+ if (action === "streams") {
505
+ const streams = await db.listStreams();
506
+ if (context.pretty) {
507
+ writeLogBullets(context.stdout, streams.map((s) => ({ text: pc.green(s), icon: pc.green("●") })), "thingd events streams");
508
+ }
509
+ else {
510
+ writeJson(context.stdout, streams, false);
511
+ }
512
+ return;
513
+ }
514
+ if (action === "list") {
515
+ const stream = optionalToken(context.parsed, 2);
516
+ const events = limitItems(await db.events.list(stream), optionalInt(context.parsed, "limit"));
517
+ if (context.pretty) {
518
+ const bullets = events.map((ev) => {
519
+ const { id, type, createdAt, ...data } = ev;
520
+ const ts = createdAt ? pc.dim(new Date(createdAt).toLocaleString()) : "";
521
+ const dataStr = Object.keys(data).length > 0 ? ` ${pc.dim(JSON.stringify(data))}` : "";
522
+ return `${pc.green(id)} ${pc.magenta(type)} ${ts}${dataStr}`;
523
+ });
524
+ writeLogBullets(context.stdout, bullets.map((t) => ({ text: t, icon: pc.green("●") })), stream ? `thingd events list ${stream}` : "thingd events list");
525
+ }
526
+ else {
527
+ writeJson(context.stdout, events, false);
528
+ }
529
+ return;
530
+ }
531
+ if (action === "append") {
532
+ const stream = requiredToken(context.parsed, 2, "stream");
533
+ const type = requiredToken(context.parsed, 3, "event type");
534
+ const event = buildMemoryEvent(context.parsed, type);
535
+ writeJson(context.stdout, await db.events.append(stream, event), context.pretty);
536
+ return;
537
+ }
538
+ throw new Error(`Unknown events action: ${action}`);
539
+ });
540
+ }
541
+ async function runCollections(context) {
542
+ const action = requiredToken(context.parsed, 1, "collections action");
543
+ await withDb(context, async (db) => {
544
+ if (action === "list") {
545
+ const collections = await db.listCollections();
546
+ if (context.pretty) {
547
+ writeLogBullets(context.stdout, collections.map((c) => ({ text: pc.cyan(c) })), "thingd collections list");
548
+ }
549
+ else {
550
+ writeJson(context.stdout, collections, false);
551
+ }
552
+ return;
553
+ }
554
+ throw new Error(`Unknown collections action: ${action}`);
555
+ });
556
+ }
557
+ async function runStreams(context) {
558
+ const action = requiredToken(context.parsed, 1, "streams action");
559
+ await withDb(context, async (db) => {
560
+ if (action === "list") {
561
+ const streams = await db.listStreams();
562
+ if (context.pretty) {
563
+ writeLogBullets(context.stdout, streams.map((s) => ({ text: pc.green(s), icon: pc.green("●") })), "thingd streams list");
564
+ }
565
+ else {
566
+ writeJson(context.stdout, streams, false);
567
+ }
568
+ return;
569
+ }
570
+ throw new Error(`Unknown streams action: ${action}`);
571
+ });
572
+ }
573
+ async function runMetrics(context) {
574
+ await withDb(context, async (db) => {
575
+ const [objects, events, activeJobs, deadJobs] = await Promise.all([
576
+ db.countObjects(),
577
+ db.countEvents(),
578
+ db.countActiveJobs(),
579
+ db.countDeadJobs(),
580
+ ]);
581
+ if (context.pretty) {
582
+ writeLog(context.stdout, [
583
+ { label: "Objects", value: pc.cyan(String(objects)) },
584
+ { label: "Events", value: pc.green(String(events)) },
585
+ { label: "Active Jobs", value: pc.yellow(String(activeJobs)) },
586
+ { label: "Dead Jobs", value: pc.red(String(deadJobs)) },
587
+ ], "thingd metrics");
588
+ return;
589
+ }
590
+ writeJson(context.stdout, {
591
+ objects,
592
+ events,
593
+ activeJobs,
594
+ deadJobs,
595
+ }, context.pretty);
596
+ });
597
+ }
598
+ async function runQueues(context) {
599
+ const action = requiredToken(context.parsed, 1, "queues action");
600
+ await withDb(context, async (db) => {
601
+ if (action === "list-all") {
602
+ const queues = await db.listQueues();
603
+ if (context.pretty) {
604
+ writeLogBullets(context.stdout, queues.map((q) => ({ text: pc.magenta(q), icon: pc.magenta("◇") })), "thingd queues list-all");
605
+ }
606
+ else {
607
+ writeJson(context.stdout, queues, false);
608
+ }
609
+ return;
610
+ }
611
+ const queueName = requiredToken(context.parsed, 2, "queue");
612
+ const queue = db.queue(queueName);
613
+ if (action === "stats") {
614
+ const [activeJobs, deadJobs] = await Promise.all([queue.list(), queue.dead()]);
615
+ const totalActive = activeJobs.length;
616
+ const totalDead = deadJobs.length;
617
+ const leasedJobs = activeJobs.filter((job) => job.status === "leased");
618
+ const readyJobs = activeJobs.filter((job) => job.status === "ready");
619
+ if (context.pretty) {
620
+ writeLog(context.stdout, [
621
+ { label: "Queue", value: pc.magenta(queueName) },
622
+ { label: "Ready", value: pc.cyan(String(readyJobs.length)) },
623
+ { label: "Leased", value: pc.yellow(String(leasedJobs.length)) },
624
+ { label: "Dead", value: pc.red(String(totalDead)) },
625
+ { label: "Total Active", value: String(totalActive) },
626
+ ], "thingd queues stats");
627
+ return;
628
+ }
629
+ writeJson(context.stdout, {
630
+ queue: queueName,
631
+ totalActive,
632
+ ready: readyJobs.length,
633
+ leased: leasedJobs.length,
634
+ dead: totalDead,
635
+ }, false);
636
+ return;
637
+ }
638
+ if (action === "push") {
639
+ const payload = parseJsonRecord(requiredFlag(context.parsed, "payload"));
640
+ const options = {
641
+ idempotencyKey: stringFlag(context.parsed, "idempotency-key"),
642
+ maxAttempts: optionalInt(context.parsed, "max-attempts"),
643
+ delayMs: optionalInt(context.parsed, "delay-ms"),
644
+ };
645
+ writeJson(context.stdout, await queue.push(payload, compactOptions(options)), context.pretty);
646
+ return;
647
+ }
648
+ if (action === "claim") {
649
+ const options = {
650
+ leaseMs: optionalInt(context.parsed, "lease-ms"),
651
+ };
652
+ writeJson(context.stdout, await queue.claim(compactOptions(options)), context.pretty);
653
+ return;
654
+ }
655
+ if (action === "ack") {
656
+ writeJson(context.stdout, await queue.ack(requiredToken(context.parsed, 3, "job id")), context.pretty);
657
+ return;
658
+ }
659
+ if (action === "nack") {
660
+ const options = {
661
+ delayMs: optionalInt(context.parsed, "delay-ms"),
662
+ error: stringFlag(context.parsed, "error"),
663
+ };
664
+ writeJson(context.stdout, await queue.nack(requiredToken(context.parsed, 3, "job id"), compactOptions(options)), context.pretty);
665
+ return;
666
+ }
667
+ if (action === "list") {
668
+ const jobs = limitItems(await queue.list(), optionalInt(context.parsed, "limit"));
669
+ if (context.pretty) {
670
+ const bullets = jobs.map((job) => {
671
+ const statusColor = job.status === "leased" ? pc.yellow : pc.cyan;
672
+ return `${pc.dim(job.id)} ${statusColor(job.status)} ${pc.dim(`${job.attempts}/${job.maxAttempts}`)}`;
673
+ });
674
+ writeLogBullets(context.stdout, bullets.map((t) => ({ text: t, icon: pc.cyan("●") })), `thingd queues list ${queueName}`);
675
+ }
676
+ else {
677
+ writeJson(context.stdout, jobs, false);
678
+ }
679
+ return;
680
+ }
681
+ if (action === "dead") {
682
+ const jobs = limitItems(await queue.dead(), optionalInt(context.parsed, "limit"));
683
+ if (context.pretty) {
684
+ const bullets = jobs.map((job) => {
685
+ const err = job.lastError ? ` ${pc.dim(job.lastError)}` : "";
686
+ return `${pc.dim(job.id)} ${pc.dim(`${job.attempts}/${job.maxAttempts}`)}${err}`;
687
+ });
688
+ writeLogBullets(context.stdout, bullets.map((t) => ({ text: t, icon: pc.red("○") })), `thingd queues dead ${queueName}`);
689
+ }
690
+ else {
691
+ writeJson(context.stdout, jobs, false);
692
+ }
693
+ return;
694
+ }
695
+ throw new Error(`Unknown queues action: ${action}`);
696
+ });
697
+ }
698
+ async function runLinks(context) {
699
+ const action = requiredToken(context.parsed, 1, "links action");
700
+ await withDb(context, async (db) => {
701
+ if (action === "count") {
702
+ const count = await db.countLinks();
703
+ if (context.pretty) {
704
+ writeLog(context.stdout, [{ label: "Links", value: String(count) }], "thingd links count");
705
+ }
706
+ else {
707
+ writeJson(context.stdout, { count }, false);
708
+ }
709
+ return;
710
+ }
711
+ if (action === "create") {
712
+ const fromRef = requiredToken(context.parsed, 2, "from reference");
713
+ const linkType = requiredToken(context.parsed, 3, "link type");
714
+ const toRef = requiredToken(context.parsed, 4, "to reference");
715
+ const weight = optionalInt(context.parsed, "weight");
716
+ const metadata = stringFlag(context.parsed, "metadata");
717
+ writeJson(context.stdout, await db.links.create(fromRef, linkType, toRef, weight ?? undefined, metadata), context.pretty);
718
+ return;
719
+ }
720
+ if (action === "get") {
721
+ const id = requiredToken(context.parsed, 2, "link id");
722
+ writeJson(context.stdout, await db.links.get(id), context.pretty);
723
+ return;
724
+ }
725
+ if (action === "delete") {
726
+ const id = requiredToken(context.parsed, 2, "link id");
727
+ writeJson(context.stdout, { deleted: await db.links.delete(id) }, context.pretty);
728
+ return;
729
+ }
730
+ if (action === "neighbors") {
731
+ const reference = requiredToken(context.parsed, 2, "reference");
732
+ const direction = (stringFlag(context.parsed, "direction") ?? "Both");
733
+ const linkType = stringFlag(context.parsed, "type");
734
+ const limit = optionalInt(context.parsed, "limit");
735
+ const neighbors = await db.links.neighbors(reference, direction, {
736
+ linkType: linkType ?? undefined,
737
+ limit: limit ?? undefined,
738
+ });
739
+ if (context.pretty) {
740
+ const bullets = neighbors.map((l) => `${pc.dim(l.id)} ${pc.green(l.fromRef)} ${pc.magenta(l.linkType)} ${pc.green(l.toRef)}`);
741
+ writeLogBullets(context.stdout, bullets.map((t) => ({ text: t })), `thingd links neighbors ${reference}`);
742
+ }
743
+ else {
744
+ writeJson(context.stdout, neighbors, false);
745
+ }
746
+ return;
747
+ }
748
+ throw new Error(`Unknown links action: ${action}`);
749
+ });
750
+ }
751
+ export async function withDb(context, callback) {
752
+ const connection = resolveConnection(context);
753
+ const db = await ThingD.open({
754
+ path: connection.path,
755
+ url: connection.cloud ? connection.path : undefined,
756
+ driver: connection.driver,
757
+ authToken: connection.authToken,
758
+ });
759
+ try {
760
+ await callback(db);
761
+ }
762
+ finally {
763
+ await db.close();
764
+ }
765
+ }
766
+ function buildMemoryObject(parsed, id) {
767
+ const data = stringFlag(parsed, "data");
768
+ const text = stringFlag(parsed, "text");
769
+ if (data === undefined && text === undefined) {
770
+ throw new Error("objects put requires --text or --data");
771
+ }
772
+ return {
773
+ ...(data === undefined ? {} : parseJsonRecord(data)),
774
+ id,
775
+ ...(text === undefined ? {} : { text }),
776
+ };
777
+ }
778
+ function buildMemoryEvent(parsed, type) {
779
+ const data = stringFlag(parsed, "data");
780
+ const text = stringFlag(parsed, "text");
781
+ return {
782
+ ...(data === undefined ? {} : parseJsonRecord(data)),
783
+ type,
784
+ ...(text === undefined ? {} : { text }),
785
+ };
786
+ }
787
+ export function resolveConnection(context) {
788
+ const url = stringFlag(context.parsed, "url") ?? context.env.THINGD_URL;
789
+ const path = url ?? stringFlag(context.parsed, "path") ?? context.env.THINGD_PATH ?? defaultThingdDbPath();
790
+ const cloud = isCloudPath(path);
791
+ let driver = parseDriver(stringFlag(context.parsed, "driver") ?? context.env.THINGD_DRIVER);
792
+ if (!driver) {
793
+ if (cloud) {
794
+ driver = "cloud";
795
+ }
796
+ else if (path !== ":memory:") {
797
+ driver = "native";
798
+ }
799
+ else {
800
+ driver = "memory";
801
+ }
802
+ }
803
+ if (!cloud && path === defaultThingdDbPath()) {
804
+ ensureThingdDir();
805
+ }
806
+ return {
807
+ path,
808
+ driver,
809
+ authToken: stringFlag(context.parsed, "auth-token") ?? context.env.THINGD_AUTH_TOKEN,
810
+ cloud,
811
+ };
812
+ }
813
+ function parseArgs(args) {
814
+ const tokens = [];
815
+ const flags = new Map();
816
+ const booleans = new Set();
817
+ for (let index = 0; index < args.length; index += 1) {
818
+ const arg = args[index];
819
+ if (arg === undefined) {
820
+ continue;
821
+ }
822
+ if (!arg.startsWith("-")) {
823
+ tokens.push(arg);
824
+ continue;
825
+ }
826
+ const name = arg.replace(/^-+/, "");
827
+ const next = args[index + 1];
828
+ if (BOOLEAN_FLAGS.has(name)) {
829
+ booleans.add(name);
830
+ continue;
831
+ }
832
+ if (next === undefined || next.startsWith("-")) {
833
+ booleans.add(name);
834
+ continue;
835
+ }
836
+ addFlagValue(flags, name, next);
837
+ index += 1;
838
+ }
839
+ return {
840
+ tokens,
841
+ flags,
842
+ booleans,
843
+ };
844
+ }
845
+ function addFlagValue(flags, name, value) {
846
+ const values = flags.get(name) ?? [];
847
+ values.push(value);
848
+ flags.set(name, values);
849
+ }
850
+ export function hasFlag(parsed, name) {
851
+ return parsed.booleans.has(name) || parsed.flags.has(name);
852
+ }
853
+ export function stringFlag(parsed, name) {
854
+ return parsed.flags.get(name)?.at(-1);
855
+ }
856
+ export function stringFlags(parsed, name) {
857
+ const values = parsed.flags.get(name);
858
+ return values && values.length > 0 ? values : undefined;
859
+ }
860
+ export function requiredFlag(parsed, name) {
861
+ const value = stringFlag(parsed, name);
862
+ if (value === undefined) {
863
+ throw new Error(`Missing required flag: --${name}`);
864
+ }
865
+ return value;
866
+ }
867
+ export function optionalToken(parsed, index) {
868
+ return parsed.tokens[index];
869
+ }
870
+ export function requiredToken(parsed, index, name) {
871
+ const value = optionalToken(parsed, index);
872
+ if (!value) {
873
+ throw new Error(`Missing required argument: ${name}`);
874
+ }
875
+ return value;
876
+ }
877
+ function optionalInt(parsed, name) {
878
+ const value = stringFlag(parsed, name);
879
+ if (value === undefined) {
880
+ return undefined;
881
+ }
882
+ const parsedValue = Number.parseInt(value, 10);
883
+ if (!Number.isSafeInteger(parsedValue) || parsedValue < 0) {
884
+ throw new Error(`--${name} must be a non-negative integer`);
885
+ }
886
+ return parsedValue;
887
+ }
888
+ function parseDriver(value) {
889
+ if (value === undefined) {
890
+ return undefined;
891
+ }
892
+ if (value === "memory" || value === "native" || value === "cloud") {
893
+ return value;
894
+ }
895
+ throw new Error(`Unsupported driver: ${value}`);
896
+ }
897
+ function parseJsonRecord(value) {
898
+ const parsed = JSON.parse(value);
899
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
900
+ throw new Error("Expected a JSON object");
901
+ }
902
+ return parsed;
903
+ }
904
+ function compactOptions(options) {
905
+ return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined));
906
+ }
907
+ function limitItems(items, limit) {
908
+ return limit === undefined ? items : items.slice(0, limit);
909
+ }
910
+ function isCloudPath(path) {
911
+ return path.startsWith("http://") || path.startsWith("https://") || path.startsWith("thingd://");
912
+ }
913
+ function resolveCloudMcpUrl(value) {
914
+ const url = new URL(normalizeCloudUrl(value));
915
+ if (url.pathname === "" || url.pathname === "/") {
916
+ url.pathname = "/mcp";
917
+ }
918
+ return url.toString();
919
+ }
920
+ function resolveCloudBaseUrl(value) {
921
+ const url = new URL(normalizeCloudUrl(value));
922
+ if (url.pathname === "/mcp") {
923
+ url.pathname = "/";
924
+ }
925
+ return url.toString();
926
+ }
927
+ function normalizeCloudUrl(value) {
928
+ return value.startsWith("thingd://") ? `http://${value.slice("thingd://".length)}` : value;
929
+ }
930
+ async function fetchJson(url, authToken) {
931
+ const response = await fetch(url, {
932
+ headers: authToken
933
+ ? {
934
+ Authorization: `Bearer ${authToken}`,
935
+ }
936
+ : undefined,
937
+ });
938
+ if (!response.ok) {
939
+ throw new Error(`HTTP ${response.status} from ${url.toString()}`);
940
+ }
941
+ return response.json();
942
+ }
943
+ export function writeJson(target, data, pretty) {
944
+ target.write(`${JSON.stringify(data, null, pretty ? 2 : 0)}\n`);
945
+ }
946
+ export function writeText(target, text) {
947
+ target.write(text.endsWith("\n") ? text : `${text}\n`);
948
+ }
949
+ function resolveCliPath() {
950
+ const scriptPath = process.argv[1];
951
+ if (!scriptPath) {
952
+ throw new Error("Could not detect thingd CLI path from process.argv[1].");
953
+ }
954
+ try {
955
+ return realpathSync(resolve(scriptPath));
956
+ }
957
+ catch {
958
+ return resolve(scriptPath);
959
+ }
960
+ }
961
+ async function runDashboard(context) {
962
+ const portStr = stringFlag(context.parsed, "port");
963
+ const port = portStr ? Number.parseInt(portStr, 10) : 8758;
964
+ if (Number.isNaN(port) || port <= 0) {
965
+ throw new Error("--port must be a positive integer");
966
+ }
967
+ const connection = resolveConnection(context);
968
+ context.stderr.write(`\n${pc.bold(pc.blue("thingd Inspector Dashboard"))}\n` +
969
+ `Starting local REST server on ${pc.cyan(`http://localhost:${port}`)}...\n` +
970
+ `Database path: ${pc.green(connection.path)}\n` +
971
+ `Storage engine: ${pc.cyan(connection.driver || "memory")}\n\n`);
972
+ const { startDashboardServer } = await import("./dashboard/server.js");
973
+ const { server: _server, close } = await startDashboardServer(connection, port);
974
+ context.stderr.write(`${pc.green("✔ Dashboard successfully loaded.")}\n` +
975
+ `Opening browser... (Press ${pc.yellow("Ctrl+C")} to stop the server)\n\n`);
976
+ await openBrowser(`http://localhost:${port}`);
977
+ return new Promise((_resolve) => {
978
+ process.on("SIGINT", async () => {
979
+ await close();
980
+ process.exit(0);
981
+ });
982
+ process.on("SIGTERM", async () => {
983
+ await close();
984
+ process.exit(0);
985
+ });
986
+ });
987
+ }
988
+ async function openBrowser(url) {
989
+ const { exec } = await import("node:child_process");
990
+ const startCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
991
+ exec(`${startCommand} ${url}`, () => {
992
+ // Ignore browser spawn errors silently
993
+ });
994
+ }
995
+ let isMain = false;
996
+ if (process.argv[1]) {
997
+ try {
998
+ isMain = import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
999
+ }
1000
+ catch {
1001
+ // Ignore realpath resolution errors.
1002
+ }
1003
+ }
1004
+ if (isMain) {
1005
+ runCli()
1006
+ .then((code) => {
1007
+ if (code !== 0) {
1008
+ process.exit(code);
1009
+ }
1010
+ })
1011
+ .catch((error) => {
1012
+ console.error(error);
1013
+ process.exit(1);
1014
+ });
1015
+ }