@henols/c64-re-tools 1.1.0 → 2.0.0-rc.3

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -37
  3. package/distribution/package.ts +36 -0
  4. package/distribution/plugin.ts +120 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +73 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +103 -0
  8. package/skills/c64-basic/SKILL.md +67 -0
  9. package/skills/c64-basic/scripts/basic.ts +46 -0
  10. package/skills/c64-disk/SKILL.md +79 -0
  11. package/skills/c64-disk/scripts/disk.ts +65 -0
  12. package/skills/c64-emulator/SKILL.md +124 -0
  13. package/skills/c64-knowledge/SKILL.md +102 -0
  14. package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
  15. package/skills/c64-memory-map/SKILL.md +76 -0
  16. package/skills/c64-memory-map/scripts/memmap.ts +81 -0
  17. package/skills/c64-memory-map/scripts/platform.ts +424 -0
  18. package/skills/c64-provenance/SKILL.md +76 -0
  19. package/skills/c64-provenance/scripts/compare.ts +92 -0
  20. package/skills/c64-provenance/scripts/provenance.ts +83 -0
  21. package/skills/c64-reverse-engineering/SKILL.md +90 -0
  22. package/skills/c64-static-analysis/SKILL.md +96 -0
  23. package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
  24. package/skills/c64-static-analysis/scripts/findings.ts +73 -0
  25. package/skills/c64-testing/SKILL.md +79 -0
  26. package/skills/c64-testing/references/scenario-format.md +76 -0
  27. package/skills/c64-testing/scripts/checklist.ts +51 -0
  28. package/skills/c64-testing/scripts/scenario.ts +473 -0
  29. package/skills/c64-testing/scripts/test.ts +55 -0
  30. package/skills/c64-unpacker/SKILL.md +82 -0
  31. package/skills/c64-unpacker/scripts/evidence.ts +135 -0
  32. package/skills/c64-unpacker/scripts/unpack.ts +216 -0
  33. package/src/c64.ts +281 -0
  34. package/src/cli/ap-sdk.ts +44 -0
  35. package/src/cli/cleanup.ts +194 -0
  36. package/src/cli/main.ts +122 -0
  37. package/src/cli/status.ts +34 -0
  38. package/src/host/main.ts +106 -0
  39. package/src/host/server.ts +376 -0
  40. package/src/host/tools/c1541.ts +255 -0
  41. package/src/host/tools/index.ts +31 -0
  42. package/src/host/tools/petcat.ts +70 -0
  43. package/src/host/tools/status.ts +30 -0
  44. package/src/host/tools/version.ts +41 -0
  45. package/src/host/vice/adapter.ts +761 -0
  46. package/src/host/vice/binary-monitor.ts +389 -0
  47. package/src/host/vice/chips.ts +108 -0
  48. package/src/host/vice/connect.ts +34 -0
  49. package/src/host/vice/process.ts +402 -0
  50. package/src/host/vice/screen.ts +183 -0
  51. package/src/host/vice/session.ts +1316 -0
  52. package/src/host/vice/text-monitor.ts +223 -0
  53. package/src/host-client/connect.ts +238 -0
  54. package/src/host-client/tools.ts +89 -0
  55. package/src/host-client/transfer.ts +100 -0
  56. package/src/host-client/vice-session.ts +254 -0
  57. package/src/knowledge/database.ts +132 -0
  58. package/src/knowledge/history.ts +128 -0
  59. package/src/knowledge/import.ts +344 -0
  60. package/src/knowledge/read.ts +191 -0
  61. package/src/knowledge/schema.ts +140 -0
  62. package/src/knowledge/write.ts +348 -0
  63. package/src/mcp/main.ts +106 -0
  64. package/src/mcp/server.ts +307 -0
  65. package/src/mcp/tools/debug.ts +237 -0
  66. package/src/mcp/tools/execution.ts +116 -0
  67. package/src/mcp/tools/input.ts +69 -0
  68. package/src/mcp/tools/machine.ts +97 -0
  69. package/src/mcp/tools/media.ts +106 -0
  70. package/src/mcp/tools/memory.ts +203 -0
  71. package/src/mcp/tools/video.ts +260 -0
  72. package/src/native/acme.ts +116 -0
  73. package/src/native/discover.ts +141 -0
  74. package/src/native/dxa.ts +166 -0
  75. package/src/native/ghidra/analyze.ts +86 -0
  76. package/src/native/ghidra/index.ts +157 -0
  77. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  78. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  79. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  80. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  81. package/src/native/ghidra/scripts/C64Export.java +242 -0
  82. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  83. package/src/native/local.ts +34 -0
  84. package/src/native/processes.ts +350 -0
  85. package/src/native/run.ts +172 -0
  86. package/src/native/staging.ts +83 -0
  87. package/src/native/status.ts +101 -0
  88. package/src/native/types.ts +228 -0
  89. package/src/native/watchdog.ts +78 -0
  90. package/src/project.ts +39 -0
  91. package/src/protocol/framing.ts +132 -0
  92. package/src/protocol/messages.ts +243 -0
  93. package/src/protocol/tools.ts +247 -0
  94. package/src/protocol/vice.ts +1235 -0
  95. package/src/script.ts +59 -0
  96. package/src/time.ts +15 -0
  97. package/src/trace.ts +162 -0
  98. package/THIRD-PARTY-NOTICES.md +0 -26
  99. package/bin/cli.mjs +0 -283
  100. package/skills/acme-build/SKILL.md +0 -260
  101. package/skills/acme-build/scripts/acme.mjs +0 -341
  102. package/skills/acme-build/template.a +0 -53
  103. package/skills/c64-disk-access/SKILL.md +0 -159
  104. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  105. package/skills/c64-memory-mapping/SKILL.md +0 -631
  106. package/skills/c64-memory-mapping/memmap.json +0 -8800
  107. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  108. package/skills/c64-petcat/SKILL.md +0 -87
  109. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  110. package/skills/c64-program-recon/SKILL.md +0 -706
  111. package/skills/c64-program-recon/references/control-flow.md +0 -180
  112. package/skills/c64-program-recon/references/graphics.md +0 -79
  113. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  114. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  115. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  116. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  117. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  118. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  119. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  120. package/skills/c64-provenance-diff/SKILL.md +0 -292
  121. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  122. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  123. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  124. package/skills/c64-ram-capture/SKILL.md +0 -474
  125. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  126. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  127. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  128. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  129. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  130. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  131. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  132. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  133. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  134. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  135. package/skills/c64-ram-capture/transients/README.md +0 -137
  136. package/skills/routine-queue-walker/SKILL.md +0 -394
  137. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  138. package/skills/vice-wedge-triage/SKILL.md +0 -269
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Henrik Olsson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,61 +1,162 @@
1
- # @henols/c64-re-tools
1
+ # c64-re-tools
2
2
 
