@laitszkin/apollo-toolkit 3.13.2 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/AGENTS.md +7 -7
  2. package/CHANGELOG.md +27 -0
  3. package/CLAUDE.md +8 -8
  4. package/analyse-app-logs/SKILL.md +3 -3
  5. package/bin/apollo-toolkit.ts +7 -0
  6. package/codex/codex-memory-manager/SKILL.md +2 -2
  7. package/codex/learn-skill-from-conversations/SKILL.md +3 -3
  8. package/dist/bin/apollo-toolkit.d.ts +2 -0
  9. package/dist/bin/apollo-toolkit.js +7 -0
  10. package/dist/lib/cli.d.ts +41 -0
  11. package/dist/lib/cli.js +655 -0
  12. package/dist/lib/installer.d.ts +59 -0
  13. package/dist/lib/installer.js +404 -0
  14. package/dist/lib/tool-runner.d.ts +19 -0
  15. package/dist/lib/tool-runner.js +536 -0
  16. package/dist/lib/tools/architecture.d.ts +2 -0
  17. package/dist/lib/tools/architecture.js +34 -0
  18. package/dist/lib/tools/create-specs.d.ts +2 -0
  19. package/dist/lib/tools/create-specs.js +175 -0
  20. package/dist/lib/tools/docs-to-voice.d.ts +2 -0
  21. package/dist/lib/tools/docs-to-voice.js +705 -0
  22. package/dist/lib/tools/enforce-video-aspect-ratio.d.ts +2 -0
  23. package/dist/lib/tools/enforce-video-aspect-ratio.js +312 -0
  24. package/dist/lib/tools/extract-conversations.d.ts +2 -0
  25. package/dist/lib/tools/extract-conversations.js +105 -0
  26. package/dist/lib/tools/extract-pdf-text.d.ts +2 -0
  27. package/dist/lib/tools/extract-pdf-text.js +92 -0
  28. package/dist/lib/tools/filter-logs.d.ts +2 -0
  29. package/dist/lib/tools/filter-logs.js +94 -0
  30. package/dist/lib/tools/find-github-issues.d.ts +2 -0
  31. package/dist/lib/tools/find-github-issues.js +176 -0
  32. package/dist/lib/tools/generate-storyboard-images.d.ts +2 -0
  33. package/dist/lib/tools/generate-storyboard-images.js +419 -0
  34. package/dist/lib/tools/log-cli-utils.d.ts +35 -0
  35. package/dist/lib/tools/log-cli-utils.js +233 -0
  36. package/dist/lib/tools/open-github-issue.d.ts +2 -0
  37. package/dist/lib/tools/open-github-issue.js +750 -0
  38. package/dist/lib/tools/read-github-issue.d.ts +2 -0
  39. package/dist/lib/tools/read-github-issue.js +134 -0
  40. package/dist/lib/tools/render-error-book.d.ts +2 -0
  41. package/dist/lib/tools/render-error-book.js +265 -0
  42. package/dist/lib/tools/render-katex.d.ts +2 -0
  43. package/dist/lib/tools/render-katex.js +294 -0
  44. package/dist/lib/tools/review-threads.d.ts +2 -0
  45. package/dist/lib/tools/review-threads.js +491 -0
  46. package/dist/lib/tools/search-logs.d.ts +2 -0
  47. package/dist/lib/tools/search-logs.js +164 -0
  48. package/dist/lib/tools/sync-memory-index.d.ts +2 -0
  49. package/dist/lib/tools/sync-memory-index.js +113 -0
  50. package/dist/lib/tools/validate-openai-agent-config.d.ts +2 -0
  51. package/dist/lib/tools/validate-openai-agent-config.js +184 -0
  52. package/dist/lib/tools/validate-skill-frontmatter.d.ts +2 -0
  53. package/dist/lib/tools/validate-skill-frontmatter.js +118 -0
  54. package/dist/lib/types.d.ts +82 -0
  55. package/dist/lib/types.js +2 -0
  56. package/dist/lib/updater.d.ts +34 -0
  57. package/dist/lib/updater.js +112 -0
  58. package/dist/lib/utils/format.d.ts +2 -0
  59. package/dist/lib/utils/format.js +6 -0
  60. package/dist/lib/utils/terminal.d.ts +12 -0
  61. package/dist/lib/utils/terminal.js +26 -0
  62. package/docs-to-voice/SKILL.md +0 -1
  63. package/generate-spec/SKILL.md +1 -1
  64. package/katex/SKILL.md +1 -2
  65. package/lib/cli.ts +780 -0
  66. package/lib/installer.ts +466 -0
  67. package/lib/tool-runner.ts +561 -0
  68. package/lib/tools/architecture.ts +34 -0
  69. package/lib/tools/create-specs.ts +204 -0
  70. package/lib/tools/docs-to-voice.ts +799 -0
  71. package/lib/tools/enforce-video-aspect-ratio.ts +368 -0
  72. package/lib/tools/extract-conversations.ts +114 -0
  73. package/lib/tools/extract-pdf-text.ts +99 -0
  74. package/lib/tools/filter-logs.ts +118 -0
  75. package/lib/tools/find-github-issues.ts +211 -0
  76. package/lib/tools/generate-storyboard-images.ts +455 -0
  77. package/lib/tools/log-cli-utils.ts +262 -0
  78. package/lib/tools/open-github-issue.ts +930 -0
  79. package/lib/tools/read-github-issue.ts +179 -0
  80. package/lib/tools/render-error-book.ts +300 -0
  81. package/lib/tools/render-katex.ts +325 -0
  82. package/lib/tools/review-threads.ts +590 -0
  83. package/lib/tools/search-logs.ts +200 -0
  84. package/lib/tools/sync-memory-index.ts +114 -0
  85. package/lib/tools/validate-openai-agent-config.ts +209 -0
  86. package/lib/tools/validate-skill-frontmatter.ts +124 -0
  87. package/lib/types.ts +90 -0
  88. package/lib/updater.ts +165 -0
  89. package/lib/utils/format.ts +7 -0
  90. package/lib/utils/terminal.ts +22 -0
  91. package/open-github-issue/SKILL.md +2 -2
  92. package/optimise-skill/SKILL.md +1 -1
  93. package/package.json +13 -4
  94. package/resources/project-architecture/assets/architecture.css +764 -0
  95. package/resources/project-architecture/assets/viewer.client.js +144 -0
  96. package/resources/project-architecture/index.html +42 -0
  97. package/review-spec-related-changes/SKILL.md +1 -1
  98. package/solve-issues-found-during-review/SKILL.md +2 -1
  99. package/tsconfig.json +28 -0
  100. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  101. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  102. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  103. package/analyse-app-logs/scripts/filter_logs_by_time.py +0 -64
  104. package/analyse-app-logs/scripts/log_cli_utils.py +0 -112
  105. package/analyse-app-logs/scripts/search_logs.py +0 -137
  106. package/analyse-app-logs/tests/test_filter_logs_by_time.py +0 -95
  107. package/analyse-app-logs/tests/test_search_logs.py +0 -100
  108. package/codex/codex-memory-manager/scripts/extract_recent_conversations.py +0 -369
  109. package/codex/codex-memory-manager/scripts/sync_memory_index.py +0 -130
  110. package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +0 -177
  111. package/codex/codex-memory-manager/tests/test_memory_template.py +0 -37
  112. package/codex/codex-memory-manager/tests/test_sync_memory_index.py +0 -84
  113. package/codex/learn-skill-from-conversations/scripts/extract_recent_conversations.py +0 -369
  114. package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +0 -177
  115. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  116. package/docs-to-voice/scripts/docs_to_voice.py +0 -1385
  117. package/docs-to-voice/scripts/docs_to_voice.sh +0 -11
  118. package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +0 -210
  119. package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +0 -115
  120. package/docs-to-voice/tests/test_docs_to_voice_settings.py +0 -43
  121. package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +0 -51
  122. package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +0 -57
  123. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  124. package/generate-spec/scripts/create-specs +0 -215
  125. package/generate-spec/tests/test_create_specs.py +0 -200
  126. package/init-project-html/scripts/architecture-bootstrap-render.js +0 -16
  127. package/init-project-html/scripts/architecture.js +0 -296
  128. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  129. package/katex/scripts/render_katex.py +0 -247
  130. package/katex/scripts/render_katex.sh +0 -11
  131. package/katex/tests/test_render_katex.py +0 -174
  132. package/learning-error-book/scripts/render_error_book_json_to_pdf.py +0 -590
  133. package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +0 -134
  134. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  135. package/open-github-issue/scripts/open_github_issue.py +0 -705
  136. package/open-github-issue/tests/test_open_github_issue.py +0 -381
  137. package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +0 -763
  138. package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +0 -177
  139. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  140. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  141. package/read-github-issue/scripts/find_issues.py +0 -148
  142. package/read-github-issue/scripts/read_issue.py +0 -108
  143. package/read-github-issue/tests/test_find_issues.py +0 -127
  144. package/read-github-issue/tests/test_read_issue.py +0 -109
  145. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  146. package/resolve-review-comments/scripts/review_threads.py +0 -425
  147. package/resolve-review-comments/tests/test_review_threads.py +0 -74
  148. package/scripts/validate_openai_agent_config.py +0 -209
  149. package/scripts/validate_skill_frontmatter.py +0 -131
  150. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
  151. package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +0 -350
  152. package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +0 -194
  153. package/weekly-financial-event-report/scripts/extract_pdf_text_pdfkit.swift +0 -99
  154. package/weekly-financial-event-report/tests/test_extract_pdf_text_pdfkit.py +0 -64
