@thinremote/thinr-cli 1.2.0-alpha.1 → 1.3.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 +63 -1
- package/README.md +235 -30
- package/bin/thinr.js +43 -25
- package/commands/_shared.js +154 -0
- package/commands/dashboard.js +17 -0
- package/commands/device/_shared.js +17 -34
- package/commands/device/console.js +1 -0
- package/commands/device/delete.js +53 -0
- package/commands/device/exec.js +7 -6
- package/commands/device/fs.js +193 -0
- package/commands/device/index.js +9 -0
- package/commands/device/list.js +35 -23
- package/commands/device/logs.js +178 -0
- package/commands/device/property.js +30 -28
- package/commands/device/proxy.js +3 -0
- package/commands/device/resource.js +30 -29
- package/commands/device/status.js +35 -14
- package/commands/device/transfer.js +236 -0
- package/commands/device/update.js +15 -10
- package/commands/fleet/index.js +17 -0
- package/commands/fleet/upgrade.js +275 -0
- package/commands/logout.js +10 -11
- package/commands/playbook/_shared.js +12 -0
- package/commands/playbook/index.js +18 -0
- package/commands/playbook/run.js +302 -0
- package/commands/playbook/validate.js +38 -0
- package/commands/product/_shared.js +8 -0
- package/commands/product/exec.js +243 -0
- package/commands/product/fs.js +218 -0
- package/commands/product/index.js +35 -0
- package/commands/product/list.js +86 -0
- package/commands/product/logs.js +289 -0
- package/commands/product/metric.js +124 -0
- package/commands/product/playbook.js +911 -0
- package/commands/product/property.js +76 -0
- package/commands/product/props.js +125 -0
- package/commands/product/push.js +184 -0
- package/commands/product/resource.js +76 -0
- package/commands/product/status.js +213 -0
- package/commands/profile.js +76 -8
- package/dist/dashboard.js +5644 -0
- package/lib/agent-releases.js +59 -0
- package/lib/agent-versions.js +73 -0
- package/lib/alarms.js +252 -0
- package/lib/api.js +8 -2
- package/lib/auth.js +68 -20
- package/lib/bucket.js +87 -0
- package/lib/command-stream.js +213 -0
- package/lib/config.js +5 -3
- package/lib/console.js +19 -5
- package/lib/constants.js +83 -0
- package/lib/dashboard/event-stream.js +239 -0
- package/lib/dashboard/metric-aggregate.js +64 -0
- package/lib/dashboard/metric-filter.js +39 -0
- package/lib/debug-log.js +93 -0
- package/lib/device-api.js +131 -148
- package/lib/devices.js +18 -21
- package/lib/errors.js +12 -0
- package/lib/fleet-upgrade.js +106 -0
- package/lib/format.js +114 -0
- package/lib/mcp/registry.js +14 -0
- package/lib/mcp/tools-alarms.js +510 -0
- package/lib/mcp/tools-buckets.js +103 -0
- package/lib/mcp/tools-devices.js +33 -3
- package/lib/mcp/tools-exec.js +30 -17
- package/lib/mcp/tools-filesystem.js +102 -6
- package/lib/mcp/tools-install.js +153 -0
- package/lib/mcp/tools-logs.js +225 -0
- package/lib/mcp/tools-playbook.js +211 -0
- package/lib/mcp/tools-products.js +1226 -105
- package/lib/mcp/tools-profile.js +634 -0
- package/lib/mcp/tools-tokens.js +371 -0
- package/lib/monitoring.js +26 -1
- package/lib/playbook/actions.js +447 -0
- package/lib/playbook/expression.js +302 -0
- package/lib/playbook/fleet.js +175 -0
- package/lib/playbook/loader.js +247 -0
- package/lib/playbook/runner.js +300 -0
- package/lib/playbook/schema.js +209 -0
- package/lib/playbook/vars.js +209 -0
- package/lib/product/log-presets.js +196 -0
- package/lib/product/logs.js +485 -0
- package/lib/product-orchestrator.js +124 -0
- package/lib/product.js +523 -20
- package/lib/profile.js +665 -0
- package/lib/property.js +7 -40
- package/lib/proxy.js +9 -8
- package/lib/resource.js +10 -14
- package/lib/status.js +78 -37
- package/lib/tokens.js +374 -0
- package/package.json +14 -3
- package/commands/product.js +0 -407
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import {
|
|
3
|
+
getAlarmInstances,
|
|
4
|
+
getAlarmInstance,
|
|
5
|
+
getAlarmInstanceStats,
|
|
6
|
+
updateAlarmInstance,
|
|
7
|
+
deleteAlarmInstance,
|
|
8
|
+
getAlarmRules,
|
|
9
|
+
getAlarmRule,
|
|
10
|
+
createAlarmRule,
|
|
11
|
+
updateAlarmRule,
|
|
12
|
+
deleteAlarmRule,
|
|
13
|
+
parseAlarmState,
|
|
14
|
+
parseAlarmSeverity,
|
|
15
|
+
ALARM_STATE_LABEL,
|
|
16
|
+
ALARM_SEVERITY_LABEL,
|
|
17
|
+
ALARM_STATE,
|
|
18
|
+
} from '../alarms.js';
|
|
19
|
+
import { inputError } from '../errors.js';
|
|
20
|
+
|
|
21
|
+
// Helpers ------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function normalizeStateInput(value) {
|
|
24
|
+
if (value == null) return undefined;
|
|
25
|
+
const arr = Array.isArray(value) ? value : [value];
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const v of arr) {
|
|
28
|
+
const parsed = parseAlarmState(v);
|
|
29
|
+
if (parsed === undefined) {
|
|
30
|
+
throw inputError(
|
|
31
|
+
`Unknown alarm state "${v}". Valid: active, ack, latched, shelved, cleared.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
out.push(parsed);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeSeverityInput(value) {
|
|
40
|
+
if (value == null) return undefined;
|
|
41
|
+
const arr = Array.isArray(value) ? value : [value];
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const v of arr) {
|
|
44
|
+
const parsed = parseAlarmSeverity(v);
|
|
45
|
+
if (parsed === undefined) {
|
|
46
|
+
throw inputError(
|
|
47
|
+
`Unknown alarm severity "${v}". Valid: low, medium, high, critical.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
out.push(parsed);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function originLabel(inst) {
|
|
56
|
+
const origin = inst?.origin || {};
|
|
57
|
+
if (!origin.id && !origin.name) return '';
|
|
58
|
+
const id = origin.id || '';
|
|
59
|
+
const name = origin.name && origin.name !== id ? ` (${origin.name})` : '';
|
|
60
|
+
return ` ${id}${name}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ageMsToHuman(ms) {
|
|
64
|
+
if (!Number.isFinite(ms) || ms <= 0) return '';
|
|
65
|
+
const sec = Math.floor(ms / 1000);
|
|
66
|
+
if (sec < 60) return `${sec}s`;
|
|
67
|
+
const min = Math.floor(sec / 60);
|
|
68
|
+
if (min < 60) return `${min}m`;
|
|
69
|
+
const h = Math.floor(min / 60);
|
|
70
|
+
if (h < 24) return `${h}h`;
|
|
71
|
+
const d = Math.floor(h / 24);
|
|
72
|
+
return `${d}d`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function instanceAgeMs(inst) {
|
|
76
|
+
const v = inst?.created;
|
|
77
|
+
if (v == null) return 0;
|
|
78
|
+
if (typeof v === 'number') return Date.now() - v;
|
|
79
|
+
if (typeof v === 'object' && v.$date != null) {
|
|
80
|
+
const t = typeof v.$date === 'number' ? v.$date : Date.parse(v.$date);
|
|
81
|
+
return Number.isFinite(t) ? Date.now() - t : 0;
|
|
82
|
+
}
|
|
83
|
+
if (typeof v === 'string') {
|
|
84
|
+
const t = Date.parse(v);
|
|
85
|
+
return Number.isFinite(t) ? Date.now() - t : 0;
|
|
86
|
+
}
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderInstanceRow(inst) {
|
|
91
|
+
const id = inst?.instance || '?';
|
|
92
|
+
const rule = inst?.alarm?.rule || '?';
|
|
93
|
+
const sev = ALARM_SEVERITY_LABEL[inst?.severity ?? 0] || '?';
|
|
94
|
+
const state = ALARM_STATE_LABEL[inst?.state ?? 0] || '?';
|
|
95
|
+
const age = ageMsToHuman(instanceAgeMs(inst));
|
|
96
|
+
const ann = inst?.annotation ? ` note="${inst.annotation}"` : '';
|
|
97
|
+
return `[${state.padEnd(8)}] ${sev.padEnd(5)} ${id} rule=${rule}${originLabel(inst)}${age ? ` age=${age}` : ''}${ann}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Tools --------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
async function toolAlarmInstances(args) {
|
|
103
|
+
const state = normalizeStateInput(args.state);
|
|
104
|
+
const severity = normalizeSeverityInput(args.severity);
|
|
105
|
+
const list = await getAlarmInstances({
|
|
106
|
+
user: args.user,
|
|
107
|
+
count: Number.isFinite(args.count) && args.count > 0 ? Math.floor(args.count) : 100,
|
|
108
|
+
index: Number.isFinite(args.index) && args.index >= 0 ? Math.floor(args.index) : 0,
|
|
109
|
+
name: args.name,
|
|
110
|
+
state,
|
|
111
|
+
severity,
|
|
112
|
+
sort: args.sort,
|
|
113
|
+
order: args.order,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (list.length === 0) {
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: 'text', text: 'No alarm instances match the filter.' }],
|
|
119
|
+
isError: false,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const lines = list.map(renderInstanceRow);
|
|
124
|
+
return {
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: 'text',
|
|
128
|
+
text: `${list.length} alarm instance(s):\n${lines.join('\n')}`,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
isError: false,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function toolAlarmInstanceStats(args) {
|
|
136
|
+
const stats = await getAlarmInstanceStats({ user: args.user });
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: 'text', text: JSON.stringify(stats, null, 2) }],
|
|
139
|
+
isError: false,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function toolAlarmInstanceGet(args) {
|
|
144
|
+
if (!args.instance) throw inputError('instance is required');
|
|
145
|
+
const inst = await getAlarmInstance(args.instance, { user: args.user });
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: 'text', text: JSON.stringify(inst, null, 2) }],
|
|
148
|
+
isError: false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function toolAlarmInstanceUpdate(args) {
|
|
153
|
+
if (!args.instance) throw inputError('instance is required');
|
|
154
|
+
|
|
155
|
+
const body = {};
|
|
156
|
+
|
|
157
|
+
if (args.state !== undefined) {
|
|
158
|
+
const parsed = parseAlarmState(args.state);
|
|
159
|
+
if (parsed === undefined) {
|
|
160
|
+
throw inputError(
|
|
161
|
+
`Unknown alarm state "${args.state}". Valid: active, ack, latched, shelved, cleared.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
body.state = parsed;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (args.annotation !== undefined) {
|
|
168
|
+
body.annotation = String(args.annotation);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (args.reactivation_minutes !== undefined) {
|
|
172
|
+
const minutes = Number(args.reactivation_minutes);
|
|
173
|
+
if (!Number.isFinite(minutes) || minutes < 0) {
|
|
174
|
+
throw inputError('reactivation_minutes must be a non-negative number');
|
|
175
|
+
}
|
|
176
|
+
// The server expects the timespan with magnitude + value. Zero
|
|
177
|
+
// resets the scheduled reactivation, so we pass it through as-is.
|
|
178
|
+
body.reactivation = {
|
|
179
|
+
type: minutes > 0 ? 'timespan' : 'none',
|
|
180
|
+
timespan: { magnitude: 'minute', value: minutes },
|
|
181
|
+
};
|
|
182
|
+
} else if (args.reactivation !== undefined) {
|
|
183
|
+
body.reactivation = args.reactivation;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (Object.keys(body).length === 0) {
|
|
187
|
+
throw inputError(
|
|
188
|
+
'Nothing to update. Pass at least one of: state, annotation, reactivation_minutes.',
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const updated = await updateAlarmInstance(args.instance, body, { user: args.user });
|
|
193
|
+
const stateLabel = ALARM_STATE_LABEL[updated?.state ?? 0] || '?';
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: 'text',
|
|
198
|
+
text: `Updated ${args.instance} → state=${stateLabel}` +
|
|
199
|
+
(updated?.annotation ? `, annotation="${updated.annotation}"` : ''),
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
isError: false,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function toolAlarmInstanceDelete(args) {
|
|
207
|
+
if (!args.instance) throw inputError('instance is required');
|
|
208
|
+
await deleteAlarmInstance(args.instance, { user: args.user });
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: 'text', text: `Deleted alarm instance ${args.instance}.` }],
|
|
211
|
+
isError: false,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function toolAlarmRules(args) {
|
|
216
|
+
const rules = await getAlarmRules({ user: args.user });
|
|
217
|
+
if (rules.length === 0) {
|
|
218
|
+
return {
|
|
219
|
+
content: [{ type: 'text', text: 'No alarm rules configured.' }],
|
|
220
|
+
isError: false,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const lines = rules.map((r) => {
|
|
224
|
+
const id = r.rule || r._id || '?';
|
|
225
|
+
const name = r.name && r.name !== id ? ` (${r.name})` : '';
|
|
226
|
+
const enabled = r.enabled ? '✓' : '✗';
|
|
227
|
+
const desc = r.description ? ` — ${r.description}` : '';
|
|
228
|
+
return `${enabled} ${id}${name}${desc}`;
|
|
229
|
+
});
|
|
230
|
+
return {
|
|
231
|
+
content: [
|
|
232
|
+
{ type: 'text', text: `${rules.length} alarm rule(s):\n${lines.join('\n')}` },
|
|
233
|
+
],
|
|
234
|
+
isError: false,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function toolAlarmRuleRead(args) {
|
|
239
|
+
if (!args.rule) throw inputError('rule is required');
|
|
240
|
+
const rule = await getAlarmRule(args.rule, { user: args.user });
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: 'text', text: JSON.stringify(rule, null, 2) }],
|
|
243
|
+
isError: false,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function toolAlarmRuleWrite(args) {
|
|
248
|
+
if (!args.rule) throw inputError('rule is required');
|
|
249
|
+
|
|
250
|
+
// Decide create vs update by probing the server. The CREATE schema
|
|
251
|
+
// requires `rule`, `name`, `enabled`; UPDATE only accepts a subset.
|
|
252
|
+
let exists = false;
|
|
253
|
+
try {
|
|
254
|
+
await getAlarmRule(args.rule, { user: args.user });
|
|
255
|
+
exists = true;
|
|
256
|
+
} catch (e) {
|
|
257
|
+
if (e?.code !== 'not_found') throw e;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (exists) {
|
|
261
|
+
const body = {};
|
|
262
|
+
if (args.name !== undefined) body.name = args.name;
|
|
263
|
+
if (args.description !== undefined) body.description = args.description;
|
|
264
|
+
if (args.enabled !== undefined) body.enabled = !!args.enabled;
|
|
265
|
+
if (args.config !== undefined) body.config = args.config;
|
|
266
|
+
if (Object.keys(body).length === 0) {
|
|
267
|
+
throw inputError(
|
|
268
|
+
'Nothing to update. Pass at least one of: name, description, enabled, config.',
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
await updateAlarmRule(args.rule, body, { user: args.user });
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: 'text', text: `Updated alarm rule "${args.rule}".` }],
|
|
274
|
+
isError: false,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!args.name) throw inputError('name is required when creating a rule');
|
|
279
|
+
if (args.enabled === undefined) {
|
|
280
|
+
throw inputError('enabled is required when creating a rule (true or false)');
|
|
281
|
+
}
|
|
282
|
+
const body = {
|
|
283
|
+
rule: args.rule,
|
|
284
|
+
name: args.name,
|
|
285
|
+
enabled: !!args.enabled,
|
|
286
|
+
};
|
|
287
|
+
if (args.description !== undefined) body.description = args.description;
|
|
288
|
+
if (args.config !== undefined) body.config = args.config;
|
|
289
|
+
await createAlarmRule(body, { user: args.user });
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: 'text', text: `Created alarm rule "${args.rule}".` }],
|
|
292
|
+
isError: false,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function toolAlarmRuleDelete(args) {
|
|
297
|
+
if (!args.rule) throw inputError('rule is required');
|
|
298
|
+
await deleteAlarmRule(args.rule, { user: args.user });
|
|
299
|
+
return {
|
|
300
|
+
content: [{ type: 'text', text: `Deleted alarm rule "${args.rule}".` }],
|
|
301
|
+
isError: false,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Description boilerplate shared by every tool that takes a state string.
|
|
306
|
+
const STATE_DESC = `Alarm state, accepted as label or number: "active" (1), "ack" (2), "latched" (3), "shelved" (4), "cleared" (5). Numbers also accepted.`;
|
|
307
|
+
const SEVERITY_DESC = `Alarm severity, accepted as label or number: "low" (1), "medium" (2), "high" (3), "critical" (4). Numbers also accepted.`;
|
|
308
|
+
|
|
309
|
+
export const tools = [
|
|
310
|
+
{
|
|
311
|
+
name: 'thinr_alarm_instances',
|
|
312
|
+
description: `List active alarm instances across the fleet. Returns one row per instance with state, severity, instance id, originating rule, the device it fired on, and age. Filter by \`state\`, \`severity\` (single value or array), or \`name\` substring. By default the server returns up to 100 instances sorted state-asc, severity-desc, created-desc — use \`count\`/\`index\` to paginate. Use this first when triaging alerts; pair with thinr_alarm_instance_get for full evaluation/notification details and thinr_alarm_instance_update to acknowledge or shelve.`,
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
properties: {
|
|
316
|
+
state: {
|
|
317
|
+
description: `Filter by ${STATE_DESC} Pass an array to combine ("active" + "ack").`,
|
|
318
|
+
oneOf: [
|
|
319
|
+
{ type: 'string' },
|
|
320
|
+
{ type: 'number' },
|
|
321
|
+
{ type: 'array', items: { type: ['string', 'number'] } },
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
severity: {
|
|
325
|
+
description: `Filter by ${SEVERITY_DESC} Pass an array to combine.`,
|
|
326
|
+
oneOf: [
|
|
327
|
+
{ type: 'string' },
|
|
328
|
+
{ type: 'number' },
|
|
329
|
+
{ type: 'array', items: { type: ['string', 'number'] } },
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
name: {
|
|
333
|
+
type: 'string',
|
|
334
|
+
description: 'Server-side regex matched against the instance name.',
|
|
335
|
+
},
|
|
336
|
+
count: {
|
|
337
|
+
type: 'number',
|
|
338
|
+
description: 'Page size (default 100).',
|
|
339
|
+
},
|
|
340
|
+
index: {
|
|
341
|
+
type: 'number',
|
|
342
|
+
description: 'Page offset (default 0).',
|
|
343
|
+
},
|
|
344
|
+
sort: { type: 'string', description: 'Field to sort by.' },
|
|
345
|
+
order: { type: 'string', description: '"asc" or "desc".' },
|
|
346
|
+
user: { type: 'string', description: 'API user (admin impersonation).' },
|
|
347
|
+
},
|
|
348
|
+
required: [],
|
|
349
|
+
},
|
|
350
|
+
handler: toolAlarmInstances,
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: 'thinr_alarm_instance_stats',
|
|
354
|
+
description:
|
|
355
|
+
'Return counts of alarm instances grouped by state ({ activated, acknowledged, latched, shelved, cleared, none }). Use this to get a high-level health summary without pulling every instance.',
|
|
356
|
+
inputSchema: {
|
|
357
|
+
type: 'object',
|
|
358
|
+
properties: {
|
|
359
|
+
user: { type: 'string', description: 'API user.' },
|
|
360
|
+
},
|
|
361
|
+
required: [],
|
|
362
|
+
},
|
|
363
|
+
handler: toolAlarmInstanceStats,
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: 'thinr_alarm_instance_get',
|
|
367
|
+
description:
|
|
368
|
+
'Fetch the full document for a single alarm instance, including evaluation values, notification history, annotation, and reactivation schedule. Use after thinr_alarm_instances to drill into a specific alert.',
|
|
369
|
+
inputSchema: {
|
|
370
|
+
type: 'object',
|
|
371
|
+
properties: {
|
|
372
|
+
instance: { type: 'string', description: 'Instance ID.' },
|
|
373
|
+
user: { type: 'string', description: 'API user.' },
|
|
374
|
+
},
|
|
375
|
+
required: ['instance'],
|
|
376
|
+
},
|
|
377
|
+
handler: toolAlarmInstanceGet,
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: 'thinr_alarm_instance_update',
|
|
381
|
+
description: `Modify an active alarm instance. Common operations:
|
|
382
|
+
|
|
383
|
+
- Acknowledge: { state: "ack" } — the alert is muted but stays visible until cleared.
|
|
384
|
+
- Shelve (silence temporarily): { state: "shelved", reactivation_minutes: 60 } — the rule is paused for that timespan.
|
|
385
|
+
- Clear (mark resolved): { state: "cleared" }. Cleared alarms cannot be reactivated.
|
|
386
|
+
- Annotate: { annotation: "investigated, false positive" } — adds a note visible in the UI.
|
|
387
|
+
|
|
388
|
+
Pass any combination of fields. The backend rejects re-activating a cleared alarm. \`reactivation_minutes\` is a convenience that maps to the server's reactivation timespan; pass 0 to remove the schedule. For lower-level control pass \`reactivation\` as a raw object ({ type, timespan: { magnitude, value } }).`,
|
|
389
|
+
inputSchema: {
|
|
390
|
+
type: 'object',
|
|
391
|
+
properties: {
|
|
392
|
+
instance: { type: 'string', description: 'Instance ID.' },
|
|
393
|
+
state: {
|
|
394
|
+
description: STATE_DESC,
|
|
395
|
+
oneOf: [{ type: 'string' }, { type: 'number' }],
|
|
396
|
+
},
|
|
397
|
+
annotation: {
|
|
398
|
+
type: 'string',
|
|
399
|
+
description: 'Free-form note (max 512 chars). Visible in the dashboard.',
|
|
400
|
+
},
|
|
401
|
+
reactivation_minutes: {
|
|
402
|
+
type: 'number',
|
|
403
|
+
description:
|
|
404
|
+
'Schedule the rule to reactivate after N minutes. 0 cancels any pending reactivation. Convenience wrapper over the raw `reactivation` object.',
|
|
405
|
+
},
|
|
406
|
+
reactivation: {
|
|
407
|
+
type: 'object',
|
|
408
|
+
description:
|
|
409
|
+
'Raw reactivation object: { type: "none"|"timespan", timespan: { value, magnitude: "second"|"minute"|"hour"|"day"|"week"|"month"|"year" } }. Prefer reactivation_minutes for the common case.',
|
|
410
|
+
},
|
|
411
|
+
user: { type: 'string', description: 'API user.' },
|
|
412
|
+
},
|
|
413
|
+
required: ['instance'],
|
|
414
|
+
},
|
|
415
|
+
handler: toolAlarmInstanceUpdate,
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: 'thinr_alarm_instance_delete',
|
|
419
|
+
description:
|
|
420
|
+
'Permanently delete an alarm instance from history. Prefer thinr_alarm_instance_update with state="cleared" for normal lifecycle handling — deletion removes the audit trail.',
|
|
421
|
+
inputSchema: {
|
|
422
|
+
type: 'object',
|
|
423
|
+
properties: {
|
|
424
|
+
instance: { type: 'string', description: 'Instance ID.' },
|
|
425
|
+
user: { type: 'string', description: 'API user.' },
|
|
426
|
+
},
|
|
427
|
+
required: ['instance'],
|
|
428
|
+
},
|
|
429
|
+
handler: toolAlarmInstanceDelete,
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: 'thinr_alarm_rules',
|
|
433
|
+
description:
|
|
434
|
+
'List configured alarm rules with their enabled flag and description. Each row is one rule (the template that creates instances when its conditions fire). Use thinr_alarm_rule_read to see the full configuration of a single rule.',
|
|
435
|
+
inputSchema: {
|
|
436
|
+
type: 'object',
|
|
437
|
+
properties: {
|
|
438
|
+
user: { type: 'string', description: 'API user.' },
|
|
439
|
+
},
|
|
440
|
+
required: [],
|
|
441
|
+
},
|
|
442
|
+
handler: toolAlarmRules,
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: 'thinr_alarm_rule_read',
|
|
446
|
+
description:
|
|
447
|
+
'Read the full configuration of a single alarm rule (activation conditions, data sources, severity, notifications, reminder, normalization). Returns the document as stored in the database — feed it back to thinr_alarm_rule_write to clone or edit.',
|
|
448
|
+
inputSchema: {
|
|
449
|
+
type: 'object',
|
|
450
|
+
properties: {
|
|
451
|
+
rule: { type: 'string', description: 'Rule ID.' },
|
|
452
|
+
user: { type: 'string', description: 'API user.' },
|
|
453
|
+
},
|
|
454
|
+
required: ['rule'],
|
|
455
|
+
},
|
|
456
|
+
handler: toolAlarmRuleRead,
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
name: 'thinr_alarm_rule_write',
|
|
460
|
+
description: `Create or update an alarm rule. The tool first checks whether the rule exists: if missing it issues a CREATE (\`name\` and \`enabled\` are required); if present it issues an UPDATE with only the supplied fields. The \`config\` block carries the activation conditions, data sources, severity, normalization, reminder and notifications — its shape mirrors the backend's alarm_rule schema. To copy an existing rule, read it with thinr_alarm_rule_read, change \`rule\`, and pass the rest back here.`,
|
|
461
|
+
inputSchema: {
|
|
462
|
+
type: 'object',
|
|
463
|
+
properties: {
|
|
464
|
+
rule: {
|
|
465
|
+
type: 'string',
|
|
466
|
+
description: 'Stable rule ID. Cannot be changed after creation.',
|
|
467
|
+
},
|
|
468
|
+
name: { type: 'string', description: 'Display name (required on create).' },
|
|
469
|
+
description: { type: 'string', description: 'Free-form description.' },
|
|
470
|
+
enabled: {
|
|
471
|
+
type: 'boolean',
|
|
472
|
+
description:
|
|
473
|
+
'Whether the rule is active. Required on create. On update, omit to keep the current value.',
|
|
474
|
+
},
|
|
475
|
+
config: {
|
|
476
|
+
type: 'object',
|
|
477
|
+
description:
|
|
478
|
+
'Rule configuration: activation, normalization, data sources, severity, reminder, check_interval, notifications. Pass the full object as returned by thinr_alarm_rule_read.',
|
|
479
|
+
},
|
|
480
|
+
user: { type: 'string', description: 'API user.' },
|
|
481
|
+
},
|
|
482
|
+
required: ['rule'],
|
|
483
|
+
},
|
|
484
|
+
handler: toolAlarmRuleWrite,
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: 'thinr_alarm_rule_delete',
|
|
488
|
+
description:
|
|
489
|
+
'Delete an alarm rule. Active instances generated from the rule are stopped immediately, but historical instances stay in the database.',
|
|
490
|
+
inputSchema: {
|
|
491
|
+
type: 'object',
|
|
492
|
+
properties: {
|
|
493
|
+
rule: { type: 'string', description: 'Rule ID.' },
|
|
494
|
+
user: { type: 'string', description: 'API user.' },
|
|
495
|
+
},
|
|
496
|
+
required: ['rule'],
|
|
497
|
+
},
|
|
498
|
+
handler: toolAlarmRuleDelete,
|
|
499
|
+
},
|
|
500
|
+
];
|
|
501
|
+
|
|
502
|
+
// Re-export for tests / consumers that want to know which states are
|
|
503
|
+
// considered "active" (everything except cleared).
|
|
504
|
+
export const ALARM_NON_CLEARED_STATES = [
|
|
505
|
+
ALARM_STATE.NONE,
|
|
506
|
+
ALARM_STATE.ACTIVATED,
|
|
507
|
+
ALARM_STATE.ACKNOWLEDGED,
|
|
508
|
+
ALARM_STATE.LATCHED,
|
|
509
|
+
ALARM_STATE.SHELVED,
|
|
510
|
+
];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { getBucketData } from '../bucket.js';
|
|
3
|
+
import { inputError } from '../errors.js';
|
|
4
|
+
|
|
5
|
+
async function toolBucketRead(args) {
|
|
6
|
+
if (!args.bucket)
|
|
7
|
+
throw inputError(
|
|
8
|
+
'bucket is required. Buckets are declared in the product profile (default: "monitoring").',
|
|
9
|
+
);
|
|
10
|
+
const data = await getBucketData({
|
|
11
|
+
bucket: args.bucket,
|
|
12
|
+
device: args.device,
|
|
13
|
+
user: args.user,
|
|
14
|
+
items: args.items || 10,
|
|
15
|
+
sort: args.sort || 'desc',
|
|
16
|
+
minutes: args.minutes,
|
|
17
|
+
min_ts: args.min_ts,
|
|
18
|
+
max_ts: args.max_ts,
|
|
19
|
+
agg: args.agg,
|
|
20
|
+
agg_type: args.agg_type,
|
|
21
|
+
fields: args.fields,
|
|
22
|
+
group_by: args.group_by,
|
|
23
|
+
});
|
|
24
|
+
const text =
|
|
25
|
+
Array.isArray(data) && data.length === 0
|
|
26
|
+
? 'No data'
|
|
27
|
+
: JSON.stringify(data, null, 2);
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: 'text', text }],
|
|
30
|
+
isError: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const tools = [
|
|
35
|
+
{
|
|
36
|
+
name: 'thinr_bucket_read',
|
|
37
|
+
description: `Query any time-series bucket declared in the product profile. Use this for custom metrics (domain data, business events, custom sensors) beyond the default \`monitoring\` bucket — for \`monitoring\` itself prefer \`thinr_monitoring\`, which formats the output.
|
|
38
|
+
|
|
39
|
+
Modes:
|
|
40
|
+
- device set: historical time series scoped to that device.
|
|
41
|
+
- no device: user-level query across the fleet. By default returns raw interleaved samples; pass \`group_by=device\` (assuming the bucket tags samples with \`device\`) to get one row per device.
|
|
42
|
+
|
|
43
|
+
Custom buckets may not share the \`monitoring\` schema — don't assume a \`device\` tag exists unless the product profile defines it.`,
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
bucket: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description:
|
|
50
|
+
'Bucket ID as declared in the product profile (e.g. "monitoring", or a custom one like "sensors", "events").',
|
|
51
|
+
},
|
|
52
|
+
device: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description:
|
|
55
|
+
'Device ID. Omit for a one-row-per-device fleet view.',
|
|
56
|
+
},
|
|
57
|
+
items: {
|
|
58
|
+
type: 'number',
|
|
59
|
+
description:
|
|
60
|
+
'Number of data points to return (default: 10). In multi-device mode set ≥ number of devices.',
|
|
61
|
+
},
|
|
62
|
+
minutes: {
|
|
63
|
+
type: 'number',
|
|
64
|
+
description:
|
|
65
|
+
'Get data from the last N minutes. Alternative to min_ts/max_ts.',
|
|
66
|
+
},
|
|
67
|
+
min_ts: { type: 'number', description: 'Minimum timestamp in milliseconds' },
|
|
68
|
+
max_ts: {
|
|
69
|
+
type: 'number',
|
|
70
|
+
description: 'Maximum timestamp in milliseconds (0 = now)',
|
|
71
|
+
},
|
|
72
|
+
sort: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description:
|
|
75
|
+
'Sort order by timestamp: "asc" or "desc" (default: "desc"). Does NOT sort by metric — do that client-side.',
|
|
76
|
+
},
|
|
77
|
+
agg: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description:
|
|
80
|
+
'Time-window size for windowed aggregation: "5m", "10m", "1h", "6h". Only meaningful with a specific device. For a per-device single value across the whole range, omit `agg` and pass `agg_type` alone.',
|
|
81
|
+
},
|
|
82
|
+
agg_type: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description:
|
|
85
|
+
'Reducer applied to each group/window. Supported: "mean", "max", "min", "sum", "count", "first", "last", "median", "spread", "stddev".',
|
|
86
|
+
},
|
|
87
|
+
fields: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description:
|
|
90
|
+
'Comma-separated fields to return (e.g., "temperature,humidity"). Narrower queries are cheaper.',
|
|
91
|
+
},
|
|
92
|
+
group_by: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description:
|
|
95
|
+
'Optional group key (e.g. "device" for a one-row-per-device fleet view). Omit for raw samples — custom buckets may not carry a "device" tag.',
|
|
96
|
+
},
|
|
97
|
+
user: { type: 'string', description: 'API user' },
|
|
98
|
+
},
|
|
99
|
+
required: ['bucket'],
|
|
100
|
+
},
|
|
101
|
+
handler: toolBucketRead,
|
|
102
|
+
},
|
|
103
|
+
];
|
package/lib/mcp/tools-devices.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
import { getDevices, getDevice } from '../devices.js';
|
|
2
|
+
import { getDevices, getDevice, deleteDevice } from '../devices.js';
|
|
3
3
|
import { setDeviceProduct } from '../product.js';
|
|
4
4
|
import { inputError } from '../errors.js';
|
|
5
|
+
import { BYTES } from '../constants.js';
|
|
5
6
|
import { getAPI } from './helpers.js';
|
|
6
7
|
|
|
7
8
|
async function toolDevices(args) {
|
|
@@ -87,11 +88,11 @@ async function toolDeviceInfo(args) {
|
|
|
87
88
|
);
|
|
88
89
|
if (mon.memory)
|
|
89
90
|
parts.push(
|
|
90
|
-
`Memory: ${mon.memory.usage?.toFixed(1)}% (${(mon.memory.total /
|
|
91
|
+
`Memory: ${mon.memory.usage?.toFixed(1)}% (${(mon.memory.total / BYTES.GB).toFixed(1)}GB total)`,
|
|
91
92
|
);
|
|
92
93
|
if (mon.disk?.root)
|
|
93
94
|
parts.push(
|
|
94
|
-
`Disk: ${mon.disk.root.usage?.toFixed(1)}% (${(mon.disk.root.total /
|
|
95
|
+
`Disk: ${mon.disk.root.usage?.toFixed(1)}% (${(mon.disk.root.total / BYTES.GB).toFixed(0)}GB total)`,
|
|
95
96
|
);
|
|
96
97
|
if (mon.network)
|
|
97
98
|
parts.push(
|
|
@@ -131,6 +132,21 @@ async function toolDeviceSetProduct(args) {
|
|
|
131
132
|
return { content: [{ type: 'text', text: label }], isError: false };
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
async function toolDeviceDelete(args) {
|
|
136
|
+
const device = args.device;
|
|
137
|
+
if (!device) throw inputError('device is required');
|
|
138
|
+
const removed = await deleteDevice(device, args.user);
|
|
139
|
+
return {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: removed ? `Deleted ${device}` : `Device ${device} did not exist`,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
isError: false,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
134
150
|
export const tools = [
|
|
135
151
|
{
|
|
136
152
|
name: 'thinr_devices',
|
|
@@ -191,4 +207,18 @@ export const tools = [
|
|
|
191
207
|
},
|
|
192
208
|
handler: toolDeviceSetProduct,
|
|
193
209
|
},
|
|
210
|
+
{
|
|
211
|
+
name: 'thinr_device_delete',
|
|
212
|
+
description:
|
|
213
|
+
'Delete a device record from the platform. Destructive and irreversible — drops the device, its server-side metadata, and stored properties. The agent on the host (if still running) will be left orphaned and will not auto-recreate the record. Idempotent: returns success when the device was already absent.',
|
|
214
|
+
inputSchema: {
|
|
215
|
+
type: 'object',
|
|
216
|
+
properties: {
|
|
217
|
+
device: { type: 'string', description: 'Device ID to delete.' },
|
|
218
|
+
user: { type: 'string', description: 'API user' },
|
|
219
|
+
},
|
|
220
|
+
required: ['device'],
|
|
221
|
+
},
|
|
222
|
+
handler: toolDeviceDelete,
|
|
223
|
+
},
|
|
194
224
|
];
|