@foldkit/devtools-mcp 0.24.1-canary.2eb97fb142a1 → 0.24.1-canary.396b19976386
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 +18 -16
- package/dist/install.js +2 -2
- package/dist/relayClient.d.ts +37 -0
- package/dist/relayClient.d.ts.map +1 -0
- package/dist/relayClient.js +276 -0
- package/dist/relayLocation.d.ts +9 -1
- package/dist/relayLocation.d.ts.map +1 -1
- package/dist/relayLocation.js +57 -19
- package/dist/relayRegistry.d.ts +12 -2
- package/dist/relayRegistry.d.ts.map +1 -1
- package/dist/relayRegistry.js +74 -14
- package/dist/relayRegistryTrust.d.ts +19 -0
- package/dist/relayRegistryTrust.d.ts.map +1 -0
- package/dist/relayRegistryTrust.js +456 -0
- package/dist/server.js +6393 -1382
- package/dist/tools.d.ts +2 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +37 -56
- package/package.json +5 -5
- package/dist/webSocketClient.d.ts +0 -28
- package/dist/webSocketClient.d.ts.map +0 -1
- package/dist/webSocketClient.js +0 -134
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ With it attached, agents can:
|
|
|
6
6
|
|
|
7
7
|
- Read the current Model, or any historical Model by history index
|
|
8
8
|
- Narrow reads with dot-string paths and summarized payloads to fit token budgets
|
|
9
|
-
- List and inspect the Message history, with Command and Mount lifecycle, diffs, and
|
|
9
|
+
- List and inspect the Message history, with Command and Mount lifecycle, diffs, and Block chains
|
|
10
10
|
- Query history server-side: filter entries by changed Model paths, count Messages by tag, tail the latest entries, and diff the Models at two indices
|
|
11
11
|
- Read the recorded init Model, the Commands returned from `init`, and the Mounts that fired during the first render
|
|
12
12
|
- Inspect Runtime state: current index, retained history bounds, pause status
|
|
@@ -36,10 +36,10 @@ pnpm add -D @foldkit/devtools-mcp
|
|
|
36
36
|
yarn add -D @foldkit/devtools-mcp
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Pass your `Message` Schema
|
|
39
|
+
Pass your `Message` Schema as `devTools.Message` when calling the Application Composition's owned `assembleApplication` method to let the agent dispatch Messages. The Runtime rejects a payload that does not match the Schema before it reaches update:
|
|
40
40
|
|
|
41
41
|
```typescript
|
|
42
|
-
|
|
42
|
+
ApplicationComposition.assembleApplication({
|
|
43
43
|
devTools: {
|
|
44
44
|
// Rest of your DevTools config
|
|
45
45
|
Message,
|
|
@@ -61,16 +61,16 @@ Keep API keys, passwords, access and refresh tokens, authorization headers, sess
|
|
|
61
61
|
|
|
62
62
|
## Tools
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Every tool except `foldkit_list_runtimes` accepts an optional `runtime_id`. Without one, the tool uses the most recently connected Runtime of the most recently started dev server.
|
|
65
65
|
|
|
66
66
|
| Tool | Description |
|
|
67
67
|
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
-
| `foldkit_list_runtimes` | Returns metadata for every connected browser tab. Agents call this first to discover which Runtime to target.
|
|
68
|
+
| `foldkit_list_runtimes` | Returns metadata for every connected browser tab. Each tab carries the `projectRoot` of the dev server it belongs to, unless `FOLDKIT_DEVTOOLS_MCP_PORT` or `FOLDKIT_DEVTOOLS_MCP_HOST` is set. Agents call this first to discover which Runtime to target. |
|
|
69
69
|
| `foldkit_get_model` | Snapshots the current Model. Accepts an optional `path` to narrow to a subtree and `expand` to control summarization. |
|
|
70
70
|
| `foldkit_get_model_at` | Snapshots a historical Model after a given history entry. Pass `index: N - 1` to read the Model just before Message `N`. Same `path`/`expand` semantics as `foldkit_get_model`. Indices outside the readable range (older entries are evicted past the rolling buffer) are rejected with the valid bounds. For the initial Model (and the init Commands and Mounts), use `foldkit_get_init`. |
|
|
71
71
|
| `foldkit_get_init` | Reads the recorded initial Model, the Commands returned from the application's `init` function, and the Mounts that fired during the first render. Each Command and Mount carries its declared args. Equivalent to selecting the synthetic "init" row in the DevTools panel. |
|
|
72
72
|
| `foldkit_get_runtime_state` | Snapshots the Runtime's DevTools state: history bounds, current paused/live status, and whether init is recorded. Useful for understanding what `foldkit_list_messages` and `foldkit_get_message` will see and detecting whether the Runtime is paused. |
|
|
73
|
-
| `foldkit_list_messages` | Lists Message history entries. Each entry carries the Message body, Commands triggered (with args), Mounts that started or ended during the resulting render (with args), timestamp, an `isModelChanged` flag, the diff path lists (`changedPaths` / `affectedPaths`), and any extracted
|
|
73
|
+
| `foldkit_list_messages` | Lists Message history entries. Each entry carries the Message body, Commands triggered (with args), Mounts that started or ended during the resulting render (with args), timestamp, an `isModelChanged` flag, the diff path lists (`changedPaths` / `affectedPaths`), and any extracted Block chain. Filter server-side with `changed_paths_match`, read the latest entries with `from_end`, and paginate forward with `since_index`. |
|
|
74
74
|
| `foldkit_count_messages_by_tag` | Counts retained history entries by Message tag, without payloads, sorted by count descending. A cheap reconnaissance call before paging through history: it surfaces the high-frequency Messages worth filtering out, and with `changed_paths_match` it answers which Message tags touch a Model subtree. |
|
|
75
75
|
| `foldkit_diff_models` | Diffs the Models at two history indices server-side, returning path-level changes `{ path, before, after }` with summarized values. Each side is `{ _tag: 'Present', value }`, or `{ _tag: 'Absent' }` when the path does not exist on that side. Pass `changed_paths_match` to narrow the diff to a subtree. |
|
|
76
76
|
| `foldkit_get_message` | Reads one entry at a given index. The response carries the SerializedEntry only; to inspect the Model around the entry, call `foldkit_get_model_at` with `index - 1` (before) and `index` (after). Use `foldkit_get_init` for the synthetic init entry. |
|
|
@@ -102,25 +102,27 @@ High-frequency flows (drag-paint, scroll, keystroke) can fill the history buffer
|
|
|
102
102
|
Three components cooperate:
|
|
103
103
|
|
|
104
104
|
- **Browser bridge** (in `foldkit`): runs alongside DevTools, subscribes to the DevTools store, and exchanges typed frames over Vite's HMR WebSocket.
|
|
105
|
-
- **Vite plugin relay** (in `@foldkit/vite-plugin`):
|
|
105
|
+
- **Vite plugin relay** (in `@foldkit/vite-plugin`): gives each dev server its own loopback listener by default, publishes its address, and forwards traffic between browsers and MCP clients.
|
|
106
106
|
- **MCP server** (this package): runs as a Node child process under your AI agent, connects to the plugin's relay over WebSocket, and exposes the typed tools over MCP's stdio transport.
|
|
107
107
|
|
|
108
108
|
Multiple browser tabs can be connected at once and each is addressable by its connection id. Tabs that close (gracefully or not) are pruned from the live Runtime list automatically.
|
|
109
109
|
|
|
110
110
|
## Configuration
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
An agent started at a workspace root can inspect every Foldkit app running under it. If no dev server runs under the agent's project root, the MCP server finds the nearest root that encloses it. This also lets an agent started in `src/` or through a symlink find the app. `foldkit_list_runtimes` lists the Runtimes from every matching dev server and includes each server's `projectRoot`.
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
116
|
-
| `FOLDKIT_PROJECT_ROOT` | Project to search. Defaults to the working directory. A relay for that directory or one inside it can match. |
|
|
117
|
-
| `FOLDKIT_DEVTOOLS_MCP_PORT` | Skips discovery and connects to this port. Set it to the `devToolsMcpPort` in your Vite config. Without this setting, the server tries `9988` if discovery finds nothing. |
|
|
118
|
-
| `FOLDKIT_DEVTOOLS_MCP_HOST` | Overrides the hostname of a discovered relay or configured port. |
|
|
119
|
-
| `FOLDKIT_DEVTOOLS_RELAY_DIRECTORY` | Registry location. Defaults to a directory under `XDG_RUNTIME_DIR` when set, or under the OS temporary directory. Set it in both processes if they use different sandboxes. |
|
|
114
|
+
The MCP server connects when a tool is called. It checks the registry on every call, so the next call can reach a dev server that started or restarted after the agent.
|
|
120
115
|
|
|
121
|
-
|
|
116
|
+
| Environment variable | What it changes |
|
|
117
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
118
|
+
| `FOLDKIT_PROJECT_ROOT` | Project to search. Defaults to the working directory. |
|
|
119
|
+
| `FOLDKIT_DEVTOOLS_MCP_PORT` | Skips discovery and connects to this port. Set it to the `devToolsMcpPort` in your Vite config. When discovery finds no relay, set this variable to use a fixed port. |
|
|
120
|
+
| `FOLDKIT_DEVTOOLS_MCP_HOST` | Skips discovery and connects to this host, on `FOLDKIT_DEVTOOLS_MCP_PORT` or `9988`. Set it when a `devToolsMcpPort` relay runs on another host. |
|
|
121
|
+
| `FOLDKIT_DEVTOOLS_RELAY_DIRECTORY` | Registry location. Defaults to a directory under `XDG_RUNTIME_DIR` when set, `~/.cache` on macOS and Linux, or the user temporary directory on Windows. Set it in both processes if they use different sandboxes. On Windows, it must be a direct child of the user's temporary directory. |
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
An automatically discovered relay's registry record contains a random token. The MCP client uses it to sign a connection challenge and checks the relay's response before sending tool requests. The plugin refuses to publish the record if another user can read or replace the registry directory. The MCP server applies the same check before reading it. Both reject symlinks and junctions; on macOS they also check directory ACLs. Clients without access to the registry cannot connect, but the agent using this MCP server can inspect the Runtime data.
|
|
124
|
+
|
|
125
|
+
On Windows, both processes use PowerShell to check the directory's owner, access list, ancestors, and reparse points. The registry must be a private direct child of the user's temporary directory. If the check cannot run or refuses the directory, configure `devToolsMcpPort` in Vite and set `FOLDKIT_DEVTOOLS_MCP_PORT` to the same port. A fixed port listens on every interface without a token. Do not use it on a shared or untrusted network.
|
|
124
126
|
|
|
125
127
|
## Notes
|
|
126
128
|
|
package/dist/install.js
CHANGED
|
@@ -36,9 +36,9 @@ const printNextSteps = (alreadyRegistered) => {
|
|
|
36
36
|
console.log('');
|
|
37
37
|
console.log('Next steps:');
|
|
38
38
|
console.log('');
|
|
39
|
-
console.log(' 1. Pass
|
|
39
|
+
console.log(' 1. Pass the complete Message Codec to ApplicationComposition.assembleApplication (enables dispatch):');
|
|
40
40
|
console.log('');
|
|
41
|
-
console.log(' devTools: { Message }');
|
|
41
|
+
console.log(' devTools: { Message: ApplicationComposition.Message }');
|
|
42
42
|
console.log('');
|
|
43
43
|
console.log(' 2. Start your dev server, then restart your AI agent (Claude Code, Cursor, etc.).');
|
|
44
44
|
console.log('');
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Cause, Effect, Option } from 'effect';
|
|
2
|
+
import { Request, type Response, type RuntimeInfo } from 'foldkit/devtools-protocol';
|
|
3
|
+
import type { RelayTarget } from './relayLocation.js';
|
|
4
|
+
export type ListedRuntime = Readonly<{
|
|
5
|
+
runtime: typeof RuntimeInfo.Type;
|
|
6
|
+
maybeProjectRoot: Option.Option<string>;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* Connects MCP tool calls to the DevTools relays for a project.
|
|
10
|
+
*
|
|
11
|
+
* Each call checks the registry, connects to matching relays, and lists their
|
|
12
|
+
* Runtimes before routing a request. It closes connections to relays that are
|
|
13
|
+
* no longer published. Concurrent calls share one registry lookup. A dev server
|
|
14
|
+
* started or restarted after the previous call is reached on the next one.
|
|
15
|
+
*
|
|
16
|
+
* `listRuntimes` returns Runtimes from every connected relay, oldest dev server
|
|
17
|
+
* first. `sendRequest` uses the newest relay that lists the requested Runtime.
|
|
18
|
+
* Without an id, it uses the most recently connected Runtime of the most
|
|
19
|
+
* recently started dev server. An id that no relay lists returns an error
|
|
20
|
+
* without waiting for the request timeout.
|
|
21
|
+
*
|
|
22
|
+
* Cancelling one call leaves shared discovery available to other calls. `close`
|
|
23
|
+
* interrupts discovery, closes the relay connections, and fails pending
|
|
24
|
+
* requests.
|
|
25
|
+
*/
|
|
26
|
+
export type RelayClient = Readonly<{
|
|
27
|
+
listRuntimes: Effect.Effect<ReadonlyArray<ListedRuntime>, Error>;
|
|
28
|
+
sendRequest: (request: typeof Request.Type, maybeRuntimeId: Option.Option<string>) => Effect.Effect<typeof Response.Type, Cause.TimeoutError | Error>;
|
|
29
|
+
close: Effect.Effect<void>;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Returns an error message for display. Effect's `TimeoutError` has no
|
|
33
|
+
* `message`, so its string form is used instead.
|
|
34
|
+
*/
|
|
35
|
+
export declare const errorReason: (error: Error) => string;
|
|
36
|
+
export declare const makeRelayClient: <Services>(resolveTargets: Effect.Effect<ReadonlyArray<RelayTarget>, never, Services>) => Effect.Effect<RelayClient, never, Services>;
|
|
37
|
+
//# sourceMappingURL=relayClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relayClient.d.ts","sourceRoot":"","sources":["../src/relayClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,KAAK,EAKV,MAAM,EAKN,MAAM,EASP,MAAM,QAAQ,CAAA;AACf,OAAO,EACL,OAAO,EAEP,KAAK,QAAQ,EAEb,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAA;AAIlC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAqDrD,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC,OAAO,EAAE,OAAO,WAAW,CAAC,IAAI,CAAA;IAChC,gBAAgB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CACxC,CAAC,CAAA;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC,CAAA;IAChE,WAAW,EAAE,CACX,OAAO,EAAE,OAAO,OAAO,CAAC,IAAI,EAC5B,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAClC,MAAM,CAAC,MAAM,CAAC,OAAO,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC,CAAA;IACpE,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CAC3B,CAAC,CAAA;AAiBF;;;GAGG;AACH,eAAO,MAAM,WAAW,UAAW,KAAK,KAAG,MACe,CAAA;AA4H1D,eAAO,MAAM,eAAe,GAAI,QAAQ,kBACtB,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,KACzE,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,EAAE,QAAQ,CAkTzC,CAAA"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { Array, Console, Data, Deferred, Duration, Effect, Exit, HashMap, HashSet, Match, Option, Predicate, Ref, Result, Schema, Scope, Semaphore, String, pipe, } from 'effect';
|
|
2
|
+
import { Request, RequestFrame, ResponseFrame, } from 'foldkit/devtools-protocol';
|
|
3
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
4
|
+
import { WebSocket } from 'ws';
|
|
5
|
+
const REQUEST_TIMEOUT = Duration.seconds(10);
|
|
6
|
+
const CONNECT_TIMEOUT = Duration.seconds(2);
|
|
7
|
+
const LIST_RUNTIMES_TIMEOUT = Duration.seconds(2);
|
|
8
|
+
const NOT_CONNECTED_REASON = 'Not connected to a Foldkit dev server. Start your Foldkit Vite dev server and retry the tool call.';
|
|
9
|
+
const NO_RUNTIMES_REASON = 'No connected Foldkit runtimes. Open a Foldkit dev page and try again.';
|
|
10
|
+
const INVALID_ADDRESS_REASON = 'the relay address is not a valid URL';
|
|
11
|
+
const UNVERIFIED_RELAY_REASON = 'the relay could not prove its identity';
|
|
12
|
+
const CLOSED_REASON = 'The Foldkit relay client is closed.';
|
|
13
|
+
const RELAY_TOKEN_PARAMETER = 'token';
|
|
14
|
+
const RELAY_CHALLENGE_HEADER = 'X-Foldkit-Relay-Challenge';
|
|
15
|
+
const RELAY_CLIENT_PROOF_HEADER = 'X-Foldkit-Relay-Client-Proof';
|
|
16
|
+
const RELAY_SERVER_PROOF_HEADER = 'x-foldkit-relay-server-proof';
|
|
17
|
+
const RELAY_TOKEN_PATTERN = /^[0-9a-f]{64}$/;
|
|
18
|
+
const RELAY_PROOF_PATTERN = /^[0-9a-f]{64}$/;
|
|
19
|
+
const RELAY_CHALLENGE_BYTES = 32;
|
|
20
|
+
const encodeRequestFrameToJson = Schema.encodeUnknownSync(Schema.fromJsonString(RequestFrame));
|
|
21
|
+
const RefreshTurn = Data.taggedEnum();
|
|
22
|
+
const generateRequestId = () => `req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
23
|
+
const parseUrl = Option.liftThrowable((url) => new URL(url));
|
|
24
|
+
const relayUrlForLog = (url) => Option.match(parseUrl(url), {
|
|
25
|
+
onNone: () => Array.headNonEmpty(String.split(url, '?')),
|
|
26
|
+
onSome: parsed => {
|
|
27
|
+
parsed.search = '';
|
|
28
|
+
return parsed.toString();
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Returns an error message for display. Effect's `TimeoutError` has no
|
|
33
|
+
* `message`, so its string form is used instead.
|
|
34
|
+
*/
|
|
35
|
+
export const errorReason = (error) => error.message ? error.message : globalThis.String(error);
|
|
36
|
+
const toError = (description) => (cause) => cause instanceof Error
|
|
37
|
+
? cause
|
|
38
|
+
: new Error(`${description}: ${globalThis.String(cause)}`);
|
|
39
|
+
const computeRelayProof = (token, role, challenge) => createHmac('sha256', token).update(`${role}:${challenge}`).digest('hex');
|
|
40
|
+
const attemptOpen = (url) => Effect.suspend(() => Option.match(parseUrl(url), {
|
|
41
|
+
onNone: () => Effect.fail(new Error(INVALID_ADDRESS_REASON)),
|
|
42
|
+
onSome: parsedUrl => {
|
|
43
|
+
const maybeToken = Option.fromNullishOr(parsedUrl.searchParams.get(RELAY_TOKEN_PARAMETER));
|
|
44
|
+
if (Option.isSome(maybeToken) &&
|
|
45
|
+
!RELAY_TOKEN_PATTERN.test(maybeToken.value)) {
|
|
46
|
+
return Effect.fail(new Error(INVALID_ADDRESS_REASON));
|
|
47
|
+
}
|
|
48
|
+
const maybeChallenge = Option.map(maybeToken, () => randomBytes(RELAY_CHALLENGE_BYTES).toString('hex'));
|
|
49
|
+
parsedUrl.searchParams.delete(RELAY_TOKEN_PARAMETER);
|
|
50
|
+
return Effect.callback(resume => {
|
|
51
|
+
const constructed = Result.try({
|
|
52
|
+
try: () => Option.match(Option.all([maybeToken, maybeChallenge]), {
|
|
53
|
+
onNone: () => new WebSocket(parsedUrl),
|
|
54
|
+
onSome: ([token, challenge]) => new WebSocket(parsedUrl, {
|
|
55
|
+
headers: {
|
|
56
|
+
[RELAY_CHALLENGE_HEADER]: challenge,
|
|
57
|
+
[RELAY_CLIENT_PROOF_HEADER]: computeRelayProof(token, 'client', challenge),
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
}),
|
|
61
|
+
catch: toError('Failed to open a connection'),
|
|
62
|
+
});
|
|
63
|
+
if (Result.isFailure(constructed)) {
|
|
64
|
+
resume(Effect.fail(constructed.failure));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const socket = constructed.success;
|
|
68
|
+
let isSettled = false;
|
|
69
|
+
let isVerified = Option.isNone(maybeToken);
|
|
70
|
+
socket.once('upgrade', response => {
|
|
71
|
+
if (Option.isNone(maybeToken) || Option.isNone(maybeChallenge)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const presented = response.headers[RELAY_SERVER_PROOF_HEADER];
|
|
75
|
+
const expected = Buffer.from(computeRelayProof(maybeToken.value, 'server', maybeChallenge.value), 'hex');
|
|
76
|
+
const isValidProof = Predicate.isString(presented) &&
|
|
77
|
+
RELAY_PROOF_PATTERN.test(presented) &&
|
|
78
|
+
timingSafeEqual(expected, Buffer.from(presented, 'hex'));
|
|
79
|
+
if (isValidProof) {
|
|
80
|
+
isVerified = true;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
isSettled = true;
|
|
84
|
+
socket.terminate();
|
|
85
|
+
resume(Effect.fail(new Error(UNVERIFIED_RELAY_REASON)));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
socket.once('open', () => {
|
|
89
|
+
if (!isVerified) {
|
|
90
|
+
isSettled = true;
|
|
91
|
+
socket.terminate();
|
|
92
|
+
resume(Effect.fail(new Error(UNVERIFIED_RELAY_REASON)));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
isSettled = true;
|
|
96
|
+
resume(Effect.succeed(socket));
|
|
97
|
+
});
|
|
98
|
+
socket.on('error', error => {
|
|
99
|
+
if (!isSettled) {
|
|
100
|
+
isSettled = true;
|
|
101
|
+
resume(Effect.fail(error));
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return Effect.sync(() => {
|
|
105
|
+
isSettled = true;
|
|
106
|
+
socket.terminate();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
}));
|
|
111
|
+
const isOpen = ({ socket }) => socket.readyState === WebSocket.OPEN;
|
|
112
|
+
export const makeRelayClient = (resolveTargets) => Effect.gen(function* () {
|
|
113
|
+
const pendingResponsesRef = yield* Ref.make(HashMap.empty());
|
|
114
|
+
const socketsRef = yield* Ref.make(HashMap.empty());
|
|
115
|
+
const refreshLock = yield* Semaphore.make(1);
|
|
116
|
+
const pendingRefreshRef = yield* Ref.make(Option.none());
|
|
117
|
+
const refreshScope = yield* Scope.make();
|
|
118
|
+
const closed = yield* Deferred.make();
|
|
119
|
+
const context = yield* Effect.context();
|
|
120
|
+
const attachHandlers = (socket) => {
|
|
121
|
+
socket.on('message', raw => {
|
|
122
|
+
Effect.runForkWith(context)(handleIncomingMessage(raw, pendingResponsesRef));
|
|
123
|
+
});
|
|
124
|
+
socket.on('error', error => {
|
|
125
|
+
console.error(`[foldkit-devtools-mcp] socket error: ${error.message}`);
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
const openTarget = (target) => Effect.uninterruptibleMask(restore => restore(attemptOpen(target.url).pipe(Effect.timeoutOrElse({
|
|
129
|
+
duration: CONNECT_TIMEOUT,
|
|
130
|
+
orElse: () => Effect.fail(new Error(`no connection within ${Duration.format(CONNECT_TIMEOUT)}`)),
|
|
131
|
+
}))).pipe(Effect.flatMap(socket => Effect.gen(function* () {
|
|
132
|
+
attachHandlers(socket);
|
|
133
|
+
yield* Ref.update(socketsRef, HashMap.set(target.key, { socket, target }));
|
|
134
|
+
yield* Console.error(`[foldkit-devtools-mcp] connected to ${relayUrlForLog(target.url)}`);
|
|
135
|
+
})))).pipe(Effect.catch(error => Console.error(`[foldkit-devtools-mcp] connect attempt to ${relayUrlForLog(target.url)} failed: ${errorReason(error)}`)));
|
|
136
|
+
const reconcile = Effect.gen(function* () {
|
|
137
|
+
if (yield* Deferred.isDone(closed)) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const targets = yield* Effect.provideContext(resolveTargets, context);
|
|
141
|
+
const targetKeys = HashSet.fromIterable(Array.map(targets, ({ key }) => key));
|
|
142
|
+
const sockets = yield* Ref.get(socketsRef);
|
|
143
|
+
const isKept = (relaySocket, key) => HashSet.has(targetKeys, key) && isOpen(relaySocket);
|
|
144
|
+
const keptSockets = HashMap.filter(sockets, isKept);
|
|
145
|
+
const droppedSockets = HashMap.filter(sockets, (relaySocket, key) => !isKept(relaySocket, key));
|
|
146
|
+
yield* Effect.sync(() => {
|
|
147
|
+
for (const { socket } of HashMap.values(droppedSockets)) {
|
|
148
|
+
socket.terminate();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
yield* Ref.set(socketsRef, keptSockets);
|
|
152
|
+
const missingTargets = Array.filter(targets, ({ key }) => !HashMap.has(keptSockets, key));
|
|
153
|
+
yield* Effect.forEach(missingTargets, openTarget, {
|
|
154
|
+
concurrency: 'unbounded',
|
|
155
|
+
discard: true,
|
|
156
|
+
});
|
|
157
|
+
const nextSockets = yield* Ref.get(socketsRef);
|
|
158
|
+
return pipe(targets, Array.reverse, Array.map(({ key }) => HashMap.get(nextSockets, key)), Array.getSomes);
|
|
159
|
+
});
|
|
160
|
+
const takeRefreshTurn = (ownRefresh) => Ref.modify(pendingRefreshRef, (maybePendingRefresh) => Option.match(maybePendingRefresh, {
|
|
161
|
+
onNone: () => [
|
|
162
|
+
RefreshTurn.Leader({ refresh: ownRefresh }),
|
|
163
|
+
Option.some(ownRefresh),
|
|
164
|
+
],
|
|
165
|
+
onSome: refresh => [
|
|
166
|
+
RefreshTurn.Follower({ refresh }),
|
|
167
|
+
maybePendingRefresh,
|
|
168
|
+
],
|
|
169
|
+
}));
|
|
170
|
+
const leadRefresh = (refresh) => Deferred.into(refreshLock.withPermits(1)(Effect.gen(function* () {
|
|
171
|
+
yield* Ref.set(pendingRefreshRef, Option.none());
|
|
172
|
+
return yield* reconcile;
|
|
173
|
+
})), refresh).pipe(Effect.asVoid);
|
|
174
|
+
const refreshRelays = Effect.uninterruptibleMask(restore => Effect.gen(function* () {
|
|
175
|
+
if (yield* Deferred.isDone(closed)) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
const ownRefresh = yield* Deferred.make();
|
|
179
|
+
const turn = yield* takeRefreshTurn(ownRefresh);
|
|
180
|
+
yield* Match.value(turn).pipe(Match.withReturnType(), Match.tagsExhaustive({
|
|
181
|
+
Leader: ({ refresh }) => Effect.forkIn(leadRefresh(refresh), refreshScope, {
|
|
182
|
+
startImmediately: true,
|
|
183
|
+
}).pipe(Effect.asVoid),
|
|
184
|
+
Follower: () => Effect.void,
|
|
185
|
+
}));
|
|
186
|
+
return yield* restore(Deferred.await(turn.refresh));
|
|
187
|
+
}));
|
|
188
|
+
const exchange = (socket, request, maybeConnectionId, timeout) => Effect.acquireUseRelease(Effect.gen(function* () {
|
|
189
|
+
if (yield* Deferred.isDone(closed)) {
|
|
190
|
+
return yield* Effect.fail(new Error(CLOSED_REASON));
|
|
191
|
+
}
|
|
192
|
+
const id = generateRequestId();
|
|
193
|
+
const deferred = yield* Deferred.make();
|
|
194
|
+
yield* Ref.update(pendingResponsesRef, HashMap.set(id, deferred));
|
|
195
|
+
return { id, deferred };
|
|
196
|
+
}), ({ id, deferred }) => {
|
|
197
|
+
const frame = {
|
|
198
|
+
id,
|
|
199
|
+
maybeConnectionId,
|
|
200
|
+
request,
|
|
201
|
+
};
|
|
202
|
+
return Effect.try({
|
|
203
|
+
try: () => socket.send(encodeRequestFrameToJson(frame)),
|
|
204
|
+
catch: toError('Failed to send request'),
|
|
205
|
+
}).pipe(Effect.andThen(Effect.raceFirst(Deferred.await(deferred).pipe(Effect.timeout(timeout)), Deferred.await(closed).pipe(Effect.andThen(Effect.fail(new Error(CLOSED_REASON)))))));
|
|
206
|
+
}, ({ id }) => Ref.update(pendingResponsesRef, HashMap.remove(id)));
|
|
207
|
+
const listRelayRuntimes = ({ socket, target, }) => exchange(socket, Request.RequestListRuntimes(), Option.none(), LIST_RUNTIMES_TIMEOUT).pipe(Effect.flatMap(response => Match.value(response).pipe(Match.withReturnType(), Match.tag('ResponseRuntimes', ({ runtimes }) => Effect.succeed(runtimes)), Match.tag('ResponseError', ({ reason }) => Effect.fail(new Error(reason))), Match.orElse(({ _tag }) => Effect.fail(new Error(`the relay answered ${_tag}`))))), Effect.catch(error => Console.error(`[foldkit-devtools-mcp] listing runtimes at ${relayUrlForLog(target.url)} failed: ${errorReason(error)}`).pipe(Effect.as([]))));
|
|
208
|
+
const listRelays = Effect.gen(function* () {
|
|
209
|
+
const relaySockets = yield* refreshRelays;
|
|
210
|
+
if (Array.isReadonlyArrayEmpty(relaySockets)) {
|
|
211
|
+
return yield* Effect.fail(new Error(NOT_CONNECTED_REASON));
|
|
212
|
+
}
|
|
213
|
+
return yield* Effect.forEach(relaySockets, relaySocket => Effect.map(listRelayRuntimes(relaySocket), runtimes => ({
|
|
214
|
+
relaySocket,
|
|
215
|
+
runtimes,
|
|
216
|
+
})), { concurrency: 'unbounded' });
|
|
217
|
+
});
|
|
218
|
+
const listRuntimes = Effect.map(listRelays, listings => Array.flatMap(listings, ({ relaySocket, runtimes }) => Array.map(runtimes, runtime => ({
|
|
219
|
+
runtime,
|
|
220
|
+
maybeProjectRoot: relaySocket.target.maybeProjectRoot,
|
|
221
|
+
}))));
|
|
222
|
+
const sendRequest = (request, maybeRuntimeId) => Effect.gen(function* () {
|
|
223
|
+
const listings = yield* listRelays;
|
|
224
|
+
const route = yield* routeFromListings(listings, maybeRuntimeId);
|
|
225
|
+
return yield* exchange(route.socket, request, Option.some(route.connectionId), REQUEST_TIMEOUT);
|
|
226
|
+
});
|
|
227
|
+
const close = Effect.uninterruptible(Effect.gen(function* () {
|
|
228
|
+
yield* Deferred.succeed(closed, undefined);
|
|
229
|
+
yield* Scope.close(refreshScope, Exit.void);
|
|
230
|
+
yield* refreshLock.withPermits(1)(Effect.gen(function* () {
|
|
231
|
+
const sockets = yield* Ref.getAndSet(socketsRef, HashMap.empty());
|
|
232
|
+
yield* Effect.sync(() => {
|
|
233
|
+
for (const { socket } of HashMap.values(sockets)) {
|
|
234
|
+
socket.terminate();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}));
|
|
238
|
+
}));
|
|
239
|
+
const client = { listRuntimes, sendRequest, close };
|
|
240
|
+
return client;
|
|
241
|
+
});
|
|
242
|
+
const routeToRuntime = (listings, runtimeId) => Option.match(Array.findLast(listings, ({ runtimes }) => Array.some(runtimes, ({ connectionId }) => connectionId === runtimeId)), {
|
|
243
|
+
onNone: () => Effect.fail(new Error(`No connected Foldkit Runtime has the id ${runtimeId}. Call foldkit_list_runtimes for the current ids.`)),
|
|
244
|
+
onSome: ({ relaySocket }) => Effect.succeed({
|
|
245
|
+
socket: relaySocket.socket,
|
|
246
|
+
connectionId: runtimeId,
|
|
247
|
+
}),
|
|
248
|
+
});
|
|
249
|
+
const routeToDefaultRuntime = (listings) => Option.match(pipe(listings, Array.flatMap(({ relaySocket, runtimes }) => Array.map(runtimes, ({ connectionId }) => ({
|
|
250
|
+
socket: relaySocket.socket,
|
|
251
|
+
connectionId,
|
|
252
|
+
}))), Array.last), {
|
|
253
|
+
onNone: () => Effect.fail(new Error(NO_RUNTIMES_REASON)),
|
|
254
|
+
onSome: Effect.succeed,
|
|
255
|
+
});
|
|
256
|
+
const routeFromListings = (listings, maybeRuntimeId) => Option.match(maybeRuntimeId, {
|
|
257
|
+
onNone: () => routeToDefaultRuntime(listings),
|
|
258
|
+
onSome: runtimeId => routeToRuntime(listings, runtimeId),
|
|
259
|
+
});
|
|
260
|
+
const handleIncomingMessage = (raw, pendingResponsesRef) => {
|
|
261
|
+
const decoded = Schema.decodeExit(Schema.fromJsonString(ResponseFrame))(raw.toString());
|
|
262
|
+
return Exit.match(decoded, {
|
|
263
|
+
onFailure: error => Effect.sync(() => console.error('[foldkit-devtools-mcp] failed to decode frame', error)),
|
|
264
|
+
onSuccess: responseFrame => Effect.gen(function* () {
|
|
265
|
+
const map = yield* Ref.get(pendingResponsesRef);
|
|
266
|
+
const maybeDeferred = HashMap.get(map, responseFrame.id);
|
|
267
|
+
yield* Option.match(maybeDeferred, {
|
|
268
|
+
onNone: () => Effect.void,
|
|
269
|
+
onSome: deferred => Effect.gen(function* () {
|
|
270
|
+
yield* Ref.update(pendingResponsesRef, HashMap.remove(responseFrame.id));
|
|
271
|
+
yield* Deferred.succeed(deferred, responseFrame.response);
|
|
272
|
+
}),
|
|
273
|
+
});
|
|
274
|
+
}),
|
|
275
|
+
});
|
|
276
|
+
};
|
package/dist/relayLocation.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { Effect, Option } from 'effect';
|
|
2
|
+
import type { ChildProcessSpawner } from 'effect/process';
|
|
3
|
+
import { type RelayRegistryReader, type RelayRegistryServices } from './relayRegistry.js';
|
|
2
4
|
export type Settings = Readonly<{
|
|
3
5
|
maybeConfiguredPort: Option.Option<string>;
|
|
4
6
|
maybeConfiguredHost: Option.Option<string>;
|
|
5
7
|
projectRoot: string;
|
|
6
8
|
}>;
|
|
9
|
+
export type RelayTarget = Readonly<{
|
|
10
|
+
key: string;
|
|
11
|
+
url: string;
|
|
12
|
+
maybeProjectRoot: Option.Option<string>;
|
|
13
|
+
}>;
|
|
7
14
|
export declare const loadSettings: Effect.Effect<Settings>;
|
|
8
|
-
export declare const
|
|
15
|
+
export declare const configuredRelayUrl: (settings: Settings) => Option.Option<string>;
|
|
16
|
+
export declare const resolveRelayTargets: (settings: Settings, registryReader: RelayRegistryReader) => Effect.Effect<ReadonlyArray<RelayTarget>, never, RelayRegistryServices | ChildProcessSpawner.ChildProcessSpawner>;
|
|
9
17
|
//# sourceMappingURL=relayLocation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relayLocation.d.ts","sourceRoot":"","sources":["../src/relayLocation.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"relayLocation.d.ts","sourceRoot":"","sources":["../src/relayLocation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAGzD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAE3B,MAAM,oBAAoB,CAAA;AAS3B,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC1C,mBAAmB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC1C,WAAW,EAAE,MAAM,CAAA;CACpB,CAAC,CAAA;AAEF,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IACjC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,gBAAgB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CACxC,CAAC,CAAA;AAEF,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAe5B,CAAA;AAmDrB,eAAO,MAAM,kBAAkB,aACnB,QAAQ,KACjB,MAAM,CAAC,MAAM,CAAC,MAAM,CAiBtB,CAAA;AAED,eAAO,MAAM,mBAAmB,aACpB,QAAQ,kBACF,mBAAmB,KAClC,MAAM,CAAC,MAAM,CACd,aAAa,CAAC,WAAW,CAAC,EAC1B,KAAK,EACL,qBAAqB,GAAG,mBAAmB,CAAC,mBAAmB,CAmB7D,CAAA"}
|
package/dist/relayLocation.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { Config, Effect, Option } from 'effect';
|
|
2
|
-
import {
|
|
1
|
+
import { Array, Config, Effect, Option } from 'effect';
|
|
2
|
+
import { isIPv6 } from 'node:net';
|
|
3
|
+
import { discoverRelays, } from './relayRegistry.js';
|
|
3
4
|
const LEGACY_DEFAULT_PORT = 9988;
|
|
4
5
|
const DEFAULT_HOST = 'localhost';
|
|
6
|
+
const AUTOMATIC_RELAY_HOST = '127.0.0.1';
|
|
7
|
+
const AUTOMATIC_RELAY_PATH = '/__foldkit/devtools-mcp';
|
|
8
|
+
const RELAY_TOKEN_PARAMETER = 'token';
|
|
9
|
+
const RELAY_TOKEN_PATTERN = /^[0-9a-f]{64}$/;
|
|
5
10
|
export const loadSettings = Effect.gen(function* () {
|
|
6
11
|
const maybeConfiguredPort = yield* Config.option(Config.String('FOLDKIT_DEVTOOLS_MCP_PORT'));
|
|
7
12
|
const maybeConfiguredHost = yield* Config.option(Config.String('FOLDKIT_DEVTOOLS_MCP_HOST'));
|
|
@@ -12,22 +17,55 @@ export const loadSettings = Effect.gen(function* () {
|
|
|
12
17
|
projectRoot: Option.getOrElse(maybeProjectRoot, () => process.cwd()),
|
|
13
18
|
};
|
|
14
19
|
}).pipe(Effect.orDie);
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return parsed.toString();
|
|
22
|
-
},
|
|
20
|
+
const urlHost = (host) => (isIPv6(host) ? `[${host}]` : host);
|
|
21
|
+
const relayUrl = (host, port) => `ws://${urlHost(host)}:${port}`;
|
|
22
|
+
const fixedTarget = (url) => ({
|
|
23
|
+
key: url,
|
|
24
|
+
url,
|
|
25
|
+
maybeProjectRoot: Option.none(),
|
|
23
26
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}))),
|
|
32
|
-
});
|
|
27
|
+
const parseUrl = Option.liftThrowable((url) => new URL(url));
|
|
28
|
+
const hasValidPort = (url) => {
|
|
29
|
+
const port = globalThis.Number(url.port);
|
|
30
|
+
return (url.port !== '' &&
|
|
31
|
+
globalThis.Number.isInteger(port) &&
|
|
32
|
+
port > 0 &&
|
|
33
|
+
port <= 65_535);
|
|
33
34
|
};
|
|
35
|
+
const isPublishedRelayUrl = (value) => Option.exists(parseUrl(value), url => {
|
|
36
|
+
if (url.protocol !== 'ws:' ||
|
|
37
|
+
url.username !== '' ||
|
|
38
|
+
url.password !== '' ||
|
|
39
|
+
url.hash !== '' ||
|
|
40
|
+
!hasValidPort(url)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const parameters = Array.fromIterable(url.searchParams);
|
|
44
|
+
return (url.hostname === AUTOMATIC_RELAY_HOST &&
|
|
45
|
+
url.pathname === AUTOMATIC_RELAY_PATH &&
|
|
46
|
+
Array.length(parameters) === 1 &&
|
|
47
|
+
Option.exists(Array.head(parameters), ([name, token]) => name === RELAY_TOKEN_PARAMETER && RELAY_TOKEN_PATTERN.test(token)));
|
|
48
|
+
});
|
|
49
|
+
export const configuredRelayUrl = (settings) => {
|
|
50
|
+
const { maybeConfiguredHost, maybeConfiguredPort } = settings;
|
|
51
|
+
if (Option.isSome(maybeConfiguredHost) ||
|
|
52
|
+
Option.isSome(maybeConfiguredPort)) {
|
|
53
|
+
const host = Option.getOrElse(maybeConfiguredHost, () => DEFAULT_HOST);
|
|
54
|
+
const port = Option.getOrElse(maybeConfiguredPort, () => LEGACY_DEFAULT_PORT);
|
|
55
|
+
return Option.some(relayUrl(host, port));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
return Option.none();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
export const resolveRelayTargets = (settings, registryReader) => Option.match(configuredRelayUrl(settings), {
|
|
62
|
+
onSome: url => Effect.succeed([fixedTarget(url)]),
|
|
63
|
+
onNone: () => Effect.map(discoverRelays(settings.projectRoot, registryReader), records => {
|
|
64
|
+
const trustedRecords = Array.filter(records, record => isPublishedRelayUrl(record.url));
|
|
65
|
+
return Array.map(trustedRecords, (record) => ({
|
|
66
|
+
key: record.id,
|
|
67
|
+
url: record.url,
|
|
68
|
+
maybeProjectRoot: Option.some(record.root),
|
|
69
|
+
}));
|
|
70
|
+
}),
|
|
71
|
+
});
|
package/dist/relayRegistry.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import { Effect, FileSystem,
|
|
1
|
+
import { Effect, FileSystem, HashSet, Path, Ref } from 'effect';
|
|
2
|
+
import type { ChildProcessSpawner } from 'effect/process';
|
|
2
3
|
import { RelayRecord } from 'foldkit/devtools-protocol';
|
|
4
|
+
import { type RelayRegistryTrust } from './relayRegistryTrust.js';
|
|
3
5
|
export type RelayRegistryServices = FileSystem.FileSystem | Path.Path;
|
|
4
|
-
export
|
|
6
|
+
export type RelayRegistryReader = Readonly<{
|
|
7
|
+
trust: RelayRegistryTrust;
|
|
8
|
+
reportedRefusals: Ref.Ref<HashSet.HashSet<string>>;
|
|
9
|
+
}>;
|
|
10
|
+
type RootPathApi = Pick<Path.Path, 'isAbsolute' | 'relative' | 'sep'>;
|
|
11
|
+
export declare const isWithinRoot: (root: string, candidate: string, pathApi: RootPathApi) => boolean;
|
|
12
|
+
export declare const makeRelayRegistryReader: Effect.Effect<RelayRegistryReader>;
|
|
13
|
+
export declare const discoverRelays: (projectRoot: string, registryReader: RelayRegistryReader) => Effect.Effect<ReadonlyArray<RelayRecord>, never, RelayRegistryServices | ChildProcessSpawner.ChildProcessSpawner>;
|
|
14
|
+
export {};
|
|
5
15
|
//# sourceMappingURL=relayRegistry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relayRegistry.d.ts","sourceRoot":"","sources":["../src/relayRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"relayRegistry.d.ts","sourceRoot":"","sources":["../src/relayRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,MAAM,EACN,UAAU,EACV,OAAO,EAGP,IAAI,EACJ,GAAG,EAIJ,MAAM,QAAQ,CAAA;AACf,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AACzD,OAAO,EAGL,WAAW,EACZ,MAAM,2BAA2B,CAAA;AAGlC,OAAO,EACL,KAAK,kBAAkB,EAExB,MAAM,yBAAyB,CAAA;AAMhC,MAAM,MAAM,qBAAqB,GAAG,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAA;AAErE,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,KAAK,EAAE,kBAAkB,CAAA;IACzB,gBAAgB,EAAE,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;CACnD,CAAC,CAAA;AAEF,KAAK,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,GAAG,UAAU,GAAG,KAAK,CAAC,CAAA;AA8FrE,eAAO,MAAM,YAAY,SACjB,MAAM,aACD,MAAM,WACR,WAAW,KACnB,OASF,CAAA;AAED,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,MAAM,CAAC,mBAAmB,CASnE,CAAA;AAwEJ,eAAO,MAAM,cAAc,gBACZ,MAAM,kBACH,mBAAmB,KAClC,MAAM,CAAC,MAAM,CACd,aAAa,CAAC,WAAW,CAAC,EAC1B,KAAK,EACL,qBAAqB,GAAG,mBAAmB,CAAC,mBAAmB,CAqD7D,CAAA"}
|