@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,91 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0] - 2026-10-14
4
+
5
+ Incompatible change of the MCP tools: thinr is now the generic core it
6
+ shares with the `thinger` CLI, `@thinger-io/cli-core` 0.3.0, plus what only
7
+ a device with the agent can do. From 84 tools to 24, and from about 27,600
8
+ to about 8,000 tokens in `tools/list`. Also incompatible for playbooks: the
9
+ step actions `mv` and `rm` are now `move` and `delete`.
10
+
11
+ ### Changed
12
+
13
+ - The generic core (API client, auth, profiles, REST helpers, generic
14
+ commands and MCP tools) comes from `@thinger-io/cli-core`. thinr supplies
15
+ its identity (`lib/identity.js`) and its own commands, types and tools.
16
+ Profiles and `THINR_*` variables are unchanged.
17
+ - Server resources (devices, products, buckets, tokens, alarms, properties,
18
+ product profiles…) go through `thinr_resource_list` / `_get` / `_create` /
19
+ `_update` / `_delete` / `_clone` / `_call` with a `type`, described by
20
+ `thinr_resource_describe`; device resources through
21
+ `thinr_device_resource_list` / `_call`.
22
+ - ThinRemote's own resources are resource types served by those same
23
+ tools: `device_fs` (a device's files and folders; actions `move`, `push`,
24
+ `pull`), `device_script`, `product_script`, `product_metric`,
25
+ `product_log_source` and `product_playbook` (actions `validate`, `run`,
26
+ `rollout`). Creating one that exists fails, as does updating one that
27
+ does not (a `device_fs` file is written either way). Actions on devices:
28
+ `agent_update` and `agent_install` (the install command).
29
+ - The one tool of thinr's own, `thinr_device_exec`, runs a command on one
30
+ device or on several (`devices`, `product` and `group`, or `all`: every
31
+ device of the ThinRemote products); offline devices are left out unless
32
+ `include_offline`, and named when listed by id; `dry_run` lists the
33
+ targets.
34
+ - Old tools and where they went: `exec` → `device_exec`; `read`, `write`,
35
+ `ls`, `rm`, `mv`, `mkdir`, `push`, `pull` → type `device_fs`;
36
+ `script_*` → type `device_script`; `product_script_*` → type
37
+ `product_script`; `product_metric_*` → type `product_metric`;
38
+ `product_logs_*` → type `product_log_source` (presets in its
39
+ description); `product_playbook_*`, `playbook_schema`,
40
+ `playbook_validate`, `playbook_run` → type `product_playbook`; `update` →
41
+ action `agent_update`; `agent_install_command` → action `agent_install`;
42
+ `product_exec` → `device_exec` with `product` (its `all`, which included
43
+ offline devices, is now `include_offline`); `monitoring` → the device
44
+ resource `monitoring` (live) and the bucket `monitoring` (history);
45
+ `product_write` → a playbook (`write` and `move` steps, rolled out);
46
+ `devices`, `device_info`, `products`, `product_create`… → the
47
+ `resource_*` tools; `profiles` → `profile_list`; `bucket_read` →
48
+ `bucket_data_read`.
49
+ - A playbook run needs one explicit target (`device`, `devices`, `group` or
50
+ `all`): never the whole product by omission. Its variables are checked
51
+ against what the playbook declares, inline ones too.
52
+ - From the MCP server, `push`, `pull` (and playbook `content_file`) only
53
+ reach files inside one local folder, `THINR_MCP_LOCAL_DIR` or where the
54
+ server started; `pull` replaces a file only when asked.
55
+ - Deleting a product also deletes its script storage.
56
+ - MCP calls with a `profile` argument are scoped to that call.
57
+ - `thinr logout` revokes the API token it created at a username/password
58
+ login (`--keep-token` to keep it) and accepts `--all`.
59
+ - Node 20 or later, as cli-core.
60
+
61
+ ### Added
62
+
63
+ - Guides for agents, served by `thinr_doc_read` and as MCP resources:
64
+ `thinremote`, `thinremote-scripts`, `thinremote-playbooks`,
65
+ `thinremote-metrics-logs`; MCP server instructions with a map of where
66
+ each thing is.
67
+ - Reading a device with the agent connected adds `agent.system_info`, its
68
+ file system base path and `agent.where`, the map of its tools.
69
+ - `thinr login --server --username --token` signs in without prompts.
70
+ - `thinr device info`, `create`, `set-product`, `property-set`;
71
+ `thinr product info`, `create`, `delete`; `thinr bucket list`, `read`.
72
+ - From cli-core: dashboards, storage files, bucket data, endpoints, alarm
73
+ rule tests and stats, the server's documentation as MCP resources.
74
+
75
+ ### Fixed
76
+
77
+ - Reading a device showed no system information: it asked the agent for
78
+ `system_info` instead of `system`.
79
+
80
+ ## [1.3.5] - 2026-09-24
81
+
82
+ ### Fixed
83
+
84
+ - `thinr_ls` describes what "/" actually resolves to: the agent process's
85
+ working directory, which is normally the filesystem root. 1.3.4 said it was
86
+ a base directory "usually /var/log", which was a stale service definition on
87
+ one device rather than how installs behave.
88
+
3
89
  ## [1.3.4] - 2026-09-24
