@laitszkin/apollo-toolkit 3.13.2 → 3.14.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 (154) hide show
  1. package/AGENTS.md +7 -7
  2. package/CHANGELOG.md +27 -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 +34 -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 +184 -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 +34 -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 +209 -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,930 @@
1
+ // @ts-nocheck
2
+ import { execFile } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
4
+ import { request as httpsRequest } from 'node:https';
5
+ import { request as httpRequest } from 'node:http';
6
+ import { tmpdir } from 'node:os';
7
+ import { join as joinPath } from 'node:path';
8
+ import { cwd } from 'node:process';
9
+ import type { ToolContext } from '../types';
10
+
11
+ const GITHUB_API_BASE = 'https://api.github.com';
12
+ const README_ACCEPT = 'application/vnd.github.raw+json';
13
+ const JSON_ACCEPT = 'application/vnd.github+json';
14
+ const DEFAULT_REPRO_ZH =
15
+ '尚未穩定重現;需補充更多執行期資料。';
16
+ const DEFAULT_REPRO_EN =
17
+ 'Not yet reliably reproducible; more runtime evidence is required.';
18
+
19
+ const ISSUE_TYPE_PROBLEM = 'problem';
20
+ const ISSUE_TYPE_FEATURE = 'feature';
21
+ const ISSUE_TYPE_PERFORMANCE = 'performance';
22
+ const ISSUE_TYPE_SECURITY = 'security';
23
+ const ISSUE_TYPE_DOCS = 'docs';
24
+ const ISSUE_TYPE_OBSERVABILITY = 'observability';
25
+
26
+ const ISSUE_TYPES = [
27
+ ISSUE_TYPE_PROBLEM,
28
+ ISSUE_TYPE_FEATURE,
29
+ ISSUE_TYPE_PERFORMANCE,
30
+ ISSUE_TYPE_SECURITY,
31
+ ISSUE_TYPE_DOCS,
32
+ ISSUE_TYPE_OBSERVABILITY,
33
+ ] as const;
34
+
35
+ const PROBLEM_BDD_MARKER_GROUPS: [RegExp, RegExp, RegExp][] = [
36
+ [
37
+ /Expected Behavior\s*\(BDD\)/i,
38
+ /Current Behavior\s*\(BDD\)/i,
39
+ /Behavior Gap/i,
40
+ ],
41
+ [
42
+ /預期行為\s*[((]BDD[))]/i,
43
+ /(?:目前|當前)行為\s*[((]BDD[))]/i,
44
+ /行為(?:落差|差異)/i,
45
+ ],
46
+ ];
47
+
48
+ const TEXT_FIELDS = [
49
+ 'title',
50
+ 'problem_description',
51
+ 'suspected_cause',
52
+ 'reproduction',
53
+ 'proposal',
54
+ 'reason',
55
+ 'suggested_architecture',
56
+ 'impact',
57
+ 'evidence',
58
+ 'suggested_action',
59
+ 'affected_scope',
60
+ ] as const;
61
+
62
+ const PAYLOAD_FIELDS = new Set([
63
+ 'title',
64
+ 'issue_type',
65
+ 'problem_description',
66
+ 'suspected_cause',
67
+ 'reproduction',
68
+ 'proposal',
69
+ 'reason',
70
+ 'suggested_architecture',
71
+ 'impact',
72
+ 'evidence',
73
+ 'suggested_action',
74
+ 'severity',
75
+ 'affected_scope',
76
+ 'repo',
77
+ 'dry_run',
78
+ ]);
79
+
80
+ interface OpenIssueArgs {
81
+ payloadFile: string | null;
82
+ title: string | null;
83
+ issueType: string | null;
84
+ problemDescription: string | null;
85
+ suspectedCause: string | null;
86
+ reproduction: string | null;
87
+ proposal: string | null;
88
+ reason: string | null;
89
+ suggestedArchitecture: string | null;
90
+ impact: string | null;
91
+ evidence: string | null;
92
+ suggestedAction: string | null;
93
+ severity: string | null;
94
+ affectedScope: string | null;
95
+ repo: string | null;
96
+ dryRun: boolean;
97
+ }
98
+
99
+ function parseArgs(argv: string[]): OpenIssueArgs {
100
+ const args: OpenIssueArgs = {
101
+ payloadFile: null,
102
+ title: null,
103
+ issueType: null,
104
+ problemDescription: null,
105
+ suspectedCause: null,
106
+ reproduction: null,
107
+ proposal: null,
108
+ reason: null,
109
+ suggestedArchitecture: null,
110
+ impact: null,
111
+ evidence: null,
112
+ suggestedAction: null,
113
+ severity: null,
114
+ affectedScope: null,
115
+ repo: null,
116
+ dryRun: false,
117
+ };
118
+
119
+ let i = 0;
120
+ while (i < argv.length) {
121
+ const arg = argv[i];
122
+ switch (arg) {
123
+ case '--payload-file':
124
+ if (i + 1 < argv.length) args.payloadFile = argv[++i];
125
+ break;
126
+ case '--title':
127
+ if (i + 1 < argv.length) args.title = argv[++i];
128
+ break;
129
+ case '--issue-type':
130
+ if (i + 1 < argv.length) args.issueType = argv[++i];
131
+ break;
132
+ case '--problem-description':
133
+ if (i + 1 < argv.length) args.problemDescription = argv[++i];
134
+ break;
135
+ case '--suspected-cause':
136
+ if (i + 1 < argv.length) args.suspectedCause = argv[++i];
137
+ break;
138
+ case '--reproduction':
139
+ if (i + 1 < argv.length) args.reproduction = argv[++i];
140
+ break;
141
+ case '--proposal':
142
+ if (i + 1 < argv.length) args.proposal = argv[++i];
143
+ break;
144
+ case '--reason':
145
+ if (i + 1 < argv.length) args.reason = argv[++i];
146
+ break;
147
+ case '--suggested-architecture':
148
+ if (i + 1 < argv.length) args.suggestedArchitecture = argv[++i];
149
+ break;
150
+ case '--impact':
151
+ if (i + 1 < argv.length) args.impact = argv[++i];
152
+ break;
153
+ case '--evidence':
154
+ if (i + 1 < argv.length) args.evidence = argv[++i];
155
+ break;
156
+ case '--suggested-action':
157
+ if (i + 1 < argv.length) args.suggestedAction = argv[++i];
158
+ break;
159
+ case '--severity':
160
+ if (i + 1 < argv.length) args.severity = argv[++i];
161
+ break;
162
+ case '--affected-scope':
163
+ if (i + 1 < argv.length) args.affectedScope = argv[++i];
164
+ break;
165
+ case '--repo':
166
+ if (i + 1 < argv.length) args.repo = argv[++i];
167
+ break;
168
+ case '--dry-run':
169
+ args.dryRun = true;
170
+ break;
171
+ default:
172
+ if (!arg.startsWith('-')) {
173
+ // unsupported positional — ignore per Python behavior
174
+ }
175
+ break;
176
+ }
177
+ i++;
178
+ }
179
+
180
+ return args;
181
+ }
182
+
183
+ // ---- Utilities ----
184
+
185
+ interface CommandResult {
186
+ stdout: string;
187
+ stderr: string;
188
+ exitCode: number;
189
+ }
190
+
191
+ function runCommand(cmd: string, cmdArgs: string[]): Promise<CommandResult> {
192
+ return new Promise((resolve) => {
193
+ execFile(cmd, cmdArgs, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
194
+ if (error) {
195
+ resolve({
196
+ stdout: stdout || '',
197
+ stderr: stderr || '',
198
+ exitCode: (error as NodeJS.ErrnoException & { status?: number }).status ?? 1,
199
+ });
200
+ } else {
201
+ resolve({ stdout: stdout || '', stderr: stderr || '', exitCode: 0 });
202
+ }
203
+ });
204
+ });
205
+ }
206
+
207
+ function normalizeKey(key: string): string {
208
+ return key.replace(/-/g, '_');
209
+ }
210
+
211
+ interface PayloadEntry {
212
+ [key: string]: unknown;
213
+ }
214
+
215
+ function readPayloadFile(rawPath: string): PayloadEntry {
216
+ let rawContent: string;
217
+ let context: string;
218
+
219
+ if (rawPath === '-') {
220
+ // We cannot read stdin here easily; throw clear error
221
+ throw new Error('stdin payload (-) is not supported in handler mode; use a file path');
222
+ } else {
223
+ rawContent = readFileSync(rawPath, 'utf-8');
224
+ context = rawPath;
225
+ }
226
+
227
+ let payload: unknown;
228
+ try {
229
+ payload = JSON.parse(rawContent);
230
+ } catch (exc) {
231
+ throw new Error(`Invalid JSON payload in ${context}: ${(exc as Error).message}`);
232
+ }
233
+
234
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
235
+ throw new Error(`Invalid JSON payload in ${context}: top-level value must be an object.`);
236
+ }
237
+
238
+ const normalized: PayloadEntry = {};
239
+ for (const [rawKey, value] of Object.entries(payload as Record<string, unknown>)) {
240
+ const key = normalizeKey(rawKey);
241
+ if (!PAYLOAD_FIELDS.has(key)) {
242
+ throw new Error(`Unsupported payload key: ${rawKey}`);
243
+ }
244
+ normalized[key] = value;
245
+ }
246
+ return normalized;
247
+ }
248
+
249
+ function readAtFileValue(fieldName: string, value: string | null): string | null {
250
+ if (value === null) return null;
251
+ if (value.startsWith('@@')) return value.slice(1);
252
+ if (value === '@-') {
253
+ throw new Error('stdin reading (@-) is not supported in handler mode');
254
+ }
255
+ if (value.startsWith('@') && value.length > 1) {
256
+ const filePath = value.slice(1);
257
+ try {
258
+ return readFileSync(filePath, 'utf-8');
259
+ } catch (exc) {
260
+ throw new Error(
261
+ `Unable to read @${fieldName} file ${filePath}: ${(exc as Error).message}`,
262
+ );
263
+ }
264
+ }
265
+ return value;
266
+ }
267
+
268
+ function requireNonEmpty(value: string | null | undefined, message: string): void {
269
+ if (!(value || '').trim()) {
270
+ throw new Error(message);
271
+ }
272
+ }
273
+
274
+ function hasRequiredProblemBddSections(problemDescription: string): boolean {
275
+ const normalized = problemDescription.trim();
276
+ return PROBLEM_BDD_MARKER_GROUPS.some((group) =>
277
+ group.every((pattern) => pattern.test(normalized)),
278
+ );
279
+ }
280
+
281
+ function hasGhAuth(): Promise<boolean> {
282
+ return runCommand('gh', ['auth', 'status']).then((r) => r.exitCode === 0);
283
+ }
284
+
285
+ function getToken(env: Record<string, string | undefined>): string | null {
286
+ return env.GITHUB_TOKEN || env.GH_TOKEN || null;
287
+ }
288
+
289
+ function resolveRepo(explicitRepo: string | null): string {
290
+ if (explicitRepo) {
291
+ return validateRepo(explicitRepo);
292
+ }
293
+ // Try to resolve from git remote
294
+ const result = runCommand('git', [
295
+ 'remote', 'get-url', 'origin',
296
+ ]);
297
+ // This is sync because it's called in a non-async context flow
298
+ // Actually, let me use the promisified version — but we need to make the caller async
299
+ // For now, throw a helpful error
300
+ throw new Error(
301
+ '--repo is required in handler mode. Unable to auto-detect from git remote in-process.',
302
+ );
303
+ }
304
+
305
+ function validateRepo(repo: string): string {
306
+ const candidate = repo.trim();
307
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(candidate)) {
308
+ throw new Error('Invalid repo format. Use owner/repo.');
309
+ }
310
+ return candidate;
311
+ }
312
+
313
+ // ---- HTTP helpers ----
314
+
315
+ function githubRequest(
316
+ method: string,
317
+ path: string,
318
+ token: string | null,
319
+ accept: string,
320
+ payload?: Record<string, unknown>,
321
+ ): Promise<string> {
322
+ return new Promise((resolve, reject) => {
323
+ const headers: Record<string, string> = {
324
+ Accept: accept,
325
+ 'User-Agent': 'open-github-issue-skill',
326
+ 'X-GitHub-Api-Version': '2022-11-28',
327
+ };
328
+
329
+ if (token) {
330
+ headers['Authorization'] = `Bearer ${token}`;
331
+ }
332
+
333
+ let body: string | undefined;
334
+ if (payload !== undefined) {
335
+ body = JSON.stringify(payload);
336
+ headers['Content-Type'] = 'application/json';
337
+ }
338
+
339
+ const url = new URL(`${GITHUB_API_BASE}${path}`);
340
+ const req = httpsRequest(
341
+ url,
342
+ {
343
+ method,
344
+ headers,
345
+ },
346
+ (res) => {
347
+ let data = '';
348
+ res.on('data', (chunk: Buffer) => {
349
+ data += chunk.toString('utf-8');
350
+ });
351
+ res.on('end', () => {
352
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
353
+ resolve(data);
354
+ } else {
355
+ const detail = data || 'unknown error';
356
+ reject(
357
+ new Error(`GitHub API ${res.statusCode} ${path}: ${detail}`),
358
+ );
359
+ }
360
+ });
361
+ },
362
+ );
363
+
364
+ req.on('error', (err) => {
365
+ reject(new Error(`GitHub API request failed for ${path}: ${err.message}`));
366
+ });
367
+
368
+ if (body !== undefined) {
369
+ req.write(body);
370
+ }
371
+ req.end();
372
+ });
373
+ }
374
+
375
+ async function fetchRemoteReadme(
376
+ repo: string,
377
+ ghAuthenticated: boolean,
378
+ token: string | null,
379
+ ): Promise<string> {
380
+ if (ghAuthenticated) {
381
+ const result = await runCommand('gh', [
382
+ 'api',
383
+ '-H',
384
+ `Accept: ${README_ACCEPT}`,
385
+ `repos/${repo}/readme`,
386
+ ]);
387
+ if (result.exitCode === 0) {
388
+ return result.stdout;
389
+ }
390
+ }
391
+
392
+ try {
393
+ return await githubRequest('GET', `/repos/${repo}/readme`, token, README_ACCEPT);
394
+ } catch {
395
+ return '';
396
+ }
397
+ }
398
+
399
+ function detectIssueLanguage(readmeContent: string): string {
400
+ if (!readmeContent.trim()) return 'en';
401
+
402
+ const chineseChars = (readmeContent.match(/[一-鿿]/g) || []).length;
403
+ const languageChars = (
404
+ readmeContent.match(/[A-Za-z一-鿿]/g) || []
405
+ ).length;
406
+
407
+ if (chineseChars >= 20 && languageChars > 0 && chineseChars / languageChars >= 0.08) {
408
+ return 'zh';
409
+ }
410
+ return 'en';
411
+ }
412
+
413
+ // ---- Issue body builder ----
414
+
415
+ function buildIssueBody(params: {
416
+ issueType: string;
417
+ language: string;
418
+ title: string;
419
+ problemDescription: string | null;
420
+ suspectedCause: string | null;
421
+ reproduction: string | null;
422
+ proposal: string | null;
423
+ reason: string | null;
424
+ suggestedArchitecture: string | null;
425
+ impact: string | null;
426
+ evidence: string | null;
427
+ suggestedAction: string | null;
428
+ severity: string | null;
429
+ affectedScope: string | null;
430
+ }): string {
431
+ const {
432
+ issueType,
433
+ language,
434
+ title,
435
+ problemDescription,
436
+ suspectedCause,
437
+ reproduction,
438
+ proposal,
439
+ reason,
440
+ suggestedArchitecture,
441
+ impact,
442
+ evidence,
443
+ suggestedAction,
444
+ severity,
445
+ affectedScope,
446
+ } = params;
447
+
448
+ if (issueType === ISSUE_TYPE_FEATURE) {
449
+ const proposalText = (proposal || title).trim();
450
+ const reasonText = (reason || '').trim();
451
+ const architectureText = (suggestedArchitecture || '').trim();
452
+
453
+ if (language === 'zh') {
454
+ return (
455
+ '### 功能提案\n' +
456
+ `${proposalText}\n\n` +
457
+ '### 原因\n' +
458
+ `${reasonText}\n\n` +
459
+ '### 建議架構\n' +
460
+ `${architectureText}\n`
461
+ );
462
+ }
463
+
464
+ return (
465
+ '### Feature Proposal\n' +
466
+ `${proposalText}\n\n` +
467
+ '### Why This Is Needed\n' +
468
+ `${reasonText}\n\n` +
469
+ '### Suggested Architecture\n' +
470
+ `${architectureText}\n`
471
+ );
472
+ }
473
+
474
+ if (issueType === ISSUE_TYPE_PERFORMANCE) {
475
+ if (language === 'zh') {
476
+ return (
477
+ '### 效能問題\n' +
478
+ `${(problemDescription || '').trim()}\n\n` +
479
+ '### 影響\n' +
480
+ `${(impact || '').trim()}\n\n` +
481
+ '### 證據\n' +
482
+ `${(evidence || '').trim()}\n\n` +
483
+ '### 建議行動\n' +
484
+ `${(suggestedAction || '').trim()}\n`
485
+ );
486
+ }
487
+ return (
488
+ '### Performance Problem\n' +
489
+ `${(problemDescription || '').trim()}\n\n` +
490
+ '### Impact\n' +
491
+ `${(impact || '').trim()}\n\n` +
492
+ '### Evidence\n' +
493
+ `${(evidence || '').trim()}\n\n` +
494
+ '### Suggested Action\n' +
495
+ `${(suggestedAction || '').trim()}\n`
496
+ );
497
+ }
498
+
499
+ if (issueType === ISSUE_TYPE_SECURITY) {
500
+ if (language === 'zh') {
501
+ return (
502
+ '### 安全風險\n' +
503
+ `${(problemDescription || '').trim()}\n\n` +
504
+ '### 嚴重程度\n' +
505
+ `${(severity || '').trim()}\n\n` +
506
+ '### 受影響範圍\n' +
507
+ `${(affectedScope || '').trim()}\n\n` +
508
+ '### 影響\n' +
509
+ `${(impact || '').trim()}\n\n` +
510
+ '### 證據\n' +
511
+ `${(evidence || '').trim()}\n\n` +
512
+ '### 建議緩解\n' +
513
+ `${(suggestedAction || '').trim()}\n`
514
+ );
515
+ }
516
+ return (
517
+ '### Security Risk\n' +
518
+ `${(problemDescription || '').trim()}\n\n` +
519
+ '### Severity\n' +
520
+ `${(severity || '').trim()}\n\n` +
521
+ '### Affected Scope\n' +
522
+ `${(affectedScope || '').trim()}\n\n` +
523
+ '### Impact\n' +
524
+ `${(impact || '').trim()}\n\n` +
525
+ '### Evidence\n' +
526
+ `${(evidence || '').trim()}\n\n` +
527
+ '### Suggested Mitigation\n' +
528
+ `${(suggestedAction || '').trim()}\n`
529
+ );
530
+ }
531
+
532
+ if (issueType === ISSUE_TYPE_DOCS) {
533
+ if (language === 'zh') {
534
+ return (
535
+ '### 文件缺口\n' +
536
+ `${(problemDescription || '').trim()}\n\n` +
537
+ '### 證據\n' +
538
+ `${(evidence || '').trim()}\n\n` +
539
+ '### 建議更新\n' +
540
+ `${(suggestedAction || '').trim()}\n`
541
+ );
542
+ }
543
+ return (
544
+ '### Documentation Gap\n' +
545
+ `${(problemDescription || '').trim()}\n\n` +
546
+ '### Evidence\n' +
547
+ `${(evidence || '').trim()}\n\n` +
548
+ '### Suggested Update\n' +
549
+ `${(suggestedAction || '').trim()}\n`
550
+ );
551
+ }
552
+
553
+ if (issueType === ISSUE_TYPE_OBSERVABILITY) {
554
+ if (language === 'zh') {
555
+ return (
556
+ '### 可觀測性缺口\n' +
557
+ `${(problemDescription || '').trim()}\n\n` +
558
+ '### 影響\n' +
559
+ `${(impact || '').trim()}\n\n` +
560
+ '### 證據\n' +
561
+ `${(evidence || '').trim()}\n\n` +
562
+ '### 建議儀表化\n' +
563
+ `${(suggestedAction || '').trim()}\n`
564
+ );
565
+ }
566
+ return (
567
+ '### Observability Gap\n' +
568
+ `${(problemDescription || '').trim()}\n\n` +
569
+ '### Impact\n' +
570
+ `${(impact || '').trim()}\n\n` +
571
+ '### Evidence\n' +
572
+ `${(evidence || '').trim()}\n\n` +
573
+ '### Suggested Instrumentation\n' +
574
+ `${(suggestedAction || '').trim()}\n`
575
+ );
576
+ }
577
+
578
+ // Default: problem
579
+ if (language === 'zh') {
580
+ const reproText = (reproduction || DEFAULT_REPRO_ZH).trim();
581
+ return (
582
+ '### 問題描述\n' +
583
+ `${(problemDescription || '').trim()}\n\n` +
584
+ '### 推測原因\n' +
585
+ `${(suspectedCause || '').trim()}\n\n` +
586
+ '### 重現條件(如有)\n' +
587
+ `${reproText}\n`
588
+ );
589
+ }
590
+
591
+ const reproText = (reproduction || DEFAULT_REPRO_EN).trim();
592
+ return (
593
+ '### Problem Description\n' +
594
+ `${(problemDescription || '').trim()}\n\n` +
595
+ '### Suspected Cause\n' +
596
+ `${(suspectedCause || '').trim()}\n\n` +
597
+ '### Reproduction Conditions (if available)\n' +
598
+ `${reproText}\n`
599
+ );
600
+ }
601
+
602
+ // ---- Issue creation ----
603
+
604
+ async function createIssueWithGh(
605
+ repo: string,
606
+ title: string,
607
+ body: string,
608
+ ): Promise<string> {
609
+ // Write body to a temp file
610
+ const tmpFile = joinPath(tmpdir(), `issue-${Date.now()}.md`);
611
+ const { writeFileSync, unlinkSync } = await import('node:fs');
612
+ writeFileSync(tmpFile, body, 'utf-8');
613
+
614
+ try {
615
+ const result = await runCommand('gh', [
616
+ 'issue',
617
+ 'create',
618
+ '--repo',
619
+ repo,
620
+ '--title',
621
+ title,
622
+ '--body-file',
623
+ tmpFile,
624
+ ]);
625
+
626
+ if (result.exitCode !== 0) {
627
+ throw new Error(result.stderr.trim() || 'gh issue create failed');
628
+ }
629
+
630
+ const urlMatch = result.stdout.match(
631
+ /https:\/\/github\.com\/[^\s]+\/issues\/\d+/,
632
+ );
633
+ return urlMatch ? urlMatch[0] : result.stdout.trim();
634
+ } finally {
635
+ try {
636
+ unlinkSync(tmpFile);
637
+ } catch {
638
+ // ignore cleanup errors
639
+ }
640
+ }
641
+ }
642
+
643
+ async function createIssueWithToken(
644
+ repo: string,
645
+ title: string,
646
+ body: string,
647
+ token: string,
648
+ ): Promise<string> {
649
+ const response = await githubRequest('POST', `/repos/${repo}/issues`, token, JSON_ACCEPT, {
650
+ title,
651
+ body,
652
+ });
653
+ const parsed = JSON.parse(response);
654
+ const issueUrl: string | undefined = parsed.html_url;
655
+ if (!issueUrl) {
656
+ throw new Error('Issue created but response did not include html_url');
657
+ }
658
+ return issueUrl;
659
+ }
660
+
661
+ // ---- Validation ----
662
+
663
+ function validateIssueContent(args: OpenIssueArgs): void {
664
+ const issueType = args.issueType || ISSUE_TYPE_PROBLEM;
665
+
666
+ if (issueType === ISSUE_TYPE_FEATURE) {
667
+ requireNonEmpty(args.reason, 'Feature issues require --reason.');
668
+ requireNonEmpty(
669
+ args.suggestedArchitecture,
670
+ 'Feature issues require --suggested-architecture.',
671
+ );
672
+ return;
673
+ }
674
+
675
+ if (issueType === ISSUE_TYPE_PERFORMANCE) {
676
+ requireNonEmpty(args.problemDescription, 'Performance issues require --problem-description.');
677
+ requireNonEmpty(args.impact, 'Performance issues require --impact.');
678
+ requireNonEmpty(args.evidence, 'Performance issues require --evidence.');
679
+ requireNonEmpty(args.suggestedAction, 'Performance issues require --suggested-action.');
680
+ return;
681
+ }
682
+
683
+ if (issueType === ISSUE_TYPE_SECURITY) {
684
+ requireNonEmpty(args.problemDescription, 'Security issues require --problem-description.');
685
+ requireNonEmpty(args.affectedScope, 'Security issues require --affected-scope.');
686
+ requireNonEmpty(args.impact, 'Security issues require --impact.');
687
+ requireNonEmpty(args.evidence, 'Security issues require --evidence.');
688
+ requireNonEmpty(args.suggestedAction, 'Security issues require --suggested-action.');
689
+ requireNonEmpty(args.severity, 'Security issues require --severity.');
690
+ return;
691
+ }
692
+
693
+ if (issueType === ISSUE_TYPE_DOCS) {
694
+ requireNonEmpty(args.problemDescription, 'Docs issues require --problem-description.');
695
+ requireNonEmpty(args.evidence, 'Docs issues require --evidence.');
696
+ requireNonEmpty(args.suggestedAction, 'Docs issues require --suggested-action.');
697
+ return;
698
+ }
699
+
700
+ if (issueType === ISSUE_TYPE_OBSERVABILITY) {
701
+ requireNonEmpty(args.problemDescription, 'Observability issues require --problem-description.');
702
+ requireNonEmpty(args.impact, 'Observability issues require --impact.');
703
+ requireNonEmpty(args.evidence, 'Observability issues require --evidence.');
704
+ requireNonEmpty(args.suggestedAction, 'Observability issues require --suggested-action.');
705
+ return;
706
+ }
707
+
708
+ // Problem issue
709
+ requireNonEmpty(args.problemDescription, 'Problem issues require --problem-description.');
710
+ requireNonEmpty(args.suspectedCause, 'Problem issues require --suspected-cause.');
711
+ if (!hasRequiredProblemBddSections(args.problemDescription || '')) {
712
+ throw new Error(
713
+ 'Problem issues require --problem-description to include ' +
714
+ 'Expected Behavior (BDD), Current Behavior (BDD), and Behavior Gap sections.',
715
+ );
716
+ }
717
+ }
718
+
719
+ function hydrateArgs(args: OpenIssueArgs): OpenIssueArgs {
720
+ const result = { ...args };
721
+
722
+ // Load from payload file if provided
723
+ if (result.payloadFile) {
724
+ const payload = readPayloadFile(result.payloadFile);
725
+ for (const [key, value] of Object.entries(payload)) {
726
+ if (key === 'dry_run') {
727
+ if (typeof value !== 'boolean') {
728
+ throw new Error("Payload field 'dry_run' must be a boolean.");
729
+ }
730
+ if (!result.dryRun) {
731
+ result.dryRun = value;
732
+ }
733
+ continue;
734
+ }
735
+
736
+ // String fields
737
+ if (TEXT_FIELDS.includes(key as (typeof TEXT_FIELDS)[number])) {
738
+ if (value !== null && typeof value !== 'string') {
739
+ throw new Error(`Payload field '${key}' must be a string or null.`);
740
+ }
741
+ } else if (typeof value !== 'string') {
742
+ throw new Error(`Payload field '${key}' must be a string.`);
743
+ }
744
+
745
+ const currentVal = (result as Record<string, unknown>)[key];
746
+ if (currentVal === null || currentVal === '') {
747
+ (result as Record<string, unknown>)[key] = value;
748
+ }
749
+ }
750
+ }
751
+
752
+ // Set default issue type
753
+ if (!result.issueType) {
754
+ result.issueType = ISSUE_TYPE_PROBLEM;
755
+ }
756
+ if (!ISSUE_TYPES.includes(result.issueType as (typeof ISSUE_TYPES)[number])) {
757
+ throw new Error(`Invalid issue_type: ${result.issueType}`);
758
+ }
759
+
760
+ // Resolve @-prefixed file values
761
+ for (const fieldName of TEXT_FIELDS) {
762
+ const resultObj = result as Record<string, unknown>;
763
+ const val = resultObj[fieldName] as string | null;
764
+ resultObj[fieldName] = readAtFileValue(fieldName, val);
765
+ }
766
+
767
+ // Title is required
768
+ if (!(result.title || '').trim()) {
769
+ throw new Error('Issue title is required. Pass --title or include title in --payload-file.');
770
+ }
771
+
772
+ return result;
773
+ }
774
+
775
+ async function resolveRepoAsync(
776
+ explicitRepo: string | null,
777
+ context: ToolContext,
778
+ ): Promise<string> {
779
+ if (explicitRepo) return validateRepo(explicitRepo);
780
+
781
+ // Try to resolve from git remote
782
+ const result = await runCommand('git', ['remote', 'get-url', 'origin']);
783
+ if (result.exitCode !== 0) {
784
+ context.stderr.write(
785
+ 'Unable to resolve origin remote. Pass --repo owner/repo.\n',
786
+ );
787
+ throw new Error('--repo resolution failed');
788
+ }
789
+
790
+ const remote = result.stdout.trim();
791
+ const match = remote.match(
792
+ /github\.com[:/](?<owner>[A-Za-z0-9_.-]+)\/(?<repo>[A-Za-z0-9_.-]+?)(?:\.git)?$/,
793
+ );
794
+ if (!match?.groups) {
795
+ context.stderr.write(
796
+ 'Origin remote is not a GitHub repository. Pass --repo owner/repo.\n',
797
+ );
798
+ throw new Error('--repo resolution failed');
799
+ }
800
+
801
+ return `${match.groups.owner}/${match.groups.repo}`;
802
+ }
803
+
804
+ // ---- Main handler ----
805
+
806
+ interface IssueResult {
807
+ repo: string;
808
+ issue_type: string;
809
+ language: string;
810
+ mode: string;
811
+ issue_url: string;
812
+ issue_title: string;
813
+ issue_body: string;
814
+ publish_error: string;
815
+ }
816
+
817
+ export async function openGitHubIssueHandler(
818
+ argv: string[],
819
+ context: ToolContext,
820
+ ): Promise<number> {
821
+ const { stdout, stderr, env } = context;
822
+
823
+ let args: OpenIssueArgs;
824
+ try {
825
+ args = hydrateArgs(parseArgs(argv));
826
+ validateIssueContent(args);
827
+ } catch (err) {
828
+ stderr.write(`Error: ${(err as Error).message}\n`);
829
+ return 1;
830
+ }
831
+
832
+ const ghAuthenticated = await hasGhAuth();
833
+ const token = getToken(env);
834
+
835
+ let repo: string;
836
+ try {
837
+ repo = await resolveRepoAsync(args.repo, context);
838
+ } catch {
839
+ return 1;
840
+ }
841
+
842
+ const readmeContent = await fetchRemoteReadme(repo, ghAuthenticated, token);
843
+ const language = detectIssueLanguage(readmeContent);
844
+
845
+ const issueBody = buildIssueBody({
846
+ issueType: args.issueType || ISSUE_TYPE_PROBLEM,
847
+ language,
848
+ title: args.title || '',
849
+ problemDescription: args.problemDescription,
850
+ suspectedCause: args.suspectedCause,
851
+ reproduction: args.reproduction,
852
+ proposal: args.proposal,
853
+ reason: args.reason,
854
+ suggestedArchitecture: args.suggestedArchitecture,
855
+ impact: args.impact,
856
+ evidence: args.evidence,
857
+ suggestedAction: args.suggestedAction,
858
+ severity: args.severity,
859
+ affectedScope: args.affectedScope,
860
+ });
861
+
862
+ let mode = 'draft-only';
863
+ let issueUrl = '';
864
+ let publishError = '';
865
+
866
+ if (args.dryRun) {
867
+ mode = 'dry-run';
868
+ } else if (ghAuthenticated) {
869
+ try {
870
+ issueUrl = await createIssueWithGh(repo, args.title || '', issueBody);
871
+ mode = 'gh-cli';
872
+ } catch (exc) {
873
+ if (token) {
874
+ try {
875
+ issueUrl = await createIssueWithToken(
876
+ repo,
877
+ args.title || '',
878
+ issueBody,
879
+ token,
880
+ );
881
+ mode = 'github-token';
882
+ } catch (tokenExc) {
883
+ publishError = (tokenExc as Error).message;
884
+ }
885
+ } else {
886
+ publishError = (exc as Error).message;
887
+ }
888
+ }
889
+ } else if (token) {
890
+ try {
891
+ issueUrl = await createIssueWithToken(
892
+ repo,
893
+ args.title || '',
894
+ issueBody,
895
+ token,
896
+ );
897
+ mode = 'github-token';
898
+ } catch (exc) {
899
+ publishError = (exc as Error).message;
900
+ }
901
+ }
902
+
903
+ const output: IssueResult = {
904
+ repo,
905
+ issue_type: args.issueType || ISSUE_TYPE_PROBLEM,
906
+ language: language === 'zh' ? 'zh' : 'en',
907
+ mode,
908
+ issue_url: issueUrl,
909
+ issue_title: args.title || '',
910
+ issue_body: issueBody,
911
+ publish_error: publishError,
912
+ };
913
+
914
+ stdout.write(JSON.stringify(output, null, 2) + '\n');
915
+
916
+ if (mode === 'draft-only') {
917
+ if (publishError) {
918
+ stderr.write(
919
+ `Issue publish failed. Return draft only: ${publishError}\n`,
920
+ );
921
+ } else {
922
+ stderr.write(
923
+ 'No authenticated gh CLI session and no GitHub token found. ' +
924
+ 'Return draft issue body only.\n',
925
+ );
926
+ }
927
+ }
928
+
929
+ return 0;
930
+ }