rapt0r-cli 1.0.0__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 (34) hide show
  1. rapt0r_cli-1.0.0/LICENSE +21 -0
  2. rapt0r_cli-1.0.0/PKG-INFO +172 -0
  3. rapt0r_cli-1.0.0/README.md +132 -0
  4. rapt0r_cli-1.0.0/pyproject.toml +77 -0
  5. rapt0r_cli-1.0.0/rapt0r/__init__.py +1 -0
  6. rapt0r_cli-1.0.0/rapt0r/__main__.py +4 -0
  7. rapt0r_cli-1.0.0/rapt0r/core/__init__.py +0 -0
  8. rapt0r_cli-1.0.0/rapt0r/core/builder.py +20 -0
  9. rapt0r_cli-1.0.0/rapt0r/core/context_vars.py +97 -0
  10. rapt0r_cli-1.0.0/rapt0r/core/llm.py +123 -0
  11. rapt0r_cli-1.0.0/rapt0r/core/loader.py +137 -0
  12. rapt0r_cli-1.0.0/rapt0r/core/output.py +62 -0
  13. rapt0r_cli-1.0.0/rapt0r/core/utils.py +54 -0
  14. rapt0r_cli-1.0.0/rapt0r/main.py +471 -0
  15. rapt0r_cli-1.0.0/rapt0r/templates/coding/api-design.md +42 -0
  16. rapt0r_cli-1.0.0/rapt0r/templates/coding/code-review.md +41 -0
  17. rapt0r_cli-1.0.0/rapt0r/templates/coding/commit.md +59 -0
  18. rapt0r_cli-1.0.0/rapt0r/templates/coding/debug.md +35 -0
  19. rapt0r_cli-1.0.0/rapt0r/templates/coding/explain.md +36 -0
  20. rapt0r_cli-1.0.0/rapt0r/templates/coding/new-feature.md +33 -0
  21. rapt0r_cli-1.0.0/rapt0r/templates/coding/performance.md +35 -0
  22. rapt0r_cli-1.0.0/rapt0r/templates/coding/refactor.md +36 -0
  23. rapt0r_cli-1.0.0/rapt0r/templates/coding/security.md +39 -0
  24. rapt0r_cli-1.0.0/rapt0r/templates/coding/tests.md +40 -0
  25. rapt0r_cli-1.0.0/rapt0r/templates/content/brainstorm.md +39 -0
  26. rapt0r_cli-1.0.0/rapt0r/templates/content/documentation.md +23 -0
  27. rapt0r_cli-1.0.0/rapt0r/ui.py +115 -0
  28. rapt0r_cli-1.0.0/rapt0r_cli.egg-info/PKG-INFO +172 -0
  29. rapt0r_cli-1.0.0/rapt0r_cli.egg-info/SOURCES.txt +32 -0
  30. rapt0r_cli-1.0.0/rapt0r_cli.egg-info/dependency_links.txt +1 -0
  31. rapt0r_cli-1.0.0/rapt0r_cli.egg-info/entry_points.txt +2 -0
  32. rapt0r_cli-1.0.0/rapt0r_cli.egg-info/requires.txt +10 -0
  33. rapt0r_cli-1.0.0/rapt0r_cli.egg-info/top_level.txt +1 -0
  34. rapt0r_cli-1.0.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 i73
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: rapt0r-cli
3
+ Version: 1.0.0
4
+ Summary: Offline terminal prompt builder — answer a few questions, get a perfect AI prompt in your clipboard. Zero tokens spent on explanations.
5
+ Author: i73
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/i73i73i73i73/RAPT0R
8
+ Project-URL: Repository, https://github.com/i73i73i73i73/RAPT0R
9
+ Project-URL: Issues, https://github.com/i73i73i73i73/RAPT0R/issues
10
+ Project-URL: Changelog, https://github.com/i73i73i73i73/RAPT0R/blob/main/CHANGELOG.md
11
+ Keywords: prompt,ai,cli,terminal,tui,prompt-engineering,llm,claude,openai,developer-tools
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Software Development
25
+ Classifier: Topic :: Terminals
26
+ Classifier: Topic :: Utilities
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: InquirerPy<0.4,>=0.3.4
31
+ Requires-Dist: rich
32
+ Requires-Dist: pyperclip
33
+ Requires-Dist: PyYAML
34
+ Requires-Dist: Jinja2>=3.0
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest; extra == "dev"
37
+ Requires-Dist: ruff; extra == "dev"
38
+ Requires-Dist: build; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ <div align="center">
42
+
43
+ <img src="docs/rapt0r.png" alt="RAPT0R" width="100%"/>
44
+
45
+ <br>
46
+
47
+ **build fast. ship faster.**<br>
48
+ *prompt builder by i73*
49
+
50
+ [![Python](https://img.shields.io/badge/Python-3.9%2B-blueviolet?style=for-the-badge&logo=python)](https://python.org)
51
+ [![Terminal](https://img.shields.io/badge/CLI-Terminal-darkviolet?style=for-the-badge&logo=gnome-terminal)](https://github.com/i73i73i73i73/RAPT0R)
52
+ [![License](https://img.shields.io/badge/License-MIT-success?style=for-the-badge)](LICENSE)
53
+
54
+ ![demo](docs/demo.gif)
55
+
56
+ </div>
57
+
58
+ ---
59
+
60
+ > **Don't spend tokens explaining yourself — spend them on results.**
61
+
62
+ **RAPT0R** is a terminal prompt builder. Answer a few questions, get a battle-tested AI prompt in your clipboard, or execute it directly against an LLM. Offline by design — your context window goes entirely to the actual work.
63
+
64
+ ## ✨ Features
65
+
66
+ - 🔌 **Offline by default** — build complex prompts with zero API calls, or optionally stream execution directly to OpenAI/Anthropic.
67
+ - ⚡️ **Fast** — answer 3-5 questions, the finished prompt is already in your clipboard.
68
+ - 📁 **Project context** — drop a `.rapt0r` file in your repo and your stack/rules/architecture get injected into every prompt automatically.
69
+ - 🔎 **Instant search** — press `f` anywhere to fuzzy-search templates across all categories.
70
+ - 🌳 **Branching** — smart templates ask follow-up questions based on your answers.
71
+ - 🛠 **Built-in manager** — create, edit, and delete templates right from the menu.
72
+ - 🎨 **Polished TUI** — custom colors, number hotkeys, icons, built on `InquirerPy` and `rich`.
73
+
74
+ ---
75
+
76
+ ## 🚀 Install
77
+
78
+ ```bash
79
+ pip install rapt0r-cli
80
+ ```
81
+
82
+ Run it:
83
+
84
+ ```bash
85
+ rapt0r
86
+ ```
87
+
88
+ <details>
89
+ <summary>Or run from source</summary>
90
+
91
+ ```bash
92
+ git clone https://github.com/i73i73i73i73/RAPT0R.git
93
+ cd RAPT0R
94
+ pip3 install -e .
95
+ rapt0r # or: python3 main.py
96
+ ```
97
+ </details>
98
+
99
+ ---
100
+
101
+ ## 📁 Project context (`.rapt0r`)
102
+
103
+ Stop re-explaining your project to the AI in every prompt. Describe it once — RAPT0R appends it to every prompt it builds.
104
+
105
+ Create a `.rapt0r` file in your project root (any plain text or markdown):
106
+
107
+ ```markdown
108
+ Stack: FastAPI + React + PostgreSQL
109
+ Style: type hints everywhere, no classes where functions do
110
+ Architecture: monorepo, backend in /api, frontend in /web
111
+ Rules: never touch migrations by hand, tests are pytest
112
+ ```
113
+
114
+ Run `rapt0r` from anywhere inside the project — it finds the file by walking up the directory tree and adds a `## Project context` section to the generated prompt.
115
+
116
+ ---
117
+
118
+ ## ⌨️ Hotkeys
119
+
120
+ | Key | Action |
121
+ |-----|--------|
122
+ | `1-9` | Jump to item |
123
+ | `f` | Fuzzy search across all templates |
124
+ | `0` | Exit / back |
125
+ | `Ctrl+C` | Cancel current flow |
126
+
127
+ ---
128
+
129
+ ## 📝 Writing your own templates
130
+
131
+ Templates are plain Markdown with **YAML frontmatter**. Press `Create template` in the menu, or drop a `.md` file into a category folder:
132
+
133
+ ```markdown
134
+ ---
135
+ name: "✨ My awesome prompt"
136
+ questions:
137
+ role: "What role should the AI play?"
138
+ task: "Describe the task in one sentence:"
139
+ branch:
140
+ question: "Add a constraints section?"
141
+ y:
142
+ questions:
143
+ limits: "List the constraints, comma-separated:"
144
+ body: "constraints_body"
145
+ ---
146
+
147
+ You act as: {{role}}.
148
+ Your task: {{task}}.
149
+
150
+ {{constraints_body}}
151
+ ```
152
+
153
+ ### Syntax
154
+
155
+ - `name` — display name in the menu.
156
+ - `questions` — `key: "Question?"` pairs; keys become `{{variables}}` in the body. Add `choices:` for a pick-list.
157
+ - `branch` *(optional)* — yes/no fork that asks extra questions and switches the body.
158
+ - `{{variable}}` — replaced with your answers.
159
+
160
+ Where templates live: the repo's `rapt0r/templates/` when running from source, `~/.rapt0r/templates/` when installed via pip (seeded automatically on first run). Generated prompts are saved to `~/.rapt0r/prompts/` (or `prompts/temp/` in a source checkout).
161
+
162
+ ---
163
+
164
+ ## 🧠 Philosophy
165
+
166
+ > **Precise context = fewer tokens = better results.**
167
+
168
+ Every API call where you explain your project from scratch costs money and context window. RAPT0R forces the discipline of giving the AI exactly the inputs it needs — offline, for free, in seconds.
169
+
170
+ <div align="center">
171
+ <sub>built with ❤️ for AI engineering</sub>
172
+ </div>
@@ -0,0 +1,132 @@
1
+ <div align="center">
2
+
3
+ <img src="docs/rapt0r.png" alt="RAPT0R" width="100%"/>
4
+
5
+ <br>
6
+
7
+ **build fast. ship faster.**<br>
8
+ *prompt builder by i73*
9
+
10
+ [![Python](https://img.shields.io/badge/Python-3.9%2B-blueviolet?style=for-the-badge&logo=python)](https://python.org)
11
+ [![Terminal](https://img.shields.io/badge/CLI-Terminal-darkviolet?style=for-the-badge&logo=gnome-terminal)](https://github.com/i73i73i73i73/RAPT0R)
12
+ [![License](https://img.shields.io/badge/License-MIT-success?style=for-the-badge)](LICENSE)
13
+
14
+ ![demo](docs/demo.gif)
15
+
16
+ </div>
17
+
18
+ ---
19
+
20
+ > **Don't spend tokens explaining yourself — spend them on results.**
21
+
22
+ **RAPT0R** is a terminal prompt builder. Answer a few questions, get a battle-tested AI prompt in your clipboard, or execute it directly against an LLM. Offline by design — your context window goes entirely to the actual work.
23
+
24
+ ## ✨ Features
25
+
26
+ - 🔌 **Offline by default** — build complex prompts with zero API calls, or optionally stream execution directly to OpenAI/Anthropic.
27
+ - ⚡️ **Fast** — answer 3-5 questions, the finished prompt is already in your clipboard.
28
+ - 📁 **Project context** — drop a `.rapt0r` file in your repo and your stack/rules/architecture get injected into every prompt automatically.
29
+ - 🔎 **Instant search** — press `f` anywhere to fuzzy-search templates across all categories.
30
+ - 🌳 **Branching** — smart templates ask follow-up questions based on your answers.
31
+ - 🛠 **Built-in manager** — create, edit, and delete templates right from the menu.
32
+ - 🎨 **Polished TUI** — custom colors, number hotkeys, icons, built on `InquirerPy` and `rich`.
33
+
34
+ ---
35
+
36
+ ## 🚀 Install
37
+
38
+ ```bash
39
+ pip install rapt0r-cli
40
+ ```
41
+
42
+ Run it:
43
+
44
+ ```bash
45
+ rapt0r
46
+ ```
47
+
48
+ <details>
49
+ <summary>Or run from source</summary>
50
+
51
+ ```bash
52
+ git clone https://github.com/i73i73i73i73/RAPT0R.git
53
+ cd RAPT0R
54
+ pip3 install -e .
55
+ rapt0r # or: python3 main.py
56
+ ```
57
+ </details>
58
+
59
+ ---
60
+
61
+ ## 📁 Project context (`.rapt0r`)
62
+
63
+ Stop re-explaining your project to the AI in every prompt. Describe it once — RAPT0R appends it to every prompt it builds.
64
+
65
+ Create a `.rapt0r` file in your project root (any plain text or markdown):
66
+
67
+ ```markdown
68
+ Stack: FastAPI + React + PostgreSQL
69
+ Style: type hints everywhere, no classes where functions do
70
+ Architecture: monorepo, backend in /api, frontend in /web
71
+ Rules: never touch migrations by hand, tests are pytest
72
+ ```
73
+
74
+ Run `rapt0r` from anywhere inside the project — it finds the file by walking up the directory tree and adds a `## Project context` section to the generated prompt.
75
+
76
+ ---
77
+
78
+ ## ⌨️ Hotkeys
79
+
80
+ | Key | Action |
81
+ |-----|--------|
82
+ | `1-9` | Jump to item |
83
+ | `f` | Fuzzy search across all templates |
84
+ | `0` | Exit / back |
85
+ | `Ctrl+C` | Cancel current flow |
86
+
87
+ ---
88
+
89
+ ## 📝 Writing your own templates
90
+
91
+ Templates are plain Markdown with **YAML frontmatter**. Press `Create template` in the menu, or drop a `.md` file into a category folder:
92
+
93
+ ```markdown
94
+ ---
95
+ name: "✨ My awesome prompt"
96
+ questions:
97
+ role: "What role should the AI play?"
98
+ task: "Describe the task in one sentence:"
99
+ branch:
100
+ question: "Add a constraints section?"
101
+ y:
102
+ questions:
103
+ limits: "List the constraints, comma-separated:"
104
+ body: "constraints_body"
105
+ ---
106
+
107
+ You act as: {{role}}.
108
+ Your task: {{task}}.
109
+
110
+ {{constraints_body}}
111
+ ```
112
+
113
+ ### Syntax
114
+
115
+ - `name` — display name in the menu.
116
+ - `questions` — `key: "Question?"` pairs; keys become `{{variables}}` in the body. Add `choices:` for a pick-list.
117
+ - `branch` *(optional)* — yes/no fork that asks extra questions and switches the body.
118
+ - `{{variable}}` — replaced with your answers.
119
+
120
+ Where templates live: the repo's `rapt0r/templates/` when running from source, `~/.rapt0r/templates/` when installed via pip (seeded automatically on first run). Generated prompts are saved to `~/.rapt0r/prompts/` (or `prompts/temp/` in a source checkout).
121
+
122
+ ---
123
+
124
+ ## 🧠 Philosophy
125
+
126
+ > **Precise context = fewer tokens = better results.**
127
+
128
+ Every API call where you explain your project from scratch costs money and context window. RAPT0R forces the discipline of giving the AI exactly the inputs it needs — offline, for free, in seconds.
129
+
130
+ <div align="center">
131
+ <sub>built with ❤️ for AI engineering</sub>
132
+ </div>
@@ -0,0 +1,77 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rapt0r-cli"
7
+ version = "1.0.0"
8
+ description = "Offline terminal prompt builder — answer a few questions, get a perfect AI prompt in your clipboard. Zero tokens spent on explanations."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "i73" }]
13
+ keywords = [
14
+ "prompt",
15
+ "ai",
16
+ "cli",
17
+ "terminal",
18
+ "tui",
19
+ "prompt-engineering",
20
+ "llm",
21
+ "claude",
22
+ "openai",
23
+ "developer-tools",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 5 - Production/Stable",
27
+ "Environment :: Console",
28
+ "Intended Audience :: Developers",
29
+ "License :: OSI Approved :: MIT License",
30
+ "Operating System :: OS Independent",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3 :: Only",
33
+ "Programming Language :: Python :: 3.9",
34
+ "Programming Language :: Python :: 3.10",
35
+ "Programming Language :: Python :: 3.11",
36
+ "Programming Language :: Python :: 3.12",
37
+ "Programming Language :: Python :: 3.13",
38
+ "Topic :: Software Development",
39
+ "Topic :: Terminals",
40
+ "Topic :: Utilities",
41
+ ]
42
+ dependencies = [
43
+ "InquirerPy>=0.3.4,<0.4",
44
+ "rich",
45
+ "pyperclip",
46
+ "PyYAML",
47
+ "Jinja2>=3.0",
48
+ ]
49
+
50
+ [project.optional-dependencies]
51
+ dev = ["pytest", "ruff", "build"]
52
+
53
+ [project.urls]
54
+ Homepage = "https://github.com/i73i73i73i73/RAPT0R"
55
+ Repository = "https://github.com/i73i73i73i73/RAPT0R"
56
+ Issues = "https://github.com/i73i73i73i73/RAPT0R/issues"
57
+ Changelog = "https://github.com/i73i73i73i73/RAPT0R/blob/main/CHANGELOG.md"
58
+
59
+ [project.scripts]
60
+ rapt0r = "rapt0r.main:main"
61
+
62
+ [tool.setuptools.packages.find]
63
+ include = ["rapt0r*"]
64
+
65
+ [tool.setuptools.package-data]
66
+ rapt0r = ["templates/*.md", "templates/**/*.md"]
67
+
68
+ [tool.pytest.ini_options]
69
+ testpaths = ["tests"]
70
+
71
+ [tool.ruff]
72
+ target-version = "py39"
73
+ line-length = 100
74
+
75
+ [tool.ruff.lint]
76
+ select = ["E", "F", "W"]
77
+ ignore = ["E501"]
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,4 @@
1
+ from rapt0r.main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
File without changes
@@ -0,0 +1,20 @@
1
+ import logging
2
+ from jinja2 import Environment, BaseLoader, DebugUndefined
3
+
4
+ from rapt0r.core.utils import DEFAULT_BODY
5
+ from rapt0r.core.context_vars import extract_context_vars, resolve_context_vars
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ _env = Environment(loader=BaseLoader(), undefined=DebugUndefined, keep_trailing_newline=True)
10
+
11
+
12
+ def build_prompt(bodies, answers, body_key=None):
13
+ key = body_key if body_key and body_key in bodies else DEFAULT_BODY
14
+ if key not in bodies:
15
+ logger.warning(f"Body section {key!r} not found, using first available")
16
+ key = next(iter(bodies))
17
+ vars_ = {k: v for k, v in answers.items() if not k.startswith("__")}
18
+ body, pending = extract_context_vars(bodies[key])
19
+ result = _env.from_string(body).render(**vars_)
20
+ return resolve_context_vars(result, pending)
@@ -0,0 +1,97 @@
1
+ """Dynamic context variables: {{git_diff}}, {{tree}}, {{tree:path}}, {{file:path}}.
2
+
3
+ The colon forms are not valid Jinja2 syntax, and injected content may itself
4
+ contain ``{{``, so these tokens are swapped for opaque sentinels before the
5
+ Jinja2 render and resolved afterwards (see builder.build_prompt).
6
+ """
7
+
8
+ import os
9
+ import re
10
+ import subprocess
11
+
12
+ MAX_BYTES = 30000
13
+ MAX_TREE_ENTRIES = 500
14
+ SKIP_DIRS = {"__pycache__", "node_modules", "venv", ".venv"}
15
+
16
+ CONTEXT_VAR_NAMES = {"git_diff", "tree", "file"}
17
+
18
+ _PATTERN = re.compile(r"\{\{\s*(git_diff|tree|file)(?::([^}]+))?\s*\}\}")
19
+
20
+
21
+ def _truncate(text):
22
+ if len(text) > MAX_BYTES:
23
+ return text[:MAX_BYTES] + "\n... [truncated]"
24
+ return text
25
+
26
+
27
+ def _git_diff():
28
+ try:
29
+ out = subprocess.run(
30
+ ["git", "diff", "HEAD"], capture_output=True, text=True, timeout=10
31
+ )
32
+ except (OSError, subprocess.TimeoutExpired):
33
+ return "[git diff unavailable]"
34
+ if out.returncode != 0:
35
+ return "[git diff unavailable: not a git repository?]"
36
+ return _truncate(out.stdout.strip()) or "[no uncommitted changes]"
37
+
38
+
39
+ def _tree(root):
40
+ root = (root or ".").strip() or "."
41
+ if not os.path.isdir(root):
42
+ return f"[directory not found: {root}]"
43
+ lines = []
44
+ for dirpath, dirnames, filenames in os.walk(root):
45
+ dirnames[:] = sorted(
46
+ d for d in dirnames if not d.startswith(".") and d not in SKIP_DIRS
47
+ )
48
+ for f in sorted(filenames):
49
+ lines.append(os.path.relpath(os.path.join(dirpath, f), root))
50
+ if len(lines) >= MAX_TREE_ENTRIES:
51
+ lines.append("... [truncated]")
52
+ return "\n".join(lines)
53
+ return "\n".join(lines) or "[empty directory]"
54
+
55
+
56
+ def _file(path):
57
+ path = (path or "").strip()
58
+ try:
59
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
60
+ return _truncate(f.read())
61
+ except OSError as e:
62
+ return f"[could not read {path}: {e}]"
63
+
64
+
65
+ def _resolve(kind, arg):
66
+ if kind == "git_diff":
67
+ return _git_diff()
68
+ if kind == "tree":
69
+ return _tree(arg)
70
+ if kind == "file":
71
+ return _file(arg)
72
+ return ""
73
+
74
+
75
+ def extract_context_vars(text):
76
+ """Replace context tokens with sentinels safe to pass through Jinja2."""
77
+ pending = {}
78
+
79
+ def repl(m):
80
+ token = f"\x00CTX{len(pending)}\x00"
81
+ pending[token] = (m.group(1), m.group(2))
82
+ return token
83
+
84
+ return _PATTERN.sub(repl, text), pending
85
+
86
+
87
+ def resolve_context_vars(text, pending):
88
+ """Substitute sentinels with their resolved content after the render."""
89
+ for token, (kind, arg) in pending.items():
90
+ text = text.replace(token, _resolve(kind, arg))
91
+ return text
92
+
93
+
94
+ def expand_context_vars(text):
95
+ """One-shot expansion (extract + resolve), used directly in tests."""
96
+ extracted, pending = extract_context_vars(text)
97
+ return resolve_context_vars(extracted, pending)
@@ -0,0 +1,123 @@
1
+ """Stream a built prompt to an LLM over raw HTTP (no SDK dependencies).
2
+
3
+ Providers are detected from environment variables:
4
+ ANTHROPIC_API_KEY -> Anthropic Messages API
5
+ OPENAI_API_KEY -> OpenAI Chat Completions API
6
+
7
+ Model overrides: RAPT0R_ANTHROPIC_MODEL, RAPT0R_OPENAI_MODEL.
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import urllib.error
13
+ import urllib.request
14
+
15
+ ANTHROPIC_URL = "https://api.anthropic.com/v1/messages"
16
+ OPENAI_URL = "https://api.openai.com/v1/chat/completions"
17
+
18
+ DEFAULT_ANTHROPIC_MODEL = "claude-opus-4-8"
19
+ DEFAULT_OPENAI_MODEL = "gpt-5-codex"
20
+
21
+ TIMEOUT = 600
22
+
23
+
24
+ class LLMError(Exception):
25
+ pass
26
+
27
+
28
+ def available_providers():
29
+ providers = []
30
+ if os.environ.get("ANTHROPIC_API_KEY"):
31
+ providers.append("anthropic")
32
+ if os.environ.get("OPENAI_API_KEY"):
33
+ providers.append("openai")
34
+ return providers
35
+
36
+
37
+ def iter_sse_data(lines):
38
+ """Yield parsed JSON payloads from an iterable of raw SSE byte lines."""
39
+ for raw in lines:
40
+ line = raw.decode("utf-8", errors="replace").strip()
41
+ if not line.startswith("data:"):
42
+ continue
43
+ data = line[len("data:"):].strip()
44
+ if data == "[DONE]":
45
+ return
46
+ try:
47
+ yield json.loads(data)
48
+ except json.JSONDecodeError:
49
+ continue
50
+
51
+
52
+ def _post_stream(url, headers, body):
53
+ req = urllib.request.Request(
54
+ url, data=json.dumps(body).encode("utf-8"), headers=headers, method="POST"
55
+ )
56
+ try:
57
+ return urllib.request.urlopen(req, timeout=TIMEOUT)
58
+ except urllib.error.HTTPError as e:
59
+ detail = e.read().decode("utf-8", errors="replace")[:500]
60
+ raise LLMError(f"HTTP {e.code}: {detail}") from e
61
+ except urllib.error.URLError as e:
62
+ raise LLMError(f"Network error: {e.reason}") from e
63
+
64
+
65
+ def _stream_anthropic(prompt):
66
+ model = os.environ.get("RAPT0R_ANTHROPIC_MODEL", DEFAULT_ANTHROPIC_MODEL)
67
+ resp = _post_stream(
68
+ ANTHROPIC_URL,
69
+ {
70
+ "x-api-key": os.environ["ANTHROPIC_API_KEY"],
71
+ "anthropic-version": "2023-06-01",
72
+ "content-type": "application/json",
73
+ },
74
+ {
75
+ "model": model,
76
+ "max_tokens": 64000,
77
+ "stream": True,
78
+ "thinking": {"type": "adaptive"},
79
+ "messages": [{"role": "user", "content": prompt}],
80
+ },
81
+ )
82
+ with resp:
83
+ for event in iter_sse_data(resp):
84
+ etype = event.get("type")
85
+ if etype == "content_block_delta":
86
+ delta = event.get("delta", {})
87
+ if delta.get("type") == "text_delta":
88
+ yield delta.get("text", "")
89
+ elif etype == "error":
90
+ raise LLMError(event.get("error", {}).get("message", "stream error"))
91
+
92
+
93
+ def _stream_openai(prompt):
94
+ model = os.environ.get("RAPT0R_OPENAI_MODEL", DEFAULT_OPENAI_MODEL)
95
+ resp = _post_stream(
96
+ OPENAI_URL,
97
+ {
98
+ "Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}",
99
+ "content-type": "application/json",
100
+ },
101
+ {
102
+ "model": model,
103
+ "stream": True,
104
+ "messages": [{"role": "user", "content": prompt}],
105
+ },
106
+ )
107
+ with resp:
108
+ for event in iter_sse_data(resp):
109
+ choices = event.get("choices") or []
110
+ if choices:
111
+ text = choices[0].get("delta", {}).get("content")
112
+ if text:
113
+ yield text
114
+
115
+
116
+ def stream_completion(prompt, provider):
117
+ """Yield response text chunks from the given provider."""
118
+ if provider == "anthropic":
119
+ yield from _stream_anthropic(prompt)
120
+ elif provider == "openai":
121
+ yield from _stream_openai(prompt)
122
+ else:
123
+ raise LLMError(f"Unknown provider: {provider}")