blueprint-extractor-mcp 8.2.6 → 8.2.7

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 (172) hide show
  1. package/README.md +23 -5
  2. package/dist/active-editor-session.d.ts +0 -0
  3. package/dist/active-editor-session.js +0 -0
  4. package/dist/automation-controller.d.ts +0 -0
  5. package/dist/automation-controller.js +0 -0
  6. package/dist/catalogs/example-catalog.d.ts +0 -0
  7. package/dist/catalogs/example-catalog.js +0 -0
  8. package/dist/compactor.d.ts +0 -0
  9. package/dist/compactor.js +0 -0
  10. package/dist/editor-instance-registry.d.ts +0 -0
  11. package/dist/editor-instance-registry.js +0 -0
  12. package/dist/editor-instance-types.d.ts +0 -0
  13. package/dist/editor-instance-types.js +0 -0
  14. package/dist/execution/adapters/commandlet-adapter.d.ts +14 -2
  15. package/dist/execution/adapters/commandlet-adapter.js +145 -54
  16. package/dist/execution/adapters/editor-adapter.d.ts +0 -0
  17. package/dist/execution/adapters/editor-adapter.js +0 -0
  18. package/dist/execution/adapters/lazy-commandlet-adapter.d.ts +0 -0
  19. package/dist/execution/adapters/lazy-commandlet-adapter.js +0 -0
  20. package/dist/execution/adaptive-executor.d.ts +9 -0
  21. package/dist/execution/adaptive-executor.js +74 -9
  22. package/dist/execution/execution-adapter.d.ts +0 -0
  23. package/dist/execution/execution-adapter.js +0 -0
  24. package/dist/execution/execution-mode-detector.d.ts +0 -0
  25. package/dist/execution/execution-mode-detector.js +0 -0
  26. package/dist/execution/index.d.ts +0 -0
  27. package/dist/execution/index.js +0 -0
  28. package/dist/helpers/active-editor-utils.d.ts +0 -0
  29. package/dist/helpers/active-editor-utils.js +0 -0
  30. package/dist/helpers/alias-registration.d.ts +0 -0
  31. package/dist/helpers/alias-registration.js +0 -0
  32. package/dist/helpers/blueprint-dsl-parser.d.ts +0 -0
  33. package/dist/helpers/blueprint-dsl-parser.js +0 -0
  34. package/dist/helpers/blueprint-validation.d.ts +0 -0
  35. package/dist/helpers/blueprint-validation.js +0 -0
  36. package/dist/helpers/capture.d.ts +0 -0
  37. package/dist/helpers/capture.js +0 -0
  38. package/dist/helpers/commonui-button-style.d.ts +0 -0
  39. package/dist/helpers/commonui-button-style.js +0 -0
  40. package/dist/helpers/composite-patterns.d.ts +3 -0
  41. package/dist/helpers/composite-patterns.js +0 -0
  42. package/dist/helpers/formatting.d.ts +0 -0
  43. package/dist/helpers/formatting.js +0 -0
  44. package/dist/helpers/live-coding.d.ts +0 -0
  45. package/dist/helpers/live-coding.js +0 -0
  46. package/dist/helpers/material-dsl-parser.d.ts +0 -0
  47. package/dist/helpers/material-dsl-parser.js +0 -0
  48. package/dist/helpers/mutation-filter.d.ts +0 -0
  49. package/dist/helpers/mutation-filter.js +0 -0
  50. package/dist/helpers/next-step-hints.d.ts +0 -0
  51. package/dist/helpers/next-step-hints.js +0 -0
  52. package/dist/helpers/operation-deny-list.d.ts +0 -0
  53. package/dist/helpers/operation-deny-list.js +0 -0
  54. package/dist/helpers/package-metadata.d.ts +0 -0
  55. package/dist/helpers/package-metadata.js +0 -0
  56. package/dist/helpers/phantom-filter.d.ts +0 -0
  57. package/dist/helpers/phantom-filter.js +0 -0
  58. package/dist/helpers/project-resolution.d.ts +0 -0
  59. package/dist/helpers/project-resolution.js +0 -0
  60. package/dist/helpers/project-utils.d.ts +0 -0
  61. package/dist/helpers/project-utils.js +0 -0
  62. package/dist/helpers/property-path-parser.d.ts +0 -0
  63. package/dist/helpers/property-path-parser.js +0 -0
  64. package/dist/helpers/property-shorthand.d.ts +0 -0
  65. package/dist/helpers/property-shorthand.js +0 -0
  66. package/dist/helpers/response-summarizer.d.ts +0 -0
  67. package/dist/helpers/response-summarizer.js +0 -0
  68. package/dist/helpers/slot-presets.d.ts +0 -0
  69. package/dist/helpers/slot-presets.js +0 -0
  70. package/dist/helpers/subsystem.d.ts +0 -0
  71. package/dist/helpers/subsystem.js +0 -0
  72. package/dist/helpers/token-budget.d.ts +0 -0
  73. package/dist/helpers/token-budget.js +0 -0
  74. package/dist/helpers/tool-help.d.ts +0 -0
  75. package/dist/helpers/tool-help.js +0 -0
  76. package/dist/helpers/tool-registration.d.ts +0 -0
  77. package/dist/helpers/tool-registration.js +54 -6
  78. package/dist/helpers/tool-results.d.ts +0 -0
  79. package/dist/helpers/tool-results.js +0 -0
  80. package/dist/helpers/verification.d.ts +0 -0
  81. package/dist/helpers/verification.js +0 -0
  82. package/dist/helpers/widget-class-aliases.d.ts +0 -0
  83. package/dist/helpers/widget-class-aliases.js +0 -0
  84. package/dist/helpers/widget-diff-parser.d.ts +0 -0
  85. package/dist/helpers/widget-diff-parser.js +0 -0
  86. package/dist/helpers/widget-dsl-parser.d.ts +0 -0
  87. package/dist/helpers/widget-dsl-parser.js +0 -0
  88. package/dist/helpers/widget-recipe-formatter.d.ts +0 -0
  89. package/dist/helpers/widget-recipe-formatter.js +0 -0
  90. package/dist/helpers/widget-recipe-parser.d.ts +0 -0
  91. package/dist/helpers/widget-recipe-parser.js +0 -0
  92. package/dist/helpers/widget-utils.d.ts +0 -0
  93. package/dist/helpers/widget-utils.js +0 -0
  94. package/dist/helpers/workspace-project.d.ts +0 -0
  95. package/dist/helpers/workspace-project.js +0 -0
  96. package/dist/index.d.ts +0 -0
  97. package/dist/index.js +0 -0
  98. package/dist/project-controller.d.ts +0 -0
  99. package/dist/project-controller.js +0 -0
  100. package/dist/prompts/prompt-catalog.d.ts +0 -0
  101. package/dist/prompts/prompt-catalog.js +0 -0
  102. package/dist/register-server-resources.d.ts +0 -0
  103. package/dist/register-server-resources.js +0 -0
  104. package/dist/register-server-tools.d.ts +0 -0
  105. package/dist/register-server-tools.js +2 -0
  106. package/dist/resources/example-and-capture-resources.d.ts +0 -0
  107. package/dist/resources/example-and-capture-resources.js +0 -0
  108. package/dist/resources/static-doc-resources.d.ts +0 -0
  109. package/dist/resources/static-doc-resources.js +0 -0
  110. package/dist/schemas/tool-inputs.d.ts +138 -87
  111. package/dist/schemas/tool-inputs.js +0 -0
  112. package/dist/schemas/tool-results.d.ts +1098 -687
  113. package/dist/schemas/tool-results.js +6 -3
  114. package/dist/server-config.d.ts +9 -1
  115. package/dist/server-config.js +81 -68
  116. package/dist/server-factory.d.ts +0 -0
  117. package/dist/server-factory.js +0 -0
  118. package/dist/tool-context.d.ts +0 -0
  119. package/dist/tool-context.js +0 -0
  120. package/dist/tool-surface-manager.d.ts +0 -0
  121. package/dist/tool-surface-manager.js +0 -0
  122. package/dist/tools/analysis-tools.d.ts +0 -0
  123. package/dist/tools/analysis-tools.js +0 -0
  124. package/dist/tools/animation-authoring.d.ts +0 -0
  125. package/dist/tools/animation-authoring.js +0 -0
  126. package/dist/tools/automation-runs.d.ts +0 -0
  127. package/dist/tools/automation-runs.js +0 -0
  128. package/dist/tools/blueprint-authoring.d.ts +0 -0
  129. package/dist/tools/blueprint-authoring.js +0 -0
  130. package/dist/tools/commonui-button-style.d.ts +0 -0
  131. package/dist/tools/commonui-button-style.js +0 -0
  132. package/dist/tools/composite-tools.d.ts +0 -0
  133. package/dist/tools/composite-tools.js +0 -0
  134. package/dist/tools/composite-workflows.d.ts +0 -0
  135. package/dist/tools/composite-workflows.js +160 -46
  136. package/dist/tools/data-and-input.d.ts +0 -0
  137. package/dist/tools/data-and-input.js +0 -0
  138. package/dist/tools/extraction.d.ts +0 -0
  139. package/dist/tools/extraction.js +0 -0
  140. package/dist/tools/import-jobs.d.ts +0 -0
  141. package/dist/tools/import-jobs.js +0 -0
  142. package/dist/tools/material-authoring.d.ts +0 -0
  143. package/dist/tools/material-authoring.js +0 -0
  144. package/dist/tools/material-instance.d.ts +0 -0
  145. package/dist/tools/material-instance.js +0 -0
  146. package/dist/tools/project-control.d.ts +0 -0
  147. package/dist/tools/project-control.js +0 -0
  148. package/dist/tools/project-intelligence.d.ts +0 -0
  149. package/dist/tools/project-intelligence.js +0 -0
  150. package/dist/tools/recipe-tools.d.ts +0 -0
  151. package/dist/tools/recipe-tools.js +4 -2
  152. package/dist/tools/schema-and-ai-authoring.d.ts +0 -0
  153. package/dist/tools/schema-and-ai-authoring.js +0 -0
  154. package/dist/tools/tables-and-curves.d.ts +0 -0
  155. package/dist/tools/tables-and-curves.js +0 -0
  156. package/dist/tools/utility-tools.d.ts +3 -1
  157. package/dist/tools/utility-tools.js +2 -1
  158. package/dist/tools/widget-animation-authoring.d.ts +0 -0
  159. package/dist/tools/widget-animation-authoring.js +0 -0
  160. package/dist/tools/widget-extraction.d.ts +0 -0
  161. package/dist/tools/widget-extraction.js +0 -0
  162. package/dist/tools/widget-structure.d.ts +0 -0
  163. package/dist/tools/widget-structure.js +0 -0
  164. package/dist/tools/widget-verification.d.ts +0 -0
  165. package/dist/tools/widget-verification.js +0 -0
  166. package/dist/tools/window-ui.d.ts +0 -0
  167. package/dist/tools/window-ui.js +0 -0
  168. package/dist/types.d.ts +0 -0
  169. package/dist/types.js +0 -0
  170. package/dist/ue-client.d.ts +0 -0
  171. package/dist/ue-client.js +0 -0
  172. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <h1 align="center">Blueprint Extractor MCP</h1>
