c64bridge 0.9.0 → 0.9.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/README.md +158 -164
- package/data/vice/vice-binary-monitor-spec.md +21 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +76 -2
- package/dist/device.js +43 -3
- package/dist/rag/indexer.js +18 -8
- package/dist/sidwaveCompiler.js +1 -1
- package/dist/{assemblyConverter.js → tools/assember.js} +10 -1
- package/dist/{basicConverter.js → tools/basicTokenizer.js} +1 -1
- package/dist/tools/debug.js +158 -5
- package/dist/tools/disassembler.js +478 -0
- package/dist/tools/input.js +338 -0
- package/dist/tools/memory.js +317 -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/schema.js +4 -5
- package/dist/tools/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/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 +136 -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 +6718 -1257
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/scripts/run-coverage.mjs +7 -0
- package/scripts/run-tests.ts +2 -0
- package/scripts/update-readme.ts +79 -2
- package/server.json +2 -2
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { assemblyToPrgDetailed, AssemblyError } from "./assember.js";
|
|
3
|
+
import { basicToPrg } from "./basicTokenizer.js";
|
|
3
4
|
import { defineToolModule, OPERATION_DISCRIMINATOR, } from "./types.js";
|
|
4
|
-
import { booleanSchema, objectSchema, stringSchema } from "./schema.js";
|
|
5
|
+
import { booleanSchema, objectSchema, optionalSchema, stringSchema } from "./schema.js";
|
|
5
6
|
import { textResult } from "./responses.js";
|
|
6
7
|
import { ToolExecutionError, ToolError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
|
|
7
8
|
import { pollForProgramOutcome } from "./pollValidator.js";
|
|
9
|
+
import { clearViceSymbols, setViceSymbols, parseViceSymbolFile } from "./symbolRegistry.js";
|
|
8
10
|
function extractFailureDetails(details) {
|
|
9
11
|
if (details === undefined || details === null) {
|
|
10
12
|
return undefined;
|
|
@@ -163,6 +165,14 @@ function normalizeRuntimeErrors(errors) {
|
|
|
163
165
|
text: error.raw,
|
|
164
166
|
}));
|
|
165
167
|
}
|
|
168
|
+
export const __programRunnerHelpersForTests = {
|
|
169
|
+
extractFailureDetails,
|
|
170
|
+
toRecord,
|
|
171
|
+
stripOperationDiscriminator,
|
|
172
|
+
parseBasicRuntimeErrors,
|
|
173
|
+
attemptAutoFixBasicProgram,
|
|
174
|
+
normalizeRuntimeErrors,
|
|
175
|
+
};
|
|
166
176
|
function structuredExecutionError(message, data) {
|
|
167
177
|
return {
|
|
168
178
|
content: [
|
|
@@ -221,6 +231,10 @@ const prgFileArgsSchema = objectSchema({
|
|
|
221
231
|
description: prgPathDescription,
|
|
222
232
|
minLength: 1,
|
|
223
233
|
}),
|
|
234
|
+
symbolsFile: optionalSchema(stringSchema({
|
|
235
|
+
description: "Path to a VICE symbol file (.vs) to load alongside the PRG. Symbols are used by the disassemble tool when annotating addresses.",
|
|
236
|
+
minLength: 1,
|
|
237
|
+
})),
|
|
224
238
|
},
|
|
225
239
|
required: ["path"],
|
|
226
240
|
additionalProperties: false,
|
|
@@ -424,9 +438,13 @@ async function executeUploadRunAsm(rawArgs, ctx) {
|
|
|
424
438
|
sourceLength: parsed.program.length,
|
|
425
439
|
...(shouldVerify ? { verify: true } : {}),
|
|
426
440
|
});
|
|
427
|
-
// Assemble locally to expose structured metadata
|
|
428
|
-
const
|
|
441
|
+
// Assemble locally to expose structured metadata and capture symbols
|
|
442
|
+
const asmResult = assemblyToPrgDetailed(parsed.program);
|
|
443
|
+
const prg = asmResult.prg;
|
|
429
444
|
const entryAddress = prg.readUInt16LE(0);
|
|
445
|
+
if (ctx.platform.id === "vice") {
|
|
446
|
+
setViceSymbols(asmResult.symbols.entries());
|
|
447
|
+
}
|
|
430
448
|
const result = await ctx.client.uploadAndRunAsm(parsed.program);
|
|
431
449
|
if (!result.success) {
|
|
432
450
|
return toolErrorResult(new ToolExecutionError("C64 firmware reported failure while running assembly program", {
|
|
@@ -527,6 +545,24 @@ async function executeRunPrg(rawArgs, ctx) {
|
|
|
527
545
|
try {
|
|
528
546
|
const parsed = prgFileArgsSchema.parse(rawArgs ?? {});
|
|
529
547
|
ctx.logger.info("Running PRG file", { path: parsed.path });
|
|
548
|
+
let symbolsLoaded = 0;
|
|
549
|
+
if (ctx.platform.id === "vice") {
|
|
550
|
+
clearViceSymbols();
|
|
551
|
+
if (parsed.symbolsFile) {
|
|
552
|
+
try {
|
|
553
|
+
const content = await fs.promises.readFile(parsed.symbolsFile, "utf8");
|
|
554
|
+
const syms = parseViceSymbolFile(content);
|
|
555
|
+
setViceSymbols(syms.entries());
|
|
556
|
+
symbolsLoaded = syms.size;
|
|
557
|
+
ctx.logger.info("Loaded VICE symbols from file", { path: parsed.symbolsFile, count: symbolsLoaded });
|
|
558
|
+
}
|
|
559
|
+
catch (fileError) {
|
|
560
|
+
throw new ToolExecutionError(`Failed to read symbols file: ${parsed.symbolsFile}`, {
|
|
561
|
+
details: { message: fileError instanceof Error ? fileError.message : String(fileError) },
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
530
566
|
const result = await ctx.client.runPrgFile(parsed.path);
|
|
531
567
|
if (!result.success) {
|
|
532
568
|
throw new ToolExecutionError("C64 firmware reported failure while running PRG", {
|
|
@@ -539,12 +575,15 @@ async function executeRunPrg(rawArgs, ctx) {
|
|
|
539
575
|
path: parsed.path,
|
|
540
576
|
entryAddress: null,
|
|
541
577
|
resources: ["c64://guide/bootstrap"],
|
|
578
|
+
...(symbolsLoaded > 0 ? { symbolsLoaded } : {}),
|
|
542
579
|
};
|
|
543
|
-
const
|
|
580
|
+
const symbolNote = symbolsLoaded > 0 ? ` Loaded ${symbolsLoaded} debug symbol(s).` : "";
|
|
581
|
+
const base = textResult(`PRG ${parsed.path} loaded and executed.${symbolNote}`, {
|
|
544
582
|
success: true,
|
|
545
583
|
path: parsed.path,
|
|
546
584
|
entryAddress: null,
|
|
547
585
|
details: toRecord(result.details) ?? null,
|
|
586
|
+
...(symbolsLoaded > 0 ? { symbolsLoaded } : {}),
|
|
548
587
|
});
|
|
549
588
|
return { ...base, structuredContent: { type: "json", data } };
|
|
550
589
|
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { defineToolModule, } from "../types.js";
|
|
2
|
+
import { getPlatformStatus } from "../../platform.js";
|
|
3
|
+
import { arraySchema, booleanSchema, objectSchema, optionalSchema, stringSchema, } from "../schema.js";
|
|
4
|
+
import { jsonResult } from "../responses.js";
|
|
5
|
+
import { ToolValidationError, unknownErrorResult } from "../errors.js";
|
|
6
|
+
const commandSchema = objectSchema({
|
|
7
|
+
description: "A single batch command.",
|
|
8
|
+
properties: {
|
|
9
|
+
tool: stringSchema({ description: "MCP tool name (e.g. c64_memory, c64_debug).", minLength: 1 }),
|
|
10
|
+
args: objectSchema({
|
|
11
|
+
description: "Arguments to pass to the tool (must include 'op' for grouped tools).",
|
|
12
|
+
properties: {},
|
|
13
|
+
additionalProperties: true,
|
|
14
|
+
}),
|
|
15
|
+
description: optionalSchema(stringSchema({ description: "Optional label for this command in the result.", maxLength: 128 })),
|
|
16
|
+
},
|
|
17
|
+
required: ["tool", "args"],
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
});
|
|
20
|
+
const executeBatchArgsSchema = objectSchema({
|
|
21
|
+
description: "Execute multiple tool calls in one request.",
|
|
22
|
+
properties: {
|
|
23
|
+
commands: arraySchema(commandSchema, {
|
|
24
|
+
description: "Ordered list of commands to execute.",
|
|
25
|
+
minItems: 1,
|
|
26
|
+
}),
|
|
27
|
+
stopOnError: optionalSchema(booleanSchema({
|
|
28
|
+
description: "Halt the batch after the first failure. Default: true.",
|
|
29
|
+
default: true,
|
|
30
|
+
})),
|
|
31
|
+
},
|
|
32
|
+
required: ["commands"],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
});
|
|
35
|
+
export const batchModuleGroup = defineToolModule({
|
|
36
|
+
domain: "batch",
|
|
37
|
+
summary: "Execute multiple tool calls in a single MCP request for reduced round-trip overhead.",
|
|
38
|
+
supportedPlatforms: ["c64u", "vice"],
|
|
39
|
+
resources: [],
|
|
40
|
+
prompts: [],
|
|
41
|
+
defaultTags: ["batch", "orchestration"],
|
|
42
|
+
workflowHints: [
|
|
43
|
+
"Use execute_batch when you need to run 3 or more sequential operations — it eliminates per-call network overhead.",
|
|
44
|
+
"Each command must include 'tool' and 'args'; grouped tools require 'op' inside args.",
|
|
45
|
+
],
|
|
46
|
+
tools: [
|
|
47
|
+
{
|
|
48
|
+
name: "c64_batch",
|
|
49
|
+
description: "Execute multiple c64bridge tool calls in a single request. Reduces latency for multi-step workflows.",
|
|
50
|
+
summary: "Runs an ordered list of tool commands and returns all results with per-command timing.",
|
|
51
|
+
inputSchema: executeBatchArgsSchema.jsonSchema,
|
|
52
|
+
tags: ["batch", "orchestration", "grouped"],
|
|
53
|
+
examples: [
|
|
54
|
+
{
|
|
55
|
+
name: "Read registers then step",
|
|
56
|
+
description: "Get registers, step one instruction, get registers again",
|
|
57
|
+
arguments: {
|
|
58
|
+
commands: [
|
|
59
|
+
{ tool: "c64_debug", args: { op: "get_registers" }, description: "before" },
|
|
60
|
+
{ tool: "c64_debug", args: { op: "step", count: 1 }, description: "step" },
|
|
61
|
+
{ tool: "c64_debug", args: { op: "get_registers" }, description: "after" },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "Clear screen and type text",
|
|
67
|
+
description: "Fill screen RAM then feed keyboard",
|
|
68
|
+
arguments: {
|
|
69
|
+
commands: [
|
|
70
|
+
{ tool: "c64_memory", args: { op: "fill_memory", address: "$0400", length: 1000, pattern: "20" } },
|
|
71
|
+
{ tool: "c64_input", args: { op: "write_text", text: "HELLO{RETURN}" } },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
execute: async (rawArgs, ctx) => {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = executeBatchArgsSchema.parse(rawArgs);
|
|
79
|
+
const stopOnError = parsed.stopOnError !== false;
|
|
80
|
+
// Lazy import to avoid circular dependency at module-init time.
|
|
81
|
+
const { toolRegistry } = await import("./index.js");
|
|
82
|
+
const results = [];
|
|
83
|
+
let succeeded = 0;
|
|
84
|
+
let failed = 0;
|
|
85
|
+
const batchStart = Date.now();
|
|
86
|
+
for (let i = 0; i < parsed.commands.length; i++) {
|
|
87
|
+
const cmd = parsed.commands[i];
|
|
88
|
+
const toolName = typeof cmd.tool === "string" ? cmd.tool : "";
|
|
89
|
+
const args = (typeof cmd.args === "object" && cmd.args !== null) ? cmd.args : {};
|
|
90
|
+
const label = typeof cmd.description === "string" ? cmd.description : undefined;
|
|
91
|
+
if (!toolName) {
|
|
92
|
+
const err = { index: i, tool: String(cmd.tool), description: label, success: false, error: "tool name must be a non-empty string", elapsedMs: 0 };
|
|
93
|
+
results.push(err);
|
|
94
|
+
failed++;
|
|
95
|
+
if (stopOnError)
|
|
96
|
+
break;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (toolName === "c64_batch") {
|
|
100
|
+
results.push({
|
|
101
|
+
index: i,
|
|
102
|
+
tool: toolName,
|
|
103
|
+
description: label,
|
|
104
|
+
success: false,
|
|
105
|
+
error: "Nested c64_batch execution is not supported.",
|
|
106
|
+
elapsedMs: 0,
|
|
107
|
+
});
|
|
108
|
+
failed++;
|
|
109
|
+
if (stopOnError)
|
|
110
|
+
break;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const cmdStart = Date.now();
|
|
114
|
+
try {
|
|
115
|
+
const toolResult = await toolRegistry.invoke(toolName, args, {
|
|
116
|
+
...ctx,
|
|
117
|
+
platform: getPlatformStatus(),
|
|
118
|
+
});
|
|
119
|
+
const elapsedMs = Date.now() - cmdStart;
|
|
120
|
+
const isError = toolResult.isError === true;
|
|
121
|
+
const text = toolResult.content.map((c) => c.text).join("\n");
|
|
122
|
+
const resultPayload = toolResult.structuredContent?.data ?? text;
|
|
123
|
+
if (isError) {
|
|
124
|
+
results.push({ index: i, tool: toolName, description: label, success: false, error: text, elapsedMs });
|
|
125
|
+
failed++;
|
|
126
|
+
if (stopOnError)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
results.push({ index: i, tool: toolName, description: label, success: true, result: resultPayload, elapsedMs });
|
|
131
|
+
succeeded++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const elapsedMs = Date.now() - cmdStart;
|
|
136
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
137
|
+
results.push({ index: i, tool: toolName, description: label, success: false, error: message, elapsedMs });
|
|
138
|
+
failed++;
|
|
139
|
+
if (stopOnError)
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const totalElapsedMs = Date.now() - batchStart;
|
|
144
|
+
const summary = {
|
|
145
|
+
total: parsed.commands.length,
|
|
146
|
+
executed: results.length,
|
|
147
|
+
succeeded,
|
|
148
|
+
failed,
|
|
149
|
+
elapsedMs: totalElapsedMs,
|
|
150
|
+
results,
|
|
151
|
+
};
|
|
152
|
+
ctx.logger.info("Batch complete", { total: parsed.commands.length, succeeded, failed, elapsedMs: totalElapsedMs });
|
|
153
|
+
return jsonResult(summary, { success: failed === 0, succeeded, failed, total: parsed.commands.length });
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error instanceof ToolValidationError) {
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text", text: error.message }],
|
|
159
|
+
isError: true,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return unknownErrorResult(error);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
});
|
|
@@ -3,8 +3,8 @@ import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema,
|
|
|
3
3
|
import { graphicsModule, graphicsOperationHandlers as groupedGraphicsHandlers } from "../graphics.js";
|
|
4
4
|
import { booleanSchema, literalSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "../schema.js";
|
|
5
5
|
import { buildDescriptorIndex, ensureDescriptor, extendSchemaWithOp, createOperationHandlers, } from "./utils.js";
|
|
6
|
-
import { ToolError, toolErrorResult, unknownErrorResult, } from "../errors.js";
|
|
7
6
|
import { jsonResult } from "../responses.js";
|
|
7
|
+
import { ToolError, toolErrorResult, unknownErrorResult, } from "../errors.js";
|
|
8
8
|
const graphicsDescriptorIndex = buildDescriptorIndex(graphicsModule);
|
|
9
9
|
const captureFrameArgsSchema = objectSchema({
|
|
10
10
|
description: "Capture one or more complete video frames from the active backend.",
|
|
@@ -30,6 +30,72 @@ const captureFrameArgsSchema = objectSchema({
|
|
|
30
30
|
required: ["op"],
|
|
31
31
|
additionalProperties: false,
|
|
32
32
|
});
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// get_display_state helpers — VIC-II register decoding
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
function parseVicState(d011, d016, d018, dd00, d020, d021) {
|
|
37
|
+
const bank = (~dd00) & 0x03; // CIA2 Port A bits 0-1 select VIC bank (inverted)
|
|
38
|
+
const bankBase = bank * 0x4000;
|
|
39
|
+
const bitmapMode = (d011 & 0x20) !== 0;
|
|
40
|
+
const extendedColor = (d011 & 0x40) !== 0;
|
|
41
|
+
const multicolor = (d016 & 0x10) !== 0;
|
|
42
|
+
const screenVisible = (d011 & 0x10) !== 0;
|
|
43
|
+
const raster8 = (d011 & 0x80) !== 0;
|
|
44
|
+
const vertScroll = d011 & 0x07;
|
|
45
|
+
const horizScroll = d016 & 0x07;
|
|
46
|
+
const columns40 = (d016 & 0x08) !== 0;
|
|
47
|
+
const rows25 = (d011 & 0x08) !== 0;
|
|
48
|
+
// D018 bits 4-7 = screen RAM offset (× $0400); bits 1-3 = char/bitmap pointer (× $0800)
|
|
49
|
+
const screenRamOffset = ((d018 >> 4) & 0x0f) * 0x0400;
|
|
50
|
+
const screenRamAddress = bankBase + screenRamOffset;
|
|
51
|
+
const charOrBitmapPointer = ((d018 >> 1) & 0x07) * 0x0800;
|
|
52
|
+
const charOrBitmapAddress = bankBase + charOrBitmapPointer;
|
|
53
|
+
let mode;
|
|
54
|
+
if (bitmapMode && multicolor)
|
|
55
|
+
mode = "multicolor_bitmap";
|
|
56
|
+
else if (bitmapMode)
|
|
57
|
+
mode = "hires_bitmap";
|
|
58
|
+
else if (extendedColor)
|
|
59
|
+
mode = "extended_color_text";
|
|
60
|
+
else if (multicolor)
|
|
61
|
+
mode = "multicolor_text";
|
|
62
|
+
else
|
|
63
|
+
mode = "standard_text";
|
|
64
|
+
return {
|
|
65
|
+
mode,
|
|
66
|
+
bank,
|
|
67
|
+
bankBase: `$${bankBase.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
68
|
+
screenRamAddress: `$${screenRamAddress.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
69
|
+
charOrBitmapAddress: `$${charOrBitmapAddress.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
70
|
+
screenVisible,
|
|
71
|
+
bitmapMode,
|
|
72
|
+
extendedColor,
|
|
73
|
+
multicolor,
|
|
74
|
+
rows25,
|
|
75
|
+
columns40,
|
|
76
|
+
vertScroll,
|
|
77
|
+
horizScroll,
|
|
78
|
+
raster8,
|
|
79
|
+
borderColor: d020 & 0x0f,
|
|
80
|
+
backgroundColor: d021 & 0x0f,
|
|
81
|
+
registers: {
|
|
82
|
+
d011: `$${d011.toString(16).toUpperCase().padStart(2, "0")}`,
|
|
83
|
+
d016: `$${d016.toString(16).toUpperCase().padStart(2, "0")}`,
|
|
84
|
+
d018: `$${d018.toString(16).toUpperCase().padStart(2, "0")}`,
|
|
85
|
+
d020: `$${d020.toString(16).toUpperCase().padStart(2, "0")}`,
|
|
86
|
+
d021: `$${d021.toString(16).toUpperCase().padStart(2, "0")}`,
|
|
87
|
+
dd00: `$${dd00.toString(16).toUpperCase().padStart(2, "0")}`,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const getDisplayStateArgsSchema = objectSchema({
|
|
92
|
+
description: "Read VIC-II and CIA2 registers to determine the current graphics mode and memory layout. Works on C64U and VICE; both backends are read through shared memory primitives so the response shape is identical.",
|
|
93
|
+
properties: {
|
|
94
|
+
op: literalSchema("get_display_state"),
|
|
95
|
+
},
|
|
96
|
+
required: ["op"],
|
|
97
|
+
additionalProperties: false,
|
|
98
|
+
});
|
|
33
99
|
const graphicsOperations = [
|
|
34
100
|
{
|
|
35
101
|
op: "capture_frame",
|
|
@@ -105,6 +171,35 @@ const graphicsOperations = [
|
|
|
105
171
|
schema: extendSchemaWithOp("render_bitmap", ensureDescriptor(graphicsDescriptorIndex, "render_bitmap").inputSchema, { description: "Import an image file, convert it to VIC-II bitmap memory, write it into RAM, and display it." }),
|
|
106
172
|
handler: groupedGraphicsHandlers.render_bitmap,
|
|
107
173
|
},
|
|
174
|
+
{
|
|
175
|
+
op: "get_display_state",
|
|
176
|
+
schema: getDisplayStateArgsSchema.jsonSchema,
|
|
177
|
+
handler: async (_rawArgs, ctx) => {
|
|
178
|
+
try {
|
|
179
|
+
// Use cross-platform memory reads so the same handler works on both
|
|
180
|
+
// C64U and VICE. The VIC-II and CIA2 registers live in machine memory
|
|
181
|
+
// either way; the underlying facade hides the transport difference.
|
|
182
|
+
const vicRegs = await ctx.client.readMemoryRaw(0xD011, 8);
|
|
183
|
+
const colorRegs = await ctx.client.readMemoryRaw(0xD020, 2);
|
|
184
|
+
const cia2 = await ctx.client.readMemoryRaw(0xDD00, 1);
|
|
185
|
+
const d011 = vicRegs[0] ?? 0;
|
|
186
|
+
const d016 = vicRegs[5] ?? 0; // $D016 = $D011 + 5
|
|
187
|
+
const d018 = vicRegs[7] ?? 0; // $D018 = $D011 + 7
|
|
188
|
+
const d020 = colorRegs[0] ?? 0;
|
|
189
|
+
const d021 = colorRegs[1] ?? 0;
|
|
190
|
+
const dd00 = cia2[0] ?? 0x03;
|
|
191
|
+
const backend = await ctx.client.getActiveBackendType();
|
|
192
|
+
const state = { ...parseVicState(d011, d016, d018, dd00, d020, d021), backend };
|
|
193
|
+
ctx.logger.info("Read VIC-II display state", { mode: state.mode, bank: state.bank, backend });
|
|
194
|
+
return jsonResult(state, { success: true, mode: state.mode, backend });
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error instanceof ToolError)
|
|
198
|
+
return toolErrorResult(error);
|
|
199
|
+
return unknownErrorResult(error);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
},
|
|
108
203
|
];
|
|
109
204
|
const graphicsOperationHandlers = createOperationHandlers(graphicsOperations);
|
|
110
205
|
export const graphicsModuleGroup = defineToolModule({
|
|
@@ -154,6 +249,11 @@ export const graphicsModuleGroup = defineToolModule({
|
|
|
154
249
|
description: "Convert a PNG into VIC bitmap memory and display it",
|
|
155
250
|
arguments: { op: "render_bitmap", imagePath: "./artifacts/sample.png", format: "hires" },
|
|
156
251
|
},
|
|
252
|
+
{
|
|
253
|
+
name: "Read VIC-II state",
|
|
254
|
+
description: "Decode graphics mode, memory layout, and colours from VIC-II registers (cross-platform)",
|
|
255
|
+
arguments: { op: "get_display_state" },
|
|
256
|
+
},
|
|
157
257
|
],
|
|
158
258
|
execute: createOperationDispatcher("c64_graphics", graphicsOperationHandlers),
|
|
159
259
|
},
|
|
@@ -14,6 +14,8 @@ import { extractModule } from "./extract.js";
|
|
|
14
14
|
import { streamModule } from "./stream.js";
|
|
15
15
|
import { debugModuleGroup as debugModule } from "../debug.js";
|
|
16
16
|
import { viceModuleGroup as viceModule } from "../vice.js";
|
|
17
|
+
import { inputModuleGroup as inputModule } from "./input.js";
|
|
18
|
+
import { batchModuleGroup as batchModule } from "./batch.js";
|
|
17
19
|
const modules = [
|
|
18
20
|
programModule,
|
|
19
21
|
memoryModule,
|
|
@@ -30,6 +32,8 @@ const modules = [
|
|
|
30
32
|
streamModule,
|
|
31
33
|
debugModule,
|
|
32
34
|
viceModule,
|
|
35
|
+
inputModule,
|
|
36
|
+
batchModule,
|
|
33
37
|
];
|
|
34
38
|
const toolMap = new Map();
|
|
35
39
|
for (const module of modules) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema, } from "../types.js";
|
|
2
|
-
import { memoryModule, memoryOperationHandlers as groupedMemoryHandlers } from "../memory.js";
|
|
2
|
+
import { memoryModule, memoryOperationHandlers as groupedMemoryHandlers, disassembleArgsSchema, copyMemoryArgsSchema, fillMemoryArgsSchema, searchMemoryArgsSchema, compareMemoryArgsSchema, saveMemoryArgsSchema, } from "../memory.js";
|
|
3
3
|
import { metaModule } from "../meta/index.js";
|
|
4
4
|
import { buildDescriptorIndex, ensureDescriptor, extendSchemaWithOp, createOperationHandlers, invokeModuleTool, } from "./utils.js";
|
|
5
5
|
const memoryDescriptorIndex = buildDescriptorIndex(memoryModule);
|
|
@@ -25,6 +25,36 @@ const memoryOperations = [
|
|
|
25
25
|
schema: extendSchemaWithOp("wait_for_text", ensureDescriptor(metaDescriptorIndex, "wait_for_screen_text").inputSchema, { description: "Poll the screen until a substring or regex appears, or timeout elapses." }),
|
|
26
26
|
handler: async (rawArgs, ctx) => invokeModuleTool(metaModule, "wait_for_screen_text", rawArgs, ctx),
|
|
27
27
|
},
|
|
28
|
+
{
|
|
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 both C64U and VICE." }),
|
|
31
|
+
handler: groupedMemoryHandlers.disassemble,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
op: "copy_memory",
|
|
35
|
+
schema: extendSchemaWithOp("copy_memory", copyMemoryArgsSchema.jsonSchema, { description: "Copy a RAM region to another address." }),
|
|
36
|
+
handler: groupedMemoryHandlers.copy_memory,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
op: "fill_memory",
|
|
40
|
+
schema: extendSchemaWithOp("fill_memory", fillMemoryArgsSchema.jsonSchema, { description: "Fill a memory range with a repeating byte pattern." }),
|
|
41
|
+
handler: groupedMemoryHandlers.fill_memory,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
op: "search_memory",
|
|
45
|
+
schema: extendSchemaWithOp("search_memory", searchMemoryArgsSchema.jsonSchema, { description: "Search for a byte pattern within a memory range and return matching addresses." }),
|
|
46
|
+
handler: groupedMemoryHandlers.search_memory,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
op: "compare_memory",
|
|
50
|
+
schema: extendSchemaWithOp("compare_memory", compareMemoryArgsSchema.jsonSchema, { description: "Compare two memory regions byte-by-byte and report differences." }),
|
|
51
|
+
handler: groupedMemoryHandlers.compare_memory,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
op: "save_memory",
|
|
55
|
+
schema: extendSchemaWithOp("save_memory", saveMemoryArgsSchema.jsonSchema, { description: "Dump a memory range to a local file, with an optional PRG load-address header." }),
|
|
56
|
+
handler: groupedMemoryHandlers.save_memory,
|
|
57
|
+
},
|
|
28
58
|
];
|
|
29
59
|
const memoryOperationHandlers = createOperationHandlers(memoryOperations);
|
|
30
60
|
export const memoryModuleGroup = defineToolModule({
|
|
@@ -59,6 +89,36 @@ export const memoryModuleGroup = defineToolModule({
|
|
|
59
89
|
description: "Poll until the READY. prompt appears",
|
|
60
90
|
arguments: { op: "wait_for_text", pattern: "READY." },
|
|
61
91
|
},
|
|
92
|
+
{
|
|
93
|
+
name: "Disassemble entry point",
|
|
94
|
+
description: "Disassemble 20 instructions starting at $0810",
|
|
95
|
+
arguments: { op: "disassemble", address: "$0810", instructionCount: 20 },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "Copy screen RAM",
|
|
99
|
+
description: "Copy 1000 bytes of screen RAM to a scratch area",
|
|
100
|
+
arguments: { op: "copy_memory", source: "$0400", dest: "$C000", length: 1000 },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "Clear screen RAM",
|
|
104
|
+
description: "Fill screen RAM with space character ($20)",
|
|
105
|
+
arguments: { op: "fill_memory", address: "$0400", length: 1000, pattern: "20" },
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "Search for RTS",
|
|
109
|
+
description: "Find RTS ($60) instructions in BASIC ROM",
|
|
110
|
+
arguments: { op: "search_memory", startAddress: "$A000", endAddress: "$BFFF", pattern: "60" },
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "Compare two buffers",
|
|
114
|
+
description: "Check whether two 256-byte regions differ",
|
|
115
|
+
arguments: { op: "compare_memory", address1: "$C000", address2: "$C100", length: 256 },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "Save PRG dump",
|
|
119
|
+
description: "Save $0801-$CFFF as a PRG file",
|
|
120
|
+
arguments: { op: "save_memory", startAddress: "$0801", endAddress: "$CFFF", filePath: "/tmp/dump.prg" },
|
|
121
|
+
},
|
|
62
122
|
],
|
|
63
123
|
execute: createOperationDispatcher("c64_memory", memoryOperationHandlers),
|
|
64
124
|
},
|
package/dist/tools/schema.js
CHANGED
|
@@ -195,15 +195,14 @@ function isPlainObject(value) {
|
|
|
195
195
|
}
|
|
196
196
|
export function objectSchema(options) {
|
|
197
197
|
const required = options.required ?? [];
|
|
198
|
+
const additionalProperties = options.additionalProperties ?? false;
|
|
198
199
|
const propertyEntries = Object.entries(options.properties).map(([key, schema]) => [key, schema.jsonSchema]);
|
|
199
200
|
const jsonSchema = {
|
|
200
201
|
type: "object",
|
|
201
202
|
...(options.description ? { description: options.description } : {}),
|
|
202
203
|
properties: Object.fromEntries(propertyEntries),
|
|
203
204
|
...(required.length > 0 ? { required: required.map((key) => key) } : {}),
|
|
204
|
-
|
|
205
|
-
? { additionalProperties: options.additionalProperties }
|
|
206
|
-
: { additionalProperties: false }),
|
|
205
|
+
additionalProperties,
|
|
207
206
|
};
|
|
208
207
|
return createSchema(jsonSchema, (value, path) => {
|
|
209
208
|
ensureType(isPlainObject(value), "Expected an object", path, {
|
|
@@ -234,7 +233,7 @@ export function objectSchema(options) {
|
|
|
234
233
|
}
|
|
235
234
|
}
|
|
236
235
|
}
|
|
237
|
-
if (
|
|
236
|
+
if (!additionalProperties) {
|
|
238
237
|
for (const key of Object.keys(input)) {
|
|
239
238
|
if (!hasOwn.call(options.properties, key)) {
|
|
240
239
|
throw new ToolValidationError("Unexpected property", {
|
|
@@ -243,7 +242,7 @@ export function objectSchema(options) {
|
|
|
243
242
|
}
|
|
244
243
|
}
|
|
245
244
|
}
|
|
246
|
-
else
|
|
245
|
+
else {
|
|
247
246
|
for (const key of Object.keys(input)) {
|
|
248
247
|
if (!hasOwn.call(options.properties, key)) {
|
|
249
248
|
result[key] = input[key];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process symbol registry for VICE debug sessions.
|
|
3
|
+
* Maps addresses to label names so the disassembler can annotate output.
|
|
4
|
+
* Cleared and repopulated each time a new program is loaded.
|
|
5
|
+
*/
|
|
6
|
+
const viceAddressToLabel = new Map();
|
|
7
|
+
export function setViceSymbols(nameToAddress) {
|
|
8
|
+
viceAddressToLabel.clear();
|
|
9
|
+
for (const [name, address] of nameToAddress) {
|
|
10
|
+
if (address >= 0 && address <= 0xffff) {
|
|
11
|
+
viceAddressToLabel.set(address, name);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function getViceSymbol(address) {
|
|
16
|
+
return viceAddressToLabel.get(address);
|
|
17
|
+
}
|
|
18
|
+
export function clearViceSymbols() {
|
|
19
|
+
viceAddressToLabel.clear();
|
|
20
|
+
}
|
|
21
|
+
export function getViceSymbols() {
|
|
22
|
+
return viceAddressToLabel;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse a VICE symbol file (.vs) into a name->address map.
|
|
26
|
+
*
|
|
27
|
+
* Supported line formats:
|
|
28
|
+
* add_label 0x0810 .main
|
|
29
|
+
* add_label C:0810 .main
|
|
30
|
+
* al C:0810 .main
|
|
31
|
+
* al 0810 .main
|
|
32
|
+
*/
|
|
33
|
+
export function parseViceSymbolFile(content) {
|
|
34
|
+
const symbols = new Map();
|
|
35
|
+
for (const rawLine of content.split("\n")) {
|
|
36
|
+
const line = rawLine.trim();
|
|
37
|
+
if (!line || line.startsWith(";") || line.startsWith("#"))
|
|
38
|
+
continue;
|
|
39
|
+
const match = /^(?:add_label|al)\s+(?:[A-Za-z]+:)?(?:0x|\$)?([0-9A-Fa-f]+)\s+\.?(\S+)/i.exec(line);
|
|
40
|
+
if (match) {
|
|
41
|
+
const addr = parseInt(match[1], 16);
|
|
42
|
+
const name = match[2];
|
|
43
|
+
if (!isNaN(addr) && addr >= 0 && addr <= 0xffff && name) {
|
|
44
|
+
symbols.set(name, addr);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return symbols;
|
|
49
|
+
}
|
package/dist/tools/types.js
CHANGED
|
@@ -36,16 +36,30 @@ export function discriminatedUnionSchema(options) {
|
|
|
36
36
|
const opValues = [];
|
|
37
37
|
const mergedProperties = {};
|
|
38
38
|
const opDescriptions = [];
|
|
39
|
+
const operationMetadata = [];
|
|
39
40
|
for (const variant of variants) {
|
|
40
41
|
const variantProps = variant.properties ?? {};
|
|
41
42
|
const discProp = variantProps[discriminator];
|
|
42
43
|
const opValue = discProp?.const;
|
|
44
|
+
const variantDesc = variant.description ?? discProp?.description ?? "";
|
|
45
|
+
const requiredInputs = (variant.required ?? []).filter((name) => name !== discriminator);
|
|
46
|
+
const optionalInputs = Object.keys(variantProps).filter((name) => name !== discriminator && !requiredInputs.includes(name));
|
|
43
47
|
if (opValue) {
|
|
44
48
|
opValues.push(opValue);
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
const requiredSummary = requiredInputs.length > 0
|
|
50
|
+
? ` Required inputs: ${requiredInputs.join(", ")}.`
|
|
51
|
+
: " Required inputs: none.";
|
|
52
|
+
const optionalSummary = optionalInputs.length > 0
|
|
53
|
+
? ` Optional inputs: ${optionalInputs.join(", ")}.`
|
|
54
|
+
: "";
|
|
55
|
+
opDescriptions.push(`${opValue}: ${variantDesc || `Operation ${opValue}`}${requiredSummary}${optionalSummary}`);
|
|
56
|
+
operationMetadata.push({
|
|
57
|
+
op: opValue,
|
|
58
|
+
...(variantDesc ? { description: variantDesc } : {}),
|
|
59
|
+
required: requiredInputs,
|
|
60
|
+
optional: optionalInputs,
|
|
61
|
+
inputSchema: variant,
|
|
62
|
+
});
|
|
49
63
|
}
|
|
50
64
|
for (const [key, propSchema] of Object.entries(variantProps)) {
|
|
51
65
|
if (key === discriminator)
|
|
@@ -74,6 +88,7 @@ export function discriminatedUnionSchema(options) {
|
|
|
74
88
|
},
|
|
75
89
|
required: [discriminator],
|
|
76
90
|
additionalProperties: false,
|
|
91
|
+
...(operationMetadata.length > 0 ? { "x-c64bridge-operations": operationMetadata } : {}),
|
|
77
92
|
};
|
|
78
93
|
return schema;
|
|
79
94
|
}
|