@thijshakkenberg/valkyrie-mom-mcp 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +24 -4
  3. package/agents/scenario-designer.md +7 -2
  4. package/dist/artwork/comfyui.d.ts +146 -0
  5. package/dist/artwork/comfyui.d.ts.map +1 -0
  6. package/dist/artwork/comfyui.js +214 -0
  7. package/dist/artwork/comfyui.js.map +1 -0
  8. package/dist/io/package-manifest.d.ts +18 -0
  9. package/dist/io/package-manifest.d.ts.map +1 -0
  10. package/dist/io/package-manifest.js +57 -0
  11. package/dist/io/package-manifest.js.map +1 -0
  12. package/dist/resources/format-docs.d.ts +1 -1
  13. package/dist/resources/format-docs.d.ts.map +1 -1
  14. package/dist/resources/format-docs.js +17 -9
  15. package/dist/resources/format-docs.js.map +1 -1
  16. package/dist/server.d.ts.map +1 -1
  17. package/dist/server.js +68 -7
  18. package/dist/server.js.map +1 -1
  19. package/dist/validation/rules/custom-images.d.ts.map +1 -1
  20. package/dist/validation/rules/custom-images.js +24 -2
  21. package/dist/validation/rules/custom-images.js.map +1 -1
  22. package/dist/validation/rules/package-name.d.ts +9 -0
  23. package/dist/validation/rules/package-name.d.ts.map +1 -0
  24. package/dist/validation/rules/package-name.js +32 -0
  25. package/dist/validation/rules/package-name.js.map +1 -0
  26. package/dist/validation/rules/puzzles.d.ts +15 -0
  27. package/dist/validation/rules/puzzles.d.ts.map +1 -0
  28. package/dist/validation/rules/puzzles.js +76 -0
  29. package/dist/validation/rules/puzzles.js.map +1 -0
  30. package/dist/validation/validator.d.ts.map +1 -1
  31. package/dist/validation/validator.js +4 -0
  32. package/dist/validation/validator.js.map +1 -1
  33. package/package.json +1 -1
  34. package/skills/artwork/SKILL.md +120 -0
  35. package/skills/scenario/SKILL.md +4 -1
  36. package/skills/ui-and-puzzles/SKILL.md +50 -133
  37. package/src/artwork/comfyui.ts +289 -0
  38. package/src/io/package-manifest.ts +59 -0
  39. package/src/resources/format-docs.ts +17 -9
  40. package/src/server.ts +85 -7
  41. package/src/validation/rules/custom-images.ts +23 -2
  42. package/src/validation/rules/package-name.ts +33 -0
  43. package/src/validation/rules/puzzles.ts +79 -0
  44. package/src/validation/validator.ts +4 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valkyrie-mom",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "AI-assisted Mansions of Madness scenario editor for Valkyrie",
5
5
  "author": {
6
6
  "name": "Thijs Hakkenberg"
package/README.md CHANGED
@@ -8,7 +8,7 @@ MCP (Model Context Protocol) server and Claude Code plugin for AI-assisted Mansi
8
8
 
9
9
  This server exposes Valkyrie scenario editing as MCP tools, enabling AI assistants (Claude, etc.) to create, modify, validate, and build complete MoM scenarios. It auto-detects the Valkyrie editor directory so scenarios appear directly in the app.
10
10
 
11
- The plugin bundles 7 skills covering advanced patterns (event loops, mythos scaling, tile placement chains, custom puzzles, etc.) and an autonomous scenario designer agent.
11
+ The plugin bundles 8 skills covering advanced patterns (event loops, mythos scaling, tile placement chains, custom puzzles, generated artwork, etc.) and an autonomous scenario designer agent.
12
12
 
13
13
  ## Install
14
14
 
@@ -21,7 +21,7 @@ claude plugin install valkyrie-mom
21
21
 
22
22
  This gives you:
23
23
  - **MCP server** with 25 tools for scenario editing (auto-started)
24
- - **7 skills**: `/scenario`, `/event-patterns`, `/tile-placement`, `/variables-and-mythos`, `/custom-monsters`, `/ui-and-puzzles`, `/items-and-distribution`
24
+ - **8 skills**: `/scenario`, `/event-patterns`, `/tile-placement`, `/variables-and-mythos`, `/custom-monsters`, `/ui-and-puzzles`, `/items-and-distribution`, `/artwork`
25
25
  - **Scenario designer agent** for autonomous scenario creation
26
26
  - **5 MCP resources** for format documentation
27
27
 
@@ -73,6 +73,7 @@ npx tsx src/index.ts # Run MCP server via stdio
73
73
  | `/custom-monsters` | Custom activations, evade/horror events, round-based spawn triggering |
74
74
  | `/ui-and-puzzles` | Prologues, interactive journals, combination locks, built-in puzzle types |
75
75
  | `/items-and-distribution` | Random items, unique items, starting items, inspection events |
76
+ | `/artwork` | Generate pictures with a local ComfyUI (FLUX.2 [klein] 4B) and show them: cover, intro cutscene, handouts, scenes, monsters, tokens, image puzzles |
76
77
 
77
78
  ## MCP Tools
78
79
 
@@ -84,7 +85,8 @@ npx tsx src/index.ts # Run MCP server via stdio
84
85
  | `load_scenario` | Load an existing scenario from a directory |
85
86
  | `get_scenario_state` | Get current scenario component/localization summary |
86
87
  | `validate_scenario` | Run all validation rules |
87
- | `build_scenario` | Save and build `.valkyrie` package |
88
+ | `save_scenario` | Write the scenario to its folder (edits stay in memory until saved or built) |
89
+ | `build_scenario` | Save and build the `.valkyrie` package with Valkyrie's manifest `.ini` and cover image (default: `Desktop/<Package>/`, like Valkyrie's Create Package) |
88
90
  | `set_quest_config` | Set difficulty, play time, investigator count, cover image |
