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
package/dist/tools/input.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema, } from "./types.js";
|
|
2
2
|
import { arraySchema, literalSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "./schema.js";
|
|
3
|
-
import { textResult } from "./responses.js";
|
|
3
|
+
import { jsonResult, textResult } from "./responses.js";
|
|
4
4
|
import { ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
|
|
5
|
+
function isMissingNativeInputEndpoint(error) {
|
|
6
|
+
return typeof error === "object"
|
|
7
|
+
&& error !== null
|
|
8
|
+
&& "response" in error
|
|
9
|
+
&& error.response?.status === 404;
|
|
10
|
+
}
|
|
11
|
+
function nativeInputUnavailableResult(operation) {
|
|
12
|
+
return textResult(`Native physical input is unavailable on this firmware, so ${operation} was not sent. Use c64://platform/status before choosing native input; use key or write_text for ordinary C64 input.`, { success: false, code: "native_input_unavailable", fallback: "key_or_write_text" });
|
|
13
|
+
}
|
|
5
14
|
// ---------------------------------------------------------------------------
|
|
6
15
|
// PETSCII token expansion table
|
|
7
16
|
// Maps {TOKEN} placeholders to PETSCII/control-byte values that are queued via
|
|
@@ -82,13 +91,9 @@ function expandPetsciiTokens(text) {
|
|
|
82
91
|
}
|
|
83
92
|
// ---------------------------------------------------------------------------
|
|
84
93
|
// Joystick helpers
|
|
85
|
-
//
|
|
86
|
-
//
|
|
94
|
+
// Bits 0-4: Up / Down / Left / Right / Fire, active LOW (0 = pressed).
|
|
95
|
+
// VICE must use Binary Monitor Joyport Set; CIA register writes are not input.
|
|
87
96
|
// ---------------------------------------------------------------------------
|
|
88
|
-
const JOYSTICK_PORT_ADDRESS = {
|
|
89
|
-
1: 0xDC01,
|
|
90
|
-
2: 0xDC00,
|
|
91
|
-
};
|
|
92
97
|
const JOYSTICK_BIT = {
|
|
93
98
|
up: 0,
|
|
94
99
|
down: 1,
|
|
@@ -96,6 +101,14 @@ const JOYSTICK_BIT = {
|
|
|
96
101
|
right: 3,
|
|
97
102
|
fire: 4,
|
|
98
103
|
};
|
|
104
|
+
const KEYBOARD_INPUTS = [
|
|
105
|
+
"inst_del", "return", "cursor_left_right", "f7", "f1", "f3", "f5", "cursor_up_down",
|
|
106
|
+
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
|
107
|
+
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
|
108
|
+
"left_shift", "right_shift", "plus", "minus", "period", "colon", "at", "comma", "pound", "star", "semicolon", "clr_home", "equals", "arrow_up", "slash", "arrow_left", "ctrl", "space", "commodore", "run_stop", "restore",
|
|
109
|
+
];
|
|
110
|
+
const JOYSTICK_INPUTS = ["up", "down", "left", "right", "fire", "fire2", "fire3"];
|
|
111
|
+
const INPUT_TRANSITIONS = ["press", "release", "tap"];
|
|
99
112
|
function joystickByte(controls) {
|
|
100
113
|
let mask = 0xff;
|
|
101
114
|
for (const ctrl of controls) {
|
|
@@ -157,18 +170,18 @@ export const keyArgsSchema = objectSchema({
|
|
|
157
170
|
additionalProperties: false,
|
|
158
171
|
});
|
|
159
172
|
export const joystickArgsSchema = objectSchema({
|
|
160
|
-
description: "Simulate joystick input
|
|
173
|
+
description: "Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set.",
|
|
161
174
|
properties: {
|
|
162
175
|
op: literalSchema("joystick"),
|
|
163
176
|
port: numberSchema({
|
|
164
|
-
description: "Joystick port (1
|
|
177
|
+
description: "Joystick port (1 or 2).",
|
|
165
178
|
integer: true,
|
|
166
179
|
minimum: 1,
|
|
167
180
|
maximum: 2,
|
|
168
181
|
}),
|
|
169
182
|
controls: arraySchema(stringSchema({
|
|
170
183
|
description: "Control to activate.",
|
|
171
|
-
enum:
|
|
184
|
+
enum: JOYSTICK_INPUTS,
|
|
172
185
|
}), {
|
|
173
186
|
description: "List of controls to activate simultaneously.",
|
|
174
187
|
minItems: 0,
|
|
@@ -188,6 +201,32 @@ export const joystickArgsSchema = objectSchema({
|
|
|
188
201
|
required: ["op", "port", "controls", "action"],
|
|
189
202
|
additionalProperties: false,
|
|
190
203
|
});
|
|
204
|
+
export const keyboardArgsSchema = objectSchema({
|
|
205
|
+
description: "Send physical C64 keyboard matrix events through machine:input (a C64U firmware version that provides it, or U64 3.15+).",
|
|
206
|
+
properties: {
|
|
207
|
+
op: literalSchema("keyboard"),
|
|
208
|
+
inputs: arraySchema(stringSchema({ description: "Physical keyboard input.", enum: KEYBOARD_INPUTS }), {
|
|
209
|
+
description: "One or more keys to transition together (for example left_shift + a).",
|
|
210
|
+
minItems: 1,
|
|
211
|
+
maxItems: 8,
|
|
212
|
+
}),
|
|
213
|
+
transition: stringSchema({ description: "press holds, release frees, tap presses and releases.", enum: INPUT_TRANSITIONS }),
|
|
214
|
+
},
|
|
215
|
+
required: ["op", "inputs", "transition"],
|
|
216
|
+
additionalProperties: false,
|
|
217
|
+
});
|
|
218
|
+
const releaseAllArgsSchema = objectSchema({
|
|
219
|
+
description: "Release every key and joystick control injected through machine:input (a C64U firmware version that provides it, or U64 3.15+).",
|
|
220
|
+
properties: { op: literalSchema("release_all") },
|
|
221
|
+
required: ["op"],
|
|
222
|
+
additionalProperties: false,
|
|
223
|
+
});
|
|
224
|
+
const inputStateArgsSchema = objectSchema({
|
|
225
|
+
description: "Read the keys and joystick controls held through machine:input (a C64U firmware version that provides it, or U64 3.15+).",
|
|
226
|
+
properties: { op: literalSchema("state") },
|
|
227
|
+
required: ["op"],
|
|
228
|
+
additionalProperties: false,
|
|
229
|
+
});
|
|
191
230
|
// ---------------------------------------------------------------------------
|
|
192
231
|
// Handlers
|
|
193
232
|
// ---------------------------------------------------------------------------
|
|
@@ -250,30 +289,92 @@ const inputOperationHandlers = {
|
|
|
250
289
|
try {
|
|
251
290
|
const parsed = joystickArgsSchema.parse(args);
|
|
252
291
|
const port = parsed.port;
|
|
253
|
-
const addr = JOYSTICK_PORT_ADDRESS[port];
|
|
254
292
|
const durationMs = parsed.durationMs ?? 80;
|
|
255
293
|
const pressedByte = joystickByte(parsed.controls);
|
|
256
294
|
const releasedByte = 0xff;
|
|
295
|
+
const platform = await ctx.client.getActiveBackendType();
|
|
296
|
+
if (platform === "c64u") {
|
|
297
|
+
if (parsed.controls.length === 0 && parsed.action === "release") {
|
|
298
|
+
const state = await ctx.client.sendInputEvents({ events: [{ kind: "release_all" }] });
|
|
299
|
+
return textResult("Released all REST-injected keyboard and joystick inputs.", { success: true, action: "release_all", state });
|
|
300
|
+
}
|
|
301
|
+
const state = await ctx.client.sendInputEvents({
|
|
302
|
+
events: [{ kind: "joystick", port, inputs: [...parsed.controls], transition: parsed.action }],
|
|
303
|
+
});
|
|
304
|
+
return textResult(`Joystick port ${port} ${parsed.action}: ${parsed.controls.join(", ")}.`, {
|
|
305
|
+
success: true, port, action: parsed.action, controls: parsed.controls, state,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (parsed.controls.some((control) => !(control in JOYSTICK_BIT))) {
|
|
309
|
+
throw new ToolValidationError("VICE joystick supports up, down, left, right, and fire only.", { path: "$.controls" });
|
|
310
|
+
}
|
|
257
311
|
if (parsed.action === "release") {
|
|
258
|
-
await ctx.client.
|
|
259
|
-
ctx.logger.info("Released joystick", { port
|
|
312
|
+
await ctx.client.viceJoyportSet(port, releasedByte);
|
|
313
|
+
ctx.logger.info("Released joystick", { port });
|
|
260
314
|
return textResult(`Joystick port ${port} released.`, { success: true, port, action: "release" });
|
|
261
315
|
}
|
|
262
316
|
if (parsed.action === "press") {
|
|
263
|
-
await ctx.client.
|
|
317
|
+
await ctx.client.viceJoyportSet(port, pressedByte);
|
|
264
318
|
ctx.logger.info("Pressed joystick", { port, controls: parsed.controls, byte: pressedByte });
|
|
265
319
|
return textResult(`Joystick port ${port} pressed: ${parsed.controls.join(", ") || "none"}.`, { success: true, port, action: "press", controls: parsed.controls });
|
|
266
320
|
}
|
|
267
321
|
// tap: press → wait → release
|
|
268
|
-
await ctx.client.
|
|
322
|
+
await ctx.client.viceJoyportSet(port, pressedByte);
|
|
269
323
|
await new Promise((res) => setTimeout(res, durationMs));
|
|
270
|
-
await ctx.client.
|
|
324
|
+
await ctx.client.viceJoyportSet(port, releasedByte);
|
|
271
325
|
ctx.logger.info("Tapped joystick", { port, controls: parsed.controls, durationMs });
|
|
272
326
|
return textResult(`Joystick port ${port} tapped: ${parsed.controls.join(", ") || "none"} for ${durationMs}ms.`, { success: true, port, action: "tap", controls: parsed.controls, durationMs });
|
|
273
327
|
}
|
|
274
328
|
catch (error) {
|
|
275
329
|
if (error instanceof ToolValidationError)
|
|
276
330
|
return toolErrorResult(error);
|
|
331
|
+
if (isMissingNativeInputEndpoint(error))
|
|
332
|
+
return nativeInputUnavailableResult("the joystick event");
|
|
333
|
+
return unknownErrorResult(error);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
keyboard: async (args, ctx) => {
|
|
337
|
+
try {
|
|
338
|
+
const parsed = keyboardArgsSchema.parse(args);
|
|
339
|
+
const state = await ctx.client.sendInputEvents({
|
|
340
|
+
events: [{ kind: "keyboard", inputs: [...parsed.inputs], transition: parsed.transition }],
|
|
341
|
+
});
|
|
342
|
+
return textResult(`Keyboard ${parsed.transition}: ${parsed.inputs.join(", ")}.`, {
|
|
343
|
+
success: true, inputs: parsed.inputs, transition: parsed.transition, state,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
if (error instanceof ToolValidationError)
|
|
348
|
+
return toolErrorResult(error);
|
|
349
|
+
if (isMissingNativeInputEndpoint(error))
|
|
350
|
+
return nativeInputUnavailableResult("the keyboard event");
|
|
351
|
+
return unknownErrorResult(error);
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
release_all: async (args, ctx) => {
|
|
355
|
+
try {
|
|
356
|
+
releaseAllArgsSchema.parse(args);
|
|
357
|
+
const state = await ctx.client.sendInputEvents({ events: [{ kind: "release_all" }] });
|
|
358
|
+
return textResult("Released all REST-injected keyboard and joystick inputs.", { success: true, state });
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
if (error instanceof ToolValidationError)
|
|
362
|
+
return toolErrorResult(error);
|
|
363
|
+
if (isMissingNativeInputEndpoint(error))
|
|
364
|
+
return nativeInputUnavailableResult("the release request");
|
|
365
|
+
return unknownErrorResult(error);
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
state: async (args, ctx) => {
|
|
369
|
+
try {
|
|
370
|
+
inputStateArgsSchema.parse(args);
|
|
371
|
+
return jsonResult(await ctx.client.getInputState());
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (error instanceof ToolValidationError)
|
|
375
|
+
return toolErrorResult(error);
|
|
376
|
+
if (isMissingNativeInputEndpoint(error))
|
|
377
|
+
return nativeInputUnavailableResult("the input-state query");
|
|
277
378
|
return unknownErrorResult(error);
|
|
278
379
|
}
|
|
279
380
|
},
|
|
@@ -283,30 +384,36 @@ const inputOperationHandlers = {
|
|
|
283
384
|
// ---------------------------------------------------------------------------
|
|
284
385
|
export const inputModule = defineToolModule({
|
|
285
386
|
domain: "input",
|
|
286
|
-
summary: "
|
|
287
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
387
|
+
summary: "Keyboard input plus native Ultimate REST keyboard and joystick control.",
|
|
388
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
288
389
|
resources: ["c64://guide/bootstrap", "c64://memory/map", "c64://io/cia/spec"],
|
|
289
390
|
prompts: [],
|
|
290
391
|
defaultTags: ["input"],
|
|
291
392
|
workflowHints: [
|
|
292
|
-
"Use write_text with {RETURN} tokens to automate BASIC entry;
|
|
393
|
+
"Use write_text with {RETURN} tokens to automate BASIC entry; use key for one PETSCII/KERNAL queue key. Neither is for navigating Ultimate firmware menus.",
|
|
293
394
|
"write_text and key inject through the KERNAL keyboard queue ($0277/$00C6) so they work on both C64U and VICE.",
|
|
294
|
-
"
|
|
395
|
+
"Use keyboard for physical C64 key combinations (including modifiers) on machine:input, and release_all to recover from interrupted holds. It requires a C64U firmware version that provides the endpoint, or U64 3.15+.",
|
|
396
|
+
"Before native input, read c64://platform/status. If machine:input is unavailable or unknown, use key or write_text for ordinary C64 input instead of attempting physical matrix events.",
|
|
397
|
+
"Change C64U/U64/U2 configuration through c64_config REST operations, never by keyboard menu navigation. Use keyboard navigation only where REST has no equivalent, such as the machine code monitor, visual SID editor, or Tool Menu.",
|
|
398
|
+
"Joystick uses machine:input on a C64U firmware version that provides the endpoint or U64 3.15+; VICE supports up/down/left/right/fire through its monitor.",
|
|
295
399
|
],
|
|
296
400
|
tools: [
|
|
297
401
|
{
|
|
298
402
|
name: "c64_input",
|
|
299
|
-
description: "
|
|
300
|
-
summary: "Types text,
|
|
403
|
+
description: "Cross-platform PETSCII typing plus native Ultimate keyboard and joystick events.",
|
|
404
|
+
summary: "Types text, sends physical key combinations, and controls joysticks.",
|
|
301
405
|
inputSchema: discriminatedUnionSchema({
|
|
302
|
-
description: "Input operations: write_text, key, joystick.",
|
|
406
|
+
description: "Input operations: write_text, key, keyboard, joystick, release_all, state.",
|
|
303
407
|
variants: [
|
|
304
408
|
writeTextArgsSchema.jsonSchema,
|
|
305
409
|
keyArgsSchema.jsonSchema,
|
|
306
410
|
joystickArgsSchema.jsonSchema,
|
|
411
|
+
keyboardArgsSchema.jsonSchema,
|
|
412
|
+
releaseAllArgsSchema.jsonSchema,
|
|
413
|
+
inputStateArgsSchema.jsonSchema,
|
|
307
414
|
],
|
|
308
415
|
}),
|
|
309
|
-
operationPlatforms: { joystick: ["vice"] },
|
|
416
|
+
operationPlatforms: { joystick: ["c64u", "vice"], keyboard: ["c64u"], release_all: ["c64u"], state: ["c64u"] },
|
|
310
417
|
tags: ["input", "keyboard", "joystick"],
|
|
311
418
|
examples: [
|
|
312
419
|
{
|
|
@@ -324,6 +431,11 @@ export const inputModule = defineToolModule({
|
|
|
324
431
|
description: "Brief rightward tap on joystick port 2",
|
|
325
432
|
arguments: { op: "joystick", port: 2, controls: ["right"], action: "tap", durationMs: 80 },
|
|
326
433
|
},
|
|
434
|
+
{
|
|
435
|
+
name: "Type an uppercase A",
|
|
436
|
+
description: "Tap a physical key chord through Ultimate REST input",
|
|
437
|
+
arguments: { op: "keyboard", inputs: ["left_shift", "a"], transition: "tap" },
|
|
438
|
+
},
|
|
327
439
|
{
|
|
328
440
|
name: "Press fire on port 1",
|
|
329
441
|
description: "Hold fire button on joystick port 1",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineToolModule } from "./types.js";
|
|
2
2
|
import { objectSchema } from "./schema.js";
|
|
3
|
-
import { textResult } from "./responses.js";
|
|
3
|
+
import { jsonResult, textResult } from "./responses.js";
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
4
5
|
import { ToolError, ToolExecutionError, toolErrorResult, unknownErrorResult, } from "./errors.js";
|
|
5
6
|
function createNoArgsSchema(description) {
|
|
6
7
|
return objectSchema({
|
|
@@ -24,12 +25,20 @@ function normaliseFailure(details) {
|
|
|
24
25
|
}
|
|
25
26
|
return { value: details };
|
|
26
27
|
}
|
|
28
|
+
function isMissingMenuScreenEndpoint(error) {
|
|
29
|
+
return typeof error === "object"
|
|
30
|
+
&& error !== null
|
|
31
|
+
&& "response" in error
|
|
32
|
+
&& error.response?.status === 404;
|
|
33
|
+
}
|
|
27
34
|
const resetArgsSchema = createNoArgsSchema("No arguments required to reset the C64.");
|
|
28
35
|
const rebootArgsSchema = createNoArgsSchema("No arguments required to reboot the C64 firmware.");
|
|
29
36
|
const pauseArgsSchema = createNoArgsSchema("No arguments required to pause the machine.");
|
|
30
37
|
const resumeArgsSchema = createNoArgsSchema("No arguments required to resume the machine after a pause.");
|
|
31
38
|
const poweroffArgsSchema = createNoArgsSchema("No arguments required to power off the machine.");
|
|
39
|
+
const powerCycleArgsSchema = createNoArgsSchema("No arguments required to power cycle the active system.");
|
|
32
40
|
const menuButtonArgsSchema = createNoArgsSchema("No arguments required to toggle the Ultimate menu button.");
|
|
41
|
+
const readMenuScreenArgsSchema = createNoArgsSchema("No arguments required to read the active Ultimate menu screen matrix.");
|
|
33
42
|
export const machineControlModule = defineToolModule({
|
|
34
43
|
domain: "machine",
|
|
35
44
|
summary: "Power, reset, pause/resume, and diagnostic controls for the C64 and Ultimate hardware.",
|
|
@@ -54,7 +63,7 @@ export const machineControlModule = defineToolModule({
|
|
|
54
63
|
workflowHints: [
|
|
55
64
|
"Use when the user wants a quick restart without losing power; mention that memory contents may persist.",
|
|
56
65
|
],
|
|
57
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
66
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
58
67
|
async execute(args, ctx) {
|
|
59
68
|
try {
|
|
60
69
|
resetArgsSchema.parse(args ?? {});
|
|
@@ -92,7 +101,7 @@ export const machineControlModule = defineToolModule({
|
|
|
92
101
|
workflowHints: [
|
|
93
102
|
"Choose reboot when configuration changed or hardware is stuck; warn that it will interrupt any running program.",
|
|
94
103
|
],
|
|
95
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
104
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
96
105
|
async execute(args, ctx) {
|
|
97
106
|
try {
|
|
98
107
|
rebootArgsSchema.parse(args ?? {});
|
|
@@ -130,7 +139,7 @@ export const machineControlModule = defineToolModule({
|
|
|
130
139
|
workflowHints: [
|
|
131
140
|
"Pause when the user needs a stable memory snapshot; remind them to resume to continue execution.",
|
|
132
141
|
],
|
|
133
|
-
supportedPlatforms: ["c64u"],
|
|
142
|
+
supportedPlatforms: ["c64u", "u2"],
|
|
134
143
|
async execute(args, ctx) {
|
|
135
144
|
try {
|
|
136
145
|
pauseArgsSchema.parse(args ?? {});
|
|
@@ -168,7 +177,7 @@ export const machineControlModule = defineToolModule({
|
|
|
168
177
|
workflowHints: [
|
|
169
178
|
"Call after a pause or diagnostic halt and confirm the machine is running again.",
|
|
170
179
|
],
|
|
171
|
-
supportedPlatforms: ["c64u"],
|
|
180
|
+
supportedPlatforms: ["c64u", "u2"],
|
|
172
181
|
async execute(args, ctx) {
|
|
173
182
|
try {
|
|
174
183
|
resumeArgsSchema.parse(args ?? {});
|
|
@@ -231,6 +240,41 @@ export const machineControlModule = defineToolModule({
|
|
|
231
240
|
}
|
|
232
241
|
},
|
|
233
242
|
},
|
|
243
|
+
{
|
|
244
|
+
name: "power_cycle",
|
|
245
|
+
description: "Return the active system to a fresh state: C64U/U64 uses verified Tool Menu navigation, U2-family reboots through REST, and managed VICE restarts.",
|
|
246
|
+
summary: "Performs the platform-appropriate full power-cycle equivalent and reports the strategy used.",
|
|
247
|
+
inputSchema: powerCycleArgsSchema.jsonSchema,
|
|
248
|
+
tags: ["power", "reboot", "fresh-start"],
|
|
249
|
+
prerequisites: [],
|
|
250
|
+
examples: [
|
|
251
|
+
{ name: "Fresh start", description: "Power cycle the active system", arguments: {} },
|
|
252
|
+
],
|
|
253
|
+
workflowHints: [
|
|
254
|
+
"Use only when the user explicitly requests a fresh start or power cycle; it interrupts active work.",
|
|
255
|
+
"On C64U/U64, verify Tool Menu navigation through machine:menu_screen before selecting Power Cycle.",
|
|
256
|
+
],
|
|
257
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
258
|
+
async execute(args, ctx) {
|
|
259
|
+
try {
|
|
260
|
+
powerCycleArgsSchema.parse(args ?? {});
|
|
261
|
+
ctx.logger.info("Power cycling active backend");
|
|
262
|
+
const result = await ctx.client.powerCycle();
|
|
263
|
+
if (!result.success) {
|
|
264
|
+
throw new ToolExecutionError("Power cycle did not complete", {
|
|
265
|
+
details: normaliseFailure(result.details),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const details = toRecord(result.details) ?? {};
|
|
269
|
+
return textResult("Power-cycle request completed.", { success: true, details });
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (error instanceof ToolError)
|
|
273
|
+
return toolErrorResult(error);
|
|
274
|
+
return unknownErrorResult(error);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
},
|
|
234
278
|
{
|
|
235
279
|
name: "menu_button",
|
|
236
280
|
description: "Toggle the Ultimate 64 menu button.",
|
|
@@ -244,7 +288,7 @@ export const machineControlModule = defineToolModule({
|
|
|
244
288
|
workflowHints: [
|
|
245
289
|
"Use when the user needs to open or close the Ultimate menu; suggest following up with drive operations if relevant.",
|
|
246
290
|
],
|
|
247
|
-
supportedPlatforms: ["c64u"],
|
|
291
|
+
supportedPlatforms: ["c64u", "u2"],
|
|
248
292
|
async execute(args, ctx) {
|
|
249
293
|
try {
|
|
250
294
|
menuButtonArgsSchema.parse(args ?? {});
|
|
@@ -269,5 +313,40 @@ export const machineControlModule = defineToolModule({
|
|
|
269
313
|
}
|
|
270
314
|
},
|
|
271
315
|
},
|
|
316
|
+
{
|
|
317
|
+
name: "read_menu_screen",
|
|
318
|
+
description: "Read the raw character and colour matrix for the currently visible Ultimate menu screen.",
|
|
319
|
+
summary: "Returns the firmware-defined menu matrix bytes, including text and colour information.",
|
|
320
|
+
inputSchema: readMenuScreenArgsSchema.jsonSchema,
|
|
321
|
+
tags: ["menu", "screen", "diagnostic"],
|
|
322
|
+
prerequisites: ["menu_button"],
|
|
323
|
+
examples: [
|
|
324
|
+
{ name: "Inspect active menu", description: "Read the currently visible Ultimate menu", arguments: {} },
|
|
325
|
+
],
|
|
326
|
+
workflowHints: [
|
|
327
|
+
"Read c64://platform/status first. Call after opening the Ultimate menu; if the endpoint is unavailable or no menu is active, the tool returns fallback guidance instead of a transport error.",
|
|
328
|
+
"The firmware returns an opaque character-and-colour matrix, preserved as base64 without guessing a layout.",
|
|
329
|
+
],
|
|
330
|
+
supportedPlatforms: ["c64u", "u2"],
|
|
331
|
+
async execute(args, ctx) {
|
|
332
|
+
try {
|
|
333
|
+
readMenuScreenArgsSchema.parse(args ?? {});
|
|
334
|
+
const bytes = await ctx.client.readMenuScreen();
|
|
335
|
+
return jsonResult({
|
|
336
|
+
encoding: "base64",
|
|
337
|
+
byteLength: bytes.length,
|
|
338
|
+
matrix: Buffer.from(bytes).toString("base64"),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if (error instanceof ToolError)
|
|
343
|
+
return toolErrorResult(error);
|
|
344
|
+
if (isMissingMenuScreenEndpoint(error)) {
|
|
345
|
+
return textResult("No menu matrix is available. Open the Ultimate menu and retry; if it remains unavailable, this firmware does not provide machine:menu_screen. Use REST configuration instead of menu navigation where possible.", { success: false, code: "native_menu_screen_unavailable", fallback: "rest_configuration_or_manual_menu" });
|
|
346
|
+
}
|
|
347
|
+
return unknownErrorResult(error);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
},
|
|
272
351
|
],
|
|
273
352
|
});
|
package/dist/tools/memory.js
CHANGED
|
@@ -243,11 +243,12 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
243
243
|
details: detailRecord,
|
|
244
244
|
});
|
|
245
245
|
}
|
|
246
|
+
const pinnedFacade = ctx.client.pinFacade ? await ctx.client.pinFacade() : undefined; // Pin for this whole pause/read/write/resume sequence so a concurrent select_backend can't retarget it (HARD01-016).
|
|
246
247
|
const canPause = supportsMachinePause(ctx);
|
|
247
248
|
let paused = false;
|
|
248
249
|
try {
|
|
249
250
|
if (canPause) {
|
|
250
|
-
const pauseResult = await ctx.client.pause();
|
|
251
|
+
const pauseResult = await ctx.client.pause(pinnedFacade);
|
|
251
252
|
if (!pauseResult.success) {
|
|
252
253
|
throw new ToolExecutionError("C64 firmware reported failure while pausing", {
|
|
253
254
|
details: normaliseFailure(pauseResult.details),
|
|
@@ -258,7 +259,7 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
258
259
|
const expectedBytes = expectedInfo?.bytes ?? new Uint8Array();
|
|
259
260
|
const maskBytes = maskInfo?.bytes;
|
|
260
261
|
const readLength = Math.max(1, Math.max(writeInfo.bytes.length, expectedBytes.length));
|
|
261
|
-
const preRead = await ctx.client.readMemory(parsed.address, String(readLength));
|
|
262
|
+
const preRead = await ctx.client.readMemory(parsed.address, String(readLength), pinnedFacade);
|
|
262
263
|
if (!preRead.success) {
|
|
263
264
|
throw new ToolExecutionError("C64 firmware reported failure while reading memory", {
|
|
264
265
|
details: normaliseFailure(preRead.details),
|
|
@@ -286,13 +287,13 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
286
287
|
});
|
|
287
288
|
}
|
|
288
289
|
}
|
|
289
|
-
const writeResult = await ctx.client.writeMemory(parsed.address, writeInfo.canonical);
|
|
290
|
+
const writeResult = await ctx.client.writeMemory(parsed.address, writeInfo.canonical, pinnedFacade);
|
|
290
291
|
if (!writeResult.success) {
|
|
291
292
|
throw new ToolExecutionError("C64 firmware reported failure while writing memory", {
|
|
292
293
|
details: normaliseFailure(writeResult.details),
|
|
293
294
|
});
|
|
294
295
|
}
|
|
295
|
-
const postRead = await ctx.client.readMemory(parsed.address, String(Math.max(1, writeInfo.bytes.length)));
|
|
296
|
+
const postRead = await ctx.client.readMemory(parsed.address, String(Math.max(1, writeInfo.bytes.length)), pinnedFacade);
|
|
296
297
|
if (!postRead.success) {
|
|
297
298
|
throw new ToolExecutionError("C64 firmware reported failure while reading back memory", {
|
|
298
299
|
details: normaliseFailure(postRead.details),
|
|
@@ -336,6 +337,20 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
336
337
|
if (preMismatches.length > 0) {
|
|
337
338
|
verificationMetadata.preReadMismatches = preMismatches;
|
|
338
339
|
}
|
|
340
|
+
if (paused) { // A failed resume here leaves the machine paused: a recoverable operation failure, not a log.
|
|
341
|
+
const resumeResult = await ctx.client.resume(pinnedFacade);
|
|
342
|
+
if (!resumeResult.success) {
|
|
343
|
+
throw new ToolExecutionError("Write completed but the machine could not be resumed", {
|
|
344
|
+
details: {
|
|
345
|
+
machinePaused: true,
|
|
346
|
+
resume: normaliseFailure(resumeResult.details),
|
|
347
|
+
recovery: "Call c64_system with op 'resume' before further machine operations.",
|
|
348
|
+
verification: verificationMetadata,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
paused = false;
|
|
353
|
+
}
|
|
339
354
|
return textResult(`Wrote ${resolvedLength ?? "the provided"} bytes starting at ${resolvedAddress} (verified).`, {
|
|
340
355
|
success: true,
|
|
341
356
|
address: resolvedAddress,
|
|
@@ -350,7 +365,7 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
350
365
|
finally {
|
|
351
366
|
if (paused) {
|
|
352
367
|
try {
|
|
353
|
-
const resumeResult = await ctx.client.resume();
|
|
368
|
+
const resumeResult = await ctx.client.resume(pinnedFacade);
|
|
354
369
|
if (!resumeResult.success) {
|
|
355
370
|
ctx.logger.warn("C64 resume reported failure after write", {
|
|
356
371
|
details: normaliseFailure(resumeResult.details),
|
|
@@ -685,7 +700,7 @@ export { disassembleArgsSchema, copyMemoryArgsSchema, fillMemoryArgsSchema, sear
|
|
|
685
700
|
export const memoryModule = defineToolModule({
|
|
686
701
|
domain: "memory",
|
|
687
702
|
summary: "Screen, main memory, and low-level inspection utilities.",
|
|
688
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
703
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
689
704
|
resources: [
|
|
690
705
|
"c64://guide/bootstrap",
|
|
691
706
|
"c64://basic/spec",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { objectSchema, stringSchema, arraySchema, numberSchema, optionalSchema, booleanSchema } from "../schema.js";
|
|
2
2
|
import { jsonResult } from "../responses.js";
|
|
3
|
-
import { ToolError, toolErrorResult, unknownErrorResult } from "../errors.js";
|
|
3
|
+
import { ToolError, ToolValidationError, toolErrorResult, unknownErrorResult } from "../errors.js";
|
|
4
4
|
import { promises as fs } from "node:fs";
|
|
5
|
-
import { resolve as resolvePath, join as joinPath } from "node:path";
|
|
5
|
+
import { resolve as resolvePath, join as joinPath, relative, sep, isAbsolute } from "node:path";
|
|
6
6
|
const bundleRunArtifactsArgsSchema = objectSchema({
|
|
7
7
|
description: "Gather screen capture, memory snapshot, and debugreg into a structured bundle.",
|
|
8
8
|
properties: {
|
|
@@ -41,29 +41,32 @@ export const tools = [
|
|
|
41
41
|
try {
|
|
42
42
|
const parsed = bundleRunArtifactsArgsSchema.parse(args ?? {});
|
|
43
43
|
const runId = parsed.runId;
|
|
44
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(runId) || runId === "." || runId === "..") {
|
|
45
|
+
throw new ToolValidationError("runId must be a non-empty safe filename (letters, digits, dot, underscore, and dash only).", { path: "$.runId" });
|
|
46
|
+
}
|
|
44
47
|
const outputPath = resolvePath(String(parsed.outputPath));
|
|
45
48
|
const captureScreen = parsed.captureScreen !== false;
|
|
46
49
|
const captureDebugReg = parsed.captureDebugReg !== false;
|
|
47
50
|
const memoryRanges = (parsed.memoryRanges ?? []);
|
|
48
51
|
await fs.mkdir(outputPath, { recursive: true });
|
|
49
|
-
const runPath =
|
|
52
|
+
const runPath = containedPath(outputPath, runId);
|
|
50
53
|
await fs.mkdir(runPath, { recursive: true });
|
|
51
54
|
const artifacts = {};
|
|
52
55
|
// Capture screen
|
|
53
56
|
if (captureScreen) {
|
|
54
57
|
const screen = await ctx.client.readScreen();
|
|
55
|
-
const screenPath =
|
|
58
|
+
const screenPath = containedPath(runPath, "screen.txt");
|
|
56
59
|
await fs.writeFile(screenPath, screen, "utf8");
|
|
57
60
|
artifacts.screen = screenPath;
|
|
58
61
|
}
|
|
59
62
|
// Capture memory ranges
|
|
60
63
|
if (memoryRanges.length > 0) {
|
|
61
|
-
const memoryPath =
|
|
64
|
+
const memoryPath = containedPath(runPath, "memory");
|
|
62
65
|
await fs.mkdir(memoryPath, { recursive: true });
|
|
63
66
|
for (let i = 0; i < memoryRanges.length; i++) {
|
|
64
67
|
const range = memoryRanges[i];
|
|
65
68
|
const result = await ctx.client.readMemory(range.address, String(range.length));
|
|
66
|
-
const rangePath =
|
|
69
|
+
const rangePath = containedPath(memoryPath, `range_${i}.hex`);
|
|
67
70
|
await fs.writeFile(rangePath, result.data ?? "", "utf8");
|
|
68
71
|
artifacts[`memory_range_${i}`] = rangePath;
|
|
69
72
|
}
|
|
@@ -71,12 +74,12 @@ export const tools = [
|
|
|
71
74
|
// Capture debugreg
|
|
72
75
|
if (captureDebugReg) {
|
|
73
76
|
const debugreg = await ctx.client.debugregRead();
|
|
74
|
-
const debugPath =
|
|
77
|
+
const debugPath = containedPath(runPath, "debugreg.json");
|
|
75
78
|
await fs.writeFile(debugPath, JSON.stringify(debugreg, null, 2), "utf8");
|
|
76
79
|
artifacts.debugreg = debugPath;
|
|
77
80
|
}
|
|
78
81
|
// Write manifest
|
|
79
|
-
const manifestPath =
|
|
82
|
+
const manifestPath = containedPath(runPath, "manifest.json");
|
|
80
83
|
const manifest = {
|
|
81
84
|
runId,
|
|
82
85
|
createdAt: new Date().toISOString(),
|
|
@@ -93,3 +96,12 @@ export const tools = [
|
|
|
93
96
|
},
|
|
94
97
|
},
|
|
95
98
|
];
|
|
99
|
+
function containedPath(base, name) {
|
|
100
|
+
if (!name || isAbsolute(name) || name.includes("/") || name.includes("\\"))
|
|
101
|
+
throw new ToolValidationError("Unsafe artifact filename.");
|
|
102
|
+
const candidate = resolvePath(joinPath(base, name));
|
|
103
|
+
const rel = relative(base, candidate);
|
|
104
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
105
|
+
throw new ToolValidationError("Artifact path escapes its output directory.");
|
|
106
|
+
return candidate;
|
|
107
|
+
}
|
|
@@ -86,6 +86,13 @@ async function ensureTasksLoaded() {
|
|
|
86
86
|
if (parsed && Array.isArray(parsed.tasks)) {
|
|
87
87
|
for (const pt of parsed.tasks) {
|
|
88
88
|
const t = fromPersistedTask(pt);
|
|
89
|
+
if (t.status === "running") {
|
|
90
|
+
t.status = "stopped";
|
|
91
|
+
t.stoppedAt = new Date();
|
|
92
|
+
t.updatedAt = new Date();
|
|
93
|
+
t.lastError = "Interrupted by MCP server restart; start the task again to resume monitoring.";
|
|
94
|
+
t.nextRunAt = null;
|
|
95
|
+
}
|
|
89
96
|
TASKS.set(t.name, t);
|
|
90
97
|
}
|
|
91
98
|
}
|
|
@@ -184,7 +191,10 @@ function scheduleNextRun(task, ctx) {
|
|
|
184
191
|
if (task.status !== "running")
|
|
185
192
|
return;
|
|
186
193
|
try {
|
|
187
|
-
await runOperation(task.operation, task.args, ctx);
|
|
194
|
+
const result = await runOperation(task.operation, task.args, ctx);
|
|
195
|
+
if (result && typeof result === "object" && "success" in result && result.success === false) {
|
|
196
|
+
throw new Error(`Operation returned failure: ${JSON.stringify(result.details ?? result)}`);
|
|
197
|
+
}
|
|
188
198
|
task.iterations += 1;
|
|
189
199
|
task.updatedAt = new Date();
|
|
190
200
|
if (task.maxIterations && task.iterations >= task.maxIterations) {
|
package/dist/tools/meta/index.js
CHANGED
|
@@ -30,7 +30,7 @@ export const metaModule = defineToolModule({
|
|
|
30
30
|
summary: "High-level meta tools that orchestrate multiple MCP actions.",
|
|
31
31
|
resources: ["c64://guide/bootstrap", "c64://assembly/6510-spec"],
|
|
32
32
|
defaultTags: ["meta", "orchestration", "experimental"],
|
|
33
|
-
supportedPlatforms: ["c64u", "vice"],
|
|
33
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
34
34
|
workflowHints: [
|
|
35
35
|
"Use meta tools to reduce round-trips by composing several steps into one.",
|
|
36
36
|
],
|