ue-mcp 1.3.3 → 1.3.4

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 (36) hide show
  1. package/dist/dialog-guard.d.ts +287 -0
  2. package/dist/dialog-guard.js +700 -0
  3. package/dist/dialog-guard.js.map +1 -0
  4. package/dist/editor-control.d.ts +77 -3
  5. package/dist/editor-control.js +163 -108
  6. package/dist/editor-control.js.map +1 -1
  7. package/dist/epic-cache.d.ts +12 -0
  8. package/dist/epic-cache.js +13 -0
  9. package/dist/epic-cache.js.map +1 -1
  10. package/dist/flow/guarded-bridge.d.ts +44 -10
  11. package/dist/flow/guarded-bridge.js +88 -20
  12. package/dist/flow/guarded-bridge.js.map +1 -1
  13. package/dist/flow/http-server.js +47 -4
  14. package/dist/flow/http-server.js.map +1 -1
  15. package/dist/flow/task-factory.js +41 -0
  16. package/dist/flow/task-factory.js.map +1 -1
  17. package/dist/flow/task-guards.js +17 -1
  18. package/dist/flow/task-guards.js.map +1 -1
  19. package/dist/index.js +191 -68
  20. package/dist/index.js.map +1 -1
  21. package/dist/locking.js +15 -0
  22. package/dist/locking.js.map +1 -1
  23. package/dist/offline.js +11 -2
  24. package/dist/offline.js.map +1 -1
  25. package/dist/session.js +8 -1
  26. package/dist/session.js.map +1 -1
  27. package/dist/tool-counts.json +2 -2
  28. package/dist/tools/editor.js +27 -5
  29. package/dist/tools/editor.js.map +1 -1
  30. package/package.json +2 -2
  31. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +1 -1
  32. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerScrubTests.cpp +1 -1
  33. package/plugin/ue_mcp_bridge/UE_MCP_Bridge.uplugin +1 -1
  34. package/dist/dialog-gate.d.ts +0 -73
  35. package/dist/dialog-gate.js +0 -197
  36. package/dist/dialog-gate.js.map +0 -1
