pi-cursor-sdk 0.1.62 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +5 -5
  3. package/docs/cursor-live-smoke-checklist.md +4 -4
  4. package/docs/cursor-model-ux-spec.md +5 -3
  5. package/docs/cursor-native-tool-visual-audit.md +1 -1
  6. package/docs/cursor-testing-lessons.md +1 -1
  7. package/docs/platform-smoke.md +3 -3
  8. package/node_modules/fast-uri/index.js +37 -1
  9. package/node_modules/fast-uri/package.json +1 -1
  10. package/node_modules/fast-uri/test/security.test.js +136 -0
  11. package/node_modules/hono/dist/cjs/context.js +28 -13
  12. package/node_modules/hono/dist/cjs/helper/proxy/index.js +4 -0
  13. package/node_modules/hono/dist/cjs/helper/streaming/sse.js +1 -1
  14. package/node_modules/hono/dist/cjs/hono-base.js +3 -2
  15. package/node_modules/hono/dist/cjs/jsx/base.js +27 -23
  16. package/node_modules/hono/dist/cjs/jsx/hooks/index.js +16 -13
  17. package/node_modules/hono/dist/cjs/middleware/cache/index.js +103 -8
  18. package/node_modules/hono/dist/cjs/middleware/compress/index.js +5 -0
  19. package/node_modules/hono/dist/cjs/middleware/cors/index.js +2 -2
  20. package/node_modules/hono/dist/cjs/middleware/etag/index.js +1 -1
  21. package/node_modules/hono/dist/cjs/middleware/jwk/jwk.js +9 -4
  22. package/node_modules/hono/dist/cjs/middleware/jwt/jwt.js +9 -4
  23. package/node_modules/hono/dist/cjs/middleware/language/language.js +10 -6
  24. package/node_modules/hono/dist/cjs/middleware/method-not-allowed/index.js +90 -0
  25. package/node_modules/hono/dist/cjs/middleware/secure-headers/secure-headers.js +10 -4
  26. package/node_modules/hono/dist/cjs/request.js +7 -9
  27. package/node_modules/hono/dist/cjs/router/reg-exp-router/node.js +55 -56
  28. package/node_modules/hono/dist/cjs/router/reg-exp-router/router.js +64 -85
  29. package/node_modules/hono/dist/cjs/router/reg-exp-router/trie.js +13 -5
  30. package/node_modules/hono/dist/cjs/router.js +1 -1
  31. package/node_modules/hono/dist/cjs/utils/accept.js +1 -1
  32. package/node_modules/hono/dist/cjs/utils/cookie.js +3 -2
  33. package/node_modules/hono/dist/cjs/utils/url.js +8 -8
  34. package/node_modules/hono/dist/context.js +28 -13
  35. package/node_modules/hono/dist/helper/proxy/index.js +4 -0
  36. package/node_modules/hono/dist/helper/streaming/sse.js +1 -1
  37. package/node_modules/hono/dist/hono-base.js +3 -2
  38. package/node_modules/hono/dist/jsx/base.js +27 -23
  39. package/node_modules/hono/dist/jsx/hooks/index.js +16 -13
  40. package/node_modules/hono/dist/middleware/cache/index.js +103 -8
  41. package/node_modules/hono/dist/middleware/compress/index.js +5 -0
  42. package/node_modules/hono/dist/middleware/cors/index.js +2 -2
  43. package/node_modules/hono/dist/middleware/etag/index.js +1 -1
  44. package/node_modules/hono/dist/middleware/jwk/jwk.js +9 -4
  45. package/node_modules/hono/dist/middleware/jwt/jwt.js +9 -4
  46. package/node_modules/hono/dist/middleware/language/language.js +10 -6
  47. package/node_modules/hono/dist/middleware/method-not-allowed/index.js +68 -0
  48. package/node_modules/hono/dist/middleware/secure-headers/secure-headers.js +10 -4
  49. package/node_modules/hono/dist/request.js +8 -10
  50. package/node_modules/hono/dist/router/reg-exp-router/node.js +55 -56
  51. package/node_modules/hono/dist/router/reg-exp-router/router.js +64 -85
  52. package/node_modules/hono/dist/router/reg-exp-router/trie.js +13 -5
  53. package/node_modules/hono/dist/router.js +1 -1
  54. package/node_modules/hono/dist/types/adapter/aws-lambda/types.d.ts +9 -0
  55. package/node_modules/hono/dist/types/client/types.d.ts +1 -1
  56. package/node_modules/hono/dist/types/hono-base.d.ts +4 -3
  57. package/node_modules/hono/dist/types/jsx/base.d.ts +4 -2
  58. package/node_modules/hono/dist/types/jsx/dom/index.d.ts +5 -5
  59. package/node_modules/hono/dist/types/jsx/dom/intrinsic-element/components.d.ts +2 -2
  60. package/node_modules/hono/dist/types/jsx/dom/server.d.ts +5 -5
  61. package/node_modules/hono/dist/types/jsx/hooks/index.d.ts +8 -6
  62. package/node_modules/hono/dist/types/jsx/index.d.ts +5 -5
  63. package/node_modules/hono/dist/types/middleware/cache/index.d.ts +6 -4
  64. package/node_modules/hono/dist/types/middleware/cors/index.d.ts +1 -1
  65. package/node_modules/hono/dist/types/middleware/jsx-renderer/index.d.ts +2 -2
  66. package/node_modules/hono/dist/types/middleware/jwk/jwk.d.ts +2 -0
  67. package/node_modules/hono/dist/types/middleware/jwt/jwt.d.ts +2 -0
  68. package/node_modules/hono/dist/types/middleware/method-not-allowed/index.d.ts +49 -0
  69. package/node_modules/hono/dist/types/router/reg-exp-router/node.d.ts +1 -1
  70. package/node_modules/hono/dist/types/router/reg-exp-router/trie.d.ts +2 -1
  71. package/node_modules/hono/dist/types/router.d.ts +1 -1
  72. package/node_modules/hono/dist/types/utils/headers.d.ts +2 -2
  73. package/node_modules/hono/dist/types/utils/url.d.ts +1 -0
  74. package/node_modules/hono/dist/utils/accept.js +1 -1
  75. package/node_modules/hono/dist/utils/cookie.js +4 -3
  76. package/node_modules/hono/dist/utils/url.js +6 -7
  77. package/node_modules/hono/package.json +10 -3
  78. package/node_modules/ip-address/README.md +146 -126
  79. package/node_modules/ip-address/dist/common.d.ts +29 -0
  80. package/node_modules/ip-address/dist/common.js +44 -5
  81. package/node_modules/ip-address/dist/common.js.map +1 -1
  82. package/node_modules/ip-address/dist/ipv4.d.ts +8 -1
  83. package/node_modules/ip-address/dist/ipv4.js +24 -20
  84. package/node_modules/ip-address/dist/ipv4.js.map +1 -1
  85. package/node_modules/ip-address/dist/ipv6.d.ts +68 -3
  86. package/node_modules/ip-address/dist/ipv6.js +180 -53
  87. package/node_modules/ip-address/dist/ipv6.js.map +1 -1
  88. package/node_modules/ip-address/dist/v4/constants.js +5 -1
  89. package/node_modules/ip-address/dist/v4/constants.js.map +1 -1
  90. package/node_modules/ip-address/dist/v6/constants.js +3 -2
  91. package/node_modules/ip-address/dist/v6/constants.js.map +1 -1
  92. package/node_modules/ip-address/package.json +6 -4
  93. package/package.json +5 -5
  94. package/scripts/platform-smoke/live-suite-runner.mjs +15 -39
  95. package/scripts/platform-smoke/platform-build-windows.ps1 +1 -1
  96. package/scripts/platform-smoke/scenarios.mjs +1 -1
  97. package/scripts/platform-smoke.mjs +5 -5
  98. package/scripts/refresh-cursor-model-snapshots.mjs +16 -7
  99. package/shared/cursor-model-selection-identities.d.mts +21 -0
  100. package/shared/cursor-model-selection-identities.mjs +95 -0
  101. package/src/bundled-context-windows.ts +18 -12
  102. package/src/context.ts +1 -1
  103. package/src/cursor-context-tools.ts +1 -1
  104. package/src/cursor-display-only-trace.ts +1 -1
  105. package/src/cursor-fallback-models.generated.ts +875 -361
  106. package/src/cursor-live-run-accounting.ts +1 -1
  107. package/src/cursor-live-run-coordinator.ts +1 -1
  108. package/src/cursor-native-replay-routing.ts +1 -1
  109. package/src/cursor-partial-content-emitter.ts +1 -1
  110. package/src/cursor-pi-tool-bridge-mcp.ts +1 -1
  111. package/src/cursor-pi-tool-bridge-run.ts +1 -1
  112. package/src/cursor-pi-tool-bridge-types.ts +1 -1
  113. package/src/cursor-provider-lazy.ts +1 -1
  114. package/src/cursor-provider-live-run-drain.ts +1 -1
  115. package/src/cursor-provider-overflow.ts +1 -1
  116. package/src/cursor-provider-run-finalizer.ts +1 -1
  117. package/src/cursor-provider-turn-coordinator.ts +1 -1
  118. package/src/cursor-provider-turn-lifecycle-emitter.ts +1 -1
  119. package/src/cursor-provider-turn-prepare.ts +1 -1
  120. package/src/cursor-provider-turn-types.ts +1 -1
  121. package/src/cursor-provider.ts +1 -1
  122. package/src/cursor-run-final-text.ts +1 -1
  123. package/src/cursor-sdk-event-debug.ts +1 -1
  124. package/src/cursor-session-agent.ts +1 -1
  125. package/src/cursor-session-send-policy.ts +1 -1
  126. package/src/cursor-usage-accounting.ts +1 -1
  127. package/src/model-discovery.ts +32 -89
  128. package/node_modules/cross-spawn/node_modules/which/CHANGELOG.md +0 -166
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 - 2026-08-06
4
+
5
+ ### Added
6
+
7
+ - Add a dated, hash-verified evidence bundle for Cursor's persisted system messages and reconstructed tool guidance for Grok 4.5, Claude Opus/Fable 5, and GPT-5.6 Sol/Terra/Luna.
8
+
9
+ ### Changed
10
+
11
+ - Require Pi 0.84.0 or later, pin the local Pi validation packages to exact 0.84.0 with TypeBox 1.3.7, move pi-ai imports from the temporary compatibility entrypoint to the supported root API, and update test harness contexts for Pi's scoped-model and native-provider registration types.
12
+ - Bound Vitest concurrency to four workers so process and filesystem contract tests remain reliable on high-core platform-smoke hosts.
13
+ - Refresh the 34-model Cursor fallback catalog and checkpoint-derived context-window snapshot from the live `@cursor/sdk` 1.0.23 runtime.
14
+
15
+ ### Security
16
+
17
+ - Refresh vulnerable transitive releases within their existing ranges: bundled Hono, fast-uri, and ip-address plus development-only PostCSS and protobufjs. The remaining production audit findings are confined to the pinned Cursor SDK → ConnectRPC → undici chain, which has no compatible fix.
18
+
19
+ ### Fixed
20
+
21
+ - Normalize checkpoint context-window keys to current selectable model identities, collapse redundant default `:fast`/`:slow` aliases, reject conflicting equivalent selections, remove stale or ambiguous aliases, and reuse base-model context evidence for unobserved equivalent aliases.
22
+ - Give Windows platform-build checks the same 15-second Vitest scheduling headroom as the full Windows test run, while preserving normal local test timeouts.
23
+ - Pass live PTY smoke prompts as direct Node argv through Pi's interactive initial-message contract, isolate unrelated startup probes with `PI_OFFLINE=1`, and keep final markers out of prompt echoes.
24
+ - Run required platform targets sequentially so concurrent VM/container load and Cursor API calls cannot starve otherwise healthy smoke lanes.
25
+
3
26
  ## 0.1.62 - 2026-07-29
