@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.
Files changed (126) hide show
  1. package/README.md +28 -62
  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 +25 -6
  6. package/skills/c64-assembler/scripts/assemble.ts +45 -10
  7. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  8. package/skills/c64-basic/SKILL.md +15 -7
  9. package/skills/c64-basic/scripts/basic.ts +4 -4
  10. package/skills/c64-disk/SKILL.md +34 -11
  11. package/skills/c64-disk/scripts/disk.ts +65 -10
  12. package/skills/c64-disk/scripts/lib/containers.ts +124 -0
  13. package/skills/c64-docs/SKILL.md +138 -0
  14. package/skills/c64-docs/references/page-types.md +141 -0
  15. package/skills/c64-docs/references/style.md +43 -0
  16. package/skills/c64-docs/scripts/generate.ts +102 -0
  17. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  18. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  19. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  20. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  21. package/skills/c64-docs/scripts/link.ts +207 -0
  22. package/skills/c64-emulator/SKILL.md +29 -8
  23. package/skills/c64-knowledge/SKILL.md +201 -15
  24. package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
  25. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  26. package/skills/c64-memory-map/SKILL.md +14 -2
  27. package/skills/c64-memory-map/scripts/memmap.ts +15 -4
  28. package/skills/c64-provenance/SKILL.md +100 -11
  29. package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
  30. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  31. package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
  32. package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
  33. package/skills/c64-provenance/scripts/provenance.ts +477 -24
  34. package/skills/c64-reverse-engineering/SKILL.md +90 -11
  35. package/skills/c64-source/SKILL.md +174 -0
  36. package/skills/c64-source/scripts/generate.ts +788 -0
  37. package/skills/c64-source/scripts/lib/loader.ts +630 -0
  38. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  39. package/skills/c64-static-analysis/SKILL.md +53 -13
  40. package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
  41. package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
  42. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
  43. package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
  44. package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
  45. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  46. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  47. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  48. package/skills/c64-testing/SKILL.md +57 -4
  49. package/skills/c64-testing/references/scenario-format.md +50 -6
  50. package/skills/c64-testing/scripts/checklist.ts +3 -3
  51. package/skills/c64-testing/scripts/diff.ts +167 -0
  52. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  53. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
  54. package/skills/c64-testing/scripts/run.ts +241 -0
  55. package/skills/c64-testing/scripts/test.ts +20 -10
  56. package/skills/c64-unpacker/SKILL.md +88 -21
  57. package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
  58. package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
  59. package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
  60. package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
  61. package/skills/c64-unpacker/scripts/unpack.ts +545 -61
  62. package/src/c64.ts +240 -12
  63. package/src/cli/cleanup.ts +1 -1
  64. package/src/cli/main.ts +14 -8
  65. package/src/cli/status.ts +105 -11
  66. package/src/graphics.ts +158 -0
  67. package/src/host/main.ts +57 -31
  68. package/src/host/server.ts +30 -47
  69. package/src/host/tools/c1541.ts +34 -5
  70. package/src/host/tools/index.ts +6 -2
  71. package/src/host/tools/petcat.ts +10 -3
  72. package/src/host/tools/status.ts +23 -6
  73. package/src/host/vice/adapter.ts +64 -20
  74. package/src/host/vice/chips.ts +11 -1
  75. package/src/host/vice/process.ts +19 -8
  76. package/src/host/vice/screen.ts +4 -44
  77. package/src/host/vice/session.ts +343 -60
  78. package/src/host-client/autostart.ts +55 -0
  79. package/src/host-client/connect.ts +14 -14
  80. package/src/host-client/coverage.ts +36 -0
  81. package/src/host-client/image.ts +63 -0
  82. package/src/host-client/steps.ts +175 -0
  83. package/src/host-client/tools.ts +7 -6
  84. package/src/host-client/transfer.ts +60 -10
  85. package/src/host-client/vice-session.ts +107 -28
  86. package/src/knowledge/conflicts.ts +161 -0
  87. package/src/knowledge/database.ts +150 -38
  88. package/src/knowledge/expression.ts +209 -0
  89. package/src/knowledge/graph.ts +104 -0
  90. package/src/knowledge/history.ts +105 -27
  91. package/src/knowledge/import.ts +141 -85
  92. package/src/knowledge/join.ts +211 -0
  93. package/src/knowledge/program.ts +258 -0
  94. package/src/knowledge/read.ts +445 -63
  95. package/src/knowledge/schema.ts +277 -65
  96. package/src/knowledge/text.ts +415 -0
  97. package/src/knowledge/write.ts +1145 -150
  98. package/src/mcp/main.ts +44 -19
  99. package/src/mcp/output.ts +39 -0
  100. package/src/mcp/server.ts +15 -4
  101. package/src/mcp/tools/debug.ts +202 -31
  102. package/src/mcp/tools/execution.ts +39 -5
  103. package/src/mcp/tools/input.ts +7 -4
  104. package/src/mcp/tools/machine.ts +14 -2
  105. package/src/mcp/tools/media.ts +44 -16
  106. package/src/mcp/tools/memory.ts +68 -12
  107. package/src/mcp/tools/video.ts +20 -9
  108. package/src/native/discover.ts +44 -6
  109. package/src/native/ghidra/analyze.ts +2 -2
  110. package/src/native/ghidra/index.ts +19 -3
  111. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  112. package/src/native/ghidra/scripts/C64Export.java +33 -1
  113. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  114. package/src/native/status.ts +15 -3
  115. package/src/native/types.ts +43 -8
  116. package/src/opcodes.ts +133 -0
  117. package/src/package.ts +21 -0
  118. package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
  119. package/src/project.ts +166 -3
  120. package/src/protocol/messages.ts +17 -10
  121. package/src/protocol/tools.ts +27 -22
  122. package/src/protocol/vice.ts +255 -33
  123. package/src/script.ts +49 -9
  124. package/src/trace.ts +55 -23
  125. package/skills/c64-provenance/scripts/compare.ts +0 -92
  126. package/skills/c64-unpacker/scripts/evidence.ts +0 -135
