@thinremote/thinr-cli 1.3.4 → 1.4.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +117 -123
  3. package/bin/thinr.js +43 -105
  4. package/commands/_shared.js +10 -90
  5. package/commands/device/_shared.js +2 -2
  6. package/commands/device/exec.js +2 -2
  7. package/commands/device/fs.js +2 -2
  8. package/commands/device/index.js +5 -19
  9. package/commands/device/logs.js +4 -4
  10. package/commands/device/status.js +1 -1
  11. package/commands/device/transfer.js +2 -2
  12. package/commands/device/update.js +8 -2
  13. package/commands/fleet/upgrade.js +3 -3
  14. package/commands/playbook/run.js +2 -2
  15. package/commands/playbook/validate.js +2 -2
  16. package/commands/product/_shared.js +1 -1
  17. package/commands/product/exec.js +2 -2
  18. package/commands/product/fs.js +2 -2
  19. package/commands/product/index.js +4 -11
  20. package/commands/product/logs.js +2 -2
  21. package/commands/product/metric.js +2 -2
  22. package/commands/product/playbook.js +5 -5
  23. package/commands/product/property.js +4 -4
  24. package/commands/product/push.js +2 -2
  25. package/commands/product/resource.js +4 -4
  26. package/commands/product/status.js +3 -3
  27. package/dist/dashboard.js +12 -12
  28. package/docs/guides/thinremote-metrics-logs.md +59 -0
  29. package/docs/guides/thinremote-playbooks.md +133 -0
  30. package/docs/guides/thinremote-scripts.md +115 -0
  31. package/docs/guides/thinremote.md +116 -0
  32. package/lib/command-stream.js +3 -2
  33. package/lib/console.js +6 -6
  34. package/lib/constants.js +1 -19
  35. package/lib/dashboard/event-stream.js +5 -5
  36. package/lib/device-api.js +4 -4
  37. package/lib/guides.js +41 -0
  38. package/lib/identity.js +75 -0
  39. package/lib/local-files.js +54 -0
  40. package/lib/mcp/{tools-install.js → agent-install.js} +12 -61
  41. package/lib/mcp/helpers.js +5 -25
  42. package/lib/mcp/playbook-ops.js +393 -0
  43. package/lib/mcp/registry.js +9 -43
  44. package/lib/mcp/resource-hooks.js +53 -0
  45. package/lib/mcp/resource-types.js +857 -0
  46. package/lib/mcp/tools-exec.js +217 -177
  47. package/lib/monitoring.js +3 -3
  48. package/lib/playbook/actions.js +30 -18
  49. package/lib/playbook/expression.js +1 -4
  50. package/lib/playbook/fleet.js +4 -1
  51. package/lib/playbook/loader.js +16 -8
  52. package/lib/playbook/runner.js +40 -8
  53. package/lib/playbook/schema.js +108 -34
  54. package/lib/product/log-presets.js +1 -1
  55. package/lib/product/logs.js +2 -2
  56. package/lib/product-orchestrator.js +14 -9
  57. package/lib/product.js +29 -227
  58. package/lib/proxy.js +4 -4
  59. package/lib/status.js +13 -5
  60. package/package.json +4 -2
  61. package/commands/device/delete.js +0 -53
  62. package/commands/device/list.js +0 -55
  63. package/commands/device/property.js +0 -52
  64. package/commands/device/resource.js +0 -52
  65. package/commands/logout.js +0 -47
  66. package/commands/product/list.js +0 -86
  67. package/commands/product/props.js +0 -125
  68. package/commands/profile.js +0 -173
  69. package/lib/alarms.js +0 -223
  70. package/lib/api.js +0 -89
  71. package/lib/auth.js +0 -428
  72. package/lib/bucket.js +0 -82
  73. package/lib/concurrency.js +0 -52
  74. package/lib/config.js +0 -230
  75. package/lib/devices.js +0 -60
  76. package/lib/errors.js +0 -65
  77. package/lib/format.js +0 -114
  78. package/lib/mcp/server.js +0 -67
  79. package/lib/mcp/tools-alarms.js +0 -507
  80. package/lib/mcp/tools-buckets.js +0 -99
  81. package/lib/mcp/tools-devices.js +0 -235
  82. package/lib/mcp/tools-filesystem.js +0 -281
  83. package/lib/mcp/tools-logs.js +0 -225
  84. package/lib/mcp/tools-playbook.js +0 -210
  85. package/lib/mcp/tools-products.js +0 -1557
  86. package/lib/mcp/tools-profile.js +0 -635
  87. package/lib/mcp/tools-profiles.js +0 -30
  88. package/lib/mcp/tools-properties.js +0 -79
  89. package/lib/mcp/tools-resources.js +0 -78
  90. package/lib/mcp/tools-scripts.js +0 -212
  91. package/lib/mcp/tools-tokens.js +0 -369
  92. package/lib/output.js +0 -114
  93. package/lib/paths.js +0 -32
  94. package/lib/profile.js +0 -657
  95. package/lib/property.js +0 -75
  96. package/lib/resource.js +0 -151
  97. package/lib/storage.js +0 -117
  98. package/lib/tokens.js +0 -366
