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.
- package/.github/skills/assembly-program/SKILL.md +37 -0
- package/.github/skills/basic-program/SKILL.md +40 -0
- package/.github/skills/cross-platform-demo/SKILL.md +32 -0
- package/.github/skills/drive-manager/SKILL.md +33 -0
- package/.github/skills/graphics-demo/SKILL.md +30 -0
- package/.github/skills/hello-world/SKILL.md +36 -0
- package/.github/skills/memory-debug/SKILL.md +32 -0
- package/.github/skills/pr-converge/SKILL.md +33 -0
- package/.github/skills/printer-job/SKILL.md +30 -0
- package/.github/skills/sid-music/SKILL.md +35 -0
- package/.github/skills/software-launch/SKILL.md +31 -0
- package/.github/skills/stream-control/SKILL.md +29 -0
- package/.github/skills/system-control/SKILL.md +32 -0
- package/AGENTS.md +28 -7
- package/README.md +70 -15
- package/data/assembly/assembly-spec.md +2 -0
- package/dist/c64Client.js +238 -60
- package/dist/chargen.js +0 -4
- package/dist/config.js +53 -28
- package/dist/device.js +92 -37
- package/dist/diagnostics.js +66 -5
- package/dist/hostPaths.js +128 -0
- package/dist/logger.js +3 -2
- package/dist/loggingHttpClient.js +71 -2
- package/dist/mcp-server.js +416 -291
- package/dist/numericArg.js +55 -0
- package/dist/petscii.js +13 -6
- package/dist/platform.js +12 -3
- package/dist/prompts/registry.js +6 -1
- package/dist/redact.js +166 -0
- package/dist/tools/access.js +66 -0
- package/dist/tools/debug.js +6 -0
- package/dist/tools/developer.js +77 -36
- package/dist/tools/errors.js +54 -3
- package/dist/tools/input.js +25 -8
- package/dist/tools/machineControl.js +113 -21
- package/dist/tools/memory.js +135 -30
- package/dist/tools/meta/artifacts.js +5 -5
- package/dist/tools/meta/background.js +216 -54
- package/dist/tools/meta/compilation.js +6 -5
- package/dist/tools/meta/config.js +20 -10
- package/dist/tools/meta/configInventory.js +25 -2
- package/dist/tools/meta/diagnostics.js +2 -1
- package/dist/tools/meta/filesystem.js +16 -14
- package/dist/tools/meta/graphics.js +48 -20
- package/dist/tools/meta/memory.js +45 -19
- package/dist/tools/meta/program.js +10 -7
- package/dist/tools/meta/screen.js +116 -25
- package/dist/tools/pauseGuard.js +23 -0
- package/dist/tools/pollValidator.js +53 -8
- package/dist/tools/programRunners.js +62 -18
- package/dist/tools/registry/batch.js +17 -11
- package/dist/tools/registry/config.js +9 -2
- package/dist/tools/registry/disk.js +12 -2
- package/dist/tools/registry/drive.js +3 -0
- package/dist/tools/registry/extract.js +3 -0
- package/dist/tools/registry/graphics.js +59 -6
- package/dist/tools/registry/index.js +17 -1
- package/dist/tools/registry/memory.js +11 -1
- package/dist/tools/registry/platform.js +4 -1
- package/dist/tools/registry/printer.js +2 -1
- package/dist/tools/registry/program.js +5 -1
- package/dist/tools/registry/rag.js +1 -0
- package/dist/tools/registry/sound.js +12 -1
- package/dist/tools/registry/stream.js +2 -1
- package/dist/tools/registry/system.js +8 -1
- package/dist/tools/schema.js +7 -7
- package/dist/tools/streaming.js +34 -4
- package/dist/tools/translation/assembler.js +247 -24
- package/dist/tools/translation/basicDetokenizer.js +80 -0
- package/dist/tools/translation/basicTokenizer.js +8 -3
- package/dist/tools/translation/disassembler.js +14 -0
- package/dist/tools/types.js +258 -7
- package/dist/tools/vice.js +5 -16
- package/dist/vicBitmap.js +1 -1
- package/dist/vice/mockServer.js +3 -0
- package/dist/vice/process.js +19 -0
- package/dist/vice/resourcePolicy.js +36 -0
- package/dist/vice/viceClient.js +22 -5
- package/doc/vice/support-matrix.md +17 -11
- package/mcp/schemas/c64_batch.schema.json +2 -1
- package/mcp/schemas/c64_config.schema.json +33 -466
- package/mcp/schemas/c64_debug.schema.json +3 -602
- package/mcp/schemas/c64_disk.schema.json +17 -344
- package/mcp/schemas/c64_drive.schema.json +2 -157
- package/mcp/schemas/c64_extract.schema.json +10 -364
- package/mcp/schemas/c64_graphics.schema.json +19 -426
- package/mcp/schemas/c64_input.schema.json +5 -318
- package/mcp/schemas/c64_memory.schema.json +23 -491
- package/mcp/schemas/c64_printer.schema.json +3 -258
- package/mcp/schemas/c64_program.schema.json +16 -491
- package/mcp/schemas/c64_rag.schema.json +1 -81
- package/mcp/schemas/c64_select_backend.schema.json +2 -36
- package/mcp/schemas/c64_sound.schema.json +20 -822
- package/mcp/schemas/c64_stream.schema.json +4 -75
- package/mcp/schemas/c64_system.schema.json +28 -348
- package/mcp/schemas/c64_vice.schema.json +2 -71
- package/mcp/server.json +1 -1
- package/mcp/tools.json +1990 -6900
- package/mcp.json +21 -1
- package/package.json +19 -8
- package/scripts/mockC64Server.mjs +44 -0
- package/scripts/run-coverage.mjs +27 -1
- package/scripts/smoke-run.sh +3 -2
- package/scripts/start.mjs +23 -1
- package/server.json +53 -3
- package/dist/.tsbuildinfo +0 -1
- package/doc/agent-state-spec.md +0 -243
- package/doc/plans/bun/baseline-metrics.md +0 -16
- package/doc/plans/bun/migration-checklist.md +0 -55
- package/doc/plans/bun/migration.md +0 -83
- package/doc/plans/improve-knowledge/notes.md +0 -43
- package/doc/plans/improve-knowledge/plan.md +0 -121
- package/doc/plans/improve-knowledge/rollout.md +0 -110
- package/doc/plans/meta-tools/README.md +0 -110
- package/doc/plans/meta-tools/analysis.md +0 -272
- package/doc/plans/meta-tools/catalog.md +0 -435
- package/doc/plans/meta-tools/notes.md +0 -192
- package/doc/plans/meta-tools/rollout.md +0 -368
- package/doc/plans/prompts/README.md +0 -96
- package/doc/plans/prompts/user-prompt-ideas.md +0 -147
- package/doc/plans/tool-consolidation/notes.md +0 -28
- package/doc/plans/tool-consolidation/plan.md +0 -169
- package/doc/plans/tool-consolidation/rollout.md +0 -104
- package/doc/plans/vice-backend-switch/plan.md +0 -486
- package/doc/plans/vice-backend-switch-2/plan.md +0 -48
- package/doc/plans/vice-support/notes.md +0 -63
- package/doc/plans/vice-support/plan.md +0 -88
- package/doc/plans/vice-support/rollout.md +0 -99
- package/doc/reviews/audit.md +0 -143
- package/doc/reviews/hardening/01-fable/prompt-handover.md +0 -125
- package/doc/reviews/hardening/01-fable/prompt.md +0 -98
- package/doc/reviews/hardening/01-fable/review-prompt.md +0 -468
- package/doc/reviews/hardening/01-fable/review.md +0 -1234
- package/doc/reviews/vice-support-review.md +0 -185
- package/scripts/compare-rest/rest-compare-2025-11-01T00-25-21-830Z.json +0 -2513
package/dist/tools/errors.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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);
|
package/dist/tools/input.js
CHANGED
|
@@ -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
|
|
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(/\{([^}]+)\}|[
|
|
79
|
+
return text.replace(/\{([^}]+)\}|\r\n|[\s\S]/gu, (match, token, offset) => {
|
|
74
80
|
if (token === undefined) {
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|