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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +17 -3
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +5 -2
  6. package/skills/c64-assembler/scripts/assemble.ts +8 -1
  7. package/skills/c64-assembler/scripts/compare.ts +84 -0
  8. package/skills/c64-basic/SKILL.md +4 -5
  9. package/skills/c64-disk/SKILL.md +20 -6
  10. package/skills/c64-disk/scripts/containers.ts +124 -0
  11. package/skills/c64-disk/scripts/disk.ts +59 -5
  12. package/skills/c64-emulator/SKILL.md +12 -5
  13. package/skills/c64-knowledge/SKILL.md +68 -4
  14. package/skills/c64-knowledge/scripts/knowledge.ts +262 -30
  15. package/skills/c64-memory-map/SKILL.md +4 -1
  16. package/skills/c64-memory-map/scripts/memmap.ts +14 -3
  17. package/skills/c64-provenance/SKILL.md +38 -9
  18. package/skills/c64-provenance/scripts/compare.ts +60 -29
  19. package/skills/c64-provenance/scripts/provenance.ts +250 -19
  20. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  21. package/skills/c64-provenance/scripts/separate.ts +132 -0
  22. package/skills/c64-reverse-engineering/SKILL.md +65 -11
  23. package/skills/c64-source/SKILL.md +119 -0
  24. package/skills/c64-source/scripts/generate.ts +445 -0
  25. package/skills/c64-source/scripts/loader.ts +324 -0
  26. package/skills/c64-source/scripts/opcodes.ts +61 -0
  27. package/skills/c64-source/scripts/source.ts +560 -0
  28. package/skills/c64-static-analysis/SKILL.md +28 -5
  29. package/skills/c64-static-analysis/scripts/analyze.ts +107 -14
  30. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  31. package/skills/c64-static-analysis/scripts/findings.ts +6 -4
  32. package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
  33. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  34. package/skills/c64-testing/SKILL.md +27 -3
  35. package/skills/c64-testing/references/scenario-format.md +43 -3
  36. package/skills/c64-testing/scripts/run.ts +189 -0
  37. package/skills/c64-testing/scripts/scenario.ts +153 -135
  38. package/skills/c64-testing/scripts/test.ts +16 -6
  39. package/skills/c64-unpacker/SKILL.md +69 -18
  40. package/skills/c64-unpacker/scripts/evidence.ts +113 -41
  41. package/skills/c64-unpacker/scripts/loads.ts +197 -0
  42. package/skills/c64-unpacker/scripts/machine.ts +68 -0
  43. package/skills/c64-unpacker/scripts/stages.ts +67 -0
  44. package/skills/c64-unpacker/scripts/unpack.ts +429 -55
  45. package/src/c64.ts +126 -10
  46. package/src/cli/main.ts +7 -4
  47. package/src/cli/status.ts +11 -7
  48. package/src/host/main.ts +8 -3
  49. package/src/host/server.ts +6 -5
  50. package/src/host/tools/petcat.ts +5 -1
  51. package/src/host/tools/status.ts +8 -2
  52. package/src/host/vice/adapter.ts +25 -10
  53. package/src/host/vice/process.ts +10 -5
  54. package/src/host/vice/session.ts +89 -32
  55. package/src/host-client/autostart.ts +45 -0
  56. package/src/host-client/connect.ts +5 -2
  57. package/src/host-client/coverage.ts +54 -0
  58. package/src/host-client/steps.ts +170 -0
  59. package/src/host-client/transfer.ts +29 -3
  60. package/src/host-client/vice-session.ts +79 -17
  61. package/src/knowledge/history.ts +48 -13
  62. package/src/knowledge/import.ts +104 -57
  63. package/src/knowledge/read.ts +203 -52
  64. package/src/knowledge/schema.ts +94 -12
  65. package/src/knowledge/write.ts +433 -123
  66. package/src/mcp/main.ts +43 -15
  67. package/src/mcp/output.ts +32 -0
  68. package/src/mcp/server.ts +13 -4
  69. package/src/mcp/tools/debug.ts +110 -24
  70. package/src/mcp/tools/execution.ts +3 -2
  71. package/src/mcp/tools/input.ts +7 -4
  72. package/src/mcp/tools/media.ts +26 -9
  73. package/src/mcp/tools/memory.ts +57 -10
  74. package/src/mcp/tools/video.ts +9 -3
  75. package/src/native/discover.ts +43 -6
  76. package/src/native/ghidra/index.ts +19 -3
  77. package/src/native/ghidra/scripts/C64Export.java +33 -1
  78. package/src/native/status.ts +15 -3
  79. package/src/native/types.ts +30 -1
  80. package/src/package.ts +21 -0
  81. package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
  82. package/src/protocol/messages.ts +17 -1
  83. package/src/protocol/tools.ts +7 -3
  84. package/src/protocol/vice.ts +86 -24
  85. package/src/script.ts +3 -2
package/README.md CHANGED
@@ -19,7 +19,7 @@ The design is in [`docs/redesign/`](docs/redesign/README.md), and
19
19
 
20
20
  ## Install
21
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.
22
+ You need Node.js 24 or newer, and VICE 3.9 or later (`x64sc`, `c1541`, `petcat`) with its ROM files; use the latest VICE release. `status` and the host name the known flaws of an older VICE. ACME, DXA and Ghidra are needed only for the skills that use them: [docs/install-tools.md](docs/install-tools.md) has the steps for each system.
23
23
 
24
24
  Start the Host Runtime and leave it running:
25
25
 
@@ -39,6 +39,21 @@ claude
39
39
 
40
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).
41
41
 
