@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
@@ -1,705 +0,0 @@
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 sys
11
- import tempfile
12
- from pathlib import Path
13
- from urllib import error, request
14
-
15
- GITHUB_API_BASE = "https://api.github.com"
16
- README_ACCEPT = "application/vnd.github.raw+json"
17
- JSON_ACCEPT = "application/vnd.github+json"
18
- DEFAULT_REPRO_ZH = "尚未穩定重現;需補充更多執行期資料。"
19
- DEFAULT_REPRO_EN = "Not yet reliably reproducible; more runtime evidence is required."
20
- ISSUE_TYPE_PROBLEM = "problem"
21
- ISSUE_TYPE_FEATURE = "feature"
22
- ISSUE_TYPE_PERFORMANCE = "performance"
23
- ISSUE_TYPE_SECURITY = "security"
24
- ISSUE_TYPE_DOCS = "docs"
25
- ISSUE_TYPE_OBSERVABILITY = "observability"
26
- 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
- ]
34
- PROBLEM_BDD_MARKER_GROUPS = (
35
- (
36
- r"Expected Behavior\s*\(BDD\)",
37
- r"Current Behavior\s*\(BDD\)",
38
- r"Behavior Gap",
39
- ),
40
- (
41
- r"預期行為\s*[((]BDD[))]",
42
- r"(?:目前|當前)行為\s*[((]BDD[))]",
43
- r"行為(?:落差|差異)",
44
- ),
45
- )
46
- TEXT_FIELDS = (
47
- "title",
48
- "problem_description",
49
- "suspected_cause",
50
- "reproduction",
51
- "proposal",
52
- "reason",
53
- "suggested_architecture",
54
- "impact",
55
- "evidence",
56
- "suggested_action",
57
- "affected_scope",
58
- )
59
- PAYLOAD_FIELDS = frozenset(
60
- (
61
- "title",
62
- "issue_type",
63
- "problem_description",
64
- "suspected_cause",
65
- "reproduction",
66
- "proposal",
67
- "reason",
68
- "suggested_architecture",
69
- "impact",
70
- "evidence",
71
- "suggested_action",
72
- "severity",
73
- "affected_scope",
74
- "repo",
75
- "dry_run",
76
- )
77
- )
78
-
79
-
80
- def parse_args() -> argparse.Namespace:
81
- parser = argparse.ArgumentParser(
82
- description=(
83
- "Publish a structured GitHub issue or feature proposal. "
84
- "Auth order: gh CLI login -> GitHub token -> draft only."
85
- )
86
- )
87
- parser.add_argument(
88
- "--payload-file",
89
- help=(
90
- "Path to a JSON payload file. Use '-' to read JSON from stdin. "
91
- "CLI flags override values loaded from the payload."
92
- ),
93
- )
94
- parser.add_argument("--title", help="Issue title")
95
- parser.add_argument(
96
- "--issue-type",
97
- choices=ISSUE_TYPES,
98
- default=None,
99
- help="Structured issue type to publish.",
100
- )
101
- parser.add_argument(
102
- "--problem-description",
103
- help="Issue section content: problem description",
104
- )
105
- parser.add_argument(
106
- "--suspected-cause",
107
- help="Issue section content: suspected cause",
108
- )
109
- parser.add_argument(
110
- "--reproduction",
111
- help="Issue section content: reproduction conditions (optional)",
112
- )
113
- parser.add_argument(
114
- "--proposal",
115
- help="Issue section content: feature proposal summary (optional; defaults to title)",
116
- )
117
- parser.add_argument(
118
- "--reason",
119
- help="Issue section content: why the feature is needed",
120
- )
121
- parser.add_argument(
122
- "--suggested-architecture",
123
- help="Issue section content: suggested architecture for the feature",
124
- )
125
- parser.add_argument(
126
- "--impact",
127
- help="Issue section content: concrete user, system, or business impact",
128
- )
129
- parser.add_argument(
130
- "--evidence",
131
- help="Issue section content: concrete evidence, metrics, logs, or repository facts",
132
- )
133
- parser.add_argument(
134
- "--suggested-action",
135
- help="Issue section content: suggested remediation, mitigation, or next action",
136
- )
137
- parser.add_argument(
138
- "--severity",
139
- choices=["critical", "high", "medium", "low"],
140
- help="Issue section content: severity level for security-oriented issues",
141
- )
142
- parser.add_argument(
143
- "--affected-scope",
144
- help="Issue section content: affected endpoint, module, user cohort, or system surface",
145
- )
146
- parser.add_argument(
147
- "--repo",
148
- help="Target repository in owner/repo format. Defaults to origin remote.",
149
- )
150
- parser.add_argument(
151
- "--dry-run",
152
- action="store_true",
153
- help="Build and print payload only, without creating an issue.",
154
- )
155
- return parser.parse_args()
156
-
157
-
158
- def normalize_payload_key(key: str) -> str:
159
- return key.replace("-", "_")
160
-
161
-
162
- def read_payload_file(raw_path: str) -> dict[str, object]:
163
- if raw_path == "-":
164
- raw_content = sys.stdin.read()
165
- context = "stdin"
166
- else:
167
- path = Path(raw_path).expanduser()
168
- raw_content = path.read_text(encoding="utf-8")
169
- context = str(path)
170
-
171
- try:
172
- payload = json.loads(raw_content)
173
- except json.JSONDecodeError as exc:
174
- raise SystemExit(f"Invalid JSON payload in {context}: {exc}") from exc
175
-
176
- if not isinstance(payload, dict):
177
- raise SystemExit(f"Invalid JSON payload in {context}: top-level value must be an object.")
178
-
179
- normalized: dict[str, object] = {}
180
- for raw_key, value in payload.items():
181
- key = normalize_payload_key(str(raw_key))
182
- if key not in PAYLOAD_FIELDS:
183
- raise SystemExit(f"Unsupported payload key: {raw_key}")
184
- normalized[key] = value
185
- return normalized
186
-
187
-
188
- def payload_value_to_string(field_name: str, value: object) -> str | None:
189
- if value is None:
190
- return None
191
- if isinstance(value, str):
192
- return value
193
- raise SystemExit(f"Payload field '{field_name}' must be a string or null.")
194
-
195
-
196
- def read_at_file_value(field_name: str, value: str | None) -> str | None:
197
- if value is None:
198
- return None
199
- if value.startswith("@@"):
200
- return value[1:]
201
- if value == "@-":
202
- return sys.stdin.read()
203
- if value.startswith("@") and len(value) > 1:
204
- path = Path(value[1:]).expanduser()
205
- try:
206
- return path.read_text(encoding="utf-8")
207
- except OSError as exc:
208
- raise SystemExit(f"Unable to read @{field_name} file {path}: {exc}") from exc
209
- return value
210
-
211
-
212
- def hydrate_args(args: argparse.Namespace) -> argparse.Namespace:
213
- payload_file = getattr(args, "payload_file", None)
214
- if payload_file:
215
- payload = read_payload_file(payload_file)
216
- for field_name, value in payload.items():
217
- current_value = getattr(args, field_name, None)
218
- if field_name == "dry_run":
219
- if not isinstance(value, bool):
220
- raise SystemExit("Payload field 'dry_run' must be a boolean.")
221
- if not current_value:
222
- setattr(args, field_name, value)
223
- continue
224
-
225
- if field_name in TEXT_FIELDS:
226
- value = payload_value_to_string(field_name, value)
227
- elif not isinstance(value, str):
228
- raise SystemExit(f"Payload field '{field_name}' must be a string.")
229
-
230
- if current_value is None or current_value == "":
231
- setattr(args, field_name, value)
232
-
233
- if args.issue_type is None:
234
- args.issue_type = ISSUE_TYPE_PROBLEM
235
- if args.issue_type not in ISSUE_TYPES:
236
- raise SystemExit(f"Invalid issue_type: {args.issue_type}")
237
-
238
- for field_name in TEXT_FIELDS:
239
- setattr(args, field_name, read_at_file_value(field_name, getattr(args, field_name, None)))
240
-
241
- if not (args.title or "").strip():
242
- raise SystemExit("Issue title is required. Pass --title or include title in --payload-file.")
243
- return args
244
-
245
-
246
- def validate_issue_content_args(args: argparse.Namespace) -> None:
247
- if args.issue_type == ISSUE_TYPE_FEATURE:
248
- if not (args.reason or "").strip():
249
- raise SystemExit("Feature issues require --reason.")
250
- if not (args.suggested_architecture or "").strip():
251
- raise SystemExit("Feature issues require --suggested-architecture.")
252
- return
253
-
254
- if args.issue_type == ISSUE_TYPE_PERFORMANCE:
255
- require_non_empty(args.problem_description, "Performance issues require --problem-description.")
256
- require_non_empty(args.impact, "Performance issues require --impact.")
257
- require_non_empty(args.evidence, "Performance issues require --evidence.")
258
- require_non_empty(args.suggested_action, "Performance issues require --suggested-action.")
259
- return
260
-
261
- if args.issue_type == ISSUE_TYPE_SECURITY:
262
- require_non_empty(args.problem_description, "Security issues require --problem-description.")
263
- require_non_empty(args.affected_scope, "Security issues require --affected-scope.")
264
- require_non_empty(args.impact, "Security issues require --impact.")
265
- require_non_empty(args.evidence, "Security issues require --evidence.")
266
- require_non_empty(args.suggested_action, "Security issues require --suggested-action.")
267
- require_non_empty(args.severity, "Security issues require --severity.")
268
- return
269
-
270
- if args.issue_type == ISSUE_TYPE_DOCS:
271
- require_non_empty(args.problem_description, "Docs issues require --problem-description.")
272
- require_non_empty(args.evidence, "Docs issues require --evidence.")
273
- require_non_empty(args.suggested_action, "Docs issues require --suggested-action.")
274
- return
275
-
276
- if args.issue_type == ISSUE_TYPE_OBSERVABILITY:
277
- require_non_empty(args.problem_description, "Observability issues require --problem-description.")
278
- require_non_empty(args.impact, "Observability issues require --impact.")
279
- require_non_empty(args.evidence, "Observability issues require --evidence.")
280
- require_non_empty(args.suggested_action, "Observability issues require --suggested-action.")
281
- return
282
-
283
- if not (args.problem_description or "").strip():
284
- raise SystemExit("Problem issues require --problem-description.")
285
- if not (args.suspected_cause or "").strip():
286
- raise SystemExit("Problem issues require --suspected-cause.")
287
- if not has_required_problem_bdd_sections(args.problem_description or ""):
288
- raise SystemExit(
289
- "Problem issues require --problem-description to include "
290
- "Expected Behavior (BDD), Current Behavior (BDD), and Behavior Gap sections."
291
- )
292
-
293
-
294
- def require_non_empty(value: str | None, message: str) -> None:
295
- if not (value or "").strip():
296
- raise SystemExit(message)
297
-
298
-
299
- def has_required_problem_bdd_sections(problem_description: str) -> bool:
300
- normalized = problem_description.strip()
301
- return any(
302
- all(re.search(pattern, normalized, flags=re.IGNORECASE) for pattern in marker_group)
303
- for marker_group in PROBLEM_BDD_MARKER_GROUPS
304
- )
305
-
306
-
307
- def run_command(args: list[str]) -> subprocess.CompletedProcess[str]:
308
- return subprocess.run(args, check=False, capture_output=True, text=True)
309
-
310
-
311
- def has_gh_auth() -> bool:
312
- if shutil.which("gh") is None:
313
- return False
314
- result = run_command(["gh", "auth", "status"])
315
- return result.returncode == 0
316
-
317
-
318
- def get_token() -> str | None:
319
- for key in ("GITHUB_TOKEN", "GH_TOKEN"):
320
- value = os.getenv(key, "").strip()
321
- if value:
322
- return value
323
- return None
324
-
325
-
326
- def resolve_repo(explicit_repo: str | None) -> str:
327
- if explicit_repo:
328
- return validate_repo(explicit_repo)
329
-
330
- remote_result = run_command(["git", "remote", "get-url", "origin"])
331
- if remote_result.returncode != 0:
332
- raise SystemExit("Unable to resolve origin remote. Pass --repo owner/repo.")
333
-
334
- remote = remote_result.stdout.strip()
335
- match = re.search(
336
- r"github\.com[:/](?P<owner>[A-Za-z0-9_.-]+)/(?P<repo>[A-Za-z0-9_.-]+?)(?:\.git)?$",
337
- remote,
338
- )
339
- if not match:
340
- raise SystemExit("Origin remote is not a GitHub repository. Pass --repo owner/repo.")
341
-
342
- return f"{match.group('owner')}/{match.group('repo')}"
343
-
344
-
345
- def validate_repo(repo: str) -> str:
346
- candidate = repo.strip()
347
- if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", candidate):
348
- raise SystemExit("Invalid repo format. Use owner/repo.")
349
- return candidate
350
-
351
-
352
- def github_request(
353
- method: str,
354
- path: str,
355
- *,
356
- token: str | None,
357
- accept: str,
358
- payload: dict | None = None,
359
- ) -> str:
360
- headers = {
361
- "Accept": accept,
362
- "User-Agent": "open-github-issue-skill",
363
- "X-GitHub-Api-Version": "2022-11-28",
364
- }
365
- data = None
366
-
367
- if token:
368
- headers["Authorization"] = f"Bearer {token}"
369
-
370
- if payload is not None:
371
- data = json.dumps(payload).encode("utf-8")
372
- headers["Content-Type"] = "application/json"
373
-
374
- req = request.Request(
375
- url=f"{GITHUB_API_BASE}{path}",
376
- data=data,
377
- headers=headers,
378
- method=method,
379
- )
380
-
381
- try:
382
- with request.urlopen(req, timeout=30) as response:
383
- return response.read().decode("utf-8")
384
- except error.HTTPError as exc:
385
- detail = exc.read().decode("utf-8", errors="replace")
386
- raise RuntimeError(f"GitHub API {exc.code} {path}: {detail}") from exc
387
- except error.URLError as exc:
388
- raise RuntimeError(f"GitHub API request failed for {path}: {exc.reason}") from exc
389
-
390
-
391
- def fetch_remote_readme(repo: str, gh_authenticated: bool, token: str | None) -> str:
392
- if gh_authenticated:
393
- result = run_command(
394
- ["gh", "api", "-H", f"Accept: {README_ACCEPT}", f"repos/{repo}/readme"]
395
- )
396
- if result.returncode == 0:
397
- return result.stdout
398
-
399
- try:
400
- return github_request(
401
- "GET",
402
- f"/repos/{repo}/readme",
403
- token=token,
404
- accept=README_ACCEPT,
405
- )
406
- except RuntimeError:
407
- return ""
408
-
409
-
410
- def detect_issue_language(readme_content: str) -> str:
411
- if not readme_content.strip():
412
- return "en"
413
-
414
- chinese_chars = len(re.findall(r"[\u4e00-\u9fff]", readme_content))
415
- language_chars = len(re.findall(r"[A-Za-z\u4e00-\u9fff]", readme_content))
416
-
417
- if chinese_chars >= 20 and chinese_chars / max(language_chars, 1) >= 0.08:
418
- return "zh"
419
- return "en"
420
-
421
-
422
- def build_issue_body(
423
- *,
424
- issue_type: str,
425
- language: str,
426
- title: str,
427
- problem_description: str | None,
428
- suspected_cause: str | None,
429
- reproduction: str | None,
430
- proposal: str | None,
431
- reason: str | None,
432
- suggested_architecture: str | None,
433
- impact: str | None = None,
434
- evidence: str | None = None,
435
- suggested_action: str | None = None,
436
- severity: str | None = None,
437
- affected_scope: str | None = None,
438
- ) -> str:
439
- if issue_type == ISSUE_TYPE_FEATURE:
440
- proposal_text = (proposal or title).strip()
441
- reason_text = (reason or "").strip()
442
- architecture_text = (suggested_architecture or "").strip()
443
-
444
- if language == "zh":
445
- return (
446
- "### 功能提案\n"
447
- f"{proposal_text}\n\n"
448
- "### 原因\n"
449
- f"{reason_text}\n\n"
450
- "### 建議架構\n"
451
- f"{architecture_text}\n"
452
- )
453
-
454
- return (
455
- "### Feature Proposal\n"
456
- f"{proposal_text}\n\n"
457
- "### Why This Is Needed\n"
458
- f"{reason_text}\n\n"
459
- "### Suggested Architecture\n"
460
- f"{architecture_text}\n"
461
- )
462
-
463
- if issue_type == ISSUE_TYPE_PERFORMANCE:
464
- if language == "zh":
465
- return (
466
- "### 效能問題\n"
467
- f"{(problem_description or '').strip()}\n\n"
468
- "### 影響\n"
469
- f"{(impact or '').strip()}\n\n"
470
- "### 證據\n"
471
- f"{(evidence or '').strip()}\n\n"
472
- "### 建議行動\n"
473
- f"{(suggested_action or '').strip()}\n"
474
- )
475
- return (
476
- "### Performance Problem\n"
477
- f"{(problem_description or '').strip()}\n\n"
478
- "### Impact\n"
479
- f"{(impact or '').strip()}\n\n"
480
- "### Evidence\n"
481
- f"{(evidence or '').strip()}\n\n"
482
- "### Suggested Action\n"
483
- f"{(suggested_action or '').strip()}\n"
484
- )
485
-
486
- if issue_type == ISSUE_TYPE_SECURITY:
487
- if language == "zh":
488
- return (
489
- "### 安全風險\n"
490
- f"{(problem_description or '').strip()}\n\n"
491
- "### 嚴重程度\n"
492
- f"{(severity or '').strip()}\n\n"
493
- "### 受影響範圍\n"
494
- f"{(affected_scope or '').strip()}\n\n"
495
- "### 影響\n"
496
- f"{(impact or '').strip()}\n\n"
497
- "### 證據\n"
498
- f"{(evidence or '').strip()}\n\n"
499
- "### 建議緩解\n"
500
- f"{(suggested_action or '').strip()}\n"
501
- )
502
- return (
503
- "### Security Risk\n"
504
- f"{(problem_description or '').strip()}\n\n"
505
- "### Severity\n"
506
- f"{(severity or '').strip()}\n\n"
507
- "### Affected Scope\n"
508
- f"{(affected_scope or '').strip()}\n\n"
509
- "### Impact\n"
510
- f"{(impact or '').strip()}\n\n"
511
- "### Evidence\n"
512
- f"{(evidence or '').strip()}\n\n"
513
- "### Suggested Mitigation\n"
514
- f"{(suggested_action or '').strip()}\n"
515
- )
516
-
517
- if issue_type == ISSUE_TYPE_DOCS:
518
- if language == "zh":
519
- return (
520
- "### 文件缺口\n"
521
- f"{(problem_description or '').strip()}\n\n"
522
- "### 證據\n"
523
- f"{(evidence or '').strip()}\n\n"
524
- "### 建議更新\n"
525
- f"{(suggested_action or '').strip()}\n"
526
- )
527
- return (
528
- "### Documentation Gap\n"
529
- f"{(problem_description or '').strip()}\n\n"
530
- "### Evidence\n"
531
- f"{(evidence or '').strip()}\n\n"
532
- "### Suggested Update\n"
533
- f"{(suggested_action or '').strip()}\n"
534
- )
535
-
536
- if issue_type == ISSUE_TYPE_OBSERVABILITY:
537
- if language == "zh":
538
- return (
539
- "### 可觀測性缺口\n"
540
- f"{(problem_description or '').strip()}\n\n"
541
- "### 影響\n"
542
- f"{(impact or '').strip()}\n\n"
543
- "### 證據\n"
544
- f"{(evidence or '').strip()}\n\n"
545
- "### 建議儀表化\n"
546
- f"{(suggested_action or '').strip()}\n"
547
- )
548
- return (
549
- "### Observability Gap\n"
550
- f"{(problem_description or '').strip()}\n\n"
551
- "### Impact\n"
552
- f"{(impact or '').strip()}\n\n"
553
- "### Evidence\n"
554
- f"{(evidence or '').strip()}\n\n"
555
- "### Suggested Instrumentation\n"
556
- f"{(suggested_action or '').strip()}\n"
557
- )
558
-
559
- if language == "zh":
560
- repro_text = (reproduction or DEFAULT_REPRO_ZH).strip()
561
- return (
562
- "### 問題描述\n"
563
- f"{(problem_description or '').strip()}\n\n"
564
- "### 推測原因\n"
565
- f"{(suspected_cause or '').strip()}\n\n"
566
- "### 重現條件(如有)\n"
567
- f"{repro_text}\n"
568
- )
569
-
570
- repro_text = (reproduction or DEFAULT_REPRO_EN).strip()
571
- return (
572
- "### Problem Description\n"
573
- f"{(problem_description or '').strip()}\n\n"
574
- "### Suspected Cause\n"
575
- f"{(suspected_cause or '').strip()}\n\n"
576
- "### Reproduction Conditions (if available)\n"
577
- f"{repro_text}\n"
578
- )
579
-
580
-
581
- def create_issue_with_gh(repo: str, title: str, body: str) -> str:
582
- tmp_file: Path | None = None
583
- try:
584
- with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".md", delete=False) as handle:
585
- handle.write(body)
586
- tmp_file = Path(handle.name)
587
-
588
- result = run_command(
589
- [
590
- "gh",
591
- "issue",
592
- "create",
593
- "--repo",
594
- repo,
595
- "--title",
596
- title,
597
- "--body-file",
598
- str(tmp_file),
599
- ]
600
- )
601
- if result.returncode != 0:
602
- raise RuntimeError(result.stderr.strip() or "gh issue create failed")
603
-
604
- url_match = re.search(r"https://github\.com/[^\s]+/issues/\d+", result.stdout)
605
- return url_match.group(0) if url_match else result.stdout.strip()
606
- finally:
607
- if tmp_file and tmp_file.exists():
608
- tmp_file.unlink()
609
-
610
-
611
- def create_issue_with_token(repo: str, title: str, body: str, token: str) -> str:
612
- response = github_request(
613
- "POST",
614
- f"/repos/{repo}/issues",
615
- token=token,
616
- accept=JSON_ACCEPT,
617
- payload={"title": title, "body": body},
618
- )
619
- parsed = json.loads(response)
620
- issue_url = parsed.get("html_url", "")
621
- if not issue_url:
622
- raise RuntimeError("Issue created but response did not include html_url")
623
- return issue_url
624
-
625
-
626
- def main() -> int:
627
- args = hydrate_args(parse_args())
628
- validate_issue_content_args(args)
629
-
630
- gh_authenticated = has_gh_auth()
631
- token = get_token()
632
- repo = resolve_repo(args.repo)
633
-
634
- readme_content = fetch_remote_readme(repo, gh_authenticated, token)
635
- language = detect_issue_language(readme_content)
636
-
637
- issue_body = build_issue_body(
638
- issue_type=args.issue_type,
639
- language=language,
640
- title=args.title,
641
- problem_description=args.problem_description,
642
- suspected_cause=args.suspected_cause,
643
- reproduction=args.reproduction,
644
- proposal=args.proposal,
645
- reason=args.reason,
646
- suggested_architecture=args.suggested_architecture,
647
- impact=args.impact,
648
- evidence=args.evidence,
649
- suggested_action=args.suggested_action,
650
- severity=args.severity,
651
- affected_scope=args.affected_scope,
652
- )
653
-
654
- mode = "draft-only"
655
- issue_url = ""
656
- publish_error = ""
657
-
658
- if args.dry_run:
659
- mode = "dry-run"
660
- elif gh_authenticated:
661
- try:
662
- issue_url = create_issue_with_gh(repo, args.title, issue_body)
663
- mode = "gh-cli"
664
- except RuntimeError as exc:
665
- if token:
666
- try:
667
- issue_url = create_issue_with_token(repo, args.title, issue_body, token)
668
- mode = "github-token"
669
- except RuntimeError as token_exc:
670
- publish_error = str(token_exc)
671
- else:
672
- publish_error = str(exc)
673
- elif token:
674
- try:
675
- issue_url = create_issue_with_token(repo, args.title, issue_body, token)
676
- mode = "github-token"
677
- except RuntimeError as exc:
678
- publish_error = str(exc)
679
-
680
- output = {
681
- "repo": repo,
682
- "issue_type": args.issue_type,
683
- "language": "zh" if language == "zh" else "en",
684
- "mode": mode,
685
- "issue_url": issue_url,
686
- "issue_title": args.title,
687
- "issue_body": issue_body,
688
- "publish_error": publish_error,
689
- }
690
- print(json.dumps(output, ensure_ascii=False))
691
-
692
- if mode == "draft-only":
693
- if publish_error:
694
- print(f"Issue publish failed. Return draft only: {publish_error}", file=sys.stderr)
695
- else:
696
- print(
697
- "No authenticated gh CLI session and no GitHub token found. "
698
- "Return draft issue body only.",
699
- file=sys.stderr,
700
- )
701
- return 0
702
-
703
-
704
- if __name__ == "__main__":
705
- raise SystemExit(main())