bama
BAMA: Build Any Mac App. Agent skills to research, clone, design, build, optimize, and test native macOS apps with Swift and SwiftUI.
翻譯準備中
Comprehensive data-layer access to your Skyrim SE load order: read any record, author patches into a new plugin, create/remove records, and look up record schemas, Papyrus signatures, and distributor
翻譯準備中
技能C# · PowerShellGitHub @Avick3110自行安裝營運者未確認
試用過了嗎? 點一下就好,不需要登入。
Comprehensive data-layer access to your Skyrim SE load order: read any record, author patches into a new plugin, create/remove records, and look up record schemas, Papyrus signatures, and distributor grammars - in plain English, through Claude or Codex.
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
houseCARL is an MCP server that exposes a Skyrim Special Edition load order at the data layer. It reads a Mod Organizer 2 instance's profile and presents every plugin record, every Data-relative asset, the compiled Papyrus, the SKSE plugin layer and the SkyPatcher INI layer to an AI assistant as a set of tools. It is built on Mutagen. The host is Claude Code or OpenAI Codex.
A write produces a new plugin in a new MO2 mod folder. Editing an existing plugin in place is an opt-in that names the file. MO2 does not need to be running. No plugin file handle is held between calls.檢視完整 README ↗houseCARL is an MCP server that exposes a Skyrim Special Edition load order at the data layer. It reads a Mod Organizer 2 instance's profile and presents every plugin record, every Data-relative asset, the compiled Papyrus, the SKSE plugin layer and the SkyPatcher INI layer to an AI assistant as a set of tools. It is built on Mutagen. The host is Claude Code or OpenAI Codex.
A write produces a new plugin in a new MO2 mod folder. Editing an existing plugin in place is an opt-in that names the file. MO2 does not need to be running. No plugin file handle is held between calls.
| Process | One C#/.NET 10 executable, MCP over stdio |
| Substrate | Mutagen.Bethesda.Skyrim 0.54.4 |
| Tools | 31 |
| Skills | 7 |
| Record coverage | 133 record types, 242 sub-structures, 497 polymorphic arms, 280 enums, generated at build time |
| Licence | GPL-3.0-only |
Four rules. The surface follows from them.
All 31 tools carry the housecarl_ prefix.
| Substrate | Tools |
|---|---|
| Records | records check apply create remove forward copy write_seq create_plugin compact_plugin merge_plugins |
| Assets | asset_status place |
| NIF | nif_inspect nif_set |
| Papyrus | compile_script decompile_script |
| BSA | bsa_list bsa_extract bsa_repack |
| SkyPatcher | skypatcher_layer |
| SKSE | skse |
| Nexus | nexus_search nexus_mod nexus_graphql nexus_check_updates nexus_identify |
| Session | set_mo2_instance set_tool_path load_order_status update_status |
records is the read surface for the record plane. A read is composed from four axes:
| Axis | Decides | Values |
|---|---|---|
| SELECT | which records | formids types plugins (with defined_in) conflicts_only where references walk |
| SOURCE | whose version | the winner (default), a plugin filename active or not, a {file, mod} pair, the SkyPatcher overlay pre or post replay; versus names a second pole for delta and tree |
| PROJECT | the shape of the answer | identity summary fields rows everything aggregate delta tree chain info_order |
| TRANSPORT | the rendering | format (text, json, dense) limit offset max_chars counts_only to_file |
A write is composed from the op list, the lane and the transport:
| Axis | Values |
|---|---|
| Verb | Set Add Remove SetAtIndex InsertAtIndex ReplaceAll Merge CopyFrom |
| Lane | patch= a new plugin (default), into= an existing houseCARL patch, in_place= a named plugin overwritten, dry_run= |
| Transport | readback format max_chars |
One read. Every WEAP Requiem.esp touches whose damage, as Requiem.esp sets it, is 50 or more, reading two fields from that plugin. where_source="winner" and fields_source="winner" ask the same question of the load-order winner.
housecarl_records(
plugins = {"names": ["Requiem.esp"]},
types = ["WEAP"],
where = ["BasicStats.Damage >= 50"],
project = {"form": "fields", "fields": ["BasicStats.Damage", "Keywords"]})
One write. Two ops on one record, validated and stopped before disk.
housecarl_apply(
ops = [
{"formid": "013989:Skyrim.esm", "field_path": "BasicStats.Damage", "op": "Set", "value": "12"},
{"formid": "013989:Skyrim.esm", "field_path": "Keywords", "op": "Add", "value": "0A8668:Skyrim.esm"}],
patch = "IronSwordRebalance",
dry_run = true)
dry_run runs the full pipeline (winner resolution, schema pre-flight, every op applied in memory, the reference check) and stops before disk. It returns what would change, or the refusal the real call would return.
Predicates. where= accepts comparisons (BasicStats.Damage >= 50), EditorID tests (editorid startswith REQ_), the negated string operators (Name not contains Dagger, not startswith), flag tests over bit fields (BodyTemplate.FirstPersonFlags has Body, has_any, has_none), presence (VirtualMachineAdapter exists), membership from a file (formid not in @<path>), one link step (Perks->editorid startswith REQ_NULL_), a provenance term (winner = X.esp), a containment step (*parent.EditorID), and quantified steps over a list (Effects[*none].BaseEffect->editorid startswith REQ_, Effects[*count] > 2). Predicates are ANDed.
FormIDs. A record is addressed as XXXXXX:Plugin.esp, or in the runtime form the console, Papyrus log and crash log print (FExxxYYY, XX######), resolved against the current order. Reads accept both forms. Writes accept the plugin form only; a runtime form is refused with the plugin form to use.
Large results. Every response on the record plane carries an epoch stamp identifying the index build it was answered from. A result over the render limit is written in full to a JSONL file whose first line is a manifest, and the response names the file. The file re-enters a later call as formids=["@<absolute path>"], epoch-checked, and a plain file with one plugin filename per line scopes a call as plugins={"names": ["@<absolute path>"]}, or check's plugins=/exclude=; an inline plugin filename that starts with @ is written @@.
Findings. check runs derived-findings families over the order: errors (dangling FormLinks, missing masters, unparseable records), scripts (script properties the VMAD does not bind), dialogue (dialogue graph validation over seeded topics and quests — this family takes seeds, it does not sweep), facegen (per NPC: the mod winning the head .nif, the mod winning the face .dds, the plugin winning the record, the mismatch class). Each family's description states what it does not cover.
The schema is generated from Mutagen.Bethesda.Skyrim 0.54.4 by reflection at build time: 1,174 types, at full field depth, with per-field type, cardinality, writability, nullability and xEdit signature. The mutagen-reference skill carries the same generated data for offline lookup.
A record type Mutagen does not model is absent from the reference and from the tools. A request for it is refused by name as a library coverage gap. The server is published with trimming off: trimming a reflection-driven server strips types and loses coverage without an error.
Record identity is not writable. FormKey on a record, and ModKey and Master on the mod header and master references, are refused by the write pre-flight and reported as not writable by the reference.
Four layers below the plugin plane decide what the game runs. houseCARL reads all four.
place writes a winning override into a new mod folder and reports that the file is written and not yet winning.Data\SKSE\Plugins, resolved to its winning mod, with each DLL's declared version metadata read without loading it. Two audits: native Papyrus functions declared by scripts against the DLLs that must implement them; form references declared in SKSE configuration files against the load order.The SKSE and SkyPatcher tools report what a file declares. They do not observe the running game.
Two independent precedences decide an NPC's face: the VFS for the baked files, the load order for the record. check findings=["facegen"] joins them. Causes and fixes: docs/facegen.md.
Three lanes, mutually exclusive.
| Lane | Parameter | Target | Existing files |
|---|---|---|---|
| New patch (default) | patch= | A new mod folder houseCARL - <name> holding one plugin whose masters span every referenced plugin | Untouched |
| Extend | into= | An existing houseCARL patch; a record already in it is edited, one not in it is copied in from the winner first | The named patch only |
| In place | in_place= | The named plugin, including one houseCARL did not author; re-laid out on save as xEdit and the CK do | The named plugin is overwritten |
A patch name already taken by an earlier houseCARL patch is suffixed. A name matching a plugin on disk that the order is not loading is refused, naming the place and the file.
The in-place lane keeps no backup and has no undo. The first in-place write to a given plugin returns a confirmation instead of writing; the call is repeated with acknowledge=true. The acknowledgement covers the overwrite of that plugin only. The pre-flight and record verify run on every call. Before any change, the unedited plugin is re-serialized in memory, and the write is refused, file untouched, if any record would lose a subrecord or change its length; the refusal names the record and the subrecords. into= runs the same check on the patch it extends. Four measured losses that carry no information are allowed: an all-zero LTEX INAM below form version 43, a deleted REFR's NAME, an all-zero REFR XRMR, and a RACE PHWT written back as its own leading bytes with an all-zero tail cut.
Pre-flight. Every op is checked before any file is opened for writing: record type, field path, enum value, verb against cardinality, value range, writability, record identity, the target type of every FormLink. Each failing op is named and counted. One failing op refuses the whole call.
Readback. Each edited field is read back from the written file. readback=true deep-reads every touched record. The readback reports the file's content. The patch has no effect until it is enabled in MO2; MO2 adds a newly activated plugin at the end of the load order.
Beyond field edits. Create records with fresh FormIDs, including nested dialogue structures. Remove records and list entries. Forward a named plugin's version of a record as a winning override, or revert a record to vanilla. Copy an NPC's appearance closure into a standalone record with no dependency on the donor. Create an empty plugin. ESL-compact a plugin, carrying its FormID-keyed FaceGen and voice files. Merge plugins with collision-only renumbering. Unused masters are trimmed on every write.
Dialogue records have bookkeeping that a byte-valid insert does not satisfy: docs/dialogue.md.
Seven skills ship with the plugin: /housecarl:<name> in Claude Code, $housecarl in Codex. Each is a SKILL.md and a references/ tree. Reference corpora are read by grep, never loaded whole. Four of the seven — mutagen-reference, papyrus-reference, spid-authoring, kid-authoring — carry a references/index.jsonl mapping a name to the file that holds it; the two generated corpora carry the line as well.
| Skill | Carries |
|---|---|
mutagen-reference | The schema of every modelled record type: fields, types, cardinality, writability, enum values. Emitted by the same generator pass as the server's rulebook. |
papyrus-reference | Papyrus, SKSE and shipped SKSE-plugin API signatures (PapyrusUtil, JContainers, MCMHelper, po3, SkyUI), 7,345 entries, from papyrus-index. A function the corpus does not carry is reported as absent. |
skypatcher-authoring | SkyPatcher 6.4.1 INI grammar and the offline check of a draft INI through the overlay. |
spid-authoring | SPID 7.3.0 _DISTR.ini grammar. |
kid-authoring | KID 3.5.0 _KID.ini grammar. |
open-animation-replacer | OAR 3.0.0 config.json / user.json conditions, submod priorities, DAR _conditions.txt conversion. |
skse-plugin-authoring | Native SKSE plugin DLLs on CommonLibSSE-NG: lifecycle, event sinks, trampoline and Address Library hooks, native Papyrus functions, SE + AE + VR from one DLL. |
Dialogue and facegen are not skills. Their tools carry the bookkeeping, and the measured facts are in docs/dialogue.md and docs/facegen.md, cited from the tool descriptions.
houseCARL-<version>.zip from the latest release.houseCARL-Setup.exe.[1] Claude Code, [2] Codex, [3] Both, [4] Uninstall.
~/.claude/skills/housecarl/, server registered in ~/.claude.json. The terminal CLI and the desktop app's Code tab both read these.%LOCALAPPDATA%\houseCARL\server\, skills flat under ~/.agents/skills/ with a $housecarl entry point, server registered as [mcp_servers.housecarl] in ~/.codex/config.toml.q quits.ModOrganizer.ini. The instance can be changed at any time by asking.Flags, for an unattended run: --claude / --codex / --both pick the host, --yes skips the confirm at the plan, and --uninstall removes instead of installing. A run whose input is redirected has nobody to answer a question, so it stops and names the flag that answers it rather than assuming one.
Updating: quit Claude Code and Codex first. Setup cannot replace a server a session is running; if one is, it stops and says so.
The package is plain enough to install without the setup utility, which is the way out if the exe will not run on a machine. This produces the same layout setup produces.
Extract the zip and move the whole housecarl folder to ~/.claude/skills/housecarl, so that folder holds .claude-plugin/, server/ and skills/. Claude Code loads the skills out of it.
Register the server in ~/.claude.json, under the top-level mcpServers object, leaving everything else in the file alone:
"housecarl": {
"type": "stdio",
"command": "<full path to server\\housecarl-mcp.exe from step 1>",
"args": []
}
The path is the .exe, not the .dll, and its backslashes are doubled, as JSON requires.
Restart the host.
~/.claude.json is Claude Code's file — the terminal CLI and the desktop app's Code tab. The desktop app's plain chat is a separate surface with its own file, %APPDATA%\Claude\claude_desktop_config.json, which takes the same mcpServers entry; put houseCARL there as well if that is where you want it. Neither surface reads ~/.claude/claude_mcp_config.json, so editing that file changes nothing and leaves the host spawning whatever server it had registered before.
A provider that refuses the whole server at tools/list over JSON schema nesting depth: set HOUSECARL_MAX_SCHEMA_DEPTH on houseCARL's server entry to the depth that provider accepts (10, for the one that reports "exceeds the maximum nesting depth of 10 levels"). The published schemas are cut there and closed with a node saying nesting continues below and is accepted but is not spelled out; what the tools accept does not change. It applies to the server entry, so every model behind that entry gets the cut schemas — use a second entry to split them. Unset, the schemas publish in full, and a value that is not a whole number of 4 or more stops the server with one sentence naming the variable — below 4 a schema loses the parameters and parameter types every call is checked against.
Uninstalling: [4] Uninstall, or --uninstall with a host flag. It removes the skill folders it recorded installing, the server, the rest of the files a houseCARL package ships, and the housecarl entry in ~/.claude.json and ~/.codex/config.toml — each config copied to a .houseCARL.uninstall.bak beside it first, and every other byte in it left as it was. Anything under those locations that houseCARL did not install stays where it is, and the run names it. The saved MO2 instance sits beside the server and goes with it; the patches houseCARL wrote live in the MO2 mods folder and are left alone. When it is over, the run lists what it actually took off.
Requires a .NET 10.0 SDK, feature band 400 or later, Windows and PowerShell.
git clone https://github.com/Avick3110/houseCARL.git
cd houseCARL
./scripts/build-plugin.ps1
The script regenerates the rulebook, publishes the server framework-dependent with trimming off, bundles the skills, builds the setup utility, and packs release/houseCARL-<version>.zip. Install the output with houseCARL-Setup.exe, with claude --plugin-dir ./dist/housecarl, or through the bundled local marketplace descriptor. The script header has the details.
> point houseCARL at D:\Modding\ARR
> which plugins override Hulda's NPC record, and what does each change
records: formids=[<Hulda>], project={"form": "tree"}
> which line answers Hulda's greeting, and which plugin moved it
records: types=["DIAL"], where=["Quest = <quest>"], project={"form": "info_order"}
> set the iron sword's damage to 12, into a patch called IronRebalance
records: formids=["013989:Skyrim.esm"], project={"form": "fields", "fields": ["BasicStats.Damage"]}
apply: ops=[{"formid": "013989:Skyrim.esm", "field_path": "BasicStats.Damage", "op": "Set", "value": "12"}],
patch="IronRebalance", dry_run=true
apply: the same call without dry_run
> every weapon in the order whose winning damage is 50 or more, as a table
records: types=["WEAP"], where=["BasicStats.Damage >= 50"],
project={"form": "fields", "fields": ["BasicStats.Damage"]}, format="dense"
A write resolves the record's load-order winner and overrides it into the patch. The edit is made once, on that record; which plugins touched it before is what project={"form": "tree"} shows, not something the write repeats per plugin.
Each write lands as its own MO2 mod folder, houseCARL - <patch>. Refresh MO2, review the plugin, sort it, enable it. houseCARL does none of those.
One process, MCP over stdio. The load order is read from the MO2 profile's loadorder.txt, modlist.txt and plugins.txt. No USVFS, no hook into MO2. Records parse on access from Mutagen's lazy binary overlay. A structural index (FormKey to winning plugin and override count, plus the touching-plugin list for contested records) is built by opening each plugin once and disposing it; measured at 125–185 MB on a 3,400-plugin order. A call that needs record bodies opens each plugin at most once for the call. No plugin handle is held between calls, so MO2, xEdit and Explorer operate on the plugins normally while houseCARL runs.
Freshness is a last-write-plus-size check on every call. A rebuild is an immutable snapshot swapped in as one reference. A plugin the build cannot read is excluded whole with the reason, and every response from that build says which plugins are missing.
Writes are verified by a per-kind oracle: 17 cells, each performing one mutation two ways — through the reflection engine, and through a hand-written typed Mutagen setter — with the two output plugins required to be byte-identical. The engine is blind to which record it edits, so proving a kind proves every record carrying that kind. The oracle is run by hand against a real Skyrim.esm. 134 probe harnesses run in CI against real plugins.
The only outbound network use is the Nexus lookups. They need no account or API key. Offline, they say so, and every local tool keeps working. update_status reads MO2's local cache first and checks at the exact-file level.
decompile_script reconstructs .psc from .pex without an external tool. Measured on a 3,400-plugin order: 98.80% of 10,189 provable script pairs decompile and recompile byte-exact. A script it cannot prove is reported as such in the output.
| Path | What |
|---|---|
src/housecarl-mcp/ | The MCP server and tool surface |
src/housecarl-core/ | Record, asset, read and write engines; the load-order resolver |
src/housecarl-generator/ | Build-time schema generator; the probe runner |
src/housecarl-mcp-tests/ | xUnit tests against the built server |
src/housecarl-setup/ | Installer |
plugin/ | Plugin manifest, changelog, notices; skills are copied in at build |
.claude/skills/ | Skill sources |
docs/ | Architecture notes and decision records |
standards/ | Testing and naming |
dotnet build housecarl.sln -c Release
dotnet test src/housecarl-mcp-tests -c Release --no-build
Read CLAUDE.md before changing anything, then the note in docs/architecture/ for the subsystem. Decisions are recorded one per file in docs/decisions/. Contribution process: CONTRIBUTING.md.
2.0.0 replaces the 1.x tool surface with 31 tools built from one grammar. There is no alias layer and no deprecation window. A 1.x tool name is refused with one sentence naming its successor and the call shape to use. The rows those sentences come from are in src/housecarl-mcp/AliasTable.cs.
GPL-3.0-only. See LICENSE. Required by Mutagen (GPL-3.0-only, no linking exception), which houseCARL is built on and bundles. Third-party components and licences are listed in THIRD-PARTY-NOTICES.txt with corresponding-source pointers.
papyrus-reference.open-animation-replacer skill, and two earlier skills since retired.