ostruct-cli 0.8.8__py3-none-any.whl → 1.0.0__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 (50) hide show
  1. ostruct/cli/__init__.py +3 -15
  2. ostruct/cli/attachment_processor.py +455 -0
  3. ostruct/cli/attachment_template_bridge.py +973 -0
  4. ostruct/cli/cli.py +187 -33
  5. ostruct/cli/click_options.py +775 -692
  6. ostruct/cli/code_interpreter.py +195 -12
  7. ostruct/cli/commands/__init__.py +0 -3
  8. ostruct/cli/commands/run.py +289 -62
  9. ostruct/cli/config.py +23 -22
  10. ostruct/cli/constants.py +89 -0
  11. ostruct/cli/errors.py +191 -6
  12. ostruct/cli/explicit_file_processor.py +0 -15
  13. ostruct/cli/file_info.py +118 -14
  14. ostruct/cli/file_list.py +82 -1
  15. ostruct/cli/file_search.py +68 -2
  16. ostruct/cli/help_json.py +235 -0
  17. ostruct/cli/mcp_integration.py +13 -16
  18. ostruct/cli/params.py +217 -0
  19. ostruct/cli/plan_assembly.py +335 -0
  20. ostruct/cli/plan_printing.py +385 -0
  21. ostruct/cli/progress_reporting.py +8 -56
  22. ostruct/cli/quick_ref_help.py +128 -0
  23. ostruct/cli/rich_config.py +299 -0
  24. ostruct/cli/runner.py +397 -190
  25. ostruct/cli/security/__init__.py +2 -0
  26. ostruct/cli/security/allowed_checker.py +41 -0
  27. ostruct/cli/security/normalization.py +13 -9
  28. ostruct/cli/security/security_manager.py +558 -17
  29. ostruct/cli/security/types.py +15 -0
  30. ostruct/cli/template_debug.py +283 -261
  31. ostruct/cli/template_debug_help.py +233 -142
  32. ostruct/cli/template_env.py +46 -5
  33. ostruct/cli/template_filters.py +415 -8
  34. ostruct/cli/template_processor.py +240 -619
  35. ostruct/cli/template_rendering.py +49 -73
  36. ostruct/cli/template_validation.py +2 -1
  37. ostruct/cli/token_validation.py +35 -15
  38. ostruct/cli/types.py +15 -19
  39. ostruct/cli/unicode_compat.py +283 -0
  40. ostruct/cli/upload_manager.py +448 -0
  41. ostruct/cli/utils.py +30 -0
  42. ostruct/cli/validators.py +272 -54
  43. {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/METADATA +292 -126
  44. ostruct_cli-1.0.0.dist-info/RECORD +80 -0
  45. ostruct/cli/commands/quick_ref.py +0 -54
  46. ostruct/cli/template_optimizer.py +0 -478
  47. ostruct_cli-0.8.8.dist-info/RECORD +0 -71
  48. {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/LICENSE +0 -0
  49. {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/WHEEL +0 -0
  50. {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,8 @@ We isolate this code here and provide proper type annotations for Click's
5
5
  decorator-based API.
6
6
  """
7
7
 
8
- from typing import Any, Callable, TypeVar, Union, cast
8
+ import logging
9
+ from typing import Any, Callable, List, TypeVar, Union, cast
9
10
 
10
11
  import click
11
12
  from click import Command
@@ -16,11 +17,10 @@ from ostruct.cli.errors import ( # noqa: F401 - Used in error handling
16
17
  SystemPromptError,
17
18
  TaskTemplateVariableError,
18
19
  )
19
- from ostruct.cli.validators import (
20
- validate_json_variable,
21
- validate_name_path_pair,
22
- validate_variable,
23
- )
20
+ from ostruct.cli.validators import validate_json_variable, validate_variable
21
+
22
+ from .constants import DefaultPaths
23
+ from .help_json import print_command_help_json as print_help_json
24
24
 
25
25
  P = ParamSpec("P")
26
26
  R = TypeVar("R")
@@ -28,6 +28,122 @@ F = TypeVar("F", bound=Callable[..., Any])
28
28
  CommandDecorator = Callable[[F], Command]
29
29
  DecoratedCommand = Union[Command, Callable[..., Any]]
30
30
 
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def _handle_help_debug(
35
+ ctx: click.Context, param: click.Parameter, value: bool
36
+ ) -> None:
37
+ """Handle --help-debug flag by showing debug help and exiting."""
38
+ if not value or ctx.resilient_parsing:
39
+ return
40
+
41
+ from .template_debug_help import show_template_debug_help
42
+
43
+ show_template_debug_help()
44
+ ctx.exit()
45
+
46
+
47
+ def get_available_models() -> List[str]:
48
+ """Get list of available models from registry that support structured output.
49
+
50
+ Returns:
51
+ Sorted list of model names that support structured output
52
+
53
+ Note:
54
+ Registry handles its own caching internally.
55
+ Falls back to basic model list if registry fails.
56
+ """
57
+ try:
58
+ from openai_model_registry import ModelRegistry
59
+
60
+ registry = ModelRegistry.get_instance()
61
+ all_models = list(registry.models)
62
+
63
+ # Filter to only models that support structured output
64
+ supported_models = []
65
+ for model in all_models:
66
+ try:
67
+ capabilities = registry.get_capabilities(model)
68
+ if getattr(capabilities, "supports_structured_output", True):
69
+ supported_models.append(model)
70
+ except Exception:
71
+ continue
72
+
73
+ return (
74
+ sorted(supported_models)
75
+ if supported_models
76
+ else _get_fallback_models()
77
+ )
78
+
79
+ except Exception as e:
80
+ logger.debug(f"Failed to load models from registry: {e}")
81
+ return _get_fallback_models()
82
+
83
+
84
+ def _get_fallback_models() -> List[str]:
85
+ """Fallback model list when registry is unavailable."""
86
+ return ["gpt-4o", "gpt-4o-mini", "o1", "o1-mini", "o3-mini"]
87
+
88
+
89
+ class ModelChoice(click.Choice):
90
+ """Custom Choice type with better error messages and help display for models."""
91
+
92
+ def convert(
93
+ self,
94
+ value: Any,
95
+ param: click.Parameter | None,
96
+ ctx: click.Context | None,
97
+ ) -> str:
98
+ try:
99
+ return super().convert(value, param, ctx)
100
+ except click.BadParameter:
101
+ choices_list = list(self.choices)
102
+ available = ", ".join(choices_list[:5])
103
+ more_count = len(choices_list) - 5
104
+ more_text = f" (and {more_count} more)" if more_count > 0 else ""
105
+
106
+ raise click.BadParameter(
107
+ f"Invalid model '{value}'. Available models: {available}{more_text}.\n"
108
+ f"Run 'ostruct list-models' to see all {len(choices_list)} available models."
109
+ )
110
+
111
+ def shell_complete(
112
+ self, ctx: click.Context, param: click.Parameter, incomplete: str
113
+ ) -> list:
114
+ """Provide shell completion for model names."""
115
+ from click.shell_completion import CompletionItem
116
+
117
+ return [
118
+ CompletionItem(choice)
119
+ for choice in self.choices
120
+ if choice.startswith(incomplete)
121
+ ]
122
+
123
+ def get_metavar(
124
+ self, param: click.Parameter, ctx: click.Context | None = None
125
+ ) -> str:
126
+ """Override metavar to show simple model info instead of complex list."""
127
+ choices_list = list(self.choices)
128
+
129
+ # Simple, clean display
130
+ return f"[{len(choices_list)} models available - run 'ostruct list-models' for full list]"
131
+
132
+
133
+ def create_model_choice() -> ModelChoice:
134
+ """Create a ModelChoice object for model selection with error handling."""
135
+ try:
136
+ models = get_available_models()
137
+ if not models:
138
+ raise ValueError("No models available")
139
+ return ModelChoice(models, case_sensitive=True)
140
+ except Exception as e:
141
+ logger.warning(f"Failed to load dynamic model list: {e}")
142
+ logger.warning("Falling back to basic model validation")
143
+
144
+ fallback_models = _get_fallback_models()
145
+ return ModelChoice(fallback_models, case_sensitive=True)
146
+
31
147
 
32
148
  def parse_feature_flags(
33
149
  enabled_features: tuple[str, ...], disabled_features: tuple[str, ...]
@@ -79,347 +195,113 @@ def parse_feature_flags(
79
195
  return parsed
80
196
 
81
197
 
82
- def debug_options(f: Union[Command, Callable[..., Any]]) -> Command:
83
- """Add debug-related CLI options."""
84
- # Initial conversion to Command if needed
85
- cmd: Any = f if isinstance(f, Command) else f
86
-
87
- # Add options without redundant casts
88
- cmd = click.option(
89
- "--show-model-schema",
90
- is_flag=True,
91
- help="Show generated Pydantic model schema",
92
- )(cmd)
198
+ # Helper functions moved to help_json.py for unified help system
93
199
 
94
- cmd = click.option(
95
- "--debug-validation",
96
- is_flag=True,
97
- help="Show detailed validation errors",
98
- )(cmd)
99
-
100
- cmd = click.option(
101
- "--debug",
102
- is_flag=True,
103
- help="🐛 Enable debug-level logging including template expansion",
104
- )(cmd)
105
-
106
- cmd = click.option(
107
- "--show-templates",
108
- is_flag=True,
109
- help="📝 Show expanded templates before sending to API",
110
- )(cmd)
111
-
112
- cmd = click.option(
113
- "--debug-templates",
114
- is_flag=True,
115
- help="🔍 Enable detailed template expansion debugging with step-by-step analysis",
116
- )(cmd)
117
-
118
- cmd = click.option(
119
- "--show-context",
120
- is_flag=True,
121
- help="📋 Show template variable context summary",
122
- )(cmd)
123
-
124
- cmd = click.option(
125
- "--show-context-detailed",
126
- is_flag=True,
127
- help="📋 Show detailed template variable context with content preview",
128
- )(cmd)
129
-
130
- cmd = click.option(
131
- "--show-pre-optimization",
132
- is_flag=True,
133
- help="🔧 Show template content before optimization is applied",
134
- )(cmd)
135
-
136
- cmd = click.option(
137
- "--show-optimization-diff",
138
- is_flag=True,
139
- help="🔄 Show template optimization changes (before/after comparison)",
140
- )(cmd)
141
-
142
- cmd = click.option(
143
- "--no-optimization",
144
- is_flag=True,
145
- help="⚡ Skip template optimization entirely for debugging",
146
- )(cmd)
147
-
148
- cmd = click.option(
149
- "--show-optimization-steps",
150
- is_flag=True,
151
- help="🔧 Show detailed optimization step tracking with before/after changes",
152
- )(cmd)
153
-
154
- cmd = click.option(
155
- "--optimization-step-detail",
156
- type=click.Choice(["summary", "detailed"]),
157
- default="summary",
158
- help="📊 Level of optimization step detail (summary shows overview, detailed shows full diffs)",
159
- )(cmd)
160
-
161
- cmd = click.option(
162
- "--help-debug",
163
- is_flag=True,
164
- help="📚 Show comprehensive template debugging help and examples",
165
- )(cmd)
166
-
167
- # Final cast to Command for return type
168
- return cast(Command, cmd)
169
200
 
170
-
171
- def file_options(f: Union[Command, Callable[..., Any]]) -> Command:
172
- """Add file-related CLI options."""
201
+ def debug_options(f: Union[Command, Callable[..., Any]]) -> Command:
202
+ """Add debug-related CLI options (now consolidated into debug_progress_options)."""
203
+ # All debug options have been moved to debug_progress_options for better grouping
204
+ # This function is kept for backward compatibility but does nothing
173
205
  cmd: Any = f if isinstance(f, Command) else f
174
-
175
- cmd = click.option(
176
- "-f",
177
- "--file",
178
- "files",
179
- multiple=True,
180
- nargs=2,
181
- metavar="<NAME> <PATH>",
182
- callback=validate_name_path_pair,
183
- help="""[LEGACY] Associate a file with a variable name for template access only.
184
- The file will be available in your template as the specified variable.
185
- For explicit tool routing, use -ft, -fc, or -fs instead.
186
- Example: -f code main.py -f test test_main.py""",
187
- shell_complete=click.Path(exists=True, file_okay=True, dir_okay=False),
188
- )(cmd)
189
-
190
- cmd = click.option(
191
- "-d",
192
- "--dir",
193
- "dir",
194
- multiple=True,
195
- nargs=2,
196
- metavar="<NAME> <DIR>",
197
- callback=validate_name_path_pair,
198
- help="""[LEGACY] Associate a directory with a variable name for template access only.
199
- All files in the directory will be available in your template. Use -R for recursive scanning.
200
- For explicit tool routing, use -dt, -dc, or -ds instead.
201
- Example: -d src ./src""",
202
- shell_complete=click.Path(exists=True, file_okay=False, dir_okay=True),
203
- )(cmd)
204
-
205
- # Template files with auto-naming ONLY (single argument)
206
- cmd = click.option(
207
- "-ft",
208
- "--file-for-template",
209
- "template_files",
210
- multiple=True,
211
- type=click.Path(exists=True, file_okay=True, dir_okay=False),
212
- help="""📄 [TEMPLATE] Files for template access only (auto-naming). These files will be available
213
- in your template but will not be uploaded to any tools. Use for configuration files,
214
- small data files, or any content you want to reference in templates.
215
- Format: -ft path (auto-generates variable name from filename).
216
- Access file content: {{ variable.content }} (not just {{ variable }})
217
- Example: -ft config.yaml → config_yaml variable, use {{ config_yaml.content }}""",
218
- shell_complete=click.Path(exists=True, file_okay=True, dir_okay=False),
219
- )(cmd)
220
-
221
- # Template files with two-argument alias syntax (explicit naming)
222
- cmd = click.option(
223
- "--fta",
224
- "--file-for-template-alias",
225
- "template_file_aliases",
226
- multiple=True,
227
- nargs=2,
228
- metavar="<NAME> <PATH>",
229
- callback=validate_name_path_pair,
230
- help="""📄 [TEMPLATE] Files for template with custom aliases. Use this for reusable
231
- templates where you need stable variable names independent of file paths.
232
- Format: --fta name path (supports tab completion for paths).
233
- Access file content: {{ name.content }} (not just {{ name }})
234
- Example: --fta config_data config.yaml → use {{ config_data.content }}""",
235
- shell_complete=click.Path(exists=True, file_okay=True, dir_okay=False),
236
- )(cmd)
237
-
238
- cmd = click.option(
239
- "-dt",
240
- "--dir-for-template",
241
- "template_dirs",
242
- multiple=True,
243
- type=click.Path(exists=True, file_okay=False, dir_okay=True),
244
- help="""📁 [TEMPLATE] Directories for template access only (auto-naming). All files will be available
245
- in your template but will not be uploaded to any tools. Use for project configurations,
246
- reference data, or any directory content you want accessible in templates.
247
- Format: -dt path (auto-generates variable name from directory name).
248
- Example: -dt ./config -dt ./data""",
249
- shell_complete=click.Path(exists=True, file_okay=False, dir_okay=True),
250
- )(cmd)
251
-
252
- # Template directories with two-argument alias syntax (explicit naming)
253
- cmd = click.option(
254
- "--dta",
255
- "--dir-for-template-alias",
256
- "template_dir_aliases",
257
- multiple=True,
258
- nargs=2,
259
- metavar="<NAME> <PATH>",
260
- callback=validate_name_path_pair,
261
- help="""📁 [TEMPLATE] Directories for template with custom aliases. Use this for reusable
262
- templates where you need stable variable names independent of directory paths.
263
- Format: --dta name path (supports tab completion for paths).
264
- Example: --dta config_data ./settings --dta source_code ./src""",
265
- shell_complete=click.Path(exists=True, file_okay=False, dir_okay=True),
266
- )(cmd)
267
-
268
- cmd = click.option(
269
- "--file-for",
270
- "tool_files",
271
- nargs=2,
272
- multiple=True,
273
- metavar="TOOL PATH",
274
- help="""🔄 [ADVANCED] Route files to specific tools. Use this for precise control
275
- over which tools receive which files. Supports tab completion for both tool names
276
- and file paths.
277
- Format: --file-for TOOL PATH
278
- Examples:
279
- --file-for code-interpreter analysis.py
280
- --file-for file-search docs.pdf
281
- --file-for template config.yaml""",
282
- )(cmd)
283
-
284
- cmd = click.option(
285
- "-p",
286
- "--pattern",
287
- "patterns",
288
- multiple=True,
289
- nargs=2,
290
- metavar="<NAME> <PATTERN>",
291
- help="""[LEGACY] Associate a glob pattern with a variable name. Matching files will be
292
- available in your template. Use -R for recursive matching.
293
- Example: -p logs '*.log'""",
294
- )(cmd)
295
-
296
- cmd = click.option(
297
- "-R",
298
- "--recursive",
299
- is_flag=True,
300
- help="Process directories and patterns recursively",
301
- )(cmd)
302
-
303
- cmd = click.option(
304
- "--base-dir",
305
- type=click.Path(exists=True, file_okay=False, dir_okay=True),
306
- help="""Base directory for resolving relative paths. All file operations will be
307
- relative to this directory. Defaults to current directory.""",
308
- shell_complete=click.Path(exists=True, file_okay=False, dir_okay=True),
309
- )(cmd)
310
-
311
- cmd = click.option(
312
- "-A",
313
- "--allow",
314
- "allowed_dirs",
315
- multiple=True,
316
- type=click.Path(exists=True, file_okay=False, dir_okay=True),
317
- help="""Add an allowed directory for security. Files must be within allowed
318
- directories. Can be specified multiple times.""",
319
- shell_complete=click.Path(exists=True, file_okay=False, dir_okay=True),
320
- )(cmd)
321
-
322
- cmd = click.option(
323
- "--allowed-dir-file",
324
- type=click.Path(exists=True, file_okay=True, dir_okay=False),
325
- help="""File containing allowed directory paths, one per line. Lines starting
326
- with # are treated as comments.""",
327
- shell_complete=click.Path(exists=True, file_okay=True, dir_okay=False),
328
- )(cmd)
329
-
330
206
  return cast(Command, cmd)
331
207
 
332
208
 
333
- def variable_options(f: Union[Command, Callable[..., Any]]) -> Command:
334
- """Add variable-related CLI options."""
335
- cmd: Any = f if isinstance(f, Command) else f
336
-
337
- cmd = click.option(
338
- "-V",
339
- "--var",
340
- "var",
341
- multiple=True,
342
- metavar="name=value",
343
- callback=validate_variable,
344
- help="""🏷️ [VARIABLES] Define a simple string variable for template substitution.
209
+ def variable_options(cmd: Callable[..., Any]) -> Callable[..., Any]:
210
+ """Add variable-related options to a command."""
211
+ # Apply options first (in reverse order since they stack)
212
+ for deco in (
213
+ click.option(
214
+ "-J",
215
+ "--json-var",
216
+ "json_var",
217
+ multiple=True,
218
+ metavar='name=\'{"json":"value"}\'',
219
+ callback=validate_json_variable,
220
+ help="""📋 [VARIABLES] Define a JSON variable for complex data structures.
221
+ JSON variables are parsed and available in templates as structured objects.
222
+ Format: name='{"key":"value"}'
223
+ Example: -J config='{"env":"prod","debug":true}'""",
224
+ ),
225
+ click.option(
226
+ "-V",
227
+ "--var",
228
+ "var",
229
+ multiple=True,
230
+ metavar="name=value",
231
+ callback=validate_variable,
232
+ help="""🏷️ [VARIABLES] Define a simple string variable for template substitution.
345
233
  Variables are available in your template as {{ variable_name }}.
346
234
  Format: name=value
347
235
  Example: -V debug=true -V env=prod""",
348
- )(cmd)
236
+ ),
237
+ ):
238
+ cmd = deco(cmd)
349
239
 
350
- cmd = click.option(
351
- "-J",
352
- "--json-var",
353
- "json_var",
354
- multiple=True,
355
- metavar='name=\'{"json":"value"}\'',
356
- callback=validate_json_variable,
357
- help="""📋 [VARIABLES] Define a JSON variable for complex data structures.
358
- JSON variables are parsed and available in templates as structured objects.
359
- Format: name='{"key":"value"}'
360
- Example: -J config='{"env":"prod","debug":true}'""",
361
- )(cmd)
362
-
363
- return cast(Command, cmd)
240
+ return cast(Callable[..., Any], cmd)
364
241
 
365
242
 
366
243
  def model_options(f: Union[Command, Callable[..., Any]]) -> Command:
367
244
  """Add model-related CLI options."""
368
245
  cmd: Any = f if isinstance(f, Command) else f
369
246
 
370
- cmd = click.option(
371
- "-m",
372
- "--model",
373
- default="gpt-4o",
374
- show_default=True,
375
- help="""OpenAI model to use. Must support structured output.
376
- Supported models:
377
- - gpt-4o (128k context window)
378
- - o1 (200k context window)
379
- - o3-mini (200k context window)""",
380
- )(cmd)
381
-
382
- cmd = click.option(
383
- "--temperature",
384
- type=click.FloatRange(0.0, 2.0),
385
- help="""Sampling temperature. Controls randomness in the output.
386
- Range: 0.0 to 2.0. Lower values are more focused.""",
387
- )(cmd)
388
-
389
- cmd = click.option(
390
- "--max-output-tokens",
391
- type=click.IntRange(1, None),
392
- help="""Maximum number of tokens in the output.
393
- Higher values allow longer responses but cost more.""",
394
- )(cmd)
395
-
396
- cmd = click.option(
397
- "--top-p",
398
- type=click.FloatRange(0.0, 1.0),
399
- help="""Top-p (nucleus) sampling parameter. Controls diversity.
400
- Range: 0.0 to 1.0. Lower values are more focused.""",
401
- )(cmd)
402
-
403
- cmd = click.option(
404
- "--frequency-penalty",
405
- type=click.FloatRange(-2.0, 2.0),
406
- help="""Frequency penalty for text generation.
407
- Range: -2.0 to 2.0. Positive values reduce repetition.""",
408
- )(cmd)
409
-
410
- cmd = click.option(
411
- "--presence-penalty",
412
- type=click.FloatRange(-2.0, 2.0),
413
- help="""Presence penalty for text generation.
414
- Range: -2.0 to 2.0. Positive values encourage new topics.""",
415
- )(cmd)
416
-
417
- cmd = click.option(
418
- "--reasoning-effort",
419
- type=click.Choice(["low", "medium", "high"]),
420
- help="""Control reasoning effort (if supported by model).
421
- Higher values may improve output quality but take longer.""",
422
- )(cmd)
247
+ # Create model choice with enhanced error handling
248
+ model_choice = create_model_choice()
249
+
250
+ # Ensure default is in the list
251
+ default_model = "gpt-4o"
252
+ choices_list = list(model_choice.choices)
253
+ if default_model not in choices_list and choices_list:
254
+ default_model = choices_list[0]
255
+
256
+ # Apply Model Configuration Options using click-option-group
257
+ # Apply options first (in reverse order since they stack)
258
+ for deco in (
259
+ click.option(
260
+ "--reasoning-effort",
261
+ type=click.Choice(["low", "medium", "high"]),
262
+ help="""Control reasoning effort (if supported by model).
263
+ Higher values may improve output quality but take longer.""",
264
+ ),
265
+ click.option(
266
+ "--presence-penalty",
267
+ type=click.FloatRange(-2.0, 2.0),
268
+ help="""Presence penalty for text generation.
269
+ Range: -2.0 to 2.0. Positive values encourage new topics.""",
270
+ ),
271
+ click.option(
272
+ "--frequency-penalty",
273
+ type=click.FloatRange(-2.0, 2.0),
274
+ help="""Frequency penalty for text generation.
275
+ Range: -2.0 to 2.0. Positive values reduce repetition.""",
276
+ ),
277
+ click.option(
278
+ "--top-p",
279
+ type=click.FloatRange(0.0, 1.0),
280
+ help="""Top-p (nucleus) sampling parameter. Controls diversity.
281
+ Range: 0.0 to 1.0. Lower values are more focused.""",
282
+ ),
283
+ click.option(
284
+ "--max-output-tokens",
285
+ type=click.IntRange(1, None),
286
+ help="""Maximum number of tokens in the output.
287
+ Higher values allow longer responses but cost more.""",
288
+ ),
289
+ click.option(
290
+ "--temperature",
291
+ type=click.FloatRange(0.0, 2.0),
292
+ help="""Sampling temperature. Controls randomness in the output.
293
+ Range: 0.0 to 2.0. Lower values are more focused.""",
294
+ ),
295
+ click.option(
296
+ "-m",
297
+ "--model",
298
+ type=model_choice,
299
+ default=default_model,
300
+ show_default=True,
301
+ help="OpenAI model to use. Must support structured output. Run 'ostruct list-models' for complete list.",
302
+ ),
303
+ ):
304
+ cmd = deco(cmd)
423
305
 
424
306
  return cast(Command, cmd)
425
307
 
@@ -428,28 +310,33 @@ def system_prompt_options(f: Union[Command, Callable[..., Any]]) -> Command:
428
310
  """Add system prompt related CLI options."""
429
311
  cmd: Any = f if isinstance(f, Command) else f
430
312
 
431
- cmd = click.option(
432
- "--sys-prompt",
433
- "system_prompt",
434
- help="""Provide system prompt directly. This sets the initial context
435
- for the model. Example: --sys-prompt "You are a code reviewer." """,
436
- )(cmd)
437
-
438
- cmd = click.option(
439
- "--sys-file",
440
- "system_prompt_file",
441
- type=click.Path(exists=True, dir_okay=False),
442
- help="""Load system prompt from file. The file should contain the prompt text.
313
+ # Apply System Prompt Options using click-option-group
314
+ # Apply options first (in reverse order since they stack)
315
+ for deco in (
316
+ click.option(
317
+ "--ignore-task-sysprompt",
318
+ is_flag=True,
319
+ help="""Ignore system prompt in task template. By default, system prompts
320
+ in template frontmatter are used.""",
321
+ ),
322
+ click.option(
323
+ "--sys-file",
324
+ "system_prompt_file",
325
+ type=click.Path(exists=True, dir_okay=False),
326
+ help="""Load system prompt from file. The file should contain the prompt text.
443
327
  Example: --sys-file prompts/code_review.txt""",
444
- shell_complete=click.Path(exists=True, file_okay=True, dir_okay=False),
445
- )(cmd)
446
-
447
- cmd = click.option(
448
- "--ignore-task-sysprompt",
449
- is_flag=True,
450
- help="""Ignore system prompt in task template. By default, system prompts
451
- in template frontmatter are used.""",
452
- )(cmd)
328
+ shell_complete=click.Path(
329
+ exists=True, file_okay=True, dir_okay=False
330
+ ),
331
+ ),
332
+ click.option(
333
+ "--sys-prompt",
334
+ "system_prompt",
335
+ help="""Provide system prompt directly. This sets the initial context
336
+ for the model. Example: --sys-prompt "You are a code reviewer.\"""",
337
+ ),
338
+ ):
339
+ cmd = deco(cmd)
453
340
 
454
341
  return cast(Command, cmd)
455
342
 
@@ -458,20 +345,36 @@ def output_options(f: Union[Command, Callable[..., Any]]) -> Command:
458
345
  """Add output-related CLI options."""
459
346
  cmd: Any = f if isinstance(f, Command) else f
460
347
 
461
- cmd = click.option(
462
- "--output-file",
463
- type=click.Path(dir_okay=False),
464
- help="""Write output to file instead of stdout.
465
- Example: --output-file result.json""",
466
- shell_complete=click.Path(file_okay=True, dir_okay=False),
467
- )(cmd)
468
-
469
- cmd = click.option(
470
- "--dry-run",
471
- is_flag=True,
472
- help="""Validate and render but skip API call. Useful for testing
473
- template rendering and validation.""",
474
- )(cmd)
348
+ # Apply Output and Execution Options using click-option-group
349
+ # Apply options first (in reverse order since they stack)
350
+ for deco in (
351
+ click.option(
352
+ "--run-summary-json",
353
+ is_flag=True,
354
+ help="""Output run summary as JSON to stderr (cannot be used with --dry-run).
355
+ Provides machine-readable execution summary after live runs.""",
356
+ ),
357
+ click.option(
358
+ "--dry-run-json",
359
+ is_flag=True,
360
+ help="""Output execution plan as JSON (requires --dry-run).
361
+ Outputs structured execution plan to stdout for programmatic consumption.""",
362
+ ),
363
+ click.option(
364
+ "--dry-run",
365
+ is_flag=True,
366
+ help="""Validate and render but skip API call. Useful for testing
367
+ template rendering and validation.""",
368
+ ),
369
+ click.option(
370
+ "--output-file",
371
+ type=click.Path(dir_okay=False),
372
+ help="""Write output to file instead of stdout.
373
+ Example: --output-file result.json""",
374
+ shell_complete=click.Path(file_okay=True, dir_okay=False),
375
+ ),
376
+ ):
377
+ cmd = deco(cmd)
475
378
 
476
379
  return cast(Command, cmd)
477
380
 
@@ -480,26 +383,28 @@ def api_options(f: Union[Command, Callable[..., Any]]) -> Command:
480
383
  """Add API-related CLI options."""
481
384
  cmd: Any = f if isinstance(f, Command) else f
482
385
 
483
- cmd = click.option(
484
- "--config",
485
- type=click.Path(exists=True),
486
- help="Configuration file path (default: ostruct.yaml)",
487
- )(cmd)
488
-
489
- cmd = click.option(
490
- "--api-key",
491
- help="""OpenAI API key. If not provided, uses OPENAI_API_KEY
492
- environment variable.""",
493
- )(cmd)
494
-
495
- # API timeout for OpenAI calls
496
- cmd = click.option(
497
- "--timeout",
498
- type=click.FloatRange(1.0, None),
499
- default=60.0,
500
- show_default=True,
501
- help="Timeout in seconds for OpenAI API calls.",
502
- )(cmd)
386
+ # Apply Configuration and API Options using click-option-group
387
+ # Apply options first (in reverse order since they stack)
388
+ for deco in (
389
+ click.option(
390
+ "--timeout",
391
+ type=click.FloatRange(1.0, None),
392
+ default=60.0,
393
+ show_default=True,
394
+ help="Timeout in seconds for OpenAI API calls.",
395
+ ),
396
+ click.option(
397
+ "--api-key",
398
+ help="""OpenAI API key. If not provided, uses OPENAI_API_KEY
399
+ environment variable.""",
400
+ ),
401
+ click.option(
402
+ "--config",
403
+ type=click.Path(exists=True),
404
+ help="Configuration file path (default: ostruct.yaml)",
405
+ ),
406
+ ):
407
+ cmd = deco(cmd)
503
408
 
504
409
  return cast(Command, cmd)
505
410
 
@@ -508,246 +413,159 @@ def mcp_options(f: Union[Command, Callable[..., Any]]) -> Command:
508
413
  """Add MCP (Model Context Protocol) server CLI options."""
509
414
  cmd: Any = f if isinstance(f, Command) else f
510
415
 
511
- cmd = click.option(
512
- "--mcp-server",
513
- "mcp_servers",
514
- multiple=True,
515
- help="""🔌 [MCP] Connect to Model Context Protocol server for extended capabilities.
416
+ # Apply MCP Server Configuration Options using click-option-group
417
+ # Apply options first (in reverse order since they stack)
418
+ for deco in (
419
+ click.option(
420
+ "--mcp-headers",
421
+ help="""JSON string of headers for MCP servers.
422
+ Example: --mcp-headers '{"Authorization": "Bearer token"}'""",
423
+ ),
424
+ click.option(
425
+ "--mcp-require-approval",
426
+ type=click.Choice(["always", "never"]),
427
+ default="never",
428
+ show_default=True,
429
+ help="""Approval level for MCP tool usage. CLI usage requires 'never'.""",
430
+ ),
431
+ click.option(
432
+ "--mcp-allowed-tools",
433
+ "mcp_allowed_tools",
434
+ multiple=True,
435
+ help="""Allowed tools per server. Format: server_label:tool1,tool2
436
+ Example: --mcp-allowed-tools deepwiki:search,summary""",
437
+ ),
438
+ click.option(
439
+ "--mcp-server",
440
+ "mcp_servers",
441
+ multiple=True,
442
+ help="""🔌 [MCP] Connect to Model Context Protocol server for extended capabilities.
516
443
  MCP servers provide additional tools like web search, databases, APIs, etc.
517
444
  Format: [label@]url
518
445
  Example: --mcp-server deepwiki@https://mcp.deepwiki.com/sse""",
519
- )(cmd)
520
-
521
- cmd = click.option(
522
- "--mcp-allowed-tools",
523
- "mcp_allowed_tools",
524
- multiple=True,
525
- help="""Allowed tools per server. Format: server_label:tool1,tool2
526
- Example: --mcp-allowed-tools deepwiki:search,summary""",
527
- )(cmd)
528
-
529
- cmd = click.option(
530
- "--mcp-require-approval",
531
- type=click.Choice(["always", "never"]),
532
- default="never",
533
- show_default=True,
534
- help="""Approval level for MCP tool usage. CLI usage requires 'never'.""",
535
- )(cmd)
536
-
537
- cmd = click.option(
538
- "--mcp-headers",
539
- help="""JSON string of headers for MCP servers.
540
- Example: --mcp-headers '{"Authorization": "Bearer token"}'""",
541
- )(cmd)
446
+ ),
447
+ ):
448
+ cmd = deco(cmd)
542
449
 
543
450
  return cast(Command, cmd)
544
451
 
545
452
 
546
- def code_interpreter_options(f: Union[Command, Callable[..., Any]]) -> Command:
547
- """Add Code Interpreter CLI options."""
453
+ def feature_options(f: Union[Command, Callable[..., Any]]) -> Command:
454
+ """Add feature flag and configuration options (without legacy file routing)."""
548
455
  cmd: Any = f if isinstance(f, Command) else f
549
456
 
550
- # Code interpreter files with auto-naming ONLY (single argument)
551
- cmd = click.option(
552
- "-fc",
553
- "--file-for-code-interpreter",
554
- "code_interpreter_files",
555
- multiple=True,
556
- type=click.Path(exists=True, file_okay=True, dir_okay=False),
557
- help="""💻 [CODE INTERPRETER] Files to upload for code execution and analysis (auto-naming).
558
- Perfect for data files (CSV, JSON), code files (Python, R), or any files that
559
- need computational processing. Files are uploaded to an isolated execution environment.
560
- Format: -fc path (auto-generates variable name from filename).
561
- Example: -fc data.csv → data_csv variable, -fc analysis.py → analysis_py variable""",
562
- shell_complete=click.Path(exists=True, file_okay=True, dir_okay=False),
563
- )(cmd)
564
-
565
- # Code interpreter files with two-argument alias syntax (explicit naming)
566
- cmd = click.option(
567
- "--fca",
568
- "--file-for-code-interpreter-alias",
569
- "code_interpreter_file_aliases",
570
- multiple=True,
571
- nargs=2,
572
- metavar="<NAME> <PATH>",
573
- callback=validate_name_path_pair,
574
- help="""💻 [CODE INTERPRETER] Files for code execution with custom aliases.
575
- Format: --fca name path (supports tab completion for paths).
576
- Example: --fca dataset src/data.csv --fca script analysis.py""",
577
- shell_complete=click.Path(exists=True, file_okay=True, dir_okay=False),
578
- )(cmd)
579
-
580
- cmd = click.option(
581
- "-dc",
582
- "--dir-for-code-interpreter",
583
- "code_interpreter_dirs",
584
- multiple=True,
585
- type=click.Path(exists=True, file_okay=False, dir_okay=True),
586
- help="""📂 [CODE INTERPRETER] Directories to upload for code execution (auto-naming). All files
587
- in the directory will be uploaded to the execution environment. Use for datasets,
588
- code repositories, or any directory that needs computational processing.
589
- Format: -dc path (auto-generates variable name from directory name).
590
- Example: -dc ./data -dc ./scripts""",
591
- shell_complete=click.Path(exists=True, file_okay=False, dir_okay=True),
592
- )(cmd)
593
-
594
- # Code interpreter directories with two-argument alias syntax (explicit naming)
595
- cmd = click.option(
596
- "--dca",
597
- "--dir-for-code-interpreter-alias",
598
- "code_interpreter_dir_aliases",
599
- multiple=True,
600
- nargs=2,
601
- metavar="<NAME> <PATH>",
602
- callback=validate_name_path_pair,
603
- help="""📂 [CODE INTERPRETER] Directories for code execution with custom aliases.
604
- Format: --dca name path (supports tab completion for paths).
605
- Example: --dca dataset ./data --dca source_code ./src""",
606
- shell_complete=click.Path(exists=True, file_okay=False, dir_okay=True),
607
- )(cmd)
608
-
609
- cmd = click.option(
610
- "--code-interpreter-download-dir",
611
- type=click.Path(file_okay=False, dir_okay=True),
612
- default="./downloads",
613
- show_default=True,
614
- help="""Directory to save files generated by Code Interpreter.
615
- Example: --code-interpreter-download-dir ./results""",
616
- shell_complete=click.Path(file_okay=False, dir_okay=True),
617
- )(cmd)
618
-
619
- cmd = click.option(
620
- "--code-interpreter-cleanup",
621
- is_flag=True,
622
- default=True,
623
- show_default=True,
624
- help="""Clean up uploaded files after execution to save storage quota.""",
625
- )(cmd)
626
-
627
- # Feature flags for experimental features
628
- cmd = click.option(
629
- "--enable-feature",
630
- "enabled_features",
631
- multiple=True,
632
- metavar="<FEATURE>",
633
- help="""🔧 [EXPERIMENTAL] Enable experimental features.
457
+ # Apply Code Interpreter Configuration Options using click-option-group
458
+ # Apply options first (in reverse order since they stack)
459
+ for deco in (
460
+ click.option(
461
+ "--ci-cleanup",
462
+ is_flag=True,
463
+ default=True,
464
+ show_default=True,
465
+ help="""🤖 [CODE INTERPRETER] Clean up uploaded files after execution to save storage quota.""",
466
+ ),
467
+ click.option(
468
+ "--ci-duplicate-outputs",
469
+ type=click.Choice(["overwrite", "rename", "skip"]),
470
+ default=None, # Will use config default or fallback to "overwrite"
471
+ show_default="overwrite",
472
+ help="""🤖 [CODE INTERPRETER] Handle duplicate output file names.
473
+ 'overwrite' replaces existing files (default),
474
+ 'rename' creates unique names (file_1.txt, file_2.txt),
475
+ 'skip' ignores files that already exist.
476
+ Example: --ci-duplicate-outputs rename""",
477
+ ),
478
+ click.option(
479
+ "--ci-download-dir",
480
+ type=click.Path(file_okay=False, dir_okay=True),
481
+ default=None, # Will use config default or fallback in runner.py
482
+ show_default=DefaultPaths.CODE_INTERPRETER_OUTPUT_DIR,
483
+ help="""🤖 [CODE INTERPRETER] Directory to save files generated by Code Interpreter.
484
+ Example: --ci-download-dir ./results""",
485
+ shell_complete=click.Path(file_okay=False, dir_okay=True),
486
+ ),
487
+ ):
488
+ cmd = deco(cmd)
489
+
490
+ # Apply the group decorator LAST so it sees all the options
491
+ cmd = cmd
492
+
493
+ # Apply Experimental Features Options using click-option-group
494
+ # Apply options first (in reverse order since they stack)
495
+ for deco in (
496
+ click.option(
497
+ "--disable-feature",
498
+ "disabled_features",
499
+ multiple=True,
500
+ metavar="<FEATURE>",
501
+ help="""🔧 [EXPERIMENTAL] Disable experimental features.
502
+ Available features:
503
+ • ci-download-hack - Force single-pass mode for Code Interpreter downloads.
504
+ Overrides config file setting.
505
+ Example: --disable-feature ci-download-hack""",
506
+ ),
507
+ click.option(
508
+ "--enable-feature",
509
+ "enabled_features",
510
+ multiple=True,
511
+ metavar="<FEATURE>",
512
+ help="""🔧 [EXPERIMENTAL] Enable experimental features.
634
513
  Available features:
635
514
  • ci-download-hack - Enable two-pass sentinel mode for reliable Code Interpreter
636
515
  file downloads with structured output. Overrides config file setting.
637
516
  Example: --enable-feature ci-download-hack""",
638
- )(cmd)
517
+ ),
518
+ ):
519
+ cmd = deco(cmd)
639
520
 
640
- cmd = click.option(
641
- "--disable-feature",
642
- "disabled_features",
643
- multiple=True,
644
- metavar="<FEATURE>",
645
- help="""🔧 [EXPERIMENTAL] Disable experimental features.
646
- Available features:
647
- • ci-download-hack - Force single-pass mode for Code Interpreter downloads.
648
- Overrides config file setting.
649
- Example: --disable-feature ci-download-hack""",
650
- )(cmd)
521
+ # Apply the group decorator LAST so it sees all the options
522
+ cmd = cmd
651
523
 
652
524
  return cast(Command, cmd)
653
525
 
654
526
 
655
- def file_search_options(f: Union[Command, Callable[..., Any]]) -> Command:
656
- """Add File Search CLI options."""
527
+ def file_search_config_options(
528
+ f: Union[Command, Callable[..., Any]],
529
+ ) -> Command:
530
+ """Add File Search configuration options (without legacy file routing)."""
657
531
  cmd: Any = f if isinstance(f, Command) else f
658
532
 
659
- # File search files with auto-naming ONLY (single argument)
660
- cmd = click.option(
661
- "-fs",
662
- "--file-for-search",
663
- "file_search_files",
664
- multiple=True,
665
- type=click.Path(exists=True, file_okay=True, dir_okay=False),
666
- help="""🔍 [FILE SEARCH] Files to upload for semantic vector search (auto-naming). Perfect for
667
- documents (PDF, TXT, MD), manuals, knowledge bases, or any text content you want to
668
- search through. Files are processed into a searchable vector store.
669
- Format: -fs path (auto-generates variable name from filename).
670
- Example: -fs docs.pdf → docs_pdf variable, -fs manual.txt → manual_txt variable""",
671
- shell_complete=click.Path(exists=True, file_okay=True, dir_okay=False),
672
- )(cmd)
673
-
674
- # File search files with two-argument alias syntax (explicit naming)
675
- cmd = click.option(
676
- "--fsa",
677
- "--file-for-search-alias",
678
- "file_search_file_aliases",
679
- multiple=True,
680
- nargs=2,
681
- metavar="<NAME> <PATH>",
682
- callback=validate_name_path_pair,
683
- help="""🔍 [FILE SEARCH] Files for search with custom aliases.
684
- Format: --fsa name path (supports tab completion for paths).
685
- Example: --fsa manual src/docs.pdf --fsa knowledge base.txt""",
686
- shell_complete=click.Path(exists=True, file_okay=True, dir_okay=False),
687
- )(cmd)
688
-
689
- cmd = click.option(
690
- "-ds",
691
- "--dir-for-search",
692
- "file_search_dirs",
693
- multiple=True,
694
- type=click.Path(exists=True, file_okay=False, dir_okay=True),
695
- help="""📁 [FILE SEARCH] Directories to upload for semantic search (auto-naming). All files in the
696
- directory will be processed into a searchable vector store. Use for documentation
697
- directories, knowledge bases, or any collection of searchable documents.
698
- Format: -ds path (auto-generates variable name from directory name).
699
- Example: -ds ./docs -ds ./manuals""",
700
- shell_complete=click.Path(exists=True, file_okay=False, dir_okay=True),
701
- )(cmd)
702
-
703
- # File search directories with two-argument alias syntax (explicit naming)
704
- cmd = click.option(
705
- "--dsa",
706
- "--dir-for-search-alias",
707
- "file_search_dir_aliases",
708
- multiple=True,
709
- nargs=2,
710
- metavar="<NAME> <PATH>",
711
- callback=validate_name_path_pair,
712
- help="""📁 [FILE SEARCH] Directories for search with custom aliases.
713
- Format: --dsa name path (supports tab completion for paths).
714
- Example: --dsa documentation ./docs --dsa knowledge_base ./manuals""",
715
- shell_complete=click.Path(exists=True, file_okay=False, dir_okay=True),
716
- )(cmd)
717
-
718
- cmd = click.option(
719
- "--file-search-vector-store-name",
720
- default="ostruct_search",
721
- show_default=True,
722
- help="""Name for the vector store created for File Search.
723
- Example: --file-search-vector-store-name project_docs""",
724
- )(cmd)
725
-
726
- cmd = click.option(
727
- "--file-search-cleanup",
728
- is_flag=True,
729
- default=True,
730
- show_default=True,
731
- help="""Clean up uploaded files and vector stores after execution.""",
732
- )(cmd)
733
-
734
- cmd = click.option(
735
- "--file-search-retry-count",
736
- type=click.IntRange(1, 10),
737
- default=3,
738
- show_default=True,
739
- help="""Number of retry attempts for File Search operations.
740
- Higher values improve reliability for intermittent failures.""",
741
- )(cmd)
742
-
743
- cmd = click.option(
744
- "--file-search-timeout",
745
- type=click.FloatRange(10.0, 300.0),
746
- default=60.0,
747
- show_default=True,
748
- help="""Timeout in seconds for vector store indexing.
749
- Typically instant but may take longer for large files.""",
750
- )(cmd)
533
+ # Apply File Search Configuration Options using click-option-group
534
+ # Apply options first (in reverse order since they stack)
535
+ for deco in (
536
+ click.option(
537
+ "--fs-timeout",
538
+ type=float,
539
+ default=60.0,
540
+ help="""📁 [FILE SEARCH] Timeout in seconds for vector store indexing operations.
541
+ Increase for large file uploads.""",
542
+ ),
543
+ click.option(
544
+ "--fs-retries",
545
+ type=int,
546
+ default=3,
547
+ help="""📁 [FILE SEARCH] Number of retry attempts for file search operations.
548
+ Increase for unreliable network connections.""",
549
+ ),
550
+ click.option(
551
+ "--fs-cleanup",
552
+ is_flag=True,
553
+ default=True,
554
+ help="""📁 [FILE SEARCH] Clean up uploaded files and vector stores after use.
555
+ Disable with --no-fs-cleanup to keep files for debugging.""",
556
+ ),
557
+ click.option(
558
+ "--fs-store-name",
559
+ type=str,
560
+ default="ostruct_search",
561
+ help="""📁 [FILE SEARCH] Name for the vector store used for file search.
562
+ Example: --fs-store-name project_docs""",
563
+ ),
564
+ ):
565
+ cmd = deco(cmd)
566
+
567
+ # Apply the group decorator LAST so it sees all the options
568
+ cmd = cmd
751
569
 
752
570
  return cast(Command, cmd)
753
571
 
@@ -756,51 +574,38 @@ def web_search_options(f: Union[Command, Callable[..., Any]]) -> Command:
756
574
  """Add Web Search CLI options."""
757
575
  cmd: Any = f if isinstance(f, Command) else f
758
576
 
759
- cmd = click.option(
760
- "--web-search",
761
- is_flag=True,
762
- help="""🌐 [WEB SEARCH] Enable OpenAI web search tool for up-to-date information.
763
- Allows the model to search the web for current events, recent updates, and real-time data.
764
- Note: Search queries may be sent to external services via OpenAI.
765
-
766
- ⚠️ DEPRECATED: Use --enable-tool web-search instead. Will be removed in v0.9.0.""",
767
- )(cmd)
768
-
769
- cmd = click.option(
770
- "--no-web-search",
771
- is_flag=True,
772
- help="""Explicitly disable web search even if enabled by default in configuration.
773
-
774
- ⚠️ DEPRECATED: Use --disable-tool web-search instead. Will be removed in v0.9.0.""",
775
- )(cmd)
776
-
777
- cmd = click.option(
778
- "--user-country",
779
- type=str,
780
- help="""🌐 [WEB SEARCH] Specify user country for geographically tailored search results.
781
- Used to improve search relevance by location (e.g., 'US', 'UK', 'Germany').""",
782
- )(cmd)
783
-
784
- cmd = click.option(
785
- "--user-city",
786
- type=str,
787
- help="""🌐 [WEB SEARCH] Specify user city for geographically tailored search results.
577
+ # Apply Web Search Configuration Options using click-option-group
578
+ # Apply options first (in reverse order since they stack)
579
+ for deco in (
580
+ click.option(
581
+ "--ws-context-size",
582
+ type=click.Choice(["low", "medium", "high"]),
583
+ help="""🌐 [WEB SEARCH] Control the amount of content retrieved from search results.
584
+ 'low' = brief snippets, 'medium' = balanced content, 'high' = comprehensive content.""",
585
+ ),
586
+ click.option(
587
+ "--ws-region",
588
+ type=str,
589
+ help="""🌐 [WEB SEARCH] Specify user region/state for geographically tailored search results.
590
+ Used to improve search relevance by location (e.g., 'California', 'Texas').""",
591
+ ),
592
+ click.option(
593
+ "--ws-city",
594
+ type=str,
595
+ help="""🌐 [WEB SEARCH] Specify user city for geographically tailored search results.
788
596
  Used to improve search relevance by location (e.g., 'San Francisco', 'London').""",
789
- )(cmd)
790
-
791
- cmd = click.option(
792
- "--user-region",
793
- type=str,
794
- help="""🌐 [WEB SEARCH] Specify user region/state for geographically tailored search results.
795
- Used to improve search relevance by location (e.g., 'California', 'Bavaria').""",
796
- )(cmd)
797
-
798
- cmd = click.option(
799
- "--search-context-size",
800
- type=click.Choice(["low", "medium", "high"]),
801
- help="""🌐 [WEB SEARCH] Control the amount of content retrieved from web pages.
802
- 'low' retrieves minimal content, 'high' retrieves comprehensive content. Default: medium.""",
803
- )(cmd)
597
+ ),
598
+ click.option(
599
+ "--ws-country",
600
+ type=str,
601
+ help="""🌐 [WEB SEARCH] Specify user country for geographically tailored search results.
602
+ Used to improve search relevance by location (e.g., 'US', 'UK', 'Germany').""",
603
+ ),
604
+ ):
605
+ cmd = deco(cmd)
606
+
607
+ # Apply the group decorator LAST so it sees all the options
608
+ cmd = cmd
804
609
 
805
610
  return cast(Command, cmd)
806
611
 
@@ -809,73 +614,351 @@ def tool_toggle_options(f: Union[Command, Callable[..., Any]]) -> Command:
809
614
  """Add universal tool toggle CLI options."""
810
615
  cmd: Any = f if isinstance(f, Command) else f
811
616
 
812
- cmd = click.option(
813
- "--enable-tool",
814
- "enabled_tools",
815
- multiple=True,
816
- metavar="<TOOL>",
817
- help="""🔧 [TOOL TOGGLES] Enable a tool for this run (repeatable).
617
+ # Apply Tool Integration Options using click-option-group
618
+ # Apply options first (in reverse order since they stack)
619
+ for deco in (
620
+ click.option(
621
+ "--disable-tool",
622
+ "disabled_tools",
623
+ multiple=True,
624
+ metavar="<TOOL>",
625
+ help="""🔧 [TOOL TOGGLES] Disable a tool for this run (repeatable).
626
+ Overrides configuration file and implicit activation.
627
+ Available tools: code-interpreter, file-search, web-search, mcp
628
+ Example: --disable-tool web-search --disable-tool mcp""",
629
+ ),
630
+ click.option(
631
+ "--enable-tool",
632
+ "enabled_tools",
633
+ multiple=True,
634
+ metavar="<TOOL>",
635
+ help="""🔧 [TOOL TOGGLES] Enable a tool for this run (repeatable).
818
636
  Overrides configuration file and implicit activation.
819
637
  Available tools: code-interpreter, file-search, web-search, mcp
820
638
  Example: --enable-tool code-interpreter --enable-tool web-search""",
821
- )(cmd)
639
+ ),
640
+ ):
641
+ cmd = deco(cmd)
822
642
 
823
- cmd = click.option(
824
- "--disable-tool",
825
- "disabled_tools",
826
- multiple=True,
827
- metavar="<TOOL>",
828
- help="""🔧 [TOOL TOGGLES] Disable a tool for this run (repeatable).
829
- Overrides configuration file and implicit activation.
830
- Available tools: code-interpreter, file-search, web-search, mcp
831
- Example: --disable-tool web-search --disable-tool mcp""",
832
- )(cmd)
643
+ # Apply the group decorator LAST so it sees all the options
644
+ cmd = cmd
833
645
 
834
646
  return cast(Command, cmd)
835
647
 
836
648
 
837
649
  def debug_progress_options(f: Union[Command, Callable[..., Any]]) -> Command:
838
650
  """Add debugging and progress CLI options."""
651
+ # Import the new infrastructure for template debug
652
+ from .template_debug import parse_td
653
+
839
654
  cmd: Any = f if isinstance(f, Command) else f
840
655
 
841
- cmd = click.option(
842
- "--no-progress", is_flag=True, help="Disable progress indicators"
843
- )(cmd)
656
+ # Apply Debug and Development Options using click-option-group
657
+ # Apply options first (in reverse order since they stack)
658
+ for deco in (
659
+ click.option(
660
+ "--help-debug",
661
+ is_flag=True,
662
+ is_eager=True,
663
+ expose_value=False,
664
+ callback=lambda ctx, param, value: _handle_help_debug(
665
+ ctx, param, value
666
+ ),
667
+ help="📚 Show comprehensive template debugging help and examples",
668
+ ),
669
+ click.option(
670
+ "--debug",
671
+ is_flag=True,
672
+ help="🐛 Enable debug-level logging including template expansion",
673
+ ),
674
+ click.option(
675
+ "--debug-validation",
676
+ is_flag=True,
677
+ help="Show detailed validation errors",
678
+ ),
679
+ click.option(
680
+ "--show-model-schema",
681
+ is_flag=True,
682
+ help="Show generated Pydantic model schema",
683
+ ),
684
+ click.option(
685
+ "-t",
686
+ "--template-debug",
687
+ metavar="CAPACITIES",
688
+ default=None,
689
+ is_flag=False,
690
+ flag_value="all",
691
+ expose_value=False,
692
+ callback=lambda ctx, p, v: (
693
+ ctx.obj.setdefault("_template_debug_caps", parse_td(v))
694
+ if ctx.obj is not None and v is not None
695
+ else None
696
+ ),
697
+ help="🔍 Debug prompt-template expansion. "
698
+ "Capacities: pre-expand,vars,preview,steps,post-expand "
699
+ "(comma list or 'all'). Use -t CAPACITIES or bare -t for all capacities.",
700
+ ),
701
+ click.option("--verbose", is_flag=True, help="Enable verbose logging"),
702
+ click.option(
703
+ "--progress",
704
+ type=click.Choice(["none", "basic", "detailed"]),
705
+ default="basic",
706
+ show_default=True,
707
+ help="""Control progress display. 'none' disables progress indicators,
708
+ 'basic' shows key steps, 'detailed' shows all operations.""",
709
+ ),
710
+ ):
711
+ cmd = deco(cmd)
712
+
713
+ # Apply the group decorator LAST so it sees all the options
714
+ cmd = cmd
715
+
716
+ return cast(Command, cmd)
717
+
718
+
719
+ def security_options(f: Union[Command, Callable[..., Any]]) -> Command:
720
+ """Add path security and allowlist CLI options."""
721
+ cmd: Any = f if isinstance(f, Command) else f
722
+
723
+ # Apply Security and Path Control Options using click-option-group
724
+ # Apply options first (in reverse order since they stack)
725
+ for deco in (
726
+ click.option(
727
+ "--allow-list",
728
+ "allow_list",
729
+ multiple=True,
730
+ type=click.Path(exists=True, dir_okay=False, resolve_path=True),
731
+ help="📋 Allow paths from file list for strict/warn mode (repeatable)",
732
+ ),
733
+ click.option(
734
+ "--allow-file",
735
+ "allow_file",
736
+ multiple=True,
737
+ type=click.Path(exists=True, dir_okay=False, resolve_path=True),
738
+ help="📄 Allow specific file for strict/warn mode (repeatable)",
739
+ ),
740
+ click.option(
741
+ "--allow",
742
+ "allow_dir",
743
+ multiple=True,
744
+ type=click.Path(exists=True, file_okay=False),
745
+ help="🗂️ Allow directory for strict/warn mode (repeatable)",
746
+ ),
747
+ click.option(
748
+ "-S",
749
+ "--path-security",
750
+ type=click.Choice(
751
+ ["permissive", "warn", "strict"], case_sensitive=False
752
+ ),
753
+ help="🔒 Path security mode: permissive (allow all), warn (log warnings), strict (allowlist only)",
754
+ ),
755
+ ):
756
+ cmd = deco(cmd)
757
+
758
+ # Apply the group decorator LAST so it sees all the options
759
+ cmd = cmd
760
+
761
+ return cast(Command, cmd)
844
762
 
845
- cmd = click.option(
846
- "--progress-level",
847
- type=click.Choice(["none", "basic", "detailed"]),
848
- default="basic",
849
- show_default=True,
850
- help="""Control progress verbosity. 'none' shows no progress,
851
- 'basic' shows key steps, 'detailed' shows all steps.""",
852
- )(cmd)
763
+
764
+ def help_options(f: Union[Command, Callable[..., Any]]) -> Command:
765
+ """Add help-related CLI options."""
766
+ cmd: Any = f if isinstance(f, Command) else f
853
767
 
854
768
  cmd = click.option(
855
- "--verbose", is_flag=True, help="Enable verbose logging"
769
+ "--help-json",
770
+ is_flag=True,
771
+ callback=print_help_json,
772
+ expose_value=False,
773
+ is_eager=True,
774
+ hidden=True, # Hide from help output - feature not ready for release
775
+ help="📖 Output command help in JSON format for programmatic consumption",
856
776
  )(cmd)
857
777
 
858
778
  return cast(Command, cmd)
859
779
 
860
780
 
781
+ def file_options(f: Union[Command, Callable[..., Any]]) -> Command:
782
+ """Add file attachment options with target/alias syntax."""
783
+
784
+ # Import validation functions here to avoid circular imports
785
+ def validate_attachment_file(
786
+ ctx: click.Context, param: click.Parameter, value: Any
787
+ ) -> Any:
788
+ from .params import normalise_targets, validate_attachment_alias
789
+
790
+ if not value:
791
+ return []
792
+
793
+ result = []
794
+ for spec, path in value:
795
+ # Parse spec part: [targets:]alias
796
+ if ":" in spec:
797
+ # Check for Windows drive letter false positive
798
+ if len(spec) == 2 and spec[1] == ":" and spec[0].isalpha():
799
+ prefix, alias = "prompt", spec
800
+ else:
801
+ prefix, alias = spec.split(":", 1)
802
+ else:
803
+ prefix, alias = "prompt", spec
804
+
805
+ # Normalize targets
806
+ try:
807
+ targets = normalise_targets(prefix)
808
+ except click.BadParameter as e:
809
+ raise click.BadParameter(
810
+ f"Invalid target(s) in '{prefix}' for {param.name}. {e}"
811
+ )
812
+
813
+ # Validate alias
814
+ try:
815
+ alias = validate_attachment_alias(alias)
816
+ except click.BadParameter as e:
817
+ raise click.BadParameter(
818
+ f"Invalid alias for {param.name}: {e}"
819
+ )
820
+
821
+ result.append(
822
+ {
823
+ "alias": alias,
824
+ "path": path,
825
+ "targets": targets,
826
+ "recursive": False,
827
+ "pattern": None,
828
+ }
829
+ )
830
+
831
+ return result
832
+
833
+ def validate_attachment_dir(
834
+ ctx: click.Context, param: click.Parameter, value: Any
835
+ ) -> Any:
836
+ return validate_attachment_file(ctx, param, value)
837
+
838
+ def validate_attachment_collect(
839
+ ctx: click.Context, param: click.Parameter, value: Any
840
+ ) -> Any:
841
+ if not value:
842
+ return []
843
+
844
+ result = []
845
+ for spec, path in value:
846
+ # Parse spec part: [targets:]alias
847
+ if ":" in spec:
848
+ if len(spec) == 2 and spec[1] == ":" and spec[0].isalpha():
849
+ prefix, alias = "prompt", spec
850
+ else:
851
+ prefix, alias = spec.split(":", 1)
852
+ else:
853
+ prefix, alias = "prompt", spec
854
+
855
+ # Handle collect @filelist syntax
856
+ processed_path = path
857
+ if path.startswith("@"):
858
+ filelist_path = path[1:] # Remove @
859
+ if not filelist_path:
860
+ raise click.BadParameter(
861
+ f"Filelist path cannot be empty after @ for {param.name}"
862
+ )
863
+ processed_path = ("@", filelist_path)
864
+
865
+ result.append(
866
+ {
867
+ "alias": alias,
868
+ "path": processed_path,
869
+ "targets": set([prefix.lower()]),
870
+ "recursive": False,
871
+ "pattern": None,
872
+ }
873
+ )
874
+
875
+ return result
876
+
877
+ # Apply File Attachment Options using click-option-group
878
+ # Fix: Attach options first, then wrap them in the group decorator last
879
+ cmd: Any = f if isinstance(f, Command) else f
880
+
881
+ # Attach options first (in reverse order since they stack)
882
+ for deco in (
883
+ click.option(
884
+ "--pattern",
885
+ metavar="GLOB",
886
+ help="Apply to last --dir/--collect (replaces legacy --glob)",
887
+ ),
888
+ click.option(
889
+ "--recursive",
890
+ is_flag=True,
891
+ help="Apply to last --dir/--collect",
892
+ ),
893
+ click.option(
894
+ "-C",
895
+ "--collect",
896
+ "collects",
897
+ multiple=True,
898
+ nargs=2,
899
+ callback=validate_attachment_collect,
900
+ metavar="[TARGETS:]ALIAS @FILELIST",
901
+ help="Attach file collection: '[targets:]alias @file-list.txt'",
902
+ ),
903
+ click.option(
904
+ "-D",
905
+ "--dir",
906
+ "dirs",
907
+ multiple=True,
908
+ nargs=2,
909
+ callback=validate_attachment_dir,
910
+ metavar="[TARGETS:]ALIAS PATH",
911
+ help="Attach directory: '[targets:]alias path'. Targets: prompt (default), code-interpreter/ci, file-search/fs",
912
+ ),
913
+ click.option(
914
+ "-F",
915
+ "--file",
916
+ "attaches",
917
+ multiple=True,
918
+ nargs=2,
919
+ callback=validate_attachment_file,
920
+ metavar="[TARGETS:]ALIAS PATH",
921
+ help="Attach file: '[targets:]alias path'. Targets: prompt (default), code-interpreter/ci, file-search/fs",
922
+ ),
923
+ ):
924
+ cmd = deco(cmd)
925
+
926
+ # Apply the group decorator LAST so it sees all the options
927
+ cmd = cmd
928
+
929
+ return cast(Command, cmd)
930
+
931
+
861
932
  def all_options(f: Union[Command, Callable[..., Any]]) -> Command:
862
- """Apply all CLI options to a command."""
933
+ """Apply all CLI options to a command in progressive disclosure order.
934
+
935
+ Order: Essential → Core Workflow → Advanced Features → Debug/Development
936
+ """
863
937
  cmd: Any = f if isinstance(f, Command) else f
864
938
 
865
- # Apply option groups in order
866
- cmd = file_options(cmd)
867
- cmd = variable_options(cmd)
868
- cmd = model_options(cmd)
869
- cmd = system_prompt_options(cmd)
870
- cmd = output_options(cmd)
871
- cmd = api_options(cmd)
872
- cmd = mcp_options(cmd)
873
- cmd = code_interpreter_options(cmd)
874
- cmd = file_search_options(cmd)
939
+ # Apply option groups in progressive disclosure order (REVERSE order since they stack)
940
+ # Debug and Development Options (last - most advanced)
941
+ cmd = help_options(cmd)
942
+ cmd = debug_progress_options(cmd)
943
+ cmd = debug_options(cmd)
944
+
945
+ # Advanced Configuration Options
946
+ cmd = security_options(cmd) # Path security and allowlist options
875
947
  cmd = web_search_options(cmd)
948
+ cmd = file_search_config_options(cmd) # File search config
949
+ cmd = feature_options(cmd) # Feature flags and config
950
+ cmd = mcp_options(cmd)
951
+ cmd = api_options(cmd)
952
+
953
+ # Core Workflow Options
954
+ cmd = output_options(cmd)
955
+ cmd = system_prompt_options(cmd)
876
956
  cmd = tool_toggle_options(cmd)
877
- cmd = debug_options(cmd)
878
- cmd = debug_progress_options(cmd)
957
+ cmd = file_options(cmd) # File attachment system
958
+ cmd = model_options(cmd)
959
+
960
+ # Essential Options (first - most important)
961
+ cmd = variable_options(cmd)
879
962
 
880
963
  return cast(Command, cmd)
881
964