42
+ ### Dev container
43
+
44
+ The agent runs in the container, VICE and the Host Runtime on the host. In `.devcontainer/devcontainer.json`:
45
+
46
+ ```json
47
+ "features": {
48
+ "https://github.com/henols/c64-re-tools/releases/latest/download/devcontainer-feature-c64-re-tools.tgz": { "target": "claude" }
49
+ },
50
+ "runArgs": ["--add-host=host.docker.internal:host-gateway"]
51
+ ```
52
+
53
+ Until 2.0.0 is released, the `latest` URL has no Feature. Use the prerelease: `https://github.com/henols/c64-re-tools/releases/download/v2.0.0-rc.4/devcontainer-feature-c64-re-tools.tgz` with `{ "target": "claude", "channel": "next" }`.
54
+
55
+ The Feature installs ACME, dxa, Java 21 and Ghidra, puts the toolkit into the project when the container is created, and updates it at each start. `target` is a harness or a comma list. `channel` is `latest` (default) or `next`. Debian and Ubuntu images only. `runArgs` is for Docker on Linux. Start the Host Runtime on the host as above.
56
+
42
57
  ### Settings
43
58
 
44
59
  | Variable | Read by | Meaning |
@@ -57,8 +72,6 @@ and settings stay unchanged.
57
72
 
58
73
  ## Use it with an agent
59
74
 
60
- ## Use it with an agent
61
-
62
75
  1. Install for your harness: `--target claude`, `codex`, `pi`, `opencode`, `gemini`, `copilot`, `cursor` or `windsurf`. Without `--target`, `install` installs for all of them.
63
76
  2. Start the Host Runtime and leave it running.
64
77
  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.
@@ -93,6 +106,7 @@ The agent keeps what it learns about the program in `.c64-re-tools/knowledge.db`
93
106
  | c64-unpacker | Packing evidence, and capture of the unpacked program from the emulator. |
94
107
  | c64-memory-map | Platform meaning of addresses and register values. |
95
108
  | c64-provenance | Byte comparison of releases. |
109
+ | c64-source | ACME source from the image and the knowledge, checked byte by byte, and a loader for the entry state. |
96
110
  | c64-assembler | ACME builds with diagnostics and symbols. |
97
111
  | c64-testing | Original-against-rebuild scenarios with PASS, FAIL or INCONCLUSIVE. |
98
112
 
@@ -2,22 +2,7 @@
2
2
  // only reads package.json, so a caller that needs the declaration does not
3
3
  // build the plugin.
4
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
- })();
5
+ import { PACKAGE } from "../src/package.ts";
21
6
 
22
7
  /**
23
8
  * The agent starts the MCP server through npx with the newest version of the
@@ -13,7 +13,8 @@ import { dirname, join, relative, resolve, sep } from "node:path";
13
13
 
14
14
  import { definePlugin, defineSkill, type Skill } from "@jalco/ap-sdk";
15
15
 
16
- import { MCP_SERVER, PACKAGE } from "./package.ts";
16
+ import { PACKAGE } from "../src/package.ts";
17
+ import { MCP_SERVER } from "./package.ts";
17
18
 
18
19
  const root = resolve(import.meta.dirname, "..");
19
20
  const SOURCE = join(root, "src");
@@ -27,9 +28,10 @@ const SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(?\s*|\bnew URL\(\s*)(["'])((?:\.\.?
27
28
 
28
29
  /**
29
30
  * 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
+ * the skill, and the name and version of c64-re-tools that the copy comes
32
+ * from, which src/package.ts reads.
31
33
  */
32
- export const SKILL_PACKAGE = `${JSON.stringify({ type: "module", version: PACKAGE.version, imports: { "#src/*": "./src/*" } }, null, 2)}\n`;
34
+ export const SKILL_PACKAGE = `${JSON.stringify({ name: PACKAGE.name, type: "module", version: PACKAGE.version, imports: { "#src/*": "./src/*" } }, null, 2)}\n`;
33
35
 
34
36
  /** The scripts that a SKILL.md runs, by file name. */
