@prateek_ai/agents-maker 1.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +659 -0
  3. package/agents/architect_agent.md +175 -0
  4. package/agents/code_agent.md +178 -0
  5. package/agents/compression_agent.md +226 -0
  6. package/agents/execution_agent.md +157 -0
  7. package/agents/orchestrator.md +406 -0
  8. package/agents/reviewer_agent.md +134 -0
  9. package/agents/ui_agent.md +147 -0
  10. package/agents/ux_agent.md +144 -0
  11. package/bin/cli.js +79 -0
  12. package/config/agents.yaml +388 -0
  13. package/config/domain_profiles.yaml +394 -0
  14. package/config/project.yaml.example +16 -0
  15. package/config/token_policies.yaml +325 -0
  16. package/context_loaders/__init__.py +15 -0
  17. package/context_loaders/__pycache__/__init__.cpython-314.pyc +0 -0
  18. package/context_loaders/__pycache__/file_chunker.cpython-314.pyc +0 -0
  19. package/context_loaders/__pycache__/project_summary.cpython-314.pyc +0 -0
  20. package/context_loaders/__pycache__/repo_tree.cpython-314.pyc +0 -0
  21. package/context_loaders/file_chunker.py +252 -0
  22. package/context_loaders/project_summary.py +369 -0
  23. package/context_loaders/repo_tree.py +203 -0
  24. package/package.json +46 -0
  25. package/quickstart.ps1 +252 -0
  26. package/quickstart.sh +232 -0
  27. package/requirements.txt +3 -0
  28. package/skills/analyze_repo.md +86 -0
  29. package/skills/animated_website.md +285 -0
  30. package/skills/compare_approaches.md +86 -0
  31. package/skills/define_data_schema.md +99 -0
  32. package/skills/design_api.md +126 -0
  33. package/skills/improve_copy.md +69 -0
  34. package/skills/review_code.md +83 -0
  35. package/skills/review_layout.md +89 -0
  36. package/skills/suggest_next.md +105 -0
  37. package/skills/summarize_history.md +89 -0
  38. package/skills/write_process_map.md +105 -0
  39. package/skills/write_tests.md +97 -0
  40. package/token_optimization/__pycache__/compressor.cpython-314.pyc +0 -0
  41. package/token_optimization/compressor.py +502 -0
  42. package/token_optimization/output_styles.md +80 -0
  43. package/tools/__pycache__/domain_utils.cpython-314.pyc +0 -0
  44. package/tools/__pycache__/generate_claude_md.cpython-314.pyc +0 -0
  45. package/tools/__pycache__/validate_kit.cpython-314.pyc +0 -0
  46. package/tools/domain_utils.py +141 -0
  47. package/tools/generate_claude_md.py +269 -0
  48. package/tools/generate_platform_configs.py +467 -0
  49. package/tools/generate_prompt.py +461 -0
  50. package/tools/init_project.py +454 -0
  51. package/tools/test_kit.py +363 -0
  52. package/tools/validate_kit.py +504 -0
