scry-run 0.1.0__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.
- {scry_run-0.1.0 → scry_run-0.1.2}/.claude/settings.local.json +2 -1
- scry_run-0.1.2/PKG-INFO +105 -0
- scry_run-0.1.2/README.md +75 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/pyproject.toml +1 -1
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/backends/frozen.py +6 -6
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/cli/init.py +73 -12
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/cli/run.py +25 -1
- scry_run-0.1.2/src/scry_run/console.py +96 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/generator.py +32 -1
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/logging.py +12 -9
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/meta.py +18 -17
- {scry_run-0.1.0 → scry_run-0.1.2}/uv.lock +1 -1
- scry_run-0.1.0/PKG-INFO +0 -282
- scry_run-0.1.0/README.md +0 -252
- scry_run-0.1.0/src/scry_run/console.py +0 -52
- {scry_run-0.1.0 → scry_run-0.1.2}/.gitignore +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/__init__.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/backends/__init__.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/backends/base.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/backends/claude.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/backends/registry.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/cache.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/cli/__init__.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/cli/apps.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/cli/cache.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/cli/config_cmd.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/cli/env.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/config.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/context.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/home.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/src/scry_run/packages.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/conftest.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_cache.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_cli.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_cli_default.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_cli_env.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_cli_run.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_config.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_context.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_generator.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_home.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_integration.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_logging.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_meta.py +0 -0
- {scry_run-0.1.0 → scry_run-0.1.2}/tests/test_packages.py +0 -0
scry_run-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scry-run
|
|
3
|
+
Version: 0.1.2
|
|
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
|
+
Project-URL: Homepage, https://github.com/Tener/scry-run
|
|
6
|
+
Project-URL: Repository, https://github.com/Tener/scry-run
|
|
7
|
+
Author: Krzysztof Skrzętnicki
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: ai,claude,code-generation,llm,metaclass
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: claude-agent-sdk>=0.1.0
|
|
20
|
+
Requires-Dist: click>=8.0.0
|
|
21
|
+
Requires-Dist: jinja2>=3.0.0
|
|
22
|
+
Requires-Dist: rich>=13.0.0
|
|
23
|
+
Requires-Dist: tomli>=2.0.0; python_version < '3.11'
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-timeout>=2.0.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# scry-run
|
|
32
|
+
|
|
33
|
+
**Describe an app, run it.** A CLI tool that generates and runs Python applications from natural language descriptions.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv tool install scry-run
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or run without installing:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uvx scry-run --help
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Requires [Claude Code CLI](https://github.com/anthropics/claude-code) to be installed.
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Create an app
|
|
53
|
+
scry-run init --name=todoist --description='minimal web todo app'
|
|
54
|
+
|
|
55
|
+
# Run it
|
|
56
|
+
scry-run run todoist
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Demos
|
|
60
|
+
|
|
61
|
+
### Creating a simple Hello World app
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
### Building a maze game with PyGame
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
## CLI Commands
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Create a new app
|
|
73
|
+
scry-run init --name=NAME --description=DESC
|
|
74
|
+
|
|
75
|
+
# List all apps
|
|
76
|
+
scry-run list
|
|
77
|
+
|
|
78
|
+
# Run an app
|
|
79
|
+
scry-run run myapp [args...]
|
|
80
|
+
|
|
81
|
+
# Get app info
|
|
82
|
+
scry-run info myapp
|
|
83
|
+
scry-run which myapp
|
|
84
|
+
|
|
85
|
+
# Remove or reset an app
|
|
86
|
+
scry-run rm myapp
|
|
87
|
+
scry-run reset myapp
|
|
88
|
+
|
|
89
|
+
# Cache management
|
|
90
|
+
scry-run cache list myapp
|
|
91
|
+
scry-run cache show myapp MyClass.method
|
|
92
|
+
scry-run cache export myapp --output=code.py
|
|
93
|
+
scry-run cache prune myapp --all
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
| Variable | Description | Default |
|
|
99
|
+
|----------|-------------|---------|
|
|
100
|
+
| `SCRY_RUN_BACKEND` | Backend: `claude`, `frozen`, or `auto` | `auto` |
|
|
101
|
+
| `SCRY_RUN_MODEL` | Model override (e.g., `opus`) | (backend default) |
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
scry_run-0.1.2/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# scry-run
|
|
2
|
+
|
|
3
|
+
**Describe an app, run it.** A CLI tool that generates and runs Python applications from natural language descriptions.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv tool install scry-run
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uvx scry-run --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires [Claude Code CLI](https://github.com/anthropics/claude-code) to be installed.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Create an app
|
|
23
|
+
scry-run init --name=todoist --description='minimal web todo app'
|
|
24
|
+
|
|
25
|
+
# Run it
|
|
26
|
+
scry-run run todoist
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Demos
|
|
30
|
+
|
|
31
|
+
### Creating a simple Hello World app
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
### Building a maze game with PyGame
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
## CLI Commands
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Create a new app
|
|
43
|
+
scry-run init --name=NAME --description=DESC
|
|
44
|
+
|
|
45
|
+
# List all apps
|
|
46
|
+
scry-run list
|
|
47
|
+
|
|
48
|
+
# Run an app
|
|
49
|
+
scry-run run myapp [args...]
|
|
50
|
+
|
|
51
|
+
# Get app info
|
|
52
|
+
scry-run info myapp
|
|
53
|
+
scry-run which myapp
|
|
54
|
+
|
|
55
|
+
# Remove or reset an app
|
|
56
|
+
scry-run rm myapp
|
|
57
|
+
scry-run reset myapp
|
|
58
|
+
|
|
59
|
+
# Cache management
|
|
60
|
+
scry-run cache list myapp
|
|
61
|
+
scry-run cache show myapp MyClass.method
|
|
62
|
+
scry-run cache export myapp --output=code.py
|
|
63
|
+
scry-run cache prune myapp --all
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
| Variable | Description | Default |
|
|
69
|
+
|----------|-------------|---------|
|
|
70
|
+
| `SCRY_RUN_BACKEND` | Backend: `claude`, `frozen`, or `auto` | `auto` |
|
|
71
|
+
| `SCRY_RUN_MODEL` | Model override (e.g., `opus`) | (backend default) |
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "scry-run"
|
|
3
|
-
version = "0.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" }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Frozen backend - errors on any generation attempt.
|
|
2
2
|
|
|
3
|
-
This backend is used
|
|
4
|
-
Any attempt to generate new code will raise FrozenAppError.
|
|
3
|
+
This backend is used when code generation should be disabled and only cached methods
|
|
4
|
+
should be used. Any attempt to generate new code will raise FrozenAppError.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from typing import Optional
|
|
@@ -14,8 +14,8 @@ class FrozenAppError(RuntimeError):
|
|
|
14
14
|
"""Raised when a frozen app attempts to generate code.
|
|
15
15
|
|
|
16
16
|
This error indicates that a method was called that doesn't exist
|
|
17
|
-
in the cache of a
|
|
18
|
-
or the method needs to be
|
|
17
|
+
in the cache of a frozen app. The backend needs to be changed to allow
|
|
18
|
+
generation, or the method needs to be pre-generated and cached.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
def __init__(self, class_name: str, attr_name: str):
|
|
@@ -24,7 +24,7 @@ class FrozenAppError(RuntimeError):
|
|
|
24
24
|
super().__init__(
|
|
25
25
|
f"Frozen app cannot generate code for '{class_name}.{attr_name}'. "
|
|
26
26
|
f"This method is not in the cache. Either:\n"
|
|
27
|
-
f" 1. Run the
|
|
27
|
+
f" 1. Run the app with a real backend to generate this method first\n"
|
|
28
28
|
f" 2. Set _llm_backend to a real backend (e.g., 'claude') to unfreeze"
|
|
29
29
|
)
|
|
30
30
|
|
|
@@ -32,7 +32,7 @@ class FrozenAppError(RuntimeError):
|
|
|
32
32
|
class FrozenBackend(GeneratorBackend):
|
|
33
33
|
"""Backend that refuses to generate any code.
|
|
34
34
|
|
|
35
|
-
Used
|
|
35
|
+
Used when all methods should come from cache and generation is disabled.
|
|
36
36
|
Any generation attempt raises FrozenAppError with helpful message.
|
|
37
37
|
"""
|
|
38
38
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
354
|
+
description = expanded_desc
|
|
307
355
|
elif choice == "e":
|
|
308
356
|
# Open in editor
|
|
309
|
-
edited = click.edit(
|
|
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,11 +423,14 @@ 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")
|
|
372
434
|
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
435
|
console.print()
|
|
375
436
|
console.print("[dim]Methods are generated on first use using Claude CLI.[/dim]")
|
|
@@ -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)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Console output utilities for consistent styling."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
# Stderr console for status messages
|
|
10
|
+
err_console = Console(stderr=True, highlight=False)
|
|
11
|
+
|
|
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
|
+
|
|
27
|
+
def _timestamp() -> str:
|
|
28
|
+
"""Return current time as HH:MM:SS."""
|
|
29
|
+
return datetime.now().strftime("%H:%M:%S")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def status(msg: str) -> None:
|
|
33
|
+
"""Print a dim status message. Uses title bar in quiet mode."""
|
|
34
|
+
if _is_quiet():
|
|
35
|
+
_set_title(msg)
|
|
36
|
+
return
|
|
37
|
+
err_console.print(f"[dim]{_timestamp()} \\[scry-run][/dim] {msg}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def info(msg: str) -> None:
|
|
41
|
+
"""Print an info message (cyan). Uses title bar in quiet mode."""
|
|
42
|
+
if _is_quiet():
|
|
43
|
+
_set_title(msg)
|
|
44
|
+
return
|
|
45
|
+
err_console.print(f"[dim]{_timestamp()}[/dim] [cyan]\\[scry-run][/cyan] {msg}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def success(msg: str) -> None:
|
|
49
|
+
"""Print a success message (green). Uses title bar in quiet mode."""
|
|
50
|
+
if _is_quiet():
|
|
51
|
+
_set_title(f"✓ {msg}")
|
|
52
|
+
return
|
|
53
|
+
err_console.print(f"[dim]{_timestamp()}[/dim] [green]\\[scry-run][/green] {msg}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def warning(msg: str) -> None:
|
|
57
|
+
"""Print a warning message (yellow)."""
|
|
58
|
+
err_console.print(f"[dim]{_timestamp()}[/dim] [yellow]\\[scry-run][/yellow] {msg}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def error(msg: str) -> None:
|
|
62
|
+
"""Print an error message (red)."""
|
|
63
|
+
err_console.print(f"[dim]{_timestamp()}[/dim] [red]\\[scry-run][/red] {msg}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def generating(class_name: str, attr_name: str) -> None:
|
|
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
|
|
71
|
+
err_console.print(f"[dim]{_timestamp()}[/dim] [cyan]\\[scry-run][/cyan] Generating {class_name}.{attr_name}...")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def generated(class_name: str, attr_name: str) -> None:
|
|
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
|
|
79
|
+
err_console.print(f"[dim]{_timestamp()}[/dim] [green]\\[scry-run][/green] Generated {class_name}.{attr_name} ✓")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def using_cached(class_name: str, attr_name: str) -> None:
|
|
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
|
|
87
|
+
err_console.print(f"[dim]{_timestamp()} \\[scry-run][/dim] Using cached {class_name}.{attr_name}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def backend_selected(backend_name: str, model: str | None, reason: str) -> None:
|
|
91
|
+
"""Print backend selection message. Uses title bar in quiet mode."""
|
|
92
|
+
if _is_quiet():
|
|
93
|
+
_set_title(f"Backend: {backend_name}")
|
|
94
|
+
return
|
|
95
|
+
model_str = f" (model={model})" if model else ""
|
|
96
|
+
err_console.print(f"[dim]{_timestamp()} \\[scry-run][/dim] Using backend: {backend_name}{model_str} ({reason})")
|
|
@@ -603,8 +603,31 @@ IMPORTANT: Output ONLY the JSON object, no markdown, no explanation, no code fen
|
|
|
603
603
|
|
|
604
604
|
# Delegate to backend with retries
|
|
605
605
|
last_error: Optional[Exception] = None
|
|
606
|
+
previous_errors: list[str] = [] # Track errors for feedback
|
|
606
607
|
|
|
607
608
|
for attempt in range(self.MAX_RETRIES):
|
|
609
|
+
# Build error feedback for retry attempts
|
|
610
|
+
error_feedback = ""
|
|
611
|
+
if previous_errors:
|
|
612
|
+
error_feedback = f"""
|
|
613
|
+
|
|
614
|
+
## PREVIOUS GENERATION FAILED
|
|
615
|
+
|
|
616
|
+
This is generation attempt #{attempt + 1}. Previous attempt(s) caused errors:
|
|
617
|
+
|
|
618
|
+
{chr(10).join(f"- Attempt #{i+1}: {err}" for i, err in enumerate(previous_errors))}
|
|
619
|
+
|
|
620
|
+
Please analyze these errors and generate FIXED code that avoids them.
|
|
621
|
+
"""
|
|
622
|
+
|
|
623
|
+
# Rebuild prompt with error feedback if needed
|
|
624
|
+
if error_feedback:
|
|
625
|
+
if supports_context:
|
|
626
|
+
prompt = self._build_frame_prompt(class_name, attr_name, is_classmethod, runtime_context + error_feedback)
|
|
627
|
+
else:
|
|
628
|
+
prompt = self._build_prompt(context + error_feedback, class_name, attr_name, is_classmethod, installed_packages)
|
|
629
|
+
logger.log_generation_start(class_name, attr_name, prompt)
|
|
630
|
+
|
|
608
631
|
try:
|
|
609
632
|
result = self._backend.generate_code(prompt)
|
|
610
633
|
|
|
@@ -645,7 +668,15 @@ IMPORTANT: Output ONLY the JSON object, no markdown, no explanation, no code fen
|
|
|
645
668
|
# Log the validation error with full details
|
|
646
669
|
code_str = result.code if 'result' in dir() and hasattr(result, 'code') else None
|
|
647
670
|
logger.log_validation_error(e, code_str)
|
|
648
|
-
|
|
671
|
+
|
|
672
|
+
# Track error for feedback on next attempt
|
|
673
|
+
error_msg = f"{type(e).__name__}: {e}"
|
|
674
|
+
if code_str:
|
|
675
|
+
# Include a snippet of the problematic code
|
|
676
|
+
code_preview = code_str[:200] + "..." if len(code_str) > 200 else code_str
|
|
677
|
+
error_msg += f"\n Generated code (preview): {code_preview}"
|
|
678
|
+
previous_errors.append(error_msg)
|
|
679
|
+
|
|
649
680
|
if attempt >= self.MAX_RETRIES - 1:
|
|
650
681
|
raise
|
|
651
682
|
warning(f"Validation error, retrying ({attempt + 1}/{self.MAX_RETRIES})...")
|
|
@@ -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.
|
|
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
|
|
49
|
+
"""Get log directory, with fallback to ~/.scry-run/logs/."""
|
|
50
50
|
if self._log_dir:
|
|
51
51
|
return self._log_dir
|
|
52
|
-
|
|
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:
|