@@ -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);
@@ -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
  });
@@ -1,14 +1,16 @@
1
1
  // Media tools: c64_autostart, c64_program_load, c64_disk_attach and
2
- // c64_snapshot. Paths are project-relative; the host-client reads the file and
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 directory, for example build/game.prg");
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
- "index selects a file on a disk image (default 0, the first file). " +
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
- index: z.number().int().min(0).max(0xffff).default(0).describe("file number on a disk image. 0 is the first file"),
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({ state: z.enum(RUN_STATES) }),
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
- return session.autostart(input);
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 result = await session.programLoad(input.address === undefined ? { path: input.path } : { path: input.path, address: input.address });
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
- "Action restore puts the machine back in that state and stops the CPU. The c64_timing stopwatch keeps its reading. " +
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 and discard: 1 to 64 letters, digits, dots, underscores or hyphens"),
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, name } = input;
115
+ const { action, ...fields } = input;
97
116
  if (action === "list") {
98
- requireFields(action, { name }, []);
117
+ requireFields(action, fields, []);
99
118
  return session.snapshot({ action });
100
119
  }
101
- requireFields(action, { name }, ["name"], ["name"]);
102
- return session.snapshot({ action, name: name! });
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
 
@@ -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(MAX_MEMORY_READ).describe(`number of bytes, 1 to ${MAX_MEMORY_READ}`),
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(MAX_MEMORY_READ), data: HexData }),
27
- readOnly: true,
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 result = await session.memoryRead(input);
30
- return { address: formatC64Address(result.address), size: result.data.length / 2, data: result.data };
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: Byte, right: Byte })),
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
- return session.memoryCompare(input);
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: true,
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 result = await session.disassemble(input);
199
- return { instructions: result.instructions.map((instruction) => ({ ...instruction, address: formatC64Address(instruction.address) })) };
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];
@@ -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
- const state = await session.vicii();
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((sprite) => ({ ...sprite, dataAddress: formatC64Address(sprite.dataAddress) })) };
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 and the current timer values. " +
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") {
@@ -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: "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.",
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: "Install dxa (from the xa package, https://www.floodgap.com/retrotech/xa/) on the machine that runs the skill scripts so that dxa is on PATH, or set C64RT_DXA to its full path.",
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
- /** "x64sc (VICE 3.10)", "c1541 (VICE 3.9)" or "VICE 3.9.0": the major and minor version. */
70
- export function viceVersionOf(text: string): { major: number; minor: number } | undefined {
71
- const match = /VICE (\d+)\.(\d+)/.exec(text);
72
- return match === null ? undefined : { major: Number(match[1]), minor: Number(match[2]) };
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
- "(the directory that holds support/analyzeHeadless), or put analyzeHeadless on PATH.";
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
- return { root, analyzeHeadless, settingsName: `ghidra_${version}_${release}`, version };
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:/);