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/types.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getPlatformStatus, isPlatformSupported, setPlatform, } from "../platform.js";
|
|
2
2
|
import { withDiagnosticSpan } from "../diagnostics.js";
|
|
3
3
|
import { ToolUnsupportedPlatformError, ToolValidationError } from "./errors.js";
|
|
4
|
+
import { annotationsForEffects } from "./access.js";
|
|
4
5
|
export const OPERATION_DISCRIMINATOR = "op";
|
|
5
6
|
export const VERIFY_PROPERTY_NAME = "verify";
|
|
6
7
|
export const VERIFY_PROPERTY_SCHEMA = Object.freeze({
|
|
@@ -35,6 +36,7 @@ export function discriminatedUnionSchema(options) {
|
|
|
35
36
|
// so we merge all variant properties and use an enum for the discriminator.
|
|
36
37
|
const opValues = [];
|
|
37
38
|
const mergedProperties = {};
|
|
39
|
+
const propertyVariants = {};
|
|
38
40
|
const opDescriptions = [];
|
|
39
41
|
const operationMetadata = [];
|
|
40
42
|
for (const variant of variants) {
|
|
@@ -64,11 +66,14 @@ export function discriminatedUnionSchema(options) {
|
|
|
64
66
|
for (const [key, propSchema] of Object.entries(variantProps)) {
|
|
65
67
|
if (key === discriminator)
|
|
66
68
|
continue;
|
|
67
|
-
|
|
68
|
-
mergedProperties[key] = propSchema;
|
|
69
|
-
}
|
|
69
|
+
(propertyVariants[key] ?? (propertyVariants[key] = [])).push(propSchema);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
// Variants may declare the same property with different limits. The flattened
|
|
73
|
+
// property must accept every variant's values; per-op validation stays strict.
|
|
74
|
+
for (const [key, schemas] of Object.entries(propertyVariants)) {
|
|
75
|
+
mergedProperties[key] = combinePropertySchemas(schemas);
|
|
76
|
+
}
|
|
72
77
|
const opPropertySchema = opValues.length > 0
|
|
73
78
|
? { type: "string", enum: opValues, description: `Selects the operation.` }
|
|
74
79
|
: { type: "string", description: `Selects the operation.` };
|
|
@@ -92,6 +97,170 @@ export function discriminatedUnionSchema(options) {
|
|
|
92
97
|
};
|
|
93
98
|
return schema;
|
|
94
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Returns the input schema as published on the wire. The per-operation schemas in
|
|
102
|
+
* "x-c64bridge-operations" duplicate the property information of the flattened
|
|
103
|
+
* schema and roughly halve its size, so they are omitted unless a client opts in
|
|
104
|
+
* with C64BRIDGE_VERBOSE_SCHEMA=1. The registry keeps them for documentation generators.
|
|
105
|
+
*/
|
|
106
|
+
export function publishedInputSchema(schema, options = {}) {
|
|
107
|
+
const verbose = options.verbose ?? process.env.C64BRIDGE_VERBOSE_SCHEMA === "1";
|
|
108
|
+
if (!schema || verbose || !("x-c64bridge-operations" in schema)) {
|
|
109
|
+
return schema;
|
|
110
|
+
}
|
|
111
|
+
const { "x-c64bridge-operations": _operations, ...published } = schema;
|
|
112
|
+
return published;
|
|
113
|
+
}
|
|
114
|
+
const OP_LIMITS_NOTE = "Limits depend on op.";
|
|
115
|
+
const WIDENABLE_KEYS = new Set([
|
|
116
|
+
"type", "description", "enum", "const", "default", "minimum", "maximum", "minLength", "maxLength",
|
|
117
|
+
"minItems", "maxItems", "pattern", "format", "items", "examples",
|
|
118
|
+
]);
|
|
119
|
+
function deepEqual(a, b) {
|
|
120
|
+
if (a === b)
|
|
121
|
+
return true;
|
|
122
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
123
|
+
return false;
|
|
124
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
125
|
+
return false;
|
|
126
|
+
const aKeys = Object.keys(a);
|
|
127
|
+
const bKeys = Object.keys(b);
|
|
128
|
+
if (aKeys.length !== bKeys.length)
|
|
129
|
+
return false;
|
|
130
|
+
return aKeys.every((key) => deepEqual(a[key], b[key]));
|
|
131
|
+
}
|
|
132
|
+
function withoutDescription(schema) {
|
|
133
|
+
const { description: _description, ...rest } = schema;
|
|
134
|
+
return rest;
|
|
135
|
+
}
|
|
136
|
+
function typeSet(schema) {
|
|
137
|
+
if (schema.type === undefined) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const types = Array.isArray(schema.type) ? [...schema.type] : [schema.type];
|
|
141
|
+
return types;
|
|
142
|
+
}
|
|
143
|
+
function enumValues(schema) {
|
|
144
|
+
if (schema.enum)
|
|
145
|
+
return schema.enum;
|
|
146
|
+
if ("const" in schema)
|
|
147
|
+
return [schema.const];
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
function smallest(a, b) {
|
|
151
|
+
return a === undefined || b === undefined ? undefined : Math.min(a, b);
|
|
152
|
+
}
|
|
153
|
+
function largest(a, b) {
|
|
154
|
+
return a === undefined || b === undefined ? undefined : Math.max(a, b);
|
|
155
|
+
}
|
|
156
|
+
/** Returns a schema accepting everything either input accepts, or undefined when that cannot be expressed safely. */
|
|
157
|
+
function widenPair(a, b) {
|
|
158
|
+
const aRecord = a;
|
|
159
|
+
const bRecord = b;
|
|
160
|
+
for (const key of new Set([...Object.keys(aRecord), ...Object.keys(bRecord)])) {
|
|
161
|
+
if (!WIDENABLE_KEYS.has(key) && !deepEqual(aRecord[key], bRecord[key])) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const aTypes = typeSet(a);
|
|
166
|
+
const bTypes = typeSet(b);
|
|
167
|
+
let types;
|
|
168
|
+
if (aTypes && bTypes) {
|
|
169
|
+
types = Array.from(new Set([...aTypes, ...bTypes]));
|
|
170
|
+
if (types.includes("number")) {
|
|
171
|
+
types = types.filter((type) => type !== "integer");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Structured types only widen when their element schema widens.
|
|
175
|
+
let items;
|
|
176
|
+
if (a.items !== undefined || b.items !== undefined) {
|
|
177
|
+
if (!a.items || !b.items || Array.isArray(a.items) || Array.isArray(b.items)) {
|
|
178
|
+
if (!deepEqual(a.items, b.items))
|
|
179
|
+
return undefined;
|
|
180
|
+
items = a.items;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
items = combinePropertySchemas([a.items, b.items], false);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const aEnum = enumValues(a);
|
|
187
|
+
const bEnum = enumValues(b);
|
|
188
|
+
const enumUnion = aEnum && bEnum
|
|
189
|
+
? [...aEnum, ...bEnum.filter((value) => !aEnum.includes(value))]
|
|
190
|
+
: undefined;
|
|
191
|
+
const minimum = smallest(a.minimum, b.minimum);
|
|
192
|
+
const maximum = largest(a.maximum, b.maximum);
|
|
193
|
+
const minLength = smallest(a.minLength, b.minLength);
|
|
194
|
+
const maxLength = largest(a.maxLength, b.maxLength);
|
|
195
|
+
const minItems = smallest(a.minItems, b.minItems);
|
|
196
|
+
const maxItems = largest(a.maxItems, b.maxItems);
|
|
197
|
+
const result = {};
|
|
198
|
+
for (const [key, value] of Object.entries(a)) {
|
|
199
|
+
if (!WIDENABLE_KEYS.has(key))
|
|
200
|
+
result[key] = value;
|
|
201
|
+
}
|
|
202
|
+
if (types)
|
|
203
|
+
result.type = types.length === 1 ? types[0] : types;
|
|
204
|
+
if (a.description !== undefined || b.description !== undefined)
|
|
205
|
+
result.description = a.description ?? b.description;
|
|
206
|
+
if (enumUnion)
|
|
207
|
+
result.enum = enumUnion;
|
|
208
|
+
if (minimum !== undefined)
|
|
209
|
+
result.minimum = minimum;
|
|
210
|
+
if (maximum !== undefined)
|
|
211
|
+
result.maximum = maximum;
|
|
212
|
+
if (minLength !== undefined)
|
|
213
|
+
result.minLength = minLength;
|
|
214
|
+
if (maxLength !== undefined)
|
|
215
|
+
result.maxLength = maxLength;
|
|
216
|
+
if (minItems !== undefined)
|
|
217
|
+
result.minItems = minItems;
|
|
218
|
+
if (maxItems !== undefined)
|
|
219
|
+
result.maxItems = maxItems;
|
|
220
|
+
if (items)
|
|
221
|
+
result.items = items;
|
|
222
|
+
if (a.pattern !== undefined && a.pattern === b.pattern)
|
|
223
|
+
result.pattern = a.pattern;
|
|
224
|
+
if (a.format !== undefined && a.format === b.format)
|
|
225
|
+
result.format = a.format;
|
|
226
|
+
if (deepEqual(a.default, b.default) && a.default !== undefined)
|
|
227
|
+
result.default = a.default;
|
|
228
|
+
if (deepEqual(a.examples, b.examples) && a.examples !== undefined)
|
|
229
|
+
result.examples = a.examples;
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Combines the schemas that different operations declare for one property into a
|
|
234
|
+
* single schema accepting all of their values. Identical schemas are returned untouched.
|
|
235
|
+
*/
|
|
236
|
+
export function combinePropertySchemas(schemas, annotate = true) {
|
|
237
|
+
const first = schemas[0];
|
|
238
|
+
const distinct = [];
|
|
239
|
+
for (const schema of schemas) {
|
|
240
|
+
if (!distinct.some((existing) => deepEqual(withoutDescription(existing), withoutDescription(schema)))) {
|
|
241
|
+
distinct.push(schema);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (distinct.length === 1) {
|
|
245
|
+
return first;
|
|
246
|
+
}
|
|
247
|
+
let merged = distinct[0];
|
|
248
|
+
for (const schema of distinct.slice(1)) {
|
|
249
|
+
merged = merged ? widenPair(merged, schema) : undefined;
|
|
250
|
+
}
|
|
251
|
+
const result = merged ?? {
|
|
252
|
+
anyOf: distinct.map(withoutDescription),
|
|
253
|
+
...(first.description ? { description: first.description } : {}),
|
|
254
|
+
};
|
|
255
|
+
if (!annotate) {
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
const description = result.description;
|
|
259
|
+
if (description?.includes(OP_LIMITS_NOTE)) {
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
return { ...result, description: description ? `${description} ${OP_LIMITS_NOTE}` : OP_LIMITS_NOTE };
|
|
263
|
+
}
|
|
95
264
|
export function createOperationDispatcher(toolName, handlers) {
|
|
96
265
|
const allowed = Object.keys(handlers).sort();
|
|
97
266
|
return async (args, ctx) => {
|
|
@@ -107,7 +276,7 @@ export function createOperationDispatcher(toolName, handlers) {
|
|
|
107
276
|
// Own properties only: "constructor", "toString", etc. must not resolve to Object.prototype members.
|
|
108
277
|
const handler = hasOwnKey(handlers, opKey) ? handlers[opKey] : undefined;
|
|
109
278
|
if (!handler) {
|
|
110
|
-
throw new ToolValidationError(`${toolName} does not support ${OPERATION_DISCRIMINATOR} "${opValue}"`, { path: `$.${OPERATION_DISCRIMINATOR}`, details: { allowed } });
|
|
279
|
+
throw new ToolValidationError(`${toolName} does not support ${OPERATION_DISCRIMINATOR} "${opValue}"`, { path: `$.${OPERATION_DISCRIMINATOR}`, details: { allowed, received: opValue } });
|
|
111
280
|
}
|
|
112
281
|
return handler(record, ctx);
|
|
113
282
|
};
|
|
@@ -145,25 +314,40 @@ export function defineToolModule(config) {
|
|
|
145
314
|
...(prerequisites ? { prerequisites } : {}),
|
|
146
315
|
...(platforms ? { platforms } : {}),
|
|
147
316
|
...(tool.operationPlatforms ? { operationPlatforms: tool.operationPlatforms } : {}),
|
|
317
|
+
...(tool.operationEffects ? { operationEffects: tool.operationEffects } : {}),
|
|
318
|
+
...(tool.effect ? { effect: tool.effect } : {}),
|
|
148
319
|
};
|
|
320
|
+
const declaredEffects = tool.operationEffects ? Object.values(tool.operationEffects) : tool.effect ? [tool.effect] : [];
|
|
149
321
|
return {
|
|
150
322
|
name: tool.name,
|
|
151
323
|
description: tool.description,
|
|
152
|
-
inputSchema: tool.inputSchema,
|
|
324
|
+
inputSchema: tagOperations(tool.inputSchema, tool.operationPlatforms, tool.operationEffects),
|
|
325
|
+
annotations: annotationsForEffects(declaredEffects),
|
|
153
326
|
metadata,
|
|
154
327
|
};
|
|
155
328
|
});
|
|
156
329
|
},
|
|
330
|
+
effectOf(name, args) {
|
|
331
|
+
const tool = toolMap.get(name);
|
|
332
|
+
return tool ? resolveToolEffect(tool, args) : undefined;
|
|
333
|
+
},
|
|
157
334
|
async invoke(name, args, ctx) {
|
|
158
335
|
const tool = toolMap.get(name);
|
|
159
336
|
if (!tool) {
|
|
160
|
-
throw new
|
|
337
|
+
throw new ToolValidationError(`Unknown tool: ${name}`, {
|
|
338
|
+
path: "$.name",
|
|
339
|
+
details: { allowed: Array.from(toolMap.keys()).sort(), received: name },
|
|
340
|
+
});
|
|
161
341
|
}
|
|
162
342
|
const platforms = resolveSupportedPlatforms(tool, args, defaultPlatforms);
|
|
163
343
|
const status = ctx.platform ?? getPlatformStatus();
|
|
164
344
|
const setter = ctx.setPlatform ?? setPlatform;
|
|
165
345
|
if (!isPlatformSupported(status.id, platforms)) {
|
|
166
|
-
throw new ToolUnsupportedPlatformError(resolveUnsupportedToolName(name, tool, args), status.id, platforms
|
|
346
|
+
throw new ToolUnsupportedPlatformError(resolveUnsupportedToolName(name, tool, args), status.id, platforms, {
|
|
347
|
+
toolName: name,
|
|
348
|
+
op: getSelectedOperation(args),
|
|
349
|
+
configured: configuredBackends(ctx),
|
|
350
|
+
});
|
|
167
351
|
}
|
|
168
352
|
const enrichedCtx = {
|
|
169
353
|
...ctx,
|
|
@@ -179,6 +363,73 @@ export function defineToolModule(config) {
|
|
|
179
363
|
},
|
|
180
364
|
};
|
|
181
365
|
}
|
|
366
|
+
const ALL_PLATFORMS = ["c64u", "u2", "vice"];
|
|
367
|
+
/**
|
|
368
|
+
* Marks operations in the operation list of a grouped tool description: operations that are not
|
|
369
|
+
* available on every backend get their backend list and operations that are not read-only get
|
|
370
|
+
* their effect, for example "- unmount [c64u, u2] [write]: ...".
|
|
371
|
+
*/
|
|
372
|
+
export function tagOperations(schema, operationPlatforms, operationEffects) {
|
|
373
|
+
if (!schema?.description || (!operationPlatforms && !operationEffects)) {
|
|
374
|
+
return schema;
|
|
375
|
+
}
|
|
376
|
+
let description = schema.description;
|
|
377
|
+
const ops = new Set([...Object.keys(operationPlatforms ?? {}), ...Object.keys(operationEffects ?? {})]);
|
|
378
|
+
for (const op of ops) {
|
|
379
|
+
const platforms = ownValue(operationPlatforms, op);
|
|
380
|
+
const effect = ownValue(operationEffects, op);
|
|
381
|
+
let tags = "";
|
|
382
|
+
if (platforms && platforms.length > 0 && !ALL_PLATFORMS.every((id) => platforms.includes(id))) {
|
|
383
|
+
tags += ` [${platforms.join(", ")}]`;
|
|
384
|
+
}
|
|
385
|
+
if (effect && effect !== "read") {
|
|
386
|
+
tags += ` [${effect}]`;
|
|
387
|
+
}
|
|
388
|
+
if (!tags) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const marker = `\n- ${op}: `;
|
|
392
|
+
if (description.includes(marker)) {
|
|
393
|
+
description = description.replace(marker, `\n- ${op}${tags}: `);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return description === schema.description ? schema : { ...schema, description };
|
|
397
|
+
}
|
|
398
|
+
function resolveToolEffect(tool, args) {
|
|
399
|
+
const op = getSelectedOperation(args);
|
|
400
|
+
const overridden = tool.effectOf?.(op, args);
|
|
401
|
+
if (overridden) {
|
|
402
|
+
return overridden;
|
|
403
|
+
}
|
|
404
|
+
if (!tool.operationEffects) {
|
|
405
|
+
// Tools without operations; an undeclared effect fails closed.
|
|
406
|
+
return tool.effect ?? "destructive";
|
|
407
|
+
}
|
|
408
|
+
if (!op) {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
const declared = ownValue(tool.operationEffects, op);
|
|
412
|
+
if (declared) {
|
|
413
|
+
return declared;
|
|
414
|
+
}
|
|
415
|
+
const opSchema = tool.inputSchema?.properties?.[OPERATION_DISCRIMINATOR];
|
|
416
|
+
const known = opSchema?.enum?.includes(op) === true;
|
|
417
|
+
// An operation of the schema without a declared effect fails closed; an unknown name is left
|
|
418
|
+
// to the dispatcher, which reports the validation error.
|
|
419
|
+
return known ? "destructive" : undefined;
|
|
420
|
+
}
|
|
421
|
+
function configuredBackends(ctx) {
|
|
422
|
+
const client = ctx.client;
|
|
423
|
+
if (typeof client?.getAvailableBackends !== "function") {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
return client.getAvailableBackends();
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
182
433
|
function resolveSupportedPlatforms(tool, args, defaultPlatforms) {
|
|
183
434
|
const basePlatforms = mergePlatforms(defaultPlatforms, tool.supportedPlatforms) ?? defaultPlatforms;
|
|
184
435
|
const operation = getSelectedOperation(args);
|
package/dist/tools/vice.js
CHANGED
|
@@ -2,17 +2,7 @@ import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema,
|
|
|
2
2
|
import { literalSchema, objectSchema, stringSchema, } from "./schema.js";
|
|
3
3
|
import { jsonResult, textResult } from "./responses.js";
|
|
4
4
|
import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
|
|
5
|
-
|
|
6
|
-
const SAFE_RESOURCE_PREFIXES = [
|
|
7
|
-
"C64",
|
|
8
|
-
"Machine",
|
|
9
|
-
"Sid",
|
|
10
|
-
"VIC",
|
|
11
|
-
"VICII",
|
|
12
|
-
"CIA",
|
|
13
|
-
"Joy",
|
|
14
|
-
"Sound",
|
|
15
|
-
];
|
|
5
|
+
import { RESOURCE_NAME_PATTERN, resourceNameProblem } from "../vice/resourcePolicy.js";
|
|
16
6
|
const resourceValueSchema = {
|
|
17
7
|
jsonSchema: {
|
|
18
8
|
type: ["string", "integer"],
|
|
@@ -124,6 +114,7 @@ export const viceModuleGroup = defineToolModule({
|
|
|
124
114
|
description: "VICE emulator operations available via the c64_vice tool.",
|
|
125
115
|
variants: viceOperationSchemas.map((schema) => schema.jsonSchema),
|
|
126
116
|
}),
|
|
117
|
+
operationEffects: { resource_get: "read", resource_set: "write" },
|
|
127
118
|
tags: ["vice", "resource", "grouped"],
|
|
128
119
|
examples: [
|
|
129
120
|
{
|
|
@@ -148,10 +139,8 @@ function handleToolError(error) {
|
|
|
148
139
|
return unknownErrorResult(error);
|
|
149
140
|
}
|
|
150
141
|
function ensureSafeResourceName(name, path) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (!SAFE_RESOURCE_PREFIXES.some((prefix) => name.startsWith(prefix))) {
|
|
155
|
-
throw new ToolValidationError(`Resource name must start with one of: ${SAFE_RESOURCE_PREFIXES.join(", ")}`, { path, details: { name } });
|
|
142
|
+
const problem = resourceNameProblem(name);
|
|
143
|
+
if (problem) {
|
|
144
|
+
throw new ToolValidationError(problem.message, { path, details: { name } });
|
|
156
145
|
}
|
|
157
146
|
}
|
package/dist/vicBitmap.js
CHANGED
|
@@ -6,7 +6,7 @@ const COLOR_RAM_BYTE_LENGTH = 1000;
|
|
|
6
6
|
const COLOR_RAM_ADDRESS = 0xD800;
|
|
7
7
|
const CHAR_ROM_SHADOW_OFFSET = 0x1000;
|
|
8
8
|
const CHAR_ROM_SHADOW_LENGTH = 0x1000;
|
|
9
|
-
const VIC_PALETTE = [
|
|
9
|
+
export const VIC_PALETTE = [
|
|
10
10
|
[0x00, 0x00, 0x00],
|
|
11
11
|
[0xFF, 0xFF, 0xFF],
|
|
12
12
|
[0x81, 0x33, 0x38],
|
package/dist/vice/mockServer.js
CHANGED
|
@@ -34,6 +34,8 @@ export class ViceMockServer {
|
|
|
34
34
|
this.server = null;
|
|
35
35
|
this.memory = new Uint8Array(0x10000);
|
|
36
36
|
this.helloReady = false;
|
|
37
|
+
/** Test hook: the payload of every keyboard feed command received, in order. */
|
|
38
|
+
this.keyboardFeeds = [];
|
|
37
39
|
this.heartbeat = null;
|
|
38
40
|
this.checkpoints = new Map();
|
|
39
41
|
this.nextCheckpointId = 1;
|
|
@@ -241,6 +243,7 @@ export class ViceMockServer {
|
|
|
241
243
|
}
|
|
242
244
|
case 0x72: { // keyboard feed
|
|
243
245
|
const len = body[0] ?? 0;
|
|
246
|
+
this.keyboardFeeds.push(Buffer.from(body.subarray(1, 1 + len)));
|
|
244
247
|
const text = body.subarray(1, 1 + len).toString("ascii").toUpperCase();
|
|
245
248
|
if (text.includes("RUN")) {
|
|
246
249
|
this.renderHello();
|
package/dist/vice/process.js
CHANGED
|
@@ -185,7 +185,26 @@ export async function terminateProcess(child, signal = "SIGTERM", timeoutMs = 10
|
|
|
185
185
|
catch { }
|
|
186
186
|
await waitForExit(child, timeoutMs);
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Rejects when something already listens on the monitor address. waitForPort
|
|
190
|
+
* succeeds against any listener, so without this check a foreign process on
|
|
191
|
+
* the port would be taken for the new emulator's monitor.
|
|
192
|
+
*/
|
|
193
|
+
export async function assertMonitorPortFree(host, port) {
|
|
194
|
+
if (!Number.isInteger(port) || port <= 0)
|
|
195
|
+
return;
|
|
196
|
+
await new Promise((resolve, reject) => {
|
|
197
|
+
const probe = net.connect({ host, port });
|
|
198
|
+
probe.setTimeout(500, () => probe.destroy(new Error("timeout")));
|
|
199
|
+
probe.once("connect", () => {
|
|
200
|
+
probe.destroy();
|
|
201
|
+
reject(new Error(`Cannot start VICE: port ${port} on ${host} is already in use by another process. Stop it or configure a different vice.port.`));
|
|
202
|
+
});
|
|
203
|
+
probe.once("error", () => resolve()); // refused or unreachable: nothing is listening
|
|
204
|
+
});
|
|
205
|
+
}
|
|
188
206
|
export async function startViceProcess(options) {
|
|
207
|
+
await assertMonitorPortFree(options.host, options.port);
|
|
189
208
|
const debugEnabled = process.env.VICE_DEVICE_TEST_DEBUG === "1";
|
|
190
209
|
// When headless=true, skip Xvfb entirely and rely on VICE's own -headless flag (VICE 3.5+).
|
|
191
210
|
const useHeadless = options.headless === true || process.env.VICE_HEADLESS === "1";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which VICE resources may be read or written through the MCP tools.
|
|
3
|
+
* Shared by c64_vice and by c64_config on the VICE backend, so neither path
|
|
4
|
+
* can reach a resource the other refuses (several VICE resources hold host
|
|
5
|
+
* file names).
|
|
6
|
+
*/
|
|
7
|
+
export const RESOURCE_NAME_PATTERN = /^[A-Z][A-Za-z0-9_.-]{0,63}$/;
|
|
8
|
+
export const SAFE_RESOURCE_PREFIXES = [
|
|
9
|
+
"C64",
|
|
10
|
+
"Machine",
|
|
11
|
+
"Sid",
|
|
12
|
+
"VIC",
|
|
13
|
+
"VICII",
|
|
14
|
+
"CIA",
|
|
15
|
+
"Joy",
|
|
16
|
+
"Sound",
|
|
17
|
+
"Warp", // WarpMode: a plain on/off switch
|
|
18
|
+
];
|
|
19
|
+
/** Resources that name host files or directories, even under a safe prefix (for example SoundRecordDeviceArg). */
|
|
20
|
+
const HOST_FILE_RESOURCE_PATTERN = /(File|Image|Dir|Directory|Path|Arg|Name)\d*$/;
|
|
21
|
+
/** Returns why a resource name is refused, or undefined when it is allowed. */
|
|
22
|
+
export function resourceNameProblem(name) {
|
|
23
|
+
if (!RESOURCE_NAME_PATTERN.test(name)) {
|
|
24
|
+
return {
|
|
25
|
+
kind: "pattern",
|
|
26
|
+
message: "Resource names must start with an uppercase letter and use letters, numbers, dashes, underscores, or periods.",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (!SAFE_RESOURCE_PREFIXES.some((prefix) => name.startsWith(prefix))) {
|
|
30
|
+
return { kind: "prefix", message: `Resource name must start with one of: ${SAFE_RESOURCE_PREFIXES.join(", ")}` };
|
|
31
|
+
}
|
|
32
|
+
if (HOST_FILE_RESOURCE_PATTERN.test(name)) {
|
|
33
|
+
return { kind: "host_file", message: `Resource '${name}' refers to a host file or directory and cannot be accessed through this tool.` };
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
package/dist/vice/viceClient.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* VICE Binary Monitor client with debugger, resource, and display helpers.
|
|
3
3
|
*/
|
|
4
4
|
import net from "node:net";
|
|
5
|
+
/** How long a TCP connect to the monitor may take before it is abandoned. */
|
|
6
|
+
export const VICE_CONNECT_TIMEOUT_MS = 5000;
|
|
5
7
|
export class ViceClient {
|
|
6
8
|
constructor() {
|
|
7
9
|
this.buffer = Buffer.alloc(0);
|
|
@@ -12,16 +14,27 @@ export class ViceClient {
|
|
|
12
14
|
setUnsolicitedHandler(handler) {
|
|
13
15
|
this.onUnsolicitedHandler = handler;
|
|
14
16
|
}
|
|
15
|
-
async connect(port, host = "127.0.0.1") {
|
|
16
|
-
|
|
17
|
+
async connect(port, host = "127.0.0.1", options = {}) {
|
|
18
|
+
const timeoutMs = options.connectTimeoutMs ?? VICE_CONNECT_TIMEOUT_MS;
|
|
19
|
+
this.socket = options.socketFactory ? options.socketFactory(host, port) : net.connect({ host, port });
|
|
17
20
|
this.closing = false;
|
|
18
21
|
this.socket.setNoDelay(true);
|
|
19
22
|
await new Promise((resolve, reject) => {
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
// A host that drops the SYN would otherwise block until the OS gives up.
|
|
25
|
+
this.socket.off("connect", onConnect);
|
|
26
|
+
this.socket.off("error", onError);
|
|
27
|
+
this.socket.on("error", () => { }); // destroy() may still emit an error
|
|
28
|
+
this.socket.destroy();
|
|
29
|
+
reject(new Error(`Timed out after ${timeoutMs} ms connecting to the VICE monitor at ${host}:${port}`));
|
|
30
|
+
}, timeoutMs);
|
|
20
31
|
const onConnect = () => {
|
|
32
|
+
clearTimeout(timer);
|
|
21
33
|
this.socket.off("error", onError);
|
|
22
34
|
resolve();
|
|
23
35
|
};
|
|
24
36
|
const onError = (error) => {
|
|
37
|
+
clearTimeout(timer);
|
|
25
38
|
this.socket.off("connect", onConnect);
|
|
26
39
|
reject(error);
|
|
27
40
|
};
|
|
@@ -212,10 +225,14 @@ export class ViceClient {
|
|
|
212
225
|
}
|
|
213
226
|
async keyboardFeed(text) {
|
|
214
227
|
if (!text)
|
|
215
|
-
|
|
228
|
+
throw new Error("keyboardFeed requires non-empty text");
|
|
216
229
|
const bytes = Buffer.from(text, "ascii");
|
|
217
|
-
|
|
218
|
-
|
|
230
|
+
// The command carries its length in one byte, so longer text goes out as
|
|
231
|
+
// consecutive commands, in order.
|
|
232
|
+
for (let offset = 0; offset < bytes.length; offset += 255) {
|
|
233
|
+
const chunk = bytes.subarray(offset, offset + 255);
|
|
234
|
+
await this.send(0x72, Buffer.concat([Buffer.from([chunk.length]), chunk]));
|
|
235
|
+
}
|
|
219
236
|
}
|
|
220
237
|
/** Set the VICE joyport's active-low control byte (BM command 0xA2). */
|
|
221
238
|
async joyportSet(port, activeLowMask) {
|
|
@@ -21,15 +21,18 @@ This document is the source of truth for VICE support in the public MCP surface.
|
|
|
21
21
|
|
|
22
22
|
| Tool | Operations |
|
|
23
23
|
| --- | --- |
|
|
24
|
-
| `c64_program` | `run_prg`, `upload_run_basic`, `upload_run_asm`, `batch_run` |
|
|
25
|
-
| `c64_memory` | `read`, `read_screen`, `wait_for_text`, `
|
|
26
|
-
| `c64_system` | `reset`, `reboot`, `poweroff`, `start_task`, `stop_task`, `stop_all_tasks`, `list_tasks` |
|
|
27
|
-
| `c64_graphics` | `render_petscii_art`, `render_bitmap`, `render_sprite`, `render_petscii_text` |
|
|
24
|
+
| `c64_program` | `load_prg`, `run_prg`, `upload_run_basic`, `upload_run_asm`, `batch_run`, `bundle_run`, `cross_platform_greeting` |
|
|
25
|
+
| `c64_memory` | `read`, `write`, `read_screen`, `wait_for_text`, `list_basic`, `disassemble`, `copy_memory`, `fill_memory`, `search_memory`, `compare_memory`, `save_memory` |
|
|
26
|
+
| `c64_system` | `reset`, `reboot`, `poweroff`, `power_cycle`, `start_task`, `stop_task`, `stop_all_tasks`, `list_tasks`, `performance_report` |
|
|
27
|
+
| `c64_graphics` | `capture_frame`, `render_petscii_art`, `render_bitmap`, `render_sprite`, `render_petscii_text`, `get_display_state` |
|
|
28
28
|
| `c64_rag` | `basic`, `asm` |
|
|
29
|
-
| `c64_disk` | `list_drives`, `mount
|
|
29
|
+
| `c64_disk` | `list_drives`, `mount` |
|
|
30
30
|
| `c64_drive` | `reset`, `power_on`, `power_off`, `set_mode` |
|
|
31
31
|
| `c64_config` | `list`, `get`, `set`, `batch_update`, `info`, `version`, `snapshot`, `restore`, `diff` |
|
|
32
|
-
| `c64_sound` | `set_volume`, `reset`, `note_on`, `note_off`, `silence_all`, `generate`, `compile_play` |
|
|
32
|
+
| `c64_sound` | `set_volume`, `reset`, `note_on`, `note_off`, `silence_all`, `generate`, `compile_play`, `play_preset`, `pipeline`, `analyze`, `record_analyze` |
|
|
33
|
+
| `c64_input` | `write_text`, `key`, `joystick` |
|
|
34
|
+
| `c64_extract` | `sprites`, `charset`, `memory_dump`, `firmware_health` |
|
|
35
|
+
| `c64_select_backend` | `select` |
|
|
33
36
|
| `c64_debug` | all operations |
|
|
34
37
|
| `c64_vice` | all operations |
|
|
35
38
|
|
|
@@ -37,15 +40,18 @@ This document is the source of truth for VICE support in the public MCP surface.
|
|
|
37
40
|
|
|
38
41
|
| Tool | Operations |
|
|
39
42
|
| --- | --- |
|
|
40
|
-
| `c64_program` | `
|
|
41
|
-
| `c64_disk` | `file_info`, `create_image`, `find_and_run` |
|
|
43
|
+
| `c64_program` | `run_crt` |
|
|
44
|
+
| `c64_disk` | `unmount`, `file_info`, `create_image`, `find_and_run`, `find_paths` |
|
|
42
45
|
| `c64_drive` | `load_rom` |
|
|
43
|
-
| `c64_sound` | `
|
|
46
|
+
| `c64_sound` | `capture_samples`, `play_sid_file`, `play_mod_file` |
|
|
44
47
|
| `c64_config` | `load_flash`, `save_flash`, `reset_defaults`, `read_debugreg`, `write_debugreg`, `shuffle` |
|
|
45
|
-
| `c64_system` | `pause`, `resume`, `menu` |
|
|
48
|
+
| `c64_system` | `pause`, `resume`, `menu`, `read_menu_screen` |
|
|
49
|
+
| `c64_input` | `keyboard`, `release_all`, `state` |
|
|
50
|
+
| `c64_extract` | `fs_stats` |
|
|
46
51
|
| `c64_stream` | all operations |
|
|
47
52
|
| `c64_printer` | all operations |
|
|
48
|
-
|
|
53
|
+
|
|
54
|
+
The per-operation support is declared in `src/tools/registry/*.ts` (`operationPlatforms` and `supportedPlatforms`); the generated tables in `README.md` and `mcp/*.json` reflect it. If this file and the registry differ, the registry is correct.
|
|
49
55
|
|
|
50
56
|
## Notes
|
|
51
57
|
|