@henols/c64-re-tools 0.1.4
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/README.md +61 -0
- package/bin/cli.mjs +226 -0
- package/package.json +53 -0
- package/skills/acme-build/SKILL.md +224 -0
- package/skills/acme-build/scripts/acme.mjs +263 -0
- package/skills/acme-build/template.a +39 -0
- package/skills/c64-memory-mapping/SKILL.md +199 -0
- package/skills/c64-memory-mapping/memmap.json +8800 -0
- package/skills/c64-memory-mapping/scripts/driver.mjs +553 -0
- package/skills/c64-program-recon/SKILL.md +172 -0
- package/skills/c64-program-recon/references/control-flow.md +174 -0
- package/skills/c64-program-recon/references/graphics.md +73 -0
- package/skills/c64-program-recon/references/observation-hazards.md +118 -0
- package/skills/c64-program-recon/references/reconstruction.md +128 -0
- package/skills/c64-program-recon/references/sound-and-input.md +68 -0
- package/skills/c64-program-recon/references/tool-selection.md +55 -0
- package/skills/c64-program-recon/scripts/derive.mjs +364 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +62 -0
- package/skills/c64-provenance-diff/SKILL.md +257 -0
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +981 -0
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +665 -0
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +383 -0
- package/skills/c64-ram-capture/SKILL.md +306 -0
- package/skills/c64-ram-capture/scripts/compare.mjs +258 -0
- package/skills/c64-ram-capture/scripts/d64-parse.mjs +243 -0
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +243 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +317 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +133 -0
- package/skills/c64-ram-capture/scripts/project-paths.mjs +81 -0
- package/skills/c64-ram-capture/scripts/releases.mjs +109 -0
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +75 -0
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +575 -0
- package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +339 -0
- package/skills/c64-ram-capture/templates/capture-record.template.md +59 -0
- package/skills/vice-wedge-triage/SKILL.md +149 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @henols/c64-re-tools
|
|
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.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
From the project you want to set up:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx @henols/c64-re-tools
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
That will:
|
|
17
|
+
|
|
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`
|
|
21
|
+
|
|
22
|
+
Then restart Claude Code in that project.
|
|
23
|
+
|
|
24
|
+
### Options
|
|
25
|
+
|
|
26
|
+
| Option | Effect |
|
|
27
|
+
| --- | --- |
|
|
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. |
|
|
33
|
+
|
|
34
|
+
Re-running is safe: existing skills and an existing `vice` entry are kept unless
|
|
35
|
+
you pass `--force`.
|
|
36
|
+
|
|
37
|
+
## What gets installed
|
|
38
|
+
|
|
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.
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- **This installer** runs on Node ≥ 18.
|
|
46
|
+
- **The VICE MCP server** it wires up requires **Node ≥ 22.18** (or ≥ 23.6), 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.
|
|
49
|
+
|
|
50
|
+
## Alternative: Claude Code plugin
|
|
51
|
+
|
|
52
|
+
If you use Claude Code, you can instead install everything as a plugin:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
/plugin marketplace add henols/c64-re-tools
|
|
56
|
+
/plugin install c64-re-tools@c64-re-tools
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT © Henrik Olsson
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @henols/c64-re-tools installer.
|
|
3
|
+
//
|
|
4
|
+
// Installs the C64 reverse-engineering skills and wires the VICE MCP server into
|
|
5
|
+
// a target project:
|
|
6
|
+
// npx @henols/c64-re-tools [targetDir] [--force] [--dry-run] [--vendor]
|
|
7
|
+
//
|
|
8
|
+
// * copies the bundled skills into <target>/.claude/skills/
|
|
9
|
+
// * merges a `vice` server entry into <target>/.mcp.json (never clobbering
|
|
10
|
+
// other servers), launching it via `npx -y @henols/vice-mcp`
|
|
11
|
+
// * with --vendor, also `npm install`s @henols/vice-mcp into the project and
|
|
12
|
+
// wires .mcp.json to the local copy (pinned/offline use)
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
existsSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readdirSync,
|
|
22
|
+
cpSync,
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
|
|
25
|
+
const HERE = dirname(fileURLToPath(import.meta.url)); // installer/bin (packed) or repo installer/bin (dev)
|
|
26
|
+
const PKG_ROOT = dirname(HERE);
|
|
27
|
+
const SKILLS_SRC = join(PKG_ROOT, "skills");
|
|
28
|
+
|
|
29
|
+
const SELF = readJson(join(PKG_ROOT, "package.json")) ?? {};
|
|
30
|
+
const SELF_VERSION = typeof SELF.version === "string" ? SELF.version : "0.0.0";
|
|
31
|
+
const MCP_PKG = "@henols/vice-mcp";
|
|
32
|
+
// Wire the project to the exact vice-mcp version this installer was built against.
|
|
33
|
+
const MCP_VERSION =
|
|
34
|
+
(SELF.dependencies && typeof SELF.dependencies[MCP_PKG] === "string"
|
|
35
|
+
? SELF.dependencies[MCP_PKG].replace(/^[\^~]/, "")
|
|
36
|
+
: SELF_VERSION);
|
|
37
|
+
|
|
38
|
+
function readJson(path) {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseArgs(argv) {
|
|
47
|
+
const opts = { force: false, dryRun: false, vendor: false, help: false, target: undefined };
|
|
48
|
+
for (const arg of argv) {
|
|
49
|
+
if (arg === "--force") opts.force = true;
|
|
50
|
+
else if (arg === "--dry-run" || arg === "-n") opts.dryRun = true;
|
|
51
|
+
else if (arg === "--vendor") opts.vendor = true;
|
|
52
|
+
else if (arg === "--help" || arg === "-h") opts.help = true;
|
|
53
|
+
else if (arg.startsWith("-")) {
|
|
54
|
+
console.error(`c64-re-tools: unknown flag ${JSON.stringify(arg)} (try --help)`);
|
|
55
|
+
process.exit(2);
|
|
56
|
+
} else if (opts.target === undefined) opts.target = arg;
|
|
57
|
+
else {
|
|
58
|
+
console.error(`c64-re-tools: unexpected extra argument ${JSON.stringify(arg)} (try --help)`);
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return opts;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const HELP = `c64-re-tools -- install the C64 reverse-engineering skills + VICE MCP server into a project
|
|
66
|
+
|
|
67
|
+
Usage:
|
|
68
|
+
npx @henols/c64-re-tools [targetDir] [options]
|
|
69
|
+
|
|
70
|
+
Arguments:
|
|
71
|
+
targetDir Project to install into (default: current directory)
|
|
72
|
+
|
|
73
|
+
Options:
|
|
74
|
+
--force Overwrite existing skills and an existing 'vice' MCP entry
|
|
75
|
+
--vendor Also 'npm install -D ${MCP_PKG}' into the project and wire
|
|
76
|
+
.mcp.json to the local copy (pinned/offline), instead of npx
|
|
77
|
+
--dry-run, -n Show what would change without writing anything
|
|
78
|
+
--help, -h Show this help
|
|
79
|
+
|
|
80
|
+
What it does:
|
|
81
|
+
1. Copies bundled skills into <target>/.claude/skills/
|
|
82
|
+
2. Adds a 'vice' server to <target>/.mcp.json (other servers are preserved)
|
|
83
|
+
|
|
84
|
+
Requires Node >= 22.18 to RUN the vice MCP server (this installer runs on Node >= 18).`;
|
|
85
|
+
|
|
86
|
+
function viceServerEntry(vendor) {
|
|
87
|
+
return {
|
|
88
|
+
command: "npx",
|
|
89
|
+
args: vendor ? [MCP_PKG] : ["-y", `${MCP_PKG}@${MCP_VERSION}`],
|
|
90
|
+
timeout: 150000,
|
|
91
|
+
env: { MASTRA_TELEMETRY_DISABLED: "1" },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function installSkills(target, { force, dryRun }) {
|
|
96
|
+
if (!existsSync(SKILLS_SRC)) {
|
|
97
|
+
console.error(
|
|
98
|
+
`c64-re-tools: FAIL -- bundled skills not found at ${SKILLS_SRC}. ` +
|
|
99
|
+
`(In a dev checkout, run 'node scripts/sync-skills.mjs' first.)`
|
|
100
|
+
);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const names = readdirSync(SKILLS_SRC, { withFileTypes: true })
|
|
104
|
+
.filter((d) => d.isDirectory())
|
|
105
|
+
.map((d) => d.name)
|
|
106
|
+
.sort();
|
|
107
|
+
const destRoot = join(target, ".claude", "skills");
|
|
108
|
+
const installed = [];
|
|
109
|
+
const skipped = [];
|
|
110
|
+
for (const name of names) {
|
|
111
|
+
const dest = join(destRoot, name);
|
|
112
|
+
if (existsSync(dest) && !force) {
|
|
113
|
+
skipped.push(name);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!dryRun) {
|
|
117
|
+
mkdirSync(destRoot, { recursive: true });
|
|
118
|
+
cpSync(join(SKILLS_SRC, name), dest, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
installed.push(name);
|
|
121
|
+
}
|
|
122
|
+
return { destRoot, installed, skipped, total: names.length };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function wireMcp(target, { force, dryRun, vendor }) {
|
|
126
|
+
const mcpPath = join(target, ".mcp.json");
|
|
127
|
+
let config = { mcpServers: {} };
|
|
128
|
+
if (existsSync(mcpPath)) {
|
|
129
|
+
const parsed = readJson(mcpPath);
|
|
130
|
+
if (parsed === undefined) {
|
|
131
|
+
console.error(
|
|
132
|
+
`c64-re-tools: FAIL -- ${mcpPath} exists but is not valid JSON. ` +
|
|
133
|
+
`Refusing to overwrite it; fix or remove it and re-run.`
|
|
134
|
+
);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
config = parsed;
|
|
138
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
139
|
+
console.error(`c64-re-tools: FAIL -- ${mcpPath} is not a JSON object.`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
if (typeof config.mcpServers !== "object" || config.mcpServers === null) {
|
|
143
|
+
config.mcpServers = {};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const existed = Object.prototype.hasOwnProperty.call(config.mcpServers, "vice");
|
|
147
|
+
let action;
|
|
148
|
+
if (existed && !force) {
|
|
149
|
+
action = "kept"; // leave the user's existing entry alone
|
|
150
|
+
} else {
|
|
151
|
+
action = existed ? "updated" : "added";
|
|
152
|
+
if (!dryRun) {
|
|
153
|
+
config.mcpServers.vice = viceServerEntry(vendor);
|
|
154
|
+
mkdirSync(dirname(mcpPath), { recursive: true });
|
|
155
|
+
writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { mcpPath, action };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function vendorInstall(target, { dryRun }) {
|
|
162
|
+
const spec = `${MCP_PKG}@${MCP_VERSION}`;
|
|
163
|
+
if (dryRun) return { ran: false, spec };
|
|
164
|
+
const res = spawnSync("npm", ["install", "--save-dev", spec], {
|
|
165
|
+
cwd: target,
|
|
166
|
+
stdio: "inherit",
|
|
167
|
+
});
|
|
168
|
+
if (res.status !== 0) {
|
|
169
|
+
console.error(
|
|
170
|
+
`c64-re-tools: WARN -- 'npm install --save-dev ${spec}' exited ${res.status}. ` +
|
|
171
|
+
`Skills and .mcp.json were still written; install the package manually if needed.`
|
|
172
|
+
);
|
|
173
|
+
return { ran: true, ok: false, spec };
|
|
174
|
+
}
|
|
175
|
+
return { ran: true, ok: true, spec };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function main() {
|
|
179
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
180
|
+
if (opts.help) {
|
|
181
|
+
console.log(HELP);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const target = resolve(opts.target ?? process.cwd());
|
|
185
|
+
if (!existsSync(target)) {
|
|
186
|
+
console.error(`c64-re-tools: FAIL -- target directory does not exist: ${target}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.error(`c64-re-tools ${SELF_VERSION} -> ${target}${opts.dryRun ? " (dry run)" : ""}`);
|
|
191
|
+
|
|
192
|
+
const skills = installSkills(target, opts);
|
|
193
|
+
if (opts.vendor) vendorInstall(target, opts);
|
|
194
|
+
const mcp = wireMcp(target, opts);
|
|
195
|
+
|
|
196
|
+
// Summary
|
|
197
|
+
console.error("");
|
|
198
|
+
console.error(` skills -> ${skills.destRoot}`);
|
|
199
|
+
console.error(
|
|
200
|
+
` ${skills.installed.length} installed${
|
|
201
|
+
opts.force ? "" : `, ${skills.skipped.length} already present (use --force to overwrite)`
|
|
202
|
+
} of ${skills.total}`
|
|
203
|
+
);
|
|
204
|
+
if (skills.installed.length) console.error(` + ${skills.installed.join(", ")}`);
|
|
205
|
+
if (skills.skipped.length && !opts.force)
|
|
206
|
+
console.error(` = ${skills.skipped.join(", ")} (kept)`);
|
|
207
|
+
console.error(` mcp -> ${mcp.mcpPath}`);
|
|
208
|
+
if (mcp.action === "kept") {
|
|
209
|
+
console.error(` 'vice' already configured -- kept (use --force to overwrite)`);
|
|
210
|
+
} else {
|
|
211
|
+
console.error(
|
|
212
|
+
` 'vice' ${mcp.action}${
|
|
213
|
+
opts.vendor ? ` (local ${MCP_PKG})` : ` (npx -y ${MCP_PKG}@${MCP_VERSION})`
|
|
214
|
+
}`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
console.error("");
|
|
218
|
+
if (opts.dryRun) {
|
|
219
|
+
console.error("Dry run -- nothing was written.");
|
|
220
|
+
} else {
|
|
221
|
+
console.error("Done. Restart Claude Code in this project so it picks up the skills and MCP server.");
|
|
222
|
+
console.error("Note: running the vice MCP server requires Node >= 22.18 (or >= 23.6).");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@henols/c64-re-tools",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Installer that adds the C64 reverse-engineering skills and the VICE emulator MCP server (@henols/vice-mcp) to a project.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"c64-re-tools": "bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"skills/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "Henrik Olsson",
|
|
23
|
+
"url": "https://github.com/henols"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/henols/c64-re-tools.git",
|
|
28
|
+
"directory": "installer"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/henols/c64-re-tools#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/henols/c64-re-tools/issues"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"c64",
|
|
36
|
+
"commodore-64",
|
|
37
|
+
"vice",
|
|
38
|
+
"6502",
|
|
39
|
+
"6510",
|
|
40
|
+
"acme",
|
|
41
|
+
"reverse-engineering",
|
|
42
|
+
"mcp",
|
|
43
|
+
"claude",
|
|
44
|
+
"installer"
|
|
45
|
+
],
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@henols/vice-mcp": "0.1.4"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"sync-skills": "node scripts/sync-skills.mjs",
|
|
51
|
+
"prepack": "node scripts/sync-skills.mjs"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: acme-build
|
|
3
|
+
description: Assemble Commodore 64 6510 assembly with the ACME cross assembler. Use when asked to assemble, build, compile or link .a/.asm 6502/6510 source, produce a C64 .prg, scaffold a new C64 program, list the symbols a program uses, or turn a .prg back into ACME source.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Assembling C64 source with ACME
|
|
7
|
+
|
|
8
|
+
Source in, `.prg` out. Everything goes through one script:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
A=.claude/skills/acme-build/scripts/acme.mjs # from the repo root
|
|
12
|
+
|
|
13
|
+
node $A new game.asm # scaffold a C64 program
|
|
14
|
+
node $A build game.asm # assemble -> .prg .sym .vs .rep
|
|
15
|
+
node $A sym game.asm # the symbols the program uses
|
|
16
|
+
node $A disasm game.prg # object code back into ACME source
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The script wraps `acme` and `toacme` and nothing else — **assembling only**. Running
|
|
20
|
+
the result on a C64 belongs to the emulator skills (`acme.mjs:3-4` says so, and the
|
|
21
|
+
absent `run` verb is not an omission). It contacts nothing.
|
|
22
|
+
|
|
23
|
+
Options: `-o FILE` `--out-dir DIR` `-f FORMAT` `--setpc ADDR` `-DSYM=VAL`
|
|
24
|
+
`-I DIR` `--no-report` `--json`.
|
|
25
|
+
|
|
26
|
+
## Build
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node $A build game.asm
|
|
30
|
+
```
|
|
31
|
+
```
|
|
32
|
+
Saving 53 (0x35) bytes (0x801 - 0x836 exclusive).
|
|
33
|
+
built game.prg (55 bytes) load $0801-$0836 53 bytes of code
|
|
34
|
+
symbols: game.sym (4 used / 121 total)
|
|
35
|
+
debug labels: game.vs (4 addresses)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The indented line is ACME's own `-v1` note, passed straight through. Three side
|
|
39
|
+
files land next to the `.prg`, and `--no-report` drops the `.rep`:
|
|
40
|
+
|
|
41
|
+
| file | contents |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `.prg` | the program, with its load address |
|
|
44
|
+
| `.sym` | every symbol, with the used ones marked |
|
|
45
|
+
| `.vs` | address labels, ready for a debugger or monitor |
|
|
46
|
+
| `.rep` | each source line with the address and bytes it produced |
|
|
47
|
+
|
|
48
|
+
Only address-typed *and* referenced symbols survive into the `.vs` — raw
|
|
49
|
+
`--vicelabels` output lists constants too, and a debugger reading
|
|
50
|
+
`viccolor_WHITE = $1` would relabel the 6510 processor port at `$0001`
|
|
51
|
+
(`curateLabels`, `scripts/acme.mjs`) — hence 4 addresses against 121 total
|
|
52
|
+
symbols above. Load it with `mcp__plugin_c64-re-tools_vice__vice_symbols_load` (format `vice`), this
|
|
53
|
+
project's only route to the emulator (`.claude/CLAUDE.md` § Version Compatibility
|
|
54
|
+
/ § Emulator Access).
|
|
55
|
+
|
|
56
|
+
Re-run `build` until it exits 0 — a clean exit means every symbol resolved.
|
|
57
|
+
|
|
58
|
+
Add `--json` to act on diagnostics programmatically:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
node $A build game.asm --json
|
|
62
|
+
```
|
|
63
|
+
```json
|
|
64
|
+
{ "ok": false,
|
|
65
|
+
"diags": [ { "file": "game.asm", "line": 26, "severity": "error",
|
|
66
|
+
"zone": "Zone <untitled>",
|
|
67
|
+
"message": "Number does not fit in 8 bits." } ] }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Use the `.rep` listing to map source to memory:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
16 0801 0b080a00 !word .eol, 10 ; link to next line, line number
|
|
74
|
+
26 080d a900 lda #viccolor_BLACK
|
|
75
|
+
27 080f 8d21d0 sta vic_cbg ; $d021 background
|
|
76
|
+
29 0814 8d20d0 sta vic_cborder ; $d020 border
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Build variants with `-D`, giving each its own `-o` so the symbol files stay
|
|
80
|
+
separate:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
node $A build game.asm -DBORDER=2 -o v2.prg # -> v2.prg v2.sym v2.vs
|
|
84
|
+
node $A build game.asm -DBORDER=5 -o v5.prg # -> v5.prg v5.sym v5.vs
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Inspect the result with `od`:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
od -An -tx1 game.prg | head -2
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The first two bytes are the little-endian load address (`01 08` = `$0801`); code
|
|
94
|
+
follows.
|
|
95
|
+
|
|
96
|
+
## Writing source
|
|
97
|
+
|
|
98
|
+
Start from the scaffold — it carries a BASIC stub whose `SYS` target is computed,
|
|
99
|
+
so the entry point stays correct as the program grows:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
node $A new game.asm
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use the C64 symbol library instead of writing addresses by hand:
|
|
106
|
+
|
|
107
|
+
| `!source <...>` | gives you | example |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `<cbm/c64/vic.a>` | `vic_*` registers, `viccolor_*` constants | `vic_cborder` = `$d020` |
|
|
110
|
+
| `<cbm/c64/kernal.a>` | `k_*` KERNAL entry points `$ff81`–`$fff5` | `k_chrout` = `$ffd2` |
|
|
111
|
+
| `<cbm/c64/cia1.a>` / `<cbm/c64/cia2.a>` | `cia1_*` / `cia2_*` | keyboard, joystick, timers |
|
|
112
|
+
| `<cbm/c64/sid.a>` | `sid_*` | sound |
|
|
113
|
+
|
|
114
|
+
KERNAL routines use the **`k_`** prefix — `k_chrout`, `k_getin`, `k_setnam`,
|
|
115
|
+
`k_plot`. Several have aliases (`k_bsout` and `k_basout` are also `$ffd2`).
|
|
116
|
+
Run `node $A sym game.asm` to see what a build resolved:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
addr $80d entry
|
|
120
|
+
addr $ffd2 k_chrout
|
|
121
|
+
addr $d021 vic_cbg
|
|
122
|
+
addr $d020 vic_cborder
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Let `-o` name the output and leave `!to` out of the source, so the filename you
|
|
126
|
+
pass is the filename you get.
|
|
127
|
+
|
|
128
|
+
The 6510's illegal opcodes are always available: `lax dcp sax slo rla sre rra
|
|
129
|
+
isc anc alr arr sbx las tas sha shx shy jam`. Verified — `lax $fb / dcp $fc /
|
|
130
|
+
sax $fd / slo $02 / anc #$0f / sbx #$10` assembles to
|
|
131
|
+
`a7 fb c7 fc 87 fd 07 02 0b 0f cb 10`. Keep `!cpu 6510` at the top of any source
|
|
132
|
+
you also assemble by hand, so these stay recognised as mnemonics.
|
|
133
|
+
|
|
134
|
+
## Disassembly
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
node $A disasm game.prg
|
|
138
|
+
```
|
|
139
|
+
```
|
|
140
|
+
game.dis.a: 28 lines
|
|
141
|
+
Read it as a linear decode: trust the instruction stream, and
|
|
142
|
+
treat strings, tables and the BASIC stub as data. To reassemble,
|
|
143
|
+
define the out-of-range labels it emits (Ld020, Lffd2, ...) and
|
|
144
|
+
indent its illegal-opcode lines to the operand column.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The default output is `<stem>.dis.a` — one more `.a` file the agent's Read tool
|
|
148
|
+
refuses (see Troubleshooting). Pass a second positional ending `.asm` for an
|
|
149
|
+
agent-readable listing instead (same stdout shape, `game.dis.asm: 28 lines` in
|
|
150
|
+
place of the first line):
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
node $A disasm game.prg game.dis.asm
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`game.dis.asm` itself then holds:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
*=$0801
|
|
160
|
+
L0801 !by$0b;ANC# <- BASIC stub, read as data
|
|
161
|
+
L0802 php
|
|
162
|
+
L0805 SHX L3032, y
|
|
163
|
+
...
|
|
164
|
+
L080d lda #$00 <- code, decoded correctly
|
|
165
|
+
L080f sta Ld021
|
|
166
|
+
L0812 lda #$05
|
|
167
|
+
L0814 sta Ld020
|
|
168
|
+
L081e jsr Lffd2
|
|
169
|
+
L0824 rts
|
|
170
|
+
L0825 pha <- PETSCII string, read as data
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Read it as a linear decode: the instruction stream is accurate, and strings,
|
|
174
|
+
tables and the BASIC stub appear as instructions — interpret those regions as
|
|
175
|
+
data. To reassemble the listing, define the out-of-range labels it emits
|
|
176
|
+
(`Ld020`, `Lffd2`) and indent its illegal-opcode lines to the operand column.
|
|
177
|
+
|
|
178
|
+
## Setup
|
|
179
|
+
|
|
180
|
+
Put `acme` and `toacme` on `$PATH`. The wrapper auto-probes `$ACME`,
|
|
181
|
+
`/usr/local/share/acme`, `/usr/share/acme`, `/usr/lib/acme` and `~/.acme` itself,
|
|
182
|
+
so `$ACME` only needs setting by hand when calling `acme` directly. Verified live
|
|
183
|
+
in this container on 2026-08-04: the library resolves to `/usr/local/share/acme`
|
|
184
|
+
(`/usr/share/acme` doesn't exist), ACME release 0.97 "Zem" (31 Jan 2021) at
|
|
185
|
+
`/usr/bin/acme`. **Confidence: HIGH** — read off `acme --version` and the probe
|
|
186
|
+
result, not off a package manifest.
|
|
187
|
+
|
|
188
|
+
Copy `acme.mjs` into any project's `.claude/skills/acme-build/scripts/`, and
|
|
189
|
+
`template.a` into `.claude/skills/acme-build/`, to use this elsewhere.
|
|
190
|
+
|
|
191
|
+
## Which skill does what
|
|
192
|
+
|
|
193
|
+
This one turns source into bytes. It does not restate what the others carry.
|
|
194
|
+
|
|
195
|
+
| Need | Go to |
|
|
196
|
+
|---|---|
|
|
197
|
+
| Where to start on an unknown program, and which address to read next | `c64-program-recon` |
|
|
198
|
+
| What a specific address or bit means, or annotating a listing | `c64-memory-mapping` — `node … lookup '$D018'` |
|
|
199
|
+
| A verified 64K image, or comparing two captures | `c64-ram-capture` |
|
|
200
|
+
| **Source in, `.prg` out — and a first-pass dead listing back** | here |
|
|
201
|
+
|
|
202
|
+
## References
|
|
203
|
+
|
|
204
|
+
| File | Covers |
|
|
205
|
+
|---|---|
|
|
206
|
+
| `scripts/acme.mjs` | The driver. Its comments are the contract for every flag above |
|
|
207
|
+
| `template.a` | The scaffold `new` writes: BASIC stub with a computed `SYS`, the three `!source` libraries, no `!to` |
|
|
208
|
+
|
|
209
|
+
Findings that make RE faster go in `.planning/RE-FINDINGS.md` **at the moment you
|
|
210
|
+
find them**, graded with `Evidence:` and `Confidence:`. Promote by re-logging with
|
|
211
|
+
the new evidence, never by editing a grade in place. File-changing work enters
|
|
212
|
+
through a GSD command (`/gsd-quick`).
|
|
213
|
+
|
|
214
|
+
## Troubleshooting
|
|
215
|
+
|
|
216
|
+
| Symptom | Fix |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `install the ACME cross assembler and put acme on PATH` | Install ACME. |
|
|
219
|
+
| `for <...> includes, set $ACME to …` | `export ACME=<dir holding cbm/c64/vic.a>`. |
|
|
220
|
+
| `Value not defined (kernal_chrout)` | Use the `k_` prefix — `k_chrout`. `node $A sym` lists what resolved. |
|
|
221
|
+
| `Label name not in leftmost column` + `Syntax error` on a mnemonic | Add `!cpu 6510`. |
|
|
222
|
+
| `Output file already chosen` | Remove `!to` from the source and keep `-o`. |
|
|
223
|
+
| `Number does not fit in 8 bits` | Pass a value 0–255, or drop the `#` if you meant an address. |
|
|
224
|
+
| `This tool cannot read binary files. The file appears to be a binary .a file.` | The file is fine — ACME source is plain text; the agent's Read tool refuses the `.a` extension regardless of content, and Edit needs a prior successful Read. Scaffold and write source as `.asm` instead — the driver accepts `.a`/`.asm`/`.s`, all three assemble byte-identically (verified both directions in this container, 2026-08-04). To read an existing `.a` file, use `sed -n '1,60p' file.a`. |
|