@@ -0,0 +1,59 @@
1
+ # ThinRemote fleet metrics and log sources
2
+
3
+ Two per-product settings for operating a fleet from **thinr**: the metrics
4
+ its terminal dashboard (`thinr` with no arguments) shows for a product, and
5
+ the log sources an operator can stream from a device (in that dashboard or
6
+ with `thinr device logs`). Today only thinr reads them; they are not the web
7
+ console's dashboards (for those, see the server's `dashboard-sdk` guide and
8
+ `thinr_dashboard_publish`). Both live on the server as product properties,
9
+ so the devices need nothing and other clients could reuse them.
10
+
11
+ Tool names below omit the `thinr_` prefix.
12
+
13
+ ## Metrics
14
+
15
+ A metric is a value thinr's dashboard asks every device of the product for,
16
+ and combines across the fleet: free disk, connected clients, service version…
17
+ They live in the `dashboard_metrics` product property; edit them as the
18
+ resource type `product_metric` (parent the product, id the metric's name:
19
+ `resource_list`, `_get`, `_create`, `_update`, `_delete`), not by writing the
20
+ property by hand.
21
+
22
+ Fields of a metric:
23
+
24
+ | Field | Meaning |
25
+ | --------------- | -------------------------------------------------------------------------------------------------------- |
26
+ | `name` | Stable id (letters, digits, `_`, `-`): the resource id |
27
+ | `label` | Title in the panel |
28
+ | `resource` | Product API resource called on each device, usually a product script |
29
+ | `field` | Dot path to the value in the response (`disk.used_pct`); omit for scalar responses |
30
+ | `aggregation` | `sum`, `avg`, `max`, `min` (numeric), `count` (devices reporting a value), `top`, `distribution`, `none` |
31
+ | `visualization` | `kpi` (default: value and its age), `sparkline` (value and recent trend), `bar`, `list` |
32
+ | `interval` | Refresh in seconds (≥ 1); 0 or omitted relays only what devices push |
33
+ | `unit` | Display suffix (`%`, `devices`) |
34
+
35
+ Recipe: write a product script that prints the value as JSON (see
36
+ `thinremote-scripts`), check it with `device_resource_call` on one device,
37
+ then register the metric pointing at the script's resource name.
38
+
39
+ ## Log sources
40
+
41
+ A log source is a named shell command the agent streams when an operator
42
+ opens a device's logs in thinr (`journalctl -f`, `docker logs -f app`,
43
+ `tail -F /var/log/nginx/access.log`). They live in the `logs` product
44
+ property; without it, products show a single `system` source running
45
+ journalctl (listed with `fallback: true`). They are the resource type
46
+ `product_log_source`, parent the product, id the source's name.
47
+
48
+ - `resource_create`: a source by `name` (≤ 32 chars, letters, digits,
49
+ `_`, `-`; up to 32 sources). The `command` is stored and run as is.
50
+ `default: true` makes it the one shown first (and the one
51
+ `thinr device logs` uses without `--source`).
52
+ - Structured rendering: add a `preset` (`resource_describe` type
53
+ `product_log_source` lists them:
54
+ journalctl, spdlog, nginx…) or your own `pattern`, a regex with named
55
+ groups `time`, `level` and `msg`. With a `level` group, operators can filter
56
+ by severity. `preset` and `pattern` are mutually exclusive.
57
+ - `resource_update` with `{"default": true}` picks the default;
58
+ `resource_delete` drops a source (removing the last one deletes the
59
+ property, back to the fallback).
@@ -0,0 +1,133 @@
1
+ # ThinRemote playbooks
2
+
3
+ A playbook is a YAML document with ordered steps the agent executes on each
4
+ device: write a file, run a command, install a script, restart a service…
5
+ It is the safe way to change many machines: validated before it runs,
6
+ testable on one device, rolled out in batches with a failure threshold, and
7
+ recorded.
8
+
9
+ Tool names below omit the `thinr_` prefix.
10
+
11
+ ## Authoring flow
12
+
13
+ A saved playbook is a resource of type `product_playbook`, with the product
14
+ as parent and its name as id; validating, running and rolling it out are
15
+ its actions (`resource_call`).
16
+
17
+ 1. `resource_describe` type `product_playbook` once: its `playbook_format`
18
+ is the full schema (document, `vars`, step fields, every action with its
19
+ parameters, the `when` syntax). Write against it.
20
+ 2. Draft the YAML, then action `validate` with the YAML in `body.content`,
21
+ and fix what it reports. Repeat until it is clean.
22
+ 3. Save it: `resource_create` type `product_playbook`, body
23
+ `{"content": "<yaml>", "description"?: "…"}` (validated again: nothing is
24
+ saved if invalid; it writes `playbooks/<name>.yaml` in the product storage
25
+ and registers it in the `playbooks` product property). Change it later
26
+ with `resource_update`.
27
+ 4. Try it on one device: action `run` with the playbook's `id` and
28
+ `body.device`, first with `dry_run: true` (the resolved plan, no device
29
+ contacted), then `check: true` (contacts the device read-only: each step
30
+ says changed / unchanged / unknown), then for real.
31
+ 5. Roll it out: action `rollout` (below).
32
+
33
+ For a one-off run that is not worth saving, action `run` without `id` takes
34
+ the YAML in `body.content`, with the same `dry_run` and `check`; it targets
35
+ `body.device`, `body.devices`, `body.group` or every device of the product.
36
+
37
+ `resource_list` and `resource_get` show what a product has (`get` includes
38
+ the YAML); `resource_delete` removes one (index entry, then file).
39
+
40
+ ## Document
41
+
42
+ ```yaml
43
+ name: nginx-config
44
+ description: Deploy nginx.conf and reload
45
+ target:
46
+ product: web_gateways # or devices: [gw1, gw2]; group: madrid narrows
47
+ concurrency: 10 # parallel devices (action run)
48
+ fail_fast: false
49
+ vars:
50
+ port:
51
+ default: 8080
52
+ type: number
53
+ description: Listening port
54
+ release:
55
+ type: string
56
+ required: true # must be given at run time
57
+ steps:
58
+ - name: Write config to a temp file
59
+ action: write
60
+ path: /tmp/nginx.conf
61
+ content: |
62
+ server { listen {{ port }}; }
63
+ - name: Check it
64
+ action: exec
65
+ command: nginx -t -c /tmp/nginx.conf
66
+ register: check
67
+ - name: Move it into place
68
+ action: move
69
+ source: /tmp/nginx.conf
70
+ destination: /etc/nginx/nginx.conf
71
+ overwrite: true
72
+ when: check.ok
73
+ - name: Reload
74
+ action: exec
75
+ command: systemctl reload nginx
76
+ ```
77
+
78
+ - `vars`: plain (`port: 8080`) or extended (`default`, `type` string |
79
+ number | boolean | object, `description`, `overridable: false` for
80
+ internal ones, `required: true`). Used in any parameter as `{{ name }}`.
81
+ Overrides passed at run time (`vars`) are validated against the declared
82
+ type; unknown, non-overridable or missing required ones are errors, and
83
+ they never change the stored playbook.
84
+ - Step fields: `name`, `action`, the action's parameters, and optionally
85
+ `when` (skip unless true), `register` (expose `{ ok, result | error,
86
+ duration_ms }` to later steps), `pause_after` (seconds).
87
+ - `when` syntax: `==`, `!=`, `>`, `<`, `>=`, `<=`, `and`, `or`, `not`,
88
+ parentheses, quoted strings, numbers, `true` / `false` / `null`, dotted
89
+ paths (`check.ok`, `agent.version`).
90
+
91
+ Actions:
92
+
93
+ | Action | Required | Optional | Does |
94
+ | ---------------------------------- | ----------------------- | --------------------------- | ---------------------------------------------------------- |
95
+ | `exec` | `command` | `timeout` | runs a shell command; non-zero exit fails the step |
96
+ | `write` | `path`, `content` | | writes inline content |
97
+ | `push` / `pull` | `source`, `destination` | | copies a file from / to the machine running the playbook |
98
+ | `mkdir` | `path` | | creates a directory |
99
+ | `move` | `source`, `destination` | `overwrite` | moves; write to a temp file then `move` for atomic deploys |
100
+ | `delete` | `path` | `recursive` | deletes |
101
+ | `property_set` | `property`, `value` | | sets a device property |
102
+ | `resource` | `resource` | `inputs` | calls a device resource |
103
+ | `script_install` / `script_delete` | `name` | `content` or `content_file` | installs or removes a device script |
104
+ | `update` | `op` | `channel` | checks for or applies an agent update |
105
+ | `sleep` | `seconds` | | waits |
106
+
107
+ Run from the MCP server, `push`, `pull` and `content_file` only reach files
108
+ inside its local folder (`THINR_MCP_LOCAL_DIR`, or where it started).
109
+
110
+ `check` cannot predict `exec` or `resource` steps: they report `unknown`.
111
+
112
+ ## Rolling out
113
+
114
+ Action `rollout` (`resource_call` type `product_playbook`, the playbook's
115
+ `id`) runs a saved playbook on every matching device of the product, in batches of `body.batch_size` (default 5). A batch finishes before
116
+ the next starts; after each one the cumulative failure rate is compared
117
+ with `body.failure_threshold` (default 10 %), and reaching it stops the rollout
118
+ without touching the remaining devices. Narrow the targets with `group` or
119
+ `filter`; offline devices are skipped unless `include_offline`. `dry_run`
120
+ lists the plan and the devices; `check` reports what would change.
121
+
122
+ Each rollout writes a report to `playbooks/runs/<timestamp>-<name>-<user>.json`
123
+ in the product storage (read it with `storage_file_read`, storage named like
124
+ the product): resolved variables, per-device outcome, and why it stopped if
125
+ it did. A rollout that stops does not undo the devices it already changed:
126
+ write files to a temporary path and `move` them into place, so a device is
127
+ never left with half a change.
128
+
129
+ Writing the same file to every device is a playbook too: a `write` step
130
+ (and a `move`), rolled out.
131
+
132
+ Rollouts change real machines: confirm with the user, test on one device
133
+ first, and start with small batches.
@@ -0,0 +1,115 @@
1
+ # ThinRemote scripts
2
+
3
+ A script turns an operation (read a sensor, rotate a log, run a backup,
4
+ collect a health report) into a **resource**: something with a name and an
5
+ input/output shape that tools, dashboards and other devices can call. Use one
6
+ instead of `thinr_device_exec` whenever the operation will be repeated.
7
+
8
+ Both kinds are resource types: `resource_list` / `_get` / `_create` /
9
+ `_update` / `_delete` with type `device_script` (parent the device) or
10
+ `product_script` (parent the product), the file name as id.
11
+
12
+ Tool names below omit the `thinr_` prefix.
13
+
14
+ ## Device scripts or product scripts
15
+
16
+ | | Device script | Product script |
17
+ | ----------- | ------------------------------------- | --------------------------------------------------- |
18
+ | Lives in | the device: `<agent base>/scripts/` | the product's file storage: `scripts/` |
19
+ | Reaches | that device | every device of the product, current and future |
20
+ | Changing it | one write per device | one write, effective on the next call everywhere |
21
+ | Type | `device_script`, parent the device | `product_script`, parent the product |
22
+ | Called with | `device_resource_call` on that device | `device_resource_call` on any device of the product |
23
+
24
+ Prefer **product scripts** for fleets: the code is downloaded on each call,
25
+ so there is nothing to deploy and no drift between devices. Use a device
26
+ script for something specific to one machine.
27
+
28
+ The resource name is the file name without extension: `scripts/backup.sh` is
29
+ the resource `backup`. The extension is kept only for editor highlighting.
30
+
31
+ Scripts run with the agent's privileges, root for a system install:
32
+ installing one grants permanent remote execution of that code. Treat it like
33
+ adding a service.
34
+
35
+ ## Product scripts
36
+
37
+ Creating one (`resource_create` type `product_script`, body `{content, input?, output?}`)
38
+ does everything needed: it creates the
39
+ product's storage if missing, writes the `index.js` wrapper the first time,
40
+ links it as the product's code, uploads the script and creates or updates
41
+ the product API resource, so the script is callable at once. The product must
42
+ exist (`resource_create` type `product`).
43
+
44
+ How a call works: the product API resource targets the agent's `cmd`
45
+ resource; the product code reads the script from the storage, sends it with
46
+ the caller's input on stdin, and parses stdout as JSON (`parse_stdout`). A
47
+ non-zero exit returns `{error, retcode}`; invalid JSON returns
48
+ `{error: "invalid JSON in stdout", stdout}`.
49
+
50
+ Contract:
51
+
52
+ - Any executable source. With a shebang (`#!/usr/bin/env python3`,
53
+ `#!/bin/bash`…) it runs with that interpreter; without one it runs through
54
+ the shell (pipes, redirects and `&&` work).
55
+ - The object the caller sends arrives on **stdin** as JSON: parse it with
56
+ `jq`, `json.load`, `JSON.parse`.
57
+ - With `output` true (the default), **stdout must be valid JSON**: the
58
+ resource returns the decoded object.
59
+
60
+ Choose the resource shape with `input` and `output`:
61
+
62
+ | Shape | `input` | `output` | Example | Console shows |
63
+ | ---------------- | ------------------ | -------- | ---------------- | ------------------------------------- |
64
+ | output only | omit | true | stats, status | the output |
65
+ | input and output | `{field: default}` | true | transform, query | an input form and the output |
66
+ | input only | `{field: default}` | false | notify, log | an input form; the call replies empty |
67
+ | run | omit | false | ping, restart | a button |
68
+
69
+ `input` maps field names (`^[A-Za-z_][A-Za-z0-9_]*$`) to default values
70
+ (string, number or boolean), e.g. `{"threshold": 80, "label": "warm"}`: the
71
+ console renders typed fields and callers may override each one. Omit it for
72
+ a free-form payload passed to stdin as is.
73
+
74
+ Example, output only (`scripts/disk.sh`):
75
+
76
+ ```sh
77
+ #!/bin/bash
78
+ df -P / | awk 'NR==2 {printf "{\"used_pct\": %d, \"avail_kb\": %d}\n", $5, $4}'
79
+ ```
80
+
81
+ Example, input and output (`scripts/ping.py`, `input: {"host": "8.8.8.8"}`):
82
+
83
+ ```python
84
+ #!/usr/bin/env python3
85
+ import json, subprocess, sys
86
+ host = json.load(sys.stdin).get("host", "8.8.8.8")
87
+ ok = subprocess.run(["ping", "-c", "1", "-W", "2", host], capture_output=True).returncode == 0
88
+ print(json.dumps({"host": host, "reachable": ok}))
89
+ ```
90
+
91
+ ## Device scripts
92
+
93
+ Creating one (`resource_create` type `device_script`, body `{content}`)
94
+ writes the file in the agent's scripts folder, makes it executable and
95
+ reloads the scripts extension (`$scripts/reload`), so the resource is
96
+ callable at once. `resource_list` shows each script with the schema it
97
+ describes; `resource_get` adds its source.
98
+
99
+ Contract:
100
+
101
+ - Executable with a shebang.
102
+ - Called with `--describe`, it prints a JSON object with `input` and/or
103
+ `output` sample objects, e.g. `{"input": {"path": ""}, "output": {"size": 0}}`:
104
+ that becomes the resource's schema.
105
+ - Called normally, the input object arrives on stdin as JSON; stdout is
106
+ parsed as JSON. An object's keys become the output fields; any other value
107
+ appears under `output`. A non-zero exit is an error.
108
+
109
+ ## Writing JSON safely
110
+
111
+ Build JSON with a tool, never by pasting values into a string: a hostname or
112
+ a log line with a quote or a backslash breaks it. Use `jq -n --arg v "$x"
113
+ '{v: $v}'`, or Python's `json.dumps`. Output landing under `output` (device
114
+ scripts) or an `invalid JSON` error (product scripts) means the JSON was
115
+ malformed.
@@ -0,0 +1,116 @@
1
+ # ThinRemote: devices, agent and products
2
+
3
+ ThinRemote manages fleets of machines (gateways, servers, edge boxes) on a
4
+ Thinger.io server. Each machine runs **thinr-agent**, which connects to the
5
+ server as a device and lets you operate it remotely: files, commands,
6
+ scripts, monitoring, logs.
7
+
8
+ Almost everything is a **resource type** behind the generic tools
9
+ `resource_list` / `_get` / `_create` / `_update` / `_delete` and
10
+ `resource_call` (its actions), described by `resource_describe`: the
11
+ server's types (device, product, bucket, property, token, alarm…; read the
12
+ server's `concepts` guide with `doc_read`) and ThinRemote's, below. The one
13
+ tool of its own is `device_exec`, for shell commands.
14
+
15
+ Tool names below omit the `thinr_` prefix.
16
+
17
+ ## The device and its agent
18
+
19
+ A ThinRemote device is a Generic (IOTMP) device whose firmware is the agent.
20
+ The agent runs as an OS user (root for a system install), so what you can do
21
+ on the machine is what that user can do. `resource_get` with type `device`
22
+ adds what the agent reports when it is connected: `agent.system_info`
23
+ (hostname, OS, architecture, user…), `agent.fs_base_path`, and `agent.where`,
24
+ a map of where everything below is. Check them before assuming write access
25
+ to system paths.
26
+
27
+ What the agent offers, and how to reach it:
28
+
29
+ | Need | How |
30
+ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
31
+ | Run a shell command | `device_exec` with `device` (output and exit code; `timeout` in s) |
32
+ | List a folder | `resource_list` type `device_fs`, parent the device, `filter: {"path": "/var/log"}` |
33
+ | Read a file | `resource_get` type `device_fs`, parent the device, id the path |
34
+ | Write a file / create a folder | `resource_create` (or `_update`) type `device_fs`, body `{"content": "…"}` / `{"type": "directory"}` |
35
+ | Delete a file or folder | `resource_delete` type `device_fs` |
36
+ | Move, or copy binaries from/to this machine | `resource_call` type `device_fs`, action `move`, `push` or `pull` |
37
+ | CPU, memory, disk, network, load: now | `device_resource_call` resource `monitoring` |
38
+ | The same, as history | `bucket_data_read` bucket `monitoring` with `device` (fields such as `cpu.usage`, `memory.usage`, `disk.root.usage`, `load.1m`) |
39
+ | A reusable, typed operation on one device | type `device_script` (see `thinremote-scripts`) |
40
+ | Call what the agent or a script exposes | `device_resource_list`, `device_resource_call` |
41
+ | Update the agent | `resource_call` type `device`, action `agent_update`, body `{"action": "check"}` or `{"action": "apply"}` |
42
+ | Install the agent on a new machine | `resource_call` type `device`, action `agent_install` (a shell one-liner; no id) |
43
+
44
+ Paths are absolute on the device. `/` on its own lists the agent's working
45
+ directory (normally the root, reported as `fs_base_path`); nothing is
46
+ confined to it.
47
+
48
+ Built-in agent resources (`cmd`, `monitoring`, `system`, `update`, `agent`)
49
+ keep those names: a device script with one of them would hide the real one,
50
+ so creating it is refused unless forced.
51
+
52
+ ## Products: one configuration for a fleet
53
+
54
+ A product groups the devices of a fleet and holds what they share. ThinRemote
55
+ products are created with `resource_create` type `product` and come with
56
+ `config.type: "thinremote"` and a `monitoring` bucket that stores each
57
+ device's `monitoring` resource every 60 s (the history above; a device of
58
+ another kind of product has no history unless its product gets that bucket). On top of the
59
+ server's product, ThinRemote keeps these types, all with the product as
60
+ parent:
61
+
62
+ | Type | What it is | Where it lives | Guide |
63
+ | -------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------- |
64
+ | `product_script` | Code run on any device of the product, kept in one place | file storage named like the product, `scripts/`; one product API resource | `thinremote-scripts` |
65
+ | `product_playbook` | Ordered steps rolled out across devices; actions `validate`, `run`, `rollout` | same storage, `playbooks/<name>.yaml`, indexed in the `playbooks` property | `thinremote-playbooks` |
66
+ | `product_metric` | Metrics for thinr's terminal dashboard | `dashboard_metrics` product property | `thinremote-metrics-logs` |
67
+ | `product_log_source` | Log sources for thinr (dashboard, `thinr device logs`) | `logs` product property | `thinremote-metrics-logs` |
68
+
69
+ Creating one that already exists fails (update it instead), and updating one
70
+ that does not exist fails too, as with the server's types.
71
+
72
+ Deleting a product with `resource_delete` also deletes its file storage (its
73
+ scripts and playbooks).
74
+
75
+ Devices join a product when they are created with `product`, with
76
+ `resource_update` (`{"product": "<id>"}`), or by autoprovisioning (type
77
+ `product_profile_autoprovision`).
78
+
79
+ ## Fleet operations
80
+
81
+ | Need | How |
82
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
83
+ | Same command on many devices, once | `device_exec` with `devices`, `product` (and `group`) or `all: true`; `dry_run` lists the targets first |
84
+ | Same file on every device | a playbook with a `write` step (to a temporary file, then `move`), rolled out |
85
+ | A repeatable, versioned change with checks | a playbook: action `run` on one device, then action `rollout` |
86
+ | A value from every device | a product script + `device_resource_call` per device, or a metric |
87
+ | Which devices are online | `resource_list` type `device` with `filter: {"product": "<id>"}` |
88
+ | Fleet health at a glance | `bucket_data_read` bucket `monitoring` without `device`, `group_by` device |
89
+
90
+ Offline devices are skipped unless asked (`include_offline`). Commands on
91
+ several devices and rollouts act at once: confirm with the user first.
92
+
93
+ ## Recipes
94
+
95
+ **Bring a new machine in.** `resource_create` type `product` (if the fleet
96
+ has none) → `resource_call` type `device` action `agent_install` → run the
97
+ command on the machine → the device appears (autoprovisioned or created) →
98
+ `resource_update` its `product` if needed → `resource_get` to confirm
99
+ `agent.system_info`.
100
+
101
+ **Expose a health check for the whole fleet.** Write it once as a product
102
+ script (`resource_create` type `product_script`, output-only, JSON on
103
+ stdout) → call it on a device with `device_resource_call` to check it →
104
+ register a metric over it (`resource_create` type `product_metric`) so
105
+ thinr's dashboard aggregates it across the fleet.
106
+
107
+ **Change a config file on every machine, safely.** `resource_describe` type
108
+ `product_playbook` (the format) → draft a playbook that writes the file and
109
+ restarts the service → action `validate` → `resource_create` to save it →
110
+ action `run` on one device with `check: true`, then for real → action
111
+ `rollout` with a small `batch_size`.
112
+
113
+ **Investigate one machine.** `resource_get` type `device` (agent info) →
114
+ `device_resource_call` resource `monitoring` → `device_exec` for specifics →
115
+ `resource_get` type `device_fs` for log files, or the product's log sources
116
+ with `thinr device logs`.
@@ -2,7 +2,8 @@
2
2
  import WebSocket from 'ws';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { TIMEOUTS } from './constants.js';
