@thinremote/thinr-cli 1.0.2 → 1.1.0-alpha.2
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 +90 -0
- package/README.md +362 -87
- package/bin/thinr.js +51 -11
- package/commands/device/_shared.js +48 -0
- package/commands/device/console.js +15 -0
- package/commands/device/exec.js +76 -0
- package/commands/device/index.js +31 -0
- package/commands/device/list.js +43 -0
- package/commands/device/property.js +50 -0
- package/commands/device/proxy.js +42 -0
- package/commands/device/resource.js +51 -0
- package/commands/device/status.js +26 -0
- package/commands/device/update.js +53 -0
- package/commands/logout.js +12 -5
- package/commands/product.js +146 -177
- package/commands/profile.js +105 -0
- package/lib/api.js +54 -31
- package/lib/auth.js +225 -309
- package/lib/config.js +176 -45
- package/lib/console.js +87 -108
- package/lib/device-api.js +257 -0
- package/lib/devices.js +69 -0
- package/lib/errors.js +53 -0
- package/lib/mcp/helpers.js +34 -0
- package/lib/mcp/registry.js +32 -0
- package/lib/mcp/server.js +67 -0
- package/lib/mcp/tools-devices.js +194 -0
- package/lib/mcp/tools-exec.js +213 -0
- package/lib/mcp/tools-filesystem.js +163 -0
- package/lib/mcp/tools-products.js +232 -0
- package/lib/mcp/tools-profiles.js +30 -0
- package/lib/mcp/tools-properties.js +79 -0
- package/lib/mcp/tools-resources.js +76 -0
- package/lib/mcp/tools-scripts.js +172 -0
- package/lib/monitoring.js +72 -0
- package/lib/output.js +114 -0
- package/lib/product.js +373 -0
- package/lib/property.js +41 -57
- package/lib/proxy.js +58 -90
- package/lib/resource.js +106 -72
- package/lib/status.js +7 -33
- package/lib/storage.js +128 -0
- package/package.json +68 -37
- package/.github/workflows/publish.yml +0 -79
- package/commands/device.js +0 -268
- package/lib/device.js +0 -76
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { getDevices, getDevice } from '../devices.js';
|
|
3
|
+
import { setDeviceProduct } from '../product.js';
|
|
4
|
+
import { inputError } from '../errors.js';
|
|
5
|
+
import { getAPI } from './helpers.js';
|
|
6
|
+
|
|
7
|
+
async function toolDevices(args) {
|
|
8
|
+
const query = args.query ? String(args.query).trim() : '';
|
|
9
|
+
const filter = query ? { name: query } : {};
|
|
10
|
+
const devices = await getDevices(filter, args.user);
|
|
11
|
+
|
|
12
|
+
const renderLine = (d) => {
|
|
13
|
+
const status = d.connection?.active ? 'online ' : 'offline';
|
|
14
|
+
const label = d.name ? ` (${d.name})` : '';
|
|
15
|
+
const product = d.type && d.type !== 'Generic' ? ` [${d.type}]` : '';
|
|
16
|
+
return `${status} ${d.device}${label}${product}`;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
if (!query) {
|
|
20
|
+
return {
|
|
21
|
+
content: [
|
|
22
|
+
{ type: 'text', text: devices.map(renderLine).join('\n') || 'No devices found' },
|
|
23
|
+
],
|
|
24
|
+
isError: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const limit = Number.isFinite(args.limit) && args.limit > 0 ? Math.floor(args.limit) : 20;
|
|
29
|
+
const limited = devices.slice(0, limit);
|
|
30
|
+
const header =
|
|
31
|
+
devices.length === 0
|
|
32
|
+
? `No devices match "${query}"`
|
|
33
|
+
: `${devices.length} match(es) for "${query}"${devices.length > limit ? ` — showing first ${limit}` : ''}:`;
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: 'text', text: [header, ...limited.map(renderLine)].join('\n') }],
|
|
36
|
+
isError: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function toolDeviceInfo(args) {
|
|
41
|
+
const { api: deviceApi } = getAPI(args.device, args.user);
|
|
42
|
+
const parts = [];
|
|
43
|
+
|
|
44
|
+
// Server-side record first — surfaces the assigned product,
|
|
45
|
+
// asset group, description, etc. that the device's own agent
|
|
46
|
+
// can't see. Best effort: don't fail the whole tool if the
|
|
47
|
+
// record can't be read.
|
|
48
|
+
try {
|
|
49
|
+
const record = await getDevice(args.device, args.user);
|
|
50
|
+
const meta = [];
|
|
51
|
+
if (record.device) meta.push(`device: ${record.device}`);
|
|
52
|
+
if (record.name) meta.push(`name: ${record.name}`);
|
|
53
|
+
if (record.product) meta.push(`product: ${record.product}`);
|
|
54
|
+
if (record.asset_group) meta.push(`asset_group: ${record.asset_group}`);
|
|
55
|
+
if (record.description) meta.push(`description: ${record.description}`);
|
|
56
|
+
if (record.connection) {
|
|
57
|
+
const c = record.connection;
|
|
58
|
+
meta.push(
|
|
59
|
+
`connection: ${c.active ? 'online' : 'offline'}${c.ip_address ? ` from ${c.ip_address}` : ''}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (meta.length) {
|
|
63
|
+
parts.push('--- Server record ---');
|
|
64
|
+
parts.push(...meta);
|
|
65
|
+
parts.push('');
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const sysInfo = await deviceApi.getResource('system_info');
|
|
71
|
+
parts.push('--- System info ---');
|
|
72
|
+
parts.push(JSON.stringify(sysInfo, null, 2));
|
|
73
|
+
} catch {
|
|
74
|
+
const info = await deviceApi.exec(
|
|
75
|
+
'echo "hostname=$(hostname);os=$(uname -s);arch=$(uname -m);kernel=$(uname -r);user=$(whoami);home=$HOME;uptime=$(uptime -p 2>/dev/null || uptime)"',
|
|
76
|
+
10,
|
|
77
|
+
);
|
|
78
|
+
parts.push(info.stdout || '');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const mon = await deviceApi.getResource('monitoring');
|
|
83
|
+
parts.push('\n--- Real-time monitoring ---');
|
|
84
|
+
if (mon.cpu)
|
|
85
|
+
parts.push(
|
|
86
|
+
`CPU: ${mon.cpu.usage?.toFixed(1)}% (${mon.cpu.cores} cores, ${mon.cpu.temperature?.toFixed(1)}°C)`,
|
|
87
|
+
);
|
|
88
|
+
if (mon.memory)
|
|
89
|
+
parts.push(
|
|
90
|
+
`Memory: ${mon.memory.usage?.toFixed(1)}% (${(mon.memory.total / 1073741824).toFixed(1)}GB total)`,
|
|
91
|
+
);
|
|
92
|
+
if (mon.disk?.root)
|
|
93
|
+
parts.push(
|
|
94
|
+
`Disk: ${mon.disk.root.usage?.toFixed(1)}% (${(mon.disk.root.total / 1073741824).toFixed(0)}GB total)`,
|
|
95
|
+
);
|
|
96
|
+
if (mon.network)
|
|
97
|
+
parts.push(
|
|
98
|
+
`Network: rx=${(mon.network.rx_rate / 1024).toFixed(1)}KB/s tx=${(mon.network.tx_rate / 1024).toFixed(1)}KB/s`,
|
|
99
|
+
);
|
|
100
|
+
if (mon.load)
|
|
101
|
+
parts.push(
|
|
102
|
+
`Load: ${mon.load['1m']?.toFixed(2)} / ${mon.load['5m']?.toFixed(2)} / ${mon.load['15m']?.toFixed(2)}`,
|
|
103
|
+
);
|
|
104
|
+
if (mon.uptime)
|
|
105
|
+
parts.push(
|
|
106
|
+
`Uptime: ${Math.floor(mon.uptime / 86400)}d ${Math.floor((mon.uptime % 86400) / 3600)}h`,
|
|
107
|
+
);
|
|
108
|
+
} catch {}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const rootInfo = await deviceApi.info('/');
|
|
112
|
+
parts.push(`\nfs_base_path=${rootInfo.path || '/'}`);
|
|
113
|
+
} catch {}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: 'text', text: parts.join('\n') }],
|
|
117
|
+
isError: false,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function toolDeviceSetProduct(args) {
|
|
122
|
+
const device = args.device;
|
|
123
|
+
if (!device) throw inputError('device is required');
|
|
124
|
+
if (args.product === undefined || args.product === null)
|
|
125
|
+
throw inputError('product is required (pass "" to unassign)');
|
|
126
|
+
await setDeviceProduct(device, args.product, args.user);
|
|
127
|
+
const label =
|
|
128
|
+
args.product === ''
|
|
129
|
+
? `Unassigned ${device} from its product.`
|
|
130
|
+
: `Assigned ${device} → product "${args.product}".`;
|
|
131
|
+
return { content: [{ type: 'text', text: label }], isError: false };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const tools = [
|
|
135
|
+
{
|
|
136
|
+
name: 'thinr_devices',
|
|
137
|
+
description:
|
|
138
|
+
'List remote devices with their online/offline status. Pass `query` to filter by device id or name (case-insensitive regex on the server side, so plain substrings, anchors, wildcards and alternations all work). Use this before any other device tool to discover or narrow down ids.',
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
query: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description:
|
|
145
|
+
'Optional pattern matched against device id and name. Plain substrings work ("office"); regex metacharacters are also honoured ("^raspberry", "mac|book"). Case-insensitive.',
|
|
146
|
+
},
|
|
147
|
+
limit: {
|
|
148
|
+
type: 'number',
|
|
149
|
+
description:
|
|
150
|
+
'Maximum matches to return when `query` is set (default: 20). Ignored when listing without a query.',
|
|
151
|
+
},
|
|
152
|
+
user: {
|
|
153
|
+
type: 'string',
|
|
154
|
+
description:
|
|
155
|
+
'API user (for admin impersonation). Omit to use your own account.',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
required: [],
|
|
159
|
+
},
|
|
160
|
+
handler: toolDevices,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'thinr_device_info',
|
|
164
|
+
description:
|
|
165
|
+
'Get device information: hostname, OS, architecture, filesystem base path, and system resources.',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
device: { type: 'string', description: 'Device ID' },
|
|
170
|
+
user: { type: 'string', description: 'API user' },
|
|
171
|
+
},
|
|
172
|
+
required: ['device'],
|
|
173
|
+
},
|
|
174
|
+
handler: toolDeviceInfo,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'thinr_device_set_product',
|
|
178
|
+
description:
|
|
179
|
+
"Assign a device to a product, or unassign it by passing an empty string. Devices inherit the product's API resources and profile configuration (scripts, buckets, etc.) once assigned. Typical flow: create product scripts with thinr_product_script_write, then set a device's product here so it can invoke those scripts.",
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
device: { type: 'string', description: 'Device ID' },
|
|
184
|
+
product: {
|
|
185
|
+
type: 'string',
|
|
186
|
+
description: 'Product ID to assign, or empty string "" to unassign.',
|
|
187
|
+
},
|
|
188
|
+
user: { type: 'string', description: 'API user' },
|
|
189
|
+
},
|
|
190
|
+
required: ['device', 'product'],
|
|
191
|
+
},
|
|
192
|
+
handler: toolDeviceSetProduct,
|
|
193
|
+
},
|
|
194
|
+
];
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { getMonitoringData } from '../monitoring.js';
|
|
3
|
+
import { inputError } from '../errors.js';
|
|
4
|
+
import { getAPI } from './helpers.js';
|
|
5
|
+
|
|
6
|
+
async function toolExec(args) {
|
|
7
|
+
const { api } = getAPI(args.device, args.user);
|
|
8
|
+
// Default to 30s so a hung command doesn't lock the session
|
|
9
|
+
// indefinitely. Callers can pass a larger value for known-long
|
|
10
|
+
// operations (compiles, backups, …).
|
|
11
|
+
const timeout = Number.isFinite(args.timeout) && args.timeout > 0 ? args.timeout : 30;
|
|
12
|
+
let output = '';
|
|
13
|
+
const { exitCode, timedOut } = await api.execStream(args.command, {
|
|
14
|
+
timeout,
|
|
15
|
+
onStdout: (s) => {
|
|
16
|
+
output += s;
|
|
17
|
+
},
|
|
18
|
+
onStderr: (s) => {
|
|
19
|
+
output += s;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const trailer = [];
|
|
23
|
+
if (timedOut) trailer.push(`[command timed out after ${timeout}s]`);
|
|
24
|
+
trailer.push(`[exit code: ${exitCode ?? 'unknown'}]`);
|
|
25
|
+
const text = [output.trimEnd(), trailer.join(' ')].filter(Boolean).join('\n');
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: 'text', text: text || '(no output)' }],
|
|
28
|
+
isError: timedOut || exitCode !== 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function toolUpdate(args) {
|
|
33
|
+
const { api: deviceApi } = getAPI(args.device, args.user);
|
|
34
|
+
const payload = {
|
|
35
|
+
action: args.action,
|
|
36
|
+
channel: args.channel || 'latest',
|
|
37
|
+
};
|
|
38
|
+
const result = await deviceApi.callResource('update', payload);
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
41
|
+
isError: false,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function toolMonitoring(args) {
|
|
46
|
+
const device = args.device;
|
|
47
|
+
const user = args.user;
|
|
48
|
+
|
|
49
|
+
// Real-time: read the device's `monitoring` resource directly.
|
|
50
|
+
if (args.realtime) {
|
|
51
|
+
if (!device) throw inputError('Device required for real-time monitoring');
|
|
52
|
+
const { api: deviceApi } = getAPI(device, args.user);
|
|
53
|
+
const mon = await deviceApi.getResource('monitoring');
|
|
54
|
+
const parts = [`Real-time monitoring for ${device}:`];
|
|
55
|
+
if (mon.cpu)
|
|
56
|
+
parts.push(
|
|
57
|
+
`CPU: ${mon.cpu.usage?.toFixed(1)}% (${mon.cpu.cores} cores, ${mon.cpu.temperature?.toFixed(1)}°C)`,
|
|
58
|
+
);
|
|
59
|
+
if (mon.memory)
|
|
60
|
+
parts.push(
|
|
61
|
+
`Memory: ${mon.memory.usage?.toFixed(1)}% (available: ${(mon.memory.available / 1073741824).toFixed(1)}GB / total: ${(mon.memory.total / 1073741824).toFixed(1)}GB)`,
|
|
62
|
+
);
|
|
63
|
+
if (mon.disk?.root)
|
|
64
|
+
parts.push(
|
|
65
|
+
`Disk: ${mon.disk.root.usage?.toFixed(1)}% (available: ${(mon.disk.root.available / 1073741824).toFixed(1)}GB / total: ${(mon.disk.root.total / 1073741824).toFixed(0)}GB)`,
|
|
66
|
+
);
|
|
67
|
+
if (mon.network)
|
|
68
|
+
parts.push(
|
|
69
|
+
`Network: rx=${(mon.network.rx_rate / 1024).toFixed(1)}KB/s tx=${(mon.network.tx_rate / 1024).toFixed(1)}KB/s`,
|
|
70
|
+
);
|
|
71
|
+
if (mon.load)
|
|
72
|
+
parts.push(
|
|
73
|
+
`Load: ${mon.load['1m']?.toFixed(2)} / ${mon.load['5m']?.toFixed(2)} / ${mon.load['15m']?.toFixed(2)}`,
|
|
74
|
+
);
|
|
75
|
+
if (mon.processes) parts.push(`Processes: ${mon.processes.total}`);
|
|
76
|
+
if (mon.uptime)
|
|
77
|
+
parts.push(
|
|
78
|
+
`Uptime: ${Math.floor(mon.uptime / 86400)}d ${Math.floor((mon.uptime % 86400) / 3600)}h ${Math.floor((mon.uptime % 3600) / 60)}m`,
|
|
79
|
+
);
|
|
80
|
+
if (mon.agent) parts.push(`Agent: ${mon.agent.version}`);
|
|
81
|
+
return { content: [{ type: 'text', text: parts.join('\n') }], isError: false };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Historical: query the monitoring bucket.
|
|
85
|
+
const data = await getMonitoringData({
|
|
86
|
+
device,
|
|
87
|
+
user,
|
|
88
|
+
items: args.items || 10,
|
|
89
|
+
sort: args.sort || 'desc',
|
|
90
|
+
minutes: args.minutes,
|
|
91
|
+
min_ts: args.min_ts,
|
|
92
|
+
max_ts: args.max_ts,
|
|
93
|
+
agg: args.agg,
|
|
94
|
+
agg_type: args.agg_type,
|
|
95
|
+
fields: args.fields,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
let output;
|
|
99
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
100
|
+
output = data
|
|
101
|
+
.map((d) => {
|
|
102
|
+
const ts = new Date(d.ts).toISOString();
|
|
103
|
+
const parts = [ts];
|
|
104
|
+
if (d.device) parts.push(`device=${d.device}`);
|
|
105
|
+
if (d.cpu) {
|
|
106
|
+
const cpuParts = [];
|
|
107
|
+
if (d.cpu.usage !== undefined) cpuParts.push(`${d.cpu.usage.toFixed(1)}%`);
|
|
108
|
+
if (d.cpu.temperature !== undefined)
|
|
109
|
+
cpuParts.push(`${d.cpu.temperature.toFixed(1)}°C`);
|
|
110
|
+
if (cpuParts.length) parts.push(`cpu=${cpuParts.join(' ')}`);
|
|
111
|
+
}
|
|
112
|
+
if (d.memory) parts.push(`mem=${d.memory.usage?.toFixed(1)}%`);
|
|
113
|
+
if (d.disk?.root) parts.push(`disk=${d.disk.root.usage?.toFixed(1)}%`);
|
|
114
|
+
if (d.network)
|
|
115
|
+
parts.push(
|
|
116
|
+
`net_rx=${(d.network.rx_rate / 1024)?.toFixed(1)}KB/s tx=${(d.network.tx_rate / 1024)?.toFixed(1)}KB/s`,
|
|
117
|
+
);
|
|
118
|
+
if (d.load) parts.push(`load=${d.load['1m']?.toFixed(2)}`);
|
|
119
|
+
if (d.processes) parts.push(`procs=${d.processes.total}`);
|
|
120
|
+
return parts.join(' | ');
|
|
121
|
+
})
|
|
122
|
+
.join('\n');
|
|
123
|
+
} else {
|
|
124
|
+
output = JSON.stringify(data, null, 2);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: 'text', text: output || 'No data' }],
|
|
129
|
+
isError: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const tools = [
|
|
134
|
+
{
|
|
135
|
+
name: 'thinr_exec',
|
|
136
|
+
description: `Execute a shell command on a remote device. Returns stdout, stderr, and exit code.`,
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
command: { type: 'string', description: 'Shell command to execute' },
|
|
141
|
+
device: { type: 'string', description: 'Device ID (from thinr_devices)' },
|
|
142
|
+
timeout: { type: 'number', description: 'Timeout in seconds (default: 30)' },
|
|
143
|
+
user: { type: 'string', description: 'API user (for admin impersonation)' },
|
|
144
|
+
},
|
|
145
|
+
required: ['command', 'device'],
|
|
146
|
+
},
|
|
147
|
+
handler: toolExec,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'thinr_update',
|
|
151
|
+
description: `Check for agent updates or apply an update on a remote device.`,
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
device: { type: 'string', description: 'Device ID' },
|
|
156
|
+
action: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
description: '"check" to check for updates, "apply" to install the update',
|
|
159
|
+
},
|
|
160
|
+
channel: { type: 'string', description: 'Update channel (default: "latest")' },
|
|
161
|
+
user: { type: 'string', description: 'API user' },
|
|
162
|
+
},
|
|
163
|
+
required: ['action', 'device'],
|
|
164
|
+
},
|
|
165
|
+
handler: toolUpdate,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'thinr_monitoring',
|
|
169
|
+
description: `Get monitoring data (CPU, memory, disk, network, temperature, load) from a device or all devices. Use realtime=true for live data from the device, or omit for historical data from the monitoring bucket.`,
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {
|
|
173
|
+
realtime: {
|
|
174
|
+
type: 'boolean',
|
|
175
|
+
description:
|
|
176
|
+
'If true, get live data directly from the device. If false/omitted, query historical data from the monitoring bucket.',
|
|
177
|
+
},
|
|
178
|
+
device: {
|
|
179
|
+
type: 'string',
|
|
180
|
+
description:
|
|
181
|
+
'Device ID. Required for realtime. Omit for historical data from ALL devices.',
|
|
182
|
+
},
|
|
183
|
+
items: {
|
|
184
|
+
type: 'number',
|
|
185
|
+
description: 'Number of data points to return (default: 10)',
|
|
186
|
+
},
|
|
187
|
+
minutes: {
|
|
188
|
+
type: 'number',
|
|
189
|
+
description: 'Get data from the last N minutes. Alternative to min_ts/max_ts.',
|
|
190
|
+
},
|
|
191
|
+
min_ts: { type: 'number', description: 'Minimum timestamp in milliseconds' },
|
|
192
|
+
max_ts: {
|
|
193
|
+
type: 'number',
|
|
194
|
+
description: 'Maximum timestamp in milliseconds (0 = now)',
|
|
195
|
+
},
|
|
196
|
+
sort: {
|
|
197
|
+
type: 'string',
|
|
198
|
+
description: 'Sort order: "asc" or "desc" (default: "desc")',
|
|
199
|
+
},
|
|
200
|
+
agg: { type: 'string', description: 'Aggregation period: "5m", "10m", "1h", "6h"' },
|
|
201
|
+
agg_type: { type: 'string', description: 'Aggregation type: "mean", "min", "max"' },
|
|
202
|
+
fields: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
description:
|
|
205
|
+
'Comma-separated fields to return (e.g., "cpu.usage,memory.usage,disk.root.usage")',
|
|
206
|
+
},
|
|
207
|
+
user: { type: 'string', description: 'API user' },
|
|
208
|
+
},
|
|
209
|
+
required: [],
|
|
210
|
+
},
|
|
211
|
+
handler: toolMonitoring,
|
|
212
|
+
},
|
|
213
|
+
];
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { getAPI } from './helpers.js';
|
|
3
|
+
|
|
4
|
+
async function toolRead(args) {
|
|
5
|
+
const { api } = getAPI(args.device, args.user);
|
|
6
|
+
const content = await api.readFile(args.path);
|
|
7
|
+
return {
|
|
8
|
+
content: [{ type: 'text', text: content.toString('utf8') }],
|
|
9
|
+
isError: false,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function toolWrite(args) {
|
|
14
|
+
const { api } = getAPI(args.device, args.user);
|
|
15
|
+
await api.writeFile(args.path, Buffer.from(args.content, 'utf8'));
|
|
16
|
+
return {
|
|
17
|
+
content: [{ type: 'text', text: `Written ${args.content.length} bytes to ${args.path}` }],
|
|
18
|
+
isError: false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function toolLs(args) {
|
|
23
|
+
const { api } = getAPI(args.device, args.user);
|
|
24
|
+
const entries = await api.listDir(args.path || '/', args.include_hidden || false);
|
|
25
|
+
const lines = entries.map((e) => {
|
|
26
|
+
const type = e.type === 'directory' ? 'd' : '-';
|
|
27
|
+
const mode = e.mode || 'rwxr-xr-x';
|
|
28
|
+
const size = (e.size || 0).toString().padStart(8);
|
|
29
|
+
return `${type}${mode} ${size} ${e.name}`;
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: 'text', text: lines.join('\n') || '(empty directory)' }],
|
|
33
|
+
isError: false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function toolMkdir(args) {
|
|
38
|
+
const { api } = getAPI(args.device, args.user);
|
|
39
|
+
await api.mkdir(args.path);
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: `Created directory: ${args.path}` }],
|
|
42
|
+
isError: false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function toolDelete(args) {
|
|
47
|
+
const { api } = getAPI(args.device, args.user);
|
|
48
|
+
await api.delete(args.path, args.recursive !== false);
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: 'text', text: `Deleted: ${args.path}` }],
|
|
51
|
+
isError: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function toolMove(args) {
|
|
56
|
+
const { api } = getAPI(args.device, args.user);
|
|
57
|
+
await api.move(args.source, args.destination, args.overwrite || false);
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: 'text', text: `Moved: ${args.source} → ${args.destination}` }],
|
|
60
|
+
isError: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const tools = [
|
|
65
|
+
{
|
|
66
|
+
name: 'thinr_read',
|
|
67
|
+
description: `Read a file from a remote device. Paths are absolute on the device filesystem (e.g., "/etc/hosts", "/var/log/syslog"). Subject to the OS user the agent runs as; check whoami (or thinr_device_info) before assuming write access to system paths like /etc.`,
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
path: { type: 'string', description: 'Absolute file path on the device' },
|
|
72
|
+
device: { type: 'string', description: 'Device ID' },
|
|
73
|
+
user: { type: 'string', description: 'API user' },
|
|
74
|
+
},
|
|
75
|
+
required: ['path', 'device'],
|
|
76
|
+
},
|
|
77
|
+
handler: toolRead,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'thinr_write',
|
|
81
|
+
description: `Write content to a file on a remote device. Paths are absolute on the device filesystem (e.g., "/etc/hosts", "/var/log/syslog"). Subject to the OS user the agent runs as; check whoami (or thinr_device_info) before assuming write access to system paths like /etc.`,
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
path: { type: 'string', description: 'Absolute file path on the device' },
|
|
86
|
+
content: { type: 'string', description: 'File content to write' },
|
|
87
|
+
device: { type: 'string', description: 'Device ID' },
|
|
88
|
+
user: { type: 'string', description: 'API user' },
|
|
89
|
+
},
|
|
90
|
+
required: ['path', 'content', 'device'],
|
|
91
|
+
},
|
|
92
|
+
handler: toolWrite,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'thinr_ls',
|
|
96
|
+
description: `List directory contents on a remote device. Paths are absolute on the device filesystem (e.g., "/", "/etc", "/var/log"). Subject to the OS user the agent runs as; check whoami (or thinr_device_info) before assuming write access to system paths like /etc. Use include_hidden to see dotfiles.`,
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
path: { type: 'string', description: 'Directory path (default: /)' },
|
|
101
|
+
include_hidden: {
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
description: 'Include hidden files (default: false)',
|
|
104
|
+
},
|
|
105
|
+
device: { type: 'string', description: 'Device ID' },
|
|
106
|
+
user: { type: 'string', description: 'API user' },
|
|
107
|
+
},
|
|
108
|
+
required: ['device'],
|
|
109
|
+
},
|
|
110
|
+
handler: toolLs,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'thinr_mkdir',
|
|
114
|
+
description: `Create a directory on a remote device. Paths are absolute on the device filesystem (e.g., "/etc/hosts", "/var/log/syslog"). Subject to the OS user the agent runs as; check whoami (or thinr_device_info) before assuming write access to system paths like /etc.`,
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
path: { type: 'string', description: 'Directory path to create' },
|
|
119
|
+
device: { type: 'string', description: 'Device ID' },
|
|
120
|
+
user: { type: 'string', description: 'API user' },
|
|
121
|
+
},
|
|
122
|
+
required: ['path', 'device'],
|
|
123
|
+
},
|
|
124
|
+
handler: toolMkdir,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'thinr_delete',
|
|
128
|
+
description: `Delete a file or directory on a remote device. Paths are absolute on the device filesystem (e.g., "/etc/hosts", "/var/log/syslog"). Subject to the OS user the agent runs as; check whoami (or thinr_device_info) before assuming write access to system paths like /etc.`,
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {
|
|
132
|
+
path: { type: 'string', description: 'Path to delete' },
|
|
133
|
+
recursive: {
|
|
134
|
+
type: 'boolean',
|
|
135
|
+
description: 'Delete directory contents recursively (default: true)',
|
|
136
|
+
},
|
|
137
|
+
device: { type: 'string', description: 'Device ID' },
|
|
138
|
+
user: { type: 'string', description: 'API user' },
|
|
139
|
+
},
|
|
140
|
+
required: ['path', 'device'],
|
|
141
|
+
},
|
|
142
|
+
handler: toolDelete,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'thinr_move',
|
|
146
|
+
description: `Move or rename a file or directory on a remote device. Paths are absolute on the device filesystem (e.g., "/etc/hosts", "/var/log/syslog"). Subject to the OS user the agent runs as; check whoami (or thinr_device_info) before assuming write access to system paths like /etc.`,
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
source: { type: 'string', description: 'Source path' },
|
|
151
|
+
destination: { type: 'string', description: 'Destination path' },
|
|
152
|
+
overwrite: {
|
|
153
|
+
type: 'boolean',
|
|
154
|
+
description: 'Overwrite destination if exists (default: false)',
|
|
155
|
+
},
|
|
156
|
+
device: { type: 'string', description: 'Device ID' },
|
|
157
|
+
user: { type: 'string', description: 'API user' },
|
|
158
|
+
},
|
|
159
|
+
required: ['source', 'destination', 'device'],
|
|
160
|
+
},
|
|
161
|
+
handler: toolMove,
|
|
162
|
+
},
|
|
163
|
+
];
|