@thinremote/thinr-cli 1.3.1 → 1.3.3

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.
@@ -115,13 +115,11 @@ async function toolTokenDelete(args) {
115
115
  // ─── Device-level tokens ────────────────────────────────────────────
116
116
 
117
117
  async function toolDeviceTokenList(args) {
118
- if (!args.device) throw inputError('device is required');
119
- const tokens = await listDeviceTokens(args.device, args.user);
118
+ if (!args.device_id) throw inputError('device is required');
119
+ const tokens = await listDeviceTokens(args.device_id, args.user);
120
120
  if (tokens.length === 0) {
121
121
  return {
122
- content: [
123
- { type: 'text', text: `No device tokens on device "${args.device}".` },
124
- ],
122
+ content: [{ type: 'text', text: `No device tokens on device "${args.device_id}".` }],
125
123
  isError: false,
126
124
  };
127
125
  }
@@ -129,7 +127,7 @@ async function toolDeviceTokenList(args) {
129
127
  content: [
130
128
  {
131
129
  type: 'text',
132
- text: `${tokens.length} device token(s) on "${args.device}":\n${JSON.stringify(tokens, null, 2)}`,
130
+ text: `${tokens.length} device token(s) on "${args.device_id}":\n${JSON.stringify(tokens, null, 2)}`,
133
131
  },
134
132
  ],
135
133
  isError: false,
@@ -137,11 +135,11 @@ async function toolDeviceTokenList(args) {
137
135
  }
138
136
 
139
137
  async function toolDeviceTokenCreate(args) {
140
- if (!args.device) throw inputError('device is required');
138
+ if (!args.device_id) throw inputError('device is required');
141
139
  if (!args.token_name) throw inputError('token_name is required');
142
140
 
143
141
  const doc = await createDeviceToken(
144
- args.device,
142
+ args.device_id,
145
143
  {
146
144
  token_name: args.token_name,
147
145
  token_resources: args.token_resources,
@@ -153,7 +151,7 @@ async function toolDeviceTokenCreate(args) {
153
151
  content: [
154
152
  {
155
153
  type: 'text',
156
- text: `Created device token "${args.token_name}" on "${args.device}".\n${JSON.stringify(doc, null, 2)}`,
154
+ text: `Created device token "${args.token_name}" on "${args.device_id}".\n${JSON.stringify(doc, null, 2)}`,
157
155
  },
158
156
  ],
159
157
  isError: false,
@@ -161,16 +159,16 @@ async function toolDeviceTokenCreate(args) {
161
159
  }
162
160
 
163
161
  async function toolDeviceTokenDelete(args) {
164
- if (!args.device) throw inputError('device is required');
162
+ if (!args.device_id) throw inputError('device is required');
165
163
  if (!args.token_id) throw inputError('token_id is required');
166
- const removed = await deleteDeviceToken(args.device, args.token_id, args.user);
164
+ const removed = await deleteDeviceToken(args.device_id, args.token_id, args.user);
167
165
  return {
168
166
  content: [
169
167
  {
170
168
  type: 'text',
171
169
  text: removed
172
- ? `Deleted device token "${args.token_id}" from "${args.device}".`
173
- : `Device token "${args.token_id}" was not configured on "${args.device}".`,
170
+ ? `Deleted device token "${args.token_id}" from "${args.device_id}".`
171
+ : `Device token "${args.token_id}" was not configured on "${args.device_id}".`,
174
172
  },
175
173
  ],
176
174
  isError: false,
@@ -318,10 +316,10 @@ ${EXPIRY_NOTE}`,
318
316
  inputSchema: {
319
317
  type: 'object',
320
318
  properties: {
321
- device: { type: 'string', description: 'Device ID.' },
319
+ device_id: { type: 'string', description: 'Device ID.' },
322
320
  user: { type: 'string', description: 'API user.' },
323
321
  },
324
- required: ['device'],
322
+ required: ['device_id'],
325
323
  },
326
324
  handler: toolDeviceTokenList,
327
325
  },
@@ -336,7 +334,7 @@ Example: a token that lets a remote script call only the \`monitoring\` and \`re
336
334
  inputSchema: {
337
335
  type: 'object',
338
336
  properties: {
339
- device: { type: 'string', description: 'Device ID.' },
337
+ device_id: { type: 'string', description: 'Device ID.' },
340
338
  token_name: { type: 'string', description: 'Display name for the new token.' },
341
339
  token_resources: {
342
340
  type: 'array',
@@ -350,7 +348,7 @@ Example: a token that lets a remote script call only the \`monitoring\` and \`re
350
348
  },
351
349
  user: { type: 'string', description: 'API user.' },
352
350
  },
353
- required: ['device', 'token_name'],
351
+ required: ['device_id', 'token_name'],
354
352
  },
355
353
  handler: toolDeviceTokenCreate,
356
354
  },
@@ -360,11 +358,11 @@ Example: a token that lets a remote script call only the \`monitoring\` and \`re
360
358
  inputSchema: {
361
359
  type: 'object',
362
360
  properties: {
363
- device: { type: 'string', description: 'Device ID.' },
361
+ device_id: { type: 'string', description: 'Device ID.' },
364
362
  token_id: { type: 'string', description: 'Token id to remove.' },
365
363
  user: { type: 'string', description: 'API user.' },
366
364
  },
367
- required: ['device', 'token_id'],
365
+ required: ['device_id', 'token_id'],
368
366
  },
369
367
  handler: toolDeviceTokenDelete,
370
368
  },
package/lib/monitoring.js CHANGED
@@ -1,10 +1,6 @@
1
1
  import api from './api.js';
2
2
  import { apiError } from './errors.js';
3
- import { requireConfig } from './config.js';
4
-
5
- function resolveUser(user) {
6
- return user || requireConfig().username;
7
- }
3
+ import { v2, v3 } from './paths.js';
8
4
 
9
5
  /**
10
6
  * Query the monitoring bucket. When `device` is provided, scopes to
@@ -55,7 +51,6 @@ export async function getMonitoringData({
55
51
  fields,
56
52
  group_by,
57
53
  } = {}) {
58
- const apiUser = resolveUser(user);
59
54
  const params = { items, sort };
60
55
  if (minutes) {
61
56
  params.min_ts = Date.now() - minutes * 60 * 1000;
@@ -80,13 +75,12 @@ export async function getMonitoringData({
80
75
  .split(',')
81
76
  .map((s) => s.trim())
82
77
  .includes('device');
83
- params.fields =
84
- !device && !effectiveGroupBy && !hasDevice ? `device,${fields}` : fields;
78
+ params.fields = !device && !effectiveGroupBy && !hasDevice ? `device,${fields}` : fields;
85
79
  }
86
80
 
87
81
  const url = device
88
- ? `/v3/users/${apiUser}/devices/${device}/buckets/monitoring/data`
89
- : `/v2/users/${apiUser}/buckets/monitoring/data`;
82
+ ? `${v3(user)}/devices/${device}/buckets/monitoring/data`
83
+ : `${v2(user)}/buckets/monitoring/data`;
90
84
 
91
85
  try {
92
86
  const res = await api.get(url, { params });
package/lib/paths.js ADDED
@@ -0,0 +1,32 @@
1
+ // @ts-check
2
+ import { resolveUser } from './config.js';
3
+
4
+ /**
5
+ * Account-scoped base paths, one per API version.
6
+ *
7
+ * Every endpoint the CLI talks to hangs off `/v{N}/users/{account}`, and
8
+ * the account is the part that is easy to get wrong: leaving it out still
9
+ * produces a valid URL, one that works against your own account and fails
10
+ * only when acting on behalf of another — with a "not found" that points
11
+ * nowhere near the real problem. Building the prefix in one place keeps
12
+ * the account in the signature, where it is visible, and `tests/paths.test.js`
13
+ * fails the build if a module starts hand-rolling these again.
14
+ *
15
+ * Pass `user` to address another account; omit it for the active profile.
16
+ */
17
+ export function v1(user) {
18
+ return `/v1/users/${resolveUser(user)}`;
19
+ }
20
+
21
+ export function v2(user) {
22
+ return `/v2/users/${resolveUser(user)}`;
23
+ }
24
+
25
+ export function v3(user) {
26
+ return `/v3/users/${resolveUser(user)}`;
27
+ }
28
+
29
+ /** `/v3/users/{account}/devices/{deviceId}` — the most common prefix. */
30
+ export function device(user, deviceId) {
31
+ return `${v3(user)}/devices/${deviceId}`;
32
+ }
@@ -126,7 +126,7 @@ export const HANDLERS = {
126
126
  },
127
127
 
128
128
  async property_set(ctx, params) {
129
- await setDeviceProperty(ctx.deviceId, params.property, params.value);
129
+ await setDeviceProperty(ctx.deviceId, params.property, params.value, ctx.user);
130
130
  return `set ${params.property}`;
131
131
  },
132
132
 
@@ -275,7 +275,7 @@ export const CHECKERS = {
275
275
  async property_set(ctx, params) {
276
276
  let current;
277
277
  try {
278
- current = await getDeviceProperty(ctx.deviceId, params.property);
278
+ current = await getDeviceProperty(ctx.deviceId, params.property, ctx.user);
279
279
  } catch (err) {
280
280
  if (err?.code === 'not_found') {
281
281
  return { status: 'changed', summary: `create property ${params.property}` };
package/lib/product.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import api from './api.js';
2
2
  import { apiError, inputError } from './errors.js';
3
- import { requireConfig } from './config.js';
4
3
  import {
5
4
  storageExists,
6
5
  createStorage,
@@ -12,6 +11,7 @@ import {
12
11
  deleteStorageFile,
13
12
  } from './storage.js';
14
13
  import { parsePlaybook } from './playbook/loader.js';
14
+ import { v1 } from './paths.js';
15
15
 
16
16
  /**
17
17
  * Helpers for ThinRemote products and the product-script workflow.
@@ -28,14 +28,6 @@ import { parsePlaybook } from './playbook/loader.js';
28
28
  * sequence.
29
29
  */
30
30
 
31
- function resolveUser(user) {
32
- return user || requireConfig().username;
33
- }
34
-
35
- function v1(user) {
36
- return `/v1/users/${resolveUser(user)}`;
37
- }
38
-
39
31
  // ─── Low-level: products ─────────────────────────────────────────────
40
32
 
41
33
  export async function getProducts(user) {
package/lib/profile.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  import api from './api.js';
3
3
  import { apiError, inputError } from './errors.js';
4
- import { requireConfig } from './config.js';
4
+ import { v1 } from './paths.js';
5
5
 
6
6
  /**
7
7
  * Helpers for product *profile* configuration.
@@ -68,14 +68,6 @@ import { requireConfig } from './config.js';
68
68
  * device code change.
69
69
  */
70
70
 
71
- function resolveUser(user) {
72
- return user || requireConfig().username;
73
- }
74
-
75
- function v1(user) {
76
- return `/v1/users/${resolveUser(user)}`;
77
- }
78
-
79
71
  // ─── Listener config validators ──────────────────────────────────────
80
72
  //
81
73
  // Each `data.source` shape (for buckets/properties) and each request
package/lib/property.js CHANGED
@@ -1,28 +1,23 @@
1
- import { requireConfig } from './config.js';
1
+ import { resolveUser } from './config.js';
2
+ import { device } from './paths.js';
2
3
  import api from './api.js';
3
4
  import { apiError } from './errors.js';
4
5
  import { info, success } from './format.js';
5
6
 
6
- export async function getDeviceProperties(deviceId) {
7
- const config = requireConfig();
8
-
7
+ export async function getDeviceProperties(deviceId, user) {
9
8
  try {
10
- const response = await api.get(
11
- `/v3/users/${config.username}/devices/${deviceId}/properties`,
12
- );
9
+ const response = await api.get(`${device(user, deviceId)}/properties`);
13
10
  return response.data;
14
11
  } catch (error) {
15
- throw apiError(error, { notFound: `Device not found: ${deviceId}` });
12
+ throw apiError(error, {
13
+ notFound: `Device not found: ${deviceId} (account ${resolveUser(user)})`,
14
+ });
16
15
  }
17
16
  }
18
17
 
19
- export async function getDeviceProperty(deviceId, propertyId) {
20
- const config = requireConfig();
21
-
18
+ export async function getDeviceProperty(deviceId, propertyId, user) {
22
19
  try {
23
- const response = await api.get(
24
- `/v3/users/${config.username}/devices/${deviceId}/properties/${propertyId}`,
25
- );
20
+ const response = await api.get(`${device(user, deviceId)}/properties/${propertyId}`);
26
21
  return response.data.value;
27
22
  } catch (error) {
28
23
  throw apiError(error, {
@@ -33,17 +28,16 @@ export async function getDeviceProperty(deviceId, propertyId) {
33
28
 
34
29
  // PUT so the operation is idempotent — missing properties are created,
35
30
  // existing ones are overwritten wholesale.
36
- export async function setDeviceProperty(deviceId, propertyId, value) {
37
- const config = requireConfig();
38
-
31
+ export async function setDeviceProperty(deviceId, propertyId, value, user) {
39
32
  try {
40
- const response = await api.put(
41
- `/v3/users/${config.username}/devices/${deviceId}/properties/${propertyId}`,
42
- { value },
43
- );
33
+ const response = await api.put(`${device(user, deviceId)}/properties/${propertyId}`, {
34
+ value,
35
+ });
44
36
  return response.data;
45
37
  } catch (error) {
46
- throw apiError(error, { notFound: `Device not found: ${deviceId}` });
38
+ throw apiError(error, {
39
+ notFound: `Device not found: ${deviceId} (account ${resolveUser(user)})`,
40
+ });
47
41
  }
48
42
  }
49
43
 
@@ -59,11 +53,7 @@ export function formatDeviceProperties(deviceId, properties) {
59
53
 
60
54
  export function formatDeviceProperty(deviceId, propertyId, propertyValue) {
61
55
  let formattedValue =
62
- '\nValue of property ' +
63
- info(propertyId) +
64
- ' for device ' +
65
- info(deviceId) +
66
- ':\n';
56
+ '\nValue of property ' + info(propertyId) + ' for device ' + info(deviceId) + ':\n';
67
57
  const flattenedValue = flatten(propertyValue);
68
58
  for (const key in flattenedValue) {
69
59
  formattedValue += `${key}: ${success(flattenedValue[key])}\n`;
package/lib/resource.js CHANGED
@@ -1,28 +1,19 @@
1
- import { requireConfig } from './config.js';
1
+ import { resolveUser } from './config.js';
2
+ import { device } from './paths.js';
2
3
  import api from './api.js';
3
4
  import { apiError } from './errors.js';
4
5
  import { info, warning, hint } from './format.js';
5
6
 
6
- /**
7
- * Account the request is addressed to: `options.user` for admin access to
8
- * another account's devices, the active profile's own user otherwise.
9
- * Matches `createDeviceAPI`, so both halves of a device interaction (files
10
- * and resources) resolve the same way.
11
- */
12
- function apiUser(options = {}) {
13
- return options.user || requireConfig().username;
14
- }
15
-
16
7
  // Drops internal resources whose keys start with `$`; those belong to
17
8
  // the agent's own framework (cmd, fs, scripts…) and shouldn't surface
18
9
  // through the user-facing resource list.
19
10
  export async function getDeviceResources(deviceId, options = {}) {
20
- const user = apiUser(options);
11
+ const user = resolveUser(options.user);
21
12
 
22
13
  try {
23
14
  // `/resources/api` merges firmware resources with API resources
24
15
  // inherited from the product profile, which `/resources` alone omits.
25
- const response = await api.get(`/v3/users/${user}/devices/${deviceId}/resources/api`);
16
+ const response = await api.get(`${device(user, deviceId)}/resources/api`);
26
17
  return Object.fromEntries(
27
18
  Object.entries(response.data).filter(([key]) => !key.startsWith('$')),
28
19
  );
@@ -39,11 +30,9 @@ export async function getDeviceResources(deviceId, options = {}) {
39
30
  * sample, not to trigger the input side.
40
31
  */
41
32
  export async function readDeviceResource(deviceId, resourceId, options = {}) {
42
- const user = apiUser(options);
33
+ const user = resolveUser(options.user);
43
34
  try {
44
- const response = await api.get(
45
- `/v3/users/${user}/devices/${deviceId}/resources/${resourceId}`,
46
- );
35
+ const response = await api.get(`${device(user, deviceId)}/resources/${resourceId}`);
47
36
  return response.data;
48
37
  } catch (error) {
49
38
  throw apiError(error, {
@@ -62,11 +51,9 @@ export async function readDeviceResource(deviceId, resourceId, options = {}) {
62
51
  * exposes /api — callers should tolerate 404/501.
63
52
  */
64
53
  export async function getDeviceResourceApi(deviceId, resourceId, options = {}) {
65
- const user = apiUser(options);
54
+ const user = resolveUser(options.user);
66
55
  try {
67
- const response = await api.get(
68
- `/v3/users/${user}/devices/${deviceId}/resources/${resourceId}/api`,
69
- );
56
+ const response = await api.get(`${device(user, deviceId)}/resources/${resourceId}/api`);
70
57
  return response.data;
71
58
  } catch (error) {
72
59
  throw apiError(error, {
@@ -76,11 +63,11 @@ export async function getDeviceResourceApi(deviceId, resourceId, options = {}) {
76
63
  }
77
64
 
78
65
  export async function executeDeviceResource(deviceId, resourceId, inputs, options = {}) {
79
- const user = apiUser(options);
66
+ const user = resolveUser(options.user);
80
67
 
81
68
  try {
82
69
  const response = await api.post(
83
- `/v3/users/${user}/devices/${deviceId}/resources/${resourceId}`,
70
+ `${device(user, deviceId)}/resources/${resourceId}`,
84
71
  inputs,
85
72
  );
86
73
  return response.data;
package/lib/status.js CHANGED
@@ -1,29 +1,23 @@
1
- import { requireConfig } from './config.js';
1
+ import { resolveUser } from './config.js';
2
+ import { v1 } from './paths.js';
2
3
  import api from './api.js';
3
4
  import { apiError } from './errors.js';
4
- import {
5
- formatUptime,
6
- colorPct,
7
- formatBytes,
8
- label,
9
- muted,
10
- success,
11
- error,
12
- } from './format.js';
5
+ import { formatUptime, colorPct, formatBytes, label, muted, success, error } from './format.js';
13
6
 
14
7
  /**
15
8
  * Get device status
16
9
  * @param {string} deviceId - Device ID to check
10
+ * @param {string} [user] - Account that owns the device (defaults to the active profile)
17
11
  * @returns {Promise<Object>} Status information
18
12
  */
19
- export async function getDeviceStatus(deviceId) {
20
- const config = requireConfig();
21
-
13
+ export async function getDeviceStatus(deviceId, user) {
22
14
  try {
23
- const response = await api.get(`/v1/users/${config.username}/devices/${deviceId}/stats`);
15
+ const response = await api.get(`${v1(user)}/devices/${deviceId}/stats`);
24
16
  return response.data;
25
17
  } catch (error) {
26
- throw apiError(error, { notFound: `Device not found: ${deviceId}` });
18
+ throw apiError(error, {
19
+ notFound: `Device not found: ${deviceId} (account ${resolveUser(user)})`,
20
+ });
27
21
  }
28
22
  }
29
23
 
@@ -80,17 +74,13 @@ export function formatDeviceStatus(deviceId, status, monitoring = null) {
80
74
  const root = monitoring.disk.root;
81
75
  const detail =
82
76
  root.available != null && root.total != null
83
- ? muted(
84
- ` (${formatBytes(root.available)} free of ${formatBytes(root.total)})`,
85
- )
77
+ ? muted(` (${formatBytes(root.available)} free of ${formatBytes(root.total)})`)
86
78
  : '';
87
79
  lines.push(` Disk: ${colorPct(root.usage)}${detail}`);
88
80
  }
89
81
  if (monitoring.load) {
90
82
  const l = monitoring.load;
91
- const parts = [l['1m'], l['5m'], l['15m']].map((v) =>
92
- v != null ? v.toFixed(2) : '—',
93
- );
83
+ const parts = [l['1m'], l['5m'], l['15m']].map((v) => (v != null ? v.toFixed(2) : '—'));
94
84
  lines.push(` Load: ${parts.join(' · ')}`);
95
85
  }
96
86
  if (monitoring.uptime != null) {
package/lib/storage.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import api from './api.js';
2
2
  import { apiError } from './errors.js';
3
- import { requireConfig } from './config.js';
3
+ import { v1, v2 } from './paths.js';
4
4
 
5
5
  /**
6
6
  * Helpers for ThinRemote file storages — independent buckets that the
@@ -13,17 +13,6 @@ import { requireConfig } from './config.js';
13
13
  * so orchestration code can stay branch-light.
14
14
  */
15
15
 
16
- function resolveUser(user) {
17
- return user || requireConfig().username;
18
- }
19
-
20
- function v1(user) {
21
- return `/v1/users/${resolveUser(user)}`;
22
- }
23
- function v2(user) {
24
- return `/v2/users/${resolveUser(user)}`;
25
- }
26
-
27
16
  export async function getStorage(storageId, user) {
28
17
  try {
29
18
  const res = await api.get(`${v1(user)}/storages/${storageId}`);
package/lib/tokens.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  import api from './api.js';
3
3
  import { apiError, inputError } from './errors.js';
4
- import { requireConfig } from './config.js';
4
+ import { v1 } from './paths.js';
5
5
 
6
6
  /**
7
7
  * Helpers for ThinRemote access tokens — both user-level (broad scope,
@@ -60,14 +60,6 @@ import { requireConfig } from './config.js';
60
60
  * it.
61
61
  */
62
62
 
63
- function resolveUser(user) {
64
- return user || requireConfig().username;
65
- }
66
-
67
- function v1(user) {
68
- return `/v1/users/${resolveUser(user)}`;
69
- }
70
-
71
63
  // ─── Duration helpers ───────────────────────────────────────────────
72
64
 
73
65
  const DURATION_RE = /^(\d+)([smhdwy])$/;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinremote/thinr-cli",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "CLI for ThinRemote — remote management for IoT devices, with an MCP server for AI clients",
5
5
  "main": "bin/thinr.js",
6
6
  "bin": {