c64bridge 1.0.7 → 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 +69 -14
- package/data/assembly/assembly-spec.md +2 -0
- package/dist/c64Client.js +238 -60
- package/dist/config.js +48 -26
- package/dist/device.js +92 -37
- package/dist/diagnostics.js +62 -2
- package/dist/hostPaths.js +128 -0
- package/dist/loggingHttpClient.js +59 -1
- package/dist/mcp-server.js +409 -286
- package/dist/numericArg.js +55 -0
- package/dist/petscii.js +13 -1
- package/dist/platform.js +12 -3
- package/dist/prompts/registry.js +6 -1
- package/dist/tools/access.js +66 -0
- package/dist/tools/debug.js +6 -0
- package/dist/tools/developer.js +67 -34
- package/dist/tools/errors.js +54 -3
- package/dist/tools/input.js +23 -6
- package/dist/tools/machineControl.js +112 -20
- 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 +15 -6
- 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 +16 -4
- 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 +25 -475
- 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 +1983 -6910
- package/mcp.json +21 -1
- package/package.json +16 -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 +35 -2
- package/dist/.tsbuildinfo +0 -1
- package/doc/agent-state-spec.md +0 -243
- package/doc/audit/secret-handling.md +0 -61
- 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
|
@@ -3,7 +3,9 @@ import { jsonResult } from "../responses.js";
|
|
|
3
3
|
import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult } from "../errors.js";
|
|
4
4
|
import { normalizeErrorDetails } from "./util.js";
|
|
5
5
|
import { promises as fs } from "node:fs";
|
|
6
|
-
import { dirname
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { resolveHostChildPath, resolveHostPath } from "../../hostPaths.js";
|
|
8
|
+
import { acquireOwnedPause, resumeWarning } from "../pauseGuard.js";
|
|
7
9
|
function supportsMachinePause(ctx) {
|
|
8
10
|
const platformId = ctx.platform?.id ?? ctx.client?.type;
|
|
9
11
|
return platformId !== "vice";
|
|
@@ -214,14 +216,14 @@ export const tools = [
|
|
|
214
216
|
throw new ToolValidationError("Stride must be at least 63 bytes to avoid overlapping sprites", { path: "$.stride" });
|
|
215
217
|
}
|
|
216
218
|
const canPause = pauseDuringRead && supportsMachinePause(ctx);
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
throw new ToolExecutionError("Pause failed before sprite scan", { details: normalizeErrorDetails(pauseResume.details) });
|
|
219
|
+
const owned = await acquireOwnedPause(ctx.client, canPause);
|
|
220
|
+
const pinned = owned.facade;
|
|
221
|
+
if (canPause && !owned.result.success) {
|
|
222
|
+
throw new ToolExecutionError("Pause failed before sprite scan", { details: normalizeErrorDetails(owned.result.details) });
|
|
222
223
|
}
|
|
224
|
+
let released = false;
|
|
223
225
|
try {
|
|
224
|
-
const read = await ctx.client.readMemory(`$${formatAddressHex(startAddress)}`, String(length));
|
|
226
|
+
const read = await ctx.client.readMemory(`$${formatAddressHex(startAddress)}`, String(length), pinned);
|
|
225
227
|
if (!read.success || typeof read.data !== "string") {
|
|
226
228
|
throw new ToolExecutionError("C64 backend reported failure while reading memory", { details: normalizeErrorDetails(read.details) });
|
|
227
229
|
}
|
|
@@ -251,16 +253,25 @@ export const tools = [
|
|
|
251
253
|
}
|
|
252
254
|
let outputFiles = [];
|
|
253
255
|
if (parsed.outputDir && sprites.length > 0) {
|
|
254
|
-
const resolvedDir =
|
|
256
|
+
const resolvedDir = resolveHostPath(String(parsed.outputDir), { purpose: "write" });
|
|
255
257
|
await fs.mkdir(resolvedDir, { recursive: true });
|
|
256
258
|
outputFiles = await Promise.all(sprites.map(async (sprite) => {
|
|
257
|
-
const filePath =
|
|
259
|
+
const filePath = resolveHostChildPath(resolvedDir, `sprite_${sprite.address.replace("$", "").toLowerCase()}.spr`);
|
|
258
260
|
await fs.mkdir(dirname(filePath), { recursive: true });
|
|
259
261
|
await fs.writeFile(filePath, Buffer.from(hexClean(sprite.bytesHex), "hex"));
|
|
260
262
|
sprite.filePath = filePath;
|
|
261
263
|
return { path: filePath, address: sprite.address };
|
|
262
264
|
}));
|
|
263
265
|
}
|
|
266
|
+
let warnings;
|
|
267
|
+
if (canPause) {
|
|
268
|
+
released = true;
|
|
269
|
+
const result = await owned.release();
|
|
270
|
+
if (!result.success) {
|
|
271
|
+
warnings = [resumeWarning(result.details)];
|
|
272
|
+
ctx.logger.warn("C64 resume reported failure after sprite scan", { details: result.details });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
264
275
|
return jsonResult({
|
|
265
276
|
scanned: {
|
|
266
277
|
address: `$${formatAddressHex(startAddress)}`,
|
|
@@ -271,11 +282,14 @@ export const tools = [
|
|
|
271
282
|
},
|
|
272
283
|
sprites,
|
|
273
284
|
outputFiles,
|
|
285
|
+
...(warnings ? { warnings } : {}),
|
|
274
286
|
}, { success: true });
|
|
275
287
|
}
|
|
276
288
|
finally {
|
|
277
|
-
if (canPause) {
|
|
278
|
-
await
|
|
289
|
+
if (canPause && !released) {
|
|
290
|
+
const result = await owned.release();
|
|
291
|
+
if (!result.success)
|
|
292
|
+
ctx.logger.warn("C64 resume reported failure after sprite scan", { details: result.details });
|
|
279
293
|
}
|
|
280
294
|
}
|
|
281
295
|
}
|
|
@@ -333,18 +347,18 @@ export const tools = [
|
|
|
333
347
|
scanLocations.push(...commonLocations);
|
|
334
348
|
}
|
|
335
349
|
const canPause = pauseDuringRead && supportsMachinePause(ctx);
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
throw new ToolExecutionError("Pause failed before charset scan", { details: normalizeErrorDetails(pauseResume.details) });
|
|
350
|
+
const owned = await acquireOwnedPause(ctx.client, canPause);
|
|
351
|
+
const pinned = owned.facade;
|
|
352
|
+
if (canPause && !owned.result.success) {
|
|
353
|
+
throw new ToolExecutionError("Pause failed before charset scan", { details: normalizeErrorDetails(owned.result.details) });
|
|
341
354
|
}
|
|
355
|
+
let released = false;
|
|
342
356
|
try {
|
|
343
357
|
const candidates = [];
|
|
344
358
|
for (const startAddress of scanLocations) {
|
|
345
359
|
if (startAddress + charsetSize - 1 > 0xFFFF)
|
|
346
360
|
continue;
|
|
347
|
-
const read = await ctx.client.readMemory(`$${formatAddressHex(startAddress)}`, String(charsetSize));
|
|
361
|
+
const read = await ctx.client.readMemory(`$${formatAddressHex(startAddress)}`, String(charsetSize), pinned);
|
|
348
362
|
if (!read.success || typeof read.data !== "string") {
|
|
349
363
|
continue; // Skip this location if read fails
|
|
350
364
|
}
|
|
@@ -357,11 +371,22 @@ export const tools = [
|
|
|
357
371
|
candidates.push({ ...analysis, bytes });
|
|
358
372
|
}
|
|
359
373
|
}
|
|
374
|
+
// Resume as soon as the reads are done, so a failure can be reported in the result.
|
|
375
|
+
let warnings;
|
|
376
|
+
if (canPause) {
|
|
377
|
+
released = true;
|
|
378
|
+
const result = await owned.release();
|
|
379
|
+
if (!result.success) {
|
|
380
|
+
warnings = [resumeWarning(result.details)];
|
|
381
|
+
ctx.logger.warn("C64 resume reported failure after charset scan", { details: result.details });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
360
384
|
if (candidates.length === 0) {
|
|
361
385
|
return jsonResult({
|
|
362
386
|
found: false,
|
|
363
387
|
scanned: scanLocations.length,
|
|
364
388
|
message: "No valid charsets found matching criteria",
|
|
389
|
+
...(warnings ? { warnings } : {}),
|
|
365
390
|
}, { success: true });
|
|
366
391
|
}
|
|
367
392
|
// Sort by non-empty chars descending, then by entropy descending
|
|
@@ -374,7 +399,7 @@ export const tools = [
|
|
|
374
399
|
const bestCandidate = candidates[0];
|
|
375
400
|
let outputFile = null;
|
|
376
401
|
if (parsed.outputPath && bestCandidate.bytes) {
|
|
377
|
-
const resolvedPath =
|
|
402
|
+
const resolvedPath = resolveHostPath(String(parsed.outputPath), { purpose: "write" });
|
|
378
403
|
await fs.mkdir(dirname(resolvedPath), { recursive: true });
|
|
379
404
|
await fs.writeFile(resolvedPath, Buffer.from(bestCandidate.bytes));
|
|
380
405
|
outputFile = { path: resolvedPath, address: bestCandidate.address };
|
|
@@ -386,11 +411,14 @@ export const tools = [
|
|
|
386
411
|
candidates: candidates.length,
|
|
387
412
|
outputFile,
|
|
388
413
|
paused: canPause,
|
|
414
|
+
...(warnings ? { warnings } : {}),
|
|
389
415
|
}, { success: true });
|
|
390
416
|
}
|
|
391
417
|
finally {
|
|
392
|
-
if (canPause) {
|
|
393
|
-
await
|
|
418
|
+
if (canPause && !released) {
|
|
419
|
+
const result = await owned.release();
|
|
420
|
+
if (!result.success)
|
|
421
|
+
ctx.logger.warn("C64 resume reported failure after charset scan", { details: result.details });
|
|
394
422
|
}
|
|
395
423
|
}
|
|
396
424
|
}
|
|
@@ -2,10 +2,12 @@ import { objectSchema, stringSchema, numberSchema, optionalSchema, booleanSchema
|
|
|
2
2
|
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
|
-
import { dirname
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
import { resolveHostPath } from "../../hostPaths.js";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
7
8
|
import { normalizeErrorDetails } from "./util.js";
|
|
8
9
|
import { stripHexByteSeparators } from "../hex.js";
|
|
10
|
+
import { acquireOwnedPause, resumeWarning } from "../pauseGuard.js";
|
|
9
11
|
function supportsMachinePause(ctx) {
|
|
10
12
|
const platformId = ctx.platform?.id ?? ctx.client?.type;
|
|
11
13
|
return platformId !== "vice";
|
|
@@ -94,16 +96,17 @@ export const tools = [
|
|
|
94
96
|
const readLen = Math.max(writeBytes.length, verifyLen);
|
|
95
97
|
const canPause = supportsMachinePause(ctx);
|
|
96
98
|
let paused = false;
|
|
99
|
+
const owned = await acquireOwnedPause(ctx.client, canPause);
|
|
100
|
+
const pinned = owned.facade;
|
|
97
101
|
if (canPause) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
throw new ToolExecutionError("C64 backend reported failure while pausing", { details: normalizeErrorDetails(pauseResult.details) });
|
|
102
|
+
if (!owned.result.success) {
|
|
103
|
+
throw new ToolExecutionError("C64 backend reported failure while pausing", { details: normalizeErrorDetails(owned.result.details) });
|
|
101
104
|
}
|
|
102
105
|
paused = true;
|
|
103
106
|
}
|
|
104
107
|
let preReadHex;
|
|
105
108
|
try {
|
|
106
|
-
const pre = await ctx.client.readMemory(parsed.address, String(Math.max(1, readLen)));
|
|
109
|
+
const pre = await ctx.client.readMemory(parsed.address, String(Math.max(1, readLen)), pinned);
|
|
107
110
|
if (!pre.success) {
|
|
108
111
|
throw new ToolExecutionError("C64 backend reported failure while reading memory", { details: normalizeErrorDetails(pre.details) });
|
|
109
112
|
}
|
|
@@ -123,11 +126,11 @@ export const tools = [
|
|
|
123
126
|
throw new ToolExecutionError("Verification failed before write", { details: { mismatches: errors, address: parsed.address } });
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
|
-
const write = await ctx.client.writeMemory(parsed.address, bytesToHex(writeBytes));
|
|
129
|
+
const write = await ctx.client.writeMemory(parsed.address, bytesToHex(writeBytes), pinned);
|
|
127
130
|
if (!write.success) {
|
|
128
131
|
throw new ToolExecutionError("C64 backend reported failure while writing memory", { details: normalizeErrorDetails(write.details) });
|
|
129
132
|
}
|
|
130
|
-
const post = await ctx.client.readMemory(parsed.address, String(Math.max(1, writeBytes.length)));
|
|
133
|
+
const post = await ctx.client.readMemory(parsed.address, String(Math.max(1, writeBytes.length)), pinned);
|
|
131
134
|
if (!post.success) {
|
|
132
135
|
throw new ToolExecutionError("C64 backend reported failure while reading back memory", { details: normalizeErrorDetails(post.details) });
|
|
133
136
|
}
|
|
@@ -145,17 +148,29 @@ export const tools = [
|
|
|
145
148
|
if (diffs.length > 0) {
|
|
146
149
|
throw new ToolExecutionError("Post-write verification failed", { details: { address: parsed.address, diffs } });
|
|
147
150
|
}
|
|
151
|
+
let warnings;
|
|
152
|
+
if (paused) {
|
|
153
|
+
paused = false; // released here, so the finally block must not release again
|
|
154
|
+
const released = await owned.release();
|
|
155
|
+
if (!released.success) {
|
|
156
|
+
warnings = [resumeWarning(released.details)];
|
|
157
|
+
ctx.logger.warn("C64 resume reported failure after verified write", { details: released.details });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
148
160
|
return jsonResult({
|
|
149
161
|
address: parsed.address,
|
|
150
162
|
wrote: bytesToHex(writeBytes),
|
|
151
163
|
preRead: preReadHex,
|
|
152
164
|
postRead: post.data ?? "",
|
|
153
165
|
paused,
|
|
166
|
+
...(warnings ? { warnings } : {}),
|
|
154
167
|
}, { success: true });
|
|
155
168
|
}
|
|
156
169
|
finally {
|
|
157
170
|
if (paused) {
|
|
158
|
-
await
|
|
171
|
+
const released = await owned.release();
|
|
172
|
+
if (!released.success)
|
|
173
|
+
ctx.logger.warn("C64 resume reported failure after verified write", { details: released.details });
|
|
159
174
|
}
|
|
160
175
|
}
|
|
161
176
|
}
|
|
@@ -179,18 +194,18 @@ export const tools = [
|
|
|
179
194
|
const length = parsed.length;
|
|
180
195
|
const chunk = Math.min(Math.max(1, parsed.chunkSize ?? 512), 4096);
|
|
181
196
|
const pause = parsed.pauseDuringRead !== false;
|
|
182
|
-
const outputPath =
|
|
197
|
+
const outputPath = resolveHostPath(String(parsed.outputPath), { purpose: "write" });
|
|
183
198
|
const outDir = dirname(outputPath);
|
|
184
199
|
await fs.mkdir(outDir, { recursive: true });
|
|
185
200
|
const canPause = pause && supportsMachinePause(ctx);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
201
|
+
const owned = await acquireOwnedPause(ctx.client, canPause);
|
|
202
|
+
const pinned = owned.facade;
|
|
203
|
+
if (canPause && !owned.result.success) {
|
|
204
|
+
throw new ToolExecutionError("Pause failed before dump", { details: normalizeErrorDetails(owned.result.details) });
|
|
191
205
|
}
|
|
192
206
|
const buf = Buffer.allocUnsafe(length);
|
|
193
207
|
let offset = 0;
|
|
208
|
+
let released = false;
|
|
194
209
|
try {
|
|
195
210
|
while (offset < length) {
|
|
196
211
|
const remaining = length - offset;
|
|
@@ -205,7 +220,7 @@ export const tools = [
|
|
|
205
220
|
while (!success && attempts <= (parsed.retries ?? 1)) {
|
|
206
221
|
attempts += 1;
|
|
207
222
|
try {
|
|
208
|
-
const r = await ctx.client.readMemory(`$${formatAddressHex(addr)}`, String(take));
|
|
223
|
+
const r = await ctx.client.readMemory(`$${formatAddressHex(addr)}`, String(take), pinned);
|
|
209
224
|
if (!r.success || typeof r.data !== "string") {
|
|
210
225
|
throw new ToolExecutionError("Firmware returned failure for chunk", { details: normalizeErrorDetails(r.details) });
|
|
211
226
|
}
|
|
@@ -243,12 +258,23 @@ export const tools = [
|
|
|
243
258
|
paused: canPause,
|
|
244
259
|
createdAt: new Date().toISOString(),
|
|
245
260
|
};
|
|
246
|
-
await fs.writeFile(`${outputPath}.json`, JSON.stringify(manifest, null, 2), "utf8");
|
|
247
|
-
|
|
261
|
+
await fs.writeFile(resolveHostPath(`${outputPath}.json`, { purpose: "write" }), JSON.stringify(manifest, null, 2), "utf8");
|
|
262
|
+
let warnings;
|
|
263
|
+
if (canPause) {
|
|
264
|
+
released = true;
|
|
265
|
+
const result = await owned.release();
|
|
266
|
+
if (!result.success) {
|
|
267
|
+
warnings = [resumeWarning(result.details)];
|
|
268
|
+
ctx.logger.warn("C64 resume reported failure after dump", { details: result.details });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return jsonResult({ manifest, ...(warnings ? { warnings } : {}) }, { success: true });
|
|
248
272
|
}
|
|
249
273
|
finally {
|
|
250
|
-
if (canPause) {
|
|
251
|
-
await
|
|
274
|
+
if (canPause && !released) {
|
|
275
|
+
const result = await owned.release();
|
|
276
|
+
if (!result.success)
|
|
277
|
+
ctx.logger.warn("C64 resume reported failure after dump", { details: result.details });
|
|
252
278
|
}
|
|
253
279
|
}
|
|
254
280
|
}
|
|
@@ -5,6 +5,7 @@ import { measureSidSilence } from "../audio.js";
|
|
|
5
5
|
import { getPlatformStatus } from "../../platform.js";
|
|
6
6
|
import { promises as fs } from "node:fs";
|
|
7
7
|
import { resolve as resolvePath, join as joinPath } from "node:path";
|
|
8
|
+
import { defaultHostOutputDir, resolveHostChildPath, resolveHostPath } from "../../hostPaths.js";
|
|
8
9
|
import { sleep, formatTimestampSpec } from "./util.js";
|
|
9
10
|
import { getTasksHomeDir } from "./background.js";
|
|
10
11
|
import { Jimp } from "jimp";
|
|
@@ -325,7 +326,9 @@ export const tools = [
|
|
|
325
326
|
const captureScreenshot = visibleViceFastPath ? false : parsed.captureScreenshot !== false;
|
|
326
327
|
const restoreActiveBackend = parsed.restoreActiveBackend !== false;
|
|
327
328
|
const outputPath = (captureScreenshot && requestedBackends.some((backend) => backend !== "u2")) || parsed.outputPath
|
|
328
|
-
?
|
|
329
|
+
? (parsed.outputPath
|
|
330
|
+
? resolveHostPath(String(parsed.outputPath), { purpose: "write" })
|
|
331
|
+
: defaultHostOutputDir("artifacts", "greetings", `run_${Date.now()}`))
|
|
329
332
|
: undefined;
|
|
330
333
|
const timeoutMs = parsed.timeoutMs ?? 1500;
|
|
331
334
|
const pollIntervalMs = parsed.pollIntervalMs ?? 100;
|
|
@@ -434,7 +437,7 @@ export const tools = [
|
|
|
434
437
|
await timedStep("persist_screenshot", async () => {
|
|
435
438
|
screenshotAnalysis = analyseCapturedFrame(frame);
|
|
436
439
|
screenshotPath = outputPath
|
|
437
|
-
?
|
|
440
|
+
? resolveHostChildPath(outputPath, `${backend}.png`)
|
|
438
441
|
: undefined;
|
|
439
442
|
if (screenshotPath) {
|
|
440
443
|
await writeCapturedFramePng(frame, screenshotPath);
|
|
@@ -514,7 +517,7 @@ export const tools = [
|
|
|
514
517
|
...(restoreError ? { restoreError } : {}),
|
|
515
518
|
};
|
|
516
519
|
if (outputPath) {
|
|
517
|
-
const reportPath =
|
|
520
|
+
const reportPath = resolveHostChildPath(outputPath, "results.json");
|
|
518
521
|
await fs.writeFile(reportPath, JSON.stringify(payload, null, 2), "utf8");
|
|
519
522
|
payload.outputPath = outputPath;
|
|
520
523
|
payload.reportPath = reportPath;
|
|
@@ -571,7 +574,7 @@ export const tools = [
|
|
|
571
574
|
}
|
|
572
575
|
// Prepare output directory
|
|
573
576
|
const outputPath = parsed.outputPath
|
|
574
|
-
?
|
|
577
|
+
? resolveHostPath(String(parsed.outputPath), { purpose: "write" })
|
|
575
578
|
: resolvePath(joinPath(getTasksHomeDir(), `shuffle_${Date.now()}`));
|
|
576
579
|
await fs.mkdir(outputPath, { recursive: true });
|
|
577
580
|
const results = [];
|
|
@@ -619,7 +622,7 @@ export const tools = [
|
|
|
619
622
|
});
|
|
620
623
|
}
|
|
621
624
|
// Write log
|
|
622
|
-
const logPath =
|
|
625
|
+
const logPath = resolveHostChildPath(outputPath, "shuffle.json");
|
|
623
626
|
await fs.writeFile(logPath, JSON.stringify({ programs: results, summary: { total: results.length, errors: results.filter(r => r.error).length } }, null, 2), "utf8");
|
|
624
627
|
return jsonResult({ outputPath, programs: results.length, errors: results.filter(r => r.error).length, logPath }, { success: true });
|
|
625
628
|
}
|
|
@@ -660,7 +663,7 @@ export const tools = [
|
|
|
660
663
|
const durationMs = parsed.durationMs ?? 2000;
|
|
661
664
|
const resetDelayMs = parsed.resetDelayMs ?? 100;
|
|
662
665
|
const outputPath = parsed.outputPath
|
|
663
|
-
?
|
|
666
|
+
? resolveHostPath(String(parsed.outputPath), { purpose: "write" })
|
|
664
667
|
: resolvePath(joinPath(getTasksHomeDir(), `batch_${Date.now()}`));
|
|
665
668
|
await fs.mkdir(outputPath, { recursive: true });
|
|
666
669
|
const results = [];
|
|
@@ -768,7 +771,7 @@ export const tools = [
|
|
|
768
771
|
}
|
|
769
772
|
}
|
|
770
773
|
// Write junit-like report
|
|
771
|
-
const reportPath =
|
|
774
|
+
const reportPath = resolveHostChildPath(outputPath, "results.json");
|
|
772
775
|
const summary = {
|
|
773
776
|
total: results.length,
|
|
774
777
|
passed: results.filter(r => r.status === "pass").length,
|
|
@@ -1,18 +1,120 @@
|
|
|
1
1
|
import { objectSchema, stringSchema, numberSchema, optionalSchema, booleanSchema } from "../schema.js";
|
|
2
2
|
import { jsonResult } from "../responses.js";
|
|
3
|
-
import { ToolError, ToolExecutionError, toolErrorResult, unknownErrorResult } from "../errors.js";
|
|
3
|
+
import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult } from "../errors.js";
|
|
4
4
|
import { sleep } from "./util.js";
|
|
5
5
|
function escapeRegex(s) {
|
|
6
6
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7
7
|
}
|
|
8
|
+
export const MAX_PATTERN_LENGTH = 200;
|
|
9
|
+
export const MAX_WAIT_TIMEOUT_MS = 120000;
|
|
10
|
+
export const MIN_POLL_INTERVAL_MS = 20;
|
|
11
|
+
/**
|
|
12
|
+
* Checks a user regular expression before it runs on the event loop. A pattern
|
|
13
|
+
* such as (a+)+$ takes exponential time on a screen of identical characters, so
|
|
14
|
+
* quantified groups that contain a quantifier are rejected, as are
|
|
15
|
+
* backreferences (also a source of super-linear matching).
|
|
16
|
+
*/
|
|
17
|
+
export function assertSafeScreenPattern(pattern) {
|
|
18
|
+
const path = "$.pattern";
|
|
19
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
20
|
+
throw new ToolValidationError(`Pattern is ${pattern.length} characters; the limit is ${MAX_PATTERN_LENGTH}`, { path });
|
|
21
|
+
}
|
|
22
|
+
// One entry per open group: whether it contains a quantifier.
|
|
23
|
+
const groups = [];
|
|
24
|
+
let inClass = false;
|
|
25
|
+
let previousWasGroupWithQuantifier = false;
|
|
26
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
27
|
+
const char = pattern[index];
|
|
28
|
+
const isQuantifier = !inClass && (char === "*" || char === "+" || char === "?" || (char === "{" && /^\{\d+(,\d*)?\}/.test(pattern.slice(index))));
|
|
29
|
+
if (char === "\\") {
|
|
30
|
+
const next = pattern[index + 1] ?? "";
|
|
31
|
+
if (!inClass && (/[1-9]/.test(next) || (next === "k" && pattern[index + 2] === "<"))) {
|
|
32
|
+
throw new ToolValidationError("Backreferences are not supported in screen patterns", { path });
|
|
33
|
+
}
|
|
34
|
+
index += 1;
|
|
35
|
+
previousWasGroupWithQuantifier = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (inClass) {
|
|
39
|
+
if (char === "]")
|
|
40
|
+
inClass = false;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (char === "[") {
|
|
44
|
+
inClass = true;
|
|
45
|
+
previousWasGroupWithQuantifier = false;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (char === "(") {
|
|
49
|
+
groups.push(false);
|
|
50
|
+
previousWasGroupWithQuantifier = false;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (char === ")") {
|
|
54
|
+
previousWasGroupWithQuantifier = groups.pop() ?? false;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (isQuantifier) {
|
|
58
|
+
// "(?" opens a group modifier, and a "?" after another quantifier makes it lazy; neither is a repeat.
|
|
59
|
+
const opensModifier = char === "?" && pattern[index - 1] === "(";
|
|
60
|
+
const isLazyMarker = char === "?" && "*+?}".includes(pattern[index - 1] ?? "");
|
|
61
|
+
if (!opensModifier && !isLazyMarker) {
|
|
62
|
+
if (previousWasGroupWithQuantifier) {
|
|
63
|
+
throw new ToolValidationError("Nested quantifiers (a repeated group that itself contains a repeat) are not supported in screen patterns", { path });
|
|
64
|
+
}
|
|
65
|
+
if (groups.length > 0)
|
|
66
|
+
groups[groups.length - 1] = true;
|
|
67
|
+
}
|
|
68
|
+
previousWasGroupWithQuantifier = false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
previousWasGroupWithQuantifier = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function compileScreenPattern(pattern, isRegex, caseInsensitive) {
|
|
75
|
+
const flags = caseInsensitive ? "i" : undefined;
|
|
76
|
+
if (!isRegex) {
|
|
77
|
+
return new RegExp(escapeRegex(pattern), flags);
|
|
78
|
+
}
|
|
79
|
+
assertSafeScreenPattern(pattern);
|
|
80
|
+
try {
|
|
81
|
+
return new RegExp(pattern, flags);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
throw new ToolValidationError(`Invalid regular expression: ${error instanceof Error ? error.message : String(error)}`, { path: "$.pattern" });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Polls the screen until the pattern matches or the timeout elapses. Does not throw on timeout. */
|
|
88
|
+
export async function waitForScreenText(client, options) {
|
|
89
|
+
const timeoutMs = Math.min(options.timeoutMs ?? 3000, MAX_WAIT_TIMEOUT_MS);
|
|
90
|
+
const intervalMs = Math.max(options.intervalMs ?? 100, MIN_POLL_INTERVAL_MS);
|
|
91
|
+
const regex = compileScreenPattern(options.pattern, options.isRegex ?? false, options.caseInsensitive ?? true);
|
|
92
|
+
const start = Date.now();
|
|
93
|
+
let lastScreen;
|
|
94
|
+
while (Date.now() - start < timeoutMs) {
|
|
95
|
+
const screen = await client.readScreen();
|
|
96
|
+
lastScreen = screen;
|
|
97
|
+
const match = screen.match(regex);
|
|
98
|
+
if (match && match.index !== undefined) {
|
|
99
|
+
return {
|
|
100
|
+
matched: true,
|
|
101
|
+
match: { index: match.index, length: match[0]?.length ?? 0, text: match[0] ?? "" },
|
|
102
|
+
elapsedMs: Date.now() - start,
|
|
103
|
+
screen,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
await sleep(intervalMs);
|
|
107
|
+
}
|
|
108
|
+
return { matched: false, elapsedMs: Date.now() - start, screen: lastScreen };
|
|
109
|
+
}
|
|
8
110
|
const waitForScreenTextArgsSchema = objectSchema({
|
|
9
111
|
description: "Poll screen until text or regex matches, or timeout elapses.",
|
|
10
112
|
properties: {
|
|
11
|
-
pattern: stringSchema({ description: "Substring or regex to find", minLength: 1 }),
|
|
113
|
+
pattern: stringSchema({ description: "Substring or regex to find (at most 200 characters; regex with nested quantifiers or backreferences is rejected)", minLength: 1, maxLength: 200 }),
|
|
12
114
|
isRegex: optionalSchema(booleanSchema({ description: "Interpret pattern as regular expression", default: false }), false),
|
|
13
115
|
caseInsensitive: optionalSchema(booleanSchema({ description: "Case-insensitive search", default: true }), true),
|
|
14
|
-
timeoutMs: optionalSchema(numberSchema({ description: "Overall timeout in milliseconds", integer: true, minimum: 1, default: 3000 }), 3000),
|
|
15
|
-
intervalMs: optionalSchema(numberSchema({ description: "Poll interval in milliseconds", integer: true, minimum:
|
|
116
|
+
timeoutMs: optionalSchema(numberSchema({ description: "Overall timeout in milliseconds (1-120000)", integer: true, minimum: 1, maximum: 120000, default: 3000 }), 3000),
|
|
117
|
+
intervalMs: optionalSchema(numberSchema({ description: "Poll interval in milliseconds (at least 20)", integer: true, minimum: 20, default: 100 }), 100),
|
|
16
118
|
},
|
|
17
119
|
required: ["pattern"],
|
|
18
120
|
additionalProperties: false,
|
|
@@ -28,28 +130,17 @@ export const tools = [
|
|
|
28
130
|
async execute(args, ctx) {
|
|
29
131
|
try {
|
|
30
132
|
const parsed = waitForScreenTextArgsSchema.parse(args ?? {});
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const match = screen.match(regex);
|
|
39
|
-
if (match && match.index !== undefined) {
|
|
40
|
-
return jsonResult({
|
|
41
|
-
matched: true,
|
|
42
|
-
match: {
|
|
43
|
-
index: match.index,
|
|
44
|
-
length: match[0]?.length ?? 0,
|
|
45
|
-
text: match[0] ?? "",
|
|
46
|
-
},
|
|
47
|
-
elapsedMs: Date.now() - start,
|
|
48
|
-
}, { success: true });
|
|
49
|
-
}
|
|
50
|
-
await sleep(Math.max(1, parsed.intervalMs ?? 100));
|
|
133
|
+
const result = await waitForScreenText(ctx.client, parsed);
|
|
134
|
+
if (result.matched) {
|
|
135
|
+
return jsonResult({
|
|
136
|
+
matched: true,
|
|
137
|
+
match: result.match,
|
|
138
|
+
elapsedMs: result.elapsedMs,
|
|
139
|
+
}, { success: true });
|
|
51
140
|
}
|
|
52
|
-
throw new ToolExecutionError("Timeout waiting for screen text", {
|
|
141
|
+
throw new ToolExecutionError("Timeout waiting for screen text", {
|
|
142
|
+
details: { pattern: parsed.pattern, timeoutMs: parsed.timeoutMs, elapsedMs: result.elapsedMs, screen: result.screen ?? null },
|
|
143
|
+
});
|
|
53
144
|
}
|
|
54
145
|
catch (error) {
|
|
55
146
|
if (error instanceof ToolError)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export async function acquireOwnedPause(client, enabled = true) {
|
|
2
|
+
const c = client;
|
|
3
|
+
const facade = typeof c.pinFacade === "function" ? await c.pinFacade() : undefined;
|
|
4
|
+
if (!enabled)
|
|
5
|
+
return { facade, result: { success: true }, release: async () => ({ success: true }) };
|
|
6
|
+
const result = typeof c.acquirePause === "function" ? await c.acquirePause(facade) : await c.pause(facade);
|
|
7
|
+
return {
|
|
8
|
+
facade,
|
|
9
|
+
result,
|
|
10
|
+
async release() {
|
|
11
|
+
try {
|
|
12
|
+
return typeof c.releasePause === "function" ? await c.releasePause(facade) : await c.resume(facade);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return { success: false, details: { message: error instanceof Error ? error.message : String(error) } };
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function resumeWarning(details) {
|
|
21
|
+
const text = details && typeof details === "object" && "message" in details ? String(details.message) : "";
|
|
22
|
+
return `The machine could not be resumed after the operation${text ? ` (${text})` : ""}; call c64_system with op 'resume'.`;
|
|
23
|
+
}
|