@aexol/spectral 0.9.125 → 0.9.127

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.
Files changed (75) hide show
  1. package/dist/agent/index.d.ts.map +1 -1
  2. package/dist/agent/index.js +5 -0
  3. package/dist/cli.js +6 -0
  4. package/dist/commands/update.d.ts +17 -0
  5. package/dist/commands/update.d.ts.map +1 -0
  6. package/dist/commands/update.js +292 -0
  7. package/dist/extensions/seo/analyzers/content.d.ts +29 -6
  8. package/dist/extensions/seo/analyzers/content.d.ts.map +1 -1
  9. package/dist/extensions/seo/analyzers/content.js +243 -8
  10. package/dist/extensions/seo/analyzers/crawler.d.ts.map +1 -1
  11. package/dist/extensions/seo/analyzers/crawler.js +133 -7
  12. package/dist/extensions/seo/analyzers/drift.d.ts.map +1 -1
  13. package/dist/extensions/seo/analyzers/drift.js +86 -29
  14. package/dist/extensions/seo/analyzers/images.d.ts +10 -4
  15. package/dist/extensions/seo/analyzers/images.d.ts.map +1 -1
  16. package/dist/extensions/seo/analyzers/images.js +153 -5
  17. package/dist/extensions/seo/analyzers/performance.d.ts +3 -3
  18. package/dist/extensions/seo/analyzers/performance.d.ts.map +1 -1
  19. package/dist/extensions/seo/analyzers/performance.js +77 -6
  20. package/dist/extensions/seo/analyzers/readability.d.ts.map +1 -1
  21. package/dist/extensions/seo/analyzers/readability.js +17 -9
  22. package/dist/extensions/seo/analyzers/schema.d.ts.map +1 -1
  23. package/dist/extensions/seo/analyzers/schema.js +99 -10
  24. package/dist/extensions/seo/analyzers/sitemap.d.ts.map +1 -1
  25. package/dist/extensions/seo/analyzers/sitemap.js +57 -16
  26. package/dist/extensions/seo/analyzers/technical.d.ts +0 -7
  27. package/dist/extensions/seo/analyzers/technical.d.ts.map +1 -1
  28. package/dist/extensions/seo/analyzers/technical.js +408 -37
  29. package/dist/extensions/seo/config.d.ts +2 -0
  30. package/dist/extensions/seo/config.d.ts.map +1 -1
  31. package/dist/extensions/seo/config.js +2 -0
  32. package/dist/extensions/seo/tools/batch.js +1 -1
  33. package/dist/extensions/seo/tools/content.d.ts.map +1 -1
  34. package/dist/extensions/seo/tools/content.js +5 -3
  35. package/dist/extensions/seo/tools/full-audit.d.ts +1 -1
  36. package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -1
  37. package/dist/extensions/seo/tools/full-audit.js +391 -66
  38. package/dist/extensions/seo/tools/helpers.d.ts +15 -4
  39. package/dist/extensions/seo/tools/helpers.d.ts.map +1 -1
  40. package/dist/extensions/seo/tools/helpers.js +57 -6
  41. package/dist/extensions/seo/tools/performance.d.ts.map +1 -1
  42. package/dist/extensions/seo/tools/performance.js +17 -11
  43. package/dist/extensions/seo/tools/report.d.ts.map +1 -1
  44. package/dist/extensions/seo/tools/report.js +513 -47
  45. package/dist/extensions/seo/types.d.ts +295 -3
  46. package/dist/extensions/seo/types.d.ts.map +1 -1
  47. package/dist/extensions/seo/utils/fetcher.d.ts +4 -0
  48. package/dist/extensions/seo/utils/fetcher.d.ts.map +1 -1
  49. package/dist/extensions/seo/utils/fetcher.js +77 -11
  50. package/dist/extensions/seo/utils/parser.d.ts +3 -1
  51. package/dist/extensions/seo/utils/parser.d.ts.map +1 -1
  52. package/dist/extensions/seo/utils/parser.js +105 -25
  53. package/dist/extensions/seo/utils/renderer.d.ts +6 -1
  54. package/dist/extensions/seo/utils/renderer.d.ts.map +1 -1
  55. package/dist/extensions/seo/utils/renderer.js +207 -1
  56. package/dist/extensions/seo/utils/url-safety.d.ts.map +1 -1
  57. package/dist/extensions/seo/utils/url-safety.js +19 -3
  58. package/dist/extensions/web/tools/fetch.js +2 -2
  59. package/dist/memory/tools/compact-context.d.ts.map +1 -1
  60. package/dist/memory/tools/compact-context.js +10 -3
  61. package/dist/sdk/agent-core/agent-loop.d.ts.map +1 -1
  62. package/dist/sdk/agent-core/agent-loop.js +87 -4
  63. package/dist/sdk/agent-core/types.d.ts +1 -0
  64. package/dist/sdk/agent-core/types.d.ts.map +1 -1
  65. package/dist/sdk/ai/utils/validation.d.ts +0 -7
  66. package/dist/sdk/ai/utils/validation.d.ts.map +1 -1
  67. package/dist/sdk/ai/utils/validation.js +21 -1
  68. package/dist/server/session-stream.d.ts.map +1 -1
  69. package/dist/server/session-stream.js +41 -0
  70. package/dist/server/storage.d.ts +9 -0
  71. package/dist/server/storage.d.ts.map +1 -1
  72. package/dist/server/storage.js +35 -9
  73. package/dist/server/wire.d.ts +2 -0
  74. package/dist/server/wire.d.ts.map +1 -1
  75. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/utils/parser.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAMV,QAAQ,EAGT,MAAM,aAAa,CAAC;AAgGrB,8CAA8C;AAC9C,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,CA4IhF;AAED,uDAAuD;AACvD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED,oDAAoD;AACpD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAerD"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/utils/parser.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAMV,QAAQ,EAGT,MAAM,aAAa,CAAC;AA0HrB,8CAA8C;AAC9C,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,CAuKhF;AAED,uDAAuD;AACvD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKhD;AACD,mEAAmE;AACnE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAavD;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAwBrD"}