3
- One-command installer that adds the **C64 reverse-engineering skills** and the
4
- **VICE emulator MCP server** ([`@henols/vice-mcp`](https://www.npmjs.com/package/@henols/vice-mcp))
5
- to a project, so an MCP client such as Claude Code can drive a Commodore 64
6
- emulator for reverse-engineering work.
3
+ An agent-friendly Commodore 64 engineering toolkit for reverse engineering
4
+ unknown applications and for developing, debugging, building and testing new
5
+ C64 applications.
7
6
 
8
- ## Usage
7
+ An agent drives a real VICE emulator through an MCP server, and uses skills
8
+ for the rest: disk images, BASIC, static analysis with DXA and Ghidra, a project
9
+ knowledge database, the ACME assembler, unpacking, release comparison and
10
+ functional-equivalence tests.
9
11
 
10
- From the project you want to set up:
12
+ Architecture in one line: one MCP process owns one VICE instance and is
13
+ VICE-only; a Host Runtime on the graphical host owns VICE and the tools that
14
+ come with it (c1541, petcat); skill scripts call the Host Runtime for those,
15
+ run ACME, DXA and Ghidra themselves, and keep project knowledge local in
16
+ `.c64-re-tools/knowledge.db`.
17
+ The design is in [`docs/redesign/`](docs/redesign/README.md), and
18
+ [`docs/plan.md`](docs/plan.md) tracks the implementation.
11
19
 
12
- ```sh
13
- npx @henols/c64-re-tools
20
+ ## Install
21
+
22
+ You need Node.js 24 or newer, and VICE 3.9 or later (`x64sc`, `c1541`, `petcat`) with its ROM files. ACME, DXA and Ghidra are needed only for the skills that use them.
23
+
24
+ Start the Host Runtime and leave it running:
25
+
26
+ ```
27
+ npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host
28
+ ```
29
+
30
+ Install into your project and start the agent:
31
+
32
+ ```
33
+ cd my-c64-project
34
+ npx -y @henols/c64-re-tools@latest install --target claude
35
+ claude
14
36
  ```
15
37
 
16
- That will:
38
+ `npx -y @henols/c64-re-tools@latest status` shows what works.
17
39
 
18
- 1. Copy the bundled skills into `<project>/.claude/skills/`
19
- 2. Add a `vice` server to `<project>/.mcp.json` (existing servers are preserved),
20
- launched via `npx -y @henols/vice-mcp`
40
+ Until 2.0.0 is released, `@latest` is the old 1.1.0. Use the prerelease instead. For the prerelease, a dev container and debugging, see [docs/debug-install.md](docs/debug-install.md).
21
41
 
22
- Then restart Claude Code in that project.
42
+ ### Settings
43
+
44
+ | Variable | Read by | Meaning |
45
+ | --- | --- | --- |
46
+ | `C64RT_VICE` | host | Full path of `x64sc` when it is not on `PATH`. |
47
+ | `C64RT_C1541`, `C64RT_PETCAT` | host | Full path of the tool when it is not on `PATH`. |
48
+ | `C64RT_ACME`, `C64RT_DXA` | skills, CLI | Full path of the tool when it is not on `PATH`. |
49
+ | `C64RT_GHIDRA` | skills, CLI | The Ghidra installation directory, when `analyzeHeadless` is not on `PATH`. |
50
+ | `C64RT_HOST` | MCP, skills, CLI | `host:port` of the Host Runtime when it is not on 127.0.0.1, `host.docker.internal` or `host.containers.internal` at 6464. |
51
+ | `C64RT_VIDEO` | MCP | `pal` (default) or `ntsc`, fixed for the life of the MCP process. |
52
+ | `C64RT_TRACE` | host, MCP, skills, CLI | A directory for a trace of what the programs do, for a person who debugs a run. See [docs/debug-install.md](docs/debug-install.md). Nothing of the trace reaches the agent. |
53
+
54
+ Ghidra runs with its own settings directory for each request. The toolkit
55
+ brings its own NMOS 6510 language (all 256 opcodes); your Ghidra installation
56
+ and settings stay unchanged.
57
+
58
+ ## Use it with an agent
59
+
60
+ ## Use it with an agent
61
+
62
+ 1. Install for your harness: `--target claude`, `codex`, `pi`, `opencode`, `gemini`, `copilot`, `cursor` or `windsurf`. Without `--target`, `install` installs for all of them.
63
+ 2. Start the Host Runtime and leave it running.
64
+ 3. Start the harness in the project, for example `claude` or `codex`, or open the project in Cursor or Windsurf. The harness starts the MCP server and finds the skills by itself.
65
+ 4. Ask for the result you want, in plain words. The agent picks the skills and the `c64_*` tools.
66
+
67
+ Examples:
68
+
69
+ ```
70
+ Reverse engineer original/game.d64. Find how it starts, unpack it if it is packed,
71
+ and name the main routines in project knowledge.
23
72
 
24
- ### Options
73
+ Find the routine that draws the score, and explain how it works.
25
74
 
26
- | Option | Effect |
75
+ Write a program in ACME that scrolls a text at the bottom of the screen.
76
+ Build it, run it in the emulator, and write a test for it.
77
+
78
+ Show me the emulator window.
79
+ ```
80
+
81
+ The agent keeps what it learns about the program in `.c64-re-tools/knowledge.db`. Commit that file with the project.
82
+
83
+ ## Skills
84
+
85
+ | Skill | Use |
27
86
  | --- | --- |
28
- | `[targetDir]` | Install into this directory instead of the current one. |
29
- | `--force` | Overwrite existing skills and an existing `vice` MCP entry. |
30
- | `--vendor` | Also `npm install -D @henols/vice-mcp` into the project and wire `.mcp.json` to the local copy (pinned / offline), instead of `npx`. |
31
- | `--dry-run`, `-n` | Show what would change without writing anything. |
32
- | `--help`, `-h` | Show help. |
87
+ | c64-reverse-engineering | Coordinates an investigation and picks the specialist for the next unknown. |
88
+ | c64-emulator | Runs and observes the program in VICE through the MCP tools. |
89
+ | c64-static-analysis | A fast first pass with DXA and a deeper pass with Ghidra into knowledge, with seeds from knowledge and decompilation. |
90
+ | c64-knowledge | Reads and writes symbols, regions, comments and references, with history. |
91
+ | c64-disk | Directory, BAM, entries, sector chains and extraction of disk images. |
92
+ | c64-basic | BASIC V2 listings and the SYS/USR handoff to machine code. |
93
+ | c64-unpacker | Packing evidence, and capture of the unpacked program from the emulator. |
94
+ | c64-memory-map | Platform meaning of addresses and register values. |
95
+ | c64-provenance | Byte comparison of releases. |
96
+ | c64-assembler | ACME builds with diagnostics and symbols. |
97
+ | c64-testing | Original-against-rebuild scenarios with PASS, FAIL or INCONCLUSIVE. |
98
+
99
+ ## Examples
100
+
101
+ Reverse engineering, from an unknown disk to a verified rebuild (the commands
102
+ that the skills run for the agent; `<skills>` is the installed skills
103
+ directory):
104
+
105
+ ```
106
+ node <skills>/c64-disk/scripts/disk.ts directory original/game.d64
107
+ node <skills>/c64-disk/scripts/disk.ts read original/game.d64 GAME --out extracted/game.prg
108
+ node <skills>/c64-basic/scripts/basic.ts extracted/game.prg # the SYS address, e.g. $080d
109
+ node <skills>/c64-unpacker/scripts/unpack.ts inspect extracted/game.prg
110
+ node <skills>/c64-static-analysis/scripts/analyze.ts extracted/game.prg --analyzer dxa --listing analysis/game.lst
111
+ node <skills>/c64-static-analysis/scripts/analyze.ts extracted/game.prg --entry '$080d'
112
+ node <skills>/c64-knowledge/scripts/knowledge.ts rename '$080d' main --kind routine --reason 'entry from BASIC'
113
+ node <skills>/c64-static-analysis/scripts/analyze.ts extracted/game.prg --entry '$080d' --decompile '$080d'
114
+ node <skills>/c64-assembler/scripts/assemble.ts --source-root src --entry game.a --out build/game.prg > build/game.json
115
+ node <skills>/c64-testing/scripts/test.ts tests/counts.json # PASS, FAIL or INCONCLUSIVE
116
+ ```
33
117
 
34
- Re-running is safe: existing skills and an existing `vice` entry are kept unless
35
- you pass `--force`.
118
+ Development: build with the c64-assembler skill, then load and debug with the
119
+ MCP tools (`c64_program_load`, `c64_breakpoint`, `c64_run_until`,
120
+ `c64_memory_read`, `c64_screen`), and test with the c64-testing skill.
36
121
 
37
- ## What gets installed
122
+ ## Developing
38
123
 
39
- - **Skills** — `acme-build`, `c64-memory-mapping`, `c64-program-recon`,
40
- `c64-provenance-diff`, `c64-ram-capture`, `vice-wedge-triage`.
41
- - **MCP server** — `@henols/vice-mcp`, exposing the `vice` tools.
124
+ To work on the toolkit itself, Node 24 or newer and pnpm are required; install pnpm yourself (for example
125
+ `corepack enable pnpm`). `packageManager` in `package.json` pins its version.
42
126
 
43
- ## Requirements
127
+ ```
128
+ pnpm install --frozen-lockfile
129
+ pnpm typecheck # tsc checks everything; it emits nothing
130
+ pnpm test # node --test on src/**/*.test.ts and test/**/*.test.ts
131
+ ```
132
+
133
+ Unit tests sit beside their source as `src/**/*.test.ts`; `test/` holds
134
+ integration and end-to-end tests. Node runs the TypeScript directly (type
135
+ stripping), so relative imports name the `.ts` file. Skill scripts import
136
+ `src/` as `#src/...` (the `imports` map in `package.json`); an installed skill
137
+ gets the `src/` modules it reaches and its own `package.json` with that map.
138
+
139
+ Tests that need real tools are opt-in. Without them they are reported as
140
+ skipped, never as passed:
141
+
142
+ ```
143
+ C64RT_LIVE_VICE=/usr/bin/x64sc C64RT_GHIDRA=/opt/ghidra pnpm test
144
+ ```
44
145
 
45
- - **This installer** runs on Node ≥ 18.
46
- - **The VICE MCP server** it wires up requires **Node ≥ 24**, and a
47
- host with VICE (`x64sc`) reachable from the MCP client. See the
48
- [`@henols/vice-mcp`](https://www.npmjs.com/package/@henols/vice-mcp) readme.
146
+ ACME, DXA, c1541 and petcat tests run when the tools are on `PATH`.
49
147
 
50
- ## Alternative: Claude Code plugin
148
+ To release, push a tag. See [docs/releasing.md](docs/releasing.md).
51
149
 
52
- If you use Claude Code, you can instead install everything as a plugin:
150
+ ## Layout
53
151
 
54
152
  ```
55
- /plugin marketplace add henols/c64-re-tools
56
- /plugin install c64-re-tools@c64-re-tools
153
+ src/ # mcp, host, host-client, native, knowledge, cli; unit tests beside the source
154
+ skills/ # the skills: SKILL.md, scripts (TypeScript) and references
155
+ distribution/ # the AP SDK plugin definition: skills with the src/ modules they reach
156
+ test/ # fixtures, integration and end-to-end tests
157
+ docs/redesign/ # the design
57
158
  ```
58
159
 
59
160
  ## License
60
161
 
61
- MIT © Henrik Olsson
162
+ MIT. See [`LICENSE`](./LICENSE).
@@ -0,0 +1,36 @@
1
+ // The npm package and the VICE MCP declaration that starts it. This module
2
+ // only reads package.json, so a caller that needs the declaration does not
3
+ // build the plugin.
4
+
5
+ import { readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+
8
+ /**
9
+ * The npm dist-tag that a version is published under: a prerelease
10
+ * (2.0.0-rc.1) goes to "next", a release to "latest".
11
+ */
12
+ export function channelOf(version: string): "latest" | "next" {
13
+ return version.includes("-") ? "next" : "latest";
14
+ }
15
+
16
+ /** The name, version and dist-tag of this npm package, from its package.json. */
17
+ export const PACKAGE = (() => {
18
+ const metadata = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8")) as { name: string; version: string };
19
+ return { name: metadata.name, version: metadata.version, tag: channelOf(metadata.version) };
20
+ })();
21
+
22
+ /**
23
+ * The agent starts the MCP server through npx with the newest version of the
24
+ * channel it was installed from, so it updates itself and an install from a
25
+ * prerelease does not start a release; nothing is linked or installed globally.
26
+ * The entry point is TypeScript, run by tsx: Node does not strip types under
27
+ * node_modules.
28
+ */
29
+ export function mcpServerFor(platform: NodeJS.Platform): { command: string; args: string[] } {
30
+ const npx = ["-y", `--package=${PACKAGE.name}@${PACKAGE.tag}`, "c64-re-tools-mcp"];
31
+ // On native Windows npx is a .cmd shim, which a harness starts only through cmd /c.
32
+ return platform === "win32" ? { command: "cmd", args: ["/c", "npx", ...npx] } : { command: "npx", args: npx };
33
+ }
34
+
35
+ /** The declaration for the machine where install runs: the plugin is evaluated there. */
36
+ export const MCP_SERVER = mcpServerFor(process.platform);
@@ -0,0 +1,120 @@
1
+ // The portable c64-re-tools plugin for @jalco/ap-sdk: the skills
2
+ // with their TypeScript scripts, the src/ modules those scripts reach, their
3
+ // references, and the VICE MCP declaration. AP SDK maps it to each harness's
4
+ // native layout; nothing at run time depends on it.
5
+ //
6
+ // Nothing is built: an installed skill holds the same .ts files as this
7
+ // repository, and Node runs them by type stripping. Skill scripts import
8
+ // src/ through the "#src/*" subpath import; each installed skill gets a
9
+ // package.json that maps it to its own copy of those modules.
10
+
11
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
12
+ import { dirname, join, relative, resolve, sep } from "node:path";
13
+
14
+ import { definePlugin, defineSkill, type Skill } from "@jalco/ap-sdk";
15
+
16
+ import { MCP_SERVER, PACKAGE } from "./package.ts";
17
+
18
+ const root = resolve(import.meta.dirname, "..");
19
+ const SOURCE = join(root, "src");
20
+ const SCRIPT_REFERENCE = /scripts\/([a-z][a-z0-9-]*)\.ts/g;
21
+ /**
22
+ * The modules a file reaches: static and dynamic imports, re-exports, and
23
+ * new URL("...", import.meta.url) for a module that is spawned or an asset
24
+ * directory. Only relative and #src specifiers; packages are node: built-ins.
25
+ */
26
+ const SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(?\s*|\bnew URL\(\s*)(["'])((?:\.\.?\/|#src\/)[^"']*)\1/g;
27
+
28
+ /**
29
+ * The package.json of an installed skill: ES modules, #src/* resolved inside
30
+ * the skill, and the version of c64-re-tools that the copy comes from.
31
+ */
32
+ export const SKILL_PACKAGE = `${JSON.stringify({ type: "module", version: PACKAGE.version, imports: { "#src/*": "./src/*" } }, null, 2)}\n`;
33
+
34
+ /** The scripts that a SKILL.md runs, by file name. */
35
+ export function scriptsOf(skillMarkdown: string): string[] {
36
+ return [...new Set([...skillMarkdown.matchAll(SCRIPT_REFERENCE)].map((match) => match[1]!))].sort();
37
+ }
38
+
39
+ function filesBelow(directory: string): string[] {
40
+ if (!existsSync(directory)) return [];
41
+ return readdirSync(directory, { recursive: true, encoding: "utf8" })
42
+ .map((path) => join(directory, path))
43
+ .filter((path) => statSync(path).isFile())
44
+ .sort();
45
+ }
46
+
47
+ const inside = (directory: string, path: string) => path === directory || path.startsWith(directory + sep);
48
+
49
+ /**
50
+ * Every file that the given scripts reach, transitively: the scripts, their
51
+ * sibling modules, and the src/ modules and asset directories below them.
52
+ * A reach out of the skill directory and src/ is an error.
53
+ */
54
+ export function closureOf(skillDirectory: string, scripts: readonly string[]): string[] {
55
+ const found = new Set<string>();
56
+ const pending = [...scripts];
57
+ while (pending.length > 0) {
58
+ const file = pending.pop()!;
59
+ if (found.has(file)) continue;
60
+ if (!inside(SOURCE, file) && !inside(skillDirectory, file)) throw new Error(`${relative(root, file)} is outside the skill and src/`);
61
+ if (!existsSync(file)) throw new Error(`${relative(root, file)} does not exist`);
62
+ if (statSync(file).isDirectory()) {
63
+ pending.push(...filesBelow(file));
64
+ continue;
65
+ }
66
+ found.add(file);
67
+ if (!file.endsWith(".ts")) continue;
68
+ for (const match of readFileSync(file, "utf8").matchAll(SPECIFIER)) {
69
+ const specifier = match[2]!;
70
+ pending.push(specifier.startsWith("#src/") ? join(SOURCE, specifier.slice("#src/".length)) : resolve(dirname(file), specifier));
71
+ }
72
+ }
73
+ return [...found].sort();
74
+ }
75
+
76
+ /** One skill: SKILL.md frontmatter and body, scripts (executable), the src/ modules they reach, and references. */
77
+ function loadSkill(directory: string): Skill {
78
+ const text = readFileSync(join(directory, "SKILL.md"), "utf8");
79
+ const frontmatter = /^---\n([\s\S]*?)\n---\n/.exec(text);
80
+ if (frontmatter === null) throw new Error(`${directory}/SKILL.md has no frontmatter`);
81
+ const field = (name: string) => {
82
+ const value = new RegExp(`^${name}: (.+)$`, "m").exec(frontmatter[1]!)?.[1];
83
+ if (value === undefined) throw new Error(`${directory}/SKILL.md has no ${name}`);
84
+ return value;
85
+ };
86
+ const scripts = scriptsOf(text).map((script) => join(directory, "scripts", `${script}.ts`));
87
+ const code = closureOf(directory, scripts).map((path) => ({
88
+ path: (inside(SOURCE, path) ? relative(root, path) : relative(directory, path)).split("\\").join("/"),
89
+ content: readFileSync(path, "utf8"),
90
+ ...(scripts.includes(path) ? { executable: true } : {}),
91
+ }));
92
+ const references = filesBelow(join(directory, "references")).map((path) => ({ path: relative(directory, path).split("\\").join("/"), content: readFileSync(path, "utf8") }));
93
+ const resources = [...code, ...references, ...(scripts.length > 0 ? [{ path: "package.json", content: SKILL_PACKAGE }] : [])];
94
+ return defineSkill({
95
+ name: field("name"),
96
+ description: field("description"),
97
+ instructions: text.slice(frontmatter[0].length).replace(/^\n+/, ""),
98
+ ...(resources.length > 0 ? { resources } : {}),
99
+ });
100
+ }
101
+
102
+ function c64Plugin() {
103
+ const skills = readdirSync(join(root, "skills"), { withFileTypes: true })
104
+ .filter((entry) => entry.isDirectory())
105
+ .map((entry) => entry.name)
106
+ .sort()
107
+ .map((name) => loadSkill(join(root, "skills", name)));
108
+ return definePlugin({
109
+ id: "c64-re-tools",
110
+ version: PACKAGE.version,
111
+ description: "Commodore 64 reverse engineering and development: a live VICE emulator, static analysis, disk and BASIC tools, project knowledge, building and testing.",
112
+ author: { name: "Henrik Olsson" },
113
+ homepage: "https://github.com/henols/c64-re-tools",
114
+ license: "MIT",
115
+ skills,
116
+ mcpServers: { "c64-re-tools": MCP_SERVER },
117
+ });
118
+ }
119
+
120
+ export default c64Plugin();
package/package.json CHANGED
@@ -1,55 +1,49 @@
1
1
  {
2
2
  "name": "@henols/c64-re-tools",
3
- "version": "1.1.0",
4
- "description": "Installer that adds the C64 reverse-engineering skills and the VICE emulator MCP server (@henols/vice-mcp) to a project.",
3
+ "version": "2.0.0-rc.3",
4
+ "description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
5
+ "license": "MIT",
6
+ "author": "Henrik Olsson",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/henols/c64-re-tools.git"
10
+ },
5
11
  "type": "module",
12
+ "packageManager": "pnpm@12.9.1+sha512.06b055fff5cd20dc12781de173ced3310ce0951bf2ec620211a16045d5444d5c93a663210764ef29a653a610459ba0368f0e74f84d80c54723cf95aae7bc0d6d",
13
+ "imports": {
14
+ "#src/*": "./src/*"
15
+ },
6
16
  "bin": {
7
- "c64-re-tools": "bin/cli.mjs"
17
+ "c64-re-tools": "src/cli/main.ts",
18
+ "c64-re-tools-mcp": "src/mcp/main.ts",
19
+ "c64-re-tools-host": "src/host/main.ts"
8
20
  },
9
21
  "files": [
10
- "bin/",
22
+ "src/",
23
+ "!src/**/*.test.ts",
24
+ "!src/**/*.testkit.ts",
11
25
  "skills/",
12
- "README.md",
13
- "THIRD-PARTY-NOTICES.md"
26
+ "distribution/package.ts",
27
+ "distribution/plugin.ts",
28
+ "LICENSE",
29
+ "README.md"
14
30
  ],
15
31
  "engines": {
16
- "node": ">=18"
17
- },
18
- "publishConfig": {
19
- "access": "public"
20
- },
21
- "license": "MIT",
22
- "author": {
23
- "name": "Henrik Olsson",
24
- "url": "https://github.com/henols"
32
+ "node": ">=24.0.0"
25
33
  },
26
- "repository": {
27
- "type": "git",
28
- "url": "git+https://github.com/henols/c64-re-tools.git",
29
- "directory": "installer"
34
+ "scripts": {
35
+ "host": "node src/host/main.ts",
36
+ "typecheck": "tsc -p tsconfig.json",
37
+ "test": "node --test \"src/**/*.test.ts\" \"test/**/*.test.ts\""
30
38
  },
31
- "homepage": "https://github.com/henols/c64-re-tools#readme",
32
- "bugs": {
33
- "url": "https://github.com/henols/c64-re-tools/issues"
39
+ "devDependencies": {
40
+ "@types/node": "24.13.3",
41
+ "typescript": "7.0.2"
34
42
  },
35
- "keywords": [
36
- "c64",
37
- "commodore-64",
38
- "vice",
39
- "6502",
40
- "6510",
41
- "acme",
42
- "reverse-engineering",
43
- "mcp",
44
- "claude",
45
- "installer"
46
- ],
47
43
  "dependencies": {
48
- "@henols/vice-mcp": "1.1.0"
49
- },
50
- "scripts": {
51
- "sync-skills": "node scripts/sync-skills.mjs",
52
- "prepack": "node scripts/sync-skills.mjs",
53
- "test": "node --test '*.test.mjs'"
44
+ "@modelcontextprotocol/sdk": "1.32.0",
45
+ "zod": "4.6.5",
46
+ "@jalco/ap-sdk": "0.4.1",
47
+ "tsx": "4.23.15"
54
48
  }
55
49
  }
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: c64-assembler
3
+ description: Use this skill to assemble C64 6510 source code with the ACME assembler and write the program (PRG) into the project. It reports source errors with file and line, and the symbols with their addresses or values. Use it to build a rebuilt or new program. Do not use it to run the program. Give the PRG to the c64-emulator skill for that.
4
+ ---
5
+
6
+ # c64-assembler
7
+
8
+ ## Purpose
9
+
10
+ Assemble C64 source code with ACME. Write the program into the project. Report diagnostics and symbols.
11
+
12
+ This skill does not run the program and does not change project knowledge.
13
+
14
+ ## Inputs
15
+
16
+ - The source root: the project directory that holds all source files of the program.
17
+ - The entry source: the main file, relative to the source root.
18
+ - The output path: where to write the PRG, relative to the project directory.
19
+ - Optional include directories, symbol definitions and a start address.
20
+
21
+ ## Tools / execution path
22
+
23
+ The script runs ACME on this machine. It copies the source root to a temporary directory, assembles it there and writes the program into the project. The host runtime is not necessary.
24
+
25
+ ```
26
+ node <skill>/scripts/assemble.ts --source-root src --entry main.a --out build/game.prg
27
+ ```
28
+
29
+ | Option | Meaning |
30
+ |---|---|
31
+ | `--include lib` | An include directory, relative to the source root. Use it again for more directories. |
32
+ | `--define DEBUG=1` | An assembler symbol: an integer, also as `$` hex, or `true` or `false`. Use it again for more symbols. |
33
+ | `--set-pc '$0801'` | The start address when the source does not set one. |
34
+
35
+ The script copies the full source root. Put all files that the program includes under it. Version control directories stay out.
36
+
37
+ The script prints one JSON object. The target CPU is the 6510 and the output is always a C64 PRG.
38
+
39
+ ## Workflow
40
+
41
+ 1. Find the source root and the entry source. The entry source sets its own start address with `* = $0801` or similar.
42
+ 2. Run the script.
43
+ 3. If `assembled` is `true`, the PRG is at `output`. `loadRange` tells where it loads. `symbols` gives each symbol as an `address` or a `value`.
44
+ 4. If `assembled` is `false`, read `diagnostics`. Each one has a severity, a file, a line and a message. Correct the source and run the script again.
45
+ 5. To run the program, give the PRG path to the c64-emulator skill.
46
+
47
+ ## Knowledge
48
+
49
+ Automatic write:
50
+ - none.
51
+
52
+ Do not store:
53
+ - diagnostics
54
+ - build output
55
+
56
+ A symbol from the rebuilt source can give the address of a routine in the rebuild. Use it to compare runs with the c64-testing skill.
57
+
58
+ ## Result
59
+
60
+ The PRG in the project at the output path, with its load range and symbols. Or the source diagnostics when the assembly failed.
61
+
62
+ The exit status is 0 for a PRG. The exit status is 1 for `assembled: false` and for an error. The exit status is 2 for a wrong argument.
63
+
64
+ ## Failure and conflicts
65
+
66
+ - `assembled: false`: the source has errors. This is a normal result. Correct the source.
67
+ - `installation-incomplete`: ACME is not installed on this machine. Tell the user the message.
68
+ - `invalid-input`: an option or a path is wrong. Read the message and correct the call.
69
+
70
+ ## Handoffs
71
+
72
+ - Run or debug the program → c64-emulator
73
+ - Compare the rebuild with the original → c64-testing
@@ -0,0 +1,103 @@
1
+ // The c64-assembler script: assembles project source with ACME, which it runs
2
+ // itself, and writes the program into the project.
3
+
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
+ import { dirname } from "node:path";
6
+ import { parseArgs } from "node:util";
7
+
8
+ import { formatC64Address, parseC64Address } from "#src/c64.ts";
9
+ import { readProjectTree } from "#src/host-client/transfer.ts";
10
+ import { assemble } from "#src/native/acme.ts";
11
+ import { localToolContext } from "#src/native/local.ts";
12
+ import type { AcmeParams } from "#src/native/types.ts";
13
+ import { resolveProjectPath } from "#src/project.ts";
14
+ import { runScript, UsageError } from "#src/script.ts";
15
+
16
+ const USAGE = `assemble.ts --source-root <dir> --entry <file> --out <file.prg> [options]
17
+
18
+ --source-root <dir> project directory to assemble from; all of it is sent
19
+ --entry <file> the main source file, relative to the source root
20
+ --out <file.prg> where to write the program, relative to the project
21
+ --include <dir> include directory relative to the source root (repeatable)
22
+ --define NAME=VALUE assembler symbol: an integer ($hex allowed) or true/false (repeatable)
23
+ --set-pc <address> start address when the source sets none, for example '$0801'
24
+
25
+ The result is one JSON object. Addresses are $ and four hex digits.`;
26
+
27
+ function parseDefine(text: string): [string, number | boolean] {
28
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.+)$/.exec(text);
29
+ if (match === null) throw new UsageError(`--define needs NAME=VALUE, not ${text}`);
30
+ const value = match[2]!;
31
+ if (value === "true" || value === "false") return [match[1]!, value === "true"];
32
+ if (/^\$[0-9a-fA-F]+$/.test(value)) return [match[1]!, Number.parseInt(value.slice(1), 16)];
33
+ if (/^-?\d+$/.test(value)) return [match[1]!, Number(value)];
34
+ throw new UsageError(`--define ${match[1]} needs an integer or true/false, not ${value}`);
35
+ }
36
+
37
+ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }> {
38
+ const { values } = parseArgs({
39
+ args: argv,
40
+ strict: true,
41
+ options: {
42
+ "source-root": { type: "string" },
43
+ entry: { type: "string" },
44
+ out: { type: "string" },
45
+ include: { type: "string", multiple: true },
46
+ define: { type: "string", multiple: true },
47
+ "set-pc": { type: "string" },
48
+ help: { type: "boolean" },
49
+ },
50
+ });
51
+ if (values.help) throw new UsageError("");
52
+ const sourceRoot = values["source-root"];
53
+ const entry = values.entry;
54
+ const out = values.out;
55
+ if (sourceRoot === undefined || entry === undefined || out === undefined) throw new UsageError("--source-root, --entry and --out are necessary");
56
+ let outPath: string;
57
+ try {
58
+ outPath = resolveProjectPath(out);
59
+ } catch {
60
+ throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${out}`);
61
+ }
62
+ const defines = Object.fromEntries((values.define ?? []).map(parseDefine));
63
+ let setPc: number | undefined;
64
+ if (values["set-pc"] !== undefined) {
65
+ try {
66
+ setPc = parseC64Address(values["set-pc"]);
67
+ } catch {
68
+ throw new UsageError(`--set-pc must be $ followed by four hex digits, not ${values["set-pc"]}`);
69
+ }
70
+ }
71
+ const tree = readProjectTree(sourceRoot);
72
+ if (!tree.files.some((file) => file.path === entry)) throw new UsageError(`--entry ${entry} is not a file in ${sourceRoot}`);
73
+ const params: AcmeParams = { files: tree.files, entrySource: entry, includeDirs: values.include ?? [], defines, ...(setPc === undefined ? {} : { setPc }) };
74
+
75
+ const { result, attachments } = await assemble(params, tree.contents, localToolContext());
76
+ if (!result.assembled) return { output: { assembled: false, diagnostics: result.diagnostics }, failed: true };
77
+ mkdirSync(dirname(outPath), { recursive: true });
78
+ writeFileSync(outPath, attachments![0]!);
79
+ const range = result.loadRange!;
80
+ return {
81
+ output: {
82
+ assembled: true,
83
+ output: out,
84
+ loadRange: { start: formatC64Address(range.start), end: formatC64Address(range.end), bytes: range.bytes },
85
+ symbols: result.symbols!.map((symbol) =>
86
+ symbol.kind === "address"
87
+ ? { name: symbol.name, kind: "address", address: formatC64Address(symbol.address), used: symbol.used }
88
+ : { name: symbol.name, kind: "constant", value: symbol.value, used: symbol.used },
89
+ ),
90
+ diagnostics: result.diagnostics,
91
+ },
92
+ failed: false,
93
+ };
94
+ }
95
+
96
+ await runScript(
97
+ async () => {
98
+ const { output, failed } = await run(process.argv.slice(2));
99
+ if (failed) process.exitCode = 1;
100
+ return output;
101
+ },
102
+ { usage: USAGE },
103
+ );