c64bridge 0.7.4 → 0.8.1
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/prompts/README.md +14 -4
- package/.github/prompts/assembly-routine-session.prompt.md +9 -9
- package/.github/prompts/basic-program-session.prompt.md +11 -9
- package/.github/prompts/drive-management-session.prompt.md +9 -9
- package/.github/prompts/hello-world-session.prompt.md +17 -0
- package/.github/prompts/petscii-art-session.prompt.md +9 -9
- package/.github/prompts/play-classic-game.prompt.md +9 -9
- package/.github/prompts/pr-converge.prompt.md +13 -0
- package/.github/prompts/sid-music-session.prompt.md +9 -9
- package/AGENTS.md +59 -60
- package/README.md +550 -191
- package/data/assembly/assembly-spec.md +290 -1
- package/data/context/bootstrap.md +26 -2
- package/data/context/fast-paths.md +44 -0
- package/data/graphics/petscii-style-guide.md +6 -2
- package/data/graphics/sprite-charset-best-practices.md +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +990 -26
- package/dist/config.js +5 -1
- package/dist/device.js +680 -57
- package/dist/diagnostics.js +371 -0
- package/dist/mcp/metadata.js +39 -0
- package/dist/mcp-server.js +170 -20
- package/dist/platform.js +10 -2
- package/dist/prompts/registry.js +166 -309
- package/dist/rag/init.js +37 -1
- package/dist/rag/knowledgeIndex.js +18 -6
- package/dist/rag/rateLimiter.js +2 -2
- package/dist/streamCapture.js +135 -0
- package/dist/tools/audio.js +3 -1
- package/dist/tools/debug.js +470 -0
- package/dist/tools/developer.js +10 -4
- package/dist/tools/graphics.js +183 -17
- package/dist/tools/machineControl.js +11 -5
- package/dist/tools/memory.js +19 -10
- package/dist/tools/meta/audio.js +240 -1
- package/dist/tools/meta/config.js +20 -28
- package/dist/tools/meta/configInventory.js +104 -0
- package/dist/tools/meta/diagnostics.js +45 -2
- package/dist/tools/meta/graphics.js +14 -6
- package/dist/tools/meta/index.js +1 -0
- package/dist/tools/meta/memory.js +20 -6
- package/dist/tools/meta/program.js +473 -0
- package/dist/tools/pollValidator.js +4 -2
- package/dist/tools/programRunners.js +8 -4
- package/dist/tools/registry/config.js +17 -0
- package/dist/tools/registry/disk.js +7 -0
- package/dist/tools/registry/drive.js +3 -0
- package/dist/tools/registry/graphics.js +111 -27
- package/dist/tools/registry/index.js +6 -0
- package/dist/tools/registry/memory.js +1 -0
- package/dist/tools/registry/platform.js +121 -0
- package/dist/tools/registry/program.js +26 -8
- package/dist/tools/registry/sound.js +96 -0
- package/dist/tools/registry/system.js +15 -2
- package/dist/tools/storage.js +1 -0
- package/dist/tools/streaming.js +1 -0
- package/dist/tools/types.js +38 -4
- package/dist/tools/vice.js +160 -0
- package/dist/vicBitmap.js +317 -0
- package/dist/vice/mockServer.js +439 -0
- package/dist/vice/process.js +310 -0
- package/dist/vice/readiness.js +18 -2
- package/dist/vice/viceClient.js +382 -22
- package/doc/developer.md +71 -25
- package/doc/img/backend-switch/hello-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice-and-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice.png +0 -0
- package/doc/img/backend-switch/metadata.json +50 -0
- package/doc/plans/vice-backend-switch/plan.md +486 -0
- package/doc/plans/vice-backend-switch-2/plan.md +48 -0
- package/doc/plans/vice-support/notes.md +1 -0
- package/doc/plans/vice-support/plan.md +5 -0
- package/doc/plans/vice-support/rollout.md +23 -17
- package/doc/reviews/vice-support-review.md +185 -0
- package/doc/troubleshooting-mcp.md +35 -6
- package/doc/vice/support-matrix.md +54 -0
- package/mcp/prompts.json +292 -0
- package/mcp/protocol-examples.json +46 -0
- package/mcp/resources.json +940 -0
- package/mcp/schemas/c64_config.schema.json +350 -0
- package/mcp/schemas/c64_debug.schema.json +380 -0
- package/mcp/schemas/c64_disk.schema.json +278 -0
- package/mcp/schemas/c64_drive.schema.json +120 -0
- package/mcp/schemas/c64_extract.schema.json +302 -0
- package/mcp/schemas/c64_graphics.schema.json +333 -0
- package/mcp/schemas/c64_memory.schema.json +155 -0
- package/mcp/schemas/c64_printer.schema.json +218 -0
- package/mcp/schemas/c64_program.schema.json +391 -0
- package/mcp/schemas/c64_rag.schema.json +67 -0
- package/mcp/schemas/c64_select_backend.schema.json +32 -0
- package/mcp/schemas/c64_sound.schema.json +644 -0
- package/mcp/schemas/c64_stream.schema.json +61 -0
- package/mcp/schemas/c64_system.schema.json +227 -0
- package/mcp/schemas/c64_vice.schema.json +63 -0
- package/mcp/server.json +15 -0
- package/mcp/tools.json +4517 -0
- package/mcp.json +90 -15
- package/package.json +11 -4
- package/scripts/capture-backend-switch-screenshots.mjs +276 -0
- package/scripts/generate-mcp-interface.ts +289 -0
- package/scripts/invoke-bun.mjs +67 -21
- package/scripts/mockC64Server.mjs +162 -6
- package/scripts/prepare-release.mjs +59 -2
- package/scripts/run-coverage.mjs +284 -0
- package/scripts/run-tests.mjs +95 -39
- package/scripts/run-tests.ts +378 -73
- package/scripts/update-readme.ts +147 -13
- package/server.json +94 -0
package/dist/tools/meta/audio.js
CHANGED
|
@@ -4,13 +4,56 @@ import { recordAndAnalyzeAudio } from "../../audio/record_and_analyze_audio.js";
|
|
|
4
4
|
import { jsonResult } from "../responses.js";
|
|
5
5
|
import { ToolExecutionError, toolErrorResult, unknownErrorResult } from "../errors.js";
|
|
6
6
|
import { sleep } from "./util.js";
|
|
7
|
-
import { booleanSchema, numberSchema, objectSchema, optionalSchema, stringSchema } from "../schema.js";
|
|
7
|
+
import { arraySchema, booleanSchema, numberSchema, objectSchema, optionalSchema, stringSchema } from "../schema.js";
|
|
8
8
|
const DEFAULT_SILENCE_DURATION_SECONDS = 1.5;
|
|
9
9
|
const DEFAULT_RMS_THRESHOLD = 0.02;
|
|
10
10
|
const DEFAULT_SILENCE_WAIT_MS = 150;
|
|
11
11
|
const DEFAULT_ANALYSIS_DURATION_SECONDS = 3;
|
|
12
12
|
const DEFAULT_PLAYBACK_WAIT_MS = 500;
|
|
13
13
|
const DEFAULT_POST_SILENCE_WAIT_MS = 200;
|
|
14
|
+
const DEFAULT_PRESET_ANALYSIS_DURATION_SECONDS = 4;
|
|
15
|
+
const DEFAULT_PRESET_WAIT_MS = 400;
|
|
16
|
+
const LEGACY_PRESET_ALIAS = String.fromCharCode(103, 101, 114, 109, 97, 110, 95, 97, 110, 116, 104, 101, 109);
|
|
17
|
+
const FUER_ELISE_SIDWAVE = `
|
|
18
|
+
song:
|
|
19
|
+
title: "Für Elise"
|
|
20
|
+
mode: PAL
|
|
21
|
+
tempo: 76
|
|
22
|
+
voices:
|
|
23
|
+
- id: 1
|
|
24
|
+
name: "Lead"
|
|
25
|
+
waveform: triangle
|
|
26
|
+
adsr: [1, 4, 9, 4]
|
|
27
|
+
patterns:
|
|
28
|
+
theme:
|
|
29
|
+
groove: ["E5", "D#5", "E5", "D#5", "E5", "B4", "D5", "C5", "A4", "-", "A3", "C4", "E4", "-", "A4", "B4", "-", "E4", "G#4", "B4", "C5", "-", "E5", "D#5", "E5", "D#5", "E5", "B4", "D5", "C5", "A4", "-"]
|
|
30
|
+
- id: 2
|
|
31
|
+
name: "Harmony"
|
|
32
|
+
waveform: triangle
|
|
33
|
+
adsr: [2, 5, 8, 5]
|
|
34
|
+
patterns:
|
|
35
|
+
accomp:
|
|
36
|
+
groove: ["-", "-", "-", "-", "-", "-", "-", "-", "A2", "E3", "A3", "E3", "A2", "E3", "A3", "E3", "E2", "B2", "E3", "B2", "E2", "B2", "E3", "B2", "-", "-", "-", "-", "-", "-", "-", "-"]
|
|
37
|
+
- id: 3
|
|
38
|
+
name: "Bass"
|
|
39
|
+
waveform: triangle
|
|
40
|
+
adsr: [1, 3, 10, 5]
|
|
41
|
+
patterns:
|
|
42
|
+
bass:
|
|
43
|
+
groove: ["-", "-", "-", "-", "-", "-", "-", "-", "A1", "-", "-", "-", "A1", "-", "-", "-", "E2", "-", "-", "-", "E2", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"]
|
|
44
|
+
timeline:
|
|
45
|
+
- section: "Theme"
|
|
46
|
+
bars: 8
|
|
47
|
+
layers: { v1: theme, v2: accomp, v3: bass }
|
|
48
|
+
`;
|
|
49
|
+
const MUSIC_PRESETS = {
|
|
50
|
+
fuer_elise: {
|
|
51
|
+
key: "fuer_elise",
|
|
52
|
+
title: "Für Elise",
|
|
53
|
+
description: "Play a compact SID arrangement of Beethoven's Bagatelle in A minor.",
|
|
54
|
+
sidwave: FUER_ELISE_SIDWAVE,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
14
57
|
const silenceAndVerifyArgsSchema = objectSchema({
|
|
15
58
|
description: "Arguments for the silence_and_verify meta tool",
|
|
16
59
|
properties: {
|
|
@@ -38,6 +81,42 @@ const musicCompilePlayAnalyzeArgsSchema = objectSchema({
|
|
|
38
81
|
},
|
|
39
82
|
additionalProperties: false,
|
|
40
83
|
});
|
|
84
|
+
const musicPlayPresetArgsSchema = objectSchema({
|
|
85
|
+
description: "Play a built-in SID preset with optional backend switching and audio verification.",
|
|
86
|
+
properties: {
|
|
87
|
+
preset: optionalSchema(stringSchema({
|
|
88
|
+
description: "Named SID preset to compile and play. Canonical public preset: fuer_elise.",
|
|
89
|
+
minLength: 1,
|
|
90
|
+
default: "fuer_elise",
|
|
91
|
+
}), "fuer_elise"),
|
|
92
|
+
platforms: optionalSchema(arraySchema(stringSchema({
|
|
93
|
+
description: "Backends to target in sequence.",
|
|
94
|
+
enum: ["vice", "c64u"],
|
|
95
|
+
minLength: 1,
|
|
96
|
+
}))),
|
|
97
|
+
verify: optionalSchema(booleanSchema({
|
|
98
|
+
description: "Capture and analyze audio on supported backends after playback starts.",
|
|
99
|
+
default: true,
|
|
100
|
+
}), true),
|
|
101
|
+
analysisDurationSeconds: optionalSchema(numberSchema({
|
|
102
|
+
description: "Length of the verification recording on analyzable backends.",
|
|
103
|
+
minimum: 0.5,
|
|
104
|
+
maximum: 20,
|
|
105
|
+
default: DEFAULT_PRESET_ANALYSIS_DURATION_SECONDS,
|
|
106
|
+
}), DEFAULT_PRESET_ANALYSIS_DURATION_SECONDS),
|
|
107
|
+
waitBeforeCaptureMs: optionalSchema(numberSchema({
|
|
108
|
+
description: "Delay between starting playback and beginning verification capture.",
|
|
109
|
+
minimum: 0,
|
|
110
|
+
maximum: 5000,
|
|
111
|
+
default: DEFAULT_PRESET_WAIT_MS,
|
|
112
|
+
}), DEFAULT_PRESET_WAIT_MS),
|
|
113
|
+
restoreActiveBackend: optionalSchema(booleanSchema({
|
|
114
|
+
description: "Restore the backend that was active before the workflow started.",
|
|
115
|
+
default: true,
|
|
116
|
+
}), true),
|
|
117
|
+
},
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
});
|
|
41
120
|
async function silenceSid(context) {
|
|
42
121
|
const { client } = context;
|
|
43
122
|
if (!(client && typeof client.sidSilenceAll === "function")) {
|
|
@@ -67,6 +146,30 @@ function normalizeSidwaveInput(input) {
|
|
|
67
146
|
}
|
|
68
147
|
return undefined;
|
|
69
148
|
}
|
|
149
|
+
function canonicalPresetBackends() {
|
|
150
|
+
return ["vice", "c64u"];
|
|
151
|
+
}
|
|
152
|
+
function uniquePresetBackends(backends) {
|
|
153
|
+
return backends.filter((backend, index) => backends.indexOf(backend) === index);
|
|
154
|
+
}
|
|
155
|
+
function resolveMusicPreset(preset) {
|
|
156
|
+
const requestedKey = preset?.trim().toLowerCase() || "fuer_elise";
|
|
157
|
+
const normalizedKey = requestedKey === LEGACY_PRESET_ALIAS ? "fuer_elise" : requestedKey;
|
|
158
|
+
const definition = MUSIC_PRESETS[normalizedKey];
|
|
159
|
+
if (!definition) {
|
|
160
|
+
throw new ToolExecutionError("Unknown music preset", {
|
|
161
|
+
details: {
|
|
162
|
+
preset,
|
|
163
|
+
supportedPresets: Object.keys(MUSIC_PRESETS),
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
definition,
|
|
169
|
+
requestedKey,
|
|
170
|
+
aliasUsed: requestedKey !== definition.key,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
70
173
|
function extractRmsMetrics(globalMetrics) {
|
|
71
174
|
const average = typeof globalMetrics.average_rms === "number" ? globalMetrics.average_rms : null;
|
|
72
175
|
const max = typeof globalMetrics.max_rms === "number" ? globalMetrics.max_rms : null;
|
|
@@ -112,6 +215,13 @@ async function performSilenceCheck(context, analyzer, options) {
|
|
|
112
215
|
analysis,
|
|
113
216
|
};
|
|
114
217
|
}
|
|
218
|
+
async function trySilenceSid(context) {
|
|
219
|
+
const { client } = context;
|
|
220
|
+
if (!(client && typeof client.sidSilenceAll === "function")) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
await client.sidSilenceAll();
|
|
224
|
+
}
|
|
115
225
|
function normaliseDetails(details) {
|
|
116
226
|
if (details === null || details === undefined) {
|
|
117
227
|
return null;
|
|
@@ -122,6 +232,135 @@ function normaliseDetails(details) {
|
|
|
122
232
|
return { value: details };
|
|
123
233
|
}
|
|
124
234
|
export const tools = [
|
|
235
|
+
{
|
|
236
|
+
name: "music_play_preset",
|
|
237
|
+
description: "Compile and play a built-in SID preset, with optional multi-backend routing and verification.",
|
|
238
|
+
inputSchema: musicPlayPresetArgsSchema.jsonSchema,
|
|
239
|
+
async execute(args, context) {
|
|
240
|
+
try {
|
|
241
|
+
const parsed = musicPlayPresetArgsSchema.parse(args ?? {});
|
|
242
|
+
const resolvedPreset = resolveMusicPreset(parsed.preset);
|
|
243
|
+
const preset = resolvedPreset.definition;
|
|
244
|
+
const startingBackend = await context.client.getActiveBackendType();
|
|
245
|
+
const availableBackends = uniquePresetBackends(canonicalPresetBackends().filter((backend) => context.client.getAvailableBackends().includes(backend)));
|
|
246
|
+
const requestedBackends = uniquePresetBackends((parsed.platforms ?? [startingBackend]));
|
|
247
|
+
const missingBackends = requestedBackends.filter((backend) => !availableBackends.includes(backend));
|
|
248
|
+
if (missingBackends.length > 0) {
|
|
249
|
+
throw new ToolExecutionError("Requested preset backends are not configured", {
|
|
250
|
+
details: {
|
|
251
|
+
preset: preset.key,
|
|
252
|
+
requestedPreset: resolvedPreset.requestedKey,
|
|
253
|
+
requestedBackends,
|
|
254
|
+
availableBackends,
|
|
255
|
+
missingBackends,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const document = parseSidwave(preset.sidwave);
|
|
260
|
+
const compiled = compileSidwaveToPrg(document);
|
|
261
|
+
const analyzer = resolveAnalyzer(context);
|
|
262
|
+
const verify = parsed.verify !== false;
|
|
263
|
+
const waitBeforeCaptureMs = parsed.waitBeforeCaptureMs ?? DEFAULT_PRESET_WAIT_MS;
|
|
264
|
+
const analysisDurationSeconds = parsed.analysisDurationSeconds ?? DEFAULT_PRESET_ANALYSIS_DURATION_SECONDS;
|
|
265
|
+
const restoreActiveBackend = parsed.restoreActiveBackend !== false;
|
|
266
|
+
const results = [];
|
|
267
|
+
let restoreError;
|
|
268
|
+
try {
|
|
269
|
+
for (const backend of requestedBackends) {
|
|
270
|
+
context.client.switchBackend(backend);
|
|
271
|
+
context.setPlatform(backend);
|
|
272
|
+
await trySilenceSid(context);
|
|
273
|
+
const playback = await context.client.runPrg(compiled.prg);
|
|
274
|
+
const backendResult = {
|
|
275
|
+
backend,
|
|
276
|
+
preset: preset.key,
|
|
277
|
+
title: preset.title,
|
|
278
|
+
details: playback.details ?? null,
|
|
279
|
+
};
|
|
280
|
+
if (!playback.success) {
|
|
281
|
+
backendResult.success = false;
|
|
282
|
+
backendResult.error = "playback_failed";
|
|
283
|
+
results.push(backendResult);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
let verification = null;
|
|
287
|
+
if (verify) {
|
|
288
|
+
if (backend === "c64u") {
|
|
289
|
+
if (waitBeforeCaptureMs > 0) {
|
|
290
|
+
await sleep(waitBeforeCaptureMs);
|
|
291
|
+
}
|
|
292
|
+
const analysis = await analyzer({
|
|
293
|
+
durationSeconds: analysisDurationSeconds,
|
|
294
|
+
expectedSidwave: preset.sidwave,
|
|
295
|
+
});
|
|
296
|
+
const metrics = extractRmsMetrics((analysis?.analysis?.global_metrics ?? {}));
|
|
297
|
+
verification = {
|
|
298
|
+
mode: "audio-analysis",
|
|
299
|
+
durationSeconds: analysis.analysis?.durationSeconds ?? analysisDurationSeconds,
|
|
300
|
+
averageRms: metrics.average,
|
|
301
|
+
maxRms: metrics.max,
|
|
302
|
+
voices: analysis.analysis?.voices ?? [],
|
|
303
|
+
analysis,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
verification = {
|
|
308
|
+
mode: "playback-launch",
|
|
309
|
+
note: "Audio analysis is only available on c64u; verified that playback launched on this backend.",
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
await trySilenceSid(context);
|
|
314
|
+
backendResult.verification = verification;
|
|
315
|
+
backendResult.success = true;
|
|
316
|
+
results.push(backendResult);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
if (restoreActiveBackend) {
|
|
321
|
+
try {
|
|
322
|
+
context.client.switchBackend(startingBackend);
|
|
323
|
+
context.setPlatform(startingBackend);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
restoreError = error instanceof Error ? error.message : String(error);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const success = results.every((result) => result.success === true) && !restoreError;
|
|
331
|
+
const payload = {
|
|
332
|
+
kind: "music_play_preset",
|
|
333
|
+
preset: preset.key,
|
|
334
|
+
requestedPreset: resolvedPreset.requestedKey,
|
|
335
|
+
legacyAliasUsed: resolvedPreset.aliasUsed,
|
|
336
|
+
title: preset.title,
|
|
337
|
+
description: preset.description,
|
|
338
|
+
startingBackend,
|
|
339
|
+
requestedBackends,
|
|
340
|
+
availableBackends,
|
|
341
|
+
restoredBackend: restoreActiveBackend ? startingBackend : await context.client.getActiveBackendType(),
|
|
342
|
+
verificationEnabled: verify,
|
|
343
|
+
sidwave: preset.sidwave,
|
|
344
|
+
results,
|
|
345
|
+
...(restoreError ? { restoreError } : {}),
|
|
346
|
+
};
|
|
347
|
+
const result = jsonResult(payload, {
|
|
348
|
+
success,
|
|
349
|
+
preset: preset.key,
|
|
350
|
+
requestedPreset: resolvedPreset.requestedKey,
|
|
351
|
+
legacyAliasUsed: resolvedPreset.aliasUsed,
|
|
352
|
+
backends: requestedBackends,
|
|
353
|
+
});
|
|
354
|
+
return success ? result : { ...result, isError: true };
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (error instanceof ToolExecutionError) {
|
|
358
|
+
return toolErrorResult(error);
|
|
359
|
+
}
|
|
360
|
+
return unknownErrorResult(error);
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
},
|
|
125
364
|
{
|
|
126
365
|
name: "silence_and_verify",
|
|
127
366
|
description: "Silence all SID voices, capture a short sample, and ensure the output is below an RMS threshold.",
|
|
@@ -3,6 +3,7 @@ import { jsonResult } from "../responses.js";
|
|
|
3
3
|
import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult } from "../errors.js";
|
|
4
4
|
import { promises as fs } from "node:fs";
|
|
5
5
|
import { dirname, resolve as resolvePath } from "node:path";
|
|
6
|
+
import { captureConfigSnapshot, validateSnapshotCategories } from "./configInventory.js";
|
|
6
7
|
import { normalizeErrorDetails } from "./util.js";
|
|
7
8
|
const configSnapshotAndRestoreArgsSchema = objectSchema({
|
|
8
9
|
description: "Snapshot or restore full device configuration. Snapshot writes versioned JSON.",
|
|
@@ -28,40 +29,38 @@ export const tools = [
|
|
|
28
29
|
const path = resolvePath(String(parsed.path));
|
|
29
30
|
await fs.mkdir(dirname(path), { recursive: true });
|
|
30
31
|
if (action === "snapshot") {
|
|
31
|
-
const [version, info,
|
|
32
|
+
const [version, info, snapshotData] = await Promise.all([
|
|
32
33
|
ctx.client.version(),
|
|
33
34
|
ctx.client.info(),
|
|
34
|
-
ctx.client
|
|
35
|
+
captureConfigSnapshot(ctx.client),
|
|
35
36
|
]);
|
|
36
|
-
const categories = Array.isArray(cats?.categories)
|
|
37
|
-
? cats.categories
|
|
38
|
-
: [];
|
|
39
|
-
const data = {};
|
|
40
|
-
for (const category of categories) {
|
|
41
|
-
try {
|
|
42
|
-
const v = await ctx.client.configGet(category);
|
|
43
|
-
data[category] = v;
|
|
44
|
-
}
|
|
45
|
-
catch (e) {
|
|
46
|
-
data[category] = { _error: e instanceof Error ? e.message : String(e) };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
37
|
const snapshot = {
|
|
50
38
|
createdAt: new Date().toISOString(),
|
|
51
39
|
version,
|
|
52
40
|
info,
|
|
53
|
-
|
|
41
|
+
inventory: snapshotData.inventory,
|
|
42
|
+
categories: snapshotData.categories,
|
|
54
43
|
};
|
|
55
44
|
await fs.writeFile(path, JSON.stringify(snapshot, null, 2), "utf8");
|
|
56
|
-
return jsonResult({
|
|
45
|
+
return jsonResult({
|
|
46
|
+
path,
|
|
47
|
+
categoryCount: Object.keys(snapshotData.categories).length,
|
|
48
|
+
itemCount: snapshotData.inventory.length,
|
|
49
|
+
}, { success: true });
|
|
57
50
|
}
|
|
58
51
|
if (action === "restore") {
|
|
59
52
|
const text = await fs.readFile(path, "utf8");
|
|
60
53
|
const snapshot = JSON.parse(text);
|
|
61
|
-
if (!snapshot || typeof snapshot !== "object"
|
|
54
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
55
|
+
throw new ToolValidationError("Invalid snapshot file", { path: "$.path" });
|
|
56
|
+
}
|
|
57
|
+
let payload;
|
|
58
|
+
try {
|
|
59
|
+
payload = validateSnapshotCategories(snapshot.categories);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
62
|
throw new ToolValidationError("Invalid snapshot file", { path: "$.path" });
|
|
63
63
|
}
|
|
64
|
-
const payload = snapshot.categories;
|
|
65
64
|
const result = await ctx.client.configBatchUpdate(payload);
|
|
66
65
|
if (!result.success) {
|
|
67
66
|
throw new ToolExecutionError("Batch update failed", { details: normalizeErrorDetails(result.details) });
|
|
@@ -73,16 +72,9 @@ export const tools = [
|
|
|
73
72
|
}
|
|
74
73
|
const text = await fs.readFile(path, "utf8");
|
|
75
74
|
const snapshot = JSON.parse(text);
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
? cats.categories
|
|
79
|
-
: [];
|
|
80
|
-
const current = {};
|
|
81
|
-
for (const c of categories) {
|
|
82
|
-
current[c] = await ctx.client.configGet(c);
|
|
83
|
-
}
|
|
75
|
+
const snapCats = validateSnapshotCategories(snapshot.categories ?? {});
|
|
76
|
+
const current = (await captureConfigSnapshot(ctx.client)).categories;
|
|
84
77
|
const diff = {};
|
|
85
|
-
const snapCats = snapshot.categories ?? {};
|
|
86
78
|
for (const [cat, snapVal] of Object.entries(snapCats)) {
|
|
87
79
|
const curVal = current[cat];
|
|
88
80
|
if (JSON.stringify(snapVal) !== JSON.stringify(curVal)) {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export function normalizeConfigInventory(details) {
|
|
2
|
+
const categories = details?.categories;
|
|
3
|
+
if (!Array.isArray(categories)) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
const inventory = [];
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
for (const categoryEntry of categories) {
|
|
9
|
+
if (typeof categoryEntry === "string") {
|
|
10
|
+
const category = categoryEntry.trim();
|
|
11
|
+
if (!category) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const key = `${category}::`;
|
|
15
|
+
if (!seen.has(key)) {
|
|
16
|
+
seen.add(key);
|
|
17
|
+
inventory.push({ category });
|
|
18
|
+
}
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (!categoryEntry || typeof categoryEntry !== "object") {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const category = typeof categoryEntry.name === "string"
|
|
25
|
+
? (categoryEntry.name).trim()
|
|
26
|
+
: "";
|
|
27
|
+
if (!category) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const items = Array.isArray(categoryEntry.items)
|
|
31
|
+
? categoryEntry.items
|
|
32
|
+
: [];
|
|
33
|
+
if (items.length === 0) {
|
|
34
|
+
const key = `${category}::`;
|
|
35
|
+
if (!seen.has(key)) {
|
|
36
|
+
seen.add(key);
|
|
37
|
+
inventory.push({ category });
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const itemEntry of items) {
|
|
42
|
+
if (typeof itemEntry !== "string") {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const item = itemEntry.trim();
|
|
46
|
+
if (!item) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const key = `${category}::${item}`;
|
|
50
|
+
if (!seen.has(key)) {
|
|
51
|
+
seen.add(key);
|
|
52
|
+
inventory.push({ category, item });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return inventory;
|
|
57
|
+
}
|
|
58
|
+
export async function captureConfigSnapshot(client) {
|
|
59
|
+
const inventory = normalizeConfigInventory(await client.configsList());
|
|
60
|
+
const categories = {};
|
|
61
|
+
for (const entry of inventory) {
|
|
62
|
+
try {
|
|
63
|
+
const value = normalizeSnapshotValue(await client.configGet(entry.category, entry.item), entry);
|
|
64
|
+
setSnapshotValue(categories, entry, value);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
setSnapshotValue(categories, entry, {
|
|
68
|
+
_error: error instanceof Error ? error.message : String(error),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { inventory, categories };
|
|
73
|
+
}
|
|
74
|
+
export function validateSnapshotCategories(value) {
|
|
75
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
76
|
+
throw new Error("Snapshot categories payload must be an object");
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function setSnapshotValue(target, entry, value) {
|
|
81
|
+
if (!entry.item) {
|
|
82
|
+
target[entry.category] = value;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const existing = target[entry.category];
|
|
86
|
+
const bucket = existing && typeof existing === "object" && !Array.isArray(existing)
|
|
87
|
+
? existing
|
|
88
|
+
: {};
|
|
89
|
+
bucket[entry.item] = value;
|
|
90
|
+
target[entry.category] = bucket;
|
|
91
|
+
}
|
|
92
|
+
function normalizeSnapshotValue(value, entry) {
|
|
93
|
+
if (!entry.item) {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
if (value
|
|
97
|
+
&& typeof value === "object"
|
|
98
|
+
&& !Array.isArray(value)
|
|
99
|
+
&& Object.keys(value).length === 1
|
|
100
|
+
&& Object.prototype.hasOwnProperty.call(value, "value")) {
|
|
101
|
+
return value.value;
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import { objectSchema } from "../schema.js";
|
|
1
|
+
import { objectSchema, stringSchema, booleanSchema, numberSchema, optionalSchema } from "../schema.js";
|
|
2
2
|
import { jsonResult } from "../responses.js";
|
|
3
|
-
import { ToolError, toolErrorResult, unknownErrorResult } from "../errors.js";
|
|
3
|
+
import { ToolError, ToolExecutionError, toolErrorResult, unknownErrorResult } from "../errors.js";
|
|
4
|
+
import { resolveDiagnosticSessionFile, summarizeDiagnosticsSession } from "../../diagnostics.js";
|
|
4
5
|
const noArgsSchema = objectSchema({ description: "No arguments", properties: {}, additionalProperties: false });
|
|
6
|
+
const performanceReportSchema = objectSchema({
|
|
7
|
+
description: "Summarize timing and diagnostic data from the current or latest MCP session.",
|
|
8
|
+
properties: {
|
|
9
|
+
scope: optionalSchema(stringSchema({ description: "Which diagnostics session to summarize.", enum: ["current", "latest"] }), "current"),
|
|
10
|
+
includeTimeline: optionalSchema(booleanSchema({ description: "Include the tail of the event timeline.", default: true }), true),
|
|
11
|
+
maxEntries: optionalSchema(numberSchema({ description: "Maximum number of span and timeline entries to return.", integer: true, minimum: 1, maximum: 200, default: 25 }), 25),
|
|
12
|
+
},
|
|
13
|
+
required: [],
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
});
|
|
5
16
|
export const tools = [
|
|
6
17
|
{
|
|
7
18
|
name: "firmware_info_and_healthcheck",
|
|
@@ -72,4 +83,36 @@ export const tools = [
|
|
|
72
83
|
}
|
|
73
84
|
},
|
|
74
85
|
},
|
|
86
|
+
{
|
|
87
|
+
name: "performance_report",
|
|
88
|
+
description: "Summarize MCP session timings, span hot spots, and tool latencies from diagnostics logs.",
|
|
89
|
+
summary: "Returns a structured performance report for the current or latest diagnostics session.",
|
|
90
|
+
inputSchema: performanceReportSchema.jsonSchema,
|
|
91
|
+
tags: ["diagnostics", "performance", "profiling"],
|
|
92
|
+
examples: [{ name: "Current session", description: "Inspect the active MCP session trace", arguments: {} }],
|
|
93
|
+
async execute(args) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = performanceReportSchema.parse(args ?? {});
|
|
96
|
+
const scope = parsed.scope ?? "current";
|
|
97
|
+
const filePath = resolveDiagnosticSessionFile(scope) ?? resolveDiagnosticSessionFile("latest");
|
|
98
|
+
if (!filePath) {
|
|
99
|
+
throw new ToolExecutionError("No diagnostics session file is available yet");
|
|
100
|
+
}
|
|
101
|
+
const report = summarizeDiagnosticsSession(filePath, {
|
|
102
|
+
includeTimeline: parsed.includeTimeline !== false,
|
|
103
|
+
maxEntries: Number(parsed.maxEntries ?? 25),
|
|
104
|
+
});
|
|
105
|
+
return jsonResult(report, {
|
|
106
|
+
success: true,
|
|
107
|
+
filePath,
|
|
108
|
+
sessionId: report.sessionId,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error instanceof ToolError)
|
|
113
|
+
return toolErrorResult(error);
|
|
114
|
+
return unknownErrorResult(error);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
},
|
|
75
118
|
];
|
|
@@ -4,6 +4,10 @@ import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, un
|
|
|
4
4
|
import { normalizeErrorDetails } from "./util.js";
|
|
5
5
|
import { promises as fs } from "node:fs";
|
|
6
6
|
import { dirname, resolve as resolvePath } from "node:path";
|
|
7
|
+
function supportsMachinePause(ctx) {
|
|
8
|
+
const platformId = ctx.platform?.id ?? ctx.client?.type;
|
|
9
|
+
return platformId !== "vice";
|
|
10
|
+
}
|
|
7
11
|
function parseAddressNumeric(value) {
|
|
8
12
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
9
13
|
throw new ToolValidationError("Address must be a non-empty string", { path: "$.address" });
|
|
@@ -214,10 +218,11 @@ export const tools = [
|
|
|
214
218
|
if (stride < 63) {
|
|
215
219
|
throw new ToolValidationError("Stride must be at least 63 bytes to avoid overlapping sprites", { path: "$.stride" });
|
|
216
220
|
}
|
|
217
|
-
const
|
|
221
|
+
const canPause = pauseDuringRead && supportsMachinePause(ctx);
|
|
222
|
+
const pauseResume = canPause
|
|
218
223
|
? await ctx.client.pause()
|
|
219
224
|
: { success: true };
|
|
220
|
-
if (
|
|
225
|
+
if (canPause && !pauseResume.success) {
|
|
221
226
|
throw new ToolExecutionError("Pause failed before sprite scan", { details: normalizeErrorDetails(pauseResume.details) });
|
|
222
227
|
}
|
|
223
228
|
try {
|
|
@@ -267,13 +272,14 @@ export const tools = [
|
|
|
267
272
|
length,
|
|
268
273
|
stride,
|
|
269
274
|
candidates,
|
|
275
|
+
paused: canPause,
|
|
270
276
|
},
|
|
271
277
|
sprites,
|
|
272
278
|
outputFiles,
|
|
273
279
|
}, { success: true });
|
|
274
280
|
}
|
|
275
281
|
finally {
|
|
276
|
-
if (
|
|
282
|
+
if (canPause) {
|
|
277
283
|
await ctx.client.resume();
|
|
278
284
|
}
|
|
279
285
|
}
|
|
@@ -331,10 +337,11 @@ export const tools = [
|
|
|
331
337
|
// Default: scan common locations
|
|
332
338
|
scanLocations.push(...commonLocations);
|
|
333
339
|
}
|
|
334
|
-
const
|
|
340
|
+
const canPause = pauseDuringRead && supportsMachinePause(ctx);
|
|
341
|
+
const pauseResume = canPause
|
|
335
342
|
? await ctx.client.pause()
|
|
336
343
|
: { success: true };
|
|
337
|
-
if (
|
|
344
|
+
if (canPause && !pauseResume.success) {
|
|
338
345
|
throw new ToolExecutionError("Pause failed before charset scan", { details: normalizeErrorDetails(pauseResume.details) });
|
|
339
346
|
}
|
|
340
347
|
try {
|
|
@@ -383,10 +390,11 @@ export const tools = [
|
|
|
383
390
|
charset: analysisWithoutBytes,
|
|
384
391
|
candidates: candidates.length,
|
|
385
392
|
outputFile,
|
|
393
|
+
paused: canPause,
|
|
386
394
|
}, { success: true });
|
|
387
395
|
}
|
|
388
396
|
finally {
|
|
389
|
-
if (
|
|
397
|
+
if (canPause) {
|
|
390
398
|
await ctx.client.resume();
|
|
391
399
|
}
|
|
392
400
|
}
|
package/dist/tools/meta/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export const metaModule = defineToolModule({
|
|
|
30
30
|
summary: "High-level meta tools that orchestrate multiple MCP actions.",
|
|
31
31
|
resources: ["c64://context/bootstrap", "c64://specs/assembly"],
|
|
32
32
|
defaultTags: ["meta", "orchestration", "experimental"],
|
|
33
|
+
supportedPlatforms: ["c64u", "vice"],
|
|
33
34
|
workflowHints: [
|
|
34
35
|
"Use meta tools to reduce round-trips by composing several steps into one.",
|
|
35
36
|
],
|
|
@@ -5,6 +5,10 @@ import { promises as fs } from "node:fs";
|
|
|
5
5
|
import { dirname, resolve as resolvePath } from "node:path";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { normalizeErrorDetails } from "./util.js";
|
|
8
|
+
function supportsMachinePause(ctx) {
|
|
9
|
+
const platformId = ctx.platform?.id ?? ctx.client?.type;
|
|
10
|
+
return platformId !== "vice";
|
|
11
|
+
}
|
|
8
12
|
function hexClean(input) {
|
|
9
13
|
const trimmed = input.trim();
|
|
10
14
|
const withoutPrefix = trimmed.startsWith("$") ? trimmed.slice(1) : trimmed;
|
|
@@ -94,9 +98,14 @@ export const tools = [
|
|
|
94
98
|
const maskBytes = parsed.mask ? hexToBytes(parsed.mask) : undefined;
|
|
95
99
|
const verifyLen = expectedBytes.length;
|
|
96
100
|
const readLen = Math.max(writeBytes.length, verifyLen);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
const canPause = supportsMachinePause(ctx);
|
|
102
|
+
let paused = false;
|
|
103
|
+
if (canPause) {
|
|
104
|
+
const pauseResult = await ctx.client.pause();
|
|
105
|
+
if (!pauseResult.success) {
|
|
106
|
+
throw new ToolExecutionError("C64 firmware reported failure while pausing", { details: normalizeErrorDetails(pauseResult.details) });
|
|
107
|
+
}
|
|
108
|
+
paused = true;
|
|
100
109
|
}
|
|
101
110
|
let preReadHex;
|
|
102
111
|
try {
|
|
@@ -147,10 +156,13 @@ export const tools = [
|
|
|
147
156
|
wrote: bytesToHex(writeBytes),
|
|
148
157
|
preRead: preReadHex,
|
|
149
158
|
postRead: post.data ?? "",
|
|
159
|
+
paused,
|
|
150
160
|
}, { success: true });
|
|
151
161
|
}
|
|
152
162
|
finally {
|
|
153
|
-
|
|
163
|
+
if (paused) {
|
|
164
|
+
await ctx.client.resume();
|
|
165
|
+
}
|
|
154
166
|
}
|
|
155
167
|
}
|
|
156
168
|
catch (error) {
|
|
@@ -176,7 +188,8 @@ export const tools = [
|
|
|
176
188
|
const outputPath = resolvePath(String(parsed.outputPath));
|
|
177
189
|
const outDir = dirname(outputPath);
|
|
178
190
|
await fs.mkdir(outDir, { recursive: true });
|
|
179
|
-
|
|
191
|
+
const canPause = pause && supportsMachinePause(ctx);
|
|
192
|
+
if (canPause) {
|
|
180
193
|
const res = await ctx.client.pause();
|
|
181
194
|
if (!res.success) {
|
|
182
195
|
throw new ToolExecutionError("Pause failed before dump", { details: normalizeErrorDetails(res.details) });
|
|
@@ -233,13 +246,14 @@ export const tools = [
|
|
|
233
246
|
format: (parsed.format ?? "hex").toString().toLowerCase(),
|
|
234
247
|
checksum,
|
|
235
248
|
outputPath,
|
|
249
|
+
paused: canPause,
|
|
236
250
|
createdAt: new Date().toISOString(),
|
|
237
251
|
};
|
|
238
252
|
await fs.writeFile(`${outputPath}.json`, JSON.stringify(manifest, null, 2), "utf8");
|
|
239
253
|
return jsonResult({ manifest }, { success: true });
|
|
240
254
|
}
|
|
241
255
|
finally {
|
|
242
|
-
if (
|
|
256
|
+
if (canPause) {
|
|
243
257
|
await ctx.client.resume();
|
|
244
258
|
}
|
|
245
259
|
}
|