@kortix/sandbox 0.4.1
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/config/customize.sh +143 -0
- package/config/kortix-env-setup.sh +25 -0
- package/kortix-master/package.json +22 -0
- package/kortix-master/src/config.ts +22 -0
- package/kortix-master/src/index.ts +44 -0
- package/kortix-master/src/routes/env.ts +65 -0
- package/kortix-master/src/routes/proxy.ts +108 -0
- package/kortix-master/src/routes/update.ts +185 -0
- package/kortix-master/src/services/proxy.ts +43 -0
- package/kortix-master/src/services/secret-store.ts +156 -0
- package/kortix-master/tsconfig.json +14 -0
- package/opencode/agents/kortix-browser.md +142 -0
- package/opencode/agents/kortix-build.md +62 -0
- package/opencode/agents/kortix-explore.md +66 -0
- package/opencode/agents/kortix-image-gen.md +33 -0
- package/opencode/agents/kortix-main.md +450 -0
- package/opencode/agents/kortix-plan.md +100 -0
- package/opencode/agents/kortix-research.md +84 -0
- package/opencode/agents/kortix-sheets.md +61 -0
- package/opencode/agents/kortix-slides.md +64 -0
- package/opencode/agents/kortix-web-dev.md +572 -0
- package/opencode/commands/email.md +36 -0
- package/opencode/commands/init.md +43 -0
- package/opencode/commands/journal.md +44 -0
- package/opencode/commands/memory-init.md +81 -0
- package/opencode/commands/memory-search.md +50 -0
- package/opencode/commands/memory-status.md +56 -0
- package/opencode/commands/research.md +36 -0
- package/opencode/commands/search.md +38 -0
- package/opencode/commands/slides.md +32 -0
- package/opencode/commands/spreadsheet.md +30 -0
- package/opencode/memory.json +37 -0
- package/opencode/ocx.jsonc +10 -0
- package/opencode/opencode.jsonc +103 -0
- package/opencode/package.json +25 -0
- package/opencode/patches/apply.sh +19 -0
- package/opencode/patches/opencode-pty-spawn.txt +49 -0
- package/opencode/plugin/background-agents.ts.disabled +483 -0
- package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
- package/opencode/plugin/kdco-primitives/index.ts +26 -0
- package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
- package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
- package/opencode/plugin/kdco-primitives/shell.ts +138 -0
- package/opencode/plugin/kdco-primitives/temp.ts +36 -0
- package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
- package/opencode/plugin/kdco-primitives/types.ts +13 -0
- package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
- package/opencode/plugin/memory.ts +306 -0
- package/opencode/plugin/worktree/state.ts +412 -0
- package/opencode/plugin/worktree/terminal.ts +1002 -0
- package/opencode/plugin/worktree.ts +861 -0
- package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
- package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
- package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
- package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
- package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
- package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
- package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
- package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
- package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
- package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
- package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
- package/opencode/skills/KORTIX-email/SKILL.md +145 -0
- package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
- package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
- package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
- package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
- package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
- package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
- package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
- package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
- package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
- package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
- package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
- package/opencode/skills/KORTIX-pdf/forms.md +36 -0
- package/opencode/skills/KORTIX-pdf/reference.md +105 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
- package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
- package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
- package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
- package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
- package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
- package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
- package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
- package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
- package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
- package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
- package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
- package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
- package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
- package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
- package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
- package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
- package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
- package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
- package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
- package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
- package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
- package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
- package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
- package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
- package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
- package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
- package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
- package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
- package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
- package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
- package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
- package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
- package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
- package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
- package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
- package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
- package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
- package/opencode/skills/KORTIX-session-search/Untitled +1 -0
- package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
- package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
- package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
- package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
- package/opencode/tools/image-gen.ts +342 -0
- package/opencode/tools/image-search.ts +190 -0
- package/opencode/tools/memory-get.ts +168 -0
- package/opencode/tools/memory-search.ts +247 -0
- package/opencode/tools/presentation-gen.ts +723 -0
- package/opencode/tools/scrape-webpage.ts +115 -0
- package/opencode/tools/scripts/.python-version +1 -0
- package/opencode/tools/scripts/convert_pdf.py +184 -0
- package/opencode/tools/scripts/convert_pptx.py +562 -0
- package/opencode/tools/scripts/pyproject.toml +11 -0
- package/opencode/tools/scripts/uv.lock +287 -0
- package/opencode/tools/scripts/validate_slide.py +74 -0
- package/opencode/tools/show-user.ts +217 -0
- package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
- package/opencode/tools/tests/image-gen.test.ts +215 -0
- package/opencode/tools/tests/image-search.test.ts +125 -0
- package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
- package/opencode/tools/tests/presentation-gen.test.ts +389 -0
- package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
- package/opencode/tools/tests/show-user.test.ts +241 -0
- package/opencode/tools/tests/video-gen.test.ts +110 -0
- package/opencode/tools/tests/web-search.test.ts +106 -0
- package/opencode/tools/video-gen.ts +200 -0
- package/opencode/tools/web-search.ts +153 -0
- package/opencode/tsconfig.json +29 -0
- package/package.json +36 -0
- package/patch-agent-browser.js +100 -0
- package/postinstall.sh +88 -0
- package/services/KORTIX-presentation-viewer/run +37 -0
- package/services/agent-browser-viewer/run +48 -0
- package/services/kortix-master/run +16 -0
- package/services/lss-sync/run +22 -0
- package/services/opencode-serve/run +25 -0
- package/services/opencode-web/run +21 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import {
|
|
3
|
+
writeFileSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
appendFileSync,
|
|
8
|
+
} from "fs";
|
|
9
|
+
import { resolve, dirname, join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
|
|
12
|
+
const SHOW_DIR = join(process.env.HOME || homedir(), ".show-user");
|
|
13
|
+
const QUEUE_FILE = `${SHOW_DIR}/queue.jsonl`;
|
|
14
|
+
|
|
15
|
+
interface ShowEntry {
|
|
16
|
+
id: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
type: "file" | "url" | "image" | "text" | "error";
|
|
19
|
+
title?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
path?: string;
|
|
22
|
+
url?: string;
|
|
23
|
+
content?: string;
|
|
24
|
+
metadata?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ensureShowDir(): void {
|
|
28
|
+
mkdirSync(SHOW_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function generateId(): string {
|
|
32
|
+
return `show_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function appendToQueue(entry: ShowEntry): void {
|
|
36
|
+
ensureShowDir();
|
|
37
|
+
appendFileSync(QUEUE_FILE, JSON.stringify(entry) + "\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readQueue(): ShowEntry[] {
|
|
41
|
+
if (!existsSync(QUEUE_FILE)) return [];
|
|
42
|
+
const content = readFileSync(QUEUE_FILE, "utf-8").trim();
|
|
43
|
+
if (!content) return [];
|
|
44
|
+
return content
|
|
45
|
+
.split("\n")
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.map((line) => {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(line) as ShowEntry;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
.filter((e): e is ShowEntry => e !== null);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function clearQueue(): void {
|
|
58
|
+
ensureShowDir();
|
|
59
|
+
writeFileSync(QUEUE_FILE, "");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default tool({
|
|
63
|
+
description:
|
|
64
|
+
"Show outputs and attachments to the human user. Use this tool to present files, images, URLs, " +
|
|
65
|
+
"text content, or error messages to the user. Items are appended to a queue that the user's UI " +
|
|
66
|
+
"reads from. Actions: 'show' (add item to queue), 'list' (list queued items), 'clear' (clear queue). " +
|
|
67
|
+
"Always use this after generating a deliverable (image, document, video, presentation, etc.) " +
|
|
68
|
+
"so the human can see and access it.",
|
|
69
|
+
args: {
|
|
70
|
+
action: tool.schema
|
|
71
|
+
.string()
|
|
72
|
+
.describe(
|
|
73
|
+
"Action to perform: 'show' (add item to queue), 'list' (list queued items), 'clear' (clear the queue)",
|
|
74
|
+
),
|
|
75
|
+
type: tool.schema
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe(
|
|
79
|
+
"Type of item to show. Required for 'show' action. " +
|
|
80
|
+
"Options: 'file' (any file on disk), 'image' (image file), 'url' (web link), " +
|
|
81
|
+
"'text' (inline text content), 'error' (error message for user)",
|
|
82
|
+
),
|
|
83
|
+
title: tool.schema
|
|
84
|
+
.string()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe(
|
|
87
|
+
"Short title for the item (displayed as heading). E.g. 'Generated Logo', 'Research Report'",
|
|
88
|
+
),
|
|
89
|
+
description: tool.schema
|
|
90
|
+
.string()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe(
|
|
93
|
+
"Optional longer description of the item. E.g. 'A 1024x1024 logo based on your brand colors'",
|
|
94
|
+
),
|
|
95
|
+
path: tool.schema
|
|
96
|
+
.string()
|
|
97
|
+
.optional()
|
|
98
|
+
.describe(
|
|
99
|
+
"Absolute path to a file on disk. Required when type is 'file' or 'image'. " +
|
|
100
|
+
"E.g. '/workspace/output/logo.png'",
|
|
101
|
+
),
|
|
102
|
+
url: tool.schema
|
|
103
|
+
.string()
|
|
104
|
+
.optional()
|
|
105
|
+
.describe(
|
|
106
|
+
"URL to show the user. Required when type is 'url'. " +
|
|
107
|
+
"E.g. 'https://example.com/report'",
|
|
108
|
+
),
|
|
109
|
+
content: tool.schema
|
|
110
|
+
.string()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe(
|
|
113
|
+
"Inline text content to show. Required when type is 'text' or 'error'. " +
|
|
114
|
+
"Supports markdown formatting.",
|
|
115
|
+
),
|
|
116
|
+
metadata: tool.schema
|
|
117
|
+
.string()
|
|
118
|
+
.optional()
|
|
119
|
+
.describe(
|
|
120
|
+
"Optional JSON string of extra metadata. E.g. '{\"width\":1024,\"format\":\"png\"}'",
|
|
121
|
+
),
|
|
122
|
+
},
|
|
123
|
+
async execute(args, _context) {
|
|
124
|
+
const action = args.action;
|
|
125
|
+
|
|
126
|
+
if (!["show", "list", "clear"].includes(action)) {
|
|
127
|
+
return `Error: Invalid action '${action}'. Use 'show', 'list', or 'clear'.`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- LIST ---
|
|
131
|
+
if (action === "list") {
|
|
132
|
+
const entries = readQueue();
|
|
133
|
+
if (entries.length === 0) {
|
|
134
|
+
return JSON.stringify(
|
|
135
|
+
{ success: true, action: "list", count: 0, items: [] },
|
|
136
|
+
null,
|
|
137
|
+
2,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return JSON.stringify(
|
|
141
|
+
{ success: true, action: "list", count: entries.length, items: entries },
|
|
142
|
+
null,
|
|
143
|
+
2,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- CLEAR ---
|
|
148
|
+
if (action === "clear") {
|
|
149
|
+
clearQueue();
|
|
150
|
+
return JSON.stringify(
|
|
151
|
+
{ success: true, action: "clear", message: "Queue cleared." },
|
|
152
|
+
null,
|
|
153
|
+
2,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- SHOW ---
|
|
158
|
+
const type = args.type as ShowEntry["type"] | undefined;
|
|
159
|
+
if (!type || !["file", "image", "url", "text", "error"].includes(type)) {
|
|
160
|
+
return `Error: 'type' is required for 'show' action. Use 'file', 'image', 'url', 'text', or 'error'.`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validate required fields per type
|
|
164
|
+
if ((type === "file" || type === "image") && !args.path) {
|
|
165
|
+
return `Error: 'path' is required when type is '${type}'.`;
|
|
166
|
+
}
|
|
167
|
+
if (type === "url" && !args.url) {
|
|
168
|
+
return `Error: 'url' is required when type is 'url'.`;
|
|
169
|
+
}
|
|
170
|
+
if ((type === "text" || type === "error") && !args.content) {
|
|
171
|
+
return `Error: 'content' is required when type is '${type}'.`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Verify file exists for file/image types
|
|
175
|
+
if ((type === "file" || type === "image") && args.path) {
|
|
176
|
+
const absPath = resolve(args.path);
|
|
177
|
+
if (!existsSync(absPath)) {
|
|
178
|
+
return `Error: File not found: ${absPath}`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Parse optional metadata
|
|
183
|
+
let metadata: Record<string, unknown> | undefined;
|
|
184
|
+
if (args.metadata) {
|
|
185
|
+
try {
|
|
186
|
+
metadata = JSON.parse(args.metadata);
|
|
187
|
+
} catch {
|
|
188
|
+
return `Error: Invalid JSON in 'metadata' parameter.`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const entry: ShowEntry = {
|
|
193
|
+
id: generateId(),
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
type,
|
|
196
|
+
...(args.title && { title: args.title }),
|
|
197
|
+
...(args.description && { description: args.description }),
|
|
198
|
+
...(args.path && { path: resolve(args.path) }),
|
|
199
|
+
...(args.url && { url: args.url }),
|
|
200
|
+
...(args.content && { content: args.content }),
|
|
201
|
+
...(metadata && { metadata }),
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
appendToQueue(entry);
|
|
205
|
+
|
|
206
|
+
return JSON.stringify(
|
|
207
|
+
{
|
|
208
|
+
success: true,
|
|
209
|
+
action: "show",
|
|
210
|
+
entry,
|
|
211
|
+
message: `Item '${args.title || type}' added to show queue.`,
|
|
212
|
+
},
|
|
213
|
+
null,
|
|
214
|
+
2,
|
|
215
|
+
);
|
|
216
|
+
},
|
|
217
|
+
});
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E test for the presentation-gen EACCES fix.
|
|
3
|
+
*
|
|
4
|
+
* Simulates the exact Docker sandbox scenario:
|
|
5
|
+
* - _context.worktree = "/" (no git repo)
|
|
6
|
+
* - _context.directory = process.cwd() (e.g. /workspace)
|
|
7
|
+
* - NO output_dir provided
|
|
8
|
+
*
|
|
9
|
+
* Verifies that slides are created under cwd, not under "/presentations".
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, rmSync, readFileSync } from "fs";
|
|
12
|
+
import { resolve, join } from "path";
|
|
13
|
+
|
|
14
|
+
// ── Helpers ──
|
|
15
|
+
|
|
16
|
+
function pass(name: string) {
|
|
17
|
+
process.stdout.write(` PASS ${name}\n`);
|
|
18
|
+
}
|
|
19
|
+
function fail(name: string, reason: string) {
|
|
20
|
+
process.stdout.write(` FAIL ${name}: ${reason}\n`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
function assert(cond: boolean, name: string, reason: string) {
|
|
24
|
+
if (cond) pass(name);
|
|
25
|
+
else fail(name, reason);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Setup ──
|
|
29
|
+
|
|
30
|
+
const TEST_DIR = resolve(import.meta.dir, "e2e-output");
|
|
31
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
32
|
+
|
|
33
|
+
// Import the tool
|
|
34
|
+
const presGen = (await import("../presentation-gen.ts")).default;
|
|
35
|
+
|
|
36
|
+
process.stdout.write("\n=== E2E: presentation-gen EACCES fix ===\n\n");
|
|
37
|
+
|
|
38
|
+
// ── Test 1: worktree="/" without output_dir should NOT try to mkdir /presentations ──
|
|
39
|
+
|
|
40
|
+
process.stdout.write("Test 1: worktree='/' falls back to _context.directory\n");
|
|
41
|
+
{
|
|
42
|
+
const ctx = { directory: TEST_DIR, worktree: "/" } as Parameters<
|
|
43
|
+
typeof presGen.execute
|
|
44
|
+
>[1];
|
|
45
|
+
|
|
46
|
+
const raw = await presGen.execute(
|
|
47
|
+
{
|
|
48
|
+
action: "create_slide",
|
|
49
|
+
presentation_name: "e2e_test",
|
|
50
|
+
slide_number: 1,
|
|
51
|
+
slide_title: "Title Slide",
|
|
52
|
+
content:
|
|
53
|
+
'<div style="width:1920px;height:1080px;background:#111;display:flex;align-items:center;justify-content:center;"><h1 style="color:#fff;font-size:64px;">E2E Test</h1></div>',
|
|
54
|
+
presentation_title: "E2E Test Presentation",
|
|
55
|
+
// NOTE: no output_dir — this is the bug scenario
|
|
56
|
+
},
|
|
57
|
+
ctx,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const result = JSON.parse(raw as string);
|
|
61
|
+
assert(result.success === true, "create_slide succeeds", `got: ${raw}`);
|
|
62
|
+
assert(
|
|
63
|
+
result.presentation_path === "presentations/e2e_test",
|
|
64
|
+
"presentation_path is relative",
|
|
65
|
+
`got: ${result.presentation_path}`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Verify the file was created under TEST_DIR, not under /
|
|
69
|
+
const slidePath = join(TEST_DIR, result.slide_file);
|
|
70
|
+
assert(
|
|
71
|
+
existsSync(slidePath),
|
|
72
|
+
`slide file exists at ${slidePath}`,
|
|
73
|
+
"file not found",
|
|
74
|
+
);
|
|
75
|
+
assert(
|
|
76
|
+
!existsSync("/presentations/e2e_test"),
|
|
77
|
+
"nothing created at /presentations/e2e_test",
|
|
78
|
+
"files were incorrectly created at filesystem root!",
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Verify HTML content
|
|
82
|
+
const html = readFileSync(slidePath, "utf-8");
|
|
83
|
+
assert(html.includes("1920"), "slide has 1920 viewport", "missing viewport");
|
|
84
|
+
assert(html.includes("E2E Test"), "slide has content", "missing content");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Test 2: Create multiple slides in "parallel" (sequential but no output_dir) ──
|
|
88
|
+
|
|
89
|
+
process.stdout.write("\nTest 2: Multiple slides without output_dir\n");
|
|
90
|
+
{
|
|
91
|
+
const ctx = { directory: TEST_DIR, worktree: "/" } as Parameters<
|
|
92
|
+
typeof presGen.execute
|
|
93
|
+
>[1];
|
|
94
|
+
|
|
95
|
+
for (let i = 2; i <= 5; i++) {
|
|
96
|
+
const raw = await presGen.execute(
|
|
97
|
+
{
|
|
98
|
+
action: "create_slide",
|
|
99
|
+
presentation_name: "e2e_test",
|
|
100
|
+
slide_number: i,
|
|
101
|
+
slide_title: `Slide ${i}`,
|
|
102
|
+
content: `<div style="width:1920px;height:1080px;background:#222;padding:60px;box-sizing:border-box;"><h2 style="color:#0ff;font-size:48px;">Slide ${i}</h2></div>`,
|
|
103
|
+
presentation_title: "E2E Test Presentation",
|
|
104
|
+
},
|
|
105
|
+
ctx,
|
|
106
|
+
);
|
|
107
|
+
const result = JSON.parse(raw as string);
|
|
108
|
+
assert(
|
|
109
|
+
result.success === true,
|
|
110
|
+
`slide ${i} created`,
|
|
111
|
+
`failed: ${raw}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Verify all 5 slides exist
|
|
116
|
+
const listRaw = await presGen.execute(
|
|
117
|
+
{ action: "list_slides", presentation_name: "e2e_test" },
|
|
118
|
+
ctx,
|
|
119
|
+
);
|
|
120
|
+
const listResult = JSON.parse(listRaw as string);
|
|
121
|
+
assert(
|
|
122
|
+
listResult.total_slides === 5,
|
|
123
|
+
`list_slides returns 5`,
|
|
124
|
+
`got ${listResult.total_slides}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Test 3: images/ directory created correctly ──
|
|
129
|
+
|
|
130
|
+
process.stdout.write("\nTest 3: images/ directory created under correct path\n");
|
|
131
|
+
{
|
|
132
|
+
const imagesDir = join(TEST_DIR, "presentations", "images");
|
|
133
|
+
assert(existsSync(imagesDir), "images/ dir exists under TEST_DIR", "not found");
|
|
134
|
+
assert(
|
|
135
|
+
!existsSync("/presentations/images"),
|
|
136
|
+
"no images/ at filesystem root",
|
|
137
|
+
"images dir incorrectly at /",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Test 4: metadata.json is correct ──
|
|
142
|
+
|
|
143
|
+
process.stdout.write("\nTest 4: metadata.json structure\n");
|
|
144
|
+
{
|
|
145
|
+
const metaPath = join(TEST_DIR, "presentations", "e2e_test", "metadata.json");
|
|
146
|
+
assert(existsSync(metaPath), "metadata.json exists", "not found");
|
|
147
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
148
|
+
assert(
|
|
149
|
+
meta.presentation_name === "e2e_test",
|
|
150
|
+
"presentation_name correct",
|
|
151
|
+
`got: ${meta.presentation_name}`,
|
|
152
|
+
);
|
|
153
|
+
assert(
|
|
154
|
+
meta.title === "E2E Test Presentation",
|
|
155
|
+
"title correct",
|
|
156
|
+
`got: ${meta.title}`,
|
|
157
|
+
);
|
|
158
|
+
assert(
|
|
159
|
+
Object.keys(meta.slides).length === 5,
|
|
160
|
+
"5 slides in metadata",
|
|
161
|
+
`got: ${Object.keys(meta.slides).length}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Test 5: viewer.html generated ──
|
|
166
|
+
|
|
167
|
+
process.stdout.write("\nTest 5: viewer.html generated\n");
|
|
168
|
+
{
|
|
169
|
+
const viewerPath = join(TEST_DIR, "presentations", "e2e_test", "viewer.html");
|
|
170
|
+
// viewer.html depends on the skill template existing — might not in test env
|
|
171
|
+
if (existsSync(viewerPath)) {
|
|
172
|
+
pass("viewer.html exists");
|
|
173
|
+
} else {
|
|
174
|
+
process.stdout.write(" SKIP viewer.html (skill template not available in test env)\n");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Test 6: list_presentations works ──
|
|
179
|
+
|
|
180
|
+
process.stdout.write("\nTest 6: list_presentations\n");
|
|
181
|
+
{
|
|
182
|
+
const ctx = { directory: TEST_DIR, worktree: "/" } as Parameters<
|
|
183
|
+
typeof presGen.execute
|
|
184
|
+
>[1];
|
|
185
|
+
const raw = await presGen.execute(
|
|
186
|
+
{ action: "list_presentations" },
|
|
187
|
+
ctx,
|
|
188
|
+
);
|
|
189
|
+
const result = JSON.parse(raw as string);
|
|
190
|
+
assert(result.success === true, "list succeeds", `got: ${raw}`);
|
|
191
|
+
assert(
|
|
192
|
+
result.total_count === 1,
|
|
193
|
+
"1 presentation found",
|
|
194
|
+
`got: ${result.total_count}`,
|
|
195
|
+
);
|
|
196
|
+
assert(
|
|
197
|
+
result.presentations[0].total_slides === 5,
|
|
198
|
+
"presentation has 5 slides",
|
|
199
|
+
`got: ${result.presentations[0].total_slides}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Test 7: delete_slide works ──
|
|
204
|
+
|
|
205
|
+
process.stdout.write("\nTest 7: delete_slide\n");
|
|
206
|
+
{
|
|
207
|
+
const ctx = { directory: TEST_DIR, worktree: "/" } as Parameters<
|
|
208
|
+
typeof presGen.execute
|
|
209
|
+
>[1];
|
|
210
|
+
const raw = await presGen.execute(
|
|
211
|
+
{ action: "delete_slide", presentation_name: "e2e_test", slide_number: 5 },
|
|
212
|
+
ctx,
|
|
213
|
+
);
|
|
214
|
+
const result = JSON.parse(raw as string);
|
|
215
|
+
assert(result.success === true, "delete succeeds", `got: ${raw}`);
|
|
216
|
+
assert(
|
|
217
|
+
result.remaining_slides === 4,
|
|
218
|
+
"4 slides remaining",
|
|
219
|
+
`got: ${result.remaining_slides}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Test 8: delete_presentation works ──
|
|
224
|
+
|
|
225
|
+
process.stdout.write("\nTest 8: delete_presentation\n");
|
|
226
|
+
{
|
|
227
|
+
const ctx = { directory: TEST_DIR, worktree: "/" } as Parameters<
|
|
228
|
+
typeof presGen.execute
|
|
229
|
+
>[1];
|
|
230
|
+
const raw = await presGen.execute(
|
|
231
|
+
{ action: "delete_presentation", presentation_name: "e2e_test" },
|
|
232
|
+
ctx,
|
|
233
|
+
);
|
|
234
|
+
const result = JSON.parse(raw as string);
|
|
235
|
+
assert(result.success === true, "delete presentation succeeds", `got: ${raw}`);
|
|
236
|
+
|
|
237
|
+
const presDir = join(TEST_DIR, "presentations", "e2e_test");
|
|
238
|
+
assert(!existsSync(presDir), "presentation dir removed", "still exists");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Test 9: worktree with valid path still works (non-"/" case) ──
|
|
242
|
+
|
|
243
|
+
process.stdout.write("\nTest 9: valid worktree still works\n");
|
|
244
|
+
{
|
|
245
|
+
const ctx = { directory: "/tmp", worktree: TEST_DIR } as Parameters<
|
|
246
|
+
typeof presGen.execute
|
|
247
|
+
>[1];
|
|
248
|
+
const raw = await presGen.execute(
|
|
249
|
+
{
|
|
250
|
+
action: "create_slide",
|
|
251
|
+
presentation_name: "worktree_pres",
|
|
252
|
+
slide_number: 1,
|
|
253
|
+
slide_title: "Valid Worktree",
|
|
254
|
+
content: '<div style="width:1920px;height:1080px;background:#333;"><h1 style="color:#fff;">Worktree Test</h1></div>',
|
|
255
|
+
presentation_title: "Worktree Test",
|
|
256
|
+
},
|
|
257
|
+
ctx,
|
|
258
|
+
);
|
|
259
|
+
const result = JSON.parse(raw as string);
|
|
260
|
+
assert(result.success === true, "create with valid worktree", `got: ${raw}`);
|
|
261
|
+
// Should use worktree (TEST_DIR), not directory (/tmp)
|
|
262
|
+
const slidePath = join(TEST_DIR, result.slide_file);
|
|
263
|
+
assert(existsSync(slidePath), "file under worktree dir", "not found");
|
|
264
|
+
assert(
|
|
265
|
+
!existsSync(join("/tmp", result.slide_file)),
|
|
266
|
+
"not under /tmp",
|
|
267
|
+
"incorrectly used directory instead of worktree",
|
|
268
|
+
);
|
|
269
|
+
// Cleanup
|
|
270
|
+
rmSync(join(TEST_DIR, "presentations", "worktree_pres"), { recursive: true, force: true });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Cleanup ──
|
|
274
|
+
|
|
275
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
276
|
+
|
|
277
|
+
process.stdout.write("\n=== All E2E tests passed! ===\n\n");
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { readFileSync, existsSync, rmSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
|
|
4
|
+
const envPath = resolve(import.meta.dir, "../../.env");
|
|
5
|
+
for (const line of readFileSync(envPath, "utf-8").split("\n")) {
|
|
6
|
+
const trimmed = line.trim();
|
|
7
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
8
|
+
const eq = trimmed.indexOf("=");
|
|
9
|
+
if (eq > 0) process.env[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const OUTPUT_DIR = resolve(import.meta.dir, "test-output");
|
|
13
|
+
|
|
14
|
+
async function test(name: string, fn: () => Promise<void>) {
|
|
15
|
+
process.stdout.write(`\n=== ${name} ===\n`);
|
|
16
|
+
try {
|
|
17
|
+
await fn();
|
|
18
|
+
process.stdout.write(`PASS\n`);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
process.stdout.write(`FAIL: ${e}\n`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assert(cond: boolean, msg: string) {
|
|
26
|
+
if (!cond) throw new Error(msg);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const imageGen = (await import("../image-gen.ts")).default;
|
|
30
|
+
|
|
31
|
+
const ctx = { directory: process.cwd(), worktree: process.cwd() } as Parameters<
|
|
32
|
+
typeof imageGen.execute
|
|
33
|
+
>[1];
|
|
34
|
+
|
|
35
|
+
// ── Validation Tests (no API calls) ──
|
|
36
|
+
|
|
37
|
+
await test("image_gen: missing API key", async () => {
|
|
38
|
+
const saved = process.env.REPLICATE_API_TOKEN;
|
|
39
|
+
delete process.env.REPLICATE_API_TOKEN;
|
|
40
|
+
const raw = await imageGen.execute(
|
|
41
|
+
{ action: "generate", prompt: "test" },
|
|
42
|
+
ctx,
|
|
43
|
+
);
|
|
44
|
+
process.env.REPLICATE_API_TOKEN = saved;
|
|
45
|
+
assert(
|
|
46
|
+
(raw as string).includes("REPLICATE_API_TOKEN"),
|
|
47
|
+
"should mention missing key",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await test("image_gen: invalid action", async () => {
|
|
52
|
+
const raw = await imageGen.execute({ action: "dance" }, ctx);
|
|
53
|
+
assert(
|
|
54
|
+
(raw as string).includes("Invalid action"),
|
|
55
|
+
"should reject invalid action",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await test("image_gen: generate without prompt", async () => {
|
|
60
|
+
const raw = await imageGen.execute({ action: "generate" }, ctx);
|
|
61
|
+
assert((raw as string).includes("prompt"), "should require prompt");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await test("image_gen: edit without image_path", async () => {
|
|
65
|
+
const raw = await imageGen.execute(
|
|
66
|
+
{
|
|
67
|
+
action: "edit",
|
|
68
|
+
prompt: "make it blue",
|
|
69
|
+
},
|
|
70
|
+
ctx,
|
|
71
|
+
);
|
|
72
|
+
assert((raw as string).includes("image_path"), "should require image_path");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await test("image_gen: upscale without image_path", async () => {
|
|
76
|
+
const raw = await imageGen.execute({ action: "upscale" }, ctx);
|
|
77
|
+
assert((raw as string).includes("image_path"), "should require image_path");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await test("image_gen: remove_bg without image_path", async () => {
|
|
81
|
+
const raw = await imageGen.execute({ action: "remove_bg" }, ctx);
|
|
82
|
+
assert((raw as string).includes("image_path"), "should require image_path");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── Live API Tests ──
|
|
86
|
+
|
|
87
|
+
let generatedImagePath = "";
|
|
88
|
+
|
|
89
|
+
await test("image_gen: generate image", async () => {
|
|
90
|
+
const raw = await imageGen.execute(
|
|
91
|
+
{
|
|
92
|
+
action: "generate",
|
|
93
|
+
prompt: "a simple red circle on white background",
|
|
94
|
+
size: "1024x1024",
|
|
95
|
+
quality: "low",
|
|
96
|
+
output_dir: OUTPUT_DIR,
|
|
97
|
+
},
|
|
98
|
+
ctx,
|
|
99
|
+
);
|
|
100
|
+
const result = JSON.parse(raw as string);
|
|
101
|
+
assert(result.success === true, `expected success: ${raw}`);
|
|
102
|
+
assert(typeof result.output_path === "string", "should have output_path");
|
|
103
|
+
assert(
|
|
104
|
+
existsSync(result.output_path),
|
|
105
|
+
`file should exist: ${result.output_path}`,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const fileSize = readFileSync(result.output_path).length;
|
|
109
|
+
assert(fileSize > 1000, `file too small (${fileSize} bytes), likely empty`);
|
|
110
|
+
|
|
111
|
+
generatedImagePath = result.output_path;
|
|
112
|
+
process.stdout.write(` saved: ${result.output_path} (${fileSize} bytes)\n`);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await test("image_gen: edit image", async () => {
|
|
116
|
+
assert(
|
|
117
|
+
generatedImagePath.length > 0,
|
|
118
|
+
"need generated image from previous test",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const raw = await imageGen.execute(
|
|
122
|
+
{
|
|
123
|
+
action: "edit",
|
|
124
|
+
prompt: "change the red circle to a blue square",
|
|
125
|
+
image_path: generatedImagePath,
|
|
126
|
+
quality: "low",
|
|
127
|
+
output_dir: OUTPUT_DIR,
|
|
128
|
+
},
|
|
129
|
+
ctx,
|
|
130
|
+
);
|
|
131
|
+
const result = JSON.parse(raw as string);
|
|
132
|
+
assert(result.success === true, `expected success: ${raw}`);
|
|
133
|
+
assert(existsSync(result.output_path), "edited file should exist");
|
|
134
|
+
|
|
135
|
+
const fileSize = readFileSync(result.output_path).length;
|
|
136
|
+
process.stdout.write(` saved: ${result.output_path} (${fileSize} bytes)\n`);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await test("image_gen: upscale image", async () => {
|
|
140
|
+
assert(
|
|
141
|
+
generatedImagePath.length > 0,
|
|
142
|
+
"need generated image from previous test",
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const raw = await imageGen.execute(
|
|
146
|
+
{
|
|
147
|
+
action: "upscale",
|
|
148
|
+
image_path: generatedImagePath,
|
|
149
|
+
output_dir: OUTPUT_DIR,
|
|
150
|
+
},
|
|
151
|
+
ctx,
|
|
152
|
+
);
|
|
153
|
+
const result = JSON.parse(raw as string);
|
|
154
|
+
assert(result.success === true, `expected success: ${raw}`);
|
|
155
|
+
assert(existsSync(result.output_path), "upscaled file should exist");
|
|
156
|
+
|
|
157
|
+
const origSize = readFileSync(generatedImagePath).length;
|
|
158
|
+
const upscaledSize = readFileSync(result.output_path).length;
|
|
159
|
+
process.stdout.write(
|
|
160
|
+
` original: ${origSize} bytes -> upscaled: ${upscaledSize} bytes\n`,
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await test("image_gen: remove background", async () => {
|
|
165
|
+
assert(
|
|
166
|
+
generatedImagePath.length > 0,
|
|
167
|
+
"need generated image from previous test",
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const raw = await imageGen.execute(
|
|
171
|
+
{
|
|
172
|
+
action: "remove_bg",
|
|
173
|
+
image_path: generatedImagePath,
|
|
174
|
+
output_dir: OUTPUT_DIR,
|
|
175
|
+
},
|
|
176
|
+
ctx,
|
|
177
|
+
);
|
|
178
|
+
const result = JSON.parse(raw as string);
|
|
179
|
+
assert(result.success === true, `expected success: ${raw}`);
|
|
180
|
+
assert(existsSync(result.output_path), "bg-removed file should exist");
|
|
181
|
+
assert(
|
|
182
|
+
result.output_path.endsWith(".png"),
|
|
183
|
+
"bg-removed should be PNG (transparency)",
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const fileSize = readFileSync(result.output_path).length;
|
|
187
|
+
process.stdout.write(` saved: ${result.output_path} (${fileSize} bytes)\n`);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await test("image_gen: nonexistent input image", async () => {
|
|
191
|
+
const raw = await imageGen.execute(
|
|
192
|
+
{
|
|
193
|
+
action: "upscale",
|
|
194
|
+
image_path: "/tmp/does-not-exist-12345.png",
|
|
195
|
+
output_dir: OUTPUT_DIR,
|
|
196
|
+
},
|
|
197
|
+
ctx,
|
|
198
|
+
);
|
|
199
|
+
const result = JSON.parse(raw as string);
|
|
200
|
+
assert(result.success === false, "should fail for nonexistent image");
|
|
201
|
+
assert(
|
|
202
|
+
result.error.includes("not found") || result.error.includes("not exist"),
|
|
203
|
+
"should mention file not found",
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ── Cleanup (skip with --keep-output) ──
|
|
208
|
+
|
|
209
|
+
if (!process.argv.includes("--keep-output")) {
|
|
210
|
+
try {
|
|
211
|
+
rmSync(OUTPUT_DIR, { recursive: true, force: true });
|
|
212
|
+
} catch {}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
process.stdout.write("\n\nAll image-gen tests passed!\n");
|