c64bridge 0.7.4 → 0.8.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/.github/prompts/README.md +14 -4
- package/.github/prompts/assembly-routine-session.prompt.md +9 -9
- package/.github/prompts/basic-program-session.prompt.md +11 -9
- package/.github/prompts/drive-management-session.prompt.md +9 -9
- package/.github/prompts/hello-world-session.prompt.md +17 -0
- package/.github/prompts/petscii-art-session.prompt.md +9 -9
- package/.github/prompts/play-classic-game.prompt.md +9 -9
- package/.github/prompts/pr-converge.prompt.md +13 -0
- package/.github/prompts/sid-music-session.prompt.md +9 -9
- package/AGENTS.md +59 -60
- package/README.md +550 -191
- package/data/assembly/assembly-spec.md +290 -1
- package/data/context/bootstrap.md +26 -2
- package/data/context/fast-paths.md +44 -0
- package/data/graphics/petscii-style-guide.md +6 -2
- package/data/graphics/sprite-charset-best-practices.md +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +990 -26
- package/dist/config.js +5 -1
- package/dist/device.js +680 -57
- package/dist/diagnostics.js +371 -0
- package/dist/mcp/metadata.js +39 -0
- package/dist/mcp-server.js +170 -20
- package/dist/platform.js +10 -2
- package/dist/prompts/registry.js +166 -309
- package/dist/rag/init.js +37 -1
- package/dist/rag/knowledgeIndex.js +18 -6
- package/dist/rag/rateLimiter.js +2 -2
- package/dist/streamCapture.js +135 -0
- package/dist/tools/audio.js +3 -1
- package/dist/tools/debug.js +470 -0
- package/dist/tools/developer.js +10 -4
- package/dist/tools/graphics.js +183 -17
- package/dist/tools/machineControl.js +11 -5
- package/dist/tools/memory.js +19 -10
- package/dist/tools/meta/audio.js +240 -1
- package/dist/tools/meta/config.js +20 -28
- package/dist/tools/meta/configInventory.js +104 -0
- package/dist/tools/meta/diagnostics.js +45 -2
- package/dist/tools/meta/graphics.js +14 -6
- package/dist/tools/meta/index.js +1 -0
- package/dist/tools/meta/memory.js +20 -6
- package/dist/tools/meta/program.js +473 -0
- package/dist/tools/pollValidator.js +4 -2
- package/dist/tools/programRunners.js +8 -4
- package/dist/tools/registry/config.js +17 -0
- package/dist/tools/registry/disk.js +7 -0
- package/dist/tools/registry/drive.js +3 -0
- package/dist/tools/registry/graphics.js +111 -27
- package/dist/tools/registry/index.js +6 -0
- package/dist/tools/registry/memory.js +1 -0
- package/dist/tools/registry/platform.js +121 -0
- package/dist/tools/registry/program.js +26 -8
- package/dist/tools/registry/sound.js +96 -0
- package/dist/tools/registry/system.js +15 -2
- package/dist/tools/storage.js +1 -0
- package/dist/tools/streaming.js +1 -0
- package/dist/tools/types.js +38 -4
- package/dist/tools/vice.js +160 -0
- package/dist/vicBitmap.js +317 -0
- package/dist/vice/mockServer.js +439 -0
- package/dist/vice/process.js +310 -0
- package/dist/vice/readiness.js +18 -2
- package/dist/vice/viceClient.js +382 -22
- package/doc/developer.md +71 -25
- package/doc/img/backend-switch/hello-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice-and-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice.png +0 -0
- package/doc/img/backend-switch/metadata.json +50 -0
- package/doc/plans/vice-backend-switch/plan.md +486 -0
- package/doc/plans/vice-backend-switch-2/plan.md +48 -0
- package/doc/plans/vice-support/notes.md +1 -0
- package/doc/plans/vice-support/plan.md +5 -0
- package/doc/plans/vice-support/rollout.md +23 -17
- package/doc/reviews/vice-support-review.md +185 -0
- package/doc/troubleshooting-mcp.md +35 -6
- package/doc/vice/support-matrix.md +54 -0
- package/mcp/prompts.json +292 -0
- package/mcp/protocol-examples.json +46 -0
- package/mcp/resources.json +940 -0
- package/mcp/schemas/c64_config.schema.json +350 -0
- package/mcp/schemas/c64_debug.schema.json +380 -0
- package/mcp/schemas/c64_disk.schema.json +278 -0
- package/mcp/schemas/c64_drive.schema.json +120 -0
- package/mcp/schemas/c64_extract.schema.json +302 -0
- package/mcp/schemas/c64_graphics.schema.json +333 -0
- package/mcp/schemas/c64_memory.schema.json +155 -0
- package/mcp/schemas/c64_printer.schema.json +218 -0
- package/mcp/schemas/c64_program.schema.json +391 -0
- package/mcp/schemas/c64_rag.schema.json +67 -0
- package/mcp/schemas/c64_select_backend.schema.json +32 -0
- package/mcp/schemas/c64_sound.schema.json +644 -0
- package/mcp/schemas/c64_stream.schema.json +61 -0
- package/mcp/schemas/c64_system.schema.json +227 -0
- package/mcp/schemas/c64_vice.schema.json +63 -0
- package/mcp/server.json +15 -0
- package/mcp/tools.json +4517 -0
- package/mcp.json +90 -15
- package/package.json +11 -4
- package/scripts/capture-backend-switch-screenshots.mjs +276 -0
- package/scripts/generate-mcp-interface.ts +289 -0
- package/scripts/invoke-bun.mjs +67 -21
- package/scripts/mockC64Server.mjs +162 -6
- package/scripts/prepare-release.mjs +59 -2
- package/scripts/run-coverage.mjs +284 -0
- package/scripts/run-tests.mjs +95 -39
- package/scripts/run-tests.ts +378 -73
- package/scripts/update-readme.ts +147 -13
- package/server.json +94 -0
|
@@ -5,6 +5,178 @@ import { promises as fs } from "node:fs";
|
|
|
5
5
|
import { resolve as resolvePath, join as joinPath } from "node:path";
|
|
6
6
|
import { sleep, formatTimestampSpec } from "./util.js";
|
|
7
7
|
import { getTasksHomeDir } from "./background.js";
|
|
8
|
+
import { Jimp } from "jimp";
|
|
9
|
+
import { withDiagnosticSpan } from "../../diagnostics.js";
|
|
10
|
+
const C64_SCREENSHOT_PALETTE = [
|
|
11
|
+
0x000000ff,
|
|
12
|
+
0xffffffff,
|
|
13
|
+
0x813338ff,
|
|
14
|
+
0x75cec8ff,
|
|
15
|
+
0x8e3c97ff,
|
|
16
|
+
0x56ac4dff,
|
|
17
|
+
0x2e2c9bff,
|
|
18
|
+
0xedf171ff,
|
|
19
|
+
0x8e5029ff,
|
|
20
|
+
0x553800ff,
|
|
21
|
+
0xc46c71ff,
|
|
22
|
+
0x4a4a4aff,
|
|
23
|
+
0x7b7b7bff,
|
|
24
|
+
0xa9ff9fff,
|
|
25
|
+
0x706debff,
|
|
26
|
+
0xb2b2b2ff,
|
|
27
|
+
];
|
|
28
|
+
function hasOwnProperty(value, key) {
|
|
29
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
30
|
+
}
|
|
31
|
+
function isVisibleViceSessionPreferred() {
|
|
32
|
+
const visible = String(process.env.VICE_VISIBLE ?? "true").trim().toLowerCase();
|
|
33
|
+
const forceXvfb = String(process.env.FORCE_XVFB ?? "0").trim().toLowerCase();
|
|
34
|
+
const disableXvfb = String(process.env.DISABLE_XVFB ?? "0").trim().toLowerCase();
|
|
35
|
+
if (forceXvfb === "1" || forceXvfb === "true" || forceXvfb === "yes") {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (disableXvfb === "1" || disableXvfb === "true" || disableXvfb === "yes") {
|
|
39
|
+
return visible !== "0" && visible !== "false" && visible !== "no";
|
|
40
|
+
}
|
|
41
|
+
return visible !== "0" && visible !== "false" && visible !== "no";
|
|
42
|
+
}
|
|
43
|
+
function shouldUseVisibleViceGreetingFastPath(rawArgs, requestedBackends) {
|
|
44
|
+
if (!rawArgs || typeof rawArgs !== "object" || Array.isArray(rawArgs)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (requestedBackends.length !== 1 || requestedBackends[0] !== "vice") {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (!isVisibleViceSessionPreferred()) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const args = rawArgs;
|
|
54
|
+
return !hasOwnProperty(args, "verify")
|
|
55
|
+
&& !hasOwnProperty(args, "captureScreenshot")
|
|
56
|
+
&& !hasOwnProperty(args, "outputPath");
|
|
57
|
+
}
|
|
58
|
+
function canonicalGreetingBackends() {
|
|
59
|
+
return ["vice", "c64u"];
|
|
60
|
+
}
|
|
61
|
+
function uniqueGreetingBackends(backends) {
|
|
62
|
+
return backends.filter((backend, index) => backends.indexOf(backend) === index);
|
|
63
|
+
}
|
|
64
|
+
function normaliseScreenText(screen) {
|
|
65
|
+
return screen.replace(/\r\n?/g, "\n").replace(/\s+/g, " ").trim().toUpperCase();
|
|
66
|
+
}
|
|
67
|
+
function screenContainsExpectedText(screen, expectedText) {
|
|
68
|
+
return normaliseScreenText(screen).includes(normaliseScreenText(expectedText));
|
|
69
|
+
}
|
|
70
|
+
function applyGreetingTemplate(template, backend) {
|
|
71
|
+
const upper = backend.toUpperCase();
|
|
72
|
+
const lower = backend.toLowerCase();
|
|
73
|
+
return template
|
|
74
|
+
.replace(/\{PLATFORM\}/g, upper)
|
|
75
|
+
.replace(/\{BACKEND\}/g, upper)
|
|
76
|
+
.replace(/\{platform\}/g, lower)
|
|
77
|
+
.replace(/\{backend\}/g, lower);
|
|
78
|
+
}
|
|
79
|
+
function escapeBasicString(value) {
|
|
80
|
+
return value.replace(/"/g, '""');
|
|
81
|
+
}
|
|
82
|
+
function buildGreetingProgram(message) {
|
|
83
|
+
return [
|
|
84
|
+
"10 PRINT CHR$(147)",
|
|
85
|
+
`20 PRINT \"${escapeBasicString(message)}\"`,
|
|
86
|
+
"30 END",
|
|
87
|
+
].join("\n");
|
|
88
|
+
}
|
|
89
|
+
function analyseCapturedFrame(frame) {
|
|
90
|
+
const totalPixels = Math.max(1, frame.width * frame.height);
|
|
91
|
+
let nonBackgroundPixels = 0;
|
|
92
|
+
const uniqueColors = new Set();
|
|
93
|
+
if (frame.bitsPerPixel === 4 || frame.bitsPerPixel === 8) {
|
|
94
|
+
for (let index = 0; index < totalPixels; index += 1) {
|
|
95
|
+
const value = frame.pixels[index] ?? 0;
|
|
96
|
+
uniqueColors.add(value);
|
|
97
|
+
if (value !== 0) {
|
|
98
|
+
nonBackgroundPixels += 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const bytesPerPixel = frame.bitsPerPixel >= 24 && frame.bitsPerPixel % 8 === 0
|
|
104
|
+
? frame.bitsPerPixel / 8
|
|
105
|
+
: 0;
|
|
106
|
+
if (bytesPerPixel > 0) {
|
|
107
|
+
for (let index = 0; index < totalPixels; index += 1) {
|
|
108
|
+
const offset = index * bytesPerPixel;
|
|
109
|
+
const red = frame.pixels[offset] ?? 0;
|
|
110
|
+
const green = frame.pixels[offset + 1] ?? 0;
|
|
111
|
+
const blue = frame.pixels[offset + 2] ?? 0;
|
|
112
|
+
const alpha = bytesPerPixel >= 4 ? (frame.pixels[offset + 3] ?? 255) : 255;
|
|
113
|
+
uniqueColors.add(((red & 0xff) << 24) | ((green & 0xff) << 16) | ((blue & 0xff) << 8) | (alpha & 0xff));
|
|
114
|
+
if (red !== 0 || green !== 0 || blue !== 0) {
|
|
115
|
+
nonBackgroundPixels += 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const nonBackgroundRatio = Number((nonBackgroundPixels / totalPixels).toFixed(4));
|
|
121
|
+
return {
|
|
122
|
+
width: frame.width,
|
|
123
|
+
height: frame.height,
|
|
124
|
+
bitsPerPixel: frame.bitsPerPixel,
|
|
125
|
+
complete: frame.complete,
|
|
126
|
+
totalPixels,
|
|
127
|
+
nonBackgroundPixels,
|
|
128
|
+
nonBackgroundRatio,
|
|
129
|
+
uniqueColorCount: uniqueColors.size,
|
|
130
|
+
looksNonBlank: nonBackgroundPixels >= Math.max(1, Math.floor(totalPixels * 0.005)),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function indexedFrameColour(value) {
|
|
134
|
+
return C64_SCREENSHOT_PALETTE[value & 0x0f] ?? C64_SCREENSHOT_PALETTE[0];
|
|
135
|
+
}
|
|
136
|
+
function rgbaToJimpColour(red, green, blue, alpha = 255) {
|
|
137
|
+
return (((red & 0xff) << 24) | ((green & 0xff) << 16) | ((blue & 0xff) << 8) | (alpha & 0xff)) >>> 0;
|
|
138
|
+
}
|
|
139
|
+
async function writeCapturedFramePng(frame, filePath) {
|
|
140
|
+
const image = new Jimp({ width: frame.width, height: frame.height, color: C64_SCREENSHOT_PALETTE[0] });
|
|
141
|
+
const totalPixels = frame.width * frame.height;
|
|
142
|
+
if (frame.bitsPerPixel === 4 || frame.bitsPerPixel === 8) {
|
|
143
|
+
for (let index = 0; index < totalPixels; index += 1) {
|
|
144
|
+
image.setPixelColor(indexedFrameColour(frame.pixels[index] ?? 0), index % frame.width, Math.floor(index / frame.width));
|
|
145
|
+
}
|
|
146
|
+
await image.write(filePath);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const bytesPerPixel = frame.bitsPerPixel >= 24 && frame.bitsPerPixel % 8 === 0
|
|
150
|
+
? frame.bitsPerPixel / 8
|
|
151
|
+
: 0;
|
|
152
|
+
for (let index = 0; index < totalPixels; index += 1) {
|
|
153
|
+
const x = index % frame.width;
|
|
154
|
+
const y = Math.floor(index / frame.width);
|
|
155
|
+
if (bytesPerPixel === 3 || bytesPerPixel === 4) {
|
|
156
|
+
const offset = index * bytesPerPixel;
|
|
157
|
+
image.setPixelColor(rgbaToJimpColour(frame.pixels[offset] ?? 0, frame.pixels[offset + 1] ?? 0, frame.pixels[offset + 2] ?? 0, bytesPerPixel === 4 ? (frame.pixels[offset + 3] ?? 255) : 255), x, y);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const value = frame.pixels[index] ?? 0;
|
|
161
|
+
image.setPixelColor(rgbaToJimpColour(value, value, value), x, y);
|
|
162
|
+
}
|
|
163
|
+
await image.write(filePath);
|
|
164
|
+
}
|
|
165
|
+
async function waitForGreetingScreen(ctx, expectedText, timeoutMs, pollIntervalMs) {
|
|
166
|
+
const deadline = Date.now() + timeoutMs;
|
|
167
|
+
let lastScreen = "";
|
|
168
|
+
while (Date.now() <= deadline) {
|
|
169
|
+
lastScreen = await ctx.client.readScreen();
|
|
170
|
+
if (screenContainsExpectedText(lastScreen, expectedText)) {
|
|
171
|
+
return { matched: true, screen: lastScreen };
|
|
172
|
+
}
|
|
173
|
+
if (Date.now() + pollIntervalMs > deadline) {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
await sleep(pollIntervalMs);
|
|
177
|
+
}
|
|
178
|
+
return { matched: false, screen: lastScreen };
|
|
179
|
+
}
|
|
8
180
|
const programShuffleArgsSchema = objectSchema({
|
|
9
181
|
description: "Discover PRG/CRT files under root path, run each for a duration, capture screen, then reset.",
|
|
10
182
|
properties: {
|
|
@@ -49,7 +221,308 @@ const batchRunWithAssertionsArgsSchema = objectSchema({
|
|
|
49
221
|
required: ["programs"],
|
|
50
222
|
additionalProperties: false,
|
|
51
223
|
});
|
|
224
|
+
const crossPlatformGreetingArgsSchema = objectSchema({
|
|
225
|
+
description: "Show a platform-customized greeting on one or more backends, capture screenshots, and verify the result.",
|
|
226
|
+
properties: {
|
|
227
|
+
platforms: optionalSchema(arraySchema(stringSchema({
|
|
228
|
+
description: "Backends to target in sequence.",
|
|
229
|
+
enum: ["vice", "c64u"],
|
|
230
|
+
minLength: 1,
|
|
231
|
+
})), ["vice", "c64u"]),
|
|
232
|
+
messageTemplate: optionalSchema(stringSchema({
|
|
233
|
+
description: "Greeting template. Use {PLATFORM}/{BACKEND} for uppercase or {platform}/{backend} for lowercase substitution.",
|
|
234
|
+
minLength: 1,
|
|
235
|
+
}), "HAVE A GREAT DAY, {PLATFORM}!"),
|
|
236
|
+
verify: optionalSchema(booleanSchema({
|
|
237
|
+
description: "Poll the text screen and require the rendered greeting to appear.",
|
|
238
|
+
default: true,
|
|
239
|
+
}), true),
|
|
240
|
+
captureScreenshot: optionalSchema(booleanSchema({
|
|
241
|
+
description: "Capture a framebuffer screenshot and save it as a PNG per backend.",
|
|
242
|
+
default: true,
|
|
243
|
+
}), true),
|
|
244
|
+
outputPath: optionalSchema(stringSchema({
|
|
245
|
+
description: "Directory for screenshots and the workflow summary JSON.",
|
|
246
|
+
minLength: 1,
|
|
247
|
+
})),
|
|
248
|
+
restoreActiveBackend: optionalSchema(booleanSchema({
|
|
249
|
+
description: "Restore the backend that was active before the workflow started.",
|
|
250
|
+
default: true,
|
|
251
|
+
}), true),
|
|
252
|
+
timeoutMs: optionalSchema(numberSchema({
|
|
253
|
+
description: "Maximum time to wait for each greeting to appear on screen.",
|
|
254
|
+
integer: true,
|
|
255
|
+
minimum: 100,
|
|
256
|
+
maximum: 5000,
|
|
257
|
+
default: 1500,
|
|
258
|
+
}), 1500),
|
|
259
|
+
pollIntervalMs: optionalSchema(numberSchema({
|
|
260
|
+
description: "Delay between screen polls while verifying the greeting.",
|
|
261
|
+
integer: true,
|
|
262
|
+
minimum: 50,
|
|
263
|
+
maximum: 1000,
|
|
264
|
+
default: 100,
|
|
265
|
+
}), 100),
|
|
266
|
+
},
|
|
267
|
+
required: [],
|
|
268
|
+
additionalProperties: false,
|
|
269
|
+
});
|
|
52
270
|
export const tools = [
|
|
271
|
+
{
|
|
272
|
+
name: "cross_platform_greeting",
|
|
273
|
+
description: "Render a customized greeting on VICE and/or C64U, capture screenshots, and verify both results in one workflow.",
|
|
274
|
+
summary: "One-call cross-platform greeting demo with backend switching, screenshot capture, and text-screen verification.",
|
|
275
|
+
inputSchema: crossPlatformGreetingArgsSchema.jsonSchema,
|
|
276
|
+
tags: ["orchestration", "demo", "screenshots", "verification"],
|
|
277
|
+
examples: [
|
|
278
|
+
{
|
|
279
|
+
name: "Greet both backends",
|
|
280
|
+
description: "Show a platform-specific greeting on VICE and C64U in one call.",
|
|
281
|
+
arguments: { op: "cross_platform_greeting" },
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "Custom template on VICE",
|
|
285
|
+
description: "Render a custom greeting only on the emulator.",
|
|
286
|
+
arguments: {
|
|
287
|
+
op: "cross_platform_greeting",
|
|
288
|
+
platforms: ["vice"],
|
|
289
|
+
messageTemplate: "HELLO FROM {PLATFORM}!",
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
async execute(args, ctx) {
|
|
294
|
+
try {
|
|
295
|
+
const toolStartedAt = Date.now();
|
|
296
|
+
const parsed = crossPlatformGreetingArgsSchema.parse(args ?? {});
|
|
297
|
+
const availableBackends = uniqueGreetingBackends(canonicalGreetingBackends().filter((backend) => ctx.client.getAvailableBackends().includes(backend)));
|
|
298
|
+
const requestedBackends = uniqueGreetingBackends((parsed.platforms ?? availableBackends));
|
|
299
|
+
const missingBackends = requestedBackends.filter((backend) => !availableBackends.includes(backend));
|
|
300
|
+
if (missingBackends.length > 0) {
|
|
301
|
+
throw new ToolExecutionError("Requested greeting backends are not configured", {
|
|
302
|
+
details: {
|
|
303
|
+
requestedBackends,
|
|
304
|
+
availableBackends,
|
|
305
|
+
missingBackends,
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (requestedBackends.length === 0) {
|
|
310
|
+
throw new ToolExecutionError("No configured backends are available for the greeting workflow", {
|
|
311
|
+
details: { availableBackends },
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const visibleViceFastPath = shouldUseVisibleViceGreetingFastPath(args ?? {}, requestedBackends);
|
|
315
|
+
const verify = visibleViceFastPath ? false : parsed.verify !== false;
|
|
316
|
+
const captureScreenshot = visibleViceFastPath ? false : parsed.captureScreenshot !== false;
|
|
317
|
+
const restoreActiveBackend = parsed.restoreActiveBackend !== false;
|
|
318
|
+
const outputPath = captureScreenshot || parsed.outputPath
|
|
319
|
+
? resolvePath(String(parsed.outputPath ?? joinPath(process.cwd(), "artifacts", "greetings", `run_${Date.now()}`)))
|
|
320
|
+
: undefined;
|
|
321
|
+
const timeoutMs = parsed.timeoutMs ?? 1500;
|
|
322
|
+
const pollIntervalMs = parsed.pollIntervalMs ?? 100;
|
|
323
|
+
const template = parsed.messageTemplate ?? "HAVE A GREAT DAY, {PLATFORM}!";
|
|
324
|
+
const startingBackend = await ctx.client.getActiveBackendType();
|
|
325
|
+
const results = [];
|
|
326
|
+
let restoreError;
|
|
327
|
+
let restoreBackendLatencyMs = 0;
|
|
328
|
+
if (outputPath) {
|
|
329
|
+
await fs.mkdir(outputPath, { recursive: true });
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
for (const backend of requestedBackends) {
|
|
333
|
+
const expectedText = applyGreetingTemplate(template, backend);
|
|
334
|
+
const program = buildGreetingProgram(expectedText);
|
|
335
|
+
const timeline = [];
|
|
336
|
+
let executionMode = "direct_screen_write";
|
|
337
|
+
let preparedCaptureSession = false;
|
|
338
|
+
const client = ctx.client;
|
|
339
|
+
const timedStep = async (name, fn) => {
|
|
340
|
+
const startedAt = Date.now();
|
|
341
|
+
try {
|
|
342
|
+
const result = await withDiagnosticSpan("greeting", name, { backend }, fn);
|
|
343
|
+
timeline.push({ name, latencyMs: Date.now() - startedAt, ok: true });
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
timeline.push({
|
|
348
|
+
name,
|
|
349
|
+
latencyMs: Date.now() - startedAt,
|
|
350
|
+
ok: false,
|
|
351
|
+
error: error instanceof Error ? error.message : String(error),
|
|
352
|
+
});
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
await timedStep("switch_backend", async () => {
|
|
357
|
+
ctx.client.switchBackend(backend);
|
|
358
|
+
ctx.setPlatform(backend);
|
|
359
|
+
});
|
|
360
|
+
let prepareCapturePromise;
|
|
361
|
+
if (captureScreenshot && backend === "c64u" && typeof client.prepareVideoCapture === "function") {
|
|
362
|
+
prepareCapturePromise = timedStep("prepare_capture", async () => {
|
|
363
|
+
await client.prepareVideoCapture?.({ keepAliveMs: Math.max(500, timeoutMs) });
|
|
364
|
+
preparedCaptureSession = true;
|
|
365
|
+
}).catch(() => {
|
|
366
|
+
preparedCaptureSession = false;
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const runResult = await timedStep("render_greeting_screen", async () => {
|
|
370
|
+
if (typeof client.renderGreetingScreen === "function") {
|
|
371
|
+
executionMode = "direct_screen_write";
|
|
372
|
+
return client.renderGreetingScreen({ message: expectedText });
|
|
373
|
+
}
|
|
374
|
+
executionMode = "basic_program";
|
|
375
|
+
return ctx.client.uploadAndRunBasic(program);
|
|
376
|
+
});
|
|
377
|
+
if (prepareCapturePromise) {
|
|
378
|
+
await prepareCapturePromise;
|
|
379
|
+
}
|
|
380
|
+
const backendResult = {
|
|
381
|
+
backend,
|
|
382
|
+
expectedText,
|
|
383
|
+
program,
|
|
384
|
+
executionMode,
|
|
385
|
+
timeline,
|
|
386
|
+
};
|
|
387
|
+
if (!runResult.success) {
|
|
388
|
+
if (preparedCaptureSession && typeof client.releaseVideoCapture === "function") {
|
|
389
|
+
try {
|
|
390
|
+
await timedStep("release_capture", () => client.releaseVideoCapture?.());
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// Preserve the original render failure and best-effort cleanup.
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
backendResult.success = false;
|
|
397
|
+
backendResult.error = "basic_run_failed";
|
|
398
|
+
backendResult.details = runResult.details ?? null;
|
|
399
|
+
results.push(backendResult);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
let screen = "";
|
|
403
|
+
let screenMatched = false;
|
|
404
|
+
if (verify) {
|
|
405
|
+
const waited = await timedStep("verify_screen", () => waitForGreetingScreen(ctx, expectedText, timeoutMs, pollIntervalMs));
|
|
406
|
+
screen = waited.screen;
|
|
407
|
+
screenMatched = waited.matched;
|
|
408
|
+
}
|
|
409
|
+
backendResult.screen = screen;
|
|
410
|
+
let screenshotPath;
|
|
411
|
+
let screenshotAnalysis;
|
|
412
|
+
let screenshotError;
|
|
413
|
+
if (captureScreenshot) {
|
|
414
|
+
try {
|
|
415
|
+
const capture = await timedStep("capture_screenshot", () => client.captureFrames({
|
|
416
|
+
count: 1,
|
|
417
|
+
reuseSession: preparedCaptureSession,
|
|
418
|
+
keepAliveMs: preparedCaptureSession ? Math.max(500, timeoutMs) : 0,
|
|
419
|
+
}));
|
|
420
|
+
const frame = capture.frames[0];
|
|
421
|
+
if (!frame) {
|
|
422
|
+
throw new Error("No video frame returned by backend capture");
|
|
423
|
+
}
|
|
424
|
+
await timedStep("persist_screenshot", async () => {
|
|
425
|
+
screenshotAnalysis = analyseCapturedFrame(frame);
|
|
426
|
+
screenshotPath = outputPath
|
|
427
|
+
? resolvePath(joinPath(outputPath, `${backend}.png`))
|
|
428
|
+
: undefined;
|
|
429
|
+
if (screenshotPath) {
|
|
430
|
+
await writeCapturedFramePng(frame, screenshotPath);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
screenshotError = error instanceof Error ? error.message : String(error);
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
if (preparedCaptureSession && typeof client.releaseVideoCapture === "function") {
|
|
439
|
+
try {
|
|
440
|
+
await timedStep("release_capture", () => client.releaseVideoCapture?.());
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
// Surface screenshot failures, but do not overwrite a successful capture result.
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
backendResult.verification = {
|
|
449
|
+
screenContainsExpectedText: verify ? screenMatched : undefined,
|
|
450
|
+
screenshotCaptured: captureScreenshot ? !screenshotError : undefined,
|
|
451
|
+
screenshotAnalysis,
|
|
452
|
+
};
|
|
453
|
+
if (screenshotPath) {
|
|
454
|
+
backendResult.screenshotPath = screenshotPath;
|
|
455
|
+
}
|
|
456
|
+
if (screenshotError) {
|
|
457
|
+
backendResult.screenshotError = screenshotError;
|
|
458
|
+
}
|
|
459
|
+
backendResult.success = runResult.success
|
|
460
|
+
&& (!verify || screenMatched)
|
|
461
|
+
&& (!captureScreenshot || !screenshotError);
|
|
462
|
+
backendResult.promptToVisibleLatencyMs = timeline
|
|
463
|
+
.filter((entry) => entry.ok === true && (entry.name === "switch_backend" || entry.name === "render_greeting_screen" || entry.name === "verify_screen"))
|
|
464
|
+
.reduce((total, entry) => total + Number(entry.latencyMs ?? 0), 0);
|
|
465
|
+
backendResult.totalLatencyMs = timeline
|
|
466
|
+
.filter((entry) => entry.ok === true)
|
|
467
|
+
.reduce((total, entry) => total + Number(entry.latencyMs ?? 0), 0);
|
|
468
|
+
results.push(backendResult);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
if (restoreActiveBackend) {
|
|
473
|
+
try {
|
|
474
|
+
const restoreStartedAt = Date.now();
|
|
475
|
+
await withDiagnosticSpan("greeting", "restore_backend", { backend: startingBackend }, async () => {
|
|
476
|
+
ctx.client.switchBackend(startingBackend);
|
|
477
|
+
ctx.setPlatform(startingBackend);
|
|
478
|
+
});
|
|
479
|
+
restoreBackendLatencyMs = Date.now() - restoreStartedAt;
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
restoreError = error instanceof Error ? error.message : String(error);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const toolCallLatencyMs = Date.now() - toolStartedAt;
|
|
487
|
+
const backendTimedLatencyMs = results
|
|
488
|
+
.map((result) => Number(result.totalLatencyMs ?? 0))
|
|
489
|
+
.reduce((total, latency) => total + latency, 0);
|
|
490
|
+
const orchestrationOverheadMs = Math.max(0, toolCallLatencyMs - backendTimedLatencyMs - restoreBackendLatencyMs);
|
|
491
|
+
const success = results.every((result) => result.success === true) && !restoreError;
|
|
492
|
+
const payload = {
|
|
493
|
+
kind: "cross_platform_greeting",
|
|
494
|
+
availableBackends,
|
|
495
|
+
requestedBackends,
|
|
496
|
+
startingBackend,
|
|
497
|
+
restoredBackend: restoreActiveBackend ? startingBackend : await ctx.client.getActiveBackendType(),
|
|
498
|
+
outputPath: outputPath ?? null,
|
|
499
|
+
fastPath: visibleViceFastPath ? "visible_vice_no_probe" : null,
|
|
500
|
+
toolCallLatencyMs,
|
|
501
|
+
restoreBackendLatencyMs,
|
|
502
|
+
orchestrationOverheadMs,
|
|
503
|
+
results,
|
|
504
|
+
...(restoreError ? { restoreError } : {}),
|
|
505
|
+
};
|
|
506
|
+
if (outputPath) {
|
|
507
|
+
const reportPath = resolvePath(joinPath(outputPath, "results.json"));
|
|
508
|
+
await fs.writeFile(reportPath, JSON.stringify(payload, null, 2), "utf8");
|
|
509
|
+
payload.outputPath = outputPath;
|
|
510
|
+
payload.reportPath = reportPath;
|
|
511
|
+
}
|
|
512
|
+
const result = jsonResult(payload, {
|
|
513
|
+
success,
|
|
514
|
+
backends: requestedBackends,
|
|
515
|
+
outputPath: outputPath ?? null,
|
|
516
|
+
});
|
|
517
|
+
return success ? result : { ...result, isError: true };
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
if (error instanceof ToolError)
|
|
521
|
+
return toolErrorResult(error);
|
|
522
|
+
return unknownErrorResult(error);
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
},
|
|
53
526
|
{
|
|
54
527
|
name: "program_shuffle",
|
|
55
528
|
description: "Discover and run PRG/CRT programs under a root path, capturing screens and resetting between runs.",
|
|
@@ -202,8 +202,10 @@ async function pollAsmOutcome(client, logger, config) {
|
|
|
202
202
|
const lowMemAndScreen = await client.readMemoryRaw(0x0000, 0x7E8); // $0000-$07E7
|
|
203
203
|
// Extract jiffy clock for separate comparison (at offset $00A0 in the buffer)
|
|
204
204
|
const jiffyClock = lowMemAndScreen.slice(0xA0, 0xA3);
|
|
205
|
-
//
|
|
206
|
-
const
|
|
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);
|
|
207
209
|
const ioSignature = computeCrc32(combinedBuffer);
|
|
208
210
|
// Check for any activity
|
|
209
211
|
if (prevIoSignature !== null && ioSignature !== prevIoSignature) {
|
|
@@ -24,6 +24,8 @@ function toRecord(details) {
|
|
|
24
24
|
return { value: details };
|
|
25
25
|
}
|
|
26
26
|
const BASIC_MAX_LINE = 63999;
|
|
27
|
+
const prgPathDescription = "Path to the PRG image. Use an Ultimate-visible filesystem path on c64u, or a host-local filesystem path on VICE.";
|
|
28
|
+
const crtPathDescription = "Path to the CRT image on Ultimate-visible storage. CRT mounting is only supported on c64u.";
|
|
27
29
|
function stripOperationDiscriminator(value) {
|
|
28
30
|
const { [OPERATION_DISCRIMINATOR]: _ignored, ...rest } = value;
|
|
29
31
|
return rest;
|
|
@@ -213,10 +215,10 @@ const uploadAsmArgsSchema = objectSchema({
|
|
|
213
215
|
additionalProperties: false,
|
|
214
216
|
});
|
|
215
217
|
const prgFileArgsSchema = objectSchema({
|
|
216
|
-
description: "Arguments for loading or running a PRG
|
|
218
|
+
description: "Arguments for loading or running a PRG file.",
|
|
217
219
|
properties: {
|
|
218
220
|
path: stringSchema({
|
|
219
|
-
description:
|
|
221
|
+
description: prgPathDescription,
|
|
220
222
|
minLength: 1,
|
|
221
223
|
}),
|
|
222
224
|
},
|
|
@@ -224,10 +226,10 @@ const prgFileArgsSchema = objectSchema({
|
|
|
224
226
|
additionalProperties: false,
|
|
225
227
|
});
|
|
226
228
|
const crtFileArgsSchema = objectSchema({
|
|
227
|
-
description: "Arguments for running a CRT image stored on
|
|
229
|
+
description: "Arguments for running a CRT image stored on Ultimate-visible storage.",
|
|
228
230
|
properties: {
|
|
229
231
|
path: stringSchema({
|
|
230
|
-
description:
|
|
232
|
+
description: crtPathDescription,
|
|
231
233
|
minLength: 1,
|
|
232
234
|
}),
|
|
233
235
|
},
|
|
@@ -676,6 +678,7 @@ export const programRunnersModule = defineToolModule({
|
|
|
676
678
|
"Stage PRG files without running when the user wants to inspect memory first.",
|
|
677
679
|
"Confirm the Ultimate filesystem path (e.g. //USB0/demo.prg) is accessible before invoking.",
|
|
678
680
|
],
|
|
681
|
+
supportedPlatforms: ["c64u"],
|
|
679
682
|
async execute(args, ctx) {
|
|
680
683
|
return executeLoadPrg(args, ctx);
|
|
681
684
|
},
|
|
@@ -723,6 +726,7 @@ export const programRunnersModule = defineToolModule({
|
|
|
723
726
|
arguments: { path: "//USB0/game.crt" },
|
|
724
727
|
},
|
|
725
728
|
],
|
|
729
|
+
supportedPlatforms: ["c64u"],
|
|
726
730
|
async execute(args, ctx) {
|
|
727
731
|
try {
|
|
728
732
|
const parsed = crtFileArgsSchema.parse(args ?? {});
|
|
@@ -154,6 +154,7 @@ export const configModuleGroup = defineToolModule({
|
|
|
154
154
|
resources: ["c64://context/bootstrap", "c64://docs/index"],
|
|
155
155
|
prompts: ["memory-debug"],
|
|
156
156
|
defaultTags: ["config", "diagnostics"],
|
|
157
|
+
supportedPlatforms: ["c64u", "vice"],
|
|
157
158
|
workflowHints: [
|
|
158
159
|
"List categories before changing values so users can confirm firmware-provided names.",
|
|
159
160
|
"Mention when operations persist to flash or modify debug registers to highlight impacts.",
|
|
@@ -169,6 +170,22 @@ export const configModuleGroup = defineToolModule({
|
|
|
169
170
|
variants: configOperations.map((operation) => operation.schema),
|
|
170
171
|
}),
|
|
171
172
|
tags: ["config", "diagnostics", "grouped"],
|
|
173
|
+
operationPlatforms: {
|
|
174
|
+
load_flash: ["c64u"],
|
|
175
|
+
save_flash: ["c64u"],
|
|
176
|
+
reset_defaults: ["c64u"],
|
|
177
|
+
read_debugreg: ["c64u"],
|
|
178
|
+
write_debugreg: ["c64u"],
|
|
179
|
+
shuffle: ["c64u"],
|
|
180
|
+
},
|
|
181
|
+
operationToolNames: {
|
|
182
|
+
load_flash: "config_load_from_flash",
|
|
183
|
+
save_flash: "config_save_to_flash",
|
|
184
|
+
reset_defaults: "config_reset_to_default",
|
|
185
|
+
read_debugreg: "debugreg_read",
|
|
186
|
+
write_debugreg: "debugreg_write",
|
|
187
|
+
shuffle: "program_shuffle",
|
|
188
|
+
},
|
|
172
189
|
examples: [
|
|
173
190
|
{
|
|
174
191
|
name: "List categories",
|
|
@@ -251,6 +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
255
|
resources: ["c64://context/bootstrap"],
|
|
255
256
|
prompts: ["drive-management"],
|
|
256
257
|
defaultTags: ["storage", "drive"],
|
|
@@ -268,6 +269,12 @@ export const diskModuleGroup = defineToolModule({
|
|
|
268
269
|
variants: diskOperations.map((operation) => operation.schema),
|
|
269
270
|
}),
|
|
270
271
|
tags: ["storage", "drive", "grouped"],
|
|
272
|
+
operationPlatforms: { file_info: ["c64u"], create_image: ["c64u"], find_and_run: ["c64u"] },
|
|
273
|
+
operationToolNames: {
|
|
274
|
+
file_info: "file_info",
|
|
275
|
+
create_image: "create_image",
|
|
276
|
+
find_and_run: "find_and_run_program_by_name",
|
|
277
|
+
},
|
|
271
278
|
examples: [
|
|
272
279
|
{
|
|
273
280
|
name: "Mount image with verification",
|
|
@@ -33,6 +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
37
|
resources: ["c64://context/bootstrap"],
|
|
37
38
|
defaultTags: ["drive", "hardware"],
|
|
38
39
|
workflowHints: [
|
|
@@ -49,6 +50,8 @@ export const driveModuleGroup = defineToolModule({
|
|
|
49
50
|
variants: driveOperations.map((operation) => operation.schema),
|
|
50
51
|
}),
|
|
51
52
|
tags: ["drive", "hardware", "grouped"],
|
|
53
|
+
operationPlatforms: { load_rom: ["c64u"] },
|
|
54
|
+
operationToolNames: { load_rom: "drive_load_rom" },
|
|
52
55
|
examples: [
|
|
53
56
|
{
|
|
54
57
|
name: "Power on drive",
|