@seanyao/roll 2026.529.5 → 2026.601.2
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/CHANGELOG.md +57 -25
- package/README.md +10 -7
- package/bin/roll +3952 -317
- package/conventions/config.yaml +7 -0
- package/lib/__pycache__/github_sync.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop_result_eval.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-home.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_git.cpython-314.pyc +0 -0
- package/lib/__pycache__/slides-render.cpython-314.pyc +0 -0
- package/lib/agent_usage/__init__.py +4 -0
- package/lib/agent_usage/__pycache__/__init__.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/gemini.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/kimi.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/openai.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/qwen.cpython-314.pyc +0 -0
- package/lib/agent_usage/gemini.py +127 -0
- package/lib/agent_usage/kimi.py +127 -0
- package/lib/agent_usage/openai.py +126 -0
- package/lib/agent_usage/qwen.py +128 -0
- package/lib/context_feed_budget.sh +194 -0
- package/lib/github_sync.py +876 -0
- package/lib/i18n/agent.sh +54 -0
- package/lib/i18n/init.sh +22 -0
- package/lib/i18n/peer.sh +7 -0
- package/lib/i18n/peer_help.sh +4 -0
- package/lib/i18n/skills_catalog.sh +30 -0
- package/lib/loop-exit-summary.py +393 -0
- package/lib/loop-fmt.py +93 -75
- package/lib/loop_pick_agent.py +241 -170
- package/lib/loop_result_eval.py +469 -0
- package/lib/model_prices.py +0 -10
- package/lib/roll-home.py +1 -28
- package/lib/roll-loop-status.py +330 -40
- package/lib/roll-onboard-render.py +378 -0
- package/lib/roll-peer.py +1 -1
- package/lib/roll-plan-validate.py +165 -0
- package/lib/roll_git.py +41 -0
- package/lib/slides/components/README.md +8 -2
- package/lib/slides/templates/introduction-v3.html +1 -6
- package/lib/slides-render.py +305 -15
- package/lib/slides-validate.py +195 -7
- package/package.json +1 -1
- package/skills/roll-.changelog/SKILL.md +67 -56
- package/skills/roll-brief/SKILL.md +1 -1
- package/skills/roll-build/SKILL.md +14 -12
- package/skills/roll-deck/SKILL.md +152 -0
- package/skills/roll-design/SKILL.md +13 -6
- package/skills/roll-doc/SKILL.md +269 -6
- package/skills/roll-fix/SKILL.md +15 -9
- package/skills/roll-loop/SKILL.md +9 -7
- package/skills/roll-notes/SKILL.md +1 -1
- package/skills/roll-onboard/SKILL.md +85 -0
- package/skills/roll-peer/SKILL.md +6 -5
- package/lib/agent_routes_lint.py +0 -203
- package/skills/roll-research/SKILL.md +0 -316
- package/skills/roll-research/references/schema.json +0 -166
- package/skills/roll-research/scripts/md_to_pdf.py +0 -289
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 数字生命卡兹克 (Digital Life Khazix)
|
|
2
|
-
// Released under the MIT License: https://github.com/KKKKhazix/khazix-skills/blob/main/LICENSE
|
|
3
|
-
// This file contains substantial portions of the Horizontal-Vertical Analysis Framework
|
|
4
|
-
// from khazix-skills (https://github.com/KKKKhazix/khazix-skills).
|
|
5
|
-
{
|
|
6
|
-
"$schema": "Horizontal-Vertical Analysis Framework",
|
|
7
|
-
"version": "1.0",
|
|
8
|
-
"description": "A dual-axis analysis framework for systematically researching products, companies, concepts, or people. The vertical axis traces the complete lifecycle, the horizontal axis expands into current competitive comparison.",
|
|
9
|
-
|
|
10
|
-
"meta": {
|
|
11
|
-
"subject": {
|
|
12
|
-
"name": "string — Name of the research subject",
|
|
13
|
-
"type": "enum: product | company | concept | person | other",
|
|
14
|
-
"one_line_definition": "string — Define what this thing is in one sentence",
|
|
15
|
-
"field": "string — Industry / discipline / domain",
|
|
16
|
-
"research_start_date": "date — When the research began",
|
|
17
|
-
"researcher": "string — Who is conducting this research",
|
|
18
|
-
"research_motivation": "string — Why research this, what was the trigger"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
"vertical_axis_lifecycle": {
|
|
23
|
-
"description": "Complete timeline from origin to present, presented as narrative — the more detailed and multifaceted, the better",
|
|
24
|
-
|
|
25
|
-
"origin": {
|
|
26
|
-
"birth_context": "string — The era, industry state, and technological environment when it was born",
|
|
27
|
-
"birth_motivation": "string — Why did it appear? What problem did it solve? What gap did it fill?",
|
|
28
|
-
"founders_or_initiators": {
|
|
29
|
-
"who": "string",
|
|
30
|
-
"background": "string — What had these people done before, why were they the ones to do this",
|
|
31
|
-
"initial_vision": "string — What they originally wanted to build"
|
|
32
|
-
},
|
|
33
|
-
"birth_date": "date",
|
|
34
|
-
"birth_location_or_environment": "string",
|
|
35
|
-
"early_form": "string — What it looked like initially, how it differs from today",
|
|
36
|
-
"seed_event": "string — Was there a key event or inspiration that directly prompted its creation"
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
"key_milestone_timeline": [
|
|
40
|
-
{
|
|
41
|
-
"date": "date",
|
|
42
|
-
"event_name": "string",
|
|
43
|
-
"event_description": "string — What happened",
|
|
44
|
-
"why_important": "string — How this event changed the subsequent trajectory",
|
|
45
|
-
"decisions_and_choices": "string — What choices were made at this node, what was abandoned",
|
|
46
|
-
"external_trigger": "string | null — Was it triggered by external events (policy, competitor moves, market changes)",
|
|
47
|
-
"results_and_impact": "string — Direct and indirect consequences of this event"
|
|
48
|
-
}
|
|
49
|
-
],
|
|
50
|
-
|
|
51
|
-
"phase_segmentation": [
|
|
52
|
-
{
|
|
53
|
-
"phase_name": "string — e.g.: inception, rapid growth, transformation, maturity",
|
|
54
|
-
"time_span": "string — Start to end dates",
|
|
55
|
-
"core_characteristics": "string — The most prominent features of this phase",
|
|
56
|
-
"core_tensions": "string — The biggest challenge or internal tension during this phase",
|
|
57
|
-
"key_figures": ["string — People who played key roles during this phase"],
|
|
58
|
-
"key_decisions": ["string — Major decisions made during this phase"],
|
|
59
|
-
"phase_outcomes": "string — What results were delivered by the end of this phase"
|
|
60
|
-
}
|
|
61
|
-
],
|
|
62
|
-
|
|
63
|
-
"path_dependency_analysis": {
|
|
64
|
-
"lock_in_decisions": ["string — Which early decisions locked in later development direction, becoming irreversible"],
|
|
65
|
-
"missed_forks": ["string — Which key moments could have gone a different way"],
|
|
66
|
-
"self_reinforcing_mechanisms": "string — What mechanisms deepened the path (network effects, ecosystem lock-in, user habits, etc.)"
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
"narrative_threads": {
|
|
70
|
-
"main_story": "string — If the entire journey could be summarized in one story, what would it be",
|
|
71
|
-
"recurring_themes": ["string — Patterns, tensions, or themes that appear repeatedly across different phases"],
|
|
72
|
-
"turning_points": ["string — The most dramatic turns"],
|
|
73
|
-
"unresolved_mysteries": ["string — What information is still missing and needs further investigation"]
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
"horizontal_axis_competitive_comparison": {
|
|
78
|
-
"description": "At the current point in time, systematic horizontal comparison with competitors/peers",
|
|
79
|
-
|
|
80
|
-
"analysis_time_snapshot": "date — The baseline point in time for horizontal comparison",
|
|
81
|
-
|
|
82
|
-
"subject_profile": {
|
|
83
|
-
"current_positioning": "string — How it currently defines itself",
|
|
84
|
-
"core_strengths": ["string — Its strongest points"],
|
|
85
|
-
"core_weaknesses": ["string — Its weakest points"],
|
|
86
|
-
"target_users": "string — Who it serves",
|
|
87
|
-
"business_model": "string — How it makes money / sustains operations",
|
|
88
|
-
"technical_approach": "string — Underlying technology choices",
|
|
89
|
-
"user_scale_and_growth": "string — Current scale and growth trend",
|
|
90
|
-
"user_reputation": {
|
|
91
|
-
"positive_feedback": ["string — Most frequently praised points"],
|
|
92
|
-
"negative_feedback": ["string — Most frequently criticized points"],
|
|
93
|
-
"typical_use_cases": ["string — Main scenarios where users use it"]
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
"competitor_list": [
|
|
98
|
-
{
|
|
99
|
-
"competitor_name": "string",
|
|
100
|
-
"competitor_type": "enum: direct | indirect | potential | alternative",
|
|
101
|
-
"one_line_definition": "string",
|
|
102
|
-
"current_positioning": "string",
|
|
103
|
-
"core_strengths": ["string"],
|
|
104
|
-
"core_weaknesses": ["string"],
|
|
105
|
-
"target_users": "string",
|
|
106
|
-
"business_model": "string",
|
|
107
|
-
"technical_approach": "string",
|
|
108
|
-
"user_scale_and_growth": "string",
|
|
109
|
-
"user_reputation": {
|
|
110
|
-
"positive_feedback": ["string"],
|
|
111
|
-
"negative_feedback": ["string"],
|
|
112
|
-
"typical_use_cases": ["string"]
|
|
113
|
-
},
|
|
114
|
-
"key_difference_from_subject": "string — Compared to the research subject, what is the most fundamental difference",
|
|
115
|
-
"threat_level": "enum: high | medium | low",
|
|
116
|
-
"threat_analysis": "string — Why it constitutes this level of threat"
|
|
117
|
-
}
|
|
118
|
-
],
|
|
119
|
-
|
|
120
|
-
"dimensional_comparison_matrix": {
|
|
121
|
-
"description": "Select the most critical dimensions and compare the subject against all competitors",
|
|
122
|
-
"comparison_dimensions": [
|
|
123
|
-
{
|
|
124
|
-
"dimension_name": "string — e.g.: performance, pricing, usability, ecosystem, community activity",
|
|
125
|
-
"why_this_dimension": "string — Why this dimension matters",
|
|
126
|
-
"performance_by_player": {
|
|
127
|
-
"subject": "string",
|
|
128
|
-
"competitor_a": "string",
|
|
129
|
-
"competitor_b": "string"
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
]
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
"competitive_landscape_assessment": {
|
|
136
|
-
"current_landscape": "string — Is it a hundred flowers blooming, a duopoly, a monopoly, or something else",
|
|
137
|
-
"landscape_formation_reasons": "string — Why is it this landscape",
|
|
138
|
-
"landscape_evolution_trend": "string — Where might it head next",
|
|
139
|
-
"subject_position": "string — Where does the subject sit in this landscape"
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
"cross_axis_insights": {
|
|
144
|
-
"description": "Cross-analysis of vertical and horizontal axes — the most valuable part of HV Analysis",
|
|
145
|
-
|
|
146
|
-
"how_history_shaped_current_position": "string — Which decisions and events from the vertical trajectory determined its position in today's horizontal comparison",
|
|
147
|
-
"longitudinal_comparison_of_competitors": "string — If competitors are also placed on the timeline, how do their origins and evolutions differ",
|
|
148
|
-
"historical_roots_of_current_advantages": ["string — Each current advantage traced back to which historical node"],
|
|
149
|
-
"historical_roots_of_current_disadvantages": ["string — Each current weakness traced back to which historical decision"],
|
|
150
|
-
"future_projection": {
|
|
151
|
-
"based_on_vertical_trends": "string — If the current trajectory continues, what happens next",
|
|
152
|
-
"based_on_horizontal_competition": "string — How will changes in the competitive landscape affect it",
|
|
153
|
-
"most_likely_scenario": "string",
|
|
154
|
-
"most_dangerous_scenario": "string",
|
|
155
|
-
"most_optimistic_scenario": "string"
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
"output_requirements": {
|
|
160
|
-
"vertical_output": "Presented as narrative/story, not a timeline list. Must have causal logic, details, people — let readers feel 'why it arrived where it is today step by step'",
|
|
161
|
-
"horizontal_output": "Presented as comparative analysis, with both structured dimensional comparison and qualitative judgments and insights",
|
|
162
|
-
"cross_axis_output": "Presented as analytical commentary, weaving information from both axes to produce original judgments",
|
|
163
|
-
"suggested_word_count": "Vertical 3,000–8,000 words, Horizontal 2,000–5,000 words, Cross-axis 1,000–2,000 words",
|
|
164
|
-
"source_attribution": "All key facts must cite information sources (links, documents, interviews, etc.)"
|
|
165
|
-
}
|
|
166
|
-
}
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Deep Research Report: Markdown to PDF converter (WeasyPrint)
|
|
4
|
-
Usage: python md_to_pdf.py input.md output.pdf [--title "Report Title"] [--author "Author"]
|
|
5
|
-
|
|
6
|
-
Dependencies: pip install weasyprint markdown --break-system-packages
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import sys
|
|
10
|
-
import os
|
|
11
|
-
import re
|
|
12
|
-
import argparse
|
|
13
|
-
import markdown
|
|
14
|
-
|
|
15
|
-
# ── CSS Styles ──
|
|
16
|
-
CSS_TEMPLATE = """
|
|
17
|
-
@page {
|
|
18
|
-
size: A4;
|
|
19
|
-
margin: 25mm 20mm 20mm 20mm;
|
|
20
|
-
|
|
21
|
-
@top-center {
|
|
22
|
-
content: "HEADER_TEXT";
|
|
23
|
-
font-family: "Droid Sans Fallback", Helvetica, Arial, sans-serif;
|
|
24
|
-
font-size: 8pt;
|
|
25
|
-
color: #95a5a6;
|
|
26
|
-
border-bottom: 0.5pt solid #ecf0f1;
|
|
27
|
-
padding-bottom: 3mm;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
@bottom-center {
|
|
31
|
-
content: "Page " counter(page);
|
|
32
|
-
font-family: "Droid Sans Fallback", Helvetica, Arial, sans-serif;
|
|
33
|
-
font-size: 8pt;
|
|
34
|
-
color: #95a5a6;
|
|
35
|
-
border-top: 0.8pt solid #1a5276;
|
|
36
|
-
padding-top: 2mm;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
@page :first {
|
|
41
|
-
@top-center { content: none; }
|
|
42
|
-
@bottom-center { content: none; }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
body {
|
|
46
|
-
font-family: "Droid Sans Fallback", Helvetica, Arial, sans-serif;
|
|
47
|
-
font-size: 10.5pt;
|
|
48
|
-
line-height: 1.75;
|
|
49
|
-
color: #2c3e50;
|
|
50
|
-
text-align: justify;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/* Cover page */
|
|
54
|
-
.cover {
|
|
55
|
-
page-break-after: always;
|
|
56
|
-
text-align: center;
|
|
57
|
-
padding-top: 45%;
|
|
58
|
-
}
|
|
59
|
-
.cover h1 {
|
|
60
|
-
font-size: 28pt;
|
|
61
|
-
color: #1a5276;
|
|
62
|
-
margin-bottom: 8mm;
|
|
63
|
-
font-weight: bold;
|
|
64
|
-
letter-spacing: 2pt;
|
|
65
|
-
}
|
|
66
|
-
.cover .subtitle {
|
|
67
|
-
font-size: 14pt;
|
|
68
|
-
color: #95a5a6;
|
|
69
|
-
margin-bottom: 6mm;
|
|
70
|
-
}
|
|
71
|
-
.cover .meta {
|
|
72
|
-
font-size: 11pt;
|
|
73
|
-
color: #95a5a6;
|
|
74
|
-
margin-bottom: 4mm;
|
|
75
|
-
}
|
|
76
|
-
.cover .divider {
|
|
77
|
-
width: 60%;
|
|
78
|
-
margin: 8mm auto;
|
|
79
|
-
border: none;
|
|
80
|
-
border-top: 1.5pt solid #1a5276;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/* H1 */
|
|
84
|
-
h1 {
|
|
85
|
-
font-size: 20pt;
|
|
86
|
-
color: #1a5276;
|
|
87
|
-
margin-top: 16mm;
|
|
88
|
-
margin-bottom: 6mm;
|
|
89
|
-
padding-bottom: 3mm;
|
|
90
|
-
border-bottom: 2pt solid #1a5276;
|
|
91
|
-
page-break-before: always;
|
|
92
|
-
font-weight: bold;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/* H2 */
|
|
96
|
-
h2 {
|
|
97
|
-
font-size: 14pt;
|
|
98
|
-
color: #1e8449;
|
|
99
|
-
margin-top: 10mm;
|
|
100
|
-
margin-bottom: 5mm;
|
|
101
|
-
font-weight: bold;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/* H3 */
|
|
105
|
-
h3 {
|
|
106
|
-
font-size: 12pt;
|
|
107
|
-
color: #2e86c1;
|
|
108
|
-
margin-top: 6mm;
|
|
109
|
-
margin-bottom: 3mm;
|
|
110
|
-
font-weight: bold;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
h4 {
|
|
114
|
-
font-size: 11pt;
|
|
115
|
-
color: #5b2c6f;
|
|
116
|
-
margin-top: 5mm;
|
|
117
|
-
margin-bottom: 2mm;
|
|
118
|
-
font-weight: bold;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/* Paragraphs */
|
|
122
|
-
p {
|
|
123
|
-
margin-top: 1.5mm;
|
|
124
|
-
margin-bottom: 1.5mm;
|
|
125
|
-
orphans: 3;
|
|
126
|
-
widows: 3;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/* Blockquotes */
|
|
130
|
-
blockquote {
|
|
131
|
-
margin: 4mm 0;
|
|
132
|
-
padding: 4mm 4mm 4mm 10mm;
|
|
133
|
-
background: #f8f9fa;
|
|
134
|
-
border-left: 3pt solid #1a5276;
|
|
135
|
-
color: #5d6d7e;
|
|
136
|
-
font-size: 10pt;
|
|
137
|
-
}
|
|
138
|
-
blockquote p {
|
|
139
|
-
margin: 1mm 0;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/* Bold */
|
|
143
|
-
strong, b {
|
|
144
|
-
font-weight: bold;
|
|
145
|
-
color: #1a252f;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/* Inline code */
|
|
149
|
-
code {
|
|
150
|
-
font-family: "Courier New", Courier, monospace;
|
|
151
|
-
background: #fdf2e9;
|
|
152
|
-
color: #c0392b;
|
|
153
|
-
padding: 0.5mm 1.5mm;
|
|
154
|
-
border-radius: 2pt;
|
|
155
|
-
font-size: 9.5pt;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/* Tables */
|
|
159
|
-
table {
|
|
160
|
-
width: 100%;
|
|
161
|
-
border-collapse: collapse;
|
|
162
|
-
margin: 4mm 0;
|
|
163
|
-
font-size: 9.5pt;
|
|
164
|
-
}
|
|
165
|
-
thead th {
|
|
166
|
-
background: #1a5276;
|
|
167
|
-
color: white;
|
|
168
|
-
padding: 3mm;
|
|
169
|
-
text-align: left;
|
|
170
|
-
font-weight: bold;
|
|
171
|
-
}
|
|
172
|
-
tbody td {
|
|
173
|
-
padding: 2.5mm 3mm;
|
|
174
|
-
border-bottom: 0.5pt solid #bdc3c7;
|
|
175
|
-
}
|
|
176
|
-
tbody tr:nth-child(even) {
|
|
177
|
-
background: #f8f9fa;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/* Horizontal rule */
|
|
181
|
-
hr {
|
|
182
|
-
border: none;
|
|
183
|
-
border-top: 0.5pt solid #bdc3c7;
|
|
184
|
-
margin: 4mm 0;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/* Lists */
|
|
188
|
-
ul, ol {
|
|
189
|
-
margin: 2mm 0;
|
|
190
|
-
padding-left: 8mm;
|
|
191
|
-
}
|
|
192
|
-
li {
|
|
193
|
-
margin-bottom: 1mm;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/* Links */
|
|
197
|
-
a {
|
|
198
|
-
color: #2e86c1;
|
|
199
|
-
text-decoration: none;
|
|
200
|
-
}
|
|
201
|
-
"""
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def md_to_html(md_text, title="Deep Research Report", subtitle="Let's roll",
|
|
205
|
-
meta_line="", author="roll"):
|
|
206
|
-
"""Convert Markdown to HTML with cover page"""
|
|
207
|
-
|
|
208
|
-
# Convert body with markdown library
|
|
209
|
-
html_body = markdown.markdown(
|
|
210
|
-
md_text,
|
|
211
|
-
extensions=['tables', 'fenced_code', 'nl2br'],
|
|
212
|
-
output_format='html5'
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
# Extract first H1 for cover (remove from body)
|
|
216
|
-
first_h1_match = re.search(r'<h1>(.*?)</h1>', html_body)
|
|
217
|
-
if first_h1_match:
|
|
218
|
-
extracted_title = first_h1_match.group(1)
|
|
219
|
-
if not title or title == "Deep Research Report":
|
|
220
|
-
title = extracted_title
|
|
221
|
-
html_body = html_body.replace(first_h1_match.group(0), '', 1)
|
|
222
|
-
|
|
223
|
-
# Replace header placeholder in CSS
|
|
224
|
-
css = CSS_TEMPLATE.replace("HEADER_TEXT", f"{title} | Deep Research Report")
|
|
225
|
-
|
|
226
|
-
# Build cover page
|
|
227
|
-
cover_html = f"""
|
|
228
|
-
<div class="cover">
|
|
229
|
-
<h1 style="page-break-before: avoid; border: none;">{title}</h1>
|
|
230
|
-
<div class="subtitle">{subtitle}</div>
|
|
231
|
-
{"<div class='meta'>" + meta_line + "</div>" if meta_line else ""}
|
|
232
|
-
<hr class="divider">
|
|
233
|
-
<div class="meta">Author: {author}</div>
|
|
234
|
-
</div>
|
|
235
|
-
"""
|
|
236
|
-
|
|
237
|
-
full_html = f"""<!DOCTYPE html>
|
|
238
|
-
<html lang="en">
|
|
239
|
-
<head>
|
|
240
|
-
<meta charset="UTF-8">
|
|
241
|
-
<style>{css}</style>
|
|
242
|
-
</head>
|
|
243
|
-
<body>
|
|
244
|
-
{cover_html}
|
|
245
|
-
{html_body}
|
|
246
|
-
</body>
|
|
247
|
-
</html>"""
|
|
248
|
-
|
|
249
|
-
return full_html
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def main():
|
|
253
|
-
parser = argparse.ArgumentParser(description="Deep Research Report: Markdown to PDF")
|
|
254
|
-
parser.add_argument("input", help="Input Markdown file path")
|
|
255
|
-
parser.add_argument("output", help="Output PDF file path")
|
|
256
|
-
parser.add_argument("--title", default=None, help="Report title")
|
|
257
|
-
parser.add_argument("--author", default="roll", help="Author name")
|
|
258
|
-
parser.add_argument("--subtitle", default="Let's roll", help="Report subtitle")
|
|
259
|
-
args = parser.parse_args()
|
|
260
|
-
|
|
261
|
-
with open(args.input, "r", encoding="utf-8") as f:
|
|
262
|
-
md_text = f.read()
|
|
263
|
-
|
|
264
|
-
# Extract metadata line
|
|
265
|
-
meta_line = ""
|
|
266
|
-
for line in md_text.split("\n"):
|
|
267
|
-
stripped = line.strip().lstrip(">").strip()
|
|
268
|
-
if "research date" in stripped.lower() or "field:" in stripped.lower() or "subject type" in stripped.lower():
|
|
269
|
-
meta_line = stripped
|
|
270
|
-
break
|
|
271
|
-
|
|
272
|
-
html = md_to_html(md_text, title=args.title or "Deep Research Report",
|
|
273
|
-
subtitle=args.subtitle, meta_line=meta_line, author=args.author)
|
|
274
|
-
|
|
275
|
-
# Save intermediate HTML (for debugging)
|
|
276
|
-
html_path = args.output.replace('.pdf', '.html')
|
|
277
|
-
with open(html_path, 'w', encoding='utf-8') as f:
|
|
278
|
-
f.write(html)
|
|
279
|
-
print(f"[OK] HTML generated: {html_path}")
|
|
280
|
-
|
|
281
|
-
# Convert to PDF
|
|
282
|
-
from weasyprint import HTML
|
|
283
|
-
HTML(string=html).write_pdf(args.output)
|
|
284
|
-
size_kb = os.path.getsize(args.output) / 1024
|
|
285
|
-
print(f"[OK] PDF generated: {args.output} ({size_kb:.1f} KB)")
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if __name__ == "__main__":
|
|
289
|
-
main()
|