4
27
 
5
28
  ### Added
package/README.md CHANGED
@@ -51,10 +51,10 @@ If pi started without a key, run `/cursor-refresh-models` after `/login` to refr
51
51
  ## Requirements
52
52
 
53
53
  - Node.js 22.19+
54
- - pi 0.80.9 or newer recommended; pi core peer metadata is intentionally unpinned so newer pi releases are not blocked
54
+ - Pi 0.84.0 or later; pi core peer metadata remains optional and uses `"*"` ranges per Pi package guidance
55
55
  - a Cursor SDK API key saved through `/login`, available as `CURSOR_API_KEY`, or passed with pi's `--api-key`
56
56
 
57
- No global `@cursor/sdk` install is required. This package depends on exact `@cursor/sdk@1.0.23`, so normal package installation brings in the SDK version this extension was built and tested against. Cursor SDK 1.0.23 declares its Node ConnectRPC transport dependency directly, so npm installs place `@connectrpc/connect-node` where the SDK can resolve it. The extension intentionally does not bundle `@cursor/sdk` or its platform packages, because packing from one maintainer OS can otherwise ship the wrong optional SDK binary for another OS. Cursor SDK 1.0.23 keeps the older `sqlite3 -> node-gyp@8` dependency chain out of the runtime tree, so deprecated install warnings for `inflight`, `rimraf`, `glob@7`, `npmlog`, `gauge`, `are-we-there-yet`, and `tar@6` from that chain are not expected. This package follows pi package guidance by declaring pi core package peers with `"*"` ranges, so users who update pi before this extension is republished are not blocked by peer metadata. The current recommended and validated pi baseline is 0.80.9 plus Cursor SDK 1.0.23; older pi compatibility paths are best-effort and older Cursor SDK compatibility paths are not maintained.
57
+ No global `@cursor/sdk` install is required. This package depends on exact `@cursor/sdk@1.0.23`, so normal package installation brings in the SDK version this extension was built and tested against. Cursor SDK 1.0.23 declares its Node ConnectRPC transport dependency directly, so npm installs place `@connectrpc/connect-node` where the SDK can resolve it. The extension intentionally does not bundle `@cursor/sdk` or its platform packages, because packing from one maintainer OS can otherwise ship the wrong optional SDK binary for another OS. Cursor SDK 1.0.23 keeps the older `sqlite3 -> node-gyp@8` dependency chain out of the runtime tree, so deprecated install warnings for `inflight`, `rimraf`, `glob@7`, `npmlog`, `gauge`, `are-we-there-yet`, and `tar@6` from that chain are not expected. Older Pi and Cursor SDK compatibility paths are not maintained.
58
58
 
59
59
  ## Install
60
60
 
@@ -78,7 +78,7 @@ Use `-l` if you want the package recorded in the current project's `.pi/settings
78
78
  pi install -l npm:pi-cursor-sdk
79
79
  ```
80
80
 
81
- Pi 0.80.9 loads project-local extensions only after project trust is resolved, so this extension cannot observe that trust event. When a project-local install needs to read or write `.pi/cursor-sdk.json`, start every such run with explicit approval:
81
+ Pi 0.84.0 loads project-local extensions only after project trust is resolved, so this extension cannot observe that trust event. When a project-local install needs to read or write `.pi/cursor-sdk.json`, start every such run with explicit approval:
82
82
 
83
83
  ```bash
84
84
  pi --approve --model cursor/composer-2-5
