@thinremote/thinr-cli 1.2.0-alpha.1 → 1.2.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 +45 -1
- package/README.md +192 -16
- package/bin/thinr.js +23 -24
- 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 +30 -17
- package/lib/bucket.js +87 -0
- package/lib/command-stream.js +199 -0
- package/lib/config.js +5 -3
- package/lib/console.js +12 -4
- package/lib/constants.js +76 -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 +109 -147
- package/lib/devices.js +18 -21
- 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
package/lib/format.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
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/registry.js
CHANGED
|
@@ -6,7 +6,14 @@ import { tools as resourcesTools } from './tools-resources.js';
|
|
|
6
6
|
import { tools as propertiesTools } from './tools-properties.js';
|
|
7
7
|
import { tools as scriptsTools } from './tools-scripts.js';
|
|
8
8
|
import { tools as productsTools } from './tools-products.js';
|
|
9
|
+
import { tools as playbookTools } from './tools-playbook.js';
|
|
9
10
|
import { tools as profilesTools } from './tools-profiles.js';
|
|
11
|
+
import { tools as installTools } from './tools-install.js';
|
|
12
|
+
import { tools as bucketsTools } from './tools-buckets.js';
|
|
13
|
+
import { tools as logsTools } from './tools-logs.js';
|
|
14
|
+
import { tools as productProfileTools } from './tools-profile.js';
|
|
15
|
+
import { tools as tokensTools } from './tools-tokens.js';
|
|
16
|
+
import { tools as alarmsTools } from './tools-alarms.js';
|
|
10
17
|
|
|
11
18
|
export const tools = [
|
|
12
19
|
...devicesTools,
|
|
@@ -16,7 +23,14 @@ export const tools = [
|
|
|
16
23
|
...propertiesTools,
|
|
17
24
|
...scriptsTools,
|
|
18
25
|
...productsTools,
|
|
26
|
+
...playbookTools,
|
|
19
27
|
...profilesTools,
|
|
28
|
+
...installTools,
|
|
29
|
+
...bucketsTools,
|
|
30
|
+
...logsTools,
|
|
31
|
+
...productProfileTools,
|
|
32
|
+
...tokensTools,
|
|
33
|
+
...alarmsTools,
|
|
20
34
|
];
|
|
21
35
|
|
|
22
36
|
// Inject the optional `profile` parameter on every tool so callers can
|
|
@@ -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
|
+
];
|