@henols/c64-re-tools 1.0.0 → 2.0.0-rc.10

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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +166 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +126 -0
  26. package/skills/c64-source/scripts/generate.ts +490 -0
  27. package/skills/c64-source/scripts/loader.ts +571 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +595 -0
  30. package/skills/c64-static-analysis/SKILL.md +119 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-testing/SKILL.md +103 -0
  37. package/skills/c64-testing/references/scenario-format.md +116 -0
  38. package/skills/c64-testing/scripts/checklist.ts +51 -0
  39. package/skills/c64-testing/scripts/run.ts +189 -0
  40. package/skills/c64-testing/scripts/scenario.ts +493 -0
  41. package/skills/c64-testing/scripts/test.ts +65 -0
  42. package/skills/c64-unpacker/SKILL.md +135 -0
  43. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  44. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  45. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  46. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  47. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  48. package/src/c64.ts +411 -0
  49. package/src/cli/ap-sdk.ts +44 -0
  50. package/src/cli/cleanup.ts +194 -0
  51. package/src/cli/main.ts +125 -0
  52. package/src/cli/status.ts +61 -0
  53. package/src/host/main.ts +134 -0
  54. package/src/host/server.ts +392 -0
  55. package/src/host/tools/c1541.ts +284 -0
  56. package/src/host/tools/index.ts +35 -0
  57. package/src/host/tools/petcat.ts +77 -0
  58. package/src/host/tools/status.ts +47 -0
  59. package/src/host/tools/version.ts +41 -0
  60. package/src/host/vice/adapter.ts +804 -0
  61. package/src/host/vice/binary-monitor.ts +389 -0
  62. package/src/host/vice/chips.ts +118 -0
  63. package/src/host/vice/connect.ts +34 -0
  64. package/src/host/vice/process.ts +413 -0
  65. package/src/host/vice/screen.ts +184 -0
  66. package/src/host/vice/session.ts +1426 -0
  67. package/src/host/vice/text-monitor.ts +223 -0
  68. package/src/host-client/autostart.ts +55 -0
  69. package/src/host-client/connect.ts +241 -0
  70. package/src/host-client/coverage.ts +63 -0
  71. package/src/host-client/steps.ts +170 -0
  72. package/src/host-client/tools.ts +89 -0
  73. package/src/host-client/transfer.ts +126 -0
  74. package/src/host-client/vice-session.ts +316 -0
  75. package/src/knowledge/database.ts +132 -0
  76. package/src/knowledge/history.ts +163 -0
  77. package/src/knowledge/import.ts +391 -0
  78. package/src/knowledge/read.ts +342 -0
  79. package/src/knowledge/schema.ts +229 -0
  80. package/src/knowledge/write.ts +663 -0
  81. package/src/mcp/main.ts +134 -0
  82. package/src/mcp/output.ts +32 -0
  83. package/src/mcp/server.ts +316 -0
  84. package/src/mcp/tools/debug.ts +327 -0
  85. package/src/mcp/tools/execution.ts +117 -0
  86. package/src/mcp/tools/input.ts +72 -0
  87. package/src/mcp/tools/machine.ts +100 -0
  88. package/src/mcp/tools/media.ts +129 -0
  89. package/src/mcp/tools/memory.ts +250 -0
  90. package/src/mcp/tools/video.ts +271 -0
  91. package/src/native/acme.ts +116 -0
  92. package/src/native/discover.ts +179 -0
  93. package/src/native/dxa.ts +166 -0
  94. package/src/native/ghidra/analyze.ts +86 -0
  95. package/src/native/ghidra/index.ts +173 -0
  96. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  97. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  98. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  99. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  100. package/src/native/ghidra/scripts/C64Export.java +274 -0
  101. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  102. package/src/native/local.ts +34 -0
  103. package/src/native/processes.ts +350 -0
  104. package/src/native/run.ts +172 -0
  105. package/src/native/staging.ts +83 -0
  106. package/src/native/status.ts +113 -0
  107. package/src/native/types.ts +257 -0
  108. package/src/native/watchdog.ts +78 -0
  109. package/src/package.ts +21 -0
  110. package/src/platform.ts +441 -0
  111. package/src/project.ts +39 -0
  112. package/src/protocol/framing.ts +132 -0
  113. package/src/protocol/messages.ts +259 -0
  114. package/src/protocol/tools.ts +266 -0
  115. package/src/protocol/vice.ts +1348 -0
  116. package/src/script.ts +60 -0
  117. package/src/time.ts +15 -0
  118. package/src/trace.ts +194 -0
  119. package/THIRD-PARTY-NOTICES.md +0 -26
  120. package/bin/cli.mjs +0 -283
  121. package/skills/acme-build/SKILL.md +0 -260
  122. package/skills/acme-build/scripts/acme.mjs +0 -341
  123. package/skills/acme-build/template.a +0 -53
  124. package/skills/c64-disk-access/SKILL.md +0 -159
  125. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  126. package/skills/c64-memory-mapping/SKILL.md +0 -631
  127. package/skills/c64-memory-mapping/memmap.json +0 -8800
  128. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  129. package/skills/c64-petcat/SKILL.md +0 -87
  130. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  131. package/skills/c64-program-recon/SKILL.md +0 -706
  132. package/skills/c64-program-recon/references/control-flow.md +0 -180
  133. package/skills/c64-program-recon/references/graphics.md +0 -79
  134. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  135. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  136. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  137. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  138. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  139. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  140. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  141. package/skills/c64-provenance-diff/SKILL.md +0 -292
  142. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  143. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  144. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  145. package/skills/c64-ram-capture/SKILL.md +0 -474
  146. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  147. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  148. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  149. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  150. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  151. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  152. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  153. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  154. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  155. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  156. package/skills/c64-ram-capture/transients/README.md +0 -137
  157. package/skills/routine-queue-walker/SKILL.md +0 -394
  158. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  159. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,170 @@