@@ -0,0 +1,325 @@
1
+ # Token Policies
2
+ # Controls input compression and output verbosity per workflow.
3
+ #
4
+ # Fields per workflow policy:
5
+ # max_input_files - max number of file snippets in context
6
+ # max_input_tokens - soft limit (tokens); compression triggered above this
7
+ # history_summarize_after_turns - turn count that triggers history summarization
8
+ # output_style - default output verbosity preset (see output_styles section)
9
+ # relevance_drop_threshold - relevance score [0.0–1.0] below which files are excluded
10
+ # snippet_max_lines - lines per file before truncation is applied
11
+ # snippet_head_lines - lines kept from start of a truncated file
12
+ # snippet_tail_lines - lines kept from end of a truncated file
13
+
14
+ defaults: &defaults
15
+ max_input_files: 8
16
+ max_input_tokens: 24000
17
+ history_summarize_after_turns: 6
18
+ output_style: standard
19
+ relevance_drop_threshold: 0.35
20
+ snippet_max_lines: 200
21
+ snippet_head_lines: 40
22
+ snippet_tail_lines: 40
23
+
24
+ workflows:
25
+
26
+ code_review:
27
+ <<: *defaults
28
+ max_input_files: 10
29
+ max_input_tokens: 20000
30
+ output_style: review_checklist
31
+ relevance_drop_threshold: 0.40
32
+
33
+ feature_implementation:
34
+ <<: *defaults
35
+ max_input_files: 6
36
+ max_input_tokens: 18000
37
+ history_summarize_after_turns: 5
38
+ output_style: detailed_with_code
39
+
40
+ feature_design:
41
+ <<: *defaults
42
+ max_input_files: 5
43
+ max_input_tokens: 12000
44
+ output_style: design_brief
45
+
46
+ ui_improvement:
47
+ <<: *defaults
48
+ max_input_files: 4
49
+ max_input_tokens: 10000
50
+ output_style: design_brief
51
+ snippet_max_lines: 150
52
+ snippet_head_lines: 60 # keep more of component props + structure
53
+ snippet_tail_lines: 30
54
+
55
+ ux_critique:
56
+ <<: *defaults
57
+ max_input_files: 3
58
+ max_input_tokens: 8000
59
+ output_style: concise_bullets
60
+
61
+ refactoring:
62
+ <<: *defaults
63
+ max_input_files: 6
64
+ max_input_tokens: 16000
65
+ output_style: detailed_with_code
66
+ history_summarize_after_turns: 4
67
+
68
+ test_generation:
69
+ <<: *defaults
70
+ max_input_files: 5
71
+ max_input_tokens: 14000
72
+ output_style: detailed_with_code
73
+
74
+ generic_project_lifecycle:
75
+ <<: *defaults
76
+ max_input_files: 6
77
+ max_input_tokens: 16000
78
+ history_summarize_after_turns: 5
79
+ output_style: standard
80
+ run_compression_after_phase: true
81
+
82
+ phases:
83
+
84
+ task_framing:
85
+ max_input_files: 0
86
+ max_input_tokens: 4000
87
+ history_summarize_after_turns: 3
88
+ output_style: qa_brief
89
+ run_compression_after_phase: true
90
+
91
+ requirements:
92
+ max_input_files: 5
93
+ max_input_tokens: 14000
94
+ history_summarize_after_turns: 4
95
+ output_style: requirements_spec
96
+ run_compression_after_phase: true
97
+
98
+ solution_design:
99
+ max_input_files: 5
100
+ max_input_tokens: 12000
101
+ history_summarize_after_turns: 4
102
+ output_style: solution_design
103
+ run_compression_after_phase: true
104
+
105
+ implementation:
106
+ max_input_files: 6
107
+ max_input_tokens: 18000
108
+ history_summarize_after_turns: 5
109
+ output_style: implementation_slice
110
+ run_compression_after_phase: false
111
+
112
+ review_refinement:
113
+ max_input_files: 8
114
+ max_input_tokens: 20000
115
+ history_summarize_after_turns: 4
116
+ output_style: critique_summary
117
+ run_compression_after_phase: true
118
+
119
+ handoff:
120
+ max_input_files: 3
121
+ max_input_tokens: 8000
122
+ history_summarize_after_turns: 3
123
+ output_style: handoff_package
124
+ run_compression_after_phase: true
125
+
126
+ settings:
127
+ project_state_snapshot_after_phase: true
128
+ force_state_block_after_turns: 20
129
+ cross_session_resume_on_project_state: true
130
+
131
+ domains:
132
+
133
+ software:
134
+ implementation:
135
+ max_input_files: 8
136
+ max_input_tokens: 22000
137
+ review_refinement:
138
+ max_input_files: 10
139
+ output_style: critique_summary
140
+
141
+ content:
142
+ solution_design:
143
+ max_input_tokens: 8000
144
+ output_style: solution_design
145
+ implementation:
146
+ max_input_files: 3
147
+ max_input_tokens: 10000
148
+
149
+ research:
150
+ implementation:
151
+ max_input_files: 4
152
+ max_input_tokens: 12000
153
+ review_refinement:
154
+ max_input_tokens: 16000
155
+
156
+ data_analytics:
157
+ solution_design:
158
+ max_input_files: 6
159
+ implementation:
160
+ max_input_files: 8
161
+
162
+ product_design:
163
+ solution_design:
164
+ max_input_files: 4
165
+ max_input_tokens: 10000
166
+ output_style: solution_design
167
+ implementation:
168
+ max_input_files: 3
169
+ max_input_tokens: 10000
170
+ review_refinement:
171
+ max_input_files: 5
172
+ max_input_tokens: 14000
173
+
174
+ marketing:
175
+ solution_design:
176
+ max_input_tokens: 8000
177
+ implementation:
178
+ max_input_files: 3
179
+ max_input_tokens: 10000
180
+
181
+ ops_process:
182
+ solution_design:
183
+ max_input_files: 3
184
+ max_input_tokens: 8000
185
+ implementation:
186
+ max_input_files: 3
187
+ max_input_tokens: 10000
188
+ review_refinement:
189
+ max_input_files: 4
190
+ max_input_tokens: 12000
191
+
192
+ general:
193
+ task_framing:
194
+ max_input_tokens: 8000
195
+ history_summarize_after_turns: 10
196
+ requirements:
197
+ max_input_files: 4
198
+ max_input_tokens: 12000
199
+ solution_design:
200
+ max_input_files: 4
201
+ max_input_tokens: 12000
202
+ implementation:
203
+ max_input_files: 5
204
+ max_input_tokens: 20000
205
+ history_summarize_after_turns: 8
206
+ review_refinement:
207
+ max_input_files: 6
208
+ max_input_tokens: 18000
209
+
210
+ # Output verbosity styles
211
+ # These map to the full definitions in token_optimization/output_styles.md.
212
+ # Keys here must match style keys referenced in workflow policies and agent defaults.
213
+
214
+ output_styles:
215
+
216
+ concise_bullets:
217
+ description: Short bullet lists, no prose paragraphs. Max 300 words per response.
218
+ max_response_tokens: 600
219
+ format_hints:
220
+ - Use bullet lists for all findings.
221
+ - One sentence per bullet.
222
+ - No introductory or closing paragraphs.
223
+ - Omit explanations unless the reason is non-obvious.
224
+
225
+ standard:
226
+ description: Mix of prose and bullets. Balanced detail. Max 600 words per response.
227
+ max_response_tokens: 1200
228
+ format_hints:
229
+ - Use prose for context, bullets for lists of items.
230
+ - Include brief rationale for recommendations.
231
+ - One code snippet maximum unless more are essential.
232
+
233
+ detailed_with_code:
234
+ description: >
235
+ Full explanations with inline code patches or complete file sections.
236
+ Used for implementation tasks.
237
+ max_response_tokens: 3000
238
+ format_hints:
239
+ - Include full code blocks with language tags.
240
+ - Explain non-obvious decisions in 1–2 sentences.
241
+ - Use diff format (+ / -) when showing changes to existing code.
242
+ - Add a "What changed and why" summary at the end.
243
+
244
+ design_brief:
245
+ description: >
246
+ Structured design output: tables, bullet lists, short prose.
247
+ No inline code unless an interface or schema is being defined.
248
+ max_response_tokens: 1500
249
+ format_hints:
250
+ - Use tables for comparisons and API contracts.
251
+ - Use numbered lists for sequences and steps.
252
+ - Use bullet lists for options and trade-offs.
253
+ - Keep each section under 150 words.
254
+
255
+ review_checklist:
256
+ description: >
257
+ Tabular review output. Each finding has severity, location, and
258
+ recommendation. No prose paragraphs.
259
+ max_response_tokens: 1200
260
+ format_hints:
261
+ - Output a markdown table with columns: Severity | File:Line | Issue | Recommendation.
262
+ - Severity values: critical | high | medium | low | info.
263
+ - One row per finding.
264
+ - Add a one-line summary count at the top (e.g., "3 high, 2 medium, 1 low").
265
+
266
+ # --- Generic Project Lifecycle styles ---
267
+
268
+ qa_brief:
269
+ description: Numbered question list for task framing. Max 5 questions per turn, no prose.
270
+ max_response_tokens: 500
271
+ format_hints:
272
+ - Numbered list of questions only, no preamble.
273
+ - Each question is one sentence, max 20 words.
274
+ - After user answers, emit task_profile block and nothing else.
275
+
276
+ requirements_spec:
277
+ description: Structured tables and bullets covering goals, non-goals, deliverables, constraints, assumptions.
278
+ max_response_tokens: 1200
279
+ format_hints:
280
+ - Required sections: Goals, Non-Goals, Target Audience, Deliverables, Constraints, Assumptions.
281
+ - "Tag each constraint with its type: [technical], [time], [compliance], [resource], [tone]."
282
+ - No prose paragraphs longer than 2 sentences.
283
+
284
+ solution_design:
285
+ description: Structured design artifact with Context, Approach, Structure, Risks sections.
286
+ max_response_tokens: 2000
287
+ format_hints:
288
+ - Required sections: Context, Approach, Structure, Risks & Open Questions.
289
+ - Structure section varies by domain (see docs/domains.md).
290
+ - Tables for comparisons; numbered lists for sequences.
291
+ - No implementation code; schemas and interfaces acceptable.
292
+ - End with numbered Open Questions list.
293
+
294
+ implementation_slice:
295
+ description: Increment plan + work content + approval prompt. One increment at a time.
296
+ max_response_tokens: 2500
297
+ format_hints:
298
+ - Start with 3-line Increment Plan (this slice, depends on, next slice).
299
+ - Software uses diff format; content uses section heading + body + transition note.
300
+ - End with "Approve this increment / request changes / change direction?"
301
+
302
+ critique_summary:
303
+ description: Verdict + severity-rated findings table + positive findings + action prompt.
304
+ max_response_tokens: 1500
305
+ format_hints:
306
+ - Open with one-line verdict (ready_to_ship | minor_revisions | significant_revisions).
307
+ - Table columns: Severity | Area | Issue | Recommendation.
308
+ - Severity: critical, high, medium, low, info.
309
+ - Minimum 2 positive findings.
310
+ - End with "Apply all fixes / apply selected fixes / discuss?"
311
+
312
+ handoff_package:
313
+ description: Summary bullets + how-to steps + prioritized next steps. No prose paragraphs.
314
+ max_response_tokens: 1200
315
+ format_hints:
316
+ - Required sections: Summary (3-5 bullets), How to Use/Run/Deploy, What's Done, What's Next.
317
+ - What's Next must use P1/P2/P3 priority tiers.
318
+ - Domain-specific additions (env vars for software, key findings table for research, etc.).
319
+
320
+ # Compression triggers
321
+ # The Compression Agent is invoked automatically when:
322
+ compression_triggers:
323
+ context_token_ratio: 0.75 # context exceeds 75% of model's max input tokens
324
+ history_turns_exceeded: true # turns exceed history_summarize_after_turns for the workflow
325
+ explicit_user_request: true # user says "summarize", "compress", "reduce context"
@@ -0,0 +1,15 @@
1
+ """
2
+ context_loaders
3
+
4
+ Utilities for generating compact project context for agent sessions.
5
+
6
+ Scripts:
7
+ repo_tree.py - Walk repo and emit a filtered file tree with descriptions.
8
+ file_chunker.py - Extract and truncate key files for agent context.
9
+ project_summary.py - Produce a compact stack/services/entrypoints summary.
10
+
11
+ Typical session setup:
12
+ python context_loaders/project_summary.py --path /your/repo
13
+ python context_loaders/repo_tree.py --path /your/repo --filter src/ app/
14
+ (paste both outputs as the opening message in your agent session)
15
+ """
@@ -0,0 +1,252 @@
1
+ """
2
+ context_loaders/file_chunker.py
3
+
4
+ Extract, read, and optionally truncate specific files for inclusion in an
5
+ agent context block. Designed to be used after repo_tree.py identifies
6
+ the relevant files.
7
+
8
+ Usage:
9
+ # Read specific files
10
+ python context_loaders/file_chunker.py \\
11
+ --path /your/repo \\
12
+ --files services/user_service.py models/user.py tests/test_user_service.py
13
+
14
+ # Read all files under a directory, truncated
15
+ python context_loaders/file_chunker.py \\
16
+ --path /your/repo \\
17
+ --dirs services/ models/ \\
18
+ --max-lines 200
19
+
20
+ # Output to a file
21
+ python context_loaders/file_chunker.py \\
22
+ --path /your/repo \\
23
+ --files services/user_service.py \\
24
+ --output chunks.txt
25
+
26
+ Output format (paste directly into agent context):
27
+ ### services/user_service.py
28
+ ```python
29
+ <file content or truncated snippet>
30
+ ```
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ __version__ = "1.0.0"
36
+
37
+ import argparse
38
+ import sys
39
+ from pathlib import Path
40
+
41
+ # Default truncation settings (matches token_optimization defaults)
42
+ DEFAULT_MAX_LINES = 200
43
+ DEFAULT_HEAD_LINES = 40
44
+ DEFAULT_TAIL_LINES = 40
45
+
46
+ # Extensions that have a known language tag for fenced code blocks
47
+ LANG_MAP: dict[str, str] = {
48
+ ".py": "python",
49
+ ".ts": "typescript",
50
+ ".tsx": "tsx",
51
+ ".js": "javascript",
52
+ ".jsx": "jsx",
53
+ ".go": "go",
54
+ ".java": "java",
55
+ ".rb": "ruby",
56
+ ".rs": "rust",
57
+ ".cs": "csharp",
58
+ ".cpp": "cpp",
59
+ ".c": "c",
60
+ ".sql": "sql",
61
+ ".yaml": "yaml",
62
+ ".yml": "yaml",
63
+ ".toml": "toml",
64
+ ".json": "json",
65
+ ".md": "markdown",
66
+ ".sh": "bash",
67
+ ".bash": "bash",
68
+ ".graphql": "graphql",
69
+ ".gql": "graphql",
70
+ ".proto": "protobuf",
71
+ ".html": "html",
72
+ ".css": "css",
73
+ ".scss": "scss",
74
+ }
75
+
76
+
77
+ def _lang_tag(path: Path) -> str:
78
+ return LANG_MAP.get(path.suffix.lower(), "text")
79
+
80
+
81
+ def truncate_content(content: str, max_lines: int, head_lines: int, tail_lines: int) -> tuple[str, int]:
82
+ """
83
+ Truncate content to head + tail with a gap marker.
84
+ Returns (truncated_content, lines_omitted).
85
+ """
86
+ lines = content.splitlines()
87
+ if len(lines) <= max_lines:
88
+ return content, 0
89
+
90
+ head = lines[:head_lines]
91
+ tail = lines[-tail_lines:]
92
+ omitted = len(lines) - head_lines - tail_lines
93
+ gap = f"# ... [{omitted} lines omitted — request full file if needed] ..."
94
+ return "\n".join(head + [gap] + tail), omitted
95
+
96
+
97
+ def read_file(
98
+ file_path: Path,
99
+ max_lines: int = DEFAULT_MAX_LINES,
100
+ head_lines: int = DEFAULT_HEAD_LINES,
101
+ tail_lines: int = DEFAULT_TAIL_LINES,
102
+ no_truncate: bool = False,
103
+ ) -> dict:
104
+ """
105
+ Read a single file and return a dict with path, content, lang, and truncation info.
106
+ """
107
+ try:
108
+ content = file_path.read_text(encoding="utf-8", errors="replace")
109
+ except (OSError, PermissionError) as e:
110
+ return {
111
+ "path": str(file_path),
112
+ "content": f"# Error reading file: {e}",
113
+ "lang": "text",
114
+ "truncated": False,
115
+ "lines_omitted": 0,
116
+ "total_lines": 0,
117
+ }
118
+
119
+ total_lines = content.count("\n") + 1
120
+
121
+ if no_truncate:
122
+ truncated_content, omitted = content, 0
123
+ else:
124
+ truncated_content, omitted = truncate_content(content, max_lines, head_lines, tail_lines)
125
+
126
+ return {
127
+ "path": str(file_path),
128
+ "content": truncated_content,
129
+ "lang": _lang_tag(file_path),
130
+ "truncated": omitted > 0,
131
+ "lines_omitted": omitted,
132
+ "total_lines": total_lines,
133
+ }
134
+
135
+
136
+ def format_chunk(chunk: dict, root: Path | None = None) -> str:
137
+ """Format a single file chunk as a fenced code block for agent context."""
138
+ display_path = chunk["path"]
139
+ if root:
140
+ try:
141
+ display_path = str(Path(chunk["path"]).relative_to(root))
142
+ except ValueError:
143
+ pass
144
+
145
+ header = f"### {display_path}"
146
+ if chunk["truncated"]:
147
+ header += f" _(truncated: {chunk['lines_omitted']} lines omitted of {chunk['total_lines']} total)_"
148
+
149
+ return f"{header}\n```{chunk['lang']}\n{chunk['content']}\n```"
150
+
151
+
152
+ def collect_from_dirs(
153
+ root: Path,
154
+ dirs: list[str],
155
+ extensions: set[str] | None = None,
156
+ max_files: int = 20,
157
+ ) -> list[Path]:
158
+ """Collect file paths from specified subdirectories."""
159
+ if extensions is None:
160
+ extensions = set(LANG_MAP.keys())
161
+
162
+ paths: list[Path] = []
163
+ for d in dirs:
164
+ target = root / d.lstrip("/")
165
+ if not target.is_dir():
166
+ print(f"Warning: {target} is not a directory — skipping.", file=sys.stderr)
167
+ continue
168
+ for p in sorted(target.rglob("*")):
169
+ if p.is_file() and p.suffix in extensions:
170
+ paths.append(p)
171
+ if len(paths) >= max_files:
172
+ break
173
+ return paths[:max_files]
174
+
175
+
176
+ def main() -> None:
177
+ parser = argparse.ArgumentParser(
178
+ description="Extract and format file chunks for agent context."
179
+ )
180
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
181
+ parser.add_argument("--path", required=True, help="Repository root directory.")
182
+ parser.add_argument(
183
+ "--files", nargs="*", metavar="FILE",
184
+ help="Relative paths to specific files (from --path root)."
185
+ )
186
+ parser.add_argument(
187
+ "--dirs", nargs="*", metavar="DIR",
188
+ help="Subdirectories to collect all key files from."
189
+ )
190
+ parser.add_argument("--max-lines", type=int, default=DEFAULT_MAX_LINES,
191
+ help=f"Max lines before truncation (default: {DEFAULT_MAX_LINES}).")
192
+ parser.add_argument("--head-lines", type=int, default=DEFAULT_HEAD_LINES,
193
+ help=f"Lines kept from start on truncation (default: {DEFAULT_HEAD_LINES}).")
194
+ parser.add_argument("--tail-lines", type=int, default=DEFAULT_TAIL_LINES,
195
+ help=f"Lines kept from end on truncation (default: {DEFAULT_TAIL_LINES}).")
196
+ parser.add_argument("--no-truncate", action="store_true",
197
+ help="Disable truncation (caution: may produce very large output).")
198
+ parser.add_argument("--max-files", type=int, default=20,
199
+ help="Max files when using --dirs (default: 20).")
200
+ parser.add_argument("--output", help="Write output to this file instead of stdout.")
201
+ args = parser.parse_args()
202
+
203
+ root = Path(args.path).resolve()
204
+ if not root.is_dir():
205
+ print(f"Error: {root} is not a directory.", file=sys.stderr)
206
+ sys.exit(1)
207
+
208
+ file_paths: list[Path] = []
209
+
210
+ if args.files:
211
+ root_resolved = root.resolve()
212
+ for f in args.files:
213
+ p = (root / f).resolve()
214
+ if not str(p).startswith(str(root_resolved)):
215
+ print(f"[WARN] Skipping {f} — path escapes project root.", file=sys.stderr)
216
+ continue
217
+ if p.is_file():
218
+ file_paths.append(p)
219
+ else:
220
+ print(f"[WARN] {p} not found — skipping.", file=sys.stderr)
221
+
222
+ if args.dirs:
223
+ file_paths.extend(collect_from_dirs(root, args.dirs, max_files=args.max_files))
224
+
225
+ if not file_paths:
226
+ print("No files found. Provide --files or --dirs.", file=sys.stderr)
227
+ sys.exit(1)
228
+
229
+ chunks = [
230
+ read_file(
231
+ p,
232
+ max_lines=args.max_lines,
233
+ head_lines=args.head_lines,
234
+ tail_lines=args.tail_lines,
235
+ no_truncate=args.no_truncate,
236
+ )
237
+ for p in file_paths
238
+ ]
239
+
240
+ header = f"## Relevant Files ({len(chunks)} files)\n"
241
+ body = "\n\n".join(format_chunk(c, root=root) for c in chunks)
242
+ output = header + "\n" + body + "\n"
243
+
244
+ if args.output:
245
+ Path(args.output).write_text(output, encoding="utf-8")
246
+ print(f"Written to {args.output}")
247
+ else:
248
+ sys.stdout.buffer.write(output.encode("utf-8"))
249
+
250
+
251
+ if __name__ == "__main__":
252
+ main()