@laitszkin/apollo-toolkit 2.14.23 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/AGENTS.md +3 -0
  2. package/CHANGELOG.md +11 -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/katex/SKILL.md +3 -3
  25. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  26. package/katex/tests/test_render_katex.py +174 -0
  27. package/learning-error-book/SKILL.md +2 -2
  28. package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +134 -0
  29. package/lib/cli.js +66 -0
  30. package/lib/tool-runner.js +214 -0
  31. package/maintain-project-constraints/SKILL.md +3 -3
  32. package/maintain-skill-catalog/SKILL.md +2 -2
  33. package/novel-to-short-video/SKILL.md +2 -2
  34. package/open-github-issue/README.md +31 -22
  35. package/open-github-issue/SKILL.md +54 -40
  36. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  37. package/open-github-issue/scripts/open_github_issue.py +130 -3
  38. package/open-github-issue/tests/test_open_github_issue.py +95 -0
  39. package/openai-text-to-image-storyboard/README.md +1 -1
  40. package/openai-text-to-image-storyboard/SKILL.md +1 -1
  41. package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +177 -0
  42. package/package.json +1 -1
  43. package/read-github-issue/SKILL.md +9 -9
  44. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  45. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  46. package/resolve-review-comments/SKILL.md +8 -8
  47. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  48. package/review-codebases/README.md +2 -0
  49. package/review-codebases/SKILL.md +1 -0
  50. package/text-to-short-video/README.md +1 -1
  51. package/text-to-short-video/SKILL.md +1 -1
  52. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
  53. package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +194 -0
  54. package/weekly-financial-event-report/SKILL.md +2 -2
  55. package/weekly-financial-event-report/tests/test_extract_pdf_text_pdfkit.py +64 -0
@@ -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
+ };
@@ -70,9 +70,9 @@ Example:
70
70
  ## Common Commands
71
71
 
72
72
  - `npm test` — run the repository's automated test suite.
73
- - `python3 scripts/validate_skill_frontmatter.py` — validate every top-level `SKILL.md` frontmatter block.
74
- - `python3 scripts/validate_openai_agent_config.py` — validate every `agents/openai.yaml` interface config.
75
- - `./scripts/install_skills.sh codex` — install the current toolkit into the local Codex skills directory.
73
+ - `apltk validate-skill-frontmatter` — validate every top-level `SKILL.md` frontmatter block.
74
+ - `apltk validate-openai-agent-config` — validate every `agents/openai.yaml` interface config.
75
+ - `apltk codex` — install the current toolkit into the local Codex skills directory.
76
76
 
77
77
  ## Core Business Flow
78
78
 
@@ -56,8 +56,8 @@ Keep a skill repository coherent when many top-level skills evolve together.
56
56
  ### 4) Validate the catalog after changes
57
57
 
58
58
  - Run:
59
- - `python3 scripts/validate_skill_frontmatter.py`
60
- - `python3 scripts/validate_openai_agent_config.py`
59
+ - `apltk validate-skill-frontmatter`
60
+ - `apltk validate-openai-agent-config`
61
61
  - If the change touched installer or repo-discovery behavior, verify the relevant install scripts or discovery logic as well.
62
62
  - Resolve validation failures before finishing; missing `agents/openai.yaml`, stale prompt references, and mismatched skill names are catalog bugs, not follow-up work.
63
63