4
90
 
5
91
  ### Fixed
package/README.md CHANGED
@@ -12,7 +12,7 @@ Two operating modes:
12
12
 
13
13
  - **CLI** — interactive or scripted device management from a shell.
14
14
  - **MCP server** — a stdio [Model Context Protocol][mcp] server that
15
- exposes the same operations as 80+ typed tools for AI clients like
15
+ exposes the same operations as 24 typed tools for AI clients like
16
16
  Claude Code, Claude Desktop and Cursor.
17
17
 
18
18
  Both modes share the same auth, profiles, and device API layer, so
@@ -159,6 +159,19 @@ thinr device list --json # JSON envelope with the raw API records
159
159
 
160
160
  Combine with the global `-u, --user` flag for admin impersonation.
161
161
 
162
+ #### Records — `info` / `create` / `delete` / `set-product`
163
+
164
+ ```bash
165
+ thinr device info <deviceId> # server record as JSON
166
+ thinr device create <deviceId> [-t Generic|MQTT|HTTP|Virtual|Subdevice] [-p product]
167
+ thinr device delete <deviceId> [-y]
168
+ thinr device set-product <deviceId> <productId> # "" to unassign
169
+ thinr device property-set <deviceId> <property> <json>
170
+ ```
171
+
172
+ `create` generates random credentials for Generic/MQTT devices unless
173
+ `-c` is given; they are shown once.
174
+
162
175
  #### `console`
163
176
 
164
177
  Open an interactive terminal on the device:
@@ -336,9 +349,9 @@ thinr product exec my-product "apt-get -y upgrade" -c 5 --timeout 600
336
349
  thinr product exec my-product "systemctl restart agent" -g staging
337
350
  ```
338
351
 
339
- Also exposed as an MCP tool (`thinr_product_exec`) with the same
340
- parameters, so agents can drive a fleet operation with a single call
341
- instead of looping `thinr_exec` per device.
352
+ Also exposed to MCP clients as `thinr_device_exec` with `product` (and
353
+ `group`), so agents run a fleet operation with a single call instead of
354
+ looping per device.
342
355
 
343
356
  #### `playbook`
344
357
 
@@ -417,25 +430,25 @@ thinr profile delete <name> [--json]
417
430
 
418
431
  ### `thinr logout`
419
432
 
420
- Remove the active profile's credentials and config:
433
+ Forget the active profile and revoke the API token `thinr` created for it
434
+ at a username/password login:
421
435
 
422
436
  ```bash
