c64bridge 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/data/embeddings_basic.json +1 -1
- package/data/embeddings_hardware.json +1 -1
- package/data/embeddings_mixed.json +1 -1
- package/data/embeddings_other.json +1 -1
- package/data/graphics/character-set.csv +64 -64
- package/data/sound/sidwave.md +113 -36
- package/data/sound/sidwave.schema.json +138 -28
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +144 -150
- package/dist/config.js +93 -39
- package/dist/device.js +252 -130
- package/dist/diagnostics.js +10 -2
- package/dist/knowledge.js +2 -1
- package/dist/mcp-server.js +12 -4
- package/dist/petscii.js +18 -15
- package/dist/petsciiArt.js +40 -14
- package/dist/rag/indexer.js +2 -1
- package/dist/sidwave.js +453 -61
- package/dist/sidwaveCompiler.js +1424 -275
- package/dist/tools/audio.js +97 -18
- package/dist/tools/debug.js +9 -9
- package/dist/tools/developer.js +7 -7
- package/dist/tools/errors.js +19 -1
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/hex.js +9 -0
- package/dist/tools/input.js +21 -18
- package/dist/tools/machineControl.js +6 -6
- package/dist/tools/memory.js +11 -10
- package/dist/tools/meta/audio.js +8 -2
- package/dist/tools/meta/background.js +6 -1
- package/dist/tools/meta/filesystem.js +37 -10
- package/dist/tools/meta/graphics.js +11 -16
- package/dist/tools/meta/memory.js +16 -22
- package/dist/tools/meta/program.js +28 -9
- package/dist/tools/pollValidator.js +30 -2
- package/dist/tools/printer.js +4 -4
- package/dist/tools/programRunners.js +10 -9
- package/dist/tools/registry/disk.js +7 -5
- package/dist/tools/storage.js +21 -17
- package/dist/tools/streaming.js +2 -2
- package/dist/tools/translation/assembler.js +192 -57
- package/dist/tools/translation/basicTokenizer.js +44 -10
- package/dist/tools/translation/opcodeInventory.js +1 -1
- package/dist/tools/types.js +11 -4
- package/dist/tools/vice.js +1 -1
- package/dist/vicBitmap.js +16 -0
- package/dist/vice/mockServer.js +99 -8
- package/dist/vice/readiness.js +4 -9
- package/dist/vice/viceClient.js +34 -6
- package/mcp/schemas/c64_disk.schema.json +2 -2
- package/mcp/schemas/c64_input.schema.json +3 -3
- package/mcp/schemas/c64_program.schema.json +2 -2
- package/mcp/schemas/c64_system.schema.json +6 -4
- package/mcp/schemas/c64_vice.schema.json +3 -3
- package/mcp/server.json +1 -1
- package/mcp/tools.json +20 -14
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/scripts/mockC64Server.mjs +113 -20
- package/scripts/run-coverage.mjs +31 -1
- package/scripts/update-readme.ts +3 -2
- package/server.json +2 -2
package/dist/tools/audio.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { compileSidwaveToPrg, compileSidwaveToSid } from "../sidwaveCompiler.js";
|
|
2
|
-
import { parseSidwave } from "../sidwave.js";
|
|
2
|
+
import { midiToHz, noteNameToMidi, parseSidwave } from "../sidwave.js";
|
|
3
3
|
import { recordAndAnalyzeAudio } from "../audio/record_and_analyze_audio.js";
|
|
4
4
|
import { defineToolModule } from "./types.js";
|
|
5
5
|
import { booleanSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "./schema.js";
|
|
@@ -11,6 +11,7 @@ const DEFAULT_SILENCE_VERIFY_DURATION_SECONDS = 1.5;
|
|
|
11
11
|
const DEFAULT_SILENCE_VERIFY_RMS_THRESHOLD = 0.02;
|
|
12
12
|
const DEFAULT_SILENCE_VERIFY_WAIT_MS = 150;
|
|
13
13
|
const generatedPlaybacks = new Set();
|
|
14
|
+
let lastGeneratedPlayback = Promise.resolve();
|
|
14
15
|
export function cancelGeneratedSidPlayback() {
|
|
15
16
|
for (const controller of generatedPlaybacks)
|
|
16
17
|
controller.abort();
|
|
@@ -317,8 +318,35 @@ function transposeNote(note, semitones) {
|
|
|
317
318
|
const newOctave = Math.floor(midi / 12) - 1;
|
|
318
319
|
return `${names[wrapped]}${newOctave}`;
|
|
319
320
|
}
|
|
320
|
-
|
|
321
|
-
|
|
321
|
+
/** Highest frequency the SID's 16-bit frequency register can produce (about 3848 Hz PAL, 3995 Hz NTSC). */
|
|
322
|
+
function maxSidFrequencyHz(system) {
|
|
323
|
+
return (0xffff * (system === "PAL" ? 985248 : 1022727)) / 16777216;
|
|
324
|
+
}
|
|
325
|
+
/** Reject a note or frequency the SID cannot play instead of letting playback clamp or skip it. */
|
|
326
|
+
function assertPlayableFrequency(hz, label, system, path) {
|
|
327
|
+
const max = maxSidFrequencyHz(system);
|
|
328
|
+
if (hz > max + 0.5) {
|
|
329
|
+
throw new ToolValidationError(`${label} (${hz.toFixed(1)} Hz) is above the SID's highest ${system} frequency of ${max.toFixed(1)} Hz`, { path });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function noteHz(note) {
|
|
333
|
+
const midi = noteNameToMidi(note);
|
|
334
|
+
return midi === undefined ? Number.NaN : midiToHz(midi);
|
|
335
|
+
}
|
|
336
|
+
function sleep(ms, signal) {
|
|
337
|
+
return new Promise((resolve) => {
|
|
338
|
+
if (signal?.aborted) {
|
|
339
|
+
resolve();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const timer = setTimeout(done, ms);
|
|
343
|
+
function done() {
|
|
344
|
+
clearTimeout(timer);
|
|
345
|
+
signal?.removeEventListener("abort", done);
|
|
346
|
+
resolve();
|
|
347
|
+
}
|
|
348
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
349
|
+
});
|
|
322
350
|
}
|
|
323
351
|
function shouldAutoAnalyze(request) {
|
|
324
352
|
const lowered = request.toLowerCase();
|
|
@@ -334,6 +362,28 @@ function resolveAnalyzer(ctx) {
|
|
|
334
362
|
}
|
|
335
363
|
return (options) => recordAndAnalyzeAudio(options);
|
|
336
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Measure whether the SID is silent by capturing audio (the Ultimate audio
|
|
367
|
+
* stream on C64U, otherwise the host microphone). The SID's control registers
|
|
368
|
+
* are write-only, so reading them back cannot show whether a voice sounds.
|
|
369
|
+
*/
|
|
370
|
+
export async function measureSidSilence(ctx) {
|
|
371
|
+
if (DEFAULT_SILENCE_VERIFY_WAIT_MS > 0) {
|
|
372
|
+
await sleep(DEFAULT_SILENCE_VERIFY_WAIT_MS);
|
|
373
|
+
}
|
|
374
|
+
const analysis = await resolveAnalyzer(ctx)({ durationSeconds: DEFAULT_SILENCE_VERIFY_DURATION_SECONDS });
|
|
375
|
+
const { average, max } = extractRmsMetrics((analysis?.analysis?.global_metrics ?? {}));
|
|
376
|
+
if (average === null && max === null) {
|
|
377
|
+
throw new ToolExecutionError("Audio analysis did not provide RMS metrics for silence verification");
|
|
378
|
+
}
|
|
379
|
+
const maxRms = max ?? average ?? 0;
|
|
380
|
+
return {
|
|
381
|
+
silent: maxRms <= DEFAULT_SILENCE_VERIFY_RMS_THRESHOLD,
|
|
382
|
+
averageRms: average ?? maxRms,
|
|
383
|
+
maxRms,
|
|
384
|
+
threshold: DEFAULT_SILENCE_VERIFY_RMS_THRESHOLD,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
337
387
|
function extractRmsMetrics(globalMetrics) {
|
|
338
388
|
const average = typeof globalMetrics.average_rms === "number" ? globalMetrics.average_rms : null;
|
|
339
389
|
const max = typeof globalMetrics.max_rms === "number" ? globalMetrics.max_rms : null;
|
|
@@ -440,7 +490,7 @@ export const audioModule = defineToolModule({
|
|
|
440
490
|
});
|
|
441
491
|
const result = await ctx.client.sidSetVolume(parsed.volume);
|
|
442
492
|
if (!result.success) {
|
|
443
|
-
throw new ToolExecutionError("C64
|
|
493
|
+
throw new ToolExecutionError("C64 backend reported failure while setting SID volume", {
|
|
444
494
|
details: normaliseFailure(result.details),
|
|
445
495
|
});
|
|
446
496
|
}
|
|
@@ -483,7 +533,7 @@ export const audioModule = defineToolModule({
|
|
|
483
533
|
cancelGeneratedSidPlayback(); // Reset is the documented way to stop a generated arpeggio.
|
|
484
534
|
const result = await ctx.client.sidReset(parsed.hard);
|
|
485
535
|
if (!result.success) {
|
|
486
|
-
throw new ToolExecutionError("C64
|
|
536
|
+
throw new ToolExecutionError("C64 backend reported failure while resetting SID", {
|
|
487
537
|
details: normaliseFailure(result.details),
|
|
488
538
|
});
|
|
489
539
|
}
|
|
@@ -520,6 +570,13 @@ export const audioModule = defineToolModule({
|
|
|
520
570
|
try {
|
|
521
571
|
const parsed = sidNoteOnArgsSchema.parse(args ?? {});
|
|
522
572
|
const voice = Math.max(1, Math.min(3, Math.floor(parsed.voice ?? 1)));
|
|
573
|
+
const system = (parsed.system ?? "PAL");
|
|
574
|
+
if (parsed.note) {
|
|
575
|
+
assertPlayableFrequency(noteHz(parsed.note), parsed.note, system, "$.note");
|
|
576
|
+
}
|
|
577
|
+
else if (parsed.frequencyHz !== undefined) {
|
|
578
|
+
assertPlayableFrequency(parsed.frequencyHz, `${parsed.frequencyHz} Hz`, system, "$.frequencyHz");
|
|
579
|
+
}
|
|
523
580
|
ctx.logger.info("Starting SID voice", {
|
|
524
581
|
voice,
|
|
525
582
|
note: parsed.note ?? null,
|
|
@@ -539,7 +596,7 @@ export const audioModule = defineToolModule({
|
|
|
539
596
|
release: parsed.release,
|
|
540
597
|
});
|
|
541
598
|
if (!result.success) {
|
|
542
|
-
throw new ToolExecutionError("C64
|
|
599
|
+
throw new ToolExecutionError("C64 backend reported failure while starting SID voice", {
|
|
543
600
|
details: normaliseFailure(result.details),
|
|
544
601
|
});
|
|
545
602
|
}
|
|
@@ -593,7 +650,7 @@ export const audioModule = defineToolModule({
|
|
|
593
650
|
ctx.logger.info("Stopping SID voice", { voice });
|
|
594
651
|
const result = await ctx.client.sidNoteOff(voice);
|
|
595
652
|
if (!result.success) {
|
|
596
|
-
throw new ToolExecutionError("C64
|
|
653
|
+
throw new ToolExecutionError("C64 backend reported failure while stopping SID voice", {
|
|
597
654
|
details: normaliseFailure(result.details),
|
|
598
655
|
});
|
|
599
656
|
}
|
|
@@ -633,7 +690,7 @@ export const audioModule = defineToolModule({
|
|
|
633
690
|
cancelGeneratedSidPlayback(); // Otherwise its remaining notes would resume writing right after this silences the chip.
|
|
634
691
|
const result = await ctx.client.sidSilenceAll();
|
|
635
692
|
if (!result.success) {
|
|
636
|
-
throw new ToolExecutionError("C64
|
|
693
|
+
throw new ToolExecutionError("C64 backend reported failure while silencing SID", {
|
|
637
694
|
details: normaliseFailure(result.details),
|
|
638
695
|
});
|
|
639
696
|
}
|
|
@@ -724,7 +781,7 @@ export const audioModule = defineToolModule({
|
|
|
724
781
|
ctx.logger.info("Playing SID file", { path: parsed.path, songnr: parsed.songnr ?? null });
|
|
725
782
|
const result = await ctx.client.sidplayFile(parsed.path, parsed.songnr);
|
|
726
783
|
if (!result.success) {
|
|
727
|
-
throw new ToolExecutionError("C64
|
|
784
|
+
throw new ToolExecutionError("C64 backend reported failure while starting SID playback", {
|
|
728
785
|
details: normaliseFailure(result.details),
|
|
729
786
|
});
|
|
730
787
|
}
|
|
@@ -764,7 +821,7 @@ export const audioModule = defineToolModule({
|
|
|
764
821
|
ctx.logger.info("Playing MOD file", { path: parsed.path });
|
|
765
822
|
const result = await ctx.client.modplayFile(parsed.path);
|
|
766
823
|
if (!result.success) {
|
|
767
|
-
throw new ToolExecutionError("C64
|
|
824
|
+
throw new ToolExecutionError("C64 backend reported failure while starting MOD playback", {
|
|
768
825
|
details: normaliseFailure(result.details),
|
|
769
826
|
});
|
|
770
827
|
}
|
|
@@ -811,6 +868,8 @@ export const audioModule = defineToolModule({
|
|
|
811
868
|
const iv = intervals[i % intervals.length];
|
|
812
869
|
const note = transposeNote(parsed.root, iv);
|
|
813
870
|
const durationMs = parsed.preset === "expression" ? expressiveDurations[i % expressiveDurations.length] : parsed.tempoMs;
|
|
871
|
+
// Playback runs in the background, so a note the SID cannot play must be rejected here.
|
|
872
|
+
assertPlayableFrequency(noteHz(note), note, "PAL", "$.root");
|
|
814
873
|
timeline.push({ t: timestamp, note, durationMs });
|
|
815
874
|
timestamp += durationMs;
|
|
816
875
|
}
|
|
@@ -825,8 +884,12 @@ export const audioModule = defineToolModule({
|
|
|
825
884
|
const controller = new AbortController();
|
|
826
885
|
generatedPlaybacks.add(controller);
|
|
827
886
|
const pinnedFacadePromise = ctx.client.pinFacade ? ctx.client.pinFacade() : Promise.resolve(undefined); // Pin once so a later select_backend can't retarget notes mid-arpeggio (HARD01-030).
|
|
828
|
-
|
|
887
|
+
// A replaced sequence sends its final gate-off on voice 1; waiting for it
|
|
888
|
+
// keeps that gate-off from cutting off this sequence's first note.
|
|
889
|
+
const previousPlayback = lastGeneratedPlayback;
|
|
890
|
+
lastGeneratedPlayback = (async () => {
|
|
829
891
|
try {
|
|
892
|
+
await previousPlayback;
|
|
830
893
|
const pinnedFacade = await pinnedFacadePromise;
|
|
831
894
|
await ctx.client.sidSetVolume(8, pinnedFacade);
|
|
832
895
|
for (let i = 0; i < parsed.steps; i += 1) {
|
|
@@ -845,10 +908,14 @@ export const audioModule = defineToolModule({
|
|
|
845
908
|
release: 0,
|
|
846
909
|
}, pinnedFacade);
|
|
847
910
|
const durationMs = parsed.preset === "expression" ? expressiveDurations[i % expressiveDurations.length] : parsed.tempoMs;
|
|
848
|
-
|
|
911
|
+
// Hold GATE for most of the note, then clear it for a short gap so the next note retriggers ADSR.
|
|
912
|
+
const gateGapMs = Math.max(1, Math.min(20, Math.floor(durationMs / 8)));
|
|
913
|
+
// Cancellation ends the sleep at once, so the final gate-off below follows promptly.
|
|
914
|
+
await sleep(Math.max(0, durationMs - gateGapMs), controller.signal);
|
|
915
|
+
if (controller.signal.aborted)
|
|
916
|
+
break;
|
|
849
917
|
await ctx.client.sidNoteOff(1, pinnedFacade);
|
|
850
|
-
|
|
851
|
-
await sleep(Math.max(0, durationMs - Math.max(1, Math.min(20, Math.floor(durationMs / 8)))));
|
|
918
|
+
await sleep(gateGapMs, controller.signal);
|
|
852
919
|
}
|
|
853
920
|
await ctx.client.sidNoteOff(1, pinnedFacade);
|
|
854
921
|
}
|
|
@@ -889,7 +956,15 @@ export const audioModule = defineToolModule({
|
|
|
889
956
|
tags: ["sid", "music", "compiler"],
|
|
890
957
|
prerequisites: [],
|
|
891
958
|
examples: [
|
|
892
|
-
{
|
|
959
|
+
{
|
|
960
|
+
name: "Compile PRG",
|
|
961
|
+
description: "Play compiled PRG",
|
|
962
|
+
arguments: {
|
|
963
|
+
sidwave: "sidwave: 1.0\ntitle: Demo\ntempo: 120\nmode: PAL\nvoices:\n - id: 1\n waveform: triangle\n patterns: { A: { groove: [C4, E4, G4, C5] } }\ntimeline:\n - { bars: 2, layers: { v1: A } }\n",
|
|
964
|
+
output: "prg",
|
|
965
|
+
dryRun: false,
|
|
966
|
+
},
|
|
967
|
+
},
|
|
893
968
|
],
|
|
894
969
|
workflowHints: [
|
|
895
970
|
"Use when the user provides SIDWAVE/CPG scores or asks for export to PRG/SID; share any generated download URIs in your response.",
|
|
@@ -920,7 +995,7 @@ export const audioModule = defineToolModule({
|
|
|
920
995
|
const sid = compileSidwaveToSid(document, compiled.prg, { entryAddress: compiled.entryAddress });
|
|
921
996
|
const result = await ctx.client.sidplayAttachment(sid.sid);
|
|
922
997
|
if (!result.success) {
|
|
923
|
-
throw new ToolExecutionError("C64
|
|
998
|
+
throw new ToolExecutionError("C64 backend reported failure while playing compiled SID", {
|
|
924
999
|
details: normaliseFailure(result.details),
|
|
925
1000
|
});
|
|
926
1001
|
}
|
|
@@ -930,7 +1005,7 @@ export const audioModule = defineToolModule({
|
|
|
930
1005
|
else {
|
|
931
1006
|
const result = await ctx.client.runPrg(compiled.prg);
|
|
932
1007
|
if (!result.success) {
|
|
933
|
-
throw new ToolExecutionError("C64
|
|
1008
|
+
throw new ToolExecutionError("C64 backend reported failure while running compiled PRG", {
|
|
934
1009
|
details: normaliseFailure(result.details),
|
|
935
1010
|
});
|
|
936
1011
|
}
|
|
@@ -946,13 +1021,17 @@ export const audioModule = defineToolModule({
|
|
|
946
1021
|
pulse_width: voice.pulse_width,
|
|
947
1022
|
adsr: voice.adsr,
|
|
948
1023
|
}));
|
|
949
|
-
|
|
1024
|
+
// Warnings (for example a PRG that leaves no room for BASIC) go into the text that clients show.
|
|
1025
|
+
const warningText = compiled.warnings.length > 0 ? `\nWarning: ${compiled.warnings.join("\nWarning: ")}` : "";
|
|
1026
|
+
return textResult(`${message}${warningText}`, {
|
|
950
1027
|
success: true,
|
|
951
1028
|
ranOnC64,
|
|
952
1029
|
dryRun: parsed.dryRun,
|
|
953
1030
|
format: outputFormat,
|
|
954
1031
|
prgSize: compiled.prg.length,
|
|
955
1032
|
entryAddress: compiled.entryAddress,
|
|
1033
|
+
basicFreeBytes: compiled.basicFreeBytes,
|
|
1034
|
+
warnings: compiled.warnings,
|
|
956
1035
|
song: document.song,
|
|
957
1036
|
voices: voiceSummaries,
|
|
958
1037
|
runDetails,
|
package/dist/tools/debug.js
CHANGED
|
@@ -572,17 +572,17 @@ function formatAddress(value) {
|
|
|
572
572
|
}
|
|
573
573
|
function parseAddress(value, label) {
|
|
574
574
|
const trimmed = value.trim();
|
|
575
|
-
|
|
576
|
-
let
|
|
577
|
-
if (
|
|
578
|
-
|
|
579
|
-
literal = trimmed.slice(1);
|
|
575
|
+
// Anchored patterns: Number.parseInt alone would read "$08G0" as $0008.
|
|
576
|
+
let parsed = Number.NaN;
|
|
577
|
+
if (/^\$[0-9a-f]+$/i.test(trimmed)) {
|
|
578
|
+
parsed = Number.parseInt(trimmed.slice(1), 16);
|
|
580
579
|
}
|
|
581
|
-
else if (
|
|
582
|
-
|
|
583
|
-
|
|
580
|
+
else if (/^0x[0-9a-f]+$/i.test(trimmed)) {
|
|
581
|
+
parsed = Number.parseInt(trimmed.slice(2), 16);
|
|
582
|
+
}
|
|
583
|
+
else if (/^[0-9]+$/.test(trimmed)) {
|
|
584
|
+
parsed = Number.parseInt(trimmed, 10);
|
|
584
585
|
}
|
|
585
|
-
const parsed = Number.parseInt(literal, radix);
|
|
586
586
|
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 0xffff) {
|
|
587
587
|
throw new ToolValidationError(`Invalid ${label}`, { path: `$.${label}` });
|
|
588
588
|
}
|
package/dist/tools/developer.js
CHANGED
|
@@ -229,7 +229,7 @@ export const developerModule = defineToolModule({
|
|
|
229
229
|
});
|
|
230
230
|
const result = await ctx.client.configSet(parsed.category, parsed.item, String(parsed.value));
|
|
231
231
|
if (!result.success) {
|
|
232
|
-
throw new ToolExecutionError("C64
|
|
232
|
+
throw new ToolExecutionError("C64 backend reported failure while updating configuration", {
|
|
233
233
|
details: toRecord(result.details),
|
|
234
234
|
});
|
|
235
235
|
}
|
|
@@ -275,7 +275,7 @@ export const developerModule = defineToolModule({
|
|
|
275
275
|
]));
|
|
276
276
|
const result = await ctx.client.configBatchUpdate(requestPayload);
|
|
277
277
|
if (!result.success) {
|
|
278
|
-
throw new ToolExecutionError("C64
|
|
278
|
+
throw new ToolExecutionError("C64 backend reported failure during batch configuration update", {
|
|
279
279
|
details: toRecord(result.details),
|
|
280
280
|
});
|
|
281
281
|
}
|
|
@@ -312,7 +312,7 @@ export const developerModule = defineToolModule({
|
|
|
312
312
|
ctx.logger.info("Loading configuration from flash");
|
|
313
313
|
const result = await ctx.client.configLoadFromFlash();
|
|
314
314
|
if (!result.success) {
|
|
315
|
-
throw new ToolExecutionError("C64
|
|
315
|
+
throw new ToolExecutionError("C64 backend reported failure while loading from flash", {
|
|
316
316
|
details: toRecord(result.details),
|
|
317
317
|
});
|
|
318
318
|
}
|
|
@@ -348,7 +348,7 @@ export const developerModule = defineToolModule({
|
|
|
348
348
|
ctx.logger.info("Saving configuration to flash");
|
|
349
349
|
const result = await ctx.client.configSaveToFlash();
|
|
350
350
|
if (!result.success) {
|
|
351
|
-
throw new ToolExecutionError("C64
|
|
351
|
+
throw new ToolExecutionError("C64 backend reported failure while saving to flash", {
|
|
352
352
|
details: toRecord(result.details),
|
|
353
353
|
});
|
|
354
354
|
}
|
|
@@ -384,7 +384,7 @@ export const developerModule = defineToolModule({
|
|
|
384
384
|
ctx.logger.info("Resetting configuration to defaults");
|
|
385
385
|
const result = await ctx.client.configResetToDefault();
|
|
386
386
|
if (!result.success) {
|
|
387
|
-
throw new ToolExecutionError("C64
|
|
387
|
+
throw new ToolExecutionError("C64 backend reported failure while resetting configuration", {
|
|
388
388
|
details: toRecord(result.details),
|
|
389
389
|
});
|
|
390
390
|
}
|
|
@@ -484,7 +484,7 @@ export const developerModule = defineToolModule({
|
|
|
484
484
|
ctx.logger.info("Reading debug register");
|
|
485
485
|
const result = await ctx.client.debugregRead();
|
|
486
486
|
if (!result.success) {
|
|
487
|
-
throw new ToolExecutionError("C64
|
|
487
|
+
throw new ToolExecutionError("C64 backend reported failure while reading debug register", {
|
|
488
488
|
details: toRecord(result.details),
|
|
489
489
|
});
|
|
490
490
|
}
|
|
@@ -524,7 +524,7 @@ export const developerModule = defineToolModule({
|
|
|
524
524
|
ctx.logger.info("Writing debug register", { value });
|
|
525
525
|
const result = await ctx.client.debugregWrite(value);
|
|
526
526
|
if (!result.success) {
|
|
527
|
-
throw new ToolExecutionError("C64
|
|
527
|
+
throw new ToolExecutionError("C64 backend reported failure while writing debug register", {
|
|
528
528
|
details: toRecord(result.details),
|
|
529
529
|
});
|
|
530
530
|
}
|
package/dist/tools/errors.js
CHANGED
|
@@ -44,14 +44,32 @@ export function toolErrorResult(error) {
|
|
|
44
44
|
...(error.code !== undefined ? { code: error.code } : {}),
|
|
45
45
|
...(error.details !== undefined ? { details: error.details } : {}),
|
|
46
46
|
};
|
|
47
|
-
const
|
|
47
|
+
const located = error.path ? `${error.message} (at ${error.path})` : error.message;
|
|
48
|
+
// Clients often show only the text content, so surface the underlying cause
|
|
49
|
+
// (for example a range check or firmware error string) next to the summary.
|
|
50
|
+
const detailMessage = describeDetailMessage(error.details);
|
|
51
|
+
const message = detailMessage && !located.includes(detailMessage) ? `${located}: ${detailMessage}` : located;
|
|
48
52
|
const base = textResult(message, { error: metadata });
|
|
49
53
|
return { ...base, isError: true };
|
|
50
54
|
}
|
|
55
|
+
function describeDetailMessage(details) {
|
|
56
|
+
if (!details || typeof details !== "object") {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const message = details.message;
|
|
60
|
+
return typeof message === "string" && message.trim().length > 0 ? message.trim() : undefined;
|
|
61
|
+
}
|
|
51
62
|
export function unknownErrorResult(error) {
|
|
52
63
|
if (error instanceof ToolError) {
|
|
53
64
|
return toolErrorResult(error);
|
|
54
65
|
}
|
|
66
|
+
// Input documents such as SIDWAVE scores are checked outside the tool schemas;
|
|
67
|
+
// their errors carry a path and are input errors, not internal failures.
|
|
68
|
+
if (error instanceof Error && error.name === "SidwaveValidationError") {
|
|
69
|
+
const path = error.path;
|
|
70
|
+
const metadata = { kind: "validation", ...(typeof path === "string" ? { path } : {}) };
|
|
71
|
+
return { ...textResult(error.message, { error: metadata }), isError: true };
|
|
72
|
+
}
|
|
55
73
|
const metadata = { kind: "unknown" };
|
|
56
74
|
const message = error instanceof Error ? error.message : String(error);
|
|
57
75
|
const base = textResult(message, { error: metadata });
|
package/dist/tools/graphics.js
CHANGED
|
@@ -349,7 +349,7 @@ async function executeGenerateSprite(rawArgs, ctx) {
|
|
|
349
349
|
multicolour: parsed.multicolour,
|
|
350
350
|
});
|
|
351
351
|
if (!result.success) {
|
|
352
|
-
throw new ToolExecutionError("C64
|
|
352
|
+
throw new ToolExecutionError("C64 backend reported failure while rendering sprite", {
|
|
353
353
|
details: normaliseFailure(result.details),
|
|
354
354
|
});
|
|
355
355
|
}
|
|
@@ -383,7 +383,7 @@ async function executeRenderPetscii(rawArgs, ctx) {
|
|
|
383
383
|
});
|
|
384
384
|
const result = await ctx.client.renderPetsciiScreenAndRun(parsed);
|
|
385
385
|
if (!result.success) {
|
|
386
|
-
throw new ToolExecutionError("C64
|
|
386
|
+
throw new ToolExecutionError("C64 backend reported failure while rendering PETSCII text", {
|
|
387
387
|
details: normaliseFailure(result.details),
|
|
388
388
|
});
|
|
389
389
|
}
|
|
@@ -426,7 +426,7 @@ async function executeGenerateBitmap(rawArgs, ctx) {
|
|
|
426
426
|
screenAddress: parsed.screenAddress,
|
|
427
427
|
});
|
|
428
428
|
if (!result.success) {
|
|
429
|
-
throw new ToolExecutionError("C64
|
|
429
|
+
throw new ToolExecutionError("C64 backend reported failure while displaying bitmap image", {
|
|
430
430
|
details: normaliseFailure(result.details),
|
|
431
431
|
});
|
|
432
432
|
}
|
|
@@ -498,7 +498,7 @@ async function executeCreatePetscii(rawArgs, ctx) {
|
|
|
498
498
|
if (!parsed.dryRun) {
|
|
499
499
|
runResult = await ctx.client.uploadAndRunBasic(art.program);
|
|
500
500
|
if (!runResult.success) {
|
|
501
|
-
throw new ToolExecutionError("C64
|
|
501
|
+
throw new ToolExecutionError("C64 backend reported failure while rendering PETSCII art", {
|
|
502
502
|
details: normaliseFailure(runResult.details),
|
|
503
503
|
});
|
|
504
504
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalise a user-supplied hex byte string such as "$A9 $00", "A9_00" or
|
|
3
|
+
* "$A900" to bare hex digits. A "$" may prefix the whole string or each
|
|
4
|
+
* whitespace-separated byte group; whitespace and "_" separators are removed.
|
|
5
|
+
* The result is not validated, so callers still check length and digits.
|
|
6
|
+
*/
|
|
7
|
+
export function stripHexByteSeparators(input) {
|
|
8
|
+
return input.trim().replace(/(^|\s)\$/g, "$1").replace(/[\s_]/g, "");
|
|
9
|
+
}
|
package/dist/tools/input.js
CHANGED
|
@@ -57,36 +57,40 @@ const PETSCII_TOKENS = {
|
|
|
57
57
|
LIGHT_GREY: 155,
|
|
58
58
|
REVERSE_ON: 18,
|
|
59
59
|
REVERSE_OFF: 146,
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// FLASH ON/OFF, ESC and TAB are C128 codes: on a C64, 148 is INST (inserts a
|
|
61
|
+
// space) and 9 re-enables SHIFT+C= case switching, so they are not offered.
|
|
62
62
|
LOWERCASE: 14,
|
|
63
63
|
UPPERCASE: 142,
|
|
64
|
-
ESCAPE: 27,
|
|
65
|
-
TAB: 9,
|
|
66
64
|
CURSOR_UP: 145,
|
|
67
65
|
CURSOR_DOWN: 17,
|
|
68
66
|
CURSOR_LEFT: 157,
|
|
69
67
|
CURSOR_RIGHT: 29,
|
|
70
68
|
};
|
|
69
|
+
// ASCII a-z are shifted graphics characters in PETSCII, so typed "print" would
|
|
70
|
+
// reach BASIC as graphics and give ?SYNTAX ERROR. Literal letters are therefore
|
|
71
|
+
// sent as the unshifted PETSCII letters $41-$5A; {tokens} keep their codes.
|
|
71
72
|
function expandPetsciiTokens(text) {
|
|
72
|
-
return text.replace(/\{([^}]+)\}/g, (_match, token) => {
|
|
73
|
+
return text.replace(/\{([^}]+)\}|[a-z]/g, (_match, token) => {
|
|
74
|
+
if (token === undefined) {
|
|
75
|
+
return _match.toUpperCase();
|
|
76
|
+
}
|
|
73
77
|
const upper = token.trim().toUpperCase();
|
|
74
78
|
const code = PETSCII_TOKENS[upper];
|
|
75
79
|
if (code !== undefined) {
|
|
76
80
|
return String.fromCharCode(code);
|
|
77
81
|
}
|
|
78
|
-
// Numeric form: {$1D} or {29}
|
|
79
|
-
let num;
|
|
80
|
-
if (upper
|
|
82
|
+
// Numeric form: {$1D} or {29}. Anchored, so {12AB} is not read as {12}.
|
|
83
|
+
let num = NaN;
|
|
84
|
+
if (/^\$[0-9A-F]{1,2}$/.test(upper)) {
|
|
81
85
|
num = parseInt(upper.slice(1), 16);
|
|
82
86
|
}
|
|
83
|
-
else {
|
|
87
|
+
else if (/^\d{1,3}$/.test(upper)) {
|
|
84
88
|
num = parseInt(upper, 10);
|
|
85
89
|
}
|
|
86
90
|
if (!isNaN(num) && num >= 0 && num <= 255) {
|
|
87
91
|
return String.fromCharCode(num);
|
|
88
92
|
}
|
|
89
|
-
return _match; //
|
|
93
|
+
return _match.toUpperCase(); // type unrecognised tokens literally
|
|
90
94
|
});
|
|
91
95
|
}
|
|
92
96
|
// ---------------------------------------------------------------------------
|
|
@@ -170,7 +174,7 @@ export const keyArgsSchema = objectSchema({
|
|
|
170
174
|
additionalProperties: false,
|
|
171
175
|
});
|
|
172
176
|
export const joystickArgsSchema = objectSchema({
|
|
173
|
-
description: "Simulate joystick input. On C64U/U64 this uses machine:input
|
|
177
|
+
description: "Simulate joystick input. On C64U/U64 this uses machine:input. On VICE it attaches VICE's I/O simulation joyport device while pressed; that device also pulls CIA lines 5-7 low, so port 1 produces phantom keys (e.g. SHIFT+C=) and port 2 reads $1E for up instead of $7E. Prefer port 2 on VICE and test bits 0-4.",
|
|
174
178
|
properties: {
|
|
175
179
|
op: literalSchema("joystick"),
|
|
176
180
|
port: numberSchema({
|
|
@@ -266,7 +270,7 @@ const inputOperationHandlers = {
|
|
|
266
270
|
const keyByte = tokenCode !== undefined
|
|
267
271
|
? tokenCode
|
|
268
272
|
: parsed.key.length === 1
|
|
269
|
-
? parsed.key.charCodeAt(0) & 0xff
|
|
273
|
+
? parsed.key.toUpperCase().charCodeAt(0) & 0xff
|
|
270
274
|
: (() => {
|
|
271
275
|
throw new ToolValidationError(`Unrecognised key: ${parsed.key}. Use a single character or a PETSCII token name.`, { path: "$.key" });
|
|
272
276
|
})();
|
|
@@ -291,7 +295,6 @@ const inputOperationHandlers = {
|
|
|
291
295
|
const port = parsed.port;
|
|
292
296
|
const durationMs = parsed.durationMs ?? 80;
|
|
293
297
|
const pressedByte = joystickByte(parsed.controls);
|
|
294
|
-
const releasedByte = 0xff;
|
|
295
298
|
const platform = await ctx.client.getActiveBackendType();
|
|
296
299
|
if (platform === "c64u") {
|
|
297
300
|
if (parsed.controls.length === 0 && parsed.action === "release") {
|
|
@@ -309,19 +312,19 @@ const inputOperationHandlers = {
|
|
|
309
312
|
throw new ToolValidationError("VICE joystick supports up, down, left, right, and fire only.", { path: "$.controls" });
|
|
310
313
|
}
|
|
311
314
|
if (parsed.action === "release") {
|
|
312
|
-
await ctx.client.
|
|
315
|
+
await ctx.client.viceJoystickRelease(port);
|
|
313
316
|
ctx.logger.info("Released joystick", { port });
|
|
314
317
|
return textResult(`Joystick port ${port} released.`, { success: true, port, action: "release" });
|
|
315
318
|
}
|
|
316
319
|
if (parsed.action === "press") {
|
|
317
|
-
await ctx.client.
|
|
320
|
+
await ctx.client.viceJoystickPress(port, pressedByte);
|
|
318
321
|
ctx.logger.info("Pressed joystick", { port, controls: parsed.controls, byte: pressedByte });
|
|
319
|
-
return textResult(`Joystick port ${port} pressed: ${parsed.controls.join(", ") || "none"}
|
|
322
|
+
return textResult(`Joystick port ${port} pressed: ${parsed.controls.join(", ") || "none"}.${port === 1 ? " On VICE, port 1 also shows phantom keys while held." : ""}`, { success: true, port, action: "press", controls: parsed.controls });
|
|
320
323
|
}
|
|
321
324
|
// tap: press → wait → release
|
|
322
|
-
await ctx.client.
|
|
325
|
+
await ctx.client.viceJoystickPress(port, pressedByte);
|
|
323
326
|
await new Promise((res) => setTimeout(res, durationMs));
|
|
324
|
-
await ctx.client.
|
|
327
|
+
await ctx.client.viceJoystickRelease(port);
|
|
325
328
|
ctx.logger.info("Tapped joystick", { port, controls: parsed.controls, durationMs });
|
|
326
329
|
return textResult(`Joystick port ${port} tapped: ${parsed.controls.join(", ") || "none"} for ${durationMs}ms.`, { success: true, port, action: "tap", controls: parsed.controls, durationMs });
|
|
327
330
|
}
|
|
@@ -70,7 +70,7 @@ export const machineControlModule = defineToolModule({
|
|
|
70
70
|
ctx.logger.info("Resetting C64");
|
|
71
71
|
const result = await ctx.client.reset();
|
|
72
72
|
if (!result.success) {
|
|
73
|
-
throw new ToolExecutionError("C64
|
|
73
|
+
throw new ToolExecutionError("C64 backend reported failure while resetting", {
|
|
74
74
|
details: normaliseFailure(result.details),
|
|
75
75
|
});
|
|
76
76
|
}
|
|
@@ -108,7 +108,7 @@ export const machineControlModule = defineToolModule({
|
|
|
108
108
|
ctx.logger.info("Rebooting C64");
|
|
109
109
|
const result = await ctx.client.reboot();
|
|
110
110
|
if (!result.success) {
|
|
111
|
-
throw new ToolExecutionError("C64
|
|
111
|
+
throw new ToolExecutionError("C64 backend reported failure while rebooting", {
|
|
112
112
|
details: normaliseFailure(result.details),
|
|
113
113
|
});
|
|
114
114
|
}
|
|
@@ -146,7 +146,7 @@ export const machineControlModule = defineToolModule({
|
|
|
146
146
|
ctx.logger.info("Pausing C64 execution");
|
|
147
147
|
const result = await ctx.client.pause();
|
|
148
148
|
if (!result.success) {
|
|
149
|
-
throw new ToolExecutionError("C64
|
|
149
|
+
throw new ToolExecutionError("C64 backend reported failure while pausing", {
|
|
150
150
|
details: normaliseFailure(result.details),
|
|
151
151
|
});
|
|
152
152
|
}
|
|
@@ -184,7 +184,7 @@ export const machineControlModule = defineToolModule({
|
|
|
184
184
|
ctx.logger.info("Resuming C64 execution");
|
|
185
185
|
const result = await ctx.client.resume();
|
|
186
186
|
if (!result.success) {
|
|
187
|
-
throw new ToolExecutionError("C64
|
|
187
|
+
throw new ToolExecutionError("C64 backend reported failure while resuming", {
|
|
188
188
|
details: normaliseFailure(result.details),
|
|
189
189
|
});
|
|
190
190
|
}
|
|
@@ -222,7 +222,7 @@ export const machineControlModule = defineToolModule({
|
|
|
222
222
|
ctx.logger.info("Powering off C64");
|
|
223
223
|
const result = await ctx.client.poweroff();
|
|
224
224
|
if (!result.success) {
|
|
225
|
-
throw new ToolExecutionError("C64
|
|
225
|
+
throw new ToolExecutionError("C64 backend reported failure while powering off", {
|
|
226
226
|
details: normaliseFailure(result.details),
|
|
227
227
|
});
|
|
228
228
|
}
|
|
@@ -295,7 +295,7 @@ export const machineControlModule = defineToolModule({
|
|
|
295
295
|
ctx.logger.info("Toggling Ultimate menu button");
|
|
296
296
|
const result = await ctx.client.menuButton();
|
|
297
297
|
if (!result.success) {
|
|
298
|
-
throw new ToolExecutionError("C64
|
|
298
|
+
throw new ToolExecutionError("C64 backend reported failure while toggling menu button", {
|
|
299
299
|
details: normaliseFailure(result.details),
|
|
300
300
|
});
|
|
301
301
|
}
|