@jk3labs/paperclip-plugin-file-browser 0.2.3 → 0.2.4
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/IMPLEMENTATION_NOTES.md +0 -0
- package/README.md +80 -19
- package/dist/__tests__/file-browser.spec.js +140 -0
- package/dist/index.js +110 -0
- package/index.js +82 -0
- package/jest.config.js +6 -0
- package/manifest.ts +15 -0
- package/package.json +34 -48
- package/src/__tests__/file-browser.spec.ts +161 -0
- package/src/index.ts +128 -0
- package/tsconfig.json +15 -0
- package/dist/manifest.js +0 -89
- package/dist/manifest.js.map +0 -7
- package/dist/ui/index.js +0 -187
- package/dist/ui/index.js.map +0 -7
- package/dist/worker.js +0 -9268
- package/dist/worker.js.map +0 -7
|
Binary file
|
package/README.md
CHANGED
|
@@ -1,30 +1,91 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Paperclip File Browser Plugin
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A Paperclip plugin for browsing, downloading, and zipping files in the workspace.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
```
|
|
7
|
+
1. Clone this repository:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/jk3labs/paperclip-plugin-file-browser.git
|
|
11
|
+
cd paperclip-plugin-file-browser
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
2. Install dependencies:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
3. Compile the plugin:
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
folder on your machine.
|
|
22
|
+
```bash
|
|
23
|
+
npx tsc
|
|
24
|
+
```
|
|
18
25
|
|
|
26
|
+
4. Install the plugin in Paperclip:
|
|
19
27
|
|
|
28
|
+
```bash
|
|
29
|
+
paperclipai plugin install ./paperclip-plugin-file-browser
|
|
30
|
+
```
|
|
20
31
|
|
|
21
|
-
##
|
|
32
|
+
## API Endpoints
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
### 1. List Directory Contents
|
|
35
|
+
|
|
36
|
+
**Endpoint:** `GET /files/list`
|
|
37
|
+
|
|
38
|
+
**Query Parameters:**
|
|
39
|
+
- `path` (optional): Relative path to the directory (defaults to root).
|
|
40
|
+
|
|
41
|
+
**Response:**
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"files": ["file1.txt", "file2.txt"],
|
|
45
|
+
"directories": ["dir1", "dir2"],
|
|
46
|
+
"currentPath": "relative/path",
|
|
47
|
+
"rootDir": "/absolute/root/path"
|
|
48
|
+
}
|
|
25
49
|
```
|
|
26
50
|
|
|
27
|
-
|
|
51
|
+
**Errors:**
|
|
52
|
+
- `403 Forbidden`: Access denied (path outside root).
|
|
53
|
+
- `404 Not Found`: Directory does not exist.
|
|
54
|
+
- `500 Internal Server Error`: Server error.
|
|
55
|
+
|
|
56
|
+
### 2. Download File
|
|
57
|
+
|
|
58
|
+
**Endpoint:** `GET /files/download`
|
|
59
|
+
|
|
60
|
+
**Query Parameters:**
|
|
61
|
+
- `path` (required): Relative path to the file.
|
|
62
|
+
|
|
63
|
+
**Response:**
|
|
64
|
+
- Streams the file for download.
|
|
65
|
+
|
|
66
|
+
**Errors:**
|
|
67
|
+
- `400 Bad Request`: Path parameter missing.
|
|
68
|
+
- `403 Forbidden`: Access denied (path outside root).
|
|
69
|
+
- `404 Not Found`: File does not exist.
|
|
70
|
+
- `500 Internal Server Error`: Server error.
|
|
71
|
+
|
|
72
|
+
### 3. Download Directory as ZIP
|
|
73
|
+
|
|
74
|
+
**Endpoint:** `GET /files/zip`
|
|
75
|
+
|
|
76
|
+
**Query Parameters:**
|
|
77
|
+
- `path` (required): Relative path to the directory.
|
|
78
|
+
|
|
79
|
+
**Response:**
|
|
80
|
+
- Streams the directory as a ZIP file.
|
|
81
|
+
|
|
82
|
+
**Errors:**
|
|
83
|
+
- `400 Bad Request`: Path parameter missing.
|
|
84
|
+
- `403 Forbidden`: Access denied (path outside root).
|
|
85
|
+
- `404 Not Found`: Directory does not exist.
|
|
86
|
+
- `500 Internal Server Error`: Server error.
|
|
87
|
+
|
|
88
|
+
## Development
|
|
28
89
|
|
|
29
|
-
- `
|
|
30
|
-
-
|
|
90
|
+
- Run `npx tsc --watch` to enable TypeScript watch mode.
|
|
91
|
+
- Reinstall the plugin after making changes.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const supertest_1 = __importDefault(require("supertest"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
jest.mock('archiver', () => {
|
|
11
|
+
return jest.fn().mockImplementation(() => {
|
|
12
|
+
const { PassThrough } = require('stream');
|
|
13
|
+
const stream = new PassThrough();
|
|
14
|
+
setTimeout(() => { stream.end(); }, 50);
|
|
15
|
+
return {
|
|
16
|
+
pipe: (dest) => { stream.pipe(dest); return dest; },
|
|
17
|
+
directory: () => { },
|
|
18
|
+
finalize: jest.fn().mockResolvedValue(undefined),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
const index_1 = __importDefault(require("../index"));
|
|
23
|
+
const mockRootDir = path_1.default.join(__dirname, 'mock-root');
|
|
24
|
+
function createApp() {
|
|
25
|
+
const app = (0, express_1.default)();
|
|
26
|
+
const routers = [];
|
|
27
|
+
const context = {
|
|
28
|
+
addRouter: (basePath, router) => {
|
|
29
|
+
routers.push({ basePath, router });
|
|
30
|
+
},
|
|
31
|
+
environment: {
|
|
32
|
+
rootDir: mockRootDir,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
index_1.default.register(context);
|
|
36
|
+
for (const { basePath, router } of routers) {
|
|
37
|
+
app.use(basePath, router);
|
|
38
|
+
}
|
|
39
|
+
return app;
|
|
40
|
+
}
|
|
41
|
+
describe('File Browser Plugin', () => {
|
|
42
|
+
let app;
|
|
43
|
+
const mockFilePath = path_1.default.join(mockRootDir, 'test.txt');
|
|
44
|
+
const mockDirPath = path_1.default.join(mockRootDir, 'test-dir');
|
|
45
|
+
const mockNestedFilePath = path_1.default.join(mockDirPath, 'nested.txt');
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
await fs_extra_1.default.ensureDir(mockRootDir);
|
|
48
|
+
await fs_extra_1.default.writeFile(mockFilePath, 'test content');
|
|
49
|
+
await fs_extra_1.default.ensureDir(mockDirPath);
|
|
50
|
+
await fs_extra_1.default.writeFile(mockNestedFilePath, 'nested content');
|
|
51
|
+
app = createApp();
|
|
52
|
+
});
|
|
53
|
+
afterAll(async () => {
|
|
54
|
+
await fs_extra_1.default.remove(mockRootDir);
|
|
55
|
+
});
|
|
56
|
+
describe('Plugin registration', () => {
|
|
57
|
+
it('should throw if rootDir is invalid', () => {
|
|
58
|
+
const badContext = {
|
|
59
|
+
addRouter: () => { },
|
|
60
|
+
environment: { rootDir: '/nonexistent/path/that/does/not/exist' },
|
|
61
|
+
};
|
|
62
|
+
expect(() => index_1.default.register(badContext)).toThrow('Invalid root directory');
|
|
63
|
+
});
|
|
64
|
+
it('should register a router on /files', () => {
|
|
65
|
+
const registeredPaths = [];
|
|
66
|
+
const ctx = {
|
|
67
|
+
addRouter: (basePath) => { registeredPaths.push(basePath); },
|
|
68
|
+
environment: { rootDir: mockRootDir },
|
|
69
|
+
};
|
|
70
|
+
index_1.default.register(ctx);
|
|
71
|
+
expect(registeredPaths).toContain('/files');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('GET /files/list', () => {
|
|
75
|
+
it('should list files and directories in root', async () => {
|
|
76
|
+
const response = await (0, supertest_1.default)(app).get('/files/list');
|
|
77
|
+
expect(response.status).toBe(200);
|
|
78
|
+
expect(response.body.files).toContain('test.txt');
|
|
79
|
+
expect(response.body.directories).toContain('test-dir');
|
|
80
|
+
expect(response.body.currentPath).toBe('');
|
|
81
|
+
});
|
|
82
|
+
it('should list files and directories in subdirectory', async () => {
|
|
83
|
+
const response = await (0, supertest_1.default)(app).get('/files/list?path=test-dir');
|
|
84
|
+
expect(response.status).toBe(200);
|
|
85
|
+
expect(response.body.files).toContain('nested.txt');
|
|
86
|
+
expect(response.body.directories).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
it('should return 404 for non-existent directory', async () => {
|
|
89
|
+
const response = await (0, supertest_1.default)(app).get('/files/list?path=non-existent');
|
|
90
|
+
expect(response.status).toBe(404);
|
|
91
|
+
});
|
|
92
|
+
it('should return 403 for path traversal attempt', async () => {
|
|
93
|
+
const response = await (0, supertest_1.default)(app).get('/files/list?path=../../../etc');
|
|
94
|
+
expect(response.status).toBe(403);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('GET /files/download', () => {
|
|
98
|
+
it('should download a file', async () => {
|
|
99
|
+
const response = await (0, supertest_1.default)(app).get('/files/download?path=test.txt');
|
|
100
|
+
expect(response.status).toBe(200);
|
|
101
|
+
expect(response.text).toBe('test content');
|
|
102
|
+
});
|
|
103
|
+
it('should return 400 when path is missing', async () => {
|
|
104
|
+
const response = await (0, supertest_1.default)(app).get('/files/download');
|
|
105
|
+
expect(response.status).toBe(400);
|
|
106
|
+
expect(response.body.error).toBe('Path parameter is required');
|
|
107
|
+
});
|
|
108
|
+
it('should return 404 for non-existent file', async () => {
|
|
109
|
+
const response = await (0, supertest_1.default)(app).get('/files/download?path=non-existent.txt');
|
|
110
|
+
expect(response.status).toBe(404);
|
|
111
|
+
});
|
|
112
|
+
it('should return 403 for path traversal attempt', async () => {
|
|
113
|
+
const response = await (0, supertest_1.default)(app).get('/files/download?path=../../../etc/passwd');
|
|
114
|
+
expect(response.status).toBe(403);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe('GET /files/zip', () => {
|
|
118
|
+
it('should return a zip response for a valid directory', async () => {
|
|
119
|
+
const response = await (0, supertest_1.default)(app).get('/files/zip?path=test-dir');
|
|
120
|
+
expect(response.status).toBe(200);
|
|
121
|
+
});
|
|
122
|
+
it('should return 400 when path is missing', async () => {
|
|
123
|
+
const response = await (0, supertest_1.default)(app).get('/files/zip');
|
|
124
|
+
expect(response.status).toBe(400);
|
|
125
|
+
expect(response.body.error).toBe('Path parameter is required');
|
|
126
|
+
});
|
|
127
|
+
it('should return 404 for non-existent directory', async () => {
|
|
128
|
+
const response = await (0, supertest_1.default)(app).get('/files/zip?path=non-existent');
|
|
129
|
+
expect(response.status).toBe(404);
|
|
130
|
+
});
|
|
131
|
+
it('should return 404 when path points to a file', async () => {
|
|
132
|
+
const response = await (0, supertest_1.default)(app).get('/files/zip?path=test.txt');
|
|
133
|
+
expect(response.status).toBe(404);
|
|
134
|
+
});
|
|
135
|
+
it('should return 403 for path traversal attempt', async () => {
|
|
136
|
+
const response = await (0, supertest_1.default)(app).get('/files/zip?path=../../../etc');
|
|
137
|
+
expect(response.status).toBe(403);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const archiver_1 = __importDefault(require("archiver"));
|
|
10
|
+
;
|
|
11
|
+
const plugin = {
|
|
12
|
+
id: 'file-browser',
|
|
13
|
+
name: 'File Browser Plugin',
|
|
14
|
+
description: 'A plugin to browse, download, and zip files in the Paperclip workspace.',
|
|
15
|
+
register: (context) => {
|
|
16
|
+
const router = express_1.default.Router();
|
|
17
|
+
const rootDir = context.environment.rootDir;
|
|
18
|
+
// Validate rootDir exists
|
|
19
|
+
if (!rootDir || !(fs_extra_1.default.existsSync(rootDir))) {
|
|
20
|
+
throw new Error(`Invalid root directory: ${rootDir}`);
|
|
21
|
+
}
|
|
22
|
+
router.get('/list', async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const relativePath = req.query.path || '';
|
|
25
|
+
const targetPath = path_1.default.join(rootDir, relativePath);
|
|
26
|
+
// Security: Ensure targetPath is within rootDir
|
|
27
|
+
if (!targetPath.startsWith(rootDir)) {
|
|
28
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
29
|
+
}
|
|
30
|
+
const entries = await fs_extra_1.default.readdir(targetPath, { withFileTypes: true });
|
|
31
|
+
const files = entries
|
|
32
|
+
.filter(dirent => dirent.isFile())
|
|
33
|
+
.map(dirent => dirent.name);
|
|
34
|
+
const directories = entries
|
|
35
|
+
.filter(dirent => dirent.isDirectory())
|
|
36
|
+
.map(dirent => dirent.name);
|
|
37
|
+
res.json({
|
|
38
|
+
files,
|
|
39
|
+
directories,
|
|
40
|
+
currentPath: path_1.default.relative(rootDir, targetPath),
|
|
41
|
+
rootDir
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
const statusCode = error.code === 'ENOENT' ? 404 : 500;
|
|
47
|
+
res.status(statusCode).json({ error: message });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
router.get('/download', async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const relativePath = req.query.path;
|
|
53
|
+
if (!relativePath) {
|
|
54
|
+
return res.status(400).json({ error: 'Path parameter is required' });
|
|
55
|
+
}
|
|
56
|
+
const targetPath = path_1.default.join(rootDir, relativePath);
|
|
57
|
+
// Security: Ensure targetPath is within rootDir
|
|
58
|
+
if (!targetPath.startsWith(rootDir)) {
|
|
59
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
60
|
+
}
|
|
61
|
+
// Check if path exists and is a file
|
|
62
|
+
const stats = await fs_extra_1.default.stat(targetPath);
|
|
63
|
+
if (!stats.isFile()) {
|
|
64
|
+
return res.status(404).json({ error: 'File not found' });
|
|
65
|
+
}
|
|
66
|
+
// Stream file for download
|
|
67
|
+
res.download(targetPath, path_1.default.basename(targetPath));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
+
const statusCode = error.code === 'ENOENT' ? 404 : 500;
|
|
72
|
+
res.status(statusCode).json({ error: message });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
router.get('/zip', async (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const relativePath = req.query.path;
|
|
78
|
+
if (!relativePath) {
|
|
79
|
+
return res.status(400).json({ error: 'Path parameter is required' });
|
|
80
|
+
}
|
|
81
|
+
const targetPath = path_1.default.join(rootDir, relativePath);
|
|
82
|
+
// Security: Ensure targetPath is within rootDir
|
|
83
|
+
if (!targetPath.startsWith(rootDir)) {
|
|
84
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
85
|
+
}
|
|
86
|
+
// Check if path exists and is a directory
|
|
87
|
+
const stats = await fs_extra_1.default.stat(targetPath);
|
|
88
|
+
if (!stats.isDirectory()) {
|
|
89
|
+
return res.status(404).json({ error: 'Directory not found' });
|
|
90
|
+
}
|
|
91
|
+
// Set headers for ZIP download
|
|
92
|
+
res.attachment(`${path_1.default.basename(targetPath)}.zip`);
|
|
93
|
+
// Create ZIP stream
|
|
94
|
+
const archive = (0, archiver_1.default)('zip', {
|
|
95
|
+
zlib: { level: 9 } // Maximum compression
|
|
96
|
+
});
|
|
97
|
+
archive.pipe(res);
|
|
98
|
+
archive.directory(targetPath, false);
|
|
99
|
+
await archive.finalize();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
103
|
+
const statusCode = error.code === 'ENOENT' ? 404 : 500;
|
|
104
|
+
res.status(statusCode).json({ error: message });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
context.addRouter('/files', router);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
exports.default = plugin;
|
package/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const express = require("express");
|
|
4
|
+
const fs = require("fs-extra");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
module.exports = (pluginContext) => {
|
|
8
|
+
const router = express.Router();
|
|
9
|
+
|
|
10
|
+
// Dynamic root lookup: Use Paperclip's workspace root
|
|
11
|
+
const rootDir = pluginContext.workspaceRoot || process.env.PAPERCLIP_WORKSPACE_ROOT || process.cwd();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* GET /files/list
|
|
15
|
+
* List files and directories at the given path
|
|
16
|
+
*/
|
|
17
|
+
router.get("/list", async (req, res) => {
|
|
18
|
+
const { dir } = req.query;
|
|
19
|
+
if (!dir) {
|
|
20
|
+
return res.status(400).json({ error: "Query parameter 'dir' is required" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const targetPath = path.join(rootDir, dir);
|
|
24
|
+
try {
|
|
25
|
+
const stats = await fs.stat(targetPath);
|
|
26
|
+
if (!stats.isDirectory()) {
|
|
27
|
+
return res.status(400).json({ error: "Path is not a directory" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const items = await fs.readdir(targetPath);
|
|
31
|
+
const detailedItems = await Promise.all(
|
|
32
|
+
items.map(async (item) => {
|
|
33
|
+
const itemPath = path.join(targetPath, item);
|
|
34
|
+
const itemStats = await fs.stat(itemPath);
|
|
35
|
+
return {
|
|
36
|
+
name: item,
|
|
37
|
+
path: path.relative(rootDir, itemPath),
|
|
38
|
+
isDirectory: itemStats.isDirectory(),
|
|
39
|
+
size: itemStats.size,
|
|
40
|
+
modified: itemStats.mtime
|
|
41
|
+
};
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
res.json({
|
|
46
|
+
path: dir,
|
|
47
|
+
items: detailedItems
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
res.status(500).json({ error: error.message });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* GET /files/read
|
|
56
|
+
* Read file content
|
|
57
|
+
*/
|
|
58
|
+
router.get("/read", async (req, res) => {
|
|
59
|
+
const { file } = req.query;
|
|
60
|
+
if (!file) {
|
|
61
|
+
return res.status(400).json({ error: "Query parameter 'file' is required" });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const filePath = path.join(rootDir, file);
|
|
65
|
+
try {
|
|
66
|
+
const stats = await fs.stat(filePath);
|
|
67
|
+
if (!stats.isFile()) {
|
|
68
|
+
return res.status(400).json({ error: "Path is not a file" });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
72
|
+
res.json({
|
|
73
|
+
path: file,
|
|
74
|
+
content
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
res.status(500).json({ error: error.message });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return router;
|
|
82
|
+
};
|
package/jest.config.js
ADDED
package/manifest.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PaperclipPluginManifestV1 } from '@paperclipai/shared';
|
|
2
|
+
|
|
3
|
+
export const manifest: PaperclipPluginManifestV1 = {
|
|
4
|
+
id: 'jkl.file-browser',
|
|
5
|
+
apiVersion: 1,
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
displayName: 'File Browser',
|
|
8
|
+
description: 'A plugin to browse files in the Paperclip workspace.',
|
|
9
|
+
author: 'JKL <support@jkl.co>',
|
|
10
|
+
categories: ['utilities'],
|
|
11
|
+
capabilities: [],
|
|
12
|
+
entrypoints: {
|
|
13
|
+
worker: 'dist/index.js',
|
|
14
|
+
},
|
|
15
|
+
};
|
package/package.json
CHANGED
|
@@ -1,48 +1,34 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@jk3labs/paperclip-plugin-file-browser",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
},
|
|
36
|
-
"devDependencies": {
|
|
37
|
-
"@paperclipai/plugin-sdk": "2026.525.0",
|
|
38
|
-
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
39
|
-
"@rollup/plugin-typescript": "^12.1.2",
|
|
40
|
-
"@types/node": "^24.6.0",
|
|
41
|
-
"@types/react": "^19.0.8",
|
|
42
|
-
"esbuild": "^0.27.3",
|
|
43
|
-
"rollup": "^4.38.0",
|
|
44
|
-
"tslib": "^2.8.1",
|
|
45
|
-
"typescript": "^5.7.3",
|
|
46
|
-
"vitest": "^3.0.5"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@jk3labs/paperclip-plugin-file-browser",
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"description": "A Paperclip plugin for browsing files in the workspace",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest",
|
|
8
|
+
"test:watch": "jest --watch"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"paperclip",
|
|
12
|
+
"plugin",
|
|
13
|
+
"file-browser"
|
|
14
|
+
],
|
|
15
|
+
"author": "JKL",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@types/archiver": "^7.0.0",
|
|
19
|
+
"archiver": "^8.0.0",
|
|
20
|
+
"express": "^4.18.2",
|
|
21
|
+
"fs-extra": "^11.1.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/express": "^5.0.6",
|
|
25
|
+
"@types/fs-extra": "^11.0.4",
|
|
26
|
+
"@types/jest": "^30.0.0",
|
|
27
|
+
"@types/node": "^25.9.1",
|
|
28
|
+
"@types/supertest": "^7.2.0",
|
|
29
|
+
"jest": "^30.4.2",
|
|
30
|
+
"supertest": "^7.2.2",
|
|
31
|
+
"ts-jest": "^29.4.11",
|
|
32
|
+
"typescript": "^6.0.3"
|
|
33
|
+
}
|
|
34
|
+
}
|