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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +3 -1
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-basic/SKILL.md +3 -3
  6. package/skills/c64-emulator/SKILL.md +2 -2
  7. package/skills/c64-knowledge/SKILL.md +17 -3
  8. package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
  9. package/skills/c64-memory-map/scripts/memmap.ts +1 -1
  10. package/skills/c64-provenance/SKILL.md +3 -2
  11. package/skills/c64-provenance/scripts/provenance.ts +4 -2
  12. package/skills/c64-provenance/scripts/runtime.ts +14 -2
  13. package/skills/c64-source/SKILL.md +4 -4
  14. package/skills/c64-source/scripts/generate.ts +57 -14
  15. package/skills/c64-source/scripts/loader.ts +5 -1
  16. package/skills/c64-source/scripts/source.ts +3 -1
  17. package/skills/c64-static-analysis/SKILL.md +2 -2
  18. package/skills/c64-static-analysis/scripts/analyze.ts +8 -4
  19. package/skills/c64-unpacker/SKILL.md +5 -5
  20. package/skills/c64-unpacker/scripts/evidence.ts +29 -22
  21. package/skills/c64-unpacker/scripts/loads.ts +30 -7
  22. package/skills/c64-unpacker/scripts/stages.ts +67 -0
  23. package/skills/c64-unpacker/scripts/unpack.ts +167 -47
  24. package/src/c64.ts +37 -12
  25. package/src/cli/main.ts +7 -4
  26. package/src/cli/status.ts +9 -7
  27. package/src/host/main.ts +8 -3
  28. package/src/host/tools/petcat.ts +5 -1
  29. package/src/host/tools/status.ts +8 -2
  30. package/src/host/vice/process.ts +10 -5
  31. package/src/host/vice/session.ts +26 -13
  32. package/src/host-client/connect.ts +2 -1
  33. package/src/host-client/coverage.ts +0 -5
  34. package/src/host-client/vice-session.ts +37 -4
  35. package/src/knowledge/history.ts +8 -0
  36. package/src/knowledge/import.ts +34 -3
  37. package/src/knowledge/write.ts +2 -1
  38. package/src/mcp/main.ts +43 -15
  39. package/src/mcp/server.ts +9 -2
  40. package/src/mcp/tools/execution.ts +3 -2
  41. package/src/mcp/tools/input.ts +6 -4
  42. package/src/mcp/tools/media.ts +4 -3
  43. package/src/native/discover.ts +38 -4
  44. package/src/package.ts +21 -0
  45. package/src/protocol/tools.ts +7 -3
  46. package/src/protocol/vice.ts +14 -6
@@ -6,7 +6,9 @@
6
6
  // that the snapshot no longer holds are closed. Rows of the user, the LLM or
7
7
  // another analyzer are never replaced: a finding that contradicts one is
8
8
  // reported as a conflict. Code against data is a contradiction; a different
9
- // name or a finer data type for the same kind of memory is not.
9
+ // name or a finer data type for the same kind of memory is not. One exception
10
+ // (D58): where a run executed an instruction start, a code finding replaces
11
+ // the data of another analyzer, never of the user, the LLM or a heuristic.
10
12
 
11
13
  import { createHash } from "node:crypto";
12
14
  import type { DatabaseSync } from "node:sqlite";
@@ -28,7 +30,7 @@ import {
28
30
  type SymbolRow,
29
31
  } from "./read.ts";