3
3
  <p align="center">
4
4
  Give AI assistants full read/write access to Unreal Engine projects<br>
5
- through a live editor connection.
5
+ through a live editor or headless commandlet lane.
6
6
  </p>
7
7
  </p>
8
8
 
@@ -17,7 +17,10 @@
17
17
 
18
18
  ## Overview
19
19
 
20
- Blueprint Extractor MCP is a [Model Context Protocol](https://modelcontextprotocol.io) server that bridges AI coding assistants (Claude Code, Codex, OpenCode, etc.) to a running Unreal Editor instance via the Remote Control HTTP API.
20
+ Blueprint Extractor MCP is a [Model Context Protocol](https://modelcontextprotocol.io) server that bridges AI coding assistants (Claude Code, Codex, OpenCode, etc.) to Unreal Engine through two execution lanes:
21
+
22
+ - **Editor lane** via the Remote Control HTTP API for editor-bound and interactive workflows
23
+ - **Commandlet lane** for headless-safe extraction and authoring when no reachable editor session is available
21
24
 
22
25
  For compatible tools, the server can also execute through a commandlet lane when no reachable editor session is available. When an editor is already running, editor execution stays preferred for editor-only flows and for save paths that need to avoid package-lock contention.
23
26
 
@@ -49,11 +52,13 @@ For compatible tools, the server can also execute through a commandlet lane when
49
52
 
50
53
  ### Prerequisites
51
54
 
52
- You need three things running:
55
+ You need these prerequisites:
53
56
 
54
57
  1. **Node.js 18+**
55
- 2. **Unreal Editor** with the **Remote Control API** plugin enabled
56
- 3. **[BlueprintExtractor](https://github.com/SunGrow/ue-blueprint-extractor)** plugin installed in your project
58
+ 2. **[BlueprintExtractor](https://github.com/SunGrow/ue-blueprint-extractor)** plugin installed in your project
59
+ 3. One execution lane configured:
60
+ - **Editor lane**: Unreal Editor running with the **Remote Control API** plugin enabled
61
+ - **Commandlet lane**: resolvable `UE_ENGINE_ROOT` and `UE_PROJECT_PATH` so headless-safe tools can launch `UnrealEditor-Cmd`
57
62
 
58
63
  ### Run
59
64
 
@@ -63,6 +68,8 @@ npx blueprint-extractor-mcp
63
68
 
64
69
  Connects to the editor at `127.0.0.1:30010` by default.
65
70
 
71
+ For headless-safe tools, also set `UE_ENGINE_ROOT` and `UE_PROJECT_PATH` so the MCP server can resolve the commandlet lane when no editor is reachable.
72
+
66
73
  ### Add to Your AI Client
67
74
 
68
75
  <table>
@@ -84,6 +91,15 @@ codex mcp add --env UE_REMOTE_CONTROL_PORT=30010 \
84
91
  blueprint-extractor -- npx -y blueprint-extractor-mcp@latest
85
92
  ```
86
93
 
94
+ ```bash
95
+ # Optional for headless-safe tools when no editor is running
96
+ codex mcp add \
97
+ --env UE_REMOTE_CONTROL_PORT=30010 \
98
+ --env UE_ENGINE_ROOT="C:\\Program Files\\Epic Games\\UE_5.7" \
99
+ --env UE_PROJECT_PATH="D:\\Development\\V2\\CyberVolleyball6vs6.uproject" \
100
+ blueprint-extractor -- npx -y blueprint-extractor-mcp@latest
101
+ ```
102
+
87
103
  </td></tr>
88
104
  <tr><td><b>OpenCode</b></td></tr>
89
105
  <tr><td>
@@ -168,6 +184,8 @@ See [../docs/CURRENT_STATUS.md](../docs/CURRENT_STATUS.md) for the current valid
168
184
  | `UE_BUILD_PLATFORM` | &mdash; | e.g. `Win64` |
169
185
  | `UE_BUILD_CONFIGURATION` | &mdash; | e.g. `Development` |
170
186
 
187
+ `get_tool_help` now reports execution compatibility per tool, including whether the tool is editor-only or headless-safe.
188
+
171
189
  `get_project_automation_context` surfaces the coarse editor-derived `engineRoot`, `projectFilePath`, `editorTarget`, and `isPlayingInEditor` state that project-control and verification flows use for fallback or guard logic.
172
190
 
173
191
  `get_editor_context` is the separate read-only editor-state snapshot for selection, open asset editors, active level, and PIE summary. It stays session-bound and intentionally does not open assets, change focus, or switch viewports.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/dist/compactor.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -13,11 +13,16 @@ export interface CommandletAdapterOptions {
13
13
  requestTimeoutMs?: number;
14
14
  }
15
15
  export declare class CommandletAdapter implements ExecutionAdapter {
16
+ private static readonly MAX_LOG_TAIL_LINES;
16
17
  private options;
17
18
  private process;
18
19
  private requestId;
19
20
  private pendingRequests;
20
- private buffer;
21
+ private stdoutBuffer;
22
+ private stderrBuffer;
23
+ private recentLogs;
24
+ private startupPromise;
25
+ private startupState;
21
26
  private readonly spawnProcess;
22
27
  private readonly platform;
23
28
  private readonly startupTimeoutMs;
@@ -29,5 +34,12 @@ export declare class CommandletAdapter implements ExecutionAdapter {
29
34
  getMode(): 'commandlet';
30
35
  getCapabilities(): ReadonlySet<ToolCapability>;
31
36
  shutdown(): Promise<void>;
32
- private processBuffer;
37
+ private spawnAndWaitForReady;
38
+ private processStdoutBuffer;
39
+ private processStderrBuffer;
40
+ private appendLogLine;
41
+ private withRecentLogs;
42
+ private resolveStartup;
43
+ private rejectStartup;
44
+ private handleProcessTermination;
33
45
  }
@@ -8,11 +8,16 @@ import { resolveEditorExecutable } from '../../project-controller.js';
8
8
  const STARTUP_TIMEOUT_MS = 30_000;
9
9
  const REQUEST_TIMEOUT_MS = 60_000;
10
10
  export class CommandletAdapter {
11
+ static MAX_LOG_TAIL_LINES = 100;
11
12
  options;
12
13
  process = null;
13
14
  requestId = 0;
14
15
  pendingRequests = new Map();
15
- buffer = '';
16
+ stdoutBuffer = '';
17
+ stderrBuffer = '';
18
+ recentLogs = [];
19
+ startupPromise = null;
20
+ startupState = null;
16
21
  spawnProcess;
17
22
  platform;
18
23
  startupTimeoutMs;
@@ -25,60 +30,37 @@ export class CommandletAdapter {
25
30
  this.requestTimeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
26
31
  }
27
32
  async initialize() {
28
- if (this.process)
29
- return;
30
- const editorCmd = await resolveEditorExecutable(this.options.engineRoot, this.platform, 'commandlet');
31
- this.process = this.spawnProcess(editorCmd, [
32
- this.options.projectPath,
33
- '-run=blueprintextractor',
34
- '-stdin',
35
- '-unattended',
36
- '-nosplash',
37
- '-nullrhi',
38
- ], {
39
- stdio: ['pipe', 'pipe', 'pipe'],
40
- });
41
- this.process.stdout?.on('data', (data) => {
42
- this.buffer += data.toString();
43
- this.processBuffer();
44
- });
45
- this.process.on('exit', () => {
46
- this.process = null;
47
- // Reject all pending requests
48
- for (const [, pending] of this.pendingRequests) {
49
- clearTimeout(pending.timer);
50
- pending.reject(new Error('Commandlet process exited'));
33
+ if (this.process && !this.process.killed) {
34
+ if (this.startupPromise) {
35
+ await this.startupPromise;
51
36
  }
52
- this.pendingRequests.clear();
53
- });
54
- // Wait for startup
55
- await new Promise((resolve, reject) => {
56
- const timer = setTimeout(() => {
57
- reject(new Error(`Commandlet startup timed out after ${this.startupTimeoutMs}ms`));
58
- }, this.startupTimeoutMs);
59
- const onData = () => {
60
- clearTimeout(timer);
61
- this.process?.stdout?.off('data', onData);
62
- resolve();
63
- };
64
- // Resolve on first stdout output (indicating process is ready)
65
- this.process?.stdout?.on('data', onData);
66
- this.process?.on('error', (err) => {
67
- clearTimeout(timer);
68
- reject(err);
69
- });
70
- });
37
+ return;
38
+ }
39
+ if (this.startupPromise) {
40
+ await this.startupPromise;
41
+ return;
42
+ }
43
+ this.startupPromise = this.spawnAndWaitForReady();
44
+ try {
45
+ await this.startupPromise;
46
+ }
47
+ finally {
48
+ this.startupPromise = null;
49
+ }
71
50
  }
72
51
  async execute(_subsystem, method, params) {
73
- if (!this.process?.stdin) {
74
- throw new Error('Commandlet process not running. Call initialize() first.');
52
+ if (!this.process?.stdin || this.process.killed) {
53
+ await this.initialize();
54
+ }
55
+ if (!this.process?.stdin || this.process.killed) {
56
+ throw new Error(this.withRecentLogs('Commandlet process not running. Call initialize() first.'));
75
57
  }
76
58
  const id = ++this.requestId;
77
59
  const request = JSON.stringify({ jsonrpc: '2.0', id, method, params });
78
60
  return new Promise((resolve, reject) => {
79
61
  const timer = setTimeout(() => {
80
62
  this.pendingRequests.delete(id);
81
- reject(new Error(`Commandlet request timed out after ${this.requestTimeoutMs}ms`));
63
+ reject(new Error(this.withRecentLogs(`Commandlet request timed out after ${this.requestTimeoutMs}ms`)));
82
64
  }, this.requestTimeoutMs);
83
65
  this.pendingRequests.set(id, { resolve, reject, timer });
84
66
  this.process.stdin.write(request + '\n');
@@ -94,21 +76,80 @@ export class CommandletAdapter {
94
76
  return COMMANDLET_CAPABILITIES;
95
77
  }
96
78
  async shutdown() {
97
- if (this.process) {
98
- this.process.stdin?.end();
99
- this.process.kill();
100
- this.process = null;
79
+ if (!this.process) {
80
+ return;
81
+ }
82
+ this.process.stdin?.end();
83
+ this.process.kill();
84
+ this.process = null;
85
+ }
86
+ async spawnAndWaitForReady() {
87
+ this.stdoutBuffer = '';
88
+ this.stderrBuffer = '';
89
+ this.recentLogs = [];
90
+ const editorCmd = await resolveEditorExecutable(this.options.engineRoot, this.platform, 'commandlet');
91
+ const startupWaiter = new Promise((resolve, reject) => {
92
+ const timer = setTimeout(() => {
93
+ reject(new Error(this.withRecentLogs(`Commandlet startup timed out after ${this.startupTimeoutMs}ms`)));
94
+ }, this.startupTimeoutMs);
95
+ this.startupState = { resolve, reject, timer };
96
+ });
97
+ try {
98
+ this.process = this.spawnProcess(editorCmd, [
99
+ this.options.projectPath,
100
+ '-run=blueprintextractor',
101
+ '-stdin',
102
+ '-unattended',
103
+ '-nosplash',
104
+ '-nullrhi',
105
+ ], {
106
+ stdio: ['pipe', 'pipe', 'pipe'],
107
+ });
108
+ this.process.stdout?.on('data', (data) => {
109
+ this.stdoutBuffer += data.toString();
110
+ this.processStdoutBuffer();
111
+ });
112
+ this.process.stderr?.on('data', (data) => {
113
+ this.stderrBuffer += data.toString();
114
+ this.processStderrBuffer();
115
+ });
116
+ this.process.on('exit', (code, signal) => {
117
+ const suffix = code != null
118
+ ? ` with code ${code}`
119
+ : signal
120
+ ? ` with signal ${signal}`
121
+ : '';
122
+ this.handleProcessTermination(`Commandlet process exited${suffix}`);
123
+ });
124
+ this.process.on('error', (error) => {
125
+ const message = error instanceof Error ? error.message : String(error);
126
+ this.handleProcessTermination(`Commandlet process error: ${message}`);
127
+ });
128
+ }
129
+ catch (error) {
130
+ const startupError = error instanceof Error ? error : new Error(String(error));
131
+ this.rejectStartup(startupError);
132
+ throw startupError;
101
133
  }
134
+ await startupWaiter;
102
135
  }
103
- processBuffer() {
104
- const lines = this.buffer.split('\n');
105
- this.buffer = lines.pop() ?? '';
136
+ processStdoutBuffer() {
137
+ const lines = this.stdoutBuffer.split('\n');
138
+ this.stdoutBuffer = lines.pop() ?? '';
106
139
  for (const line of lines) {
107
140
  const trimmed = line.trim();
108
141
  if (!trimmed)
109
142
  continue;
110
143
  try {
111
144
  const response = JSON.parse(trimmed);
145
+ if (response.jsonrpc === '2.0'
146
+ && response.id === 0
147
+ && typeof response.result === 'object'
148
+ && response.result !== null
149
+ && response.result.ready === true) {
150
+ this.resolveStartup();
151
+ continue;
152
+ }
112
153
  if (response.id != null && this.pendingRequests.has(response.id)) {
113
154
  const pending = this.pendingRequests.get(response.id);
114
155
  this.pendingRequests.delete(response.id);
@@ -123,11 +164,61 @@ export class CommandletAdapter {
123
164
  ? response.result
124
165
  : { result: response.result });
125
166
  }
167
+ continue;
126
168
  }
127
169
  }
128
170
  catch {
129
- // Non-JSON output — skip (may be engine log lines)
171
+ this.appendLogLine(trimmed);
172
+ }
173
+ }
174
+ }
175
+ processStderrBuffer() {
176
+ const lines = this.stderrBuffer.split('\n');
177
+ this.stderrBuffer = lines.pop() ?? '';
178
+ for (const line of lines) {
179
+ const trimmed = line.trim();
180
+ if (!trimmed) {
181
+ continue;
130
182
  }
183
+ this.appendLogLine(trimmed);
184
+ }
185
+ }
186
+ appendLogLine(line) {
187
+ this.recentLogs.push(line);
188
+ if (this.recentLogs.length > CommandletAdapter.MAX_LOG_TAIL_LINES) {
189
+ this.recentLogs.splice(0, this.recentLogs.length - CommandletAdapter.MAX_LOG_TAIL_LINES);
190
+ }
191
+ }
192
+ withRecentLogs(message) {
193
+ if (this.recentLogs.length === 0) {
194
+ return message;
195
+ }
196
+ return `${message}\nRecent commandlet logs:\n${this.recentLogs.join('\n')}`;
197
+ }
198
+ resolveStartup() {
199
+ if (!this.startupState) {
200
+ return;
201
+ }
202
+ clearTimeout(this.startupState.timer);
203
+ this.startupState.resolve();
204
+ this.startupState = null;
205
+ }
206
+ rejectStartup(error) {
207
+ if (!this.startupState) {
208
+ return;
209
+ }
210
+ clearTimeout(this.startupState.timer);
211
+ this.startupState.reject(error);
212
+ this.startupState = null;
213
+ }
214
+ handleProcessTermination(baseMessage) {
215
+ const error = new Error(this.withRecentLogs(baseMessage));
216
+ this.rejectStartup(error);
217
+ this.process = null;
218
+ for (const [, pending] of this.pendingRequests) {
219
+ clearTimeout(pending.timer);
220
+ pending.reject(error);
131
221
  }
222
+ this.pendingRequests.clear();
132
223
  }
133
224
  }
File without changes
File without changes
@@ -5,6 +5,7 @@
5
5
  import type { ExecutionAdapter, ToolCapability, ExecutionMode } from './execution-adapter.js';
6
6
  import { ExecutionModeDetector } from './execution-mode-detector.js';
7
7
  export type ToolModeAnnotation = 'both' | 'editor_only' | 'read_only';
8
+ export type ExecutionLane = 'editor' | 'commandlet';
8
9
  export type EditorFallbackCaller = (method: string, params: Record<string, unknown>, options?: {
9
10
  timeoutMs?: number;
10
11
  }) => Promise<Record<string, unknown>>;
@@ -22,13 +23,21 @@ export declare class AdaptiveExecutor {
22
23
  * parameter to executeRouted).
23
24
  */
24
25
  private _activeToolName;
26
+ private _activeToolExecutionMetadata;
25
27
  constructor(editorAdapter: ExecutionAdapter, commandletAdapter: ExecutionAdapter | null, detector: ExecutionModeDetector);
26
28
  setToolMode(toolName: string, mode: ToolModeAnnotation): void;
27
29
  getToolMode(toolName: string): ToolModeAnnotation;
30
+ getSupportedExecutionModes(toolName: string): ExecutionLane[];
28
31
  getCurrentMode(): Promise<ExecutionMode>;
29
32
  /** Set the active tool name before a handler runs. Cleared after handler completes. */
30
33
  setActiveToolName(name: string | null): void;
31
34
  getActiveToolName(): string | null;
35
+ getActiveToolExecutionMetadata(): {
36
+ runtime_mode?: ExecutionLane;
37
+ supported_modes: ExecutionLane[];
38
+ fallback_used?: boolean;
39
+ } | null;
40
+ private recordToolExecution;
32
41
  /**
33
42
  * Route a callSubsystemJson-shaped call through the executor.
34
43
  * For editor mode, delegates to the provided editorFallback (the original
@@ -36,6 +36,7 @@ export class AdaptiveExecutor {
36
36
  * parameter to executeRouted).
37
37
  */
38
38
  _activeToolName = null;
39
+ _activeToolExecutionMetadata = null;
39
40
  constructor(editorAdapter, commandletAdapter, detector) {
40
41
  this.editorAdapter = editorAdapter;
41
42
  this.commandletAdapter = commandletAdapter;
@@ -47,16 +48,47 @@ export class AdaptiveExecutor {
47
48
  getToolMode(toolName) {
48
49
  return this.toolModes.get(toolName) ?? 'editor_only';
49
50
  }
51
+ getSupportedExecutionModes(toolName) {
52
+ return this.getToolMode(toolName) === 'editor_only'
53
+ ? ['editor']
54
+ : ['editor', 'commandlet'];
55
+ }
50
56
  getCurrentMode() {
51
57
  return this.detector.detect().then((r) => r.mode);
52
58
  }
53
59
  /** Set the active tool name before a handler runs. Cleared after handler completes. */
54
60
  setActiveToolName(name) {
55
61
  this._activeToolName = name;
62
+ this._activeToolExecutionMetadata = name
63
+ ? { supportedModes: this.getSupportedExecutionModes(name) }
64
+ : null;
56
65
  }
57
66
  getActiveToolName() {
58
67
  return this._activeToolName;
59
68
  }
69
+ getActiveToolExecutionMetadata() {
70
+ if (!this._activeToolExecutionMetadata) {
71
+ return null;
72
+ }
73
+ return {
74
+ runtime_mode: this._activeToolExecutionMetadata.mode,
75
+ supported_modes: [...this._activeToolExecutionMetadata.supportedModes],
76
+ ...(typeof this._activeToolExecutionMetadata.fallbackUsed === 'boolean'
77
+ ? { fallback_used: this._activeToolExecutionMetadata.fallbackUsed }
78
+ : {}),
79
+ };
80
+ }
81
+ recordToolExecution(toolName, mode, fallbackUsed) {
82
+ if (this._activeToolName !== toolName) {
83
+ return;
84
+ }
85
+ const current = this._activeToolExecutionMetadata ?? {
86
+ supportedModes: this.getSupportedExecutionModes(toolName),
87
+ };
88
+ current.mode = mode;
89
+ current.fallbackUsed = current.fallbackUsed === true ? true : fallbackUsed;
90
+ this._activeToolExecutionMetadata = current;
91
+ }
60
92
  /**
61
93
  * Route a callSubsystemJson-shaped call through the executor.
62
94
  * For editor mode, delegates to the provided editorFallback (the original
@@ -89,15 +121,26 @@ export class AdaptiveExecutor {
89
121
  throw error;
90
122
  }
91
123
  this.detector.invalidateCache();
92
- return this.commandletAdapter.execute('BlueprintExtractor', method, params);
124
+ try {
125
+ const result = await this.commandletAdapter.execute('BlueprintExtractor', method, params);
126
+ this.recordToolExecution(toolName, 'commandlet', true);
127
+ return result;
128
+ }
129
+ catch (fallbackError) {
130
+ this.recordToolExecution(toolName, 'commandlet', true);
131
+ throw fallbackError;
132
+ }
93
133
  };
94
134
  // Editor mode keeps the existing direct path, with commandlet fallback for
95
135
  // compatible tools when the editor call fails.
96
136
  if (detection.mode === 'editor') {
97
137
  try {
98
- return await editorFallback(method, params, options);
138
+ const result = await editorFallback(method, params, options);
139
+ this.recordToolExecution(toolName, 'editor', false);
140
+ return result;
99
141
  }
100
142
  catch (error) {
143
+ this.recordToolExecution(toolName, 'editor', false);
101
144
  if (toolMode === 'editor_only') {
102
145
  throw error;
103
146
  }
@@ -118,7 +161,9 @@ export class AdaptiveExecutor {
118
161
  const editorAvailable = await this.editorAdapter.isAvailable();
119
162
  if (editorAvailable) {
120
163
  this.detector.invalidateCache();
121
- return await editorFallback(method, params, options);
164
+ const result = await editorFallback(method, params, options);
165
+ this.recordToolExecution(toolName, 'editor', true);
166
+ return result;
122
167
  }
123
168
  }
124
169
  catch {
@@ -126,15 +171,20 @@ export class AdaptiveExecutor {
126
171
  }
127
172
  }
128
173
  try {
129
- return await this.commandletAdapter.execute('BlueprintExtractor', method, params);
174
+ const result = await this.commandletAdapter.execute('BlueprintExtractor', method, params);
175
+ this.recordToolExecution(toolName, 'commandlet', false);
176
+ return result;
130
177
  }
131
178
  catch (error) {
179
+ this.recordToolExecution(toolName, 'commandlet', false);
132
180
  if (toolMode !== 'read_only' && shouldFallbackToEditorOnLock(error)) {
133
181
  try {
134
182
  const editorAvailable = await this.editorAdapter.isAvailable();
135
183
  if (editorAvailable) {
136
184
  this.detector.invalidateCache();
137
- return await editorFallback(method, params, options);
185
+ const result = await editorFallback(method, params, options);
186
+ this.recordToolExecution(toolName, 'editor', true);
187
+ return result;
138
188
  }
139
189
  }
140
190
  catch {
@@ -157,7 +207,15 @@ export class AdaptiveExecutor {
157
207
  ? 'write_simple'
158
208
  : 'write_complex';
159
209
  if (detection.mode === 'editor') {
160
- return this.editorAdapter.execute(subsystem, method, params);
210
+ try {
211
+ const result = await this.editorAdapter.execute(subsystem, method, params);
212
+ this.recordToolExecution(toolName, 'editor', false);
213
+ return result;
214
+ }
215
+ catch (error) {
216
+ this.recordToolExecution(toolName, 'editor', false);
217
+ throw error;
218
+ }
161
219
  }
162
220
  // Try commandlet for compatible tools
163
221
  if (detection.mode === 'commandlet' && this.commandletAdapter) {
@@ -173,7 +231,9 @@ export class AdaptiveExecutor {
173
231
  const editorAvailable = await this.editorAdapter.isAvailable();
174
232
  if (editorAvailable) {
175
233
  this.detector.invalidateCache();
176
- return await this.editorAdapter.execute(subsystem, method, params);
234
+ const result = await this.editorAdapter.execute(subsystem, method, params);
235
+ this.recordToolExecution(toolName, 'editor', true);
236
+ return result;
177
237
  }
178
238
  }
179
239
  catch {
@@ -181,15 +241,20 @@ export class AdaptiveExecutor {
181
241
  }
182
242
  }
183
243
  try {
184
- return await this.commandletAdapter.execute(subsystem, method, params);
244
+ const result = await this.commandletAdapter.execute(subsystem, method, params);
245
+ this.recordToolExecution(toolName, 'commandlet', false);
246
+ return result;
185
247
  }
186
248
  catch (error) {
249
+ this.recordToolExecution(toolName, 'commandlet', false);
187
250
  if (toolMode !== 'read_only' && shouldFallbackToEditorOnLock(error)) {
188
251
  try {
189
252
  const editorAvailable = await this.editorAdapter.isAvailable();
190
253
  if (editorAvailable) {
191
254
  this.detector.invalidateCache();
192
- return await this.editorAdapter.execute(subsystem, method, params);
255
+ const result = await this.editorAdapter.execute(subsystem, method, params);
256
+ this.recordToolExecution(toolName, 'editor', true);
257
+ return result;
193
258
  }
194
259
  }
195
260
  catch {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -24,6 +24,9 @@ export interface CompositeToolResult {
24
24
  task_support: 'optional' | 'required' | 'forbidden';
25
25
  status?: string;
26
26
  progress_message?: string;
27
+ runtime_mode?: 'editor' | 'commandlet';
28
+ supported_modes?: Array<'editor' | 'commandlet'>;
29
+ fallback_used?: boolean;
27
30
  };
28
31
  }
29
32
  type Result<T, E> = {
File without changes
File without changes
File without changes
File without changes
File without changes