c64bridge 1.0.6 → 1.1.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 (136) hide show
  1. package/.github/skills/assembly-program/SKILL.md +37 -0
  2. package/.github/skills/basic-program/SKILL.md +40 -0
  3. package/.github/skills/cross-platform-demo/SKILL.md +32 -0
  4. package/.github/skills/drive-manager/SKILL.md +33 -0
  5. package/.github/skills/graphics-demo/SKILL.md +30 -0
  6. package/.github/skills/hello-world/SKILL.md +36 -0
  7. package/.github/skills/memory-debug/SKILL.md +32 -0
  8. package/.github/skills/pr-converge/SKILL.md +33 -0
  9. package/.github/skills/printer-job/SKILL.md +30 -0
  10. package/.github/skills/sid-music/SKILL.md +35 -0
  11. package/.github/skills/software-launch/SKILL.md +31 -0
  12. package/.github/skills/stream-control/SKILL.md +29 -0
  13. package/.github/skills/system-control/SKILL.md +32 -0
  14. package/AGENTS.md +28 -7
  15. package/README.md +70 -15
  16. package/data/assembly/assembly-spec.md +2 -0
  17. package/dist/c64Client.js +238 -60
  18. package/dist/chargen.js +0 -4
  19. package/dist/config.js +53 -28
  20. package/dist/device.js +92 -37
  21. package/dist/diagnostics.js +66 -5
  22. package/dist/hostPaths.js +128 -0
  23. package/dist/logger.js +3 -2
  24. package/dist/loggingHttpClient.js +71 -2
  25. package/dist/mcp-server.js +416 -291
  26. package/dist/numericArg.js +55 -0
  27. package/dist/petscii.js +13 -6
  28. package/dist/platform.js +12 -3
  29. package/dist/prompts/registry.js +6 -1
  30. package/dist/redact.js +166 -0
  31. package/dist/tools/access.js +66 -0
  32. package/dist/tools/debug.js +6 -0
  33. package/dist/tools/developer.js +77 -36
  34. package/dist/tools/errors.js +54 -3
  35. package/dist/tools/input.js +25 -8
  36. package/dist/tools/machineControl.js +113 -21
  37. package/dist/tools/memory.js +135 -30
  38. package/dist/tools/meta/artifacts.js +5 -5
  39. package/dist/tools/meta/background.js +216 -54
  40. package/dist/tools/meta/compilation.js +6 -5
  41. package/dist/tools/meta/config.js +20 -10
  42. package/dist/tools/meta/configInventory.js +25 -2
  43. package/dist/tools/meta/diagnostics.js +2 -1
  44. package/dist/tools/meta/filesystem.js +16 -14
  45. package/dist/tools/meta/graphics.js +48 -20
  46. package/dist/tools/meta/memory.js +45 -19
  47. package/dist/tools/meta/program.js +10 -7
  48. package/dist/tools/meta/screen.js +116 -25
  49. package/dist/tools/pauseGuard.js +23 -0
  50. package/dist/tools/pollValidator.js +53 -8
  51. package/dist/tools/programRunners.js +62 -18
  52. package/dist/tools/registry/batch.js +17 -11
  53. package/dist/tools/registry/config.js +9 -2
  54. package/dist/tools/registry/disk.js +12 -2
  55. package/dist/tools/registry/drive.js +3 -0
  56. package/dist/tools/registry/extract.js +3 -0
  57. package/dist/tools/registry/graphics.js +59 -6
  58. package/dist/tools/registry/index.js +17 -1
  59. package/dist/tools/registry/memory.js +11 -1
  60. package/dist/tools/registry/platform.js +4 -1
  61. package/dist/tools/registry/printer.js +2 -1
  62. package/dist/tools/registry/program.js +5 -1
  63. package/dist/tools/registry/rag.js +1 -0
  64. package/dist/tools/registry/sound.js +12 -1
  65. package/dist/tools/registry/stream.js +2 -1
  66. package/dist/tools/registry/system.js +8 -1
  67. package/dist/tools/schema.js +7 -7
  68. package/dist/tools/streaming.js +34 -4
  69. package/dist/tools/translation/assembler.js +247 -24
  70. package/dist/tools/translation/basicDetokenizer.js +80 -0
  71. package/dist/tools/translation/basicTokenizer.js +8 -3
  72. package/dist/tools/translation/disassembler.js +14 -0
  73. package/dist/tools/types.js +258 -7
  74. package/dist/tools/vice.js +5 -16
  75. package/dist/vicBitmap.js +1 -1
  76. package/dist/vice/mockServer.js +3 -0
  77. package/dist/vice/process.js +19 -0
  78. package/dist/vice/resourcePolicy.js +36 -0
  79. package/dist/vice/viceClient.js +22 -5
  80. package/doc/vice/support-matrix.md +17 -11
  81. package/mcp/schemas/c64_batch.schema.json +2 -1
  82. package/mcp/schemas/c64_config.schema.json +33 -466
  83. package/mcp/schemas/c64_debug.schema.json +3 -602
  84. package/mcp/schemas/c64_disk.schema.json +17 -344
  85. package/mcp/schemas/c64_drive.schema.json +2 -157
  86. package/mcp/schemas/c64_extract.schema.json +10 -364
  87. package/mcp/schemas/c64_graphics.schema.json +19 -426
  88. package/mcp/schemas/c64_input.schema.json +5 -318
  89. package/mcp/schemas/c64_memory.schema.json +23 -491
  90. package/mcp/schemas/c64_printer.schema.json +3 -258
  91. package/mcp/schemas/c64_program.schema.json +16 -491
  92. package/mcp/schemas/c64_rag.schema.json +1 -81
  93. package/mcp/schemas/c64_select_backend.schema.json +2 -36
  94. package/mcp/schemas/c64_sound.schema.json +20 -822
  95. package/mcp/schemas/c64_stream.schema.json +4 -75
  96. package/mcp/schemas/c64_system.schema.json +28 -348
  97. package/mcp/schemas/c64_vice.schema.json +2 -71
  98. package/mcp/server.json +1 -1
  99. package/mcp/tools.json +1990 -6900
  100. package/mcp.json +21 -1
  101. package/package.json +19 -8
  102. package/scripts/mockC64Server.mjs +44 -0
  103. package/scripts/run-coverage.mjs +27 -1
  104. package/scripts/smoke-run.sh +3 -2
  105. package/scripts/start.mjs +23 -1
  106. package/server.json +53 -3
  107. package/dist/.tsbuildinfo +0 -1
  108. package/doc/agent-state-spec.md +0 -243
  109. package/doc/plans/bun/baseline-metrics.md +0 -16
  110. package/doc/plans/bun/migration-checklist.md +0 -55
  111. package/doc/plans/bun/migration.md +0 -83
  112. package/doc/plans/improve-knowledge/notes.md +0 -43
  113. package/doc/plans/improve-knowledge/plan.md +0 -121
  114. package/doc/plans/improve-knowledge/rollout.md +0 -110
  115. package/doc/plans/meta-tools/README.md +0 -110
  116. package/doc/plans/meta-tools/analysis.md +0 -272
  117. package/doc/plans/meta-tools/catalog.md +0 -435
  118. package/doc/plans/meta-tools/notes.md +0 -192
  119. package/doc/plans/meta-tools/rollout.md +0 -368
  120. package/doc/plans/prompts/README.md +0 -96
  121. package/doc/plans/prompts/user-prompt-ideas.md +0 -147
  122. package/doc/plans/tool-consolidation/notes.md +0 -28
  123. package/doc/plans/tool-consolidation/plan.md +0 -169
  124. package/doc/plans/tool-consolidation/rollout.md +0 -104
  125. package/doc/plans/vice-backend-switch/plan.md +0 -486
  126. package/doc/plans/vice-backend-switch-2/plan.md +0 -48
  127. package/doc/plans/vice-support/notes.md +0 -63
  128. package/doc/plans/vice-support/plan.md +0 -88
  129. package/doc/plans/vice-support/rollout.md +0 -99
  130. package/doc/reviews/audit.md +0 -143
  131. package/doc/reviews/hardening/01-fable/prompt-handover.md +0 -125
  132. package/doc/reviews/hardening/01-fable/prompt.md +0 -98
  133. package/doc/reviews/hardening/01-fable/review-prompt.md +0 -468
  134. package/doc/reviews/hardening/01-fable/review.md +0 -1234
  135. package/doc/reviews/vice-support-review.md +0 -185
  136. package/scripts/compare-rest/rest-compare-2025-11-01T00-25-21-830Z.json +0 -2513