89
91
 
90
92
  ### Components
@@ -111,6 +113,24 @@ npx tsx src/index.ts # Run MCP server via stdio
111
113
  | `suggest_tile_layout` | Suggest coordinates for large (7x7) tiles in linear, L-shape, or hub-spoke layouts |
112
114
  | `place_tile_relative` | Position and rotation for a new tile against an existing one, with a door lined up |
113
115
 
116
+ ### Artwork
117
+ | Tool | Description |
118
+ |------|-------------|
119
+ | `artwork_status` | Check a local ComfyUI server and the FLUX.2 [klein] 4B model files; prints comfy-cli setup and download commands for anything missing |
120
+ | `generate_artwork` | Generate an image (4 steps, CFG 1, size presets per use) and save it into the scenario folder, with a preview |
121
+
122
+ Artwork is optional and runs entirely on your machine. Setup with [comfy-cli](https://github.com/Comfy-Org/comfy-cli):
123
+
124
+ ```bash
125
+ pip install comfy-cli && comfy install
126
+ comfy model download --url https://huggingface.co/Comfy-Org/flux2-klein-4B/resolve/main/split_files/diffusion_models/flux-2-klein-4b.safetensors --relative-path models/diffusion_models
127
+ comfy model download --url https://huggingface.co/Comfy-Org/flux2-klein-4B/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors --relative-path models/text_encoders
128
+ comfy model download --url https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/vae/flux2-vae.safetensors --relative-path models/vae
129
+ comfy launch --background
130
+ ```
131
+
132
+ On NVIDIA GPUs with less VRAM, the fp8 model (`black-forest-labs/FLUX.2-klein-4b-fp8`) is smaller. Set `VALKYRIE_COMFYUI_URL` if ComfyUI runs somewhere other than `http://127.0.0.1:8188`. The `/artwork` skill covers prompts and where Valkyrie shows images.
133
+
114
134
  ### Reference
115
135
  | Tool | Description |
116
136
  |------|-------------|
@@ -201,7 +221,7 @@ npm run build # Compile to dist/
201
221
 
202
222
  ```
203
223
  .claude-plugin/ Plugin manifest
204
- skills/ 7 skill SKILL.md files
224
+ skills/ 8 skill SKILL.md files
205
225
  agents/ Scenario designer agent
206
226
  src/
207
227
  io/ INI parser/writer, localization CSV, ZIP packager
@@ -27,6 +27,7 @@ skills:
27
27
  - custom-monsters
28
28
  - ui-and-puzzles
29
29
  - items-and-distribution
30
+ - artwork
30
31
  ---
31
32
 
32
33
  # Scenario Designer Agent
@@ -40,6 +41,7 @@ You are an autonomous Mansions of Madness 2nd Edition scenario designer. You cre
40
41
  - Place tokens for exploration, searches, and encounters
41
42
  - Configure monster spawns with appropriate difficulty scaling
42
43
  - Build custom puzzles and UI overlays
44
+ - Generate scenario artwork (cover, intro, handouts) when a local ComfyUI is available (`artwork_status`)
43
45
  - Set up mythos scaling for tension progression
44
46
  - Write atmospheric narrative text in the Lovecraftian horror style
45
47
  - Validate scenarios for correctness and completeness
@@ -54,6 +56,7 @@ Use these skills for detailed pattern guidance:
54
56
  - `/custom-monsters` — Custom activations, evade/horror events, spawn triggering
55
57
  - `/ui-and-puzzles` — Prologues, interactive journals, combination locks, built-in puzzles
56
58
  - `/items-and-distribution` — Random items, unique items, starting items, inspection events
59
+ - `/artwork` — Generate pictures with ComfyUI (FLUX.2 [klein]) and show them: cover, intro cutscene, handouts, scenes, monsters, tokens, image puzzles
57
60
 
58
61
  ## Design Principles
59
62
 
@@ -93,7 +96,8 @@ Use these skills for detailed pattern guidance:
93
96
  - Use `{rnd:hero}` for random hero selection, `{c:EventName}` for recall
94
97
 
95
98
  ### UI & Puzzles
96
- - Always use `vunits=True` for resolution independence
99
+ - Always use `vunits=True` for resolution independence; positions are offsets from the screen centre
100
+ - A puzzle is an event started from a button (`event1=PuzzleX`); tell its story in the event before it
97
101
  - Layering order matters: background first, buttons LAST
98
102
  - Clean up all UI elements when dismissing overlays
99
103
 
@@ -109,7 +113,8 @@ Use these skills for detailed pattern guidance:
109
113
  8. Configure items with `upsert_item`
110
114
  9. Write all narrative with `set_localization`
111
115
  10. Validate with `validate_scenario`
112
- 11. Build with `build_scenario`
116
+ 11. Save with `save_scenario` (edits stay in memory until saved or built)
117
+ 12. Build with `build_scenario`
113
118
 
114
119
  ## Available MCP Tools
115
120
 
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Scenario artwork through a local ComfyUI server running FLUX.2 [klein] 4B.
3
+ * The workflow is ComfyUI's API format; the settings are the ones the distilled
4
+ * model is built for (4 steps, CFG 1, euler), which take ~25 s per image on an M1 Pro
5
+ * once the model is loaded (the first image also loads ~15 GB of weights).
6
+ */
7
+ export declare const DEFAULT_COMFYUI_URL: string;
8
+ /** Image sizes per use in a scenario (width x height, multiples of 16) */
9
+ export declare const ARTWORK_PRESETS: {
10
+ /** Scenario list picture (quest.ini image=) and the intro cutscene's left panel */
11
+ readonly cover: {
12
+ readonly width: 896;
13
+ readonly height: 896;
14
+ };
15
+ readonly intro: {
16
+ readonly width: 896;
17
+ readonly height: 896;
18
+ };
19
+ /** Letters, parchments, photographs, journal pages: portrait handouts shown during play */
20
+ readonly handout: {
21
+ readonly width: 768;
22
+ readonly height: 1024;
23
+ };
24
+ /** Landscape pictures shown during play (a room, a revelation, the ending) */
25
+ readonly scene: {
26
+ readonly width: 1024;
27
+ readonly height: 768;
28
+ };
29
+ /** Custom monster portrait (CustomMonster image=) */
30
+ readonly monster: {
31
+ readonly width: 768;
32
+ readonly height: 768;
33
+ };
34
+ /** Board token artwork (Token customImage=) */
35
+ readonly token: {
36
+ readonly width: 512;
37
+ readonly height: 512;
38
+ };
39
+ };
40
+ export type ArtworkPreset = keyof typeof ARTWORK_PRESETS;
41
+ /** Appended to every prompt unless a style is given, so a scenario's pictures look like one set */
42
+ export declare const DEFAULT_STYLE: string;
43
+ /** The three files of the FLUX.2 [klein] 4B setup and where to get them */
44
+ export declare const FLUX2_KLEIN_MODELS: {
45
+ readonly diffusion: {
46
+ readonly folder: "diffusion_models";
47
+ readonly match: RegExp;
48
+ readonly downloads: readonly [{
49
+ readonly file: "flux-2-klein-4b.safetensors";
50
+ readonly note: "bf16, 7.2 GB; use this on Apple Silicon (MPS has no fp8)";
51
+ readonly url: "https://huggingface.co/Comfy-Org/flux2-klein-4B/resolve/main/split_files/diffusion_models/flux-2-klein-4b.safetensors";
52
+ }, {
53
+ readonly file: "flux-2-klein-4b-fp8.safetensors";
54
+ readonly note: "fp8, ~4 GB; for NVIDIA GPUs with less VRAM";
55
+ readonly url: "https://huggingface.co/black-forest-labs/FLUX.2-klein-4b-fp8/resolve/main/flux-2-klein-4b-fp8.safetensors";
56
+ }];
57
+ };
58
+ readonly textEncoder: {
59
+ readonly folder: "text_encoders";
60
+ readonly match: RegExp;
61
+ readonly downloads: readonly [{
62
+ readonly file: "qwen_3_4b.safetensors";
63
+ readonly note: "7.5 GB";
64
+ readonly url: "https://huggingface.co/Comfy-Org/flux2-klein-4B/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors";
65
+ }];
66
+ };
67
+ readonly vae: {
68
+ readonly folder: "vae";
69
+ readonly match: RegExp;
70
+ readonly downloads: readonly [{
71
+ readonly file: "flux2-vae.safetensors";
72
+ readonly note: "321 MB; FLUX.1 and SDXL VAEs give distorted colours";
73
+ readonly url: "https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/vae/flux2-vae.safetensors";
74
+ }];
75
+ };
76
+ };
77
+ export interface Flux2KleinSettings {
78
+ diffusionModel: string;
79
+ textEncoder: string;
80
+ vae: string;
81
+ prompt: string;
82
+ width: number;
83
+ height: number;
84
+ seed: number;
85
+ steps: number;
86
+ cfg: number;
87
+ filenamePrefix: string;
88
+ }
89
+ /** Recommended sampling for the distilled model; the undistilled "base" model needs more steps and real CFG */
90
+ export declare function recommendedSampling(diffusionModel: string): {
91
+ steps: number;
92
+ cfg: number;
93
+ };
94
+ /** ComfyUI API-format workflow: FLUX.2 [klein] text-to-image */
95
+ export declare function buildFlux2KleinWorkflow(s: Flux2KleinSettings): Record<string, unknown>;
96
+ type Fetch = typeof fetch;
97
+ export interface ArtworkStatus {
98
+ reachable: boolean;
99
+ url: string;
100
+ error?: string;
101
+ comfyuiVersion?: string;
102
+ device?: string;
103
+ models: {
104
+ diffusion?: string;
105
+ textEncoder?: string;
106
+ vae?: string;
107
+ };
108
+ missing: Array<keyof typeof FLUX2_KLEIN_MODELS>;
109
+ }
110
+ /** Checks the ComfyUI server and which of the FLUX.2 [klein] files it can load */
111
+ export declare function getArtworkStatus(url?: string, fetchFn?: Fetch): Promise<ArtworkStatus>;
112
+ /** Human-readable setup instructions for what getArtworkStatus found missing */
113
+ export declare function setupInstructions(status: ArtworkStatus): string;
114
+ export interface GenerateArtworkOptions {
115
+ prompt: string;
116
+ /** Where to save the image; .jpg/.jpeg saves a JPEG (much smaller in the package), otherwise PNG */
117
+ outputFile: string;
118
+ preset?: ArtworkPreset;
119
+ width?: number;
120
+ height?: number;
121
+ seed?: number;
122
+ steps?: number;
123
+ cfg?: number;
124
+ /** Replaces DEFAULT_STYLE; pass '' for no style suffix */
125
+ style?: string;
126
+ url?: string;
127
+ timeoutMs?: number;
128
+ pollMs?: number;
129
+ fetchFn?: Fetch;
130
+ }
131
+ export interface GenerateArtworkResult {
132
+ file: string;
133
+ width: number;
134
+ height: number;
135
+ seed: number;
136
+ steps: number;
137
+ cfg: number;
138
+ diffusionModel: string;
139
+ seconds: number;
140
+ /** Compact JPEG of the result, for showing the image back to the caller */
141
+ preview: Buffer;
142
+ }
143
+ /** Generates one image with FLUX.2 [klein] on a running ComfyUI and saves it to outputFile */
144
+ export declare function generateArtwork(opts: GenerateArtworkOptions): Promise<GenerateArtworkResult>;
145
+ export {};
146
+ //# sourceMappingURL=comfyui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comfyui.d.ts","sourceRoot":"","sources":["../../src/artwork/comfyui.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,214 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ /**
4
+ * Scenario artwork through a local ComfyUI server running FLUX.2 [klein] 4B.
5
+ * The workflow is ComfyUI's API format; the settings are the ones the distilled
6
+ * model is built for (4 steps, CFG 1, euler), which take ~25 s per image on an M1 Pro
7
+ * once the model is loaded (the first image also loads ~15 GB of weights).
8
+ */
9
+ export const DEFAULT_COMFYUI_URL = process.env.VALKYRIE_COMFYUI_URL ?? 'http://127.0.0.1:8188';
10
+ /** Image sizes per use in a scenario (width x height, multiples of 16) */
11
+ export const ARTWORK_PRESETS = {
12
+ /** Scenario list picture (quest.ini image=) and the intro cutscene's left panel */
13
+ cover: { width: 896, height: 896 },
14
+ intro: { width: 896, height: 896 },
15
+ /** Letters, parchments, photographs, journal pages: portrait handouts shown during play */
16
+ handout: { width: 768, height: 1024 },
17
+ /** Landscape pictures shown during play (a room, a revelation, the ending) */
18
+ scene: { width: 1024, height: 768 },
19
+ /** Custom monster portrait (CustomMonster image=) */
20
+ monster: { width: 768, height: 768 },
21
+ /** Board token artwork (Token customImage=) */
22
+ token: { width: 512, height: 512 },
23
+ };
24
+ /** Appended to every prompt unless a style is given, so a scenario's pictures look like one set */
25
+ export const DEFAULT_STYLE = 'Dark painterly Lovecraftian horror illustration in the style of a 1920s Mansions of Madness board game card, '
26
+ + 'muted desaturated palette, deep shadows, eerie atmosphere, no text, no letters, no watermark.';
27
+ /** The three files of the FLUX.2 [klein] 4B setup and where to get them */
28
+ export const FLUX2_KLEIN_MODELS = {
29
+ diffusion: {
30
+ folder: 'diffusion_models',
31
+ match: /flux-?2-klein-4b/i,
32
+ downloads: [
33
+ { file: 'flux-2-klein-4b.safetensors', note: 'bf16, 7.2 GB; use this on Apple Silicon (MPS has no fp8)', url: 'https://huggingface.co/Comfy-Org/flux2-klein-4B/resolve/main/split_files/diffusion_models/flux-2-klein-4b.safetensors' },
34
+ { file: 'flux-2-klein-4b-fp8.safetensors', note: 'fp8, ~4 GB; for NVIDIA GPUs with less VRAM', url: 'https://huggingface.co/black-forest-labs/FLUX.2-klein-4b-fp8/resolve/main/flux-2-klein-4b-fp8.safetensors' },
35
+ ],
36
+ },
37
+ textEncoder: {
38
+ folder: 'text_encoders',
39
+ match: /qwen_?3_?4b/i,
40
+ downloads: [
41
+ { file: 'qwen_3_4b.safetensors', note: '7.5 GB', url: 'https://huggingface.co/Comfy-Org/flux2-klein-4B/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors' },
42
+ ],
43
+ },
44
+ vae: {
45
+ folder: 'vae',
46
+ match: /flux2-vae/i,
47
+ downloads: [
48
+ { file: 'flux2-vae.safetensors', note: '321 MB; FLUX.1 and SDXL VAEs give distorted colours', url: 'https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/vae/flux2-vae.safetensors' },
49
+ ],
50
+ },
51
+ };
52
+ /** Recommended sampling for the distilled model; the undistilled "base" model needs more steps and real CFG */
53
+ export function recommendedSampling(diffusionModel) {
54
+ return /base/i.test(diffusionModel) ? { steps: 20, cfg: 4 } : { steps: 4, cfg: 1 };
55
+ }
56
+ /** ComfyUI API-format workflow: FLUX.2 [klein] text-to-image */
57
+ export function buildFlux2KleinWorkflow(s) {
58
+ return {
59
+ '1': { class_type: 'UNETLoader', inputs: { unet_name: s.diffusionModel, weight_dtype: 'default' } },
60
+ '2': { class_type: 'CLIPLoader', inputs: { clip_name: s.textEncoder, type: 'flux2' } },
61
+ '3': { class_type: 'VAELoader', inputs: { vae_name: s.vae } },
62
+ '4': { class_type: 'CLIPTextEncode', inputs: { clip: ['2', 0], text: s.prompt } },
63
+ '5': { class_type: 'ConditioningZeroOut', inputs: { conditioning: ['4', 0] } },
64
+ '6': { class_type: 'EmptyFlux2LatentImage', inputs: { width: s.width, height: s.height, batch_size: 1 } },
65
+ '7': { class_type: 'Flux2Scheduler', inputs: { steps: s.steps, width: s.width, height: s.height } },
66
+ '8': { class_type: 'KSamplerSelect', inputs: { sampler_name: 'euler' } },
67
+ '9': { class_type: 'RandomNoise', inputs: { noise_seed: s.seed } },
68
+ '10': { class_type: 'CFGGuider', inputs: { model: ['1', 0], positive: ['4', 0], negative: ['5', 0], cfg: s.cfg } },
69
+ '11': { class_type: 'SamplerCustomAdvanced', inputs: { noise: ['9', 0], guider: ['10', 0], sampler: ['8', 0], sigmas: ['7', 0], latent_image: ['6', 0] } },
70
+ '12': { class_type: 'VAEDecode', inputs: { samples: ['11', 0], vae: ['3', 0] } },
71
+ '13': { class_type: 'SaveImage', inputs: { images: ['12', 0], filename_prefix: s.filenamePrefix } },
72
+ };
73
+ }
74
+ async function getJson(fetchFn, url) {
75
+ const res = await fetchFn(url, { signal: AbortSignal.timeout(10_000) });
76
+ if (!res.ok)
77
+ throw new Error(`${url} answered ${res.status}`);
78
+ return res.json();
79
+ }
80
+ /** Picks the preferred file: the distilled model over "base", then the first match */
81
+ function pickModel(files, match) {
82
+ const candidates = files.filter(f => match.test(f));
83
+ return candidates.find(f => !/base/i.test(f)) ?? candidates[0];
84
+ }
85
+ /** Checks the ComfyUI server and which of the FLUX.2 [klein] files it can load */
86
+ export async function getArtworkStatus(url = DEFAULT_COMFYUI_URL, fetchFn = fetch) {
87
+ const status = { reachable: false, url, models: {}, missing: [] };
88
+ try {
89
+ const stats = await getJson(fetchFn, `${url}/system_stats`);
90
+ status.reachable = true;
91
+ status.comfyuiVersion = stats.system?.comfyui_version;
92
+ status.device = stats.devices?.[0]?.name;
93
+ }
94
+ catch (e) {
95
+ status.error = e.message;
96
+ status.missing = ['diffusion', 'textEncoder', 'vae'];
97
+ return status;
98
+ }
99
+ for (const key of ['diffusion', 'textEncoder', 'vae']) {
100
+ const spec = FLUX2_KLEIN_MODELS[key];
101
+ let files = [];
102
+ try {
103
+ files = await getJson(fetchFn, `${url}/models/${spec.folder}`);
104
+ }
105
+ catch {
106
+ // Older ComfyUI without /models/<folder>: treat as missing so the setup hint is shown
107
+ }
108
+ const found = pickModel(files, spec.match);
109
+ if (found)
110
+ status.models[key] = found;
111
+ else
112
+ status.missing.push(key);
113
+ }
114
+ return status;
115
+ }
116
+ /** Human-readable setup instructions for what getArtworkStatus found missing */
117
+ export function setupInstructions(status) {
118
+ const lines = [];
119
+ if (!status.reachable) {
120
+ lines.push(`ComfyUI is not reachable at ${status.url}${status.error ? ` (${status.error})` : ''}. With comfy-cli:`, ' pip install comfy-cli (or: uv tool install comfy-cli)', ' comfy install (once; installs ComfyUI into a workspace)', ' comfy launch --background', 'Models kept elsewhere: comfy launch --background -- --extra-model-paths-config <yaml listing diffusion_models, text_encoders, vae>.', 'Set VALKYRIE_COMFYUI_URL if ComfyUI runs on another address.');
121
+ }
122
+ for (const key of status.missing) {
123
+ const spec = FLUX2_KLEIN_MODELS[key];
124
+ lines.push(`Missing ${spec.folder} model. Download one of:`);
125
+ for (const d of spec.downloads) {
126
+ lines.push(` ${d.file} (${d.note}):`, ` comfy model download --url ${d.url} --relative-path models/${spec.folder}`);
127
+ }
128
+ }
129
+ return lines.join('\n');
130
+ }
131
+ function roundTo16(n) {
132
+ return Math.max(256, Math.round(n / 16) * 16);
133
+ }
134
+ /** Generates one image with FLUX.2 [klein] on a running ComfyUI and saves it to outputFile */
135
+ export async function generateArtwork(opts) {
136
+ const url = opts.url ?? DEFAULT_COMFYUI_URL;
137
+ const fetchFn = opts.fetchFn ?? fetch;
138
+ const status = await getArtworkStatus(url, fetchFn);
139
+ if (!status.reachable || status.missing.length > 0) {
140
+ throw new Error(`ComfyUI is not ready for FLUX.2 [klein].\n${setupInstructions(status)}`);
141
+ }
142
+ const diffusionModel = status.models.diffusion;
143
+ const size = ARTWORK_PRESETS[opts.preset ?? 'scene'];
144
+ const width = roundTo16(opts.width ?? size.width);
145
+ const height = roundTo16(opts.height ?? size.height);
146
+ const sampling = recommendedSampling(diffusionModel);
147
+ const style = opts.style ?? DEFAULT_STYLE;
148
+ const settings = {
149
+ diffusionModel,
150
+ textEncoder: status.models.textEncoder,
151
+ vae: status.models.vae,
152
+ prompt: style ? `${opts.prompt.trim()} ${style}` : opts.prompt.trim(),
153
+ width,
154
+ height,
155
+ seed: opts.seed ?? Math.floor(Math.random() * 2 ** 32),
156
+ steps: opts.steps ?? sampling.steps,
157
+ cfg: opts.cfg ?? sampling.cfg,
158
+ filenamePrefix: `valkyrie_${path.basename(opts.outputFile, path.extname(opts.outputFile)).replace(/[^\w-]/g, '_')}`,
159
+ };
160
+ const started = Date.now();
161
+ const submit = await fetchFn(`${url}/prompt`, {
162
+ method: 'POST',
163
+ headers: { 'Content-Type': 'application/json' },
164
+ body: JSON.stringify({ prompt: buildFlux2KleinWorkflow(settings) }),
165
+ signal: AbortSignal.timeout(30_000),
166
+ });
167
+ const submitted = await submit.json();
168
+ if (!submit.ok || !submitted.prompt_id) {
169
+ throw new Error(`ComfyUI rejected the workflow: ${submitted.error?.message ?? submit.status} ${JSON.stringify(submitted.node_errors ?? {})}`);
170
+ }
171
+ // The first image of a session also loads the models, which can take minutes
172
+ const timeoutMs = opts.timeoutMs ?? 15 * 60_000;
173
+ const pollMs = opts.pollMs ?? 2000;
174
+ let image;
175
+ while (!image) {
176
+ if (Date.now() - started > timeoutMs)
177
+ throw new Error(`ComfyUI did not finish within ${Math.round(timeoutMs / 1000)} s (prompt ${submitted.prompt_id})`);
178
+ await new Promise(r => setTimeout(r, pollMs));
179
+ const history = await getJson(fetchFn, `${url}/history/${submitted.prompt_id}`);
180
+ const entry = history[submitted.prompt_id];
181
+ if (!entry)
182
+ continue;
183
+ if (entry.status?.status_str === 'error') {
184
+ throw new Error(`ComfyUI failed: ${JSON.stringify(entry.status.messages ?? []).slice(0, 1000)}`);
185
+ }
186
+ image = Object.values(entry.outputs ?? {}).flatMap(o => o.images ?? [])[0];
187
+ if (!image && entry.status?.status_str === 'success')
188
+ throw new Error('ComfyUI finished without an image');
189
+ }
190
+ const view = (extra = '') => `${url}/view?filename=${encodeURIComponent(image.filename)}&subfolder=${encodeURIComponent(image.subfolder)}&type=${image.type}${extra}`;
191
+ const download = async (u) => {
192
+ const res = await fetchFn(u, { signal: AbortSignal.timeout(60_000) });
193
+ if (!res.ok)
194
+ throw new Error(`Could not download the image from ComfyUI (${res.status})`);
195
+ return Buffer.from(await res.arrayBuffer());
196
+ };
197
+ const jpeg = /\.jpe?g$/i.test(opts.outputFile);
198
+ const full = await download(view(jpeg ? '&preview=jpeg;90' : ''));
199
+ const preview = jpeg ? full : await download(view('&preview=jpeg;80'));
200
+ fs.mkdirSync(path.dirname(opts.outputFile), { recursive: true });
201
+ fs.writeFileSync(opts.outputFile, full);
202
+ return {
203
+ file: opts.outputFile,
204
+ width,
205
+ height,
206
+ seed: settings.seed,
207
+ steps: settings.steps,
208
+ cfg: settings.cfg,
209
+ diffusionModel,
210
+ seconds: Math.round((Date.now() - started) / 1000),
211
+ preview,
212
+ };
213
+ }
214
+ //# sourceMappingURL=comfyui.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comfyui.js","sourceRoot":"","sources":["../../src/artwork/comfyui.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { ScenarioModel } from '../model/scenario-model.js';
2
+ /**
3
+ * Package name Valkyrie can publish: the scenario folder name, which must not contain spaces
4
+ * (it becomes the package and manifest file name, and the key in the download list).
5
+ * "The Wrath of Elder Claude" → "TheWrathOfElderClaude".
6
+ */
7
+ export declare function toPackageName(name: string): string;
8
+ /**
9
+ * Writes what Valkyrie's editor "Create Package" writes next to the .valkyrie file
10
+ * (EditorTools.CreatePackage): <Package>.ini, the manifest a scenario list entry is made from,
11
+ * and a copy of the cover image. version is the SHA-256 of the package, as .NET's
12
+ * BitConverter.ToString prints it ("AB-CD-...").
13
+ */
14
+ export declare function writePackageManifest(model: ScenarioModel, packagePath: string): {
15
+ manifest: string;
16
+ icon?: string;
17
+ };
18
+ //# sourceMappingURL=package-manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-manifest.d.ts","sourceRoot":"","sources":["../../src/io/package-manifest.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAoCnH"}
@@ -0,0 +1,57 @@
1
+ import { createHash } from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { serializeQuestConfig } from '../model/component-types.js';
5
+ /**
6
+ * Package name Valkyrie can publish: the scenario folder name, which must not contain spaces
7
+ * (it becomes the package and manifest file name, and the key in the download list).
8
+ * "The Wrath of Elder Claude" → "TheWrathOfElderClaude".
9
+ */
10
+ export function toPackageName(name) {
11
+ const words = name.normalize('NFKD').replace(/[̀-ͯ]/g, '').split(/[^A-Za-z0-9]+/).filter(Boolean);
12
+ return words.map(w => w[0].toUpperCase() + w.slice(1)).join('') || 'Scenario';
13
+ }
14
+ /**
15
+ * Writes what Valkyrie's editor "Create Package" writes next to the .valkyrie file
16
+ * (EditorTools.CreatePackage): <Package>.ini, the manifest a scenario list entry is made from,
17
+ * and a copy of the cover image. version is the SHA-256 of the package, as .NET's
18
+ * BitConverter.ToString prints it ("AB-CD-...").
19
+ */
20
+ export function writePackageManifest(model, packagePath) {
21
+ const destination = path.dirname(packagePath);
22
+ const packageName = path.basename(packagePath, path.extname(packagePath));
23
+ const digest = createHash('sha256').update(fs.readFileSync(packagePath)).digest('hex').toUpperCase();
24
+ const version = digest.match(/../g).join('-');
25
+ const config = { ...model.questConfig, version };
26
+ let icon;
27
+ if (config.image) {
28
+ const source = path.join(model.scenarioDir, config.image);
29
+ icon = path.basename(config.image);
30
+ if (fs.existsSync(source))
31
+ fs.copyFileSync(source, path.join(destination, icon));
32
+ config.image = icon;
33
+ }
34
+ const lines = ['[Quest]'];
35
+ for (const [k, v] of Object.entries(serializeQuestConfig(config)))
36
+ lines.push(`${k}=${v}`);
37
+ // The localization file holds the default language; each text is written under that language's name
38
+ const lang = config.defaultlanguage || 'English';
39
+ const text = (key) => model.localization.get(key);
40
+ const oneLine = (v) => v.replace(/\r/g, '').replace(/\\n|\n/g, '');
41
+ const escaped = (v) => v.replace(/\r/g, '').replace(/\n/g, '\\n');
42
+ const entries = [
43
+ ['name', text('quest.name'), v => v],
44
+ ['synopsys', text('quest.synopsys'), oneLine],
45
+ ['description', text('quest.description'), escaped],
46
+ ['authors', text('quest.authors'), escaped],
47
+ ['authors_short', text('quest.authors_short'), oneLine],
48
+ ];
49
+ for (const [field, value, format] of entries) {
50
+ if (value !== undefined)
51
+ lines.push(`${field}.${lang}=${format(value)}`);
52
+ }
53
+ const manifest = path.join(destination, `${packageName}.ini`);
54
+ fs.writeFileSync(manifest, lines.join('\n') + '\n');
55
+ return { manifest, icon };
56
+ }
57
+ //# sourceMappingURL=package-manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-manifest.js","sourceRoot":"","sources":["../../src/io/package-manifest.ts"],"names":[],"mappings":"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"}