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,13 +5,243 @@ including proper logging configuration and template visibility features.
5
5
  """
6
6
 
7
7
  import logging
8
+ import os
8
9
  import time
9
10
  from dataclasses import dataclass
10
- from typing import Any, Dict, List, Optional
11
+ from enum import Enum
12
+ from typing import Any, Dict, List, Optional, Set
11
13
 
12
14
  import click
13
15
 
14
16
 
17
+ class TDCap(str, Enum):
18
+ """Template Debug Capacity enum for granular debugging control."""
19
+
20
+ PRE_EXPAND = "pre-expand"
21
+ VARS = "vars"
22
+ PREVIEW = "preview"
23
+ STEPS = "steps"
24
+ POST_EXPAND = "post-expand"
25
+
26
+
27
+ ALL_CAPS: Set[TDCap] = set(TDCap)
28
+
29
+ # Tag mapping for consistent output prefixes
30
+ TAG_MAP = {
31
+ TDCap.PRE_EXPAND: "PRE",
32
+ TDCap.VARS: "VARS",
33
+ TDCap.PREVIEW: "PREVIEW",
34
+ TDCap.STEPS: "STEP",
35
+ TDCap.POST_EXPAND: "TPL",
36
+ }
37
+
38
+
39
+ def parse_td(value: str | None) -> Set[TDCap]:
40
+ """Parse template debug capacity string into set of capacities.
41
+
42
+ Args:
43
+ value: Comma-separated capacity list or "all" or None
44
+
45
+ Returns:
46
+ Set of TDCap enum values
47
+
48
+ Raises:
49
+ click.BadParameter: If invalid capacity specified
50
+ """
51
+ if value is None or value.lower() == "all":
52
+ return ALL_CAPS
53
+
54
+ caps = set()
55
+ for tok in value.split(","):
56
+ tok = tok.strip()
57
+ try:
58
+ caps.add(TDCap(tok))
59
+ except ValueError:
60
+ valid_caps = ", ".join(c.value for c in TDCap)
61
+ raise click.BadParameter(
62
+ f"Unknown template-debug capacity '{tok}'. "
63
+ f"Valid: all, {valid_caps}"
64
+ )
65
+ return caps
66
+
67
+
68
+ def td_log(ctx: click.Context, cap: TDCap, msg: str) -> None:
69
+ """Log template debug message if capacity is enabled.
70
+
71
+ Args:
72
+ ctx: Click context containing template debug configuration
73
+ cap: Template debug capacity for this message
74
+ msg: Message to log
75
+ """
76
+ if not ctx.obj:
77
+ return
78
+
79
+ active: Set[TDCap] | None = ctx.obj.get("_template_debug_caps")
80
+ if active and cap in active:
81
+ tag = TAG_MAP[cap]
82
+ click.echo(f"[{tag}] {msg}", err=True)
83
+
84
+
85
+ MAX_PREVIEW = int(os.getenv("OSTRUCT_TEMPLATE_PREVIEW_LIMIT", "4096"))
86
+
87
+
88
+ def preview_snip(val: Any, max_size: int | None = None) -> str:
89
+ """Create size-aware preview of variable content.
90
+
91
+ Args:
92
+ val: Variable value to preview
93
+ max_size: Maximum preview size (default: OSTRUCT_TEMPLATE_PREVIEW_LIMIT)
94
+
95
+ Returns:
96
+ Preview string with truncation info if needed
97
+ """
98
+ if max_size is None:
99
+ max_size = MAX_PREVIEW
100
+
101
+ # Handle FileInfoList objects specially to avoid multi-file content access errors
102
+ from .file_list import FileInfoList
103
+
104
+ if isinstance(val, FileInfoList):
105
+ if len(val) == 0:
106
+ txt = "No files attached"
107
+ type_info = ""
108
+ elif len(val) == 1:
109
+ try:
110
+ txt = str(val.content)
111
+ type_info = f" ({type(val).__name__})"
112
+ except Exception:
113
+ txt = "1 file attached (content not accessible)"
114
+ type_info = ""
115
+ else:
116
+ txt = f"{len(val)} files attached (use indexing or loop to access individual files)"
117
+ type_info = ""
118
+ # Handle other objects with content property
119
+ elif hasattr(
120
+ val, "content"
121
+ ): # FileInfo objects and other content-bearing objects
122
+ try:
123
+ txt = str(val.content)
124
+ type_info = f" ({type(val).__name__})"
125
+ except ValueError:
126
+ # Handle other content access failures
127
+ txt = f"Content access failed for {type(val).__name__}"
128
+ type_info = ""
129
+ elif isinstance(val, (dict, list)):
130
+ import json
131
+
132
+ txt = json.dumps(val, indent=2)
133
+ type_info = f" ({type(val).__name__} with {len(val)} items)"
134
+ else:
135
+ txt = str(val)
136
+ type_info = f" ({type(val).__name__})"
137
+
138
+ if len(txt) > max_size:
139
+ return f"{txt[:max_size]}... [truncated {len(txt) - max_size} chars]{type_info}"
140
+ return f"{txt}{type_info}"
141
+
142
+
143
+ def get_active_capacities(ctx: click.Context | None = None) -> Set[TDCap]:
144
+ """Get currently active template debug capacities.
145
+
146
+ Args:
147
+ ctx: Click context (auto-detected if None)
148
+
149
+ Returns:
150
+ Set of active capacities, empty if none
151
+ """
152
+ if ctx is None:
153
+ try:
154
+ ctx = click.get_current_context()
155
+ except RuntimeError:
156
+ return set()
157
+
158
+ if not ctx.obj:
159
+ return set()
160
+
161
+ result = ctx.obj.get("_template_debug_caps", set())
162
+ return result if isinstance(result, set) else set()
163
+
164
+
165
+ def is_capacity_active(cap: TDCap, ctx: click.Context | None = None) -> bool:
166
+ """Check if specific template debug capacity is active.
167
+
168
+ Args:
169
+ cap: Capacity to check
170
+ ctx: Click context (auto-detected if None)
171
+
172
+ Returns:
173
+ True if capacity is active
174
+ """
175
+ active_caps = get_active_capacities(ctx)
176
+ return cap in active_caps
177
+
178
+
179
+ def td_log_if_active(
180
+ cap: TDCap, msg: str, ctx: click.Context | None = None
181
+ ) -> None:
182
+ """Log template debug message if capacity is active (convenience wrapper).
183
+
184
+ Args:
185
+ cap: Template debug capacity
186
+ msg: Message to log
187
+ ctx: Click context (auto-detected if None)
188
+ """
189
+ if ctx is None:
190
+ try:
191
+ ctx = click.get_current_context()
192
+ except RuntimeError:
193
+ return
194
+
195
+ td_log(ctx, cap, msg)
196
+
197
+
198
+ def td_log_vars(
199
+ variables: Dict[str, Any], ctx: click.Context | None = None
200
+ ) -> None:
201
+ """Log variable summary for VARS capacity.
202
+
203
+ Args:
204
+ variables: Dictionary of template variables
205
+ ctx: Click context (auto-detected if None)
206
+ """
207
+ if not is_capacity_active(TDCap.VARS, ctx):
208
+ return
209
+
210
+ for name, value in variables.items():
211
+ type_name = type(value).__name__
212
+ td_log_if_active(TDCap.VARS, f"{name} : {type_name}", ctx)
213
+
214
+
215
+ def td_log_preview(
216
+ variables: Dict[str, Any], ctx: click.Context | None = None
217
+ ) -> None:
218
+ """Log variable content preview for PREVIEW capacity.
219
+
220
+ Args:
221
+ variables: Dictionary of template variables
222
+ ctx: Click context (auto-detected if None)
223
+ """
224
+ if not is_capacity_active(TDCap.PREVIEW, ctx):
225
+ return
226
+
227
+ for name, value in variables.items():
228
+ preview = preview_snip(value)
229
+ td_log_if_active(TDCap.PREVIEW, f"{name} → {preview}", ctx)
230
+
231
+
232
+ def td_log_step(
233
+ step_num: int, description: str, ctx: click.Context | None = None
234
+ ) -> None:
235
+ """Log template expansion step for STEPS capacity.
236
+
237
+ Args:
238
+ step_num: Step number in expansion process
239
+ description: Description of what this step does
240
+ ctx: Click context (auto-detected if None)
241
+ """
242
+ td_log_if_active(TDCap.STEPS, f"Step {step_num}: {description}", ctx)
243
+
244
+
15
245
  def configure_debug_logging(
16
246
  verbose: bool = False, debug: bool = False
17
247
  ) -> None:
@@ -81,7 +311,6 @@ def log_template_expansion(
81
311
  def show_template_content(
82
312
  system_prompt: str,
83
313
  user_prompt: str,
84
- show_templates: bool = False,
85
314
  debug: bool = False,
86
315
  ) -> None:
87
316
  """Show template content with appropriate formatting.
