webflow-mcp 0.3.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/.webflow/flows/_template/index.ts +14 -0
- package/.webflow/flows/_template/schema.json +11 -0
- package/.webflow/skills/README.md +3 -0
- package/README.md +215 -0
- package/dist/src/anti-block/provider-gate.d.ts +19 -0
- package/dist/src/anti-block/provider-gate.js +116 -0
- package/dist/src/cli/commands/approvals.d.ts +5 -0
- package/dist/src/cli/commands/approvals.js +69 -0
- package/dist/src/cli/commands/config.d.ts +4 -0
- package/dist/src/cli/commands/config.js +56 -0
- package/dist/src/cli/commands/create-flow.d.ts +4 -0
- package/dist/src/cli/commands/create-flow.js +27 -0
- package/dist/src/cli/commands/doctor.d.ts +19 -0
- package/dist/src/cli/commands/doctor.js +241 -0
- package/dist/src/cli/commands/fork.d.ts +4 -0
- package/dist/src/cli/commands/fork.js +19 -0
- package/dist/src/cli/commands/init.d.ts +4 -0
- package/dist/src/cli/commands/init.js +22 -0
- package/dist/src/cli/commands/login.d.ts +26 -0
- package/dist/src/cli/commands/login.js +145 -0
- package/dist/src/cli/commands/profiles.d.ts +40 -0
- package/dist/src/cli/commands/profiles.js +142 -0
- package/dist/src/cli/commands/run.d.ts +10 -0
- package/dist/src/cli/commands/run.js +13 -0
- package/dist/src/cli/commands/setup.d.ts +11 -0
- package/dist/src/cli/commands/setup.js +46 -0
- package/dist/src/cli/commands/worker.d.ts +29 -0
- package/dist/src/cli/commands/worker.js +30 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +336 -0
- package/dist/src/config/webflow-config.d.ts +35 -0
- package/dist/src/config/webflow-config.js +119 -0
- package/dist/src/errors/self-healing.d.ts +62 -0
- package/dist/src/errors/self-healing.js +77 -0
- package/dist/src/mcp/cli.d.ts +2 -0
- package/dist/src/mcp/cli.js +36 -0
- package/dist/src/mcp/relay-gateway.d.ts +24 -0
- package/dist/src/mcp/relay-gateway.js +208 -0
- package/dist/src/mcp/server.d.ts +8 -0
- package/dist/src/mcp/server.js +62 -0
- package/dist/src/mcp/tool-registry.d.ts +10 -0
- package/dist/src/mcp/tool-registry.js +366 -0
- package/dist/src/profiles/profile-manager.d.ts +33 -0
- package/dist/src/profiles/profile-manager.js +37 -0
- package/dist/src/profiles/profile-state.d.ts +31 -0
- package/dist/src/profiles/profile-state.js +122 -0
- package/dist/src/repertoire/local-repertoire.d.ts +3 -0
- package/dist/src/repertoire/local-repertoire.js +59 -0
- package/dist/src/runner/browser-session.d.ts +23 -0
- package/dist/src/runner/browser-session.js +58 -0
- package/dist/src/runner/cdp-session.d.ts +25 -0
- package/dist/src/runner/cdp-session.js +156 -0
- package/dist/src/runner/chrome.d.ts +18 -0
- package/dist/src/runner/chrome.js +56 -0
- package/dist/src/runner/flow-loader.d.ts +11 -0
- package/dist/src/runner/flow-loader.js +115 -0
- package/dist/src/runner/flow-runner.d.ts +70 -0
- package/dist/src/runner/flow-runner.js +378 -0
- package/dist/src/runner/types.d.ts +112 -0
- package/dist/src/runner/types.js +1 -0
- package/dist/src/schema/translator.d.ts +5 -0
- package/dist/src/schema/translator.js +104 -0
- package/dist/src/schema/types.d.ts +32 -0
- package/dist/src/schema/types.js +1 -0
- package/dist/src/security/flow-approvals.d.ts +34 -0
- package/dist/src/security/flow-approvals.js +324 -0
- package/dist/src/security/flow-release.d.ts +82 -0
- package/dist/src/security/flow-release.js +392 -0
- package/dist/src/security/official-flow-trust.d.ts +2 -0
- package/dist/src/security/official-flow-trust.js +16 -0
- package/dist/src/security/trusted-flow-keys.d.ts +8 -0
- package/dist/src/security/trusted-flow-keys.js +132 -0
- package/dist/src/shared/credentials.d.ts +21 -0
- package/dist/src/shared/credentials.js +99 -0
- package/dist/src/shared/errors.d.ts +6 -0
- package/dist/src/shared/errors.js +16 -0
- package/dist/src/shared/fs.d.ts +5 -0
- package/dist/src/shared/fs.js +23 -0
- package/dist/src/shared/paths.d.ts +112 -0
- package/dist/src/shared/paths.js +257 -0
- package/dist/src/shared/semaphore.d.ts +8 -0
- package/dist/src/shared/semaphore.js +23 -0
- package/dist/src/shared/taxonomy.d.ts +21 -0
- package/dist/src/shared/taxonomy.js +20 -0
- package/dist/src/worker/connection.d.ts +72 -0
- package/dist/src/worker/connection.js +427 -0
- package/dist/src/worker/lifecycle.d.ts +50 -0
- package/dist/src/worker/lifecycle.js +154 -0
- package/dist/src/worker/protocol.d.ts +188 -0
- package/dist/src/worker/protocol.js +156 -0
- package/dist/src/worker/worker-service.d.ts +112 -0
- package/dist/src/worker/worker-service.js +258 -0
- package/package.json +50 -0
- package/taxonomy.json +99 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { SelfHealingPayload } from '../errors/self-healing.js';
|
|
2
|
+
import type { CatalogEntry } from './worker-service.js';
|
|
3
|
+
import type { WebflowConfig } from '../config/webflow-config.js';
|
|
4
|
+
import type { BrowserVisibility, GlobalVisibilityPreference } from '../config/webflow-config.js';
|
|
5
|
+
import type { BrowserMode } from '../runner/types.js';
|
|
6
|
+
import type { SignedFlowRelease } from '../security/flow-release.js';
|
|
7
|
+
export declare const WORKER_PROTOCOL_VERSION = 2;
|
|
8
|
+
export declare const WORKER_VERSION = "0.3.0";
|
|
9
|
+
export declare const SUPPORTED_FLOW_RELEASE_FORMATS: readonly [1];
|
|
10
|
+
export declare const SUPPORTED_FLOW_RUNTIME_APIS: readonly [1];
|
|
11
|
+
/**
|
|
12
|
+
* Wire contract between a local worker and the (future, out-of-repo) VPS relay.
|
|
13
|
+
* RFC 0001, section 2.3. This file is the source of truth for the message shapes;
|
|
14
|
+
* the relay implementation must keep its own copy in sync until/unless the types
|
|
15
|
+
* are published as a shared package.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately NOT included in v1: a `run_progress` message. FlowModule.run()
|
|
18
|
+
* (src/runner/types.ts) has no progress-callback API, so there is nothing real to
|
|
19
|
+
* stream — adding progress messages here without a producer would be fake
|
|
20
|
+
* streaming. Add it to the flow contract first if this is ever needed.
|
|
21
|
+
*/
|
|
22
|
+
export interface HelloMessage {
|
|
23
|
+
type: 'hello';
|
|
24
|
+
workerId: string;
|
|
25
|
+
credential: string;
|
|
26
|
+
protocolVersion: typeof WORKER_PROTOCOL_VERSION;
|
|
27
|
+
workerVersion: string;
|
|
28
|
+
flowReleaseFormats: number[];
|
|
29
|
+
runtimeApiVersions: number[];
|
|
30
|
+
/**
|
|
31
|
+
* Locally-known flows only. Under the Fase 3 "everything is pushed" model (see
|
|
32
|
+
* `run.code` below) this is typically empty — the relay's own flow store is the
|
|
33
|
+
* catalog source of truth, not the worker. Kept for backward compat / diagnostics
|
|
34
|
+
* with the Fase 0-2 local-worker-catalog mode.
|
|
35
|
+
*/
|
|
36
|
+
catalog: CatalogEntry[];
|
|
37
|
+
}
|
|
38
|
+
export interface HelloAckMessage {
|
|
39
|
+
type: 'hello_ack';
|
|
40
|
+
ok: boolean;
|
|
41
|
+
userId?: string;
|
|
42
|
+
reason?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface RunMessage {
|
|
45
|
+
type: 'run';
|
|
46
|
+
jobId: string;
|
|
47
|
+
flow: string;
|
|
48
|
+
input: unknown;
|
|
49
|
+
/** Every network-originated run requires a signed immutable release artifact. */
|
|
50
|
+
release: SignedFlowRelease;
|
|
51
|
+
/** One-off execution overrides. Persisted preferences live in config_request. */
|
|
52
|
+
browserMode?: BrowserMode;
|
|
53
|
+
browserVisibility?: BrowserVisibility;
|
|
54
|
+
}
|
|
55
|
+
export interface RunResultMessage {
|
|
56
|
+
type: 'run_result';
|
|
57
|
+
jobId: string;
|
|
58
|
+
ok: boolean;
|
|
59
|
+
result?: unknown;
|
|
60
|
+
error?: SelfHealingPayload;
|
|
61
|
+
/** See FlowExecutionSuccess.visibilityNudge (src/runner/types.ts) — carried over the
|
|
62
|
+
* wire so the relay can surface it as an `askUser` hint without a second round-trip. */
|
|
63
|
+
visibilityNudge?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface PingMessage {
|
|
66
|
+
type: 'ping';
|
|
67
|
+
}
|
|
68
|
+
export interface PongMessage {
|
|
69
|
+
type: 'pong';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Config/repertoire live on the worker's own disk (never in the relay's database —
|
|
73
|
+
* same principle as login sessions/profiles). These four message types let the relay
|
|
74
|
+
* forward an agent's get/set calls to the worker that owns that state, and get a
|
|
75
|
+
* correlated reply back. Pure local disk I/O, so replies should be near-instant —
|
|
76
|
+
* see WorkerRegistry's separate short timeout for these (relay/src/worker-registry.ts),
|
|
77
|
+
* distinct from the multi-minute job timeout used for `run`.
|
|
78
|
+
*/
|
|
79
|
+
export type ConfigAction = {
|
|
80
|
+
action: 'get';
|
|
81
|
+
} | {
|
|
82
|
+
action: 'set_default_visibility';
|
|
83
|
+
value: GlobalVisibilityPreference;
|
|
84
|
+
} | {
|
|
85
|
+
action: 'set_flow_visibility';
|
|
86
|
+
flowName: string;
|
|
87
|
+
value: BrowserVisibility | null;
|
|
88
|
+
} | {
|
|
89
|
+
action: 'set_default_browser_mode';
|
|
90
|
+
value: BrowserMode | 'inherit';
|
|
91
|
+
} | {
|
|
92
|
+
action: 'set_flow_browser_mode';
|
|
93
|
+
flowName: string;
|
|
94
|
+
value: BrowserMode | null;
|
|
95
|
+
};
|
|
96
|
+
export interface ConfigRequestMessage {
|
|
97
|
+
type: 'config_request';
|
|
98
|
+
requestId: string;
|
|
99
|
+
request: ConfigAction;
|
|
100
|
+
}
|
|
101
|
+
export interface ConfigResponseMessage {
|
|
102
|
+
type: 'config_response';
|
|
103
|
+
requestId: string;
|
|
104
|
+
ok: boolean;
|
|
105
|
+
config?: WebflowConfig;
|
|
106
|
+
error?: string;
|
|
107
|
+
}
|
|
108
|
+
export type RepertoireAction = {
|
|
109
|
+
action: 'list';
|
|
110
|
+
} | {
|
|
111
|
+
action: 'add';
|
|
112
|
+
flowName: string;
|
|
113
|
+
} | {
|
|
114
|
+
action: 'remove';
|
|
115
|
+
flowName: string;
|
|
116
|
+
};
|
|
117
|
+
export interface RepertoireRequestMessage {
|
|
118
|
+
type: 'repertoire_request';
|
|
119
|
+
requestId: string;
|
|
120
|
+
request: RepertoireAction;
|
|
121
|
+
}
|
|
122
|
+
export interface RepertoireResponseMessage {
|
|
123
|
+
type: 'repertoire_response';
|
|
124
|
+
requestId: string;
|
|
125
|
+
ok: boolean;
|
|
126
|
+
flows?: string[];
|
|
127
|
+
error?: string;
|
|
128
|
+
}
|
|
129
|
+
/** Serializable mirror of ProfileStatusInfo (../profiles/profile-state.ts) — only the
|
|
130
|
+
* fields an MCP client needs, no local filesystem paths. */
|
|
131
|
+
export interface ProfileStatusWire {
|
|
132
|
+
profileId: string;
|
|
133
|
+
status: 'missing' | 'exists_unverified' | 'setup_completed' | 'verified' | 'login_required';
|
|
134
|
+
probablyValid: boolean | 'unknown';
|
|
135
|
+
loginUrl?: string;
|
|
136
|
+
setupCommand: string;
|
|
137
|
+
lastInteractiveSetupAt?: string;
|
|
138
|
+
lastSuccessfulRunAt?: string;
|
|
139
|
+
lastLoginRequiredAt?: string | null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Same disk-state principle as config/repertoire above (never the relay's database).
|
|
143
|
+
* `status` reads the last known state (fast, near-instant). `login` opens a real,
|
|
144
|
+
* visible Chrome window on this machine for the human to log in manually and returns
|
|
145
|
+
* as soon as the window is open — it does NOT wait for the user to finish (that can
|
|
146
|
+
* take minutes and must never hold up an MCP tool call). The worker keeps the
|
|
147
|
+
* per-profile lock held in the background until the window closes, so a flow run
|
|
148
|
+
* against the same profile can't start mid-login; call `status` again afterward to
|
|
149
|
+
* see whether it succeeded.
|
|
150
|
+
*/
|
|
151
|
+
export type ProfileAction = {
|
|
152
|
+
action: 'status';
|
|
153
|
+
flowName: string;
|
|
154
|
+
loginProvider?: string;
|
|
155
|
+
loginUrl?: string;
|
|
156
|
+
} | {
|
|
157
|
+
action: 'login';
|
|
158
|
+
flowName: string;
|
|
159
|
+
loginProvider?: string;
|
|
160
|
+
loginUrl?: string;
|
|
161
|
+
};
|
|
162
|
+
export interface ProfileRequestMessage {
|
|
163
|
+
type: 'profile_request';
|
|
164
|
+
requestId: string;
|
|
165
|
+
request: ProfileAction;
|
|
166
|
+
}
|
|
167
|
+
export interface ProfileResponseMessage {
|
|
168
|
+
type: 'profile_response';
|
|
169
|
+
requestId: string;
|
|
170
|
+
ok: boolean;
|
|
171
|
+
status?: ProfileStatusWire;
|
|
172
|
+
/** Only set for the `login` action. */
|
|
173
|
+
launched?: boolean;
|
|
174
|
+
error?: string;
|
|
175
|
+
}
|
|
176
|
+
/** Messages the worker sends. */
|
|
177
|
+
export type WorkerToRelayMessage = HelloMessage | RunResultMessage | PingMessage | PongMessage | ConfigResponseMessage | RepertoireResponseMessage | ProfileResponseMessage;
|
|
178
|
+
/** Messages the worker receives. */
|
|
179
|
+
export type RelayToWorkerMessage = HelloAckMessage | RunMessage | PingMessage | PongMessage | ConfigRequestMessage | RepertoireRequestMessage | ProfileRequestMessage;
|
|
180
|
+
/**
|
|
181
|
+
* Defensive parse for messages arriving over the socket. A malformed or unexpected
|
|
182
|
+
* message from a misbehaving/compromised relay must never crash the worker process
|
|
183
|
+
* that also happens to be driving the user's real, logged-in Chrome (RFC 0001,
|
|
184
|
+
* section 5: don't trust the relay blindly). Throws WebFlowError on anything that
|
|
185
|
+
* doesn't match a known shape; callers should catch and drop the message.
|
|
186
|
+
*/
|
|
187
|
+
export declare function parseRelayToWorkerMessage(raw: string): RelayToWorkerMessage;
|
|
188
|
+
export declare function serializeWorkerMessage(message: WorkerToRelayMessage): string;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { WebFlowError } from '../shared/errors.js';
|
|
2
|
+
export const WORKER_PROTOCOL_VERSION = 2;
|
|
3
|
+
export const WORKER_VERSION = '0.3.0';
|
|
4
|
+
export const SUPPORTED_FLOW_RELEASE_FORMATS = [1];
|
|
5
|
+
export const SUPPORTED_FLOW_RUNTIME_APIS = [1];
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Defensive parse for messages arriving over the socket. A malformed or unexpected
|
|
11
|
+
* message from a misbehaving/compromised relay must never crash the worker process
|
|
12
|
+
* that also happens to be driving the user's real, logged-in Chrome (RFC 0001,
|
|
13
|
+
* section 5: don't trust the relay blindly). Throws WebFlowError on anything that
|
|
14
|
+
* doesn't match a known shape; callers should catch and drop the message.
|
|
15
|
+
*/
|
|
16
|
+
export function parseRelayToWorkerMessage(raw) {
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'Message from relay is not valid JSON.');
|
|
23
|
+
}
|
|
24
|
+
if (!isRecord(parsed) || typeof parsed.type !== 'string') {
|
|
25
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'Message from relay is missing a string "type" field.');
|
|
26
|
+
}
|
|
27
|
+
switch (parsed.type) {
|
|
28
|
+
case 'hello_ack':
|
|
29
|
+
if (typeof parsed.ok !== 'boolean') {
|
|
30
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'hello_ack message must have a boolean "ok" field.');
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
type: 'hello_ack',
|
|
34
|
+
ok: parsed.ok,
|
|
35
|
+
userId: typeof parsed.userId === 'string' ? parsed.userId : undefined,
|
|
36
|
+
reason: typeof parsed.reason === 'string' ? parsed.reason : undefined
|
|
37
|
+
};
|
|
38
|
+
case 'run':
|
|
39
|
+
if (typeof parsed.jobId !== 'string' || typeof parsed.flow !== 'string') {
|
|
40
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'run message must have string "jobId" and "flow" fields.');
|
|
41
|
+
}
|
|
42
|
+
if (Object.hasOwn(parsed, 'code') || Object.hasOwn(parsed, 'schema') || Object.hasOwn(parsed, 'requiresLogin')
|
|
43
|
+
|| Object.hasOwn(parsed, 'loginProvider') || Object.hasOwn(parsed, 'loginUrl')) {
|
|
44
|
+
throw new WebFlowError('FLOW_SIGNATURE_REQUIRED', 'Unsigned remote flow fields are no longer accepted.');
|
|
45
|
+
}
|
|
46
|
+
if (!isRecord(parsed.release)) {
|
|
47
|
+
throw new WebFlowError('FLOW_SIGNATURE_REQUIRED', 'Every remote run requires a signed flow release.');
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
type: 'run',
|
|
51
|
+
jobId: parsed.jobId,
|
|
52
|
+
flow: parsed.flow,
|
|
53
|
+
input: parsed.input,
|
|
54
|
+
release: parsed.release,
|
|
55
|
+
browserMode: parsed.browserMode === 'managed' || parsed.browserMode === 'cdp' ? parsed.browserMode : undefined,
|
|
56
|
+
browserVisibility: parsed.browserVisibility === 'visible' || parsed.browserVisibility === 'hidden' ? parsed.browserVisibility : undefined
|
|
57
|
+
};
|
|
58
|
+
case 'ping':
|
|
59
|
+
return { type: 'ping' };
|
|
60
|
+
case 'pong':
|
|
61
|
+
return { type: 'pong' };
|
|
62
|
+
case 'config_request': {
|
|
63
|
+
if (typeof parsed.requestId !== 'string' || !isRecord(parsed.request) || typeof parsed.request.action !== 'string') {
|
|
64
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'config_request message must have a string "requestId" and a "request" object with an "action".');
|
|
65
|
+
}
|
|
66
|
+
const request = parseConfigAction(parsed.request);
|
|
67
|
+
return { type: 'config_request', requestId: parsed.requestId, request };
|
|
68
|
+
}
|
|
69
|
+
case 'repertoire_request': {
|
|
70
|
+
if (typeof parsed.requestId !== 'string' || !isRecord(parsed.request) || typeof parsed.request.action !== 'string') {
|
|
71
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'repertoire_request message must have a string "requestId" and a "request" object with an "action".');
|
|
72
|
+
}
|
|
73
|
+
const request = parseRepertoireAction(parsed.request);
|
|
74
|
+
return { type: 'repertoire_request', requestId: parsed.requestId, request };
|
|
75
|
+
}
|
|
76
|
+
case 'profile_request': {
|
|
77
|
+
if (typeof parsed.requestId !== 'string' || !isRecord(parsed.request) || typeof parsed.request.action !== 'string') {
|
|
78
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'profile_request message must have a string "requestId" and a "request" object with an "action".');
|
|
79
|
+
}
|
|
80
|
+
const request = parseProfileAction(parsed.request);
|
|
81
|
+
return { type: 'profile_request', requestId: parsed.requestId, request };
|
|
82
|
+
}
|
|
83
|
+
default:
|
|
84
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', `Unknown message type from relay: "${parsed.type}".`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function parseConfigAction(raw) {
|
|
88
|
+
switch (raw.action) {
|
|
89
|
+
case 'get':
|
|
90
|
+
return { action: 'get' };
|
|
91
|
+
case 'set_default_visibility':
|
|
92
|
+
if (raw.value !== 'visible' && raw.value !== 'hidden' && raw.value !== 'inherit') {
|
|
93
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'set_default_visibility requires value "visible", "hidden", or "inherit".');
|
|
94
|
+
}
|
|
95
|
+
return { action: 'set_default_visibility', value: raw.value };
|
|
96
|
+
case 'set_flow_visibility':
|
|
97
|
+
if (typeof raw.flowName !== 'string' || raw.flowName.length === 0) {
|
|
98
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'set_flow_visibility requires a non-empty "flowName".');
|
|
99
|
+
}
|
|
100
|
+
if (raw.value !== 'visible' && raw.value !== 'hidden' && raw.value !== null) {
|
|
101
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'set_flow_visibility requires value "visible", "hidden", or null.');
|
|
102
|
+
}
|
|
103
|
+
return { action: 'set_flow_visibility', flowName: raw.flowName, value: raw.value };
|
|
104
|
+
case 'set_default_browser_mode':
|
|
105
|
+
if (raw.value !== 'managed' && raw.value !== 'cdp' && raw.value !== 'inherit') {
|
|
106
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'set_default_browser_mode requires value "managed", "cdp", or "inherit".');
|
|
107
|
+
}
|
|
108
|
+
return { action: 'set_default_browser_mode', value: raw.value };
|
|
109
|
+
case 'set_flow_browser_mode':
|
|
110
|
+
if (typeof raw.flowName !== 'string' || raw.flowName.length === 0) {
|
|
111
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'set_flow_browser_mode requires a non-empty "flowName".');
|
|
112
|
+
}
|
|
113
|
+
if (raw.value !== 'managed' && raw.value !== 'cdp' && raw.value !== null) {
|
|
114
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'set_flow_browser_mode requires value "managed", "cdp", or null.');
|
|
115
|
+
}
|
|
116
|
+
return { action: 'set_flow_browser_mode', flowName: raw.flowName, value: raw.value };
|
|
117
|
+
default:
|
|
118
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', `Unknown config action: "${String(raw.action)}".`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function parseRepertoireAction(raw) {
|
|
122
|
+
switch (raw.action) {
|
|
123
|
+
case 'list':
|
|
124
|
+
return { action: 'list' };
|
|
125
|
+
case 'add':
|
|
126
|
+
if (typeof raw.flowName !== 'string' || raw.flowName.length === 0) {
|
|
127
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'add requires a non-empty "flowName".');
|
|
128
|
+
}
|
|
129
|
+
return { action: 'add', flowName: raw.flowName };
|
|
130
|
+
case 'remove':
|
|
131
|
+
if (typeof raw.flowName !== 'string' || raw.flowName.length === 0) {
|
|
132
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', 'remove requires a non-empty "flowName".');
|
|
133
|
+
}
|
|
134
|
+
return { action: 'remove', flowName: raw.flowName };
|
|
135
|
+
default:
|
|
136
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', `Unknown repertoire action: "${String(raw.action)}".`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function parseProfileAction(raw) {
|
|
140
|
+
if (typeof raw.flowName !== 'string' || raw.flowName.length === 0) {
|
|
141
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', `${String(raw.action)} requires a non-empty "flowName".`);
|
|
142
|
+
}
|
|
143
|
+
const loginProvider = typeof raw.loginProvider === 'string' ? raw.loginProvider : undefined;
|
|
144
|
+
const loginUrl = typeof raw.loginUrl === 'string' ? raw.loginUrl : undefined;
|
|
145
|
+
switch (raw.action) {
|
|
146
|
+
case 'status':
|
|
147
|
+
return { action: 'status', flowName: raw.flowName, loginProvider, loginUrl };
|
|
148
|
+
case 'login':
|
|
149
|
+
return { action: 'login', flowName: raw.flowName, loginProvider, loginUrl };
|
|
150
|
+
default:
|
|
151
|
+
throw new WebFlowError('INVALID_RELAY_MESSAGE', `Unknown profile action: "${String(raw.action)}".`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export function serializeWorkerMessage(message) {
|
|
155
|
+
return JSON.stringify(message);
|
|
156
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { FlowRunner } from '../runner/flow-runner.js';
|
|
2
|
+
import type { FlowRunOptions } from '../runner/flow-runner.js';
|
|
3
|
+
import type { BrowserMode, FlowExecutionResult } from '../runner/types.js';
|
|
4
|
+
import type { ProfileStatusInfo } from '../profiles/profile-state.js';
|
|
5
|
+
import type { SupportedSchema } from '../schema/types.js';
|
|
6
|
+
import type { BrowserVisibility, GlobalBrowserModePreference, GlobalVisibilityPreference, WebflowConfig } from '../config/webflow-config.js';
|
|
7
|
+
import { type VerifiedRemoteFlow } from '../security/flow-release.js';
|
|
8
|
+
/**
|
|
9
|
+
* What a worker reports to the relay so it can register MCP tools on the worker's
|
|
10
|
+
* behalf. Mirrors the same schema/description resolution tool-registry.ts already
|
|
11
|
+
* does for the local stdio path (RFC 0001, section 2.2). `requiresLogin`/`loginProvider`
|
|
12
|
+
* mirror the flow's `capabilities.requiresLogin`/`capabilities.profile` so a future
|
|
13
|
+
* relay/dashboard can tell a user "connect this account first" instead of guessing.
|
|
14
|
+
*/
|
|
15
|
+
export interface CatalogEntry {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: SupportedSchema;
|
|
19
|
+
requiresLogin: boolean;
|
|
20
|
+
loginProvider?: string;
|
|
21
|
+
loginUrl?: string;
|
|
22
|
+
defaultBrowserMode?: BrowserMode;
|
|
23
|
+
supportedBrowserModes?: BrowserMode[];
|
|
24
|
+
}
|
|
25
|
+
export interface WorkerServiceOptions {
|
|
26
|
+
/** Inject a runner (tests); defaults to a real FlowRunner over projectRoot. */
|
|
27
|
+
runner?: FlowRunner;
|
|
28
|
+
/**
|
|
29
|
+
* Local, worker-owner-controlled default for whether Chrome is visible. The remote
|
|
30
|
+
* relay/client has no channel to override this — only the person watching their own
|
|
31
|
+
* screen decides (see RFC 0001 follow-up, 2026-07-06). Defaults to headless (false).
|
|
32
|
+
*/
|
|
33
|
+
headed?: boolean;
|
|
34
|
+
browserMode?: BrowserMode;
|
|
35
|
+
browserVisibility?: BrowserVisibility;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Local, network-agnostic execution surface for the worker transport (RFC 0001,
|
|
39
|
+
* Fase 0/1). Delegates every bit of actual execution to the existing
|
|
40
|
+
* FlowRunner/discoverFlows — this class only adds catalog building, a per-profile
|
|
41
|
+
* lock, and a login pre-check, none of which existed before because a single stdio
|
|
42
|
+
* client never called runFlow twice concurrently and always ran with a human at the
|
|
43
|
+
* keyboard to notice a login wall.
|
|
44
|
+
*/
|
|
45
|
+
export declare class WorkerService {
|
|
46
|
+
private readonly projectRoot;
|
|
47
|
+
private readonly runner;
|
|
48
|
+
private readonly headed?;
|
|
49
|
+
private readonly browserMode?;
|
|
50
|
+
private readonly browserVisibility?;
|
|
51
|
+
private flowsByName;
|
|
52
|
+
private loginMetaByName;
|
|
53
|
+
private locks;
|
|
54
|
+
constructor(projectRoot: string, options?: WorkerServiceOptions);
|
|
55
|
+
/**
|
|
56
|
+
* Discovers flows, compiles their schemas, and loads each flow module once to read
|
|
57
|
+
* its `capabilities` (requiresLogin/profile). Refreshes the internal catalog used by
|
|
58
|
+
* runFlow(). Call again after reconnecting to a relay so a stale catalog (e.g. a flow
|
|
59
|
+
* removed from disk) never lingers (RFC 0001, section 2.3: reconnection resends the
|
|
60
|
+
* full catalog; the relay overwrites, never merges).
|
|
61
|
+
*/
|
|
62
|
+
buildCatalog(): Promise<CatalogEntry[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Runs a flow by name against the catalog built by buildCatalog(). Two safety nets
|
|
65
|
+
* that a single local stdio client never needed:
|
|
66
|
+
*
|
|
67
|
+
* 1. Login pre-check: if the flow declares `requiresLogin: true` and its resolved
|
|
68
|
+
* profile directory does not exist yet, fails fast with a `LOGIN_REQUIRED` error
|
|
69
|
+
* instead of letting Playwright launch a fresh, unauthenticated Chrome and time
|
|
70
|
+
* out minutes later on a login wall with a confusing generic error.
|
|
71
|
+
* 2. Per-profile lock: serializes concurrent calls that resolve to the same profile
|
|
72
|
+
* so two jobs never race to launch Chrome against the same user-data-dir (one
|
|
73
|
+
* userDataDir = one process at a time; see modules/sessions-y-profiles.md).
|
|
74
|
+
*/
|
|
75
|
+
runFlow(name: string, input: unknown, options?: FlowRunOptions): Promise<FlowExecutionResult>;
|
|
76
|
+
/**
|
|
77
|
+
* Runs a flow whose code the relay pushed alongside the job (RFC 0001, Fase 3) —
|
|
78
|
+
* there is no local flow definition to look up at all, by design: nothing, paid or
|
|
79
|
+
* free, is meant to persist on this machine. Reuses the same login pre-check and
|
|
80
|
+
* per-profile lock as runFlow(); only where the module comes from differs.
|
|
81
|
+
*/
|
|
82
|
+
runVerifiedRemoteFlow(verified: VerifiedRemoteFlow, input: unknown, options?: FlowRunOptions): Promise<FlowExecutionResult>;
|
|
83
|
+
/**
|
|
84
|
+
* Config/repertoire are worker-local disk state (~/.webflow/config.json,
|
|
85
|
+
* ~/.webflow/repertoire.json) — never the relay's database, same principle as
|
|
86
|
+
* login sessions/profiles. These are the only entry points the relay's forwarded
|
|
87
|
+
* config_request/repertoire_request messages call into (see connection.ts). Reuse
|
|
88
|
+
* the same enqueue() lock used for flow runs, under fixed keys, so two concurrent
|
|
89
|
+
* requests never race writing the same file.
|
|
90
|
+
*/
|
|
91
|
+
getConfig(): Promise<WebflowConfig>;
|
|
92
|
+
setDefaultBrowserVisibility(value: GlobalVisibilityPreference): Promise<WebflowConfig>;
|
|
93
|
+
setFlowBrowserVisibility(flowName: string, value: BrowserVisibility | null): Promise<WebflowConfig>;
|
|
94
|
+
setDefaultBrowserMode(value: GlobalBrowserModePreference): Promise<WebflowConfig>;
|
|
95
|
+
setFlowBrowserMode(flowName: string, value: BrowserMode | null): Promise<WebflowConfig>;
|
|
96
|
+
listRepertoire(): Promise<string[]>;
|
|
97
|
+
/** Fast, read-only disk check — same status a running flow would see before deciding LOGIN_REQUIRED. */
|
|
98
|
+
checkProfileStatus(flowName: string, loginProvider?: string, loginUrl?: string): Promise<ProfileStatusInfo>;
|
|
99
|
+
/**
|
|
100
|
+
* Opens a real, visible Chrome window on this machine for the human to log in
|
|
101
|
+
* manually, then returns as soon as the window is up — it does not wait for them
|
|
102
|
+
* to finish (that can take minutes, far too long for an MCP tool call). The
|
|
103
|
+
* per-profile lock (same one runFlow/runRemoteFlow use) is still held for the full
|
|
104
|
+
* session in the background, so an automated run against this profile can't start
|
|
105
|
+
* mid-login; call checkProfileStatus afterward to see whether it succeeded.
|
|
106
|
+
*/
|
|
107
|
+
startProfileLogin(flowName: string, loginProvider?: string, loginUrl?: string): Promise<ProfileStatusInfo>;
|
|
108
|
+
addToRepertoire(flowName: string): Promise<string[]>;
|
|
109
|
+
removeFromRepertoire(flowName: string): Promise<string[]>;
|
|
110
|
+
private enqueue;
|
|
111
|
+
private resolveProfileName;
|
|
112
|
+
}
|