onetool-mcp 1.0.0b1__py3-none-any.whl → 1.0.0rc2__py3-none-any.whl

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 (81) hide show
  1. onetool/cli.py +63 -4
  2. onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
  3. onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
  4. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
  5. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
  6. ot/__main__.py +6 -6
  7. ot/config/__init__.py +48 -46
  8. ot/config/global_templates/__init__.py +2 -2
  9. ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
  10. ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
  11. ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
  12. ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
  13. ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
  14. ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
  15. ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
  16. ot/config/global_templates/diagram.yaml +167 -0
  17. ot/config/global_templates/onetool.yaml +3 -1
  18. ot/config/{defaults → global_templates}/prompts.yaml +102 -97
  19. ot/config/global_templates/security.yaml +31 -0
  20. ot/config/global_templates/servers.yaml +93 -12
  21. ot/config/global_templates/snippets.yaml +5 -26
  22. ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
  23. ot/config/loader.py +221 -105
  24. ot/config/mcp.py +5 -1
  25. ot/config/secrets.py +192 -190
  26. ot/decorators.py +116 -116
  27. ot/executor/__init__.py +35 -35
  28. ot/executor/base.py +16 -16
  29. ot/executor/fence_processor.py +83 -83
  30. ot/executor/linter.py +142 -142
  31. ot/executor/pep723.py +288 -288
  32. ot/executor/runner.py +20 -6
  33. ot/executor/simple.py +163 -163
  34. ot/executor/validator.py +603 -164
  35. ot/http_client.py +145 -145
  36. ot/logging/__init__.py +37 -37
  37. ot/logging/entry.py +213 -213
  38. ot/logging/format.py +191 -188
  39. ot/logging/span.py +349 -349
  40. ot/meta.py +236 -14
  41. ot/paths.py +32 -49
  42. ot/prompts.py +218 -218
  43. ot/proxy/manager.py +14 -2
  44. ot/registry/__init__.py +189 -189
  45. ot/registry/parser.py +269 -269
  46. ot/server.py +330 -315
  47. ot/shortcuts/__init__.py +15 -15
  48. ot/shortcuts/aliases.py +87 -87
  49. ot/shortcuts/snippets.py +258 -258
  50. ot/stats/__init__.py +35 -35
  51. ot/stats/html.py +2 -2
  52. ot/stats/reader.py +354 -354
  53. ot/stats/timing.py +57 -57
  54. ot/support.py +63 -63
  55. ot/tools.py +1 -1
  56. ot/utils/batch.py +161 -161
  57. ot/utils/cache.py +120 -120
  58. ot/utils/exceptions.py +23 -23
  59. ot/utils/factory.py +178 -179
  60. ot/utils/format.py +65 -65
  61. ot/utils/http.py +202 -202
  62. ot/utils/platform.py +45 -45
  63. ot/utils/truncate.py +69 -69
  64. ot_tools/__init__.py +4 -4
  65. ot_tools/_convert/__init__.py +12 -12
  66. ot_tools/_convert/pdf.py +254 -254
  67. ot_tools/diagram.yaml +167 -167
  68. ot_tools/scaffold.py +2 -2
  69. ot_tools/transform.py +124 -19
  70. ot_tools/web_fetch.py +94 -43
  71. onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
  72. onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
  73. ot/config/defaults/bench.yaml +0 -4
  74. ot/config/defaults/onetool.yaml +0 -25
  75. ot/config/defaults/servers.yaml +0 -7
  76. ot/config/defaults/snippets.yaml +0 -4
  77. ot_tools/firecrawl.py +0 -732
  78. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
  79. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
  80. /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
  81. /ot/config/{defaults → global_templates}/tool_templates/isolated.py +0 -0
