scry-run 0.1.1__tar.gz → 0.1.2__tar.gz

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 (42) hide show
  1. {scry_run-0.1.1 → scry_run-0.1.2}/.claude/settings.local.json +2 -1
  2. {scry_run-0.1.1 → scry_run-0.1.2}/PKG-INFO +1 -1
  3. {scry_run-0.1.1 → scry_run-0.1.2}/pyproject.toml +1 -1
  4. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/cli/init.py +73 -11
  5. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/cli/run.py +25 -1
  6. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/console.py +44 -7
  7. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/logging.py +12 -9
  8. {scry_run-0.1.1 → scry_run-0.1.2}/uv.lock +1 -1
  9. {scry_run-0.1.1 → scry_run-0.1.2}/.gitignore +0 -0
  10. {scry_run-0.1.1 → scry_run-0.1.2}/README.md +0 -0
  11. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/__init__.py +0 -0
  12. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/backends/__init__.py +0 -0
  13. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/backends/base.py +0 -0
  14. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/backends/claude.py +0 -0
  15. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/backends/frozen.py +0 -0
  16. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/backends/registry.py +0 -0
  17. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/cache.py +0 -0
  18. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/cli/__init__.py +0 -0
  19. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/cli/apps.py +0 -0
  20. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/cli/cache.py +0 -0
  21. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/cli/config_cmd.py +0 -0
  22. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/cli/env.py +0 -0
  23. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/config.py +0 -0
  24. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/context.py +0 -0
  25. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/generator.py +0 -0
  26. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/home.py +0 -0
  27. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/meta.py +0 -0
  28. {scry_run-0.1.1 → scry_run-0.1.2}/src/scry_run/packages.py +0 -0
  29. {scry_run-0.1.1 → scry_run-0.1.2}/tests/conftest.py +0 -0
  30. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_cache.py +0 -0
  31. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_cli.py +0 -0
  32. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_cli_default.py +0 -0
  33. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_cli_env.py +0 -0
  34. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_cli_run.py +0 -0
  35. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_config.py +0 -0
  36. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_context.py +0 -0
  37. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_generator.py +0 -0
  38. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_home.py +0 -0
  39. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_integration.py +0 -0
  40. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_logging.py +0 -0
  41. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_meta.py +0 -0
  42. {scry_run-0.1.1 → scry_run-0.1.2}/tests/test_packages.py +0 -0
@@ -6,7 +6,8 @@
6
6
  "Bash(uv run pytest:*)",
7
7
  "Bash(uv run python -m pytest:*)",
8
8
  "Bash(uv sync:*)",
9
- "Bash(uv lock:*)"
9
+ "Bash(uv lock:*)",
10
+ "Bash(uv run:*)"
10
11
  ]
11
12
  }
12
13
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scry-run
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: LLM-powered dynamic code generation via metaclasses. Define classes with docstrings, call any method—code generates automatically, caches persistently, and executes instantly.
5
5
  Project-URL: Homepage, https://github.com/Tener/scry-run
6
6
  Project-URL: Repository, https://github.com/Tener/scry-run
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scry-run"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "LLM-powered dynamic code generation via metaclasses. Define classes with docstrings, call any method—code generates automatically, caches persistently, and executes instantly."
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -39,7 +39,7 @@ IMPORTANT GUIDELINES:
39
39
  - STICK TO WHAT THE USER ASKED FOR - don't add unrelated features
40
40
  - Less is more - a focused app is better than a bloated one
41
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!)
42
+ - Total description length: 100-200 words (keep it concise!)
43
43
 
44
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
45
  - Use standard CLI patterns: commands, subcommands, flags, and positional arguments
@@ -52,10 +52,23 @@ IMPORTANT GUIDELINES:
52
52
  - GOOD: `todo add "buy milk"`, `todo list --due=today`, `todo done 3`
53
53
  - BAD: `todo what's due today?` (question form)
54
54
 
