c64bridge 1.0.4 → 1.0.5

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 (41) hide show
  1. package/data/embeddings_basic.json +1 -1
  2. package/data/embeddings_hardware.json +1 -1
  3. package/data/embeddings_mixed.json +1 -1
  4. package/data/embeddings_other.json +1 -1
  5. package/data/graphics/character-set.csv +64 -64
  6. package/dist/.tsbuildinfo +1 -1
  7. package/dist/c64Client.js +65 -61
  8. package/dist/config.js +75 -38
  9. package/dist/device.js +122 -106
  10. package/dist/knowledge.js +2 -1
  11. package/dist/petscii.js +18 -15
  12. package/dist/petsciiArt.js +40 -14
  13. package/dist/rag/indexer.js +2 -1
  14. package/dist/sidwave.js +1 -1
  15. package/dist/sidwaveCompiler.js +3 -9
  16. package/dist/tools/audio.js +27 -6
  17. package/dist/tools/debug.js +9 -9
  18. package/dist/tools/errors.js +12 -1
  19. package/dist/tools/hex.js +9 -0
  20. package/dist/tools/input.js +13 -7
  21. package/dist/tools/memory.js +5 -4
  22. package/dist/tools/meta/background.js +6 -1
  23. package/dist/tools/meta/filesystem.js +37 -10
  24. package/dist/tools/meta/graphics.js +10 -15
  25. package/dist/tools/meta/memory.js +12 -18
  26. package/dist/tools/meta/program.js +19 -4
  27. package/dist/tools/pollValidator.js +30 -2
  28. package/dist/tools/programRunners.js +5 -4
  29. package/dist/tools/translation/assembler.js +150 -47
  30. package/dist/tools/translation/basicTokenizer.js +39 -9
  31. package/dist/tools/types.js +11 -4
  32. package/dist/vicBitmap.js +16 -0
  33. package/dist/vice/mockServer.js +7 -3
  34. package/dist/vice/viceClient.js +16 -6
  35. package/mcp/schemas/c64_system.schema.json +6 -4
  36. package/mcp/server.json +1 -1
  37. package/mcp/tools.json +6 -4
  38. package/mcp.json +1 -1
  39. package/package.json +1 -1
  40. package/scripts/mockC64Server.mjs +98 -19
  41. package/server.json +2 -2
package/dist/c64Client.js CHANGED
@@ -11,7 +11,8 @@ import { createSocket } from "node:dgram";
11
11
  import { networkInterfaces } from "node:os";
12
12
  import axios from "axios";
13
13
  import { basicToPrg } from "./tools/translation/basicTokenizer.js";
14
- import { assemblyToPrg } from "./tools/translation/assembler.js";
14
+ import { assemblyToPrg, prgEntryAddress } from "./tools/translation/assembler.js";
15
+ import { stripHexByteSeparators } from "./tools/hex.js";
15
16
  import { petsciiToAscii, screenCodesToAscii } from "./petscii.js";
16
17
  import { resolveAddressSymbol } from "./knowledge.js";
17
18
  import { createAllFacades, createFacade, } from "./device.js";
@@ -302,9 +303,6 @@ export class C64Client {
302
303
  }
303
304
  writeDiagnosticEvent("client_switch_backend", { backend: type });
304
305
  }