ot_tools/diagram.yaml CHANGED
@@ -1,167 +1,167 @@
1
- # Diagram tool configuration
2
- # Load via: include: [diagram.yaml] (falls back to bundled)
3
-
4
- tools:
5
- diagram:
6
- backend:
7
- type: kroki
8
- remote_url: https://kroki.io
9
- self_hosted_url: http://localhost:8000
10
- prefer: self_hosted # remote | self_hosted | auto
11
- timeout: 30.0
12
-
13
- policy:
14
- rules: |
15
- NEVER use ASCII art or text-based diagrams in markdown.
16
- Use the diagram tools for all visual representations.
17
- Save output as SVG and reference in markdown.
18
- Always generate source first, then render.
19
- Choose the provider based on diagram type - see instructions.
20
- preferred_format: svg
21
- preferred_providers:
22
- - mermaid
23
- - d2
24
- - plantuml
25
-
26
- output:
27
- dir: ../diagrams # Relative to config dir (.onetool/../diagrams)
28
- naming: "{provider}_{name}_{timestamp}"
29
- default_format: svg
30
- save_source: true
31
-
32
- instructions:
33
- mermaid:
34
- when_to_use: |
35
- - Flowcharts and decision trees
36
- - Sequence diagrams for API flows
37
- - Class diagrams for data models
38
- - State diagrams for workflows
39
- - Gantt charts for project timelines
40
- - Mindmaps for brainstorming
41
- style_tips: |
42
- Use subgraphs to group related nodes.
43
- Keep flowcharts top-to-bottom (TD) for readability.
44
- Limit sequence diagrams to 5-7 participants.
45
-
46
- QUOTING RULES (critical):
47
- - Sequence diagrams: NO quotes after 'as' (they appear literally)
48
- Use: participant WS as Web Server
49
- NOT: participant WS as "Web Server"
50
- - Flowcharts/class: USE quotes for labels with spaces
51
- Use: A["Start Process"], B{"Decision?"}
52
- - Never put spaces in node/participant IDs
53
- syntax_guide: https://mermaid.js.org/syntax/
54
- example: |
55
- sequenceDiagram
56
- participant C as Client
57
- participant S as Server
58
- C->>S: Request
59
- S-->>C: Response
60
-
61
- plantuml:
62
- when_to_use: |
63
- - Complex UML diagrams (class, component, deployment)
64
- - C4 architecture diagrams (use stdlib)
65
- - Detailed sequence diagrams with notes
66
- - Diagrams requiring themes and skinparams
67
- style_tips: |
68
- Use skinparam for consistent theming.
69
- Leverage !include for reusable components.
70
- Use packages to organise large diagrams.
71
- Add notes for context on complex relationships.
72
-
73
- QUOTING RULES (critical):
74
- - Always quote display names BEFORE 'as':
75
- participant "Web Server" as WS
76
- - Never put spaces in aliases (the ID after 'as')
77
- - Use stereotypes for interfaces: <<interface>>
78
- syntax_guide: https://plantuml.com/
79
- example: |
80
- @startuml
81
- actor User
82
- participant "API Gateway" as GW
83
- database DB
84
-
85
- User -> GW: Login request
86
- GW -> DB: Check credentials
87
- DB --> GW: User record
88
- GW --> User: 200 OK
89
- @enduml
90
-
91
- d2:
92
- when_to_use: |
93
- - Clean architecture diagrams
94
- - System context diagrams
95
- - Hand-drawn style (sketch mode)
96
- - Layouts with automatic positioning
97
- - C4-style container diagrams
98
- style_tips: |
99
- Use containers for logical grouping.
100
- D2 auto-layouts well - avoid manual positioning.
101
- Use markdown in labels for rich formatting.
102
- Leverage layers for complex diagrams.
103
- Use shape: person for actors.
104
- Direction hint: direction: right or direction: down.
105
-
106
- QUOTING RULES (critical):
107
- - Always quote labels after colon: node: "Display Name"
108
- - IDs (before colon) should not have spaces
109
- - Use style.sketch: true for hand-drawn look
110
- syntax_guide: https://d2lang.com/tour/intro
111
- example: |
112
- direction: right
113
-
114
- user: "User" {
115
- shape: person
116
- }
117
-
118
- system: "My System" {
119
- api: "API Server"
120
- db: "Database"
121
- }
122
-
123
- user -> system.api: "Uses"
124
- system.api -> system.db
125
-
126
- templates:
127
- api-flow:
128
- provider: mermaid
129
- diagram_type: sequence
130
- description: REST API request/response flow
131
- file: diagram-templates/api-flow.mmd
132
-
133
- microservices:
134
- provider: d2
135
- diagram_type: architecture
136
- description: Microservices architecture layout
137
- file: diagram-templates/microservices.d2
138
-
139
- c4-context:
140
- provider: plantuml
141
- diagram_type: c4
142
- description: C4 system context diagram
143
- file: diagram-templates/c4-context.puml
144
-
145
- state-machine:
146
- provider: mermaid
147
- diagram_type: state
148
- description: State machine diagram
149
- file: diagram-templates/state-machine.mmd
150
-
151
- class-diagram:
152
- provider: mermaid
153
- diagram_type: class
154
- description: Class/data model diagram
155
- file: diagram-templates/class-diagram.mmd
156
-
157
- project-gantt:
158
- provider: mermaid
159
- diagram_type: gantt
160
- description: Project timeline Gantt chart
161
- file: diagram-templates/project-gantt.mmd
162
-
163
- feature-mindmap:
164
- provider: mermaid
165
- diagram_type: mindmap
166
- description: Feature brainstorming mindmap
167
- file: diagram-templates/feature-mindmap.mmd
1
+ # Diagram tool configuration
2
+ # Load via: include: [config/diagram.yaml]
3
+
4
+ tools:
5
+ diagram:
6
+ backend:
7
+ type: kroki
8
+ remote_url: https://kroki.io
9
+ self_hosted_url: http://localhost:8000
10
+ prefer: self_hosted # remote | self_hosted | auto
11
+ timeout: 30.0
12
+
13
+ policy:
14
+ rules: |
15
+ NEVER use ASCII art or text-based diagrams in markdown.
16
+ Use the diagram tools for all visual representations.
17
+ Save output as SVG and reference in markdown.
18
+ Always generate source first, then render.
19
+ Choose the provider based on diagram type - see instructions.
20
+ preferred_format: svg
21
+ preferred_providers:
22
+ - mermaid
23
+ - d2
24
+ - plantuml
25
+
26
+ output:
27
+ dir: ../diagrams # Relative to config dir (.onetool/../diagrams)
28
+ naming: "{provider}_{name}_{timestamp}"
29
+ default_format: svg
30
+ save_source: true
31
+
32
+ instructions:
33
+ mermaid:
34
+ when_to_use: |
35
+ - Flowcharts and decision trees
36
+ - Sequence diagrams for API flows
37
+ - Class diagrams for data models
38
+ - State diagrams for workflows
39
+ - Gantt charts for project timelines
40
+ - Mindmaps for brainstorming
41
+ style_tips: |
42
+ Use subgraphs to group related nodes.
43
+ Keep flowcharts top-to-bottom (TD) for readability.
44
+ Limit sequence diagrams to 5-7 participants.
45
+
46
+ QUOTING RULES (critical):
47
+ - Sequence diagrams: NO quotes after 'as' (they appear literally)
48
+ Use: participant WS as Web Server
49
+ NOT: participant WS as "Web Server"
50
+ - Flowcharts/class: USE quotes for labels with spaces
51
+ Use: A["Start Process"], B{"Decision?"}
52
+ - Never put spaces in node/participant IDs
53
+ syntax_guide: https://mermaid.js.org/syntax/
54
+ example: |
55
+ sequenceDiagram
56
+ participant C as Client
57
+ participant S as Server
58
+ C->>S: Request
59
+ S-->>C: Response
60
+
61
+ plantuml:
62
+ when_to_use: |
63
+ - Complex UML diagrams (class, component, deployment)
64
+ - C4 architecture diagrams (use stdlib)
65
+ - Detailed sequence diagrams with notes
66
+ - Diagrams requiring themes and skinparams
67
+ style_tips: |
68
+ Use skinparam for consistent theming.
69
+ Leverage !include for reusable components.
70
+ Use packages to organise large diagrams.
71
+ Add notes for context on complex relationships.
72
+
73
+ QUOTING RULES (critical):
74
+ - Always quote display names BEFORE 'as':
75
+ participant "Web Server" as WS
76
+ - Never put spaces in aliases (the ID after 'as')
77
+ - Use stereotypes for interfaces: <<interface>>
78
+ syntax_guide: https://plantuml.com/
79
+ example: |
80
+ @startuml
81
+ actor User
82
+ participant "API Gateway" as GW
83
+ database DB
84
+
85
+ User -> GW: Login request
86
+ GW -> DB: Check credentials
87
+ DB --> GW: User record
88
+ GW --> User: 200 OK
89
+ @enduml
90
+
91
+ d2:
92
+ when_to_use: |
93
+ - Clean architecture diagrams
94
+ - System context diagrams
95
+ - Hand-drawn style (sketch mode)
96
+ - Layouts with automatic positioning
97
+ - C4-style container diagrams
98
+ style_tips: |
99
+ Use containers for logical grouping.
100
+ D2 auto-layouts well - avoid manual positioning.
101
+ Use markdown in labels for rich formatting.
102
+ Leverage layers for complex diagrams.
103
+ Use shape: person for actors.
104
+ Direction hint: direction: right or direction: down.
105
+
106
+ QUOTING RULES (critical):
107
+ - Always quote labels after colon: node: "Display Name"
108
+ - IDs (before colon) should not have spaces
109
+ - Use style.sketch: true for hand-drawn look
110
+ syntax_guide: https://d2lang.com/tour/intro
111
+ example: |
112
+ direction: right
113
+
114
+ user: "User" {
115
+ shape: person
116
+ }
117
+
118
+ system: "My System" {
119
+ api: "API Server"
120
+ db: "Database"
121
+ }
122
+
123
+ user -> system.api: "Uses"
124
+ system.api -> system.db
125
+
126
+ templates:
127
+ api-flow:
128
+ provider: mermaid
129
+ diagram_type: sequence
130
+ description: REST API request/response flow
131
+ file: config/diagram-templates/api-flow.mmd
132
+
133
+ microservices:
134
+ provider: d2
135
+ diagram_type: architecture
136
+ description: Microservices architecture layout
137
+ file: config/diagram-templates/microservices.d2
138
+
139
+ c4-context:
140
+ provider: plantuml
141
+ diagram_type: c4
142
+ description: C4 system context diagram
143
+ file: config/diagram-templates/c4-context.puml
144
+
145
+ state-machine:
146
+ provider: mermaid
147
+ diagram_type: state
148
+ description: State machine diagram
149
+ file: config/diagram-templates/state-machine.mmd
150
+
151
+ class-diagram:
152
+ provider: mermaid
153
+ diagram_type: class
154
+ description: Class/data model diagram
155
+ file: config/diagram-templates/class-diagram.mmd
156
+
157
+ project-gantt:
158
+ provider: mermaid
159
+ diagram_type: gantt
160
+ description: Project timeline Gantt chart
161
+ file: config/diagram-templates/project-gantt.mmd
162
+
163
+ feature-mindmap:
164
+ provider: mermaid
165
+ diagram_type: mindmap
166
+ description: Feature brainstorming mindmap
167
+ file: config/diagram-templates/feature-mindmap.mmd
ot_tools/scaffold.py CHANGED
@@ -24,9 +24,9 @@ __all__ = ["create", "extensions", "templates", "validate"]
24
24
 
