@qvac/harness 0.8.0 → 0.8.6
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/CHANGELOG.md +97 -1
- package/README.md +49 -0
- package/dist-lib/index.d.ts +31 -2
- package/dist-lib/index.js +365 -53
- package/dist-lib/lib/audio-transcode-bare.d.ts +1 -1
- package/dist-lib/lib/audio-transcode-bare.js +22 -25
- package/dist-lib/lib/audio-transcode-node.d.ts +1 -1
- package/dist-lib/lib/audio-transcode-node.js +1 -3
- package/dist-lib/lib/capabilities/audio-resample.d.ts +1 -1
- package/dist-lib/lib/capabilities/audio-resample.js +7 -8
- package/dist-lib/lib/capabilities/completion.d.ts +5 -2
- package/dist-lib/lib/capabilities/completion.js +22 -4
- package/dist-lib/lib/capabilities/download.d.ts +3 -1
- package/dist-lib/lib/capabilities/download.js +1 -1
- package/dist-lib/lib/capabilities/preview-model-source.d.ts +1 -0
- package/dist-lib/lib/capabilities/preview-model-source.js +1 -0
- package/dist-lib/lib/capabilities/usage-breakdown.js +4 -1
- package/dist-lib/lib/capabilities/voice.d.ts +5 -1
- package/dist-lib/lib/capabilities/voice.js +94 -26
- package/dist-lib/lib/connect.d.ts +4 -1
- package/dist-lib/lib/connect.js +14 -1
- package/dist-lib/lib/contract-types.d.ts +10 -0
- package/dist-lib/lib/docx/index.d.ts +1 -1
- package/dist-lib/lib/docx/index.js +16 -24
- package/dist-lib/lib/models/download-gate.d.ts +20 -0
- package/dist-lib/lib/models/download-gate.js +73 -0
- package/dist-lib/lib/models/memory-cap.d.ts +13 -0
- package/dist-lib/lib/models/memory-cap.js +21 -0
- package/dist-lib/lib/models/model-catalog-constants.d.ts +3 -1
- package/dist-lib/lib/models/model-catalog-constants.js +9 -1
- package/dist-lib/lib/models/model-catalog.d.ts +6 -2
- package/dist-lib/lib/models/model-catalog.js +35 -3
- package/dist-lib/lib/models/model-handle.d.ts +8 -1
- package/dist-lib/lib/models/model-handle.js +20 -2
- package/dist-lib/lib/models/model-list.d.ts +8 -1
- package/dist-lib/lib/models/model-list.js +23 -0
- package/dist-lib/lib/models/model-pool.d.ts +33 -5
- package/dist-lib/lib/models/model-pool.js +97 -29
- package/dist-lib/lib/office/extract.d.ts +9 -0
- package/dist-lib/lib/office/extract.js +19 -0
- package/dist-lib/lib/office/index.d.ts +1 -1
- package/dist-lib/lib/office/index.js +74 -19
- package/dist-lib/lib/serve.js +18 -2
- package/dist-lib/lib/tools/exec/adapters/python/module-distributions.js +0 -3
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +7 -50
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +38 -695
- package/dist-lib/lib/tools/exec/adapters/python/run-session.js +1 -85
- package/dist-lib/lib/tools/exec.js +9 -20
- package/dist-lib/lib/tools/generate-image.d.ts +4 -2
- package/dist-lib/lib/tools/generate-image.js +5 -2
- package/dist-lib/lib/tools/generate-music.d.ts +4 -2
- package/dist-lib/lib/tools/generate-music.js +5 -2
- package/dist-lib/lib/tools/office/ops.d.ts +13 -0
- package/dist-lib/lib/tools/office/ops.js +100 -0
- package/dist-lib/lib/tools/office.d.ts +12 -0
- package/dist-lib/lib/tools/office.js +486 -0
- package/dist-lib/lib/tools/translate.d.ts +3 -1
- package/dist-lib/lib/tools/translate.js +6 -1
- package/dist-lib/schema/harness.js +41 -3
- package/dist-lib/spec/bind.js +9 -0
- package/dist-lib/spec/calls.js +3 -0
- package/dist-lib/spec/hrpc/hrpc.json +39 -0
- package/dist-lib/spec/hrpc/index.js +39 -3
- package/dist-lib/spec/hrpc/messages.js +93 -10
- package/dist-lib/spec/hyperschema/index.js +93 -10
- package/dist-lib/spec/hyperschema/schema.json +57 -1
- package/dist-lib/spec/hyperschema/types.d.ts +19 -2
- package/package.json +13 -9
- package/wheels/manifest.json +1 -160
- package/worker/harness-sidecar.mjs +3 -1
- package/dist-lib/lib/office/pptx.d.ts +0 -1
- package/dist-lib/lib/office/pptx.js +0 -26
- package/dist-lib/lib/office/python.d.ts +0 -8
- package/dist-lib/lib/office/python.js +0 -32
- package/dist-lib/lib/office/xlsx.d.ts +0 -1
- package/dist-lib/lib/office/xlsx.js +0 -26
- package/dist-lib/lib/tools/exec/adapters/python/create-effect.d.ts +0 -7
- package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +0 -318
- package/wheels/cffi-2.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/cryptography-47.0.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/defusedxml-0.7.1-py2.py3-none-any.whl +0 -0
- package/wheels/et_xmlfile-2.0.0-py3-none-any.whl +0 -0
- package/wheels/fonttools-4.62.1-py3-none-any.whl +0 -0
- package/wheels/fpdf2-2.8.8-py3-none-any.whl +0 -0
- package/wheels/lxml-6.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/openpyxl-3.1.5-py2.py3-none-any.whl +0 -0
- package/wheels/pillow-12.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/pycparser-3.0-py3-none-any.whl +0 -0
- package/wheels/pypdf-6.15.0-py3-none-any.whl +0 -0
- package/wheels/python_docx-1.2.0-py3-none-any.whl +0 -0
- package/wheels/python_pptx-1.0.2-py3-none-any.whl +0 -0
- package/wheels/six-1.17.0-py2.py3-none-any.whl +0 -0
- package/wheels/typing_extensions-4.15.0-py3-none-any.whl +0 -0
- package/wheels/xlsxwriter-3.2.9-py3-none-any.whl +0 -0
|
@@ -308,7 +308,7 @@ export function stringLiteralProgramHint(source) {
|
|
|
308
308
|
}
|
|
309
309
|
return ('command is wrapped in a string literal, so Python evaluates it as text and runs ' +
|
|
310
310
|
'nothing: remove the wrapping quotes (and any escaped \\" quotes) so the first line ' +
|
|
311
|
-
'of command is the first line of the program, e.g.
|
|
311
|
+
'of command is the first line of the program, e.g. import json.');
|
|
312
312
|
}
|
|
313
313
|
const COLLAPSED_COMPOUND = /;\s*(?:async\s+)?(?:for|if|while|with|def|class|try|match)\b/;
|
|
314
314
|
export function checkCollapsedCompound(source) {
|
|
@@ -320,159 +320,6 @@ export function checkCollapsedCompound(source) {
|
|
|
320
320
|
'cannot follow a semicolon. Pass multi-line Python source with newline characters, ' +
|
|
321
321
|
'not a single-line semicolon chain.');
|
|
322
322
|
}
|
|
323
|
-
const MANGLED_PPTX = [
|
|
324
|
-
['textframe', 'text_frame'],
|
|
325
|
-
['addslide', 'add_slide'],
|
|
326
|
-
['slidelayouts', 'slide_layouts'],
|
|
327
|
-
['slidewidth', 'slide_width'],
|
|
328
|
-
['slideheight', 'slide_height'],
|
|
329
|
-
['wordwrap', 'word_wrap'],
|
|
330
|
-
['addparagraph', 'add_paragraph'],
|
|
331
|
-
['addtextbox', 'add_textbox'],
|
|
332
|
-
['addpicture', 'add_picture'],
|
|
333
|
-
['ppalign', 'PP_ALIGN']
|
|
334
|
-
];
|
|
335
|
-
export function mangledPptxApiHint(source) {
|
|
336
|
-
if (!/\bpptx\b|Presentation\s*\(/.test(source))
|
|
337
|
-
return null;
|
|
338
|
-
const found = [];
|
|
339
|
-
for (const [bad, good] of MANGLED_PPTX) {
|
|
340
|
-
if (new RegExp(`\\b${bad}\\b`, 'i').test(source) && !source.includes(good)) {
|
|
341
|
-
found.push(`${bad} → ${good}`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if (found.length === 0) {
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
347
|
-
return (`python-pptx names keep their underscores — found stripped forms (${found.join(', ')}). ` +
|
|
348
|
-
'Restore them (text_frame not textframe, add_slide not addslide) and keep the skill ' +
|
|
349
|
-
'sample multi-line with real newlines.');
|
|
350
|
-
}
|
|
351
|
-
export function wrongRgbColorHint(source) {
|
|
352
|
-
if (!/\bpptx\b|Presentation\s*\(/.test(source))
|
|
353
|
-
return null;
|
|
354
|
-
if (/\bRGBColor\b/.test(source))
|
|
355
|
-
return null;
|
|
356
|
-
const code = source.replace(/#[^\n]*/g, '').replace(/'[^'\n]*'|"[^"\n]*"/g, '');
|
|
357
|
-
if (!/\bRgbColor\b|\brgbColor\b|\brgbcolor\b|\bRGB_Color\b|\brgb_color\b/.test(code)) {
|
|
358
|
-
return null;
|
|
359
|
-
}
|
|
360
|
-
return ('python-pptx color type is RGBColor (RGB in all caps): ' +
|
|
361
|
-
'from pptx.dml.color import RGBColor — not RgbColor / rgbColor. ' +
|
|
362
|
-
'Do not catch that ImportError and continue; fix the name and rerun.');
|
|
363
|
-
}
|
|
364
|
-
const FPDF_SIZE_KEYWORD = /\b(cell|multi_cell)\s*\([^)]*\b(width|height)\s*=/;
|
|
365
|
-
export function fpdfSizeKeywordHint(source) {
|
|
366
|
-
if (!/\bfpdf\b|\bFPDF\b/.test(source))
|
|
367
|
-
return null;
|
|
368
|
-
const match = FPDF_SIZE_KEYWORD.exec(source);
|
|
369
|
-
if (!match) {
|
|
370
|
-
return null;
|
|
371
|
-
}
|
|
372
|
-
return (`fpdf2 ${match[1]}() has no "${match[2]}" keyword — the size parameters are the positional ` +
|
|
373
|
-
'w and h: write every line (headings and body) with pdf.multi_cell(0, 6, text, ' +
|
|
374
|
-
'new_x="LMARGIN", new_y="NEXT"). Rename the arguments and keep new_x/new_y; ' +
|
|
375
|
-
'deleting keywords to silence the error prints later lines on top of each other.');
|
|
376
|
-
}
|
|
377
|
-
export function fpdfKeywordErrorHint(stderr) {
|
|
378
|
-
const match = /TypeError: FPDF\.(\w+)\(\) got an unexpected keyword argument '(\w+)'/.exec(stderr);
|
|
379
|
-
if (!match) {
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
const sized = match[2] === 'width' || match[2] === 'height';
|
|
383
|
-
const fix = sized
|
|
384
|
-
? 'the size parameters are the positional w and h'
|
|
385
|
-
: 'copy the exact call shape from the pdf skill sample';
|
|
386
|
-
return (`fpdf2 ${match[1]}() has no "${match[2]}" keyword — ${fix}: ` +
|
|
387
|
-
'write every line (headings and body) with pdf.multi_cell(0, 6, text, ' +
|
|
388
|
-
'new_x="LMARGIN", new_y="NEXT"). Rename the arguments and keep new_x/new_y; ' +
|
|
389
|
-
'deleting keywords to silence the error prints later lines on top of each other.');
|
|
390
|
-
}
|
|
391
|
-
const LATIN1_SUBSTITUTES = [
|
|
392
|
-
[/[\u2010-\u2015\u2212]/gu, '-'],
|
|
393
|
-
[/[\u2022\u25e6\u25aa\u25cf]/gu, '-'],
|
|
394
|
-
[/\u2026/gu, '...'],
|
|
395
|
-
[/\u2192/gu, '->'],
|
|
396
|
-
[/\u2190/gu, '<-'],
|
|
397
|
-
[/\u2248/gu, '~'],
|
|
398
|
-
[/\u2264/gu, '<='],
|
|
399
|
-
[/\u2265/gu, '>='],
|
|
400
|
-
[/\u2122/gu, '(TM)'],
|
|
401
|
-
[/\u20ac/gu, 'EUR'],
|
|
402
|
-
[/[\u2000-\u200a]/gu, ' '],
|
|
403
|
-
[/[\u200b-\u200d\ufeff]/gu, '']
|
|
404
|
-
];
|
|
405
|
-
export function normalizeFpdfSource(source) {
|
|
406
|
-
if (!/\bfpdf\b|\bFPDF\b/.test(source))
|
|
407
|
-
return source;
|
|
408
|
-
let out = source;
|
|
409
|
-
for (const [from, to] of LATIN1_SUBSTITUTES) {
|
|
410
|
-
out = out.replace(from, to);
|
|
411
|
-
}
|
|
412
|
-
return out;
|
|
413
|
-
}
|
|
414
|
-
const CURLY_QUOTE = /[\u2018\u2019\u201a\u2032\u201c\u201d\u201e\u2033]/u;
|
|
415
|
-
const TEXT_WITHOUT_COORDS = /\.text\s*\(\s*['"]/;
|
|
416
|
-
const TEXT_IS_STAMP_ONLY = 'fpdf2 text(x, y, string) paints at a fixed point with no wrapping \u2014 it is only for the ' +
|
|
417
|
-
'watermark stamp. Write document text (headings, paragraphs, bullets) with ' +
|
|
418
|
-
'pdf.multi_cell(0, 6, text, new_x="LMARGIN", new_y="NEXT").';
|
|
419
|
-
export function fpdfTextCallHint(source) {
|
|
420
|
-
if (!/\bfpdf\b|\bFPDF\b/.test(source))
|
|
421
|
-
return null;
|
|
422
|
-
if (!TEXT_WITHOUT_COORDS.test(source))
|
|
423
|
-
return null;
|
|
424
|
-
return TEXT_IS_STAMP_ONLY;
|
|
425
|
-
}
|
|
426
|
-
export function fpdfTextErrorHint(stderr) {
|
|
427
|
-
if (!/TypeError: FPDF\.text\(\)/.test(stderr))
|
|
428
|
-
return null;
|
|
429
|
-
return TEXT_IS_STAMP_ONLY;
|
|
430
|
-
}
|
|
431
|
-
export function fpdfNonLatin1Hint(source) {
|
|
432
|
-
if (!/\bfpdf\b|\bFPDF\b/.test(source))
|
|
433
|
-
return null;
|
|
434
|
-
if (source.includes('.replace('))
|
|
435
|
-
return null;
|
|
436
|
-
const bad = /[^\u0000-\u00ff]/u.exec(normalizeFpdfSource(source))?.[0];
|
|
437
|
-
if (!bad) {
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
if (CURLY_QUOTE.test(bad)) {
|
|
441
|
-
return (`fpdf2 core fonts are latin-1 only and the curly quote ${bad} is outside it — rewrite ` +
|
|
442
|
-
'the strings with straight quotes \' and " and rerun.');
|
|
443
|
-
}
|
|
444
|
-
return (`fpdf2 core fonts are latin-1 only and "${bad}" cannot be rendered — there are no Unicode ` +
|
|
445
|
-
'fonts in this runtime. Drop it or use a latin transliteration, and tell the user CJK and ' +
|
|
446
|
-
'emoji cannot appear in the PDF.');
|
|
447
|
-
}
|
|
448
|
-
export function fpdfUnicodeErrorHint(stderr) {
|
|
449
|
-
const encoding = /FPDFUnicodeEncodingException/.test(stderr) ||
|
|
450
|
-
(/UnicodeEncodeError: 'latin-1' codec/.test(stderr) && /fpdf/.test(stderr));
|
|
451
|
-
if (!encoding) {
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
return ('a character outside latin-1 reached an fpdf2 core font: replace it with ASCII ' +
|
|
455
|
-
'(bullets "- ", straight quotes, plain hyphens). CJK and emoji cannot be rendered — ' +
|
|
456
|
-
'tell the user instead of retrying with the same string.');
|
|
457
|
-
}
|
|
458
|
-
export function fpdfHorizontalSpaceHint(stderr) {
|
|
459
|
-
if (!/FPDFException: Not enough horizontal space/.test(stderr))
|
|
460
|
-
return null;
|
|
461
|
-
return ('an earlier cell/multi_cell left the cursor at the right margin: add ' +
|
|
462
|
-
'new_x="LMARGIN", new_y="NEXT" to every cell and multi_cell call so each ' +
|
|
463
|
-
'one starts on its own line.');
|
|
464
|
-
}
|
|
465
|
-
export function rgbColorImportHint(stderr) {
|
|
466
|
-
const name = /cannot import name '([^']+)' from 'pptx\.dml\.color'/.exec(stderr)?.[1];
|
|
467
|
-
if (!name || name === 'RGBColor') {
|
|
468
|
-
return null;
|
|
469
|
-
}
|
|
470
|
-
if (!/^rgb_?color$/i.test(name)) {
|
|
471
|
-
return null;
|
|
472
|
-
}
|
|
473
|
-
return (`pptx.dml.color exports RGBColor (all-caps RGB), not ${name}: ` +
|
|
474
|
-
'from pptx.dml.color import RGBColor');
|
|
475
|
-
}
|
|
476
323
|
export function checkPythonSource(command) {
|
|
477
324
|
const source = unwrapPythonSource(command) ?? command;
|
|
478
325
|
const literal = stringLiteralProgramHint(source);
|
|
@@ -483,26 +330,6 @@ export function checkPythonSource(command) {
|
|
|
483
330
|
if (collapsed) {
|
|
484
331
|
return collapsed;
|
|
485
332
|
}
|
|
486
|
-
const mangled = mangledPptxApiHint(source);
|
|
487
|
-
if (mangled) {
|
|
488
|
-
return mangled;
|
|
489
|
-
}
|
|
490
|
-
const rgb = wrongRgbColorHint(source);
|
|
491
|
-
if (rgb) {
|
|
492
|
-
return rgb;
|
|
493
|
-
}
|
|
494
|
-
const fpdfKeyword = fpdfSizeKeywordHint(source);
|
|
495
|
-
if (fpdfKeyword) {
|
|
496
|
-
return fpdfKeyword;
|
|
497
|
-
}
|
|
498
|
-
const fpdfLatin1 = fpdfNonLatin1Hint(source);
|
|
499
|
-
if (fpdfLatin1) {
|
|
500
|
-
return fpdfLatin1;
|
|
501
|
-
}
|
|
502
|
-
const fpdfText = fpdfTextCallHint(source);
|
|
503
|
-
if (fpdfText) {
|
|
504
|
-
return fpdfText;
|
|
505
|
-
}
|
|
506
333
|
const first = command.trimStart().split('\n')[0] ?? '';
|
|
507
334
|
if (!SHELL_WRAPPER.test(first)) {
|
|
508
335
|
return null;
|
|
@@ -511,7 +338,7 @@ export function checkPythonSource(command) {
|
|
|
511
338
|
return null;
|
|
512
339
|
}
|
|
513
340
|
if (/^pip3?\b/.test(first) || /^(python3?|py)\s+-m\s+pip\b/.test(first)) {
|
|
514
|
-
return 'language "python" does not run shell commands: install packages via the packages argument (e.g. packages: ["
|
|
341
|
+
return 'language "python" does not run shell commands: install packages via the packages argument (e.g. packages: ["requests"]), not pip';
|
|
515
342
|
}
|
|
516
343
|
if (unwrapPythonSource(command)) {
|
|
517
344
|
return null;
|
|
@@ -522,295 +349,8 @@ export function syntaxErrorHint(stderr, source) {
|
|
|
522
349
|
if (!/SyntaxError/.test(stderr))
|
|
523
350
|
return null;
|
|
524
351
|
return (checkCollapsedCompound(source) ??
|
|
525
|
-
mangledPptxApiHint(source) ??
|
|
526
352
|
'SyntaxError means the Python source is invalid as written — keep real newlines for for/if/with/def bodies; do not collapse a multi-line program into one semicolon-separated line.');
|
|
527
353
|
}
|
|
528
|
-
export function checkPptxBuildSource(source, outputs, packages) {
|
|
529
|
-
if (!outputs?.some((name) => name.toLowerCase().endsWith('.pptx'))) {
|
|
530
|
-
return null;
|
|
531
|
-
}
|
|
532
|
-
const pkgPptx = (packages ?? []).some((p) => {
|
|
533
|
-
const name = (p.split(/[=<>!~[]/)[0] ?? '').trim().toLowerCase();
|
|
534
|
-
return name === 'python-pptx' || name === 'pptx';
|
|
535
|
-
});
|
|
536
|
-
const srcPptx = /\bpptx\b|\bPresentation\s*\(/.test(source);
|
|
537
|
-
if (!pkgPptx && !srcPptx) {
|
|
538
|
-
return null;
|
|
539
|
-
}
|
|
540
|
-
const builds = /\bPresentation\s*\(/.test(source) && /\.save\s*\(/.test(source);
|
|
541
|
-
if (builds) {
|
|
542
|
-
return null;
|
|
543
|
-
}
|
|
544
|
-
return ('outputs declare a .pptx but command does not build one: pass multi-line python-pptx ' +
|
|
545
|
-
'source that constructs Presentation(...) and calls save("deck.pptx") matching an ' +
|
|
546
|
-
'outputs entry. Do not use python -c, os.listdir, pip, or a different package pin — ' +
|
|
547
|
-
'language="python" already runs bare source, and packages must stay ["python-pptx==1.0.2"].');
|
|
548
|
-
}
|
|
549
|
-
export function tripleQuoteAt(source, index) {
|
|
550
|
-
if (source.startsWith('"""', index))
|
|
551
|
-
return '"""';
|
|
552
|
-
return source.startsWith("'''", index) ? "'''" : null;
|
|
553
|
-
}
|
|
554
|
-
export function isEscaped(source, index) {
|
|
555
|
-
let slashes = 0;
|
|
556
|
-
while (index - slashes > 0 && source[index - slashes - 1] === '\\') {
|
|
557
|
-
slashes++;
|
|
558
|
-
}
|
|
559
|
-
return slashes % 2 === 1;
|
|
560
|
-
}
|
|
561
|
-
function splitCallArgs(source, open) {
|
|
562
|
-
const args = [];
|
|
563
|
-
let arg = '';
|
|
564
|
-
let depth = 0;
|
|
565
|
-
let quote = null;
|
|
566
|
-
for (let i = open; i < source.length; i++) {
|
|
567
|
-
const ch = source.charAt(i);
|
|
568
|
-
if (quote) {
|
|
569
|
-
arg += ch;
|
|
570
|
-
if (ch === quote && !isEscaped(source, i)) {
|
|
571
|
-
quote = null;
|
|
572
|
-
}
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
if (ch === '"' || ch === "'") {
|
|
576
|
-
quote = ch;
|
|
577
|
-
arg += ch;
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
if (ch === '(' || ch === '[' || ch === '{') {
|
|
581
|
-
depth++;
|
|
582
|
-
if (depth > 1) {
|
|
583
|
-
arg += ch;
|
|
584
|
-
}
|
|
585
|
-
continue;
|
|
586
|
-
}
|
|
587
|
-
if (ch === ')' || ch === ']' || ch === '}') {
|
|
588
|
-
depth--;
|
|
589
|
-
if (depth === 0) {
|
|
590
|
-
args.push(arg);
|
|
591
|
-
return args.map((a) => a.trim());
|
|
592
|
-
}
|
|
593
|
-
arg += ch;
|
|
594
|
-
continue;
|
|
595
|
-
}
|
|
596
|
-
if (ch === ',' && depth === 1) {
|
|
597
|
-
args.push(arg);
|
|
598
|
-
arg = '';
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
arg += ch;
|
|
602
|
-
}
|
|
603
|
-
return null;
|
|
604
|
-
}
|
|
605
|
-
const MIN_MEANT_EMU = 12700;
|
|
606
|
-
function bareEmuMistake(arg) {
|
|
607
|
-
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(arg))
|
|
608
|
-
return false;
|
|
609
|
-
const value = Math.abs(Number(arg));
|
|
610
|
-
return value !== 0 && value < MIN_MEANT_EMU;
|
|
611
|
-
}
|
|
612
|
-
function stripCommentsAndStrings(source) {
|
|
613
|
-
let out = '';
|
|
614
|
-
let i = 0;
|
|
615
|
-
while (i < source.length) {
|
|
616
|
-
const ch = source.charAt(i);
|
|
617
|
-
const triple = tripleQuoteAt(source, i);
|
|
618
|
-
const quote = triple ?? (ch === '"' || ch === "'" ? ch : null);
|
|
619
|
-
if (quote) {
|
|
620
|
-
i += quote.length;
|
|
621
|
-
while (i < source.length && !(source.startsWith(quote, i) && !isEscaped(source, i))) {
|
|
622
|
-
i++;
|
|
623
|
-
}
|
|
624
|
-
out += '""';
|
|
625
|
-
i += quote.length;
|
|
626
|
-
continue;
|
|
627
|
-
}
|
|
628
|
-
if (ch === '#') {
|
|
629
|
-
while (i < source.length && source[i] !== '\n') {
|
|
630
|
-
i++;
|
|
631
|
-
}
|
|
632
|
-
continue;
|
|
633
|
-
}
|
|
634
|
-
out += ch;
|
|
635
|
-
i++;
|
|
636
|
-
}
|
|
637
|
-
return out;
|
|
638
|
-
}
|
|
639
|
-
const LENGTH_LIBRARY = {
|
|
640
|
-
pptx: {
|
|
641
|
-
module: 'pptx.util',
|
|
642
|
-
effect: 'the shape collapses to an invisible box in the top-left corner and the deck is delivered broken',
|
|
643
|
-
example: 'add_textbox(Inches(1), Inches(1), Inches(8), Inches(1.5))'
|
|
644
|
-
},
|
|
645
|
-
docx: {
|
|
646
|
-
module: 'docx.shared',
|
|
647
|
-
effect: 'the image collapses to nothing and the document is delivered broken',
|
|
648
|
-
example: 'add_picture("chart.png", width=Inches(5.5))'
|
|
649
|
-
}
|
|
650
|
-
};
|
|
651
|
-
function lengthLibrary(source, packages) {
|
|
652
|
-
const names = (packages ?? []).map((p) => (p.split(/[=<>!~[]/)[0] ?? '').trim().toLowerCase());
|
|
653
|
-
if (names.includes('python-pptx') || names.includes('pptx')) {
|
|
654
|
-
return 'pptx';
|
|
655
|
-
}
|
|
656
|
-
if (names.includes('python-docx') || names.includes('docx')) {
|
|
657
|
-
return 'docx';
|
|
658
|
-
}
|
|
659
|
-
if (/\bpptx\b|\bPresentation\s*\(/.test(source)) {
|
|
660
|
-
return 'pptx';
|
|
661
|
-
}
|
|
662
|
-
if (/\bdocx\b|\bDocument\s*\(/.test(source)) {
|
|
663
|
-
return 'docx';
|
|
664
|
-
}
|
|
665
|
-
return null;
|
|
666
|
-
}
|
|
667
|
-
const LENGTH_KEYWORDS = ['left', 'top', 'width', 'height'];
|
|
668
|
-
const FIRST_LENGTH_ARG = { add_textbox: 0, add_picture: 1 };
|
|
669
|
-
function callLibrary(method, before, fallback) {
|
|
670
|
-
if (method === 'add_textbox')
|
|
671
|
-
return 'pptx';
|
|
672
|
-
if (/\bshapes\s*\.\s*$/.test(before))
|
|
673
|
-
return 'pptx';
|
|
674
|
-
if (/\b(?:doc|document|run|paragraph|cell)\s*\.\s*$/.test(before)) {
|
|
675
|
-
return 'docx';
|
|
676
|
-
}
|
|
677
|
-
return fallback;
|
|
678
|
-
}
|
|
679
|
-
export function checkBareLengthArgs(source, packages) {
|
|
680
|
-
const clean = stripCommentsAndStrings(source);
|
|
681
|
-
const library = lengthLibrary(clean, packages);
|
|
682
|
-
if (!library) {
|
|
683
|
-
return null;
|
|
684
|
-
}
|
|
685
|
-
for (const method of ['add_textbox', 'add_picture']) {
|
|
686
|
-
const calls = new RegExp(`\\b${method}\\s*\\(`, 'g');
|
|
687
|
-
let call;
|
|
688
|
-
while ((call = calls.exec(clean)) !== null) {
|
|
689
|
-
const args = splitCallArgs(clean, calls.lastIndex - 1);
|
|
690
|
-
if (!args) {
|
|
691
|
-
continue;
|
|
692
|
-
}
|
|
693
|
-
const offender = args.find((arg, index) => {
|
|
694
|
-
const eq = arg.indexOf('=');
|
|
695
|
-
const name = eq === -1 ? '' : arg.slice(0, eq).trim();
|
|
696
|
-
if (eq !== -1 && /^[A-Za-z_]\w*$/.test(name)) {
|
|
697
|
-
return LENGTH_KEYWORDS.includes(name) && bareEmuMistake(arg.slice(eq + 1).trim());
|
|
698
|
-
}
|
|
699
|
-
return index >= FIRST_LENGTH_ARG[method] && bareEmuMistake(arg);
|
|
700
|
-
});
|
|
701
|
-
if (offender !== undefined) {
|
|
702
|
-
const before = clean.slice(Math.max(0, call.index - 40), call.index);
|
|
703
|
-
return bareLengthError(`${method}(...)`, offender, callLibrary(method, before, library));
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
if (library === 'pptx') {
|
|
708
|
-
const assignments = /\bslide_(?:width|height)\s*=\s*([^\n]+)/g;
|
|
709
|
-
let assigned;
|
|
710
|
-
while ((assigned = assignments.exec(clean)) !== null) {
|
|
711
|
-
const value = (assigned[1] ?? '').trim();
|
|
712
|
-
if (bareEmuMistake(value)) {
|
|
713
|
-
return bareLengthError('slide_width / slide_height', value, library);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
return null;
|
|
718
|
-
}
|
|
719
|
-
function bareLengthError(where, value, library) {
|
|
720
|
-
const fix = LENGTH_LIBRARY[library];
|
|
721
|
-
return (`${where} was given the bare number ${value} where a typed length is required: ` +
|
|
722
|
-
`python-${library} reads a bare number as EMU (914400 to the inch), so ${fix.effect}. ` +
|
|
723
|
-
'Nothing raises, so this never reaches you as an error. Wrap every position and size ' +
|
|
724
|
-
`with Inches(...), Pt(...) or Emu(...) from ${fix.module}, e.g. ${fix.example}. ` +
|
|
725
|
-
'Do not rescale the raw numbers.');
|
|
726
|
-
}
|
|
727
|
-
export function checkPdfBuildSource(source, outputs, packages) {
|
|
728
|
-
if (!outputs?.some((name) => name.toLowerCase().endsWith('.pdf')))
|
|
729
|
-
return null;
|
|
730
|
-
const pkgPdf = (packages ?? []).some((p) => {
|
|
731
|
-
const name = (p.split(/[=<>!~[]/)[0] ?? '').trim().toLowerCase();
|
|
732
|
-
return name === 'fpdf2' || name === 'pypdf';
|
|
733
|
-
});
|
|
734
|
-
const srcPdf = /\b(fpdf|FPDF|pypdf|PdfWriter|PdfReader)\b/.test(source);
|
|
735
|
-
if (!pkgPdf && !srcPdf) {
|
|
736
|
-
return null;
|
|
737
|
-
}
|
|
738
|
-
const builds = /\.output\s*\(|\.write\s*\(/.test(source);
|
|
739
|
-
if (builds) {
|
|
740
|
-
return null;
|
|
741
|
-
}
|
|
742
|
-
return ('outputs declare a .pdf but command never writes one: pass multi-line Python that ' +
|
|
743
|
-
'builds the document and saves it under the declared name — fpdf2 ends with ' +
|
|
744
|
-
'pdf.output("name.pdf"), pypdf with writer.write("name.pdf"). Do not send diagnostics ' +
|
|
745
|
-
'like os.listdir or print-only scripts while outputs still lists the file.');
|
|
746
|
-
}
|
|
747
|
-
export function stripCommentsAndDocstrings(source) {
|
|
748
|
-
let out = '';
|
|
749
|
-
let i = 0;
|
|
750
|
-
while (i < source.length) {
|
|
751
|
-
const ch = source.charAt(i);
|
|
752
|
-
const triple = tripleQuoteAt(source, i);
|
|
753
|
-
if (triple) {
|
|
754
|
-
i += triple.length;
|
|
755
|
-
while (i < source.length && !(source.startsWith(triple, i) && !isEscaped(source, i))) {
|
|
756
|
-
i++;
|
|
757
|
-
}
|
|
758
|
-
out += '""';
|
|
759
|
-
i += triple.length;
|
|
760
|
-
continue;
|
|
761
|
-
}
|
|
762
|
-
if (ch === '"' || ch === "'") {
|
|
763
|
-
const start = i;
|
|
764
|
-
i++;
|
|
765
|
-
while (i < source.length && !(source[i] === ch && !isEscaped(source, i))) {
|
|
766
|
-
i++;
|
|
767
|
-
}
|
|
768
|
-
i++;
|
|
769
|
-
out += source.slice(start, Math.min(i, source.length));
|
|
770
|
-
continue;
|
|
771
|
-
}
|
|
772
|
-
if (ch === '#') {
|
|
773
|
-
while (i < source.length && source[i] !== '\n') {
|
|
774
|
-
i++;
|
|
775
|
-
}
|
|
776
|
-
continue;
|
|
777
|
-
}
|
|
778
|
-
out += ch;
|
|
779
|
-
i++;
|
|
780
|
-
}
|
|
781
|
-
return out;
|
|
782
|
-
}
|
|
783
|
-
export const PDF_READ = /(?:PdfReader|append)\s*\(\s*['"]([\w.-]+\.pdf)['"]|clone_from\s*=\s*['"]([\w.-]+\.pdf)['"]|open\s*\(\s*['"]([\w.-]+\.pdf)['"]\s*(?:,\s*(?:mode\s*=\s*)?['"](?<mode>[rwaxb+]+)['"])?/g;
|
|
784
|
-
export const OFFICE_READ = /(?:load_workbook|Document|Presentation)\s*\(\s*['"]([\w.-]+\.(?:xlsx|xlsm|docx|pptx))['"]/g;
|
|
785
|
-
export function checkUnstagedRead(source, inputs) {
|
|
786
|
-
const staged = new Set((inputs ?? []).map((i) => i.path));
|
|
787
|
-
const clean = stripCommentsAndDocstrings(source);
|
|
788
|
-
for (const pattern of [PDF_READ, OFFICE_READ]) {
|
|
789
|
-
const reads = new RegExp(pattern.source, 'g');
|
|
790
|
-
let match;
|
|
791
|
-
while ((match = reads.exec(clean)) !== null) {
|
|
792
|
-
const name = match.slice(1).find((group) => group !== undefined) ?? '';
|
|
793
|
-
const mode = match.groups?.mode;
|
|
794
|
-
if (mode !== undefined && /[wax]/.test(mode)) {
|
|
795
|
-
continue;
|
|
796
|
-
}
|
|
797
|
-
if (staged.has(name)) {
|
|
798
|
-
continue;
|
|
799
|
-
}
|
|
800
|
-
const before = clean.slice(0, match.index);
|
|
801
|
-
if (before.includes(`"${name}"`) || before.includes(`'${name}'`)) {
|
|
802
|
-
continue;
|
|
803
|
-
}
|
|
804
|
-
return (`${name} does not exist in the working directory — it starts empty on every run, so ` +
|
|
805
|
-
'files from earlier calls are gone. Stage the attachment via inputs: ' +
|
|
806
|
-
`[{ "attachmentId": "<id from the tool result that produced it, or from the ` +
|
|
807
|
-
`[Attached file …] line if the user uploaded it>", "path": "${name}" }] ` +
|
|
808
|
-
'and open it by that bare name; if no id for it appears anywhere in the chat, ' +
|
|
809
|
-
'ask the user to attach the file.');
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
return null;
|
|
813
|
-
}
|
|
814
354
|
const ATTACHMENT_ID = /^[0-9a-f]{32}$/i;
|
|
815
355
|
export function missingFileHint(stderr) {
|
|
816
356
|
const name = /FileNotFoundError: \[Errno \d+\] No such file or directory: '([^']+)'/.exec(stderr)?.[1];
|
|
@@ -935,6 +475,39 @@ function prepare(args, cwd) {
|
|
|
935
475
|
}
|
|
936
476
|
return { packages, inputs, outputs, workdir };
|
|
937
477
|
}
|
|
478
|
+
const OFFICE_MODULES = {
|
|
479
|
+
pptx: { tool: 'pptx', skill: 'presentations' },
|
|
480
|
+
docx: { tool: 'docx', skill: 'word' },
|
|
481
|
+
openpyxl: { tool: 'xlsx', skill: 'excel' }
|
|
482
|
+
};
|
|
483
|
+
const OFFICE_PACKAGES = {
|
|
484
|
+
'python-pptx': 'pptx',
|
|
485
|
+
'python-docx': 'docx',
|
|
486
|
+
openpyxl: 'openpyxl'
|
|
487
|
+
};
|
|
488
|
+
const OFFICE_IMPORT = /^\s*(?:from\s+(pptx|docx|openpyxl)\b|import\s+(?:[\w.]+\s*(?:as\s+\w+\s*)?,\s*)*(pptx|docx|openpyxl)\b)/m;
|
|
489
|
+
function officeAnswer(module, toolNames) {
|
|
490
|
+
const office = OFFICE_MODULES[module];
|
|
491
|
+
if (!office) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
const call = toolNames?.includes(office.tool)
|
|
495
|
+
? `call the ${office.tool} tool`
|
|
496
|
+
: `load the "${office.skill}" skill and call the ${office.tool} tool it grants`;
|
|
497
|
+
return `module "${module}" is not installed: Office files are read and edited without Python here. Do not retry in Python; ${call} with the same content.`;
|
|
498
|
+
}
|
|
499
|
+
export function officeModuleHint(stderr, toolNames) {
|
|
500
|
+
const module = /ModuleNotFoundError: No module named '([^'.]+)/.exec(stderr)?.[1];
|
|
501
|
+
return module ? officeAnswer(module, toolNames) : null;
|
|
502
|
+
}
|
|
503
|
+
export function officeImportError(source, { packages, toolNames }) {
|
|
504
|
+
const imported = OFFICE_IMPORT.exec(source);
|
|
505
|
+
const declared = (packages ?? [])
|
|
506
|
+
.map((spec) => OFFICE_PACKAGES[spec.split(/[=<>!~[ ]/)[0]?.toLowerCase() ?? ''])
|
|
507
|
+
.find(Boolean);
|
|
508
|
+
const module = imported?.[1] ?? imported?.[2] ?? declared;
|
|
509
|
+
return module ? officeAnswer(module, toolNames) : null;
|
|
510
|
+
}
|
|
938
511
|
export function missingModuleHint(stderr, { packages, sandbox = false, network = false } = {}) {
|
|
939
512
|
const module = /ModuleNotFoundError: No module named '([^']+)'/.exec(stderr)?.[1];
|
|
940
513
|
if (!module) {
|
|
@@ -968,53 +541,6 @@ export function missingModuleHint(stderr, { packages, sandbox = false, network =
|
|
|
968
541
|
}
|
|
969
542
|
return `module "${root}" is not installed: add "${spec}" to packages (packages: ["${spec}"]) and run the same source again. There is no pip.`;
|
|
970
543
|
}
|
|
971
|
-
export function nonePlaceholderHint(stderr) {
|
|
972
|
-
if (!/AttributeError: 'NoneType' object has no attribute '(text|text_frame)'/.test(stderr)) {
|
|
973
|
-
return null;
|
|
974
|
-
}
|
|
975
|
-
return 'a placeholder the layout does not have reads as None: the blank layout has no title and no body, so shapes.title is None there. Use a layout that owns the placeholder (title+content is commonly index 1), or keep the blank layout and add every text box explicitly with shapes.add_textbox(left, top, width, height).';
|
|
976
|
-
}
|
|
977
|
-
export function slideShapesHint(stderr) {
|
|
978
|
-
const attribute = /AttributeError: 'Slide' object has no attribute '(add_picture|add_textbox|addpicture|addtextbox)'/.exec(stderr)?.[1];
|
|
979
|
-
if (!attribute) {
|
|
980
|
-
return null;
|
|
981
|
-
}
|
|
982
|
-
const method = attribute.startsWith('addp') ? 'add_picture' : 'add_textbox';
|
|
983
|
-
return `a Slide has no add_* methods of its own — they live on its shapes collection: slide.shapes.${method}(...), not slide.${method}(...).`;
|
|
984
|
-
}
|
|
985
|
-
export function shapeFontHint(stderr) {
|
|
986
|
-
const attribute = /AttributeError: '\w*(?:Placeholder|Shape|Slide)\w*' object has no attribute '(font|alignment|text_align|word_wrap|paragraphs|add_paragraph)'/.exec(stderr)?.[1];
|
|
987
|
-
if (!attribute) {
|
|
988
|
-
return null;
|
|
989
|
-
}
|
|
990
|
-
return `"${attribute}" belongs to the text frame or a paragraph inside it, not to the shape: go through shape.text_frame first (shape.text_frame.word_wrap, shape.text_frame.paragraphs[0].font.size, shape.text_frame.paragraphs[0].alignment). shape.text is the one shortcut that reads through to the frame.`;
|
|
991
|
-
}
|
|
992
|
-
export function shapesBackgroundHint(stderr) {
|
|
993
|
-
if (!/AttributeError: '\w*(?:Shapes|Shape|Placeholder)\w*' object has no attribute 'background'/.test(stderr)) {
|
|
994
|
-
return null;
|
|
995
|
-
}
|
|
996
|
-
return "'background' lives on the slide, not on its shapes: fill = slide.background.fill; fill.solid(); fill.fore_color.rgb = RGBColor(...). A shape is tinted through its own shape.fill the same way.";
|
|
997
|
-
}
|
|
998
|
-
export function paragraphFillHint(stderr) {
|
|
999
|
-
if (!/AttributeError: '(?:_Paragraph|_Run|TextFrame)' object has no attribute 'fill'/.test(stderr)) {
|
|
1000
|
-
return null;
|
|
1001
|
-
}
|
|
1002
|
-
return "text has no 'fill': color it through the font — paragraph.font.color.rgb = RGBColor(...). A fill belongs to a shape (shape.fill) or to slide.background.fill, never to a paragraph or text frame.";
|
|
1003
|
-
}
|
|
1004
|
-
export function readOnlyPropertyHint(stderr) {
|
|
1005
|
-
if (!/AttributeError: can't set attribute/.test(stderr))
|
|
1006
|
-
return null;
|
|
1007
|
-
if (!/pptx[/\\]util\.py/.test(stderr))
|
|
1008
|
-
return null;
|
|
1009
|
-
return 'a python-pptx container is read-only: font, text_frame, paragraphs, runs and shapes cannot be assigned, only reached through. Set the leaf property instead — paragraph.font.size = Pt(20) and paragraph.font.bold = True, not paragraph.font = …; and write text with text_frame.text or paragraph.text, never text_frame = …';
|
|
1010
|
-
}
|
|
1011
|
-
export function paragraphIndexHint(stderr, outputs) {
|
|
1012
|
-
if (!/IndexError: tuple index out of range/.test(stderr))
|
|
1013
|
-
return null;
|
|
1014
|
-
if (!outputs.some((name) => name.toLowerCase().endsWith('.pptx')))
|
|
1015
|
-
return null;
|
|
1016
|
-
return 'a new text frame holds exactly one paragraph, so text_frame.paragraphs[1] does not exist yet: write the first through paragraphs[0] (or text_frame.text) and create each one after it with text_frame.add_paragraph(), which returns the new paragraph. A paragraph has .text, .font and .alignment — it has no .paragraphs and no .add_paragraph of its own.';
|
|
1017
|
-
}
|
|
1018
544
|
export function shellNameHint(stderr) {
|
|
1019
545
|
const name = /NameError: name '(python3?|py|pip3?)' is not defined/.exec(stderr)?.[1];
|
|
1020
546
|
if (!name) {
|
|
@@ -1060,110 +586,6 @@ export function packageFetchFailedHint(stderr) {
|
|
|
1060
586
|
function fail(stderr) {
|
|
1061
587
|
return { stdout: '', stderr, exitCode: 1 };
|
|
1062
588
|
}
|
|
1063
|
-
export const IMAGE_INPUT = /\.(png|jpe?g|gif|webp|bmp|heic|heif|avif|tiff?)$/i;
|
|
1064
|
-
const PPT_MEDIA = b4a.from('ppt/media/');
|
|
1065
|
-
const deckAttachedThisTurn = new WeakMap();
|
|
1066
|
-
export function pptxHasEmbeddedMedia(bytes) {
|
|
1067
|
-
outer: for (let i = 0; i <= bytes.length - PPT_MEDIA.length; i++) {
|
|
1068
|
-
for (let j = 0; j < PPT_MEDIA.length; j++) {
|
|
1069
|
-
if (bytes[i + j] !== PPT_MEDIA[j])
|
|
1070
|
-
continue outer;
|
|
1071
|
-
}
|
|
1072
|
-
return true;
|
|
1073
|
-
}
|
|
1074
|
-
return false;
|
|
1075
|
-
}
|
|
1076
|
-
export function softFailedDeckHint(stdout, exitCode, source) {
|
|
1077
|
-
if (exitCode !== 0)
|
|
1078
|
-
return null;
|
|
1079
|
-
if (!/\bPresentation\s*\(/.test(source))
|
|
1080
|
-
return null;
|
|
1081
|
-
if (!/Image loading error|expected if file not found|cannot import name ['"]RgbColor['"]/i.test(stdout)) {
|
|
1082
|
-
return null;
|
|
1083
|
-
}
|
|
1084
|
-
return ('the deck soft-failed (caught an image/import error and continued): remove try/except ' +
|
|
1085
|
-
'around imports and add_picture, fix the error, and rerun once — a partial deck must not be saved');
|
|
1086
|
-
}
|
|
1087
|
-
export function stagedImagesMissingFromDeck(result, { inputs, outputs, source }) {
|
|
1088
|
-
if (result.exitCode !== 0)
|
|
1089
|
-
return null;
|
|
1090
|
-
if (!/\bPresentation\s*\(/.test(source))
|
|
1091
|
-
return null;
|
|
1092
|
-
const images = (inputs ?? []).filter((i) => IMAGE_INPUT.test(i.path));
|
|
1093
|
-
const [first] = images;
|
|
1094
|
-
if (!first) {
|
|
1095
|
-
return null;
|
|
1096
|
-
}
|
|
1097
|
-
for (const name of outputs ?? []) {
|
|
1098
|
-
if (!name.toLowerCase().endsWith('.pptx'))
|
|
1099
|
-
continue;
|
|
1100
|
-
const bytes = result.files?.get(name);
|
|
1101
|
-
if (!bytes) {
|
|
1102
|
-
continue;
|
|
1103
|
-
}
|
|
1104
|
-
if (!pptxHasEmbeddedMedia(bytes)) {
|
|
1105
|
-
return (`staged image(s) ${images.map((i) => i.path).join(', ')} were not embedded: ` +
|
|
1106
|
-
`call slide.shapes.add_picture("${first.path}", …) without try/except, then save`);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
return null;
|
|
1110
|
-
}
|
|
1111
|
-
export function checkStagedImagesUsed(source, inputs, outputs) {
|
|
1112
|
-
if (!inputs?.length)
|
|
1113
|
-
return null;
|
|
1114
|
-
if (!outputs?.some((n) => n.toLowerCase().endsWith('.pptx')))
|
|
1115
|
-
return null;
|
|
1116
|
-
if (!/\bPresentation\s*\(/.test(source)) {
|
|
1117
|
-
return null;
|
|
1118
|
-
}
|
|
1119
|
-
const images = inputs.filter((i) => IMAGE_INPUT.test(i.path));
|
|
1120
|
-
if (!images.length) {
|
|
1121
|
-
return null;
|
|
1122
|
-
}
|
|
1123
|
-
if (!/\.add_picture\s*\(/.test(source)) {
|
|
1124
|
-
return ('inputs stage image(s) but command never calls slide.shapes.add_picture(...): ' +
|
|
1125
|
-
'embed each staged path or drop it from inputs');
|
|
1126
|
-
}
|
|
1127
|
-
const missing = images.filter((i) => !source.includes(`"${i.path}"`) && !source.includes(`'${i.path}'`));
|
|
1128
|
-
if (missing.length) {
|
|
1129
|
-
return (`inputs stage ${missing.map((m) => m.path).join(', ')} but command never opens those files — ` +
|
|
1130
|
-
`pass the bare path to slide.shapes.add_picture("…")`);
|
|
1131
|
-
}
|
|
1132
|
-
if (/\btry\s*:(?:(?!\bexcept\b|\bfinally\b)[\s\S]){0,1200}?\badd_picture\b/.test(source)) {
|
|
1133
|
-
return 'do not wrap add_picture in try/except: if the image fails the run must fail so you can fix it';
|
|
1134
|
-
}
|
|
1135
|
-
return null;
|
|
1136
|
-
}
|
|
1137
|
-
export function checkTurnImagesStaged(source, { inputs, outputs, turnImages }) {
|
|
1138
|
-
if (!turnImages?.length)
|
|
1139
|
-
return null;
|
|
1140
|
-
if (!outputs?.some((n) => n.toLowerCase().endsWith('.pptx')))
|
|
1141
|
-
return null;
|
|
1142
|
-
if (!/\bPresentation\s*\(/.test(source)) {
|
|
1143
|
-
return null;
|
|
1144
|
-
}
|
|
1145
|
-
if ((inputs ?? []).some((i) => i.attachmentId === undefined)) {
|
|
1146
|
-
return null;
|
|
1147
|
-
}
|
|
1148
|
-
return (`the user's message has ${turnImages.length} uploaded image(s) but inputs stage none of them — ` +
|
|
1149
|
-
'add one id-less { "path": "…" } entry per image and embed each with slide.shapes.add_picture(...)');
|
|
1150
|
-
}
|
|
1151
|
-
export function deckDeliveredStop(attachments, exitCode, source) {
|
|
1152
|
-
if (exitCode !== 0 || !attachments?.length)
|
|
1153
|
-
return null;
|
|
1154
|
-
if (source !== undefined && !/\bpptx\b|\bPresentation\s*\(/.test(source)) {
|
|
1155
|
-
return null;
|
|
1156
|
-
}
|
|
1157
|
-
const decks = attachments.filter((a) => a.fileName.toLowerCase().endsWith('.pptx'));
|
|
1158
|
-
if (decks.length === 0) {
|
|
1159
|
-
return null;
|
|
1160
|
-
}
|
|
1161
|
-
const names = decks.map((a) => a.fileName).join(', ');
|
|
1162
|
-
const verb = decks.length === 1 ? 'is' : 'are';
|
|
1163
|
-
return (`${names} ${verb} attached to the chat. STOP: do not call exec again for this deck. ` +
|
|
1164
|
-
'A second exec that rebuilds the same .pptx will be rejected. ' +
|
|
1165
|
-
'Reply to the user with one line naming the file and slide count from stdout — nothing else.');
|
|
1166
|
-
}
|
|
1167
589
|
async function stage(inputs, fetchAttachment, opts = {}) {
|
|
1168
590
|
if (!inputs?.length)
|
|
1169
591
|
return undefined;
|
|
@@ -1181,37 +603,10 @@ async function collect(result, { declared, attach, runBudget, source }) {
|
|
|
1181
603
|
if (!declared?.length || !attach) {
|
|
1182
604
|
return base;
|
|
1183
605
|
}
|
|
1184
|
-
const pptxBuild = source !== undefined && /\bpptx\b|\bPresentation\s*\(/.test(source);
|
|
1185
|
-
const pptxNames = declared.filter((n) => n.toLowerCase().endsWith('.pptx') && result.files?.has(n));
|
|
1186
|
-
const wouldAttachPptx = result.exitCode === 0 && pptxBuild && pptxNames.length > 0;
|
|
1187
|
-
if (wouldAttachPptx) {
|
|
1188
|
-
const attached = deckAttachedThisTurn.get(attach);
|
|
1189
|
-
if (attached) {
|
|
1190
|
-
const collision = pptxNames.find((n) => attached.has(n.toLowerCase()));
|
|
1191
|
-
if (collision) {
|
|
1192
|
-
return fail(`"${collision}" was already attached this turn — do not rebuild it; reply to the user with the file name and slide count`);
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
606
|
const { attachments, missingOutputs, rejectedOutputs } = await attachOutputs(attach, (result.exitCode === 0 && result.files) || new Map(), { declared, runBudget });
|
|
1197
607
|
const rejected = [...(result.rejected ?? []), ...rejectedOutputs];
|
|
1198
|
-
const stop = deckDeliveredStop(attachments, result.exitCode, source);
|
|
1199
|
-
if (stop && result.exitCode === 0) {
|
|
1200
|
-
let attached = deckAttachedThisTurn.get(attach);
|
|
1201
|
-
if (!attached) {
|
|
1202
|
-
attached = new Set();
|
|
1203
|
-
deckAttachedThisTurn.set(attach, attached);
|
|
1204
|
-
}
|
|
1205
|
-
for (const n of pptxNames) {
|
|
1206
|
-
attached.add(n.toLowerCase());
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
const stdout = stop && result.exitCode === 0
|
|
1210
|
-
? `${base.stdout}${base.stdout && !base.stdout.endsWith('\n') ? '\n' : ''}${stop}`
|
|
1211
|
-
: base.stdout;
|
|
1212
608
|
return {
|
|
1213
609
|
...base,
|
|
1214
|
-
stdout,
|
|
1215
610
|
attachments: attachments.length > 0 ? attachments : undefined,
|
|
1216
611
|
missingOutputs: missingOutputs.length > 0 ? missingOutputs : undefined,
|
|
1217
612
|
rejectedOutputs: rejected.length > 0 ? rejected : undefined
|
|
@@ -1242,39 +637,11 @@ export function createPythonAdapter(disabled, loadPyodide, network = false) {
|
|
|
1242
637
|
if (sourceError) {
|
|
1243
638
|
return fail(sourceError);
|
|
1244
639
|
}
|
|
1245
|
-
const source =
|
|
640
|
+
const source = unwrapPythonSource(command) ?? command;
|
|
1246
641
|
const prepared = prepare(args, options.cwd);
|
|
1247
642
|
if ('error' in prepared) {
|
|
1248
643
|
return fail(prepared.error);
|
|
1249
644
|
}
|
|
1250
|
-
const pptxError = checkPptxBuildSource(source, prepared.outputs, prepared.packages);
|
|
1251
|
-
if (pptxError) {
|
|
1252
|
-
return fail(pptxError);
|
|
1253
|
-
}
|
|
1254
|
-
const pdfError = checkPdfBuildSource(source, prepared.outputs, prepared.packages);
|
|
1255
|
-
if (pdfError) {
|
|
1256
|
-
return fail(pdfError);
|
|
1257
|
-
}
|
|
1258
|
-
const bareLength = checkBareLengthArgs(source, prepared.packages);
|
|
1259
|
-
if (bareLength) {
|
|
1260
|
-
return fail(bareLength);
|
|
1261
|
-
}
|
|
1262
|
-
const unstagedRead = checkUnstagedRead(source, prepared.inputs);
|
|
1263
|
-
if (unstagedRead) {
|
|
1264
|
-
return fail(unstagedRead);
|
|
1265
|
-
}
|
|
1266
|
-
const stagedUse = checkStagedImagesUsed(source, prepared.inputs, prepared.outputs);
|
|
1267
|
-
if (stagedUse) {
|
|
1268
|
-
return fail(stagedUse);
|
|
1269
|
-
}
|
|
1270
|
-
const turnStage = checkTurnImagesStaged(source, {
|
|
1271
|
-
inputs: prepared.inputs,
|
|
1272
|
-
outputs: prepared.outputs,
|
|
1273
|
-
turnImages
|
|
1274
|
-
});
|
|
1275
|
-
if (turnStage) {
|
|
1276
|
-
return fail(turnStage);
|
|
1277
|
-
}
|
|
1278
645
|
if (prepared.outputs?.length && !attach) {
|
|
1279
646
|
return fail('this device cannot persist produced files');
|
|
1280
647
|
}
|
|
@@ -1306,40 +673,16 @@ export function createPythonAdapter(disabled, loadPyodide, network = false) {
|
|
|
1306
673
|
hosts: gated ? namedHosts(source) : undefined,
|
|
1307
674
|
askConnect: gated ? confirmConnect : undefined
|
|
1308
675
|
});
|
|
1309
|
-
const soft = softFailedDeckHint(result.stdout, result.exitCode, source);
|
|
1310
|
-
if (soft) {
|
|
1311
|
-
return fail(soft);
|
|
1312
|
-
}
|
|
1313
|
-
const missingPic = stagedImagesMissingFromDeck(result, {
|
|
1314
|
-
inputs: prepared.inputs,
|
|
1315
|
-
outputs: prepared.outputs,
|
|
1316
|
-
source
|
|
1317
|
-
});
|
|
1318
|
-
if (missingPic) {
|
|
1319
|
-
return fail(missingPic);
|
|
1320
|
-
}
|
|
1321
676
|
const hint = packageFetchFailedHint(result.stderr) ??
|
|
1322
677
|
nativeWheelHint(result.stderr) ??
|
|
678
|
+
officeModuleHint(result.stderr, toolNames) ??
|
|
1323
679
|
missingModuleHint(result.stderr, { packages, sandbox, network: reach }) ??
|
|
1324
680
|
pipFromSourceHint(result.stderr, source, { packages, sandbox }) ??
|
|
1325
681
|
noProcessHint(result.stderr, sandbox) ??
|
|
1326
682
|
noNetworkHint(result.stderr, toolNames, reach) ??
|
|
1327
683
|
missingFileHint(result.stderr) ??
|
|
1328
|
-
rgbColorImportHint(result.stderr) ??
|
|
1329
|
-
nonePlaceholderHint(result.stderr) ??
|
|
1330
|
-
slideShapesHint(result.stderr) ??
|
|
1331
|
-
shapeFontHint(result.stderr) ??
|
|
1332
|
-
shapesBackgroundHint(result.stderr) ??
|
|
1333
|
-
paragraphFillHint(result.stderr) ??
|
|
1334
|
-
readOnlyPropertyHint(result.stderr) ??
|
|
1335
|
-
paragraphIndexHint(result.stderr, prepared.outputs ?? []) ??
|
|
1336
|
-
fpdfTextErrorHint(result.stderr) ??
|
|
1337
|
-
fpdfKeywordErrorHint(result.stderr) ??
|
|
1338
|
-
fpdfHorizontalSpaceHint(result.stderr) ??
|
|
1339
|
-
fpdfUnicodeErrorHint(result.stderr) ??
|
|
1340
684
|
shellNameHint(result.stderr) ??
|
|
1341
|
-
syntaxErrorHint(result.stderr, source)
|
|
1342
|
-
mangledPptxApiHint(source);
|
|
685
|
+
syntaxErrorHint(result.stderr, source);
|
|
1343
686
|
const hinted = hint ? { ...result, stderr: `${result.stderr}\n${hint}` } : result;
|
|
1344
687
|
const installed = install && result.exitCode === 0
|
|
1345
688
|
? {
|