@mearl/client 2.17.1 → 2.19.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.
@@ -2,20 +2,59 @@ export declare class PluginError extends Error {
2
2
  readonly code: string;
3
3
  constructor(code: string, message: string);
4
4
  }
5
+ export interface PluginToolboxSandboxPage {
6
+ type: 'sandbox';
7
+ runtime?: 'preact';
8
+ entry: string;
9
+ commands: string[];
10
+ }
11
+ export interface PluginToolboxLocale {
12
+ title?: string;
13
+ description?: string;
14
+ }
15
+ export interface PluginToolboxAgentConfig {
16
+ skill: string;
17
+ layout?: 'drawer' | 'workspace';
18
+ }
19
+ export interface PluginToolboxContribution {
20
+ version: 1;
21
+ title: string;
22
+ description?: string;
23
+ icon?: string;
24
+ locales?: Record<string, PluginToolboxLocale>;
25
+ agent?: PluginToolboxAgentConfig;
26
+ page: PluginToolboxSandboxPage;
27
+ order?: number;
28
+ }
29
+ export interface PluginPageBundleRegistration {
30
+ hash: string;
31
+ size: number;
32
+ chunks: number;
33
+ }
5
34
  export interface PluginRegistration {
6
35
  entry: string;
7
36
  description?: string;
8
37
  managedBy?: string;
38
+ toolbox?: PluginToolboxContribution;
39
+ pageBundle?: PluginPageBundleRegistration;
9
40
  }
10
41
  export interface PluginRegistrationOptions {
11
42
  managedBy?: string;
43
+ toolbox?: PluginToolboxContribution;
12
44
  }
13
45
  export declare function pluginDirectory(): string;
14
46
  export declare function assertPluginName(name: string): void;
47
+ export declare function normalizePluginToolboxContribution(value: unknown): PluginToolboxContribution | undefined;
15
48
  export declare function findPluginDirectoryEntry(directory: string): string | undefined;
16
49
  export declare function resolvePluginEntry(target: string): string;
17
50
  export declare function registerPlugin(name: string, target: string, description?: string, options?: PluginRegistrationOptions): string;
18
51
  export declare function unregisterPlugin(name: string): boolean;
19
52
  export declare function findPluginRegistration(name: string): PluginRegistration | undefined;
53
+ export declare function readPluginPageBundleChunk(name: string, hash: string, index: number): {
54
+ data: string;
55
+ hash: string;
56
+ index: number;
57
+ total: number;
58
+ };
20
59
  export declare function findPluginEntry(name: string): string | undefined;
21
60
  export declare function listPluginNames(): string[];
@@ -1,8 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
2
+ import { closeSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { isAbsolute, join, resolve } from 'node:path';
5
5
  import { COMMAND_MAP } from './commands.js';
6
+ import { buildPluginPageBundle, PLUGIN_PAGE_CHUNK_BYTES, } from './pluginPageBundle.js';
6
7
  const RESERVED_NAMES = new Set([
7
8
  'check',
8
9
  'help',
@@ -21,11 +22,196 @@ export class PluginError extends Error {
21
22
  export function pluginDirectory() {
22
23
  return resolve(process.env.MEARL_PLUGIN_DIR || join(homedir(), '.mearl', 'plugins'));
23
24
  }
25
+ function pluginPageDirectory() {
26
+ return join(pluginDirectory(), '.pages');
27
+ }
28
+ function pluginPageBundlePath(name, hash) {
29
+ return join(pluginPageDirectory(), `${name}-${hash}.html`);
30
+ }
31
+ function removePluginPageBundles(name, exceptHash) {
32
+ let files;
33
+ try {
34
+ files = readdirSync(pluginPageDirectory());
35
+ }
36
+ catch (error) {
37
+ if (error.code === 'ENOENT')
38
+ return;
39
+ throw error;
40
+ }
41
+ const keep = exceptHash ? `${name}-${exceptHash}.html` : undefined;
42
+ for (const file of files) {
43
+ if (file === keep || !file.startsWith(`${name}-`) || !file.endsWith('.html'))
44
+ continue;
45
+ rmSync(join(pluginPageDirectory(), file), { force: true });
46
+ }
47
+ }
48
+ function normalizePageBundle(value) {
49
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
50
+ throw new Error('invalid page bundle');
51
+ }
52
+ const candidate = value;
53
+ if (typeof candidate.hash !== 'string' ||
54
+ !/^[a-f0-9]{64}$/.test(candidate.hash) ||
55
+ !Number.isInteger(candidate.size) ||
56
+ (candidate.size ?? 0) <= 0 ||
57
+ !Number.isInteger(candidate.chunks) ||
58
+ candidate.chunks !== Math.ceil((candidate.size ?? 0) / PLUGIN_PAGE_CHUNK_BYTES)) {
59
+ throw new Error('invalid page bundle');
60
+ }
61
+ return {
62
+ hash: candidate.hash,
63
+ size: candidate.size,
64
+ chunks: candidate.chunks,
65
+ };
66
+ }
67
+ function persistPluginPageBundle(name, bundle) {
68
+ const directory = pluginPageDirectory();
69
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
70
+ const target = pluginPageBundlePath(name, bundle.hash);
71
+ try {
72
+ if (statSync(target).isFile())
73
+ return;
74
+ }
75
+ catch {
76
+ // 页面内容哈希变化时创建新的不可变产物。
77
+ }
78
+ const temporary = `${target}.${randomUUID()}.tmp`;
79
+ try {
80
+ writeFileSync(temporary, bundle.content, { mode: 0o600, flag: 'wx' });
81
+ renameSync(temporary, target);
82
+ }
83
+ finally {
84
+ rmSync(temporary, { force: true });
85
+ }
86
+ }
24
87
  export function assertPluginName(name) {
25
88
  if (!/^[a-z][a-z0-9-]*$/.test(name) || RESERVED_NAMES.has(name) || COMMAND_MAP.has(name)) {
26
89
  throw new PluginError('PLUGIN_INVALID_NAME', `不可用的插件名称: ${name}`);
27
90
  }
28
91
  }
92
+ export function normalizePluginToolboxContribution(value) {
93
+ if (value === undefined)
94
+ return undefined;
95
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
96
+ throw new Error('toolbox 必须是对象');
97
+ }
98
+ const candidate = value;
99
+ const title = typeof candidate.title === 'string' ? candidate.title.trim() : '';
100
+ const description = typeof candidate.description === 'string' ? candidate.description.trim() : undefined;
101
+ const icon = typeof candidate.icon === 'string' ? candidate.icon.trim() : undefined;
102
+ if (candidate.version !== 1)
103
+ throw new Error('toolbox.version 必须为 1');
104
+ if (!title || title.length > 80)
105
+ throw new Error('toolbox.title 必须是 1-80 个字符');
106
+ if (description && description.length > 240) {
107
+ throw new Error('toolbox.description 不能超过 240 个字符');
108
+ }
109
+ if (icon && (!/^[a-z][a-z0-9-]*$/.test(icon) || icon.length > 40)) {
110
+ throw new Error('toolbox.icon 必须是短横线命名的图标名称');
111
+ }
112
+ let locales;
113
+ if (candidate.locales !== undefined) {
114
+ if (!candidate.locales ||
115
+ typeof candidate.locales !== 'object' ||
116
+ Array.isArray(candidate.locales)) {
117
+ throw new Error('toolbox.locales 必须是对象');
118
+ }
119
+ const entries = Object.entries(candidate.locales);
120
+ if (entries.length === 0 || entries.length > 20) {
121
+ throw new Error('toolbox.locales 必须包含 1-20 个语言');
122
+ }
123
+ locales = Object.fromEntries(entries.map(([locale, localized]) => {
124
+ if (!/^[a-z]{2,3}(?:[_-][A-Za-z0-9]{2,8})*$/.test(locale)) {
125
+ throw new Error(`toolbox.locales 包含无效语言: ${locale}`);
126
+ }
127
+ if (!localized || typeof localized !== 'object' || Array.isArray(localized)) {
128
+ throw new Error(`toolbox.locales.${locale} 必须是对象`);
129
+ }
130
+ const localizedTitle = typeof localized.title === 'string' ? localized.title.trim() : undefined;
131
+ const localizedDescription = typeof localized.description === 'string' ? localized.description.trim() : undefined;
132
+ if (localizedTitle !== undefined && (!localizedTitle || localizedTitle.length > 80)) {
133
+ throw new Error(`toolbox.locales.${locale}.title 必须是 1-80 个字符`);
134
+ }
135
+ if (localizedDescription !== undefined && localizedDescription.length > 240) {
136
+ throw new Error(`toolbox.locales.${locale}.description 不能超过 240 个字符`);
137
+ }
138
+ if (!localizedTitle && !localizedDescription) {
139
+ throw new Error(`toolbox.locales.${locale} 至少声明 title 或 description`);
140
+ }
141
+ return [
142
+ locale,
143
+ {
144
+ ...(localizedTitle ? { title: localizedTitle } : {}),
145
+ ...(localizedDescription ? { description: localizedDescription } : {}),
146
+ },
147
+ ];
148
+ }));
149
+ }
150
+ if (!candidate.page || typeof candidate.page !== 'object' || Array.isArray(candidate.page)) {
151
+ throw new Error('toolbox.page 必须是对象');
152
+ }
153
+ const pageCandidate = candidate.page;
154
+ if (pageCandidate.type !== 'sandbox') {
155
+ throw new Error('toolbox.page.type 必须为 sandbox');
156
+ }
157
+ if (pageCandidate.runtime !== undefined && pageCandidate.runtime !== 'preact') {
158
+ throw new Error('toolbox.page.runtime 仅支持 preact');
159
+ }
160
+ const pageEntry = typeof pageCandidate.entry === 'string' ? pageCandidate.entry.trim() : '';
161
+ if (!pageEntry ||
162
+ pageEntry.length > 240 ||
163
+ isAbsolute(pageEntry) ||
164
+ pageEntry.includes('\\') ||
165
+ pageEntry.split('/').some(segment => !segment || segment === '.' || segment === '..')) {
166
+ throw new Error('toolbox.page.entry 必须是插件目录内的相对 HTML 路径');
167
+ }
168
+ if (!Array.isArray(pageCandidate.commands)) {
169
+ throw new Error('toolbox.page.commands 必须是命令名称数组');
170
+ }
171
+ const commands = pageCandidate.commands.map(command => typeof command === 'string' ? command.trim() : '');
172
+ if (commands.length === 0 ||
173
+ commands.length > 64 ||
174
+ new Set(commands).size !== commands.length ||
175
+ commands.some(command => !/^[a-z][a-z0-9-]*$/.test(command))) {
176
+ throw new Error('toolbox.page.commands 必须包含 1-64 个不重复的插件命令名称');
177
+ }
178
+ const page = {
179
+ type: 'sandbox',
180
+ ...(pageCandidate.runtime ? { runtime: pageCandidate.runtime } : {}),
181
+ entry: pageEntry,
182
+ commands,
183
+ };
184
+ let agent;
185
+ if (candidate.agent !== undefined) {
186
+ if (!candidate.agent || typeof candidate.agent !== 'object' || Array.isArray(candidate.agent)) {
187
+ throw new Error('toolbox.agent 必须是对象');
188
+ }
189
+ const agentCandidate = candidate.agent;
190
+ const skill = typeof agentCandidate.skill === 'string' ? agentCandidate.skill.trim() : '';
191
+ if (!skill || skill.length > 128 || !/^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/.test(skill)) {
192
+ throw new Error('toolbox.agent.skill 必须是有效的 Skill 名称');
193
+ }
194
+ const layout = agentCandidate.layout;
195
+ if (layout !== undefined && layout !== 'drawer' && layout !== 'workspace') {
196
+ throw new Error('toolbox.agent.layout 必须是 drawer 或 workspace');
197
+ }
198
+ agent = { skill, ...(layout ? { layout } : {}) };
199
+ }
200
+ if (candidate.order !== undefined &&
201
+ (!Number.isInteger(candidate.order) || candidate.order < 0 || candidate.order > 10_000)) {
202
+ throw new Error('toolbox.order 必须是 0-10000 的整数');
203
+ }
204
+ return {
205
+ version: 1,
206
+ title,
207
+ ...(description ? { description } : {}),
208
+ ...(icon ? { icon } : {}),
209
+ ...(locales ? { locales } : {}),
210
+ ...(agent ? { agent } : {}),
211
+ page,
212
+ ...(candidate.order !== undefined ? { order: candidate.order } : {}),
213
+ };
214
+ }
29
215
  export function findPluginDirectoryEntry(directory) {
30
216
  for (const filename of ['main.mjs', 'main.py']) {
31
217
  const entry = join(directory, 'scripts', filename);
@@ -63,10 +249,31 @@ export function registerPlugin(name, target, description, options = {}) {
63
249
  const temporary = `${file}.${randomUUID()}.tmp`;
64
250
  const summary = description?.trim();
65
251
  const managedBy = options.managedBy?.trim();
252
+ let toolbox;
253
+ try {
254
+ toolbox = normalizePluginToolboxContribution(options.toolbox);
255
+ }
256
+ catch (error) {
257
+ throw new PluginError('PLUGIN_INVALID_TOOLBOX', error instanceof Error ? error.message : String(error));
258
+ }
259
+ let builtPage;
260
+ if (toolbox) {
261
+ try {
262
+ builtPage = buildPluginPageBundle(entry, toolbox.page.entry);
263
+ }
264
+ catch (error) {
265
+ throw new PluginError('PLUGIN_INVALID_TOOLBOX_PAGE', error instanceof Error ? error.message : String(error));
266
+ }
267
+ persistPluginPageBundle(name, builtPage);
268
+ }
66
269
  const record = {
67
270
  entry,
68
271
  ...(summary ? { description: summary } : {}),
69
272
  ...(managedBy ? { managedBy } : {}),
273
+ ...(toolbox ? { toolbox } : {}),
274
+ ...(builtPage
275
+ ? { pageBundle: { hash: builtPage.hash, size: builtPage.size, chunks: builtPage.chunks } }
276
+ : {}),
70
277
  };
71
278
  try {
72
279
  writeFileSync(temporary, `${JSON.stringify(record)}\n`, { mode: 0o600, flag: 'wx' });
@@ -75,6 +282,7 @@ export function registerPlugin(name, target, description, options = {}) {
75
282
  finally {
76
283
  rmSync(temporary, { force: true });
77
284
  }
285
+ removePluginPageBundles(name, builtPage?.hash);
78
286
  return entry;
79
287
  }
80
288
  export function unregisterPlugin(name) {
@@ -82,6 +290,7 @@ export function unregisterPlugin(name) {
82
290
  const file = join(pluginDirectory(), `${name}.json`);
83
291
  try {
84
292
  rmSync(file);
293
+ removePluginPageBundles(name);
85
294
  return true;
86
295
  }
87
296
  catch (error) {
@@ -113,16 +322,67 @@ export function findPluginRegistration(name) {
113
322
  const managedBy = typeof record.managedBy === 'string' && record.managedBy.trim()
114
323
  ? record.managedBy.trim()
115
324
  : undefined;
325
+ const toolbox = normalizePluginToolboxContribution(record.toolbox);
326
+ const pageBundle = toolbox ? normalizePageBundle(record.pageBundle) : undefined;
116
327
  return {
117
328
  entry: record.entry,
118
329
  ...(description ? { description } : {}),
119
330
  ...(managedBy ? { managedBy } : {}),
331
+ ...(toolbox ? { toolbox } : {}),
332
+ ...(pageBundle ? { pageBundle } : {}),
120
333
  };
121
334
  }
122
335
  catch {
123
336
  throw new PluginError('PLUGIN_REGISTRY_INVALID', `无效的插件记录: ${file}`);
124
337
  }
125
338
  }
339
+ export function readPluginPageBundleChunk(name, hash, index) {
340
+ assertPluginName(name);
341
+ const registration = findPluginRegistration(name);
342
+ const bundle = registration?.pageBundle;
343
+ if (!bundle || bundle.hash !== hash) {
344
+ throw new PluginError('PLUGIN_PAGE_CHANGED', `插件页面已更新: ${name}`);
345
+ }
346
+ if (!Number.isInteger(index) || index < 0 || index >= bundle.chunks) {
347
+ throw new PluginError('PLUGIN_PAGE_CHUNK_INVALID', `插件页面分块无效: ${index}`);
348
+ }
349
+ const path = pluginPageBundlePath(name, hash);
350
+ let size;
351
+ try {
352
+ size = statSync(path).size;
353
+ }
354
+ catch {
355
+ throw new PluginError('PLUGIN_PAGE_INVALID', `插件页面产物不存在: ${name}`);
356
+ }
357
+ if (size !== bundle.size) {
358
+ throw new PluginError('PLUGIN_PAGE_INVALID', `插件页面产物损坏: ${name}`);
359
+ }
360
+ const start = index * PLUGIN_PAGE_CHUNK_BYTES;
361
+ const content = Buffer.allocUnsafe(Math.min(PLUGIN_PAGE_CHUNK_BYTES, bundle.size - start));
362
+ let descriptor;
363
+ try {
364
+ descriptor = openSync(path, 'r');
365
+ }
366
+ catch {
367
+ throw new PluginError('PLUGIN_PAGE_INVALID', `插件页面产物无法读取: ${name}`);
368
+ }
369
+ let bytesRead;
370
+ try {
371
+ bytesRead = readSync(descriptor, content, 0, content.byteLength, start);
372
+ }
373
+ finally {
374
+ closeSync(descriptor);
375
+ }
376
+ if (bytesRead !== content.byteLength) {
377
+ throw new PluginError('PLUGIN_PAGE_INVALID', `插件页面产物损坏: ${name}`);
378
+ }
379
+ return {
380
+ data: content.toString('base64'),
381
+ hash,
382
+ index,
383
+ total: bundle.chunks,
384
+ };
385
+ }
126
386
  export function findPluginEntry(name) {
127
387
  return findPluginRegistration(name)?.entry;
128
388
  }
@@ -7,6 +7,7 @@ export interface PythonPluginDescriptor {
7
7
  commands: PythonPluginCommand[];
8
8
  description?: string;
9
9
  name?: string;
10
+ toolbox?: unknown;
10
11
  }
11
12
  export interface PythonPluginApp {
12
13
  readonly options: Readonly<PluginOptions>;
@@ -143,10 +143,13 @@ try:
143
143
 
144
144
  plugin_name = getattr(module, "name", None)
145
145
  plugin_description = getattr(module, "description", None)
146
+ plugin_toolbox = getattr(module, "toolbox", None)
146
147
  if plugin_name is not None and not isinstance(plugin_name, str):
147
148
  raise TypeError("入口的 name 必须为字符串")
148
149
  if plugin_description is not None and not isinstance(plugin_description, str):
149
150
  raise TypeError("入口的 description 必须为字符串")
151
+ if plugin_toolbox is not None and not isinstance(plugin_toolbox, dict):
152
+ raise TypeError("入口的 toolbox 必须为对象")
150
153
  register = getattr(module, "register", None)
151
154
  if not callable(register):
152
155
  raise TypeError("入口必须导出 register(app) 函数")
@@ -168,6 +171,8 @@ try:
168
171
  descriptor["name"] = plugin_name
169
172
  if plugin_description is not None:
170
173
  descriptor["description"] = plugin_description
174
+ if plugin_toolbox is not None:
175
+ descriptor["toolbox"] = plugin_toolbox
171
176
  _send(descriptor)
172
177
 
173
178
  if mode == "run":
@@ -233,6 +238,7 @@ function parseDescriptor(message) {
233
238
  commands,
234
239
  ...(typeof message.name === 'string' ? { name: message.name } : {}),
235
240
  ...(typeof message.description === 'string' ? { description: message.description } : {}),
241
+ ...(message.toolbox !== undefined ? { toolbox: message.toolbox } : {}),
236
242
  };
237
243
  }
238
244
  function bridgedError(message) {
package/dist/socket.js CHANGED
@@ -13,7 +13,8 @@ import os from 'node:os';
13
13
  import path from 'node:path';
14
14
  import { createRequire } from 'node:module';
15
15
  import { StringDecoder } from 'node:string_decoder';
16
- import { ensureInstalledProvidersRunning } from '@mearl/provider/runtime';
16
+ import { listInstalledProviders } from '@mearl/provider/host';
17
+ import { readLiveProvider } from '@mearl/provider/runtime';
17
18
  import { resolveFileRefs } from './fileRefs.js';
18
19
  import { listLiveBrowsers, findBrowser, describeBrowser, } from './browserRegistry.js';
19
20
  import { resolveActionTimeoutSec } from './actionTimeouts.js';
@@ -95,12 +96,8 @@ function browserListFromProviderRecords(browsers) {
95
96
  browsers: providerBrowsers,
96
97
  };
97
98
  }
98
- function mergeWarnings(result, warnings) {
99
- const resultWarnings = Array.isArray(result?.warnings)
100
- ? result.warnings.filter((warning) => typeof warning === 'string')
101
- : [];
102
- const merged = [...new Set([...resultWarnings, ...warnings])];
103
- return { ...result, ...(merged.length > 0 ? { warnings: merged } : {}) };
99
+ function areInstalledProvidersRunning() {
100
+ return listInstalledProviders().every(provider => readLiveProvider(provider.providerId));
104
101
  }
105
102
  function isConnectedBrowser(browser) {
106
103
  return browser.status === undefined || browser.status === 'connected';
@@ -243,11 +240,7 @@ export function invoke(action, data = {}, options = {}) {
243
240
  // 统一解析 payload 顶层字段中的 `@<path>` 文件引用(所有 action 通用)
244
241
  const resolvedData = resolveFileRefs(data);
245
242
  if (action === 'browser_list') {
246
- const hasNativeAggregator = listLiveBrowsers().some(liveBrowser => liveBrowser.transport !== 'provider');
247
- const providerStartup = hasNativeAggregator
248
- ? Promise.resolve([])
249
- : ensureInstalledProvidersRunning();
250
- return providerStartup.then(providerWarnings => dispatchInvoke(action, resolvedData, timeoutSec, undefined, undefined, controlSource).then(result => decorateBrowserListResult(mergeWarnings(result, providerWarnings))));
243
+ return dispatchInvoke(action, resolvedData, timeoutSec, undefined, undefined, controlSource).then(result => decorateBrowserListResult(result));
251
244
  }
252
245
  const selector = browser ?? defaultBrowser ?? process.env.MEARL_BROWSER ?? undefined;
253
246
  return dispatchInvoke(action, resolvedData, timeoutSec, transport, selector, controlSource);
@@ -325,7 +318,9 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
325
318
  throw error;
326
319
  }
327
320
  }
328
- if (action === 'browser_list' && browsers.some(browser => browser.transport === 'provider')) {
321
+ if (action === 'browser_list' &&
322
+ browsers.some(browser => browser.transport === 'provider') &&
323
+ areInstalledProvidersRunning()) {
329
324
  return browserListFromProviderRecords(browsers);
330
325
  }
331
326
  return invokeViaLegacy(action, data, timeoutSec, transport, controlSource, /* isRetry */ false);
@@ -1,6 +1,8 @@
1
1
  export interface CloudServerConfig {
2
2
  server: string;
3
3
  }
4
+ /** Number of local calendar days between CLI update checks; null disables them. */
5
+ export declare function loadCliAutoUpdateIntervalDays(configPath?: string): number | null;
4
6
  export declare function getCloudServerConfigPath(): string;
5
7
  export declare function loadCloudServerConfig(): CloudServerConfig | null;
6
8
  export declare function loadLocalConnectorId(): string | null;
@@ -2,6 +2,29 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  const MEARL_CONFIG_DIR = join(homedir(), '.mearl');
5
+ /** Number of local calendar days between CLI update checks; null disables them. */
6
+ export function loadCliAutoUpdateIntervalDays(configPath = join(MEARL_CONFIG_DIR, 'config.json')) {
7
+ try {
8
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
9
+ if (config.cliAutoUpdate === false)
10
+ return null;
11
+ if (config.cliAutoUpdate === undefined)
12
+ return 1;
13
+ if (typeof config.cliAutoUpdate === 'object' &&
14
+ config.cliAutoUpdate !== null &&
15
+ !Array.isArray(config.cliAutoUpdate)) {
16
+ const { intervalDays } = config.cliAutoUpdate;
17
+ if (typeof intervalDays === 'number' &&
18
+ Number.isSafeInteger(intervalDays) &&
19
+ intervalDays > 0)
20
+ return intervalDays;
21
+ }
22
+ }
23
+ catch {
24
+ // A missing or invalid config uses the daily default.
25
+ }
26
+ return 1;
27
+ }
5
28
  export function getCloudServerConfigPath() {
6
29
  return join(MEARL_CONFIG_DIR, 'cloud-server.json');
7
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/client",
3
- "version": "2.17.1",
3
+ "version": "2.19.0",
4
4
  "description": "Unified Mearl SDK & CLI for local and remote browsers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -59,8 +59,8 @@
59
59
  "commander": "^15.0.0",
60
60
  "fastest-levenshtein": "^1.0.16",
61
61
  "ws": "^8.18.0",
62
- "@mearl/cloud-types": "2.17.1",
63
- "@mearl/provider": "2.17.1"
62
+ "@mearl/provider": "2.19.0",
63
+ "@mearl/cloud-types": "2.19.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/node": "^24.9.1",