c64bridge 0.9.2 → 1.0.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/AGENTS.md +24 -3
- package/README.md +271 -161
- package/data/basic/rom-routines.md +269 -0
- package/data/context/bootstrap.md +1 -1
- package/data/io/cia-spec.md +2 -2
- package/data/{memory/kernal-memory-map.md → kernal/rom-routines.md} +3 -2
- package/data/memory/low-memory-map.md +1 -0
- package/data/memory/memory-map.md +13 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +127 -12
- package/dist/config.js +34 -8
- package/dist/device.js +75 -45
- package/dist/mcp-server.js +28 -8
- package/dist/platform.js +13 -2
- package/dist/rag/knowledgeIndex.js +54 -9
- package/dist/rag/resourceUris.js +2 -0
- package/dist/tools/audio.js +1 -1
- package/dist/tools/developer.js +6 -6
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/input.js +129 -12
- package/dist/tools/machineControl.js +85 -6
- package/dist/tools/memory.js +1 -1
- package/dist/tools/meta/index.js +1 -1
- package/dist/tools/programRunners.js +5 -5
- package/dist/tools/rag.js +1 -1
- package/dist/tools/registry/batch.js +1 -1
- package/dist/tools/registry/config.js +5 -5
- package/dist/tools/registry/disk.js +2 -2
- package/dist/tools/registry/drive.js +2 -2
- package/dist/tools/registry/graphics.js +2 -2
- package/dist/tools/registry/memory.js +2 -2
- package/dist/tools/registry/platform.js +7 -5
- package/dist/tools/registry/program.js +5 -5
- package/dist/tools/registry/rag.js +1 -1
- package/dist/tools/registry/sound.js +4 -4
- package/dist/tools/registry/system.js +13 -3
- package/dist/tools/storage.js +1 -1
- package/doc/c64u/c64-openapi.yaml +1130 -774
- package/doc/developer.md +3 -2
- package/doc/u2/u2-openapi.yaml +1192 -0
- package/generated/c64/index.js +142 -9
- package/generated/openapi/c64u/index.js +1117 -0
- package/generated/openapi/u2/index.js +929 -0
- package/mcp/resources.json +60 -7
- package/mcp/schemas/c64_graphics.schema.json +3 -3
- package/mcp/schemas/c64_input.schema.json +247 -6
- package/mcp/schemas/c64_memory.schema.json +3 -3
- package/mcp/schemas/c64_program.schema.json +3 -3
- package/mcp/schemas/c64_select_backend.schema.json +2 -0
- package/mcp/schemas/c64_system.schema.json +41 -1
- package/mcp/server.json +1 -1
- package/mcp/tools.json +382 -41
- package/mcp.json +21 -5
- package/package.json +14 -8
- package/scripts/mockC64Server.mjs +40 -0
- package/scripts/prepare-release.mjs +13 -2
- package/scripts/update-readme.ts +19 -30
- package/server.json +2 -2
package/dist/c64Client.js
CHANGED
|
@@ -10,14 +10,26 @@ import { createSocket } from "node:dgram";
|
|
|
10
10
|
import axios from "axios";
|
|
11
11
|
import { basicToPrg } from "./tools/translation/basicTokenizer.js";
|
|
12
12
|
import { assemblyToPrg } from "./tools/translation/assembler.js";
|
|
13
|
-
import { screenCodesToAscii } from "./petscii.js";
|
|
13
|
+
import { petsciiToAscii, screenCodesToAscii } from "./petscii.js";
|
|
14
14
|
import { resolveAddressSymbol } from "./knowledge.js";
|
|
15
|
-
import { createAllFacades, createFacade } from "./device.js";
|
|
15
|
+
import { createAllFacades, createFacade, } from "./device.js";
|
|
16
16
|
import { Api } from "../generated/c64/index.js";
|
|
17
17
|
import { createLoggingHttpClient } from "./loggingHttpClient.js";
|
|
18
18
|
import { withDiagnosticSpan, writeDiagnosticEvent } from "./diagnostics.js";
|
|
19
19
|
import { C64U_NTSC_AUDIO_SAMPLE_RATE, C64U_PAL_AUDIO_SAMPLE_RATE, collectCompleteVideoFrames, parseAudioPacket, parseVideoPacket, } from "./streamCapture.js";
|
|
20
20
|
import { buildVicBitmapRegisters, resolveVicBitmapMemoryLayout, } from "./vicBitmap.js";
|
|
21
|
+
function menuMatrixIncludes(matrix, expected) {
|
|
22
|
+
// Firmware supplies the character matrix followed by its colour matrix. The
|
|
23
|
+
// documented wire format is raw bytes, so only inspect the first 40×25
|
|
24
|
+
// character cells when that complete layout is present.
|
|
25
|
+
const characters = matrix.length >= 2000 ? matrix.subarray(0, 1000) : matrix;
|
|
26
|
+
const normalizedExpected = expected.replace(/\s+/g, " ").toUpperCase();
|
|
27
|
+
return [screenCodesToAscii(characters), petsciiToAscii(characters)]
|
|
28
|
+
.some((text) => text.replace(/\s+/g, " ").toUpperCase().includes(normalizedExpected));
|
|
29
|
+
}
|
|
30
|
+
function isMissingEndpoint(error) {
|
|
31
|
+
return axios.isAxiosError(error) && error.response?.status === 404;
|
|
32
|
+
}
|
|
21
33
|
const TEXT_SCREEN_COLUMNS = 40;
|
|
22
34
|
const TEXT_SCREEN_ROWS = 25;
|
|
23
35
|
const TEXT_SCREEN_SIZE = TEXT_SCREEN_COLUMNS * TEXT_SCREEN_ROWS;
|
|
@@ -45,6 +57,7 @@ export class C64Client {
|
|
|
45
57
|
this.warmupPromises = new Map();
|
|
46
58
|
this.greetingDd00Cache = new Map();
|
|
47
59
|
this.greetingDd00Warmups = new Map();
|
|
60
|
+
this.machineInputCapabilityProbes = new Map();
|
|
48
61
|
this.c64uVideoCaptureSession = null;
|
|
49
62
|
this.activeType = "c64u";
|
|
50
63
|
this.baseUrl = baseUrl;
|
|
@@ -59,7 +72,6 @@ export class C64Client {
|
|
|
59
72
|
}).then((sel) => sel.facade);
|
|
60
73
|
this.allFacades.set("c64u", this.facadePromise);
|
|
61
74
|
this.initPromise = Promise.resolve();
|
|
62
|
-
void this.primeGreetingDd00Cache(["c64u"]);
|
|
63
75
|
return;
|
|
64
76
|
}
|
|
65
77
|
const allFacadesPromise = createAllFacades(undefined, {
|
|
@@ -67,21 +79,17 @@ export class C64Client {
|
|
|
67
79
|
preferredC64uNetworkPassword: options.networkPassword,
|
|
68
80
|
});
|
|
69
81
|
this.facadePromise = allFacadesPromise.then(({ primary }) => primary.facade);
|
|
70
|
-
this.initPromise = allFacadesPromise.then(({ primary,
|
|
82
|
+
this.initPromise = allFacadesPromise.then(({ primary, facades }) => {
|
|
71
83
|
const primaryPromise = Promise.resolve(primary.facade);
|
|
72
84
|
this.activeType = primary.selected;
|
|
73
85
|
this.facadePromise = primaryPromise;
|
|
74
86
|
this.allFacades.set(primary.selected, primaryPromise);
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
for (const [type, facade] of facades) {
|
|
88
|
+
if (type !== primary.selected) {
|
|
89
|
+
this.allFacades.set(type, Promise.resolve(facade));
|
|
90
|
+
}
|
|
77
91
|
}
|
|
78
92
|
});
|
|
79
|
-
void this.initPromise.then(async () => {
|
|
80
|
-
if (this.allFacades.has("vice")) {
|
|
81
|
-
await this.prewarmBackends(["vice"]);
|
|
82
|
-
}
|
|
83
|
-
void this.primeGreetingDd00Cache();
|
|
84
|
-
}).catch(() => { });
|
|
85
93
|
}
|
|
86
94
|
async requireViceBackend() {
|
|
87
95
|
const facade = await this.facadePromise;
|
|
@@ -97,6 +105,28 @@ export class C64Client {
|
|
|
97
105
|
await this.initPromise;
|
|
98
106
|
return this.activeType;
|
|
99
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Discover native endpoint availability without changing machine state.
|
|
110
|
+
* `machine:input` has a safe GET probe; `machine:menu_screen` is only
|
|
111
|
+
* distinguishable from an inactive menu once a menu is visible.
|
|
112
|
+
*/
|
|
113
|
+
async getNativeEndpointCapabilities() {
|
|
114
|
+
const facade = await this.facadePromise;
|
|
115
|
+
if (facade.type === "vice") {
|
|
116
|
+
return { machineInput: "unavailable", machineMenuScreen: "unavailable" };
|
|
117
|
+
}
|
|
118
|
+
if (facade.type === "u2") {
|
|
119
|
+
return { machineInput: "unavailable", machineMenuScreen: "unknown" };
|
|
120
|
+
}
|
|
121
|
+
let probe = this.machineInputCapabilityProbes.get(facade.type);
|
|
122
|
+
if (!probe) {
|
|
123
|
+
probe = facade.getInputState()
|
|
124
|
+
.then(() => "available")
|
|
125
|
+
.catch((error) => isMissingEndpoint(error) ? "unavailable" : "unknown");
|
|
126
|
+
this.machineInputCapabilityProbes.set(facade.type, probe);
|
|
127
|
+
}
|
|
128
|
+
return { machineInput: await probe, machineMenuScreen: "unknown" };
|
|
129
|
+
}
|
|
100
130
|
getAvailableBackends() {
|
|
101
131
|
return Array.from(this.allFacades.keys());
|
|
102
132
|
}
|
|
@@ -692,6 +722,79 @@ export class C64Client {
|
|
|
692
722
|
return { success: false, details: this.normaliseError(error) };
|
|
693
723
|
}
|
|
694
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* Start from a clean machine state. C64U/U64 firmware exposes this only in
|
|
727
|
+
* the Tool Menu; U2-family firmware uses its equivalent REST reboot and
|
|
728
|
+
* VICE is restarted when it is managed by c64bridge.
|
|
729
|
+
*/
|
|
730
|
+
async powerCycle() {
|
|
731
|
+
try {
|
|
732
|
+
const facade = await this.facadePromise;
|
|
733
|
+
if (facade.type === "u2") {
|
|
734
|
+
const result = await facade.reboot();
|
|
735
|
+
return { ...result, details: { strategy: "reboot", result: result.details } };
|
|
736
|
+
}
|
|
737
|
+
if (facade.type === "vice") {
|
|
738
|
+
const result = await facade.powerCycle();
|
|
739
|
+
return { ...result, details: { strategy: "fresh_start", result: result.details } };
|
|
740
|
+
}
|
|
741
|
+
const endpoints = await this.getNativeEndpointCapabilities();
|
|
742
|
+
if (endpoints.machineInput !== "available") {
|
|
743
|
+
return {
|
|
744
|
+
success: false,
|
|
745
|
+
details: {
|
|
746
|
+
code: "native_input_unavailable",
|
|
747
|
+
message: "C64U/U64 Power Cycle requires machine:input and machine:menu_screen, which this firmware does not currently expose.",
|
|
748
|
+
fallback: "Use c64_system reboot if a firmware reboot is acceptable, or power cycle from the device itself.",
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
const menuResult = await facade.menuButton();
|
|
753
|
+
if (!menuResult.success) {
|
|
754
|
+
throw new Error("Unable to open the Ultimate menu for power cycling");
|
|
755
|
+
}
|
|
756
|
+
const initial = await this.readVerifiedMenuScreen(facade, "open Ultimate menu");
|
|
757
|
+
await this.requirePowerCycleInput(facade, ["f1"]);
|
|
758
|
+
await this.readVerifiedMenuScreen(facade, "open Tool Menu", "TOOL MENU", initial);
|
|
759
|
+
await this.requirePowerCycleInput(facade, ["return"]);
|
|
760
|
+
let current = await this.readVerifiedMenuScreen(facade, "enter Tool Menu", "POWER CYCLE");
|
|
761
|
+
for (let index = 0; index < 4; index += 1) {
|
|
762
|
+
await this.requirePowerCycleInput(facade, ["cursor_up_down"]);
|
|
763
|
+
current = await this.readVerifiedMenuScreen(facade, `move to Power Cycle (${index + 1}/4)`, "POWER CYCLE", current);
|
|
764
|
+
}
|
|
765
|
+
await this.requirePowerCycleInput(facade, ["return"]);
|
|
766
|
+
return { success: true, details: { strategy: "tool_menu", verifiedMenu: "Tool Menu", selectedItem: "Power Cycle" } };
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
if (isMissingEndpoint(error)) {
|
|
770
|
+
return {
|
|
771
|
+
success: false,
|
|
772
|
+
details: {
|
|
773
|
+
code: "native_menu_screen_unavailable",
|
|
774
|
+
message: "C64U/U64 Power Cycle could not verify Tool Menu navigation because machine:menu_screen is unavailable.",
|
|
775
|
+
fallback: "Use c64_system reboot if a firmware reboot is acceptable, or power cycle from the device itself.",
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
return { success: false, details: this.normaliseError(error) };
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async requirePowerCycleInput(facade, inputs) {
|
|
783
|
+
await facade.sendInputEvents({ events: [{ kind: "keyboard", inputs, transition: "tap" }] });
|
|
784
|
+
}
|
|
785
|
+
async readVerifiedMenuScreen(facade, phase, requiredText, previous) {
|
|
786
|
+
const matrix = await facade.readMenuScreen();
|
|
787
|
+
if (matrix.length === 0) {
|
|
788
|
+
throw new Error(`Unable to ${phase}: machine:menu_screen returned an empty matrix`);
|
|
789
|
+
}
|
|
790
|
+
if (previous && Buffer.compare(Buffer.from(previous), Buffer.from(matrix)) === 0) {
|
|
791
|
+
throw new Error(`Unable to ${phase}: machine:menu_screen did not change after menu input`);
|
|
792
|
+
}
|
|
793
|
+
if (requiredText && !menuMatrixIncludes(matrix, requiredText)) {
|
|
794
|
+
throw new Error(`Unable to ${phase}: machine:menu_screen did not contain '${requiredText}'`);
|
|
795
|
+
}
|
|
796
|
+
return matrix;
|
|
797
|
+
}
|
|
695
798
|
async menuButton() {
|
|
696
799
|
try {
|
|
697
800
|
if (await this.shouldUseC64uMockBypass()) {
|
|
@@ -705,6 +808,18 @@ export class C64Client {
|
|
|
705
808
|
return { success: false, details: this.normaliseError(error) };
|
|
706
809
|
}
|
|
707
810
|
}
|
|
811
|
+
async readMenuScreen() {
|
|
812
|
+
const facade = await this.facadePromise;
|
|
813
|
+
return facade.readMenuScreen();
|
|
814
|
+
}
|
|
815
|
+
async getInputState() {
|
|
816
|
+
const facade = await this.facadePromise;
|
|
817
|
+
return facade.getInputState();
|
|
818
|
+
}
|
|
819
|
+
async sendInputEvents(batch) {
|
|
820
|
+
const facade = await this.facadePromise;
|
|
821
|
+
return facade.sendInputEvents(batch);
|
|
822
|
+
}
|
|
708
823
|
async debugregRead() {
|
|
709
824
|
try {
|
|
710
825
|
if (await this.shouldUseC64uMockBypass()) {
|
package/dist/config.js
CHANGED
|
@@ -7,6 +7,7 @@ const DEFAULT_CONFIG = {
|
|
|
7
7
|
c64_host: DEFAULT_HOST,
|
|
8
8
|
baseUrl: buildBaseUrl(DEFAULT_HOST, DEFAULT_PORT),
|
|
9
9
|
c64_port: DEFAULT_PORT,
|
|
10
|
+
vicePrewarm: false,
|
|
10
11
|
};
|
|
11
12
|
let cachedConfig = null;
|
|
12
13
|
export function loadConfig() {
|
|
@@ -33,15 +34,20 @@ export function loadConfig() {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
rawConfig ?? (rawConfig = {});
|
|
36
|
-
// New schema:
|
|
37
|
+
// New schema: choose the configured Ultimate hardware profile; retain
|
|
38
|
+
// c64u and legacy fields as the default when C64_MODE is not set to u2.
|
|
37
39
|
const c64u = rawConfig?.c64u;
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
+
const u2 = rawConfig?.u2;
|
|
41
|
+
const useU2 = process.env.C64_MODE?.trim().toLowerCase() === "u2";
|
|
42
|
+
const hardware = useU2 ? (u2 ?? c64u) : (c64u ?? u2);
|
|
43
|
+
const vice = rawConfig?.vice;
|
|
44
|
+
const parsedC64uHost = parseEndpoint(configuredString(hardware?.host));
|
|
45
|
+
const parsedC64uHostname = parseEndpoint(configuredString(hardware?.hostname));
|
|
40
46
|
const parsedLegacyHost = parseEndpoint(configuredString(rawConfig?.c64_host));
|
|
41
47
|
const parsedLegacyIp = parseEndpoint(configuredString(rawConfig?.c64_ip));
|
|
42
|
-
const parsedEnvHost = parseEndpoint(configuredString(process.env.C64U_HOST));
|
|
48
|
+
const parsedEnvHost = parseEndpoint(configuredString(useU2 ? process.env.U2_HOST ?? process.env.C64U_HOST : process.env.C64U_HOST));
|
|
43
49
|
const parsedBaseOverrides = [
|
|
44
|
-
parseEndpoint(normaliseBaseUrl(
|
|
50
|
+
parseEndpoint(normaliseBaseUrl(hardware?.baseUrl)),
|
|
45
51
|
parseEndpoint(normaliseBaseUrl(rawConfig?.baseUrl)),
|
|
46
52
|
];
|
|
47
53
|
const hostCandidates = [
|
|
@@ -53,9 +59,9 @@ export function loadConfig() {
|
|
|
53
59
|
...parsedBaseOverrides.map((entry) => entry.hostname),
|
|
54
60
|
];
|
|
55
61
|
const portCandidates = [
|
|
56
|
-
configuredPort(process.env.C64U_PORT),
|
|
62
|
+
configuredPort(useU2 ? process.env.U2_PORT ?? process.env.C64U_PORT : process.env.C64U_PORT),
|
|
57
63
|
parsedEnvHost.port,
|
|
58
|
-
configuredPort(
|
|
64
|
+
configuredPort(hardware?.port),
|
|
59
65
|
parsedC64uHost.port,
|
|
60
66
|
parsedC64uHostname.port,
|
|
61
67
|
configuredPort(rawConfig?.c64_port),
|
|
@@ -72,7 +78,8 @@ export function loadConfig() {
|
|
|
72
78
|
c64_host: hostWithPort,
|
|
73
79
|
baseUrl,
|
|
74
80
|
c64_port: port,
|
|
75
|
-
networkPassword: firstDefined(configuredString(process.env.C64U_PASSWORD), configuredString(
|
|
81
|
+
networkPassword: firstDefined(configuredString(useU2 ? process.env.U2_PASSWORD ?? process.env.C64U_PASSWORD : process.env.C64U_PASSWORD), configuredString(hardware?.networkPassword), configuredString(rawConfig?.networkPassword)),
|
|
82
|
+
vicePrewarm: firstDefined(configuredBoolean(process.env.VICE_PREWARM), configuredBoolean(vice?.prewarm), configuredBoolean(rawConfig?.vicePrewarm)) ?? false,
|
|
76
83
|
};
|
|
77
84
|
cachedConfig = config;
|
|
78
85
|
return config;
|
|
@@ -92,6 +99,25 @@ function configuredPort(value) {
|
|
|
92
99
|
}
|
|
93
100
|
return undefined;
|
|
94
101
|
}
|
|
102
|
+
function configuredBoolean(value) {
|
|
103
|
+
if (typeof value === "boolean") {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
if (typeof value === "number") {
|
|
107
|
+
if (value === 1)
|
|
108
|
+
return true;
|
|
109
|
+
if (value === 0)
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (typeof value === "string") {
|
|
113
|
+
const normalized = value.trim().toLowerCase();
|
|
114
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
115
|
+
return true;
|
|
116
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
95
121
|
function normaliseBaseUrl(value) {
|
|
96
122
|
const input = configuredString(value);
|
|
97
123
|
if (!input)
|
package/dist/device.js
CHANGED
|
@@ -77,10 +77,13 @@ function readConfigFile() {
|
|
|
77
77
|
if (!merged.c64u && json?.c64u && typeof json.c64u === "object" && !Array.isArray(json.c64u)) {
|
|
78
78
|
merged.c64u = json.c64u;
|
|
79
79
|
}
|
|
80
|
+
if (!merged.u2 && json?.u2 && typeof json.u2 === "object" && !Array.isArray(json.u2)) {
|
|
81
|
+
merged.u2 = json.u2;
|
|
82
|
+
}
|
|
80
83
|
if (!merged.vice && json?.vice && typeof json.vice === "object" && !Array.isArray(json.vice)) {
|
|
81
84
|
merged.vice = json.vice;
|
|
82
85
|
}
|
|
83
|
-
if (merged.c64u && merged.vice) {
|
|
86
|
+
if ((merged.c64u || merged.u2) && merged.vice) {
|
|
84
87
|
break;
|
|
85
88
|
}
|
|
86
89
|
}
|
|
@@ -90,17 +93,17 @@ function readConfigFile() {
|
|
|
90
93
|
return foundCandidate ? merged : null;
|
|
91
94
|
}
|
|
92
95
|
class C64uBackend {
|
|
93
|
-
constructor(config) {
|
|
94
|
-
this.type =
|
|
95
|
-
const envHost = configuredString(process.env.C64U_HOST);
|
|
96
|
-
const envPort = configuredPort(process.env.C64U_PORT);
|
|
96
|
+
constructor(config, type = "c64u") {
|
|
97
|
+
this.type = type;
|
|
98
|
+
const envHost = configuredString(type === "u2" ? process.env.U2_HOST ?? process.env.C64U_HOST : process.env.C64U_HOST);
|
|
99
|
+
const envPort = configuredPort(type === "u2" ? process.env.U2_PORT ?? process.env.C64U_PORT : process.env.C64U_PORT);
|
|
97
100
|
const configBaseUrl = normaliseBaseUrl(config.baseUrl);
|
|
98
101
|
const parsedConfigBaseUrl = configBaseUrl ? parseEndpoint(configBaseUrl) : {};
|
|
99
102
|
const baseUrl = envHost !== undefined || envPort !== undefined
|
|
100
103
|
? buildBaseUrl(firstDefined(envHost, configuredString(config.host), configuredString(config.hostname), parsedConfigBaseUrl.hostname) ?? DEFAULT_C64U_HOST, firstDefined(envPort, configuredPort(config.port), parsedConfigBaseUrl.port) ?? DEFAULT_C64U_PORT)
|
|
101
104
|
: resolveBaseUrl(config);
|
|
102
105
|
this.baseUrl = baseUrl;
|
|
103
|
-
this.networkPassword = firstDefined(configuredString(process.env.C64U_PASSWORD), configuredString(config.networkPassword));
|
|
106
|
+
this.networkPassword = firstDefined(configuredString(type === "u2" ? process.env.U2_PASSWORD ?? process.env.C64U_PASSWORD : process.env.C64U_PASSWORD), configuredString(config.networkPassword));
|
|
104
107
|
const http = createLoggingHttpClient({
|
|
105
108
|
baseURL: baseUrl,
|
|
106
109
|
timeout: 10000,
|
|
@@ -108,6 +111,11 @@ class C64uBackend {
|
|
|
108
111
|
});
|
|
109
112
|
this.api = new Api(http);
|
|
110
113
|
}
|
|
114
|
+
requireC64u(feature) {
|
|
115
|
+
if (this.type === "u2") {
|
|
116
|
+
throw unsupported(`${feature} is unavailable on U2-family cartridges`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
111
119
|
getBaseUrl() { return this.baseUrl; }
|
|
112
120
|
async ping() {
|
|
113
121
|
try {
|
|
@@ -185,10 +193,26 @@ class C64uBackend {
|
|
|
185
193
|
async reboot() { const res = await this.api.v1.machineRebootUpdate(":reboot"); return { success: true, details: res.data }; }
|
|
186
194
|
async pause() { const res = await this.api.v1.machinePauseUpdate(":pause"); return { success: true, details: res.data }; }
|
|
187
195
|
async resume() { const res = await this.api.v1.machineResumeUpdate(":resume"); return { success: true, details: res.data }; }
|
|
188
|
-
async poweroff() { const res = await this.api.v1.machinePoweroffUpdate(":poweroff"); return { success: true, details: res.data }; }
|
|
196
|
+
async poweroff() { this.requireC64u("poweroff"); const res = await this.api.v1.machinePoweroffUpdate(":poweroff"); return { success: true, details: res.data }; }
|
|
197
|
+
async powerCycle() { this.requireC64u("powerCycle"); throw unsupported("powerCycle must be coordinated by C64Client"); }
|
|
189
198
|
async menuButton() { const res = await this.api.v1.machineMenuButtonUpdate(":menu_button"); return { success: true, details: res.data }; }
|
|
190
|
-
async
|
|
191
|
-
|
|
199
|
+
async readMenuScreen() {
|
|
200
|
+
const response = await this.api.v1.machineMenuScreenList(":menu_screen", { format: "arraybuffer", headers: { Accept: "application/octet-stream" } });
|
|
201
|
+
const body = response.data;
|
|
202
|
+
return body instanceof ArrayBuffer ? new Uint8Array(body) : extractBytes(body);
|
|
203
|
+
}
|
|
204
|
+
async getInputState() {
|
|
205
|
+
this.requireC64u("getInputState");
|
|
206
|
+
const response = await this.api.v1.machineInputList(":input");
|
|
207
|
+
return response.data;
|
|
208
|
+
}
|
|
209
|
+
async sendInputEvents(batch) {
|
|
210
|
+
this.requireC64u("sendInputEvents");
|
|
211
|
+
const response = await this.api.v1.machineInputCreate(":input", batch);
|
|
212
|
+
return response.data;
|
|
213
|
+
}
|
|
214
|
+
async debugregRead() { this.requireC64u("debugregRead"); const res = await this.api.v1.machineDebugregList(":debugreg"); return { success: true, value: res.data.value, details: res.data }; }
|
|
215
|
+
async debugregWrite(value) { this.requireC64u("debugregWrite"); const res = await this.api.v1.machineDebugregUpdate(":debugreg", { value }); return { success: true, value: res.data.value, details: res.data }; }
|
|
192
216
|
async version() { const res = await this.api.v1.versionList(); return res.data; }
|
|
193
217
|
async info() { const res = await this.api.v1.infoList(); return res.data; }
|
|
194
218
|
async drivesList() { const res = await this.api.v1.drivesList(); return res.data; }
|
|
@@ -199,8 +223,8 @@ class C64uBackend {
|
|
|
199
223
|
async driveOff(d) { const res = await this.api.v1.drivesOffUpdate(d, ":off"); return { success: true, details: res.data }; }
|
|
200
224
|
async driveSetMode(d, mode) { const res = await this.api.v1.drivesSetModeUpdate(d, ":set_mode", { mode }); return { success: true, details: res.data }; }
|
|
201
225
|
async driveLoadRom(d, romPath) { const res = await this.api.v1.drivesLoadRomUpdate(d, ":load_rom", { file: romPath }); return { success: true, details: res.data }; }
|
|
202
|
-
async streamStart(s, ip) { const res = await this.api.v1.streamsStartUpdate(s, ":start", { ip }); return { success: true, details: res.data }; }
|
|
203
|
-
async streamStop(s) { const res = await this.api.v1.streamsStopUpdate(s, ":stop"); return { success: true, details: res.data }; }
|
|
226
|
+
async streamStart(s, ip) { this.requireC64u("streamStart"); const res = await this.api.v1.streamsStartUpdate(s, ":start", { ip }); return { success: true, details: res.data }; }
|
|
227
|
+
async streamStop(s) { this.requireC64u("streamStop"); const res = await this.api.v1.streamsStopUpdate(s, ":stop"); return { success: true, details: res.data }; }
|
|
204
228
|
async configsList() { const res = await this.api.v1.configsList(); return res.data; }
|
|
205
229
|
async configGet(cat, item) { const res = item ? await this.api.v1.configsDetail2(cat, item) : await this.api.v1.configsDetail(cat); return res.data; }
|
|
206
230
|
async configSet(cat, item, value) { const res = await this.api.v1.configsUpdate(cat, item, { value }); return { success: true, details: res.data }; }
|
|
@@ -580,6 +604,7 @@ export class ViceBackend {
|
|
|
580
604
|
return { success: false, details };
|
|
581
605
|
}
|
|
582
606
|
}
|
|
607
|
+
async powerCycle() { return this.nuclearReset(); }
|
|
583
608
|
async nuclearReset() {
|
|
584
609
|
const poweroffResult = await this.poweroff();
|
|
585
610
|
if (!poweroffResult.success) {
|
|
@@ -606,6 +631,9 @@ export class ViceBackend {
|
|
|
606
631
|
}
|
|
607
632
|
}
|
|
608
633
|
async menuButton() { throw unsupported("menuButton"); }
|
|
634
|
+
async readMenuScreen() { throw unsupported("readMenuScreen"); }
|
|
635
|
+
async getInputState() { throw unsupported("getInputState"); }
|
|
636
|
+
async sendInputEvents(_batch) { throw unsupported("sendInputEvents"); }
|
|
609
637
|
async debugregRead() { throw unsupported("debugregRead"); }
|
|
610
638
|
async debugregWrite(_v) { throw unsupported("debugregWrite"); }
|
|
611
639
|
async version() { return { emulator: "vice", host: this.host, port: this.port }; }
|
|
@@ -885,46 +913,53 @@ export function __buildViceProcessOptionsForTests(input) {
|
|
|
885
913
|
return buildManagedViceProcessOptions(input);
|
|
886
914
|
}
|
|
887
915
|
export async function createFacade(logger, options) {
|
|
916
|
+
const envMode = (process.env.C64_MODE || "").toLowerCase().trim();
|
|
888
917
|
// Caller-forced preference: use c64u with provided base URL (used by tests and server wiring)
|
|
889
918
|
if (options?.preferredC64uBaseUrl) {
|
|
919
|
+
const type = envMode === "u2" ? "u2" : "c64u";
|
|
890
920
|
const backend = new C64uBackend({
|
|
891
921
|
baseUrl: options.preferredC64uBaseUrl,
|
|
892
922
|
networkPassword: options.preferredC64uNetworkPassword,
|
|
893
|
-
});
|
|
894
|
-
logger?.info?.(
|
|
895
|
-
return { facade: backend, selected:
|
|
923
|
+
}, type);
|
|
924
|
+
logger?.info?.(`Active backend: ${type} (forced by caller)`);
|
|
925
|
+
return { facade: backend, selected: type, reason: "forced by caller", details: { baseUrl: options.preferredC64uBaseUrl } };
|
|
896
926
|
}
|
|
897
927
|
const cfg = readConfigFile();
|
|
898
|
-
const envMode = (process.env.C64_MODE || "").toLowerCase().trim();
|
|
899
928
|
const hasC64u = Boolean(cfg?.c64u);
|
|
929
|
+
const hasU2 = Boolean(cfg?.u2);
|
|
900
930
|
const hasVice = Boolean(cfg?.vice);
|
|
901
931
|
if (envMode === "c64u") {
|
|
902
932
|
const backend = new C64uBackend(cfg?.c64u ?? {});
|
|
903
933
|
logger?.info?.("Active backend: c64u (from env override)");
|
|
904
934
|
return { facade: backend, selected: "c64u", reason: "env override", details: { baseUrl: backend.getBaseUrl?.() } };
|
|
905
935
|
}
|
|
936
|
+
if (envMode === "u2") {
|
|
937
|
+
const backend = new C64uBackend(cfg?.u2 ?? {}, "u2");
|
|
938
|
+
logger?.info?.("Active backend: u2 (from env override)");
|
|
939
|
+
return { facade: backend, selected: "u2", reason: "env override", details: { baseUrl: backend.getBaseUrl?.() } };
|
|
940
|
+
}
|
|
906
941
|
if (envMode === "vice") {
|
|
907
942
|
const backend = new ViceBackend(cfg?.vice ?? {});
|
|
908
943
|
logger?.info?.("Active backend: vice (from env override)");
|
|
909
944
|
const endpoint = backend.getEndpoint();
|
|
910
945
|
return { facade: backend, selected: "vice", reason: "env override", details: { host: endpoint.host, port: endpoint.port } };
|
|
911
946
|
}
|
|
912
|
-
if (hasC64u
|
|
947
|
+
if (hasC64u) {
|
|
913
948
|
const backend = new C64uBackend(cfg.c64u);
|
|
914
949
|
logger?.info?.("Active backend: c64u (from config)");
|
|
915
|
-
return { facade: backend, selected: "c64u", reason: "config only", details: { baseUrl: backend.getBaseUrl?.() } };
|
|
950
|
+
return { facade: backend, selected: "c64u", reason: hasVice ? "both defined (prefer c64u)" : "config only", details: { baseUrl: backend.getBaseUrl?.() } };
|
|
951
|
+
}
|
|
952
|
+
if (hasU2) {
|
|
953
|
+
const backend = new C64uBackend(cfg.u2, "u2");
|
|
954
|
+
logger?.info?.("Active backend: u2 (from config)");
|
|
955
|
+
return { facade: backend, selected: "u2", reason: "config only", details: { baseUrl: backend.getBaseUrl?.() } };
|
|
916
956
|
}
|
|
917
|
-
if (
|
|
957
|
+
if (hasVice) {
|
|
918
958
|
const backend = new ViceBackend(cfg.vice);
|
|
919
959
|
logger?.info?.("Active backend: vice (from config)");
|
|
920
960
|
const endpoint = backend.getEndpoint();
|
|
921
961
|
return { facade: backend, selected: "vice", reason: "config only", details: { host: endpoint.host, port: endpoint.port } };
|
|
922
962
|
}
|
|
923
|
-
if (hasC64u && hasVice) {
|
|
924
|
-
const backend = new C64uBackend(cfg.c64u);
|
|
925
|
-
logger?.info?.("Active backend: c64u (both defined; default preference)");
|
|
926
|
-
return { facade: backend, selected: "c64u", reason: "both defined (prefer c64u)", details: { baseUrl: backend.getBaseUrl?.() } };
|
|
927
|
-
}
|
|
928
963
|
// No configuration
|
|
929
964
|
const probeBase = resolveBaseUrl({});
|
|
930
965
|
try {
|
|
@@ -942,35 +977,30 @@ export async function createFacade(logger, options) {
|
|
|
942
977
|
return { facade: backend, selected: "vice", reason: "fallback (hardware unavailable)", details: { host: endpoint.host, port: endpoint.port } };
|
|
943
978
|
}
|
|
944
979
|
export async function createAllFacades(logger, options) {
|
|
945
|
-
|
|
980
|
+
// C64U/VICE preserve config-file selection semantics. U2 needs the
|
|
981
|
+
// caller-provided endpoint because it is selected through C64_MODE=u2.
|
|
982
|
+
const primary = await createFacade(logger, process.env.C64_MODE?.toLowerCase().trim() === "u2" ? options : undefined);
|
|
946
983
|
const config = readConfigFile();
|
|
947
|
-
const
|
|
948
|
-
if (
|
|
949
|
-
|
|
984
|
+
const facades = new Map([[primary.selected, primary.facade]]);
|
|
985
|
+
if (config?.c64u && primary.selected !== "c64u") {
|
|
986
|
+
facades.set("c64u", new C64uBackend(config.c64u));
|
|
987
|
+
}
|
|
988
|
+
if (config?.u2 && primary.selected !== "u2") {
|
|
989
|
+
facades.set("u2", new C64uBackend(config.u2, "u2"));
|
|
990
|
+
}
|
|
991
|
+
if (config?.vice && primary.selected !== "vice") {
|
|
992
|
+
facades.set("vice", new ViceBackend(config.vice));
|
|
993
|
+
}
|
|
994
|
+
if (!facades.has("c64u") && !config?.u2 && shouldProvisionSecondaryC64u(config, options) && primary.selected !== "u2") {
|
|
995
|
+
facades.set("c64u", new C64uBackend({
|
|
950
996
|
...(config?.c64u ?? {}),
|
|
951
997
|
...(!config?.c64u?.baseUrl && options?.preferredC64uBaseUrl ? { baseUrl: options.preferredC64uBaseUrl } : {}),
|
|
952
998
|
...(!config?.c64u?.networkPassword && options?.preferredC64uNetworkPassword
|
|
953
999
|
? { networkPassword: options.preferredC64uNetworkPassword }
|
|
954
1000
|
: {}),
|
|
955
|
-
};
|
|
956
|
-
return {
|
|
957
|
-
primary,
|
|
958
|
-
secondary: new C64uBackend(secondaryConfig),
|
|
959
|
-
secondaryType,
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
if (secondaryType === "vice" && config?.vice) {
|
|
963
|
-
return {
|
|
964
|
-
primary,
|
|
965
|
-
secondary: new ViceBackend(config.vice),
|
|
966
|
-
secondaryType,
|
|
967
|
-
};
|
|
1001
|
+
}));
|
|
968
1002
|
}
|
|
969
|
-
return {
|
|
970
|
-
primary,
|
|
971
|
-
secondary: null,
|
|
972
|
-
secondaryType: null,
|
|
973
|
-
};
|
|
1003
|
+
return { primary, facades };
|
|
974
1004
|
}
|
|
975
1005
|
function shouldProvisionSecondaryC64u(config, options) {
|
|
976
1006
|
return Boolean(config?.c64u
|
package/dist/mcp-server.js
CHANGED
|
@@ -60,6 +60,7 @@ async function main() {
|
|
|
60
60
|
writeDiagnosticEvent("config_loaded", {
|
|
61
61
|
baseUrl,
|
|
62
62
|
hasNetworkPassword: Boolean(config.networkPassword),
|
|
63
|
+
vicePrewarm: config.vicePrewarm,
|
|
63
64
|
});
|
|
64
65
|
// Initialize C64 client (reuse existing)
|
|
65
66
|
const client = await withDiagnosticSpan("startup", "create_client", { baseUrl }, async () => new C64Client(baseUrl, {
|
|
@@ -69,11 +70,22 @@ async function main() {
|
|
|
69
70
|
const initialBackendType = await withDiagnosticSpan("startup", "resolve_active_backend", {}, () => client.getActiveBackendType());
|
|
70
71
|
setPlatform(initialBackendType);
|
|
71
72
|
writeDiagnosticEvent("platform_initialised", { platform: initialBackendType });
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
if (config.vicePrewarm) {
|
|
74
|
+
const availableBackends = client.getAvailableBackends();
|
|
75
|
+
if (availableBackends.includes("vice")) {
|
|
76
|
+
writeDiagnosticEvent("vice_prewarm_start");
|
|
77
|
+
void client.prewarmBackends(["vice"])
|
|
78
|
+
.then((result) => {
|
|
79
|
+
writeDiagnosticEvent("vice_prewarm_complete", { result });
|
|
80
|
+
})
|
|
81
|
+
.catch((error) => {
|
|
82
|
+
writeDiagnosticEvent("vice_prewarm_failed", { error });
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
writeDiagnosticEvent("vice_prewarm_skipped", { reason: "vice_not_configured" });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
77
89
|
const rag = createLazyRagRetriever(() => initRag(), {
|
|
78
90
|
onInitStart() {
|
|
79
91
|
writeDiagnosticEvent("rag_init_start");
|
|
@@ -153,7 +165,7 @@ async function main() {
|
|
|
153
165
|
{
|
|
154
166
|
uri: PLATFORM_RESOURCE_URI,
|
|
155
167
|
mimeType: "text/markdown",
|
|
156
|
-
text: renderPlatformStatusMarkdown(client),
|
|
168
|
+
text: await renderPlatformStatusMarkdown(client),
|
|
157
169
|
},
|
|
158
170
|
],
|
|
159
171
|
};
|
|
@@ -435,10 +447,11 @@ function createPlatformResourceDescriptor() {
|
|
|
435
447
|
},
|
|
436
448
|
};
|
|
437
449
|
}
|
|
438
|
-
function renderPlatformStatusMarkdown(client) {
|
|
450
|
+
async function renderPlatformStatusMarkdown(client) {
|
|
439
451
|
const status = getPlatformStatus();
|
|
440
452
|
const availableBackends = client.getAvailableBackends();
|
|
441
453
|
const capabilities = describePlatformCapabilities(toolRegistry.list());
|
|
454
|
+
const nativeEndpoints = await client.getNativeEndpointCapabilities();
|
|
442
455
|
const lines = [
|
|
443
456
|
"# MCP Platform Status",
|
|
444
457
|
"",
|
|
@@ -454,6 +467,13 @@ function renderPlatformStatusMarkdown(client) {
|
|
|
454
467
|
status.limitedFeatures.length > 0
|
|
455
468
|
? ["## Limited or Unavailable Features", "", ...status.limitedFeatures.map((feature) => `- ${feature}`)].join("\n")
|
|
456
469
|
: "",
|
|
470
|
+
"## Native REST Endpoint Availability",
|
|
471
|
+
"",
|
|
472
|
+
`- \`machine:input\`: **${nativeEndpoints.machineInput}**`,
|
|
473
|
+
`- \`machine:menu_screen\`: **${nativeEndpoints.machineMenuScreen}** (it can only be confirmed while an Ultimate menu is visible)`,
|
|
474
|
+
"",
|
|
475
|
+
"Use physical-input, menu-screen, and C64U/U64 Tool Menu power-cycle operations only when the required endpoint is available. Prefer `key` or `write_text` for ordinary C64 input when `machine:input` is unavailable.",
|
|
476
|
+
"",
|
|
457
477
|
"## Tool Compatibility",
|
|
458
478
|
"",
|
|
459
479
|
].filter(Boolean);
|
|
@@ -475,7 +495,7 @@ function renderPlatformStatusMarkdown(client) {
|
|
|
475
495
|
}
|
|
476
496
|
async function logConnectivity(client, baseUrl) {
|
|
477
497
|
const backendType = await client.getBackendType();
|
|
478
|
-
if (backendType
|
|
498
|
+
if (backendType === "vice") {
|
|
479
499
|
writeDiagnosticEvent("connectivity_probe_skipped", { baseUrl, reason: "vice_backend" });
|
|
480
500
|
console.error("Skipping direct REST connectivity probe because active backend is VICE");
|
|
481
501
|
try {
|
package/dist/platform.js
CHANGED
|
@@ -10,6 +10,10 @@ const PLATFORM_FEATURES = {
|
|
|
10
10
|
],
|
|
11
11
|
limited: [],
|
|
12
12
|
},
|
|
13
|
+
u2: {
|
|
14
|
+
features: ["ultimate-rest-api", "cartridge-execution", "sid-control", "drive-management", "printer-integration"],
|
|
15
|
+
limited: ["no-machine-input", "no-debugreg", "no-poweroff", "no-streaming"],
|
|
16
|
+
},
|
|
13
17
|
vice: {
|
|
14
18
|
features: [
|
|
15
19
|
"software-emulation",
|
|
@@ -23,7 +27,7 @@ const PLATFORM_FEATURES = {
|
|
|
23
27
|
limited: ["no-rest-api", "no-firmware-filesystem", "no-flash-config", "limited-sid"],
|
|
24
28
|
},
|
|
25
29
|
};
|
|
26
|
-
let currentPlatform = process.env.C64_MODE === "vice" ? "vice" : "c64u";
|
|
30
|
+
let currentPlatform = process.env.C64_MODE === "vice" ? "vice" : process.env.C64_MODE === "u2" ? "u2" : "c64u";
|
|
27
31
|
export function getPlatformStatus() {
|
|
28
32
|
const spec = PLATFORM_FEATURES[currentPlatform];
|
|
29
33
|
return {
|
|
@@ -48,11 +52,12 @@ export function isPlatformSupported(platform, supported) {
|
|
|
48
52
|
export function describePlatformCapabilities(tools) {
|
|
49
53
|
const map = {
|
|
50
54
|
c64u: { available: [], unsupported: [] },
|
|
55
|
+
u2: { available: [], unsupported: [] },
|
|
51
56
|
vice: { available: [], unsupported: [] },
|
|
52
57
|
};
|
|
53
58
|
for (const tool of tools) {
|
|
54
59
|
const supported = tool.metadata.platforms ?? ["c64u"];
|
|
55
|
-
for (const id of ["c64u", "vice"]) {
|
|
60
|
+
for (const id of ["c64u", "u2", "vice"]) {
|
|
56
61
|
if (supported.includes(id)) {
|
|
57
62
|
map[id].available.push(tool.name);
|
|
58
63
|
}
|
|
@@ -69,6 +74,12 @@ export function describePlatformCapabilities(tools) {
|
|
|
69
74
|
tools: map.c64u.available.sort(),
|
|
70
75
|
unsupported_tools: map.c64u.unsupported.sort(),
|
|
71
76
|
},
|
|
77
|
+
u2: {
|
|
78
|
+
features: PLATFORM_FEATURES.u2.features,
|
|
79
|
+
limited_features: PLATFORM_FEATURES.u2.limited,
|
|
80
|
+
tools: map.u2.available.sort(),
|
|
81
|
+
unsupported_tools: map.u2.unsupported.sort(),
|
|
82
|
+
},
|
|
72
83
|
vice: {
|
|
73
84
|
features: PLATFORM_FEATURES.vice.features,
|
|
74
85
|
limited_features: PLATFORM_FEATURES.vice.limited,
|