@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/README.md +82 -31
  3. package/lib/auth.js +25 -1
  4. package/lib/bucket-export.js +91 -0
  5. package/lib/cli.js +106 -50
  6. package/lib/commands/_shared.js +89 -1
  7. package/lib/commands/api.js +90 -0
  8. package/lib/commands/arguments.js +62 -0
  9. package/lib/commands/bucket.js +52 -44
  10. package/lib/commands/dashboard.js +5 -2
  11. package/lib/commands/data.js +539 -0
  12. package/lib/commands/device.js +128 -241
  13. package/lib/commands/grammar.js +279 -0
  14. package/lib/commands/product.js +104 -214
  15. package/lib/commands/profile.js +1 -0
  16. package/lib/commands/resources.js +975 -0
  17. package/lib/dashboard-init.js +4 -3
  18. package/lib/dashboard.js +10 -2
  19. package/lib/mcp/catalog.js +302 -64
  20. package/lib/mcp/core-types.js +36 -0
  21. package/lib/mcp/helpers.js +23 -2
  22. package/lib/mcp/naming.js +20 -0
  23. package/lib/mcp/registry.js +18 -67
  24. package/lib/mcp/resource-hooks.js +19 -5
  25. package/lib/mcp/server.js +43 -11
  26. package/lib/mcp/tools-resource.js +1234 -330
  27. package/lib/mcp/types/bucket-data.js +151 -0
  28. package/lib/mcp/{tools-dashboards.js → types/dashboard-apps.js} +99 -37
  29. package/lib/mcp/types/device-resource.js +117 -0
  30. package/lib/mcp/types/doc.js +35 -0
  31. package/lib/mcp/types/plugin-call.js +75 -0
  32. package/lib/mcp/types/product-api.js +44 -0
  33. package/lib/mcp/types/profile.js +37 -0
  34. package/lib/mcp/types/storage-file.js +170 -0
  35. package/lib/openapi.js +18 -0
  36. package/lib/product-apply.js +121 -0
  37. package/lib/raw-api.js +111 -0
  38. package/lib/storage.js +41 -13
  39. package/lib/tokens.js +16 -2
  40. package/package.json +1 -1
  41. package/lib/mcp/tools-buckets.js +0 -130
  42. package/lib/mcp/tools-devices.js +0 -134
  43. package/lib/mcp/tools-misc.js +0 -48
  44. package/lib/mcp/tools-plugins.js +0 -63
  45. package/lib/mcp/tools-products.js +0 -52
  46. 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 25 generic tools: eight
35
- `resource_*` tools over a catalog of the server's resource types, and the
36
- operations that are not CRUD (bucket data, device resources, storage
37
- files, dashboard development and publishing, the documentation…), plus that
38
- documentation as MCP resources.
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`, a `schema` and `notes` for
90
- `resource_describe`, and `actions` for `resource_call` (`on: 'item'`,
91
- `'collection'` or `'either'`). The core checks the parent and id, and
92
- searches and pages what `list` returns. See `LocalType` in
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
- Tool names follow `<object>[_<subobject>]_<verb>`, with the verbs list, get,
103
- describe, create, update, delete, clone, call, read, write, send.
104
-
105
- The server's collections are reached through eight generic tools —
106
- `resource_describe`, `resource_list`, `resource_get`, `resource_create`,
107
- `resource_update`, `resource_delete`, `resource_clone`, `resource_call`
108
- (actions that are not CRUD) — that take a `type`
109
- from the catalog (`lib/mcp/catalog.js`), a `parent` for child types and an
110
- `id`. The catalog is a reviewed table, never discovered at run time: each
111
- type has its path, API version, parent, id and the verbs it allows, with the
112
- server's operationId for each (the tests check them against the bundled
113
- OpenAPI document). `resource_describe` reads the request schemas and
114
- examples from the OpenAPI document of the server the profile points at.
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
+ }