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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
C64 Bridge - An MCP Server for the Commodore 64 Ultimate
|
|
3
|
+
Copyright (C) 2025 Christian Gleissner
|
|
4
|
+
|
|
5
|
+
Licensed under the GNU General Public License v2.0 or later.
|
|
6
|
+
See <https://www.gnu.org/licenses/> for details.
|
|
7
|
+
*/
|
|
8
|
+
import { ToolValidationError } from "./tools/errors.js";
|
|
9
|
+
/**
|
|
10
|
+
* Shared parser for memory address and length arguments.
|
|
11
|
+
*
|
|
12
|
+
* Accepted forms: `$C000`, `0xC000`, `%1010`, and decimal. Returns undefined
|
|
13
|
+
* when the text is not a numeric literal at all, so the caller can try a symbol
|
|
14
|
+
* name next. A decimal with a leading zero (`0400`) is rejected as ambiguous,
|
|
15
|
+
* because it was previously read as decimal 400 although it looks like hex.
|
|
16
|
+
*/
|
|
17
|
+
export function parseNumericLiteral(raw, path) {
|
|
18
|
+
const text = raw.trim();
|
|
19
|
+
if (/^\$[0-9a-f]+$/i.test(text))
|
|
20
|
+
return Number.parseInt(text.slice(1), 16);
|
|
21
|
+
if (/^0x[0-9a-f]+$/i.test(text))
|
|
22
|
+
return Number.parseInt(text.slice(2), 16);
|
|
23
|
+
if (/^%[01]+$/.test(text))
|
|
24
|
+
return Number.parseInt(text.slice(1), 2);
|
|
25
|
+
if (/^[0-9]+$/.test(text)) {
|
|
26
|
+
if (text.length > 1 && text.startsWith("0")) {
|
|
27
|
+
throw new ToolValidationError(`Ambiguous number "${text}": a decimal with a leading zero could also be hexadecimal. Write "$${text}" for hex or "${Number.parseInt(text, 10)}" for decimal.`, { path });
|
|
28
|
+
}
|
|
29
|
+
return Number.parseInt(text, 10);
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Throws for a bare string of hex digits that contains A-F (`C000`), which
|
|
35
|
+
* would otherwise be reported as an unknown symbol or an unparsable number.
|
|
36
|
+
* Call it after symbol lookup so names that happen to be hex digits still work.
|
|
37
|
+
*/
|
|
38
|
+
export function assertNotBareHex(raw, path) {
|
|
39
|
+
const text = raw.trim();
|
|
40
|
+
if (/^[0-9a-f]*[a-f][0-9a-f]*$/i.test(text)) {
|
|
41
|
+
throw new ToolValidationError(`Ambiguous value "${text}": hexadecimal needs a prefix. Write "$${text.toUpperCase()}" or "0x${text.toUpperCase()}".`, { path });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function formatAddressForMessage(value) {
|
|
45
|
+
return `$${value.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
46
|
+
}
|
|
47
|
+
/** Validates that an address (and optional length) lies within $0000-$FFFF. */
|
|
48
|
+
export function assertAddressRange(address, length, path) {
|
|
49
|
+
if (!Number.isInteger(address) || address < 0 || address > 0xffff) {
|
|
50
|
+
throw new ToolValidationError("Address must be in range $0000-$FFFF", { path });
|
|
51
|
+
}
|
|
52
|
+
if (length !== undefined && address + length - 1 > 0xffff) {
|
|
53
|
+
throw new ToolValidationError(`Memory range ${formatAddressForMessage(address)} + ${length} bytes passes $FFFF`, { path });
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/petscii.js
CHANGED
|
@@ -63,12 +63,22 @@ function ensureScreenCodeMap() {
|
|
|
63
63
|
}
|
|
64
64
|
screenCodeMapInitialised = true;
|
|
65
65
|
}
|
|
66
|
+
/** Letters of the lower-case set; reverse-video codes (+128) show the same letter. */
|
|
67
|
+
function lowerCaseSetChar(code) {
|
|
68
|
+
const base = code & 0x7f;
|
|
69
|
+
if (base >= 1 && base <= 26)
|
|
70
|
+
return String.fromCharCode(0x60 + base);
|
|
71
|
+
if (base >= 0x41 && base <= 0x5a)
|
|
72
|
+
return String.fromCharCode(base);
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
66
75
|
export function screenCodesToAscii(bytes, options) {
|
|
67
76
|
ensureScreenCodeMap();
|
|
68
77
|
const columns = options?.columns && options.columns > 0 ? options.columns : 40;
|
|
69
78
|
const maxRows = options?.rows && options.rows > 0 ? options.rows : undefined;
|
|
70
79
|
const trimSpaces = options?.trimTrailingSpaces !== false;
|
|
71
80
|
const trimEmptyRows = options?.trimTrailingEmptyRows !== false;
|
|
81
|
+
const lowerCase = options?.charset === "lower";
|
|
72
82
|
const lines = [];
|
|
73
83
|
const totalCells = maxRows ? Math.min(bytes.length, columns * maxRows) : bytes.length;
|
|
74
84
|
for (let index = 0; index < totalCells; index += columns) {
|
|
@@ -79,7 +89,9 @@ export function screenCodesToAscii(bytes, options) {
|
|
|
79
89
|
let line = "";
|
|
80
90
|
for (let idx = index; idx < end; idx += 1) {
|
|
81
91
|
const code = bytes[idx] ?? 0;
|
|
82
|
-
const char =
|
|
92
|
+
const char = lowerCase
|
|
93
|
+
? lowerCaseSetChar(code & 0xff) ?? SCREEN_CODE_TO_ASCII.get(code & 0xff) ?? petsciiByteToChar(code & 0xff)
|
|
94
|
+
: SCREEN_CODE_TO_ASCII.get(code & 0xff) ?? petsciiByteToChar(code & 0xff);
|
|
83
95
|
line += char;
|
|
84
96
|
}
|
|
85
97
|
if (trimSpaces) {
|
package/dist/platform.js
CHANGED
|
@@ -51,15 +51,21 @@ export function isPlatformSupported(platform, supported) {
|
|
|
51
51
|
}
|
|
52
52
|
export function describePlatformCapabilities(tools) {
|
|
53
53
|
const map = {
|
|
54
|
-
c64u: { available: [], unsupported: [] },
|
|
55
|
-
u2: { available: [], unsupported: [] },
|
|
56
|
-
vice: { available: [], unsupported: [] },
|
|
54
|
+
c64u: { available: [], unsupported: [], unsupportedOperations: [] },
|
|
55
|
+
u2: { available: [], unsupported: [], unsupportedOperations: [] },
|
|
56
|
+
vice: { available: [], unsupported: [], unsupportedOperations: [] },
|
|
57
57
|
};
|
|
58
58
|
for (const tool of tools) {
|
|
59
59
|
const supported = tool.metadata.platforms ?? ["c64u"];
|
|
60
60
|
for (const id of ["c64u", "u2", "vice"]) {
|
|
61
61
|
if (supported.includes(id)) {
|
|
62
62
|
map[id].available.push(tool.name);
|
|
63
|
+
// Operations of an available tool that this backend does not provide.
|
|
64
|
+
for (const [op, platforms] of Object.entries(tool.metadata.operationPlatforms ?? {})) {
|
|
65
|
+
if (platforms.length > 0 && !platforms.includes(id)) {
|
|
66
|
+
map[id].unsupportedOperations.push(`${tool.name}.${op}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
63
69
|
}
|
|
64
70
|
else {
|
|
65
71
|
map[id].unsupported.push(tool.name);
|
|
@@ -73,18 +79,21 @@ export function describePlatformCapabilities(tools) {
|
|
|
73
79
|
limited_features: PLATFORM_FEATURES.c64u.limited,
|
|
74
80
|
tools: map.c64u.available.sort(),
|
|
75
81
|
unsupported_tools: map.c64u.unsupported.sort(),
|
|
82
|
+
unsupported_operations: map.c64u.unsupportedOperations.sort(),
|
|
76
83
|
},
|
|
77
84
|
u2: {
|
|
78
85
|
features: PLATFORM_FEATURES.u2.features,
|
|
79
86
|
limited_features: PLATFORM_FEATURES.u2.limited,
|
|
80
87
|
tools: map.u2.available.sort(),
|
|
81
88
|
unsupported_tools: map.u2.unsupported.sort(),
|
|
89
|
+
unsupported_operations: map.u2.unsupportedOperations.sort(),
|
|
82
90
|
},
|
|
83
91
|
vice: {
|
|
84
92
|
features: PLATFORM_FEATURES.vice.features,
|
|
85
93
|
limited_features: PLATFORM_FEATURES.vice.limited,
|
|
86
94
|
tools: map.vice.available.sort(),
|
|
87
95
|
unsupported_tools: map.vice.unsupported.sort(),
|
|
96
|
+
unsupported_operations: map.vice.unsupportedOperations.sort(),
|
|
88
97
|
},
|
|
89
98
|
},
|
|
90
99
|
};
|
package/dist/prompts/registry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { listKnowledgeResources } from "../rag/knowledgeIndex.js";
|
|
2
2
|
import { RESOURCE_URIS } from "../rag/resourceUris.js";
|
|
3
3
|
import { toolRegistry } from "../tools/registry/index.js";
|
|
4
|
+
import { ToolValidationError } from "../tools/errors.js";
|
|
4
5
|
const ASSEMBLY_HARDWARE_NOTES = {
|
|
5
6
|
sid: [
|
|
6
7
|
"When routing this request, keep SID register usage, voice allocation, and timing side effects in scope.",
|
|
@@ -354,7 +355,11 @@ export function createPromptRegistry() {
|
|
|
354
355
|
resolve(name, args) {
|
|
355
356
|
const definition = definitionByName.get(name);
|
|
356
357
|
if (!definition) {
|
|
357
|
-
|
|
358
|
+
const allowed = Array.from(definitionByName.keys()).sort();
|
|
359
|
+
throw new ToolValidationError(`Unknown prompt: ${name}. Valid prompts: ${allowed.join(", ")}`, {
|
|
360
|
+
path: "$.name",
|
|
361
|
+
details: { allowed, received: name },
|
|
362
|
+
});
|
|
358
363
|
}
|
|
359
364
|
const rawArgs = args ?? {};
|
|
360
365
|
const prepared = definition.prepareArgs ? definition.prepareArgs(rawArgs) : {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ToolValidationError } from "./errors.js";
|
|
2
|
+
export const ACCESS_ENV_VAR = "C64BRIDGE_ACCESS";
|
|
3
|
+
const ACCESS_MODES = ["full", "no-destructive", "read-only"];
|
|
4
|
+
const warnedValues = new Set();
|
|
5
|
+
/**
|
|
6
|
+
* Reads the access mode from the environment at call time. An unset or empty variable means
|
|
7
|
+
* "full". An unknown value fails closed to "read-only" and logs a warning once per value.
|
|
8
|
+
*/
|
|
9
|
+
export function getAccessMode(env = process.env) {
|
|
10
|
+
const raw = env[ACCESS_ENV_VAR];
|
|
11
|
+
if (raw === undefined || raw.trim() === "") {
|
|
12
|
+
return "full";
|
|
13
|
+
}
|
|
14
|
+
const value = raw.trim().toLowerCase();
|
|
15
|
+
if (ACCESS_MODES.includes(value)) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
if (!warnedValues.has(value)) {
|
|
19
|
+
warnedValues.add(value);
|
|
20
|
+
process.stderr.write(`[c64bridge] Unknown ${ACCESS_ENV_VAR} value "${raw}"; using "read-only". Valid values: ${ACCESS_MODES.join(", ")}.\n`);
|
|
21
|
+
}
|
|
22
|
+
return "read-only";
|
|
23
|
+
}
|
|
24
|
+
export function isEffectAllowed(mode, effect) {
|
|
25
|
+
switch (mode) {
|
|
26
|
+
case "full":
|
|
27
|
+
return true;
|
|
28
|
+
case "no-destructive":
|
|
29
|
+
return effect !== "destructive";
|
|
30
|
+
default:
|
|
31
|
+
return effect === "read";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Throws a validation error when the active access mode does not allow the effect. */
|
|
35
|
+
export function assertAccess(toolName, op, effect) {
|
|
36
|
+
const mode = getAccessMode();
|
|
37
|
+
if (isEffectAllowed(mode, effect)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const subject = op ? `${toolName} operation "${op}"` : toolName;
|
|
41
|
+
throw new ToolValidationError(`${subject} is blocked: it has effect "${effect}" and the access mode is "${mode}" (${ACCESS_ENV_VAR}=${mode}). `
|
|
42
|
+
+ `${mode === "read-only" ? "Only read operations are allowed." : "Destructive operations are not allowed."}`, {
|
|
43
|
+
path: "$.op",
|
|
44
|
+
details: { tool: toolName, op: op ?? null, effect, accessMode: mode, variable: ACCESS_ENV_VAR },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Effect of a background task operation (read, write, read_screen, menu_button and the *_memory aliases). */
|
|
48
|
+
export function taskOperationEffect(operation) {
|
|
49
|
+
switch (operation) {
|
|
50
|
+
case "read":
|
|
51
|
+
case "read_memory":
|
|
52
|
+
case "read_screen":
|
|
53
|
+
return "read";
|
|
54
|
+
default:
|
|
55
|
+
// write, write_memory, menu_button and unknown names (rejected later by validation).
|
|
56
|
+
return "write";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function annotationsForEffects(effects) {
|
|
60
|
+
return {
|
|
61
|
+
readOnlyHint: effects.length > 0 && effects.every((effect) => effect === "read"),
|
|
62
|
+
destructiveHint: effects.includes("destructive"),
|
|
63
|
+
idempotentHint: false,
|
|
64
|
+
openWorldHint: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
package/dist/tools/debug.js
CHANGED
|
@@ -494,6 +494,12 @@ export const debugModuleGroup = defineToolModule({
|
|
|
494
494
|
description: "Available debugger operations.",
|
|
495
495
|
variants: debugOperationSchemas.map((schema) => schema.jsonSchema),
|
|
496
496
|
}),
|
|
497
|
+
operationEffects: {
|
|
498
|
+
list_checkpoints: "read", get_checkpoint: "read", create_checkpoint: "write", delete_checkpoint: "write",
|
|
499
|
+
toggle_checkpoint: "write", set_condition: "write", list_registers: "read", get_registers: "read",
|
|
500
|
+
set_registers: "write", step: "write", step_return: "write", get_monitor_state: "read",
|
|
501
|
+
wait_for_state: "read", nuclear_reset: "write", continue_execution: "write",
|
|
502
|
+
},
|
|
497
503
|
tags: ["debug", "breakpoint", "registers", "grouped"],
|
|
498
504
|
examples: [
|
|
499
505
|
{
|
package/dist/tools/developer.js
CHANGED
|
@@ -20,6 +20,16 @@ const noArgsSchema = objectSchema({
|
|
|
20
20
|
properties: {},
|
|
21
21
|
additionalProperties: false,
|
|
22
22
|
});
|
|
23
|
+
const flashCategoryArgsSchema = objectSchema({
|
|
24
|
+
description: "Optional configuration category; omit to apply to the whole device configuration.",
|
|
25
|
+
properties: {
|
|
26
|
+
category: optionalSchema(stringSchema({
|
|
27
|
+
description: "Configuration category name (e.g. 'Audio Mixer'). Omit to act on every category.",
|
|
28
|
+
minLength: 1,
|
|
29
|
+
})),
|
|
30
|
+
},
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
});
|
|
23
33
|
const categorySchema = stringSchema({
|
|
24
34
|
description: "Configuration category reported by the Ultimate firmware (e.g. 'Audio').",
|
|
25
35
|
minLength: 1,
|
|
@@ -70,16 +80,44 @@ const configGetArgsSchema = objectSchema({
|
|
|
70
80
|
required: ["category"],
|
|
71
81
|
additionalProperties: false,
|
|
72
82
|
});
|
|
83
|
+
const BATCH_CATEGORY_SCHEMA = {
|
|
84
|
+
type: "object",
|
|
85
|
+
description: "Item/value pairs of one category (primitive values only).",
|
|
86
|
+
additionalProperties: {
|
|
87
|
+
type: ["string", "number", "boolean"],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
function isCategoryMap(value) {
|
|
91
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
92
|
+
&& Object.values(value).every((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry));
|
|
93
|
+
}
|
|
94
|
+
function parseCategoryMap(input, resolvedPath, into) {
|
|
95
|
+
for (const [category, items] of Object.entries(input)) {
|
|
96
|
+
if (typeof items !== "object" || items === null || Array.isArray(items)) {
|
|
97
|
+
throw new ToolValidationError("Each category must map to an object of item values", {
|
|
98
|
+
path: `${resolvedPath}.${category}`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const parsedItems = into[category] ?? {};
|
|
102
|
+
for (const [item, rawValue] of Object.entries(items)) {
|
|
103
|
+
parsedItems[item] = primitiveValueSchema.parse(rawValue, `${resolvedPath}.${category}.${item}`);
|
|
104
|
+
}
|
|
105
|
+
into[category] = parsedItems;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
73
108
|
const configBatchSchema = {
|
|
74
109
|
jsonSchema: {
|
|
75
|
-
description: "
|
|
110
|
+
description: "Apply configuration changes. Documented form: `updates`, an object mapping categories to item/value pairs. "
|
|
111
|
+
+ "For backward compatibility the runtime also accepts the categories as top-level keys next to `op`.",
|
|
76
112
|
type: "object",
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
113
|
+
properties: {
|
|
114
|
+
updates: {
|
|
115
|
+
type: "object",
|
|
116
|
+
description: "Categories mapped to item/value pairs (primitive values only), for example {\"Audio\": {\"Volume\": 80}}.",
|
|
117
|
+
additionalProperties: BATCH_CATEGORY_SCHEMA,
|
|
81
118
|
},
|
|
82
119
|
},
|
|
120
|
+
additionalProperties: BATCH_CATEGORY_SCHEMA,
|
|
83
121
|
},
|
|
84
122
|
parse(value, path) {
|
|
85
123
|
const resolvedPath = path ?? "$";
|
|
@@ -88,21 +126,16 @@ const configBatchSchema = {
|
|
|
88
126
|
path: resolvedPath,
|
|
89
127
|
});
|
|
90
128
|
}
|
|
91
|
-
const input = value;
|
|
129
|
+
const input = { ...value };
|
|
92
130
|
const categories = {};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
const parsedItems = {};
|
|
100
|
-
for (const [item, rawValue] of Object.entries(items)) {
|
|
101
|
-
const parsedValue = primitiveValueSchema.parse(rawValue, `${resolvedPath}.${category}.${item}`);
|
|
102
|
-
parsedItems[item] = parsedValue;
|
|
103
|
-
}
|
|
104
|
-
categories[category] = parsedItems;
|
|
131
|
+
// `updates` is the documented form. A legacy category literally named "updates"
|
|
132
|
+
// holds primitive values, so it is not a category map and stays a legacy category.
|
|
133
|
+
if (Object.prototype.hasOwnProperty.call(input, "updates") && isCategoryMap(input.updates)) {
|
|
134
|
+
const updates = input.updates;
|
|
135
|
+
delete input.updates;
|
|
136
|
+
parseCategoryMap(updates, `${resolvedPath}.updates`, categories);
|
|
105
137
|
}
|
|
138
|
+
parseCategoryMap(input, resolvedPath, categories);
|
|
106
139
|
if (Object.keys(categories).length === 0) {
|
|
107
140
|
throw new ToolValidationError("Batch payload must contain at least one category", {
|
|
108
141
|
path: resolvedPath,
|
|
@@ -265,7 +298,7 @@ export const developerModule = defineToolModule({
|
|
|
265
298
|
relatedResources: ["c64://guide/bootstrap"],
|
|
266
299
|
tags: ["config", "write"],
|
|
267
300
|
examples: [
|
|
268
|
-
{ name: "Batch update", description: "Update multiple settings", arguments: { Audio: { Volume: 80, DACMode: "stereo" } } },
|
|
301
|
+
{ name: "Batch update", description: "Update multiple settings", arguments: { updates: { Audio: { Volume: 80, DACMode: "stereo" } } } },
|
|
269
302
|
],
|
|
270
303
|
workflowHints: [
|
|
271
304
|
"Use when multiple categories must be updated together; remind the user to review validation errors closely.",
|
|
@@ -305,7 +338,7 @@ export const developerModule = defineToolModule({
|
|
|
305
338
|
name: "config_load_from_flash",
|
|
306
339
|
description: "Load configuration settings from flash storage.",
|
|
307
340
|
summary: "Restores the firmware configuration by reading the persisted flash snapshot.",
|
|
308
|
-
inputSchema:
|
|
341
|
+
inputSchema: flashCategoryArgsSchema.jsonSchema,
|
|
309
342
|
relatedResources: ["c64://guide/bootstrap"],
|
|
310
343
|
tags: ["config", "flash"],
|
|
311
344
|
examples: [
|
|
@@ -316,15 +349,15 @@ export const developerModule = defineToolModule({
|
|
|
316
349
|
],
|
|
317
350
|
async execute(args, ctx) {
|
|
318
351
|
try {
|
|
319
|
-
|
|
320
|
-
ctx.logger.info("Loading configuration from flash");
|
|
321
|
-
const result = await ctx.client.configLoadFromFlash();
|
|
352
|
+
const { category } = flashCategoryArgsSchema.parse(args ?? {});
|
|
353
|
+
ctx.logger.info("Loading configuration from flash", { category });
|
|
354
|
+
const result = await ctx.client.configLoadFromFlash(category);
|
|
322
355
|
if (!result.success) {
|
|
323
356
|
throw new ToolExecutionError("C64 backend reported failure while loading from flash", {
|
|
324
357
|
details: toRecord(result.details),
|
|
325
358
|
});
|
|
326
359
|
}
|
|
327
|
-
return textResult(
|
|
360
|
+
return textResult(`Configuration${category ? ` category ${category}` : ""} loaded from flash.`, {
|
|
328
361
|
success: true,
|
|
329
362
|
details: toRecord(result.details) ?? null,
|
|
330
363
|
});
|
|
@@ -341,7 +374,7 @@ export const developerModule = defineToolModule({
|
|
|
341
374
|
name: "config_save_to_flash",
|
|
342
375
|
description: "Persist current configuration settings to flash storage.",
|
|
343
376
|
summary: "Saves the active configuration snapshot so it survives power cycles.",
|
|
344
|
-
inputSchema:
|
|
377
|
+
inputSchema: flashCategoryArgsSchema.jsonSchema,
|
|
345
378
|
relatedResources: ["c64://guide/bootstrap"],
|
|
346
379
|
tags: ["config", "flash"],
|
|
347
380
|
examples: [
|
|
@@ -352,15 +385,15 @@ export const developerModule = defineToolModule({
|
|
|
352
385
|
],
|
|
353
386
|
async execute(args, ctx) {
|
|
354
387
|
try {
|
|
355
|
-
|
|
356
|
-
ctx.logger.info("Saving configuration to flash");
|
|
357
|
-
const result = await ctx.client.configSaveToFlash();
|
|
388
|
+
const { category } = flashCategoryArgsSchema.parse(args ?? {});
|
|
389
|
+
ctx.logger.info("Saving configuration to flash", { category });
|
|
390
|
+
const result = await ctx.client.configSaveToFlash(category);
|
|
358
391
|
if (!result.success) {
|
|
359
392
|
throw new ToolExecutionError("C64 backend reported failure while saving to flash", {
|
|
360
393
|
details: toRecord(result.details),
|
|
361
394
|
});
|
|
362
395
|
}
|
|
363
|
-
return textResult(
|
|
396
|
+
return textResult(`Configuration${category ? ` category ${category}` : ""} saved to flash.`, {
|
|
364
397
|
success: true,
|
|
365
398
|
details: toRecord(result.details) ?? null,
|
|
366
399
|
});
|
|
@@ -377,7 +410,7 @@ export const developerModule = defineToolModule({
|
|
|
377
410
|
name: "config_reset_to_default",
|
|
378
411
|
description: "Reset configuration categories to their factory defaults.",
|
|
379
412
|
summary: "Instructs the firmware to discard overrides and restore default configuration values.",
|
|
380
|
-
inputSchema:
|
|
413
|
+
inputSchema: flashCategoryArgsSchema.jsonSchema,
|
|
381
414
|
relatedResources: ["c64://guide/bootstrap"],
|
|
382
415
|
tags: ["config", "reset"],
|
|
383
416
|
examples: [
|
|
@@ -388,15 +421,15 @@ export const developerModule = defineToolModule({
|
|
|
388
421
|
],
|
|
389
422
|
async execute(args, ctx) {
|
|
390
423
|
try {
|
|
391
|
-
|
|
392
|
-
ctx.logger.info("Resetting configuration to defaults");
|
|
393
|
-
const result = await ctx.client.configResetToDefault();
|
|
424
|
+
const { category } = flashCategoryArgsSchema.parse(args ?? {});
|
|
425
|
+
ctx.logger.info("Resetting configuration to defaults", { category });
|
|
426
|
+
const result = await ctx.client.configResetToDefault(category);
|
|
394
427
|
if (!result.success) {
|
|
395
428
|
throw new ToolExecutionError("C64 backend reported failure while resetting configuration", {
|
|
396
429
|
details: toRecord(result.details),
|
|
397
430
|
});
|
|
398
431
|
}
|
|
399
|
-
return textResult(
|
|
432
|
+
return textResult(`Configuration${category ? ` category ${category}` : ""} reset to defaults.`, {
|
|
400
433
|
success: true,
|
|
401
434
|
details: toRecord(result.details) ?? null,
|
|
402
435
|
});
|
package/dist/tools/errors.js
CHANGED
|
@@ -22,14 +22,39 @@ export class ToolExecutionError extends ToolError {
|
|
|
22
22
|
super(message, "execution", options);
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
function joinBackends(backends) {
|
|
26
|
+
return backends.join(" or ");
|
|
27
|
+
}
|
|
28
|
+
export function describeUnsupportedPlatform(subject, platform, supported, configured) {
|
|
29
|
+
const base = supported.length > 0
|
|
30
|
+
? `${subject} requires backend ${joinBackends(supported)} (active: ${platform}).`
|
|
31
|
+
: `${subject} is not available on backend ${platform}.`;
|
|
32
|
+
if (supported.length === 0) {
|
|
33
|
+
return base;
|
|
34
|
+
}
|
|
35
|
+
if (configured === undefined) {
|
|
36
|
+
return base;
|
|
37
|
+
}
|
|
38
|
+
const switchable = supported.filter((id) => id !== platform && configured.includes(id));
|
|
39
|
+
if (switchable.length > 0) {
|
|
40
|
+
return `${base} Use \`c64_select_backend\` to switch to ${joinBackends(switchable)}.`;
|
|
41
|
+
}
|
|
42
|
+
return `${base} None of these backends is configured on this server.`;
|
|
43
|
+
}
|
|
25
44
|
export class ToolUnsupportedPlatformError extends ToolExecutionError {
|
|
26
|
-
constructor(tool, platform, supported) {
|
|
27
|
-
|
|
45
|
+
constructor(tool, platform, supported, options) {
|
|
46
|
+
const subject = options?.toolName
|
|
47
|
+
? options.op
|
|
48
|
+
? `\`${options.toolName}\` op \`${options.op}\``
|
|
49
|
+
: `\`${options.toolName}\``
|
|
50
|
+
: tool;
|
|
51
|
+
super(describeUnsupportedPlatform(subject, platform, supported, options?.configured), {
|
|
28
52
|
code: "unsupported_platform",
|
|
29
53
|
details: {
|
|
30
54
|
tool,
|
|
31
55
|
platform,
|
|
32
56
|
supported,
|
|
57
|
+
...(options?.configured ? { configured: options.configured } : {}),
|
|
33
58
|
},
|
|
34
59
|
});
|
|
35
60
|
this.tool = tool;
|
|
@@ -48,7 +73,9 @@ export function toolErrorResult(error) {
|
|
|
48
73
|
// Clients often show only the text content, so surface the underlying cause
|
|
49
74
|
// (for example a range check or firmware error string) next to the summary.
|
|
50
75
|
const detailMessage = describeDetailMessage(error.details);
|
|
51
|
-
const
|
|
76
|
+
const withDetail = detailMessage && !located.includes(detailMessage) ? `${located}: ${detailMessage}` : located;
|
|
77
|
+
const limits = describeLimits(error.details);
|
|
78
|
+
const message = limits ? `${withDetail} [${limits}]` : withDetail;
|
|
52
79
|
const base = textResult(message, { error: metadata });
|
|
53
80
|
return { ...base, isError: true };
|
|
54
81
|
}
|
|
@@ -59,6 +86,30 @@ function describeDetailMessage(details) {
|
|
|
59
86
|
const message = details.message;
|
|
60
87
|
return typeof message === "string" && message.trim().length > 0 ? message.trim() : undefined;
|
|
61
88
|
}
|
|
89
|
+
const MAX_LISTED_VALUES = 40;
|
|
90
|
+
// Valid values and bounds recorded by validators, so a client sees them without reading the schema.
|
|
91
|
+
function describeLimits(details) {
|
|
92
|
+
if (!details || typeof details !== "object") {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const record = details;
|
|
96
|
+
const parts = [];
|
|
97
|
+
if (Array.isArray(record.allowed) && record.allowed.length > 0) {
|
|
98
|
+
const values = record.allowed.map((value) => String(value));
|
|
99
|
+
const shown = values.slice(0, MAX_LISTED_VALUES).join(", ");
|
|
100
|
+
parts.push(values.length > MAX_LISTED_VALUES ? `allowed: ${shown}, ... (${values.length} total)` : `allowed: ${shown}`);
|
|
101
|
+
}
|
|
102
|
+
for (const key of ["minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems"]) {
|
|
103
|
+
const value = record[key];
|
|
104
|
+
if (typeof value === "number" || typeof value === "string") {
|
|
105
|
+
parts.push(`${key}: ${value}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (parts.length > 0 && record.received !== undefined && typeof record.received !== "object") {
|
|
109
|
+
parts.push(`received: ${JSON.stringify(record.received)}`);
|
|
110
|
+
}
|
|
111
|
+
return parts.length > 0 ? parts.join("; ") : undefined;
|
|
112
|
+
}
|
|
62
113
|
export function unknownErrorResult(error) {
|
|
63
114
|
if (error instanceof ToolError) {
|
|
64
115
|
return toolErrorResult(error);
|
package/dist/tools/input.js
CHANGED
|
@@ -69,10 +69,26 @@ const PETSCII_TOKENS = {
|
|
|
69
69
|
// ASCII a-z are shifted graphics characters in PETSCII, so typed "print" would
|
|
70
70
|
// reach BASIC as graphics and give ?SYNTAX ERROR. Literal letters are therefore
|
|
71
71
|
// sent as the unshifted PETSCII letters $41-$5A; {tokens} keep their codes.
|
|
72
|
+
// Newline is RETURN. Any other character without a PETSCII equivalent, and any
|
|
73
|
+
// unknown {TOKEN}, is rejected instead of being typed as something else.
|
|
74
|
+
function describeChar(char) {
|
|
75
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
76
|
+
return `'${codePoint < 0x20 ? "" : char}' (U+${codePoint.toString(16).toUpperCase().padStart(4, "0")})`;
|
|
77
|
+
}
|
|
72
78
|
function expandPetsciiTokens(text) {
|
|
73
|
-
return text.replace(/\{([^}]+)\}|[
|
|
79
|
+
return text.replace(/\{([^}]+)\}|\r\n|[\s\S]/gu, (match, token, offset) => {
|
|
74
80
|
if (token === undefined) {
|
|
75
|
-
|
|
81
|
+
if (match === "\n" || match === "\r" || match === "\r\n") {
|
|
82
|
+
return String.fromCharCode(13);
|
|
83
|
+
}
|
|
84
|
+
const code = match.codePointAt(0) ?? 0;
|
|
85
|
+
if (code >= 0x61 && code <= 0x7a) {
|
|
86
|
+
return match.toUpperCase();
|
|
87
|
+
}
|
|
88
|
+
if (code >= 0x20 && code <= 0x7e) {
|
|
89
|
+
return match;
|
|
90
|
+
}
|
|
91
|
+
throw new ToolValidationError(`Character ${describeChar(match)} at position ${offset + 1} of the text has no PETSCII mapping. Use printable ASCII, \\n for RETURN, or a {TOKEN}.`, { path: "$.text" });
|
|
76
92
|
}
|
|
77
93
|
const upper = token.trim().toUpperCase();
|
|
78
94
|
const code = PETSCII_TOKENS[upper];
|
|
@@ -90,7 +106,7 @@ function expandPetsciiTokens(text) {
|
|
|
90
106
|
if (!isNaN(num) && num >= 0 && num <= 255) {
|
|
91
107
|
return String.fromCharCode(num);
|
|
92
108
|
}
|
|
93
|
-
|
|
109
|
+
throw new ToolValidationError(`Unknown token {${token}} at position ${offset + 1} of the text. Valid tokens: ${Object.keys(PETSCII_TOKENS).map((name) => `{${name}}`).join(", ")}, or a numeric code such as {$1D} or {29} (0-255).`, { path: "$.text" });
|
|
94
110
|
});
|
|
95
111
|
}
|
|
96
112
|
// ---------------------------------------------------------------------------
|
|
@@ -131,7 +147,7 @@ export const writeTextArgsSchema = objectSchema({
|
|
|
131
147
|
properties: {
|
|
132
148
|
op: literalSchema("write_text"),
|
|
133
149
|
text: stringSchema({
|
|
134
|
-
description: "Text to type
|
|
150
|
+
description: "Text to type: printable ASCII (letters are sent as PETSCII upper case), \\n for RETURN, and {TOKEN}s such as {RETURN}, {CLR}, {F1}…{F8}, {UP}, {DOWN}, {LEFT}, {RIGHT}, colour names, or {$1D}/{29} codes. Characters with no PETSCII mapping and unknown tokens are rejected.",
|
|
135
151
|
minLength: 1,
|
|
136
152
|
maxLength: 512,
|
|
137
153
|
}),
|
|
@@ -147,7 +163,7 @@ export const writeTextArgsSchema = objectSchema({
|
|
|
147
163
|
additionalProperties: false,
|
|
148
164
|
});
|
|
149
165
|
export const keyArgsSchema = objectSchema({
|
|
150
|
-
description: "
|
|
166
|
+
description: "Press a single key one or more times, optionally with a delay between presses.",
|
|
151
167
|
properties: {
|
|
152
168
|
op: literalSchema("key"),
|
|
153
169
|
key: stringSchema({
|
|
@@ -270,7 +286,7 @@ const inputOperationHandlers = {
|
|
|
270
286
|
const keyByte = tokenCode !== undefined
|
|
271
287
|
? tokenCode
|
|
272
288
|
: parsed.key.length === 1
|
|
273
|
-
? parsed.key
|
|
289
|
+
? expandPetsciiTokens(parsed.key).charCodeAt(0)
|
|
274
290
|
: (() => {
|
|
275
291
|
throw new ToolValidationError(`Unrecognised key: ${parsed.key}. Use a single character or a PETSCII token name.`, { path: "$.key" });
|
|
276
292
|
})();
|
|
@@ -417,6 +433,7 @@ export const inputModule = defineToolModule({
|
|
|
417
433
|
],
|
|
418
434
|
}),
|
|
419
435
|
operationPlatforms: { joystick: ["c64u", "vice"], keyboard: ["c64u"], release_all: ["c64u"], state: ["c64u"] },
|
|
436
|
+
operationEffects: { write_text: "write", key: "write", keyboard: "write", joystick: "write", release_all: "write", state: "read" },
|
|
420
437
|
tags: ["input", "keyboard", "joystick"],
|
|
421
438
|
examples: [
|
|
422
439
|
{
|