@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/config.js
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Profile-based configuration store.
|
|
7
|
-
*
|
|
8
|
-
* On disk the config file looks like:
|
|
9
|
-
* {
|
|
10
|
-
* "default": "<profile-name>",
|
|
11
|
-
* "profiles": {
|
|
12
|
-
* "<profile-name>": { server, username, token, refresh_token, ... }
|
|
13
|
-
* }
|
|
14
|
-
* }
|
|
15
|
-
*
|
|
16
|
-
* Profile names default to the server hostname so login against a new
|
|
17
|
-
* server automatically creates an isolated profile.
|
|
18
|
-
*
|
|
19
|
-
* Legacy single-profile files (top-level server/username/token) are
|
|
20
|
-
* detected on read and migrated to the new layout transparently. The
|
|
21
|
-
* migration uses the legacy `server` field as the profile name.
|
|
22
|
-
*
|
|
23
|
-
* The "active" profile is the one all helpers operate on. It is picked
|
|
24
|
-
* (in order): explicit override via setActiveProfile(), the THINR_PROFILE
|
|
25
|
-
* environment variable, the file's `default` field.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
let activeProfileOverride = null;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Resolve the on-disk config path, creating the parent directory on
|
|
32
|
-
* first access. Internal — every consumer goes through readStore /
|
|
33
|
-
* writeStore so they never have to know the location.
|
|
34
|
-
*/
|
|
35
|
-
function getConfigPath() {
|
|
36
|
-
const homeDir = os.homedir();
|
|
37
|
-
const configDir = path.join(homeDir, '.config', 'thinr-cli');
|
|
38
|
-
|
|
39
|
-
if (!fs.existsSync(configDir)) {
|
|
40
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return path.join(configDir, 'config.json');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function emptyStore() {
|
|
47
|
-
return { default: null, profiles: {} };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function readStore() {
|
|
51
|
-
const configPath = getConfigPath();
|
|
52
|
-
if (!fs.existsSync(configPath)) return emptyStore();
|
|
53
|
-
let raw;
|
|
54
|
-
try {
|
|
55
|
-
raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
56
|
-
} catch {
|
|
57
|
-
return emptyStore();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Legacy format: top-level {server, username, token, ...}. Migrate to
|
|
61
|
-
// a profile keyed by the server hostname.
|
|
62
|
-
if (raw && typeof raw === 'object' && !raw.profiles && raw.server) {
|
|
63
|
-
const name = String(raw.server);
|
|
64
|
-
return { default: name, profiles: { [name]: { ...raw } } };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!raw || typeof raw !== 'object' || !raw.profiles) return emptyStore();
|
|
68
|
-
return raw;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function writeStore(store) {
|
|
72
|
-
const configPath = getConfigPath();
|
|
73
|
-
fs.writeFileSync(configPath, JSON.stringify(store, null, 2), 'utf8');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Override the active profile for the current process. Pass null to fall
|
|
78
|
-
* back to env var / file default. Used by the CLI entry point when the
|
|
79
|
-
* user passes --profile.
|
|
80
|
-
*/
|
|
81
|
-
export function setActiveProfile(name) {
|
|
82
|
-
activeProfileOverride = name || null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Resolve the active profile name, or null if no profile exists.
|
|
87
|
-
*/
|
|
88
|
-
export function getActiveProfile() {
|
|
89
|
-
if (activeProfileOverride) return activeProfileOverride;
|
|
90
|
-
if (process.env.THINR_PROFILE) return process.env.THINR_PROFILE;
|
|
91
|
-
const store = readStore();
|
|
92
|
-
if (store.default && store.profiles[store.default]) return store.default;
|
|
93
|
-
// Fall back to the only profile if there's exactly one.
|
|
94
|
-
const names = Object.keys(store.profiles);
|
|
95
|
-
return names.length === 1 ? names[0] : null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* List all profile names.
|
|
100
|
-
*/
|
|
101
|
-
export function listProfiles() {
|
|
102
|
-
return Object.keys(readStore().profiles);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Get a specific profile by name (or undefined if missing).
|
|
107
|
-
*/
|
|
108
|
-
export function getProfile(name) {
|
|
109
|
-
const store = readStore();
|
|
110
|
-
return store.profiles[name];
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Set the persisted default profile.
|
|
115
|
-
*/
|
|
116
|
-
export function setDefaultProfile(name) {
|
|
117
|
-
const store = readStore();
|
|
118
|
-
if (!store.profiles[name]) {
|
|
119
|
-
throw new Error(`Profile not found: ${name}`);
|
|
120
|
-
}
|
|
121
|
-
store.default = name;
|
|
122
|
-
writeStore(store);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Remove a profile by name. Returns true if removed.
|
|
127
|
-
*/
|
|
128
|
-
export function deleteProfile(name) {
|
|
129
|
-
const store = readStore();
|
|
130
|
-
if (!store.profiles[name]) return false;
|
|
131
|
-
delete store.profiles[name];
|
|
132
|
-
if (store.default === name) {
|
|
133
|
-
const remaining = Object.keys(store.profiles);
|
|
134
|
-
store.default = remaining[0] || null;
|
|
135
|
-
}
|
|
136
|
-
writeStore(store);
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Check if any profile is configured.
|
|
142
|
-
*/
|
|
143
|
-
export function configExists() {
|
|
144
|
-
const store = readStore();
|
|
145
|
-
if (!Object.keys(store.profiles).length) return false;
|
|
146
|
-
return getActiveProfile() != null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Read the active profile's data.
|
|
151
|
-
* @returns {Object} Active profile fields (server, username, token, ...) or {}
|
|
152
|
-
*/
|
|
153
|
-
export function readConfig() {
|
|
154
|
-
const store = readStore();
|
|
155
|
-
const name = getActiveProfile();
|
|
156
|
-
if (!name) return {};
|
|
157
|
-
return store.profiles[name] || {};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Read the active profile and assert that it has the fields every API
|
|
162
|
-
* call needs (server, username, token). Throws a tagged error so the
|
|
163
|
-
* MCP and CLI surfaces both classify the failure as `not_configured`
|
|
164
|
-
* via classifyError. Replaces the same boilerplate that used to live
|
|
165
|
-
* at the top of every helper in lib/.
|
|
166
|
-
*/
|
|
167
|
-
/**
|
|
168
|
-
* The account an API call is addressed to: an explicit one when acting on
|
|
169
|
-
* behalf of another account, the active profile's own otherwise. Single
|
|
170
|
-
* source of truth — every path in lib/paths.js goes through it.
|
|
171
|
-
*/
|
|
172
|
-
export function resolveUser(user) {
|
|
173
|
-
return user || requireConfig().username;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function requireConfig() {
|
|
177
|
-
const config = readConfig();
|
|
178
|
-
if (!config.token || !config.server || !config.username) {
|
|
179
|
-
const err = new Error('Not configured. Run thinr without parameters to set up.');
|
|
180
|
-
err.code = 'not_configured';
|
|
181
|
-
throw err;
|
|
182
|
-
}
|
|
183
|
-
return config;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Write a fresh profile from the supplied data. The profile name is
|
|
188
|
-
* picked (in order): an explicit override set via setActiveProfile()
|
|
189
|
-
* (used by `profile add` to scope the write to a named profile), the
|
|
190
|
-
* supplied `server`, or the currently active profile name. Becomes the
|
|
191
|
-
* default if there isn't one yet.
|
|
192
|
-
*/
|
|
193
|
-
export function writeConfig(config) {
|
|
194
|
-
const store = readStore();
|
|
195
|
-
const name = activeProfileOverride || (config && config.server) || getActiveProfile();
|
|
196
|
-
if (!name) {
|
|
197
|
-
throw new Error('Cannot write config without a server / profile name');
|
|
198
|
-
}
|
|
199
|
-
store.profiles[name] = { ...config };
|
|
200
|
-
if (!store.default) store.default = name;
|
|
201
|
-
writeStore(store);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Merge fields into the active profile.
|
|
206
|
-
*/
|
|
207
|
-
export function mergeConfig(newConfig) {
|
|
208
|
-
const store = readStore();
|
|
209
|
-
const name = getActiveProfile();
|
|
210
|
-
if (!name) {
|
|
211
|
-
// No active profile yet: fall back to writeConfig semantics.
|
|
212
|
-
return writeConfig(newConfig);
|
|
213
|
-
}
|
|
214
|
-
store.profiles[name] = { ...store.profiles[name], ...newConfig };
|
|
215
|
-
writeStore(store);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Delete the entire config (all profiles).
|
|
220
|
-
*/
|
|
221
|
-
export function deleteConfig() {
|
|
222
|
-
const configPath = getConfigPath();
|
|
223
|
-
if (!fs.existsSync(configPath)) return true;
|
|
224
|
-
try {
|
|
225
|
-
fs.unlinkSync(configPath);
|
|
226
|
-
return true;
|
|
227
|
-
} catch {
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
}
|
package/lib/devices.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import api from './api.js';
|
|
2
|
-
import { apiError } from './errors.js';
|
|
3
|
-
import { v1 } from './paths.js';
|
|
4
|
-
|
|
5
|
-
export async function getDevices(filter, user = null) {
|
|
6
|
-
let devices = [];
|
|
7
|
-
|
|
8
|
-
let queryParametersString = '';
|
|
9
|
-
for (const [key, value] of Object.entries(filter)) {
|
|
10
|
-
if (value) queryParametersString += key + '=' + value + '&';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const count = 50;
|
|
15
|
-
const url = `${v1(user)}/devices?count=${count}&${queryParametersString}`;
|
|
16
|
-
let index = 0;
|
|
17
|
-
let res_length = 0;
|
|
18
|
-
|
|
19
|
-
do {
|
|
20
|
-
const response = await api.get(`${url}index=${index}`);
|
|
21
|
-
res_length = response.data.length;
|
|
22
|
-
index += res_length;
|
|
23
|
-
devices = devices.concat(response.data);
|
|
24
|
-
} while (res_length === count);
|
|
25
|
-
|
|
26
|
-
return devices;
|
|
27
|
-
} catch (error) {
|
|
28
|
-
const product = filter && filter.product;
|
|
29
|
-
const notFound = product ? `Product not found: ${product}` : 'Devices not found';
|
|
30
|
-
throw apiError(error, { notFound });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Server-side metadata (assigned product, description, asset group,
|
|
35
|
-
// connection state…). For device-side data — system_info, monitoring —
|
|
36
|
-
// hit the agent resources directly instead.
|
|
37
|
-
export async function getDevice(deviceId, user = null) {
|
|
38
|
-
try {
|
|
39
|
-
const response = await api.get(`${v1(user)}/devices/${deviceId}`);
|
|
40
|
-
return response.data || {};
|
|
41
|
-
} catch (error) {
|
|
42
|
-
throw apiError(error, { notFound: `Device not found: ${deviceId}` });
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Idempotent: returns true if the device record was removed, false if it
|
|
47
|
-
// was already absent. Other failures (auth, network) propagate as apiError.
|
|
48
|
-
export async function deleteDevice(deviceId, user = null) {
|
|
49
|
-
try {
|
|
50
|
-
await api.delete(`${v1(user)}/devices/${deviceId}`);
|
|
51
|
-
return true;
|
|
52
|
-
} catch (error) {
|
|
53
|
-
if (error.response?.status === 404) return false;
|
|
54
|
-
throw apiError(error);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function filterActiveDevices(devices) {
|
|
59
|
-
return devices.filter((device) => device.connection && device.connection.active === true);
|
|
60
|
-
}
|
package/lib/errors.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Normalize an axios / fetch-style error into a single Error with a stable
|
|
3
|
-
* `.code` and `.status`. Replaces the repeated if-response/if-request
|
|
4
|
-
* blocks in status.js / property.js / resource.js / device.js.
|
|
5
|
-
*
|
|
6
|
-
* Pass `messages.notFound` to override the 404 message with something
|
|
7
|
-
* that names the missing entity (e.g. "Device not found: abc").
|
|
8
|
-
*/
|
|
9
|
-
export function apiError(error, messages = {}) {
|
|
10
|
-
// Errors normalized upstream (the tagged 'unauthorized' from a failed
|
|
11
|
-
// token refresh, inputError, ...) carry a stable string code and no
|
|
12
|
-
// axios envelope: pass them through so the code survives.
|
|
13
|
-
if (
|
|
14
|
-
error &&
|
|
15
|
-
typeof error.code === 'string' &&
|
|
16
|
-
!error.isAxiosError &&
|
|
17
|
-
!error.response &&
|
|
18
|
-
!error.request
|
|
19
|
-
) {
|
|
20
|
-
return error;
|
|
21
|
-
}
|
|
22
|
-
if (error && error.response) {
|
|
23
|
-
const status = error.response.status;
|
|
24
|
-
if (status === 404) {
|
|
25
|
-
const err = new Error(messages.notFound || 'Not found');
|
|
26
|
-
err.code = 'not_found';
|
|
27
|
-
err.status = 404;
|
|
28
|
-
return err;
|
|
29
|
-
}
|
|
30
|
-
if (status === 401 || status === 403) {
|
|
31
|
-
const err = new Error(
|
|
32
|
-
messages.unauthorized ||
|
|
33
|
-
'Unauthorized. Your token may have expired. Please reconfigure.',
|
|
34
|
-
);
|
|
35
|
-
err.code = 'unauthorized';
|
|
36
|
-
err.status = status;
|
|
37
|
-
return err;
|
|
38
|
-
}
|
|
39
|
-
const err = new Error(`Server error: ${status} ${error.response.statusText || ''}`.trim());
|
|
40
|
-
err.code = 'server_error';
|
|
41
|
-
err.status = status;
|
|
42
|
-
return err;
|
|
43
|
-
}
|
|
44
|
-
if (error && error.request) {
|
|
45
|
-
const err = new Error('No response from server. Please check your connection.');
|
|
46
|
-
err.code = 'network_error';
|
|
47
|
-
return err;
|
|
48
|
-
}
|
|
49
|
-
const err = new Error(error?.message || String(error));
|
|
50
|
-
err.code = 'error';
|
|
51
|
-
return err;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Pre-tagged error for invalid / missing user input (CLI option,
|
|
56
|
-
* MCP tool argument, …). Picked up by `classifyError` so it surfaces
|
|
57
|
-
* as `code: 'input_error'` in both the CLI JSON envelope and the
|
|
58
|
-
* MCP error metadata, matching the same code the CLI's `printErr`
|
|
59
|
-
* uses for argument validation failures.
|
|
60
|
-
*/
|
|
61
|
-
export function inputError(message) {
|
|
62
|
-
const err = new Error(message);
|
|
63
|
-
err.code = 'input_error';
|
|
64
|
-
return err;
|
|
65
|
-
}
|
package/lib/format.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
|
|
4
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
5
|
-
// Theme — semantic wrappers over chalk so every CLI surface speaks
|
|
6
|
-
// the same visual language. New colours / a `--no-color` switch / a
|
|
7
|
-
// dark-on-light adjustment only have to be touched here.
|
|
8
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
/** Title / header text. */
|
|
11
|
-
export const label = (s) => chalk.bold(s);
|
|
12
|
-
/** Secondary / disabled / "—" text. */
|
|
13
|
-
export const muted = (s) => chalk.dim(s);
|
|
14
|
-
/** Hints, trailing metadata, footers, timestamps. */
|
|
15
|
-
export const hint = (s) => chalk.gray(s);
|
|
16
|
-
/** Positive states (online, ok, success). */
|
|
17
|
-
export const success = (s) => chalk.green(s);
|
|
18
|
-
/** Negative states (offline, failed, errored). */
|
|
19
|
-
export const error = (s) => chalk.red(s);
|
|
20
|
-
/** Caution states (timeout, at-risk thresholds). */
|
|
21
|
-
export const warning = (s) => chalk.yellow(s);
|
|
22
|
-
/** Decorative accents (separators, prompts). */
|
|
23
|
-
export const accent = (s) => chalk.cyan(s);
|
|
24
|
-
/** Neutral-but-noteworthy identifiers (device ids, resource names). */
|
|
25
|
-
export const info = (s) => chalk.blue(s);
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Format a duration in seconds as a human-readable string: `309d16h`,
|
|
29
|
-
* `42d15h`, `17m`, `42s`, etc. Returns `—` for invalid/missing values.
|
|
30
|
-
*
|
|
31
|
-
* @param {number | null | undefined} seconds
|
|
32
|
-
* @returns {string}
|
|
33
|
-
*/
|
|
34
|
-
export function formatUptime(seconds) {
|
|
35
|
-
if (seconds == null || !Number.isFinite(seconds) || seconds < 0) return '—';
|
|
36
|
-
const d = Math.floor(seconds / 86400);
|
|
37
|
-
const h = Math.floor((seconds % 86400) / 3600);
|
|
38
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
39
|
-
if (d > 0) return `${d}d${h}h`;
|
|
40
|
-
if (h > 0) return `${h}h${m}m`;
|
|
41
|
-
if (m > 0) return `${m}m`;
|
|
42
|
-
return `${Math.floor(seconds)}s`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Format seconds elapsed as a "time ago" label: `5s ago`, `3m ago`,
|
|
47
|
-
* `2h ago`, `4d ago`. Returns `—` for invalid/missing values.
|
|
48
|
-
*
|
|
49
|
-
* @param {number | null | undefined} seconds
|
|
50
|
-
* @returns {string}
|
|
51
|
-
*/
|
|
52
|
-
export function formatAgo(seconds) {
|
|
53
|
-
if (seconds == null || !Number.isFinite(seconds) || seconds < 0) return '—';
|
|
54
|
-
if (seconds < 60) return `${Math.floor(seconds)}s ago`;
|
|
55
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
56
|
-
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
57
|
-
return `${Math.floor(seconds / 86400)}d ago`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Render a percentage value coloured by threshold: green below 70 %,
|
|
62
|
-
* yellow below 90 %, red at or above 90 %. Returns a dim `—` when the
|
|
63
|
-
* value is missing. Formatted with no decimal places so columns in
|
|
64
|
-
* cli-table3 stay aligned.
|
|
65
|
-
*
|
|
66
|
-
* @param {number | null | undefined} v
|
|
67
|
-
* @returns {string}
|
|
68
|
-
*/
|
|
69
|
-
export function colorPct(v) {
|
|
70
|
-
if (v == null || !Number.isFinite(v)) return muted('—');
|
|
71
|
-
const s = `${v.toFixed(0)}%`;
|
|
72
|
-
if (v >= 90) return error(s);
|
|
73
|
-
if (v >= 70) return warning(s);
|
|
74
|
-
return success(s);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Format a duration in seconds as a short ETA label: `12s`, `3m15s`,
|
|
79
|
-
* `2h30m`. Returns `—` for non-finite / missing values.
|
|
80
|
-
*
|
|
81
|
-
* @param {number | null | undefined} seconds
|
|
82
|
-
* @returns {string}
|
|
83
|
-
*/
|
|
84
|
-
export function formatETA(seconds) {
|
|
85
|
-
if (seconds == null || !Number.isFinite(seconds) || seconds < 0) return '—';
|
|
86
|
-
if (seconds < 60) return `${Math.max(1, Math.ceil(seconds))}s`;
|
|
87
|
-
if (seconds < 3600) {
|
|
88
|
-
const m = Math.floor(seconds / 60);
|
|
89
|
-
const s = Math.ceil(seconds % 60);
|
|
90
|
-
return s > 0 ? `${m}m${s}s` : `${m}m`;
|
|
91
|
-
}
|
|
92
|
-
const h = Math.floor(seconds / 3600);
|
|
93
|
-
const m = Math.ceil((seconds % 3600) / 60);
|
|
94
|
-
return m > 0 ? `${h}h${m}m` : `${h}h`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Format a byte count as a human-readable string picking the largest
|
|
99
|
-
* unit where the value is ≥ 1. `2 bytes`, `12.3 KB`, `145 MB`,
|
|
100
|
-
* `2.4 GB`. Returns `—` for invalid/missing values.
|
|
101
|
-
*
|
|
102
|
-
* @param {number | null | undefined} bytes
|
|
103
|
-
* @returns {string}
|
|
104
|
-
*/
|
|
105
|
-
export function formatBytes(bytes) {
|
|
106
|
-
if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return '—';
|
|
107
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
108
|
-
const kb = bytes / 1024;
|
|
109
|
-
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
110
|
-
const mb = kb / 1024;
|
|
111
|
-
if (mb < 1024) return `${mb.toFixed(mb >= 100 ? 0 : 1)} MB`;
|
|
112
|
-
const gb = mb / 1024;
|
|
113
|
-
return `${gb.toFixed(gb >= 100 ? 0 : 2)} GB`;
|
|
114
|
-
}
|
package/lib/mcp/server.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
-
import { readConfig } from '../config.js';
|
|
6
|
-
import { setBaseURL } from '../api.js';
|
|
7
|
-
import { classifyError } from '../output.js';
|
|
8
|
-
import { tools } from './registry.js';
|
|
9
|
-
import { withProfile } from './helpers.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Start a generic MCP server for ThinRemote.
|
|
13
|
-
* All tools accept device (when relevant) plus optional user/profile parameters.
|
|
14
|
-
*
|
|
15
|
-
* @param {{ version?: string }} [opts] `version` is echoed back in the
|
|
16
|
-
* MCP `initialize` response's `serverInfo.version`. Caller is expected
|
|
17
|
-
* to pass the CLI's `package.json#version` so the two never drift.
|
|
18
|
-
*/
|
|
19
|
-
export async function startMCPServer({ version = 'unknown' } = {}) {
|
|
20
|
-
const config = readConfig();
|
|
21
|
-
if (config.server) {
|
|
22
|
-
setBaseURL(`https://${config.server}`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const server = new Server(
|
|
26
|
-
{
|
|
27
|
-
name: 'thinr',
|
|
28
|
-
version,
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
capabilities: { tools: {} },
|
|
32
|
-
},
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
36
|
-
tools: tools.map(({ handler: _handler, ...t }) => t),
|
|
37
|
-
}));
|
|
38
|
-
|
|
39
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
40
|
-
const name = request.params.name;
|
|
41
|
-
const args = request.params.arguments || {};
|
|
42
|
-
const tool = tools.find((t) => t.name === name);
|
|
43
|
-
if (!tool) {
|
|
44
|
-
return {
|
|
45
|
-
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
46
|
-
isError: true,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
return await withProfile(args.profile, () => tool.handler(args));
|
|
51
|
-
} catch (error) {
|
|
52
|
-
// Surface the same {message, code} pair the CLI emits in JSON
|
|
53
|
-
// mode so MCP clients can pattern-match on `[code]` instead of
|
|
54
|
-
// doing fuzzy matches on the human message.
|
|
55
|
-
const { message, code } = classifyError(error);
|
|
56
|
-
return {
|
|
57
|
-
content: [{ type: 'text', text: `Error [${code}]: ${message}` }],
|
|
58
|
-
isError: true,
|
|
59
|
-
_meta: { code, message },
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const transport = new StdioServerTransport();
|
|
65
|
-
await server.connect(transport);
|
|
66
|
-
console.error('[thinr-mcp] Server started');
|
|
67
|
-
}
|