423
- thinr logout
437
+ thinr logout # active profile (or --profile <name>)
438
+ thinr logout --all # every profile
439
+ thinr logout --keep-token # leave the token on the server
424
440
  ```
425
441
 
426
- Other profiles are left untouched.
442
+ Other profiles are left untouched. `thinr login` re-authenticates the
443
+ active profile; `--server`, `--username` and `--token` skip the prompts.
427
444
 
428
445
  ---
429
446
 
430
447
  ## Part 2 — MCP server
431
448
 
432
- `thinr` ships an [MCP][mcp] server that exposes the CLI's device and
433
- fleet operations as a set of 80+ typed tools. AI clients (Claude Code,
434
- Claude Desktop, Cursor, and any other MCP-compatible host) can list
435
- devices, run shell commands, read/write files, call resources, manage
436
- products, author and roll out playbooks, triage alarms and rotate
437
- access tokens through the same API the CLI uses — with stable schemas
438
- and a consistent error contract.
449
+ `thinr` ships an [MCP][mcp] server for AI clients (Claude Code, Claude
450
+ Desktop, Cursor and any other MCP host): 24 tools that reach everything the
451
+ CLI does, through the same API, with stable schemas and one error contract.
439
452
 
440
453
  ### Starting the server
441
454
 
@@ -443,117 +456,91 @@ and a consistent error contract.
443
456
  thinr mcp
444
457
  ```
445
458
 
446
- The server speaks [MCP over stdio][mcp-stdio]. Every tool call carries
447
- its own `device`, `user` and `profile` arguments, so a single running
448
- server handles any device of any configured environment without a
449
- restart.
459
+ The server speaks [MCP over stdio][mcp-stdio]. Every tool call carries its
460
+ own `profile` and `user` arguments, so a single running server handles any
461
+ device of any configured environment without a restart.
450
462
 
451
463
  [mcp-stdio]: https://modelcontextprotocol.io/specification/server/transport#stdio
452
464
 
