@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.
- package/README.md +3 -1
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-basic/SKILL.md +3 -3
- package/skills/c64-emulator/SKILL.md +2 -2
- package/skills/c64-knowledge/SKILL.md +17 -3
- package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
- package/skills/c64-memory-map/scripts/memmap.ts +1 -1
- package/skills/c64-provenance/SKILL.md +3 -2
- package/skills/c64-provenance/scripts/provenance.ts +4 -2
- package/skills/c64-provenance/scripts/runtime.ts +14 -2
- package/skills/c64-source/SKILL.md +4 -4
- package/skills/c64-source/scripts/generate.ts +57 -14
- package/skills/c64-source/scripts/loader.ts +5 -1
- package/skills/c64-source/scripts/source.ts +3 -1
- package/skills/c64-static-analysis/SKILL.md +2 -2
- package/skills/c64-static-analysis/scripts/analyze.ts +8 -4
- package/skills/c64-unpacker/SKILL.md +5 -5
- package/skills/c64-unpacker/scripts/evidence.ts +29 -22
- package/skills/c64-unpacker/scripts/loads.ts +30 -7
- package/skills/c64-unpacker/scripts/stages.ts +67 -0
- package/skills/c64-unpacker/scripts/unpack.ts +167 -47
- package/src/c64.ts +37 -12
- package/src/cli/main.ts +7 -4
- package/src/cli/status.ts +9 -7
- package/src/host/main.ts +8 -3
- package/src/host/tools/petcat.ts +5 -1
- package/src/host/tools/status.ts +8 -2
- package/src/host/vice/process.ts +10 -5
- package/src/host/vice/session.ts +26 -13
- package/src/host-client/connect.ts +2 -1
- package/src/host-client/coverage.ts +0 -5
- package/src/host-client/vice-session.ts +37 -4
- package/src/knowledge/history.ts +8 -0
- package/src/knowledge/import.ts +34 -3
- package/src/knowledge/write.ts +2 -1
- package/src/mcp/main.ts +43 -15
- package/src/mcp/server.ts +9 -2
- package/src/mcp/tools/execution.ts +3 -2
- package/src/mcp/tools/input.ts +6 -4
- package/src/mcp/tools/media.ts +4 -3
- package/src/native/discover.ts +38 -4
- package/src/package.ts +21 -0
- package/src/protocol/tools.ts +7 -3
- package/src/protocol/vice.ts +14 -6
package/src/knowledge/import.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/src/knowledge/write.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
46
|
-
*
|
|
47
|
-
* host
|
|
48
|
-
*
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
package/src/mcp/tools/input.ts
CHANGED
|
@@ -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);
|
package/src/mcp/tools/media.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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
|
|
package/src/native/discover.ts
CHANGED
|
@@ -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
|
-
/**
|
|
73
|
-
export
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
})();
|
package/src/protocol/tools.ts
CHANGED
|
@@ -61,7 +61,7 @@ export type PetcatResult =
|
|
|
61
61
|
| {
|
|
62
62
|
decoded: true;
|
|
63
63
|
loadAddress: number;
|
|
64
|
-
/** The address
|
|
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
|
-
(
|
|
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) &&
|
package/src/protocol/vice.ts
CHANGED
|
@@ -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
|
-
|
|
425
|
-
|
|
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 =
|
|
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
|
}
|