scry-run 0.1.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.
scry_run/cli/init.py ADDED
@@ -0,0 +1,375 @@
1
+ """Init command for creating new scry-run projects."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import click
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.prompt import Prompt, Confirm
10
+
11
+ from scry_run.home import ensure_home_exists, get_app_dir
12
+ from scry_run.packages import ensure_scry_run_installed
13
+
14
+ console = Console()
15
+
16
+
17
+ EXPAND_PROMPT = '''You are helping design a CLI application. The user provided a brief description, and you need to expand it into a clear, detailed description that will guide code generation.
18
+
19
+ App name: {name}
20
+ User's description: {description}
21
+
22
+ Expand this into a clear app description that includes:
23
+
24
+ 1. **Core Purpose** (1-2 sentences) - What the app does at its heart
25
+
26
+ 2. **Key Features** (2-4 bullet points) - Features that directly support the core purpose:
27
+ - Focus on what the user actually asked for
28
+ - Only add features that are ESSENTIAL to the core purpose
29
+ - Do NOT invent extra features the user didn't mention
30
+ - Keep it minimal and focused
31
+
32
+ 3. **Usage Examples** (2-3 examples) - Show concrete CLI invocations like:
33
+ ```
34
+ {name} <command> [args]
35
+ ```
36
+ Keep examples simple and directly relevant to the core purpose.
37
+
38
+ IMPORTANT GUIDELINES:
39
+ - STICK TO WHAT THE USER ASKED FOR - don't add unrelated features
40
+ - Less is more - a focused app is better than a bloated one
41
+ - If the user said "todo list", make a todo list - not a project management suite
42
+ - Total length: 100-200 words (keep it concise!)
43
+
44
+ **OPEN WORLD PRINCIPLE**: This app uses scry-run for dynamic code generation. Methods are generated on-demand at runtime, meaning the set of commands is effectively unlimited - but inputs must still be STRUCTURED like a normal CLI:
45
+ - Use standard CLI patterns: commands, subcommands, flags, and positional arguments
46
+ - The vocabulary of commands/arguments is open-ended, but the SYNTAX is structured
47
+ - NOT a natural language interface - avoid examples like "how do I..." or question-form inputs
48
+ - Example for "hello" app:
49
+ - GOOD: `hello korean`, `hello --lang=french --formal`, `hello random`
50
+ - BAD: `hello how do you say goodbye in korean?` (natural language query)
51
+ - Example for "todo" app:
52
+ - GOOD: `todo add "buy milk"`, `todo list --due=today`, `todo done 3`
53
+ - BAD: `todo what's due today?` (question form)
54
+
55
+ Return ONLY the expanded description text, no JSON or markdown code blocks.'''
56
+
57
+
58
+ def expand_description(name: str, description: str) -> str | None:
59
+ """Use LLM to expand a brief description into a detailed one.
60
+
61
+ Args:
62
+ name: App name
63
+ description: User's brief description
64
+
65
+ Returns:
66
+ Expanded description, or None if expansion fails
67
+ """
68
+ try:
69
+ from scry_run.generator import CodeGenerator, ScryRunError
70
+
71
+ generator = CodeGenerator()
72
+ prompt = EXPAND_PROMPT.format(name=name, description=description)
73
+
74
+ # Use generate_freeform for text generation
75
+ expanded = generator.generate_freeform(prompt)
76
+ return expanded.strip()
77
+ except ScryRunError as e:
78
+ console.print(f"[yellow]Warning:[/yellow] Could not expand description: {e.message}")
79
+ return None
80
+ except Exception as e:
81
+ console.print(f"[yellow]Warning:[/yellow] Could not expand description: {e}")
82
+ return None
83
+
84
+
85
+ MAIN_TEMPLATE = '''#!/usr/bin/env python3
86
+ """
87
+ {description}
88
+
89
+ Generated by scry-run. Run with: scry-run run {name}
90
+ """
91
+
92
+ import sys
93
+
94
+ # =============================================================================
95
+ # scry-run: LLM-POWERED DYNAMIC CODE GENERATION
96
+ # =============================================================================
97
+ #
98
+ # This module uses automatic code generation. ANY METHOD CALLED WILL BE
99
+ # AUTO-GENERATED if it doesn't exist. Use this power!
100
+ #
101
+ # =============================================================================
102
+ # WHAT GETS AUTO-GENERATED
103
+ # =============================================================================
104
+ #
105
+ # 1. CONSTRUCTOR (__init__):
106
+ # - If you create an object with arguments, __init__ gets generated!
107
+ # - Example: app = MyClass("config.json", debug=True)
108
+ # - The LLM sees the args and generates appropriate initialization
109
+ #
110
+ # 2. INSTANCE METHODS (self.xxx):
111
+ # - Any method you call on an instance gets generated
112
+ # - Including _private_helpers - they work too!
113
+ #
114
+ # 3. CLASS METHODS (MyClass.xxx):
115
+ # - Class-level method calls also trigger generation
116
+ #
117
+ # =============================================================================
118
+ # CORE PRINCIPLES - READ CAREFULLY!
119
+ # =============================================================================
120
+ #
121
+ # 1. KEEP FUNCTIONS SHORT (5-15 lines max!)
122
+ # - Each function should do ONE thing
123
+ # - Long functions are WRONG - break them up
124
+ # - Call helper methods freely - they'll be generated!
125
+ #
126
+ # 2. USE DYNAMIC DISPATCH instead of conditionals/maps:
127
+ # BAD: if lang == "french": return "Bonjour"
128
+ # elif lang == "spanish": return "Hola"
129
+ # GOOD: return self._greet_in_{{lang}}() # Dynamic method call!
130
+ #
131
+ # BAD: handlers = {{"add": self.add, "remove": self.remove}}
132
+ # GOOD: return getattr(self, f"_handle_{{command}}")(*args)
133
+ #
134
+ # 3. DELEGATE EVERYTHING:
135
+ # - Don't implement logic inline - call a method for it
136
+ # - self._parse_args(args) instead of inline parsing
137
+ # - self._format_output(result) instead of inline formatting
138
+ # - self._validate_input(x) instead of inline checks
139
+ #
140
+ # 4. USE METAPROGRAMMING PATTERNS:
141
+ # - getattr(self, f"_do_{{action}}")() for dynamic dispatch
142
+ # - self._handle_{{category}}_{{item}}() for 2-level dispatch
143
+ # - Call methods that don't exist yet - they WILL be generated!
144
+ #
145
+ # 5. PREFER RECURSION & COMPOSITION:
146
+ # - Process items one at a time via self._process_single(item)
147
+ # - Compose results: self._combine(self._step1(), self._step2())
148
+ # - Chain transformations: self._step3(self._step2(self._step1(x)))
149
+ #
150
+ # 6. NEVER HARDCODE LISTS/MAPS:
151
+ # BAD: LANGUAGES = ["english", "french", "spanish"]
152
+ # GOOD: Call self._get_supported_languages() - generated dynamically!
153
+ #
154
+ # BAD: COMMANDS = {{"help": ..., "run": ...}}
155
+ # GOOD: Check hasattr(self, f"_cmd_{{name}}") or just call it!
156
+ #
157
+ # 7. ERROR HANDLING VIA METHODS:
158
+ # - self._handle_error(e) instead of inline handling
159
+ # - self._fallback_for_{{operation}}() for graceful degradation
160
+ #
161
+ # 8. HANDLING CLI ARGUMENTS & FLEXIBILITY:
162
+ # - DESIGN FOR UNKNOWN FLAGS: The user might add new flags!
163
+ # - Use argparse.ArgumentParser().parse_known_args()
164
+ # - Return tuple (known_args, unknown_args) to preserve unknown flags
165
+ # - FLEXIBLE SCOPE: If a flag/command is outside the description scope,
166
+ # or involves "unseen-as-of-yet" flags, HANDLE IT FLEXIBLY.
167
+ # - INFER INTENT: Use the flag name to guess the desired feature.
168
+ #
169
+ # =============================================================================
170
+ # EXAMPLE PATTERNS
171
+ # =============================================================================
172
+ #
173
+ # Creating MULTIPLE CLASSES dynamically:
174
+ # from scry_run import llm_create
175
+ # TodoItem = llm_create("TodoItem", "A single todo with title and done status")
176
+ # Database = llm_create("Database", "SQLite database wrapper")
177
+ #
178
+ # item = TodoItem("Buy milk")
179
+ # item.mark_done() # auto-generated!
180
+ #
181
+ # Constructor with args (auto-generated __init__):
182
+ # app = TodoList("my_tasks.db") # __init__ generated with db_path param!
183
+ #
184
+ # Dynamic language handler:
185
+ # def greet(self, lang): return getattr(self, f"_greet_{{lang}}")()
186
+ #
187
+ # Command dispatcher:
188
+ # def run(self, cmd, *args): return getattr(self, f"_cmd_{{cmd}}")(*args)
189
+ #
190
+ # Recursive processing:
191
+ # def process(self, items):
192
+ # if not items: return []
193
+ # return [self._process_one(items[0])] + self.process(items[1:])
194
+ #
195
+ # Chained transformation:
196
+ # def transform(self, x):
197
+ # return self._finalize(self._enhance(self._prepare(x)))
198
+ #
199
+ # =============================================================================
200
+ from scry_run import ScryClass, scry_create
201
+
202
+
203
+ class {class_name}(ScryClass):
204
+ """{description}
205
+
206
+ Methods are auto-generated. Keep them SHORT and MODULAR!
207
+ Use dynamic dispatch: getattr(self, f"_handle_{{x}}")()
208
+
209
+ To create additional classes, use llm_create:
210
+ OtherClass = llm_create("OtherClass", "Description here")
211
+ """
212
+ pass
213
+
214
+
215
+ if __name__ == "__main__":
216
+ app = {class_name}()
217
+ result = app.main(sys.argv[1:])
218
+ if result is not None:
219
+ print(result)
220
+ '''
221
+
222
+
223
+ def to_class_name(name: str) -> str:
224
+ """Convert a project name to a valid Python class name."""
225
+ # Remove special characters and convert to PascalCase
226
+ parts = name.replace("-", "_").replace(" ", "_").split("_")
227
+ return "".join(part.capitalize() for part in parts if part)
228
+
229
+
230
+ @click.command()
231
+ @click.option(
232
+ "--name", "-n",
233
+ help="App name (used for the directory)",
234
+ )
235
+ @click.option(
236
+ "--description", "-d",
237
+ help="App description",
238
+ )
239
+ @click.option(
240
+ "--auto-expand/--no-auto-expand",
241
+ default=True,
242
+ help="Automatically expand description with features and examples (default: enabled)",
243
+ )
244
+ def init(
245
+ name: str | None,
246
+ description: str | None,
247
+ auto_expand: bool,
248
+ ) -> None:
249
+ """Initialize a new scry-run app.
250
+
251
+ Creates an app in ~/.scry-run/apps/<name>/ with:
252
+ - app.py: The main executable
253
+ - cache.json: Empty cache file
254
+ - logs/: Directory for log files
255
+
256
+ By default, your brief description is expanded using AI to include
257
+ features and usage examples. Use --no-auto-expand to disable.
258
+
259
+ Examples:
260
+
261
+ # Interactive mode (recommended)
262
+ scry-run init
263
+
264
+ # Non-interactive with auto-expand
265
+ scry-run init --name=todoist --description='todo list app'
266
+
267
+ # Skip auto-expand
268
+ scry-run init --name=myapp --description='my app' --no-auto-expand
269
+ """
270
+ # Interactive mode if options not provided
271
+ if not name:
272
+ console.print("[bold blue]scry-run App Setup[/bold blue]\n")
273
+ name = Prompt.ask("App name", default="myapp")
274
+
275
+ if not description:
276
+ description = Prompt.ask(
277
+ "App description (brief is fine - will be expanded)",
278
+ default=f"A {name} application"
279
+ )
280
+
281
+ # Auto-expand description using LLM
282
+ if auto_expand:
283
+ console.print()
284
+ console.print("[dim]Expanding description with AI...[/dim]")
285
+ expanded = expand_description(name, description)
286
+
287
+ if expanded:
288
+ # Show the expanded description
289
+ console.print()
290
+ panel = Panel(
291
+ expanded,
292
+ title="[bold]Expanded Description[/bold]",
293
+ border_style="blue",
294
+ )
295
+ console.print(panel)
296
+ console.print()
297
+
298
+ # Let user confirm, edit, or reject
299
+ console.print("[dim](y)es, (n)o, or (e)dit in $EDITOR[/dim]")
300
+ choice = Prompt.ask(
301
+ "Use this description?",
302
+ choices=["y", "n", "e"],
303
+ default="y",
304
+ )
305
+ if choice == "y":
306
+ description = expanded
307
+ elif choice == "e":
308
+ # Open in editor
309
+ edited = click.edit(expanded)
310
+ if edited:
311
+ description = edited.strip()
312
+ console.print("[dim]Using edited description.[/dim]")
313
+ else:
314
+ console.print("[dim]Editor returned empty, using original.[/dim]")
315
+ else:
316
+ console.print("[dim]Using original description.[/dim]")
317
+ else:
318
+ console.print("[dim]Using original description.[/dim]")
319
+
320
+ # Validate name
321
+ if not name.replace("_", "").replace("-", "").isalnum():
322
+ console.print("[red]Error:[/red] App name must be alphanumeric (with - or _)")
323
+ raise SystemExit(1)
324
+
325
+ # Ensure home directory exists
326
+ ensure_home_exists()
327
+
328
+ # Get app directory
329
+ app_dir = get_app_dir(name)
330
+
331
+ # Check for existing app
332
+ if app_dir.exists():
333
+ if not Confirm.ask(f"[yellow]App '{name}' already exists[/yellow]. Overwrite?"):
334
+ console.print("[yellow]Aborted.[/yellow]")
335
+ raise SystemExit(0)
336
+
337
+ # Create app directory
338
+ app_dir.mkdir(parents=True, exist_ok=True)
339
+
340
+ # Create virtual environment and install scry-run
341
+ ensure_scry_run_installed(app_dir)
342
+
343
+ class_name = to_class_name(name)
344
+
345
+ # Create app.py
346
+ app_file = app_dir / "app.py"
347
+ app_content = MAIN_TEMPLATE.format(
348
+ name=name,
349
+ description=description,
350
+ class_name=class_name,
351
+ )
352
+ app_file.write_text(app_content)
353
+ app_file.chmod(app_file.stat().st_mode | 0o111) # Make executable
354
+
355
+ # Create empty cache.json
356
+ cache_file = app_dir / "cache.json"
357
+ cache_file.write_text(json.dumps({}))
358
+
359
+ # Create logs directory
360
+ logs_dir = app_dir / "logs"
361
+ logs_dir.mkdir(exist_ok=True)
362
+
363
+ console.print()
364
+ console.print("[bold green]App created successfully![/bold green]")
365
+ console.print()
366
+ console.print(f" [dim]Location:[/dim] {app_dir}")
367
+ console.print(f" [dim]Main file:[/dim] {app_file}")
368
+ console.print()
369
+ console.print("[bold]Useful commands:[/bold]")
370
+ console.print(f" [cyan]scry-run run {name}[/cyan] Run your app")
371
+ console.print(f" [cyan]scry-run info {name}[/cyan] View app details and cache stats")
372
+ console.print(f" [cyan]scry-run cache list {name}[/cyan] List generated methods")
373
+ console.print(f" [cyan]scry-run bake {name}[/cyan] Export as standalone package")
374
+ console.print()
375
+ console.print("[dim]Methods are generated on first use using Claude CLI.[/dim]")
scry_run/cli/run.py ADDED
@@ -0,0 +1,71 @@
1
+ """Run command for executing scry-run apps."""
2
+
3
+ import os
4
+ import subprocess
5
+
6
+ import click
7
+ from rich.console import Console
8
+
9
+ from scry_run.home import get_app_dir
10
+ from scry_run.config import load_config, get_env_vars
11
+ from scry_run.packages import ensure_scry_run_installed
12
+
13
+ console = Console()
14
+
15
+
16
+ @click.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
17
+ @click.argument("app_name")
18
+ @click.argument("args", nargs=-1, type=click.UNPROCESSED)
19
+ @click.pass_context
20
+ def run(ctx, app_name: str, args: tuple[str, ...]) -> None:
21
+ """Run an scry-run app.
22
+
23
+ Loads config from ~/.scry-run/config.toml, converts to env vars,
24
+ and executes the app with the provided arguments.
25
+
26
+ Examples:
27
+
28
+ scry-run run todo-app
29
+ scry-run run todo-app add "Buy milk"
30
+ scry-run run todo-app --help
31
+ """
32
+ # Find app
33
+ app_dir = get_app_dir(app_name)
34
+ app_py = app_dir / "app.py"
35
+
36
+ if not app_py.exists():
37
+ console.print(f"[red]Error:[/red] App '{app_name}' not found.")
38
+ console.print(f"[dim]Expected at: {app_py}[/dim]")
39
+ console.print()
40
+ console.print("Create it with:")
41
+ console.print(f" [cyan]scry-run init --name {app_name} --description '...'[/cyan]")
42
+ ctx.exit(1)
43
+
44
+ # Load config and convert to env vars
45
+ config = load_config()
46
+ env_vars = get_env_vars(config)
47
+
48
+ # Merge with current environment (env_vars already respects existing env vars)
49
+ env = os.environ.copy()
50
+ env.update(env_vars)
51
+
52
+ # Clear venv-related environment variables to prevent interference
53
+ # from an activated venv (uv run --directory will use the app's venv)
54
+ env.pop("VIRTUAL_ENV", None)
55
+ env.pop("PYTHONHOME", None)
56
+ # Remove activated venv's bin dir from PATH if present
57
+ if "VIRTUAL_ENV" in os.environ:
58
+ venv_bin = os.path.join(os.environ["VIRTUAL_ENV"], "bin")
59
+ path_parts = env.get("PATH", "").split(os.pathsep)
60
+ path_parts = [p for p in path_parts if p != venv_bin]
61
+ env["PATH"] = os.pathsep.join(path_parts)
62
+
63
+ # Ensure app has scry-run installed
64
+ ensure_scry_run_installed(app_dir)
65
+
66
+ # Build command
67
+ cmd = ["uv", "run", "--directory", str(app_dir), "python", str(app_py)] + list(args)
68
+
69
+ # Run app
70
+ result = subprocess.run(cmd, env=env)
71
+ ctx.exit(result.returncode)
scry_run/config.py ADDED
@@ -0,0 +1,141 @@
1
+ """Configuration management for scry-run.
2
+
3
+ Loads config from ~/.scry-run/config.toml and converts to env vars.
4
+ """
5
+
6
+ import os
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ try:
12
+ import tomllib
13
+ except ImportError:
14
+ import tomli as tomllib
15
+
16
+ from scry_run.home import get_config_path
17
+
18
+
19
+ DEFAULT_CONFIG = """\
20
+ # scry-run configuration
21
+ # https://github.com/scry-run/scry-run
22
+
23
+ [defaults]
24
+ backend = "auto" # auto, claude, frozen
25
+ quiet = false # Suppress generation messages
26
+ full_context = true # Use full codebase context
27
+
28
+ [logging]
29
+ level = "info" # debug, info, warn, error
30
+
31
+ [backend.claude]
32
+ # model = "opus" # sonnet, opus, haiku (or full model ID)
33
+ """
34
+
35
+
36
+ @dataclass
37
+ class Config:
38
+ """Configuration for scry-run."""
39
+
40
+ # Defaults
41
+ backend: str = "auto"
42
+ quiet: bool = False
43
+ full_context: bool = True
44
+
45
+ # Logging
46
+ log_level: str = "info"
47
+
48
+ # Backend: claude
49
+ claude_model: Optional[str] = None
50
+
51
+
52
+ def load_config() -> Config:
53
+ """Load configuration from file and environment.
54
+
55
+ Priority (highest to lowest):
56
+ 1. Environment variables
57
+ 2. Config file (~/.scry-run/config.toml)
58
+ 3. Default values
59
+
60
+ Returns:
61
+ Config object with merged settings
62
+ """
63
+ config_path = get_config_path()
64
+
65
+ # Start with defaults
66
+ kwargs = {}
67
+
68
+ # Load from file if exists
69
+ if config_path.exists():
70
+ with open(config_path, "rb") as f:
71
+ data = tomllib.load(f)
72
+
73
+ # Parse [defaults] section
74
+ defaults = data.get("defaults", {})
75
+ if "backend" in defaults:
76
+ kwargs["backend"] = defaults["backend"]
77
+ if "quiet" in defaults:
78
+ kwargs["quiet"] = defaults["quiet"]
79
+ if "full_context" in defaults:
80
+ kwargs["full_context"] = defaults["full_context"]
81
+
82
+ # Parse [logging] section
83
+ logging = data.get("logging", {})
84
+ if "level" in logging:
85
+ kwargs["log_level"] = logging["level"]
86
+
87
+ # Parse backend sections
88
+ backends = data.get("backend", {})
89
+
90
+ # [backend.claude]
91
+ claude = backends.get("claude", {})
92
+ if "model" in claude:
93
+ kwargs["claude_model"] = claude["model"]
94
+
95
+ # Environment variables OVERRIDE config file (highest priority)
96
+ if env_backend := os.environ.get("SCRY_BACKEND"):
97
+ kwargs["backend"] = env_backend
98
+ if env_quiet := os.environ.get("SCRY_QUIET"):
99
+ kwargs["quiet"] = env_quiet.lower() in ("true", "1", "yes")
100
+ if env_full_context := os.environ.get("SCRY_FULL_CONTEXT"):
101
+ kwargs["full_context"] = env_full_context.lower() in ("true", "1", "yes")
102
+ if env_log_level := os.environ.get("SCRY_LOG_LEVEL"):
103
+ kwargs["log_level"] = env_log_level
104
+
105
+ return Config(**kwargs)
106
+
107
+
108
+ def get_env_vars(config: Config) -> dict[str, str]:
109
+ """Convert config to environment variables dict for subprocess.
110
+
111
+ Existing environment variables take precedence.
112
+
113
+ Args:
114
+ config: Config object to convert
115
+
116
+ Returns:
117
+ Dict of environment variable names to values
118
+ """
119
+ env_vars = {}
120
+
121
+ def set_if_value(key: str, value) -> None:
122
+ """Set env var if value is truthy, respecting existing env."""
123
+ if key in os.environ:
124
+ env_vars[key] = os.environ[key]
125
+ elif value is not None and value != "":
126
+ env_vars[key] = str(value).lower() if isinstance(value, bool) else str(value)
127
+
128
+ # Core settings (always have defaults)
129
+ set_if_value("SCRY_BACKEND", config.backend)
130
+ set_if_value("SCRY_QUIET", config.quiet)
131
+ set_if_value("SCRY_FULL_CONTEXT", config.full_context)
132
+ set_if_value("SCRY_LOG_LEVEL", config.log_level)
133
+
134
+ # Model env vars - export for selected backend, but always preserve existing env vars
135
+ if config.backend == "claude":
136
+ set_if_value("SCRY_MODEL", config.claude_model)
137
+ # For "auto" or other: still preserve any existing env vars
138
+ if "SCRY_MODEL" in os.environ:
139
+ env_vars["SCRY_MODEL"] = os.environ["SCRY_MODEL"]
140
+
141
+ return env_vars
scry_run/console.py ADDED
@@ -0,0 +1,52 @@
1
+ """Console output utilities for consistent styling."""
2
+
3
+ from rich.console import Console
4
+
5
+ # Stderr console for status messages
6
+ err_console = Console(stderr=True, highlight=False)
7
+
8
+
9
+ def status(msg: str) -> None:
10
+ """Print a dim status message."""
11
+ err_console.print(f"[dim]\\[scry-run][/dim] {msg}")
12
+
13
+
14
+ def info(msg: str) -> None:
15
+ """Print an info message (cyan)."""
16
+ err_console.print(f"[cyan]\\[scry-run][/cyan] {msg}")
17
+
18
+
19
+ def success(msg: str) -> None:
20
+ """Print a success message (green)."""
21
+ err_console.print(f"[green]\\[scry-run][/green] {msg}")
22
+
23
+
24
+ def warning(msg: str) -> None:
25
+ """Print a warning message (yellow)."""
26
+ err_console.print(f"[yellow]\\[scry-run][/yellow] {msg}")
27
+
28
+
29
+ def error(msg: str) -> None:
30
+ """Print an error message (red)."""
31
+ err_console.print(f"[red]\\[scry-run][/red] {msg}")
32
+
33
+
34
+ def generating(class_name: str, attr_name: str) -> None:
35
+ """Print a 'generating' message."""
36
+ err_console.print(f"[cyan]\\[scry-run][/cyan] Generating {class_name}.{attr_name}...")
37
+
38
+
39
+ def generated(class_name: str, attr_name: str) -> None:
40
+ """Print a 'generated' success message."""
41
+ err_console.print(f"[green]\\[scry-run][/green] Generated {class_name}.{attr_name} ✓")
42
+
43
+
44
+ def using_cached(class_name: str, attr_name: str) -> None:
45
+ """Print a 'using cached' message."""
46
+ err_console.print(f"[dim]\\[scry-run][/dim] Using cached {class_name}.{attr_name}")
47
+
48
+
49
+ def backend_selected(backend_name: str, model: str | None, reason: str) -> None:
50
+ """Print backend selection message."""
51
+ model_str = f" (model={model})" if model else ""
52
+ err_console.print(f"[dim]\\[scry-run][/dim] Using backend: {backend_name}{model_str} ({reason})")