453
- ### Tool catalog
454
-
455
- Grouped by capability. Every tool accepts optional `device` (when
456
- relevant), `user` and `profile` arguments, so a single session can
457
- target any device, account or environment without restart.
458
-
459
- | Area | Tools |
460
- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
461
- | Discovery | `thinr_devices` (with optional `query` for regex filtering), `thinr_device_info`, `thinr_profiles` |
462
- | Provisioning and lifecycle | `thinr_agent_install_command`, `thinr_device_set_product`, `thinr_device_delete` |
463
- | Shell | `thinr_exec` (buffered), with streaming stdout/stderr |
464
- | Filesystem | `thinr_read`, `thinr_write`, `thinr_push`, `thinr_pull`, `thinr_ls`, `thinr_mkdir`, `thinr_rm`, `thinr_mv` |
465
- | Resources | `thinr_resource_list` (with `in`/`out` schemas), `thinr_resource_call` |
466
- | Properties (device) | `thinr_property_get`, `thinr_property_set` |
467
- | Scripts (device) | `thinr_script_list`, `thinr_script_write`, `thinr_script_delete` |
468
- | Monitoring and agent updates | `thinr_monitoring`, `thinr_bucket_read`, `thinr_update` |
469
- | Alarms | `thinr_alarm_instances`, `thinr_alarm_instance_stats`, `thinr_alarm_instance_get`, `thinr_alarm_instance_update`, `thinr_alarm_instance_delete`, `thinr_alarm_rules`, `thinr_alarm_rule_read/write/delete` |
470
- | Access tokens | User: `thinr_token_list`, `thinr_token_get`, `thinr_token_create`, `thinr_token_update`, `thinr_token_delete`. Device: `thinr_device_token_list`, `thinr_device_token_create`, `thinr_device_token_delete` |
471
- | Products | `thinr_products`, `thinr_product_create`, `thinr_product_update`, `thinr_product_delete`, `thinr_product_exec`, `thinr_product_write` |
472
- | Product scripts | `thinr_product_script_list`, `thinr_product_script_read`, `thinr_product_script_write`, `thinr_product_script_delete` |
473
- | Product properties | `thinr_product_property_get`, `thinr_product_property_set`, `thinr_product_property_delete` |
474
- | Product profile schema | `thinr_product_profile_api_{list,get,set,delete}`, `thinr_product_profile_bucket_{list,get,set,delete}`, `thinr_product_profile_property_{list,get,set,delete}` |
475
- | Product log sources | `thinr_product_logs_list`, `thinr_product_logs_add`, `thinr_product_logs_remove`, `thinr_product_logs_presets`, `thinr_product_logs_set_default` |
476
- | Product metrics | `thinr_product_metric_list`, `thinr_product_metric_set`, `thinr_product_metric_delete` |
477
- | Playbooks (authoring) | `thinr_playbook_schema`, `thinr_playbook_validate`, `thinr_playbook_run` (ad-hoc) |
478
- | Product playbooks | `thinr_product_playbook_list`, `thinr_product_playbook_read`, `thinr_product_playbook_write`, `thinr_product_playbook_run` (single device), `thinr_product_playbook_rollout` (fleet), `thinr_product_playbook_delete` |
479
-
480
- Full input/output schemas are published via standard MCP
481
- `list_tools`; the client will show them when you connect.
465
+ ### Tools
466
+
467
+ thinr is built on [`@thinger-io/cli-core`][cli-core], the core it shares
468
+ with the `thinger` CLI, and serves its 23 generic tools with the `thinr_`
469
+ prefix plus one of its own:
470
+
471
+ | Tools | What for |
472
+ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
473
+ | `thinr_resource_describe`, `_list`, `_get`, `_create`, `_update`, `_delete`, `_clone`, `_call` | Every resource type: the server's and ThinRemote's (below), and their actions |
474
+ | `thinr_device_resource_list`, `thinr_device_resource_call` | What a connected device exposes: agent resources (`monitoring`, `cmd`…) and its scripts |
475
+ | `thinr_bucket_data_read`, `thinr_bucket_data_write` | Time series, monitoring history included (bucket `monitoring`) |
476
+ | `thinr_storage_file_*`, `thinr_dashboard_publish`, `thinr_plugin_call`, `thinr_product_resource_call`, `thinr_device_data_send`, `thinr_device_overview` | Storages, web dashboards, plugins, product APIs, HTTP devices, account usage |
477
+ | `thinr_doc_read`, `thinr_profile_list` | Guides for agents (`thinremote`, `thinremote-scripts`, `thinremote-playbooks`, `thinremote-metrics-logs`) and profiles |
478
+ | `thinr_device_exec` | A shell command on one device, or on several at once (`devices`, `product`, `group`, `all`) |
479
+
480
+ ThinRemote adds its own resource types to the generic tools, so they are
481
+ listed, read, written and deleted like a device or a token:
482
+
483
+ | Type | Parent | What it is | Actions (`thinr_resource_call`) |
484
+ | -------------------- | ------- | ------------------------------------------------------------------------------ | ------------------------------- |
485
+ | `device_fs` | device | Files and folders of the device (id: the path) | `move`, `push`, `pull` |
486
+ | `device_script` | device | Scripts of the agent, each a resource of the device | |
487
+ | `product_script` | product | Scripts run on any device of the product, kept once in its storage | |
488
+ | `product_metric` | product | Metrics of thinr's terminal dashboard | |
489
+ | `product_log_source` | product | Log sources an operator streams from a device | |
490
+ | `product_playbook` | product | Playbooks: YAML steps validated, tried on one device and rolled out in batches | `validate`, `run`, `rollout` |
491
+
492
+ and two actions to the server's `device` type: `agent_update` (check for or
493
+ install a new agent) and `agent_install` (the command that installs the
494
+ agent on a new machine). Live monitoring is the device resource
495
+ `monitoring`; its history, the bucket `monitoring`.
496
+
497
+ Full input schemas come with the standard MCP `tools/list`, and
498
+ `thinr_resource_describe` gives each type's fields and actions.
499
+
500
+ [cli-core]: https://github.com/thinger-io/thinger-cli-core
482
501
 