@@ -318,7 +318,7 @@ PI_CURSOR_LOCAL_RESUME=0 pi --model cursor/composer-2-5
318
318
 
319
319
  Resume is strict: the current pi session file/id, branch path prefix, cwd/repo root, model/API/tool-surface pool key, SDK store identity, and compaction generation must match. Each persisted pi session gets a SQLite store under `<getDefaultSdkStateRoot(cwd)>/pi-sessions/<session-hash>/`, and that same store is used for create/resume, transcript reads, checkpoint lookup, and exact-ID cleanup so parallel pi sessions do not contend on one workspace `index.db`. Fileless sessions use a unique OS-temporary store per acquisition, remove it on graceful disposal, and start a fresh agent after invalidation instead of reopening a disposed temporary store. Legacy resume entries still try the SDK's default workspace store; if that resume fails or the agent is later replaced, the new agent moves to the per-session store. A trailing user message already present at process startup is crash-ambiguous and invalidates the old handle; only a user message appended in the current process may span a recorded handle, preventing restart from resending an already-submitted prompt. A successful process reattachment bootstraps the current pi transcript once while retaining the resumed Cursor agent's native state; later in-process turns remain incremental. If `Agent.resume()` fails, pi bootstraps a new local Cursor agent from the current transcript and streams one display-only continuity note. Superseded local agents can be cleaned up explicitly with `/cursor-local-resume-cleanup --dry-run` and `/cursor-local-resume-cleanup --yes`; cleanup only deletes exact recorded `agent-*` IDs from their recorded store. Cloud resume remains disabled; `/cursor-cloud list|archive|delete` only manages recorded cloud agents.
320
320
 
321
- Config can also set non-secret defaults in `~/.pi/agent/cursor-sdk.json` or trusted `.pi/cursor-sdk.json`. Project config activates only when Pi's project-trust flow reached this extension and approved the project, or the run started with explicit `--approve`; Pi's implicit trust for a project with no recognized resources is not enough. Because Pi 0.80.9 loads project-local package extensions after the trust event, `pi install -l` users must pass `--approve` on every run that reads or writes `.pi/cursor-sdk.json`. A trust resource added after trust resolution requires restarting pi. `/cursor-runtime ... --save-project` requires the same trust provenance and does not create Pi trust resources automatically. Explicit runtime, fast-default, and HTTP transport saves preserve unrecognized fields, reject malformed or non-object JSON without rewriting it, and serialize concurrent writers. A completed global preference write is retained if Pi's subsequent session-journal append fails, because Pi may already have mutated the in-memory branch; the command reports that partial journal failure and ignores the uncertain session entry until a later successful save or session restart. If a process is force-killed during the tiny update window, the next save reports the `.lock` path; remove it only after confirming no pi process is writing that config.
321
+ Config can also set non-secret defaults in `~/.pi/agent/cursor-sdk.json` or trusted `.pi/cursor-sdk.json`. Project config activates only when Pi's project-trust flow reached this extension and approved the project, or the run started with explicit `--approve`; Pi's implicit trust for a project with no recognized resources is not enough. Because Pi 0.84.0 loads project-local package extensions after the trust event, `pi install -l` users must pass `--approve` on every run that reads or writes `.pi/cursor-sdk.json`. A trust resource added after trust resolution requires restarting pi. `/cursor-runtime ... --save-project` requires the same trust provenance and does not create Pi trust resources automatically. Explicit runtime, fast-default, and HTTP transport saves preserve unrecognized fields, reject malformed or non-object JSON without rewriting it, and serialize concurrent writers. A completed global preference write is retained if Pi's subsequent session-journal append fails, because Pi may already have mutated the in-memory branch; the command reports that partial journal failure and ignores the uncertain session entry until a later successful save or session restart. If a process is force-killed during the tiny update window, the next save reports the `.lock` path; remove it only after confirming no pi process is writing that config.
322
322
 
323
323
  ```json
324
324
  {
@@ -519,7 +519,7 @@ See [Cursor testing lessons](docs/cursor-testing-lessons.md#cursor-sdk-event-cap
519
519
 
520
520
  If startup has no stored `/login` key or `CURSOR_API_KEY`, model discovery fails, or discovery returns no models, the extension registers a bundled fallback snapshot of the latest reviewed Cursor SDK model catalog and notifies interactive users when possible. Pi CLI `--api-key` remains available to provider turns but is not parsed independently during startup discovery.
521
521
 
522
- The fallback snapshot includes Composer 2.5 (`composer-2.5` and `composer-2-5`), Composer 2, Cursor's GPT-5.6 Luna/Sol/Terra models, Claude, Gemini, Grok, Kimi, and other model IDs exposed by the reviewed `Cursor.models.list()` output. Pi's separate `openai-codex` catalog is owned by Pi itself; Pi 0.80.5 adds 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`.
522
+ The fallback snapshot includes Composer 2.5 (`composer-2.5` and `composer-2-5`), Composer 2, Cursor's GPT-5.6 Luna/Sol/Terra models, Claude, Gemini, Grok, Kimi, and other model IDs exposed by the reviewed `Cursor.models.list()` output. Pi's separate `openai-codex` catalog is owned by Pi itself; Pi 0.84.0 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`. 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.
523
523
 
524
524
  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.
525
525
 
@@ -67,8 +67,8 @@ The replay scan flags only error `toolResult` / error assistant messages with `T
67
67
 
68
68
  Pass criteria:
69
69
 
70
- - `pi --version` reports pi 0.80.9 for this cutover baseline.
71
- - `npm ls` shows `@cursor/sdk@1.0.23` and local `@earendil-works/*@0.80.9` packages.
70
+ - `pi --version` reports Pi 0.84.0 for this cutover baseline.
71
+ - `npm ls` shows `@cursor/sdk@1.0.23` and local `@earendil-works/*@0.84.0` packages.
72
72
  - `cursor/composer-2-5` appears in the model list.
73
73
  - No Cursor key or auth token is printed.
74
74
  - If neither `~/.pi/agent/auth.json` cursor auth nor `CURSOR_API_KEY` is available, stop and report the live smoke as blocked.
@@ -125,7 +125,7 @@ Observe with `tmux capture-pane -pt "$SESSION"` or attach manually.
125
125
  Pass criteria:
126
126
 
127
127
  - Footer shows `(cursor) composer-2-5`. With `--cursor-no-fast`, Cursor fast mode is off and the Cursor extension status should show `cursor:local · fast:off`; ignore unrelated status text from other extensions.
128
- - The run uses pi 0.80.9 `--session-id` successfully.
128
+ - The run uses Pi 0.84.0 `--session-id` successfully.
129
129
  - Assistant answer appears correctly.
130
130
  - `/session` shows one user and one assistant message for the simple run.
131
131
  - Persisted JSONL has one assistant message. If the screen appears duplicated, inspect JSONL before deciding whether it is a rendering bug.
@@ -133,7 +133,7 @@ Pass criteria:
133
133
 
134
134
  ## 4. Focused visual card/color rendering check
135
135
 
