@stigmer/runner 3.4.1 → 3.5.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/dist/.build-fingerprint +1 -1
- package/dist/activities/attach-session.d.ts +34 -0
- package/dist/activities/attach-session.js +95 -0
- package/dist/activities/attach-session.js.map +1 -0
- package/dist/activities/discover-mcp-server.d.ts +9 -0
- package/dist/activities/discover-mcp-server.js +6 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/connect-backfill.d.ts +2 -1
- package/dist/activities/execute-cursor/connect-backfill.js +2 -2
- package/dist/activities/execute-cursor/connect-backfill.js.map +1 -1
- package/dist/activities/execute-cursor/fetch-interceptor.js +31 -0
- package/dist/activities/execute-cursor/fetch-interceptor.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +70 -3
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +7 -1
- package/dist/activities/execute-cursor/mcp-resolver.js +15 -2
- package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/sdk-warmup.d.ts +44 -0
- package/dist/activities/execute-cursor/sdk-warmup.js +61 -0
- package/dist/activities/execute-cursor/sdk-warmup.js.map +1 -0
- package/dist/activities/execute-deep-agent/setup.js +48 -5
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +9 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +19 -5
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +13 -1
- package/dist/client/stigmer-client.js +40 -5
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/client/token-claims.d.ts +9 -6
- package/dist/client/token-claims.js +22 -6
- package/dist/client/token-claims.js.map +1 -1
- package/dist/main.d.ts +8 -1
- package/dist/main.js +122 -2
- package/dist/main.js.map +1 -1
- package/dist/middleware/index.d.ts +0 -1
- package/dist/middleware/index.js +0 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/otel-metrics.d.ts +10 -1
- package/dist/otel-metrics.js +22 -1
- package/dist/otel-metrics.js.map +1 -1
- package/dist/pool-member.d.ts +61 -0
- package/dist/pool-member.js +51 -0
- package/dist/pool-member.js.map +1 -0
- package/dist/runner-manager.d.ts +7 -0
- package/dist/runner-manager.js +39 -1
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +15 -1
- package/dist/runner.js.map +1 -1
- package/dist/shared/cold-start-timing.d.ts +89 -0
- package/dist/shared/cold-start-timing.js +166 -0
- package/dist/shared/cold-start-timing.js.map +1 -0
- package/dist/shared/connect-backfill.d.ts +2 -1
- package/dist/shared/connect-backfill.js +4 -2
- package/dist/shared/connect-backfill.js.map +1 -1
- package/dist/shared/mcp-manager.d.ts +5 -15
- package/dist/shared/mcp-manager.js +5 -32
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +15 -5
- package/dist/shared/mcp-resolver.js +23 -6
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/mcp-transport-guard.d.ts +53 -0
- package/dist/shared/mcp-transport-guard.js +70 -0
- package/dist/shared/mcp-transport-guard.js.map +1 -0
- package/dist/shared/tool-kind.js +6 -0
- package/dist/shared/tool-kind.js.map +1 -1
- package/dist/tools/index.d.ts +27 -0
- package/dist/tools/index.js +27 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/think-tool.js.map +1 -0
- package/dist/tools/url-guard.d.ts +64 -0
- package/dist/tools/url-guard.js +211 -0
- package/dist/tools/url-guard.js.map +1 -0
- package/dist/tools/web-fetch-tool.d.ts +53 -0
- package/dist/tools/web-fetch-tool.js +216 -0
- package/dist/tools/web-fetch-tool.js.map +1 -0
- package/package.json +4 -2
- package/src/__tests__/otel-metrics.test.ts +6 -0
- package/src/__tests__/pool-member.test.ts +76 -0
- package/src/activities/__tests__/attach-session.test.ts +133 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +34 -11
- package/src/activities/attach-session.ts +112 -0
- package/src/activities/discover-mcp-server.ts +19 -1
- package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +77 -0
- package/src/activities/execute-cursor/__tests__/sdk-warmup.test.ts +45 -0
- package/src/activities/execute-cursor/connect-backfill.ts +3 -0
- package/src/activities/execute-cursor/fetch-interceptor.ts +39 -0
- package/src/activities/execute-cursor/index.ts +72 -3
- package/src/activities/execute-cursor/mcp-resolver.ts +21 -2
- package/src/activities/execute-cursor/sdk-warmup.ts +68 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +24 -0
- package/src/activities/execute-deep-agent/setup.ts +48 -7
- package/src/activities/execute-deep-agent/subagent-transformer.ts +27 -2
- package/src/client/__tests__/stigmer-client.test.ts +77 -2
- package/src/client/__tests__/token-claims.test.ts +15 -1
- package/src/client/stigmer-client.ts +51 -6
- package/src/client/token-claims.ts +29 -6
- package/src/main.ts +139 -2
- package/src/middleware/index.ts +0 -1
- package/src/otel-metrics.ts +25 -1
- package/src/pool-member.ts +99 -0
- package/src/runner-manager.ts +52 -0
- package/src/runner.ts +15 -0
- package/src/shared/__tests__/cold-start-timing.test.ts +271 -0
- package/src/shared/__tests__/connect-backfill.test.ts +11 -11
- package/src/shared/__tests__/mcp-manager.test.ts +2 -73
- package/src/shared/__tests__/mcp-resolver.test.ts +108 -0
- package/src/shared/__tests__/mcp-transport-guard.test.ts +71 -0
- package/src/shared/cold-start-timing.ts +197 -0
- package/src/shared/connect-backfill.ts +5 -1
- package/src/shared/mcp-manager.ts +4 -41
- package/src/shared/mcp-resolver.ts +29 -6
- package/src/shared/mcp-transport-guard.ts +81 -0
- package/src/shared/tool-kind.ts +6 -0
- package/src/tools/__tests__/url-guard.test.ts +137 -0
- package/src/tools/__tests__/web-fetch-tool.test.ts +193 -0
- package/src/tools/index.ts +28 -0
- package/src/tools/url-guard.ts +232 -0
- package/src/tools/web-fetch-tool.ts +275 -0
- package/dist/middleware/think-tool.js.map +0 -1
- /package/dist/{middleware → tools}/think-tool.d.ts +0 -0
- /package/dist/{middleware → tools}/think-tool.js +0 -0
- /package/src/{middleware → tools}/__tests__/think-tool.test.ts +0 -0
- /package/src/{middleware → tools}/think-tool.ts +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native `web_fetch` tool — built-in URL retrieval for the deep-agent harness.
|
|
3
|
+
*
|
|
4
|
+
* Closes the harness parity gap of stigmer/stigmer#214: the Cursor harness
|
|
5
|
+
* ships WebFetch as a CLI built-in, while native runs previously needed an
|
|
6
|
+
* MCP server (seedpack `fetch`) for the same capability. Both tool names
|
|
7
|
+
* classify to TOOL_KIND_FETCH, so rendering is identical across harnesses.
|
|
8
|
+
*
|
|
9
|
+
* Design constraints, in order:
|
|
10
|
+
*
|
|
11
|
+
* - Security: every URL — including every redirect hop — passes the URL
|
|
12
|
+
* guard (url-guard.ts). web_fetch is auto-approved, so the guard is the
|
|
13
|
+
* only boundary between "fetch a page" and the runner's network position.
|
|
14
|
+
* - Model ergonomics: HTML is reduced to Markdown (turndown, with obvious
|
|
15
|
+
* chrome stripped), and `start_index`/`max_length` give the model
|
|
16
|
+
* deterministic pagination over large pages — the same surface the
|
|
17
|
+
* seedpack fetch MCP exposed, so existing agent instructions keep working.
|
|
18
|
+
* - Budget discipline: the default window (20 000 chars) sits deliberately
|
|
19
|
+
* below the tool-truncation middleware's 30 000-char default, so the
|
|
20
|
+
* truncation layer never chops fetch output and the model's pagination
|
|
21
|
+
* arithmetic stays exact.
|
|
22
|
+
*
|
|
23
|
+
* Failures return a plain "Error: …" string instead of throwing: a fetch
|
|
24
|
+
* miss is information the model should route around (try the .md export,
|
|
25
|
+
* cite a different page), not an execution fault.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { tool } from "@langchain/core/tools";
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
import TurndownService from "turndown";
|
|
31
|
+
import { validateFetchUrl, UrlGuardError, type GuardPosture } from "./url-guard.js";
|
|
32
|
+
|
|
33
|
+
/** Ceiling on response bytes read from the wire, before any conversion. */
|
|
34
|
+
const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
35
|
+
|
|
36
|
+
/** Per-request timeout; applied to each redirect hop independently. */
|
|
37
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
38
|
+
|
|
39
|
+
/** Redirect chain limit — matches the common browser/curl default region. */
|
|
40
|
+
const MAX_REDIRECTS = 5;
|
|
41
|
+
|
|
42
|
+
/** Default pagination window; see file header for why it is below 30 000. */
|
|
43
|
+
const DEFAULT_MAX_LENGTH = 20_000;
|
|
44
|
+
|
|
45
|
+
/** Hard ceiling for a caller-supplied max_length. */
|
|
46
|
+
const MAX_MAX_LENGTH = 100_000;
|
|
47
|
+
|
|
48
|
+
const USER_AGENT = "Stigmer/1.0 (web_fetch; +https://stigmer.ai)";
|
|
49
|
+
|
|
50
|
+
export interface WebFetchToolOptions {
|
|
51
|
+
/** URL guard posture — derive via resolveGuardPosture(config.mode). */
|
|
52
|
+
readonly posture: GuardPosture;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createWebFetchTool(options: WebFetchToolOptions) {
|
|
56
|
+
return tool(
|
|
57
|
+
async (input: { url: string; max_length?: number; start_index?: number }) => {
|
|
58
|
+
try {
|
|
59
|
+
return await runFetch(input, options.posture);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err instanceof UrlGuardError) {
|
|
62
|
+
return `Error: ${err.message}`;
|
|
63
|
+
}
|
|
64
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
65
|
+
return `Error: Fetching ${input.url} timed out after ${REQUEST_TIMEOUT_MS / 1000}s.`;
|
|
66
|
+
}
|
|
67
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
68
|
+
return `Error: Failed to fetch ${input.url}: ${message}`;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "web_fetch",
|
|
73
|
+
description:
|
|
74
|
+
"Fetch the contents of a URL over http(s). HTML pages are converted " +
|
|
75
|
+
"to Markdown; plain text, Markdown, JSON, and other text formats are " +
|
|
76
|
+
"returned as-is. Binary content is not supported.\n\n" +
|
|
77
|
+
"Large pages are windowed: at most max_length characters are " +
|
|
78
|
+
"returned per call (default 20000). If the result ends with a " +
|
|
79
|
+
"truncation notice, call web_fetch again with the same url and the " +
|
|
80
|
+
"start_index the notice gives you to continue reading.",
|
|
81
|
+
schema: z.object({
|
|
82
|
+
url: z.string().describe("The http(s) URL to fetch."),
|
|
83
|
+
max_length: z
|
|
84
|
+
.number()
|
|
85
|
+
.int()
|
|
86
|
+
.positive()
|
|
87
|
+
.max(MAX_MAX_LENGTH)
|
|
88
|
+
.optional()
|
|
89
|
+
.describe(`Maximum characters to return (default ${DEFAULT_MAX_LENGTH}).`),
|
|
90
|
+
start_index: z
|
|
91
|
+
.number()
|
|
92
|
+
.int()
|
|
93
|
+
.nonnegative()
|
|
94
|
+
.optional()
|
|
95
|
+
.describe("Character offset to start from, for paginating large pages (default 0)."),
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function runFetch(
|
|
102
|
+
input: { url: string; max_length?: number; start_index?: number },
|
|
103
|
+
posture: GuardPosture,
|
|
104
|
+
): Promise<string> {
|
|
105
|
+
const maxLength = input.max_length ?? DEFAULT_MAX_LENGTH;
|
|
106
|
+
const startIndex = input.start_index ?? 0;
|
|
107
|
+
|
|
108
|
+
const { response, finalUrl } = await fetchFollowingRedirects(input.url, posture);
|
|
109
|
+
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
// Drain defensively; some agents (undici) keep the connection reserved otherwise.
|
|
112
|
+
await response.body?.cancel().catch(() => undefined);
|
|
113
|
+
return `Error: ${finalUrl} responded with HTTP ${response.status} ${response.statusText}.`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
117
|
+
if (!isTextual(contentType)) {
|
|
118
|
+
await response.body?.cancel().catch(() => undefined);
|
|
119
|
+
return (
|
|
120
|
+
`Error: ${finalUrl} returned non-text content (${contentType || "unknown type"}). ` +
|
|
121
|
+
"web_fetch only supports text formats."
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { text, bytesTruncated } = await readBodyCapped(response, MAX_RESPONSE_BYTES);
|
|
126
|
+
|
|
127
|
+
const content = isHtml(contentType) ? htmlToMarkdown(text) : text;
|
|
128
|
+
|
|
129
|
+
return paginate(content, {
|
|
130
|
+
url: finalUrl,
|
|
131
|
+
startIndex,
|
|
132
|
+
maxLength,
|
|
133
|
+
bytesTruncated,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fetch with `redirect: "manual"`, re-validating each hop against the URL
|
|
139
|
+
* guard — automatic following would let a public host 302 the runner into
|
|
140
|
+
* a private address, bypassing the pre-flight check entirely.
|
|
141
|
+
*/
|
|
142
|
+
async function fetchFollowingRedirects(
|
|
143
|
+
rawUrl: string,
|
|
144
|
+
posture: GuardPosture,
|
|
145
|
+
): Promise<{ response: Response; finalUrl: string }> {
|
|
146
|
+
let url = await validateFetchUrl(rawUrl, posture);
|
|
147
|
+
|
|
148
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
149
|
+
const response = await fetch(url, {
|
|
150
|
+
redirect: "manual",
|
|
151
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
152
|
+
headers: {
|
|
153
|
+
"user-agent": USER_AGENT,
|
|
154
|
+
accept: "text/html, text/markdown;q=0.9, text/plain;q=0.8, application/json;q=0.7, */*;q=0.5",
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (response.status < 300 || response.status >= 400) {
|
|
159
|
+
return { response, finalUrl: url.toString() };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const location = response.headers.get("location");
|
|
163
|
+
await response.body?.cancel().catch(() => undefined);
|
|
164
|
+
if (!location) {
|
|
165
|
+
throw new Error(`redirect (HTTP ${response.status}) without a Location header`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Resolve relative redirects against the current hop, then re-guard.
|
|
169
|
+
url = await validateFetchUrl(new URL(location, url).toString(), posture);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw new Error(`too many redirects (more than ${MAX_REDIRECTS})`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Stream the body, stopping at the byte cap instead of buffering unbounded. */
|
|
176
|
+
async function readBodyCapped(
|
|
177
|
+
response: Response,
|
|
178
|
+
maxBytes: number,
|
|
179
|
+
): Promise<{ text: string; bytesTruncated: boolean }> {
|
|
180
|
+
if (!response.body) {
|
|
181
|
+
return { text: "", bytesTruncated: false };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const reader = response.body.getReader();
|
|
185
|
+
const chunks: Uint8Array[] = [];
|
|
186
|
+
let total = 0;
|
|
187
|
+
let bytesTruncated = false;
|
|
188
|
+
|
|
189
|
+
for (;;) {
|
|
190
|
+
const { done, value } = await reader.read();
|
|
191
|
+
if (done) break;
|
|
192
|
+
if (value) {
|
|
193
|
+
total += value.byteLength;
|
|
194
|
+
if (total > maxBytes) {
|
|
195
|
+
chunks.push(value.subarray(0, value.byteLength - (total - maxBytes)));
|
|
196
|
+
bytesTruncated = true;
|
|
197
|
+
await reader.cancel().catch(() => undefined);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
chunks.push(value);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const merged = new Uint8Array(chunks.reduce((n, c) => n + c.byteLength, 0));
|
|
205
|
+
let offset = 0;
|
|
206
|
+
for (const chunk of chunks) {
|
|
207
|
+
merged.set(chunk, offset);
|
|
208
|
+
offset += chunk.byteLength;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { text: new TextDecoder("utf-8").decode(merged), bytesTruncated };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isHtml(contentType: string): boolean {
|
|
215
|
+
return /text\/html|application\/xhtml\+xml/i.test(contentType);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isTextual(contentType: string): boolean {
|
|
219
|
+
if (contentType === "") return true; // no header — assume text, the cap bounds the damage
|
|
220
|
+
return /^(text\/|application\/(json|xml|xhtml\+xml|rss\+xml|atom\+xml|javascript|x-yaml|yaml|toml))/i.test(
|
|
221
|
+
contentType,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Reduce HTML to Markdown. Page chrome that never carries answerable
|
|
227
|
+
* content (scripts, styles, navigation, embedded media shells) is removed
|
|
228
|
+
* before conversion so the pagination window is spent on substance.
|
|
229
|
+
*/
|
|
230
|
+
function htmlToMarkdown(html: string): string {
|
|
231
|
+
const turndown = new TurndownService({
|
|
232
|
+
headingStyle: "atx",
|
|
233
|
+
codeBlockStyle: "fenced",
|
|
234
|
+
bulletListMarker: "-",
|
|
235
|
+
});
|
|
236
|
+
turndown.remove([
|
|
237
|
+
"script", "style", "noscript", "iframe", "object", "embed",
|
|
238
|
+
"nav", "aside", "footer", "form", "button", "select", "canvas",
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
const markdown = turndown.turndown(html);
|
|
242
|
+
// Collapse the blank-line runs left behind by removed blocks.
|
|
243
|
+
return markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Apply the start_index/max_length window and explain any truncation. */
|
|
247
|
+
function paginate(
|
|
248
|
+
content: string,
|
|
249
|
+
opts: { url: string; startIndex: number; maxLength: number; bytesTruncated: boolean },
|
|
250
|
+
): string {
|
|
251
|
+
if (opts.startIndex >= content.length && content.length > 0) {
|
|
252
|
+
return `Error: start_index ${opts.startIndex} is beyond the end of the content (${content.length} characters).`;
|
|
253
|
+
}
|
|
254
|
+
if (content.length === 0) {
|
|
255
|
+
return `[${opts.url} returned an empty body]`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const window = content.slice(opts.startIndex, opts.startIndex + opts.maxLength);
|
|
259
|
+
const end = opts.startIndex + window.length;
|
|
260
|
+
const notices: string[] = [];
|
|
261
|
+
|
|
262
|
+
if (end < content.length) {
|
|
263
|
+
notices.push(
|
|
264
|
+
`[Content truncated at ${end} of ${content.length} characters. ` +
|
|
265
|
+
`Call web_fetch again with start_index=${end} to continue.]`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (opts.bytesTruncated) {
|
|
269
|
+
notices.push(
|
|
270
|
+
`[The response exceeded the ${MAX_RESPONSE_BYTES / (1024 * 1024)} MB fetch limit and was cut off.]`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return notices.length > 0 ? `${window}\n\n${notices.join("\n")}` : window;
|
|
275
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"think-tool.js","sourceRoot":"","sources":["../../src/middleware/think-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,UAAU,eAAe;IAC7B,OAAO,IAAI,CACT,KAAK,EAAE,MAA2B,EAAE,EAAE,CAAC,IAAI,EAC3C;QACE,IAAI,EAAE,OAAO;QACb,WAAW,EACT,yDAAyD;YACzD,8EAA8E;YAC9E,4EAA4E;YAC5E,kCAAkC;YAClC,4BAA4B;YAC5B,qEAAqE;YACrE,qEAAqE;YACrE,iEAAiE;YACjE,mEAAmE;YACnE,kEAAkE;YAClE,yDAAyD;QAC3D,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;YACf,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,oCAAoC,CAAC;SACnE,CAAC;KACH,CACF,CAAC;AACJ,CAAC"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|