5
- import { device } from './paths.js';
5
+ import { serverWebSocketURL } from '@thinger-io/cli-core/config';
6
+ import { device } from '@thinger-io/cli-core/paths';
6
7
 
7
8
  /**
8
9
  * One-shot execution of a shell command on a device over the IOTMP
@@ -80,7 +81,7 @@ export class CommandStream {
80
81
  const { server, apiUser, deviceId, cmd, timeout } = this.opts;
81
82
  const params = new URLSearchParams({ cmd });
82
83
  if (timeout && timeout > 0) params.set('timeout', String(timeout));
83
- return `wss://${server}${device(apiUser, deviceId)}/resources/${this.resource}?${params}`;
84
+ return `${serverWebSocketURL(server)}${device(apiUser, deviceId)}/resources/${this.resource}?${params}`;
84
85
  }
85
86
 
86
87
  _openSocket() {
package/lib/console.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import WebSocket from 'ws';
2
- import { requireConfig } from './config.js';
3
- import { ensureFreshToken } from './auth.js';
4
- import api from './api.js';
5
- import { warning, error as errorStyle } from './format.js';
6
- import { device } from './paths.js';
2
+ import { requireConfig, serverWebSocketURL } from '@thinger-io/cli-core/config';
3
+ import { ensureFreshToken } from '@thinger-io/cli-core/auth';
4
+ import api from '@thinger-io/cli-core/api';
5
+ import { warning, error as errorStyle } from '@thinger-io/cli-core/format';
6
+ import { device } from '@thinger-io/cli-core/paths';
7
7
 
8
8
  /**
9
9
  * Creates a WebSocket connection to a device terminal. Returns a
@@ -27,7 +27,7 @@ export async function connectToDeviceConsole(deviceId, user) {
27
27
  const token = (await ensureFreshToken()) || config.token;
28
28
 
29
29
  const sessionId = createUUID();
30
- const wsUrl = `wss://${config.server}${device(user, deviceId)}/resources/$terminal/${sessionId}?raw=1`;
30
+ const wsUrl = `${serverWebSocketURL(config.server)}${device(user, deviceId)}/resources/$terminal/${sessionId}?raw=1`;
31
31
  const ws = new WebSocket(wsUrl, {
32
32
  headers: { Authorization: `Bearer ${token}` },
33
33
  });
package/lib/constants.js CHANGED
@@ -4,21 +4,11 @@
4
4
  * Central home for the numeric knobs the CLI scatters otherwise.
5
5
  * Keep every "magic number" that has meaning beyond its immediate call
6
6
  * site here so we can tune them in one place (and grep for them when
7
- * debugging a stuck transfer or a missed OAuth poll).
7
+ * debugging a stuck transfer).
8
8
  */
