@mearl/client 2.19.2 → 2.20.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.
@@ -173,7 +173,7 @@ export interface BrowserInfoBase {
173
173
  }
174
174
  export type BrowserScreenshotKind = 'native' | 'reconstructed' | 'none';
175
175
  /** Browser-targeted actions that an external provider may implement. */
176
- export type BrowserCapabilityAction = ExtensionBrowserAction;
176
+ export type BrowserCapabilityAction = ExtensionBrowserAction | 'page_download';
177
177
  /** Capabilities advertised by an external browser-control provider. */
178
178
  export interface BrowserCapabilities {
179
179
  actions: BrowserCapabilityAction[];
@@ -222,6 +222,52 @@ export interface BrowserReleaseResult {
222
222
  affectedTabIds: number[];
223
223
  failures?: BrowserReleaseFailure[];
224
224
  }
225
+ export interface ControlSessionInfo {
226
+ sessionId: string;
227
+ browserId: string;
228
+ transport: BrowserTransport;
229
+ status: 'active' | 'user' | 'releasing' | 'released';
230
+ generation: number;
231
+ tabs: Array<{
232
+ tabId: number;
233
+ source: 'created' | 'attached';
234
+ title?: string;
235
+ url?: string;
236
+ }>;
237
+ reason?: string;
238
+ }
239
+ export interface BrowserReleaseParams {
240
+ /** Close only session-created pages; attached user pages are protected. */
241
+ closeCreatedTabs?: boolean;
242
+ keepTabIds?: number[];
243
+ /** Preserve the browser environment by default for a scoped release. */
244
+ preserveEnvironment?: boolean;
245
+ keepArtifactIds?: string[];
246
+ }
247
+ export interface BrowserControlParams {
248
+ operation: 'handoff' | 'resume';
249
+ tabId?: number;
250
+ reason?: string;
251
+ }
252
+ export interface BrowserArtifact {
253
+ artifactId: string;
254
+ filename: string;
255
+ bytes: number;
256
+ location: 'browser';
257
+ /** Global browser route is attached by the unified client. */
258
+ browserId?: string;
259
+ }
260
+ export interface PageDownloadParams {
261
+ tabId: number;
262
+ trigger: Omit<PageClickParams, 'tabId'>;
263
+ /** Restrict the expected download URL; useful when the browser cannot identify its source. */
264
+ url?: string;
265
+ timeout?: number;
266
+ }
267
+ export interface PageDownloadResult {
268
+ download: BrowserArtifact;
269
+ action: unknown;
270
+ }
225
271
  export interface CookieCopyScope {
226
272
  /** Maintained authentication-domain presets to include. */
227
273
  presets?: Array<'mearl-services'>;
@@ -595,7 +641,14 @@ export interface PageNavigateParams {
595
641
  export interface PageUploadParams {
596
642
  tabId: number;
597
643
  selector: string;
598
- filePaths: string[];
644
+ /** Absolute paths available to the browser host. */
645
+ filePaths?: string[];
646
+ /** SDK/CLI stages caller-local files before dispatch. Mutually exclusive with filePaths. */
647
+ files?: Array<{
648
+ filePath: string;
649
+ } | {
650
+ artifactId: string;
651
+ }>;
599
652
  observe?: PageObservationWaitOptions | false;
600
653
  diagnostics?: PageDiagnosticsSetting;
601
654
  }
@@ -616,9 +669,10 @@ export interface SetTimezoneResult {
616
669
  timezone: string | null;
617
670
  }
618
671
  export interface GetCookieParams {
619
- tabId: number;
672
+ /** Select the tab's cookie store; omitted for the browser's default store. */
673
+ tabId?: number;
620
674
  name: string;
621
- /** Cookie lookup URL; defaults to the current tab URL. */
675
+ /** Cookie lookup URL; required without tabId, otherwise defaults to the tab URL. */
622
676
  url?: string;
623
677
  domain?: string;
624
678
  path?: string;
@@ -633,9 +687,12 @@ export interface GetCookieResult {
633
687
  path?: string;
634
688
  }
635
689
  export interface SetCookieParams {
636
- tabId: number;
690
+ /** Select the tab's cookie store; required when reload is true. */
691
+ tabId?: number;
637
692
  name: string;
638
693
  value: string;
694
+ /** Cookie target URL; required when tabId is omitted. */
695
+ url?: string;
639
696
  domain?: string;
640
697
  path?: string;
641
698
  reload?: boolean;
@@ -715,9 +772,19 @@ export interface BrowserActionMap {
715
772
  response: SelectBrowserResult;
716
773
  };
717
774
  browser_release: {
718
- request: undefined;
775
+ request: BrowserReleaseParams;
719
776
  response: BrowserReleaseResult;
720
777
  };
778
+ browser_control: {
779
+ request: BrowserControlParams;
780
+ response: {
781
+ session: ControlSessionInfo;
782
+ };
783
+ };
784
+ page_download: {
785
+ request: PageDownloadParams;
786
+ response: PageDownloadResult;
787
+ };
721
788
  browser_launch: {
722
789
  request: BrowserLaunchParams;
723
790
  response: ManagedBrowserInfo;
@@ -829,10 +896,12 @@ export interface BrowserActionMap {
829
896
  }
830
897
  export type BrowserAction = keyof BrowserActionMap;
831
898
  export type BrowserActionRequest<A extends BrowserAction> = BrowserActionMap[A]['request'];
832
- export type BrowserActionResponse<A extends BrowserAction> = BrowserActionMap[A]['response'];
899
+ export type BrowserActionResponse<A extends BrowserAction> = BrowserActionMap[A]['response'] & {
900
+ session?: ControlSessionInfo;
901
+ };
833
902
  export type BrowserCommandAction = Exclude<BrowserAction, 'browser_select_browser'>;
834
- export type ExtensionBrowserAction = Exclude<BrowserCommandAction, 'browser_list' | 'browser_launch' | 'browser_close'>;
835
- export declare const TAB_ID_REQUIRED_ACTIONS: readonly ["tab_checkpoint", "capture_checkpoint", "get_requests", "get_logs", "get_events", "set_mock", "get_mocks", "page_screenshot", "tab_close", "page_click", "page_drag", "page_type", "page_scroll", "page_hover", "page_eval", "page_inspect", "page_snapshot", "page_press", "page_wait", "page_navigate", "page_upload", "page_frames", "set_device_emulation", "set_app_profile", "set_timezone", "get_cookie", "set_cookie", "run_actions"];
903
+ export type ExtensionBrowserAction = Exclude<BrowserCommandAction, 'browser_list' | 'browser_launch' | 'browser_close' | 'browser_control' | 'page_download'>;
904
+ export declare const TAB_ID_REQUIRED_ACTIONS: readonly ["tab_checkpoint", "capture_checkpoint", "get_requests", "get_logs", "get_events", "set_mock", "get_mocks", "page_screenshot", "tab_close", "page_click", "page_drag", "page_type", "page_scroll", "page_hover", "page_eval", "page_inspect", "page_snapshot", "page_press", "page_wait", "page_navigate", "page_upload", "page_download", "page_frames", "set_device_emulation", "set_app_profile", "set_timezone", "run_actions"];
836
905
  export type TabIdRequiredAction = (typeof TAB_ID_REQUIRED_ACTIONS)[number];
837
906
  export declare function actionRequiresTabId(action: unknown): action is TabIdRequiredAction;
838
907
  export declare function validateRequiredActionTabId(action: string, data: unknown): void;
@@ -853,6 +922,8 @@ export declare const BROWSER_ACTIONS: {
853
922
  readonly browser_list: true;
854
923
  readonly browser_select_browser: true;
855
924
  readonly browser_release: true;
925
+ readonly browser_control: true;
926
+ readonly page_download: true;
856
927
  readonly browser_launch: true;
857
928
  readonly browser_close: true;
858
929
  readonly page_screenshot: true;
@@ -895,6 +966,8 @@ export declare const BROWSER_COMMAND_ACTIONS: {
895
966
  readonly send_mtop_request: true;
896
967
  readonly browser_list: true;
897
968
  readonly browser_release: true;
969
+ readonly browser_control: true;
970
+ readonly page_download: true;
898
971
  readonly browser_launch: true;
899
972
  readonly browser_close: true;
900
973
  readonly page_screenshot: true;
@@ -962,6 +1035,46 @@ export declare const EXTENSION_BROWSER_ACTIONS: {
962
1035
  readonly request_domain_permission: true;
963
1036
  readonly run_actions: true;
964
1037
  };
1038
+ export declare const BROWSER_CAPABILITY_ACTIONS: {
1039
+ readonly page_download: true;
1040
+ readonly tab_checkpoint: true;
1041
+ readonly capture_checkpoint: true;
1042
+ readonly get_requests: true;
1043
+ readonly get_logs: true;
1044
+ readonly get_events: true;
1045
+ readonly set_mock: true;
1046
+ readonly get_mocks: true;
1047
+ readonly set_rule: true;
1048
+ readonly get_rules: true;
1049
+ readonly send_request: true;
1050
+ readonly send_mtop_request: true;
1051
+ readonly browser_release: true;
1052
+ readonly page_screenshot: true;
1053
+ readonly tab_open: true;
1054
+ readonly tab_close: true;
1055
+ readonly tab_list: true;
1056
+ readonly tab_group: true;
1057
+ readonly page_click: true;
1058
+ readonly page_drag: true;
1059
+ readonly page_type: true;
1060
+ readonly page_scroll: true;
1061
+ readonly page_hover: true;
1062
+ readonly page_eval: true;
1063
+ readonly page_inspect: true;
1064
+ readonly page_snapshot: true;
1065
+ readonly page_press: true;
1066
+ readonly page_wait: true;
1067
+ readonly page_navigate: true;
1068
+ readonly page_upload: true;
1069
+ readonly page_frames: true;
1070
+ readonly set_device_emulation: true;
1071
+ readonly set_app_profile: true;
1072
+ readonly set_timezone: true;
1073
+ readonly get_cookie: true;
1074
+ readonly set_cookie: true;
1075
+ readonly request_domain_permission: true;
1076
+ readonly run_actions: true;
1077
+ };
965
1078
  export declare function isBrowserAction(value: unknown): value is BrowserAction;
966
1079
  export declare function isBrowserCommandAction(value: unknown): value is BrowserCommandAction;
967
1080
  export declare function isBrowserCapabilityAction(value: unknown): value is BrowserCapabilityAction;
@@ -72,12 +72,11 @@ export const TAB_ID_REQUIRED_ACTIONS = [
72
72
  'page_wait',
73
73
  'page_navigate',
74
74
  'page_upload',
75
+ 'page_download',
75
76
  'page_frames',
76
77
  'set_device_emulation',
77
78
  'set_app_profile',
78
79
  'set_timezone',
79
- 'get_cookie',
80
- 'set_cookie',
81
80
  'run_actions',
82
81
  ];
83
82
  const TAB_ID_REQUIRED_ACTION_SET = new Set(TAB_ID_REQUIRED_ACTIONS);
@@ -85,11 +84,22 @@ export function actionRequiresTabId(action) {
85
84
  return typeof action === 'string' && TAB_ID_REQUIRED_ACTION_SET.has(action);
86
85
  }
87
86
  export function validateRequiredActionTabId(action, data) {
88
- if (!actionRequiresTabId(action))
87
+ const cookieAction = action === 'get_cookie' || action === 'set_cookie';
88
+ if (!actionRequiresTabId(action) && !cookieAction)
89
89
  return;
90
- const tabId = data && typeof data === 'object' && !Array.isArray(data)
91
- ? data.tabId
90
+ const input = data && typeof data === 'object' && !Array.isArray(data)
91
+ ? data
92
92
  : undefined;
93
+ const tabId = input?.tabId;
94
+ if (cookieAction && tabId === undefined) {
95
+ if (action === 'set_cookie' && input?.reload === true) {
96
+ throw new Error('set_cookie: reload requires tabId');
97
+ }
98
+ if (typeof input?.url !== 'string' || !input.url.trim()) {
99
+ throw new Error(`${action}: specify url or tabId`);
100
+ }
101
+ return;
102
+ }
93
103
  if (typeof tabId !== 'number' || !Number.isSafeInteger(tabId) || tabId < 0) {
94
104
  throw new Error(`${action} requires a numeric tabId. Call tab_open or tab_list to obtain a tabId.`);
95
105
  }
@@ -119,6 +129,8 @@ export const BROWSER_ACTIONS = {
119
129
  browser_list: true,
120
130
  browser_select_browser: true,
121
131
  browser_release: true,
132
+ browser_control: true,
133
+ page_download: true,
122
134
  browser_launch: true,
123
135
  browser_close: true,
124
136
  page_screenshot: true,
@@ -161,6 +173,8 @@ export const BROWSER_COMMAND_ACTIONS = {
161
173
  send_mtop_request: true,
162
174
  browser_list: true,
163
175
  browser_release: true,
176
+ browser_control: true,
177
+ page_download: true,
164
178
  browser_launch: true,
165
179
  browser_close: true,
166
180
  page_screenshot: true,
@@ -228,6 +242,10 @@ export const EXTENSION_BROWSER_ACTIONS = {
228
242
  request_domain_permission: true,
229
243
  run_actions: true,
230
244
  };
245
+ export const BROWSER_CAPABILITY_ACTIONS = {
246
+ ...EXTENSION_BROWSER_ACTIONS,
247
+ page_download: true,
248
+ };
231
249
  export function isBrowserAction(value) {
232
250
  return typeof value === 'string' && Object.hasOwn(BROWSER_ACTIONS, value);
233
251
  }
@@ -235,7 +253,7 @@ export function isBrowserCommandAction(value) {
235
253
  return typeof value === 'string' && Object.hasOwn(BROWSER_COMMAND_ACTIONS, value);
236
254
  }
237
255
  export function isBrowserCapabilityAction(value) {
238
- return isExtensionBrowserAction(value);
256
+ return typeof value === 'string' && Object.hasOwn(BROWSER_CAPABILITY_ACTIONS, value);
239
257
  }
240
258
  export function isExtensionBrowserAction(value) {
241
259
  return typeof value === 'string' && Object.hasOwn(EXTENSION_BROWSER_ACTIONS, value);
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invoke, invokePlugin, listCloudConnectors, } from './unified.js';
8
8
  export type { UnifiedClientOptions, UnifiedInvokeOptions, UnifiedInvokeOptions as InvokeOptions, } from './unified.js';
9
9
  export type { PluginInvocation, PluginInvokeOptions, PluginProgress, StructuredPluginProgress, } from './plugin.js';
10
- export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
10
+ export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_CAPABILITY_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
11
11
  export type { ActionStep, AppProfileName, BrowserAction, BrowserActionMap, BrowserActionRequest, BrowserActionResponse, BrowserCapabilities, BrowserCapabilityAction, BrowserCloseParams, BrowserCommandAction, BrowserInfo, BrowserInfoBase, BrowserJsonValue, BrowserLaunchParams, BrowserProviderOperation, BrowserListResult, BrowserPairingInfo, BrowserReleaseFailure, BrowserReleaseResult, BrowserProviderDefinition, BrowserProviderIdentity, BrowserProviderInputSchema, BrowserProviderListResult, BrowserReference, BrowserDetails, BrowserSource, BrowserStatus, BrowserScreenshotKind, BrowserTransport, CaptureCheckpointParams, CaptureCheckpointResult, BrowserDevicePresetName, BrowserEmulationOptions, ExtensionBrowserAction, GetCookieParams, GetCookieResult, GetEventsParams, GetLogsParams, GetMocksParams, GetRequestsParams, ManagedBrowserInfo, PageClickParams, PageDragEndpoint, PageDragParams, PageDragPoint, PageActAction, PageDiagnosticsOptions, PageDiagnosticsSetting, PageEvalParams, PageFramesParams, PageHoverParams, PageInspectFilter, PageInspectParams, PageNavigateParams, PageObservationWaitOptions, PagePressParams, PageScreenshotParams, PageScrollParams, PageSnapshotParams, PageSnapshotQuery, PageTypeParams, PageUploadParams, PageWaitParams, RegularBrowserInfo, RequestDomainPermissionParams, RulesResult, RunActionName, RunActionsParams, SelectBrowserParams, SelectBrowserResult, SendRequestFile, SendRequestFormValue, SendMtopRequestParams, SendRequestParams, SetAppProfileParams, SetCookieParams, SetCookieResult, SetDeviceEmulationParams, SetMockParams, SetRuleParams, SetTimezoneParams, SetTimezoneResult, TabCloseParams, TabOpenParams, } from './generated-browser-action-protocol.js';
12
12
  import { type UnifiedInvokeOptions } from './unified.js';
13
13
  import type * as Protocol from './generated-browser-action-protocol.js';
@@ -24,6 +24,7 @@ export declare function sendMtopRequest(params: Protocol.SendMtopRequestParams,
24
24
  export declare function browserList(options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_list'>>;
25
25
  export declare function selectBrowser(params?: Protocol.SelectBrowserParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_select_browser'>>;
26
26
  export declare function browserRelease(options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_release'>>;
27
+ export declare function browserRelease(params: Protocol.BrowserReleaseParams, options: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_release'>>;
27
28
  export declare function browserLaunch(params: Protocol.BrowserLaunchParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_launch'>>;
28
29
  export declare function browserClose(params: Protocol.BrowserCloseParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_close'>>;
29
30
  export declare function setRule(params: Protocol.SetRuleParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'set_rule'>>;
@@ -52,3 +53,11 @@ export declare function setTimezone(params: Protocol.SetTimezoneParams, options?
52
53
  export declare function getCookie(params: Protocol.GetCookieParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'get_cookie'>>;
53
54
  export declare function setCookie(params: Protocol.SetCookieParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'set_cookie'>>;
54
55
  export declare function runActions(params: Protocol.RunActionsParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'run_actions'>>;
56
+ export declare function browserControl(params: Protocol.BrowserControlParams, options?: UnifiedInvokeOptions): Promise<any>;
57
+ export declare function pageDownload(params: Protocol.PageDownloadParams, options?: UnifiedInvokeOptions): Promise<any>;
58
+ export declare function saveArtifact(artifact: Protocol.BrowserArtifact & {
59
+ browserId?: string;
60
+ }, outputPath: string, options?: UnifiedInvokeOptions): Promise<{
61
+ outputPath: string;
62
+ bytes: number;
63
+ }>;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * then routes actions using the global browser ids returned by browser_list.
6
6
  */
7
7
  export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invoke, invokePlugin, listCloudConnectors, } from './unified.js';
8
- export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
8
+ export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_CAPABILITY_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
9
9
  import { invoke } from './unified.js';
10
10
  export function tabCheckpoint(params, options) {
11
11
  return invoke('tab_checkpoint', params, options);
@@ -41,8 +41,10 @@ export function browserList(options) {
41
41
  export function selectBrowser(params = {}, options) {
42
42
  return invoke('browser_select_browser', params, options);
43
43
  }
44
- export function browserRelease(options) {
45
- return invoke('browser_release', {}, options);
44
+ export function browserRelease(paramsOrOptions = {}, options) {
45
+ return options
46
+ ? invoke('browser_release', paramsOrOptions, options)
47
+ : invoke('browser_release', {}, paramsOrOptions);
46
48
  }
47
49
  export function browserLaunch(params, options) {
48
50
  return invoke('browser_launch', params, options);
@@ -128,3 +130,13 @@ export function setCookie(params, options) {
128
130
  export function runActions(params, options) {
129
131
  return invoke('run_actions', params, options);
130
132
  }
133
+ export function browserControl(params, options) {
134
+ return invoke('browser_control', params, options);
135
+ }
136
+ export function pageDownload(params, options) {
137
+ return invoke('page_download', params, options);
138
+ }
139
+ export async function saveArtifact(artifact, outputPath, options = {}) {
140
+ const { exportArtifact } = await import('./artifacts.js');
141
+ return exportArtifact({ invoke }, artifact, outputPath, options);
142
+ }
package/dist/plugin.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type OfficialPluginInstaller } from './officialPluginInstaller.js';
2
- import { type PluginToolboxContribution } from './pluginRegistry.js';
2
+ import { type PluginToolboxContribution, type PluginRegistration } from './pluginRegistry.js';
3
3
  import type { UnifiedInvokeOptions } from './unified.js';
4
4
  export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, readPluginPageBundleChunk, type PluginRegistration, type PluginRegistrationOptions, type PluginToolboxAgentConfig, type PluginPageBundleRegistration, type PluginToolboxContribution, type PluginToolboxLocale, type PluginToolboxSandboxPage, } from './pluginRegistry.js';
5
5
  export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, type OfficialPluginDefinition, type OfficialPluginName, } from './officialPluginCatalog.js';
@@ -22,11 +22,14 @@ export interface PluginExecutionContext {
22
22
  }
23
23
  export interface PluginApp {
24
24
  readonly options: Readonly<PluginOptions>;
25
- command(signature: string, handler: PluginHandler, description?: string): void;
25
+ /** Examples omit the `mearl <plugin>` prefix, which help adds when displayed. */
26
+ command(signature: string, handler: PluginHandler, description?: string, examples?: string[]): void;
26
27
  invoke(action: string, payload?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
27
28
  progress(progress: PluginProgress): void;
28
29
  }
29
30
  export type PluginHandler = (input: Record<string, any>) => unknown | Promise<unknown>;
31
+ /** 工具列表仅检查入口;页面资源只在打开对应工具时检查。 */
32
+ export declare function refreshRegisteredPlugin(name: string, includePage?: boolean): Promise<PluginRegistration | undefined>;
30
33
  /** 注册和执行复用同一宿主能力边界;仅 handler 执行期间开放浏览器调用。 */
31
34
  export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'invoke' | 'options' | 'progress'>, pluginName?: string): Promise<{
32
35
  name: string | undefined;
package/dist/plugin.js CHANGED
@@ -1,11 +1,12 @@
1
- import { readFileSync, statSync } from 'node:fs';
1
+ import { readFileSync } from 'node:fs';
2
2
  import { dirname, extname, resolve } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { Command } from 'commander';
5
5
  import { COMMAND_MAP } from './commands.js';
6
6
  import { findOfficialPlugin } from './officialPluginCatalog.js';
7
7
  import { ensureOfficialPluginRegistered, } from './officialPluginInstaller.js';
8
- import { findPluginRegistration, normalizePluginToolboxContribution, PluginError, } from './pluginRegistry.js';
8
+ import { findPluginRegistration, pluginRegistrationNeedsRefresh, registerPlugin, pluginDirectory, normalizePluginToolboxContribution, PluginError, } from './pluginRegistry.js';
9
+ import { pluginFileVersion } from './pluginSource.js';
9
10
  import { describePythonPlugin, runPythonPluginCommand } from './pythonPlugin.js';
10
11
  export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, readPluginPageBundleChunk, } from './pluginRegistry.js';
11
12
  export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, } from './officialPluginCatalog.js';
@@ -67,14 +68,61 @@ function missingPluginEntryMessage(entry, pluginName) {
67
68
  `卸载命令:npx @mearl/setup plugin uninstall ${name}`,
68
69
  ].join('\n');
69
70
  }
71
+ const registrationRefreshes = new Map();
72
+ /** 工具列表仅检查入口;页面资源只在打开对应工具时检查。 */
73
+ export async function refreshRegisteredPlugin(name, includePage = false) {
74
+ const key = `${pluginDirectory()}/${name}`;
75
+ const pending = registrationRefreshes.get(key);
76
+ if (pending) {
77
+ await pending;
78
+ return refreshRegisteredPlugin(name, includePage);
79
+ }
80
+ const registration = findPluginRegistration(name);
81
+ if (!registration)
82
+ return undefined;
83
+ const entryChanged = pluginRegistrationNeedsRefresh(registration);
84
+ if (!entryChanged && (!includePage || !pluginRegistrationNeedsRefresh(registration, true))) {
85
+ return registration;
86
+ }
87
+ const refresh = (async () => {
88
+ const entryVersion = pluginFileVersion(registration.entry);
89
+ const loaded = entryChanged
90
+ ? await loadPlugin(registration.entry, {
91
+ options: {},
92
+ invoke: async () => {
93
+ throw new Error('注册时不能执行浏览器操作');
94
+ },
95
+ progress: () => {
96
+ throw new Error('注册时不能报告执行进度');
97
+ },
98
+ }, name)
99
+ : registration;
100
+ // 异步加载期间其他进程可能卸载或重新绑定同名插件。
101
+ const current = findPluginRegistration(name);
102
+ if (JSON.stringify(current) !== JSON.stringify(registration))
103
+ return current;
104
+ if (pluginFileVersion(registration.entry) !== entryVersion) {
105
+ throw new PluginError('PLUGIN_SOURCE_CHANGED', `插件更新进行中,请重新打开工具: ${name}`);
106
+ }
107
+ registerPlugin(name, registration.entry, loaded.description, {
108
+ toolbox: loaded.toolbox,
109
+ ...(registration.managedBy ? { managedBy: registration.managedBy } : {}),
110
+ });
111
+ return findPluginRegistration(name);
112
+ })();
113
+ registrationRefreshes.set(key, refresh);
114
+ try {
115
+ return await refresh;
116
+ }
117
+ finally {
118
+ registrationRefreshes.delete(key);
119
+ }
120
+ }
70
121
  /** 注册和执行复用同一宿主能力边界;仅 handler 执行期间开放浏览器调用。 */
71
122
  export async function loadPlugin(entry, context, pluginName) {
72
123
  let entryVersion;
73
124
  try {
74
- const stat = statSync(entry);
75
- if (!stat.isFile())
76
- throw new Error('not a file');
77
- entryVersion = `${stat.mtimeMs}-${stat.size}`;
125
+ entryVersion = pluginFileVersion(entry);
78
126
  }
79
127
  catch {
80
128
  throw new PluginError('PLUGIN_ENTRY_MISSING', missingPluginEntryMessage(entry, pluginName));
@@ -87,11 +135,15 @@ export async function loadPlugin(entry, context, pluginName) {
87
135
  let toolbox;
88
136
  const app = Object.freeze({
89
137
  options: Object.freeze({ ...context.options }),
90
- command(signature, handler, description = '') {
138
+ command(signature, handler, description = '', examples = []) {
91
139
  if (!registering)
92
140
  throw new Error('命令只能在入口注册阶段声明');
93
141
  const [name, ...args] = signature.trim().split(/\s+/);
94
- if (!/^[a-z][a-z0-9-]*$/.test(name) || commands.has(name) || typeof handler !== 'function') {
142
+ if (!/^[a-z][a-z0-9-]*$/.test(name) ||
143
+ commands.has(name) ||
144
+ typeof handler !== 'function' ||
145
+ !Array.isArray(examples) ||
146
+ examples.some((example) => typeof example !== 'string' || !example.trim())) {
95
147
  throw new Error(`无效或重复的命令: ${signature}`);
96
148
  }
97
149
  const command = new Command(name).description(description);
@@ -104,7 +156,7 @@ export async function loadPlugin(entry, context, pluginName) {
104
156
  const names = command.registeredArguments.map(argument => argument.name());
105
157
  if (new Set(names).size !== names.length)
106
158
  throw new Error(`参数名称重复: ${signature}`);
107
- commands.set(name, { command, signature, handler });
159
+ commands.set(name, { command, signature, handler, examples: [...examples] });
108
160
  },
109
161
  invoke(action, payload = {}, options = {}) {
110
162
  if (!executing)
@@ -175,20 +227,13 @@ export async function loadPlugin(entry, context, pluginName) {
175
227
  },
176
228
  help(namespace, name) {
177
229
  const selected = name ? [find(name)] : [...commands.values()];
178
- const example = selected[0].command;
179
- const input = Object.fromEntries(example.registeredArguments.map(argument => [
180
- argument.name(),
181
- argument.variadic ? [`<${argument.name()}>`] : `<${argument.name()}>`,
182
- ]));
230
+ const examples = selected.flatMap(({ examples }) => examples);
183
231
  return [
232
+ '用法:',
184
233
  ...selected.map(({ command, signature }) => ` ${namespace} ${signature} ${command.description()}`),
185
- '',
186
- ` ${namespace} --payload '${JSON.stringify({ command: example.name(), input })}'`,
187
- '',
188
- '常用选项: --browser --timeout --compact --output',
189
- '连接或传输排障运行 mearl check --help。',
190
- '插件选项: --name=value 传入字符串业务字段;--all 传入 all=true;--verbose; -h, --help; -v, --version',
191
- '同名宿主选项及非字符串字段使用 --payload;业务字段与校验规则见对应 skill。',
234
+ ...(examples.length
235
+ ? ['', '示例:', ...examples.map(example => ` ${namespace} ${example}`)]
236
+ : []),
192
237
  ].join('\n');
193
238
  },
194
239
  async run(positionals, payload, namedInput = {}) {
@@ -4,5 +4,6 @@ export interface BuiltPluginPageBundle {
4
4
  hash: string;
5
5
  size: number;
6
6
  chunks: number;
7
+ sources: Record<string, string>;
7
8
  }
8
9
  export declare function buildPluginPageBundle(entry: string, pageEntry: string): BuiltPluginPageBundle;
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readFileSync, realpathSync } from 'node:fs';
3
3
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { pluginFileVersion } from './pluginSource.js';
4
5
  export const PLUGIN_PAGE_CHUNK_BYTES = 384 * 1024;
5
6
  const MAX_PAGE_SOURCE_BYTES = 16 * 1024 * 1024;
6
7
  const MAX_PAGE_BUNDLE_BYTES = 24 * 1024 * 1024;
@@ -31,6 +32,40 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
31
32
  let resolveReady;
32
33
  const ready = new Promise(resolve => { resolveReady = resolve; });
33
34
 
35
+ let pageModalOpen = null;
36
+ let modalRestoreFrame = null;
37
+ const syncPageModal = () => {
38
+ if (!port) return;
39
+ const open = Boolean(document.querySelector?.('dialog:modal'));
40
+ if (open && modalRestoreFrame !== null) {
41
+ cancelAnimationFrame(modalRestoreFrame);
42
+ modalRestoreFrame = null;
43
+ }
44
+ if (open === pageModalOpen) return;
45
+ if (!open && pageModalOpen === true) {
46
+ if (modalRestoreFrame !== null) return;
47
+ // Closing a dialog restores focus and can scroll the anchors before the next paint.
48
+ modalRestoreFrame = requestAnimationFrame(() => {
49
+ modalRestoreFrame = null;
50
+ if (document.querySelector?.('dialog:modal')) return;
51
+ const positions = [];
52
+ for (const { presentation } of inlineAgentPanels.values()) {
53
+ if (!presentation.element.isConnected) continue;
54
+ const rect = presentation.element.getBoundingClientRect();
55
+ if (![rect.top, rect.left, rect.width, rect.height].every(Number.isFinite)) continue;
56
+ const bounds = { top: rect.top, left: rect.left, width: rect.width, height: rect.height };
57
+ presentation.bounds = bounds;
58
+ positions.push({ key: presentation.key, bounds });
59
+ }
60
+ pageModalOpen = false;
61
+ port.postMessage({ type: 'page-modal', open: false, positions });
62
+ });
63
+ return;
64
+ }
65
+ pageModalOpen = open;
66
+ port.postMessage({ type: 'page-modal', open });
67
+ };
68
+
34
69
  const requestHost = async (type, payload = {}) => {
35
70
  await ready;
36
71
  const requestId = String(++requestSequence);
@@ -468,6 +503,19 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
468
503
  port.start();
469
504
  resolveReady(context);
470
505
  port.postMessage({ type: 'ready' });
506
+ // Dialogs in the sandbox cannot cover Agent overlays in the parent document.
507
+ syncPageModal();
508
+ const modalObserver = typeof MutationObserver === 'function'
509
+ ? new MutationObserver(syncPageModal)
510
+ : null;
511
+ modalObserver?.observe(document.documentElement, {
512
+ attributes: true,
513
+ attributeFilter: ['open'],
514
+ childList: true,
515
+ subtree: true,
516
+ });
517
+ document.addEventListener('toggle', syncPageModal, true);
518
+ document.addEventListener('close', syncPageModal, true);
471
519
  };
472
520
  window.addEventListener('message', connect);
473
521
  window.parent.postMessage({ type: 'mearl-plugin-ready' }, '*');
@@ -578,6 +626,7 @@ export function buildPluginPageBundle(entry, pageEntry) {
578
626
  const root = realpathSync(pluginRoot(entry));
579
627
  let sourceBytes = 0;
580
628
  const countedFiles = new Set();
629
+ const sources = {};
581
630
  const resolveLocalFile = (reference, fromFile) => {
582
631
  const cleanReference = reference.trim().split(/[?#]/, 1)[0];
583
632
  if (!cleanReference || isEmbeddedReference(cleanReference))
@@ -585,14 +634,17 @@ export function buildPluginPageBundle(entry, pageEntry) {
585
634
  if (isRemoteReference(cleanReference) || isAbsolute(cleanReference)) {
586
635
  throw new Error(`toolbox.page 不允许远程或绝对资源: ${reference}`);
587
636
  }
588
- const resolved = realpathSync(resolve(dirname(fromFile), cleanReference));
637
+ const sourcePath = resolve(dirname(fromFile), cleanReference);
638
+ const resolved = realpathSync(sourcePath);
589
639
  const relativePath = relative(root, resolved);
590
640
  if (relativePath === '..' || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
591
641
  throw new Error(`toolbox.page 资源超出插件目录: ${reference}`);
592
642
  }
643
+ sources[sourcePath] = pluginFileVersion(sourcePath);
593
644
  return resolved;
594
645
  };
595
646
  const readAsset = (path) => {
647
+ sources[path] ??= pluginFileVersion(path);
596
648
  const content = readFileSync(path);
597
649
  if (!countedFiles.has(path)) {
598
650
  countedFiles.add(path);
@@ -676,5 +728,6 @@ export function buildPluginPageBundle(entry, pageEntry) {
676
728
  hash: createHash('sha256').update(buffer).digest('hex'),
677
729
  size: buffer.byteLength,
678
730
  chunks: Math.ceil(buffer.byteLength / PLUGIN_PAGE_CHUNK_BYTES),
731
+ sources,
679
732
  };
680
733
  }
@@ -37,7 +37,12 @@ export interface PluginRegistration {
37
37
  managedBy?: string;
38
38
  toolbox?: PluginToolboxContribution;
39
39
  pageBundle?: PluginPageBundleRegistration;
40
+ source?: {
41
+ entry: string;
42
+ page?: Record<string, string>;
43
+ };
40
44
  }
45
+ export declare function pluginRegistrationNeedsRefresh(registration: PluginRegistration, includePage?: boolean): boolean;
41
46
  export interface PluginRegistrationOptions {
42
47
  managedBy?: string;
43
48
  toolbox?: PluginToolboxContribution;