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.
Files changed (58) hide show
  1. package/AGENTS.md +24 -3
  2. package/README.md +271 -161
  3. package/data/basic/rom-routines.md +269 -0
  4. package/data/context/bootstrap.md +1 -1
  5. package/data/io/cia-spec.md +2 -2
  6. package/data/{memory/kernal-memory-map.md → kernal/rom-routines.md} +3 -2
  7. package/data/memory/low-memory-map.md +1 -0
  8. package/data/memory/memory-map.md +13 -0
  9. package/dist/.tsbuildinfo +1 -1
  10. package/dist/c64Client.js +127 -12
  11. package/dist/config.js +34 -8
  12. package/dist/device.js +75 -45
  13. package/dist/mcp-server.js +28 -8
  14. package/dist/platform.js +13 -2
  15. package/dist/rag/knowledgeIndex.js +54 -9
  16. package/dist/rag/resourceUris.js +2 -0
  17. package/dist/tools/audio.js +1 -1
  18. package/dist/tools/developer.js +6 -6
  19. package/dist/tools/graphics.js +4 -4
  20. package/dist/tools/input.js +129 -12
  21. package/dist/tools/machineControl.js +85 -6
  22. package/dist/tools/memory.js +1 -1
  23. package/dist/tools/meta/index.js +1 -1
  24. package/dist/tools/programRunners.js +5 -5
  25. package/dist/tools/rag.js +1 -1
  26. package/dist/tools/registry/batch.js +1 -1
  27. package/dist/tools/registry/config.js +5 -5
  28. package/dist/tools/registry/disk.js +2 -2
  29. package/dist/tools/registry/drive.js +2 -2
  30. package/dist/tools/registry/graphics.js +2 -2
  31. package/dist/tools/registry/memory.js +2 -2
  32. package/dist/tools/registry/platform.js +7 -5
  33. package/dist/tools/registry/program.js +5 -5
  34. package/dist/tools/registry/rag.js +1 -1
  35. package/dist/tools/registry/sound.js +4 -4
  36. package/dist/tools/registry/system.js +13 -3
  37. package/dist/tools/storage.js +1 -1
  38. package/doc/c64u/c64-openapi.yaml +1130 -774
  39. package/doc/developer.md +3 -2
  40. package/doc/u2/u2-openapi.yaml +1192 -0
  41. package/generated/c64/index.js +142 -9
  42. package/generated/openapi/c64u/index.js +1117 -0
  43. package/generated/openapi/u2/index.js +929 -0
  44. package/mcp/resources.json +60 -7
  45. package/mcp/schemas/c64_graphics.schema.json +3 -3
  46. package/mcp/schemas/c64_input.schema.json +247 -6
  47. package/mcp/schemas/c64_memory.schema.json +3 -3
  48. package/mcp/schemas/c64_program.schema.json +3 -3
  49. package/mcp/schemas/c64_select_backend.schema.json +2 -0
  50. package/mcp/schemas/c64_system.schema.json +41 -1
  51. package/mcp/server.json +1 -1
  52. package/mcp/tools.json +382 -41
  53. package/mcp.json +21 -5
  54. package/package.json +14 -8
  55. package/scripts/mockC64Server.mjs +40 -0
  56. package/scripts/prepare-release.mjs +13 -2
  57. package/scripts/update-readme.ts +19 -30
  58. 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, secondary, secondaryType }) => {
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
- if (secondary && secondaryType) {
76
- this.allFacades.set(secondaryType, Promise.resolve(secondary));
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: prefer c64u.{host,port,baseUrl}; keep legacy fields as fallback
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 parsedC64uHost = parseEndpoint(configuredString(c64u?.host));
39
- const parsedC64uHostname = parseEndpoint(configuredString(c64u?.hostname));
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(c64u?.baseUrl)),
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(c64u?.port),
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(c64u?.networkPassword), configuredString(rawConfig?.networkPassword)),
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 = "c64u";
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 debugregRead() { const res = await this.api.v1.machineDebugregList(":debugreg"); return { success: true, value: res.data.value, details: res.data }; }
191
- async debugregWrite(value) { const res = await this.api.v1.machineDebugregUpdate(":debugreg", { value }); return { success: true, value: res.data.value, details: res.data }; }
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?.("Active backend: c64u (forced by caller)");
895
- return { facade: backend, selected: "c64u", reason: "forced by caller", details: { baseUrl: options.preferredC64uBaseUrl } };
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 && !hasVice) {
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 (!hasC64u && hasVice) {
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
- const primary = await createFacade(logger);
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 secondaryType = primary.selected === "c64u" ? "vice" : "c64u";
948
- if (secondaryType === "c64u" && shouldProvisionSecondaryC64u(config, options)) {
949
- const secondaryConfig = {
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
@@ -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
- void withDiagnosticSpan("startup", "prewarm_backends", { backends: ["vice"] }, () => client.prewarmBackends(["vice"])).then((results) => {
73
- writeDiagnosticEvent("backend_prewarm_complete", { results });
74
- }).catch((error) => {
75
- writeDiagnosticEvent("backend_prewarm_failed", { error });
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 !== "c64u") {
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,