framewatch-mcp-server 0.1.1 → 0.2.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.
Files changed (127) hide show
  1. package/README.md +797 -8
  2. package/dist/constants.d.ts +264 -0
  3. package/dist/constants.js +268 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/engine/browser.js +4 -0
  6. package/dist/engine/browser.js.map +1 -1
  7. package/dist/engine/clicks.d.ts +221 -0
  8. package/dist/engine/clicks.js +801 -0
  9. package/dist/engine/clicks.js.map +1 -0
  10. package/dist/engine/forms.d.ts +137 -0
  11. package/dist/engine/forms.js +474 -0
  12. package/dist/engine/forms.js.map +1 -0
  13. package/dist/engine/hmr.d.ts +41 -0
  14. package/dist/engine/hmr.js +91 -0
  15. package/dist/engine/hmr.js.map +1 -0
  16. package/dist/engine/inspect.d.ts +31 -0
  17. package/dist/engine/inspect.js +383 -0
  18. package/dist/engine/inspect.js.map +1 -0
  19. package/dist/engine/interaction.d.ts +6 -1
  20. package/dist/engine/interaction.js +21 -6
  21. package/dist/engine/interaction.js.map +1 -1
  22. package/dist/engine/links.d.ts +134 -0
  23. package/dist/engine/links.js +384 -0
  24. package/dist/engine/links.js.map +1 -0
  25. package/dist/engine/mocks.d.ts +53 -0
  26. package/dist/engine/mocks.js +148 -0
  27. package/dist/engine/mocks.js.map +1 -0
  28. package/dist/engine/rtl.d.ts +129 -0
  29. package/dist/engine/rtl.js +540 -0
  30. package/dist/engine/rtl.js.map +1 -0
  31. package/dist/engine/seo.d.ts +189 -0
  32. package/dist/engine/seo.js +398 -0
  33. package/dist/engine/seo.js.map +1 -0
  34. package/dist/engine/snapshot.d.ts +29 -0
  35. package/dist/engine/snapshot.js +10 -0
  36. package/dist/engine/snapshot.js.map +1 -0
  37. package/dist/engine/vue.d.ts +54 -0
  38. package/dist/engine/vue.js +419 -0
  39. package/dist/engine/vue.js.map +1 -0
  40. package/dist/index.js +42 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/tools/accessibility.js +4 -3
  43. package/dist/tools/accessibility.js.map +1 -1
  44. package/dist/tools/api-mock.d.ts +405 -0
  45. package/dist/tools/api-mock.js +186 -0
  46. package/dist/tools/api-mock.js.map +1 -0
  47. package/dist/tools/capture.d.ts +83 -23
  48. package/dist/tools/capture.js +105 -60
  49. package/dist/tools/capture.js.map +1 -1
  50. package/dist/tools/compare.js +4 -3
  51. package/dist/tools/compare.js.map +1 -1
  52. package/dist/tools/dead-clicks.d.ts +128 -0
  53. package/dist/tools/dead-clicks.js +570 -0
  54. package/dist/tools/dead-clicks.js.map +1 -0
  55. package/dist/tools/form-test.d.ts +112 -0
  56. package/dist/tools/form-test.js +477 -0
  57. package/dist/tools/form-test.js.map +1 -0
  58. package/dist/tools/index.d.ts +16 -1
  59. package/dist/tools/index.js +42 -1
  60. package/dist/tools/index.js.map +1 -1
  61. package/dist/tools/inspect.d.ts +78 -0
  62. package/dist/tools/inspect.js +136 -0
  63. package/dist/tools/inspect.js.map +1 -0
  64. package/dist/tools/interact.d.ts +29 -16
  65. package/dist/tools/interact.js +99 -13
  66. package/dist/tools/interact.js.map +1 -1
  67. package/dist/tools/links.d.ts +129 -0
  68. package/dist/tools/links.js +640 -0
  69. package/dist/tools/links.js.map +1 -0
  70. package/dist/tools/responsive.d.ts +6 -6
  71. package/dist/tools/responsive.js +9 -9
  72. package/dist/tools/responsive.js.map +1 -1
  73. package/dist/tools/rtl.d.ts +241 -0
  74. package/dist/tools/rtl.js +410 -0
  75. package/dist/tools/rtl.js.map +1 -0
  76. package/dist/tools/save-auth.d.ts +22 -22
  77. package/dist/tools/screenshot.js +13 -5
  78. package/dist/tools/screenshot.js.map +1 -1
  79. package/dist/tools/seo.d.ts +113 -0
  80. package/dist/tools/seo.js +281 -0
  81. package/dist/tools/seo.js.map +1 -0
  82. package/dist/tools/snapshot.d.ts +122 -0
  83. package/dist/tools/snapshot.js +183 -0
  84. package/dist/tools/snapshot.js.map +1 -0
  85. package/dist/tools/wait-for.d.ts +107 -0
  86. package/dist/tools/wait-for.js +167 -0
  87. package/dist/tools/wait-for.js.map +1 -0
  88. package/dist/utils/arabic-text.d.ts +14 -0
  89. package/dist/utils/arabic-text.js +193 -0
  90. package/dist/utils/arabic-text.js.map +1 -0
  91. package/dist/utils/budget.d.ts +41 -0
  92. package/dist/utils/budget.js +182 -0
  93. package/dist/utils/budget.js.map +1 -0
  94. package/dist/utils/format.d.ts +11 -1
  95. package/dist/utils/format.js +27 -4
  96. package/dist/utils/format.js.map +1 -1
  97. package/dist/utils/highlight.d.ts +69 -0
  98. package/dist/utils/highlight.js +181 -0
  99. package/dist/utils/highlight.js.map +1 -0
  100. package/dist/utils/link-rules.d.ts +100 -0
  101. package/dist/utils/link-rules.js +284 -0
  102. package/dist/utils/link-rules.js.map +1 -0
  103. package/dist/utils/mock-rules.d.ts +144 -0
  104. package/dist/utils/mock-rules.js +224 -0
  105. package/dist/utils/mock-rules.js.map +1 -0
  106. package/dist/utils/rtl-rules.d.ts +142 -0
  107. package/dist/utils/rtl-rules.js +296 -0
  108. package/dist/utils/rtl-rules.js.map +1 -0
  109. package/dist/utils/seo-rules.d.ts +129 -0
  110. package/dist/utils/seo-rules.js +726 -0
  111. package/dist/utils/seo-rules.js.map +1 -0
  112. package/dist/utils/snapshot-rules.d.ts +33 -0
  113. package/dist/utils/snapshot-rules.js +111 -0
  114. package/dist/utils/snapshot-rules.js.map +1 -0
  115. package/dist/utils/storage-state.d.ts +42 -1
  116. package/dist/utils/storage-state.js +115 -4
  117. package/dist/utils/storage-state.js.map +1 -1
  118. package/dist/utils/style-rules.d.ts +107 -0
  119. package/dist/utils/style-rules.js +223 -0
  120. package/dist/utils/style-rules.js.map +1 -0
  121. package/dist/utils/test-data.d.ts +75 -0
  122. package/dist/utils/test-data.js +294 -0
  123. package/dist/utils/test-data.js.map +1 -0
  124. package/dist/utils/vue-rules.d.ts +72 -0
  125. package/dist/utils/vue-rules.js +108 -0
  126. package/dist/utils/vue-rules.js.map +1 -0
  127. package/package.json +6 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/tools/snapshot.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n DEFAULT_SNAPSHOT_MAX_CHARS,\n DEFAULT_SNAPSHOT_WAIT_MS,\n MAX_SNAPSHOT_MAX_CHARS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n VUE_READY_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { getSessionPage, withSessionLock, type SessionOptions } from \"../engine/browser.js\";\nimport { hmrFor } from \"../engine/hmr.js\";\nimport { takeSnapshot, type PageSnapshot } from \"../engine/snapshot.js\";\nimport { componentTree, detectVue, waitForVueReady } from \"../engine/vue.js\";\nimport { describeVue, formatComponentTree, type VueInfo } from \"../utils/vue-rules.js\";\nimport { describeScale } from \"../utils/format.js\";\nimport { resizeForOutput, toBase64 } from \"../utils/image.js\";\nimport { loginFormVisible, resolveStorageState, storageStateField, withAuthNote, type ResolvedAuth } from \"../utils/storage-state.js\";\nimport type { Viewport } from \"../types.js\";\n\nexport const SNAPSHOT_TOOL_NAME = \"framewatch_snapshot\";\n\nexport const snapshotInputShape = {\n url: z\n .string()\n .url()\n .optional()\n .describe(\"Open this URL first. Omit to read the page left open by framewatch_interact / framewatch_inspect.\"),\n selector: z.string().optional().describe(\"Only this container, e.g. `main` or `#checkout`\"),\n mode: z\n .enum([\"full\", \"interactive\"])\n .default(\"full\")\n .describe(\"`full` is the whole tree with headings and text; `interactive` is a flat list of what can be clicked, typed into or chosen\"),\n max_chars: z\n .number()\n .int()\n .min(200)\n .max(MAX_SNAPSHOT_MAX_CHARS)\n .default(DEFAULT_SNAPSHOT_MAX_CHARS)\n .describe(\"Cut the tree past this many characters (a note says how many lines went)\"),\n include_screenshot: z.boolean().default(false).describe(\"Also return a screenshot of the page as it was read\"),\n include_components: z\n .boolean()\n .default(false)\n .describe(\"On a Vue app: also return the component tree (names and nesting) from the root\"),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_SNAPSHOT_WAIT_MS)\n .describe(\n \"Settle time (ms) after opening `url`. On a Vue app this is a ceiling: the page is read as soon as the app \" +\n \"is mounted and its router ready\",\n ),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) before reading\"),\n wait_for_timeout_ms: z.number().int().min(1).default(SELECTOR_TIMEOUT_MS).describe(\"Max time (ms) to wait for `wait_for`\"),\n viewport: z\n .object({\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT),\n })\n .optional()\n .describe(\"Resize the page to this first (defaults to leaving it as it is)\"),\n storage_state: storageStateField,\n};\n\nexport const snapshotInputSchema = z.object(snapshotInputShape);\nexport type SnapshotInput = z.input<typeof snapshotInputSchema>;\n\n/**\n * Read the page as a tree of named elements, on the interact session's page so\n * the refs it hands out are the ones the next `framewatch_interact` or\n * `framewatch_inspect` can use.\n */\nexport function snapshotPage(rawInput: SnapshotInput): Promise<CallToolResult> {\n return withSessionLock(() => runSnapshot(rawInput));\n}\n\nasync function runSnapshot(rawInput: SnapshotInput): Promise<CallToolResult> {\n const parsed = snapshotInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Snapshot failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n try {\n const opened = await openSessionPage(input, \"Snapshot\");\n if (\"error\" in opened) return errorResult(opened.error);\n const { page, vue, auth, login_visible } = opened;\n\n const snapshot = await takeSnapshot(page, {\n ...(input.selector ? { selector: input.selector } : {}),\n mode: input.mode,\n max_chars: input.max_chars,\n });\n\n const viewport = page.viewportSize() ?? { width: 0, height: 0 };\n const title = await page.title().catch(() => \"\");\n const lines = [\n `Snapshot of ${page.url()}${title ? ` — \"${title}\"` : \"\"} — ${describeScale(viewport).replace(/, images.*$/, \"\")} — ${describeCounts(snapshot)}${vue ? ` — ${describeVue(vue)}` : \"\"}`,\n ...(input.include_screenshot ? [describeScale(viewport)] : []),\n ...(input.selector ? [`Scope: ${input.selector}`] : []),\n REF_HINT,\n \"\",\n snapshot.text,\n ...(snapshot.cut_lines > 0\n ? [`… ${snapshot.cut_lines} more lines cut — raise \\`max_chars\\`, pass \\`selector\\`, or use mode \"interactive\".`]\n : []),\n ];\n if (input.include_components) {\n lines.push(\"\", ...(await describeComponents(page, vue)));\n }\n\n const content: CallToolResult[\"content\"] = [{ type: \"text\", text: lines.join(\"\\n\") }];\n if (input.include_screenshot) {\n const png = await page.screenshot({ type: \"png\" });\n content.unshift({ type: \"image\", data: toBase64(await resizeForOutput(png)), mimeType: \"image/png\" });\n }\n hmrFor(page).markSeen();\n return withAuthNote({ content }, auth, login_visible);\n } catch (error) {\n return errorResult(describeSessionFailure(\"Snapshot\", input, error));\n }\n}\n\nconst REF_HINT =\n \"Refs: pass one as `ref` to framewatch_interact (to act on it) or framewatch_inspect (to measure it). \" +\n \"They stay valid until the page changes — snapshot again after an action that re-rendered. \" +\n 'In a framewatch_capture script, target the same element with a selector such as role=button[name=\"Sign in\"].';\n\nexport function describeCounts(snapshot: Pick<PageSnapshot, \"elements\" | \"interactive\">): string {\n return `${snapshot.elements} elements, ${snapshot.interactive} interactive`;\n}\n\n/** The component tree, or why there is none. */\nasync function describeComponents(page: Awaited<ReturnType<typeof getSessionPage>>[\"page\"], vue: VueInfo | null): Promise<string[]> {\n if (!vue) return [\"Components: no Vue app on this page\"];\n if (vue.major < 3) return [\"Components: Vue 2 — component details need Vue 3\"];\n if (vue.production) return [\"Components: production build — no component data on elements\"];\n const tree = await componentTree(page);\n if (!tree) return [\"Components: could not be read\"];\n return formatComponentTree(tree.root, tree.total);\n}\n\n/** Input the session-page tools share: where to open, how long to wait, what size. */\nexport interface SessionPageInput {\n url?: string;\n wait_ms?: number;\n wait_for?: string;\n wait_for_timeout_ms?: number;\n viewport?: Viewport;\n storage_state?: string;\n}\n\n/**\n * The session page, opened at `url` when one is given. Shared by snapshot and\n * inspect, which both read the page interact drives; `toolName` is only for\n * the error wording.\n */\nexport interface OpenedSessionPage {\n page: Awaited<ReturnType<typeof getSessionPage>>[\"page\"];\n vue: VueInfo | null;\n /** The saved auth in force for this call, if any. */\n auth: ResolvedAuth | null;\n /** Whether the page showed a login form after opening — only checked when `url` was given. */\n login_visible: boolean;\n}\n\nexport async function openSessionPage(input: SessionPageInput, toolName: string): Promise<OpenedSessionPage | { error: string }> {\n const options: SessionOptions = {};\n if (input.viewport) options.viewport = input.viewport;\n const auth = await resolveStorageState(input.storage_state);\n if (auth) options.storageState = { path: auth.path, state: auth.state };\n\n const { page, previousUrl } = await getSessionPage(options);\n const target = input.url ?? previousUrl;\n if (target !== undefined) {\n await page.goto(target, { waitUntil: \"load\", timeout: NAVIGATION_TIMEOUT_MS });\n if (input.wait_for) {\n await page.waitForSelector(input.wait_for, { state: \"visible\", timeout: input.wait_for_timeout_ms ?? SELECTOR_TIMEOUT_MS });\n }\n // `wait_ms` is a ceiling, not a sleep: a Vue app is read the moment it is\n // mounted and its router has resolved; a page without one gets the full wait.\n const { vue } = await waitForVueReady(page, { detect_ms: input.wait_ms ?? 0, ready_ms: VUE_READY_TIMEOUT_MS });\n if (page.url() === \"about:blank\") return { error: noPage(toolName) };\n return { page, vue, auth, login_visible: auth && input.url !== undefined ? await loginFormVisible(page) : false };\n }\n if (page.url() === \"about:blank\") return { error: noPage(toolName) };\n return { page, vue: await detectVue(page), auth, login_visible: false };\n}\n\nfunction noPage(toolName: string): string {\n return `${toolName} failed: no page is open yet — pass \\`url\\` to open one (e.g. http://localhost:3000).`;\n}\n\nexport function describeSessionFailure(toolName: string, input: { url?: string; wait_for?: string; wait_for_timeout_ms?: number }, error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return `${toolName} failed: Playwright's Chromium browser is not installed. Run \\`npx playwright install chromium\\` and try again. (${firstLine})`;\n }\n if (/^page\\.goto:/.test(message)) {\n return `${toolName} failed: could not open ${input.url ?? \"the page\"} — ${firstLine}`;\n }\n if (input.wait_for && /^page\\.waitForSelector:/.test(message)) {\n return `${toolName} failed: selector \"${input.wait_for}\" did not become visible within ${input.wait_for_timeout_ms ?? SELECTOR_TIMEOUT_MS}ms.`;\n }\n return `${toolName} failed: ${firstLine}`;\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\nexport function registerSnapshotTool(server: McpServer): void {\n server.registerTool(\n SNAPSHOT_TOOL_NAME,\n {\n title: \"Snapshot\",\n description:\n \"Read the page as a tree of named elements — every heading, field, button and link with its accessible name \" +\n \"and a short ref like `e8` — so you know what is on the page and what to call it instead of guessing selectors \" +\n \"from a screenshot. Pass a ref as `ref` to framewatch_interact to act on that element, or to framewatch_inspect \" +\n \"to measure it. Works on the page framewatch_interact keeps open; pass `url` to open one. \" +\n \"Use mode `interactive` for a short list of only what can be acted on.\",\n inputSchema: snapshotInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async (args) => snapshotPage(args),\n );\n}\n"]}
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4
+ export declare const WAIT_FOR_TOOL_NAME = "framewatch_wait_for";
5
+ export declare const waitForInputShape: {
6
+ url: z.ZodOptional<z.ZodString>;
7
+ until: z.ZodDefault<z.ZodEnum<["hot_update", "vue_ready", "selector", "network_idle"]>>;
8
+ selector: z.ZodOptional<z.ZodString>;
9
+ timeout_ms: z.ZodDefault<z.ZodNumber>;
10
+ include_screenshot: z.ZodDefault<z.ZodBoolean>;
11
+ include_snapshot: z.ZodDefault<z.ZodBoolean>;
12
+ wait_ms: z.ZodDefault<z.ZodNumber>;
13
+ viewport: z.ZodOptional<z.ZodObject<{
14
+ width: z.ZodNumber;
15
+ height: z.ZodNumber;
16
+ }, "strip", z.ZodTypeAny, {
17
+ width: number;
18
+ height: number;
19
+ }, {
20
+ width: number;
21
+ height: number;
22
+ }>>;
23
+ storage_state: z.ZodOptional<z.ZodString>;
24
+ };
25
+ export declare const waitForInputSchema: z.ZodEffects<z.ZodObject<{
26
+ url: z.ZodOptional<z.ZodString>;
27
+ until: z.ZodDefault<z.ZodEnum<["hot_update", "vue_ready", "selector", "network_idle"]>>;
28
+ selector: z.ZodOptional<z.ZodString>;
29
+ timeout_ms: z.ZodDefault<z.ZodNumber>;
30
+ include_screenshot: z.ZodDefault<z.ZodBoolean>;
31
+ include_snapshot: z.ZodDefault<z.ZodBoolean>;
32
+ wait_ms: z.ZodDefault<z.ZodNumber>;
33
+ viewport: z.ZodOptional<z.ZodObject<{
34
+ width: z.ZodNumber;
35
+ height: z.ZodNumber;
36
+ }, "strip", z.ZodTypeAny, {
37
+ width: number;
38
+ height: number;
39
+ }, {
40
+ width: number;
41
+ height: number;
42
+ }>>;
43
+ storage_state: z.ZodOptional<z.ZodString>;
44
+ }, "strip", z.ZodTypeAny, {
45
+ wait_ms: number;
46
+ timeout_ms: number;
47
+ include_screenshot: boolean;
48
+ include_snapshot: boolean;
49
+ until: "selector" | "hot_update" | "vue_ready" | "network_idle";
50
+ url?: string | undefined;
51
+ viewport?: {
52
+ width: number;
53
+ height: number;
54
+ } | undefined;
55
+ storage_state?: string | undefined;
56
+ selector?: string | undefined;
57
+ }, {
58
+ url?: string | undefined;
59
+ viewport?: {
60
+ width: number;
61
+ height: number;
62
+ } | undefined;
63
+ wait_ms?: number | undefined;
64
+ storage_state?: string | undefined;
65
+ timeout_ms?: number | undefined;
66
+ selector?: string | undefined;
67
+ include_screenshot?: boolean | undefined;
68
+ include_snapshot?: boolean | undefined;
69
+ until?: "selector" | "hot_update" | "vue_ready" | "network_idle" | undefined;
70
+ }>, {
71
+ wait_ms: number;
72
+ timeout_ms: number;
73
+ include_screenshot: boolean;
74
+ include_snapshot: boolean;
75
+ until: "selector" | "hot_update" | "vue_ready" | "network_idle";
76
+ url?: string | undefined;
77
+ viewport?: {
78
+ width: number;
79
+ height: number;
80
+ } | undefined;
81
+ storage_state?: string | undefined;
82
+ selector?: string | undefined;
83
+ }, {
84
+ url?: string | undefined;
85
+ viewport?: {
86
+ width: number;
87
+ height: number;
88
+ } | undefined;
89
+ wait_ms?: number | undefined;
90
+ storage_state?: string | undefined;
91
+ timeout_ms?: number | undefined;
92
+ selector?: string | undefined;
93
+ include_screenshot?: boolean | undefined;
94
+ include_snapshot?: boolean | undefined;
95
+ until?: "selector" | "hot_update" | "vue_ready" | "network_idle" | undefined;
96
+ }>;
97
+ export type WaitForInput = z.input<typeof waitForInputSchema>;
98
+ /**
99
+ * Wait for the session page to reach a state worth looking at, then look.
100
+ *
101
+ * The one that earns the tool its place is `hot_update`: after the agent
102
+ * saves a file, Vite patches the open page in place, and this returns the
103
+ * moment that patch has landed — no reload, no replayed flow, no guessed
104
+ * sleep. The other conditions replace the guessed sleeps elsewhere.
105
+ */
106
+ export declare function waitFor(rawInput: WaitForInput): Promise<CallToolResult>;
107
+ export declare function registerWaitForTool(server: McpServer): void;
@@ -0,0 +1,167 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_SNAPSHOT_MAX_CHARS, DEFAULT_SNAPSHOT_WAIT_MS, DEFAULT_WAIT_FOR_TIMEOUT_MS, MAX_VIEWPORT_HEIGHT, MAX_VIEWPORT_WIDTH, MAX_WAIT_FOR_TIMEOUT_MS, VUE_DETECT_MS, VUE_READY_TIMEOUT_MS, } from "../constants.js";
3
+ import { withSessionLock } from "../engine/browser.js";
4
+ import { hasViteClient, hmrFor } from "../engine/hmr.js";
5
+ import { takeSnapshot } from "../engine/snapshot.js";
6
+ import { detectVue, waitForVueReady } from "../engine/vue.js";
7
+ import { describeScale } from "../utils/format.js";
8
+ import { resizeForOutput, toBase64 } from "../utils/image.js";
9
+ import { storageStateField, withAuthNote } from "../utils/storage-state.js";
10
+ import { describeVue } from "../utils/vue-rules.js";
11
+ import { describeCounts, describeSessionFailure, openSessionPage } from "./snapshot.js";
12
+ export const WAIT_FOR_TOOL_NAME = "framewatch_wait_for";
13
+ export const waitForInputShape = {
14
+ url: z
15
+ .string()
16
+ .url()
17
+ .optional()
18
+ .describe("Open this URL first. Omit to wait on the page left open by framewatch_interact / framewatch_snapshot."),
19
+ until: z
20
+ .enum(["hot_update", "vue_ready", "selector", "network_idle"])
21
+ .default("hot_update")
22
+ .describe("`hot_update`: Vite applied a hot update (or full reload) newer than the last tool call on this page — " +
23
+ "use it right after saving a file. `vue_ready`: a Vue app is mounted and its router has resolved. " +
24
+ "`selector`: `selector` is visible. `network_idle`: no requests for 500ms."),
25
+ selector: z.string().optional().describe("For `until: selector` — the element to wait for"),
26
+ timeout_ms: z
27
+ .number()
28
+ .int()
29
+ .min(1)
30
+ .max(MAX_WAIT_FOR_TIMEOUT_MS)
31
+ .default(DEFAULT_WAIT_FOR_TIMEOUT_MS)
32
+ .describe("Give up after this long"),
33
+ include_screenshot: z.boolean().default(true).describe("Return a screenshot once the condition holds"),
34
+ include_snapshot: z.boolean().default(false).describe("Also return a framewatch_snapshot with fresh refs"),
35
+ wait_ms: z.number().int().min(0).default(DEFAULT_SNAPSHOT_WAIT_MS).describe("Settle time (ms) after opening `url`"),
36
+ viewport: z
37
+ .object({
38
+ width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH),
39
+ height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT),
40
+ })
41
+ .optional()
42
+ .describe("Resize the page to this first (defaults to leaving it as it is)"),
43
+ storage_state: storageStateField,
44
+ };
45
+ export const waitForInputSchema = z.object(waitForInputShape).superRefine((value, ctx) => {
46
+ if (value.until === "selector" && !value.selector) {
47
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "`until: selector` needs a `selector`", path: ["selector"] });
48
+ }
49
+ });
50
+ /**
51
+ * Wait for the session page to reach a state worth looking at, then look.
52
+ *
53
+ * The one that earns the tool its place is `hot_update`: after the agent
54
+ * saves a file, Vite patches the open page in place, and this returns the
55
+ * moment that patch has landed — no reload, no replayed flow, no guessed
56
+ * sleep. The other conditions replace the guessed sleeps elsewhere.
57
+ */
58
+ export function waitFor(rawInput) {
59
+ return withSessionLock(() => runWaitFor(rawInput));
60
+ }
61
+ async function runWaitFor(rawInput) {
62
+ const parsed = waitForInputSchema.safeParse(rawInput);
63
+ if (!parsed.success) {
64
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; ");
65
+ return errorResult(`Wait failed: invalid input — ${issues}`);
66
+ }
67
+ const input = parsed.data;
68
+ try {
69
+ const opened = await openSessionPage(input, "Wait");
70
+ if ("error" in opened)
71
+ return errorResult(opened.error);
72
+ const { page, auth, login_visible } = opened;
73
+ const watcher = hmrFor(page);
74
+ const since = watcher.lastSeen;
75
+ const started = Date.now();
76
+ const elapsed = () => Date.now() - started;
77
+ let headline;
78
+ let vue = null;
79
+ switch (input.until) {
80
+ case "hot_update": {
81
+ const event = await watcher.waitForEvent(since, input.timeout_ms);
82
+ if (!event) {
83
+ if (!watcher.connected && !(await hasViteClient(page))) {
84
+ return errorResult(`Wait failed: ${page.url()} has no Vite dev-server connection (no \`[vite] connected.\` in the console and no /@vite/client script), so there will never be a hot update to wait for. Is this the dev server, or a built page?`);
85
+ }
86
+ const latest = watcher.latest;
87
+ const last = latest ? ` — last one landed ${started - latest.t}ms before this call (${latest.path})` : "";
88
+ return errorResult(`Wait failed: No hot update within ${input.timeout_ms}ms${last}. Save the file, then call again.`);
89
+ }
90
+ const landed = event.t - started;
91
+ if (event.kind === "reload") {
92
+ await page.waitForLoadState("load", { timeout: input.timeout_ms }).catch(() => undefined);
93
+ }
94
+ vue = (await waitForVueReady(page, { detect_ms: VUE_DETECT_MS, ready_ms: VUE_READY_TIMEOUT_MS })).vue;
95
+ const what = event.kind === "reload" ? "Full reload" : "Hot update";
96
+ const kind = event.kind === "css" ? " (css)" : "";
97
+ headline = `${what} landed after ${Math.max(0, landed)}ms${kind}${event.path ? `: ${event.path}` : ""}`;
98
+ break;
99
+ }
100
+ case "vue_ready": {
101
+ const result = await waitForVueReady(page, { detect_ms: input.timeout_ms, ready_ms: input.timeout_ms });
102
+ if (!result.vue)
103
+ return errorResult(`Wait failed: No Vue app mounted on ${page.url()} within ${input.timeout_ms}ms.`);
104
+ vue = result.vue;
105
+ headline = `Vue ready after ${result.waited_ms}ms`;
106
+ break;
107
+ }
108
+ case "selector": {
109
+ try {
110
+ await page.waitForSelector(input.selector, { state: "visible", timeout: input.timeout_ms });
111
+ }
112
+ catch {
113
+ return errorResult(`Wait failed: "${input.selector}" did not appear within ${input.timeout_ms}ms on ${page.url()}.`);
114
+ }
115
+ headline = `"${input.selector}" appeared after ${elapsed()}ms`;
116
+ vue = await detectVue(page);
117
+ break;
118
+ }
119
+ case "network_idle": {
120
+ try {
121
+ await page.waitForLoadState("networkidle", { timeout: input.timeout_ms });
122
+ }
123
+ catch {
124
+ return errorResult(`Wait failed: the network did not go idle within ${input.timeout_ms}ms on ${page.url()}.`);
125
+ }
126
+ headline = `Network idle after ${elapsed()}ms`;
127
+ vue = await detectVue(page);
128
+ break;
129
+ }
130
+ }
131
+ const viewport = page.viewportSize() ?? { width: 0, height: 0 };
132
+ const lines = [`${headline}${vue ? ` — ${describeVue(vue)}` : ""} — ${page.url()} — ${describeScale(viewport)}`];
133
+ const content = [{ type: "text", text: lines.join("\n") }];
134
+ if (input.include_screenshot) {
135
+ const png = await page.screenshot({ type: "png" });
136
+ content.push({ type: "image", data: toBase64(await resizeForOutput(png)), mimeType: "image/png" });
137
+ }
138
+ if (input.include_snapshot) {
139
+ const snapshot = await takeSnapshot(page, { mode: "full", max_chars: DEFAULT_SNAPSHOT_MAX_CHARS });
140
+ const text = [`Snapshot — ${describeCounts(snapshot)} (refs valid until the page changes)`, "", snapshot.text];
141
+ if (snapshot.cut_lines > 0)
142
+ text.push(`… ${snapshot.cut_lines} more lines cut — use framewatch_snapshot with \`selector\` or mode "interactive".`);
143
+ content.push({ type: "text", text: text.join("\n") });
144
+ }
145
+ watcher.markSeen();
146
+ return withAuthNote({ content }, auth, login_visible);
147
+ }
148
+ catch (error) {
149
+ return errorResult(describeSessionFailure("Wait", input, error));
150
+ }
151
+ }
152
+ function errorResult(text) {
153
+ return { isError: true, content: [{ type: "text", text }] };
154
+ }
155
+ export function registerWaitForTool(server) {
156
+ server.registerTool(WAIT_FOR_TOOL_NAME, {
157
+ title: "Wait for",
158
+ description: "Wait for the open page to reach a state worth looking at, then screenshot it. After you save a file, " +
159
+ "`until: hot_update` returns the moment Vite has patched the page in place (or reloaded it) — no reload, " +
160
+ "no replayed flow, no guessed sleep — and names the file that changed. `vue_ready` waits for a Vue app to " +
161
+ "mount and its router to resolve; `selector` for an element; `network_idle` for requests to stop. " +
162
+ "Works on the page framewatch_interact / framewatch_snapshot keep open; `include_snapshot` returns fresh refs.",
163
+ inputSchema: waitForInputShape,
164
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
165
+ }, async (args) => waitFor(args));
166
+ }
167
+ //# sourceMappingURL=wait-for.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait-for.js","sourceRoot":"","sources":["../../src/tools/wait-for.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,2BAA2B,EAC3B,mBAAmB,EACnB,kBAAkB,EAClB,uBAAuB,EACvB,aAAa,EACb,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAgB,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAExF,MAAM,CAAC,MAAM,kBAAkB,GAAG,qBAAqB,CAAC;AAExD,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,GAAG,EAAE,CAAC;SACH,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,QAAQ,CAAC,uGAAuG,CAAC;IACpH,KAAK,EAAE,CAAC;SACL,IAAI,CAAC,CAAC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;SAC7D,OAAO,CAAC,YAAY,CAAC;SACrB,QAAQ,CACP,wGAAwG;QACtG,mGAAmG;QACnG,2EAA2E,CAC9E;IACH,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iDAAiD,CAAC;IAC3F,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,uBAAuB,CAAC;SAC5B,OAAO,CAAC,2BAA2B,CAAC;SACpC,QAAQ,CAAC,yBAAyB,CAAC;IACtC,kBAAkB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,8CAA8C,CAAC;IACtG,gBAAgB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,mDAAmD,CAAC;IAC1G,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC,QAAQ,CAAC,sCAAsC,CAAC;IACnH,QAAQ,EAAE,CAAC;SACR,MAAM,CAAC;QACN,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC;QACtD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,mBAAmB,CAAC;KACzD,CAAC;SACD,QAAQ,EAAE;SACV,QAAQ,CAAC,iEAAiE,CAAC;IAC9E,aAAa,EAAE,iBAAiB;CACjC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,WAAW,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;IACvF,IAAI,KAAK,CAAC,KAAK,KAAK,UAAU,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QAClD,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,sCAAsC,EAAE,IAAI,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;IACrH,CAAC;AACH,CAAC,CAAC,CAAC;AAGH;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,QAAsB;IAC5C,OAAO,eAAe,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;AACrD,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,QAAsB;IAC9C,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACtD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,OAAO,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzG,OAAO,WAAW,CAAC,gCAAgC,MAAM,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC;IAE1B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACpD,IAAI,OAAO,IAAI,MAAM;YAAE,OAAO,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACxD,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,MAAM,CAAC;QAC7C,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC;QAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;QAEnD,IAAI,QAAgB,CAAC;QACrB,IAAI,GAAG,GAAmB,IAAI,CAAC;QAE/B,QAAQ,KAAK,CAAC,KAAK,EAAE,CAAC;YACpB,KAAK,YAAY,CAAC,CAAC,CAAC;gBAClB,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;gBAClE,IAAI,CAAC,KAAK,EAAE,CAAC;oBACX,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,CAAC,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;wBACvD,OAAO,WAAW,CAChB,gBAAgB,IAAI,CAAC,GAAG,EAAE,qMAAqM,CAChO,CAAC;oBACJ,CAAC;oBACD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;oBAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,sBAAsB,OAAO,GAAG,MAAM,CAAC,CAAC,wBAAwB,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC1G,OAAO,WAAW,CAAC,qCAAqC,KAAK,CAAC,UAAU,KAAK,IAAI,mCAAmC,CAAC,CAAC;gBACxH,CAAC;gBACD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC;gBACjC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBAC5B,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;gBAC5F,CAAC;gBACD,GAAG,GAAG,CAAC,MAAM,eAAe,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,aAAa,EAAE,QAAQ,EAAE,oBAAoB,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;gBACtG,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC;gBACpE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClD,QAAQ,GAAG,GAAG,IAAI,iBAAiB,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;gBACxG,MAAM;YACR,CAAC;YACD,KAAK,WAAW,CAAC,CAAC,CAAC;gBACjB,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,UAAU,EAAE,QAAQ,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;gBACxG,IAAI,CAAC,MAAM,CAAC,GAAG;oBAAE,OAAO,WAAW,CAAC,sCAAsC,IAAI,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC,UAAU,KAAK,CAAC,CAAC;gBACtH,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;gBACjB,QAAQ,GAAG,mBAAmB,MAAM,CAAC,SAAS,IAAI,CAAC;gBACnD,MAAM;YACR,CAAC;YACD,KAAK,UAAU,CAAC,CAAC,CAAC;gBAChB,IAAI,CAAC;oBACH,MAAM,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,QAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;gBAC/F,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,WAAW,CAAC,iBAAiB,KAAK,CAAC,QAAQ,2BAA2B,KAAK,CAAC,UAAU,SAAS,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;gBACvH,CAAC;gBACD,QAAQ,GAAG,IAAI,KAAK,CAAC,QAAQ,oBAAoB,OAAO,EAAE,IAAI,CAAC;gBAC/D,GAAG,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;gBAC5B,MAAM;YACR,CAAC;YACD,KAAK,cAAc,CAAC,CAAC,CAAC;gBACpB,IAAI,CAAC;oBACH,MAAM,IAAI,CAAC,gBAAgB,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;gBAC5E,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,WAAW,CAAC,mDAAmD,KAAK,CAAC,UAAU,SAAS,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;gBAChH,CAAC;gBACD,QAAQ,GAAG,sBAAsB,OAAO,EAAE,IAAI,CAAC;gBAC/C,GAAG,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;gBAC5B,MAAM;YACR,CAAC;QACH,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;QAChE,MAAM,KAAK,GAAG,CAAC,GAAG,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,GAAG,EAAE,MAAM,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACjH,MAAM,OAAO,GAA8B,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtF,IAAI,KAAK,CAAC,kBAAkB,EAAE,CAAC;YAC7B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACnD,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,eAAe,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC;QACrG,CAAC;QACD,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;YAC3B,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,0BAA0B,EAAE,CAAC,CAAC;YACnG,MAAM,IAAI,GAAG,CAAC,cAAc,cAAc,CAAC,QAAQ,CAAC,sCAAsC,EAAE,EAAE,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC/G,IAAI,QAAQ,CAAC,SAAS,GAAG,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,SAAS,oFAAoF,CAAC,CAAC;YACnJ,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,OAAO,CAAC,QAAQ,EAAE,CAAC;QACnB,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;IACxD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,WAAW,CAAC,sBAAsB,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;IACnE,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAiB;IACnD,MAAM,CAAC,YAAY,CACjB,kBAAkB,EAClB;QACE,KAAK,EAAE,UAAU;QACjB,WAAW,EACT,uGAAuG;YACvG,0GAA0G;YAC1G,2GAA2G;YAC3G,mGAAmG;YACnG,+GAA+G;QACjH,WAAW,EAAE,iBAAiB;QAC9B,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE;KACxG,EACD,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAC9B,CAAC;AACJ,CAAC","sourcesContent":["import { z } from \"zod\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n DEFAULT_SNAPSHOT_MAX_CHARS,\n DEFAULT_SNAPSHOT_WAIT_MS,\n DEFAULT_WAIT_FOR_TIMEOUT_MS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n MAX_WAIT_FOR_TIMEOUT_MS,\n VUE_DETECT_MS,\n VUE_READY_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { withSessionLock } from \"../engine/browser.js\";\nimport { hasViteClient, hmrFor } from \"../engine/hmr.js\";\nimport { takeSnapshot } from \"../engine/snapshot.js\";\nimport { detectVue, waitForVueReady } from \"../engine/vue.js\";\nimport { describeScale } from \"../utils/format.js\";\nimport { resizeForOutput, toBase64 } from \"../utils/image.js\";\nimport { storageStateField, withAuthNote } from \"../utils/storage-state.js\";\nimport { describeVue, type VueInfo } from \"../utils/vue-rules.js\";\nimport { describeCounts, describeSessionFailure, openSessionPage } from \"./snapshot.js\";\n\nexport const WAIT_FOR_TOOL_NAME = \"framewatch_wait_for\";\n\nexport const waitForInputShape = {\n url: z\n .string()\n .url()\n .optional()\n .describe(\"Open this URL first. Omit to wait on the page left open by framewatch_interact / framewatch_snapshot.\"),\n until: z\n .enum([\"hot_update\", \"vue_ready\", \"selector\", \"network_idle\"])\n .default(\"hot_update\")\n .describe(\n \"`hot_update`: Vite applied a hot update (or full reload) newer than the last tool call on this page — \" +\n \"use it right after saving a file. `vue_ready`: a Vue app is mounted and its router has resolved. \" +\n \"`selector`: `selector` is visible. `network_idle`: no requests for 500ms.\",\n ),\n selector: z.string().optional().describe(\"For `until: selector` — the element to wait for\"),\n timeout_ms: z\n .number()\n .int()\n .min(1)\n .max(MAX_WAIT_FOR_TIMEOUT_MS)\n .default(DEFAULT_WAIT_FOR_TIMEOUT_MS)\n .describe(\"Give up after this long\"),\n include_screenshot: z.boolean().default(true).describe(\"Return a screenshot once the condition holds\"),\n include_snapshot: z.boolean().default(false).describe(\"Also return a framewatch_snapshot with fresh refs\"),\n wait_ms: z.number().int().min(0).default(DEFAULT_SNAPSHOT_WAIT_MS).describe(\"Settle time (ms) after opening `url`\"),\n viewport: z\n .object({\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT),\n })\n .optional()\n .describe(\"Resize the page to this first (defaults to leaving it as it is)\"),\n storage_state: storageStateField,\n};\n\nexport const waitForInputSchema = z.object(waitForInputShape).superRefine((value, ctx) => {\n if (value.until === \"selector\" && !value.selector) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message: \"`until: selector` needs a `selector`\", path: [\"selector\"] });\n }\n});\nexport type WaitForInput = z.input<typeof waitForInputSchema>;\n\n/**\n * Wait for the session page to reach a state worth looking at, then look.\n *\n * The one that earns the tool its place is `hot_update`: after the agent\n * saves a file, Vite patches the open page in place, and this returns the\n * moment that patch has landed — no reload, no replayed flow, no guessed\n * sleep. The other conditions replace the guessed sleeps elsewhere.\n */\nexport function waitFor(rawInput: WaitForInput): Promise<CallToolResult> {\n return withSessionLock(() => runWaitFor(rawInput));\n}\n\nasync function runWaitFor(rawInput: WaitForInput): Promise<CallToolResult> {\n const parsed = waitForInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Wait failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n try {\n const opened = await openSessionPage(input, \"Wait\");\n if (\"error\" in opened) return errorResult(opened.error);\n const { page, auth, login_visible } = opened;\n const watcher = hmrFor(page);\n const since = watcher.lastSeen;\n const started = Date.now();\n const elapsed = (): number => Date.now() - started;\n\n let headline: string;\n let vue: VueInfo | null = null;\n\n switch (input.until) {\n case \"hot_update\": {\n const event = await watcher.waitForEvent(since, input.timeout_ms);\n if (!event) {\n if (!watcher.connected && !(await hasViteClient(page))) {\n return errorResult(\n `Wait failed: ${page.url()} has no Vite dev-server connection (no \\`[vite] connected.\\` in the console and no /@vite/client script), so there will never be a hot update to wait for. Is this the dev server, or a built page?`,\n );\n }\n const latest = watcher.latest;\n const last = latest ? ` — last one landed ${started - latest.t}ms before this call (${latest.path})` : \"\";\n return errorResult(`Wait failed: No hot update within ${input.timeout_ms}ms${last}. Save the file, then call again.`);\n }\n const landed = event.t - started;\n if (event.kind === \"reload\") {\n await page.waitForLoadState(\"load\", { timeout: input.timeout_ms }).catch(() => undefined);\n }\n vue = (await waitForVueReady(page, { detect_ms: VUE_DETECT_MS, ready_ms: VUE_READY_TIMEOUT_MS })).vue;\n const what = event.kind === \"reload\" ? \"Full reload\" : \"Hot update\";\n const kind = event.kind === \"css\" ? \" (css)\" : \"\";\n headline = `${what} landed after ${Math.max(0, landed)}ms${kind}${event.path ? `: ${event.path}` : \"\"}`;\n break;\n }\n case \"vue_ready\": {\n const result = await waitForVueReady(page, { detect_ms: input.timeout_ms, ready_ms: input.timeout_ms });\n if (!result.vue) return errorResult(`Wait failed: No Vue app mounted on ${page.url()} within ${input.timeout_ms}ms.`);\n vue = result.vue;\n headline = `Vue ready after ${result.waited_ms}ms`;\n break;\n }\n case \"selector\": {\n try {\n await page.waitForSelector(input.selector!, { state: \"visible\", timeout: input.timeout_ms });\n } catch {\n return errorResult(`Wait failed: \"${input.selector}\" did not appear within ${input.timeout_ms}ms on ${page.url()}.`);\n }\n headline = `\"${input.selector}\" appeared after ${elapsed()}ms`;\n vue = await detectVue(page);\n break;\n }\n case \"network_idle\": {\n try {\n await page.waitForLoadState(\"networkidle\", { timeout: input.timeout_ms });\n } catch {\n return errorResult(`Wait failed: the network did not go idle within ${input.timeout_ms}ms on ${page.url()}.`);\n }\n headline = `Network idle after ${elapsed()}ms`;\n vue = await detectVue(page);\n break;\n }\n }\n\n const viewport = page.viewportSize() ?? { width: 0, height: 0 };\n const lines = [`${headline}${vue ? ` — ${describeVue(vue)}` : \"\"} — ${page.url()} — ${describeScale(viewport)}`];\n const content: CallToolResult[\"content\"] = [{ type: \"text\", text: lines.join(\"\\n\") }];\n if (input.include_screenshot) {\n const png = await page.screenshot({ type: \"png\" });\n content.push({ type: \"image\", data: toBase64(await resizeForOutput(png)), mimeType: \"image/png\" });\n }\n if (input.include_snapshot) {\n const snapshot = await takeSnapshot(page, { mode: \"full\", max_chars: DEFAULT_SNAPSHOT_MAX_CHARS });\n const text = [`Snapshot — ${describeCounts(snapshot)} (refs valid until the page changes)`, \"\", snapshot.text];\n if (snapshot.cut_lines > 0) text.push(`… ${snapshot.cut_lines} more lines cut — use framewatch_snapshot with \\`selector\\` or mode \"interactive\".`);\n content.push({ type: \"text\", text: text.join(\"\\n\") });\n }\n watcher.markSeen();\n return withAuthNote({ content }, auth, login_visible);\n } catch (error) {\n return errorResult(describeSessionFailure(\"Wait\", input, error));\n }\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\nexport function registerWaitForTool(server: McpServer): void {\n server.registerTool(\n WAIT_FOR_TOOL_NAME,\n {\n title: \"Wait for\",\n description:\n \"Wait for the open page to reach a state worth looking at, then screenshot it. After you save a file, \" +\n \"`until: hot_update` returns the moment Vite has patched the page in place (or reloaded it) — no reload, \" +\n \"no replayed flow, no guessed sleep — and names the file that changed. `vue_ready` waits for a Vue app to \" +\n \"mount and its router to resolve; `selector` for an element; `network_idle` for requests to stop. \" +\n \"Works on the page framewatch_interact / framewatch_snapshot keep open; `include_snapshot` returns fresh refs.\",\n inputSchema: waitForInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },\n },\n async (args) => waitFor(args),\n );\n}\n"]}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Build Arabic text about as long as `original`.
3
+ *
4
+ * `seed` makes the choice deterministic and varied: the same string in the
5
+ * same place gets the same replacement on every run (so two runs of the tool
6
+ * are comparable, and the LTR and RTL passes agree), while two different
7
+ * elements of the same length do not all get the same word.
8
+ *
9
+ * A string that is only digits and punctuation keeps its shape and is merely
10
+ * transliterated — see `ARABIC_DIGITS`.
11
+ */
12
+ export declare function arabicFor(original: string, seed?: number): string;
13
+ /** Latin digits to Arabic-Indic, leaving everything else alone. */
14
+ export declare function toArabicDigits(value: string): string;
@@ -0,0 +1,193 @@
1
+ import { MAX_ARABIC_INJECTION_LENGTH } from "../constants.js";
2
+ /**
3
+ * Arabic stand-in text, matched to the length of what it replaces.
4
+ *
5
+ * `framewatch_rtl` can replace every visible string on a page with Arabic, so
6
+ * that the layout is tested against text that actually renders right-to-left
7
+ * rather than against English sitting inside an RTL container. English in an
8
+ * RTL page is a bidirectional edge case of its own and hides the ordinary
9
+ * bugs: the Latin run keeps its own direction, so a container that never
10
+ * mirrored still *looks* plausible.
11
+ *
12
+ * The length matching is the part that matters. If "Add to cart" became a
13
+ * three-character word, the button would shrink and any overflow the tool
14
+ * reported afterwards would be an artifact of the substitution rather than a
15
+ * property of the page — and if it became a paragraph, every page would
16
+ * overflow. So each replacement is built to within a few characters of the
17
+ * string it replaces, and the report's overflow findings mean what they say.
18
+ *
19
+ * Nothing here touches a browser: it is a string in, a string out, so every
20
+ * case is unit-testable without launching Chromium.
21
+ */
22
+ /**
23
+ * The vocabulary, grouped by length so a replacement can be assembled to fit.
24
+ *
25
+ * Real words, not lorem ipsum: an Arabic reader glancing at a screenshot
26
+ * should see a plausible interface, and the shaping/ligature behaviour of real
27
+ * words is what stresses the text rendering. These are the words a UI is
28
+ * actually made of — actions, labels, navigation.
29
+ */
30
+ const WORDS = [
31
+ // 2–4 characters
32
+ "نعم",
33
+ "لا",
34
+ "حسنا",
35
+ "بحث",
36
+ "إغلاق",
37
+ "حفظ",
38
+ "فتح",
39
+ "التالي",
40
+ "السابق",
41
+ "رجوع",
42
+ // 5–8
43
+ "تسجيل",
44
+ "الدخول",
45
+ "الخروج",
46
+ "الرئيسية",
47
+ "المنتجات",
48
+ "الخدمات",
49
+ "الطلبات",
50
+ "الحساب",
51
+ "الإعدادات",
52
+ "المفضلة",
53
+ // 9+
54
+ "أضف إلى السلة",
55
+ "إتمام الشراء",
56
+ "تواصل معنا",
57
+ "من نحن",
58
+ "الشروط والأحكام",
59
+ "سياسة الخصوصية",
60
+ "اشترك في النشرة",
61
+ "عرض جميع النتائج",
62
+ ];
63
+ /** A sentence's worth, for replacing paragraphs rather than labels. */
64
+ const SENTENCES = [
65
+ "مرحبا بك في متجرنا الإلكتروني",
66
+ "نقدم لك أفضل المنتجات بأسعار تنافسية",
67
+ "يمكنك تصفح الأقسام والاطلاع على العروض",
68
+ "خدمة التوصيل متاحة إلى جميع المناطق",
69
+ "فريق الدعم جاهز للإجابة عن أسئلتك",
70
+ ];
71
+ /**
72
+ * Arabic-Indic digits, so a number reads as Arabic too.
73
+ *
74
+ * Kept as a separate mapping rather than being replaced by words: a price, a
75
+ * quantity or a date is still a number in an Arabic interface, and turning
76
+ * "24" into a word would change what the element is rather than what language
77
+ * it is in — which is the opposite of what this substitution is for.
78
+ */
79
+ const ARABIC_DIGITS = ["٠", "١", "٢", "٣", "٤", "٥", "٦", "٧", "٨", "٩"];
80
+ /**
81
+ * Build Arabic text about as long as `original`.
82
+ *
83
+ * `seed` makes the choice deterministic and varied: the same string in the
84
+ * same place gets the same replacement on every run (so two runs of the tool
85
+ * are comparable, and the LTR and RTL passes agree), while two different
86
+ * elements of the same length do not all get the same word.
87
+ *
88
+ * A string that is only digits and punctuation keeps its shape and is merely
89
+ * transliterated — see `ARABIC_DIGITS`.
90
+ */
91
+ export function arabicFor(original, seed = 0) {
92
+ const source = String(original ?? "");
93
+ const trimmed = source.trim();
94
+ if (trimmed === "")
95
+ return source;
96
+ // Numbers, prices, times: keep the shape, change the digits. Replacing
97
+ // "١٢:٣٠" with a word would be a different element, not a translated one.
98
+ if (!/\p{L}/u.test(trimmed))
99
+ return toArabicDigits(source);
100
+ const target = Math.min(trimmed.length, MAX_ARABIC_INJECTION_LENGTH);
101
+ const built = target > 24 ? buildSentence(target, seed) : buildPhrase(target, seed);
102
+ // Leading/trailing whitespace is layout — a label may rely on it — so the
103
+ // original's edges are preserved and only the text between them replaced.
104
+ const leading = source.slice(0, source.length - source.trimStart().length);
105
+ const trailing = source.slice(source.trimEnd().length);
106
+ return `${leading}${built}${trailing}`;
107
+ }
108
+ /**
109
+ * Words joined until they are about `target` characters long.
110
+ *
111
+ * Each step picks the word that gets closest to the remaining space rather
112
+ * than taking the first candidate and giving up when it does not fit. Giving
113
+ * up is the trap: a 19-character string whose first word happens to be 10
114
+ * characters would stop there, nine short, and every button on the page would
115
+ * come back narrower than it really is — which is exactly the measurement
116
+ * error this whole module exists to avoid.
117
+ */
118
+ function buildPhrase(target, seed) {
119
+ // A starting word no longer than the target, so a 3-character label does
120
+ // not become a 13-character phrase.
121
+ const candidates = WORDS.filter((word) => word.length <= Math.max(3, target));
122
+ const pool = candidates.length > 0 ? candidates : WORDS;
123
+ let out = pool[hash(seed) % pool.length];
124
+ let step = 1;
125
+ while (out.length < target - 1 && step <= 40) {
126
+ const remaining = target - out.length - 1;
127
+ const next = bestFit(pool, remaining, seed + step);
128
+ // Nothing left in the vocabulary fits the gap: stop rather than overshoot.
129
+ if (next === null)
130
+ break;
131
+ out = `${out} ${next}`;
132
+ step += 1;
133
+ }
134
+ return out;
135
+ }
136
+ /**
137
+ * The word closest to `remaining` characters without overshooting by more
138
+ * than a hair, chosen deterministically among equally good candidates.
139
+ */
140
+ function bestFit(pool, remaining, seed) {
141
+ if (remaining <= 0)
142
+ return null;
143
+ // A slight overshoot is better than a large undershoot: landing two
144
+ // characters long is invisible, landing nine short is a different layout.
145
+ const fitting = pool.filter((word) => word.length <= remaining + 2);
146
+ if (fitting.length === 0)
147
+ return null;
148
+ let best = fitting[0].length;
149
+ for (const word of fitting) {
150
+ if (Math.abs(word.length - remaining) < Math.abs(best - remaining))
151
+ best = word.length;
152
+ }
153
+ const closest = fitting.filter((word) => word.length === best);
154
+ return closest[hash(seed) % closest.length];
155
+ }
156
+ /** Sentences joined until they are about `target` characters long. */
157
+ function buildSentence(target, seed) {
158
+ let out = SENTENCES[hash(seed) % SENTENCES.length];
159
+ let step = 1;
160
+ while (out.length < target - 4) {
161
+ const next = SENTENCES[hash(seed + step) % SENTENCES.length];
162
+ if (out.length + 2 + next.length > target + 8) {
163
+ // No whole sentence fits the remainder; pad with words instead of
164
+ // overshooting, so a long paragraph still lands near its original length.
165
+ const filler = buildPhrase(target - out.length - 1, seed + step);
166
+ out = `${out}، ${filler}`;
167
+ break;
168
+ }
169
+ out = `${out}، ${next}`;
170
+ step += 1;
171
+ if (step > 40)
172
+ break;
173
+ }
174
+ return out;
175
+ }
176
+ /** Latin digits to Arabic-Indic, leaving everything else alone. */
177
+ export function toArabicDigits(value) {
178
+ return String(value ?? "").replace(/[0-9]/g, (digit) => ARABIC_DIGITS[Number(digit)]);
179
+ }
180
+ /**
181
+ * A small deterministic hash.
182
+ *
183
+ * Not for security — for repeatability. `Math.random()` would make two runs of
184
+ * the same audit disagree about which element overflowed, which is exactly the
185
+ * kind of flakiness that makes a testing tool untrusted.
186
+ */
187
+ function hash(seed) {
188
+ let value = (seed | 0) + 0x9e3779b9;
189
+ value = Math.imul(value ^ (value >>> 16), 0x21f0aaad);
190
+ value = Math.imul(value ^ (value >>> 15), 0x735a2d97);
191
+ return Math.abs(value ^ (value >>> 15));
192
+ }
193
+ //# sourceMappingURL=arabic-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arabic-text.js","sourceRoot":"","sources":["../../src/utils/arabic-text.ts"],"names":[],"mappings":"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","sourcesContent":["import { MAX_ARABIC_INJECTION_LENGTH } from \"../constants.js\";\n\n/**\n * Arabic stand-in text, matched to the length of what it replaces.\n *\n * `framewatch_rtl` can replace every visible string on a page with Arabic, so\n * that the layout is tested against text that actually renders right-to-left\n * rather than against English sitting inside an RTL container. English in an\n * RTL page is a bidirectional edge case of its own and hides the ordinary\n * bugs: the Latin run keeps its own direction, so a container that never\n * mirrored still *looks* plausible.\n *\n * The length matching is the part that matters. If \"Add to cart\" became a\n * three-character word, the button would shrink and any overflow the tool\n * reported afterwards would be an artifact of the substitution rather than a\n * property of the page — and if it became a paragraph, every page would\n * overflow. So each replacement is built to within a few characters of the\n * string it replaces, and the report's overflow findings mean what they say.\n *\n * Nothing here touches a browser: it is a string in, a string out, so every\n * case is unit-testable without launching Chromium.\n */\n\n/**\n * The vocabulary, grouped by length so a replacement can be assembled to fit.\n *\n * Real words, not lorem ipsum: an Arabic reader glancing at a screenshot\n * should see a plausible interface, and the shaping/ligature behaviour of real\n * words is what stresses the text rendering. These are the words a UI is\n * actually made of — actions, labels, navigation.\n */\nconst WORDS: readonly string[] = [\n // 2–4 characters\n \"نعم\",\n \"لا\",\n \"حسنا\",\n \"بحث\",\n \"إغلاق\",\n \"حفظ\",\n \"فتح\",\n \"التالي\",\n \"السابق\",\n \"رجوع\",\n // 5–8\n \"تسجيل\",\n \"الدخول\",\n \"الخروج\",\n \"الرئيسية\",\n \"المنتجات\",\n \"الخدمات\",\n \"الطلبات\",\n \"الحساب\",\n \"الإعدادات\",\n \"المفضلة\",\n // 9+\n \"أضف إلى السلة\",\n \"إتمام الشراء\",\n \"تواصل معنا\",\n \"من نحن\",\n \"الشروط والأحكام\",\n \"سياسة الخصوصية\",\n \"اشترك في النشرة\",\n \"عرض جميع النتائج\",\n];\n\n/** A sentence's worth, for replacing paragraphs rather than labels. */\nconst SENTENCES: readonly string[] = [\n \"مرحبا بك في متجرنا الإلكتروني\",\n \"نقدم لك أفضل المنتجات بأسعار تنافسية\",\n \"يمكنك تصفح الأقسام والاطلاع على العروض\",\n \"خدمة التوصيل متاحة إلى جميع المناطق\",\n \"فريق الدعم جاهز للإجابة عن أسئلتك\",\n];\n\n/**\n * Arabic-Indic digits, so a number reads as Arabic too.\n *\n * Kept as a separate mapping rather than being replaced by words: a price, a\n * quantity or a date is still a number in an Arabic interface, and turning\n * \"24\" into a word would change what the element is rather than what language\n * it is in — which is the opposite of what this substitution is for.\n */\nconst ARABIC_DIGITS = [\"٠\", \"١\", \"٢\", \"٣\", \"٤\", \"٥\", \"٦\", \"٧\", \"٨\", \"٩\"] as const;\n\n/**\n * Build Arabic text about as long as `original`.\n *\n * `seed` makes the choice deterministic and varied: the same string in the\n * same place gets the same replacement on every run (so two runs of the tool\n * are comparable, and the LTR and RTL passes agree), while two different\n * elements of the same length do not all get the same word.\n *\n * A string that is only digits and punctuation keeps its shape and is merely\n * transliterated — see `ARABIC_DIGITS`.\n */\nexport function arabicFor(original: string, seed = 0): string {\n const source = String(original ?? \"\");\n const trimmed = source.trim();\n if (trimmed === \"\") return source;\n\n // Numbers, prices, times: keep the shape, change the digits. Replacing\n // \"١٢:٣٠\" with a word would be a different element, not a translated one.\n if (!/\\p{L}/u.test(trimmed)) return toArabicDigits(source);\n\n const target = Math.min(trimmed.length, MAX_ARABIC_INJECTION_LENGTH);\n const built = target > 24 ? buildSentence(target, seed) : buildPhrase(target, seed);\n\n // Leading/trailing whitespace is layout — a label may rely on it — so the\n // original's edges are preserved and only the text between them replaced.\n const leading = source.slice(0, source.length - source.trimStart().length);\n const trailing = source.slice(source.trimEnd().length);\n return `${leading}${built}${trailing}`;\n}\n\n/**\n * Words joined until they are about `target` characters long.\n *\n * Each step picks the word that gets closest to the remaining space rather\n * than taking the first candidate and giving up when it does not fit. Giving\n * up is the trap: a 19-character string whose first word happens to be 10\n * characters would stop there, nine short, and every button on the page would\n * come back narrower than it really is — which is exactly the measurement\n * error this whole module exists to avoid.\n */\nfunction buildPhrase(target: number, seed: number): string {\n // A starting word no longer than the target, so a 3-character label does\n // not become a 13-character phrase.\n const candidates = WORDS.filter((word) => word.length <= Math.max(3, target));\n const pool = candidates.length > 0 ? candidates : WORDS;\n\n let out = pool[hash(seed) % pool.length];\n let step = 1;\n while (out.length < target - 1 && step <= 40) {\n const remaining = target - out.length - 1;\n const next = bestFit(pool, remaining, seed + step);\n // Nothing left in the vocabulary fits the gap: stop rather than overshoot.\n if (next === null) break;\n out = `${out} ${next}`;\n step += 1;\n }\n return out;\n}\n\n/**\n * The word closest to `remaining` characters without overshooting by more\n * than a hair, chosen deterministically among equally good candidates.\n */\nfunction bestFit(pool: readonly string[], remaining: number, seed: number): string | null {\n if (remaining <= 0) return null;\n // A slight overshoot is better than a large undershoot: landing two\n // characters long is invisible, landing nine short is a different layout.\n const fitting = pool.filter((word) => word.length <= remaining + 2);\n if (fitting.length === 0) return null;\n\n let best = fitting[0].length;\n for (const word of fitting) {\n if (Math.abs(word.length - remaining) < Math.abs(best - remaining)) best = word.length;\n }\n const closest = fitting.filter((word) => word.length === best);\n return closest[hash(seed) % closest.length];\n}\n\n/** Sentences joined until they are about `target` characters long. */\nfunction buildSentence(target: number, seed: number): string {\n let out = SENTENCES[hash(seed) % SENTENCES.length];\n let step = 1;\n while (out.length < target - 4) {\n const next = SENTENCES[hash(seed + step) % SENTENCES.length];\n if (out.length + 2 + next.length > target + 8) {\n // No whole sentence fits the remainder; pad with words instead of\n // overshooting, so a long paragraph still lands near its original length.\n const filler = buildPhrase(target - out.length - 1, seed + step);\n out = `${out}، ${filler}`;\n break;\n }\n out = `${out}، ${next}`;\n step += 1;\n if (step > 40) break;\n }\n return out;\n}\n\n/** Latin digits to Arabic-Indic, leaving everything else alone. */\nexport function toArabicDigits(value: string): string {\n return String(value ?? \"\").replace(/[0-9]/g, (digit) => ARABIC_DIGITS[Number(digit)]);\n}\n\n/**\n * A small deterministic hash.\n *\n * Not for security — for repeatability. `Math.random()` would make two runs of\n * the same audit disagree about which element overflowed, which is exactly the\n * kind of flakiness that makes a testing tool untrusted.\n */\nfunction hash(seed: number): number {\n let value = (seed | 0) + 0x9e3779b9;\n value = Math.imul(value ^ (value >>> 16), 0x21f0aaad);\n value = Math.imul(value ^ (value >>> 15), 0x735a2d97);\n return Math.abs(value ^ (value >>> 15));\n}\n"]}
@@ -0,0 +1,41 @@
1
+ import type { CallToolResult, ImageContent } from "@modelcontextprotocol/sdk/types.js";
2
+ /**
3
+ * The image budget.
4
+ *
5
+ * Claude Code caps one MCP tool result at `MAX_MCP_OUTPUT_TOKENS` (25,000 by
6
+ * default) and counts base64 image data toward it. A result over the cap is
7
+ * written to a file and replaced with a reference, so the model sees no
8
+ * images at all — and a single screenshot of a real page is 180 KB of PNG,
9
+ * which is over on its own. Every result therefore passes through here on
10
+ * its way out: each image is encoded the cheapest way that is still faithful,
11
+ * and if the result still does not fit it is degraded in a fixed order —
12
+ * crops first, then frame size, then frames from the middle — and says so.
13
+ * Fewer images that arrive beat all of them lost.
14
+ */
15
+ export interface ImageMark {
16
+ /** A `crop` duplicates part of a frame and is the first thing to go. */
17
+ role?: "frame" | "crop";
18
+ /** Never dropped while any unmarked frame remains (first/last/interaction frames). */
19
+ keep?: boolean;
20
+ }
21
+ /** Stamp an image block with its role, for `fitToBudget` to read (and strip). */
22
+ export declare function markImage(block: ImageContent, mark: ImageMark): ImageContent;
23
+ /** The cap in force: what the shell that started Claude Code exported, or the default. */
24
+ export declare function capFromEnv(env?: NodeJS.ProcessEnv): number;
25
+ /** Base64 characters of image data a result may carry once its text is paid for. */
26
+ export declare function imageBudgetChars(capTokens: number, textChars: number): number;
27
+ export interface BudgetStats {
28
+ total: number;
29
+ kept: number;
30
+ crops_dropped: number;
31
+ frames_dropped: number;
32
+ /** Width the surviving frames were shrunk to, or undefined when untouched. */
33
+ width?: number;
34
+ cap_tokens: number;
35
+ }
36
+ export declare function describeBudget(stats: BudgetStats): string;
37
+ export interface FitOptions {
38
+ /** Override the cap (tests); defaults to `capFromEnv()`. */
39
+ cap_tokens?: number;
40
+ }
41
+ export declare function fitToBudget(result: CallToolResult, options?: FitOptions): Promise<CallToolResult>;