diff --git a/apps/site/docs/en/ui-testing-framework.mdx b/apps/site/docs/en/ui-testing-framework.mdx new file mode 100644 index 0000000000..0e819e1eca --- /dev/null +++ b/apps/site/docs/en/ui-testing-framework.mdx @@ -0,0 +1,274 @@ +# AI-native UI Testing Framework for Natural-language Cases + +The hard part of UI testing is not writing the first browser script. It is keeping the suite readable, writable, and maintainable as the product changes. Traditional scripts quickly fill with selectors, waits, login helpers, data setup, and failure screenshots, until only a few test specialists can tell what they actually verify. + +:::info This is a brand-new v2 framework + +This page describes Midscene's newly designed v2 testing framework — a separate, new thing whose authoring model and positioning differ from the existing YAML player. This page covers the new framework only; migration and compatibility with the older version are out of scope here. + +::: + +Midscene is designed around three core ideas: + +- Cases must stay readable. Test authors write natural-language user paths in YAML, so QA, business teams, and engineers can review the case itself instead of first decoding a script implementation. +- The engineering architecture must split responsibilities cleanly. YAML focuses on what the user should accomplish; `midscene.config.ts` manages target environments, UI Agent creation, execution policy, reporting, and runtime extensions; TypeScript code owns data setup, device integration, deterministic checks, and internal tools. +- The architecture must be ready for Agentic Testing. Teams can start from the UI path, but conclusions do not have to stop at the UI. `ui`, `verify`, `agent`, skill references, and runtime extensions let tests connect API responses, database state, logs, analytics, and the tools a team already relies on. + +Midscene is not a choice between lightweight YAML and serious test engineering. It makes the first case lightweight while letting the same authoring model grow into a long-lived regression suite. + +## Start with Simple UI Tasks + +Midscene starts by helping teams write a simple UI task clearly, run it, and replay it. For most smoke tests and lightweight regression projects, the first useful milestone is not setting up a complex testing project. It is turning a core user path into a readable, repeatable, inspectable case. + +A YAML case keeps the path readable. The case only describes the user path — the target environment lives in `midscene.config.ts`, never in the case file: + +```yaml +flow: + - ui: Search for "running shoes" + - ui: Open the first product + - ui: | + Read the product name and price. + + Record them in the conclusion. + - verify: The product detail page shows a visible Add to cart button +``` + +YAML makes "what this user path should do" clear enough for review, business confirmation, and team collaboration. Midscene handles the AI UI actions, visual understanding, assertions, screenshots, and report generation around that case. + +That simple shape should cover most early projects: + +```text +. + e2e/ + dashboard.yaml + checkout.yaml + pricing.yaml +``` + +The case remains close to business language, while the runner gives it a repeatable execution and a report that can be inspected after success or failure. + +## Connect External Context with `verify` and `agent` + +`verify` and `agent` nodes are not new UI operation entries; they make judgments or explore freely from the current test context. There is a deliberate split here: Midscene itself focuses on UI capabilities (`ui` nodes are executed by Midscene's UI Agent), while nodes that need reasoning, orchestration, and external context — `verify` and `agent` — are handed to a **swappable, general-purpose agent framework**. The current built-in is Pi, the lightweight agent framework used by OpenClaw (see [earendil-works/pi](https://github.com/earendil-works/pi)). This layer is intentionally swappable: it may later be replaced with the Codex Agent SDK or other community options, so Midscene's testing capabilities evolve alongside the community agent ecosystem instead of being locked to one implementation. + +`verify` and `agent` use the same kind of Agent capability, but differ in **semantics** and in their **effect** on the test conclusion: + +- `verify` carries test judgment semantics: it must decide pass or fail, and a failed verification fails the current case. It is the test's **deterministic gate** — the part a regression suite actually uses to gate CI. +- `agent` is a free-running agent with no fixed judgment semantics. It is about **room for creativity and imagination** — summarizing, attributing, investigating deeper, proposing follow-ups, and even deciding on its own what to look at or analyze next from a natural-language instruction. Precisely because of that freedom, face its other side honestly: its output is inherently non-deterministic, and the same case can surface different observations across two runs. So `agent` by default **does not participate in the case's pass/fail decision**; it produces human-facing diagnostics and suggestions, not regression assertions. When you need a stable, reproducible verdict, use `verify`; when you want the test to add a layer of exploration and insight beyond the UI, use `agent`. + +For example, you can let `agent` freely probe the current page for potential issues: + +```yaml +flow: + - ui: Open the checkout page + - agent: | + Freely inspect the current checkout flow and find anything that looks off: + copy, prices, button states, potential usability problems. + + List your findings with likely causes and follow-up suggestions. +``` + +Every flow step produces an output. This forms an explicit **context contract**: when Pi Agent executes a `verify` or `agent` node, all it can see is — + +- **Every previous step itself** — that is, what each step was asked to do (its intent). +- **The output of each previous step**, such as conclusions recorded by `ui` nodes or `conclusion` values returned by runtime nodes. +- **The current UI screenshot**, so it can understand the current page or screen state. + +Nothing else. It does not see the full execution process of previous nodes: a `ui` node may click, type, and retry several times to create an order, but later `verify` / `agent` nodes only see **what that node finally output**. It also cannot see historical screenshots — only the current one. + +This yields one rule that holds throughout: **the only channel that carries anything forward is the output.** If a later step needs something, the earlier step must write it explicitly into its own output: + +```yaml +flow: + - ui: | + Create a test order. + + Name this step's output createOrder, and record: + - orderId: the order id + - pageState: the current page state + + - verify: | + Use $database to verify that the orderId from the output named createOrder exists. + + - agent: | + Analyze this test's risk from the output named createOrder, database verification result, + and current screenshot. +``` + +Here, `ui` still takes only natural-language input. `createOrder` is the output name requested in that natural-language instruction, and `orderId` is a field in that output. Note that since every previous step's output is already in context, naming is **not** about "it won't pass forward unless named" — it is about referring to one specific output **unambiguously** among many. Later nodes can then reference "the `orderId` from the output named `createOrder`" in natural language. + +External systems stay in natural language as well. `$database`, `$logs`, and other `$name` references are resolved by the runtime engine as skills. Pi Agent uses skill results together with previous step outputs and the current screenshot for **that single** `verify` or `agent` run. But note: **a skill result belongs only to that run** and does not automatically enter the context of later nodes. If a later step needs it, the current node must write it into its own output. + +A fuller case can look like this: + +```yaml +name: Create Order + +flow: + - prepareOrderFixture: + scenario: paid-order + - ui: | + Sign in with a test account and create a test order. + + Record in the conclusion: + - order id + - current page state + - whether the order was created successfully + - verify: | + Use $database to verify that the order id from the previous conclusion + really exists and that the order status is paid. + - verify: | + Use $logs to check whether any related ERROR appeared during the test. + - verify: The order detail page shows payment success + - agent: Analyze the risk of this test from all verification results + - notifySlack +``` + +In this example, `ui` creates the order and records order information; `verify` uses `$database` and `$logs` for external checks and returns a pass or fail judgment; `agent` summarizes the verification results and current screenshot; `notifySlack` is a custom node added later through runtime. + +The two kinds of extension here are **layered**, not competing: `$name` + skill is the **lightweight integration layer** — references like `$database` and `$logs` only need a registered skill, and then you can reference them directly in natural language at very low cost; `defineRuntime` (such as `prepareOrderFixture` and `notifySlack`) is the **lower-level extension** for defining standalone YAML nodes that own a whole step's execution. Use a `$name` skill when you just need to feed external context into `verify` / `agent`; use `defineRuntime` when you need full control over how a step runs. + +## Extension and Integration + +As a project grows from lightweight cases into a long-lived regression suite, engineering complexity should move into configuration and extension layers instead of being copied into every YAML file. Midscene provides `midscene.config.ts` as the project-level config-as-code entry for test discovery, execution policy, output, UI Agent creation, and runtime extensions. + +```ts +import { defineMidsceneConfig } from '@midscene/testing-framework'; + +export default defineMidsceneConfig({ + // A single `uiAgent` field defines the run target. An object is config-style + // (the framework builds the UI Agent from `type` + `options`); a function is + // programmatic (you build it yourself, see below). + uiAgent: { + type: 'web', + options: { + url: 'https://shop.example.com', + }, + }, + + testDir: './e2e', + include: ['**/*.yaml'], + exclude: ['**/*.draft.yaml'], + + testRunner: { + maxConcurrency: 1, + bail: 0, + testTimeout: 120_000, + }, + + output: { + summary: './midscene_run/output/summary.json', + reportDir: './midscene_run/report', + }, + + uiAgentOptions: { + aiActContext: 'The user is already signed in as a smoke-test account.', + generateReport: true, + }, +}); +``` + +With this config in place, the project can stay direct: + +```text +. + midscene.config.ts + e2e/ + dashboard.yaml + checkout.yaml +``` + +`e2e/*.yaml` describes what the user should accomplish, while `midscene.config.ts` describes the target type and platform connection options, testRunner behavior, shared UI Agent options, and reporting. When `uiAgent` is an object, the framework creates the UI Agent from its `type` and `options`. If a project needs custom devices, remote services, or custom agent construction logic, set `uiAgent` to a factory function instead — the same single field, now holding the construction logic, so the runtime target is never defined twice. `options` (platform connection parameters like url / deviceId) and `uiAgentOptions` (Agent behavior like aiActContext / generateReport) stay distinct. + +```ts +import { agentFromAdbDevice } from '@midscene/android'; +import { defineMidsceneConfig } from '@midscene/testing-framework'; + +export default defineMidsceneConfig({ + testDir: './e2e', + + uiAgentOptions: { + aiActContext: 'The user is already signed in as a smoke-test account.', + generateReport: true, + }, + + // Programmatic form: the same `uiAgent` field, filled with a factory. + uiAgent: async ({ uiAgentOptions, env }) => ({ + agent: await agentFromAdbDevice(env.ANDROID_DEVICE_ID, { + ...uiAgentOptions, + androidAdbPath: env.ANDROID_ADB_PATH, + autoDismissKeyboard: false, + }), + }), +}); +``` + +YAML can also gain new project-specific nodes. Compared with the lightweight `$name` skill integration, `defineRuntime` is the lower-level extension: it defines standalone YAML nodes that own a whole step's execution. For example, `prepareOrderFixture` and `notifySlack` can be registered as custom runtimes: + +```ts +import { + defineMidsceneConfig, + defineRuntime, +} from '@midscene/testing-framework'; + +export default defineMidsceneConfig({ + uiAgent: { + type: 'web', + options: { + url: 'http://127.0.0.1:3000', + }, + }, + + testDir: './e2e', + + runtime: { + prepareOrderFixture: defineRuntime(async (ctx) => { + const fixture = await createOrderFixture(ctx.input); + ctx.state.orderFixture = fixture; + + return { + conclusion: `Prepared order fixture ${fixture.id}`, + }; + }), + + notifySlack: defineRuntime(async (ctx) => { + await sendSlackSummary(ctx.result); + + return { + conclusion: 'Slack notification sent', + }; + }), + }, +}); +``` + +A runtime node receives a single context argument with `input`, `uiAgent`, `outputs`, `state`, `result`, and `env`. It has two channels, matching the context contract above — keep them distinct: + +- The `conclusion` in the return value is the **context-facing output**: like any other step's output, it enters the context of later `verify` / `agent` nodes. +- `ctx.state` (such as `ctx.state.orderFixture`) is **engineering-facing TypeScript state** for passing structured data between runtime nodes, and **does not enter Pi Agent's context**. In other words, the agent cannot see `ctx.state`, only `conclusion`. To make a value available to a later `verify` / `agent`, put it in `conclusion`. + +This direction keeps the low-friction YAML-driven UI testing model intact. YAML remains the human-facing expression for the test, and TypeScript config remains the engineering entry for registering capabilities: ordinary paths stay in natural language, while places that need deterministic evidence can connect to the team's own tools. + +## Built on Rstest + +Midscene is built as a higher-level testing framework on top of Rstest. For an AI-driven UI testing framework, the real value is not how fast the runner is — each node's duration is dominated by model inference — but whether it can reliably carry the capabilities a test engineering setup needs: lifecycle, fixtures, concurrency, filtering, failure reporting, and CI integration. Rstest provides these at the base layer, and Midscene wraps them with natural-language cases, AI UI actions, visual assertions, screenshots, replay reports, and diagnostics. + +Most users can rely on that foundation through Midscene's YAML runner and `midscene.config.ts` without learning Rstest project details. The `midscene.config.ts` fields are intentionally aligned with Rstest concepts such as include/exclude, maxConcurrency, retry, timeout, setup, teardown, and reporters, while keeping Midscene-specific UI Agent creation in the same config. + +### What Rstest Provides + +Rstest gives the Midscene project a reliable test engineering base: + +- **Standard test lifecycle**: setup / teardown / hooks give login setup, test-data initialization, and cleanup explicit attachment points instead of pushing them into every case. +- **Fixture model**: declare shared prerequisites (accounts, device connections, fixture data) as reusable, composable fixtures, injected per case as needed. +- **Concurrency and isolation**: cases can run concurrently, with the runner handling scheduling and isolation so a regression suite's total CI time stays manageable. +- **Filtering and failure reporting**: filter cases by file, name, or tag, paired with standard failure reports for easy triage and reruns. +- **Unified runtime model**: YAML cases, runtime nodes, and config extensions share the same underlying runtime model, so teams can start lightweight and grow into a long-lived regression suite without switching frameworks. + +Rstest is itself written in Rust with good execution performance; but for Midscene users, the mature test engineering capabilities above matter more than the runner's raw speed — in AI testing, the time is mostly spent on model inference. + +### Next Steps + +- Run a YAML case from the command line: [YAML script runner](./yaml-script-runner) +- Look up every YAML field: [Workflow in YAML format](./automate-with-scripts-in-yaml) +- Start from platform guides: [Android](./android-getting-started), [iOS](./ios-getting-started), [Computer](./computer-getting-started) diff --git a/apps/site/docs/zh/ui-testing-framework.mdx b/apps/site/docs/zh/ui-testing-framework.mdx new file mode 100644 index 0000000000..caa8bd9017 --- /dev/null +++ b/apps/site/docs/zh/ui-testing-framework.mdx @@ -0,0 +1,271 @@ +# 面向自然语言用例的 AI 原生 UI Testing Framework + +UI Test 真正的难点,不是写出第一条浏览器脚本,而是让团队长期愿意写、看得懂、维护得起。传统脚本很快会被选择器、等待逻辑、登录辅助函数、测试数据准备和失败截图塞满,最后只有少数测试工程师能理解它们到底在验证什么。 + +:::info 这是全新的 v2 测试框架 + +本文描述的是 Midscene 全新设计的 v2 测试框架——一套独立的新事物,它的表达方式和定位都与现有 YAML player 不同。本文只介绍这套新框架本身,不涉及与旧版本的迁移或兼容。 + +::: + +Midscene 的设计围绕三个核心要点展开: + +- 用例必须可读。测试作者用 YAML 写自然语言用户路径,QA、业务同学和工程师都能直接 review case 本身,而不是先读懂一套脚本实现。 +- 工程架构必须优雅拆分职责。YAML 专注描述用户要完成什么;`midscene.config.ts` 管理目标环境、UI Agent 创建、运行策略、报告输出和 runtime 扩展;TypeScript 代码承接数据准备、设备接入、确定性校验和团队内部工具。 +- 架构必须面向 Agentic Testing。团队可以从 UI 路径切入测试,但结论不必止步于 UI。`ui`、`verify`、`agent`、skill 引用和 runtime 扩展让测试可以继续连接接口响应、数据库状态、日志、埋点和团队已有工具。 + +Midscene 不是让团队在“轻量 YAML”和“严肃测试工程”之间二选一,而是让第一条 case 足够轻,同时让同一套表达方式继续长成长期回归套件。 + +## 从简单 UI 任务开始 + +Midscene 的第一步,是让团队用 YAML 把一个简单 UI 任务写清楚、跑起来、回放出来。对于大多数 Smoke Test 和轻量回归项目,第一个有价值的里程碑不是搭建复杂工程,而是把核心用户路径变成可读、可重复执行、可分析的 case。 + +YAML case 可以让路径保持可读。case 只描述用户路径,运行目标环境放在 `midscene.config.ts` 里,绝不写进 case 文件: + +```yaml +flow: + - ui: Search for "running shoes" + - ui: Open the first product + - ui: | + Read the product name and price. + + Record them in the conclusion. + - verify: The product detail page shows a visible Add to cart button +``` + +YAML 可以把“一个用户路径应该是什么样”组织得足够清楚,便于 code review、业务确认和团队协作。围绕这个 case,Midscene 负责 AI UI 操作、视觉理解、断言、截图和报告生成。 + +这种简单形态可以覆盖大多数早期项目: + +```text +. + e2e/ + dashboard.yaml + checkout.yaml + pricing.yaml +``` + +用例本身仍然接近业务语言,runner 则提供可重复执行的过程,以及成功或失败后都可以检查的报告。 + +## 用 `verify` 和 `agent` 连接外部能力 + +`verify` 和 `agent` 节点不是新的 UI 操作入口,而是基于当前测试上下文做判断或自由探索。这里有一个有意为之的分工:Midscene 自身专注 UI 能力(`ui` 节点由 Midscene 的 UI Agent 执行);而 `verify` 和 `agent` 这类需要推理、编排、连接外部上下文的节点,交给一个**可替换的通用 Agent 框架**来执行。当前内置的是 Pi——OpenClaw 采用的轻量 Agent 框架(参见 [earendil-works/pi](https://github.com/earendil-works/pi))。这一层刻意做成可替换的:未来也可能换成 Codex Agent SDK 等社区方案,让 Midscene 的测试能力跟随社区 Agent 生态一起演进,而不是绑死在某一个实现上。 + +`verify` 和 `agent` 使用同一类 Agent 能力,区别在于**语义**,以及它们对测试结论的**影响**: + +- `verify` 带有测试判定语义:它必须给出通过或不通过的结论,不通过会让当前 case 失败。它是测试的**确定性闸门**,是回归套件真正用来 gate CI 的部分。 +- `agent` 是一个自由运行的 Agent,没有固定判定语义,强调的是**创造和想象的空间**——总结、归因、深入排查、提出后续建议,甚至按自然语言要求自行决定接下来该看什么、分析什么。也正因为这种自由,要正视它的另一面:它的输出天然带有不确定性,同一个 case 两次运行可能给出不同的观察。因此 `agent` 默认**不参与 case 的通过/失败判定**,它产出的是供人阅读的诊断与建议,而不是回归断言。需要稳定、可复现地卡住结论时,用 `verify`;想让测试在 UI 之外多一层探索和洞察时,用 `agent`。 + +比如,可以让 `agent` 在当前页面上自由探查潜在问题: + +```yaml +flow: + - ui: 打开结账页面 + - agent: | + 自由检查当前结账流程,找出任何看起来不合理的地方: + 文案、价格、按钮状态、潜在的可用性问题。 + + 列出你的发现,并给出可能的原因和后续建议。 +``` + +每个 flow 步骤都有输出。这构成了一条明确的**上下文契约**:当 Pi Agent 执行某个 `verify` 或 `agent` 节点时,它能看到的全部就是—— + +- **所有过往步骤本身**,也就是每一步要做什么(它的意图)。 +- **每个过往步骤的输出**,例如 `ui` 节点记录的结论、runtime 节点返回的 `conclusion`。 +- **当前 UI 截图**,用来理解此刻页面或屏幕上的状态。 + +除此之外,没有别的。它不会看到前序节点的完整执行过程:一个 `ui` 节点为了创建订单可能经历了多次点击、输入和重试,后续 `verify` / `agent` 只能看到这个节点**最终输出了什么**。它也看不到历史截图——只有当前这一张。 + +由此得到一条贯穿始终的规则:**唯一能往后传递的通道就是 output。** 后续步骤要用到某个东西,前面那一步就必须把它明确写进自己的输出里: + +```yaml +flow: + - ui: | + 创建一笔测试订单。 + + 将这一步的输出命名为 createOrder,并记录: + - orderId: 订单号 + - pageState: 当前页面状态 + + - verify: | + 使用 $database 验证名为 createOrder 的输出中的 orderId 是否真实存在。 + + - agent: | + 根据名为 createOrder 的输出、数据库验证结果和当前截图,分析本次测试风险。 +``` + +这里的 `ui` 仍然只有自然语言输入。`createOrder` 是这段自然语言要求 Pi Agent 记录的输出名称,`orderId` 是该输出里的字段。需要说明的是:既然所有过往步骤的输出本就都在上下文里,命名**不是**“不命名就传不过去”,而是为了在多个输出之间**无歧义地指代**某一个——后续节点可以直接用自然语言引用“名为 `createOrder` 的输出中的 `orderId`”。 + +对外部系统的引用也保持在自然语言里。`$database`、`$logs` 这样的 `$name` 会被运行时引擎解析为对应 skill;Pi Agent 会把 skill 结果、过往步骤的输出和当前截图一起,用于**当前这一次** `verify` 或 `agent`。但要注意:**skill 结果只属于这一次执行**,不会自动进入后续节点的上下文。如果后面还要用到,需由当前节点把它写进自己的输出。 + +一个更完整的 case 可以长成这样: + +```yaml +name: Create Order + +flow: + - prepareOrderFixture: + scenario: paid-order + - ui: | + 使用测试账号登录系统,创建一笔测试订单。 + + 在结论中记录: + - 订单号 + - 当前页面状态 + - 是否创建成功 + - verify: | + 使用 $database 验证前面结论中的订单号是否真实存在,且订单状态是 paid。 + - verify: | + 使用 $logs 检查测试期间是否出现相关 ERROR。 + - verify: 订单详情页展示支付成功 + - agent: 根据所有验证结果分析本次测试风险 + - notifySlack +``` + +这个例子里,`ui` 负责创建订单并输出订单信息;`verify` 用 `$database` 和 `$logs` 做外部验证,并给出通过或不通过的判断;`agent` 汇总验证结果和当前截图;`notifySlack` 是后面通过 runtime 扩展出来的自定义节点。 + +这里的两种扩展方式是**分层**的,并不冲突:`$name` + skill 是**轻量接入层**——像 `$database`、`$logs` 这样的 `$name` 引用,只要注册好对应 skill,就能在自然语言里直接引用,接入成本很低;`defineRuntime`(如 `prepareOrderFixture`、`notifySlack`)是**更底层的扩展方案**,用来定义独立的 YAML 节点、接管一整步的执行逻辑。需要快速把外部上下文喂给 `verify` / `agent`,就用 `$name` skill;需要完全掌控一个步骤怎么跑,就用 `defineRuntime`。 + +## 扩展和集成能力 + +当项目从轻量 case 长成长期回归套件时,工程复杂度应该进入配置和扩展层,而不是塞回每个 YAML 文件。Midscene 提供 `midscene.config.ts` 作为项目级 config-as-code 入口,用来管理用例发现、执行策略、输出位置、UI Agent 创建和 runtime 扩展。 + +```ts +import { defineMidsceneConfig } from '@midscene/testing-framework'; + +export default defineMidsceneConfig({ + // 单个 `uiAgent` 字段定义运行目标。传对象是配置式(框架据 `type` + `options` + // 创建 UI Agent);传函数是编程式(自行构造,见下文)。 + uiAgent: { + type: 'web', + options: { + url: 'https://shop.example.com', + }, + }, + + testDir: './e2e', + include: ['**/*.yaml'], + exclude: ['**/*.draft.yaml'], + + testRunner: { + maxConcurrency: 1, + bail: 0, + testTimeout: 120_000, + }, + + output: { + summary: './midscene_run/output/summary.json', + reportDir: './midscene_run/report', + }, + + uiAgentOptions: { + aiActContext: 'The user is already signed in as a smoke-test account.', + generateReport: true, + }, +}); +``` + +有了这个配置之后,项目结构仍然可以保持直接: + +```text +. + midscene.config.ts + e2e/ + dashboard.yaml + checkout.yaml +``` + +`e2e/*.yaml` 描述用户要完成什么,`midscene.config.ts` 描述 target 类型和平台连接参数、testRunner 行为、共享 UI Agent 参数和报告。当 `uiAgent` 是对象时,框架会据其 `type` 和 `options` 创建 UI Agent;如果项目需要接入自定义设备、远程服务或自定义的 Agent 构造逻辑,把 `uiAgent` 设为工厂函数即可——还是同一个字段,只是换成构造逻辑,从根上避免出现两套运行目标定义。`options`(平台连接参数,如 url / deviceId)与 `uiAgentOptions`(Agent 行为,如 aiActContext / generateReport)是两类不同的东西,都保留。 + +```ts +import { agentFromAdbDevice } from '@midscene/android'; +import { defineMidsceneConfig } from '@midscene/testing-framework'; + +export default defineMidsceneConfig({ + testDir: './e2e', + + uiAgentOptions: { + aiActContext: 'The user is already signed in as a smoke-test account.', + generateReport: true, + }, + + // 编程式:同一个 `uiAgent` 字段,填工厂函数。 + uiAgent: async ({ uiAgentOptions, env }) => ({ + agent: await agentFromAdbDevice(env.ANDROID_DEVICE_ID, { + ...uiAgentOptions, + androidAdbPath: env.ANDROID_ADB_PATH, + autoDismissKeyboard: false, + }), + }), +}); +``` + +YAML 也可以按项目需要扩展新的节点。相比 `$name` skill 的轻量接入,`defineRuntime` 是更底层的扩展方案:它定义独立的 YAML 节点、接管整步执行逻辑。比如 `prepareOrderFixture` 和 `notifySlack` 可以注册成自定义 runtime: + +```ts +import { + defineMidsceneConfig, + defineRuntime, +} from '@midscene/testing-framework'; + +export default defineMidsceneConfig({ + uiAgent: { + type: 'web', + options: { + url: 'http://127.0.0.1:3000', + }, + }, + + testDir: './e2e', + + runtime: { + prepareOrderFixture: defineRuntime(async (ctx) => { + const fixture = await createOrderFixture(ctx.input); + ctx.state.orderFixture = fixture; + + return { + conclusion: `Prepared order fixture ${fixture.id}`, + }; + }), + + notifySlack: defineRuntime(async (ctx) => { + await sendSlackSummary(ctx.result); + + return { + conclusion: 'Slack notification sent', + }; + }), + }, +}); +``` + +runtime 节点接收单个上下文参数,包含 `input`、`uiAgent`、`outputs`、`state`、`result`、`env`。它有两条信道,对应上面讲过的上下文契约,要分清: + +- 返回值里的 `conclusion` 是**面向上下文的输出**,会和其它步骤的输出一样进入后续 `verify` / `agent` 的上下文。 +- `ctx.state`(如 `ctx.state.orderFixture`)是**面向工程的 TypeScript 状态**,供 runtime 节点之间传递结构化数据,**不会进入 Pi Agent 的上下文**。换句话说,agent 看不到 `ctx.state`,只看得到 `conclusion`。要让某个值被后续的 `verify` / `agent` 用到,就得把它放进 `conclusion`。 + +这条路线不会丢掉 YAML 驱动 UI Test 的低门槛。相反,它把 YAML 作为面向人的测试表达,把 TypeScript 配置作为面向工程的能力注册入口:普通路径继续用自然语言描述,真正需要确定性证据的地方再接入团队自己的工具。 + +## 基于 Rstest 构建 + +Midscene 是基于 Rstest 封装构建的上层测试框架。对一个 AI 驱动的 UI 测试框架来说,真正的价值不在 runner 本身有多快——每个节点的耗时主要由模型推理决定——而在于它能不能稳稳地接住一套测试工程该有的能力:生命周期、fixture、并发、用例过滤、失败上报和 CI 接入。Rstest 在底层提供了这些,Midscene 则把它们封装成自然语言用例、AI UI 操作、视觉断言、截图、回放报告和诊断信息。 + +绝大多数用户可以通过 Midscene 的 YAML runner 和 `midscene.config.ts` 直接使用这套底座,无需了解 Rstest 的项目细节。`midscene.config.ts` 的字段会刻意和 Rstest 的概念对齐,例如 include/exclude、maxConcurrency、retry、timeout、setup、teardown 和 reporters,同时把 Midscene 特有的 UI Agent 创建入口留在同一个配置里。 + +### Rstest 提供的工程能力 + +Rstest 为 Midscene 项目提供可靠的测试工程底座: + +- **标准测试生命周期**:setup / teardown / hook 给登录态准备、测试数据初始化和清理提供明确的挂载点,而不必把这些塞进每个用例。 +- **Fixture 模型**:把共享的前置依赖(账号、设备连接、fixture 数据)声明成可复用、可组合的 fixture,并按用例需要注入。 +- **并发与隔离**:用例可以并发执行,由 runner 负责调度与隔离,让回归套件在 CI 上的整体耗时可控。 +- **用例过滤与失败上报**:按文件、名称或标签筛选用例,配合标准的失败报告,方便定位和重跑。 +- **统一运行模型**:YAML case、runtime 节点和配置扩展共享同一个底层运行模型,团队可以从轻量项目起步,再自然长成长期回归套件,而不必更换框架。 + +Rstest 本身基于 Rust 编写、执行层性能良好;但对 Midscene 用户而言,更有价值的是上面这套成熟的测试工程能力,而不是 runner 的原始速度——毕竟在 AI 测试里,时间主要花在模型推理上。 + +### 下一步 + +- 从命令行运行 YAML case:[YAML 脚本运行器](./yaml-script-runner) +- 查询完整 YAML 字段:[YAML 格式的工作流](./automate-with-scripts-in-yaml) +- 从平台指南开始:[Android](./android-getting-started)、[iOS](./ios-getting-started)、[Computer](./computer-getting-started) diff --git a/packages/core/src/connection-options.ts b/packages/core/src/connection-options.ts new file mode 100644 index 0000000000..3ec74d4410 --- /dev/null +++ b/packages/core/src/connection-options.ts @@ -0,0 +1,127 @@ +/** + * Canonical per-platform connection / launch target options. + * + * These are the first-class "how to reach the target" types. They describe the + * connection only — agent behavior (`AgentOpt`) and YAML run config + * (`MidsceneYamlScriptConfig`) are expressed separately. The + * `MidsceneYamlScript*Env` types in `./yaml` are composed FROM these (env = + * connection + run config + agent behavior), so the connection options are the + * source of truth, not a byproduct of the YAML schema. + */ +import type { + AndroidDeviceOpt, + HarmonyDeviceOpt, + IOSDeviceOpt, +} from './device'; + +/** How to reach / launch a web target. */ +export interface WebConnectionOpt { + // for web only + serve?: string; + url: string; + + // puppeteer only + userAgent?: string; + acceptInsecureCerts?: boolean; + viewportWidth?: number; + viewportHeight?: number; + deviceScaleFactor?: number; + waitForNetworkIdle?: { + timeout?: number; + continueOnNetworkIdleError?: boolean; // should continue if failed to wait for network idle, true for default + }; + cookie?: string; + + /** + * Extra HTTP headers sent with every request (Puppeteer only, not supported + * in bridge mode). Useful when the server validates custom request headers. + * + * Header values must be strings. Quote values that YAML would otherwise parse + * as a boolean or number (e.g. `true`, `false`, `123`), such as `"true"`. + * + * @example + * ```yaml + * web: + * url: https://example.com + * extraHTTPHeaders: + * X-Custom-Token: my-token + * Accept-Language: en-US + * ``` + */ + extraHTTPHeaders?: Record; + + forceSameTabNavigation?: boolean; // if track the newly opened tab, true for default in yaml script + + /** + * Custom Chrome launch arguments (Puppeteer only, not supported in bridge mode). + * + * Allows passing custom command-line arguments to Chrome/Chromium when launching the browser. + * This is useful for testing scenarios that require specific browser configurations. + * + * ⚠️ Security Warning: Some arguments (e.g., --no-sandbox, --disable-web-security) may + * reduce browser security. Use only in controlled testing environments. + * + * @example + * ```yaml + * web: + * url: https://example.com + * chromeArgs: + * - '--disable-features=ThirdPartyCookiePhaseout' + * - '--disable-features=SameSiteByDefaultCookies' + * - '--window-size=1920,1080' + * ``` + */ + chromeArgs?: string[]; + + // bridge mode config + bridgeMode?: false | 'newTabWithUrl' | 'currentTab'; + closeNewTabsAfterDisconnect?: boolean; + + /** + * CDP (Chrome DevTools Protocol) endpoint URL. + * When specified, connects to an existing Chrome browser via CDP instead of launching a new one. + * + * @example + * ```yaml + * web: + * url: https://example.com + * cdpEndpoint: ws://localhost:9222/devtools/browser/xxxx + * ``` + */ + cdpEndpoint?: string; +} + +/** How to reach / launch an Android target (device driver options + which device + what to launch). */ +export interface AndroidConnectionOpt + extends Omit { + // The Android device ID to connect to, optional, will use the first device if not specified + deviceId?: string; + + // The URL or app package to launch, optional, will use the current screen if not specified + launch?: string; +} + +/** How to reach / launch an iOS target. */ +export interface IOSConnectionOpt extends Omit { + // The URL or app bundle ID to launch, optional, will use the current screen if not specified + launch?: string; +} + +/** How to reach / launch a HarmonyOS target. */ +export interface HarmonyConnectionOpt + extends Omit { + // The HarmonyOS device ID to connect to, optional, will use the first device if not specified + deviceId?: string; + + // The app package to launch, optional, will use the current screen if not specified + launch?: string; + + // Custom mapping of app names to bundle names, user-provided mappings take precedence over defaults + appNameMapping?: Record; +} + +/** How to reach a computer target. */ +export interface ComputerConnectionOpt { + // The display ID to use, optional, will use the primary display if not specified + displayId?: string; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ff12df9d4f..295cb70a2a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -52,11 +52,20 @@ export type { MidsceneYamlScriptWebEnv, MidsceneYamlScriptAndroidEnv, MidsceneYamlScriptIOSEnv, + MidsceneYamlScriptComputerEnv, MidsceneYamlScriptEnv, LocateOption, DetailedLocateParam, } from './yaml'; +export type { + WebConnectionOpt, + AndroidConnectionOpt, + IOSConnectionOpt, + HarmonyConnectionOpt, + ComputerConnectionOpt, +} from './connection-options'; + export { Agent, type AgentOpt, type AiActOptions, createAgent } from './agent'; // Dump utilities diff --git a/packages/core/src/yaml.ts b/packages/core/src/yaml.ts index 81f8b9b5b8..16ae1101dc 100644 --- a/packages/core/src/yaml.ts +++ b/packages/core/src/yaml.ts @@ -1,9 +1,11 @@ import type { TMultimodalPrompt, TUserPrompt } from './common'; import type { - AndroidDeviceOpt, - HarmonyDeviceOpt, - IOSDeviceOpt, -} from './device'; + AndroidConnectionOpt, + ComputerConnectionOpt, + HarmonyConnectionOpt, + IOSConnectionOpt, + WebConnectionOpt, +} from './connection-options'; import type { AgentOpt, LocateResultElement, Rect } from './types'; import type { UIContext } from './types'; @@ -125,119 +127,29 @@ export interface MidsceneYamlScriptEnvGeneralInterface { param?: Record; } +// The YAML-script env types are the connection options plus the YAML run +// config (and, for web, agent behavior). Connection options are the source of +// truth — see `./connection-options`. export interface MidsceneYamlScriptWebEnv - extends MidsceneYamlScriptConfig, - MidsceneYamlScriptAgentOpt { - // for web only - serve?: string; - url: string; - - // puppeteer only - userAgent?: string; - acceptInsecureCerts?: boolean; - viewportWidth?: number; - viewportHeight?: number; - deviceScaleFactor?: number; - waitForNetworkIdle?: { - timeout?: number; - continueOnNetworkIdleError?: boolean; // should continue if failed to wait for network idle, true for default - }; - cookie?: string; - - /** - * Extra HTTP headers sent with every request (Puppeteer only, not supported - * in bridge mode). Useful when the server validates custom request headers. - * - * Header values must be strings. Quote values that YAML would otherwise parse - * as a boolean or number (e.g. `true`, `false`, `123`), such as `"true"`. - * - * @example - * ```yaml - * web: - * url: https://example.com - * extraHTTPHeaders: - * X-Custom-Token: my-token - * Accept-Language: en-US - * ``` - */ - extraHTTPHeaders?: Record; - - forceSameTabNavigation?: boolean; // if track the newly opened tab, true for default in yaml script - - /** - * Custom Chrome launch arguments (Puppeteer only, not supported in bridge mode). - * - * Allows passing custom command-line arguments to Chrome/Chromium when launching the browser. - * This is useful for testing scenarios that require specific browser configurations. - * - * ⚠️ Security Warning: Some arguments (e.g., --no-sandbox, --disable-web-security) may - * reduce browser security. Use only in controlled testing environments. - * - * @example - * ```yaml - * web: - * url: https://example.com - * chromeArgs: - * - '--disable-features=ThirdPartyCookiePhaseout' - * - '--disable-features=SameSiteByDefaultCookies' - * - '--window-size=1920,1080' - * ``` - */ - chromeArgs?: string[]; - - // bridge mode config - bridgeMode?: false | 'newTabWithUrl' | 'currentTab'; - closeNewTabsAfterDisconnect?: boolean; - - /** - * CDP (Chrome DevTools Protocol) endpoint URL. - * When specified, connects to an existing Chrome browser via CDP instead of launching a new one. - * - * @example - * ```yaml - * web: - * url: https://example.com - * cdpEndpoint: ws://localhost:9222/devtools/browser/xxxx - * ``` - */ - cdpEndpoint?: string; -} + extends WebConnectionOpt, + MidsceneYamlScriptConfig, + MidsceneYamlScriptAgentOpt {} export interface MidsceneYamlScriptAndroidEnv - extends MidsceneYamlScriptConfig, - Omit { - // The Android device ID to connect to, optional, will use the first device if not specified - deviceId?: string; - - // The URL or app package to launch, optional, will use the current screen if not specified - launch?: string; -} + extends AndroidConnectionOpt, + MidsceneYamlScriptConfig {} export interface MidsceneYamlScriptIOSEnv - extends MidsceneYamlScriptConfig, - Omit { - // The URL or app bundle ID to launch, optional, will use the current screen if not specified - launch?: string; -} + extends IOSConnectionOpt, + MidsceneYamlScriptConfig {} export interface MidsceneYamlScriptHarmonyEnv - extends MidsceneYamlScriptConfig, - Omit { - // The HarmonyOS device ID to connect to, optional, will use the first device if not specified - deviceId?: string; - - // The app package to launch, optional, will use the current screen if not specified - launch?: string; - - // Custom mapping of app names to bundle names, user-provided mappings take precedence over defaults - appNameMapping?: Record; -} + extends HarmonyConnectionOpt, + MidsceneYamlScriptConfig {} export interface MidsceneYamlScriptComputerEnv - extends MidsceneYamlScriptConfig { - // The display ID to use, optional, will use the primary display if not specified - displayId?: string; -} + extends ComputerConnectionOpt, + MidsceneYamlScriptConfig {} export type MidsceneYamlScriptEnv = | MidsceneYamlScriptWebEnv diff --git a/packages/testing-framework/README.md b/packages/testing-framework/README.md new file mode 100644 index 0000000000..00ba21717a --- /dev/null +++ b/packages/testing-framework/README.md @@ -0,0 +1,74 @@ +# @midscene/testing-framework + +AI-native v2 UI testing framework for natural-language cases (Phase 0). + +Write test cases as natural-language flows in YAML; let Midscene's UI Agent drive +the UI and a swappable general-purpose agent (Pi by default) make gating +judgments and free-form analysis. + +> This is the Phase 0 implementation of RFC 0001 +> (`rfcs/0001-v2-testing-framework-phase0.md`). It covers the node model, +> `midscene.config.ts`, `defineRuntime` / `$name` skills, the verify verdict +> contract, the output contract, and context assembly. Rstest wiring and +> v1→v2 migration are out of scope for this phase. + +## Concepts + +- **Cases are natural language.** A case YAML has only a `name` and a `flow`; + the environment/target lives in `midscene.config.ts`. +- **Node model:** + - `ui` — natural-language UI action, run by Midscene's UI Agent. + - `verify` — gating judgment; must produce a pass/fail verdict (fail-closed). + - `soft` — same as verify, but failure only records a warning. + - `agent` — advisory free exploration; never changes pass/fail. + - custom nodes — registered via `defineRuntime`, own a whole step. +- **One model endpoint.** `verify`/`soft`/`agent` run on Pi, pointed at the same + `MIDSCENE_MODEL_BASE_URL` endpoint as the UI Agent (RFC decision C′). +- **Output is the only channel forward.** Each step records a natural-language + conclusion; later nodes reference it by name. The current screenshot is the + only image; `state` (engineering-facing) never reaches the agent. + +## Quick start + +```ts +// midscene.config.ts +import { defineMidsceneConfig } from '@midscene/testing-framework'; + +export default defineMidsceneConfig({ + uiAgent: { type: 'web', options: { url: 'https://shop.example.com' } }, + testDir: './e2e', + output: { summary: './midscene_run/output/summary.json' }, + uiAgentOptions: { generateReport: true }, +}); +``` + +```yaml +# e2e/checkout.yaml +name: Add to cart +flow: + - ui: Open the first product + - verify: The product detail page shows a visible "Add to cart" button + - agent: Inspect the page for anything that looks off +``` + +```bash +midscene-tf run # run all discovered cases +midscene-tf run e2e/x.yaml # run a specific case +``` + +See a runnable demo in this package's [`example/`](./example) directory. + +## Programmatic API + +```ts +import { runAll, loadConfig } from '@midscene/testing-framework'; + +const { config } = await loadConfig(process.cwd()); +const summary = await runAll(config); +``` + +## Swapping the general agent + +The general agent that backs `verify`/`soft`/`agent` is swappable. Provide your +own `generalAgent` (a `GeneralAgentAdapter`) in `midscene.config.ts` to replace +the default Pi-backed implementation. diff --git a/packages/testing-framework/bin/midscene-tf b/packages/testing-framework/bin/midscene-tf new file mode 100755 index 0000000000..0942d7aea8 --- /dev/null +++ b/packages/testing-framework/bin/midscene-tf @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require("../dist/lib/cli.js"); diff --git a/packages/testing-framework/example/.gitignore b/packages/testing-framework/example/.gitignore new file mode 100644 index 0000000000..86b63a7a6e --- /dev/null +++ b/packages/testing-framework/example/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +midscene_run/ +.env diff --git a/packages/testing-framework/example/README.md b/packages/testing-framework/example/README.md new file mode 100644 index 0000000000..637e075344 --- /dev/null +++ b/packages/testing-framework/example/README.md @@ -0,0 +1,55 @@ +# Midscene v2 Testing Framework — Example + +A self-contained demo of [`@midscene/testing-framework`](..) +(the AI-native v2 UI testing framework, Phase 0). Copy this folder out, install, +set your model env vars, and run. + +## What it shows + +- A **config-style** `uiAgent` (web) in `midscene.config.ts` — environment lives + in config, never in the case YAML. +- The full node model in `e2e/*.yaml`: + - `ui` — natural-language UI actions (run by Midscene's UI Agent) + - `verify` — gating judgment with a forced pass/fail verdict + - `soft` — non-gating soft assertion (failure → warning only) + - `agent` — advisory free exploration (never gates) + - custom **runtime** nodes (`prepareCartFixture`, `notify`) via `defineRuntime` +- A `$name` **skill** reference (`$catalog`) backed by `skills/catalog/SKILL.md`. +- The **output contract**: steps record natural-language conclusions that later + `verify` / `agent` nodes reference by name. + +## Run it + +```bash +# 1. install +pnpm install # or npm install / yarn + +# 2. configure the model (UI Agent + Pi share one endpoint) +cp .env.example .env # then edit, or export the vars in your shell + +# 3. run all cases +pnpm test + +# run a single case +pnpm test:one +``` + +By default the demo runs against the bundled static page in `site/index.html` +(offline). Set `DEMO_URL` to point at your own app. + +Results are written to `midscene_run/output/summary.json`, and Midscene HTML +reports for the UI steps land in `midscene_run/report/`. + +## Layout + +```text +. + midscene.config.ts # uiAgent + discovery + runtime nodes + e2e/ + product-detail.yaml # ui + verify + soft + agent + add-to-cart.yaml # custom node + $catalog skill + verify + agent + notify + skills/ + catalog/SKILL.md # a $name skill (Pi discovers/loads it) + site/ + index.html # tiny static demo app +``` diff --git a/packages/testing-framework/example/e2e/add-to-cart.yaml b/packages/testing-framework/example/e2e/add-to-cart.yaml new file mode 100644 index 0000000000..df760db381 --- /dev/null +++ b/packages/testing-framework/example/e2e/add-to-cart.yaml @@ -0,0 +1,26 @@ +name: Add a product to the cart + +flow: + - prepareCartFixture: + scenario: anonymous-checkout + + - ui: Open the "Trail Backpack" product + + - ui: | + Click "Add to cart". + Name this step's output cartResult and record whether the + "Added to cart" confirmation became visible. + + - verify: | + Use the output named cartResult and the current screenshot to confirm + the product was added to the cart (the "Added to cart" badge is visible). + + - verify: | + Use $catalog to confirm the "Trail Backpack" is a known catalog product + and report its expected price. + + - agent: | + Summarize the risk of this add-to-cart flow based on all previous + verification results and the current screenshot. + + - notify diff --git a/packages/testing-framework/example/e2e/product-detail.yaml b/packages/testing-framework/example/e2e/product-detail.yaml new file mode 100644 index 0000000000..34efd3dd44 --- /dev/null +++ b/packages/testing-framework/example/e2e/product-detail.yaml @@ -0,0 +1,12 @@ +name: Open a product and check Add to cart + +flow: + - ui: Open the "Running Shoes" product + - ui: | + Read the product name and price on the detail page. + Name this step's output productInfo and record the name and price. + - verify: The product detail page shows a visible "Add to cart" button + - soft: The page has no obvious layout glitches + - agent: | + Briefly inspect this product detail page and note anything that looks + off (copy, pricing, button states) with follow-up suggestions. diff --git a/packages/testing-framework/example/midscene.config.ts b/packages/testing-framework/example/midscene.config.ts new file mode 100644 index 0000000000..2393ad1f7e --- /dev/null +++ b/packages/testing-framework/example/midscene.config.ts @@ -0,0 +1,73 @@ +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { + defineMidsceneConfig, + defineRuntime, +} from '@midscene/testing-framework'; + +// The demo ships a tiny static page so the example runs offline (only the model +// endpoint needs network). Point `DEMO_URL` at your own app to try real flows. +const demoUrl = + process.env.DEMO_URL ?? + pathToFileURL(join(__dirname, 'site', 'index.html')).href; + +export default defineMidsceneConfig({ + // —— run target: single `uiAgent` field (config-style object) —— + uiAgent: { + type: 'web', + options: { + url: demoUrl, + }, + }, + + // —— case discovery —— + testDir: './e2e', + include: ['**/*.yaml'], + exclude: ['**/*.draft.yaml'], + + // —— execution policy (aligned with Rstest concepts) —— + testRunner: { + maxConcurrency: 1, + bail: 0, + testTimeout: 120_000, + }, + + // —— output —— + output: { + summary: './midscene_run/output/summary.json', + reportDir: './midscene_run/report', + }, + + // —— shared UI Agent behavior —— + uiAgentOptions: { + aiActContext: 'The user is browsing a demo shop as an anonymous visitor.', + generateReport: true, + }, + + // —— custom YAML nodes (defineRuntime, RFC §3) —— + runtime: { + // A fixture-prep node: writes engineering state (not visible to the agent) + // and a natural-language conclusion (visible to later verify/agent). + prepareCartFixture: defineRuntime(async (rawInput, ctx) => { + const input = (rawInput ?? {}) as { scenario?: string }; + const scenario = input.scenario ?? 'default'; + ctx.state.cartFixture = { id: `cart-${Date.now()}`, scenario }; + + return { + conclusion: `Prepared a "${scenario}" cart fixture for this run.`, + output: { scenario }, + }; + }), + + // A side-effect node that reads the accumulated case result. + notify: defineRuntime(async (_input, ctx) => { + const failed = ctx.result.steps.filter((s) => s.status === 'failed'); + return { + conclusion: + failed.length === 0 + ? 'All gating checks passed; no alert needed.' + : `Would alert: ${failed.length} step(s) failed.`, + }; + }), + }, +}); diff --git a/packages/testing-framework/example/package.json b/packages/testing-framework/example/package.json new file mode 100644 index 0000000000..0e3eca7b11 --- /dev/null +++ b/packages/testing-framework/example/package.json @@ -0,0 +1,19 @@ +{ + "name": "midscene-testing-framework-example", + "version": "0.0.0", + "private": true, + "description": "Copy-out demo for @midscene/testing-framework (v2 Phase 0)", + "type": "module", + "scripts": { + "test": "midscene-tf run", + "test:one": "midscene-tf run e2e/product-detail.yaml" + }, + "dependencies": { + "@midscene/testing-framework": "latest", + "@midscene/web": "latest", + "puppeteer": ">=20.0.0" + }, + "engines": { + "node": ">=18.19.0" + } +} diff --git a/packages/testing-framework/example/site/index.html b/packages/testing-framework/example/site/index.html new file mode 100644 index 0000000000..36daedba8b --- /dev/null +++ b/packages/testing-framework/example/site/index.html @@ -0,0 +1,76 @@ + + + + + + Midscene Demo Shop + + + +
Midscene Demo Shop
+
+
+ +
+
+

Running Shoes

+
$89.00
+
+
+

Trail Backpack

+
$129.00
+
+
+
+ +
+

+
+

Lightweight, breathable, built for everyday runs.

+ + +
+
+ + + + diff --git a/packages/testing-framework/example/skills/catalog/SKILL.md b/packages/testing-framework/example/skills/catalog/SKILL.md new file mode 100644 index 0000000000..0e91c7fc40 --- /dev/null +++ b/packages/testing-framework/example/skills/catalog/SKILL.md @@ -0,0 +1,29 @@ +--- +name: catalog +description: >- + Look up demo-shop catalog products and their expected prices. Use this skill + whenever a test references $catalog to confirm a product exists and to report + its canonical price. +--- + +# Catalog skill + +This demo skill represents an external source of truth a real test might query +(a database, an internal API, a price service). For the demo it is static. + +Known catalog products: + +| Product | SKU | Expected price | +| -------------- | ----- | -------------- | +| Running Shoes | sku-1 | $89.00 | +| Trail Backpack | sku-2 | $129.00 | + +When asked to confirm a product: + +1. Find the product by name in the table above. +2. If it exists, report it as a known catalog product and state the expected + price. +3. If it does not exist, say so clearly — the verification should fail. + +In a real project this skill would run a command or call an API to fetch the +truth. Replace the static table with that lookup. diff --git a/packages/testing-framework/package.json b/packages/testing-framework/package.json new file mode 100644 index 0000000000..e3a10f8e68 --- /dev/null +++ b/packages/testing-framework/package.json @@ -0,0 +1,69 @@ +{ + "name": "@midscene/testing-framework", + "version": "1.8.9", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/web-infra-dev/midscene.git", + "directory": "packages/testing-framework" + }, + "description": "AI-native v2 UI testing framework for natural-language cases (Phase 0)", + "keywords": [ + "AI testing", + "UI testing", + "natural language testing", + "midscene", + "agentic testing" + ], + "main": "./dist/lib/index.js", + "module": "./dist/es/index.mjs", + "types": "./dist/types/index.d.ts", + "bin": { + "midscene-tf": "./bin/midscene-tf" + }, + "files": ["bin", "dist", "README.md"], + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/es/index.mjs", + "require": "./dist/lib/index.js" + }, + "./package.json": "./package.json" + }, + "scripts": { + "dev": "npm run build:watch", + "build": "rslib build", + "build:watch": "rslib build --watch --no-clean", + "test": "vitest --run", + "test:u": "vitest --run -u" + }, + "dependencies": { + "@earendil-works/pi-ai": "^0.78.0", + "@earendil-works/pi-coding-agent": "^0.78.0", + "@midscene/core": "workspace:*", + "@midscene/shared": "workspace:*", + "jiti": "2.7.0", + "js-yaml": "4.1.0" + }, + "peerDependencies": { + "@midscene/web": "workspace:*" + }, + "peerDependenciesMeta": { + "@midscene/web": { + "optional": true + } + }, + "devDependencies": { + "@midscene/web": "workspace:*", + "@rslib/core": "^0.18.3", + "@types/js-yaml": "4.0.9", + "@types/node": "^18.0.0", + "dotenv": "^16.4.5", + "typescript": "^5.8.3", + "vitest": "3.0.5" + }, + "engines": { + "node": ">=18.19.0" + }, + "license": "MIT" +} diff --git a/packages/testing-framework/rslib.config.ts b/packages/testing-framework/rslib.config.ts new file mode 100644 index 0000000000..353840c6fd --- /dev/null +++ b/packages/testing-framework/rslib.config.ts @@ -0,0 +1,50 @@ +import { defineConfig } from '@rslib/core'; +import { createTypeCheckPlugin } from '../../scripts/rsbuild-utils.ts'; +import { version } from './package.json'; + +export default defineConfig({ + lib: [ + { + output: { + distPath: { + root: 'dist/lib', + }, + }, + format: 'cjs', + syntax: 'es2020', + }, + { + output: { + distPath: { + root: 'dist/es', + }, + }, + dts: { + bundle: true, + distPath: 'dist/types', + }, + format: 'esm', + syntax: 'es2020', + }, + ], + source: { + tsconfigPath: 'tsconfig.build.json', + entry: { + index: './src/index.ts', + cli: './src/cli.ts', + }, + define: { + __VERSION__: JSON.stringify(version), + }, + }, + output: { + // Pi and the platform UI agents are heavy runtime deps; keep them external. + externals: [ + '@earendil-works/pi-coding-agent', + '@earendil-works/pi-ai', + '@midscene/web', + '@midscene/web/puppeteer-agent-launcher', + ], + }, + plugins: [createTypeCheckPlugin()], +}); diff --git a/packages/testing-framework/src/cli.ts b/packages/testing-framework/src/cli.ts new file mode 100644 index 0000000000..918c805166 --- /dev/null +++ b/packages/testing-framework/src/cli.ts @@ -0,0 +1,89 @@ +/** + * `midscene-tf` — minimal CLI for the v2 testing framework (Phase 0). + * + * Usage: + * midscene-tf run [--config ] [--root ] [file...] + */ +import { loadConfig } from './runner/load-config'; +import { runAll } from './runner/run'; +import type { RunSummary } from './types'; + +interface ParsedArgs { + command: string; + config?: string; + root?: string; + files: string[]; +} + +function parseArgs(argv: string[]): ParsedArgs { + const args: ParsedArgs = { command: argv[0] ?? 'run', files: [] }; + for (let i = 1; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--config' || arg === '-c') { + args.config = argv[++i]; + } else if (arg === '--root' || arg === '-r') { + args.root = argv[++i]; + } else if (arg.startsWith('-')) { + throw new Error(`[midscene] Unknown flag: ${arg}`); + } else { + args.files.push(arg); + } + } + return args; +} + +function printSummary(summary: RunSummary): void { + console.log(''); + console.log(`Midscene v2 — ${summary.total} case(s)`); + for (const c of summary.cases) { + const mark = c.status === 'passed' ? '✓' : '✗'; + console.log(` ${mark} ${c.name} (${c.durationMs}ms)`); + for (const step of c.steps) { + const stepMark = + step.status === 'passed' + ? '✓' + : step.status === 'failed' + ? '✗' + : step.status === 'warning' + ? '!' + : '·'; + const detail = step.verdict + ? ` — ${step.verdict.reason}` + : step.error + ? ` — ${step.error}` + : ''; + console.log(` ${stepMark} [${step.node}]${detail}`); + } + for (const w of c.warnings) { + console.log(` ! ${w}`); + } + } + console.log(''); + console.log( + `Passed: ${summary.passed} Failed: ${summary.failed} (${summary.durationMs}ms)`, + ); +} + +export async function main(argv = process.argv.slice(2)): Promise { + const args = parseArgs(argv); + + if (args.command !== 'run') { + console.error(`[midscene] Unknown command "${args.command}". Try: run`); + return 2; + } + + const { config } = await loadConfig(args.config ?? args.root); + const summary = await runAll(config, { + projectRoot: args.root, + files: args.files, + }); + printSummary(summary); + return summary.failed > 0 ? 1 : 0; +} + +main() + .then((code) => process.exit(code)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/packages/testing-framework/src/config/index.ts b/packages/testing-framework/src/config/index.ts new file mode 100644 index 0000000000..2c8a6656ca --- /dev/null +++ b/packages/testing-framework/src/config/index.ts @@ -0,0 +1,27 @@ +import { getDebug } from '@midscene/shared/logger'; +import type { MidsceneConfig } from './types'; + +const warn = getDebug('testing-framework:config', { console: true }); + +/** + * Identity helper for `midscene.config.ts`, giving full type inference and a + * stable import surface (RFC §2). + */ +export function defineMidsceneConfig(config: MidsceneConfig): MidsceneConfig { + if (!config || typeof config !== 'object') { + throw new Error('[midscene] defineMidsceneConfig expects a config object.'); + } + if (!config.uiAgent) { + // A missing uiAgent is recoverable for some flows (e.g. cases that only use + // custom runtime nodes), so warn instead of failing the whole config load. + warn( + 'midscene.config.ts does not define a `uiAgent` (object or factory function); ui/verify/soft/agent nodes will have no UI Agent to run against.', + ); + } + if (!config.testDir) { + throw new Error('[midscene] midscene.config.ts must define a `testDir`.'); + } + return config; +} + +export * from './types'; diff --git a/packages/testing-framework/src/config/types.ts b/packages/testing-framework/src/config/types.ts new file mode 100644 index 0000000000..d86db8ba37 --- /dev/null +++ b/packages/testing-framework/src/config/types.ts @@ -0,0 +1,98 @@ +/** + * `midscene.config.ts` schema (RFC §2). Environment / target lives here, never + * in the case YAML. + */ +import type { + AndroidConnectionOpt, + ComputerConnectionOpt, + IOSConnectionOpt, + WebConnectionOpt, +} from '@midscene/core'; +import type { Agent } from '@midscene/core/agent'; +import type { AgentOpt } from '@midscene/core/agent'; +import type { GeneralAgentAdapter } from '../general-agent/types'; +import type { RuntimeNode } from '../runtime'; + +/** Shared UI Agent behavior parameters (aiActContext, generateReport, ...). */ +export type UIAgentOptions = AgentOpt; + +/** + * Configuration-style UI Agent: the framework builds the agent from `type` + + * `options`. `options` is the platform connection target, typed against the + * canonical per-platform connection types from `@midscene/core` + * (`WebConnectionOpt` / `AndroidConnectionOpt` / ...). Those are the pure + * "how to reach the target" shapes the agent launchers consume — agent + * behavior is expressed separately via `uiAgentOptions`. Keeping `options` + * bound to core means it can never drift from the launcher inputs. + */ +export type UIAgentConfig = + | { type: 'web'; options: WebConnectionOpt } + | { type: 'android'; options?: AndroidConnectionOpt } + | { type: 'ios'; options?: IOSConnectionOpt } + | { type: 'computer'; options?: ComputerConnectionOpt }; + +/** Platforms the framework can build a UI Agent for out of the box. */ +export type UIAgentType = UIAgentConfig['type']; + +/** Context passed to a programmatic UI Agent factory. */ +export interface UIAgentFactoryCtx { + uiAgentOptions?: UIAgentOptions; + env: NodeJS.ProcessEnv; +} + +/** Programmatic UI Agent: the project fully controls construction. */ +export type UIAgentFactory = (ctx: UIAgentFactoryCtx) => Promise<{ + agent: Agent; + /** Optional cleanup invoked after the case finishes (close browser, etc). */ + cleanup?: () => Promise; +}>; + +/** + * The single `uiAgent` field (RFC §2.1): an object means config-style, a + * function means programmatic. One key, union type — no two ways to define a + * run target. + */ +export type UIAgent = UIAgentConfig | UIAgentFactory; + +export interface TestRunnerOptions { + maxConcurrency?: number; + bail?: number; + testTimeout?: number; + retry?: number; +} + +export interface OutputOptions { + /** Path to write the aggregate run summary JSON. */ + summary?: string; + /** Directory for Midscene HTML reports. */ + reportDir?: string; +} + +export interface MidsceneConfig { + /** How the UI Agent is created (RFC §2.1). */ + uiAgent: UIAgent; + + // —— case discovery —— + testDir: string; + /** Defaults to ['**\/*.yaml']. */ + include?: string[]; + exclude?: string[]; + + // —— execution policy (aligned with Rstest concepts) —— + testRunner?: TestRunnerOptions; + + // —— output —— + output?: OutputOptions; + + // —— shared UI Agent params —— + uiAgentOptions?: UIAgentOptions; + + // —— extension points —— + /** Custom YAML nodes (RFC §3). */ + runtime?: Record; + /** Replacement for the default Pi-backed general agent layer (RFC §6). */ + generalAgent?: GeneralAgentAdapter; +} + +/** Defaults applied when reading a config. */ +export const DEFAULT_INCLUDE = ['**/*.yaml']; diff --git a/packages/testing-framework/src/context/assembler.ts b/packages/testing-framework/src/context/assembler.ts new file mode 100644 index 0000000000..a1261cf055 --- /dev/null +++ b/packages/testing-framework/src/context/assembler.ts @@ -0,0 +1,101 @@ +/** + * Context assembly (RFC §7). + * + * When executing a `verify` / `agent` node, the agent sees EXACTLY: + * for each past step (in order): + * - node type + instruction (text, or object input for custom nodes) + * - that step's output (natural language; runtime nodes use conclusion) + * - if verify/soft: its pass/fail + reason + * + the current UI screenshot (handed separately as an image) + * + the skills pre-loaded into the agent + * + * Explicitly excluded ("nothing else"): execution traces, historical + * screenshots, runtime `state`, intermediate skill-call results. + * + * Phase 0 does NOT truncate (predictability > compactness). + */ +import type { StepResult } from '../types'; + +export interface AssembleContextInput { + /** The case name, for a small header. */ + caseName: string; + /** All steps executed before the current node, in order. */ + pastSteps: ReadonlyArray; + /** The current node's instruction. */ + instruction: string; + /** The current node's kind, for framing. */ + kind: 'verify' | 'soft' | 'agent'; +} + +export function assembleContext(input: AssembleContextInput): string { + const { caseName, pastSteps, instruction, kind } = input; + const lines: string[] = []; + + lines.push(`# Test case: ${caseName}`); + lines.push(''); + lines.push( + 'You are running inside a UI test. Below is the full history of previous ' + + 'steps and their outputs. You also receive the current UI screenshot as ' + + 'an image. This is everything you can see — there is no other hidden state.', + ); + lines.push(''); + + if (pastSteps.length === 0) { + lines.push('## Previous steps'); + lines.push('(none — this is the first step)'); + } else { + lines.push('## Previous steps'); + for (const step of pastSteps) { + lines.push(''); + lines.push(`### Step ${step.index + 1}: ${step.node}`); + lines.push(`- Intent: ${formatInput(step.input)}`); + if (step.output?.text) { + lines.push(`- Output: ${step.output.text}`); + } + if (step.output?.structured) { + lines.push(`- Output fields: ${safeJson(step.output.structured)}`); + } + if (step.verdict) { + lines.push( + `- Verdict: ${step.verdict.pass ? 'PASS' : 'FAIL'} — ${step.verdict.reason}`, + ); + } + if (step.error) { + lines.push(`- Error: ${step.error}`); + } + } + } + + lines.push(''); + lines.push('## Current task'); + if (kind === 'agent') { + lines.push( + 'Freely explore and analyze based on the history above and the current ' + + 'screenshot. Your output is advisory and does NOT decide pass/fail.', + ); + } else { + lines.push( + 'Make a judgment. You MUST finish by calling the `report_verdict` tool ' + + 'with `pass`, `reason`, and optional `evidence`. If you cannot ' + + 'confidently determine the result, report `pass: false`.', + ); + } + lines.push(''); + lines.push(instruction.trim()); + + return lines.join('\n'); +} + +function formatInput(input: unknown): string { + if (typeof input === 'string') return input.trim(); + if (input === undefined) return '(no input)'; + return safeJson(input); +} + +function safeJson(value: unknown): string { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} diff --git a/packages/testing-framework/src/engine/output-store.ts b/packages/testing-framework/src/engine/output-store.ts new file mode 100644 index 0000000000..007124c23b --- /dev/null +++ b/packages/testing-framework/src/engine/output-store.ts @@ -0,0 +1,27 @@ +import type { OutputStore, StepOutput } from '../types'; + +interface StoredOutput { + node: string; + index: number; + output: StepOutput; +} + +/** + * Mutable backing store for step outputs. Exposes a read-only {@link OutputStore} + * view to runtime nodes (RFC §3). + */ +export class OutputStoreImpl implements OutputStore { + private readonly outputs: StoredOutput[] = []; + + add(node: string, index: number, output: StepOutput): void { + this.outputs.push({ node, index, output }); + } + + all(): ReadonlyArray { + return this.outputs; + } + + latest(): StepOutput | undefined { + return this.outputs[this.outputs.length - 1]?.output; + } +} diff --git a/packages/testing-framework/src/engine/run-case.ts b/packages/testing-framework/src/engine/run-case.ts new file mode 100644 index 0000000000..a4fc67f2ff --- /dev/null +++ b/packages/testing-framework/src/engine/run-case.ts @@ -0,0 +1,119 @@ +import type { Agent } from '@midscene/core/agent'; +import type { GeneralAgentAdapter } from '../general-agent/types'; +import type { RuntimeNode } from '../runtime'; +import type { CaseResult, StepResult } from '../types'; +import type { ParsedCase } from '../yaml/types'; +import { OutputStoreImpl } from './output-store'; +import { type RunNodeDeps, runNode } from './run-node'; + +export interface RunCaseOptions { + parsed: ParsedCase; + file: string; + uiAgent: Agent; + generalAgent: GeneralAgentAdapter; + runtimeNodes: Record; + projectRoot: string; + env: NodeJS.ProcessEnv; +} + +/** + * Execute a single case (one parsed flow). Returns a structured result; never + * throws for node-level failures (those are recorded as failed steps). + */ +export async function runCase(options: RunCaseOptions): Promise { + const { + parsed, + file, + uiAgent, + generalAgent, + runtimeNodes, + projectRoot, + env, + } = options; + const caseName = parsed.name ?? file; + + const outputs = new OutputStoreImpl(); + const state: Record = {}; + const steps: StepResult[] = []; + const warnings: string[] = []; + const startedAt = Date.now(); + let status: CaseResult['status'] = 'passed'; + + for (let index = 0; index < parsed.flow.length; index++) { + const step = parsed.flow[index]; + const stepStart = Date.now(); + + const deps: RunNodeDeps = { + uiAgent, + generalAgent, + runtimeNodes, + outputs, + state, + projectRoot, + caseName, + caseFile: file, + pastSteps: steps, + env, + }; + + let stepResult: StepResult; + try { + const outcome = await runNode(step.node, step.input, deps); + stepResult = { + index, + node: step.node, + input: step.input, + status: outcome.status, + output: outcome.output, + verdict: outcome.verdict, + error: outcome.error, + durationMs: Date.now() - stepStart, + }; + } catch (err) { + // Hard failure: ui action threw, runtime node threw, unknown node, etc. + stepResult = { + index, + node: step.node, + input: step.input, + status: 'failed', + error: (err as Error).message, + durationMs: Date.now() - stepStart, + }; + } + + steps.push(stepResult); + if (stepResult.output) { + outputs.add(step.node, index, stepResult.output); + } + if (stepResult.status === 'warning' && stepResult.error) { + warnings.push(stepResult.error); + } + if (stepResult.status === 'warning' && stepResult.verdict) { + warnings.push( + `soft check failed at step ${index + 1} (${step.node}): ${stepResult.verdict.reason}`, + ); + } + + if (stepResult.status === 'failed') { + // A gating failure stops the flow; later steps depend on prior ones. + status = 'failed'; + break; + } + } + + return { + name: caseName, + file, + status, + steps, + warnings, + durationMs: Date.now() - startedAt, + reportFile: getReportFile(uiAgent), + }; +} + +function getReportFile(agent: Agent): string | undefined { + const candidate = (agent as unknown as { reportFile?: string | null }) + .reportFile; + return candidate ?? undefined; +} diff --git a/packages/testing-framework/src/engine/run-node.ts b/packages/testing-framework/src/engine/run-node.ts new file mode 100644 index 0000000000..659e1004d2 --- /dev/null +++ b/packages/testing-framework/src/engine/run-node.ts @@ -0,0 +1,203 @@ +import type { Agent } from '@midscene/core/agent'; +import { assembleContext } from '../context/assembler'; +import { extractSkillReferences } from '../general-agent/skills'; +import type { GeneralAgentAdapter } from '../general-agent/types'; +import type { RuntimeNode, RuntimeNodeContext } from '../runtime'; +import type { StepOutput, StepResult, Verdict } from '../types'; +import { isBuiltinNode } from '../yaml/types'; +import type { OutputStoreImpl } from './output-store'; + +export interface RunNodeDeps { + uiAgent: Agent; + generalAgent: GeneralAgentAdapter; + runtimeNodes: Record; + outputs: OutputStoreImpl; + /** Shared engineering-facing state across runtime nodes. */ + state: Record; + projectRoot: string; + caseName: string; + caseFile: string; + /** Steps already executed (read-only context for the current node). */ + pastSteps: ReadonlyArray; + env: NodeJS.ProcessEnv; +} + +export interface RunNodeOutcome { + status: StepResult['status']; + output?: StepOutput; + verdict?: Verdict; + error?: string; +} + +/** + * Execute a single flow step and return its outcome. Throwing is reserved for + * unexpected engine errors; node-level failures are reported via `status`. + */ +export async function runNode( + node: string, + input: unknown, + deps: RunNodeDeps, +): Promise { + if (isBuiltinNode(node)) { + switch (node) { + case 'ui': + return runUiNode(input as string, deps); + case 'verify': + return runJudgmentNode('verify', input as string, deps); + case 'soft': + return runJudgmentNode('soft', input as string, deps); + case 'agent': + return runAgentNode(input as string, deps); + } + } + return runCustomNode(node, input, deps); +} + +async function runUiNode( + instruction: string, + deps: RunNodeDeps, +): Promise { + // The UI Agent performs the natural-language action. Errors propagate up so + // the case fails (RFC §8). + const acted = await deps.uiAgent.aiAct(instruction); + + let text = typeof acted === 'string' && acted.trim() ? acted.trim() : ''; + if (!text) { + // Produce a context-facing conclusion grounded in the current screen, + // honoring any "record these values" request in the instruction. + text = await deps.uiAgent.aiAsk( + `In natural language, summarize the result of performing the following instruction on the current screen. If the instruction asked to record or name any values, include them explicitly.\n\nInstruction:\n${instruction}`, + ); + } + + return { status: 'info', output: { text } }; +} + +async function runJudgmentNode( + kind: 'verify' | 'soft', + instruction: string, + deps: RunNodeDeps, +): Promise { + const { data, mediaType } = await captureScreenshot(deps.uiAgent); + const context = assembleContext({ + caseName: deps.caseName, + pastSteps: deps.pastSteps, + instruction, + kind, + }); + + const result = await deps.generalAgent.run({ + kind, + instruction, + context, + screenshotBase64: data, + screenshotMediaType: mediaType, + referencedSkills: extractSkillReferences(instruction), + projectRoot: deps.projectRoot, + }); + + // Fail-closed: a missing/unparseable verdict is treated as failure (RFC §6). + const verdict: Verdict = result.verdict ?? { + pass: false, + reason: + 'The agent did not report a verdict via report_verdict; treated as failure (fail-closed).', + }; + + const output: StepOutput = { + text: result.text || verdict.reason, + }; + + if (verdict.pass) { + return { status: 'passed', output, verdict }; + } + // verify gates the case; soft only warns. + return { + status: kind === 'verify' ? 'failed' : 'warning', + output, + verdict, + }; +} + +async function runAgentNode( + instruction: string, + deps: RunNodeDeps, +): Promise { + const { data, mediaType } = await captureScreenshot(deps.uiAgent); + const context = assembleContext({ + caseName: deps.caseName, + pastSteps: deps.pastSteps, + instruction, + kind: 'agent', + }); + + // `agent` is advisory: its output never changes pass/fail. Even internal + // errors are downgraded to a warning (RFC §8). + try { + const result = await deps.generalAgent.run({ + kind: 'agent', + instruction, + context, + screenshotBase64: data, + screenshotMediaType: mediaType, + referencedSkills: extractSkillReferences(instruction), + projectRoot: deps.projectRoot, + }); + return { status: 'info', output: { text: result.text } }; + } catch (err) { + return { + status: 'warning', + error: `agent node error (advisory, non-gating): ${(err as Error).message}`, + }; + } +} + +async function runCustomNode( + node: string, + input: unknown, + deps: RunNodeDeps, +): Promise { + const runtimeNode = deps.runtimeNodes[node]; + if (!runtimeNode) { + throw new Error( + `[midscene] Unknown node "${node}". It is not a built-in node and is not registered under \`runtime\` in midscene.config.ts.`, + ); + } + + const ctx: RuntimeNodeContext = { + uiAgent: deps.uiAgent, + outputs: deps.outputs, + state: deps.state, + result: { + name: deps.caseName, + file: deps.caseFile, + steps: deps.pastSteps, + }, + env: deps.env, + }; + + // A runtime node that throws fails the case (RFC §8). + const result = await runtimeNode(input, ctx); + return { + status: 'info', + output: { text: result.conclusion, structured: result.output }, + }; +} + +async function captureScreenshot( + agent: Agent, +): Promise<{ data?: string; mediaType: string }> { + try { + const raw = await agent.interface.screenshotBase64(); + return splitDataUrl(raw); + } catch { + return { data: undefined, mediaType: 'image/png' }; + } +} + +function splitDataUrl(value: string): { data: string; mediaType: string } { + const match = /^data:(image\/[\w.+-]+);base64,(.*)$/s.exec(value); + if (match) { + return { mediaType: match[1], data: match[2] }; + } + return { mediaType: 'image/png', data: value }; +} diff --git a/packages/testing-framework/src/general-agent/pi-general-agent.ts b/packages/testing-framework/src/general-agent/pi-general-agent.ts new file mode 100644 index 0000000000..ca13c77025 --- /dev/null +++ b/packages/testing-framework/src/general-agent/pi-general-agent.ts @@ -0,0 +1,246 @@ +/** + * Default general agent, backed by Pi (`@earendil-works/pi-coding-agent`). + * + * This is the Phase 0 implementation of the swappable general agent layer used + * by `verify` / `soft` / `agent` nodes. + * + * Decision C′ (RFC §4.1 / §10) — RESOLVED here. Pi exposes + * `ModelRegistry.registerProvider({ baseUrl, apiKey, models })`, which lets us + * point Pi at the SAME OpenAI-compatible endpoint Midscene's UI Agent uses + * (`MIDSCENE_MODEL_BASE_URL` / `MIDSCENE_MODEL_API_KEY` / `MIDSCENE_MODEL_NAME`). + * So `verify`/`agent` and `ui` share one model endpoint without any Pi changes. + */ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { Type } from '@earendil-works/pi-ai'; +import type { Model } from '@earendil-works/pi-ai'; +import { + AuthStorage, + DefaultResourceLoader, + ModelRegistry, + SessionManager, + createAgentSession, + defineTool, + getAgentDir, +} from '@earendil-works/pi-coding-agent'; +import { getDebug } from '@midscene/shared/logger'; +import type { Verdict } from '../types'; +import type { + GeneralAgentAdapter, + GeneralAgentInput, + GeneralAgentResult, +} from './types'; + +const debug = getDebug('testing-framework:pi'); + +const PROVIDER_NAME = 'midscene'; + +export interface PiGeneralAgentOptions { + /** Endpoint base URL. Defaults to MIDSCENE_MODEL_BASE_URL. */ + baseUrl?: string; + /** API key. Defaults to MIDSCENE_MODEL_API_KEY. */ + apiKey?: string; + /** Model id/name. Defaults to MIDSCENE_MODEL_NAME. */ + modelName?: string; + /** Context window hint passed to Pi. */ + contextWindow?: number; + /** Max output tokens hint passed to Pi. */ + maxTokens?: number; +} + +interface PreparedModel { + authStorage: AuthStorage; + modelRegistry: ModelRegistry; + model: Model<'openai-completions'>; +} + +/** + * Pi-backed implementation of {@link GeneralAgentAdapter}. + */ +export class PiGeneralAgent implements GeneralAgentAdapter { + private prepared?: PreparedModel; + private readonly loaderCache = new Map(); + + constructor(private readonly options: PiGeneralAgentOptions = {}) {} + + async run(input: GeneralAgentInput): Promise { + const prepared = this.prepareModel(); + const loader = await this.getResourceLoader(input.projectRoot); + + let capturedVerdict: Verdict | undefined; + const needsVerdict = input.kind === 'verify' || input.kind === 'soft'; + + const customTools = needsVerdict + ? [ + defineTool({ + name: 'report_verdict', + label: 'Report verdict', + description: + 'Call this exactly once when your judgment is complete to submit ' + + 'the pass/fail verdict for this verification.', + parameters: Type.Object({ + pass: Type.Boolean({ + description: 'Whether the verification passed.', + }), + reason: Type.String({ + description: 'Human-readable rationale for the verdict.', + }), + evidence: Type.Optional( + Type.Unknown({ + description: 'Optional supporting evidence.', + }), + ), + }), + execute: async (_id, params) => { + capturedVerdict = { + pass: params.pass, + reason: params.reason, + evidence: params.evidence, + }; + return { + content: [{ type: 'text', text: 'Verdict recorded.' }], + details: capturedVerdict, + terminate: true, + }; + }, + }), + ] + : []; + + const { session } = await createAgentSession({ + cwd: input.projectRoot, + model: prepared.model, + modelRegistry: prepared.modelRegistry, + authStorage: prepared.authStorage, + sessionManager: SessionManager.inMemory(), + resourceLoader: loader, + customTools, + // verify/agent only read the UI; they must not mutate the project files. + // `read` and `bash` stay enabled so skills can fetch external context. + excludeTools: ['edit', 'write'], + }); + + try { + const promptText = this.buildPrompt(input); + const images = input.screenshotBase64 + ? [ + { + type: 'image' as const, + data: input.screenshotBase64, + mimeType: input.screenshotMediaType ?? 'image/png', + }, + ] + : undefined; + + await session.prompt(promptText, images ? { images } : undefined); + + const text = session.getLastAssistantText() ?? ''; + debug('pi run finished', { + kind: input.kind, + hasVerdict: Boolean(capturedVerdict), + }); + + return { text, verdict: capturedVerdict }; + } finally { + session.dispose(); + } + } + + private buildPrompt(input: GeneralAgentInput): string { + const parts = [input.context]; + if (input.referencedSkills.length > 0) { + parts.push(''); + parts.push( + `This task references the following skills: ${input.referencedSkills + .map((s) => `$${s}`) + .join(', ')}. Load and use them as needed to complete the task.`, + ); + } + return parts.join('\n'); + } + + private prepareModel(): PreparedModel { + if (this.prepared) return this.prepared; + + const baseUrl = this.options.baseUrl ?? process.env.MIDSCENE_MODEL_BASE_URL; + const apiKey = this.options.apiKey ?? process.env.MIDSCENE_MODEL_API_KEY; + const modelName = this.options.modelName ?? process.env.MIDSCENE_MODEL_NAME; + + if (!baseUrl) { + throw new Error( + '[midscene] Pi general agent requires MIDSCENE_MODEL_BASE_URL ' + + '(or PiGeneralAgentOptions.baseUrl) so verify/agent share the UI Agent endpoint.', + ); + } + if (!apiKey) { + throw new Error( + '[midscene] Pi general agent requires MIDSCENE_MODEL_API_KEY (or PiGeneralAgentOptions.apiKey).', + ); + } + if (!modelName) { + throw new Error( + '[midscene] Pi general agent requires MIDSCENE_MODEL_NAME (or PiGeneralAgentOptions.modelName).', + ); + } + + const authStorage = AuthStorage.inMemory(); + const modelRegistry = ModelRegistry.inMemory(authStorage); + + modelRegistry.registerProvider(PROVIDER_NAME, { + baseUrl, + apiKey, + models: [ + { + id: modelName, + name: modelName, + api: 'openai-completions', + reasoning: false, + input: ['text', 'image'], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: this.options.contextWindow ?? 128_000, + maxTokens: this.options.maxTokens ?? 8_192, + }, + ], + }); + + const model = modelRegistry.find(PROVIDER_NAME, modelName) as + | Model<'openai-completions'> + | undefined; + if (!model) { + throw new Error( + `[midscene] Failed to register Pi model "${modelName}" at ${baseUrl}.`, + ); + } + + this.prepared = { authStorage, modelRegistry, model }; + return this.prepared; + } + + private async getResourceLoader( + projectRoot: string, + ): Promise { + const cached = this.loaderCache.get(projectRoot); + if (cached) return cached; + + // Convention: project skills live under `/skills`. Pi also + // discovers its own default skill locations relative to cwd. The framework + // only POINTS Pi at the skills — discovery/activation stays Pi's job. + const additionalSkillPaths: string[] = []; + const conventionalSkillsDir = join(projectRoot, 'skills'); + if (existsSync(conventionalSkillsDir)) { + additionalSkillPaths.push(conventionalSkillsDir); + } + + const loader = new DefaultResourceLoader({ + cwd: projectRoot, + agentDir: getAgentDir(), + additionalSkillPaths, + noExtensions: true, + noThemes: true, + noPromptTemplates: true, + }); + await loader.reload(); + this.loaderCache.set(projectRoot, loader); + return loader; + } +} diff --git a/packages/testing-framework/src/general-agent/skills.ts b/packages/testing-framework/src/general-agent/skills.ts new file mode 100644 index 0000000000..5d5863cef6 --- /dev/null +++ b/packages/testing-framework/src/general-agent/skills.ts @@ -0,0 +1,23 @@ +/** + * `$name` skill references (RFC §4). + * + * The framework does NOT register or activate skills. It only statically + * extracts the `$name` tokens from a node's instruction so it can: + * 1. surface them to the agent as a strong load hint, and + * 2. optionally validate that the referenced skills exist. + * + * Actual loading/activation is Pi's job (progressive disclosure). + */ + +// $name: starts with `$`, then a letter/underscore, then word chars or hyphens. +const SKILL_TOKEN = /\$([A-Za-z_][A-Za-z0-9_-]*)/g; + +/** Extract the unique set of `$name` skill references from an instruction. */ +export function extractSkillReferences(instruction: string): string[] { + if (!instruction) return []; + const found = new Set(); + for (const match of instruction.matchAll(SKILL_TOKEN)) { + found.add(match[1]); + } + return [...found]; +} diff --git a/packages/testing-framework/src/general-agent/types.ts b/packages/testing-framework/src/general-agent/types.ts new file mode 100644 index 0000000000..a327cad79c --- /dev/null +++ b/packages/testing-framework/src/general-agent/types.ts @@ -0,0 +1,55 @@ +/** + * GeneralAgentAdapter — the swappable general-purpose agent layer (RFC §6, + * design doc "swappable agent framework"). It is the counterpart to the UI + * Agent (`ui-agent/`): the UI Agent *acts on* the page (`ui` nodes), while the + * general agent *reasons about* it and gates (`verify` / `soft` / `agent` + * nodes). The default implementation wraps Pi; teams can replace it via the + * `generalAgent` field in `midscene.config.ts`. + * + * Naming note: "runtime" is reserved for custom YAML nodes (`defineRuntime`, + * RFC §3) — this layer deliberately avoids that word to keep the two extension + * points distinct. + * + * Phase 0 keeps this interface deliberately minimal: a single `run` entry that + * the engine calls for `verify` / `soft` / `agent` nodes. + */ +import type { Verdict } from '../types'; + +export interface GeneralAgentInput { + /** + * Node kind. `verify` and `soft` both must produce a verdict; `agent` is + * advisory and never produces one. + */ + kind: 'verify' | 'soft' | 'agent'; + /** The natural-language instruction from the YAML node. */ + instruction: string; + /** + * The assembled context (RFC §7): every past step's intent + output + + * verify verdicts. Plain text. + */ + context: string; + /** Current UI screenshot as bare base64 PNG (no data: prefix). */ + screenshotBase64?: string; + /** PNG media type override; defaults to image/png. */ + screenshotMediaType?: string; + /** `$name` tokens referenced by the instruction (RFC §4). */ + referencedSkills: string[]; + /** Project root, used for skill discovery and the agent's cwd. */ + projectRoot: string; +} + +export interface GeneralAgentResult { + /** The agent's final natural-language message. */ + text: string; + /** + * For verify/soft: the structured verdict, or undefined when the agent + * never reported one (the engine treats undefined as fail-closed, RFC §6). + */ + verdict?: Verdict; +} + +export interface GeneralAgentAdapter { + run(input: GeneralAgentInput): Promise; + /** Release any underlying resources. */ + dispose?(): Promise; +} diff --git a/packages/testing-framework/src/index.ts b/packages/testing-framework/src/index.ts new file mode 100644 index 0000000000..0acad2d162 --- /dev/null +++ b/packages/testing-framework/src/index.ts @@ -0,0 +1,77 @@ +/** + * @midscene/testing-framework — AI-native v2 UI testing framework (Phase 0). + * + * Public surface implementing RFC 0001: + * - `defineMidsceneConfig` / `defineRuntime` authoring helpers + * - the node model, verdict contract, output contract, context-assembly + * contract (as types) + * - a lightweight runner (`runAll`) and CLI (`midscene-tf`) + * - the default Pi-backed general agent with a custom model base URL + * (decision C′, RFC §4.1) + */ + +// —— authoring helpers —— +export { defineMidsceneConfig } from './config'; +export { defineRuntime } from './runtime'; + +// —— config types —— +export type { + MidsceneConfig, + UIAgent, + UIAgentConfig, + UIAgentFactory, + UIAgentFactoryCtx, + UIAgentOptions, + UIAgentType, + TestRunnerOptions, + OutputOptions, +} from './config/types'; + +// —— runtime node contract —— +export type { + RuntimeNode, + RuntimeNodeContext, + RuntimeNodeResult, +} from './runtime'; + +// —— core contracts —— +export type { + Verdict, + StepOutput, + StepStatus, + StepResult, + CaseResult, + RunSummary, + OutputStore, + TestResultSoFar, + BuiltinNodeType, +} from './types'; + +// —— general agent (swappable) —— +export type { + GeneralAgentAdapter, + GeneralAgentInput, + GeneralAgentResult, +} from './general-agent/types'; +export { PiGeneralAgent } from './general-agent/pi-general-agent'; +export type { PiGeneralAgentOptions } from './general-agent/pi-general-agent'; +export { extractSkillReferences } from './general-agent/skills'; + +// —— YAML —— +export { parseCaseYaml } from './yaml/parse'; +export type { ParsedCase, FlowStep } from './yaml/types'; +export { BUILTIN_NODES, isBuiltinNode } from './yaml/types'; + +// —— context assembly —— +export { assembleContext } from './context/assembler'; +export type { AssembleContextInput } from './context/assembler'; + +// —— engine / runner —— +export { runCase } from './engine/run-case'; +export type { RunCaseOptions } from './engine/run-case'; +export { runAll } from './runner/run'; +export type { RunAllOptions } from './runner/run'; +export { loadConfig, resolveConfigPath } from './runner/load-config'; +export { discoverCases } from './runner/glob'; +export { createUIAgent } from './ui-agent/factory'; +export type { ResolvedUIAgent } from './ui-agent/factory'; diff --git a/packages/testing-framework/src/runner/glob.ts b/packages/testing-framework/src/runner/glob.ts new file mode 100644 index 0000000000..7063387c1e --- /dev/null +++ b/packages/testing-framework/src/runner/glob.ts @@ -0,0 +1,79 @@ +import { readdirSync, statSync } from 'node:fs'; +import { join, relative, sep } from 'node:path'; + +/** + * Minimal glob support for case discovery. Supports `**`, `*`, and `?` against + * POSIX-style relative paths. Kept dependency-free on purpose (Phase 0 only + * needs patterns like `**\/*.yaml` and `**\/*.draft.yaml`). + */ +export function globToRegExp(pattern: string): RegExp { + let re = ''; + for (let i = 0; i < pattern.length; i++) { + const ch = pattern[i]; + if (ch === '*') { + if (pattern[i + 1] === '*') { + // `**` — match across path segments + i++; + if (pattern[i + 1] === '/') i++; + re += '(?:.*/)?'; + } else { + // `*` — match within a single segment + re += '[^/]*'; + } + } else if (ch === '?') { + re += '[^/]'; + } else if ('.+^${}()|[]\\'.includes(ch)) { + re += `\\${ch}`; + } else { + re += ch; + } + } + return new RegExp(`^${re}$`); +} + +export function matchesAny(relPath: string, patterns: string[]): boolean { + return patterns.some((p) => globToRegExp(p).test(relPath)); +} + +/** Recursively list files under `dir` as POSIX-style paths relative to it. */ +export function listFiles(dir: string): string[] { + const out: string[] = []; + const walk = (current: string) => { + let entries: string[]; + try { + entries = readdirSync(current); + } catch { + return; + } + for (const entry of entries) { + const full = join(current, entry); + let stat: ReturnType; + try { + stat = statSync(full); + } catch { + continue; + } + if (stat.isDirectory()) { + if (entry === 'node_modules' || entry.startsWith('.')) continue; + walk(full); + } else { + out.push(relative(dir, full).split(sep).join('/')); + } + } + }; + walk(dir); + return out; +} + +export function discoverCases( + testDir: string, + include: string[], + exclude: string[] = [], +): string[] { + const files = listFiles(testDir); + return files + .filter((f) => matchesAny(f, include)) + .filter((f) => exclude.length === 0 || !matchesAny(f, exclude)) + .sort() + .map((f) => join(testDir, f)); +} diff --git a/packages/testing-framework/src/runner/load-config.ts b/packages/testing-framework/src/runner/load-config.ts new file mode 100644 index 0000000000..f4fb85af70 --- /dev/null +++ b/packages/testing-framework/src/runner/load-config.ts @@ -0,0 +1,48 @@ +import { existsSync } from 'node:fs'; +import { isAbsolute, resolve } from 'node:path'; +import { createJiti } from 'jiti'; +import type { MidsceneConfig } from '../config/types'; + +const CONFIG_CANDIDATES = [ + 'midscene.config.ts', + 'midscene.config.mts', + 'midscene.config.js', + 'midscene.config.mjs', +]; + +/** Resolve the config file path from an explicit path or a project root. */ +export function resolveConfigPath(cwdOrPath: string = process.cwd()): string { + const abs = isAbsolute(cwdOrPath) ? cwdOrPath : resolve(cwdOrPath); + // If it points directly at a file, use it. + if (existsSync(abs) && /\.(ts|mts|js|mjs)$/.test(abs)) { + return abs; + } + for (const candidate of CONFIG_CANDIDATES) { + const full = resolve(abs, candidate); + if (existsSync(full)) return full; + } + throw new Error( + `[midscene] Could not find midscene.config.ts in ${abs}. Looked for: ${CONFIG_CANDIDATES.join(', ')}.`, + ); +} + +/** + * Load and return the config object from a `midscene.config.*` file. Uses jiti + * so TypeScript config works without a build step. + */ +export async function loadConfig( + cwdOrPath?: string, +): Promise<{ config: MidsceneConfig; configPath: string }> { + const configPath = resolveConfigPath(cwdOrPath); + const jiti = createJiti(configPath, { interopDefault: true }); + const loaded = (await jiti.import(configPath, { + default: true, + })) as MidsceneConfig; + + if (!loaded || typeof loaded !== 'object') { + throw new Error( + `[midscene] ${configPath} must default-export a config object from defineMidsceneConfig().`, + ); + } + return { config: loaded, configPath }; +} diff --git a/packages/testing-framework/src/runner/run.ts b/packages/testing-framework/src/runner/run.ts new file mode 100644 index 0000000000..32235427d2 --- /dev/null +++ b/packages/testing-framework/src/runner/run.ts @@ -0,0 +1,114 @@ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, isAbsolute, resolve } from 'node:path'; +import { getDebug } from '@midscene/shared/logger'; +import { DEFAULT_INCLUDE, type MidsceneConfig } from '../config/types'; +import { runCase } from '../engine/run-case'; +import { PiGeneralAgent } from '../general-agent/pi-general-agent'; +import type { GeneralAgentAdapter } from '../general-agent/types'; +import type { CaseResult, RunSummary } from '../types'; +import { createUIAgent } from '../ui-agent/factory'; +import { parseCaseYaml } from '../yaml/parse'; +import { discoverCases } from './glob'; + +const debug = getDebug('testing-framework:runner'); + +export interface RunAllOptions { + /** Root used to resolve relative paths (testDir, output). Default: cwd. */ + projectRoot?: string; + /** Restrict to specific case files (absolute or project-relative). */ + files?: string[]; + /** Override the process environment passed to nodes/factories. */ + env?: NodeJS.ProcessEnv; +} + +/** + * Run an entire suite from a resolved config. This is the lightweight Phase 0 + * runner; Rstest wiring is out of scope (RFC scope note). + */ +export async function runAll( + config: MidsceneConfig, + options: RunAllOptions = {}, +): Promise { + const projectRoot = options.projectRoot + ? resolve(options.projectRoot) + : process.cwd(); + const env = options.env ?? process.env; + + const testDir = resolvePath(projectRoot, config.testDir); + const include = config.include ?? DEFAULT_INCLUDE; + const exclude = config.exclude ?? []; + + const files = + options.files && options.files.length > 0 + ? options.files.map((f) => resolvePath(projectRoot, f)) + : discoverCases(testDir, include, exclude); + + debug('discovered cases', files); + + const generalAgent: GeneralAgentAdapter = + config.generalAgent ?? new PiGeneralAgent(); + const runtimeNodes = config.runtime ?? {}; + + const startedAt = new Date(); + const cases: CaseResult[] = []; + const bail = config.testRunner?.bail ?? 0; + let failures = 0; + + for (const file of files) { + const source = readFileSync(file, 'utf-8'); + const parsed = parseCaseYaml(source, file); + + const { agent, cleanup } = await createUIAgent( + config.uiAgent, + config.uiAgentOptions, + env, + ); + + try { + const result = await runCase({ + parsed, + file, + uiAgent: agent, + generalAgent, + runtimeNodes, + projectRoot, + env, + }); + cases.push(result); + if (result.status === 'failed') failures++; + } finally { + await cleanup?.(); + } + + if (bail > 0 && failures >= bail) { + debug('bail threshold reached', { bail, failures }); + break; + } + } + + await generalAgent.dispose?.(); + + const finishedAt = new Date(); + const summary: RunSummary = { + startedAt: startedAt.toISOString(), + finishedAt: finishedAt.toISOString(), + durationMs: finishedAt.getTime() - startedAt.getTime(), + total: cases.length, + passed: cases.filter((c) => c.status === 'passed').length, + failed: cases.filter((c) => c.status === 'failed').length, + cases, + }; + + if (config.output?.summary) { + const summaryPath = resolvePath(projectRoot, config.output.summary); + mkdirSync(dirname(summaryPath), { recursive: true }); + writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf-8'); + debug('wrote summary', summaryPath); + } + + return summary; +} + +function resolvePath(root: string, p: string): string { + return isAbsolute(p) ? p : resolve(root, p); +} diff --git a/packages/testing-framework/src/runtime.ts b/packages/testing-framework/src/runtime.ts new file mode 100644 index 0000000000..b09176d2f0 --- /dev/null +++ b/packages/testing-framework/src/runtime.ts @@ -0,0 +1,49 @@ +/** + * `defineRuntime` — custom YAML nodes (RFC §3). + * + * A runtime node owns a whole step's execution. Its handler receives two + * arguments: `input` (this node's own YAML value) and `context` (the ambient + * execution context). It has two output channels: + * - `conclusion` (+ optional `output`): context-facing, flows into later + * verify/agent nodes. + * - `state`: engineering-facing TypeScript state shared between runtime + * nodes; the agent never sees it. + */ +import type { Agent, OutputStore, TestResultSoFar } from './types'; + +export interface RuntimeNodeContext { + /** The UI Agent — runtime nodes may also drive the page. */ + uiAgent: Agent; + /** All past context-facing outputs (read-only). */ + outputs: OutputStore; + /** + * Engineering-facing TS state shared across runtime nodes. NOT visible to + * the agent. Use `conclusion` to expose anything to later verify/agent. + */ + state: Record; + /** The case's accumulated result so far. */ + result: TestResultSoFar; + /** Process environment. */ + env: NodeJS.ProcessEnv; +} + +export interface RuntimeNodeResult { + /** Context-facing output. Enters later verify/agent context. */ + conclusion: string; + /** Optional structured output (also enters context). */ + output?: Record; +} + +export type RuntimeNode = ( + /** This node's YAML value (string or object). */ + input: unknown, + context: RuntimeNodeContext, +) => Promise; + +/** + * Identity helper that gives a custom node full type inference. Mirrors the + * `defineRuntime` entry described in the design doc. + */ +export function defineRuntime(node: RuntimeNode): RuntimeNode { + return node; +} diff --git a/packages/testing-framework/src/types.ts b/packages/testing-framework/src/types.ts new file mode 100644 index 0000000000..ec224eb176 --- /dev/null +++ b/packages/testing-framework/src/types.ts @@ -0,0 +1,100 @@ +/** + * Core contracts for the v2 testing framework (Phase 0). + * + * These types formalize the decisions in RFC 0001. They intentionally model + * "what must be agreed before building": the node model, the verify verdict + * contract, the output contract, and the context-assembly contract. + */ +import type { Agent } from '@midscene/core/agent'; + +/** Built-in node types plus the open-ended custom (runtime) node name. */ +export type BuiltinNodeType = 'ui' | 'verify' | 'soft' | 'agent'; + +/** + * A verify/soft verdict. `verify` gates the case; `soft` only records a warning. + * See RFC §6. + */ +export interface Verdict { + pass: boolean; + /** Human-readable rationale. Always written into the report. */ + reason: string; + /** Optional: screenshot refs, skill response fragments, etc. */ + evidence?: unknown; +} + +/** + * The context-facing output of a single step (RFC §5, §7). + * + * Output is plain natural language by design — there is no schema. `text` + * is the natural-language conclusion; `structured` is an optional bag of + * fields a runtime node chose to expose (it also flows into context). + */ +export interface StepOutput { + text: string; + structured?: Record; +} + +/** Status of a single executed step. */ +export type StepStatus = 'passed' | 'failed' | 'warning' | 'info'; + +/** Result of executing one flow step. */ +export interface StepResult { + index: number; + /** Node type or custom node name. */ + node: string; + /** The instruction given to the node (text, or object for custom nodes). */ + input: unknown; + status: StepStatus; + /** Context-facing output of this step (RFC §7). */ + output?: StepOutput; + /** Present for verify/soft nodes. */ + verdict?: Verdict; + /** Error message when the step threw. */ + error?: string; + /** Wall-clock duration in milliseconds. */ + durationMs: number; +} + +/** Result of executing a single case (one YAML file / flow). */ +export interface CaseResult { + name: string; + file: string; + status: 'passed' | 'failed'; + steps: StepResult[]; + /** Warnings collected from `soft` failures and `agent` errors. */ + warnings: string[]; + durationMs: number; + /** Path to the Midscene HTML report for the UI agent, if generated. */ + reportFile?: string; +} + +/** Aggregate run summary written to `output.summary`. */ +export interface RunSummary { + startedAt: string; + finishedAt: string; + durationMs: number; + total: number; + passed: number; + failed: number; + cases: CaseResult[]; +} + +/** + * The accumulated, read-only view of the case so far, handed to runtime nodes + * (RFC §3) and used to assemble agent context (RFC §7). + */ +export interface TestResultSoFar { + name: string; + file: string; + steps: ReadonlyArray; +} + +/** Read-only store of every past step's context-facing output (RFC §3). */ +export interface OutputStore { + /** All outputs in flow order. */ + all(): ReadonlyArray<{ node: string; index: number; output: StepOutput }>; + /** The most recent output, if any. */ + latest(): StepOutput | undefined; +} + +export type { Agent }; diff --git a/packages/testing-framework/src/ui-agent/factory.ts b/packages/testing-framework/src/ui-agent/factory.ts new file mode 100644 index 0000000000..5629026ef7 --- /dev/null +++ b/packages/testing-framework/src/ui-agent/factory.ts @@ -0,0 +1,134 @@ +/** + * UI Agent creation (RFC §2.1). + * + * `config.uiAgent` is a union: an object (config-style) or a factory function + * (programmatic). This module resolves both into a live Midscene UI Agent plus + * an optional cleanup hook. + */ +import type { AndroidConnectionOpt, WebConnectionOpt } from '@midscene/core'; +import type { Agent } from '@midscene/core/agent'; +import type { UIAgent, UIAgentConfig, UIAgentOptions } from '../config/types'; + +export interface ResolvedUIAgent { + agent: Agent; + cleanup?: () => Promise; +} + +export async function createUIAgent( + uiAgent: UIAgent | undefined, + uiAgentOptions: UIAgentOptions | undefined, + env: NodeJS.ProcessEnv, +): Promise { + if (!uiAgent) { + // `defineMidsceneConfig` only warns about a missing `uiAgent` (some flows + // use custom runtime nodes only). Once a case actually needs the UI Agent, + // fail with a clear, actionable message rather than a cryptic crash. + throw new Error( + '[midscene] This case needs a UI Agent, but `uiAgent` is not configured in midscene.config.ts. Add a `uiAgent` object or factory function.', + ); + } + if (typeof uiAgent === 'function') { + // Programmatic factory: the project fully controls construction. + const result = await uiAgent({ uiAgentOptions, env }); + if (!result?.agent) { + throw new Error( + '[midscene] The uiAgent factory must resolve to `{ agent }`.', + ); + } + return { agent: result.agent, cleanup: result.cleanup }; + } + + return createFromConfig(uiAgent, uiAgentOptions); +} + +async function createFromConfig( + config: UIAgentConfig, + uiAgentOptions: UIAgentOptions | undefined, +): Promise { + switch (config.type) { + case 'web': + return createWebAgent(config.options, uiAgentOptions); + case 'android': + return createAndroidAgent(config.options, uiAgentOptions); + case 'ios': + case 'computer': + throw new Error( + `[midscene] uiAgent.type "${config.type}" is not yet supported by the config-style factory. Provide a \`uiAgent\` factory function instead.`, + ); + default: + throw new Error( + `[midscene] Unknown uiAgent.type "${(config as UIAgentConfig).type}".`, + ); + } +} + +async function createWebAgent( + options: WebConnectionOpt, + uiAgentOptions: UIAgentOptions | undefined, +): Promise { + if (!options?.url) { + throw new Error('[midscene] uiAgent.type "web" requires `options.url`.'); + } + + let mod: typeof import('@midscene/web/puppeteer-agent-launcher'); + try { + mod = await import('@midscene/web/puppeteer-agent-launcher'); + } catch (err) { + throw new Error( + `[midscene] Could not load @midscene/web for the web UI Agent. Install \`@midscene/web\` and \`puppeteer\`. Original error: ${(err as Error).message}`, + ); + } + + const { agent, freeFn } = await mod.puppeteerAgentForTarget( + options, + uiAgentOptions, + ); + + return { + agent, + cleanup: async () => { + for (const free of freeFn) { + try { + await free.fn(); + } catch { + // best-effort cleanup + } + } + }, + }; +} + +async function createAndroidAgent( + options: AndroidConnectionOpt | undefined, + uiAgentOptions: UIAgentOptions | undefined, +): Promise { + const env = options ?? {}; + // `@midscene/android` is an optional peer; load it loosely so the framework + // does not hard-depend on it. + const spec = '@midscene/android'; + let mod: { + agentFromAdbDevice: ( + deviceId?: string, + opts?: Record, + ) => Promise; + }; + try { + mod = (await import(spec)) as typeof mod; + } catch (err) { + throw new Error( + `[midscene] Could not load @midscene/android for the android UI Agent. Original error: ${(err as Error).message}`, + ); + } + + const agent = await mod.agentFromAdbDevice(env.deviceId, { + ...uiAgentOptions, + ...env, + }); + + return { + agent, + cleanup: async () => { + await agent.destroy?.(); + }, + }; +} diff --git a/packages/testing-framework/src/yaml/parse.ts b/packages/testing-framework/src/yaml/parse.ts new file mode 100644 index 0000000000..e53e1d66c3 --- /dev/null +++ b/packages/testing-framework/src/yaml/parse.ts @@ -0,0 +1,120 @@ +import yaml from 'js-yaml'; +import { type FlowStep, type ParsedCase, isBuiltinNode } from './types'; + +/** + * Parse a v2 case YAML string (RFC §1). + * + * Rules enforced here: + * - top-level has `flow` (ordered list) and optional `name`; no v1 `web:` / + * `android:` / `tasks:` environment fields. + * - each step is either a single-key map (`node: value`) or a bare string + * (a custom node with no input, e.g. `- notifySlack`). + * - built-in nodes (ui/verify/soft/agent) must take a string value. + * - custom nodes may take a string or an object. + */ +export function parseCaseYaml(source: string, file = ''): ParsedCase { + let doc: unknown; + try { + doc = yaml.load(source); + } catch (err) { + throw new Error( + `[midscene] Failed to parse YAML in ${file}: ${(err as Error).message}`, + ); + } + + if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) { + throw new Error( + `[midscene] ${file}: a case file must be a mapping with a \`flow\` list.`, + ); + } + + const record = doc as Record; + + for (const legacy of [ + 'web', + 'android', + 'ios', + 'computer', + 'tasks', + 'target', + ]) { + if (legacy in record) { + throw new Error( + `[midscene] ${file}: \`${legacy}\` is not allowed in a v2 case file. Environment and target belong in midscene.config.ts; the case only describes the flow.`, + ); + } + } + + const { name, flow } = record; + + if (name !== undefined && typeof name !== 'string') { + throw new Error(`[midscene] ${file}: \`name\` must be a string.`); + } + + if (!Array.isArray(flow)) { + throw new Error(`[midscene] ${file}: \`flow\` must be a list of steps.`); + } + + const steps: FlowStep[] = flow.map((raw, i) => parseStep(raw, i, file)); + + if (steps.length === 0) { + throw new Error( + `[midscene] ${file}: \`flow\` must contain at least one step.`, + ); + } + + return { name, flow: steps }; +} + +function parseStep(raw: unknown, index: number, file: string): FlowStep { + const where = `${file}: flow[${index}]`; + + // Bare string step: a custom node name with no input (e.g. `- notifySlack`). + if (typeof raw === 'string') { + const node = raw.trim(); + if (!node) { + throw new Error(`[midscene] ${where}: empty step.`); + } + if (isBuiltinNode(node)) { + throw new Error( + `[midscene] ${where}: built-in node \`${node}\` requires a natural-language instruction.`, + ); + } + return { node, input: undefined }; + } + + if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) { + throw new Error( + `[midscene] ${where}: a step must be a single-key mapping (\`node: value\`) or a bare node name.`, + ); + } + + const keys = Object.keys(raw as Record); + if (keys.length !== 1) { + throw new Error( + `[midscene] ${where}: a step must have exactly one key (the node), got: ${keys.join(', ') || '(none)'}.`, + ); + } + + const node = keys[0]; + const input = (raw as Record)[node]; + + if (isBuiltinNode(node)) { + if (typeof input !== 'string') { + throw new Error( + `[midscene] ${where}: built-in node \`${node}\` must take a natural-language string, not ${describeType(input)}.`, + ); + } + if (!input.trim()) { + throw new Error(`[midscene] ${where}: \`${node}\` instruction is empty.`); + } + } + + return { node, input }; +} + +function describeType(value: unknown): string { + if (value === null) return 'null'; + if (Array.isArray(value)) return 'a list'; + return `a ${typeof value}`; +} diff --git a/packages/testing-framework/src/yaml/types.ts b/packages/testing-framework/src/yaml/types.ts new file mode 100644 index 0000000000..a0327695b4 --- /dev/null +++ b/packages/testing-framework/src/yaml/types.ts @@ -0,0 +1,23 @@ +/** Parsed representation of a v2 case YAML (RFC §1). */ +export interface FlowStep { + /** Node type (ui/verify/soft/agent) or a custom runtime node name. */ + node: string; + /** + * The node's input. For built-in nodes this is always a string; for custom + * nodes it may be a string, an object, or undefined (bare-name step). + */ + input: unknown; +} + +export interface ParsedCase { + /** Optional human-readable name. */ + name?: string; + flow: FlowStep[]; +} + +export const BUILTIN_NODES = ['ui', 'verify', 'soft', 'agent'] as const; +export type BuiltinNode = (typeof BUILTIN_NODES)[number]; + +export function isBuiltinNode(node: string): node is BuiltinNode { + return (BUILTIN_NODES as readonly string[]).includes(node); +} diff --git a/packages/testing-framework/tests/smoke/README.md b/packages/testing-framework/tests/smoke/README.md new file mode 100644 index 0000000000..18e8dd4234 --- /dev/null +++ b/packages/testing-framework/tests/smoke/README.md @@ -0,0 +1,14 @@ +# Smoke tests + +These are standalone smoke scripts (not part of `vitest`). Build the package +first (`npx nx build @midscene/testing-framework`), then run with `node`. + +| Script | What it checks | Needs network? | Needs a browser? | +| ------ | -------------- | -------------- | ---------------- | +| `pi-wiring.mjs` | Decision C′: Pi registers a custom base-URL provider, resolves the API key, selects the model, and activates the `report_verdict` tool. No model call. | no | no | +| `browser-smoke.mjs` | Real headless Chrome: discover + parse the example cases, launch the web UI Agent, navigate, capture a screenshot, and drive the engine (runtime node + verify) with a **stubbed** agent runtime. | no | yes | +| `model-smoke.mjs` | Full end-to-end: runs the real example cases with the real UI Agent (`ui`) and real Pi runtime (`verify`/`soft`/`agent`) on the same model endpoint. | yes (model endpoint) | yes | + +`pi-wiring.mjs` and `browser-smoke.mjs` run in CI-like sandboxes. `model-smoke.mjs` +requires `MIDSCENE_MODEL_BASE_URL` / `MIDSCENE_MODEL_API_KEY` / `MIDSCENE_MODEL_NAME` +(and a VL `MIDSCENE_MODEL_FAMILY`) and a network path to that endpoint. diff --git a/packages/testing-framework/tests/smoke/browser-smoke.mjs b/packages/testing-framework/tests/smoke/browser-smoke.mjs new file mode 100644 index 0000000000..830551165b --- /dev/null +++ b/packages/testing-framework/tests/smoke/browser-smoke.mjs @@ -0,0 +1,109 @@ +import { dirname, join } from 'node:path'; +// Real-browser smoke: launches the web UI Agent against the bundled demo page, +// captures a screenshot, and drives the engine end-to-end. The MODEL is stubbed +// (this sandbox cannot reach the model endpoint), so this exercises everything +// EXCEPT live inference: config -> ui agent (chrome launch + navigate) -> +// screenshot capture -> context assembly -> verify path -> summary. +import { pathToFileURL } from 'node:url'; +import { fileURLToPath } from 'node:url'; +import { + createUIAgent, + defineRuntime, + discoverCases, + parseCaseYaml, + runCase, +} from '../../dist/es/index.mjs'; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = join(here, '../../../..'); +const demoUrl = pathToFileURL( + join(repoRoot, 'example', 'site', 'index.html'), +).href; + +// 1) discovery + parse against the real example cases +const found = discoverCases( + join(repoRoot, 'example', 'e2e'), + ['**/*.yaml'], + ['**/*.draft.yaml'], +); +console.log( + 'DISCOVERED', + found.map((f) => f.split('/').pop()), +); +for (const file of found) { + const fs = await import('node:fs'); + parseCaseYaml(fs.readFileSync(file, 'utf-8'), file); +} +console.log('PARSE_OK'); + +// 2) launch the real web UI agent (headless chrome) and navigate to the demo +const { agent, cleanup } = await createUIAgent( + { type: 'web', options: { url: demoUrl } }, + { generateReport: false }, + process.env, +); + +try { + const shot = await agent.interface.screenshotBase64(); + if (!/^data:image\/(png|jpeg);base64,/.test(shot)) { + throw new Error('screenshot is not a data URL'); + } + console.log( + 'SCREENSHOT_OK', + `${shot.slice(0, 28)}... (${shot.length} bytes)`, + ); + + // 3) drive the engine with a runtime node + verify, using a stubbed agent + // runtime so no model call is needed. The stub asserts it received the + // assembled context + the real screenshot. + const parsed = parseCaseYaml(` +name: smoke +flow: + - prepareCartFixture: + scenario: smoke + - verify: Confirm the demo shop page rendered +`); + + let sawScreenshot = false; + let sawConclusion = false; + const stubGeneralAgent = { + run: async (input) => { + sawScreenshot = Boolean(input.screenshotBase64); + sawConclusion = input.context.includes('smoke'); + return { + text: 'looks fine', + verdict: { pass: true, reason: 'rendered' }, + }; + }, + }; + + const result = await runCase({ + parsed, + file: 'smoke.yaml', + uiAgent: agent, + generalAgent: stubGeneralAgent, + runtimeNodes: { + prepareCartFixture: defineRuntime(async (input, ctx) => { + ctx.state.fixture = { scenario: input?.scenario }; + return { conclusion: `prepared ${input?.scenario} fixture` }; + }), + }, + projectRoot: repoRoot, + env: process.env, + }); + + if (result.status !== 'passed') { + throw new Error(`expected passed, got ${result.status}`); + } + if (!sawScreenshot) throw new Error('verify did not receive a screenshot'); + if (!sawConclusion) + throw new Error('runtime conclusion did not reach verify context'); + + console.log('ENGINE_OK', { + status: result.status, + steps: result.steps.map((s) => `${s.node}:${s.status}`), + }); + console.log('BROWSER_SMOKE_OK'); +} finally { + await cleanup?.(); +} diff --git a/packages/testing-framework/tests/smoke/model-smoke.mjs b/packages/testing-framework/tests/smoke/model-smoke.mjs new file mode 100644 index 0000000000..1e10798417 --- /dev/null +++ b/packages/testing-framework/tests/smoke/model-smoke.mjs @@ -0,0 +1,70 @@ +import { dirname, join } from 'node:path'; +// Full model-backed smoke. Run this in an environment that can reach your +// MIDSCENE_MODEL_BASE_URL endpoint (this CI sandbox cannot, so it is not part +// of the automated suite). It runs the real example cases against the bundled +// demo page using the real UI Agent (ui nodes) and the real Pi runtime +// (verify/soft/agent nodes) on the SAME model endpoint. +// +// export MIDSCENE_MODEL_BASE_URL=... +// export MIDSCENE_MODEL_API_KEY=... +// export MIDSCENE_MODEL_NAME=... +// export MIDSCENE_MODEL_FAMILY=... # a VL model is required for UI grounding +// node packages/testing-framework/tests/smoke/model-smoke.mjs +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { runAll } from '../../dist/es/index.mjs'; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = join(here, '../../../..'); +const exampleDir = join(repoRoot, 'example'); +const demoUrl = + process.env.DEMO_URL ?? + pathToFileURL(join(exampleDir, 'site', 'index.html')).href; + +for (const v of [ + 'MIDSCENE_MODEL_BASE_URL', + 'MIDSCENE_MODEL_API_KEY', + 'MIDSCENE_MODEL_NAME', +]) { + if (!process.env[v]) { + console.error(`Missing required env var: ${v}`); + process.exit(2); + } +} + +const summary = await runAll( + { + uiAgent: { type: 'web', options: { url: demoUrl } }, + testDir: join(exampleDir, 'e2e'), + include: ['**/*.yaml'], + exclude: ['**/*.draft.yaml'], + output: { + summary: join(exampleDir, 'midscene_run/output/summary.json'), + reportDir: join(exampleDir, 'midscene_run/report'), + }, + uiAgentOptions: { + aiActContext: 'The user is browsing a demo shop as an anonymous visitor.', + generateReport: true, + }, + runtime: { + prepareCartFixture: async (input, ctx) => { + ctx.state.cartFixture = { scenario: input?.scenario }; + return { + conclusion: `Prepared a "${input?.scenario}" cart fixture.`, + }; + }, + notify: async (_input, ctx) => { + const failed = ctx.result.steps.filter((s) => s.status === 'failed'); + return { + conclusion: + failed.length === 0 + ? 'All gating checks passed; no alert needed.' + : `Would alert: ${failed.length} step(s) failed.`, + }; + }, + }, + }, + { projectRoot: exampleDir }, +); + +console.log(JSON.stringify(summary, null, 2)); +process.exit(summary.failed > 0 ? 1 : 0); diff --git a/packages/testing-framework/tests/smoke/pi-wiring.mjs b/packages/testing-framework/tests/smoke/pi-wiring.mjs new file mode 100644 index 0000000000..aa01b5f572 --- /dev/null +++ b/packages/testing-framework/tests/smoke/pi-wiring.mjs @@ -0,0 +1,92 @@ +import { Type } from '@earendil-works/pi-ai'; +// Validates decision C′: Pi can be pointed at a custom OpenAI-compatible base +// URL (MIDSCENE_MODEL_BASE_URL) so verify/agent share the UI Agent endpoint. +// This constructs the provider + session WITHOUT making a network call. +import { + AuthStorage, + DefaultResourceLoader, + ModelRegistry, + SessionManager, + createAgentSession, + defineTool, + getAgentDir, +} from '@earendil-works/pi-coding-agent'; + +const baseUrl = + process.env.MIDSCENE_MODEL_BASE_URL ?? 'https://example.test/v1'; +const apiKey = process.env.MIDSCENE_MODEL_API_KEY ?? 'sk-fake'; +const modelName = process.env.MIDSCENE_MODEL_NAME ?? 'fake-model'; + +const authStorage = AuthStorage.inMemory(); +const registry = ModelRegistry.inMemory(authStorage); +registry.registerProvider('midscene', { + baseUrl, + apiKey, + models: [ + { + id: modelName, + name: modelName, + api: 'openai-completions', + reasoning: false, + input: ['text', 'image'], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 8192, + }, + ], +}); + +const model = registry.find('midscene', modelName); +if (!model) throw new Error('model not found after registerProvider'); +if (model.baseUrl !== baseUrl) throw new Error('baseUrl override failed'); +if (!registry.hasConfiguredAuth(model)) throw new Error('auth not configured'); + +const auth = await registry.getApiKeyAndHeaders(model); +if (!auth.ok || auth.apiKey !== apiKey) + throw new Error('apiKey resolution failed'); + +const loader = new DefaultResourceLoader({ + cwd: process.cwd(), + agentDir: getAgentDir(), + noExtensions: true, + noThemes: true, + noPromptTemplates: true, +}); +await loader.reload(); + +const reportVerdict = defineTool({ + name: 'report_verdict', + label: 'Report verdict', + description: 'submit verdict', + parameters: Type.Object({ pass: Type.Boolean(), reason: Type.String() }), + execute: async (_id, p) => ({ + content: [{ type: 'text', text: 'ok' }], + details: p, + terminate: true, + }), +}); + +const { session } = await createAgentSession({ + cwd: process.cwd(), + model, + modelRegistry: registry, + authStorage, + sessionManager: SessionManager.inMemory(), + resourceLoader: loader, + customTools: [reportVerdict], + excludeTools: ['edit', 'write'], +}); + +if (!session.model) throw new Error('session has no model'); +if (session.model.baseUrl !== baseUrl) + throw new Error('session model baseUrl mismatch'); +const toolNames = session.getActiveToolNames(); +if (!toolNames.includes('report_verdict')) + throw new Error(`report_verdict not active; got: ${toolNames.join(',')}`); + +session.dispose(); +console.log('PI_WIRING_OK', { + model: session?.model?.id, + baseUrl: model.baseUrl, + activeTools: toolNames, +}); diff --git a/packages/testing-framework/tests/unit-test/config.test.ts b/packages/testing-framework/tests/unit-test/config.test.ts new file mode 100644 index 0000000000..ae771f5696 --- /dev/null +++ b/packages/testing-framework/tests/unit-test/config.test.ts @@ -0,0 +1,53 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { defineMidsceneConfig } from '../../src/config'; +import { defineRuntime } from '../../src/runtime'; + +describe('defineMidsceneConfig', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('accepts a config-style uiAgent object', () => { + const config = defineMidsceneConfig({ + uiAgent: { type: 'web', options: { url: 'https://x.test' } }, + testDir: './e2e', + }); + expect(config.uiAgent).toMatchObject({ type: 'web' }); + }); + + it('accepts a programmatic uiAgent factory', () => { + const config = defineMidsceneConfig({ + uiAgent: async () => ({ agent: {} as never }), + testDir: './e2e', + }); + expect(typeof config.uiAgent).toBe('function'); + }); + + it('warns but does not throw without uiAgent', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const config = + // @ts-expect-error intentionally missing + defineMidsceneConfig({ testDir: './e2e' }); + expect(config.uiAgent).toBeUndefined(); + expect(warnSpy).toHaveBeenCalledWith( + '[Midscene]', + expect.stringMatching(/uiAgent/), + ); + }); + + it('throws without testDir', () => { + expect(() => + // @ts-expect-error intentionally missing testDir + defineMidsceneConfig({ + uiAgent: { type: 'web', options: { url: 'https://x.test' } }, + }), + ).toThrow(/testDir/); + }); +}); + +describe('defineRuntime', () => { + it('returns the node function unchanged', () => { + const node = defineRuntime(async () => ({ conclusion: 'done' })); + expect(typeof node).toBe('function'); + }); +}); diff --git a/packages/testing-framework/tests/unit-test/context-and-skills.test.ts b/packages/testing-framework/tests/unit-test/context-and-skills.test.ts new file mode 100644 index 0000000000..4724a3397f --- /dev/null +++ b/packages/testing-framework/tests/unit-test/context-and-skills.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from 'vitest'; +import { assembleContext } from '../../src/context/assembler'; +import { OutputStoreImpl } from '../../src/engine/output-store'; +import { extractSkillReferences } from '../../src/general-agent/skills'; +import type { StepResult } from '../../src/types'; + +describe('extractSkillReferences', () => { + it('extracts unique $name tokens', () => { + expect( + extractSkillReferences('Use $database and $logs, again $database'), + ).toEqual(['database', 'logs']); + }); + it('returns empty for no references', () => { + expect(extractSkillReferences('just check the page')).toEqual([]); + }); + it('supports hyphenated names', () => { + expect(extractSkillReferences('use $order-db')).toEqual(['order-db']); + }); +}); + +describe('assembleContext', () => { + const pastSteps: StepResult[] = [ + { + index: 0, + node: 'ui', + input: 'Create an order', + status: 'info', + output: { text: 'Created order #123', structured: { orderId: '123' } }, + durationMs: 1, + }, + { + index: 1, + node: 'verify', + input: 'Order exists', + status: 'passed', + output: { text: 'ok' }, + verdict: { pass: true, reason: 'found in db' }, + durationMs: 1, + }, + ]; + + it('includes intents, outputs, and verdicts', () => { + const ctx = assembleContext({ + caseName: 'Create Order', + pastSteps, + instruction: 'Use $database to verify orderId', + kind: 'verify', + }); + expect(ctx).toContain('Create Order'); + expect(ctx).toContain('Create an order'); + expect(ctx).toContain('Created order #123'); + expect(ctx).toContain('"orderId":"123"'); + expect(ctx).toContain('PASS — found in db'); + expect(ctx).toContain('report_verdict'); + expect(ctx).toContain('Use $database to verify orderId'); + }); + + it('frames agent nodes as advisory', () => { + const ctx = assembleContext({ + caseName: 'c', + pastSteps: [], + instruction: 'look around', + kind: 'agent', + }); + expect(ctx).toContain('advisory'); + expect(ctx).toContain('first step'); + }); +}); + +describe('OutputStoreImpl', () => { + it('tracks outputs in order and latest', () => { + const store = new OutputStoreImpl(); + expect(store.latest()).toBeUndefined(); + store.add('ui', 0, { text: 'first' }); + store.add('verify', 1, { text: 'second' }); + expect(store.all()).toHaveLength(2); + expect(store.latest()?.text).toBe('second'); + }); +}); diff --git a/packages/testing-framework/tests/unit-test/engine.test.ts b/packages/testing-framework/tests/unit-test/engine.test.ts new file mode 100644 index 0000000000..d2371937e3 --- /dev/null +++ b/packages/testing-framework/tests/unit-test/engine.test.ts @@ -0,0 +1,192 @@ +import { describe, expect, it, vi } from 'vitest'; +import { runCase } from '../../src/engine/run-case'; +import type { + GeneralAgentAdapter, + GeneralAgentInput, + GeneralAgentResult, +} from '../../src/general-agent/types'; +import { defineRuntime } from '../../src/runtime'; +import type { Agent } from '../../src/types'; +import { parseCaseYaml } from '../../src/yaml/parse'; + +function fakeAgent(overrides: Partial> = {}): Agent { + const agent = { + aiAct: vi.fn(async () => undefined), + aiAsk: vi.fn(async () => 'did the thing'), + interface: { + screenshotBase64: vi.fn(async () => 'data:image/png;base64,AAAA'), + }, + reportFile: '/tmp/report.html', + ...overrides, + }; + return agent as unknown as Agent; +} + +function fakeGeneralAgent( + handler: (input: GeneralAgentInput) => GeneralAgentResult, +): GeneralAgentAdapter { + return { run: async (input) => handler(input) }; +} + +const base = { + projectRoot: '/proj', + env: {} as NodeJS.ProcessEnv, + runtimeNodes: {}, +}; + +describe('runCase node semantics', () => { + it('ui produces a natural-language output', async () => { + const parsed = parseCaseYaml('flow:\n - ui: do something'); + const result = await runCase({ + ...base, + parsed, + file: 'c.yaml', + uiAgent: fakeAgent(), + generalAgent: fakeGeneralAgent(() => ({ text: '' })), + }); + expect(result.status).toBe('passed'); + expect(result.steps[0].output?.text).toBe('did the thing'); + }); + + it('verify pass keeps case green', async () => { + const parsed = parseCaseYaml('flow:\n - verify: ok?'); + const result = await runCase({ + ...base, + parsed, + file: 'c.yaml', + uiAgent: fakeAgent(), + generalAgent: fakeGeneralAgent(() => ({ + text: 'looks good', + verdict: { pass: true, reason: 'all good' }, + })), + }); + expect(result.status).toBe('passed'); + expect(result.steps[0].verdict?.pass).toBe(true); + }); + + it('verify fail fails the case and stops the flow', async () => { + const parsed = parseCaseYaml('flow:\n - verify: ok?\n - ui: next'); + const uiAgent = fakeAgent(); + const result = await runCase({ + ...base, + parsed, + file: 'c.yaml', + uiAgent, + generalAgent: fakeGeneralAgent(() => ({ + text: 'nope', + verdict: { pass: false, reason: 'missing' }, + })), + }); + expect(result.status).toBe('failed'); + expect(result.steps).toHaveLength(1); // stopped before ui + }); + + it('verify with NO verdict is fail-closed', async () => { + const parsed = parseCaseYaml('flow:\n - verify: ok?'); + const result = await runCase({ + ...base, + parsed, + file: 'c.yaml', + uiAgent: fakeAgent(), + generalAgent: fakeGeneralAgent(() => ({ text: 'I am not sure' })), + }); + expect(result.status).toBe('failed'); + expect(result.steps[0].verdict?.pass).toBe(false); + expect(result.steps[0].verdict?.reason).toMatch(/fail-closed/); + }); + + it('soft fail only warns, does not gate', async () => { + const parsed = parseCaseYaml('flow:\n - soft: nit?\n - ui: keep going'); + const result = await runCase({ + ...base, + parsed, + file: 'c.yaml', + uiAgent: fakeAgent(), + generalAgent: fakeGeneralAgent((input) => + input.kind === 'soft' + ? { text: 'minor', verdict: { pass: false, reason: 'tiny glitch' } } + : { text: '' }, + ), + }); + expect(result.status).toBe('passed'); + expect(result.steps).toHaveLength(2); + expect(result.warnings.join(' ')).toMatch(/tiny glitch/); + }); + + it('agent is advisory and never gates, even on error', async () => { + const parsed = parseCaseYaml('flow:\n - agent: explore'); + const result = await runCase({ + ...base, + parsed, + file: 'c.yaml', + uiAgent: fakeAgent(), + generalAgent: { + run: async () => { + throw new Error('boom'); + }, + }, + }); + expect(result.status).toBe('passed'); + expect(result.warnings.join(' ')).toMatch(/boom/); + }); + + it('ui action throwing fails the case', async () => { + const parsed = parseCaseYaml('flow:\n - ui: do'); + const result = await runCase({ + ...base, + parsed, + file: 'c.yaml', + uiAgent: fakeAgent({ + aiAct: vi.fn(async () => { + throw new Error('click failed'); + }), + }), + generalAgent: fakeGeneralAgent(() => ({ text: '' })), + }); + expect(result.status).toBe('failed'); + expect(result.steps[0].error).toMatch(/click failed/); + }); + + it('custom runtime node exposes conclusion and state', async () => { + const parsed = parseCaseYaml( + 'flow:\n - prep:\n scenario: paid\n - verify: check', + ); + const seen: string[] = []; + const result = await runCase({ + ...base, + runtimeNodes: { + prep: defineRuntime(async (input, ctx) => { + ctx.state.fixtureId = 'fx-1'; + return { + conclusion: `prepared ${(input as { scenario: string }).scenario}`, + }; + }), + }, + parsed, + file: 'c.yaml', + uiAgent: fakeAgent(), + generalAgent: fakeGeneralAgent((input) => { + seen.push(input.context); + return { text: 'ok', verdict: { pass: true, reason: 'fine' } }; + }), + }); + expect(result.status).toBe('passed'); + expect(result.steps[0].output?.text).toBe('prepared paid'); + // conclusion flows into later verify context; state never does + expect(seen[0]).toContain('prepared paid'); + expect(seen[0]).not.toContain('fixtureId'); + }); + + it('unknown node fails the case', async () => { + const parsed = parseCaseYaml('flow:\n - mysteryNode: x'); + const result = await runCase({ + ...base, + parsed, + file: 'c.yaml', + uiAgent: fakeAgent(), + generalAgent: fakeGeneralAgent(() => ({ text: '' })), + }); + expect(result.status).toBe('failed'); + expect(result.steps[0].error).toMatch(/Unknown node/); + }); +}); diff --git a/packages/testing-framework/tests/unit-test/glob.test.ts b/packages/testing-framework/tests/unit-test/glob.test.ts new file mode 100644 index 0000000000..3d851aba30 --- /dev/null +++ b/packages/testing-framework/tests/unit-test/glob.test.ts @@ -0,0 +1,40 @@ +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { discoverCases, globToRegExp, matchesAny } from '../../src/runner/glob'; + +describe('globToRegExp', () => { + it('matches **/*.yaml across segments', () => { + const re = globToRegExp('**/*.yaml'); + expect(re.test('a.yaml')).toBe(true); + expect(re.test('e2e/a.yaml')).toBe(true); + expect(re.test('e2e/deep/a.yaml')).toBe(true); + expect(re.test('a.yml')).toBe(false); + }); + + it('* stays within a segment', () => { + const re = globToRegExp('*.yaml'); + expect(re.test('a.yaml')).toBe(true); + expect(re.test('dir/a.yaml')).toBe(false); + }); + + it('matchesAny supports draft exclusion', () => { + expect(matchesAny('e2e/a.draft.yaml', ['**/*.draft.yaml'])).toBe(true); + expect(matchesAny('e2e/a.yaml', ['**/*.draft.yaml'])).toBe(false); + }); +}); + +describe('discoverCases', () => { + it('includes yaml and excludes drafts', () => { + const dir = mkdtempSync(join(tmpdir(), 'mts-glob-')); + mkdirSync(join(dir, 'e2e'), { recursive: true }); + writeFileSync(join(dir, 'e2e', 'a.yaml'), 'flow: []'); + writeFileSync(join(dir, 'e2e', 'b.draft.yaml'), 'flow: []'); + writeFileSync(join(dir, 'e2e', 'c.txt'), 'nope'); + + const found = discoverCases(dir, ['**/*.yaml'], ['**/*.draft.yaml']); + expect(found).toHaveLength(1); + expect(found[0]).toContain('a.yaml'); + }); +}); diff --git a/packages/testing-framework/tests/unit-test/runner-smoke.test.ts b/packages/testing-framework/tests/unit-test/runner-smoke.test.ts new file mode 100644 index 0000000000..3b695915d2 --- /dev/null +++ b/packages/testing-framework/tests/unit-test/runner-smoke.test.ts @@ -0,0 +1,110 @@ +import { mkdtempSync, readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it, vi } from 'vitest'; +import type { MidsceneConfig } from '../../src/config/types'; +import type { + GeneralAgentAdapter, + GeneralAgentInput, +} from '../../src/general-agent/types'; +import { runAll } from '../../src/runner/run'; +import { defineRuntime } from '../../src/runtime'; +import type { Agent, RunSummary } from '../../src/types'; + +/** + * CI-friendly mock-model smoke. Runs the REAL runner end-to-end over the REAL + * example cases — discovery, YAML parsing, the node engine, output store, and + * summary writing — while mocking the two external boundaries the sandbox/CI + * cannot reach: the browser (a fake UI Agent) and the model (a mock agent + * runtime). No network, no Chrome, fully deterministic. + */ + +const here = dirname(fileURLToPath(import.meta.url)); +const packageRoot = join(here, '../..'); +const exampleDir = join(packageRoot, 'example'); + +function fakeUiAgent(): Agent { + return { + aiAct: vi.fn(async () => undefined), + aiAsk: vi.fn(async () => 'recorded the requested values'), + interface: { + screenshotBase64: vi.fn(async () => 'data:image/png;base64,AAAA'), + }, + reportFile: undefined, + } as unknown as Agent; +} + +describe('runner mock-model smoke (example cases)', () => { + it('runs the example suite green with a mocked browser + model', async () => { + const seenVerify: GeneralAgentInput[] = []; + + const mockGeneralAgent: GeneralAgentAdapter = { + run: async (input) => { + if (input.kind === 'verify' || input.kind === 'soft') { + seenVerify.push(input); + return { + text: 'verified', + verdict: { pass: true, reason: 'mock pass' }, + }; + } + return { text: 'mock analysis' }; + }, + }; + + const summaryPath = join( + mkdtempSync(join(tmpdir(), 'mts-smoke-')), + 'summary.json', + ); + + const config: MidsceneConfig = { + uiAgent: async () => ({ agent: fakeUiAgent() }), + testDir: join(exampleDir, 'e2e'), + include: ['**/*.yaml'], + exclude: ['**/*.draft.yaml'], + output: { summary: summaryPath }, + generalAgent: mockGeneralAgent, + runtime: { + prepareCartFixture: defineRuntime(async (rawInput, ctx) => { + const input = (rawInput ?? {}) as { scenario?: string }; + ctx.state.cartFixture = { scenario: input.scenario }; + return { conclusion: `Prepared a "${input.scenario}" cart fixture.` }; + }), + notify: defineRuntime(async (_input, ctx) => { + const failed = ctx.result.steps.filter((s) => s.status === 'failed'); + return { + conclusion: failed.length === 0 ? 'no alert needed' : 'would alert', + }; + }), + }, + }; + + const summary = await runAll(config, { projectRoot: exampleDir }); + + // both example cases discovered and green + expect(summary.total).toBe(2); + expect(summary.failed).toBe(0); + expect(summary.passed).toBe(2); + + // the $catalog skill reference reached the verify boundary + const referenced = seenVerify.flatMap((i) => i.referencedSkills); + expect(referenced).toContain('catalog'); + + // verify always received the (mocked) current screenshot + expect(seenVerify.every((i) => Boolean(i.screenshotBase64))).toBe(true); + + // a runtime conclusion is visible in later context; engineering state is not + const ctxWithFixture = seenVerify.find((i) => + i.context.includes('cart fixture'), + ); + expect(ctxWithFixture).toBeDefined(); + expect(ctxWithFixture?.context).not.toContain('cartFixture'); + + // the summary file was written and round-trips + const written = JSON.parse( + readFileSync(summaryPath, 'utf-8'), + ) as RunSummary; + expect(written.total).toBe(2); + expect(written.cases.map((c) => c.status)).toEqual(['passed', 'passed']); + }); +}); diff --git a/packages/testing-framework/tests/unit-test/yaml-parse.test.ts b/packages/testing-framework/tests/unit-test/yaml-parse.test.ts new file mode 100644 index 0000000000..8f4c6181b9 --- /dev/null +++ b/packages/testing-framework/tests/unit-test/yaml-parse.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from 'vitest'; +import { parseCaseYaml } from '../../src/yaml/parse'; + +describe('parseCaseYaml', () => { + it('parses name + flow with built-in and custom nodes', () => { + const parsed = parseCaseYaml(` +name: Create Order +flow: + - prepareOrderFixture: + scenario: paid-order + - ui: Search for "running shoes" + - verify: The Add to cart button is visible + - soft: No obvious layout glitches + - agent: Inspect the page for anything off + - notifySlack +`); + expect(parsed.name).toBe('Create Order'); + expect(parsed.flow).toHaveLength(6); + expect(parsed.flow[0]).toEqual({ + node: 'prepareOrderFixture', + input: { scenario: 'paid-order' }, + }); + expect(parsed.flow[1]).toEqual({ + node: 'ui', + input: 'Search for "running shoes"', + }); + expect(parsed.flow[5]).toEqual({ node: 'notifySlack', input: undefined }); + }); + + it('allows multi-line natural-language instructions', () => { + const parsed = parseCaseYaml(` +flow: + - ui: | + Create a test order. + Record orderId and pageState. +`); + expect(parsed.flow[0].node).toBe('ui'); + expect(parsed.flow[0].input).toContain('Record orderId'); + }); + + it('rejects v1 environment fields', () => { + expect(() => + parseCaseYaml(` +web: + url: https://example.com +flow: + - ui: do something +`), + ).toThrow(/not allowed in a v2 case file/); + }); + + it('rejects a built-in node with object input', () => { + expect(() => + parseCaseYaml(` +flow: + - verify: + foo: bar +`), + ).toThrow(/must take a natural-language string/); + }); + + it('rejects a built-in bare name without instruction', () => { + expect(() => + parseCaseYaml(` +flow: + - verify +`), + ).toThrow(/requires a natural-language instruction/); + }); + + it('rejects a step with multiple keys', () => { + expect(() => + parseCaseYaml(` +flow: + - ui: do + verify: check +`), + ).toThrow(/exactly one key/); + }); + + it('requires a flow list', () => { + expect(() => parseCaseYaml('name: x')).toThrow(/must be a list of steps/); + }); + + it('requires at least one step', () => { + expect(() => parseCaseYaml('flow: []')).toThrow(/at least one step/); + }); +}); diff --git a/packages/testing-framework/tsconfig.build.json b/packages/testing-framework/tsconfig.build.json new file mode 100644 index 0000000000..51532825a3 --- /dev/null +++ b/packages/testing-framework/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/testing-framework/tsconfig.json b/packages/testing-framework/tsconfig.json new file mode 100644 index 0000000000..5f88c00c0c --- /dev/null +++ b/packages/testing-framework/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../shared/tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "module": "ES2020", + "declarationDir": "dist/types", + "types": ["node"], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src", "tests/unit-test"], + "references": [ + { + "path": "../core" + }, + { + "path": "../shared" + }, + { + "path": "../web-integration" + } + ] +} diff --git a/packages/testing-framework/vitest.config.ts b/packages/testing-framework/vitest.config.ts new file mode 100644 index 0000000000..613747d241 --- /dev/null +++ b/packages/testing-framework/vitest.config.ts @@ -0,0 +1,22 @@ +import path from 'node:path'; +import dotenv from 'dotenv'; +import { defineConfig } from 'vitest/config'; + +/** + * Read environment variables from the repo root .env (if present). + */ +dotenv.config({ + path: path.join(__dirname, '../../.env'), +}); + +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, + test: { + include: ['tests/unit-test/**/*.test.ts'], + testTimeout: 30 * 1000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f7b834236..91d122b4d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1604,6 +1604,49 @@ importers: specifier: 3.0.5 version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + packages/testing-framework: + dependencies: + '@earendil-works/pi-ai': + specifier: ^0.78.0 + version: 0.78.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)(ws@8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76) + '@earendil-works/pi-coding-agent': + specifier: ^0.78.0 + version: 0.78.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)(ws@8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76) + '@midscene/core': + specifier: workspace:* + version: link:../core + '@midscene/shared': + specifier: workspace:* + version: link:../shared + jiti: + specifier: 2.7.0 + version: 2.7.0 + js-yaml: + specifier: 4.1.0 + version: 4.1.0 + devDependencies: + '@midscene/web': + specifier: workspace:* + version: link:../web-integration + '@rslib/core': + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.130))(typescript@5.8.3) + '@types/js-yaml': + specifier: 4.0.9 + version: 4.0.9 + '@types/node': + specifier: ^18.0.0 + version: 18.19.130 + dotenv: + specifier: ^16.4.5 + version: 16.4.7 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vitest: + specifier: 3.0.5 + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.130)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + packages/visualizer: dependencies: '@ant-design/icons': @@ -1860,6 +1903,12 @@ importers: specifier: 3.0.5 version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + tmp-20332-jvOvjDBiByix: + devDependencies: + nx: + specifier: 22.7.5 + version: 22.7.5 + packages: '@alloc/quick-lru@5.2.0': @@ -1904,6 +1953,15 @@ packages: peerDependencies: react: '>=16.9.0' + '@anthropic-ai/sdk@0.91.1': + resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@appium/logger@1.6.1': resolution: {integrity: sha512-3TWpLR1qVQ0usLJ6R49iN4TV9Zs0nog1oL3hakCglwP0g4ZllwwEbp+2b1ovJfX6oOv1wXNREyokq2uxU5gB/Q==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=8'} @@ -1993,6 +2051,107 @@ packages: resolution: {integrity: sha512-Hb4o6h1Pf6yRUAX07DR4JVY7dmQw+RVQMW5/m55GoiAT/VRoKCWBtIUPPOnqDVhbx1Cjfil9b6EDrgJsUAujEQ==} engines: {node: '>= 10'} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.17': + resolution: {integrity: sha512-r8o4h2K7j6P9ngno+8ei0aK0U/4JwDb7A2fMMxGVoSqDN8AFlIzSDeZHME9LcVLR2codyhtr1WAAg+/nmkeeMA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.43': + resolution: {integrity: sha512-g0XVQKzaA/4cq1vz1IvCQwYM+1Pkv01J9yHDpCTXekVuGZRDEz0wqBQ1AuYTq7FM6uik4uBGH8Tb5d9YvgeA7g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.45': + resolution: {integrity: sha512-w9PuOoKCt6+xoESvY+zlV0u3PKQ0mVL259PcsVR6a3S/uYJJHnIi4r1NxdJHEcNldUVRIciltWnFMGBR4YEm3g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.48': + resolution: {integrity: sha512-+6BQ6Lrnc+EyAGElLRW6j+Sa+RirPHnIJsobvYO6nnyK+oGKmz1ne/ieclbLWyjyDKEU3/JVJWcWY3VLFPvGtQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.47': + resolution: {integrity: sha512-Iy2ebWVgrZBH05464uJiQYu6HSSiROnwVZptthEFXx2gWjo1ORCxEAFZB5Cr2MdfrSnZ+0QUPkZ1ZpCqpkUrLQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.50': + resolution: {integrity: sha512-b05Aelq5cqAvCCDQjCYacl0XmR8QhBNSqLbsdISkQmlQBa5oPS66zYPteWcSp5LswbpoIe552EUGjluKiadBig==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.43': + resolution: {integrity: sha512-GPokLNyvTfCmuaHk+v3GKVs4ZT3cMu5kgS2a+NPkOMt96cq6fSIK0g+mZHpGS6Cd4QGrPKesANEaLUKgOskTzg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.47': + resolution: {integrity: sha512-0AzvLrzlvJs0DzbeWGvNj+bX3Uzd7VNS6vDqCOdZzBlCGKGd78uxctJSW9iK/Rt/nxiJqpTvrYQlVJ4guVM2Dw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.47': + resolution: {integrity: sha512-eksfbUErOejUAGWBAcNqaP7IX21oUOEo73d9R56k9Ua4d57qS90NEYkWJsuSGzTXMFulCu17qXJI/qGmM7hvoA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.19': + resolution: {integrity: sha512-MZhrsChY4jwEp7LQnNkcNSvF4KHjDC8es1pgu61h6L48fY7YgRqDfGRoT4ADd7lj4dB+gtOYITgmf7k4QQ2TKg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.15': + resolution: {integrity: sha512-4qYsO6temM6rEawcxHpMPWnRSIiLzsKhuizMlXCVujj54Q+HoGkVlcxk8S+5ekq/hOBdkyRnQjNsZaeRBz60hg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.25': + resolution: {integrity: sha512-1u/r6SYArJr5qBHWQzwGw8cQu32V5Rcx68qb4v+ZhHXFn6dGDtCG5ImyULCLxhTktibLTh2qaRHOoHmkTKCyvA==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.997.15': + resolution: {integrity: sha512-Fpri1/PXKMKveORZ7E00VLTlWS5DkfZkW70PUE+bOnpWpAeHAQLoiDHhkzN3kNWbbSsGg64+IZYiq/EZgME3Mg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.31': + resolution: {integrity: sha512-Kn2up9SlG1KC6wRtwf0d7waTGF6rvp9DxYqB54x6UCKdQ6kyaXCqHL4WGb5vUJga5kS8FxnjhY0LqM28aMvnNQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1048.0': + resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1060.0': + resolution: {integrity: sha512-6NZaMKkFhpaNiwLpHi1sZaYjidL/lCJE6ME6NxwA8gv9vQna+Kr0j4OFwVoz6tANRWM3WbGz6jiPsGX/Vkjwow==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.10': + resolution: {integrity: sha512-992QrTO7G9qCvKD0fx1rMlqcL14plUcRAbwmqqYVsuF3GrqcvlAL9qxR+baMafarEZ+l7DUQ5lCMmt5mbMhF7g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.27': + resolution: {integrity: sha512-hpsCXCOI436kxWpjtRuIHVvuPP81MOw8f18jzfZeg+UOiiOvlqWcmWChzEhJEu16cOC6+ku4ncBN+7rdt+DZ9g==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -2391,6 +2550,24 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} + '@earendil-works/pi-agent-core@0.78.0': + resolution: {integrity: sha512-xhWd59Qzd8yO88gYQw2S4dEQstJJEiUtxRP01//YzVJ61jCtUASMfcyAmYhgGYR4Onp7GmwEAbBBGOiV6Iwk9g==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-ai@0.78.0': + resolution: {integrity: sha512-q0hUrvT6ngT6cgBX0oIbzfQfmzztgdkZobP8OTL+sCOOBlnG6+1YRt8g7zO9CC/4NdeYEqa7uGqWdQhH0fjCLA==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-coding-agent@0.78.0': + resolution: {integrity: sha512-gXt6pD3BoSG0yLwfLqb6844vz6qAO87PvNrv+YSDYKP3QliTjcwIld9v4ihmDcmBjO13QwKswubq/lYCvn4bkg==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-tui@0.78.0': + resolution: {integrity: sha512-3a705FnsVVUhAyceShNB3kS2rpxcxLcx+hqB0u6MMMpHwQGbW+m++MqA6r7eOzq/8FLx5e3vDh38h/SVTk2qzw==} + engines: {node: '>=22.19.0'} + '@electron/asar@4.2.0': resolution: {integrity: sha512-npW1NW5yy8EB9XY/vEw9sUdgmq0sJEhmSBb6bqyFOAw1CSkrhvAvO6QWlW8CdIMo8VN1lkdF345l/MeW0LrY0Q==} engines: {node: '>=22.12.0'} @@ -2430,15 +2607,24 @@ packages: '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -2858,6 +3044,15 @@ packages: resolution: {integrity: sha512-PyUXQWB42s4jBli435TDiYuVsadwRHnMc27YaLouINktvTWsL3FcKrRMGawTayFk46X+n5bE23RjUTWQwrukWw==} engines: {node: '>= 0.10.0'} + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -3518,6 +3713,69 @@ packages: resolution: {integrity: sha512-0FOIepYR4ugPYaHwK7hDeHDkfPOBVvayt9QpvRbi2LT/h2b0GaE/gM9Gag7fsnyYyNaTZ2IGyOuVg07IYepvYQ==} engines: {node: '>=20.0.0'} + '@mariozechner/clipboard-darwin-arm64@0.3.9': + resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@mariozechner/clipboard-darwin-universal@0.3.9': + resolution: {integrity: sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==} + engines: {node: '>= 10'} + os: [darwin] + + '@mariozechner/clipboard-darwin-x64@0.3.9': + resolution: {integrity: sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + resolution: {integrity: sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + resolution: {integrity: sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@mariozechner/clipboard@0.3.9': + resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==} + engines: {node: '>= 10'} + '@mdn/browser-compat-data@7.1.7': resolution: {integrity: sha512-bpWZ7hidvjrwNWcMngZ8nTMTxn8WhnQntsGqEYgPr1vjy66kfwfDVizwXg6PvsgoANZ7nhuRBmvzjpCMk4ITDw==} @@ -3543,6 +3801,9 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@mistralai/mistralai@2.2.1': + resolution: {integrity: sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==} + '@modelcontextprotocol/inspector-cli@0.16.3': resolution: {integrity: sha512-6Hbh+QVRsEDel7hA9qiRklwWEoW4dQXHw4Ltr8JdsU1ziqem4/ERgGxthg/d+qrmbVwW9shOqPIaGoRFaZ264g==} hasBin: true @@ -3601,6 +3862,9 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@nodable/entities@2.1.1': + resolution: {integrity: sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3618,51 +3882,101 @@ packages: cpu: [arm64] os: [darwin] + '@nx/nx-darwin-arm64@22.7.5': + resolution: {integrity: sha512-eoPtwx0qZqvRUD+VVOHm150AlSYwYoPxkDHBBGqKCn5nzPspb0lLWw8q83crM/L1M928YgK0WmGf3C++7eqsTA==} + cpu: [arm64] + os: [darwin] + '@nx/nx-darwin-x64@22.1.3': resolution: {integrity: sha512-XmdccOBp1Lx9DXUzYDX65mkFqFvXaxUKm1d63bfA43vxIYUpR59SASB81KRQ/Q4dgvvU27C0EJuxSJbXsSkSYw==} cpu: [x64] os: [darwin] + '@nx/nx-darwin-x64@22.7.5': + resolution: {integrity: sha512-VLOn/ZoEn3HfjSj+yIHLCM56/el79r+9I28CkZNHaSXJQWZ3edSkcgcfYjVxCurpN2VEwDQHLBeFCH8M+lQ7wQ==} + cpu: [x64] + os: [darwin] + '@nx/nx-freebsd-x64@22.1.3': resolution: {integrity: sha512-O+o4mqPwhKxfdsri4KxDbXbjwIwr04GfTSfA0TwgXs6hFf68qmc45FAmPGrPSvxIJg9+mUVDeFirdS8GcUE0jQ==} cpu: [x64] os: [freebsd] + '@nx/nx-freebsd-x64@22.7.5': + resolution: {integrity: sha512-LEVer/E2xfGvK9Go+imMQoEninOoq/38Z2bhV1SD3AThXrp1xaLFVkW5jQ6juebeVkAeztEoMLFlr576egS0vw==} + cpu: [x64] + os: [freebsd] + '@nx/nx-linux-arm-gnueabihf@22.1.3': resolution: {integrity: sha512-ZIPDgzLq8qmvrZ3Bp+bWXam5uKwahjcChBNtORVtrHQfm4mxov2RMUMKTg2ZsVAWVP64zK+gmzG5LuoZjPMm4Q==} cpu: [arm] os: [linux] + '@nx/nx-linux-arm-gnueabihf@22.7.5': + resolution: {integrity: sha512-NP27EFGpmFJM6RL1Ey/AFJ7gA2xuqtIHaw6jjSNGvfrnZRUNaway30GrVaGGeODf0DsvAty/unqoBMPy6kDHbw==} + cpu: [arm] + os: [linux] + '@nx/nx-linux-arm64-gnu@22.1.3': resolution: {integrity: sha512-wgpPaTpQKl+cCkSuE5zamTVrg14mRvT+bLAeN/yHSUgMztvGxwl3Ll+K9DgEcktBo1PLECTWNkVaW8IAsJm4Rg==} cpu: [arm64] os: [linux] + '@nx/nx-linux-arm64-gnu@22.7.5': + resolution: {integrity: sha512-QLnkJl3HkHsPfpLiNiAiMfpfAeFpic0U1diAxF8RqChOkCpQ7ulvyBVgE1UrQxvhd+gFQ3ed5RNDxtCRw8nTiw==} + cpu: [arm64] + os: [linux] + '@nx/nx-linux-arm64-musl@22.1.3': resolution: {integrity: sha512-o9XmQehSPR2y0RD4evD+Ob3lNFuwsFOL5upVJqZ3rcE6GkJIFPg8SwEP5FaRIS5MwS04fxnek20NZ18BHjjV/g==} cpu: [arm64] os: [linux] + '@nx/nx-linux-arm64-musl@22.7.5': + resolution: {integrity: sha512-cEP6KmwBgnb38+jTTaibWCjwXcHmigqhTfy0tN1be7WZr6bHxbqNLsXqKRN70PSNA3HouZcxw1cdRL8tqbPBBA==} + cpu: [arm64] + os: [linux] + '@nx/nx-linux-x64-gnu@22.1.3': resolution: {integrity: sha512-ekcinyDNTa2huVe02T2SFMR8oArohozRbMGO19zftbObXXI4dLdoAuLNb3vK9Pe4vYOpkhfxBVkZvcWMmx7JdA==} cpu: [x64] os: [linux] + '@nx/nx-linux-x64-gnu@22.7.5': + resolution: {integrity: sha512-tbaX1tZCSpGifDNBfDdEZAMxVF3Yg4bhFP/bm1needc0diqb+Zflc0u5tM5/6BWDMITQDwenJVsNiQ8ZdtJURA==} + cpu: [x64] + os: [linux] + '@nx/nx-linux-x64-musl@22.1.3': resolution: {integrity: sha512-CqpRIJeIgELCqIgjtSsYnnLi6G0uqjbp/Pw9d7w4im4/NmJXqaE9gxpdHA1eowXLgAy9W1LkfzCPS8Q2IScPuQ==} cpu: [x64] os: [linux] + '@nx/nx-linux-x64-musl@22.7.5': + resolution: {integrity: sha512-H0M7csOZIgPT822LqjxSXzf4MXRND15vIkAQe3F3Jlr3Si8LC3tzbL52aVcRfgb8MF/xOB5U47mSwxWt1M2bPQ==} + cpu: [x64] + os: [linux] + '@nx/nx-win32-arm64-msvc@22.1.3': resolution: {integrity: sha512-YbuWb8KQsAR9G0+7b4HA16GV962/VWtRcdS7WY2yaScmPT2W5rObl528Y2j4DuB0j/MVZj12qJKrYfUyjL+UJA==} cpu: [arm64] os: [win32] + '@nx/nx-win32-arm64-msvc@22.7.5': + resolution: {integrity: sha512-JTcZch9YAnDL1gbhqePz3DZ4x7iYemLn1yJzrjbbXAmXju2eiiJiZvJJHbV06+SP9HKXDT8RjTKuAWTdVxnHug==} + cpu: [arm64] + os: [win32] + '@nx/nx-win32-x64-msvc@22.1.3': resolution: {integrity: sha512-G90Sp409ypeOUbmj6nmEbdy043KJUKaZ7pffxmM6i63yEe2F2WdmMgdi525vUEgmq+pfB9zQQOX1sDR/rPFvtg==} cpu: [x64] os: [win32] + '@nx/nx-win32-x64-msvc@22.7.5': + resolution: {integrity: sha512-ngcMyHdBJ9FSz2nHdbZ7gtJlFq0O2b05sPAsVMkZ18CKzdaA1qrBDJfsMO49hPCny505eiT766+CkKdaCDl5kA==} + cpu: [x64] + os: [win32] + '@opentelemetry/api-logs@0.210.0': resolution: {integrity: sha512-CMtLxp+lYDriveZejpBND/2TmadrrhUfChyxzmkFtHaMDdSKfP59MAYyA0ICBvEBdm3iXwLcaj/8Ic/pnGw9Yg==} engines: {node: '>=8.0.0'} @@ -3756,18 +4070,30 @@ packages: '@protobufjs/codegen@2.0.4': resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + '@protobufjs/eventemitter@1.1.0': resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + '@protobufjs/fetch@1.1.0': resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + '@protobufjs/float@1.0.2': resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} '@protobufjs/inquire@1.1.0': resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + '@protobufjs/inquire@1.1.2': + resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} + '@protobufjs/path@1.1.2': resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} @@ -3777,6 +4103,9 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@puppeteer/browsers@2.9.0': resolution: {integrity: sha512-8+xM+cFydYET4X/5/3yZMHs7sjS6c9I6H5I3xJdb6cinzxWUT/I2QVw4avxCQ8QDndwdHkG/FiSZIrCjAbaKvQ==} engines: {node: '>=18'} @@ -4816,6 +5145,9 @@ packages: '@silvia-odwyer/photon-node@0.3.3': resolution: {integrity: sha512-30nDWTHQ7/d1xGnO41ol5tnBA1Bmo2N6h9HNPByBbIYU2xCYB9g4o4zB6vxAq15ixrBRTjb1Nnz1K0Jli3Hxnw==} + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + '@silvia-odwyer/photon@0.3.3': resolution: {integrity: sha512-8BhUjEch4slwRe8uXnaA4vcA5uiiOTT90UMsxulOj2gN98X1p0q9Z4Ysk4DkD05uNgbR9XoSqtZ37w+33w4QKQ==} @@ -4834,6 +5166,46 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@smithy/core@3.24.6': + resolution: {integrity: sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.3.7': + resolution: {integrity: sha512-xj8gq/bjFABAh6qWPSDCYcY3kzQIm4b561C+YnHH4zGq8rOgzQ3Shk+JGlpUxSd41UGiO6FkLdUCtNX1FAeHgg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.4.6': + resolution: {integrity: sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.7.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.7.6': + resolution: {integrity: sha512-3fya8i7GrJilQouk4cZJKdy5k8MWQBpjfXrRNaXDedH8r779tr0jcxyH3+yoTmsluc2+vF4S343yFbnvu8ExDQ==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.4.6': + resolution: {integrity: sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.14.3': + resolution: {integrity: sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -5751,6 +6123,9 @@ packages: axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + axios@1.16.0: + resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} + axios@1.8.3: resolution: {integrity: sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==} @@ -5774,6 +6149,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.3: + resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} + engines: {node: 20 || >=22} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -5856,6 +6235,9 @@ packages: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -5896,6 +6278,9 @@ packages: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + boxen@8.0.1: resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} engines: {node: '>=18'} @@ -5919,8 +6304,8 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -5979,6 +6364,9 @@ packages: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-equal@0.0.1: resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==} engines: {node: '>=0.4.0'} @@ -6791,6 +7179,10 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + diffie-hellman@5.0.3: resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} @@ -6840,6 +7232,10 @@ packages: resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} engines: {node: '>=12'} + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -6861,12 +7257,20 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + edit-json-file@1.8.1: resolution: {integrity: sha512-x8L381+GwqxQejPipwrUZIyAg5gDQ9tLVwiETOspgXiaQztLsrOm7luBW5+Pe31aNezuzDY79YyzF+7viCRPXA==} ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + ejs@5.0.1: + resolution: {integrity: sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==} + engines: {node: '>=0.12.18'} + hasBin: true + electron-to-chromium@1.5.182: resolution: {integrity: sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==} @@ -7248,6 +7652,13 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} + + fast-xml-parser@5.7.3: + resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -7411,6 +7822,15 @@ packages: debug: optional: true + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -7527,6 +7947,14 @@ packages: resolution: {integrity: sha512-HmKyTFGomdAchz4umx8MwBnrnfFmdpwiTyGA4ZOF7rya2Lmgbc9qate4yweInL+0gUBVImhaz12SBGpW3SY4Yg==} engines: {node: '>=22.12.0'} + gaxios@7.1.4: + resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} @@ -7549,6 +7977,10 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -7692,6 +8124,14 @@ packages: resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + google-auth-library@10.6.2: + resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -7835,6 +8275,10 @@ packages: hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} + engines: {node: ^20.17.0 || >=22.9.0} + html-encoding-sniffer@3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -8396,6 +8840,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} @@ -8419,10 +8867,6 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} @@ -8444,6 +8888,9 @@ packages: resolution: {integrity: sha512-r79EVB8jaNAZbq8hvanL8e8JGu2ZNr2bXdHC4ZdQhRImpSPpnWwm5DYVzQ5QxJmtGtKhNNuvqGgbNaFl604fEQ==} engines: {node: '>=6'} + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -8457,6 +8904,10 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -8511,6 +8962,12 @@ packages: resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} engines: {node: '>=12.20'} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -8788,10 +9245,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.0.2: - resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} - engines: {node: 20 || >=22} - lru-cache@11.3.5: resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} engines: {node: 20 || >=22} @@ -8856,6 +9309,11 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} @@ -9426,11 +9884,23 @@ packages: '@swc/core': optional: true - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: + nx@22.7.5: + resolution: {integrity: sha512-zoxsJabb33jl1QYnalDn0bicryrEBgSzdKp90d7VGGv/jDgzKrcLg/hw2ZxeYiOjWPIT/o8QNT9G9vTs4dv3AQ==} + hasBin: true + peerDependencies: + '@swc-node/register': ^1.11.1 + '@swc/core': ^1.15.8 + peerDependenciesMeta: + '@swc-node/register': + optional: true + '@swc/core': + optional: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -9494,6 +9964,18 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openai@6.26.0: + resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + openai@6.3.0: resolution: {integrity: sha512-E6vOGtZvdcb4yXQ5jXvDlUG599OhIkb/GjBLZXS+qk0HF+PJReIldEc9hM8Ft81vn+N6dRdFRb7BZNK8bbvXrw==} hasBin: true @@ -9708,6 +10190,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -9723,6 +10208,10 @@ packages: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -9961,6 +10450,9 @@ packages: resolution: {integrity: sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg==} engines: {node: '>=6'} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} @@ -9970,6 +10462,10 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protobufjs@7.6.2: + resolution: {integrity: sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==} + engines: {node: '>=12.0.0'} + protobufjs@8.0.0: resolution: {integrity: sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==} engines: {node: '>=12.0.0'} @@ -9985,6 +10481,10 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} @@ -10948,6 +11448,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -11106,6 +11611,10 @@ packages: engines: {node: '>=6'} hasBin: true + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} @@ -11348,6 +11857,9 @@ packages: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} + strnum@2.3.0: + resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} + strtok3@6.3.0: resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} engines: {node: '>=10'} @@ -11571,6 +12083,10 @@ packages: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} + tmp@0.2.6: + resolution: {integrity: sha512-5sJPdPjfI5Kx+qbrDesxkglRBxW//g7hCsqspEjwkewGvBMGIKMOTKzLt1hFVJzyadba3lDUN20O9qhvbQUSTA==} + engines: {node: '>=14.14'} + tn1150@0.1.0: resolution: {integrity: sha512-DbplOfQFkqG5IHcDyyrs/lkvSr3mPUVsFf/RbDppOshs22yTPnSJWEe6FkYd1txAwU/zcnR905ar2fi4kwF29w==} engines: {node: '>=0.12'} @@ -11631,6 +12147,9 @@ packages: truncate-utf8-bytes@1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-checker-rspack-plugin@1.2.2: resolution: {integrity: sha512-I9TV5+vg9PfHgdWgmn2J2APfZ4YCszfo7hytKQXJ0bJsxR/MMRRsfyyc2cHCTDS7pyhosdug9WthVWiYdeGYtA==} peerDependencies: @@ -11725,6 +12244,9 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + typebox@1.1.38: + resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -11778,14 +12300,14 @@ packages: resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==} engines: {node: '>=18.17'} - undici@7.16.0: - resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} - engines: {node: '>=20.18.1'} - undici@7.25.0: resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} engines: {node: '>=20.18.1'} + undici@8.3.0: + resolution: {integrity: sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==} + engines: {node: '>=22.19.0'} + unhead@2.1.13: resolution: {integrity: sha512-jO9M1sI6b2h/1KpIu4Jeu+ptumLmUKboRRLxys5pYHFeT+lqTzfNHbYUX9bxVDhC1FBszAGuWcUVlmvIPsah8Q==} @@ -12248,6 +12770,10 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + xml-parse-from-string@1.0.1: resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} @@ -12303,6 +12829,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -12376,6 +12907,11 @@ packages: peerDependencies: zod: ^3.24.1 + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -12413,14 +12949,14 @@ snapshots: '@ant-design/cssinjs-utils@1.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ant-design/cssinjs': 1.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) '@ant-design/cssinjs@1.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@emotion/hash': 0.8.0 '@emotion/unitless': 0.7.5 classnames: 2.5.1 @@ -12448,13 +12984,19 @@ snapshots: '@ant-design/react-slick@1.1.2(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 json2mq: 0.2.0 react: 18.3.1 resize-observer-polyfill: 1.5.1 throttle-debounce: 5.0.2 + '@anthropic-ai/sdk@0.91.1(zod@3.25.76)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 3.25.76 + '@appium/logger@1.6.1': dependencies: console-control-strings: 1.1.0 @@ -12576,6 +13118,229 @@ snapshots: '@ast-grep/napi-win32-ia32-msvc': 0.37.0 '@ast-grep/napi-win32-x64-msvc': 0.37.0 + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.10 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.10 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.10 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.10 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.17 + '@aws-sdk/credential-provider-node': 3.972.50 + '@aws-sdk/eventstream-handler-node': 3.972.19 + '@aws-sdk/middleware-eventstream': 3.972.15 + '@aws-sdk/middleware-websocket': 3.972.25 + '@aws-sdk/token-providers': 3.1048.0 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/fetch-http-handler': 5.4.6 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/core@3.974.17': + dependencies: + '@aws-sdk/types': 3.973.10 + '@aws-sdk/xml-builder': 3.972.27 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.24.6 + '@smithy/signature-v4': 5.4.6 + '@smithy/types': 4.14.3 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.43': + dependencies: + '@aws-sdk/core': 3.974.17 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.45': + dependencies: + '@aws-sdk/core': 3.974.17 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/fetch-http-handler': 5.4.6 + '@smithy/node-http-handler': 4.7.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.48': + dependencies: + '@aws-sdk/core': 3.974.17 + '@aws-sdk/credential-provider-env': 3.972.43 + '@aws-sdk/credential-provider-http': 3.972.45 + '@aws-sdk/credential-provider-login': 3.972.47 + '@aws-sdk/credential-provider-process': 3.972.43 + '@aws-sdk/credential-provider-sso': 3.972.47 + '@aws-sdk/credential-provider-web-identity': 3.972.47 + '@aws-sdk/nested-clients': 3.997.15 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/credential-provider-imds': 4.3.7 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.47': + dependencies: + '@aws-sdk/core': 3.974.17 + '@aws-sdk/nested-clients': 3.997.15 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.50': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.43 + '@aws-sdk/credential-provider-http': 3.972.45 + '@aws-sdk/credential-provider-ini': 3.972.48 + '@aws-sdk/credential-provider-process': 3.972.43 + '@aws-sdk/credential-provider-sso': 3.972.47 + '@aws-sdk/credential-provider-web-identity': 3.972.47 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/credential-provider-imds': 4.3.7 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.43': + dependencies: + '@aws-sdk/core': 3.974.17 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.47': + dependencies: + '@aws-sdk/core': 3.974.17 + '@aws-sdk/nested-clients': 3.997.15 + '@aws-sdk/token-providers': 3.1060.0 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.47': + dependencies: + '@aws-sdk/core': 3.974.17 + '@aws-sdk/nested-clients': 3.997.15 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/eventstream-handler-node@3.972.19': + dependencies: + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.15': + dependencies: + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.25': + dependencies: + '@aws-sdk/core': 3.974.17 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/fetch-http-handler': 5.4.6 + '@smithy/signature-v4': 5.4.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.15': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.17 + '@aws-sdk/signature-v4-multi-region': 3.996.31 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/fetch-http-handler': 5.4.6 + '@smithy/node-http-handler': 4.7.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.31': + dependencies: + '@aws-sdk/types': 3.973.10 + '@smithy/signature-v4': 5.4.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1048.0': + dependencies: + '@aws-sdk/core': 3.974.17 + '@aws-sdk/nested-clients': 3.997.15 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1060.0': + dependencies: + '@aws-sdk/core': 3.974.17 + '@aws-sdk/nested-clients': 3.997.15 + '@aws-sdk/types': 3.973.10 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.10': + dependencies: + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.27': + dependencies: + '@smithy/types': 4.14.3 + fast-xml-parser: 5.7.3 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -13161,11 +13926,79 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} + '@earendil-works/pi-agent-core@0.78.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)(ws@8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)': + dependencies: + '@earendil-works/pi-ai': 0.78.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)(ws@8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76) + ignore: 7.0.5 + typebox: 1.1.38 + yaml: 2.9.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-ai@0.78.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)(ws@8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)': + dependencies: + '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) + '@aws-sdk/client-bedrock-runtime': 3.1048.0 + '@google/genai': 1.52.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + '@mistralai/mistralai': 2.2.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) + '@smithy/node-http-handler': 4.7.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + openai: 6.26.0(ws@8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76) + partial-json: 0.1.7 + typebox: 1.1.38 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-coding-agent@0.78.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)(ws@8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)': + dependencies: + '@earendil-works/pi-agent-core': 0.78.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)(ws@8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76) + '@earendil-works/pi-ai': 0.78.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)(ws@8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76) + '@earendil-works/pi-tui': 0.78.0 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + cross-spawn: 7.0.6 + diff: 8.0.4 + glob: 13.0.6 + highlight.js: 10.7.3 + hosted-git-info: 9.0.3 + ignore: 7.0.5 + jiti: 2.7.0 + minimatch: 10.2.5 + proper-lockfile: 4.1.2 + typebox: 1.1.38 + undici: 8.3.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.9 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-tui@0.78.0': + dependencies: + get-east-asian-width: 1.6.0 + marked: 15.0.12 + '@electron/asar@4.2.0': dependencies: commander: 13.1.0 glob: 13.0.6 - minimatch: 10.0.3 + minimatch: 10.2.5 '@electron/get@2.0.3': dependencies: @@ -13259,30 +14092,43 @@ snapshots: dependencies: '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 - optional: true + + '@emnapi/core@1.4.5': + dependencies: + '@emnapi/wasi-threads': 1.0.4 + tslib: 2.8.1 '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 + optional: true '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 - optional: true + + '@emnapi/runtime@1.4.5': + dependencies: + tslib: 2.8.1 '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.4': + dependencies: + tslib: 2.8.1 '@emnapi/wasi-threads@1.1.0': dependencies: tslib: 2.8.1 + optional: true '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 - optional: true '@emotion/hash@0.8.0': {} @@ -13541,6 +14387,17 @@ snapshots: '@fregante/relaxed-json@2.0.0': {} + '@google/genai@1.52.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)': + dependencies: + google-auth-library: 10.6.2 + p-retry: 4.6.2 + protobufjs: 7.6.2 + ws: 8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -13960,11 +14817,13 @@ snapshots: optionalDependencies: '@types/node': 18.19.62 - '@isaacs/balanced-match@4.0.1': {} + '@isaacs/balanced-match@4.0.1': + optional: true '@isaacs/brace-expansion@5.0.1': dependencies: '@isaacs/balanced-match': 4.0.1 + optional: true '@isaacs/cliui@8.0.2': dependencies: @@ -14320,6 +15179,50 @@ snapshots: js-yaml: 4.1.0 tinyglobby: 0.2.15 + '@mariozechner/clipboard-darwin-arm64@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-universal@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-x64@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard@0.3.9': + optionalDependencies: + '@mariozechner/clipboard-darwin-arm64': 0.3.9 + '@mariozechner/clipboard-darwin-universal': 0.3.9 + '@mariozechner/clipboard-darwin-x64': 0.3.9 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.9 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-musl': 0.3.9 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.9 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.9 + optional: true + '@mdn/browser-compat-data@7.1.7': {} '@mdx-js/mdx@3.1.1': @@ -14453,6 +15356,15 @@ snapshots: '@microsoft/tsdoc@0.15.1': optional: true + '@mistralai/mistralai@2.2.1(bufferutil@4.0.9)(utf-8-validate@6.0.5)': + dependencies: + ws: 8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@modelcontextprotocol/inspector-cli@0.16.3': dependencies: '@modelcontextprotocol/sdk': 1.17.2 @@ -14499,7 +15411,7 @@ snapshots: '@modelcontextprotocol/sdk': 1.17.2 cors: 2.8.5 express: 5.1.0 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) + ws: 8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) zod: 3.25.76 transitivePeerDependencies: - bufferutil @@ -14613,8 +15525,8 @@ snapshots: '@napi-rs/wasm-runtime@0.2.4': dependencies: - '@emnapi/core': 1.7.1 - '@emnapi/runtime': 1.7.1 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.9.0 '@napi-rs/wasm-runtime@1.0.7': @@ -14638,6 +15550,8 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@nodable/entities@2.1.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -14653,33 +15567,63 @@ snapshots: '@nx/nx-darwin-arm64@22.1.3': optional: true + '@nx/nx-darwin-arm64@22.7.5': + optional: true + '@nx/nx-darwin-x64@22.1.3': optional: true + '@nx/nx-darwin-x64@22.7.5': + optional: true + '@nx/nx-freebsd-x64@22.1.3': optional: true + '@nx/nx-freebsd-x64@22.7.5': + optional: true + '@nx/nx-linux-arm-gnueabihf@22.1.3': optional: true + '@nx/nx-linux-arm-gnueabihf@22.7.5': + optional: true + '@nx/nx-linux-arm64-gnu@22.1.3': optional: true + '@nx/nx-linux-arm64-gnu@22.7.5': + optional: true + '@nx/nx-linux-arm64-musl@22.1.3': optional: true + '@nx/nx-linux-arm64-musl@22.7.5': + optional: true + '@nx/nx-linux-x64-gnu@22.1.3': optional: true + '@nx/nx-linux-x64-gnu@22.7.5': + optional: true + '@nx/nx-linux-x64-musl@22.1.3': optional: true + '@nx/nx-linux-x64-musl@22.7.5': + optional: true + '@nx/nx-win32-arm64-msvc@22.1.3': optional: true + '@nx/nx-win32-arm64-msvc@22.7.5': + optional: true + '@nx/nx-win32-x64-msvc@22.1.3': optional: true + '@nx/nx-win32-x64-msvc@22.7.5': + optional: true + '@opentelemetry/api-logs@0.210.0': dependencies: '@opentelemetry/api': 1.9.0 @@ -14777,39 +15721,46 @@ snapshots: '@polka/url@1.0.0-next.28': {} - '@protobufjs/aspromise@1.1.2': - optional: true + '@protobufjs/aspromise@1.1.2': {} - '@protobufjs/base64@1.1.2': - optional: true + '@protobufjs/base64@1.1.2': {} '@protobufjs/codegen@2.0.4': optional: true + '@protobufjs/codegen@2.0.5': {} + '@protobufjs/eventemitter@1.1.0': optional: true + '@protobufjs/eventemitter@1.1.1': {} + '@protobufjs/fetch@1.1.0': dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/inquire': 1.1.0 optional: true - '@protobufjs/float@1.0.2': - optional: true + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} '@protobufjs/inquire@1.1.0': optional: true - '@protobufjs/path@1.1.2': - optional: true + '@protobufjs/inquire@1.1.2': {} - '@protobufjs/pool@1.1.0': - optional: true + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} '@protobufjs/utf8@1.1.0': optional: true + '@protobufjs/utf8@1.1.1': {} + '@puppeteer/browsers@2.9.0': dependencies: debug: 4.4.0 @@ -15207,7 +16158,7 @@ snapshots: '@rc-component/color-picker@2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ant-design/fast-color': 2.0.6 - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -15226,7 +16177,7 @@ snapshots: '@rc-component/mutate-observer@1.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -15242,7 +16193,7 @@ snapshots: '@rc-component/qrcode@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -15250,7 +16201,7 @@ snapshots: '@rc-component/tour@1.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/portal': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@rc-component/trigger': 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 @@ -15260,7 +16211,7 @@ snapshots: '@rc-component/trigger@2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/portal': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -16151,6 +17102,8 @@ snapshots: '@silvia-odwyer/photon-node@0.3.3': {} + '@silvia-odwyer/photon-node@0.3.4': {} + '@silvia-odwyer/photon@0.3.3': {} '@sinclair/typebox@0.34.41': {} @@ -16161,6 +17114,60 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@smithy/core@3.24.6': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.3.7': + dependencies: + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.4.6': + dependencies: + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.3': + dependencies: + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.6': + dependencies: + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@smithy/signature-v4@5.4.6': + dependencies: + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@smithy/types@4.14.3': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@socket.io/component-emitter@3.1.2': {} '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.10)': @@ -16258,7 +17265,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 enhanced-resolve: 5.20.1 - jiti: 2.6.1 + jiti: 2.7.0 lightningcss: 1.30.1 magic-string: 0.30.17 source-map-js: 1.2.1 @@ -17312,6 +18319,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.16.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + axios@1.8.3: dependencies: follow-redirects: 1.15.9 @@ -17337,6 +18352,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.3: {} + balanced-match@4.0.4: {} bare-events@2.5.0: @@ -17409,6 +18426,8 @@ snapshots: big-integer@1.6.52: {} + bignumber.js@9.3.1: {} + binary-extensions@2.3.0: {} bindings@1.5.0: @@ -17465,6 +18484,8 @@ snapshots: boolean@3.2.0: optional: true + bowser@2.14.1: {} + boxen@8.0.1: dependencies: ansi-align: 3.0.1 @@ -17502,7 +18523,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.5: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -17589,6 +18610,8 @@ snapshots: buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} + buffer-equal@0.0.1: {} buffer-from@1.1.2: {} @@ -17750,7 +18773,7 @@ snapshots: commander: 11.1.0 edit-json-file: 1.8.1 globby: 13.2.2 - js-yaml: 4.1.1 + js-yaml: 4.1.0 semver: 7.5.2 table: 6.9.0 type-fest: 4.41.0 @@ -17777,7 +18800,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.16.0 + undici: 7.25.0 whatwg-mimetype: 4.0.0 chokidar@3.6.0: @@ -18118,7 +19141,7 @@ snapshots: dependencies: '@types/node': 18.19.130 cosmiconfig: 9.0.0(typescript@5.8.3) - jiti: 2.6.1 + jiti: 2.7.0 typescript: 5.8.3 cosmiconfig@8.3.6(typescript@5.8.3): @@ -18134,7 +19157,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.1.0 parse-json: 5.2.0 optionalDependencies: typescript: 5.8.3 @@ -18449,6 +19472,8 @@ snapshots: diff@4.0.2: {} + diff@8.0.4: {} + diffie-hellman@5.0.3: dependencies: bn.js: 4.12.0 @@ -18507,6 +19532,10 @@ snapshots: dependencies: dotenv: 16.4.7 + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.4.7 + dotenv@16.4.5: {} dotenv@16.4.7: {} @@ -18528,6 +19557,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + edit-json-file@1.8.1: dependencies: find-value: 1.0.13 @@ -18538,6 +19571,8 @@ snapshots: ee-first@1.1.1: {} + ejs@5.0.1: {} + electron-to-chromium@1.5.182: {} electron-to-chromium@1.5.260: {} @@ -19166,6 +20201,18 @@ snapshots: fast-uri@3.1.0: {} + fast-xml-builder@1.2.0: + dependencies: + path-expression-matcher: 1.5.0 + xml-naming: 0.1.0 + + fast-xml-parser@5.7.3: + dependencies: + '@nodable/entities': 2.1.1 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.3.0 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -19339,6 +20386,8 @@ snapshots: follow-redirects@1.15.9: {} + follow-redirects@1.16.0: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -19475,6 +20524,22 @@ snapshots: transitivePeerDependencies: - supports-color + gaxios@7.1.4: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.4 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + generate-function@2.3.1: dependencies: is-property: 1.0.2 @@ -19493,6 +20558,8 @@ snapshots: get-east-asian-width@1.4.0: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -19589,7 +20656,7 @@ snapshots: foreground-child: 3.3.0 jackspeak: 3.4.3 minimatch: 9.0.5 - minipass: 7.1.2 + minipass: 7.1.3 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 @@ -19692,6 +20759,19 @@ snapshots: merge2: 1.4.1 slash: 4.0.0 + google-auth-library@10.6.2: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.4 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + gopd@1.2.0: {} got@11.8.6: @@ -19926,6 +21006,10 @@ snapshots: hosted-git-info@2.8.9: {} + hosted-git-info@9.0.3: + dependencies: + lru-cache: 11.3.5 + html-encoding-sniffer@3.0.0: dependencies: whatwg-encoding: 2.0.0 @@ -20479,6 +21563,8 @@ snapshots: jiti@2.6.1: {} + jiti@2.7.0: {} + jju@1.4.0: {} jose@5.9.6: {} @@ -20498,10 +21584,6 @@ snapshots: dependencies: argparse: 2.0.1 - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - jsbn@1.1.0: {} jsdom@29.0.2: @@ -20541,6 +21623,10 @@ snapshots: stream-combiner: 0.2.2 unorm: 1.6.0 + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} json-cycle@1.5.0: {} @@ -20551,6 +21637,11 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.28.4 + ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -20598,6 +21689,17 @@ snapshots: junk@4.0.1: {} + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -20829,8 +21931,7 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 - long@5.3.2: - optional: true + long@5.3.2: {} longest-streak@3.1.0: {} @@ -20852,8 +21953,6 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.0.2: {} - lru-cache@11.3.5: {} lru-cache@4.1.5: @@ -20914,6 +22013,8 @@ snapshots: markdown-table@3.0.4: {} + marked@15.0.12: {} + marky@1.3.0: {} matcher@3.0.0: @@ -21496,10 +22597,11 @@ snapshots: minimatch@10.0.3: dependencies: '@isaacs/brace-expansion': 5.0.1 + optional: true minimatch@10.2.5: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.6 minimatch@3.1.2: dependencies: @@ -21748,6 +22850,132 @@ snapshots: transitivePeerDependencies: - debug + nx@22.7.5: + dependencies: + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@emnapi/wasi-threads': 1.0.4 + '@jest/diff-sequences': 30.0.1 + '@napi-rs/wasm-runtime': 0.2.4 + '@tybys/wasm-util': 0.9.0 + '@yarnpkg/lockfile': 1.1.0 + '@zkochan/js-yaml': 0.0.7 + ansi-colors: 4.1.3 + ansi-regex: 5.0.1 + ansi-styles: 4.3.0 + argparse: 2.0.1 + asynckit: 0.4.0 + axios: 1.16.0 + balanced-match: 4.0.3 + base64-js: 1.5.1 + bl: 4.1.0 + brace-expansion: 5.0.6 + buffer: 5.7.1 + call-bind-apply-helpers: 1.0.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + cliui: 8.0.1 + clone: 1.0.4 + color-convert: 2.0.1 + color-name: 1.1.4 + combined-stream: 1.0.8 + defaults: 1.0.4 + define-lazy-prop: 2.0.0 + delayed-stream: 1.0.0 + dotenv: 16.4.7 + dotenv-expand: 12.0.3 + dunder-proto: 1.0.1 + ejs: 5.0.1 + emoji-regex: 8.0.0 + end-of-stream: 1.4.5 + enquirer: 2.3.6 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + escalade: 3.2.0 + escape-string-regexp: 1.0.5 + figures: 3.2.0 + flat: 5.0.2 + follow-redirects: 1.16.0 + form-data: 4.0.5 + fs-constants: 1.0.0 + function-bind: 1.1.2 + get-caller-file: 2.0.5 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + has-flag: 4.0.0 + has-symbols: 1.1.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + ieee754: 1.2.1 + ignore: 7.0.5 + inherits: 2.0.4 + is-docker: 2.2.1 + is-fullwidth-code-point: 3.0.0 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + is-wsl: 2.2.0 + json5: 2.2.3 + jsonc-parser: 3.2.0 + lines-and-columns: 2.0.3 + log-symbols: 4.1.0 + math-intrinsics: 1.1.0 + mime-db: 1.52.0 + mime-types: 2.1.35 + mimic-fn: 2.1.0 + minimatch: 10.2.5 + minimist: 1.2.8 + npm-run-path: 4.0.1 + once: 1.4.0 + onetime: 5.1.2 + open: 8.4.2 + ora: 5.3.0 + path-key: 3.1.1 + picocolors: 1.1.1 + proxy-from-env: 2.1.0 + readable-stream: 3.6.2 + require-directory: 2.1.1 + resolve.exports: 2.0.3 + restore-cursor: 3.1.0 + safe-buffer: 5.2.1 + semver: 7.7.4 + signal-exit: 3.0.7 + smol-toml: 1.6.1 + string-width: 4.2.3 + string_decoder: 1.3.0 + strip-ansi: 6.0.1 + strip-bom: 3.0.0 + supports-color: 7.2.0 + tar-stream: 2.2.0 + tmp: 0.2.6 + tree-kill: 1.2.2 + tsconfig-paths: 4.2.0 + tslib: 2.8.1 + util-deprecate: 1.0.2 + wcwidth: 1.0.1 + wrap-ansi: 7.0.0 + wrappy: 1.0.2 + y18n: 5.0.8 + yaml: 2.9.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@nx/nx-darwin-arm64': 22.7.5 + '@nx/nx-darwin-x64': 22.7.5 + '@nx/nx-freebsd-x64': 22.7.5 + '@nx/nx-linux-arm-gnueabihf': 22.7.5 + '@nx/nx-linux-arm64-gnu': 22.7.5 + '@nx/nx-linux-arm64-musl': 22.7.5 + '@nx/nx-linux-x64-gnu': 22.7.5 + '@nx/nx-linux-x64-musl': 22.7.5 + '@nx/nx-win32-arm64-msvc': 22.7.5 + '@nx/nx-win32-x64-msvc': 22.7.5 + transitivePeerDependencies: + - debug + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -21824,9 +23052,14 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openai@6.26.0(ws@8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76): + optionalDependencies: + ws: 8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + zod: 3.25.76 + openai@6.3.0(ws@8.20.0)(zod@3.25.76): optionalDependencies: - ws: 8.20.0 + ws: 8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) zod: 3.25.76 opener@1.5.2: {} @@ -21845,7 +23078,7 @@ snapshots: bl: 4.1.0 chalk: 4.1.2 cli-cursor: 3.1.0 - cli-spinners: 2.6.1 + cli-spinners: 2.9.2 is-interactive: 1.0.0 log-symbols: 4.1.0 strip-ansi: 6.0.1 @@ -22060,6 +23293,8 @@ snapshots: parseurl@1.3.3: {} + partial-json@0.1.7: {} + path-browserify@1.0.1: {} path-exists@3.0.0: {} @@ -22068,6 +23303,8 @@ snapshots: path-exists@5.0.0: {} + path-expression-matcher@1.5.0: {} + path-is-absolute@1.0.1: {} path-is-inside@1.0.2: {} @@ -22083,12 +23320,12 @@ snapshots: path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 - minipass: 7.1.2 + minipass: 7.1.3 path-scurry@2.0.0: dependencies: - lru-cache: 11.0.2 - minipass: 7.1.2 + lru-cache: 11.3.5 + minipass: 7.1.3 path-scurry@2.0.2: dependencies: @@ -22271,12 +23508,33 @@ snapshots: dependencies: make-error: 1.3.6 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + property-information@6.5.0: {} property-information@7.0.0: {} proto-list@1.2.4: {} + protobufjs@7.6.2: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 18.19.130 + long: 5.3.2 + protobufjs@8.0.0: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -22313,6 +23571,8 @@ snapshots: proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} + prr@1.0.1: optional: true @@ -22440,7 +23700,7 @@ snapshots: rc-cascader@3.28.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 array-tree-filter: 2.1.0 classnames: 2.5.1 rc-select: 14.15.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22451,7 +23711,7 @@ snapshots: rc-checkbox@3.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -22459,7 +23719,7 @@ snapshots: rc-collapse@3.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22468,7 +23728,7 @@ snapshots: rc-dialog@9.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/portal': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22478,7 +23738,7 @@ snapshots: rc-drawer@7.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/portal': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22488,7 +23748,7 @@ snapshots: rc-dropdown@4.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/trigger': 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22497,7 +23757,7 @@ snapshots: rc-field-form@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/async-validator': 5.0.4 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -22505,7 +23765,7 @@ snapshots: rc-image@7.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/portal': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 rc-dialog: 9.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22516,7 +23776,7 @@ snapshots: rc-input-number@9.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/mini-decimal': 1.1.0 classnames: 2.5.1 rc-input: 1.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22526,7 +23786,7 @@ snapshots: rc-input@1.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -22534,7 +23794,7 @@ snapshots: rc-mentions@2.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/trigger': 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 rc-input: 1.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22546,7 +23806,7 @@ snapshots: rc-menu@9.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/trigger': 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22557,7 +23817,7 @@ snapshots: rc-motion@2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -22565,7 +23825,7 @@ snapshots: rc-notification@5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22583,7 +23843,7 @@ snapshots: rc-pagination@4.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -22591,7 +23851,7 @@ snapshots: rc-picker@4.6.15(date-fns@2.30.0)(dayjs@1.11.13)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/trigger': 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 rc-overflow: 1.3.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22606,7 +23866,7 @@ snapshots: rc-progress@4.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -22614,7 +23874,7 @@ snapshots: rc-rate@2.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -22622,7 +23882,7 @@ snapshots: rc-resize-observer@1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -22631,7 +23891,7 @@ snapshots: rc-segmented@2.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22640,7 +23900,7 @@ snapshots: rc-select@14.15.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/trigger': 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22652,7 +23912,7 @@ snapshots: rc-slider@11.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -22660,7 +23920,7 @@ snapshots: rc-steps@6.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -22668,7 +23928,7 @@ snapshots: rc-switch@4.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -22676,7 +23936,7 @@ snapshots: rc-table@7.47.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/context': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22687,7 +23947,7 @@ snapshots: rc-tabs@15.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-dropdown: 4.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-menu: 9.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22699,7 +23959,7 @@ snapshots: rc-textarea@1.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-input: 1.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22709,7 +23969,7 @@ snapshots: rc-tooltip@6.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@rc-component/trigger': 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classnames: 2.5.1 react: 18.3.1 @@ -22717,7 +23977,7 @@ snapshots: rc-tree-select@5.23.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-select: 14.15.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-tree: 5.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22727,7 +23987,7 @@ snapshots: rc-tree@5.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-motion: 2.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -22737,7 +23997,7 @@ snapshots: rc-upload@4.8.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 classnames: 2.5.1 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -23459,6 +24719,8 @@ snapshots: semver@7.7.3: {} + semver@7.7.4: {} + send@0.19.0: dependencies: debug: 2.6.9 @@ -23718,6 +24980,8 @@ snapshots: wcwidth: 1.0.1 yargs: 15.4.1 + smol-toml@1.6.1: {} + snake-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -24003,6 +25267,8 @@ snapshots: strip-json-comments@5.0.3: {} + strnum@2.3.0: {} + strtok3@6.3.0: dependencies: '@tokenizer/token': 0.3.0 @@ -24222,6 +25488,8 @@ snapshots: tmp@0.2.5: {} + tmp@0.2.6: {} + tn1150@0.1.0: dependencies: unorm: 1.6.0 @@ -24271,6 +25539,8 @@ snapshots: dependencies: utf8-byte-length: 1.0.5 + ts-algebra@2.0.0: {} + ts-checker-rspack-plugin@1.2.2(@rspack/core@1.6.8)(typescript@5.8.3): dependencies: '@babel/code-frame': 7.27.1 @@ -24382,6 +25652,8 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.1 + typebox@1.1.38: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -24442,10 +25714,10 @@ snapshots: undici@6.22.0: {} - undici@7.16.0: {} - undici@7.25.0: {} + undici@8.3.0: {} + unhead@2.1.13: dependencies: hookable: 6.0.1 @@ -25230,8 +26502,10 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 6.0.5 - ws@8.20.0: - optional: true + ws@8.20.0(bufferutil@4.0.9)(utf-8-validate@6.0.5): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 6.0.5 wsl-utils@0.1.0: dependencies: @@ -25248,6 +26522,8 @@ snapshots: xml-name-validator@5.0.0: {} + xml-naming@0.1.0: {} + xml-parse-from-string@1.0.1: {} xml2js@0.5.0: @@ -25284,6 +26560,8 @@ snapshots: yaml@2.8.2: {} + yaml@2.9.0: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 @@ -25364,6 +26642,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.25.76: {} zustand@4.5.2(@types/react@18.3.23)(immer@10.1.1)(react@18.3.1):