@thinremote/thinr-cli 1.0.2-alpha.2 → 1.1.0-alpha.1

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +362 -87
  3. package/bin/thinr.js +51 -11
  4. package/commands/device/_shared.js +48 -0
  5. package/commands/device/console.js +15 -0
  6. package/commands/device/exec.js +76 -0
  7. package/commands/device/index.js +31 -0
  8. package/commands/device/list.js +43 -0
  9. package/commands/device/property.js +50 -0
  10. package/commands/device/proxy.js +42 -0
  11. package/commands/device/resource.js +51 -0
  12. package/commands/device/status.js +26 -0
  13. package/commands/device/update.js +53 -0
  14. package/commands/logout.js +12 -5
  15. package/commands/product.js +146 -177
  16. package/commands/profile.js +105 -0
  17. package/lib/api.js +54 -31
  18. package/lib/auth.js +225 -309
  19. package/lib/config.js +176 -45
  20. package/lib/console.js +87 -108
  21. package/lib/device-api.js +257 -0
  22. package/lib/devices.js +69 -0
  23. package/lib/errors.js +53 -0
  24. package/lib/mcp/helpers.js +34 -0
  25. package/lib/mcp/registry.js +32 -0
  26. package/lib/mcp/server.js +63 -0
  27. package/lib/mcp/tools-devices.js +194 -0
  28. package/lib/mcp/tools-exec.js +213 -0
  29. package/lib/mcp/tools-filesystem.js +163 -0
  30. package/lib/mcp/tools-products.js +232 -0
  31. package/lib/mcp/tools-profiles.js +30 -0
  32. package/lib/mcp/tools-properties.js +79 -0
  33. package/lib/mcp/tools-resources.js +76 -0
  34. package/lib/mcp/tools-scripts.js +172 -0
  35. package/lib/monitoring.js +72 -0
  36. package/lib/output.js +114 -0
  37. package/lib/product.js +373 -0
  38. package/lib/property.js +41 -57
  39. package/lib/proxy.js +58 -90
  40. package/lib/resource.js +106 -72
  41. package/lib/status.js +7 -33
  42. package/lib/storage.js +128 -0
  43. package/package.json +68 -37
  44. package/.github/workflows/publish.yml +0 -79
  45. package/commands/device.js +0 -268
  46. package/lib/device.js +0 -76
