@mearl/client 2.20.5 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -2
- package/dist/cli.js +10 -5
- package/dist/commands.d.ts +14 -5
- package/dist/commands.js +21 -2
- package/dist/generated-app-profile-definition.js +20 -10
- package/dist/generated-app-profile-runtime-types.d.ts +4 -0
- package/dist/generated-browser-action-protocol.d.ts +11 -1
- package/dist/generated-browser-action-protocol.js +3 -0
- package/dist/generated-plugin-diagnostics.d.ts +110 -0
- package/dist/generated-plugin-diagnostics.js +1 -0
- package/dist/generated-plugin-execution.d.ts +27 -0
- package/dist/generated-plugin-execution.js +1 -0
- package/dist/generated-plugin-models.d.ts +64 -0
- package/dist/generated-plugin-models.js +1 -0
- package/dist/generated-plugin-pages.d.ts +76 -0
- package/dist/generated-plugin-pages.js +59 -0
- package/dist/generated-plugin-recovery.d.ts +32 -0
- package/dist/generated-plugin-recovery.js +1 -0
- package/dist/generated-plugin-state.d.ts +50 -0
- package/dist/generated-plugin-state.js +1 -0
- package/dist/generated-plugin-tasks.d.ts +74 -0
- package/dist/generated-plugin-tasks.js +7 -0
- package/dist/generated-toolbox-icon.d.ts +10 -0
- package/dist/generated-toolbox-icon.js +37 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +6 -0
- package/dist/plugin-task-host.d.ts +1 -0
- package/dist/plugin-task-host.js +14 -0
- package/dist/plugin-worker.d.ts +1 -0
- package/dist/plugin-worker.js +17 -0
- package/dist/plugin.d.ts +57 -4
- package/dist/plugin.js +296 -15
- package/dist/pluginCliOutput.d.ts +2 -0
- package/dist/pluginCliOutput.js +7 -0
- package/dist/pluginCommandWorker.d.ts +2 -0
- package/dist/pluginCommandWorker.js +174 -0
- package/dist/pluginDiagnostics.d.ts +26 -0
- package/dist/pluginDiagnostics.js +309 -0
- package/dist/pluginExecution.d.ts +21 -0
- package/dist/pluginExecution.js +43 -0
- package/dist/pluginExecutor.d.ts +29 -0
- package/dist/pluginExecutor.js +223 -0
- package/dist/pluginExecutorProtocol.d.ts +55 -0
- package/dist/pluginExecutorProtocol.js +47 -0
- package/dist/pluginExecutorSupervisor.d.ts +4 -0
- package/dist/pluginExecutorSupervisor.js +63 -0
- package/dist/pluginHost.d.ts +20 -0
- package/dist/pluginHost.js +53 -0
- package/dist/pluginHostConnection.d.ts +35 -0
- package/dist/pluginHostConnection.js +257 -0
- package/dist/pluginHostFiles.d.ts +1 -0
- package/dist/pluginHostFiles.js +21 -0
- package/dist/pluginHostPaths.d.ts +11 -0
- package/dist/pluginHostPaths.js +22 -0
- package/dist/pluginHostServer.d.ts +7 -0
- package/dist/pluginHostServer.js +252 -0
- package/dist/pluginModelCatalog.d.ts +14 -0
- package/dist/pluginModelCatalog.js +51 -0
- package/dist/pluginModelDefinition.d.ts +6 -0
- package/dist/pluginModelDefinition.js +75 -0
- package/dist/pluginModelSessionWorker.d.ts +23 -0
- package/dist/pluginModelSessionWorker.js +60 -0
- package/dist/pluginModelSessions.d.ts +16 -0
- package/dist/pluginModelSessions.js +234 -0
- package/dist/pluginModels.d.ts +17 -0
- package/dist/pluginModels.js +47 -0
- package/dist/pluginPageBundle.js +74 -17
- package/dist/pluginPageDefinition.d.ts +3 -0
- package/dist/pluginPageDefinition.js +62 -0
- package/dist/pluginPageManager.d.ts +57 -0
- package/dist/pluginPageManager.js +877 -0
- package/dist/pluginPages.d.ts +21 -0
- package/dist/pluginPages.js +46 -0
- package/dist/pluginProcessTree.d.ts +2 -0
- package/dist/pluginProcessTree.js +31 -0
- package/dist/pluginRecoveryDefinition.d.ts +3 -0
- package/dist/pluginRecoveryDefinition.js +56 -0
- package/dist/pluginRegistry.d.ts +2 -3
- package/dist/pluginRegistry.js +14 -13
- package/dist/pluginRevision.d.ts +14 -0
- package/dist/pluginRevision.js +227 -0
- package/dist/pluginRuntimeCleanup.d.ts +2 -0
- package/dist/pluginRuntimeCleanup.js +55 -0
- package/dist/pluginState.d.ts +26 -0
- package/dist/pluginState.js +28 -0
- package/dist/pluginStateDefinition.d.ts +4 -0
- package/dist/pluginStateDefinition.js +68 -0
- package/dist/pluginStateManager.d.ts +24 -0
- package/dist/pluginStateManager.js +132 -0
- package/dist/pluginStateStore.d.ts +57 -0
- package/dist/pluginStateStore.js +396 -0
- package/dist/pluginTaskManager.d.ts +92 -0
- package/dist/pluginTaskManager.js +1080 -0
- package/dist/pluginTaskProtocol.d.ts +47 -0
- package/dist/pluginTaskProtocol.js +1 -0
- package/dist/pluginTaskStore.d.ts +53 -0
- package/dist/pluginTaskStore.js +319 -0
- package/dist/pluginTaskWire.d.ts +4 -0
- package/dist/pluginTaskWire.js +42 -0
- package/dist/pluginTasks.d.ts +51 -0
- package/dist/pluginTasks.js +181 -0
- package/dist/pythonPlugin.d.ts +13 -2
- package/dist/pythonPlugin.js +235 -30
- package/dist/socket.js +5 -2
- package/dist/unified.d.ts +1 -0
- package/dist/unified.js +81 -0
- package/package.json +7 -3
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { fork } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { processIdentity } from '@mearl/daemon-core';
|
|
5
|
+
import { pluginExecutionError } from './pluginExecutorProtocol.js';
|
|
6
|
+
import { stopPluginProcessTree } from './pluginProcessTree.js';
|
|
7
|
+
import { validatePluginModelResult } from './pluginModelDefinition.js';
|
|
8
|
+
/** Opt-in conversations retain a supervised process and thread within one execution owner. */
|
|
9
|
+
export class PluginModelSessions {
|
|
10
|
+
idleMs;
|
|
11
|
+
limit;
|
|
12
|
+
workerPath;
|
|
13
|
+
sessions = new Map();
|
|
14
|
+
closing = new Set();
|
|
15
|
+
closed = false;
|
|
16
|
+
constructor(idleMs = 5 * 60_000, limit = 2, workerPath = fileURLToPath(new URL('./pluginModelSessionWorker.js', import.meta.url))) {
|
|
17
|
+
this.idleMs = idleMs;
|
|
18
|
+
this.limit = limit;
|
|
19
|
+
this.workerPath = workerPath;
|
|
20
|
+
}
|
|
21
|
+
execute(owner, ...args) {
|
|
22
|
+
const [plugin, , context, options = {}] = args;
|
|
23
|
+
const request = options.model;
|
|
24
|
+
const configuration = Object.fromEntries(Object.entries(request)
|
|
25
|
+
.filter(([key]) => !['prompt', 'outputSchema'].includes(key))
|
|
26
|
+
.sort(([a], [b]) => a.localeCompare(b)));
|
|
27
|
+
const key = createHash('sha256')
|
|
28
|
+
.update(JSON.stringify([
|
|
29
|
+
owner,
|
|
30
|
+
plugin,
|
|
31
|
+
context.invocation?.installationId,
|
|
32
|
+
context.invocation?.revision,
|
|
33
|
+
options.cwd,
|
|
34
|
+
Object.entries(options.environment ?? {}).sort(([a], [b]) => a.localeCompare(b)),
|
|
35
|
+
configuration,
|
|
36
|
+
]))
|
|
37
|
+
.digest('hex');
|
|
38
|
+
options.signal?.throwIfAborted();
|
|
39
|
+
if (this.closed)
|
|
40
|
+
return Promise.reject(new Error('Model conversations are closed'));
|
|
41
|
+
let session = this.sessions.get(key);
|
|
42
|
+
if (!session || session.closed) {
|
|
43
|
+
if (this.sessions.size >= this.limit) {
|
|
44
|
+
const idle = [...this.sessions.values()].find(value => !value.queued);
|
|
45
|
+
if (!idle)
|
|
46
|
+
return Promise.reject(new Error('Model conversation limit reached'));
|
|
47
|
+
void this.remove(idle).catch(error => console.error('Model conversation cleanup failed:', error));
|
|
48
|
+
}
|
|
49
|
+
const child = fork(this.workerPath, [], {
|
|
50
|
+
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
51
|
+
serialization: 'advanced',
|
|
52
|
+
execArgv: [],
|
|
53
|
+
detached: process.platform !== 'win32',
|
|
54
|
+
cwd: options.cwd,
|
|
55
|
+
env: options.environment,
|
|
56
|
+
});
|
|
57
|
+
// The supervisor remains responsible for process-tree cleanup if its host disconnects.
|
|
58
|
+
const exited = new Promise(resolve => child.once('close', () => resolve()));
|
|
59
|
+
session = { owner, child, tail: Promise.resolve(), queued: 0, closed: false, exited };
|
|
60
|
+
const current = session;
|
|
61
|
+
child.on('error', () => {
|
|
62
|
+
void this.remove(current).catch(error => console.error('Model conversation cleanup failed:', error));
|
|
63
|
+
});
|
|
64
|
+
child.once('exit', () => {
|
|
65
|
+
void this.remove(current).catch(error => console.error('Model conversation cleanup failed:', error));
|
|
66
|
+
});
|
|
67
|
+
this.sessions.set(key, session);
|
|
68
|
+
}
|
|
69
|
+
this.sessions.delete(key);
|
|
70
|
+
this.sessions.set(key, session);
|
|
71
|
+
clearTimeout(session.timer);
|
|
72
|
+
const current = session;
|
|
73
|
+
current.queued++;
|
|
74
|
+
let started = false;
|
|
75
|
+
const operation = current.tail
|
|
76
|
+
.catch(() => { })
|
|
77
|
+
.then(async () => {
|
|
78
|
+
options.signal?.throwIfAborted();
|
|
79
|
+
if (current.closed)
|
|
80
|
+
throw new Error('Model conversation has ended');
|
|
81
|
+
started = true;
|
|
82
|
+
return this.run(current, args);
|
|
83
|
+
});
|
|
84
|
+
current.tail = operation.finally(() => {
|
|
85
|
+
current.queued--;
|
|
86
|
+
if (!current.queued && !current.closed) {
|
|
87
|
+
current.timer = setTimeout(() => {
|
|
88
|
+
void this.remove(current).catch(error => console.error('Model conversation cleanup failed:', error));
|
|
89
|
+
}, this.idleMs);
|
|
90
|
+
current.timer.unref();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
void current.tail.catch(() => { });
|
|
94
|
+
if (!options.signal)
|
|
95
|
+
return operation;
|
|
96
|
+
const signal = options.signal;
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const abort = () => {
|
|
99
|
+
if (!started)
|
|
100
|
+
reject(Object.assign(new Error('Model execution cancelled'), {
|
|
101
|
+
code: 'PLUGIN_EXECUTION_CANCELLED',
|
|
102
|
+
}));
|
|
103
|
+
};
|
|
104
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
105
|
+
if (signal.aborted)
|
|
106
|
+
abort();
|
|
107
|
+
void operation
|
|
108
|
+
.then(resolve, reject)
|
|
109
|
+
.finally(() => signal.removeEventListener('abort', abort));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
run(session, args) {
|
|
113
|
+
const options = args[3], request = options.model, child = session.child;
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
let finished = false, stderr = '';
|
|
116
|
+
let timer;
|
|
117
|
+
const finish = async (failure, result) => {
|
|
118
|
+
if (finished)
|
|
119
|
+
return;
|
|
120
|
+
finished = true;
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
options.signal?.removeEventListener('abort', abort);
|
|
123
|
+
child.removeListener('message', message);
|
|
124
|
+
child.removeListener('error', failed);
|
|
125
|
+
child.removeListener('exit', exited);
|
|
126
|
+
child.stderr?.removeListener('data', capture);
|
|
127
|
+
try {
|
|
128
|
+
if (failure)
|
|
129
|
+
await this.remove(session);
|
|
130
|
+
options.onExecutorStopped?.();
|
|
131
|
+
if (failure)
|
|
132
|
+
reject(failure);
|
|
133
|
+
else
|
|
134
|
+
resolve(result);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
reject(error);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const failed = (error) => {
|
|
141
|
+
void finish(error);
|
|
142
|
+
};
|
|
143
|
+
const exited = () => failed(new Error(stderr || 'Model conversation executor exited'));
|
|
144
|
+
const abort = () => failed(Object.assign(new Error('Model execution cancelled'), {
|
|
145
|
+
code: 'PLUGIN_EXECUTION_CANCELLED',
|
|
146
|
+
}));
|
|
147
|
+
const capture = (chunk) => {
|
|
148
|
+
stderr = (stderr + chunk.toString()).slice(-65536);
|
|
149
|
+
};
|
|
150
|
+
const message = (event) => {
|
|
151
|
+
if (finished)
|
|
152
|
+
return;
|
|
153
|
+
try {
|
|
154
|
+
if (event.type === 'result')
|
|
155
|
+
void finish(undefined, validatePluginModelResult(request, event.result));
|
|
156
|
+
else if (event.type === 'error')
|
|
157
|
+
failed(pluginExecutionError(event.error));
|
|
158
|
+
else if (event.type === 'diagnostic')
|
|
159
|
+
options.onDiagnostic?.(event.metadata);
|
|
160
|
+
else if (event.type === 'delta')
|
|
161
|
+
options.onProgress?.({
|
|
162
|
+
message: 'model.delta',
|
|
163
|
+
data: { delta: event.delta, index: event.index, ...event.metadata, partial: true },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
void finish(error);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
child.on('message', message);
|
|
171
|
+
child.once('error', failed);
|
|
172
|
+
child.once('exit', exited);
|
|
173
|
+
child.stderr?.on('data', capture);
|
|
174
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
175
|
+
if (options.executionTimeoutMs)
|
|
176
|
+
timer = setTimeout(() => failed(Object.assign(new Error('Model execution deadline exceeded'), {
|
|
177
|
+
code: 'PLUGIN_EXECUTION_DEADLINE',
|
|
178
|
+
})), options.executionTimeoutMs);
|
|
179
|
+
try {
|
|
180
|
+
if (options.signal?.aborted) {
|
|
181
|
+
abort();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (child.pid)
|
|
185
|
+
options.onExecutorStarted?.({
|
|
186
|
+
pid: child.pid,
|
|
187
|
+
processIdentity: processIdentity(child.pid),
|
|
188
|
+
});
|
|
189
|
+
child.send({ request }, error => {
|
|
190
|
+
if (error)
|
|
191
|
+
failed(error);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
void finish(error);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
remove(session) {
|
|
200
|
+
if (session.closing)
|
|
201
|
+
return session.closing;
|
|
202
|
+
session.closed = true;
|
|
203
|
+
clearTimeout(session.timer);
|
|
204
|
+
for (const [key, value] of this.sessions)
|
|
205
|
+
if (value === session)
|
|
206
|
+
this.sessions.delete(key);
|
|
207
|
+
session.closing = (session.child.pid ? stopPluginProcessTree(session.child.pid) : Promise.resolve())
|
|
208
|
+
.then(() => {
|
|
209
|
+
let timer;
|
|
210
|
+
const timeout = new Promise((_, reject) => {
|
|
211
|
+
timer = setTimeout(() => reject(new Error('Model conversation did not exit')), 2000);
|
|
212
|
+
});
|
|
213
|
+
return Promise.race([session.exited, timeout]).finally(() => clearTimeout(timer));
|
|
214
|
+
})
|
|
215
|
+
.catch(cause => {
|
|
216
|
+
throw Object.assign(new Error('Unable to confirm model conversation cleanup', { cause }), {
|
|
217
|
+
code: 'PLUGIN_EXECUTOR_CLEANUP_FAILED',
|
|
218
|
+
details: { executionStopped: false },
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
this.closing.add(session);
|
|
222
|
+
void session.closing.finally(() => this.closing.delete(session)).catch(() => { });
|
|
223
|
+
return session.closing;
|
|
224
|
+
}
|
|
225
|
+
async release(owner) {
|
|
226
|
+
await Promise.all([...new Set([...this.sessions.values(), ...this.closing])]
|
|
227
|
+
.filter(value => value.owner === owner)
|
|
228
|
+
.map(value => this.remove(value)));
|
|
229
|
+
}
|
|
230
|
+
async close() {
|
|
231
|
+
this.closed = true;
|
|
232
|
+
await Promise.all([...new Set([...this.sessions.values(), ...this.closing])].map(value => this.remove(value)));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PluginHostClientOptions } from './pluginHostConnection.js';
|
|
2
|
+
import { PluginTaskClient } from './pluginTasks.js';
|
|
3
|
+
import type { PluginTaskStartOptions, PluginTaskWaitOptions } from './pluginTasks.js';
|
|
4
|
+
import type { PluginModelRequest, PluginModelResult, PluginModelCapability, PluginModelList } from './generated-plugin-models.js';
|
|
5
|
+
import type { PluginTaskStartResult } from './generated-plugin-tasks.js';
|
|
6
|
+
export type * from './generated-plugin-models.js';
|
|
7
|
+
export declare class PluginModelClient {
|
|
8
|
+
readonly tasks: PluginTaskClient;
|
|
9
|
+
constructor(options?: PluginHostClientOptions);
|
|
10
|
+
capabilities(): Promise<{
|
|
11
|
+
providers: PluginModelCapability[];
|
|
12
|
+
}>;
|
|
13
|
+
list(provider?: string): Promise<PluginModelList>;
|
|
14
|
+
start(plugin: string, request: PluginModelRequest, options?: PluginTaskStartOptions): Promise<PluginTaskStartResult>;
|
|
15
|
+
run(plugin: string, request: PluginModelRequest, options?: PluginTaskStartOptions & Pick<PluginTaskWaitOptions, 'signal' | 'onProgress'>): Promise<PluginModelResult>;
|
|
16
|
+
disconnect(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PluginTaskClient } from './pluginTasks.js';
|
|
2
|
+
export class PluginModelClient {
|
|
3
|
+
tasks;
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.tasks = new PluginTaskClient(options);
|
|
6
|
+
}
|
|
7
|
+
capabilities() {
|
|
8
|
+
return this.tasks.connection.request('model.capabilities');
|
|
9
|
+
}
|
|
10
|
+
list(provider = 'codex') {
|
|
11
|
+
return this.tasks.connection.request('model.list', { provider }, 20000);
|
|
12
|
+
}
|
|
13
|
+
async start(plugin, request, options = {}) {
|
|
14
|
+
const { cwd = process.cwd(), environment = process.env, executionTimeoutMs, idempotencyKey, ...routing } = options;
|
|
15
|
+
return this.tasks.connection.request('start', {
|
|
16
|
+
kind: 'model',
|
|
17
|
+
plugin,
|
|
18
|
+
command: 'model',
|
|
19
|
+
input: { request },
|
|
20
|
+
cwd,
|
|
21
|
+
environment: Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== undefined)),
|
|
22
|
+
options: routing,
|
|
23
|
+
executionTimeoutMs,
|
|
24
|
+
idempotencyKey,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async run(plugin, request, options = {}) {
|
|
28
|
+
const { signal, onProgress, ...execution } = options;
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
const started = await this.start(plugin, request, execution);
|
|
31
|
+
const cancel = () => {
|
|
32
|
+
void this.tasks.cancel(started.taskId).catch(() => { });
|
|
33
|
+
};
|
|
34
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
35
|
+
try {
|
|
36
|
+
if (signal?.aborted)
|
|
37
|
+
cancel();
|
|
38
|
+
return (await this.tasks.wait(started.taskId, { onProgress }));
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
signal?.removeEventListener('abort', cancel);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
disconnect() {
|
|
45
|
+
this.tasks.disconnect();
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/pluginPageBundle.js
CHANGED
|
@@ -29,9 +29,7 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
29
29
|
let port = null;
|
|
30
30
|
let requestSequence = 0;
|
|
31
31
|
const pending = new Map();
|
|
32
|
-
const pageEventListeners = new Map();
|
|
33
32
|
const contextListeners = new Set();
|
|
34
|
-
let subscriptionSequence = 0;
|
|
35
33
|
let resolveReady;
|
|
36
34
|
const ready = new Promise(resolve => { resolveReady = resolve; });
|
|
37
35
|
|
|
@@ -87,17 +85,24 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
87
85
|
};
|
|
88
86
|
|
|
89
87
|
const subscribe = async (source, target, listener) => {
|
|
90
|
-
if (source !== 'page' || !target ||
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
if (source !== 'page' || !target || typeof target.instanceId !== 'string' ||
|
|
89
|
+
!/^pi_[0-9a-f-]{36}$/i.test(target.instanceId) || typeof listener !== 'function') throw new Error('Invalid page event subscription');
|
|
90
|
+
let stopped = false, timer, cursor = target.cursor ?? 0;
|
|
91
|
+
const poll = async (waitMs = 0) => {
|
|
92
|
+
let delay = 0;
|
|
93
|
+
try {
|
|
94
|
+
const page = await invoke('host.page.events', { instanceId: target.instanceId, cursor, waitMs });
|
|
95
|
+
if (stopped) return;
|
|
96
|
+
const metadata = { url: page.instance.target.url, instance: page.instance, gap: page.gap };
|
|
97
|
+
if (page.gap) listener({ type: 'resync' }, metadata);
|
|
98
|
+
for (const event of page.events) { if (stopped) return; listener(event, metadata); }
|
|
99
|
+
cursor = page.nextCursor;
|
|
100
|
+
if (page.instance.state !== 'active') return;
|
|
101
|
+
} catch (error) { if (stopped) return; delay = 500; listener({ type: 'connection-error', error: { message: error.message } }); }
|
|
102
|
+
if (!stopped) timer = setTimeout(() => { void poll(25000); }, delay);
|
|
100
103
|
};
|
|
104
|
+
await poll();
|
|
105
|
+
return () => { stopped = true; clearTimeout(timer); };
|
|
101
106
|
};
|
|
102
107
|
|
|
103
108
|
const handleExternalLinkClick = event => {
|
|
@@ -404,7 +409,62 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
404
409
|
if (context) listener(context);
|
|
405
410
|
return () => contextListeners.delete(listener);
|
|
406
411
|
};
|
|
407
|
-
const
|
|
412
|
+
const readTaskResult = async taskId => {
|
|
413
|
+
const task = await invoke('host.task.get', { taskId });
|
|
414
|
+
const reference = task.result;
|
|
415
|
+
if (task.state !== 'success' || !reference || !Number.isSafeInteger(reference.bytes) ||
|
|
416
|
+
reference.bytes < 1 || reference.bytes > 64 * 1024 * 1024 ||
|
|
417
|
+
!Number.isSafeInteger(reference.chunks) || reference.chunks < 1 || reference.chunks > 1024 ||
|
|
418
|
+
!/^[a-f0-9]{64}$/.test(reference.sha256)) throw new Error('Invalid task result reference');
|
|
419
|
+
const data = new Uint8Array(reference.bytes);
|
|
420
|
+
let offset = 0;
|
|
421
|
+
for (let index = 0; index < reference.chunks; index++) {
|
|
422
|
+
const chunk = await invoke('host.task.result', { taskId, index });
|
|
423
|
+
if (chunk.taskId !== taskId || chunk.index !== index || chunk.sha256 !== reference.sha256 ||
|
|
424
|
+
chunk.bytes !== reference.bytes || chunk.chunks !== reference.chunks) throw new Error('Task result chunk identity mismatch');
|
|
425
|
+
const binary = atob(chunk.data);
|
|
426
|
+
if (offset + binary.length > data.length) throw new Error('Task result size mismatch');
|
|
427
|
+
for (let i = 0; i < binary.length; i++) data[offset++] = binary.charCodeAt(i);
|
|
428
|
+
}
|
|
429
|
+
const hash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', data)),
|
|
430
|
+
value => value.toString(16).padStart(2, '0')).join('');
|
|
431
|
+
if (offset !== reference.bytes || hash !== reference.sha256) throw new Error('Task result integrity check failed');
|
|
432
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(data)).value;
|
|
433
|
+
};
|
|
434
|
+
const waitTask = async (taskId, options = {}) => {
|
|
435
|
+
if (options.timeoutMs !== undefined && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error('Invalid task wait timeout');
|
|
436
|
+
const deadline = options.timeoutMs === undefined ? Infinity : Date.now() + options.timeoutMs;
|
|
437
|
+
let cursor = 0, lastSequence = -1;
|
|
438
|
+
for (;;) {
|
|
439
|
+
options.signal?.throwIfAborted();
|
|
440
|
+
const remaining = deadline - Date.now();
|
|
441
|
+
if (remaining <= 0) throw Object.assign(new Error('Waiting for the task timed out; execution continues'), { code: 'PLUGIN_TASK_WAIT_TIMEOUT' });
|
|
442
|
+
let page;
|
|
443
|
+
try {
|
|
444
|
+
page = await invoke('host.task.events', { taskId, cursor, waitMs: Math.min(1000, remaining) });
|
|
445
|
+
options.onConnectionChange?.(false);
|
|
446
|
+
} catch (error) { options.onConnectionChange?.(true); throw error; }
|
|
447
|
+
options.signal?.throwIfAborted();
|
|
448
|
+
const task = page.task;
|
|
449
|
+
cursor = page.nextCursor;
|
|
450
|
+
if (task.progress && task.sequence !== lastSequence) options.onProgress?.(task.progress, task);
|
|
451
|
+
lastSequence = task.sequence;
|
|
452
|
+
if (task.state === 'success') return readTaskResult(taskId);
|
|
453
|
+
if (!['queued', 'running', 'cancelling'].includes(task.state))
|
|
454
|
+
throw Object.assign(new Error(task.error?.message || 'Plugin task ended with ' + task.state), { code: task.error?.code });
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
const listTasks = async (options = {}) => {
|
|
458
|
+
const tasks = [];
|
|
459
|
+
let cursor;
|
|
460
|
+
do {
|
|
461
|
+
const page = await invoke('host.task.list', { activeOnly: options.activeOnly !== false, ...(cursor ? { cursor } : {}) });
|
|
462
|
+
tasks.push(...page.tasks);
|
|
463
|
+
cursor = page.nextCursor;
|
|
464
|
+
} while (cursor);
|
|
465
|
+
return { tasks };
|
|
466
|
+
};
|
|
467
|
+
const api = { invoke, subscribe, readTaskResult, waitTask, listTasks, onContextChange, ready: async () => { await ready; return context; }, runAgentTask, setAgentGuides, setAgentPanelVisible, setQueryParam };
|
|
408
468
|
const ui = globalThis.__mearlToolboxUi;
|
|
409
469
|
if (ui !== undefined) {
|
|
410
470
|
if (!ui || typeof ui !== 'object' || ui.runtime !== 'preact') {
|
|
@@ -448,10 +508,7 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
448
508
|
port = nextPort;
|
|
449
509
|
port.onmessage = responseEvent => {
|
|
450
510
|
const response = responseEvent.data;
|
|
451
|
-
|
|
452
|
-
pageEventListeners.get(response.subscriptionId)?.(response.event, response.metadata);
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
511
|
+
|
|
455
512
|
if (response?.type === 'context') {
|
|
456
513
|
applyContext(response.context);
|
|
457
514
|
return;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PluginPageRuntime } from './generated-plugin-pages.js';
|
|
2
|
+
export declare function normalizePluginPageRuntime(value: unknown): PluginPageRuntime | undefined;
|
|
3
|
+
export declare function compilePluginPageSchema(schema: Record<string, unknown>): import("ajv/dist/core.js").AnyValidateFunction<unknown>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { parse } from 'acorn';
|
|
2
|
+
import { Ajv } from 'ajv';
|
|
3
|
+
import { isPluginPageMatchPattern } from './generated-plugin-pages.js';
|
|
4
|
+
const validators = new WeakMap();
|
|
5
|
+
export function normalizePluginPageRuntime(value) {
|
|
6
|
+
if (value === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
const runtime = value;
|
|
9
|
+
if (!runtime ||
|
|
10
|
+
runtime.version !== 1 ||
|
|
11
|
+
!Array.isArray(runtime.matches) ||
|
|
12
|
+
!runtime.matches.length ||
|
|
13
|
+
runtime.matches.length > 64 ||
|
|
14
|
+
!runtime.matches.every(isPluginPageMatchPattern) ||
|
|
15
|
+
typeof runtime.code !== 'string' ||
|
|
16
|
+
Buffer.byteLength(runtime.code) > 384 * 1024 ||
|
|
17
|
+
Buffer.byteLength(JSON.stringify(runtime)) > 448 * 1024)
|
|
18
|
+
throw new Error('Invalid plugin pageRuntime declaration');
|
|
19
|
+
const program = parse(`(${runtime.code}\n)`, { ecmaVersion: 'latest' });
|
|
20
|
+
const statement = program.body[0];
|
|
21
|
+
if (program.body.length !== 1 ||
|
|
22
|
+
statement.type !== 'ExpressionStatement' ||
|
|
23
|
+
!['ArrowFunctionExpression', 'FunctionExpression'].includes(statement.expression?.type ?? ''))
|
|
24
|
+
throw new Error('pageRuntime.code must be a self-contained function expression');
|
|
25
|
+
if (runtime.events !== undefined &&
|
|
26
|
+
(!runtime.events ||
|
|
27
|
+
typeof runtime.events !== 'object' ||
|
|
28
|
+
Array.isArray(runtime.events) ||
|
|
29
|
+
Object.keys(runtime.events).length > 32))
|
|
30
|
+
throw new Error('Invalid page runtime events');
|
|
31
|
+
for (const [name, event] of Object.entries(runtime.events ?? {})) {
|
|
32
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(name) ||
|
|
33
|
+
!event ||
|
|
34
|
+
!/^[a-z][a-z0-9-]*$/.test(event.command) ||
|
|
35
|
+
!event.schema ||
|
|
36
|
+
typeof event.schema !== 'object' ||
|
|
37
|
+
Array.isArray(event.schema) ||
|
|
38
|
+
(event.policy !== undefined && !['serial', 'latest', 'parallel'].includes(event.policy)) ||
|
|
39
|
+
(event.concurrency !== undefined &&
|
|
40
|
+
(!Number.isSafeInteger(event.concurrency) ||
|
|
41
|
+
event.concurrency < 1 ||
|
|
42
|
+
event.concurrency > 8)) ||
|
|
43
|
+
(event.maxPending !== undefined &&
|
|
44
|
+
(!Number.isSafeInteger(event.maxPending) ||
|
|
45
|
+
event.maxPending < 1 ||
|
|
46
|
+
event.maxPending > 64)) ||
|
|
47
|
+
event.schema.$async)
|
|
48
|
+
throw new Error(`Invalid page event binding: ${name}`);
|
|
49
|
+
compilePluginPageSchema(event.schema);
|
|
50
|
+
}
|
|
51
|
+
if (runtime.initialize !== undefined && !/^[a-z][a-z0-9-]*$/.test(runtime.initialize))
|
|
52
|
+
throw new Error('Invalid page runtime initialize command');
|
|
53
|
+
return JSON.parse(JSON.stringify(runtime));
|
|
54
|
+
}
|
|
55
|
+
export function compilePluginPageSchema(schema) {
|
|
56
|
+
let validate = validators.get(schema);
|
|
57
|
+
if (!validate) {
|
|
58
|
+
validate = new Ajv({ strict: true, allErrors: false }).compile(schema);
|
|
59
|
+
validators.set(schema, validate);
|
|
60
|
+
}
|
|
61
|
+
return validate;
|
|
62
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { PluginDiagnosticPage } from './generated-plugin-diagnostics.js';
|
|
2
|
+
import type { PluginStateManager } from './pluginStateManager.js';
|
|
3
|
+
import type { PluginInvocationIdentity } from './generated-plugin-execution.js';
|
|
4
|
+
import type { PluginRevision } from './pluginRevision.js';
|
|
5
|
+
import type { PluginOptions } from './plugin.js';
|
|
6
|
+
import type { PluginPageTarget } from './generated-plugin-pages.js';
|
|
7
|
+
import type { PluginTaskBridge, PluginTaskManager } from './pluginTaskManager.js';
|
|
8
|
+
interface Execution {
|
|
9
|
+
cwd: string;
|
|
10
|
+
environment: Record<string, string>;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
revision?: PluginRevision;
|
|
13
|
+
expectedTarget?: PluginPageTarget;
|
|
14
|
+
parentIdentity?: PluginInvocationIdentity;
|
|
15
|
+
}
|
|
16
|
+
/** The browser owns document identity; this service owns revisions and event execution. */
|
|
17
|
+
export declare class PluginPageManager {
|
|
18
|
+
private readonly tasks;
|
|
19
|
+
private readonly states;
|
|
20
|
+
private readonly releaseModels?;
|
|
21
|
+
private readonly instances;
|
|
22
|
+
private readonly changes;
|
|
23
|
+
private readonly heartbeat;
|
|
24
|
+
private readonly reconciling;
|
|
25
|
+
private closed;
|
|
26
|
+
private readonly describing;
|
|
27
|
+
constructor(tasks: PluginTaskManager, states: PluginStateManager, releaseModels?: ((owner: string) => Promise<void>) | undefined);
|
|
28
|
+
resolveStateTarget(plugin: string, instanceId: unknown, bridge: PluginTaskBridge, options: PluginOptions): Promise<{
|
|
29
|
+
target: {
|
|
30
|
+
browserSessionId: string;
|
|
31
|
+
tabId: number;
|
|
32
|
+
frameId: string | number;
|
|
33
|
+
documentId: string;
|
|
34
|
+
url: string;
|
|
35
|
+
};
|
|
36
|
+
assertActive: () => void;
|
|
37
|
+
}>;
|
|
38
|
+
diagnosticSnapshot(): PluginDiagnosticPage[];
|
|
39
|
+
private reconcile;
|
|
40
|
+
private bounded;
|
|
41
|
+
private invoke;
|
|
42
|
+
private unavailable;
|
|
43
|
+
private validate;
|
|
44
|
+
private own;
|
|
45
|
+
private bind;
|
|
46
|
+
private append;
|
|
47
|
+
private waitForChange;
|
|
48
|
+
private invalidate;
|
|
49
|
+
call(plugin: string, input: Record<string, unknown>, bridge: PluginTaskBridge, options?: PluginOptions, execution?: Execution): Promise<any>;
|
|
50
|
+
receive(plugin: string, input: Record<string, unknown>, bridge: PluginTaskBridge, options?: PluginOptions): Promise<unknown>;
|
|
51
|
+
private drain;
|
|
52
|
+
private execute;
|
|
53
|
+
private start;
|
|
54
|
+
private wait;
|
|
55
|
+
close(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export {};
|