file-share-mcp 1.0.1 → 1.0.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.
- package/dist/gui.js +66 -32
- package/dist/mcp-server.js +157 -14
- package/dist/utils/file-manager.js +67 -23
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/public/assets/index-CXzfXp4f.css +1 -0
- package/public/assets/index-DaxgjfWE.js +69 -0
- package/public/index.html +2 -2
- package/public/assets/index-CD6a3a8W.js +0 -69
- package/public/assets/index-hXhXe8uC.css +0 -1
package/dist/gui.js
CHANGED
|
@@ -4,49 +4,37 @@ import cors from 'cors';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import fs from 'fs';
|
|
7
|
+
import os from 'os';
|
|
7
8
|
import axios from 'axios';
|
|
8
9
|
import { setupLogging } from './utils/logger.js';
|
|
9
10
|
import { getConfig, saveConfig } from './utils/config-manager.js';
|
|
10
|
-
import { getSharedFiles, validateSafePath, deleteFile } from './utils/file-manager.js';
|
|
11
|
+
import { getSharedFiles, validateSafePath, deleteFile, createDirectory, moveFile, deleteDirectory } from './utils/file-manager.js';
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = path.dirname(__filename);
|
|
13
14
|
setupLogging('GUI');
|
|
14
15
|
const app = express();
|
|
15
16
|
app.use(cors());
|
|
16
17
|
app.use(express.json());
|
|
17
|
-
|
|
18
|
-
// but we want to be sure no other library writes to stdout.
|
|
19
|
-
// Multer configuration for file uploads
|
|
20
|
-
const storage = multer.diskStorage({
|
|
21
|
-
destination: (req, file, cb) => {
|
|
22
|
-
const config = getConfig();
|
|
23
|
-
if (!fs.existsSync(config.shared_folder_path)) {
|
|
24
|
-
fs.mkdirSync(config.shared_folder_path, { recursive: true });
|
|
25
|
-
}
|
|
26
|
-
cb(null, config.shared_folder_path);
|
|
27
|
-
},
|
|
28
|
-
filename: (req, file, cb) => {
|
|
29
|
-
cb(null, file.originalname);
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
const upload = multer({ storage });
|
|
18
|
+
const upload = multer({ dest: os.tmpdir() });
|
|
33
19
|
// Endpoints
|
|
34
20
|
app.get('/proxy/health', (req, res) => {
|
|
35
21
|
res.json({ status: 'healthy' });
|
|
36
22
|
});
|
|
37
23
|
app.get('/api/files', (req, res) => {
|
|
38
24
|
try {
|
|
39
|
-
const
|
|
40
|
-
|
|
25
|
+
const relativePath = req.query.path || '';
|
|
26
|
+
const entries = getSharedFiles(relativePath);
|
|
27
|
+
res.json(entries);
|
|
41
28
|
}
|
|
42
29
|
catch (error) {
|
|
43
30
|
res.status(500).json({ error: error.message });
|
|
44
31
|
}
|
|
45
32
|
});
|
|
46
|
-
app.get('/api/download
|
|
33
|
+
app.get('/api/download', (req, res) => {
|
|
47
34
|
try {
|
|
48
|
-
const
|
|
49
|
-
|
|
35
|
+
const relativePath = req.query.path || '';
|
|
36
|
+
const safePath = validateSafePath(relativePath);
|
|
37
|
+
if (fs.existsSync(safePath) && fs.statSync(safePath).isFile()) {
|
|
50
38
|
res.download(safePath);
|
|
51
39
|
}
|
|
52
40
|
else {
|
|
@@ -57,9 +45,10 @@ app.get('/api/download/:filename', (req, res) => {
|
|
|
57
45
|
res.status(400).send(error.message);
|
|
58
46
|
}
|
|
59
47
|
});
|
|
60
|
-
app.delete('/api/files
|
|
48
|
+
app.delete('/api/files', (req, res) => {
|
|
61
49
|
try {
|
|
62
|
-
|
|
50
|
+
const relativePath = req.query.path || '';
|
|
51
|
+
deleteFile(relativePath);
|
|
63
52
|
res.json({ message: 'File deleted successfully' });
|
|
64
53
|
}
|
|
65
54
|
catch (error) {
|
|
@@ -68,13 +57,14 @@ app.delete('/api/files/:filename', (req, res) => {
|
|
|
68
57
|
});
|
|
69
58
|
const PREVIEWABLE_EXTENSIONS = ['.txt', '.md'];
|
|
70
59
|
const PREVIEW_MAX_SIZE = 1024 * 1024; // 1 MB
|
|
71
|
-
app.get('/api/preview
|
|
60
|
+
app.get('/api/preview', (req, res) => {
|
|
72
61
|
try {
|
|
73
|
-
const
|
|
62
|
+
const relativePath = req.query.path || '';
|
|
63
|
+
const ext = path.extname(relativePath).toLowerCase();
|
|
74
64
|
if (!PREVIEWABLE_EXTENSIONS.includes(ext)) {
|
|
75
65
|
return res.status(400).json({ error: 'Preview not supported for this file type' });
|
|
76
66
|
}
|
|
77
|
-
const safePath = validateSafePath(
|
|
67
|
+
const safePath = validateSafePath(relativePath);
|
|
78
68
|
if (!fs.existsSync(safePath)) {
|
|
79
69
|
return res.status(404).json({ error: 'File not found' });
|
|
80
70
|
}
|
|
@@ -90,11 +80,56 @@ app.get('/api/preview/:filename', (req, res) => {
|
|
|
90
80
|
}
|
|
91
81
|
});
|
|
92
82
|
app.post('/api/upload', upload.single('file'), (req, res) => {
|
|
93
|
-
if (req.file)
|
|
94
|
-
res.json({
|
|
83
|
+
if (!req.file)
|
|
84
|
+
return res.status(400).json({ error: 'No file uploaded' });
|
|
85
|
+
const targetPath = req.body.targetPath || '';
|
|
86
|
+
try {
|
|
87
|
+
const dir = validateSafePath(targetPath);
|
|
88
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
89
|
+
const dest = path.join(dir, path.basename(req.file.originalname));
|
|
90
|
+
fs.renameSync(req.file.path, dest);
|
|
91
|
+
res.json({ message: 'File uploaded successfully', filename: path.basename(req.file.originalname) });
|
|
95
92
|
}
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
catch (err) {
|
|
94
|
+
try {
|
|
95
|
+
fs.unlinkSync(req.file.path);
|
|
96
|
+
}
|
|
97
|
+
catch { }
|
|
98
|
+
res.status(400).json({ error: err.message });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
app.delete('/api/folder', (req, res) => {
|
|
102
|
+
try {
|
|
103
|
+
const relativePath = req.query.path || '';
|
|
104
|
+
deleteDirectory(relativePath);
|
|
105
|
+
res.json({ message: 'Directory deleted successfully' });
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
res.status(400).json({ error: error.message });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
app.post('/api/mkdir', (req, res) => {
|
|
112
|
+
try {
|
|
113
|
+
const { path: dirPath } = req.body;
|
|
114
|
+
if (!dirPath)
|
|
115
|
+
return res.status(400).json({ error: 'path is required' });
|
|
116
|
+
createDirectory(dirPath);
|
|
117
|
+
res.json({ message: 'Directory created successfully' });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
res.status(400).json({ error: error.message });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
app.post('/api/move', (req, res) => {
|
|
124
|
+
try {
|
|
125
|
+
const { from, to } = req.body;
|
|
126
|
+
if (!from || !to)
|
|
127
|
+
return res.status(400).json({ error: 'from and to are required' });
|
|
128
|
+
moveFile(from, to);
|
|
129
|
+
res.json({ message: 'File moved successfully' });
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
res.status(400).json({ error: error.message });
|
|
98
133
|
}
|
|
99
134
|
});
|
|
100
135
|
app.get('/api/config', (req, res) => {
|
|
@@ -155,7 +190,6 @@ async function registerWithProxy() {
|
|
|
155
190
|
if (!config.PROXY_URL || !config.PROXY_APP_PATH)
|
|
156
191
|
return null;
|
|
157
192
|
try {
|
|
158
|
-
// Check if proxy is up
|
|
159
193
|
await axios.get(`${config.PROXY_URL}/proxy/health`, { timeout: 2000 });
|
|
160
194
|
const response = await axios.post(`${config.PROXY_URL}/proxy/register`, {
|
|
161
195
|
path: config.PROXY_APP_PATH,
|
package/dist/mcp-server.js
CHANGED
|
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'url';
|
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import { setupLogging } from './utils/logger.js';
|
|
11
11
|
import { getConfig } from './utils/config-manager.js';
|
|
12
|
-
import { getSharedFiles, validateSafePath } from './utils/file-manager.js';
|
|
12
|
+
import { getSharedFiles, validateSafePath, writeSharedFile, copyToSharedFolder, createDirectory, deleteDirectory, moveFile, } from './utils/file-manager.js';
|
|
13
13
|
import { VERSION } from './version.js';
|
|
14
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -99,10 +99,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
name: 'list_shared_files',
|
|
102
|
-
description: 'List
|
|
102
|
+
description: 'List files and directories in the shared folder. Use the optional path parameter to navigate into a subdirectory.',
|
|
103
103
|
inputSchema: {
|
|
104
104
|
type: 'object',
|
|
105
|
-
properties: {
|
|
105
|
+
properties: {
|
|
106
|
+
path: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'Relative path of the subdirectory to list (e.g. "docs/reports"). Defaults to the root shared folder.',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
106
111
|
},
|
|
107
112
|
},
|
|
108
113
|
{
|
|
@@ -122,15 +127,105 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
122
127
|
{
|
|
123
128
|
name: 'preview_file',
|
|
124
129
|
description: 'Preview the content of a .txt or .md (Markdown) file from the shared folder. Returns the raw text content (max 1 MB).',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
path: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
description: 'Relative path to the file in the shared folder (e.g. "docs/notes.md").',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
required: ['path'],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'write_file',
|
|
143
|
+
description: 'Write text content to a new file in the shared folder, making it immediately available to the user.',
|
|
125
144
|
inputSchema: {
|
|
126
145
|
type: 'object',
|
|
127
146
|
properties: {
|
|
128
147
|
filename: {
|
|
129
148
|
type: 'string',
|
|
130
|
-
description: '
|
|
149
|
+
description: 'Name of the file to create (e.g. "report.md"). Must not contain path separators.',
|
|
150
|
+
},
|
|
151
|
+
content: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
description: 'Text content to write into the file.',
|
|
154
|
+
},
|
|
155
|
+
path: {
|
|
156
|
+
type: 'string',
|
|
157
|
+
description: 'Target subdirectory relative to the shared folder (e.g. "docs/reports"). The directory is created if it does not exist.',
|
|
131
158
|
},
|
|
132
159
|
},
|
|
133
|
-
required: ['filename'],
|
|
160
|
+
required: ['filename', 'content'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'copy_file',
|
|
165
|
+
description: 'Copy an existing file from the agent workspace to the shared folder, making it available to the user.',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
source_path: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: 'Absolute path to the source file in the agent workspace.',
|
|
172
|
+
},
|
|
173
|
+
filename: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'Optional target filename in the shared folder. Defaults to the source file basename.',
|
|
176
|
+
},
|
|
177
|
+
path: {
|
|
178
|
+
type: 'string',
|
|
179
|
+
description: 'Target subdirectory relative to the shared folder (e.g. "exports"). The directory is created if it does not exist.',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
required: ['source_path'],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'create_directory',
|
|
187
|
+
description: 'Create a new subdirectory inside the shared folder.',
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
path: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
description: 'Relative path of the directory to create (e.g. "docs/reports"). Parent directories are created automatically.',
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
required: ['path'],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'delete_directory',
|
|
201
|
+
description: 'Delete an empty subdirectory from the shared folder. Fails if the directory contains files or subdirectories.',
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
path: {
|
|
206
|
+
type: 'string',
|
|
207
|
+
description: 'Relative path of the directory to delete (e.g. "docs/drafts").',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
required: ['path'],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'move_file',
|
|
215
|
+
description: 'Move or rename a file within the shared folder.',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
from: {
|
|
220
|
+
type: 'string',
|
|
221
|
+
description: 'Relative path of the source file (e.g. "report.md" or "docs/draft.md").',
|
|
222
|
+
},
|
|
223
|
+
to: {
|
|
224
|
+
type: 'string',
|
|
225
|
+
description: 'Relative path of the destination (e.g. "archive/report.md").',
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
required: ['from', 'to'],
|
|
134
229
|
},
|
|
135
230
|
},
|
|
136
231
|
],
|
|
@@ -147,15 +242,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
147
242
|
};
|
|
148
243
|
}
|
|
149
244
|
case 'list_shared_files': {
|
|
150
|
-
const
|
|
151
|
-
|
|
245
|
+
const { path: relativePath } = z.object({ path: z.string().optional() }).parse(args);
|
|
246
|
+
const entries = getSharedFiles(relativePath ?? '');
|
|
247
|
+
if (entries.length === 0) {
|
|
248
|
+
const location = relativePath ? `"${relativePath}"` : 'the shared folder';
|
|
152
249
|
return {
|
|
153
|
-
content: [{ type: 'text', text:
|
|
250
|
+
content: [{ type: 'text', text: `${location} is empty.` }],
|
|
154
251
|
};
|
|
155
252
|
}
|
|
156
|
-
const
|
|
253
|
+
const dirs = entries.filter(e => e.type === 'directory').map(e => `📁 ${e.name}/`);
|
|
254
|
+
const files = entries.filter(e => e.type === 'file').map(e => `📄 ${e.name} (${((e.size ?? 0) / 1024).toFixed(2)} KB)`);
|
|
255
|
+
const list = [...dirs, ...files].join('\n');
|
|
256
|
+
const location = relativePath ? `"${relativePath}"` : 'shared folder';
|
|
157
257
|
return {
|
|
158
|
-
content: [{ type: 'text', text: `
|
|
258
|
+
content: [{ type: 'text', text: `Contents of ${location}:\n${list}` }],
|
|
159
259
|
};
|
|
160
260
|
}
|
|
161
261
|
case 'share_file': {
|
|
@@ -165,18 +265,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
165
265
|
};
|
|
166
266
|
}
|
|
167
267
|
case 'preview_file': {
|
|
168
|
-
const {
|
|
169
|
-
const ext = path.extname(
|
|
268
|
+
const { path: relativePath } = z.object({ path: z.string() }).parse(args);
|
|
269
|
+
const ext = path.extname(relativePath).toLowerCase();
|
|
170
270
|
if (!['.txt', '.md'].includes(ext)) {
|
|
171
271
|
return {
|
|
172
272
|
content: [{ type: 'text', text: `Error: Preview is only supported for .txt and .md files.` }],
|
|
173
273
|
isError: true,
|
|
174
274
|
};
|
|
175
275
|
}
|
|
176
|
-
const safePath = validateSafePath(
|
|
276
|
+
const safePath = validateSafePath(relativePath);
|
|
177
277
|
if (!fs.existsSync(safePath)) {
|
|
178
278
|
return {
|
|
179
|
-
content: [{ type: 'text', text: `Error: File "${
|
|
279
|
+
content: [{ type: 'text', text: `Error: File "${relativePath}" not found in shared folder.` }],
|
|
180
280
|
isError: true,
|
|
181
281
|
};
|
|
182
282
|
}
|
|
@@ -192,6 +292,49 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
192
292
|
content: [{ type: 'text', text: content }],
|
|
193
293
|
};
|
|
194
294
|
}
|
|
295
|
+
case 'write_file': {
|
|
296
|
+
const { filename, content, path: relativePath } = z.object({
|
|
297
|
+
filename: z.string(),
|
|
298
|
+
content: z.string(),
|
|
299
|
+
path: z.string().optional(),
|
|
300
|
+
}).parse(args);
|
|
301
|
+
const dest = writeSharedFile(filename, content, relativePath ?? '');
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: 'text', text: `File "${path.basename(dest)}" written to shared folder${relativePath ? ` (${relativePath})` : ''}.` }],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
case 'copy_file': {
|
|
307
|
+
const { source_path, filename, path: relativePath } = z.object({
|
|
308
|
+
source_path: z.string(),
|
|
309
|
+
filename: z.string().optional(),
|
|
310
|
+
path: z.string().optional(),
|
|
311
|
+
}).parse(args);
|
|
312
|
+
const copiedName = copyToSharedFolder(source_path, filename, relativePath ?? '');
|
|
313
|
+
return {
|
|
314
|
+
content: [{ type: 'text', text: `File "${copiedName}" copied to shared folder.` }],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
case 'create_directory': {
|
|
318
|
+
const { path: dirPath } = z.object({ path: z.string() }).parse(args);
|
|
319
|
+
createDirectory(dirPath);
|
|
320
|
+
return {
|
|
321
|
+
content: [{ type: 'text', text: `Directory "${dirPath}" created successfully.` }],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
case 'delete_directory': {
|
|
325
|
+
const { path: dirPath } = z.object({ path: z.string() }).parse(args);
|
|
326
|
+
deleteDirectory(dirPath);
|
|
327
|
+
return {
|
|
328
|
+
content: [{ type: 'text', text: `Directory "${dirPath}" deleted successfully.` }],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
case 'move_file': {
|
|
332
|
+
const { from, to } = z.object({ from: z.string(), to: z.string() }).parse(args);
|
|
333
|
+
moveFile(from, to);
|
|
334
|
+
return {
|
|
335
|
+
content: [{ type: 'text', text: `File moved from "${from}" to "${to}".` }],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
195
338
|
default:
|
|
196
339
|
throw new Error(`Tool not found: ${name}`);
|
|
197
340
|
}
|
|
@@ -1,39 +1,83 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { getConfig } from './config-manager.js';
|
|
4
|
-
export function
|
|
4
|
+
export function validateSafePath(relativePath) {
|
|
5
5
|
const { shared_folder_path } = getConfig();
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const resolvedBase = path.resolve(shared_folder_path);
|
|
7
|
+
const fullPath = path.resolve(shared_folder_path, relativePath);
|
|
8
|
+
if (fullPath !== resolvedBase && !fullPath.startsWith(resolvedBase + path.sep)) {
|
|
9
|
+
throw new Error('Path traversal detected');
|
|
10
|
+
}
|
|
11
|
+
return fullPath;
|
|
12
|
+
}
|
|
13
|
+
export function getSharedFiles(relativePath = '') {
|
|
14
|
+
const dirPath = validateSafePath(relativePath);
|
|
15
|
+
if (!fs.existsSync(dirPath)) {
|
|
16
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
8
17
|
return [];
|
|
9
18
|
}
|
|
10
|
-
return fs.readdirSync(
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
return fs.readdirSync(dirPath).map(name => {
|
|
20
|
+
const fullPath = path.join(dirPath, name);
|
|
21
|
+
const stats = fs.statSync(fullPath);
|
|
22
|
+
if (stats.isDirectory()) {
|
|
23
|
+
return { name, type: 'directory', mtime: stats.mtime };
|
|
24
|
+
}
|
|
16
25
|
return {
|
|
17
|
-
name
|
|
26
|
+
name,
|
|
27
|
+
type: 'file',
|
|
18
28
|
size: stats.size,
|
|
19
29
|
mtime: stats.mtime,
|
|
20
|
-
extension: path.extname(
|
|
30
|
+
extension: path.extname(name).toLowerCase(),
|
|
21
31
|
};
|
|
32
|
+
}).sort((a, b) => {
|
|
33
|
+
if (a.type !== b.type)
|
|
34
|
+
return a.type === 'directory' ? -1 : 1;
|
|
35
|
+
return a.name.localeCompare(b.name);
|
|
22
36
|
});
|
|
23
37
|
}
|
|
24
|
-
export function
|
|
25
|
-
const
|
|
26
|
-
const safeName = path.basename(filename);
|
|
27
|
-
const fullPath = path.join(shared_folder_path, safeName);
|
|
28
|
-
// Extra safety check
|
|
29
|
-
if (!fullPath.startsWith(path.resolve(shared_folder_path))) {
|
|
30
|
-
throw new Error('Path traversal detected');
|
|
31
|
-
}
|
|
32
|
-
return fullPath;
|
|
33
|
-
}
|
|
34
|
-
export function deleteFile(filename) {
|
|
35
|
-
const fullPath = validateSafePath(filename);
|
|
38
|
+
export function deleteFile(relativePath) {
|
|
39
|
+
const fullPath = validateSafePath(relativePath);
|
|
36
40
|
if (fs.existsSync(fullPath)) {
|
|
37
41
|
fs.unlinkSync(fullPath);
|
|
38
42
|
}
|
|
39
43
|
}
|
|
44
|
+
export function writeSharedFile(filename, content, relativePath = '') {
|
|
45
|
+
const dir = validateSafePath(relativePath);
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
const dest = path.join(dir, path.basename(filename));
|
|
48
|
+
fs.writeFileSync(dest, content, 'utf-8');
|
|
49
|
+
return dest;
|
|
50
|
+
}
|
|
51
|
+
export function copyToSharedFolder(sourcePath, filename, relativePath = '') {
|
|
52
|
+
const targetFilename = filename ? path.basename(filename) : path.basename(sourcePath);
|
|
53
|
+
const stats = fs.statSync(sourcePath);
|
|
54
|
+
if (!stats.isFile()) {
|
|
55
|
+
throw new Error(`Source path is not a file: ${sourcePath}`);
|
|
56
|
+
}
|
|
57
|
+
const dir = validateSafePath(relativePath);
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
fs.copyFileSync(sourcePath, path.join(dir, targetFilename));
|
|
60
|
+
return relativePath ? `${relativePath}/${targetFilename}` : targetFilename;
|
|
61
|
+
}
|
|
62
|
+
export function createDirectory(relativePath) {
|
|
63
|
+
const fullPath = validateSafePath(relativePath);
|
|
64
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
export function deleteDirectory(relativePath) {
|
|
67
|
+
const fullPath = validateSafePath(relativePath);
|
|
68
|
+
if (!fs.existsSync(fullPath))
|
|
69
|
+
throw new Error('Directory not found');
|
|
70
|
+
const contents = fs.readdirSync(fullPath);
|
|
71
|
+
if (contents.length > 0)
|
|
72
|
+
throw new Error('Directory is not empty');
|
|
73
|
+
fs.rmdirSync(fullPath);
|
|
74
|
+
}
|
|
75
|
+
export function moveFile(fromRelative, toRelative) {
|
|
76
|
+
const fromPath = validateSafePath(fromRelative);
|
|
77
|
+
const toPath = validateSafePath(toRelative);
|
|
78
|
+
if (!fs.existsSync(fromPath)) {
|
|
79
|
+
throw new Error(`File not found: ${fromRelative}`);
|
|
80
|
+
}
|
|
81
|
+
fs.mkdirSync(path.dirname(toPath), { recursive: true });
|
|
82
|
+
fs.renameSync(fromPath, toPath);
|
|
83
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.0.
|
|
1
|
+
export const VERSION = '1.0.3';
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-200:oklch(88.5% .062 18.334);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-900:oklch(39.6% .141 25.723);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-900:oklch(37.9% .146 265.522);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-4xl:56rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--leading-relaxed:1.625;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:currentColor}::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.fixed{position:fixed}.inset-0{inset:calc(var(--spacing)*0)}.z-40{z-index:40}.z-50{z-index:50}.mx-auto{margin-inline:auto}.-mt-0\.5{margin-top:calc(var(--spacing)*-.5)}.-mt-4{margin-top:calc(var(--spacing)*-4)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-8{margin-top:calc(var(--spacing)*8)}.mr-1{margin-right:calc(var(--spacing)*1)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.ml-2{margin-left:calc(var(--spacing)*2)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.h-3{height:calc(var(--spacing)*3)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-full{height:100%}.max-h-52{max-height:calc(var(--spacing)*52)}.max-h-\[85vh\]{max-height:85vh}.max-h-\[500px\]{max-height:500px}.min-h-\[200px\]{min-height:200px}.min-h-screen{min-height:100vh}.w-3{width:calc(var(--spacing)*3)}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-full{width:100%}.max-w-4xl{max-width:var(--container-4xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.scale-\[1\.02\]{scale:1.02}.rotate-90{rotate:90deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-700>:not(:last-child)){border-color:var(--color-gray-700)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-blue-400{border-color:var(--color-blue-400)}.border-blue-500{border-color:var(--color-blue-500)}.border-blue-500\/50{border-color:#3080ff80}@supports (color:color-mix(in lab,red,red)){.border-blue-500\/50{border-color:color-mix(in oklab,var(--color-blue-500)50%,transparent)}}.border-gray-600{border-color:var(--color-gray-600)}.border-gray-700{border-color:var(--color-gray-700)}.border-gray-700\/60{border-color:#36415399}@supports (color:color-mix(in lab,red,red)){.border-gray-700\/60{border-color:color-mix(in oklab,var(--color-gray-700)60%,transparent)}}.border-red-500{border-color:var(--color-red-500)}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black)60%,transparent)}}.bg-black\/70{background-color:#000000b3}@supports (color:color-mix(in lab,red,red)){.bg-black\/70{background-color:color-mix(in oklab,var(--color-black)70%,transparent)}}.bg-blue-500\/10{background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/10{background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.bg-blue-500\/15{background-color:#3080ff26}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/15{background-color:color-mix(in oklab,var(--color-blue-500)15%,transparent)}}.bg-blue-600{background-color:var(--color-blue-600)}.bg-blue-900\/40{background-color:#1c398e66}@supports (color:color-mix(in lab,red,red)){.bg-blue-900\/40{background-color:color-mix(in oklab,var(--color-blue-900)40%,transparent)}}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-800\/50{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.bg-gray-800\/50{background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}.bg-gray-900{background-color:var(--color-gray-900)}.bg-red-500\/15{background-color:#fb2c3626}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/15{background-color:color-mix(in oklab,var(--color-red-500)15%,transparent)}}.bg-red-600{background-color:var(--color-red-600)}.bg-red-900\/30{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.bg-red-900\/30{background-color:color-mix(in oklab,var(--color-red-900)30%,transparent)}}.bg-yellow-500\/15{background-color:#edb20026}@supports (color:color-mix(in lab,red,red)){.bg-yellow-500\/15{background-color:color-mix(in oklab,var(--color-yellow-500)15%,transparent)}}.bg-yellow-600{background-color:var(--color-yellow-600)}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-blue-400{--tw-gradient-from:var(--color-blue-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-500{--tw-gradient-to:var(--color-purple-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.p-12{padding:calc(var(--spacing)*12)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-4{padding-block:calc(var(--spacing)*4)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-5{padding-left:calc(var(--spacing)*5)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.whitespace-pre-wrap{white-space:pre-wrap}.text-blue-400{color:var(--color-blue-400)}.text-gray-100{color:var(--color-gray-100)}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-purple-400{color:var(--color-purple-400)}.text-red-200{color:var(--color-red-200)}.text-red-400{color:var(--color-red-400)}.text-transparent{color:#0000}.text-white{color:var(--color-white)}.text-yellow-400{color:var(--color-yellow-400)}.text-yellow-500\/80{color:#edb200cc}@supports (color:color-mix(in lab,red,red)){.text-yellow-500\/80{color:color-mix(in oklab,var(--color-yellow-500)80%,transparent)}}.uppercase{text-transform:uppercase}.italic{font-style:italic}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}@media(hover:hover){.group-hover\:scale-110:is(:where(.group):hover *){--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.group-hover\:text-blue-400:is(:where(.group):hover *){color:var(--color-blue-400)}.hover\:border-blue-500:hover{border-color:var(--color-blue-500)}.hover\:bg-blue-500\/5:hover{background-color:#3080ff0d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-blue-500\/5:hover{background-color:color-mix(in oklab,var(--color-blue-500)5%,transparent)}}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-gray-700\/40:hover{background-color:#36415366}@supports (color:color-mix(in lab,red,red)){.hover\:bg-gray-700\/40:hover{background-color:color-mix(in oklab,var(--color-gray-700)40%,transparent)}}.hover\:bg-purple-600:hover{background-color:var(--color-purple-600)}.hover\:bg-red-600:hover{background-color:var(--color-red-600)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:bg-yellow-600:hover{background-color:var(--color-yellow-600)}.hover\:bg-yellow-700:hover{background-color:var(--color-yellow-700)}.hover\:text-blue-300:hover{color:var(--color-blue-300)}.hover\:text-blue-400:hover{color:var(--color-blue-400)}.hover\:text-gray-100:hover{color:var(--color-gray-100)}.hover\:text-white:hover{color:var(--color-white)}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:48rem){.md\:col-span-1{grid-column:span 1/span 1}.md\:col-span-2{grid-column:span 2/span 2}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}:root{color-scheme:light dark;color:#ffffffde;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:#242424;font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;font-weight:400;line-height:1.5}body{min-width:320px;min-height:100vh;margin:0;display:flex}#root{width:100%}.markdown-preview{color:#e2e8f0;font-size:.9rem;line-height:1.75}.markdown-preview h1,.markdown-preview h2,.markdown-preview h3,.markdown-preview h4,.markdown-preview h5,.markdown-preview h6{color:#f1f5f9;margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.3}.markdown-preview h1{border-bottom:1px solid #334155;padding-bottom:.3em;font-size:1.75rem}.markdown-preview h2{border-bottom:1px solid #1e293b;padding-bottom:.2em;font-size:1.35rem}.markdown-preview h3{font-size:1.1rem}.markdown-preview p{margin-top:.75em;margin-bottom:.75em}.markdown-preview a{color:#60a5fa;text-decoration:underline}.markdown-preview a:hover{color:#93c5fd}.markdown-preview strong{color:#f8fafc;font-weight:700}.markdown-preview em{font-style:italic}.markdown-preview code{color:#a5f3fc;background:#1e293b;border-radius:4px;padding:.15em .4em;font-family:JetBrains Mono,Fira Code,monospace;font-size:.85em}.markdown-preview pre{background:#0f172a;border:1px solid #334155;border-radius:8px;margin:1em 0;padding:1em 1.25em;overflow-x:auto}.markdown-preview pre code{color:#e2e8f0;background:0 0;padding:0;font-size:.85em}.markdown-preview blockquote{color:#94a3b8;border-left:3px solid #6366f1;margin-left:0;padding-left:1em;font-style:italic}.markdown-preview ul,.markdown-preview ol{margin:.75em 0;padding-left:1.5em}.markdown-preview li{margin:.3em 0}.markdown-preview table{border-collapse:collapse;width:100%;margin:1em 0;font-size:.875em}.markdown-preview th,.markdown-preview td{text-align:left;border:1px solid #334155;padding:.5em .75em}.markdown-preview th{background:#1e293b;font-weight:600}.markdown-preview tr:nth-child(2n){background:#0f172a}.markdown-preview hr{border:none;border-top:1px solid #334155;margin:1.5em 0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-duration{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes spin{to{transform:rotate(360deg)}}
|