thingd-cli 0.11.0 → 0.12.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/README.md CHANGED
@@ -132,14 +132,22 @@ thingd mcp-http --path ./thingd.db --driver native --port 8757 --auth-token chan
132
132
 
133
133
  ### Command Reference
134
134
 
135
- **Metrics & Discovery**
135
+ **System & Diagnostics**
136
136
  ```bash
137
- thingd metrics # Get total counts (objects, events, activeJobs, deadJobs)
137
+ thingd doctor # Run Node, native binding, and remote connectivity diagnostics
138
+ thingd metrics # Get total database counts (objects, events, activeJobs, deadJobs)
138
139
  thingd status # Check cluster health (requires --url)
139
140
  thingd tools # List available MCP tools (requires --url)
140
- thingd collections list # List all collection names
141
- thingd streams list # List all stream names
142
- thingd queues list-all # List all queue names
141
+ thingd bench rust --smoke # Run Rust SQLite engine smoke benchmarks
142
+ thingd bench rust --count 500 # Run Rust SQLite engine benchmarks with specific run count
143
+ ```
144
+
145
+ **Discovery & Collections**
146
+ ```bash
147
+ thingd collections list # List all active collections
148
+ thingd streams list # List all active stream names (alias for events streams)
149
+ thingd events streams # List all active event streams
150
+ thingd queues list-all # List all active queue names
143
151
  ```
144
152
 
145
153
  **Search**
@@ -149,10 +157,11 @@ thingd search "my query" [--collection <name>] [--limit <n>]
149
157
 
150
158
  **Objects**
151
159
  ```bash
152
- thingd objects put decisions rust-core --text "Use Rust for the core engine."
153
- thingd objects put decisions rust-core --data '{"status":"active"}'
154
- thingd objects get decisions rust-core
155
- thingd objects delete decisions rust-core
160
+ thingd objects list decisions # List all objects inside a collection
161
+ thingd objects put decisions core --text "msg" # Put object with plain text content
162
+ thingd objects put decisions core --data '{"a":1}' # Put object with arbitrary JSON data
163
+ thingd objects get decisions core # Fetch a specific object by ID
164
+ thingd objects delete decisions core # Delete an object by ID
156
165
  ```
157
166
 
158
167
  **Events**
@@ -163,6 +172,7 @@ thingd events list project:thingd
163
172
 
164
173
  **Queues**
165
174
  ```bash
175
+ thingd queues stats embed # View queue statistics (ready, leased, dead jobs)
166
176
  thingd queues push embed --payload '{"object":"docs/readme"}'
167
177
  thingd queues claim embed
168
178
  thingd queues ack embed <jobId>
@@ -172,3 +182,4 @@ thingd queues dead embed
172
182
  ```
173
183
 
174
184
 