25
25
  def _get_templates_dir() -> Path:
26
26
  """Get the extension templates directory."""
27
- from ot.paths import get_bundled_config_dir
27
+ from ot.paths import get_global_templates_dir
28
28
 
29
- return get_bundled_config_dir() / "tool_templates"
29
+ return get_global_templates_dir() / "tool_templates"
30
30
 
31
31
 
32
32
  def templates() -> str:
ot_tools/transform.py CHANGED
@@ -1,10 +1,10 @@
1
1
  """Transform - LLM-powered data transformation.
2
2
 
3
- Takes input data and a prompt, uses an LLM to transform/process it.
3
+ Takes data and a prompt, uses an LLM to transform/process it.
4
4
 
5
5
  Example:
6
6
  llm.transform(
7
- brave.search(query="metal prices", count=10),
7
+ data=brave.search(query="metal prices", count=10),
8
8
  prompt="Extract prices as YAML with fields: metal, price, unit, url",
9
9
  )
10
10
 
@@ -23,7 +23,7 @@ from __future__ import annotations
23
23
  # Pack for dot notation: llm.transform()
24
24
  pack = "llm"
25
25
 
26
- __all__ = ["transform"]
26
+ __all__ = ["transform", "transform_file"]
27
27
 
28
28
  # Dependency declarations for CLI validation
29
29
  __ot_requires__ = {
@@ -38,6 +38,7 @@ from pydantic import BaseModel, Field
38
38
 
39
39
  from ot.config import get_secret, get_tool_config
40
40
  from ot.logging import LogSpan
41
+ from ot.paths import resolve_cwd_path
41
42
 
42
43
 
43
44
  class Config(BaseModel):
@@ -82,19 +83,19 @@ def _get_api_config() -> tuple[str | None, str | None, str | None, Config]:
82
83
 
83
84
  def transform(
84
85
  *,
85
- input: Any,
86
+ data: Any,
86
87
  prompt: str,
87
88
  model: str | None = None,
88
89
  json_mode: bool = False,
89
90
  ) -> str:
90
- """Transform input data using an LLM.
91
+ """Transform data using an LLM.
91
92
 
92
- Takes any input data (typically a string result from another tool call)
93
+ Takes any data (typically a string result from another tool call)
93
94
  and processes it according to the prompt instructions.
94
95
 
95
96
  Args:
96
- input: Data to transform (will be converted to string if not already)
97
- prompt: Instructions for how to transform/process the input
97
+ data: Data to transform (will be converted to string if not already)
98
+ prompt: Instructions for how to transform/process the data
98
99
  model: AI model to use (uses transform.model from config if not specified)
99
100
  json_mode: If True, request JSON output format from the model
100
101
 
@@ -104,25 +105,25 @@ def transform(
104
105
  Examples:
105
106
  # Extract structured data from search results
106
107
  llm.transform(
107
- input=brave.search(query="gold price today", count=5),
108
+ data=brave.search(query="gold price today", count=5),
108
109
  prompt="Extract the current gold price in USD/oz as a single number",
109
110
  )
110
111
 
111
112
  # Convert to YAML format
112
113
  llm.transform(
113
- input=brave.search(query="metal prices", count=10),
114
+ data=brave.search(query="metal prices", count=10),
114
115
  prompt="Return ONLY valid YAML with fields: metal, price, unit, url",
115
116
  )
116
117
 
117
118
  # Summarize content
118
119
  llm.transform(
119
- input=some_long_text,
120
+ data=some_long_text,
120
121
  prompt="Summarize this in 3 bullet points"
121
122
  )
122
123
 
123
124
  # Get JSON output
124
125
  llm.transform(
125
- input=data,
126
+ data=my_data,
126
127
  prompt="Extract name and email as JSON",
127
128
  json_mode=True
128
129
  )
@@ -133,12 +134,12 @@ def transform(
133
134
  s.add(error="empty_prompt")
134
135
  return "Error: prompt is required and cannot be empty"
135
136
 
136
- input_str = str(input)
137
- if not input_str.strip():
138
- s.add(error="empty_input")
139
- return "Error: input is required and cannot be empty"
137
+ data_str = str(data)
138
+ if not data_str.strip():
139
+ s.add(error="empty_data")
140
+ return "Error: data is required and cannot be empty"
140
141
 
141
- s.add(inputLen=len(input_str))
142
+ s.add(dataLen=len(data_str))
142
143
 
143
144
  # Get API config
144
145
  api_key, base_url, default_model, config = _get_api_config()
@@ -158,8 +159,8 @@ def transform(
158
159
  client = OpenAI(api_key=api_key, base_url=base_url, timeout=config.timeout)
159
160
 
160
161
  # Build the message
161
- user_message = f"""Input data:
162
- {input_str}
162
+ user_message = f"""Data:
163
+ {data_str}
163
164
 
164
165
  Instructions:
165
166
  {prompt}"""
