@beremaran/godot-agent-loop 1.0.1 → 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.
Files changed (41) hide show
  1. package/README.md +59 -34
  2. package/addons/godot_agent_loop/README.md +26 -16
  3. package/addons/godot_agent_loop/plugin.cfg +3 -2
  4. package/addons/godot_agent_loop/plugin.gd +840 -25
  5. package/agent-plugin/.claude-plugin/plugin.json +1 -1
  6. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  7. package/agent-plugin/.mcp.json +1 -1
  8. package/agent-plugin/adapter-manifest.json +3 -3
  9. package/agent-plugin/pi/extension.ts +1 -1
  10. package/agent-plugin/skills/build-godot-game/SKILL.md +26 -5
  11. package/agent-plugin/skills/debug-godot-game/SKILL.md +20 -7
  12. package/agent-plugin/skills/ship-godot-game/SKILL.md +13 -3
  13. package/agent-plugin/skills/verify-godot-change/SKILL.md +17 -2
  14. package/build/authoring-session-manager.js +24 -1
  15. package/build/domain-tool-registries.js +5 -1
  16. package/build/editor-authoring-router.js +228 -0
  17. package/build/editor-bridge-protocol.js +2 -1
  18. package/build/editor-connection.js +29 -24
  19. package/build/editor-mutation-guard.js +3 -1
  20. package/build/editor-plugin-installer.js +2 -1
  21. package/build/editor-session-registry.js +392 -0
  22. package/build/editor-sync-queue.js +135 -0
  23. package/build/index.js +208 -29
  24. package/build/lifecycle-trace.js +80 -0
  25. package/build/scripts/mcp_editor_plugin.gd +840 -25
  26. package/build/scripts/mcp_interaction_server.gd +76 -2
  27. package/build/scripts/mcp_runtime/core_domain.gd +2 -1
  28. package/build/scripts/mcp_runtime/system_domain.gd +2 -0
  29. package/build/server-instructions.js +5 -5
  30. package/build/session-timing.js +17 -1
  31. package/build/tool-definitions.js +99 -6
  32. package/build/tool-handlers/game-tool-handlers.js +21 -4
  33. package/build/tool-handlers/lifecycle-tool-handlers.js +361 -27
  34. package/build/tool-handlers/project-handler-services.js +45 -6
  35. package/build/tool-handlers/project-tool-handlers.js +4 -4
  36. package/build/tool-manifest.js +29 -1
  37. package/build/tool-mutation-policy.js +1 -0
  38. package/build/tool-surface.js +4 -4
  39. package/build/utils.js +2 -2
  40. package/package.json +4 -3
  41. package/product.json +7 -5
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "godot-agent-loop",
4
4
  "displayName": "Godot Agent Loop",
5
- "version": "1.0.1",
5
+ "version": "1.1.0",
6
6
  "description": "Build, playtest, and prove Godot games with an MCP automation loop.",
