esp-modkit 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +35 -0
- data/LICENSE +21 -0
- data/README.md +117 -0
- data/docs/architecture.md +125 -0
- data/docs/authoring-guide.md +206 -0
- data/docs/getting-started.md +183 -0
- data/docs/reference/api/active-project.md +22 -0
- data/docs/reference/api/agent.md +24 -0
- data/docs/reference/api/docs-generator.md +20 -0
- data/docs/reference/api/http-server.md +46 -0
- data/docs/reference/api/index.md +38 -0
- data/docs/reference/api/introspection.md +17 -0
- data/docs/reference/api/mcp-installer.md +26 -0
- data/docs/reference/api/mcp-server.md +27 -0
- data/docs/reference/api/mw-builder.md +14 -0
- data/docs/reference/api/mw-data-files.md +20 -0
- data/docs/reference/api/mw-dialogue-dsl.md +58 -0
- data/docs/reference/api/mw-i18n.md +20 -0
- data/docs/reference/api/mw-linter.md +18 -0
- data/docs/reference/api/mw-loader.md +26 -0
- data/docs/reference/api/mw-openmw-config.md +15 -0
- data/docs/reference/api/mw-operations.md +24 -0
- data/docs/reference/api/mw-preflight.md +17 -0
- data/docs/reference/api/mw-reference-index.md +21 -0
- data/docs/reference/api/mw-scaffolder.md +13 -0
- data/docs/reference/api/mw-script-blob.md +31 -0
- data/docs/reference/api/mw-script-extractor.md +17 -0
- data/docs/reference/api/operations.md +25 -0
- data/docs/reference/api/plugins.md +24 -0
- data/docs/reference/api/preferences.md +13 -0
- data/docs/reference/api/project-marker.md +23 -0
- data/docs/reference/api/providers.md +22 -0
- data/docs/reference/api/recents.md +17 -0
- data/docs/reference/api/ui.md +21 -0
- data/docs/reference/api/vcs.md +17 -0
- data/docs/reference/api/watcher.md +11 -0
- data/docs/reference/commands.md +271 -0
- data/docs/walkthrough.md +193 -0
- data/exe/esp +10 -0
- data/lib/esp/active_project.rb +71 -0
- data/lib/esp/agent.rb +104 -0
- data/lib/esp/cli/docs.rb +44 -0
- data/lib/esp/cli/i18n.rb +67 -0
- data/lib/esp/cli/mcp.rb +52 -0
- data/lib/esp/cli/plugins.rb +42 -0
- data/lib/esp/cli/refs.rb +137 -0
- data/lib/esp/cli/support.rb +87 -0
- data/lib/esp/cli.rb +317 -0
- data/lib/esp/docs_generator.rb +148 -0
- data/lib/esp/http_server.rb +232 -0
- data/lib/esp/introspection.rb +151 -0
- data/lib/esp/mcp_installer.rb +122 -0
- data/lib/esp/mcp_server.rb +465 -0
- data/lib/esp/mw/builder.rb +71 -0
- data/lib/esp/mw/data_files.rb +67 -0
- data/lib/esp/mw/dialogue_dsl.rb +209 -0
- data/lib/esp/mw/i18n.rb +113 -0
- data/lib/esp/mw/linter.rb +103 -0
- data/lib/esp/mw/loader.rb +130 -0
- data/lib/esp/mw/openmw_config.rb +138 -0
- data/lib/esp/mw/operations.rb +374 -0
- data/lib/esp/mw/preflight.rb +161 -0
- data/lib/esp/mw/reference_index.rb +182 -0
- data/lib/esp/mw/scaffolder.rb +197 -0
- data/lib/esp/mw/script_blob.rb +87 -0
- data/lib/esp/mw/script_extractor.rb +85 -0
- data/lib/esp/mw/tes3conv.rb +38 -0
- data/lib/esp/operations.rb +285 -0
- data/lib/esp/plugins.rb +75 -0
- data/lib/esp/preferences.rb +63 -0
- data/lib/esp/project_marker.rb +99 -0
- data/lib/esp/providers/anthropic.rb +74 -0
- data/lib/esp/providers/ollama.rb +102 -0
- data/lib/esp/providers/openai.rb +91 -0
- data/lib/esp/providers.rb +76 -0
- data/lib/esp/recents.rb +74 -0
- data/lib/esp/ui.rb +144 -0
- data/lib/esp/vcs.rb +112 -0
- data/lib/esp/version.rb +11 -0
- data/lib/esp/watcher.rb +55 -0
- data/lib/esp.rb +85 -0
- data/locales/en.yml +164 -0
- data/locales/fr.yml +10 -0
- metadata +241 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
Install the gem (puts `esp` on your `PATH`):
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
gem install esp-modkit
|
|
9
|
+
esp doctor # confirm Ruby, tes3conv, and the references index
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Native extensions (`sqlite3`, `zstd-ruby`) compile on install, so a C
|
|
13
|
+
toolchain is required. To hack on the toolchain itself, work from a clone
|
|
14
|
+
instead and put `bin/esp` on your `PATH`:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
git clone git@github.com:ninjacazaam/esp-modkit.git
|
|
18
|
+
cd esp-modkit
|
|
19
|
+
bundle install
|
|
20
|
+
bin/esp setup # git diff driver + pre-commit hook for this repo
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
You also need [tes3conv] on `PATH`. It is **not on Homebrew**; install
|
|
24
|
+
via a release binary or Cargo:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
# Option A — release binary (no Rust toolchain required)
|
|
28
|
+
# Download the macOS build from:
|
|
29
|
+
# https://github.com/Greatness7/tes3conv/releases
|
|
30
|
+
# then put `tes3conv` somewhere on PATH (e.g. /opt/homebrew/bin/).
|
|
31
|
+
|
|
32
|
+
# Option B — build from source with Rust
|
|
33
|
+
cargo install --git https://github.com/Greatness7/tes3conv
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Optional, only if you want to author mods in those languages:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
brew install python deno # python3 + deno (TypeScript)
|
|
40
|
+
nvm install --lts # node (JavaScript)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
[tes3conv]: https://github.com/Greatness7/tes3conv
|
|
44
|
+
|
|
45
|
+
## Index the vanilla data (one-time)
|
|
46
|
+
|
|
47
|
+
Lint, `refs find`, and the AI tools need a fast lookup table of vanilla
|
|
48
|
+
records. Build it once, **per user** — the index lives at
|
|
49
|
+
`$ESP_DATA_DIR/references/` (default: `~/.config/esp/references/`), so
|
|
50
|
+
a single index serves every project you author.
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
esp refs unpack # unpack Morrowind/Tribunal/Bloodmoon to JSON
|
|
54
|
+
esp refs index # build the SQLite index (~70k records, ~3s)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
You only need to re-run these if you upgrade Morrowind.
|
|
58
|
+
|
|
59
|
+
## First project, first mod
|
|
60
|
+
|
|
61
|
+
A **project** is any directory with a `.esp/project.json` marker. Make
|
|
62
|
+
one anywhere on disk; `esp` discovers it by walking up from wherever you
|
|
63
|
+
run the command.
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
mkdir -p ~/morrowind/MyFirstMod && cd ~/morrowind/MyFirstMod
|
|
67
|
+
esp init # writes .esp/project.json + git init + mods/ + dist/
|
|
68
|
+
|
|
69
|
+
esp new MyMod # mods/MyMod/MyMod.json + README
|
|
70
|
+
$EDITOR mods/MyMod/MyMod.json # author the records
|
|
71
|
+
|
|
72
|
+
esp lint MyMod # check refs against vanilla data
|
|
73
|
+
esp build MyMod # -> dist/MyMod.esp (in this project, not the toolchain)
|
|
74
|
+
esp install MyMod # registers with openmw.cfg
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Launch OpenMW; enable `MyMod.esp` in the launcher's content list.
|
|
78
|
+
|
|
79
|
+
`esp new` picks `json` by default. Use `--format rb|py|js|mjs|ts` for any
|
|
80
|
+
other authoring language; the scaffold drops in a working starter that
|
|
81
|
+
prints (or returns) one TES3 Header record. Author defaults to your
|
|
82
|
+
`git config user.name`; override with `--author "Some Name"`.
|
|
83
|
+
|
|
84
|
+
### Picking which project to operate on
|
|
85
|
+
|
|
86
|
+
`esp` resolves the project root, in order:
|
|
87
|
+
|
|
88
|
+
1. `--root PATH` flag on the command.
|
|
89
|
+
2. `ESP_PROJECT_ROOT` environment variable.
|
|
90
|
+
3. Walking up from the current directory to find a `.esp/project.json`
|
|
91
|
+
(git-style discovery — works from any subdirectory of the project).
|
|
92
|
+
4. As a last resort, the toolchain repo itself (with a one-line warning
|
|
93
|
+
on stderr explaining how to make this directory a project).
|
|
94
|
+
|
|
95
|
+
So `cd` into your project and run commands; or pass `--root` when
|
|
96
|
+
scripting across projects; or `export ESP_PROJECT_ROOT=~/morrowind/MyFirstMod`
|
|
97
|
+
to pin a default for a shell.
|
|
98
|
+
|
|
99
|
+
## Installing the built plugin
|
|
100
|
+
|
|
101
|
+
`esp install MyMod` registers the project's `dist/` directory with OpenMW
|
|
102
|
+
via `data=` + `content=` in `openmw.cfg`. Nothing is copied; OpenMW reads
|
|
103
|
+
your build output in place.
|
|
104
|
+
|
|
105
|
+
The original engine (Steam / GOG Morrowind) doesn't have that
|
|
106
|
+
`data=` mechanism — it loads `.esp` files from one fixed directory.
|
|
107
|
+
For those installations, pass `--copy-to` (explicit path) or
|
|
108
|
+
`--to-data-files` (auto-detects the Morrowind install):
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
esp install MyMod --to-data-files # copy to Morrowind/Data Files/
|
|
112
|
+
esp install MyMod --copy-to /some/path # copy to an explicit dir
|
|
113
|
+
esp install MyMod --no-register-openmw # skip the openmw.cfg part
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The two install paths compose — by default `--to-data-files` *also*
|
|
117
|
+
registers with OpenMW, so a multi-engine user gets both at once.
|
|
118
|
+
|
|
119
|
+
## Paths on macOS
|
|
120
|
+
|
|
121
|
+
| Thing | Default location |
|
|
122
|
+
|---|---|
|
|
123
|
+
| Vanilla references (per-user) | `~/.config/esp/references/` (override: `$ESP_REFERENCES_DIR` or `$ESP_DATA_DIR`) |
|
|
124
|
+
| Morrowind data | `~/Library/Application Support/Steam/steamapps/common/Morrowind/Data Files/` |
|
|
125
|
+
| OpenMW config | `~/Library/Preferences/openmw/openmw.cfg` |
|
|
126
|
+
| tes3conv binary | wherever `which tes3conv` points (Homebrew default: `/opt/homebrew/bin/tes3conv`) |
|
|
127
|
+
|
|
128
|
+
Override the Morrowind data path with `MORROWIND_DATA=/custom/path esp refs unpack`.
|
|
129
|
+
|
|
130
|
+
## Wiring esp into an AI client (MCP)
|
|
131
|
+
|
|
132
|
+
`esp` ships a [Model Context Protocol](https://modelcontextprotocol.io) server
|
|
133
|
+
so an AI assistant can author, build, lint, and query a mod project as native
|
|
134
|
+
tools. The server speaks JSON-RPC over stdio — you never run it by hand; the
|
|
135
|
+
client launches it.
|
|
136
|
+
|
|
137
|
+
Register it with one command:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
esp mcp install # Claude Code (writes .mcp.json in cwd)
|
|
141
|
+
esp mcp install --client claude-desktop # Claude Desktop
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- **Claude Code** writes a project-scoped `.mcp.json` in the current
|
|
145
|
+
directory with an absolute path to `bin/esp`; the server connects on
|
|
146
|
+
next launch (Claude Code prompts to approve a changed `.mcp.json`).
|
|
147
|
+
Run `esp mcp install` from your project root so the marker lands
|
|
148
|
+
beside `.esp/project.json`. The path is machine-specific —
|
|
149
|
+
`${CLAUDE_PROJECT_DIR}` is *not* an option here, since it's unset when
|
|
150
|
+
the config is parsed, so a server using it silently fails to launch.
|
|
151
|
+
- **Claude Desktop** merges into
|
|
152
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`,
|
|
153
|
+
also with an absolute path. Fully quit and restart Claude Desktop to
|
|
154
|
+
pick it up.
|
|
155
|
+
|
|
156
|
+
Re-running is idempotent (reports `unchanged`); other servers and keys
|
|
157
|
+
in the config are preserved. The by-hand equivalent for Claude Code is
|
|
158
|
+
`claude mcp add --transport stdio esp -- /path/to/bin/esp mcp serve`.
|
|
159
|
+
|
|
160
|
+
The MCP tools mirror the CLI's payloads byte-for-byte. They include
|
|
161
|
+
project management (`open_project`, `active_project`, `projects_new`,
|
|
162
|
+
`projects_recent`) so an AI client outside the toolchain repo — Claude
|
|
163
|
+
Desktop especially — can target an arbitrary project directory at runtime.
|
|
164
|
+
|
|
165
|
+
- **Discover** — `commands`, `version`.
|
|
166
|
+
- **Open a project** — `open_project`, `active_project`, `projects_recent`,
|
|
167
|
+
`projects_new`.
|
|
168
|
+
- **Author** — `scaffold`, `records_read`, `record_write`, `dialogue_write`,
|
|
169
|
+
`extract_scripts`.
|
|
170
|
+
- **Validate & build** — `lint`, `build`, `build_all`.
|
|
171
|
+
- **Vanilla data** — `refs_find`.
|
|
172
|
+
- **Game integration** — `unpack`, `install`, `plugins_list`.
|
|
173
|
+
|
|
174
|
+
The server also exposes **resources**: the `esp docs introspect` surface
|
|
175
|
+
and the generated reference docs, so a client can read the full
|
|
176
|
+
capability map without a tool call.
|
|
177
|
+
|
|
178
|
+
## Where to go next
|
|
179
|
+
|
|
180
|
+
- [Authoring guide](authoring-guide.md) — every source format, scripts, i18n.
|
|
181
|
+
- [Architecture](architecture.md) — how the pipeline fits together.
|
|
182
|
+
- [Command reference](reference/commands.md) — every flag, auto-generated.
|
|
183
|
+
- [API reference](reference/api/index.md) — library modules, auto-generated.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::ActiveProject
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/active_project.rb`
|
|
6
|
+
|
|
7
|
+
Process-scoped state for "which project is the user currently working in"
|
|
8
|
+
(step 23). Swapped in by ESPresso's POST /open-project; helpers that need
|
|
9
|
+
a working-tree root (vcs_root for diff/approve/reject today; more later)
|
|
10
|
+
consult this before falling back to Esp::ROOT.
|
|
11
|
+
|
|
12
|
+
Step 22.5 slice 2: also tracks the project's game id (`mw` today, future
|
|
13
|
+
`ob`/`sr`) so the plugin registry knows which Operations module owns each
|
|
14
|
+
op for the current request. Missing game on a project read defaults to
|
|
15
|
+
`mw` for backward compat (every pre-rename project is Morrowind).
|
|
16
|
+
|
|
17
|
+
Stored module-globally rather than per-request because the WEBrick backend
|
|
18
|
+
is single-tenant per-instance (one ESPresso ↔ one esp serve), so a global
|
|
19
|
+
is the right shape; the mutex guards against the thread-per-request model
|
|
20
|
+
racing a concurrent set + read.
|
|
21
|
+
|
|
22
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::Agent
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/agent.rb`
|
|
6
|
+
|
|
7
|
+
A hand-rolled tool-use loop. The agent authors mods by calling the same
|
|
8
|
+
curated tool surface MCP exposes (Esp::McpServer::TOOLS), each tool
|
|
9
|
+
dispatched to Esp::Operations.public_send(op, …) — so the in-house agent
|
|
10
|
+
and external MCP clients share one tool definition.
|
|
11
|
+
|
|
12
|
+
The LLM is reached through an injected **provider** (see Esp::Providers), so
|
|
13
|
+
the loop is provider-agnostic and fully testable with a stub and no live
|
|
14
|
+
key. When none is given, the default provider is built from the registry
|
|
15
|
+
(Esp::Providers.default_id). See .claude/roadmap/19-agent-workspace.md.
|
|
16
|
+
|
|
17
|
+
Transcript shape (neutral, owned by Agent; providers translate it to their
|
|
18
|
+
native wire format):
|
|
19
|
+
{ role: :user, text: String }
|
|
20
|
+
{ role: :assistant, text: String, tool_calls: [{id:, name:, input:}],
|
|
21
|
+
raw: <provider-native message, opaque to Agent> }
|
|
22
|
+
{ role: :tool, results: [{id:, content: String, is_error: bool}] }
|
|
23
|
+
|
|
24
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::DocsGenerator
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/docs_generator.rb`
|
|
6
|
+
|
|
7
|
+
Renders the data from Esp::Introspection into markdown files under
|
|
8
|
+
docs/reference/. Each file is split into two regions: an `esp:auto`
|
|
9
|
+
block (regenerated by `esp docs build`, enforced fresh by lefthook so
|
|
10
|
+
it can't drift) and a hand-written tail below it that survives rebuilds.
|
|
11
|
+
write_doc rewrites only the auto block and preserves the tail verbatim,
|
|
12
|
+
so an unchanged source reproduces the file byte-for-byte — that
|
|
13
|
+
idempotence is what lets the freshness hook keep diffing the whole file.
|
|
14
|
+
|
|
15
|
+
The marker literal kept the `mw:auto` token for one release so existing
|
|
16
|
+
generated files migrate cleanly: manual_tail accepts both the new
|
|
17
|
+
`esp:auto` marker and the legacy `mw:auto` one. After the next regen,
|
|
18
|
+
every file carries the new marker and the fallback is dead code.
|
|
19
|
+
|
|
20
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::HttpServer
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/http_server.rb`
|
|
6
|
+
|
|
7
|
+
Localhost HTTP API mirroring the CLI surface. A thin WEBrick shell over
|
|
8
|
+
Esp::Operations — same operations as `bin/esp` and `esp mcp serve`, same
|
|
9
|
+
payload shapes, different transport. Intended as the backing store for
|
|
10
|
+
the GUI. WEBrick is thread-per-request, so the long-lived /agent stream
|
|
11
|
+
doesn't block other routes.
|
|
12
|
+
|
|
13
|
+
Routes:
|
|
14
|
+
POST /agent — body: { prompt, provider?, model? } → text/event-stream
|
|
15
|
+
GET /up — { status, version } (liveness)
|
|
16
|
+
GET /version — { version }
|
|
17
|
+
GET /commands — full Esp::Introspection.command_tree
|
|
18
|
+
GET /providers — { providers:[{id,default_model,configured}], default }
|
|
19
|
+
GET /active-project — { root: String|null }
|
|
20
|
+
POST /open-project — body: { root } → { root } (sets active project)
|
|
21
|
+
GET /projects/recent — { projects: [{root,name,opened_at}, ...] }
|
|
22
|
+
POST /projects/new — body: { project, mod, format?, author? } → { root, mod, source }
|
|
23
|
+
GET /preferences — { mods_home, ... } (merged over defaults)
|
|
24
|
+
POST /preferences — body: { mods_home?, ... } → resolved prefs
|
|
25
|
+
POST /build — body: { mod, locale? }
|
|
26
|
+
POST /build-all — body: { locale? }
|
|
27
|
+
POST /unpack — body: { plugin, name? }
|
|
28
|
+
POST /install — body: { mod }
|
|
29
|
+
POST /lint — body: { mod }
|
|
30
|
+
POST /i18n/check — body: { mod }
|
|
31
|
+
POST /scaffold — body: { mod, format?, author?, description?, force? }
|
|
32
|
+
POST /extract-scripts — body: { mod }
|
|
33
|
+
POST /records/read — body: { mod }
|
|
34
|
+
POST /records/write — body: { mod, record }
|
|
35
|
+
POST /dialogue/write — body: { mod, spec }
|
|
36
|
+
GET /plugins — query: config?
|
|
37
|
+
GET /refs/find — query: q, type, like, limit, show
|
|
38
|
+
POST /refs/resolve — body: { ids: [String] } → { types: {id => type} }
|
|
39
|
+
GET /ollama/models — { models: [String] } from Ollama's /api/tags
|
|
40
|
+
GET /diff — query: scope?, root? (working-tree changes + diffs)
|
|
41
|
+
POST /approve — body: { paths, root? } (git add)
|
|
42
|
+
POST /reject — body: { paths, root? } (restore/delete — destructive)
|
|
43
|
+
|
|
44
|
+
CORS is wide open (Access-Control-Allow-Origin: *) — dev-only tool.
|
|
45
|
+
|
|
46
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# API reference
|
|
4
|
+
|
|
5
|
+
Core library modules. Shell modules are `Esp::<Name>`;
|
|
6
|
+
Morrowind-plugin modules are `Esp::Mw::<Name>`.
|
|
7
|
+
|
|
8
|
+
- [`Esp::ActiveProject`](active-project.md)
|
|
9
|
+
- [`Esp::Agent`](agent.md)
|
|
10
|
+
- [`Esp::DocsGenerator`](docs-generator.md)
|
|
11
|
+
- [`Esp::HttpServer`](http-server.md)
|
|
12
|
+
- [`Esp::Introspection`](introspection.md)
|
|
13
|
+
- [`Esp::McpInstaller`](mcp-installer.md)
|
|
14
|
+
- [`Esp::McpServer`](mcp-server.md)
|
|
15
|
+
- [`Esp::Operations`](operations.md)
|
|
16
|
+
- [`Esp::Plugins`](plugins.md)
|
|
17
|
+
- [`Esp::Preferences`](preferences.md)
|
|
18
|
+
- [`Esp::ProjectMarker`](project-marker.md)
|
|
19
|
+
- [`Esp::Providers`](providers.md)
|
|
20
|
+
- [`Esp::Recents`](recents.md)
|
|
21
|
+
- [`Esp::Ui`](ui.md)
|
|
22
|
+
- [`Esp::Vcs`](vcs.md)
|
|
23
|
+
- [`Esp::Watcher`](watcher.md)
|
|
24
|
+
- [`Esp::Mw::Builder`](mw-builder.md)
|
|
25
|
+
- [`Esp::Mw::DataFiles`](mw-data-files.md)
|
|
26
|
+
- [`Esp::Mw::DialogueDsl`](mw-dialogue-dsl.md)
|
|
27
|
+
- [`Esp::Mw::I18n`](mw-i18n.md)
|
|
28
|
+
- [`Esp::Mw::Linter`](mw-linter.md)
|
|
29
|
+
- [`Esp::Mw::Loader`](mw-loader.md)
|
|
30
|
+
- [`Esp::Mw::OpenmwConfig`](mw-openmw-config.md)
|
|
31
|
+
- [`Esp::Mw::Operations`](mw-operations.md)
|
|
32
|
+
- [`Esp::Mw::Preflight`](mw-preflight.md)
|
|
33
|
+
- [`Esp::Mw::ReferenceIndex`](mw-reference-index.md)
|
|
34
|
+
- [`Esp::Mw::Scaffolder`](mw-scaffolder.md)
|
|
35
|
+
- [`Esp::Mw::ScriptBlob`](mw-script-blob.md)
|
|
36
|
+
- [`Esp::Mw::ScriptExtractor`](mw-script-extractor.md)
|
|
37
|
+
|
|
38
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::Introspection
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/introspection.rb`
|
|
6
|
+
|
|
7
|
+
Builds a structured snapshot of the CLI command tree and the core
|
|
8
|
+
library modules. Two consumers:
|
|
9
|
+
|
|
10
|
+
- `esp docs build` renders this into markdown under docs/reference/.
|
|
11
|
+
- `esp docs introspect` emits it as JSON so AI tools, ESPresso,
|
|
12
|
+
and the MCP server can discover capabilities at runtime.
|
|
13
|
+
|
|
14
|
+
The introspection is read-only and cheap — it just walks Thor's
|
|
15
|
+
metadata and reads each lib/esp/{,mw/}*.rb header comment block.
|
|
16
|
+
|
|
17
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::McpInstaller
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/mcp_installer.rb`
|
|
6
|
+
|
|
7
|
+
Registers `esp mcp serve` with a local MCP client by merging a stdio
|
|
8
|
+
server entry into that client's config JSON, preserving any other servers
|
|
9
|
+
and top-level keys already there.
|
|
10
|
+
|
|
11
|
+
Deliberately kept out of Esp::Operations: rewriting a user's AI-client
|
|
12
|
+
config is a CLI/operator action, never something an agent should be able
|
|
13
|
+
to drive over MCP itself.
|
|
14
|
+
|
|
15
|
+
The two clients share the `mcpServers` / stdio shape and an identical
|
|
16
|
+
server entry; they differ only in where the config lives. The command is
|
|
17
|
+
an absolute path to `bin/esp` — note `${CLAUDE_PROJECT_DIR}` looks tempting
|
|
18
|
+
for a portable Claude Code entry but is unset when `.mcp.json` is parsed
|
|
19
|
+
(it's a hooks-only variable), so the server silently fails to launch. The
|
|
20
|
+
cost is a machine-specific path in the config.
|
|
21
|
+
|
|
22
|
+
Step-22.5 migration: the install action quietly drops any existing entry
|
|
23
|
+
named `mw` (the historical pre-rename name) when it writes the new `esp`
|
|
24
|
+
entry, so a re-install after the rename leaves a single, current entry.
|
|
25
|
+
|
|
26
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::McpServer
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/mcp_server.rb`
|
|
6
|
+
|
|
7
|
+
Model Context Protocol server over stdio, so AI assistants (Claude Code,
|
|
8
|
+
Claude Desktop, …) can author, build, lint, and query a mod project as
|
|
9
|
+
native tools. It is a thin shell over Esp::Operations — the same service
|
|
10
|
+
layer the HTTP API uses — so every tool returns the same payload the CLI
|
|
11
|
+
`--json` mode and `esp serve` already emit.
|
|
12
|
+
|
|
13
|
+
Transport is the MCP stdio convention: newline-delimited JSON-RPC 2.0,
|
|
14
|
+
one message per line, no embedded newlines. Requests carry an `id` and
|
|
15
|
+
get a response; notifications (no `id`) get none. The protocol stream
|
|
16
|
+
owns stdout, so all diagnostics go to stderr.
|
|
17
|
+
|
|
18
|
+
Operation errors (missing mod, no index, bad source) come back as a
|
|
19
|
+
tool result with `isError: true` — the model sees the message and can
|
|
20
|
+
recover. Only malformed protocol traffic (bad JSON, unknown method,
|
|
21
|
+
unknown tool) uses a JSON-RPC error response.
|
|
22
|
+
|
|
23
|
+
Alongside tools, the server exposes read-only **resources**
|
|
24
|
+
(`resources/list` + `resources/read`): the command-tree introspection
|
|
25
|
+
and the narrative docs, so an agent can pull context without a tool call.
|
|
26
|
+
|
|
27
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::Mw::Builder
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/mw/builder.rb`
|
|
6
|
+
|
|
7
|
+
Orchestrates mods/<Mod>/<Mod>.json -> dist/<Mod>.esp:
|
|
8
|
+
1. Load source JSON.
|
|
9
|
+
2. Run Esp::Mw::Preflight to inline text_source files and regenerate
|
|
10
|
+
Script record blobs.
|
|
11
|
+
3. Hand the canonical JSON to tes3conv via a tempfile (whose
|
|
12
|
+
extension must be .json — tes3conv sniffs by extension).
|
|
13
|
+
|
|
14
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::Mw::DataFiles
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/mw/data_files.rb`
|
|
6
|
+
|
|
7
|
+
Where Morrowind keeps its vanilla data files (`Morrowind.esm` and
|
|
8
|
+
friends, plus user-installed `.esp` plugins for the original engine).
|
|
9
|
+
Engine-agnostic — OpenMW reads them via `data=` in openmw.cfg; the
|
|
10
|
+
original engine reads them in place. The same set of probe paths
|
|
11
|
+
serves both:
|
|
12
|
+
|
|
13
|
+
- `esp refs unpack` reads the vanilla ESMs out of here.
|
|
14
|
+
- `esp install --to-data-files` copies a built plugin into here so
|
|
15
|
+
the original engine's launcher picks it up.
|
|
16
|
+
|
|
17
|
+
Detection is best-effort, per-OS Steam/GOG defaults. Anything
|
|
18
|
+
non-default is overridable via $MORROWIND_DATA or an explicit flag.
|
|
19
|
+
|
|
20
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::Mw::DialogueDsl
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/mw/dialogue_dsl.rb`
|
|
6
|
+
|
|
7
|
+
Block-style DSL for emitting Dialogue (DIAL) and DialogueInfo (INFO)
|
|
8
|
+
records without the bookkeeping. Used inside Ruby mod sources via the
|
|
9
|
+
loader-exposed `dialogue { ... }` helper.
|
|
10
|
+
|
|
11
|
+
The contract: a `dialogue` block returns a flat Array of records
|
|
12
|
+
that the author splats into their mod's records array. The DSL
|
|
13
|
+
handles:
|
|
14
|
+
|
|
15
|
+
- One DIAL record per `topic`, with `dialogue_type` carried through.
|
|
16
|
+
- INFO records under each topic, chained via prev_id/next_id in
|
|
17
|
+
author order — Morrowind evaluates filters top-down, so author
|
|
18
|
+
order is filter precedence.
|
|
19
|
+
- Speaker scoping via `speaker "Name" do ... end`. Nested blocks
|
|
20
|
+
inherit; an explicit `speaker:` on an info wins.
|
|
21
|
+
- i18n: a `t("key")` helper is available inside the block when the
|
|
22
|
+
loader passes an Esp::Mw::I18n instance.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
|
|
26
|
+
dialogue do
|
|
27
|
+
speaker "Hrisskar Flat-Foot" do
|
|
28
|
+
topic "Flat-Foot" do
|
|
29
|
+
info t("hrisskar.flat_foot.intro")
|
|
30
|
+
info t("hrisskar.flat_foot.legion"), pc_faction: "Imperial Legion"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
topic "AFSN_Tracker", type: :journal do
|
|
35
|
+
info "Stage 10 text", journal_index: 10
|
|
36
|
+
info "Stage 20 text", journal_index: 20
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Supported info kwargs:
|
|
41
|
+
speaker: speaker_id filter (overrides surrounding scope)
|
|
42
|
+
race: speaker_race filter
|
|
43
|
+
class: speaker_class filter (note the keyword clash —
|
|
44
|
+
use the string key or the `class_:` alias)
|
|
45
|
+
faction: speaker_faction filter
|
|
46
|
+
cell: speaker_cell filter
|
|
47
|
+
sex: :any | :female | :male
|
|
48
|
+
pc_faction: player_faction filter
|
|
49
|
+
pc_rank: player_rank filter
|
|
50
|
+
speaker_rank: NPC rank filter
|
|
51
|
+
disposition: minimum disposition
|
|
52
|
+
sound: sound_path
|
|
53
|
+
result_script: inline MWScript text to run on activation
|
|
54
|
+
result_script_source: path to .mwscript file (resolved relative to
|
|
55
|
+
the mod source dir at preflight time)
|
|
56
|
+
journal_index: for Journal-type topics — maps to data.disposition
|
|
57
|
+
|
|
58
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::Mw::I18n
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/mw/i18n.rb`
|
|
6
|
+
|
|
7
|
+
Locale-aware string lookup. Two interfaces:
|
|
8
|
+
|
|
9
|
+
- `t(key)` returns the resolved string (with default-locale fallback)
|
|
10
|
+
and tracks misses. Used directly from the Ruby DSL.
|
|
11
|
+
- `resolve!(value)` walks any data structure and replaces
|
|
12
|
+
`"@t:<key>"` sentinel strings via `t`. Used post-load for JSON or
|
|
13
|
+
subprocess-loader output (Python/JS/TS) where in-process helpers
|
|
14
|
+
can't reach.
|
|
15
|
+
|
|
16
|
+
Catalogues are nested-hash YAML files at
|
|
17
|
+
`mods/<Mod>/i18n/<locale>.yaml`. Lookup is dot-pathed:
|
|
18
|
+
`t("activator.stump")` reads `{activator: {stump: "..."}}`.
|
|
19
|
+
|
|
20
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::Mw::Linter
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/mw/linter.rb`
|
|
6
|
+
|
|
7
|
+
Checks a mod's records for dangling references and missing-master
|
|
8
|
+
coverage. Uses Esp::Mw::ReferenceIndex for vanilla lookups and the mod's
|
|
9
|
+
own records for self-references.
|
|
10
|
+
|
|
11
|
+
Severity model:
|
|
12
|
+
- :error — the ref points at nothing the mod or any indexed ESM defines.
|
|
13
|
+
- :warning — the ref resolves in a vanilla ESM that isn't listed in
|
|
14
|
+
the mod's TES3 Header.masters. OpenMW will load the mod
|
|
15
|
+
but log warnings; the mod author probably forgot to add
|
|
16
|
+
the dependency.
|
|
17
|
+
|
|
18
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::Mw::Loader
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/mw/loader.rb`
|
|
6
|
+
|
|
7
|
+
Loads a mod's source records from one of the supported formats.
|
|
8
|
+
|
|
9
|
+
The contract: every loader returns an Array of record hashes shaped
|
|
10
|
+
for tes3conv. Keys are normalised to strings so downstream code
|
|
11
|
+
(preflight, builder) doesn't have to care which format the source
|
|
12
|
+
was authored in.
|
|
13
|
+
|
|
14
|
+
Supported source files (per mod folder, exactly one):
|
|
15
|
+
mods/<Mod>/<Mod>.json — straight JSON
|
|
16
|
+
mods/<Mod>/<Mod>.rb — Ruby file; last expression is the Array
|
|
17
|
+
mods/<Mod>/<Mod>.py — Python script; prints JSON Array to stdout
|
|
18
|
+
mods/<Mod>/<Mod>.js — Node script; same contract
|
|
19
|
+
mods/<Mod>/<Mod>.mjs — Node ES module; same contract
|
|
20
|
+
mods/<Mod>/<Mod>.ts — Deno TypeScript; same contract
|
|
21
|
+
|
|
22
|
+
For subprocess loaders (py/js/mjs/ts) the interpreter runs with cwd
|
|
23
|
+
set to the mod's source directory, so relative file reads from the
|
|
24
|
+
script just work. The script's own path is passed as the last arg.
|
|
25
|
+
|
|
26
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::Mw::OpenmwConfig
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/mw/openmw_config.rb`
|
|
6
|
+
|
|
7
|
+
Reads and edits OpenMW's openmw.cfg. Knows the per-OS location of the
|
|
8
|
+
*user* config (the one OpenMW's launcher edits) and can parse the
|
|
9
|
+
data=/content= lines to enumerate installed plugins.
|
|
10
|
+
|
|
11
|
+
v1 targets the single user config; OPENMW_CONFIG (or an explicit path)
|
|
12
|
+
overrides it. The full global/local/user hierarchy and ?token? data
|
|
13
|
+
paths are not merged/expanded yet — see roadmap/17.
|
|
14
|
+
|
|
15
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::Mw::Operations
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/mw/operations.rb`
|
|
6
|
+
|
|
7
|
+
The Morrowind plugin's half of the service layer. Same contract as
|
|
8
|
+
Esp::Operations (string-keyed params hash in, plain Hash out) — these
|
|
9
|
+
are the ops that decode TES3 records, drive tes3conv, or otherwise know
|
|
10
|
+
what a "mod" is. Frontends never call this module directly; they route
|
|
11
|
+
through Esp::Operations.dispatch(op, input).
|
|
12
|
+
|
|
13
|
+
Every op resolves the project root from `Esp::ActiveProject.resolve`
|
|
14
|
+
(step 23.5 slice 1) so an `open_project` request actually moves where
|
|
15
|
+
build/lint/etc. operate — they no longer silently target Esp::ROOT.
|
|
16
|
+
Precedence: explicit `root:` in params > active project > Esp::ROOT.
|
|
17
|
+
|
|
18
|
+
InputError is aliased from the shell so plugin ops raise the same class
|
|
19
|
+
the frontends rescue. Loader / Preflight / Tes3conv error classes are
|
|
20
|
+
registered with Esp::Operations::ERROR_CODES at the bottom of this file
|
|
21
|
+
— they couldn't be in the shell table because the plugin hadn't loaded
|
|
22
|
+
yet when that table was built.
|
|
23
|
+
|
|
24
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::Mw::Preflight
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/mw/preflight.rb`
|
|
6
|
+
|
|
7
|
+
Resolves text_source into inline text and regenerates Script-record
|
|
8
|
+
subrecords (SCHD header + SCVR vars + SCDT bytecode placeholder) into
|
|
9
|
+
the form tes3conv expects. Mutates records in place.
|
|
10
|
+
|
|
11
|
+
Validation rules:
|
|
12
|
+
- Source must be pure ASCII (MWScript's compiler is silent on UTF-8).
|
|
13
|
+
- Variable identifiers must be <= 20 chars (MWScript truncates ~20-23).
|
|
14
|
+
- Warn (don't fail) when a script's `Begin <name>` doesn't match its
|
|
15
|
+
record id; vanilla often disagrees so this is intentionally soft.
|
|
16
|
+
|
|
17
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!-- esp:auto — regenerated by `esp docs build`; edits here are overwritten -->
|
|
2
|
+
|
|
3
|
+
# Esp::Mw::ReferenceIndex
|
|
4
|
+
|
|
5
|
+
**Source:** `lib/esp/mw/reference_index.rb`
|
|
6
|
+
|
|
7
|
+
SQLite index over the unpacked vanilla ESM JSONs. Replaces "grep over
|
|
8
|
+
200MB of JSON" with a millisecond keyed lookup.
|
|
9
|
+
|
|
10
|
+
Schema is intentionally narrow: just enough to answer "where is this
|
|
11
|
+
record defined?" and "what's its full JSON?". Full-record retrieval
|
|
12
|
+
falls back to reading the source ESM at the stored array index.
|
|
13
|
+
|
|
14
|
+
Storage location (step 23.5 slice 2): vanilla data is *not* per-project
|
|
15
|
+
— it's identical for every user, and ~150 MB of it. It lives at
|
|
16
|
+
`$ESP_REFERENCES_DIR` (explicit override), else
|
|
17
|
+
`$ESP_DATA_DIR/references/`, else `~/.config/esp/references/`. One
|
|
18
|
+
index across every project. The defaults resolve at call time, not at
|
|
19
|
+
require, so an env-var change between invocations takes effect.
|
|
20
|
+
|
|
21
|
+
<!-- /esp:auto — write durable docs below this line; they survive rebuilds -->
|