@@ -89,12 +318,14 @@ def show_template_content(
89
318
  Args:
90
319
  system_prompt: System prompt content
91
320
  user_prompt: User prompt content
92
- show_templates: Show templates flag
93
321
  debug: Debug flag
94
322
  """
95
323
  logger = logging.getLogger(__name__)
96
324
 
97
- if show_templates or debug:
325
+ # Show if debug mode or if post-expand capacity is active
326
+ should_show = debug or is_capacity_active(TDCap.POST_EXPAND)
327
+
328
+ if should_show:
98
329
  # Use click.echo for immediate output that bypasses logging
99
330
  click.echo("📝 Template Content:", err=True)
100
331
  click.echo("=" * 50, err=True)
@@ -126,10 +357,54 @@ def show_file_content_expansions(context: Dict[str, Any]) -> None:
126
357
 
127
358
  logger.debug("📁 File Content Expansions:")
128
359
  for key, value in context.items():
129
- if hasattr(value, "content"): # FileInfo object
130
- logger.debug(
131
- f" → {key}: {getattr(value, 'path', 'unknown')} ({len(value.content)} chars)"
132
- )
360
+ # Check for specific file-related types more safely
361
+ from .attachment_template_bridge import LazyFileContent
362
+ from .file_info import FileInfo
363
+ from .file_list import FileInfoList
364
+
365
+ if isinstance(value, FileInfo):
366
+ try:
367
+ content_len = len(value.content) if value.content else 0
368
+ logger.debug(f" → {key}: {value.path} ({content_len} chars)")
369
+ except Exception as e:
370
+ logger.debug(
371
+ f" → {key}: FileInfo at {value.path} (content access failed: {e})"
372
+ )
373
+ elif isinstance(value, FileInfoList):
374
+ try:
375
+ file_count = len(value)
376
+ if file_count > 0:
377
+ # Try to get content length safely
378
+ try:
379
+ content_len = (
380
+ len(value.content) if value.content else 0
381
+ )
382
+ logger.debug(
383
+ f" → {key}: FileInfoList with {file_count} files ({content_len} chars)"
384
+ )
385
+ except ValueError:
386
+ logger.debug(
387
+ f" → {key}: FileInfoList with {file_count} files (empty)"
388
+ )
389
+ else:
390
+ logger.debug(f" → {key}: FileInfoList (empty)")
391
+ except Exception as e:
392
+ logger.debug(f" → {key}: FileInfoList (access failed: {e})")
393
+ elif isinstance(value, LazyFileContent):
394
+ try:
395
+ # Show user-friendly file information instead of class name
396
+ file_size = getattr(value, "actual_size", None) or 0
397
+ if file_size > 0:
398
+ size_str = f"{file_size:,} bytes"
399
+ else:
400
+ size_str = "unknown size"
401
+ logger.debug(
402
+ f" → {key}: file {value.name} ({size_str}) at {value.path}"
403
+ )
404
+ except Exception as e:
405
+ logger.debug(
406
+ f" → {key}: file {getattr(value, 'name', 'unknown')} (access failed: {e})"
407
+ )
133
408
  elif isinstance(value, str) and len(value) > 100:
134
409
  logger.debug(f" → {key}: {len(value)} chars")
135
410
  elif isinstance(value, dict):
@@ -467,75 +742,6 @@ def display_context_detailed(context: Dict[str, Any]) -> None:
467
742
  click.echo("=" * 50, err=True)
468
743
 
469
744
 
470
- def show_pre_optimization_template(template_content: str) -> None:
471
- """Display template content before optimization is applied.
472
-
473
- Args:
474
- template_content: Raw template content before optimization
475
- """
476
- click.echo("🔧 Template Before Optimization:", err=True)
477
- click.echo("=" * 50, err=True)
478
- click.echo(template_content, err=True)
479
- click.echo("=" * 50, err=True)
480
-
481
-
482
- def show_optimization_diff(original: str, optimized: str) -> None:
483
- """Show template optimization changes in a readable diff format.
484
-
485
- Args:
486
- original: Original template content
487
- optimized: Optimized template content
488
- """
489
- click.echo("🔄 Template Optimization Changes:", err=True)
490
- click.echo("=" * 50, err=True)
491
-
492
- # Simple line-by-line comparison
493
- original_lines = original.split("\n")
494
- optimized_lines = optimized.split("\n")
495
-
496
- # Show basic statistics
497
- click.echo(
498
- f"Original: {len(original_lines)} lines, {len(original)} chars",
499
- err=True,
500
- )
501
- click.echo(
502
- f"Optimized: {len(optimized_lines)} lines, {len(optimized)} chars",
503
- err=True,
504
- )
505
-
506
- if original == optimized:
507
- click.echo("✅ No optimization changes made", err=True)
508
- click.echo("=" * 50, err=True)
509
- return
510
-
511
- click.echo("\nChanges:", err=True)
512
-
513
- # Find differences line by line
514
- max_lines = max(len(original_lines), len(optimized_lines))
515
- changes_found = False
516
-
517
- for i in range(max_lines):
518
- orig_line = original_lines[i] if i < len(original_lines) else ""
519
- opt_line = optimized_lines[i] if i < len(optimized_lines) else ""
520
-
521
- if orig_line != opt_line:
522
- changes_found = True
523
- click.echo(f" Line {i + 1}:", err=True)
524
- if orig_line:
525
- click.echo(f" - {orig_line}", err=True)
526
- if opt_line:
527
- click.echo(f" + {opt_line}", err=True)
528
-
529
- if not changes_found:
530
- # If no line-by-line differences but content differs, show character-level diff
531
- click.echo(
532
- " Content differs but not at line level (whitespace/formatting changes)",
533
- err=True,
534
- )
535
-
536
- click.echo("=" * 50, err=True)
537
-
538
-
539
745
  def detect_undefined_variables(
540
746
  template_content: str, context: Dict[str, Any]
541
747
  ) -> List[str]:
@@ -562,187 +768,3 @@ def detect_undefined_variables(
562
768
  undefined_vars.append(var)
563
769
 
564
770
  return undefined_vars
565
-
566
-
567
- @dataclass
568
- class OptimizationStep:
569
- """Information about a single optimization step."""
570
-
571
- name: str
572
- before: str
573
- after: str
574
- reason: str
575
- timestamp: float
576
- chars_changed: int = 0
577
-
578
- def __post_init__(self) -> None:
579
- """Calculate character changes after initialization."""
580
- self.chars_changed = len(self.after) - len(self.before)
581
-
582
-
583
- class OptimizationStepTracker:
584
- """Tracker for detailed optimization step analysis."""
585
-
586
- def __init__(self, enabled: bool = False):
587
- """Initialize the optimization step tracker.
588
-
589
- Args:
590
- enabled: Whether step tracking is enabled
591
- """
592
- self.enabled = enabled
593
- self.steps: List[OptimizationStep] = []
594
-
595
- def log_step(
596
- self,
597
- step_name: str,
598
- before: str,
599
- after: str,
600
- reason: str,
601
- ) -> None:
602
- """Log an optimization step.
603
-
604
- Args:
605
- step_name: Name/description of the optimization step
606
- before: Content before this step
607
- after: Content after this step
608
- reason: Explanation of why this step was applied
609
- """
610
- if self.enabled:
611
- step = OptimizationStep(
612
- name=step_name,
613
- before=before,
614
- after=after,
615
- reason=reason,
616
- timestamp=time.time(),
617
- )
618
- self.steps.append(step)
619
-
620
- def show_step_summary(self) -> None:
621
- """Show a summary of optimization steps."""
622
- if not self.enabled or not self.steps:
623
- return
624
-
625
- click.echo("🔧 Optimization Steps Summary:", err=True)
626
- click.echo("=" * 50, err=True)
627
-
628
- total_chars_changed = 0
629
- for i, step in enumerate(self.steps, 1):
630
- total_chars_changed += step.chars_changed
631
- change_indicator = (
632
- "📈"
633
- if step.chars_changed > 0
634
- else "📉" if step.chars_changed < 0 else "📊"
635
- )
636
-
637
- click.echo(f" {i}. {step.name}: {step.reason}", err=True)
638
- if step.before != step.after:
639
- click.echo(
640
- f" {change_indicator} Changed: {len(step.before)} → {len(step.after)} chars ({step.chars_changed:+d})",
641
- err=True,
642
- )
643
- else:
644
- click.echo(" 📊 No changes made", err=True)
645
-
646
- click.echo(
647
- f"\n📊 Total: {total_chars_changed:+d} characters changed",
648
- err=True,
649
- )
650
- click.echo("=" * 50, err=True)
651
-
652
- def show_detailed_steps(self) -> None:
653
- """Show detailed information for each optimization step."""
654
- if not self.enabled or not self.steps:
655
- return
656
-
657
- click.echo("🔍 Detailed Optimization Steps:", err=True)
658
- click.echo("=" * 50, err=True)
659
-
660
- for i, step in enumerate(self.steps, 1):
661
- click.echo(f"\n--- Step {i}: {step.name} ---", err=True)
662
- click.echo(f"Reason: {step.reason}", err=True)
663
- click.echo(f"Character change: {step.chars_changed:+d}", err=True)
664
-
665
- if step.before != step.after:
666
- click.echo("Changes:", err=True)
667
- _show_step_diff(step.before, step.after)
668
- else:
669
- click.echo("✅ No changes made", err=True)
670
-
671
- click.echo("=" * 50, err=True)
672
-
673
- def get_step_stats(self) -> Dict[str, Any]:
674
- """Get statistics about optimization steps.
675
-
676
- Returns:
677
- Dictionary with step statistics
678
- """
679
- if not self.steps:
680
- return {}
681
-
682
- total_steps = len(self.steps)
683
- total_chars_changed = sum(step.chars_changed for step in self.steps)
684
- steps_with_changes = sum(
685
- 1 for step in self.steps if step.before != step.after
686
- )
687
-
688
- return {
689
- "total_steps": total_steps,
690
- "steps_with_changes": steps_with_changes,
691
- "total_chars_changed": total_chars_changed,
692
- "step_names": [step.name for step in self.steps],
693
- }
694
-
695
-
696
- def _show_step_diff(before: str, after: str) -> None:
697
- """Show a simple diff between before and after content.
698
-
699
- Args:
700
- before: Content before changes
701
- after: Content after changes
702
- """
703
- before_lines = before.split("\n")
704
- after_lines = after.split("\n")
705
-
706
- max_lines = max(len(before_lines), len(after_lines))
707
- changes_shown = 0
708
- max_changes = 5 # Limit output for readability
709
-
710
- for i in range(max_lines):
711
- if changes_shown >= max_changes:
712
- click.echo(
713
- f" ... ({max_lines - i} more lines not shown)", err=True
714
- )
715
- break
716
-
717
- before_line = before_lines[i] if i < len(before_lines) else ""
718
- after_line = after_lines[i] if i < len(after_lines) else ""
719
-
720
- if before_line != after_line:
721
- changes_shown += 1
722
- click.echo(f" Line {i + 1}:", err=True)
723
- if before_line:
724
- click.echo(f" - {before_line}", err=True)
725
- if after_line:
726
- click.echo(f" + {after_line}", err=True)
727
-
728
-
729
- def show_optimization_steps(
730
- steps: List[OptimizationStep], detail_level: str = "summary"
731
- ) -> None:
732
- """Show optimization steps with specified detail level.
733
-
734
- Args:
735
- steps: List of optimization steps
736
- detail_level: Level of detail ("summary" or "detailed")
737
- """
738
- if not steps:
739
- click.echo("ℹ️ No optimization steps were recorded", err=True)
740
- return
741
-
742
- tracker = OptimizationStepTracker(enabled=True)
743
- tracker.steps = steps
744
-
745
- if detail_level == "detailed":
746
- tracker.show_detailed_steps()
747
- else:
748
- tracker.show_step_summary()