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,469 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createPetsciiArt } from "../petsciiArt.js";
3
+ import { defineToolModule } from "./types.js";
4
+ import { booleanSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "./schema.js";
5
+ import { jsonResult, textResult } from "./responses.js";
6
+ import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
7
+ function toRecord(details) {
8
+ if (details && typeof details === "object") {
9
+ return details;
10
+ }
11
+ return undefined;
12
+ }
13
+ function normaliseFailure(details) {
14
+ if (details === undefined || details === null) {
15
+ return undefined;
16
+ }
17
+ if (typeof details === "object") {
18
+ return details;
19
+ }
20
+ return { value: details };
21
+ }
22
+ function decodeSpriteString(value, path) {
23
+ const trimmed = value.trim();
24
+ if (!trimmed) {
25
+ throw new ToolValidationError("Sprite string must not be empty", { path });
26
+ }
27
+ const collapsed = trimmed.replace(/\s+/g, "");
28
+ const base64Pattern = /^(?:[A-Za-z0-9+\/_-]{4})*(?:[A-Za-z0-9+\/_-]{2}(?:==)?|[A-Za-z0-9+\/_-]{3}=)?$/;
29
+ if (collapsed.length % 4 === 0 && base64Pattern.test(collapsed)) {
30
+ try {
31
+ const decoded = Buffer.from(collapsed.replace(/-/g, "+").replace(/_/g, "/"), "base64");
32
+ if (decoded.length === 63) {
33
+ return Uint8Array.from(decoded);
34
+ }
35
+ }
36
+ catch {
37
+ // Fallback to hex parsing below
38
+ }
39
+ }
40
+ const withoutPrefix = collapsed.startsWith("$") ? collapsed.slice(1) : collapsed;
41
+ const cleaned = withoutPrefix.replace(/[^0-9A-Fa-f]/g, "");
42
+ if (cleaned.length !== 63 * 2) {
43
+ throw new ToolValidationError("Sprite hex string must contain exactly 126 hex characters", { path });
44
+ }
45
+ try {
46
+ return Uint8Array.from(Buffer.from(cleaned, "hex"));
47
+ }
48
+ catch (error) {
49
+ throw new ToolValidationError("Unable to parse sprite hex string", { path, cause: error });
50
+ }
51
+ }
52
+ const spriteBytesSchema = {
53
+ jsonSchema: {
54
+ description: "63-byte sprite definition provided as base64/hex string or array of bytes.",
55
+ type: ["string", "array"],
56
+ items: {
57
+ type: "integer",
58
+ minimum: 0,
59
+ maximum: 255,
60
+ },
61
+ minItems: 63,
62
+ maxItems: 63,
63
+ },
64
+ parse(value, path) {
65
+ const resolvedPath = path ?? "$";
66
+ if (typeof value === "string") {
67
+ return decodeSpriteString(value, resolvedPath);
68
+ }
69
+ if (Array.isArray(value)) {
70
+ if (value.length !== 63) {
71
+ throw new ToolValidationError("Sprite byte array must contain exactly 63 entries", { path: resolvedPath });
72
+ }
73
+ const bytes = new Uint8Array(63);
74
+ for (let i = 0; i < value.length; i += 1) {
75
+ const item = value[i];
76
+ if (typeof item !== "number" || !Number.isFinite(item)) {
77
+ throw new ToolValidationError("Sprite byte array must contain numbers", { path: `${resolvedPath}[${i}]` });
78
+ }
79
+ if (item < 0 || item > 255) {
80
+ throw new ToolValidationError("Sprite byte values must be between 0 and 255", {
81
+ path: `${resolvedPath}[${i}]`,
82
+ details: { value: item },
83
+ });
84
+ }
85
+ bytes[i] = item & 0xff;
86
+ }
87
+ return bytes;
88
+ }
89
+ throw new ToolValidationError("Sprite must be provided as a string or 63-byte array", { path: resolvedPath });
90
+ },
91
+ };
92
+ const spriteArgsSchema = objectSchema({
93
+ description: "Generate a PRG that displays a single sprite using raw sprite data.",
94
+ properties: {
95
+ sprite: spriteBytesSchema,
96
+ index: numberSchema({
97
+ description: "Sprite index (0-7) to configure in screen memory.",
98
+ integer: true,
99
+ minimum: 0,
100
+ maximum: 7,
101
+ default: 0,
102
+ }),
103
+ x: numberSchema({
104
+ description: "Sprite X position (0-511).",
105
+ integer: true,
106
+ minimum: 0,
107
+ maximum: 511,
108
+ default: 100,
109
+ }),
110
+ y: numberSchema({
111
+ description: "Sprite Y position (0-255).",
112
+ integer: true,
113
+ minimum: 0,
114
+ maximum: 255,
115
+ default: 100,
116
+ }),
117
+ color: numberSchema({
118
+ description: "Sprite colour index (0-15).",
119
+ integer: true,
120
+ minimum: 0,
121
+ maximum: 15,
122
+ default: 1,
123
+ }),
124
+ multicolour: booleanSchema({
125
+ description: "Enable multicolour mode for the sprite.",
126
+ default: false,
127
+ }),
128
+ },
129
+ required: ["sprite"],
130
+ additionalProperties: false,
131
+ });
132
+ const bitmapSchema = {
133
+ jsonSchema: {
134
+ description: "Explicit bitmap definition for PETSCII rendering.",
135
+ type: "object",
136
+ properties: {
137
+ width: { type: "integer", minimum: 1, maximum: 320 },
138
+ height: { type: "integer", minimum: 1, maximum: 200 },
139
+ pixels: {
140
+ type: "array",
141
+ items: { type: "integer", minimum: 0, maximum: 1 },
142
+ },
143
+ },
144
+ required: ["width", "height", "pixels"],
145
+ additionalProperties: false,
146
+ },
147
+ parse(value, path) {
148
+ const resolvedPath = path ?? "$";
149
+ if (!value || typeof value !== "object") {
150
+ throw new ToolValidationError("Bitmap definition must be an object", { path: resolvedPath });
151
+ }
152
+ const input = value;
153
+ const width = Number(input.width);
154
+ const height = Number(input.height);
155
+ if (!Number.isFinite(width) || !Number.isFinite(height)) {
156
+ throw new ToolValidationError("Bitmap width and height must be numbers", { path: resolvedPath });
157
+ }
158
+ if (width <= 0 || height <= 0 || width > 320 || height > 200) {
159
+ throw new ToolValidationError("Bitmap dimensions must be within 1..320 x 1..200", { path: resolvedPath, details: { width, height } });
160
+ }
161
+ const pixels = input.pixels;
162
+ if (!Array.isArray(pixels)) {
163
+ throw new ToolValidationError("Bitmap pixels must be an array", { path: `${resolvedPath}.pixels` });
164
+ }
165
+ if (pixels.length !== width * height) {
166
+ throw new ToolValidationError("Bitmap pixel array length must equal width*height", {
167
+ path: `${resolvedPath}.pixels`,
168
+ details: { expected: width * height, received: pixels.length },
169
+ });
170
+ }
171
+ const out = new Uint8Array(width * height);
172
+ for (let i = 0; i < pixels.length; i += 1) {
173
+ const valueAt = pixels[i];
174
+ if (typeof valueAt !== "number" || !Number.isFinite(valueAt)) {
175
+ throw new ToolValidationError("Bitmap pixels must be numeric", { path: `${resolvedPath}.pixels[${i}]` });
176
+ }
177
+ out[i] = valueAt > 0 ? 1 : 0;
178
+ }
179
+ return { width, height, pixels: out };
180
+ },
181
+ };
182
+ const petsciiImageArgsSchema = objectSchema({
183
+ description: "Generate PETSCII art from text, prompts, or explicit bitmap data.",
184
+ properties: {
185
+ prompt: optionalSchema(stringSchema({
186
+ description: "Natural language prompt describing the desired PETSCII art.",
187
+ minLength: 1,
188
+ })),
189
+ text: optionalSchema(stringSchema({
190
+ description: "Exact text to render in PETSCII (overrides prompt derivation).",
191
+ minLength: 1,
192
+ })),
193
+ maxWidth: optionalSchema(numberSchema({
194
+ description: "Maximum bitmap width in pixels (1-320).",
195
+ integer: true,
196
+ minimum: 1,
197
+ maximum: 320,
198
+ })),
199
+ maxHeight: optionalSchema(numberSchema({
200
+ description: "Maximum bitmap height in pixels (1-200).",
201
+ integer: true,
202
+ minimum: 1,
203
+ maximum: 200,
204
+ })),
205
+ borderColor: optionalSchema(numberSchema({
206
+ description: "Border colour index (0-15).",
207
+ integer: true,
208
+ minimum: 0,
209
+ maximum: 15,
210
+ })),
211
+ backgroundColor: optionalSchema(numberSchema({
212
+ description: "Background colour index (0-15).",
213
+ integer: true,
214
+ minimum: 0,
215
+ maximum: 15,
216
+ })),
217
+ foregroundColor: optionalSchema(numberSchema({
218
+ description: "Foreground colour index (0-15).",
219
+ integer: true,
220
+ minimum: 0,
221
+ maximum: 15,
222
+ })),
223
+ dryRun: booleanSchema({
224
+ description: "When true, skip uploading the BASIC program to the C64.",
225
+ default: false,
226
+ }),
227
+ bitmap: optionalSchema(bitmapSchema),
228
+ },
229
+ additionalProperties: false,
230
+ });
231
+ const renderPetsciiScreenArgsSchema = objectSchema({
232
+ description: "Arguments for rendering PETSCII text on the main screen.",
233
+ properties: {
234
+ text: stringSchema({
235
+ description: "The PETSCII text to print after clearing the screen.",
236
+ minLength: 1,
237
+ }),
238
+ borderColor: optionalSchema(numberSchema({
239
+ description: "Border colour index (0-15).",
240
+ integer: true,
241
+ minimum: 0,
242
+ maximum: 15,
243
+ })),
244
+ backgroundColor: optionalSchema(numberSchema({
245
+ description: "Background colour index (0-15).",
246
+ integer: true,
247
+ minimum: 0,
248
+ maximum: 15,
249
+ })),
250
+ },
251
+ required: ["text"],
252
+ additionalProperties: false,
253
+ });
254
+ export const graphicsModule = defineToolModule({
255
+ domain: "graphics",
256
+ summary: "PETSCII art, sprite workflows, and VIC-II graphics helpers.",
257
+ resources: [
258
+ "c64://specs/vic",
259
+ "c64://specs/assembly",
260
+ "c64://specs/basic",
261
+ ],
262
+ prompts: ["graphics-demo", "basic-program", "assembly-program"],
263
+ defaultTags: ["graphics", "vic"],
264
+ workflowHints: [
265
+ "Suggest graphics helpers when the user asks for sprites, PETSCII art, or screen layout tweaks.",
266
+ "Mention how VIC-II state changes (colours, sprite positions) affect follow-up memory operations.",
267
+ ],
268
+ tools: [
269
+ {
270
+ name: "generate_sprite_prg",
271
+ description: "Generate and execute a PRG that displays a sprite from raw 63-byte data. See c64://specs/vic for registers.",
272
+ summary: "Uploads minimal machine code to copy sprite data, configure VIC-II, and render a sprite.",
273
+ inputSchema: spriteArgsSchema.jsonSchema,
274
+ relatedResources: ["c64://specs/vic"],
275
+ relatedPrompts: ["graphics-demo", "assembly-program"],
276
+ tags: ["sprite", "assembly"],
277
+ prerequisites: ["upload_and_run_asm"],
278
+ examples: [
279
+ {
280
+ name: "Render sprite 0",
281
+ description: "Show sprite at 100,100",
282
+ arguments: { sprite: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", index: 0, x: 100, y: 100, color: 1, multicolour: false },
283
+ },
284
+ ],
285
+ workflowHints: [
286
+ "Use when the user supplies sprite bytes or asks to preview graphics quickly; describe resulting coordinates and colours.",
287
+ "Remind the user that sprites live in banked memory so further tweaks may require write_memory calls.",
288
+ ],
289
+ async execute(args, ctx) {
290
+ try {
291
+ const parsed = spriteArgsSchema.parse(args ?? {});
292
+ ctx.logger.info("Generating sprite PRG", {
293
+ index: parsed.index,
294
+ x: parsed.x,
295
+ y: parsed.y,
296
+ color: parsed.color,
297
+ multicolour: parsed.multicolour,
298
+ });
299
+ const result = await ctx.client.generateAndRunSpritePrg({
300
+ spriteBytes: parsed.sprite,
301
+ spriteIndex: parsed.index,
302
+ x: parsed.x,
303
+ y: parsed.y,
304
+ color: parsed.color,
305
+ multicolour: parsed.multicolour,
306
+ });
307
+ if (!result.success) {
308
+ throw new ToolExecutionError("C64 firmware reported failure while running sprite PRG", {
309
+ details: normaliseFailure(result.details),
310
+ });
311
+ }
312
+ return textResult("Sprite PRG generated and executed successfully.", {
313
+ success: true,
314
+ index: parsed.index,
315
+ x: parsed.x,
316
+ y: parsed.y,
317
+ color: parsed.color,
318
+ multicolour: parsed.multicolour,
319
+ spriteByteLength: parsed.sprite.length,
320
+ details: toRecord(result.details) ?? null,
321
+ });
322
+ }
323
+ catch (error) {
324
+ if (error instanceof ToolError) {
325
+ return toolErrorResult(error);
326
+ }
327
+ return unknownErrorResult(error);
328
+ }
329
+ },
330
+ },
331
+ {
332
+ name: "render_petscii_screen",
333
+ description: "Render PETSCII text to the screen with optional border/background colours. See c64://specs/basic.",
334
+ summary: "Generates a BASIC program that clears the screen, sets colours, and prints text.",
335
+ inputSchema: renderPetsciiScreenArgsSchema.jsonSchema,
336
+ relatedResources: ["c64://specs/basic", "c64://context/bootstrap"],
337
+ relatedPrompts: ["basic-program", "graphics-demo"],
338
+ tags: ["basic", "screen"],
339
+ prerequisites: ["upload_and_run_basic"],
340
+ examples: [
341
+ {
342
+ name: "Render text",
343
+ description: "Blue border, black background",
344
+ arguments: { text: "HELLO", borderColor: 6, backgroundColor: 0 },
345
+ },
346
+ ],
347
+ workflowHints: [
348
+ "Call after generating PETSCII text or when the user wants border/background colour changes applied.",
349
+ "Echo the colour indices and mention CLEAR + PRINT so the user knows what ran.",
350
+ ],
351
+ async execute(args, ctx) {
352
+ try {
353
+ const parsed = renderPetsciiScreenArgsSchema.parse(args ?? {});
354
+ ctx.logger.info("Rendering PETSCII screen", {
355
+ textLength: parsed.text.length,
356
+ borderColor: parsed.borderColor,
357
+ backgroundColor: parsed.backgroundColor,
358
+ });
359
+ const result = await ctx.client.renderPetsciiScreenAndRun(parsed);
360
+ if (!result.success) {
361
+ throw new ToolExecutionError("C64 firmware reported failure while rendering PETSCII text", {
362
+ details: normaliseFailure(result.details),
363
+ });
364
+ }
365
+ return textResult("PETSCII screen rendered successfully.", {
366
+ success: true,
367
+ textLength: parsed.text.length,
368
+ borderColor: parsed.borderColor ?? null,
369
+ backgroundColor: parsed.backgroundColor ?? null,
370
+ details: toRecord(result.details) ?? null,
371
+ });
372
+ }
373
+ catch (error) {
374
+ if (error instanceof ToolError) {
375
+ return toolErrorResult(error);
376
+ }
377
+ return unknownErrorResult(error);
378
+ }
379
+ },
380
+ },
381
+ {
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/vic.",
384
+ summary: "Synthesises PETSCII art, generates a BASIC program, and uploads it unless dry-run is requested.",
385
+ inputSchema: petsciiImageArgsSchema.jsonSchema,
386
+ relatedResources: ["c64://specs/basic", "c64://specs/vic"],
387
+ relatedPrompts: ["graphics-demo", "basic-program"],
388
+ tags: ["petscii", "basic"],
389
+ prerequisites: ["upload_and_run_basic"],
390
+ examples: [
391
+ {
392
+ name: "Generate PETSCII",
393
+ description: "Run art with default colours",
394
+ arguments: { prompt: "cat", dryRun: false },
395
+ },
396
+ ],
397
+ workflowHints: [
398
+ "Trigger when the user provides creative prompts; clarify whether you ran the art or left it as a dry run.",
399
+ "Provide follow-up suggestions like saving the PRG or capturing the screen after rendering.",
400
+ ],
401
+ async execute(args, ctx) {
402
+ try {
403
+ const parsed = petsciiImageArgsSchema.parse(args ?? {});
404
+ if (!parsed.prompt && !parsed.text && !parsed.bitmap) {
405
+ throw new ToolValidationError("Provide a prompt, text, or explicit bitmap definition", { path: "$.prompt" });
406
+ }
407
+ ctx.logger.info("Generating PETSCII art", {
408
+ hasPrompt: Boolean(parsed.prompt),
409
+ hasText: Boolean(parsed.text),
410
+ dryRun: parsed.dryRun,
411
+ hasBitmap: Boolean(parsed.bitmap),
412
+ maxWidth: parsed.maxWidth,
413
+ maxHeight: parsed.maxHeight,
414
+ });
415
+ const art = createPetsciiArt({
416
+ prompt: parsed.prompt,
417
+ text: parsed.text,
418
+ maxWidth: parsed.maxWidth,
419
+ maxHeight: parsed.maxHeight,
420
+ borderColor: parsed.borderColor,
421
+ backgroundColor: parsed.backgroundColor,
422
+ foregroundColor: parsed.foregroundColor,
423
+ bitmap: parsed.bitmap,
424
+ });
425
+ let runResult;
426
+ if (!parsed.dryRun) {
427
+ runResult = await ctx.client.uploadAndRunBasic(art.program);
428
+ if (!runResult.success) {
429
+ throw new ToolExecutionError("C64 firmware reported failure while rendering PETSCII art", {
430
+ details: normaliseFailure(runResult.details),
431
+ });
432
+ }
433
+ }
434
+ const ranOnC64 = !parsed.dryRun && Boolean(runResult?.success);
435
+ const data = {
436
+ success: parsed.dryRun ? true : Boolean(runResult?.success ?? true),
437
+ ranOnC64,
438
+ runDetails: runResult?.details ?? null,
439
+ program: art.program,
440
+ bitmapHex: art.bitmapHex,
441
+ rowHex: art.rowHex,
442
+ width: art.bitmap.width,
443
+ height: art.bitmap.height,
444
+ charColumns: art.charColumns,
445
+ charRows: art.charRows,
446
+ petsciiCodes: art.petsciiCodes,
447
+ usedShape: art.usedShape ?? null,
448
+ sourceText: art.sourceText ?? null,
449
+ ragRefs: null,
450
+ };
451
+ return jsonResult(data, {
452
+ ranOnC64,
453
+ dryRun: parsed.dryRun,
454
+ width: art.bitmap.width,
455
+ height: art.bitmap.height,
456
+ charColumns: art.charColumns,
457
+ charRows: art.charRows,
458
+ });
459
+ }
460
+ catch (error) {
461
+ if (error instanceof ToolError) {
462
+ return toolErrorResult(error);
463
+ }
464
+ return unknownErrorResult(error);
465
+ }
466
+ },
467
+ },
468
+ ],
469
+ });