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 +99 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +81 -20
- package/dist/interactive.d.ts +2 -0
- package/dist/interactive.d.ts.map +1 -0
- package/dist/interactive.js +1540 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,50 +1,121 @@
|
|
|
1
1
|
# thingd-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/thingd-cli)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
pnpm --filter thingd-cli test
|
|
14
|
+
npm install -g thingd-cli
|
|
14
15
|
```
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
Or run it on the fly using `npx`:
|
|
17
18
|
|
|
18
19
|
```bash
|
|
19
|
-
thingd
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
THINGD_AUTH_TOKEN=change-me
|
|
33
|
-
thingd status
|
|
34
|
-
thingd tools
|
|
36
|
+
thingd
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
|
|
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>
|
|
42
|
-
--path <path>
|
|
43
|
-
--driver <driver>
|
|
44
|
-
--pretty
|
|
45
|
-
--limit <n>
|
|
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
|
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
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
|
|
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")
|
|
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.
|
|
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 =
|
|
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: "
|
|
106
|
-
url:
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
|
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 ?? (
|
|
346
|
+
driver: driver ?? (cloud ? "cloud" : undefined),
|
|
286
347
|
authToken: stringFlag(context.parsed, "auth-token") ?? context.env.THINGD_AUTH_TOKEN,
|
|
287
|
-
|
|
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 === "
|
|
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
|
|
448
|
+
function isCloudPath(path) {
|
|
388
449
|
return path.startsWith("http://") || path.startsWith("https://") || path.startsWith("thingd://");
|
|
389
450
|
}
|
|
390
|
-
function
|
|
391
|
-
const url = new URL(
|
|
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
|
|
398
|
-
const url = new URL(
|
|
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
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AA2jDA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA4BvD"}
|