@laitszkin/apollo-toolkit 2.0.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 (204) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +100 -0
  3. package/LICENSE +21 -0
  4. package/README.md +144 -0
  5. package/align-project-documents/SKILL.md +94 -0
  6. package/align-project-documents/agents/openai.yaml +4 -0
  7. package/analyse-app-logs/LICENSE +21 -0
  8. package/analyse-app-logs/README.md +126 -0
  9. package/analyse-app-logs/SKILL.md +121 -0
  10. package/analyse-app-logs/agents/openai.yaml +4 -0
  11. package/analyse-app-logs/references/investigation-checklist.md +58 -0
  12. package/analyse-app-logs/references/log-signal-patterns.md +52 -0
  13. package/answering-questions-with-research/SKILL.md +46 -0
  14. package/answering-questions-with-research/agents/openai.yaml +4 -0
  15. package/bin/apollo-toolkit.js +7 -0
  16. package/commit-and-push/LICENSE +21 -0
  17. package/commit-and-push/README.md +26 -0
  18. package/commit-and-push/SKILL.md +70 -0
  19. package/commit-and-push/agents/openai.yaml +4 -0
  20. package/commit-and-push/references/branch-naming.md +15 -0
  21. package/commit-and-push/references/commit-messages.md +19 -0
  22. package/deep-research-topics/LICENSE +21 -0
  23. package/deep-research-topics/README.md +43 -0
  24. package/deep-research-topics/SKILL.md +84 -0
  25. package/deep-research-topics/agents/openai.yaml +4 -0
  26. package/develop-new-features/LICENSE +21 -0
  27. package/develop-new-features/README.md +52 -0
  28. package/develop-new-features/SKILL.md +105 -0
  29. package/develop-new-features/agents/openai.yaml +4 -0
  30. package/develop-new-features/references/testing-e2e.md +35 -0
  31. package/develop-new-features/references/testing-integration.md +42 -0
  32. package/develop-new-features/references/testing-property-based.md +44 -0
  33. package/develop-new-features/references/testing-unit.md +37 -0
  34. package/discover-edge-cases/CHANGELOG.md +19 -0
  35. package/discover-edge-cases/LICENSE +21 -0
  36. package/discover-edge-cases/README.md +87 -0
  37. package/discover-edge-cases/SKILL.md +124 -0
  38. package/discover-edge-cases/agents/openai.yaml +4 -0
  39. package/discover-edge-cases/references/architecture-edge-cases.md +41 -0
  40. package/discover-edge-cases/references/code-edge-cases.md +46 -0
  41. package/docs-to-voice/.env.example +106 -0
  42. package/docs-to-voice/CHANGELOG.md +71 -0
  43. package/docs-to-voice/LICENSE +21 -0
  44. package/docs-to-voice/README.md +118 -0
  45. package/docs-to-voice/SKILL.md +107 -0
  46. package/docs-to-voice/agents/openai.yaml +4 -0
  47. package/docs-to-voice/scripts/docs_to_voice.py +1385 -0
  48. package/docs-to-voice/scripts/docs_to_voice.sh +11 -0
  49. package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +210 -0
  50. package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +115 -0
  51. package/docs-to-voice/tests/test_docs_to_voice_settings.py +43 -0
  52. package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +57 -0
  53. package/enhance-existing-features/CHANGELOG.md +35 -0
  54. package/enhance-existing-features/LICENSE +21 -0
  55. package/enhance-existing-features/README.md +54 -0
  56. package/enhance-existing-features/SKILL.md +120 -0
  57. package/enhance-existing-features/agents/openai.yaml +4 -0
  58. package/enhance-existing-features/references/e2e-tests.md +25 -0
  59. package/enhance-existing-features/references/integration-tests.md +30 -0
  60. package/enhance-existing-features/references/property-based-tests.md +33 -0
  61. package/enhance-existing-features/references/unit-tests.md +29 -0
  62. package/feature-propose/LICENSE +21 -0
  63. package/feature-propose/README.md +23 -0
  64. package/feature-propose/SKILL.md +107 -0
  65. package/feature-propose/agents/openai.yaml +4 -0
  66. package/feature-propose/references/enhancement-features.md +25 -0
  67. package/feature-propose/references/important-features.md +25 -0
  68. package/feature-propose/references/mvp-features.md +25 -0
  69. package/feature-propose/references/performance-features.md +25 -0
  70. package/financial-research/SKILL.md +208 -0
  71. package/financial-research/agents/openai.yaml +4 -0
  72. package/financial-research/assets/weekly_market_report_template.md +45 -0
  73. package/fix-github-issues/SKILL.md +98 -0
  74. package/fix-github-issues/agents/openai.yaml +4 -0
  75. package/fix-github-issues/scripts/list_issues.py +148 -0
  76. package/fix-github-issues/tests/test_list_issues.py +127 -0
  77. package/generate-spec/LICENSE +21 -0
  78. package/generate-spec/README.md +61 -0
  79. package/generate-spec/SKILL.md +96 -0
  80. package/generate-spec/agents/openai.yaml +4 -0
  81. package/generate-spec/references/templates/checklist.md +78 -0
  82. package/generate-spec/references/templates/spec.md +55 -0
  83. package/generate-spec/references/templates/tasks.md +35 -0
  84. package/generate-spec/scripts/create-specs +123 -0
  85. package/harden-app-security/CHANGELOG.md +27 -0
  86. package/harden-app-security/LICENSE +21 -0
  87. package/harden-app-security/README.md +46 -0
  88. package/harden-app-security/SKILL.md +127 -0
  89. package/harden-app-security/agents/openai.yaml +4 -0
  90. package/harden-app-security/references/agent-attack-catalog.md +117 -0
  91. package/harden-app-security/references/common-software-attack-catalog.md +168 -0
  92. package/harden-app-security/references/red-team-extreme-scenarios.md +81 -0
  93. package/harden-app-security/references/risk-checklist.md +78 -0
  94. package/harden-app-security/references/security-test-patterns-agent.md +101 -0
  95. package/harden-app-security/references/security-test-patterns-finance.md +88 -0
  96. package/harden-app-security/references/test-snippets.md +73 -0
  97. package/improve-observability/SKILL.md +114 -0
  98. package/improve-observability/agents/openai.yaml +4 -0
  99. package/learn-skill-from-conversations/CHANGELOG.md +15 -0
  100. package/learn-skill-from-conversations/LICENSE +22 -0
  101. package/learn-skill-from-conversations/README.md +47 -0
  102. package/learn-skill-from-conversations/SKILL.md +85 -0
  103. package/learn-skill-from-conversations/agents/openai.yaml +4 -0
  104. package/learn-skill-from-conversations/scripts/extract_recent_conversations.py +369 -0
  105. package/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +176 -0
  106. package/learning-error-book/SKILL.md +112 -0
  107. package/learning-error-book/agents/openai.yaml +4 -0
  108. package/learning-error-book/assets/error_book_template.md +66 -0
  109. package/learning-error-book/scripts/render_markdown_to_pdf.py +367 -0
  110. package/lib/cli.js +338 -0
  111. package/lib/installer.js +225 -0
  112. package/maintain-project-constraints/SKILL.md +109 -0
  113. package/maintain-project-constraints/agents/openai.yaml +4 -0
  114. package/maintain-skill-catalog/README.md +18 -0
  115. package/maintain-skill-catalog/SKILL.md +66 -0
  116. package/maintain-skill-catalog/agents/openai.yaml +4 -0
  117. package/novel-to-short-video/CHANGELOG.md +53 -0
  118. package/novel-to-short-video/LICENSE +21 -0
  119. package/novel-to-short-video/README.md +63 -0
  120. package/novel-to-short-video/SKILL.md +233 -0
  121. package/novel-to-short-video/agents/openai.yaml +4 -0
  122. package/novel-to-short-video/references/plan-template.md +71 -0
  123. package/novel-to-short-video/references/roles-json.md +41 -0
  124. package/open-github-issue/LICENSE +21 -0
  125. package/open-github-issue/README.md +97 -0
  126. package/open-github-issue/SKILL.md +119 -0
  127. package/open-github-issue/agents/openai.yaml +4 -0
  128. package/open-github-issue/scripts/open_github_issue.py +380 -0
  129. package/open-github-issue/tests/test_open_github_issue.py +159 -0
  130. package/open-source-pr-workflow/CHANGELOG.md +32 -0
  131. package/open-source-pr-workflow/LICENSE +21 -0
  132. package/open-source-pr-workflow/README.md +23 -0
  133. package/open-source-pr-workflow/SKILL.md +123 -0
  134. package/open-source-pr-workflow/agents/openai.yaml +4 -0
  135. package/openai-text-to-image-storyboard/.env.example +10 -0
  136. package/openai-text-to-image-storyboard/CHANGELOG.md +49 -0
  137. package/openai-text-to-image-storyboard/LICENSE +21 -0
  138. package/openai-text-to-image-storyboard/README.md +99 -0
  139. package/openai-text-to-image-storyboard/SKILL.md +107 -0
  140. package/openai-text-to-image-storyboard/agents/openai.yaml +4 -0
  141. package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +763 -0
  142. package/package.json +36 -0
  143. package/record-spending/SKILL.md +113 -0
  144. package/record-spending/agents/openai.yaml +4 -0
  145. package/record-spending/references/account-format.md +33 -0
  146. package/record-spending/references/workbook-layout.md +84 -0
  147. package/resolve-review-comments/SKILL.md +122 -0
  148. package/resolve-review-comments/agents/openai.yaml +4 -0
  149. package/resolve-review-comments/references/adoption-criteria.md +23 -0
  150. package/resolve-review-comments/scripts/review_threads.py +425 -0
  151. package/resolve-review-comments/tests/test_review_threads.py +74 -0
  152. package/review-change-set/LICENSE +21 -0
  153. package/review-change-set/README.md +55 -0
  154. package/review-change-set/SKILL.md +103 -0
  155. package/review-change-set/agents/openai.yaml +4 -0
  156. package/review-codebases/LICENSE +21 -0
  157. package/review-codebases/README.md +67 -0
  158. package/review-codebases/SKILL.md +109 -0
  159. package/review-codebases/agents/openai.yaml +4 -0
  160. package/scripts/install_skills.ps1 +283 -0
  161. package/scripts/install_skills.sh +262 -0
  162. package/scripts/validate_openai_agent_config.py +194 -0
  163. package/scripts/validate_skill_frontmatter.py +110 -0
  164. package/specs-to-project-docs/LICENSE +21 -0
  165. package/specs-to-project-docs/README.md +57 -0
  166. package/specs-to-project-docs/SKILL.md +111 -0
  167. package/specs-to-project-docs/agents/openai.yaml +4 -0
  168. package/specs-to-project-docs/references/templates/architecture.md +29 -0
  169. package/specs-to-project-docs/references/templates/configuration.md +29 -0
  170. package/specs-to-project-docs/references/templates/developer-guide.md +33 -0
  171. package/specs-to-project-docs/references/templates/docs-index.md +39 -0
  172. package/specs-to-project-docs/references/templates/features.md +25 -0
  173. package/specs-to-project-docs/references/templates/getting-started.md +38 -0
  174. package/specs-to-project-docs/references/templates/readme.md +49 -0
  175. package/systematic-debug/LICENSE +21 -0
  176. package/systematic-debug/README.md +81 -0
  177. package/systematic-debug/SKILL.md +59 -0
  178. package/systematic-debug/agents/openai.yaml +4 -0
  179. package/text-to-short-video/.env.example +36 -0
  180. package/text-to-short-video/LICENSE +21 -0
  181. package/text-to-short-video/README.md +82 -0
  182. package/text-to-short-video/SKILL.md +221 -0
  183. package/text-to-short-video/agents/openai.yaml +4 -0
  184. package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +350 -0
  185. package/version-release/CHANGELOG.md +53 -0
  186. package/version-release/LICENSE +21 -0
  187. package/version-release/README.md +28 -0
  188. package/version-release/SKILL.md +94 -0
  189. package/version-release/agents/openai.yaml +4 -0
  190. package/version-release/references/branch-naming.md +15 -0
  191. package/version-release/references/changelog-writing.md +8 -0
  192. package/version-release/references/commit-messages.md +19 -0
  193. package/version-release/references/readme-writing.md +12 -0
  194. package/version-release/references/semantic-versioning.md +12 -0
  195. package/video-production/CHANGELOG.md +104 -0
  196. package/video-production/LICENSE +18 -0
  197. package/video-production/README.md +68 -0
  198. package/video-production/SKILL.md +213 -0
  199. package/video-production/agents/openai.yaml +4 -0
  200. package/video-production/references/plan-template.md +54 -0
  201. package/video-production/references/roles-json.md +41 -0
  202. package/weekly-financial-event-report/SKILL.md +195 -0
  203. package/weekly-financial-event-report/agents/openai.yaml +4 -0
  204. package/weekly-financial-event-report/assets/financial_event_report_template.md +53 -0
