c64bridge 0.5.1 → 0.6.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/README.md +17 -5
- package/data/assembly/assembly-spec.md +24 -0
- package/data/assembly/examples/video/hello_world.asm +39 -0
- package/data/basic/basic-pitfalls.md +236 -0
- package/data/basic/basic-spec.md +507 -198
- package/data/video/petscii-style-guide.md +105 -0
- package/data/video/sprite-charset-best-practices.md +292 -0
- package/dist/c64Client.js +1 -0
- package/dist/prompts/registry.js +10 -2
- package/dist/rag/knowledgeIndex.js +111 -24
- package/dist/rag/retriever.js +110 -4
- package/dist/tools/audio.js +26 -13
- package/dist/tools/developer.js +34 -0
- package/dist/tools/graphics.js +6 -5
- package/dist/tools/memory.js +1 -1
- package/dist/tools/pollValidator.js +256 -0
- package/dist/tools/programRunners.js +352 -12
- package/dist/tools/rag.js +1 -7
- package/doc/MCP_SETUP.md +7 -1
- package/doc/bun/baseline-metrics.md +16 -0
- package/doc/bun/migration-checklist.md +55 -0
- package/doc/bun/migration.md +83 -0
- package/doc/developer.md +72 -10
- package/doc/improve-knowledge/notes.md +29 -0
- package/doc/improve-knowledge/plan.md +28 -3
- package/doc/improve-knowledge/rollout.md +39 -5
- package/doc/meta-tools/README.md +101 -0
- package/doc/meta-tools/analysis.md +255 -0
- package/doc/{meta-tools.md → meta-tools/catalog.md} +2 -2
- package/doc/meta-tools/notes.md +174 -0
- package/doc/meta-tools/rollout.md +343 -0
- package/mcp.json +1 -1
- package/package.json +17 -10
- package/scripts/c64-cli.mjs +42 -7
- package/scripts/c64-tool.sh +7 -1
- package/scripts/check-node-compat.mjs +107 -0
- package/scripts/invoke-bun.mjs +58 -0
- package/scripts/rag-rebuild.mjs +11 -13
- package/scripts/run-mcp-check.sh +10 -2
- package/scripts/run-tests.mjs +3 -3
- package/scripts/run-tests.ts +187 -0
- package/scripts/start.mjs +3 -26
- package/scripts/update-readme.ts +17 -1
- package/scripts/verify-package.mjs +0 -1
- package/data/assembly/examples/effects/sinewave.asm +0 -5
- package/doc/meta-tools-rollout.md +0 -211
- package/scripts/register-ts-node.mjs +0 -6
- /package/data/assembly/examples/{graphics/sprite/mover.asm → video/sprite_mover.asm} +0 -0
package/dist/rag/retriever.js
CHANGED
|
@@ -3,9 +3,24 @@ C64 Bridge - Local RAG Retriever
|
|
|
3
3
|
GPL-2.0-only
|
|
4
4
|
*/
|
|
5
5
|
import { cosineSimilarity } from "./embeddings.js";
|
|
6
|
+
import { listKnowledgeResources } from "./knowledgeIndex.js";
|
|
6
7
|
const BASIC_SIGNAL_RE = /(^\s*\d{1,5}\s)|\b(PRINT|POKE|GOTO|GOSUB|RESTORE|READ|DATA|INPUT|CHR\$|TI\$|TAB\()/im;
|
|
7
8
|
const ASM_SIGNAL_RE = /(^\s*(?:[A-Z_][\w]*:)?\s*(?:\.?[A-Z]{2,4})\b)|\$[0-9A-F]{2,4}/im;
|
|
8
9
|
const PROVENANCE_COMMENT_RE = /^\s*<!--\s*Source:\s*(.*?)\s*-->\s*/i;
|
|
10
|
+
// Build a mapping from file paths to resource URIs
|
|
11
|
+
function buildPathToUriMap() {
|
|
12
|
+
const resources = listKnowledgeResources();
|
|
13
|
+
const map = new Map();
|
|
14
|
+
for (const resource of resources) {
|
|
15
|
+
if (resource.relativePath) {
|
|
16
|
+
// Normalize the path for matching
|
|
17
|
+
const normalizedPath = resource.relativePath.replace(/\\/g, "/");
|
|
18
|
+
map.set(normalizedPath, resource.uri);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return map;
|
|
22
|
+
}
|
|
23
|
+
const PATH_TO_URI_MAP = buildPathToUriMap();
|
|
9
24
|
export class LocalRagRetriever {
|
|
10
25
|
constructor(model, opts) {
|
|
11
26
|
this.model = model;
|
|
@@ -78,7 +93,9 @@ export class LocalRagRetriever {
|
|
|
78
93
|
if (!normalized || normalized === "other")
|
|
79
94
|
push(this.other);
|
|
80
95
|
candidates.sort((a, b) => b.score - a.score);
|
|
81
|
-
|
|
96
|
+
// Apply duplicate suppression and diversity
|
|
97
|
+
const deduplicated = deduplicateAndDiversify(candidates, topK);
|
|
98
|
+
return deduplicated.map((candidate) => ({
|
|
82
99
|
snippet: candidate.snippet,
|
|
83
100
|
origin: candidate.origin,
|
|
84
101
|
uri: candidate.uri,
|
|
@@ -86,14 +103,82 @@ export class LocalRagRetriever {
|
|
|
86
103
|
sourcePath: candidate.record.sourcePath,
|
|
87
104
|
sourceUrl: candidate.record.sourceUrl,
|
|
88
105
|
sourceRepoUrl: candidate.record.sourceRepoUrl,
|
|
89
|
-
license: candidate.record.license,
|
|
90
106
|
licenseSpdxId: candidate.record.licenseSpdxId,
|
|
91
|
-
licenseName: candidate.record.licenseName,
|
|
92
|
-
licenseUrl: candidate.record.licenseUrl,
|
|
93
107
|
attribution: candidate.record.attribution,
|
|
94
108
|
}));
|
|
95
109
|
}
|
|
96
110
|
}
|
|
111
|
+
function deduplicateAndDiversify(candidates, topK) {
|
|
112
|
+
// Step 1: Remove near-duplicates based on snippet similarity
|
|
113
|
+
const results = [];
|
|
114
|
+
const seenSnippets = new Set();
|
|
115
|
+
for (const candidate of candidates) {
|
|
116
|
+
// Normalize snippet for comparison (lowercase, trim whitespace)
|
|
117
|
+
const normalizedSnippet = candidate.snippet
|
|
118
|
+
.toLowerCase()
|
|
119
|
+
.replace(/\s+/g, " ")
|
|
120
|
+
.trim()
|
|
121
|
+
.slice(0, 100); // Compare first 100 chars
|
|
122
|
+
// Skip if we've seen this snippet before
|
|
123
|
+
if (seenSnippets.has(normalizedSnippet)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Check for high similarity with existing results
|
|
127
|
+
let isDuplicate = false;
|
|
128
|
+
for (const existing of results) {
|
|
129
|
+
const existingNormalized = existing.snippet
|
|
130
|
+
.toLowerCase()
|
|
131
|
+
.replace(/\s+/g, " ")
|
|
132
|
+
.trim()
|
|
133
|
+
.slice(0, 100);
|
|
134
|
+
// Simple Jaccard similarity on words
|
|
135
|
+
const words1 = new Set(normalizedSnippet.split(/\s+/));
|
|
136
|
+
const words2 = new Set(existingNormalized.split(/\s+/));
|
|
137
|
+
const intersection = new Set([...words1].filter((w) => words2.has(w)));
|
|
138
|
+
const union = new Set([...words1, ...words2]);
|
|
139
|
+
const similarity = intersection.size / union.size;
|
|
140
|
+
if (similarity > 0.85) {
|
|
141
|
+
isDuplicate = true;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!isDuplicate) {
|
|
146
|
+
results.push(candidate);
|
|
147
|
+
seenSnippets.add(normalizedSnippet);
|
|
148
|
+
}
|
|
149
|
+
// Stop if we have enough results
|
|
150
|
+
if (results.length >= topK) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Step 2: Ensure diversity in origins/resources
|
|
155
|
+
// If we have fewer results than topK, try to add more from different origins
|
|
156
|
+
if (results.length < topK && results.length < candidates.length) {
|
|
157
|
+
const usedOrigins = new Set(results.map((r) => r.origin || r.uri || "unknown"));
|
|
158
|
+
const remaining = candidates.filter((c) => !results.includes(c));
|
|
159
|
+
for (const candidate of remaining) {
|
|
160
|
+
const candidateOrigin = candidate.origin || candidate.uri || "unknown";
|
|
161
|
+
// Prefer candidates from different origins
|
|
162
|
+
if (!usedOrigins.has(candidateOrigin)) {
|
|
163
|
+
results.push(candidate);
|
|
164
|
+
usedOrigins.add(candidateOrigin);
|
|
165
|
+
if (results.length >= topK) {
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Fill remaining slots if still under topK
|
|
171
|
+
for (const candidate of remaining) {
|
|
172
|
+
if (!results.includes(candidate)) {
|
|
173
|
+
results.push(candidate);
|
|
174
|
+
if (results.length >= topK) {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return results.slice(0, topK);
|
|
181
|
+
}
|
|
97
182
|
function extractSnippetAndOrigin(text) {
|
|
98
183
|
const match = PROVENANCE_COMMENT_RE.exec(text);
|
|
99
184
|
if (!match) {
|
|
@@ -107,9 +192,30 @@ function extractSnippetAndOrigin(text) {
|
|
|
107
192
|
};
|
|
108
193
|
}
|
|
109
194
|
function deriveUri(record, origin) {
|
|
195
|
+
// If origin is already a c64:// URI, use it
|
|
110
196
|
if (origin && origin.startsWith("c64://")) {
|
|
111
197
|
return origin;
|
|
112
198
|
}
|
|
199
|
+
// Try to map the file path from origin to a resource URI
|
|
200
|
+
if (origin) {
|
|
201
|
+
// Extract the file path (before any '#' anchor)
|
|
202
|
+
const filePath = origin.split("#")[0];
|
|
203
|
+
const resourceUri = PATH_TO_URI_MAP.get(filePath);
|
|
204
|
+
if (resourceUri) {
|
|
205
|
+
// Preserve any anchor/section from the original origin
|
|
206
|
+
const anchor = origin.includes("#") ? "#" + origin.split("#")[1] : "";
|
|
207
|
+
return resourceUri + anchor;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Try to map the source path from the record
|
|
211
|
+
if (record.sourcePath) {
|
|
212
|
+
const normalizedPath = record.sourcePath.replace(/\\/g, "/");
|
|
213
|
+
const resourceUri = PATH_TO_URI_MAP.get(normalizedPath);
|
|
214
|
+
if (resourceUri) {
|
|
215
|
+
return resourceUri;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Fallback to source URLs
|
|
113
219
|
if (record.sourceUrl) {
|
|
114
220
|
return record.sourceUrl;
|
|
115
221
|
}
|
package/dist/tools/audio.js
CHANGED
|
@@ -191,8 +191,12 @@ const musicGenerateArgsSchema = objectSchema({
|
|
|
191
191
|
waveform: stringSchema({
|
|
192
192
|
description: "Waveform for playback.",
|
|
193
193
|
enum: ["pulse", "saw", "tri", "noise"],
|
|
194
|
-
default: "
|
|
194
|
+
default: "tri",
|
|
195
195
|
}),
|
|
196
|
+
preset: optionalSchema(stringSchema({
|
|
197
|
+
description: "Optional musical preset to influence timing/expression.",
|
|
198
|
+
enum: ["classic", "expression"],
|
|
199
|
+
}), "classic"),
|
|
196
200
|
},
|
|
197
201
|
additionalProperties: false,
|
|
198
202
|
});
|
|
@@ -474,7 +478,7 @@ export const audioModule = defineToolModule({
|
|
|
474
478
|
description: "Trigger a SID voice with configurable waveform, pulse width, and ADSR envelope. See c64://specs/sid.",
|
|
475
479
|
summary: "Resolves note or frequency, clamps parameters, and writes the SID voice registers.",
|
|
476
480
|
inputSchema: sidNoteOnArgsSchema.jsonSchema,
|
|
477
|
-
tags: ["sid", "control", "music"],
|
|
481
|
+
tags: ["sid", "control", "music", "pal-ntsc"],
|
|
478
482
|
prerequisites: ["sid_volume"],
|
|
479
483
|
examples: [
|
|
480
484
|
{ name: "Note on", description: "Voice 1 C4 triangle", arguments: { voice: 1, note: "C4", waveform: "tri" } },
|
|
@@ -511,12 +515,16 @@ export const audioModule = defineToolModule({
|
|
|
511
515
|
});
|
|
512
516
|
}
|
|
513
517
|
const detailRecord = toRecord(result.details) ?? {};
|
|
514
|
-
|
|
518
|
+
const systemNote = parsed.system
|
|
519
|
+
? `Using ${parsed.system} timing.`
|
|
520
|
+
: "Using default PAL timing. For NTSC, pass system: 'NTSC'. Check $02A6 memory location to verify system.";
|
|
521
|
+
return textResult(`Started SID voice ${voice}${parsed.note ? ` with note ${parsed.note.toUpperCase()}` : ""}. ${systemNote}`, {
|
|
515
522
|
success: true,
|
|
516
523
|
voice,
|
|
517
524
|
note: parsed.note ?? null,
|
|
518
525
|
frequencyHz: parsed.frequencyHz ?? null,
|
|
519
526
|
system: parsed.system ?? "PAL",
|
|
527
|
+
systemReminder: "$02A6 contains PAL/NTSC flag (1=NTSC, 0=PAL)",
|
|
520
528
|
waveform: parsed.waveform ?? "pulse",
|
|
521
529
|
pulseWidth: parsed.pulseWidth,
|
|
522
530
|
envelope: {
|
|
@@ -699,10 +707,11 @@ export const audioModule = defineToolModule({
|
|
|
699
707
|
inputSchema: musicGenerateArgsSchema.jsonSchema,
|
|
700
708
|
relatedResources: ["c64://specs/sid"],
|
|
701
709
|
relatedPrompts: ["sid-music"],
|
|
702
|
-
tags: ["sid", "music", "generator"],
|
|
710
|
+
tags: ["sid", "music", "generator", "pal-ntsc"],
|
|
703
711
|
prerequisites: ["sid_volume"],
|
|
704
712
|
examples: [
|
|
705
|
-
{ name: "C major", description: "C4 arpeggio", arguments: { root: "C4", pattern: "0,4,7", steps: 8, tempoMs: 120, waveform: "
|
|
713
|
+
{ name: "C major", description: "C4 arpeggio (triangle)", arguments: { root: "C4", pattern: "0,4,7", steps: 8, tempoMs: 120, waveform: "tri" } },
|
|
714
|
+
{ name: "Expressive C major", description: "C4 arpeggio with phrasing", arguments: { root: "C4", pattern: "0,4,7", steps: 8, preset: "expression" } },
|
|
706
715
|
],
|
|
707
716
|
workflowHints: [
|
|
708
717
|
"Offer as a quick inspiration loop when the user wants to hear something immediately; explain how to tweak pattern or tempo.",
|
|
@@ -713,11 +722,13 @@ export const audioModule = defineToolModule({
|
|
|
713
722
|
const intervals = parseIntervals(parsed.pattern);
|
|
714
723
|
const timeline = [];
|
|
715
724
|
let timestamp = 0;
|
|
725
|
+
const expressiveDurations = [250, 180, 180, 400];
|
|
716
726
|
for (let i = 0; i < parsed.steps; i += 1) {
|
|
717
727
|
const iv = intervals[i % intervals.length];
|
|
718
728
|
const note = transposeNote(parsed.root, iv);
|
|
719
|
-
|
|
720
|
-
timestamp
|
|
729
|
+
const durationMs = parsed.preset === "expression" ? expressiveDurations[i % expressiveDurations.length] : parsed.tempoMs;
|
|
730
|
+
timeline.push({ t: timestamp, note, durationMs });
|
|
731
|
+
timestamp += durationMs;
|
|
721
732
|
}
|
|
722
733
|
ctx.logger.info("Scheduling SID arpeggio", {
|
|
723
734
|
root: parsed.root,
|
|
@@ -735,14 +746,15 @@ export const audioModule = defineToolModule({
|
|
|
735
746
|
await ctx.client.sidNoteOn({
|
|
736
747
|
voice: 1,
|
|
737
748
|
note,
|
|
738
|
-
waveform: parsed.waveform,
|
|
749
|
+
waveform: (parsed.waveform ?? "tri"),
|
|
739
750
|
pulseWidth: 0x0800,
|
|
740
751
|
attack: 1,
|
|
741
|
-
decay:
|
|
742
|
-
sustain:
|
|
743
|
-
release:
|
|
752
|
+
decay: 7,
|
|
753
|
+
sustain: 15,
|
|
754
|
+
release: 0,
|
|
744
755
|
});
|
|
745
|
-
|
|
756
|
+
const durationMs = parsed.preset === "expression" ? expressiveDurations[i % expressiveDurations.length] : parsed.tempoMs;
|
|
757
|
+
await sleep(durationMs);
|
|
746
758
|
}
|
|
747
759
|
await ctx.client.sidNoteOff(1);
|
|
748
760
|
}
|
|
@@ -757,7 +769,8 @@ export const audioModule = defineToolModule({
|
|
|
757
769
|
intervals,
|
|
758
770
|
steps: parsed.steps,
|
|
759
771
|
tempoMs: parsed.tempoMs,
|
|
760
|
-
waveform: parsed.waveform,
|
|
772
|
+
waveform: parsed.waveform ?? "tri",
|
|
773
|
+
preset: parsed.preset ?? "classic",
|
|
761
774
|
timeline,
|
|
762
775
|
});
|
|
763
776
|
}
|
package/dist/tools/developer.js
CHANGED
|
@@ -140,6 +140,9 @@ export const developerModule = defineToolModule({
|
|
|
140
140
|
inputSchema: noArgsSchema.jsonSchema,
|
|
141
141
|
relatedResources: ["c64://context/bootstrap"],
|
|
142
142
|
tags: ["config", "list"],
|
|
143
|
+
examples: [
|
|
144
|
+
{ name: "List configs", description: "Enumerate configuration categories", arguments: {} },
|
|
145
|
+
],
|
|
143
146
|
workflowHints: [
|
|
144
147
|
"Run first to enumerate categories before reading or writing specific settings; summarise the top-level sections for the user.",
|
|
145
148
|
],
|
|
@@ -171,6 +174,10 @@ export const developerModule = defineToolModule({
|
|
|
171
174
|
inputSchema: configGetArgsSchema.jsonSchema,
|
|
172
175
|
relatedResources: ["c64://context/bootstrap"],
|
|
173
176
|
tags: ["config", "read"],
|
|
177
|
+
examples: [
|
|
178
|
+
{ name: "Get audio config", description: "Read audio category", arguments: { category: "Audio" } },
|
|
179
|
+
{ name: "Get volume", description: "Read specific setting", arguments: { category: "Audio", item: "Volume" } },
|
|
180
|
+
],
|
|
174
181
|
workflowHints: [
|
|
175
182
|
"Use after config_list to fetch details; restate category/item so the user can cross-check values before changes.",
|
|
176
183
|
],
|
|
@@ -203,6 +210,9 @@ export const developerModule = defineToolModule({
|
|
|
203
210
|
inputSchema: configSetArgsSchema.jsonSchema,
|
|
204
211
|
relatedResources: ["c64://context/bootstrap"],
|
|
205
212
|
tags: ["config", "write"],
|
|
213
|
+
examples: [
|
|
214
|
+
{ name: "Set volume", description: "Adjust audio volume", arguments: { category: "Audio", item: "Volume", value: 75 } },
|
|
215
|
+
],
|
|
206
216
|
workflowHints: [
|
|
207
217
|
"Confirm the user-supplied value before writing; mention whether a reboot is required for the change to take effect.",
|
|
208
218
|
],
|
|
@@ -243,6 +253,9 @@ export const developerModule = defineToolModule({
|
|
|
243
253
|
inputSchema: configBatchSchema.jsonSchema,
|
|
244
254
|
relatedResources: ["c64://context/bootstrap"],
|
|
245
255
|
tags: ["config", "write"],
|
|
256
|
+
examples: [
|
|
257
|
+
{ name: "Batch update", description: "Update multiple settings", arguments: { Audio: { Volume: 80, DACMode: "stereo" } } },
|
|
258
|
+
],
|
|
246
259
|
workflowHints: [
|
|
247
260
|
"Use when multiple categories must be updated together; remind the user to review validation errors closely.",
|
|
248
261
|
],
|
|
@@ -283,6 +296,9 @@ export const developerModule = defineToolModule({
|
|
|
283
296
|
inputSchema: noArgsSchema.jsonSchema,
|
|
284
297
|
relatedResources: ["c64://context/bootstrap"],
|
|
285
298
|
tags: ["config", "flash"],
|
|
299
|
+
examples: [
|
|
300
|
+
{ name: "Load from flash", description: "Restore saved configuration", arguments: {} },
|
|
301
|
+
],
|
|
286
302
|
workflowHints: [
|
|
287
303
|
"Use when the user wants to revert to saved flash configuration; warn that unsaved RAM changes are discarded.",
|
|
288
304
|
],
|
|
@@ -316,6 +332,9 @@ export const developerModule = defineToolModule({
|
|
|
316
332
|
inputSchema: noArgsSchema.jsonSchema,
|
|
317
333
|
relatedResources: ["c64://context/bootstrap"],
|
|
318
334
|
tags: ["config", "flash"],
|
|
335
|
+
examples: [
|
|
336
|
+
{ name: "Save to flash", description: "Persist current config", arguments: {} },
|
|
337
|
+
],
|
|
319
338
|
workflowHints: [
|
|
320
339
|
"Run after configuration changes the user wants to persist; mention that flash writes take a moment.",
|
|
321
340
|
],
|
|
@@ -349,6 +368,9 @@ export const developerModule = defineToolModule({
|
|
|
349
368
|
inputSchema: noArgsSchema.jsonSchema,
|
|
350
369
|
relatedResources: ["c64://context/bootstrap"],
|
|
351
370
|
tags: ["config", "reset"],
|
|
371
|
+
examples: [
|
|
372
|
+
{ name: "Reset config", description: "Restore factory defaults", arguments: {} },
|
|
373
|
+
],
|
|
352
374
|
workflowHints: [
|
|
353
375
|
"Confirm with the user before wiping settings; note that the firmware must restart to apply defaults fully.",
|
|
354
376
|
],
|
|
@@ -382,6 +404,9 @@ export const developerModule = defineToolModule({
|
|
|
382
404
|
inputSchema: noArgsSchema.jsonSchema,
|
|
383
405
|
relatedResources: ["c64://context/bootstrap"],
|
|
384
406
|
tags: ["diagnostics", "version"],
|
|
407
|
+
examples: [
|
|
408
|
+
{ name: "Get version", description: "Check firmware version", arguments: {} },
|
|
409
|
+
],
|
|
385
410
|
workflowHints: [
|
|
386
411
|
"Run when the user wonders which firmware build is active; include API and FPGA versions if available.",
|
|
387
412
|
],
|
|
@@ -410,6 +435,9 @@ export const developerModule = defineToolModule({
|
|
|
410
435
|
inputSchema: noArgsSchema.jsonSchema,
|
|
411
436
|
relatedResources: ["c64://context/bootstrap"],
|
|
412
437
|
tags: ["diagnostics", "info"],
|
|
438
|
+
examples: [
|
|
439
|
+
{ name: "Get hardware info", description: "Query hardware status", arguments: {} },
|
|
440
|
+
],
|
|
413
441
|
workflowHints: [
|
|
414
442
|
"Use to answer hardware capability questions; highlight serials, board revisions, or temperature if present.",
|
|
415
443
|
],
|
|
@@ -438,6 +466,9 @@ export const developerModule = defineToolModule({
|
|
|
438
466
|
inputSchema: noArgsSchema.jsonSchema,
|
|
439
467
|
relatedResources: ["c64://context/bootstrap"],
|
|
440
468
|
tags: ["debug"],
|
|
469
|
+
examples: [
|
|
470
|
+
{ name: "Read debug register", description: "Check $D7FF value", arguments: {} },
|
|
471
|
+
],
|
|
441
472
|
workflowHints: [
|
|
442
473
|
"Read the register when debugging DMA or cartridge interactions; translate the hex value into context for the user.",
|
|
443
474
|
],
|
|
@@ -474,6 +505,9 @@ export const developerModule = defineToolModule({
|
|
|
474
505
|
inputSchema: debugWriteArgsSchema.jsonSchema,
|
|
475
506
|
relatedResources: ["c64://context/bootstrap"],
|
|
476
507
|
tags: ["debug"],
|
|
508
|
+
examples: [
|
|
509
|
+
{ name: "Write debug register", description: "Set $D7FF to $42", arguments: { value: "42" } },
|
|
510
|
+
],
|
|
477
511
|
workflowHints: [
|
|
478
512
|
"Confirm with the user before writing arbitrary values; mention side effects on cartridge or DMA behaviour.",
|
|
479
513
|
],
|
package/dist/tools/graphics.js
CHANGED
|
@@ -273,7 +273,7 @@ export const graphicsModule = defineToolModule({
|
|
|
273
273
|
inputSchema: spriteArgsSchema.jsonSchema,
|
|
274
274
|
relatedResources: ["c64://specs/vic"],
|
|
275
275
|
relatedPrompts: ["graphics-demo", "assembly-program"],
|
|
276
|
-
tags: ["sprite", "assembly"],
|
|
276
|
+
tags: ["sprite", "assembly", "pal-ntsc"],
|
|
277
277
|
prerequisites: ["upload_and_run_asm"],
|
|
278
278
|
examples: [
|
|
279
279
|
{
|
|
@@ -380,12 +380,12 @@ export const graphicsModule = defineToolModule({
|
|
|
380
380
|
},
|
|
381
381
|
{
|
|
382
382
|
name: "create_petscii_image",
|
|
383
|
-
description: "Create PETSCII art from prompts or text, optionally run it on the C64, and return metadata. See c64://specs/basic and c64://specs/
|
|
384
|
-
summary: "Synthesises PETSCII art, generates a BASIC program, and uploads it unless dry-run is requested.",
|
|
383
|
+
description: "Create PETSCII art from prompts or text, optionally run it on the C64, and return metadata including PETSCII codes and glyphs. See c64://specs/basic, c64://specs/vic, and c64://specs/charset.",
|
|
384
|
+
summary: "Synthesises PETSCII art, generates a BASIC program with preview metadata (petsciiCodes, glyphs, dimensions), and uploads it unless dry-run is requested.",
|
|
385
385
|
inputSchema: petsciiImageArgsSchema.jsonSchema,
|
|
386
|
-
relatedResources: ["c64://specs/basic", "c64://specs/vic"],
|
|
386
|
+
relatedResources: ["c64://specs/basic", "c64://specs/vic", "c64://specs/charset"],
|
|
387
387
|
relatedPrompts: ["graphics-demo", "basic-program"],
|
|
388
|
-
tags: ["petscii", "basic"],
|
|
388
|
+
tags: ["petscii", "basic", "pal-ntsc"],
|
|
389
389
|
prerequisites: ["upload_and_run_basic"],
|
|
390
390
|
examples: [
|
|
391
391
|
{
|
|
@@ -396,6 +396,7 @@ export const graphicsModule = defineToolModule({
|
|
|
396
396
|
],
|
|
397
397
|
workflowHints: [
|
|
398
398
|
"Trigger when the user provides creative prompts; clarify whether you ran the art or left it as a dry run.",
|
|
399
|
+
"Response includes petsciiCodes array and glyphs for character-level inspection.",
|
|
399
400
|
"Provide follow-up suggestions like saving the PRG or capturing the screen after rendering.",
|
|
400
401
|
],
|
|
401
402
|
async execute(args, ctx) {
|
package/dist/tools/memory.js
CHANGED
|
@@ -79,7 +79,7 @@ export const memoryModule = defineToolModule({
|
|
|
79
79
|
relatedResources: ["c64://context/bootstrap", "c64://specs/basic"],
|
|
80
80
|
relatedPrompts: ["memory-debug", "basic-program", "assembly-program"],
|
|
81
81
|
tags: ["screen", "memory"],
|
|
82
|
-
prerequisites: [
|
|
82
|
+
prerequisites: [],
|
|
83
83
|
examples: [
|
|
84
84
|
{
|
|
85
85
|
name: "Capture screen",
|
|
@@ -0,0 +1,256 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Load poll configuration from environment variables with defaults.
|
|
10
|
+
*/
|
|
11
|
+
export function loadPollConfig() {
|
|
12
|
+
// In test environments with mock target, use shorter timeouts to avoid interfering with test screen queues
|
|
13
|
+
const isTestMode = process.env.C64_TEST_TARGET === "mock" || process.env.NODE_ENV === "test";
|
|
14
|
+
const defaultMaxMs = isTestMode ? 100 : 2000;
|
|
15
|
+
const defaultIntervalMs = isTestMode ? 30 : 200;
|
|
16
|
+
const maxMs = parseInt(process.env.C64BRIDGE_POLL_MAX_MS ?? String(defaultMaxMs), 10);
|
|
17
|
+
const intervalMs = parseInt(process.env.C64BRIDGE_POLL_INTERVAL_MS ?? String(defaultIntervalMs), 10);
|
|
18
|
+
return {
|
|
19
|
+
maxMs: Number.isFinite(maxMs) && maxMs > 0 ? maxMs : defaultMaxMs,
|
|
20
|
+
intervalMs: Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : defaultIntervalMs,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Wait for a specified duration.
|
|
25
|
+
*/
|
|
26
|
+
function delay(ms) {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compute SHA-1 hash of screen content for change detection.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Compute fast 32-bit CRC32 checksum of buffer for change detection.
|
|
34
|
+
*/
|
|
35
|
+
function computeCrc32(buffer) {
|
|
36
|
+
let crc = 0xFFFFFFFF;
|
|
37
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
38
|
+
crc ^= buffer[i];
|
|
39
|
+
for (let j = 0; j < 8; j++) {
|
|
40
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xEDB88320 : 0);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Compare two Uint8Arrays for equality.
|
|
47
|
+
*/
|
|
48
|
+
function buffersEqual(a, b) {
|
|
49
|
+
if (a.length !== b.length)
|
|
50
|
+
return false;
|
|
51
|
+
for (let i = 0; i < a.length; i++) {
|
|
52
|
+
if (a[i] !== b[i])
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Concatenate multiple Uint8Arrays into a single buffer.
|
|
59
|
+
*/
|
|
60
|
+
function concatBuffers(...buffers) {
|
|
61
|
+
const totalLength = buffers.reduce((sum, buf) => sum + buf.length, 0);
|
|
62
|
+
const result = new Uint8Array(totalLength);
|
|
63
|
+
let offset = 0;
|
|
64
|
+
for (const buf of buffers) {
|
|
65
|
+
result.set(buf, offset);
|
|
66
|
+
offset += buf.length;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Extract BASIC error information from screen text.
|
|
72
|
+
* Looks for patterns like "?SYNTAX ERROR" or "SYNTAX ERROR IN 120".
|
|
73
|
+
*/
|
|
74
|
+
function extractBasicError(screen) {
|
|
75
|
+
const upperScreen = screen.toUpperCase();
|
|
76
|
+
// Check if ERROR appears on screen
|
|
77
|
+
if (!upperScreen.includes("ERROR")) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
// Try to match "?ERROR_TYPE ERROR" or "ERROR_TYPE ERROR IN LINE"
|
|
81
|
+
const errorMatch = /\?([A-Z\s]+)\s+ERROR(?:\s+IN\s+(\d+))?/i.exec(screen);
|
|
82
|
+
if (errorMatch) {
|
|
83
|
+
const errorType = errorMatch[1]?.trim().replace(/\s+/g, " ");
|
|
84
|
+
const lineStr = errorMatch[2];
|
|
85
|
+
const line = lineStr ? parseInt(lineStr, 10) : undefined;
|
|
86
|
+
return {
|
|
87
|
+
message: errorType,
|
|
88
|
+
line: Number.isFinite(line) ? line : undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Fallback: just "ERROR" appears
|
|
92
|
+
return { message: "UNKNOWN ERROR" };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Poll for BASIC program outcome by detecting errors on screen.
|
|
96
|
+
* First waits for RUN or ERROR to appear, then continues polling for errors.
|
|
97
|
+
*/
|
|
98
|
+
async function pollBasicOutcome(client, logger, config) {
|
|
99
|
+
const startTime = Date.now();
|
|
100
|
+
let pollCount = 0;
|
|
101
|
+
let runDetected = false;
|
|
102
|
+
logger.debug("Starting BASIC outcome polling", { maxMs: config.maxMs, intervalMs: config.intervalMs });
|
|
103
|
+
while (Date.now() - startTime < config.maxMs) {
|
|
104
|
+
pollCount++;
|
|
105
|
+
try {
|
|
106
|
+
const screen = await client.readScreen();
|
|
107
|
+
const upperScreen = screen.toUpperCase();
|
|
108
|
+
// Check if RUN or ERROR appeared (indicating execution started or failed)
|
|
109
|
+
if (!runDetected && (upperScreen.includes("RUN") || upperScreen.includes("ERROR"))) {
|
|
110
|
+
runDetected = true;
|
|
111
|
+
logger.debug("Program execution detected", { pollCount, elapsed: Date.now() - startTime });
|
|
112
|
+
}
|
|
113
|
+
// Check for errors
|
|
114
|
+
const errorInfo = extractBasicError(screen);
|
|
115
|
+
if (errorInfo) {
|
|
116
|
+
logger.debug("BASIC error detected", { errorInfo, pollCount, elapsed: Date.now() - startTime });
|
|
117
|
+
return {
|
|
118
|
+
status: "error",
|
|
119
|
+
type: "BASIC",
|
|
120
|
+
message: errorInfo.message,
|
|
121
|
+
line: errorInfo.line,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// If we detected RUN and no error yet, continue polling
|
|
125
|
+
if (runDetected) {
|
|
126
|
+
// Continue polling for a bit to catch any delayed errors
|
|
127
|
+
// but if we've been polling for a while without errors, consider it successful
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
logger.debug("Failed to read screen during BASIC polling", { error, pollCount });
|
|
132
|
+
}
|
|
133
|
+
await delay(config.intervalMs);
|
|
134
|
+
}
|
|
135
|
+
logger.debug("BASIC polling completed without errors", { pollCount, elapsed: Date.now() - startTime });
|
|
136
|
+
return {
|
|
137
|
+
status: "ok",
|
|
138
|
+
type: "BASIC",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Poll for Assembly program outcome by detecting hardware and screen activity.
|
|
143
|
+
* Monitors VIC-II, SID, CIA, jiffy clock, screen memory, and low memory (zero page + stack)
|
|
144
|
+
* to determine if program is alive or crashed.
|
|
145
|
+
* First waits for RUN to appear, then monitors for activity across multiple memory regions.
|
|
146
|
+
*/
|
|
147
|
+
async function pollAsmOutcome(client, logger, config) {
|
|
148
|
+
const startTime = Date.now();
|
|
149
|
+
let pollCount = 0;
|
|
150
|
+
let runDetected = false;
|
|
151
|
+
logger.debug("Starting ASM outcome polling with hardware monitoring", { maxMs: config.maxMs, intervalMs: config.intervalMs });
|
|
152
|
+
// Wait for RUN to appear first
|
|
153
|
+
while (Date.now() - startTime < config.maxMs && !runDetected) {
|
|
154
|
+
try {
|
|
155
|
+
const screen = await client.readScreen();
|
|
156
|
+
const upperScreen = screen.toUpperCase();
|
|
157
|
+
if (upperScreen.includes("RUN") || upperScreen.includes("ERROR")) {
|
|
158
|
+
runDetected = true;
|
|
159
|
+
logger.debug("RUN detected, starting 100ms stabilization", { elapsed: Date.now() - startTime });
|
|
160
|
+
// Wait 100ms for stabilization as per spec
|
|
161
|
+
await delay(100);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
logger.debug("Failed to read screen while waiting for RUN", { error });
|
|
167
|
+
}
|
|
168
|
+
await delay(config.intervalMs);
|
|
169
|
+
}
|
|
170
|
+
// If RUN never appeared, assume instant execution (success)
|
|
171
|
+
if (!runDetected) {
|
|
172
|
+
logger.debug("RUN never detected, assuming instant execution");
|
|
173
|
+
return {
|
|
174
|
+
status: "ok",
|
|
175
|
+
type: "ASM",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// Now poll hardware regions to detect activity
|
|
179
|
+
let prevIoSignature = null;
|
|
180
|
+
let prevJiffyClock = null;
|
|
181
|
+
let alive = false;
|
|
182
|
+
const pollDeadline = Date.now() + config.maxMs;
|
|
183
|
+
while (Date.now() < pollDeadline && !alive) {
|
|
184
|
+
pollCount++;
|
|
185
|
+
try {
|
|
186
|
+
// Read I/O regions in optimized batches as per comment #3466803675
|
|
187
|
+
// All of $D000-$DFFF in a single call (covers VIC-II, SID, CIA1, CIA2)
|
|
188
|
+
const ioRegions = await client.readMemoryRaw(0xD000, 0x1000); // $D000-$DFFF
|
|
189
|
+
// Read low memory through screen RAM in a single call: $0000-$07E7
|
|
190
|
+
// This covers zero page, stack, jiffy clock ($00A0-$00A2), and screen memory
|
|
191
|
+
const lowMemAndScreen = await client.readMemoryRaw(0x0000, 0x7E8); // $0000-$07E7
|
|
192
|
+
// Extract jiffy clock for separate comparison (at offset $00A0 in the buffer)
|
|
193
|
+
const jiffyClock = lowMemAndScreen.slice(0xA0, 0xA3);
|
|
194
|
+
// Concatenate all regions and compute signature
|
|
195
|
+
const combinedBuffer = concatBuffers(ioRegions, lowMemAndScreen);
|
|
196
|
+
const ioSignature = computeCrc32(combinedBuffer);
|
|
197
|
+
// Check for any activity
|
|
198
|
+
if (prevIoSignature !== null && ioSignature !== prevIoSignature) {
|
|
199
|
+
logger.debug("Hardware or screen activity detected", {
|
|
200
|
+
pollCount,
|
|
201
|
+
elapsed: Date.now() - startTime,
|
|
202
|
+
signatureChanged: true
|
|
203
|
+
});
|
|
204
|
+
alive = true;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
if (prevJiffyClock !== null && !buffersEqual(jiffyClock, prevJiffyClock)) {
|
|
208
|
+
logger.debug("Jiffy clock advanced", {
|
|
209
|
+
pollCount,
|
|
210
|
+
elapsed: Date.now() - startTime,
|
|
211
|
+
clockChanged: true
|
|
212
|
+
});
|
|
213
|
+
alive = true;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
prevIoSignature = ioSignature;
|
|
217
|
+
prevJiffyClock = jiffyClock;
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
logger.debug("Failed to read memory during ASM polling", { error, pollCount });
|
|
221
|
+
}
|
|
222
|
+
await delay(config.intervalMs);
|
|
223
|
+
}
|
|
224
|
+
if (alive) {
|
|
225
|
+
logger.debug("ASM program alive - hardware or screen progressing", { pollCount, elapsed: Date.now() - startTime });
|
|
226
|
+
return {
|
|
227
|
+
status: "ok",
|
|
228
|
+
type: "ASM",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
// No activity detected - consider it crashed
|
|
232
|
+
logger.debug("ASM program crashed - no VIC/CIA/TI/screen progression", { pollCount, elapsed: Date.now() - startTime });
|
|
233
|
+
return {
|
|
234
|
+
status: "crashed",
|
|
235
|
+
type: "ASM",
|
|
236
|
+
reason: "no VIC/CIA/TI/screen progression within window",
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Main polling function that polls for program outcome.
|
|
241
|
+
*
|
|
242
|
+
* @param type - Type of program (BASIC or ASM)
|
|
243
|
+
* @param client - C64 client for screen reading
|
|
244
|
+
* @param logger - Logger for debug output
|
|
245
|
+
* @param config - Optional poll configuration (uses environment defaults if not provided)
|
|
246
|
+
* @returns Promise resolving to poll result
|
|
247
|
+
*/
|
|
248
|
+
export async function pollForProgramOutcome(type, client, logger, config) {
|
|
249
|
+
const pollConfig = config ?? loadPollConfig();
|
|
250
|
+
logger.debug("Starting program outcome polling", { type, maxMs: pollConfig.maxMs, intervalMs: pollConfig.intervalMs });
|
|
251
|
+
// Poll for program outcome based on type
|
|
252
|
+
if (type === "BASIC") {
|
|
253
|
+
return pollBasicOutcome(client, logger, pollConfig);
|
|
254
|
+
}
|
|
255
|
+
return pollAsmOutcome(client, logger, pollConfig);
|
|
256
|
+
}
|