@@ -211,3 +212,107 @@ Instructions:
211
212
  error_msg = "Authentication error - check OPENAI_API_KEY in secrets.yaml"
212
213
  s.add(error=error_msg)
213
214
  return f"Error: {error_msg}"
215
+
216
+
217
+ def transform_file(
218
+ *,
219
+ prompt: str,
220
+ in_file: str,
221
+ out_file: str,
222
+ model: str | None = None,
223
+ json_mode: bool = False,
224
+ ) -> str:
225
+ """Transform a file's content using an LLM and write to output file.
226
+
227
+ Reads the input file, transforms its content according to the prompt,
228
+ and writes the result to the output file.
229
+
230
+ Args:
231
+ prompt: Instructions for how to transform/process the content
232
+ in_file: Path to input file (relative to cwd or absolute)
233
+ out_file: Path to output file (relative to cwd or absolute)
234
+ model: AI model to use (uses transform.model from config if not specified)
235
+ json_mode: If True, request JSON output format from the model
236
+
237
+ Returns:
238
+ Success message with bytes written, or error message
239
+
240
+ Examples:
241
+ # Convert markdown to restructured text
242
+ llm.transform_file(
243
+ prompt="Convert this markdown to reStructuredText format",
244
+ in_file="README.md",
245
+ out_file="README.rst",
246
+ )
247
+
248
+ # Extract data as JSON
249
+ llm.transform_file(
250
+ prompt="Extract all URLs and their descriptions as JSON",
251
+ in_file="links.txt",
252
+ out_file="links.json",
253
+ json_mode=True,
254
+ )
255
+
256
+ # Translate content
257
+ llm.transform_file(
258
+ prompt="Translate this to Spanish",
259
+ in_file="greeting.txt",
260
+ out_file="greeting_es.txt",
261
+ )
262
+ """
263
+ with LogSpan(
264
+ span="llm.transform_file", promptLen=len(prompt), inFile=in_file, outFile=out_file
265
+ ) as s:
266
+ # Validate prompt
267
+ if not prompt or not prompt.strip():
268
+ s.add(error="empty_prompt")
269
+ return "Error: prompt is required and cannot be empty"
270
+
271
+ # Resolve and read input file
272
+ try:
273
+ in_path = resolve_cwd_path(in_file)
274
+ if not in_path.exists():
275
+ s.add(error="in_file_not_found")
276
+ return f"Error: Input file not found: {in_file}"
277
+ if not in_path.is_file():
278
+ s.add(error="in_file_not_file")
279
+ return f"Error: Input path is not a file: {in_file}"
280
+ in_content = in_path.read_text(encoding="utf-8")
281
+ except UnicodeDecodeError as e:
282
+ s.add(error="in_file_decode_error")
283
+ return f"Error: Could not decode input file as UTF-8: {e}"
284
+ except OSError as e:
285
+ s.add(error=f"in_file_read_error: {e}")
286
+ return f"Error: Could not read input file: {e}"
287
+
288
+ if not in_content.strip():
289
+ s.add(error="empty_in_file")
290
+ return "Error: Input file is empty"
291
+
292
+ s.add(inLen=len(in_content))
293
+
294
+ # Transform the content
295
+ result = transform(
296
+ data=in_content,
297
+ prompt=prompt,
298
+ model=model,
299
+ json_mode=json_mode,
300
+ )
301
+
302
+ # Check if transform returned an error
303
+ if result.startswith("Error:"):
304
+ s.add(error="transform_failed")
305
+ return result
306
+
307
+ # Resolve and write output file
308
+ try:
309
+ out_path = resolve_cwd_path(out_file)
310
+ # Create parent directories if needed
311
+ out_path.parent.mkdir(parents=True, exist_ok=True)
312
+ out_path.write_text(result, encoding="utf-8")
313
+ bytes_written = len(result.encode("utf-8"))
314
+ s.add(outLen=bytes_written)
315
+ return f"OK: Transformed {in_file} -> {out_file} ({bytes_written} bytes)"
316
+ except OSError as e:
317
+ s.add(error=f"out_file_write_error: {e}")
318
+ return f"Error: Could not write output file: {e}"