c64bridge 0.5.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 (138) hide show
  1. package/.github/prompts/README.md +6 -0
  2. package/.github/prompts/assembly-routine-session.prompt.md +13 -0
  3. package/.github/prompts/basic-program-session.prompt.md +13 -0
  4. package/.github/prompts/drive-management-session.prompt.md +13 -0
  5. package/.github/prompts/petscii-art-session.prompt.md +13 -0
  6. package/.github/prompts/play-classic-game.prompt.md +13 -0
  7. package/.github/prompts/sid-music-session.prompt.md +13 -0
  8. package/AGENTS.md +173 -0
  9. package/LICENSE +339 -0
  10. package/README.md +582 -0
  11. package/data/README.md +16 -0
  12. package/data/api/basic-api-spec.md +154 -0
  13. package/data/api/kernal-api-spec.md +68 -0
  14. package/data/assembly/assembly-spec.md +196 -0
  15. package/data/assembly/examples/effects/sinewave.asm +5 -0
  16. package/data/assembly/examples/graphics/sprite/mover.asm +15 -0
  17. package/data/assembly/examples/sid-file-structure.md +8 -0
  18. package/data/assembly/examples/sid-overview.md +11 -0
  19. package/data/audio/examples/Biosphere.sid +0 -0
  20. package/data/audio/sid-file-structure.md +91 -0
  21. package/data/audio/sid-programming-best-practices.md +198 -0
  22. package/data/audio/sid-spec.md +166 -0
  23. package/data/audio/sidwave.md +131 -0
  24. package/data/audio/sidwave.schema.json +64 -0
  25. package/data/basic/basic-spec.md +230 -0
  26. package/data/basic/examples/audio/alle-meine-entchen-expressive.bas +41 -0
  27. package/data/basic/examples/audio/c-major-scale-pleasant.bas +29 -0
  28. package/data/basic/examples/io/joystick.bas +5 -0
  29. package/data/basic/examples/video/bounce.bas +8 -0
  30. package/data/basic/examples/video/entchen-petscii.bas +16 -0
  31. package/data/basic/examples/video/hello-world.bas +4 -0
  32. package/data/basic/examples/video/wave.bas +7 -0
  33. package/data/context/bootstrap.md +148 -0
  34. package/data/context/chat.md +36 -0
  35. package/data/drive/drive-spec.md +215 -0
  36. package/data/embeddings_asm.json +1 -0
  37. package/data/embeddings_basic.json +1 -0
  38. package/data/embeddings_hardware.json +1 -0
  39. package/data/embeddings_mixed.json +1 -0
  40. package/data/embeddings_other.json +1 -0
  41. package/data/io/cia-spec.md +115 -0
  42. package/data/io/io-spec.md +124 -0
  43. package/data/memory/kernal-memory-map.md +227 -0
  44. package/data/memory/low-memory-map.md +133 -0
  45. package/data/memory/memory-map.md +186 -0
  46. package/data/printer/printer-commodore-bitmap.md +31 -0
  47. package/data/printer/printer-commodore.md +28 -0
  48. package/data/printer/printer-epson-bitmap.md +29 -0
  49. package/data/printer/printer-epson.md +30 -0
  50. package/data/printer/printer-prompts.md +29 -0
  51. package/data/printer/printer-spec.md +180 -0
  52. package/data/video/character-set.csv +257 -0
  53. package/data/video/vic-spec.md +276 -0
  54. package/dist/assemblyConverter.js +1299 -0
  55. package/dist/audio/record_and_analyze_audio.js +292 -0
  56. package/dist/basicConverter.js +193 -0
  57. package/dist/bootstrap/stdio-logger.js +32 -0
  58. package/dist/c64Client.js +1052 -0
  59. package/dist/chargen.js +197 -0
  60. package/dist/config.js +142 -0
  61. package/dist/context.js +157 -0
  62. package/dist/device.js +413 -0
  63. package/dist/index.js +7 -0
  64. package/dist/knowledge.js +454 -0
  65. package/dist/logger.js +182 -0
  66. package/dist/loggingHttpClient.js +88 -0
  67. package/dist/mcp-server.js +426 -0
  68. package/dist/petscii.js +135 -0
  69. package/dist/petsciiArt.js +356 -0
  70. package/dist/platform.js +82 -0
  71. package/dist/prompts/registry.js +560 -0
  72. package/dist/rag/discover.config.json +41 -0
  73. package/dist/rag/discover.js +401 -0
  74. package/dist/rag/embeddings.js +60 -0
  75. package/dist/rag/externalFetcher.js +934 -0
  76. package/dist/rag/indexer.js +770 -0
  77. package/dist/rag/init.js +139 -0
  78. package/dist/rag/knowledgeIndex.js +361 -0
  79. package/dist/rag/loggingRetriever.js +46 -0
  80. package/dist/rag/rateLimiter.js +127 -0
  81. package/dist/rag/retriever.js +74 -0
  82. package/dist/rag/sources.csv +31 -0
  83. package/dist/rag/types.js +5 -0
  84. package/dist/rag/urlUtils.js +38 -0
  85. package/dist/sidplayRunner.js +240 -0
  86. package/dist/sidwave.js +101 -0
  87. package/dist/sidwaveCompiler.js +329 -0
  88. package/dist/tools/audio.js +944 -0
  89. package/dist/tools/developer.js +515 -0
  90. package/dist/tools/errors.js +59 -0
  91. package/dist/tools/graphics.js +469 -0
  92. package/dist/tools/machineControl.js +267 -0
  93. package/dist/tools/memory.js +224 -0
  94. package/dist/tools/printer.js +482 -0
  95. package/dist/tools/programRunners.js +315 -0
  96. package/dist/tools/rag.js +153 -0
  97. package/dist/tools/registry.js +58 -0
  98. package/dist/tools/responses.js +36 -0
  99. package/dist/tools/schema.js +304 -0
  100. package/dist/tools/storage.js +740 -0
  101. package/dist/tools/streaming.js +132 -0
  102. package/dist/tools/types.js +99 -0
  103. package/dist/viceRunner.js +239 -0
  104. package/doc/MCP_SETUP.md +153 -0
  105. package/doc/README.md +7 -0
  106. package/doc/developer.md +312 -0
  107. package/doc/img/logo.png +0 -0
  108. package/doc/img/prompts/duck_petscii.png +0 -0
  109. package/doc/img/prompts/duck_song.png +0 -0
  110. package/doc/img/vscode/vscode-copilot-c64-chat-mode.png +0 -0
  111. package/doc/img/vscode/vscode-copilot-hello-world.png +0 -0
  112. package/doc/prompts/README.md +96 -0
  113. package/doc/prompts/user-prompt-ideas.md +147 -0
  114. package/doc/rest/c64-openapi.yaml +979 -0
  115. package/doc/rest/c64-rest-api.md +106 -0
  116. package/doc/troubleshooting-mcp.md +177 -0
  117. package/generated/c64/index.js +857 -0
  118. package/mcp.json +106 -0
  119. package/package.json +79 -0
  120. package/scripts/c64-cli.mjs +142 -0
  121. package/scripts/c64-tool.sh +83 -0
  122. package/scripts/check-package.mjs +89 -0
  123. package/scripts/cli.js +61 -0
  124. package/scripts/fetch-external-sources.mjs +67 -0
  125. package/scripts/generate-changelog.mjs +131 -0
  126. package/scripts/postbuild.mjs +101 -0
  127. package/scripts/prepare-release.mjs +58 -0
  128. package/scripts/rag-rebuild.mjs +53 -0
  129. package/scripts/register-ts-node.mjs +6 -0
  130. package/scripts/run-mcp-check.sh +104 -0
  131. package/scripts/run-tests.mjs +195 -0
  132. package/scripts/sid2wav.sh +94 -0
  133. package/scripts/smoke-run.sh +65 -0
  134. package/scripts/start-mock-c64.mjs +51 -0
  135. package/scripts/start-mock.mjs +31 -0
  136. package/scripts/start.mjs +72 -0
  137. package/scripts/update-readme.ts +164 -0
  138. package/scripts/verify-package.mjs +147 -0