55
- Return ONLY the expanded description text, no JSON or markdown code blocks.'''
55
+ **APP FLAGS** - Recommend runtime flags for the app:
56
+ - "quiet": true - For TUI/interactive apps (curses, textual, rich.live, pygame, etc.)
57
+ This shows scry-run status in terminal title bar instead of stderr, preventing display disruption.
58
+ Only set to true if the app uses a TUI or graphical library.
56
59
 
60
+ Return a JSON object with this structure:
61
+ {{
62
+ "description": "The expanded description text here...",
63
+ "flags": {{
64
+ "quiet": false
65
+ }}
66
+ }}
57
67
 
58
- def expand_description(name: str, description: str) -> str | None:
68
+ Return ONLY the JSON object, no markdown code fences or explanation.'''
69
+
70
+
71
+ def expand_description(name: str, description: str) -> dict | None:
59
72
  """Use LLM to expand a brief description into a detailed one.
60
73
 
61
74
  Args:
@@ -63,7 +76,7 @@ def expand_description(name: str, description: str) -> str | None:
63
76
  description: User's brief description
64
77
 
65
78
  Returns:
66
- Expanded description, or None if expansion fails
79
+ Dict with 'description' and 'flags' keys, or None if expansion fails
67
80
  """
68
81
  try:
69
82
  from scry_run.generator import CodeGenerator, ScryRunError
@@ -72,8 +85,32 @@ def expand_description(name: str, description: str) -> str | None:
72
85
  prompt = EXPAND_PROMPT.format(name=name, description=description)
73
86
 
74
87
  # Use generate_freeform for text generation
75
- expanded = generator.generate_freeform(prompt)
76
- return expanded.strip()
88
+ response = generator.generate_freeform(prompt)
89
+ response = response.strip()
90
+
91
+ # Parse JSON response
92
+ # Handle potential markdown code fences
93
+ if response.startswith("```"):
94
+ lines = response.split("\n")
95
+ # Remove first and last lines (code fences)
96
+ response = "\n".join(lines[1:-1])
97
+
98
+ result = json.loads(response)
99
+
100
+ # Validate structure
101
+ if "description" not in result:
102
+ console.print("[yellow]Warning:[/yellow] LLM response missing 'description' field")
103
+ return None
104
+
105
+ # Ensure flags exists with defaults
106
+ if "flags" not in result:
107
+ result["flags"] = {}
108
+
109
+ return result
110
+
111
+ except json.JSONDecodeError as e:
112
+ console.print(f"[yellow]Warning:[/yellow] Could not parse LLM response as JSON: {e}")
113
+ return None
77
114
  except ScryRunError as e:
78
115
  console.print(f"[yellow]Warning:[/yellow] Could not expand description: {e.message}")
79
116
  return None
@@ -279,20 +316,31 @@ def init(
279
316
  )
280
317
 
281
318
  # Auto-expand description using LLM
319
+ # Track recommended flags from LLM
320
+ recommended_flags: dict = {}
321
+
282
322
  if auto_expand:
283
323
  console.print()
284
324
  console.print("[dim]Expanding description with AI...[/dim]")
285
- expanded = expand_description(name, description)
325
+ result = expand_description(name, description)
326
+
327
+ if result:
328
+ expanded_desc = result["description"]
329
+ recommended_flags = result.get("flags", {})
286
330
 
287
- if expanded:
288
331
  # Show the expanded description
289
332
  console.print()
290
333
  panel = Panel(
291
- expanded,
334
+ expanded_desc,
292
335
  title="[bold]Expanded Description[/bold]",
293
336
  border_style="blue",
294
337
  )
295
338
  console.print(panel)
339
+
340
+ # Show recommended flags if any are non-default
341
+ if recommended_flags.get("quiet"):
342
+ console.print("[dim]Recommended: quiet mode (TUI app)[/dim]")
343
+
296
344
  console.print()
297
345
 
298
346
  # Let user confirm, edit, or reject
@@ -303,10 +351,10 @@ def init(
303
351
  default="y",
304
352
  )
305
353
  if choice == "y":
306
- description = expanded
354
+ description = expanded_desc
307
355
  elif choice == "e":