483
502
  ### Authoring playbooks from natural language
484
503
 
485
- Playbooks are declarative YAML documents — steps like `exec`, `write`,
486
- `install_package`, `service`, plus a `target` block and a `vars`
487
- block — that the runner applies to a device or a fleet. With the MCP
488
- server connected, an AI assistant can turn a natural-language request
489
- into a validated playbook and store it on the relevant product
490
- without the user ever writing YAML by hand.
491
-
492
- The intended authoring loop is:
493
-
494
- 1. **Fetch the schema.** Call `thinr_playbook_schema` once at the
495
- start of the session to get the authoritative list of top-level
496
- fields, target block, step actions with their parameters, and the
497
- two accepted forms of the `vars` block (plain and extended).
498
- 2. **Draft the YAML.** Translate the user's request into a playbook
499
- draft. Use the extended `vars` form
500
- (`default` / `description` / `type` / `overridable` / `required`)
501
- whenever the user hints at knobs the caller may need to tune at
502
- run time.
503
- 3. **Validate and iterate.** Call `thinr_playbook_validate` with the
504
- draft. On failure, read the consolidated error message, fix the
505
- YAML, and call validate again. Never skip this step.
506
- 4. **Upload.** Once validation is clean, call
507
- `thinr_product_playbook_write` with the product id, a short name
508
- (letters / digits / underscore / dash), and the validated YAML.
509
- The tool re-validates server-side; never pass `skip_validation` to
510
- mask a real error.
511
- 5. **Pre-flight on one device.** Call
512
- `thinr_product_playbook_run` with a single `device` (plus
513
- `dry_run: true` or `check: true` for a read-only preview) to
514
- confirm the steps behave as expected.
515
- 6. **Roll out.** Finally, `thinr_product_playbook_rollout` rolls the
516
- playbook across the fleet in batches with a failure-rate kill
517
- switch, and writes a persistent report to `playbooks/runs/` in the
518
- product file storage.
519
-
520
- Example prompt the user can give the assistant:
521
-
522
- > "On product `edge-gateways`, create a playbook called
523
- > `nginx-upgrade` that updates the nginx package, renders
524
- > `/etc/nginx/nginx.conf` from a template, and restarts nginx only
525
- > if the config check passes. Make `worker_connections` an
526
- > overridable integer variable, default 1024. Then try it on
527
- > `edge-gw-17` in check mode."
528
-
529
- A well-behaved assistant translates this into:
530
-
531
- - `thinr_playbook_schema` → read the authoring contract.
532
- - Draft YAML → `thinr_playbook_validate` until clean.
533
- - `thinr_product_playbook_write` with `product="edge-gateways"`,
534
- `name="nginx-upgrade"`, and the validated content.
535
- - `thinr_product_playbook_run` with `device="edge-gw-17"` and
536
- `check: true` to preview what would change.
537
-
538
- After that a second message like "looks good, roll it out to staging"
539
- maps to `thinr_product_playbook_rollout` with the matching
540
- `group` / `filter` and a conservative `batch_size`.
504
+ Playbooks are YAML documents with ordered steps (`exec`, `write`, `move`,
505
+ `delete`, `script_install`…), a `target` and `vars`, that the agent applies
506
+ on each device. With the MCP server connected, an assistant can turn a
507
+ request into a validated playbook stored on the product, without anyone
508
+ writing YAML by hand:
509
+
510
+ 1. **Read the format.** `thinr_resource_describe` type `product_playbook`:
511
+ its `playbook_format` lists the document fields, the target, the two
512
+ forms of `vars` (plain and extended) and every step action.
513
+ 2. **Draft and validate.** Write the YAML and call `thinr_resource_call`
514
+ type `product_playbook`, action `validate`, with it in `body.content`;
515
+ fix what it reports until it is clean.
516
+ 3. **Save.** `thinr_resource_create` type `product_playbook`, parent the
517
+ product, id a short name, body `{content}`. It is validated again and
518
+ nothing is saved if invalid.
519
+ 4. **Pre-flight on one device.** Action `run` with the playbook's id and
520
+ `body.device`, with `dry_run: true` or `check: true` for a read-only
521
+ preview, then for real.
522
+ 5. **Roll out.** Action `rollout` runs it across the fleet in batches with a
523
+ failure-rate stop, and writes a report to `playbooks/runs/` in the
524
+ product storage.
525
+
526
+ Example prompt:
527
+
528
+ > "On product `edge-gateways`, create a playbook called `nginx-upgrade`
529
+ > that updates the nginx package, renders `/etc/nginx/nginx.conf` from a
530
+ > template, and restarts nginx only if the config check passes. Make
531
+ > `worker_connections` an overridable integer variable, default 1024. Then
532
+ > try it on `edge-gw-17` in check mode."
533
+
534
+ A second message like "looks good, roll it out to staging" maps to the
535
+ `rollout` action with `group` and a conservative `batch_size`.
541
536
 