136
- This is the canonical inner-loop visual debug path for Cursor provider/runtime changes. It requires offscreen TUI visual inspection, not only JSONL or code review. Use pi 0.80.9, `@cursor/sdk@1.0.23`, a fresh temporary session dir, Cursor SDK `plan` mode, native replay enabled, and the checked-in visual runner. The runner resolves `pi` by directly walking the parent `PATH`, uses `process.execPath` for Node, and prepends that Node directory for both prereq checks and tmux launches so `#!/usr/bin/env node` shims use the validated Node. The default matrix is native replay only: native replay registration is forced on, settings sources are `none`, the pi bridge is off, overlapping built-in pi tools are not exposed, and inherited Cursor SDK event-debug artifact env is cleared. With `--event-debug`, debug capture writes to a deterministic directory under `VISUAL_DIR`.
136
+ This is the canonical inner-loop visual debug path for Cursor provider/runtime changes. It requires offscreen TUI visual inspection, not only JSONL or code review. Use Pi 0.84.0, `@cursor/sdk@1.0.23`, a fresh temporary session dir, Cursor SDK `plan` mode, native replay enabled, and the checked-in visual runner. The runner resolves `pi` by directly walking the parent `PATH`, uses `process.execPath` for Node, and prepends that Node directory for both prereq checks and tmux launches so `#!/usr/bin/env node` shims use the validated Node. The default matrix is native replay only: native replay registration is forced on, settings sources are `none`, the pi bridge is off, overlapping built-in pi tools are not exposed, and inherited Cursor SDK event-debug artifact env is cleared. With `--event-debug`, debug capture writes to a deterministic directory under `VISUAL_DIR`.
137
137
 
138
138
  ```bash
139
139
  VISUAL_DIR="$(mktemp -d /tmp/pi-cursor-sdk-1016-visual.XXXXXX)"
@@ -15,7 +15,7 @@ Current implementation notes:
15
15
  - Cursor status uses one coordinated `ctx.ui.setStatus("cursor", ...)` value for fast, non-default plan mode, and the local-only `http1` transport marker; the default pi footer remains intact.
16
16
  - Installed `@cursor/sdk` user messages accept images, and Cursor models are treated as image-capable; registered input metadata is `text` plus `image`.
17
17
  - Image payload forwarding sends images only from the latest user message. If the latest user turn is plain text after an earlier image turn, the transcript keeps an `[image omitted from transcript]` placeholder but no image bytes are sent to Cursor. The prompt explicitly tells Cursor that prior image bytes are unavailable and to ask the user to reattach or describe a prior image when needed. Carrying images forward across turns remains a future product decision because it affects token cost, privacy, stale visual context, and expected multimodal follow-up behavior.
18
- - Exact `@cursor/sdk@1.0.23` is a package dependency of this extension; users should not need a global SDK install. pi 0.80.9 is the current recommended validation baseline, while published pi core peer dependencies use `"*"` ranges per current pi package guidance. Newer pi versions are allowed to attempt loading this extension before a matching extension release exists; compatibility is best-effort until validated.
18
+ - Exact `@cursor/sdk@1.0.23` is a package dependency of this extension; users should not need a global SDK install. Pi 0.84.0 is the minimum supported and current validation baseline, while optional published Pi core peer dependencies use `"*"` ranges per current Pi package guidance.
19
19
  - Startup discovery does not duplicate Pi CLI parsing: it uses stored `~/.pi/agent/auth.json` API-key auth from `/login`, then `CURSOR_API_KEY`, otherwise it registers the bundled fallback catalog. Provider turns keep Pi's resolved `options.apiKey`. `/cursor-refresh-models` and `/cursor-cloud` mutations resolve provider `cursor` through the command context's Pi ModelRegistry, then normalize placeholders through `CURSOR_API_KEY`. The extension config file stores only non-secret Cursor-only state such as fast defaults and the user-level local HTTP transport preference.
20
20
  - Cursor Cloud repository overrides accept only HTTPS repository URLs without userinfo, query parameters, or fragments. Invalid values fail during preflight before `Agent.create()`, messages never echo the supplied URL, and shared provider/maintainer scrubbing removes URL/SCP-style userinfo defensively.
21
21
  - Cursor Cloud requires a persisted pi session. Immediately after remote `Agent.create()` returns, before debug work or abort checks, the provider appends a branch-local pi lifecycle entry, fsyncs the existing Pi session JSONL anchor through a read-write descriptor, and then fsyncs a newline-framed sidecar keyed by the stable pi session ID (POSIX mode `0600`; Windows inherits the user session directory ACL) in the session directory. Journal creation is exclusive, and existing append/read opens reject symlinks and require matching regular-file descriptor/path identity before using the descriptor. Existing session files use the exact lifecycle entry ID as anchor; fileless first turns use an orphan marker that a same-session-ID restart durably claims onto exactly one matching or replacement branch, surviving the timestamped path change and later JSONL creation without granting sibling access; returned run IDs are fsynced before abort/wait handling, readers skip malformed/truncated lines independently, and branch-only history events are reduced after deduplicated sidecar history, and tombstones are tracked before records exist. A durable sidecar result remains authoritative if its optional Pi mirror append fails; if the sidecar result itself fails, the prior durable intent remains pending and blocks retry rather than claiming success. `--no-session` and durable-ledger failures fail closed before send, while post-send ledger failure requests bounded cancellation. `/cursor-cloud` always validates the embedded session ID, accepts only null anchors while truly fileless, and reconciles each orphan to one branch before listing or mutation. Successive record events merge run/branch metadata monotonically even when mirrored sources arrive out of order. Archive/delete require resolved Cursor auth, fsync a durable intent before the SDK call, and fsync a durable success result afterward; unresolved intent blocks repeat mutation and requires dashboard inspection.
@@ -29,7 +29,7 @@ Current implementation notes:
29
29
  - The provider also registers `cursor_ask_question` for Cursor models when the bridge and default-on `PI_CURSOR_ASK_QUESTION` control are enabled. While the tool awaits pi UI input it emits package event `pi-cursor-sdk:ask-question:blocked` with `{ active: true }` and clears `{ active: false }` in `finally`; the tool runs with `executionMode: "sequential"` so parallel sibling calls cannot overlap dialogs. Cursor sees it as `pi__cursor_ask_question`, and pi executes it through the normal tool path so interactive users can choose options from pi UI. `PI_CURSOR_ASK_QUESTION=0` removes only this tool while preserving the rest of the bridge. In non-UI modes it reports that UI is unavailable so Cursor can state a default assumption instead. When pi has visible Agent Skills loaded, the provider rewrites the skill catalog for Cursor and registers `cursor_activate_skill` as `pi__cursor_activate_skill`; pi executes it through the normal tool path so Cursor can load the full `SKILL.md` and skill resource list for the current pi-loaded skill source of truth. `PI_CURSOR_PI_TOOL_BRIDGE=0` disables the local bridge, including question and skill activation bridging. Cloud Cursor agents remain out of scope for the bridge.
30
30
  - The bridge queues MCP calls, emits provider `toolcall_*` events, waits for matching pi `toolResult` messages by `toolCallId`, resolves the result back into the same live Cursor SDK run without creating a new `Agent`, and never calls tool `execute()` handlers directly. The same-run resume invariant holds unless the run was disposed, aborted, or cancelled.
31
31
  - Cursor SDK MCP tool calls use a guarded timeout override because installed `@cursor/sdk` 1.0.23 still has a 60-second MCP request default with no public per-server timeout option. The extension extends the verified Cursor SDK MCP `callTool` timeout path to 3600 seconds by default and shortens the verified first-send MCP initialize/listTools timeout paths to 10 seconds by default so unavailable configured MCP servers do not block the first reply for a full minute; unknown MCP protocol timeout stacks keep the SDK default. Users can override tool-call timeouts with `PI_CURSOR_MCP_TOOL_TIMEOUT_MS` or `PI_CURSOR_MCP_TOOL_TIMEOUT_SECONDS`, and initialize/listTools timeouts with `PI_CURSOR_MCP_CONNECT_TIMEOUT_MS` or `PI_CURSOR_MCP_CONNECT_TIMEOUT_SECONDS`. Bridged `CallTool` waits also have a local fail-closed deadline that defaults to and cannot exceed the effective MCP tool timeout; `PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS` can lower it, expiry or MCP cancellation aborts active pi execution when available, and expired bridge events are dropped before pi tool emission.
32
- - Cursor SDK local safety controls are off by default. `--cursor-auto-review` / `PI_CURSOR_AUTO_REVIEW` and `--cursor-sandbox` / `PI_CURSOR_SANDBOX` pass only explicit enabled values into `Agent.create({ local })`; user or trusted project config can set `local.autoReview` and `local.sandboxOptions.enabled`; project config is active only when Pi's project-trust flow reached the extension and approved the project or the run used explicit `--approve`, and project saves require the same immutable trust provenance rather than creating Pi trust resources automatically. Pi 0.80.9 loads `pi install -l` project-local extensions after the trust event, so those installs require `--approve` on every run that reads or writes `.pi/cursor-sdk.json`. Explicit runtime, fast-default, and HTTP transport saves preserve unrecognized config fields, reject malformed or non-object JSON without rewriting it, and use one lock-protected read-modify-write path; fast saves mutate only the selected model key. Because Pi can mutate its in-memory session branch before a journal append throws, a completed global save is authoritative and the command reports the partial journal failure instead of attempting an ambiguous rollback; the new global value stays authoritative over stale branch entries until a later successful save or session restart.
32
+ - Cursor SDK local safety controls are off by default. `--cursor-auto-review` / `PI_CURSOR_AUTO_REVIEW` and `--cursor-sandbox` / `PI_CURSOR_SANDBOX` pass only explicit enabled values into `Agent.create({ local })`; user or trusted project config can set `local.autoReview` and `local.sandboxOptions.enabled`; project config is active only when Pi's project-trust flow reached the extension and approved the project or the run used explicit `--approve`, and project saves require the same immutable trust provenance rather than creating Pi trust resources automatically. Pi 0.84.0 loads `pi install -l` project-local extensions after the trust event, so those installs require `--approve` on every run that reads or writes `.pi/cursor-sdk.json`. Explicit runtime, fast-default, and HTTP transport saves preserve unrecognized config fields, reject malformed or non-object JSON without rewriting it, and use one lock-protected read-modify-write path; fast saves mutate only the selected model key. Because Pi can mutate its in-memory session branch before a journal append throws, a completed global save is authoritative and the command reports the partial journal failure instead of attempting an ambiguous rollback; the new global value stays authoritative over stale branch entries until a later successful save or session restart.
33
33
  - Local HTTP/1.1/SSE compatibility is strictly opt-in through `PI_CURSOR_HTTP_1_1`, `/cursor-http on|off|toggle`, or user `cursor-sdk.json` `local.useHttp1ForAgent`. Precedence is session, environment, user, then the built-in unset default; project config is excluded. Unset makes no `Cursor.configure()` call. Explicit values configure the installed SDK before local `Agent.create()`, extension-owned explicit state is cleared with the SDK's documented `null` reset when returning to unset and during session shutdown before module reload, and default/HTTP2/HTTP1 choices split pooled local agents. Pi's supported CLI/TUI/print/RPC lifecycle has one active session runtime per process; concurrent independent `AgentSession` embedding in one process is outside this transport toggle's contract because the installed SDK setting and executor cache are module-global. The footer adds `http1` only for enabled local runtime; cloud creation and status remain untouched.
34
34
  - Bridge diagnostics are opt-in only: `PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1` writes typed, allowlisted, scrubbed single-line JSONL records to `process.stderr` with prefix `[pi-cursor-sdk:bridge]`. Diagnostics are scrubbed operational logs, not anonymous telemetry. They intentionally include tool names, safe correlation IDs, run lifecycle, exposed pi↔MCP name pairs, queued requests, result resolution, rejection, cancellation, and pending counts. Correlation IDs are generated independently from the tokenized endpoint path, and Cursor MCP call IDs are hashed before serialization. Diagnostics must not include endpoint paths/URLs/path components/tokens, API keys, bearer tokens, cookies, session credentials, raw args/results, stdout/stderr payloads, file contents, Cursor settings output, or local private session paths in tracked docs, and they must not call pi UI status, notification, or footer APIs. If tool names themselves are unacceptable for a release target, bridge debug diagnostics are not safe for shared logs under the current contract.
35
35
  - This repo does not provide a generic desktop-automation, browser-driver, or CDP recipe. Provider docs should describe pi-cursor-sdk's Cursor provider/bridge contract only.
@@ -134,7 +134,9 @@ CURSOR_API_KEY="your-key" npm run refresh:cursor-snapshots -- --write \
134
134
  --context-windows ~/.pi/agent/cursor-sdk-context-windows.json
135
135
  ```
