@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
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 +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
package/src/mcp/tools/input.ts
CHANGED
|
@@ -7,15 +7,18 @@ import { WireFailure } from "../../protocol/messages.ts";
|
|
|
7
7
|
import { JOYSTICK_DIRECTIONS, MAX_KEYBOARD_BYTES } from "../../protocol/vice.ts";
|
|
8
8
|
import { defineTool } from "../server.ts";
|
|
9
9
|
|
|
10
|
+
const EMULATOR_FLAW = z.string().optional().describe("a known flaw of the emulator version that affects input, and the version that fixes it");
|
|
11
|
+
|
|
10
12
|
export const c64Keyboard = defineTool({
|
|
11
13
|
name: "c64_keyboard",
|
|
12
14
|
title: "Type on the C64 keyboard",
|
|
13
15
|
description:
|
|
14
16
|
`Type up to ${MAX_KEYBOARD_BYTES} keys into the C64 keyboard queue. The C64 reads them while it runs. ` +
|
|
15
17
|
"Only a program that reads keys through the KERNAL sees them. A program that reads the keyboard hardware directly does not see them. Many games do that. " +
|
|
18
|
+
"No command presses one key of the keyboard matrix: for one specific key, ask the user to press it in the window. " +
|
|
16
19
|
'Mode text types text, for example "RUN\\n". Letters of either case type the normal (unshifted) key. A line break types RETURN. ' +
|
|
17
20
|
"Mode petscii types PETSCII codes 1 to 255 in bytes, for example [147] to clear the screen. " +
|
|
18
|
-
"This does not change the run state of the machine.",
|
|
21
|
+
"This does not change the run state of the machine. emulatorFlaw in the result names a known flaw of the emulator version that affects input, and the version that fixes it.",
|
|
19
22
|
inputSchema: z
|
|
20
23
|
.object({
|
|
21
24
|
mode: z.enum(["text", "petscii"]),
|
|
@@ -23,7 +26,7 @@ export const c64Keyboard = defineTool({
|
|
|
23
26
|
bytes: z.array(z.number().int().min(1).max(255)).min(1).optional().describe("mode petscii: PETSCII codes to type"),
|
|
24
27
|
})
|
|
25
28
|
.strict(),
|
|
26
|
-
outputSchema: z.object({ queuedBytes: z.number().int().min(1).max(MAX_KEYBOARD_BYTES) }),
|
|
29
|
+
outputSchema: z.object({ queuedBytes: z.number().int().min(1).max(MAX_KEYBOARD_BYTES), emulatorFlaw: EMULATOR_FLAW }),
|
|
27
30
|
readOnly: false,
|
|
28
31
|
async run(input, session) {
|
|
29
32
|
let petscii: Uint8Array;
|
|
@@ -51,7 +54,7 @@ export const c64Joystick = defineTool({
|
|
|
51
54
|
description:
|
|
52
55
|
"Set the joystick in control port 1 or 2: a direction (or center) and the fire button. " +
|
|
53
56
|
"The C64 sees this state until you change it. To release the joystick, set direction center and fire false. " +
|
|
54
|
-
"Most games read port 2. This does not change the run state of the machine.",
|
|
57
|
+
"Most games read port 2. This does not change the run state of the machine. emulatorFlaw in the result names a known flaw of the emulator version that affects input, and the version that fixes it.",
|
|
55
58
|
inputSchema: z
|
|
56
59
|
.object({
|
|
57
60
|
port: z.union([z.literal(1), z.literal(2)]).describe("control port 1 or 2"),
|
|
@@ -59,7 +62,7 @@ export const c64Joystick = defineTool({
|
|
|
59
62
|
fire: z.boolean().default(false),
|
|
60
63
|
})
|
|
61
64
|
.strict(),
|
|
62
|
-
outputSchema: z.object({ port: z.union([z.literal(1), z.literal(2)]), direction: z.enum(JOYSTICK_DIRECTIONS), fire: z.boolean() }),
|
|
65
|
+
outputSchema: z.object({ port: z.union([z.literal(1), z.literal(2)]), direction: z.enum(JOYSTICK_DIRECTIONS), fire: z.boolean(), emulatorFlaw: EMULATOR_FLAW }),
|
|
63
66
|
readOnly: false,
|
|
64
67
|
async run(input, session) {
|
|
65
68
|
return session.joystick(input);
|
package/src/mcp/tools/machine.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
import { formatC64Address } from "../../c64.ts";
|
|
6
|
+
import { PACKAGE } from "../../package.ts";
|
|
6
7
|
import { VIDEO_STANDARDS } from "../../protocol/messages.ts";
|
|
7
|
-
import { RESET_MODES, RUN_STATES, WINDOW_ACTIONS } from "../../protocol/vice.ts";
|
|
8
|
+
import { POINT_STOP_REASONS, RESET_MODES, RUN_STATES, WINDOW_ACTIONS } from "../../protocol/vice.ts";
|
|
8
9
|
import { AddressOutput, defineTool } from "../server.ts";
|
|
9
10
|
|
|
10
11
|
export const c64Status = defineTool({
|
|
@@ -13,7 +14,10 @@ export const c64Status = defineTool({
|
|
|
13
14
|
description:
|
|
14
15
|
"Get the state of the C64. The result tells if the CPU runs or has stopped, the video standard (pal or ntsc), " +
|
|
15
16
|
"and if warp mode is on. It also tells if the emulator window is open (window). " +
|
|
16
|
-
"When the CPU has stopped, the result also gives the program counter (pc)."
|
|
17
|
+
"When the CPU has stopped, the result also gives the program counter (pc). " +
|
|
18
|
+
"When a breakpoint, a watchpoint or a JAM opcode stopped the CPU, stopReason tells which, and pointId gives the id of the breakpoint or watchpoint. " +
|
|
19
|
+
"cartridge is true while a cartridge from c64_autostart is attached: a reset keeps it, and an autostart of another file detaches it. " +
|
|
20
|
+
"version is the c64-re-tools version of this MCP server.",
|
|
17
21
|
inputSchema: z.object({}).strict(),
|
|
18
22
|
outputSchema: z.object({
|
|
19
23
|
state: z.enum(RUN_STATES),
|
|
@@ -21,6 +25,10 @@ export const c64Status = defineTool({
|
|
|
21
25
|
warp: z.boolean(),
|
|
22
26
|
window: z.boolean().describe("true when the emulator window is open (c64_window)"),
|
|
23
27
|
pc: AddressOutput.optional().describe("program counter. Only when the CPU is stopped"),
|
|
28
|
+
stopReason: z.enum(POINT_STOP_REASONS).optional().describe("what stopped the CPU. Only when a breakpoint, a watchpoint or a JAM opcode stopped it"),
|
|
29
|
+
pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped the CPU"),
|
|
30
|
+
cartridge: z.boolean().describe("true while a cartridge from c64_autostart is attached"),
|
|
31
|
+
version: z.string().describe("the c64-re-tools version of this MCP server"),
|
|
24
32
|
}),
|
|
25
33
|
readOnly: true,
|
|
26
34
|
async run(_input, session) {
|
|
@@ -30,7 +38,11 @@ export const c64Status = defineTool({
|
|
|
30
38
|
videoStandard: status.videoStandard,
|
|
31
39
|
warp: status.warp,
|
|
32
40
|
window: status.window,
|
|
41
|
+
cartridge: status.cartridge,
|
|
42
|
+
version: PACKAGE.version,
|
|
33
43
|
...(status.pc === undefined ? {} : { pc: formatC64Address(status.pc) }),
|
|
44
|
+
...(status.stopReason === undefined ? {} : { stopReason: status.stopReason }),
|
|
45
|
+
...(status.pointId === undefined ? {} : { pointId: status.pointId }),
|
|
34
46
|
};
|
|
35
47
|
},
|
|
36
48
|
});
|
package/src/mcp/tools/media.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// Media tools: c64_autostart, c64_program_load, c64_disk_attach and
|
|
2
|
-
// c64_snapshot. Paths are project
|
|
3
|
-
// the Host Runtime receives only its bytes.
|
|
2
|
+
// c64_snapshot. Paths are relative to the project folder (D68); the host-client
|
|
3
|
+
// reads the file and the Host Runtime receives only its bytes.
|
|
4
4
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
7
7
|
import { formatC64Address } from "../../c64.ts";
|
|
8
|
+
import { projectFolder, readProjectFile } from "../../host-client/transfer.ts";
|
|
8
9
|
import { AUTOSTART_TYPES, DISK_TYPES, MAX_SNAPSHOTS, RUN_STATES } from "../../protocol/vice.ts";
|
|
10
|
+
import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
|
|
9
11
|
import { AddressInput, AddressOutput, defineTool, requireFields, TransientName } from "../server.ts";
|
|
10
12
|
|
|
11
|
-
const ProjectPath = z.string().min(1).describe("path relative to the project
|
|
13
|
+
const ProjectPath = z.string().min(1).describe("path relative to the project folder, for example build/game.prg");
|
|
12
14
|
|
|
13
15
|
const list = (types: readonly string[]) => types.map((type) => `.${type}`).join(", ");
|
|
14
16
|
|
|
@@ -18,19 +20,28 @@ export const c64Autostart = defineTool({
|
|
|
18
20
|
description:
|
|
19
21
|
`Start a program, or a disk, tape or cartridge image, from the project (${list(AUTOSTART_TYPES)}). ` +
|
|
20
22
|
"The C64 resets and loads the file. With run true (the default), it also runs the file. A disk image stays in drive 8. " +
|
|
21
|
-
"
|
|
23
|
+
"A cartridge stays in the C64 through every reset, as on a real C64. An autostart of a file that is not a cartridge detaches it first. " +
|
|
24
|
+
"Both joysticks are centred first: joystick port 1 uses the keyboard lines, and a held direction changes the RUN that the autostart types. " +
|
|
25
|
+
"Without index, a disk image loads like LOAD\"*\",8,1. index selects one directory entry by its number in the c64-disk directory list (0 is the first entry). " +
|
|
26
|
+
"The tool refuses an index of a DEL entry, or an index after the last entry. " +
|
|
22
27
|
"The machine runs when this tool returns, and the load continues.",
|
|
23
28
|
inputSchema: z
|
|
24
29
|
.object({
|
|
25
30
|
path: ProjectPath,
|
|
26
|
-
|
|
31
|
+
project: ProjectInput.optional(),
|
|
32
|
+
index: z.number().int().min(0).max(0xfffe).optional().describe("directory entry of a disk image, 0 is the first entry. Without it, the first file loads as with LOAD\"*\""),
|
|
27
33
|
run: z.boolean().default(true).describe("true runs the program after it loads"),
|
|
28
34
|
})
|
|
29
35
|
.strict(),
|
|
30
|
-
outputSchema: z.object({
|
|
36
|
+
outputSchema: z.object({
|
|
37
|
+
state: z.enum(RUN_STATES),
|
|
38
|
+
emulatorFlaw: z.string().optional().describe("for a tape image: a known flaw of this VICE version that stops a tape load"),
|
|
39
|
+
joysticksCentred: z.array(z.union([z.literal(1), z.literal(2)])).optional().describe("the joystick ports that held a direction or fire and were centred before the reset"),
|
|
40
|
+
}),
|
|
31
41
|
readOnly: false,
|
|
32
42
|
async run(input, session) {
|
|
33
|
-
|
|
43
|
+
// VICE numbers the directory entries from 1, and its 0 loads "*".
|
|
44
|
+
return session.autostart({ path: input.path, index: input.index === undefined ? 0 : input.index + 1, run: input.run, root: projectFolder(input.project) });
|
|
34
45
|
},
|
|
35
46
|
});
|
|
36
47
|
|
|
@@ -44,13 +55,15 @@ export const c64ProgramLoad = defineTool({
|
|
|
44
55
|
inputSchema: z
|
|
45
56
|
.object({
|
|
46
57
|
path: ProjectPath,
|
|
58
|
+
project: ProjectInput.optional(),
|
|
47
59
|
address: AddressInput.optional().describe("load here instead of at the file's own load address"),
|
|
48
60
|
})
|
|
49
61
|
.strict(),
|
|
50
62
|
outputSchema: z.object({ state: z.enum(RUN_STATES), loadAddress: AddressOutput, size: z.number().int().min(1).max(0x10000) }),
|
|
51
63
|
readOnly: false,
|
|
52
64
|
async run(input, session) {
|
|
53
|
-
const
|
|
65
|
+
const root = projectFolder(input.project);
|
|
66
|
+
const result = await session.programLoad(input.address === undefined ? { path: input.path, root } : { path: input.path, address: input.address, root });
|
|
54
67
|
return { state: result.state, loadAddress: formatC64Address(result.loadAddress), size: result.size };
|
|
55
68
|
},
|
|
56
69
|
});
|
|
@@ -62,11 +75,11 @@ export const c64DiskAttach = defineTool({
|
|
|
62
75
|
`Put a disk image from the project into drive 8 (${list(DISK_TYPES)}). ` +
|
|
63
76
|
"Nothing loads or runs. To load from the disk, type LOAD on the C64 or use c64_autostart. " +
|
|
64
77
|
"This does not change the run state of the machine.",
|
|
65
|
-
inputSchema: z.object({ path: ProjectPath }).strict(),
|
|
78
|
+
inputSchema: z.object({ path: ProjectPath, project: ProjectInput.optional() }).strict(),
|
|
66
79
|
outputSchema: z.object({ attached: z.boolean() }),
|
|
67
80
|
readOnly: false,
|
|
68
81
|
async run(input, session) {
|
|
69
|
-
return session.diskAttach(input);
|
|
82
|
+
return session.diskAttach({ path: input.path, root: projectFolder(input.project) });
|
|
70
83
|
},
|
|
71
84
|
});
|
|
72
85
|
|
|
@@ -75,12 +88,17 @@ export const c64Snapshot = defineTool({
|
|
|
75
88
|
title: "Machine snapshots",
|
|
76
89
|
description:
|
|
77
90
|
"Save and restore the whole machine state, attached disks included. Action save keeps a snapshot under a name you choose. " +
|
|
78
|
-
"
|
|
91
|
+
"With out, save also writes it to that project file as a snapshot file (.vsf), so a later session can use it. " +
|
|
92
|
+
"Action restore puts the machine back in that state and stops the CPU. Give name for a snapshot of this session, or file for a .vsf in the project. " +
|
|
93
|
+
"A file restores only in the emulator version and with the machine settings that saved it. The c64_timing stopwatch keeps its reading. " +
|
|
79
94
|
`Action list gives the names, and action discard removes one. A session keeps at most ${MAX_SNAPSHOTS} snapshots until it ends.`,
|
|
80
95
|
inputSchema: z
|
|
81
96
|
.object({
|
|
82
97
|
action: z.enum(["save", "restore", "list", "discard"]),
|
|
83
|
-
name: TransientName.optional().describe("save, restore
|
|
98
|
+
name: TransientName.optional().describe("save and discard, and restore without file: 1 to 64 letters, digits, dots, underscores or hyphens"),
|
|
99
|
+
out: OutInput.optional().describe("save only: also write the snapshot to this project file, for example snapshots/room0.vsf. An existing file is overwritten"),
|
|
100
|
+
file: ProjectPath.optional().describe("restore only: a .vsf file in the project, in place of name"),
|
|
101
|
+
project: ProjectInput.optional().describe("save with out, and restore with file: the project folder, relative to the folder where the agent started. Give it when that folder is not in a project and has projects below it"),
|
|
84
102
|
})
|
|
85
103
|
.strict(),
|
|
86
104
|
outputSchema: z.object({
|
|
@@ -90,16 +108,26 @@ export const c64Snapshot = defineTool({
|
|
|
90
108
|
state: z.enum(RUN_STATES).optional(),
|
|
91
109
|
snapshots: z.array(z.string()).optional(),
|
|
92
110
|
discarded: z.boolean().optional(),
|
|
111
|
+
...WrittenOutput,
|
|
93
112
|
}),
|
|
94
113
|
readOnly: false,
|
|
95
114
|
async run(input, session) {
|
|
96
|
-
const { action,
|
|
115
|
+
const { action, ...fields } = input;
|
|
97
116
|
if (action === "list") {
|
|
98
|
-
requireFields(action,
|
|
117
|
+
requireFields(action, fields, []);
|
|
99
118
|
return session.snapshot({ action });
|
|
100
119
|
}
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
if (action === "restore" && fields.file !== undefined) {
|
|
121
|
+
requireFields(action, fields, ["file", "project"], ["file"]);
|
|
122
|
+
return session.snapshotRestore(readProjectFile(fields.file, { root: projectFolder(fields.project) }).bytes);
|
|
123
|
+
}
|
|
124
|
+
if (action === "save") {
|
|
125
|
+
requireFields(action, fields, fields.out === undefined ? ["name"] : ["name", "out", "project"], ["name"]);
|
|
126
|
+
const saved = await session.snapshot({ action, name: fields.name! });
|
|
127
|
+
return fields.out === undefined ? saved : { ...saved, ...writeOut(fields.out, await session.snapshotExport(fields.name!), fields.project) };
|
|
128
|
+
}
|
|
129
|
+
requireFields(action, fields, ["name"], ["name"]);
|
|
130
|
+
return session.snapshot({ action, name: fields.name! });
|
|
103
131
|
},
|
|
104
132
|
});
|
|
105
133
|
|
package/src/mcp/tools/memory.ts
CHANGED
|
@@ -6,28 +6,50 @@ import { z } from "zod";
|
|
|
6
6
|
import { formatC64Address } from "../../c64.ts";
|
|
7
7
|
import { WireFailure } from "../../protocol/messages.ts";
|
|
8
8
|
import { MAX_COMPARE_DIFFERENCES, MAX_COMPARE_SIZE, MAX_DISASSEMBLE, MAX_MEMORY_READ, MAX_MEMORY_WRITE, MAX_SEARCH_PATTERN, MAX_SEARCH_RESULTS, type RegisterValues } from "../../protocol/vice.ts";
|
|
9
|
+
import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
|
|
9
10
|
import { AddressInput, AddressOutput, Byte, defineTool, HexData, HexDataInput, MemoryViewInput, requireFields, SpaceInput } from "../server.ts";
|
|
10
11
|
|
|
12
|
+
/** With out, a read or a disassembly can be larger than one answer of the session: it is read in parts (D39). */
|
|
13
|
+
const MAX_MEMORY_READ_OUT = 0x10000;
|
|
14
|
+
const MAX_DISASSEMBLE_OUT = 32768;
|
|
15
|
+
/** A memory byte in a result: two lowercase hex digits, as memory data is. */
|
|
16
|
+
const HexByte = z.string().regex(/^[0-9a-f]{2}$/);
|
|
17
|
+
|
|
11
18
|
export const c64MemoryRead = defineTool({
|
|
12
19
|
name: "c64_memory_read",
|
|
13
20
|
title: "Read C64 memory",
|
|
14
21
|
description:
|
|
15
22
|
`Read 1 to ${MAX_MEMORY_READ} bytes of memory, starting at an address. ` +
|
|
16
23
|
"The range must not go past $ffff. Use space drive8 to read the memory of the 1541 disk drive. " +
|
|
24
|
+
`With out, it reads up to ${MAX_MEMORY_READ_OUT} bytes and writes them as raw bytes to that project file. The result then gives the file, the number of bytes and a SHA-256 in place of data. ` +
|
|
17
25
|
"This does not change the run state of the machine.",
|
|
18
26
|
inputSchema: z
|
|
19
27
|
.object({
|
|
20
28
|
address: AddressInput,
|
|
21
|
-
size: z.number().int().min(1).max(
|
|
29
|
+
size: z.number().int().min(1).max(MAX_MEMORY_READ_OUT).describe(`number of bytes, 1 to ${MAX_MEMORY_READ}, or to ${MAX_MEMORY_READ_OUT} with out`),
|
|
22
30
|
space: SpaceInput,
|
|
23
31
|
view: MemoryViewInput,
|
|
32
|
+
out: OutInput.optional(),
|
|
33
|
+
project: ProjectInput.optional(),
|
|
24
34
|
})
|
|
25
35
|
.strict(),
|
|
26
|
-
outputSchema: z.object({ address: AddressOutput, size: z.number().int().min(1).max(
|
|
27
|
-
readOnly:
|
|
36
|
+
outputSchema: z.object({ address: AddressOutput, size: z.number().int().min(1).max(MAX_MEMORY_READ_OUT), data: HexData.optional(), ...WrittenOutput }),
|
|
37
|
+
readOnly: false,
|
|
28
38
|
async run(input, session) {
|
|
29
|
-
const
|
|
30
|
-
|
|
39
|
+
const { out, project, ...read } = input;
|
|
40
|
+
if (out === undefined && project !== undefined) throw new WireFailure("invalid-input", "project is used only with out.");
|
|
41
|
+
if (out === undefined) {
|
|
42
|
+
if (read.size > MAX_MEMORY_READ) throw new WireFailure("invalid-input", `size can be at most ${MAX_MEMORY_READ} without out.`);
|
|
43
|
+
const result = await session.memoryRead(read);
|
|
44
|
+
return { address: formatC64Address(result.address), size: result.data.length / 2, data: result.data };
|
|
45
|
+
}
|
|
46
|
+
if (read.address + read.size > 0x10000) throw new WireFailure("invalid-input", "The range must not go past $ffff.");
|
|
47
|
+
const parts: Buffer[] = [];
|
|
48
|
+
for (let at = read.address; at < read.address + read.size; at += MAX_MEMORY_READ) {
|
|
49
|
+
const part = await session.memoryRead({ ...read, address: at, size: Math.min(MAX_MEMORY_READ, read.address + read.size - at) });
|
|
50
|
+
parts.push(Buffer.from(part.data, "hex"));
|
|
51
|
+
}
|
|
52
|
+
return { address: formatC64Address(read.address), size: read.size, ...writeOut(out, Buffer.concat(parts), project) };
|
|
31
53
|
},
|
|
32
54
|
});
|
|
33
55
|
|
|
@@ -162,6 +184,7 @@ export const c64MemoryCompare = defineTool({
|
|
|
162
184
|
description:
|
|
163
185
|
`Compare two memory ranges of size bytes (1 to ${MAX_COMPARE_SIZE}). Each range has its own address, space and view. ` +
|
|
164
186
|
`The result tells if they are equal, how many bytes differ, and the first ${MAX_COMPARE_DIFFERENCES} differences with their offset. ` +
|
|
187
|
+
"The bytes of a difference are lowercase hex. " +
|
|
165
188
|
"This does not change the run state of the machine.",
|
|
166
189
|
inputSchema: z
|
|
167
190
|
.object({ left: Location, right: Location, size: z.number().int().min(1).max(MAX_COMPARE_SIZE) })
|
|
@@ -169,11 +192,13 @@ export const c64MemoryCompare = defineTool({
|
|
|
169
192
|
outputSchema: z.object({
|
|
170
193
|
equal: z.boolean(),
|
|
171
194
|
differentBytes: z.number().int().min(0),
|
|
172
|
-
firstDifferences: z.array(z.object({ offset: z.number().int().min(0), left:
|
|
195
|
+
firstDifferences: z.array(z.object({ offset: z.number().int().min(0), left: HexByte, right: HexByte })),
|
|
173
196
|
}),
|
|
174
197
|
readOnly: true,
|
|
175
198
|
async run(input, session) {
|
|
176
|
-
|
|
199
|
+
const result = await session.memoryCompare(input);
|
|
200
|
+
const hex = (byte: number) => byte.toString(16).padStart(2, "0");
|
|
201
|
+
return { ...result, firstDifferences: result.firstDifferences.map((difference) => ({ ...difference, left: hex(difference.left), right: hex(difference.right) })) };
|
|
177
202
|
},
|
|
178
203
|
});
|
|
179
204
|
|
|
@@ -183,21 +208,52 @@ export const c64Disassemble = defineTool({
|
|
|
183
208
|
description:
|
|
184
209
|
`Disassemble count instructions (1 to ${MAX_DISASSEMBLE}) from an address, as the bytes are in the live machine now. ` +
|
|
185
210
|
"Use it to read code that a program changed or unpacked at run time. It includes the undocumented opcodes. " +
|
|
211
|
+
`With out, it disassembles up to ${MAX_DISASSEMBLE_OUT} instructions, or to $ffff, and writes them as a listing to that project file. The result then gives the file in place of the instructions. ` +
|
|
186
212
|
"This does not change the run state of the machine.",
|
|
187
213
|
inputSchema: z
|
|
188
214
|
.object({
|
|
189
215
|
address: AddressInput,
|
|
190
|
-
count: z.number().int().min(1).max(MAX_DISASSEMBLE),
|
|
216
|
+
count: z.number().int().min(1).max(MAX_DISASSEMBLE_OUT).describe(`1 to ${MAX_DISASSEMBLE}, or to ${MAX_DISASSEMBLE_OUT} with out`),
|
|
191
217
|
space: SpaceInput,
|
|
192
218
|
view: MemoryViewInput,
|
|
219
|
+
out: OutInput.optional(),
|
|
220
|
+
project: ProjectInput.optional(),
|
|
193
221
|
})
|
|
194
222
|
.strict(),
|
|
195
|
-
outputSchema: z.object({ instructions: z.array(z.object({ address: AddressOutput, bytes: HexData, text: z.string() })) }),
|
|
196
|
-
readOnly:
|
|
223
|
+
outputSchema: z.object({ instructions: z.array(z.object({ address: AddressOutput, bytes: HexData, text: z.string() })).optional(), count: z.number().int().min(0).optional(), ...WrittenOutput }),
|
|
224
|
+
readOnly: false,
|
|
197
225
|
async run(input, session) {
|
|
198
|
-
const
|
|
199
|
-
|
|
226
|
+
const { out, project, ...request } = input;
|
|
227
|
+
if (out === undefined && project !== undefined) throw new WireFailure("invalid-input", "project is used only with out.");
|
|
228
|
+
if (out === undefined) {
|
|
229
|
+
if (request.count > MAX_DISASSEMBLE) throw new WireFailure("invalid-input", `count can be at most ${MAX_DISASSEMBLE} without out.`);
|
|
230
|
+
const result = await session.disassemble(request);
|
|
231
|
+
return { instructions: result.instructions.map((instruction) => ({ ...instruction, address: formatC64Address(instruction.address) })) };
|
|
232
|
+
}
|
|
233
|
+
const lines: string[] = [];
|
|
234
|
+
let address = request.address;
|
|
235
|
+
// Ends at count, or where the disassembly would wrap past $ffff.
|
|
236
|
+
let wrapped = false;
|
|
237
|
+
while (!wrapped && lines.length < request.count && address <= 0xffff) {
|
|
238
|
+
const { instructions } = await session.disassemble({ ...request, address, count: Math.min(MAX_DISASSEMBLE, request.count - lines.length) });
|
|
239
|
+
if (instructions.length === 0) break;
|
|
240
|
+
for (const instruction of instructions) {
|
|
241
|
+
if (lines.length === request.count) break;
|
|
242
|
+
if (instruction.address < address) {
|
|
243
|
+
wrapped = true;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
lines.push(`${formatC64Address(instruction.address)} ${spaced(instruction.bytes).padEnd(9)} ${instruction.text}`);
|
|
247
|
+
address = instruction.address + instruction.bytes.length / 2;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { count: lines.length, ...writeOut(out, `${lines.join("\n")}\n`, project) };
|
|
200
251
|
},
|
|
201
252
|
});
|
|
202
253
|
|
|
254
|
+
/** Hex bytes as pairs with spaces: "a90a" is "a9 0a". */
|
|
255
|
+
export function spaced(hex: string): string {
|
|
256
|
+
return (hex.match(/../g) ?? []).join(" ");
|
|
257
|
+
}
|
|
258
|
+
|
|
203
259
|
export const memoryTools = [c64MemoryRead, c64MemoryWrite, c64MemorySearch, c64MemoryCompare, c64Registers, c64Disassemble];
|
package/src/mcp/tools/video.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
|
-
import { formatC64Address } from "../../c64.ts";
|
|
6
|
+
import { formatC64Address, formatSprite, formatVicii } from "../../c64.ts";
|
|
7
7
|
import { WireFailure } from "../../protocol/messages.ts";
|
|
8
8
|
import { CIA_SELECTIONS, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, MAX_OBSERVE_BYTES, MAX_OBSERVE_RANGES, SPACES, VICII_MODES, type ObserveParams } from "../../protocol/vice.ts";
|
|
9
|
+
import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
|
|
9
10
|
import { AddressInput, AddressOutput, Byte, defineTool, HexData, MemoryViewInput, requireFields, SpaceInput, ToolOutput, TransientName } from "../server.ts";
|
|
10
11
|
|
|
11
12
|
const Color = z.number().int().min(0).max(15);
|
|
@@ -30,8 +31,7 @@ export const c64Vicii = defineTool({
|
|
|
30
31
|
}),
|
|
31
32
|
readOnly: true,
|
|
32
33
|
async run(_input, session) {
|
|
33
|
-
|
|
34
|
-
return { ...state, screenAddress: formatC64Address(state.screenAddress), graphicsAddress: formatC64Address(state.graphicsAddress) };
|
|
34
|
+
return formatVicii(await session.vicii());
|
|
35
35
|
},
|
|
36
36
|
});
|
|
37
37
|
|
|
@@ -61,7 +61,7 @@ export const c64Sprite = defineTool({
|
|
|
61
61
|
readOnly: true,
|
|
62
62
|
async run(input, session) {
|
|
63
63
|
const result = await session.sprites(input.sprites ?? []);
|
|
64
|
-
return { sprites: result.sprites.map(
|
|
64
|
+
return { sprites: result.sprites.map(formatSprite) };
|
|
65
65
|
},
|
|
66
66
|
});
|
|
67
67
|
|
|
@@ -73,9 +73,12 @@ const CiaOutput = z.object({
|
|
|
73
73
|
ddrB: Byte,
|
|
74
74
|
timerA: z.number().int().min(0).max(0xffff),
|
|
75
75
|
timerB: z.number().int().min(0).max(0xffff),
|
|
76
|
+
latchA: z.number().int().min(0).max(0xffff).describe("the value that timer A loads when it underflows"),
|
|
77
|
+
latchB: z.number().int().min(0).max(0xffff).describe("the value that timer B loads when it underflows"),
|
|
76
78
|
controlA: Byte,
|
|
77
79
|
controlB: Byte,
|
|
78
80
|
interruptStatus: Byte,
|
|
81
|
+
interruptMask: z.number().int().min(0).max(0x1f).describe("the interrupt sources that are on, as bits 0-4 of the interrupt control register"),
|
|
79
82
|
tod: z.object({ hours: z.number().int(), minutes: z.number().int(), seconds: z.number().int(), tenths: z.number().int() }),
|
|
80
83
|
});
|
|
81
84
|
|
|
@@ -83,8 +86,8 @@ export const c64Cia = defineTool({
|
|
|
83
86
|
name: "c64_cia",
|
|
84
87
|
title: "CIA state",
|
|
85
88
|
description:
|
|
86
|
-
"Get the state of CIA 1, CIA 2 or both. The state has the ports, the data direction registers
|
|
87
|
-
`It also has the control registers, the interrupt flags and the time of day (hours 0 to 23). ${NO_CHANGE}`,
|
|
89
|
+
"Get the state of CIA 1, CIA 2 or both. The state has the ports, the data direction registers, the current timer values and the timer latches. " +
|
|
90
|
+
`It also has the control registers, the interrupt flags, the interrupt mask and the time of day (hours 0 to 23). ${NO_CHANGE}`,
|
|
88
91
|
inputSchema: z.object({ cia: z.enum(CIA_SELECTIONS).default("both") }).strict(),
|
|
89
92
|
outputSchema: z.object({ chips: z.array(CiaOutput) }),
|
|
90
93
|
readOnly: true,
|
|
@@ -129,8 +132,10 @@ export const c64Screen = defineTool({
|
|
|
129
132
|
title: "Capture and compare the C64 screen",
|
|
130
133
|
description:
|
|
131
134
|
"Action capture returns the last frame the C64 drew, with its borders, as a PNG image, plus its width and height in pixels. " +
|
|
135
|
+
"With out, capture writes the PNG to that project file, and compare writes its difference image there. The result then has no image. " +
|
|
132
136
|
"With baseline, the session also keeps that frame under the name. " +
|
|
133
137
|
"Action compare compares the current frame with a baseline, pixel by pixel. mask lists rectangles to ignore. " +
|
|
138
|
+
"A mask that covers the whole frame is invalid input. comparedPixels is the number of pixels outside the mask. " +
|
|
134
139
|
"match is true when the part of compared pixels that differ is at most maxMismatchRatio (default 0). bounds holds every different pixel. " +
|
|
135
140
|
"With includeDiff, compare also returns an image with the different pixels in magenta. " +
|
|
136
141
|
"Action list gives the baseline names. Action discard removes a baseline. Baselines stay until the session ends. " +
|
|
@@ -143,6 +148,8 @@ export const c64Screen = defineTool({
|
|
|
143
148
|
maxMismatchRatio: z.number().min(0).max(1).optional().describe("compare only: 0 to 1, default 0"),
|
|
144
149
|
mask: z.array(Rectangle).max(MAX_MASK_RECTANGLES).optional().describe("compare only: rectangles to ignore, in frame pixels"),
|
|
145
150
|
includeDiff: z.boolean().optional().describe("compare only: also return a difference image"),
|
|
151
|
+
out: OutInput.optional(),
|
|
152
|
+
project: ProjectInput.optional().describe("capture and compare with out only: the project folder, relative to the folder where the agent started. Give it when that folder is not in a project and has projects below it"),
|
|
146
153
|
})
|
|
147
154
|
.strict(),
|
|
148
155
|
outputSchema: z.object({
|
|
@@ -151,28 +158,32 @@ export const c64Screen = defineTool({
|
|
|
151
158
|
baseline: z.string().optional(),
|
|
152
159
|
match: z.boolean().optional(),
|
|
153
160
|
mismatchingPixels: z.number().int().min(0).optional(),
|
|
161
|
+
comparedPixels: z.number().int().min(1).optional(),
|
|
154
162
|
mismatchRatio: z.number().min(0).max(1).optional(),
|
|
155
163
|
bounds: Rectangle.optional(),
|
|
156
164
|
baselines: z.array(z.string()).optional(),
|
|
157
165
|
discarded: z.boolean().optional(),
|
|
166
|
+
...WrittenOutput,
|
|
158
167
|
}),
|
|
159
168
|
readOnly: false,
|
|
160
169
|
async run(input, session) {
|
|
161
170
|
const { action, ...fields } = input;
|
|
162
171
|
if (action === "capture") {
|
|
163
|
-
requireFields(action, fields, ["baseline"]);
|
|
172
|
+
requireFields(action, fields, fields.out === undefined ? ["baseline"] : ["baseline", "out", "project"]);
|
|
164
173
|
const shot = await session.screenCapture(fields.baseline);
|
|
165
174
|
const result = { width: shot.width, height: shot.height, ...(shot.baseline === undefined ? {} : { baseline: shot.baseline }) };
|
|
175
|
+
if (fields.out !== undefined) return { ...result, ...writeOut(fields.out, Buffer.from(shot.png, "base64"), fields.project) };
|
|
166
176
|
return new ToolOutput(result, [{ type: "image", data: shot.png, mimeType: "image/png" }]);
|
|
167
177
|
}
|
|
168
178
|
if (action === "compare") {
|
|
169
|
-
requireFields(action, fields, ["baseline", "maxMismatchRatio", "mask", "includeDiff"], ["baseline"]);
|
|
179
|
+
requireFields(action, fields, ["baseline", "maxMismatchRatio", "mask", "includeDiff", ...(fields.out === undefined ? [] : ["out", "project"])], ["baseline"]);
|
|
170
180
|
const { diffPng, ...result } = await session.screenCompare({
|
|
171
181
|
baseline: fields.baseline!,
|
|
172
182
|
maxMismatchRatio: fields.maxMismatchRatio ?? 0,
|
|
173
183
|
mask: fields.mask ?? [],
|
|
174
|
-
includeDiff: fields.includeDiff ?? false,
|
|
184
|
+
includeDiff: (fields.includeDiff ?? false) || fields.out !== undefined,
|
|
175
185
|
});
|
|
186
|
+
if (fields.out !== undefined && diffPng !== undefined) return { ...result, ...writeOut(fields.out, Buffer.from(diffPng, "base64"), fields.project) };
|
|
176
187
|
return diffPng === undefined ? result : new ToolOutput(result, [{ type: "image", data: diffPng, mimeType: "image/png" }]);
|
|
177
188
|
}
|
|
178
189
|
if (action === "list") {
|
package/src/native/discover.ts
CHANGED
|
@@ -7,6 +7,9 @@ import { isAbsolute, join, posix, win32 } from "node:path";
|
|
|
7
7
|
|
|
8
8
|
import { WireFailure } from "../protocol/messages.ts";
|
|
9
9
|
|
|
10
|
+
/** Where the user finds the install steps of the native tools for each system (D38). */
|
|
11
|
+
export const INSTALL_STEPS = "Steps for each system: https://github.com/henols/c64-re-tools/blob/main/docs/install-tools.md";
|
|
12
|
+
|
|
10
13
|
export interface ToolSpec {
|
|
11
14
|
/** Human name, for messages. */
|
|
12
15
|
name: string;
|
|
@@ -25,7 +28,7 @@ export const ACME: ToolSpec = {
|
|
|
25
28
|
envVar: "C64RT_ACME",
|
|
26
29
|
binaries: ["acme"],
|
|
27
30
|
where: "on this machine",
|
|
28
|
-
remedy:
|
|
31
|
+
remedy: `Install the ACME cross-assembler on the machine that runs the skill scripts so that acme is on PATH, or set C64RT_ACME to its full path. ${INSTALL_STEPS}`,
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
export const C1541: ToolSpec = {
|
|
@@ -49,7 +52,7 @@ export const DXA: ToolSpec = {
|
|
|
49
52
|
envVar: "C64RT_DXA",
|
|
50
53
|
binaries: ["dxa"],
|
|
51
54
|
where: "on this machine",
|
|
52
|
-
remedy:
|
|
55
|
+
remedy: `Install dxa 0.1.5 on the machine that runs the skill scripts so that dxa is on PATH, or set C64RT_DXA to its full path. ${INSTALL_STEPS}`,
|
|
53
56
|
};
|
|
54
57
|
|
|
55
58
|
export const VICE: ToolSpec = {
|
|
@@ -66,10 +69,45 @@ export const VICE: ToolSpec = {
|
|
|
66
69
|
*/
|
|
67
70
|
export const MIN_VICE = { major: 3, minor: 9 } as const;
|
|
68
71
|
|
|
69
|
-
/**
|
|
70
|
-
export
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
/** A VICE version; `revision` is the SVN revision of a build from VICE's source tree (nightly builds), absent for a release. */
|
|
73
|
+
export interface ViceVersion {
|
|
74
|
+
major: number;
|
|
75
|
+
minor: number;
|
|
76
|
+
revision?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** "x64sc (VICE 3.10)", "c1541 (VICE 3.9)", "VICE 3.9.0" or "x64sc (VICE 3.10 SVN r45999)": the major and minor version, and the SVN revision. */
|
|
80
|
+
export function viceVersionOf(text: string): ViceVersion | undefined {
|
|
81
|
+
const match = /VICE (\d+)\.(\d+)(?:[.\d]*)(?: SVN r(\d+))?/.exec(text);
|
|
82
|
+
if (match === null) return undefined;
|
|
83
|
+
return { major: Number(match[1]), minor: Number(match[2]), ...(match[3] === undefined ? {} : { revision: Number(match[3]) }) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Known flaws of VICE versions that c64-re-tools still runs on (D56). A flaw
|
|
88
|
+
* is not worked around: it is named with the VICE version that fixes it.
|
|
89
|
+
* `upTo` is the last flawed major.minor; `fixedRevision` the first SVN
|
|
90
|
+
* revision with the fix, for builds from VICE's source tree.
|
|
91
|
+
*/
|
|
92
|
+
const VICE_FLAWS = [
|
|
93
|
+
{
|
|
94
|
+
upTo: { major: 3, minor: 10 },
|
|
95
|
+
fixedRevision: 45971,
|
|
96
|
+
text:
|
|
97
|
+
"VICE 3.10 and older hold bits 5-7 of CIA 1 ports A and B low with the joystick device that c64-re-tools uses (VICE bug #2202). " +
|
|
98
|
+
"$DC00 and $DC01 read $1F with no input. A program that waits for one key of the keyboard matrix (for example LDA $DC01 / CMP #$EF for SPACE) waits forever. " +
|
|
99
|
+
"A program that compares the full byte of a joystick port also waits forever. " +
|
|
100
|
+
"A KERNAL tape LOAD of a .tap image stops after FOUND, with no LOADING, and the C64 shows READY. The fix is in the next VICE release, and in VICE nightly builds now (SVN r45971 or later). " +
|
|
101
|
+
"Install the latest VICE when this matters.",
|
|
102
|
+
},
|
|
103
|
+
] as const;
|
|
104
|
+
|
|
105
|
+
/** The known flaws of this VICE version, as text for a person or an agent. */
|
|
106
|
+
export function viceFlaws(version: ViceVersion): string[] {
|
|
107
|
+
return VICE_FLAWS.filter((flaw) => {
|
|
108
|
+
const newer = version.major > flaw.upTo.major || (version.major === flaw.upTo.major && version.minor > flaw.upTo.minor);
|
|
109
|
+
return !newer && (version.revision === undefined || version.revision < flaw.fixedRevision);
|
|
110
|
+
}).map((flaw) => flaw.text);
|
|
73
111
|
}
|
|
74
112
|
|
|
75
113
|
export function isOlderThanMinimum(version: { major: number; minor: number }): boolean {
|
|
@@ -25,7 +25,7 @@ export async function analyze(
|
|
|
25
25
|
const base = start;
|
|
26
26
|
|
|
27
27
|
// Ghidra disassembles and defines data only in the loaded bytes; labels may name any address.
|
|
28
|
-
const { entryPoints, dataRanges } = seedsInside(params, start, end);
|
|
28
|
+
const { entryPoints, codeStarts, dataRanges } = seedsInside(params, start, end);
|
|
29
29
|
if (entryPoints.length === 0) {
|
|
30
30
|
throw new WireFailure("invalid-input", "Give at least one entry point inside the program, for example the SYS address of its BASIC loader.");
|
|
31
31
|
}
|
|
@@ -33,7 +33,7 @@ export async function analyze(
|
|
|
33
33
|
const workspace = Workspace.create(context.supervisor);
|
|
34
34
|
try {
|
|
35
35
|
workspace.materialize("input", [{ path: "image.bin", size: body.length }], [body]);
|
|
36
|
-
writeFileSync(workspace.path("seeds.json"), JSON.stringify({ start, end, entryPoints, dataRanges, labels: params.labels }));
|
|
36
|
+
writeFileSync(workspace.path("seeds.json"), JSON.stringify({ start, end, entryPoints, codeStarts, dataRanges, labels: params.labels }));
|
|
37
37
|
writeFileSync(
|
|
38
38
|
workspace.path("request.json"),
|
|
39
39
|
JSON.stringify({
|
|
@@ -10,7 +10,7 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
|
|
12
12
|
import { WireFailure } from "../../protocol/messages.ts";
|
|
13
|
-
import { findOnPath, isExecutableFile } from "../discover.ts";
|
|
13
|
+
import { findOnPath, INSTALL_STEPS, isExecutableFile } from "../discover.ts";
|
|
14
14
|
import type { ProcessSupervisor } from "../processes.ts";
|
|
15
15
|
import type { Workspace } from "../staging.ts";
|
|
16
16
|
import { outputTail, runToolOrFail, type ToolRun } from "../run.ts";
|
|
@@ -22,7 +22,7 @@ export const SCRIPT_DIRECTORY = fileURLToPath(new URL("./scripts/", import.meta.
|
|
|
22
22
|
|
|
23
23
|
const REMEDY =
|
|
24
24
|
"Install Ghidra on the machine that runs the skill scripts and set C64RT_GHIDRA to its installation directory " +
|
|
25
|
-
|
|
25
|
+
`(the directory that holds support/analyzeHeadless), or put analyzeHeadless on PATH. ${INSTALL_STEPS}`;
|
|
26
26
|
|
|
27
27
|
export interface GhidraInstallation {
|
|
28
28
|
/** The installation directory. */
|
|
@@ -31,6 +31,8 @@ export interface GhidraInstallation {
|
|
|
31
31
|
/** For example ghidra_12.1.3_PUBLIC: the name of Ghidra's settings directory. */
|
|
32
32
|
settingsName: string;
|
|
33
33
|
version: string;
|
|
34
|
+
/** The lowest Java version this Ghidra runs on (application.java.min), when it names one. */
|
|
35
|
+
javaMin?: string;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/** Ghidra's launcher is a shell script, and a batch file on Windows. */
|
|
@@ -49,7 +51,8 @@ function installationAt(root: string, source: string): GhidraInstallation {
|
|
|
49
51
|
const version = value("application.version");
|
|
50
52
|
const release = value("application.release.name");
|
|
51
53
|
if (version === undefined || release === undefined) throw new WireFailure("installation-incomplete", `The Ghidra installation at ${source} has no version. ${REMEDY}`);
|
|
52
|
-
|
|
54
|
+
const javaMin = value("application.java.min");
|
|
55
|
+
return { root, analyzeHeadless, settingsName: `ghidra_${version}_${release}`, version, ...(javaMin === undefined || javaMin === "" ? {} : { javaMin }) };
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
/** The version of a Ghidra installation as a result and the tool status name it, for example "Ghidra 12.1.3". */
|
|
@@ -123,6 +126,18 @@ export interface HeadlessOptions {
|
|
|
123
126
|
env?: NodeJS.ProcessEnv;
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
/** What Ghidra's launcher (launch.sh, launch.bat) prints when it finds no Java that it can use. */
|
|
130
|
+
export const NO_USABLE_JDK = /The 'java' command could not be found in your PATH or with JAVA_HOME|Unable to prompt user for JDK path, no TTY detected|Failed to find a supported JDK/;
|
|
131
|
+
|
|
132
|
+
/** A Ghidra without a usable JDK is an incomplete installation that names the Java version it needs (D38). */
|
|
133
|
+
export function noJdk(ghidra: GhidraInstallation): WireFailure {
|
|
134
|
+
const java = ghidra.javaMin === undefined ? "a JDK" : `JDK ${ghidra.javaMin} or later`;
|
|
135
|
+
return new WireFailure(
|
|
136
|
+
"installation-incomplete",
|
|
137
|
+
`${ghidraVersion(ghidra)} found no Java that it can use. It needs ${java}. Install it on the machine that runs the skill scripts and put its java on PATH, or set JAVA_HOME to it. ${INSTALL_STEPS}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
126
141
|
/** Imports one file into a disposable project and runs the scripts. Fails on timeout, abort or a Ghidra error exit. */
|
|
127
142
|
export async function runHeadless(options: HeadlessOptions): Promise<ToolRun> {
|
|
128
143
|
const env = prepareSettings(options.workspace, options.ghidra, options.env);
|
|
@@ -148,6 +163,7 @@ export async function runHeadless(options: HeadlessOptions): Promise<ToolRun> {
|
|
|
148
163
|
for (const script of options.preScripts ?? []) argv.push("-preScript", script.name, ...script.args);
|
|
149
164
|
for (const script of options.postScripts ?? []) argv.push("-postScript", script.name, ...script.args);
|
|
150
165
|
const run = await runToolOrFail("Ghidra", "The Ghidra analysis", { argv, cwd: options.workspace.root, timeoutMs: options.timeoutMs }, { supervisor: options.supervisor, signal: options.signal, env });
|
|
166
|
+
if (run.code !== 0 && NO_USABLE_JDK.test(run.stdout + run.stderr)) throw noJdk(options.ghidra);
|
|
151
167
|
if (run.code !== 0 || /^ERROR REPORT SCRIPT ERROR|Exception in thread "main"/m.test(run.stdout + run.stderr)) {
|
|
152
168
|
// Ghidra's own error lines tell the user what to fix.
|
|
153
169
|
const tail = outputTail("Ghidra", run, options.workspace.root, /ERROR|Exception|Error:/);
|