@@ -0,0 +1,292 @@
1
+ import { parseSidwave, midiToHz } from "../sidwave.js";
2
+ export async function recordAndAnalyzeAudio(params) {
3
+ const duration = Math.max(0.5, Math.min(120, Number(params.durationSeconds || 0)));
4
+ if (!Number.isFinite(duration)) {
5
+ throw new Error("durationSeconds must be a number");
6
+ }
7
+ // Capture PCM from default microphone using naudiodon (PortAudio)
8
+ const sampleRate = 44100;
9
+ const channelCount = 1;
10
+ let portAudio;
11
+ try {
12
+ // Use non-literal specifier to avoid TS resolution during tests when module isn't present
13
+ const naModule = "naudiodon";
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ portAudio = await import(naModule);
16
+ }
17
+ catch (e) {
18
+ throw new Error("Audio backend not available. Please install 'naudiodon' dependencies (PortAudio). No external CLI tools are required.");
19
+ }
20
+ const input = new portAudio.AudioIO({
21
+ inOptions: {
22
+ channelCount,
23
+ sampleFormat: portAudio.SampleFormat16Bit,
24
+ sampleRate,
25
+ deviceId: -1,
26
+ closeOnError: true,
27
+ },
28
+ });
29
+ const chunks = [];
30
+ input.on("data", (chunk) => {
31
+ chunks.push(chunk);
32
+ });
33
+ await new Promise((resolve, reject) => {
34
+ try {
35
+ input.start();
36
+ }
37
+ catch (err) {
38
+ reject(err);
39
+ return;
40
+ }
41
+ setTimeout(() => {
42
+ try {
43
+ // Some versions expose quit(); others expose stop(). Try both safely.
44
+ if (typeof input.quit === "function")
45
+ input.quit();
46
+ else if (typeof input.stop === "function")
47
+ input.stop();
48
+ }
49
+ catch { }
50
+ resolve();
51
+ }, Math.round(duration * 1000));
52
+ });
53
+ const pcm16 = Buffer.concat(chunks);
54
+ const float32 = convertInt16ToFloat32(pcm16);
55
+ const analysis = await analyzePcm(float32, sampleRate, params.expectedSidwave);
56
+ return analysis;
57
+ }
58
+ function convertInt16ToFloat32(buf) {
59
+ const len = Math.floor(buf.length / 2);
60
+ const out = new Float32Array(len);
61
+ for (let i = 0; i < len; i += 1) {
62
+ const v = buf.readInt16LE(i * 2);
63
+ out[i] = v / 32768;
64
+ }
65
+ return out;
66
+ }
67
+ async function analyzePcm(signal, sampleRate, expectedSidwave) {
68
+ let Pitchfinder;
69
+ let Meyda = null;
70
+ try {
71
+ Pitchfinder = (await import("pitchfinder")).default ?? (await import("pitchfinder"));
72
+ }
73
+ catch (e) {
74
+ throw new Error("Missing dependency: pitchfinder");
75
+ }
76
+ try {
77
+ // meyda exports default in recent versions
78
+ const m = await import("meyda");
79
+ Meyda = m.default ?? m;
80
+ }
81
+ catch (e) {
82
+ // Optional: fall back to manual RMS if meyda is unavailable in environment
83
+ Meyda = null;
84
+ }
85
+ const windowSize = 2048;
86
+ const hopSize = 512;
87
+ const yin = Pitchfinder.YIN({ sampleRate });
88
+ const frames = [];
89
+ for (let i = 0; i + windowSize <= signal.length; i += hopSize) {
90
+ const win = signal.subarray(i, i + windowSize);
91
+ const rms = computeRms(win, Meyda, sampleRate, windowSize);
92
+ const minRms = 0.01; // simple noise gate
93
+ let freq = null;
94
+ if (rms >= minRms) {
95
+ try {
96
+ const f = yin(win);
97
+ if (Number.isFinite(f) && f > 20 && f < 8000)
98
+ freq = f;
99
+ }
100
+ catch { }
101
+ }
102
+ frames.push({ t0: i / sampleRate, t1: (i + windowSize) / sampleRate, freq, rms });
103
+ }
104
+ const segments = groupSegments(frames, sampleRate);
105
+ // Map to detected notes with deviations
106
+ let expected;
107
+ if (expectedSidwave) {
108
+ try {
109
+ expected = parseSidwave(expectedSidwave);
110
+ }
111
+ catch {
112
+ expected = undefined;
113
+ }
114
+ }
115
+ const expectedMidiSet = extractExpectedMidiSet(expected);
116
+ const detected_notes = segments.map((seg) => {
117
+ if (seg.freq == null) {
118
+ return { note: null, frequency: null, duration_ms: Math.round(seg.duration * 1000), uncertain: true };
119
+ }
120
+ const midiMeasured = Math.round(69 + 12 * Math.log2(seg.freq / 440));
121
+ let deviation_cents;
122
+ if (expectedMidiSet && expectedMidiSet.length) {
123
+ const nearest = expectedMidiSet.reduce((best, m) => {
124
+ const hz = midiToHz(m);
125
+ const cents = 1200 * Math.log2(seg.freq / hz);
126
+ const d = Math.abs(cents);
127
+ return d < best.dist ? { midi: m, dist: d, cents } : best;
128
+ }, { midi: midiMeasured, dist: Infinity, cents: 0 });
129
+ deviation_cents = nearest.cents;
130
+ }
131
+ else {
132
+ const noteHz = midiToHz(midiMeasured);
133
+ deviation_cents = 1200 * Math.log2(seg.freq / noteHz);
134
+ }
135
+ const name = midiToName(midiMeasured);
136
+ return {
137
+ note: name,
138
+ frequency: round(seg.freq, 0.1),
139
+ duration_ms: Math.round(seg.duration * 1000),
140
+ deviation_cents: round(deviation_cents, 0.1),
141
+ };
142
+ });
143
+ // Basic BPM estimate from note change intervals
144
+ const onsetsSec = [];
145
+ let lastName = null;
146
+ let lastStart = null;
147
+ for (const seg of segments) {
148
+ const name = seg.freq ? midiToName(Math.round(69 + 12 * Math.log2(seg.freq / 440))) : null;
149
+ if (name && name !== lastName) {
150
+ onsetsSec.push(seg.t0);
151
+ lastName = name;
152
+ lastStart = seg.t0;
153
+ }
154
+ }
155
+ const ioi = [];
156
+ for (let i = 1; i < onsetsSec.length; i += 1)
157
+ ioi.push(onsetsSec[i] - onsetsSec[i - 1]);
158
+ const medianIoi = ioi.length ? median(ioi) : null;
159
+ const detected_bpm = medianIoi ? (60 / medianIoi) : null;
160
+ const avgDev = average(detected_notes
161
+ .map((n) => (n && typeof n.deviation_cents === "number" ? Math.abs(n.deviation_cents) : null))
162
+ .filter((x) => x != null));
163
+ const voices = [
164
+ { id: 1, detected_notes, average_deviation: avgDev },
165
+ { id: 2, detected_notes: [], average_deviation: null },
166
+ { id: 3, detected_notes: [], average_deviation: null },
167
+ ];
168
+ return {
169
+ sidwave: 1.0,
170
+ analysis: {
171
+ source: "microphone",
172
+ durationSeconds: signal.length / sampleRate,
173
+ voices,
174
+ global_metrics: {
175
+ average_pitch_deviation: avgDev,
176
+ detected_bpm: detected_bpm ? round(detected_bpm, 0.1) : null,
177
+ },
178
+ },
179
+ };
180
+ }
181
+ // Test-only helper: bypasses microphone capture and analyzes provided PCM buffer.
182
+ export async function analyzePcmForTest(signal, sampleRate, expectedSidwave) {
183
+ return analyzePcm(signal, sampleRate, expectedSidwave);
184
+ }
185
+ function groupSegments(frames, _sampleRate) {
186
+ const segments = [];
187
+ let cur = null;
188
+ const freqTolCents = 35; // merge if within ~1/3 semitone
189
+ for (const f of frames) {
190
+ if (!cur) {
191
+ cur = { t0: f.t0, t1: f.t1, freq: f.freq };
192
+ continue;
193
+ }
194
+ if (f.freq == null && cur.freq == null) {
195
+ cur.t1 = f.t1;
196
+ continue;
197
+ }
198
+ if (f.freq != null && cur.freq != null) {
199
+ const cents = 1200 * Math.log2(f.freq / cur.freq);
200
+ if (Math.abs(cents) <= freqTolCents) {
201
+ // same note region
202
+ cur.t1 = f.t1;
203
+ // slight running average to stabilise
204
+ cur.freq = 0.9 * cur.freq + 0.1 * f.freq;
205
+ continue;
206
+ }
207
+ }
208
+ segments.push({ t0: cur.t0, t1: cur.t1, duration: cur.t1 - cur.t0, freq: cur.freq });
209
+ cur = { t0: f.t0, t1: f.t1, freq: f.freq };
210
+ }
211
+ if (cur)
212
+ segments.push({ t0: cur.t0, t1: cur.t1, duration: cur.t1 - cur.t0, freq: cur.freq });
213
+ return segments.filter((s) => s.duration > 0.01);
214
+ }
215
+ function midiToName(midi) {
216
+ const names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
217
+ const n = ((midi % 12) + 12) % 12;
218
+ const oct = Math.floor(midi / 12) - 1;
219
+ return `${names[n]}${oct}`;
220
+ }
221
+ function round(n, step = 1) {
222
+ return Math.round(n / step) * step;
223
+ }
224
+ function median(xs) {
225
+ const a = xs.slice().sort((x, y) => x - y);
226
+ const mid = Math.floor(a.length / 2);
227
+ return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) / 2;
228
+ }
229
+ function average(xs) {
230
+ if (!xs.length)
231
+ return null;
232
+ const s = xs.reduce((acc, v) => acc + v, 0);
233
+ return s / xs.length;
234
+ }
235
+ function extractExpectedMidiSet(expected) {
236
+ if (!expected)
237
+ return null;
238
+ const set = new Set();
239
+ for (const v of expected.voices ?? []) {
240
+ const p = v.patterns ?? {};
241
+ for (const name of Object.keys(p)) {
242
+ const pat = p[name];
243
+ const notes = Array.isArray(pat?.notes) ? pat.notes : Array.isArray(pat?.groove) ? pat.groove : undefined;
244
+ if (!notes)
245
+ continue;
246
+ for (const n of notes) {
247
+ const m = noteNameToMidiSafe(n);
248
+ if (typeof m === "number")
249
+ set.add(m);
250
+ }
251
+ }
252
+ }
253
+ return Array.from(set.values()).sort((a, b) => a - b);
254
+ }
255
+ function noteNameToMidiSafe(note) {
256
+ try {
257
+ // noteNameToMidi is imported indirectly via sidwave types; fallback if missing
258
+ const m = /^([A-Ga-g])([#b]?)(-?\d+)$/.exec((note || "").trim());
259
+ if (!m)
260
+ return undefined;
261
+ const letter = m[1].toUpperCase();
262
+ const accidental = m[2];
263
+ const octave = Number(m[3]);
264
+ const base = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
265
+ let semi = base[letter] ?? 0;
266
+ if (accidental === "#")
267
+ semi += 1;
268
+ if (accidental === "b")
269
+ semi -= 1;
270
+ const midi = (octave + 1) * 12 + semi;
271
+ return midi;
272
+ }
273
+ catch {
274
+ return undefined;
275
+ }
276
+ }
277
+ function computeRms(win, Meyda, sampleRate, bufferSize) {
278
+ try {
279
+ if (Meyda && typeof Meyda.extract === "function") {
280
+ const features = Meyda.extract(["rms"], win, { sampleRate, bufferSize });
281
+ const val = Number(features?.rms ?? 0);
282
+ if (Number.isFinite(val))
283
+ return val;
284
+ }
285
+ }
286
+ catch { }
287
+ // fallback
288
+ let sum = 0;
289
+ for (let i = 0; i < win.length; i += 1)
290
+ sum += win[i] * win[i];
291
+ return Math.sqrt(sum / Math.max(1, win.length));
292
+ }
@@ -0,0 +1,193 @@
1
+ /*
2
+ C64 Bridge - An MCP Server for the Commodore 64 Ultimate
3
+ Copyright (C) 2025 Christian Gleissner
4
+
5
+ Licensed under the GNU General Public License v2.0 or later.
6
+ See <https://www.gnu.org/licenses/> for details.
7
+ */
8
+ import { Buffer } from "node:buffer";
9
+ import { asciiToPetscii, encodeStringWithNames } from "./petscii.js";
10
+ const DEFAULT_START_ADDRESS = 0x0801;
11
+ const REM_TOKEN = 0x8f;
12
+ const TOKEN_TABLE = buildTokenTable();
13
+ export function basicToPrg(source, options) {
14
+ const startAddress = options?.startAddress ?? DEFAULT_START_ADDRESS;
15
+ const normalizedSource = normalize(source);
16
+ const lines = normalizedSource.split("\n");
17
+ const lineBuffers = [];
18
+ let currentAddress = startAddress;
19
+ for (const rawLine of lines) {
20
+ const trimmedRight = rawLine.replace(/\s+$/u, "");
21
+ if (trimmedRight === "") {
22
+ continue;
23
+ }
24
+ const match = /^\s*(\d+)\s*(.*)$/u.exec(trimmedRight);
25
+ if (!match) {
26
+ throw new Error(`Expected line number at: "${rawLine}"`);
27
+ }
28
+ const lineNumber = Number.parseInt(match[1], 10);
29
+ if (!Number.isInteger(lineNumber) || lineNumber < 0 || lineNumber > 65535) {
30
+ throw new Error(`Line number out of range: ${match[1]}`);
31
+ }
32
+ const content = match[2] ?? "";
33
+ const tokenised = tokenize(content);
34
+ const lineLength = 2 + 2 + tokenised.length + 1;
35
+ const nextAddress = currentAddress + lineLength;
36
+ const lineBuffer = Buffer.alloc(lineLength);
37
+ lineBuffer.writeUInt16LE(nextAddress, 0);
38
+ lineBuffer.writeUInt16LE(lineNumber, 2);
39
+ Buffer.from(tokenised).copy(lineBuffer, 4);
40
+ lineBuffer[4 + tokenised.length] = 0x00;
41
+ lineBuffers.push(lineBuffer);
42
+ currentAddress = nextAddress;
43
+ }
44
+ const header = Buffer.alloc(2);
45
+ header.writeUInt16LE(startAddress, 0);
46
+ return Buffer.concat([header, ...lineBuffers, Buffer.from([0x00, 0x00])]);
47
+ }
48
+ function normalize(input) {
49
+ const unified = input.replace(/\r\n?/g, "\n");
50
+ const withoutTrailingNewlines = unified.replace(/\n+$/u, "");
51
+ return `${withoutTrailingNewlines}\n`;
52
+ }
53
+ function tokenize(content) {
54
+ const upper = content.toUpperCase();
55
+ const bytes = [];
56
+ let index = 0;
57
+ let inString = false;
58
+ let inRemark = false;
59
+ while (index < content.length) {
60
+ if (!inString && !inRemark) {
61
+ const token = matchToken(upper, index);
62
+ if (token) {
63
+ bytes.push(token.byte);
64
+ index += token.text.length;
65
+ if (token.byte === REM_TOKEN) {
66
+ inRemark = true;
67
+ }
68
+ continue;
69
+ }
70
+ }
71
+ const char = content[index];
72
+ // Special handling for string literals: encode the inner text in one go
73
+ if (!inRemark && char === '"') {
74
+ bytes.push(asciiToPetscii('"'));
75
+ inString = !inString;
76
+ index += 1;
77
+ if (inString) {
78
+ // capture until next quote or end of line
79
+ const start = index;
80
+ let end = start;
81
+ while (end < content.length && content[end] !== '"') {
82
+ end += 1;
83
+ }
84
+ const segment = content.slice(start, end);
85
+ const encoded = encodeStringWithNames(segment);
86
+ for (const b of encoded)
87
+ bytes.push(b);
88
+ index = end; // leave closing quote to be processed next iteration
89
+ }
90
+ continue;
91
+ }
92
+ const plainChar = (!inString && !inRemark ? upper[index] : char) ?? char;
93
+ bytes.push(asciiToPetscii(plainChar));
94
+ index += 1;
95
+ }
96
+ if (bytes.length === 0) {
97
+ bytes.push(0x20);
98
+ }
99
+ return Uint8Array.from(bytes);
100
+ }
101
+ function matchToken(upperSource, startIndex) {
102
+ for (const entry of TOKEN_TABLE) {
103
+ if (upperSource.startsWith(entry.text, startIndex)) {
104
+ return entry;
105
+ }
106
+ }
107
+ return undefined;
108
+ }
109
+ // asciiToPetscii is imported from petscii.ts
110
+ function buildTokenTable() {
111
+ const baseTokens = [
112
+ ["END", 0x80],
113
+ ["FOR", 0x81],
114
+ ["NEXT", 0x82],
115
+ ["DATA", 0x83],
116
+ ["INPUT#", 0x84],
117
+ ["INPUT", 0x85],
118
+ ["DIM", 0x86],
119
+ ["READ", 0x87],
120
+ ["LET", 0x88],
121
+ ["GOTO", 0x89],
122
+ ["RUN", 0x8a],
123
+ ["IF", 0x8b],
124
+ ["RESTORE", 0x8c],
125
+ ["GOSUB", 0x8d],
126
+ ["RETURN", 0x8e],
127
+ ["REM", REM_TOKEN],
128
+ ["STOP", 0x90],
129
+ ["ON", 0x91],
130
+ ["WAIT", 0x92],
131
+ ["LOAD", 0x93],
132
+ ["SAVE", 0x94],
133
+ ["VERIFY", 0x95],
134
+ ["DEF", 0x96],
135
+ ["POKE", 0x97],
136
+ ["PRINT#", 0x98],
137
+ ["PRINT", 0x99],
138
+ ["CONT", 0x9a],
139
+ ["LIST", 0x9b],
140
+ ["CLR", 0x9c],
141
+ ["CMD", 0x9d],
142
+ ["SYS", 0x9e],
143
+ ["OPEN", 0x9f],
144
+ ["CLOSE", 0xa0],
145
+ ["GET", 0xa1],
146
+ ["NEW", 0xa2],
147
+ ["TAB(", 0xa3],
148
+ ["TO", 0xa4],
149
+ ["FN", 0xa5],
150
+ ["SPC(", 0xa6],
151
+ ["THEN", 0xa7],
152
+ ["NOT", 0xa8],
153
+ ["STEP", 0xa9],
154
+ ["+", 0xaa],
155
+ ["-", 0xab],
156
+ ["*", 0xac],
157
+ ["/", 0xad],
158
+ ["^", 0xae],
159
+ ["AND", 0xaf],
160
+ ["OR", 0xb0],
161
+ [">", 0xb1],
162
+ ["=", 0xb2],
163
+ ["<", 0xb3],
164
+ ["SGN", 0xb4],
165
+ ["INT", 0xb5],
166
+ ["ABS", 0xb6],
167
+ ["USR", 0xb7],
168
+ ["FRE", 0xb8],
169
+ ["POS", 0xb9],
170
+ ["SQR", 0xba],
171
+ ["RND", 0xbb],
172
+ ["LOG", 0xbc],
173
+ ["EXP", 0xbd],
174
+ ["COS", 0xbe],
175
+ ["SIN", 0xbf],
176
+ ["TAN", 0xc0],
177
+ ["ATN", 0xc1],
178
+ ["PEEK", 0xc2],
179
+ ["LEN", 0xc3],
180
+ ["STR$", 0xc4],
181
+ ["VAL", 0xc5],
182
+ ["ASC", 0xc6],
183
+ ["CHR$", 0xc7],
184
+ ["LEFT$", 0xc8],
185
+ ["RIGHT$", 0xc9],
186
+ ["MID$", 0xca],
187
+ ["GO", 0xcb],
188
+ ];
189
+ return baseTokens
190
+ .slice()
191
+ .sort((a, b) => b[0].length - a[0].length)
192
+ .map(([text, byte]) => ({ text, byte }));
193
+ }
@@ -0,0 +1,32 @@
1
+ import { Console } from "node:console";
2
+ const METHODS = [
3
+ "log",
4
+ "info",
5
+ "debug",
6
+ "warn",
7
+ "error",
8
+ "dir",
9
+ "trace",
10
+ "time",
11
+ "timeEnd",
12
+ "timeLog",
13
+ "table",
14
+ "group",
15
+ "groupCollapsed",
16
+ "groupEnd",
17
+ "count",
18
+ "countReset",
19
+ "assert",
20
+ "profile",
21
+ "profileEnd",
22
+ ];
23
+ const stderrConsole = new Console({ stdout: process.stderr, stderr: process.stderr });
24
+ const consoleMethods = console;
25
+ for (const method of METHODS) {
26
+ const implementation = stderrConsole[method];
27
+ if (typeof implementation === "function") {
28
+ consoleMethods[method] = (...args) => {
29
+ return implementation.apply(stderrConsole, args);
30
+ };
31
+ }
32
+ }