thingd-cli 0.1.0 → 0.2.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
@@ -1,50 +1,121 @@
1
1
  # thingd-cli
2
2
 
3
- Admin and operator CLI for `thingd`.
3
+ [![npm version](https://badge.fury.io/js/thingd-cli.svg)](https://www.npmjs.com/package/thingd-cli)
4
4
 
5
- This package provides the `thingd` binary. It uses the public
6
- `thingd` SDK for store access and can connect to a local store or
7
- a remote sidecar through `THINGD_URL`.
5
+ Admin and operator CLI for [thingd](https://www.npmjs.com/package/thingd).
8
6
 
9
- ## Build And Test
7
+ This package provides the `thingd` binary. It uses the public `thingd` SDK for store access and can connect to a local store or a remote sidecar through `THINGD_URL`.
8
+
9
+ ## Installation
10
+
11
+ Install globally via npm to use the `thingd` command anywhere:
10
12
 
11
13
  ```bash
12
- pnpm --filter thingd-cli build
13
- pnpm --filter thingd-cli test
14
+ npm install -g thingd-cli
14
15
  ```
15
16
 
16
- ## Usage
17
+ Or run it on the fly using `npx`:
17
18
 
18
19
  ```bash
19
- thingd status
20
- thingd objects put decisions rust-core --text "Use Rust for the core engine."
21
- thingd objects get decisions rust-core
22
- thingd events append project:thingd decision.made --text "Picked the CLI shape."
23
- thingd events list project:thingd
24
- thingd queues push embed --payload '{"object":"docs/readme"}'
25
- thingd queues claim embed
20
+ npx thingd-cli
26
21
  ```
27
22
 
28
- Remote sidecar mode:
23
+ ## Two Modes of Operation
24
+
25
+ The CLI is designed to support both human operators and automated scripts through two distinct modes:
26
+
27
+ 1. **Interactive Dashboard (TUI)**: Run `thingd` without any arguments to launch a beautiful, interactive terminal UI. Perfect for exploring collections, editing objects, and managing queues manually.
28
+ 2. **Non-Interactive CLI**: Run `thingd <command>` for one-off operations. This mode intentionally emits JSON so it is easy to use from shell scripts, CI/CD pipelines, and AI-agent workflows.
29
29
 
30
+ ---
31
+
32
+ ## 1. Interactive Dashboard (TUI)
33
+
34
+ To start the interactive dashboard, simply run:
30
35
  ```bash
31
- THINGD_URL=http://127.0.0.1:8757
32
- THINGD_AUTH_TOKEN=change-me
33
- thingd status
34
- thingd tools
36
+ thingd
35
37
  ```
36
38
 
37
- Common options:
39
+ ### Connection Mode
40
+ Upon launch, you will be prompted to select a connection driver:
41
+ - `Memory`: An ephemeral, in-memory instance (great for quick testing).
42
+ - `Native`: Connects directly to a local SQLite `.db` file (requires a file path).
43
+ - `Cloud`: Connects to a remote ThingD cluster (requires a URL and an optional Bearer Token).
44
+
45
+ ### Keyboard Shortcuts
46
+ Once connected, the TUI provides a dual-pane layout: a navigation sidebar on the left and a detailed viewer/editor on the right.
47
+
48
+ | Shortcut | Action |
49
+ |----------|--------|
50
+ | `↑` / `↓` (or `k` / `j`) | Navigate the tree vertically |
51
+ | `←` / `→` (or `h` / `l`) | Expand or collapse folders (Collections, Streams, Queues) |
52
+ | `Enter` | Expand node or open a folder |
53
+ | `c` | **Create** a new Object, Event, or push a Queue job |
54
+ | `e` | **Edit** an Object or retry a Queue job |
55
+ | `d` | **Delete** an Object or Ack/Resolve a Queue job |
56
+ | `/` (or `f`) | **Global Semantic Search** across the database |
57
+ | `i` | **Info**: View Connection Info & Cloud Cluster Status |
58
+ | `r` | **Refresh** data from the database |
59
+ | `s` | **Switch** database connections |
60
+ | `q` | **Quit** |
61
+
62
+ ---
63
+
64
+ ## 2. Non-Interactive CLI
65
+
66
+ For scripts and automation, you can bypass the TUI entirely.
67
+
68
+ ### Common Options
69
+ You can configure the connection via environment variables or CLI flags:
38
70
 
39
71
  ```txt
40
72
  --url <url> remote thingd URL. Defaults to THINGD_URL
41
- --auth-token <tok> remote bearer token. Defaults to THINGD_AUTH_TOKEN
42
- --path <path> local database path. Defaults to THINGD_PATH or :memory:
43
- --driver <driver> memory, native, or remote
44
- --pretty pretty-print JSON output
45
- --limit <n> result limit for search and list commands
73
+ --auth-token <tok> remote bearer token. Defaults to THINGD_AUTH_TOKEN
74
+ --path <path> local database path. Defaults to THINGD_PATH or :memory:
75
+ --driver <driver> memory, native, or cloud
76
+ --pretty pretty-print JSON output
77
+ --limit <n> result limit for search and list commands
78
+ ```
79
+
80
+ ### Command Reference
81
+
82
+ **Metrics & Discovery**
83
+ ```bash
84
+ thingd metrics # Get total counts (objects, events, activeJobs, deadJobs)
85
+ thingd status # Check cluster health (requires --url)
86
+ thingd tools # List available MCP tools (requires --url)
87
+ thingd collections list # List all collection names
88
+ thingd streams list # List all stream names
89
+ thingd queues list-all # List all queue names
90
+ ```
91
+
92
+ **Search**
93
+ ```bash
94
+ thingd search "my query" [--collection <name>] [--limit <n>]
95
+ ```
96
+
97
+ **Objects**
98
+ ```bash
99
+ thingd objects put decisions rust-core --text "Use Rust for the core engine."
100
+ thingd objects put decisions rust-core --data '{"status":"active"}'
101
+ thingd objects get decisions rust-core
102
+ thingd objects delete decisions rust-core
103
+ ```
104
+
105
+ **Events**
106
+ ```bash
107
+ thingd events append project:thingd decision.made --text "Picked the CLI shape."
108
+ thingd events list project:thingd
109
+ ```
110
+
111
+ **Queues**
112
+ ```bash
113
+ thingd queues push embed --payload '{"object":"docs/readme"}'
114
+ thingd queues claim embed
115
+ thingd queues ack embed <jobId>
116
+ thingd queues nack embed <jobId> --error "failed to fetch" --delay-ms 5000
117
+ thingd queues list embed
118
+ thingd queues dead embed
46
119
  ```
47
120
 
48
- The first CLI version intentionally emits JSON so it is easy to use from shell
49
- scripts, tests, and AI-agent workflows.
50
121
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAgBA,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;AAwDF,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CA4BjB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAiBA,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;AA4DF,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAiCjB"}
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { pathToFileURL } from "node:url";
3
3
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
4
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
5
  import { ThingD, } from "thingd";
6
+ import { runInteractiveCli } from "./interactive.js";
6
7
  const HELP_TEXT = `thingd
7
8
 
8
9
  Admin and operator CLI for thingd.
@@ -17,18 +18,22 @@ Usage:
17
18
  thingd objects delete <collection> <id>
18
19
  thingd events append <stream> <type> [--text <text>] [--data '{"field":"value"}']
19
20
  thingd events list [stream] [--limit <n>]
21
+ thingd collections list
22
+ thingd streams list
23
+ thingd queues list-all
20
24
  thingd queues push <queue> --payload '{"key":"value"}'
21
25
  thingd queues claim <queue> [--lease-ms <ms>]
22
26
  thingd queues ack <queue> <jobId>
23
27
  thingd queues nack <queue> <jobId> [--error <message>] [--delay-ms <ms>]
24
28
  thingd queues list <queue> [--limit <n>]
25
29
  thingd queues dead <queue> [--limit <n>]
30
+ thingd metrics
26
31
 
27
32
  Options:
28
33
  --url <url> remote thingd URL. Defaults to THINGD_URL
29
34
  --auth-token <tok> remote bearer token. Defaults to THINGD_AUTH_TOKEN
30
35
  --path <path> local database path. Defaults to THINGD_PATH or :memory:
31
- --driver <driver> memory, native, or remote
36
+ --driver <driver> memory, native, or cloud
32
37
  --pretty pretty-print JSON output
33
38
  --limit <n> result limit for search and list commands
34
39
  -h, --help show help
@@ -44,10 +49,14 @@ export async function runCli(args = process.argv.slice(2), options = {}) {
44
49
  pretty: hasFlag(parsed, "pretty"),
45
50
  };
46
51
  try {
47
- if (hasFlag(parsed, "help") || hasFlag(parsed, "h") || parsed.tokens.length === 0) {
52
+ if (hasFlag(parsed, "help") || hasFlag(parsed, "h")) {
48
53
  writeText(context.stdout, HELP_TEXT);
49
54
  return 0;
50
55
  }
56
+ if (parsed.tokens.length === 0) {
57
+ await runInteractiveCli();
58
+ return 0;
59
+ }
51
60
  await runCommand(context);
52
61
  return 0;
53
62
  }
@@ -80,15 +89,27 @@ async function runCommand(context) {
80
89
  await runEvents(context);
81
90
  return;
82
91
  }
92
+ if (command === "collections") {
93
+ await runCollections(context);
94
+ return;
95
+ }
96
+ if (command === "streams") {
97
+ await runStreams(context);
98
+ return;
99
+ }
83
100
  if (command === "queues") {
84
101
  await runQueues(context);
85
102
  return;
86
103
  }
104
+ if (command === "metrics") {
105
+ await runMetrics(context);
106
+ return;
107
+ }
87
108
  throw new Error(`Unknown command: ${command}`);
88
109
  }
89
110
  async function runStatus(context) {
90
111
  const connection = resolveConnection(context);
91
- if (!connection.remote) {
112
+ if (!connection.cloud) {
92
113
  writeJson(context.stdout, {
93
114
  mode: "local",
94
115
  driver: connection.driver ?? "memory",
@@ -96,28 +117,28 @@ async function runStatus(context) {
96
117
  }, context.pretty);
97
118
  return;
98
119
  }
99
- const baseUrl = resolveRemoteBaseUrl(connection.path);
120
+ const baseUrl = resolveCloudBaseUrl(connection.path);
100
121
  const [health, cluster] = await Promise.all([
101
122
  fetchJson(new URL("/healthz", baseUrl), connection.authToken),
102
123
  fetchJson(new URL("/cluster/status", baseUrl), connection.authToken),
103
124
  ]);
104
125
  writeJson(context.stdout, {
105
- mode: "remote",
106
- url: resolveRemoteMcpUrl(connection.path),
126
+ mode: "cloud",
127
+ url: resolveCloudMcpUrl(connection.path),
107
128
  health,
108
129
  cluster,
109
130
  }, context.pretty);
110
131
  }
111
132
  async function runTools(context) {
112
133
  const connection = resolveConnection(context);
113
- if (!connection.remote) {
134
+ if (!connection.cloud) {
114
135
  throw new Error("tools requires --url or THINGD_URL because tools are exposed by the MCP runtime");
115
136
  }
116
137
  const client = new Client({
117
138
  name: "thingd-cli",
118
139
  version: "0.1.0",
119
140
  });
120
- const transport = new StreamableHTTPClientTransport(new URL(resolveRemoteMcpUrl(connection.path)), {
141
+ const transport = new StreamableHTTPClientTransport(new URL(resolveCloudMcpUrl(connection.path)), {
121
142
  requestInit: connection.authToken
122
143
  ? {
123
144
  headers: {
@@ -194,10 +215,50 @@ async function runEvents(context) {
194
215
  throw new Error(`Unknown events action: ${action}`);
195
216
  });
196
217
  }
218
+ async function runCollections(context) {
219
+ const action = requiredToken(context.parsed, 1, "collections action");
220
+ await withDb(context, async (db) => {
221
+ if (action === "list") {
222
+ writeJson(context.stdout, await db.listCollections(), context.pretty);
223
+ return;
224
+ }
225
+ throw new Error(`Unknown collections action: ${action}`);
226
+ });
227
+ }
228
+ async function runStreams(context) {
229
+ const action = requiredToken(context.parsed, 1, "streams action");
230
+ await withDb(context, async (db) => {
231
+ if (action === "list") {
232
+ writeJson(context.stdout, await db.listStreams(), context.pretty);
233
+ return;
234
+ }
235
+ throw new Error(`Unknown streams action: ${action}`);
236
+ });
237
+ }
238
+ async function runMetrics(context) {
239
+ await withDb(context, async (db) => {
240
+ const [objects, events, activeJobs, deadJobs] = await Promise.all([
241
+ db.countObjects(),
242
+ db.countEvents(),
243
+ db.countActiveJobs(),
244
+ db.countDeadJobs(),
245
+ ]);
246
+ writeJson(context.stdout, {
247
+ objects,
248
+ events,
249
+ activeJobs,
250
+ deadJobs,
251
+ }, context.pretty);
252
+ });
253
+ }
197
254
  async function runQueues(context) {
198
255
  const action = requiredToken(context.parsed, 1, "queues action");
199
- const queueName = requiredToken(context.parsed, 2, "queue");
200
256
  await withDb(context, async (db) => {
257
+ if (action === "list-all") {
258
+ writeJson(context.stdout, await db.listQueues(), context.pretty);
259
+ return;
260
+ }
261
+ const queueName = requiredToken(context.parsed, 2, "queue");
201
262
  const queue = db.queue(queueName);
202
263
  if (action === "push") {
203
264
  const payload = parseJsonRecord(requiredFlag(context.parsed, "payload"));
@@ -243,7 +304,7 @@ async function withDb(context, callback) {
243
304
  const connection = resolveConnection(context);
244
305
  const db = await ThingD.open({
245
306
  path: connection.path,
246
- url: connection.remote ? connection.path : undefined,
307
+ url: connection.cloud ? connection.path : undefined,
247
308
  driver: connection.driver,
248
309
  authToken: connection.authToken,
249
310
  });
@@ -278,13 +339,13 @@ function buildMemoryEvent(parsed, type) {
278
339
  function resolveConnection(context) {
279
340
  const url = stringFlag(context.parsed, "url") ?? context.env.THINGD_URL;
280
341
  const path = url ?? stringFlag(context.parsed, "path") ?? context.env.THINGD_PATH ?? ":memory:";
281
- const remote = isRemotePath(path);
342
+ const cloud = isCloudPath(path);
282
343
  const driver = parseDriver(stringFlag(context.parsed, "driver") ?? context.env.THINGD_DRIVER);
283
344
  return {
284
345
  path,
285
- driver: driver ?? (remote ? "remote" : undefined),
346
+ driver: driver ?? (cloud ? "cloud" : undefined),
286
347
  authToken: stringFlag(context.parsed, "auth-token") ?? context.env.THINGD_AUTH_TOKEN,
287
- remote,
348
+ cloud,
288
349
  };
289
350
  }
290
351
  function parseArgs(args) {
@@ -366,7 +427,7 @@ function parseDriver(value) {
366
427
  if (value === undefined) {
367
428
  return undefined;
368
429
  }
369
- if (value === "memory" || value === "native" || value === "remote") {
430
+ if (value === "memory" || value === "native" || value === "cloud") {
370
431
  return value;
371
432
  }
372
433
  throw new Error(`Unsupported driver: ${value}`);
@@ -384,24 +445,24 @@ function compactOptions(options) {
384
445
  function limitItems(items, limit) {
385
446
  return limit === undefined ? items : items.slice(0, limit);
386
447
  }
387
- function isRemotePath(path) {
448
+ function isCloudPath(path) {
388
449
  return path.startsWith("http://") || path.startsWith("https://") || path.startsWith("thingd://");
389
450
  }
390
- function resolveRemoteMcpUrl(value) {
391
- const url = new URL(normalizeRemoteUrl(value));
451
+ function resolveCloudMcpUrl(value) {
452
+ const url = new URL(normalizeCloudUrl(value));
392
453
  if (url.pathname === "" || url.pathname === "/") {
393
454
  url.pathname = "/mcp";
394
455
  }
395
456
  return url.toString();
396
457
  }
397
- function resolveRemoteBaseUrl(value) {
398
- const url = new URL(normalizeRemoteUrl(value));
458
+ function resolveCloudBaseUrl(value) {
459
+ const url = new URL(normalizeCloudUrl(value));
399
460
  if (url.pathname === "/mcp") {
400
461
  url.pathname = "/";
401
462
  }
402
463
  return url.toString();
403
464
  }
404
- function normalizeRemoteUrl(value) {
465
+ function normalizeCloudUrl(value) {
405
466
  return value.startsWith("thingd://") ? `http://${value.slice("thingd://".length)}` : value;
406
467
  }
407
468
  async function fetchJson(url, authToken) {
@@ -0,0 +1,2 @@
1
+ export declare function runInteractiveCli(): Promise<void>;
2
+ //# sourceMappingURL=interactive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AA2jDA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA4BvD"}