1
+ // The action steps of the step language (D43): write, registers, joystick,
2
+ // type, frames and runUntil. c64-testing scenarios and the c64-unpacker
3
+ // steps file share them; each caller adds its own steps around them.
4
+
5
+ import { parseC64Address, textToPetscii } from "../c64.ts";
6
+ import { JOYSTICK_DIRECTIONS, MAX_EXECUTION_COUNT, MAX_KEYBOARD_BYTES, MAX_MEMORY_WRITE, MAX_TIMEOUT_FRAMES } from "../protocol/vice.ts";
7
+ import type { RunTarget, RunUntilResult } from "../protocol/vice.ts";
8
+ import type { ViceSessionClient } from "./vice-session.ts";
9
+
10
+ /** A "$xxxx" address, or a symbol name where the caller resolves names. */
11
+ export type Location = string;
12
+
13
+ type JoystickDirection = (typeof JOYSTICK_DIRECTIONS)[number];
14
+
15
+ export type ActionStep =
16
+ | { write: { at: Location; bytes: number[] } }
17
+ | { registers: Partial<Record<"pc" | "a" | "x" | "y" | "sp", number | Location>> }
18
+ | { joystick: { port: 1 | 2; direction: JoystickDirection; fire?: boolean } }
19
+ | { type: string }
20
+ | { frames: number }
21
+ | { runUntil: { at: Location; raster?: number } | { raster: number } | { memory: { at: Location; equals: number } }; timeoutFrames: number };
22
+
23
+ /** The highest raster line of a PAL frame; an NTSC frame has fewer, which the emulator checks. */
24
+ const MAX_RASTER_LINE = 311;
25
+
26
+ export const ACTION_STEP_KINDS = ["write", "registers", "joystick", "type", "frames", "runUntil"] as const;
27
+
28
+ /** The keys a step of each kind can have, when it is more than the kind itself. */
29
+ export const ACTION_STEP_KEYS: Record<string, readonly string[]> = { runUntil: ["runUntil", "timeoutFrames"] };
30
+
31
+ /** The registers a registers step can set. */
32
+ export const SET_REGISTERS = ["pc", "a", "x", "y", "sp"];
33
+
34
+ export const isObject = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
35
+ export const isInteger = (value: unknown, min: number, max: number): value is number => Number.isInteger(value) && (value as number) >= min && (value as number) <= max;
36
+
37
+ /** Checks a "$xxxx" address, or also a symbol name when `names` allows it. */
38
+ export function checkLocation(value: unknown, what: string, fail: (message: string) => never, names = true): void {
39
+ if (typeof value !== "string" || value === "") fail(`${what} must be a $ address${names ? " or a symbol name" : ""}`);
40
+ if ((value as string).startsWith("$")) {
41
+ try {
42
+ parseC64Address(value as string);
43
+ } catch {
44
+ fail(`${what} must be $ followed by four hex digits, not ${value as string}`);
45
+ }
46
+ } else if (!names) {
47
+ fail(`${what} must be $ followed by four hex digits, not ${value as string}`);
48
+ }
49
+ }
50
+
51
+ /** Checks one action step of the given kind. `names` says whether symbol names may stand for addresses. */
52
+ export function checkActionStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never, names = true): void {
53
+ switch (kind) {
54
+ case "write": {
55
+ const write = step.write;
56
+ if (!isObject(write) || Object.keys(write).some((key) => key !== "at" && key !== "bytes")) return fail(`${at}: write needs at and bytes, and nothing else`);
57
+ checkLocation(write.at, `${at}: write.at`, fail, names);
58
+ if (!Array.isArray(write.bytes) || write.bytes.length === 0 || write.bytes.length > MAX_MEMORY_WRITE || !write.bytes.every((byte) => isInteger(byte, 0, 0xff))) {
59
+ fail(`${at}: write.bytes must be a list of 1 to ${MAX_MEMORY_WRITE} numbers from 0 to 255`);
60
+ }
61
+ return;
62
+ }
63
+ case "registers": {
64
+ const registers = step.registers;
65
+ if (!isObject(registers) || Object.keys(registers).length === 0) return fail(`${at}: registers must name at least one of ${SET_REGISTERS.join(", ")}`);
66
+ for (const [name, value] of Object.entries(registers)) {
67
+ if (!SET_REGISTERS.includes(name)) fail(`${at}: registers can set only ${SET_REGISTERS.join(", ")}, not ${name}`);
68
+ if (typeof value === "string") checkLocation(value, `${at}: registers.${name}`, fail, names);
69
+ else if (!isInteger(value, 0, name === "pc" ? 0xffff : 0xff)) fail(`${at}: registers.${name} must be ${names ? "a symbol name, " : ""}a $ address or a number from 0 to ${name === "pc" ? 65535 : 255}`);
70
+ }
71
+ return;
72
+ }
73
+ case "joystick": {
74
+ const joystick = step.joystick;
75
+ if (!isObject(joystick) || Object.keys(joystick).some((key) => !["port", "direction", "fire"].includes(key))) return fail(`${at}: joystick needs port and direction, and fire is optional`);
76
+ if (joystick.port !== 1 && joystick.port !== 2) fail(`${at}: joystick.port must be 1 or 2`);
77
+ if (!(JOYSTICK_DIRECTIONS as readonly unknown[]).includes(joystick.direction)) fail(`${at}: joystick.direction must be one of ${JOYSTICK_DIRECTIONS.join(", ")}`);
78
+ if (joystick.fire !== undefined && typeof joystick.fire !== "boolean") fail(`${at}: joystick.fire must be true or false`);
79
+ return;
80
+ }
81
+ case "type": {
82
+ if (typeof step.type !== "string" || step.type === "") return fail(`${at}: type needs text`);
83
+ let bytes: Uint8Array;
84
+ try {
85
+ bytes = textToPetscii(step.type);
86
+ } catch (error) {
87
+ return fail(`${at}: ${(error as Error).message}`);
88
+ }
89
+ if (bytes.length > MAX_KEYBOARD_BYTES) fail(`${at}: type can have at most ${MAX_KEYBOARD_BYTES} keys`);
90
+ return;
91
+ }
92
+ case "frames":
93
+ if (!isInteger(step.frames, 1, MAX_EXECUTION_COUNT)) fail(`${at} needs a frame count from 1 to ${MAX_EXECUTION_COUNT}`);
94
+ return;
95
+ case "runUntil": {
96
+ const target = step.runUntil;
97
+ const keys = isObject(target) ? Object.keys(target).sort().join(",") : "";
98
+ // at, raster, at with raster (the address only on that line, D46), or memory.
99
+ if (!["at", "raster", "at,raster", "memory"].includes(keys)) return fail(`${at}: runUntil needs at, raster, at and raster, or memory`);
100
+ const until = target as Record<string, unknown>;
101
+ if ("raster" in until && !isInteger(until.raster, 0, MAX_RASTER_LINE)) fail(`${at}: runUntil.raster must be a raster line from 0 to ${MAX_RASTER_LINE}`);
102
+ if ("at" in until) checkLocation(until.at, `${at}: runUntil.at`, fail, names);
103
+ else if ("memory" in until) {
104
+ const memory = until.memory;
105
+ if (!isObject(memory) || Object.keys(memory).some((key) => key !== "at" && key !== "equals")) return fail(`${at}: runUntil.memory needs at and equals, and nothing else`);
106
+ checkLocation(memory.at, `${at}: runUntil.memory.at`, fail, names);
107
+ if (!isInteger(memory.equals, 0, 0xff)) fail(`${at}: runUntil.memory.equals must be a number from 0 to 255`);
108
+ }
109
+ if (!isInteger(step.timeoutFrames, 1, MAX_TIMEOUT_FRAMES)) fail(`${at} needs timeoutFrames from 1 to ${MAX_TIMEOUT_FRAMES}`);
110
+ return;
111
+ }
112
+ default:
113
+ fail(`${at}: ${kind} is not an action step`);
114
+ }
115
+ }
116
+
117
+ /** Checks a list of action steps only, with $ addresses only: the steps file of c64-unpacker. */
118
+ export function checkActionSteps(value: unknown, fail: (message: string) => never): ActionStep[] {
119
+ if (!Array.isArray(value) || value.length === 0) return fail("the steps are a JSON list with at least one step");
120
+ value.forEach((step: unknown, index) => {
121
+ const at = `step ${index + 1}`;
122
+ if (!isObject(step)) return fail(`${at} must be an object`);
123
+ const kinds = Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key));
124
+ if (kinds.length !== 1) return fail(`${at} must have exactly one of ${ACTION_STEP_KINDS.join(", ")}`);
125
+ const kind = kinds[0]!;
126
+ const allowed = ACTION_STEP_KEYS[kind] ?? [kind];
127
+ for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
128
+ checkActionStep(kind, step, at, fail, false);
129
+ });
130
+ return value as ActionStep[];
131
+ }
132
+
133
+ /** Stops the CPU when it runs: a write, a register change or an exact frame count needs it stopped. */
134
+ async function stop(client: ViceSessionClient): Promise<void> {
135
+ if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
136
+ }
137
+
138
+ /**
139
+ * Runs one action step. `at` turns a location into an address. A runUntil
140
+ * step that does not reach its target returns the stop, for the caller to judge.
141
+ */
142
+ export async function runActionStep(client: ViceSessionClient, step: ActionStep, at: (location: Location) => number): Promise<RunUntilResult | undefined> {
143
+ if ("write" in step) {
144
+ await stop(client);
145
+ await client.memoryWrite({ address: at(step.write.at), data: Buffer.from(step.write.bytes).toString("hex"), space: "c64", view: "cpu" });
146
+ } else if ("registers" in step) {
147
+ await stop(client);
148
+ const values: Record<string, number> = {};
149
+ for (const [name, value] of Object.entries(step.registers)) values[name] = typeof value === "number" ? value : at(value as Location);
150
+ await client.registersSet("c64", values);
151
+ } else if ("joystick" in step) {
152
+ await client.joystick({ port: step.joystick.port, direction: step.joystick.direction, fire: step.joystick.fire ?? false });
153
+ } else if ("type" in step) {
154
+ await client.keyboard(textToPetscii(step.type));
155
+ } else if ("frames" in step) {
156
+ await stop(client);
157
+ await client.execution({ action: "advance-frames", count: step.frames, space: "c64" });
158
+ } else {
159
+ const until = step.runUntil;
160
+ const target: RunTarget =
161
+ "at" in until
162
+ ? { kind: "address", address: at(until.at), space: "c64", ...(until.raster === undefined ? {} : { condition: { kind: "raster" as const, line: until.raster } }) }
163
+ : "raster" in until
164
+ ? { kind: "raster", line: until.raster }
165
+ : { kind: "memory", address: at(until.memory.at), operator: "eq", value: until.memory.equals, space: "c64", view: "cpu" };
166
+ const reached = await client.runUntil({ target, timeoutFrames: step.timeoutFrames });
167
+ if (!reached.reached) return reached;
168
+ }
169
+ return undefined;
170
+ }
@@ -0,0 +1,89 @@
1
+ // Short-lived typed requests to the Host Runtime for skill scripts:
2
+ // VICE's own tools (c1541, petcat) and the host tool status. Each request
3
+ // opens its own "tool" connection, sends bytes, and closes. Skill scripts run
4
+ // ACME, DXA and Ghidra themselves through src/native.
5
+
6
+ import { ProtocolError, WireFailure } from "../protocol/messages.ts";
7
+ import { DISK_IMAGE_TYPES, validateToolResult, type C1541Result, type DiskAction, type DiskImageType, type PetcatResult, type ToolOperation, type ToolOperations, type HostInfo, type ToolStatus } from "../protocol/tools.ts";
8
+ import { HostConnection } from "./connect.ts";
9
+ import { readProjectFile } from "./transfer.ts";
10
+
11
+ export interface ToolCallOptions {
12
+ env?: NodeJS.ProcessEnv;
13
+ }
14
+
15
+ /** Sends one tool request and returns its validated result and attachments. */
16
+ export async function callTool<O extends ToolOperation>(
17
+ op: O,
18
+ params: ToolOperations[O]["params"],
19
+ attachments: readonly Uint8Array[],
20
+ options: ToolCallOptions = {},
21
+ ): Promise<{ result: ToolOperations[O]["result"]; attachments: Buffer[] }> {
22
+ const connection = await HostConnection.open({ role: "tool", ...(options.env === undefined ? {} : { env: options.env }) });
23
+ try {
24
+ return await new Promise((resolve, reject) => {
25
+ connection.onMessage((message, received) => {
26
+ if (message.type !== "reply" || message.id !== 1) {
27
+ reject(new WireFailure("installation-incomplete", "The c64-re-tools host runtime answered out of turn. Install the same c64-re-tools release on both sides."));
28
+ return;
29
+ }
30
+ if ("error" in message) {
31
+ reject(new WireFailure(message.error.code, message.error.message));
32
+ return;
33
+ }
34
+ try {
35
+ resolve({ result: validateToolResult(op, message.result, received), attachments: received });
36
+ } catch (error) {
37
+ reject(
38
+ new WireFailure(
39
+ "operation-failed",
40
+ error instanceof ProtocolError ? "The c64-re-tools host runtime sent an invalid reply." : "The c64-re-tools host-client could not read the reply of the host runtime.",
41
+ ),
42
+ );
43
+ }
44
+ });
45
+ void connection.closed.then(() =>
46
+ reject(new WireFailure("machine-unavailable", "The connection to the c64-re-tools host runtime was lost during the request. Check that the Host Runtime (c64-re-tools-host) is running.")),
47
+ );
48
+ connection.send({ type: "request", id: 1, op, params }, attachments);
49
+ });
50
+ } finally {
51
+ await connection.close();
52
+ }
53
+ }
54
+
55
+ export interface InspectDiskRequest {
56
+ /** Project-relative disk image; its extension gives the image type. */
57
+ image: string;
58
+ action: DiskAction;
59
+ /** entry, chain and read: the file name as the directory shows it. */
60
+ name?: string;
61
+ }
62
+
63
+ /** c1541.inspect: a found read also returns the file's bytes. */
64
+ export async function inspectDisk(request: InspectDiskRequest, options: ToolCallOptions = {}): Promise<C1541Result & { data?: Buffer }> {
65
+ const file = readProjectFile(request.image);
66
+ if (!(DISK_IMAGE_TYPES as readonly string[]).includes(file.type)) {
67
+ throw new WireFailure("invalid-input", `${request.image} is not a disk image that this tool reads. Use a .d64, .d71, .d81 or .g64 file.`);
68
+ }
69
+ const params: ToolOperations["c1541.inspect"]["params"] = { action: request.action, imageType: file.type as DiskImageType };
70
+ if (request.name !== undefined) params.name = request.name;
71
+ const { result, attachments } = await callTool("c1541.inspect", params, [file.bytes], options);
72
+ return result.action === "read" && result.found ? { ...result, data: attachments[0]! } : result;
73
+ }
74
+
75
+ /** petcat.decode: the C64 BASIC V2 listing of a project program and its machine-code handoffs. */
76
+ export async function decodeBasic(request: { program: string }, options: ToolCallOptions = {}): Promise<PetcatResult> {
77
+ const file = readProjectFile(request.program);
78
+ return (await callTool("petcat.decode", {}, [file.bytes], options)).result;
79
+ }
80
+
81
+ /** host.status: the Host Runtime, and each native tool on the host, found and run once. */
82
+ export async function hostTools(options: ToolCallOptions = {}): Promise<{ host: HostInfo; tools: ToolStatus[] }> {
83
+ return (await callTool("host.status", {}, [], options)).result;
84
+ }
85
+
86
+ /** Skill scripts see failures and wire limits through the host-client, never the private protocol module. */
87
+ export { WireFailure } from "../protocol/messages.ts";
88
+ export { DISK_ACTIONS } from "../protocol/tools.ts";
89
+ export { JOYSTICK_DIRECTIONS, MAX_BASELINES, MAX_EXECUTION_COUNT, MAX_KEYBOARD_BYTES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, MAX_MEMORY_WRITE, MAX_TIMEOUT_FRAMES, maskCoversFrame } from "../protocol/vice.ts";
@@ -0,0 +1,126 @@
1
+ // Reads project files on the client side for transfer to the Host Runtime.
2
+ // The host never sees a client path: only bytes and the file's type.
3
+
4
+ import { lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
5
+ import { dirname, extname, join, relative, sep } from "node:path";
6
+
7
+ import { isInside, projectRoot, resolveProjectFile, resolveProjectPath } from "../project.ts";
8
+ import { MAX_ATTACHMENT_BYTES } from "../protocol/framing.ts";
9
+ import { WireFailure } from "../protocol/messages.ts";
10
+ import { MAX_TREE_FILES, type SourceTree } from "../protocol/tools.ts";
11
+
12
+ export interface ProjectFile {
13
+ bytes: Buffer;
14
+ /** Lowercase extension without the dot; empty when the file has none. */
15
+ type: string;
16
+ }
17
+
18
+ /**
19
+ * Reads one project file. Throws WireFailure: invalid-input for a path the
20
+ * project rules refuse, not-found when nothing is there, limit-exceeded when
21
+ * the file is too large to transfer.
22
+ */
23
+ export function readProjectFile(path: string, options: { root?: string } = {}): ProjectFile {
24
+ const root = options.root ?? projectRoot();
25
+ let real: string;
26
+ try {
27
+ real = resolveProjectFile(path, root);
28
+ } catch (error) {
29
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
30
+ throw new WireFailure("not-found", `There is no file at ${path} in the project directory.`);
31
+ }
32
+ if (error instanceof TypeError) {
33
+ throw new WireFailure("invalid-input", `${path}: give a path relative to the project directory that stays inside it.`);
34
+ }
35
+ throw error;
36
+ }
37
+ const stat = statSync(real);
38
+ if (!stat.isFile()) throw new WireFailure("invalid-input", `${path} is not a file.`);
39
+ if (stat.size > MAX_ATTACHMENT_BYTES) {
40
+ throw new WireFailure("limit-exceeded", `${path} has ${stat.size} bytes. The limit is ${MAX_ATTACHMENT_BYTES} bytes.`);
41
+ }
42
+ return { bytes: readFileSync(real), type: extname(path).slice(1).toLowerCase() };
43
+ }
44
+
45
+ /**
46
+ * Writes bytes to a project file on the client side, making its directories
47
+ * and overwriting it. Throws WireFailure(invalid-input) for a path outside the
48
+ * project, also one that leaves it through a symbolic link.
49
+ */
50
+ export function writeProjectFile(path: string, bytes: Uint8Array, options: { root?: string; what?: string } = {}): void {
51
+ const root = options.root ?? projectRoot();
52
+ const refuse = () => new WireFailure("invalid-input", `${options.what ?? path}: give a path relative to the project directory that stays inside it, not ${path}.`);
53
+ let file: string;
54
+ try {
55
+ file = resolveProjectPath(path, root);
56
+ } catch {
57
+ throw refuse();
58
+ }
59
+ mkdirSync(dirname(file), { recursive: true });
60
+ if (!isInside(realpathSync(root), realpathSync(dirname(file)))) throw refuse();
61
+ let link = false;
62
+ try {
63
+ link = lstatSync(file).isSymbolicLink();
64
+ } catch (error) {
65
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
66
+ }
67
+ if (link) throw refuse();
68
+ writeFileSync(file, bytes);
69
+ }
70
+
71
+ /** Directories never staged: version control and the toolkit's own project state. */
72
+ const SKIPPED_DIRECTORIES = new Set([".git", ".hg", ".svn", ".c64-re-tools"]);
73
+
74
+ /**
75
+ * Reads a project directory as a source tree for staging: ordinary
76
+ * files with relative POSIX paths. A symbolic link is followed only when its
77
+ * target stays inside the source root; otherwise the tree is refused.
78
+ */
79
+ export function readProjectTree(path: string, options: { root?: string } = {}): { files: SourceTree; contents: Buffer[] } {
80
+ const projectRootPath = options.root ?? projectRoot();
81
+ let sourceRoot: string;
82
+ try {
83
+ sourceRoot = resolveProjectFile(path, projectRootPath);
84
+ } catch (error) {
85
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new WireFailure("not-found", `There is no directory ${path} in the project directory.`);
86
+ if (error instanceof TypeError) throw new WireFailure("invalid-input", `${path}: give a directory relative to the project directory that stays inside it.`);
87
+ throw error;
88
+ }
89
+ if (!statSync(sourceRoot).isDirectory()) throw new WireFailure("invalid-input", `${path} is not a directory.`);
90
+
91
+ const files: SourceTree = [];
92
+ const contents: Buffer[] = [];
93
+ let total = 0;
94
+ // `realDirectory` is where `directory` really is; `ancestors` holds the real
95
+ // location of every directory on the walk down to it, so a link to one of
96
+ // them is a loop.
97
+ const walk = (directory: string, realDirectory: string, ancestors: Set<string>): void => {
98
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
99
+ const full = join(directory, entry.name);
100
+ let target = join(realDirectory, entry.name);
101
+ if (entry.isSymbolicLink()) {
102
+ target = realpathSync(full);
103
+ if (!isInside(sourceRoot, target)) {
104
+ throw new WireFailure("invalid-input", `${relative(projectRootPath, full)} links outside ${path}. Staging refuses a link that goes outside the source directory.`);
105
+ }
106
+ }
107
+ const stat = statSync(target);
108
+ if (stat.isDirectory()) {
109
+ if (SKIPPED_DIRECTORIES.has(entry.name)) continue;
110
+ if (ancestors.has(target)) {
111
+ throw new WireFailure("invalid-input", `${relative(projectRootPath, full)} points at a directory that contains it. Staging refuses a link loop.`);
112
+ }
113
+ walk(full, target, new Set(ancestors).add(target));
114
+ continue;
115
+ }
116
+ if (!stat.isFile()) continue;
117
+ if (files.length >= MAX_TREE_FILES) throw new WireFailure("limit-exceeded", `${path} has more than ${MAX_TREE_FILES} files.`);
118
+ total += stat.size;
119
+ if (total > MAX_ATTACHMENT_BYTES) throw new WireFailure("limit-exceeded", `${path} holds more than ${MAX_ATTACHMENT_BYTES} bytes.`);
120
+ files.push({ path: relative(sourceRoot, full).split(sep).join("/"), size: stat.size });
121
+ contents.push(readFileSync(target));
122
+ }
123
+ };
124
+ walk(sourceRoot, sourceRoot, new Set([sourceRoot]));
125
+ return { files, contents };
126
+ }