@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.
- package/CHANGELOG.md +86 -0
- package/README.md +117 -123
- package/bin/thinr.js +43 -105
- package/commands/_shared.js +10 -90
- package/commands/device/_shared.js +2 -2
- package/commands/device/exec.js +2 -2
- package/commands/device/fs.js +2 -2
- package/commands/device/index.js +5 -19
- package/commands/device/logs.js +4 -4
- package/commands/device/status.js +1 -1
- package/commands/device/transfer.js +2 -2
- package/commands/device/update.js +8 -2
- package/commands/fleet/upgrade.js +3 -3
- package/commands/playbook/run.js +2 -2
- package/commands/playbook/validate.js +2 -2
- package/commands/product/_shared.js +1 -1
- package/commands/product/exec.js +2 -2
- package/commands/product/fs.js +2 -2
- package/commands/product/index.js +4 -11
- package/commands/product/logs.js +2 -2
- package/commands/product/metric.js +2 -2
- package/commands/product/playbook.js +5 -5
- package/commands/product/property.js +4 -4
- package/commands/product/push.js +2 -2
- package/commands/product/resource.js +4 -4
- package/commands/product/status.js +3 -3
- package/dist/dashboard.js +12 -12
- package/docs/guides/thinremote-metrics-logs.md +59 -0
- package/docs/guides/thinremote-playbooks.md +133 -0
- package/docs/guides/thinremote-scripts.md +115 -0
- package/docs/guides/thinremote.md +116 -0
- package/lib/command-stream.js +3 -2
- package/lib/console.js +6 -6
- package/lib/constants.js +1 -19
- package/lib/dashboard/event-stream.js +5 -5
- package/lib/device-api.js +4 -4
- package/lib/guides.js +41 -0
- package/lib/identity.js +75 -0
- package/lib/local-files.js +54 -0
- package/lib/mcp/{tools-install.js → agent-install.js} +12 -61
- package/lib/mcp/helpers.js +5 -25
- package/lib/mcp/playbook-ops.js +393 -0
- package/lib/mcp/registry.js +9 -43
- package/lib/mcp/resource-hooks.js +53 -0
- package/lib/mcp/resource-types.js +857 -0
- package/lib/mcp/tools-exec.js +217 -177
- package/lib/monitoring.js +3 -3
- package/lib/playbook/actions.js +30 -18
- package/lib/playbook/expression.js +1 -4
- package/lib/playbook/fleet.js +4 -1
- package/lib/playbook/loader.js +16 -8
- package/lib/playbook/runner.js +40 -8
- package/lib/playbook/schema.js +108 -34
- package/lib/product/log-presets.js +1 -1
- package/lib/product/logs.js +2 -2
- package/lib/product-orchestrator.js +14 -9
- package/lib/product.js +29 -227
- package/lib/proxy.js +4 -4
- package/lib/status.js +13 -5
- package/package.json +4 -2
- package/commands/device/delete.js +0 -53
- package/commands/device/list.js +0 -55
- package/commands/device/property.js +0 -52
- package/commands/device/resource.js +0 -52
- package/commands/logout.js +0 -47
- package/commands/product/list.js +0 -86
- package/commands/product/props.js +0 -125
- package/commands/profile.js +0 -173
- package/lib/alarms.js +0 -223
- package/lib/api.js +0 -89
- package/lib/auth.js +0 -428
- package/lib/bucket.js +0 -82
- package/lib/concurrency.js +0 -52
- package/lib/config.js +0 -230
- package/lib/devices.js +0 -60
- package/lib/errors.js +0 -65
- package/lib/format.js +0 -114
- package/lib/mcp/server.js +0 -67
- package/lib/mcp/tools-alarms.js +0 -507
- package/lib/mcp/tools-buckets.js +0 -99
- package/lib/mcp/tools-devices.js +0 -235
- package/lib/mcp/tools-filesystem.js +0 -281
- package/lib/mcp/tools-logs.js +0 -225
- package/lib/mcp/tools-playbook.js +0 -210
- package/lib/mcp/tools-products.js +0 -1557
- package/lib/mcp/tools-profile.js +0 -635
- package/lib/mcp/tools-profiles.js +0 -30
- package/lib/mcp/tools-properties.js +0 -79
- package/lib/mcp/tools-resources.js +0 -78
- package/lib/mcp/tools-scripts.js +0 -212
- package/lib/mcp/tools-tokens.js +0 -369
- package/lib/output.js +0 -114
- package/lib/paths.js +0 -32
- package/lib/profile.js +0 -657
- package/lib/property.js +0 -75
- package/lib/resource.js +0 -151
- package/lib/storage.js +0 -117
- 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
|
-
}
|