# Implementation Plan: High-Frequency Editor Operations > **Goal**: Fill the 9 highest-impact tool gaps identified in the Cocos Creator business-development workflow analysis. > > **Current state**: 101 tools (core=37, full=101), version 0.4.0 > **Target state**: 110 tools (core=38, full=110) ## Tool Summary | # | Tool | Profile | Side | Phase | New file? | |---|---|---|---|---|---| | 1 | `create_scene` | `full` | editor | 1 | `lib/tools/scene-management.js` | | 2 | `query_scene_state` | `core` | editor | 1 | same | | 3 | `copy_paste_node` | `full` | editor | 1 | same | | 4 | `rename_node` | `full` | scene | 2 | scene.js + scene-management.js | | 5 | `reparent_node` | `full` | scene | 2 | same | | 6 | `create_prefab` | `full` | editor | 3 | prefabs.js + tool-registry.js | | 7 | `create_script` | `full` | editor | 4 | `lib/tools/scripts.js` | | 8 | `batch_asset_ops` | `full` | editor | 5 | assets-advanced.js (append) | | 9 | `find_unused_assets` | `full` | editor | 5 | same | ## Files Changed | File | Action | Phase | |---|---|---| | `lib/tools/scene-management.js` | **New** — factory for tools 1-5 | 1, 2 | | `scene.js` | **Modify** — add `renameNode`, `reparentNode` methods | 2 | | `lib/prefabs.js` | **Modify** — add `createPrefab` helper | 3 | | `lib/tool-registry.js` | **Modify** — import + spread new factories, add `create_prefab` inline | 1-5 | | `lib/tools/scripts.js` | **New** — factory for tool 7 | 4 | | `lib/tools/assets-advanced.js` | **Modify** — append tools 8-9 | 5 | | `docs/TOOLS.md` | **Regenerate** via `npm run docs:generate` | final | | `README.md` / `README_CN.md` | **Modify** — add new tool categories | final | | `CHANGELOG.md` | **Modify** — add `## [0.5.0]` entry | final | | `test/*.test.js` | **New** — unit tests for pure-logic helpers | each phase | --- ## Phase 1: Scene Management Tools **File**: `lib/tools/scene-management.js` (new) **Dependencies**: `{ createSchema, getRuntimeContext }` — uses `Editor.Message.request` directly (editor-side), no scene bridge needed for tools 1-3. **Imports**: Reuse `tryEditorRequests` from `./cocos-project.js`. ### Tool 1: `create_scene` ```js { name: 'create_scene', profile: 'full', description: '[core] Create a new scene asset in the Cocos project and open it.', inputSchema: createSchema( { sceneName: { type: 'string', description: 'Name of the new scene (without extension).' }, savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/scenes/NewScene.scene.' }, open: { type: 'boolean', description: 'Whether to open the scene after creation. Default true.' }, }, ['sceneName', 'savePath'] ), handler: async (args) => { ... } } ``` **Implementation approach**: Use `Editor.Message.request('asset-db', 'create-asset', savePath, '')` to create an empty `.scene` file. Cocos Creator's asset-db will generate a valid scene template automatically when the file extension is `.scene`. Then optionally `openAsset(savePath)`. Fallback: if `create-asset` doesn't auto-generate scene content, write a minimal scene JSON template to disk and let asset-db import it. ```js // Pseudocode const result = await Editor.Message.request('asset-db', 'create-asset', args.savePath, ''); // result contains { uuid, url, ... } if (args.open !== false) { await openAsset(result.url || args.savePath); } return { created: true, sceneName: args.sceneName, url: result.url, uuid: result.uuid }; ``` **IPC mapping**: - `asset-db: create-asset` (primary) - `asset-db: import-asset` (fallback if create-asset doesn't work for scenes) **Error handling**: Throw if `savePath` doesn't end with `.scene`. Throw if asset already exists. --- ### Tool 2: `query_scene_state` ```js { name: 'query_scene_state', profile: 'core', description: '[specialist] Query scene state: dirty (unsaved changes), ready, or soft-reload.', inputSchema: createSchema( { action: { type: 'string', enum: ['is_dirty', 'is_ready', 'soft_reload'], description: 'State query or action.' }, }, ['action'] ), handler: async (args) => { ... } } ``` **Why `core`**: `is_dirty` is a critical signal for AI to decide whether to save before making changes. This is the only new tool promoted to `core`. **IPC mapping**: - `is_dirty` → `Editor.Message.request('scene', 'query-is-dirty')` → returns boolean - `is_ready` → `Editor.Message.request('scene', 'query-ready')` → returns boolean - `soft_reload` → `Editor.Message.request('scene', 'soft-reload')` → reloads scene from disk **Implementation**: ```js const IPC_MAP = { is_dirty: { channel: 'scene', method: 'query-is-dirty' }, is_ready: { channel: 'scene', method: 'query-ready' }, soft_reload: { channel: 'scene', method: 'soft-reload' }, }; const candidate = IPC_MAP[args.action]; const result = await tryEditorRequests([candidate]); return { action: args.action, result: result.result }; ``` --- ### Tool 3: `copy_paste_node` ```js { name: 'copy_paste_node', profile: 'full', description: '[core] Copy, cut, or paste scene nodes via the Cocos editor clipboard.', inputSchema: createSchema( { action: { type: 'string', enum: ['copy', 'cut', 'paste'], description: 'Clipboard action.' }, uuids: { type: 'array', items: { type: 'string' }, description: 'Node UUIDs to copy or cut.' }, target: { type: 'string', description: 'Target parent node UUID for paste.' }, keepWorldTransform: { type: 'boolean', description: 'Keep world transform on paste. Default false.' }, }, ['action'] ), handler: async (args) => { ... } } ``` **IPC mapping**: - `copy` → `Editor.Message.request('scene', 'copy-node', args.uuids)` - `cut` → `Editor.Message.request('scene', 'cut-node', args.uuids)` - `paste` → `Editor.Message.request('scene', 'paste-node', { target: args.target, keepWorldTransform: args.keepWorldTransform })` **Validation**: - `copy`/`cut` requires `uuids` non-empty - `paste` requires `target` --- ## Phase 2: Node Operations **Files**: `scene.js` (add methods) + `lib/tools/scene-management.js` (add tool definitions) ### Tool 4: `rename_node` (scene-side) **scene.js method** — add to `exports.methods`: ```js async renameNode(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const oldName = node.name; const oldPath = getNodePath(node); const newName = String(options.newName || '').trim(); if (!newName) { throw new Error('newName is required.'); } node.name = newName; return { renamed: true, oldName, newName, oldPath, newPath: getNodePath(node), uuid: node.uuid, }; }, ``` **Tool definition** (in `scene-management.js`): ```js { name: 'rename_node', profile: 'full', description: '[core] Rename a scene node.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node UUID.' }, name: { type: 'string', description: 'Fallback exact node name.' }, newName: { type: 'string', description: 'New name for the node.' }, }, ['newName'] ), handler: async (args) => sceneBridge.call('renameNode', args), } ``` **Dependencies**: `{ createSchema, sceneBridge }` — add `sceneBridge` to the factory's dependency bag. --- ### Tool 5: `reparent_node` (scene-side) **scene.js method** — add to `exports.methods`: ```js async reparentNode(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const oldPath = getNodePath(node); const oldParent = node.parent; let newParent; if (options.targetUuid) { newParent = findNodeByUuid(options.targetUuid); } else if (options.targetPath) { newParent = findNodeByPath(options.targetPath); } else if (options.targetName) { newParent = findNodeByName(options.targetName); } if (!newParent) { throw new Error('Target parent node was not found.'); } const siblingIndex = Number.isFinite(options.siblingIndex) ? options.siblingIndex : -1; if (siblingIndex >= 0 && siblingIndex <= newParent.children.length) { newParent.insertChild(node, siblingIndex); } else { node.parent = newParent; } return { reparented: true, uuid: node.uuid, oldPath, newPath: getNodePath(node), oldParent: oldParent ? oldParent.name : null, newParent: newParent.name, siblingIndex: node.getSiblingIndex(), }; }, ``` **Tool definition** (in `scene-management.js`): ```js { name: 'reparent_node', profile: 'full', description: '[core] Move a node to a new parent in the scene hierarchy.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path to move.' }, uuid: { type: 'string', description: 'Node UUID to move.' }, name: { type: 'string', description: 'Fallback exact node name.' }, targetPath: { type: 'string', description: 'New parent node path.' }, targetUuid: { type: 'string', description: 'New parent node UUID.' }, targetName: { type: 'string', description: 'Fallback exact new parent name.' }, siblingIndex: { type: 'number', description: 'Optional insert position among siblings. Default: append.' }, }, [] ), handler: async (args) => sceneBridge.call('reparentNode', args), } ``` --- ## Phase 3: Prefab Creation **Files**: `lib/prefabs.js` (add helper) + `lib/tool-registry.js` (add inline tool) ### Tool 6: `create_prefab` **lib/prefabs.js** — add `createPrefab` helper: ```js async function createPrefab(options = {}) { const nodeUuid = String(options.nodeUuid || '').trim(); const savePath = String(options.savePath || '').trim(); if (!nodeUuid) throw new Error('nodeUuid is required.'); if (!savePath) throw new Error('savePath is required.'); const result = await Editor.Message.request('scene', 'create-prefab', nodeUuid, savePath); return { created: true, nodeUuid, savePath, result, }; } ``` **Tool definition** (inline in `tool-registry.js`, near existing prefab tools ~line 760): ```js { name: 'create_prefab', profile: 'full', description: '[core] Create a prefab asset from a scene node.', inputSchema: createSchema( { nodeUuid: { type: 'string', description: 'UUID of the scene node to save as prefab.' }, savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/prefabs/Enemy.prefab.' }, }, ['nodeUuid', 'savePath'] ), handler: async (args) => createPrefab(args), }, ``` **IPC mapping**: `Editor.Message.request('scene', 'create-prefab', nodeUuid, savePath)` **Notes**: The `create-prefab` scene message is well-documented in Cocos Creator 3.8+. It takes a node UUID and a target `.prefab` path, creates the prefab asset, and links the scene node as a prefab instance. --- ## Phase 4: Script Creation **File**: `lib/tools/scripts.js` (new) **Dependencies**: `{ createSchema, getRuntimeContext }` — uses `fs` for file writing + `Editor.Message.request('asset-db', 'refresh-asset')` for refresh. ### Tool 7: `create_script` ```js { name: 'create_script', profile: 'full', description: '[core] Create a new TypeScript component script with a standard Cocos template.', inputSchema: createSchema( { scriptName: { type: 'string', description: 'Class name in PascalCase, e.g. PlayerController.' }, savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/scripts/PlayerController.ts.' }, template: { type: 'string', enum: ['component', 'plain'], description: 'Script template. Default: component.' }, properties: { type: 'array', items: { type: 'object' }, description: 'Optional @property declarations.' }, }, ['scriptName', 'savePath'] ), handler: async (args) => { ... } } ``` **Template generation** (pure logic, unit-testable): ```js function generateComponentTemplate(className, properties = []) { const props = properties.map(p => { const type = p.type || 'number'; const name = p.name || 'property'; return ` @property({ type: ${type} })\n ${name}: ${type} = ${p.default || '0'};`; }).join('\n\n'); return `import { _decorator, Component, ${properties.map(p => p.type).filter(t => t && t !== 'number' && t !== 'string' && t !== 'boolean').join(', ')} } from 'cc'; const { ccclass, property } = _decorator; @ccclass('${className}') export class ${className} extends Component { ${props || ' // Add properties here'} }`; ``` **Implementation**: 1. Generate template string from `scriptName` + `template` + `properties` 2. Write to project filesystem via `resolveProjectPath` + `fs.writeFileSync` 3. Refresh asset-db: `Editor.Message.request('asset-db', 'refresh-asset', savePath)` 4. Return `{ created: true, scriptName, savePath, className }` **Unit tests** (test/scripts.test.js): - `generateComponentTemplate('PlayerController')` → contains `@ccclass`, `export class PlayerController extends Component` - `generateComponentTemplate('Enemy', [{name:'speed',type:'number',default:'5'}])` → contains `@property({ type: number })` and `speed: number = 5` - `generatePlainTemplate('Utils')` → no `@ccclass`, just `export class Utils` --- ## Phase 5: Asset Batch Operations **File**: `lib/tools/assets-advanced.js` (append to existing) **Dependencies**: `{ createSchema, getRuntimeContext }` — uses `Editor.Message.request('asset-db', ...)` and `listAssets`. ### Tool 8: `batch_asset_ops` ```js { name: 'batch_asset_ops', profile: 'full', description: '[core] Batch import or delete assets in the Cocos asset database.', inputSchema: createSchema( { action: { type: 'string', enum: ['import', 'delete'], description: 'Batch operation.' }, sourceDirectory: { type: 'string', description: 'Local filesystem source (import only).' }, targetDirectory: { type: 'string', description: 'Asset-db target URL, e.g. db://assets/textures (import only).' }, urls: { type: 'array', items: { type: 'string' }, description: 'Asset URLs to delete (delete only).' }, fileFilter: { type: 'array', items: { type: 'string' }, description: 'File extensions, e.g. [".png", ".jpg"].' }, recursive: { type: 'boolean', description: 'Import recursively. Default false.' }, overwrite: { type: 'boolean', description: 'Overwrite existing assets. Default false.' }, }, ['action'] ), handler: async (args) => { ... } } ``` **IPC mapping**: - `import` → `Editor.Message.request('asset-db', 'import', targetDirectory, sourceDirectory)` - `delete` → `Editor.Message.request('asset-db', 'delete-assets', urls)` **Validation**: - `import` requires `sourceDirectory` + `targetDirectory` - `delete` requires `urls` non-empty ### Tool 9: `find_unused_assets` ```js { name: 'find_unused_assets', profile: 'full', description: '[core] Find assets not referenced by any scene, prefab, or script in the project.', inputSchema: createSchema( { directory: { type: 'string', description: 'Asset-db directory to scan. Default: db://assets.' }, excludeDirectories: { type: 'array', items: { type: 'string' }, description: 'Directories to exclude.' }, assetTypes: { type: 'array', items: { type: 'string' }, description: 'Filter by asset type, e.g. ["cc.Texture2D", "cc.AudioClip"].' }, limit: { type: 'number', description: 'Max results. Default 200.' }, }, [] ), handler: async (args) => { ... } } ``` **Implementation** (pure-logic core, unit-testable): ```js async function findUnusedAssets(projectPath, options = {}) { // 1. Query all assets in the directory const allAssets = await listAssets({ pattern: options.directory || 'db://assets/**', ccType: options.assetTypes, }); // 2. Build a set of all referenced UUIDs by scanning: // - All .scene files // - All .prefab files // - All .ts/.js files (for dynamic asset references) // - All .anim files // - All .meta files (for sub-asset references) const referencedUuids = new Set(); // ... scan files, collect UUIDs using collectUuidReferences from assets-advanced.js // 3. Filter: assets whose UUID is NOT in referencedUuids const unused = allAssets.filter(asset => !referencedUuids.has(asset.uuid)); return { totalScanned: allAssets.length, unusedCount: unused.length, unused: unused.slice(0, options.limit || 200), }; } ``` **Unit tests** (test/find-unused-assets.test.js): - Mock `listAssets` + file scanning → verify correct filtering - Test exclude directories - Test asset type filtering --- ## Registration ### `lib/tool-registry.js` changes **1. Add imports** (top of file): ```js const { createSceneManagementTools } = require('./tools/scene-management'); const { createScriptTools } = require('./tools/scripts'); const { createPrefab } = require('./prefabs'); // add to existing destructure ``` **2. Spread factories** into the `tools` array: ```js // After existing cocos-project tools (~line 717) ...createSceneManagementTools({ createSchema, sceneBridge, getRuntimeContext }), // After existing prefab tools (~line 780) { name: 'create_prefab', // ... inline definition handler: async (args) => createPrefab(args), }, // After existing file tools (~line 1315) ...createScriptTools({ createSchema, getRuntimeContext }), // After existing assets-advanced tools (~line 1023) // batch_asset_ops and find_unused_assets are appended inside // createAssetsAdvancedTools via the existing factory ``` **3. Pass `sceneBridge` to `createSceneManagementTools`** — currently only `createSceneEventTools` receives `sceneBridge`. Add it to the scene-management factory call. ### `lib/tools/assets-advanced.js` changes Append `batch_asset_ops` and `find_unused_assets` tool definitions to the `createAssetsAdvancedTools` return array. Add `getRuntimeContext` to the factory's dependency bag (already received). ### `lib/tools/scene-management.js` factory signature ```js function createSceneManagementTools({ createSchema, sceneBridge, getRuntimeContext }) { return [ // Tool 1: create_scene (editor-side, uses getRuntimeContext for projectPath) // Tool 2: query_scene_state (editor-side, uses tryEditorRequests) // Tool 3: copy_paste_node (editor-side, uses tryEditorRequests) // Tool 4: rename_node (scene-side, uses sceneBridge.call) // Tool 5: reparent_node (scene-side, uses sceneBridge.call) ]; } module.exports = { createSceneManagementTools }; ``` --- ## Implementation Order Execute phases sequentially. Each phase ends with `npm run check && npm test` before proceeding. ``` Phase 1 (tools 1-3) ──▶ npm run check && npm test │ Phase 2 (tools 4-5) ──▶ npm run check && npm test │ Phase 3 (tool 6) ──▶ npm run check && npm test │ Phase 4 (tool 7) ──▶ npm run check && npm test │ Phase 5 (tools 8-9) ──▶ npm run check && npm test │ Final: docs:generate ──▶ docs:check ──▶ release:check ──▶ pack:dry-run ``` ## Verification Checklist (per phase) - [ ] `npm run check` — syntax check all files - [ ] `npm test` — unit tests pass - [ ] `npm run docs:generate` — regenerate TOOLS.md (final phase only) - [ ] `npm run docs:check` — docs in sync (final phase only) - [ ] `npm run release:check` — release metadata valid (final phase only) - [ ] `npm run pack:dry-run` — package builds (final phase only) ## Version Bump - `package.json` version: `0.4.0` → `0.5.0` - `server.json` version(s): update to match - `CHANGELOG.md`: add `## [0.5.0] - 2026-06-30` entry - Git tag: `v0.5.0` ## Expected Outcome | Metric | Before | After | |---|---|---| | `full` profile tools | 101 | 110 (+9) | | `core` profile tools | 37 | 38 (+1: `query_scene_state`) | | New files | — | `lib/tools/scene-management.js`, `lib/tools/scripts.js` | | Modified files | — | `scene.js`, `lib/prefabs.js`, `lib/tool-registry.js`, `lib/tools/assets-advanced.js` | | New scene.js methods | — | `renameNode`, `reparentNode` | | New unit test files | — | `test/scene-management.test.js`, `test/scripts.test.js`, `test/find-unused-assets.test.js` | | Gaps filled | Scene creation, node clipboard, node rename/reparent, prefab creation, script creation, batch assets, unused asset scan | All high-frequency gaps covered |