@laitszkin/apollo-toolkit 3.13.2 → 3.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/AGENTS.md +7 -7
  2. package/CHANGELOG.md +36 -0
  3. package/CLAUDE.md +8 -8
  4. package/analyse-app-logs/SKILL.md +3 -3
  5. package/bin/apollo-toolkit.ts +7 -0
  6. package/codex/codex-memory-manager/SKILL.md +2 -2
  7. package/codex/learn-skill-from-conversations/SKILL.md +3 -3
  8. package/dist/bin/apollo-toolkit.d.ts +2 -0
  9. package/dist/bin/apollo-toolkit.js +7 -0
  10. package/dist/lib/cli.d.ts +41 -0
  11. package/dist/lib/cli.js +655 -0
  12. package/dist/lib/installer.d.ts +59 -0
  13. package/dist/lib/installer.js +404 -0
  14. package/dist/lib/tool-runner.d.ts +19 -0
  15. package/dist/lib/tool-runner.js +536 -0
  16. package/dist/lib/tools/architecture.d.ts +2 -0
  17. package/dist/lib/tools/architecture.js +23 -0
  18. package/dist/lib/tools/create-specs.d.ts +2 -0
  19. package/dist/lib/tools/create-specs.js +175 -0
  20. package/dist/lib/tools/docs-to-voice.d.ts +2 -0
  21. package/dist/lib/tools/docs-to-voice.js +705 -0
  22. package/dist/lib/tools/enforce-video-aspect-ratio.d.ts +2 -0
  23. package/dist/lib/tools/enforce-video-aspect-ratio.js +312 -0
  24. package/dist/lib/tools/extract-conversations.d.ts +2 -0
  25. package/dist/lib/tools/extract-conversations.js +105 -0
  26. package/dist/lib/tools/extract-pdf-text.d.ts +2 -0
  27. package/dist/lib/tools/extract-pdf-text.js +92 -0
  28. package/dist/lib/tools/filter-logs.d.ts +2 -0
  29. package/dist/lib/tools/filter-logs.js +94 -0
  30. package/dist/lib/tools/find-github-issues.d.ts +2 -0
  31. package/dist/lib/tools/find-github-issues.js +176 -0
  32. package/dist/lib/tools/generate-storyboard-images.d.ts +2 -0
  33. package/dist/lib/tools/generate-storyboard-images.js +419 -0
  34. package/dist/lib/tools/log-cli-utils.d.ts +35 -0
  35. package/dist/lib/tools/log-cli-utils.js +233 -0
  36. package/dist/lib/tools/open-github-issue.d.ts +2 -0
  37. package/dist/lib/tools/open-github-issue.js +750 -0
  38. package/dist/lib/tools/read-github-issue.d.ts +2 -0
  39. package/dist/lib/tools/read-github-issue.js +134 -0
  40. package/dist/lib/tools/render-error-book.d.ts +2 -0
  41. package/dist/lib/tools/render-error-book.js +265 -0
  42. package/dist/lib/tools/render-katex.d.ts +2 -0
  43. package/dist/lib/tools/render-katex.js +294 -0
  44. package/dist/lib/tools/review-threads.d.ts +2 -0
  45. package/dist/lib/tools/review-threads.js +491 -0
  46. package/dist/lib/tools/search-logs.d.ts +2 -0
  47. package/dist/lib/tools/search-logs.js +164 -0
  48. package/dist/lib/tools/sync-memory-index.d.ts +2 -0
  49. package/dist/lib/tools/sync-memory-index.js +113 -0
  50. package/dist/lib/tools/validate-openai-agent-config.d.ts +2 -0
  51. package/dist/lib/tools/validate-openai-agent-config.js +190 -0
  52. package/dist/lib/tools/validate-skill-frontmatter.d.ts +2 -0
  53. package/dist/lib/tools/validate-skill-frontmatter.js +118 -0
  54. package/dist/lib/types.d.ts +82 -0
  55. package/dist/lib/types.js +2 -0
  56. package/dist/lib/updater.d.ts +34 -0
  57. package/dist/lib/updater.js +112 -0
  58. package/dist/lib/utils/format.d.ts +2 -0
  59. package/dist/lib/utils/format.js +6 -0
  60. package/dist/lib/utils/terminal.d.ts +12 -0
  61. package/dist/lib/utils/terminal.js +26 -0
  62. package/docs-to-voice/SKILL.md +0 -1
  63. package/generate-spec/SKILL.md +1 -1
  64. package/katex/SKILL.md +1 -2
  65. package/lib/cli.ts +780 -0
  66. package/lib/installer.ts +466 -0
  67. package/lib/tool-runner.ts +561 -0
  68. package/lib/tools/architecture.ts +20 -0
  69. package/lib/tools/create-specs.ts +204 -0
  70. package/lib/tools/docs-to-voice.ts +799 -0
  71. package/lib/tools/enforce-video-aspect-ratio.ts +368 -0
  72. package/lib/tools/extract-conversations.ts +114 -0
  73. package/lib/tools/extract-pdf-text.ts +99 -0
  74. package/lib/tools/filter-logs.ts +118 -0
  75. package/lib/tools/find-github-issues.ts +211 -0
  76. package/lib/tools/generate-storyboard-images.ts +455 -0
  77. package/lib/tools/log-cli-utils.ts +262 -0
  78. package/lib/tools/open-github-issue.ts +930 -0
  79. package/lib/tools/read-github-issue.ts +179 -0
  80. package/lib/tools/render-error-book.ts +300 -0
  81. package/lib/tools/render-katex.ts +325 -0
  82. package/lib/tools/review-threads.ts +590 -0
  83. package/lib/tools/search-logs.ts +200 -0
  84. package/lib/tools/sync-memory-index.ts +114 -0
  85. package/lib/tools/validate-openai-agent-config.ts +213 -0
  86. package/lib/tools/validate-skill-frontmatter.ts +124 -0
  87. package/lib/types.ts +90 -0
  88. package/lib/updater.ts +165 -0
  89. package/lib/utils/format.ts +7 -0
  90. package/lib/utils/terminal.ts +22 -0
  91. package/open-github-issue/SKILL.md +2 -2
  92. package/optimise-skill/SKILL.md +1 -1
  93. package/package.json +13 -4
  94. package/resources/project-architecture/assets/architecture.css +764 -0
  95. package/resources/project-architecture/assets/viewer.client.js +144 -0
  96. package/resources/project-architecture/index.html +42 -0
  97. package/review-spec-related-changes/SKILL.md +1 -1
  98. package/solve-issues-found-during-review/SKILL.md +2 -1
  99. package/tsconfig.json +28 -0
  100. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  101. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  102. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  103. package/analyse-app-logs/scripts/filter_logs_by_time.py +0 -64
  104. package/analyse-app-logs/scripts/log_cli_utils.py +0 -112
  105. package/analyse-app-logs/scripts/search_logs.py +0 -137
  106. package/analyse-app-logs/tests/test_filter_logs_by_time.py +0 -95
  107. package/analyse-app-logs/tests/test_search_logs.py +0 -100
  108. package/codex/codex-memory-manager/scripts/extract_recent_conversations.py +0 -369
  109. package/codex/codex-memory-manager/scripts/sync_memory_index.py +0 -130
  110. package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +0 -177
  111. package/codex/codex-memory-manager/tests/test_memory_template.py +0 -37
  112. package/codex/codex-memory-manager/tests/test_sync_memory_index.py +0 -84
  113. package/codex/learn-skill-from-conversations/scripts/extract_recent_conversations.py +0 -369
  114. package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +0 -177
  115. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  116. package/docs-to-voice/scripts/docs_to_voice.py +0 -1385
  117. package/docs-to-voice/scripts/docs_to_voice.sh +0 -11
  118. package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +0 -210
  119. package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +0 -115
  120. package/docs-to-voice/tests/test_docs_to_voice_settings.py +0 -43
  121. package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +0 -51
  122. package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +0 -57
  123. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  124. package/generate-spec/scripts/create-specs +0 -215
  125. package/generate-spec/tests/test_create_specs.py +0 -200
  126. package/init-project-html/scripts/architecture-bootstrap-render.js +0 -16
  127. package/init-project-html/scripts/architecture.js +0 -296
  128. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  129. package/katex/scripts/render_katex.py +0 -247
  130. package/katex/scripts/render_katex.sh +0 -11
  131. package/katex/tests/test_render_katex.py +0 -174
  132. package/learning-error-book/scripts/render_error_book_json_to_pdf.py +0 -590
  133. package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +0 -134
  134. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  135. package/open-github-issue/scripts/open_github_issue.py +0 -705
  136. package/open-github-issue/tests/test_open_github_issue.py +0 -381
  137. package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +0 -763
  138. package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +0 -177
  139. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  140. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  141. package/read-github-issue/scripts/find_issues.py +0 -148
  142. package/read-github-issue/scripts/read_issue.py +0 -108
  143. package/read-github-issue/tests/test_find_issues.py +0 -127
  144. package/read-github-issue/tests/test_read_issue.py +0 -109
  145. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  146. package/resolve-review-comments/scripts/review_threads.py +0 -425
  147. package/resolve-review-comments/tests/test_review_threads.py +0 -74
  148. package/scripts/validate_openai_agent_config.py +0 -209
  149. package/scripts/validate_skill_frontmatter.py +0 -131
  150. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
  151. package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +0 -350
  152. package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +0 -194
  153. package/weekly-financial-event-report/scripts/extract_pdf_text_pdfkit.swift +0 -99
  154. package/weekly-financial-event-report/tests/test_extract_pdf_text_pdfkit.py +0 -64