542
537
  ### Per-call controls
543
538
 
544
- The server starts with no per-session pin: every tool call must say
545
- which device it's for, and may optionally override the account or
546
- profile for that single call.
547
-
548
- - **`device`** — required for any device-scoped tool. Use
549
- `thinr_devices` (optionally with `query` to filter by regex) to
550
- discover ids.
551
- - **`user`** — optional admin impersonation; falls back to the
552
- authenticated user of the active profile.
553
- - **`profile`** — optional. Switches the active profile for that
554
- single tool call so an agent can hop between prod and staging
555
- without restarting the server. List the configured profiles with
556
- `thinr_profiles`.
539
+ - **`profile`**: optional on every tool. Uses another configured server for
540
+ that call only; `thinr_profile_list` lists them.
541
+ - **`user`**: optional admin impersonation; defaults to the profile's user.
542
+ - Devices are named per call (`device`, or `parent` for a device's
543
+ resources): list them with `thinr_resource_list` type `device`.
557
544
 
558
545
  ### Integrating with Claude Code
559
546
 
@@ -563,9 +550,8 @@ Register the server once with the Claude Code CLI:
563
550
  claude mcp add thinr -s user -- thinr mcp
564
551
  ```
565
552
 
566
- That's it — a single MCP server entry handles every device of the
567
- active profile. Tool calls pass `device` (and optionally `user` /
568
- `profile`) per call.
553
+ That's it: a single MCP server entry handles every device of every
554
+ configured profile.
569
555
 
570
556
  ### Integrating with Claude Desktop, Cursor and other MCP hosts
571
557
 
@@ -625,13 +611,21 @@ Use `thinr profile use <name>` to change the persisted default, or
625
611
  ```
626
612
  thinr-cli/
627
613
  ├── bin/ # CLI entry point (thinr)
628
- ├── commands/ # Commander subcommand modules
629
- ├── lib/ # Core: api, auth, config, device-api, errors,
630
- │ # output, mcp-server, proxy, console, …
614
+ ├── commands/ # ThinRemote subcommands (agent, fleet, playbooks…)
615
+ ├── lib/ # ThinRemote modules: identity, device-api, proxy,
616
+ │ # console, playbooks, product scripts, MCP tools…
631
617
  ├── package.json
632
618
  └── README.md
633
619
  ```
634
620
 
621
+ The generic part — API client, auth, profiles, REST helpers, the generic
622
+ commands (login, logout, profile, device list/info/property/resource,
623
+ product list/property-\_, bucket) and the generic MCP tools — comes from
624
+ [@thinger-io/cli-core](https://github.com/thinger-io/cli-core), shared
625
+ with the `thinger` CLI. `lib/identity.js` is what makes it `thinr`: names,
626
+ `~/.config/thinr-cli`, `THINR\__`, the OAuth client and the product
627
+ template.
628
+
635
629
  Local development:
636
630
 
637
631
  ```bash
