@henols/c64-re-tools 2.1.0 → 2.1.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/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +7 -3
- package/skills/c64-assembler/scripts/assembler.ts +29 -11
- package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
- package/skills/c64-basic/SKILL.md +1 -0
- package/skills/c64-disk/SKILL.md +3 -2
- package/skills/c64-disk/scripts/disk.ts +26 -12
- package/skills/c64-disk/scripts/lib/containers.ts +10 -11
- package/skills/c64-docs/SKILL.md +2 -1
- package/skills/c64-docs/scripts/docs.ts +1 -1
- package/skills/c64-docs/scripts/lib/links.ts +5 -3
- package/skills/c64-docs/scripts/lib/reference.ts +2 -2
- package/skills/c64-emulator/SKILL.md +9 -2
- package/skills/c64-knowledge/SKILL.md +9 -5
- package/skills/c64-knowledge/scripts/knowledge.ts +91 -53
- package/skills/c64-knowledge/scripts/lib/listing.ts +8 -30
- package/skills/c64-memory-map/SKILL.md +2 -1
- package/skills/c64-memory-map/scripts/memory-map.ts +4 -3
- package/skills/c64-provenance/SKILL.md +6 -4
- package/skills/c64-provenance/scripts/lib/library.ts +18 -6
- package/skills/c64-provenance/scripts/lib/separate.ts +45 -6
- package/skills/c64-provenance/scripts/provenance.ts +14 -10
- package/skills/c64-source/SKILL.md +3 -0
- package/skills/c64-source/scripts/lib/source.ts +9 -31
- package/skills/c64-source/scripts/source.ts +14 -10
- package/skills/c64-static-analysis/SKILL.md +2 -0
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +6 -2
- package/skills/c64-static-analysis/scripts/static-analysis.ts +18 -8
- package/skills/c64-testing/SKILL.md +14 -3
- package/skills/c64-testing/references/recording-format.md +2 -0
- package/skills/c64-testing/references/scenario-format.md +6 -0
- package/skills/c64-testing/scripts/lib/diff-files.ts +76 -14
- package/skills/c64-testing/scripts/lib/diff.ts +3 -3
- package/skills/c64-testing/scripts/lib/recording.ts +50 -15
- package/skills/c64-testing/scripts/lib/replay.ts +12 -1
- package/skills/c64-testing/scripts/lib/runs.ts +84 -4
- package/skills/c64-testing/scripts/lib/scenario.ts +43 -6
- package/skills/c64-testing/scripts/testing.ts +20 -7
- package/skills/c64-unpacker/SKILL.md +17 -10
- package/skills/c64-unpacker/scripts/lib/evidence.ts +4 -5
- package/skills/c64-unpacker/scripts/lib/loads.ts +14 -7
- package/skills/c64-unpacker/scripts/lib/machine.ts +30 -3
- package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
- package/skills/c64-unpacker/scripts/unpacker.ts +93 -42
- package/src/host/vice/adapter.ts +4 -1
- package/src/host/vice/session.ts +148 -13
- package/src/host-client/image.ts +40 -8
- package/src/host-client/steps.ts +145 -9
- package/src/host-client/tools.ts +1 -1
- package/src/host-client/transfer.ts +33 -18
- package/src/knowledge/expression.ts +1 -1
- package/src/knowledge/history.ts +74 -4
- package/src/knowledge/import.ts +79 -7
- package/src/knowledge/names.ts +2 -1
- package/src/knowledge/program.ts +32 -18
- package/src/knowledge/read.ts +46 -0
- package/src/knowledge/schema.ts +4 -2
- package/src/knowledge/text.ts +2 -0
- package/src/knowledge/write.ts +58 -16
- package/src/mcp/inputs.ts +6 -3
- package/src/mcp/output.ts +5 -0
- package/src/mcp/server.ts +20 -4
- package/src/mcp/tools/debug.ts +36 -9
- package/src/mcp/tools/execution.ts +18 -5
- package/src/mcp/tools/machine.ts +3 -1
- package/src/mcp/tools/media.ts +14 -2
- package/src/mcp/tools/memory.ts +11 -6
- package/src/mcp/tools/video.ts +5 -3
- package/src/media.ts +42 -0
- package/src/opcodes.ts +51 -18
- package/src/project.ts +111 -3
- package/src/protocol/vice.ts +8 -2
- package/src/script.ts +55 -3
package/src/knowledge/write.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { type AddressRange, formatC64Address, instructionLength, SPACE_PATTERN,
|
|
|
8
8
|
import { isoCet } from "../time.ts";
|
|
9
9
|
import { CONFLICT_RULES, conflicts, conflictTable, type Conflict, type ConflictRow } from "./conflicts.ts";
|
|
10
10
|
import { transaction } from "./database.ts";
|
|
11
|
+
import { labelProblem } from "../opcodes.ts";
|
|
11
12
|
import { checkExpression, evaluate, identifiers, operandText, replaceIdentifiers } from "./expression.ts";
|
|
12
13
|
import {
|
|
13
14
|
COMMENT_COLUMNS,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
referencesFrom,
|
|
33
34
|
REGION_COLUMNS,
|
|
34
35
|
regionsOverlapping,
|
|
36
|
+
coverageRuns,
|
|
35
37
|
resolveRevision,
|
|
36
38
|
SUPPRESSION_COLUMNS,
|
|
37
39
|
suppressionsOverlapping,
|
|
@@ -49,6 +51,7 @@ import {
|
|
|
49
51
|
toSuppression,
|
|
50
52
|
toSymbol,
|
|
51
53
|
type CommentRow,
|
|
54
|
+
type CoverageRun,
|
|
52
55
|
type MacroRow,
|
|
53
56
|
type MacroUseRow,
|
|
54
57
|
type ModuleRow,
|
|
@@ -59,7 +62,7 @@ import {
|
|
|
59
62
|
type SuppressionRow,
|
|
60
63
|
type SymbolRow,
|
|
61
64
|
} from "./read.ts";
|
|
62
|
-
import { COMMENT_PLACEMENTS, EVIDENCE_KINDS, MAX_ELEMENT, MODULE_FILE, DEFAULT_SPACE, GENERATED_FILES, isGeneratedFile, MACRO_NAME, MACRO_PARAM, SWITCH_NAME, NO_ELEMENT_TYPES, OPERAND_SELECTS, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type EvidenceKind, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
|
|
65
|
+
import { CODE_REGION_TYPES, COMMENT_PLACEMENTS, EVIDENCE_KINDS, MAX_ELEMENT, MODULE_FILE, DEFAULT_SPACE, GENERATED_FILES, isGeneratedFile, MACRO_NAME, MACRO_PARAM, SWITCH_NAME, NO_ELEMENT_TYPES, OPERAND_SELECTS, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type EvidenceKind, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
|
|
63
66
|
import { newRevisionId, rowId, sqlName } from "./text.ts";
|
|
64
67
|
|
|
65
68
|
/** What a change rests on: a coverage run, a test or another project file with its SHA-256, or a note. */
|
|
@@ -136,6 +139,10 @@ export interface ChangeContext {
|
|
|
136
139
|
inputHash?: string;
|
|
137
140
|
toolVersion?: string;
|
|
138
141
|
evidence?: Evidence[];
|
|
142
|
+
/** A revert: the revision whose knowledge it restores. */
|
|
143
|
+
restores?: string;
|
|
144
|
+
/** A revert of one space: the space whose knowledge it restores. Without it, a revert restores every space. */
|
|
145
|
+
restoresSpace?: Space;
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
/**
|
|
@@ -177,8 +184,8 @@ export class Change {
|
|
|
177
184
|
const now = new Date();
|
|
178
185
|
const id = newRevisionId(currentRevision(this.#db), now);
|
|
179
186
|
this.#db
|
|
180
|
-
.prepare("INSERT INTO revisions (id, created_at, origin, operation, input_hash, tool_version, description) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
181
|
-
.run(id, isoCet(now), this.#context.origin, this.#operation, this.#context.inputHash ?? null, this.#context.toolVersion ?? null, this.#context.description ?? null);
|
|
187
|
+
.prepare("INSERT INTO revisions (id, created_at, origin, operation, input_hash, tool_version, description, restored_revision, restored_space) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
188
|
+
.run(id, isoCet(now), this.#context.origin, this.#operation, this.#context.inputHash ?? null, this.#context.toolVersion ?? null, this.#context.description ?? null, this.#context.restores ?? null, this.#context.restoresSpace ?? null);
|
|
182
189
|
this.revision = id;
|
|
183
190
|
const evidence = this.#db.prepare("INSERT INTO evidence (revision, kind, ref, sha256, space) VALUES (?, ?, ?, ?, ?)");
|
|
184
191
|
for (const item of this.#context.evidence ?? []) evidence.run(id, item.kind, item.ref, item.sha256 ?? null, item.space ?? null);
|
|
@@ -321,7 +328,7 @@ function checkEvidence(evidence: Evidence[] | undefined): void {
|
|
|
321
328
|
}
|
|
322
329
|
|
|
323
330
|
/** Runs semantic writes in one transaction, as one revision. */
|
|
324
|
-
export function write<T>(db: DatabaseSync, context: WriteContext, operation: string, work: (change: Change) => T): { revision: string | null; current: T } {
|
|
331
|
+
export function write<T>(db: DatabaseSync, context: WriteContext & Pick<ChangeContext, "restores">, operation: string, work: (change: Change) => T): { revision: string | null; current: T } {
|
|
325
332
|
if (context.origin !== "user" && context.origin !== "llm") {
|
|
326
333
|
throw new Refusal("invalid-input", "A semantic write comes from the user or the LLM.");
|
|
327
334
|
}
|
|
@@ -365,9 +372,8 @@ export function checkSpace(request: InSpace): void {
|
|
|
365
372
|
function checkSymbol(request: SymbolRequest): void {
|
|
366
373
|
checkSpace(request);
|
|
367
374
|
checkAddress(request.address);
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
375
|
+
const problem = labelProblem(request.name);
|
|
376
|
+
if (problem !== undefined) throw new Refusal("invalid-input", problem);
|
|
371
377
|
if (request.kind !== undefined) checkOneOf(SYMBOL_KINDS, request.kind, "kind");
|
|
372
378
|
}
|
|
373
379
|
|
|
@@ -572,6 +578,32 @@ function checkOperand(request: OperandRequest): void {
|
|
|
572
578
|
|
|
573
579
|
const shown = (value: number, size: number) => (value < 0 ? String(value) : `$${value.toString(16).padStart(size === 0x100 ? 2 : 4, "0")}`);
|
|
574
580
|
|
|
581
|
+
/**
|
|
582
|
+
* Refuses an address that is not the start of an instruction. The code
|
|
583
|
+
* regions of the knowledge give the instructions: they are decoded from the
|
|
584
|
+
* start of the region, as the listing and the source decode them.
|
|
585
|
+
*/
|
|
586
|
+
function checkInstructionStart(db: DatabaseSync, space: Space, image: OperandImage, address: number): void {
|
|
587
|
+
const where = formatC64Address(address);
|
|
588
|
+
const code = regionsOverlapping(db, address, address, space).find((region) => CODE_REGION_TYPES.includes(region.type));
|
|
589
|
+
if (code === undefined) {
|
|
590
|
+
throw new Refusal("invalid-input", `${where} is in no code region, so the knowledge does not know an instruction that starts there. Classify the code that holds the instruction first (classify <range> code), then write the operand.`);
|
|
591
|
+
}
|
|
592
|
+
let start = code.start;
|
|
593
|
+
let next = code.start;
|
|
594
|
+
while (next < address) {
|
|
595
|
+
const opcode = image.byteAt(next);
|
|
596
|
+
if (opcode === undefined) {
|
|
597
|
+
throw new Refusal("invalid-input", `The code region ${formatC64Address(code.start)}-${formatC64Address(code.end)} has no program byte at ${formatC64Address(next)}, so its instructions up to ${where} are not known. Correct the region (classify), then write the operand.`);
|
|
598
|
+
}
|
|
599
|
+
start = next;
|
|
600
|
+
next += instructionLength(opcode);
|
|
601
|
+
}
|
|
602
|
+
if (next !== address) {
|
|
603
|
+
throw new Refusal("invalid-input", `${where} is not the start of an instruction. It is an operand byte of the instruction at ${formatC64Address(start)}, decoded from the start of the code region at ${formatC64Address(code.start)}. Give ${formatC64Address(start)}.`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
575
607
|
/**
|
|
576
608
|
* Refuses an expression whose value is not the operand in the image, and
|
|
577
609
|
* returns the addresses of its names. Symbols of the knowledge come first, then
|
|
@@ -581,6 +613,7 @@ function checkOperandValue(db: DatabaseSync, space: Space, image: OperandImage,
|
|
|
581
613
|
const where = formatC64Address(request.address);
|
|
582
614
|
const opcode = image.byteAt(request.address);
|
|
583
615
|
if (opcode === undefined) throw new Refusal("invalid-input", `${where} is outside the image.`);
|
|
616
|
+
checkInstructionStart(db, space, image, request.address);
|
|
584
617
|
const length = instructionLength(opcode);
|
|
585
618
|
if (length === 1) throw new Refusal("invalid-input", `The instruction at ${where} (opcode ${shown(opcode, 0x100)}) has no operand.`);
|
|
586
619
|
if ((opcode & 0x1f) === 0x10) throw new Refusal("invalid-input", `The instruction at ${where} is a branch: its operand is a distance, not a value.`);
|
|
@@ -1168,16 +1201,25 @@ const KEYS = {
|
|
|
1168
1201
|
|
|
1169
1202
|
/**
|
|
1170
1203
|
* Makes the current knowledge equal to what it was at `revision`, as a new
|
|
1171
|
-
* revision.
|
|
1204
|
+
* revision. With `space`, only the rows of that space change; without it,
|
|
1205
|
+
* every space changes. History is never erased; restored rows keep their origin.
|
|
1172
1206
|
*/
|
|
1173
|
-
export function revert(db: DatabaseSync, context: WriteContext, request: { revision: string }): WriteResult<{ restoredRevision: string }> {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1207
|
+
export function revert(db: DatabaseSync, context: WriteContext, request: { revision: string; space?: Space }): WriteResult<{ restoredRevision: string }> {
|
|
1208
|
+
checkSpace(request);
|
|
1209
|
+
const restored = resolveRevision(db, request.revision);
|
|
1210
|
+
const only = request.space;
|
|
1211
|
+
return write(db, { ...context, restores: restored, ...(only === undefined ? {} : { restoresSpace: only }) }, "revert", (change) => {
|
|
1212
|
+
// The coverage maps that the knowledge has follow the revert, also when no coverage range changes.
|
|
1213
|
+
const maps = (runs: CoverageRun[]) => JSON.stringify(runs.filter((run) => only === undefined || run.space === only).map((run) => [run.name, run.sha256, run.space, run.revision]));
|
|
1214
|
+
if (maps(coverageRuns(db, restored)) !== maps(coverageRuns(db))) change.id();
|
|
1215
|
+
const inSpace = only === undefined ? "" : " AND space = ?";
|
|
1216
|
+
const spaceArgs = only === undefined ? [] : [only];
|
|
1217
|
+
const at = `valid_from_revision <= ? AND (valid_to_revision IS NULL OR valid_to_revision > ?)${inSpace}`;
|
|
1218
|
+
const current = `valid_to_revision IS NULL${inSpace}`;
|
|
1177
1219
|
for (const [table, spec] of Object.entries(KEYS)) {
|
|
1178
1220
|
const name = table === "references" ? '"references"' : table;
|
|
1179
|
-
const then = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${at}`).all(restored, restored) as Array<Record<string, unknown>>;
|
|
1180
|
-
const now = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE
|
|
1221
|
+
const then = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${at}`).all(restored, restored, ...spaceArgs) as Array<Record<string, unknown>>;
|
|
1222
|
+
const now = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${current}`).all(...spaceArgs) as Array<Record<string, unknown>>;
|
|
1181
1223
|
const key = (row: Record<string, unknown>) => (spec.key as (row: unknown) => string)(spec.map(row as never));
|
|
1182
1224
|
const thenKeys = new Set(then.map(key));
|
|
1183
1225
|
const nowKeys = new Set(now.map(key));
|
|
@@ -1219,8 +1261,8 @@ export function revert(db: DatabaseSync, context: WriteContext, request: { revis
|
|
|
1219
1261
|
}
|
|
1220
1262
|
// The program: a row is the same when all its values are the same.
|
|
1221
1263
|
for (const [table, columns] of PROGRAM_TABLES) {
|
|
1222
|
-
const then = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE ${at}`).all(restored, restored) as Array<Record<string, unknown>>;
|
|
1223
|
-
const now = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE
|
|
1264
|
+
const then = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE ${at}`).all(restored, restored, ...spaceArgs) as Array<Record<string, unknown>>;
|
|
1265
|
+
const now = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE ${current}`).all(...spaceArgs) as Array<Record<string, unknown>>;
|
|
1224
1266
|
const key = (row: Record<string, unknown>) => JSON.stringify(columns.split(", ").map((column) => row[column]));
|
|
1225
1267
|
const thenKeys = new Set(then.map(key));
|
|
1226
1268
|
const nowKeys = new Set(now.map(key));
|
package/src/mcp/inputs.ts
CHANGED
|
@@ -33,12 +33,15 @@ export const AddressInput = z
|
|
|
33
33
|
.transform((value) => c64AddressOf(value)!)
|
|
34
34
|
.describe("C64 address: $ and 1 to 4 hex digits, $0000 to $ffff");
|
|
35
35
|
|
|
36
|
+
/** The space of a memory target or condition: the emulator tests only C64 memory. */
|
|
37
|
+
const MemorySpaceInput = SpaceInput.describe("c64 only: the emulator tests only C64 memory. For the drive, use a watchpoint or an address target in space drive8");
|
|
38
|
+
|
|
36
39
|
const rasterLine = z.number().int().min(0).max(MAX_RASTER_LINE).describe("raster line: PAL 0-311, NTSC 0-262");
|
|
37
40
|
|
|
38
41
|
const condition = z
|
|
39
42
|
.discriminatedUnion("kind", [
|
|
40
43
|
z.object({ kind: z.literal("register"), register: z.enum(CONDITION_REGISTERS), operator: z.enum(COMPARISONS), value: Byte }).strict(),
|
|
41
|
-
z.object({ kind: z.literal("memory"), address: AddressInput, operator: z.enum(COMPARISONS), value: Byte, space:
|
|
44
|
+
z.object({ kind: z.literal("memory"), address: AddressInput, operator: z.enum(COMPARISONS), value: Byte, space: MemorySpaceInput, view: MemoryViewInput }).strict(),
|
|
42
45
|
z.object({ kind: z.literal("raster"), line: rasterLine, cycle: z.number().int().min(0).max(64).optional().describe("cycle in the line: PAL 0-62, NTSC 0-64. True from this cycle on") }).strict(),
|
|
43
46
|
])
|
|
44
47
|
.describe("register: a CPU register compared to a byte. memory: a byte in memory compared to a byte. raster: the raster position");
|
|
@@ -50,7 +53,7 @@ export const TOOL_INPUTS = {
|
|
|
50
53
|
|
|
51
54
|
runTarget: z.discriminatedUnion("kind", [
|
|
52
55
|
z.object({ kind: z.literal("address"), address: AddressInput, space: SpaceInput, condition: condition.optional().describe("stop at the address only when this is true") }).strict(),
|
|
53
|
-
z.object({ kind: z.literal("memory"), address: AddressInput, operator: z.enum(COMPARISONS), value: Byte, space:
|
|
56
|
+
z.object({ kind: z.literal("memory"), address: AddressInput, operator: z.enum(COMPARISONS), value: Byte, space: MemorySpaceInput, view: MemoryViewInput }).strict(),
|
|
54
57
|
z.object({ kind: z.literal("raster"), line: rasterLine, cycle: z.number().int().min(0).max(64).optional().describe("cycle in the line: PAL 0-62, NTSC 0-64. Default 0") }).strict(),
|
|
55
58
|
]),
|
|
56
59
|
|
|
@@ -82,7 +85,7 @@ export const TOOL_INPUTS = {
|
|
|
82
85
|
|
|
83
86
|
joystick: z
|
|
84
87
|
.object({
|
|
85
|
-
port: z.union([z.literal(1), z.literal(2)]).describe("control port 1 or 2"),
|
|
88
|
+
port: z.union([z.literal(1), z.literal(2)], { error: "port must be the number 1 or 2" }).describe("control port 1 or 2"),
|
|
86
89
|
direction: z.enum(JOYSTICK_DIRECTIONS),
|
|
87
90
|
fire: z.boolean().default(false),
|
|
88
91
|
})
|
package/src/mcp/output.ts
CHANGED
|
@@ -22,6 +22,11 @@ export const WrittenOutput = {
|
|
|
22
22
|
sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
/** Lines as file text: each line ends with a newline, and no lines give an empty file. */
|
|
26
|
+
export function linesText(lines: readonly string[]): string {
|
|
27
|
+
return lines.map((line) => `${line}\n`).join("");
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
/** Writes `data` to the file `path` of the project that `project` names (or the project lookup finds), and gives what the result shows in its place. */
|
|
26
31
|
export function writeOut(path: string, data: Uint8Array | string, project: string | undefined): Written {
|
|
27
32
|
return writeReported(path, data, { what: "out", root: projectFolder(project) });
|
package/src/mcp/server.ts
CHANGED
|
@@ -91,6 +91,17 @@ export function requireFields(action: string, given: Record<string, unknown>, al
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Checks the fields that go with out: project only with out, and the
|
|
96
|
+
* fields of `notWithOut` only without it.
|
|
97
|
+
*/
|
|
98
|
+
export function requireOut(given: { out?: unknown; project?: unknown } & Record<string, unknown>, notWithOut: readonly string[] = []): void {
|
|
99
|
+
if (given.out === undefined && given.project !== undefined) throw new WireFailure("invalid-input", "project is used only with out.");
|
|
100
|
+
for (const field of notWithOut) {
|
|
101
|
+
if (given.out !== undefined && given[field] !== undefined) throw new WireFailure("invalid-input", `${field} is not used with out.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
94
105
|
/** A tool result that carries content blocks (for example an image) beside its structured result. */
|
|
95
106
|
export class ToolOutput<T> {
|
|
96
107
|
readonly structured: T;
|
|
@@ -163,12 +174,17 @@ function errorResult(error: WireError): CallToolResult {
|
|
|
163
174
|
return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: { code: error.code, message: shownMessage(error) } }) }] };
|
|
164
175
|
}
|
|
165
176
|
|
|
166
|
-
/**
|
|
167
|
-
|
|
177
|
+
/**
|
|
178
|
+
* "address: Invalid string: must match ..." style messages, one sentence per problem.
|
|
179
|
+
* An unknown field of the tool input also gets the fields that the tool takes.
|
|
180
|
+
*/
|
|
181
|
+
function describeIssues(error: z.ZodError, input?: z.ZodObject): string {
|
|
168
182
|
return error.issues
|
|
169
183
|
.map((issue) => {
|
|
170
184
|
const where = issue.path.length === 0 ? "input" : issue.path.join(".");
|
|
171
|
-
|
|
185
|
+
const names = input === undefined ? [] : Object.keys(input.shape);
|
|
186
|
+
const fields = issue.code !== "unrecognized_keys" || issue.path.length !== 0 || input === undefined ? "" : names.length === 0 ? ". The tool takes no fields" : `. The fields are ${names.join(", ")}`;
|
|
187
|
+
return `${where}: ${issue.message}${fields}`;
|
|
172
188
|
})
|
|
173
189
|
.join(". ");
|
|
174
190
|
}
|
|
@@ -211,7 +227,7 @@ async function runCall(
|
|
|
211
227
|
const tool = tools.get(name);
|
|
212
228
|
if (tool === undefined) return errorResult({ code: "invalid-input", message: `There is no tool named ${name}.` });
|
|
213
229
|
const parsed = tool.inputSchema.safeParse(args ?? {});
|
|
214
|
-
if (!parsed.success) return errorResult({ code: "invalid-input", message: describeIssues(parsed.error) });
|
|
230
|
+
if (!parsed.success) return errorResult({ code: "invalid-input", message: describeIssues(parsed.error, tool.inputSchema) });
|
|
215
231
|
try {
|
|
216
232
|
let output: unknown;
|
|
217
233
|
try {
|
package/src/mcp/tools/debug.ts
CHANGED
|
@@ -7,9 +7,9 @@ import { COVERAGE, executedRamCode, EXECUTE, formatC64Address, READ, WRITE, type
|
|
|
7
7
|
import { WireFailure } from "../../protocol/messages.ts";
|
|
8
8
|
import { CHECKPOINT_ACTIONS, MAX_BACKTRACE, MAX_HISTORY, MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, TRACE_ENDS, MAX_MEMMAP_RANGES, MAX_MEMORY_READ, MAX_PROFILE, MAX_WATCH_SIZE, SPACES, TIMING_ACTIONS, WATCH_ACCESS, type Breakpoint, type Watchpoint } from "../../protocol/vice.ts";
|
|
9
9
|
import { readCoverage } from "../../host-client/coverage.ts";
|
|
10
|
-
import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
|
|
10
|
+
import { linesText, OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
|
|
11
11
|
import { AddressInput, Byte, SpaceInput, TOOL_INPUTS } from "../inputs.ts";
|
|
12
|
-
import { AddressOutput, ConditionOutput, defineTool, HexData, requireFields, showCondition, toCondition } from "../server.ts";
|
|
12
|
+
import { AddressOutput, ConditionOutput, defineTool, HexData, requireFields, requireOut, showCondition, toCondition } from "../server.ts";
|
|
13
13
|
import { spaced } from "./memory.ts";
|
|
14
14
|
|
|
15
15
|
/** No default here: a filled-in default would count as a field the other actions refuse. */
|
|
@@ -169,7 +169,7 @@ export const c64CpuHistory = defineTool({
|
|
|
169
169
|
const raster = entry.rasterLine === undefined ? "" : ` line ${entry.rasterLine} cycle ${entry.rasterCycle ?? 0}`;
|
|
170
170
|
return `${formatC64Address(entry.address)} ${spaced(entry.bytes).padEnd(9)} ${entry.text.padEnd(16)} a=${byte(entry.a)} x=${byte(entry.x)} y=${byte(entry.y)} sp=${byte(entry.sp)}${raster}`;
|
|
171
171
|
});
|
|
172
|
-
return { count: entries.length, ...writeOut(out,
|
|
172
|
+
return { count: entries.length, ...writeOut(out, linesText(lines), project) };
|
|
173
173
|
},
|
|
174
174
|
});
|
|
175
175
|
|
|
@@ -184,6 +184,7 @@ export const c64Trace = defineTool({
|
|
|
184
184
|
"A point is exec (an instruction at an address from start to end), read or write (an access from start to end). " +
|
|
185
185
|
`Give 1 to ${MAX_TRACE_POINTS} points. ` +
|
|
186
186
|
"Each hit gives the instruction (pc, bytes, text) and the registers a, x, y and sp before it. " +
|
|
187
|
+
"point in a hit is the number of its point in points, 0 for the first. " +
|
|
187
188
|
"It also gives the frame from 0, and the raster line and cycle where the instruction started. " +
|
|
188
189
|
"A read or write hit also gives the address that the instruction used, and the byte there after the access. " +
|
|
189
190
|
`The trace ends after frames frames (1 to ${MAX_TRACE_FRAMES}), at maxHits hits (at most ${MAX_TRACE_HITS}), or at a breakpoint, a watchpoint or a JAM. ` +
|
|
@@ -233,7 +234,7 @@ export const c64Trace = defineTool({
|
|
|
233
234
|
const shown = traced.hits.map(({ address, ...hit }) => ({ ...hit, pc: formatC64Address(hit.pc), ...(address === undefined ? {} : { address: formatC64Address(address) }) }));
|
|
234
235
|
const summary = { end: traced.end, frames: traced.frames, pc: formatC64Address(traced.pc), count: traced.hits.length, hits: shown.slice(0, TRACE_LISTED) };
|
|
235
236
|
if (out === undefined) return summary;
|
|
236
|
-
return { ...summary, ...writeOut(out,
|
|
237
|
+
return { ...summary, ...writeOut(out, linesText(shown.map((hit) => JSON.stringify(hit))), project) };
|
|
237
238
|
},
|
|
238
239
|
});
|
|
239
240
|
|
|
@@ -338,7 +339,9 @@ export const c64Memmap = defineTool({
|
|
|
338
339
|
"A read on a byte right after executed code, or at a return address, can be a dummy read that the CPU does not use. " +
|
|
339
340
|
"Action summary gives the counts for RAM, ROM and I/O. It also gives the executed code in RAM with its operand bytes, and the RAM that was written and executed. " +
|
|
340
341
|
`Each list has at most ${MAX_SUMMARY_RANGES} ranges. ` +
|
|
341
|
-
"Action clear sets all access records to zero."
|
|
342
|
+
"Action clear sets all access records to zero. " +
|
|
343
|
+
"The emulator decides RAM, ROM or I/O from one bit of $01. At $A000-$BFFF it gives ROM when bit 0 is set, also when bit 1 is clear and the CPU used RAM. " +
|
|
344
|
+
"Every access at $D000-$DFFF is I/O, also when $01 shows RAM or the character ROM there. emulatorFlaw in the result tells when ROM at $A000-$BFFF can be RAM.",
|
|
342
345
|
inputSchema: z
|
|
343
346
|
.object({
|
|
344
347
|
action: z.enum(["read", "summary", "clear"]),
|
|
@@ -361,6 +364,7 @@ export const c64Memmap = defineTool({
|
|
|
361
364
|
writtenAndExecuted: z.array(RangeOutput).optional(),
|
|
362
365
|
writtenAndExecutedTruncated: z.boolean().optional(),
|
|
363
366
|
cleared: z.boolean().optional(),
|
|
367
|
+
emulatorFlaw: z.string().optional().describe("a known flaw of VICE that can make the ROM and I/O classes wrong in this result"),
|
|
364
368
|
...WrittenOutput,
|
|
365
369
|
}),
|
|
366
370
|
readOnly: false,
|
|
@@ -390,13 +394,15 @@ export const c64Memmap = defineTool({
|
|
|
390
394
|
...(code.length > MAX_SUMMARY_RANGES ? { codeTruncated: true } : {}),
|
|
391
395
|
writtenAndExecuted: showRanges(written),
|
|
392
396
|
...(written.length > MAX_SUMMARY_RANGES ? { writtenAndExecutedTruncated: true } : {}),
|
|
397
|
+
...basicRomFlaw(romInBasicArea(map, start)),
|
|
393
398
|
};
|
|
394
399
|
}
|
|
400
|
+
requireOut(fields, ["maxRanges"]);
|
|
401
|
+
requireFields("read", fields, ["start", "end", "maxRanges", "out", "project"]);
|
|
395
402
|
if (fields.out !== undefined) {
|
|
396
|
-
|
|
397
|
-
return writeOut(fields.out,
|
|
403
|
+
const map = await readCoverage(session, start, end);
|
|
404
|
+
return { ...writeOut(fields.out, map, fields.project), ...basicRomFlaw(romInBasicArea(map, start)) };
|
|
398
405
|
}
|
|
399
|
-
requireFields("read", fields, ["start", "end", "maxRanges"]);
|
|
400
406
|
const result = await session.memmap({ action: "read", start, end, maxRanges: fields.maxRanges ?? 256 });
|
|
401
407
|
const ranges = result.ranges.map((range) => ({
|
|
402
408
|
start: formatC64Address(range.start),
|
|
@@ -406,8 +412,29 @@ export const c64Memmap = defineTool({
|
|
|
406
412
|
write: (range.flags & WRITE) !== 0,
|
|
407
413
|
in: sources(range.flags),
|
|
408
414
|
}));
|
|
409
|
-
|
|
415
|
+
const flaw = basicRomFlaw(result.ranges.some((range) => range.start <= 0xbfff && range.end >= 0xa000 && (range.flags & ROM_ACCESS) !== 0));
|
|
416
|
+
return result.next === undefined ? { ranges, ...flaw } : { ranges, truncated: true, next: formatC64Address(result.next), ...flaw };
|
|
410
417
|
},
|
|
411
418
|
});
|
|
412
419
|
|
|
420
|
+
const ROM_ACCESS = COVERAGE.romRead | COVERAGE.romExecute;
|
|
421
|
+
|
|
422
|
+
/** True when the map from `start` has a ROM access at $A000-$BFFF. */
|
|
423
|
+
function romInBasicArea(map: Uint8Array, start: number): boolean {
|
|
424
|
+
for (let address = Math.max(start, 0xa000); address <= Math.min(start + map.length - 1, 0xbfff); address++) {
|
|
425
|
+
if ((map[address - start]! & ROM_ACCESS) !== 0) return true;
|
|
426
|
+
}
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** VICE tests only bit 0 of $01 for $A000-$BFFF; the C64 shows BASIC ROM there only when bits 0 and 1 are set. */
|
|
431
|
+
function basicRomFlaw(present: boolean): { emulatorFlaw?: string } {
|
|
432
|
+
if (!present) return {};
|
|
433
|
+
return {
|
|
434
|
+
emulatorFlaw:
|
|
435
|
+
"The emulator gives ROM for an access at $A000-$BFFF when bit 0 of $01 is set, also when bit 1 is clear and the CPU used the RAM there. " +
|
|
436
|
+
"Read $01 of the program at that time before you take rom at $A000-$BFFF as the BASIC ROM.",
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
413
440
|
export const debugTools = [c64Breakpoint, c64Watchpoint, c64CpuHistory, c64Trace, c64Backtrace, c64Profile, c64Memmap, c64Timing];
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
import { formatC64Address } from "../../c64.ts";
|
|
6
|
+
import { isRefusal } from "../../errors.ts";
|
|
6
7
|
import { EXECUTION_ACTIONS, MAX_EXECUTION_COUNT, MAX_HISTORY, MAX_TIMEOUT_FRAMES, POINT_STOP_REASONS, RUN_STATES, SPACES, STOP_REASONS, type RunTarget } from "../../protocol/vice.ts";
|
|
7
8
|
import { Byte, SpaceInput, TOOL_INPUTS } from "../inputs.ts";
|
|
8
|
-
import { AddressOutput, defineTool, toCondition, ToolOutput } from "../server.ts";
|
|
9
|
+
import { AddressOutput, defineTool, toCondition, ToolOutput, type ViceSessionApi } from "../server.ts";
|
|
9
10
|
import { c64Observe } from "./video.ts";
|
|
10
11
|
|
|
11
12
|
export const c64Execution = defineTool({
|
|
@@ -19,6 +20,8 @@ export const c64Execution = defineTool({
|
|
|
19
20
|
"advance-frames runs exactly count video frames (count is necessary). It stops at the same raster position, count frames later. " +
|
|
20
21
|
"A breakpoint, a watchpoint or a JAM opcode can stop it before that. advancedFrames tells how many frames ran. " +
|
|
21
22
|
"stopReason tells why it stopped: frames (all frames ran), breakpoint, watchpoint or jam. pointId gives the id of the breakpoint or watchpoint. " +
|
|
23
|
+
"space tells which CPU stopped: c64, or drive8 for a breakpoint or watchpoint in the disk drive. pc is the address in that space. " +
|
|
24
|
+
"A CPU that a JAM stopped runs again only after c64_reset, a snapshot restore or c64_autostart. " +
|
|
22
25
|
"To wait for a breakpoint or watchpoint after resume, use advance-frames with a large count. " +
|
|
23
26
|
"step, next, until-return and advance-frames first stop a running CPU. Use count only with step, next and advance-frames. " +
|
|
24
27
|
"step, next and until-return work only in space c64. For the drive, use c64_run_until with an address in space drive8.",
|
|
@@ -42,6 +45,7 @@ export const c64Execution = defineTool({
|
|
|
42
45
|
advancedFrames: z.number().int().min(0).max(MAX_EXECUTION_COUNT).optional().describe("frames advance-frames ran"),
|
|
43
46
|
stopReason: z.enum(["frames", ...POINT_STOP_REASONS]).optional().describe("advance-frames: why it stopped"),
|
|
44
47
|
pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped advance-frames"),
|
|
48
|
+
space: z.enum(SPACES).optional().describe("advance-frames: the CPU that stopped, c64 or drive8. pc is the address in that space"),
|
|
45
49
|
}),
|
|
46
50
|
readOnly: false,
|
|
47
51
|
async run(input, session) {
|
|
@@ -54,6 +58,7 @@ export const c64Execution = defineTool({
|
|
|
54
58
|
...(result.advancedFrames === undefined ? {} : { advancedFrames: result.advancedFrames }),
|
|
55
59
|
...(result.stopReason === undefined ? {} : { stopReason: result.stopReason }),
|
|
56
60
|
...(result.pointId === undefined ? {} : { pointId: result.pointId }),
|
|
61
|
+
...(result.space === undefined ? {} : { space: result.space }),
|
|
57
62
|
};
|
|
58
63
|
},
|
|
59
64
|
});
|
|
@@ -93,6 +98,7 @@ export const c64RunUntil = defineTool({
|
|
|
93
98
|
history: z
|
|
94
99
|
.array(z.object({ address: AddressOutput, bytes: z.string(), text: z.string(), a: Byte, x: Byte, y: Byte, sp: Byte, rasterLine: z.number().int().min(0).optional(), rasterCycle: z.number().int().min(0).optional() }))
|
|
95
100
|
.optional(),
|
|
101
|
+
historyMissing: z.string().optional().describe("why the result has no history, for example an old history after a JAM"),
|
|
96
102
|
}),
|
|
97
103
|
readOnly: false,
|
|
98
104
|
async run(input, session) {
|
|
@@ -108,10 +114,7 @@ export const c64RunUntil = defineTool({
|
|
|
108
114
|
}
|
|
109
115
|
const result = await session.runUntil({ target, timeoutFrames: input.timeoutFrames });
|
|
110
116
|
const answer = { ...result, pc: formatC64Address(result.pc) };
|
|
111
|
-
const history =
|
|
112
|
-
input.history === undefined
|
|
113
|
-
? {}
|
|
114
|
-
: { history: (await session.cpuHistory({ limit: input.history, space: result.space })).entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
|
|
117
|
+
const history = input.history === undefined ? {} : await runHistory(session, input.history, result.space);
|
|
115
118
|
if (input.observe === undefined) return { ...answer, ...history };
|
|
116
119
|
// The observation at the stop: an image of the screen comes with the result as c64_observe gives it.
|
|
117
120
|
const observed = await c64Observe.run(input.observe, session);
|
|
@@ -120,4 +123,14 @@ export const c64RunUntil = defineTool({
|
|
|
120
123
|
},
|
|
121
124
|
});
|
|
122
125
|
|
|
126
|
+
/** The history at the stop; the run result stays when the session refuses the history. */
|
|
127
|
+
async function runHistory(session: ViceSessionApi, limit: number, space: "c64" | "drive8") {
|
|
128
|
+
try {
|
|
129
|
+
return { history: (await session.cpuHistory({ limit, space })).entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (isRefusal(error) && error.code === "operation-failed") return { historyMissing: error.message };
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
export const executionTools = [c64Execution, c64RunUntil];
|
package/src/mcp/tools/machine.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { formatC64Address } from "../../c64.ts";
|
|
6
6
|
import { PACKAGE } from "../../package.ts";
|
|
7
7
|
import { VIDEO_STANDARDS } from "../../protocol/messages.ts";
|
|
8
|
-
import { POINT_STOP_REASONS, RESET_MODES, RUN_STATES, WINDOW_ACTIONS } from "../../protocol/vice.ts";
|
|
8
|
+
import { POINT_STOP_REASONS, RESET_MODES, RUN_STATES, SPACES, WINDOW_ACTIONS } from "../../protocol/vice.ts";
|
|
9
9
|
import { AddressOutput, defineTool } from "../server.ts";
|
|
10
10
|
|
|
11
11
|
export const c64Status = defineTool({
|
|
@@ -27,6 +27,7 @@ export const c64Status = defineTool({
|
|
|
27
27
|
pc: AddressOutput.optional().describe("program counter. Only when the CPU is stopped"),
|
|
28
28
|
stopReason: z.enum(POINT_STOP_REASONS).optional().describe("what stopped the CPU. Only when a breakpoint, a watchpoint or a JAM opcode stopped it"),
|
|
29
29
|
pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped the CPU"),
|
|
30
|
+
space: z.enum(SPACES).optional().describe("with pc: c64, or drive8 when a breakpoint or watchpoint in the disk drive stopped the CPU. pc is the address in that space"),
|
|
30
31
|
cartridge: z.boolean().describe("true while a cartridge from c64_autostart is attached"),
|
|
31
32
|
version: z.string().describe("the c64-re-tools version of this MCP server"),
|
|
32
33
|
}),
|
|
@@ -43,6 +44,7 @@ export const c64Status = defineTool({
|
|
|
43
44
|
...(status.pc === undefined ? {} : { pc: formatC64Address(status.pc) }),
|
|
44
45
|
...(status.stopReason === undefined ? {} : { stopReason: status.stopReason }),
|
|
45
46
|
...(status.pointId === undefined ? {} : { pointId: status.pointId }),
|
|
47
|
+
...(status.space === undefined ? {} : { space: status.space }),
|
|
46
48
|
};
|
|
47
49
|
},
|
|
48
50
|
});
|
package/src/mcp/tools/media.ts
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
// c64_snapshot. Paths are relative to the project folder; the host-client
|
|
3
3
|
// reads the file and the Host Runtime receives only its bytes.
|
|
4
4
|
|
|
5
|
+
import { extname } from "node:path";
|
|
6
|
+
|
|
5
7
|
import { z } from "zod";
|
|
6
8
|
|
|
7
9
|
import { formatC64Address } from "../../c64.ts";
|
|
8
10
|
import { projectFolder, readProjectFile } from "../../host-client/transfer.ts";
|
|
11
|
+
import { WireFailure } from "../../protocol/messages.ts";
|
|
9
12
|
import { AUTOSTART_TYPES, DISK_TYPES, MAX_SNAPSHOTS, RUN_STATES } from "../../protocol/vice.ts";
|
|
10
13
|
import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
|
|
11
14
|
import { AddressInput } from "../inputs.ts";
|
|
12
|
-
import { AddressOutput, defineTool, requireFields, TransientName } from "../server.ts";
|
|
15
|
+
import { AddressOutput, defineTool, requireFields, requireOut, TransientName } from "../server.ts";
|
|
13
16
|
|
|
14
17
|
const ProjectPath = z.string().min(1).describe("path relative to the project folder, for example build/game.prg");
|
|
15
18
|
|
|
@@ -63,6 +66,10 @@ export const c64ProgramLoad = defineTool({
|
|
|
63
66
|
outputSchema: z.object({ state: z.enum(RUN_STATES), loadAddress: AddressOutput, size: z.number().int().min(1).max(0x10000) }),
|
|
64
67
|
readOnly: false,
|
|
65
68
|
async run(input, session) {
|
|
69
|
+
const type = extname(input.path).slice(1).toLowerCase();
|
|
70
|
+
if (type !== "prg" && (AUTOSTART_TYPES as readonly string[]).includes(type)) {
|
|
71
|
+
throw new WireFailure("invalid-input", `${input.path} is a .${type} image, not a PRG. Use c64_autostart, or c64_disk_attach for a disk image.`);
|
|
72
|
+
}
|
|
66
73
|
const root = projectFolder(input.project);
|
|
67
74
|
const result = await session.programLoad(input.address === undefined ? { path: input.path, root } : { path: input.path, address: input.address, root });
|
|
68
75
|
return { state: result.state, loadAddress: formatC64Address(result.loadAddress), size: result.size };
|
|
@@ -118,12 +125,17 @@ export const c64Snapshot = defineTool({
|
|
|
118
125
|
requireFields(action, fields, []);
|
|
119
126
|
return session.snapshot({ action });
|
|
120
127
|
}
|
|
128
|
+
if (action === "restore") {
|
|
129
|
+
if (fields.name !== undefined && fields.file !== undefined) throw new WireFailure("invalid-input", "Action restore takes name or file, not both.");
|
|
130
|
+
if (fields.name === undefined && fields.file === undefined) throw new WireFailure("invalid-input", "Action restore needs name (a snapshot of this session) or file (a .vsf in the project).");
|
|
131
|
+
}
|
|
121
132
|
if (action === "restore" && fields.file !== undefined) {
|
|
122
133
|
requireFields(action, fields, ["file", "project"], ["file"]);
|
|
123
134
|
return session.snapshotRestore(readProjectFile(fields.file, { root: projectFolder(fields.project) }).bytes);
|
|
124
135
|
}
|
|
125
136
|
if (action === "save") {
|
|
126
|
-
|
|
137
|
+
requireOut(fields);
|
|
138
|
+
requireFields(action, fields, ["name", "out", "project"], ["name"]);
|
|
127
139
|
const saved = await session.snapshot({ action, name: fields.name! });
|
|
128
140
|
return fields.out === undefined ? saved : { ...saved, ...writeOut(fields.out, await session.snapshotExport(fields.name!), fields.project) };
|
|
129
141
|
}
|
package/src/mcp/tools/memory.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
import { formatC64Address } from "../../c64.ts";
|
|
7
7
|
import { WireFailure } from "../../protocol/messages.ts";
|
|
8
8
|
import { MAX_COMPARE_DIFFERENCES, MAX_COMPARE_SIZE, MAX_DISASSEMBLE, MAX_MEMORY_READ, MAX_MEMORY_WRITE, MAX_SEARCH_PATTERN, MAX_SEARCH_RESULTS, type RegisterValues } from "../../protocol/vice.ts";
|
|
9
|
-
import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
|
|
9
|
+
import { linesText, OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
|
|
10
10
|
import { AddressInput, Byte, MemoryViewInput, SpaceInput, TOOL_INPUTS } from "../inputs.ts";
|
|
11
11
|
import { AddressOutput, defineTool, HexData, requireFields } from "../server.ts";
|
|
12
12
|
|
|
@@ -123,7 +123,7 @@ export const c64MemorySearch = defineTool({
|
|
|
123
123
|
title: "Search C64 memory",
|
|
124
124
|
description:
|
|
125
125
|
`Find a byte pattern of 1 to ${MAX_SEARCH_PATTERN} bytes in memory from start to end. ?? matches any byte. ` +
|
|
126
|
-
`The result gives the start address of each match, at most maxResults (1 to ${MAX_SEARCH_RESULTS}, default 100). ` +
|
|
126
|
+
`The result gives the start address of each match, at most maxResults (1 to ${MAX_SEARCH_RESULTS}, default 100). truncated: true tells that more matches can follow. ` +
|
|
127
127
|
"This does not change the run state of the machine.",
|
|
128
128
|
inputSchema: z
|
|
129
129
|
.object({
|
|
@@ -135,15 +135,20 @@ export const c64MemorySearch = defineTool({
|
|
|
135
135
|
maxResults: z.number().int().min(1).max(MAX_SEARCH_RESULTS).default(100),
|
|
136
136
|
})
|
|
137
137
|
.strict(),
|
|
138
|
-
outputSchema: z.object({ matches: z.array(AddressOutput) }),
|
|
138
|
+
outputSchema: z.object({ matches: z.array(AddressOutput), truncated: z.boolean().optional().describe("true when the result has maxResults matches: more can follow. Search again from the address after the last match") }),
|
|
139
139
|
readOnly: true,
|
|
140
140
|
async run(input, session) {
|
|
141
141
|
if (input.pattern.length > MAX_SEARCH_PATTERN) {
|
|
142
142
|
throw new WireFailure("invalid-input", `pattern has ${input.pattern.length} bytes. The limit is ${MAX_SEARCH_PATTERN}.`);
|
|
143
143
|
}
|
|
144
144
|
if (input.pattern.every((token) => token === null)) throw new WireFailure("invalid-input", "pattern needs at least one byte that is not ??.");
|
|
145
|
-
const
|
|
146
|
-
|
|
145
|
+
const end = input.end ?? 0xffff;
|
|
146
|
+
if (end < input.start) throw new WireFailure("invalid-input", "end is lower than start.");
|
|
147
|
+
// VICE refuses a range shorter than the pattern. Such a range has no match.
|
|
148
|
+
if (end - input.start + 1 < input.pattern.length) return { matches: [] };
|
|
149
|
+
const result = await session.memorySearch({ ...input, end });
|
|
150
|
+
const matches = result.matches.map(formatC64Address);
|
|
151
|
+
return matches.length >= input.maxResults ? { matches, truncated: true } : { matches };
|
|
147
152
|
},
|
|
148
153
|
});
|
|
149
154
|
|
|
@@ -220,7 +225,7 @@ export const c64Disassemble = defineTool({
|
|
|
220
225
|
address = instruction.address + instruction.bytes.length / 2;
|
|
221
226
|
}
|
|
222
227
|
}
|
|
223
|
-
return { count: lines.length, ...writeOut(out,
|
|
228
|
+
return { count: lines.length, ...writeOut(out, linesText(lines), project) };
|
|
224
229
|
},
|
|
225
230
|
});
|
|
226
231
|
|
package/src/mcp/tools/video.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { WireFailure } from "../../protocol/messages.ts";
|
|
|
8
8
|
import { CIA_SELECTIONS, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, MAX_OBSERVE_BYTES, MAX_OBSERVE_RANGES, SPACES, VICII_MODES, type ObserveParams } from "../../protocol/vice.ts";
|
|
9
9
|
import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
|
|
10
10
|
import { AddressInput, Byte, MemoryViewInput, SpaceInput } from "../inputs.ts";
|
|
11
|
-
import { AddressOutput, defineTool, HexData, requireFields, ToolOutput, TransientName } from "../server.ts";
|
|
11
|
+
import { AddressOutput, defineTool, HexData, requireFields, requireOut, ToolOutput, TransientName } from "../server.ts";
|
|
12
12
|
|
|
13
13
|
const Color = z.number().int().min(0).max(15);
|
|
14
14
|
const NO_CHANGE = "This does not change the run state of the machine.";
|
|
@@ -170,14 +170,16 @@ export const c64Screen = defineTool({
|
|
|
170
170
|
async run(input, session) {
|
|
171
171
|
const { action, ...fields } = input;
|
|
172
172
|
if (action === "capture") {
|
|
173
|
-
|
|
173
|
+
requireOut(fields);
|
|
174
|
+
requireFields(action, fields, ["baseline", "out", "project"]);
|
|
174
175
|
const shot = await session.screenCapture(fields.baseline);
|
|
175
176
|
const result = { width: shot.width, height: shot.height, ...(shot.baseline === undefined ? {} : { baseline: shot.baseline }) };
|
|
176
177
|
if (fields.out !== undefined) return { ...result, ...writeOut(fields.out, Buffer.from(shot.png, "base64"), fields.project) };
|
|
177
178
|
return new ToolOutput(result, [{ type: "image", data: shot.png, mimeType: "image/png" }]);
|
|
178
179
|
}
|
|
179
180
|
if (action === "compare") {
|
|
180
|
-
|
|
181
|
+
requireOut(fields);
|
|
182
|
+
requireFields(action, fields, ["baseline", "maxMismatchRatio", "mask", "includeDiff", "out", "project"], ["baseline"]);
|
|
181
183
|
const { diffPng, ...result } = await session.screenCompare({
|
|
182
184
|
baseline: fields.baseline!,
|
|
183
185
|
maxMismatchRatio: fields.maxMismatchRatio ?? 0,
|
package/src/media.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Media file signatures and sizes. The host checks a file with them before
|
|
2
|
+
// VICE gets it: VICE takes a file that is not of its type in another way, and
|
|
3
|
+
// a CRT or T64 that it cannot read loads as a PRG with a successful autostart.
|
|
4
|
+
|
|
5
|
+
import { Refusal } from "./errors.ts";
|
|
6
|
+
|
|
7
|
+
/** The signature at the start of each media type that has one. */
|
|
8
|
+
const SIGNATURES: Readonly<Record<string, readonly string[]>> = {
|
|
9
|
+
crt: ["C64 CARTRIDGE "],
|
|
10
|
+
t64: ["C64"],
|
|
11
|
+
tap: ["C64-TAPE-RAW"],
|
|
12
|
+
g64: ["GCR-1541"],
|
|
13
|
+
p00: ["C64File"],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** True when `bytes` start with a signature of `type`, or `type` has none. */
|
|
17
|
+
export function hasSignature(type: string, bytes: Uint8Array): boolean {
|
|
18
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
19
|
+
const signatures = SIGNATURES[type];
|
|
20
|
+
return signatures === undefined || signatures.some((signature) => buffer.toString("latin1", 0, signature.length) === signature);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The file sizes that VICE reads for each disk image type without a signature. */
|
|
24
|
+
const SIZES: Readonly<Record<string, readonly number[]>> = {
|
|
25
|
+
d64: [174848, 175531, 196608, 197376, 205312, 206114],
|
|
26
|
+
d71: [349696, 351062],
|
|
27
|
+
d81: [819200, 822400],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** The x64 header: 64 bytes that start with these four, then a d64 image. */
|
|
31
|
+
const X64_MAGIC = Buffer.from([0x43, 0x15, 0x41, 0x64]);
|
|
32
|
+
|
|
33
|
+
/** Refuses a file whose bytes are not of `type` (its extension), with media-error. */
|
|
34
|
+
export function checkMedia(type: string, bytes: Buffer): void {
|
|
35
|
+
const refuse = (why: string) => {
|
|
36
|
+
throw new Refusal("media-error", `The file is not a valid .${type} file: ${why}. Give a file of that type.`);
|
|
37
|
+
};
|
|
38
|
+
if (!hasSignature(type, bytes)) refuse(`it does not start with ${SIGNATURES[type]!.map((signature) => `"${signature.trimEnd()}"`).join(" or ")}`);
|
|
39
|
+
const sizes = SIZES[type];
|
|
40
|
+
if (sizes !== undefined && !sizes.includes(bytes.length)) refuse(`it has ${bytes.length} bytes, and a .${type} image has ${sizes.join(", ")}`);
|
|
41
|
+
if (type === "x64" && (!bytes.subarray(0, 4).equals(X64_MAGIC) || !SIZES.d64!.includes(bytes.length - 64))) refuse("it has no x64 header before a d64 image");
|
|
42
|
+
}
|