@henols/c64-re-tools 2.1.1 → 2.1.3
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 +6 -3
- package/skills/c64-assembler/scripts/assembler.ts +19 -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 +1 -0
- package/skills/c64-disk/scripts/disk.ts +2 -2
- package/skills/c64-disk/scripts/lib/containers.ts +4 -4
- 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 +2 -0
- package/skills/c64-emulator/SKILL.md +4 -1
- package/skills/c64-knowledge/SKILL.md +12 -6
- package/skills/c64-knowledge/scripts/knowledge.ts +41 -25
- package/skills/c64-knowledge/scripts/lib/listing.ts +16 -31
- package/skills/c64-memory-map/SKILL.md +3 -0
- package/skills/c64-memory-map/scripts/memory-map.ts +23 -4
- package/skills/c64-provenance/SKILL.md +5 -3
- package/skills/c64-provenance/scripts/lib/library.ts +28 -5
- package/skills/c64-provenance/scripts/lib/separate.ts +34 -6
- package/skills/c64-provenance/scripts/provenance.ts +22 -10
- package/skills/c64-source/SKILL.md +2 -1
- package/skills/c64-source/scripts/lib/source.ts +25 -33
- package/skills/c64-source/scripts/source.ts +13 -17
- package/skills/c64-static-analysis/SKILL.md +6 -3
- package/skills/c64-static-analysis/scripts/lib/findings.ts +23 -8
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +38 -25
- package/skills/c64-static-analysis/scripts/static-analysis.ts +31 -19
- package/skills/c64-testing/SKILL.md +9 -5
- package/skills/c64-testing/references/scenario-format.md +5 -3
- package/skills/c64-testing/scripts/lib/diff-files.ts +41 -10
- package/skills/c64-testing/scripts/lib/recording.ts +3 -3
- package/skills/c64-testing/scripts/lib/replay.ts +1 -1
- package/skills/c64-testing/scripts/lib/runs.ts +45 -9
- package/skills/c64-testing/scripts/lib/scenario.ts +130 -81
- package/skills/c64-testing/scripts/testing.ts +9 -6
- package/skills/c64-unpacker/SKILL.md +10 -7
- package/skills/c64-unpacker/scripts/lib/loads.ts +15 -6
- package/skills/c64-unpacker/scripts/lib/machine.ts +61 -3
- package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
- package/skills/c64-unpacker/scripts/unpacker.ts +21 -17
- package/src/host/vice/adapter.ts +4 -1
- package/src/host/vice/session.ts +144 -11
- package/src/host-client/image.ts +27 -13
- package/src/host-client/steps.ts +41 -5
- package/src/host-client/tools.ts +4 -3
- package/src/host-client/transfer.ts +22 -29
- package/src/knowledge/history.ts +55 -2
- package/src/knowledge/import.ts +79 -7
- package/src/knowledge/program.ts +31 -2
- package/src/knowledge/read.ts +7 -4
- package/src/knowledge/schema.ts +3 -2
- package/src/knowledge/text.ts +1 -0
- package/src/knowledge/write.ts +86 -42
- package/src/mcp/inputs.ts +6 -3
- package/src/mcp/output.ts +1 -1
- package/src/mcp/server.ts +9 -1
- package/src/mcp/tools/debug.ts +31 -4
- package/src/mcp/tools/machine.ts +3 -1
- package/src/mcp/tools/media.ts +5 -1
- package/src/mcp/tools/memory.ts +4 -3
- package/src/media.ts +57 -0
- package/src/opcodes.ts +81 -18
- package/src/platform.ts +14 -1
- package/src/project.ts +118 -3
- package/src/protocol/vice.ts +4 -1
- package/src/script.ts +118 -5
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,
|
|
@@ -61,7 +62,7 @@ import {
|
|
|
61
62
|
type SuppressionRow,
|
|
62
63
|
type SymbolRow,
|
|
63
64
|
} from "./read.ts";
|
|
64
|
-
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";
|
|
65
|
+
import { CODE_REGION_TYPES, COMMENT_PLACEMENTS, EVIDENCE_KINDS, MAX_ELEMENT, MODULE_FILE, DEFAULT_SPACE, GENERATED_FILES, isGeneratedFile, isSemantic, 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
66
|
import { newRevisionId, rowId, sqlName } from "./text.ts";
|
|
66
67
|
|
|
67
68
|
/** What a change rests on: a coverage run, a test or another project file with its SHA-256, or a note. */
|
|
@@ -140,6 +141,8 @@ export interface ChangeContext {
|
|
|
140
141
|
evidence?: Evidence[];
|
|
141
142
|
/** A revert: the revision whose knowledge it restores. */
|
|
142
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;
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
/**
|
|
@@ -181,8 +184,8 @@ export class Change {
|
|
|
181
184
|
const now = new Date();
|
|
182
185
|
const id = newRevisionId(currentRevision(this.#db), now);
|
|
183
186
|
this.#db
|
|
184
|
-
.prepare("INSERT INTO revisions (id, created_at, origin, operation, input_hash, tool_version, description, restored_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
185
|
-
.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);
|
|
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);
|
|
186
189
|
this.revision = id;
|
|
187
190
|
const evidence = this.#db.prepare("INSERT INTO evidence (revision, kind, ref, sha256, space) VALUES (?, ?, ?, ?, ?)");
|
|
188
191
|
for (const item of this.#context.evidence ?? []) evidence.run(id, item.kind, item.ref, item.sha256 ?? null, item.space ?? null);
|
|
@@ -350,6 +353,27 @@ const result = <T, P>(written: { revision: string | null; current: Step<T, P> })
|
|
|
350
353
|
// Each write has a check, which refuses bad input before anything opens, and a
|
|
351
354
|
// step, which runs inside a change. A single write and a batch share both.
|
|
352
355
|
|
|
356
|
+
/**
|
|
357
|
+
* True when a write of `origin` over a current row with the same content
|
|
358
|
+
* records a decision, so it replaces the row: over a row of an analyzer (the
|
|
359
|
+
* next run of the analyzer can retire its own rows, never a decision), and the
|
|
360
|
+
* user over the LLM. The LLM over the user changes nothing.
|
|
361
|
+
*/
|
|
362
|
+
function confirms(existing: Origin, origin: Origin): boolean {
|
|
363
|
+
return existing !== origin && (!isSemantic(existing) || origin === "user");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The bytes of the program of a space in the knowledge, by address: undefined
|
|
368
|
+
* outside it. A module and a comment are checked against them.
|
|
369
|
+
*/
|
|
370
|
+
function programBytes(db: DatabaseSync, space: Space): (address: number) => number | undefined {
|
|
371
|
+
const rows = db.prepare("SELECT address, bytes FROM current_memory WHERE space = ?").all(space) as Array<{ address: number; bytes: string }>;
|
|
372
|
+
const memory = new Map<number, number>();
|
|
373
|
+
for (const row of rows) Buffer.from(row.bytes, "hex").forEach((byte, offset) => memory.set(row.address + offset, byte));
|
|
374
|
+
return (address) => memory.get(address);
|
|
375
|
+
}
|
|
376
|
+
|
|
353
377
|
/** Every request names its space; without one it is the C64. */
|
|
354
378
|
type InSpace = { space?: Space };
|
|
355
379
|
type SymbolRequest = InSpace & { address: number; name: string; kind?: SymbolKind };
|
|
@@ -369,9 +393,8 @@ export function checkSpace(request: InSpace): void {
|
|
|
369
393
|
function checkSymbol(request: SymbolRequest): void {
|
|
370
394
|
checkSpace(request);
|
|
371
395
|
checkAddress(request.address);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
396
|
+
const problem = labelProblem(request.name);
|
|
397
|
+
if (problem !== undefined) throw new Refusal("invalid-input", problem);
|
|
375
398
|
if (request.kind !== undefined) checkOneOf(SYMBOL_KINDS, request.kind, "kind");
|
|
376
399
|
}
|
|
377
400
|
|
|
@@ -379,7 +402,7 @@ function renameStep(db: DatabaseSync, change: Change, origin: Origin, request: S
|
|
|
379
402
|
const space = spaceOf(request);
|
|
380
403
|
const existing = symbolAt(db, request.address, space);
|
|
381
404
|
const kind = request.kind ?? existing?.kind ?? "label";
|
|
382
|
-
if (existing !== undefined && existing.name === request.name && existing.kind === kind) return { current: existing };
|
|
405
|
+
if (existing !== undefined && existing.name === request.name && existing.kind === kind && !confirms(existing.origin, origin)) return { current: existing };
|
|
383
406
|
const owner = symbolNamed(db, request.name, space);
|
|
384
407
|
if (owner !== undefined && owner.address !== request.address) {
|
|
385
408
|
throw new Refusal("conflict", `The name ${request.name} already belongs to ${formatC64Address(owner.address)}. Rename or remove that symbol first.`);
|
|
@@ -457,7 +480,7 @@ function classifyStep(db: DatabaseSync, change: Change, origin: Origin, request:
|
|
|
457
480
|
const overlapping = regionsOverlapping(db, request.start, request.end, space);
|
|
458
481
|
const [only] = overlapping;
|
|
459
482
|
const same = overlapping.length === 1 && only!.start === request.start && only!.end === request.end && only!.type === request.type && only!.element === request.element;
|
|
460
|
-
if (same) return { current: overlapping };
|
|
483
|
+
if (same && !confirms(only!.origin, origin)) return { current: overlapping };
|
|
461
484
|
clearRange(db, change, request.start, request.end, space);
|
|
462
485
|
change.insertRegion({ space, start: request.start, end: request.end, type: request.type, ...(request.element === undefined ? {} : { element: request.element }), origin });
|
|
463
486
|
liftRegionSuppressions(db, change, request.start, request.end, space);
|
|
@@ -488,14 +511,17 @@ function checkComment(request: CommentRequest): string {
|
|
|
488
511
|
return text;
|
|
489
512
|
}
|
|
490
513
|
|
|
491
|
-
function commentStep(db: DatabaseSync, change: Change, origin: Origin, request: CommentRequest): Step<CommentRow> {
|
|
514
|
+
function commentStep(db: DatabaseSync, change: Change, origin: Origin, request: CommentRequest): Step<CommentRow, CommentRow> {
|
|
492
515
|
const space = spaceOf(request);
|
|
493
516
|
const text = request.text.trim();
|
|
494
517
|
const existing = commentsAt(db, request.address, space).find((comment) => comment.placement === request.placement);
|
|
495
|
-
if (existing?.text === text) return { current: existing };
|
|
518
|
+
if (existing?.text === text && !confirms(existing.origin, origin)) return { current: existing };
|
|
519
|
+
// A comment belongs to an instruction: the listing and the source show it at the start of the instruction only.
|
|
520
|
+
checkInstructionStart(db, space, programBytes(db, space), request.address, "comment");
|
|
496
521
|
if (existing !== undefined) change.close("comments", "space = ? AND address = ? AND placement = ?", space, request.address, request.placement);
|
|
497
522
|
change.insertComment({ space, address: request.address, placement: request.placement, text, origin });
|
|
498
|
-
|
|
523
|
+
const current = commentsAt(db, request.address, space).find((comment) => comment.placement === request.placement)!;
|
|
524
|
+
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
499
525
|
}
|
|
500
526
|
|
|
501
527
|
function checkPlacement(request: PlacementRequest): void {
|
|
@@ -521,14 +547,16 @@ function checkReference(request: ReferenceRequest): void {
|
|
|
521
547
|
if (request.kind === POINTER_KIND) throw new Refusal("invalid-input", "Only an operand expression writes a pointer reference: use operand and unoperand.");
|
|
522
548
|
}
|
|
523
549
|
|
|
524
|
-
function referenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<ReferenceRow> {
|
|
550
|
+
function referenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<ReferenceRow, ReferenceRow> {
|
|
525
551
|
const space = spaceOf(request);
|
|
526
552
|
const find = () => referencesFrom(db, request.from, space).find((reference) => reference.to === request.to && reference.kind === request.kind);
|
|
527
553
|
const existing = find();
|
|
528
|
-
if (existing !== undefined) return { current: existing };
|
|
554
|
+
if (existing !== undefined && !confirms(existing.origin, origin)) return { current: existing };
|
|
555
|
+
// A confirmation of a reference of an analyzer makes it a reference of the writer: the next run of the analyzer keeps it.
|
|
556
|
+
if (existing !== undefined) change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ?", space, request.from, request.to, request.kind);
|
|
529
557
|
change.insertReference({ space, from: request.from, to: request.to, kind: request.kind, origin });
|
|
530
558
|
change.close("suppressions", "space = ? AND entity = 'reference' AND start_address = ? AND to_address = ? AND kind = ?", space, request.from, request.to, request.kind);
|
|
531
|
-
return { current: find()! };
|
|
559
|
+
return existing !== undefined ? { current: find()!, previous: existing } : { current: find()! };
|
|
532
560
|
}
|
|
533
561
|
|
|
534
562
|
function unreferenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<null> {
|
|
@@ -579,26 +607,32 @@ const shown = (value: number, size: number) => (value < 0 ? String(value) : `$${
|
|
|
579
607
|
/**
|
|
580
608
|
* Refuses an address that is not the start of an instruction. The code
|
|
581
609
|
* regions of the knowledge give the instructions: they are decoded from the
|
|
582
|
-
* start of the region, as the listing and the source decode them.
|
|
610
|
+
* start of the region, as the listing and the source decode them. An operand
|
|
611
|
+
* expression needs an instruction, so it is refused also outside the code
|
|
612
|
+
* regions and where the program has no byte. A module and a comment are
|
|
613
|
+
* refused only at an operand byte of a known instruction: they can start data.
|
|
583
614
|
*/
|
|
584
|
-
function checkInstructionStart(db: DatabaseSync, space: Space,
|
|
615
|
+
function checkInstructionStart(db: DatabaseSync, space: Space, byteAt: (address: number) => number | undefined, address: number, write: "operand" | "module" | "comment"): void {
|
|
585
616
|
const where = formatC64Address(address);
|
|
586
617
|
const code = regionsOverlapping(db, address, address, space).find((region) => CODE_REGION_TYPES.includes(region.type));
|
|
587
618
|
if (code === undefined) {
|
|
619
|
+
if (write !== "operand") return;
|
|
588
620
|
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.`);
|
|
589
621
|
}
|
|
590
622
|
let start = code.start;
|
|
591
623
|
let next = code.start;
|
|
592
624
|
while (next < address) {
|
|
593
|
-
const opcode =
|
|
625
|
+
const opcode = byteAt(next);
|
|
594
626
|
if (opcode === undefined) {
|
|
627
|
+
if (write !== "operand") return;
|
|
595
628
|
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.`);
|
|
596
629
|
}
|
|
597
630
|
start = next;
|
|
598
631
|
next += instructionLength(opcode);
|
|
599
632
|
}
|
|
600
633
|
if (next !== address) {
|
|
601
|
-
|
|
634
|
+
const what = write === "operand" ? "" : ` A ${write} is at the start of an instruction, as the listing and the source show it.`;
|
|
635
|
+
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)}.${what} Give ${formatC64Address(start)}.`);
|
|
602
636
|
}
|
|
603
637
|
}
|
|
604
638
|
|
|
@@ -611,7 +645,7 @@ function checkOperandValue(db: DatabaseSync, space: Space, image: OperandImage,
|
|
|
611
645
|
const where = formatC64Address(request.address);
|
|
612
646
|
const opcode = image.byteAt(request.address);
|
|
613
647
|
if (opcode === undefined) throw new Refusal("invalid-input", `${where} is outside the image.`);
|
|
614
|
-
checkInstructionStart(db, space, image, request.address);
|
|
648
|
+
checkInstructionStart(db, space, (at) => image.byteAt(at), request.address, "operand");
|
|
615
649
|
const length = instructionLength(opcode);
|
|
616
650
|
if (length === 1) throw new Refusal("invalid-input", `The instruction at ${where} (opcode ${shown(opcode, 0x100)}) has no operand.`);
|
|
617
651
|
if ((opcode & 0x1f) === 0x10) throw new Refusal("invalid-input", `The instruction at ${where} is a branch: its operand is a distance, not a value.`);
|
|
@@ -644,10 +678,10 @@ function operandStep(db: DatabaseSync, change: Change, context: ChangeContext &
|
|
|
644
678
|
if (request.expression !== undefined) wanted.expression = request.expression.trim();
|
|
645
679
|
const targets = new Set(checkOperandValue(db, space, context.image, request, operandText(wanted)));
|
|
646
680
|
const existing = operandAt(db, request.address, space);
|
|
647
|
-
|
|
681
|
+
const origin = context.origin as OperandRow["origin"];
|
|
682
|
+
if (existing !== undefined && existing.select === wanted.select && existing.symbol === wanted.symbol && existing.offset === wanted.offset && existing.expression === wanted.expression && !confirms(existing.origin, origin)) {
|
|
648
683
|
return { current: existing };
|
|
649
684
|
}
|
|
650
|
-
const origin = context.origin as OperandRow["origin"];
|
|
651
685
|
if (existing !== undefined) change.close("operands", "space = ? AND address = ?", space, request.address);
|
|
652
686
|
change.insertOperand({ space, address: request.address, ...wanted, origin });
|
|
653
687
|
const pointers = referencesFrom(db, request.address, space).filter((reference) => reference.kind === POINTER_KIND);
|
|
@@ -681,19 +715,22 @@ function checkModule(request: ModuleRequest): void {
|
|
|
681
715
|
if (request.title !== undefined && (request.title.trim().length === 0 || request.title.length > MAX_COMMENT)) throw new Refusal("invalid-input", `A module title has 1 to ${MAX_COMMENT} characters.`);
|
|
682
716
|
}
|
|
683
717
|
|
|
684
|
-
function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: ModuleRequest): Step<ModuleRow> {
|
|
718
|
+
function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: ModuleRequest): Step<ModuleRow, ModuleRow> {
|
|
685
719
|
const space = spaceOf(request);
|
|
686
720
|
const existing = moduleAt(db, request.start, space);
|
|
687
721
|
// A module write without a title keeps the title it has.
|
|
688
722
|
const title = request.title === undefined ? existing?.title : request.title.trim();
|
|
689
|
-
if (existing !== undefined && existing.file === request.file && existing.title === title) return { current: existing };
|
|
723
|
+
if (existing !== undefined && existing.file === request.file && existing.title === title && !confirms(existing.origin, origin)) return { current: existing };
|
|
724
|
+
// A file of the source starts at an instruction: a start inside one splits it into bytes.
|
|
725
|
+
checkInstructionStart(db, space, programBytes(db, space), request.start, "module");
|
|
690
726
|
const owner = moduleWithFile(db, request.file, space);
|
|
691
727
|
if (owner !== undefined && owner.start !== request.start) {
|
|
692
728
|
throw new Refusal("conflict", `The file ${request.file} already belongs to the module at ${formatC64Address(owner.start)}. Give this module another file, or remove that module first.`);
|
|
693
729
|
}
|
|
694
730
|
if (existing !== undefined) change.close("modules", "space = ? AND start_address = ?", space, request.start);
|
|
695
731
|
change.insertModule({ space, start: request.start, file: request.file, ...(title === undefined ? {} : { title }), origin: origin as ModuleRow["origin"] });
|
|
696
|
-
|
|
732
|
+
const current = moduleAt(db, request.start, space)!;
|
|
733
|
+
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
697
734
|
}
|
|
698
735
|
|
|
699
736
|
function unmoduleStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
|
|
@@ -758,9 +795,10 @@ function patchStep(db: DatabaseSync, change: Change, context: ChangeContext & {
|
|
|
758
795
|
const symbol = symbolNamed(db, request.switch, space);
|
|
759
796
|
if (symbol !== undefined) throw new Refusal("conflict", `The switch name ${request.switch} is the name of the symbol at ${formatC64Address(symbol.address)}. Give the switch another name.`);
|
|
760
797
|
if (space === DEFAULT_SPACE && context.image.platform.has(request.switch)) throw new Refusal("conflict", `The switch name ${request.switch} is a C64 platform name. Give the switch another name.`);
|
|
761
|
-
|
|
798
|
+
// A write at the start of a patch of the same switch changes that patch; every other patch that it overlaps is a conflict.
|
|
799
|
+
const other = patchesOverlapping(db, request.start, request.end, space).find((row) => row.start !== request.start || row.switch !== request.switch);
|
|
762
800
|
if (other !== undefined) {
|
|
763
|
-
throw new Refusal("conflict", `The patch ${range} overlaps the patch of ${other.switch} at ${formatC64Address(other.start)}-${formatC64Address(other.end)}. Change or remove that patch first (unpatch).`);
|
|
801
|
+
throw new Refusal("conflict", `The patch ${range} of ${request.switch} overlaps the patch of ${other.switch} at ${formatC64Address(other.start)}-${formatC64Address(other.end)}. A patch cannot overlap another patch, and only a patch of the same switch at the same start replaces it. Change or remove that patch first (unpatch).`);
|
|
764
802
|
}
|
|
765
803
|
const [use] = macroUsesOverlapping(db, request.start, request.end, space);
|
|
766
804
|
if (use !== undefined) {
|
|
@@ -770,8 +808,8 @@ function patchStep(db: DatabaseSync, change: Change, context: ChangeContext & {
|
|
|
770
808
|
const code = request.code.trimEnd();
|
|
771
809
|
// A patch write without a comment keeps the comment it has, as a module keeps its title.
|
|
772
810
|
const comment = request.comment === undefined ? existing?.comment : request.comment.trim();
|
|
773
|
-
if (existing !== undefined && existing.end === request.end && existing.switch === request.switch && existing.code === code && existing.comment === comment) return { current: existing };
|
|
774
811
|
const origin = context.origin as PatchRow["origin"];
|
|
812
|
+
if (existing !== undefined && existing.end === request.end && existing.switch === request.switch && existing.code === code && existing.comment === comment && !confirms(existing.origin, origin)) return { current: existing };
|
|
775
813
|
if (existing !== undefined) change.close("patches", "space = ? AND start_address = ?", space, request.start);
|
|
776
814
|
change.insertPatch({ space, start: request.start, end: request.end, switch: request.switch, code, ...(comment === undefined ? {} : { comment }), origin });
|
|
777
815
|
const current = patchesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start)!;
|
|
@@ -813,7 +851,7 @@ function macroStep(db: DatabaseSync, change: Change, origin: Origin, request: Ma
|
|
|
813
851
|
const params = request.params ?? [];
|
|
814
852
|
const body = request.body.trimEnd();
|
|
815
853
|
const comment = request.comment === undefined ? existing?.comment : request.comment.trim();
|
|
816
|
-
if (existing !== undefined && listText(existing.params) === listText(params) && existing.body === body && existing.comment === comment) return { current: existing };
|
|
854
|
+
if (existing !== undefined && listText(existing.params) === listText(params) && existing.body === body && existing.comment === comment && !confirms(existing.origin, origin)) return { current: existing };
|
|
817
855
|
if (existing !== undefined && existing.params.length !== params.length) {
|
|
818
856
|
const uses = listMacroUses(db, undefined, request.name, undefined, space);
|
|
819
857
|
if (uses.length > 0) {
|
|
@@ -899,8 +937,8 @@ function useStep(db: DatabaseSync, change: Change, context: ChangeContext & { im
|
|
|
899
937
|
throw new Refusal("conflict", `The macro use ${range} overlaps the use of ${other.macro} at ${formatC64Address(other.start)}-${formatC64Address(other.end)}. Remove that use first (unuse).`);
|
|
900
938
|
}
|
|
901
939
|
const existing = macroUsesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start);
|
|
902
|
-
if (existing !== undefined && existing.end === request.end && existing.macro === request.macro && listText(existing.args) === listText(args)) return { current: existing };
|
|
903
940
|
const origin = context.origin as MacroUseRow["origin"];
|
|
941
|
+
if (existing !== undefined && existing.end === request.end && existing.macro === request.macro && listText(existing.args) === listText(args) && !confirms(existing.origin, origin)) return { current: existing };
|
|
904
942
|
if (existing !== undefined) change.close("macro_uses", "space = ? AND start_address = ?", space, request.start);
|
|
905
943
|
change.insertMacroUse({ space, start: request.start, end: request.end, macro: request.macro, args, origin });
|
|
906
944
|
const current = macroUsesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start)!;
|
|
@@ -938,7 +976,7 @@ export function unclassifyRegion(db: DatabaseSync, context: WriteContext, reques
|
|
|
938
976
|
return result(write(db, context, "unclassify-region", (change) => unclassifyStep(db, change, context.origin, request)));
|
|
939
977
|
}
|
|
940
978
|
|
|
941
|
-
export function setComment(db: DatabaseSync, context: WriteContext, request: CommentRequest): WriteResult<CommentRow> {
|
|
979
|
+
export function setComment(db: DatabaseSync, context: WriteContext, request: CommentRequest): WriteResult<CommentRow, CommentRow> {
|
|
942
980
|
checkComment(request);
|
|
943
981
|
return result(write(db, context, "set-comment", (change) => commentStep(db, change, context.origin, request)));
|
|
944
982
|
}
|
|
@@ -948,7 +986,7 @@ export function removeComment(db: DatabaseSync, context: WriteContext, request:
|
|
|
948
986
|
return result(write(db, context, "remove-comment", (change) => uncommentStep(db, change, context.origin, request)));
|
|
949
987
|
}
|
|
950
988
|
|
|
951
|
-
export function addReference(db: DatabaseSync, context: WriteContext, request: ReferenceRequest): WriteResult<ReferenceRow> {
|
|
989
|
+
export function addReference(db: DatabaseSync, context: WriteContext, request: ReferenceRequest): WriteResult<ReferenceRow, ReferenceRow> {
|
|
952
990
|
checkReference(request);
|
|
953
991
|
return result(write(db, context, "add-reference", (change) => referenceStep(db, change, context.origin, request)));
|
|
954
992
|
}
|
|
@@ -959,7 +997,7 @@ export function removeReference(db: DatabaseSync, context: WriteContext, request
|
|
|
959
997
|
}
|
|
960
998
|
|
|
961
999
|
/** Starts a file of the generated source at an address; one module per address and per file. */
|
|
962
|
-
export function setModule(db: DatabaseSync, context: WriteContext, request: ModuleRequest): WriteResult<ModuleRow> {
|
|
1000
|
+
export function setModule(db: DatabaseSync, context: WriteContext, request: ModuleRequest): WriteResult<ModuleRow, ModuleRow> {
|
|
963
1001
|
checkModule(request);
|
|
964
1002
|
return result(write(db, context, "set-module", (change) => moduleStep(db, change, context.origin, request)));
|
|
965
1003
|
}
|
|
@@ -1051,7 +1089,7 @@ export const MAX_BATCH = 10_000;
|
|
|
1051
1089
|
/** The result of each write of a batch, in order. */
|
|
1052
1090
|
export type BatchStep = Step<
|
|
1053
1091
|
SymbolRow | RegionRow[] | CommentRow | ReferenceRow | ModuleRow | OperandRow | PatchRow | MacroRow | MacroUseRow | null,
|
|
1054
|
-
SymbolRow | RegionRow[] | OperandRow | PatchRow | MacroRow | MacroUseRow
|
|
1092
|
+
SymbolRow | RegionRow[] | CommentRow | ReferenceRow | ModuleRow | OperandRow | PatchRow | MacroRow | MacroUseRow
|
|
1055
1093
|
>;
|
|
1056
1094
|
|
|
1057
1095
|
/**
|
|
@@ -1199,19 +1237,25 @@ const KEYS = {
|
|
|
1199
1237
|
|
|
1200
1238
|
/**
|
|
1201
1239
|
* Makes the current knowledge equal to what it was at `revision`, as a new
|
|
1202
|
-
* revision.
|
|
1240
|
+
* revision. With `space`, only the rows of that space change; without it,
|
|
1241
|
+
* every space changes. History is never erased; restored rows keep their origin.
|
|
1203
1242
|
*/
|
|
1204
|
-
export function revert(db: DatabaseSync, context: WriteContext, request: { revision: string }): WriteResult<{ restoredRevision: string }> {
|
|
1243
|
+
export function revert(db: DatabaseSync, context: WriteContext, request: { revision: string; space?: Space }): WriteResult<{ restoredRevision: string }> {
|
|
1244
|
+
checkSpace(request);
|
|
1205
1245
|
const restored = resolveRevision(db, request.revision);
|
|
1206
|
-
|
|
1246
|
+
const only = request.space;
|
|
1247
|
+
return write(db, { ...context, restores: restored, ...(only === undefined ? {} : { restoresSpace: only }) }, "revert", (change) => {
|
|
1207
1248
|
// The coverage maps that the knowledge has follow the revert, also when no coverage range changes.
|
|
1208
|
-
const maps = (runs: CoverageRun[]) => JSON.stringify(runs.map((run) => [run.name, run.sha256, run.space, run.revision]));
|
|
1249
|
+
const maps = (runs: CoverageRun[]) => JSON.stringify(runs.filter((run) => only === undefined || run.space === only).map((run) => [run.name, run.sha256, run.space, run.revision]));
|
|
1209
1250
|
if (maps(coverageRuns(db, restored)) !== maps(coverageRuns(db))) change.id();
|
|
1210
|
-
const
|
|
1251
|
+
const inSpace = only === undefined ? "" : " AND space = ?";
|
|
1252
|
+
const spaceArgs = only === undefined ? [] : [only];
|
|
1253
|
+
const at = `valid_from_revision <= ? AND (valid_to_revision IS NULL OR valid_to_revision > ?)${inSpace}`;
|
|
1254
|
+
const current = `valid_to_revision IS NULL${inSpace}`;
|
|
1211
1255
|
for (const [table, spec] of Object.entries(KEYS)) {
|
|
1212
1256
|
const name = table === "references" ? '"references"' : table;
|
|
1213
|
-
const then = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${at}`).all(restored, restored) as Array<Record<string, unknown>>;
|
|
1214
|
-
const now = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE
|
|
1257
|
+
const then = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${at}`).all(restored, restored, ...spaceArgs) as Array<Record<string, unknown>>;
|
|
1258
|
+
const now = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${current}`).all(...spaceArgs) as Array<Record<string, unknown>>;
|
|
1215
1259
|
const key = (row: Record<string, unknown>) => (spec.key as (row: unknown) => string)(spec.map(row as never));
|
|
1216
1260
|
const thenKeys = new Set(then.map(key));
|
|
1217
1261
|
const nowKeys = new Set(now.map(key));
|
|
@@ -1253,8 +1297,8 @@ export function revert(db: DatabaseSync, context: WriteContext, request: { revis
|
|
|
1253
1297
|
}
|
|
1254
1298
|
// The program: a row is the same when all its values are the same.
|
|
1255
1299
|
for (const [table, columns] of PROGRAM_TABLES) {
|
|
1256
|
-
const then = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE ${at}`).all(restored, restored) as Array<Record<string, unknown>>;
|
|
1257
|
-
const now = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE
|
|
1300
|
+
const then = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE ${at}`).all(restored, restored, ...spaceArgs) as Array<Record<string, unknown>>;
|
|
1301
|
+
const now = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE ${current}`).all(...spaceArgs) as Array<Record<string, unknown>>;
|
|
1258
1302
|
const key = (row: Record<string, unknown>) => JSON.stringify(columns.split(", ").map((column) => row[column]));
|
|
1259
1303
|
const thenKeys = new Set(then.map(key));
|
|
1260
1304
|
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
|
@@ -7,7 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
|
|
8
8
|
import { projectFolder, writeReported, type Written } from "../host-client/transfer.ts";
|
|
9
9
|
|
|
10
|
-
export const OutInput = z.string().min(1).describe("write the
|
|
10
|
+
export const OutInput = z.string().min(1).describe("write the data to this file, relative to the project folder. An existing file is overwritten. The tool description tells what the result then gives");
|
|
11
11
|
|
|
12
12
|
/** The project of a tool with a path. */
|
|
13
13
|
export const ProjectInput = z
|
package/src/mcp/server.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
import { formatC64Address } from "../c64.ts";
|
|
11
11
|
import { Byte, TOOL_INPUTS } from "./inputs.ts";
|
|
12
12
|
import { LostBeforeAnswer, type ViceSessionClient } from "../host-client/vice-session.ts";
|
|
13
|
+
import { checkOut } from "../host-client/transfer.ts";
|
|
13
14
|
import { isRefusal } from "../errors.ts";
|
|
14
15
|
import { shownMessage, WireFailure, type WireError } from "../protocol/messages.ts";
|
|
15
16
|
import { COMPARISONS, CONDITION_REGISTERS, MEMORY_VIEWS, SPACES, type Condition } from "../protocol/vice.ts";
|
|
@@ -179,10 +180,14 @@ function errorResult(error: WireError): CallToolResult {
|
|
|
179
180
|
* An unknown field of the tool input also gets the fields that the tool takes.
|
|
180
181
|
*/
|
|
181
182
|
function describeIssues(error: z.ZodError, input?: z.ZodObject): string {
|
|
183
|
+
// An unknown field also breaks the rules that look at the whole object: name only the field.
|
|
184
|
+
const unknown = error.issues.some((issue) => issue.code === "unrecognized_keys");
|
|
182
185
|
return error.issues
|
|
186
|
+
.filter((issue) => !unknown || issue.code !== "custom")
|
|
183
187
|
.map((issue) => {
|
|
184
188
|
const where = issue.path.length === 0 ? "input" : issue.path.join(".");
|
|
185
|
-
const
|
|
189
|
+
const names = input === undefined ? [] : Object.keys(input.shape);
|
|
190
|
+
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(", ")}`;
|
|
186
191
|
return `${where}: ${issue.message}${fields}`;
|
|
187
192
|
})
|
|
188
193
|
.join(". ");
|
|
@@ -228,6 +233,9 @@ async function runCall(
|
|
|
228
233
|
const parsed = tool.inputSchema.safeParse(args ?? {});
|
|
229
234
|
if (!parsed.success) return errorResult({ code: "invalid-input", message: describeIssues(parsed.error, tool.inputSchema) });
|
|
230
235
|
try {
|
|
236
|
+
// An out path is checked before the tool runs: a refused path must not lose the work or leave its side effects.
|
|
237
|
+
const fields = parsed.data as { out?: unknown; project?: unknown };
|
|
238
|
+
if (typeof fields.out === "string") checkOut(fields.out, typeof fields.project === "string" ? fields.project : undefined);
|
|
231
239
|
let output: unknown;
|
|
232
240
|
try {
|
|
233
241
|
output = await tool.run(parsed.data, await session());
|
package/src/mcp/tools/debug.ts
CHANGED
|
@@ -186,7 +186,8 @@ export const c64Trace = defineTool({
|
|
|
186
186
|
"Each hit gives the instruction (pc, bytes, text) and the registers a, x, y and sp before it. " +
|
|
187
187
|
"point in a hit is the number of its point in points, 0 for the first. " +
|
|
188
188
|
"It also gives the frame from 0, and the raster line and cycle where the instruction started. " +
|
|
189
|
-
"A read or write hit also gives the address that the instruction used, and the byte
|
|
189
|
+
"A read or write hit also gives the address that the instruction used, and value, the byte that it read or wrote. " +
|
|
190
|
+
"At $D000-$DFFF only a load or a store gives value: a read after the access can give another byte. " +
|
|
190
191
|
`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. ` +
|
|
191
192
|
`The result lists the first ${TRACE_LISTED} hits. With out, it writes all hits as JSON lines to that project file. ` +
|
|
192
193
|
"Each hit stops the machine for a short time. Thus a point with many hits in each frame makes the trace slow. Use a small range and a maxHits. " +
|
|
@@ -339,7 +340,9 @@ export const c64Memmap = defineTool({
|
|
|
339
340
|
"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. " +
|
|
340
341
|
"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. " +
|
|
341
342
|
`Each list has at most ${MAX_SUMMARY_RANGES} ranges. ` +
|
|
342
|
-
"Action clear sets all access records to zero."
|
|
343
|
+
"Action clear sets all access records to zero. " +
|
|
344
|
+
"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. " +
|
|
345
|
+
"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.",
|
|
343
346
|
inputSchema: z
|
|
344
347
|
.object({
|
|
345
348
|
action: z.enum(["read", "summary", "clear"]),
|
|
@@ -362,6 +365,7 @@ export const c64Memmap = defineTool({
|
|
|
362
365
|
writtenAndExecuted: z.array(RangeOutput).optional(),
|
|
363
366
|
writtenAndExecutedTruncated: z.boolean().optional(),
|
|
364
367
|
cleared: z.boolean().optional(),
|
|
368
|
+
emulatorFlaw: z.string().optional().describe("a known flaw of VICE that can make the ROM and I/O classes wrong in this result"),
|
|
365
369
|
...WrittenOutput,
|
|
366
370
|
}),
|
|
367
371
|
readOnly: false,
|
|
@@ -391,12 +395,14 @@ export const c64Memmap = defineTool({
|
|
|
391
395
|
...(code.length > MAX_SUMMARY_RANGES ? { codeTruncated: true } : {}),
|
|
392
396
|
writtenAndExecuted: showRanges(written),
|
|
393
397
|
...(written.length > MAX_SUMMARY_RANGES ? { writtenAndExecutedTruncated: true } : {}),
|
|
398
|
+
...basicRomFlaw(romInBasicArea(map, start)),
|
|
394
399
|
};
|
|
395
400
|
}
|
|
396
401
|
requireOut(fields, ["maxRanges"]);
|
|
397
402
|
requireFields("read", fields, ["start", "end", "maxRanges", "out", "project"]);
|
|
398
403
|
if (fields.out !== undefined) {
|
|
399
|
-
|
|
404
|
+
const map = await readCoverage(session, start, end);
|
|
405
|
+
return { ...writeOut(fields.out, map, fields.project), ...basicRomFlaw(romInBasicArea(map, start)) };
|
|
400
406
|
}
|
|
401
407
|
const result = await session.memmap({ action: "read", start, end, maxRanges: fields.maxRanges ?? 256 });
|
|
402
408
|
const ranges = result.ranges.map((range) => ({
|
|
@@ -407,8 +413,29 @@ export const c64Memmap = defineTool({
|
|
|
407
413
|
write: (range.flags & WRITE) !== 0,
|
|
408
414
|
in: sources(range.flags),
|
|
409
415
|
}));
|
|
410
|
-
|
|
416
|
+
const flaw = basicRomFlaw(result.ranges.some((range) => range.start <= 0xbfff && range.end >= 0xa000 && (range.flags & ROM_ACCESS) !== 0));
|
|
417
|
+
return result.next === undefined ? { ranges, ...flaw } : { ranges, truncated: true, next: formatC64Address(result.next), ...flaw };
|
|
411
418
|
},
|
|
412
419
|
});
|
|
413
420
|
|
|
421
|
+
const ROM_ACCESS = COVERAGE.romRead | COVERAGE.romExecute;
|
|
422
|
+
|
|
423
|
+
/** True when the map from `start` has a ROM access at $A000-$BFFF. */
|
|
424
|
+
function romInBasicArea(map: Uint8Array, start: number): boolean {
|
|
425
|
+
for (let address = Math.max(start, 0xa000); address <= Math.min(start + map.length - 1, 0xbfff); address++) {
|
|
426
|
+
if ((map[address - start]! & ROM_ACCESS) !== 0) return true;
|
|
427
|
+
}
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** VICE tests only bit 0 of $01 for $A000-$BFFF; the C64 shows BASIC ROM there only when bits 0 and 1 are set. */
|
|
432
|
+
function basicRomFlaw(present: boolean): { emulatorFlaw?: string } {
|
|
433
|
+
if (!present) return {};
|
|
434
|
+
return {
|
|
435
|
+
emulatorFlaw:
|
|
436
|
+
"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. " +
|
|
437
|
+
"Read $01 of the program at that time before you take rom at $A000-$BFFF as the BASIC ROM.",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
414
441
|
export const debugTools = [c64Breakpoint, c64Watchpoint, c64CpuHistory, c64Trace, c64Backtrace, c64Profile, c64Memmap, c64Timing];
|
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
|
@@ -44,6 +44,10 @@ export const c64Autostart = defineTool({
|
|
|
44
44
|
}),
|
|
45
45
|
readOnly: false,
|
|
46
46
|
async run(input, session) {
|
|
47
|
+
const type = extname(input.path).slice(1).toLowerCase();
|
|
48
|
+
if (input.index !== undefined && !(DISK_TYPES as readonly string[]).includes(type)) {
|
|
49
|
+
throw new WireFailure("invalid-input", `index selects an entry of a disk image (${list(DISK_TYPES)}). A .${type} file has no directory: leave out index.`);
|
|
50
|
+
}
|
|
47
51
|
// VICE numbers the directory entries from 1, and its 0 loads "*".
|
|
48
52
|
return session.autostart({ path: input.path, index: input.index === undefined ? 0 : input.index + 1, run: input.run, root: projectFolder(input.project) });
|
|
49
53
|
},
|
|
@@ -131,7 +135,7 @@ export const c64Snapshot = defineTool({
|
|
|
131
135
|
}
|
|
132
136
|
if (action === "restore" && fields.file !== undefined) {
|
|
133
137
|
requireFields(action, fields, ["file", "project"], ["file"]);
|
|
134
|
-
return session.snapshotRestore(readProjectFile(fields.file, { root: projectFolder(fields.project) }).bytes);
|
|
138
|
+
return session.snapshotRestore(readProjectFile(fields.file, { root: projectFolder(fields.project), what: "file" }).bytes);
|
|
135
139
|
}
|
|
136
140
|
if (action === "save") {
|
|
137
141
|
requireOut(fields);
|
package/src/mcp/tools/memory.ts
CHANGED
|
@@ -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,7 +135,7 @@ 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) {
|
|
@@ -147,7 +147,8 @@ export const c64MemorySearch = defineTool({
|
|
|
147
147
|
// VICE refuses a range shorter than the pattern. Such a range has no match.
|
|
148
148
|
if (end - input.start + 1 < input.pattern.length) return { matches: [] };
|
|
149
149
|
const result = await session.memorySearch({ ...input, end });
|
|
150
|
-
|
|
150
|
+
const matches = result.matches.map(formatC64Address);
|
|
151
|
+
return matches.length >= input.maxResults ? { matches, truncated: true } : { matches };
|
|
151
152
|
},
|
|
152
153
|
});
|
|
153
154
|
|