@@ -0,0 +1,287 @@
1
+ /**
2
+ * The dialog guard. One per editor, one implementation, one decision.
3
+ *
4
+ * A modal dialog parks Unreal's game thread. Nothing may run while one is up,
5
+ * whatever raised it and whichever route the caller came in by, and the caller
6
+ * has to be forced to deal with it rather than being left to notice.
7
+ *
8
+ * Everything that can reach an editor delegates here:
9
+ *
10
+ * GuardedBridge.call every bridge request, so tool actions, flow steps,
11
+ * nested flow runs and handler-internal calls are all
12
+ * covered at the one boundary they share
13
+ * tool dispatch actions served in this process, which never reach the
14
+ * bridge and so cannot be refused by it
15
+ * the HTTP routes the same, through the same object
16
+ *
17
+ * There is no second copy of this logic. A call site decides nothing; it asks
18
+ * `check` and does what it says.
19
+ *
20
+ * DETECTION is proactive. The plugin publishes the active modal to its status
21
+ * file from the modal-loop tick, which is the one tick that keeps running while
22
+ * the game thread is parked, so a watcher there knows a dialog appeared even if
23
+ * nothing is being called and the session is completely idle. The plugin's own
24
+ * refusal and an on-demand probe both feed the same state, so a dialog is
25
+ * caught by whichever notices first.
26
+ */
27
+ import type { EditorSession } from "./session.js";
28
+ import type { IBridge } from "./bridge.js";
29
+ import type { ElicitFn } from "./types.js";
30
+ import type { DialogMode } from "./user-state.js";
31
+ /** The dialog, as every layer describes it. */
32
+ export interface BlockingDialog {
33
+ title: string;
34
+ message: string;
35
+ buttons: string[];
36
+ choices: Array<{
37
+ buttonLabel: string;
38
+ respondWith: string;
39
+ }>;
40
+ }
41
+ export declare function isModalSafeMethod(method: string): boolean;
42
+ /** True for the refusal the plugin's own gate emits. */
43
+ export declare function isDialogRefusal(v: unknown): boolean;
44
+ /** A button press, and whether the editor confirmed it landed. */
45
+ export interface DialogPress {
46
+ button: string;
47
+ confirmed: boolean;
48
+ }
49
+ /** What a caller must do about this call. */
50
+ export type GuardDecision = {
51
+ allow: true;
52
+ } | {
53
+ allow: false;
54
+ refusal: Record<string, unknown>;
55
+ };
56
+ export interface GuardDeps {
57
+ /** How this machine wants a blocking dialog handled. */
58
+ mode: () => DialogMode;
59
+ /** Reads the live dialog list. Modal-safe, so it answers while parked. */
60
+ probe: () => Promise<unknown>;
61
+ /** Presses one button by label. */
62
+ press: (buttonLabel: string) => Promise<unknown>;
63
+ /** Present only when the connected client advertised elicitation. */
64
+ elicit?: () => ElicitFn | undefined;
65
+ /**
66
+ * The editor's published status snapshot, polled so a modal raised while
67
+ * nothing is running is still noticed.
68
+ *
69
+ * Supplied rather than read here, so this is not a second reader of the
70
+ * same file: the caller passes the instance-aware one, which prefers
71
+ * `status.<pid>.json` over the shared file two editors of one project
72
+ * take turns writing, and which reports how old the snapshot is.
73
+ */
74
+ readSnapshot?: () => {
75
+ modal?: unknown;
76
+ ageSeconds?: number;
77
+ } | null;
78
+ /** Whether the bridge socket is currently up. */
79
+ isConnected?: () => boolean;
80
+ }
81
+ /**
82
+ * How old a status file may be and still describe a live editor. The plugin
83
+ * flushes it on a timer while running, so anything older is a leftover.
84
+ */
85
+ export declare const STATUS_STALE_AFTER_MS = 15000;
86
+ export declare class DialogGuard {
87
+ private deps;
88
+ private blocking;
89
+ /** True when the only evidence of a dialog is a file nobody is updating. */
90
+ private staleStatus;
91
+ /** What the last interactive answer did, for callers that report it. */
92
+ private pressed;
93
+ /** Identity of the dialog the last press was aimed at. */
94
+ private lastAsked;
95
+ /** The ask in flight, so parallel calls share it instead of each asking. */
96
+ private asking;
97
+ /** Which dialog that in-flight ask is about. */
98
+ private askingFor;
99
+ private poll;
100
+ constructor(deps: GuardDeps);
101
+ /** Replace the dependencies without losing what the guard knows. */
102
+ setDeps(deps: GuardDeps): void;
103
+ /** What the last interactive answer did, or null if nothing was pressed. */
104
+ get lastPressed(): DialogPress | null;
105
+ /** The dialog currently believed to be blocking, if any. */
106
+ get current(): BlockingDialog | null;
107
+ /** How this machine wants a blocking dialog handled, before elicitation. */
108
+ get mode(): DialogMode;
109
+ /** Whether there is anybody to put an elicitation form in front of. */
110
+ get canElicit(): boolean;
111
+ /** Record a dialog. Called by the watcher, a probe, or a plugin refusal. */
112
+ note(dialog: BlockingDialog): void;
113
+ /**
114
+ * Record that the editor is clear.
115
+ *
116
+ * Only ever called with positive evidence: a probe that ANSWERED and listed
117
+ * nothing, or a non-modal-safe bridge call that ran. A failed probe is not
118
+ * evidence and must never land here, or a dropped socket would disarm the
119
+ * guard while the dialog is still on screen.
120
+ */
121
+ clear(): void;
122
+ /**
123
+ * Whether this mode hands over the calls that press a dialog's buttons.
124
+ *
125
+ * The one place that rule lives. stop_editor computed `mode !== "defer"`
126
+ * itself, so the same decision existed twice and could drift: the guard's
127
+ * defer branch omits `choices` entirely while the other kept a stripped
128
+ * version of them.
129
+ *
130
+ * Not conditioned on elicitation. `auto` exists precisely for a caller
131
+ * with nobody to ask, so withholding the press calls from it would leave
132
+ * that caller no way to answer at all. Routes with nobody on them are
133
+ * handled where they are known about, in `refusal`.
134
+ */
135
+ static handsOverPressCalls(mode: DialogMode, canElicit?: boolean): boolean;
136
+ /**
137
+ * The mode as it actually applies, given whether anyone can be asked.
138
+ *
139
+ * interactive with no channel to a person is not interactive: it would
140
+ * report "interactive" and hand the buttons to the agent anyway, which is
141
+ * auto's behaviour under a mode whose contract is that a PERSON chooses.
142
+ * The mode resolver already falls back to defer when the client advertises
143
+ * no elicitation; this applies the same rule everywhere else.
144
+ */
145
+ static effectiveMode(mode: DialogMode, canElicit: boolean): DialogMode;
146
+ /** True when this bridge method may be sent while a modal is up. */
147
+ static bridgeAllowed(method: string): boolean;
148
+ /** True when this tool action may run while a modal is up. */
149
+ static actionAllowed(taskName: string): boolean;
150
+ /**
151
+ * The single decision.
152
+ *
153
+ * `subject` is the bridge method or the `tool.action` being attempted, and is
154
+ * only used to name it back to the caller and to check the allow list.
155
+ */
156
+ check(subject: string, kind: "bridge" | "action", opts?: {
157
+ canElicit?: boolean;
158
+ }): Promise<GuardDecision>;
159
+ /**
160
+ * Recovery actions: always permitted, whatever is latched.
161
+ *
162
+ * Relaunching or stopping an editor is how a caller gets out of a state
163
+ * where the editor is stuck. An editor whose socket has parked while its
164
+ * status thread keeps publishing holds the latch fresh indefinitely, so
165
+ * gating these refused the only way back forever. They still report the
166
+ * dialog through the same stamp every allowed read carries.
167
+ */
168
+ static isRecoveryAction(taskName: string): boolean;
169
+ /**
170
+ * The decision about a dialog the caller ALREADY has, with no probe.
171
+ *
172
+ * `check` is this plus finding the dialog first. A caller that just read it
173
+ * (stop_editor, which must read before it sends a quit) uses this, so the
174
+ * mode is applied in exactly one place without paying for a second read or
175
+ * risking a re-probe that answers differently.
176
+ */
177
+ decideFor(subject: string, dialog: BlockingDialog, opts?: {
178
+ canElicit?: boolean;
179
+ }): Promise<GuardDecision>;
180
+ /**
181
+ * Re-read what is on screen, deciding nothing.
182
+ *
183
+ * `check` applies the mode, which in interactive raises a form and presses a
184
+ * button. A caller that has just answered a dialog by hand and only wants
185
+ * the state corrected must not do that: it would put a form up for whatever
186
+ * prompt the first answer surfaced and press a button on it, unasked.
187
+ */
188
+ refresh(): Promise<void>;
189
+ /**
190
+ * What is on screen right now.
191
+ *
192
+ * The watcher usually knows already. When it does not, ask the editor. A
193
+ * probe that THROWS is not an answer: the state is left exactly as it was,
194
+ * so an unreachable editor cannot disarm the guard.
195
+ */
196
+ private currentDialog;
197
+ /** Put it to the person; return the button they chose, or null. */
198
+ private askUser;
199
+ /** The one refusal shape, whatever the route and whatever the mode. */
200
+ refusal(subject: string, dialog: BlockingDialog, opts?: {
201
+ canElicit?: boolean;
202
+ }): Record<string, unknown>;
203
+ /**
204
+ * The refusal shape. ONE definition, so every route hands a caller the same
205
+ * fields to branch on.
206
+ *
207
+ * Static because stop_editor builds its refusal without an instance: it runs
208
+ * over its own transport while a quit may be in flight. It used to assemble
209
+ * its own object, which carried `dialogBlocking` but no `refusedMethod`,
210
+ * `dialogTitle`, `dialogMessage` or `error`, so a client reading those got
211
+ * undefined depending on which route refused it.
212
+ */
213
+ static describeRefusal(subject: string, dialog: BlockingDialog, resolvedMode: DialogMode, canElicit?: boolean): Record<string, unknown>;
214
+ /**
215
+ * Learn from a bridge reply.
216
+ *
217
+ * A refusal names the dialog. Anything else is evidence the game thread ran,
218
+ * but ONLY for a method the plugin would have refused: a modal-safe method
219
+ * answers either way, so reading the dialog list must not be mistaken for the
220
+ * dialog having gone.
221
+ */
222
+ observe(method: string, result: unknown): void;
223
+ /**
224
+ * Watch the editor's status file so a dialog raised while nothing is running
225
+ * is known immediately, rather than at the next call.
226
+ *
227
+ * The plugin refreshes that file from the modal-loop tick, which keeps firing
228
+ * while the game thread is parked. Watching costs nothing on the hot path and
229
+ * needs no bridge traffic; the poll is a fallback for platforms where the
230
+ * watch does not fire.
231
+ */
232
+ startWatching(intervalMs?: number): void;
233
+ stopWatching(): void;
234
+ }
235
+ /**
236
+ * A raw bridge that still cannot press a dialog button.
237
+ *
238
+ * Plugin guard tasks run on the RAW bridge on purpose: routing them through
239
+ * GuardedBridge would re-enter the guard pipeline that is running them. That
240
+ * left one real hole, because `set_dialog_policy` is modal-safe in the plugin
241
+ * and WILL be served: a guard task could arm a policy that answers the modal
242
+ * already on screen, with no person involved, under a mode that promises
243
+ * exactly the opposite.
244
+ *
245
+ * Applied to the SESSION's bridge, so it holds on every path: the guarded
246
+ * bridge wraps it, guard tasks get it, and a handler reaching for
247
+ * `ctx.session.bridge` directly gets it too. Wrapping only one caller left the
248
+ * escape one line away.
249
+ *
250
+ * This refuses those two methods and nothing else, from state already held, so
251
+ * it adds no round-trip and cannot recurse.
252
+ */
253
+ export declare function withoutDialogActuation<T extends IBridge>(session: EditorSession, raw: T): T;
254
+ /**
255
+ * Mark a successful result as coming from an editor that is blocked.
256
+ *
257
+ * An allowed read still SAYS a dialog is up: get_status is the first call every
258
+ * client makes, and reporting a healthy editor while the game thread is parked
259
+ * is the one answer it must never give.
260
+ *
261
+ * `editorBlockedByDialog` deliberately is NOT `dialogBlocking`, which means
262
+ * "this call was refused". Stamping that here would have a client treat a
263
+ * successful read as a refusal.
264
+ *
265
+ * Returns the value unchanged when there is nothing to say, or when the shape
266
+ * cannot carry the fields: an array is `typeof "object"`, so it took the
267
+ * properties and then lost them silently in JSON.stringify.
268
+ */
269
+ export declare function stampBlockedEditor(data: unknown, dialog: BlockingDialog | null): unknown;
270
+ /**
271
+ * The guard for a session, created from the session itself if it has none.
272
+ *
273
+ * The gate fails closed: no guard means the boundary cannot establish whether
274
+ * a modal is up, so it refuses. That is only safe if every session HAS one,
275
+ * and guards were created in one startup pass, so a session registered any
276
+ * other way had none and was refused everything it ever tried.
277
+ *
278
+ * Every dependency here is derivable from the session, so there is no reason
279
+ * for that gap to exist. What startup adds on top is the client's elicitation
280
+ * capability, which is not knowable here; guardFor replaces the deps, so that
281
+ * pass upgrades this guard rather than competing with it.
282
+ */
283
+ export declare function ensureGuard(session: EditorSession): Promise<DialogGuard>;
284
+ export declare function guardFor(session: EditorSession, deps: GuardDeps): DialogGuard;
285
+ export declare function existingGuard(session: EditorSession): DialogGuard | undefined;
286
+ /** Test seam. */
287
+ export declare function forgetGuard(session: EditorSession): void;