@panelwave/mcp 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +157 -3
- package/dist/bridge.d.ts +34 -0
- package/dist/bridge.js +180 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +89 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +50 -0
- package/dist/files.d.ts +65 -0
- package/dist/files.js +316 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +80 -0
- package/dist/local-tools.d.ts +31 -0
- package/dist/local-tools.js +176 -0
- package/dist/remote.d.ts +79 -0
- package/dist/remote.js +207 -0
- package/dist/sandbox.d.ts +7 -0
- package/dist/sandbox.js +107 -0
- package/dist/tool.d.ts +59 -0
- package/dist/tool.js +71 -0
- package/dist/upload.d.ts +76 -0
- package/dist/upload.js +582 -0
- package/package.json +50 -6
package/dist/files.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SKIP_DIRS = exports.MAX_DEPTH = void 0;
|
|
37
|
+
exports.mimeOf = mimeOf;
|
|
38
|
+
exports.kindOfMime = kindOfMime;
|
|
39
|
+
exports.globToRegExp = globToRegExp;
|
|
40
|
+
exports.matchesGlob = matchesGlob;
|
|
41
|
+
exports.imageDimensions = imageDimensions;
|
|
42
|
+
exports.naturalCompare = naturalCompare;
|
|
43
|
+
exports.looksBinary = looksBinary;
|
|
44
|
+
exports.decodeUtf8 = decodeUtf8;
|
|
45
|
+
exports.readHead = readHead;
|
|
46
|
+
exports.scanFolder = scanFolder;
|
|
47
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
48
|
+
const path = __importStar(require("node:path"));
|
|
49
|
+
const image_size_1 = require("image-size");
|
|
50
|
+
const errors_1 = require("./errors");
|
|
51
|
+
const sandbox_1 = require("./sandbox");
|
|
52
|
+
const MIME = {
|
|
53
|
+
'.png': 'image/png',
|
|
54
|
+
'.jpg': 'image/jpeg',
|
|
55
|
+
'.jpeg': 'image/jpeg',
|
|
56
|
+
'.webp': 'image/webp',
|
|
57
|
+
'.gif': 'image/gif',
|
|
58
|
+
'.avif': 'image/avif',
|
|
59
|
+
'.svg': 'image/svg+xml',
|
|
60
|
+
'.bmp': 'image/bmp',
|
|
61
|
+
'.tif': 'image/tiff',
|
|
62
|
+
'.tiff': 'image/tiff',
|
|
63
|
+
'.mp4': 'video/mp4',
|
|
64
|
+
'.m4v': 'video/mp4',
|
|
65
|
+
'.mov': 'video/quicktime',
|
|
66
|
+
'.webm': 'video/webm',
|
|
67
|
+
'.mkv': 'video/x-matroska',
|
|
68
|
+
'.mp3': 'audio/mpeg',
|
|
69
|
+
'.wav': 'audio/wav',
|
|
70
|
+
'.ogg': 'audio/ogg',
|
|
71
|
+
'.oga': 'audio/ogg',
|
|
72
|
+
'.m4a': 'audio/mp4',
|
|
73
|
+
'.aac': 'audio/aac',
|
|
74
|
+
'.flac': 'audio/flac',
|
|
75
|
+
'.opus': 'audio/opus',
|
|
76
|
+
'.woff': 'font/woff',
|
|
77
|
+
'.woff2': 'font/woff2',
|
|
78
|
+
'.ttf': 'font/ttf',
|
|
79
|
+
'.otf': 'font/otf',
|
|
80
|
+
'.txt': 'text/plain',
|
|
81
|
+
'.md': 'text/markdown',
|
|
82
|
+
'.markdown': 'text/markdown',
|
|
83
|
+
'.fountain': 'text/x-fountain',
|
|
84
|
+
'.fdx': 'application/xml',
|
|
85
|
+
'.json': 'application/json',
|
|
86
|
+
'.csv': 'text/csv',
|
|
87
|
+
'.tsv': 'text/tab-separated-values',
|
|
88
|
+
'.xml': 'application/xml',
|
|
89
|
+
'.html': 'text/html',
|
|
90
|
+
'.htm': 'text/html',
|
|
91
|
+
'.yaml': 'application/yaml',
|
|
92
|
+
'.yml': 'application/yaml',
|
|
93
|
+
'.srt': 'application/x-subrip',
|
|
94
|
+
'.vtt': 'text/vtt',
|
|
95
|
+
'.pdf': 'application/pdf',
|
|
96
|
+
'.zip': 'application/zip',
|
|
97
|
+
};
|
|
98
|
+
function mimeOf(filename) {
|
|
99
|
+
return MIME[path.extname(filename).toLowerCase()] ?? 'application/octet-stream';
|
|
100
|
+
}
|
|
101
|
+
/** Asset kind as the gateway understands it (image/video/audio/font), plus text and other. */
|
|
102
|
+
function kindOfMime(mime) {
|
|
103
|
+
if (mime.startsWith('image/'))
|
|
104
|
+
return 'image';
|
|
105
|
+
if (mime.startsWith('video/'))
|
|
106
|
+
return 'video';
|
|
107
|
+
if (mime.startsWith('audio/'))
|
|
108
|
+
return 'audio';
|
|
109
|
+
if (mime.startsWith('font/'))
|
|
110
|
+
return 'font';
|
|
111
|
+
if (mime.startsWith('text/') || ['application/json', 'application/xml', 'application/yaml', 'application/x-subrip'].includes(mime))
|
|
112
|
+
return 'text';
|
|
113
|
+
return 'other';
|
|
114
|
+
}
|
|
115
|
+
function escapeRe(s) {
|
|
116
|
+
return s.replace(/[.+^$()|[\]\\]/g, '\\$&');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Compile a file-name glob: `*` (any run), `?` (one char), `{a,b}` (alternatives),
|
|
120
|
+
* matched case-insensitively against the file name. Several patterns may be
|
|
121
|
+
* given separated by `;`.
|
|
122
|
+
*/
|
|
123
|
+
function globToRegExp(glob) {
|
|
124
|
+
let re = '';
|
|
125
|
+
let depth = 0;
|
|
126
|
+
for (const ch of glob) {
|
|
127
|
+
if (ch === '*')
|
|
128
|
+
re += '[^/\\\\]*';
|
|
129
|
+
else if (ch === '?')
|
|
130
|
+
re += '[^/\\\\]';
|
|
131
|
+
else if (ch === '{') {
|
|
132
|
+
depth++;
|
|
133
|
+
re += '(?:';
|
|
134
|
+
}
|
|
135
|
+
else if (ch === '}' && depth > 0) {
|
|
136
|
+
depth--;
|
|
137
|
+
re += ')';
|
|
138
|
+
}
|
|
139
|
+
else if (ch === ',' && depth > 0)
|
|
140
|
+
re += '|';
|
|
141
|
+
else
|
|
142
|
+
re += escapeRe(ch);
|
|
143
|
+
}
|
|
144
|
+
if (depth > 0)
|
|
145
|
+
throw new errors_1.BridgeError('INVALID_INPUT', `Unbalanced "{" in glob ${glob}.`);
|
|
146
|
+
return new RegExp(`^${re}$`, 'i');
|
|
147
|
+
}
|
|
148
|
+
function matchesGlob(name, glob) {
|
|
149
|
+
if (!glob)
|
|
150
|
+
return true;
|
|
151
|
+
return glob
|
|
152
|
+
.split(';')
|
|
153
|
+
.map((g) => g.trim())
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.some((g) => globToRegExp(g).test(name));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Display dimensions of an image file. EXIF orientations 5–8 rotate the image
|
|
159
|
+
* by 90°, so width and height are swapped to match what viewers show.
|
|
160
|
+
* Returns undefined for files image-size cannot read (and for SVGs without size).
|
|
161
|
+
*/
|
|
162
|
+
async function imageDimensions(filePath) {
|
|
163
|
+
// Headers normally fit in 512 KB; JPEGs with a large EXIF/XMP block before
|
|
164
|
+
// the frame header get a second, bigger read.
|
|
165
|
+
for (const bytes of HEADER_READS) {
|
|
166
|
+
let head;
|
|
167
|
+
try {
|
|
168
|
+
head = await readHead(filePath, bytes);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const r = (0, image_size_1.imageSize)(head.buf);
|
|
175
|
+
if (!r.width || !r.height)
|
|
176
|
+
return undefined;
|
|
177
|
+
const rotated = r.orientation !== undefined && r.orientation >= 5 && r.orientation <= 8;
|
|
178
|
+
return { width: rotated ? r.height : r.width, height: rotated ? r.width : r.height, ...(r.orientation ? { orientation: r.orientation } : {}) };
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
if (head.sizeBytes <= head.buf.length)
|
|
182
|
+
return undefined; // whole file read — not an image we understand
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const HEADER_READS = [512 * 1024, 8 * 1024 * 1024];
|
|
188
|
+
/** Natural sort (panel-2 before panel-10), case-insensitive. */
|
|
189
|
+
function naturalCompare(a, b) {
|
|
190
|
+
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
|
|
191
|
+
}
|
|
192
|
+
function looksBinary(buf) {
|
|
193
|
+
const n = Math.min(buf.length, 8192);
|
|
194
|
+
for (let i = 0; i < n; i++)
|
|
195
|
+
if (buf[i] === 0)
|
|
196
|
+
return true;
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Decode UTF-8 text, dropping a BOM and — when the buffer was cut — a partial
|
|
201
|
+
* multi-byte sequence at the end.
|
|
202
|
+
*/
|
|
203
|
+
function decodeUtf8(buf, cut) {
|
|
204
|
+
let end = buf.length;
|
|
205
|
+
if (cut) {
|
|
206
|
+
// Step back over continuation bytes to the lead byte of the last sequence.
|
|
207
|
+
let i = end - 1;
|
|
208
|
+
while (i >= 0 && i > end - 4 && (buf[i] & 0xc0) === 0x80)
|
|
209
|
+
i--;
|
|
210
|
+
if (i >= 0) {
|
|
211
|
+
const lead = buf[i];
|
|
212
|
+
const need = lead >= 0xf0 ? 4 : lead >= 0xe0 ? 3 : lead >= 0xc0 ? 2 : 1;
|
|
213
|
+
if (end - i < need)
|
|
214
|
+
end = i;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
let start = 0;
|
|
218
|
+
if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf)
|
|
219
|
+
start = 3;
|
|
220
|
+
return new TextDecoder('utf-8').decode(buf.subarray(start, Math.max(start, end)));
|
|
221
|
+
}
|
|
222
|
+
async function readHead(filePath, maxBytes) {
|
|
223
|
+
const handle = await fs.open(filePath, 'r');
|
|
224
|
+
try {
|
|
225
|
+
const stat = await handle.stat();
|
|
226
|
+
const len = Math.min(stat.size, maxBytes);
|
|
227
|
+
const buf = Buffer.alloc(len);
|
|
228
|
+
let read = 0;
|
|
229
|
+
while (read < len) {
|
|
230
|
+
const { bytesRead } = await handle.read(buf, read, len - read, read);
|
|
231
|
+
if (!bytesRead)
|
|
232
|
+
break;
|
|
233
|
+
read += bytesRead;
|
|
234
|
+
}
|
|
235
|
+
return { buf: buf.subarray(0, read), sizeBytes: stat.size };
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
await handle.close();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
exports.MAX_DEPTH = 12;
|
|
242
|
+
exports.SKIP_DIRS = new Set(['node_modules', '.git', '.svn', '.hg', '__MACOSX', '$RECYCLE.BIN', 'System Volume Information']);
|
|
243
|
+
/**
|
|
244
|
+
* Walk a folder inside the sandbox: natural order, subfolders after the files
|
|
245
|
+
* of a folder, symlinks followed only when their target is allowed too,
|
|
246
|
+
* node_modules/.git and (unless asked) dot entries skipped.
|
|
247
|
+
*/
|
|
248
|
+
async function scanFolder(folder, allowedDirs, opts) {
|
|
249
|
+
const root = (0, sandbox_1.resolveAllowed)(folder, allowedDirs);
|
|
250
|
+
const stat = await fs.stat(root).catch((e) => {
|
|
251
|
+
throw (0, errors_1.toBridgeError)(e);
|
|
252
|
+
});
|
|
253
|
+
if (!stat.isDirectory())
|
|
254
|
+
throw new errors_1.BridgeError('INVALID_INPUT', `${folder} is a file, not a folder (pw_local_read_text reads a text file).`);
|
|
255
|
+
const found = [];
|
|
256
|
+
const skipped = [];
|
|
257
|
+
let truncated = false;
|
|
258
|
+
const walk = async (dir, rel, depth) => {
|
|
259
|
+
if (truncated)
|
|
260
|
+
return;
|
|
261
|
+
opts.signal?.throwIfAborted();
|
|
262
|
+
let entries;
|
|
263
|
+
try {
|
|
264
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
skipped.push(rel || '.');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
entries.sort((a, b) => naturalCompare(a.name, b.name));
|
|
271
|
+
const subdirs = [];
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
if (!opts.includeHidden && entry.name.startsWith('.'))
|
|
274
|
+
continue;
|
|
275
|
+
const abs = path.join(dir, entry.name);
|
|
276
|
+
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
277
|
+
let isDir = entry.isDirectory();
|
|
278
|
+
let isFile = entry.isFile();
|
|
279
|
+
let target = abs;
|
|
280
|
+
if (entry.isSymbolicLink()) {
|
|
281
|
+
try {
|
|
282
|
+
target = (0, sandbox_1.resolveAllowed)(abs, allowedDirs);
|
|
283
|
+
const s = await fs.stat(target);
|
|
284
|
+
isDir = s.isDirectory();
|
|
285
|
+
isFile = s.isFile();
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
skipped.push(relPath);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (isDir) {
|
|
293
|
+
if (opts.recursive && depth < exports.MAX_DEPTH && !exports.SKIP_DIRS.has(entry.name))
|
|
294
|
+
subdirs.push({ abs: target, rel: relPath });
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (!isFile || !matchesGlob(entry.name, opts.glob) || (opts.accept && !opts.accept(entry.name)))
|
|
298
|
+
continue;
|
|
299
|
+
if (found.length >= opts.limit) {
|
|
300
|
+
truncated = true;
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const s = await fs.stat(target);
|
|
305
|
+
found.push({ abs, rel: relPath, size: s.size, mtime: s.mtime });
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
skipped.push(relPath);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (const sub of subdirs)
|
|
312
|
+
await walk(sub.abs, sub.rel, depth + 1);
|
|
313
|
+
};
|
|
314
|
+
await walk(root, '', 0);
|
|
315
|
+
return { root, found, skipped, truncated };
|
|
316
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export { startBridge, createBridgeServer, LOCAL_INSTRUCTIONS } from './bridge';
|
|
3
|
+
export { loadConfig, ConfigError, DEFAULT_URL } from './config';
|
|
4
|
+
export { RemoteProxy } from './remote';
|
|
5
|
+
export { LOCAL_TOOLS } from './local-tools';
|
|
6
|
+
export declare function main(argv?: string[]): Promise<void>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.LOCAL_TOOLS = exports.RemoteProxy = exports.DEFAULT_URL = exports.ConfigError = exports.loadConfig = exports.LOCAL_INSTRUCTIONS = exports.createBridgeServer = exports.startBridge = void 0;
|
|
5
|
+
exports.main = main;
|
|
6
|
+
/**
|
|
7
|
+
* panelwave-mcp — local stdio bridge to the hosted PanelWave MCP server.
|
|
8
|
+
*
|
|
9
|
+
* npx -y @panelwave/mcp
|
|
10
|
+
*
|
|
11
|
+
* Environment:
|
|
12
|
+
* PANELWAVE_TOKEN personal access token (required)
|
|
13
|
+
* PANELWAVE_MCP_URL hosted endpoint (default https://mcp.panelwave.org/mcp)
|
|
14
|
+
* PANELWAVE_ALLOWED_DIRS folders the local file tools may read, separated by
|
|
15
|
+
* ";" on Windows and ":" elsewhere (default: the working directory)
|
|
16
|
+
*
|
|
17
|
+
* stdout carries the MCP protocol; all diagnostics go to stderr.
|
|
18
|
+
*/
|
|
19
|
+
const stdio_1 = require("@modelcontextprotocol/server/stdio");
|
|
20
|
+
const bridge_1 = require("./bridge");
|
|
21
|
+
const config_1 = require("./config");
|
|
22
|
+
var bridge_2 = require("./bridge");
|
|
23
|
+
Object.defineProperty(exports, "startBridge", { enumerable: true, get: function () { return bridge_2.startBridge; } });
|
|
24
|
+
Object.defineProperty(exports, "createBridgeServer", { enumerable: true, get: function () { return bridge_2.createBridgeServer; } });
|
|
25
|
+
Object.defineProperty(exports, "LOCAL_INSTRUCTIONS", { enumerable: true, get: function () { return bridge_2.LOCAL_INSTRUCTIONS; } });
|
|
26
|
+
var config_2 = require("./config");
|
|
27
|
+
Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return config_2.loadConfig; } });
|
|
28
|
+
Object.defineProperty(exports, "ConfigError", { enumerable: true, get: function () { return config_2.ConfigError; } });
|
|
29
|
+
Object.defineProperty(exports, "DEFAULT_URL", { enumerable: true, get: function () { return config_2.DEFAULT_URL; } });
|
|
30
|
+
var remote_1 = require("./remote");
|
|
31
|
+
Object.defineProperty(exports, "RemoteProxy", { enumerable: true, get: function () { return remote_1.RemoteProxy; } });
|
|
32
|
+
var local_tools_1 = require("./local-tools");
|
|
33
|
+
Object.defineProperty(exports, "LOCAL_TOOLS", { enumerable: true, get: function () { return local_tools_1.LOCAL_TOOLS; } });
|
|
34
|
+
const HELP = `panelwave-mcp ${config_1.version} — PanelWave MCP bridge (stdio)
|
|
35
|
+
|
|
36
|
+
Add it to your MCP client, e.g. Claude Code:
|
|
37
|
+
claude mcp add panelwave --env PANELWAVE_TOKEN=pw_pat_… -- npx -y @panelwave/mcp
|
|
38
|
+
|
|
39
|
+
Environment:
|
|
40
|
+
PANELWAVE_TOKEN personal access token (required)
|
|
41
|
+
PANELWAVE_MCP_URL hosted endpoint (default ${config_1.DEFAULT_URL})
|
|
42
|
+
PANELWAVE_ALLOWED_DIRS folders the local file tools may read (default: working directory)
|
|
43
|
+
`;
|
|
44
|
+
function log(message) {
|
|
45
|
+
process.stderr.write(`[panelwave-mcp] ${message}\n`);
|
|
46
|
+
}
|
|
47
|
+
async function main(argv = process.argv.slice(2)) {
|
|
48
|
+
if (argv.includes('--version') || argv.includes('-v')) {
|
|
49
|
+
process.stdout.write(`${config_1.version}\n`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
53
|
+
process.stdout.write(HELP);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const config = (0, config_1.loadConfig)();
|
|
57
|
+
log(config.allowedDirs.length
|
|
58
|
+
? `v${config_1.version} → ${config.url}; local folders: ${config.allowedDirs.join(', ')}`
|
|
59
|
+
: `v${config_1.version} → ${config.url}; local file tools are OFF — set PANELWAVE_ALLOWED_DIRS to the folder with your scripts and artwork to use them`);
|
|
60
|
+
const bridge = await (0, bridge_1.startBridge)(config, { log });
|
|
61
|
+
let closing = false;
|
|
62
|
+
const shutdown = async (code = 0) => {
|
|
63
|
+
if (closing)
|
|
64
|
+
return;
|
|
65
|
+
closing = true;
|
|
66
|
+
await bridge.close().catch(() => undefined);
|
|
67
|
+
process.exit(code);
|
|
68
|
+
};
|
|
69
|
+
bridge.server.onclose = () => void shutdown(0);
|
|
70
|
+
process.on('SIGINT', () => void shutdown(0));
|
|
71
|
+
process.on('SIGTERM', () => void shutdown(0));
|
|
72
|
+
process.stdin.on('end', () => void shutdown(0));
|
|
73
|
+
await bridge.server.connect(new stdio_1.StdioServerTransport());
|
|
74
|
+
}
|
|
75
|
+
if (require.main === module) {
|
|
76
|
+
main().catch((e) => {
|
|
77
|
+
log(e instanceof config_1.ConfigError ? e.message : `failed to start: ${e instanceof Error ? e.stack ?? e.message : String(e)}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type LocalTool } from './tool';
|
|
3
|
+
export type { LocalTool, LocalToolContext, LocalToolResult } from './tool';
|
|
4
|
+
export { errorResult } from './tool';
|
|
5
|
+
export declare const LIST_LIMIT_DEFAULT = 1000;
|
|
6
|
+
export declare const LIST_LIMIT_MAX = 5000;
|
|
7
|
+
declare const zLocalFile: z.ZodObject<{
|
|
8
|
+
path: z.ZodString;
|
|
9
|
+
relativePath: z.ZodString;
|
|
10
|
+
name: z.ZodString;
|
|
11
|
+
sizeBytes: z.ZodNumber;
|
|
12
|
+
mime: z.ZodString;
|
|
13
|
+
kind: z.ZodEnum<{
|
|
14
|
+
image: "image";
|
|
15
|
+
video: "video";
|
|
16
|
+
audio: "audio";
|
|
17
|
+
font: "font";
|
|
18
|
+
text: "text";
|
|
19
|
+
other: "other";
|
|
20
|
+
}>;
|
|
21
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
22
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
23
|
+
orientation: z.ZodOptional<z.ZodNumber>;
|
|
24
|
+
modifiedAt: z.ZodString;
|
|
25
|
+
}, z.core.$strip>;
|
|
26
|
+
export type LocalFile = z.infer<typeof zLocalFile>;
|
|
27
|
+
export declare const listFiles: LocalTool;
|
|
28
|
+
export declare const READ_DEFAULT_BYTES: number;
|
|
29
|
+
export declare const READ_MAX_BYTES: number;
|
|
30
|
+
export declare const readText: LocalTool;
|
|
31
|
+
export declare const LOCAL_TOOLS: LocalTool[];
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.LOCAL_TOOLS = exports.readText = exports.READ_MAX_BYTES = exports.READ_DEFAULT_BYTES = exports.listFiles = exports.LIST_LIMIT_MAX = exports.LIST_LIMIT_DEFAULT = exports.errorResult = void 0;
|
|
37
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const zod_1 = require("zod");
|
|
40
|
+
const errors_1 = require("./errors");
|
|
41
|
+
const files_1 = require("./files");
|
|
42
|
+
const sandbox_1 = require("./sandbox");
|
|
43
|
+
const tool_1 = require("./tool");
|
|
44
|
+
const upload_1 = require("./upload");
|
|
45
|
+
var tool_2 = require("./tool");
|
|
46
|
+
Object.defineProperty(exports, "errorResult", { enumerable: true, get: function () { return tool_2.errorResult; } });
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// pw_local_list_files
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
exports.LIST_LIMIT_DEFAULT = 1000;
|
|
51
|
+
exports.LIST_LIMIT_MAX = 5000;
|
|
52
|
+
const zFileKind = zod_1.z.enum(['image', 'video', 'audio', 'font', 'text', 'other']);
|
|
53
|
+
const zLocalFile = zod_1.z.object({
|
|
54
|
+
path: zod_1.z.string().describe('Absolute path — pass it to pw_local_read_text or pw_local_upload_files.'),
|
|
55
|
+
relativePath: zod_1.z.string().describe('Path relative to the listed folder, with forward slashes.'),
|
|
56
|
+
name: zod_1.z.string(),
|
|
57
|
+
sizeBytes: zod_1.z.number().int(),
|
|
58
|
+
mime: zod_1.z.string(),
|
|
59
|
+
kind: zFileKind,
|
|
60
|
+
width: zod_1.z.number().int().optional().describe('Display width in px (EXIF orientation applied).'),
|
|
61
|
+
height: zod_1.z.number().int().optional(),
|
|
62
|
+
orientation: zod_1.z.number().int().optional().describe('EXIF orientation (1–8) when present.'),
|
|
63
|
+
modifiedAt: zod_1.z.string(),
|
|
64
|
+
});
|
|
65
|
+
const listFilesInput = zod_1.z.object({
|
|
66
|
+
folder: zod_1.z.string().min(1).describe('Folder to list — absolute, or relative to the first allowed folder.'),
|
|
67
|
+
glob: zod_1.z.string().max(500).optional().describe('File-name filter: * ? and {a,b}, several patterns separated by ";" — e.g. "*.{png,jpg,webp}". Case-insensitive.'),
|
|
68
|
+
recursive: zod_1.z.boolean().default(false).describe('Include subfolders (node_modules/.git are always skipped).'),
|
|
69
|
+
includeHidden: zod_1.z.boolean().default(false).describe('Include dot files and dot folders.'),
|
|
70
|
+
dimensions: zod_1.z.boolean().default(true).describe('Read pixel dimensions of images (reads only the file header).'),
|
|
71
|
+
limit: zod_1.z.number().int().min(1).max(exports.LIST_LIMIT_MAX).default(exports.LIST_LIMIT_DEFAULT),
|
|
72
|
+
});
|
|
73
|
+
const listFilesOutput = zod_1.z.object({
|
|
74
|
+
folder: zod_1.z.string(),
|
|
75
|
+
files: zod_1.z.array(zLocalFile),
|
|
76
|
+
count: zod_1.z.number().int(),
|
|
77
|
+
truncated: zod_1.z.boolean().describe('True when more files matched than `limit`.'),
|
|
78
|
+
totalSizeBytes: zod_1.z.number().int(),
|
|
79
|
+
byKind: zod_1.z.record(zod_1.z.string(), zod_1.z.number().int()),
|
|
80
|
+
skipped: zod_1.z.array(zod_1.z.string()).describe('Entries that could not be read or point outside the allowed folders.'),
|
|
81
|
+
});
|
|
82
|
+
exports.listFiles = (0, tool_1.defineLocalTool)({
|
|
83
|
+
name: 'pw_local_list_files',
|
|
84
|
+
title: 'List local files',
|
|
85
|
+
description: 'List files in a folder on this computer (only inside the folders the PanelWave bridge was allowed to read). Returns absolute paths, size, mime type, kind (image/video/audio/font/text/other) and — for images — pixel dimensions with EXIF orientation applied. Files are sorted naturally (panel-2 before panel-10). Use it to find panel artwork before pw_local_upload_files, or a script before pw_local_read_text.',
|
|
86
|
+
input: listFilesInput,
|
|
87
|
+
output: listFilesOutput,
|
|
88
|
+
async run(args, ctx) {
|
|
89
|
+
const { root, found, skipped, truncated } = await (0, files_1.scanFolder)(args.folder, ctx.allowedDirs, {
|
|
90
|
+
glob: args.glob,
|
|
91
|
+
recursive: args.recursive,
|
|
92
|
+
includeHidden: args.includeHidden,
|
|
93
|
+
limit: args.limit,
|
|
94
|
+
signal: ctx.signal,
|
|
95
|
+
});
|
|
96
|
+
const files = await (0, tool_1.mapLimit)(found, 8, async (f) => {
|
|
97
|
+
const name = path.basename(f.abs);
|
|
98
|
+
const mime = (0, files_1.mimeOf)(name);
|
|
99
|
+
const kind = (0, files_1.kindOfMime)(mime);
|
|
100
|
+
const row = { path: f.abs, relativePath: f.rel, name, sizeBytes: f.size, mime, kind, modifiedAt: f.mtime.toISOString() };
|
|
101
|
+
if (args.dimensions && kind === 'image') {
|
|
102
|
+
const dim = await (0, files_1.imageDimensions)(f.abs);
|
|
103
|
+
if (dim)
|
|
104
|
+
Object.assign(row, dim);
|
|
105
|
+
}
|
|
106
|
+
return row;
|
|
107
|
+
});
|
|
108
|
+
const byKind = {};
|
|
109
|
+
let totalSizeBytes = 0;
|
|
110
|
+
for (const f of files) {
|
|
111
|
+
byKind[f.kind] = (byKind[f.kind] ?? 0) + 1;
|
|
112
|
+
totalSizeBytes += f.sizeBytes;
|
|
113
|
+
}
|
|
114
|
+
const kinds = Object.entries(byKind)
|
|
115
|
+
.map(([k, n]) => `${n} ${k}`)
|
|
116
|
+
.join(', ');
|
|
117
|
+
const lines = files.slice(0, 200).map((f) => `- ${f.relativePath} (${(0, tool_1.formatBytes)(f.sizeBytes)}${f.width ? `, ${f.width}×${f.height}` : ''})`);
|
|
118
|
+
const text = [
|
|
119
|
+
files.length ? `${files.length} file${files.length === 1 ? '' : 's'} in ${root} (${kinds}; ${(0, tool_1.formatBytes)(totalSizeBytes)})${truncated ? ` — truncated at ${args.limit}, narrow the glob or raise limit` : ''}:` : `No matching files in ${root}.`,
|
|
120
|
+
...lines,
|
|
121
|
+
...(files.length > lines.length ? [`… and ${files.length - lines.length} more (see structured result).`] : []),
|
|
122
|
+
...(skipped.length ? [`Skipped ${skipped.length} unreadable or out-of-sandbox entr${skipped.length === 1 ? 'y' : 'ies'}.`] : []),
|
|
123
|
+
].join('\n');
|
|
124
|
+
return { text, data: { folder: root, files, count: files.length, truncated, totalSizeBytes, byKind, skipped } };
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// pw_local_read_text
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
exports.READ_DEFAULT_BYTES = 1024 * 1024;
|
|
131
|
+
exports.READ_MAX_BYTES = 5 * 1024 * 1024;
|
|
132
|
+
const readTextInput = zod_1.z.object({
|
|
133
|
+
path: zod_1.z.string().min(1).describe('File to read — absolute, or relative to the first allowed folder.'),
|
|
134
|
+
maxBytes: zod_1.z.number().int().min(1).max(exports.READ_MAX_BYTES).default(exports.READ_DEFAULT_BYTES).describe('Read at most this many bytes (default 1 MiB, max 5 MiB).'),
|
|
135
|
+
});
|
|
136
|
+
const readTextOutput = zod_1.z.object({
|
|
137
|
+
path: zod_1.z.string(),
|
|
138
|
+
name: zod_1.z.string(),
|
|
139
|
+
mime: zod_1.z.string(),
|
|
140
|
+
text: zod_1.z.string(),
|
|
141
|
+
sizeBytes: zod_1.z.number().int(),
|
|
142
|
+
returnedBytes: zod_1.z.number().int(),
|
|
143
|
+
truncated: zod_1.z.boolean(),
|
|
144
|
+
});
|
|
145
|
+
exports.readText = (0, tool_1.defineLocalTool)({
|
|
146
|
+
name: 'pw_local_read_text',
|
|
147
|
+
title: 'Read a local text file',
|
|
148
|
+
description: 'Read a UTF-8 text file on this computer — a script, screenplay (.fountain), outline (.md/.json) or subtitle file — from the folders the PanelWave bridge may read. Long files are cut at maxBytes (default 1 MiB) and flagged `truncated`. Binary files are refused.',
|
|
149
|
+
input: readTextInput,
|
|
150
|
+
output: readTextOutput,
|
|
151
|
+
async run(args, ctx) {
|
|
152
|
+
// No dotfiles or dot-folders (.ssh, .env, .git …): scripts never live there, secrets often do.
|
|
153
|
+
const file = (0, sandbox_1.resolveAllowed)(args.path, ctx.allowedDirs, { allowHidden: false });
|
|
154
|
+
const stat = await fs.stat(file).catch((e) => {
|
|
155
|
+
throw (0, errors_1.toBridgeError)(e);
|
|
156
|
+
});
|
|
157
|
+
if (stat.isDirectory())
|
|
158
|
+
throw new errors_1.BridgeError('INVALID_INPUT', `${args.path} is a folder. Use pw_local_list_files to list it.`);
|
|
159
|
+
const { buf, sizeBytes } = await (0, files_1.readHead)(file, args.maxBytes).catch((e) => {
|
|
160
|
+
throw (0, errors_1.toBridgeError)(e);
|
|
161
|
+
});
|
|
162
|
+
if ((0, files_1.looksBinary)(buf))
|
|
163
|
+
throw new errors_1.BridgeError('PRECONDITION_FAILED', `${path.basename(file)} is a binary file (${(0, files_1.mimeOf)(file)}), not text. Upload media with pw_local_upload_files instead.`);
|
|
164
|
+
const truncated = sizeBytes > buf.length;
|
|
165
|
+
const text = (0, files_1.decodeUtf8)(buf, truncated);
|
|
166
|
+
const name = path.basename(file);
|
|
167
|
+
const header = truncated
|
|
168
|
+
? `${name}: first ${(0, tool_1.formatBytes)(buf.length)} of ${(0, tool_1.formatBytes)(sizeBytes)} (truncated — raise maxBytes up to 5 MiB to read more).`
|
|
169
|
+
: `${name} (${(0, tool_1.formatBytes)(sizeBytes)}):`;
|
|
170
|
+
return {
|
|
171
|
+
text: `${header}\n\n${text}`,
|
|
172
|
+
data: { path: file, name, mime: (0, files_1.mimeOf)(name), text, sizeBytes, returnedBytes: Buffer.byteLength(text, 'utf8'), truncated },
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
exports.LOCAL_TOOLS = [exports.listFiles, exports.readText, upload_1.uploadFiles];
|