unity-open-mcp 1.2.3 → 1.4.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/README.md +34 -2
- package/dist/agent-identity.d.ts +12 -0
- package/dist/agent-identity.js +2 -0
- package/dist/agent-identity.js.map +1 -1
- package/dist/batch-spawn.d.ts +39 -0
- package/dist/batch-spawn.js +287 -69
- package/dist/batch-spawn.js.map +1 -1
- package/dist/capabilities/build-capabilities.js +2 -0
- package/dist/capabilities/build-capabilities.js.map +1 -1
- package/dist/capabilities/discovery.d.ts +51 -0
- package/dist/capabilities/discovery.js +87 -0
- package/dist/capabilities/discovery.js.map +1 -0
- package/dist/capabilities/lifecycle.d.ts +6 -0
- package/dist/capabilities/lifecycle.js +17 -1
- package/dist/capabilities/lifecycle.js.map +1 -1
- package/dist/capabilities/tool-groups.js +1 -0
- package/dist/capabilities/tool-groups.js.map +1 -1
- package/dist/child-supervision.d.ts +6 -0
- package/dist/child-supervision.js +12 -0
- package/dist/child-supervision.js.map +1 -1
- package/dist/cli/args.d.ts +24 -2
- package/dist/cli/args.js +97 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/cli.js +42 -3
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/help-text.d.ts +1 -0
- package/dist/cli/help-text.js +83 -3
- package/dist/cli/help-text.js.map +1 -1
- package/dist/cli/json-guards.d.ts +2 -0
- package/dist/cli/json-guards.js +5 -0
- package/dist/cli/json-guards.js.map +1 -0
- package/dist/cli/setup-command.d.ts +116 -0
- package/dist/cli/setup-command.js +696 -0
- package/dist/cli/setup-command.js.map +1 -0
- package/dist/cli/update-command.d.ts +30 -0
- package/dist/cli/update-command.js +271 -0
- package/dist/cli/update-command.js.map +1 -0
- package/dist/compile-verify.d.ts +3 -0
- package/dist/compile-verify.js +4 -0
- package/dist/compile-verify.js.map +1 -1
- package/dist/constants.d.ts +10 -2
- package/dist/constants.js +10 -2
- package/dist/constants.js.map +1 -1
- package/dist/dialog-dismiss.d.ts +108 -12
- package/dist/dialog-dismiss.js +294 -109
- package/dist/dialog-dismiss.js.map +1 -1
- package/dist/dialog-policy.d.ts +55 -9
- package/dist/dialog-policy.js +85 -15
- package/dist/dialog-policy.js.map +1 -1
- package/dist/editor-search.d.ts +2 -0
- package/dist/editor-search.js +8 -0
- package/dist/editor-search.js.map +1 -0
- package/dist/gate-error.d.ts +3 -0
- package/dist/gate-error.js.map +1 -1
- package/dist/index.js +56 -1
- package/dist/index.js.map +1 -1
- package/dist/instance-discovery.d.ts +16 -0
- package/dist/instance-discovery.js +45 -0
- package/dist/instance-discovery.js.map +1 -1
- package/dist/jobs/adapters.d.ts +15 -0
- package/dist/jobs/adapters.js +147 -0
- package/dist/jobs/adapters.js.map +1 -0
- package/dist/jobs/handler.d.ts +3 -0
- package/dist/jobs/handler.js +29 -0
- package/dist/jobs/handler.js.map +1 -0
- package/dist/jobs/job-manager.d.ts +123 -0
- package/dist/jobs/job-manager.js +290 -0
- package/dist/jobs/job-manager.js.map +1 -0
- package/dist/live-client.d.ts +50 -2
- package/dist/live-client.js +221 -26
- package/dist/live-client.js.map +1 -1
- package/dist/local-tools.d.ts +17 -0
- package/dist/local-tools.js +47 -0
- package/dist/local-tools.js.map +1 -0
- package/dist/project-path.d.ts +99 -0
- package/dist/project-path.js +171 -0
- package/dist/project-path.js.map +1 -0
- package/dist/release-lookup.d.ts +32 -0
- package/dist/release-lookup.js +147 -0
- package/dist/release-lookup.js.map +1 -0
- package/dist/routers.d.ts +17 -3
- package/dist/routers.js +29 -8
- package/dist/routers.js.map +1 -1
- package/dist/running-unity.d.ts +21 -0
- package/dist/running-unity.js +50 -0
- package/dist/running-unity.js.map +1 -1
- package/dist/schema-defaults.d.ts +4 -3
- package/dist/schema-defaults.js +20 -6
- package/dist/schema-defaults.js.map +1 -1
- package/dist/server-instructions.js +7 -1
- package/dist/server-instructions.js.map +1 -1
- package/dist/server.d.ts +10 -3
- package/dist/server.js +24 -25
- package/dist/server.js.map +1 -1
- package/dist/setup/codex-toml.d.ts +23 -0
- package/dist/setup/codex-toml.js +514 -0
- package/dist/setup/codex-toml.js.map +1 -0
- package/dist/setup/portable-config.d.ts +59 -0
- package/dist/setup/portable-config.js +241 -0
- package/dist/setup/portable-config.js.map +1 -0
- package/dist/skill/SKILL.md +68 -0
- package/dist/skill/client-paths.js +2 -4
- package/dist/skill/client-paths.js.map +1 -1
- package/dist/skill/copy-references.d.ts +2 -0
- package/dist/skill/copy-references.js +16 -0
- package/dist/skill/copy-references.js.map +1 -0
- package/dist/skill/generate-skill.js +5 -3
- package/dist/skill/generate-skill.js.map +1 -1
- package/dist/skill/references/batch-and-gates.md +142 -0
- package/dist/skill/references/compile-and-safe-mode.md +163 -0
- package/dist/skill/references/discovery-and-groups.md +30 -0
- package/dist/skill/references/routing-and-lifecycle.md +62 -0
- package/dist/skill/references/senses-and-tests.md +41 -0
- package/dist/skill/references/yaml-and-offline-work.md +33 -0
- package/dist/templates/mcp-wrapper.sh +35 -0
- package/dist/tool-contract.d.ts +8 -0
- package/dist/tool-contract.js +253 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-router.d.ts +62 -7
- package/dist/tool-router.js +455 -17
- package/dist/tool-router.js.map +1 -1
- package/dist/tool-session-state.d.ts +6 -0
- package/dist/tool-session-state.js +14 -0
- package/dist/tool-session-state.js.map +1 -1
- package/dist/tools/agent-capabilities.js +9 -0
- package/dist/tools/agent-capabilities.js.map +1 -1
- package/dist/tools/batch-execute.js +17 -14
- package/dist/tools/batch-execute.js.map +1 -1
- package/dist/tools/bridge-status.js +4 -5
- package/dist/tools/bridge-status.js.map +1 -1
- package/dist/tools/compile-check.js +6 -4
- package/dist/tools/compile-check.js.map +1 -1
- package/dist/tools/component-get.js +2 -2
- package/dist/tools/component-get.js.map +1 -1
- package/dist/tools/component-modify.js +4 -4
- package/dist/tools/component-modify.js.map +1 -1
- package/dist/tools/execute-csharp.js +8 -4
- package/dist/tools/execute-csharp.js.map +1 -1
- package/dist/tools/gameobject-modify.js +2 -1
- package/dist/tools/gameobject-modify.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +10 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inputsim-key.js +12 -14
- package/dist/tools/inputsim-key.js.map +1 -1
- package/dist/tools/inputsim-pointer.js +3 -3
- package/dist/tools/inputsim-pointer.js.map +1 -1
- package/dist/tools/inputsim-step.js +7 -8
- package/dist/tools/inputsim-step.js.map +1 -1
- package/dist/tools/inputsim-touch.js +9 -10
- package/dist/tools/inputsim-touch.js.map +1 -1
- package/dist/tools/jobs.d.ts +40 -0
- package/dist/tools/jobs.js +18 -0
- package/dist/tools/jobs.js.map +1 -0
- package/dist/tools/manage-tools.js +10 -5
- package/dist/tools/manage-tools.js.map +1 -1
- package/dist/tools/project-commands.d.ts +40 -0
- package/dist/tools/project-commands.js +23 -0
- package/dist/tools/project-commands.js.map +1 -0
- package/dist/tools/read-compile-errors.js +14 -93
- package/dist/tools/read-compile-errors.js.map +1 -1
- package/dist/tools/scan-paths.js +4 -4
- package/dist/tools/scan-paths.js.map +1 -1
- package/dist/tools/schema-fragments.d.ts +15 -3
- package/dist/tools/schema-fragments.js +23 -13
- package/dist/tools/schema-fragments.js.map +1 -1
- package/dist/tools/upgrade.d.ts +40 -0
- package/dist/tools/upgrade.js +55 -0
- package/dist/tools/upgrade.js.map +1 -0
- package/dist/unity-install-discovery.d.ts +24 -0
- package/dist/unity-install-discovery.js +113 -4
- package/dist/unity-install-discovery.js.map +1 -1
- package/package.json +4 -3
- package/templates/mcp-wrapper.sh +35 -0
package/dist/dialog-dismiss.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// deps beyond MCP SDK"): only child_process and os.
|
|
25
25
|
import { execFile, execFileSync } from "node:child_process";
|
|
26
26
|
import { platform as nodePlatform } from "node:os";
|
|
27
|
-
import { DIALOG_TITLE_FRAGMENTS, parseDialogPolicy, preferenceTokensForPolicy, genericFallbackTokens, blockedKindsForPolicy, } from "./dialog-policy.js";
|
|
27
|
+
import { DIALOG_TITLE_FRAGMENTS, DIALOG_FOCUSED_BUTTON_TOKENS, parseDialogPolicy, preferenceTokensForPolicy, genericFallbackTokens, blockedKindsForPolicy, titleMatchesDialogKind, UNITY_EDITOR_WINDOW_TITLE_MARKER, } from "./dialog-policy.js";
|
|
28
28
|
/**
|
|
29
29
|
* Window-title fragments matched against the Unity launch-errors dialog.
|
|
30
30
|
* Kept as the M13 T4.5 back-compat export (the launch-errors subset of
|
|
@@ -120,7 +120,7 @@ export async function tryDismissDialog(opts) {
|
|
|
120
120
|
function buildTokenTable(opts) {
|
|
121
121
|
const kinds = {};
|
|
122
122
|
for (const kind of Object.keys(DIALOG_TITLE_FRAGMENTS)) {
|
|
123
|
-
const tokens = preferenceTokensForPolicy(kind, opts.policy, opts.allowProjectUpgrade, opts.allowUnsavedSceneDismiss);
|
|
123
|
+
const tokens = preferenceTokensForPolicy(kind, opts.policy, opts.allowProjectUpgrade, opts.allowUnsavedSceneDismiss, opts.allowVersionMismatch);
|
|
124
124
|
kinds[kind] = {
|
|
125
125
|
fragments: [...DIALOG_TITLE_FRAGMENTS[kind]],
|
|
126
126
|
tokens: tokens === null ? null : [...tokens],
|
|
@@ -129,7 +129,7 @@ function buildTokenTable(opts) {
|
|
|
129
129
|
return {
|
|
130
130
|
kinds,
|
|
131
131
|
blocked: [
|
|
132
|
-
...blockedKindsForPolicy(opts.policy, opts.allowProjectUpgrade, opts.allowUnsavedSceneDismiss),
|
|
132
|
+
...blockedKindsForPolicy(opts.policy, opts.allowProjectUpgrade, opts.allowUnsavedSceneDismiss, opts.allowVersionMismatch),
|
|
133
133
|
],
|
|
134
134
|
genericTokens: [...genericFallbackTokens(opts.policy)],
|
|
135
135
|
};
|
|
@@ -145,8 +145,9 @@ function buildTokenTable(opts) {
|
|
|
145
145
|
* Strategy:
|
|
146
146
|
* 1. P/Invoke `EnumWindows` to enumerate every visible top-level window
|
|
147
147
|
* owned by `Unity.exe`.
|
|
148
|
-
* 2. For each,
|
|
149
|
-
*
|
|
148
|
+
* 2. For each, skip the main Editor window ({@link isUnityEditorWindowTitle}),
|
|
149
|
+
* normalize the title (alphanumeric lowercase) and match it against the
|
|
150
|
+
* embedded per-kind fragment table to classify it.
|
|
150
151
|
* 3. If the kind is in the blocked list → emit `blocked:<kind>` and skip
|
|
151
152
|
* the click. If the kind's token list is null → skip (not-found).
|
|
152
153
|
* 4. Walk child windows with `EnumChildWindows`, find a Button whose
|
|
@@ -212,6 +213,8 @@ namespace UnityOpenMcp.Dialogs {
|
|
|
212
213
|
sb.Length = 0;
|
|
213
214
|
GetWindowTextW(hWnd, sb, sb.Capacity);
|
|
214
215
|
var title = sb.ToString();
|
|
216
|
+
// The main Editor window title embeds project/scene names; never classify it.
|
|
217
|
+
if (title.IndexOf("${UNITY_EDITOR_WINDOW_TITLE_MARKER}", StringComparison.OrdinalIgnoreCase) >= 0) continue;
|
|
215
218
|
var norm = Norm(title);
|
|
216
219
|
string kind = null;
|
|
217
220
|
foreach (var kv in table.kinds) {
|
|
@@ -312,49 +315,37 @@ async function tryDismissWindows(opts) {
|
|
|
312
315
|
// ---------------------------------------------------------------------------
|
|
313
316
|
/**
|
|
314
317
|
* The AppleScript template used for the macOS dismiss path. Exposed as a
|
|
315
|
-
* function of the
|
|
318
|
+
* function of the probe options so tests can assert the script shape without
|
|
316
319
|
* launching osascript.
|
|
317
320
|
*
|
|
318
|
-
* Strategy: iterate every window of the Unity process
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
321
|
+
* Strategy: iterate every window of the Unity process except the main Editor
|
|
322
|
+
* window (its title contains {@link UNITY_EDITOR_WINDOW_TITLE_MARKER}),
|
|
323
|
+
* classify it by its title normalized with `normLabel` (see
|
|
324
|
+
* {@link macosTitleMatch}), and act per kind as {@link focusDialogAction}
|
|
325
|
+
* decides from the shared policy table (see {@link macosDialogKindBlock}):
|
|
326
|
+
*
|
|
327
|
+
* - `focus` — the policy picks the kind's focused/default button
|
|
328
|
+
* ({@link DIALOG_FOCUSED_BUTTON_TOKENS}): press Return, but
|
|
329
|
+
* only when the window shows a button with that label.
|
|
330
|
+
* - `named` — the policy picks another button (e.g. Enter Safe Mode, or
|
|
331
|
+
* Quit/Cancel under `cancel`): click the first button whose
|
|
332
|
+
* normalized title equals a policy token, in priority order,
|
|
333
|
+
* or report `blocked:<kind>` when none is found. Return is
|
|
334
|
+
* never pressed, because it would click the focused button.
|
|
335
|
+
* - `blocked` — the policy blocks the kind: report `blocked:<kind>`.
|
|
336
|
+
* - `skip` — the policy declines the kind: no block; the window falls
|
|
337
|
+
* through to `not-found` so polling continues.
|
|
338
|
+
*
|
|
339
|
+
* Project Upgrade is always reported as `blocked` (never clicked), and
|
|
340
|
+
* unsaved scene changes have their own named-button block behind the opt-in.
|
|
341
|
+
* If Unity is not running or nothing matches, the script reports `not-found`.
|
|
342
|
+
* Any AppleScript exception (e.g. Accessibility permission not granted) is
|
|
343
|
+
* reported as `error:<msg>`.
|
|
324
344
|
*
|
|
325
345
|
* Requires the Terminal / `node` binary to have been granted Accessibility
|
|
326
346
|
* permission in System Settings → Privacy & Security → Accessibility.
|
|
327
|
-
*
|
|
328
|
-
* The token table is JSON-escaped into the script header.
|
|
329
347
|
*/
|
|
330
348
|
export function macosDismissAppleScript(opts) {
|
|
331
|
-
// The macOS path cannot do per-button selection as precisely as the Windows
|
|
332
|
-
// path (AppleScript's named-button click works but is brittle across
|
|
333
|
-
// Unity versions); instead it classifies the window title and presses
|
|
334
|
-
// Return to click the FOCUSED (default) button — which under the default /
|
|
335
|
-
// auto / ignore / recover policies IS the safe choice (Ignore on
|
|
336
|
-
// launch-errors, Continue on non-matching-editor, OK on auto-graphics-api).
|
|
337
|
-
// Project Upgrade is detected and reported as `blocked` (never Return-clicked
|
|
338
|
-
// without the explicit opt-in). The token table from buildTokenTable is
|
|
339
|
-
// consulted only to decide whether the policy declines a kind entirely
|
|
340
|
-
// (manual / safe-mode on a kind with no safe button → no click, not-found);
|
|
341
|
-
// when it declines, the script returns not-found for that kind.
|
|
342
|
-
const launchFrags = DIALOG_TITLE_FRAGMENTS.launch_errors;
|
|
343
|
-
const nonMatchFrags = DIALOG_TITLE_FRAGMENTS.non_matching_editor;
|
|
344
|
-
const graphicsFrags = DIALOG_TITLE_FRAGMENTS.auto_graphics_api;
|
|
345
|
-
const sceneModifiedFrags = DIALOG_TITLE_FRAGMENTS.scene_modified_externally;
|
|
346
|
-
// Whether the active policy dismisses each safe kind at all. Under manual,
|
|
347
|
-
// or safe-mode on non_matching/auto_graphics (no safe button), the loop
|
|
348
|
-
// should NOT click — return not-found so polling continues.
|
|
349
|
-
const dismissesLaunch = preferenceTokensForPolicy("launch_errors", opts.policy, opts.allowProjectUpgrade) !== null;
|
|
350
|
-
const dismissesNonMatch = preferenceTokensForPolicy("non_matching_editor", opts.policy, opts.allowProjectUpgrade) !== null;
|
|
351
|
-
const dismissesGraphics = preferenceTokensForPolicy("auto_graphics_api", opts.policy, opts.allowProjectUpgrade) !== null;
|
|
352
|
-
// scene_modified_externally: safe to Return-click (focused button is Reload)
|
|
353
|
-
// under auto/ignore/recover. unsaved_scene_changes is intentionally NOT
|
|
354
|
-
// Return-clicked here — it's destructive (data loss either way) and is
|
|
355
|
-
// blocked by default; even with the opt-in the per-button Windows path is
|
|
356
|
-
// the safer surface for it.
|
|
357
|
-
const dismissesSceneModified = preferenceTokensForPolicy("scene_modified_externally", opts.policy, opts.allowProjectUpgrade) !== null;
|
|
358
349
|
const unsavedTokens = opts.allowUnsavedSceneDismiss
|
|
359
350
|
? preferenceTokensForPolicy("unsaved_scene_changes", opts.policy, opts.allowProjectUpgrade, true)
|
|
360
351
|
: null;
|
|
@@ -373,6 +364,9 @@ export function macosDismissAppleScript(opts) {
|
|
|
373
364
|
return "Cancel";
|
|
374
365
|
return t;
|
|
375
366
|
});
|
|
367
|
+
const unsavedClick = opts.allowUnsavedSceneDismiss && unsavedButtonLabels.length > 0
|
|
368
|
+
? unsavedSceneMacOsClickBlocks(unsavedButtonLabels)
|
|
369
|
+
: { window: "", sheets: "" };
|
|
376
370
|
return `
|
|
377
371
|
on run
|
|
378
372
|
try
|
|
@@ -387,22 +381,29 @@ on run
|
|
|
387
381
|
on error
|
|
388
382
|
set wt to ""
|
|
389
383
|
end try
|
|
390
|
-
--
|
|
391
|
-
--
|
|
392
|
-
|
|
393
|
-
|
|
384
|
+
-- Dialog fragments are normalized (letters and digits only), so match
|
|
385
|
+
-- them against the normalized title, never the raw one.
|
|
386
|
+
set wn to my normLabel(wt)
|
|
387
|
+
-- The main Editor window title ("<Project> - <Scene> - ... - Unity
|
|
388
|
+
-- <version>") embeds user-chosen names and "SAFE MODE"; never classify
|
|
389
|
+
-- it as a dialog. Its sheets are still scanned below.
|
|
390
|
+
if not (wt contains "${UNITY_EDITOR_WINDOW_TITLE_MARKER}") then
|
|
391
|
+
-- Project Upgrade: NEVER click without the explicit opt-in, even
|
|
392
|
+
-- though the focused button is usually Confirm. Report blocked.
|
|
393
|
+
if (wt contains "Upgrade") and (wt contains "Project") then
|
|
394
|
+
return "blocked:" & "project_upgrade"
|
|
395
|
+
end if
|
|
396
|
+
${macosDialogKindBlock("non_matching_editor", opts)}
|
|
397
|
+
-- Unsaved scene changes: blocked unless the dedicated opt-in is set.
|
|
398
|
+
if ${(opts.allowUnsavedSceneDismiss ? "false" : "true")} then
|
|
399
|
+
if wt contains "Have Been Modified" or wt contains "Unsaved changes" or wt contains "Save Changes" then return "blocked:" & "unsaved_scene_changes"
|
|
400
|
+
end if
|
|
401
|
+
${unsavedClick.window}
|
|
402
|
+
${macosDialogKindBlock("launch_errors", opts)}
|
|
403
|
+
${macosDialogKindBlock("auto_graphics_api", opts)}
|
|
404
|
+
${macosDialogKindBlock("scene_modified_externally", opts)}
|
|
394
405
|
end if
|
|
395
|
-
|
|
396
|
-
if ${(opts.allowUnsavedSceneDismiss ? "false" : "true")} then
|
|
397
|
-
if wt contains "Have Been Modified" or wt contains "Unsaved changes" or wt contains "Save Changes" then return "blocked:" & "unsaved_scene_changes"
|
|
398
|
-
end if
|
|
399
|
-
${opts.allowUnsavedSceneDismiss && unsavedButtonLabels.length > 0
|
|
400
|
-
? unsavedSceneMacOsClickBlock(unsavedButtonLabels)
|
|
401
|
-
: ""}
|
|
402
|
-
${launchFrags.map((f) => fragmentCheck(f, "launch_errors", dismissesLaunch)).join("\n ")}
|
|
403
|
-
${nonMatchFrags.map((f) => fragmentCheck(f, "non_matching_editor", dismissesNonMatch)).join("\n ")}
|
|
404
|
-
${graphicsFrags.map((f) => fragmentCheck(f, "auto_graphics_api", dismissesGraphics)).join("\n ")}
|
|
405
|
-
${sceneModifiedFrags.map((f) => fragmentCheck(f, "scene_modified_externally", dismissesSceneModified)).join("\n ")}
|
|
406
|
+
${unsavedClick.sheets}
|
|
406
407
|
end repeat
|
|
407
408
|
end tell
|
|
408
409
|
end tell
|
|
@@ -411,10 +412,151 @@ on run
|
|
|
411
412
|
return "error:" & errMsg
|
|
412
413
|
end try
|
|
413
414
|
end run
|
|
415
|
+
|
|
416
|
+
${MACOS_NORM_LABEL_HANDLER}`;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* AppleScript mirror of {@link normalizeDialogLabel}: keep ASCII letters and
|
|
420
|
+
* digits only. AppleScript string comparison (`is`, `contains`) ignores case
|
|
421
|
+
* by default, so the handler does not lowercase. Exported for tests.
|
|
422
|
+
*/
|
|
423
|
+
export const MACOS_NORM_LABEL_HANDLER = `
|
|
424
|
+
-- Mirror of normalizeDialogLabel: keep ASCII letters and digits only.
|
|
425
|
+
-- (String comparison is case-insensitive by default, so no lowercasing.)
|
|
426
|
+
on normLabel(s)
|
|
427
|
+
set out to ""
|
|
428
|
+
repeat with c in (characters of s)
|
|
429
|
+
set ch to contents of c
|
|
430
|
+
set cid to id of ch
|
|
431
|
+
-- "id of" yields a LIST for a character made of several code points
|
|
432
|
+
-- (decomposed accents, emoji); comparing a list numerically would raise
|
|
433
|
+
-- and abort the whole probe, so such characters are simply dropped.
|
|
434
|
+
if class of cid is integer then
|
|
435
|
+
if (cid >= 48 and cid <= 57) or (cid >= 65 and cid <= 90) or (cid >= 97 and cid <= 122) then set out to out & ch
|
|
436
|
+
end if
|
|
437
|
+
end repeat
|
|
438
|
+
return out
|
|
439
|
+
end normLabel
|
|
414
440
|
`;
|
|
441
|
+
/**
|
|
442
|
+
* How a focus-driven platform (macOS AppleScript, Linux xdotool) may act on a
|
|
443
|
+
* dialog of the given kind under the active policy. Derived from the shared
|
|
444
|
+
* policy table so every platform agrees:
|
|
445
|
+
*
|
|
446
|
+
* - `blocked` — the policy blocks the kind (e.g. Project Upgrade without its
|
|
447
|
+
* opt-in). Report `blocked:<kind>`, never click.
|
|
448
|
+
* - `skip` — the policy declines the kind (no preference tokens). Do
|
|
449
|
+
* nothing; try the next kind.
|
|
450
|
+
* - `focus` — the policy's first choice is the kind's focused button
|
|
451
|
+
* ({@link DIALOG_FOCUSED_BUTTON_TOKENS}): pressing Return is
|
|
452
|
+
* safe.
|
|
453
|
+
* - `named` — the policy's first choice is some other button (e.g. Enter
|
|
454
|
+
* Safe Mode, Quit, Cancel). Return would press the focused
|
|
455
|
+
* button instead, so only a named-button click may act.
|
|
456
|
+
*
|
|
457
|
+
* Pure; exported for tests.
|
|
458
|
+
*/
|
|
459
|
+
export function focusDialogAction(kind, opts) {
|
|
460
|
+
const blocked = blockedKindsForPolicy(opts.policy, opts.allowProjectUpgrade, opts.allowUnsavedSceneDismiss, opts.allowVersionMismatch);
|
|
461
|
+
if (blocked.includes(kind))
|
|
462
|
+
return "blocked";
|
|
463
|
+
const tokens = preferenceTokensForPolicy(kind, opts.policy, opts.allowProjectUpgrade, opts.allowUnsavedSceneDismiss, opts.allowVersionMismatch);
|
|
464
|
+
if (tokens === null)
|
|
465
|
+
return "skip";
|
|
466
|
+
return tokens[0] === DIALOG_FOCUSED_BUTTON_TOKENS[kind] ? "focus" : "named";
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* AppleScript title condition per focus-classified kind, over `wn` — the
|
|
470
|
+
* window title normalized by `normLabel`. The fragments are normalized
|
|
471
|
+
* (letters and digits only), and AppleScript `contains` ignores case but not
|
|
472
|
+
* spaces or punctuation, so matching the raw title would never fire
|
|
473
|
+
* (`"Enter Safe Mode?" contains "safemode"` is false). Non-Matching Editor
|
|
474
|
+
* matches the short `nonmatchingeditor` stem so both the hyphenated and the
|
|
475
|
+
* spaced spelling qualify. Exported for tests.
|
|
476
|
+
*/
|
|
477
|
+
export function macosTitleMatch(kind) {
|
|
478
|
+
const fragments = kind === "non_matching_editor" ? ["nonmatchingeditor"] : DIALOG_TITLE_FRAGMENTS[kind];
|
|
479
|
+
return fragments.map((f) => `(wn contains "${f}")`).join(" or ");
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* AppleScript block for one dialog kind inside the window loop (`w` / `wt`),
|
|
483
|
+
* shaped by {@link focusDialogAction}:
|
|
484
|
+
*
|
|
485
|
+
* - `blocked` → `blocked:<kind>` when the title matches.
|
|
486
|
+
* - `skip` → a comment only; the window falls through.
|
|
487
|
+
* - `focus` → press Return on the focused button, when the window has a
|
|
488
|
+
* button whose normalized title equals the kind's focused
|
|
489
|
+
* token; otherwise fall through (no press).
|
|
490
|
+
* - `named` → for each policy token in priority order, click the first
|
|
491
|
+
* button whose normalized title equals it; if none matches,
|
|
492
|
+
* report `blocked:<kind>`. Never presses Return.
|
|
493
|
+
*
|
|
494
|
+
* Exact normalized equality (not substring) keeps a short token such as `no`
|
|
495
|
+
* from matching an unrelated label such as "Ignore".
|
|
496
|
+
*
|
|
497
|
+
* Pure; exported for tests.
|
|
498
|
+
*/
|
|
499
|
+
export function macosDialogKindBlock(kind, opts) {
|
|
500
|
+
const titleMatch = macosTitleMatch(kind);
|
|
501
|
+
switch (focusDialogAction(kind, opts)) {
|
|
502
|
+
case "skip":
|
|
503
|
+
return `-- policy declines ${kind}; skip`;
|
|
504
|
+
case "blocked":
|
|
505
|
+
return `-- policy blocks ${kind}
|
|
506
|
+
if ${titleMatch} then
|
|
507
|
+
return "blocked:" & "${kind}"
|
|
508
|
+
end if`;
|
|
509
|
+
case "focus": {
|
|
510
|
+
const focusedToken = DIALOG_FOCUSED_BUTTON_TOKENS[kind];
|
|
511
|
+
return `-- policy picks the focused button on ${kind}: press Return, but only
|
|
512
|
+
-- when the window shows that button, so Return lands on a real dialog
|
|
513
|
+
if ${titleMatch} then
|
|
514
|
+
repeat with btn in buttons of w
|
|
515
|
+
try
|
|
516
|
+
set bt to (title of btn) as text
|
|
517
|
+
on error
|
|
518
|
+
set bt to ""
|
|
519
|
+
end try
|
|
520
|
+
if my normLabel(bt) is "${focusedToken}" then
|
|
521
|
+
set frontmost to true
|
|
522
|
+
key code 36
|
|
523
|
+
return "dismissed:Focus:${kind}"
|
|
524
|
+
end if
|
|
525
|
+
end repeat
|
|
526
|
+
end if`;
|
|
527
|
+
}
|
|
528
|
+
case "named": {
|
|
529
|
+
const tokens = preferenceTokensForPolicy(kind, opts.policy, opts.allowProjectUpgrade, opts.allowUnsavedSceneDismiss, opts.allowVersionMismatch) ?? [];
|
|
530
|
+
const tokenList = tokens.map((t) => `"${t}"`).join(", ");
|
|
531
|
+
return `-- policy picks a non-focused button on ${kind}: click it by name,
|
|
532
|
+
-- never press Return (that would click the focused button)
|
|
533
|
+
if ${titleMatch} then
|
|
534
|
+
repeat with tok in {${tokenList}}
|
|
535
|
+
repeat with btn in buttons of w
|
|
536
|
+
try
|
|
537
|
+
set bt to (title of btn) as text
|
|
538
|
+
on error
|
|
539
|
+
set bt to ""
|
|
540
|
+
end try
|
|
541
|
+
if my normLabel(bt) is (contents of tok) then
|
|
542
|
+
set frontmost to true
|
|
543
|
+
click btn
|
|
544
|
+
return "dismissed:" & bt & ":${kind}"
|
|
545
|
+
end if
|
|
546
|
+
end repeat
|
|
547
|
+
end repeat
|
|
548
|
+
return "blocked:" & "${kind}"
|
|
549
|
+
end if`;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
415
552
|
}
|
|
416
|
-
/**
|
|
417
|
-
|
|
553
|
+
/**
|
|
554
|
+
* AppleScript blocks that match the unsaved-scene prompt and click a named
|
|
555
|
+
* button: `window` checks the dialog window's own title (skipped for the main
|
|
556
|
+
* Editor window), `sheets` checks the sheets of any window (Unity 6 attaches
|
|
557
|
+
* the prompt to the main Editor window).
|
|
558
|
+
*/
|
|
559
|
+
function unsavedSceneMacOsClickBlocks(buttonLabels) {
|
|
418
560
|
const textFrags = [
|
|
419
561
|
"Have Been Modified",
|
|
420
562
|
"Unsaved changes",
|
|
@@ -424,9 +566,9 @@ function unsavedSceneMacOsClickBlock(buttonLabels) {
|
|
|
424
566
|
];
|
|
425
567
|
const titleMatch = (varName) => textFrags.map((f) => `${varName} contains "${f}"`).join(" or ");
|
|
426
568
|
const staticTextScan = (container, flagVar) => `
|
|
427
|
-
repeat with
|
|
569
|
+
repeat with stx in static texts of ${container}
|
|
428
570
|
try
|
|
429
|
-
set tx to (value of
|
|
571
|
+
set tx to (value of stx) as text
|
|
430
572
|
on error
|
|
431
573
|
set tx to ""
|
|
432
574
|
end try
|
|
@@ -450,11 +592,13 @@ function unsavedSceneMacOsClickBlock(buttonLabels) {
|
|
|
450
592
|
end if
|
|
451
593
|
end repeat`)
|
|
452
594
|
.join("");
|
|
453
|
-
return
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
${
|
|
457
|
-
|
|
595
|
+
return {
|
|
596
|
+
window: `
|
|
597
|
+
-- Top-level window title (some Unity versions).
|
|
598
|
+
if (${titleMatch("wt")}) then
|
|
599
|
+
${clickButtons("w")}
|
|
600
|
+
end if`,
|
|
601
|
+
sheets: `
|
|
458
602
|
-- Unity 6 save prompt is usually a sheet on the main Editor window.
|
|
459
603
|
try
|
|
460
604
|
repeat with s in sheets of w
|
|
@@ -470,27 +614,8 @@ function unsavedSceneMacOsClickBlock(buttonLabels) {
|
|
|
470
614
|
${clickButtons("s")}
|
|
471
615
|
end if
|
|
472
616
|
end repeat
|
|
473
|
-
end try
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* Build the per-fragment AppleScript check block. When the policy dismisses
|
|
477
|
-
* the kind, the block activates the window + presses Return and returns the
|
|
478
|
-
* dismissed token; when the policy declines the kind (null tokens), the block
|
|
479
|
-
* is omitted entirely (the loop falls through to the next fragment).
|
|
480
|
-
*
|
|
481
|
-
* AppleScript `contains` is case-insensitive by default, so the lowercased
|
|
482
|
-
* fragment matches Unity's mixed-case titles ("Enter Safe Mode?" contains
|
|
483
|
-
* "entersafemode" → false, BUT "safemode" matches; the fragment list is built
|
|
484
|
-
* to have at least one case-insensitive hit per title).
|
|
485
|
-
*/
|
|
486
|
-
function fragmentCheck(fragment, kind, dismisses) {
|
|
487
|
-
if (!dismisses)
|
|
488
|
-
return `-- policy declines ${kind}; skip`;
|
|
489
|
-
return `if wt contains "${fragment}" then
|
|
490
|
-
set frontmost to true
|
|
491
|
-
key code 36
|
|
492
|
-
return "dismissed:Focus:${kind}"
|
|
493
|
-
end if`;
|
|
617
|
+
end try`,
|
|
618
|
+
};
|
|
494
619
|
}
|
|
495
620
|
async function tryDismissMacOS(opts) {
|
|
496
621
|
return new Promise((resolve) => {
|
|
@@ -553,6 +678,24 @@ export function _resetXdotoolPresenceForTests() {
|
|
|
553
678
|
export function regexEscapeForXdotool(s) {
|
|
554
679
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
555
680
|
}
|
|
681
|
+
/**
|
|
682
|
+
* The `xdotool search --name` pattern for a normalized title fragment.
|
|
683
|
+
* Fragments are letters and digits only ("entersafemode"), while real titles
|
|
684
|
+
* carry spaces and punctuation ("Enter Safe Mode?"), so the pattern allows any
|
|
685
|
+
* run of non-alphanumerics between consecutive characters and spells each
|
|
686
|
+
* letter in both cases. That is the regex form of "the normalized title
|
|
687
|
+
* contains the fragment" and does not depend on xdotool's case handling.
|
|
688
|
+
* Any other character is escaped literally.
|
|
689
|
+
*/
|
|
690
|
+
export function xdotoolTitlePattern(fragment) {
|
|
691
|
+
return [...fragment]
|
|
692
|
+
.map((ch) => /[a-z]/i.test(ch)
|
|
693
|
+
? `[${ch.toLowerCase()}${ch.toUpperCase()}]`
|
|
694
|
+
: /[0-9]/.test(ch)
|
|
695
|
+
? ch
|
|
696
|
+
: regexEscapeForXdotool(ch))
|
|
697
|
+
.join("[^A-Za-z0-9]*");
|
|
698
|
+
}
|
|
556
699
|
/**
|
|
557
700
|
* Look up currently-running Unity Editor PIDs on the local box. Used by the
|
|
558
701
|
* Linux/X11 path to scope the title-fragment match to Unity processes only.
|
|
@@ -574,6 +717,19 @@ function getUnityPidsLinux() {
|
|
|
574
717
|
return [];
|
|
575
718
|
}
|
|
576
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* What the Linux/X11 path does with a Unity-owned window of the given kind:
|
|
722
|
+
* {@link focusDialogAction} with `named` folded into `blocked`, because
|
|
723
|
+
* xdotool can only press Return on the focused (default) button — it cannot
|
|
724
|
+
* click a named one (e.g. `cancel` on the Safe Mode prompt, whose focused
|
|
725
|
+
* button is Ignore, is reported as blocked for a human).
|
|
726
|
+
*
|
|
727
|
+
* Pure; exported for tests.
|
|
728
|
+
*/
|
|
729
|
+
export function linuxFocusAction(kind, opts) {
|
|
730
|
+
const action = focusDialogAction(kind, opts);
|
|
731
|
+
return action === "named" ? "blocked" : action;
|
|
732
|
+
}
|
|
577
733
|
async function tryDismissLinuxX11(opts) {
|
|
578
734
|
if (!isXdotoolAvailable()) {
|
|
579
735
|
return {
|
|
@@ -589,11 +745,11 @@ async function tryDismissLinuxX11(opts) {
|
|
|
589
745
|
// first kind whose xdotool search surfaces a Unity-owned window.
|
|
590
746
|
// - blocked kinds (project_upgrade without opt-in) → report blocked.
|
|
591
747
|
// - decline kinds (tokens === null) → skip (treat as not-found).
|
|
592
|
-
// -
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
|
|
748
|
+
// - policy picks the kind's focused button → activate the window + send
|
|
749
|
+
// Return to click it.
|
|
750
|
+
// - policy picks any other button → blocked: xdotool cannot click a
|
|
751
|
+
// named button, and Return would press the focused one instead (see
|
|
752
|
+
// linuxFocusAction / DIALOG_FOCUSED_BUTTON_TOKENS).
|
|
597
753
|
const kinds = Object.keys(DIALOG_TITLE_FRAGMENTS);
|
|
598
754
|
// Prefer launch_errors first (the common stall), then the safe kinds, then
|
|
599
755
|
// the destructive kinds last so a blocked destructive modal doesn't shadow a
|
|
@@ -640,7 +796,7 @@ async function tryDismissLinuxX11(opts) {
|
|
|
640
796
|
return;
|
|
641
797
|
}
|
|
642
798
|
const kind = order[idx++];
|
|
643
|
-
const
|
|
799
|
+
const action = linuxFocusAction(kind, opts);
|
|
644
800
|
const fragments = DIALOG_TITLE_FRAGMENTS[kind];
|
|
645
801
|
// Probe this kind's fragments one at a time.
|
|
646
802
|
let fIdx = 0;
|
|
@@ -652,7 +808,7 @@ async function tryDismissLinuxX11(opts) {
|
|
|
652
808
|
return;
|
|
653
809
|
}
|
|
654
810
|
const fragment = fragments[fIdx++];
|
|
655
|
-
const child = execFile("xdotool", ["search", "--name",
|
|
811
|
+
const child = execFile("xdotool", ["search", "--name", xdotoolTitlePattern(fragment)], { timeout: XDOTOOL_PROBE_TIMEOUT_MS }, (err, stdout) => {
|
|
656
812
|
activeChild = null;
|
|
657
813
|
disarmWatchdog();
|
|
658
814
|
if (settled)
|
|
@@ -662,21 +818,28 @@ async function tryDismissLinuxX11(opts) {
|
|
|
662
818
|
return;
|
|
663
819
|
}
|
|
664
820
|
const candidateIds = stdout.trim().split(/\s+/).filter(Boolean);
|
|
665
|
-
|
|
821
|
+
// Re-check the title in Node: it must still classify as this
|
|
822
|
+
// kind and must not be the main Editor window, whose title can
|
|
823
|
+
// contain a fragment (a scene name, or "SAFE MODE").
|
|
824
|
+
const acceptTitle = (title) => titleMatchesDialogKind(title, kind);
|
|
825
|
+
findUnityOwnedWindow(candidateIds, unityPids, acceptTitle, (winId) => {
|
|
666
826
|
if (!winId) {
|
|
667
827
|
tryNextFragment();
|
|
668
828
|
return;
|
|
669
829
|
}
|
|
670
|
-
if (
|
|
830
|
+
if (action !== "focus") {
|
|
671
831
|
// Found the dialog but the policy declines. Report blocked
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
if (blocked
|
|
832
|
+
// for genuinely blocked kinds; a `skip` decline (manual /
|
|
833
|
+
// safe-mode on a kind with no safe button) is reported as
|
|
834
|
+
// not-found so the loop keeps ticking.
|
|
835
|
+
if (action === "blocked") {
|
|
836
|
+
const policyBlocks = blockedKindsForPolicy(opts.policy, opts.allowProjectUpgrade, opts.allowUnsavedSceneDismiss, opts.allowVersionMismatch).includes(kind);
|
|
676
837
|
finish({
|
|
677
838
|
kind: "blocked",
|
|
678
839
|
dialog: kind,
|
|
679
|
-
message:
|
|
840
|
+
message: policyBlocks
|
|
841
|
+
? `Policy '${opts.policy}' declines to dismiss ${kind} dialog`
|
|
842
|
+
: `Policy '${opts.policy}' selects a button other than the focused one on the ${kind} dialog; xdotool can only press Return on the focused button, so it needs a human`,
|
|
680
843
|
});
|
|
681
844
|
return;
|
|
682
845
|
}
|
|
@@ -722,14 +885,15 @@ async function tryDismissLinuxX11(opts) {
|
|
|
722
885
|
}
|
|
723
886
|
/**
|
|
724
887
|
* Walk `candidateIds` and call `done(winId)` with the first window owned by
|
|
725
|
-
* a Unity PID
|
|
726
|
-
*
|
|
888
|
+
* a Unity PID whose title (`xdotool getwindowname`) passes `acceptTitle`, or
|
|
889
|
+
* `done(undefined)` if none match. Sequential — the list is small and
|
|
890
|
+
* parallelism buys nothing meaningful.
|
|
727
891
|
*
|
|
728
892
|
* `stop` (optional) is polled before each probe and recursion step: once the
|
|
729
893
|
* outer probe has settled, the walk must not spawn further getwindowpid
|
|
730
894
|
* children (the result would be discarded anyway).
|
|
731
895
|
*/
|
|
732
|
-
function findUnityOwnedWindow(candidateIds, unityPids, done, stop) {
|
|
896
|
+
function findUnityOwnedWindow(candidateIds, unityPids, acceptTitle, done, stop) {
|
|
733
897
|
let idx = 0;
|
|
734
898
|
const next = () => {
|
|
735
899
|
if (stop?.())
|
|
@@ -749,7 +913,7 @@ function findUnityOwnedWindow(candidateIds, unityPids, done, stop) {
|
|
|
749
913
|
}
|
|
750
914
|
const pid = parseInt(stdout.trim(), 10);
|
|
751
915
|
if (Number.isFinite(pid) && unityPids.has(pid)) {
|
|
752
|
-
|
|
916
|
+
checkTitle(winId);
|
|
753
917
|
return;
|
|
754
918
|
}
|
|
755
919
|
next();
|
|
@@ -757,6 +921,23 @@ function findUnityOwnedWindow(candidateIds, unityPids, done, stop) {
|
|
|
757
921
|
// SIGKILL escalation for a wedged getwindowpid probe.
|
|
758
922
|
const disarmWatchdog = armSigkillEscalation(child, LINUX_PROBE_TIMEOUT_MS, () => stop?.() ?? false);
|
|
759
923
|
};
|
|
924
|
+
const checkTitle = (winId) => {
|
|
925
|
+
if (stop?.())
|
|
926
|
+
return;
|
|
927
|
+
const child = execFile("xdotool", ["getwindowname", winId], { timeout: LINUX_PROBE_TIMEOUT_MS }, (err, stdout) => {
|
|
928
|
+
disarmWatchdog();
|
|
929
|
+
if (stop?.())
|
|
930
|
+
return;
|
|
931
|
+
// An unreadable title is not evidence of a dialog: skip the window.
|
|
932
|
+
if (!err && acceptTitle(stdout.replace(/\r?\n$/, ""))) {
|
|
933
|
+
done(winId);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
next();
|
|
937
|
+
});
|
|
938
|
+
// SIGKILL escalation for a wedged getwindowname probe.
|
|
939
|
+
const disarmWatchdog = armSigkillEscalation(child, LINUX_PROBE_TIMEOUT_MS, () => stop?.() ?? false);
|
|
940
|
+
};
|
|
760
941
|
next();
|
|
761
942
|
}
|
|
762
943
|
// ---------------------------------------------------------------------------
|
|
@@ -896,6 +1077,7 @@ export function readDismissConfig(env = process.env) {
|
|
|
896
1077
|
policy,
|
|
897
1078
|
allowProjectUpgrade: env.UNITY_OPEN_MCP_ALLOW_PROJECT_UPGRADE === "1",
|
|
898
1079
|
allowUnsavedSceneDismiss: env.UNITY_OPEN_MCP_ALLOW_UNSAVED_SCENE_DISMISS === "1",
|
|
1080
|
+
allowVersionMismatch: env.UNITY_OPEN_MCP_ALLOW_VERSION_MISMATCH === "1",
|
|
899
1081
|
};
|
|
900
1082
|
}
|
|
901
1083
|
function parsePositiveInt(raw, fallback) {
|
|
@@ -948,6 +1130,7 @@ export async function pollAndDismissDialogs(opts) {
|
|
|
948
1130
|
policy: opts.policy,
|
|
949
1131
|
allowProjectUpgrade: opts.allowProjectUpgrade,
|
|
950
1132
|
allowUnsavedSceneDismiss: opts.allowUnsavedSceneDismiss,
|
|
1133
|
+
allowVersionMismatch: opts.allowVersionMismatch,
|
|
951
1134
|
// M12 — thread the loop's abort signal into the probe so an in-flight
|
|
952
1135
|
// osascript/PowerShell child is killed the moment the readiness abort
|
|
953
1136
|
// fires, instead of running out its own ~5s timeout after the compile
|
|
@@ -966,17 +1149,19 @@ export async function pollAndDismissDialogs(opts) {
|
|
|
966
1149
|
// one error and surfaces the next.
|
|
967
1150
|
}
|
|
968
1151
|
else if (outcome.kind === "blocked") {
|
|
969
|
-
//
|
|
970
|
-
//
|
|
1152
|
+
// Destructive or version-mismatch dialog: log once per kind per run,
|
|
1153
|
+
// then keep polling. A human must dismiss
|
|
971
1154
|
// it; exiting would hide the stall behind a clean-looking readiness
|
|
972
1155
|
// abort.
|
|
973
1156
|
if (!seenBlocked.has(outcome.dialog)) {
|
|
974
1157
|
seenBlocked.add(outcome.dialog);
|
|
975
1158
|
const optIn = outcome.dialog === "project_upgrade"
|
|
976
1159
|
? "Set UNITY_OPEN_MCP_ALLOW_PROJECT_UPGRADE=1 to opt in to auto-confirm (irreversible)."
|
|
977
|
-
: outcome.dialog === "
|
|
978
|
-
? "Set
|
|
979
|
-
:
|
|
1160
|
+
: outcome.dialog === "non_matching_editor"
|
|
1161
|
+
? "Set UNITY_OPEN_MCP_ALLOW_VERSION_MISMATCH=1 only for an intentional upgrade or compatibility run."
|
|
1162
|
+
: outcome.dialog === "unsaved_scene_changes"
|
|
1163
|
+
? "Set UNITY_OPEN_MCP_ALLOW_UNSAVED_SCENE_DISMISS=1 to opt in to auto-dismiss (destructive — data loss either way)."
|
|
1164
|
+
: "Dismiss it manually or change UNITY_OPEN_MCP_DIALOG_POLICY.";
|
|
980
1165
|
log(`[unity-open-mcp] dialog auto-dismiss blocked on ${outcome.dialog} ` +
|
|
981
1166
|
`(policy=${opts.policy}): ${outcome.message}. ${optIn}`);
|
|
982
1167
|
}
|