136
136
 
137
- Both modes call `Cursor.models.list({ apiKey })` and use the same sanitizer and stable sort. `--check` byte-compares `src/cursor-fallback-models.generated.ts` without writing; `--write` refreshes it and updates `src/bundled-context-windows.ts` only when `--context-windows` is provided. Generated provenance and command output record the installed `@cursor/sdk` version and model count. The script prints model IDs/counts only and scrubs known auth material from SDK errors; it must not print or store API keys. Review generated diffs before committing because Cursor can change aliases, defaults, and parameter meanings.
137
+ Both modes call `Cursor.models.list({ apiKey })` and use the same sanitizer and stable sort. `--check` byte-compares `src/cursor-fallback-models.generated.ts` without writing; `--write` refreshes it and updates `src/bundled-context-windows.ts` only when `--context-windows` is provided. Context-window inputs are limited to current selectable model IDs; redundant default `:fast`/`:slow` aliases collapse to one key, conflicting equivalent selections fail generation, and stale or ambiguous aliases are omitted. Generated provenance and command output record the installed `@cursor/sdk` version and model count. The script prints model IDs/counts only and scrubs known auth material from SDK errors; it must not print or store API keys. Review generated diffs before committing because Cursor can change aliases, defaults, and parameter meanings.
138
+
139
+ Dated evidence for Cursor's assistant-visible, model-specific system text and reconstructed tool guidance lives in [Cursor System Prompts and Tool Guidance — 2026-08-02](https://github.com/fitchmultz/pi-cursor-sdk/blob/main/docs/evidence/cursor-system-prompts-2026-08-02/README.md). Keep that evidence separate from pi-cursor-sdk's own bootstrap prompt: Cursor persists its base system message in the local SDK checkpoint, while this extension sends Pi context and bridge instructions as user content.
138
140
 
139
141
  ## Design Direction
140
142
 
@@ -6,7 +6,7 @@ This workflow is the canonical repo path for verifying Cursor SDK tool replay th
6
6
 
7
7
  Use it before accepting replay-card commits or PRs, and for every Cursor provider/runtime release where TUI card/color behavior could regress. Text logs and JSONL are necessary, but they are not enough when the claim is visual parity: always keep PNGs for the exact prompt, and keep before/after PNGs when reviewing a rendering change.
8
8
 
9
- Current validation baseline: pi 0.80.9, exact `@cursor/sdk@1.0.23`, local validation packages `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` at 0.80.9. Published pi core peer dependencies use `"*"` ranges per current pi package guidance, so newer pi installs can try the extension before a matching validation release exists.
9
+ Current validation baseline: Pi 0.84.0 or later, exact `@cursor/sdk@1.0.23`, and local validation packages `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` at exact 0.84.0. Optional published Pi core peer dependencies use `"*"` ranges per current Pi package guidance.
10
10
 
11
11
  ## Cursor SDK 1.0.17 / pi 0.79.0 cutover visual record
12
12
 
