c64bridge 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -135
- package/data/assembly/assembly-spec.md +1 -1
- package/data/context/bootstrap.md +3 -3
- package/data/context/fast-paths.md +3 -1
- package/data/graphics/petscii-style-guide.md +1 -1
- package/data/graphics/sprite-charset-best-practices.md +14 -13
- package/data/sound/sid-file-structure.md +5 -5
- package/data/sound/sid-programming-best-practices.md +5 -5
- package/data/vice/vice-binary-monitor-spec.md +26 -10
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +107 -8
- package/dist/device.js +43 -3
- package/dist/rag/indexer.js +18 -8
- package/dist/sidwaveCompiler.js +1 -1
- package/dist/tools/debug.js +158 -5
- package/dist/tools/input.js +336 -0
- package/dist/tools/memory.js +326 -1
- package/dist/tools/programRunners.js +45 -6
- package/dist/tools/registry/batch.js +167 -0
- package/dist/tools/registry/graphics.js +101 -1
- package/dist/tools/registry/index.js +4 -0
- package/dist/tools/registry/input.js +2 -0
- package/dist/tools/registry/memory.js +61 -1
- package/dist/tools/responses.js +14 -1
- package/dist/tools/schema.js +4 -5
- package/dist/{assemblyConverter.js → tools/translation/assembler.js} +66 -163
- package/dist/{basicConverter.js → tools/translation/basicTokenizer.js} +1 -1
- package/dist/tools/translation/disassembler.js +150 -0
- package/dist/tools/translation/opcodeInventory.js +126 -0
- package/dist/tools/translation/symbolRegistry.js +49 -0
- package/dist/tools/types.js +19 -4
- package/dist/vice/process.js +43 -2
- package/dist/vice/viceClient.js +5 -1
- package/doc/reviews/audit.md +143 -0
- package/mcp/schemas/c64_batch.schema.json +53 -0
- package/mcp/schemas/c64_config.schema.json +461 -2
- package/mcp/schemas/c64_debug.schema.json +641 -3
- package/mcp/schemas/c64_disk.schema.json +340 -2
- package/mcp/schemas/c64_drive.schema.json +157 -2
- package/mcp/schemas/c64_extract.schema.json +359 -2
- package/mcp/schemas/c64_graphics.schema.json +415 -3
- package/mcp/schemas/c64_input.schema.json +254 -0
- package/mcp/schemas/c64_memory.schema.json +562 -3
- package/mcp/schemas/c64_printer.schema.json +253 -2
- package/mcp/schemas/c64_program.schema.json +499 -2
- package/mcp/schemas/c64_rag.schema.json +82 -2
- package/mcp/schemas/c64_select_backend.schema.json +35 -2
- package/mcp/schemas/c64_sound.schema.json +793 -2
- package/mcp/schemas/c64_stream.schema.json +73 -2
- package/mcp/schemas/c64_system.schema.json +301 -2
- package/mcp/schemas/c64_vice.schema.json +71 -2
- package/mcp/server.json +1 -1
- package/mcp/tools.json +7509 -1929
- package/mcp.json +1 -1
- package/package.json +2 -2
- package/scripts/c64-cli.mjs +2 -2
- package/scripts/run-coverage.mjs +61 -3
- package/scripts/run-tests.mjs +1 -0
- package/scripts/run-tests.ts +3 -0
- package/scripts/update-readme.ts +79 -2
- package/server.json +2 -2
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema, } from "./types.js";
|
|
2
|
+
import { arraySchema, literalSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "./schema.js";
|
|
3
|
+
import { textResult } from "./responses.js";
|
|
4
|
+
import { ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// PETSCII token expansion table
|
|
7
|
+
// Maps {TOKEN} placeholders to PETSCII/control-byte values that are queued via
|
|
8
|
+
// KERNAL keyboard-buffer injection on both C64U and VICE.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const PETSCII_TOKENS = {
|
|
11
|
+
RETURN: 13,
|
|
12
|
+
CR: 13,
|
|
13
|
+
CLR: 147,
|
|
14
|
+
HOME: 19,
|
|
15
|
+
DEL: 20,
|
|
16
|
+
INS: 148,
|
|
17
|
+
INSERT: 148,
|
|
18
|
+
STOP: 3,
|
|
19
|
+
RUN: 131,
|
|
20
|
+
SHIFT_RETURN: 141,
|
|
21
|
+
F1: 133,
|
|
22
|
+
F2: 137,
|
|
23
|
+
F3: 134,
|
|
24
|
+
F4: 138,
|
|
25
|
+
F5: 135,
|
|
26
|
+
F6: 139,
|
|
27
|
+
F7: 136,
|
|
28
|
+
F8: 140,
|
|
29
|
+
UP: 145,
|
|
30
|
+
DOWN: 17,
|
|
31
|
+
LEFT: 157,
|
|
32
|
+
RIGHT: 29,
|
|
33
|
+
BLACK: 144,
|
|
34
|
+
WHITE: 5,
|
|
35
|
+
RED: 28,
|
|
36
|
+
CYAN: 159,
|
|
37
|
+
PURPLE: 156,
|
|
38
|
+
GREEN: 30,
|
|
39
|
+
BLUE: 31,
|
|
40
|
+
YELLOW: 158,
|
|
41
|
+
ORANGE: 129,
|
|
42
|
+
BROWN: 149,
|
|
43
|
+
LIGHT_RED: 150,
|
|
44
|
+
DARK_GREY: 151,
|
|
45
|
+
MEDIUM_GREY: 152,
|
|
46
|
+
LIGHT_GREEN: 153,
|
|
47
|
+
LIGHT_BLUE: 154,
|
|
48
|
+
LIGHT_GREY: 155,
|
|
49
|
+
REVERSE_ON: 18,
|
|
50
|
+
REVERSE_OFF: 146,
|
|
51
|
+
FLASH_ON: 130,
|
|
52
|
+
FLASH_OFF: 148,
|
|
53
|
+
LOWERCASE: 14,
|
|
54
|
+
UPPERCASE: 142,
|
|
55
|
+
ESCAPE: 27,
|
|
56
|
+
TAB: 9,
|
|
57
|
+
CURSOR_UP: 145,
|
|
58
|
+
CURSOR_DOWN: 17,
|
|
59
|
+
CURSOR_LEFT: 157,
|
|
60
|
+
CURSOR_RIGHT: 29,
|
|
61
|
+
};
|
|
62
|
+
function expandPetsciiTokens(text) {
|
|
63
|
+
return text.replace(/\{([^}]+)\}/g, (_match, token) => {
|
|
64
|
+
const upper = token.trim().toUpperCase();
|
|
65
|
+
const code = PETSCII_TOKENS[upper];
|
|
66
|
+
if (code !== undefined) {
|
|
67
|
+
return String.fromCharCode(code);
|
|
68
|
+
}
|
|
69
|
+
// Numeric form: {$1D} or {29}
|
|
70
|
+
let num;
|
|
71
|
+
if (upper.startsWith("$")) {
|
|
72
|
+
num = parseInt(upper.slice(1), 16);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
num = parseInt(upper, 10);
|
|
76
|
+
}
|
|
77
|
+
if (!isNaN(num) && num >= 0 && num <= 255) {
|
|
78
|
+
return String.fromCharCode(num);
|
|
79
|
+
}
|
|
80
|
+
return _match; // leave unrecognised tokens as-is
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Joystick helpers
|
|
85
|
+
// Port 2 → CIA1 Port A ($DC00), Port 1 → CIA1 Port B ($DC01)
|
|
86
|
+
// Bits 0-4: Up / Down / Left / Right / Fire, active LOW (0 = pressed)
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
const JOYSTICK_PORT_ADDRESS = {
|
|
89
|
+
1: 0xDC01,
|
|
90
|
+
2: 0xDC00,
|
|
91
|
+
};
|
|
92
|
+
const JOYSTICK_BIT = {
|
|
93
|
+
up: 0,
|
|
94
|
+
down: 1,
|
|
95
|
+
left: 2,
|
|
96
|
+
right: 3,
|
|
97
|
+
fire: 4,
|
|
98
|
+
};
|
|
99
|
+
function joystickByte(controls) {
|
|
100
|
+
let mask = 0xff;
|
|
101
|
+
for (const ctrl of controls) {
|
|
102
|
+
const bit = JOYSTICK_BIT[ctrl.toLowerCase()];
|
|
103
|
+
if (bit !== undefined) {
|
|
104
|
+
mask &= ~(1 << bit);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return mask & 0xff;
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Schemas
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
export const writeTextArgsSchema = objectSchema({
|
|
113
|
+
description: "Send a text string to the keyboard buffer, with PETSCII token expansion.",
|
|
114
|
+
properties: {
|
|
115
|
+
op: literalSchema("write_text"),
|
|
116
|
+
text: stringSchema({
|
|
117
|
+
description: "Text to type. Use {RETURN}, {CLR}, {F1}…{F8}, {UP/DOWN/LEFT/RIGHT}, colour names, etc.",
|
|
118
|
+
minLength: 1,
|
|
119
|
+
maxLength: 512,
|
|
120
|
+
}),
|
|
121
|
+
delayMs: optionalSchema(numberSchema({
|
|
122
|
+
description: "Delay in ms after sending (default 0).",
|
|
123
|
+
integer: true,
|
|
124
|
+
minimum: 0,
|
|
125
|
+
maximum: 10000,
|
|
126
|
+
default: 0,
|
|
127
|
+
})),
|
|
128
|
+
},
|
|
129
|
+
required: ["op", "text"],
|
|
130
|
+
additionalProperties: false,
|
|
131
|
+
});
|
|
132
|
+
export const keyArgsSchema = objectSchema({
|
|
133
|
+
description: "Tap a single key or hold it for a duration.",
|
|
134
|
+
properties: {
|
|
135
|
+
op: literalSchema("key"),
|
|
136
|
+
key: stringSchema({
|
|
137
|
+
description: "Key to press: a printable character, or a PETSCII token name (RETURN, F1…F8, UP, etc.).",
|
|
138
|
+
minLength: 1,
|
|
139
|
+
maxLength: 32,
|
|
140
|
+
}),
|
|
141
|
+
durationMs: optionalSchema(numberSchema({
|
|
142
|
+
description: "Delay in ms after each queued key press before the next press (default 0 = no delay).",
|
|
143
|
+
integer: true,
|
|
144
|
+
minimum: 0,
|
|
145
|
+
maximum: 5000,
|
|
146
|
+
default: 0,
|
|
147
|
+
})),
|
|
148
|
+
count: optionalSchema(numberSchema({
|
|
149
|
+
description: "Number of times to press the key (default 1).",
|
|
150
|
+
integer: true,
|
|
151
|
+
minimum: 1,
|
|
152
|
+
maximum: 100,
|
|
153
|
+
default: 1,
|
|
154
|
+
})),
|
|
155
|
+
},
|
|
156
|
+
required: ["op", "key"],
|
|
157
|
+
additionalProperties: false,
|
|
158
|
+
});
|
|
159
|
+
export const joystickArgsSchema = objectSchema({
|
|
160
|
+
description: "Simulate joystick input by writing directly to CIA1 Port A/B registers.",
|
|
161
|
+
properties: {
|
|
162
|
+
op: literalSchema("joystick"),
|
|
163
|
+
port: numberSchema({
|
|
164
|
+
description: "Joystick port (1 = $DC01, 2 = $DC00).",
|
|
165
|
+
integer: true,
|
|
166
|
+
minimum: 1,
|
|
167
|
+
maximum: 2,
|
|
168
|
+
}),
|
|
169
|
+
controls: arraySchema(stringSchema({
|
|
170
|
+
description: "Control to activate.",
|
|
171
|
+
enum: ["up", "down", "left", "right", "fire"],
|
|
172
|
+
}), {
|
|
173
|
+
description: "List of controls to activate simultaneously.",
|
|
174
|
+
minItems: 0,
|
|
175
|
+
}),
|
|
176
|
+
action: stringSchema({
|
|
177
|
+
description: "press = hold until a release call; release = restore all bits; tap = press then release after durationMs.",
|
|
178
|
+
enum: ["press", "release", "tap"],
|
|
179
|
+
}),
|
|
180
|
+
durationMs: optionalSchema(numberSchema({
|
|
181
|
+
description: "Duration to hold for tap action in ms (default 80).",
|
|
182
|
+
integer: true,
|
|
183
|
+
minimum: 10,
|
|
184
|
+
maximum: 5000,
|
|
185
|
+
default: 80,
|
|
186
|
+
})),
|
|
187
|
+
},
|
|
188
|
+
required: ["op", "port", "controls", "action"],
|
|
189
|
+
additionalProperties: false,
|
|
190
|
+
});
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Handlers
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
function textToPetsciiBytes(text) {
|
|
195
|
+
const bytes = [];
|
|
196
|
+
for (let i = 0; i < text.length; i++) {
|
|
197
|
+
bytes.push(text.charCodeAt(i) & 0xff);
|
|
198
|
+
}
|
|
199
|
+
return Uint8Array.from(bytes);
|
|
200
|
+
}
|
|
201
|
+
const inputOperationHandlers = {
|
|
202
|
+
write_text: async (args, ctx) => {
|
|
203
|
+
try {
|
|
204
|
+
const parsed = writeTextArgsSchema.parse(args);
|
|
205
|
+
const expanded = expandPetsciiTokens(parsed.text);
|
|
206
|
+
const petsciiBytes = textToPetsciiBytes(expanded);
|
|
207
|
+
await ctx.client.injectKeyboardQueue(petsciiBytes);
|
|
208
|
+
if (parsed.delayMs && parsed.delayMs > 0) {
|
|
209
|
+
await new Promise((res) => setTimeout(res, parsed.delayMs));
|
|
210
|
+
}
|
|
211
|
+
ctx.logger.info("Sent keyboard text", { length: expanded.length });
|
|
212
|
+
return textResult(`Sent ${expanded.length} character(s) to keyboard buffer.`, { success: true, length: expanded.length });
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (error instanceof ToolValidationError)
|
|
216
|
+
return toolErrorResult(error);
|
|
217
|
+
return unknownErrorResult(error);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
key: async (args, ctx) => {
|
|
221
|
+
try {
|
|
222
|
+
const parsed = keyArgsSchema.parse(args);
|
|
223
|
+
const count = parsed.count ?? 1;
|
|
224
|
+
const durationMs = parsed.durationMs ?? 0;
|
|
225
|
+
// Resolve key to a character: try token first, then single char
|
|
226
|
+
const tokenCode = PETSCII_TOKENS[parsed.key.toUpperCase()];
|
|
227
|
+
const keyByte = tokenCode !== undefined
|
|
228
|
+
? tokenCode
|
|
229
|
+
: parsed.key.length === 1
|
|
230
|
+
? parsed.key.charCodeAt(0) & 0xff
|
|
231
|
+
: (() => {
|
|
232
|
+
throw new ToolValidationError(`Unrecognised key: ${parsed.key}. Use a single character or a PETSCII token name.`, { path: "$.key" });
|
|
233
|
+
})();
|
|
234
|
+
for (let i = 0; i < count; i++) {
|
|
235
|
+
await ctx.client.injectKeyboardQueue(Uint8Array.of(keyByte));
|
|
236
|
+
if (durationMs > 0) {
|
|
237
|
+
await new Promise((res) => setTimeout(res, durationMs));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
ctx.logger.info("Pressed key", { key: parsed.key, count, durationMs });
|
|
241
|
+
return textResult(`Pressed '${parsed.key}' ${count} time${count === 1 ? "" : "s"}.`, { success: true, key: parsed.key, count, durationMs });
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
if (error instanceof ToolValidationError)
|
|
245
|
+
return toolErrorResult(error);
|
|
246
|
+
return unknownErrorResult(error);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
joystick: async (args, ctx) => {
|
|
250
|
+
try {
|
|
251
|
+
const parsed = joystickArgsSchema.parse(args);
|
|
252
|
+
const port = parsed.port;
|
|
253
|
+
const addr = JOYSTICK_PORT_ADDRESS[port];
|
|
254
|
+
const durationMs = parsed.durationMs ?? 80;
|
|
255
|
+
const pressedByte = joystickByte(parsed.controls);
|
|
256
|
+
const releasedByte = 0xff;
|
|
257
|
+
if (parsed.action === "release") {
|
|
258
|
+
await ctx.client.viceMemSet(addr, Uint8Array.of(releasedByte));
|
|
259
|
+
ctx.logger.info("Released joystick", { port, address: `$${addr.toString(16).toUpperCase()}` });
|
|
260
|
+
return textResult(`Joystick port ${port} released.`, { success: true, port, action: "release" });
|
|
261
|
+
}
|
|
262
|
+
if (parsed.action === "press") {
|
|
263
|
+
await ctx.client.viceMemSet(addr, Uint8Array.of(pressedByte));
|
|
264
|
+
ctx.logger.info("Pressed joystick", { port, controls: parsed.controls, byte: pressedByte });
|
|
265
|
+
return textResult(`Joystick port ${port} pressed: ${parsed.controls.join(", ") || "none"}.`, { success: true, port, action: "press", controls: parsed.controls });
|
|
266
|
+
}
|
|
267
|
+
// tap: press → wait → release
|
|
268
|
+
await ctx.client.viceMemSet(addr, Uint8Array.of(pressedByte));
|
|
269
|
+
await new Promise((res) => setTimeout(res, durationMs));
|
|
270
|
+
await ctx.client.viceMemSet(addr, Uint8Array.of(releasedByte));
|
|
271
|
+
ctx.logger.info("Tapped joystick", { port, controls: parsed.controls, durationMs });
|
|
272
|
+
return textResult(`Joystick port ${port} tapped: ${parsed.controls.join(", ") || "none"} for ${durationMs}ms.`, { success: true, port, action: "tap", controls: parsed.controls, durationMs });
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (error instanceof ToolValidationError)
|
|
276
|
+
return toolErrorResult(error);
|
|
277
|
+
return unknownErrorResult(error);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Module definition
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
export const inputModule = defineToolModule({
|
|
285
|
+
domain: "input",
|
|
286
|
+
summary: "Cross-platform keyboard input plus VICE-only joystick simulation.",
|
|
287
|
+
supportedPlatforms: ["c64u", "vice"],
|
|
288
|
+
resources: ["c64://guide/bootstrap", "c64://memory/map", "c64://io/cia/spec"],
|
|
289
|
+
prompts: [],
|
|
290
|
+
defaultTags: ["input"],
|
|
291
|
+
workflowHints: [
|
|
292
|
+
"Use write_text with {RETURN} tokens to automate BASIC entry; prefer key for individual control keys.",
|
|
293
|
+
"write_text and key inject through the KERNAL keyboard queue ($0277/$00C6) so they work on both C64U and VICE.",
|
|
294
|
+
"Joystick tap is suitable for one-shot moves; use press/release pairs for timed holds. Joystick is VICE-only.",
|
|
295
|
+
],
|
|
296
|
+
tools: [
|
|
297
|
+
{
|
|
298
|
+
name: "c64_input",
|
|
299
|
+
description: "Keyboard buffer injection (cross-platform) and joystick simulation (VICE only).",
|
|
300
|
+
summary: "Types text, taps keys, and simulates joystick movements.",
|
|
301
|
+
inputSchema: discriminatedUnionSchema({
|
|
302
|
+
description: "Input operations: write_text, key, joystick.",
|
|
303
|
+
variants: [
|
|
304
|
+
writeTextArgsSchema.jsonSchema,
|
|
305
|
+
keyArgsSchema.jsonSchema,
|
|
306
|
+
joystickArgsSchema.jsonSchema,
|
|
307
|
+
],
|
|
308
|
+
}),
|
|
309
|
+
operationPlatforms: { joystick: ["vice"] },
|
|
310
|
+
tags: ["input", "keyboard", "joystick"],
|
|
311
|
+
examples: [
|
|
312
|
+
{
|
|
313
|
+
name: "Type BASIC line",
|
|
314
|
+
description: "Enter a BASIC line and press RETURN",
|
|
315
|
+
arguments: { op: "write_text", text: "PRINT \"HELLO\"{RETURN}" },
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "Press F1",
|
|
319
|
+
description: "Send the F1 function key",
|
|
320
|
+
arguments: { op: "key", key: "F1" },
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "Tap joystick right",
|
|
324
|
+
description: "Brief rightward tap on joystick port 2",
|
|
325
|
+
arguments: { op: "joystick", port: 2, controls: ["right"], action: "tap", durationMs: 80 },
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: "Press fire on port 1",
|
|
329
|
+
description: "Hold fire button on joystick port 1",
|
|
330
|
+
arguments: { op: "joystick", port: 1, controls: ["fire"], action: "press" },
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
execute: createOperationDispatcher("c64_input", inputOperationHandlers),
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
});
|