185
+
@@ -0,0 +1,3 @@
1
+ import { type CliContext } from "./index.js";
2
+ export declare function runDoctor(context: CliContext): Promise<void>;
3
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,YAAY,CAAC;AAEhE,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAsMlE"}
package/dist/doctor.js ADDED
@@ -0,0 +1,164 @@
1
+ import { existsSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import pc from "picocolors";
6
+ import { resolveConnection } from "./index.js";
7
+ export async function runDoctor(context) {
8
+ context.stderr.write(`\n${pc.bold("thingd doctor")}\n`);
9
+ context.stderr.write(`${pc.dim("Running system diagnostics and connectivity tests...")}\n\n`);
10
+ let healthy = true;
11
+ // 1. Check Node version
12
+ const nodeVersion = process.version;
13
+ const majorVersion = Number.parseInt(nodeVersion.slice(1).split(".")[0] ?? "0", 10);
14
+ if (majorVersion >= 20) {
15
+ context.stderr.write(` ${pc.green("✓")} Node version: ${pc.cyan(nodeVersion)} (OK)\n`);
16
+ }
17
+ else {
18
+ healthy = false;
19
+ context.stderr.write(` ${pc.red("×")} Node version: ${pc.yellow(nodeVersion)} (Requires >= v20.x)\n`);
20
+ }
21
+ // 2. Resolve connection options
22
+ const connection = resolveConnection(context);
23
+ // 3. Native Driver Checks
24
+ if (connection.driver === "native") {
25
+ const customPath = process.env.THINGD_NATIVE_PATH;
26
+ if (customPath) {
27
+ if (existsSync(customPath)) {
28
+ try {
29
+ const require = createRequire(import.meta.url);
30
+ const binding = require(customPath);
31
+ if (binding?.NativeThingStore) {
32
+ context.stderr.write(` ${pc.green("✓")} Native Binding: ${pc.cyan("Loaded via THINGD_NATIVE_PATH")} (${pc.dim(customPath)})\n`);
33
+ }
34
+ else {
35
+ healthy = false;
36
+ context.stderr.write(` ${pc.red("×")} Native Binding: ${pc.yellow("Loaded but missing NativeThingStore export")} (${pc.dim(customPath)})\n`);
37
+ }
38
+ }
39
+ catch (error) {
40
+ healthy = false;
41
+ context.stderr.write(` ${pc.red("×")} Native Binding: ${pc.yellow(`Failed to load: ${error instanceof Error ? error.message : String(error)}`)} (${pc.dim(customPath)})\n`);
42
+ }
43
+ }
44
+ else {
45
+ healthy = false;
46
+ context.stderr.write(` ${pc.red("×")} Native Binding: ${pc.yellow("File does not exist")} at THINGD_NATIVE_PATH="${pc.dim(customPath)}"\n`);
47
+ }
48
+ }
49
+ else {
50
+ // Auto-detect sibling binary
51
+ let detectedPath = null;
52
+ try {
53
+ const scriptPath = process.argv[1];
54
+ if (scriptPath) {
55
+ const cliDir = join(resolve(scriptPath), "..", "..");
56
+ const candidates = [
57
+ join(cliDir, "node_modules", "thingd-native", "dist", "thingd_native.node"),
58
+ join(cliDir, "..", "thingd-native", "dist", "thingd_native.node"),
59
+ join(homedir(), "Space/Programming/personal/thingd/packages/thingd-native/dist/thingd_native.node"),
60
+ join(homedir(), "Space/Programming/personal/thingd-cloud/packages/thingd-native/dist/thingd_native.node"),
61
+ ];
62
+ for (const candidate of candidates) {
63
+ if (existsSync(candidate)) {
64
+ detectedPath = candidate;
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ catch {
71
+ // Ignore
72
+ }
73
+ if (detectedPath) {
74
+ try {
75
+ const require = createRequire(import.meta.url);
76
+ const binding = require(detectedPath);
77
+ if (binding?.NativeThingStore) {
78
+ context.stderr.write(` ${pc.green("✓")} Native Binding: ${pc.cyan("Auto-detected and loaded successfully")} (${pc.dim(detectedPath)})\n`);
79
+ }
80
+ else {
81
+ healthy = false;
82
+ context.stderr.write(` ${pc.red("×")} Native Binding: ${pc.yellow("Auto-detected but missing NativeThingStore export")} (${pc.dim(detectedPath)})\n`);
83
+ }
84
+ }
85
+ catch (error) {
86
+ healthy = false;
87
+ context.stderr.write(` ${pc.red("×")} Native Binding: ${pc.yellow(`Failed to load auto-detected binding: ${error instanceof Error ? error.message : String(error)}`)} (${pc.dim(detectedPath)})\n`);
88
+ }
89
+ }
90
+ else {
91
+ healthy = false;
92
+ context.stderr.write(` ${pc.red("×")} Native Binding: ${pc.yellow('Not found. Run "pnpm --filter thingd-native build" or configure THINGD_NATIVE_PATH.')}\n`);
93
+ }
94
+ }
95
+ }
96
+ else {
97
+ context.stderr.write(` ${pc.dim("○")} Native Binding: Skipped (Using driver: "${connection.driver ?? "memory"}")\n`);
98
+ }
99
+ // 4. Remote Sidecar Reachability & Auth Checks
100
+ if (connection.cloud) {
101
+ const rawUrl = connection.path;
102
+ const isLocal = rawUrl.includes("localhost") || rawUrl.includes("127.0.0.1");
103
+ if (!connection.authToken && !isLocal) {
104
+ context.stderr.write(` ${pc.yellow("⚠")} Auth Token: ${pc.yellow("Missing THINGD_AUTH_TOKEN for remote server (might fail)")}\n`);
105
+ }
106
+ else if (connection.authToken) {
107
+ context.stderr.write(` ${pc.green("✓")} Auth Token: ${pc.cyan("Configured")}\n`);
108
+ }
109
+ else {
110
+ context.stderr.write(` ${pc.green("✓")} Auth Token: ${pc.dim("Not required for local sidecar")}\n`);
111
+ }
112
+ try {
113
+ const normalizeUrl = (val) => val.startsWith("thingd://") ? `http://${val.slice("thingd://".length)}` : val;
114
+ const targetUrl = new URL(normalizeUrl(rawUrl));
115
+ if (targetUrl.pathname === "/mcp" || targetUrl.pathname === "") {
116
+ targetUrl.pathname = "/healthz";
117
+ }
118
+ else {
119
+ targetUrl.pathname = `${targetUrl.pathname.replace(/\/mcp$/, "")}/healthz`;
120
+ }
121
+ context.stderr.write(` ${pc.dim("○")} Connectivity: Checking reachability to ${pc.cyan(targetUrl.toString())}...\n`);
122
+ const controller = new AbortController();
123
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
124
+ const headers = {};
125
+ if (connection.authToken) {
126
+ headers.Authorization = `Bearer ${connection.authToken}`;
127
+ }
128
+ try {
129
+ const response = await fetch(targetUrl, {
130
+ signal: controller.signal,
131
+ headers,
132
+ });
133
+ clearTimeout(timeoutId);
134
+ if (response.ok) {
135
+ context.stderr.write(` ${pc.green("✓")} Connectivity: ${pc.cyan("Connected successfully!")} (${pc.dim(`HTTP ${response.status}`)})\n`);
136
+ }
137
+ else {
138
+ healthy = false;
139
+ context.stderr.write(` ${pc.red("×")} Connectivity: ${pc.yellow(`Server responded with non-2xx status`)} (${pc.dim(`HTTP ${response.status}`)})\n`);
140
+ }
141
+ }
142
+ catch (fetchError) {
143
+ clearTimeout(timeoutId);
144
+ healthy = false;
145
+ context.stderr.write(` ${pc.red("×")} Connectivity: ${pc.yellow(`Failed to connect. Connection refused or timed out.`)} (${pc.dim(fetchError instanceof Error ? fetchError.message : String(fetchError))})\n`);
146
+ }
147
+ }
148
+ catch (urlError) {
149
+ healthy = false;
150
+ context.stderr.write(` ${pc.red("×")} Connectivity: ${pc.yellow(`Invalid URL structure: ${urlError instanceof Error ? urlError.message : String(urlError)}`)}\n`);
151
+ }
152
+ }
153
+ else {
154
+ context.stderr.write(` ${pc.green("✓")} Connectivity: ${pc.cyan("Local SQLite Store")} (${pc.dim(connection.path)})\n`);
155
+ }
156
+ // Final Summary Report
157
+ context.stderr.write("\n");
158
+ if (healthy) {
159
+ context.stderr.write(` ${pc.bold(pc.green("Diagnosis: Everything looks healthy!"))}\n\n`);
160
+ }
161
+ else {
162
+ context.stderr.write(` ${pc.bold(pc.yellow("Diagnosis: Some items require attention (see errors above)."))}\n\n`);
163
+ }
164
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAOA,OAAO,EAOL,MAAM,EACN,KAAK,YAAY,EAClB,MAAM,QAAQ,CAAC;AAKhB,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjD,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7B,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAmDF,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAiEjB;AA+UD,wBAAsB,MAAM,CAC1B,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAcf;AA4BD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,GAAG,iBAAiB,CA2BxE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AASA,OAAO,EAOL,MAAM,EACN,KAAK,YAAY,EAClB,MAAM,QAAQ,CAAC;AAKhB,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjD,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7B,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AA0DF,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAiEjB;AA8jBD,wBAAsB,MAAM,CAC1B,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAcf;AA4BD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,GAAG,iBAAiB,CA2BxE"}
package/dist/index.js CHANGED
@@ -4,6 +4,8 @@ import { resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
6
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
7
+ import Table from "cli-table3";
8
+ import pc from "picocolors";
7
9
  import { ThingD, } from "thingd";
8
10
  import { runInteractiveCli } from "./interactive.js";
9
11
  import { runMcp } from "./mcp.js";
@@ -16,24 +18,30 @@ Usage:
16
18
  thingd status [--url <url>]
17
19
  thingd tools --url <url>
18
20
  thingd install [--raw] [--claude] [--cursor]
21
+ thingd doctor
19
22
  thingd mcp [--path <path>] [--driver <driver>]
20
23
  thingd mcp-http [--path <path>] [--driver <driver>] [--host <host>] [--port <port>] [--auth-token <tok>] [--allow-unauthenticated]
21
24
  thingd search <query> [--collection <name>] [--limit <n>]
25
+ thingd objects list <collection>
22
26
  thingd objects get <collection> <id>
23
27
  thingd objects put <collection> <id> --text <text>
24
28
  thingd objects put <collection> <id> --data '{"field":"value"}'
25
29
  thingd objects delete <collection> <id>
30
+ thingd events streams
26
31
  thingd events append <stream> <type> [--text <text>] [--data '{"field":"value"}']
27
32
  thingd events list [stream] [--limit <n>]
28
33
  thingd collections list
29
34
  thingd streams list
30
35
  thingd queues list-all
36
+ thingd queues stats <queue>
31
37
  thingd queues push <queue> --payload '{"key":"value"}'
32
38
  thingd queues claim <queue> [--lease-ms <ms>]
33
39
  thingd queues ack <queue> <jobId>
34
40
  thingd queues nack <queue> <jobId> [--error <message>] [--delay-ms <ms>]
35
41
  thingd queues list <queue> [--limit <n>]
36
42
  thingd queues dead <queue> [--limit <n>]
43
+ thingd bench rust --smoke
44
+ thingd bench rust --count <n>
37
45
  thingd metrics
38
46
 
39
47
  Options:
@@ -54,6 +62,7 @@ const BOOLEAN_FLAGS = new Set([
54
62
  "raw",
55
63
  "claude",
56
64
  "cursor",
65
+ "smoke",
57
66
  ]);
58
67
  export async function runCli(args = process.argv.slice(2), options = {}) {
59
68
  // Auto-detect and set THINGD_NATIVE_PATH if not already set, to allow global execution
@@ -136,6 +145,15 @@ async function runCommand(context) {
136
145
  await runInstall(context);
137
146
  return;
138
147
  }
148
+ if (command === "doctor") {
149
+ const { runDoctor } = await import("./doctor.js");
150
+ await runDoctor(context);
151
+ return;
152
+ }
153
+ if (command === "bench") {
154
+ await runBench(context);
155
+ return;
156
+ }
139
157
  if (command === "objects") {
140
158
  await runObjects(context);
141
159
  return;
@@ -162,6 +180,63 @@ async function runCommand(context) {
162
180
  }
163
181
  throw new Error(`Unknown command: ${command}`);
164
182
  }
183
+ async function runBench(context) {
184
+ const target = requiredToken(context.parsed, 1, "benchmark target (rust)");
185
+ if (target !== "rust") {
186
+ throw new Error(`Unsupported benchmark target: ${target}`);
187
+ }
188
+ const isSmoke = hasFlag(context.parsed, "smoke");
189
+ const countStr = stringFlag(context.parsed, "count");
190
+ const count = countStr ? Number.parseInt(countStr, 10) : isSmoke ? 100 : undefined;
191
+ if (count === undefined) {
192
+ throw new Error("bench rust requires --smoke or --count <n>");
193
+ }
194
+ if (Number.isNaN(count) || count <= 0) {
195
+ throw new Error("--count must be a positive integer");
196
+ }
197
+ try {
198
+ const { execSync } = await import("node:child_process");
199
+ try {
200
+ execSync("cargo --version", { stdio: "ignore" });
201
+ }
202
+ catch {
203
+ throw new Error("Rust toolchain (cargo) is not installed or not in the PATH. Cannot run Rust benchmarks.");
204
+ }
205
+ context.stderr.write(`\n${pc.bold("Running Rust storage benchmark")} (Count: ${pc.cyan(count)})...\n\n`);
206
+ const { spawn } = await import("node:child_process");
207
+ const child = spawn("cargo", [
208
+ "run",
209
+ "--release",
210
+ "-p",
211
+ "thingd-core",
212
+ "--example",
213
+ "storage_bench",
214
+ "--features",
215
+ "sqlite",
216
+ "--",
217
+ String(count),
218
+ ], {
219
+ stdio: "inherit",
220
+ cwd: resolve(resolveCliPath(), "../../../.."),
221
+ });
222
+ return new Promise((resolvePromise, rejectPromise) => {
223
+ child.on("close", (code) => {
224
+ if (code === 0) {
225
+ resolvePromise();
226
+ }
227
+ else {
228
+ rejectPromise(new Error(`Benchmark failed with exit code: ${code}`));
229
+ }
230
+ });
231
+ child.on("error", (error) => {
232
+ rejectPromise(error);
233
+ });
234
+ });
235
+ }
236
+ catch (err) {
237
+ throw new Error(`Failed to run benchmark: ${err instanceof Error ? err.message : String(err)}`);
238
+ }
239
+ }
165
240
  async function runStatus(context) {
166
241
  const connection = resolveConnection(context);
167
242
  if (!connection.cloud) {
@@ -233,8 +308,32 @@ async function runSearch(context) {
233
308
  async function runObjects(context) {
234
309
  const action = requiredToken(context.parsed, 1, "objects action");
235
310
  const collection = requiredToken(context.parsed, 2, "collection");
236
- const id = requiredToken(context.parsed, 3, "object id");
237
311
  await withDb(context, async (db) => {
312
+ if (action === "list") {
313
+ const objects = await db.listObjects(collection);
314
+ if (context.pretty) {
315
+ const table = new Table({
316
+ head: ["ID", "Version", "Created At", "Updated At", "Data"],
317
+ style: { head: ["green"] },
318
+ });
319
+ for (const obj of objects) {
320
+ const { id, collection: _, createdAt, updatedAt, version, ...data } = obj;
321
+ table.push([
322
+ id,
323
+ String(version),
324
+ createdAt ? new Date(createdAt).toLocaleString() : "",
325
+ updatedAt ? new Date(updatedAt).toLocaleString() : "",
326
+ JSON.stringify(data),
327
+ ]);
328
+ }
329
+ context.stdout.write(`${table.toString()}\n`);
330
+ }
331
+ else {
332
+ writeJson(context.stdout, objects, false);
333
+ }
334
+ return;
335
+ }
336
+ const id = requiredToken(context.parsed, 3, "object id");
238
337
  if (action === "get") {
239
338
  writeJson(context.stdout, await db.get(collection, id), context.pretty);
240
339
  return;
@@ -254,10 +353,47 @@ async function runObjects(context) {
254
353
  async function runEvents(context) {
255
354
  const action = requiredToken(context.parsed, 1, "events action");
256
355
  await withDb(context, async (db) => {
356
+ if (action === "streams") {
357
+ const streams = await db.listStreams();
358
+ if (context.pretty) {
359
+ const table = new Table({
360
+ head: ["Stream Name"],
361
+ style: { head: ["green"] },
362
+ });
363
+ for (const str of streams) {
364
+ table.push([str]);
365
+ }
366
+ context.stdout.write(`${table.toString()}\n`);
367
+ }
368
+ else {
369
+ writeJson(context.stdout, streams, false);
370
+ }
371
+ return;
372
+ }
257
373
  if (action === "list") {
258
374
  const stream = optionalToken(context.parsed, 2);
259
- const events = await db.events.list(stream);
260
- writeJson(context.stdout, limitItems(events, optionalInt(context.parsed, "limit")), context.pretty);
375
+ const events = limitItems(await db.events.list(stream), optionalInt(context.parsed, "limit"));
376
+ if (context.pretty) {
377
+ const table = new Table({
378
+ head: ["Event ID", "Stream", "Event Type", "Created At", "Text", "Data"],
379
+ style: { head: ["green"] },
380
+ });
381
+ for (const ev of events) {
382
+ const { id, stream: evStream, type, createdAt, text, ...data } = ev;
383
+ table.push([
384
+ id,
385
+ evStream,
386
+ type,
387
+ createdAt ? new Date(createdAt).toLocaleString() : "",
388
+ text ?? "",
389
+ JSON.stringify(data),
390
+ ]);
391
+ }
392
+ context.stdout.write(`${table.toString()}\n`);
393
+ }
394
+ else {
395
+ writeJson(context.stdout, events, false);
396
+ }
261
397
  return;
262
398
  }
263
399
  if (action === "append") {
@@ -274,7 +410,20 @@ async function runCollections(context) {
274
410
  const action = requiredToken(context.parsed, 1, "collections action");
275
411
  await withDb(context, async (db) => {
276
412
  if (action === "list") {
277
- writeJson(context.stdout, await db.listCollections(), context.pretty);
413
+ const collections = await db.listCollections();
414
+ if (context.pretty) {
415
+ const table = new Table({
416
+ head: ["Collection Name"],
417
+ style: { head: ["green"] },
418
+ });
419
+ for (const col of collections) {
420
+ table.push([col]);
421
+ }
422
+ context.stdout.write(`${table.toString()}\n`);
423
+ }
424
+ else {
425
+ writeJson(context.stdout, collections, false);
426
+ }
278
427
  return;
279
428
  }
280
429
  throw new Error(`Unknown collections action: ${action}`);
@@ -284,7 +433,20 @@ async function runStreams(context) {
284
433
  const action = requiredToken(context.parsed, 1, "streams action");
285
434
  await withDb(context, async (db) => {
286
435
  if (action === "list") {
287
- writeJson(context.stdout, await db.listStreams(), context.pretty);
436
+ const streams = await db.listStreams();
437
+ if (context.pretty) {
438
+ const table = new Table({
439
+ head: ["Stream Name"],
440
+ style: { head: ["green"] },
441
+ });
442
+ for (const str of streams) {
443
+ table.push([str]);
444
+ }
445
+ context.stdout.write(`${table.toString()}\n`);
446
+ }
447
+ else {
448
+ writeJson(context.stdout, streams, false);
449
+ }
288
450
  return;
289
451
  }
290
452
  throw new Error(`Unknown streams action: ${action}`);
@@ -310,11 +472,50 @@ async function runQueues(context) {
310
472
  const action = requiredToken(context.parsed, 1, "queues action");
311
473
  await withDb(context, async (db) => {
312
474
  if (action === "list-all") {
313
- writeJson(context.stdout, await db.listQueues(), context.pretty);
475
+ const queues = await db.listQueues();
476
+ if (context.pretty) {
477
+ const table = new Table({
478
+ head: ["Queue Name"],
479
+ style: { head: ["green"] },
480
+ });
481
+ for (const q of queues) {
482
+ table.push([q]);
483
+ }
484
+ context.stdout.write(`${table.toString()}\n`);
485
+ }
486
+ else {
487
+ writeJson(context.stdout, queues, false);
488
+ }
314
489
  return;
315
490
  }
316
491
  const queueName = requiredToken(context.parsed, 2, "queue");
317
492
  const queue = db.queue(queueName);
493
+ if (action === "stats") {
494
+ const [activeJobs, deadJobs] = await Promise.all([queue.list(), queue.dead()]);
495
+ const totalActive = activeJobs.length;
496
+ const totalDead = deadJobs.length;
497
+ const leasedJobs = activeJobs.filter((job) => job.status === "leased");
498
+ const readyJobs = activeJobs.filter((job) => job.status === "ready");
499
+ const stats = {
500
+ queue: queueName,
501
+ totalActive,
502
+ ready: readyJobs.length,
503
+ leased: leasedJobs.length,
504
+ dead: totalDead,
505
+ };
506
+ if (context.pretty) {
507
+ const table = new Table({
508
+ head: ["Stat Metric", "Value"],
509
+ style: { head: ["green"] },
510
+ });
511
+ table.push(["Queue Name", queueName], ["Ready Jobs", String(readyJobs.length)], ["Leased Jobs", String(leasedJobs.length)], ["Dead Jobs", String(totalDead)], ["Total Active", String(totalActive)]);
512
+ context.stdout.write(`${table.toString()}\n`);
513
+ }
514
+ else {
515
+ writeJson(context.stdout, stats, false);
516
+ }
517
+ return;
518
+ }
318
519
  if (action === "push") {
319
520
  const payload = parseJsonRecord(requiredFlag(context.parsed, "payload"));
320
521
  const options = {
@@ -345,11 +546,51 @@ async function runQueues(context) {
345
546
  return;
346
547
  }
347
548
  if (action === "list") {
348
- writeJson(context.stdout, limitItems(await queue.list(), optionalInt(context.parsed, "limit")), context.pretty);
549
+ const jobs = limitItems(await queue.list(), optionalInt(context.parsed, "limit"));
550
+ if (context.pretty) {
551
+ const table = new Table({
552
+ head: ["Job ID", "Status", "Attempts", "Max Attempts", "Available At", "Payload"],
553
+ style: { head: ["green"] },
554
+ });
555
+ for (const job of jobs) {
556
+ table.push([
557
+ job.id,
558
+ job.status,
559
+ String(job.attempts),
560
+ String(job.maxAttempts),
561
+ job.availableAt ? new Date(job.availableAt).toLocaleString() : "",
562
+ JSON.stringify(job.payload),
563
+ ]);
564
+ }
565
+ context.stdout.write(`${table.toString()}\n`);
566
+ }
567
+ else {
568
+ writeJson(context.stdout, jobs, false);
569
+ }
349
570
  return;
350
571
  }
351
572
  if (action === "dead") {
352
- writeJson(context.stdout, limitItems(await queue.dead(), optionalInt(context.parsed, "limit")), context.pretty);
573
+ const jobs = limitItems(await queue.dead(), optionalInt(context.parsed, "limit"));
574
+ if (context.pretty) {
575
+ const table = new Table({
576
+ head: ["Job ID", "Attempts", "Max Attempts", "Dead At", "Last Error", "Payload"],
577
+ style: { head: ["green"] },
578
+ });
579
+ for (const job of jobs) {
580
+ table.push([
581
+ job.id,
582
+ String(job.attempts),
583
+ String(job.maxAttempts),
584
+ job.deadAt ? new Date(job.deadAt).toLocaleString() : "",
585
+ job.lastError ?? "",
586
+ JSON.stringify(job.payload),
587
+ ]);
588
+ }
589
+ context.stdout.write(`${table.toString()}\n`);
590
+ }
591
+ else {
592
+ writeJson(context.stdout, jobs, false);
593
+ }
353
594
  return;
354
595
  }
355
596
  throw new Error(`Unknown queues action: ${action}`);
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwB7C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAqInE"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwB7C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAyInE"}
package/dist/install.js CHANGED
@@ -127,9 +127,15 @@ export async function runInstall(context) {
127
127
  };
128
128
  context.stdout.write(`${JSON.stringify(fullConfig, null, 2)}\n`);
129
129
  }
130
- if (showClaude || showCursor) {
130
+ if (choice === "1") {
131
131
  context.stderr.write(`\n Restart Claude Desktop to activate. Cursor activates immediately after pasting.\n\n`);
132
132
  }
133
+ else if (choice === "2") {
134
+ context.stderr.write(`\n Restart Claude Desktop to activate.\n\n`);
135
+ }
136
+ else if (choice === "3") {
137
+ context.stderr.write(`\n Cursor activates immediately after pasting.\n\n`);
138
+ }
133
139
  }
134
140
  function findGlobalBinPath() {
135
141
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AA2jDA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA4BvD"}
1
+ {"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AA4jDA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA4BvD"}
@@ -229,11 +229,12 @@ async function fetchResources() {
229
229
  db.listStreams(),
230
230
  db.listQueues?.() ?? Promise.resolve([]),
231
231
  ]);
232
- totalObjects = isNaN(objCount) || objCount === 0 ? totalObjects : objCount;
233
- totalEventsCount = isNaN(evtCount) || evtCount === 0 ? totalEventsCount : evtCount;
232
+ totalObjects = Number.isNaN(objCount) || objCount === 0 ? totalObjects : objCount;
233
+ totalEventsCount = Number.isNaN(evtCount) || evtCount === 0 ? totalEventsCount : evtCount;
234
234
  totalActiveJobsCount =
235
- isNaN(activeCount) || activeCount === 0 ? totalActiveJobsCount : activeCount;
236
- totalDeadJobsCount = isNaN(deadCount) || deadCount === 0 ? totalDeadJobsCount : deadCount;
235
+ Number.isNaN(activeCount) || activeCount === 0 ? totalActiveJobsCount : activeCount;
236
+ totalDeadJobsCount =
237
+ Number.isNaN(deadCount) || deadCount === 0 ? totalDeadJobsCount : deadCount;
237
238
  collections = nativeCollections.length > 0 ? nativeCollections : ["decisions", "load-test"];
238
239
  streams =
239
240
  nativeStreams.length > 0
@@ -712,9 +713,9 @@ function draw() {
712
713
  else {
713
714
  titleStr = ` thingd ${pc.dim("|")} ${driver.toUpperCase()} ${pc.dim("|")} ${dbPath} `;
714
715
  }
715
- buf += pc.inverse(padToWidth(titleStr, W)) + "\n";
716
+ buf += `${pc.inverse(padToWidth(titleStr, W))}\n`;
716
717
  // Separator
717
- buf += pc.dim("─".repeat(sideW) + "─┬─" + "─".repeat(viewW)) + "\n";
718
+ buf += `${pc.dim(`${"─".repeat(sideW)}─┬─${"─".repeat(viewW)}`)}\n`;
718
719
  // Build Form Lines if active
719
720
  if (formState?.active) {
720
721
  viewerLines = [`${pc.bgCyan(pc.black(` ${formState.title} `))}`, ""];
@@ -783,15 +784,15 @@ function draw() {
783
784
  // Viewer
784
785
  const vLine = viewerLines[r + viewerScroll] ?? "";
785
786
  const right = fitToWidth(vLine, viewW, false);
786
- buf += left + pc.dim(" │ ") + right + "\n";
787
+ buf += `${left + pc.dim(" │ ") + right}\n`;
787
788
  }
788
789
  // Separator
789
- buf += pc.dim("─".repeat(sideW) + "─┴─" + "─".repeat(viewW)) + "\n";
790
+ buf += `${pc.dim(`${"─".repeat(sideW)}─┴─${"─".repeat(viewW)}`)}\n`;
790
791
  // Footer
791
792
  let help;
792
793
  if (formState?.active) {
793
794
  const hasOptions = formState.fields[formState.activeIndex]?.options;
794
- help = ` ${pc.dim("↑↓")} focus ${hasOptions ? pc.dim("←→") + " select " : ""}${pc.dim("enter")} submit ${pc.dim("ctrl+e")} editor ${pc.dim("esc")} cancel `;
795
+ help = ` ${pc.dim("↑↓")} focus ${hasOptions ? `${pc.dim("←→")} select ` : ""}${pc.dim("enter")} submit ${pc.dim("ctrl+e")} editor ${pc.dim("esc")} cancel `;
795
796
  }
796
797
  else if (!connected) {
797
798
  help = ` ${pc.dim("↑↓")} nav ${pc.dim("enter")} connect ${pc.dim("q")} quit `;
@@ -897,7 +898,7 @@ async function launchEditor(f) {
897
898
  const newContent = fs.readFileSync(tmpFile, "utf-8");
898
899
  f.value = newContent.trim();
899
900
  }
900
- catch (e) { }
901
+ catch (_e) { }
901
902
  if (process.stdin.isTTY)
902
903
  process.stdin.setRawMode(true);
903
904
  process.stdin.on("keypress", keypressHandler);
@@ -928,7 +929,7 @@ function parsePayload(str) {
928
929
  v = true;
929
930
  else if (v === "false")
930
931
  v = false;
931
- else if (!isNaN(Number(v)))
932
+ else if (!Number.isNaN(Number(v)))
932
933
  v = Number(v);
933
934
  }
934
935
  obj[k] = v;
@@ -985,8 +986,8 @@ async function handleCreate(selected) {
985
986
  try {
986
987
  id = crypto.randomUUID();
987
988
  }
988
- catch (e) {
989
- id = "obj_" + Date.now().toString(36) + Math.random().toString(36).substring(2);
989
+ catch (_e) {
990
+ id = `obj_${Date.now().toString(36)}${Math.random().toString(36).substring(2)}`;
990
991
  }
991
992
  }
992
993
  const data = parsePayload(vals.payload || "");
@@ -1109,7 +1110,7 @@ async function handleSearch() {
1109
1110
  const options = {};
1110
1111
  if (limitStr) {
1111
1112
  const limit = parseInt(limitStr, 10);
1112
- if (!isNaN(limit))
1113
+ if (!Number.isNaN(limit))
1113
1114
  options.limit = limit;
1114
1115
  }
1115
1116
  const results = await db.search(query, options);
@@ -1147,7 +1148,7 @@ async function handleInfo() {
1147
1148
  const u = new URL(p, urlObj.toString());
1148
1149
  const headers = {};
1149
1150
  if (authToken)
1150
- headers["Authorization"] = `Bearer ${authToken}`;
1151
+ headers.Authorization = `Bearer ${authToken}`;
1151
1152
  const res = await fetch(u, { headers });
1152
1153
  if (!res.ok)
1153
1154
  throw new Error(`HTTP ${res.status}`);
@@ -1232,7 +1233,7 @@ function setupKeypress() {
1232
1233
  }
1233
1234
  else if (key.name === "left" || key.name === "right") {
1234
1235
  const f = formState.fields[formState.activeIndex];
1235
- if (f && f.options && f.options.length > 0) {
1236
+ if (f?.options && f.options.length > 0) {
1236
1237
  const currentIndex = f.options.indexOf(f.value);
1237
1238
  let nextIndex = key.name === "right" ? currentIndex + 1 : currentIndex - 1;
1238
1239
  if (nextIndex < 0)
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,YAAY,CAAC;AAWpB,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;CACvC,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,0BAA+B,GACvC,IAAI,CA6WN"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,YAAY,CAAC;AAWpB,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;CACvC,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,0BAA+B,GACvC,IAAI,CA+XN"}
package/dist/mcp/tools.js CHANGED
@@ -305,6 +305,19 @@ export function registerThingdTools(server, db, options = {}) {
305
305
  openWorldHint: false,
306
306
  },
307
307
  }, async ({ queue }) => jsonResult(await db.queue(queue).dead()));
308
+ server.registerTool("thing_objects_list", {
309
+ title: "List Objects",
310
+ description: "List all thingd objects in a collection.",
311
+ inputSchema: {
312
+ collection: z.string().min(1),
313
+ },
314
+ annotations: {
315
+ readOnlyHint: true,
316
+ destructiveHint: false,
317
+ idempotentHint: true,
318
+ openWorldHint: false,
319
+ },
320
+ }, async ({ collection }) => jsonResult(await db.listObjects(collection)));
308
321
  }
309
322
  function auditMetadata(actor, source) {
310
323
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thingd-cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Command-line interface, Interactive TUI Dashboard, and MCP server for thingd.",
5
5
  "type": "module",
6
6
  "author": "Sayan Mohsin",
@@ -33,7 +33,7 @@
33
33
  "cli-table3": "^0.6.5",
34
34
  "picocolors": "^1.1.1",
35
35
  "zod": "^4.4.3",
36
- "thingd": "0.11.0"
36
+ "thingd": "0.12.0"
37
37
  },
38
38
  "engines": {
39
39
  "node": ">=20"