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.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +35 -0
  3. data/LICENSE +21 -0
  4. data/README.md +117 -0
  5. data/docs/architecture.md +125 -0
  6. data/docs/authoring-guide.md +206 -0
  7. data/docs/getting-started.md +183 -0
  8. data/docs/reference/api/active-project.md +22 -0
  9. data/docs/reference/api/agent.md +24 -0
  10. data/docs/reference/api/docs-generator.md +20 -0
  11. data/docs/reference/api/http-server.md +46 -0
  12. data/docs/reference/api/index.md +38 -0
  13. data/docs/reference/api/introspection.md +17 -0
  14. data/docs/reference/api/mcp-installer.md +26 -0
  15. data/docs/reference/api/mcp-server.md +27 -0
  16. data/docs/reference/api/mw-builder.md +14 -0
  17. data/docs/reference/api/mw-data-files.md +20 -0
  18. data/docs/reference/api/mw-dialogue-dsl.md +58 -0
  19. data/docs/reference/api/mw-i18n.md +20 -0
  20. data/docs/reference/api/mw-linter.md +18 -0
  21. data/docs/reference/api/mw-loader.md +26 -0
  22. data/docs/reference/api/mw-openmw-config.md +15 -0
  23. data/docs/reference/api/mw-operations.md +24 -0
  24. data/docs/reference/api/mw-preflight.md +17 -0
  25. data/docs/reference/api/mw-reference-index.md +21 -0
  26. data/docs/reference/api/mw-scaffolder.md +13 -0
  27. data/docs/reference/api/mw-script-blob.md +31 -0
  28. data/docs/reference/api/mw-script-extractor.md +17 -0
  29. data/docs/reference/api/operations.md +25 -0
  30. data/docs/reference/api/plugins.md +24 -0
  31. data/docs/reference/api/preferences.md +13 -0
  32. data/docs/reference/api/project-marker.md +23 -0
  33. data/docs/reference/api/providers.md +22 -0
  34. data/docs/reference/api/recents.md +17 -0
  35. data/docs/reference/api/ui.md +21 -0
  36. data/docs/reference/api/vcs.md +17 -0
  37. data/docs/reference/api/watcher.md +11 -0
  38. data/docs/reference/commands.md +271 -0
  39. data/docs/walkthrough.md +193 -0
  40. data/exe/esp +10 -0
  41. data/lib/esp/active_project.rb +71 -0
  42. data/lib/esp/agent.rb +104 -0
  43. data/lib/esp/cli/docs.rb +44 -0
  44. data/lib/esp/cli/i18n.rb +67 -0
  45. data/lib/esp/cli/mcp.rb +52 -0
  46. data/lib/esp/cli/plugins.rb +42 -0
  47. data/lib/esp/cli/refs.rb +137 -0
  48. data/lib/esp/cli/support.rb +87 -0
  49. data/lib/esp/cli.rb +317 -0
  50. data/lib/esp/docs_generator.rb +148 -0
  51. data/lib/esp/http_server.rb +232 -0
  52. data/lib/esp/introspection.rb +151 -0
  53. data/lib/esp/mcp_installer.rb +122 -0
  54. data/lib/esp/mcp_server.rb +465 -0
  55. data/lib/esp/mw/builder.rb +71 -0
  56. data/lib/esp/mw/data_files.rb +67 -0
  57. data/lib/esp/mw/dialogue_dsl.rb +209 -0
  58. data/lib/esp/mw/i18n.rb +113 -0
  59. data/lib/esp/mw/linter.rb +103 -0
  60. data/lib/esp/mw/loader.rb +130 -0
  61. data/lib/esp/mw/openmw_config.rb +138 -0
  62. data/lib/esp/mw/operations.rb +374 -0
  63. data/lib/esp/mw/preflight.rb +161 -0
  64. data/lib/esp/mw/reference_index.rb +182 -0
  65. data/lib/esp/mw/scaffolder.rb +197 -0
  66. data/lib/esp/mw/script_blob.rb +87 -0
  67. data/lib/esp/mw/script_extractor.rb +85 -0
  68. data/lib/esp/mw/tes3conv.rb +38 -0
  69. data/lib/esp/operations.rb +285 -0
  70. data/lib/esp/plugins.rb +75 -0
  71. data/lib/esp/preferences.rb +63 -0
  72. data/lib/esp/project_marker.rb +99 -0
  73. data/lib/esp/providers/anthropic.rb +74 -0
  74. data/lib/esp/providers/ollama.rb +102 -0
  75. data/lib/esp/providers/openai.rb +91 -0
  76. data/lib/esp/providers.rb +76 -0
  77. data/lib/esp/recents.rb +74 -0
  78. data/lib/esp/ui.rb +144 -0
  79. data/lib/esp/vcs.rb +112 -0
  80. data/lib/esp/version.rb +11 -0
  81. data/lib/esp/watcher.rb +55 -0
  82. data/lib/esp.rb +85 -0
  83. data/locales/en.yml +164 -0
  84. data/locales/fr.yml +10 -0
  85. 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 -->