@@ -685,7 +679,7 @@ The script only touches resources it creates itself (identified by
685
679
  the `playbook-e2e-` product-id prefix). If a run is interrupted
686
680
  before the `finally` cleanup executes — e.g. the process is killed
687
681
  — list leftovers with `thinr product list --json` and drop each one
688
- through the MCP `thinr_product_delete` tool (from an AI client) or
682
+ through the MCP `thinr_resource_delete` tool with type `product` (from an AI client) or
689
683
  via a Node one-liner reusing `deleteProductWithStorage` from
690
684
  `lib/product.js`.
691
685
 
package/bin/thinr.js CHANGED
@@ -3,11 +3,22 @@
3
3
  import { readFileSync } from 'fs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { dirname, resolve } from 'path';
6
- import { Command } from 'commander';
7
6
  import figlet from 'figlet';
8
- import { accent, info } from '../lib/format.js';
9
- import { configExists, getActiveProfile, readConfig, setActiveProfile } from '../lib/config.js';
10
- import { detectJsonModeFromArgv } from '../lib/output.js';
7
+ import { defineIdentity } from '@thinger-io/cli-core/identity';
8
+ import { runCli } from '@thinger-io/cli-core/cli';
9
+ import { authenticate } from '@thinger-io/cli-core/auth';
10
+ import { configExists } from '@thinger-io/cli-core/config';
11
+ import { accent, info } from '@thinger-io/cli-core/format';
12
+ import { mcpInstructions, thinrIdentity } from '../lib/identity.js';
13
+ import { tools as mcpTools } from '../lib/mcp/registry.js';
14
+ import { resourceHooks } from '../lib/mcp/resource-hooks.js';
15
+ import { resourceActions, resourceTypes } from '../lib/mcp/resource-types.js';
16
+ import { guides } from '../lib/guides.js';
17
+ import { registerDeviceCommands } from '../commands/device/index.js';
18
+ import { registerProductCommands } from '../commands/product/index.js';
19
+ import { fleetCommand } from '../commands/fleet/index.js';
20
+ import { playbookCommand } from '../commands/playbook/index.js';
21
+ import { runDashboard } from '../commands/dashboard.js';
11
22
 
12
23
  // Single source of truth for the version: package.json. Avoids the drift
13
24
  // between `package.json#version` and a hardcoded string in this file.
@@ -15,110 +26,40 @@ const pkg = JSON.parse(
15
26
  readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8'),
16
27
  );
17
28
 
18
- // Pick up --profile early so subsequent config reads use the right one.
19
- // Commander parses options later, but the config is touched before that.
20
- for (let i = 2; i < process.argv.length; i++) {
21
- const a = process.argv[i];
22
- if (a === '--profile' && process.argv[i + 1]) {
23
- setActiveProfile(process.argv[i + 1]);
24
- break;
25
- }
26
- if (a.startsWith('--profile=')) {
27
- setActiveProfile(a.slice('--profile='.length));
28
- break;
29
- }
30
- }
31
-
32
- // Set JSON mode before any command runs so spinners/logs can opt out.
33
- detectJsonModeFromArgv();
34
- import { authenticate } from '../lib/auth.js';
35
- import { deviceCommand } from '../commands/device/index.js';
36
- import { fleetCommand } from '../commands/fleet/index.js';
37
- import { logoutCommand } from '../commands/logout.js';
38
- import { playbookCommand } from '../commands/playbook/index.js';
39
- import { productCommand } from '../commands/product/index.js';
40
- import { profileCommand } from '../commands/profile.js';
41
- import { runDashboard } from '../commands/dashboard.js';
42
- import { setBaseURL } from '../lib/api.js';
43
- import { startMCPServer } from '../lib/mcp/server.js';
44
-
45
29
  function displayBanner() {
46
30
  console.log(accent(figlet.textSync('thinr-cli', { horizontalLayout: 'full' })));
47
31
  console.log(info('Thin Remote CLI - Remote management for IoT devices'));
48
32
  console.log();
49
33
  }
50
34
 
51
- const program = new Command();
52
-
53
- program
54
- .name('thinr')
55
- .description('CLI for ThinRemote - Remote management for IoT devices')
56
- .version(pkg.version)
57
- .option('-u, --user <username>', 'API user override (admin impersonation)')
58
- .option('--profile <name>', 'Configuration profile to use (defaults to the saved default)');
35
+ defineIdentity(thinrIdentity);
59
36
 
60
- deviceCommand(program);
61
- fleetCommand(program);
62
- productCommand(program);
63
- playbookCommand(program);
64
- profileCommand(program);
65
- logoutCommand(program);
37
+ await runCli({
38
+ version: pkg.version,
39
+ mcpTools,
40
+ resourceHooks,
41
+ resourceTypes,
42
+ resourceActions,
43
+ docs: guides,
44
+ mcpInstructions,
45
+ register(program, groups) {
46
+ registerDeviceCommands(groups.device);
47
+ registerProductCommands(groups.product);
48
+ fleetCommand(program);
49
+ playbookCommand(program);
66
50
 
67
- program
68
- .command('login')
69
- .helpGroup('Profile:')
70
- .description('Re-authenticate the current profile (renew an expired session)')
71
- .action(async () => {
72
- try {
73
- // Pin the target to the active/default profile by name so the
74
- // refreshed credentials overwrite it in place, even when the
75
- // profile name differs from the server hostname.
76
- const name = getActiveProfile();
77
- if (name) setActiveProfile(name);
78
- await authenticate();
79
- console.log(info('Signed in.'));
80
- } catch (error) {
81
- console.error(`Error: ${error.message}`);
82
- process.exit(1);
83
- }
84
- });
85
-
86
- program
87
- .command('mcp')
88
- .helpGroup('Integration:')
89
- .description('Start the MCP server (stdio) for AI tool integration')
90
- .action(async () => {
91
- try {
92
- await startMCPServer({ version: pkg.version });
93
- } catch (error) {
94
- console.error(`Error: ${error.message}`);
95
- process.exit(1);
96
- }
97
- });
98
-
99
- // Handle help
100
- const originalHelp = program.help;
101
- program.help = function (cb) {
102
- displayBanner();
103
- return originalHelp.call(this, cb);
104
- };
105
-
106
- // initialize API base URL
107
- if (configExists()) {
108
- const config = readConfig();
109
- if (config.server) {
110
- // Initialize API with the server if configured
111
- setBaseURL(`https://${config.server}`);
112
- }
113
- }
114
-
115
- // Handle no command (just "thinr"): launch the dashboard when running
116
- // interactively. Non-TTY contexts (pipes, scripts, `thinr | cat`) fall
117
- // back to `--help` so they get something useful instead of an Ink crash.
118
- if (process.argv.length <= 2) {
119
- if (!process.stdout.isTTY) {
120
- program.help();
121
- } else {
51
+ const originalHelp = program.help;
52
+ program.help = function (cb) {
53
+ displayBanner();
54
+ return originalHelp.call(this, cb);
55
+ };
56
+ },
57
+ // Just `thinr`: launch the dashboard when running interactively (signing
58
+ // in first if there is no profile yet). Non-TTY contexts (pipes,
59
+ // scripts, `thinr | cat`) fall back to `--help` so they get something
60
+ // useful instead of an Ink crash.
61
+ async onNoCommand(program) {
62
+ if (!process.stdout.isTTY) program.help();
122
63
  if (!configExists()) {
123
64
  displayBanner();
124
65
  try {
@@ -129,8 +70,5 @@ if (process.argv.length <= 2) {
129
70
  }
130
71
  }
131
72
  await runDashboard();
132
- }
133
- } else {
134
- // Parse arguments - don't show banner for regular commands
135
- program.parse(process.argv);
136
- }
73
+ },
74
+ });