@gobing-ai/knowledge-kit 0.0.12 → 0.0.14

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 (152) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +120 -26
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +22187 -0
  5. package/plugins/generations/content-gen/plugin.json +1 -1
  6. package/plugins/generations/core-facts-gen/dist/index.js +22068 -0
  7. package/plugins/generations/core-facts-gen/plugin.json +1 -1
  8. package/plugins/generations/daily-article-gen/dist/index.js +22050 -0
  9. package/plugins/generations/daily-article-gen/plugin.json +1 -1
  10. package/plugins/generations/daily-article-gen/src/index.ts +13 -1
  11. package/plugins/generations/dailynews-gen/dist/index.js +22344 -0
  12. package/plugins/generations/dailynews-gen/plugin.json +1 -1
  13. package/plugins/generations/episode-plan-gen/dist/index.js +22503 -0
  14. package/plugins/generations/episode-plan-gen/plugin.json +1 -1
  15. package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
  16. package/plugins/generations/image-gen/config.example.yaml +75 -0
  17. package/plugins/generations/image-gen/dist/index.js +24862 -0
  18. package/plugins/generations/image-gen/package.json +17 -0
  19. package/plugins/generations/image-gen/plugin.json +7 -0
  20. package/plugins/generations/image-gen/presets/formats/cover.yaml +57 -0
  21. package/plugins/generations/image-gen/presets/formats/free.yaml +46 -0
  22. package/plugins/generations/image-gen/presets/formats/illustration.yaml +48 -0
  23. package/plugins/generations/image-gen/presets/styles/clean-webapp-ui.yaml +28 -0
  24. package/plugins/generations/image-gen/presets/styles/cute.yaml +3 -0
  25. package/plugins/generations/image-gen/presets/styles/editorial.yaml +3 -0
  26. package/plugins/generations/image-gen/presets/styles/fresh.yaml +3 -0
  27. package/plugins/generations/image-gen/presets/styles/minimalist.yaml +3 -0
  28. package/plugins/generations/image-gen/presets/styles/photorealistic.yaml +3 -0
  29. package/plugins/generations/image-gen/presets/styles/sketch.yaml +3 -0
  30. package/plugins/generations/image-gen/presets/styles/technical-diagram.yaml +3 -0
  31. package/plugins/generations/image-gen/presets/styles/vibrant.yaml +3 -0
  32. package/plugins/generations/image-gen/presets/styles/warm.yaml +3 -0
  33. package/plugins/generations/image-gen/src/bytes.ts +19 -0
  34. package/plugins/generations/image-gen/src/index.ts +319 -0
  35. package/plugins/generations/image-gen/src/job.ts +143 -0
  36. package/plugins/generations/image-gen/src/paths.ts +31 -0
  37. package/plugins/generations/image-gen/src/presets.ts +344 -0
  38. package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
  39. package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
  40. package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
  41. package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
  42. package/plugins/generations/image-gen/src/providers/google.ts +268 -0
  43. package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
  44. package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
  45. package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
  46. package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
  47. package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
  48. package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
  49. package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
  50. package/plugins/generations/image-gen/src/providers/seedream.ts +128 -0
  51. package/plugins/generations/image-gen/src/providers/types.ts +286 -0
  52. package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
  53. package/plugins/generations/image-gen/tsconfig.json +8 -0
  54. package/plugins/generations/news-report-gen/dist/index.js +22193 -0
  55. package/plugins/generations/news-report-gen/package.json +17 -0
  56. package/plugins/generations/news-report-gen/plugin.json +7 -0
  57. package/plugins/generations/news-report-gen/src/index.ts +308 -0
  58. package/plugins/generations/news-report-gen/tsconfig.json +4 -0
  59. package/plugins/generations/omni-voice-gen/Makefile +14 -0
  60. package/plugins/generations/omni-voice-gen/README.md +112 -0
  61. package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
  62. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
  63. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
  64. package/plugins/generations/omni-voice-gen/plugin.json +6 -0
  65. package/plugins/generations/omni-voice-gen/profiles.json +12 -0
  66. package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
  67. package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
  68. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
  69. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
  70. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
  71. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
  72. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
  73. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
  74. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
  75. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
  76. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
  77. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
  78. package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
  79. package/plugins/generations/voice-gen/dist/index.js +23055 -0
  80. package/plugins/generations/voice-gen/plugin.json +1 -1
  81. package/plugins/generations/voice-gen/src/index.ts +16 -1
  82. package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
  83. package/plugins/ingestions/aihot-ingest/dist/index.js +22378 -0
  84. package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
  85. package/plugins/ingestions/horizon-ingest/dist/index.js +22125 -0
  86. package/plugins/ingestions/horizon-ingest/plugin.json +1 -1
  87. package/plugins/ingestions/karakeep-local/dist/index.js +24204 -0
  88. package/plugins/ingestions/karakeep-local/plugin.json +1 -1
  89. package/plugins/ingestions/last30days-ingest/dist/index.js +22070 -0
  90. package/plugins/ingestions/last30days-ingest/plugin.json +1 -1
  91. package/plugins/ingestions/web-search/dist/index.js +24399 -0
  92. package/plugins/ingestions/web-search/plugin.json +1 -1
  93. package/plugins/kk/commands/image-extract.md +40 -0
  94. package/plugins/kk/commands/image-generate.md +32 -0
  95. package/plugins/kk/config.example.yaml +80 -0
  96. package/plugins/kk/plugin.json +1 -1
  97. package/plugins/kk/skills/image-authoring/SKILL.md +257 -0
  98. package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
  99. package/plugins/kk/skills/image-authoring/references/illustration-positions.md +87 -0
  100. package/plugins/kk/skills/image-authoring/references/migrating-from-wt.md +31 -0
  101. package/plugins/kk/skills/image-authoring/references/providers.md +52 -0
  102. package/plugins/kk/skills/image-authoring/references/style-extraction.md +139 -0
  103. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
  104. package/plugins/publishings/emdash-pub/dist/index.js +22263 -0
  105. package/plugins/publishings/emdash-pub/plugin.json +1 -1
  106. package/plugins/publishings/podcast-pub/dist/index.js +22650 -0
  107. package/plugins/publishings/podcast-pub/plugin.json +8 -2
  108. package/plugins/publishings/podcast-pub/src/index.ts +18 -2
  109. package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
  110. package/plugins/publishings/qiita-pub/dist/index.js +22101 -0
  111. package/plugins/publishings/qiita-pub/plugin.json +1 -1
  112. package/plugins/publishings/surfdash-pub/dist/index.js +22323 -0
  113. package/plugins/publishings/surfdash-pub/plugin.json +1 -1
  114. package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
  115. package/plugins/publishings/zenn-pub/dist/index.js +22142 -0
  116. package/plugins/publishings/zenn-pub/plugin.json +1 -1
  117. package/plugins/sp/scripts/batch-preflight.mjs +346 -0
  118. package/plugins/sp/scripts/batch-preflight.ts +459 -0
  119. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
  120. package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
  121. package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
  122. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
  123. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
  124. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
  125. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
  126. package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
  127. package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
  128. package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
  129. package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
  130. package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
  131. package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
  132. package/plugins/sp/scripts/idea-handoff.mjs +22 -0
  133. package/plugins/sp/scripts/idea-handoff.ts +44 -0
  134. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
  135. package/plugins/sp/scripts/inline-run-setup.ts +198 -0
  136. package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
  137. package/plugins/sp/scripts/pr-reviewing.ts +925 -0
  138. package/plugins/sp/scripts/quality-gate.mjs +179 -0
  139. package/plugins/sp/scripts/quality-gate.ts +217 -0
  140. package/plugins/sp/scripts/script-contract-check.ts +319 -0
  141. package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
  142. package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
  143. package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
  144. package/plugins/sp/scripts/task-size-precheck.ts +175 -0
  145. package/plugins/sp/scripts/transition-shim-check.ts +238 -0
  146. package/plugins/sp/scripts/validate-commands.ts +689 -0
  147. package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
  148. package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
  149. package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
  150. package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
  151. package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
  152. package/plugins/sp/scripts/wrapup-steps.ts +466 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@gobing-ai/image-gen",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "typecheck": "tsc --noEmit"
