pi-cursor-sdk 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +4 -4
  3. package/dist/cursor-fallback-models.generated.js +161 -1
  4. package/dist/cursor-local-run-terminal-error.js +57 -0
  5. package/dist/cursor-provider-turn-finalize.js +2 -0
  6. package/dist/cursor-provider-turn-send.js +3 -0
  7. package/dist/cursor-session-store.js +2 -1
  8. package/docs/cursor-live-smoke-checklist.md +2 -2
  9. package/docs/cursor-model-ux-spec.md +4 -2
  10. package/docs/cursor-native-tool-visual-audit.md +1 -1
  11. package/docs/cursor-testing-lessons.md +7 -1
  12. package/docs/cursor-tool-surfaces.md +3 -3
  13. package/docs/development.md +41 -6
  14. package/docs/reference.md +9 -5
  15. package/node_modules/@modelcontextprotocol/core/node_modules/zod/package.json +1 -1
  16. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
  17. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
  18. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
  19. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/core/compile.ts +9 -0
  20. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/core/schemas.ts +35 -1
  21. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/core/util.ts +13 -0
  22. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/core/versions.ts +1 -1
  23. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/compile.cjs +8 -0
  24. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/compile.js +8 -0
  25. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/schemas.cjs +30 -0
  26. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/schemas.d.cts +3 -1
  27. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/schemas.d.ts +3 -1
  28. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/schemas.js +30 -0
  29. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/util.cjs +15 -0
  30. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/util.d.cts +2 -0
  31. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/util.d.ts +2 -0
  32. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/util.js +14 -0
  33. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/versions.cjs +1 -1
  34. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/versions.js +1 -1
  35. package/node_modules/@modelcontextprotocol/server/node_modules/zod/package.json +1 -1
  36. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
  37. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
  38. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
  39. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/core/compile.ts +9 -0
  40. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/core/schemas.ts +35 -1
  41. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/core/util.ts +13 -0
  42. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/core/versions.ts +1 -1
  43. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/compile.cjs +8 -0
  44. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/compile.js +8 -0
  45. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/schemas.cjs +30 -0
  46. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/schemas.d.cts +3 -1
  47. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/schemas.d.ts +3 -1
  48. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/schemas.js +30 -0
  49. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/util.cjs +15 -0
  50. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/util.d.cts +2 -0
  51. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/util.d.ts +2 -0
  52. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/util.js +14 -0
  53. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/versions.cjs +1 -1
  54. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/versions.js +1 -1
  55. package/package.json +10 -3
  56. package/scripts/steering-rpc-smoke.mjs +1 -1
  57. package/src/cursor-fallback-models.generated.ts +161 -1
  58. package/src/cursor-local-run-terminal-error.ts +58 -0
  59. package/src/cursor-provider-turn-finalize.ts +2 -0
  60. package/src/cursor-provider-turn-send.ts +2 -0
  61. package/src/cursor-session-store.ts +2 -1
@@ -12,6 +12,20 @@ No paid Cursor Cloud testing for generic PRs or releases. Only a PR or issue exp
12
12
 
13
13
  The older live smoke helpers remain useful for inner-loop debugging and focused visual audits, not as the release gate. Use [Cursor live smoke checklist](./cursor-live-smoke-checklist.md), `npm run smoke:visual`, `npm run smoke:live`, or direct `pi --approve -e . --cursor-no-fast --model cursor/grok-4.6` runs only when a changed TUI/card/runtime behavior needs new live evidence. Select one relevant check rather than executing every helper. `npm run smoke:visual` captures an offscreen PTY rendered through browser/xterm and saved as PNG screenshots with Playwright, or with `agent_browser` from the generated HTML when available. Its default matrix is native replay only: native replay registration is forced on, Cursor setting sources are disabled, the pi bridge is off, overlapping built-in pi tools are not exposed, and inherited Cursor SDK event-debug artifact env is cleared; `--event-debug` writes to a deterministic debug directory under the visual output directory. The visible TUI/output, rendered screenshots, scrubbed diagnostics, and persisted JSONL must agree. See [Cursor testing lessons](./cursor-testing-lessons.md) for auth.json seeding, isolated `/tmp` harness layout, JSONL replay-error scans, and other regression traps.
14
14
 
15
+ ## Zod peer and bridge packaging
16
+
17
+ Keep the exact SDK peer `zod@4.6.6` external and the bridge's newer nested `zod@4.6.7` copies bundled. Both contain the upstream bounded-collection fix; their published tarballs differ only in version metadata. This uses npm's ordinary locked dependency layout, not overrides, copied dependencies or SDK patches.
18
+
19
+ Preserve that layout when updating dependencies. Installing the bridge's Zod version at the package root or deduplicating can move the shared copy to the root, where npm includes it in the bridge bundle. Pi's `--legacy-peer-deps` consumer then hoists the unbundled SDK without an accessible Zod peer. `--omit=peer` is not equivalent and does not reproduce that failure.
20
+
21
+ `test/package-metadata.test.ts` owns the required peer/entrypoint contract, SDK v4/v4-mini and bridge parser controls, and the packed-root exclusion guard.
22
+
23
+ Release qualification compares runtime dependency metadata, the runtime lock graph, and bundled files with the previous published release. A change to those inputs, or an added or removed source line that mentions Zod, triggers installer qualification. The source-line check includes prose.
24
+
25
+ The qualifier installs the exact candidate tarball into fresh consumers with default npm, Pi-managed npm (`--legacy-peer-deps`), npm `--omit=peer`, Pi-managed pnpm (peer auto-installation off) and Pi-managed Bun (`--omit=peer`). It checks actual SDK ESM imports and SDK-relative `zod`, `zod/v3`, `zod/v4`, `zod/v4-mini` resolution, plus the bundled bridge under Node and Bun, and retains artifact hashes and installer versions. A clean audit or source-only parser test does not prove the published installation works.
26
+
27
+ The installer qualifier attaches its passed or skipped receipt to the prepared review scope. The deep reviewer must read that receipt, and the review command verifies its hash before and after the review.
28
+
15
29
  ## Maintainer Cursor SDK event capture
16
30
 
17
31
  Use `npm run debug:sdk-events` to capture timestamped `run.stream()`, `onDelta`, and `onStep` timelines for one direct `@cursor/sdk` run.