@@ -0,0 +1,2 @@
1
+ import type { ToolContext } from '../types';
2
+ export declare function readGitHubIssueHandler(argv: string[], context: ToolContext): Promise<number>;
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readGitHubIssueHandler = readGitHubIssueHandler;
4
+ // @ts-nocheck
5
+ const node_child_process_1 = require("node:child_process");
6
+ const ISSUE_FIELDS = 'number,title,body,state,author,labels,assignees,comments,createdAt,updatedAt,closedAt,url';
7
+ function parseArgs(argv) {
8
+ const args = {
9
+ issue: null,
10
+ repo: null,
11
+ comments: false,
12
+ json: false,
13
+ };
14
+ let i = 0;
15
+ while (i < argv.length) {
16
+ const arg = argv[i];
17
+ switch (arg) {
18
+ case '--repo':
19
+ if (i + 1 < argv.length)
20
+ args.repo = argv[++i];
21
+ break;
22
+ case '--comments':
23
+ args.comments = true;
24
+ break;
25
+ case '--json':
26
+ args.json = true;
27
+ break;
28
+ default:
29
+ if (!arg.startsWith('-')) {
30
+ args.issue = arg;
31
+ }
32
+ break;
33
+ }
34
+ i++;
35
+ }
36
+ return args;
37
+ }
38
+ function runGh(cmdArgs) {
39
+ return new Promise((resolve) => {
40
+ (0, node_child_process_1.execFile)('gh', cmdArgs, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
41
+ if (error) {
42
+ resolve({
43
+ stdout: stdout || '',
44
+ stderr: stderr || '',
45
+ exitCode: error.status ?? 1,
46
+ });
47
+ }
48
+ else {
49
+ resolve({ stdout: stdout || '', stderr: stderr || '', exitCode: 0 });
50
+ }
51
+ });
52
+ });
53
+ }
54
+ function buildCommand(args) {
55
+ const cmd = [
56
+ 'issue',
57
+ 'view',
58
+ args.issue || '',
59
+ '--json',
60
+ ISSUE_FIELDS,
61
+ ];
62
+ if (args.repo) {
63
+ cmd.push('--repo', args.repo);
64
+ }
65
+ return cmd;
66
+ }
67
+ function joinNames(items, key) {
68
+ if (!items)
69
+ return '-';
70
+ const values = items
71
+ .map((item) => String(item[key] || ''))
72
+ .filter(Boolean);
73
+ return values.length > 0 ? values.join(', ') : '-';
74
+ }
75
+ function printSummary(issue, includeComments, context) {
76
+ const { stdout } = context;
77
+ stdout.write(`Number: #${issue.number ?? ''}\n`);
78
+ stdout.write(`Title: ${issue.title ?? ''}\n`);
79
+ stdout.write(`State: ${issue.state ?? ''}\n`);
80
+ stdout.write(`URL: ${issue.url ?? ''}\n`);
81
+ const author = issue.author?.login ?? '-';
82
+ stdout.write(`Author: ${author}\n`);
83
+ stdout.write(`Labels: ${joinNames(issue.labels, 'name')}\n`);
84
+ stdout.write(`Assignees: ${joinNames(issue.assignees, 'login')}\n`);
85
+ stdout.write(`Created: ${issue.createdAt ?? ''}\n`);
86
+ stdout.write(`Updated: ${issue.updatedAt ?? ''}\n`);
87
+ stdout.write(`Closed: ${issue.closedAt ?? '-'}\n`);
88
+ stdout.write('\n');
89
+ stdout.write('Body:\n');
90
+ stdout.write(`${issue.body || '-'}\n`);
91
+ if (includeComments) {
92
+ const comments = issue.comments;
93
+ stdout.write('\n');
94
+ stdout.write(`Comments (${comments?.length ?? 0}):\n`);
95
+ if (!comments || comments.length === 0) {
96
+ stdout.write('-\n');
97
+ return;
98
+ }
99
+ for (const comment of comments) {
100
+ const commentAuthor = comment.author?.login ?? '-';
101
+ const created = comment.createdAt ?? '';
102
+ const body = comment.body || '-';
103
+ stdout.write(`- [${created}] ${commentAuthor}: ${body}\n`);
104
+ }
105
+ }
106
+ }
107
+ async function readGitHubIssueHandler(argv, context) {
108
+ const { stdout, stderr } = context;
109
+ const args = parseArgs(argv);
110
+ if (!args.issue) {
111
+ stderr.write('Error: issue number or URL is required.\n');
112
+ return 1;
113
+ }
114
+ const cmd = buildCommand(args);
115
+ const result = await runGh(cmd);
116
+ if (result.exitCode !== 0) {
117
+ stderr.write(result.stderr.trim() || 'gh issue view failed.\n');
118
+ return result.exitCode;
119
+ }
120
+ let issue;
121
+ try {
122
+ issue = JSON.parse(result.stdout);
123
+ }
124
+ catch {
125
+ stderr.write('Error: unable to parse gh output as JSON.\n');
126
+ return 1;
127
+ }
128
+ if (args.json) {
129
+ stdout.write(JSON.stringify(issue, null, 2) + '\n');
130
+ return 0;
131
+ }
132
+ printSummary(issue, args.comments, context);
133
+ return 0;
134
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolContext } from '../types';
2
+ export declare function renderErrorBookHandler(args: string[], context: ToolContext): Promise<number>;
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderErrorBookHandler = renderErrorBookHandler;
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ function safeText(value, defaultValue = '-') {
11
+ if (value === null || value === undefined)
12
+ return defaultValue;
13
+ if (typeof value === 'string')
14
+ return value.trim() || defaultValue;
15
+ if (typeof value === 'number')
16
+ return String(value);
17
+ if (Array.isArray(value)) {
18
+ const parts = value.map((v) => safeText(v, '')).filter(Boolean);
19
+ return parts.join(', ') || defaultValue;
20
+ }
21
+ return String(value);
22
+ }
23
+ function escapeHtml(text) {
24
+ return text
25
+ .replace(/&/g, '&amp;')
26
+ .replace(/</g, '&lt;')
27
+ .replace(/>/g, '&gt;')
28
+ .replace(/"/g, '&quot;')
29
+ .replace(/'/g, '&#039;');
30
+ }
31
+ function buildHtmlContent(data) {
32
+ const title = safeText(data.title, 'Error Book');
33
+ const type = safeText(data.book_type, 'general');
34
+ const updated = safeText(data.last_updated);
35
+ const questions = Array.isArray(data.questions) ? data.questions : [];
36
+ let html = `<!DOCTYPE html>
37
+ <html lang="en">
38
+ <head>
39
+ <meta charset="utf-8">
40
+ <title>${escapeHtml(title)}</title>
41
+ <style>
42
+ @page { margin: 16mm; }
43
+ body { font-family: -apple-system, 'PingFang SC', 'Noto Sans CJK', Helvetica, Arial, sans-serif; font-size: 11pt; line-height: 1.55; color: #1F2937; }
44
+ h1 { font-size: 24pt; margin-bottom: 0; }
45
+ .subtitle { font-size: 11pt; color: #6B7280; margin-top: 4pt; }
46
+ h2 { font-size: 16pt; color: #0F766E; border-bottom: 1px solid #D1D5DB; padding-bottom: 4pt; margin-top: 20pt; }
47
+ h3 { font-size: 14pt; color: #1F2937; margin-top: 16pt; }
48
+ h4 { font-size: 11pt; color: #1F2937; margin-top: 12pt; margin-bottom: 4pt; }
49
+ table { border-collapse: collapse; width: 100%; margin: 8pt 0; font-size: 9pt; }
50
+ th { background: #0F766E; color: white; padding: 6pt 8pt; text-align: left; }
51
+ td { padding: 6pt 8pt; border: 1px solid #D1D5DB; vertical-align: top; }
52
+ tr:nth-child(even) td { background: #F8FAFC; }
53
+ .callout { background: #FFEDD5; border-left: 4px solid #9A3412; padding: 8pt 10pt; margin: 8pt 0; }
54
+ .meta { font-size: 10pt; color: #6B7280; }
55
+ .section { margin-top: 20pt; }
56
+ .question-block { page-break-inside: avoid; }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <h1>${escapeHtml(title)}</h1>
61
+ <p class="subtitle">Type: ${escapeHtml(type)} | Last updated: ${escapeHtml(updated)}</p>
62
+ <p class="subtitle">A structured review of mistakes, concepts, and corrections.</p>
63
+ `;
64
+ // Coverage scope table
65
+ html += '<h2>Coverage Scope</h2>\n<table><thead><tr><th>Source</th><th>Questions</th><th>Notes</th></tr></thead><tbody>\n';
66
+ const coverageScope = Array.isArray(data.coverage_scope) ? data.coverage_scope : [];
67
+ if (coverageScope.length === 0) {
68
+ html += '<tr><td>-</td><td>-</td><td>-</td></tr>\n';
69
+ }
70
+ for (const item of coverageScope) {
71
+ html += `<tr><td>${escapeHtml(safeText(item.source_path))}</td><td>${escapeHtml(safeText(item.included_questions))}</td><td>${escapeHtml(safeText(item.notes))}</td></tr>\n`;
72
+ }
73
+ html += '</tbody></table>\n';
74
+ // Mistake overview
75
+ html += '<h2>Common Mistake Types Overview</h2>\n';
76
+ const overview = Array.isArray(data.mistake_overview) ? data.mistake_overview : [];
77
+ if (overview.length === 0) {
78
+ html += '<p>No mistake overview provided.</p>\n';
79
+ }
80
+ for (const entry of overview) {
81
+ html += `<div style="background:#F8FAFC;border:1px solid #D1D5DB;padding:8pt 10pt;margin:6pt 0;">
82
+ <h4>${escapeHtml(safeText(entry.type))}</h4>
83
+ <p>${escapeHtml(safeText(entry.summary))}</p>
84
+ <p class="meta">Representative questions: ${escapeHtml(safeText(entry.representative_questions))}</p>
85
+ </div>\n`;
86
+ }
87
+ // Concept highlights
88
+ html += '<h2>Conceptual Mistake Highlights</h2>\n';
89
+ const concepts = Array.isArray(data.concept_highlights) ? data.concept_highlights : [];
90
+ if (concepts.length === 0) {
91
+ html += '<p>No concept highlights provided.</p>\n';
92
+ }
93
+ for (const concept of concepts) {
94
+ const checklist = Array.isArray(concept.checklist) ? concept.checklist : [];
95
+ html += `<h3>${escapeHtml(safeText(concept.name, 'Unnamed concept'))}</h3>
96
+ <table><tbody>
97
+ <tr><td style="background:#EEF2FF;width:22%;font-weight:bold;">Definition</td><td>${escapeHtml(safeText(concept.definition))}</td></tr>
98
+ <tr><td style="background:#EEF2FF;font-weight:bold;">Common misjudgment</td><td>${escapeHtml(safeText(concept.common_misjudgment))}</td></tr>
99
+ <tr><td style="background:#EEF2FF;font-weight:bold;">Checklist</td><td>${checklist.map((c) => `- ${escapeHtml(safeText(c))}`).join('<br>') || '-'}</td></tr>
100
+ </tbody></table>\n`;
101
+ }
102
+ // Questions
103
+ if (questions.length > 0) {
104
+ html += '<div style="page-break-before:always;"></div>\n';
105
+ html += '<h2>Mistake-by-Mistake Analysis & Solutions</h2>\n';
106
+ for (const q of questions) {
107
+ const qMeta = [
108
+ ['Source', safeText(q.source_path)],
109
+ ['Locator', safeText(q.page_or_locator)],
110
+ ['User answer', safeText(q.user_answer)],
111
+ ['Correct answer', safeText(q.correct_answer)],
112
+ ['Mistake type', safeText(q.mistake_type)],
113
+ ['Concepts', safeText(q.concepts)],
114
+ ];
115
+ html += `<div class="question-block">
116
+ <h3>${escapeHtml(safeText(q.question_id, 'Unnamed question'))}</h3>
117
+ <table><tbody>
118
+ ${qMeta.map(([label, val]) => `<tr><td style="background:#F8FAFC;width:18%;font-weight:bold;">${escapeHtml(label)}</td><td>${escapeHtml(val)}</td></tr>`).join('\n')}
119
+ </tbody></table>
120
+ <h4>Stem</h4>
121
+ <p>${escapeHtml(safeText(q.stem))}</p>
122
+ <h4>Why it was wrong</h4>
123
+ <p>${escapeHtml(safeText(q.why_wrong))}</p>
124
+ `;
125
+ // Correct solution steps
126
+ const steps = Array.isArray(q.correct_solution_steps) ? q.correct_solution_steps : [];
127
+ if (steps.length > 0) {
128
+ html += '<h4>Correct solution</h4>\n<ul>\n';
129
+ for (const step of steps) {
130
+ html += `<li>${escapeHtml(safeText(step))}</li>\n`;
131
+ }
132
+ html += '</ul>\n';
133
+ }
134
+ // Options table for MC questions
135
+ const options = Array.isArray(q.options) ? q.options : [];
136
+ if (options.length > 0) {
137
+ html += '<h4>Option-by-option reasoning</h4>\n<table><thead><tr><th>Option</th><th>Text</th><th>Verdict</th><th>Reason</th></tr></thead><tbody>\n';
138
+ for (const opt of options) {
139
+ html += `<tr><td>${escapeHtml(safeText(opt.label))}</td><td>${escapeHtml(safeText(opt.text))}</td><td>${escapeHtml(safeText(opt.verdict))}</td><td>${escapeHtml(safeText(opt.reason))}</td></tr>\n`;
140
+ }
141
+ html += '</tbody></table>\n';
142
+ }
143
+ // Step comparison for long questions
144
+ const stepComparison = Array.isArray(q.step_comparison) ? q.step_comparison : [];
145
+ if (stepComparison.length > 0) {
146
+ html += '<h4>Step-by-step comparison</h4>\n<table><thead><tr><th>Step</th><th>Expected</th><th>User</th><th>Gap</th><th>Fix</th></tr></thead><tbody>\n';
147
+ for (const sc of stepComparison) {
148
+ html += `<tr><td>${escapeHtml(safeText(sc.step_no))}</td><td>${escapeHtml(safeText(sc.expected_step))}</td><td>${escapeHtml(safeText(sc.user_step))}</td><td>${escapeHtml(safeText(sc.gap))}</td><td>${escapeHtml(safeText(sc.fix))}</td></tr>\n`;
149
+ }
150
+ html += '</tbody></table>\n';
151
+ }
152
+ html += '</div>\n';
153
+ }
154
+ }
155
+ html += '\n</body>\n</html>\n';
156
+ return html;
157
+ }
158
+ async function renderErrorBookHandler(args, context) {
159
+ const stdout = context.stdout || process.stdout;
160
+ const stderr = context.stderr || process.stderr;
161
+ let inputFile = '';
162
+ let outputFile = '';
163
+ for (let i = 0; i < args.length; i++) {
164
+ const arg = args[i];
165
+ if (arg === '--help' || arg === '-h') {
166
+ stdout.write(`Usage: apltk render-error-book --input <json> --output <pdf>
167
+
168
+ Options:
169
+ --input Input JSON file path
170
+ --output Output PDF file path
171
+ `);
172
+ return 0;
173
+ }
174
+ if (arg === '--input') {
175
+ inputFile = args[++i] || '';
176
+ }
177
+ else if (arg === '--output') {
178
+ outputFile = args[++i] || '';
179
+ }
180
+ else if (!inputFile && !arg.startsWith('-')) {
181
+ // positional: first is input, second is output (backward compat)
182
+ if (!inputFile) {
183
+ inputFile = arg;
184
+ }
185
+ else if (!outputFile) {
186
+ outputFile = arg;
187
+ }
188
+ }
189
+ }
190
+ if (!inputFile) {
191
+ stderr.write('Error: --input is required.\n');
192
+ return 1;
193
+ }
194
+ if (!outputFile) {
195
+ stderr.write('Error: --output is required.\n');
196
+ return 1;
197
+ }
198
+ const inputPath = node_path_1.default.resolve(inputFile);
199
+ const outputPath = node_path_1.default.resolve(outputFile);
200
+ if (!node_fs_1.default.existsSync(inputPath)) {
201
+ stderr.write(`Error: Input file not found: ${inputPath}\n`);
202
+ return 1;
203
+ }
204
+ let data;
205
+ try {
206
+ const raw = node_fs_1.default.readFileSync(inputPath, 'utf-8');
207
+ data = JSON.parse(raw);
208
+ }
209
+ catch (err) {
210
+ const msg = err instanceof Error ? err.message : 'Invalid JSON';
211
+ stderr.write(`Error: Failed to parse input JSON: ${msg}\n`);
212
+ return 1;
213
+ }
214
+ // Generate HTML
215
+ const htmlContent = buildHtmlContent(data);
216
+ // Try to convert to PDF
217
+ const tmpHtmlFile = node_path_1.default.join(node_fs_1.default.mkdtempSync('error-book-'), 'output.html');
218
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(tmpHtmlFile), { recursive: true });
219
+ node_fs_1.default.writeFileSync(tmpHtmlFile, htmlContent, 'utf-8');
220
+ let converted = false;
221
+ // Try pandoc first
222
+ try {
223
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(outputPath), { recursive: true });
224
+ (0, node_child_process_1.execSync)(`pandoc "${tmpHtmlFile}" -o "${outputPath}" --pdf-engine=weasyprint 2>/dev/null || ` +
225
+ `pandoc "${tmpHtmlFile}" -o "${outputPath}" --pdf-engine=wkhtmltopdf 2>/dev/null || ` +
226
+ `pandoc "${tmpHtmlFile}" -o "${outputPath}"`, { stdio: 'ignore', timeout: 60000 });
227
+ if (node_fs_1.default.existsSync(outputPath) && node_fs_1.default.statSync(outputPath).size > 0) {
228
+ converted = true;
229
+ }
230
+ }
231
+ catch {
232
+ // Fall through
233
+ }
234
+ // Try wkhtmltopdf
235
+ if (!converted) {
236
+ try {
237
+ (0, node_child_process_1.execSync)(`wkhtmltopdf "${tmpHtmlFile}" "${outputPath}"`, { stdio: 'ignore', timeout: 60000 });
238
+ if (node_fs_1.default.existsSync(outputPath) && node_fs_1.default.statSync(outputPath).size > 0) {
239
+ converted = true;
240
+ }
241
+ }
242
+ catch {
243
+ // Fall through
244
+ }
245
+ }
246
+ // Clean up temp file
247
+ try {
248
+ node_fs_1.default.unlinkSync(tmpHtmlFile);
249
+ node_fs_1.default.rmdirSync(node_path_1.default.dirname(tmpHtmlFile));
250
+ }
251
+ catch {
252
+ // ignore cleanup errors
253
+ }
254
+ if (converted) {
255
+ stdout.write(`${outputPath}\n`);
256
+ return 0;
257
+ }
258
+ // Fallback: write HTML and report
259
+ const htmlOutputPath = outputPath.replace(/\.pdf$/i, '.html') + '.html';
260
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(htmlOutputPath), { recursive: true });
261
+ node_fs_1.default.writeFileSync(htmlOutputPath, htmlContent, 'utf-8');
262
+ stdout.write(`${htmlOutputPath}\n`);
263
+ stderr.write('Warning: No PDF converter found (pandoc/wkhtmltopdf). HTML was written instead.\n');
264
+ return 0;
265
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolContext } from '../types';
2
+ export declare function renderKatexHandler(args: string[], context: ToolContext): Promise<number>;