c64bridge 0.9.3 → 1.0.1
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/AGENTS.md +19 -3
- package/README.md +190 -148
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +277 -41
- package/dist/config.js +30 -18
- package/dist/device.js +191 -125
- package/dist/knowledge.js +1 -1
- package/dist/loggingHttpClient.js +15 -3
- package/dist/mcp-server.js +65 -16
- package/dist/platform.js +13 -2
- package/dist/rag/indexer.js +11 -8
- package/dist/rag/init.js +11 -8
- package/dist/sidwaveCompiler.js +2 -2
- package/dist/tools/audio.js +27 -6
- package/dist/tools/developer.js +6 -6
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/input.js +136 -24
- package/dist/tools/machineControl.js +85 -6
- package/dist/tools/memory.js +21 -6
- package/dist/tools/meta/artifacts.js +20 -8
- package/dist/tools/meta/background.js +11 -1
- package/dist/tools/meta/index.js +1 -1
- package/dist/tools/pollValidator.js +19 -51
- package/dist/tools/printer.js +1 -0
- package/dist/tools/programRunners.js +5 -5
- package/dist/tools/rag.js +1 -1
- package/dist/tools/registry/batch.js +1 -1
- package/dist/tools/registry/config.js +5 -5
- package/dist/tools/registry/disk.js +2 -2
- package/dist/tools/registry/drive.js +2 -2
- package/dist/tools/registry/graphics.js +2 -2
- package/dist/tools/registry/memory.js +2 -2
- package/dist/tools/registry/platform.js +9 -5
- package/dist/tools/registry/printer.js +1 -0
- package/dist/tools/registry/program.js +5 -5
- package/dist/tools/registry/rag.js +1 -1
- package/dist/tools/registry/sound.js +4 -4
- package/dist/tools/registry/system.js +13 -3
- package/dist/tools/storage.js +1 -1
- package/dist/vice/mockServer.js +33 -0
- package/dist/vice/process.js +11 -1
- package/dist/vice/viceClient.js +35 -6
- package/doc/c64u/c64-openapi.yaml +1130 -774
- package/doc/reviews/hardening/01-fable/prompt-handover.md +125 -0
- package/doc/reviews/hardening/01-fable/prompt.md +98 -0
- package/doc/reviews/hardening/01-fable/review-prompt.md +468 -0
- package/doc/reviews/hardening/01-fable/review.md +1234 -0
- package/doc/u2/u2-openapi.yaml +1192 -0
- package/generated/c64/index.js +142 -9
- package/generated/openapi/c64u/index.js +1117 -0
- package/generated/openapi/u2/index.js +929 -0
- package/mcp/schemas/c64_graphics.schema.json +3 -3
- package/mcp/schemas/c64_input.schema.json +249 -8
- package/mcp/schemas/c64_memory.schema.json +3 -3
- package/mcp/schemas/c64_program.schema.json +3 -3
- package/mcp/schemas/c64_select_backend.schema.json +2 -0
- package/mcp/schemas/c64_system.schema.json +41 -1
- package/mcp/server.json +1 -1
- package/mcp/tools.json +386 -44
- package/mcp.json +14 -2
- package/package.json +4 -2
- package/scripts/mockC64Server.mjs +171 -2
- package/scripts/prepare-release.mjs +13 -2
- package/scripts/update-readme.ts +18 -30
- package/server.json +3 -3
|
@@ -75,13 +75,7 @@ function concatBuffers(...buffers) {
|
|
|
75
75
|
* Looks for patterns like "?SYNTAX ERROR" or "SYNTAX ERROR IN 120".
|
|
76
76
|
*/
|
|
77
77
|
function extractBasicError(screen) {
|
|
78
|
-
const
|
|
79
|
-
// Check if ERROR appears on screen
|
|
80
|
-
if (!upperScreen.includes("ERROR")) {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
// Try to match "?ERROR_TYPE ERROR" or "ERROR_TYPE ERROR IN LINE"
|
|
84
|
-
const errorMatch = /\?([A-Z\s]+)\s+ERROR(?:\s+IN\s+(\d+))?/i.exec(screen);
|
|
78
|
+
const errorMatch = /(?:^|\n)\s*\?([A-Z\s]+?)\s+ERROR(?:\s+IN\s+(\d+))?(?=\s|$)/im.exec(screen); // A real error has a leading "?"; ordinary output like "0 ERRORS FOUND" must never match.
|
|
85
79
|
if (errorMatch) {
|
|
86
80
|
const errorType = errorMatch[1]?.trim().replace(/\s+/g, " ");
|
|
87
81
|
const lineStr = errorMatch[2];
|
|
@@ -91,8 +85,7 @@ function extractBasicError(screen) {
|
|
|
91
85
|
line: Number.isFinite(line) ? line : undefined,
|
|
92
86
|
};
|
|
93
87
|
}
|
|
94
|
-
|
|
95
|
-
return { message: "UNKNOWN ERROR" };
|
|
88
|
+
return null;
|
|
96
89
|
}
|
|
97
90
|
/**
|
|
98
91
|
* Poll for BASIC program outcome by detecting errors on screen.
|
|
@@ -124,17 +117,20 @@ async function pollBasicOutcome(client, logger, config) {
|
|
|
124
117
|
line: errorInfo.line,
|
|
125
118
|
};
|
|
126
119
|
}
|
|
127
|
-
// If we detected RUN and no error yet, continue polling
|
|
128
|
-
if (runDetected) {
|
|
129
|
-
// Continue polling for a bit to catch any delayed errors
|
|
130
|
-
// but if we've been polling for a while without errors, consider it successful
|
|
131
|
-
}
|
|
132
120
|
}
|
|
133
121
|
catch (error) {
|
|
134
122
|
logger.debug("Failed to read screen during BASIC polling", { error, pollCount });
|
|
135
123
|
}
|
|
136
124
|
await delay(config.intervalMs);
|
|
137
125
|
}
|
|
126
|
+
if (!runDetected) { // Neither "RUN" nor an error ever appeared for the whole window. On working backends the firmware's own LOAD/RUN echo shows this text, so its total absence means execution never started despite the reported success (HARD01-011/036).
|
|
127
|
+
logger.debug("BASIC polling completed without ever observing RUN or an error", { pollCount, elapsed: Date.now() - startTime });
|
|
128
|
+
return {
|
|
129
|
+
status: "error",
|
|
130
|
+
type: "BASIC",
|
|
131
|
+
message: "No execution activity detected: the firmware reported success, but the program does not appear to have run.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
138
134
|
logger.debug("BASIC polling completed without errors", { pollCount, elapsed: Date.now() - startTime });
|
|
139
135
|
return {
|
|
140
136
|
status: "ok",
|
|
@@ -178,38 +174,20 @@ async function pollAsmOutcome(client, logger, config) {
|
|
|
178
174
|
}
|
|
179
175
|
await delay(config.intervalMs);
|
|
180
176
|
}
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
logger.debug("RUN never detected, assuming instant execution");
|
|
184
|
-
return {
|
|
185
|
-
status: "ok",
|
|
186
|
-
type: "ASM",
|
|
187
|
-
};
|
|
177
|
+
if (!runDetected) { // Fall through to the liveness check rather than assuming success. Skipping it here used to report false success for any run that never types anything visible, e.g. a firmware run_prg that silently no-ops instead of running the PRG (HARD01-036).
|
|
178
|
+
logger.debug("RUN never detected; falling through to hardware liveness check instead of assuming success");
|
|
188
179
|
}
|
|
189
|
-
//
|
|
190
|
-
let prevIoSignature = null;
|
|
191
|
-
let prevJiffyClock = null;
|
|
180
|
+
let previousSignature = null; // Only stable RAM: reading I/O can ack CIA interrupts on Ultimate, and raster/timer values aren't evidence of liveness.
|
|
192
181
|
let alive = false;
|
|
193
182
|
const pollDeadline = Date.now() + config.maxMs;
|
|
194
183
|
while (Date.now() < pollDeadline && !alive) {
|
|
195
184
|
pollCount++;
|
|
196
185
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const ioRegions = await client.readMemoryRaw(0xD000, 0x1000); // $D000-$DFFF
|
|
200
|
-
// Read low memory through screen RAM in a single call: $0000-$07E7
|
|
201
|
-
// This covers zero page, stack, jiffy clock ($00A0-$00A2), and screen memory
|
|
202
|
-
const lowMemAndScreen = await client.readMemoryRaw(0x0000, 0x7E8); // $0000-$07E7
|
|
203
|
-
// Extract jiffy clock for separate comparison (at offset $00A0 in the buffer)
|
|
204
|
-
const jiffyClock = lowMemAndScreen.slice(0xA0, 0xA3);
|
|
205
|
-
// Exclude the jiffy clock from the broad signature so it can be tracked separately.
|
|
206
|
-
const lowMemSignatureBuffer = lowMemAndScreen.slice();
|
|
207
|
-
lowMemSignatureBuffer.fill(0, 0xA0, 0xA3);
|
|
208
|
-
const combinedBuffer = concatBuffers(ioRegions, lowMemSignatureBuffer);
|
|
209
|
-
const ioSignature = computeCrc32(combinedBuffer);
|
|
186
|
+
const screenRam = await client.readMemoryRaw(0x0400, 0x03e8);
|
|
187
|
+
const signature = computeCrc32(screenRam);
|
|
210
188
|
// Check for any activity
|
|
211
|
-
if (
|
|
212
|
-
logger.debug("
|
|
189
|
+
if (previousSignature !== null && signature !== previousSignature) {
|
|
190
|
+
logger.debug("Program-visible screen activity detected", {
|
|
213
191
|
pollCount,
|
|
214
192
|
elapsed: Date.now() - startTime,
|
|
215
193
|
signatureChanged: true
|
|
@@ -217,17 +195,7 @@ async function pollAsmOutcome(client, logger, config) {
|
|
|
217
195
|
alive = true;
|
|
218
196
|
break;
|
|
219
197
|
}
|
|
220
|
-
|
|
221
|
-
logger.debug("Jiffy clock advanced", {
|
|
222
|
-
pollCount,
|
|
223
|
-
elapsed: Date.now() - startTime,
|
|
224
|
-
clockChanged: true
|
|
225
|
-
});
|
|
226
|
-
alive = true;
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
prevIoSignature = ioSignature;
|
|
230
|
-
prevJiffyClock = jiffyClock;
|
|
198
|
+
previousSignature = signature;
|
|
231
199
|
}
|
|
232
200
|
catch (error) {
|
|
233
201
|
logger.debug("Failed to read memory during ASM polling", { error, pollCount });
|
|
@@ -246,7 +214,7 @@ async function pollAsmOutcome(client, logger, config) {
|
|
|
246
214
|
return {
|
|
247
215
|
status: "crashed",
|
|
248
216
|
type: "ASM",
|
|
249
|
-
reason: "no
|
|
217
|
+
reason: "no program-visible screen progression within window",
|
|
250
218
|
};
|
|
251
219
|
}
|
|
252
220
|
/**
|
package/dist/tools/printer.js
CHANGED
|
@@ -225,6 +225,7 @@ function validateEpsonDensity(args) {
|
|
|
225
225
|
}
|
|
226
226
|
export const printerModule = defineToolModule({
|
|
227
227
|
domain: "printer",
|
|
228
|
+
supportedPlatforms: ["c64u", "u2"],
|
|
228
229
|
summary: "Printer workflow helpers for Commodore MPS and Epson FX devices, including prompt templates.",
|
|
229
230
|
resources: [
|
|
230
231
|
"c64://printer/spec",
|
|
@@ -668,7 +668,7 @@ export const programRunnersModule = defineToolModule({
|
|
|
668
668
|
"Invoke right after you generate BASIC source so it runs on the C64 without extra user steps.",
|
|
669
669
|
"Ensure the program includes line numbers and uppercase keywords before calling the tool.",
|
|
670
670
|
],
|
|
671
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
671
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
672
672
|
async execute(args, ctx) {
|
|
673
673
|
return executeUploadRunBasic(args, ctx);
|
|
674
674
|
},
|
|
@@ -693,7 +693,7 @@ export const programRunnersModule = defineToolModule({
|
|
|
693
693
|
"Use when the user requests to run new 6502 code; surface any assembler diagnostics in your reply.",
|
|
694
694
|
"Mention the entry routine or important addresses after execution so the user can continue debugging.",
|
|
695
695
|
],
|
|
696
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
696
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
697
697
|
async execute(args, ctx) {
|
|
698
698
|
return executeUploadRunAsm(args, ctx);
|
|
699
699
|
},
|
|
@@ -717,7 +717,7 @@ export const programRunnersModule = defineToolModule({
|
|
|
717
717
|
"Stage PRG files without running when the user wants to inspect memory first.",
|
|
718
718
|
"Confirm the Ultimate filesystem path (e.g. //USB0/demo.prg) is accessible before invoking.",
|
|
719
719
|
],
|
|
720
|
-
supportedPlatforms: ["c64u"],
|
|
720
|
+
supportedPlatforms: ["c64u", "u2"],
|
|
721
721
|
async execute(args, ctx) {
|
|
722
722
|
return executeLoadPrg(args, ctx);
|
|
723
723
|
},
|
|
@@ -733,7 +733,7 @@ export const programRunnersModule = defineToolModule({
|
|
|
733
733
|
"Call when the user provides a PRG path and expects immediate execution without compiling.",
|
|
734
734
|
"Mention that firmware issues a RUN so the user knows the machine state changed.",
|
|
735
735
|
],
|
|
736
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
736
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
737
737
|
prerequisites: ["drives_list"],
|
|
738
738
|
examples: [
|
|
739
739
|
{
|
|
@@ -765,7 +765,7 @@ export const programRunnersModule = defineToolModule({
|
|
|
765
765
|
arguments: { path: "//USB0/game.crt" },
|
|
766
766
|
},
|
|
767
767
|
],
|
|
768
|
-
supportedPlatforms: ["c64u"],
|
|
768
|
+
supportedPlatforms: ["c64u", "u2"],
|
|
769
769
|
async execute(args, ctx) {
|
|
770
770
|
try {
|
|
771
771
|
const parsed = crtFileArgsSchema.parse(args ?? {});
|
package/dist/tools/rag.js
CHANGED
|
@@ -181,7 +181,7 @@ export const ragModule = defineToolModule({
|
|
|
181
181
|
],
|
|
182
182
|
prompts: ["basic-program", "assembly-program"],
|
|
183
183
|
defaultTags: ["rag", "search"],
|
|
184
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
184
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
185
185
|
workflowHints: [
|
|
186
186
|
"Call RAG tools when the user needs references or examples before generating new code.",
|
|
187
187
|
"Summarise the number of refs returned and suggest follow-up actions like reading specific docs.",
|
|
@@ -35,7 +35,7 @@ const executeBatchArgsSchema = objectSchema({
|
|
|
35
35
|
export const batchModuleGroup = defineToolModule({
|
|
36
36
|
domain: "batch",
|
|
37
37
|
summary: "Execute multiple tool calls in a single MCP request for reduced round-trip overhead.",
|
|
38
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
38
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
39
39
|
resources: [],
|
|
40
40
|
prompts: [],
|
|
41
41
|
defaultTags: ["batch", "orchestration"],
|
|
@@ -154,7 +154,7 @@ export const configModuleGroup = defineToolModule({
|
|
|
154
154
|
resources: ["c64://guide/bootstrap", "c64://guide/index"],
|
|
155
155
|
prompts: ["memory-debug"],
|
|
156
156
|
defaultTags: ["config", "diagnostics"],
|
|
157
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
157
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
158
158
|
workflowHints: [
|
|
159
159
|
"List categories before changing values so users can confirm firmware-provided names.",
|
|
160
160
|
"Mention when operations persist to flash or modify debug registers to highlight impacts.",
|
|
@@ -171,12 +171,12 @@ export const configModuleGroup = defineToolModule({
|
|
|
171
171
|
}),
|
|
172
172
|
tags: ["config", "diagnostics", "grouped"],
|
|
173
173
|
operationPlatforms: {
|
|
174
|
-
load_flash: ["c64u"],
|
|
175
|
-
save_flash: ["c64u"],
|
|
176
|
-
reset_defaults: ["c64u"],
|
|
174
|
+
load_flash: ["c64u", "u2"],
|
|
175
|
+
save_flash: ["c64u", "u2"],
|
|
176
|
+
reset_defaults: ["c64u", "u2"],
|
|
177
177
|
read_debugreg: ["c64u"],
|
|
178
178
|
write_debugreg: ["c64u"],
|
|
179
|
-
shuffle: ["c64u"],
|
|
179
|
+
shuffle: ["c64u", "u2"],
|
|
180
180
|
},
|
|
181
181
|
operationToolNames: {
|
|
182
182
|
load_flash: "config_load_from_flash",
|
|
@@ -251,7 +251,7 @@ const diskOperationHandlers = createOperationHandlers(diskOperations);
|
|
|
251
251
|
export const diskModuleGroup = defineToolModule({
|
|
252
252
|
domain: "storage",
|
|
253
253
|
summary: "Grouped disk image management, mounting, and discovery tools.",
|
|
254
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
254
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
255
255
|
resources: ["c64://guide/bootstrap"],
|
|
256
256
|
prompts: ["drive-management"],
|
|
257
257
|
defaultTags: ["storage", "drive"],
|
|
@@ -269,7 +269,7 @@ export const diskModuleGroup = defineToolModule({
|
|
|
269
269
|
variants: diskOperations.map((operation) => operation.schema),
|
|
270
270
|
}),
|
|
271
271
|
tags: ["storage", "drive", "grouped"],
|
|
272
|
-
operationPlatforms: { file_info: ["c64u"], create_image: ["c64u"], find_and_run: ["c64u"] },
|
|
272
|
+
operationPlatforms: { file_info: ["c64u", "u2"], create_image: ["c64u", "u2"], find_and_run: ["c64u", "u2"] },
|
|
273
273
|
operationToolNames: {
|
|
274
274
|
file_info: "file_info",
|
|
275
275
|
create_image: "create_image",
|
|
@@ -33,7 +33,7 @@ const driveOperationHandlers = createOperationHandlers(driveOperations);
|
|
|
33
33
|
export const driveModuleGroup = defineToolModule({
|
|
34
34
|
domain: "drive",
|
|
35
35
|
summary: "Grouped drive power, reset, ROM, and mode helpers.",
|
|
36
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
36
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
37
37
|
resources: ["c64://guide/bootstrap"],
|
|
38
38
|
defaultTags: ["drive", "hardware"],
|
|
39
39
|
workflowHints: [
|
|
@@ -50,7 +50,7 @@ export const driveModuleGroup = defineToolModule({
|
|
|
50
50
|
variants: driveOperations.map((operation) => operation.schema),
|
|
51
51
|
}),
|
|
52
52
|
tags: ["drive", "hardware", "grouped"],
|
|
53
|
-
operationPlatforms: { load_rom: ["c64u"] },
|
|
53
|
+
operationPlatforms: { load_rom: ["c64u", "u2"] },
|
|
54
54
|
operationToolNames: { load_rom: "drive_load_rom" },
|
|
55
55
|
examples: [
|
|
56
56
|
{
|
|
@@ -89,7 +89,7 @@ function parseVicState(d011, d016, d018, dd00, d020, d021) {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
const getDisplayStateArgsSchema = objectSchema({
|
|
92
|
-
description: "Read VIC-II and CIA2 registers to determine the current graphics mode and memory layout.
|
|
92
|
+
description: "Read VIC-II and CIA2 registers to determine the current graphics mode and memory layout. The same shared-memory path is used on C64U/U64, U2-family hardware, and VICE, so the response shape is identical.",
|
|
93
93
|
properties: {
|
|
94
94
|
op: literalSchema("get_display_state"),
|
|
95
95
|
},
|
|
@@ -212,7 +212,7 @@ export const graphicsModuleGroup = defineToolModule({
|
|
|
212
212
|
"Use render helpers for PETSCII text, PETSCII art, sprites, and bitmaps; clarify whether PETSCII art executed or stayed a dry run.",
|
|
213
213
|
"Mention sprite positions/colours so follow-up memory inspection stays grounded.",
|
|
214
214
|
],
|
|
215
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
215
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
216
216
|
tools: [
|
|
217
217
|
{
|
|
218
218
|
name: "c64_graphics",
|
|
@@ -27,7 +27,7 @@ const memoryOperations = [
|
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
op: "disassemble",
|
|
30
|
-
schema: extendSchemaWithOp("disassemble", disassembleArgsSchema.jsonSchema, { description: "Disassemble a memory region into annotated 6502/6510 instructions, including undocumented opcodes with canonical names. Symbol annotations from `.vs` files are applied when available. Works on
|
|
30
|
+
schema: extendSchemaWithOp("disassemble", disassembleArgsSchema.jsonSchema, { description: "Disassemble a memory region into annotated 6502/6510 instructions, including undocumented opcodes with canonical names. Symbol annotations from `.vs` files are applied when available. Works on C64U/U64, U2-family hardware, and VICE." }),
|
|
31
31
|
handler: groupedMemoryHandlers.disassemble,
|
|
32
32
|
},
|
|
33
33
|
{
|
|
@@ -60,7 +60,7 @@ const memoryOperationHandlers = createOperationHandlers(memoryOperations);
|
|
|
60
60
|
export const memoryModuleGroup = defineToolModule({
|
|
61
61
|
domain: "memory",
|
|
62
62
|
summary: "Grouped memory, screen, and polling operations.",
|
|
63
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
63
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
64
64
|
resources: ["c64://guide/bootstrap", "c64://basic/spec", "c64://assembly/6510-spec"],
|
|
65
65
|
prompts: ["memory-debug", "basic-program", "assembly-program"],
|
|
66
66
|
defaultTags: ["memory", "debug"],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describePlatformCapabilities } from "../../platform.js";
|
|
2
|
+
import { cancelGeneratedSidPlayback } from "../audio.js";
|
|
2
3
|
import { debugModuleGroup as debugModule } from "../debug.js";
|
|
3
4
|
import { viceModuleGroup as viceModule } from "../vice.js";
|
|
4
5
|
import { jsonResult } from "../responses.js";
|
|
@@ -50,7 +51,7 @@ const selectBackendSchema = operationSchema("select", {
|
|
|
50
51
|
properties: {
|
|
51
52
|
backend: {
|
|
52
53
|
type: "string",
|
|
53
|
-
enum: ["c64u", "vice"],
|
|
54
|
+
enum: ["c64u", "u2", "vice"],
|
|
54
55
|
description: "Backend to activate for subsequent tool calls.",
|
|
55
56
|
},
|
|
56
57
|
},
|
|
@@ -73,17 +74,20 @@ const platformOperationHandlers = {
|
|
|
73
74
|
isError: true,
|
|
74
75
|
};
|
|
75
76
|
}
|
|
77
|
+
cancelGeneratedSidPlayback(); // A backend switch is the documented cue to stop it rather than let it keep playing on the old backend.
|
|
76
78
|
ctx.client.switchBackend(backend);
|
|
77
79
|
ctx.setPlatform(backend);
|
|
78
80
|
const { availableTools, unavailableTools } = describeTargetPlatform(backend);
|
|
79
|
-
const switchBackTarget =
|
|
81
|
+
const switchBackTarget = availableBackends.find((candidate) => candidate !== backend);
|
|
80
82
|
const data = {
|
|
81
83
|
success: true,
|
|
82
84
|
activeBackend: backend,
|
|
83
85
|
configuredBackends: availableBackends,
|
|
84
86
|
availableTools,
|
|
85
87
|
unavailableTools,
|
|
86
|
-
usageHint:
|
|
88
|
+
usageHint: switchBackTarget
|
|
89
|
+
? `Use c64_select_backend again with backend: "${switchBackTarget}" to switch back.`
|
|
90
|
+
: "No other backend is configured.",
|
|
87
91
|
};
|
|
88
92
|
return jsonResult(data, { success: true });
|
|
89
93
|
},
|
|
@@ -91,7 +95,7 @@ const platformOperationHandlers = {
|
|
|
91
95
|
export const platformModuleGroup = defineToolModule({
|
|
92
96
|
domain: "platform",
|
|
93
97
|
summary: "Runtime backend selection and platform-status coordination.",
|
|
94
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
98
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
95
99
|
resources: ["c64://platform/status", "c64://guide/bootstrap"],
|
|
96
100
|
prompts: [],
|
|
97
101
|
defaultTags: ["platform", "backend", "grouped"],
|
|
@@ -101,7 +105,7 @@ export const platformModuleGroup = defineToolModule({
|
|
|
101
105
|
tools: [
|
|
102
106
|
{
|
|
103
107
|
name: "c64_select_backend",
|
|
104
|
-
description: "Switch the active backend between C64U hardware and the VICE emulator at runtime.",
|
|
108
|
+
description: "Switch the active backend between C64U/U64 hardware, U2-family cartridges, and the VICE emulator at runtime.",
|
|
105
109
|
summary: "Changes the active backend without restarting the MCP server and reports resulting tool availability.",
|
|
106
110
|
inputSchema: discriminatedUnionSchema({
|
|
107
111
|
description: "Runtime backend selection operations.",
|
|
@@ -116,6 +116,7 @@ function invokePrintTool(toolName, rawArgs, ctx) {
|
|
|
116
116
|
}
|
|
117
117
|
export const printerModuleGroup = defineToolModule({
|
|
118
118
|
domain: "printer",
|
|
119
|
+
supportedPlatforms: ["c64u", "u2"],
|
|
119
120
|
summary: "Grouped printer text, bitmap, and character definition helpers.",
|
|
120
121
|
resources: ["c64://guide/bootstrap"],
|
|
121
122
|
defaultTags: ["printer", "device"],
|
|
@@ -12,7 +12,7 @@ const programOperations = [
|
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
op: "run_prg",
|
|
15
|
-
schema: extendSchemaWithOp("run_prg", ensureDescriptor(programDescriptorIndex, "run_prg").inputSchema, { description: "Load and execute a PRG from Ultimate-visible storage on
|
|
15
|
+
schema: extendSchemaWithOp("run_prg", ensureDescriptor(programDescriptorIndex, "run_prg").inputSchema, { description: "Load and execute a PRG from Ultimate-visible storage on C64U/U64 or U2-family hardware, or from a host-local path on VICE." }),
|
|
16
16
|
handler: async (rawArgs, ctx) => invokeModuleTool(programRunnersModule, "run_prg", rawArgs, ctx),
|
|
17
17
|
},
|
|
18
18
|
{
|
|
@@ -55,10 +55,10 @@ export const programModule = defineToolModule({
|
|
|
55
55
|
defaultTags: ["programs", "execution"],
|
|
56
56
|
workflowHints: [
|
|
57
57
|
"Choose BASIC or assembly uploaders based on the language you just generated for the user.",
|
|
58
|
-
"Prefer PRG or CRT runners when the user supplies a file path instead of source text; PRG paths are host-local on VICE and Ultimate-visible on
|
|
59
|
-
"For a quick visible confirmation on VICE
|
|
58
|
+
"Prefer PRG or CRT runners when the user supplies a file path instead of source text; PRG paths are host-local on VICE and Ultimate-visible on hardware.",
|
|
59
|
+
"For a quick visible confirmation on VICE, C64U/U64, or U2-family hardware, prefer cross_platform_greeting instead of composing manual backend switches and BASIC upload steps.",
|
|
60
60
|
],
|
|
61
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
61
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
62
62
|
tools: [
|
|
63
63
|
{
|
|
64
64
|
name: "c64_program",
|
|
@@ -69,7 +69,7 @@ export const programModule = defineToolModule({
|
|
|
69
69
|
variants: programOperations.map((operation) => operation.schema),
|
|
70
70
|
}),
|
|
71
71
|
tags: ["programs", "execution", "grouped"],
|
|
72
|
-
operationPlatforms: { load_prg: ["c64u"], run_crt: ["c64u"], bundle_run: ["c64u"] },
|
|
72
|
+
operationPlatforms: { load_prg: ["c64u", "u2"], run_crt: ["c64u", "u2"], bundle_run: ["c64u", "u2"] },
|
|
73
73
|
operationToolNames: {
|
|
74
74
|
load_prg: "load_prg",
|
|
75
75
|
run_crt: "run_crt",
|
|
@@ -25,7 +25,7 @@ export const ragModuleGroup = defineToolModule({
|
|
|
25
25
|
"Use BASIC retrieval before synthesising new BASIC code and mention primary resources in responses.",
|
|
26
26
|
"For assembly, note registers or addresses surfaced so the user can inspect them further.",
|
|
27
27
|
],
|
|
28
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
28
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
29
29
|
tools: [
|
|
30
30
|
{
|
|
31
31
|
name: "c64_rag",
|
|
@@ -139,7 +139,7 @@ const soundOperationHandlers = createOperationHandlers(soundOperations);
|
|
|
139
139
|
export const soundModuleGroup = defineToolModule({
|
|
140
140
|
domain: "audio",
|
|
141
141
|
summary: "Grouped SID control, playback, composition, and analysis operations.",
|
|
142
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
142
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
143
143
|
resources: ["c64://sound/sid/spec", "c64://sound/sidwave/spec", "c64://sound/sid/file-format"],
|
|
144
144
|
prompts: ["sid-music"],
|
|
145
145
|
defaultTags: ["sid", "audio"],
|
|
@@ -159,9 +159,9 @@ export const soundModuleGroup = defineToolModule({
|
|
|
159
159
|
tags: ["sid", "audio", "grouped"],
|
|
160
160
|
operationPlatforms: {
|
|
161
161
|
capture_samples: ["c64u"],
|
|
162
|
-
play_sid_file: ["c64u"],
|
|
163
|
-
play_mod_file: ["c64u"],
|
|
164
|
-
pipeline: ["c64u"],
|
|
162
|
+
play_sid_file: ["c64u", "u2"],
|
|
163
|
+
play_mod_file: ["c64u", "u2"],
|
|
164
|
+
pipeline: ["c64u", "u2"],
|
|
165
165
|
analyze: ["c64u"],
|
|
166
166
|
record_analyze: ["c64u"],
|
|
167
167
|
},
|
|
@@ -30,11 +30,21 @@ const systemOperations = [
|
|
|
30
30
|
schema: extendSchemaWithOp("poweroff", ensureDescriptor(machineDescriptorIndex, "poweroff").inputSchema, { description: "Request a controlled shutdown via the Ultimate firmware." }),
|
|
31
31
|
handler: async (rawArgs, ctx) => invokeModuleTool(machineControlModule, "poweroff", rawArgs, ctx),
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
op: "power_cycle",
|
|
35
|
+
schema: extendSchemaWithOp("power_cycle", ensureDescriptor(machineDescriptorIndex, "power_cycle").inputSchema, { description: "Return the active backend to a fresh state. C64U/U64 Tool Menu verification requires machine:menu_screen (a C64U firmware version that provides it, or U64 3.15+); U2-family reboots through REST." }),
|
|
36
|
+
handler: async (rawArgs, ctx) => invokeModuleTool(machineControlModule, "power_cycle", rawArgs, ctx),
|
|
37
|
+
},
|
|
33
38
|
{
|
|
34
39
|
op: "menu",
|
|
35
40
|
schema: extendSchemaWithOp("menu", ensureDescriptor(machineDescriptorIndex, "menu_button").inputSchema, { description: "Toggle the Ultimate menu button for navigation." }),
|
|
36
41
|
handler: async (rawArgs, ctx) => invokeModuleTool(machineControlModule, "menu_button", rawArgs, ctx),
|
|
37
42
|
},
|
|
43
|
+
{
|
|
44
|
+
op: "read_menu_screen",
|
|
45
|
+
schema: extendSchemaWithOp("read_menu_screen", ensureDescriptor(machineDescriptorIndex, "read_menu_screen").inputSchema, { description: "Read the active Ultimate menu's raw character and colour matrix through machine:menu_screen (a C64U firmware version that provides it, or U64/U2 3.15+)." }),
|
|
46
|
+
handler: async (rawArgs, ctx) => invokeModuleTool(machineControlModule, "read_menu_screen", rawArgs, ctx),
|
|
47
|
+
},
|
|
38
48
|
{
|
|
39
49
|
op: "start_task",
|
|
40
50
|
schema: extendSchemaWithOp("start_task", ensureDescriptor(metaDescriptorIndex, "start_background_task").inputSchema, { description: "Start a named background task that runs on an interval." }),
|
|
@@ -65,7 +75,7 @@ const systemOperationHandlers = createOperationHandlers(systemOperations);
|
|
|
65
75
|
export const systemModuleGroup = defineToolModule({
|
|
66
76
|
domain: "system",
|
|
67
77
|
summary: "Grouped machine control and background task orchestration.",
|
|
68
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
78
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
69
79
|
resources: ["c64://guide/bootstrap"],
|
|
70
80
|
prompts: ["memory-debug"],
|
|
71
81
|
defaultTags: ["system", "control"],
|
|
@@ -83,8 +93,8 @@ export const systemModuleGroup = defineToolModule({
|
|
|
83
93
|
variants: systemOperations.map((operation) => operation.schema),
|
|
84
94
|
}),
|
|
85
95
|
tags: ["system", "control", "grouped"],
|
|
86
|
-
operationPlatforms: { pause: ["c64u"], resume: ["c64u"], menu: ["c64u"] },
|
|
87
|
-
operationToolNames: { pause: "pause", resume: "resume", menu: "menu_button" },
|
|
96
|
+
operationPlatforms: { pause: ["c64u", "u2"], resume: ["c64u", "u2"], menu: ["c64u", "u2"], read_menu_screen: ["c64u", "u2"] },
|
|
97
|
+
operationToolNames: { pause: "pause", resume: "resume", menu: "menu_button", read_menu_screen: "read_menu_screen" },
|
|
88
98
|
examples: [
|
|
89
99
|
{
|
|
90
100
|
name: "Soft reset",
|
package/dist/tools/storage.js
CHANGED
|
@@ -143,7 +143,7 @@ const createDnpArgsSchema = objectSchema({
|
|
|
143
143
|
export const storageModule = defineToolModule({
|
|
144
144
|
domain: "storage",
|
|
145
145
|
summary: "Drive management, disk image creation, and file inspection utilities.",
|
|
146
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
146
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
147
147
|
resources: ["c64://guide/bootstrap"],
|
|
148
148
|
prompts: ["memory-debug"],
|
|
149
149
|
defaultTags: ["drive", "storage"],
|
package/dist/vice/mockServer.js
CHANGED
|
@@ -2,6 +2,10 @@ import net from "node:net";
|
|
|
2
2
|
import { asciiToScreenCodes } from "./readiness.js";
|
|
3
3
|
const SCREEN_BASE = 0x0400;
|
|
4
4
|
const SCREEN_SIZE = 40 * 25;
|
|
5
|
+
const KEYBOARD_BUFFER = 0x0277; // KERNAL keyboard buffer base ($0277), where a typed SYS command lands.
|
|
6
|
+
const SYS_COMMAND_PATTERN = /SYS\s*\d+/i; // upload_run_asm enters an assembled program via a typed SYSnnnnn.
|
|
7
|
+
const HEARTBEAT_GRANULARITY_MS = 10; // Well below the poll interval so consecutive liveness reads always differ.
|
|
8
|
+
const HEARTBEAT_DURATION_MS = 1000; // How long a SYS-triggered program keeps "running".
|
|
5
9
|
function buildDefaultRegisterMetadata() {
|
|
6
10
|
return Object.freeze([
|
|
7
11
|
{ id: 0, name: "PC", bits: 16 },
|
|
@@ -30,6 +34,7 @@ export class ViceMockServer {
|
|
|
30
34
|
this.server = null;
|
|
31
35
|
this.memory = new Uint8Array(0x10000);
|
|
32
36
|
this.helloReady = false;
|
|
37
|
+
this.heartbeat = null;
|
|
33
38
|
this.checkpoints = new Map();
|
|
34
39
|
this.nextCheckpointId = 1;
|
|
35
40
|
this.registerMetadata = buildDefaultRegisterMetadata();
|
|
@@ -186,6 +191,7 @@ export class ViceMockServer {
|
|
|
186
191
|
for (let i = 0; i < length; i += 1) {
|
|
187
192
|
payload[2 + i] = this.memory[(start + i) & 0xffff];
|
|
188
193
|
}
|
|
194
|
+
this.applyHeartbeatOverlay(start, length, payload);
|
|
189
195
|
return buildResponse(cmd, reqId, payload);
|
|
190
196
|
}
|
|
191
197
|
case 0x02: { // mem set
|
|
@@ -197,6 +203,12 @@ export class ViceMockServer {
|
|
|
197
203
|
}
|
|
198
204
|
if (start <= SCREEN_BASE && end >= SCREEN_BASE) {
|
|
199
205
|
this.helloReady = false;
|
|
206
|
+
this.heartbeat = null; // A client writing screen RAM has taken over; end any simulated run.
|
|
207
|
+
}
|
|
208
|
+
this.maybeStartAsmHeartbeat(start, end, payload);
|
|
209
|
+
const KEYBOARD_NDX = 0x00c6; // This mock runs no real 6502 code, so simulate the KERNAL IRQ draining $00C6 shortly after each write.
|
|
210
|
+
if (start <= KEYBOARD_NDX && end >= KEYBOARD_NDX) {
|
|
211
|
+
setTimeout(() => { this.memory[KEYBOARD_NDX] = 0; }, 5).unref();
|
|
200
212
|
}
|
|
201
213
|
return buildResponse(cmd, reqId);
|
|
202
214
|
}
|
|
@@ -411,6 +423,7 @@ export class ViceMockServer {
|
|
|
411
423
|
this.memory[0x31] = 0x03;
|
|
412
424
|
this.memory[0x32] = 0x08;
|
|
413
425
|
this.helloReady = false;
|
|
426
|
+
this.heartbeat = null;
|
|
414
427
|
this.checkpoints.clear();
|
|
415
428
|
this.nextCheckpointId = 1;
|
|
416
429
|
this.registerValues.clear();
|
|
@@ -431,6 +444,26 @@ export class ViceMockServer {
|
|
|
431
444
|
hello.copy(Buffer.from(this.memory.buffer, offset, hello.length));
|
|
432
445
|
this.helloReady = true;
|
|
433
446
|
}
|
|
447
|
+
maybeStartAsmHeartbeat(start, end, payload) {
|
|
448
|
+
if (start > KEYBOARD_BUFFER || end < KEYBOARD_BUFFER)
|
|
449
|
+
return; // Only a write covering the KERNAL keyboard buffer can be a typed SYS trigger.
|
|
450
|
+
if (!SYS_COMMAND_PATTERN.test(payload.toString("ascii")))
|
|
451
|
+
return;
|
|
452
|
+
this.heartbeat = { startedAt: Date.now(), baseValue: this.memory[SCREEN_BASE] };
|
|
453
|
+
}
|
|
454
|
+
applyHeartbeatOverlay(start, length, payload) {
|
|
455
|
+
const heartbeat = this.heartbeat;
|
|
456
|
+
if (!heartbeat)
|
|
457
|
+
return;
|
|
458
|
+
const elapsed = Date.now() - heartbeat.startedAt;
|
|
459
|
+
if (elapsed < 0 || elapsed >= HEARTBEAT_DURATION_MS)
|
|
460
|
+
return;
|
|
461
|
+
const offset = SCREEN_BASE - start;
|
|
462
|
+
if (offset < 0 || offset >= length)
|
|
463
|
+
return;
|
|
464
|
+
const ticks = Math.floor(elapsed / HEARTBEAT_GRANULARITY_MS);
|
|
465
|
+
payload[2 + offset] = (heartbeat.baseValue + ticks) & 0xff;
|
|
466
|
+
}
|
|
434
467
|
}
|
|
435
468
|
export async function startViceMockServer(options) {
|
|
436
469
|
const server = new ViceMockServer(options);
|
package/dist/vice/process.js
CHANGED
|
@@ -378,10 +378,20 @@ export async function startViceProcess(options) {
|
|
|
378
378
|
await terminateProcess(xvfb, "SIGKILL", 200);
|
|
379
379
|
}
|
|
380
380
|
};
|
|
381
|
+
const stopSync = () => {
|
|
382
|
+
for (const process of [child, xvfb]) {
|
|
383
|
+
if (!process || process.exitCode !== null || process.signalCode !== null)
|
|
384
|
+
continue;
|
|
385
|
+
try {
|
|
386
|
+
process.kill("SIGKILL");
|
|
387
|
+
}
|
|
388
|
+
catch { }
|
|
389
|
+
}
|
|
390
|
+
};
|
|
381
391
|
child.once("exit", async () => {
|
|
382
392
|
await terminateProcess(xvfb, "SIGTERM", 0);
|
|
383
393
|
});
|
|
384
|
-
return { host: options.host, port: options.port, process: child, stop };
|
|
394
|
+
return { host: options.host, port: options.port, process: child, stop, stopSync };
|
|
385
395
|
}
|
|
386
396
|
function isIgnorableSocketDirError(error) {
|
|
387
397
|
if (!error || typeof error !== "object" || !("code" in error)) {
|