@@ -22,19 +36,40 @@ See [Cursor testing lessons](./cursor-testing-lessons.md#cursor-sdk-event-captur
22
36
 
23
37
  ## Automatic npm releases (maintainers)
24
38
 
25
- Follow the [shared release procedure](https://github.com/fitchmultz/.github#automatic-npm-releases): merge a reviewed PR into `main` with an intentional `package.json` version bump and a matching versioned `CHANGELOG.md` section. Automation never bumps versions, overwrites releases, or republishes an existing version. Once configured and enabled, it runs the existing offline compatibility and native macOS/Windows jobs, qualifies one candidate tarball, then publishes it without an approval step.
39
+ Follow the [shared release procedure](https://github.com/fitchmultz/.github#automatic-npm-releases). When enabled, the `npm release` workflow plans releases on `main` pushes, a daily schedule, and manual dispatch. It runs the existing offline compatibility and native macOS/Windows jobs and qualifies a candidate tarball before publication to npm and GitHub.
40
+
41
+ Before merging, check the final PR head (up to date with `main`) against the [release review gate](../AGENTS.md#release-review-gate-maintainer) and [cost-conscious verification policy](#maintainer-cost-conscious-verification). Complete the exact-release-diff deep review with zero findings, the documented pre-release catalog check, and any genuinely necessary changed-behavior proof. When automatic versioning is enabled, its version PR receives this qualification before merge; publication independently qualifies the retained post-merge candidate. Authenticated catalog and review steps run in the protected release environment, with separate credentials for each process. Automated Cursor PR reviews remain unchanged, and no routine paid Cloud campaigns or paid retries are added. Automated publication disables npm hooks to publish the checked bytes; existing manual publisher instructions remain valid.
42
+
43
+ Before retrying a failed or unpublished candidate, inspect its exact source/run identities and the shared retry procedure. An authentication repair alone does not authorize publishing an old candidate after `main` advances. Never move an existing tag, overwrite its draft/assets, or relabel old proof. The [0.5.1 changelog](../CHANGELOG.md#051-unpublished-source-history) is unpublished source history carried into 0.5.2, not a successful publication record. Set repository variable `NPM_RELEASE_ENABLED` to anything other than `true` to stop new release plans; cancel pending runs separately when needed. Workflow validation is not evidence of a completed real OIDC publication.
26
44
 
27
- For the first release containing the native-tool opt-out and `/cursor-abort` from #372, update their availability notes in `README.md`, `docs/reference.md`, and `docs/cursor-tool-surfaces.md` to name the minimum supporting release version. Preserve the warning that published 0.5.3 lacks these controls, and do not call the candidate published until npm/GitHub publication records confirm it.
45
+ Publication records are independent: npm published 0.5.2, while GitHub's `v0.5.2` release remains a draft bound to source `9820ae7`. Preserve that tag, draft, and candidate assets unchanged. Verify npm and GitHub records independently for each new release.
28
46
 
29
- Merging publishes, so before merging check the final PR head (up to date with `main`) and version against the [release review gate](../AGENTS.md#release-review-gate-maintainer) and [cost-conscious verification policy](#maintainer-cost-conscious-verification). Complete the exact-release-diff deep review with zero findings, the documented pre-release catalog check, and any genuinely necessary changed-behavior proof. Merging attests genuine satisfaction of existing requirements; it does not create proof. Authenticated checks remain outside CI; automated Cursor PR reviews remain unchanged, and no routine paid Cloud campaigns or paid retries are added. Automated publication disables npm hooks to publish the checked bytes; existing manual publisher instructions remain valid.
47
+ ### Review and catalog qualification commands
30
48
 
31
- Failed/unpublished candidates can retry daily at 12:17 UTC or via manual dispatch of `npm release` on `main` only while the current source still owns the same version, tag, draft, and candidate assets. Inspect their exact source/run identities first: an authentication repair alone does not authorize publishing an old candidate after `main` advances. When source advances beyond a reserved candidate, use the next unused stable version and fresh qualification; never move its tag, overwrite its draft/assets, or relabel old proof. The [0.5.1 changelog](../CHANGELOG.md#051-unpublished-source-history) is unpublished source history carried into 0.5.2, not a successful publication record. Set repository variable `NPM_RELEASE_ENABLED` to anything other than `true` to stop new release plans; cancel pending runs separately when needed. Workflow validation is not evidence of a completed real OIDC publication.
49
+ Run these maintainer commands from the repository root.
32
50
 
33
- Publication records are independent: on 2026-10-07, npm listed 0.5.2 as published and `latest`, while GitHub's `v0.5.2` release remained a draft bound to source `9820ae7`. Preserve that tag, draft, and candidate assets unchanged. The 0.5.3 source uses a new version and candidate; its preparation is not proof of npm or GitHub publication.
51
+ `scripts/release-review.mjs` performs independent, read-only Pi reviews with the actual thermo-nuclear skill and its complete rubric. It requires the current Pi CLI, `ZAI_API_KEY`, and `PI_RELEASE_REVIEW_SKILL` pointing to that skill's `SKILL.md`. Run each review in a separate process without GitHub, npm publishing, OIDC, App, or Cursor credentials; Pi uses an isolated home and receives only its model credential. The receipt records the reviewed Git identities, skill/rubric hashes, and verdict; sibling files retain redacted Pi events and copies of the review inputs.
52
+
53
+ ```bash
54
+ node scripts/release-review.mjs pre-commit --receipt /tmp/pre-commit-review.json
55
+ node scripts/release-review.mjs pre-push --base "$OUTGOING_BASE_SHA" --receipt /tmp/pre-push-review.json
56
+ ```
57
+
58
+ Pre-commit reviews `HEAD` against the complete staged tree and rejects unstaged tracked changes. Pre-push reviews the outgoing commits and tree against the exact remote branch tip, or the captured `main` tip for a new branch; pass its full SHA as `OUTGOING_BASE_SHA`. Pre-push requires a clean tracked checkout and index. Both reject incomplete reviews or file reads, material findings, or a tree changed during review. Page through long files until the read tool reports no continuation. The default direct Z.ai reviewer is `glm-5.3-flash`, which can inspect images; an override via `PI_RELEASE_REVIEW_MODEL` must also support images when the reviewed changes contain them. Minor polish is recorded; release qualification requires zero findings, including polish. When `PI_RELEASE_REVIEW_AUTOMATION_VERSIONER` names the shared versioner, the receipt binds and retains that exact file as review context.
59
+
60
+ To supply retained verification evidence or prior finding dispositions, add `--review-context PATH` to a pre-commit, pre-push, or release review. The reviewer must read the whole file and assess its claims independently. The receipt retains its contents and SHA-256, and the command rejects changes to that file during review. Release reviews retain prepared qualification evidence alongside optional user context and require both files to be read and remain unchanged. Supplied context does not replace required source reads or qualification checks.
61
+
62
+ For a release candidate, `catalog --receipt PATH` runs the authenticated catalog check on a clean tracked checkout using `CURSOR_API_KEY` and binds its receipt and output to the current source, GitHub run, and attempt. For local staged changes, `catalog --staged` instead binds the staged tree; unstaged tracked changes are still rejected. In a separate step, `release-prepare --source SHA --release-source SHA --artifact DIR --catalog-receipt PATH --evidence DIR` collects the exact previous published tag-to-source diff, previous npm package and registry receipt, candidate artifacts, release notes, and associated PR/issue closure context. For a prospective release PR, add `--pull NUMBER` to include its current PR, review threads, and linked issue context; preparation rejects a PR whose head does not match the candidate source. The evidence directory must not already exist. Preparation requires `GITHUB_REPOSITORY`, `GITHUB_RUN_ID`, `GITHUB_RUN_ATTEMPT`, and a read-only `GH_TOKEN`.
63
+
64
+ Run `release --source SHA --release-source SHA --evidence DIR --receipt PATH` in another process with the model key but no GitHub or Cursor credentials. It must retain the same `GITHUB_RUN_ID` and `GITHUB_RUN_ATTEMPT` as preparation, such as in a separate step of that GitHub Actions run. It revalidates source, run, catalog, package bytes and extracted trees before and after the review. Release qualification requires every finding, including polish, to be resolved. A successful receipt is evidence of the review and catalog scope; it does not replace the other release checks or publish anything.
65
+
66
+ A release reviewer must open every changed package path. A complete changed-source read can also cover a byte-identical package copy. The receipt verifies the SHA-256 match and records both paths and trees in `readEvidence.artifactSourceReads`. Different package content still requires a complete read.
34
67
 
35
68
  ## Development
36
69
 
37
- Required qualification targets are the latest stable official Pi and latest maintained `fitchmultz/pi` main, with optional wildcard Pi/TypeBox host peers and exact Cursor SDK **1.0.37**. CI resolves the official version/fork commit once and retains exact SDK/CLI provenance. Types, tests and package checks use each selected host's consistent dependency graph; the locked development Pi **1.1.0** dependencies are reproducible snapshots, not qualification targets. Updating these snapshots does not upgrade the user's Pi installation or change the official Pi 0.87.1 minimum. After `npm ci --ignore-scripts`, run `node scripts/ci-compatibility.mjs --automation /path/to/automation` in an empty HOME/agent profile for credential-free latest qualification. Native macOS/Windows checks select the same resolved latest official graph. This builds the manifest entry, runs existing type/unit/package checks, and tests the compiled provider's registration plus production prompt shaping against native Pi transcript/tool transitions. Replay arguments must match their JSON stream deltas. A second native contract runs the registered Cursor provider with only its external SDK transport/storage substituted: real loopback bridge execution, display-only replay, persisted usage, incremental sends, tree navigation, compaction, request-boundary steering, abort and reload/disposal. Tests use no live Cursor service.
70
+ Required qualification targets are the latest stable official Pi and latest maintained `fitchmultz/pi` main, with optional wildcard Pi/TypeBox host peers and exact Cursor SDK **1.0.40**. CI resolves the official version/fork commit once and retains exact SDK/CLI provenance. Types, tests and package checks use each selected host's consistent dependency graph; the locked development Pi **1.1.0** dependencies are reproducible snapshots, not qualification targets. Updating these snapshots does not upgrade the user's Pi installation or change the official Pi 0.87.1 minimum. After `npm ci --ignore-scripts`, run `node scripts/ci-compatibility.mjs --automation /path/to/automation` in an empty HOME/agent profile for credential-free latest qualification. Native macOS/Windows checks select the same resolved latest official graph. This builds the manifest entry, runs existing type/unit/package checks, and tests the compiled provider's registration plus production prompt shaping against native Pi transcript/tool transitions. Replay arguments must match their JSON stream deltas. A second native contract runs the registered Cursor provider with only its external SDK transport/storage substituted: real loopback bridge execution, display-only replay, persisted usage, incremental sends, tree navigation, compaction, request-boundary steering, abort and reload/disposal. Tests use no live Cursor service.
71
+
72
+ The frozen development `pi-coding-agent@1.1.0` input declares Undici 8.10.2. A version-scoped npm override selects released Undici 8.12.3 for that owner only, addressing its development audit findings without changing the Pi cohort or SDK transport. Dependency overrides apply only at the install root; this does not patch consumers' independently installed Pi hosts. Reassess the override when updating the development cohort.
38
73
 
39
74
  TypeScript 7 builds and checks package types. Build and typecheck commands invoke `node_modules/typescript/bin/tsc` explicitly to avoid npm's shared `tsc` shim selecting a different compiler. `@typescript/typescript6` is dev-only for AST architecture and installed-SDK contract tests because TypeScript 7 does not expose a stable compiler API. Vitest 5 runs with `clearMocks: true` by default.
40
75
 
package/docs/reference.md CHANGED
@@ -53,7 +53,7 @@ If pi started without a key, run `/cursor-refresh-models` after `/login` to refr
53
53
  - optional Pi and TypeBox peer metadata uses `"*"` ranges per Pi package guidance
54
54
  - a Cursor SDK API key saved through `/login`, available as `CURSOR_API_KEY`, or passed with pi's `--api-key`
55
55
 
56
- No global `@cursor/sdk` install is required. This package depends on exact `@cursor/sdk@1.0.37`, so normal package installation brings in that SDK. The extension intentionally does not bundle `@cursor/sdk` or its platform packages, because packing from one maintainer OS can ship the wrong optional SDK binary for another OS. The SDK has used its current single-file/new-chunk package layout since 1.0.28. Pi hosts that pass transcript-only provider contexts are supported through Pi 0.87.1's required public replay helpers.
56
+ No global `@cursor/sdk` install is required. This package depends on exact `@cursor/sdk@1.0.40`, so normal package installation brings in that SDK. Its required Zod peer is supplied directly at patched version `4.6.6`, including for installers that do not auto-install peers. The bundled MCP bridge closure retains its own newer nested Zod `4.6.7` copies. This intentional locked layout keeps the SDK peer unbundled and reachable under Pi's managed npm installation; see [packaging verification](development.md#zod-peer-and-bridge-packaging). The extension intentionally does not bundle `@cursor/sdk` or its platform packages, because packing from one maintainer OS can ship the wrong optional SDK binary for another OS. The SDK has used its current single-file/new-chunk package layout since 1.0.28. Pi hosts that pass transcript-only provider contexts are supported through Pi 0.87.1's required public replay helpers.
57
57
 
58
58
  ## Install
59
59
 
@@ -544,7 +544,7 @@ Local Cursor runs use two separate tool surfaces (the [MCP-only opt-out](./curso
544
544
  - **Cursor-native surface:** Cursor local-agent tools, Cursor settings, plugins, and configured Cursor MCP servers. These remain owned by the Cursor SDK local agent path. Pi CLI tool toggles such as `--no-tools`, `--tools`, and `--exclude-tools` do not disable this Cursor-native surface.
545
545
  - **pi bridge surface:** pi-cursor-sdk exposes bridgeable active pi tools through a per-run local loopback MCP bridge when the bridge is enabled and the current pi tool registry has exposed tools. Pi CLI tool toggles affect this bridge surface because they change pi's active tool registry.
546
546
 
547
- The bridge uses stable MCP v2 with `@modelcontextprotocol/server@2.3.1`, `@modelcontextprotocol/hono@2.0.2`, `hono@4.13.13`, and `@hono/node-server@2.1.3`, bundled as its runtime closure. It binds only to loopback and validates Hono `Host` and `Origin` headers.
547
+ The bridge uses stable MCP v2 with `@modelcontextprotocol/server@2.3.1`, `@modelcontextprotocol/hono@2.0.2`, `hono@4.13.13`, and `@hono/node-server@2.1.3`, bundled as its runtime closure with their Zod `4.6.7` dependency. It binds only to loopback and validates Hono `Host` and `Origin` headers.
548
548
 
549
549
  Bridge capabilities are snapshotted from `pi.getActiveTools()` and `pi.getAllTools()` for each Cursor run, including per-tool prompt guidelines when pi exposes them. Cursor sees active bridgeable pi tools as collision-safe MCP names such as `pi__sem_reindex` only when they are exposed in that current run. Cursor is instructed to prefer exposed `pi__*` tools for their described capabilities, using the current schemas and prompt guidelines rather than hardcoded delegation/MCP names; Cursor-configured MCP and Cursor-native subagents are fallbacks when no matching exposed Pi tool is available. Pi session output, tool cards, confirmations, hooks, renderers, history, and abort behavior use the real pi tool name, such as `sem_reindex`. The bridge queues Cursor's MCP call, emits a normal pi `toolCall`, waits for the matching pi `toolResult`, and resolves that result back into the same live Cursor SDK run without creating a new `Agent`, unless the run was disposed, aborted, or cancelled. The bridge does not call pi tool `execute()` handlers directly.
550
550
 
@@ -552,7 +552,7 @@ Overlapping built-in pi tools (`read`, `bash`, `write`, `edit`, `grep`, `find`,
552
552
 
553
553
  Cursor-native tool replay is separate from the bridge. Replay cards are display-only recorded Cursor SDK activity. They never re-run Cursor-side commands, reapply Cursor edits, call MCP servers, or mutate pi state. See [Cursor native tool replay](./cursor-native-tool-replay.md).
554
554
 
555
- **Availability:** `PI_CURSOR_NATIVE_TOOLS` and `/cursor-abort` require **0.5.4 or later**, including [#372](https://github.com/fitchmultz/pi-cursor-sdk/pull/372). Source checkouts preparing 0.5.4 remain candidates until exact publication records exist; the version bump is not proof of publication. Verify exact npm/GitHub publication records and the installed artifact before relying on availability. Published **0.5.3 ignores the flag and does not register the command**. Development version labels alone do not prove that a control exists.
555
+ **Availability:** `PI_CURSOR_NATIVE_TOOLS` and `/cursor-abort` require **0.5.4 or later**, including [#372](https://github.com/fitchmultz/pi-cursor-sdk/pull/372). Published **0.5.3 ignores the flag and does not register the command**. Verify the installed version before relying on either control; a development version label alone does not prove availability.
556
556
 
557
557
  Pi `tool_call` hooks cannot govern native Cursor execution: replay arrives after the original work. Hiding replay or disabling the bridge does not disable native tools. On a supporting revision, `PI_CURSOR_NATIVE_TOOLS=0` instead selects public SDK `tools: ["mcp"]` for ordinary LOCAL create and resume, excluding every other main tool including native task/subagents. Only the opted-out policy changes pooled/durable/resume identity; defaults remain unchanged. It is captured before acquisition; a pending opposite-policy run rejects continuation before consuming/delivering Pi results. Restore its prior policy to finish it, or run `/cursor-abort` to explicitly release that session's pending local run before retrying; ordinary mismatch never cancels it automatically. Pi's normal abort only signals an active Pi turn, so an idle abort after the error has settled cannot reach the old Cursor waiter. A real active provider abort, including a signal already aborted before preparation, releases its exact scoped pending run. The command sends no model turn; when its owned pending run has an active Cursor Pi turn, it also calls that turn's public abort before release. It does not abort a non-Cursor Pi turn, touch sibling sessions or Cloud agents, or undo already-executed work. Cloud rejects the opt-out before SDK/lifecycle side effects; isolated text-only LOCAL summaries keep `tools: []`. The flag uses the canonical boolean parser (off: `0`, `false`, `off`, `none`, `no`, `disabled`; on: `1`, `true`, `on`, `yes`, `enabled`; trim/case-insensitive; unknown/blank/unset means default on). MCP-only retains ambient Cursor MCP unless setting sources are disabled; it is not a sandbox or OS security boundary, and a permitted Pi/MCP tool can itself execute arbitrary work.
558
558
 
@@ -628,13 +628,15 @@ These server observations do not identify the client as Cursor or prove ongoing
628
628
 
629
629
  If no matching cached catalog is available in a cache-only phase, auth is missing, or live discovery fails or returns no models, the extension retains known bundled fallback identities and notifies interactive users when possible. By default these models remain available without auth. With `PI_CURSOR_HIDE_MODELS_WHEN_LOGGED_OUT=1`, unauthenticated picker/list availability is empty, but known CLI selection and native login remain functional. Pi owns `--api-key` parsing.
630
630
 
631
- The fallback snapshot includes Grok 4.6, Composer 2.5 (`composer-2.5` and `composer-2-5`), Composer 2, Cursor's GPT-5.6 Luna/Sol/Terra models, Claude, Gemini, Grok 4.5, Kimi, and other model IDs exposed by the reviewed `Cursor.models.list()` output. Recommended local/smoke runs use `cursor/grok-4.6`. Pi's separate `openai-codex` catalog is owned by Pi itself and includes native `gpt-5.6-luna`, `gpt-5.6-sol`, and `gpt-5.6-terra` support. The exact checked-in Cursor snapshot lives in `src/cursor-fallback-models.generated.ts`; its generator provenance records SDK 1.0.37 and 48 models from the authenticated catalog-only refresh on 2026-10-22. That refresh added Gemini 4 Argon (`gemini-4-argon`) and Muse Wonder (`muse-wonder-1.0`), preserving all 46 prior model records. The 33 bundled checkpoint-derived entries remain unchanged; no Agent/model turn or Cloud run was performed, and no new checkpoint-derived context limit is established. A dated maintainer capture documents the assistant-visible [Cursor system prompts and tool guidance](https://github.com/fitchmultz/pi-cursor-sdk/blob/main/docs/evidence/cursor-system-prompts-2026-08-02/README.md) for Grok 4.5, Opus 5, Fable 5, and the GPT-5.6 Sol/Terra/Luna family.
631
+ The fallback snapshot includes Grok 4.6, Composer 2.5 (`composer-2.5` and `composer-2-5`), Composer 2, Cursor's GPT-5.6 Luna/Sol/Terra models, Claude, Gemini, Grok 4.5, Kimi, and other model IDs exposed by the reviewed `Cursor.models.list()` output. Recommended local/smoke runs use `cursor/grok-4.6`. Pi's separate `openai-codex` catalog is owned by Pi itself and includes native `gpt-5.6-luna`, `gpt-5.6-sol`, and `gpt-5.6-terra` support. The exact checked-in Cursor snapshot lives in `src/cursor-fallback-models.generated.ts`; its generator provenance records SDK 1.0.40 and 49 models from the authenticated catalog-only refresh on 2026-10-31. That refresh added DeepSeek V4.1 Flash 1017 (`deepseek-v41-flash-1017`), retained the prior 48 model IDs, and recorded `cyber=false` on Claude Opus 5.5 variants. The 33 bundled checkpoint-derived entries remain unchanged; no Agent/model turn or Cloud run was performed, and no new checkpoint-derived context limit is established. A dated maintainer capture documents the assistant-visible [Cursor system prompts and tool guidance](https://github.com/fitchmultz/pi-cursor-sdk/blob/main/docs/evidence/cursor-system-prompts-2026-08-02/README.md) for Grok 4.5, Opus 5, Fable 5, and the GPT-5.6 Sol/Terra/Luna family.
632
632
 
633
633
  Actual Cursor runs still need a key from `/login`, `CURSOR_API_KEY`, or `--api-key`. If you add auth after startup, run `/cursor-refresh-models` to refresh the full live Cursor model catalog without restarting pi.
634
634
 
635
635
  ## Limits
636
636
 
637
- - **Commit/PR attribution has no supported opt-out.** SDK 1.0.37 exposes neither attribution control through public create/local/send/configure options. CLI-file edits and SDK-internal patches are not supported extension settings; [#302](https://github.com/fitchmultz/pi-cursor-sdk/issues/302) tracks both controls.
637
+ - **SDK hook compatibility is not blanket enforcement.** SDK 1.0.40 enables nested Claude-format denial/rewrite translation in its LOCAL hook runtime and improves Windows hook input/exit handling. The installed-package contracts exercise the settings/command-hook boundary, not a public Agent/Pi/rtk run; original [#173](https://github.com/fitchmultz/pi-cursor-sdk/issues/173) remains unqualified. Pi hooks still do not intercept native SDK execution. See [the bounded hook contract](./cursor-testing-lessons.md#sdk-1040-hook-maintenance-boundary).
638
+ - **Zod's bounded-collection fix is not a universal resource cap.** The SDK resolves `zod`, `zod/v3`, `zod/v4` and `zod/v4-mini` from its direct `4.6.6` peer; the bridge uses nested `4.6.7` copies. Both versions' v4 parsers check array, set and map size bounds before parsing elements ([GHSA-mrhx-c7xc-m3w5](https://github.com/advisories/GHSA-mrhx-c7xc-m3w5)). The legacy v3 implementation is unchanged, and unbounded schemas still accumulate element errors. No active SDK exploit is established. Deduplicating the SDK peer into the bridge bundle breaks Pi's managed npm installation, so [packaging checks](development.md#zod-peer-and-bridge-packaging) remain necessary even when the audit is clean.
639
+ - **Commit/PR attribution has no supported opt-out.** SDK 1.0.40 exposes neither attribution control through public create/local/send/configure options. CLI-file edits and SDK-internal patches are not supported extension settings; [#302](https://github.com/fitchmultz/pi-cursor-sdk/issues/302) tracks both controls.
638
640
 
639
641
  - **Cloud runtime is explicit and minimal.** Local remains the default. Cloud runs create Cursor cloud agents only after first-use acknowledgement and safety preflight, use fresh context by default, do not expose the pi bridge or local MCP, do not forward pi env vars, support explicit Cursor-managed environment selection, name agents from the pi session title when available, stream display-only agent/run/branch/PR/artifact/raw-usage telemetry when available, and record only explicit session-branch lifecycle commands for cleanup (`/cursor-cloud list|archive|delete`).
640
642
  - **The pi tool bridge is local and MCP-backed.** Bridgeable active pi tools are exposed to local Cursor agents through a tokenized `127.0.0.1` MCP endpoint (the URL host can be set to `localhost` with `PI_CURSOR_PI_TOOL_BRIDGE_HOST`); internal Cursor replay activity names are excluded, and overlapping built-in pi tools are hidden by default. Set `PI_CURSOR_PI_TOOL_BRIDGE=0` to disable it or `PI_CURSOR_EXPOSE_BUILTIN_TOOLS=1` to expose overlapping built-ins too.
@@ -656,6 +658,8 @@ You may be seeing fallback startup models or a missing/invalid Cursor SDK API ke
656
658
 
657
659
  When a Cursor run fails after auth is configured, pi surfaces scrubbed provider detail instead of only `Cursor SDK run failed`. Structured errors retain bounded name, code, message, and up to two nested causes; headers and other object fields are not displayed. Generic completed-run failures include safe run metadata such as model id, a short run id prefix, and duration when available, and are phrased as pi retryable provider errors so automatic retry/backoff can recover transient SDK failures. Unknown startup failures, module-loading errors, and ambiguous session authentication errors are not diagnosed as invalid API keys. Explicit missing, invalid, or revoked API-key errors still show key setup guidance.
658
660
 
661
+ When a LOCAL run fails, the provider cancels it while the Cursor SDK records the failure, so shell commands and other native tool processes started by that run stop instead of running until their own timeout. SDK 1.0.40 cancels native tools only while a run is still running, and it records the failure before marking the run failed. Pi still reports the SDK's failure message as an error, not a cancellation. When the SDK attaches a separate error code, that code is not available after cancellation and is omitted. An explicit Pi abort is still reported as a cancellation. Cloud runs are unchanged.
662
+
659
663
  Aborted runs now include a likely cause when determinable, for example `Cancelled: prompt interrupted.` for user cancel or `Cancelled: Cursor SDK run was cancelled.` for SDK-side cancellation.
660
664
 
661
665
  SDK 1.0.37 source preserves SDK 1.0.36's client bearer refresh and unauthenticated handling, but those mechanisms do not establish the cause of [#247](https://github.com/fitchmultz/pi-cursor-sdk/issues/247)'s historical idle failure. Reloading filesystem config is not a public auth-reset API. No guessed idle TTL or extension-level automatic resend is added: resending can duplicate executed tool effects.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod",
3
- "version": "4.6.5",
3
+ "version": "4.6.7",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -140,6 +140,33 @@ test("continue parsing despite array size error", () => {
140
140
  `);
141
141
  });
142
142
 
143
+ test("an upper size bound fails before any element is parsed", () => {
144
+ let parsed = 0;
145
+ const el = z.string().refine(() => ++parsed);
146
+ let refined = 0;
147
+ const schema = z
148
+ .array(el)
149
+ .max(2, "too many")
150
+ .refine(() => ++refined);
151
+ expect(schema.safeParse([1, 2, 3]).error!.issues).toMatchObject([{ code: "too_big", path: [], message: "too many" }]);
152
+ expect(z.array(el).length(2).safeParse(["a", "b", "c"]).error!.issues).toMatchObject([
153
+ { code: "too_big", exact: true },
154
+ ]);
155
+ expect(z.object({ a: z.array(el).max(1) }).safeParse({ a: [1, 2] }).error!.issues).toMatchObject([
156
+ { code: "too_big", path: ["a"] },
157
+ ]);
158
+ expect([parsed, refined]).toEqual([0, 0]);
159
+
160
+ // a check that may rewrite the value ends the gate
161
+ expect(
162
+ z
163
+ .array(z.number())
164
+ .overwrite((a) => a.slice(0, 2))
165
+ .max(2)
166
+ .parse([1, 2, 3])
167
+ ).toEqual([1, 2]);
168
+ });
169
+
143
170
  test("parse should fail given sparse array", () => {
144
171
  const schema = z.array(z.string()).nonempty().min(1).max(3);
145
172
  const result = schema.safeParse(new Array(3));
@@ -97,6 +97,13 @@ test("failing when map is bigger than max() ", () => {
97
97
  expect(result.success).toEqual(false);
98
98
  expect(result.error!.issues.length).toEqual(1);
99
99
  expect(result.error!.issues[0].code).toEqual("too_big");
100
+ const bad = new Map([
101
+ [1, 1],
102
+ [2, 2],
103
+ ]);
104
+ const capped = z.map(z.string(), z.string()).max(1);
105
+ expect(capped.safeParse(bad).error!.issues).toMatchObject([{ code: "too_big" }]);
106
+ expect(capped.safeParse(new Set([1, 2])).error!.issues[0]).toMatchObject({ code: "invalid_type", expected: "map" });
100
107
  });
101
108
 
102
109
  test("failing when map does not match size() ", () => {
@@ -83,6 +83,12 @@ test("failing when set is bigger than max() ", () => {
83
83
  expect(result.success).toEqual(false);
84
84
  expect(result.error!.issues.length).toEqual(1);
85
85
  expect(result.error!.issues[0].code).toEqual("too_big");
86
+ expect(
87
+ z
88
+ .set(z.string())
89
+ .max(1)
90
+ .safeParse(new Set([1, 2])).error!.issues
91
+ ).toMatchObject([{ code: "too_big" }]);
86
92
  });
87
93
 
88
94
  test("failing when set does not match size() ", () => {
@@ -1551,6 +1551,12 @@ function generateNullableCheck(
1551
1551
  return outputVar;
1552
1552
  }
1553
1553
 
1554
+ function generateSizeGate(doc: Doc, schema: SomeType, size: string): void {
1555
+ for (const [max, check] of util.sizeCaps(schema._zod.def.checks)) {
1556
+ doc.write(`if (${size} > ${numericOperand(max, check._zod.def.check)}) return INVALID;`);
1557
+ }
1558
+ }
1559
+
1554
1560
  function generateArrayCheck(
1555
1561
  doc: Doc,
1556
1562
  ctx: CompileContext,
@@ -1560,6 +1566,7 @@ function generateArrayCheck(
1560
1566
  ): string | null {
1561
1567
  const def = schema._zod.def as unknown as { element: SomeType };
1562
1568
  doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
1569
+ generateSizeGate(doc, schema, `${accessor}.length`);
1563
1570
 
1564
1571
  // Build a new array with validated/transformed elements.
1565
1572
  const outputVar = buildsValue ? newVar(ctx) : null;
@@ -2094,6 +2101,7 @@ function generateMapCheck(doc: Doc, ctx: CompileContext, schema: SomeType, acces
2094
2101
  const def = schema._zod.def as unknown as { keyType: SomeType; valueType: SomeType };
2095
2102
 
2096
2103
  doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
2104
+ generateSizeGate(doc, schema, `${accessor}.size`);
2097
2105
 
2098
2106
  const outputVar = newVar(ctx);
2099
2107
  const kVar = newVar(ctx);
@@ -2115,6 +2123,7 @@ function generateSetCheck(doc: Doc, ctx: CompileContext, schema: SomeType, acces
2115
2123
  const def = schema._zod.def as unknown as { valueType: SomeType };
2116
2124
 
2117
2125
  doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
2126
+ generateSizeGate(doc, schema, `${accessor}.size`);
2118
2127
 
2119
2128
  const outputVar = newVar(ctx);
2120
2129
  const valVar = newVar(ctx);
@@ -1844,6 +1844,34 @@ export interface $ZodArrayInternals<T extends SomeType = $ZodType> extends _$Zod
1844
1844
 
1845
1845
  export interface $ZodArray<T extends SomeType = $ZodType> extends $ZodType<any, any, $ZodArrayInternals<T>> {}
1846
1846
 
1847
+ // an oversized container fails its leading size checks before any element is parsed
1848
+ function capSize(inst: $ZodType, size: (input: any) => number): void {
1849
+ const caps = util.sizeCaps(inst._zod.def.checks);
1850
+ if (!caps.length) return;
1851
+ const cap = Math.min(...caps.map((c) => c[0]));
1852
+ const parse = inst._zod.parse;
1853
+ inst._zod.parse = (payload, ctx) => {
1854
+ const n = size(payload.value);
1855
+ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
1856
+ for (const [max, ch] of util.sizeCaps(inst._zod.def.checks)) {
1857
+ if (n > max && ch._zod.def.when!(payload)) {
1858
+ const start = payload.issues.length;
1859
+ ch._zod.check(payload as ParsePayload<never>);
1860
+ // the value is still the raw input, so no later check may read it
1861
+ (payload.issues[start] as { continue?: boolean }).continue = false;
1862
+ util.attachSchema(payload.issues, start, inst);
1863
+ return payload;
1864
+ }
1865
+ }
1866
+ }
1867
+ return parse(payload, ctx);
1868
+ };
1869
+ }
1870
+
1871
+ const arraySize = (input: unknown): number => (Array.isArray(input) ? input.length : -1);
1872
+ const mapSize = (input: unknown): number => (input instanceof Map ? input.size : -1);
1873
+ const setSize = (input: unknown): number => (input instanceof Set ? input.size : -1);
1874
+
1847
1875
  function handleArrayResult(result: ParsePayload<any>, final: ParsePayload<any[]>, index: number) {
1848
1876
  if (result.issues.length) {
1849
1877
  final.issues.push(...util.prefixIssues(index, result.issues));
@@ -1899,6 +1927,7 @@ export const $ZodArray: core.$constructor<$ZodArray> = /*@__PURE__*/ core.$const
1899
1927
 
1900
1928
  return payload; //handleArrayResultsAsync(parseResults, final);
1901
1929
  };
1930
+ capSize(inst, arraySize);
1902
1931
  });
1903
1932
 
1904
1933
  //////////////////////////////////////////
@@ -3218,8 +3247,11 @@ export type $InferZodRecordInput<
3218
3247
  : Record<core.input<Key> & PropertyKey, core.input<Value>>;
3219
3248
 
3220
3249
  export interface $ZodRecordInternals<Key extends $ZodRecordKey = $ZodRecordKey, Value extends SomeType = $ZodType>
3221
- extends $ZodTypeInternals<$InferZodRecordOutput<Key, Value>, $InferZodRecordInput<Key, Value>> {
3250
+ extends $ZodTypeInternals {
3222
3251
  def: $ZodRecordDef<Key, Value>;
3252
+ // members rather than base type arguments, which TypeScript 7.1 reports as circular for `z.json()`
3253
+ output: $InferZodRecordOutput<Key, Value>;
3254
+ input: $InferZodRecordInput<Key, Value>;
3223
3255
  isst: errors.$ZodIssueInvalidType | errors.$ZodIssueInvalidKey<Record<PropertyKey, unknown>>;
3224
3256
  optin?: "optional" | undefined;
3225
3257
  optout?: "optional" | undefined;
@@ -3480,6 +3512,7 @@ export const $ZodMap: core.$constructor<$ZodMap> = /*@__PURE__*/ core.$construct
3480
3512
  if (proms.length) return Promise.all(proms).then(() => payload);
3481
3513
  return payload;
3482
3514
  };
3515
+ capSize(inst, mapSize);
3483
3516
  });
3484
3517
 
3485
3518
  function handleMapResult(
@@ -3582,6 +3615,7 @@ export const $ZodSet: core.$constructor<$ZodSet> = /*@__PURE__*/ core.$construct
3582
3615
  if (proms.length) return Promise.all(proms).then(() => payload);
3583
3616
  return payload;
3584
3617
  };
3618
+ capSize(inst, setSize);
3585
3619
  });
3586
3620
 
3587
3621
  function handleSetResult(result: ParsePayload, final: ParsePayload<Set<any>>) {
@@ -935,6 +935,19 @@ export function unwrapMessage(message: string | { message: string } | undefined
935
935
  }
936
936
 
937
937
  /* A check holds no link back to the schema it is attached to — the same check instance is shared by every clone of that schema — so the owner is stamped onto the issues a check just raised, at the only point where both are in scope. Runs on the failure path only; `start` is the issue count from before the check ran. */
938
+ /** The leading size checks of `defChecks` that set an upper bound, each paired with it. */
939
+ export function sizeCaps(defChecks: checks.$ZodCheck<never>[] | undefined): [number, checks.$ZodCheck<never>][] {
940
+ const caps: [number, checks.$ZodCheck<never>][] = [];
941
+ for (const ch of defChecks ?? []) {
942
+ const d = ch._zod.def as any;
943
+ // a later check may rewrite the value
944
+ if (!/_(length|size)$|_equals$/.test(d.check)) break;
945
+ const max = d.maximum ?? d.length ?? d.size;
946
+ if (max >= 0) caps.push([max, ch]);
947
+ }
948
+ return caps;
949
+ }
950
+
938
951
  export function attachSchema(issues: errors.$ZodRawIssue[], start: number, inst: schemas.$ZodType): void {
939
952
  for (let i = start; i < issues.length; i++) {
940
953
  (issues[i] as any).schema ??= inst;
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 6,
4
- patch: 5 as number,
4
+ patch: 7 as number,
5
5
  } as const;
@@ -1241,9 +1241,15 @@ function generateNullableCheck(doc, ctx, schema, accessor, buildsValue = true) {
1241
1241
  doc.write(`}`);
1242
1242
  return outputVar;
1243
1243
  }
1244
+ function generateSizeGate(doc, schema, size) {
1245
+ for (const [max, check] of util.sizeCaps(schema._zod.def.checks)) {
1246
+ doc.write(`if (${size} > ${numericOperand(max, check._zod.def.check)}) return INVALID;`);
1247
+ }
1248
+ }
1244
1249
  function generateArrayCheck(doc, ctx, schema, accessor, buildsValue = true) {
1245
1250
  const def = schema._zod.def;
1246
1251
  doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
1252
+ generateSizeGate(doc, schema, `${accessor}.length`);
1247
1253
  // Build a new array with validated/transformed elements.
1248
1254
  const outputVar = buildsValue ? newVar(ctx) : null;
1249
1255
  const iVar = newVar(ctx);
@@ -1707,6 +1713,7 @@ function literalPropertyKey(ctx, key) {
1707
1713
  function generateMapCheck(doc, ctx, schema, accessor) {
1708
1714
  const def = schema._zod.def;
1709
1715
  doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
1716
+ generateSizeGate(doc, schema, `${accessor}.size`);
1710
1717
  const outputVar = newVar(ctx);
1711
1718
  const kVar = newVar(ctx);
1712
1719
  const valVar = newVar(ctx);
@@ -1723,6 +1730,7 @@ function generateMapCheck(doc, ctx, schema, accessor) {
1723
1730
  function generateSetCheck(doc, ctx, schema, accessor) {
1724
1731
  const def = schema._zod.def;
1725
1732
  doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
1733
+ generateSizeGate(doc, schema, `${accessor}.size`);
1726
1734
  const outputVar = newVar(ctx);
1727
1735
  const valVar = newVar(ctx);
1728
1736
  doc.write(`const ${outputVar} = new Set();`);
@@ -1210,9 +1210,15 @@ function generateNullableCheck(doc, ctx, schema, accessor, buildsValue = true) {
1210
1210
  doc.write(`}`);
1211
1211
  return outputVar;
1212
1212
  }
1213
+ function generateSizeGate(doc, schema, size) {
1214
+ for (const [max, check] of util.sizeCaps(schema._zod.def.checks)) {
1215
+ doc.write(`if (${size} > ${numericOperand(max, check._zod.def.check)}) return INVALID;`);
1216
+ }
1217
+ }
1213
1218
  function generateArrayCheck(doc, ctx, schema, accessor, buildsValue = true) {
1214
1219
  const def = schema._zod.def;
1215
1220
  doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
1221
+ generateSizeGate(doc, schema, `${accessor}.length`);
1216
1222
  // Build a new array with validated/transformed elements.
1217
1223
  const outputVar = buildsValue ? newVar(ctx) : null;
1218
1224
  const iVar = newVar(ctx);
@@ -1676,6 +1682,7 @@ function literalPropertyKey(ctx, key) {
1676
1682
  function generateMapCheck(doc, ctx, schema, accessor) {
1677
1683
  const def = schema._zod.def;
1678
1684
  doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
1685
+ generateSizeGate(doc, schema, `${accessor}.size`);
1679
1686
  const outputVar = newVar(ctx);
1680
1687
  const kVar = newVar(ctx);
1681
1688
  const valVar = newVar(ctx);
@@ -1692,6 +1699,7 @@ function generateMapCheck(doc, ctx, schema, accessor) {
1692
1699
  function generateSetCheck(doc, ctx, schema, accessor) {
1693
1700
  const def = schema._zod.def;
1694
1701
  doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
1702
+ generateSizeGate(doc, schema, `${accessor}.size`);
1695
1703
  const outputVar = newVar(ctx);
1696
1704
  const valVar = newVar(ctx);
1697
1705
  doc.write(`const ${outputVar} = new Set();`);
@@ -860,6 +860,33 @@ exports.$ZodDate = core.$constructor("$ZodDate", (inst, def) => {
860
860
  return payload;
861
861
  };
862
862
  });
863
+ // an oversized container fails its leading size checks before any element is parsed
864
+ function capSize(inst, size) {
865
+ const caps = util.sizeCaps(inst._zod.def.checks);
866
+ if (!caps.length)
867
+ return;
868
+ const cap = Math.min(...caps.map((c) => c[0]));
869
+ const parse = inst._zod.parse;
870
+ inst._zod.parse = (payload, ctx) => {
871
+ const n = size(payload.value);
872
+ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
873
+ for (const [max, ch] of util.sizeCaps(inst._zod.def.checks)) {
874
+ if (n > max && ch._zod.def.when(payload)) {
875
+ const start = payload.issues.length;
876
+ ch._zod.check(payload);
877
+ // the value is still the raw input, so no later check may read it
878
+ payload.issues[start].continue = false;
879
+ util.attachSchema(payload.issues, start, inst);
880
+ return payload;
881
+ }
882
+ }
883
+ }
884
+ return parse(payload, ctx);
885
+ };
886
+ }
887
+ const arraySize = (input) => (Array.isArray(input) ? input.length : -1);
888
+ const mapSize = (input) => (input instanceof Map ? input.size : -1);
889
+ const setSize = (input) => (input instanceof Set ? input.size : -1);
863
890
  function handleArrayResult(result, final, index) {
864
891
  if (result.issues.length) {
865
892
  final.issues.push(...util.prefixIssues(index, result.issues));
@@ -905,6 +932,7 @@ exports.$ZodArray = core.$constructor("$ZodArray", (inst, def) => {
905
932
  }
906
933
  return payload; //handleArrayResultsAsync(parseResults, final);
907
934
  };
935
+ capSize(inst, arraySize);
908
936
  });
909
937
  function handlePropertyResult(result, final, key, input, optin, optout) {
910
938
  const isPresent = key in input;
@@ -1942,6 +1970,7 @@ exports.$ZodMap = core.$constructor("$ZodMap", (inst, def) => {
1942
1970
  return Promise.all(proms).then(() => payload);
1943
1971
  return payload;
1944
1972
  };
1973
+ capSize(inst, mapSize);
1945
1974
  });
1946
1975
  function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
1947
1976
  if (keyResult.issues.length) {
@@ -2011,6 +2040,7 @@ exports.$ZodSet = core.$constructor("$ZodSet", (inst, def) => {
2011
2040
  return Promise.all(proms).then(() => payload);
2012
2041
  return payload;
2013
2042
  };
2043
+ capSize(inst, setSize);
2014
2044
  });
2015
2045
  function handleSetResult(result, final) {
2016
2046
  if (result.issues.length) {
@@ -834,8 +834,10 @@ export type $InferZodRecordOutput<Key extends $ZodRecordKey = $ZodRecordKey, Val
834
834
  export type $InferZodRecordInput<Key extends $ZodRecordKey = $ZodRecordKey, Value extends SomeType = $ZodType> = Key extends $partial ? Partial<Record<core.input<Key> & PropertyKey, core.input<Value>>> : [
835
835
  Value
836
836
  ] extends [OptionalInSchema] ? Partial<Record<core.input<Key> & PropertyKey, core.input<Value>>> : Record<core.input<Key> & PropertyKey, core.input<Value>>;
837
- export interface $ZodRecordInternals<Key extends $ZodRecordKey = $ZodRecordKey, Value extends SomeType = $ZodType> extends $ZodTypeInternals<$InferZodRecordOutput<Key, Value>, $InferZodRecordInput<Key, Value>> {
837
+ export interface $ZodRecordInternals<Key extends $ZodRecordKey = $ZodRecordKey, Value extends SomeType = $ZodType> extends $ZodTypeInternals {
838
838
  def: $ZodRecordDef<Key, Value>;
839
+ output: $InferZodRecordOutput<Key, Value>;
840
+ input: $InferZodRecordInput<Key, Value>;
839
841
  isst: errors.$ZodIssueInvalidType | errors.$ZodIssueInvalidKey<Record<PropertyKey, unknown>>;
840
842
  optin?: "optional" | undefined;
841
843
  optout?: "optional" | undefined;
@@ -834,8 +834,10 @@ export type $InferZodRecordOutput<Key extends $ZodRecordKey = $ZodRecordKey, Val
834
834
  export type $InferZodRecordInput<Key extends $ZodRecordKey = $ZodRecordKey, Value extends SomeType = $ZodType> = Key extends $partial ? Partial<Record<core.input<Key> & PropertyKey, core.input<Value>>> : [
835
835
  Value
836
836
  ] extends [OptionalInSchema] ? Partial<Record<core.input<Key> & PropertyKey, core.input<Value>>> : Record<core.input<Key> & PropertyKey, core.input<Value>>;
837
- export interface $ZodRecordInternals<Key extends $ZodRecordKey = $ZodRecordKey, Value extends SomeType = $ZodType> extends $ZodTypeInternals<$InferZodRecordOutput<Key, Value>, $InferZodRecordInput<Key, Value>> {
837
+ export interface $ZodRecordInternals<Key extends $ZodRecordKey = $ZodRecordKey, Value extends SomeType = $ZodType> extends $ZodTypeInternals {
838
838
  def: $ZodRecordDef<Key, Value>;
839
+ output: $InferZodRecordOutput<Key, Value>;
840
+ input: $InferZodRecordInput<Key, Value>;
839
841
  isst: errors.$ZodIssueInvalidType | errors.$ZodIssueInvalidKey<Record<PropertyKey, unknown>>;
840
842
  optin?: "optional" | undefined;
841
843
  optout?: "optional" | undefined;
@@ -816,6 +816,33 @@ export const $ZodDate = /*@__PURE__*/ core.$constructor("$ZodDate", (inst, def)
816
816
  return payload;
817
817
  };
818
818
  });
819
+ // an oversized container fails its leading size checks before any element is parsed
820
+ function capSize(inst, size) {
821
+ const caps = util.sizeCaps(inst._zod.def.checks);
822
+ if (!caps.length)
823
+ return;
824
+ const cap = Math.min(...caps.map((c) => c[0]));
825
+ const parse = inst._zod.parse;
826
+ inst._zod.parse = (payload, ctx) => {
827
+ const n = size(payload.value);
828
+ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
829
+ for (const [max, ch] of util.sizeCaps(inst._zod.def.checks)) {
830
+ if (n > max && ch._zod.def.when(payload)) {
831
+ const start = payload.issues.length;
832
+ ch._zod.check(payload);
833
+ // the value is still the raw input, so no later check may read it
834
+ payload.issues[start].continue = false;
835
+ util.attachSchema(payload.issues, start, inst);
836
+ return payload;
837
+ }
838
+ }
839
+ }
840
+ return parse(payload, ctx);
841
+ };
842
+ }
843
+ const arraySize = (input) => (Array.isArray(input) ? input.length : -1);
844
+ const mapSize = (input) => (input instanceof Map ? input.size : -1);
845
+ const setSize = (input) => (input instanceof Set ? input.size : -1);
819
846
  function handleArrayResult(result, final, index) {
820
847
  if (result.issues.length) {
821
848
  final.issues.push(...util.prefixIssues(index, result.issues));
@@ -861,6 +888,7 @@ export const $ZodArray = /*@__PURE__*/ core.$constructor("$ZodArray", (inst, def
861
888
  }
862
889
  return payload; //handleArrayResultsAsync(parseResults, final);
863
890
  };
891
+ capSize(inst, arraySize);
864
892
  });
865
893
  function handlePropertyResult(result, final, key, input, optin, optout) {
866
894
  const isPresent = key in input;
@@ -1898,6 +1926,7 @@ export const $ZodMap = /*@__PURE__*/ core.$constructor("$ZodMap", (inst, def) =>
1898
1926
  return Promise.all(proms).then(() => payload);
1899
1927
  return payload;
1900
1928
  };
1929
+ capSize(inst, mapSize);
1901
1930
  });
1902
1931
  function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
1903
1932
  if (keyResult.issues.length) {
@@ -1967,6 +1996,7 @@ export const $ZodSet = /*@__PURE__*/ core.$constructor("$ZodSet", (inst, def) =>
1967
1996
  return Promise.all(proms).then(() => payload);
1968
1997
  return payload;
1969
1998
  };
1999
+ capSize(inst, setSize);
1970
2000
  });
1971
2001
  function handleSetResult(result, final) {
1972
2002
  if (result.issues.length) {
@@ -46,6 +46,7 @@ exports.aborted = aborted;
46
46
  exports.explicitlyAborted = explicitlyAborted;
47
47
  exports.prefixIssues = prefixIssues;
48
48
  exports.unwrapMessage = unwrapMessage;
49
+ exports.sizeCaps = sizeCaps;
49
50
  exports.attachSchema = attachSchema;
50
51
  exports.finalizeIssue = finalizeIssue;
51
52
  exports.getSizableOrigin = getSizableOrigin;
@@ -620,6 +621,20 @@ function unwrapMessage(message) {
620
621
  return typeof message === "string" ? message : message?.message;
621
622
  }
622
623
  /* A check holds no link back to the schema it is attached to — the same check instance is shared by every clone of that schema — so the owner is stamped onto the issues a check just raised, at the only point where both are in scope. Runs on the failure path only; `start` is the issue count from before the check ran. */
624
+ /** The leading size checks of `defChecks` that set an upper bound, each paired with it. */
625
+ function sizeCaps(defChecks) {
626
+ const caps = [];
627
+ for (const ch of defChecks ?? []) {
628
+ const d = ch._zod.def;
629
+ // a later check may rewrite the value
630
+ if (!/_(length|size)$|_equals$/.test(d.check))
631
+ break;
632
+ const max = d.maximum ?? d.length ?? d.size;
633
+ if (max >= 0)
634
+ caps.push([max, ch]);
635
+ }
636
+ return caps;
637
+ }
623
638
  function attachSchema(issues, start, inst) {
624
639
  var _a;
625
640
  for (let i = start; i < issues.length; i++) {
@@ -221,6 +221,8 @@ export declare function prefixIssues(path: PropertyKey, issues: errors.$ZodRawIs
221
221
  export declare function unwrapMessage(message: string | {
222
222
  message: string;
223
223
  } | undefined | null): string | undefined;
224
+ /** The leading size checks of `defChecks` that set an upper bound, each paired with it. */
225
+ export declare function sizeCaps(defChecks: checks.$ZodCheck<never>[] | undefined): [number, checks.$ZodCheck<never>][];
224
226
  export declare function attachSchema(issues: errors.$ZodRawIssue[], start: number, inst: schemas.$ZodType): void;
225
227
  export declare function finalizeIssue(iss: errors.$ZodRawIssue, ctx: schemas.ParseContextInternal | undefined, config: $ZodConfig): errors.$ZodIssue;
226
228
  export declare function getSizableOrigin(input: any): "set" | "map" | "file" | "unknown";