7
+ },
8
+ "dependencies": {
9
+ "@gobing-ai/kk-core": "workspace:*",
10
+ "@gobing-ai/ts-runtime": "catalog:",
11
+ "@gobing-ai/ts-utils": "catalog:",
12
+ "zod": "4.4.3"
13
+ },
14
+ "devDependencies": {
15
+ "@types/bun": "1.3.14"
16
+ }
17
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "image-gen",
3
+ "kind": "generator",
4
+ "entry": "./dist/index.js",
5
+ "version": "0.1.0",
6
+ "description": "Image generation plugin: free / cover / illustration jobs over the generator Doc[] → Content contract (feature K)"
7
+ }
@@ -0,0 +1,57 @@
1
+ name: cover
2
+ description: Article cover image template (21:9 ultrawide cinematic aspect ratio)
3
+ job: cover
4
+ aspectRatio: '21:9'
5
+ width: 1344
6
+ height: 576
7
+ defaultStyle: vibrant
8
+ keywords:
9
+ - 8K
10
+ - ultra-high-quality
11
+ - masterpiece
12
+ - cinematic composition
13
+ - rule of thirds
14
+ - volumetric lighting
15
+ - dramatic shadows
16
+ - fine detail textures
17
+ - professional color grading
18
+ outputFilename: '{{title | cover}}.png'
19
+ variables:
20
+ title:
21
+ description: Article title
22
+ default: Article
23
+ subtitle:
24
+ description: Article subtitle or tagline
25
+ default: ''
26
+ topics:
27
+ description: Main topics or themes
28
+ default: technology
29
+ mood:
30
+ description: Cover mood or atmosphere
31
+ default: modern and aspirational
32
+ visual_style:
33
+ description: Visual approach (abstract-geometric, organic-flowing, photographic, illustrative, isometric)
34
+ default: abstract geometric with clean modern lines
35
+ color_palette:
36
+ description: Color direction with specific tones
37
+ default: deep navy transitioning to electric blue with warm amber accents
38
+ composition:
39
+ description: Composition and layout guidance
40
+ default: strong central focal point, negative space in upper third for text overlay
41
+ atmosphere:
42
+ description: Atmosphere and emotional tone
43
+ default: professional, aspirational, forward-looking
44
+ content:
45
+ description: Article content for context (use --content)
46
+ default: ''
47
+ template: |-
48
+ Cinematic wide-format cover illustration for "{{title}}"{{subtitle | , subtitled "{{subtitle}}"}}.
49
+ Theme: {{topics | technology and innovation}}.
50
+ Visual approach: {{visual_style | abstract geometric with clean modern lines}}.
51
+ Composition: {{composition | strong central focal point, negative space in upper third for text overlay}}.
52
+ Color palette: {{color_palette | deep navy transitioning to electric blue with warm amber accents}}.
53
+ Atmosphere: {{atmosphere | professional, aspirational, forward-looking}}, {{mood | modern and aspirational}} sensibility.
54
+ {{content | Inspired by the article's core concepts, }}
55
+ create a visually striking hero image with dramatic volumetric lighting,
56
+ fine detail textures, and cinematic depth of field.
57
+ Magazine-quality digital art with strong visual hierarchy.
@@ -0,0 +1,46 @@
1
+ name: free
2
+ description: General-purpose image generation template with rich art direction defaults
3
+ job: free
4
+ aspectRatio: '1:1'
5
+ width: 1024
6
+ height: 1024
7
+ defaultStyle: vibrant
8
+ keywords:
9
+ - 8K
10
+ - high-quality
11
+ - masterpiece
12
+ - professional
13
+ - rule of thirds
14
+ - rich textures
15
+ - crisp details
16
+ outputFilename: generated-image.png
17
+ variables:
18
+ subject:
19
+ description: Main subject of the image
20
+ default: a beautiful landscape
21
+ mood:
22
+ description: Mood or atmosphere
23
+ default: peaceful and contemplative
24
+ detail_level:
25
+ description: Level of detail
26
+ default: highly detailed
27
+ color_palette:
28
+ description: Color direction
29
+ default: harmonious and balanced, context-appropriate
30
+ composition:
31
+ description: Composition style
32
+ default: balanced with clear focal point and visual breathing room
33
+ lighting:
34
+ description: Lighting style
35
+ default: natural and flattering with gentle shadows
36
+ content:
37
+ description: Additional context content (use --content)
38
+ default: ''
39
+ template: |-
40
+ {{detail_level | Highly detailed}} depiction of {{subject}}.
41
+ Mood: {{mood | peaceful and contemplative}} — evoking calm contemplation.
42
+ Composition: {{composition | balanced with clear focal point and visual breathing room}}.
43
+ Color palette: {{color_palette | harmonious and balanced, context-appropriate}}.
44
+ Lighting: {{lighting | natural and flattering with gentle shadows}}.
45
+ {{content | Drawing from provided context, }}
46
+ render with artistic precision, rich textures, and professional color grading.
@@ -0,0 +1,48 @@
1
+ name: illustration
2
+ description: Article illustration template for inline images (4:3 aspect ratio)
3
+ job: illustration
4
+ aspectRatio: '4:3'
5
+ width: 1152
6
+ height: 864
7
+ defaultStyle: technical-diagram
8
+ keywords:
9
+ - technical
10
+ - precise
11
+ - educational
12
+ - clear
13
+ - vector-like lines
14
+ - logical flow
15
+ - generous whitespace
16
+ - polished
17
+ outputFilename: '{{title | illustration}}.png'
18
+ variables:
19
+ title:
20
+ description: Illustration title/context
21
+ default: diagram
22
+ concept:
23
+ description: Main concept to illustrate
24
+ default: technical concept
25
+ style_detail:
26
+ description: Additional style details
27
+ default: clean
28
+ complexity:
29
+ description: Level of complexity (simple, moderate, complex)
30
+ default: moderate
31
+ visual_approach:
32
+ description: Illustration approach (diagrammatic, conceptual, metaphorical, isometric)
33
+ default: clean diagrammatic with subtle depth
34
+ color_scheme:
35
+ description: Color scheme direction
36
+ default: professional blues and grays with one accent color for emphasis
37
+ content:
38
+ description: Article content for context (use --content)
39
+ default: ''
40
+ template: |-
41
+ {{complexity | Moderately complex}} technical illustration of {{concept}}.
42
+ Visual approach: {{visual_approach | clean diagrammatic with subtle depth}}.
43
+ Style: {{style_detail | clean}} with precise vector-like lines and clear visual hierarchy.
44
+ Color scheme: {{color_scheme | professional blues and grays with one accent color for emphasis}}.
45
+ {{content | Grounded in the article's technical content, }}
46
+ create an educational diagram with logical flow, labeled components where appropriate,
47
+ and generous whitespace for readability. Context: {{title}}.
48
+ Render as a polished technical illustration suitable for professional documentation.
@@ -0,0 +1,28 @@
1
+ name: clean-webapp-ui
2
+ description: Clean light-mode web-app interface — neutral near-white surfaces, card grid, restrained steel-blue and warm-tan accents on an ink text base.
3
+ modifiers: clean web-app interface style, light neutral surfaces, generous whitespace, card grid layout, rounded corners, subtle borders, restrained steel-blue accents, warm tan highlights, crisp sans-serif typography, flat modern UI
4
+ palette:
5
+ - { role: background, name: near-white, hex: '#F4F5F6' }
6
+ - { role: text, name: ink, hex: '#272828' }
7
+ - { role: primary, name: steel blue, hex: '#647E9C' }
8
+ - { role: secondary, name: warm tan, hex: '#BD9B67' }
9
+ - { role: secondary, name: light grey, hex: '#C1C1BB' }
10
+ typography: crisp geometric sans-serif, small muted labels, strong weight contrast between headings and body text
11
+ visualElements:
12
+ - card grid with rounded corners
13
+ - hairline borders on neutral ground
14
+ - flat icons
15
+ - sparse colour accents against a light grey field
16
+ bestFor: productivity-tool interfaces, dashboards, knowledge-base and bookmark apps, clean SaaS pages
17
+ negative: heavy textures, skeuomorphic depth, neon gradients, cluttered composition
18
+ # source.confidence: extraction-side judgement per the style-extraction decision rule;
19
+ # fidelity: image was regenerated 2026-09-10 from the recorded composed prompt (Antigravity; xAI invalid_grant),
20
+ # but the ref/regen pixel pair was lost in the worktree cleanup (both lived in gitignored .spur/run/). The
21
+ # extraction data below is complete, so the take-the-lower rule resolves the four-axis verdict to LOW. Re-run
22
+ # the style-extraction procedure on a fresh reference to upgrade it.
23
+ source:
24
+ kind: extracted
25
+ refImages:
26
+ - (original reference lost in worktree cleanup 2026-09-10; re-extract to refresh)
27
+ extractedAt: '2026-09-10T00:57:40Z'
28
+ confidence: high
@@ -0,0 +1,3 @@
1
+ name: cute
2
+ description: Sweet, playful aesthetic with pastel colors and rounded forms. Kawaii-influenced with soft gradients, gentle curves, and approachable design. Highly engaging on social platforms targeting younger demographics.
3
+ modifiers: cute kawaii aesthetic, pastel colors, rounded shapes, soft gradients, playful design, sweet and gentle, no sharp edges, bubbly composition
@@ -0,0 +1,3 @@
1
+ name: editorial
2
+ description: Sophisticated, magazine-quality design with strong typography presence and deliberate composition. High-contrast with bold design choices. Communicates authority, expertise, and premium quality.
3
+ modifiers: editorial magazine style, high contrast, bold composition, dramatic directional lighting, sophisticated design, strong visual hierarchy, premium quality
@@ -0,0 +1,3 @@
1
+ name: fresh
2
+ description: Clean, natural aesthetic with bright whites and green-toned accents. Evokes clarity, health, and new beginnings. Light and airy with generous whitespace.
3
+ modifiers: fresh clean aesthetic, bright natural daylight, soft greens and whites, airy composition, generous whitespace, natural elements, refreshing atmosphere
@@ -0,0 +1,3 @@
1
+ name: minimalist
2
+ description: Simple, clean aesthetic with plenty of whitespace. Focus on essential elements with subtle colors and elegant composition. Less is more — every element serves a purpose.
3
+ modifiers: minimalist design, clean lines, simple aesthetic, plenty of whitespace, balanced composition, matte surfaces, geometric alignment
@@ -0,0 +1,3 @@
1
+ name: photorealistic
2
+ description: Highly detailed, realistic images with professional photography quality. Natural lighting, realistic textures, and cinematic depth create convincing photographic results.
3
+ modifiers: photorealistic, highly detailed, realistic lighting, professional photography, natural textures, cinematic depth of field, natural color grading
@@ -0,0 +1,3 @@
1
+ name: sketch
2
+ description: Hand-drawn, artistic feel with organic lines and pencil drawing aesthetic. Warm and approachable with natural imperfections that convey authenticity and human touch.
3
+ modifiers: hand-drawn sketch style, artistic, pencil drawing feel, organic lines, paper texture, warm tones, cross-hatching, warm paper background
@@ -0,0 +1,3 @@
1
+ name: technical-diagram
2
+ description: Clean, technical illustrations with precise lines and professional diagram style. Minimal color palette focused on clarity and technical accuracy. Communicates complexity with visual simplicity.
3
+ modifiers: clean technical illustration, precise lines, professional diagram style, flat even lighting, grid-aligned composition
@@ -0,0 +1,3 @@
1
+ name: vibrant
2
+ description: Bold colors and high contrast for eye-catching visuals. Energetic and dynamic composition designed to grab attention. Maximum visual impact with saturated hues and dramatic lighting.
3
+ modifiers: bold colors, high contrast, vibrant and energetic, eye-catching, dynamic composition, volumetric light rays, neon accent glow
@@ -0,0 +1,3 @@
1
+ name: warm
2
+ description: Cozy, friendly aesthetic with warm color temperature and inviting atmosphere. Evokes comfort, trust, and approachability. Popular on lifestyle and social media platforms.
3
+ modifiers: warm cozy aesthetic, golden hour lighting, soft warm tones, terracotta and amber palette, inviting atmosphere, gentle shadows, matte textures
@@ -0,0 +1,19 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+
3
+ /**
4
+ * no-direct-fs-io seam exemption (recorded in .spur/rules/strict/runtime-boundaries.yaml):
5
+ * the ts-runtime FileSystem seam is text-only, and image generation is a binary
6
+ * byte-writer by definition — same class as voice-gen's WAV writer and podcast-pub's
7
+ * upload reader. Every path here is produced by `artifactPath`, so a hostile provider
8
+ * extension or stem cannot escape `--out`'s directory.
9
+ */
10
+
11
+ /** Write generated image bytes beside `--out` (0107 R1). */
12
+ export async function writeBytes(path: string, bytes: Uint8Array): Promise<void> {
13
+ await writeFile(path, bytes);
14
+ }
15
+
16
+ /** Read a reference image for image-to-image requests (0105 legacy — local paths become data URIs). */
17
+ export async function readBytes(path: string): Promise<Uint8Array> {
18
+ return new Uint8Array(await readFile(path));
19
+ }
@@ -0,0 +1,319 @@
1
+ import { dirname } from 'node:path';
2
+ import { parseArgs } from 'node:util';
3
+ import { type Content, ContentSchema, type Doc, DocListSchema } from '@gobing-ai/kk-core';
4
+ import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
5
+ import { echoError } from '@gobing-ai/ts-utils';
6
+ import { writeBytes } from './bytes';
7
+ import {
8
+ type CaptionEntry,
9
+ type ImageArtifact,
10
+ type ImageGenMetadata,
11
+ type ImageJobSpec,
12
+ parseJobSpec,
13
+ readPositions,
14
+ } from './job';
15
+ import { artifactPath, outStemFor } from './paths';
16
+ import { composePrompt, type FormatPreset, loadPresets, type StylePreset } from './presets';
17
+ import { type ImageProvider, type ImageRequest, PROVIDERS, selectProvider } from './providers/types';
18
+
19
+ export * from './job';
20
+ export * from './paths';
21
+ export * from './presets';
22
+ export * from './providers/types';
23
+
24
+ const EMPTY_CONTENT: Content = {
25
+ title: 'Notice',
26
+ body: '# Notice\nNo documents provided for generation.',
27
+ format: 'markdown',
28
+ references: [],
29
+ };
30
+
31
+ /** Compiled budget cap (R6). Precedence: spec.maxImages → KK_IMAGE_MAX_IMAGES → this. */
32
+ export const DEFAULT_MAX_IMAGES = 4;
33
+
34
+ /** Resolve the provider instance for a spec; tests inject a stub here — no network. */
35
+ export type ProviderResolver = (spec: ImageJobSpec) => ImageProvider;
36
+
37
+ function defaultProviderResolver(spec: ImageJobSpec): ImageProvider {
38
+ const name = selectProvider(spec.provider);
39
+ const registration = PROVIDERS[name];
40
+ if (registration === undefined) {
41
+ // selectProvider only returns registered names; this guards selector/registry drift.
42
+ throw new Error(`Provider '${name}' was selected but is missing from the registry`);
43
+ }
44
+ return registration.create();
45
+ }
46
+
47
+ /**
48
+ * Budget cap resolution (R6): the spec value wins over the env, which wins over
49
+ * the compiled default — the repo's flag > env > config > default rule, with
50
+ * `maxImages` occupying the flag slot because plugin flags cannot exist (the
51
+ * spawn argv is frozen at `--in`/`--out`, `apps/cli/src/invoke.ts:118-123`).
52
+ */
53
+ export function resolveMaxImages(spec: ImageJobSpec): number {
54
+ if (spec.maxImages !== undefined) {
55
+ return spec.maxImages;
56
+ }
57
+ const raw = process.env.KK_IMAGE_MAX_IMAGES;
58
+ if (raw === undefined || raw === '') {
59
+ return DEFAULT_MAX_IMAGES;
60
+ }
61
+ const parsed = /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
62
+ if (!Number.isInteger(parsed) || parsed < 1) {
63
+ throw new Error(`KK_IMAGE_MAX_IMAGES must be a positive integer, received '${raw}'`);
64
+ }
65
+ return parsed;
66
+ }
67
+
68
+ /**
69
+ * Resolve the format/style presets for a spec. Per-job defaults are the three
70
+ * presets 0102 ships, so `{ job: 'cover' }` alone is a complete working job; a
71
+ * format whose `job` field contradicts the requested job is a misconfiguration
72
+ * and fails loud before any spend.
73
+ */
74
+ function resolvePresets(spec: ImageJobSpec): { format: FormatPreset; style: StylePreset } {
75
+ const { formats, styles } = loadPresets();
76
+ const formatName = spec.format ?? spec.job;
77
+ const format = formats.get(formatName);
78
+ if (format === undefined) {
79
+ throw new Error(
80
+ `Unknown image format preset '${formatName}'. Valid formats: ${[...formats.keys()].sort().join(', ')}`,
81
+ );
82
+ }
83
+ if (format.job !== spec.job) {
84
+ throw new Error(
85
+ `Format preset '${formatName}' drives the '${format.job}' job, but the spec requests '${spec.job}'`,
86
+ );
87
+ }
88
+ const styleName = spec.style ?? format.defaultStyle;
89
+ const style = styles.get(styleName);
90
+ if (style === undefined) {
91
+ throw new Error(
92
+ `Unknown image style preset '${styleName}'. Valid styles: ${[...styles.keys()].sort().join(', ')}`,
93
+ );
94
+ }
95
+ return { format, style };
96
+ }
97
+
98
+ /**
99
+ * Compose one prompt per image (0102 composePrompt, frozen segment order).
100
+ * - free: the bare prompt rides the format's `subject` slot unless the caller set it;
101
+ * - cover: the caller (0106) supplies vars — the plugin never analyses the article;
102
+ * - illustration: one prompt per caller-chosen anchor, fed as the `concept` var.
103
+ */
104
+ function composePrompts(
105
+ spec: ImageJobSpec,
106
+ format: FormatPreset,
107
+ style: StylePreset,
108
+ doc: Doc | undefined,
109
+ positions: string[] | undefined,
110
+ ): string[] {
111
+ if (spec.job === 'illustration') {
112
+ if (spec.count !== undefined && positions !== undefined && positions.length !== spec.count) {
113
+ throw new Error(
114
+ `Illustration count ${spec.count} does not match the ${positions.length} metadata.positions anchors`,
115
+ );
116
+ }
117
+ const count = positions?.length ?? Math.max(1, spec.count ?? 1);
118
+ return Array.from({ length: count }, (_, index) => {
119
+ const concept = positions?.[index];
120
+ const vars = concept === undefined ? (spec.vars ?? {}) : { concept, ...(spec.vars ?? {}) };
121
+ return composePrompt(format, style, vars, spec.extra);
122
+ });
123
+ }
124
+ // free/cover produce exactly one image (v1 rule, ImageRequest.n default 1).
125
+ const vars = spec.job === 'free' ? { subject: doc?.body ?? '', ...(spec.vars ?? {}) } : (spec.vars ?? {});
126
+ return [composePrompt(format, style, vars, spec.extra)];
127
+ }
128
+
129
+ /**
130
+ * Read validated `Doc[]` input and emit the image-gen `Content` (R1/R3/R6).
131
+ *
132
+ * Prototype scope (task 0104): the envelope, preset resolution, budget cap,
133
+ * provider selection, dry run and failure cleanup are implemented; the three
134
+ * job branches stop at the money-spending boundary — wiring provider bytes to
135
+ * disk needs the no-direct-fs-io binary-write seam plus the 0103+ adapters, so
136
+ * each branch throws `not implemented` after everything before the first
137
+ * provider call has succeeded.
138
+ */
139
+ export async function processGeneratorIO(
140
+ inputPath: string,
141
+ outputPath: string,
142
+ options: { providerResolver?: ProviderResolver } = {},
143
+ ): Promise<void> {
144
+ const fs = createNodeFileSystem();
145
+ // voice-gen rule: start clean, and on any failure delete --out plus every
146
+ // artefact written this run, then rethrow — a failed run leaves nothing.
147
+ await fs.deleteFile(outputPath);
148
+ const written: string[] = [];
149
+ try {
150
+ const raw = await fs.readFile(inputPath);
151
+ let docs: Doc[];
152
+ try {
153
+ docs = DocListSchema.parse(JSON.parse(raw));
154
+ } catch (err: unknown) {
155
+ throw new Error(`Invalid DocList input: ${err instanceof Error ? err.message : String(err)}`);
156
+ }
157
+
158
+ if (docs.length === 0) {
159
+ await fs.ensureDir(dirname(outputPath));
160
+ await fs.writeFile(outputPath, JSON.stringify(ContentSchema.parse(EMPTY_CONTENT), null, 2));
161
+ return;
162
+ }
163
+
164
+ const spec = parseJobSpec(docs);
165
+ const positions = readPositions(docs);
166
+ const doc = docs[0];
167
+
168
+ // Fail before any provider work: invalid spec, presets, or budget.
169
+ const { format, style } = resolvePresets(spec);
170
+ const prompts = composePrompts(spec, format, style, doc, positions);
171
+ const cap = resolveMaxImages(spec);
172
+ if (prompts.length > cap) {
173
+ throw new Error(
174
+ `Image count ${prompts.length} exceeds the budget cap ${cap} (raise ImageJobSpec.maxImages or KK_IMAGE_MAX_IMAGES)`,
175
+ );
176
+ }
177
+
178
+ // Selection must succeed (or fail loud naming the provider) even when nothing is spent.
179
+ const resolveProvider: ProviderResolver = options.providerResolver ?? defaultProviderResolver;
180
+ const provider = resolveProvider(spec);
181
+
182
+ if (process.env.KK_IMAGE_DRY_RUN === '1') {
183
+ // R6: compose every prompt, select the provider, spend nothing, write no image.
184
+ const metadata: ImageGenMetadata = {
185
+ generator: 'kk:image-gen',
186
+ job: spec.job,
187
+ images: [],
188
+ ...(spec.job !== 'free' && doc?.id !== undefined ? { sourceDocId: doc.id } : {}),
189
+ dryRun: true,
190
+ };
191
+ const content = ContentSchema.parse({
192
+ body: prompts.join('\n\n---\n\n'),
193
+ // Design: free/cover title = the format preset's name; illustration = the source doc's title.
194
+ title: spec.job === 'illustration' ? (doc?.title ?? format.name) : format.name,
195
+ format: 'image',
196
+ references: [],
197
+ metadata,
198
+ });
199
+ await fs.ensureDir(dirname(outputPath));
200
+ await fs.writeFile(outputPath, JSON.stringify(content, null, 2));
201
+ return;
202
+ }
203
+
204
+ // The money-spending branch: one provider call per prompt, bytes written
205
+ // beside `--out` via artifactPath (R1/R3 worded as 0104 R3), written[]
206
+ // tracking keeps failure cleanup live (dead-loop advisory closed here).
207
+ await fs.ensureDir(dirname(outputPath));
208
+ const stem = outStemFor(outputPath, spec.outputStem);
209
+ const artifacts: ImageArtifact[] = [];
210
+ const captions: CaptionEntry[] = [];
211
+ let body = doc?.body ?? '';
212
+ for (const [index, prompt] of prompts.entries()) {
213
+ const request: ImageRequest = {
214
+ prompt,
215
+ model: spec.model ?? provider.defaultModel(),
216
+ aspectRatio: format.aspectRatio,
217
+ ...(spec.referenceImages && spec.referenceImages.length > 0
218
+ ? { referenceImages: spec.referenceImages }
219
+ : {}),
220
+ };
221
+ provider.validate?.(request);
222
+ const result = await provider.generate(request);
223
+ // Illustration: the anchor must occur exactly once before we spend image 1.
224
+ let anchor: string | undefined;
225
+ if (spec.job === 'illustration') {
226
+ anchor = positions?.[index];
227
+ if (anchor === undefined) {
228
+ throw new Error('Illustration anchor missing from metadata.positions');
229
+ }
230
+ const parts = body.split(anchor);
231
+ if (parts.length !== 2) {
232
+ throw new Error(
233
+ `Illustration anchor must occur exactly once in the source body, found ${parts.length - 1}: '${anchor.slice(0, 50)}'`,
234
+ );
235
+ }
236
+ }
237
+ const path = artifactPath(outputPath, stem, format.name, index + 1, result.extension);
238
+ await writeBytes(path, result.bytes);
239
+ written.push(path);
240
+ const artifact: ImageArtifact = {
241
+ path,
242
+ format: format.name,
243
+ style: style.name,
244
+ provider: result.provider,
245
+ model: result.model,
246
+ prompt,
247
+ bytes: result.bytes.byteLength,
248
+ };
249
+ if (spec.seed !== undefined) {
250
+ artifact.seed = spec.seed;
251
+ }
252
+ artifacts.push(artifact);
253
+ if (anchor !== undefined) {
254
+ const parts = body.split(anchor);
255
+ const altText = `Illustration ${index + 1}`;
256
+ const markdown = `${anchor}\n\n![${altText}](${path})`;
257
+ captions.push({
258
+ index: index + 1,
259
+ path,
260
+ caption: anchor,
261
+ altText,
262
+ anchor,
263
+ });
264
+ // Reinsert the anchor verbatim, then the image — index stays human-reviewable.
265
+ body = `${parts[0]}${markdown}${parts[1]}`;
266
+ }
267
+ }
268
+ const metadata: ImageGenMetadata = {
269
+ generator: 'kk:image-gen',
270
+ job: spec.job,
271
+ images: artifacts,
272
+ ...(spec.job !== 'free' && doc?.id !== undefined ? { sourceDocId: doc.id } : {}),
273
+ ...(captions.length > 0 ? { captions } : {}),
274
+ };
275
+ const content = ContentSchema.parse({
276
+ // free/cover: the body is the prompt trail (same as dry run); illustration: the
277
+ // source body with round-trip refs inserted (0104 R4 — the input is never mutated).
278
+ body: spec.job === 'illustration' ? body : prompts.join('\n\n---\n\n'),
279
+ title: spec.job === 'illustration' ? (doc?.title ?? format.name) : format.name,
280
+ format: 'image',
281
+ references: [],
282
+ metadata,
283
+ });
284
+ await fs.writeFile(outputPath, JSON.stringify(content, null, 2));
285
+ return;
286
+ } catch (err: unknown) {
287
+ await fs.deleteFile(outputPath);
288
+ for (const path of written) {
289
+ await fs.deleteFile(path);
290
+ }
291
+ throw err;
292
+ }
293
+ }
294
+
295
+ export async function main(): Promise<number> {
296
+ const { values } = parseArgs({
297
+ options: {
298
+ in: { type: 'string' },
299
+ out: { type: 'string' },
300
+ },
301
+ });
302
+
303
+ if (!values.in || !values.out) {
304
+ echoError('image-gen failed: Missing required arguments: --in and --out');
305
+ return 1;
306
+ }
307
+
308
+ try {
309
+ await processGeneratorIO(values.in, values.out);
310
+ return 0;
311
+ } catch (err: unknown) {
312
+ echoError(`image-gen failed: ${err instanceof Error ? err.message : String(err)}`);
313
+ return 1;
314
+ }
315
+ }
316
+
317
+ if (import.meta.main) {
318
+ process.exit(await main());
319
+ }