@@ -257,7 +257,7 @@ The script writes timestamped artifacts under `--out` (default `/tmp/pi-cursor-s
257
257
 
258
258
  Stdout prints artifact paths and summary counts only. Raw payloads stay on disk and may contain local paths, project text, tool args/results, or secrets — do not commit or share them.
259
259
 
260
- Hard repo rule: Cursor SDK behavior claims must come from the installed `@cursor/sdk` package and/or https://cursor.com/docs/sdk/typescript, not from memory or ad-hoc probes alone. Current cutover validation targets exact `@cursor/sdk@1.0.23` and pi 0.80.9 local packages.
260
+ Hard repo rule: Cursor SDK behavior claims must come from the installed `@cursor/sdk` package and/or https://cursor.com/docs/sdk/typescript, not from memory or ad-hoc probes alone. Current cutover validation targets exact `@cursor/sdk@1.0.23` and Pi 0.84.0 local packages.
261
261
 
262
262
  ## Pi provider SDK event capture
263
263
 
@@ -106,7 +106,7 @@ Rendering is host-side. Targets capture the real ANSI stream; the macOS host ren
106
106
 
107
107
  ## Target session model
108
108
 
109
- Each target opens one Crabbox target session, syncs once, runs all suites for that target under one coherent target run id, collects artifacts, and stops/releases the target. The release-gate entrypoint runs required targets concurrently; each target still runs its own suites in order and fails fast within that target. Platform smoke disables Crabbox git-seed sync (`CRABBOX_SYNC_GIT_SEED=false`) so every run tests the current local checkout and uncommitted smoke-runner changes rather than a remote Git seed.
109
+ Each target opens one Crabbox target session, syncs once, runs all suites for that target under one coherent target run id, collects artifacts, and stops/releases the target. The release-gate entrypoint runs required targets sequentially to prevent shared host, VM/container, and Cursor API contention; each target runs its own suites in order and fails fast within that target. Platform smoke disables Crabbox git-seed sync (`CRABBOX_SYNC_GIT_SEED=false`) so every run tests the current local checkout and uncommitted smoke-runner changes rather than a remote Git seed.
110
110
 
111
111
  ```text
112
112
  start target session
@@ -135,13 +135,13 @@ start target session
135
135
  end target session
136
136
  ```
137
137
 
138
- The target session fails fast. The release-gate path warms one Crabbox lease per target, performs one fresh sync, runs suites in order on that target, and stops that target after the first failure. Different targets run concurrently to keep wall time bounded by the slowest platform instead of the sum of all platforms. Per-suite commands remain available for diagnosis, but they are intentionally not the normal release path because repeated warmup/sync/install cycles make releases too slow.
138
+ The target session fails fast. The release-gate path handles one target at a time: it warms one Crabbox lease, performs one fresh sync, runs suites in order, and stops that target after the first failure before starting the next target. Total wall time is therefore additive across required targets; this avoids nondeterministic worker loss and live-run stalls from competing VM/container load and concurrent Cursor API calls. Per-suite commands remain available for diagnosis, but they are intentionally not the normal release path because repeated warmup/sync/install cycles make releases too slow.
139
139
 
140
140
  Runtime budget is part of the contract:
141
141
 
142
142
  - `smoke:platform:doctor` never calls Cursor.
143
143
  - `platform-build` runs once per target and is the only suite that performs the full local CI/build/typecheck/package gate. Its Windows VM invocation raises only Vitest's default per-test timeout to 15 seconds for host-contention headroom; normal `npm test` keeps the 5-second default, and explicit longer integration-test timeouts still apply.
144
- - Live suites reuse the target checkout and prepared `node_modules` when run after `platform-build`; they do not repeat `npm ci` in a target-session release run.
144
+ - Live suites reuse the target checkout and prepared `node_modules` when run after `platform-build`; they do not repeat `npm ci` in a target-session release run. Their interactive Pi process sets `PI_OFFLINE=1` to skip unrelated startup catalog/update probes; the scenario's explicit Cursor provider turn remains live. The PTY launches Pi's JavaScript entry with direct Node argv so multiline prompts remain one positional message on every target.
145
145
  - Live and local-resume suites share one target-local packed-install prep directory per target-session release run. The first such suite runs `npm pack` and `npm install --no-save <tarball>` once. Visual/abort suites install that packed path with `pi install --approve -l`; local-resume lanes pass the same packed package path to their source-tree smoke harness instead of loading the checkout extension.
146
146
  - Visual coverage is batched into one native prompt, one focused HTTP/1.1 transport prompt, one bridge prompt, and one abort/cleanup prompt per target. Do not split the card matrices into one prompt per card.
147
147
  - The gate is fail-fast by target to avoid burning Cursor calls after a platform has already failed.
@@ -26,7 +26,12 @@ function normalize (uri, options) {
26
26
  */
27
27
  function resolve (baseURI, relativeURI, options) {
28
28
  const schemelessOptions = options ? Object.assign({ scheme: 'null' }, options) : { scheme: 'null' }
29
- const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true)
29
+ const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions)
30
+ const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions)
31
+ if (baseMalformed || relativeMalformed) {
32
+ throw new Error(baseParsed.error || relativeParsed.error || 'URI is malformed.')
33
+ }
34
+ const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true)
30
35
  schemelessOptions.skipEscape = true
31
36
  return serialize(resolved, schemelessOptions)
32
37
  }
@@ -206,6 +211,15 @@ const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:
206
211
  // with or without a scheme prefix, for the literal-backslash rejection below.
207
212
  const AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/
208
213
 
214
+ // Captures the leading authority-introducer region after an optional scheme: a
215
+ // run of forward slashes, backslashes, and the characters the WHATWG URL parser
216
+ // removes before parsing (TAB U+0009, LF U+000A, CR U+000D). A valid introducer
217
+ // is exactly "//". Node treats "\" as "/" on special schemes and strips those
218
+ // characters first, so forms like "\\", "/\", "\/", "/<TAB>/", or a leading
219
+ // "<TAB>//" reach an authority in Node while fast-uri's URI_PARSE folds them into
220
+ // the path group (host confusion / SSRF / redirect bypass).
221
+ const AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/
222
+
209
223
  /**
210
224
  * @param {import('./types/index').URIComponent} parsed
211
225
  * @param {RegExpMatchArray} matches
@@ -265,6 +279,28 @@ function parseWithStatus (uri, opts) {
265
279
  malformedAuthorityOrPort = true
266
280
  }
267
281
 
282
+ // Reject a malformed or whitespace-smuggled authority introducer. fast-uri
283
+ // only recognizes a literal "//"; anything else in the leading separator run
284
+ // (a backslash, or a "//" that appears only after removing the TAB/LF/CR that
285
+ // Node strips) means the authority fast-uri parses differs from the one Node's
286
+ // URL resolves. Reject rather than rewrite, mirroring the literal-backslash
287
+ // guard above. Percent-encoded forms (%5C, %09) are untouched, valid data.
288
+ const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION)
289
+ if (introducerMatch !== null) {
290
+ const region = introducerMatch[1]
291
+ const normalizedRegion = region.replace(/[\t\n\r]/g, '')
292
+ // Two or more leading separators introduce an authority.
293
+ if (normalizedRegion.length >= 2) {
294
+ if (normalizedRegion.slice(0, 2) !== '//') {
295
+ parsed.error = parsed.error || 'URI authority must not contain a literal backslash.'
296
+ malformedAuthorityOrPort = true
297
+ } else if (region.length !== normalizedRegion.length) {
298
+ parsed.error = parsed.error || 'URI authority introducer must not contain whitespace.'
299
+ malformedAuthorityOrPort = true
300
+ }
301
+ }
302
+ }
303
+
268
304
  const matches = uri.match(URI_PARSE)
269
305
 
270
306
  if (matches) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fast-uri",
3
3
  "description": "Dependency-free RFC 3986 URI toolbox",
4
- "version": "3.1.4",
4
+ "version": "3.1.5",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
@@ -221,3 +221,139 @@ test('parse does not reject a literal backslash in the query or fragment', (t) =
221
221
  t.notOk(parsed.error, 'backslash in query/fragment does not mark the URI malformed')
222
222
  t.equal(parsed.host, 'host.example.com', 'host parsed normally')
223
223
  })
224
+
225
+ test('parse rejects a malformed authority introducer (\\\\, /\\, \\/) in place of //', (t) => {
226
+ // Regression: "\\", "/\\", "\\/" after the scheme colon are not valid authority
227
+ // introducers. Node's URL treats "\\" as interchangeable with "/" on special
228
+ // schemes, so "http:\\\\evil.com/path" would be parsed as host "evil.com" by
229
+ // Node, but fast-uri must reject it as malformed to prevent SSRF/redirect bypass.
230
+ const cases = [
231
+ 'http:\\\\evil.com/path',
232
+ 'http:/\\evil.com/path',
233
+ 'http:\\/evil.com/path',
234
+ 'ws:\\\\evil.com/chat',
235
+ 'wss:\\\\evil.com/chat',
236
+ 'ftp:\\\\evil.com/',
237
+ '\\\\evil.com/path'
238
+ ]
239
+
240
+ t.plan(cases.length)
241
+
242
+ cases.forEach((input) => {
243
+ t.equal(
244
+ fastURI.parse(input).error,
245
+ 'URI authority must not contain a literal backslash.',
246
+ input
247
+ )
248
+ })
249
+ })
250
+
251
+ test('normalize does not canonicalize a malformed-authority-introducer URI', (t) => {
252
+ const cases = [
253
+ 'http:\\\\evil.com/path',
254
+ 'http:/\\evil.com/path'
255
+ ]
256
+
257
+ t.plan(cases.length)
258
+
259
+ cases.forEach((input) => {
260
+ t.equal(fastURI.normalize(input), input, input)
261
+ })
262
+ })
263
+
264
+ test('equal returns false for malformed-authority-introducer URIs', (t) => {
265
+ const pairs = [
266
+ ['http:\\\\evil.com/path', 'http://evil.com/path'],
267
+ ['http:/\\evil.com/path', 'http://evil.com/path']
268
+ ]
269
+
270
+ t.plan(pairs.length)
271
+
272
+ pairs.forEach(([left, right]) => {
273
+ t.equal(fastURI.equal(left, right), false, `${left} != ${right}`)
274
+ })
275
+ })
276
+
277
+ test('resolve throws on malformed authority introducer', (t) => {
278
+ // resolve() returns a plain string with no error field, so the only safe
279
+ // behavior is to throw when either component has a malformed authority.
280
+ const pairs = [
281
+ ['https://allowed.com/', '\\\\evil.com/path'],
282
+ ['\\\\evil.com/path', 'https://allowed.com/'],
283
+ ['https://allowed.com/', 'http:/\\evil.com/path'],
284
+ ['https://allowed.com/', 'http:\\/evil.com/path']
285
+ ]
286
+
287
+ t.plan(pairs.length)
288
+
289
+ pairs.forEach(([base, rel]) => {
290
+ t.throws(
291
+ () => fastURI.resolve(base, rel),
292
+ /URI authority must not contain a literal backslash/,
293
+ `${base} + ${rel}`
294
+ )
295
+ })
296
+ })
297
+
298
+ test('parse rejects a whitespace-split authority introducer (TAB, LF, CR)', (t) => {
299
+ // The WHATWG URL parser removes TAB (U+0009), LF (U+000A) and CR (U+000D) from
300
+ // the input before parsing, so a stripped character wedged into the introducer
301
+ // ("/<TAB>\\", "/<TAB>/", or a leading "<TAB>//") reaches an authority in Node
302
+ // while fast-uri would otherwise fold it into the path. These must be rejected
303
+ // like the adjacent "\\", "/\\", "\\/" forms.
304
+ const cases = [
305
+ { input: '/\t\\evil.com/path', expectedError: 'URI authority must not contain a literal backslash.' },
306
+ { input: '/\t/evil.com/path', expectedError: 'URI authority introducer must not contain whitespace.' },
307
+ { input: '/\n\\evil.com/path', expectedError: 'URI authority must not contain a literal backslash.' },
308
+ { input: '/\r\\evil.com/path', expectedError: 'URI authority must not contain a literal backslash.' },
309
+ { input: '\t//evil.com/path', expectedError: 'URI authority introducer must not contain whitespace.' },
310
+ { input: '\t/\\evil.com/path', expectedError: 'URI authority must not contain a literal backslash.' },
311
+ { input: 'https:/\t/evil.com/path', expectedError: 'URI authority introducer must not contain whitespace.' }
312
+ ]
313
+
314
+ t.plan(cases.length)
315
+
316
+ cases.forEach(({ input, expectedError }) => {
317
+ t.equal(fastURI.parse(input).error, expectedError, JSON.stringify(input))
318
+ })
319
+ })
320
+
321
+ test('resolve throws on a whitespace-split authority introducer', (t) => {
322
+ const pairs = [
323
+ ['https://allowed.com/', '/\t\\evil.com/path'],
324
+ ['https://allowed.com/', '/\t/evil.com/path'],
325
+ ['https://allowed.com/', '/\n\\evil.com/path'],
326
+ ['/\t/evil.com/path', 'https://allowed.com/']
327
+ ]
328
+
329
+ t.plan(pairs.length)
330
+
331
+ pairs.forEach(([base, rel]) => {
332
+ t.throws(
333
+ () => fastURI.resolve(base, rel),
334
+ /URI authority (must not contain a literal backslash|introducer must not contain whitespace)/,
335
+ `${JSON.stringify(base)} + ${JSON.stringify(rel)}`
336
+ )
337
+ })
338
+ })
339
+
340
+ test('parse does not reject valid authority introducer patterns', (t) => {
341
+ // No false positives: "//" introducer and scheme-less "//" must be valid.
342
+ const cases = [
343
+ 'http://good.com/',
344
+ 'https://good.com/',
345
+ 'ws://good.com/chat',
346
+ 'wss://good.com/chat',
347
+ 'ftp://good.com/',
348
+ '//good.com/path',
349
+ '/absolute/path',
350
+ 'relative/path'
351
+ ]
352
+
353
+ t.plan(cases.length)
354
+
355
+ cases.forEach((input) => {
356
+ const parsed = fastURI.parse(input)
357
+ t.notOk(parsed.error, input)
358
+ })
359
+ })
@@ -295,11 +295,11 @@ class Context {
295
295
  return Object.fromEntries(this.#var);
296
296
  }
297
297
  #newResponse(data, arg, headers) {
298
- const responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders ?? new Headers();
299
- if (typeof arg === "object" && "headers" in arg) {
300
- const argHeaders = arg.headers instanceof Headers ? arg.headers : new Headers(arg.headers);
301
- for (const [key, value] of argHeaders) {
302
- if (key.toLowerCase() === "set-cookie") {
298
+ let responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders;
299
+ if (typeof arg === "object" && arg.headers) {
300
+ responseHeaders ??= new Headers();
301
+ for (const [key, value] of new Headers(arg.headers)) {
302
+ if (key === "set-cookie") {
303
303
  responseHeaders.append(key, value);
304
304
  } else {
305
305
  responseHeaders.set(key, value);
@@ -307,19 +307,34 @@ class Context {
307
307
  }
308
308
  }
309
309
  if (headers) {
310
- for (const [k, v] of Object.entries(headers)) {
311
- if (typeof v === "string") {
312
- responseHeaders.set(k, v);
313
- } else {
314
- responseHeaders.delete(k);
315
- for (const v2 of v) {
316
- responseHeaders.append(k, v2);
310
+ if (!responseHeaders) {
311
+ let count = 0;
312
+ for (const k in headers) {
313
+ if (++count > 1 || typeof headers[k] !== "string") {
314
+ responseHeaders = new Headers();
315
+ break;
316
+ }
317
+ }
318
+ }
319
+ if (responseHeaders) {
320
+ for (const k in headers) {
321
+ const v = headers[k];
322
+ if (typeof v === "string") {
323
+ responseHeaders.set(k, v);
324
+ } else {
325
+ responseHeaders.delete(k);
326
+ for (const v2 of v) {
327
+ responseHeaders.append(k, v2);
328
+ }
317
329
  }
318
330
  }
319
331
  }
320
332
  }
321
333
  const status = typeof arg === "number" ? arg : arg?.status ?? this.#status;
322
- return createResponseInstance(data, { status, headers: responseHeaders });
334
+ return createResponseInstance(data, {
335
+ status,
336
+ headers: responseHeaders ?? headers
337
+ });
323
338
  }
324
339
  newResponse = (...args) => this.#newResponse(...args);
325
340
  /**
@@ -92,6 +92,10 @@ const proxy = async (input, proxyInit) => {
92
92
  req.headers.delete("accept-encoding");
93
93
  const res = await (customFetch || fetch)(req);
94
94
  const resHeaders = new Headers(res.headers);
95
+ const connectionValue = resHeaders.get("connection");
96
+ if (connectionValue) {
97
+ connectionValue.split(",").map((h) => h.trim()).filter((h) => ALLOWED_TOKEN_PATTERN.test(h)).forEach((h) => resHeaders.delete(h));
98
+ }
95
99
  hopByHopHeaders.forEach((header) => {
96
100
  resHeaders.delete(header);
97
101
  });
@@ -42,7 +42,7 @@ class SSEStreamingApi extends import_stream.StreamingApi {
42
42
  const sseData = [
43
43
  message.event && `event: ${message.event}`,
44
44
  dataLines,
45
- message.id && `id: ${message.id}`,
45
+ message.id !== void 0 && `id: ${message.id}`,
46
46
  message.retry !== void 0 && `retry: ${message.retry}`
47
47
  ].filter(Boolean).join("\n") + "\n\n";
48
48
  await this.write(sseData);
@@ -43,6 +43,7 @@ class Hono {
43
43
  delete;
44
44
  options;
45
45
  patch;
46
+ query;
46
47
  all;
47
48
  on;
48
49
  use;
@@ -342,8 +343,8 @@ class Hono {
342
343
  * @see {@link https://hono.dev/docs/api/hono#fetch}
343
344
  *
344
345
  * @param {Request} request - request Object of request
345
- * @param {Env} Env - env Object
346
- * @param {ExecutionContext} - context of execution
346
+ * @param {Env} env - env Object
347
+ * @param {ExecutionContext} executionCtx - context of execution
347
348
  * @returns {Response | Promise<Response>} response of request
348
349
  *
349
350
  */
@@ -100,6 +100,18 @@ const booleanAttributes = [
100
100
  "reversed",
101
101
  "selected"
102
102
  ];
103
+ const resolveFunctionComponentResult = (result, suspendedContext) => result.then((resolved) => {
104
+ if (!Array.isArray(resolved) && !(resolved instanceof JSXNode)) {
105
+ return resolved;
106
+ }
107
+ const children = Array.isArray(resolved) ? resolved : [resolved];
108
+ const render = () => {
109
+ const buffer = [""];
110
+ childrenToStringToBuffer(children, buffer);
111
+ return buffer.length === 1 ? (0, import_html.raw)(buffer[0], buffer.callbacks) : (0, import_html2.stringBufferToString)(buffer, buffer.callbacks);
112
+ };
113
+ return suspendedContext ? suspendedContext(render) : (0, import_context.runWithRenderContext)(render);
114
+ });
103
115
  const childrenToStringToBuffer = (children, buffer) => {
104
116
  for (let i = 0, len = children.length; i < len; i++) {
105
117
  const child = children[i];
@@ -109,9 +121,17 @@ const childrenToStringToBuffer = (children, buffer) => {
109
121
  continue;
110
122
  } else if (child instanceof JSXNode) {
111
123
  child.toStringToBuffer(buffer);
112
- } else if (typeof child === "number" || child.isEscaped) {
124
+ } else if (typeof child === "number") {
125
+ ;
126
+ buffer[0] += child;
127
+ } else if (child.isEscaped) {
113
128
  ;
114
129
  buffer[0] += child;
130
+ const callbacks = child.callbacks;
131
+ if (callbacks) {
132
+ buffer.callbacks ||= [];
133
+ buffer.callbacks.push(...callbacks);
134
+ }
115
135
  } else if (child instanceof Promise) {
116
136
  buffer.unshift("", child);
117
137
  } else {
@@ -125,7 +145,6 @@ class JSXNode {
125
145
  key;
126
146
  children;
127
147
  isEscaped = true;
128
- suspendedContext;
129
148
  constructor(tag, props, children) {
130
149
  if (typeof tag !== "function" && !(0, import_utils.isValidTagName)(tag)) {
131
150
  throw new Error(`Invalid JSX tag name: ${tag}`);
@@ -148,7 +167,7 @@ class JSXNode {
148
167
  this.toStringToBuffer(buffer);
149
168
  return buffer.length === 1 ? "callbacks" in buffer ? (0, import_html2.resolveCallbackSync)((0, import_html.raw)(buffer[0], buffer.callbacks)).toString() : buffer[0] : (0, import_html2.stringBufferToString)(buffer, buffer.callbacks);
150
169
  };
151
- return this.suspendedContext ? this.suspendedContext(render) : (0, import_context.runWithRenderContext)(render);
170
+ return (0, import_context.runWithRenderContext)(render);
152
171
  }
153
172
  toStringToBuffer(buffer) {
154
173
  const tag = this.tag;
@@ -222,21 +241,14 @@ class JSXFunctionNode extends JSXNode {
222
241
  return;
223
242
  } else if (res instanceof Promise) {
224
243
  if (import_context.globalContexts.length === 0) {
225
- buffer.unshift("", res);
244
+ buffer.unshift("", resolveFunctionComponentResult(res));
226
245
  } else {
227
- const suspendedContext = (0, import_context.captureRenderContext)();
228
- buffer.unshift(
229
- "",
230
- res.then((childRes) => {
231
- if (childRes instanceof JSXNode) {
232
- childRes.suspendedContext = suspendedContext;
233
- }
234
- return childRes;
235
- })
236
- );
246
+ buffer.unshift("", resolveFunctionComponentResult(res, (0, import_context.captureRenderContext)()));
237
247
  }
238
248
  } else if (res instanceof JSXNode) {
239
249
  res.toStringToBuffer(buffer);
250
+ } else if (Array.isArray(res)) {
251
+ childrenToStringToBuffer(res, buffer);
240
252
  } else if (typeof res === "number" || res.isEscaped) {
241
253
  buffer[0] += res;
242
254
  if (res.callbacks) {
@@ -315,15 +327,7 @@ const shallowEqual = (a, b) => {
315
327
  return true;
316
328
  };
317
329
  const memo = (component, propsAreEqual = shallowEqual) => {
318
- let computed = null;
319
- let prevProps = void 0;
320
- const wrapper = ((props) => {
321
- if (prevProps && !propsAreEqual(prevProps, props)) {
322
- computed = null;
323
- }
324
- prevProps = props;
325
- return computed ||= component(props);
326
- });
330
+ const wrapper = ((props) => component(props));
327
331
  wrapper[import_constants.DOM_MEMO] = propsAreEqual;
328
332
  wrapper[import_constants.DOM_RENDERER] = component;
329
333
  return wrapper;