@thinger-io/cli-core 0.4.2 → 0.5.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +195 -0
  2. package/README.md +82 -31
  3. package/lib/bucket-export.js +91 -0
  4. package/lib/cli.js +106 -50
  5. package/lib/commands/_shared.js +89 -1
  6. package/lib/commands/api.js +90 -0
  7. package/lib/commands/arguments.js +62 -0
  8. package/lib/commands/bucket.js +52 -44
  9. package/lib/commands/dashboard.js +5 -2
  10. package/lib/commands/data.js +539 -0
  11. package/lib/commands/device.js +128 -241
  12. package/lib/commands/grammar.js +279 -0
  13. package/lib/commands/product.js +104 -214
  14. package/lib/commands/profile.js +1 -0
  15. package/lib/commands/resources.js +975 -0
  16. package/lib/dashboard-init.js +4 -3
  17. package/lib/dashboard.js +10 -2
  18. package/lib/mcp/catalog.js +301 -63
  19. package/lib/mcp/core-types.js +36 -0
  20. package/lib/mcp/helpers.js +23 -2
  21. package/lib/mcp/naming.js +20 -0
  22. package/lib/mcp/registry.js +18 -67
  23. package/lib/mcp/resource-hooks.js +19 -5
  24. package/lib/mcp/server.js +43 -11
  25. package/lib/mcp/tools-resource.js +1232 -330
  26. package/lib/mcp/types/bucket-data.js +151 -0
  27. package/lib/mcp/{tools-dashboards.js → types/dashboard-apps.js} +99 -37
  28. package/lib/mcp/types/device-resource.js +117 -0
  29. package/lib/mcp/types/doc.js +35 -0
  30. package/lib/mcp/types/plugin-call.js +75 -0
  31. package/lib/mcp/types/product-api.js +44 -0
  32. package/lib/mcp/types/profile.js +37 -0
  33. package/lib/mcp/types/storage-file.js +170 -0
  34. package/lib/openapi.js +18 -0
  35. package/lib/product-apply.js +121 -0
  36. package/lib/raw-api.js +111 -0
  37. package/lib/storage.js +41 -13
  38. package/lib/tokens.js +16 -2
  39. package/package.json +1 -1
  40. package/lib/mcp/tools-buckets.js +0 -130
  41. package/lib/mcp/tools-devices.js +0 -134
  42. package/lib/mcp/tools-misc.js +0 -48
  43. package/lib/mcp/tools-plugins.js +0 -63
  44. package/lib/mcp/tools-products.js +0 -52
  45. package/lib/mcp/tools-storage.js +0 -173
