@thinger-io/cli-core 0.1.1 → 0.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.
@@ -1,84 +0,0 @@
1
- // @ts-check
2
- import { getDeviceProperties, getDeviceProperty, setDeviceProperty } from '../property.js';
3
- import { inputError } from '../errors.js';
4
- import { toolName } from './naming.js';
5
-
6
- async function toolPropertyGet(args) {
7
- const device = args.device_id;
8
- if (!device)
9
- throw inputError(
10
- `No device specified. Use ${toolName('devices')} to list available devices.`,
11
- );
12
- if (args.property) {
13
- const value = await getDeviceProperty(device, args.property, args.user);
14
- return {
15
- content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
16
- isError: false,
17
- };
18
- }
19
- const list = await getDeviceProperties(device, args.user);
20
- const names = Array.isArray(list) ? list.map((p) => p.property).filter(Boolean) : [];
21
- return {
22
- content: [{ type: 'text', text: names.length ? names.join('\n') : '(no properties)' }],
23
- isError: false,
24
- };
25
- }
26
-
27
- async function toolPropertySet(args) {
28
- const device = args.device_id;
29
- if (!device)
30
- throw inputError(
31
- `No device specified. Use ${toolName('devices')} to list available devices.`,
32
- );
33
- if (!args.property) throw inputError('property is required');
34
- if (args.value === undefined) throw inputError('value is required');
35
- const saved = await setDeviceProperty(device, args.property, args.value, args.user);
36
- return {
37
- content: [
38
- {
39
- type: 'text',
40
- text: `Saved property "${saved.property}" on ${saved.device} (value: ${JSON.stringify(saved.value)})`,
41
- },
42
- ],
43
- isError: false,
44
- };
45
- }
46
-
47
- export const tools = [
48
- {
49
- name: 'property_get',
50
- description: `Read a device property. Properties are the device's structured state (e.g. configuration, last values, calibration). Call without \`property\` to list the names of every property the device has.`,
51
- inputSchema: {
52
- type: 'object',
53
- properties: {
54
- property: {
55
- type: 'string',
56
- description:
57
- 'Property name. Omit to list the names of all properties on the device.',
58
- },
59
- device_id: { type: 'string', description: 'Device ID' },
60
- user: { type: 'string', description: 'API user' },
61
- },
62
- required: ['device_id'],
63
- },
64
- handler: toolPropertyGet,
65
- },
66
- {
67
- name: 'property_set',
68
- description: `Create or overwrite a device property. The value can be any JSON-serialisable shape (string, number, object, array). Existing properties are replaced wholesale; missing ones are created. Properties live on the server, so they can be written while the device is offline; devices read them on connection, and dashboards can show them.`,
69
- inputSchema: {
70
- type: 'object',
71
- properties: {
72
- property: { type: 'string', description: 'Property name to create or overwrite.' },
73
- value: {
74
- description:
75
- 'Any JSON-serialisable value (string, number, boolean, object, array).',
76
- },
77
- device_id: { type: 'string', description: 'Device ID' },
78
- user: { type: 'string', description: 'API user' },
79
- },
80
- required: ['property', 'value', 'device_id'],
81
- },
82
- handler: toolPropertySet,
83
- },
84
- ];
@@ -1,83 +0,0 @@
1
- // @ts-check
2
- import { callDeviceResource, listDeviceResourcesWithSchemas } from '../resource.js';
3
- import { inputError } from '../errors.js';
4
- import { toolName } from './naming.js';
5
-
6
- async function toolResourceList(args) {
7
- const device = args.device_id;
8
- if (!device)
9
- throw inputError(
10
- `No device specified. Use ${toolName('devices')} to list available devices.`,
11
- );
12
- const entries = await listDeviceResourcesWithSchemas(device, { user: args.user });
13
- const fnLabel = (f) =>
14
- ({ 1: 'no params', 2: 'input', 3: 'output', 4: 'input/output' })[f] || 'unknown';
15
- const lines = entries.map((e) => {
16
- const parts = [`${e.name} [${fnLabel(e.fn)}]`];
17
- if (e.in !== undefined) parts.push(` in: ${JSON.stringify(e.in)}`);
18
- if (e.out !== undefined) parts.push(` out: ${JSON.stringify(e.out)}`);
19
- return parts.join('\n');
20
- });
21
- return {
22
- content: [{ type: 'text', text: lines.length ? lines.join('\n\n') : '(no resources)' }],
23
- isError: false,
24
- };
25
- }
26
-
27
- async function toolResourceCall(args) {
28
- const device = args.device_id;
29
- if (!device)
30
- throw inputError(
31
- `No device specified. Use ${toolName('devices')} to list available devices.`,
32
- );
33
- if (!args.resource) throw inputError('resource is required');
34
- const result = await callDeviceResource(device, args.resource, args.inputs, {
35
- user: args.user,
36
- });
37
- return {
38
- content: [
39
- {
40
- type: 'text',
41
- text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
42
- },
43
- ],
44
- isError: false,
45
- };
46
- }
47
-
48
- export const tools = [
49
- {
50
- name: 'resource_list',
51
- description: `List the callable resources exposed by a device, with their input/output schemas when available. Resources are the device's public API, declared by its firmware (e.g. "temperature" to read a sensor, "led" to switch an output) plus any API resources inherited from its product profile. Each is input-only, output-only, input/output or parameterless. The device must be online. Prefer this before calling {tool:resource_call} to discover what a device offers and how to call each resource.`,
52
- inputSchema: {
53
- type: 'object',
54
- properties: {
55
- device_id: { type: 'string', description: 'Device ID' },
56
- user: { type: 'string', description: 'API user' },
57
- },
58
- required: ['device_id'],
59
- },
60
- handler: toolResourceList,
61
- },
62
- {
63
- name: 'resource_call',
64
- description: `Invoke a resource on a device. Pass \`inputs\` when the resource declares an input schema; omit them to read an output-only resource (a GET that does not trigger the input side). Returns the resource's output. The device must be online. Use {tool:resource_list} first to discover available resources and their expected inputs.`,
65
- inputSchema: {
66
- type: 'object',
67
- properties: {
68
- resource: {
69
- type: 'string',
70
- description: 'Resource name as returned by {tool:resource_list}.',
71
- },
72
- inputs: {
73
- description:
74
- 'JSON object with the input parameters declared by the resource. Omit or pass {} for input-less resources.',
75
- },
76
- device_id: { type: 'string', description: 'Device ID' },
77
- user: { type: 'string', description: 'API user' },
78
- },
79
- required: ['resource', 'device_id'],
80
- },
81
- handler: toolResourceCall,
82
- },
83
- ];
@@ -1,370 +0,0 @@
1
- // @ts-check
2
- import {
3
- createDeviceToken,
4
- createToken,
5
- deleteDeviceToken,
6
- deleteToken,
7
- getToken,
8
- listDeviceTokens,
9
- listTokens,
10
- updateToken,
11
- } from '../tokens.js';
12
- import { inputError } from '../errors.js';
13
- import { toolName } from './naming.js';
14
-
15
- // ─── User-level tokens ──────────────────────────────────────────────
16
-
17
- async function toolTokenList(args) {
18
- const tokens = await listTokens(args.user);
19
- if (tokens.length === 0) {
20
- return {
21
- content: [{ type: 'text', text: 'No access tokens configured.' }],
22
- isError: false,
23
- };
24
- }
25
- const lines = tokens.map((t) => {
26
- const enabled = t.enabled === false ? '✗' : '✓';
27
- const name = t.name || t.token;
28
- return ` ${enabled} ${t.token} (${name})`;
29
- });
30
- return {
31
- content: [
32
- {
33
- type: 'text',
34
- text: `${tokens.length} token(s):\n${lines.join('\n')}\n\nNote: the access_token JWT is NOT returned by list — use ${toolName('token_get')} to read it.`,
35
- },
36
- ],
37
- isError: false,
38
- };
39
- }
40
-
41
- async function toolTokenGet(args) {
42
- if (!args.token_id) throw inputError('token_id is required');
43
- const doc = await getToken(args.token_id, args.user);
44
- return {
45
- content: [{ type: 'text', text: JSON.stringify(doc, null, 2) }],
46
- isError: false,
47
- };
48
- }
49
-
50
- async function toolTokenCreate(args) {
51
- if (!args.token_id) throw inputError('token_id is required');
52
- if (!args.name) throw inputError('name is required');
53
- if (!args.allow) throw inputError('allow is required (permission tree)');
54
-
55
- const doc = await createToken(
56
- {
57
- token: args.token_id,
58
- name: args.name,
59
- allow: args.allow,
60
- deny: args.deny,
61
- description: args.description,
62
- expire: args.expire,
63
- enabled: args.enabled,
64
- },
65
- args.user,
66
- );
67
- return {
68
- content: [
69
- {
70
- type: 'text',
71
- text: `Created token "${args.token_id}". Full document (including access_token JWT):\n${JSON.stringify(doc, null, 2)}\n\nIMPORTANT: persist the JWT now — it will not be visible in ${toolName('token_list')} responses; only ${toolName('token_get')} returns it.`,
72
- },
73
- ],
74
- isError: false,
75
- };
76
- }
77
-
78
- async function toolTokenUpdate(args) {
79
- if (!args.token_id) throw inputError('token_id is required');
80
- const patch = {};
81
- if (args.name !== undefined) patch.name = args.name;
82
- if (args.description !== undefined) patch.description = args.description;
83
- if (args.enabled !== undefined) patch.enabled = !!args.enabled;
84
- if (args.expire !== undefined) patch.expire = args.expire;
85
- if (args.allow !== undefined) patch.allow = args.allow;
86
- if (args.deny !== undefined) patch.deny = args.deny;
87
-
88
- const doc = await updateToken(args.token_id, patch, args.user);
89
- return {
90
- content: [
91
- {
92
- type: 'text',
93
- text: `Updated token "${args.token_id}".\n${JSON.stringify(doc, null, 2)}`,
94
- },
95
- ],
96
- isError: false,
97
- };
98
- }
99
-
100
- async function toolTokenDelete(args) {
101
- if (!args.token_id) throw inputError('token_id is required');
102
- const removed = await deleteToken(args.token_id, args.user);
103
- return {
104
- content: [
105
- {
106
- type: 'text',
107
- text: removed
108
- ? `Deleted token "${args.token_id}".`
109
- : `Token "${args.token_id}" was not configured.`,
110
- },
111
- ],
112
- isError: false,
113
- };
114
- }
115
-
116
- // ─── Device-level tokens ────────────────────────────────────────────
117
-
118
- async function toolDeviceTokenList(args) {
119
- if (!args.device_id) throw inputError('device is required');
120
- const tokens = await listDeviceTokens(args.device_id, args.user);
121
- if (tokens.length === 0) {
122
- return {
123
- content: [{ type: 'text', text: `No device tokens on device "${args.device_id}".` }],
124
- isError: false,
125
- };
126
- }
127
- return {
128
- content: [
129
- {
130
- type: 'text',
131
- text: `${tokens.length} device token(s) on "${args.device_id}":\n${JSON.stringify(tokens, null, 2)}`,
132
- },
133
- ],
134
- isError: false,
135
- };
136
- }
137
-
138
- async function toolDeviceTokenCreate(args) {
139
- if (!args.device_id) throw inputError('device is required');
140
- if (!args.token_name) throw inputError('token_name is required');
141
-
142
- const doc = await createDeviceToken(
143
- args.device_id,
144
- {
145
- token_name: args.token_name,
146
- token_resources: args.token_resources,
147
- token_expiration: args.token_expiration,
148
- },
149
- args.user,
150
- );
151
- return {
152
- content: [
153
- {
154
- type: 'text',
155
- text: `Created device token "${args.token_name}" on "${args.device_id}".\n${JSON.stringify(doc, null, 2)}`,
156
- },
157
- ],
158
- isError: false,
159
- };
160
- }
161
-
162
- async function toolDeviceTokenDelete(args) {
163
- if (!args.device_id) throw inputError('device is required');
164
- if (!args.token_id) throw inputError('token_id is required');
165
- const removed = await deleteDeviceToken(args.device_id, args.token_id, args.user);
166
- return {
167
- content: [
168
- {
169
- type: 'text',
170
- text: removed
171
- ? `Deleted device token "${args.token_id}" from "${args.device_id}".`
172
- : `Device token "${args.token_id}" was not configured on "${args.device_id}".`,
173
- },
174
- ],
175
- isError: false,
176
- };
177
- }
178
-
179
- // ─── Tool registration ───────────────────────────────────────────────
180
-
181
- const PERMISSION_GRAMMAR_NOTE = `Permission grammar (allow / deny):
182
- { "<ResourceType>": { "<id>"|"*": ["<Action>"|"*"] | "*" } }
183
-
184
- Resource types are platform categories (Device, Bucket, Product, Endpoint, Property, Token, User, …) plus "*" for any. The id level is a literal id or "*". Leaves are arrays of action strings or the wildcard "*".
185
-
186
- Limit: the grammar does NOT scope by product. Only by resource type + id. To restrict a token to one product's devices, enumerate device ids explicitly. See \`tokens.cpp:validate_token_permissions\` and \`tokens.hpp:PERMISSION_DEFINITION\` in the backend.
187
-
188
- Examples (verbatim from the server's CREATE_SCHEMA_EXAMPLE):
189
- - Admin Access:
190
- { token: "admin_access", name: "Admin Access", enabled: true, allow: { "*": { "*": "*" } } }
191
- Every resource, any name, any action.
192
- - Shared Access:
193
- { token: "shared_access", name: "Shared Access", enabled: true,
194
- allow: { "Bucket": { "mqtt_bucket": ["ReadBucket"] }, "Device": { "*": ["AccessDeviceResources"] } } }
195
- Reads from one bucket plus access to any device's resources.`;
196
-
197
- const EXPIRY_NOTE = `Expiry handling: pass either a unix-seconds number or a relative duration string ("30d", "12h", "1y", "90d"). Strings are converted client-side to absolute unix seconds before the request.`;
198
-
199
- export const tools = [
200
- {
201
- name: 'token_list',
202
- description: `List the user-level access tokens configured on the account. The response omits the JWT (\`access_token\` is excluded by the server's LIST_PROJECTION). Use \`{tool:token_get}\` to read a specific token's JWT.`,
203
- inputSchema: {
204
- type: 'object',
205
- properties: {
206
- user: { type: 'string', description: 'API user.' },
207
- },
208
- required: [],
209
- },
210
- handler: toolTokenList,
211
- },
212
- {
213
- name: 'token_get',
214
- description: `Read a single user-level access token, including the JWT (\`access_token\`). The JWT is shown only by this endpoint and on creation — list responses strip it.`,
215
- inputSchema: {
216
- type: 'object',
217
- properties: {
218
- token_id: {
219
- type: 'string',
220
- description: 'Token id (matches /^[a-zA-Z0-9_]{1,50}$/).',
221
- },
222
- user: { type: 'string', description: 'API user.' },
223
- },
224
- required: ['token_id'],
225
- },
226
- handler: toolTokenGet,
227
- },
228
- {
229
- name: 'token_create',
230
- description: `Create a user-level access token and issue a JWT scoped by an \`allow\` (and optional \`deny\`) permission tree.
231
-
232
- Use this to mint narrow-scope tokens you can hand off to scripts, integrations, or third parties so they can call the platform API on your behalf without your full credentials. The JWT is returned in the response — capture it immediately, it will not be visible from the list endpoint afterwards (only \`{tool:token_get}\` shows it).
233
-
234
- ${PERMISSION_GRAMMAR_NOTE}
235
-
236
- ${EXPIRY_NOTE}
237
-
238
- Example: a backup script that needs to invoke device resources for 90 days.
239
- { token_id: "backup_notifier", name: "Backup Notifier", allow: { "Device": { "*": ["AccessDeviceResources"] } }, expire: "90d" }`,
240
- inputSchema: {
241
- type: 'object',
242
- properties: {
243
- token_id: {
244
- type: 'string',
245
- description:
246
- 'Stable token id (letters, digits, underscore, ≤ 50 chars). Used as the URL key for read/update/delete.',
247
- },
248
- name: { type: 'string', description: 'Display name.' },
249
- description: { type: 'string', description: 'Free-form description.' },
250
- allow: {
251
- type: 'object',
252
- description:
253
- 'Permission tree. See the description for the grammar; example: { "Device": { "*": ["AccessDeviceResources"] } }.',
254
- additionalProperties: true,
255
- },
256
- deny: {
257
- type: 'object',
258
- description: 'Optional deny tree (same grammar as allow).',
259
- additionalProperties: true,
260
- },
261
- expire: {
262
- description:
263
- 'Expiry as unix seconds (number) or relative duration ("30d", "12h", "1y"). Omit for no expiry.',
264
- },
265
- enabled: {
266
- type: 'boolean',
267
- description: 'Default: true. Set to false to mint a token in disabled state.',
268
- },
269
- user: { type: 'string', description: 'API user.' },
270
- },
271
- required: ['token_id', 'name', 'allow'],
272
- },
273
- handler: toolTokenCreate,
274
- },
275
- {
276
- name: 'token_update',
277
- description: `Patch an existing token. Accepted fields per UPDATE_SCHEMA: name, description, enabled, expire, allow, deny. Pass at least one. The JWT itself cannot be rotated through this endpoint — delete and recreate to rotate.
278
-
279
- ${EXPIRY_NOTE}`,
280
- inputSchema: {
281
- type: 'object',
282
- properties: {
283
- token_id: { type: 'string', description: 'Token id to patch.' },
284
- name: { type: 'string' },
285
- description: { type: 'string' },
286
- enabled: { type: 'boolean' },
287
- expire: {
288
- description:
289
- 'Unix seconds (number) or relative duration ("30d", "12h"). Omit to leave unchanged.',
290
- },
291
- allow: { type: 'object', additionalProperties: true },
292
- deny: { type: 'object', additionalProperties: true },
293
- user: { type: 'string', description: 'API user.' },
294
- },
295
- required: ['token_id'],
296
- },
297
- handler: toolTokenUpdate,
298
- },
299
- {
300
- name: 'token_delete',
301
- description: `Delete a user-level token. Idempotent — reports cleanly when the token was already absent.`,
302
- inputSchema: {
303
- type: 'object',
304
- properties: {
305
- token_id: { type: 'string', description: 'Token id to remove.' },
306
- user: { type: 'string', description: 'API user.' },
307
- },
308
- required: ['token_id'],
309
- },
310
- handler: toolTokenDelete,
311
- },
312
-
313
- // ── Device tokens ──
314
- {
315
- name: 'device_token_list',
316
- description: `List the device-scoped tokens issued for a single device. Device tokens are a different kind from user-level tokens (\`{family:token}\`): they're issued via \`/devices/{id}/tokens\`, scoped optionally to a subset of the device's resources, and have a simpler \`{ token_name, token_resources?, token_expiration? }\` shape.`,
317
- inputSchema: {
318
- type: 'object',
319
- properties: {
320
- device_id: { type: 'string', description: 'Device ID.' },
321
- user: { type: 'string', description: 'API user.' },
322
- },
323
- required: ['device_id'],
324
- },
325
- handler: toolDeviceTokenList,
326
- },
327
- {
328
- name: 'device_token_create',
329
- description: `Issue a device-scoped token. Pass \`token_resources\` to restrict the token to a subset of the device's resources, or omit it for "all resources of this device". Pass \`token_expiration\` (unix seconds or relative like "30d") for an expiry, or omit for none.
330
-
331
- ${EXPIRY_NOTE}
332
-
333
- Example: a token that lets a remote script call only the \`monitoring\` and \`reboot\` resources of "device-007" for 24 hours.
334
- { device: "device-007", token_name: "ops-bot", token_resources: ["monitoring", "reboot"], token_expiration: "24h" }`,
335
- inputSchema: {
336
- type: 'object',
337
- properties: {
338
- device_id: { type: 'string', description: 'Device ID.' },
339
- token_name: { type: 'string', description: 'Display name for the new token.' },
340
- token_resources: {
341
- type: 'array',
342
- items: { type: 'string' },
343
- description:
344
- 'Resource names the token may access. Omit for ALL resources of the device.',
345
- },
346
- token_expiration: {
347
- description:
348
- 'Unix seconds (number) or relative duration ("30d", "24h"). Omit for no expiry.',
349
- },
350
- user: { type: 'string', description: 'API user.' },
351
- },
352
- required: ['device_id', 'token_name'],
353
- },
354
- handler: toolDeviceTokenCreate,
355
- },
356
- {
357
- name: 'device_token_delete',
358
- description: `Delete a device-scoped token. Idempotent.`,
359
- inputSchema: {
360
- type: 'object',
361
- properties: {
362
- device_id: { type: 'string', description: 'Device ID.' },
363
- token_id: { type: 'string', description: 'Token id to remove.' },
364
- user: { type: 'string', description: 'API user.' },
365
- },
366
- required: ['device_id', 'token_id'],
367
- },
368
- handler: toolDeviceTokenDelete,
369
- },
370
- ];