c64bridge 1.0.7 → 1.1.0
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/skills/assembly-program/SKILL.md +37 -0
- package/.github/skills/basic-program/SKILL.md +40 -0
- package/.github/skills/cross-platform-demo/SKILL.md +32 -0
- package/.github/skills/drive-manager/SKILL.md +33 -0
- package/.github/skills/graphics-demo/SKILL.md +30 -0
- package/.github/skills/hello-world/SKILL.md +36 -0
- package/.github/skills/memory-debug/SKILL.md +32 -0
- package/.github/skills/pr-converge/SKILL.md +33 -0
- package/.github/skills/printer-job/SKILL.md +30 -0
- package/.github/skills/sid-music/SKILL.md +35 -0
- package/.github/skills/software-launch/SKILL.md +31 -0
- package/.github/skills/stream-control/SKILL.md +29 -0
- package/.github/skills/system-control/SKILL.md +32 -0
- package/AGENTS.md +28 -7
- package/README.md +69 -14
- package/data/assembly/assembly-spec.md +2 -0
- package/dist/c64Client.js +238 -60
- package/dist/config.js +48 -26
- package/dist/device.js +92 -37
- package/dist/diagnostics.js +62 -2
- package/dist/hostPaths.js +128 -0
- package/dist/loggingHttpClient.js +59 -1
- package/dist/mcp-server.js +409 -286
- package/dist/numericArg.js +55 -0
- package/dist/petscii.js +13 -1
- package/dist/platform.js +12 -3
- package/dist/prompts/registry.js +6 -1
- package/dist/tools/access.js +66 -0
- package/dist/tools/debug.js +6 -0
- package/dist/tools/developer.js +67 -34
- package/dist/tools/errors.js +54 -3
- package/dist/tools/input.js +23 -6
- package/dist/tools/machineControl.js +112 -20
- package/dist/tools/memory.js +135 -30
- package/dist/tools/meta/artifacts.js +5 -5
- package/dist/tools/meta/background.js +216 -54
- package/dist/tools/meta/compilation.js +6 -5
- package/dist/tools/meta/config.js +15 -6
- package/dist/tools/meta/filesystem.js +16 -14
- package/dist/tools/meta/graphics.js +48 -20
- package/dist/tools/meta/memory.js +45 -19
- package/dist/tools/meta/program.js +10 -7
- package/dist/tools/meta/screen.js +116 -25
- package/dist/tools/pauseGuard.js +23 -0
- package/dist/tools/pollValidator.js +53 -8
- package/dist/tools/programRunners.js +62 -18
- package/dist/tools/registry/batch.js +16 -4
- package/dist/tools/registry/config.js +9 -2
- package/dist/tools/registry/disk.js +12 -2
- package/dist/tools/registry/drive.js +3 -0
- package/dist/tools/registry/extract.js +3 -0
- package/dist/tools/registry/graphics.js +59 -6
- package/dist/tools/registry/index.js +17 -1
- package/dist/tools/registry/memory.js +11 -1
- package/dist/tools/registry/platform.js +4 -1
- package/dist/tools/registry/printer.js +2 -1
- package/dist/tools/registry/program.js +5 -1
- package/dist/tools/registry/rag.js +1 -0
- package/dist/tools/registry/sound.js +12 -1
- package/dist/tools/registry/stream.js +2 -1
- package/dist/tools/registry/system.js +8 -1
- package/dist/tools/schema.js +7 -7
- package/dist/tools/streaming.js +34 -4
- package/dist/tools/translation/assembler.js +247 -24
- package/dist/tools/translation/basicDetokenizer.js +80 -0
- package/dist/tools/translation/basicTokenizer.js +8 -3
- package/dist/tools/translation/disassembler.js +14 -0
- package/dist/tools/types.js +258 -7
- package/dist/tools/vice.js +5 -16
- package/dist/vicBitmap.js +1 -1
- package/dist/vice/mockServer.js +3 -0
- package/dist/vice/process.js +19 -0
- package/dist/vice/resourcePolicy.js +36 -0
- package/dist/vice/viceClient.js +22 -5
- package/doc/vice/support-matrix.md +17 -11
- package/mcp/schemas/c64_batch.schema.json +2 -1
- package/mcp/schemas/c64_config.schema.json +25 -475
- package/mcp/schemas/c64_debug.schema.json +3 -602
- package/mcp/schemas/c64_disk.schema.json +17 -344
- package/mcp/schemas/c64_drive.schema.json +2 -157
- package/mcp/schemas/c64_extract.schema.json +10 -364
- package/mcp/schemas/c64_graphics.schema.json +19 -426
- package/mcp/schemas/c64_input.schema.json +5 -318
- package/mcp/schemas/c64_memory.schema.json +23 -491
- package/mcp/schemas/c64_printer.schema.json +3 -258
- package/mcp/schemas/c64_program.schema.json +16 -491
- package/mcp/schemas/c64_rag.schema.json +1 -81
- package/mcp/schemas/c64_select_backend.schema.json +2 -36
- package/mcp/schemas/c64_sound.schema.json +20 -822
- package/mcp/schemas/c64_stream.schema.json +4 -75
- package/mcp/schemas/c64_system.schema.json +28 -348
- package/mcp/schemas/c64_vice.schema.json +2 -71
- package/mcp/server.json +1 -1
- package/mcp/tools.json +1983 -6910
- package/mcp.json +21 -1
- package/package.json +16 -8
- package/scripts/mockC64Server.mjs +44 -0
- package/scripts/run-coverage.mjs +27 -1
- package/scripts/smoke-run.sh +3 -2
- package/scripts/start.mjs +23 -1
- package/server.json +35 -2
- package/dist/.tsbuildinfo +0 -1
- package/doc/agent-state-spec.md +0 -243
- package/doc/audit/secret-handling.md +0 -61
- package/doc/plans/bun/baseline-metrics.md +0 -16
- package/doc/plans/bun/migration-checklist.md +0 -55
- package/doc/plans/bun/migration.md +0 -83
- package/doc/plans/improve-knowledge/notes.md +0 -43
- package/doc/plans/improve-knowledge/plan.md +0 -121
- package/doc/plans/improve-knowledge/rollout.md +0 -110
- package/doc/plans/meta-tools/README.md +0 -110
- package/doc/plans/meta-tools/analysis.md +0 -272
- package/doc/plans/meta-tools/catalog.md +0 -435
- package/doc/plans/meta-tools/notes.md +0 -192
- package/doc/plans/meta-tools/rollout.md +0 -368
- package/doc/plans/prompts/README.md +0 -96
- package/doc/plans/prompts/user-prompt-ideas.md +0 -147
- package/doc/plans/tool-consolidation/notes.md +0 -28
- package/doc/plans/tool-consolidation/plan.md +0 -169
- package/doc/plans/tool-consolidation/rollout.md +0 -104
- package/doc/plans/vice-backend-switch/plan.md +0 -486
- package/doc/plans/vice-backend-switch-2/plan.md +0 -48
- package/doc/plans/vice-support/notes.md +0 -63
- package/doc/plans/vice-support/plan.md +0 -88
- package/doc/plans/vice-support/rollout.md +0 -99
- package/doc/reviews/audit.md +0 -143
- package/doc/reviews/hardening/01-fable/prompt-handover.md +0 -125
- package/doc/reviews/hardening/01-fable/prompt.md +0 -98
- package/doc/reviews/hardening/01-fable/review-prompt.md +0 -468
- package/doc/reviews/hardening/01-fable/review.md +0 -1234
- package/doc/reviews/vice-support-review.md +0 -185
- package/scripts/compare-rest/rest-compare-2025-11-01T00-25-21-830Z.json +0 -2513
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineToolModule } from "./types.js";
|
|
2
|
-
import { objectSchema } from "./schema.js";
|
|
2
|
+
import { booleanSchema, numberSchema, objectSchema, optionalSchema } from "./schema.js";
|
|
3
|
+
import { waitForScreenText, MAX_WAIT_TIMEOUT_MS } from "./meta/screen.js";
|
|
3
4
|
import { jsonResult, textResult } from "./responses.js";
|
|
4
5
|
import { Buffer } from "node:buffer";
|
|
5
6
|
import { ToolError, ToolExecutionError, toolErrorResult, unknownErrorResult, } from "./errors.js";
|
|
@@ -31,14 +32,104 @@ function isMissingMenuScreenEndpoint(error) {
|
|
|
31
32
|
&& "response" in error
|
|
32
33
|
&& error.response?.status === 404;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
function createWaitSchema(description) {
|
|
36
|
+
return objectSchema({
|
|
37
|
+
description,
|
|
38
|
+
properties: {
|
|
39
|
+
waitForReady: optionalSchema(booleanSchema({
|
|
40
|
+
description: "Wait until the BASIC READY. prompt is visible before returning. A timeout is reported as ready=false, not as an error.",
|
|
41
|
+
default: false,
|
|
42
|
+
}), false),
|
|
43
|
+
readyTimeoutMs: optionalSchema(numberSchema({
|
|
44
|
+
description: "Maximum time to wait for READY. when waitForReady is true (1-120000 ms).",
|
|
45
|
+
integer: true,
|
|
46
|
+
minimum: 1,
|
|
47
|
+
maximum: MAX_WAIT_TIMEOUT_MS,
|
|
48
|
+
default: 15000,
|
|
49
|
+
}), 15000),
|
|
50
|
+
},
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const READY_SETTLE_MS = 300;
|
|
55
|
+
/** After this long, a screen identical to the one before the reset counts as the new boot screen. */
|
|
56
|
+
const READY_UNCHANGED_GRACE_MS = 3000;
|
|
57
|
+
const SNAPSHOT_TIMEOUT_MS = 2000;
|
|
58
|
+
/** Resolves to undefined when the operation does not settle within `ms`; the late result is ignored. */
|
|
59
|
+
async function withinMs(operation, ms) {
|
|
60
|
+
let timer;
|
|
61
|
+
const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve(undefined), Math.max(0, ms)); });
|
|
62
|
+
try {
|
|
63
|
+
return await Promise.race([operation, timeout]);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
if (timer)
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Screen text before the reset, used to tell a stale READY. from the new one. Best effort: undefined when unreadable. */
|
|
71
|
+
async function screenBeforeReset(ctx, parsed) {
|
|
72
|
+
if (parsed.waitForReady !== true)
|
|
73
|
+
return undefined;
|
|
74
|
+
try {
|
|
75
|
+
return await withinMs(ctx.client.readScreen(), SNAPSHOT_TIMEOUT_MS);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Polls the screen until READY. appears or the timeout elapses. Never throws.
|
|
83
|
+
* A screen identical to the one read before the reset is ignored for the first
|
|
84
|
+
* few seconds, so the READY. still visible from before the reset is not taken
|
|
85
|
+
* for the new one. Each read is bounded by the remaining time.
|
|
86
|
+
*/
|
|
87
|
+
async function waitForReadyPrompt(ctx, timeoutMs, before) {
|
|
88
|
+
const start = Date.now();
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(READY_SETTLE_MS, timeoutMs)));
|
|
90
|
+
let lastError;
|
|
91
|
+
const tolerantClient = {
|
|
92
|
+
async readScreen() {
|
|
93
|
+
const remaining = timeoutMs - (Date.now() - start);
|
|
94
|
+
try {
|
|
95
|
+
const screen = await withinMs(ctx.client.readScreen(), remaining);
|
|
96
|
+
if (screen === undefined)
|
|
97
|
+
return "";
|
|
98
|
+
lastError = undefined;
|
|
99
|
+
if (before !== undefined && screen === before && Date.now() - start < READY_UNCHANGED_GRACE_MS)
|
|
100
|
+
return "";
|
|
101
|
+
return screen;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
105
|
+
return ""; // device not reachable yet
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const remaining = Math.max(1, timeoutMs - (Date.now() - start));
|
|
110
|
+
const outcome = await waitForScreenText(tolerantClient, { pattern: "READY.", timeoutMs: remaining, intervalMs: 250 });
|
|
111
|
+
return { ready: outcome.matched, readyElapsedMs: Date.now() - start, ...(outcome.matched || !lastError ? {} : { lastError }) };
|
|
112
|
+
}
|
|
113
|
+
async function withReadyResult(ctx, parsed, before, baseMessage, details) {
|
|
114
|
+
if (parsed.waitForReady !== true) {
|
|
115
|
+
return textResult(baseMessage, { success: true, details });
|
|
116
|
+
}
|
|
117
|
+
const { ready, readyElapsedMs, lastError } = await waitForReadyPrompt(ctx, parsed.readyTimeoutMs ?? 15000, before);
|
|
118
|
+
const message = ready
|
|
119
|
+
? `${baseMessage} READY. prompt reached after ${readyElapsedMs} ms.`
|
|
120
|
+
: `${baseMessage} The READY. prompt was not seen within ${readyElapsedMs} ms; the machine may still be starting.`
|
|
121
|
+
+ (lastError ? ` The last screen read failed: ${lastError}` : "");
|
|
122
|
+
return textResult(message, { success: true, details, ready, readyElapsedMs, ...(lastError ? { readyError: lastError } : {}) });
|
|
123
|
+
}
|
|
124
|
+
const resetArgsSchema = createWaitSchema("Reset the C64. Optionally wait for the READY. prompt.");
|
|
125
|
+
const rebootArgsSchema = createWaitSchema("Reboot the C64 firmware. Optionally wait for the READY. prompt.");
|
|
36
126
|
const pauseArgsSchema = createNoArgsSchema("No arguments required to pause the machine.");
|
|
37
127
|
const resumeArgsSchema = createNoArgsSchema("No arguments required to resume the machine after a pause.");
|
|
38
128
|
const poweroffArgsSchema = createNoArgsSchema("No arguments required to power off the machine.");
|
|
39
|
-
const powerCycleArgsSchema =
|
|
129
|
+
const powerCycleArgsSchema = createWaitSchema("Power cycle the active system. Optionally wait for the READY. prompt.");
|
|
40
130
|
const menuButtonArgsSchema = createNoArgsSchema("No arguments required to toggle the Ultimate menu button.");
|
|
41
131
|
const readMenuScreenArgsSchema = createNoArgsSchema("No arguments required to read the active Ultimate menu screen matrix.");
|
|
132
|
+
const POWER_RESTORE_NOTE = "The machine cannot be restarted by software afterwards: power must be restored at the outlet, and the machine starts by itself only if automatic startup on power restoration is enabled.";
|
|
42
133
|
export const machineControlModule = defineToolModule({
|
|
43
134
|
domain: "machine",
|
|
44
135
|
summary: "Power, reset, pause/resume, and diagnostic controls for the C64 and Ultimate hardware.",
|
|
@@ -66,8 +157,9 @@ export const machineControlModule = defineToolModule({
|
|
|
66
157
|
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
67
158
|
async execute(args, ctx) {
|
|
68
159
|
try {
|
|
69
|
-
resetArgsSchema.parse(args ?? {});
|
|
160
|
+
const parsed = resetArgsSchema.parse(args ?? {});
|
|
70
161
|
ctx.logger.info("Resetting C64");
|
|
162
|
+
const before = await screenBeforeReset(ctx, parsed);
|
|
71
163
|
const result = await ctx.client.reset();
|
|
72
164
|
if (!result.success) {
|
|
73
165
|
throw new ToolExecutionError("C64 backend reported failure while resetting", {
|
|
@@ -75,10 +167,7 @@ export const machineControlModule = defineToolModule({
|
|
|
75
167
|
});
|
|
76
168
|
}
|
|
77
169
|
const details = toRecord(result.details) ?? {};
|
|
78
|
-
return
|
|
79
|
-
success: true,
|
|
80
|
-
details,
|
|
81
|
-
});
|
|
170
|
+
return await withReadyResult(ctx, parsed, before, "C64 reset command issued successfully.", details);
|
|
82
171
|
}
|
|
83
172
|
catch (error) {
|
|
84
173
|
if (error instanceof ToolError) {
|
|
@@ -104,8 +193,9 @@ export const machineControlModule = defineToolModule({
|
|
|
104
193
|
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
105
194
|
async execute(args, ctx) {
|
|
106
195
|
try {
|
|
107
|
-
rebootArgsSchema.parse(args ?? {});
|
|
196
|
+
const parsed = rebootArgsSchema.parse(args ?? {});
|
|
108
197
|
ctx.logger.info("Rebooting C64");
|
|
198
|
+
const before = await screenBeforeReset(ctx, parsed);
|
|
109
199
|
const result = await ctx.client.reboot();
|
|
110
200
|
if (!result.success) {
|
|
111
201
|
throw new ToolExecutionError("C64 backend reported failure while rebooting", {
|
|
@@ -113,10 +203,7 @@ export const machineControlModule = defineToolModule({
|
|
|
113
203
|
});
|
|
114
204
|
}
|
|
115
205
|
const details = toRecord(result.details) ?? {};
|
|
116
|
-
return
|
|
117
|
-
success: true,
|
|
118
|
-
details,
|
|
119
|
-
});
|
|
206
|
+
return await withReadyResult(ctx, parsed, before, "C64 reboot command issued successfully.", details);
|
|
120
207
|
}
|
|
121
208
|
catch (error) {
|
|
122
209
|
if (error instanceof ToolError) {
|
|
@@ -144,7 +231,8 @@ export const machineControlModule = defineToolModule({
|
|
|
144
231
|
try {
|
|
145
232
|
pauseArgsSchema.parse(args ?? {});
|
|
146
233
|
ctx.logger.info("Pausing C64 execution");
|
|
147
|
-
|
|
234
|
+
// An explicit pause stays in effect until an explicit resume, even if tool calls pause and resume meanwhile.
|
|
235
|
+
const result = ctx.client.explicitPause ? await ctx.client.explicitPause(await ctx.client.pinFacade?.()) : await ctx.client.pause();
|
|
148
236
|
if (!result.success) {
|
|
149
237
|
throw new ToolExecutionError("C64 backend reported failure while pausing", {
|
|
150
238
|
details: normaliseFailure(result.details),
|
|
@@ -182,7 +270,7 @@ export const machineControlModule = defineToolModule({
|
|
|
182
270
|
try {
|
|
183
271
|
resumeArgsSchema.parse(args ?? {});
|
|
184
272
|
ctx.logger.info("Resuming C64 execution");
|
|
185
|
-
const result = await ctx.client.resume();
|
|
273
|
+
const result = ctx.client.explicitResume ? await ctx.client.explicitResume(await ctx.client.pinFacade?.()) : await ctx.client.resume();
|
|
186
274
|
if (!result.success) {
|
|
187
275
|
throw new ToolExecutionError("C64 backend reported failure while resuming", {
|
|
188
276
|
details: normaliseFailure(result.details),
|
|
@@ -204,7 +292,7 @@ export const machineControlModule = defineToolModule({
|
|
|
204
292
|
},
|
|
205
293
|
{
|
|
206
294
|
name: "poweroff",
|
|
207
|
-
description:
|
|
295
|
+
description: `Power off the machine via Ultimate firmware. See safety notes in c64://guide/bootstrap. ${POWER_RESTORE_NOTE}`,
|
|
208
296
|
summary: "Attempts a controlled shutdown through the Ultimate control interface.",
|
|
209
297
|
inputSchema: poweroffArgsSchema.jsonSchema,
|
|
210
298
|
tags: ["power"],
|
|
@@ -227,7 +315,10 @@ export const machineControlModule = defineToolModule({
|
|
|
227
315
|
});
|
|
228
316
|
}
|
|
229
317
|
const details = toRecord(result.details) ?? {};
|
|
230
|
-
|
|
318
|
+
const message = ctx.platform?.id === "vice"
|
|
319
|
+
? "Power off command acknowledged."
|
|
320
|
+
: `Power off command acknowledged. ${POWER_RESTORE_NOTE}`;
|
|
321
|
+
return textResult(message, {
|
|
231
322
|
success: true,
|
|
232
323
|
details,
|
|
233
324
|
});
|
|
@@ -257,8 +348,9 @@ export const machineControlModule = defineToolModule({
|
|
|
257
348
|
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
258
349
|
async execute(args, ctx) {
|
|
259
350
|
try {
|
|
260
|
-
powerCycleArgsSchema.parse(args ?? {});
|
|
351
|
+
const parsed = powerCycleArgsSchema.parse(args ?? {});
|
|
261
352
|
ctx.logger.info("Power cycling active backend");
|
|
353
|
+
const before = await screenBeforeReset(ctx, parsed);
|
|
262
354
|
const result = await ctx.client.powerCycle();
|
|
263
355
|
if (!result.success) {
|
|
264
356
|
throw new ToolExecutionError("Power cycle did not complete", {
|
|
@@ -266,7 +358,7 @@ export const machineControlModule = defineToolModule({
|
|
|
266
358
|
});
|
|
267
359
|
}
|
|
268
360
|
const details = toRecord(result.details) ?? {};
|
|
269
|
-
return
|
|
361
|
+
return await withReadyResult(ctx, parsed, before, "Power-cycle request completed.", details);
|
|
270
362
|
}
|
|
271
363
|
catch (error) {
|
|
272
364
|
if (error instanceof ToolError)
|
package/dist/tools/memory.js
CHANGED
|
@@ -9,6 +9,10 @@ import { disassemble, formatDisassembly } from "./translation/disassembler.js";
|
|
|
9
9
|
import { getViceSymbols } from "./translation/symbolRegistry.js";
|
|
10
10
|
import { resolveAddressSymbol } from "../knowledge.js";
|
|
11
11
|
import { stripHexByteSeparators } from "./hex.js";
|
|
12
|
+
import { resolveHostPath } from "../hostPaths.js";
|
|
13
|
+
import { petsciiToAscii } from "../petscii.js";
|
|
14
|
+
import { listBasicProgram, MAX_BASIC_IMAGE_BYTES } from "./translation/basicDetokenizer.js";
|
|
15
|
+
import { assertAddressRange, assertNotBareHex, parseNumericLiteral } from "../numericArg.js";
|
|
12
16
|
function toRecord(details) {
|
|
13
17
|
if (details && typeof details === "object") {
|
|
14
18
|
return details;
|
|
@@ -120,6 +124,11 @@ const readMemoryArgsSchema = objectSchema({
|
|
|
120
124
|
maximum: 4096,
|
|
121
125
|
default: 256,
|
|
122
126
|
}),
|
|
127
|
+
format: optionalSchema(stringSchema({
|
|
128
|
+
description: "Output layout. hex (default) returns one hex string; dump adds a 16-bytes-per-line listing with addresses and a printable PETSCII column.",
|
|
129
|
+
enum: ["hex", "dump"],
|
|
130
|
+
default: "hex",
|
|
131
|
+
}), "hex"),
|
|
123
132
|
},
|
|
124
133
|
required: ["address"],
|
|
125
134
|
additionalProperties: false,
|
|
@@ -166,11 +175,19 @@ async function executeReadScreen(rawArgs, ctx) {
|
|
|
166
175
|
try {
|
|
167
176
|
readScreenArgsSchema.parse(rawArgs ?? {});
|
|
168
177
|
ctx.logger.info("Reading C64 screen contents");
|
|
169
|
-
const
|
|
178
|
+
const client = ctx.client;
|
|
179
|
+
const detailed = typeof client.readScreenDetailed === "function" ? await client.readScreenDetailed() : undefined;
|
|
180
|
+
const screen = detailed ? detailed.text : await ctx.client.readScreen();
|
|
170
181
|
return textResult(`Current screen contents:\n${screen}`, {
|
|
171
182
|
success: true,
|
|
172
183
|
screen,
|
|
173
184
|
length: screen.length,
|
|
185
|
+
...(detailed
|
|
186
|
+
? {
|
|
187
|
+
screenBase: `$${detailed.baseAddress.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
188
|
+
charset: detailed.charset,
|
|
189
|
+
}
|
|
190
|
+
: {}),
|
|
174
191
|
});
|
|
175
192
|
}
|
|
176
193
|
catch (error) {
|
|
@@ -180,10 +197,77 @@ async function executeReadScreen(rawArgs, ctx) {
|
|
|
180
197
|
return unknownErrorResult(error);
|
|
181
198
|
}
|
|
182
199
|
}
|
|
200
|
+
const listBasicArgsSchema = objectSchema({
|
|
201
|
+
description: "List the BASIC program currently in memory. Reads the start ($2B/$2C) and end ($2D/$2E) pointers and detokenises the program text.",
|
|
202
|
+
properties: {},
|
|
203
|
+
additionalProperties: false,
|
|
204
|
+
});
|
|
205
|
+
async function readRange(ctx, start, length) {
|
|
206
|
+
const chunks = [];
|
|
207
|
+
for (let offset = 0; offset < length; offset += 4096) {
|
|
208
|
+
const count = Math.min(4096, length - offset);
|
|
209
|
+
const address = `$${(start + offset).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
210
|
+
const result = await ctx.client.readMemory(address, String(count));
|
|
211
|
+
if (!result.success) {
|
|
212
|
+
throw new ToolExecutionError("C64 backend reported failure while reading memory", { details: normaliseFailure(result.details) });
|
|
213
|
+
}
|
|
214
|
+
const bytes = parseFirmwareHex(result.data, "BASIC program read").bytes;
|
|
215
|
+
if (bytes.length !== count) {
|
|
216
|
+
throw new ToolExecutionError("Memory read returned an unexpected number of bytes", { details: { address, expected: count, received: bytes.length } });
|
|
217
|
+
}
|
|
218
|
+
chunks.push(bytes);
|
|
219
|
+
}
|
|
220
|
+
return Buffer.concat(chunks);
|
|
221
|
+
}
|
|
222
|
+
async function executeListBasic(rawArgs, ctx) {
|
|
223
|
+
try {
|
|
224
|
+
listBasicArgsSchema.parse(rawArgs ?? {});
|
|
225
|
+
const pointers = await readRange(ctx, 0x002b, 4);
|
|
226
|
+
const start = pointers[0] | (pointers[1] << 8);
|
|
227
|
+
const end = pointers[2] | (pointers[3] << 8);
|
|
228
|
+
const fmt = (value) => `$${value.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
229
|
+
if (end <= start) {
|
|
230
|
+
return jsonResult({ start: fmt(start), end: fmt(end), lines: [], listing: "" }, { success: true, lineCount: 0 });
|
|
231
|
+
}
|
|
232
|
+
const length = end - start;
|
|
233
|
+
if (length > MAX_BASIC_IMAGE_BYTES) {
|
|
234
|
+
throw new ToolExecutionError("BASIC end pointer is implausible", { details: { start: fmt(start), end: fmt(end) } });
|
|
235
|
+
}
|
|
236
|
+
const image = await readRange(ctx, start, length);
|
|
237
|
+
const lines = listBasicProgram(image, start);
|
|
238
|
+
const listing = lines.map((line) => `${line.number} ${line.text}`).join("\n");
|
|
239
|
+
return jsonResult({ start: fmt(start), end: fmt(end), lines, listing }, { success: true, lineCount: lines.length });
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (error instanceof ToolError)
|
|
243
|
+
return toolErrorResult(error);
|
|
244
|
+
return unknownErrorResult(error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** 16 bytes per line: `$ADDR: HH HH ... |text|`. Bytes with no printable PETSCII glyph show as `.`. */
|
|
248
|
+
export function formatHexDump(start, bytes) {
|
|
249
|
+
const lines = [];
|
|
250
|
+
for (let offset = 0; offset < bytes.length; offset += 16) {
|
|
251
|
+
const row = bytes.subarray(offset, offset + 16);
|
|
252
|
+
const hex = Array.from(row, (b) => b.toString(16).toUpperCase().padStart(2, "0")).join(" ").padEnd(16 * 3 - 1, " ");
|
|
253
|
+
const text = Array.from(row, (b) => {
|
|
254
|
+
if (b === 0x20 || b === 0xa0)
|
|
255
|
+
return " ";
|
|
256
|
+
const ch = petsciiToAscii(Uint8Array.of(b));
|
|
257
|
+
return ch.length === 1 && ch > " " && ch <= "~" ? ch : ".";
|
|
258
|
+
}).join("");
|
|
259
|
+
const address = ((start + offset) & 0xffff).toString(16).toUpperCase().padStart(4, "0");
|
|
260
|
+
lines.push(`$${address}: ${hex} |${text}|`);
|
|
261
|
+
}
|
|
262
|
+
return lines;
|
|
263
|
+
}
|
|
183
264
|
async function executeReadMemory(rawArgs, ctx) {
|
|
184
265
|
try {
|
|
185
266
|
const parsed = readMemoryArgsSchema.parse(rawArgs ?? {});
|
|
186
267
|
ctx.logger.info("Reading C64 memory", { address: parsed.address, length: parsed.length });
|
|
268
|
+
// Validate before any device call so a bad address is a validation error.
|
|
269
|
+
const readStart = parseAddressArg(parsed.address, "$.address");
|
|
270
|
+
assertAddressRange(readStart, parsed.length ?? 256, "$.length");
|
|
187
271
|
const result = await ctx.client.readMemory(parsed.address, String(parsed.length));
|
|
188
272
|
if (!result.success) {
|
|
189
273
|
throw new ToolExecutionError("C64 backend reported failure while reading memory", {
|
|
@@ -193,15 +277,21 @@ async function executeReadMemory(rawArgs, ctx) {
|
|
|
193
277
|
const detailRecord = toRecord(result.details) ?? {};
|
|
194
278
|
const resolvedAddress = resolveAddressLabel(detailRecord, parsed.address);
|
|
195
279
|
const resolvedLength = resolveLength(detailRecord) ?? parsed.length;
|
|
280
|
+
const dumpLines = parsed.format === "dump" && typeof result.data === "string"
|
|
281
|
+
? formatHexDump(readStart, parseFirmwareHex(result.data, "memory read").bytes)
|
|
282
|
+
: undefined;
|
|
196
283
|
const data = {
|
|
197
284
|
success: true,
|
|
198
285
|
address: resolvedAddress,
|
|
199
286
|
length: resolvedLength,
|
|
200
287
|
hexData: result.data ?? null,
|
|
288
|
+
...(dumpLines ? { dump: dumpLines } : {}),
|
|
201
289
|
details: detailRecord,
|
|
202
290
|
};
|
|
203
291
|
// Many clients show only the text content, so it carries the bytes too.
|
|
204
|
-
const hexSummary =
|
|
292
|
+
const hexSummary = dumpLines
|
|
293
|
+
? `\n${dumpLines.join("\n")}`
|
|
294
|
+
: typeof result.data === "string" && result.data.length > 0 ? `\n${result.data}` : "";
|
|
205
295
|
const base = textResult(`Read ${resolvedLength} bytes starting at ${resolvedAddress}.${hexSummary}`, data);
|
|
206
296
|
return {
|
|
207
297
|
...base,
|
|
@@ -226,6 +316,7 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
226
316
|
const expectedInfo = parsed.expected ? parseUserHex(parsed.expected, "$.expected") : undefined;
|
|
227
317
|
const maskInfo = parsed.mask ? parseUserHex(parsed.mask, "$.mask") : undefined;
|
|
228
318
|
const shouldVerify = parsed.verify || Boolean(expectedInfo) || Boolean(maskInfo);
|
|
319
|
+
assertAddressRange(parseAddressArg(parsed.address, "$.address"), writeInfo.bytes.length, "$.bytes");
|
|
229
320
|
if (!shouldVerify) {
|
|
230
321
|
const result = await ctx.client.writeMemory(parsed.address, writeInfo.canonical);
|
|
231
322
|
if (!result.success) {
|
|
@@ -249,7 +340,7 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
249
340
|
let paused = false;
|
|
250
341
|
try {
|
|
251
342
|
if (canPause) {
|
|
252
|
-
const pauseResult = await ctx.client.pause(pinnedFacade);
|
|
343
|
+
const pauseResult = ctx.client.acquirePause ? await ctx.client.acquirePause(pinnedFacade) : await ctx.client.pause(pinnedFacade);
|
|
253
344
|
if (!pauseResult.success) {
|
|
254
345
|
throw new ToolExecutionError("C64 backend reported failure while pausing", {
|
|
255
346
|
details: normaliseFailure(pauseResult.details),
|
|
@@ -339,7 +430,8 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
339
430
|
verificationMetadata.preReadMismatches = preMismatches;
|
|
340
431
|
}
|
|
341
432
|
if (paused) { // A failed resume here leaves the machine paused: a recoverable operation failure, not a log.
|
|
342
|
-
const resumeResult = await ctx.client.resume(pinnedFacade);
|
|
433
|
+
const resumeResult = ctx.client.releasePause ? await ctx.client.releasePause(pinnedFacade) : await ctx.client.resume(pinnedFacade);
|
|
434
|
+
paused = false; // released once, whatever the outcome: the finally block must not release again
|
|
343
435
|
if (!resumeResult.success) {
|
|
344
436
|
throw new ToolExecutionError("Write completed but the machine could not be resumed", {
|
|
345
437
|
details: {
|
|
@@ -366,7 +458,7 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
366
458
|
finally {
|
|
367
459
|
if (paused) {
|
|
368
460
|
try {
|
|
369
|
-
const resumeResult = await ctx.client.resume(pinnedFacade);
|
|
461
|
+
const resumeResult = ctx.client.releasePause ? await ctx.client.releasePause(pinnedFacade) : await ctx.client.resume(pinnedFacade);
|
|
370
462
|
if (!resumeResult.success) {
|
|
371
463
|
ctx.logger.warn("C64 resume reported failure after write", {
|
|
372
464
|
details: normaliseFailure(resumeResult.details),
|
|
@@ -406,7 +498,7 @@ const disassembleArgsSchema = objectSchema({
|
|
|
406
498
|
default: 64,
|
|
407
499
|
}),
|
|
408
500
|
instructionCount: optionalSchema(numberSchema({
|
|
409
|
-
description: "Stop after this many instructions
|
|
501
|
+
description: "Stop after this many instructions. When given, it replaces length: 3 x instructionCount bytes are read (fewer near $FFFF).",
|
|
410
502
|
integer: true,
|
|
411
503
|
minimum: 1,
|
|
412
504
|
maximum: 512,
|
|
@@ -418,9 +510,13 @@ const disassembleArgsSchema = objectSchema({
|
|
|
418
510
|
async function executeDisassemble(rawArgs, ctx) {
|
|
419
511
|
try {
|
|
420
512
|
const parsed = disassembleArgsSchema.parse(rawArgs ?? {});
|
|
421
|
-
const length = parsed.length ?? 64;
|
|
422
|
-
ctx.logger.info("Disassembling VICE memory", { address: parsed.address, length });
|
|
423
513
|
const address = parseAddressArg(parsed.address, "$.address");
|
|
514
|
+
// An instruction is at most 3 bytes, so 3 x instructionCount bytes always
|
|
515
|
+
// cover the requested instructions; clamp at the end of the address space.
|
|
516
|
+
const length = parsed.instructionCount !== undefined
|
|
517
|
+
? Math.min(3 * parsed.instructionCount, 0x10000 - address)
|
|
518
|
+
: parsed.length ?? 64;
|
|
519
|
+
ctx.logger.info("Disassembling VICE memory", { address: parsed.address, length });
|
|
424
520
|
validateRangeWithinAddressSpace(address, length, "$.address");
|
|
425
521
|
const bytes = await ctx.client.readMemoryRaw(address, length);
|
|
426
522
|
const symbols = ctx.platform?.id === "vice" ? getViceSymbols() : undefined;
|
|
@@ -432,6 +528,14 @@ async function executeDisassemble(rawArgs, ctx) {
|
|
|
432
528
|
address: addrHex,
|
|
433
529
|
bytesRead: bytes.length,
|
|
434
530
|
instructionCount: lines.length,
|
|
531
|
+
lines: lines.map((line) => ({
|
|
532
|
+
address: `$${line.address.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
533
|
+
bytes: line.bytes.map((value) => value.toString(16).toUpperCase().padStart(2, "0")),
|
|
534
|
+
...(line.label ? { label: line.label } : {}),
|
|
535
|
+
mnemonic: line.mnemonic,
|
|
536
|
+
operand: line.operand,
|
|
537
|
+
undocumented: line.undocumented,
|
|
538
|
+
})),
|
|
435
539
|
});
|
|
436
540
|
}
|
|
437
541
|
catch (error) {
|
|
@@ -446,26 +550,16 @@ async function executeDisassemble(rawArgs, ctx) {
|
|
|
446
550
|
// ---------------------------------------------------------------------------
|
|
447
551
|
function parseAddressArg(raw, fieldPath) {
|
|
448
552
|
const trimmed = raw.trim();
|
|
449
|
-
let addr;
|
|
450
|
-
if (
|
|
451
|
-
addr = parseInt(trimmed.slice(1), 16);
|
|
452
|
-
}
|
|
453
|
-
else if (/^0[xX][0-9A-Fa-f]+$/.test(trimmed)) {
|
|
454
|
-
addr = parseInt(trimmed.slice(2), 16);
|
|
455
|
-
}
|
|
456
|
-
else if (/^[0-9]+$/.test(trimmed)) {
|
|
457
|
-
addr = parseInt(trimmed, 10);
|
|
458
|
-
}
|
|
459
|
-
else {
|
|
553
|
+
let addr = parseNumericLiteral(trimmed, fieldPath);
|
|
554
|
+
if (addr === undefined) {
|
|
460
555
|
const resolved = resolveAddressSymbol(trimmed);
|
|
461
556
|
if (resolved === undefined) {
|
|
557
|
+
assertNotBareHex(trimmed, fieldPath);
|
|
462
558
|
throw new ToolValidationError(`Unknown address or symbol: ${trimmed}`, { path: fieldPath });
|
|
463
559
|
}
|
|
464
560
|
addr = resolved;
|
|
465
561
|
}
|
|
466
|
-
|
|
467
|
-
throw new ToolValidationError("Address must be in range $0000-$FFFF", { path: fieldPath });
|
|
468
|
-
}
|
|
562
|
+
assertAddressRange(addr, undefined, fieldPath);
|
|
469
563
|
return addr;
|
|
470
564
|
}
|
|
471
565
|
function fmtAddr(addr) {
|
|
@@ -584,14 +678,19 @@ async function executeSearchMemory(rawArgs, ctx) {
|
|
|
584
678
|
ctx.logger.info("Searching memory", { start: fmtAddr(start), end: fmtAddr(end), pattern: parsed.pattern });
|
|
585
679
|
const haystack = await ctx.client.readMemoryRaw(start, len);
|
|
586
680
|
const matches = [];
|
|
587
|
-
|
|
681
|
+
// The whole range is already read, so counting past maxResults costs no device call.
|
|
682
|
+
let totalMatches = 0;
|
|
683
|
+
outer: for (let i = 0; i <= haystack.length - needle.length; i++) {
|
|
588
684
|
for (let j = 0; j < needle.length; j++) {
|
|
589
685
|
if (haystack[i + j] !== needle[j])
|
|
590
686
|
continue outer;
|
|
591
687
|
}
|
|
592
|
-
|
|
688
|
+
totalMatches += 1;
|
|
689
|
+
if (matches.length < maxResults)
|
|
690
|
+
matches.push(fmtAddr(start + i));
|
|
593
691
|
}
|
|
594
|
-
|
|
692
|
+
const truncated = totalMatches > matches.length;
|
|
693
|
+
return jsonResult({ found: matches.length, totalMatches, truncated, matches, pattern: parsed.pattern.trim(), range: { start: fmtAddr(start), end: fmtAddr(end) } }, { success: true, found: matches.length, totalMatches, truncated });
|
|
595
694
|
}
|
|
596
695
|
catch (error) {
|
|
597
696
|
if (error instanceof ToolError)
|
|
@@ -628,8 +727,12 @@ async function executeCompareMemory(rawArgs, ctx) {
|
|
|
628
727
|
ctx.client.readMemoryRaw(a2, len),
|
|
629
728
|
]);
|
|
630
729
|
const diffs = [];
|
|
631
|
-
|
|
730
|
+
let totalDiffs = 0;
|
|
731
|
+
for (let i = 0; i < len; i++) {
|
|
632
732
|
if (r1[i] !== r2[i]) {
|
|
733
|
+
totalDiffs += 1;
|
|
734
|
+
if (diffs.length >= maxDiffs)
|
|
735
|
+
continue;
|
|
633
736
|
diffs.push({
|
|
634
737
|
offset: i,
|
|
635
738
|
address1: fmtAddr(a1 + i),
|
|
@@ -639,8 +742,9 @@ async function executeCompareMemory(rawArgs, ctx) {
|
|
|
639
742
|
});
|
|
640
743
|
}
|
|
641
744
|
}
|
|
642
|
-
const identical =
|
|
643
|
-
|
|
745
|
+
const identical = totalDiffs === 0;
|
|
746
|
+
const truncated = totalDiffs > diffs.length;
|
|
747
|
+
return jsonResult({ identical, diffCount: totalDiffs, returnedDiffs: diffs.length, truncated, diffs, region1: fmtAddr(a1), region2: fmtAddr(a2), length: len }, { success: true, identical, diffCount: totalDiffs, truncated });
|
|
644
748
|
}
|
|
645
749
|
catch (error) {
|
|
646
750
|
if (error instanceof ToolError)
|
|
@@ -671,7 +775,7 @@ async function executeSaveMemory(rawArgs, ctx) {
|
|
|
671
775
|
throw new ToolValidationError("endAddress must be ≥ startAddress", { path: "$.endAddress" });
|
|
672
776
|
const asPrg = parsed.asPrg !== false;
|
|
673
777
|
const len = end - start + 1;
|
|
674
|
-
const resolvedPath =
|
|
778
|
+
const resolvedPath = resolveHostPath(parsed.filePath, { purpose: "write" });
|
|
675
779
|
ctx.logger.info("Saving memory to file", { start: fmtAddr(start), end: fmtAddr(end), filePath: resolvedPath, asPrg });
|
|
676
780
|
const bytes = await ctx.client.readMemoryRaw(start, len);
|
|
677
781
|
const header = asPrg ? Buffer.from([start & 0xff, (start >> 8) & 0xff]) : Buffer.alloc(0);
|
|
@@ -690,6 +794,7 @@ export const memoryOperationHandlers = {
|
|
|
690
794
|
read: async (args, ctx) => executeReadMemory(stripOperationDiscriminator(args), ctx),
|
|
691
795
|
write: async (args, ctx) => executeWriteMemory(stripOperationDiscriminator(args), ctx),
|
|
692
796
|
read_screen: async (args, ctx) => executeReadScreen(stripOperationDiscriminator(args), ctx),
|
|
797
|
+
list_basic: async (args, ctx) => executeListBasic(stripOperationDiscriminator(args), ctx),
|
|
693
798
|
disassemble: async (args, ctx) => executeDisassemble(stripOperationDiscriminator(args), ctx),
|
|
694
799
|
copy_memory: async (args, ctx) => executeCopyMemory(stripOperationDiscriminator(args), ctx),
|
|
695
800
|
fill_memory: async (args, ctx) => executeFillMemory(stripOperationDiscriminator(args), ctx),
|
|
@@ -697,7 +802,7 @@ export const memoryOperationHandlers = {
|
|
|
697
802
|
compare_memory: async (args, ctx) => executeCompareMemory(stripOperationDiscriminator(args), ctx),
|
|
698
803
|
save_memory: async (args, ctx) => executeSaveMemory(stripOperationDiscriminator(args), ctx),
|
|
699
804
|
};
|
|
700
|
-
export { disassembleArgsSchema, copyMemoryArgsSchema, fillMemoryArgsSchema, searchMemoryArgsSchema, compareMemoryArgsSchema, saveMemoryArgsSchema, };
|
|
805
|
+
export { listBasicArgsSchema, disassembleArgsSchema, copyMemoryArgsSchema, fillMemoryArgsSchema, searchMemoryArgsSchema, compareMemoryArgsSchema, saveMemoryArgsSchema, };
|
|
701
806
|
export const memoryModule = defineToolModule({
|
|
702
807
|
domain: "memory",
|
|
703
808
|
summary: "Screen, main memory, and low-level inspection utilities.",
|
|
@@ -3,7 +3,8 @@ import { jsonResult } from "../responses.js";
|
|
|
3
3
|
import { ToolError, ToolValidationError, ToolUnsupportedPlatformError, toolErrorResult, unknownErrorResult } from "../errors.js";
|
|
4
4
|
import { getPlatformStatus } from "../../platform.js";
|
|
5
5
|
import { promises as fs } from "node:fs";
|
|
6
|
-
import { resolve as resolvePath, join as joinPath,
|
|
6
|
+
import { resolve as resolvePath, join as joinPath, isAbsolute } from "node:path";
|
|
7
|
+
import { isWithin, resolveHostChildPath, resolveHostPath } from "../../hostPaths.js";
|
|
7
8
|
const bundleRunArtifactsArgsSchema = objectSchema({
|
|
8
9
|
description: "Gather screen capture, memory snapshot, and debugreg into a structured bundle.",
|
|
9
10
|
properties: {
|
|
@@ -45,7 +46,7 @@ export const tools = [
|
|
|
45
46
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(runId) || runId === "." || runId === "..") {
|
|
46
47
|
throw new ToolValidationError("runId must be a non-empty safe filename (letters, digits, dot, underscore, and dash only).", { path: "$.runId" });
|
|
47
48
|
}
|
|
48
|
-
const outputPath =
|
|
49
|
+
const outputPath = resolveHostPath(String(parsed.outputPath), { purpose: "write" });
|
|
49
50
|
const captureScreen = parsed.captureScreen !== false;
|
|
50
51
|
const platform = ctx.platform?.id ?? getPlatformStatus().id;
|
|
51
52
|
const captureDebugReg = parsed.captureDebugReg ?? (platform === "c64u");
|
|
@@ -105,8 +106,7 @@ function containedPath(base, name) {
|
|
|
105
106
|
if (!name || isAbsolute(name) || name.includes("/") || name.includes("\\"))
|
|
106
107
|
throw new ToolValidationError("Unsafe artifact filename.");
|
|
107
108
|
const candidate = resolvePath(joinPath(base, name));
|
|
108
|
-
|
|
109
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
109
|
+
if (candidate === base || !isWithin(base, candidate))
|
|
110
110
|
throw new ToolValidationError("Artifact path escapes its output directory.");
|
|
111
|
-
return
|
|
111
|
+
return resolveHostChildPath(base, name);
|
|
112
112
|
}
|