@@ -1,130 +0,0 @@
1
- // @ts-check
2
- import api from '../api.js';
3
- import { TIMEOUTS } from '../constants.js';
4
- import { getBucketData } from '../bucket.js';
5
- import { apiError, inputError } from '../errors.js';
6
- import { v1 } from '../paths.js';
7
-
8
- async function toolBucketDataRead(args) {
9
- if (!args.bucket) throw inputError('bucket is required');
10
- const data = await getBucketData({
11
- bucket: args.bucket,
12
- device: args.device,
13
- user: args.user,
14
- items: args.items || 10,
15
- sort: args.sort || 'desc',
16
- minutes: args.minutes,
17
- min_ts: args.min_ts,
18
- max_ts: args.max_ts,
19
- agg: args.agg,
20
- agg_type: args.agg_type,
21
- tz: args.tz,
22
- fields: Array.isArray(args.fields) ? args.fields.join(',') : args.fields,
23
- group_by: args.group_by,
24
- });
25
- const text =
26
- Array.isArray(data) && data.length === 0 ? 'No data' : JSON.stringify(data, null, 2);
27
- return { content: [{ type: 'text', text }], isError: false };
28
- }
29
-
30
- async function toolBucketDataWrite(args) {
31
- if (!args.bucket) throw inputError('bucket is required');
32
- if (!args.data || typeof args.data !== 'object') {
33
- throw inputError('data must be a JSON object (one sample) or an array of them');
34
- }
35
- const count = Array.isArray(args.data) ? args.data.length : 1;
36
- const params = args.ts !== undefined ? { ts: args.ts } : undefined;
37
- try {
38
- await api.post(
39
- `${v1(args.user)}/buckets/${encodeURIComponent(args.bucket)}/data`,
40
- args.data,
41
- { params, timeout: TIMEOUTS.API_LONG_MS },
42
- );
43
- } catch (e) {
44
- throw apiError(e, { notFound: `Bucket not found: ${args.bucket}` });
45
- }
46
- return {
47
- content: [
48
- {
49
- type: 'text',
50
- text: `Wrote ${count} sample${count === 1 ? '' : 's'} to bucket ${args.bucket}.`,
51
- },
52
- ],
53
- isError: false,
54
- };
55
- }
56
-
57
- const userParam = { type: 'string', description: 'Account to act on (admin only).' };
58
-
59
- export const tools = [
60
- {
61
- name: 'bucket_data_read',
62
- description: `Query the time series of a bucket (find them with {tool:resource_list}: type "bucket", or "device_bucket" for one device).
63
-
64
- - Without \`device\`: the whole bucket. A bucket declared in a product profile holds every device of the product interleaved; \`group_by: "device"\` gives one row per device, when samples carry that tag.
65
- - With \`device\`: the series the bucket keeps for that device.`,
66
- inputSchema: {
67
- type: 'object',
68
- properties: {
69
- bucket: { type: 'string', description: 'Bucket id.' },
70
- device: {
71
- type: 'string',
72
- description:
73
- 'Device id, to read only its records: the bucket needs the `device` tag (product buckets have it).',
74
- },
75
- items: {
76
- type: 'number',
77
- description: 'Samples to return (default 10, at most 1000).',
78
- },
79
- minutes: {
80
- type: 'number',
81
- description: 'Last N minutes (instead of min_ts/max_ts).',
82
- },
83
- min_ts: { type: 'number', description: 'From (unix ms).' },
84
- max_ts: { type: 'number', description: 'To (unix ms, 0 = now).' },
85
- sort: { type: 'string', description: '"desc" (default) or "asc", by time.' },
86
- agg: { type: 'string', description: 'Aggregation window: "5m", "1h"…' },
87
- agg_type: {
88
- type: 'string',
89
- description:
90
- 'Reducer: mean, max, min, sum, count, first, last, mode, median, spread, stddev.',
91
- },
92
- tz: {
93
- type: 'string',
94
- description:
95
- 'IANA time zone for the aggregation windows, e.g. "Europe/Madrid": "1d" then cuts at local midnight (default UTC).',
96
- },
97
- fields: {
98
- type: ['string', 'array'],
99
- items: { type: 'string' },
100
- description:
101
- 'Fields to return: comma-separated or an array. Without fields, an aggregation covers numeric fields only (those of the latest record in the range); name string fields explicitly to read them with last, first, mode or count.',
102
- },
103
- group_by: { type: 'string', description: 'Group key, e.g. "device".' },
104
- user: userParam,
105
- },
106
- required: ['bucket'],
107
- },
108
- handler: toolBucketDataRead,
109
- },
110
- {
111
- name: 'bucket_data_write',
112
- description:
113
- 'Write samples to a bucket whose data source accepts API writes: one object, stored with the current time or `ts`, or an array of objects, each with its own numeric `ts` (unix ms; MongoDB and InfluxDB buckets). Servers limit how often a bucket can be written (429 when exceeded): for long histories, import a CSV ({tool:resource_call} action "import" on the bucket).',
114
- inputSchema: {
115
- type: 'object',
116
- properties: {
117
- bucket: { type: 'string', description: 'Bucket id.' },
118
- data: {
119
- type: ['object', 'array'],
120
- description:
121
- 'Sample fields, e.g. {"temperature": 21.5}, or an array of samples with `ts`.',
122
- },
123
- ts: { type: 'number', description: 'Timestamp of the sample (unix ms).' },
124
- user: userParam,
125
- },
126
- required: ['bucket', 'data'],
127
- },
128
- handler: toolBucketDataWrite,
129
- },
130
- ];
@@ -1,134 +0,0 @@
1
- // @ts-check
2
- import api from '../api.js';
3
- import { apiError, inputError } from '../errors.js';
4
- import { device, v1 } from '../paths.js';
5
- import { callDeviceResource, listDeviceResourcesWithSchemas } from '../resource.js';
6
-
7
- const text = (t) => ({ content: [{ type: 'text', text: t }], isError: false });
8
- const json = (data) =>
9
- text(
10
- data === undefined || data === null || data === ''
11
- ? '(no output)'
12
- : typeof data === 'string'
13
- ? data
14
- : JSON.stringify(data, null, 2),
15
- );
16
-
17
- function requireDevice(args) {
18
- if (!args.device) throw inputError('device is required');
19
- return args.device;
20
- }
21
-
22
- async function toolResourceList(args) {
23
- const entries = await listDeviceResourcesWithSchemas(requireDevice(args), {
24
- user: args.user,
25
- });
26
- const kind = (f) =>
27
- ({ 1: 'no params', 2: 'input', 3: 'output', 4: 'input/output' })[f] || 'unknown';
28
- const lines = entries.map((e) => {
29
- const parts = [`${e.name} [${kind(e.fn)}]`];
30
- if (e.in !== undefined) parts.push(` in: ${JSON.stringify(e.in)}`);
31
- if (e.out !== undefined) parts.push(` out: ${JSON.stringify(e.out)}`);
32
- return parts.join('\n');
33
- });
34
- return text(lines.length ? lines.join('\n\n') : '(no resources)');
35
- }
36
-
37
- async function toolResourceCall(args) {
38
- if (!args.resource) throw inputError('resource is required');
39
- return json(
40
- await callDeviceResource(requireDevice(args), args.resource, args.inputs, {
41
- user: args.user,
42
- }),
43
- );
44
- }
45
-
46
- async function toolDataSend(args) {
47
- const id = requireDevice(args);
48
- if (args.data === undefined) throw inputError('data is required');
49
- try {
50
- const res = await api.post(`${device(args.user, id)}/callback/data`, args.data);
51
- const answer = res.data;
52
- const shown =
53
- answer !== undefined &&
54
- answer !== '' &&
55
- (typeof answer !== 'object' || (answer && Object.keys(answer).length));
56
- return text(`Sent data to ${id}.${shown ? `\nAnswer: ${JSON.stringify(answer)}` : ''}`);
57
- } catch (e) {
58
- throw apiError(e, { notFound: `Device not found: ${id}` });
59
- }
60
- }
61
-
62
- async function toolOverview(args) {
63
- try {
64
- const [stats, locations] = await Promise.all([
65
- api.get(`${v1(args.user)}/info/devices/stats`),
66
- args.locations ? api.get(`${v1(args.user)}/info/devices/locations`) : null,
67
- ]);
68
- return json(locations ? { stats: stats.data, locations: locations.data } : stats.data);
69
- } catch (e) {
70
- throw apiError(e);
71
- }
72
- }
73
-
74
- const deviceParam = { type: 'string', description: 'Device id.' };
75
- const userParam = { type: 'string', description: 'Account to act on (admin only).' };
76
-
77
- export const tools = [
78
- {
79
- name: 'device_resource_list',
80
- description:
81
- "List the resources a connected device exposes, with their input/output shapes: the device's own API, declared by its firmware (a sensor to read, an output to switch), plus the API resources its product adds. Not to be confused with server resources ({family:resource}). The device must be online.",
82
- inputSchema: {
83
- type: 'object',
84
- properties: { device: deviceParam, user: userParam },
85
- required: ['device'],
86
- },
87
- handler: toolResourceList,
88
- },
89
- {
90
- name: 'device_resource_call',
91
- description:
92
- 'Call a resource of a connected device and return its output. With `inputs` it runs the resource; without them it only reads its output. See {tool:device_resource_list} for names and inputs.',
93
- inputSchema: {
94
- type: 'object',
95
- properties: {
96
- device: deviceParam,
97
- resource: { type: 'string', description: 'Resource name.' },
98
- inputs: { description: 'Input value or object the resource declares.' },
99
- user: userParam,
100
- },
101
- required: ['device', 'resource'],
102
- },
103
- handler: toolResourceCall,
104
- },
105
- {
106
- name: 'device_data_send',
107
- description:
108
- 'Send data to the server on behalf of an HTTP device, as the device itself would: the server runs the callback configured for it (write a bucket, set a property, call an endpoint; see type device_callback).',
109
- inputSchema: {
110
- type: 'object',
111
- properties: {
112
- device: deviceParam,
113
- data: { description: 'JSON payload, e.g. {"temperature": 21.5}.' },
114
- user: userParam,
115
- },
116
- required: ['device', 'data'],
117
- },
118
- handler: toolDataSend,
119
- },
120
- {
121
- name: 'device_overview',
122
- description:
123
- 'Usage of all the devices of the account over the last 30 days, per day; with `locations`, also the last known position of each connected device.',
124
- inputSchema: {
125
- type: 'object',
126
- properties: {
127
- locations: { type: 'boolean', description: 'Include device locations.' },
128
- user: userParam,
129
- },
130
- required: [],
131
- },
132
- handler: toolOverview,
133
- },
134
- ];
@@ -1,48 +0,0 @@
1
- // @ts-check
2
- import { getActiveProfile, getProfile, listProfiles } from '../config.js';
3
- import { listDocs, readDoc } from '../docs.js';
4
-
5
- /** The tools about the CLI itself: its documentation and its profiles. */
6
-
7
- const text = (t) => ({ content: [{ type: 'text', text: t }], isError: false });
8
-
9
- async function toolDocRead(args) {
10
- if (args.id) return text((await readDoc(args.id)).text);
11
- const docs = await listDocs();
12
- if (!docs.length) return text('No documentation available.');
13
- return text(
14
- docs
15
- .map((d) => `${d.id}: ${d.title}${d.description ? ` — ${d.description}` : ''}`)
16
- .join('\n'),
17
- );
18
- }
19
-
20
- async function toolProfileList() {
21
- const active = getActiveProfile();
22
- const lines = listProfiles().map((name) => {
23
- const data = getProfile(name) || {};
24
- return `${name === active ? '* ' : ' '}${name} (${data.username || ''}@${data.server || name})`;
25
- });
26
- return text(lines.join('\n') || 'No profiles configured');
27
- }
28
-
29
- export const tools = [
30
- {
31
- name: 'doc_read',
32
- description:
33
- "Read the guides for agents and app developers: the platform's concepts, product code, server administration, the dashboard SDK reference and types (the version the project in this folder uses), and this CLI's own. Without `id`, lists them. Read the relevant guide before building something new.",
34
- inputSchema: {
35
- type: 'object',
36
- properties: { id: { type: 'string', description: 'Document id from the list.' } },
37
- required: [],
38
- },
39
- handler: toolDocRead,
40
- },
41
- {
42
- name: 'profile_list',
43
- description:
44
- 'List the configured CLI profiles: each one is a server with its own credentials. Pass a profile name as `profile` on any other tool to use that server for one call.\n\nProfiles are not accounts: another account on the same server is reached with `user` (admin only), not with a profile.',
45
- inputSchema: { type: 'object', properties: {}, required: [] },
46
- handler: toolProfileList,
47
- },
48
- ];
@@ -1,63 +0,0 @@
1
- // @ts-check
2
- import api from '../api.js';
3
- import { TIMEOUTS } from '../constants.js';
4
- import { apiError, inputError } from '../errors.js';
5
- import { pluginApi } from '../paths.js';
6
-
7
- const METHODS = ['get', 'post', 'put', 'patch', 'delete'];
8
-
9
- /** Call the API a plugin serves (its routes are its own: no catalog). */
10
- async function toolPluginCall(args) {
11
- if (!args.plugin) throw inputError('plugin is required');
12
- const method = (args.method || 'get').toLowerCase();
13
- if (!METHODS.includes(method)) throw inputError(`method must be one of ${METHODS.join(', ')}`);
14
- const path = args.path ? `/${String(args.path).replace(/^\/+/, '')}` : '';
15
- if (path.split('/').includes('..')) throw inputError('path must not contain ".."');
16
- try {
17
- const res = await api.request({
18
- method,
19
- url: `${pluginApi(args.user, args.plugin)}${path}`,
20
- data: method === 'get' || method === 'delete' ? undefined : (args.body ?? {}),
21
- params: args.query,
22
- paramsSerializer: { indexes: null },
23
- transformResponse: [(x) => x],
24
- responseType: 'text',
25
- timeout: TIMEOUTS.API_LONG_MS,
26
- });
27
- const raw = String(res.data ?? '');
28
- let text = raw;
29
- try {
30
- text = JSON.stringify(JSON.parse(raw), null, 2);
31
- } catch {}
32
- return {
33
- content: [{ type: 'text', text: text || `${res.status} (no content)` }],
34
- isError: false,
35
- };
36
- } catch (e) {
37
- throw apiError(e, { notFound: `Not found: plugin ${args.plugin}${path}` });
38
- }
39
- }
40
-
41
- export const tools = [
42
- {
43
- name: 'plugin_call',
44
- description:
45
- 'Call the API an installed plugin serves, at its own `path` (what it offers depends on the plugin; see its documentation or try GET on its root). Find plugins with {tool:resource_list} type "plugin".',
46
- inputSchema: {
47
- type: 'object',
48
- properties: {
49
- plugin: { type: 'string', description: 'Plugin id.' },
50
- path: {
51
- type: 'string',
52
- description: 'Path inside the plugin, e.g. "/api/status".',
53
- },
54
- method: { type: 'string', enum: METHODS, description: 'Default get.' },
55
- body: { description: 'JSON body for post / put / patch.' },
56
- query: { type: 'object', description: 'Query parameters.' },
57
- user: { type: 'string', description: 'Account to act on (admin only).' },
58
- },
59
- required: ['plugin'],
60
- },
61
- handler: toolPluginCall,
62
- },
63
- ];
@@ -1,52 +0,0 @@
1
- // @ts-check
2
- import api from '../api.js';
3
- import { TIMEOUTS } from '../constants.js';
4
- import { apiError, inputError } from '../errors.js';
5
- import { v1 } from '../paths.js';
6
-
7
- const json = (data) => ({
8
- content: [{ type: 'text', text: JSON.stringify(data ?? null, null, 2) }],
9
- isError: false,
10
- });
11
-
12
- function productPath(args) {
13
- if (!args.product) throw inputError('product is required');
14
- return `${v1(args.user)}/products/${encodeURIComponent(args.product)}`;
15
- }
16
-
17
- async function toolResourceCall(args) {
18
- if (!args.resource) throw inputError('resource is required');
19
- try {
20
- const res = await api.post(
21
- `${productPath(args)}/resources/${encodeURIComponent(args.resource)}`,
22
- args.data ?? {},
23
- { timeout: TIMEOUTS.API_LONG_MS },
24
- );
25
- return json(res.data);
26
- } catch (e) {
27
- throw apiError(e, {
28
- notFound: `Not found: product ${args.product} or its resource ${args.resource}`,
29
- });
30
- }
31
- }
32
-
33
- const userParam = { type: 'string', description: 'Account to act on (admin only).' };
34
-
35
- export const tools = [
36
- {
37
- name: 'product_resource_call',
38
- description:
39
- "Call a product API resource for the product as a whole, as an external system feeding data in would. The entry needs a `device_id_resolver` (a function of the product code that returns each payload's device); `data` may be an array, one payload per device. To call it for one device, use {tool:device_resource_call}.",
40
- inputSchema: {
41
- type: 'object',
42
- properties: {
43
- product: { type: 'string', description: 'Product id.' },
44
- resource: { type: 'string', description: 'Resource name in the product profile.' },
45
- data: { description: 'JSON payload.' },
46
- user: userParam,
47
- },
48
- required: ['product', 'resource'],
49
- },
50
- handler: toolResourceCall,
51
- },
52
- ];
@@ -1,173 +0,0 @@
1
- // @ts-check
2
- import fs from 'node:fs';
3
- import nodePath from 'node:path';
4
- import api from '../api.js';
5
- import { collectFiles, uploadFiles } from '../dashboard.js';
6
- import { apiError, inputError } from '../errors.js';
7
- import { v1 } from '../paths.js';
8
- import { deleteStorageFile, readStorageFile, writeStorageFile } from '../storage.js';
9
-
10
- /** Files of a storage (the storages themselves are the catalog type "storage"). */
11
-
12
- const text = (t) => ({ content: [{ type: 'text', text: t }], isError: false });
13
- const userParam = { type: 'string', description: 'Account to act on (admin only).' };
14
- const storageParam = { type: 'string', description: 'Storage id.' };
15
-
16
- function required(args, ...names) {
17
- for (const name of names) {
18
- if (args[name] === undefined || args[name] === '') throw inputError(`${name} is required`);
19
- }
20
- }
21
-
22
- /** A path inside the storage, without leading slashes (they would double up in the URL). */
23
- const clean = (p) => String(p).replace(/^\/+/, '');
24
-
25
- async function toolList(args) {
26
- required(args, 'storage');
27
- const params = args.path ? { path: clean(args.path) } : undefined;
28
- try {
29
- const res = await api.get(
30
- `${v1(args.user)}/storages/${encodeURIComponent(args.storage)}/files`,
31
- { params },
32
- );
33
- const files = Array.isArray(res.data) ? res.data : [];
34
- return text(files.length ? files.map((f) => JSON.stringify(f)).join('\n') : 'No files.');
35
- } catch (e) {
36
- // The server keeps files on disk and creates a storage's folder with
37
- // its first file: an empty storage answers 404 too.
38
- if (e.response?.status === 404) {
39
- return text(
40
- `No files${args.path ? ` under ${clean(args.path)}` : ''} (or no storage ${args.storage}).`,
41
- );
42
- }
43
- throw apiError(e);
44
- }
45
- }
46
-
47
- async function toolRead(args) {
48
- required(args, 'storage', 'path');
49
- return text(await readStorageFile(args.storage, clean(args.path), args.user));
50
- }
51
-
52
- /** Largest single local file uploaded at once. */
53
- const MAX_FILE = 50 * 1024 * 1024;
54
- /** Largest local folder uploaded in one call. */
55
- const MAX_FOLDER = 200 * 1024 * 1024;
56
-
57
- /** Upload a local file (to `path`, default its name) or folder (under `path`). */
58
- async function uploadLocal(args) {
59
- if (args.content !== undefined) throw inputError('pass content or local, not both');
60
- const local = nodePath.resolve(String(args.local));
61
- if (!fs.existsSync(local)) throw inputError(`No such local file or folder: ${args.local}`);
62
- /** @type {Record<string, Buffer>} */
63
- let files;
64
- if (fs.statSync(local).isDirectory()) {
65
- const prefix = args.path ? `${clean(args.path).replace(/\/+$/, '')}/` : '';
66
- files = Object.fromEntries(
67
- Object.entries(
68
- collectFiles(local, {
69
- skip: new Set(['.git']),
70
- maxBytes: MAX_FOLDER,
71
- tooLarge: 'upload its subfolders or files in separate calls',
72
- }),
73
- ).map(([f, c]) => [prefix + f, c]),
74
- );
75
- if (!Object.keys(files).length) throw inputError(`${args.local} has no files`);
76
- } else {
77
- if (fs.statSync(local).size > MAX_FILE) {
78
- throw inputError(`${args.local} is larger than ${MAX_FILE / 1024 / 1024} MB`);
79
- }
80
- files = {
81
- [args.path ? clean(args.path) : nodePath.basename(local)]: fs.readFileSync(local),
82
- };
83
- }
84
- await uploadFiles(args.storage, files, args.user);
85
- const names = Object.keys(files);
86
- const bytes = Object.values(files).reduce((n, c) => n + c.length, 0);
87
- const shown =
88
- names.slice(0, 20).join(', ') + (names.length > 20 ? `… (+${names.length - 20})` : '');
89
- return text(
90
- `Uploaded ${names.length} file(s), ${bytes} bytes, to storage ${args.storage}: ${shown}.`,
91
- );
92
- }
93
-
94
- async function toolWrite(args) {
95
- required(args, 'storage');
96
- if (args.local !== undefined) return uploadLocal(args);
97
- required(args, 'path');
98
- if (typeof args.content !== 'string') throw inputError('content must be a string');
99
- await writeStorageFile(args.storage, clean(args.path), args.content, args.user);
100
- return text(
101
- `Wrote ${clean(args.path)} (${Buffer.byteLength(args.content)} bytes) to storage ${args.storage}.`,
102
- );
103
- }
104
-
105
- async function toolDelete(args) {
106
- required(args, 'storage', 'path');
107
- const deleted = await deleteStorageFile(args.storage, clean(args.path), args.user);
108
- return text(deleted ? `Deleted ${clean(args.path)}.` : `No such file: ${clean(args.path)}.`);
109
- }
110
-
111
- const pathParam = {
112
- type: 'string',
113
- description: 'File path inside the storage, e.g. "web/index.html".',
114
- };
115
-
116
- export const tools = [
117
- {
118
- name: 'storage_file_list',
119
- description:
120
- 'List the files of a storage (path, size, modification time), optionally only under a folder.',
121
- inputSchema: {
122
- type: 'object',
123
- properties: {
124
- storage: storageParam,
125
- path: { type: 'string', description: 'Folder to list.' },
126
- user: userParam,
127
- },
128
- required: ['storage'],
129
- },
130
- handler: toolList,
131
- },
132
- {
133
- name: 'storage_file_read',
134
- description: 'Read a text file of a storage.',
135
- inputSchema: {
136
- type: 'object',
137
- properties: { storage: storageParam, path: pathParam, user: userParam },
138
- required: ['storage', 'path'],
139
- },
140
- handler: toolRead,
141
- },
142
- {
143
- name: 'storage_file_write',
144
- description:
145
- 'Create or overwrite files in a storage; folders on the path are created. Either `content` (text) at `path`, or `local`: a file or folder on this machine, uploaded as is (binary too) to `path` (a file defaults to its own name; a folder goes under `path`, or the root). Use `local` for anything big: CSV to import, images, product code with its node_modules.',
146
- inputSchema: {
147
- type: 'object',
148
- properties: {
149
- storage: storageParam,
150
- path: pathParam,
151
- content: { type: 'string', description: 'Text content of the file at `path`.' },
152
- local: {
153
- type: 'string',
154
- description:
155
- 'Local file or folder to upload instead of `content` (up to 50 MB per file and 200 MB per call).',
156
- },
157
- user: userParam,
158
- },
159
- required: ['storage'],
160
- },
161
- handler: toolWrite,
162
- },
163
- {
164
- name: 'storage_file_delete',
165
- description: 'Delete a file of a storage. Permanent: confirm with the user first.',
166
- inputSchema: {
167
- type: 'object',
168
- properties: { storage: storageParam, path: pathParam, user: userParam },
169
- required: ['storage', 'path'],
170
- },
171
- handler: toolDelete,
172
- },
173
- ];