renforge 0.1.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.
- renforge-0.1.0/.gitignore +48 -0
- renforge-0.1.0/LICENSE +21 -0
- renforge-0.1.0/PKG-INFO +171 -0
- renforge-0.1.0/README.md +141 -0
- renforge-0.1.0/pyproject.toml +50 -0
- renforge-0.1.0/src/renforge/__init__.py +4 -0
- renforge-0.1.0/src/renforge/__main__.py +5 -0
- renforge-0.1.0/src/renforge/activity_log.py +93 -0
- renforge-0.1.0/src/renforge/assets.py +149 -0
- renforge-0.1.0/src/renforge/autopilot.py +171 -0
- renforge-0.1.0/src/renforge/bridge/__init__.py +10 -0
- renforge-0.1.0/src/renforge/bridge/bridge.rpy +406 -0
- renforge-0.1.0/src/renforge/bridge/client.py +139 -0
- renforge-0.1.0/src/renforge/bridge/launcher.py +132 -0
- renforge-0.1.0/src/renforge/build.py +79 -0
- renforge-0.1.0/src/renforge/cli.py +103 -0
- renforge-0.1.0/src/renforge/docs.py +122 -0
- renforge-0.1.0/src/renforge/dump.py +80 -0
- renforge-0.1.0/src/renforge/lint.py +194 -0
- renforge-0.1.0/src/renforge/project.py +54 -0
- renforge-0.1.0/src/renforge/scanner.py +247 -0
- renforge-0.1.0/src/renforge/sdk.py +263 -0
- renforge-0.1.0/src/renforge/server.py +439 -0
- renforge-0.1.0/src/renforge/tools/__init__.py +3 -0
- renforge-0.1.0/src/renforge/tools/live.py +274 -0
- renforge-0.1.0/src/renforge/tools/project_ops.py +127 -0
- renforge-0.1.0/src/renforge/tools/static.py +65 -0
- renforge-0.1.0/src/renforge/translation.py +206 -0
- renforge-0.1.0/src/renforge/ui/__init__.py +14 -0
- renforge-0.1.0/src/renforge/ui/activity.py +98 -0
- renforge-0.1.0/src/renforge/ui/graph.py +254 -0
- renforge-0.1.0/src/renforge/ui/poller.py +67 -0
- renforge-0.1.0/src/renforge/ui/server.py +330 -0
- renforge-0.1.0/src/renforge/ui/static/assets/index-g2AQglUZ.js +92 -0
- renforge-0.1.0/src/renforge/ui/static/assets/index-wSw967Fa.css +1 -0
- renforge-0.1.0/src/renforge/ui/static/index.html +13 -0
- renforge-0.1.0/src/renforge/ui/ws.py +55 -0
- renforge-0.1.0/src/renforge/util/__init__.py +10 -0
- renforge-0.1.0/src/renforge/util/files.py +33 -0
- renforge-0.1.0/src/renforge/util/subprocess.py +95 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.so
|
|
5
|
+
*.egg-info/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
coverage/
|
|
9
|
+
.coverage
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
node_modules/
|
|
14
|
+
*.tsbuildinfo
|
|
15
|
+
.DS_Store
|
|
16
|
+
*.tmp
|
|
17
|
+
*.log
|
|
18
|
+
logs/
|
|
19
|
+
examples/demo_game/.cache/
|
|
20
|
+
examples/demo_game/saves/
|
|
21
|
+
examples/demo_game/error.log
|
|
22
|
+
examples/demo_game/*.rpyc.bak
|
|
23
|
+
examples/demo_game/game/cache/
|
|
24
|
+
examples/demo_game/game/saves/
|
|
25
|
+
examples/demo_game/.renforge/
|
|
26
|
+
examples/demo_game/log.txt
|
|
27
|
+
examples/demo_game/game/renforge_bridge.rpyc.bak
|
|
28
|
+
# Bridge source injected at runtime by the launcher (the .rpyc is caught by *.rpyc)
|
|
29
|
+
renforge_bridge.rpy
|
|
30
|
+
examples/demo_game/game/gui/*.png
|
|
31
|
+
examples/demo_game/game/gui/**/*.png
|
|
32
|
+
!examples/demo_game/game/gui/bubble.png
|
|
33
|
+
!examples/demo_game/game/gui/thoughtbubble.png
|
|
34
|
+
|
|
35
|
+
# Internal working docs (reviews, competitive analysis, plans) — keep private
|
|
36
|
+
docs/
|
|
37
|
+
|
|
38
|
+
# Ren'Py / RenForge runtime artifacts
|
|
39
|
+
*.rpyc
|
|
40
|
+
*.rpymc
|
|
41
|
+
*.rpyb
|
|
42
|
+
cache/
|
|
43
|
+
saves/
|
|
44
|
+
log.txt
|
|
45
|
+
traceback.txt
|
|
46
|
+
errors.txt
|
|
47
|
+
.renforge/
|
|
48
|
+
screens
|
renforge-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RenForge contributors
|
|
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.
|
renforge-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: renforge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server, CLI, and web dashboard for Ren'Py visual-novel development
|
|
5
|
+
Project-URL: Homepage, https://github.com/alex-jordan547/renforge-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/alex-jordan547/renforge-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/alex-jordan547/renforge-mcp/issues
|
|
8
|
+
Author: RenForge contributors
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: game-dev,mcp,model-context-protocol,renpy,visual-novel
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Games/Entertainment
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: mcp>=1.0.0
|
|
21
|
+
Provides-Extra: fastmcp
|
|
22
|
+
Requires-Dist: fastmcp>=2.0.0; extra == 'fastmcp'
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
25
|
+
Provides-Extra: ui
|
|
26
|
+
Requires-Dist: starlette>=0.31; extra == 'ui'
|
|
27
|
+
Requires-Dist: uvicorn[standard]>=0.30; extra == 'ui'
|
|
28
|
+
Requires-Dist: watchfiles>=0.22; extra == 'ui'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# RenForge
|
|
32
|
+
|
|
33
|
+
RenForge is an **MCP (Model Context Protocol) server, CLI, and web dashboard**
|
|
34
|
+
for working with [Ren'Py](https://www.renpy.org/) visual-novel projects.
|
|
35
|
+
|
|
36
|
+
It lets an AI agent — or a human via the dashboard — inspect a project, launch
|
|
37
|
+
and drive a running game, read/write game state, capture screenshots, generate
|
|
38
|
+
translations, find orphaned assets, run builds, and search Ren'Py's docs.
|
|
39
|
+
|
|
40
|
+
> Status: **alpha**, actively developed. The core surfaces (MCP tools, in-game
|
|
41
|
+
> bridge, CLI, dashboard) are functional; APIs may still change.
|
|
42
|
+
|
|
43
|
+
## What it does
|
|
44
|
+
|
|
45
|
+
- **Project inspection** — summarize structure, scan scripts/labels/assets,
|
|
46
|
+
parse lint output.
|
|
47
|
+
- **Live game control** — launch a project with an injected in-game bridge, then
|
|
48
|
+
advance dialogue, list/select choices, evaluate expressions, get/set store
|
|
49
|
+
variables, poll pushed events, and capture frames the model can literally see.
|
|
50
|
+
- **Autopilot** — auto-play the game across branches and report label coverage
|
|
51
|
+
and crashes.
|
|
52
|
+
- **Assets & translations** — find orphaned/missing image+audio assets, list
|
|
53
|
+
languages, compute translation stats, generate/update `game/tl/<lang>/` files,
|
|
54
|
+
export dialogue as text.
|
|
55
|
+
- **Builds** — package desktop distributions and web builds.
|
|
56
|
+
- **Docs** — search and read Ren'Py's offline documentation.
|
|
57
|
+
- **Web dashboard** — Starlette + WebSocket UI with a live story map, activity
|
|
58
|
+
log, autopilot coverage, lint view, and game-state controls.
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
Requires Python 3.11+. With [uv](https://docs.astral.sh/uv/) installed, no
|
|
63
|
+
setup is needed:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Start the web dashboard on your project
|
|
67
|
+
uvx --from "renforge[ui]" renforge ui --project /path/to/your/game
|
|
68
|
+
|
|
69
|
+
# Or add the MCP server to Claude Code
|
|
70
|
+
claude mcp add renforge -- uvx --from "renforge[fastmcp]" renforge serve --project /path/to/your/game
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For Claude Desktop (or any MCP client using JSON config):
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"renforge": {
|
|
79
|
+
"command": "uvx",
|
|
80
|
+
"args": [
|
|
81
|
+
"--from", "renforge[fastmcp]", "renforge",
|
|
82
|
+
"serve", "--project", "/path/to/your/game"
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Prefer pip? `pip install "renforge[fastmcp,ui]"` gives you the `renforge` CLI.
|
|
90
|
+
|
|
91
|
+
## Install (dev)
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python -m venv .venv
|
|
95
|
+
source .venv/bin/activate
|
|
96
|
+
pip install -e ".[fastmcp]" # full MCP runtime (fastmcp)
|
|
97
|
+
pip install -e ".[ui]" # dashboard (starlette, uvicorn, watchfiles)
|
|
98
|
+
pip install -e ".[test]" # pytest
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The base install only requires `mcp>=1.0.0`; the server falls back to a
|
|
102
|
+
compatibility mode with a clear message if `fastmcp` is not installed.
|
|
103
|
+
|
|
104
|
+
## Usage
|
|
105
|
+
|
|
106
|
+
### CLI
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
renforge --version
|
|
110
|
+
renforge inspect <project> # lightweight project summary (JSON)
|
|
111
|
+
renforge serve [--project .] # start the MCP server (stdio transport)
|
|
112
|
+
renforge ui --project <project> [--port 8765] # start the web dashboard
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### MCP server
|
|
116
|
+
|
|
117
|
+
`renforge serve` exposes the tools below to any MCP client. A subset:
|
|
118
|
+
|
|
119
|
+
- `renforge_inspect_project`, `renforge_scan_project`, `renforge_parse_lint`
|
|
120
|
+
- `renforge_launch`, `renforge_stop`
|
|
121
|
+
- `renforge_game_state`, `renforge_advance`, `renforge_list_choices`,
|
|
122
|
+
`renforge_select_choice`, `renforge_eval`, `renforge_get_var`,
|
|
123
|
+
`renforge_set_var`, `renforge_poll_events`, `renforge_screenshot`
|
|
124
|
+
- `renforge_autopilot`
|
|
125
|
+
- `renforge_assets`, `renforge_languages`, `renforge_translation_stats`,
|
|
126
|
+
`renforge_generate_translations`, `renforge_export_dialogue`
|
|
127
|
+
- `renforge_web_build`, `renforge_distribute`
|
|
128
|
+
- `renforge_search_docs`, `renforge_get_doc`, `renforge_list_docs`
|
|
129
|
+
|
|
130
|
+
### Live control
|
|
131
|
+
|
|
132
|
+
`renforge_launch` injects a bridge into `<project>/game/` (removed on teardown)
|
|
133
|
+
and starts the game. Live tools require a display — under WSLg it works
|
|
134
|
+
directly; headless CI should wrap the call with `xvfb-run`.
|
|
135
|
+
|
|
136
|
+
### Web dashboard
|
|
137
|
+
|
|
138
|
+
`renforge ui --project <project>` serves a dashboard (default `127.0.0.1:8765`)
|
|
139
|
+
with a story map, activity log, autopilot coverage, lint view, and live game
|
|
140
|
+
controls over WebSocket.
|
|
141
|
+
|
|
142
|
+
## Examples
|
|
143
|
+
|
|
144
|
+
A small sample project lives in `examples/demo_game/`.
|
|
145
|
+
|
|
146
|
+
## Architecture
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
src/renforge/
|
|
150
|
+
cli.py # argparse entrypoint (inspect / serve / ui)
|
|
151
|
+
server.py # MCP app bootstrap + fallback + tool registration
|
|
152
|
+
bridge/ # in-game .rpy bridge, launcher, and client
|
|
153
|
+
tools/
|
|
154
|
+
live.py # running-game control (launch, eval, screenshot, ...)
|
|
155
|
+
project_ops.py # assets, translations, builds, docs
|
|
156
|
+
static.py # inspect / scan / parse-lint
|
|
157
|
+
ui/ # Starlette dashboard (server, ws, graph, activity, poller)
|
|
158
|
+
util/ # filesystem + subprocess helpers
|
|
159
|
+
sdk.py # Ren'Py SDK download/cache
|
|
160
|
+
scanner.py # script/label/asset scanning
|
|
161
|
+
lint.py # lint runner + parsing
|
|
162
|
+
autopilot.py # branch auto-play + coverage
|
|
163
|
+
translation.py # translation generation/stats
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Packaging uses `hatchling`; the console script is
|
|
167
|
+
`renforge = renforge.cli:main`.
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT
|
renforge-0.1.0/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# RenForge
|
|
2
|
+
|
|
3
|
+
RenForge is an **MCP (Model Context Protocol) server, CLI, and web dashboard**
|
|
4
|
+
for working with [Ren'Py](https://www.renpy.org/) visual-novel projects.
|
|
5
|
+
|
|
6
|
+
It lets an AI agent — or a human via the dashboard — inspect a project, launch
|
|
7
|
+
and drive a running game, read/write game state, capture screenshots, generate
|
|
8
|
+
translations, find orphaned assets, run builds, and search Ren'Py's docs.
|
|
9
|
+
|
|
10
|
+
> Status: **alpha**, actively developed. The core surfaces (MCP tools, in-game
|
|
11
|
+
> bridge, CLI, dashboard) are functional; APIs may still change.
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
- **Project inspection** — summarize structure, scan scripts/labels/assets,
|
|
16
|
+
parse lint output.
|
|
17
|
+
- **Live game control** — launch a project with an injected in-game bridge, then
|
|
18
|
+
advance dialogue, list/select choices, evaluate expressions, get/set store
|
|
19
|
+
variables, poll pushed events, and capture frames the model can literally see.
|
|
20
|
+
- **Autopilot** — auto-play the game across branches and report label coverage
|
|
21
|
+
and crashes.
|
|
22
|
+
- **Assets & translations** — find orphaned/missing image+audio assets, list
|
|
23
|
+
languages, compute translation stats, generate/update `game/tl/<lang>/` files,
|
|
24
|
+
export dialogue as text.
|
|
25
|
+
- **Builds** — package desktop distributions and web builds.
|
|
26
|
+
- **Docs** — search and read Ren'Py's offline documentation.
|
|
27
|
+
- **Web dashboard** — Starlette + WebSocket UI with a live story map, activity
|
|
28
|
+
log, autopilot coverage, lint view, and game-state controls.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
Requires Python 3.11+. With [uv](https://docs.astral.sh/uv/) installed, no
|
|
33
|
+
setup is needed:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Start the web dashboard on your project
|
|
37
|
+
uvx --from "renforge[ui]" renforge ui --project /path/to/your/game
|
|
38
|
+
|
|
39
|
+
# Or add the MCP server to Claude Code
|
|
40
|
+
claude mcp add renforge -- uvx --from "renforge[fastmcp]" renforge serve --project /path/to/your/game
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For Claude Desktop (or any MCP client using JSON config):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"renforge": {
|
|
49
|
+
"command": "uvx",
|
|
50
|
+
"args": [
|
|
51
|
+
"--from", "renforge[fastmcp]", "renforge",
|
|
52
|
+
"serve", "--project", "/path/to/your/game"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Prefer pip? `pip install "renforge[fastmcp,ui]"` gives you the `renforge` CLI.
|
|
60
|
+
|
|
61
|
+
## Install (dev)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m venv .venv
|
|
65
|
+
source .venv/bin/activate
|
|
66
|
+
pip install -e ".[fastmcp]" # full MCP runtime (fastmcp)
|
|
67
|
+
pip install -e ".[ui]" # dashboard (starlette, uvicorn, watchfiles)
|
|
68
|
+
pip install -e ".[test]" # pytest
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The base install only requires `mcp>=1.0.0`; the server falls back to a
|
|
72
|
+
compatibility mode with a clear message if `fastmcp` is not installed.
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
### CLI
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
renforge --version
|
|
80
|
+
renforge inspect <project> # lightweight project summary (JSON)
|
|
81
|
+
renforge serve [--project .] # start the MCP server (stdio transport)
|
|
82
|
+
renforge ui --project <project> [--port 8765] # start the web dashboard
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### MCP server
|
|
86
|
+
|
|
87
|
+
`renforge serve` exposes the tools below to any MCP client. A subset:
|
|
88
|
+
|
|
89
|
+
- `renforge_inspect_project`, `renforge_scan_project`, `renforge_parse_lint`
|
|
90
|
+
- `renforge_launch`, `renforge_stop`
|
|
91
|
+
- `renforge_game_state`, `renforge_advance`, `renforge_list_choices`,
|
|
92
|
+
`renforge_select_choice`, `renforge_eval`, `renforge_get_var`,
|
|
93
|
+
`renforge_set_var`, `renforge_poll_events`, `renforge_screenshot`
|
|
94
|
+
- `renforge_autopilot`
|
|
95
|
+
- `renforge_assets`, `renforge_languages`, `renforge_translation_stats`,
|
|
96
|
+
`renforge_generate_translations`, `renforge_export_dialogue`
|
|
97
|
+
- `renforge_web_build`, `renforge_distribute`
|
|
98
|
+
- `renforge_search_docs`, `renforge_get_doc`, `renforge_list_docs`
|
|
99
|
+
|
|
100
|
+
### Live control
|
|
101
|
+
|
|
102
|
+
`renforge_launch` injects a bridge into `<project>/game/` (removed on teardown)
|
|
103
|
+
and starts the game. Live tools require a display — under WSLg it works
|
|
104
|
+
directly; headless CI should wrap the call with `xvfb-run`.
|
|
105
|
+
|
|
106
|
+
### Web dashboard
|
|
107
|
+
|
|
108
|
+
`renforge ui --project <project>` serves a dashboard (default `127.0.0.1:8765`)
|
|
109
|
+
with a story map, activity log, autopilot coverage, lint view, and live game
|
|
110
|
+
controls over WebSocket.
|
|
111
|
+
|
|
112
|
+
## Examples
|
|
113
|
+
|
|
114
|
+
A small sample project lives in `examples/demo_game/`.
|
|
115
|
+
|
|
116
|
+
## Architecture
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
src/renforge/
|
|
120
|
+
cli.py # argparse entrypoint (inspect / serve / ui)
|
|
121
|
+
server.py # MCP app bootstrap + fallback + tool registration
|
|
122
|
+
bridge/ # in-game .rpy bridge, launcher, and client
|
|
123
|
+
tools/
|
|
124
|
+
live.py # running-game control (launch, eval, screenshot, ...)
|
|
125
|
+
project_ops.py # assets, translations, builds, docs
|
|
126
|
+
static.py # inspect / scan / parse-lint
|
|
127
|
+
ui/ # Starlette dashboard (server, ws, graph, activity, poller)
|
|
128
|
+
util/ # filesystem + subprocess helpers
|
|
129
|
+
sdk.py # Ren'Py SDK download/cache
|
|
130
|
+
scanner.py # script/label/asset scanning
|
|
131
|
+
lint.py # lint runner + parsing
|
|
132
|
+
autopilot.py # branch auto-play + coverage
|
|
133
|
+
translation.py # translation generation/stats
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Packaging uses `hatchling`; the console script is
|
|
137
|
+
`renforge = renforge.cli:main`.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "renforge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server, CLI, and web dashboard for Ren'Py visual-novel development"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "RenForge contributors" }]
|
|
13
|
+
keywords = ["renpy", "mcp", "model-context-protocol", "visual-novel", "game-dev"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Topic :: Games/Entertainment",
|
|
21
|
+
"Topic :: Software Development :: Libraries",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"mcp>=1.0.0"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/alex-jordan547/renforge-mcp"
|
|
29
|
+
Repository = "https://github.com/alex-jordan547/renforge-mcp"
|
|
30
|
+
Issues = "https://github.com/alex-jordan547/renforge-mcp/issues"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
fastmcp = ["fastmcp>=2.0.0"]
|
|
34
|
+
test = ["pytest>=8"]
|
|
35
|
+
ui = ["starlette>=0.31", "uvicorn[standard]>=0.30", "watchfiles>=0.22"]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
renforge = "renforge.cli:main"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build]
|
|
41
|
+
include = ["src/renforge", "src/renforge/**/*.rpy", "src/renforge/ui/static/**"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/renforge"]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.sdist]
|
|
47
|
+
include = ["src/renforge"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Activity feed helpers for MCP tool calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _coerce_project_root(project_root: str | Path) -> Path:
|
|
13
|
+
return Path(project_root).expanduser().resolve()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _coerce_files_touched(value: Any) -> list[str]:
|
|
17
|
+
if isinstance(value, str):
|
|
18
|
+
return [value]
|
|
19
|
+
if isinstance(value, (list, tuple, set)):
|
|
20
|
+
return [str(item) for item in value if isinstance(item, (str, Path))]
|
|
21
|
+
return []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _coerce_result_payload(result: Any) -> Any:
|
|
25
|
+
if isinstance(result, dict) and "ok" in result:
|
|
26
|
+
return result
|
|
27
|
+
if isinstance(result, (str, int, float, bool, list, type(None))):
|
|
28
|
+
return result
|
|
29
|
+
return str(result)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _coerce_payload(value: Any) -> Any:
|
|
33
|
+
if isinstance(value, (str, int, float, bool, type(None))):
|
|
34
|
+
return value
|
|
35
|
+
if isinstance(value, list):
|
|
36
|
+
return [_coerce_payload(item) for item in value]
|
|
37
|
+
if isinstance(value, dict):
|
|
38
|
+
return {str(k): _coerce_payload(v) for k, v in value.items()}
|
|
39
|
+
if isinstance(value, set):
|
|
40
|
+
return sorted(str(item) for item in value)
|
|
41
|
+
if isinstance(value, tuple):
|
|
42
|
+
return [_coerce_payload(item) for item in value]
|
|
43
|
+
return str(value)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def summarize_result(result: Any) -> dict[str, Any]:
|
|
47
|
+
if isinstance(result, dict):
|
|
48
|
+
ok = result.get("ok", not isinstance(result.get("error"), str))
|
|
49
|
+
files_touched: list[str] = []
|
|
50
|
+
for key in ("files_touched", "files", "changed_files", "changed", "file_touches"):
|
|
51
|
+
candidate = result.get(key)
|
|
52
|
+
if candidate:
|
|
53
|
+
files_touched = _coerce_files_touched(candidate)
|
|
54
|
+
break
|
|
55
|
+
return {"ok": bool(ok), "files_touched": files_touched, "result": result}
|
|
56
|
+
|
|
57
|
+
if isinstance(result, (str, int, float, bool, list, type(None))):
|
|
58
|
+
return {"ok": True, "files_touched": [], "result": result}
|
|
59
|
+
|
|
60
|
+
return {"ok": True, "files_touched": [], "result": str(result)}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def log_tool_call(
|
|
64
|
+
project_root: str | Path,
|
|
65
|
+
name: str,
|
|
66
|
+
params: dict[str, Any],
|
|
67
|
+
duration_ms: float,
|
|
68
|
+
result: Any,
|
|
69
|
+
files_touched: list[str] | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
summary = summarize_result(result)
|
|
72
|
+
entry = {
|
|
73
|
+
"ts": int(time.time() * 1000),
|
|
74
|
+
"name": name,
|
|
75
|
+
"params": _coerce_payload(params),
|
|
76
|
+
"duration_ms": duration_ms,
|
|
77
|
+
"ok": summary["ok"],
|
|
78
|
+
"result": _coerce_result_payload(summary["result"]),
|
|
79
|
+
"files_touched": files_touched or summary["files_touched"],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
root = _coerce_project_root(project_root)
|
|
83
|
+
if not root.exists() or not root.is_dir():
|
|
84
|
+
return
|
|
85
|
+
path = root / ".renforge" / "activity.jsonl"
|
|
86
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
|
|
88
|
+
payload = json.dumps(entry, ensure_ascii=False, separators=(",", ":"))
|
|
89
|
+
with path.open("a", encoding="utf-8") as file_obj:
|
|
90
|
+
file_obj.write(payload)
|
|
91
|
+
file_obj.write("\n")
|
|
92
|
+
file_obj.flush()
|
|
93
|
+
os.fsync(file_obj.fileno())
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Asset analysis: find orphaned and missing images/audio in a Ren'Py project.
|
|
2
|
+
|
|
3
|
+
Heuristic and deliberately conservative — Ren'Py's image resolution is dynamic
|
|
4
|
+
(``show eileen happy`` maps to a defined image or a file like
|
|
5
|
+
``images/eileen happy.png``), so this reports *likely* orphans/missing rather
|
|
6
|
+
than a proof. It reads the ``game/`` tree and the ``.rpy`` sources; the engine
|
|
7
|
+
stays the source of truth via ``lint``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif", ".tga", ".bmp"}
|
|
18
|
+
AUDIO_EXTS = {".ogg", ".opus", ".mp3", ".wav", ".flac", ".m4a", ".aac", ".mp2", ".wma"}
|
|
19
|
+
VIDEO_EXTS = {".webm", ".mp4", ".ogv", ".avi", ".mkv", ".mov", ".mpg", ".mpeg", ".flv"}
|
|
20
|
+
ASSET_EXTS = IMAGE_EXTS | AUDIO_EXTS | VIDEO_EXTS
|
|
21
|
+
|
|
22
|
+
_QUOTED_RE = re.compile(r"""["']([^"'\n]+?)["']""")
|
|
23
|
+
_IMAGE_DEF_RE = re.compile(r"^\s*image\s+([^\n=:]+?)\s*(?:=|:)")
|
|
24
|
+
_SCENE_SHOW_RE = re.compile(r"^\s*(?:scene|show)\s+(.+?)\s*(?:#.*)?$")
|
|
25
|
+
# Tokens that end the image-name part of a scene/show statement.
|
|
26
|
+
_SHOW_STOP = {"at", "with", "as", "behind", "onlayer", "zorder", "expression"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _game_dir(project_path: str | Path) -> Path:
|
|
30
|
+
return Path(project_path).expanduser().resolve() / "game"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _iter_rpy(game: Path):
|
|
34
|
+
for root, _dirs, files in os.walk(game):
|
|
35
|
+
for name in files:
|
|
36
|
+
if name.endswith(".rpy"):
|
|
37
|
+
yield Path(root) / name
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _image_name_from_show(rest: str) -> str:
|
|
41
|
+
tokens = rest.split()
|
|
42
|
+
keep: list[str] = []
|
|
43
|
+
for tok in tokens:
|
|
44
|
+
if tok in _SHOW_STOP:
|
|
45
|
+
break
|
|
46
|
+
keep.append(tok)
|
|
47
|
+
return " ".join(keep)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def analyze_assets(project_path: str | Path) -> dict[str, Any]:
|
|
51
|
+
game = _game_dir(project_path)
|
|
52
|
+
result: dict[str, Any] = {
|
|
53
|
+
"asset_files": [],
|
|
54
|
+
"orphans": [],
|
|
55
|
+
"missing_files": [],
|
|
56
|
+
"undefined_images": [],
|
|
57
|
+
}
|
|
58
|
+
if not game.is_dir():
|
|
59
|
+
result["error"] = f"no game/ directory under {project_path}"
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
# 1. Asset files on disk (relative to game/, posix-style).
|
|
63
|
+
disk_files: list[str] = []
|
|
64
|
+
for root, _dirs, files in os.walk(game):
|
|
65
|
+
for name in files:
|
|
66
|
+
if Path(name).suffix.lower() in ASSET_EXTS:
|
|
67
|
+
rel = (Path(root) / name).relative_to(game).as_posix()
|
|
68
|
+
disk_files.append(rel)
|
|
69
|
+
disk_files.sort()
|
|
70
|
+
result["asset_files"] = disk_files
|
|
71
|
+
|
|
72
|
+
# 2. References from the scripts.
|
|
73
|
+
quoted: set[str] = set()
|
|
74
|
+
defined_images: set[str] = set()
|
|
75
|
+
shown_images: set[str] = set()
|
|
76
|
+
for rpy in _iter_rpy(game):
|
|
77
|
+
try:
|
|
78
|
+
text = rpy.read_text(encoding="utf-8", errors="replace")
|
|
79
|
+
except OSError:
|
|
80
|
+
continue
|
|
81
|
+
for line in text.splitlines():
|
|
82
|
+
stripped = line.strip()
|
|
83
|
+
if not stripped or stripped.startswith("#"):
|
|
84
|
+
continue
|
|
85
|
+
for m in _QUOTED_RE.finditer(line):
|
|
86
|
+
quoted.add(m.group(1))
|
|
87
|
+
dm = _IMAGE_DEF_RE.match(line)
|
|
88
|
+
if dm:
|
|
89
|
+
defined_images.add(dm.group(1).strip())
|
|
90
|
+
sm = _SCENE_SHOW_RE.match(line)
|
|
91
|
+
if sm:
|
|
92
|
+
name = _image_name_from_show(sm.group(1))
|
|
93
|
+
if name:
|
|
94
|
+
shown_images.add(name)
|
|
95
|
+
|
|
96
|
+
quoted_basenames = {Path(q).name for q in quoted}
|
|
97
|
+
|
|
98
|
+
def _referenced(rel: str) -> bool:
|
|
99
|
+
base = Path(rel).name
|
|
100
|
+
stem = Path(rel).stem # image name candidate, e.g. "eileen happy"
|
|
101
|
+
if rel in quoted or base in quoted_basenames:
|
|
102
|
+
return True
|
|
103
|
+
if any(q.endswith(rel) or q.endswith(base) for q in quoted):
|
|
104
|
+
return True
|
|
105
|
+
# Image files referenced via `scene/show <name>` or `image <name>`.
|
|
106
|
+
if Path(rel).suffix.lower() in IMAGE_EXTS:
|
|
107
|
+
if stem in shown_images or stem in defined_images:
|
|
108
|
+
return True
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
result["orphans"] = [rel for rel in disk_files if not _referenced(rel)]
|
|
112
|
+
|
|
113
|
+
# 3. Missing: quoted references that look like *developer* asset paths but
|
|
114
|
+
# aren't on disk. We skip substitution patterns ("[prefix_]...") and the
|
|
115
|
+
# gui/ tree, whose images the GUI framework generates or renders by
|
|
116
|
+
# default — flagging those would be noise, not actionable findings.
|
|
117
|
+
disk_set = set(disk_files)
|
|
118
|
+
disk_basenames = {Path(f).name for f in disk_files}
|
|
119
|
+
for q in sorted(quoted):
|
|
120
|
+
if "[" in q or "]" in q:
|
|
121
|
+
continue
|
|
122
|
+
if q.startswith("gui/"):
|
|
123
|
+
continue
|
|
124
|
+
if Path(q).suffix.lower() in ASSET_EXTS:
|
|
125
|
+
if q not in disk_set and Path(q).name not in disk_basenames:
|
|
126
|
+
result["missing_files"].append(q)
|
|
127
|
+
|
|
128
|
+
# 4. Images shown but neither defined nor backed by a file (Ren'Py would use
|
|
129
|
+
# a placeholder). Reported separately as a soft signal.
|
|
130
|
+
disk_stems = {Path(f).stem for f in disk_files if Path(f).suffix.lower() in IMAGE_EXTS}
|
|
131
|
+
for name in sorted(shown_images):
|
|
132
|
+
if name in defined_images:
|
|
133
|
+
continue
|
|
134
|
+
# A single-tag show may resolve to "<first-tag>.png"; check tag stems too.
|
|
135
|
+
first = name.split()[0] if name.split() else name
|
|
136
|
+
if name in disk_stems or first in disk_stems or first in defined_images:
|
|
137
|
+
continue
|
|
138
|
+
result["undefined_images"].append(name)
|
|
139
|
+
|
|
140
|
+
result["summary"] = {
|
|
141
|
+
"asset_count": len(disk_files),
|
|
142
|
+
"orphan_count": len(result["orphans"]),
|
|
143
|
+
"missing_count": len(result["missing_files"]),
|
|
144
|
+
"undefined_image_count": len(result["undefined_images"]),
|
|
145
|
+
}
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
__all__ = ["analyze_assets"]
|