9
9
 
10
10
  /** Timeouts. All values in ms unless the constant name ends in `_SECONDS`. */
11
11
  export const TIMEOUTS = {
12
- /** Default axios request timeout for control-plane HTTP calls. */
13
- API_DEFAULT_MS: 10_000,
14
-
15
- /**
16
- * Refresh a JWT access token this close to (or past) its `exp`
17
- * before opening a WebSocket. WS handshakes lack the
18
- * refresh-on-401 retry that api.js gives HTTP requests.
19
- */
20
- TOKEN_EXPIRY_SLACK_MS: 30_000,
21
-
22
12
  /** POSTs to device resources (cmd, update, etc.) — mid-weight calls. */
23
13
  DEVICE_RESOURCE_CALL_MS: 30_000,
24
14
 
@@ -43,9 +33,6 @@ export const TIMEOUTS = {
43
33
  */
44
34
  EXEC_EXTRA_GRACE_MS: 5_000,
45
35
 
46
- /** Hard ceiling for the OAuth device-code flow — 10 minutes. */
47
- OAUTH_DEVICE_CODE_MS: 10 * 60 * 1000,
48
-
49
36
  /**
50
37
  * Default command timeout (in *seconds*) for user-facing exec
51
38
  * flows: CLI `device exec`, `product exec`, playbook `exec`, MCP
@@ -76,8 +63,3 @@ export const PROXY_PORTS = {
76
63
  MIN: 50_000,
77
64
  MAX: 51_000,
78
65
  };
79
-
80
- /** OAuth device-flow client identity registered for this CLI. */
81
- export const OAUTH = {
82
- CLIENT_ID: '3b40164d28730e416cbd',
83
- };
@@ -1,9 +1,9 @@
1
1
  // @ts-check
2
2
  import WebSocket from 'ws';
3
- import { readConfig } from '../config.js';
4
- import { refreshToken } from '../auth.js';
3
+ import { readConfig, serverWebSocketURL } from '@thinger-io/cli-core/config';
4
+ import { refreshToken } from '@thinger-io/cli-core/auth';
5
5
  import { debugCount, debugLog } from '../debug-log.js';
6
- import { v2 } from '../paths.js';
6
+ import { v2 } from '@thinger-io/cli-core/paths';
7
7
 
8
8
  // Single shared websocket to /v2/users/{user}/events. Both fleet
9
9
  // monitoring and product metrics live on the same endpoint with the
@@ -109,9 +109,9 @@ class EventStream {
109
109
  if (this.disposed) return;
110
110
  const config = readConfig();
111
111
  if (!config?.server || !config?.token || !config?.username) return;
112
- const url = `wss://${config.server}${v2(config.username)}/events?authorization=${config.token}`;
112
+ const url = `${serverWebSocketURL(config.server)}${v2(config.username)}/events?authorization=${config.token}`;
113
113
  debugLog('ws:events', 'connecting', {
114
- endpoint: `wss://${config.server}${v2(config.username)}/events`,
114
+ endpoint: `${serverWebSocketURL(config.server)}${v2(config.username)}/events`,
115
115
  pending_subs: this.subs.size,
116
116
  });
117
117
  const ws = new WebSocket(url);
package/lib/device-api.js CHANGED
@@ -1,10 +1,10 @@
1
- import api from './api.js';
2
- import { requireConfig, resolveUser } from './config.js';
1
+ import api from '@thinger-io/cli-core/api';
2
+ import { requireConfig, resolveUser } from '@thinger-io/cli-core/config';
3
3
  import { BYTES, TIMEOUTS } from './constants.js';
4
4
  import { CommandStream } from './command-stream.js';
5
- import { ensureFreshToken, refreshToken } from './auth.js';
5
+ import { ensureFreshToken, refreshToken } from '@thinger-io/cli-core/auth';
6
6
  import { Readable } from 'node:stream';
7
- import { device } from './paths.js';
7
+ import { device } from '@thinger-io/cli-core/paths';
8
8
 
9
9
  // Directory endpoints on the agent expect a trailing slash so it can
10
10
  // distinguish "list this directory" from "read a file with this name".
package/lib/guides.js ADDED
@@ -0,0 +1,41 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * The guides thinr ships for agents (served by `thinr_doc_read` and as MCP
5
+ * resources, next to the server's documentation). They describe what thinr
6
+ * implements on top of the server — the agent, product scripts, playbooks,
7
+ * the product dashboard — so they live and change with this code. Tool
8
+ * descriptions stay short and point here for the how-to.
9
+ */
10
+ const guide = (name) => new URL(`../docs/guides/${name}.md`, import.meta.url);
11
+
12
+ export const guides = [
13
+ {
14
+ id: 'thinremote',
15
+ title: 'ThinRemote: devices, agent and products',
16
+ description:
17
+ 'Start here: what the agent exposes and the tool for each need, what a ThinRemote product holds, fleet operations and recipes.',
18
+ file: guide('thinremote'),
19
+ },
20
+ {
21
+ id: 'thinremote-scripts',
22
+ title: 'ThinRemote scripts',
23
+ description:
24
+ 'Device and product scripts: when to use each, the stdin/stdout JSON contract, resource shapes and examples.',
25
+ file: guide('thinremote-scripts'),
26
+ },
27
+ {
28
+ id: 'thinremote-playbooks',
29
+ title: 'ThinRemote playbooks',
30
+ description:
31
+ 'Write, validate, test on one device and roll out playbooks: document format, variables, actions, batches and reports.',
32
+ file: guide('thinremote-playbooks'),
33
+ },
34
+ {
35
+ id: 'thinremote-metrics-logs',
36
+ title: 'ThinRemote fleet metrics and log sources',
37
+ description:
38
+ "Per-product metrics and log sources read by thinr's terminal dashboard and `thinr device logs` (not the web console's dashboards): fields, aggregations, presets.",
39
+ file: guide('thinremote-metrics-logs'),
40
+ },
41
+ ];