tabbied-mcp 0.2.2 → 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/LICENSE +21 -21
- package/README.md +147 -83
- package/dist/index.js.map +1 -1
- package/dist/info.d.ts +1 -1
- package/dist/info.js +10 -9
- package/dist/info.js.map +1 -1
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.js +7 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/render.js +197 -99
- package/dist/node/render.js.map +1 -1
- package/dist/node/resources.js +37 -23
- package/dist/node/resources.js.map +1 -1
- package/dist/server.js +9 -1
- package/dist/server.js.map +1 -1
- package/dist/stdio.js +2 -0
- package/dist/stdio.js.map +1 -1
- package/dist/templates.js +143 -27
- package/dist/templates.js.map +1 -1
- package/dist/tools.js +53 -5
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +11 -1
- package/dist/types.js.map +1 -1
- package/package.json +61 -55
package/dist/node/render.js
CHANGED
|
@@ -16,11 +16,49 @@ const RENDER_TIMEOUT_MS = 180_000;
|
|
|
16
16
|
// A PNG returned inline is spent context. Past this the file stays on disk and
|
|
17
17
|
// the agent gets the path - better a usable pointer than a truncated image.
|
|
18
18
|
const INLINE_BYTE_BUDGET = 1_500_000;
|
|
19
|
+
// Past these a PNG is tens of megapixels (or a browser canvas limit), which
|
|
20
|
+
// no asset this tool exists for needs.
|
|
21
|
+
const SIZE_MAX = 4096;
|
|
22
|
+
const SCALE_MAX = 4;
|
|
19
23
|
const text = (value) => ({ type: 'text', text: value });
|
|
20
24
|
const toolError = (message) => ({
|
|
21
25
|
content: [text(message)],
|
|
22
26
|
isError: true,
|
|
23
27
|
});
|
|
28
|
+
/** A whole number in [min, max], the default when absent, or null when invalid. */
|
|
29
|
+
function integerArg(value, fallback, min, max) {
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return fallback;
|
|
32
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max
|
|
33
|
+
? value
|
|
34
|
+
: null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The CLI's own message out of its stderr: each `tabbied:` line and the
|
|
38
|
+
* indented lines that continue it. Anything else there (Node's warnings, a
|
|
39
|
+
* deprecation notice from a dependency) is noise to the agent. When there is
|
|
40
|
+
* no such line, the CLI died some other way, and the tail is the best guess.
|
|
41
|
+
*/
|
|
42
|
+
function cliMessage(stderr) {
|
|
43
|
+
const lines = stderr.split('\n');
|
|
44
|
+
const kept = [];
|
|
45
|
+
let continuing = false;
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
if (line.startsWith('tabbied:')) {
|
|
48
|
+
kept.push(line);
|
|
49
|
+
continuing = true;
|
|
50
|
+
}
|
|
51
|
+
else if (continuing && /^\s+\S/.test(line)) {
|
|
52
|
+
kept.push(line);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
continuing = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return kept.length > 0
|
|
59
|
+
? kept.join('\n')
|
|
60
|
+
: lines.filter((line) => line.trim()).slice(-5).join('\n');
|
|
61
|
+
}
|
|
24
62
|
export function renderTool(catalog) {
|
|
25
63
|
return {
|
|
26
64
|
definition: {
|
|
@@ -30,12 +68,14 @@ export function renderTool(catalog) {
|
|
|
30
68
|
'palette, and option set. Use this to produce an actual asset - or to ' +
|
|
31
69
|
'preview a *customized* configuration, which preview_design cannot show ' +
|
|
32
70
|
'you (it only has the stock palette and defaults). Needs Playwright ' +
|
|
33
|
-
'
|
|
34
|
-
'
|
|
71
|
+
'where this server runs (start it as `npx -y -p tabbied-mcp -p ' +
|
|
72
|
+
'playwright tabbied-mcp`, with `npx playwright install chromium` once). ' +
|
|
73
|
+
'Without "out" the image comes back inline; with it, the file is ' +
|
|
74
|
+
'written where you ask, replacing any file already there.',
|
|
35
75
|
inputSchema: {
|
|
36
76
|
type: 'object',
|
|
37
77
|
properties: {
|
|
38
|
-
slug: { type: 'string', description: 'The design to render.' },
|
|
78
|
+
slug: { type: 'string', minLength: 1, description: 'The design to render.' },
|
|
39
79
|
format: {
|
|
40
80
|
type: 'string',
|
|
41
81
|
enum: ['svg', 'png'],
|
|
@@ -44,26 +84,40 @@ export function renderTool(catalog) {
|
|
|
44
84
|
},
|
|
45
85
|
out: {
|
|
46
86
|
type: 'string',
|
|
47
|
-
|
|
48
|
-
|
|
87
|
+
minLength: 1,
|
|
88
|
+
description: 'Absolute path to write to, ending in .svg or .png to match ' +
|
|
89
|
+
'"format". Omit to get the image back inline instead of on disk.',
|
|
49
90
|
},
|
|
50
91
|
seed: {
|
|
51
92
|
type: 'string',
|
|
93
|
+
minLength: 1,
|
|
52
94
|
description: 'Fixed seed - the same seed always gives the same image. Random ' +
|
|
53
95
|
'when omitted.',
|
|
54
96
|
},
|
|
55
97
|
palette: {
|
|
56
98
|
type: 'array',
|
|
57
|
-
items: { type: 'string' },
|
|
99
|
+
items: { type: 'string', minLength: 1 },
|
|
100
|
+
minItems: 1,
|
|
58
101
|
description: "CSS colors, background first. Omit for the design's authored palette.",
|
|
59
102
|
},
|
|
60
103
|
options: {
|
|
61
104
|
type: 'object',
|
|
62
|
-
|
|
63
|
-
|
|
105
|
+
additionalProperties: { type: ['string', 'number', 'boolean'] },
|
|
106
|
+
description: 'Option values keyed by option id, e.g. {"frequency": 0.6}. Read ' +
|
|
107
|
+
'the ids, ranges, and choices off get_design.',
|
|
108
|
+
},
|
|
109
|
+
width: {
|
|
110
|
+
type: 'integer',
|
|
111
|
+
minimum: 1,
|
|
112
|
+
maximum: SIZE_MAX,
|
|
113
|
+
description: 'Pixels wide (default 960).',
|
|
114
|
+
},
|
|
115
|
+
height: {
|
|
116
|
+
type: 'integer',
|
|
117
|
+
minimum: 1,
|
|
118
|
+
maximum: SIZE_MAX,
|
|
119
|
+
description: 'Pixels tall (default 960).',
|
|
64
120
|
},
|
|
65
|
-
width: { type: 'integer', description: 'Pixels wide (default 960).' },
|
|
66
|
-
height: { type: 'integer', description: 'Pixels tall (default 960).' },
|
|
67
121
|
fit: {
|
|
68
122
|
type: 'string',
|
|
69
123
|
enum: ['grid', 'cover', 'fixed'],
|
|
@@ -71,6 +125,8 @@ export function renderTool(catalog) {
|
|
|
71
125
|
},
|
|
72
126
|
scale: {
|
|
73
127
|
type: 'integer',
|
|
128
|
+
minimum: 1,
|
|
129
|
+
maximum: SCALE_MAX,
|
|
74
130
|
description: 'PNG device-scale factor. Defaults to 2 when writing a file and ' +
|
|
75
131
|
'1 when returning inline, to keep the response small.',
|
|
76
132
|
},
|
|
@@ -78,6 +134,9 @@ export function renderTool(catalog) {
|
|
|
78
134
|
required: ['slug', 'format'],
|
|
79
135
|
additionalProperties: false,
|
|
80
136
|
},
|
|
137
|
+
// It writes a file, so it is not read-only; it only ever adds or
|
|
138
|
+
// replaces the one file it was asked for.
|
|
139
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
81
140
|
},
|
|
82
141
|
async run(args) {
|
|
83
142
|
const slug = typeof args.slug === 'string' ? args.slug : '';
|
|
@@ -90,118 +149,157 @@ export function renderTool(catalog) {
|
|
|
90
149
|
return toolError(`"${slug}" paints effects SVG cannot represent, so it has no vector ` +
|
|
91
150
|
'export. Render it as PNG instead.');
|
|
92
151
|
}
|
|
152
|
+
// The schema states these, but a host that skips validation (or calls
|
|
153
|
+
// run() directly) must not hand the CLI a path it would resolve against
|
|
154
|
+
// whatever directory the server happened to start in.
|
|
155
|
+
const out = typeof args.out === 'string' && args.out.length > 0 ? args.out : null;
|
|
156
|
+
if (out !== null && !path.isAbsolute(out)) {
|
|
157
|
+
return toolError(`"out" must be an absolute path, got "${out}". A relative one would ` +
|
|
158
|
+
"land in the server's working directory, not yours.");
|
|
159
|
+
}
|
|
160
|
+
if (out !== null && path.extname(out).toLowerCase() !== `.${format}`) {
|
|
161
|
+
return toolError(`"out" must end in .${format} to match format "${format}", got "${out}".`);
|
|
162
|
+
}
|
|
163
|
+
const inline = out === null;
|
|
164
|
+
const width = integerArg(args.width, 960, 1, SIZE_MAX);
|
|
165
|
+
const height = integerArg(args.height, 960, 1, SIZE_MAX);
|
|
166
|
+
const scale = integerArg(args.scale, inline ? 1 : 2, 1, SCALE_MAX);
|
|
167
|
+
if (width === null || height === null) {
|
|
168
|
+
return toolError(`"width" and "height" must be whole pixels from 1 to ${SIZE_MAX}.`);
|
|
169
|
+
}
|
|
170
|
+
if (scale === null) {
|
|
171
|
+
return toolError(`"scale" must be a whole number from 1 to ${SCALE_MAX}.`);
|
|
172
|
+
}
|
|
173
|
+
// Options reach the CLI as one "id: value; id: value" string, so a `;`
|
|
174
|
+
// inside a value would end it and start an option of its own, past
|
|
175
|
+
// every check on the first. Ranges and choices are the CLI's to check.
|
|
176
|
+
let options = null;
|
|
177
|
+
if (args.options !== undefined) {
|
|
178
|
+
if (!args.options || typeof args.options !== 'object' || Array.isArray(args.options)) {
|
|
179
|
+
return toolError('"options" must be an object of option values keyed by id.');
|
|
180
|
+
}
|
|
181
|
+
const ids = design.options.map((option) => option.id);
|
|
182
|
+
const pairs = [];
|
|
183
|
+
for (const [id, value] of Object.entries(args.options)) {
|
|
184
|
+
if (!ids.includes(id)) {
|
|
185
|
+
return toolError(`"${slug}" has no option "${id}" (has: ${ids.join(', ') || 'none'}).`);
|
|
186
|
+
}
|
|
187
|
+
if (!['string', 'number', 'boolean'].includes(typeof value) || /[;\n]/.test(String(value))) {
|
|
188
|
+
return toolError(`Option "${id}" must be a single string, number, or boolean value ` +
|
|
189
|
+
'(no ";" or line breaks).');
|
|
190
|
+
}
|
|
191
|
+
pairs.push(`${id}: ${String(value)}`);
|
|
192
|
+
}
|
|
193
|
+
if (pairs.length > 0)
|
|
194
|
+
options = pairs.join('; ');
|
|
195
|
+
}
|
|
93
196
|
const root = tabbiedRoot();
|
|
94
197
|
if (!root) {
|
|
95
198
|
return toolError('The `tabbied` package is not installed next to this server, so the ' +
|
|
96
199
|
'renderer is unavailable. Run `npm install tabbied`.');
|
|
97
200
|
}
|
|
98
|
-
const inline = typeof args.out !== 'string' || args.out.length === 0;
|
|
99
201
|
// An inline render gets a scratch directory, removed once its bytes
|
|
100
|
-
// have been read back.
|
|
202
|
+
// have been read back, or once the render has failed.
|
|
101
203
|
const scratch = inline ? await mkdtemp(path.join(tmpdir(), 'tabbied-')) : null;
|
|
102
|
-
const outPath = scratch ? path.join(scratch, `${slug}.${format}`) :
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
204
|
+
const outPath = scratch ? path.join(scratch, `${slug}.${format}`) : out;
|
|
205
|
+
let keepScratch = false;
|
|
206
|
+
// A value that starts with "-" (a seed of "--browser", say) would read
|
|
207
|
+
// as the next flag, so it goes as --flag=value, which the CLI takes
|
|
208
|
+
// from tabbied 0.7.1. Everything else stays two arguments, which every
|
|
209
|
+
// version of the CLI reads.
|
|
210
|
+
const flag = (name, value) => value.startsWith('-') ? [`--${name}=${value}`] : [`--${name}`, value];
|
|
109
211
|
const argv = [
|
|
110
212
|
path.join(root, 'dist', 'cli.js'),
|
|
111
213
|
'render',
|
|
112
214
|
slug,
|
|
113
|
-
'
|
|
114
|
-
|
|
115
|
-
'
|
|
116
|
-
|
|
117
|
-
'--scale',
|
|
118
|
-
String(scale),
|
|
215
|
+
...flag('out', outPath),
|
|
216
|
+
...flag('format', format),
|
|
217
|
+
...flag('scale', String(scale)),
|
|
218
|
+
...flag('size', `${width}x${height}`),
|
|
119
219
|
];
|
|
120
|
-
const push = (flag, value) => argv.push(flag, value);
|
|
121
220
|
if (typeof args.seed === 'string' && args.seed)
|
|
122
|
-
push('
|
|
221
|
+
argv.push(...flag('seed', args.seed));
|
|
123
222
|
if (typeof args.fit === 'string')
|
|
124
|
-
push('
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
push('--palette', args.palette.filter(Boolean).join(','));
|
|
130
|
-
}
|
|
131
|
-
if (args.options && typeof args.options === 'object') {
|
|
132
|
-
const pairs = Object.entries(args.options)
|
|
133
|
-
.map(([id, value]) => `${id}: ${String(value)}`)
|
|
134
|
-
.join('; ');
|
|
135
|
-
if (pairs)
|
|
136
|
-
push('--options', pairs);
|
|
223
|
+
argv.push(...flag('fit', args.fit));
|
|
224
|
+
if (Array.isArray(args.palette)) {
|
|
225
|
+
const colors = args.palette.filter((color) => typeof color === 'string' && color.trim().length > 0);
|
|
226
|
+
if (colors.length > 0)
|
|
227
|
+
argv.push(...flag('palette', colors.join(',')));
|
|
137
228
|
}
|
|
138
|
-
|
|
229
|
+
if (options)
|
|
230
|
+
argv.push(...flag('options', options));
|
|
139
231
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
232
|
+
let stderr = '';
|
|
233
|
+
try {
|
|
234
|
+
// No shell: argv goes straight to node, so a palette or option value
|
|
235
|
+
// can't be read as shell syntax.
|
|
236
|
+
({ stderr } = await run(process.execPath, argv, {
|
|
237
|
+
timeout: RENDER_TIMEOUT_MS,
|
|
238
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
const failure = error;
|
|
243
|
+
if (failure.killed) {
|
|
244
|
+
return toolError(`Rendering timed out after ${RENDER_TIMEOUT_MS / 1000}s. Try a smaller ` +
|
|
245
|
+
'width/height or scale.');
|
|
246
|
+
}
|
|
247
|
+
const detail = typeof failure.stderr === 'string' && failure.stderr.trim()
|
|
248
|
+
? cliMessage(failure.stderr)
|
|
249
|
+
: (failure.message ?? String(error));
|
|
250
|
+
return toolError(`Rendering failed:\n${detail}`);
|
|
251
|
+
}
|
|
252
|
+
// The CLI reports SVG-fidelity caveats on stderr; they matter enough to
|
|
253
|
+
// pass through rather than swallow.
|
|
254
|
+
const notes = stderr
|
|
255
|
+
.split('\n')
|
|
256
|
+
.filter((line) => line.trim().startsWith('note:'))
|
|
257
|
+
.join('\n');
|
|
258
|
+
if (!inline) {
|
|
259
|
+
const { size } = await stat(outPath);
|
|
260
|
+
return {
|
|
261
|
+
content: [
|
|
262
|
+
text(`Rendered ${slug} to ${outPath} (${width}x${height}` +
|
|
263
|
+
(format === 'png' ? ` @${scale}x` : '') +
|
|
264
|
+
`, ${Math.round(size / 1024)} KB).` +
|
|
265
|
+
(notes ? `\n${notes}` : '')),
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (format === 'svg') {
|
|
270
|
+
const svg = await readFile(outPath, 'utf-8');
|
|
271
|
+
return {
|
|
272
|
+
content: [
|
|
273
|
+
text(`Rendered ${slug} as SVG (${width}x${height}).` +
|
|
274
|
+
(notes ? `\n${notes}` : '')),
|
|
275
|
+
text(svg),
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const png = await readFile(outPath);
|
|
280
|
+
const data = png.toString('base64');
|
|
281
|
+
if (data.length > INLINE_BYTE_BUDGET) {
|
|
282
|
+
// Kept: the answer tells the agent to read it from that path.
|
|
283
|
+
keepScratch = true;
|
|
284
|
+
return {
|
|
285
|
+
content: [
|
|
286
|
+
text(`Rendered ${slug} to ${outPath} - ${Math.round(png.byteLength / 1024)} KB, too large to return inline. Read it from that path, or ` +
|
|
287
|
+
're-render smaller (lower "scale", or a smaller width/height).'),
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
178
291
|
return {
|
|
179
292
|
content: [
|
|
180
|
-
text(`Rendered ${slug}
|
|
293
|
+
text(`Rendered ${slug} (${width}x${height} @${scale}x).` +
|
|
181
294
|
(notes ? `\n${notes}` : '')),
|
|
182
|
-
|
|
295
|
+
{ type: 'image', data, mimeType: 'image/png' },
|
|
183
296
|
],
|
|
184
297
|
};
|
|
185
298
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// Kept: the answer tells the agent to read it from that path.
|
|
190
|
-
return {
|
|
191
|
-
content: [
|
|
192
|
-
text(`Rendered ${slug} to ${outPath} - ${Math.round(png.byteLength / 1024)} KB, too large to return inline. Read it from that path, or ` +
|
|
193
|
-
're-render smaller (lower "scale", or a smaller width/height).'),
|
|
194
|
-
],
|
|
195
|
-
};
|
|
299
|
+
finally {
|
|
300
|
+
if (scratch && !keepScratch)
|
|
301
|
+
await rm(scratch, { recursive: true, force: true });
|
|
196
302
|
}
|
|
197
|
-
await discard();
|
|
198
|
-
return {
|
|
199
|
-
content: [
|
|
200
|
-
text(`Rendered ${slug} (${width}x${height} @${scale}x).` +
|
|
201
|
-
(notes ? `\n${notes}` : '')),
|
|
202
|
-
{ type: 'image', data, mimeType: 'image/png' },
|
|
203
|
-
],
|
|
204
|
-
};
|
|
205
303
|
},
|
|
206
304
|
};
|
|
207
305
|
}
|
package/dist/node/render.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/node/render.ts"],"names":[],"mappings":"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","sourcesContent":["// `render_design`, the one tool that only exists over stdio.\r\n//\r\n// It shells out to the `tabbied` CLI: the only faithful renderer for a\r\n// css-doodle pattern is css-doodle in a real browser (see docs/svg-export.md),\r\n// and the CLI already owns the headless browser, the SVG converter, and the\r\n// option parsing. A Worker has no browser, so it doesn't offer the tool.\r\nimport { execFile } from 'node:child_process';\r\nimport { mkdtemp, readFile, rm, stat } from 'node:fs/promises';\r\nimport { tmpdir } from 'node:os';\r\nimport path from 'node:path';\r\nimport { promisify } from 'node:util';\r\n\r\nimport type { Catalog, Tool, ToolResult } from '../types.js';\r\nimport { tabbiedRoot } from './resources.js';\r\n\r\nconst run = promisify(execFile);\r\n\r\n// Rendering launches a browser and waits for the first arrangement to settle.\r\nconst RENDER_TIMEOUT_MS = 180_000;\r\n\r\n// A PNG returned inline is spent context. Past this the file stays on disk and\r\n// the agent gets the path - better a usable pointer than a truncated image.\r\nconst INLINE_BYTE_BUDGET = 1_500_000;\r\n\r\nconst text = (value: string) => ({ type: 'text' as const, text: value });\r\n\r\nconst toolError = (message: string): ToolResult => ({\r\n content: [text(message)],\r\n isError: true,\r\n});\r\n\r\nexport function renderTool(catalog: Catalog): Tool {\r\n return {\r\n definition: {\r\n name: 'render_design',\r\n title: 'Render a design to a file',\r\n description:\r\n 'Render a design to SVG or PNG with a real browser, at any size, seed, ' +\r\n 'palette, and option set. Use this to produce an actual asset - or to ' +\r\n 'preview a *customized* configuration, which preview_design cannot show ' +\r\n 'you (it only has the stock palette and defaults). Needs Playwright ' +\r\n 'installed alongside this server. Without \"out\" the image comes back ' +\r\n 'inline; with it, the file is written where you ask.',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n slug: { type: 'string', description: 'The design to render.' },\r\n format: {\r\n type: 'string',\r\n enum: ['svg', 'png'],\r\n description:\r\n 'Vector or raster. A few designs cannot be vectorized - check ' +\r\n 'svgExport on the design first.',\r\n },\r\n out: {\r\n type: 'string',\r\n description:\r\n 'Absolute path to write to. Omit to get the image back inline ' +\r\n 'instead of on disk.',\r\n },\r\n seed: {\r\n type: 'string',\r\n description:\r\n 'Fixed seed - the same seed always gives the same image. Random ' +\r\n 'when omitted.',\r\n },\r\n palette: {\r\n type: 'array',\r\n items: { type: 'string' },\r\n description:\r\n \"CSS colors, background first. Omit for the design's authored palette.\",\r\n },\r\n options: {\r\n type: 'object',\r\n description:\r\n 'Option values keyed by option id, e.g. {\"scale\": 3}. Read the ' +\r\n 'ids and ranges off get_design.',\r\n },\r\n width: { type: 'integer', description: 'Pixels wide (default 960).' },\r\n height: { type: 'integer', description: 'Pixels tall (default 960).' },\r\n fit: {\r\n type: 'string',\r\n enum: ['grid', 'cover', 'fixed'],\r\n description: 'Sizing mode (default grid).',\r\n },\r\n scale: {\r\n type: 'integer',\r\n description:\r\n 'PNG device-scale factor. Defaults to 2 when writing a file and ' +\r\n '1 when returning inline, to keep the response small.',\r\n },\r\n },\r\n required: ['slug', 'format'],\r\n additionalProperties: false,\r\n },\r\n },\r\n\r\n async run(args): Promise<ToolResult> {\r\n const slug = typeof args.slug === 'string' ? args.slug : '';\r\n const design = catalog.designs.find((entry) => entry.slug === slug);\r\n if (!design) {\r\n return toolError(\r\n `No design with slug \"${slug}\". Use search_designs to find one.`\r\n );\r\n }\r\n\r\n const format = args.format === 'png' ? 'png' : 'svg';\r\n if (format === 'svg' && !design.svgExport.supported) {\r\n return toolError(\r\n `\"${slug}\" paints effects SVG cannot represent, so it has no vector ` +\r\n 'export. Render it as PNG instead.'\r\n );\r\n }\r\n\r\n const root = tabbiedRoot();\r\n if (!root) {\r\n return toolError(\r\n 'The `tabbied` package is not installed next to this server, so the ' +\r\n 'renderer is unavailable. Run `npm install tabbied`.'\r\n );\r\n }\r\n\r\n const inline = typeof args.out !== 'string' || args.out.length === 0;\r\n // An inline render gets a scratch directory, removed once its bytes\r\n // have been read back.\r\n const scratch = inline ? await mkdtemp(path.join(tmpdir(), 'tabbied-')) : null;\r\n const outPath = scratch ? path.join(scratch, `${slug}.${format}`) : (args.out as string);\r\n const discard = () => (scratch ? rm(scratch, { recursive: true, force: true }) : Promise.resolve());\r\n\r\n const scale =\r\n typeof args.scale === 'number' && Number.isFinite(args.scale)\r\n ? Math.trunc(args.scale)\r\n : inline\r\n ? 1\r\n : 2;\r\n\r\n const argv = [\r\n path.join(root, 'dist', 'cli.js'),\r\n 'render',\r\n slug,\r\n '--out',\r\n outPath,\r\n '--format',\r\n format,\r\n '--scale',\r\n String(scale),\r\n ];\r\n\r\n const push = (flag: string, value: string) => argv.push(flag, value);\r\n\r\n if (typeof args.seed === 'string' && args.seed) push('--seed', args.seed);\r\n if (typeof args.fit === 'string') push('--fit', args.fit);\r\n\r\n const width =\r\n typeof args.width === 'number' ? Math.trunc(args.width) : 960;\r\n const height =\r\n typeof args.height === 'number' ? Math.trunc(args.height) : 960;\r\n push('--size', `${width}x${height}`);\r\n\r\n if (Array.isArray(args.palette) && args.palette.length) {\r\n push('--palette', args.palette.filter(Boolean).join(','));\r\n }\r\n\r\n if (args.options && typeof args.options === 'object') {\r\n const pairs = Object.entries(args.options as Record<string, unknown>)\r\n .map(([id, value]) => `${id}: ${String(value)}`)\r\n .join('; ');\r\n if (pairs) push('--options', pairs);\r\n }\r\n\r\n let stderr = '';\r\n try {\r\n // No shell: argv goes straight to node, so a palette or option value\r\n // can't be read as shell syntax.\r\n ({ stderr } = await run(process.execPath, argv, {\r\n timeout: RENDER_TIMEOUT_MS,\r\n maxBuffer: 8 * 1024 * 1024,\r\n }));\r\n } catch (error) {\r\n const detail =\r\n error && typeof error === 'object' && 'stderr' in error\r\n ? String((error as { stderr: unknown }).stderr).trim()\r\n : error instanceof Error\r\n ? error.message\r\n : String(error);\r\n return toolError(\r\n `Rendering failed: ${detail || 'the CLI exited non-zero.'}\\n\\n` +\r\n 'If this says a browser is missing, install one alongside the ' +\r\n 'server (`npm i -D playwright`) or point TABBIED_CHROMIUM at a ' +\r\n 'Chromium binary.'\r\n );\r\n }\r\n\r\n // The CLI reports SVG-fidelity caveats on stderr; they matter enough to\r\n // pass through rather than swallow.\r\n const notes = stderr\r\n .split('\\n')\r\n .filter((line) => line.trim().startsWith('note:'))\r\n .join('\\n');\r\n\r\n if (!inline) {\r\n const { size } = await stat(outPath);\r\n return {\r\n content: [\r\n text(\r\n `Rendered ${slug} to ${outPath} (${width}x${height}` +\r\n (format === 'png' ? ` @${scale}x` : '') +\r\n `, ${Math.round(size / 1024)} KB).` +\r\n (notes ? `\\n${notes}` : '')\r\n ),\r\n ],\r\n };\r\n }\r\n\r\n if (format === 'svg') {\r\n const svg = await readFile(outPath, 'utf-8');\r\n await discard();\r\n return {\r\n content: [\r\n text(\r\n `Rendered ${slug} as SVG (${width}x${height}).` +\r\n (notes ? `\\n${notes}` : '')\r\n ),\r\n text(svg),\r\n ],\r\n };\r\n }\r\n\r\n const png = await readFile(outPath);\r\n const data = png.toString('base64');\r\n if (data.length > INLINE_BYTE_BUDGET) {\r\n // Kept: the answer tells the agent to read it from that path.\r\n return {\r\n content: [\r\n text(\r\n `Rendered ${slug} to ${outPath} - ${Math.round(\r\n png.byteLength / 1024\r\n )} KB, too large to return inline. Read it from that path, or ` +\r\n 're-render smaller (lower \"scale\", or a smaller width/height).'\r\n ),\r\n ],\r\n };\r\n }\r\n\r\n await discard();\r\n return {\r\n content: [\r\n text(\r\n `Rendered ${slug} (${width}x${height} @${scale}x).` +\r\n (notes ? `\\n${notes}` : '')\r\n ),\r\n { type: 'image', data, mimeType: 'image/png' },\r\n ],\r\n };\r\n },\r\n };\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/node/render.ts"],"names":[],"mappings":"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","sourcesContent":["// `render_design`, the one tool that only exists over stdio.\n//\n// It shells out to the `tabbied` CLI: the only faithful renderer for a\n// css-doodle pattern is css-doodle in a real browser (see docs/svg-export.md),\n// and the CLI already owns the headless browser, the SVG converter, and the\n// option parsing. A Worker has no browser, so it doesn't offer the tool.\nimport { execFile } from 'node:child_process';\nimport { mkdtemp, readFile, rm, stat } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport type { Catalog, Tool, ToolResult } from '../types.js';\nimport { tabbiedRoot } from './resources.js';\n\nconst run = promisify(execFile);\n\n// Rendering launches a browser and waits for the first arrangement to settle.\nconst RENDER_TIMEOUT_MS = 180_000;\n\n// A PNG returned inline is spent context. Past this the file stays on disk and\n// the agent gets the path - better a usable pointer than a truncated image.\nconst INLINE_BYTE_BUDGET = 1_500_000;\n\n// Past these a PNG is tens of megapixels (or a browser canvas limit), which\n// no asset this tool exists for needs.\nconst SIZE_MAX = 4096;\nconst SCALE_MAX = 4;\n\nconst text = (value: string) => ({ type: 'text' as const, text: value });\n\nconst toolError = (message: string): ToolResult => ({\n content: [text(message)],\n isError: true,\n});\n\n/** A whole number in [min, max], the default when absent, or null when invalid. */\nfunction integerArg(value: unknown, fallback: number, min: number, max: number): number | null {\n if (value === undefined) return fallback;\n return typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max\n ? value\n : null;\n}\n\n/**\n * The CLI's own message out of its stderr: each `tabbied:` line and the\n * indented lines that continue it. Anything else there (Node's warnings, a\n * deprecation notice from a dependency) is noise to the agent. When there is\n * no such line, the CLI died some other way, and the tail is the best guess.\n */\nfunction cliMessage(stderr: string): string {\n const lines = stderr.split('\\n');\n const kept: string[] = [];\n let continuing = false;\n for (const line of lines) {\n if (line.startsWith('tabbied:')) {\n kept.push(line);\n continuing = true;\n } else if (continuing && /^\\s+\\S/.test(line)) {\n kept.push(line);\n } else {\n continuing = false;\n }\n }\n return kept.length > 0\n ? kept.join('\\n')\n : lines.filter((line) => line.trim()).slice(-5).join('\\n');\n}\n\nexport function renderTool(catalog: Catalog): Tool {\n return {\n definition: {\n name: 'render_design',\n title: 'Render a design to a file',\n description:\n 'Render a design to SVG or PNG with a real browser, at any size, seed, ' +\n 'palette, and option set. Use this to produce an actual asset - or to ' +\n 'preview a *customized* configuration, which preview_design cannot show ' +\n 'you (it only has the stock palette and defaults). Needs Playwright ' +\n 'where this server runs (start it as `npx -y -p tabbied-mcp -p ' +\n 'playwright tabbied-mcp`, with `npx playwright install chromium` once). ' +\n 'Without \"out\" the image comes back inline; with it, the file is ' +\n 'written where you ask, replacing any file already there.',\n inputSchema: {\n type: 'object',\n properties: {\n slug: { type: 'string', minLength: 1, description: 'The design to render.' },\n format: {\n type: 'string',\n enum: ['svg', 'png'],\n description:\n 'Vector or raster. A few designs cannot be vectorized - check ' +\n 'svgExport on the design first.',\n },\n out: {\n type: 'string',\n minLength: 1,\n description:\n 'Absolute path to write to, ending in .svg or .png to match ' +\n '\"format\". Omit to get the image back inline instead of on disk.',\n },\n seed: {\n type: 'string',\n minLength: 1,\n description:\n 'Fixed seed - the same seed always gives the same image. Random ' +\n 'when omitted.',\n },\n palette: {\n type: 'array',\n items: { type: 'string', minLength: 1 },\n minItems: 1,\n description:\n \"CSS colors, background first. Omit for the design's authored palette.\",\n },\n options: {\n type: 'object',\n additionalProperties: { type: ['string', 'number', 'boolean'] },\n description:\n 'Option values keyed by option id, e.g. {\"frequency\": 0.6}. Read ' +\n 'the ids, ranges, and choices off get_design.',\n },\n width: {\n type: 'integer',\n minimum: 1,\n maximum: SIZE_MAX,\n description: 'Pixels wide (default 960).',\n },\n height: {\n type: 'integer',\n minimum: 1,\n maximum: SIZE_MAX,\n description: 'Pixels tall (default 960).',\n },\n fit: {\n type: 'string',\n enum: ['grid', 'cover', 'fixed'],\n description: 'Sizing mode (default grid).',\n },\n scale: {\n type: 'integer',\n minimum: 1,\n maximum: SCALE_MAX,\n description:\n 'PNG device-scale factor. Defaults to 2 when writing a file and ' +\n '1 when returning inline, to keep the response small.',\n },\n },\n required: ['slug', 'format'],\n additionalProperties: false,\n },\n // It writes a file, so it is not read-only; it only ever adds or\n // replaces the one file it was asked for.\n annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },\n },\n\n async run(args): Promise<ToolResult> {\n const slug = typeof args.slug === 'string' ? args.slug : '';\n const design = catalog.designs.find((entry) => entry.slug === slug);\n if (!design) {\n return toolError(\n `No design with slug \"${slug}\". Use search_designs to find one.`\n );\n }\n\n const format = args.format === 'png' ? 'png' : 'svg';\n if (format === 'svg' && !design.svgExport.supported) {\n return toolError(\n `\"${slug}\" paints effects SVG cannot represent, so it has no vector ` +\n 'export. Render it as PNG instead.'\n );\n }\n\n // The schema states these, but a host that skips validation (or calls\n // run() directly) must not hand the CLI a path it would resolve against\n // whatever directory the server happened to start in.\n const out = typeof args.out === 'string' && args.out.length > 0 ? args.out : null;\n if (out !== null && !path.isAbsolute(out)) {\n return toolError(\n `\"out\" must be an absolute path, got \"${out}\". A relative one would ` +\n \"land in the server's working directory, not yours.\"\n );\n }\n if (out !== null && path.extname(out).toLowerCase() !== `.${format}`) {\n return toolError(\n `\"out\" must end in .${format} to match format \"${format}\", got \"${out}\".`\n );\n }\n\n const inline = out === null;\n const width = integerArg(args.width, 960, 1, SIZE_MAX);\n const height = integerArg(args.height, 960, 1, SIZE_MAX);\n const scale = integerArg(args.scale, inline ? 1 : 2, 1, SCALE_MAX);\n if (width === null || height === null) {\n return toolError(`\"width\" and \"height\" must be whole pixels from 1 to ${SIZE_MAX}.`);\n }\n if (scale === null) {\n return toolError(`\"scale\" must be a whole number from 1 to ${SCALE_MAX}.`);\n }\n\n // Options reach the CLI as one \"id: value; id: value\" string, so a `;`\n // inside a value would end it and start an option of its own, past\n // every check on the first. Ranges and choices are the CLI's to check.\n let options: string | null = null;\n if (args.options !== undefined) {\n if (!args.options || typeof args.options !== 'object' || Array.isArray(args.options)) {\n return toolError('\"options\" must be an object of option values keyed by id.');\n }\n const ids = design.options.map((option) => option.id);\n const pairs: string[] = [];\n for (const [id, value] of Object.entries(args.options as Record<string, unknown>)) {\n if (!ids.includes(id)) {\n return toolError(\n `\"${slug}\" has no option \"${id}\" (has: ${ids.join(', ') || 'none'}).`\n );\n }\n if (!['string', 'number', 'boolean'].includes(typeof value) || /[;\\n]/.test(String(value))) {\n return toolError(\n `Option \"${id}\" must be a single string, number, or boolean value ` +\n '(no \";\" or line breaks).'\n );\n }\n pairs.push(`${id}: ${String(value)}`);\n }\n if (pairs.length > 0) options = pairs.join('; ');\n }\n\n const root = tabbiedRoot();\n if (!root) {\n return toolError(\n 'The `tabbied` package is not installed next to this server, so the ' +\n 'renderer is unavailable. Run `npm install tabbied`.'\n );\n }\n\n // An inline render gets a scratch directory, removed once its bytes\n // have been read back, or once the render has failed.\n const scratch = inline ? await mkdtemp(path.join(tmpdir(), 'tabbied-')) : null;\n const outPath = scratch ? path.join(scratch, `${slug}.${format}`) : (out as string);\n let keepScratch = false;\n\n // A value that starts with \"-\" (a seed of \"--browser\", say) would read\n // as the next flag, so it goes as --flag=value, which the CLI takes\n // from tabbied 0.7.1. Everything else stays two arguments, which every\n // version of the CLI reads.\n const flag = (name: string, value: string) =>\n value.startsWith('-') ? [`--${name}=${value}`] : [`--${name}`, value];\n const argv = [\n path.join(root, 'dist', 'cli.js'),\n 'render',\n slug,\n ...flag('out', outPath),\n ...flag('format', format),\n ...flag('scale', String(scale)),\n ...flag('size', `${width}x${height}`),\n ];\n if (typeof args.seed === 'string' && args.seed) argv.push(...flag('seed', args.seed));\n if (typeof args.fit === 'string') argv.push(...flag('fit', args.fit));\n if (Array.isArray(args.palette)) {\n const colors = args.palette.filter(\n (color): color is string => typeof color === 'string' && color.trim().length > 0\n );\n if (colors.length > 0) argv.push(...flag('palette', colors.join(',')));\n }\n if (options) argv.push(...flag('options', options));\n\n try {\n let stderr = '';\n try {\n // No shell: argv goes straight to node, so a palette or option value\n // can't be read as shell syntax.\n ({ stderr } = await run(process.execPath, argv, {\n timeout: RENDER_TIMEOUT_MS,\n maxBuffer: 8 * 1024 * 1024,\n }));\n } catch (error) {\n const failure = error as { killed?: boolean; stderr?: unknown; message?: string };\n if (failure.killed) {\n return toolError(\n `Rendering timed out after ${RENDER_TIMEOUT_MS / 1000}s. Try a smaller ` +\n 'width/height or scale.'\n );\n }\n const detail =\n typeof failure.stderr === 'string' && failure.stderr.trim()\n ? cliMessage(failure.stderr)\n : (failure.message ?? String(error));\n return toolError(`Rendering failed:\\n${detail}`);\n }\n\n // The CLI reports SVG-fidelity caveats on stderr; they matter enough to\n // pass through rather than swallow.\n const notes = stderr\n .split('\\n')\n .filter((line) => line.trim().startsWith('note:'))\n .join('\\n');\n\n if (!inline) {\n const { size } = await stat(outPath);\n return {\n content: [\n text(\n `Rendered ${slug} to ${outPath} (${width}x${height}` +\n (format === 'png' ? ` @${scale}x` : '') +\n `, ${Math.round(size / 1024)} KB).` +\n (notes ? `\\n${notes}` : '')\n ),\n ],\n };\n }\n\n if (format === 'svg') {\n const svg = await readFile(outPath, 'utf-8');\n return {\n content: [\n text(\n `Rendered ${slug} as SVG (${width}x${height}).` +\n (notes ? `\\n${notes}` : '')\n ),\n text(svg),\n ],\n };\n }\n\n const png = await readFile(outPath);\n const data = png.toString('base64');\n if (data.length > INLINE_BYTE_BUDGET) {\n // Kept: the answer tells the agent to read it from that path.\n keepScratch = true;\n return {\n content: [\n text(\n `Rendered ${slug} to ${outPath} - ${Math.round(\n png.byteLength / 1024\n )} KB, too large to return inline. Read it from that path, or ` +\n 're-render smaller (lower \"scale\", or a smaller width/height).'\n ),\n ],\n };\n }\n\n return {\n content: [\n text(\n `Rendered ${slug} (${width}x${height} @${scale}x).` +\n (notes ? `\\n${notes}` : '')\n ),\n { type: 'image', data, mimeType: 'image/png' },\n ],\n };\n } finally {\n if (scratch && !keepScratch) await rm(scratch, { recursive: true, force: true });\n }\n },\n };\n}\n"]}
|
package/dist/node/resources.js
CHANGED
|
@@ -9,6 +9,34 @@ import { readFile } from 'node:fs/promises';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
const SITE = 'https://tabbied.com';
|
|
12
|
+
// Every read here is a small static file. Without a limit, a network that
|
|
13
|
+
// swallows packets would hold a tool call (or the server's startup) open for
|
|
14
|
+
// as long as the client is willing to wait.
|
|
15
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
16
|
+
/**
|
|
17
|
+
* GET a URL, failing with a message that names it. Node's own failure is a
|
|
18
|
+
* bare "fetch failed" (the reason is in `cause`), which tells an agent nothing
|
|
19
|
+
* about what was unreachable.
|
|
20
|
+
*/
|
|
21
|
+
async function get(url) {
|
|
22
|
+
let response;
|
|
23
|
+
try {
|
|
24
|
+
response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const reason = error instanceof Error && error.name === 'TimeoutError'
|
|
28
|
+
? `no answer within ${FETCH_TIMEOUT_MS / 1000}s`
|
|
29
|
+
: error instanceof Error
|
|
30
|
+
? [error.message, error.cause?.message]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join(': ')
|
|
33
|
+
: String(error);
|
|
34
|
+
throw new Error(`${url} could not be reached (${reason})`);
|
|
35
|
+
}
|
|
36
|
+
if (!response.ok)
|
|
37
|
+
throw new Error(`${url} returned ${response.status}`);
|
|
38
|
+
return response;
|
|
39
|
+
}
|
|
12
40
|
/** Root of the installed `tabbied` package, or null when it isn't resolvable. */
|
|
13
41
|
export function tabbiedRoot() {
|
|
14
42
|
try {
|
|
@@ -33,22 +61,20 @@ export async function loadCatalog() {
|
|
|
33
61
|
`falling back to ${SITE}/catalog.json\n`);
|
|
34
62
|
}
|
|
35
63
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
64
|
+
try {
|
|
65
|
+
return (await (await get(`${SITE}/catalog.json`)).json());
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`no local catalog, and ${error instanceof Error ? error.message : String(error)}. ` +
|
|
39
69
|
'Install the `tabbied` package (npm i tabbied) or restore network access.');
|
|
40
70
|
}
|
|
41
|
-
return (await response.json());
|
|
42
71
|
}
|
|
43
72
|
/**
|
|
44
73
|
* Preview bytes for one design: the committed @2x renders the site serves, so
|
|
45
74
|
* the agent sees the same image a human browsing the gallery does.
|
|
46
75
|
*/
|
|
47
76
|
export async function fetchPreview(design) {
|
|
48
|
-
const response = await
|
|
49
|
-
if (!response.ok) {
|
|
50
|
-
throw new Error(`${design.preview} returned ${response.status}`);
|
|
51
|
-
}
|
|
77
|
+
const response = await get(design.preview);
|
|
52
78
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
53
79
|
return {
|
|
54
80
|
data: buffer.toString('base64'),
|
|
@@ -69,11 +95,7 @@ export async function fetchDocs() {
|
|
|
69
95
|
// fall through to the network copy
|
|
70
96
|
}
|
|
71
97
|
}
|
|
72
|
-
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
throw new Error(`${SITE}/llms-full.txt returned ${response.status}`);
|
|
75
|
-
}
|
|
76
|
-
return await response.text();
|
|
98
|
+
return await (await get(`${SITE}/llms-full.txt`)).text();
|
|
77
99
|
}
|
|
78
100
|
/**
|
|
79
101
|
* The editable-template index and one site's spec. Network only, deliberately:
|
|
@@ -81,17 +103,9 @@ export async function fetchDocs() {
|
|
|
81
103
|
* (docs/editable-templates.md), and the `tabbied` package does not contain them.
|
|
82
104
|
*/
|
|
83
105
|
export async function fetchTemplateCatalog() {
|
|
84
|
-
|
|
85
|
-
if (!response.ok) {
|
|
86
|
-
throw new Error(`${SITE}/editable-catalog.json returned ${response.status}`);
|
|
87
|
-
}
|
|
88
|
-
return (await response.json());
|
|
106
|
+
return (await (await get(`${SITE}/editable-catalog.json`)).json());
|
|
89
107
|
}
|
|
90
108
|
export async function fetchTemplate(slug) {
|
|
91
|
-
|
|
92
|
-
if (!response.ok) {
|
|
93
|
-
throw new Error(`${SITE}/editable/${slug}.json returned ${response.status}`);
|
|
94
|
-
}
|
|
95
|
-
return (await response.json());
|
|
109
|
+
return (await (await get(`${SITE}/editable/${encodeURIComponent(slug)}.json`)).json());
|
|
96
110
|
}
|
|
97
111
|
//# sourceMappingURL=resources.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../../src/node/resources.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,EAAE;AACF,gFAAgF;AAChF,6EAA6E;AAC7E,wEAAwE;AACxE,yDAAyD;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAS7B,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,IAAI,GAAG,qBAAqB,CAAC;AAEnC,iFAAiF;AACjF,MAAM,UAAU,WAAW;IACzB,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;IAE3B,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CACf,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAC9C,CAAC;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,yEAAyE;YACzE,uEAAuE;YACvE,qDAAqD;YACrD,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACtE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,kDAAkD,MAAM,KAAK;gBAC3D,mBAAmB,IAAI,iBAAiB,CAC3C,CAAC;QACJ,CAAC;IACH,CAAC;IAED,
|
|
1
|
+
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../../src/node/resources.ts"],"names":[],"mappings":"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","sourcesContent":["// Where the stdio server gets its catalog, previews, and docs.\n//\n// The catalog is read from the installed `tabbied` package rather than fetched,\n// so the tools describe exactly the version the caller will `npm install`: a\n// design that only exists on the site would give an import that doesn't\n// resolve. The network is the fallback, not the default.\nimport { createRequire } from 'node:module';\nimport { readFile } from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type {\n Catalog,\n CatalogDesign,\n TemplateCatalog,\n TemplateSpec,\n} from '../types.js';\n\nconst require = createRequire(import.meta.url);\n\nconst SITE = 'https://tabbied.com';\n\n// Every read here is a small static file. Without a limit, a network that\n// swallows packets would hold a tool call (or the server's startup) open for\n// as long as the client is willing to wait.\nconst FETCH_TIMEOUT_MS = 10_000;\n\n/**\n * GET a URL, failing with a message that names it. Node's own failure is a\n * bare \"fetch failed\" (the reason is in `cause`), which tells an agent nothing\n * about what was unreachable.\n */\nasync function get(url: string): Promise<Response> {\n let response: Response;\n try {\n response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });\n } catch (error) {\n const reason =\n error instanceof Error && error.name === 'TimeoutError'\n ? `no answer within ${FETCH_TIMEOUT_MS / 1000}s`\n : error instanceof Error\n ? [error.message, (error.cause as Error | undefined)?.message]\n .filter(Boolean)\n .join(': ')\n : String(error);\n throw new Error(`${url} could not be reached (${reason})`);\n }\n if (!response.ok) throw new Error(`${url} returned ${response.status}`);\n return response;\n}\n\n/** Root of the installed `tabbied` package, or null when it isn't resolvable. */\nexport function tabbiedRoot(): string | null {\n try {\n return path.dirname(require.resolve('tabbied/package.json'));\n } catch {\n return null;\n }\n}\n\nexport async function loadCatalog(): Promise<Catalog> {\n const root = tabbiedRoot();\n\n if (root) {\n try {\n return JSON.parse(\n await readFile(path.join(root, 'catalog.json'), 'utf-8')\n ) as Catalog;\n } catch (error) {\n // A resolvable package with no catalog.json means a source checkout that\n // hasn't been built. Say so - the network copy would silently disagree\n // with the local patterns the user is about to edit.\n const reason = error instanceof Error ? error.message : String(error);\n process.stderr.write(\n `tabbied-mcp: could not read the local catalog (${reason}); ` +\n `falling back to ${SITE}/catalog.json\\n`\n );\n }\n }\n\n try {\n return (await (await get(`${SITE}/catalog.json`)).json()) as Catalog;\n } catch (error) {\n throw new Error(\n `no local catalog, and ${error instanceof Error ? error.message : String(error)}. ` +\n 'Install the `tabbied` package (npm i tabbied) or restore network access.'\n );\n }\n}\n\n/**\n * Preview bytes for one design: the committed @2x renders the site serves, so\n * the agent sees the same image a human browsing the gallery does.\n */\nexport async function fetchPreview(\n design: CatalogDesign\n): Promise<{ data: string; mimeType: string }> {\n const response = await get(design.preview);\n const buffer = Buffer.from(await response.arrayBuffer());\n return {\n data: buffer.toString('base64'),\n mimeType: response.headers.get('Content-Type') ?? 'image/webp',\n };\n}\n\n/**\n * The full reference. The package's own llms.txt is the *full* text (in\n * node_modules depth beats brevity), so this works offline too.\n */\nexport async function fetchDocs(): Promise<string> {\n const root = tabbiedRoot();\n if (root) {\n try {\n return await readFile(path.join(root, 'llms.txt'), 'utf-8');\n } catch {\n // fall through to the network copy\n }\n }\n\n return await (await get(`${SITE}/llms-full.txt`)).text();\n}\n\n/**\n * The editable-template index and one site's spec. Network only, deliberately:\n * they are *site* artifacts generated from the static export\n * (docs/editable-templates.md), and the `tabbied` package does not contain them.\n */\nexport async function fetchTemplateCatalog(): Promise<TemplateCatalog> {\n return (await (await get(`${SITE}/editable-catalog.json`)).json()) as TemplateCatalog;\n}\n\nexport async function fetchTemplate(slug: string): Promise<TemplateSpec> {\n return (await (await get(`${SITE}/editable/${encodeURIComponent(slug)}.json`)).json()) as TemplateSpec;\n}\n"]}
|