30
32
  import { CODE_REGION_TYPES, DEFAULT_SPACE, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
31
- import { Change, checkAddress, checkSpace, hex, subtract, SYMBOL_NAME, type AddressRange } from "./write.ts";
33
+ import { Change, checkAddress, checkSpace, hex, subtract, SYMBOL_NAME, type AddressRange, type Evidence } from "./write.ts";
32
34
 
33
35
  export type { AddressRange };
34
36
 
@@ -47,6 +49,8 @@ export interface NormalizedFindings {
47
49
  regions: Array<{ start: number; end: number; type: RegionType }>;
48
50
  /** Owned and covered by their source address. */
49
51
  references: Array<{ from: number; to: number; kind: ReferenceKind }>;
52
+ /** True for an address where a coverage run executed an instruction start (D58). */
53
+ executed?: (address: number) => boolean;
50
54
  }
51
55
 
52
56
  export interface ImportContext {
@@ -57,6 +61,8 @@ export interface ImportContext {
57
61
  inputHash?: string;
58
62
  /** The analyzer's own version line. */
59
63
  toolVersion?: string;
64
+ /** What the import rests on besides the image, for example the coverage files. */
65
+ evidence?: Evidence[];
60
66
  }
61
67
 
62
68
  /** What a revision records about one analyzer run: the hash of the image bytes and, when the analyzer tells it, its version. */
@@ -204,7 +210,13 @@ function importSymbols(db: DatabaseSync, change: Change, findings: NormalizedFin
204
210
  result.symbols.suppressed++;
205
211
  continue;
206
212
  }
207
- const current = symbolAt(db, finding.address, space);
213
+ let current = symbolAt(db, finding.address, space);
214
+ if (current !== undefined && isCode(finding.kind) && !isCode(current.kind) && YIELDS_TO_EXECUTED_CODE.has(current.origin) && findings.executed?.(finding.address) === true) {
215
+ // A run executed this address: the data symbol of another analyzer gives way (D58).
216
+ change.close("symbols", "space = ? AND address = ?", space, current.address);
217
+ result.symbols.retired++;
218
+ current = undefined;
219
+ }
208
220
  if (current !== undefined) {
209
221
  // A semantic or other-analyzer row stays; only a code/data disagreement matters.
210
222
  if (isCode(current.kind) !== isCode(finding.kind)) {
@@ -230,6 +242,15 @@ function importSymbols(db: DatabaseSync, change: Change, findings: NormalizedFin
230
242
 
231
243
  /** Analyzers whose regions a heuristic finding replaces where it lies (D44): user/LLM > heuristic > DXA/Ghidra. */
232
244
  const YIELDS_TO_HEURISTIC = new Set<Origin>(["dxa", "ghidra"]);
245
+ /** Analyzers whose data gives way to a code finding over an executed instruction start (D58). */
246
+ const YIELDS_TO_EXECUTED_CODE = new Set<Origin>(["dxa", "ghidra"]);
247
+
248
+ /** True when an address in the range is an executed instruction start. */
249
+ function holdsExecuted(findings: NormalizedFindings, start: number, end: number): boolean {
250
+ if (findings.executed === undefined) return false;
251
+ for (let address = start; address <= end; address++) if (findings.executed(address)) return true;
252
+ return false;
253
+ }
233
254
 
234
255
  function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
235
256
  const { analyzer } = findings;
@@ -243,6 +264,16 @@ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFin
243
264
  }
244
265
  }
245
266
  }
267
+ // Executed code is evidence about its bytes (D58): another analyzer's data region under a code finding gives way where a run executed an instruction start.
268
+ for (const region of findings.regions.filter((candidate) => isCode(candidate.type))) {
269
+ for (const row of regionsOverlapping(db, region.start, region.end, space)) {
270
+ if (row.origin === analyzer || !YIELDS_TO_EXECUTED_CODE.has(row.origin) || isCode(row.type) || row.type === "dual-use") continue;
271
+ if (!holdsExecuted(findings, Math.max(row.start, region.start), Math.min(row.end, region.end))) continue;
272
+ change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, row.origin);
273
+ for (const piece of subtract(row, [region])) change.insertRegion({ space, ...piece, type: row.type, ...(row.element === undefined ? {} : { element: row.element }), origin: row.origin });
274
+ result.regions.retired++;
275
+ }
276
+ }
246
277
  const conflictWith = (row: RegionRow, region: { start: number; end: number; type: RegionType }) => {
247
278
  // dual-use bytes are code at one time and data at another: neither finding contradicts them (D42).
248
279
  if (isCode(row.type) === isCode(region.type) || row.type === "dual-use") return;
@@ -436,8 +436,9 @@ function checkModule(request: ModuleRequest): void {
436
436
 
437
437
  function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: ModuleRequest): Step<ModuleRow> {
438
438
  const space = spaceOf(request);
439
- const title = request.title?.trim();
440
439
  const existing = moduleAt(db, request.start, space);
440
+ // A module write without a title keeps the title it has (F-26).
441
+ const title = request.title === undefined ? existing?.title : request.title.trim();
441
442
  if (existing !== undefined && existing.file === request.file && existing.title === title) return { current: existing };
442
443
  const owner = moduleWithFile(db, request.file, space);
443
444
  if (owner !== undefined && owner.start !== request.start) {
package/src/mcp/main.ts CHANGED
@@ -13,17 +13,18 @@ import { machineTools } from "./tools/machine.ts";
13
13
  import { mediaTools } from "./tools/media.ts";
14
14
  import { memoryTools } from "./tools/memory.ts";
15
15
  import { videoTools } from "./tools/video.ts";
16
+ import { PACKAGE } from "../package.ts";
16
17
 
17
18
  const HELP = `c64-re-tools-mcp
18
19
 
19
20
  Usage:
20
- npx -y --package=@henols/c64-re-tools@latest c64-re-tools-mcp
21
+ npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-mcp
21
22
  c64-re-tools-mcp --help
22
23
 
23
24
  Runs the stateful C64 MCP server on stdio. Each c64-re-tools-mcp process owns
24
25
  one C64 emulator, started by the Host Runtime (c64-re-tools-host) when this
25
26
  server starts and stopped when it exits. c64-re-tools install declares it for
26
- the agent, through npx with the latest version.
27
+ the agent, through npx with the newest version of its channel.
27
28
 
28
29
  Environment:
29
30
  C64RT_VIDEO pal (default) or ntsc; fixed for the life of the process.
@@ -42,32 +43,59 @@ function videoStandard(env: NodeJS.ProcessEnv): VideoStandard {
42
43
  }
43
44
 
44
45
  /**
45
- * One session for the life of the process (1 MCP = 1 session = 1 VICE).
46
- * It opens at start; if that fails before a session existed (for example the
47
- * host was not running yet), the next tool call tries again. Once a session
48
- * existed, its loss is final and reported by the session itself. Close stops
46
+ * One session at a time (1 MCP = 1 session = 1 VICE). It opens at start; if
47
+ * that fails (for example the host was not running yet), the next tool call
48
+ * tries again. When the host connection is lost, the next call opens a new
49
+ * session (D57). A lost session that the emulator answered first gives its
50
+ * machine-state-lost once, unless a pending call got it already. Close stops
49
51
  * an open that is still waiting for the host at once.
50
52
  */
51
53
  function sessionSource(): { get: SessionSource; close(): Promise<void> } {
52
54
  let opening: Promise<ViceSessionClient> | undefined;
55
+ let lostNotice: WireFailure | undefined;
53
56
  let closed = false;
54
57
  const stop = new AbortController();
55
58
  const open = () => {
56
59
  const started = performance.now();
57
- opening = (async () => ViceSessionClient.open({ videoStandard: videoStandard(process.env), signal: stop.signal }))();
58
- void opening.then(() => trace().event("session.opened", { ms: elapsedMs(started) }), () => {});
59
- opening.catch((error: unknown) => {
60
+ const current = (async () => ViceSessionClient.open({ videoStandard: videoStandard(process.env), signal: stop.signal }))();
61
+ opening = current;
62
+ void current.then(
63
+ (client) => {
64
+ trace().event("session.opened", { ms: elapsedMs(started) });
65
+ void client.closed.then(() => drop(current, client));
66
+ },
67
+ () => {},
68
+ );
69
+ current.catch((error: unknown) => {
60
70
  log(`session not opened: ${error instanceof Error ? error.message : String(error)}`);
61
- opening = undefined;
71
+ if (opening === current) opening = undefined;
62
72
  });
63
- return opening;
73
+ return current;
74
+ };
75
+ /** Forgets a lost session, once; the next call opens a new one. */
76
+ const drop = (current: Promise<ViceSessionClient>, client: ViceSessionClient) => {
77
+ if (closed || opening !== current) return;
78
+ log("the connection to the host runtime was lost; the next call opens a new session");
79
+ opening = undefined;
80
+ if (client.answered) lostNotice = client.unreportedLoss;
64
81
  };
65
82
  open();
83
+ const get = async (): Promise<ViceSessionClient> => {
84
+ if (closed) throw new WireFailure("machine-unavailable", "The C64 session is shutting down.");
85
+ if (lostNotice !== undefined) {
86
+ const notice = lostNotice;
87
+ lostNotice = undefined;
88
+ throw notice;
89
+ }
90
+ const current = opening ?? open();
91
+ const client = await current;
92
+ if (!client.lost) return client;
93
+ // The loss is known here before the close event reaches drop.
94
+ drop(current, client);
95
+ return get();
96
+ };
66
97
  return {
67
- get: () => {
68
- if (closed) return Promise.reject(new WireFailure("machine-unavailable", "The C64 session is shutting down."));
69
- return opening ?? open();
70
- },
98
+ get,
71
99
  async close() {
72
100
  closed = true;
73
101
  stop.abort();
package/src/mcp/server.ts CHANGED
@@ -8,7 +8,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, type CallToolResult, typ
8
8
  import { z } from "zod";
9
9
 
10
10
  import { formatC64Address, parseC64Address } from "../c64.ts";
11
- import type { ViceSessionClient } from "../host-client/vice-session.ts";
11
+ import { LostBeforeAnswer, type ViceSessionClient } from "../host-client/vice-session.ts";
12
12
  import { shownMessage, WireFailure, type WireError } from "../protocol/messages.ts";
13
13
  import { COMPARISONS, CONDITION_REGISTERS, MEMORY_VIEWS, SPACES, type Condition } from "../protocol/vice.ts";
14
14
  import { elapsedMs, trace, type TraceFields } from "../trace.ts";
@@ -261,7 +261,14 @@ async function runCall(
261
261
  const parsed = tool.inputSchema.safeParse(args ?? {});
262
262
  if (!parsed.success) return errorResult({ code: "invalid-input", message: describeIssues(parsed.error) });
263
263
  try {
264
- const output = await tool.run(parsed.data, await session());
264
+ let output: unknown;
265
+ try {
266
+ output = await tool.run(parsed.data, await session());
267
+ } catch (error) {
268
+ // A session that the emulator never answered had no machine state: the call goes again on a new session (D57).
269
+ if (!(error instanceof LostBeforeAnswer)) throw error;
270
+ output = await tool.run(parsed.data, await session());
271
+ }
265
272
  // The result goes out in the shape that the tool listing advertises: keys
266
273
  // that the output schema does not name are removed, and a result that does
267
274
  // not fit the schema is an internal failure.
@@ -3,7 +3,7 @@
3
3
  import { z } from "zod";
4
4
 
5
5
  import { formatC64Address } from "../../c64.ts";
6
- import { COMPARISONS, EXECUTION_ACTIONS, MAX_EXECUTION_COUNT, MAX_TIMEOUT_FRAMES, RUN_STATES, STOP_REASONS, type RunTarget } from "../../protocol/vice.ts";
6
+ import { COMPARISONS, EXECUTION_ACTIONS, MAX_EXECUTION_COUNT, MAX_TIMEOUT_FRAMES, RUN_STATES, SPACES, STOP_REASONS, type RunTarget } from "../../protocol/vice.ts";
7
7
  import { AddressInput, AddressOutput, Byte, ConditionInput, defineTool, MemoryViewInput, SpaceInput, toCondition } from "../server.ts";
8
8
 
9
9
  export const c64Execution = defineTool({
@@ -88,6 +88,7 @@ export const c64RunUntil = defineTool({
88
88
  "Kind raster stops at the first instruction at or after a raster line and cycle. If the machine is there now, it stops at the next frame's pass. " +
89
89
  `timeoutFrames (1 to ${MAX_TIMEOUT_FRAMES}, default 3000) limits the run in video frames. ` +
90
90
  "reached tells if the target occurred. stopReason tells why the CPU stopped: target, breakpoint, watchpoint, jam (a JAM opcode) or timeout. " +
91
+ "space tells which CPU stopped: c64, or drive8 for a stop in the disk drive. pc is the address in that space. " +
91
92
  "The CPU is always stopped after this tool.",
92
93
  inputSchema: z
93
94
  .object({
@@ -95,7 +96,7 @@ export const c64RunUntil = defineTool({
95
96
  timeoutFrames: z.number().int().min(1).max(MAX_TIMEOUT_FRAMES).default(3000),
96
97
  })
97
98
  .strict(),
98
- outputSchema: z.object({ reached: z.boolean(), stopReason: z.enum(STOP_REASONS), state: z.enum(RUN_STATES), pc: AddressOutput }),
99
+ outputSchema: z.object({ reached: z.boolean(), stopReason: z.enum(STOP_REASONS), state: z.enum(RUN_STATES), pc: AddressOutput, space: z.enum(SPACES) }),
99
100
  readOnly: false,
100
101
  async run(input, session) {
101
102
  const { target: given } = input;
@@ -7,6 +7,8 @@ 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",
@@ -16,7 +18,7 @@ export const c64Keyboard = defineTool({
16
18
  "No command presses one key of the keyboard matrix: for one specific key, ask the user to press it in the window. " +
17
19
  'Mode text types text, for example "RUN\\n". Letters of either case type the normal (unshifted) key. A line break types RETURN. ' +
18
20
  "Mode petscii types PETSCII codes 1 to 255 in bytes, for example [147] to clear the screen. " +
19
- "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.",
20
22
  inputSchema: z
21
23
  .object({
22
24
  mode: z.enum(["text", "petscii"]),
@@ -24,7 +26,7 @@ export const c64Keyboard = defineTool({
24
26
  bytes: z.array(z.number().int().min(1).max(255)).min(1).optional().describe("mode petscii: PETSCII codes to type"),
25
27
  })
26
28
  .strict(),
27
- 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 }),
28
30
  readOnly: false,
29
31
  async run(input, session) {
30
32
  let petscii: Uint8Array;
@@ -52,7 +54,7 @@ export const c64Joystick = defineTool({
52
54
  description:
53
55
  "Set the joystick in control port 1 or 2: a direction (or center) and the fire button. " +
54
56
  "The C64 sees this state until you change it. To release the joystick, set direction center and fire false. " +
55
- "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.",
56
58
  inputSchema: z
57
59
  .object({
58
60
  port: z.union([z.literal(1), z.literal(2)]).describe("control port 1 or 2"),
@@ -60,7 +62,7 @@ export const c64Joystick = defineTool({
60
62
  fire: z.boolean().default(false),
61
63
  })
62
64
  .strict(),
63
- 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 }),
64
66
  readOnly: false,
65
67
  async run(input, session) {
66
68
  return session.joystick(input);
@@ -20,19 +20,20 @@ export const c64Autostart = defineTool({
20
20
  description:
21
21
  `Start a program, or a disk, tape or cartridge image, from the project (${list(AUTOSTART_TYPES)}). ` +
22
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. " +
23
- "index selects a file on a disk image (default 0, the first file). " +
23
+ "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). " +
24
24
  "The machine runs when this tool returns, and the load continues.",
25
25
  inputSchema: z
26
26
  .object({
27
27
  path: ProjectPath,
28
- index: z.number().int().min(0).max(0xffff).default(0).describe("file number on a disk image. 0 is the first file"),
28
+ 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\"*\""),
29
29
  run: z.boolean().default(true).describe("true runs the program after it loads"),
30
30
  })
31
31
  .strict(),
32
32
  outputSchema: z.object({ state: z.enum(RUN_STATES) }),
33
33
  readOnly: false,
34
34
  async run(input, session) {
35
- return session.autostart(input);
35
+ // VICE numbers the directory entries from 1, and its 0 loads "*".
36
+ return session.autostart({ path: input.path, index: input.index === undefined ? 0 : input.index + 1, run: input.run });
36
37
  },
37
38
  });
38
39
 
@@ -69,10 +69,44 @@ export const VICE: ToolSpec = {
69
69
  */
70
70
  export const MIN_VICE = { major: 3, minor: 9 } as const;
71
71
 
72
- /** "x64sc (VICE 3.10)", "c1541 (VICE 3.9)" or "VICE 3.9.0": the major and minor version. */
73
- export function viceVersionOf(text: string): { major: number; minor: number } | undefined {
74
- const match = /VICE (\d+)\.(\d+)/.exec(text);
75
- 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. The fix is in the next VICE release, and in VICE nightly builds now (SVN r45971 or later). " +
100
+ "Install the latest VICE when this matters.",
101
+ },
102
+ ] as const;
103
+
104
+ /** The known flaws of this VICE version, as text for a person or an agent. */
105
+ export function viceFlaws(version: ViceVersion): string[] {
106
+ return VICE_FLAWS.filter((flaw) => {
107
+ const newer = version.major > flaw.upTo.major || (version.major === flaw.upTo.major && version.minor > flaw.upTo.minor);
108
+ return !newer && (version.revision === undefined || version.revision < flaw.fixedRevision);
109
+ }).map((flaw) => flaw.text);
76
110
  }
77
111
 
78
112
  export function isOlderThanMinimum(version: { major: number; minor: number }): boolean {
package/src/package.ts ADDED
@@ -0,0 +1,21 @@
1
+ // The name, version and npm channel of this copy of c64-re-tools, for the npx
2
+ // commands that refusals and help texts give. The package.json above src/ is
3
+ // the package's own one; in an installed skill it is the skill's one, which
4
+ // has the same name and version (distribution/plugin.ts).
5
+
6
+ import { readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+
9
+ /**
10
+ * The npm dist-tag that a version is published under: a prerelease
11
+ * (2.0.0-rc.1) goes to "next", a release to "latest".
12
+ */
13
+ export function channelOf(version: string): "latest" | "next" {
14
+ return version.includes("-") ? "next" : "latest";
15
+ }
16
+
17
+ /** The name, version and dist-tag of this npm package, from its package.json. */
18
+ export const PACKAGE = (() => {
19
+ const metadata = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8")) as { name: string; version: string };
20
+ return { name: metadata.name, version: metadata.version, tag: channelOf(metadata.version) };
21
+ })();
@@ -61,7 +61,7 @@ export type PetcatResult =
61
61
  | {
62
62
  decoded: true;
63
63
  loadAddress: number;
64
- /** The address after the BASIC end marker; any bytes from here on are no BASIC. */
64
+ /** The first address that holds no BASIC: after a 00 00 end marker, or at a link whose high byte only is zero. */
65
65
  basicEnd: number;
66
66
  listing: string;
67
67
  lines: Array<{ number: number; text: string }>;
@@ -79,6 +79,8 @@ export interface ToolStatus {
79
79
  runs: boolean;
80
80
  /** What is wrong and what to do, when it is missing or does not run. */
81
81
  problem?: string;
82
+ /** Known flaws of this version, each with the version that fixes it (D56). The tool still runs. */
83
+ flaws?: string[];
82
84
  }
83
85
 
84
86
  export interface ToolOperations {
@@ -173,7 +175,8 @@ export function validateToolResult<O extends ToolOperation>(op: O, value: unknow
173
175
  typeof tool.name === "string" &&
174
176
  typeof tool.found === "boolean" &&
175
177
  typeof tool.runs === "boolean" &&
176
- [tool.path, tool.version, tool.problem].every((field) => field === undefined || typeof field === "string");
178
+ [tool.path, tool.version, tool.problem].every((field) => field === undefined || typeof field === "string") &&
179
+ (tool.flaws === undefined || (Array.isArray(tool.flaws) && tool.flaws.every((flaw: unknown) => typeof flaw === "string")));
177
180
  if (!Array.isArray(tools) || !tools.every(isStatus) || attachments.length !== 0) throw new ProtocolError("host.status result is malformed");
178
181
  return value as unknown as ToolOperations[O]["result"];
179
182
  }
@@ -233,7 +236,8 @@ function isPetcatResult(value: Fields): boolean {
233
236
  isObject(handoff) &&
234
237
  isInteger(handoff.line, 0, 0xffff) &&
235
238
  ((handoff.kind === "sys" && isInteger(handoff.address, 0, 0xffff) && handoff.computed === undefined) ||
236
- ((handoff.kind === "sys" || handoff.kind === "usr") && handoff.computed === true && handoff.address === undefined));
239
+ (handoff.kind === "sys" && handoff.computed === true && handoff.address === undefined && typeof handoff.expression === "string") ||
240
+ (handoff.kind === "usr" && handoff.computed === true && handoff.address === undefined));
237
241
  return (
238
242
  value.decoded === true &&
239
243
  isInteger(value.loadAddress, 0, 0xffff) &&
@@ -389,7 +389,9 @@ export interface RunUntilResult {
389
389
  reached: boolean;
390
390
  stopReason: StopReason;
391
391
  state: RunState;
392
+ /** The PC of the CPU in `space`: the CPU that stopped. */
392
393
  pc: number;
394
+ space: Space;
393
395
  }
394
396
 
395
397
  export interface ViceOperations {
@@ -414,15 +416,16 @@ export interface ViceOperations {
414
416
  runUntil: { params: { target: RunTarget; timeoutFrames: number }; result: RunUntilResult };
415
417
  /** Attachment: the PRG bytes, load address first. */
416
418
  programLoad: { params: { address?: number }; result: { state: RunState; loadAddress: number; size: number } };
417
- /** Attachment: the program or image bytes; `type` is the file's extension. */
419
+ /** Attachment: the program or image bytes; `type` is the file's extension. `index` is VICE's file number: 0 loads "*", 1 is the first directory entry. */
418
420
  autostart: { params: { type: string; index: number; run: boolean }; result: { state: RunState } };
419
421
  /** Attachment: the disk image bytes; `type` is the file's extension. */
420
422
  diskAttach: { params: { type: string }; result: { attached: boolean } };
421
423
  /** pc is the reset vector when run is false: the CPU stops there. */
422
424
  reset: { params: { mode: ResetMode; run: boolean }; result: { state: RunState; pc?: number } };
423
425
  /** `data`: PETSCII bytes as lowercase hex. */
424
- keyboard: { params: { data: string }; result: { queuedBytes: number } };
425
- joystick: { params: JoystickState; result: JoystickState };
426
+ /** `emulatorFlaw`: a known flaw of the host's VICE version that this input meets (D56). */
427
+ keyboard: { params: { data: string }; result: { queuedBytes: number; emulatorFlaw?: string } };
428
+ joystick: { params: JoystickState; result: JoystickState & { emulatorFlaw?: string } };
426
429
  breakpoint: { params: BreakpointParams; result: Breakpoint | PointRemoved | { breakpoints: Breakpoint[] } };
427
430
  watchpoint: { params: WatchpointParams; result: Watchpoint | PointRemoved | { watchpoints: Watchpoint[] } };
428
431
  cpuHistory: { params: { limit: number; space: Space }; result: { entries: HistoryEntry[] } };
@@ -1031,7 +1034,8 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1031
1034
  typeof value.reached === "boolean" &&
1032
1035
  isOneOf(STOP_REASONS, value.stopReason) &&
1033
1036
  isOneOf(RUN_STATES, value.state) &&
1034
- isInteger(value.pc, 0, 0xffff);
1037
+ isInteger(value.pc, 0, 0xffff) &&
1038
+ isOneOf(SPACES, value.space);
1035
1039
  if (!ok) throw new ProtocolError("runUntil result is malformed");
1036
1040
  break;
1037
1041
  }
@@ -1060,7 +1064,7 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1060
1064
  break;
1061
1065
  }
1062
1066
  case "keyboard": {
1063
- if (!isInteger(value.queuedBytes, 1, MAX_KEYBOARD_BYTES)) throw new ProtocolError("keyboard result is malformed");
1067
+ if (!isInteger(value.queuedBytes, 1, MAX_KEYBOARD_BYTES) || !(value.emulatorFlaw === undefined || typeof value.emulatorFlaw === "string")) throw new ProtocolError("keyboard result is malformed");
1064
1068
  break;
1065
1069
  }
1066
1070
  case "breakpoint": {
@@ -1265,7 +1269,11 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1265
1269
  break;
1266
1270
  }
1267
1271
  case "joystick": {
1268
- const ok = (value.port === 1 || value.port === 2) && isOneOf(JOYSTICK_DIRECTIONS, value.direction) && typeof value.fire === "boolean";
1272
+ const ok =
1273
+ (value.port === 1 || value.port === 2) &&
1274
+ isOneOf(JOYSTICK_DIRECTIONS, value.direction) &&
1275
+ typeof value.fire === "boolean" &&
1276
+ (value.emulatorFlaw === undefined || typeof value.emulatorFlaw === "string");
1269
1277
  if (!ok) throw new ProtocolError("joystick result is malformed");
1270
1278
  break;
1271
1279
  }