@laitszkin/apollo-toolkit 2.14.23 → 3.0.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.
- package/AGENTS.md +3 -0
- package/CHANGELOG.md +17 -0
- package/README.md +9 -0
- package/analyse-app-logs/README.md +5 -5
- package/analyse-app-logs/SKILL.md +7 -5
- package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
- package/codex/codex-memory-manager/README.md +2 -2
- package/codex/codex-memory-manager/SKILL.md +5 -5
- package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +3 -2
- package/codex/learn-skill-from-conversations/README.md +1 -1
- package/codex/learn-skill-from-conversations/SKILL.md +2 -2
- package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +3 -2
- package/docs-to-voice/README.md +3 -3
- package/docs-to-voice/SKILL.md +4 -4
- package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
- package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +51 -0
- package/feature-propose/SKILL.md +1 -0
- package/generate-spec/README.md +3 -6
- package/generate-spec/SKILL.md +2 -3
- package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
- package/generate-spec/tests/test_create_specs.py +166 -0
- package/jupiter-development/SKILL.md +5 -0
- package/katex/SKILL.md +3 -3
- package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
- package/katex/tests/test_render_katex.py +174 -0
- package/learning-error-book/SKILL.md +2 -2
- package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +134 -0
- package/lib/cli.js +66 -0
- package/lib/tool-runner.js +214 -0
- package/maintain-project-constraints/SKILL.md +3 -3
- package/maintain-skill-catalog/SKILL.md +2 -2
- package/novel-to-short-video/SKILL.md +2 -2
- package/open-github-issue/README.md +31 -22
- package/open-github-issue/SKILL.md +54 -40
- package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
- package/open-github-issue/scripts/open_github_issue.py +130 -3
- package/open-github-issue/tests/test_open_github_issue.py +95 -0
- package/openai-text-to-image-storyboard/README.md +1 -1
- package/openai-text-to-image-storyboard/SKILL.md +1 -1
- package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +177 -0
- package/package.json +1 -1
- package/read-github-issue/SKILL.md +9 -9
- package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
- package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
- package/resolve-review-comments/SKILL.md +8 -8
- package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
- package/review-codebases/README.md +2 -0
- package/review-codebases/SKILL.md +1 -0
- package/scheduled-runtime-health-check/SKILL.md +3 -0
- package/systematic-debug/SKILL.md +3 -0
- package/text-to-short-video/README.md +1 -1
- package/text-to-short-video/SKILL.md +1 -1
- package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
- package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +194 -0
- package/weekly-financial-event-report/SKILL.md +2 -2
- package/weekly-financial-event-report/tests/test_extract_pdf_text_pdfkit.py +64 -0
package/katex/SKILL.md
CHANGED
|
@@ -15,7 +15,7 @@ description: Render and embed math formulas with KaTeX using official documentat
|
|
|
15
15
|
## Standards
|
|
16
16
|
|
|
17
17
|
- Evidence: Follow the official KaTeX docs in `references/official-docs.md` before choosing render mode, options, or insertion strategy.
|
|
18
|
-
- Execution: Decide between pre-rendering and client-side auto-render first, then use `
|
|
18
|
+
- Execution: Decide between pre-rendering and client-side auto-render first, then use `apltk render-katex` for deterministic output whenever a static rendered snippet is enough.
|
|
19
19
|
- Quality: Default to `htmlAndMathml`, keep `trust` disabled unless the content source is explicitly trusted, and include the KaTeX stylesheet whenever the output will be displayed in HTML.
|
|
20
20
|
- Output: Return insertion-ready content plus the exact CSS or runtime requirements still needed by the target file.
|
|
21
21
|
|
|
@@ -41,7 +41,7 @@ Use KaTeX safely and consistently so mathematical formulas can be inserted into
|
|
|
41
41
|
Run the bundled renderer for pre-rendered output:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
|
|
44
|
+
apltk render-katex \
|
|
45
45
|
--tex '\int_0^1 x^2 \\, dx = \\frac{1}{3}' \
|
|
46
46
|
--display-mode \
|
|
47
47
|
--output-format html-fragment
|
|
@@ -88,5 +88,5 @@ See `references/insertion-patterns.md` for concrete insertion guidance.
|
|
|
88
88
|
|
|
89
89
|
- `references/official-docs.md`: condensed notes from the official KaTeX docs and direct source links.
|
|
90
90
|
- `references/insertion-patterns.md`: insertion patterns for HTML, Markdown/MDX, and auto-rendered pages.
|
|
91
|
-
- `scripts/render_katex.py`: deterministic wrapper around the official KaTeX CLI for insertion-ready output
|
|
91
|
+
- `scripts/render_katex.py`: deterministic wrapper around the official KaTeX CLI for insertion-ready output, exposed as `apltk render-katex`.
|
|
92
92
|
- `scripts/render_katex.sh`: shell wrapper for environments that prefer an executable script entrypoint.
|
|
Binary file
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import random
|
|
10
|
+
import string
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import types
|
|
15
|
+
import unittest
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from unittest.mock import patch
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "render_katex.py"
|
|
21
|
+
SHELL_SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "render_katex.sh"
|
|
22
|
+
SPEC = importlib.util.spec_from_file_location("render_katex", SCRIPT_PATH)
|
|
23
|
+
MODULE = importlib.util.module_from_spec(SPEC)
|
|
24
|
+
SPEC.loader.exec_module(MODULE)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RenderKatexTests(unittest.TestCase):
|
|
28
|
+
def test_load_macro_pairs_rejects_invalid_values(self) -> None:
|
|
29
|
+
with self.assertRaises(MODULE.KatexRenderError):
|
|
30
|
+
MODULE.load_macro_pairs(["\\RR"])
|
|
31
|
+
|
|
32
|
+
with self.assertRaises(MODULE.KatexRenderError):
|
|
33
|
+
MODULE.load_macro_pairs(["\\RR:"])
|
|
34
|
+
|
|
35
|
+
def test_normalize_path_property_returns_absolute_resolved_path(self) -> None:
|
|
36
|
+
generator = random.Random(20260418)
|
|
37
|
+
alphabet = string.ascii_letters + string.digits
|
|
38
|
+
|
|
39
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
40
|
+
with patch("pathlib.Path.cwd", return_value=Path(temp_dir)):
|
|
41
|
+
for _ in range(120):
|
|
42
|
+
depth = generator.randint(1, 4)
|
|
43
|
+
pieces = [
|
|
44
|
+
"".join(generator.choice(alphabet) for _ in range(generator.randint(1, 8)))
|
|
45
|
+
for _ in range(depth)
|
|
46
|
+
]
|
|
47
|
+
raw_path = "/".join(pieces)
|
|
48
|
+
normalized = MODULE.normalize_path(raw_path)
|
|
49
|
+
with self.subTest(raw_path=raw_path):
|
|
50
|
+
self.assertTrue(normalized.is_absolute())
|
|
51
|
+
self.assertEqual(normalized, (Path(temp_dir) / raw_path).resolve())
|
|
52
|
+
|
|
53
|
+
def test_wrap_output_json_includes_metadata(self) -> None:
|
|
54
|
+
args = types.SimpleNamespace(
|
|
55
|
+
output_format="json",
|
|
56
|
+
display_mode=True,
|
|
57
|
+
katex_format="htmlAndMathml",
|
|
58
|
+
css_href="https://cdn.example.com/katex.css",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
payload = json.loads(MODULE.wrap_output("<span>ok</span>", r"x^2", args))
|
|
62
|
+
self.assertEqual(payload["tex"], r"x^2")
|
|
63
|
+
self.assertEqual(payload["displayMode"], True)
|
|
64
|
+
self.assertEqual(payload["content"], "<span>ok</span>")
|
|
65
|
+
|
|
66
|
+
def test_run_katex_cli_builds_expected_command(self) -> None:
|
|
67
|
+
captured = {}
|
|
68
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
69
|
+
macro_file = Path(temp_dir) / "macros.json"
|
|
70
|
+
macro_file.write_text('{"\\\\RR":"\\\\mathbb{R}"}\n', encoding="utf-8")
|
|
71
|
+
args = types.SimpleNamespace(
|
|
72
|
+
katex_format="html",
|
|
73
|
+
display_mode=True,
|
|
74
|
+
leqno=True,
|
|
75
|
+
fleqn=False,
|
|
76
|
+
color_is_text_color=True,
|
|
77
|
+
no_throw_on_error=True,
|
|
78
|
+
error_color="#cc0000",
|
|
79
|
+
strict="warn",
|
|
80
|
+
trust="true",
|
|
81
|
+
max_size=12.5,
|
|
82
|
+
max_expand=500,
|
|
83
|
+
min_rule_thickness=0.08,
|
|
84
|
+
macro=[r"\RR:\mathbb{R}"],
|
|
85
|
+
macro_file=str(macro_file),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def fake_run(command, **kwargs):
|
|
89
|
+
captured["command"] = command
|
|
90
|
+
captured["kwargs"] = kwargs
|
|
91
|
+
return types.SimpleNamespace(returncode=0, stdout="<span>rendered</span>", stderr="")
|
|
92
|
+
|
|
93
|
+
with patch.object(MODULE.subprocess, "run", side_effect=fake_run):
|
|
94
|
+
output = MODULE.run_katex_cli(r"\RR + x", args)
|
|
95
|
+
|
|
96
|
+
self.assertEqual(output, "<span>rendered</span>")
|
|
97
|
+
command = captured["command"]
|
|
98
|
+
self.assertIn("--display-mode", command)
|
|
99
|
+
self.assertIn("--leqno", command)
|
|
100
|
+
self.assertIn("--color-is-text-color", command)
|
|
101
|
+
self.assertIn("--no-throw-on-error", command)
|
|
102
|
+
self.assertIn("--macro", command)
|
|
103
|
+
self.assertIn(r"\RR:\mathbb{R}", command)
|
|
104
|
+
self.assertIn("--macro-file", command)
|
|
105
|
+
self.assertIn("--input", command)
|
|
106
|
+
|
|
107
|
+
def test_main_writes_output_file_when_renderer_succeeds(self) -> None:
|
|
108
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
109
|
+
output_path = Path(temp_dir) / "out" / "katex.html"
|
|
110
|
+
argv = [
|
|
111
|
+
"--tex",
|
|
112
|
+
r"\int_0^1 x^2 dx",
|
|
113
|
+
"--output-format",
|
|
114
|
+
"html-page",
|
|
115
|
+
"--output-file",
|
|
116
|
+
str(output_path),
|
|
117
|
+
"--title",
|
|
118
|
+
"Sample Render",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
with patch.object(MODULE, "run_katex_cli", return_value="<span>ok</span>"), patch(
|
|
122
|
+
"sys.stdout", new_callable=io.StringIO
|
|
123
|
+
) as stdout:
|
|
124
|
+
exit_code = MODULE.main(argv)
|
|
125
|
+
|
|
126
|
+
self.assertEqual(exit_code, 0)
|
|
127
|
+
self.assertIn(str(output_path.resolve()), stdout.getvalue())
|
|
128
|
+
self.assertIn("<span>ok</span>", output_path.read_text(encoding="utf-8"))
|
|
129
|
+
self.assertIn("Sample Render", output_path.read_text(encoding="utf-8"))
|
|
130
|
+
|
|
131
|
+
def test_main_reports_missing_node_tooling(self) -> None:
|
|
132
|
+
error = FileNotFoundError(2, "No such file or directory", "npx")
|
|
133
|
+
|
|
134
|
+
with patch.object(MODULE, "run_katex_cli", side_effect=error), patch(
|
|
135
|
+
"sys.stderr", new_callable=io.StringIO
|
|
136
|
+
) as stderr:
|
|
137
|
+
exit_code = MODULE.main(["--tex", r"x+1"])
|
|
138
|
+
|
|
139
|
+
self.assertEqual(exit_code, 1)
|
|
140
|
+
self.assertIn("node and npx are required", stderr.getvalue())
|
|
141
|
+
|
|
142
|
+
def test_shell_wrapper_invokes_python_script_with_same_arguments(self) -> None:
|
|
143
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
144
|
+
capture_path = Path(temp_dir) / "argv.json"
|
|
145
|
+
fake_python = Path(temp_dir) / "python3"
|
|
146
|
+
fake_python.write_text(
|
|
147
|
+
f"#!{sys.executable}\n"
|
|
148
|
+
"import json, os, sys\n"
|
|
149
|
+
"with open(os.environ['CAPTURE_PATH'], 'w', encoding='utf-8') as handle:\n"
|
|
150
|
+
" json.dump(sys.argv[1:], handle)\n",
|
|
151
|
+
encoding="utf-8",
|
|
152
|
+
)
|
|
153
|
+
fake_python.chmod(0o755)
|
|
154
|
+
|
|
155
|
+
env = dict(os.environ)
|
|
156
|
+
env["PATH"] = f"{temp_dir}:{env['PATH']}"
|
|
157
|
+
env["CAPTURE_PATH"] = str(capture_path)
|
|
158
|
+
|
|
159
|
+
result = subprocess.run(
|
|
160
|
+
["bash", str(SHELL_SCRIPT_PATH), "--tex", r"x^2", "--output-format", "json"],
|
|
161
|
+
capture_output=True,
|
|
162
|
+
text=True,
|
|
163
|
+
env=env,
|
|
164
|
+
check=False,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
168
|
+
argv = json.loads(capture_path.read_text(encoding="utf-8"))
|
|
169
|
+
self.assertEqual(argv[0], str(SCRIPT_PATH))
|
|
170
|
+
self.assertEqual(argv[1:], ["--tex", r"x^2", "--output-format", "json"])
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
unittest.main()
|
|
@@ -111,8 +111,8 @@ error_book/
|
|
|
111
111
|
|
|
112
112
|
5) Render structured data -> PDF (CJK font support)
|
|
113
113
|
- Run:
|
|
114
|
-
- `
|
|
115
|
-
- `
|
|
114
|
+
- `apltk render-error-book error_book/references/mc-question-reference.json error_book/mc-question-error-book.pdf`
|
|
115
|
+
- `apltk render-error-book error_book/references/long-question-reference.json error_book/long-question-error-book.pdf`
|
|
116
116
|
- If paper size/font needs change: adjust script flags (`--help`)
|
|
117
117
|
|
|
118
118
|
## Built-in Template
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import json
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
|
|
12
|
+
REPORTLAB_AVAILABLE = importlib.util.find_spec("reportlab") is not None
|
|
13
|
+
|
|
14
|
+
if REPORTLAB_AVAILABLE:
|
|
15
|
+
from reportlab.platypus import Paragraph
|
|
16
|
+
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "render_error_book_json_to_pdf.py"
|
|
17
|
+
SPEC = importlib.util.spec_from_file_location("render_error_book_json_to_pdf", SCRIPT_PATH)
|
|
18
|
+
MODULE = importlib.util.module_from_spec(SPEC)
|
|
19
|
+
SPEC.loader.exec_module(MODULE)
|
|
20
|
+
else:
|
|
21
|
+
Paragraph = object
|
|
22
|
+
MODULE = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def minimal_payload() -> dict:
|
|
26
|
+
return {
|
|
27
|
+
"title": "Physics Error Book",
|
|
28
|
+
"book_type": "mc-question",
|
|
29
|
+
"last_updated": "2026-04-18",
|
|
30
|
+
"coverage_scope": [
|
|
31
|
+
{
|
|
32
|
+
"source_path": "notes/ch1.pdf",
|
|
33
|
+
"included_questions": ["Q1", "Q2"],
|
|
34
|
+
"notes": "Kinematics review",
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"mistake_overview": [
|
|
38
|
+
{
|
|
39
|
+
"type": "Concept mix-up",
|
|
40
|
+
"summary": "Mixed up displacement and distance.",
|
|
41
|
+
"representative_questions": ["Q1"],
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"concept_highlights": [
|
|
45
|
+
{
|
|
46
|
+
"name": "Velocity",
|
|
47
|
+
"definition": "Rate of change of displacement.",
|
|
48
|
+
"common_misjudgment": "Treating it as always positive speed.",
|
|
49
|
+
"checklist": ["Track sign", "Check reference direction"],
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"questions": [
|
|
53
|
+
{
|
|
54
|
+
"question_id": "Q1",
|
|
55
|
+
"source_path": "notes/ch1.pdf",
|
|
56
|
+
"page_or_locator": "p.3",
|
|
57
|
+
"user_answer": "B",
|
|
58
|
+
"correct_answer": "C",
|
|
59
|
+
"mistake_type": "Concept mix-up",
|
|
60
|
+
"concepts": ["Velocity", "Displacement"],
|
|
61
|
+
"stem": "A car turns around after 10 seconds.",
|
|
62
|
+
"why_wrong": "The answer used total distance instead of signed displacement.",
|
|
63
|
+
"correct_solution_steps": ["Identify direction", "Compute signed displacement"],
|
|
64
|
+
"options": [
|
|
65
|
+
{"label": "A", "text": "0", "verdict": "wrong", "reason": "Ignores movement"},
|
|
66
|
+
{"label": "C", "text": "-2", "verdict": "correct", "reason": "Signed displacement"},
|
|
67
|
+
],
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@unittest.skipUnless(REPORTLAB_AVAILABLE, "reportlab is required for PDF rendering tests")
|
|
74
|
+
class RenderErrorBookTests(unittest.TestCase):
|
|
75
|
+
def test_safe_text_handles_lists_numbers_and_defaults(self) -> None:
|
|
76
|
+
self.assertEqual(MODULE._safe_text([" A ", 2, ""], default="-"), "A, 2")
|
|
77
|
+
self.assertEqual(MODULE._safe_text(None, default="(none)"), "(none)")
|
|
78
|
+
self.assertEqual(MODULE._safe_text(" ", default="fallback"), "fallback")
|
|
79
|
+
|
|
80
|
+
def test_markup_escapes_html_and_preserves_line_breaks(self) -> None:
|
|
81
|
+
rendered = MODULE._markup("A < B\nline 2")
|
|
82
|
+
self.assertIn("A < B", rendered)
|
|
83
|
+
self.assertIn("<br/>", rendered)
|
|
84
|
+
|
|
85
|
+
def test_bullet_lines_returns_default_paragraph_when_empty(self) -> None:
|
|
86
|
+
styles = MODULE._build_styles("Helvetica", 11)
|
|
87
|
+
bullets = MODULE._bullet_lines([], styles, empty_text="No data")
|
|
88
|
+
self.assertEqual(len(bullets), 1)
|
|
89
|
+
self.assertIsInstance(bullets[0], Paragraph)
|
|
90
|
+
|
|
91
|
+
def test_build_story_contains_multiple_sections(self) -> None:
|
|
92
|
+
font_path, subfont_index = MODULE._detect_cjk_font_path(None)
|
|
93
|
+
font_name = MODULE._register_font(font_path, subfont_index)
|
|
94
|
+
styles = MODULE._build_styles(font_name, 11)
|
|
95
|
+
|
|
96
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
97
|
+
output_pdf = Path(temp_dir) / "preview.pdf"
|
|
98
|
+
doc = MODULE.SimpleDocTemplate(
|
|
99
|
+
str(output_pdf),
|
|
100
|
+
title="Physics Error Book",
|
|
101
|
+
)
|
|
102
|
+
story = MODULE._build_story(minimal_payload(), styles, doc)
|
|
103
|
+
|
|
104
|
+
self.assertGreater(len(story), 10)
|
|
105
|
+
self.assertEqual(story[1].text, "Physics Error Book")
|
|
106
|
+
|
|
107
|
+
def test_main_renders_pdf_from_minimal_payload(self) -> None:
|
|
108
|
+
font_path, _ = MODULE._detect_cjk_font_path(None)
|
|
109
|
+
|
|
110
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
111
|
+
temp_root = Path(temp_dir)
|
|
112
|
+
input_json = temp_root / "error-book.json"
|
|
113
|
+
output_pdf = temp_root / "out" / "error-book.pdf"
|
|
114
|
+
input_json.write_text(json.dumps(minimal_payload(), ensure_ascii=False), encoding="utf-8")
|
|
115
|
+
|
|
116
|
+
argv = [
|
|
117
|
+
str(input_json),
|
|
118
|
+
str(output_pdf),
|
|
119
|
+
"--font-path",
|
|
120
|
+
font_path,
|
|
121
|
+
"--font-size",
|
|
122
|
+
"10",
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
with patch("sys.argv", ["render_error_book_json_to_pdf.py", *argv]):
|
|
126
|
+
exit_code = MODULE.main()
|
|
127
|
+
|
|
128
|
+
self.assertEqual(exit_code, 0)
|
|
129
|
+
self.assertTrue(output_pdf.is_file())
|
|
130
|
+
self.assertTrue(output_pdf.read_bytes().startswith(b"%PDF"))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
unittest.main()
|
package/lib/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
syncToolkitHome,
|
|
11
11
|
getTargetRoots,
|
|
12
12
|
} = require('./installer');
|
|
13
|
+
const { formatToolList, getToolCommand, runTool } = require('./tool-runner');
|
|
13
14
|
const { checkForPackageUpdate } = require('./updater');
|
|
14
15
|
|
|
15
16
|
const TARGET_OPTIONS = [
|
|
@@ -124,6 +125,9 @@ function buildHelpText({ version, colorEnabled }) {
|
|
|
124
125
|
'Usage:',
|
|
125
126
|
' apltk [install] [codex|openclaw|trae|agents|claude-code|all]...',
|
|
126
127
|
' apollo-toolkit [install] [codex|openclaw|trae|agents|claude-code|all]...',
|
|
128
|
+
' apltk tools',
|
|
129
|
+
' apltk <tool> [...args]',
|
|
130
|
+
' apltk tools <tool> [...args]',
|
|
127
131
|
' apltk --help',
|
|
128
132
|
' apollo-toolkit --help',
|
|
129
133
|
'',
|
|
@@ -136,14 +140,37 @@ function buildHelpText({ version, colorEnabled }) {
|
|
|
136
140
|
' apltk agents',
|
|
137
141
|
' apltk claude-code',
|
|
138
142
|
' apltk all',
|
|
143
|
+
' apltk filter-logs app.log --start 2026-03-24T10:00:00Z',
|
|
144
|
+
' apltk create-specs "Membership upgrade flow" --change-name membership-upgrade-flow',
|
|
145
|
+
' apltk tools',
|
|
139
146
|
' apollo-toolkit all',
|
|
140
147
|
'',
|
|
148
|
+
'Bundled tools:',
|
|
149
|
+
formatToolList(),
|
|
150
|
+
'',
|
|
141
151
|
'Options:',
|
|
142
152
|
' --home <path> Override Apollo Toolkit home directory',
|
|
143
153
|
' --help Show this help text',
|
|
144
154
|
].join('\n');
|
|
145
155
|
}
|
|
146
156
|
|
|
157
|
+
function buildToolsHelp({ version, colorEnabled }) {
|
|
158
|
+
return [
|
|
159
|
+
buildBanner({ version, colorEnabled }),
|
|
160
|
+
'',
|
|
161
|
+
'Usage:',
|
|
162
|
+
' apltk tools',
|
|
163
|
+
' apltk <tool> [...args]',
|
|
164
|
+
' apltk tools <tool> [...args]',
|
|
165
|
+
'',
|
|
166
|
+
'Bundled tools:',
|
|
167
|
+
formatToolList(),
|
|
168
|
+
'',
|
|
169
|
+
'Tip:',
|
|
170
|
+
' Pass `--help` after a tool name to view the original script flags.',
|
|
171
|
+
].join('\n');
|
|
172
|
+
}
|
|
173
|
+
|
|
147
174
|
function readPackageJson(sourceRoot) {
|
|
148
175
|
return JSON.parse(fs.readFileSync(path.join(sourceRoot, 'package.json'), 'utf8'));
|
|
149
176
|
}
|
|
@@ -151,11 +178,36 @@ function readPackageJson(sourceRoot) {
|
|
|
151
178
|
function parseArguments(argv) {
|
|
152
179
|
const args = [...argv];
|
|
153
180
|
const result = {
|
|
181
|
+
command: 'install',
|
|
154
182
|
modes: [],
|
|
155
183
|
showHelp: false,
|
|
184
|
+
showToolsHelp: false,
|
|
156
185
|
toolkitHome: null,
|
|
186
|
+
toolName: null,
|
|
187
|
+
toolArgs: [],
|
|
157
188
|
};
|
|
158
189
|
|
|
190
|
+
if (args[0] === 'tools' || args[0] === 'tool') {
|
|
191
|
+
args.shift();
|
|
192
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
193
|
+
result.command = 'tools-help';
|
|
194
|
+
result.showToolsHelp = true;
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
result.command = 'tool';
|
|
199
|
+
result.toolName = args.shift();
|
|
200
|
+
result.toolArgs = args;
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (args[0] && getToolCommand(args[0])) {
|
|
205
|
+
result.command = 'tool';
|
|
206
|
+
result.toolName = args.shift();
|
|
207
|
+
result.toolArgs = args;
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
159
211
|
while (args.length > 0) {
|
|
160
212
|
const arg = args.shift();
|
|
161
213
|
|
|
@@ -368,6 +420,19 @@ async function run(argv, context = {}) {
|
|
|
368
420
|
stdout.write(`${buildHelpText({ version: packageJson.version, colorEnabled: supportsColor(stdout, env) })}\n`);
|
|
369
421
|
return 0;
|
|
370
422
|
}
|
|
423
|
+
if (parsed.showToolsHelp) {
|
|
424
|
+
stdout.write(`${buildToolsHelp({ version: packageJson.version, colorEnabled: supportsColor(stdout, env) })}\n`);
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
if (parsed.command === 'tool') {
|
|
428
|
+
return (context.runTool || runTool)(parsed.toolName, parsed.toolArgs, {
|
|
429
|
+
sourceRoot,
|
|
430
|
+
stdout,
|
|
431
|
+
stderr,
|
|
432
|
+
env,
|
|
433
|
+
spawnCommand: context.spawnCommand,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
371
436
|
|
|
372
437
|
const updateResult = await checkForPackageUpdate({
|
|
373
438
|
packageName: packageJson.name,
|
|
@@ -432,6 +497,7 @@ module.exports = {
|
|
|
432
497
|
buildBanner,
|
|
433
498
|
buildWelcomeScreen,
|
|
434
499
|
buildHelpText,
|
|
500
|
+
buildToolsHelp,
|
|
435
501
|
parseArguments,
|
|
436
502
|
promptForModes,
|
|
437
503
|
readPackageJson,
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const TOOL_COMMANDS = [
|
|
6
|
+
{
|
|
7
|
+
name: 'filter-logs',
|
|
8
|
+
skill: 'analyse-app-logs',
|
|
9
|
+
script: 'analyse-app-logs/scripts/filter_logs_by_time.py',
|
|
10
|
+
runner: 'python3',
|
|
11
|
+
description: 'Filter log lines by timestamp window.',
|
|
12
|
+
aliases: ['filter-logs-by-time'],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'search-logs',
|
|
16
|
+
skill: 'analyse-app-logs',
|
|
17
|
+
script: 'analyse-app-logs/scripts/search_logs.py',
|
|
18
|
+
runner: 'python3',
|
|
19
|
+
description: 'Search logs by keyword or regex.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'docs-to-voice',
|
|
23
|
+
skill: 'docs-to-voice',
|
|
24
|
+
script: 'docs-to-voice/scripts/docs_to_voice.py',
|
|
25
|
+
runner: 'python3',
|
|
26
|
+
description: 'Convert text into audio, timeline JSON, and SRT.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'create-specs',
|
|
30
|
+
skill: 'generate-spec',
|
|
31
|
+
script: 'generate-spec/scripts/create-specs',
|
|
32
|
+
runner: 'python3',
|
|
33
|
+
description: 'Create spec planning documents from templates.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'render-katex',
|
|
37
|
+
skill: 'katex',
|
|
38
|
+
script: 'katex/scripts/render_katex.py',
|
|
39
|
+
runner: 'python3',
|
|
40
|
+
description: 'Render TeX with KaTeX into reusable output.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'render-error-book',
|
|
44
|
+
skill: 'learning-error-book',
|
|
45
|
+
script: 'learning-error-book/scripts/render_error_book_json_to_pdf.py',
|
|
46
|
+
runner: 'python3',
|
|
47
|
+
description: 'Render structured error-book JSON into PDF.',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'open-github-issue',
|
|
51
|
+
skill: 'open-github-issue',
|
|
52
|
+
script: 'open-github-issue/scripts/open_github_issue.py',
|
|
53
|
+
runner: 'python3',
|
|
54
|
+
description: 'Publish or draft a structured GitHub issue.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'generate-storyboard-images',
|
|
58
|
+
skill: 'openai-text-to-image-storyboard',
|
|
59
|
+
script: 'openai-text-to-image-storyboard/scripts/generate_storyboard_images.py',
|
|
60
|
+
runner: 'python3',
|
|
61
|
+
description: 'Generate storyboard image sets from text.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'find-github-issues',
|
|
65
|
+
skill: 'read-github-issue',
|
|
66
|
+
script: 'read-github-issue/scripts/find_issues.py',
|
|
67
|
+
runner: 'python3',
|
|
68
|
+
description: 'List GitHub issues through gh.',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'read-github-issue',
|
|
72
|
+
skill: 'read-github-issue',
|
|
73
|
+
script: 'read-github-issue/scripts/read_issue.py',
|
|
74
|
+
runner: 'python3',
|
|
75
|
+
description: 'Read GitHub issue details through gh.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'review-threads',
|
|
79
|
+
skill: 'resolve-review-comments',
|
|
80
|
+
script: 'resolve-review-comments/scripts/review_threads.py',
|
|
81
|
+
runner: 'python3',
|
|
82
|
+
description: 'List or resolve GitHub PR review threads.',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'enforce-video-aspect-ratio',
|
|
86
|
+
skill: 'text-to-short-video',
|
|
87
|
+
script: 'text-to-short-video/scripts/enforce_video_aspect_ratio.py',
|
|
88
|
+
runner: 'python3',
|
|
89
|
+
description: 'Resize video output to a target aspect ratio.',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'extract-pdf-text-pdfkit',
|
|
93
|
+
skill: 'weekly-financial-event-report',
|
|
94
|
+
script: 'weekly-financial-event-report/scripts/extract_pdf_text_pdfkit.swift',
|
|
95
|
+
runner: 'swift',
|
|
96
|
+
description: 'Extract PDF text with macOS PDFKit fallback.',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'extract-codex-conversations',
|
|
100
|
+
skill: 'codex-memory-manager',
|
|
101
|
+
script: 'codex/codex-memory-manager/scripts/extract_recent_conversations.py',
|
|
102
|
+
runner: 'python3',
|
|
103
|
+
description: 'Extract recent Codex sessions for memory updates.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'sync-codex-memory-index',
|
|
107
|
+
skill: 'codex-memory-manager',
|
|
108
|
+
script: 'codex/codex-memory-manager/scripts/sync_memory_index.py',
|
|
109
|
+
runner: 'python3',
|
|
110
|
+
description: 'Sync the Codex memory index in AGENTS.md.',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'extract-skill-conversations',
|
|
114
|
+
skill: 'learn-skill-from-conversations',
|
|
115
|
+
script: 'codex/learn-skill-from-conversations/scripts/extract_recent_conversations.py',
|
|
116
|
+
runner: 'python3',
|
|
117
|
+
description: 'Extract recent Codex sessions for skill learning.',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'validate-skill-frontmatter',
|
|
121
|
+
skill: 'maintain-skill-catalog',
|
|
122
|
+
script: 'scripts/validate_skill_frontmatter.py',
|
|
123
|
+
runner: 'python3',
|
|
124
|
+
description: 'Validate SKILL.md frontmatter across the catalog.',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'validate-openai-agent-config',
|
|
128
|
+
skill: 'maintain-skill-catalog',
|
|
129
|
+
script: 'scripts/validate_openai_agent_config.py',
|
|
130
|
+
runner: 'python3',
|
|
131
|
+
description: 'Validate every skill agents/openai.yaml config.',
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const TOOL_BY_NAME = new Map();
|
|
136
|
+
|
|
137
|
+
for (const tool of TOOL_COMMANDS) {
|
|
138
|
+
TOOL_BY_NAME.set(tool.name, tool);
|
|
139
|
+
for (const alias of tool.aliases || []) {
|
|
140
|
+
TOOL_BY_NAME.set(alias, { ...tool, name: alias, canonicalName: tool.name });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getToolCommand(name) {
|
|
145
|
+
return TOOL_BY_NAME.get(name) || null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function listToolCommands() {
|
|
149
|
+
return [...TOOL_COMMANDS].sort((left, right) => left.name.localeCompare(right.name));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resolveToolCommand(name, sourceRoot) {
|
|
153
|
+
const tool = getToolCommand(name);
|
|
154
|
+
if (!tool) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
...tool,
|
|
160
|
+
scriptPath: path.join(sourceRoot, tool.script),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatToolList() {
|
|
165
|
+
const tools = listToolCommands();
|
|
166
|
+
const width = tools.reduce((max, tool) => Math.max(max, tool.name.length), 0);
|
|
167
|
+
return tools.map((tool) => {
|
|
168
|
+
const name = tool.name.padEnd(width, ' ');
|
|
169
|
+
return ` ${name} ${tool.description}`;
|
|
170
|
+
}).join('\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function runTool(toolName, toolArgs, context = {}) {
|
|
174
|
+
const sourceRoot = context.sourceRoot || path.resolve(__dirname, '..');
|
|
175
|
+
const stderr = context.stderr || process.stderr;
|
|
176
|
+
const env = context.env || process.env;
|
|
177
|
+
const spawnCommand = context.spawnCommand || spawn;
|
|
178
|
+
const tool = resolveToolCommand(toolName, sourceRoot);
|
|
179
|
+
|
|
180
|
+
if (!tool) {
|
|
181
|
+
stderr.write(`Unknown tool: ${toolName}\n\nAvailable tools:\n${formatToolList()}\n`);
|
|
182
|
+
return Promise.resolve(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!fs.existsSync(tool.scriptPath)) {
|
|
186
|
+
stderr.write(`Tool script not found: ${tool.scriptPath}\n`);
|
|
187
|
+
return Promise.resolve(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return new Promise((resolve) => {
|
|
191
|
+
const child = spawnCommand(tool.runner, [tool.scriptPath, ...toolArgs], {
|
|
192
|
+
cwd: context.cwd || process.cwd(),
|
|
193
|
+
env,
|
|
194
|
+
stdio: context.stdio || 'inherit',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
child.on('error', (error) => {
|
|
198
|
+
stderr.write(`Failed to start ${tool.runner}: ${error.message}\n`);
|
|
199
|
+
resolve(1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
child.on('close', (code) => {
|
|
203
|
+
resolve(typeof code === 'number' ? code : 1);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
formatToolList,
|
|
210
|
+
getToolCommand,
|
|
211
|
+
listToolCommands,
|
|
212
|
+
resolveToolCommand,
|
|
213
|
+
runTool,
|
|
214
|
+
};
|