308
356
  # Open in editor
309
- edited = click.edit(expanded)
357
+ edited = click.edit(expanded_desc)
310
358
  if edited:
311
359
  description = edited.strip()
312
360
  console.print("[dim]Using edited description.[/dim]")
@@ -314,6 +362,7 @@ def init(
314
362
  console.print("[dim]Editor returned empty, using original.[/dim]")
315
363
  else:
316
364
  console.print("[dim]Using original description.[/dim]")
365
+ recommended_flags = {} # Reset flags if user rejected expansion
317
366
  else:
318
367
  console.print("[dim]Using original description.[/dim]")
319
368
 
@@ -356,6 +405,15 @@ def init(
356
405
  cache_file = app_dir / "cache.json"
357
406
  cache_file.write_text(json.dumps({}))
358
407
 
408
+ # Create settings.json with default flags from LLM recommendation
409
+ settings = {
410
+ "default_flags": {
411
+ "quiet": recommended_flags.get("quiet", False),
412
+ }
413
+ }
414
+ settings_file = app_dir / "settings.json"
415
+ settings_file.write_text(json.dumps(settings, indent=2))
416
+
359
417
  # Create logs directory
360
418
  logs_dir = app_dir / "logs"
361
419
  logs_dir.mkdir(exist_ok=True)
@@ -365,7 +423,11 @@ def init(
365
423
  console.print()
366
424
  console.print(f" [dim]Location:[/dim] {app_dir}")
367
425
  console.print(f" [dim]Main file:[/dim] {app_file}")
426
+ console.print(f" [dim]Settings:[/dim] {settings_file}")
368
427
  console.print()
428
+ if recommended_flags.get("quiet"):
429
+ console.print("[dim]Quiet mode enabled (TUI app) - status shown in title bar.[/dim]")
430
+ console.print()
369
431
  console.print("[bold]Useful commands:[/bold]")
370
432
  console.print(f" [cyan]scry-run run {name}[/cyan] Run your app")
371
433
  console.print(f" [cyan]scry-run info {name}[/cyan] View app details and cache stats")
@@ -1,5 +1,6 @@
1
1
  """Run command for executing scry-run apps."""
2
2
 
3
+ import json
3
4
  import os
4
5
  import subprocess
5
6
 
@@ -13,11 +14,23 @@ from scry_run.packages import ensure_scry_run_installed
13
14
  console = Console()
14
15
 
15
16
 
17
+ def load_app_settings(app_dir) -> dict:
18
+ """Load per-app settings from settings.json."""
19
+ settings_file = app_dir / "settings.json"
20
+ if settings_file.exists():
21
+ try:
22
+ return json.loads(settings_file.read_text())
23
+ except (json.JSONDecodeError, OSError):
24
+ return {}
25
+ return {}
26
+
27
+
16
28
  @click.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
29
+ @click.option("--quiet", "-q", is_flag=True, help="Suppress status messages (for TUI apps)")
17
30
  @click.argument("app_name")
18
31
  @click.argument("args", nargs=-1, type=click.UNPROCESSED)
19
32
  @click.pass_context
20
- def run(ctx, app_name: str, args: tuple[str, ...]) -> None:
33
+ def run(ctx, quiet: bool, app_name: str, args: tuple[str, ...]) -> None:
21
34
  """Run an scry-run app.
22
35
 
23
36
  Loads config from ~/.scry-run/config.toml, converts to env vars,
@@ -28,6 +41,7 @@ def run(ctx, app_name: str, args: tuple[str, ...]) -> None:
28
41
  scry-run run todo-app
29
42
  scry-run run todo-app add "Buy milk"
30
43
  scry-run run todo-app --help
44
+ scry-run run --quiet my-tui-app
31
45
  """
32
46
  # Find app
33
47
  app_dir = get_app_dir(app_name)
@@ -41,6 +55,10 @@ def run(ctx, app_name: str, args: tuple[str, ...]) -> None:
41
55
  console.print(f" [cyan]scry-run init --name {app_name} --description '...'[/cyan]")
42
56
  ctx.exit(1)
43
57
 
58
+ # Load per-app settings
59
+ app_settings = load_app_settings(app_dir)
60
+ default_flags = app_settings.get("default_flags", {})
61
+
44
62
  # Load config and convert to env vars
45
63
  config = load_config()
46
64
  env_vars = get_env_vars(config)
@@ -49,6 +67,12 @@ def run(ctx, app_name: str, args: tuple[str, ...]) -> None:
49
67
  env = os.environ.copy()
50
68
  env.update(env_vars)
51
69
 
70
+ # Set quiet mode if requested via flag OR app default
71
+ # CLI flag takes precedence over app settings
72
+ use_quiet = quiet or default_flags.get("quiet", False)
73
+ if use_quiet:
74
+ env["SCRY_QUIET"] = "1"
75
+
52
76
  # Clear venv-related environment variables to prevent interference
53
77
  # from an activated venv (uv run --directory will use the app's venv)
54
78
  env.pop("VIRTUAL_ENV", None)
@@ -1,5 +1,7 @@
1
1
  """Console output utilities for consistent styling."""
2
2
 
3
+ import os
4
+ import sys
3
5
  from datetime import datetime
4
6
 
5
7
  from rich.console import Console
@@ -8,23 +10,46 @@ from rich.console import Console
8
10
  err_console = Console(stderr=True, highlight=False)
9
11
 
10
12
 
13
+ def _is_quiet() -> bool:
14
+ """Check if quiet mode is enabled (SCRY_QUIET=1)."""
15
+ return os.environ.get("SCRY_QUIET", "").lower() in ("1", "true", "yes", "on")
16
+
17
+
18
+ def _set_title(msg: str) -> None:
19
+ """Set terminal title bar. Used in quiet mode to show progress."""
20
+ # OSC escape sequence to set window title
21
+ # \033]0; sets both window title and icon name
22
+ # \007 is the bell character that terminates the sequence
23
+ sys.stderr.write(f"\033]0;[scry-run] {msg}\007")
24
+ sys.stderr.flush()
25
+
26
+
11
27
  def _timestamp() -> str:
12
28
  """Return current time as HH:MM:SS."""
13
29
  return datetime.now().strftime("%H:%M:%S")
14
30
 
15
31
 
16
32
  def status(msg: str) -> None:
17
- """Print a dim status message."""
33
+ """Print a dim status message. Uses title bar in quiet mode."""
34
+ if _is_quiet():
35
+ _set_title(msg)
36
+ return
18
37
  err_console.print(f"[dim]{_timestamp()} \\[scry-run][/dim] {msg}")
19
38
 
20
39
 
21
40
  def info(msg: str) -> None:
22
- """Print an info message (cyan)."""
41
+ """Print an info message (cyan). Uses title bar in quiet mode."""
42
+ if _is_quiet():
43
+ _set_title(msg)
44
+ return
23
45
  err_console.print(f"[dim]{_timestamp()}[/dim] [cyan]\\[scry-run][/cyan] {msg}")
24
46
 
25
47
 
26
48
  def success(msg: str) -> None:
27
- """Print a success message (green)."""
49
+ """Print a success message (green). Uses title bar in quiet mode."""
50
+ if _is_quiet():
51
+ _set_title(f"✓ {msg}")
52
+ return
28
53
  err_console.print(f"[dim]{_timestamp()}[/dim] [green]\\[scry-run][/green] {msg}")
29
54
 
30
55
 
@@ -39,21 +64,33 @@ def error(msg: str) -> None:
39
64
 
40
65
 
41
66
  def generating(class_name: str, attr_name: str) -> None:
42
- """Print a 'generating' message."""
67
+ """Print a 'generating' message. Uses title bar in quiet mode."""
68
+ if _is_quiet():
69
+ _set_title(f"Generating {class_name}.{attr_name}...")
70
+ return
43
71
  err_console.print(f"[dim]{_timestamp()}[/dim] [cyan]\\[scry-run][/cyan] Generating {class_name}.{attr_name}...")
44
72
 
45
73
 
46
74
  def generated(class_name: str, attr_name: str) -> None:
47
- """Print a 'generated' success message."""
75
+ """Print a 'generated' success message. Uses title bar in quiet mode."""
76
+ if _is_quiet():
77
+ _set_title(f"✓ Generated {class_name}.{attr_name}")
78
+ return
48
79
  err_console.print(f"[dim]{_timestamp()}[/dim] [green]\\[scry-run][/green] Generated {class_name}.{attr_name} ✓")
49
80
 
50
81
 
51
82
  def using_cached(class_name: str, attr_name: str) -> None:
52
- """Print a 'using cached' message."""
83
+ """Print a 'using cached' message. Uses title bar in quiet mode."""
84
+ if _is_quiet():
85
+ _set_title(f"Using cached {class_name}.{attr_name}")
86
+ return
53
87
  err_console.print(f"[dim]{_timestamp()} \\[scry-run][/dim] Using cached {class_name}.{attr_name}")
54
88
 
55
89
 
56
90
  def backend_selected(backend_name: str, model: str | None, reason: str) -> None:
57
- """Print backend selection message."""
91
+ """Print backend selection message. Uses title bar in quiet mode."""
92
+ if _is_quiet():
93
+ _set_title(f"Backend: {backend_name}")
94
+ return
58
95
  model_str = f" (model={model})" if model else ""
59
96
  err_console.print(f"[dim]{_timestamp()} \\[scry-run][/dim] Using backend: {backend_name}{model_str} ({reason})")
@@ -17,14 +17,14 @@ class ScryRunLogger:
17
17
  Log file is created in:
18
18
  1. The app's logs/ directory (if running in an app context)
19
19
  2. Directory specified by SCRY_LOG_DIR
20
- 3. Current working directory (fallback)
20
+ 3. ~/.scry-run/logs/ (fallback for CLI commands)
21
21
  """
22
-
22
+
23
23
  _instance: Optional["ScryRunLogger"] = None
24
-
24
+
25
25
  def __init__(self, log_dir: Optional[Path] = None):
26
26
  """Initialize logger.
27
-
27
+
28
28
  Args:
29
29
  log_dir: Directory for log file. If None, uses auto-detection.
30
30
  """
@@ -32,24 +32,27 @@ class ScryRunLogger:
32
32
  self._log_file: Optional[Path] = None
33
33
  # Session timestamp for unique log files per run
34
34
  self._session_id = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
35
-
35
+
36
36
  if log_dir:
37
37
  self._log_dir = log_dir
38
38
  else:
39
39
  env_dir = os.environ.get("SCRY_LOG_DIR")
40
40
  if env_dir:
41
41
  self._log_dir = Path(env_dir)
42
-
42
+
43
43
  # Debug logging is ON by default. Set SCRY_DEBUG=0 to disable.
44
44
  debug_env = os.environ.get("SCRY_DEBUG", "").lower()
45
45
  self._enabled = debug_env not in ("0", "false", "no", "off")
46
-
46
+
47
47
  @property
48
48
  def log_dir(self) -> Path:
49
- """Get log directory, with fallback to cwd."""
49
+ """Get log directory, with fallback to ~/.scry-run/logs/."""
50
50
  if self._log_dir:
51
51
  return self._log_dir
52
- return Path.cwd()
52
+ # Fallback to global scry-run logs directory (not cwd)
53
+ fallback = Path.home() / ".scry-run" / "logs"
54
+ fallback.mkdir(parents=True, exist_ok=True)
55
+ return fallback
53
56
 
54
57
  @property
55
58
  def log_file(self) -> Path:
@@ -1028,7 +1028,7 @@ wheels = [
1028
1028
 
1029
1029
  [[package]]
1030
1030
  name = "scry-run"
1031
- version = "0.1.0"
1031
+ version = "0.1.2"
1032
1032
  source = { editable = "." }
1033
1033
  dependencies = [
1034
1034
  { name = "claude-agent-sdk" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes