@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/lib/property.js DELETED
@@ -1,75 +0,0 @@
1
- import { resolveUser } from './config.js';
2
- import { device } from './paths.js';
3
- import api from './api.js';
4
- import { apiError } from './errors.js';
5
- import { info, success } from './format.js';
6
-
7
- export async function getDeviceProperties(deviceId, user) {
8
- try {
9
- const response = await api.get(`${device(user, deviceId)}/properties`);
10
- return response.data;
11
- } catch (error) {
12
- throw apiError(error, {
13
- notFound: `Device not found: ${deviceId} (account ${resolveUser(user)})`,
14
- });
15
- }
16
- }
17
-
18
- export async function getDeviceProperty(deviceId, propertyId, user) {
19
- try {
20
- const response = await api.get(`${device(user, deviceId)}/properties/${propertyId}`);
21
- return response.data.value;
22
- } catch (error) {
23
- throw apiError(error, {
24
- notFound: `Property not found: ${propertyId} on device ${deviceId}`,
25
- });
26
- }
27
- }
28
-
29
- // PUT so the operation is idempotent — missing properties are created,
30
- // existing ones are overwritten wholesale.
31
- export async function setDeviceProperty(deviceId, propertyId, value, user) {
32
- try {
33
- const response = await api.put(`${device(user, deviceId)}/properties/${propertyId}`, {
34
- value,
35
- });
36
- return response.data;
37
- } catch (error) {
38
- throw apiError(error, {
39
- notFound: `Device not found: ${deviceId} (account ${resolveUser(user)})`,
40
- });
41
- }
42
- }
43
-
44
- export function formatDeviceProperties(deviceId, properties) {
45
- let formattedProperties = '\nProperties of device ' + info(deviceId) + ':\n';
46
-
47
- properties.forEach((property) => {
48
- formattedProperties += `- ${property.property}\n`;
49
- });
50
-
51
- return formattedProperties;
52
- }
53
-
54
- export function formatDeviceProperty(deviceId, propertyId, propertyValue) {
55
- let formattedValue =
56
- '\nValue of property ' + info(propertyId) + ' for device ' + info(deviceId) + ':\n';
57
- const flattenedValue = flatten(propertyValue);
58
- for (const key in flattenedValue) {
59
- formattedValue += `${key}: ${success(flattenedValue[key])}\n`;
60
- }
61
- return formattedValue;
62
- }
63
-
64
- function flatten(obj, prefix = '', res = {}) {
65
- for (const key in obj) {
66
- const val = obj[key];
67
- const path = prefix ? `${prefix}.${key}` : key;
68
- if (typeof val === 'object' && val !== null) {
69
- flatten(val, path, res);
70
- } else {
71
- res[path] = val;
72
- }
73
- }
74
- return res;
75
- }
package/lib/resource.js DELETED
@@ -1,151 +0,0 @@
1
- import { resolveUser } from './config.js';
2
- import { device } from './paths.js';
3
- import api from './api.js';
4
- import { apiError } from './errors.js';
5
- import { info, warning, hint } from './format.js';
6
-
7
- // Drops internal resources whose keys start with `$`; those belong to
8
- // the agent's own framework (cmd, fs, scripts…) and shouldn't surface
9
- // through the user-facing resource list.
10
- export async function getDeviceResources(deviceId, options = {}) {
11
- const user = resolveUser(options.user);
12
-
13
- try {
14
- // `/resources/api` merges firmware resources with API resources
15
- // inherited from the product profile, which `/resources` alone omits.
16
- const response = await api.get(`${device(user, deviceId)}/resources/api`);
17
- return Object.fromEntries(
18
- Object.entries(response.data).filter(([key]) => !key.startsWith('$')),
19
- );
20
- } catch (error) {
21
- throw apiError(error, {
22
- notFound: `Device not found: ${deviceId} (account ${user})`,
23
- });
24
- }
25
- }
26
-
27
- /**
28
- * Read the current value of a resource via GET. Appropriate for
29
- * output-only (fn=3) or fn=4 resources when you only want the latest
30
- * sample, not to trigger the input side.
31
- */
32
- export async function readDeviceResource(deviceId, resourceId, options = {}) {
33
- const user = resolveUser(options.user);
34
- try {
35
- const response = await api.get(`${device(user, deviceId)}/resources/${resourceId}`);
36
- return response.data;
37
- } catch (error) {
38
- throw apiError(error, {
39
- notFound: `Resource not found: ${resourceId} on device ${deviceId} (account ${user})`,
40
- });
41
- }
42
- }
43
-
44
- /**
45
- * Introspect a single resource. Returns the `{in, out}` schema the
46
- * agent advertises for that resource, e.g. for `cmd`:
47
- * { in: { cmd: "", mode: "api", timeout: 30 }, out: { stdout: "", stderr: "", retcode: 0 } }
48
- *
49
- * Read-only resources (fn=3) only return `out`; input-only (fn=2) only
50
- * return `in`; fn=1 may return an empty object. Not every agent/firmware
51
- * exposes /api — callers should tolerate 404/501.
52
- */
53
- export async function getDeviceResourceApi(deviceId, resourceId, options = {}) {
54
- const user = resolveUser(options.user);
55
- try {
56
- const response = await api.get(`${device(user, deviceId)}/resources/${resourceId}/api`);
57
- return response.data;
58
- } catch (error) {
59
- throw apiError(error, {
60
- notFound: `Resource not found: ${resourceId} on device ${deviceId} (account ${user})`,
61
- });
62
- }
63
- }
64
-
65
- export async function executeDeviceResource(deviceId, resourceId, inputs, options = {}) {
66
- const user = resolveUser(options.user);
67
-
68
- try {
69
- const response = await api.post(
70
- `${device(user, deviceId)}/resources/${resourceId}`,
71
- inputs,
72
- );
73
- return response.data;
74
- } catch (error) {
75
- throw apiError(error, {
76
- notFound: `Resource not found: ${resourceId} on device ${deviceId} (account ${user})`,
77
- });
78
- }
79
- }
80
-
81
- /**
82
- * Decide GET vs POST based on whether the caller passed real inputs.
83
- * No inputs (undefined, null, or an empty object) → GET, which reads
84
- * the output side of fn=3/4 resources without triggering the input
85
- * handler. Otherwise POST with the payload.
86
- */
87
- export async function callDeviceResource(deviceId, resourceId, inputs, options = {}) {
88
- const hasInputs =
89
- inputs !== undefined &&
90
- inputs !== null &&
91
- !(typeof inputs === 'object' && !Array.isArray(inputs) && Object.keys(inputs).length === 0);
92
- return hasInputs
93
- ? executeDeviceResource(deviceId, resourceId, inputs, options)
94
- : readDeviceResource(deviceId, resourceId, options);
95
- }
96
-
97
- /**
98
- * Fetch the resource list and, in parallel, introspect each resource's
99
- * `/api` schema. Returns an array with `{name, fn, in?, out?}` so both
100
- * the CLI and the MCP can render the same information.
101
- */
102
- export async function listDeviceResourcesWithSchemas(deviceId, options = {}) {
103
- const resources = await getDeviceResources(deviceId, options);
104
- const names = Object.keys(resources);
105
- const entries = await Promise.all(
106
- names.map(async (name) => {
107
- const fn = resources[name]?.fn;
108
- try {
109
- const schema = await getDeviceResourceApi(deviceId, name, options);
110
- return { name, fn, ...schema };
111
- } catch {
112
- return { name, fn };
113
- }
114
- }),
115
- );
116
- return entries;
117
- }
118
-
119
- /**
120
- * Format the resource entries produced by listDeviceResourcesWithSchemas
121
- * (`{name, fn, in?, out?}`). Shared between CLI (with chalk) and any
122
- * other human-facing surface.
123
- */
124
- export function formatDeviceResourcesWithSchemas(deviceId, entries) {
125
- let out = `\nResources for device ${info(deviceId)}:\n`;
126
- if (!entries || entries.length === 0) {
127
- return out + warning('No resources found for this device.\n');
128
- }
129
- for (const e of entries) {
130
- const kind = getResourceType({ fn: e.fn });
131
- out += `\n- ${info(e.name)} ${hint('[' + kind + ']')}\n`;
132
- if (e.in !== undefined) out += ` in: ${JSON.stringify(e.in)}\n`;
133
- if (e.out !== undefined) out += ` out: ${JSON.stringify(e.out)}\n`;
134
- }
135
- return out;
136
- }
137
-
138
- function getResourceType(value) {
139
- switch (value.fn) {
140
- case 1:
141
- return 'no parameters';
142
- case 2:
143
- return 'input';
144
- case 3:
145
- return 'output';
146
- case 4:
147
- return 'input/output';
148
- default:
149
- return 'unknown';
150
- }
151
- }
package/lib/storage.js DELETED
@@ -1,117 +0,0 @@
1
- import api from './api.js';
2
- import { apiError } from './errors.js';
3
- import { v1, v2 } from './paths.js';
4
-
5
- /**
6
- * Helpers for ThinRemote file storages — independent buckets that the
7
- * platform exposes alongside products. The product script workflow
8
- * uses them to host the actual JS files; storages are also useful on
9
- * their own (config blobs, firmware artefacts, etc.).
10
- *
11
- * Functions return raw data and throw `apiError` on failure, except
12
- * the `*Exists` and `delete*` helpers which use boolean return values
13
- * so orchestration code can stay branch-light.
14
- */
15
-
16
- export async function getStorage(storageId, user) {
17
- try {
18
- const res = await api.get(`${v1(user)}/storages/${storageId}`);
19
- return res.data || {};
20
- } catch (e) {
21
- throw apiError(e, { notFound: `Storage not found: ${storageId}` });
22
- }
23
- }
24
-
25
- export async function storageExists(storageId, user) {
26
- try {
27
- await api.get(`${v1(user)}/storages/${storageId}`);
28
- return true;
29
- } catch (e) {
30
- if (e.response?.status === 404) return false;
31
- throw apiError(e);
32
- }
33
- }
34
-
35
- export async function createStorage(
36
- storageId,
37
- user,
38
- { name, description, public: isPublic = false } = {},
39
- ) {
40
- try {
41
- await api.post(`${v1(user)}/storages`, {
42
- storage: storageId,
43
- name: name || storageId,
44
- description: description || `Storage for ${storageId}`,
45
- public: isPublic,
46
- });
47
- } catch (e) {
48
- throw apiError(e);
49
- }
50
- }
51
-
52
- export async function deleteStorage(storageId, user) {
53
- try {
54
- await api.delete(`${v1(user)}/storages/${storageId}`);
55
- return true;
56
- } catch (e) {
57
- if (e.response?.status === 404) return false;
58
- throw apiError(e);
59
- }
60
- }
61
-
62
- export async function listStorageFiles(storageId, user) {
63
- try {
64
- const res = await api.get(`${v1(user)}/storages/${storageId}/files`);
65
- return res.data || [];
66
- } catch (e) {
67
- if (e.response?.status === 404) return [];
68
- throw apiError(e);
69
- }
70
- }
71
-
72
- /**
73
- * Read a file as raw text (no JSON parsing). The agent stores script
74
- * bodies as `text/plain` — parsing them as JSON would corrupt them.
75
- */
76
- export async function readStorageFile(storageId, filePath, user) {
77
- try {
78
- const res = await api.get(`${v2(user)}/storages/${storageId}/files/${filePath}`, {
79
- transformResponse: [(x) => x],
80
- });
81
- return res.data;
82
- } catch (e) {
83
- throw apiError(e, { notFound: `File not found in storage "${storageId}": ${filePath}` });
84
- }
85
- }
86
-
87
- export async function storageFileExists(storageId, filePath, user) {
88
- try {
89
- await api.get(`${v2(user)}/storages/${storageId}/files/${filePath}`);
90
- return true;
91
- } catch (e) {
92
- if (e.response?.status === 404) return false;
93
- throw apiError(e);
94
- }
95
- }
96
-
97
- export async function writeStorageFile(storageId, filePath, content, user) {
98
- try {
99
- await api.put(
100
- `${v2(user)}/storages/${storageId}/files/${filePath}?overwrite=true`,
101
- content,
102
- { headers: { 'Content-Type': 'text/plain' } },
103
- );
104
- } catch (e) {
105
- throw apiError(e);
106
- }
107
- }
108
-
109
- export async function deleteStorageFile(storageId, filePath, user) {
110
- try {
111
- await api.delete(`${v2(user)}/storages/${storageId}/files/${filePath}`);
112
- return true;
113
- } catch (e) {
114
- if (e.response?.status === 404) return false;
115
- throw apiError(e);
116
- }
117
- }