305
- async shouldUseC64uMockBypass() {
306
- return process.env.C64_TEST_TARGET === "mock" && (await this.getBackendType()) === "c64u";
307
- }
308
306
  async withViceMonitor(fn) {
309
307
  const backend = await this.requireViceBackend();
310
308
  return backend.withMonitor(fn);
@@ -546,7 +544,8 @@ export class C64Client {
546
544
  * tokens, and setting BASIC's TXTTAB/VARTAB/ARYTAB/STREND to the code
547
545
  * range corrupts the workspace. Instead this loads the assembled bytes
548
546
  * without running them, then types `SYS <entry>` to transfer control
549
- * directly to the first assembled instruction, whatever its address.
547
+ * to the entry point: the target of a leading SYS stub at $0801, or else
548
+ * the lowest assembled address.
550
549
  */
551
550
  async uploadAndRunAsm(program) {
552
551
  try {
@@ -554,7 +553,16 @@ export class C64Client {
554
553
  if (prg.length < 2) {
555
554
  return { success: false, details: { message: "Assembled program produced no output" } };
556
555
  }
557
- const entryAddress = prg.readUInt16LE(0);
556
+ const entryAddress = prgEntryAddress(prg);
557
+ if (entryAddress === 0x0801) {
558
+ // Loading makes BASIC relink $0801 as program lines, which overwrites raw code there.
559
+ return {
560
+ success: false,
561
+ details: {
562
+ message: "Machine code at $0801 is overwritten when BASIC relinks the loaded program. Omit the origin to get a BASIC SYS stub automatically, start the code with a BASIC SYS line, or assemble at another address such as $C000.",
563
+ },
564
+ };
565
+ }
558
566
  const facade = await this.facadePromise;
559
567
  const loadResult = await facade.loadPrg(prg);
560
568
  if (!loadResult.success) {
@@ -573,13 +581,6 @@ export class C64Client {
573
581
  }
574
582
  async runPrg(prg) {
575
583
  try {
576
- if (await this.shouldUseC64uMockBypass()) {
577
- const payload = Buffer.isBuffer(prg) ? prg : Buffer.from(prg);
578
- const response = await this.api.v1.runnersRunPrgCreate(":run_prg", payload, {
579
- headers: { "Content-Type": "application/octet-stream" },
580
- });
581
- return { success: true, details: response.data };
582
- }
583
584
  const facade = await this.facadePromise;
584
585
  const res = await facade.runPrg(prg);
585
586
  return { success: res.success, details: res.details };
@@ -659,10 +660,6 @@ export class C64Client {
659
660
  }
660
661
  async reset() {
661
662
  try {
662
- if (await this.shouldUseC64uMockBypass()) {
663
- const response = await this.api.v1.machineResetUpdate(":reset");
664
- return { success: true, details: response.data };
665
- }
666
663
  const facade = await this.facadePromise;
667
664
  const res = await facade.reset();
668
665
  return { success: res.success, details: res.details };
@@ -673,10 +670,6 @@ export class C64Client {
673
670
  }
674
671
  async reboot() {
675
672
  try {
676
- if (await this.shouldUseC64uMockBypass()) {
677
- const response = await this.api.v1.machineRebootUpdate(":reboot");
678
- return { success: true, details: response.data };
679
- }
680
673
  const facade = await this.facadePromise;
681
674
  const res = await facade.reboot();
682
675
  return { success: res.success, details: res.details };
@@ -794,7 +787,8 @@ export class C64Client {
794
787
  return { success: false, details: { message: "Voice must be 1..3" } };
795
788
  }
796
789
  const system = options.system ?? "PAL";
797
- const hz = options.frequencyHz ?? (options.note ? this.noteNameToHz(options.note) : 440);
790
+ // The tool schema documents that a note name overrides frequencyHz.
791
+ const hz = options.note ? this.noteNameToHz(options.note) : options.frequencyHz ?? 440;
798
792
  const freq16 = this.hzToSidFrequency(hz, system);
799
793
  const freqLo = freq16 & 0xff;
800
794
  const freqHi = (freq16 >> 8) & 0xff;
@@ -857,10 +851,6 @@ export class C64Client {
857
851
  }
858
852
  async pause(pinnedFacade) {
859
853
  try {
860
- if (!pinnedFacade && await this.shouldUseC64uMockBypass()) {
861
- const res = await this.api.v1.machinePauseUpdate(":pause");
862
- return { success: true, details: res.data };
863
- }
864
854
  const facade = pinnedFacade ?? await this.facadePromise;
865
855
  return await facade.pause();
866
856
  }
@@ -870,10 +860,6 @@ export class C64Client {
870
860
  }
871
861
  async resume(pinnedFacade) {
872
862
  try {
873
- if (!pinnedFacade && await this.shouldUseC64uMockBypass()) {
874
- const res = await this.api.v1.machineResumeUpdate(":resume");
875
- return { success: true, details: res.data };
876
- }
877
863
  const facade = pinnedFacade ?? await this.facadePromise;
878
864
  return await facade.resume();
879
865
  }
@@ -981,10 +967,6 @@ export class C64Client {
981
967
  }
982
968
  async menuButton() {
983
969
  try {
984
- if (await this.shouldUseC64uMockBypass()) {
985
- const res = await this.api.v1.machineMenuButtonUpdate(":menu_button");
986
- return { success: true, details: res.data };
987
- }
988
970
  const facade = await this.facadePromise;
989
971
  return await facade.menuButton();
990
972
  }
@@ -1006,10 +988,6 @@ export class C64Client {
1006
988
  }
1007
989
  async debugregRead() {
1008
990
  try {
1009
- if (await this.shouldUseC64uMockBypass()) {
1010
- const res = await this.api.v1.machineDebugregList(":debugreg");
1011
- return { success: true, value: res.data.value, details: res.data };
1012
- }
1013
991
  const facade = await this.facadePromise;
1014
992
  return await facade.debugregRead();
1015
993
  }
@@ -1019,10 +997,6 @@ export class C64Client {
1019
997
  }
1020
998
  async debugregWrite(value) {
1021
999
  try {
1022
- if (await this.shouldUseC64uMockBypass()) {
1023
- const res = await this.api.v1.machineDebugregUpdate(":debugreg", { value });
1024
- return { success: true, value: res.data.value, details: res.data };
1025
- }
1026
1000
  const facade = await this.facadePromise;
1027
1001
  return await facade.debugregWrite(value);
1028
1002
  }
@@ -1083,10 +1057,6 @@ export class C64Client {
1083
1057
  try {
1084
1058
  if (!drive || !path)
1085
1059
  throw new Error("Drive and path are required");
1086
- if (await this.shouldUseC64uMockBypass()) {
1087
- const res = await this.api.v1.drivesLoadRomUpdate(drive, ":load_rom", { file: path });
1088
- return { success: true, details: res.data };
1089
- }
1090
1060
  const facade = await this.facadePromise;
1091
1061
  const result = await facade.driveLoadRom(drive, path);
1092
1062
  return { success: result.success, details: result.details };
@@ -2184,8 +2154,7 @@ export class C64Client {
2184
2154
  if (trimmed.length === 0) {
2185
2155
  throw new Error("Expected non-empty byte string");
2186
2156
  }
2187
- const withoutPrefix = trimmed.startsWith("$") ? trimmed.slice(1) : trimmed;
2188
- const cleaned = withoutPrefix.replace(/[\s_]/g, "").toLowerCase();
2157
+ const cleaned = stripHexByteSeparators(trimmed).toLowerCase();
2189
2158
  if (cleaned.length === 0) {
2190
2159
  throw new Error("No hexadecimal data provided");
2191
2160
  }
@@ -2415,9 +2384,12 @@ export function buildPrinterBasicProgram(opts) {
2415
2384
  ln += 10;
2416
2385
  continue;
2417
2386
  }
2418
- const chunks = chunkString(logical, 45);
2387
+ // Chunk by the length of the expression, not the text: CHR$ splicing can make
2388
+ // an expression up to ten times longer, and a BASIC line must stay short
2389
+ // (the ROM's LINKPRG scans each line with an 8-bit index).
2390
+ const chunks = chunkByExpressionLength(logical, (text) => printerTextExpression(text, target, saddr), PRINTER_EXPRESSION_MAX);
2419
2391
  for (let i = 0; i < chunks.length; i += 1) {
2420
- const chunk = basicStringExpression(chunks[i]);
2392
+ const chunk = printerTextExpression(chunks[i], target, saddr);
2421
2393
  const tail = i < chunks.length - 1 ? ";" : ""; // avoid CR between chunks within the same logical line
2422
2394
  lines.push(`${ln} PRINT#1,${chunk}${tail}`);
2423
2395
  ln += 10;
@@ -2432,21 +2404,53 @@ export function buildPrinterBasicProgram(opts) {
2432
2404
  lines.push(`${ln} CLOSE1`);
2433
2405
  return lines.join("\n");
2434
2406
  }
2435
- function chunkString(input, maxLen) {
2436
- if (input.length <= maxLen)
2437
- return [input];
2407
+ /** Longest PRINT#1 expression per BASIC line; keeps each line under the 80-character input limit. */
2408
+ const PRINTER_EXPRESSION_MAX = 64;
2409
+ function chunkByExpressionLength(input, expression, maxLen) {
2438
2410
  const parts = [];
2439
- let i = 0;
2440
- while (i < input.length) {
2441
- parts.push(input.slice(i, i + maxLen));
2442
- i += maxLen;
2411
+ let current = "";
2412
+ for (const char of input) {
2413
+ if (current && expression(current + char).length > maxLen) {
2414
+ parts.push(current);
2415
+ current = "";
2416
+ }
2417
+ current += char;
2443
2418
  }
2419
+ if (current)
2420
+ parts.push(current);
2444
2421
  return parts;
2445
2422
  }
2446
- function basicStringExpression(input) {
2447
- // BASIC V2 has no doubled-quote escaping. Splice literal quote characters
2448
- // through CHR$(34), including leading/trailing/consecutive quote cases.
2449
- return input.split('"').map((part) => `"${part}"`).join(";CHR$(34);");
2423
+ /**
2424
+ * BASIC V2 has no doubled-quote escaping, so quotes are spliced in as CHR$(34).
2425
+ * String literals also store a-z as the PETSCII letters $41-$5A, so printer
2426
+ * text that needs other codes is spliced in with CHR$: an Epson printer takes
2427
+ * ASCII, so lowercase stays $61-$7A; a Commodore printer in lowercase mode
2428
+ * (secondary address 7) prints $41-$5A as lowercase and $C1-$DA as capitals.
2429
+ */
2430
+ function printerTextExpression(input, target, secondaryAddress) {
2431
+ const needsChr = (char) => {
2432
+ if (target === "epson" && char >= "a" && char <= "z")
2433
+ return char.charCodeAt(0);
2434
+ if (target === "commodore" && secondaryAddress === 7 && char >= "A" && char <= "Z")
2435
+ return char.charCodeAt(0) + 0x80;
2436
+ return undefined;
2437
+ };
2438
+ const pieces = [];
2439
+ let literal = "";
2440
+ for (const char of input) {
2441
+ const code = char === '"' ? 34 : needsChr(char);
2442
+ if (code === undefined) {
2443
+ literal += char;
2444
+ continue;
2445
+ }
2446
+ if (literal)
2447
+ pieces.push(`"${literal}"`);
2448
+ literal = "";
2449
+ pieces.push(`CHR$(${code})`);
2450
+ }
2451
+ if (literal || pieces.length === 0)
2452
+ pieces.push(`"${literal}"`);
2453
+ return pieces.join(";");
2450
2454
  }
2451
2455
  export function buildCommodoreBitmapBasicProgram(opts) {
2452
2456
  const repeats = Math.max(1, Math.floor(opts.repeats ?? 1));
package/dist/config.js CHANGED
@@ -4,13 +4,6 @@ import os from "node:os";
4
4
  import { fileURLToPath } from "url";
5
5
  const DEFAULT_HOST = "c64u";
6
6
  const DEFAULT_PORT = 80;
7
- const DEFAULT_CONFIG = {
8
- c64_host: DEFAULT_HOST,
9
- baseUrl: buildBaseUrl(DEFAULT_HOST, DEFAULT_PORT),
10
- c64_port: DEFAULT_PORT,
11
- vicePrewarm: false,
12
- backendConfig: {},
13
- };
14
7
  export function loadConfig() {
15
8
  const configPath = process.env.C64BRIDGE_CONFIG;
16
9
  const cwdConfigPath = join(process.cwd(), ".c64bridge.json");
@@ -39,61 +32,105 @@ export function loadConfig() {
39
32
  rawConfig ?? (rawConfig = {});
40
33
  // New schema: choose the configured Ultimate hardware profile; retain
41
34
  // c64u and legacy fields as the default when C64_MODE is not set to u2.
42
- const c64u = rawConfig?.c64u;
43
- const u2 = rawConfig?.u2;
35
+ const c64u = ultimateSection(rawConfig?.c64u);
36
+ const u2 = ultimateSection(rawConfig?.u2);
44
37
  const useU2 = process.env.C64_MODE?.trim().toLowerCase() === "u2";
45
- const hardware = useU2 ? (u2 ?? c64u) : (c64u ?? u2);
38
+ // Legacy top-level endpoint fields describe the single Ultimate of a
39
+ // pre-profile config, so they configure a backend only when the document
40
+ // has neither a c64u nor a u2 section. Each backend otherwise uses its own
41
+ // section, so one device's host, port or password never reaches the other.
42
+ const legacyUltimateEndpoint = !c64u && !u2 && LEGACY_ENDPOINT_KEYS.some((key) => hasLegacyValue(rawConfig?.[key]));
43
+ const ultimate = {
44
+ c64u: resolveUltimateEndpoint("c64u", c64u, c64u || !u2 ? rawConfig : undefined),
45
+ u2: resolveUltimateEndpoint("u2", u2 ?? c64u, u2 ? undefined : rawConfig),
46
+ };
47
+ // The selected profile reported by loadConfig keeps the documented fallback
48
+ // between sections and to the legacy fields.
49
+ const selected = useU2
50
+ ? resolveUltimateEndpoint("u2", u2 ?? c64u, rawConfig)
51
+ : resolveUltimateEndpoint("c64u", c64u ?? u2, rawConfig);
46
52
  const vice = rawConfig?.vice;
47
- const parsedC64uHost = parseEndpoint(configuredString(hardware?.host));
48
- const parsedC64uHostname = parseEndpoint(configuredString(hardware?.hostname));
49
- const parsedLegacyHost = parseEndpoint(configuredString(rawConfig?.c64_host));
50
- const parsedLegacyIp = parseEndpoint(configuredString(rawConfig?.c64_ip));
51
- const parsedEnvHost = parseEndpoint(configuredString(useU2 ? process.env.U2_HOST ?? process.env.C64U_HOST : process.env.C64U_HOST));
53
+ const config = {
54
+ c64_host: selected.c64_host,
55
+ baseUrl: selected.baseUrl,
56
+ c64_port: selected.port,
57
+ networkPassword: selected.networkPassword,
58
+ vicePrewarm: firstDefined(configuredBoolean(process.env.VICE_PREWARM), configuredBoolean(vice?.prewarm), configuredBoolean(rawConfig?.vicePrewarm)) ?? false,
59
+ ultimate,
60
+ legacyUltimateEndpoint,
61
+ backendConfig: {
62
+ ...(c64u ? { c64u } : {}),
63
+ ...(u2 ? { u2 } : {}),
64
+ ...(rawConfig?.vice && typeof rawConfig.vice === "object" && !Array.isArray(rawConfig.vice) ? { vice: rawConfig.vice } : {}),
65
+ },
66
+ };
67
+ return config;
68
+ }
69
+ /** Top-level fields from the pre-profile config schema that name an endpoint. */
70
+ const LEGACY_ENDPOINT_KEYS = ["baseUrl", "c64_host", "c64_ip", "c64_port"];
71
+ function hasLegacyValue(value) {
72
+ return (typeof value === "string" && value.trim().length > 0) || (typeof value === "number" && Number.isFinite(value));
73
+ }
74
+ function ultimateSection(value) {
75
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
76
+ }
77
+ /**
78
+ * The single host/port/password precedence for an Ultimate profile, used by
79
+ * loadConfig() and by every C64U/U2 backend construction:
80
+ * env > section host/hostname/port > legacy top-level c64_host/c64_ip/c64_port
81
+ * > section baseUrl > legacy top-level baseUrl > defaults. Passwords: env >
82
+ * section > legacy top-level networkPassword.
83
+ */
84
+ export function resolveUltimateEndpoint(profile, section, legacy) {
85
+ const envHost = profile === "u2" ? process.env.U2_HOST ?? process.env.C64U_HOST : process.env.C64U_HOST;
86
+ const envPort = profile === "u2" ? process.env.U2_PORT ?? process.env.C64U_PORT : process.env.C64U_PORT;
87
+ const envPassword = profile === "u2" ? process.env.U2_PASSWORD ?? process.env.C64U_PASSWORD : process.env.C64U_PASSWORD;
88
+ const parsedSectionHost = parseEndpoint(configuredString(section?.host));
89
+ const parsedSectionHostname = parseEndpoint(configuredString(section?.hostname));
90
+ const parsedLegacyHost = parseEndpoint(configuredString(legacy?.c64_host));
91
+ const parsedLegacyIp = parseEndpoint(configuredString(legacy?.c64_ip));
92
+ const parsedEnvHost = parseEndpoint(configuredString(envHost));
52
93
  const parsedBaseOverrides = [
53
- parseEndpoint(normaliseBaseUrl(hardware?.baseUrl)),
54
- parseEndpoint(normaliseBaseUrl(rawConfig?.baseUrl)),
94
+ parseEndpoint(normaliseBaseUrl(configuredString(section?.baseUrl))),
95
+ parseEndpoint(normaliseBaseUrl(configuredString(legacy?.baseUrl))),
55
96
  ];
56
97
  const hostCandidates = [
57
98
  parsedEnvHost.hostname,
58
- parsedC64uHost.hostname,
59
- parsedC64uHostname.hostname,
99
+ parsedSectionHost.hostname,
100
+ parsedSectionHostname.hostname,
60
101
  parsedLegacyHost.hostname,
61
102
  parsedLegacyIp.hostname,
62
103
  ...parsedBaseOverrides.map((entry) => entry.hostname),
63
104
  ];
64
105
  const portCandidates = [
65
- configuredPort(useU2 ? process.env.U2_PORT ?? process.env.C64U_PORT : process.env.C64U_PORT),
106
+ configuredPort(envPort),
66
107
  parsedEnvHost.port,
67
- configuredPort(hardware?.port),
68
- parsedC64uHost.port,
69
- parsedC64uHostname.port,
70
- configuredPort(rawConfig?.c64_port),
108
+ configuredPort(section?.port),
109
+ parsedSectionHost.port,
110
+ parsedSectionHostname.port,
111
+ configuredPort(legacy?.c64_port),
71
112
  parsedLegacyHost.port,
72
113
  parsedLegacyIp.port,
73
114
  ...parsedBaseOverrides.map((entry) => entry.port),
74
115
  ];
75
116
  const host = firstDefined(...hostCandidates) ?? DEFAULT_HOST;
76
117
  const port = firstDefined(...portCandidates) ?? DEFAULT_PORT;
77
- const baseUrl = buildBaseUrl(host, port);
78
118
  const hostLabel = formatHost(host);
79
- const hostWithPort = port === DEFAULT_PORT ? hostLabel : `${hostLabel}:${port}`;
80
- const config = {
81
- c64_host: hostWithPort,
82
- baseUrl,
83
- c64_port: port,
84
- networkPassword: firstDefined(configuredString(useU2 ? process.env.U2_PASSWORD ?? process.env.C64U_PASSWORD : process.env.C64U_PASSWORD), configuredString(hardware?.networkPassword), configuredString(rawConfig?.networkPassword)),
85
- vicePrewarm: firstDefined(configuredBoolean(process.env.VICE_PREWARM), configuredBoolean(vice?.prewarm), configuredBoolean(rawConfig?.vicePrewarm)) ?? false,
86
- backendConfig: {
87
- ...(rawConfig?.c64u && typeof rawConfig.c64u === "object" && !Array.isArray(rawConfig.c64u) ? { c64u: rawConfig.c64u } : {}),
88
- ...(rawConfig?.u2 && typeof rawConfig.u2 === "object" && !Array.isArray(rawConfig.u2) ? { u2: rawConfig.u2 } : {}),
89
- ...(rawConfig?.vice && typeof rawConfig.vice === "object" && !Array.isArray(rawConfig.vice) ? { vice: rawConfig.vice } : {}),
90
- },
119
+ return {
120
+ host,
121
+ port,
122
+ baseUrl: buildBaseUrl(host, port),
123
+ c64_host: port === DEFAULT_PORT ? hostLabel : `${hostLabel}:${port}`,
124
+ networkPassword: firstDefined(nonEmptyString(envPassword), nonEmptyString(section?.networkPassword), nonEmptyString(legacy?.networkPassword)),
91
125
  };
92
- return config;
93
126
  }
94
127
  function configuredString(value) {
95
128
  return typeof value === "string" ? value.trim() : undefined;
96
129
  }
130
+ function nonEmptyString(value) {
131
+ const trimmed = configuredString(value);
132
+ return trimmed ? trimmed : undefined;
133
+ }
97
134
  function configuredPort(value) {
98
135
  if (typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535) {
99
136
  return value;