@@ -0,0 +1,221 @@
1
+ ---
2
+ name: text-to-short-video
3
+ description: Generate 30-60 second short videos by directly calling an OpenAI-compatible video generation API from text. Keep role consistency by using roles.json as role prompt source and only updating role descriptions.
4
+ ---
5
+
6
+ # Text to Short Video
7
+
8
+ ## Dependencies
9
+
10
+ - Required: none.
11
+ - Conditional: none.
12
+ - Optional: none.
13
+ - Fallback: not applicable.
14
+
15
+ ## Standards
16
+
17
+ - Evidence: Use `roles.json` as the authoritative role source and collect only the minimal prompt, duration, and sizing inputs.
18
+ - Execution: Stay API-only: resolve roles, build one prompt, submit the video job, poll until completion, and download one final MP4.
19
+ - Quality: Keep duration in the 30-60 second range, preserve role identity fields, and do not route through storyboard or Remotion skills.
20
+ - Output: Save the prompt package, API records, and final short-video artifacts under the project video workspace.
21
+
22
+ ## Overview
23
+
24
+ This skill is **API-only**:
25
+
26
+ 1. Resolve role consistency from `roles.json`.
27
+ 2. Build one short-video prompt from text + roles.
28
+ 3. Call an OpenAI-compatible video generation API.
29
+ 4. Poll until the job is finished.
30
+ 5. Download one final MP4.
31
+ 6. Optionally run aspect-ratio/size post-processing.
32
+
33
+ Do not use `openai-text-to-image-storyboard` or `remotion-best-practices` in this skill.
34
+
35
+ ## Required Inputs
36
+
37
+ Collect only what is required:
38
+
39
+ - `project_dir` (absolute path)
40
+ - `content_name` (output folder/file name)
41
+ - source text or user-locked prompt
42
+ - target size (`width x height`, default `1080x1920`)
43
+ - target duration seconds (default `50`, keep in `30-60` range)
44
+
45
+ If critical inputs are missing, ask concise follow-up questions.
46
+
47
+ ## Role Definition (Required)
48
+
49
+ Always use:
50
+
51
+ - `<project_dir>/pictures/<content_name>/roles.json`
52
+
53
+ Required JSON format:
54
+
55
+ ```json
56
+ {
57
+ "characters": [
58
+ {
59
+ "id": "lin_xia",
60
+ "name": "Lin Xia",
61
+ "appearance": "short black hair, amber eyes, slim build",
62
+ "outfit": "dark trench coat, silver pendant, leather boots",
63
+ "description": "standing calmly, observant expression"
64
+ }
65
+ ]
66
+ }
67
+ ```
68
+
69
+ Consistency rules:
70
+
71
+ - Use `roles.json` as the role prompt source.
72
+ - Keep `id`, `name`, `appearance`, `outfit` unchanged for existing roles.
73
+ - Only modify `description` to reflect this clip's action/emotion.
74
+ - If a new role is required, append a new role entry; never rewrite identity fields of existing roles.
75
+ - If no recurring roles exist yet, initialize with:
76
+
77
+ ```json
78
+ {"characters": []}
79
+ ```
80
+
81
+ ## Environment Setup
82
+
83
+ Use this template:
84
+
85
+ - `/Users/tszkinlai/.codex/skills/text-to-short-video/.env.example`
86
+
87
+ Copy to:
88
+
89
+ - `/Users/tszkinlai/.codex/skills/text-to-short-video/.env`
90
+
91
+ Required keys:
92
+
93
+ - `OPENAI_API_URL`
94
+ - `OPENAI_API_KEY`
95
+
96
+ Optional keys:
97
+
98
+ - `OPENAI_VIDEO_MODEL`
99
+ - `OPENAI_VIDEO_DURATION_SECONDS`
100
+ - `OPENAI_VIDEO_ASPECT_RATIO`
101
+ - `OPENAI_VIDEO_SIZE`
102
+ - `OPENAI_VIDEO_POLL_SECONDS`
103
+ - `TEXT_TO_SHORT_VIDEO_WIDTH`
104
+ - `TEXT_TO_SHORT_VIDEO_HEIGHT`
105
+
106
+ ## Workflow
107
+
108
+ ### 1) Resolve `roles.json` before prompt generation
109
+
110
+ - Target path: `<project_dir>/pictures/<content_name>/roles.json`.
111
+ - If file exists, load and reuse role identities.
112
+ - If file does not exist, create it with the required schema.
113
+ - For existing roles, update only `description` when clip-specific motion/emotion is needed.
114
+
115
+ ### 2) Build one generation prompt from text + roles
116
+
117
+ - If the user already gives an exact prompt, reuse it directly.
118
+ - Otherwise extract one concise visual prompt from source text.
119
+ - Keep the prompt focused on one coherent short narrative beat.
120
+ - Ensure all role identity details come from `roles.json` and only `description` is clip-specific.
121
+ - Save prompt package under:
122
+ - `<project_dir>/video/<content_name>/shorts/api/prompt_input.json`
123
+
124
+ Suggested local `prompt_input.json` structure:
125
+
126
+ ```json
127
+ {
128
+ "roles_file": "<project_dir>/pictures/<content_name>/roles.json",
129
+ "description_overrides": {
130
+ "lin_xia": "running through rain, breathing hard, determined"
131
+ },
132
+ "final_prompt": "..."
133
+ }
134
+ ```
135
+
136
+ ### 3) Submit video generation request
137
+
138
+ - Endpoint: `${OPENAI_API_URL%/}/videos/generations`
139
+ - Send model/prompt/duration/size (or aspect ratio) in JSON payload.
140
+ - Save request and response records under:
141
+ - `<project_dir>/video/<content_name>/shorts/api/`
142
+
143
+ Example request fields (provider-compatible variants are allowed):
144
+
145
+ ```json
146
+ {
147
+ "model": "${OPENAI_VIDEO_MODEL}",
148
+ "prompt": "...",
149
+ "duration": 50,
150
+ "size": "1080x1920",
151
+ "aspect_ratio": "9:16"
152
+ }
153
+ ```
154
+
155
+ ### 4) Poll job status until terminal state
156
+
157
+ - Read job ID from the create response.
158
+ - Poll `${OPENAI_API_URL%/}/videos/generations/<job_id>` every `OPENAI_VIDEO_POLL_SECONDS` seconds.
159
+ - Stop only on terminal state:
160
+ - success: download output video URL/file
161
+ - failure/cancelled: report provider error and stop
162
+
163
+ ### 5) Download final MP4
164
+
165
+ Save to:
166
+
167
+ - `<project_dir>/video/<content_name>/shorts/<content_name>_important.mp4`
168
+
169
+ If provider returns multiple outputs, keep the best one that matches requested size/duration closest.
170
+
171
+ ### 6) Enforce final aspect ratio and size (optional but recommended)
172
+
173
+ When output ratio or resolution differs from target, run:
174
+
175
+ ```bash
176
+ python /Users/tszkinlai/.codex/skills/text-to-short-video/scripts/enforce_video_aspect_ratio.py \
177
+ --input-video "<downloaded_video_path>" \
178
+ --output-video "<final_output_video_path>" \
179
+ --env-file /Users/tszkinlai/.codex/skills/text-to-short-video/.env \
180
+ --force
181
+ ```
182
+
183
+ Behavior:
184
+
185
+ - aspect ratio mismatch: center-crop then scale
186
+ - same ratio but different size: scale
187
+ - already matching: no-op/copy
188
+
189
+ ## Output Contract
190
+
191
+ Return absolute paths for:
192
+
193
+ - `roles.json` used for role consistency
194
+ - prompt input JSON (if saved)
195
+ - API request payload JSON (if saved)
196
+ - API create response JSON (if saved)
197
+ - final downloaded `.mp4`
198
+ - post-processed `.mp4` (if post-processing executed)
199
+
200
+ Also report:
201
+
202
+ - role reuse summary (reused/added roles)
203
+ - which role descriptions were modified for this clip
204
+ - final prompt text source (user-locked or agent-extracted)
205
+ - job ID and final status
206
+ - duration check (`30-60` seconds)
207
+ - final render size check (`width x height`)
208
+ - whether center crop was applied
209
+
210
+ ## Quality Gate Checklist
211
+
212
+ Before finishing, verify:
213
+
214
+ - generation path is API-only (no storyboard/remotion orchestration)
215
+ - `roles.json` uses required schema and is used as prompt source
216
+ - existing role identity fields (`id/name/appearance/outfit`) were not modified
217
+ - only role `description` was changed for clip-specific behavior
218
+ - job reached a successful terminal state
219
+ - output file exists at returned absolute path
220
+ - output duration is within `30-60` seconds (or user-approved exception)
221
+ - output size matches requested target (after post-processing if needed)
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Text to Short Video"
3
+ short_description: "Turn text into 30-60s short videos via API"
4
+ default_prompt: "Use $text-to-short-video to convert my text into one 30-60 second short video using an API-only workflow. Before building the final prompt, load <project_dir>/pictures/<content_name>/roles.json as the role source and keep role consistency: do not modify existing role id/name/appearance/outfit, only update role descriptions for clip-specific actions. Then call an OpenAI-compatible /videos/generations endpoint with settings from /Users/tszkinlai/.codex/skills/text-to-short-video/.env, poll until completion, download the MP4, and run post-process crop/resize only when output aspect ratio or size mismatches."
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ import tempfile
11
+ from pathlib import Path
12
+
13
+ SKILL_DIR = Path(__file__).resolve().parent.parent
14
+ DEFAULT_ENV_FILE = SKILL_DIR / ".env"
15
+ SIZE_PATTERN = re.compile(r"^(\d{2,5})x(\d{2,5})$")
16
+
17
+
18
+ def parse_args() -> argparse.Namespace:
19
+ parser = argparse.ArgumentParser(
20
+ description=(
21
+ "Enforce final video aspect ratio and size. "
22
+ "If input aspect ratio differs from target, center-crop then scale."
23
+ )
24
+ )
25
+ parser.add_argument("--input-video", required=True, help="Path to rendered input video.")
26
+ parser.add_argument(
27
+ "--output-video",
28
+ help=(
29
+ "Path to processed output video. If omitted, write next to input as "
30
+ "<name>_aspect_fixed.mp4."
31
+ ),
32
+ )
33
+ parser.add_argument(
34
+ "--in-place",
35
+ action="store_true",
36
+ help="Overwrite input file in place (uses a temporary file then replaces input).",
37
+ )
38
+ parser.add_argument(
39
+ "--target-size",
40
+ help="Target size in WIDTHxHEIGHT format, for example 1080x1920.",
41
+ )
42
+ parser.add_argument("--target-width", type=int, help="Target width in pixels.")
43
+ parser.add_argument("--target-height", type=int, help="Target height in pixels.")
44
+ parser.add_argument(
45
+ "--env-file",
46
+ default=str(DEFAULT_ENV_FILE),
47
+ help=f"Path to .env file (default: {DEFAULT_ENV_FILE}).",
48
+ )
49
+ parser.add_argument("--force", action="store_true", help="Overwrite output if it exists.")
50
+ parser.add_argument("--ffmpeg-bin", default="ffmpeg", help="ffmpeg executable name or path.")
51
+ parser.add_argument("--ffprobe-bin", default="ffprobe", help="ffprobe executable name or path.")
52
+ return parser.parse_args()
53
+
54
+
55
+ def parse_dotenv_line(content: str, line_no: int, file_path: Path) -> tuple[str, str] | None:
56
+ stripped = content.strip()
57
+ if not stripped or stripped.startswith("#"):
58
+ return None
59
+ if stripped.startswith("export "):
60
+ stripped = stripped[7:].strip()
61
+
62
+ if "=" not in stripped:
63
+ raise SystemExit(f"Invalid .env format at {file_path}:{line_no}: missing =")
64
+
65
+ key, value = stripped.split("=", 1)
66
+ key = key.strip()
67
+ value = value.strip()
68
+
69
+ if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key):
70
+ raise SystemExit(f"Invalid .env key at {file_path}:{line_no}: {key}")
71
+
72
+ if value and value[0] in {"'", '"'}:
73
+ quote = value[0]
74
+ if len(value) >= 2 and value[-1] == quote:
75
+ value = value[1:-1]
76
+ else:
77
+ value = value.split(" #", 1)[0].strip()
78
+
79
+ return key, value
80
+
81
+
82
+ def load_dotenv_file(env_file: Path, override: bool = False) -> bool:
83
+ if not env_file.exists():
84
+ return False
85
+
86
+ for line_no, line in enumerate(env_file.read_text(encoding="utf-8").splitlines(), start=1):
87
+ parsed = parse_dotenv_line(line, line_no, env_file)
88
+ if not parsed:
89
+ continue
90
+ key, value = parsed
91
+ if override or key not in os.environ:
92
+ os.environ[key] = value
93
+ return True
94
+
95
+
96
+ def parse_size(value: str) -> tuple[int, int]:
97
+ match = SIZE_PATTERN.fullmatch(value.strip().lower())
98
+ if not match:
99
+ raise SystemExit("Invalid size format. Use WIDTHxHEIGHT, for example 1080x1920.")
100
+ width = int(match.group(1))
101
+ height = int(match.group(2))
102
+ if width <= 0 or height <= 0:
103
+ raise SystemExit("Width and height must be positive integers.")
104
+ return width, height
105
+
106
+
107
+ def required_command(command: str) -> None:
108
+ if shutil.which(command):
109
+ return
110
+ raise SystemExit(f"Missing required command: {command}")
111
+
112
+
113
+ def probe_video_size(video_path: Path, ffprobe_bin: str) -> tuple[int, int]:
114
+ cmd = [
115
+ ffprobe_bin,
116
+ "-v",
117
+ "error",
118
+ "-select_streams",
119
+ "v:0",
120
+ "-show_entries",
121
+ "stream=width,height",
122
+ "-of",
123
+ "json",
124
+ str(video_path),
125
+ ]
126
+ try:
127
+ result = subprocess.run(cmd, check=True, capture_output=True, text=True)
128
+ except subprocess.CalledProcessError as exc:
129
+ stderr = exc.stderr.strip()
130
+ raise SystemExit(f"ffprobe failed for {video_path}: {stderr}") from exc
131
+
132
+ try:
133
+ payload = json.loads(result.stdout)
134
+ except json.JSONDecodeError as exc:
135
+ raise SystemExit(f"Unable to parse ffprobe output for {video_path}.") from exc
136
+
137
+ streams = payload.get("streams")
138
+ if not isinstance(streams, list) or not streams:
139
+ raise SystemExit(f"No video stream found in {video_path}.")
140
+
141
+ first = streams[0]
142
+ if not isinstance(first, dict):
143
+ raise SystemExit(f"Unexpected ffprobe stream payload for {video_path}.")
144
+
145
+ width = first.get("width")
146
+ height = first.get("height")
147
+ if not isinstance(width, int) or not isinstance(height, int) or width <= 0 or height <= 0:
148
+ raise SystemExit(f"Invalid video dimensions from ffprobe for {video_path}.")
149
+ return width, height
150
+
151
+
152
+ def even_floor(value: int, minimum: int = 2) -> int:
153
+ floored = value if value % 2 == 0 else value - 1
154
+ return max(floored, minimum)
155
+
156
+
157
+ def build_video_filter(
158
+ input_width: int,
159
+ input_height: int,
160
+ target_width: int,
161
+ target_height: int,
162
+ ) -> tuple[str | None, bool]:
163
+ same_ratio = input_width * target_height == input_height * target_width
164
+ same_size = input_width == target_width and input_height == target_height
165
+
166
+ if same_ratio and same_size:
167
+ return None, False
168
+
169
+ if same_ratio:
170
+ return f"scale={target_width}:{target_height}", False
171
+
172
+ input_wider = input_width * target_height > input_height * target_width
173
+ if input_wider:
174
+ crop_width = input_height * target_width // target_height
175
+ crop_height = input_height
176
+ else:
177
+ crop_width = input_width
178
+ crop_height = input_width * target_height // target_width
179
+
180
+ crop_width = min(even_floor(crop_width), even_floor(input_width))
181
+ crop_height = min(even_floor(crop_height), even_floor(input_height))
182
+
183
+ offset_x = max((input_width - crop_width) // 2, 0)
184
+ offset_y = max((input_height - crop_height) // 2, 0)
185
+
186
+ return f"crop={crop_width}:{crop_height}:{offset_x}:{offset_y},scale={target_width}:{target_height}", True
187
+
188
+
189
+ def resolve_target_size(args: argparse.Namespace) -> tuple[int, int]:
190
+ if args.target_size and (args.target_width is not None or args.target_height is not None):
191
+ raise SystemExit("Use either --target-size or --target-width/--target-height, not both.")
192
+
193
+ if args.target_size:
194
+ return parse_size(args.target_size)
195
+
196
+ env_width = os.getenv("TEXT_TO_SHORT_VIDEO_WIDTH", "").strip()
197
+ env_height = os.getenv("TEXT_TO_SHORT_VIDEO_HEIGHT", "").strip()
198
+
199
+ if args.target_width is not None:
200
+ width = args.target_width
201
+ else:
202
+ try:
203
+ width = int(env_width or "1080")
204
+ except ValueError as exc:
205
+ raise SystemExit("TEXT_TO_SHORT_VIDEO_WIDTH must be an integer.") from exc
206
+
207
+ if args.target_height is not None:
208
+ height = args.target_height
209
+ else:
210
+ try:
211
+ height = int(env_height or "1920")
212
+ except ValueError as exc:
213
+ raise SystemExit("TEXT_TO_SHORT_VIDEO_HEIGHT must be an integer.") from exc
214
+
215
+ if width <= 0 or height <= 0:
216
+ raise SystemExit("Target width and height must be positive integers.")
217
+ return width, height
218
+
219
+
220
+ def resolve_output_path(args: argparse.Namespace, input_video: Path) -> Path:
221
+ if args.in_place and args.output_video:
222
+ raise SystemExit("Do not pass --output-video with --in-place.")
223
+
224
+ if args.in_place:
225
+ return input_video
226
+
227
+ if args.output_video:
228
+ output_video = Path(args.output_video).expanduser().resolve()
229
+ else:
230
+ output_video = input_video.with_name(f"{input_video.stem}_aspect_fixed.mp4")
231
+
232
+ if output_video == input_video:
233
+ raise SystemExit("Output path equals input path. Use --in-place to replace the input file.")
234
+
235
+ return output_video
236
+
237
+
238
+ def copy_if_needed(source: Path, target: Path, force: bool) -> None:
239
+ if source == target:
240
+ return
241
+ if target.exists() and not force:
242
+ raise SystemExit(f"Output already exists: {target}. Use --force to overwrite.")
243
+ target.parent.mkdir(parents=True, exist_ok=True)
244
+ shutil.copy2(source, target)
245
+
246
+
247
+ def main() -> int:
248
+ args = parse_args()
249
+
250
+ input_video = Path(args.input_video).expanduser().resolve()
251
+ if not input_video.is_file():
252
+ raise SystemExit(f"Input video not found: {input_video}")
253
+
254
+ env_file = Path(args.env_file).expanduser()
255
+ if not env_file.is_absolute():
256
+ env_file = SKILL_DIR / env_file
257
+ load_dotenv_file(env_file, override=False)
258
+
259
+ target_width, target_height = resolve_target_size(args)
260
+ output_video = resolve_output_path(args, input_video)
261
+
262
+ required_command(args.ffmpeg_bin)
263
+ required_command(args.ffprobe_bin)
264
+
265
+ input_width, input_height = probe_video_size(input_video, args.ffprobe_bin)
266
+ filter_expression, crop_applied = build_video_filter(
267
+ input_width=input_width,
268
+ input_height=input_height,
269
+ target_width=target_width,
270
+ target_height=target_height,
271
+ )
272
+
273
+ if filter_expression is None:
274
+ print(
275
+ f"[INFO] Video already matches target size and aspect ratio: "
276
+ f"{input_width}x{input_height}."
277
+ )
278
+ copy_if_needed(input_video, output_video, args.force)
279
+ if input_video != output_video:
280
+ print(f"[OK] Copied original video to: {output_video}")
281
+ return 0
282
+
283
+ replace_in_place = args.in_place
284
+ if replace_in_place:
285
+ temp_fd, temp_name = tempfile.mkstemp(
286
+ prefix=f"{input_video.stem}_aspect_tmp_",
287
+ suffix=".mp4",
288
+ dir=str(input_video.parent),
289
+ )
290
+ os.close(temp_fd)
291
+ temp_output = Path(temp_name)
292
+ else:
293
+ temp_output = output_video
294
+ if temp_output.exists() and not args.force:
295
+ raise SystemExit(f"Output already exists: {temp_output}. Use --force to overwrite.")
296
+ temp_output.parent.mkdir(parents=True, exist_ok=True)
297
+
298
+ ffmpeg_cmd = [
299
+ args.ffmpeg_bin,
300
+ "-hide_banner",
301
+ "-loglevel",
302
+ "error",
303
+ "-y" if (args.force or replace_in_place) else "-n",
304
+ "-i",
305
+ str(input_video),
306
+ "-vf",
307
+ filter_expression,
308
+ "-map",
309
+ "0:v:0",
310
+ "-map",
311
+ "0:a?",
312
+ "-c:v",
313
+ "libx264",
314
+ "-preset",
315
+ "medium",
316
+ "-crf",
317
+ "18",
318
+ "-c:a",
319
+ "aac",
320
+ "-movflags",
321
+ "+faststart",
322
+ str(temp_output),
323
+ ]
324
+
325
+ try:
326
+ subprocess.run(ffmpeg_cmd, check=True)
327
+ except subprocess.CalledProcessError as exc:
328
+ if replace_in_place and temp_output.exists():
329
+ temp_output.unlink(missing_ok=True)
330
+ raise SystemExit(f"ffmpeg failed with exit code {exc.returncode}.") from exc
331
+
332
+ if replace_in_place:
333
+ temp_output.replace(input_video)
334
+ final_output = input_video
335
+ else:
336
+ final_output = output_video
337
+
338
+ final_width, final_height = probe_video_size(final_output, args.ffprobe_bin)
339
+ print(
340
+ f"[OK] Processed video written: {final_output}\n"
341
+ f"[INFO] Input size: {input_width}x{input_height}\n"
342
+ f"[INFO] Target size: {target_width}x{target_height}\n"
343
+ f"[INFO] Output size: {final_width}x{final_height}\n"
344
+ f"[INFO] Center crop applied: {'yes' if crop_applied else 'no'}"
345
+ )
346
+ return 0
347
+
348
+
349
+ if __name__ == "__main__":
350
+ raise SystemExit(main())
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on Keep a Changelog.
6
+
7
+ ## [0.3.0] - 2026-02-22
8
+
9
+ ### Changed
10
+
11
+ - Added a default `commit+push only` workflow when users do not explicitly request versioning.
12
+ - Restricted version bump recommendations to explicit user requests instead of proactive suggestions.
13
+ - Updated workflow docs to make release-specific steps conditional on explicit version/tag/release intent.
14
+
15
+ ## [0.2.0] - 2026-02-15
16
+
17
+ ### Changed
18
+
19
+ - Added an explicit dependency contract requiring `edge-case-test-fixer` then `code-simplifier` for code-affecting releases.
20
+ - Added a docs-only classification path so documentation-only changes can skip both dependency skills.
21
+ - Reordered the submit workflow to run dependency checks before version/changelog/readme/agent document updates.
22
+
23
+ ## [0.1.3] - 2026-02-10
24
+
25
+ ### Changed
26
+
27
+ - Added explicit guidance to infer and share a preferred semver bump before user confirmation.
28
+ - Required submit prompts to include a recommended target version with a brief rationale.
29
+ - Updated README workflow description to reflect recommendation-first version selection.
30
+
31
+ ## [0.1.2] - 2026-02-07
32
+
33
+ ### Changed
34
+
35
+ - Unified submit behavior into a single workflow without a mode-selection step.
36
+ - Standardized release scope analysis to always use the last version tag to `HEAD`.
37
+ - Moved `AGENTS.md` synchronization to run before commit and push, aligned with README sync timing.
38
+ - Updated commit staging guidance to include optional `AGENTS.md` changes when applicable.
39
+
40
+ ## [0.1.1] - 2026-02-07
41
+
42
+ ### Changed
43
+
44
+ - Added a brief introduction section to improve skill onboarding guidance.
45
+ - Clarified submit workflow rules so commit messages are derived from staged diff intent.
46
+ - Prohibited version-only commit subjects such as `feat: vX.Y.Z`.
47
+ - Added release-only commit message guidance and examples in commit message references.
48
+
49
+ [0.1.1]: https://github.com/LaiTszKin/submit-changes/compare/v0.1.0...v0.1.1
50
+ [0.1.2]: https://github.com/LaiTszKin/submit-changes/compare/v0.1.1...v0.1.2
51
+ [0.1.3]: https://github.com/LaiTszKin/submit-changes/compare/v0.1.2...v0.1.3
52
+ [0.2.0]: https://github.com/LaiTszKin/submit-changes/compare/v0.1.3...v0.2.0
53
+ [0.3.0]: https://github.com/LaiTszKin/submit-changes/compare/v0.2.0...v0.3.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lai Tsz Kin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # version-release
2
+
3
+ A Codex skill for explicit release workflows: code/documentation alignment, version bump, changelog update, tagging, and push.
4
+
5
+ ## What this skill does
6
+
7
+ `version-release` helps agents perform release work in a repeatable flow:
8
+
9
+ 1. Inspect the release scope from git history.
10
+ 2. Run quality gates for code-affecting changes.
11
+ 3. Run `specs-to-project-docs` when the release scope contains new completed spec files.
12
+ 4. Align project code and categorized project docs.
13
+ 5. Resolve version and tag details.
14
+ 6. Update version files and changelog.
15
+ 7. Commit release metadata.
16
+ 8. Create and push the release tag.
17
+
18
+ ## Scope
19
+
20
+ Use this skill only when the user explicitly asks for:
21
+
22
+ - release preparation
23
+ - version bumping
24
+ - tag creation/publishing
25
+
26
+ If the release includes new completed specs, convert them into categorized project docs first and let `specs-to-project-docs` remove or archive the superseded spec files.
27
+
28
+ If the user only wants commit + push, use `commit-and-push`.