@@ -1,5 +1,5 @@
1
1
  import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema, } from "../types.js";
2
- import { memoryModule, memoryOperationHandlers as groupedMemoryHandlers, disassembleArgsSchema, copyMemoryArgsSchema, fillMemoryArgsSchema, searchMemoryArgsSchema, compareMemoryArgsSchema, saveMemoryArgsSchema, } from "../memory.js";
2
+ import { memoryModule, memoryOperationHandlers as groupedMemoryHandlers, listBasicArgsSchema, disassembleArgsSchema, copyMemoryArgsSchema, fillMemoryArgsSchema, searchMemoryArgsSchema, compareMemoryArgsSchema, saveMemoryArgsSchema, } from "../memory.js";
3
3
  import { metaModule } from "../meta/index.js";
4
4
  import { buildDescriptorIndex, ensureDescriptor, extendSchemaWithOp, createOperationHandlers, invokeModuleTool, } from "./utils.js";
5
5
  const memoryDescriptorIndex = buildDescriptorIndex(memoryModule);
@@ -25,6 +25,11 @@ const memoryOperations = [
25
25
  schema: extendSchemaWithOp("wait_for_text", ensureDescriptor(metaDescriptorIndex, "wait_for_screen_text").inputSchema, { description: "Poll the screen until a substring or regex appears, or timeout elapses." }),
26
26
  handler: async (rawArgs, ctx) => invokeModuleTool(metaModule, "wait_for_screen_text", rawArgs, ctx),
27
27
  },