@@ -0,0 +1,211 @@
1
+ // @ts-nocheck
2
+ import { execFile } from 'node:child_process';
3
+ import type { ToolContext } from '../types';
4
+
5
+ const ISSUE_FIELDS = 'number,title,state,updatedAt,url,labels,assignees';
6
+
7
+ interface FindIssuesArgs {
8
+ repo: string | null;
9
+ state: string;
10
+ limit: number;
11
+ label: string[];
12
+ search: string | null;
13
+ output: 'table' | 'json';
14
+ }
15
+
16
+ function parseArgs(argv: string[]): FindIssuesArgs {
17
+ const args: FindIssuesArgs = {
18
+ repo: null,
19
+ state: 'open',
20
+ limit: 50,
21
+ label: [],
22
+ search: null,
23
+ output: 'table',
24
+ };
25
+
26
+ let i = 0;
27
+ while (i < argv.length) {
28
+ const arg = argv[i];
29
+ switch (arg) {
30
+ case '--repo':
31
+ if (i + 1 < argv.length) args.repo = argv[++i];
32
+ break;
33
+ case '--state':
34
+ if (i + 1 < argv.length) {
35
+ const val = argv[++i];
36
+ if (['open', 'closed', 'all'].includes(val)) args.state = val;
37
+ }
38
+ break;
39
+ case '--limit':
40
+ if (i + 1 < argv.length) {
41
+ const n = parseInt(argv[++i], 10);
42
+ if (n > 0) args.limit = n;
43
+ }
44
+ break;
45
+ case '--label':
46
+ if (i + 1 < argv.length) args.label.push(argv[++i]);
47
+ break;
48
+ case '--search':
49
+ if (i + 1 < argv.length) args.search = argv[++i];
50
+ break;
51
+ case '--output':
52
+ if (i + 1 < argv.length) {
53
+ const val = argv[++i];
54
+ if (val === 'table' || val === 'json') args.output = val;
55
+ }
56
+ break;
57
+ default:
58
+ break;
59
+ }
60
+ i++;
61
+ }
62
+
63
+ return args;
64
+ }
65
+
66
+ interface CommandResult {
67
+ stdout: string;
68
+ stderr: string;
69
+ exitCode: number;
70
+ }
71
+
72
+ function runGh(cmdArgs: string[]): Promise<CommandResult> {
73
+ return new Promise((resolve) => {
74
+ execFile(
75
+ 'gh',
76
+ cmdArgs,
77
+ { maxBuffer: 10 * 1024 * 1024 },
78
+ (error, stdout, stderr) => {
79
+ if (error) {
80
+ resolve({
81
+ stdout: stdout || '',
82
+ stderr: stderr || '',
83
+ exitCode: (error as NodeJS.ErrnoException & { status?: number }).status ?? 1,
84
+ });
85
+ } else {
86
+ resolve({ stdout: stdout || '', stderr: stderr || '', exitCode: 0 });
87
+ }
88
+ },
89
+ );
90
+ });
91
+ }
92
+
93
+ function buildCommand(args: FindIssuesArgs): string[] {
94
+ const cmd: string[] = [
95
+ 'issue',
96
+ 'list',
97
+ '--state',
98
+ args.state,
99
+ '--limit',
100
+ String(args.limit),
101
+ '--json',
102
+ ISSUE_FIELDS,
103
+ ];
104
+
105
+ if (args.repo) {
106
+ cmd.push('--repo', args.repo);
107
+ }
108
+ for (const label of args.label) {
109
+ cmd.push('--label', label);
110
+ }
111
+ if (args.search) {
112
+ cmd.push('--search', args.search);
113
+ }
114
+
115
+ return cmd;
116
+ }
117
+
118
+ function truncate(text: string, width: number): string {
119
+ if (text.length <= width) return text;
120
+ if (width <= 3) return text.slice(0, width);
121
+ return text.slice(0, width - 3) + '...';
122
+ }
123
+
124
+ function formatLabels(issue: Record<string, unknown>): string {
125
+ const labels = issue.labels as Array<Record<string, unknown>> | undefined;
126
+ if (!labels) return '';
127
+ const names = labels
128
+ .map((item) => String(item.name || ''))
129
+ .filter(Boolean);
130
+ return names.join(',');
131
+ }
132
+
133
+ function formatAssignees(issue: Record<string, unknown>): string {
134
+ const assignees = issue.assignees as Array<Record<string, unknown>> | undefined;
135
+ if (!assignees) return '-';
136
+ const logins = assignees
137
+ .map((item) => String(item.login || ''))
138
+ .filter(Boolean);
139
+ return logins.length > 0 ? logins.join(',') : '-';
140
+ }
141
+
142
+ function printTable(
143
+ issues: Array<Record<string, unknown>>,
144
+ context: ToolContext,
145
+ ): void {
146
+ const { stdout } = context;
147
+ const columns = {
148
+ number: 7,
149
+ title: 54,
150
+ labels: 22,
151
+ assignees: 18,
152
+ updated: 20,
153
+ };
154
+
155
+ const header =
156
+ `${'NUMBER'.padEnd(columns.number)} ` +
157
+ `${'TITLE'.padEnd(columns.title)} ` +
158
+ `${'LABELS'.padEnd(columns.labels)} ` +
159
+ `${'ASSIGNEES'.padEnd(columns.assignees)} ` +
160
+ `${'UPDATED'.padEnd(columns.updated)}`;
161
+ stdout.write(header + '\n');
162
+ stdout.write('-'.repeat(header.length) + '\n');
163
+
164
+ for (const issue of issues) {
165
+ const number = `#${issue.number ?? ''}`;
166
+ const title = truncate(String(issue.title ?? ''), columns.title);
167
+ const labels = truncate(formatLabels(issue), columns.labels);
168
+ const assignees = truncate(formatAssignees(issue), columns.assignees);
169
+ const updated = truncate(String(issue.updatedAt ?? ''), columns.updated);
170
+
171
+ const row =
172
+ `${number.padEnd(columns.number)} ` +
173
+ `${title.padEnd(columns.title)} ` +
174
+ `${labels.padEnd(columns.labels)} ` +
175
+ `${assignees.padEnd(columns.assignees)} ` +
176
+ `${updated.padEnd(columns.updated)}`;
177
+ stdout.write(row + '\n');
178
+ }
179
+ }
180
+
181
+ export async function findGitHubIssuesHandler(
182
+ argv: string[],
183
+ context: ToolContext,
184
+ ): Promise<number> {
185
+ const { stdout, stderr } = context;
186
+ const args = parseArgs(argv);
187
+
188
+ const cmd = buildCommand(args);
189
+ const result = await runGh(cmd);
190
+
191
+ if (result.exitCode !== 0) {
192
+ stderr.write(result.stderr.trim() || 'gh issue list failed.\n');
193
+ return result.exitCode;
194
+ }
195
+
196
+ let issues: Array<Record<string, unknown>>;
197
+ try {
198
+ issues = JSON.parse(result.stdout);
199
+ } catch {
200
+ stderr.write('Error: unable to parse gh output as JSON.\n');
201
+ return 1;
202
+ }
203
+
204
+ if (args.output === 'json') {
205
+ stdout.write(JSON.stringify(issues, null, 2) + '\n');
206
+ return 0;
207
+ }
208
+
209
+ printTable(issues, context);
210
+ return 0;
211
+ }
@@ -0,0 +1,455 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import https from 'node:https';
4
+ import http from 'node:http';
5
+ import type { ToolContext } from '../types';
6
+
7
+ interface StoryboardArgs {
8
+ contentName: string | null;
9
+ projectDir: string;
10
+ envFile: string | null;
11
+ apiUrl: string | null;
12
+ apiKey: string | null;
13
+ promptsFile: string | null;
14
+ prompts: string[];
15
+ imageModel: string | null;
16
+ aspectRatio: string | null;
17
+ imageSize: string | null;
18
+ quality: string | null;
19
+ style: string | null;
20
+ help: boolean;
21
+ }
22
+
23
+ function parseArgs(args: string[]): StoryboardArgs {
24
+ const parsed: StoryboardArgs = {
25
+ contentName: null,
26
+ projectDir: '.',
27
+ envFile: null,
28
+ apiUrl: null,
29
+ apiKey: null,
30
+ promptsFile: null,
31
+ prompts: [],
32
+ imageModel: null,
33
+ aspectRatio: null,
34
+ imageSize: null,
35
+ quality: null,
36
+ style: null,
37
+ help: false,
38
+ };
39
+
40
+ for (let i = 0; i < args.length; i++) {
41
+ const arg = args[i];
42
+ if (arg === '--help' || arg === '-h') {
43
+ parsed.help = true;
44
+ continue;
45
+ }
46
+ if (arg.startsWith('--')) {
47
+ const eqIndex = arg.indexOf('=');
48
+ let key: string;
49
+ let value: string;
50
+
51
+ if (eqIndex !== -1) {
52
+ key = arg.slice(2, eqIndex);
53
+ value = arg.slice(eqIndex + 1);
54
+ } else {
55
+ key = arg.slice(2);
56
+ if (key === 'prompt') {
57
+ // --prompt can be multiple
58
+ value = args[++i] || '';
59
+ parsed.prompts.push(value);
60
+ continue;
61
+ }
62
+ value = args[++i] || '';
63
+ }
64
+
65
+ switch (key) {
66
+ case 'input':
67
+ case 'content-name':
68
+ parsed.contentName = value;
69
+ break;
70
+ case 'project-dir':
71
+ parsed.projectDir = value;
72
+ break;
73
+ case 'env-file':
74
+ parsed.envFile = value;
75
+ break;
76
+ case 'api-url':
77
+ parsed.apiUrl = value;
78
+ break;
79
+ case 'api-key':
80
+ parsed.apiKey = value;
81
+ break;
82
+ case 'prompts-file':
83
+ parsed.promptsFile = value;
84
+ break;
85
+ case 'image-model':
86
+ parsed.imageModel = value;
87
+ break;
88
+ case 'aspect-ratio':
89
+ parsed.aspectRatio = value;
90
+ break;
91
+ case 'image-size':
92
+ case 'size':
93
+ parsed.imageSize = value;
94
+ break;
95
+ case 'quality':
96
+ parsed.quality = value;
97
+ break;
98
+ case 'style':
99
+ parsed.style = value;
100
+ break;
101
+ }
102
+ } else if (!parsed.contentName && !arg.startsWith('-')) {
103
+ // positional: content name
104
+ parsed.contentName = arg;
105
+ }
106
+ }
107
+
108
+ return parsed;
109
+ }
110
+
111
+ function sanitizeComponent(name: string, fallback: string): string {
112
+ return name
113
+ .replace(/[\\/:*?"<>|]+/g, '_')
114
+ .replace(/\s+/g, '_')
115
+ .replace(/_+/g, '_')
116
+ .replace(/^[._]|[._]$/g, '') || fallback;
117
+ }
118
+
119
+ function uniquePath(filePath: string): string {
120
+ if (!fs.existsSync(filePath)) return filePath;
121
+ const dir = path.dirname(filePath);
122
+ const ext = path.extname(filePath);
123
+ const base = path.basename(filePath, ext);
124
+ let index = 2;
125
+ while (fs.existsSync(path.join(dir, `${base}_${index}${ext}`))) {
126
+ index++;
127
+ }
128
+ return path.join(dir, `${base}_${index}${ext}`);
129
+ }
130
+
131
+ function postJson(
132
+ baseUrl: string,
133
+ endpoint: string,
134
+ apiKey: string,
135
+ payload: Record<string, unknown>,
136
+ ): Promise<Record<string, unknown>> {
137
+ return new Promise((resolve, reject) => {
138
+ const url = `${baseUrl.replace(/\/+$/, '')}${endpoint}`;
139
+ const urlObj = new URL(url);
140
+ const body = JSON.stringify(payload);
141
+ const client = urlObj.protocol === 'https:' ? https : http;
142
+
143
+ const options = {
144
+ hostname: urlObj.hostname,
145
+ port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
146
+ path: urlObj.pathname + urlObj.search,
147
+ method: 'POST',
148
+ headers: {
149
+ 'Authorization': `Bearer ${apiKey}`,
150
+ 'Content-Type': 'application/json',
151
+ },
152
+ timeout: 180000,
153
+ };
154
+
155
+ const req = client.request(options, (res) => {
156
+ const chunks: Buffer[] = [];
157
+ res.on('data', (chunk: Buffer) => chunks.push(chunk));
158
+ res.on('end', () => {
159
+ const raw = Buffer.concat(chunks).toString('utf-8');
160
+ try {
161
+ resolve(JSON.parse(raw));
162
+ } catch {
163
+ reject(new Error(`Invalid JSON response: ${raw.slice(0, 200)}`));
164
+ }
165
+ });
166
+ res.on('error', reject);
167
+ });
168
+
169
+ req.on('error', reject);
170
+ req.on('timeout', () => { req.destroy(); reject(new Error('API request timed out')); });
171
+ req.write(body);
172
+ req.end();
173
+ });
174
+ }
175
+
176
+ function fetchBinary(url: string): Promise<Buffer> {
177
+ return new Promise((resolve, reject) => {
178
+ const urlObj = new URL(url);
179
+ const client = urlObj.protocol === 'https:' ? https : http;
180
+
181
+ client.get(url, { timeout: 180000 }, (res) => {
182
+ const chunks: Buffer[] = [];
183
+ res.on('data', (chunk: Buffer) => chunks.push(chunk));
184
+ res.on('end', () => resolve(Buffer.concat(chunks)));
185
+ res.on('error', reject);
186
+ }).on('error', reject);
187
+ });
188
+ }
189
+
190
+ function parsePromptEntries(raw: unknown[]): Array<{ title: string; prompt: string }> {
191
+ const items: Array<{ title: string; prompt: string }> = [];
192
+ for (let i = 0; i < raw.length; i++) {
193
+ const item = raw[i];
194
+ if (typeof item === 'string') {
195
+ const prompt = item.trim();
196
+ if (!prompt) throw new Error(`Empty prompt at index ${i}`);
197
+ items.push({ title: `scene-${i + 1}`, prompt });
198
+ } else if (item && typeof item === 'object') {
199
+ const obj = item as Record<string, unknown>;
200
+ const prompt = String(obj.prompt || '').trim();
201
+ const title = String(obj.title || `scene-${i + 1}`).trim() || `scene-${i + 1}`;
202
+ if (!prompt) throw new Error(`Empty prompt in object at index ${i}`);
203
+ items.push({ title, prompt });
204
+ } else {
205
+ throw new Error(`Invalid item type at index ${i}: expected string or object`);
206
+ }
207
+ }
208
+ if (items.length === 0) throw new Error('No prompts found.');
209
+ return items;
210
+ }
211
+
212
+ function parsePromptsFile(filePath: string): Array<{ title: string; prompt: string }> {
213
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
214
+
215
+ if (Array.isArray(raw)) {
216
+ return parsePromptEntries(raw);
217
+ }
218
+
219
+ if (raw && typeof raw === 'object') {
220
+ const scenes = raw.scenes;
221
+ if (!Array.isArray(scenes) || scenes.length === 0) {
222
+ throw new Error('Object mode requires a top-level "scenes" array.');
223
+ }
224
+
225
+ const characters: Record<string, Record<string, string>> = {};
226
+ if (Array.isArray(raw.characters)) {
227
+ for (const char of raw.characters) {
228
+ if (!char || typeof char !== 'object') continue;
229
+ const c = char as Record<string, unknown>;
230
+ const id = String(c.id || '').trim();
231
+ if (id) {
232
+ characters[id] = {
233
+ name: String(c.name || ''),
234
+ appearance: String(c.appearance || ''),
235
+ outfit: String(c.outfit || ''),
236
+ description: String(c.description || ''),
237
+ };
238
+ }
239
+ }
240
+ }
241
+
242
+ const items: Array<{ title: string; prompt: string }> = [];
243
+ for (let si = 0; si < scenes.length; si++) {
244
+ const scene = scenes[si] as Record<string, unknown>;
245
+ if (!scene || typeof scene !== 'object') {
246
+ throw new Error(`Invalid scene at index ${si}: expected object.`);
247
+ }
248
+
249
+ const title = String(scene.title || `scene-${si + 1}`).trim() || `scene-${si + 1}`;
250
+ const description = String(scene.description || '').trim();
251
+ if (!description) throw new Error(`Scene ${si}: 'description' is required.`);
252
+
253
+ let promptPayload: Record<string, unknown> = {
254
+ scene_title: title,
255
+ description,
256
+ };
257
+
258
+ const characterIds: string[] = [];
259
+ if (Array.isArray(scene.character_ids)) {
260
+ for (const cid of scene.character_ids) {
261
+ characterIds.push(String(cid).trim());
262
+ }
263
+ const sceneChars = characterIds
264
+ .map((cid) => characters[cid])
265
+ .filter(Boolean);
266
+ if (sceneChars.length > 0) {
267
+ promptPayload.characters = sceneChars;
268
+ }
269
+ }
270
+
271
+ if (scene.style) promptPayload.style = String(scene.style);
272
+ if (scene.camera) promptPayload.camera = String(scene.camera);
273
+ if (scene.lighting) promptPayload.lighting = String(scene.lighting);
274
+
275
+ items.push({
276
+ title,
277
+ prompt: JSON.stringify(promptPayload, undefined, 0),
278
+ });
279
+ }
280
+ return items;
281
+ }
282
+
283
+ throw new Error('Top-level JSON must be an array or an object.');
284
+ }
285
+
286
+ export async function generateStoryboardImagesHandler(args: string[], context: ToolContext): Promise<number> {
287
+ const stdout = context.stdout || process.stdout;
288
+ const stderr = context.stderr || process.stderr;
289
+
290
+ try {
291
+ const opts = parseArgs(args);
292
+
293
+ if (opts.help || (!opts.contentName)) {
294
+ stdout.write(`Usage: apltk generate-storyboard-images --input <name> [options]
295
+
296
+ Generate storyboard images from prompts via OpenAI-compatible API.
297
+
298
+ Options:
299
+ --input, --content-name <name> Output subfolder name under pictures/
300
+ --project-dir <path> Project root (default: .)
301
+ --env-file <path> Path to .env file
302
+ --api-url <url> API base URL for /images/generations
303
+ --api-key <key> API key
304
+ --prompts-file <path> JSON file with prompt entries
305
+ --prompt <text> Image prompt (repeatable)
306
+ --image-model <model> Image model (default: gpt-image-1)
307
+ --aspect-ratio <ratio> Aspect ratio, e.g. 16:9
308
+ --image-size <size> Image size, e.g. 1024x768
309
+ --quality <q> Image quality
310
+ --style <style> Image style
311
+
312
+ Either --prompts-file or at least one --prompt is required.
313
+ `);
314
+ return opts.contentName ? 1 : 0;
315
+ }
316
+
317
+ const projectDir = path.resolve(opts.projectDir);
318
+ const contentName = opts.contentName;
319
+
320
+ // Resolve API config
321
+ const sourceRoot = context.sourceRoot || path.resolve(__dirname, '..', '..');
322
+
323
+ // Try loading env file
324
+ const envFilePath = opts.envFile
325
+ ? path.resolve(opts.envFile)
326
+ : path.join(sourceRoot, 'openai-text-to-image-storyboard', '.env');
327
+ if (fs.existsSync(envFilePath)) {
328
+ const envContent = fs.readFileSync(envFilePath, 'utf-8');
329
+ for (const line of envContent.split('\n')) {
330
+ const trimmed = line.trim();
331
+ if (!trimmed || trimmed.startsWith('#')) continue;
332
+ const eqIndex = trimmed.indexOf('=');
333
+ if (eqIndex === -1) continue;
334
+ let key = trimmed.slice(0, eqIndex).trim();
335
+ let val = trimmed.slice(eqIndex + 1).trim();
336
+ if (key.startsWith('export ')) key = key.slice(7).trim();
337
+ // Strip quotes
338
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
339
+ val = val.slice(1, -1);
340
+ }
341
+ if (!process.env[key]) {
342
+ process.env[key] = val;
343
+ }
344
+ }
345
+ }
346
+
347
+ const apiUrl = opts.apiUrl || process.env.OPENAI_API_URL || '';
348
+ const apiKey = opts.apiKey || process.env.OPENAI_API_KEY || '';
349
+ const imageModel = opts.imageModel || process.env.OPENAI_IMAGE_MODEL || 'gpt-image-1';
350
+ const aspectRatio = opts.aspectRatio || process.env.OPENAI_IMAGE_RATIO || process.env.OPENAI_IMAGE_ASPECT_RATIO || null;
351
+ const imageSize = opts.imageSize || process.env.OPENAI_IMAGE_SIZE || null;
352
+ const quality = opts.quality || process.env.OPENAI_IMAGE_QUALITY || null;
353
+ const style = opts.style || process.env.OPENAI_IMAGE_STYLE || null;
354
+
355
+ if (!apiUrl) {
356
+ stderr.write('Error: Missing API URL. Set --api-url or OPENAI_API_URL.\n');
357
+ return 1;
358
+ }
359
+ if (!apiKey) {
360
+ stderr.write('Error: Missing API key. Set --api-key or OPENAI_API_KEY.\n');
361
+ return 1;
362
+ }
363
+
364
+ // Build prompt items
365
+ let promptItems: Array<{ title: string; prompt: string }>;
366
+ if (opts.promptsFile) {
367
+ promptItems = parsePromptsFile(path.resolve(opts.promptsFile));
368
+ } else if (opts.prompts.length > 0) {
369
+ promptItems = opts.prompts.map((p, i) => ({ title: `scene-${i + 1}`, prompt: p.trim() }));
370
+ } else {
371
+ stderr.write('Error: Either --prompts-file or at least one --prompt is required.\n');
372
+ return 1;
373
+ }
374
+
375
+ if (promptItems.length === 0) {
376
+ stderr.write('Error: No prompts provided.\n');
377
+ return 1;
378
+ }
379
+
380
+ const outputDir = path.join(projectDir, 'pictures', sanitizeComponent(contentName, 'untitled-content'));
381
+ fs.mkdirSync(outputDir, { recursive: true });
382
+
383
+ const records: Array<Record<string, unknown>> = [];
384
+
385
+ for (let i = 0; i < promptItems.length; i++) {
386
+ const item = promptItems[i];
387
+ const titleSlug = sanitizeComponent(item.title, `scene-${i + 1}`);
388
+ const imagePath = uniquePath(path.join(outputDir, `${String(i + 1).padStart(2, '0')}_${titleSlug}.png`));
389
+
390
+ const payload: Record<string, unknown> = {
391
+ model: imageModel,
392
+ prompt: item.prompt,
393
+ };
394
+ if (aspectRatio) payload.aspect_ratio = aspectRatio;
395
+ if (imageSize) payload.size = imageSize;
396
+ if (quality) payload.quality = quality;
397
+ if (style) payload.style = style;
398
+
399
+ const response = await postJson(apiUrl, '/images/generations', apiKey, payload);
400
+
401
+ const data = response.data;
402
+ if (!Array.isArray(data) || data.length === 0) {
403
+ stderr.write(`Error: No image data returned for prompt ${i + 1}.\n`);
404
+ continue;
405
+ }
406
+
407
+ const first = data[0] as Record<string, unknown>;
408
+ let imageBytes: Buffer;
409
+
410
+ if (typeof first.b64_json === 'string') {
411
+ imageBytes = Buffer.from(first.b64_json, 'base64');
412
+ } else if (typeof first.url === 'string') {
413
+ imageBytes = await fetchBinary(first.url);
414
+ } else {
415
+ stderr.write(`Error: Image payload missing b64_json/url for prompt ${i + 1}.\n`);
416
+ continue;
417
+ }
418
+
419
+ fs.writeFileSync(imagePath, imageBytes);
420
+
421
+ const record: Record<string, unknown> = {
422
+ index: i + 1,
423
+ title: item.title,
424
+ prompt: item.prompt,
425
+ file: imagePath,
426
+ };
427
+ if (typeof first.revised_prompt === 'string') {
428
+ record.revised_prompt = first.revised_prompt;
429
+ }
430
+ records.push(record);
431
+ stdout.write(`[OK] Generated ${imagePath}\n`);
432
+ }
433
+
434
+ // Write summary
435
+ const summary: Record<string, unknown> = {
436
+ content_name: contentName,
437
+ project_dir: projectDir,
438
+ output_dir: outputDir,
439
+ image_model: imageModel,
440
+ images: records,
441
+ };
442
+ if (aspectRatio) summary.aspect_ratio = aspectRatio;
443
+ if (imageSize) summary.image_size = imageSize;
444
+
445
+ const summaryPath = path.join(outputDir, 'storyboard.json');
446
+ fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf-8');
447
+ stdout.write(`[OK] Wrote plan to ${summaryPath}\n`);
448
+
449
+ return 0;
450
+ } catch (err: unknown) {
451
+ const msg = err instanceof Error ? err.message : 'Unknown error';
452
+ stderr.write(`Error: ${msg}\n`);
453
+ return 1;
454
+ }
455
+ }