@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.
Files changed (58) hide show
  1. package/AGENTS.md +3 -0
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +9 -0
  4. package/analyse-app-logs/README.md +5 -5
  5. package/analyse-app-logs/SKILL.md +7 -5
  6. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  7. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  8. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  9. package/codex/codex-memory-manager/README.md +2 -2
  10. package/codex/codex-memory-manager/SKILL.md +5 -5
  11. package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +3 -2
  12. package/codex/learn-skill-from-conversations/README.md +1 -1
  13. package/codex/learn-skill-from-conversations/SKILL.md +2 -2
  14. package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +3 -2
  15. package/docs-to-voice/README.md +3 -3
  16. package/docs-to-voice/SKILL.md +4 -4
  17. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  18. package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +51 -0
  19. package/feature-propose/SKILL.md +1 -0
  20. package/generate-spec/README.md +3 -6
  21. package/generate-spec/SKILL.md +2 -3
  22. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  23. package/generate-spec/tests/test_create_specs.py +166 -0
  24. package/jupiter-development/SKILL.md +5 -0
  25. package/katex/SKILL.md +3 -3
  26. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  27. package/katex/tests/test_render_katex.py +174 -0
  28. package/learning-error-book/SKILL.md +2 -2
  29. package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +134 -0
  30. package/lib/cli.js +66 -0
  31. package/lib/tool-runner.js +214 -0
  32. package/maintain-project-constraints/SKILL.md +3 -3
  33. package/maintain-skill-catalog/SKILL.md +2 -2
  34. package/novel-to-short-video/SKILL.md +2 -2
  35. package/open-github-issue/README.md +31 -22
  36. package/open-github-issue/SKILL.md +54 -40
  37. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  38. package/open-github-issue/scripts/open_github_issue.py +130 -3
  39. package/open-github-issue/tests/test_open_github_issue.py +95 -0
  40. package/openai-text-to-image-storyboard/README.md +1 -1
  41. package/openai-text-to-image-storyboard/SKILL.md +1 -1
  42. package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +177 -0
  43. package/package.json +1 -1
  44. package/read-github-issue/SKILL.md +9 -9
  45. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  46. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  47. package/resolve-review-comments/SKILL.md +8 -8
  48. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  49. package/review-codebases/README.md +2 -0
  50. package/review-codebases/SKILL.md +1 -0
  51. package/scheduled-runtime-health-check/SKILL.md +3 -0
  52. package/systematic-debug/SKILL.md +3 -0
  53. package/text-to-short-video/README.md +1 -1
  54. package/text-to-short-video/SKILL.md +1 -1
  55. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
  56. package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +194 -0
  57. package/weekly-financial-event-report/SKILL.md +2 -2
  58. 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 `scripts/render_katex.py` for deterministic output whenever a static rendered snippet is enough.
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
- python3 katex/scripts/render_katex.py \
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.
@@ -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
- - `python3 learning-error-book/scripts/render_error_book_json_to_pdf.py error_book/references/mc-question-reference.json error_book/mc-question-error-book.pdf`
115
- - `python3 learning-error-book/scripts/render_error_book_json_to_pdf.py error_book/references/long-question-reference.json error_book/long-question-error-book.pdf`
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 &lt; 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
+ };