28
+ {
29
+ op: "list_basic",
30
+ schema: extendSchemaWithOp("list_basic", listBasicArgsSchema.jsonSchema, { description: "List the BASIC program in memory as text: reads the $2B-$2E pointers, walks the line links and detokenises. Works on C64U/U64, U2-family hardware, and VICE." }),
31
+ handler: groupedMemoryHandlers.list_basic,
32
+ },
28
33
  {
29
34
  op: "disassemble",
30
35
  schema: extendSchemaWithOp("disassemble", disassembleArgsSchema.jsonSchema, { description: "Disassemble a memory region into annotated 6502/6510 instructions, including undocumented opcodes with canonical names. Symbol annotations from `.vs` files are applied when available. Works on C64U/U64, U2-family hardware, and VICE." }),
@@ -77,6 +82,11 @@ export const memoryModuleGroup = defineToolModule({
77
82
  description: "Memory operations available via the c64_memory tool.",
78
83
  variants: memoryOperations.map((operation) => operation.schema),
79
84
  }),
85
+ operationEffects: {
86
+ read: "read", write: "write", read_screen: "read", wait_for_text: "read", list_basic: "read", disassemble: "read",
87
+ copy_memory: "write", fill_memory: "write", search_memory: "read", compare_memory: "read",
88
+ save_memory: "write",
89
+ },
80
90
  tags: ["memory", "screen", "grouped"],
81
91
  examples: [
82
92
  {
@@ -44,6 +44,7 @@ function describeTargetPlatform(target) {
44
44
  return {
45
45
  availableTools: info.tools,
46
46
  unavailableTools: info.unsupported_tools,
47
+ unavailableOperations: info.unsupported_operations,
47
48
  };
48
49
  }
49
50
  const selectBackendSchema = operationSchema("select", {
@@ -77,7 +78,7 @@ const platformOperationHandlers = {
77
78
  cancelGeneratedSidPlayback(); // A backend switch is the documented cue to stop it rather than let it keep playing on the old backend.
78
79
  ctx.client.switchBackend(backend);
79
80
  ctx.setPlatform(backend);
80
- const { availableTools, unavailableTools } = describeTargetPlatform(backend);
81
+ const { availableTools, unavailableTools, unavailableOperations } = describeTargetPlatform(backend);
81
82
  const switchBackTarget = availableBackends.find((candidate) => candidate !== backend);
82
83
  const data = {
83
84
  success: true,
@@ -85,6 +86,7 @@ const platformOperationHandlers = {
85
86
  configuredBackends: availableBackends,
86
87
  availableTools,
87
88
  unavailableTools,
89
+ unavailableOperations,
88
90
  usageHint: switchBackTarget
89
91
  ? `Use c64_select_backend again with backend: "${switchBackTarget}" to switch back.`
90
92
  : "No other backend is configured.",
@@ -111,6 +113,7 @@ export const platformModuleGroup = defineToolModule({
111
113
  description: "Runtime backend selection operations.",
112
114
  variants: [selectBackendSchema],
113
115
  }),
116
+ operationEffects: { select: "write" },
114
117
  tags: ["platform", "backend", "switch"],
115
118
  execute: createOperationDispatcher("c64_select_backend", platformOperationHandlers),
116
119
  examples: [
@@ -133,12 +133,13 @@ export const printerModuleGroup = defineToolModule({
133
133
  description: "Printer operations available via the c64_printer tool.",
134
134
  variants: printerOperations.map((operation) => operation.schema),
135
135
  }),
136
+ operationEffects: { print_text: "write", print_bitmap: "write", define_chars: "write" },
136
137
  tags: ["printer", "device", "grouped"],
137
138
  examples: [
138
139
  {
139
140
  name: "Print text",
140
141
  description: "Generate BASIC for device 4",
141
- arguments: { op: "print_text", text: "HELLO", ensureReturn: true },
142
+ arguments: { op: "print_text", text: "HELLO", target: "commodore", formFeed: true },
142
143
  },
143
144
  {
144
145
  name: "Print bitmap",
@@ -68,8 +68,12 @@ export const programModule = defineToolModule({
68
68
  description: "Program operations available via the c64_program tool.",
69
69
  variants: programOperations.map((operation) => operation.schema),
70
70
  }),
71
+ operationEffects: {
72
+ load_prg: "write", run_prg: "write", run_crt: "write", upload_run_basic: "write",
73
+ upload_run_asm: "write", batch_run: "write", bundle_run: "write", cross_platform_greeting: "write",
74
+ },
71
75
  tags: ["programs", "execution", "grouped"],
72
- operationPlatforms: { load_prg: ["c64u", "u2"], run_crt: ["c64u", "u2"] },
76
+ operationPlatforms: { run_crt: ["c64u", "u2"] },
73
77
  operationToolNames: {
74
78
  load_prg: "load_prg",
75
79
  run_crt: "run_crt",
@@ -35,6 +35,7 @@ export const ragModuleGroup = defineToolModule({
35
35
  description: "RAG operations available via the c64_rag tool.",
36
36
  variants: ragOperations.map((operation) => operation.schema),
37
37
  }),
38
+ operationEffects: { basic: "read", asm: "read" },
38
39
  tags: ["rag", "reference", "grouped"],
39
40
  examples: [
40
41
  {
@@ -4,10 +4,12 @@ import { audioModule } from "../audio.js";
4
4
  import { metaModule } from "../meta/index.js";
5
5
  import { literalSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "../schema.js";
6
6
  import { buildDescriptorIndex, ensureDescriptor, extendSchemaWithOp, createOperationHandlers, invokeModuleTool, } from "./utils.js";
7
- import { ToolError, toolErrorResult, unknownErrorResult, } from "../errors.js";
7
+ import { ToolError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "../errors.js";
8
8
  import { jsonResult } from "../responses.js";
9
9
  const audioDescriptorIndex = buildDescriptorIndex(audioModule);
10
10
  const metaDescriptorIndex = buildDescriptorIndex(metaModule);
11
+ /** Largest encoded PCM payload one capture_samples response may carry. */
12
+ const MAX_SAMPLE_PAYLOAD_BYTES = 2 * 1024 * 1024;
11
13
  const captureSamplesArgsSchema = objectSchema({
12
14
  description: "Capture raw stereo PCM samples from the C64 Ultimate audio UDP stream.",
13
15
  properties: {
@@ -41,6 +43,10 @@ const soundOperations = [
41
43
  });
42
44
  const capture = await ctx.client.captureSamples({ count: parsed.count });
43
45
  const samplesBuffer = Buffer.from(capture.samples.buffer, capture.samples.byteOffset, capture.samples.byteLength);
46
+ const encodedBytes = parsed.encoding === "hex" ? samplesBuffer.length * 2 : Math.ceil(samplesBuffer.length / 3) * 4;
47
+ if (encodedBytes > MAX_SAMPLE_PAYLOAD_BYTES) {
48
+ throw new ToolValidationError(`Sample response would be ${encodedBytes} bytes, above the ${MAX_SAMPLE_PAYLOAD_BYTES} byte limit; lower count`, { path: "$.count", details: { count: parsed.count, encodedBytes, limitBytes: MAX_SAMPLE_PAYLOAD_BYTES } });
49
+ }
44
50
  return jsonResult({
45
51
  backend: capture.backend,
46
52
  channels: capture.channels,
@@ -156,6 +162,11 @@ export const soundModuleGroup = defineToolModule({
156
162
  description: "Sound operations available via the c64_sound tool.",
157
163
  variants: soundOperations.map((operation) => operation.schema),
158
164
  }),
165
+ operationEffects: {
166
+ capture_samples: "read", set_volume: "write", reset: "write", note_on: "write", note_off: "write",
167
+ silence_all: "write", play_sid_file: "write", play_mod_file: "write", generate: "write",
168
+ compile_play: "write", play_preset: "write", pipeline: "write", analyze: "read", record_analyze: "read",
169
+ },
159
170
  tags: ["sid", "audio", "grouped"],
160
171
  operationPlatforms: {
161
172
  capture_samples: ["c64u"],
@@ -5,7 +5,7 @@ const streamingDescriptorIndex = buildDescriptorIndex(streamingModule);
5
5
  const streamOperations = [
6
6
  {
7
7
  op: "start",
8
- schema: extendSchemaWithOp("start", ensureDescriptor(streamingDescriptorIndex, "stream_start").inputSchema, { description: "Start an Ultimate streaming session toward a host:port target." }),
8
+ schema: extendSchemaWithOp("start", ensureDescriptor(streamingDescriptorIndex, "stream_start").inputSchema, { description: "Start an Ultimate streaming session. The device sends an unencrypted UDP stream to the target (IPv4 address or hostname, optional :port)." }),
9
9
  handler: async (rawArgs, ctx) => invokeModuleTool(streamingModule, "stream_start", rawArgs, ctx),
10
10
  },
11
11
  {
@@ -34,6 +34,7 @@ export const streamModule = defineToolModule({
34
34
  description: "Streaming operations available via the c64_stream tool.",
35
35
  variants: streamOperations.map((operation) => operation.schema),
36
36
  }),
37
+ operationEffects: { start: "write", stop: "write" },
37
38
  tags: ["stream", "monitor", "grouped"],
38
39
  examples: [
39
40
  {
@@ -1,4 +1,5 @@
1
1
  import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema, } from "../types.js";
2
+ import { taskOperationEffect } from "../access.js";
2
3
  import { machineControlModule } from "../machineControl.js";
3
4
  import { metaModule } from "../meta/index.js";
4
5
  import { buildDescriptorIndex, ensureDescriptor, extendSchemaWithOp, createOperationHandlers, invokeModuleTool, } from "./utils.js";
@@ -27,7 +28,7 @@ const systemOperations = [
27
28
  },
28
29
  {
29
30
  op: "poweroff",
30
- schema: extendSchemaWithOp("poweroff", ensureDescriptor(machineDescriptorIndex, "poweroff").inputSchema, { description: "Shut down C64U/U64 or quit VICE. U2-family hardware cannot power off." }),
31
+ schema: extendSchemaWithOp("poweroff", ensureDescriptor(machineDescriptorIndex, "poweroff").inputSchema, { description: "Shut down C64U/U64 or quit VICE. U2-family hardware cannot power off. On C64U/U64 the machine cannot be restarted by software afterwards: power must be restored at the outlet, and it starts by itself only if automatic startup on power restoration is enabled." }),
31
32
  handler: async (rawArgs, ctx) => invokeModuleTool(machineControlModule, "poweroff", rawArgs, ctx),
32
33
  },
33
34
  {
@@ -92,6 +93,12 @@ export const systemModuleGroup = defineToolModule({
92
93
  description: "System operations available via the c64_system tool.",
93
94
  variants: systemOperations.map((operation) => operation.schema),
94
95
  }),
96
+ operationEffects: {
97
+ pause: "write", resume: "write", reset: "write", reboot: "destructive", poweroff: "destructive",
98
+ power_cycle: "destructive", menu: "write", read_menu_screen: "read", start_task: "write",
99
+ stop_task: "write", stop_all_tasks: "write", list_tasks: "read", performance_report: "read",
100
+ },
101
+ effectOf: (op, args) => (op === "start_task" ? taskOperationEffect(args.operation) : undefined),
95
102
  tags: ["system", "control", "grouped"],
96
103
  operationPlatforms: { poweroff: ["c64u", "vice"], pause: ["c64u", "u2"], resume: ["c64u", "u2"], menu: ["c64u", "u2"], read_menu_screen: ["c64u", "u2"] },
97
104
  operationToolNames: { poweroff: "poweroff", pause: "pause", resume: "resume", menu: "menu_button", read_menu_screen: "read_menu_screen" },
@@ -41,10 +41,10 @@ export function stringSchema(options = {}) {
41
41
  });
42
42
  const stringValue = value;
43
43
  if (options.minLength !== undefined && stringValue.length < options.minLength) {
44
- throw new ToolValidationError(`String must have length ≥ ${options.minLength}`, { path, details: { minLength: options.minLength } });
44
+ throw new ToolValidationError(`String must have length ≥ ${options.minLength}`, { path, details: { minLength: options.minLength, received: stringValue.length } });
45
45
  }
46
46
  if (options.maxLength !== undefined && stringValue.length > options.maxLength) {
47
- throw new ToolValidationError(`String must have length ≤ ${options.maxLength}`, { path, details: { maxLength: options.maxLength } });
47
+ throw new ToolValidationError(`String must have length ≤ ${options.maxLength}`, { path, details: { maxLength: options.maxLength, received: stringValue.length } });
48
48
  }
49
49
  if (options.pattern && !options.pattern.test(stringValue)) {
50
50
  throw new ToolValidationError("String does not match required pattern", {
@@ -55,7 +55,7 @@ export function stringSchema(options = {}) {
55
55
  if (options.enum && !options.enum.includes(stringValue)) {
56
56
  throw new ToolValidationError("Value must be one of the allowed options", {
57
57
  path,
58
- details: { allowed: options.enum },
58
+ details: { allowed: options.enum, received: stringValue },
59
59
  });
60
60
  }
61
61
  return stringValue;
@@ -86,13 +86,13 @@ export function numberSchema(options = {}) {
86
86
  if (options.minimum !== undefined && numberValue < options.minimum) {
87
87
  throw new ToolValidationError("Value is below minimum", {
88
88
  path,
89
- details: { minimum: options.minimum },
89
+ details: { minimum: options.minimum, received: numberValue },
90
90
  });
91
91
  }
92
92
  if (options.maximum !== undefined && numberValue > options.maximum) {
93
93
  throw new ToolValidationError("Value is above maximum", {
94
94
  path,
95
- details: { maximum: options.maximum },
95
+ details: { maximum: options.maximum, received: numberValue },
96
96
  });
97
97
  }
98
98
  return numberValue;
@@ -154,13 +154,13 @@ export function arraySchema(itemSchema, options = {}) {
154
154
  if (options.minItems !== undefined && result.length < options.minItems) {
155
155
  throw new ToolValidationError("Array has too few items", {
156
156
  path,
157
- details: { minItems: options.minItems },
157
+ details: { minItems: options.minItems, received: result.length },
158
158
  });
159
159
  }
160
160
  if (options.maxItems !== undefined && result.length > options.maxItems) {
161
161
  throw new ToolValidationError("Array has too many items", {
162
162
  path,
163
- details: { maxItems: options.maxItems },
163
+ details: { maxItems: options.maxItems, received: result.length },
164
164
  });
165
165
  }
166
166
  return result;
@@ -1,7 +1,7 @@
1
1
  import { defineToolModule } from "./types.js";
2
2
  import { objectSchema, stringSchema } from "./schema.js";
3
3
  import { textResult } from "./responses.js";
4
- import { ToolError, ToolExecutionError, toolErrorResult, unknownErrorResult, } from "./errors.js";
4
+ import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
5
5
  const streamIdentifierSchema = stringSchema({
6
6
  description: "Stream type to control (video/audio/debug).",
7
7
  enum: ["video", "audio", "debug"],
@@ -11,8 +11,8 @@ const streamStartArgsSchema = objectSchema({
11
11
  properties: {
12
12
  stream: streamIdentifierSchema,
13
13
  target: stringSchema({
14
- description: "Destination host:port or UDP target understood by the firmware.",
15
- minLength: 3,
14
+ description: "Destination of the UDP stream: IPv4 address or hostname with an optional :port (1-65535), e.g. 192.168.1.10, 192.168.1.10:11000 or pc.local:11000. The device sends the stream unencrypted over UDP to this address.",
15
+ minLength: 1,
16
16
  }),
17
17
  },
18
18
  required: ["stream", "target"],
@@ -26,6 +26,35 @@ const streamStopArgsSchema = objectSchema({
26
26
  required: ["stream"],
27
27
  additionalProperties: false,
28
28
  });
29
+ const STREAM_TARGET_FORMS = "an IPv4 address or hostname with an optional :port (1-65535), e.g. 192.168.1.10, 192.168.1.10:11000 or pc.local:11000";
30
+ const HOST_LABEL = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
31
+ export function validateStreamTarget(target) {
32
+ const fail = () => {
33
+ throw new ToolValidationError(`Invalid parameter 'target' (${JSON.stringify(target)}): expected ${STREAM_TARGET_FORMS}.`, { path: "target" });
34
+ };
35
+ const parts = target.split(":");
36
+ if (parts.length > 2)
37
+ fail();
38
+ const [host, port] = parts;
39
+ if (port !== undefined) {
40
+ if (!/^\d{1,5}$/.test(port))
41
+ fail();
42
+ const n = Number(port);
43
+ if (n < 1 || n > 65535)
44
+ fail();
45
+ }
46
+ if (!host || host.length > 253)
47
+ fail();
48
+ const labels = host.split(".");
49
+ if (!labels.every((label) => HOST_LABEL.test(label)))
50
+ fail();
51
+ if (/^[\d.]+$/.test(host)) {
52
+ // Numeric-only: must be a proper dotted IPv4 address.
53
+ const octets = labels;
54
+ if (octets.length !== 4 || !octets.every((o) => /^\d{1,3}$/.test(o) && Number(o) <= 255))
55
+ fail();
56
+ }
57
+ }
29
58
  function toRecord(details) {
30
59
  if (details && typeof details === "object") {
31
60
  return details;
@@ -59,7 +88,7 @@ export const streamingModule = defineToolModule({
59
88
  tools: [
60
89
  {
61
90
  name: "stream_start",
62
- description: "Start an Ultimate streaming session (video/audio/debug) targeting a host:port destination. See c64://guide/index for usage notes.",
91
+ description: "Start an Ultimate streaming session (video/audio/debug) sending an unencrypted UDP stream from the device to an IPv4 address or hostname with an optional :port. See c64://guide/index for usage notes.",
63
92
  summary: "Validates stream arguments and forwards the request to firmware, returning status metadata.",
64
93
  inputSchema: streamStartArgsSchema.jsonSchema,
65
94
  tags: ["stream", "start"],
@@ -74,6 +103,7 @@ export const streamingModule = defineToolModule({
74
103
  async execute(args, ctx) {
75
104
  try {
76
105
  const parsed = streamStartArgsSchema.parse(args ?? {});
106
+ validateStreamTarget(parsed.target);
77
107
  ctx.logger.info("Starting Ultimate stream", {
78
108
  stream: parsed.stream,
79
109
  target: parsed.target,