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
@@ -22,14 +22,39 @@ export class ToolExecutionError extends ToolError {
22
22
  super(message, "execution", options);
23
23
  }
24
24
  }
25
+ function joinBackends(backends) {
26
+ return backends.join(" or ");
27
+ }
28
+ export function describeUnsupportedPlatform(subject, platform, supported, configured) {
29
+ const base = supported.length > 0
30
+ ? `${subject} requires backend ${joinBackends(supported)} (active: ${platform}).`
31
+ : `${subject} is not available on backend ${platform}.`;
32
+ if (supported.length === 0) {
33
+ return base;
34
+ }
35
+ if (configured === undefined) {
36
+ return base;
37
+ }
38
+ const switchable = supported.filter((id) => id !== platform && configured.includes(id));
39
+ if (switchable.length > 0) {
40
+ return `${base} Use \`c64_select_backend\` to switch to ${joinBackends(switchable)}.`;
41
+ }
42
+ return `${base} None of these backends is configured on this server.`;
43
+ }
25
44
  export class ToolUnsupportedPlatformError extends ToolExecutionError {
26
- constructor(tool, platform, supported) {
27
- super(`Tool ${tool} is not available on platform ${platform}`, {
45
+ constructor(tool, platform, supported, options) {
46
+ const subject = options?.toolName
47
+ ? options.op
48
+ ? `\`${options.toolName}\` op \`${options.op}\``
49
+ : `\`${options.toolName}\``
50
+ : tool;
51
+ super(describeUnsupportedPlatform(subject, platform, supported, options?.configured), {
28
52
  code: "unsupported_platform",
29
53
  details: {
30
54
  tool,
31
55
  platform,
32
56
  supported,
57
+ ...(options?.configured ? { configured: options.configured } : {}),
33
58
  },
34
59
  });
35
60
  this.tool = tool;
@@ -48,7 +73,9 @@ export function toolErrorResult(error) {
48
73
  // Clients often show only the text content, so surface the underlying cause
49
74
  // (for example a range check or firmware error string) next to the summary.
50
75
  const detailMessage = describeDetailMessage(error.details);
51
- const message = detailMessage && !located.includes(detailMessage) ? `${located}: ${detailMessage}` : located;
76
+ const withDetail = detailMessage && !located.includes(detailMessage) ? `${located}: ${detailMessage}` : located;
77
+ const limits = describeLimits(error.details);
78
+ const message = limits ? `${withDetail} [${limits}]` : withDetail;
52
79
  const base = textResult(message, { error: metadata });
53
80
  return { ...base, isError: true };
54
81
  }
@@ -59,6 +86,30 @@ function describeDetailMessage(details) {
59
86
  const message = details.message;
60
87
  return typeof message === "string" && message.trim().length > 0 ? message.trim() : undefined;
61
88
  }
89
+ const MAX_LISTED_VALUES = 40;
90
+ // Valid values and bounds recorded by validators, so a client sees them without reading the schema.
91
+ function describeLimits(details) {
92
+ if (!details || typeof details !== "object") {
93
+ return undefined;
94
+ }
95
+ const record = details;
96
+ const parts = [];
97
+ if (Array.isArray(record.allowed) && record.allowed.length > 0) {
98
+ const values = record.allowed.map((value) => String(value));
99
+ const shown = values.slice(0, MAX_LISTED_VALUES).join(", ");
100
+ parts.push(values.length > MAX_LISTED_VALUES ? `allowed: ${shown}, ... (${values.length} total)` : `allowed: ${shown}`);
101
+ }
102
+ for (const key of ["minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems"]) {
103
+ const value = record[key];
104
+ if (typeof value === "number" || typeof value === "string") {
105
+ parts.push(`${key}: ${value}`);
106
+ }
107
+ }
108
+ if (parts.length > 0 && record.received !== undefined && typeof record.received !== "object") {
109
+ parts.push(`received: ${JSON.stringify(record.received)}`);
110
+ }
111
+ return parts.length > 0 ? parts.join("; ") : undefined;
112
+ }
62
113
  export function unknownErrorResult(error) {
63
114
  if (error instanceof ToolError) {
64
115
  return toolErrorResult(error);
@@ -1,7 +1,7 @@
1
1
  import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema, } from "./types.js";
2
2
  import { arraySchema, literalSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "./schema.js";
3
3
  import { jsonResult, textResult } from "./responses.js";
4
- import { ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
4
+ import { ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
5
5
  function isMissingNativeInputEndpoint(error) {
6
6
  return typeof error === "object"
7
7
  && error !== null
@@ -9,7 +9,7 @@ function isMissingNativeInputEndpoint(error) {
9
9
  && error.response?.status === 404;
10
10
  }
11
11
  function nativeInputUnavailableResult(operation) {
12
- return textResult(`Native physical input is unavailable on this firmware, so ${operation} was not sent. Use c64://platform/status before choosing native input; use key or write_text for ordinary C64 input.`, { success: false, code: "native_input_unavailable", fallback: "key_or_write_text" });
12
+ return toolErrorResult(new ToolExecutionError(`Native physical input is unavailable on this firmware, so ${operation} was not sent. Use c64://platform/status before choosing native input; use key or write_text for ordinary C64 input.`, { code: "native_input_unavailable", details: { fallback: "key_or_write_text" } }));
13
13
  }
14
14
  // ---------------------------------------------------------------------------
15
15
  // PETSCII token expansion table
@@ -69,10 +69,26 @@ const PETSCII_TOKENS = {
69
69
  // ASCII a-z are shifted graphics characters in PETSCII, so typed "print" would
70
70
  // reach BASIC as graphics and give ?SYNTAX ERROR. Literal letters are therefore
71
71
  // sent as the unshifted PETSCII letters $41-$5A; {tokens} keep their codes.
72
+ // Newline is RETURN. Any other character without a PETSCII equivalent, and any
73
+ // unknown {TOKEN}, is rejected instead of being typed as something else.
74
+ function describeChar(char) {
75
+ const codePoint = char.codePointAt(0) ?? 0;
76
+ return `'${codePoint < 0x20 ? "" : char}' (U+${codePoint.toString(16).toUpperCase().padStart(4, "0")})`;
77
+ }
72
78
  function expandPetsciiTokens(text) {
73
- return text.replace(/\{([^}]+)\}|[a-z]/g, (_match, token) => {
79
+ return text.replace(/\{([^}]+)\}|\r\n|[\s\S]/gu, (match, token, offset) => {
74
80
  if (token === undefined) {
75
- return _match.toUpperCase();
81
+ if (match === "\n" || match === "\r" || match === "\r\n") {
82
+ return String.fromCharCode(13);
83
+ }
84
+ const code = match.codePointAt(0) ?? 0;
85
+ if (code >= 0x61 && code <= 0x7a) {
86
+ return match.toUpperCase();
87
+ }
88
+ if (code >= 0x20 && code <= 0x7e) {
89
+ return match;
90
+ }
91
+ throw new ToolValidationError(`Character ${describeChar(match)} at position ${offset + 1} of the text has no PETSCII mapping. Use printable ASCII, \\n for RETURN, or a {TOKEN}.`, { path: "$.text" });
76
92
  }
77
93
  const upper = token.trim().toUpperCase();
78
94
  const code = PETSCII_TOKENS[upper];
@@ -90,7 +106,7 @@ function expandPetsciiTokens(text) {
90
106
  if (!isNaN(num) && num >= 0 && num <= 255) {
91
107
  return String.fromCharCode(num);
92
108
  }
93
- return _match.toUpperCase(); // type unrecognised tokens literally
109
+ throw new ToolValidationError(`Unknown token {${token}} at position ${offset + 1} of the text. Valid tokens: ${Object.keys(PETSCII_TOKENS).map((name) => `{${name}}`).join(", ")}, or a numeric code such as {$1D} or {29} (0-255).`, { path: "$.text" });
94
110
  });
95
111
  }
96
112
  // ---------------------------------------------------------------------------
@@ -131,7 +147,7 @@ export const writeTextArgsSchema = objectSchema({
131
147
  properties: {
132
148
  op: literalSchema("write_text"),
133
149
  text: stringSchema({
134
- description: "Text to type. Use {RETURN}, {CLR}, {F1}…{F8}, {UP/DOWN/LEFT/RIGHT}, colour names, etc.",
150
+ description: "Text to type: printable ASCII (letters are sent as PETSCII upper case), \\n for RETURN, and {TOKEN}s such as {RETURN}, {CLR}, {F1}…{F8}, {UP}, {DOWN}, {LEFT}, {RIGHT}, colour names, or {$1D}/{29} codes. Characters with no PETSCII mapping and unknown tokens are rejected.",
135
151
  minLength: 1,
136
152
  maxLength: 512,
137
153
  }),
@@ -147,7 +163,7 @@ export const writeTextArgsSchema = objectSchema({
147
163
  additionalProperties: false,
148
164
  });
149
165
  export const keyArgsSchema = objectSchema({
150
- description: "Tap a single key or hold it for a duration.",
166
+ description: "Press a single key one or more times, optionally with a delay between presses.",
151
167
  properties: {
152
168
  op: literalSchema("key"),
153
169
  key: stringSchema({
@@ -270,7 +286,7 @@ const inputOperationHandlers = {
270
286
  const keyByte = tokenCode !== undefined
271
287
  ? tokenCode
272
288
  : parsed.key.length === 1
273
- ? parsed.key.toUpperCase().charCodeAt(0) & 0xff
289
+ ? expandPetsciiTokens(parsed.key).charCodeAt(0)
274
290
  : (() => {
275
291
  throw new ToolValidationError(`Unrecognised key: ${parsed.key}. Use a single character or a PETSCII token name.`, { path: "$.key" });
276
292
  })();
@@ -417,6 +433,7 @@ export const inputModule = defineToolModule({
417
433
  ],
418
434
  }),
419
435
  operationPlatforms: { joystick: ["c64u", "vice"], keyboard: ["c64u"], release_all: ["c64u"], state: ["c64u"] },
436
+ operationEffects: { write_text: "write", key: "write", keyboard: "write", joystick: "write", release_all: "write", state: "read" },
420
437
  tags: ["input", "keyboard", "joystick"],
421
438
  examples: [
422
439
  {
@@ -1,5 +1,6 @@
1
1
  import { defineToolModule } from "./types.js";
2
- import { objectSchema } from "./schema.js";
2
+ import { booleanSchema, numberSchema, objectSchema, optionalSchema } from "./schema.js";
3
+ import { waitForScreenText, MAX_WAIT_TIMEOUT_MS } from "./meta/screen.js";
3
4
  import { jsonResult, textResult } from "./responses.js";
4
5
  import { Buffer } from "node:buffer";
5
6
  import { ToolError, ToolExecutionError, toolErrorResult, unknownErrorResult, } from "./errors.js";
@@ -31,14 +32,104 @@ function isMissingMenuScreenEndpoint(error) {
31
32
  && "response" in error
32
33
  && error.response?.status === 404;
33
34
  }
34
- const resetArgsSchema = createNoArgsSchema("No arguments required to reset the C64.");
35
- const rebootArgsSchema = createNoArgsSchema("No arguments required to reboot the C64 firmware.");
35
+ function createWaitSchema(description) {
36
+ return objectSchema({
37
+ description,
38
+ properties: {
39
+ waitForReady: optionalSchema(booleanSchema({
40
+ description: "Wait until the BASIC READY. prompt is visible before returning. A timeout is reported as ready=false, not as an error.",
41
+ default: false,
42
+ }), false),
43
+ readyTimeoutMs: optionalSchema(numberSchema({
44
+ description: "Maximum time to wait for READY. when waitForReady is true (1-120000 ms).",
45
+ integer: true,
46
+ minimum: 1,
47
+ maximum: MAX_WAIT_TIMEOUT_MS,
48
+ default: 15000,
49
+ }), 15000),
50
+ },
51
+ additionalProperties: false,
52
+ });
53
+ }
54
+ const READY_SETTLE_MS = 300;
55
+ /** After this long, a screen identical to the one before the reset counts as the new boot screen. */
56
+ const READY_UNCHANGED_GRACE_MS = 3000;
57
+ const SNAPSHOT_TIMEOUT_MS = 2000;
58
+ /** Resolves to undefined when the operation does not settle within `ms`; the late result is ignored. */
59
+ async function withinMs(operation, ms) {
60
+ let timer;
61
+ const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve(undefined), Math.max(0, ms)); });
62
+ try {
63
+ return await Promise.race([operation, timeout]);
64
+ }
65
+ finally {
66
+ if (timer)
67
+ clearTimeout(timer);
68
+ }
69
+ }
70
+ /** Screen text before the reset, used to tell a stale READY. from the new one. Best effort: undefined when unreadable. */
71
+ async function screenBeforeReset(ctx, parsed) {
72
+ if (parsed.waitForReady !== true)
73
+ return undefined;
74
+ try {
75
+ return await withinMs(ctx.client.readScreen(), SNAPSHOT_TIMEOUT_MS);
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ }
81
+ /**
82
+ * Polls the screen until READY. appears or the timeout elapses. Never throws.
83
+ * A screen identical to the one read before the reset is ignored for the first
84
+ * few seconds, so the READY. still visible from before the reset is not taken
85
+ * for the new one. Each read is bounded by the remaining time.
86
+ */
87
+ async function waitForReadyPrompt(ctx, timeoutMs, before) {
88
+ const start = Date.now();
89
+ await new Promise((resolve) => setTimeout(resolve, Math.min(READY_SETTLE_MS, timeoutMs)));
90
+ let lastError;
91
+ const tolerantClient = {
92
+ async readScreen() {
93
+ const remaining = timeoutMs - (Date.now() - start);
94
+ try {
95
+ const screen = await withinMs(ctx.client.readScreen(), remaining);
96
+ if (screen === undefined)
97
+ return "";
98
+ lastError = undefined;
99
+ if (before !== undefined && screen === before && Date.now() - start < READY_UNCHANGED_GRACE_MS)
100
+ return "";
101
+ return screen;
102
+ }
103
+ catch (error) {
104
+ lastError = error instanceof Error ? error.message : String(error);
105
+ return ""; // device not reachable yet
106
+ }
107
+ },
108
+ };
109
+ const remaining = Math.max(1, timeoutMs - (Date.now() - start));
110
+ const outcome = await waitForScreenText(tolerantClient, { pattern: "READY.", timeoutMs: remaining, intervalMs: 250 });
111
+ return { ready: outcome.matched, readyElapsedMs: Date.now() - start, ...(outcome.matched || !lastError ? {} : { lastError }) };
112
+ }
113
+ async function withReadyResult(ctx, parsed, before, baseMessage, details) {
114
+ if (parsed.waitForReady !== true) {
115
+ return textResult(baseMessage, { success: true, details });
116
+ }
117
+ const { ready, readyElapsedMs, lastError } = await waitForReadyPrompt(ctx, parsed.readyTimeoutMs ?? 15000, before);
118
+ const message = ready
119
+ ? `${baseMessage} READY. prompt reached after ${readyElapsedMs} ms.`
120
+ : `${baseMessage} The READY. prompt was not seen within ${readyElapsedMs} ms; the machine may still be starting.`
121
+ + (lastError ? ` The last screen read failed: ${lastError}` : "");
122
+ return textResult(message, { success: true, details, ready, readyElapsedMs, ...(lastError ? { readyError: lastError } : {}) });
123
+ }
124
+ const resetArgsSchema = createWaitSchema("Reset the C64. Optionally wait for the READY. prompt.");
125
+ const rebootArgsSchema = createWaitSchema("Reboot the C64 firmware. Optionally wait for the READY. prompt.");
36
126
  const pauseArgsSchema = createNoArgsSchema("No arguments required to pause the machine.");
37
127
  const resumeArgsSchema = createNoArgsSchema("No arguments required to resume the machine after a pause.");
38
128
  const poweroffArgsSchema = createNoArgsSchema("No arguments required to power off the machine.");
39
- const powerCycleArgsSchema = createNoArgsSchema("No arguments required to power cycle the active system.");
129
+ const powerCycleArgsSchema = createWaitSchema("Power cycle the active system. Optionally wait for the READY. prompt.");
40
130
  const menuButtonArgsSchema = createNoArgsSchema("No arguments required to toggle the Ultimate menu button.");
41
131
  const readMenuScreenArgsSchema = createNoArgsSchema("No arguments required to read the active Ultimate menu screen matrix.");
132
+ const POWER_RESTORE_NOTE = "The machine cannot be restarted by software afterwards: power must be restored at the outlet, and the machine starts by itself only if automatic startup on power restoration is enabled.";
42
133
  export const machineControlModule = defineToolModule({
43
134
  domain: "machine",
44
135
  summary: "Power, reset, pause/resume, and diagnostic controls for the C64 and Ultimate hardware.",
@@ -66,8 +157,9 @@ export const machineControlModule = defineToolModule({
66
157
  supportedPlatforms: ["c64u", "u2", "vice"],
67
158
  async execute(args, ctx) {
68
159
  try {
69
- resetArgsSchema.parse(args ?? {});
160
+ const parsed = resetArgsSchema.parse(args ?? {});
70
161
  ctx.logger.info("Resetting C64");
162
+ const before = await screenBeforeReset(ctx, parsed);
71
163
  const result = await ctx.client.reset();
72
164
  if (!result.success) {
73
165
  throw new ToolExecutionError("C64 backend reported failure while resetting", {
@@ -75,10 +167,7 @@ export const machineControlModule = defineToolModule({
75
167
  });
76
168
  }
77
169
  const details = toRecord(result.details) ?? {};
78
- return textResult("C64 reset command issued successfully.", {
79
- success: true,
80
- details,
81
- });
170
+ return await withReadyResult(ctx, parsed, before, "C64 reset command issued successfully.", details);
82
171
  }
83
172
  catch (error) {
84
173
  if (error instanceof ToolError) {
@@ -104,8 +193,9 @@ export const machineControlModule = defineToolModule({
104
193
  supportedPlatforms: ["c64u", "u2", "vice"],
105
194
  async execute(args, ctx) {
106
195
  try {
107
- rebootArgsSchema.parse(args ?? {});
196
+ const parsed = rebootArgsSchema.parse(args ?? {});
108
197
  ctx.logger.info("Rebooting C64");
198
+ const before = await screenBeforeReset(ctx, parsed);
109
199
  const result = await ctx.client.reboot();
110
200
  if (!result.success) {
111
201
  throw new ToolExecutionError("C64 backend reported failure while rebooting", {
@@ -113,10 +203,7 @@ export const machineControlModule = defineToolModule({
113
203
  });
114
204
  }
115
205
  const details = toRecord(result.details) ?? {};
116
- return textResult("C64 reboot command issued successfully.", {
117
- success: true,
118
- details,
119
- });
206
+ return await withReadyResult(ctx, parsed, before, "C64 reboot command issued successfully.", details);
120
207
  }
121
208
  catch (error) {
122
209
  if (error instanceof ToolError) {
@@ -144,7 +231,8 @@ export const machineControlModule = defineToolModule({
144
231
  try {
145
232
  pauseArgsSchema.parse(args ?? {});
146
233
  ctx.logger.info("Pausing C64 execution");
147
- const result = await ctx.client.pause();
234
+ // An explicit pause stays in effect until an explicit resume, even if tool calls pause and resume meanwhile.
235
+ const result = ctx.client.explicitPause ? await ctx.client.explicitPause(await ctx.client.pinFacade?.()) : await ctx.client.pause();
148
236
  if (!result.success) {
149
237
  throw new ToolExecutionError("C64 backend reported failure while pausing", {
150
238
  details: normaliseFailure(result.details),
@@ -182,7 +270,7 @@ export const machineControlModule = defineToolModule({
182
270
  try {
183
271
  resumeArgsSchema.parse(args ?? {});
184
272
  ctx.logger.info("Resuming C64 execution");
185
- const result = await ctx.client.resume();
273
+ const result = ctx.client.explicitResume ? await ctx.client.explicitResume(await ctx.client.pinFacade?.()) : await ctx.client.resume();
186
274
  if (!result.success) {
187
275
  throw new ToolExecutionError("C64 backend reported failure while resuming", {
188
276
  details: normaliseFailure(result.details),
@@ -204,7 +292,7 @@ export const machineControlModule = defineToolModule({
204
292
  },
205
293
  {
206
294
  name: "poweroff",
207
- description: "Power off the machine via Ultimate firmware. See safety notes in c64://guide/bootstrap.",
295
+ description: `Power off the machine via Ultimate firmware. See safety notes in c64://guide/bootstrap. ${POWER_RESTORE_NOTE}`,
208
296
  summary: "Attempts a controlled shutdown through the Ultimate control interface.",
209
297
  inputSchema: poweroffArgsSchema.jsonSchema,
210
298
  tags: ["power"],
@@ -227,7 +315,10 @@ export const machineControlModule = defineToolModule({
227
315
  });
228
316
  }
229
317
  const details = toRecord(result.details) ?? {};
230
- return textResult("Power off command acknowledged.", {
318
+ const message = ctx.platform?.id === "vice"
319
+ ? "Power off command acknowledged."
320
+ : `Power off command acknowledged. ${POWER_RESTORE_NOTE}`;
321
+ return textResult(message, {
231
322
  success: true,
232
323
  details,
233
324
  });
@@ -257,8 +348,9 @@ export const machineControlModule = defineToolModule({
257
348
  supportedPlatforms: ["c64u", "u2", "vice"],
258
349
  async execute(args, ctx) {
259
350
  try {
260
- powerCycleArgsSchema.parse(args ?? {});
351
+ const parsed = powerCycleArgsSchema.parse(args ?? {});
261
352
  ctx.logger.info("Power cycling active backend");
353
+ const before = await screenBeforeReset(ctx, parsed);
262
354
  const result = await ctx.client.powerCycle();
263
355
  if (!result.success) {
264
356
  throw new ToolExecutionError("Power cycle did not complete", {
@@ -266,7 +358,7 @@ export const machineControlModule = defineToolModule({
266
358
  });
267
359
  }
268
360
  const details = toRecord(result.details) ?? {};
269
- return textResult("Power-cycle request completed.", { success: true, details });
361
+ return await withReadyResult(ctx, parsed, before, "Power-cycle request completed.", details);
270
362
  }
271
363
  catch (error) {
272
364
  if (error instanceof ToolError)
@@ -342,7 +434,7 @@ export const machineControlModule = defineToolModule({
342
434
  if (error instanceof ToolError)
343
435
  return toolErrorResult(error);
344
436
  if (isMissingMenuScreenEndpoint(error)) {
345
- return textResult("No menu matrix is available. Open the Ultimate menu and retry; if it remains unavailable, this firmware does not provide machine:menu_screen. Use REST configuration instead of menu navigation where possible.", { success: false, code: "native_menu_screen_unavailable", fallback: "rest_configuration_or_manual_menu" });
437
+ return toolErrorResult(new ToolExecutionError("No menu matrix is available. Open the Ultimate menu and retry; if it remains unavailable, this firmware does not provide machine:menu_screen. Use REST configuration instead of menu navigation where possible.", { code: "native_menu_screen_unavailable", details: { fallback: "rest_configuration_or_manual_menu" }, cause: error }));
346
438
  }
347
439
  return unknownErrorResult(error);
348
440
  }