@henols/c64-re-tools 2.0.1 → 2.1.0-rc.2
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 +8 -4
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +24 -9
- package/skills/c64-assembler/scripts/assembler.ts +95 -0
- package/skills/c64-basic/SKILL.md +17 -9
- package/skills/c64-basic/scripts/basic.ts +26 -36
- package/skills/c64-disk/SKILL.md +17 -7
- package/skills/c64-disk/scripts/disk.ts +130 -104
- package/skills/c64-disk/scripts/lib/containers.ts +10 -12
- package/skills/c64-docs/SKILL.md +47 -28
- package/skills/c64-docs/references/style.md +6 -6
- package/skills/c64-docs/scripts/docs.ts +323 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
- package/skills/c64-emulator/SKILL.md +11 -7
- package/skills/c64-knowledge/SKILL.md +34 -21
- package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
- package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
- package/skills/c64-memory-map/SKILL.md +18 -10
- package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
- package/skills/c64-provenance/SKILL.md +82 -26
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +442 -0
- package/skills/c64-provenance/scripts/lib/library.ts +15 -25
- package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
- package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
- package/skills/c64-provenance/scripts/provenance.ts +222 -252
- package/skills/c64-reverse-engineering/SKILL.md +18 -15
- package/skills/c64-source/SKILL.md +28 -16
- package/skills/c64-source/scripts/lib/loader.ts +52 -77
- package/skills/c64-source/scripts/lib/source.ts +33 -42
- package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
- package/skills/c64-static-analysis/SKILL.md +24 -13
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
- package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
- package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
- package/skills/c64-testing/SKILL.md +94 -19
- package/skills/c64-testing/references/recording-format.md +125 -0
- package/skills/c64-testing/references/scenario-format.md +24 -19
- package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
- package/skills/c64-testing/scripts/lib/diff.ts +5 -3
- package/skills/c64-testing/scripts/lib/play.ts +334 -0
- package/skills/c64-testing/scripts/lib/recording.ts +662 -0
- package/skills/c64-testing/scripts/lib/replay.ts +105 -0
- package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
- package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
- package/skills/c64-testing/scripts/testing.ts +189 -0
- package/skills/c64-unpacker/SKILL.md +42 -25
- package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
- package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
- package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
- package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
- package/src/c64.ts +35 -20
- package/src/cli/main.ts +2 -2
- package/src/errors.ts +38 -0
- package/src/graphics.ts +3 -1
- package/src/host/server.ts +3 -2
- package/src/host/vice/session.ts +4 -4
- package/src/host-client/entry-state.ts +106 -0
- package/src/host-client/image.ts +21 -10
- package/src/host-client/steps.ts +285 -108
- package/src/host-client/transfer.ts +21 -15
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/database.ts +3 -23
- package/src/knowledge/expression.ts +8 -11
- package/src/knowledge/history.ts +2 -2
- package/src/knowledge/import.ts +13 -14
- package/src/knowledge/names.ts +50 -0
- package/src/knowledge/program.ts +11 -10
- package/src/knowledge/read.ts +16 -21
- package/src/knowledge/schema.ts +9 -3
- package/src/knowledge/text.ts +5 -5
- package/src/knowledge/write.ts +93 -137
- package/src/mcp/inputs.ts +105 -0
- package/src/mcp/output.ts +2 -13
- package/src/mcp/server.ts +12 -60
- package/src/mcp/tools/debug.ts +17 -12
- package/src/mcp/tools/execution.ts +5 -32
- package/src/mcp/tools/input.ts +7 -32
- package/src/mcp/tools/media.ts +6 -5
- package/src/mcp/tools/memory.ts +11 -38
- package/src/mcp/tools/video.ts +6 -5
- package/src/native/c64run.ts +162 -0
- package/src/native/discover.ts +8 -0
- package/src/native/dxa.ts +7 -6
- package/src/native/ghidra/analyze.ts +2 -1
- package/src/native/ghidra/index.ts +2 -1
- package/src/native/processes.ts +4 -3
- package/src/native/status.ts +25 -4
- package/src/native/types.ts +3 -7
- package/src/opcodes.ts +13 -13
- package/src/platform.ts +9 -9
- package/src/project.ts +16 -26
- package/src/protocol/messages.ts +3 -21
- package/src/protocol/vice.ts +7 -7
- package/src/script.ts +491 -49
- package/skills/c64-assembler/scripts/assemble.ts +0 -138
- package/skills/c64-docs/scripts/generate.ts +0 -109
- package/skills/c64-docs/scripts/link.ts +0 -239
- package/skills/c64-memory-map/scripts/memmap.ts +0 -92
- package/skills/c64-testing/scripts/checklist.ts +0 -51
- package/skills/c64-testing/scripts/test.ts +0 -65
package/src/host/server.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createServer, type Server, type Socket } from "node:net";
|
|
2
2
|
|
|
3
3
|
import { encodeFrame, MessageReader } from "../protocol/framing.ts";
|
|
4
|
+
import { isRefusal } from "../errors.ts";
|
|
4
5
|
import { checkHello, isOneOf, HEARTBEAT_TIMEOUT_MS, parseClientMessage, ProtocolError, type HostMessage, type ReceivedRequest, type VideoStandard, type WireError, WireFailure } from "../protocol/messages.ts";
|
|
5
6
|
import { TOOL_OPERATIONS, validateToolParams, type ToolOperation, type ToolOperations } from "../protocol/tools.ts";
|
|
6
7
|
import { attachmentCount, VICE_OPERATIONS, validateViceParams, type ViceOperation, type ViceOperations } from "../protocol/vice.ts";
|
|
@@ -65,7 +66,7 @@ function describe(error: unknown): string {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
function toWireError(error: unknown, fallback: WireError): WireError {
|
|
68
|
-
return error
|
|
69
|
+
return isRefusal(error) ? { code: error.code, message: error.message } : fallback;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/** Numbers the connections of this process, for the trace. */
|
|
@@ -267,7 +268,7 @@ class Connection {
|
|
|
267
268
|
this.#send({ type: "reply", id: request.id, result }, replyAttachments);
|
|
268
269
|
record({ ok: true, result });
|
|
269
270
|
} catch (error) {
|
|
270
|
-
if (!(error
|
|
271
|
+
if (!isRefusal(error)) {
|
|
271
272
|
this.#log(`operation ${request.op} failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
272
273
|
}
|
|
273
274
|
const wire = toWireError(error, { code: "operation-failed", message: "The operation failed inside the host runtime." });
|
package/src/host/vice/session.ts
CHANGED
|
@@ -450,7 +450,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
450
450
|
case "screenDiscard": {
|
|
451
451
|
const { baseline } = params as ViceOperations["screenDiscard"]["params"];
|
|
452
452
|
if (!this.#baselines.delete(baseline)) throw new WireFailure("not-found", `There is no baseline named ${baseline}.`);
|
|
453
|
-
return {
|
|
453
|
+
return { removed: true };
|
|
454
454
|
}
|
|
455
455
|
case "snapshot":
|
|
456
456
|
return this.#snapshot(params as ViceOperations["snapshot"]["params"]);
|
|
@@ -1122,7 +1122,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1122
1122
|
});
|
|
1123
1123
|
}
|
|
1124
1124
|
|
|
1125
|
-
/** Saves, restores, lists and
|
|
1125
|
+
/** Saves, restores, lists and removes machine snapshots. */
|
|
1126
1126
|
async #snapshot(params: ViceOperations["snapshot"]["params"]): Promise<ViceOperations["snapshot"]["result"]> {
|
|
1127
1127
|
if (params.action === "list") return { snapshots: [...this.#snapshots.keys()] };
|
|
1128
1128
|
const existing = this.#snapshots.get(params.name);
|
|
@@ -1136,10 +1136,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1136
1136
|
return { saved: true, name: params.name };
|
|
1137
1137
|
}
|
|
1138
1138
|
if (existing === undefined) throw new WireFailure("not-found", `There is no snapshot named ${params.name}.`);
|
|
1139
|
-
if (params.action === "
|
|
1139
|
+
if (params.action === "remove") {
|
|
1140
1140
|
rmSync(existing, { force: true });
|
|
1141
1141
|
this.#snapshots.delete(params.name);
|
|
1142
|
-
return {
|
|
1142
|
+
return { removed: true };
|
|
1143
1143
|
}
|
|
1144
1144
|
return this.#restoreFrom(existing);
|
|
1145
1145
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// The machine state at the entry of a program: what the c64-unpacker capture
|
|
2
|
+
// writes in entry_state.json and the knowledge keeps with the program. The one
|
|
3
|
+
// parser of that state, and the one way to put a program at its entry in the
|
|
4
|
+
// emulator.
|
|
5
|
+
|
|
6
|
+
import { c64AddressOf } from "../c64.ts";
|
|
7
|
+
import { Refusal } from "../errors.ts";
|
|
8
|
+
import { MAX_MEMORY_WRITE } from "./tools.ts";
|
|
9
|
+
import { readProjectFile } from "./transfer.ts";
|
|
10
|
+
import type { ViceSessionClient } from "./vice-session.ts";
|
|
11
|
+
|
|
12
|
+
/** Frames from a hard reset to the READY prompt, with margin. */
|
|
13
|
+
export const READY_FRAMES = 200;
|
|
14
|
+
|
|
15
|
+
/** The four pages that the entry state holds: $0000-$03ff. */
|
|
16
|
+
const PAGES = ["$0000", "$0100", "$0200", "$0300"] as const;
|
|
17
|
+
|
|
18
|
+
export interface EntryState {
|
|
19
|
+
pc: number;
|
|
20
|
+
a: number;
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
sp: number;
|
|
24
|
+
flags: { n: boolean; v: boolean; d: boolean; i: boolean; z: boolean; c: boolean };
|
|
25
|
+
/** The CPU port: $00 and $01. */
|
|
26
|
+
port: [number, number];
|
|
27
|
+
/** $0000-$03ff. */
|
|
28
|
+
pages: Uint8Array;
|
|
29
|
+
/** The whole entry state as the capture wrote it, without differsFromReset: what the knowledge keeps. */
|
|
30
|
+
raw: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const isObject = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
const isByte = (value: unknown): value is number => Number.isInteger(value) && (value as number) >= 0 && (value as number) <= 0xff;
|
|
35
|
+
|
|
36
|
+
/** The entry state of a value. `where` names it in a refusal, for example a file or "the entry state of the knowledge". */
|
|
37
|
+
export function parseEntryState(value: unknown, where: string): EntryState {
|
|
38
|
+
const fail = (what: string): never => {
|
|
39
|
+
throw new Refusal("invalid-input", `${where} is not an entry state of a c64-unpacker capture: ${what}. Capture the program again with c64-unpacker capture or loads.`);
|
|
40
|
+
};
|
|
41
|
+
if (!isObject(value)) return fail("it is not a JSON object");
|
|
42
|
+
const registers = isObject(value.registers) ? value.registers : fail("it has no registers");
|
|
43
|
+
const pc = typeof registers.pc === "string" ? c64AddressOf(registers.pc) : undefined;
|
|
44
|
+
if (pc === undefined) fail("registers.pc is not a $ address");
|
|
45
|
+
for (const name of ["a", "x", "y", "sp"]) if (!isByte(registers[name])) fail(`registers.${name} is not a byte`);
|
|
46
|
+
const flags = isObject(registers.flags) ? registers.flags : fail("registers.flags is not an object");
|
|
47
|
+
const port = isObject(value.cpuPort) ? [value.cpuPort.$00, value.cpuPort.$01] : fail("it has no cpuPort");
|
|
48
|
+
if (!port.every(isByte)) fail("cpuPort needs $00 and $01 as bytes");
|
|
49
|
+
const pages = isObject(value.pages) ? PAGES.map((page) => (value.pages as Record<string, unknown>)[page]) : fail("it has no pages");
|
|
50
|
+
if (!pages.every((page) => typeof page === "string" && /^[0-9a-fA-F]{512}$/.test(page))) fail("pages needs $0000, $0100, $0200 and $0300, each 256 bytes as hex");
|
|
51
|
+
const { differsFromReset: _differs, ...raw } = value;
|
|
52
|
+
return {
|
|
53
|
+
pc: pc!,
|
|
54
|
+
a: registers.a as number,
|
|
55
|
+
x: registers.x as number,
|
|
56
|
+
y: registers.y as number,
|
|
57
|
+
sp: registers.sp as number,
|
|
58
|
+
flags: { n: flags.n === true, v: flags.v === true, d: flags.d === true, i: flags.i === true, z: flags.z === true, c: flags.c === true },
|
|
59
|
+
port: port as [number, number],
|
|
60
|
+
pages: Uint8Array.from(Buffer.from((pages as string[]).join(""), "hex")),
|
|
61
|
+
raw,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Reads an entry_state.json of the project. Refuses a missing file with the remedy, and a file that is no entry state. */
|
|
66
|
+
export function readEntryState(path: string): EntryState {
|
|
67
|
+
const { bytes } = readProjectFile(path);
|
|
68
|
+
let value: unknown;
|
|
69
|
+
try {
|
|
70
|
+
value = JSON.parse(bytes.toString("utf8"));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Refusal("invalid-input", `${path} is not JSON: ${(error as Error).message}`);
|
|
73
|
+
}
|
|
74
|
+
return parseEntryState(value, path);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A hard reset, then the frames until the KERNAL shows READY. */
|
|
78
|
+
export async function resetToReady(client: ViceSessionClient): Promise<void> {
|
|
79
|
+
await client.reset({ mode: "hard", run: true });
|
|
80
|
+
await client.execution({ action: "advance-frames", count: READY_FRAMES, space: "c64" });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Writes the entry state into the machine: pages 0 to 3 in RAM from $0002, then
|
|
85
|
+
* the CPU port, whose bits switch what the CPU sees; with registers, also the
|
|
86
|
+
* CPU registers. The machine is paused first.
|
|
87
|
+
*/
|
|
88
|
+
export async function applyEntryState(client: ViceSessionClient, state: EntryState, options: { registers: boolean }): Promise<void> {
|
|
89
|
+
if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
|
|
90
|
+
await client.memoryWrite({ address: 0x0002, data: Buffer.from(state.pages.subarray(2)).toString("hex"), space: "c64", view: "ram" });
|
|
91
|
+
await client.memoryWrite({ address: 0x0000, data: Buffer.from(state.port).toString("hex"), space: "c64", view: "cpu" });
|
|
92
|
+
if (options.registers) await client.registersSet("c64", { pc: state.pc, a: state.a, x: state.x, y: state.y, sp: state.sp, flags: state.flags });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Puts a captured program at its entry: a reset to READY, the bytes of the segments, then the whole entry state. */
|
|
96
|
+
export async function startAtEntry(client: ViceSessionClient, image: { segments: ReadonlyArray<{ start: number; end: number }>; byteAt: (address: number) => number }, state: EntryState): Promise<void> {
|
|
97
|
+
await resetToReady(client);
|
|
98
|
+
for (const segment of image.segments) {
|
|
99
|
+
for (let at = segment.start; at <= segment.end; at += MAX_MEMORY_WRITE) {
|
|
100
|
+
const last = Math.min(segment.end, at + MAX_MEMORY_WRITE - 1);
|
|
101
|
+
const bytes = Buffer.from(Array.from({ length: last - at + 1 }, (_, offset) => image.byteAt(at + offset)));
|
|
102
|
+
await client.memoryWrite({ address: at, data: bytes.toString("hex"), space: "c64", view: "ram" });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
await applyEntryState(client, state, { registers: true });
|
|
106
|
+
}
|
package/src/host-client/image.ts
CHANGED
|
@@ -3,21 +3,20 @@
|
|
|
3
3
|
|
|
4
4
|
import { posix } from "node:path";
|
|
5
5
|
|
|
6
|
+
import { isRefusal, Refusal } from "../errors.ts";
|
|
6
7
|
import { formatC64Address, prgRange } from "../c64.ts";
|
|
8
|
+
import { readEntryState, type EntryState } from "./entry-state.ts";
|
|
7
9
|
import { readProjectFile } from "./transfer.ts";
|
|
8
10
|
|
|
9
|
-
/** A file that is no image: the scripts report it as a bad call. */
|
|
10
|
-
export class ImageError extends Error {
|
|
11
|
-
override name = "ImageError";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
11
|
export interface Image {
|
|
15
12
|
segments: Array<{ start: number; end: number }>;
|
|
16
13
|
/** The byte at an address inside a segment. */
|
|
17
14
|
byteAt: (address: number) => number;
|
|
15
|
+
/** image.json only: the 64 KiB memory of the capture, also outside the segments. */
|
|
16
|
+
memory?: Uint8Array;
|
|
18
17
|
entry?: number;
|
|
19
|
-
/** entry_state.json next to image.json. */
|
|
20
|
-
|
|
18
|
+
/** The entry state in entry_state.json next to image.json, when the capture wrote one. */
|
|
19
|
+
state?: EntryState;
|
|
21
20
|
/** The stages of the capture in image.json, as the unpacker wrote them. */
|
|
22
21
|
stages?: Array<Record<string, unknown>>;
|
|
23
22
|
}
|
|
@@ -30,12 +29,12 @@ export function readImage(path: string): Image {
|
|
|
30
29
|
try {
|
|
31
30
|
range = prgRange(bytes);
|
|
32
31
|
} catch (error) {
|
|
33
|
-
throw new
|
|
32
|
+
throw new Refusal("invalid-input", `${path}: ${(error as Error).message}`);
|
|
34
33
|
}
|
|
35
34
|
return { segments: [{ start: range.start, end: range.end }], byteAt: (address) => range.body[address - range.start]! };
|
|
36
35
|
}
|
|
37
36
|
const fail = (what: string): never => {
|
|
38
|
-
throw new
|
|
37
|
+
throw new Refusal("invalid-input", `${path} is not an image.json of the c64-unpacker skill: ${what}`);
|
|
39
38
|
};
|
|
40
39
|
const isObject = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
40
|
let parsed: unknown;
|
|
@@ -59,8 +58,20 @@ export function readImage(path: string): Image {
|
|
|
59
58
|
const directory = posix.dirname(path);
|
|
60
59
|
const memory = readProjectFile(posix.join(directory, manifest.image as string)).bytes;
|
|
61
60
|
if (memory.length !== 0x10000) fail(`its image has ${memory.length} bytes, not 65536`);
|
|
62
|
-
const image: Image = { segments, byteAt: (at) => memory[at]!,
|
|
61
|
+
const image: Image = { segments, byteAt: (at) => memory[at]!, memory };
|
|
62
|
+
const state = entryStateNextTo(posix.join(directory, "entry_state.json"));
|
|
63
|
+
if (state !== undefined) image.state = state;
|
|
63
64
|
if (manifest.entry !== undefined) image.entry = address(manifest.entry, "entry");
|
|
64
65
|
if (Array.isArray(manifest.stages)) image.stages = manifest.stages.map((stage, index) => (isObject(stage) ? stage : fail(`stage ${index + 1} is not an object`)));
|
|
65
66
|
return image;
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
/** The entry state of a capture, or undefined when the capture has no entry_state.json. */
|
|
70
|
+
function entryStateNextTo(path: string): EntryState | undefined {
|
|
71
|
+
try {
|
|
72
|
+
return readEntryState(path);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (isRefusal(error) && error.code === "not-found") return undefined;
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|