35
37
  export function scriptsOf(skillMarkdown: string): string[] {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/c64-re-tools",
3
- "version": "2.0.0-rc.3",
3
+ "version": "2.0.0-rc.5",
4
4
  "description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
5
5
  "license": "MIT",
6
6
  "author": "Henrik Olsson",
@@ -31,6 +31,7 @@ node <skill>/scripts/assemble.ts --source-root src --entry main.a --out build/ga
31
31
  | `--include lib` | An include directory, relative to the source root. Use it again for more directories. |
32
32
  | `--define DEBUG=1` | An assembler symbol: an integer, also as `$` hex, or `true` or `false`. Use it again for more symbols. |
33
33
  | `--set-pc '$0801'` | The start address when the source does not set one. |
34
+ | `--compare unpacked/image.json` | Check the program byte by byte against a reference. The reference is the `image.json` of the c64-unpacker skill, a PRG, or 64 KiB of memory. |
34
35
 
35
36
  The script copies the full source root. Put all files that the program includes under it. Version control directories stay out.
36
37
 
@@ -59,12 +60,14 @@ A symbol from the rebuilt source can give the address of a routine in the rebuil
59
60
 
60
61
  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
 
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
+ With `--compare`, `compare` gives `identical` and one check for each segment of the reference. A check is `identical`, `different` with the first different address, or `not covered` when the program does not hold the segment.
64
+
65
+ The exit status is 0 for a PRG. The exit status is 1 for `assembled: false`, for a `--compare` that is not identical, and for an error. The exit status is 2 for a wrong argument.
63
66
 
64
67
  ## Failure and conflicts
65
68
 
66
69
  - `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.
70
+ - `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
68
71
  - `invalid-input`: an option or a path is wrong. Read the message and correct the call.
69
72
 
70
73
  ## Handoffs
@@ -12,6 +12,7 @@ import { localToolContext } from "#src/native/local.ts";
12
12
  import type { AcmeParams } from "#src/native/types.ts";
13
13
  import { resolveProjectPath } from "#src/project.ts";
14
14
  import { runScript, UsageError } from "#src/script.ts";
15
+ import { compareBuild } from "./compare.ts";
15
16
 
16
17
  const USAGE = `assemble.ts --source-root <dir> --entry <file> --out <file.prg> [options]
17
18
 
@@ -21,6 +22,8 @@ const USAGE = `assemble.ts --source-root <dir> --entry <file> --out <file.prg> [
21
22
  --include <dir> include directory relative to the source root (repeatable)
22
23
  --define NAME=VALUE assembler symbol: an integer ($hex allowed) or true/false (repeatable)
23
24
  --set-pc <address> start address when the source sets none, for example '$0801'
25
+ --compare <file> check the program byte by byte against an image.json of the
26
+ c64-unpacker skill (each segment), a PRG, or 64 KiB of memory
24
27
 
25
28
  The result is one JSON object. Addresses are $ and four hex digits.`;
26
29
 
@@ -45,6 +48,7 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
45
48
  include: { type: "string", multiple: true },
46
49
  define: { type: "string", multiple: true },
47
50
  "set-pc": { type: "string" },
51
+ compare: { type: "string" },
48
52
  help: { type: "boolean" },
49
53
  },
50
54
  });
@@ -77,6 +81,7 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
77
81
  mkdirSync(dirname(outPath), { recursive: true });
78
82
  writeFileSync(outPath, attachments![0]!);
79
83
  const range = result.loadRange!;
84
+ const compared = values.compare === undefined ? undefined : compareBuild(attachments![0]!, values.compare);
80
85
  return {
81
86
  output: {
82
87
  assembled: true,
@@ -88,8 +93,10 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
88
93
  : { name: symbol.name, kind: "constant", value: symbol.value, used: symbol.used },
89
94
  ),
90
95
  diagnostics: result.diagnostics,
96
+ ...(compared === undefined ? {} : { compare: compared }),
91
97
  },
92
- failed: false,
98
+ // A build that differs from its reference is a failed check.
99
+ failed: compared !== undefined && !compared.identical,
93
100
  };
94
101
  }
95
102
 
@@ -0,0 +1,84 @@
1
+ // The byte check of a build (D47): the assembled program against the image
2
+ // of the unpacker (image.json with image.bin), a reference PRG, or 64 KiB of
3
+ // memory, segment by segment.
4
+
5
+ import { posix } from "node:path";
6
+
7
+ import { formatC64Address, prgRange } from "#src/c64.ts";
8
+ import { readProjectFile } from "#src/host-client/transfer.ts";
9
+ import { UsageError } from "#src/script.ts";
10
+
11
+ export interface SegmentCheck {
12
+ start: string;
13
+ end: string;
14
+ result: "identical" | "different" | "not covered";
15
+ /** Bytes of the segment that the build does not hold. */
16
+ uncovered?: number;
17
+ differences?: number;
18
+ firstDifference?: { address: string; built: number; reference: number };
19
+ }
20
+
21
+ const FULL = 0x10000;
22
+
23
+ /** The reference as memory and the ranges to check in it. */
24
+ function reference(path: string, built: { start: number; end: number }): { memory: (address: number) => number | undefined; segments: Array<{ start: number; end: number }> } {
25
+ const { bytes } = readProjectFile(path);
26
+ if (path.endsWith(".json")) {
27
+ let manifest: { image?: unknown; segments?: unknown };
28
+ try {
29
+ manifest = JSON.parse(bytes.toString("utf8")) as typeof manifest;
30
+ } catch (error) {
31
+ throw new UsageError(`--compare ${path} is not JSON: ${(error as Error).message}`);
32
+ }
33
+ const segments = manifest.segments;
34
+ if (typeof manifest.image !== "string" || !Array.isArray(segments) || segments.length === 0) throw new UsageError(`--compare ${path} is not an image.json: it needs image and segments`);
35
+ const image = readProjectFile(posix.join(posix.dirname(path), manifest.image)).bytes;
36
+ if (image.length !== FULL) throw new UsageError(`the image of ${path} has ${image.length} bytes, not 65536`);
37
+ return {
38
+ memory: (address) => image[address],
39
+ segments: segments.map((segment: { start?: unknown; end?: unknown }) => {
40
+ const parse = (value: unknown) => (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value) ? Number.parseInt(value.slice(1), 16) : NaN);
41
+ const range = { start: parse(segment.start), end: parse(segment.end) };
42
+ if (!(range.start <= range.end)) throw new UsageError(`--compare ${path}: a segment needs start and end as $ addresses`);
43
+ return range;
44
+ }),
45
+ };
46
+ }
47
+ if (bytes.length === FULL && !path.endsWith(".prg")) return { memory: (address) => bytes[address], segments: [built] };
48
+ let range: ReturnType<typeof prgRange>;
49
+ try {
50
+ range = prgRange(bytes);
51
+ } catch (error) {
52
+ throw new UsageError(`--compare ${path}: ${(error as Error).message}`);
53
+ }
54
+ return { memory: (address) => (address >= range.start && address <= range.end ? range.body[address - range.start] : undefined), segments: [{ start: range.start, end: range.end }] };
55
+ }
56
+
57
+ /** Compares the assembled PRG with the reference, segment by segment. */
58
+ export function compareBuild(prg: Uint8Array, path: string): { against: string; identical: boolean; segments: SegmentCheck[] } {
59
+ const built = prgRange(prg);
60
+ const { memory, segments } = reference(path, built);
61
+ const checks = segments.map((segment): SegmentCheck => {
62
+ const from = Math.max(segment.start, built.start);
63
+ const to = Math.min(segment.end, built.end);
64
+ const shown = { start: formatC64Address(segment.start), end: formatC64Address(segment.end) };
65
+ if (from > to) return { ...shown, result: "not covered", uncovered: segment.end - segment.start + 1 };
66
+ let differences = 0;
67
+ let first: SegmentCheck["firstDifference"];
68
+ for (let address = from; address <= to; address++) {
69
+ const builtByte = built.body[address - built.start]!;
70
+ const referenceByte = memory(address);
71
+ if (referenceByte === undefined || builtByte === referenceByte) continue;
72
+ differences++;
73
+ first ??= { address: formatC64Address(address), built: builtByte, reference: referenceByte };
74
+ }
75
+ const uncovered = segment.end - segment.start + 1 - (to - from + 1);
76
+ return {
77
+ ...shown,
78
+ result: differences > 0 ? "different" : uncovered > 0 ? "not covered" : "identical",
79
+ ...(uncovered > 0 ? { uncovered } : {}),
80
+ ...(differences > 0 ? { differences, firstDifference: first! } : {}),
81
+ };
82
+ });
83
+ return { against: path, identical: checks.every((check) => check.result === "identical"), segments: checks };
84
+ }
@@ -30,9 +30,9 @@ The script prints one JSON object. Addresses are `$` and four hex digits.
30
30
  1. Run the script on the program.
31
31
  2. Read `listing`. Each line starts with its BASIC line number. Keywords show in lower case. Control characters in strings show in braces, for example `{clr}`.
32
32
  3. Read `handoffs`. Each handoff has a `kind` (`sys` or `usr`) and the BASIC `line`.
33
- - A `sys` handoff with an `address`: BASIC calls the machine code at that address.
34
- - A handoff with `computed: true`: the address comes from a calculation at run time. Use the c64-emulator skill to find the address.
35
- 4. Compare the address with `loadRange` and `basicEnd`. From `basicEnd` to the end of `loadRange`, the file holds bytes that are not BASIC. Usually this is the machine code that the handoff calls.
33
+ - A `sys` handoff with an `address`: BASIC calls the machine code at that address. The script calculates a SYS argument with numbers, π, `+`, `-`, `*`, `/` and brackets. For example, `SYS π*656` calls `$080c`.
34
+ - A handoff with `computed: true`: the address comes from a calculation at run time, for example with `PEEK`. A `sys` handoff gives its argument in `expression`. Use the c64-emulator skill to find the address.
35
+ 4. Compare the address with `loadRange` and `basicEnd`. From `basicEnd` to the end of `loadRange`, the file holds bytes that are not BASIC. Usually this is the machine code that the handoff calls. Some programs have no end marker (two zero bytes) after the last line. The machine code starts directly after the last line. Then `basicEnd` is the first byte of that machine code.
36
36
 
37
37
  The address ranges are for a load to the load address of the file. A load without `,1` puts a BASIC program at `$0801`. Most loaders have `$0801` as their load address.
38
38
 
@@ -56,8 +56,7 @@ The exit status is 0 for a listing. The exit status is 1 for `decoded: false` an
56
56
 
57
57
  - `decoded: false`: the file is not a BASIC program. This is a normal result. Usually the file is machine code. Give it to the c64-static-analysis skill.
58
58
  - An empty `handoffs` list: the BASIC does not start machine code with SYS or USR. Do not make up a handoff. Look for a POKE into a vector or a LOAD of another file in the listing.
59
- - `installation-incomplete`: petcat is not on the host. Tell the user the message.
60
- - `operation-failed` with a message about the host runtime: the host runtime does not run. Tell the user to start the host runtime with `npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host`.
59
+ - `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
61
60
  - `not-found` or `invalid-input`: the path or an argument is wrong. Read the message and correct the call.
62
61
 
63
62
  ## Handoffs
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: c64-disk
3
- description: Use this skill to examine a C64 disk image (D64, D71, D81 or G64) and to extract files from it into the project. It shows the directory, the BAM, the directory entry of a file and the sector chain of a file. Do not use it to run a program or to decode BASIC. Give an extracted file to the c64-basic, c64-unpacker or c64-static-analysis skill.
3
+ description: Use this skill to examine a C64 disk image (D64, D71, D81 or G64), a T64 tape file or a CRT cartridge file. It extracts files into the project. It shows the directory, the BAM, the directory entry of a file and the sector chain of a file. It writes each bank of a cartridge as a PRG. Do not use it to run a program or to decode BASIC. Give an extracted file to the c64-basic, c64-unpacker or c64-static-analysis skill.
4
4
  ---
5
5
 
6
6
  # c64-disk
7
7
 
8
8
  ## Purpose
9
9
 
10
- Examine a C64 disk image. Extract a file from the image into the project.
10
+ Examine a C64 disk image, a T64 tape file or a CRT cartridge file. Extract a file from it into the project.
11
11
 
12
12
  This skill does not change the disk image and does not change project knowledge.
13
13
 
@@ -16,10 +16,11 @@ This skill does not change the disk image and does not change project knowledge.
16
16
  - The disk image: a `.d64`, `.d71`, `.d81` or `.g64` file, relative to the project directory.
17
17
  - For one file: the file name as the directory shows it.
18
18
  - To extract a file: the output path, relative to the project directory.
19
+ - A T64 tape file or a CRT cartridge file, relative to the project directory.
19
20
 
20
21
  ## Tools / execution path
21
22
 
22
- The script sends the image to the host runtime. The host runs c1541 and sends the result back. The script writes an extracted file into the project.
23
+ The script sends a disk image to the host runtime. The host runs c1541 and sends the result back. The script writes an extracted file into the project. It reads T64 and CRT files itself.
23
24
 
24
25
  ```
25
26
  node <skill>/scripts/disk.ts <action> <image> [name] [--out <file>]
@@ -32,6 +33,10 @@ node <skill>/scripts/disk.ts <action> <image> [name] [--out <file>]
32
33
  | The type, size and first block of one file | `entry original/game.d64 GAME` |
33
34
  | The track and sector of each block of one file | `chain original/game.d64 GAME` |
34
35
  | Extract one file | `read original/game.d64 GAME --out extracted/game.prg` |
36
+ | The files of a tape | `directory original/game.t64` |
37
+ | Extract one file of a tape | `read original/game.t64 GAME --out extracted/game.prg` |
38
+ | The banks of a cartridge | `directory original/game.crt` |
39
+ | Write each bank of a cartridge as a PRG | `crt original/game.crt --out cart` |
35
40
 
36
41
  The script prints one JSON object.
37
42
 
@@ -55,6 +60,15 @@ Do not store:
55
60
 
56
61
  A downstream skill records the meaning of the extracted content.
57
62
 
63
+ ## Tapes and cartridges
64
+
65
+ The script reads T64 and CRT files itself. The host runtime is not necessary for them.
66
+
67
+ - T64: `directory` and `read`. Many T64 files have a wrong end address. Then the entry has `fixedSize: true`, and the size comes from the position of the next file.
68
+ - CRT: `directory` and `crt`. The script reads hardware types 0 (normal 8K, 16K or Ultimax), 5 (Ocean), 19 (Magic Desk) and 32 (EasyFlash). It refuses other types by name. `crt` writes `crt.json` and one PRG for each CHIP packet, for example `bank3-8000.prg`.
69
+ - A cartridge bank uses the same addresses as other banks. Analyze each bank PRG with c64-static-analysis and `--space bank3`, and record its knowledge in space `bank3`.
70
+ - TAP: a TAP file holds tape signals, and turbo loaders have their own formats. The script does not read it. Start it with the c64-emulator skill, or capture its parts with `loads` of the c64-unpacker skill.
71
+
58
72
  ## Result
59
73
 
60
74
  The directory, the BAM, an entry or a sector chain as JSON. Or the extracted file in the project at the output path.
@@ -65,8 +79,7 @@ The exit status is 0 for a result with the file. The exit status is 1 for `found
65
79
 
66
80
  - `found: false`: the image has no file with that name. This is a normal result. Read the directory again and compare the names.
67
81
  - `media-error`: the image has a defect or is not a disk image. The message tells the problem, for example a sector chain that loops. Tell the user. Do not change the image.
68
- - `installation-incomplete`: c1541 is not on the host. Tell the user the message.
69
- - `operation-failed` with a message about the host runtime: the host runtime does not run. Tell the user to start the host runtime with `npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host`.
82
+ - `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
70
83
  - `invalid-input`: an argument or a path is wrong. Read the message and correct the call.
71
84
 
72
85
  A file that is not `closed` was not written fully. Its data can be incomplete.
@@ -75,5 +88,6 @@ A file that is not `closed` was not written fully. Its data can be incomplete.
75
88
 
76
89
  - BASIC program → c64-basic
77
90
  - Packed program → c64-unpacker
78
- - Machine code → c64-static-analysis
91
+ - Machine code, also a cartridge bank → c64-static-analysis
92
+ - A TAP file → c64-unpacker
79
93
  - Run the program → c64-emulator
@@ -0,0 +1,124 @@
1
+ // Cartridge and tape containers that c64-disk reads locally (D54): no host
2
+ // tool reads them. A CRT file holds CHIP packets, each a bank of ROM at a
3
+ // load address; a T64 file holds a directory of PRG files. TAP files hold
4
+ // raw pulses and go through the emulator only.
5
+
6
+ import { formatC64Address, petsciiNameToText } from "#src/c64.ts";
7
+
8
+ /** A refusal of a container file: the caller turns it into a usage error. */
9
+ export class ContainerError extends Error {}
10
+
11
+ /** The CRT hardware types this toolkit reads, by type number (D54). */
12
+ export const CRT_TYPES: Record<number, string> = { 0: "normal", 5: "Ocean", 19: "Magic Desk", 32: "EasyFlash" };
13
+
14
+ export interface CrtChip {
15
+ bank: number;
16
+ address: number;
17
+ size: number;
18
+ /** 0 ROM, 1 RAM, 2 flash. */
19
+ chipType: number;
20
+ data: Uint8Array;
21
+ }
22
+
23
+ export interface Crt {
24
+ name: string;
25
+ type: number;
26
+ typeName: string;
27
+ /** The EXROM and GAME lines at start: 0 is active. */
28
+ exrom: number;
29
+ game: number;
30
+ /** For type 0: 8K, 16K or Ultimax by the EXROM and GAME lines. */
31
+ mode?: "8k" | "16k" | "ultimax";
32
+ chips: CrtChip[];
33
+ }
34
+
35
+ const SIGNATURE = "C64 CARTRIDGE ";
36
+
37
+ export function parseCrt(bytes: Uint8Array): Crt {
38
+ const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
39
+ if (buffer.length < 0x40 || buffer.toString("latin1", 0, 16) !== SIGNATURE) throw new ContainerError("the file is not a CRT: it does not start with C64 CARTRIDGE");
40
+ const headerLength = buffer.readUInt32BE(0x10);
41
+ const type = buffer.readUInt16BE(0x16);
42
+ const exrom = buffer[0x18]!;
43
+ const game = buffer[0x19]!;
44
+ const typeName = CRT_TYPES[type];
45
+ if (typeName === undefined) {
46
+ throw new ContainerError(`the CRT has hardware type ${type}; this toolkit reads types ${Object.entries(CRT_TYPES).map(([number, name]) => `${number} (${name})`).join(", ")}`);
47
+ }
48
+ const name = buffer.toString("latin1", 0x20, 0x40).replace(/\0+$/, "");
49
+ const chips: CrtChip[] = [];
50
+ let offset = Math.max(headerLength, 0x40);
51
+ while (offset + 0x10 <= buffer.length) {
52
+ if (buffer.toString("latin1", offset, offset + 4) !== "CHIP") throw new ContainerError(`the CRT has no CHIP packet at offset ${offset}`);
53
+ const packetLength = buffer.readUInt32BE(offset + 4);
54
+ const chipType = buffer.readUInt16BE(offset + 8);
55
+ const bank = buffer.readUInt16BE(offset + 10);
56
+ const address = buffer.readUInt16BE(offset + 12);
57
+ const size = buffer.readUInt16BE(offset + 14);
58
+ if (packetLength < 0x10 || offset + packetLength > buffer.length) throw new ContainerError(`the CHIP packet at offset ${offset} runs past the end of the file`);
59
+ if (bank > 255) throw new ContainerError(`the CHIP packet at offset ${offset} has bank ${bank}; banks go up to 255`);
60
+ if (size > packetLength - 0x10 || address + size > 0x10000) throw new ContainerError(`the CHIP packet at offset ${offset} has a wrong size ${size} at ${formatC64Address(address)}`);
61
+ if (size > 0) chips.push({ bank, address, size, chipType, data: buffer.subarray(offset + 0x10, offset + 0x10 + size) });
62
+ offset += packetLength;
63
+ }
64
+ if (chips.length === 0) throw new ContainerError("the CRT has no CHIP packet with data");
65
+ const crt: Crt = { name, type, typeName, exrom, game, chips };
66
+ if (type === 0) crt.mode = exrom === 0 && game === 1 ? "8k" : exrom === 0 && game === 0 ? "16k" : "ultimax";
67
+ return crt;
68
+ }
69
+
70
+ export interface TapeEntry {
71
+ name: string;
72
+ nameBytes: number[];
73
+ /** The C64 file type byte of the entry. */
74
+ fileType: number;
75
+ start: number;
76
+ end: number;
77
+ bytes: number;
78
+ offset: number;
79
+ /** True when the end address in the directory was wrong and the size comes from the file layout. */
80
+ fixedSize?: boolean;
81
+ }
82
+
83
+ export interface Tape {
84
+ name: string;
85
+ entries: TapeEntry[];
86
+ }
87
+
88
+ export function parseT64(bytes: Uint8Array): Tape {
89
+ const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
90
+ if (buffer.length < 0x40 || !buffer.toString("latin1", 0, 32).startsWith("C64")) throw new ContainerError("the file is not a T64: it does not start with C64");
91
+ const maxEntries = buffer.readUInt16LE(0x22);
92
+ const name = buffer.toString("latin1", 0x28, 0x40).replace(/[\0 ]+$/, "");
93
+ const raw: Array<Omit<TapeEntry, "bytes" | "end"> & { end: number }> = [];
94
+ for (let index = 0; index < maxEntries; index++) {
95
+ const at = 0x40 + index * 0x20;
96
+ if (at + 0x20 > buffer.length) break;
97
+ if (buffer[at] !== 1) continue;
98
+ const nameField = [...buffer.subarray(at + 0x10, at + 0x20)];
99
+ while (nameField.length > 0 && (nameField.at(-1) === 0x20 || nameField.at(-1) === 0xa0 || nameField.at(-1) === 0x00)) nameField.pop();
100
+ raw.push({
101
+ name: petsciiNameToText(Uint8Array.from(nameField)),
102
+ nameBytes: nameField,
103
+ fileType: buffer[at + 1]!,
104
+ start: buffer.readUInt16LE(at + 2),
105
+ end: buffer.readUInt16LE(at + 4),
106
+ offset: buffer.readUInt32LE(at + 8),
107
+ });
108
+ }
109
+ // Many T64 files have a wrong end address: the next file or the end of the file bounds each one.
110
+ const offsets = [...raw.map((entry) => entry.offset), buffer.length].sort((a, b) => a - b);
111
+ const entries = raw.map((entry): TapeEntry => {
112
+ if (entry.offset >= buffer.length) throw new ContainerError(`the T64 entry ${entry.name} starts past the end of the file`);
113
+ const available = offsets.find((offset) => offset > entry.offset)! - entry.offset;
114
+ const stated = entry.end - entry.start;
115
+ const bytes = stated > 0 && stated <= available ? stated : Math.min(available, 0x10000 - entry.start);
116
+ return { ...entry, end: entry.start + bytes - 1, bytes, ...(bytes === stated ? {} : { fixedSize: true }) };
117
+ });
118
+ return { name, entries };
119
+ }
120
+
121
+ /** The PRG of a tape entry: its start address, then its bytes. */
122
+ export function tapeFile(bytes: Uint8Array, entry: TapeEntry): Buffer {
123
+ return Buffer.concat([Buffer.from([entry.start & 0xff, entry.start >> 8]), Buffer.from(bytes.subarray(entry.offset, entry.offset + entry.bytes))]);
124
+ }
@@ -1,13 +1,17 @@
1
1
  // The c64-disk script: inspects a project disk image with c1541 through the
2
- // Host Runtime and extracts files into the project.
2
+ // Host Runtime and extracts files into the project. T64 tape files and CRT
3
+ // cartridge files are read locally (D54): no host tool reads them.
3
4
 
4
5
  import { mkdirSync, writeFileSync } from "node:fs";
5
6
  import { dirname } from "node:path";
6
7
  import { parseArgs } from "node:util";
7
8
 
9
+ import { formatC64Address } from "#src/c64.ts";
8
10
  import { inspectDisk } from "#src/host-client/tools.ts";
11
+ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
9
12
  import { resolveProjectPath } from "#src/project.ts";
10
13
  import { runScript, UsageError } from "#src/script.ts";
14
+ import { ContainerError, parseCrt, parseT64, tapeFile } from "./containers.ts";
11
15
 
12
16
  const USAGE = `disk.ts <action> <image> [name] [--out <file>]
13
17
 
@@ -16,12 +20,16 @@ const USAGE = `disk.ts <action> <image> [name] [--out <file>]
16
20
  entry <image> <name> the directory entry of one file
17
21
  chain <image> <name> the track and sector of each block of one file
18
22
  read <image> <name> --out <file> write the bytes of one file into the project
23
+ crt <file.crt> --out <dir> write each CHIP packet of a cartridge as a PRG
19
24
 
20
25
  <image> is a .d64, .d71, .d81 or .g64 file, relative to the project directory.
26
+ directory and read also take a .t64 tape file, and directory a .crt cartridge
27
+ file; the script reads those itself. A CRT can have hardware type 0 (normal
28
+ 8K, 16K or Ultimax), 5 (Ocean), 19 (Magic Desk) or 32 (EasyFlash).
21
29
  <name> is the file name as the directory shows it. Write a byte that has no
22
30
  character as {$xx}, for example {$c1}. The result is one JSON object.`;
23
31
 
24
- const ACTIONS = ["directory", "bam", "entry", "chain", "read"] as const;
32
+ const ACTIONS = ["directory", "bam", "entry", "chain", "read", "crt"] as const;
25
33
  type Action = (typeof ACTIONS)[number];
26
34
 
27
35
  async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }> {
@@ -33,8 +41,8 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
33
41
  const named = action === "entry" || action === "chain" || action === "read";
34
42
  if (named && name === undefined) throw new UsageError(`${action} needs a file name`);
35
43
  if ((!named && name !== undefined) || extra.length > 0) throw new UsageError("too many arguments");
36
- if (action === "read" && values.out === undefined) throw new UsageError("read needs --out <file>");
37
- if (action !== "read" && values.out !== undefined) throw new UsageError("only read takes --out");
44
+ if ((action === "read" || action === "crt") && values.out === undefined) throw new UsageError(`${action} needs --out`);
45
+ if (action !== "read" && action !== "crt" && values.out !== undefined) throw new UsageError("only read and crt take --out");
38
46
  let outPath: string | undefined;
39
47
  if (values.out !== undefined) {
40
48
  try {
@@ -44,7 +52,17 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
44
52
  }
45
53
  }
46
54
 
47
- const result = await inspectDisk({ image, action: action as Action, ...(name === undefined ? {} : { name }) });
55
+ const kind = image.toLowerCase().endsWith(".t64") ? "t64" : image.toLowerCase().endsWith(".crt") ? "crt" : "disk";
56
+ if (action === "crt" && kind !== "crt") throw new UsageError("crt needs a .crt file");
57
+ if (kind !== "disk") {
58
+ try {
59
+ return local(kind, action as Action, image, name, values.out);
60
+ } catch (error) {
61
+ if (error instanceof ContainerError) throw new UsageError(`${image}: ${error.message}`);
62
+ throw error;
63
+ }
64
+ }
65
+ const result = await inspectDisk({ image, action: action as Exclude<Action, "crt">, ...(name === undefined ? {} : { name }) });
48
66
  if ("found" in result && !result.found) return { output: { found: false }, failed: true };
49
67
  if (result.action === "read" && result.found) {
50
68
  mkdirSync(dirname(outPath!), { recursive: true });
@@ -55,6 +73,42 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
55
73
  return { output, failed: false };
56
74
  }
57
75
 
76
+ /** A tape or cartridge file, read here (D54). */
77
+ function local(kind: "t64" | "crt", action: Action, image: string, name: string | undefined, out: string | undefined): { output: unknown; failed: boolean } {
78
+ const { bytes } = readProjectFile(image);
79
+ if (kind === "t64") {
80
+ if (action !== "directory" && action !== "read") throw new UsageError(`a T64 file has only directory and read, not ${action}`);
81
+ const tape = parseT64(bytes);
82
+ const shown = (entry: (typeof tape.entries)[number]) => ({
83
+ name: entry.name,
84
+ start: formatC64Address(entry.start),
85
+ end: formatC64Address(entry.end),
86
+ bytes: entry.bytes,
87
+ ...(entry.fixedSize ? { fixedSize: true } : {}),
88
+ });
89
+ if (action === "directory") return { output: { name: tape.name, entries: tape.entries.map(shown) }, failed: false };
90
+ const entry = tape.entries.find((candidate) => candidate.name.toLowerCase() === name!.toLowerCase());
91
+ if (entry === undefined) return { output: { found: false }, failed: true };
92
+ const file = tapeFile(bytes, entry);
93
+ writeProjectFile(out!, file, { what: "--out" });
94
+ return { output: { found: true, name: entry.name, bytes: file.length, output: out }, failed: false };
95
+ }
96
+ if (action !== "directory" && action !== "crt") throw new UsageError(`a CRT file has only directory and crt, not ${action}`);
97
+ const crt = parseCrt(bytes);
98
+ const chips = crt.chips.map((chip) => ({
99
+ bank: chip.bank,
100
+ address: formatC64Address(chip.address),
101
+ size: chip.size,
102
+ space: `bank${chip.bank}`,
103
+ file: `bank${chip.bank}-${chip.address.toString(16).padStart(4, "0")}.prg`,
104
+ }));
105
+ const output = { name: crt.name, type: crt.type, typeName: crt.typeName, exrom: crt.exrom, game: crt.game, ...(crt.mode === undefined ? {} : { mode: crt.mode }), chips };
106
+ if (action === "directory") return { output: { ...output, chips: chips.map(({ file: _file, ...chip }) => chip) }, failed: false };
107
+ crt.chips.forEach((chip, index) => writeProjectFile(`${out}/${chips[index]!.file}`, Buffer.concat([Buffer.from([chip.address & 0xff, chip.address >> 8]), chip.data]), { what: "--out" }));
108
+ writeProjectFile(`${out}/crt.json`, Buffer.from(`${JSON.stringify(output, null, 2)}\n`), { what: "--out" });
109
+ return { output: { ...output, out }, failed: false };
110
+ }
111
+
58
112
  await runScript(
59
113
  async () => {
60
114
  const { output, failed } = await run(process.argv.slice(2));
@@ -44,10 +44,14 @@ Use only the MCP tools. Each MCP session has its own C64. Nothing else can chang
44
44
  | Several of these from one moment | `c64_observe` |
45
45
  | Keys, joystick | `c64_keyboard`, `c64_joystick` |
46
46
  | Program and disk media | `c64_autostart`, `c64_program_load`, `c64_disk_attach` |
47
- | Machine state to go back to | `c64_snapshot` |
47
+ | Machine state to go back to, also in a later session | `c64_snapshot` (with `out` and `file`) |
48
48
 
49
49
  Each memory and register tool has a `space`. Use `c64` for the computer. Use `drive8` for the CPU of the 1541 disk drive. The memory `view` is `cpu` or `ram`. With `cpu`, you see what the CPU sees now, with ROM and I/O where the memory configuration puts them. With `ram`, you see the RAM under them.
50
50
 
51
+ For a large result, give `out` with a path in the project. `c64_memory_read`, `c64_disassemble`, `c64_cpu_history` and `c64_screen` then write the result to that file. The answer gives only the file, the number of bytes and a SHA-256. Use it for a memory dump of more than a few hundred bytes, a long listing, or a screen that the user wants to keep.
52
+
53
+ `c64_memmap` keeps RAM, ROM and I/O apart. Use action `summary` to see which code ran. Use action `read` with `out` to keep a coverage run as a file, for example `coverage/attract.bin`. Clear the map before each run that you want to keep apart.
54
+
51
55
  ## Workflow
52
56
 
53
57
  1. Read the project knowledge for the addresses in the question. Use the c64-knowledge skill.
@@ -64,7 +68,11 @@ Write operations need a stopped CPU. Stop the CPU with `c64_execution` action `p
64
68
 
65
69
  Step, next and until-return work only in space `c64`. The emulator cannot stop the disk drive CPU after one instruction. To find when the drive executes an address, use `c64_run_until` with an address target in space `drive8`, or a breakpoint in space `drive8`. The drive can execute a few more instructions before it stops. The computer completes its current instruction. The `pc` in the result is the drive address where it stopped.
66
70
 
67
- `c64_keyboard` puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware directly does not see them. Many games do this. For such a program, use `c64_joystick`, or open the window and ask the user to press the key.
71
+ `c64_keyboard` puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware directly does not see them. Many games do this. The emulator has no command that presses one key of the keyboard matrix. For such a program:
72
+
73
+ - VICE 3.10 and older have a known flaw: bits 5-7 of `$DC00` and `$DC01` are always 0. Then `emulatorFlaw` in the result of `c64_keyboard` and `c64_joystick` gives the flaw. With this flaw, a wait for any key that reads all keyboard columns at one time ends without a key. A wait for one specific key, or a compare of the full byte of a joystick port, never ends. Tell the user about the flaw. The next VICE release and the VICE nightly builds do not have it. Joystick port 1 uses the same lines as the keyboard.
74
+ - For one specific key, for example F1 or RUN/STOP, open the window and ask the user to press the key.
75
+ - Do not change the program to make it accept a key.
68
76
 
69
77
  Before a risky experiment, save the state with `c64_snapshot`. Restore it to try again from the same point.
70
78
 
@@ -106,11 +114,10 @@ If you cannot get the machine into the necessary state, say which state you coul
106
114
 
107
115
  - `machine-state-lost` and the message says that the VICE window closed: the window closed before `c64_window` action `close`. The emulator has no window again. The machine state is the same as at the time when the window opened. The breakpoints and the watchpoints stay. Do not use results from the window time. Read the machine state again, then continue.
108
116
  - `machine-unavailable` and the message says that the VICE window closed: the session starts the emulator again. Wait a few seconds. Then do the operation again.
109
- - `machine-state-lost` with a different message: the emulator stopped and its state is gone. Do not continue with results from before. Tell the user to restart the MCP server.
117
+ - `machine-state-lost` with a different message: the connection to the host runtime was lost, and the machine state is gone. Do not continue with results from before. The next call starts a new machine when the host runtime runs. Load the program again, and set the breakpoints and the watchpoints again. If the next call gives `machine-unavailable` with a message that the host runtime does not run, obey the rule for that message.
110
118
  - `machine-unavailable` and the message names a pause or a dialog in the emulator window: this occurs only while the window is open. Do not send more operations. Tell the user to resume the emulator (Pause, Alt+P) or to close the dialog in the VICE window. Then do the operation again. The machine state does not change.
111
- - `machine-unavailable` with a different message: the host runtime does not run, or it does not answer. Tell the user to start the host runtime with `npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host`.
112
119
  - `machine-running`: stop the CPU first, then try again.
113
- - `installation-incomplete`: tell the user the message. Do not try to repair the installation.
120
+ - `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script. Do not try to repair the installation.
114
121
  - `c64_run_until` with `stopReason` `timeout`: the target did not occur in the frame limit. This is evidence too. Report it.
115
122
  - `c64_run_until` with `stopReason` `jam`: the CPU executed a JAM opcode and stopped. Report the address.
116
123