This commit is contained in:
seb
2026-10-12 14:18:54 +02:00
parent 88ecf5bf5c
commit 06dc4629da
11 changed files with 56 additions and 12 deletions

View File

@@ -197,6 +197,32 @@ function runInWorker({ code, timeoutMs, vfsRoot, googleAuthId, appletInput, appl
});
}
/**
* Parse the applet description header.
*
* Accepts either a quoted string (e.g. `"Lorem ipsum"` or `'Lorem ipsum'`) or bare text.
* Strips surrounding quotes if present and trims whitespace.
*
* @param {string} header raw text after the `// @applet-description ` marker
* @returns {string}
*/
function parseAppletDescription(header) {
const text = String(header || '').trim();
if (!text) {
throw new ToolError('Applet description cannot be empty.');
}
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
try {
const parsed = JSON.parse(text);
if (typeof parsed === 'string' && parsed.trim()) return parsed.trim();
} catch {
const unquoted = text.slice(1, -1).trim();
if (unquoted) return unquoted;
}
}
return text;
}
function readApplet(vfsRoot, appletvfsfile) {
if (typeof appletvfsfile !== 'string' || !appletvfsfile.trim()) {
throw new ToolError('"appletvfsfile" must be a non-empty VFS-relative file path.');
@@ -228,14 +254,16 @@ function readApplet(vfsRoot, appletvfsfile) {
throw new ToolError(`Unable to read applet "${appletPath}" from the VFS: ${err.message}`);
}
const lines = source.replace(/^\uFEFF/, '').split(/\r?\n/, 3);
const lines = source.replace(/^\uFEFF/, '').split(/\r?\n/, 4);
const inputHeader = lines[0]?.match(/^\/\/ @applet-input-schema (.+)$/);
const outputHeader = lines[1]?.match(/^\/\/ @applet-output-schema (.+)$/);
if (!inputHeader || !outputHeader) {
const descHeader = lines[2]?.match(/^\/\/ @applet-description (.+)$/);
if (!inputHeader || !outputHeader || !descHeader) {
throw new ToolError(
'Applet metadata must be on the first two lines: ' +
'Applet metadata must be on the first three lines: ' +
'`// @applet-input-schema {JSON Schema}` followed by ' +
'`// @applet-output-schema name:type;name:type` (typed CSV columns).'
'`// @applet-output-schema name:type;name:type` (typed CSV columns) and ' +
'`// @applet-description "description"`.'
);
}
@@ -251,8 +279,9 @@ function readApplet(vfsRoot, appletvfsfile) {
}
const outputColumns = parseTypedCsvColumns(outputHeader[1]);
const description = parseAppletDescription(descHeader[1]);
return { source, outputColumns };
return { source, outputColumns, description, inputSchema };
}
function formatSandboxError(code, report) {
@@ -401,6 +430,7 @@ export function createJavascriptExecuteTool({ googleAuthId }) {
result.result = table.csv;
result.rows = table.rows;
result.columns = table.columns;
result.description = applet.description;
if (table.warnings.length) result.warnings = table.warnings;
}
if (truncated) {

View File

@@ -48,7 +48,7 @@ export const TOOL_DESCRIPTIONS = {
javascript_execute:
'Execute JS in an isolated sandbox via inline `code` (assign to `module.exports`) or `appletvfsfile` ' +
'(.applet.js returning typed CSV per @applet-input-schema and @applet-output-schema; details in applet.SKILL). ' +
'(.applet.js returning typed CSV per @applet-input-schema, @applet-output-schema, and @applet-description; details in applet.SKILL). ' +
'Pre-injected globals: fs/vfs, sharp, ExcelJS, PDFDocument, crypto, and fetch("/path"). No require/import.',
find_files: