@mearl/client 3.0.0 → 3.0.2
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.en.md +4 -4
- package/README.md +4 -4
- package/dist/plugins/officialPluginCatalog.d.ts +2 -0
- package/dist/plugins/officialPluginCatalog.js +68 -16
- package/dist/plugins/sdk/plugin.d.ts +2 -2
- package/dist/plugins/sdk/plugin.js +16 -6
- package/package.json +6 -6
- package/dist/pluginCliOutput.d.ts +0 -2
- package/dist/pluginCliOutput.js +0 -7
package/README.en.md
CHANGED
|
@@ -15,9 +15,9 @@ Use type-safe named methods such as `getRequests` and `sendRequest`, or the flex
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm install @mearl/client
|
|
18
|
+
npm install @mearl/client
|
|
19
19
|
# Or
|
|
20
|
-
pnpm add @mearl/client
|
|
20
|
+
pnpm add @mearl/client
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## SDK usage
|
|
@@ -66,7 +66,7 @@ mearl check
|
|
|
66
66
|
mearl check --local --json
|
|
67
67
|
|
|
68
68
|
# Update the local environment
|
|
69
|
-
npx @mearl/setup
|
|
69
|
+
npx @mearl/setup update --local
|
|
70
70
|
|
|
71
71
|
# Show action help
|
|
72
72
|
mearl <action> --help
|
|
@@ -248,7 +248,7 @@ Mearl aggregates local and configured cloud-server browsers by default, routing
|
|
|
248
248
|
latest client, native-host, cloud-connector, and extension releases. Release queries wait about 1.5 seconds at most;
|
|
249
249
|
unreachable sources report unconfirmed updates without affecting browser checks. `--json` returns machine-readable results,
|
|
250
250
|
including updates when combined with `--updates`. Available updates show appropriate commands: client/native-host use
|
|
251
|
-
`npx @mearl/setup
|
|
251
|
+
`npx @mearl/setup update`; setup-managed extensions use `npx @mearl/setup extension`.
|
|
252
252
|
|
|
253
253
|
By default, globally installed `mearl` launches an independent background updater after the first use each day to query releases and compare
|
|
254
254
|
installed client, native-host, cloud-server, and MCP server versions. When needed, it invokes
|
package/README.md
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
## 安装
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm install @mearl/client
|
|
18
|
+
npm install @mearl/client
|
|
19
19
|
# 或
|
|
20
|
-
pnpm add @mearl/client
|
|
20
|
+
pnpm add @mearl/client
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## SDK 使用
|
|
@@ -66,7 +66,7 @@ mearl check
|
|
|
66
66
|
mearl check --local --json
|
|
67
67
|
|
|
68
68
|
# 更新本地环境
|
|
69
|
-
npx @mearl/setup
|
|
69
|
+
npx @mearl/setup update --local
|
|
70
70
|
|
|
71
71
|
# 查看某个 action 的帮助
|
|
72
72
|
mearl <action> --help
|
|
@@ -248,7 +248,7 @@ Mearl 默认聚合当前机器和已配置 cloud-server 下的浏览器,并通
|
|
|
248
248
|
client、native-host、cloud-connector 和扩展的最新发布版本。发布版本查询最长等待约 1.5 秒,
|
|
249
249
|
无法访问发布源时会提示无法确认更新,不影响浏览器可用性检查。`--json` 输出机器可读的检查结果,
|
|
250
250
|
与 `--updates` 同用时包含更新状态。发现更新时会提示适用的命令:client/native-host 使用
|
|
251
|
-
`npx @mearl/setup
|
|
251
|
+
`npx @mearl/setup update`,setup 管理的扩展使用 `npx @mearl/setup extension`。
|
|
252
252
|
|
|
253
253
|
全局安装的 `mearl` CLI 默认在每天首次使用后启动独立后台进程,查询最新发布版本,并比较
|
|
254
254
|
client、native-host、cloud-server 和 MCP server 的已安装版本。发现更新时调用
|
|
@@ -21,5 +21,7 @@ export declare function validateOfficialPluginCatalog(value: unknown): PluginCat
|
|
|
21
21
|
export declare function readOfficialPluginCatalog(): PluginCatalog | undefined;
|
|
22
22
|
/** Installation hosts persist only the catalog selected by an approved preset. */
|
|
23
23
|
export declare function replaceOfficialPluginCatalog(revision: string, plugins: Record<string, OfficialPluginDefinition>, selection?: PluginCatalogSelection): void;
|
|
24
|
+
/** Remove only registrations owned by the catalog for the exact active selection. */
|
|
25
|
+
export declare function uninstallOfficialPluginCatalog(selection: PluginCatalogSelection): void;
|
|
24
26
|
export declare function findOfficialPlugin(name: string): OfficialPluginDefinition | undefined;
|
|
25
27
|
export declare function listOfficialPluginNames(setup?: string): string[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { isAbsolute, join } from 'node:path';
|
|
4
|
-
import { assertPluginName, pluginDirectory } from './pluginRegistry.js';
|
|
4
|
+
import { assertPluginName, findPluginRegistration, pluginDirectory, unregisterPlugin, } from './pluginRegistry.js';
|
|
5
5
|
export const OFFICIAL_PLUGIN_MANAGER = '@mearl/setup:official';
|
|
6
6
|
export const OFFICIAL_PLUGINS = {};
|
|
7
7
|
const MAX_CATALOG_BYTES = 256 * 1024;
|
|
@@ -10,12 +10,27 @@ export function validateOfficialPluginCatalog(value) {
|
|
|
10
10
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
11
11
|
throw new Error('Invalid managed plugin catalog');
|
|
12
12
|
const input = value;
|
|
13
|
-
if (input.schemaVersion !== 1 ||
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
if (input.schemaVersion !== 1 ||
|
|
14
|
+
typeof input.revision !== 'string' ||
|
|
15
|
+
!/^[a-f0-9]{64}$/.test(input.revision) ||
|
|
16
|
+
!input.plugins ||
|
|
17
|
+
typeof input.plugins !== 'object' ||
|
|
18
|
+
Array.isArray(input.plugins) ||
|
|
19
|
+
Object.keys(input).some(key => !['schemaVersion', 'revision', 'plugins', 'selection'].includes(key)) ||
|
|
20
|
+
Object.keys(input.plugins).length > 200)
|
|
16
21
|
throw new Error('Invalid managed plugin catalog');
|
|
17
22
|
const selection = input.selection;
|
|
18
|
-
if (selection !== undefined &&
|
|
23
|
+
if (selection !== undefined &&
|
|
24
|
+
(!selection ||
|
|
25
|
+
typeof selection !== 'object' ||
|
|
26
|
+
Array.isArray(selection) ||
|
|
27
|
+
Object.keys(selection).some(key => !['path', 'sha256'].includes(key)) ||
|
|
28
|
+
typeof selection.path !== 'string' ||
|
|
29
|
+
!isAbsolute(selection.path) ||
|
|
30
|
+
selection.path.length > 4096 ||
|
|
31
|
+
/[\0\r\n]/.test(selection.path) ||
|
|
32
|
+
typeof selection.sha256 !== 'string' ||
|
|
33
|
+
!/^[a-f0-9]{64}$/.test(selection.sha256)))
|
|
19
34
|
throw new Error('Invalid plugin catalog selection');
|
|
20
35
|
const plugins = {};
|
|
21
36
|
for (const [name, value] of Object.entries(input.plugins)) {
|
|
@@ -24,15 +39,30 @@ export function validateOfficialPluginCatalog(value) {
|
|
|
24
39
|
throw new Error('Invalid managed plugin declaration');
|
|
25
40
|
const item = value;
|
|
26
41
|
if (Object.keys(item).some(key => !['description', 'installArgs', 'setup', 'skill'].includes(key)) ||
|
|
27
|
-
typeof item.description !== 'string' ||
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
typeof item.description !== 'string' ||
|
|
43
|
+
item.description.length > 1000 ||
|
|
44
|
+
typeof item.skill !== 'string' ||
|
|
45
|
+
!/^[a-z][a-z0-9-]*$/.test(item.skill) ||
|
|
46
|
+
!Array.isArray(item.installArgs) ||
|
|
47
|
+
item.installArgs.length === 0 ||
|
|
48
|
+
item.installArgs.length > 32 ||
|
|
30
49
|
item.installArgs.some(arg => typeof arg !== 'string' || arg.length > 2048 || /[\0\r\n]/.test(arg)) ||
|
|
31
|
-
(item.setup !== undefined &&
|
|
50
|
+
(item.setup !== undefined &&
|
|
51
|
+
(typeof item.setup !== 'string' || !/^[a-z][a-z0-9-]*$/.test(item.setup))))
|
|
32
52
|
throw new Error('Invalid managed plugin declaration');
|
|
33
|
-
plugins[name] = {
|
|
53
|
+
plugins[name] = {
|
|
54
|
+
description: item.description,
|
|
55
|
+
skill: item.skill,
|
|
56
|
+
installArgs: [...item.installArgs],
|
|
57
|
+
...(item.setup !== undefined && { setup: item.setup }),
|
|
58
|
+
};
|
|
34
59
|
}
|
|
35
|
-
const catalog = {
|
|
60
|
+
const catalog = {
|
|
61
|
+
schemaVersion: 1,
|
|
62
|
+
revision: input.revision,
|
|
63
|
+
plugins,
|
|
64
|
+
...(selection ? { selection: { path: selection.path, sha256: selection.sha256 } } : {}),
|
|
65
|
+
};
|
|
36
66
|
if (Buffer.byteLength(JSON.stringify(catalog)) > MAX_CATALOG_BYTES)
|
|
37
67
|
throw new Error('Managed plugin catalog exceeds 256 KiB');
|
|
38
68
|
return catalog;
|
|
@@ -55,7 +85,8 @@ function selectionMatches(selection) {
|
|
|
55
85
|
break;
|
|
56
86
|
size += count;
|
|
57
87
|
}
|
|
58
|
-
return size === stat.size &&
|
|
88
|
+
return (size === stat.size &&
|
|
89
|
+
createHash('sha256').update(bytes.subarray(0, size)).digest('hex') === selection.sha256);
|
|
59
90
|
}
|
|
60
91
|
finally {
|
|
61
92
|
closeSync(fd);
|
|
@@ -84,7 +115,12 @@ export function readOfficialPluginCatalog() {
|
|
|
84
115
|
}
|
|
85
116
|
/** Installation hosts persist only the catalog selected by an approved preset. */
|
|
86
117
|
export function replaceOfficialPluginCatalog(revision, plugins, selection) {
|
|
87
|
-
const catalog = validateOfficialPluginCatalog({
|
|
118
|
+
const catalog = validateOfficialPluginCatalog({
|
|
119
|
+
schemaVersion: 1,
|
|
120
|
+
revision,
|
|
121
|
+
plugins,
|
|
122
|
+
...(selection ? { selection } : {}),
|
|
123
|
+
});
|
|
88
124
|
const content = JSON.stringify(catalog);
|
|
89
125
|
if (Buffer.byteLength(content) > MAX_CATALOG_BYTES)
|
|
90
126
|
throw new Error('Managed plugin catalog exceeds 256 KiB');
|
|
@@ -109,17 +145,33 @@ function refreshOfficialPlugins() {
|
|
|
109
145
|
try {
|
|
110
146
|
plugins = readOfficialPluginCatalog()?.plugins ?? {};
|
|
111
147
|
}
|
|
112
|
-
catch {
|
|
148
|
+
catch {
|
|
149
|
+
/* An invalid optional catalog cannot disable registered plugin or browser commands. */
|
|
150
|
+
}
|
|
113
151
|
for (const name of Object.keys(OFFICIAL_PLUGINS))
|
|
114
152
|
delete OFFICIAL_PLUGINS[name];
|
|
115
153
|
Object.assign(OFFICIAL_PLUGINS, plugins);
|
|
116
154
|
return OFFICIAL_PLUGINS;
|
|
117
155
|
}
|
|
156
|
+
/** Remove only registrations owned by the catalog for the exact active selection. */
|
|
157
|
+
export function uninstallOfficialPluginCatalog(selection) {
|
|
158
|
+
const catalog = readOfficialPluginCatalog();
|
|
159
|
+
if (catalog?.selection?.path !== selection.path || catalog.selection.sha256 !== selection.sha256)
|
|
160
|
+
return;
|
|
161
|
+
for (const name of Object.keys(catalog.plugins)) {
|
|
162
|
+
if (findPluginRegistration(name)?.managedBy === OFFICIAL_PLUGIN_MANAGER)
|
|
163
|
+
unregisterPlugin(name);
|
|
164
|
+
}
|
|
165
|
+
rmSync(catalogPath(), { force: true });
|
|
166
|
+
refreshOfficialPlugins();
|
|
167
|
+
}
|
|
118
168
|
refreshOfficialPlugins();
|
|
119
169
|
export function findOfficialPlugin(name) {
|
|
120
170
|
const plugins = refreshOfficialPlugins();
|
|
121
171
|
return Object.hasOwn(plugins, name) ? plugins[name] : undefined;
|
|
122
172
|
}
|
|
123
173
|
export function listOfficialPluginNames(setup) {
|
|
124
|
-
return Object.entries(refreshOfficialPlugins())
|
|
174
|
+
return Object.entries(refreshOfficialPlugins())
|
|
175
|
+
.filter(([, item]) => setup === undefined || item.setup === setup)
|
|
176
|
+
.map(([name]) => name);
|
|
125
177
|
}
|
|
@@ -4,7 +4,7 @@ import type { PluginStateDeclaration } from '@mearl/browser-core';
|
|
|
4
4
|
import type { PluginModelRequest, PluginModelResult } from '@mearl/browser-core';
|
|
5
5
|
import type { PluginPageRuntime } from '@mearl/browser-core';
|
|
6
6
|
import type { AppProfileContribution } from '@mearl/browser-core';
|
|
7
|
-
export type { AppProfileContribution, AppProfileDefinition
|
|
7
|
+
export type { AppProfileContribution, AppProfileDefinition } from '@mearl/browser-core';
|
|
8
8
|
export { invokePageEvalWithArgs } from '../../pageEvalArguments.js';
|
|
9
9
|
import { type OfficialPluginInstaller } from '../officialPluginInstaller.js';
|
|
10
10
|
import { type PluginToolboxContribution, type PluginRegistration } from '../pluginRegistry.js';
|
|
@@ -16,7 +16,7 @@ export { PluginTaskClient } from '../clients/pluginTasks.js';
|
|
|
16
16
|
export { cleanupPluginRuntime } from '../runtime/pluginRuntimeCleanup.js';
|
|
17
17
|
export { PluginPageClient } from '../clients/pluginPages.js';
|
|
18
18
|
export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, pluginRegistrationNeedsRefresh, listPluginNames, pluginDirectory, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, readPluginPageBundleChunk, type PluginRegistration, type PluginRegistrationOptions, type PluginToolboxAgentConfig, type PluginPageBundleRegistration, type PluginToolboxContribution, type PluginToolboxLocale, type PluginToolboxSandboxPage, } from '../pluginRegistry.js';
|
|
19
|
-
export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, readOfficialPluginCatalog, replaceOfficialPluginCatalog, validateOfficialPluginCatalog, type PluginCatalogSelection, type PluginCatalog, type OfficialPluginDefinition, type OfficialPluginName, } from '../officialPluginCatalog.js';
|
|
19
|
+
export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, readOfficialPluginCatalog, replaceOfficialPluginCatalog, uninstallOfficialPluginCatalog, validateOfficialPluginCatalog, type PluginCatalogSelection, type PluginCatalog, type OfficialPluginDefinition, type OfficialPluginName, } from '../officialPluginCatalog.js';
|
|
20
20
|
export type PluginOptions = UnifiedInvokeOptions;
|
|
21
21
|
export interface StructuredPluginProgress {
|
|
22
22
|
message: string;
|
|
@@ -19,7 +19,7 @@ export { PluginTaskClient } from '../clients/pluginTasks.js';
|
|
|
19
19
|
export { cleanupPluginRuntime } from '../runtime/pluginRuntimeCleanup.js';
|
|
20
20
|
export { PluginPageClient } from '../clients/pluginPages.js';
|
|
21
21
|
export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, pluginRegistrationNeedsRefresh, listPluginNames, pluginDirectory, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, readPluginPageBundleChunk, } from '../pluginRegistry.js';
|
|
22
|
-
export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, readOfficialPluginCatalog, replaceOfficialPluginCatalog, validateOfficialPluginCatalog, } from '../officialPluginCatalog.js';
|
|
22
|
+
export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, readOfficialPluginCatalog, replaceOfficialPluginCatalog, uninstallOfficialPluginCatalog, validateOfficialPluginCatalog, } from '../officialPluginCatalog.js';
|
|
23
23
|
function readPluginVersion(entry) {
|
|
24
24
|
try {
|
|
25
25
|
return readInstalledSkillMetadata(resolve(dirname(entry), '..'))?.version;
|
|
@@ -334,7 +334,10 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
334
334
|
}
|
|
335
335
|
if (commands.size === 0 && !appProfiles)
|
|
336
336
|
throw new Error('入口未注册任何命令');
|
|
337
|
-
if (defaultCommand !== undefined &&
|
|
337
|
+
if (defaultCommand !== undefined &&
|
|
338
|
+
(!commands.has(defaultCommand) ||
|
|
339
|
+
commands.get(defaultCommand)?.toolboxOnly ||
|
|
340
|
+
commands.get(defaultCommand)?.runtimeOnly))
|
|
338
341
|
throw new Error('defaultCommand 必须指向公开命令');
|
|
339
342
|
for (const command of Object.keys(taskRecovery?.commands ?? {})) {
|
|
340
343
|
if (!commands.has(command) ||
|
|
@@ -411,13 +414,16 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
411
414
|
if (payload !== undefined && Object.keys(namedInput).length > 0) {
|
|
412
415
|
throw new PluginError('PLUGIN_INVALID_INPUT', '插件业务选项与 --payload 调用不能混用');
|
|
413
416
|
}
|
|
414
|
-
const defaultPayload = defaultCommand &&
|
|
417
|
+
const defaultPayload = defaultCommand &&
|
|
418
|
+
toolboxPage === false &&
|
|
419
|
+
isObject(payload) &&
|
|
420
|
+
!Object.hasOwn(payload, 'command');
|
|
415
421
|
if (payload !== undefined && !defaultPayload && !isPluginInvocation(payload)) {
|
|
416
422
|
throw new PluginError('PLUGIN_INVALID_INPUT', '--payload 必须为 {"command":"...","input":{...}}');
|
|
417
423
|
}
|
|
418
424
|
const envelope = (defaultPayload ? { command: defaultCommand, input: payload } : payload);
|
|
419
425
|
const positionalOffset = defaultCommand && !commands.has(positionals[0]) && toolboxPage === false ? 0 : 1;
|
|
420
|
-
const commandName = envelope?.command ?? (positionalOffset === 0 ? defaultCommand : positionals[0] ?? '');
|
|
426
|
+
const commandName = envelope?.command ?? (positionalOffset === 0 ? defaultCommand : (positionals[0] ?? ''));
|
|
421
427
|
const hidden = toolboxPage === 'recovery' || toolboxPage === 'migration';
|
|
422
428
|
const selected = find(commandName, hidden ? false : toolboxPage);
|
|
423
429
|
const command = selected.command;
|
|
@@ -437,7 +443,9 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
437
443
|
: Object.fromEntries(command.registeredArguments
|
|
438
444
|
.map((argument, index) => [
|
|
439
445
|
argument.name(),
|
|
440
|
-
argument.variadic
|
|
446
|
+
argument.variadic
|
|
447
|
+
? positionals.slice(index + positionalOffset)
|
|
448
|
+
: positionals[index + positionalOffset],
|
|
441
449
|
])
|
|
442
450
|
.filter(([, value]) => value !== undefined));
|
|
443
451
|
for (const name of Object.keys(namedInput)) {
|
|
@@ -555,7 +563,9 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
555
563
|
.join('\n');
|
|
556
564
|
},
|
|
557
565
|
help(namespace, name) {
|
|
558
|
-
const selected = name
|
|
566
|
+
const selected = name
|
|
567
|
+
? [find(defaultCommand && !commands.has(name) ? defaultCommand : name)]
|
|
568
|
+
: publicCommands();
|
|
559
569
|
if (!selected.length) {
|
|
560
570
|
const location = toolbox
|
|
561
571
|
? `请在 Mearl ${toolbox.displayMode === 'popup' ? 'Popup' : '工具箱'} 中打开「${toolbox.title}」使用。`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/client",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Unified Mearl SDK & CLI for local and remote browsers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -68,11 +68,11 @@
|
|
|
68
68
|
"commander": "^14.0.3",
|
|
69
69
|
"fastest-levenshtein": "^1.0.16",
|
|
70
70
|
"ws": "^8.18.0",
|
|
71
|
-
"@mearl/daemon-core": "3.0.
|
|
72
|
-
"@mearl/cloud-types": "3.0.
|
|
73
|
-
"@mearl/
|
|
74
|
-
"@mearl/
|
|
75
|
-
"@mearl/
|
|
71
|
+
"@mearl/daemon-core": "3.0.2",
|
|
72
|
+
"@mearl/cloud-types": "3.0.2",
|
|
73
|
+
"@mearl/agent-runtime": "3.0.2",
|
|
74
|
+
"@mearl/provider": "3.0.2",
|
|
75
|
+
"@mearl/browser-core": "3.0.2"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@types/node": "^24.9.1",
|
package/dist/pluginCliOutput.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/** CLI host commands share the SDK error code without exporting arbitrary error metadata. */
|
|
2
|
-
export function writePluginCliError(error) {
|
|
3
|
-
const code = error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
|
|
4
|
-
? error.code
|
|
5
|
-
: 'PLUGIN_HOST_ERROR';
|
|
6
|
-
process.stderr.write(`${JSON.stringify({ error: { code, message: error instanceof Error ? error.message : String(error) } })}\n`);
|
|
7
|
-
}
|