Scratch Project Reader
This skill reads the project JSON from the Scratch VM and extracts a human-readable summary of every sprite's scripts.
Prerequisites
This skill drives the browser via the playwright-cli skill. Ensure that skill is installed and the Scratch editor is open in the browser session (see scratch-project-edit skill for opening it).
Workflow
Step 1 — Connect to the VM
If window.vm is not yet set, find and expose it:
playwright-cli run-code "$(cat <<'EOF'
async page => await page.evaluate(() => {
if (window.vm) return 'VM already connected';
const root = document.body;
const queue = [root];
const visited = new Set();
while (queue.length > 0) {
const node = queue.shift();
if (visited.has(node)) continue;
visited.add(node);
const keys = Object.keys(node);
const reactKey = keys.find(
key => key.startsWith('__reactInternalInstance$') ||
key.startsWith('__reactFiber$')
);
if (reactKey) {
let fiber = node[reactKey];
while (fiber) {
if (fiber.memoizedProps && fiber.memoizedProps.vm) {
window.vm = fiber.memoizedProps.vm;
return 'VM found and exposed as window.vm';
}
fiber = fiber.return;
}
}
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
queue.push(node.children[i]);
}
}
}
return 'ERROR: VM not found — editor may not be loaded yet';
})
EOF
)"
If the VM is not found, wait a few seconds and retry:
playwright-cli run-code "async page => await page.waitForTimeout(3000)"
Step 2 — Read and Summarize the Project
Run the following code to extract sprites and scripts. It returns a structured text summary.
playwright-cli run-code "$(cat <<'EOF'
async page => await page.evaluate(() => {
if (!window.vm) return 'ERROR: window.vm is not set. Run Step 1 first.';
const project = JSON.parse(window.vm.toJSON());
// Build a block chain starting from a top-level block id
function buildChain(blockId, blocks, depth) {
if (!blockId || depth > 100) return [];
const block = blocks[blockId];
if (!block) return [];
const lines = [];
// Format inputs (only show non-shadow references)
const inputParts = [];
for (const [key, val] of Object.entries(block.inputs || {})) {
const [, ref] = val;
if (ref && typeof ref === 'string' && blocks[ref] && !blocks[ref].shadow) {
// Inline reporter block
const inner = blocks[ref];
const innerField = Object.values(inner.fields || {})[0];
inputParts.push(`${key}: [${inner.opcode}${innerField ? ' ' + innerField[0] : ''}]`);
} else if (ref && typeof ref === 'object') {
// Primitive literal
inputParts.push(`${key}: ${JSON.stringify(ref[1])}`);
}
}
// Format fields
const fieldParts = Object.entries(block.fields || {}).map(
([k, v]) => `${k}: ${v[0]}`
);
const allArgs = [...inputParts, ...fieldParts].join(', ');
const indent = ' '.repeat(depth);
lines.push(`${indent}${block.opcode}${allArgs ? ' (' + allArgs + ')' : ''}`);
// Recurse into sub-stacks (e.g. if/repeat bodies)
for (const [key, val] of Object.entries(block.inputs || {})) {
if (key === 'SUBSTACK' || key === 'SUBSTACK2') {
const [, substackId] = val;
if (substackId && typeof substackId === 'string') {
lines.push(`${indent} [${key}]:`);
lines.push(...buildChain(substackId, blocks, depth + 2));
}
}
}
// Follow next block in sequence
lines.push(...buildChain(block.next, blocks, depth));
return lines;
}
const output = [];
for (const target of project.targets) {
const label = target.isStage ? '=== Stage ===' : `=== Sprite: ${target.name} ===`;
output.push(label);
const blocks = target.blocks;
const topLevelIds = Object.keys(blocks).filter(id => blocks[id].topLevel);
if (topLevelIds.length === 0) {
output.push(' (no scripts)');
}
for (const startId of topLevelIds) {
output.push('');
output.push(...buildChain(startId, blocks, 1));
}
output.push('');
}
return output.join('\n');
})
EOF
)"
The return value is a plain-text summary like:
=== Stage ===
(no scripts)
=== Sprite: Sprite1 ===
event_whenflagclicked
motion_movesteps (STEPS: 10)
control_repeat (TIMES: 10)
[SUBSTACK]:
motion_turnright (DEGREES: 15)
Step 3 — Interpret the Output
Read the summary to understand:
| Term | Meaning |
|---|---|
event_whenflagclicked |
Script starts when green flag is clicked |
motion_movesteps |
Move N steps |
control_repeat |
Repeat loop with SUBSTACK body |
looks_sayforsecs |
Say bubble for N seconds |
data_setvariableto |
Set variable |
broadcast_broadcast |
Send a broadcast message |
Use the Scratch block opcode reference for the full opcode list when you encounter an unfamiliar opcode.
Step 4 — Get Full Raw JSON (Optional)
If you need the complete unabridged project data for a specific sprite, use:
playwright-cli run-code "$(cat <<'EOF'
async page => await page.evaluate((spriteName) => {
if (!window.vm) return 'ERROR: window.vm not set';
const project = JSON.parse(window.vm.toJSON());
const target = spriteName
? project.targets.find(t => t.name === spriteName)
: project.targets.find(t => !t.isStage);
if (!target) return 'Sprite not found: ' + spriteName;
return JSON.stringify(target.blocks, null, 2);
}, 'Sprite1')
EOF
)"
Pass the sprite name as the second argument to page.evaluate. This is useful when you need exact block IDs, input structures, or field values to modify later.
Step 5 — List Variables and Lists
playwright-cli run-code "$(cat <<'EOF'
async page => await page.evaluate(() => {
if (!window.vm) return 'ERROR: window.vm not set';
const project = JSON.parse(window.vm.toJSON());
const output = [];
for (const target of project.targets) {
const label = target.isStage ? 'Stage' : `Sprite: ${target.name}`;
const vars = Object.values(target.variables || {});
const lists = Object.values(target.lists || {});
if (vars.length || lists.length) {
output.push(label);
for (const [name, value] of vars) output.push(` var ${name} = ${JSON.stringify(value)}`);
for (const [name, items] of lists) output.push(` list ${name} = [${items.join(', ')}]`);
}
}
return output.length ? output.join('\n') : '(no variables or lists found)';
})
EOF
)"
Tips
1. Run Step 2 before reading any script
Always check the summary first. It tells you which sprite names and opcodes exist, preventing wasted injection attempts.
2. Combine with scratch-project-edit when editing
After reading the project to understand its structure, use window.updateSprite from the scratch-project-edit skill to make changes. Use the raw block IDs from Step 4 as anchors.
3. Shadow blocks are skipped intentionally
Shadow blocks (e.g., default numeric literal inputs) are omitted from the summary to reduce noise. Use Step 4 (raw JSON) if you need them.