@thinger-io/cli-core 0.4.2 → 0.5.1
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/CHANGELOG.md +207 -0
- package/README.md +82 -31
- package/lib/auth.js +25 -1
- package/lib/bucket-export.js +91 -0
- package/lib/cli.js +106 -50
- package/lib/commands/_shared.js +89 -1
- package/lib/commands/api.js +90 -0
- package/lib/commands/arguments.js +62 -0
- package/lib/commands/bucket.js +52 -44
- package/lib/commands/dashboard.js +5 -2
- package/lib/commands/data.js +539 -0
- package/lib/commands/device.js +128 -241
- package/lib/commands/grammar.js +279 -0
- package/lib/commands/product.js +104 -214
- package/lib/commands/profile.js +1 -0
- package/lib/commands/resources.js +975 -0
- package/lib/dashboard-init.js +4 -3
- package/lib/dashboard.js +10 -2
- package/lib/mcp/catalog.js +302 -64
- package/lib/mcp/core-types.js +36 -0
- package/lib/mcp/helpers.js +23 -2
- package/lib/mcp/naming.js +20 -0
- package/lib/mcp/registry.js +18 -67
- package/lib/mcp/resource-hooks.js +19 -5
- package/lib/mcp/server.js +43 -11
- package/lib/mcp/tools-resource.js +1234 -330
- package/lib/mcp/types/bucket-data.js +151 -0
- package/lib/mcp/{tools-dashboards.js → types/dashboard-apps.js} +99 -37
- package/lib/mcp/types/device-resource.js +117 -0
- package/lib/mcp/types/doc.js +35 -0
- package/lib/mcp/types/plugin-call.js +75 -0
- package/lib/mcp/types/product-api.js +44 -0
- package/lib/mcp/types/profile.js +37 -0
- package/lib/mcp/types/storage-file.js +170 -0
- package/lib/openapi.js +18 -0
- package/lib/product-apply.js +121 -0
- package/lib/raw-api.js +111 -0
- package/lib/storage.js +41 -13
- package/lib/tokens.js +16 -2
- package/package.json +1 -1
- package/lib/mcp/tools-buckets.js +0 -130
- package/lib/mcp/tools-devices.js +0 -134
- package/lib/mcp/tools-misc.js +0 -48
- package/lib/mcp/tools-plugins.js +0 -63
- package/lib/mcp/tools-products.js +0 -52
- package/lib/mcp/tools-storage.js +0 -173
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,212 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.1] - 2026-10-31
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `host` action `update_settings` says what the server does: it merges the
|
|
8
|
+
body into the host's custom settings (keys left out stay, `null` removes
|
|
9
|
+
one). Its confirmation plan no longer claims it removes the keys left
|
|
10
|
+
out of the body.
|
|
11
|
+
- The browser login on a server that does not know the CLI's OAuth client
|
|
12
|
+
(older than Thinger 7.5.0, or the client was removed) says so and offers
|
|
13
|
+
username and password or a token instead of failing.
|
|
14
|
+
|
|
15
|
+
## [0.5.0] - 2026-10-21
|
|
16
|
+
|
|
17
|
+
One grammar for every command, noun first:
|
|
18
|
+
`<cli> <noun> [<noun>…] <verb> [<parent ids>…] [<id>] [options]`. Nouns
|
|
19
|
+
nest under their parent (`device property`, `product profile api`), verbs
|
|
20
|
+
come from a closed set, ids are positional and named after their noun,
|
|
21
|
+
and the options keep one meaning everywhere. Some commands of 0.4 change
|
|
22
|
+
form (see the table below); the old forms are gone, not aliased.
|
|
23
|
+
|
|
24
|
+
The MCP serves eight tools, `resource_describe|list|get|create|update|
|
|
25
|
+
delete|clone|call`, instead of 25: what had a tool of its own (bucket data,
|
|
26
|
+
storage files, device resources, the guides, the profiles, dashboard apps,
|
|
27
|
+
plugin and product API calls) is a type or an action of them, and every
|
|
28
|
+
record is addressed with `ids`, one key per level named as the server's
|
|
29
|
+
REST path names it. The old tool names and the `parent`/`id` arguments are
|
|
30
|
+
gone, not aliased (see the second table below).
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- MCP: eight tools for everything. Each takes a `type` and `ids`
|
|
35
|
+
(`{"device": "d1", "bucket": "b1"}`); the keys are the server's REST
|
|
36
|
+
placeholders (`type` for an asset type, `group` for an asset group,
|
|
37
|
+
`rule` and `instance` for alarms, `resource` for every product profile
|
|
38
|
+
category), so a body's id field and its `ids` key share a name and
|
|
39
|
+
`create` and `clone` need no mapping. Records answered by `list`, `get`
|
|
40
|
+
and `create` carry their `ids`, ready to pass back. A call with an
|
|
41
|
+
unknown argument is refused listing the ones the tool takes, and points
|
|
42
|
+
a misplaced key to `ids`, `filter` or `body` (an id in an action's body
|
|
43
|
+
too: `{"device": …}` goes in `ids`). An action that refuses its input
|
|
44
|
+
shows one of its examples.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
- The core's types and actions, served by the same tools: `bucket_data`
|
|
48
|
+
(list reads, create writes), `storage_file` (`ids.file` is the path;
|
|
49
|
+
`body.local` uploads files and folders of this machine), `device_resource`
|
|
50
|
+
(list, and its `call` action), `doc` (the guides), `profile`; actions
|
|
51
|
+
`device.send`, `device.overview`, `product_profile_api.call`,
|
|
52
|
+
`plugin.call` and `dashboard.init|dev|publish`. A CLI adds its own the
|
|
53
|
+
same way (`resourceTypes`, `resourceActions`).
|
|
54
|
+
- `resource_describe`: without a type, the catalog with each type's ids and
|
|
55
|
+
ready-to-send examples; with one, its ids, actions, the permission each
|
|
56
|
+
verb needs, its schemas and examples (the tests check every example
|
|
57
|
+
against the tools). A `describe` resource hook adds to it, and hooks
|
|
58
|
+
get `ids` and `merge` (an update that patches) in their context.
|
|
59
|
+
- Actions that change a lot at once answer a plan and run with
|
|
60
|
+
`confirm: true`: `replace_all` of the product profile categories (a diff
|
|
61
|
+
of what it adds, changes and removes), a host's `update_settings` (a diff
|
|
62
|
+
of the settings) and `restart`, `plugin.remove|kill`, alarm rules'
|
|
63
|
+
`test_notification`, and `dashboard.publish` with `prune`. MCP clients
|
|
64
|
+
that support elicitation are asked instead. The CLI asks unless `-y`.
|
|
65
|
+
- `mcp --read-only` (or `<PREFIX>_MCP_READ_ONLY=1`): only describe, list,
|
|
66
|
+
get and the read-only actions are served.
|
|
67
|
+
- Tool annotations (read-only, destructive, idempotent) on the eight tools.
|
|
68
|
+
- `doc list` and `doc get <doc>`: the guides from the command line.
|
|
69
|
+
- `api <path>`: any request to the profile's server, as `gh api` does for
|
|
70
|
+
GitHub. `{user}` stands for the account (`--user` for another), `-X`
|
|
71
|
+
picks the method (GET, or POST with a body), `--from <file|->` and
|
|
72
|
+
`-s key=value` (dots nest) build the body, `-q key=value` the query, and
|
|
73
|
+
`-o <file>` saves the answer (binary answers are only written to a file
|
|
74
|
+
or a pipe). Only paths of the profile's server: a full URL is refused.
|
|
75
|
+
- Commands for every resource type, generated from the MCP catalog:
|
|
76
|
+
`list|get|create|update|delete|clone|describe` and the type's actions,
|
|
77
|
+
for its 47 types. A child type nests under its parent's command and takes
|
|
78
|
+
its ids first (`device bucket tag get <device> <bucket> <tag>`); the
|
|
79
|
+
product profile categories go under `product profile` (`api`, `bucket`,
|
|
80
|
+
`property`, `autoprovision`, `flow`, `code`), alarms under `alarm`
|
|
81
|
+
(`alarm rule`, `alarm instance`) and assets under `asset` (`asset type`,
|
|
82
|
+
`asset group`); server types go under
|
|
83
|
+
`admin` (`admin host restart <host>`). They run the handlers of the
|
|
84
|
+
`resource_*` tools, hooks included (generated device credentials, token
|
|
85
|
+
expiries such as "30d"). Bodies come from `--from <file|->` and
|
|
86
|
+
`-s key=value` (JSON values, dots nest); create, update and clone take
|
|
87
|
+
`-n/--name` and `-d/--description` on the types that have them, `update
|
|
88
|
+
--merge` patches, `--projects` sets the projects; `list` takes
|
|
89
|
+
`--search`, `--filter`, `--count` and `--index` and returns every record
|
|
90
|
+
unless `--count`; `get` takes `-f <dot.path>` (one field) and
|
|
91
|
+
`-o <file>`; actions take `-f`, `-o` and `-q`; `delete`,
|
|
92
|
+
`replace-all`, `plugin uninstall` and `admin host restart` ask unless
|
|
93
|
+
`-y`. With `-j` they print `{ ok, data }`. What they print speaks of
|
|
94
|
+
commands, not tools: types by their command (`device property`, not
|
|
95
|
+
`device_property`), `--index` for the next page, and `describe` lists
|
|
96
|
+
the type's commands and actions where the MCP lists the tools that run
|
|
97
|
+
them, its ids as the command takes them, the create, update and clone
|
|
98
|
+
bodies as tables of fields (nested ones by their dot path, one table per
|
|
99
|
+
alternative) with their examples pretty-printed, and the permission each
|
|
100
|
+
verb needs (`-j` keeps the MCP's text).
|
|
101
|
+
- `set <parents…> <property> <value>` on every property type (device,
|
|
102
|
+
product, plugin, asset type, asset group, project and account
|
|
103
|
+
properties): the value, parsed as JSON, created when missing.
|
|
104
|
+
- Commands over the MCP tools that are not CRUD: `bucket write` (a JSON
|
|
105
|
+
sample, an array or NDJSON), `bucket export` (create the export, wait for
|
|
106
|
+
it, download it, delete it from the server unless `--keep`), `storage
|
|
107
|
+
file list|upload|download|delete` (folders and binary files), `device
|
|
108
|
+
send` (data of an HTTP device), `device overview`, `device resource
|
|
109
|
+
list|call`, `product profile api call` (a product API resource for the
|
|
110
|
+
whole product), `product apply --from <file|->` (create or update a
|
|
111
|
+
product from the JSON of `product export`: record, each profile
|
|
112
|
+
category, services and product properties) and `plugin call`.
|
|
113
|
+
- `device list`, `product list` and `bucket list` take the list options
|
|
114
|
+
too, keeping their tables; `device create` and `product create` take
|
|
115
|
+
`--from`, `-s` and `--projects`, and their id may come from the body
|
|
116
|
+
(when the record is created but its projects cannot be set, the error
|
|
117
|
+
says so).
|
|
118
|
+
- `buildProgram(opts)`: the command tree `runCli` runs, for tests; and
|
|
119
|
+
`checkGrammar(program, { exceptions })` (`commands/grammar`), which checks
|
|
120
|
+
a tree against the grammar (names, verbs, positionals, options, help
|
|
121
|
+
groups, the catalog's commands) for the tests of every CLI.
|
|
122
|
+
- `runCli({ argumentHelp })`: help of the positional arguments of the CLI's
|
|
123
|
+
own commands. Every positional argument has a description in the help.
|
|
124
|
+
- The help groups commands the same way at every level: Resources,
|
|
125
|
+
Administration, Configuration, Integration and Operations at the root;
|
|
126
|
+
Records, Data, Actions and Subresources inside a noun.
|
|
127
|
+
- MCP tool results carry their structured data for these commands (under
|
|
128
|
+
a symbol: MCP clients see the same text as before).
|
|
129
|
+
|
|
130
|
+
### Changed (breaking)
|
|
131
|
+
|
|
132
|
+
| 0.4 | 0.5 |
|
|
133
|
+
| -------------------------------------------- | ------------------------------------------------------------ |
|
|
134
|
+
| `device list <pattern>` | `device list --search <text>` (or `--filter name=<regex>`) |
|
|
135
|
+
| `device info <device>` | `device get <device>` |
|
|
136
|
+
| `device property <device>` | `device property list <device>` |
|
|
137
|
+
| `device property <device> <property> [-f p]` | `device property get <device> <property> [-f p]` |
|
|
138
|
+
| `device property-set <device> <prop> <json>` | `device property set <device> <property> <json>` |
|
|
139
|
+
| `device resource <device>` | `device resource list <device>` |
|
|
140
|
+
| `device resource <device> <res> -i k=v` | `device resource call <device> <resource> -s k=v` (`--from`) |
|
|
141
|
+
| `product info <product>` | `product get <product>` |
|
|
142
|
+
| `product property-list <product>` | `product property list <product>` |
|
|
143
|
+
| `product property-get <product> <prop>` | `product property get <product> <property>` |
|
|
144
|
+
| `product property-set <product> <prop> <v>` | `product property set <product> <property> <value>` |
|
|
145
|
+
| `product property-delete <product> <prop>` | `product property delete <product> <property> -y` |
|
|
146
|
+
|
|
147
|
+
- The outputs change with the commands: `device get` and `product get`
|
|
148
|
+
print the record as JSON (as `info` did); `device property get` and
|
|
149
|
+
`product property get` print the property record, with its value under
|
|
150
|
+
`value`, so `-f` paths start there (`-f value.setpoint` where 0.4 took
|
|
151
|
+
`-f setpoint`); `device property list` and
|
|
152
|
+
`product property list` print the property records (with `-j`, the
|
|
153
|
+
records instead of `{ product, properties: [names] }`); `product property
|
|
154
|
+
delete` asks unless `-y`. `device resource call` has no `-i/--input`: its
|
|
155
|
+
input comes from `--from` and `-s`, like every command that sends data.
|
|
156
|
+
- `register(program, groups)`'s groups are Resources at the root (they were
|
|
157
|
+
Operations), and `registerToolCommands(program)` is now
|
|
158
|
+
`registerDataCommands(program)` (`commands/data`) and no longer takes
|
|
159
|
+
them.
|
|
160
|
+
- The MCP tools (the CLI commands keep their forms and `-j` outputs; only
|
|
161
|
+
the help of some positionals is renamed: `asset type get <type>`,
|
|
162
|
+
`asset group get <group>`, `alarm rule get <rule>`, `alarm instance get
|
|
163
|
+
<instance>`, `product profile <category> get <product> <resource>`):
|
|
164
|
+
|
|
165
|
+
| 0.4 tool | 0.5 call |
|
|
166
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------ |
|
|
167
|
+
| `resource_* parent "d1" id "b1"` | `resource_* ids {"device": "d1", "bucket": "b1"}` |
|
|
168
|
+
| `bucket_data_read bucket` | `resource_list type "bucket_data" ids {bucket}` (`filter` for the query) |
|
|
169
|
+
| `bucket_data_write bucket data` | `resource_create type "bucket_data" ids {bucket} body <sample or samples>` |
|
|
170
|
+
| `storage_file_list storage path` | `resource_list type "storage_file" ids {storage} filter {path}` |
|
|
171
|
+
| `storage_file_read storage path` | `resource_get type "storage_file" ids {storage, file}` |
|
|
172
|
+
| `storage_file_write storage path …` | `resource_create type "storage_file" ids {storage, file} body {content\|local}` |
|
|
173
|
+
| `storage_file_delete storage path` | `resource_delete type "storage_file" ids {storage, file}` |
|
|
174
|
+
| `device_resource_list device` | `resource_list type "device_resource" ids {device}` |
|
|
175
|
+
| `device_resource_call device resource` | `resource_call type "device_resource" action "call" ids {device, resource}` |
|
|
176
|
+
| `device_data_send device data` | `resource_call type "device" action "send" ids {device} body <data>` |
|
|
177
|
+
| `device_overview` | `resource_call type "device" action "overview"` |
|
|
178
|
+
| `product_resource_call product resource` | `resource_call type "product_profile_api" action "call" ids {product, resource}` |
|
|
179
|
+
| `plugin_call plugin path` | `resource_call type "plugin" action "call" ids {plugin} body {path, method}` |
|
|
180
|
+
| `doc_read id` | `resource_get type "doc" ids {doc}` (`resource_list type "doc"` lists them) |
|
|
181
|
+
| `profile_list` | `resource_list type "profile"` |
|
|
182
|
+
| `dashboard_init` / `_dev` / `_publish` | `resource_call type "dashboard" action "init"` / `"dev"` / `"publish"` |
|
|
183
|
+
|
|
184
|
+
- `runCli({ mcpTools })` is gone, with `mcp/registry`'s `genericTools` and
|
|
185
|
+
`mcpTools`: a CLI adds types and actions (`resourceTypes`,
|
|
186
|
+
`resourceActions`) instead of tools, and `buildTools(opts)` builds the
|
|
187
|
+
eight. Texts refer to operations as `{op:<type>.<verb|action>}`.
|
|
188
|
+
- `lib/mcp/tools-*.js` moved to `lib/mcp/types/` (as types and actions) and
|
|
189
|
+
`lib/commands/tools.js` to `lib/commands/data.js`.
|
|
190
|
+
|
|
191
|
+
### Fixed
|
|
192
|
+
|
|
193
|
+
- Account tokens created with an expiry as a duration ("30d") through
|
|
194
|
+
`resource_create` (and `createToken`) got it in seconds: the server takes
|
|
195
|
+
unix milliseconds and refused it. A timestamp too small to be
|
|
196
|
+
milliseconds is still read as seconds. Device tokens keep seconds.
|
|
197
|
+
- Ids `.` and `..` are refused (`resource_*` tools too): the URL resolved
|
|
198
|
+
them to the parent, so deleting the property `..` of a device reached
|
|
199
|
+
the device.
|
|
200
|
+
- The storage helpers (storage files, thinr's product scripts)
|
|
201
|
+
escape each segment of a file path and refuse empty, `.` and `..`
|
|
202
|
+
segments: `a#b` reached the file `a`, `..` the storage. The plugin
|
|
203
|
+
`call` action also refuses `%2e%2e`.
|
|
204
|
+
- Listing a storage folder answered each file's path relative to that
|
|
205
|
+
folder: it is now the whole path, the one its `ids.file` reads.
|
|
206
|
+
- The help of `bucket read` and `device resource` printed some defaults
|
|
207
|
+
|
|
208
|
+
twice ("(default: 10) (default: 10)", "(default: {})").
|
|
209
|
+
|
|
3
210
|
## [0.4.2] - 2026-10-17
|
|
4
211
|
|
|
5
212
|
### Fixed
|
package/README.md
CHANGED
|
@@ -19,7 +19,16 @@ It holds everything that is not specific to a product:
|
|
|
19
19
|
`profile` (product profiles), `tokens`, `alarms`, `storage`, with every
|
|
20
20
|
account path built in `paths`.
|
|
21
21
|
- **Generic commands** (`cli`, `commands/*`) — login, logout, profile, device,
|
|
22
|
-
product, bucket, dashboard, mcp
|
|
22
|
+
product, bucket, dashboard, mcp; `api` for any request to the server; and,
|
|
23
|
+
generated from the same catalog the MCP serves, the commands of every
|
|
24
|
+
resource type (`list`, `get`, `create`, `update`, `delete`, `clone`,
|
|
25
|
+
`describe`, `set` on properties, and its actions), run by the handlers and
|
|
26
|
+
hooks of the `resource_*` tools, so a type added to the catalog reaches the
|
|
27
|
+
CLI and the MCP at once. Commands over the data and files complete them:
|
|
28
|
+
`bucket write` and `export`, `storage file list|upload|download|delete`,
|
|
29
|
+
`device send`, `overview` and `resource list|call`, `product apply`,
|
|
30
|
+
`product profile api call`, `plugin call`, `doc list|get`. A command the
|
|
31
|
+
CLI registers itself keeps its name. They all follow one grammar (below).
|
|
23
32
|
- **Dashboard apps** (`dashboard`, `dashboard-init`, `dashboard-dev`,
|
|
24
33
|
`dashboard-sdk`) — create an app from a template of
|
|
25
34
|
`@thinger-io/dashboard-sdk`, develop it on its own dev server (a link with
|
|
@@ -31,11 +40,12 @@ It holds everything that is not specific to a product:
|
|
|
31
40
|
here, marking what needs a given server version), the dashboard SDK's
|
|
32
41
|
reference and types (from its package: the project's version, else the
|
|
33
42
|
CLI's), and the CLI's own.
|
|
34
|
-
- **MCP** (`mcp/*`) — the stdio server and
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
- **MCP** (`mcp/*`) — the stdio server and its eight `resource_*` tools
|
|
44
|
+
over a catalog of the server's resource types, plus the types and actions
|
|
45
|
+
every CLI serves next to it (bucket data, storage files, device
|
|
46
|
+
resources, the guides, the profiles, dashboard apps…) and those of the
|
|
47
|
+
CLI; the guides are MCP resources too. `mcp --read-only` serves only what
|
|
48
|
+
reads.
|
|
39
49
|
- **OpenAPI** (`openapi`) — the server's own API description, cached per
|
|
40
50
|
server and version, with a bundled copy as fallback.
|
|
41
51
|
|
|
@@ -76,42 +86,83 @@ servers.
|
|
|
76
86
|
or subcommands inside the generic groups.
|
|
77
87
|
- `onNoCommand(program)` — what to do when called without arguments
|
|
78
88
|
(default: help).
|
|
79
|
-
- `mcpTools` — the CLI's own MCP tools, declared with bare names
|
|
80
|
-
(`name: 'device_exec'`) and referring to others in descriptions as
|
|
81
|
-
`{tool:resource_list}` or `{family:storage_file}`. Unknown references fail
|
|
82
|
-
at startup. A tool flagged `replaces: true` swaps out the generic tool of
|
|
83
|
-
the same name.
|
|
84
89
|
- `resourceHooks` — the CLI's hooks on catalog types (`{ product: {
|
|
85
|
-
afterDelete } }`), run after the core's: see `mcp/resource-hooks.js`.
|
|
90
|
+
afterDelete } }`), run after the core's: see `mcp/resource-hooks.js`. A
|
|
91
|
+
`describe` hook adds to what `resource_describe` shows of the type.
|
|
86
92
|
- `resourceTypes` — resource types of the CLI's own platform, served by the
|
|
87
93
|
same `resource_*` tools as the server's: instead of a REST path each has
|
|
88
94
|
functions for the verbs it supports (`list`, `get`, `create`, `update`,
|
|
89
|
-
`delete`), its `parent` and `id
|
|
90
|
-
`resource_describe`, and `actions` for `resource_call`
|
|
91
|
-
`'collection'` or `'either'`
|
|
92
|
-
|
|
95
|
+
`delete`), its `parent` and `id` (the keys of `ids`), a `schema`, `notes`
|
|
96
|
+
and `examples` for `resource_describe`, and `actions` for `resource_call`
|
|
97
|
+
(`on: 'item'`, `'collection'` or `'either'`; `readOnly`, `confirm`,
|
|
98
|
+
`bodyRequired`). The core checks the ids, the filter and the examples,
|
|
99
|
+
and searches and pages what `list` returns. See `LocalType` in
|
|
93
100
|
`mcp/tools-resource.js`.
|
|
94
101
|
- `resourceActions` — actions the CLI adds to the server's types
|
|
95
102
|
(`{ device: { reboot: { description, run } } }`), run with
|
|
96
103
|
`resource_call` like the server's. A type or action that clashes with the
|
|
97
|
-
catalog fails at startup.
|
|
98
|
-
- `mcpInstructions` — server-level guidance sent to MCP clients.
|
|
104
|
+
catalog or the core fails at startup.
|
|
105
|
+
- `mcpInstructions` — server-level guidance sent to MCP clients. Texts
|
|
106
|
+
refer to an operation as `{op:<type>.<verb|action>}` (`{op:doc.get}`),
|
|
107
|
+
checked when the tools are built.
|
|
108
|
+
- `argumentHelp` — `{ byCommand, byName }`: help of the positional
|
|
109
|
+
arguments of the CLI's own commands (`{ 'device logs': { unit: '…' } }`).
|
|
110
|
+
|
|
111
|
+
`buildProgram(opts)` builds the same command tree without running it, for
|
|
112
|
+
tests.
|
|
113
|
+
|
|
114
|
+
## Command grammar
|
|
115
|
+
|
|
116
|
+
Every CLI on the core reads the same way:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
<cli> <noun> [<noun>…] <verb> [<parent ids>…] [<id>] [<extra>] [options]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- **Nouns** are singular, kebab case, nested under their parent:
|
|
123
|
+
`device property`, `device bucket tag`, `product profile api`, at most
|
|
124
|
+
three deep; server types go under `admin`. Groups run nothing themselves.
|
|
125
|
+
- **Verbs** form a closed set: for records, `list`, `get`, `create`,
|
|
126
|
+
`update`, `delete`, `clone`, `describe` and `set` (the value of a
|
|
127
|
+
property); for data, `read`, `write`, `send`, `export`, `import`,
|
|
128
|
+
`overview`, `call`, `upload` and `download`; the catalog's actions in
|
|
129
|
+
kebab case (`test-source`, `replace-all`); and the local `init`, `dev`,
|
|
130
|
+
`publish`, `login`, `logout`, `use`, `current` and `add`.
|
|
131
|
+
- **Positionals** are ids, outermost parent first, named after their noun:
|
|
132
|
+
`device property get <device> <property>`.
|
|
133
|
+
- **Options** keep one meaning: `-j --json`, `-o --output`, `-y --yes`,
|
|
134
|
+
`-s --set`, `-q --query`, `-X --method`, `-f --field`; `-n` and `-d` are
|
|
135
|
+
name and description on create and update. Bodies come from
|
|
136
|
+
`--from <file|->` and `-s key=value`; lists take `--search`, `--filter`,
|
|
137
|
+
`--count` and `--index`; deletes ask unless `-y`.
|
|
138
|
+
|
|
139
|
+
`checkGrammar(program, { exceptions })` (`commands/grammar`) checks a whole
|
|
140
|
+
tree and returns what breaks it; each CLI's tests run it over its
|
|
141
|
+
`buildProgram`, stating what it adds (`root`, `verbs`), its short options
|
|
142
|
+
with another meaning (`shorts`) and the rules a command does not follow
|
|
143
|
+
(`skip`).
|
|
99
144
|
|
|
100
145
|
## MCP tools
|
|
101
146
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
147
|
+
The MCP serves eight tools — `resource_describe`, `resource_list`,
|
|
148
|
+
`resource_get`, `resource_create`, `resource_update`, `resource_delete`,
|
|
149
|
+
`resource_clone`, `resource_call` (actions that are not CRUD) — that take a
|
|
150
|
+
`type` from the catalog (`lib/mcp/catalog.js`), the core's types
|
|
151
|
+
(`lib/mcp/core-types.js`) or the CLI's, and `ids`: one key per level, named
|
|
152
|
+
as the server's REST path names it (`{"device": "d1", "bucket": "b1"}`,
|
|
153
|
+
`{"product": "p1", "resource": "temp"}`, `{"rule": "high_temp"}`). A record
|
|
154
|
+
the tools answer carries its `ids`, ready to pass back. The catalog is a
|
|
155
|
+
reviewed table, never discovered at run time: each type has its path, API
|
|
156
|
+
version, parent, id and the verbs it allows, with the server's operationId
|
|
157
|
+
for each (the tests check them against the bundled OpenAPI document).
|
|
158
|
+
`resource_describe` reads the request schemas and examples from the OpenAPI
|
|
159
|
+
document of the server the profile points at, and shows each type's ids,
|
|
160
|
+
actions, permissions and ready-to-send examples (checked by the tests).
|
|
161
|
+
Actions that change a lot at once (`replace_all`, a host's
|
|
162
|
+
`update_settings`, `dashboard publish` with prune…) answer a plan first and
|
|
163
|
+
run with `confirm: true`; MCP clients that support elicitation are asked
|
|
164
|
+
instead. `mcp --read-only` (or `<PREFIX>_MCP_READ_ONLY=1`) serves only
|
|
165
|
+
describe, list, get and the read-only actions.
|
|
115
166
|
|
|
116
167
|
To draft catalog entries after an API change:
|
|
117
168
|
|
package/lib/auth.js
CHANGED
|
@@ -110,7 +110,22 @@ export async function authenticate(opts = {}) {
|
|
|
110
110
|
if (method === 'userpass') {
|
|
111
111
|
await authenticateWithUserPass(opts);
|
|
112
112
|
} else if (method === 'oauth') {
|
|
113
|
-
await authenticateWithOAuth(opts, existing)
|
|
113
|
+
await authenticateWithOAuth(opts, existing).catch(async (error) => {
|
|
114
|
+
// A server older than the CLI's built-in OAuth client, or
|
|
115
|
+
// one whose admin removed it: sign in another way.
|
|
116
|
+
if (error.code !== 'oauth_client_unknown' || !process.stdin.isTTY) throw error;
|
|
117
|
+
console.log(warning(error.message));
|
|
118
|
+
const fallback = await ask(select, {
|
|
119
|
+
message: 'Sign in another way:',
|
|
120
|
+
choices: [
|
|
121
|
+
{ name: 'Username and password', value: 'userpass' },
|
|
122
|
+
{ name: 'Token', value: 'token' },
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
return fallback === 'userpass'
|
|
126
|
+
? authenticateWithUserPass(opts)
|
|
127
|
+
: authenticateWithToken(opts);
|
|
128
|
+
});
|
|
114
129
|
} else {
|
|
115
130
|
await authenticateWithToken(opts);
|
|
116
131
|
}
|
|
@@ -391,6 +406,15 @@ async function authenticateWithOAuth(opts, existing) {
|
|
|
391
406
|
spinner.succeed('OAuth User Code received');
|
|
392
407
|
} catch (error) {
|
|
393
408
|
spinner.fail('OAuth device-code request failed');
|
|
409
|
+
const status = error.response?.status;
|
|
410
|
+
if (status === 401 || status === 404) {
|
|
411
|
+
throw Object.assign(
|
|
412
|
+
new Error(
|
|
413
|
+
`The server does not know the OAuth client ${client_id} (Thinger 7.5.0 and later have it built in). Sign in with a token or username and password instead.`,
|
|
414
|
+
),
|
|
415
|
+
{ code: 'oauth_client_unknown' },
|
|
416
|
+
);
|
|
417
|
+
}
|
|
394
418
|
throw error;
|
|
395
419
|
}
|
|
396
420
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import api from './api.js';
|
|
4
|
+
import { TIMEOUTS } from './constants.js';
|
|
5
|
+
import { apiError, inputError } from './errors.js';
|
|
6
|
+
import { v1 } from './paths.js';
|
|
7
|
+
import { rawApiCall } from './raw-api.js';
|
|
8
|
+
|
|
9
|
+
/** States of an export record (buckets.cpp). */
|
|
10
|
+
const READY = 1;
|
|
11
|
+
const FAILED = 2;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Export a bucket's data to a file and download it: the server writes the
|
|
15
|
+
* file in the background and lists it among the bucket's exports when
|
|
16
|
+
* ready. The export is deleted from the server afterwards unless `keep`.
|
|
17
|
+
*
|
|
18
|
+
* @param {{
|
|
19
|
+
* bucket: string,
|
|
20
|
+
* user?: string | null,
|
|
21
|
+
* format?: 'csv' | 'json' | 'arff',
|
|
22
|
+
* timestamps?: 'iso' | 'seconds' | 'milliseconds',
|
|
23
|
+
* minTs?: number,
|
|
24
|
+
* maxTs?: number,
|
|
25
|
+
* keep?: boolean,
|
|
26
|
+
* pollMs?: number,
|
|
27
|
+
* timeoutMs?: number,
|
|
28
|
+
* }} opts
|
|
29
|
+
* @returns {Promise<{ file: string, data: Buffer | string }>}
|
|
30
|
+
*/
|
|
31
|
+
export async function exportBucket({
|
|
32
|
+
bucket,
|
|
33
|
+
user,
|
|
34
|
+
format = 'csv',
|
|
35
|
+
timestamps = 'iso',
|
|
36
|
+
minTs,
|
|
37
|
+
maxTs,
|
|
38
|
+
keep = false,
|
|
39
|
+
pollMs = 1000,
|
|
40
|
+
timeoutMs = TIMEOUTS.API_LONG_MS,
|
|
41
|
+
}) {
|
|
42
|
+
if (!['csv', 'json', 'arff'].includes(format)) throw inputError('format: csv, json or arff');
|
|
43
|
+
if (!['iso', 'seconds', 'milliseconds'].includes(timestamps)) {
|
|
44
|
+
throw inputError('timestamps: iso, seconds or milliseconds');
|
|
45
|
+
}
|
|
46
|
+
const base = `${v1(user)}/buckets/${encodeURIComponent(bucket)}/exports`;
|
|
47
|
+
let file;
|
|
48
|
+
try {
|
|
49
|
+
const res = await api.post(base, {
|
|
50
|
+
data_type: format,
|
|
51
|
+
timestamp_format: timestamps,
|
|
52
|
+
...(minTs !== undefined ? { min_ts: minTs } : {}),
|
|
53
|
+
...(maxTs !== undefined ? { max_ts: maxTs } : {}),
|
|
54
|
+
});
|
|
55
|
+
file = res.data?.file;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
throw apiError(e, { notFound: `Bucket not found: ${bucket}` });
|
|
58
|
+
}
|
|
59
|
+
if (!file) throw new Error('the server did not name the export file');
|
|
60
|
+
|
|
61
|
+
const deadline = Date.now() + timeoutMs;
|
|
62
|
+
for (;;) {
|
|
63
|
+
let state;
|
|
64
|
+
try {
|
|
65
|
+
const res = await api.get(base);
|
|
66
|
+
state = (Array.isArray(res.data) ? res.data : []).find((e) => e.file === file)?.state;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
throw apiError(e);
|
|
69
|
+
}
|
|
70
|
+
if (state === READY) break;
|
|
71
|
+
if (state === FAILED) throw new Error(`the server could not export ${bucket}`);
|
|
72
|
+
if (Date.now() > deadline) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`the export ${file} is not ready yet: find it later with bucket export-file list`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
await sleep(pollMs);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const res = await rawApiCall({ path: `${base}/${encodeURIComponent(file)}` });
|
|
81
|
+
// The bytes as the server wrote them: parsing would round numbers.
|
|
82
|
+
const data = res.raw;
|
|
83
|
+
if (!keep) {
|
|
84
|
+
try {
|
|
85
|
+
await api.delete(`${base}/${encodeURIComponent(file)}`);
|
|
86
|
+
} catch {
|
|
87
|
+
// The data is downloaded: a leftover file on the server is no failure.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { file, data };
|
|
91
|
+
}
|