7
7
  "author": {
8
8
  "name": "Berke Arslan"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "godot-agent-loop",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Build, playtest, and prove Godot games with an MCP automation loop.",
5
5
  "author": {
6
6
  "name": "Berke Arslan",
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@beremaran/godot-agent-loop@1.0.1"
7
+ "@beremaran/godot-agent-loop@1.1.0"
8
8
  ],
9
9
  "env": {
10
10
  "GODOT_MCP_TOOL_SURFACE": "compact"
@@ -2,15 +2,15 @@
2
2
  "$schema": "../docs/agent-adapter.schema.json",
3
3
  "name": "godot-agent-loop",
4
4
  "displayName": "Godot Agent Loop",
5
- "version": "1.0.1",
5
+ "version": "1.1.0",
6
6
  "description": "Build, playtest, and prove Godot games with an MCP automation loop.",
7
7
  "package": "@beremaran/godot-agent-loop",
8
8
  "mcp": {
9
- "command": ["npx", "-y", "@beremaran/godot-agent-loop@1.0.1"],
9
+ "command": ["npx", "-y", "@beremaran/godot-agent-loop@1.1.0"],
10
10
  "environment": {
11
11
  "GODOT_MCP_TOOL_SURFACE": "compact"
12
12
  },
13
- "defaultToolCount": 39,
13
+ "defaultToolCount": 40,
14
14
  "discoveryTool": "godot_tools"
15
15
  },
16
16
  "skills": [
@@ -98,7 +98,7 @@ export default function godotAgentLoopPi(pi: ExtensionAPI) {
98
98
  try {
99
99
  const launch = resolvePiServerLaunch();
100
100
  const next = new Client(
101
- { name: 'godot-agent-loop-pi', version: '1.0.1' },
101
+ { name: 'godot-agent-loop-pi', version: '1.1.0' },
102
102
  {
103
103
  capabilities: {},
104
104
  listChanged: {
@@ -9,6 +9,9 @@ Use the MCP tools for project changes and engine evidence. Do not add MCP
9
9
  autoloads, addons, or bridge files; `run_project` installs and removes its runtime
10
10
  bridge automatically.
11
11
 
12
+ Godot Agent Loop supports Godot 4.7 or later. Report an older project/engine as
13
+ outside the supported boundary before authoring.
14
+
12
15
  ## Workflow
13
16
 
14
17
  1. Inspect before writing.
@@ -17,25 +20,43 @@ bridge automatically.
17
20
  - Call `create_project` only when `project.godot` is absent.
18
21
  - Define the playable loop, controls, visible feedback, and objective before
19
22
  choosing nodes.
23
+ - If the user wants to watch, call `editor_session` with `ensure` before
24
+ authoring. Continue detached only when the returned state requires it, and
25
+ report every fallback or unsaved conflict.
20
26
  2. Author the smallest complete game.
21
- - Use `create_scene` and `add_node` for the scene tree.
27
+ - Prefer `editor_transaction` or editor-routed `create_scene`, `add_node`,
28
+ node mutation, and supported resource tools while attached. Group one
29
+ coherent human-readable change into one undo step.
22
30
  - Use `create_script` or `write_file`, then `attach_script`.
31
+ - Persist meaningful game hierarchy and resources in scenes. Use scripts for
32
+ behavior; build most persistent nodes in `_ready()` only when procedural
33
+ generation is requested or justified, and report that choice.
23
34
  - Use `manage_input_map` for named actions and `set_main_scene` for startup.
24
35
  - Use `godot_tools` `search` then `describe` when a specialized node/resource
25
36
  tool is needed; invoke it with `godot_tools` `call`.
26
37
  3. Validate static artifacts.
27
38
  - Call `validate_scripts` and fix every error.
28
39
  - Re-read the scene and project settings rather than trusting write responses.
40
+ - Run `analyze_project_integrity` and require
41
+ `main_scene_structure.meaningful_persisted_structure`, unless an explicit
42
+ procedural design was passed as `allowProceduralMainScene`.
29
43
  4. Run and exercise the game.
30
- - Call `run_project`, wait for connection, then inspect with
44
+ - Call realtime `run_project` for watched play, then inspect with
31
45
  `game_get_scene_tree`, `game_get_ui`, and `game_screenshot`.
32
- - Inject the documented controls with `game_key_press`, `game_click`, or the
33
- relevant input tool. Observe state before and after input.
46
+ - Prefer `game_wait_until` and `game_scenario` over repeated polling. Inject
47
+ controls, observe state before and after input, and keep waits bounded.
34
48
  5. Assert and finish.
35
49
  - Prefer `verify_project` for bounded node/log/screenshot assertions and
36
50
  `run_project_tests` for project tests.
37
51
  - Prove both ordinary play and the win/lose transition independently.
38
52
  - Inspect `get_debug_output` and `game_get_errors`; stop the project when done.
39
53
 
54
+ Before adding high-volume particles, trails, lights, shockwaves, fireworks, or
55
+ audio voices, establish an explicit frame/object/voice budget. For external
56
+ assets, verify source and license, record attribution or CC0 status, and confirm
57
+ the imported asset independently loads and renders or plays.
58
+
40
59
  Keep the project tree game-only. Remove temporary probes, screenshots, and test
41
- artifacts unless the user requested them as deliverables.
60
+ artifacts and MCP-owned transient bridges unless requested as deliverables.
61
+ Never remove a user's persistent addon. Report warnings, leaks, fallbacks,
62
+ unsupported metrics, and subjective polish that still needs human review.
@@ -8,11 +8,14 @@ description: Diagnose and fix a Godot 4 game that crashes, logs errors, renders
8
8
  Preserve the failing evidence, isolate one cause, make the smallest repair, and
9
9
  rerun the original reproduction. Do not begin with speculative edits.
10
10
 
11
+ Godot Agent Loop supports Godot 4.7 or later. Report an older project/engine as
12
+ outside the supported boundary before diagnosing engine behavior.
13
+
11
14
  ## Workflow
12
15
 
13
16
  1. Reproduce.
14
- - Call `run_project` and repeat the user's input with `game_key_press`,
15
- `game_click`, or the relevant input tool.
17
+ - Call realtime `run_project` for visual/timing complaints. Capture a
18
+ baseline, then repeat the user's exact input or bounded `game_scenario`.
16
19
  - Capture `get_debug_output`, `game_get_errors`, `game_get_logs`, scene/UI
17
20
  state, and a screenshot when rendering is involved.
18
21
  2. Classify the boundary.
@@ -21,17 +24,27 @@ rerun the original reproduction. Do not begin with speculative edits.
21
24
  (discover it through `godot_tools` when hidden).
22
25
  - Runtime state: use `game_get_scene_tree`, `game_get_ui`, and
23
26
  `game_get_node_info`.
24
- - Timing/input: use deterministic `game_wait`, then query input or time tools
25
- through `godot_tools` if needed.
27
+ - Timing/input: use `game_wait_until`; use deterministic waits only for
28
+ repeatable simulation checks, not as evidence of displayed frame pacing.
26
29
  3. Form one falsifiable hypothesis. Gather a second observation that distinguishes
27
30
  it from the nearest alternative. Use privileged property/method inspection only
28
31
  when the user enabled the `reflection` group; avoid `game_eval` by default.
32
+ - Change one independent variable per trial and rerun the same stress path.
33
+ Never infer one cause after disabling particles, shockwaves, audio, camera
34
+ shake, and other systems together.
35
+ - Test camera shake and any persistent per-frame effect independently.
36
+ - For a visual freeze or stutter, collect realtime FPS/frame-time and
37
+ process/render/GPU availability. Simulation counters alone are insufficient;
38
+ report GPU time as unavailable when the platform does not expose it.
29
39
  4. Stop the project before editing persistent files. Apply the minimal fix with
30
40
  the matching scene, script, resource, or settings tool.
31
41
  5. Validate and rerun.
32
42
  - Run static validation first.
33
- - Repeat the exact failing input and observation.
43
+ - Repeat the exact baseline/stress/recovery input and observation.
34
44
  - Use `verify_project` or `run_project_tests` for stable regression evidence.
35
45
  - Confirm errors are gone and adjacent behavior still works.
36
- 6. Stop the project and remove temporary probes. Report the root cause, changed
37
- artifact, reproduction, and independent passing evidence.
46
+ 6. Stop the project and remove temporary probes and MCP-owned transient bridge
47
+ artifacts. Report the root cause, isolated variable, changed artifact,
48
+ reproduction, independent passing evidence, every warning/error/ObjectDB or
49
+ orphan diagnostic, cleanup result, fallback, and unsupported metric. Never
50
+ remove a user's persistent addon.
@@ -8,6 +8,9 @@ description: Prepare and verify a Godot 4 game for a reproducible release. Use f
8
8
  Treat shipping as an evidence gate. Preserve the project until every requested
9
9
  target has a reproducible artifact and a recorded independent check.
10
10
 
11
+ Godot Agent Loop supports Godot 4.7 or later. Do not claim release readiness on
12
+ an older engine through this workflow.
13
+
11
14
  ## Workflow
12
15
 
13
16
  1. Establish the release boundary.
@@ -21,11 +24,15 @@ target has a reproducible artifact and a recorded independent check.
21
24
  imports without deleting source assets.
22
25
  - Inspect enabled addons with `manage_addon`; do not update or remove
23
26
  user-managed addons unless the user explicitly requests it.
27
+ - Require meaningful persisted main-scene structure or record the explicit
28
+ procedural design exception. Record every editor/file-backed fallback.
24
29
  3. Prove project behavior.
25
30
  - Discover and run project tests with `run_project_tests`.
26
31
  - Use `verify_project` for bounded startup, scene, log, and screenshot
27
32
  assertions. Exercise required win, lose, menu, save, or input paths.
28
- - Inspect `game_get_errors` and debug output, then stop the project.
33
+ - Prefer bounded scenarios and waits. Inspect `game_get_errors`, debug output,
34
+ performance metric availability, warnings, and live/static leak evidence,
35
+ then stop the project.
29
36
  4. Verify build-specific requirements.
30
37
  - Run `verify_dotnet_project` for C# projects and require a successful restore
31
38
  and build with the selected Godot .NET version.
@@ -38,9 +45,12 @@ target has a reproducible artifact and a recorded independent check.
38
45
  visible behavior with the acceptance criteria.
39
46
  6. Tear down and report.
40
47
  - Stop all MCP-owned projects/editors and remove only generated probes,
41
- temporary exports, screenshots, and logs that are not deliverables.
48
+ temporary exports, screenshots, logs, and MCP-owned transient bridge files
49
+ that are not deliverables. Never remove a user's persistent addon.
42
50
  - Recheck the project tree and processes. Report exact commands, versions,
43
- targets, hashes or sizes, assertions, limitations, and any unverified target.
51
+ targets, hashes or sizes, assertions, fallbacks, warnings/leaks, cleanup
52
+ state, unsupported metrics, subjective/manual-review gaps, limitations, and
53
+ any unverified target.
44
54
 
45
55
  Never claim an unavailable platform, signing identity, export template, or .NET
46
56
  toolchain passed. Separate locally proven artifacts from CI-only or manual gates.
@@ -8,28 +8,43 @@ description: Verify a Godot 4 project change with static checks and independent
8
8
  Turn the requested behavior into observable acceptance criteria before running
9
9
  the game. A successful mutation response is not evidence that the change works.
10
10
 
11
+ Godot Agent Loop supports Godot 4.7 or later. Report verification on an older
12
+ engine as outside the supported boundary.
13
+
11
14
  ## Workflow
12
15
 
13
16
  1. Check saved state.
14
17
  - Use `validate_script` or `validate_scripts` for code.
15
18
  - Use `read_scene`, `read_project_settings`, or `read_file` to confirm the
16
19
  intended artifact persisted.
20
+ - Run `analyze_project_integrity` when scene structure changed. Flag a trivial
21
+ scripted main scene unless procedural construction is an explicit design.
17
22
  2. Prefer compound verification.
18
23
  - Use `verify_project` for bounded startup, node/group/log assertions,
19
24
  optional screenshot evidence, and deterministic teardown.
20
25
  - Use `run_project_tests` with `discover` before `run` when tests exist.
26
+ - Prefer `game_wait_until` and `game_scenario` for bounded interaction evidence
27
+ rather than assembling short polling loops.
21
28
  3. Exercise interactive behavior when a compound assertion is insufficient.
22
- - Call `run_project`, then observe the baseline with `game_get_scene_tree`,
29
+ - Call realtime `run_project` for behavior a person watches, then observe the baseline with `game_get_scene_tree`,
23
30
  `game_get_ui`, `game_get_node_info`, and/or `game_screenshot`.
24
31
  - Inject input with `game_key_press`, `game_click`, or a specialized input tool
25
32
  discovered through `godot_tools`.
26
- - Wait only as long as the behavior requires with `game_wait`, then make the
33
+ - Wait only as long as the behavior requires with `game_wait_until`, then make the
27
34
  same observation again and compare the changed state.
28
35
  4. Check negative evidence.
29
36
  - Inspect `game_get_errors`, `get_debug_output`, and relevant logs.
30
37
  - Verify unrelated state still works when the change has regression risk.
38
+ - Treat warnings, ObjectDB leaks, orphan/resource diagnostics, bridge cleanup
39
+ failures, fallbacks, and unavailable metrics as evidence to resolve or
40
+ disclose, even when the positive assertion passes.
31
41
  5. Stop the project and report the exact assertions and evidence. If any
32
42
  acceptance criterion remains unobserved, say verification is incomplete.
33
43
 
44
+ Separate objective evidence (persisted values, node state, logs, timing,
45
+ rendered pixels) from subjective review (feel, audio quality, composition, and
46
+ polish). A structural check or screenshot proves neither aesthetics nor sound;
47
+ label those as manual review when no human judgment was obtained.
48
+
34
49
  Use privileged reflection only when the required group is already enabled; do
35
50
  not replace observable gameplay evidence with `game_eval`.
@@ -37,6 +37,14 @@ export class AuthoringSessionManager {
37
37
  }
38
38
  async execute(backend, params, projectPath) {
39
39
  return this.serialize(async () => {
40
+ const editorAttempt = await this.options.tryEditorOperation?.(backend.command, params, projectPath);
41
+ if (editorAttempt?.handled) {
42
+ if (!editorAttempt.result) {
43
+ throw new Error('Editor authoring router handled an operation without returning a result');
44
+ }
45
+ return editorAttempt.result;
46
+ }
47
+ const editorFallbackReason = editorAttempt?.fallbackReason;
40
48
  await this.ensureSession(projectPath);
41
49
  const state = this.current;
42
50
  if (!state?.connection.isConnected) {
@@ -63,13 +71,28 @@ export class AuthoringSessionManager {
63
71
  if (isMutatingAuthoringCommand(backend.command, params)) {
64
72
  const scenePath = affectedScenePath(params);
65
73
  const focusPath = authoringFocusPath(backend.command, params);
66
- this.options.onProjectWrite?.({
74
+ const syncResult = await this.options.onProjectWrite?.({
67
75
  project_path: projectPath,
68
76
  command: backend.command,
69
77
  ...(scenePath === undefined ? {} : { scene_path: toResourcePath(scenePath) }),
70
78
  ...(typeof params.resourcePath === 'string' ? { resource_path: toResourcePath(params.resourcePath) } : {}),
71
79
  ...(focusPath === undefined ? {} : { focus_path: focusPath }),
72
80
  });
81
+ if (syncResult) {
82
+ return {
83
+ stdout: JSON.stringify({
84
+ ...payload,
85
+ backend: 'authoring-session',
86
+ sync_status: syncResult.sync_status ?? 'failed',
87
+ editor_session: syncResult.editor_session ?? null,
88
+ fallback_reason: editorFallbackReason ?? syncResult.fallback_reason ?? null,
89
+ observed_target_state: syncResult.observed_target_state ?? null,
90
+ }),
91
+ stderr: '',
92
+ exitCode: 0,
93
+ signal: null,
94
+ };
95
+ }
73
96
  }
74
97
  return {
75
98
  stdout: typeof payload.stdout === 'string' ? payload.stdout : JSON.stringify(payload),
@@ -30,9 +30,13 @@ export function createLifecycleToolRegistry(handlers) {
30
30
  return {
31
31
  'godot_tools': args => handlers.handleGodotTools(args),
32
32
  'launch_editor': args => handlers.handleLaunchEditor(args),
33
+ 'editor_session': args => handlers.handleEditorSession(args),
33
34
  'editor_control': args => handlers.handleEditorControl(args),
35
+ 'editor_transaction': args => handlers.handleEditorTransaction(args),
34
36
  'run_project': args => handlers.handleRunProject(args),
35
37
  'verify_project': args => handlers.handleVerifyProject(args),
38
+ 'game_wait_until': args => handlers.handleGameWaitUntil(args),
39
+ 'game_scenario': args => handlers.handleGameScenario(args),
36
40
  'get_debug_output': () => handlers.handleGetDebugOutput(),
37
41
  'stop_project': () => handlers.handleStopProject(),
38
42
  'get_godot_version': () => handlers.handleGetGodotVersion(),
@@ -92,7 +96,7 @@ export function createProjectToolRegistry(handlers) {
92
96
  }
93
97
  export function createGameToolRegistry(handlers) {
94
98
  return {
95
- 'game_screenshot': () => handlers.handleGameScreenshot(),
99
+ 'game_screenshot': args => handlers.handleGameScreenshot(args),
96
100
  'game_visual_regression': args => handlers.handleGameVisualRegression(args),
97
101
  'game_click': args => handlers.handleGameClick(args),
98
102
  'game_key_press': args => handlers.handleGameKeyPress(args),
@@ -0,0 +1,228 @@
1
+ /** Maps legacy authoring commands onto atomic, undoable editor transactions. */
2
+ export class EditorAuthoringRouter {
3
+ options;
4
+ constructor(options) {
5
+ this.options = options;
6
+ }
7
+ async tryExecute(command, params, projectPath) {
8
+ const resourceRequest = editorResourceTransactionFor(command, params);
9
+ const request = resourceRequest ?? editorTransactionFor(command, params);
10
+ if (!request) {
11
+ return {
12
+ handled: false,
13
+ fallbackReason: `The editor backend does not support ${command}; using the declared authoring fallback`,
14
+ };
15
+ }
16
+ let session = await this.options.status(projectPath);
17
+ // A cooperative-lock probe or busy editor frame can briefly drop the TCP
18
+ // session. Reattach when discovery still identifies a live editor, but do
19
+ // not delay ordinary headless authoring when no editor record exists.
20
+ if (!session.connected && session.editor_pid !== null) {
21
+ session = await this.options.ensure(projectPath, 5_000);
22
+ }
23
+ if (!session.connected) {
24
+ return {
25
+ handled: false,
26
+ fallbackReason: `Editor session unavailable (${session.state})${session.reason ? `: ${session.reason}` : ''}`,
27
+ };
28
+ }
29
+ try {
30
+ const response = await this.options.send(projectPath, resourceRequest ? 'resource_transaction' : 'transaction', request, 30_000);
31
+ if (typeof response.error === 'string' && response.error.length > 0) {
32
+ return failedAttempt(`Editor transaction failed: ${response.error}`, response);
33
+ }
34
+ if (response.success !== true) {
35
+ return failedAttempt('Editor transaction returned an invalid result', response);
36
+ }
37
+ return {
38
+ handled: true,
39
+ result: {
40
+ stdout: JSON.stringify({
41
+ ...response,
42
+ backend: 'editor',
43
+ sync_status: 'acknowledged',
44
+ editor_session: session,
45
+ fallback_reason: null,
46
+ }),
47
+ stderr: '',
48
+ exitCode: 0,
49
+ signal: null,
50
+ },
51
+ };
52
+ }
53
+ catch (error) {
54
+ // The transaction may already have reached Godot. Never retry it through
55
+ // another backend, because doing so could duplicate a partial mutation.
56
+ return failedAttempt(`Editor transaction transport failed after dispatch: ${error instanceof Error ? error.message : String(error)}`);
57
+ }
58
+ }
59
+ }
60
+ function editorResourceTransactionFor(command, params) {
61
+ const resourcePath = stringParam(params, 'resourcePath');
62
+ if (!resourcePath)
63
+ return null;
64
+ if (command === 'authoring_create_resource') {
65
+ const resourceType = stringParam(params, 'resourceType');
66
+ if (!resourceType)
67
+ return null;
68
+ return {
69
+ resource_path: toResourcePath(resourcePath),
70
+ resource_type: resourceType,
71
+ name: `Create ${resourceType}`,
72
+ properties: objectParam(params, 'properties') ?? {},
73
+ };
74
+ }
75
+ if (command === 'authoring_manage_resource' && params.action === 'modify') {
76
+ return {
77
+ resource_path: toResourcePath(resourcePath),
78
+ name: `Modify ${resourcePath.split('/').at(-1) ?? 'resource'}`,
79
+ properties: objectParam(params, 'properties') ?? {},
80
+ };
81
+ }
82
+ return null;
83
+ }
84
+ function failedAttempt(message, details) {
85
+ return {
86
+ handled: true,
87
+ result: {
88
+ stdout: '',
89
+ stderr: details ? `${message}: ${JSON.stringify(details)}` : message,
90
+ exitCode: 1,
91
+ signal: null,
92
+ },
93
+ };
94
+ }
95
+ function editorTransactionFor(command, params) {
96
+ const scenePath = stringParam(params, 'scenePath');
97
+ if (!scenePath)
98
+ return null;
99
+ const base = {
100
+ scene_path: toResourcePath(scenePath),
101
+ save: true,
102
+ };
103
+ switch (command) {
104
+ case 'authoring_create_scene':
105
+ return {
106
+ ...base,
107
+ name: 'Create scene',
108
+ root_type: stringParam(params, 'rootNodeType') ?? 'Node2D',
109
+ operations: [{ op: 'save' }],
110
+ focus_path: '.',
111
+ };
112
+ case 'authoring_add_node': {
113
+ const nodeName = stringParam(params, 'nodeName');
114
+ const parentPath = normalizeNodePath(stringParam(params, 'parentNodePath') ?? '.');
115
+ if (!nodeName)
116
+ return null;
117
+ return {
118
+ ...base,
119
+ name: `Add ${nodeName}`,
120
+ operations: [{
121
+ op: 'add_node',
122
+ parent_path: parentPath,
123
+ node_type: stringParam(params, 'nodeType') ?? 'Node',
124
+ node_name: nodeName,
125
+ properties: objectParam(params, 'properties') ?? {},
126
+ }],
127
+ focus_path: joinNodePath(parentPath, nodeName),
128
+ };
129
+ }
130
+ case 'authoring_modify_node': {
131
+ const nodePath = normalizeNodePath(stringParam(params, 'nodePath') ?? '.');
132
+ return {
133
+ ...base,
134
+ name: `Modify ${nodePath}`,
135
+ operations: [{ op: 'set_properties', node_path: nodePath, properties: objectParam(params, 'properties') ?? {} }],
136
+ focus_path: nodePath,
137
+ };
138
+ }
139
+ case 'authoring_remove_node': {
140
+ const nodePath = normalizeNodePath(stringParam(params, 'nodePath') ?? '.');
141
+ return {
142
+ ...base,
143
+ name: `Remove ${nodePath}`,
144
+ operations: [{ op: 'remove_node', node_path: nodePath }],
145
+ focus_path: parentNodePath(nodePath),
146
+ };
147
+ }
148
+ case 'authoring_attach_script': {
149
+ const nodePath = normalizeNodePath(stringParam(params, 'nodePath') ?? '.');
150
+ const scriptPath = stringParam(params, 'scriptPath');
151
+ if (!scriptPath)
152
+ return null;
153
+ return {
154
+ ...base,
155
+ name: `Attach script to ${nodePath}`,
156
+ operations: [{ op: 'attach_script', node_path: nodePath, script_path: toResourcePath(scriptPath) }],
157
+ focus_path: nodePath,
158
+ };
159
+ }
160
+ case 'authoring_save_scene':
161
+ if (stringParam(params, 'newPath'))
162
+ return null;
163
+ return { ...base, name: 'Save scene', operations: [{ op: 'save' }], focus_path: '.' };
164
+ case 'authoring_manage_scene_structure':
165
+ return manageStructureTransaction(base, params);
166
+ default:
167
+ return null;
168
+ }
169
+ }
170
+ function manageStructureTransaction(base, params) {
171
+ const action = stringParam(params, 'action');
172
+ const nodePath = normalizeNodePath(stringParam(params, 'nodePath') ?? '.');
173
+ if (action === 'rename') {
174
+ const newName = stringParam(params, 'newName');
175
+ if (!newName)
176
+ return null;
177
+ return {
178
+ ...base,
179
+ name: `Rename ${nodePath}`,
180
+ operations: [{ op: 'rename_node', node_path: nodePath, name: newName }],
181
+ focus_path: joinNodePath(parentNodePath(nodePath), newName),
182
+ };
183
+ }
184
+ if (action === 'duplicate') {
185
+ return {
186
+ ...base,
187
+ name: `Duplicate ${nodePath}`,
188
+ operations: [{ op: 'duplicate_node', node_path: nodePath }],
189
+ };
190
+ }
191
+ if (action === 'move') {
192
+ const newParentPath = stringParam(params, 'newParentPath');
193
+ if (!newParentPath)
194
+ return null;
195
+ const normalizedParent = normalizeNodePath(newParentPath);
196
+ return {
197
+ ...base,
198
+ name: `Move ${nodePath}`,
199
+ operations: [{ op: 'reparent_node', node_path: nodePath, new_parent_path: normalizedParent }],
200
+ focus_path: joinNodePath(normalizedParent, nodePath.split('/').at(-1) ?? nodePath),
201
+ };
202
+ }
203
+ return null;
204
+ }
205
+ function stringParam(params, key) {
206
+ const value = params[key];
207
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
208
+ }
209
+ function objectParam(params, key) {
210
+ const value = params[key];
211
+ return value && typeof value === 'object' && !Array.isArray(value)
212
+ ? value
213
+ : undefined;
214
+ }
215
+ function toResourcePath(path) {
216
+ return path.startsWith('res://') ? path : `res://${path.replace(/^\/+/, '')}`;
217
+ }
218
+ function normalizeNodePath(path) {
219
+ const normalized = path.replace(/^\/?root\/?/, '').replace(/^\.\//, '').replace(/\/$/, '');
220
+ return normalized.length > 0 ? normalized : '.';
221
+ }
222
+ function parentNodePath(path) {
223
+ const slash = path.lastIndexOf('/');
224
+ return slash < 0 ? '.' : path.slice(0, slash);
225
+ }
226
+ function joinNodePath(parent, child) {
227
+ return parent === '.' ? child : `${parent.replace(/\/$/, '')}/${child}`;
228
+ }
@@ -1 +1,2 @@
1
- export const EDITOR_BRIDGE_PROTOCOL_VERSION = '1';
1
+ /** Protocol 2 replaces fixed environment credentials with per-editor discovery. */
2
+ export const EDITOR_BRIDGE_PROTOCOL_VERSION = '2';
@@ -2,6 +2,8 @@ import { createConnection } from 'node:net';
2
2
  import { EDITOR_BRIDGE_PROTOCOL_VERSION } from './editor-bridge-protocol.js';
3
3
  export class EditorBridgeCompatibilityError extends Error {
4
4
  }
5
+ export class EditorRequestTimeoutError extends Error {
6
+ }
5
7
  /** Authenticated newline-delimited bridge to the optional EditorPlugin. */
6
8
  export class EditorConnection {
7
9
  options;
@@ -16,16 +18,40 @@ export class EditorConnection {
16
18
  }
17
19
  async send(command, params = {}, timeoutMs = 10_000) {
18
20
  await this.ensureConnected(timeoutMs);
19
- if (command !== 'handshake')
20
- await this.ensureHandshake(timeoutMs);
21
+ if (command === 'handshake')
22
+ return this.authenticate(timeoutMs);
23
+ await this.authenticate(timeoutMs);
21
24
  return this.request(command, params, timeoutMs);
22
25
  }
26
+ async authenticate(timeoutMs = 10_000) {
27
+ await this.ensureConnected(timeoutMs);
28
+ if (!this.handshaking) {
29
+ const expected = this.options.protocolVersion ?? EDITOR_BRIDGE_PROTOCOL_VERSION;
30
+ this.handshaking = this.request('handshake', {
31
+ protocol_version: expected,
32
+ server_version: this.options.serverVersion ?? 'unknown',
33
+ }, timeoutMs).then(result => {
34
+ if (result.error || result.protocol_version !== expected) {
35
+ const actual = typeof result.protocol_version === 'string' ? result.protocol_version : 'missing';
36
+ throw new EditorBridgeCompatibilityError(`Godot Agent Loop editor protocol is incompatible: server ${expected}, addon ${actual}`);
37
+ }
38
+ return result;
39
+ });
40
+ }
41
+ try {
42
+ return await this.handshaking;
43
+ }
44
+ catch (error) {
45
+ this.disconnect();
46
+ throw error;
47
+ }
48
+ }
23
49
  request(command, params, timeoutMs) {
24
50
  const id = this.nextId++;
25
51
  return new Promise((resolve, reject) => {
26
52
  const timer = setTimeout(() => {
27
53
  this.pending.delete(id);
28
- reject(new Error(`Editor request '${command}' timed out after ${timeoutMs / 1000}s`));
54
+ reject(new EditorRequestTimeoutError(`Editor request '${command}' timed out after ${timeoutMs / 1000}s`));
29
55
  }, timeoutMs);
30
56
  this.pending.set(id, result => { clearTimeout(timer); resolve(result); });
31
57
  this.socket.write(`${JSON.stringify({ id, command, params, secret: this.options.secret })}\n`);
@@ -41,27 +67,6 @@ export class EditorConnection {
41
67
  }
42
68
  this.pending.clear();
43
69
  }
44
- async ensureHandshake(timeoutMs) {
45
- if (this.handshaking)
46
- return this.handshaking;
47
- const expected = this.options.protocolVersion ?? EDITOR_BRIDGE_PROTOCOL_VERSION;
48
- this.handshaking = this.request('handshake', {
49
- protocol_version: expected,
50
- server_version: this.options.serverVersion ?? 'unknown',
51
- }, timeoutMs).then(result => {
52
- if (result.error || result.protocol_version !== expected) {
53
- const actual = typeof result.protocol_version === 'string' ? result.protocol_version : 'missing';
54
- throw new EditorBridgeCompatibilityError(`Godot Agent Loop editor protocol is incompatible: server ${expected}, addon ${actual}`);
55
- }
56
- });
57
- try {
58
- await this.handshaking;
59
- }
60
- catch (error) {
61
- this.disconnect();
62
- throw error;
63
- }
64
- }
65
70
  async ensureConnected(timeoutMs) {
66
71
  if (this.socket)
67
72
  return;
@@ -13,8 +13,10 @@ export class EditorMutationGuard {
13
13
  async check(name, args) {
14
14
  if (!isToolCallMutating(name, args))
15
15
  return undefined;
16
+ if (typeof args.projectPath !== 'string')
17
+ return undefined;
16
18
  try {
17
- const state = await this.readDriverState(EDITOR_DRIVER_STATE_COMMAND, {}, EDITOR_DRIVER_STATE_TIMEOUT_MS);
19
+ const state = await this.readDriverState(args.projectPath, EDITOR_DRIVER_STATE_COMMAND, {}, EDITOR_DRIVER_STATE_TIMEOUT_MS);
18
20
  if (state.paused === true)
19
21
  return createErrorResponse(AGENT_MUTATIONS_PAUSED_MESSAGE);
20
22
  }