package/lib/storage.js ADDED
@@ -0,0 +1,128 @@
1
+ import api from './api.js';
2
+ import { apiError } from './errors.js';
3
+ import { requireConfig } from './config.js';
4
+
5
+ /**
6
+ * Helpers for ThinRemote file storages — independent buckets that the
7
+ * platform exposes alongside products. The product script workflow
8
+ * uses them to host the actual JS files; storages are also useful on
9
+ * their own (config blobs, firmware artefacts, etc.).
10
+ *
11
+ * Functions return raw data and throw `apiError` on failure, except
12
+ * the `*Exists` and `delete*` helpers which use boolean return values
13
+ * so orchestration code can stay branch-light.
14
+ */
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
+ export async function getStorage(storageId, user) {
28
+ try {
29
+ const res = await api.get(`${v1(user)}/storages/${storageId}`);
30
+ return res.data || {};
31
+ } catch (e) {
32
+ throw apiError(e, { notFound: `Storage not found: ${storageId}` });
33
+ }
34
+ }
35
+
36
+ export async function storageExists(storageId, user) {
37
+ try {
38
+ await api.get(`${v1(user)}/storages/${storageId}`);
39
+ return true;
40
+ } catch (e) {
41
+ if (e.response?.status === 404) return false;
42
+ throw apiError(e);
43
+ }
44
+ }
45
+
46
+ export async function createStorage(
47
+ storageId,
48
+ user,
49
+ { name, description, public: isPublic = false } = {},
50
+ ) {
51
+ try {
52
+ await api.post(`${v1(user)}/storages`, {
53
+ storage: storageId,
54
+ name: name || storageId,
55
+ description: description || `Storage for ${storageId}`,
56
+ public: isPublic,
57
+ });
58
+ } catch (e) {
59
+ throw apiError(e);
60
+ }
61
+ }
62
+
63
+ export async function deleteStorage(storageId, user) {
64
+ try {
65
+ await api.delete(`${v1(user)}/storages/${storageId}`);
66
+ return true;
67
+ } catch (e) {
68
+ if (e.response?.status === 404) return false;
69
+ throw apiError(e);
70
+ }
71
+ }
72
+
73
+ export async function listStorageFiles(storageId, user) {
74
+ try {
75
+ const res = await api.get(`${v1(user)}/storages/${storageId}/files`);
76
+ return res.data || [];
77
+ } catch (e) {
78
+ if (e.response?.status === 404) return [];
79
+ throw apiError(e);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Read a file as raw text (no JSON parsing). The agent stores script
85
+ * bodies as `text/plain` — parsing them as JSON would corrupt them.
86
+ */
87
+ export async function readStorageFile(storageId, filePath, user) {
88
+ try {
89
+ const res = await api.get(`${v2(user)}/storages/${storageId}/files/${filePath}`, {
90
+ transformResponse: [(x) => x],
91
+ });
92
+ return res.data;
93
+ } catch (e) {
94
+ throw apiError(e, { notFound: `File not found in storage "${storageId}": ${filePath}` });
95
+ }
96
+ }
97
+
98
+ export async function storageFileExists(storageId, filePath, user) {
99
+ try {
100
+ await api.get(`${v2(user)}/storages/${storageId}/files/${filePath}`);
101
+ return true;
102
+ } catch (e) {
103
+ if (e.response?.status === 404) return false;
104
+ throw apiError(e);
105
+ }
106
+ }
107
+
108
+ export async function writeStorageFile(storageId, filePath, content, user) {
109
+ try {
110
+ await api.put(
111
+ `${v2(user)}/storages/${storageId}/files/${filePath}?overwrite=true`,
112
+ content,
113
+ { headers: { 'Content-Type': 'text/plain' } },
114
+ );
115
+ } catch (e) {
116
+ throw apiError(e);
117
+ }
118
+ }
119
+
120
+ export async function deleteStorageFile(storageId, filePath, user) {
121
+ try {
122
+ await api.delete(`${v2(user)}/storages/${storageId}/files/${filePath}`);
123
+ return true;
124
+ } catch (e) {
125
+ if (e.response?.status === 404) return false;
126
+ throw apiError(e);
127
+ }
128
+ }
package/package.json CHANGED
@@ -1,39 +1,70 @@
1
1
  {
2
- "name": "@thinremote/thinr-cli",
3
- "version": "1.0.2-alpha.2",
4
- "description": "CLI for ThinRemote - Remote management for IoT devices",
5
- "main": "bin/thinr.js",
6
- "bin": {
7
- "thinr": "bin/thinr.js"
8
- },
9
- "scripts": {
10
- "start": "node bin/thinr.js",
11
- "test": "echo \"Error: no test specified\" && exit 1"
12
- },
13
- "keywords": [
14
- "cli",
15
- "iot",
16
- "remote",
17
- "management",
18
- "thing",
19
- "thinger"
20
- ],
21
- "author": "ThinRemote",
22
- "license": "MIT",
23
- "dependencies": {
24
- "axios": "^1.10.0",
25
- "chalk": "^5.4.1",
26
- "commander": "^14.0.0",
27
- "conf": "^14.0.0",
28
- "figlet": "^1.8.1",
29
- "@inquirer/prompts": "^7.5.3",
30
- "jsonwebtoken": "^9.0.2",
31
- "open": "^10.1.2",
32
- "ora": "^8.2.0",
33
- "ws": "^8.18.2"
34
- },
35
- "type": "module",
36
- "engines": {
37
- "node": ">=14.16"
38
- }
2
+ "name": "@thinremote/thinr-cli",
3
+ "version": "1.1.0-alpha.1",
4
+ "description": "CLI for ThinRemote — remote management for IoT devices, with an MCP server for AI clients",
5
+ "main": "bin/thinr.js",
6
+ "bin": {
7
+ "thinr": "bin/thinr.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "commands",
12
+ "lib",
13
+ "CHANGELOG.md",
14
+ "LICENSE",
15
+ "README.md"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/Thin-Remote/thinr-cli.git"
20
+ },
21
+ "bugs": "https://github.com/Thin-Remote/thinr-cli/issues",
22
+ "homepage": "https://github.com/Thin-Remote/thinr-cli#readme",
23
+ "scripts": {
24
+ "start": "node bin/thinr.js",
25
+ "lint": "eslint .",
26
+ "lint:fix": "eslint . --fix",
27
+ "format": "prettier --write .",
28
+ "format:check": "prettier --check .",
29
+ "typecheck": "tsc",
30
+ "test": "node --test 'tests/**/*.test.js'"
31
+ },
32
+ "keywords": [
33
+ "cli",
34
+ "iot",
35
+ "remote",
36
+ "management",
37
+ "thinremote",
38
+ "thinr",
39
+ "thinger",
40
+ "mcp"
41
+ ],
42
+ "author": "ThinRemote",
43
+ "license": "MIT",
44
+ "dependencies": {
45
+ "@inquirer/prompts": "^7.5.3",
46
+ "@modelcontextprotocol/sdk": "^1.29.0",
47
+ "axios": "^1.10.0",
48
+ "chalk": "^5.4.1",
49
+ "commander": "^14.0.0",
50
+ "conf": "^14.0.0",
51
+ "figlet": "^1.8.1",
52
+ "jsonwebtoken": "^9.0.2",
53
+ "open": "^10.1.2",
54
+ "ora": "^8.2.0",
55
+ "ws": "^8.18.2"
56
+ },
57
+ "type": "module",
58
+ "engines": {
59
+ "node": ">=18"
60
+ },
61
+ "devDependencies": {
62
+ "@eslint/js": "^10.0.1",
63
+ "@types/node": "^25.6.0",
64
+ "eslint": "^10.2.0",
65
+ "eslint-config-prettier": "^10.1.8",
66
+ "globals": "^17.5.0",
67
+ "prettier": "^3.8.3",
68
+ "typescript": "^6.0.2"
69
+ }
39
70
  }
@@ -1,79 +0,0 @@
1
- name: "publish"
2
-
3
- on:
4
- push:
5
- branches: [ main, develop ]
6
-
7
- jobs:
8
- publish:
9
- name: publish
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
-
14
- - name: Extract branch name
15
- shell: bash
16
- run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
17
-
18
- - name: checkout
19
- uses: actions/checkout@v4
20
- with:
21
- fetch-depth: 0
22
- ref: ${{ env.branch }}
23
-
24
- - name: node
25
- uses: actions/setup-node@v4
26
- with:
27
- node-version: 22
28
- registry-url: https://registry.npmjs.org
29
-
30
- - name: Get tag version
31
- run: |
32
- echo "tag_version=`cat package.json | jq '.version' | tr -d '"'`" >> $GITHUB_ENV
33
-
34
- - name: Check if tag is for develop and if it already exists
35
- run: |
36
- git fetch --prune --tags
37
- if [ ${{ env.branch }} == 'develop' ]; then
38
- for i in {1..100}; do
39
- if [ `git tag --list | grep ${{ env.tag_version }}"-alpha.${i}" | wc -l` -eq 0 ]; then
40
- tag_version=${{ env.tag_version }}"-alpha.${i}"
41
- echo "tag_version=${tag_version}" >> $GITHUB_ENV
42
- echo "npm_tag_version=develop" >> $GITHUB_ENV
43
- break;
44
- fi
45
- done
46
- elif [ ${{ env.branch }} == 'main' ]; then
47
- echo "npm_tag_version=latest" >> $GITHUB_ENV
48
- elif [ `git tag --list | grep -q ${{ env.tag_version }}` -ne 0 ]; then exit 1; fi
49
-
50
- - name: update version
51
- run: |
52
- npm version --no-git-tag-version --allow-same-version ${{ env.tag_version }}
53
-
54
- - name: publish
55
- run: npm publish --tag ${{ env.npm_tag_version }} --access public
56
- env:
57
- NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
58
-
59
- - name: Get changelog body
60
- run: |
61
- awk '/^## \[[0-9]+/,EOF' CHANGELOG.md | awk '(NR>1)' | awk '1;/^## \[[0-9]+/{exit}' | sed '$d' > release_body.md
62
-
63
- - name: Set pre-release boolean
64
- run: |
65
- prerelease=false
66
- if [ ${{ env.branch }} == 'develop' ]; then
67
- prerelease=true
68
- fi
69
- echo "prerelease=$prerelease" >> $GITHUB_ENV
70
-
71
- - name: Create GitHub release
72
- uses: ncipollo/release-action@v1
73
- with:
74
- prerelease: ${{ env.prerelease }}
75
- commit: ${{ env.branch }}
76
- tag: ${{ env.tag_version }}
77
- name: ${{ env.tag_version }}
78
- token: ${{ secrets.GITHUB_TOKEN }}
79
- bodyFile: "release_body.md"
@@ -1,268 +0,0 @@
1
- import { handleProxyAction } from '../lib/proxy.js';
2
- import { connectToDeviceConsole } from '../lib/console.js';
3
- import { getDeviceStatus, formatDeviceStatus } from '../lib/status.js';
4
- import { formatDeviceProperty, getDeviceProperty, getDeviceProperties, formatDeviceProperties } from '../lib/property.js';
5
- import chalk from 'chalk';
6
- import ora from 'ora';
7
- import { configExists } from '../lib/config.js';
8
- import { getDeviceResources, executeDeviceResource, formatDeviceResources } from "../lib/resource.js";
9
-
10
-
11
- /**
12
- * Register the device command with all device-related subcommands
13
- * @param {Command} program - Commander program instance
14
- */
15
- export function deviceCommand(program) {
16
-
17
- const device = program.command('device').description('Device commands');
18
-
19
- device
20
- .argument('<deviceId>', 'ID of the device to manage')
21
- .description('Manage device connections and proxies')
22
- .allowUnknownOption(true)
23
- .allowExcessArguments(true)
24
- .action(async (deviceId, options, command) => {
25
- // Get additional arguments manually
26
- const args = command.args.slice(1); // Skip deviceId
27
- const action = args[0];
28
- const target = args[1] && !args[1].startsWith('-') ? args[1] : null; // Check if target is provided, skip if it's an option
29
-
30
- // Check if configured
31
- if (!configExists()) {
32
- console.error(chalk.red('Error: Not configured. Run thinr without parameters to set up.'));
33
- process.exit(1);
34
- }
35
-
36
- // If no action provided, show help
37
- if (!action) {
38
- printHelp(deviceId);
39
- return;
40
- }
41
-
42
- // Parse options manually from process.argv
43
- const parsedOptions = {
44
- port: null,
45
- json: false,
46
- openBrowser: true,
47
- inputs: {},
48
- field: "",
49
- };
50
-
51
- const argv = process.argv;
52
- for (let i = 0; i < argv.length; i++) {
53
- if (argv[i] === '-p' || argv[i] === '--port') {
54
- parsedOptions.port = argv[i + 1];
55
- i++; // Skip the value
56
- } else if (argv[i] === '-j' || argv[i] === '--json') {
57
- parsedOptions.json = true;
58
- } else if (argv[i] === '--no-open') {
59
- parsedOptions.open = false;
60
- } else if (argv[i] === '-i' || argv[i] === '--input') {
61
- let inputPair = argv[i + 1].split('=');
62
- parsedOptions.inputs[inputPair[0]] = inputPair[1];
63
- } else if (argv[i] === '-f' || argv[i] === '--field') {
64
- parsedOptions.field = argv[i + 1];
65
- } else if (argv[i] === '-h' || argv[i] === '--help') {
66
- printHelp(deviceId);
67
- return;
68
- }
69
- }
70
-
71
- switch (action.toLowerCase()) {
72
- case 'tcp':
73
- await handleProxyAction(deviceId, 'tcp', target, parsedOptions);
74
- break;
75
-
76
- case 'tls':
77
- await handleProxyAction(deviceId, 'tls', target, parsedOptions);
78
- break;
79
-
80
- case 'http':
81
- await handleProxyAction(deviceId, 'http', target, parsedOptions);
82
- break;
83
-
84
- case 'console':
85
- try {
86
- await connectToDeviceConsole(deviceId);
87
- } catch (error) {
88
- console.error(chalk.red(`Error: ${error.message}`));
89
- process.exit(1);
90
- }
91
- break;
92
-
93
- case 'status': {
94
- const spinner = ora(`Checking status of ${deviceId}...`).start();
95
- try {
96
- const status = await getDeviceStatus(deviceId);
97
- spinner.succeed(`Device ${deviceId} found`);
98
-
99
- if (parsedOptions.json) {
100
- console.log(JSON.stringify(status, null, 2));
101
- } else {
102
- console.log(formatDeviceStatus(deviceId, status));
103
- }
104
- } catch (error) {
105
- spinner.fail(`Status check failed`);
106
- console.error(chalk.red(`Error: ${error.message}`));
107
- process.exit(1);
108
- }
109
- break;
110
- }
111
-
112
- case 'property': {
113
-
114
- if ( !target ) {
115
- const spinner = ora(`Getting properties for ${deviceId}...`).start();
116
- try {
117
- const result = await getDeviceProperties(deviceId);
118
- spinner.succeed(`Properties for ${deviceId} retrieved successfully`);
119
- if (parsedOptions.json) {
120
- console.log(result);
121
- } else {
122
- console.log(chalk.green(`Resource result for device ${deviceId}:`));
123
- console.log(formatDeviceProperties(deviceId, result));
124
- }
125
- } catch (error) {
126
- spinner.fail(`Property retrieval failed`);
127
- console.error(chalk.red(`Error: ${error.message}`));
128
- process.exit(1);
129
- }
130
- } else {
131
- const spinner = ora(`Retrieving property for ${deviceId}...`).start();
132
- try {
133
- const property = await getDeviceProperty(deviceId, target);
134
- spinner.succeed(`Property ${target} for ${deviceId} found`);
135
-
136
- if (parsedOptions.json) {
137
- if ( parsedOptions.field ) {
138
- console.log(parsedOptions.field.split('.').reduce((obj, key) => obj && obj[key], property));
139
- } else {
140
- console.log(JSON.stringify(property, null, 2));
141
- }
142
- } else {
143
- console.log(formatDeviceProperty(deviceId, target, property));
144
- }
145
- } catch (error) {
146
- spinner.fail(`Property retrieval failed`);
147
- console.error(chalk.red(`Error: ${error.message}`));
148
- process.exit(1);
149
- }
150
-
151
- }
152
-
153
- break;
154
- }
155
-
156
- case 'resource': {
157
-
158
- if (!target) {
159
- const spinner = ora(`Getting resources for ${deviceId}...`).start();
160
- try {
161
-
162
- const result = await getDeviceResources(deviceId);
163
- spinner.succeed(`Succesfully retrieved resources for ${deviceId}`);
164
-
165
- if (parsedOptions.json) {
166
- console.log(result);
167
- } else {
168
- //console.log(chalk.green(`Resource ${target} result for device ${deviceId}:`));
169
- console.log(chalk.green(`Resource result for device ${deviceId}:`));
170
- console.log(formatDeviceResources(deviceId, result));
171
- }
172
- } catch (error) {
173
- spinner.fail(`Resource retrieval failed`);
174
- console.error(chalk.red(`Error: ${error.message}`));
175
- process.exit(1);
176
- }
177
-
178
- } else {
179
- const spinner = ora(`Executing resource ${target} for ${deviceId}...`).start();
180
- try {
181
-
182
- const result = await executeDeviceResource(deviceId, target, parsedOptions.inputs);
183
- spinner.succeed(`Resource ${target} executed successfully for ${deviceId}`);
184
-
185
- if ( parsedOptions.field ) {
186
- console.log(parsedOptions.field.split('.').reduce((obj, key) => obj && obj[key], result));
187
- } else {
188
- console.log(result);
189
- }
190
-
191
- } catch (error) {
192
- spinner.fail(`Resource execution failed`);
193
- console.error(chalk.red(`Error: ${error.message}`));
194
- process.exit(1);
195
- }
196
- }
197
-
198
- break;
199
- }
200
-
201
- default:
202
- console.error(chalk.red(`Error: Unknown action '${action}'. Available actions: tcp, tls, http, console, status, property, resource.\n`));
203
- printHelp(deviceId);
204
- process.exit(1);
205
- }
206
- });
207
-
208
- }
209
- function printHelp(deviceId) {
210
- console.log(`Usage: thinr device ${deviceId} <command> [options]
211
-
212
- Available commands for device "${deviceId}":
213
-
214
- tcp [target] Create a TCP proxy (no TLS)
215
- Example: thinr device ${deviceId} tcp 22
216
-
217
- tls [target] Create a TLS proxy
218
- Example: thinr device ${deviceId} tls 443
219
-
220
- http [target] Create an HTTP proxy
221
- Example: thinr device ${deviceId} http 8080
222
-
223
- console Open terminal console
224
- Example: thinr device ${deviceId} console
225
-
226
- status Check device status
227
- Example: thinr device ${deviceId} status
228
-
229
- property Get device properties
230
- Example: thinr device ${deviceId} property
231
-
232
- property [propertyId] Get a specific property of the device
233
- Example: thinr device ${deviceId} property uptime
234
-
235
- resource Get resources of the device
236
- Example: thinr device ${deviceId} resource
237
-
238
- resource [resource] Execute a specific resource of the device
239
- Example: thinr device ${deviceId} resource reboot
240
-
241
- Arguments:
242
-
243
- target Target for proxy commands (address:port, port, or address)
244
- Defaults: tcp=22, tls=443, http=80
245
-
246
- propertyId ID of the property to retrieve (for property command)
247
-
248
- resource Name of the resource to execute (for resource command)
249
-
250
- Options (for proxy commands):
251
- -p, --port <port> Local port to use (default: random)
252
- --no-open Do not open browser (http only)
253
-
254
- Options (for status command):
255
- -j, --json Output as JSON
256
-
257
- Options (for property command):
258
- -j, --json Output as JSON
259
- -f, --field <field> Field to extract from the property (e.g., -f data.value)
260
-
261
- Options (for resource command):
262
- -i, --input <input>=<value> Input for the resource (e.g., -i param1=value1 -i param2=value2)
263
- -j, --json Output as JSON
264
- -f, --field <field> Field to extract from the property (e.g., -f data.value)
265
-
266
- Use "thinr device ${deviceId} <command> --help" for more details on each command.`);
267
-
268
- }
package/lib/device.js DELETED
@@ -1,76 +0,0 @@
1
- import chalk from 'chalk';
2
- import { readConfig } from './config.js';
3
- import api from './api.js';
4
-
5
- /**
6
- * Get devices belonging to a product
7
- * @param {Object} filter - Filters to query devices
8
- * @return {Promise<Array>} List of devices
9
- */
10
- export async function getDevices(filter) {
11
- // Get configuration
12
- const config = readConfig();
13
-
14
- if (!config.token || !config.server || !config.username) {
15
- throw new Error('Not configured. Run thinr without parameters to set up.');
16
- }
17
-
18
- let devices = [];
19
-
20
- // Query parameters to string
21
- let queryParametersString = "";
22
- for (const [key, value] of Object.entries(filter)) {
23
- if ( value ) queryParametersString += key+"="+value+"&";
24
- }
25
-
26
- try {
27
- const count = 50;
28
- let url = `/v1/users/${config.username}/devices?count=${count}&${queryParametersString}`;
29
-
30
- let index = 0;
31
- let res_length = 0;
32
-
33
- do {
34
- // Fetch devices for the product
35
- const response = await api.get(
36
- `${url}index=${index}`,
37
- );
38
-
39
- res_length = response.data.length;
40
- index += res_length;
41
- devices = devices.concat(response.data);
42
-
43
- } while ( res_length === count );
44
-
45
- return devices;
46
-
47
- } catch (error) {
48
- if (error.response) {
49
- // The request was made and the server responded with a status code
50
- if (error.response.status === 404) {
51
- throw new Error(`Product not found: ${productId}`);
52
- } else if (error.response.status === 401) {
53
- throw new Error('Unauthorized. Your token may have expired. Please reconfigure.');
54
- } else {
55
- throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`);
56
- }
57
- } else if (error.request) {
58
- // The request was made but no response was received
59
- throw new Error('No response from server. Please check your connection.');
60
- } else {
61
- // Something happened in setting up the request that triggered an Error
62
- throw new Error(`Error: ${error.message}`);
63
- }
64
- }
65
-
66
- }
67
-
68
- /**
69
- * Filter active devices from a list of devices
70
- * @param {Array} devices - List of devices to filter
71
- * @return {Array} List of active devices
72
- */
73
- export function filterActiveDevices(devices) {
74
- return devices.filter(device => device.connection && device.connection.active === true);
75
- }
76
-