romdevtools 0.13.0 → 0.14.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.
- package/AGENTS.md +5 -5
- package/CHANGELOG.md +62 -1
- package/README.md +13 -8
- package/package.json +1 -1
- package/src/cheats/lookup.js +39 -18
- package/src/http/routes.js +58 -33
- package/src/http/skill-doc.js +10 -9
- package/src/http/swagger.js +1 -1
- package/src/http/tool-registry.js +72 -5
- package/src/mcp/server.js +6 -5
- package/src/mcp/state.js +8 -6
- package/src/mcp/tool-manifest.js +7 -7
- package/src/mcp/tools/cheats.js +4 -3
- package/src/mcp/tools/index.js +18 -2
- package/src/mcp/tools/playtest.js +48 -35
- package/src/mcp/tools/project.js +6 -51
- package/src/mcp/tools/rom-id.js +49 -4
- package/src/mcp/tools/tile-inspect.js +1 -1
- package/src/mcp/tools/trace-vram-source.js +3 -3
- package/src/mcp/tools/watch-memory.js +27 -46
- package/src/observer/livestream.html +7 -1
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +5 -5
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +1 -1
- package/src/platforms/gb/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gb/lib/c/README.md +2 -2
- package/src/platforms/gb/lib/c/SDCC_GOTCHAS.md +1 -1
- package/src/platforms/gbc/MENTAL_MODEL.md +3 -3
- package/src/platforms/gbc/TROUBLESHOOTING.md +5 -5
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +2 -2
- package/src/platforms/gbc/lib/c/README.md +2 -2
- package/src/platforms/gbc/lib/c/SDCC_GOTCHAS.md +1 -1
- package/src/playtest/playtest.js +25 -0
package/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@ You are reading this because romdev is connected. This is the orientation. Read
|
|
|
4
4
|
|
|
5
5
|
## What this server does
|
|
6
6
|
|
|
7
|
-
Drives the full homebrew ROM dev loop for 14 retro game platforms (NES, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis, Sega Master System, Game Gear, Atari 2600/7800, Atari Lynx, Commodore 64, PC Engine / TurboGrafx-16, and MSX / MSX2). Build → run → screenshot → inspect → patch → iterate. Also a strong reverse-engineering kit: disassemble existing ROMs into byte-exact rebuildable projects (`disasm({target:'project'})`/`disasm({target:'references'})` — the workhorse for any structural hack), find a value's address with the Cheat-Engine search loop (`memory({op:'search'})`/`memory({op:'searchNext'})`), find the EXACT instruction that wrote a RAM byte (`breakpoint({on:'write'})`, a core-level write watchpoint), confirm a patch is live in the running image (`memory({op:'readCart'})`), tell whether a "found table" is really ASCII (`memory({op:'classify'})`), trace which ROM offset a Genesis graphic was DMA'd from (`
|
|
7
|
+
Drives the full homebrew ROM dev loop for 14 retro game platforms (NES, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis, Sega Master System, Game Gear, Atari 2600/7800, Atari Lynx, Commodore 64, PC Engine / TurboGrafx-16, and MSX / MSX2). Build → run → screenshot → inspect → patch → iterate. Also a strong reverse-engineering kit: disassemble existing ROMs into byte-exact rebuildable projects (`disasm({target:'project'})`/`disasm({target:'references'})` — the workhorse for any structural hack), find a value's address with the Cheat-Engine search loop (`memory({op:'search'})`/`memory({op:'searchNext'})`), find the EXACT instruction that wrote a RAM byte (`breakpoint({on:'write'})`, a core-level write watchpoint), confirm a patch is live in the running image (`memory({op:'readCart'})`), tell whether a "found table" is really ASCII (`memory({op:'classify'})`), trace which ROM offset a Genesis graphic was DMA'd from (`watch({on:'dma', precision:'sampled'})`), drive menus by screen-change (`navigate`), and look up cheats (`cheats({op:'lookup'})`/`cheats({op:'search'})`: a free, crowd-sourced labeled RAM/code map for known ROMs), apply + create cheats, convert assets, study patterns from real games. **Doing a romhack? Start with `platform({op:'doc', platform:'romhacking', name:'playbook'})`** — the decision tree that wires all of the above together. Bundled WASM toolchains and emulator cores — no system dependencies, no installs.
|
|
8
8
|
|
|
9
9
|
You drive the work. The human is a director — they may want a game, a ROM disassembly, a tool-assisted reverse-engineering session, or anything else this server can do.
|
|
10
10
|
|
|
@@ -54,7 +54,7 @@ Skip playtest only when there's clearly no human in the loop: CI runs, automated
|
|
|
54
54
|
- `input` — drive controllers, look up hardware bit layouts. `navigate` walks menus by advancing on SCREEN CHANGE (not fixed frames) and reports whether each press was consumed — the fast, reliable way to script a UI.
|
|
55
55
|
- `state` — savestates and forensic state inspection (`state({op:'save'})`, `state({op:'load'})`, `state({op:'export'})` a slot to disk without touching the live host, `state({op:'list'})`, `state({op:'dump'})`)
|
|
56
56
|
- `memory` — read/write VRAM/OAM/CGRAM/ARAM and other regions (all 14 platforms). `memory({op:'read'})` takes `offsets:[…]` to batch scattered reads in one call. **`memory({op:'search'})`/`memory({op:'searchNext'})`** = the Cheat-Engine value-search loop ("find the address of X, narrow as X changes"). **`memory({op:'readCart'})`** reads the loaded cart image to confirm a patch is live. **`memory({op:'classify'})`** says whether bytes look like ASCII/code/tile-data (kills the "found table that's really a string" trap). `memory({op:'snapshot'})` + `memory({op:'diff'})` answer "which bytes changed across this event?" (diff defaults to a clustered summary with stride detection); `state({op:'diff'})` is the coarse whole-machine version.
|
|
57
|
-
- `debug` — `sprites({op:'inspect'})`, `palette({source:'live'})`, `cpu({op:'read'})` (all 14), `audioDebug({op:'inspect'})` (the 12 systems with a sound chip — all but Atari 2600/7800; pass `frames:N` to TRACE a per-channel note-timeline for headless melody asserts), `background({view:'renderState'})`, `breakpoint({on:'write'})` (write watchpoint, all 14), **`
|
|
57
|
+
- `debug` — `sprites({op:'inspect'})`, `palette({source:'live'})`, `cpu({op:'read'})` (all 14), `audioDebug({op:'inspect'})` (the 12 systems with a sound chip — all but Atari 2600/7800; pass `frames:N` to TRACE a per-channel note-timeline for headless melody asserts), `background({view:'renderState'})`, `breakpoint({on:'write'})` (write watchpoint, all 14), **`watch({on:'dma', precision:'sampled'})`** (Genesis: which ROM offset a VRAM graphic was DMA'd from), **`disasm({target:'bytes'|'rom'|'references'|'project'})`** (ALL 14 — native binutils objdump per CPU, incl. GBA ARM7/Thumb; the byte-exact `disasm({target:'project'})` reassembles through native as/ld/objcopy), `symbols({op})` lookup, `background({view:'rendered'})`, plus **`cheats({op})`** (`cheats({op:'lookup'})` = a free labeled RAM/code map for known ROMs, `cheats({op:'search'})` to fuzzy-find a game by name, `cheats({op:'apply'})`/`cheats({op:'clear'})` non-destructively, `cheats({op:'make'})` to create codes)
|
|
58
58
|
- `assets` — convert PNGs to tiles (`encodeArt`/`importArt`), WAVs to BRR, identify ROMs (`cart({op:'identify'})`), plus the hacking toolkit (`romPatch({op})` — write/writeMany/spliceCHR/relocate/makeStored/findFree/findPointer/diff, `assembleSnippet`, `cart({op:'extract'})`, `cart({op:'wrap'})`)
|
|
59
59
|
- `project` — starter snippets per platform
|
|
60
60
|
- `show` — `playtest({op})`: `op:'open'` opens the live SDL window for a human, `op:'stop'` closes it, `op:'status'` reports liveness, `op:'framebuffer'` captures exactly what the human's window shows
|
|
@@ -297,7 +297,7 @@ Different platforms have different levels of MCP-exposed debugging — different
|
|
|
297
297
|
> lookup/apply/create), `cpu({op:'read'})`, `memory({op:'search'})`/`memory({op:'searchNext'})`/`memory({op:'readCart'})`/`memory({op:'classify'})`,
|
|
298
298
|
> `memory({op:'snapshot'})`/`memory({op:'diff'})`/`state({op:'diff'})`, `watch({on:'mem'})`/`breakpoint({on:'write',precision:'sampled'})`.
|
|
299
299
|
> `audioDebug({op:'inspect'})` covers the 12 systems with a sound chip (all but Atari 2600/7800).
|
|
300
|
-
> **`
|
|
300
|
+
> **`watch({on:'dma', precision:'exact'})`** (which DMA wrote a VRAM tile, and from where) is **Genesis-only**
|
|
301
301
|
> (VDP DMA) — elsewhere use `breakpoint({on:'write'})`/`watch({on:'range'})`. All other RE tools above
|
|
302
302
|
> work on every platform that has the register-write/watch core hooks (all 14).
|
|
303
303
|
> `disasm({target:'rom'})` + `disasm({target:'references'})` + `disasm({target:'project'})` cover **all 14** — every
|
|
@@ -1044,10 +1044,10 @@ A few platform-tool quirks worth knowing up front:
|
|
|
1044
1044
|
- **asar bank-border-crossed** can happen if your `org` + `dw` runs past $00FFFF. Native vectors are at $FFE4-$FFEE; emulation vectors at $FFF4-$FFFF. Use `scaffold({op:'snippets', platform: "snes", mode: "get", name: "lorom_header.asm"})` for the layout.
|
|
1045
1045
|
- **cc65 (NES, C64, etc.) zero page** starts at $02. cc65 reserves $00-$01 for its runtime. Your first `.res 1` lands at $02, not $00. Use `symbols({op:'map'})` after `build({output:'romWithDebug'})` to confirm.
|
|
1046
1046
|
- **NES pattern table cap = 256 tiles per nametable**. The tilemap index is 8-bit, so per-frame BG can use at most 256 unique tiles per pattern table. Auto-converting a busy illustration usually overflows. `encodeArt({stage:'tilemap'})` warns; the only workaround is mid-frame CHR bank switching (MMC3-class mapper).
|
|
1047
|
-
- **NES + GB/GBC turnkey** (R9/R10 self-contained + sound, 2026-05-25): use `scaffold({op:'project', platform, template, name, path})` to scaffold a project. The pipeline copies every file the template depends on — `{nes,gb}_runtime.{h,c}`, `gb_hardware.h`, custom `crt0.s`, linker `.cfg`, `patch-header.js` (GB) — into the project directory alongside `main.c`. **No auto-injection at build time.** The build pipeline compiles exactly what you tell it via `sources` / `sourcesPaths` / `includes` / `includePaths` / `crt0` / `crt0Path` / `linkerConfig` / `codeLoc`. Take the project elsewhere with stock cc65/sdcc and it builds the same way. The runtime APIs include sprites, BG, input, AND **sound** — `sound_init` / `sound_play_tone(channel, period, vol, length)` / `sound_play_noise` / `sound_off`. NES drives pulse1+pulse2+triangle+noise via $4000-$400F + $4015; GB drives the 4-channel APU via NR10-NR52. SFX-grade, fire-and-forget — for full music tracks, drop in famitone2 (NES) or your own driver. Templates: `default` (palette cycle), `hello_sprite` (sprite + d-pad + **beep on A press**), `tile_engine` (multi-room tile map). Docs: [`src/platforms/nes/MENTAL_MODEL.md`](src/platforms/nes/MENTAL_MODEL.md) + [`TROUBLESHOOTING.md`](src/platforms/nes/TROUBLESHOOTING.md); [`src/platforms/gb/MENTAL_MODEL.md`](src/platforms/gb/MENTAL_MODEL.md) + [`TROUBLESHOOTING.md`](src/platforms/gb/TROUBLESHOOTING.md). **Game-loop order matters on NES:** stage `oam_clear`+`oam_spr` BEFORE `ppu_wait_nmi`, not after — the NMI handler DMA's whatever shadow_oam contains at vblank-start. **GB ROM header:** both asm and C builds now auto-run `rgbfix` inside `build({output:'rom'})`, so the Nintendo logo + checksums + CGB flag are correct out of the box — no manual `
|
|
1047
|
+
- **NES + GB/GBC turnkey** (R9/R10 self-contained + sound, 2026-05-25): use `scaffold({op:'project', platform, template, name, path})` to scaffold a project. The pipeline copies every file the template depends on — `{nes,gb}_runtime.{h,c}`, `gb_hardware.h`, custom `crt0.s`, linker `.cfg`, `patch-header.js` (GB) — into the project directory alongside `main.c`. **No auto-injection at build time.** The build pipeline compiles exactly what you tell it via `sources` / `sourcesPaths` / `includes` / `includePaths` / `crt0` / `crt0Path` / `linkerConfig` / `codeLoc`. Take the project elsewhere with stock cc65/sdcc and it builds the same way. The runtime APIs include sprites, BG, input, AND **sound** — `sound_init` / `sound_play_tone(channel, period, vol, length)` / `sound_play_noise` / `sound_off`. NES drives pulse1+pulse2+triangle+noise via $4000-$400F + $4015; GB drives the 4-channel APU via NR10-NR52. SFX-grade, fire-and-forget — for full music tracks, drop in famitone2 (NES) or your own driver. Templates: `default` (palette cycle), `hello_sprite` (sprite + d-pad + **beep on A press**), `tile_engine` (multi-room tile map). Docs: [`src/platforms/nes/MENTAL_MODEL.md`](src/platforms/nes/MENTAL_MODEL.md) + [`TROUBLESHOOTING.md`](src/platforms/nes/TROUBLESHOOTING.md); [`src/platforms/gb/MENTAL_MODEL.md`](src/platforms/gb/MENTAL_MODEL.md) + [`TROUBLESHOOTING.md`](src/platforms/gb/TROUBLESHOOTING.md). **Game-loop order matters on NES:** stage `oam_clear`+`oam_spr` BEFORE `ppu_wait_nmi`, not after — the NMI handler DMA's whatever shadow_oam contains at vblank-start. **GB ROM header:** both asm and C builds now auto-run `rgbfix` inside `build({output:'rom'})`, so the Nintendo logo + checksums + CGB flag are correct out of the box — no manual header-patch step needed (use `romPatch({op:'gbHeader'})` only to fix up an external ROM).
|
|
1048
1048
|
- **Game Boy / GBC silent-failure footguns** (R54 cleanup, full detail in `platform({op:'doc', platform:"gb"|"gbc", name:"mental_model"})`):
|
|
1049
1049
|
- **The bundled `gb_crt0.s` is now actually linked.** Pre-r54 a fundamental bug in `buildZ80C` was shipping the raw .s text to sdld as if pre-assembled — sdld silently rejected it and fell back to SDCC's stock sm83 crt0 (no GB cart boot, no IRQ vectors). Map showed no `init` symbol, $0000 was $FF, $0100 was $FF. Every GB ROM ran on stock crt0 invisibly. Fixed by auto-detecting .s source vs .rel object and running it through sdasgb first. Post-fix: `init` at $0150, entry $0100 = `00 c3 50 01` (nop; jp $0150), reset vector $0000 = $C9. **This was the root cause for #14 audio AND part of why every previous "runtime should work OOTB" round still felt friction-heavy.**
|
|
1050
|
-
- **GB/GBC C builds now auto-fix the header at build time** (rgbfix runs inside `build({output:'rom'})`): Nintendo logo at $0104, header checksum at $014D, global checksum, and the CGB flag at $0143 ($00 for `.gb`, $C0 for `.gbc`). You no longer need to
|
|
1050
|
+
- **GB/GBC C builds now auto-fix the header at build time** (rgbfix runs inside `build({output:'rom'})`): Nintendo logo at $0104, header checksum at $014D, global checksum, and the CGB flag at $0143 ($00 for `.gb`, $C0 for `.gbc`). You no longer need to patch the header manually — the ROM `build({output:'rom'})` hands back boots on real hardware as-is. `romPatch({op:'gbHeader', path})` still exists if you want to override title / cart type / RAM size / etc. on an existing file.
|
|
1051
1051
|
- **`shadow_oam` is pinned at $C100** in the bundled `gb_runtime.c` via `__at(0xC100)`. OAM DMA reads ONLY the high byte and copies 160 bytes from `$XX00` — a plain `uint8_t my_oam[160]` may land at $C017 and DMA garbage. If you roll your own OAM buffer, pick an address with `0x00` low byte (e.g. $C200) and pass it directly to `oam_dma_copy`.
|
|
1052
1052
|
- **Call `enable_vblank_irq()` once at boot.** Without it, `wait_vblank()` busy-polls `LY` which updates only at WASM `frame({op:'step'})` quantum boundaries → game loop runs at ~1/30 intended speed on the emulator. After enable, `wait_vblank()` compiles to `HALT` + vblank IRQ wake (~10 cycles per frame).
|
|
1053
1053
|
- **Use `memcpy_vram(dst, src, n)` for VRAM bulk writes**, NOT raw `(uint8_t*)0x8000` casts — SDCC sm83 may elide the latter as dead code. The bundled `gb_hardware.h` declares every $FFxx register as `volatile`-typed so direct writes like `BGP = 0xE4;` are fine; the hazard is only on cast-through-pointer block copies.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,67 @@ All notable changes to `romdevtools`. Dates are release dates.
|
|
|
4
4
|
(Published as `romdev-mcp` through 0.11.0; renamed to `romdevtools` in 0.13.0 —
|
|
5
5
|
the `romdev-mcp` bin is kept as an alias.)
|
|
6
6
|
|
|
7
|
+
## 0.14.0
|
|
8
|
+
|
|
9
|
+
**Two platform-specific top-level tools folded into their domain verbs, a
|
|
10
|
+
device-free cheat search, and skill-surface polish.** Pre-1.0 the surface keeps
|
|
11
|
+
consolidating with no deprecated aliases (call `catalog({op:'whatsNew'})` for the
|
|
12
|
+
live OLD→NEW map), so the tool count drops 34 → 32.
|
|
13
|
+
|
|
14
|
+
### Changed (breaking — pre-1.0, no aliases)
|
|
15
|
+
- **`patchGbHeader` → `romPatch({op:'gbHeader'})`.** It's a ROM-file patch, same
|
|
16
|
+
family as romPatch's other ops — not a standalone Game-Boy tool. Same params
|
|
17
|
+
(path/outputPath/cgb/title/cartType/romSize/ramSize/destination), same output.
|
|
18
|
+
- **`dmaTrace` → `watch({on:'dma'})`.** The Genesis VDP-DMA trace is a log-all
|
|
19
|
+
trace like `watch`'s on:'mem'/'range'/'pc' — now a fourth `on`. Same
|
|
20
|
+
`precision:'exact'|'sampled'` + filters.
|
|
21
|
+
- **`cheats({op:'search'})` no longer needs `platform`.** Omit it and the search
|
|
22
|
+
sweeps EVERY indexed platform; each match reports its own `platform`. You don't
|
|
23
|
+
have to know the console to find a game's cheats. Pass `platform` only to scope
|
|
24
|
+
the search to one console.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **`GET /skills/romdev/SKILL.md`** is now the primary skill URL — it mirrors the
|
|
28
|
+
on-disk path agents save skills to (`~/.claude/skills/romdev/SKILL.md`) exactly.
|
|
29
|
+
`/romdev/SKILL.md` and the flat `/romdev-skill.md` are kept as aliases.
|
|
30
|
+
- **The `/livestream` observer header now links to `/documentation`** (the live
|
|
31
|
+
Swagger console), so a human watching can jump to the API docs.
|
|
32
|
+
|
|
33
|
+
### Fixed (HTTP transport — failures are now unmissable)
|
|
34
|
+
- **A failed tool call returns a non-2xx, never a 200 with the error in the body.**
|
|
35
|
+
Tools that signal failure by RETURNING a failure-shaped result ({ok:false} /
|
|
36
|
+
{opened:false} / {applied:false} / a top-level `error`) — not just by throwing —
|
|
37
|
+
now map to HTTP 400 uniformly for EVERY tool. Before, e.g. `playtest` returning
|
|
38
|
+
`{opened:false}` came back 200, so an agent driving the REST/skill surface saw
|
|
39
|
+
"success," never read the body, and reported "window's up!" while no window
|
|
40
|
+
existed. (Valid "no"/state answers — `notSupported`, `matched:false`,
|
|
41
|
+
`loaded:false` — correctly stay 200.) A failed `playtest` window-open also now
|
|
42
|
+
logs to the server console so a human at the terminal sees it regardless.
|
|
43
|
+
- **`x-romdev-session` is now REQUIRED — a missing header returns 401**, instead
|
|
44
|
+
of the server auto-minting a throwaway one-shot session (which silently dropped
|
|
45
|
+
the loaded ROM and surfaced as "No ROM loaded" a couple calls later). The 401
|
|
46
|
+
message tells the agent to pick one stable id and send it every call.
|
|
47
|
+
|
|
48
|
+
### Fixed (playtest — no more invisible windows)
|
|
49
|
+
- **`playtest({op:'open'})` now FAILS LOUDLY when there's no real display** instead
|
|
50
|
+
of reporting `opened:true` for a window nothing can see. If the server's SDL
|
|
51
|
+
comes up on the `offscreen`/`dummy` video driver (no desktop session — server
|
|
52
|
+
started over SSH, from a tty, before the desktop login, or as a headless agent
|
|
53
|
+
subprocess), the window would render and play audio but never appear on a
|
|
54
|
+
screen. We now detect the selected driver via SDL itself
|
|
55
|
+
(`sdl.info.drivers.video.current`) — ground truth, cross-platform (Linux/macOS/
|
|
56
|
+
Windows), and it correctly ALLOWS a virtual display like Xvfb (reports `x11`,
|
|
57
|
+
not `offscreen`). On the offscreen case the open throws (→ 400 / MCP error) with
|
|
58
|
+
the exact fix: run the server from inside your logged-in desktop session.
|
|
59
|
+
Headless tools (screenshot/runSource/inspect) are unaffected — offscreen stays
|
|
60
|
+
fine for everything except opening a window for a human.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
- **The skill/HTTP session docs now coach a UNIQUE, task-descriptive
|
|
64
|
+
`x-romdev-session` id** (e.g. `nes-platformer-build`) — the id is the label a
|
|
65
|
+
human sees in `/livestream`, and it's how several agents share one server
|
|
66
|
+
without clobbering each other's emulator host.
|
|
67
|
+
|
|
7
68
|
## 0.13.0
|
|
8
69
|
|
|
9
70
|
**Renamed `romdev-mcp` → `romdevtools` + a plain-HTTP tool surface and an Agent
|
|
@@ -34,7 +95,7 @@ duplication). Same Express app, same localhost trust, per-agent dynamic sessions
|
|
|
34
95
|
via zod→JSON-Schema (the same conversion MCP `tools/list` uses).
|
|
35
96
|
- **`GET /documentation`** — Swagger UI over the spec: a live "try it" console.
|
|
36
97
|
- **`GET /tool/{name}/schema`** — that tool's JSON Schema (a validator on demand).
|
|
37
|
-
- **`GET /romdev
|
|
98
|
+
- **`GET /skills/romdev/SKILL.md`** — the Agent Skills open-standard SKILL.md
|
|
38
99
|
(frontmatter + workflow guide + generated tool reference). ~100 tokens of
|
|
39
100
|
name+description until invoked vs the always-on MCP tool defs — the on-demand
|
|
40
101
|
context win. Works in Claude Code, opencode, OpenClaw, Hermes, etc. unchanged.
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ npx romdevtools
|
|
|
9
9
|
That's it — one command starts the local romdev **tool server** (no global install, no host compiler/emulator). Point any coding agent at it three ways:
|
|
10
10
|
|
|
11
11
|
- **Plain HTTP** — `POST http://127.0.0.1:7331/tool/{name}`; browse/try every tool at `/documentation`.
|
|
12
|
-
- **Agent Skill** — `GET /romdev
|
|
12
|
+
- **Agent Skill** — `GET /skills/romdev/SKILL.md` (the [Agent Skills](https://agentskills.io) standard; save it to your skills dir as `skills/romdev/SKILL.md`; ~100 tokens until invoked).
|
|
13
13
|
- **MCP** — it's also a [Model Context Protocol](https://modelcontextprotocol.io/) server at `/mcp` for clients that want it.
|
|
14
14
|
|
|
15
15
|
This package contains all the JavaScript — the tool surface, the WASM emulator host, the per-platform scaffolds, runtime/library source, and debug helpers — but **no emulator or compiler WASM itself.** Those ship in the `romdev-*` binary packages it depends on, loaded on demand the first time you build or run a given platform.
|
|
@@ -19,7 +19,7 @@ This package contains all the JavaScript — the tool surface, the WASM emulator
|
|
|
19
19
|
## What's in this package
|
|
20
20
|
|
|
21
21
|
- **`bin`**
|
|
22
|
-
- `romdevtools` → the tool server (`src/mcp/server.js`). Serves the HTTP tool routes, `/documentation`, `/romdev
|
|
22
|
+
- `romdevtools` → the tool server (`src/mcp/server.js`). Serves the HTTP tool routes, `/documentation`, `/skills/romdev/SKILL.md`, and an MCP endpoint on `http://127.0.0.1:7331` by default (`PORT` / `HOST` to override). `romdev-mcp` is kept as an alias of the same command.
|
|
23
23
|
- `romdevtools-cli` → a smoke/utility CLI, incl. `romdevtools-cli play <rom>` (SDL window, hot-plug controllers).
|
|
24
24
|
- **`src/`** — the server, MCP tools, WASM host, core/toolchain resolvers, per-platform memory interpretation, and bundled library/runtime source (cc65 libs, PVSnesLib, SGDK, libtonc/libgba, hUGEDriver, …) that scaffolded projects link against.
|
|
25
25
|
- **`examples/`** — per-platform starter projects and genre scaffolds.
|
|
@@ -58,12 +58,17 @@ running server:
|
|
|
58
58
|
- **Plain HTTP:** `POST http://127.0.0.1:7331/tool/{name}` with the args as a JSON
|
|
59
59
|
body; the response is JSON. Browse/try every tool at **`/documentation`**
|
|
60
60
|
(Swagger UI, served locally — no CDN), or get the machine spec at
|
|
61
|
-
**`/openapi.json`**.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
**`/openapi.json`**. **The agent picks its own session id** and sends it as the
|
|
62
|
+
`x-romdev-session` header on every call — it's **required** (no header → `401`;
|
|
63
|
+
the server won't silently run you in a throwaway session). Make it unique and
|
|
64
|
+
task-descriptive (e.g. `nes-platformer-build`), since it's also the label shown
|
|
65
|
+
in the `/livestream` observer. The emulator host is per-session, so the same id
|
|
66
|
+
keeps your ROM across calls, and several agents can share one server by each
|
|
67
|
+
using a different id. A call that fails returns a non-2xx (4xx) with the reason
|
|
68
|
+
in the body — never a 200 that hides an error. romdev runs **locally** and tool
|
|
69
|
+
path args (`path`, `outputPath`, …) are **local filesystem paths**, not uploads
|
|
70
|
+
— pass absolute paths on the same machine.
|
|
71
|
+
- **Agent Skill:** **`GET /skills/romdev/SKILL.md`** is a portable [Agent
|
|
67
72
|
Skills](https://agentskills.io) `SKILL.md` (works in Claude Code, opencode,
|
|
68
73
|
OpenClaw, Hermes, …). Drop it in your agent's skills dir; it costs ~100 tokens
|
|
69
74
|
until invoked (vs always-on MCP tool defs), then teaches the workflows + the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "romdevtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Tool server giving coding agents full control of homebrew ROM development AND reverse-engineering/romhacking across 14 retro platforms (NES, SNES, GB, Genesis, Atari, C64, PC Engine, MSX, ...) via WASM toolchains + emulator cores. Use over plain HTTP, as an Agent Skill, or as an MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp/server.js",
|
package/src/cheats/lookup.js
CHANGED
|
@@ -121,37 +121,58 @@ function baseName(name) {
|
|
|
121
121
|
.trim();
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// The platforms that ship a bundled cheat index — the set searchCheatGames sweeps
|
|
125
|
+
// when no `platform` is given. (Kept in step with cheats.js SUPPORTED; C64 has no
|
|
126
|
+
// source cheats so it has no index.)
|
|
127
|
+
const INDEXED_PLATFORMS = [
|
|
128
|
+
"nes", "gb", "gbc", "snes", "genesis", "sms", "gg",
|
|
129
|
+
"atari2600", "atari7800", "lynx", "gba", "pce", "msx",
|
|
130
|
+
];
|
|
131
|
+
|
|
124
132
|
/**
|
|
125
|
-
* Fuzzy-search
|
|
126
|
-
*
|
|
127
|
-
*
|
|
133
|
+
* Fuzzy-search the cheat DB by game NAME. Each match carries the `platform` it
|
|
134
|
+
* was found on, so the caller never has to know the console up front. By default
|
|
135
|
+
* this searches EVERY indexed platform (the natural ask: "find a game's cheats"
|
|
136
|
+
* — you shouldn't have to name the device); pass `platform` only to scope the
|
|
137
|
+
* search to one console. Returns best-matching game names + cheat counts WITHOUT
|
|
138
|
+
* dumping the whole DB.
|
|
128
139
|
* @param {object} a
|
|
129
|
-
* @param {string} a.platform
|
|
130
140
|
* @param {string} a.query
|
|
141
|
+
* @param {string} [a.platform] optional — restrict to one platform; omit to search all
|
|
131
142
|
* @param {number} [a.limit]
|
|
132
143
|
* @param {number} [a.minScore]
|
|
133
|
-
* @returns {Promise<{platform:string, query:string, matches:Array<{game:string, score:number, cheats:number}>, gameCount:number, note:string}>}
|
|
144
|
+
* @returns {Promise<{platform:string|null, query:string, matches:Array<{game:string, platform:string, score:number, cheats:number}>, gameCount:number, note:string}>}
|
|
134
145
|
*/
|
|
135
146
|
export async function searchCheatGames({ platform, query, limit = 12, minScore = 0.25 }) {
|
|
136
|
-
const
|
|
137
|
-
|
|
147
|
+
const platforms = platform ? [platform] : INDEXED_PLATFORMS;
|
|
148
|
+
let scored = [];
|
|
149
|
+
let gameCount = 0;
|
|
150
|
+
let missing = [];
|
|
151
|
+
for (const plat of platforms) {
|
|
152
|
+
const idx = await loadIndex(plat);
|
|
153
|
+
if (!idx || !idx.games) { missing.push(plat); continue; }
|
|
154
|
+
gameCount += idx.gameCount ?? Object.keys(idx.games).length;
|
|
155
|
+
const fuse = await getFuse(plat);
|
|
156
|
+
if (!fuse) continue;
|
|
157
|
+
for (const r of fuse.search(baseName(query))) {
|
|
158
|
+
const score = simFromFuse(r.score);
|
|
159
|
+
if (score >= minScore) scored.push({ game: r.item.game, platform: plat, score, cheats: r.item.cheats });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// When scoped to one platform and that platform has no index at all, say so.
|
|
163
|
+
if (platform && missing.length) {
|
|
138
164
|
return { platform, query, matches: [], gameCount: 0, note: `No bundled cheat index for platform '${platform}'.` };
|
|
139
165
|
}
|
|
140
|
-
const
|
|
141
|
-
const fuse = await getFuse(platform);
|
|
142
|
-
// Search the tag-stripped query against the tag-stripped baseName index.
|
|
143
|
-
const results = fuse.search(baseName(query));
|
|
144
|
-
const matches = results
|
|
145
|
-
.map((r) => ({ game: r.item.game, score: simFromFuse(r.score), cheats: r.item.cheats }))
|
|
146
|
-
.filter((m) => m.score >= minScore)
|
|
166
|
+
const matches = scored
|
|
147
167
|
.sort((a, b) => b.score - a.score || a.game.length - b.game.length)
|
|
148
168
|
.slice(0, limit);
|
|
169
|
+
const scope = platform ? `the ${platform} cheat DB` : `the cheat DB (all ${platforms.length - missing.length} platforms)`;
|
|
149
170
|
return {
|
|
150
|
-
platform
|
|
151
|
-
|
|
171
|
+
platform: platform ?? null,
|
|
172
|
+
query, matches, gameCount,
|
|
152
173
|
note: matches.length === 0
|
|
153
|
-
? `No game in
|
|
154
|
-
: `${matches.length} candidate(s) by fuzzy name match (region/revision tags ignored, typo-tolerant).
|
|
174
|
+
? `No game in ${scope} (${gameCount} games) is close to "${query}". Try fewer/looser words.`
|
|
175
|
+
: `${matches.length} candidate(s) by fuzzy name match across ${platform ? "1 platform" : "all platforms"} (each match shows its \`platform\`; region/revision tags ignored, typo-tolerant). To read a game's cheats, call cheats({op:'lookup', platform, game}) with the match's own platform. Score is name similarity, not a content guarantee — verify a label before patching.`,
|
|
155
176
|
};
|
|
156
177
|
}
|
|
157
178
|
|
package/src/http/routes.js
CHANGED
|
@@ -5,20 +5,22 @@
|
|
|
5
5
|
// GET /tool/:name/schema that tool's JSON Schema (a validator on demand)
|
|
6
6
|
// GET /openapi.json OpenAPI 3.1 spec for every /tool/:name route
|
|
7
7
|
// GET /documentation Swagger UI over /openapi.json (live "try it" console)
|
|
8
|
-
// GET /romdev
|
|
9
|
-
// doc that drives the routes, never mentions
|
|
8
|
+
// GET /skills/romdev/SKILL.md the SKILL.md (Agent Skills open standard) — the
|
|
9
|
+
// channel doc that drives the routes, never mentions
|
|
10
|
+
// MCP. Also at /romdev/SKILL.md and /romdev-skill.md.
|
|
10
11
|
//
|
|
11
|
-
// Sessions: each agent
|
|
12
|
-
//
|
|
13
|
-
// header
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
12
|
+
// Sessions: each agent picks its own stable id and sends it as x-romdev-session
|
|
13
|
+
// on EVERY call (same per-agent host isolation as MCP). The header is REQUIRED —
|
|
14
|
+
// no header → 401 (we don't auto-mint a throwaway session; that silently dropped
|
|
15
|
+
// the loaded ROM and surfaced as "No ROM loaded" later). First use of an id
|
|
16
|
+
// creates the session, reuse keeps the host across load→step→read, different ids
|
|
17
|
+
// isolate different agents. No auth beyond that — localhost trust, same as /mcp
|
|
18
|
+
// (the app already mounts localhostHostValidation()).
|
|
17
19
|
|
|
18
|
-
import { randomUUID } from "node:crypto";
|
|
19
20
|
import { buildToolRegistry, runTool, toolJsonSchema } from "./tool-registry.js";
|
|
20
21
|
import { skillPreamble, skillToolReference, buildSkillDoc } from "./skill-doc.js";
|
|
21
22
|
import { swaggerHtml, swaggerAsset } from "./swagger.js";
|
|
23
|
+
import { observer } from "../observer/bus.js";
|
|
22
24
|
import { log } from "../mcp/log.js";
|
|
23
25
|
|
|
24
26
|
const SESSION_HEADER = "x-romdev-session";
|
|
@@ -40,12 +42,16 @@ export function mountHttpToolRoutes(app, opts = {}) {
|
|
|
40
42
|
/** @type {Map<string, {registry: Map<string,any>, lastSeen: number}>} */
|
|
41
43
|
const sessions = new Map();
|
|
42
44
|
|
|
43
|
-
function getSession(sessionKey) {
|
|
45
|
+
function getSession(sessionKey, { sticky = false } = {}) {
|
|
44
46
|
let s = sessions.get(sessionKey);
|
|
45
47
|
if (!s) {
|
|
46
|
-
s = { registry: buildToolRegistry(sessionKey), lastSeen: Date.now() };
|
|
48
|
+
s = { registry: buildToolRegistry(sessionKey), lastSeen: Date.now(), sticky };
|
|
47
49
|
sessions.set(sessionKey, s);
|
|
48
50
|
log.debug(`[http] session ${sessionKey.slice(0, 8)} created (${sessions.size} active)`);
|
|
51
|
+
// Surface sticky sessions in /livestream (like the MCP path does on init).
|
|
52
|
+
// Ephemeral one-shot sessions are NOT registered (they'd spam connect/
|
|
53
|
+
// disconnect); their individual `call` events still show in the stream.
|
|
54
|
+
if (sticky) { try { observer.sessionConnected(sessionKey); } catch {} }
|
|
49
55
|
} else {
|
|
50
56
|
s.lastSeen = Date.now();
|
|
51
57
|
}
|
|
@@ -58,6 +64,7 @@ export function mountHttpToolRoutes(app, opts = {}) {
|
|
|
58
64
|
for (const [key, s] of sessions) {
|
|
59
65
|
if (now - s.lastSeen > idleMs) {
|
|
60
66
|
sessions.delete(key);
|
|
67
|
+
if (s.sticky) { try { observer.sessionDisconnected(key); } catch {} }
|
|
61
68
|
log.debug(`[http] session ${key.slice(0, 8)} reaped (idle)`);
|
|
62
69
|
}
|
|
63
70
|
}
|
|
@@ -71,30 +78,38 @@ export function mountHttpToolRoutes(app, opts = {}) {
|
|
|
71
78
|
// ── POST /tool/:name ──────────────────────────────────────────────────────
|
|
72
79
|
app.post("/tool/:name", async (req, res) => {
|
|
73
80
|
const name = req.params.name;
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
// Session model: the AGENT picks its own stable, task-descriptive id and
|
|
82
|
+
// sends it as x-romdev-session on EVERY call — first use creates the session,
|
|
83
|
+
// reuse keeps the same host/state (load→step→read), and different ids isolate
|
|
84
|
+
// different agents. NO HEADER → 401: we don't auto-mint a throwaway session
|
|
85
|
+
// (that silently dropped the loaded ROM and surfaced as "No ROM loaded" two
|
|
86
|
+
// calls later). Requiring the header up front turns that silent footgun into
|
|
87
|
+
// a loud, fixable 401.
|
|
88
|
+
const sessionKey = req.headers[SESSION_HEADER];
|
|
77
89
|
if (typeof sessionKey !== "string" || !sessionKey) {
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
res.status(401).json({
|
|
91
|
+
error: "Missing required `x-romdev-session` header. Pick ONE stable, " +
|
|
92
|
+
"task-descriptive id for yourself (e.g. 'nes-platformer-build') and send " +
|
|
93
|
+
"it on EVERY call — it's your per-session emulator key (the ROM you load " +
|
|
94
|
+
"lives under it; the next call only sees it with the SAME id) and the " +
|
|
95
|
+
"label shown in the /livestream observer. Several agents share one server " +
|
|
96
|
+
"by each using a different id.",
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
80
99
|
}
|
|
81
|
-
const { registry } = getSession(sessionKey);
|
|
100
|
+
const { registry } = getSession(sessionKey, { sticky: true });
|
|
82
101
|
const tool = registry.get(name);
|
|
83
102
|
if (!tool) {
|
|
84
103
|
res.status(404).json({
|
|
85
|
-
error: `Unknown tool '${name}'. GET /openapi.json or /romdev
|
|
104
|
+
error: `Unknown tool '${name}'. GET /openapi.json or /skills/romdev/SKILL.md for the list.`,
|
|
86
105
|
});
|
|
87
106
|
return;
|
|
88
107
|
}
|
|
89
|
-
// echo the session id
|
|
108
|
+
// echo the session id back (convenience for clients that log it)
|
|
90
109
|
res.setHeader(SESSION_HEADER, sessionKey);
|
|
91
|
-
const out = await runTool(tool, req.body);
|
|
92
|
-
if (ephemeral) {
|
|
93
|
-
// drop the ephemeral session immediately (no sticky host wanted)
|
|
94
|
-
sessions.delete(sessionKey);
|
|
95
|
-
}
|
|
110
|
+
const out = await runTool(tool, req.body, sessionKey);
|
|
96
111
|
if (out.ok) res.json(out.result);
|
|
97
|
-
else res.status(400).json({ error: out.error });
|
|
112
|
+
else res.status(400).json(out.result ?? { error: out.error });
|
|
98
113
|
});
|
|
99
114
|
|
|
100
115
|
// ── GET /tool/:name/schema ────────────────────────────────────────────────
|
|
@@ -122,17 +137,26 @@ export function mountHttpToolRoutes(app, opts = {}) {
|
|
|
122
137
|
res.send(buf);
|
|
123
138
|
});
|
|
124
139
|
|
|
125
|
-
// ── GET /romdev
|
|
126
|
-
|
|
140
|
+
// ── GET /skills/romdev/SKILL.md (primary) + aliases ───────────────────────
|
|
141
|
+
// Agents store skills on disk as skills/<name>/SKILL.md (a dir named after the
|
|
142
|
+
// skill, canonical file SKILL.md). We serve the same doc at several paths so
|
|
143
|
+
// the URL matches wherever the agent saved it:
|
|
144
|
+
// /skills/romdev/SKILL.md — primary: full disk mirror (~/.claude/skills/romdev/SKILL.md)
|
|
145
|
+
// /romdev/SKILL.md — alias: the <name>/SKILL.md tail
|
|
146
|
+
// /romdev-skill.md — alias: flat form (older refs)
|
|
147
|
+
const serveSkill = (req, res) => {
|
|
127
148
|
const md = buildSkillDoc({
|
|
128
149
|
registry: metaRegistry,
|
|
129
150
|
agentsBody: opts.agentsBody ?? "",
|
|
130
151
|
version,
|
|
131
152
|
});
|
|
132
153
|
res.type("text/markdown").send(md);
|
|
133
|
-
}
|
|
154
|
+
};
|
|
155
|
+
app.get("/skills/romdev/SKILL.md", serveSkill);
|
|
156
|
+
app.get("/romdev/SKILL.md", serveSkill);
|
|
157
|
+
app.get("/romdev-skill.md", serveSkill); // alias
|
|
134
158
|
|
|
135
|
-
log.debug("[http] tool surface mounted: POST /tool/:name, /openapi.json, /documentation, /romdev
|
|
159
|
+
log.debug("[http] tool surface mounted: POST /tool/:name, /openapi.json, /documentation, /skills/romdev/SKILL.md");
|
|
136
160
|
return { sessions, stop: () => clearInterval(reaper) };
|
|
137
161
|
}
|
|
138
162
|
|
|
@@ -158,13 +182,14 @@ export function buildOpenApi(registry, version) {
|
|
|
158
182
|
},
|
|
159
183
|
responses: {
|
|
160
184
|
200: { description: "Tool result (JSON).", content: { "application/json": { schema: { type: "object" } } } },
|
|
161
|
-
400: { description: "Validation or tool error.", content: { "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } } },
|
|
185
|
+
400: { description: "Validation or tool error (the action did not succeed).", content: { "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } } },
|
|
186
|
+
401: { description: "Missing required x-romdev-session header.", content: { "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } } },
|
|
162
187
|
404: { description: "Unknown tool." },
|
|
163
188
|
},
|
|
164
189
|
parameters: [{
|
|
165
|
-
name: SESSION_HEADER, in: "header", required:
|
|
190
|
+
name: SESSION_HEADER, in: "header", required: true,
|
|
166
191
|
schema: { type: "string" },
|
|
167
|
-
description: "Per-agent session id
|
|
192
|
+
description: "REQUIRED. Per-agent session id — pick one stable, UNIQUE, task-DESCRIPTIVE string (e.g. 'nes-platformer-build', 'zelda-romhack-text') and send it on EVERY call. It's the per-session emulator key (load→step→read state lives under it) AND the label shown in the /livestream observer, so a descriptive id tells a watching human which task each call belongs to. Several agents share one server safely by each using a different id. Missing → 401.",
|
|
168
193
|
}],
|
|
169
194
|
},
|
|
170
195
|
};
|
|
@@ -174,7 +199,7 @@ export function buildOpenApi(registry, version) {
|
|
|
174
199
|
info: {
|
|
175
200
|
title: "romdev HTTP tool API",
|
|
176
201
|
version,
|
|
177
|
-
description: "Plain-HTTP surface for romdev's retro-game-dev tools — the non-MCP way to drive the same tools. Generated from the tool registry. See /romdev
|
|
202
|
+
description: "Plain-HTTP surface for romdev's retro-game-dev tools — the non-MCP way to drive the same tools. Generated from the tool registry. See /skills/romdev/SKILL.md for the workflow guide.",
|
|
178
203
|
},
|
|
179
204
|
servers: [{ url: "/" }],
|
|
180
205
|
paths,
|
package/src/http/skill-doc.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// One shared body (AGENTS.md, channel-neutral) is wrapped per delivery channel:
|
|
4
4
|
// - MCP connection instructions = mcpPreamble + body (says "call the MCP
|
|
5
5
|
// tools"; never mentions HTTP routes / skills)
|
|
6
|
-
// - GET /romdev
|
|
6
|
+
// - GET /skills/romdev/SKILL.md = skill frontmatter + skillPreamble + body +
|
|
7
7
|
// generated tool reference (says "POST /tool/{name}"; never mentions MCP)
|
|
8
8
|
//
|
|
9
9
|
// So neither surface mentions the other: the delivery instructions live in the
|
|
@@ -38,8 +38,8 @@ export const skillPreamble = [
|
|
|
38
38
|
" • GET /tool/{name}/schema — that tool's JSON Schema (the exact parameters + types).",
|
|
39
39
|
" • GET /openapi.json — the full machine-readable API; GET /documentation — a browsable console.",
|
|
40
40
|
"",
|
|
41
|
-
"Sessions
|
|
42
|
-
"
|
|
41
|
+
"## Sessions — IMPORTANT for stateful work (load → step → read)",
|
|
42
|
+
"**Pick ONE session id for yourself and send it as the `x-romdev-session` header on EVERY call.** Make it UNIQUE and DESCRIPTIVE of what you're doing — e.g. `nes-platformer-build`, `zelda-romhack-text`, `gba-sprite-debug` (a slug, optionally with a short random suffix to stay unique). A human may be watching the live observer at /livestream, where your session id is the label for all your activity — a descriptive id tells them at a glance which agent/task each call belongs to; a bare uuid or `default` is opaque. The emulator/host is per-session: the ROM you `loadMedia` lives in YOUR session, and the next `frame`/`memory`/`cpu` call only sees it if it carries the SAME id. Do NOT send a new id each call — that's a fresh empty session every time (your loaded ROM vanishes; \"No ROM loaded\"). Several agents can share one server safely: each just sends a DIFFERENT id, so nobody clobbers another's ROM (another reason to make yours distinctive). The header is REQUIRED on every `/tool/{name}` call — omit it and you get a **401** (the server will NOT silently run you in a throwaway session). Pure file tools (romPatch/cart/encodeAudio) still need the header; just reuse your one id everywhere.",
|
|
43
43
|
"",
|
|
44
44
|
"Each tool is a domain VERB keyed by an operation axis — e.g. POST /tool/memory {\"op\":\"read\",…},",
|
|
45
45
|
"POST /tool/build {\"output\":\"rom\",…}, POST /tool/romPatch {\"op\":\"findPointer\",…}. The full per-tool",
|
|
@@ -47,7 +47,7 @@ export const skillPreamble = [
|
|
|
47
47
|
].join("\n");
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Build GET /romdev
|
|
50
|
+
* Build GET /skills/romdev/SKILL.md: frontmatter + skill preamble + shared body +
|
|
51
51
|
* generated tool reference.
|
|
52
52
|
* @param {{registry: Map<string,any>, agentsBody: string, version?: string}} args
|
|
53
53
|
* @returns {string}
|
|
@@ -68,16 +68,17 @@ export function buildSkillDoc({ registry, agentsBody, version }) {
|
|
|
68
68
|
|
|
69
69
|
// Update note — stamped with the running server's version. A saved skill is a
|
|
70
70
|
// static snapshot (it doesn't auto-update), but this doc is GENERATED live from
|
|
71
|
-
// the running server, so re-fetching always gives
|
|
72
|
-
//
|
|
71
|
+
// the running server, so re-fetching always gives the current version. An agent
|
|
72
|
+
// can check the running version two ways: the tool call POST /tool/catalog
|
|
73
|
+
// {"op":"status"} → `romdevVersion`, or GET /healthz → `version`.
|
|
73
74
|
const v = version ?? "0.0.0";
|
|
74
75
|
const updateNote = [
|
|
75
76
|
"## Keeping this skill current",
|
|
76
77
|
`This skill was generated by romdev **v${v}** (it's a snapshot — it does not auto-update). ` +
|
|
77
78
|
"romdev generates it live from the running server, so to update: run the latest `npx romdevtools`, " +
|
|
78
|
-
`then re-fetch \`GET http://localhost:7331/romdev
|
|
79
|
-
"
|
|
80
|
-
"`
|
|
79
|
+
`then re-fetch \`GET http://localhost:7331/skills/romdev/SKILL.md\` and overwrite your saved copy. ` +
|
|
80
|
+
"To check whether you're stale, ask the running server its version — `POST /tool/catalog {\"op\":\"status\"}` " +
|
|
81
|
+
"returns `romdevVersion` (or `GET /healthz` → `version`); if it's newer than the `metadata.version` above, re-fetch.",
|
|
81
82
|
].join("\n");
|
|
82
83
|
|
|
83
84
|
return [
|
package/src/http/swagger.js
CHANGED
|
@@ -56,7 +56,7 @@ export function swaggerHtml(opts = {}) {
|
|
|
56
56
|
<p>Loading interactive docs… If this doesn't render, the raw OpenAPI spec is at
|
|
57
57
|
<a href="${specUrl}"><code>${specUrl}</code></a>, every tool is callable via
|
|
58
58
|
<code>POST /tool/{name}</code>, and the workflow guide is at
|
|
59
|
-
<a href="/romdev
|
|
59
|
+
<a href="/skills/romdev/SKILL.md"><code>/skills/romdev/SKILL.md</code></a>.</p>
|
|
60
60
|
</div>
|
|
61
61
|
<div id="swagger-ui"></div>
|
|
62
62
|
<script src="${base}/swagger-ui-bundle.js"></script>
|