@@ -45,6 +45,18 @@ function resolveUrl(href, baseUrl) {
45
45
  }
46
46
  }
47
47
  /** Parse JSON-LD blocks from the HTML. */
48
+ /** Expand JSON-LD @graph arrays into individual entities. */
49
+ function flattenGraph(item) {
50
+ if (!item || typeof item !== "object")
51
+ return [item];
52
+ const obj = item;
53
+ const graph = obj["@graph"];
54
+ if (graph === undefined)
55
+ return [item];
56
+ if (Array.isArray(graph))
57
+ return graph;
58
+ return [graph];
59
+ }
48
60
  function parseJsonLd($, baseUrl) {
49
61
  const blocks = [];
50
62
  $('script[type="application/ld+json"]').each((_, el) => {
@@ -55,25 +67,41 @@ function parseJsonLd($, baseUrl) {
55
67
  const parsed = JSON.parse(raw);
56
68
  const items = Array.isArray(parsed) ? parsed : [parsed];
57
69
  for (const item of items) {
58
- const type = item?.["@type"] ?? "Unknown";
59
- const issues = [];
60
- if (!item?.["@context"])
61
- issues.push("Missing @context");
62
- if (typeof type === "string" && type.includes("/")) {
63
- // Could be a full URL type — that's fine.
64
- }
65
- // Check for relative URLs.
66
- const jsonStr = JSON.stringify(item);
67
- if (jsonStr.includes('"url": "/') || jsonStr.includes('"url":"/')) {
68
- issues.push("Contains relative URL (should be absolute)");
70
+ // Expand @graph entries into individual blocks.
71
+ const expanded = flattenGraph(item);
72
+ for (const entity of expanded) {
73
+ if (!entity || typeof entity !== "object") {
74
+ blocks.push({
75
+ type: "Invalid",
76
+ format: "json-ld",
77
+ raw,
78
+ parsed: entity,
79
+ issues: ["Invalid schema object (not an object)"],
80
+ });
81
+ continue;
82
+ }
83
+ const e = entity;
84
+ const rawType = e["@type"];
85
+ const type = typeof rawType === "string"
86
+ ? rawType.replace("https://schema.org/", "")
87
+ : Array.isArray(rawType) && rawType.length > 0
88
+ ? rawType[0].replace("https://schema.org/", "")
89
+ : "Unknown";
90
+ const issues = [];
91
+ if (!e["@context"])
92
+ issues.push("Missing @context");
93
+ const jsonStr = JSON.stringify(e);
94
+ if (/("url"\s*:\s*"\/)[^/]/.test(jsonStr)) {
95
+ issues.push("Contains relative URL (should be absolute)");
96
+ }
97
+ blocks.push({
98
+ type,
99
+ format: "json-ld",
100
+ raw,
101
+ parsed: entity,
102
+ issues,
103
+ });
69
104
  }
70
- blocks.push({
71
- type: typeof type === "string" ? type : "Composite",
72
- format: "json-ld",
73
- raw,
74
- parsed: item,
75
- issues,
76
- });
77
105
  }
78
106
  }
79
107
  catch {
@@ -155,16 +183,40 @@ export function parsePage(html, url, baseUrl) {
155
183
  const src = $(el).attr("src") ?? $(el).attr("data-src") ?? "";
156
184
  if (!src || src.startsWith("data:"))
157
185
  return;
186
+ const resolvedSrc = resolveUrl(src, effectiveBaseUrl);
187
+ const rawAlt = $(el).attr("alt");
188
+ const altValue = rawAlt ?? "";
189
+ const hasAltAttribute = rawAlt !== undefined;
190
+ const role = $(el).attr("role");
191
+ const ariaHidden = $(el).attr("aria-hidden");
158
192
  images.push({
159
- src: resolveUrl(src, effectiveBaseUrl),
160
- alt: $(el).attr("alt") ?? "",
193
+ src: resolvedSrc,
194
+ alt: altValue,
161
195
  width: $(el).attr("width"),
162
196
  height: $(el).attr("height"),
163
197
  loading: $(el).attr("loading"),
164
198
  lazyMethod: detectLazyMethod($, el),
165
199
  fetchpriority: $(el).attr("fetchpriority"),
200
+ srcset: $(el).attr("srcset"),
201
+ sizes: $(el).attr("sizes"),
202
+ decoding: $(el).attr("decoding"),
203
+ isDecorative: (hasAltAttribute && altValue === "") || role === "presentation" || ariaHidden === "true",
204
+ isInPicture: $(el).parent("picture").length > 0,
205
+ isSvg: resolvedSrc.endsWith(".svg") || resolvedSrc.startsWith("data:image/svg"),
166
206
  });
167
207
  });
208
+ const pictureCount = $("picture").length;
209
+ let svgCount = 0;
210
+ let svgMissingTitle = 0;
211
+ $("svg").each((_, el) => {
212
+ svgCount++;
213
+ const hasTitle = $(el).find("title").length > 0;
214
+ const hasRoleImg = $(el).attr("role") === "img";
215
+ const hasAriaLabel = $(el).attr("aria-label");
216
+ if (!hasTitle && !hasRoleImg && !hasAriaLabel) {
217
+ svgMissingTitle++;
218
+ }
219
+ });
168
220
  // Links
169
221
  const links = [];
170
222
  let internalLinkCount = 0;
@@ -233,6 +285,9 @@ export function parsePage(html, url, baseUrl) {
233
285
  language,
234
286
  viewport,
235
287
  charset,
288
+ pictureCount,
289
+ svgCount,
290
+ svgMissingTitle,
236
291
  };
237
292
  }
238
293
  /** Extract the visible text content from HTML body. */
@@ -242,7 +297,22 @@ export function extractText(html) {
242
297
  $("script, style, noscript").remove();
243
298
  return $("body").text().replace(/\s+/g, " ").trim();
244
299
  }
245
- /** Extract raw JSON-LD blocks as parsed objects. */
300
+ /** Extract main content text, skipping nav/footer/aside/header. */
301
+ export function extractContentText(html) {
302
+ const $ = cheerio.load(html);
303
+ $("nav, footer, aside, header").remove();
304
+ $("script, style, noscript").remove();
305
+ const main = $("main");
306
+ const article = $("article");
307
+ if (article.length > 0) {
308
+ return article.text().replace(/\s+/g, " ").trim();
309
+ }
310
+ if (main.length > 0) {
311
+ return main.text().replace(/\s+/g, " ").trim();
312
+ }
313
+ return $("body").text().replace(/\s+/g, " ").trim();
314
+ }
315
+ /** Extract raw JSON-LD blocks as parsed objects, expanding @graph arrays. */
246
316
  export function extractJsonLd(html) {
247
317
  const $ = cheerio.load(html);
248
318
  const results = [];
@@ -252,10 +322,20 @@ export function extractJsonLd(html) {
252
322
  return;
253
323
  try {
254
324
  const parsed = JSON.parse(raw);
255
- if (Array.isArray(parsed))
256
- results.push(...parsed);
257
- else
258
- results.push(parsed);
325
+ const items = Array.isArray(parsed) ? parsed : [parsed];
326
+ for (const item of items) {
327
+ const obj = item;
328
+ if (obj && typeof obj === "object" && "@graph" in obj) {
329
+ const graph = obj["@graph"];
330
+ if (Array.isArray(graph))
331
+ results.push(...graph);
332
+ else
333
+ results.push(graph);
334
+ }
335
+ else {
336
+ results.push(item);
337
+ }
338
+ }
259
339
  }
260
340
  catch {
261
341
  // skip invalid JSON
@@ -6,7 +6,7 @@
6
6
  * the SEO extension self-contained.
7
7
  */
8
8
  import type { SeoConfig } from "../config.js";
9
- import type { FetchResult } from "../types.js";
9
+ import type { FetchResult, MeasuredPerformance } from "../types.js";
10
10
  export interface RenderOptions {
11
11
  viewport?: "desktop" | "mobile";
12
12
  timeout?: number;
@@ -15,8 +15,12 @@ export interface RenderOptions {
15
15
  captureA11y?: boolean;
16
16
  /** Whether to capture console errors. */
17
17
  captureConsole?: boolean;
18
+ /** Whether to capture Web Vitals and real browser performance metrics. */
19
+ capturePerformance?: boolean;
18
20
  /** User agent override. */
19
21
  userAgent?: string;
22
+ /** Optional AbortSignal to cancel an in-flight render. */
23
+ signal?: AbortSignal;
20
24
  }
21
25
  export interface RenderResult extends FetchResult {
22
26
  isSpa: boolean;
@@ -24,6 +28,7 @@ export interface RenderResult extends FetchResult {
24
28
  consoleErrors: string[];
25
29
  renderMs: number;
26
30
  accessibilityTree: unknown | null;
31
+ measuredPerformance?: MeasuredPerformance;
27
32
  }
28
33
  /** Close the shared browser instance (called on session shutdown). */
29
34
  export declare function closeRenderer(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/utils/renderer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,aAAa,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yCAAyC;IACzC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,2BAA2B;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAa,SAAQ,WAAW;IAC/C,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC;AAeD,sEAAsE;AACtE,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAUnD;AAED,sDAAsD;AACtD,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO5D;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,EACjB,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CA8JvB"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/utils/renderer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAe,MAAM,aAAa,CAAC;AAEjF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yCAAyC;IACzC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,0EAA0E;IAC1E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,2BAA2B;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,YAAa,SAAQ,WAAW;IAC/C,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C;AAeD,sEAAsE;AACtE,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAUnD;AAED,sDAAsD;AACtD,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO5D;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,EACjB,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CAyMvB"}
@@ -42,15 +42,23 @@ export async function renderPage(url, config, opts = {}) {
42
42
  const timeout = opts.timeout ?? config.timeout;
43
43
  const blockResources = opts.blockResources ?? config.blockRenderResources;
44
44
  const captureA11y = opts.captureA11y ?? false;
45
+ const capturePerformance = opts.capturePerformance ?? false;
45
46
  const captureConsole = opts.captureConsole ?? true;
46
47
  const startTime = Date.now();
48
+ if (opts.signal?.aborted) {
49
+ throw new Error("Render aborted before launch.");
50
+ }
47
51
  const browser = await getBrowser();
52
+ if (opts.signal?.aborted) {
53
+ throw new Error("Render aborted before context creation.");
54
+ }
48
55
  const context = await browser.newContext({
49
56
  viewport: viewport === "mobile" ? MOBILE_VIEWPORT : DESKTOP_VIEWPORT,
50
57
  userAgent: opts.userAgent ?? config.userAgent,
51
58
  });
52
59
  // Block resource types to speed up rendering.
53
- if (blockResources.length > 0) {
60
+ // When capturing performance, we need all resources to load for real metrics.
61
+ if (!capturePerformance && blockResources.length > 0) {
54
62
  await context.route("**/*", (route) => {
55
63
  const resourceType = route.request().resourceType();
56
64
  if (blockResources.includes(resourceType)) {
@@ -74,6 +82,26 @@ export async function renderPage(url, config, opts = {}) {
74
82
  consoleErrors.push(err.message);
75
83
  });
76
84
  }
85
+ // Track failed requests for performance audit.
86
+ const failedRequests = [];
87
+ if (capturePerformance) {
88
+ page.on("requestfailed", (request) => {
89
+ failedRequests.push({
90
+ url: request.url(),
91
+ status: 0,
92
+ type: request.resourceType(),
93
+ });
94
+ });
95
+ page.on("response", (response) => {
96
+ if (response.status() >= 400) {
97
+ failedRequests.push({
98
+ url: response.url(),
99
+ status: response.status(),
100
+ type: response.request().resourceType(),
101
+ });
102
+ }
103
+ });
104
+ }
77
105
  let response = null;
78
106
  let finalUrl = url;
79
107
  try {
@@ -81,6 +109,9 @@ export async function renderPage(url, config, opts = {}) {
81
109
  waitUntil: "networkidle",
82
110
  timeout,
83
111
  });
112
+ if (opts.signal?.aborted) {
113
+ throw new Error("Render aborted after navigation.");
114
+ }
84
115
  if (response) {
85
116
  // Track redirects by walking the redirectedFrom() chain.
86
117
  let current = response.request().redirectedFrom();
@@ -113,6 +144,7 @@ export async function renderPage(url, config, opts = {}) {
113
144
  consoleErrors,
114
145
  renderMs: Date.now() - startTime,
115
146
  accessibilityTree: null,
147
+ measuredPerformance: undefined,
116
148
  };
117
149
  }
118
150
  const rawContent = (await response?.text()) ?? "";
@@ -120,6 +152,15 @@ export async function renderPage(url, config, opts = {}) {
120
152
  // SPA heuristic: if the rendered DOM is significantly larger than the raw
121
153
  // initial HTML, content was likely injected by JavaScript.
122
154
  const isSpa = rawContent.length > 0 && content.length > rawContent.length * 1.5;
155
+ let measuredPerformance;
156
+ if (capturePerformance) {
157
+ try {
158
+ measuredPerformance = await capturePagePerformance(page, failedRequests);
159
+ }
160
+ catch {
161
+ measuredPerformance = undefined;
162
+ }
163
+ }
123
164
  let accessibilityTree = null;
124
165
  if (captureA11y) {
125
166
  try {
@@ -186,5 +227,170 @@ export async function renderPage(url, config, opts = {}) {
186
227
  consoleErrors,
187
228
  renderMs,
188
229
  accessibilityTree,
230
+ measuredPerformance,
189
231
  };
190
232
  }
233
+ /**
234
+ * Web Vitals rating thresholds (Google's CWV thresholds).
235
+ */
236
+ const LCP_GOOD = 2500;
237
+ const LCP_POOR = 4000;
238
+ const CLS_GOOD = 0.1;
239
+ const CLS_POOR = 0.25;
240
+ const TBT_GOOD = 200;
241
+ const TBT_POOR = 600;
242
+ const FCP_GOOD = 1800;
243
+ const FCP_POOR = 3000;
244
+ const TTFB_GOOD = 800;
245
+ const TTFB_POOR = 1800;
246
+ function vitalRating(value, good, poor) {
247
+ if (value <= good)
248
+ return "good";
249
+ if (value <= poor)
250
+ return "needs-improvement";
251
+ return "poor";
252
+ }
253
+ async function capturePagePerformance(page, failedRequests) {
254
+ // Capture navigation and resource timings.
255
+ const timings = await page.evaluate(() => {
256
+ const nav = performance.getEntriesByType("navigation")[0];
257
+ const resources = performance.getEntriesByType("resource");
258
+ const navTiming = nav
259
+ ? {
260
+ dnsMs: Math.round(nav.domainLookupEnd - nav.domainLookupStart),
261
+ tcpMs: Math.round(nav.connectEnd - nav.connectStart),
262
+ sslMs: Math.round(nav.secureConnectionStart > 0 ? nav.connectEnd - nav.secureConnectionStart : 0),
263
+ ttfbMs: Math.round(nav.responseStart - nav.requestStart),
264
+ downloadMs: Math.round(nav.responseEnd - nav.responseStart),
265
+ }
266
+ : undefined;
267
+ const totalTransfer = resources.reduce((s, r) => s + (r.transferSize || 0), 0);
268
+ const heavyResources = resources
269
+ .filter((r) => (r.transferSize || 0) > 50000)
270
+ .sort((a, b) => (b.transferSize || 0) - (a.transferSize || 0))
271
+ .slice(0, 10)
272
+ .map((r) => ({
273
+ url: r.name,
274
+ type: r.initiatorType,
275
+ transferSize: r.transferSize || 0,
276
+ encodedSize: r.encodedBodySize || 0,
277
+ }));
278
+ return {
279
+ navTiming,
280
+ totalTransferKb: Math.round(totalTransfer / 1024),
281
+ resourceCount: resources.length,
282
+ heavyResources,
283
+ };
284
+ });
285
+ // Capture Web Vitals via PerformanceObserver.
286
+ const webVitals = await page.evaluate(async () => {
287
+ return new Promise((resolve) => {
288
+ const results = {};
289
+ // LCP
290
+ const lcpObs = new PerformanceObserver((list) => {
291
+ const entries = list.getEntries();
292
+ if (entries.length > 0) {
293
+ const last = entries[entries.length - 1];
294
+ const el = last.element;
295
+ results.lcp = {
296
+ value: Math.round(last.startTime),
297
+ element: el ? {
298
+ tagName: el.tagName,
299
+ id: el.id || undefined,
300
+ src: el.src || el.currentSrc || undefined,
301
+ renderTime: last.renderTime || last.startTime,
302
+ size: `${el.width || el.naturalWidth || 0}x${el.height || el.naturalHeight || 0}`,
303
+ } : undefined,
304
+ };
305
+ }
306
+ lcpObs.disconnect();
307
+ });
308
+ lcpObs.observe({ type: "largest-contentful-paint", buffered: true });
309
+ // CLS
310
+ let clsValue = 0;
311
+ const layoutShifts = [];
312
+ const clsObs = new PerformanceObserver((list) => {
313
+ for (const entry of list.getEntries()) {
314
+ if (!entry.hadRecentInput)
315
+ clsValue += entry.value;
316
+ layoutShifts.push({ value: entry.value, nodeCount: entry.sources?.length || 0 });
317
+ }
318
+ });
319
+ clsObs.observe({ type: "layout-shift", buffered: true });
320
+ // Long tasks
321
+ const longTasks = [];
322
+ const longTaskDetails = [];
323
+ const ltObs = new PerformanceObserver((list) => {
324
+ for (const entry of list.getEntries()) {
325
+ longTasks.push({ duration: entry.duration });
326
+ longTaskDetails.push({
327
+ startTime: entry.startTime,
328
+ duration: entry.duration,
329
+ attribution: entry.attribution?.[0]?.containerSrc || entry.attribution?.[0]?.containerName || undefined,
330
+ });
331
+ }
332
+ });
333
+ ltObs.observe({ type: "longtask", buffered: true });
334
+ // FCP
335
+ const fcpObs = new PerformanceObserver((list) => {
336
+ const entries = list.getEntries();
337
+ if (entries.length > 0) {
338
+ results.fcp = { value: Math.round(entries[0].startTime) };
339
+ }
340
+ fcpObs.disconnect();
341
+ });
342
+ fcpObs.observe({ type: "paint", buffered: true });
343
+ // Wait 3 seconds for metrics to settle
344
+ setTimeout(() => {
345
+ clsObs.disconnect();
346
+ ltObs.disconnect();
347
+ results.cls = { value: Math.round(clsValue * 1000) / 1000 };
348
+ results.layoutShiftCount = layoutShifts.length;
349
+ results.layoutShiftSources = layoutShifts.slice(0, 5);
350
+ results.tbt = {
351
+ value: Math.round(longTasks.reduce((s, t) => s + Math.max(0, t.duration - 50), 0)),
352
+ };
353
+ results.longTasks = {
354
+ count: longTasks.length,
355
+ totalDurationMs: Math.round(longTasks.reduce((s, t) => s + t.duration, 0)),
356
+ details: longTaskDetails,
357
+ };
358
+ resolve(results);
359
+ }, 3000);
360
+ });
361
+ });
362
+ // Assemble MeasuredPerformance.
363
+ const mp = {
364
+ navigationTiming: timings.navTiming,
365
+ resourceCount: timings.resourceCount,
366
+ totalTransferKb: timings.totalTransferKb,
367
+ topHeavyResources: timings.heavyResources,
368
+ failedRequests: failedRequests.length > 0 ? failedRequests : undefined,
369
+ };
370
+ if (webVitals.lcp) {
371
+ mp.lcp = { value: webVitals.lcp.value, rating: vitalRating(webVitals.lcp.value, LCP_GOOD, LCP_POOR), element: webVitals.lcp.element };
372
+ }
373
+ if (webVitals.cls) {
374
+ mp.cls = { value: webVitals.cls.value, rating: vitalRating(webVitals.cls.value, CLS_GOOD, CLS_POOR) };
375
+ }
376
+ if (webVitals.fcp) {
377
+ mp.fcp = { value: webVitals.fcp.value, rating: vitalRating(webVitals.fcp.value, FCP_GOOD, FCP_POOR) };
378
+ }
379
+ if (webVitals.tbt) {
380
+ mp.tbt = { value: webVitals.tbt.value, rating: vitalRating(webVitals.tbt.value, TBT_GOOD, TBT_POOR) };
381
+ }
382
+ if (webVitals.longTasks) {
383
+ mp.longTasks = webVitals.longTasks;
384
+ }
385
+ if (webVitals.layoutShiftCount !== undefined) {
386
+ mp.layoutShiftCount = webVitals.layoutShiftCount;
387
+ mp.layoutShiftSources = webVitals.layoutShiftSources;
388
+ }
389
+ if (timings.navTiming) {
390
+ mp.ttfb = {
391
+ value: timings.navTiming.ttfbMs,
392
+ rating: vitalRating(timings.navTiming.ttfbMs, TTFB_GOOD, TTFB_POOR),
393
+ };
394
+ }
395
+ return mp;
396
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"url-safety.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/utils/url-safety.ts"],"names":[],"mappings":"AAUA,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAiDD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,GAAG,EAAE,MAAM,EAAE,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;IAAE,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5D,OAAO,CAAC,YAAY,CAAC,CA8EvB;AAED,+DAA+D;AAC/D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CA8B7D;AAED,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA2BhD;AAED,wDAAwD;AACxD,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3C"}
1
+ {"version":3,"file":"url-safety.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/utils/url-safety.ts"],"names":[],"mappings":"AAUA,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAiDD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,GAAG,EAAE,MAAM,EAAE,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;IAAE,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5D,OAAO,CAAC,YAAY,CAAC,CA8EvB;AAED,+DAA+D;AAC/D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CA8B7D;AAED,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA2ChD;AAED,wDAAwD;AACxD,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3C"}
@@ -168,7 +168,19 @@ export function validateUrlBasic(urlStr) {
168
168
  }
169
169
  /** Normalize a URL for baseline matching (strip UTM, sort params, lowercase host). */
170
170
  export function normalizeUrl(url) {
171
- const UTM_PARAMS = new Set(["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"]);
171
+ const TRACKING_PARAMS = new Set([
172
+ "utm_source",
173
+ "utm_medium",
174
+ "utm_campaign",
175
+ "utm_term",
176
+ "utm_content",
177
+ // Click-IDs and marketing tracking params beyond UTM.
178
+ "gclid",
179
+ "fbclid",
180
+ "msclkid",
181
+ "mc_eid",
182
+ "mc_cid",
183
+ ]);
172
184
  let parsed;
173
185
  try {
174
186
  parsed = new URL(url);
@@ -176,15 +188,19 @@ export function normalizeUrl(url) {
176
188
  catch {
177
189
  return url;
178
190
  }
191
+ // Always strip fragments (hash).
192
+ parsed.hash = "";
193
+ // Lowercase scheme and host.
194
+ parsed.protocol = parsed.protocol.toLowerCase();
179
195
  parsed.hostname = parsed.hostname.toLowerCase();
180
196
  // Strip default ports.
181
197
  if ((parsed.protocol === "http:" && parsed.port === "80") || (parsed.protocol === "https:" && parsed.port === "443")) {
182
198
  parsed.port = "";
183
199
  }
184
- // Remove UTM params.
200
+ // Remove tracking params.
185
201
  const params = new URLSearchParams(parsed.search);
186
202
  for (const key of [...params.keys()]) {
187
- if (UTM_PARAMS.has(key.toLowerCase())) {
203
+ if (TRACKING_PARAMS.has(key.toLowerCase())) {
188
204
  params.delete(key);
189
205
  }
190
206
  }
@@ -10,10 +10,10 @@ const MAX_OUTPUT_BYTES = 200 * 1024; // 200 KB
10
10
  const webFetchSchema = Type.Object({
11
11
  url: Type.String({
12
12
  description: "The URL to fetch. Must begin with http:// or https://.",
13
- pattern: "^https?://",
14
13
  }),
15
- format: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("markdown"), Type.Literal("html"), Type.Literal("json")], {
14
+ format: Type.Optional(Type.String({
16
15
  default: "markdown",
16
+ enum: ["text", "markdown", "html", "json"],
17
17
  description: "Output format: text, markdown (default), html, or json.",
18
18
  })),
19
19
  timeout: Type.Optional(Type.Number({
@@ -1 +1 @@
1
- {"version":3,"file":"compact-context.d.ts","sourceRoot":"","sources":["../../../src/memory/tools/compact-context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EASN,KAAK,gBAAgB,EACrB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAc,KAAK,YAAY,EAAyB,MAAM,iCAAiC,CAAC;AAKvG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAErD,eAAO,MAAM,yBAAyB,oBAAoB,CAAC;AAoO3D;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,OAAO,GAAE;IAC1D,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,iCAAiC,GAAG,wBAAwB,CAAC,CAAC;IACpF,MAAM,CAAC,EAAE,gBAAgB,CAAC;CACrB,GAAG,WAAW,CAAC,OAAO,uBAAuB,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC,CAiBrF;AA+MD,wFAAwF;AACxF,wBAAgB,sCAAsC,CACrD,eAAe,EAAE,YAAY,EAAE,EAC/B,MAAM,GAAE,gBAA4C,EACpD,gBAAgB,CAAC,EAAE,MAAM,GACvB,YAAY,EAAE,CAchB;AAsQD,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAkSpF"}
1
+ {"version":3,"file":"compact-context.d.ts","sourceRoot":"","sources":["../../../src/memory/tools/compact-context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EASN,KAAK,gBAAgB,EACrB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAc,KAAK,YAAY,EAAyB,MAAM,iCAAiC,CAAC;AAKvG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAErD,eAAO,MAAM,yBAAyB,oBAAoB,CAAC;AAoO3D;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,OAAO,GAAE;IAC1D,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,iCAAiC,GAAG,wBAAwB,CAAC,CAAC;IACpF,MAAM,CAAC,EAAE,gBAAgB,CAAC;CACrB,GAAG,WAAW,CAAC,OAAO,uBAAuB,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC,CAiBrF;AA+MD,wFAAwF;AACxF,wBAAgB,sCAAsC,CACrD,eAAe,EAAE,YAAY,EAAE,EAC/B,MAAM,GAAE,gBAA4C,EACpD,gBAAgB,CAAC,EAAE,MAAM,GACvB,YAAY,EAAE,CAchB;AAsQD,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CA0SpF"}
@@ -617,7 +617,17 @@ export function registerCompactContextTool(ext, runtime) {
617
617
  // 1. Explicit request (agent-driven tool call or turn-start auto-schedule).
618
618
  // These set state.request before this context event fires, so pruning
619
619
  // applies to the current provider request with no lag.
620
+ // Compute mid-run status first so we can promote a premature after_turn
621
+ // request (scheduled at before_agent_start while the agent was transiently
622
+ // idle) to request_time when the agent is now actively mid-run.
623
+ const isMidRun = _event.providerRequest === true || hasPendingModelRequest(ctx);
620
624
  const request = state.request;
625
+ if (request && request.mode === "after_turn" && isMidRun) {
626
+ notifyDcp(ctx, "DCP-lite after-turn compaction promoted to request-time pruning — agent is mid-run", "info", {
627
+ version: 1, event: "scheduled", source: request.source, mode: "request_time", scope: request.scope, timestamp: Date.now(),
628
+ });
629
+ request.mode = "request_time";
630
+ }
621
631
  if (request && request.mode === "request_time") {
622
632
  state.request = null;
623
633
  if (runtime.compactInFlight) {
@@ -653,9 +663,6 @@ export function registerCompactContextTool(ext, runtime) {
653
663
  // The context hook is the last zero-lag seam before conversion/provider send.
654
664
  // Treat an explicit provider-preparation event as in-run even if the session
655
665
  // state has not flipped to streaming/pending yet.
656
- const isMidRun = _event.providerRequest === true || hasPendingModelRequest(ctx);
657
- if (!isMidRun)
658
- return;
659
666
  const autoSettings = getAutoOlderHistoryRequestSettings(policy);
660
667
  const autoRawTokens = safeRawTokensForContext(ctx);
661
668
  const autoUsage = ctx.getContextUsage();
@@ -1 +1 @@
1
- {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../../../src/sdk/agent-core/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGL,WAAW,EAIZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,eAAe,EACf,YAAY,EAIZ,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEzE;;;GAGG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GAClB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAsBzC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GAClB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CA6BzC;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBzB;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,CAAC,YAAY,EAAE,CAAC,CAiBzB"}
1
+ {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../../../src/sdk/agent-core/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGL,WAAW,EAIZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,eAAe,EACf,YAAY,EAIZ,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAYzE;;;GAGG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GAClB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CA2BzC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GAClB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAkCzC;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBzB;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,CAAC,YAAY,EAAE,CAAC,CAiBzB"}