[
  {
    "path": ".gitignore",
    "content": "# macOS system files\n.DS_Store\n\n# Python bytecode files\n__pycache__/\n*.pyc\n"
  },
  {
    "path": "README.md",
    "content": "# Agent Skills\n\nAgent Skills are folders of instructions, scripts, and resources that AI agents can discover and use to perform at specific tasks. Write once, use everywhere.\n\nCodex uses skills to help package capabilities that teams and individuals can use to complete specific tasks in a repeatable way. This repository catalogs skills for use and distribution with Codex.\n\nLearn more:\n- [Using skills in Codex](https://developers.openai.com/codex/skills)\n- [Create custom skills in Codex](https://developers.openai.com/codex/skills/create-skill)\n- [Agent Skills open standard](https://agentskills.io)\n\n## Installing a skill\n\nSkills in [`.system`](skills/.system/) are automatically installed in the latest version of Codex.\n\nTo install [curated](skills/.curated/) or [experimental](skills/.experimental/) skills, you can use the `$skill-installer` inside Codex.\n\nCurated skills can be installed by name (defaults to `skills/.curated`):\n\n```\n$skill-installer gh-address-comments\n```\n\nFor experimental skills, specify the skill folder. For example:\n\n```\n$skill-installer install the create-plan skill from the .experimental folder\n```\n\nOr provide the GitHub directory URL:\n\n```\n$skill-installer install https://github.com/openai/skills/tree/main/skills/.experimental/create-plan\n```\n\nAfter installing a skill, restart Codex to pick up new skills.\n\n## License\n\nThe license of an individual skill can be found directly inside the skill's directory inside the `LICENSE.txt` file.\n"
  },
  {
    "path": "contributing.md",
    "content": "## Contributing\n\n### Community values\n\n- **Be kind and inclusive.** Treat others with respect; we follow the [Contributor Covenant](https://www.contributor-covenant.org/).\n- **Assume good intent.** Written communication is hard - err on the side of generosity.\n- **Teach & learn.** If you spot something confusing, open an issue or PR with improvements.\n\n### Security & responsible AI\n\nHave you discovered a vulnerability or have concerns about model output? Please e-mail **security@openai.com** and we will respond promptly.\n"
  },
  {
    "path": "skills/.curated/aspnet-core/LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "skills/.curated/aspnet-core/SKILL.md",
    "content": "---\nname: aspnet-core\ndescription: Build, review, refactor, or architect ASP.NET Core web applications using current official guidance for .NET web development. Use when working on Blazor Web Apps, Razor Pages, MVC, Minimal APIs, controller-based Web APIs, SignalR, gRPC, middleware, dependency injection, configuration, authentication, authorization, testing, performance, deployment, or ASP.NET Core upgrades.\n---\n\n# ASP.NET Core\n\n## Overview\n\nChoose the right ASP.NET Core application model, compose the host and request pipeline correctly, and implement features in the framework style Microsoft documents today.\n\nLoad the smallest set of references that fits the task. Do not load every reference by default.\n\n## Workflow\n\n1. Confirm the target framework, SDK, and current app model.\n2. Open [references/stack-selection.md](references/stack-selection.md) first for new apps or major refactors.\n3. Open [references/program-and-pipeline.md](references/program-and-pipeline.md) next for `Program.cs`, DI, configuration, middleware, routing, logging, and static assets.\n4. Open exactly one primary app-model reference:\n   - [references/ui-blazor.md](references/ui-blazor.md)\n   - [references/ui-razor-pages.md](references/ui-razor-pages.md)\n   - [references/ui-mvc.md](references/ui-mvc.md)\n   - [references/apis-minimal-and-controllers.md](references/apis-minimal-and-controllers.md)\n5. Add cross-cutting references only as needed:\n   - [references/data-state-and-services.md](references/data-state-and-services.md)\n   - [references/security-and-identity.md](references/security-and-identity.md)\n   - [references/realtime-grpc-and-background-work.md](references/realtime-grpc-and-background-work.md)\n   - [references/testing-performance-and-operations.md](references/testing-performance-and-operations.md)\n6. Open [references/versioning-and-upgrades.md](references/versioning-and-upgrades.md) before introducing new platform APIs into an older solution or when migrating between major versions.\n7. Use [references/source-map.md](references/source-map.md) when you need the Microsoft Learn section that corresponds to a task not already covered by the focused references.\n\n## Default Operating Assumptions\n\n- Prefer the latest stable ASP.NET Core and .NET unless the repository or user request pins an older target.\n- As of March 2026, prefer .NET 10 / ASP.NET Core 10 for new production work. Treat ASP.NET Core 11 as preview unless the user explicitly asks for preview features.\n- Prefer `WebApplicationBuilder` and `WebApplication`. Avoid older `Startup` and `WebHost` patterns unless the codebase already uses them or the task is migration.\n- Prefer built-in DI, options/configuration, logging, ProblemDetails, OpenAPI, health checks, rate limiting, output caching, and Identity before adding third-party infrastructure.\n- Keep feature slices cohesive so the page, component, endpoint, controller, validation, service, data access, and tests are easy to trace.\n- Respect the existing app model. Do not rewrite Razor Pages to MVC or controllers to Minimal APIs without a clear reason.\n\n## Reference Guide\n\n- [references/_sections.md](references/_sections.md): Quick index and reading order.\n- [references/stack-selection.md](references/stack-selection.md): Choose the right ASP.NET Core application model and template.\n- [references/program-and-pipeline.md](references/program-and-pipeline.md): Structure `Program.cs`, services, middleware, routing, configuration, logging, and static assets.\n- [references/ui-blazor.md](references/ui-blazor.md): Build Blazor Web Apps, choose render modes, and use components, forms, and JS interop correctly.\n- [references/ui-razor-pages.md](references/ui-razor-pages.md): Build page-focused server-rendered apps with handlers, model binding, and conventions.\n- [references/ui-mvc.md](references/ui-mvc.md): Build controller/view applications with clear separation of concerns.\n- [references/apis-minimal-and-controllers.md](references/apis-minimal-and-controllers.md): Build HTTP APIs with Minimal APIs or controllers, including validation and response patterns.\n- [references/data-state-and-services.md](references/data-state-and-services.md): Use EF Core, `DbContext`, options, `IHttpClientFactory`, session, temp data, and app state responsibly.\n- [references/security-and-identity.md](references/security-and-identity.md): Apply authentication, authorization, Identity, secrets, data protection, CORS, CSRF, and HTTPS guidance.\n- [references/realtime-grpc-and-background-work.md](references/realtime-grpc-and-background-work.md): Use SignalR, gRPC, and hosted services.\n- [references/testing-performance-and-operations.md](references/testing-performance-and-operations.md): Add integration tests, browser tests, caching, compression, health checks, rate limits, and deployment concerns.\n- [references/versioning-and-upgrades.md](references/versioning-and-upgrades.md): Handle target frameworks, breaking changes, obsolete APIs, and migrations.\n- [references/source-map.md](references/source-map.md): Map the official ASP.NET Core documentation tree to the references in this skill.\n\n## Execution Notes\n\n- When generating new code, start from the correct `dotnet new` template and keep the generated structure recognizable.\n- When editing an existing solution, follow the solution's conventions first and use these references to avoid framework misuse or outdated patterns.\n- When a task mentions \"latest\", verify the feature on Microsoft Learn or the ASP.NET Core docs repo before relying on memory.\n"
  },
  {
    "path": "skills/.curated/aspnet-core/agents/openai.yaml",
    "content": "interface:\n  display_name: \"ASP.NET Core\"\n  short_description: \"[Windows only] Build and review ASP.NET Core web apps\"\n  icon_large: \"./assets/dotnet-logo.png\"\n  default_prompt: \"Create a new $aspnet-core website for me.\"\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/_sections.md",
    "content": "# Reference Sections\n\nUse this file as the routing table for the rest of the skill.\n\n## Start Here\n\n- New app or major redesign: `stack-selection.md` -> `program-and-pipeline.md` -> one primary app-model reference -> `security-and-identity.md` -> `testing-performance-and-operations.md`\n- Existing app feature work: primary app-model reference -> `program-and-pipeline.md` -> any needed cross-cutting references\n- API-first work: `apis-minimal-and-controllers.md` -> `security-and-identity.md` -> `data-state-and-services.md` -> `testing-performance-and-operations.md`\n- Authentication, authorization, or secrets: `security-and-identity.md`\n- Realtime, streaming, or background processing: `realtime-grpc-and-background-work.md`\n- Upgrade or migration work: `versioning-and-upgrades.md`\n\n## Primary References\n\n| File | Open when |\n| --- | --- |\n| `stack-selection.md` | Choose Blazor, Razor Pages, MVC, Minimal APIs, controllers, SignalR, or gRPC |\n| `program-and-pipeline.md` | Structure `Program.cs`, services, configuration, middleware, routing, logging, static files, and app startup |\n| `ui-blazor.md` | Build or review Blazor Web Apps and component-based UI |\n| `ui-razor-pages.md` | Build or review page-focused server-rendered applications |\n| `ui-mvc.md` | Build or review controller/view applications |\n| `apis-minimal-and-controllers.md` | Build or review HTTP APIs |\n\n## Cross-Cutting References\n\n| File | Open when |\n| --- | --- |\n| `data-state-and-services.md` | Register services, use EF Core, handle options/configuration, or manage app state |\n| `security-and-identity.md` | Add Identity, cookies, bearer auth, policies, CORS, CSRF, HTTPS, or secrets handling |\n| `realtime-grpc-and-background-work.md` | Add SignalR, gRPC, streaming, or hosted services |\n| `testing-performance-and-operations.md` | Add tests, caching, compression, health checks, rate limits, deployment, or proxy configuration |\n| `versioning-and-upgrades.md` | Migrate across ASP.NET Core versions, avoid obsolete APIs, or target preview features deliberately |\n| `source-map.md` | Map a task to the official ASP.NET Core documentation tree |\n\n## Reading Strategy\n\n- Open one app-model reference at a time unless the codebase genuinely mixes models.\n- Prefer the framework's built-in abstractions first.\n- Check `versioning-and-upgrades.md` before introducing APIs that might not exist in the repository's target framework.\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/apis-minimal-and-controllers.md",
    "content": "# APIs: Minimal And Controllers\n\nPrimary docs:\n- https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis\n- https://learn.microsoft.com/aspnet/core/web-api/\n- https://learn.microsoft.com/aspnet/core/fundamentals/error-handling-api\n\n## First Decision\n\nChoose between:\n\n- Minimal APIs for focused, low-ceremony HTTP endpoints\n- controller-based APIs for richer MVC conventions and attribute-driven behavior\n\nDo not mix both styles in the same feature unless that split is genuinely useful.\n\n## Minimal API Guidance\n\nPrefer Minimal APIs when the surface is small to medium and you want concise endpoint definitions.\n\nGood defaults:\n\n- organize endpoints with route groups\n- keep route handlers thin\n- move business logic into services\n- prefer `TypedResults` over untyped results\n- use endpoint filters when cross-cutting behavior belongs at the endpoint layer\n- use built-in validation support on supported target frameworks\n\nMinimal API reminders:\n\n- handler parameters can be bound from route, query, headers, body, form, or DI\n- authorization can be applied with `RequireAuthorization`\n- return `IResult` or `TypedResults` when response shape matters\n- use OpenAPI support for discoverable contracts\n\nOn .NET 10, Minimal APIs support built-in validation with `AddValidation()`. Use that instead of inventing parallel validation infrastructure when the target framework supports it.\n\n## Controller API Guidance\n\nPrefer controllers when the API needs:\n\n- `[ApiController]` behaviors\n- attribute routing and conventions\n- filters\n- custom formatters\n- mature controller organization in an existing codebase\n\nController defaults:\n\n- derive API controllers from `ControllerBase`\n- annotate with `[ApiController]`\n- use attribute routing\n- return ProblemDetails-compatible failures\n- let automatic model validation handle invalid requests unless there is a concrete override requirement\n\nKey `[ApiController]` behaviors:\n\n- attribute routing is required\n- invalid model state automatically becomes HTTP 400\n- binding source inference applies\n- error responses use ProblemDetails patterns\n\n## Shared API Practices\n\n- Keep request and response DTOs separate from persistence models\n- Use version-stable route and payload contracts\n- Use `CreatedAt...` patterns for resource creation\n- Prefer explicit status codes and typed results over implicit behavior\n- Apply authorization at the endpoint or controller boundary, not only inside service methods\n- Use `ProblemDetails` for errors instead of ad hoc JSON shapes\n\n## Browser-Facing Notes\n\n- Be careful with cookie-authenticated API endpoints and CORS\n- For browser-based form or file upload endpoints, account for antiforgery requirements\n- In ASP.NET Core 10, known API endpoints no longer use cookie-login redirects by default; rely on API-appropriate unauthorized responses instead\n\n## Native AOT\n\nUse `dotnet new webapiaot` only when native AOT is an explicit deployment requirement. Treat it as a constraint that affects library choice, reflection, JSON patterns, and compatibility.\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/data-state-and-services.md",
    "content": "# Data, State, And Services\n\nPrimary docs:\n- https://learn.microsoft.com/aspnet/core/data/\n- https://learn.microsoft.com/aspnet/core/fundamentals/dependency-injection\n- https://learn.microsoft.com/aspnet/core/fundamentals/http-requests\n- https://learn.microsoft.com/aspnet/core/fundamentals/app-state\n\n## Dependency Injection Defaults\n\n- Register infrastructure and business services in `Program.cs`\n- Inject dependencies through constructors by default\n- Keep scoped services request-bound\n- Avoid resolving scoped services from singletons\n- Use keyed or named patterns only when there is a real need for multiple implementations\n\n## EF Core And DbContext\n\nUse EF Core for common relational data access patterns unless the repository already uses another data layer.\n\nDefault guidance:\n\n- register `DbContext` with `AddDbContext`\n- treat `DbContext` as scoped\n- keep queries and transactions in services, not UI code\n- use migrations intentionally\n- keep entities out of public API contracts and UI view models\n\nUse `IDbContextFactory<TContext>` when the execution model is not request-scoped, such as:\n\n- Blazor components with longer-lived scopes\n- background services\n- explicit factory-driven data work\n\n## Options And Configuration\n\n- Bind structured configuration into options classes\n- validate options early when bad configuration should fail fast\n- keep configuration access close to the service that owns it\n- avoid scattering raw configuration keys across the codebase\n\n## Outbound HTTP\n\nUse `IHttpClientFactory` for outbound HTTP calls.\n\nPrefer:\n\n- named clients for distinct external systems\n- typed clients for richer integrations\n- delegating handlers for retries, headers, or telemetry concerns\n\nAvoid manual `new HttpClient()` patterns scattered through request handlers.\n\n## App State\n\nUse the smallest state mechanism that fits:\n\n- query string or route values for transparent request state\n- form posts for user input\n- TempData for short-lived redirect-friendly messages\n- session only when necessary and with an understanding of its server-side and scaling implications\n\nDo not treat session as the primary application data store.\n\n## Caching And State Boundaries\n\n- Keep cached data derivable from a durable source\n- Separate cache shape from persistence shape when it improves safety or performance\n- Revisit session, in-memory cache, and singleton state when the app scales to multiple instances\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/program-and-pipeline.md",
    "content": "# Program And Pipeline\n\nPrimary docs:\n- https://learn.microsoft.com/aspnet/core/fundamentals/\n- https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/webapplication\n- https://learn.microsoft.com/aspnet/core/fundamentals/middleware/\n- https://learn.microsoft.com/aspnet/core/fundamentals/configuration/\n\n## Startup Shape\n\nPrefer the modern hosting model:\n\n1. Create `var builder = WebApplication.CreateBuilder(args);`\n2. Register services on `builder.Services`\n3. Build `var app = builder.Build();`\n4. Configure middleware in the correct order\n5. Map endpoints\n6. Call `app.Run();`\n\nUse older `Startup` patterns only when the repository already uses them or the task is migration.\n\n## Service Registration\n\n- Register framework services explicitly: Razor Pages, controllers, Razor components, authentication, authorization, health checks, rate limiting, response compression, output caching, EF Core, and `IHttpClientFactory`\n- Keep business logic in services instead of controllers, page models, or route handlers\n- Use constructor injection as the default\n- Use options classes for structured configuration\n- Choose lifetimes intentionally:\n  - singleton: stateless or shared infrastructure\n  - scoped: request-bound work such as `DbContext`\n  - transient: lightweight stateless services\n\n## Configuration Defaults\n\n`WebApplication.CreateBuilder` already loads configuration from common providers such as:\n\n- `appsettings.json`\n- environment-specific `appsettings.{Environment}.json`\n- environment variables\n- command-line arguments\n\nFor secrets:\n\n- use Secret Manager in development\n- use a secure external store in production\n- do not commit secrets to source control\n\n## Middleware Order\n\nMiddleware order is a frequent source of broken behavior. Favor this shape and adjust only with a concrete reason:\n\n1. Forwarded headers if behind a proxy or load balancer\n2. Exception handling and HSTS for non-development environments\n3. HTTPS redirection\n4. Static files\n5. Routing when explicit routing middleware is needed\n6. CORS when endpoints require it\n7. Authentication\n8. Authorization\n9. Endpoint-specific middleware such as rate limiting or session as required\n10. Endpoint mapping with `MapRazorPages`, `MapControllers`, `MapGet`, `MapHub`, or `MapGrpcService`\n\nImportant ordering rules:\n\n- Call `UseAuthentication()` before `UseAuthorization()`\n- Keep proxy/header processing before auth, redirects, and link generation\n- Do not insert custom middleware randomly between auth and authorization without a reason\n- In Minimal API apps, explicit `UseRouting()` is usually unnecessary unless you need to control order\n\n## Routing And Endpoints\n\n- Prefer endpoint routing everywhere\n- Use route groups for larger Minimal API surfaces\n- Keep MVC and API routes explicit and predictable\n- Use areas only when the application is large enough to benefit from bounded sections\n- Keep endpoint names stable when generating links or integrating with clients\n\n## Error Handling\n\n- Use centralized exception handling instead of scattered `try/catch` blocks for ordinary request failures\n- Prefer ProblemDetails-style responses for APIs\n- Keep the developer exception page limited to development\n- Separate user-facing failures from internal exception details\n\n## Logging And Diagnostics\n\n- Use `ILogger<T>` from DI\n- Log structured values, not concatenated strings\n- Put correlation and request diagnostics in middleware or infrastructure, not business logic\n- Enable HTTP logging only when the scenario warrants it and avoid leaking sensitive data\n\n## Static Assets And Web Root\n\n- Keep public assets in `wwwroot`\n- Treat the web root as publicly readable content\n- Prevent publishing local-only static content through project file rules when needed\n- Use Razor Class Libraries for reusable UI assets across apps\n\n## Architectural Defaults\n\n- Keep `Program.cs` readable; extract feature registration to extension methods when it starts accumulating unrelated concerns\n- Prefer vertical slices or feature folders over giant \"Controllers\", \"Services\", and \"Repositories\" buckets with weak boundaries\n- Keep framework configuration close to the host and business logic out of it\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/realtime-grpc-and-background-work.md",
    "content": "# Realtime, gRPC, And Background Work\n\nPrimary docs:\n- https://learn.microsoft.com/aspnet/core/signalr/introduction\n- https://learn.microsoft.com/aspnet/core/grpc/\n- https://learn.microsoft.com/aspnet/core/fundamentals/host/hosted-services\n\n## SignalR\n\nUse SignalR when the server must push updates to connected clients in near real time.\n\nGood fits:\n\n- chat\n- dashboards\n- notifications\n- collaborative editing\n- live status streams\n\nGuidance:\n\n- model the hub as a communication boundary, not the home of business logic\n- use groups and user targeting deliberately\n- authenticate connections when data is user-specific\n- plan for scale-out if the app may run on multiple instances\n\nRemember that Blazor interactive server rendering already relies on a real-time connection. Do not add a second realtime channel unless the feature truly needs one.\n\n## gRPC\n\nUse gRPC for efficient service-to-service communication, strongly typed contracts, and streaming over HTTP/2.\n\nPrefer gRPC when:\n\n- both ends are under your control\n- performance and contract fidelity matter\n- streaming is a first-class requirement\n\nGuidance:\n\n- keep `.proto` contracts versioned and stable\n- generate client and server types from contracts\n- keep auth, logging, and DI integrated with the host\n- account for browser interoperability differences before choosing gRPC for public browser clients\n\n## Background Work\n\nUse `IHostedService` or `BackgroundService` for in-process background tasks tied to the application host.\n\nDefaults:\n\n- keep background services small and observable\n- create scopes for scoped dependencies\n- do not capture scoped services directly in singleton hosted services\n- respect cancellation tokens\n- avoid long blocking startup paths\n\nIf the work is durable, high-volume, or business-critical, consider whether it belongs in an out-of-process queue or worker instead of only inside the web host.\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/security-and-identity.md",
    "content": "# Security And Identity\n\nPrimary docs:\n- https://learn.microsoft.com/aspnet/core/security/\n- https://learn.microsoft.com/aspnet/core/security/authentication/identity\n- https://learn.microsoft.com/aspnet/core/security/authorization/introduction\n\n## Security Defaults\n\n- Use the most secure authentication flow available\n- Keep secrets out of source code and plain configuration files\n- Use Secret Manager in development\n- Use a secure production secret store\n- Enforce HTTPS\n- Apply least privilege to users, services, and data access\n\n## Authentication And Authorization\n\nAuthentication answers who the user or caller is. Authorization answers what they can do.\n\nDefault pipeline order:\n\n1. `UseAuthentication()`\n2. `UseAuthorization()`\n\nApply authorization at boundaries:\n\n- `[Authorize]` on controllers, actions, page models, or hubs\n- `RequireAuthorization()` on endpoints and route groups\n- policies for reusable rules\n- roles only when role-based checks are actually the right abstraction\n\nUse `AllowAnonymous` sparingly and intentionally.\n\n## Identity\n\nUse ASP.NET Core Identity when the app needs first-party user accounts, login flows, password management, email confirmation, MFA, or related account management.\n\nUseful starting points:\n\n- `dotnet new webapp -au Individual`\n- `dotnet new mvc -au Individual`\n\nIdentity guidance:\n\n- scaffold only the pages you truly need to customize\n- keep Identity UI updates maintainable; full scaffolding increases merge and upgrade cost\n- use policies and claims for authorization rather than encoding all decisions in page logic\n- persist data-protection keys appropriately in multi-instance deployments\n\nOn ASP.NET Core 10, Identity metrics are available for observing auth-related behavior. Use them when the app has meaningful authentication traffic or security monitoring requirements.\n\n## CSRF, CORS, And Browser Security\n\n- Use antiforgery protection for cookie-based interactive apps and form posts\n- Do not confuse CORS with authentication or authorization\n- Avoid permissive `AllowAnyOrigin` plus credentials combinations\n- Treat browser-side state as untrusted\n\n## HTTPS, HSTS, And Forwarded Headers\n\n- redirect HTTP to HTTPS\n- enable HSTS outside development when appropriate\n- configure forwarded headers correctly when behind proxies or load balancers\n- do not generate links or evaluate scheme-sensitive behavior before proxy headers are processed\n\n## Data Protection And Secrets\n\n- persist data-protection keys outside ephemeral local storage when the app runs on multiple instances\n- do not use environment variables as the preferred long-term home for production secrets when a stronger secret store is available\n- never check production credentials into source control\n\n## Blazor Note\n\nFor Blazor apps, read the general ASP.NET Core security guidance first and then the Blazor-specific security docs. Some Blazor security guidance adds to or supersedes the general guidance.\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/source-map.md",
    "content": "# ASP.NET Core Source Map\n\nThis skill is synthesized from the official ASP.NET Core documentation tree and overview pages. Use this file to map a task to the corresponding Microsoft Learn area before opening deeper docs.\n\nCore sources:\n\n- https://learn.microsoft.com/aspnet/core/\n- https://raw.githubusercontent.com/dotnet/AspNetCore.Docs/main/aspnetcore/toc.yml\n- https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore\n\n## Documentation Tree Mapping\n\n| ASP.NET Core docs area | Use this skill reference first |\n| --- | --- |\n| Overview, Get started, What's new | `stack-selection.md`, `versioning-and-upgrades.md` |\n| Fundamentals | `program-and-pipeline.md` |\n| Web apps | `ui-blazor.md`, `ui-razor-pages.md`, `ui-mvc.md` |\n| APIs | `apis-minimal-and-controllers.md` |\n| Real-time apps | `realtime-grpc-and-background-work.md` |\n| Remote Procedure Call apps | `realtime-grpc-and-background-work.md` |\n| Servers, Host and deploy | `testing-performance-and-operations.md` |\n| Test, Debug, Troubleshoot | `testing-performance-and-operations.md` |\n| Data access | `data-state-and-services.md` |\n| Security and Identity | `security-and-identity.md` |\n| Performance | `testing-performance-and-operations.md` |\n| Migration and updates | `versioning-and-upgrades.md` |\n\n## Areas To Consult Directly On Microsoft Learn\n\nThe following topics are part of the ASP.NET Core documentation tree but are not expanded into their own dedicated reference file here:\n\n- globalization and localization\n- advanced hosting and YARP details\n- debugger and diagnostics tooling specifics\n- narrow API-reference pages for individual types\n\nWhen a task is dominated by one of those areas, go straight to the matching Microsoft Learn section after checking the reference files in this skill.\n\n## Practical Deep-Dive Rule\n\n- Start with the focused reference in this skill\n- If the task depends on a narrow platform detail, open the matching Learn article\n- If the task depends on version-specific behavior, confirm the correct moniker or breaking-changes page\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/stack-selection.md",
    "content": "# Stack Selection\n\nPrimary docs:\n- https://learn.microsoft.com/aspnet/core/\n- https://learn.microsoft.com/aspnet/core/blazor/\n- https://learn.microsoft.com/aspnet/core/razor-pages/\n- https://learn.microsoft.com/aspnet/core/mvc/overview\n- https://learn.microsoft.com/aspnet/core/web-api/\n- https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis\n\n## Default Version Choice\n\n- Prefer the latest stable .NET and ASP.NET Core for new production work.\n- As of March 2026, that means `net10.0` unless the repository or user request says otherwise.\n- Treat ASP.NET Core 11 as preview. Do not adopt preview APIs by default.\n- If the repository already targets `net8.0`, `net9.0`, or another framework, stay within that target unless the task is explicitly an upgrade.\n\n## Template Short Names\n\nThe current .NET 10 SDK templates include:\n\n- `dotnet new blazor`\n- `dotnet new webapp`\n- `dotnet new mvc`\n- `dotnet new webapi`\n- `dotnet new webapiaot`\n- `dotnet new grpc`\n- `dotnet new web`\n- `dotnet new razorclasslib`\n\nVerify template names with `dotnet new list` if the environment differs.\n\n## Application Model Matrix\n\n| Model | Prefer when | Watch out for | Typical starting point |\n| --- | --- | --- | --- |\n| Blazor Web App | Build full-stack .NET UI with SSR plus optional interactivity | Interactive server needs a live connection; WebAssembly increases payload size | `dotnet new blazor` |\n| Razor Pages | Build page-focused CRUD, forms, dashboards, and line-of-business apps | Authorization cannot be applied per page handler; use MVC if handler-level control matters | `dotnet new webapp` |\n| MVC | Build large server-rendered apps with clear controller/view separation, filters, and action-based patterns | More ceremony than Razor Pages for simple page flows | `dotnet new mvc` |\n| Minimal APIs | Build focused HTTP APIs, internal services, lightweight backends, and small surface areas | Route handlers can become hard to manage if business logic or metadata grows without structure | `dotnet new webapi` or `dotnet new web` |\n| Controller-based Web API | Build APIs that benefit from `[ApiController]`, content negotiation, filters, formatters, and mature controller conventions | More ceremony than Minimal APIs for small endpoints | `dotnet new webapi` |\n| SignalR | Add server push, live updates, chat, collaborative UI, or notifications | Requires connection lifecycle management and scale-out planning | Add to an existing ASP.NET Core app |\n| gRPC | Build service-to-service or streaming RPC over HTTP/2 | Browser support is different from ordinary JSON APIs; use gRPC-Web only when needed | `dotnet new grpc` |\n\n## Fast Heuristics\n\n- Choose Blazor Web App when the UI itself should be a .NET component model.\n- Choose Razor Pages when the app is mostly page and form oriented.\n- Choose MVC when actions, views, filters, and controller conventions are the center of the design.\n- Choose Minimal APIs first for small to medium HTTP services.\n- Switch to controllers when the API needs richer attribute-driven behavior, custom formatters, or strong alignment with existing MVC/Web API conventions.\n- Keep the current app model in an existing codebase unless the mismatch is causing real complexity.\n\n## Mixed-Model Guidance\n\nASP.NET Core can mix models in one host. Common combinations:\n\n- Razor Pages or MVC for server-rendered UI plus Minimal APIs for AJAX or mobile endpoints\n- Blazor Web App plus Minimal APIs for external integration endpoints\n- MVC or Razor Pages plus SignalR for live updates\n- Web API plus gRPC for internal service-to-service calls\n\nMix models only when it simplifies the public surface. Do not add a second app model just because ASP.NET Core allows it.\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/testing-performance-and-operations.md",
    "content": "# Testing, Performance, And Operations\n\nPrimary docs:\n- https://learn.microsoft.com/aspnet/core/test/integration-tests\n- https://learn.microsoft.com/aspnet/core/host-and-deploy/\n- https://learn.microsoft.com/aspnet/core/host-and-deploy/health-checks\n- https://learn.microsoft.com/aspnet/core/performance/\n\n## Testing Strategy\n\nUse layered testing instead of relying on one style:\n\n- unit tests for pure services and business logic\n- integration tests for request pipeline, DI, database, auth, and framework wiring\n- browser tests for end-to-end user flows\n\n## Integration Tests\n\nUse `Microsoft.AspNetCore.Mvc.Testing` and `WebApplicationFactory<Program>` for integration tests.\n\nGuidance from the official docs:\n\n- use a test host and `HttpClient`\n- replace services with test doubles when needed\n- control redirects when asserting auth behavior\n- handle antiforgery correctly for form posts\n- prefer SQLite in-memory over the EF Core in-memory provider for more realistic database tests\n\nFor SPA or browser-driven scenarios, Microsoft recommends browser automation such as Playwright for .NET.\n\n## Performance Defaults\n\nReach for built-in features before custom optimization layers:\n\n- output caching\n- response caching where appropriate\n- response compression\n- HTTP request timeouts\n- rate limiting\n- static file handling\n\nGeneral performance guidance:\n\n- measure first\n- keep database and network round trips visible\n- reduce payload size\n- use streaming or pagination when data is large\n- keep synchronous blocking out of hot paths\n\n## Health Checks And Observability\n\nAdd health checks for dependencies that matter operationally.\n\nUse separate checks or tags when you need:\n\n- liveness\n- readiness\n- dependency-specific health surfaces\n\nAlso ensure:\n\n- structured logs\n- request tracing where applicable\n- metrics for critical paths such as auth, API latency, and background work\n\n## Hosting And Deployment\n\nTypical deployment flow:\n\n1. `dotnet publish`\n2. deploy the publish output\n3. run behind a process manager\n4. place a reverse proxy in front when the environment requires it\n\nKnow the deployment environment:\n\n- IIS or Windows Service on Windows\n- Kestrel plus Nginx or another reverse proxy on Linux\n- container hosting when the platform expects it\n\nBehind proxies or load balancers:\n\n- configure forwarded headers\n- validate scheme, host, and remote IP behavior\n- test auth redirects and callback URLs in the deployed topology\n\n## Operational Safeguards\n\n- add health checks for databases and critical external services\n- fail fast on invalid configuration where possible\n- keep secrets out of publish artifacts\n- verify data-protection key persistence in multi-instance deployments\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/ui-blazor.md",
    "content": "# Blazor\n\nPrimary docs:\n- https://learn.microsoft.com/aspnet/core/blazor/\n- https://learn.microsoft.com/aspnet/core/blazor/fundamentals/\n- https://learn.microsoft.com/aspnet/core/blazor/security/\n\n## Choose Blazor Deliberately\n\nPrefer Blazor when the UI itself should be built as reusable .NET components and the team wants a full-stack .NET model.\n\nCurrent guidance centers on the Blazor Web App model, which can combine:\n\n- static SSR for fast first render\n- interactive server rendering\n- interactive WebAssembly rendering\n- per-component render mode choices\n\nUse standalone Blazor WebAssembly only when the app is intentionally client-heavy or must run as static files without a server-rendered host.\n\n## Render Mode Heuristics\n\n- Start with static SSR when the page is mostly read-only and fast first paint matters\n- Use interactive server rendering when you want rich interactivity without shipping the full .NET runtime to the browser\n- Use interactive WebAssembly when offline capability, client-side execution, or browser-local compute is the point\n- Mix render modes only when the split is clear and justified\n\n## Component Patterns\n\n- Keep components focused and composable\n- Move data access and business rules into injected services\n- Pass data through parameters, not hidden global state\n- Use forms and validation with Blazor's built-in editing and validation components\n- Prefer shared Razor Class Libraries for reusable component sets\n\n## Data And Interactivity\n\n- Use DI in components with restraint; avoid turning components into service locators\n- Treat JS interop as an edge mechanism for browser APIs or third-party libraries, not the primary application model\n- Keep long-running work off the UI event path\n- Be deliberate about prerendering, streaming rendering, and enhanced navigation when they improve perceived performance\n\n## Security Notes\n\n- Follow the general ASP.NET Core security guidance first, then load the Blazor-specific docs for details that supersede it\n- Remember that client-side code and browser state are not trusted\n- Keep secrets and privileged operations on the server\n- Use authorization-aware UI only as a convenience layer; enforce rules on the server as well\n\n## When Not To Use Blazor\n\n- Do not force Blazor onto a mostly conventional server-rendered app that already fits Razor Pages or MVC well\n- Do not choose WebAssembly by default for small interaction needs that SSR or interactive server rendering handles more simply\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/ui-mvc.md",
    "content": "# MVC\n\nPrimary docs:\n- https://learn.microsoft.com/aspnet/core/mvc/overview\n- https://learn.microsoft.com/aspnet/core/mvc/controllers/\n- https://learn.microsoft.com/aspnet/core/mvc/views/\n\n## Choose MVC When Actions And Views Matter\n\nPrefer MVC when the application benefits from explicit controllers, action-based routing, filters, view models, and a strong separation between orchestration and presentation.\n\nThis is often the right fit for:\n\n- large server-rendered sites\n- applications with many cross-cutting filters or action conventions\n- applications that mix views and APIs in the same controller layer\n- teams already organized around controllers and views\n\n## Core Shape\n\nEnable MVC with views using:\n\n- `builder.Services.AddControllersWithViews();`\n- `app.MapControllerRoute(...)`\n\nKeep views focused on presentation. Keep controllers focused on HTTP orchestration. Put business rules in services.\n\n## Controller Guidance\n\n- Derive from `Controller` when the controller returns views\n- Keep actions small and explicit\n- Use model binding and validation instead of manual request parsing\n- Return view models, not EF entities, to views\n- Use POST-Redirect-GET for form submissions\n\n## View Guidance\n\n- Use layouts, partial views, and Tag Helpers to keep markup consistent\n- Keep complex display logic out of Razor markup when it becomes hard to follow\n- Use strongly typed view models\n- Avoid coupling views directly to persistence models\n\n## Structure And Scale\n\n- Use areas for large bounded sections such as Admin or BackOffice\n- Keep route conventions explicit\n- Apply filters when behavior truly belongs at the MVC layer\n- Avoid giant god controllers; split by cohesive feature or resource\n\n## Choosing MVC Over Razor Pages\n\nPrefer MVC over Razor Pages when:\n\n- multiple related actions share controller-level behavior\n- handler-level authorization or action filters matter\n- URL and action design are more natural than page-file routing\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/ui-razor-pages.md",
    "content": "# Razor Pages\n\nPrimary docs:\n- https://learn.microsoft.com/aspnet/core/razor-pages/\n- https://learn.microsoft.com/aspnet/core/tutorials/razor-pages/\n\n## Choose Razor Pages For Page-Centered Apps\n\nPrefer Razor Pages when requests naturally map to pages, forms, and page-level handlers. This is a strong default for internal tools, CRUD apps, account flows, and admin surfaces.\n\n## Core Shape\n\nEnable Razor Pages with:\n\n- `builder.Services.AddRazorPages();`\n- `app.MapRazorPages();`\n\nUse the `@page` directive to turn a `.cshtml` file into an endpoint. Keep request logic in the paired `PageModel` class when the page is more than trivial.\n\n## Routing Model\n\n- File system location defines the route by default\n- `Pages/Index.cshtml` maps to `/`\n- `Pages/Store/Index.cshtml` maps to `/Store`\n- Keep folder structure meaningful because it becomes the URL structure\n\n## PageModel Guidance\n\n- Use `OnGet`, `OnPost`, and named handlers for request processing\n- Use bindable properties and model validation for forms\n- Keep page models thin; move business logic into injected services\n- Use Tag Helpers and model binding instead of manual request parsing\n\n## Good Fits\n\n- form-heavy workflows\n- dashboards and back-office applications\n- simple content with server-side validation\n- applications where a page is the primary navigation unit\n\n## Key Limitation\n\nDo not rely on per-handler authorization with Razor Pages. Microsoft explicitly recommends using MVC controllers when different handlers on the same logical surface need different authorization behavior.\n\nPreferred responses to that limitation:\n\n- split the handlers into separate pages\n- move the surface to MVC if action-level authorization is a better fit\n\n## Organizational Guidance\n\n- Group related pages into folders\n- Use partial views for repeated fragments\n- Use areas only when the application has clear bounded sections\n- Keep shared layout and page conventions centralized\n"
  },
  {
    "path": "skills/.curated/aspnet-core/references/versioning-and-upgrades.md",
    "content": "# Versioning And Upgrades\n\nPrimary docs:\n- https://learn.microsoft.com/aspnet/core/release-notes/\n- https://learn.microsoft.com/aspnet/core/release-notes/aspnetcore-10.0\n- https://learn.microsoft.com/aspnet/core/release-notes/aspnetcore-9.0\n- https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/breaking-changes\n\n## Versioning Default\n\n- For new production apps in March 2026, prefer `net10.0`\n- For existing apps, match the repository's target framework unless the task is explicitly an upgrade\n- Before using a new API, confirm it exists in the target framework\n\n## Upgrade Workflow\n\n1. Identify the current target framework and SDK\n2. Read the \"What's new\" and breaking-changes pages for each version hop\n3. Compile and resolve obsoletions intentionally\n4. Re-run integration tests and auth flows\n5. Re-test deployment-specific behavior such as proxies, cookies, and static assets\n\n## High-Value Breaking-Change Checks\n\nWhen moving to ASP.NET Core 10, watch for:\n\n- cookie login redirects disabled for known API endpoints\n- `WithOpenApi` deprecation\n- `WebHostBuilder`, `IWebHost`, and `WebHost` obsolescence\n- Razor runtime compilation obsolescence\n\nWhen moving to ASP.NET Core 9, watch for:\n\n- `ValidateOnBuild` and `ValidateScopes` enabled in development when using `HostBuilder`\n- middleware constructor expectations and DI validation changes\n\nWhen moving to ASP.NET Core 8, watch for:\n\n- Minimal API `IFormFile` antiforgery requirements\n- `AddRateLimiter()` and `AddHttpLogging()` requirements when corresponding middleware is used\n\n## Migration Principles\n\n- Prefer migration to the modern hosting model when touching startup extensively\n- Remove compatibility shims only after tests confirm behavior\n- Avoid mixing new framework idioms with old startup architecture in a half-migrated state\n- Keep one authoritative target framework in project files unless multi-targeting is deliberate\n\n## Preview Feature Rule\n\nDo not introduce preview-only APIs or docs guidance unless the user explicitly asks for preview adoption or the repository is already on preview SDKs.\n"
  },
  {
    "path": "skills/.curated/chatgpt-apps/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don't include\n   the brackets!) The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/chatgpt-apps/SKILL.md",
    "content": "---\nname: chatgpt-apps\ndescription: Build, scaffold, refactor, and troubleshoot ChatGPT Apps SDK applications that combine an MCP server and widget UI. Use when Codex needs to design tools, register UI resources, wire the MCP Apps bridge or ChatGPT compatibility APIs, apply Apps SDK metadata or CSP or domain settings, or produce a docs-aligned project scaffold. Prefer a docs-first workflow by invoking the openai-docs skill or OpenAI developer docs MCP tools before generating code.\n---\n\n# ChatGPT Apps\n\n## Overview\n\nScaffold ChatGPT Apps SDK implementations with a docs-first, example-first workflow, then generate code that follows current Apps SDK and MCP Apps bridge patterns.\n\nUse this skill to produce:\n\n- A primary app-archetype classification and repo-shape decision\n- A tool plan (names, schemas, annotations, outputs)\n- An upstream starting-point recommendation (official example, ext-apps example, or local fallback scaffold)\n- An MCP server scaffold (resource registration, tool handlers, metadata)\n- A widget scaffold (MCP Apps bridge first, `window.openai` compatibility/extensions second)\n- A reusable Node + `@modelcontextprotocol/ext-apps` starter scaffold for low-dependency fallbacks\n- A validation report against the minimum working repo contract\n- Local dev and connector setup steps\n- A short stakeholder summary of what the app does (when requested)\n\n## Mandatory Docs-First Workflow\n\nUse `$openai-docs` first whenever building or changing a ChatGPT Apps SDK app.\n\n1. Invoke `$openai-docs` (preferred) or call the OpenAI docs MCP server directly.\n2. Fetch current Apps SDK docs before writing code, especially (baseline pages):\n   - `apps-sdk/build/mcp-server`\n   - `apps-sdk/build/chatgpt-ui`\n   - `apps-sdk/build/examples`\n   - `apps-sdk/plan/tools`\n   - `apps-sdk/reference`\n3. Fetch `apps-sdk/quickstart` when scaffolding a new app or generating a first-pass implementation, and check the official examples repo/page before inventing a scaffold from scratch.\n4. Fetch deployment/submission docs when the task includes local ChatGPT testing, hosting, or public launch:\n   - `apps-sdk/deploy`\n   - `apps-sdk/deploy/submission`\n   - `apps-sdk/app-submission-guidelines`\n5. Cite the docs URLs you used when explaining design choices or generated scaffolds.\n6. Prefer current docs guidance over older repo patterns when they differ, and call out compatibility aliases explicitly.\n7. If doc search times out or returns poor matches, fetch the canonical Apps SDK pages directly by URL and continue; do not let search failure block scaffolding.\n\nIf `$openai-docs` is unavailable, use:\n\n- `mcp__openaiDeveloperDocs__search_openai_docs`\n- `mcp__openaiDeveloperDocs__fetch_openai_doc`\n\nRead `references/apps-sdk-docs-workflow.md` for suggested doc queries and a compact checklist.\nRead `references/app-archetypes.md` to classify the request into a small number of supported app shapes before choosing examples or scaffolds.\nRead `references/repo-contract-and-validation.md` when generating or reviewing a repo so the output stays inside a stable “working app” contract.\nRead `references/search-fetch-standard.md` when the app is connector-like, data-only, sync-oriented, or meant to work well with company knowledge or deep research.\nRead `references/upstream-example-workflow.md` when starting a greenfield app or when deciding whether to adapt an upstream example or use the local fallback scaffold.\nRead `references/window-openai-patterns.md` when the task needs ChatGPT-specific widget behavior or when translating repo examples that use wrapper-specific `app.*` helpers.\n\n## Prompt Guidance\n\nUse prompts that explicitly pair this skill with `$openai-docs` so the resulting scaffold is grounded in current docs.\n\nPreferred prompt patterns:\n\n- `Use $chatgpt-apps with $openai-docs to scaffold a ChatGPT app for <use case> with a <TS/Python> MCP server and <React/vanilla> widget.`\n- `Use $chatgpt-apps with $openai-docs to adapt the closest official Apps SDK example into a ChatGPT app for <use case>.`\n- `Use $chatgpt-apps and $openai-docs to refactor this Apps SDK demo into a production-ready structure with tool annotations, CSP, and URI versioning.`\n- `Use $chatgpt-apps with $openai-docs to plan tools first, then generate the MCP server and widget code.`\n\nWhen responding, ask for or infer these inputs before coding:\n\n- Use case and primary user flows\n- Read-only vs mutating tools\n- Demo vs production target\n- Private/internal use vs public directory submission\n- Backend language and UI stack\n- Auth requirements\n- External API domains for CSP allowlists\n- Hosting target and local dev approach\n- Org ownership/verification readiness (for submission tasks)\n\n## Classify The App Before Choosing Code\n\nBefore choosing examples, repo shape, or scaffolds, classify the request into one primary archetype and state it.\n\n- `tool-only`\n- `vanilla-widget`\n- `react-widget`\n- `interactive-decoupled`\n- `submission-ready`\n\nInfer the archetype unless a missing detail is truly blocking. Use the archetype to choose:\n\n- whether a UI is needed at all\n- whether to preserve a split `server/` + `web/` layout\n- whether to prefer official OpenAI examples, ext-apps examples, or the local fallback scaffold\n- which validation checks matter most\n- whether `search` and `fetch` should be the default read-only tool surface\n\nRead `references/app-archetypes.md` for the decision rubric.\n\n## Default Starting-Point Order\n\nFor greenfield apps, prefer these starting points in order:\n\n1. **Official OpenAI examples** when a close example already matches the requested stack or interaction pattern.\n2. **Version-matched `@modelcontextprotocol/ext-apps` examples** when the user needs a lower-level or more portable MCP Apps baseline.\n3. **`scripts/scaffold_node_ext_apps.mjs`** only when no close example fits, the user wants a tiny Node + vanilla starter, or network access/example retrieval is undesirable.\n\nDo not generate a large custom scaffold from scratch if a close upstream example already exists.\nCopy the smallest matching example, remove unrelated demo code, then patch it to the current docs and the user request.\n\n## Build Workflow\n\n### 0. Classify The App Archetype\n\nPick one primary archetype before planning tools or choosing a starting point.\n\n- Prefer a single primary archetype instead of mixing several.\n- If the request is broad, infer the smallest archetype that can still satisfy it.\n- Escalate to `submission-ready` only when the user asks for public launch, directory submission, or review-ready deployment.\n- Call out the chosen archetype in your response so the user can correct it early if needed.\n\n### 1. Plan Tools Before Code\n\nDefine the tool surface area from user intents.\n\n- Use one job per tool.\n- Write tool descriptions that start with \"Use this when...\" behavior cues.\n- Make inputs explicit and machine-friendly (enums, required fields, bounds).\n- Decide whether each tool is data-only, render-only, or both.\n- Set annotations accurately (`readOnlyHint`, `destructiveHint`, `openWorldHint`; add `idempotentHint` when true).\n- If the app is connector-like, data-only, sync-oriented, or intended for company knowledge or deep research, default to the standard `search` and `fetch` tools instead of inventing custom read-only equivalents.\n- For educational/demo apps, prefer one concept per tool so the model can pick the right example cleanly.\n- Group demo tools by learning objective: data into the widget, widget actions back into the conversation or tools, host/layout environment signals, and lifecycle/streaming behavior.\n\nRead `references/search-fetch-standard.md` when `search` and `fetch` may be relevant.\n\n### 2. Choose an App Architecture\n\nChoose the simplest structure that fits the goal.\n\n- Use a **minimal demo pattern** for quick prototypes, workshops, or proofs of concept.\n- Use a **decoupled data/render pattern** for production UX so the widget does not re-render on every tool call.\n\nPrefer the decoupled pattern for non-trivial apps:\n\n- Data tools return reusable `structuredContent`.\n- Render tools attach `_meta.ui.resourceUri` and optional `_meta[\"openai/outputTemplate\"]`.\n- Render tool descriptions state prerequisites (for example, \"Call `search` first\").\n\n### 2a. Start From An Upstream Example When One Fits\n\nDefault to upstream examples for greenfield work when they are close to the requested app.\n\n- Check the official OpenAI examples first for ChatGPT-facing apps, polished UI patterns, React components, file upload flows, modal flows, or apps that resemble the docs examples.\n- Use `@modelcontextprotocol/ext-apps` examples when the request is closer to raw MCP Apps bridge/server wiring, or when version-matched package patterns matter more than ChatGPT-specific polish.\n- Pick the smallest matching example and copy only the relevant files; do not transplant an entire showcase app unchanged.\n- After copying, reconcile the example with the current docs you fetched: tool names/descriptions, annotations, `_meta.ui.*`, CSP, URI versioning, and local run instructions.\n- State which example you chose and why in one sentence.\n\nRead `references/upstream-example-workflow.md` for the selection and adaptation rubric.\n\n### 2b. Use the Starter Script When a Low-Dependency Fallback Helps\n\nUse `scripts/scaffold_node_ext_apps.mjs` only when the user wants a quick, greenfield Node starter and a vanilla HTML widget is acceptable, and no upstream example is a better starting point.\n\n- Run it only after fetching current docs, then reconcile the generated files with the docs you fetched.\n- If you choose the script instead of an upstream example, say why the fallback is better for that request.\n- Skip it when a close official example exists, when the user already has an existing app structure, when they need a non-Node stack, when they explicitly want React first, or when they only want a plan/review instead of code.\n- The script generates a minimal `@modelcontextprotocol/ext-apps` server plus a vanilla HTML widget that uses the MCP Apps bridge by default.\n- The generated widget keeps follow-up messaging on the standard `ui/message` bridge and only uses `window.openai` for optional host signals/extensions.\n- After running it, patch the generated output to match the current docs and the user request: adjust tool names/descriptions, annotations, resource metadata, URI versioning, and README/run instructions.\n\n### 3. Scaffold the MCP Server\n\nGenerate a server that:\n\n- Registers a widget resource/template with the MCP Apps UI MIME type (`text/html;profile=mcp-app`) or the SDK constant (`RESOURCE_MIME_TYPE`) when using `@modelcontextprotocol/ext-apps/server`\n- Registers tools with clear names, schemas, titles, and descriptions\n- Returns `structuredContent` (model + widget), `content` (model narration), and `_meta` (widget-only data) intentionally\n- Keeps handlers idempotent or documents non-idempotent behavior explicitly\n- Includes tool status strings (`openai/toolInvocation/*`) when helpful in ChatGPT\n\nKeep `structuredContent` concise. Move large or sensitive widget-only payloads to `_meta`.\n\n### 4. Scaffold the Widget UI\n\nUse the MCP Apps bridge first for portability, then add ChatGPT-specific `window.openai` APIs when they materially improve UX.\n\n- Listen for `ui/notifications/tool-result` (JSON-RPC over `postMessage`)\n- Render from `structuredContent`\n- Use `tools/call` for component-initiated tool calls\n- Use `ui/update-model-context` only when UI state should change what the model sees\n\nUse `window.openai` for compatibility and extensions (file upload, modal, display mode, etc.), not as the only integration path for new apps.\n\n#### API Surface Guardrails\n\n- Some examples wrap the bridge with an `app` object (for example, `@modelcontextprotocol/ext-apps/react`) and expose helper names like `app.sendMessage()`, `app.callServerTool()`, `app.openLink()`, or host getter methods.\n- Treat those wrappers as implementation details or convenience layers, not the canonical public API to teach by default.\n- For ChatGPT-facing guidance, prefer the current documented surface: `window.openai.callTool(...)`, `window.openai.sendFollowUpMessage(...)`, `window.openai.openExternal(...)`, `window.openai.requestDisplayMode(...)`, and direct globals like `window.openai.theme`, `window.openai.locale`, `window.openai.displayMode`, `window.openai.toolInput`, `window.openai.toolOutput`, `window.openai.toolResponseMetadata`, and `window.openai.widgetState`.\n- If you reference wrapper helpers from repo examples, map them back to the documented `window.openai` or MCP Apps bridge primitives and call out that the wrapper is not the normative API surface.\n- Use `references/window-openai-patterns.md` for the wrapper-to-canonical mapping and for React helper extraction patterns.\n\n### 5. Add Resource Metadata and Security\n\nSet resource metadata deliberately on the widget resource/template:\n\n- `_meta.ui.csp` with exact `connectDomains` and `resourceDomains`\n- `_meta.ui.domain` for app submission-ready deployments\n- `_meta.ui.prefersBorder` (or OpenAI compatibility alias when needed)\n- Optional `openai/widgetDescription` to reduce redundant narration\n\nAvoid `frameDomains` unless iframe embeds are core to the product.\n\n### 5a. Enforce A Minimum Working Repo Contract\n\nEvery generated repo should satisfy a small, stable contract before you consider it done.\n\n- The repo shape matches the chosen archetype.\n- The MCP server and tools are wired to a reachable `/mcp` endpoint.\n- Tools have clear descriptions, accurate annotations, and UI metadata where needed.\n- Connector-like, data-only, sync-oriented, and company-knowledge-style apps use the standard `search` and `fetch` tool shapes when relevant.\n- The widget uses the MCP Apps bridge correctly when a UI exists.\n- The repo includes enough scripts or commands for a user to run and check it locally.\n- The response explicitly says what validation was run and what was not run.\n\nRead `references/repo-contract-and-validation.md` for the detailed checklist and validation ladder.\n\n### 6. Validate the Local Loop\n\nValidate against the minimum working repo contract, not just “did files get created.”\n\n- Run the lowest-cost checks first:\n  - static contract review\n  - syntax or compile checks when feasible\n  - local `/mcp` health check when feasible\n- Then move up to runtime checks:\n  - verify tool descriptors and widget rendering in MCP Inspector\n  - test the app in ChatGPT developer mode through HTTPS tunneling\n  - exercise retries and repeated tool calls to confirm idempotent behavior\n  - check widget updates after host events and follow-up tool calls\n- If you are only delivering a scaffold and are not installing dependencies, still run low-cost checks and say exactly what you did not run.\n\nRead `references/repo-contract-and-validation.md` for the validation ladder.\n\n### 7. Connect and Test in ChatGPT (Developer Mode)\n\nFor local development, include explicit ChatGPT setup steps (not just code/run commands).\n\n- Run the MCP server locally on `http://localhost:<port>/mcp`\n- Expose the local server with a public HTTPS tunnel (for example `ngrok http <port>`)\n- Use the tunneled HTTPS URL plus `/mcp` path when connecting from ChatGPT\n- In ChatGPT, enable Developer Mode under **Settings → Apps & Connectors → Advanced settings**\n- In ChatGPT app settings, create a new app for the remote MCP server and paste the public MCP URL\n- Tell users to refresh the app after MCP tool/metadata changes so ChatGPT reloads the latest descriptors\n\nNote: Some docs/screenshots still use older \"connector\" terminology. Prefer current product wording (\"app\") while acknowledging both labels when giving step-by-step instructions.\n\n### 8. Plan Production Hosting and Deployment\n\nWhen the user asks to deploy or prepare for launch, generate hosting guidance for the MCP server (and widget assets if hosted separately).\n\n- Host behind a stable public HTTPS endpoint (not a tunnel) with dependable TLS\n- Preserve low-latency streaming behavior on `/mcp`\n- Configure secrets outside the repo (environment variables / secret manager)\n- Add logging, request latency tracking, and error visibility for tool calls\n- Add basic observability (CPU, memory, request volume) and a troubleshooting path\n- Re-test the hosted endpoint in ChatGPT Developer Mode before submission\n\n### 9. Prepare Submission and Publish (Public Apps Only)\n\nOnly include these steps when the user intends a public directory listing.\n\n- Use `apps-sdk/deploy/submission` for the submission flow and `apps-sdk/app-submission-guidelines` for review requirements\n- Keep private/internal apps in Developer Mode instead of submitting\n- Confirm org verification and Owner-role prerequisites before submission work\n- Ensure the MCP server uses a public production endpoint (no localhost/testing URLs) and has submission-ready CSP configured\n- Prepare submission artifacts: app metadata, logo/screenshots, privacy policy URL, support contact, test prompts/responses, localization info\n- If auth is required, include review-safe demo credentials and test the login path end-to-end\n- Submit for review in the Platform dashboard, monitor review status, and publish only after approval\n\n## Interactive State Guidance\n\nRead `references/interactive-state-sync-patterns.md` when the app has long-lived widget state, repeated interactions, or component-initiated tool calls (for example, games, boards, maps, dashboards, editors).\n\nUse it to choose patterns for:\n\n- State snapshots plus monotonic event tokens (`stateVersion`, `resetCount`, etc.)\n- Idempotent retry-safe handlers\n- `structuredContent` vs `_meta` partitioning\n- MCP Apps bridge-first update flows with optional `window.openai` compatibility\n- Decoupled data/render tool architecture for more complex interactive apps\n\n## Output Expectations\n\nWhen using this skill to scaffold code, produce output in this order unless the user asks otherwise:\n\n- For direct scaffold requests, do not stop at the plan: give the brief plan, then create the files immediately.\n\n1. Primary app archetype chosen and why\n2. Tool plan and architecture choice (minimal vs decoupled)\n3. Upstream starting point chosen (official example, ext-apps example, or local fallback scaffold) and why\n4. Doc pages/URLs used from `$openai-docs`\n5. File tree to create or modify\n6. Implementation (server + widget)\n7. Validation performed against the minimum working repo contract\n8. Local run/test instructions (including tunnel + ChatGPT Developer Mode app setup)\n9. Deployment/hosting guidance (if requested or implied)\n10. Submission-readiness checklist (for public launch requests)\n11. Risks, gaps, and follow-up improvements\n\n## References\n\n- `references/app-archetypes.md` for classifying requests into a small number of supported app shapes\n- `references/apps-sdk-docs-workflow.md` for doc queries, page targets, and code-generation checklist\n- `references/interactive-state-sync-patterns.md` for reusable patterns for stateful or highly interactive widget apps\n- `references/repo-contract-and-validation.md` for the minimum working repo contract and lightweight validation ladder\n- `references/search-fetch-standard.md` for when and how to default to the standard `search` and `fetch` tools\n- `references/upstream-example-workflow.md` for choosing between official examples, ext-apps examples, and the local fallback scaffold\n- `references/window-openai-patterns.md` for ChatGPT-specific extensions, wrapper API translation, and React helper patterns\n- `scripts/scaffold_node_ext_apps.mjs` for a minimal Node + `@modelcontextprotocol/ext-apps` fallback starter scaffold\n"
  },
  {
    "path": "skills/.curated/chatgpt-apps/agents/openai.yaml",
    "content": "interface:\n  display_name: \"ChatGPT Apps\"\n  short_description: \"Build and scaffold ChatGPT apps\"\n  default_prompt: \"Use $chatgpt-apps to classify the app archetype first, fetch current OpenAI Apps SDK docs before generating code, default to the standard `search` and `fetch` tools when the app is connector-like or sync-oriented, adapt the closest upstream example when one fits, and only fall back to the local Node scaffold for minimal `@modelcontextprotocol/ext-apps` starters. Produce a working repo shape, then report what validation was actually run.\"\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"openaiDeveloperDocs\"\n      description: \"OpenAI developer docs MCP server for current Apps SDK guidance\"\n      transport: \"streamable_http\"\n      url: \"https://developers.openai.com/mcp\"\npolicy:\n  allow_implicit_invocation: true\n"
  },
  {
    "path": "skills/.curated/chatgpt-apps/references/app-archetypes.md",
    "content": "# App Archetypes\n\nLoad this reference before choosing a starting point for a new ChatGPT app. The goal is to keep the skill inside a small number of supported app shapes instead of inventing a custom structure for every prompt.\n\n## Rule\n\nChoose one primary archetype per request and state it.\n\nDo not combine several archetypes unless the user explicitly asks for a hybrid app and the extra complexity is necessary.\n\n## Archetypes\n\n### `tool-only`\n\nUse when:\n\n- The user does not need an in-ChatGPT UI\n- The task is mainly search, fetch, retrieval, or background actions\n\nDefault shape:\n\n- MCP server only\n\nBest starting point:\n\n- Official docs and MCP server examples\n\nValidation emphasis:\n\n- `/mcp` route works\n- tool schemas and annotations are correct\n- no unnecessary UI resource is registered\n- if the app is connector-like or sync-oriented, `search` and `fetch` should be the default read-only tools\n\n### `vanilla-widget`\n\nUse when:\n\n- The user wants a small demo, workshop starter, or simple inline widget\n- A single HTML widget is enough\n- The user wants the fastest path to a working repo\n\nDefault shape:\n\n- Root-level server plus `public/` widget assets\n\nBest starting point:\n\n- Apps SDK quickstart first\n- Local fallback scaffold if the quickstart is not a good fit\n\nValidation emphasis:\n\n- bridge initialization\n- `ui/notifications/tool-result`\n- `tools/call` only when the widget is interactive\n\n### `react-widget`\n\nUse when:\n\n- The user wants a polished UI\n- The UI is clearly component-based\n- The user mentions React, TypeScript frontend tooling, or richer design requirements\n\nDefault shape:\n\n- Split `server/` + `web/` layout when the example already uses it\n\nBest starting point:\n\n- Official OpenAI examples\n\nValidation emphasis:\n\n- build output is wired into the server correctly\n- bundle references resolve\n- widget renders from `structuredContent`\n\n### `interactive-decoupled`\n\nUse when:\n\n- The app has repeated user interaction\n- The widget should stay mounted while tools are called repeatedly\n- The app is a board, map, editor, game, dashboard, or other stateful experience\n\nDefault shape:\n\n- Split `server/` + `web/`\n- data tools plus render tools\n\nBest starting point:\n\n- Official OpenAI examples plus `references/interactive-state-sync-patterns.md`\n\nValidation emphasis:\n\n- tool retries are safe\n- widget does not remount unnecessarily\n- state sync is intentional\n- UI tool calls work independently of model reruns\n\n### `submission-ready`\n\nUse when:\n\n- The user asks for public launch, review readiness, or directory submission\n\nDefault shape:\n\n- Smallest viable repo that still includes deployment and review requirements\n\nBest starting point:\n\n- Closest official example that matches the requested stack\n\nValidation emphasis:\n\n- `_meta.ui.domain`\n- accurate CSP\n- auth and review-safe flows\n- submission prerequisites and artifacts\n\n## Selection Heuristic\n\n- If the prompt does not mention a UI, choose `tool-only`.\n- If the prompt is about a knowledge source, sync app, connector-like integration, or deep research, strongly prefer `tool-only` plus the standard `search` and `fetch` tools unless the user clearly needs a widget.\n- If the prompt asks for a simple demo or starter, choose `vanilla-widget`.\n- If the prompt asks for a polished UI or React, choose `react-widget`.\n- If the prompt implies long-lived client state or repeated interaction, choose `interactive-decoupled`.\n- Only choose `submission-ready` when the user explicitly asks for launch or review-readiness work.\n"
  },
  {
    "path": "skills/.curated/chatgpt-apps/references/apps-sdk-docs-workflow.md",
    "content": "# Apps SDK Docs Workflow\n\nUse this reference to keep code generation aligned with current OpenAI Apps SDK docs.\n\n## Always Fetch These Pages (Baseline)\n\n- `https://developers.openai.com/apps-sdk/build/mcp-server/`\n- `https://developers.openai.com/apps-sdk/build/chatgpt-ui/`\n- `https://developers.openai.com/apps-sdk/build/examples/`\n- `https://developers.openai.com/apps-sdk/plan/tools/`\n- `https://developers.openai.com/apps-sdk/reference/`\n\n## Fetch Conditionally (Greenfield / First Pass)\n\n- `https://developers.openai.com/apps-sdk/quickstart/` for first implementation scaffolds and happy-path wiring\n- `https://developers.openai.com/apps-sdk/deploy/` when the task includes local ChatGPT testing via tunnel, hosting, or production deployment planning\n- `https://developers.openai.com/apps-sdk/deploy/submission/` when the task includes public launch, app review, or publishing steps\n- `https://developers.openai.com/apps-sdk/app-submission-guidelines/` when the task includes submission readiness, policy/reliability checks, or review-risk reduction\n\n## Suggested `openai-docs` / MCP Queries\n\nUse focused searches before fetching:\n\n- `ChatGPT Apps SDK build MCP server register resource template resourceUri outputTemplate`\n- `ChatGPT Apps SDK build ChatGPT UI MCP Apps bridge ui/notifications/tool-result`\n- `ChatGPT Apps SDK examples React widget upload modal Pizzaz`\n- `Apps SDK define tools annotations readOnlyHint destructiveHint openWorldHint`\n- `Apps SDK reference tool descriptor _meta ui.resourceUri openai/outputTemplate`\n- `ChatGPT Apps SDK quickstart build web component tools/call`\n- `ChatGPT app company knowledge compatibility search fetch tools`\n- `platform MCP search tool fetch tool schema`\n- `ChatGPT Apps SDK deploy app local development tunnel ngrok refresh connector`\n- `ChatGPT Apps SDK submit app review prerequisites app submission guidelines`\n\n## Docs-Derived Checklist (Current Guidance)\n\n### Archetype / Shape\n\n- Classify the request into one primary app archetype before choosing examples or scaffolds\n- Keep the repo shape consistent with that archetype instead of inventing a new structure for each prompt\n\n### Server\n\n- Register the widget resource/template with the MCP Apps UI MIME type (`text/html;profile=mcp-app`) or `RESOURCE_MIME_TYPE` when using `@modelcontextprotocol/ext-apps/server`\n- Version template URIs when widget HTML or JS or CSS changes in a breaking way (treat URI as cache key)\n- Set `_meta.ui.resourceUri` on render tools; optionally mirror `_meta[\"openai/outputTemplate\"]` for ChatGPT compatibility\n- Design tool handlers to be idempotent because the model may retry calls\n- Keep `structuredContent` concise and move widget-only payloads to `_meta`\n\n### Tool Design\n\n- Plan one user intent per tool\n- Use action-oriented names and precise descriptions\n- Set tool impact hints accurately (`readOnlyHint`, `destructiveHint`, `openWorldHint`)\n- Split data and render tools so that the model can fetch the data and look at it before choosing to render the widget UI or not\n- Make the widget input a list of unique identifiers (e.g. `propertyIds` for a render property map widget that takes IDs returned from the fetch properties nearby tool) if you want to make sure the widget only renders 1p data; make the widget input semantically relevant if you want to allow the model to render the widget with generated data (e.g. `questionAndAnswerPairs` for a flashcards widget)\n- For connector-like, data-only, sync-oriented, or company-knowledge-style apps, prefer the standard `search` and `fetch` tools by default\n\n### UI\n\n- Prefer the MCP Apps bridge (`ui/*` notifications + `tools/call`) for new apps\n- Prefer `ui/message` for follow-up messaging in baseline examples; treat `window.openai.sendFollowUpMessage` as optional ChatGPT-specific compatibility\n- Treat `window.openai` as compatibility plus optional ChatGPT extensions\n- Render from `structuredContent` and treat host-delivered data as untrusted input\n- Use `ui/update-model-context` only for UI state the model should reason about\n\n### Starting Point Selection\n\n- Check `apps-sdk/build/examples` and the official examples repo before generating a greenfield scaffold from scratch\n- Prefer the smallest upstream example that matches the requested stack and interaction pattern\n- Use the local fallback scaffold only when upstream examples are a poor fit or undesirable for the request\n\n### Resource Metadata / Security\n\n- Set `_meta.ui.csp.connectDomains` and `_meta.ui.csp.resourceDomains` exactly\n- Avoid `frameDomains` unless iframe embedding is central to the experience\n- Set `_meta.ui.domain` for submission-ready apps\n- Always set `openai/widgetDescription` to inform the model what the widget is to be used for\n\n### Developer Mode / Local Testing\n\n- Run the MCP server locally on `http://localhost:<port>/mcp`\n- Expose it with a public HTTPS tunnel for ChatGPT access during development\n- Use the public URL + `/mcp` when adding the app in ChatGPT settings\n- Include ChatGPT Developer Mode setup and app creation steps in implementation handoff\n- Remind users to refresh the app after MCP tool/metadata changes\n- Note terminology differences when relevant: some docs/screenshots may still say \"connector\" while product UI uses \"app\"\n\n### Validation\n\n- Validate against a minimum working repo contract, not just file creation\n- Run the cheapest useful syntax or compile check first\n- If feasible, confirm the local `/mcp` route responds before calling the result “working”\n- If you cannot run a deeper check, say so explicitly\n- If the app is connector-like or sync-oriented, verify the `search` and `fetch` tool shapes against the standard\n\n### Production Hosting / Deploy\n\n- Prefer a stable public HTTPS endpoint with reliable TLS and low-latency streaming `/mcp`\n- Document platform-specific secrets handling and environment variables\n- Include logging/metrics expectations for debugging production tool calls\n- Re-test the hosted endpoint in ChatGPT Developer Mode before submission\n\n### Submission / Review\n\n- Read `deploy/submission` and `app-submission-guidelines` together (process + policy requirements)\n- Check org verification and Owner-role prerequisites before generating submission steps\n- Ensure the endpoint is public production infrastructure (not localhost/tunnel/testing URLs)\n- Ensure CSP is defined and accurate for submission\n- Prepare submission artifacts (metadata, screenshots, privacy policy/support contacts, test prompts/responses)\n- If auth is required, prepare review-safe demo credentials and validate them outside internal networks\n\n## Generation Pattern\n\n1. Classify the app archetype.\n2. Fetch docs with `$openai-docs`.\n3. Check official examples before inventing a scaffold from scratch.\n4. Summarize relevant constraints and metadata keys.\n5. Propose tool plan and architecture.\n6. Adapt the closest example or use the local fallback scaffold.\n7. Generate or patch the server scaffold.\n8. Generate or patch the widget scaffold.\n9. Validate the repo against the minimum working contract.\n10. Add local run + tunnel + ChatGPT Developer Mode app setup instructions.\n11. Add hosting/deployment guidance when the task implies go-live.\n12. Add submission/readiness steps when the user intends public distribution.\n13. Call out compatibility aliases vs MCP Apps standard fields.\n\n## Starter Scaffold Script\n\n- Use `./scripts/scaffold_node_ext_apps.mjs <output-dir> --app-name <name>` only when the user wants a greenfield Node + `@modelcontextprotocol/ext-apps` starter and no upstream example is the better fit.\n- If the file is not executable in the current environment, fall back to `node scripts/scaffold_node_ext_apps.mjs <output-dir> --app-name <name>`.\n- The script generates `package.json`, `tsconfig.json`, `public/widget.html`, and `src/server.ts`.\n- It intentionally uses the MCP Apps bridge by default, keeps follow-up messaging on `ui/message`, and limits `window.openai` to optional host signals/extensions.\n- After generation, compare the output against the docs you fetched and adjust package versions, metadata, transport details, or URI/versioning if the docs changed.\n"
  },
  {
    "path": "skills/.curated/chatgpt-apps/references/interactive-state-sync-patterns.md",
    "content": "# Interactive State Sync Patterns\n\nUse this reference when building ChatGPT apps with long-lived widget state, repeated interactions, or component-initiated tool calls (for example: games, boards, maps, dashboards, editors, or realtime-ish UIs).\n\nDo not load this file for simple read-only render apps unless state sync behavior is part of the task.\n\n## When This Reference Helps\n\nRead this file when the app needs one or more of these patterns:\n\n- Repeated actions that may return similar data (retry, refresh, reset, reroll)\n- UI controls that trigger tool calls after the initial render\n- Local widget behavior that should also work outside ChatGPT during development\n- Multiple tool calls updating one mounted widget over time\n- Clear separation between model-visible state and widget-only state\n\n## Reusable Patterns\n\n### 1. Snapshot + Event Token\n\nReturn a stable state snapshot in `structuredContent` and add a monotonic event token for repeated actions that may not change other fields.\n\nExamples:\n\n- `stateVersion`\n- `refreshCount`\n- `resetCount`\n- `lastMutationId`\n\nUse this when the widget must detect \"same shape, new event\" updates reliably.\n\n### 2. Intent-Focused Tool Surface\n\nPrefer small, explicit tools that map to user-visible actions or data operations.\n\n- Keep names action-oriented\n- Use enums and bounded schemas where possible\n- Avoid kitchen-sink tools that mix unrelated reads and writes\n\nThis improves model tool selection and reduces malformed calls.\n\n### 3. Idempotent Handlers (or Explicitly Non-Idempotent)\n\nDesign handlers to tolerate retries. If a tool is not idempotent, make the side effect explicit and confirm intent in the flow.\n\n- Reads and pure transforms should usually be idempotent\n- Writes should include clear impact hints and current-turn confirmation where needed\n- Repeated calls with the same input should not corrupt widget state\n\n### 4. `structuredContent` / `_meta` Partitioning\n\nPartition payloads intentionally:\n\n- `structuredContent`: concise model-visible state the widget also uses\n- `content`: short narration/status text\n- `_meta`: large maps, caches, or sensitive widget-only hydration data\n\nKeep `structuredContent` small enough for follow-up reasoning and chaining.\n\n### 5. MCP Apps Bridge First, `window.openai` Second\n\nFor new scaffolds:\n\n- Prefer MCP Apps bridge notifications and `tools/call` (portable across hosts)\n- Use `window.openai` as a compatibility layer plus optional ChatGPT extensions\n\nThis keeps the app portable while still enabling ChatGPT-specific capabilities when helpful.\n\n### 6. Component-Initiated Tool Calls Without Remounting\n\nFor interactive widgets, allow the UI to call data/action tools directly and update the existing widget state instead of forcing a full re-render/remount every time.\n\nThis is especially useful for:\n\n- Refresh\n- Retry\n- Rerun\n- Toggle/filter actions\n- Incremental interactions inside one widget session\n\n### 7. Standalone / No-Host Fallback Mode\n\nWhen feasible, make the widget usable without ChatGPT during development:\n\n- If host APIs are unavailable, apply local state directly\n- Preserve basic interactions in a normal browser\n\nThis speeds up front-end iteration and reduces dependence on connector setup for every UI tweak.\n\n### 8. Decouple Data Tools from Render Tools (When Complexity Grows)\n\nUse separate data and render tools when the app has multi-step reasoning or frequent updates.\n\n- Data tools fetch/compute/mutate and return reusable `structuredContent`\n- Render tools attach the widget template and focus on presentation\n\nThis reduces unnecessary remounts and gives the model a chance to refine data before rendering.\n\n## Common Anti-Patterns\n\n- Putting large widget-only blobs into `structuredContent`\n- Attaching a widget template to every tool when only one render tool needs it\n- Using hidden client-side state as the source of truth for critical actions\n- Depending only on `window.openai` APIs for baseline app behavior\n- Using ambiguous tool names that do not match user intent\n\n## Example App Types That Benefit From These Patterns\n\n- Multiplayer or turn-based games\n- Collaborative boards / task views\n- Maps with filters and repeated searches\n- Dashboards with refresh and drill-down actions\n- Editors or builders with iterative tool calls\n"
  },
  {
    "path": "skills/.curated/chatgpt-apps/references/repo-contract-and-validation.md",
    "content": "# Repo Contract And Validation\n\nLoad this reference when scaffolding or reviewing a generated ChatGPT app repo.\n\nThe goal is not “files were created.” The goal is “the repo is plausibly runnable and follows a stable working-app contract.”\n\n## Minimum Working Repo Contract\n\nEvery generated repo should satisfy the relevant parts of this contract.\n\n### 1. Shape\n\n- The repo shape matches the chosen archetype.\n- The repo structure is simple enough that a user can identify where the server and widget live.\n\n### 2. Server\n\n- There is a clear MCP server entry point.\n- The server exposes `/mcp`.\n- The server registers tools intentionally.\n- If a UI exists, the server registers a resource/template with the MCP Apps UI MIME type.\n\n### 3. Tools\n\n- Each tool maps to one user intent.\n- Descriptions help the model choose the tool.\n- Required annotations are present and accurate.\n- UI-linked tools use `_meta.ui.resourceUri`.\n- `_meta[\"openai/outputTemplate\"]` is treated as optional compatibility, not the primary contract.\n- When the app is connector-like, data-only, sync-oriented, or intended for company knowledge or deep research, it implements standard `search` and `fetch` tools instead of custom substitutes.\n\n### 4. Widget\n\n- The widget initializes the MCP Apps bridge when needed.\n- The widget can receive `ui/notifications/tool-result`.\n- The widget renders from `structuredContent`.\n- Interactive widgets use `tools/call`.\n- Baseline follow-up messaging uses `ui/message`.\n- `window.openai` is optional and additive.\n\n### 5. Local Developer Experience\n\n- There is a clear way to start the app locally.\n- There is at least one low-cost check command when the stack supports it.\n- The response explains how to connect the app in ChatGPT Developer Mode when relevant.\n\n## Validation Ladder\n\nRun the highest level you can without overfitting to a single stack.\n\n### Level 0: Static contract review\n\nCheck for:\n\n- chosen archetype is sensible\n- repo shape matches archetype\n- `/mcp` route is present\n- tool/resource/widget responsibilities are coherent\n- if the app is connector-like or sync-oriented, `search` and `fetch` are present with the expected standard shape\n\n### Level 1: Syntax or compile checks\n\nUse the stack-appropriate cheapest check available, for example:\n\n- Python syntax check\n- TypeScript compile check\n- framework-specific lint or build sanity check if already installed\n\n### Level 2: Local runtime sanity\n\nIf feasible:\n\n- start the server\n- confirm the health route or `/mcp` endpoint responds\n\n### Level 3: Host loop validation\n\nIf feasible:\n\n- inspect with MCP Inspector\n- test through ChatGPT Developer Mode\n- confirm widget updates after tool results\n\n## Reporting Rule\n\nAlways say which validation level was reached and what was not run.\n\nThat makes the skill more reliable because it separates:\n\n- “repo shape looks right”\n- “syntax is valid”\n- “server starts”\n- “host integration was actually exercised”\n"
  },
  {
    "path": "skills/.curated/chatgpt-apps/references/search-fetch-standard.md",
    "content": "# Search And Fetch Standard\n\nLoad this reference when the app is connector-like, data-only, sync-oriented, or meant to work well with company knowledge or deep research.\n\n## Default Rule\n\nIf the app is primarily a read-only knowledge source, do not invent custom equivalents to `search` and `fetch`.\n\nDefault to implementing the standard `search` and `fetch` tools exactly, then add other tools only if the use case clearly needs them.\n\n## When This Applies\n\nUse the standard by default when the request is about:\n\n- a data-only app\n- a sync app\n- a company knowledge source\n- deep research compatibility\n- a connector-like integration over documents, tickets, wiki pages, CRM records, or similar read-only data\n\n## Tool Requirements\n\n### `search`\n\n- Read-only tool\n- Takes a single query string\n- Returns exactly one MCP content item with `type: \"text\"`\n- That text is a JSON-encoded object with:\n  - `results`\n  - each result has `id`, `title`, and `url`\n\n### `fetch`\n\n- Read-only tool\n- Takes a single document/item id string\n- Returns exactly one MCP content item with `type: \"text\"`\n- That text is a JSON-encoded object with:\n  - `id`\n  - `title`\n  - `text`\n  - `url`\n  - optional `metadata`\n\n## Implementation Rules\n\n- Match the schema exactly when the app is intended for company knowledge or deep research compatibility.\n- Use canonical `url` values for citations.\n- Mark these tools as read-only.\n- Prefer these names exactly: `search` and `fetch`.\n- If you add other read-only tools, they should complement the standard rather than replace it.\n\n## Validation Checks\n\nWhen `search` and `fetch` are relevant, verify:\n\n- both tools exist\n- they are read-only\n- their input shapes match the standard\n- their returned payloads are wrapped as one `content` item with JSON-encoded `text`\n- result URLs are canonical enough for citation use\n\n## Source\n\nThis standard is described in:\n\n- `https://developers.openai.com/apps-sdk/build/mcp-server/#company-knowledge-compatibility`\n- `https://platform.openai.com/docs/mcp`\n"
  },
  {
    "path": "skills/.curated/chatgpt-apps/references/upstream-example-workflow.md",
    "content": "# Upstream Example Workflow\n\nLoad this reference when starting a greenfield ChatGPT app or when deciding whether to adapt an upstream example or use the local fallback scaffold.\n\n## Default Order\n\nPrefer these starting points in order:\n\n1. Official OpenAI Apps SDK examples\n2. Version-matched `@modelcontextprotocol/ext-apps` examples\n3. Local `scripts/scaffold_node_ext_apps.mjs` fallback\n\nThis keeps the skill aligned with current docs and maintained example code while still preserving a low-dependency fallback when examples are not a good fit.\n\n## Choose The Right Source\n\n### 1. Official OpenAI examples\n\nPrefer these when:\n\n- The app is clearly ChatGPT-facing\n- The user wants a polished UI or React component\n- The task involves file upload, modal flows, display-mode changes, or other ChatGPT extensions\n- The docs/examples page already shows a similar interaction pattern\n\nTypical sources:\n\n- `https://developers.openai.com/apps-sdk/build/examples/`\n- `https://github.com/openai/openai-apps-sdk-examples`\n- `https://developers.openai.com/apps-sdk/quickstart/` for the smallest vanilla baseline\n\n### 2. `@modelcontextprotocol/ext-apps` examples\n\nPrefer these when:\n\n- The user needs a lower-level MCP Apps baseline\n- Portability across MCP Apps-compatible hosts matters more than ChatGPT-specific polish\n- You want version-matched examples close to the installed `@modelcontextprotocol/ext-apps` package shape\n\nThis follows the same basic idea as the upstream `create-mcp-app` skill: use maintained examples as the starting point, then adapt them.\n\nTypical examples from upstream flows:\n\n- `examples/demo-vanilla-html`\n- `examples/demo-react-simple`\n- `examples/demo-connectors-api`\n\n### 3. Local fallback scaffold\n\nUse `scripts/scaffold_node_ext_apps.mjs` when:\n\n- No close upstream example exists\n- The user wants a tiny Node + vanilla HTML starter\n- Network/example retrieval is undesirable\n- You need a throwaway starter to patch quickly during a live coding task\n\nDo not prefer the local scaffold just because it is available. It is the fallback, not the default.\n\n## Adaptation Rules\n\n- Copy the smallest matching example, not the entire showcase app.\n- Remove unrelated demo tools, assets, and routes immediately.\n- Keep the upstream file structure when it is already clean and docs-aligned.\n- Reconcile the copied example with the current docs before finishing:\n  - tool names and descriptions\n  - annotations (`readOnlyHint`, `destructiveHint`, `openWorldHint`, `idempotentHint` when true)\n  - `_meta.ui.resourceUri` and optional `_meta[\"openai/outputTemplate\"]`\n  - resource `_meta.ui.csp`, `_meta.ui.domain`, and `openai/widgetDescription`\n  - URI versioning for template changes\n  - local run/test instructions\n- State which example you chose and why.\n- If you rely on upstream code, note the source repo and branch/tag/commit when practical; avoid silently depending on a floating example shape for long-lived work.\n\n## Minimal Selection Heuristic\n\n- If the user asks for **React + polished UI**, start with official OpenAI examples.\n- If the user asks for **vanilla HTML + tiny demo**, start with the quickstart example; use the local fallback scaffold only if the quickstart is still too opinionated or unavailable.\n- If the user asks for **portable MCP Apps wiring**, start with `@modelcontextprotocol/ext-apps` examples.\n- If the user already has an app, adapt their code directly instead of importing a new example.\n"
  },
  {
    "path": "skills/.curated/chatgpt-apps/references/window-openai-patterns.md",
    "content": "# Window.openai Patterns\n\nLoad this reference when a task needs ChatGPT-only widget features, when translating older examples that use an `app` wrapper, or when a React widget should read host globals safely.\n\n## Core Rule\n\n- Build baseline widget behavior on the MCP Apps bridge: `ui/*` notifications, `tools/call`, `ui/message`, and `ui/update-model-context`.\n- Use `window.openai` only when the task specifically benefits from ChatGPT-only runtime conveniences.\n- Treat `window.openai` as additive. The app should still have a coherent baseline path on the MCP Apps standard when possible.\n\n## Canonical `window.openai` Surface\n\n### State And Data\n\n- `window.openai.toolInput`: tool arguments supplied by the host\n- `window.openai.toolOutput`: current `structuredContent`\n- `window.openai.toolResponseMetadata`: current `_meta` payload (widget-only)\n- `window.openai.widgetState`: persisted widget-local snapshot\n- `window.openai.setWidgetState(state)`: persist widget-local snapshot after meaningful UI changes\n\n### Runtime APIs\n\n- `window.openai.callTool(name, args)`: call another MCP tool from the widget\n- `window.openai.sendFollowUpMessage({ prompt, scrollToBottom? })`: ask ChatGPT to post a widget-authored follow-up message\n- `window.openai.openExternal({ href, redirectUrl? })`: open an external URL through ChatGPT's vetted flow\n- `window.openai.requestDisplayMode({ mode })`: request `inline`, `pip`, or `fullscreen`\n- `window.openai.requestModal({ params, template? })`: open a host-owned modal\n- `window.openai.requestClose()`: ask ChatGPT to close the widget\n- `window.openai.uploadFile(file)`: upload a file from the widget\n- `window.openai.getFileDownloadUrl({ fileId })`: resolve a temporary download URL\n- `window.openai.notifyIntrinsicHeight(...)`: report dynamic height changes\n- `window.openai.setOpenInAppUrl({ href })`: override the fullscreen punch-out target\n\n### Context Signals\n\n- `window.openai.theme`\n- `window.openai.displayMode`\n- `window.openai.maxHeight`\n- `window.openai.safeArea`\n- `window.openai.view`\n- `window.openai.userAgent`\n- `window.openai.locale`\n\n## Mapping From Repo Wrapper Examples\n\n- `app.callServerTool({ name, arguments })`:\n  Use `window.openai.callTool(name, args)` when you intentionally want the ChatGPT compatibility layer.\n  Use `tools/call` over the bridge when you want the portable MCP Apps path.\n- `app.sendMessage(...)`:\n  Use `ui/message` for portable bridge messaging.\n  If the task is intentionally ChatGPT-specific, `window.openai.sendFollowUpMessage({ prompt })` is the closest supported path.\n- `app.updateModelContext(...)`:\n  Use `ui/update-model-context` over the bridge.\n  This is part of the standard bridge, not a `window.openai` feature.\n- `app.openLink({ url })`:\n  Use `window.openai.openExternal({ href: url })` when you intentionally want ChatGPT's external navigation flow.\n- `app.requestDisplayMode({ mode })`:\n  Use `window.openai.requestDisplayMode({ mode })`.\n- `app.getHostContext()`:\n  Read the documented globals directly (`theme`, `displayMode`, `locale`, `maxHeight`, `safeArea`, `userAgent`).\n- `app.getHostCapabilities()` / `app.getHostVersion()`:\n  These are wrapper-level convenience APIs.\n  Prefer feature detection (`if (window.openai?.requestModal)`) and the documented globals instead of teaching these as the primary public surface.\n\n## React Helper Extraction\n\n- The repo's `src/use-openai-global.ts` is a good baseline for subscribing to host global changes without scattering direct `window.openai` reads through components.\n- The repo's `src/use-widget-state.ts` is a good baseline for mirroring React state into `window.openai.setWidgetState(...)`.\n- The repo's `src/use-widget-props.ts` is a good baseline for reading typed `toolOutput` with a local fallback.\n- Keep these helpers optional. Do not force a React abstraction when a simple vanilla widget is enough.\n"
  },
  {
    "path": "skills/.curated/chatgpt-apps/scripts/scaffold_node_ext_apps.mjs",
    "content": "#!/usr/bin/env node\n\nimport { mkdirSync, writeFileSync, existsSync, readdirSync, lstatSync } from \"node:fs\";\nimport path from \"node:path\";\n\nfunction toSlug(value) {\n  const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n  return normalized || \"example-chatgpt-app\";\n}\n\nfunction toToolName(value) {\n  const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, \"_\").replace(/_+/g, \"_\").replace(/^_+|_+$/g, \"\");\n  return normalized || \"show_example\";\n}\n\nfunction toTitle(value) {\n  const parts = value.split(/[-_]+/).filter(Boolean);\n  return parts.map((part) => part[0].toUpperCase() + part.slice(1)).join(\" \") || \"Example\";\n}\n\nfunction fillTemplate(template, mapping) {\n  let result = template;\n  for (const [key, value] of Object.entries(mapping)) {\n    result = result.replaceAll(key, value);\n  }\n  return result;\n}\n\nfunction writeFile(filePath, content) {\n  mkdirSync(path.dirname(filePath), { recursive: true });\n  writeFileSync(filePath, content, \"utf8\");\n}\n\nfunction ensureTargetDir(targetPath, force) {\n  if (existsSync(targetPath)) {\n    if (!lstatSync(targetPath).isDirectory()) {\n      throw new Error(`Output path exists and is not a directory: ${targetPath}`);\n    }\n    if (readdirSync(targetPath).length > 0 && !force) {\n      throw new Error(\n        `Refusing to write into non-empty directory: ${targetPath}\\nRe-run with --force to overwrite generated files.`\n      );\n    }\n  }\n\n  mkdirSync(targetPath, { recursive: true });\n}\n\nfunction buildPackageJson(appSlug) {\n  const packageJson = {\n    name: appSlug,\n    private: true,\n    type: \"module\",\n    scripts: {\n      dev: \"tsx watch src/server.ts\",\n      start: \"tsx src/server.ts\",\n      check: \"tsc --noEmit\",\n    },\n    dependencies: {\n      \"@modelcontextprotocol/ext-apps\": \"^1.0.1\",\n      \"@modelcontextprotocol/sdk\": \"^1.20.2\",\n      zod: \"^3.25.76\",\n    },\n    devDependencies: {\n      \"@types/node\": \"^24.3.0\",\n      tsx: \"^4.19.4\",\n      typescript: \"^5.9.2\",\n    },\n  };\n\n  return `${JSON.stringify(packageJson, null, 2)}\\n`;\n}\n\nfunction buildTsconfig() {\n  return `{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"types\": [\"node\"],\n    \"outDir\": \"dist\"\n  },\n  \"include\": [\"src/**/*.ts\"]\n}\n`;\n}\n\nconst WIDGET_TEMPLATE = `<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <title>__APP_TITLE__</title>\n    <style>\n      :root {\n        color: #0b0f19;\n        font-family: \"Inter\", system-ui, sans-serif;\n      }\n\n      * {\n        box-sizing: border-box;\n      }\n\n      body {\n        margin: 0;\n        min-height: 100vh;\n        padding: 16px;\n        background:\n          radial-gradient(circle at top right, #d8f3ff 0, transparent 40%),\n          linear-gradient(180deg, #f7fbff 0%, #edf3fb 100%);\n      }\n\n      main {\n        width: 100%;\n        max-width: 420px;\n        margin: 0 auto;\n        padding: 20px;\n        border-radius: 18px;\n        background: rgba(255, 255, 255, 0.92);\n        border: 1px solid rgba(11, 15, 25, 0.08);\n        box-shadow: 0 14px 32px rgba(11, 15, 25, 0.08);\n      }\n\n      .eyebrow {\n        margin: 0 0 8px;\n        font-size: 12px;\n        font-weight: 700;\n        letter-spacing: 0.12em;\n        text-transform: uppercase;\n        color: #4f5d75;\n      }\n\n      h1 {\n        margin: 0 0 10px;\n        font-size: 24px;\n        line-height: 1.15;\n      }\n\n      p {\n        margin: 0;\n        line-height: 1.5;\n      }\n\n      .stack {\n        display: grid;\n        gap: 12px;\n      }\n\n      button {\n        border: 0;\n        border-radius: 999px;\n        padding: 10px 14px;\n        font: inherit;\n        font-weight: 600;\n        color: white;\n        background: #0f62fe;\n        cursor: pointer;\n      }\n\n      button[hidden] {\n        display: none;\n      }\n\n      button.secondary {\n        background: #0b0f19;\n      }\n\n      .meta {\n        padding: 12px;\n        border-radius: 14px;\n        background: #f5f8fc;\n        color: #4f5d75;\n        font-size: 13px;\n      }\n    </style>\n  </head>\n  <body>\n    <main class=\"stack\">\n      <p class=\"eyebrow\">__APP_TITLE__ starter</p>\n      <h1 id=\"headline\">Waiting for tool output</h1>\n      <p id=\"message\">Call the __TOOL_NAME__ tool to hydrate this widget.</p>\n      <button id=\"tool-button\" type=\"button\">Call __TOOL_NAME__ from the widget</button>\n      <button id=\"follow-up-button\" class=\"secondary\" type=\"button\">\n        Ask the host to explain this app\n      </button>\n      <div class=\"meta\" id=\"meta\">\n        This widget uses the MCP Apps bridge by default.\n      </div>\n    </main>\n\n    <script type=\"module\">\n      const headlineEl = document.querySelector(\"#headline\");\n      const messageEl = document.querySelector(\"#message\");\n      const metaEl = document.querySelector(\"#meta\");\n      const toolButtonEl = document.querySelector(\"#tool-button\");\n      const followUpButtonEl = document.querySelector(\"#follow-up-button\");\n\n      let toolOutput = null;\n      let rpcId = 0;\n      const pendingRequests = new Map();\n\n      const render = () => {\n        const headline = toolOutput?.headline ?? \"__APP_TITLE__\";\n        const message =\n          toolOutput?.message ??\n          \"Call the __TOOL_NAME__ tool to hydrate this widget.\";\n\n        headlineEl.textContent = headline;\n        messageEl.textContent = message;\n\n        const theme = window.openai?.theme ?? \"bridge-only\";\n        metaEl.textContent =\n          \"Runtime: \" +\n          (window.openai ? \"MCP Apps bridge + optional window.openai\" : \"MCP Apps bridge only\") +\n          \" | Theme: \" +\n          theme;\n      };\n\n      const rpcNotify = (method, params) => {\n        window.parent.postMessage({ jsonrpc: \"2.0\", method, params }, \"*\");\n      };\n\n      const rpcRequest = (method, params) =>\n        new Promise((resolve, reject) => {\n          const id = ++rpcId;\n          pendingRequests.set(id, { resolve, reject });\n          window.parent.postMessage({ jsonrpc: \"2.0\", id, method, params }, \"*\");\n        });\n\n      window.addEventListener(\n        \"message\",\n        (event) => {\n          if (event.source !== window.parent) {\n            return;\n          }\n\n          const message = event.data;\n          if (!message || message.jsonrpc !== \"2.0\") {\n            return;\n          }\n\n          if (typeof message.id === \"number\") {\n            const pending = pendingRequests.get(message.id);\n            if (!pending) {\n              return;\n            }\n\n            pendingRequests.delete(message.id);\n            if (message.error) {\n              pending.reject(message.error);\n              return;\n            }\n\n            pending.resolve(message.result);\n            return;\n          }\n\n          if (message.method === \"ui/notifications/tool-result\") {\n            toolOutput = message.params?.structuredContent ?? null;\n            render();\n          }\n        },\n        { passive: true }\n      );\n\n      const initializeBridge = async () => {\n        await rpcRequest(\"ui/initialize\", {\n          appInfo: { name: \"__APP_SLUG__-widget\", version: \"0.1.0\" },\n          appCapabilities: {},\n          protocolVersion: \"2026-01-26\",\n        });\n        rpcNotify(\"ui/notifications/initialized\", {});\n      };\n\n      const bridgeReady = initializeBridge();\n\n      toolButtonEl.addEventListener(\"click\", async () => {\n        await bridgeReady;\n\n        const response = await rpcRequest(\"tools/call\", {\n          name: \"__TOOL_NAME__\",\n          arguments: {\n            message: \"Tool call triggered from the widget.\",\n          },\n        });\n\n        toolOutput = response?.structuredContent ?? toolOutput;\n        render();\n      });\n\n      followUpButtonEl.addEventListener(\"click\", async () => {\n        await bridgeReady;\n\n        rpcNotify(\"ui/message\", {\n          role: \"user\",\n          content: [\n            {\n              type: \"text\",\n              text: \"Explain how the __TOOL_NAME__ widget works.\",\n            },\n          ],\n        });\n      });\n\n      render();\n    </script>\n  </body>\n</html>\n`;\n\nconst SERVER_TEMPLATE = `import { createServer } from \"node:http\";\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport {\n  registerAppResource,\n  registerAppTool,\n  RESOURCE_MIME_TYPE,\n} from \"@modelcontextprotocol/ext-apps/server\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { z } from \"zod\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst ROOT_DIR = path.resolve(__dirname, \"..\");\nconst WIDGET_URI = \"__WIDGET_URI__\";\nconst WIDGET_HTML = readFileSync(\n  path.join(ROOT_DIR, \"public\", \"widget.html\"),\n  \"utf8\"\n);\n\nfunction createAppServer(): McpServer {\n  const server = new McpServer({\n    name: \"__APP_SLUG__\",\n    version: \"0.1.0\",\n  });\n\n  registerAppResource(\n    server,\n    \"main-widget\",\n    WIDGET_URI,\n    {},\n    async () => ({\n      contents: [\n        {\n          uri: WIDGET_URI,\n          mimeType: RESOURCE_MIME_TYPE,\n          text: WIDGET_HTML,\n          _meta: {\n            ui: {\n              prefersBorder: true,\n              csp: {\n                connectDomains: [],\n                resourceDomains: [],\n              },\n            },\n            \"openai/widgetDescription\":\n              \"__APP_TITLE__ starter widget rendered by the MCP server.\",\n          },\n        },\n      ],\n    })\n  );\n\n  registerAppTool(\n    server,\n    \"__TOOL_NAME__\",\n    {\n      title: \"__APP_TITLE__\",\n      description:\n        \"Use this when the user wants to render the __APP_TITLE__ starter widget or inspect a minimal Apps SDK tool result.\",\n      inputSchema: {\n        message: z\n          .string()\n          .optional()\n          .describe(\"Optional message to show inside the widget.\"),\n      },\n      annotations: {\n        readOnlyHint: true,\n        destructiveHint: false,\n        openWorldHint: false,\n        idempotentHint: true,\n      },\n      _meta: {\n        ui: { resourceUri: WIDGET_URI },\n        \"openai/toolInvocation/invoking\": \"Loading __APP_TITLE__\",\n        \"openai/toolInvocation/invoked\": \"__APP_TITLE__ ready\",\n      },\n    },\n    async ({ message }) => {\n      const resolvedMessage =\n        message?.trim() ||\n        \"This starter uses the MCP Apps bridge first, keeps follow-up messaging on ui/message, and limits window.openai to optional host signals.\";\n\n      return {\n        content: [\n          {\n            type: \"text\" as const,\n            text: \"Rendered the __APP_TITLE__ starter widget.\",\n          },\n        ],\n        structuredContent: {\n          headline: \"__APP_TITLE__\",\n          message: resolvedMessage,\n          source: \"__TOOL_NAME__\",\n          themeHint:\n            \"Read window.openai.theme in the widget if you need ChatGPT theme information.\",\n        },\n        _meta: {\n          \"openai/outputTemplate\": WIDGET_URI,\n        },\n      };\n    }\n  );\n\n  return server;\n}\n\nconst port = Number(process.env.PORT ?? \"__PORT__\");\nconst MCP_PATH = \"/mcp\";\n\ncreateServer(async (req, res) => {\n  if (!req.url) {\n    res.writeHead(400).end(\"Missing URL\");\n    return;\n  }\n\n  const url = new URL(req.url, \"http://\" + (req.headers.host ?? \"localhost\"));\n  const isMcpRoute = url.pathname === MCP_PATH || url.pathname.startsWith(MCP_PATH + \"/\");\n\n  if (req.method === \"OPTIONS\" && isMcpRoute) {\n    res.writeHead(204, {\n      \"Access-Control-Allow-Origin\": \"*\",\n      \"Access-Control-Allow-Methods\": \"POST, GET, DELETE, OPTIONS\",\n      \"Access-Control-Allow-Headers\": \"content-type, mcp-session-id\",\n      \"Access-Control-Expose-Headers\": \"Mcp-Session-Id\",\n    });\n    res.end();\n    return;\n  }\n\n  if (req.method === \"GET\" && url.pathname === \"/\") {\n    res.writeHead(200, { \"content-type\": \"text/plain\" }).end(\"__APP_TITLE__ MCP server\");\n    return;\n  }\n\n  const transportMethods = new Set([\"GET\", \"POST\", \"DELETE\"]);\n  if (isMcpRoute && req.method && transportMethods.has(req.method)) {\n    res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n    res.setHeader(\"Access-Control-Expose-Headers\", \"Mcp-Session-Id\");\n\n    const server = createAppServer();\n    const transport = new StreamableHTTPServerTransport({\n      sessionIdGenerator: undefined,\n      enableJsonResponse: true,\n    });\n\n    res.on(\"close\", () => {\n      transport.close();\n      server.close();\n    });\n\n    try {\n      await server.connect(transport);\n      await transport.handleRequest(req, res);\n    } catch (error) {\n      console.error(\"Failed to handle MCP request:\", error);\n      if (!res.headersSent) {\n        res.writeHead(500).end(\"Internal server error\");\n      }\n    }\n    return;\n  }\n\n  res.writeHead(404).end(\"Not Found\");\n}).listen(port, () => {\n  console.log(\"__APP_TITLE__ MCP server listening on http://localhost:\" + port + MCP_PATH);\n});\n`;\n\nfunction buildWidgetHtml(appSlug, appTitle, toolName) {\n  return fillTemplate(WIDGET_TEMPLATE, {\n    \"__APP_SLUG__\": appSlug,\n    \"__APP_TITLE__\": appTitle,\n    \"__TOOL_NAME__\": toolName,\n  });\n}\n\nfunction buildServerTs(appSlug, appTitle, toolName, widgetUri, port) {\n  return fillTemplate(SERVER_TEMPLATE, {\n    \"__APP_SLUG__\": appSlug,\n    \"__APP_TITLE__\": appTitle,\n    \"__TOOL_NAME__\": toolName,\n    \"__WIDGET_URI__\": widgetUri,\n    \"__PORT__\": String(port),\n  });\n}\n\nfunction usage() {\n  return [\n    \"Generate a minimal Node + @modelcontextprotocol/ext-apps starter with a vanilla widget that uses the MCP Apps bridge by default.\",\n    \"Prefer upstream examples first; use this scaffold as the fallback.\",\n    \"\",\n    \"Usage:\",\n    \"  ./scripts/scaffold_node_ext_apps.mjs <output_dir> [--app-name <name>] [--tool-name <name>] [--port <number>] [--force]\",\n    \"\",\n    \"If the executable bit is unavailable, run:\",\n    \"  node scripts/scaffold_node_ext_apps.mjs <output_dir> [--app-name <name>] [--tool-name <name>] [--port <number>] [--force]\",\n  ].join(\"\\\\n\");\n}\n\nfunction parseArgs(argv) {\n  const args = {\n    outputDir: null,\n    appName: \"example-chatgpt-app\",\n    toolName: null,\n    port: 8787,\n    force: false,\n  };\n\n  const tokens = [...argv];\n  while (tokens.length > 0) {\n    const token = tokens.shift();\n\n    if (!args.outputDir && !token.startsWith(\"--\")) {\n      args.outputDir = token;\n      continue;\n    }\n\n    if (token === \"--app-name\") {\n      args.appName = tokens.shift() ?? \"\";\n      continue;\n    }\n\n    if (token === \"--tool-name\") {\n      args.toolName = tokens.shift() ?? \"\";\n      continue;\n    }\n\n    if (token === \"--port\") {\n      const value = Number(tokens.shift());\n      if (!Number.isInteger(value) || value <= 0) {\n        throw new Error(\"Expected a positive integer after --port\");\n      }\n      args.port = value;\n      continue;\n    }\n\n    if (token === \"--force\") {\n      args.force = true;\n      continue;\n    }\n\n    if (token === \"--help\" || token === \"-h\") {\n      console.log(usage());\n      process.exit(0);\n    }\n\n    throw new Error(`Unknown argument: ${token}`);\n  }\n\n  if (!args.outputDir) {\n    throw new Error(`Missing required output directory.\\\\n\\\\n${usage()}`);\n  }\n\n  return args;\n}\n\nfunction main() {\n  const args = parseArgs(process.argv.slice(2));\n\n  const appSlug = toSlug(args.appName);\n  const toolName = toToolName(args.toolName || appSlug);\n  const appTitle = toTitle(appSlug);\n  const widgetUri = \"ui://widget/main-v1.html\";\n\n  const outputDir = path.resolve(args.outputDir);\n  ensureTargetDir(outputDir, args.force);\n\n  const files = new Map([\n    [path.join(outputDir, \"package.json\"), buildPackageJson(appSlug)],\n    [path.join(outputDir, \"tsconfig.json\"), buildTsconfig()],\n    [path.join(outputDir, \"public\", \"widget.html\"), buildWidgetHtml(appSlug, appTitle, toolName)],\n    [path.join(outputDir, \"src\", \"server.ts\"), buildServerTs(appSlug, appTitle, toolName, widgetUri, args.port)],\n  ]);\n\n  for (const [filePath, content] of files) {\n    writeFile(filePath, content);\n  }\n\n  console.log(\"Generated starter scaffold:\");\n  for (const filePath of files.keys()) {\n    console.log(\" -\", path.relative(outputDir, filePath));\n  }\n}\n\ntry {\n  main();\n} catch (error) {\n  console.error(error instanceof Error ? error.message : String(error));\n  process.exit(1);\n}\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/SKILL.md",
    "content": "---\nname: cloudflare-deploy\ndescription: Deploy applications and infrastructure to Cloudflare using Workers, Pages, and related platform services. Use when the user asks to deploy, host, publish, or set up a project on Cloudflare.\n---\n\n# Cloudflare Deploy\n\nConsolidated skill for building on the Cloudflare platform. Use decision trees below to find the right product, then load detailed references.\n\n## Prerequisites\n\n- When sandboxing blocks the deployment network calls, rerun with `sandbox_permissions=require_escalated`.\n- The deployment might take a few minutes. Use appropriate timeout values.\n\n## Authentication (Required Before Deploy)\n\nVerify auth before `wrangler deploy`, `wrangler pages deploy`, or `npm run deploy`:\n\n```bash\nnpx wrangler whoami    # Shows account if authenticated\n```\n\nNot authenticated? → `references/wrangler/auth.md`\n- Interactive/local: `wrangler login` (one-time OAuth)\n- CI/CD: Set `CLOUDFLARE_API_TOKEN` env var\n\n## Quick Decision Trees\n\n### \"I need to run code\"\n\n```\nNeed to run code?\n├─ Serverless functions at the edge → workers/\n├─ Full-stack web app with Git deploys → pages/\n├─ Stateful coordination/real-time → durable-objects/\n├─ Long-running multi-step jobs → workflows/\n├─ Run containers → containers/\n├─ Multi-tenant (customers deploy code) → workers-for-platforms/\n├─ Scheduled tasks (cron) → cron-triggers/\n├─ Lightweight edge logic (modify HTTP) → snippets/\n├─ Process Worker execution events (logs/observability) → tail-workers/\n└─ Optimize latency to backend infrastructure → smart-placement/\n```\n\n### \"I need to store data\"\n\n```\nNeed storage?\n├─ Key-value (config, sessions, cache) → kv/\n├─ Relational SQL → d1/ (SQLite) or hyperdrive/ (existing Postgres/MySQL)\n├─ Object/file storage (S3-compatible) → r2/\n├─ Message queue (async processing) → queues/\n├─ Vector embeddings (AI/semantic search) → vectorize/\n├─ Strongly-consistent per-entity state → durable-objects/ (DO storage)\n├─ Secrets management → secrets-store/\n├─ Streaming ETL to R2 → pipelines/\n└─ Persistent cache (long-term retention) → cache-reserve/\n```\n\n### \"I need AI/ML\"\n\n```\nNeed AI?\n├─ Run inference (LLMs, embeddings, images) → workers-ai/\n├─ Vector database for RAG/search → vectorize/\n├─ Build stateful AI agents → agents-sdk/\n├─ Gateway for any AI provider (caching, routing) → ai-gateway/\n└─ AI-powered search widget → ai-search/\n```\n\n### \"I need networking/connectivity\"\n\n```\nNeed networking?\n├─ Expose local service to internet → tunnel/\n├─ TCP/UDP proxy (non-HTTP) → spectrum/\n├─ WebRTC TURN server → turn/\n├─ Private network connectivity → network-interconnect/\n├─ Optimize routing → argo-smart-routing/\n├─ Optimize latency to backend (not user) → smart-placement/\n└─ Real-time video/audio → realtimekit/ or realtime-sfu/\n```\n\n### \"I need security\"\n\n```\nNeed security?\n├─ Web Application Firewall → waf/\n├─ DDoS protection → ddos/\n├─ Bot detection/management → bot-management/\n├─ API protection → api-shield/\n├─ CAPTCHA alternative → turnstile/\n└─ Credential leak detection → waf/ (managed ruleset)\n```\n\n### \"I need media/content\"\n\n```\nNeed media?\n├─ Image optimization/transformation → images/\n├─ Video streaming/encoding → stream/\n├─ Browser automation/screenshots → browser-rendering/\n└─ Third-party script management → zaraz/\n```\n\n### \"I need infrastructure-as-code\"\n\n```\nNeed IaC? → pulumi/ (Pulumi), terraform/ (Terraform), or api/ (REST API)\n```\n\n## Product Index\n\n### Compute & Runtime\n| Product | Reference |\n|---------|-----------|\n| Workers | `references/workers/` |\n| Pages | `references/pages/` |\n| Pages Functions | `references/pages-functions/` |\n| Durable Objects | `references/durable-objects/` |\n| Workflows | `references/workflows/` |\n| Containers | `references/containers/` |\n| Workers for Platforms | `references/workers-for-platforms/` |\n| Cron Triggers | `references/cron-triggers/` |\n| Tail Workers | `references/tail-workers/` |\n| Snippets | `references/snippets/` |\n| Smart Placement | `references/smart-placement/` |\n\n### Storage & Data\n| Product | Reference |\n|---------|-----------|\n| KV | `references/kv/` |\n| D1 | `references/d1/` |\n| R2 | `references/r2/` |\n| Queues | `references/queues/` |\n| Hyperdrive | `references/hyperdrive/` |\n| DO Storage | `references/do-storage/` |\n| Secrets Store | `references/secrets-store/` |\n| Pipelines | `references/pipelines/` |\n| R2 Data Catalog | `references/r2-data-catalog/` |\n| R2 SQL | `references/r2-sql/` |\n\n### AI & Machine Learning\n| Product | Reference |\n|---------|-----------|\n| Workers AI | `references/workers-ai/` |\n| Vectorize | `references/vectorize/` |\n| Agents SDK | `references/agents-sdk/` |\n| AI Gateway | `references/ai-gateway/` |\n| AI Search | `references/ai-search/` |\n\n### Networking & Connectivity\n| Product | Reference |\n|---------|-----------|\n| Tunnel | `references/tunnel/` |\n| Spectrum | `references/spectrum/` |\n| TURN | `references/turn/` |\n| Network Interconnect | `references/network-interconnect/` |\n| Argo Smart Routing | `references/argo-smart-routing/` |\n| Workers VPC | `references/workers-vpc/` |\n\n### Security\n| Product | Reference |\n|---------|-----------|\n| WAF | `references/waf/` |\n| DDoS Protection | `references/ddos/` |\n| Bot Management | `references/bot-management/` |\n| API Shield | `references/api-shield/` |\n| Turnstile | `references/turnstile/` |\n\n### Media & Content\n| Product | Reference |\n|---------|-----------|\n| Images | `references/images/` |\n| Stream | `references/stream/` |\n| Browser Rendering | `references/browser-rendering/` |\n| Zaraz | `references/zaraz/` |\n\n### Real-Time Communication\n| Product | Reference |\n|---------|-----------|\n| RealtimeKit | `references/realtimekit/` |\n| Realtime SFU | `references/realtime-sfu/` |\n\n### Developer Tools\n| Product | Reference |\n|---------|-----------|\n| Wrangler | `references/wrangler/` |\n| Miniflare | `references/miniflare/` |\n| C3 | `references/c3/` |\n| Observability | `references/observability/` |\n| Analytics Engine | `references/analytics-engine/` |\n| Web Analytics | `references/web-analytics/` |\n| Sandbox | `references/sandbox/` |\n| Workerd | `references/workerd/` |\n| Workers Playground | `references/workers-playground/` |\n\n### Infrastructure as Code\n| Product | Reference |\n|---------|-----------|\n| Pulumi | `references/pulumi/` |\n| Terraform | `references/terraform/` |\n| API | `references/api/` |\n\n### Other Services\n| Product | Reference |\n|---------|-----------|\n| Email Routing | `references/email-routing/` |\n| Email Workers | `references/email-workers/` |\n| Static Assets | `references/static-assets/` |\n| Bindings | `references/bindings/` |\n| Cache Reserve | `references/cache-reserve/` |\n\n## Troubleshooting\n\n### Escalated Network Access\n\nIf deployment fails due to network issues (timeouts, DNS errors, connection resets), rerun the deploy with escalated permissions (use `sandbox_permissions=require_escalated`). The deploy requires escalated network access when sandbox networking blocks outbound requests.\n\nExample guidance to the user:\n\n```\nThe deploy needs escalated network access to deploy to Cloudflare. I can rerun the command with escalated permissions—want me to proceed?\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Cloudflare Deploy\"\n  short_description: \"Deploy Workers, Pages, and platform services on Cloudflare\"\n  icon_small: \"./assets/cloudflare-small.svg\"\n  icon_large: \"./assets/cloudflare.png\"\n  default_prompt: \"Deploy this app to Cloudflare (Workers or Pages) and return URL, config, and required env vars.\"\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/agents-sdk/README.md",
    "content": "# Cloudflare Agents SDK\n\nCloudflare Agents SDK enables building AI-powered agents on Durable Objects with state, WebSockets, SQL, scheduling, and AI integration.\n\n## Core Value\nBuild stateful, globally distributed AI agents with persistent memory, real-time connections, scheduled tasks, and async workflows.\n\n## When to Use\n- Persistent state + memory required\n- Real-time WebSocket connections\n- Long-running workflows (minutes/hours)\n- Chat interfaces with AI models\n- Scheduled/recurring tasks with state\n- DB queries with agent state\n\n## What Type of Agent?\n\n| Use Case | Class | Key Features |\n|----------|-------|--------------|\n| AI chat interface | `AIChatAgent` | Auto-streaming, tools, message history, resumable |\n| MCP tool provider | `Agent` + MCP | Expose tools to AI systems |\n| Custom logic/routing | `Agent` | Full control, WebSockets, email, SQL |\n| Real-time collaboration | `Agent` | WebSocket state, broadcasts |\n| Email processing | `Agent` | `onEmail()` handler |\n\n## Quick Start\n\n**AI Chat Agent:**\n```typescript\nimport { AIChatAgent } from \"agents\";\nimport { openai } from \"@ai-sdk/openai\";\n\nexport class ChatAgent extends AIChatAgent<Env> {\n  async onChatMessage(onFinish) {\n    return this.streamText({\n      model: openai(\"gpt-4\"),\n      messages: this.messages,\n      onFinish,\n    });\n  }\n}\n```\n\n**Base Agent:**\n```typescript\nimport { Agent } from \"agents\";\n\nexport class MyAgent extends Agent<Env> {\n  onStart() {\n    this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY)`;\n  }\n  \n  async onRequest(request: Request) {\n    return Response.json({ state: this.state });\n  }\n}\n```\n\n## Reading Order\n\n| Task | Files to Read |\n|------|---------------|\n| Quick start | README only |\n| Build chat agent | README → api.md (AIChatAgent) → patterns.md |\n| Setup project | README → configuration.md |\n| Add React frontend | README → api.md (Client Hooks) → patterns.md |\n| Build MCP server | api.md (MCP) → patterns.md |\n| Background tasks | api.md (Scheduling, Task Queue) → patterns.md |\n| Debug issues | gotchas.md |\n\n## Package Entry Points\n\n| Import | Purpose |\n|--------|---------|\n| `agents` | Server-side Agent classes, lifecycle |\n| `agents/react` | `useAgent()` hook for WebSocket connections |\n| `agents/ai-react` | `useAgentChat()` hook for AI chat UIs |\n\n## In This Reference\n- [configuration.md](./configuration.md) - SDK setup, wrangler config, routing\n- [api.md](./api.md) - Agent classes, lifecycle, client hooks\n- [patterns.md](./patterns.md) - Common workflows, best practices\n- [gotchas.md](./gotchas.md) - Common issues, limits\n\n## See Also\n- durable-objects - Agent infrastructure\n- d1 - External database integration\n- workers-ai - AI model integration\n- vectorize - Vector search for RAG patterns"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/agents-sdk/api.md",
    "content": "# API Reference\n\n## Agent Classes\n\n### AIChatAgent\n\nFor AI chat with auto-streaming, message history, tools, resumable streaming.\n\n```ts\nimport { AIChatAgent } from \"agents\";\nimport { openai } from \"@ai-sdk/openai\";\n\nexport class ChatAgent extends AIChatAgent<Env> {\n  async onChatMessage(onFinish) {\n    return this.streamText({\n      model: openai(\"gpt-4\"),\n      messages: this.messages, // Auto-managed message history\n      tools: {\n        getWeather: {\n          description: \"Get weather\",\n          parameters: z.object({ city: z.string() }),\n          execute: async ({ city }) => `Sunny, 72°F in ${city}`\n        }\n      },\n      onFinish, // Persist response to this.messages\n    });\n  }\n}\n```\n\n### Agent (Base Class)\n\nFull control for custom logic, WebSockets, email, and SQL.\n\n```ts\nimport { Agent } from \"agents\";\n\nexport class MyAgent extends Agent<Env, State> {\n  // Lifecycle methods below\n}\n```\n\n**Type params:** `Agent<Env, State, ConnState>` - Env bindings, agent state, connection state\n\n## Lifecycle Hooks\n\n```ts\nonStart() { // Init/restart\n  this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT, name TEXT)`;\n}\n\nasync onRequest(req: Request) { // HTTP\n  const {pathname} = new URL(req.url);\n  if (pathname === \"/users\") return Response.json(this.sql<{id,name}>`SELECT * FROM users`);\n  return new Response(\"Not found\", {status: 404});\n}\n\nasync onConnect(conn: Connection<ConnState>, ctx: ConnectionContext) { // WebSocket\n  conn.accept();\n  conn.setState({userId: ctx.request.headers.get(\"X-User-ID\")});\n  conn.send(JSON.stringify({type: \"connected\", state: this.state}));\n}\n\nasync onMessage(conn: Connection<ConnState>, msg: WSMessage) { // WS messages\n  const m = JSON.parse(msg as string);\n  this.setState({messages: [...this.state.messages, m]});\n  this.connections.forEach(c => c.send(JSON.stringify(m)));\n}\n\nasync onEmail(email: AgentEmail) { // Email routing\n  this.sql`INSERT INTO emails (from_addr,subject,body) VALUES (${email.from},${email.headers.get(\"subject\")},${await email.text()})`;\n}\n```\n\n## State, SQL, Scheduling\n\n```ts\n// State\nthis.setState({count: 42}); // Auto-syncs\nthis.setState({...this.state, count: this.state.count + 1});\n\n// SQL (parameterized queries prevent injection)\nthis.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)`;\nthis.sql`INSERT INTO users (id,name) VALUES (${userId},${name})`;\nconst users = this.sql<{id,name}>`SELECT * FROM users WHERE id = ${userId}`;\n\n// Scheduling\nawait this.schedule(new Date(\"2026-12-25\"), \"sendGreeting\", {msg:\"Hi\"}); // Date\nawait this.schedule(60, \"checkStatus\", {}); // Delay (sec)\nawait this.schedule(\"0 0 * * *\", \"dailyCleanup\", {}); // Cron\nawait this.cancelSchedule(scheduleId);\n```\n\n## RPC Methods (@callable)\n\n```ts\nimport { Agent, callable } from \"agents\";\n\nexport class MyAgent extends Agent<Env> {\n  @callable()\n  async processTask(input: {text: string}): Promise<{result: string}> {\n    return { result: await this.env.AI.run(\"@cf/meta/llama-3.1-8b-instruct\", {prompt: input.text}) };\n  }\n}\n// Client: const result = await agent.processTask({ text: \"Hello\" });\n// Must return JSON-serializable values\n```\n\n## Connections & AI\n\n```ts\n// Connections (type: Agent<Env, State, ConnState>)\nthis.connections.forEach(c => c.send(JSON.stringify(msg))); // Broadcast\nconn.setState({userId:\"123\"}); conn.close(1000, \"Goodbye\");\n\n// Workers AI\nconst r = await this.env.AI.run(\"@cf/meta/llama-3.1-8b-instruct\", {prompt});\n\n// Manual streaming (prefer AIChatAgent)\nconst stream = await client.chat.completions.create({model: \"gpt-4\", messages, stream: true});\nfor await (const chunk of stream) conn.send(JSON.stringify({chunk: chunk.choices[0].delta.content}));\n```\n\n**Type-safe state:** `Agent<Env, State, ConnState>` - third param types `conn.state`\n\n## MCP Integration\n\nModel Context Protocol for exposing tools:\n\n```ts\n// Register & use MCP server\nawait this.mcp.registerServer(\"github\", {\n  url: env.MCP_SERVER_URL,\n  auth: { type: \"oauth\", clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET }\n});\nconst tools = await this.mcp.getAITools([\"github\"]);\nreturn this.streamText({ model: openai(\"gpt-4\"), messages: this.messages, tools, onFinish });\n```\n\n## Task Queue\n\n```ts\nawait this.queue(\"processVideo\", { videoId: \"abc123\" }); // Add task\nconst tasks = await this.dequeue(10); // Process up to 10\n```\n\n## Context & Cleanup\n\n```ts\nconst agent = getCurrentAgent<MyAgent>(); // Get current instance\nasync destroy() { /* cleanup before agent destroyed */ }\n```\n\n## AI Integration\n\n```ts\n// Workers AI\nconst r = await this.env.AI.run(\"@cf/meta/llama-3.1-8b-instruct\", {prompt});\n\n// Manual streaming (prefer AIChatAgent for auto-streaming)\nconst stream = await client.chat.completions.create({model: \"gpt-4\", messages, stream: true});\nfor await (const chunk of stream) {\n  if (chunk.choices[0]?.delta?.content) conn.send(JSON.stringify({chunk: chunk.choices[0].delta.content}));\n}\n```\n\n## Client Hooks (React)\n\n```ts\n// useAgent() - WebSocket connection + RPC\nimport { useAgent } from \"agents/react\";\nconst agent = useAgent({ agent: \"MyAgent\", name: \"user-123\" }); // name for idFromName\nconst result = await agent.processTask({ text: \"Hello\" }); // Call @callable methods\n// agent.readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED\n\n// useAgentChat() - AI chat UI\nimport { useAgentChat } from \"agents/ai-react\";\nconst agent = useAgent({ agent: \"ChatAgent\" });\nconst { messages, input, handleInputChange, handleSubmit, isLoading, stop, clearHistory } = \n  useAgentChat({ \n    agent, \n    maxSteps: 5,        // Max tool iterations\n    resume: true,       // Auto-resume on disconnect\n    onToolCall: async (toolCall) => {\n      // Client tools (human-in-the-loop)\n      if (toolCall.toolName === \"confirm\") return { ok: window.confirm(\"Proceed?\") };\n    }\n  });\n// status: \"ready\" | \"submitted\" | \"streaming\" | \"error\"\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/agents-sdk/configuration.md",
    "content": "# Configuration\n\n## Wrangler Setup\n\n```jsonc\n{\n  \"name\": \"my-agents-app\",\n  \"durable_objects\": {\n    \"bindings\": [\n      {\"name\": \"MyAgent\", \"class_name\": \"MyAgent\"}\n    ]\n  },\n  \"migrations\": [\n    {\"tag\": \"v1\", \"new_sqlite_classes\": [\"MyAgent\"]}\n  ],\n  \"ai\": {\n    \"binding\": \"AI\"\n  }\n}\n```\n\n## Environment Bindings\n\n**Type-safe pattern:**\n\n```typescript\ninterface Env {\n  AI?: Ai;                              // Workers AI\n  MyAgent?: DurableObjectNamespace<MyAgent>;\n  ChatAgent?: DurableObjectNamespace<ChatAgent>;\n  DB?: D1Database;                      // D1 database\n  KV?: KVNamespace;                     // KV storage\n  R2?: R2Bucket;                        // R2 bucket\n  OPENAI_API_KEY?: string;              // Secrets\n  GITHUB_CLIENT_ID?: string;            // MCP OAuth credentials\n  GITHUB_CLIENT_SECRET?: string;\n  QUEUE?: Queue;                        // Queues\n}\n```\n\n**Best practice:** Define all DO bindings in Env interface for type safety.\n\n## Deployment\n\n```bash\n# Local dev\nnpx wrangler dev\n\n# Deploy production\nnpx wrangler deploy\n\n# Set secrets\nnpx wrangler secret put OPENAI_API_KEY\n```\n\n## Agent Routing\n\n**Recommended: Use route helpers**\n\n```typescript\nimport { routeAgent } from \"agents\";\n\nexport default {\n  fetch(request: Request, env: Env) {\n    return routeAgent(request, env);\n  }\n}\n```\n\nHelper routes requests to agents automatically based on URL patterns.\n\n**Manual routing (advanced):**\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env) {\n    const url = new URL(request.url);\n    \n    // Named ID (deterministic)\n    const id = env.MyAgent.idFromName(\"user-123\");\n    \n    // Random ID (from URL param)\n    // const id = env.MyAgent.idFromString(url.searchParams.get(\"id\"));\n    \n    const stub = env.MyAgent.get(id);\n    return stub.fetch(request);\n  }\n}\n```\n\n**Multi-agent setup:**\n\n```typescript\nimport { routeAgent } from \"agents\";\n\nexport default {\n  fetch(request: Request, env: Env) {\n    const url = new URL(request.url);\n    \n    // Route by path\n    if (url.pathname.startsWith(\"/chat\")) {\n      return routeAgent(request, env, \"ChatAgent\");\n    }\n    if (url.pathname.startsWith(\"/task\")) {\n      return routeAgent(request, env, \"TaskAgent\");\n    }\n    \n    return new Response(\"Not found\", { status: 404 });\n  }\n}\n```\n\n## Email Routing\n\n**Code setup:**\n\n```typescript\nimport { routeAgentEmail } from \"agents\";\n\nexport default {\n  fetch: (req: Request, env: Env) => routeAgent(req, env),\n  email: (message: ForwardableEmailMessage, env: Env) => {\n    return routeAgentEmail(message, env);\n  }\n}\n```\n\n**Dashboard setup:**\n\nConfigure email routing in Cloudflare dashboard:\n\n```\nDestination: Workers with Durable Objects\nWorker: my-agents-app\n```\n\nThen handle in agent:\n\n```typescript\nexport class EmailAgent extends Agent<Env> {\n  async onEmail(email: AgentEmail) {\n    const text = await email.text();\n    // Process email\n  }\n}\n```\n\n## AI Gateway (Optional)\n\n```typescript\n// Enable caching/routing through AI Gateway\nconst response = await this.env.AI.run(\n  \"@cf/meta/llama-3.1-8b-instruct\",\n  { prompt },\n  {\n    gateway: {\n      id: \"my-gateway-id\",\n      skipCache: false,\n      cacheTtl: 3600\n    }\n  }\n);\n```\n\n## MCP Configuration (Optional)\n\nFor exposing tools via Model Context Protocol:\n\n```typescript\n// wrangler.jsonc - Add MCP OAuth secrets\n{\n  \"vars\": {\n    \"MCP_SERVER_URL\": \"https://mcp.example.com\"\n  }\n}\n\n// Set secrets via CLI\n// npx wrangler secret put GITHUB_CLIENT_ID\n// npx wrangler secret put GITHUB_CLIENT_SECRET\n```\n\nThen register in agent code (see api.md MCP section).\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/agents-sdk/gotchas.md",
    "content": "# Gotchas & Best Practices\n\n## Common Errors\n\n### \"setState() not syncing\"\n\n**Cause:** Mutating state directly or not calling `setState()` after modifications  \n**Solution:** Always use `setState()` with immutable updates:\n```ts\n// ❌ this.state.count++\n// ✅ this.setState({...this.state, count: this.state.count + 1})\n```\n\n### \"Message history grows unbounded (AIChatAgent)\"\n\n**Cause:** `this.messages` in `AIChatAgent` accumulates all messages indefinitely  \n**Solution:** Manually trim old messages periodically:\n```ts\nexport class ChatAgent extends AIChatAgent<Env> {\n  async onChatMessage(onFinish) {\n    // Keep only last 50 messages\n    if (this.messages.length > 50) {\n      this.messages = this.messages.slice(-50);\n    }\n    \n    return this.streamText({ model: openai(\"gpt-4\"), messages: this.messages, onFinish });\n  }\n}\n```\n\n### \"SQL injection vulnerability\"\n\n**Cause:** Direct string interpolation in SQL queries\n**Solution:** Use parameterized queries:\n```ts\n// ❌ this.sql`...WHERE id = '${userId}'`\n// ✅ this.sql`...WHERE id = ${userId}`\n```\n\n### \"WebSocket connection timeout\"\n\n**Cause:** Not calling `conn.accept()` in `onConnect`\n**Solution:** Always accept connections:\n```ts\nasync onConnect(conn: Connection, ctx: ConnectionContext) { conn.accept(); conn.setState({userId: \"123\"}); }\n```\n\n### \"Schedule limit exceeded\"\n\n**Cause:** More than 1000 scheduled tasks per agent\n**Solution:** Clean up old schedules and limit creation rate:\n```ts\nasync checkSchedules() { if ((await this.getSchedules()).length > 800) console.warn(\"Near limit!\"); }\n```\n\n### \"AI Gateway unavailable\"\n\n**Cause:** AI service timeout or quota exceeded  \n**Solution:** Add error handling and fallbacks:\n```ts\ntry { \n  return await this.env.AI.run(model, {prompt}); \n} catch (e) { \n  console.error(\"AI error:\", e);\n  return {error: \"Unavailable\"}; \n}\n```\n\n### \"@callable method returns undefined\"\n\n**Cause:** Method doesn't return JSON-serializable value, or has non-serializable types  \n**Solution:** Ensure return values are plain objects/arrays/primitives:\n```ts\n// ❌ Returns class instance\n@callable()\nasync getData() { return new Date(); }\n\n// ✅ Returns serializable object\n@callable()\nasync getData() { return { timestamp: Date.now() }; }\n```\n\n### \"Resumable stream not resuming\"\n\n**Cause:** Stream ID must be deterministic for resumption to work  \n**Solution:** Use AIChatAgent (automatic) or ensure consistent stream IDs:\n```ts\n// AIChatAgent handles this automatically\nexport class ChatAgent extends AIChatAgent<Env> {\n  // Resumption works out of the box\n}\n```\n\n### \"MCP connection loss on hibernation\"\n\n**Cause:** MCP server connections don't survive hibernation  \n**Solution:** Re-register servers in `onStart()` or check connection status:\n```ts\nonStart() {\n  // Re-register MCP servers after hibernation\n  await this.mcp.registerServer(\"github\", { url: env.MCP_URL, auth: {...} });\n}\n```\n\n### \"Agent not found\"\n\n**Cause:** Durable Object binding missing or incorrect class name  \n**Solution:** Verify DO binding in wrangler.jsonc and class name matches\n\n## Rate Limits & Quotas\n\n| Resource/Limit | Value | Notes |\n|----------------|-------|-------|\n| CPU per request | 30s (std), 300s (max) | Set in wrangler.jsonc |\n| Memory per instance | 128MB | Shared with WebSockets |\n| Storage per agent | 10GB | SQLite storage |\n| Scheduled tasks | 1000 per agent | Monitor with `getSchedules()` |\n| WebSocket connections | Unlimited | Within memory limits |\n| SQL columns | 100 | Per table |\n| SQL row size | 2MB | Key + value |\n| WebSocket message | 32MiB | Max size |\n| DO requests/sec | ~1000 | Per unique DO instance; rate limit if needed |\n| AI Gateway (Workers AI) | Model-specific | Check dashboard for limits |\n| MCP requests | Depends on server | Implement retry/backoff |\n\n## Best Practices\n\n### State Management\n- Use immutable updates: `setState({...this.state, key: newValue})`\n- Trim unbounded arrays (messages, logs) periodically\n- Store large data in SQL, not state\n\n### SQL Usage\n- Create tables in `onStart()`, not `onRequest()`\n- Use parameterized queries: `` sql`WHERE id = ${id}` `` (NOT `` sql`WHERE id = '${id}'` ``)\n- Index frequently queried columns\n\n### Scheduling\n- Monitor schedule count: `await this.getSchedules()`\n- Cancel completed tasks to stay under 1000 limit\n- Use cron strings for recurring tasks\n\n### WebSockets\n- Always call `conn.accept()` in `onConnect()`\n- Handle client disconnects gracefully\n- Broadcast to `this.connections` efficiently\n\n### AI Integration\n- Use `AIChatAgent` for chat interfaces (auto-streaming, resumption)\n- Trim message history to avoid token limits\n- Handle AI errors with try/catch and fallbacks\n\n### Production Deployment\n- **Rate limiting:** Implement request throttling for high-traffic agents (>1000 req/s)\n- **Monitoring:** Log critical errors, track schedule count, monitor storage usage\n- **Graceful degradation:** Handle AI service outages with fallbacks\n- **Message trimming:** Enforce max history length (e.g., 100 messages) in AIChatAgent\n- **MCP reliability:** Re-register servers on hibernation, implement retry logic\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/agents-sdk/patterns.md",
    "content": "# Patterns & Use Cases\n\n## AI Chat w/Tools\n\n**Server (AIChatAgent):**\n\n```ts\nimport { AIChatAgent } from \"agents\";\nimport { openai } from \"@ai-sdk/openai\";\nimport { tool } from \"ai\";\nimport { z } from \"zod\";\n\nexport class ChatAgent extends AIChatAgent<Env> {\n  async onChatMessage(onFinish) {\n    return this.streamText({\n      model: openai(\"gpt-4\"),\n      messages: this.messages, // Auto-managed\n      tools: {\n        getWeather: tool({\n          description: \"Get current weather\",\n          parameters: z.object({ city: z.string() }),\n          execute: async ({ city }) => `Weather in ${city}: Sunny, 72°F`\n        }),\n        searchDocs: tool({\n          description: \"Search documentation\",\n          parameters: z.object({ query: z.string() }),\n          execute: async ({ query }) => JSON.stringify(\n            this.sql<{title, content}>`SELECT title, content FROM docs WHERE content LIKE ${'%' + query + '%'}`\n          )\n        })\n      },\n      onFinish,\n    });\n  }\n}\n```\n\n**Client (React):**\n\n```tsx\nimport { useAgent } from \"agents/react\";\nimport { useAgentChat } from \"agents/ai-react\";\n\nfunction ChatUI() {\n  const agent = useAgent({ agent: \"ChatAgent\" });\n  const { messages, input, handleInputChange, handleSubmit, isLoading } = useAgentChat({ agent });\n  \n  return (\n    <div>\n      {messages.map(m => <div key={m.id}>{m.role}: {m.content}</div>)}\n      <form onSubmit={handleSubmit}>\n        <input value={input} onChange={handleInputChange} disabled={isLoading} />\n        <button disabled={isLoading}>Send</button>\n      </form>\n    </div>\n  );\n}\n```\n\n## Human-in-the-Loop (Client Tools)\n\nServer defines tool, client executes:\n\n```ts\n// Server\nexport class ChatAgent extends AIChatAgent<Env> {\n  async onChatMessage(onFinish) {\n    return this.streamText({\n      model: openai(\"gpt-4\"),\n      messages: this.messages,\n      tools: {\n        confirmAction: tool({\n          description: \"Ask user to confirm\",\n          parameters: z.object({ action: z.string() }),\n          execute: \"client\", // Client-side execution\n        })\n      },\n      onFinish,\n    });\n  }\n}\n\n// Client\nconst { messages } = useAgentChat({\n  agent,\n  onToolCall: async (toolCall) => {\n    if (toolCall.toolName === \"confirmAction\") {\n      return { confirmed: window.confirm(`Confirm: ${toolCall.args.action}?`) };\n    }\n  }\n});\n```\n\n## Task Queue & Scheduled Processing\n\n```ts\nexport class TaskAgent extends Agent<Env> {\n  onStart() { \n    this.schedule(\"*/5 * * * *\", \"processQueue\", {}); // Every 5 min\n    this.schedule(\"0 0 * * *\", \"dailyCleanup\", {}); // Daily\n  }\n  \n  async onRequest(req: Request) {\n    await this.queue(\"processVideo\", { videoId: (await req.json()).videoId });\n    return Response.json({ queued: true });\n  }\n  \n  async processQueue() {\n    const tasks = await this.dequeue(10);\n    for (const task of tasks) {\n      if (task.name === \"processVideo\") await this.processVideo(task.data.videoId);\n    }\n  }\n  \n  async dailyCleanup() {\n    this.sql`DELETE FROM logs WHERE created_at < ${Date.now() - 86400000}`;\n  }\n}\n```\n\n## Manual WebSocket Chat\n\nCustom protocols (non-AI):\n\n```ts\nexport class ChatAgent extends Agent<Env> {\n  async onConnect(conn: Connection, ctx: ConnectionContext) {\n    conn.accept();\n    conn.setState({userId: ctx.request.headers.get(\"X-User-ID\") || \"anon\"});\n    conn.send(JSON.stringify({type: \"history\", messages: this.state.messages}));\n  }\n  \n  async onMessage(conn: Connection, msg: WSMessage) {\n    const newMsg = {userId: conn.state.userId, text: JSON.parse(msg as string).text, timestamp: Date.now()};\n    this.setState({messages: [...this.state.messages, newMsg]});\n    this.connections.forEach(c => c.send(JSON.stringify(newMsg)));\n  }\n}\n```\n\n## Email Processing w/AI\n\n```ts\nexport class EmailAgent extends Agent<Env> {\n  async onEmail(email: AgentEmail) {\n    const [text, from, subject] = [await email.text(), email.from, email.headers.get(\"subject\") || \"\"];\n    this.sql`INSERT INTO emails (from_addr, subject, body) VALUES (${from}, ${subject}, ${text})`;\n    \n    const { text: summary } = await generateText({\n      model: openai(\"gpt-4o-mini\"), prompt: `Summarize: ${subject}\\n\\n${text}`\n    });\n    \n    this.connections.forEach(c => c.send(JSON.stringify({type: \"new_email\", from, summary})));\n    if (summary.includes(\"urgent\")) await this.schedule(0, \"sendAutoReply\", { to: from });\n  }\n}\n```\n\n## Real-time Collaboration\n\n```ts\nexport class GameAgent extends Agent<Env> {\n  initialState = { players: [], gameStarted: false };\n  \n  async onConnect(conn: Connection, ctx: ConnectionContext) {\n    conn.accept();\n    const playerId = ctx.request.headers.get(\"X-Player-ID\") || crypto.randomUUID();\n    conn.setState({ playerId });\n    \n    const newPlayer = { id: playerId, score: 0 };\n    this.setState({...this.state, players: [...this.state.players, newPlayer]});\n    this.connections.forEach(c => c.send(JSON.stringify({type: \"player_joined\", player: newPlayer})));\n  }\n  \n  async onMessage(conn: Connection, msg: WSMessage) {\n    const m = JSON.parse(msg as string);\n    \n    if (m.type === \"move\") {\n      this.setState({\n        ...this.state,\n        players: this.state.players.map(p => p.id === conn.state.playerId ? {...p, score: p.score + m.points} : p)\n      });\n      this.connections.forEach(c => c.send(JSON.stringify({type: \"player_moved\", playerId: conn.state.playerId})));\n    }\n    \n    if (m.type === \"start\" && this.state.players.length >= 2) {\n      this.setState({...this.state, gameStarted: true});\n      this.connections.forEach(c => c.send(JSON.stringify({type: \"game_started\"})));\n    }\n  }\n}\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ai-gateway/README.md",
    "content": "# Cloudflare AI Gateway\n\nExpert guidance for implementing Cloudflare AI Gateway - a universal gateway for AI model providers with analytics, caching, rate limiting, and routing capabilities.\n\n## When to Use This Reference\n\n- Setting up AI Gateway for any AI provider (OpenAI, Anthropic, Workers AI, etc.)\n- Implementing caching, rate limiting, or request retry/fallback\n- Configuring dynamic routing with A/B testing or model fallbacks\n- Managing provider API keys securely with BYOK\n- Adding security features (guardrails, DLP)\n- Setting up observability with logging and custom metadata\n- Debugging AI Gateway requests or optimizing configurations\n\n## Quick Start\n\n**What's your setup?**\n\n- **Using Vercel AI SDK** → Pattern 1 (recommended) - see [sdk-integration.md](./sdk-integration.md)\n- **Using OpenAI SDK** → Pattern 2 - see [sdk-integration.md](./sdk-integration.md)\n- **Cloudflare Worker + Workers AI** → Pattern 3 - see [sdk-integration.md](./sdk-integration.md)\n- **Direct HTTP (any language)** → Pattern 4 - see [configuration.md](./configuration.md)\n- **Framework (LangChain, etc.)** → See [sdk-integration.md](./sdk-integration.md)\n\n## Pattern 1: Vercel AI SDK (Recommended)\n\nMost modern pattern using official `ai-gateway-provider` package with automatic fallbacks.\n\n```typescript\nimport { createAiGateway } from 'ai-gateway-provider';\nimport { createOpenAI } from '@ai-sdk/openai';\nimport { generateText } from 'ai';\n\nconst gateway = createAiGateway({\n  accountId: process.env.CF_ACCOUNT_ID,\n  gateway: process.env.CF_GATEWAY_ID,\n});\n\nconst openai = createOpenAI({ \n  apiKey: process.env.OPENAI_API_KEY \n});\n\n// Single model\nconst { text } = await generateText({\n  model: gateway(openai('gpt-4o')),\n  prompt: 'Hello'\n});\n\n// Automatic fallback array\nconst { text } = await generateText({\n  model: gateway([\n    openai('gpt-4o'),              // Try first\n    anthropic('claude-sonnet-4-5'), // Fallback\n  ]),\n  prompt: 'Hello'\n});\n```\n\n**Install:** `npm install ai-gateway-provider ai @ai-sdk/openai @ai-sdk/anthropic`\n\n## Pattern 2: OpenAI SDK\n\nDrop-in replacement for OpenAI API with multi-provider support.\n\n```typescript\nimport OpenAI from 'openai';\n\nconst client = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY,\n  baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/compat`,\n  defaultHeaders: {\n    'cf-aig-authorization': `Bearer ${cfToken}` // For authenticated gateways\n  }\n});\n\n// Switch providers by changing model format: {provider}/{model}\nconst response = await client.chat.completions.create({\n  model: 'openai/gpt-4o', // or 'anthropic/claude-sonnet-4-5'\n  messages: [{ role: 'user', content: 'Hello!' }]\n});\n```\n\n## Pattern 3: Workers AI Binding\n\nFor Cloudflare Workers using Workers AI.\n\n```typescript\nexport default {\n  async fetch(request, env, ctx) {\n    const response = await env.AI.run(\n      '@cf/meta/llama-3-8b-instruct',\n      { messages: [{ role: 'user', content: 'Hello!' }] },\n      { \n        gateway: { \n          id: 'my-gateway',\n          metadata: { userId: '123', team: 'engineering' }\n        } \n      }\n    );\n    \n    return Response.json(response);\n  }\n};\n```\n\n## Headers Quick Reference\n\n| Header | Purpose | Example | Notes |\n|--------|---------|---------|-------|\n| `cf-aig-authorization` | Gateway auth | `Bearer {token}` | Required for authenticated gateways |\n| `cf-aig-metadata` | Tracking | `{\"userId\":\"x\"}` | Max 5 entries, flat structure |\n| `cf-aig-cache-ttl` | Cache duration | `3600` | Seconds, min 60, max 2592000 (30 days) |\n| `cf-aig-skip-cache` | Bypass cache | `true` | - |\n| `cf-aig-cache-key` | Custom cache key | `my-key` | Must be unique per response |\n| `cf-aig-collect-log` | Skip logging | `false` | Default: true |\n| `cf-aig-cache-status` | Cache hit/miss | Response only | `HIT` or `MISS` |\n\n## In This Reference\n\n| File | Purpose |\n|------|---------|\n| [sdk-integration.md](./sdk-integration.md) | Vercel AI SDK, OpenAI SDK, Workers binding patterns |\n| [configuration.md](./configuration.md) | Dashboard setup, wrangler, API tokens |\n| [features.md](./features.md) | Caching, rate limits, guardrails, DLP, BYOK, unified billing |\n| [dynamic-routing.md](./dynamic-routing.md) | Fallbacks, A/B testing, conditional routing |\n| [troubleshooting.md](./troubleshooting.md) | Debugging, errors, observability, gotchas |\n\n## Reading Order\n\n| Task | Files |\n|------|-------|\n| First-time setup | README + [configuration.md](./configuration.md) |\n| SDK integration | README + [sdk-integration.md](./sdk-integration.md) |\n| Enable caching | README + [features.md](./features.md) |\n| Setup fallbacks | README + [dynamic-routing.md](./dynamic-routing.md) |\n| Debug errors | README + [troubleshooting.md](./troubleshooting.md) |\n\n## Architecture\n\nAI Gateway acts as a proxy between your application and AI providers:\n\n```\nYour App → AI Gateway → AI Provider (OpenAI, Anthropic, etc.)\n         ↓\n    Analytics, Caching, Rate Limiting, Logging\n```\n\n**Key URL patterns:**\n- Unified API (OpenAI-compatible): `https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/compat/chat/completions`\n- Provider-specific: `https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/{provider}/{endpoint}`\n- Dynamic routes: Use route name instead of model: `dynamic/{route-name}`\n\n## Gateway Types\n\n1. **Unauthenticated Gateway**: Open access (not recommended for production)\n2. **Authenticated Gateway**: Requires `cf-aig-authorization` header with Cloudflare API token (recommended)\n\n## Provider Authentication Options\n\n1. **Unified Billing**: Use AI Gateway billing to pay for inference (keyless mode - no provider API key needed)\n2. **BYOK (Store Keys)**: Store provider API keys in Cloudflare dashboard\n3. **Request Headers**: Include provider API key in each request\n\n## Related Skills\n\n- [Workers AI](../workers-ai/README.md) - For `env.AI.run()` details\n- [Agents SDK](../agents-sdk/README.md) - For stateful AI patterns\n- [Vectorize](../vectorize/README.md) - For RAG patterns with embeddings\n\n## Resources\n\n- [Official Docs](https://developers.cloudflare.com/ai-gateway/)\n- [API Reference](https://developers.cloudflare.com/api/resources/ai_gateway/)\n- [Provider Guides](https://developers.cloudflare.com/ai-gateway/usage/providers/)\n- [Discord Community](https://discord.cloudflare.com)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ai-gateway/configuration.md",
    "content": "# Configuration & Setup\n\n## Creating a Gateway\n\n### Dashboard\nAI > AI Gateway > Create Gateway > Configure (auth, caching, rate limiting, logging)\n\n### API\n```bash\ncurl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/ai-gateway/gateways \\\n  -H \"Authorization: Bearer $CF_API_TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"my-gateway\",\"cache_ttl\":3600,\"rate_limiting_interval\":60,\"rate_limiting_limit\":100,\"collect_logs\":true}'\n```\n\n**Naming:** lowercase alphanumeric + hyphens (e.g., `prod-api`, `dev-chat`)\n\n## Wrangler Integration\n\n```toml\n[ai]\nbinding = \"AI\"\n\n[[ai.gateway]]\nid = \"my-gateway\"\n```\n\n```bash\nwrangler secret put CF_API_TOKEN\nwrangler secret put OPENAI_API_KEY  # If not using BYOK\n```\n\n## Authentication\n\n### Gateway Auth (protects gateway access)\n```typescript\nconst client = new OpenAI({\n  baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,\n  defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }\n});\n```\n\n### Provider Auth Options\n\n**1. Unified Billing (keyless)** - pay through Cloudflare, no provider key:\n```typescript\nconst client = new OpenAI({\n  baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,\n  defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }\n});\n```\nSupports: OpenAI, Anthropic, Google AI Studio\n\n**2. BYOK** - store keys in dashboard (Provider Keys > Add), no key in code\n\n**3. Request Headers** - pass provider key per request:\n```typescript\nconst client = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY,\n  baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,\n  defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }\n});\n```\n\n## API Token Permissions\n\n- **Gateway management:** AI Gateway - Read + Edit\n- **Gateway access:** AI Gateway - Read (minimum)\n\n## Gateway Management API\n\n```bash\n# List\ncurl https://api.cloudflare.com/client/v4/accounts/{account_id}/ai-gateway/gateways \\\n  -H \"Authorization: Bearer $CF_API_TOKEN\"\n\n# Get\ncurl .../gateways/{gateway_id}\n\n# Update\ncurl -X PUT .../gateways/{gateway_id} \\\n  -d '{\"cache_ttl\":7200,\"rate_limiting_limit\":200}'\n\n# Delete\ncurl -X DELETE .../gateways/{gateway_id}\n```\n\n## Getting IDs\n\n- **Account ID:** Dashboard > Overview > Copy\n- **Gateway ID:** AI Gateway > Gateway name column\n\n## Python Example\n\n```python\nfrom openai import OpenAI\nimport os\n\nclient = OpenAI(\n    api_key=os.environ.get(\"OPENAI_API_KEY\"),\n    base_url=f\"https://gateway.ai.cloudflare.com/v1/{os.environ['CF_ACCOUNT_ID']}/{os.environ['GATEWAY_ID']}/openai\",\n    default_headers={\"cf-aig-authorization\": f\"Bearer {os.environ['CF_API_TOKEN']}\"}\n)\n```\n\n## Best Practices\n\n1. **Always authenticate gateways in production**\n2. **Use BYOK or unified billing** - secrets out of code\n3. **Environment-specific gateways** - separate dev/staging/prod\n4. **Set rate limits** - prevent runaway costs\n5. **Enable logging** - track usage, debug issues\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ai-gateway/dynamic-routing.md",
    "content": "# Dynamic Routing\n\nConfigure complex routing in dashboard without code changes. Use route names instead of model names.\n\n## Usage\n\n```typescript\nconst response = await client.chat.completions.create({\n  model: 'dynamic/smart-chat', // Route name from dashboard\n  messages: [{ role: 'user', content: 'Hello!' }]\n});\n```\n\n## Node Types\n\n| Node | Purpose | Use Case |\n|------|---------|----------|\n| **Conditional** | Branch on metadata | Paid vs free users, geo routing |\n| **Percentage** | A/B split traffic | Model testing, gradual rollouts |\n| **Rate Limit** | Enforce quotas | Per-user/team limits |\n| **Budget Limit** | Cost quotas | Per-user spending caps |\n| **Model** | Call provider | Final destination |\n\n## Metadata\n\nPass via header (max 5 entries, flat only):\n```typescript\nheaders: {\n  'cf-aig-metadata': JSON.stringify({\n    userId: 'user-123',\n    tier: 'pro',\n    region: 'us-east'\n  })\n}\n```\n\n## Common Patterns\n\n**Multi-model fallback:**\n```\nStart → GPT-4 → On error: Claude → On error: Llama\n```\n\n**Tiered access:**\n```\nConditional: tier == 'enterprise' → GPT-4 (no limit)\nConditional: tier == 'pro' → Rate Limit 1000/hr → GPT-4o\nConditional: tier == 'free' → Rate Limit 10/hr → GPT-4o-mini\n```\n\n**Gradual rollout:**\n```\nPercentage: 10% → New model, 90% → Old model\n```\n\n**Cost-based fallback:**\n```\nBudget Limit: $100/day per teamId\n  < 80%: GPT-4\n  >= 80%: GPT-4o-mini\n  >= 100%: Error\n```\n\n## Version Management\n\n- Save changes as new version\n- Test with `model: 'dynamic/route@v2'`\n- Roll back by deploying previous version\n\n## Monitoring\n\nDashboard → Gateway → Dynamic Routes:\n- Request count per path\n- Success/error rates\n- Latency/cost by path\n\n## Limitations\n\n- Max 5 metadata entries\n- Values: string/number/boolean/null only\n- No nested objects\n- Route names: alphanumeric + hyphens\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ai-gateway/features.md",
    "content": "# Features & Capabilities\n\n## Caching\n\nDashboard: Settings → Cache Responses → Enable\n\n```typescript\n// Custom TTL (1 hour)\nheaders: { 'cf-aig-cache-ttl': '3600' }\n\n// Skip cache\nheaders: { 'cf-aig-skip-cache': 'true' }\n\n// Custom cache key\nheaders: { 'cf-aig-cache-key': 'greeting-en' }\n```\n\n**Limits:** TTL 60s - 30 days. **Does NOT work with streaming.**\n\n## Rate Limiting\n\nDashboard: Settings → Rate-limiting → Enable\n\n- **Fixed window:** Resets at intervals\n- **Sliding window:** Rolling window (more accurate)\n- Returns `429` when exceeded\n\n## Guardrails\n\nDashboard: Settings → Guardrails → Enable\n\nFilter prompts/responses for inappropriate content. Actions: Flag (log) or Block (reject).\n\n## Data Loss Prevention (DLP)\n\nDashboard: Settings → DLP → Enable\n\nDetect PII (emails, SSNs, credit cards). Actions: Flag, Block, or Redact.\n\n## Billing Modes\n\n| Mode | Description | Setup |\n|------|-------------|-------|\n| **Unified Billing** | Pay through Cloudflare, no provider keys | Use `cf-aig-authorization` header only |\n| **BYOK** | Store provider keys in dashboard | Add keys in Provider Keys section |\n| **Pass-through** | Send provider key with each request | Include provider's auth header |\n\n## Zero Data Retention\n\nDashboard: Settings → Privacy → Zero Data Retention\n\nNo prompts/responses stored. Request counts and costs still tracked.\n\n## Logging\n\nDashboard: Settings → Logs → Enable (up to 10M logs)\n\nEach entry: prompt, response, provider, model, tokens, cost, duration, cache status, metadata.\n\n```typescript\n// Skip logging for request\nheaders: { 'cf-aig-collect-log': 'false' }\n```\n\n**Export:** Use Logpush to S3, GCS, Datadog, Splunk, etc.\n\n## Custom Cost Tracking\n\nFor models not in Cloudflare's pricing database:\n\nDashboard: Gateway → Settings → Custom Costs\n\nOr via API: set `model`, `input_cost`, `output_cost`.\n\n## Supported Providers (22+)\n\n| Provider | Unified API | Notes |\n|----------|-------------|-------|\n| OpenAI | `openai/gpt-4o` | Full support |\n| Anthropic | `anthropic/claude-sonnet-4-5` | Full support |\n| Google AI | `google-ai-studio/gemini-2.0-flash` | Full support |\n| Workers AI | `workersai/@cf/meta/llama-3` | Native |\n| Azure OpenAI | `azure-openai/*` | Deployment names |\n| AWS Bedrock | Provider endpoint only | `/bedrock/*` |\n| Groq | `groq/*` | Fast inference |\n| Mistral, Cohere, Perplexity, xAI, DeepSeek, Cerebras | Full support | - |\n\n## Best Practices\n\n1. Enable caching for deterministic prompts\n2. Set rate limits to prevent abuse\n3. Use guardrails for user-facing AI\n4. Enable DLP for sensitive data\n5. Use unified billing or BYOK for simpler key management\n6. Enable logging for debugging\n7. Use zero data retention when privacy required\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ai-gateway/sdk-integration.md",
    "content": "# AI Gateway SDK Integration\n\n## Vercel AI SDK (Recommended)\n\n```typescript\nimport { createAiGateway } from 'ai-gateway-provider';\nimport { createOpenAI } from '@ai-sdk/openai';\nimport { generateText } from 'ai';\n\nconst gateway = createAiGateway({\n  accountId: process.env.CF_ACCOUNT_ID,\n  gateway: process.env.CF_GATEWAY_ID,\n  apiKey: process.env.CF_API_TOKEN // Optional for auth gateways\n});\n\nconst openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\n// Single model\nconst { text } = await generateText({\n  model: gateway(openai('gpt-4o')),\n  prompt: 'Hello'\n});\n\n// Automatic fallback array\nconst { text } = await generateText({\n  model: gateway([\n    openai('gpt-4o'),\n    anthropic('claude-sonnet-4-5'),\n    openai('gpt-4o-mini')\n  ]),\n  prompt: 'Complex task'\n});\n```\n\n### Options\n\n```typescript\nmodel: gateway(openai('gpt-4o'), {\n  cacheKey: 'my-key',\n  cacheTtl: 3600,\n  metadata: { userId: 'u123', team: 'eng' }, // Max 5 entries\n  retries: { maxAttempts: 3, backoff: 'exponential' }\n})\n```\n\n## OpenAI SDK\n\n```typescript\nconst client = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY,\n  baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,\n  defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }\n});\n\n// Unified API - switch providers via model name\nmodel: 'openai/gpt-4o'  // or 'anthropic/claude-sonnet-4-5'\n```\n\n## Anthropic SDK\n\n```typescript\nconst client = new Anthropic({\n  apiKey: process.env.ANTHROPIC_API_KEY,\n  baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/anthropic`,\n  defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }\n});\n```\n\n## Workers AI Binding\n\n```toml\n# wrangler.toml\n[ai]\nbinding = \"AI\"\n[[ai.gateway]]\nid = \"my-gateway\"\n```\n\n```typescript\nawait env.AI.run('@cf/meta/llama-3-8b-instruct', \n  { messages: [...] },\n  { gateway: { id: 'my-gateway', metadata: { userId: '123' } } }\n);\n```\n\n## LangChain / LlamaIndex\n\n```typescript\n// Use OpenAI SDK pattern with custom baseURL\nnew ChatOpenAI({\n  configuration: {\n    baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`\n  }\n});\n```\n\n## HTTP / cURL\n\n```bash\ncurl https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai/chat/completions \\\n  -H \"Authorization: Bearer $OPENAI_KEY\" \\\n  -H \"cf-aig-authorization: Bearer $CF_TOKEN\" \\\n  -H \"cf-aig-metadata: {\\\"userId\\\":\\\"123\\\"}\" \\\n  -d '{\"model\":\"gpt-4o\",\"messages\":[...]}'\n```\n\n## Headers Reference\n\n| Header | Purpose |\n|--------|---------|\n| `cf-aig-authorization` | Gateway auth token |\n| `cf-aig-metadata` | JSON object (max 5 keys) |\n| `cf-aig-cache-ttl` | Cache TTL in seconds |\n| `cf-aig-skip-cache` | `true` to bypass cache |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ai-gateway/troubleshooting.md",
    "content": "# AI Gateway Troubleshooting\n\n## Common Errors\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| 401 | Missing `cf-aig-authorization` header | Add header with CF API token |\n| 403 | Invalid provider key / BYOK expired | Check provider key in dashboard |\n| 429 | Rate limit exceeded | Increase limit or implement backoff |\n\n### 401 Fix\n\n```typescript\nconst client = new OpenAI({\n  baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,\n  defaultHeaders: { 'cf-aig-authorization': `Bearer ${CF_API_TOKEN}` }\n});\n```\n\n### 429 Retry Pattern\n\n```typescript\nasync function requestWithRetry(fn, maxRetries = 3) {\n  for (let i = 0; i < maxRetries; i++) {\n    try { return await fn(); }\n    catch (e) {\n      if (e.status === 429 && i < maxRetries - 1) {\n        await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));\n        continue;\n      }\n      throw e;\n    }\n  }\n}\n```\n\n## Gotchas\n\n| Issue | Reality |\n|-------|---------|\n| Metadata limits | Max 5 entries, flat only (no nesting) |\n| Cache key collision | Use unique keys per expected response |\n| BYOK + Unified Billing | Mutually exclusive |\n| Rate limit scope | Per-gateway, not per-user (use dynamic routing for per-user) |\n| Log delay | 30-60 seconds normal |\n| Streaming + caching | **Incompatible** |\n| Model name (unified API) | Prefix required: `openai/gpt-4o`, not `gpt-4o` |\n\n## Cache Not Working\n\n**Causes:**\n- Different request params (temperature, etc.)\n- Streaming enabled\n- Caching disabled in settings\n\n**Check:** `response.headers.get('cf-aig-cache-status')` → HIT or MISS\n\n## Logs Not Appearing\n\n1. Check logging enabled: Dashboard → Gateway → Settings\n2. Remove `cf-aig-collect-log: false` header\n3. Wait 30-60 seconds\n4. Check log limit (10M default)\n\n## Debugging\n\n```bash\n# Test connectivity\ncurl -v https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai/models \\\n  -H \"Authorization: Bearer $OPENAI_KEY\" \\\n  -H \"cf-aig-authorization: Bearer $CF_TOKEN\"\n```\n\n```typescript\n// Check response headers\nconsole.log('Cache:', response.headers.get('cf-aig-cache-status'));\nconsole.log('Request ID:', response.headers.get('cf-ray'));\n```\n\n## Analytics\n\nDashboard → AI Gateway → Select gateway\n\n**Metrics:** Requests, tokens, latency (p50/p95/p99), cache hit rate, costs\n\n**Log filters:** `status: error`, `provider: openai`, `cost > 0.01`, `duration > 1000`\n\n**Export:** Logpush to S3/GCS/Datadog/Splunk\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ai-search/README.md",
    "content": "# Cloudflare AI Search Reference\n\nExpert guidance for implementing Cloudflare AI Search (formerly AutoRAG), Cloudflare's managed semantic search and RAG service.\n\n## Overview\n\n**AI Search** is a managed RAG (Retrieval-Augmented Generation) pipeline that combines:\n- Automatic semantic indexing of your content\n- Vector similarity search\n- Built-in LLM generation\n\n**Key value propositions:**\n- **Zero vector management** - No manual embedding, indexing, or storage\n- **Auto-indexing** - Content automatically re-indexed every 6 hours\n- **Built-in generation** - Optional AI response generation from retrieved context\n- **Multi-source** - Index from R2 buckets or website crawls\n\n**Data source options:**\n- **R2 bucket** - Index files from Cloudflare R2 (supports MD, TXT, HTML, PDF, DOC, CSV, JSON)\n- **Website** - Crawl and index website content (requires Cloudflare-hosted domain)\n\n**Indexing lifecycle:**\n- Automatic 6-hour refresh cycle\n- Manual \"Force Sync\" available (30s rate limit)\n- Not designed for real-time updates\n\n## Quick Start\n\n**1. Create AI Search instance in dashboard:**\n- Go to Cloudflare Dashboard → AI Search → Create\n- Choose data source (R2 or website)\n- Configure instance name and settings\n\n**2. Configure Worker:**\n\n```jsonc\n// wrangler.jsonc\n{\n  \"ai\": {\n    \"binding\": \"AI\"\n  }\n}\n```\n\n**3. Use in Worker:**\n\n```typescript\nexport default {\n  async fetch(request, env) {\n    const answer = await env.AI.autorag(\"my-search-instance\").aiSearch({\n      query: \"How do I configure caching?\",\n      model: \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"\n    });\n    \n    return Response.json({ answer: answer.response });\n  }\n};\n```\n\n## When to Use AI Search\n\n### AI Search vs Vectorize\n\n| Factor | AI Search | Vectorize |\n|--------|-----------|-----------|\n| **Management** | Fully managed | Manual embedding + indexing |\n| **Use when** | Want zero-ops RAG pipeline | Need custom embeddings/control |\n| **Indexing** | Automatic (6hr cycle) | Manual via API |\n| **Generation** | Built-in optional | Bring your own LLM |\n| **Data sources** | R2 or website | Manual insert |\n| **Best for** | Docs, support, enterprise search | Custom ML pipelines, real-time |\n\n### AI Search vs Direct Workers AI\n\n| Factor | AI Search | Workers AI (direct) |\n|--------|-----------|---------------------|\n| **Context** | Automatic retrieval | Manual context building |\n| **Use when** | Need RAG (search + generate) | Simple generation tasks |\n| **Indexing** | Built-in | Not applicable |\n| **Best for** | Knowledge bases, docs | Simple chat, transformations |\n\n### search() vs aiSearch()\n\n| Method | Returns | Use When |\n|--------|---------|----------|\n| `search()` | Search results only | Building custom UI, need raw chunks |\n| `aiSearch()` | AI response + results | Need ready-to-use answer (chatbot, Q&A) |\n\n### Real-time Updates Consideration\n\n**AI Search is NOT ideal if:**\n- Need real-time content updates (<6 hours)\n- Content changes multiple times per hour\n- Strict freshness requirements\n\n**AI Search IS ideal if:**\n- Content relatively stable (docs, policies, knowledge bases)\n- 6-hour refresh acceptable\n- Prefer zero-ops over real-time\n\n## Platform Limits\n\n| Limit | Value |\n|-------|-------|\n| Max instances per account | 10 |\n| Max files per instance | 100,000 |\n| Max file size | 4 MB |\n| Index frequency | Every 6 hours |\n| Force Sync rate limit | Once per 30 seconds |\n| Filter nesting depth | 2 levels |\n| Filters per compound | 10 |\n| Score threshold range | 0.0 - 1.0 |\n\n## Reading Order\n\nNavigate these references based on your task:\n\n| Task | Read | Est. Time |\n|------|------|-----------|\n| **Understand AI Search** | README only | 5 min |\n| **Implement basic search** | README → api.md | 10 min |\n| **Configure data source** | README → configuration.md | 10 min |\n| **Production patterns** | patterns.md | 15 min |\n| **Debug issues** | gotchas.md | 10 min |\n| **Full implementation** | README → api.md → patterns.md | 30 min |\n\n## In This Reference\n\n- **[api.md](api.md)** - API endpoints, methods, TypeScript interfaces\n- **[configuration.md](configuration.md)** - Setup, data sources, wrangler config\n- **[patterns.md](patterns.md)** - Common patterns, decision guidance, code examples\n- **[gotchas.md](gotchas.md)** - Troubleshooting, code-level gotchas, limits\n\n## See Also\n\n- [Cloudflare AI Search Docs](https://developers.cloudflare.com/ai-search/)\n- [Workers AI Docs](https://developers.cloudflare.com/workers-ai/)\n- [Vectorize Docs](https://developers.cloudflare.com/vectorize/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ai-search/api.md",
    "content": "# AI Search API Reference\n\n## Workers Binding\n\n```typescript\nconst answer = await env.AI.autorag(\"instance-name\").aiSearch(options);\nconst results = await env.AI.autorag(\"instance-name\").search(options);\nconst instances = await env.AI.autorag(\"_\").listInstances();\n```\n\n## aiSearch() Options\n\n```typescript\ninterface AiSearchOptions {\n  query: string;                          // User query\n  model: string;                          // Workers AI model ID\n  system_prompt?: string;                 // LLM instructions\n  rewrite_query?: boolean;                // Fix typos (default: false)\n  max_num_results?: number;               // Max chunks (default: 10)\n  ranking_options?: { score_threshold?: number }; // 0.0-1.0 (default: 0.3)\n  reranking?: { enabled: boolean; model: string };\n  stream?: boolean;                       // Stream response (default: false)\n  filters?: Filter;                       // Metadata filters\n  page?: string;                          // Pagination token\n}\n```\n\n## Response\n\n```typescript\ninterface AiSearchResponse {\n  search_query: string;      // Query used (rewritten if enabled)\n  response: string;          // AI-generated answer\n  data: SearchResult[];      // Retrieved chunks\n  has_more: boolean;\n  next_page?: string;\n}\n\ninterface SearchResult {\n  id: string;\n  score: number;\n  content: string;\n  metadata: { filename: string; folder: string; timestamp: number };\n}\n```\n\n## Filters\n\n```typescript\n// Comparison\n{ column: \"folder\", operator: \"gte\", value: \"docs/\" }\n\n// Compound\n{ operator: \"and\", filters: [\n  { column: \"folder\", operator: \"gte\", value: \"docs/\" },\n  { column: \"timestamp\", operator: \"gte\", value: 1704067200 }\n]}\n```\n\n**Operators:** `eq`, `ne`, `gt`, `gte`, `lt`, `lte`\n\n**Built-in metadata:** `filename`, `folder`, `timestamp` (Unix seconds)\n\n## Streaming\n\n```typescript\nconst stream = await env.AI.autorag(\"docs\").aiSearch({ query, model, stream: true });\nreturn new Response(stream, { headers: { \"Content-Type\": \"text/event-stream\" } });\n```\n\n## Error Types\n\n| Error | Cause |\n|-------|-------|\n| `AutoRAGNotFoundError` | Instance doesn't exist |\n| `AutoRAGUnauthorizedError` | Invalid/missing token |\n| `AutoRAGValidationError` | Invalid parameters |\n\n## REST API\n\n```bash\ncurl https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/autorag/rags/{NAME}/ai-search \\\n  -H \"Authorization: Bearer {TOKEN}\" \\\n  -d '{\"query\": \"...\", \"model\": \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"}'\n```\n\nRequires Service API token with \"AI Search - Read\" permission.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ai-search/configuration.md",
    "content": "# AI Search Configuration\n\n## Worker Setup\n\n```jsonc\n// wrangler.jsonc\n{\n  \"ai\": { \"binding\": \"AI\" }\n}\n```\n\n```typescript\ninterface Env {\n  AI: Ai;\n}\n\nconst answer = await env.AI.autorag(\"my-instance\").aiSearch({\n  query: \"How do I configure caching?\",\n  model: \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"\n});\n```\n\n## Data Sources\n\n### R2 Bucket\n\nDashboard: AI Search → Create Instance → Select R2 bucket\n\n**Supported formats:** `.md`, `.txt`, `.html`, `.pdf`, `.doc`, `.docx`, `.csv`, `.json`\n\n**Auto-indexed metadata:** `filename`, `folder`, `timestamp`\n\n### Website Crawler\n\nRequirements:\n- Domain on Cloudflare\n- `sitemap.xml` at root\n- Bot protection must allow `CloudflareAISearch` user agent\n\n## Path Filtering (R2)\n\n```\ndocs/**/*.md          # All .md in docs/ recursively\n**/*.draft.md         # Exclude (use in exclude patterns)\n```\n\n## Indexing\n\n- **Automatic:** Every 6 hours\n- **Force Sync:** Dashboard button (30s rate limit between syncs)\n- **Pause:** Settings → Pause Indexing (existing index remains searchable)\n\n## Service API Token\n\nDashboard: AI Search → Instance → Use AI Search → API → Create Token\n\nPermissions:\n- **Read** - search operations\n- **Edit** - instance management\n\nStore securely:\n```bash\nwrangler secret put AI_SEARCH_TOKEN\n```\n\n## Multi-Environment\n\n```toml\n# wrangler.toml\n[env.production.vars]\nAI_SEARCH_INSTANCE = \"prod-docs\"\n\n[env.staging.vars]\nAI_SEARCH_INSTANCE = \"staging-docs\"\n```\n\n```typescript\nconst answer = await env.AI.autorag(env.AI_SEARCH_INSTANCE).aiSearch({ query });\n```\n\n## Monitoring\n\n```typescript\nconst instances = await env.AI.autorag(\"_\").listInstances();\nconsole.log(instances.find(i => i.name === \"docs\"));\n```\n\nDashboard shows: files indexed, status, last index time, storage usage.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ai-search/gotchas.md",
    "content": "# AI Search Gotchas\n\n## Type Safety\n\n**Timestamp precision:** Use seconds (10-digit), not milliseconds.\n```typescript\nconst nowInSeconds = Math.floor(Date.now() / 1000); // Correct\n```\n\n**Folder prefix matching:** Use `gte` for \"starts with\" on paths.\n```typescript\nfilters: { column: \"folder\", operator: \"gte\", value: \"docs/api/\" } // Matches nested\n```\n\n## Filter Limitations\n\n| Limit | Value |\n|-------|-------|\n| Max nesting depth | 2 levels |\n| Filters per compound | 10 |\n| `or` operator | Same column, `eq` only |\n\n**OR restriction example:**\n```typescript\n// ✅ Valid: same column, eq only\n{ operator: \"or\", filters: [\n  { column: \"folder\", operator: \"eq\", value: \"docs/\" },\n  { column: \"folder\", operator: \"eq\", value: \"guides/\" }\n]}\n```\n\n## Indexing Issues\n\n| Problem | Cause | Solution |\n|---------|-------|----------|\n| File not indexed | Unsupported format or >4MB | Check format (.md/.txt/.html/.pdf/.doc/.csv/.json) |\n| Index out of sync | 6-hour index cycle | Wait or use \"Force Sync\" (30s rate limit) |\n| Empty results | Index incomplete | Check dashboard for indexing status |\n\n## Auth Errors\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| `AutoRAGUnauthorizedError` | Invalid/missing token | Create Service API token with AI Search permissions |\n| `AutoRAGNotFoundError` | Wrong instance name | Verify exact name from dashboard |\n\n## Performance\n\n**Slow responses (>3s):**\n```typescript\n// Add score threshold + limit results\nranking_options: { score_threshold: 0.5 },\nmax_num_results: 10\n```\n\n**Empty results debug:**\n1. Remove filters, test basic query\n2. Lower `score_threshold` to 0.1\n3. Check index is populated\n\n## Limits\n\n| Resource | Limit |\n|----------|-------|\n| Instances per account | 10 |\n| Files per instance | 100,000 |\n| Max file size | 4 MB |\n| Index frequency | 6 hours |\n\n## Anti-Patterns\n\n**Use env vars for instance names:**\n```typescript\nconst answer = await env.AI.autorag(env.AI_SEARCH_INSTANCE).aiSearch({...});\n```\n\n**Handle specific error types:**\n```typescript\nif (error instanceof AutoRAGNotFoundError) { /* 404 */ }\nif (error instanceof AutoRAGUnauthorizedError) { /* 401 */ }\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ai-search/patterns.md",
    "content": "# AI Search Patterns\n\n## search() vs aiSearch()\n\n| Use | Method | Returns |\n|-----|--------|---------|\n| Custom UI, analytics | `search()` | Raw chunks only (~100-300ms) |\n| Chatbots, Q&A | `aiSearch()` | AI response + chunks (~500-2000ms) |\n\n## rewrite_query\n\n| Setting | Use When |\n|---------|----------|\n| `true` | User input (typos, vague queries) |\n| `false` | LLM-generated queries (already optimized) |\n\n## Multitenancy (Folder-Based)\n\n```typescript\nconst answer = await env.AI.autorag(\"saas-docs\").aiSearch({\n  query: \"refund policy\",\n  model: \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\",\n  filters: {\n    column: \"folder\",\n    operator: \"gte\",  // \"starts with\" pattern\n    value: `tenants/${tenantId}/`\n  }\n});\n```\n\n## Streaming\n\n```typescript\nconst stream = await env.AI.autorag(\"docs\").aiSearch({\n  query, model: \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\", stream: true\n});\nreturn new Response(stream, { headers: { \"Content-Type\": \"text/event-stream\" } });\n```\n\n## Score Threshold\n\n| Threshold | Use |\n|-----------|-----|\n| 0.3 (default) | Broad recall, exploratory |\n| 0.5 | Balanced, production default |\n| 0.7 | High precision, critical accuracy |\n\n## System Prompt Template\n\n```typescript\nconst systemPrompt = `You are a documentation assistant.\n- Answer ONLY based on provided context\n- If context doesn't contain answer, say \"I don't have information\"\n- Include code examples from context`;\n```\n\n## Compound Filters\n\n```typescript\n// OR: Multiple folders\nfilters: {\n  operator: \"or\",\n  filters: [\n    { column: \"folder\", operator: \"gte\", value: \"docs/api/\" },\n    { column: \"folder\", operator: \"gte\", value: \"docs/auth/\" }\n  ]\n}\n\n// AND: Folder + date\nfilters: {\n  operator: \"and\",\n  filters: [\n    { column: \"folder\", operator: \"gte\", value: \"docs/\" },\n    { column: \"timestamp\", operator: \"gte\", value: oneWeekAgoSeconds }\n  ]\n}\n```\n\n## Reranking\n\nEnable for high-stakes use cases (adds ~300ms latency):\n\n```typescript\nreranking: { enabled: true, model: \"@cf/baai/bge-reranker-base\" }\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/analytics-engine/README.md",
    "content": "# Cloudflare Workers Analytics Engine Reference\n\nExpert guidance for implementing unlimited-cardinality analytics at scale using Cloudflare Workers Analytics Engine.\n\n## What is Analytics Engine?\n\nTime-series analytics database designed for high-cardinality data (millions of unique dimensions). Write data points from Workers, query via SQL API. Use for:\n- Custom user-facing analytics dashboards\n- Usage-based billing & metering\n- Per-customer/per-feature monitoring\n- High-frequency instrumentation without performance impact\n\n**Key Capability:** Track metrics with unlimited unique values (e.g., millions of user IDs, API keys) without performance degradation.\n\n## Core Concepts\n\n| Concept | Description | Example |\n|---------|-------------|---------|\n| **Dataset** | Logical table for related metrics | `api_requests`, `user_events` |\n| **Data Point** | Single measurement with timestamp | One API request's metrics |\n| **Blobs** | String dimensions (max 20) | endpoint, method, status, user_id |\n| **Doubles** | Numeric values (max 20) | latency_ms, request_count, bytes |\n| **Indexes** | Filtered blobs for efficient queries | customer_id, api_key |\n\n## Reading Order\n\n| Task | Start Here | Then Read |\n|------|------------|-----------|\n| **First-time setup** | [configuration.md](configuration.md) → [api.md](api.md) → [patterns.md](patterns.md) | |\n| **Writing data** | [api.md](api.md) → [gotchas.md](gotchas.md) (sampling) | |\n| **Querying data** | [api.md](api.md) (SQL API) → [patterns.md](patterns.md) (examples) | |\n| **Debugging** | [gotchas.md](gotchas.md) → [api.md](api.md) (limits) | |\n| **Optimization** | [patterns.md](patterns.md) (anti-patterns) → [gotchas.md](gotchas.md) | |\n\n## When to Use Analytics Engine\n\n```\nNeed to track metrics? → Yes\n  ↓\nMillions of unique dimension values? → Yes\n    ↓\n  Need real-time queries? → Yes\n      ↓\n    Use Analytics Engine ✓\n\nAlternative scenarios:\n- Low cardinality (<10k unique values) → Workers Analytics (free tier)\n- Complex joins/relations → D1 Database\n- Logs/debugging → Tail Workers (logpush)\n- External tools → Send to external analytics (Datadog, etc.)\n```\n\n## Quick Start\n\n1. Add binding to `wrangler.jsonc`:\n```jsonc\n{\n  \"analytics_engine_datasets\": [\n    { \"binding\": \"ANALYTICS\", \"dataset\": \"my_events\" }\n  ]\n}\n```\n\n2. Write data points (fire-and-forget, no await):\n```typescript\nenv.ANALYTICS.writeDataPoint({\n  blobs: [\"/api/users\", \"GET\", \"200\"],\n  doubles: [145.2, 1],  // latency_ms, count\n  indexes: [customerId]\n});\n```\n\n3. Query via SQL API (HTTP):\n```sql\nSELECT blob1, SUM(double2) AS total_requests\nFROM my_events\nWHERE index1 = 'customer_123'\n  AND timestamp >= NOW() - INTERVAL '7' DAY\nGROUP BY blob1\nORDER BY total_requests DESC\n```\n\n## In This Reference\n\n- **[configuration.md](configuration.md)** - Setup, bindings, TypeScript types, limits\n- **[api.md](api.md)** - `writeDataPoint()`, SQL API, query syntax\n- **[patterns.md](patterns.md)** - Use cases, examples, anti-patterns\n- **[gotchas.md](gotchas.md)** - Sampling, index selection, troubleshooting\n\n## See Also\n\n- [Cloudflare Analytics Engine Docs](https://developers.cloudflare.com/analytics/analytics-engine/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/analytics-engine/api.md",
    "content": "# Analytics Engine API Reference\n\n## Writing Data\n\n### `writeDataPoint()`\n\nFire-and-forget (returns `void`, not Promise). Writes happen asynchronously.\n\n```typescript\ninterface AnalyticsEngineDataPoint {\n  blobs?: string[];      // Up to 20 strings (dimensions), 16KB each\n  doubles?: number[];    // Up to 20 numbers (metrics)\n  indexes?: string[];    // 1 indexed string for high-cardinality filtering\n}\n\nenv.ANALYTICS.writeDataPoint({\n  blobs: [\"/api/users\", \"GET\", \"200\"],\n  doubles: [145.2, 1],  // latency_ms, count\n  indexes: [\"customer_abc123\"]\n});\n```\n\n**Behaviors:** No await needed, no error thrown (check tail logs), auto-sampled at high volumes, auto-timestamped.\n\n**Blob vs Index:** Blob for GROUP BY (<100k unique), Index for filter-only (millions unique).\n\n### Full Example\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const start = Date.now();\n    const url = new URL(request.url);\n    try {\n      const response = await handleRequest(request);\n      env.ANALYTICS.writeDataPoint({\n        blobs: [url.pathname, request.method, response.status.toString()],\n        doubles: [Date.now() - start, 1],\n        indexes: [request.headers.get(\"x-api-key\") || \"anonymous\"]\n      });\n      return response;\n    } catch (error) {\n      env.ANALYTICS.writeDataPoint({\n        blobs: [url.pathname, request.method, \"500\"],\n        doubles: [Date.now() - start, 1, 0],\n      });\n      throw error;\n    }\n  }\n};\n```\n\n## SQL API (External Only)\n\n```bash\ncurl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -d \"SELECT blob1 AS endpoint, COUNT(*) AS requests FROM dataset WHERE timestamp >= NOW() - INTERVAL '1' HOUR GROUP BY blob1\"\n```\n\n### Column References\n\n```sql\n-- blob1..blob20, double1..double20, index1, timestamp\nSELECT blob1 AS endpoint, SUM(double1) AS latency, COUNT(*) AS requests\nFROM my_dataset\nWHERE index1 = 'customer_123' AND timestamp >= NOW() - INTERVAL '7' DAY\nGROUP BY blob1\nHAVING COUNT(*) > 100\nORDER BY requests DESC LIMIT 100\n```\n\n**Aggregations:** `SUM()`, `AVG()`, `COUNT()`, `MIN()`, `MAX()`, `quantile(0.95)()`\n\n**Time ranges:** `NOW() - INTERVAL '1' HOUR`, `BETWEEN '2026-01-01' AND '2026-01-31'`\n\n### Query Examples\n\n```sql\n-- Top endpoints\nSELECT blob1, COUNT(*) AS requests, AVG(double1) AS avg_latency\nFROM api_requests WHERE timestamp >= NOW() - INTERVAL '24' HOUR\nGROUP BY blob1 ORDER BY requests DESC LIMIT 20\n\n-- Error rate\nSELECT blob1, COUNT(*) AS total,\n  SUM(CASE WHEN blob3 LIKE '5%' THEN 1 ELSE 0 END) AS errors\nFROM api_requests WHERE timestamp >= NOW() - INTERVAL '1' HOUR\nGROUP BY blob1 HAVING total > 50\n\n-- P95 latency\nSELECT blob1, quantile(0.95)(double1) AS p95\nFROM api_requests GROUP BY blob1\n```\n\n## Response Format\n\n```json\n{\"data\": [{\"endpoint\": \"/api/users\", \"requests\": 1523}], \"rows\": 2}\n```\n\n## Limits\n\n| Resource | Limit |\n|----------|-------|\n| Blobs/Doubles per point | 20 each |\n| Indexes per point | 1 |\n| Blob/Index size | 16KB |\n| Data retention | 90 days |\n| Query timeout | 30s |\n\n**Critical:** High write volumes (>1M/min) trigger automatic sampling.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/analytics-engine/configuration.md",
    "content": "# Analytics Engine Configuration\n\n## Setup\n\n1. Add binding to `wrangler.jsonc`\n2. Deploy Worker\n3. Dataset created automatically on first write\n4. Query via SQL API\n\n## wrangler.jsonc\n\n```jsonc\n{\n  \"name\": \"my-worker\",\n  \"analytics_engine_datasets\": [\n    { \"binding\": \"ANALYTICS\", \"dataset\": \"my_events\" }\n  ]\n}\n```\n\nMultiple datasets for separate concerns:\n```jsonc\n{\n  \"analytics_engine_datasets\": [\n    { \"binding\": \"API_ANALYTICS\", \"dataset\": \"api_requests\" },\n    { \"binding\": \"USER_EVENTS\", \"dataset\": \"user_activity\" }\n  ]\n}\n```\n\n## TypeScript\n\n```typescript\ninterface Env {\n  ANALYTICS: AnalyticsEngineDataset;\n}\n\nexport default {\n  async fetch(request: Request, env: Env) {\n    // No await - returns void, fire-and-forget\n    env.ANALYTICS.writeDataPoint({\n      blobs: [pathname, method, status],      // String dimensions (max 20)\n      doubles: [latency, 1],                   // Numeric metrics (max 20)\n      indexes: [apiKey]                        // High-cardinality filter (max 1)\n    });\n    return response;\n  }\n};\n```\n\n## Data Point Limits\n\n| Field | Limit | SQL Access |\n|-------|-------|------------|\n| blobs | 20 strings, 16KB each | `blob1`...`blob20` |\n| doubles | 20 numbers | `double1`...`double20` |\n| indexes | 1 string, 16KB | `index1` |\n\n## Write Behavior\n\n| Scenario | Behavior |\n|----------|----------|\n| <1M writes/min | All accepted |\n| >1M writes/min | Automatic sampling |\n| Invalid data | Silent failure (check tail logs) |\n\n**Mitigate sampling:** Pre-aggregate, use multiple datasets, write only critical metrics.\n\n## Query Limits\n\n| Resource | Limit |\n|----------|-------|\n| Query timeout | 30 seconds |\n| Data retention | 90 days (default) |\n| Result size | ~10MB |\n\n## Cost\n\n**Free tier:** 10M writes/month, 1M reads/month\n\n**Paid:** $0.05 per 1M writes, $1.00 per 1M reads\n\n## Environment-Specific\n\n```jsonc\n{\n  \"analytics_engine_datasets\": [\n    { \"binding\": \"ANALYTICS\", \"dataset\": \"prod_events\" }\n  ],\n  \"env\": {\n    \"staging\": {\n      \"analytics_engine_datasets\": [\n        { \"binding\": \"ANALYTICS\", \"dataset\": \"staging_events\" }\n      ]\n    }\n  }\n}\n```\n\n## Monitoring\n\n```bash\nnpx wrangler tail  # Check for sampling/write errors\n```\n\n```sql\n-- Check write activity\nSELECT DATE_TRUNC('hour', timestamp) AS hour, COUNT(*) AS writes\nFROM my_dataset\nWHERE timestamp >= NOW() - INTERVAL '24' HOUR\nGROUP BY hour\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/analytics-engine/gotchas.md",
    "content": "# Analytics Engine Gotchas\n\n## Critical Issues\n\n### Sampling at High Volumes\n\n**Problem:** Queries return fewer points than written at >1M writes/min.\n\n**Solution:**\n```typescript\n// Pre-aggregate before writing\nlet buffer = { count: 0, total: 0 };\nbuffer.count++; buffer.total += value;\n\n// Write once per second instead of per request\nif (Date.now() % 1000 === 0) {\n  env.ANALYTICS.writeDataPoint({ doubles: [buffer.count, buffer.total] });\n}\n```\n\n**Detection:** `npx wrangler tail` → look for \"sampling enabled\"\n\n### writeDataPoint Returns void\n\n```typescript\n// ❌ Pointless await\nawait env.ANALYTICS.writeDataPoint({...});\n\n// ✅ Fire-and-forget\nenv.ANALYTICS.writeDataPoint({...});\n```\n\nWrites can fail silently. Check tail logs.\n\n### Index vs Blob\n\n| Cardinality | Use | Example |\n|-------------|-----|---------|\n| Millions | **Index** | user_id, api_key |\n| Hundreds | **Blob** | endpoint, status_code, country |\n\n```typescript\n// ✅ Correct\n{ blobs: [method, path, status], indexes: [userId] }\n```\n\n### Can't Query from Workers\n\nQuery API requires HTTP auth. Use external service or cache in KV/D1.\n\n### No Custom Timestamps\n\nAuto-generated at write time. Store original in blob if needed.\n\n## Common Errors\n\n| Error | Fix |\n|-------|-----|\n| Binding not found | Check wrangler.jsonc, redeploy |\n| No data in query | Wait 30s; check dataset name; check time range |\n| Query timeout | Add time filter; use index for filtering |\n\n## Limits\n\n| Resource | Limit |\n|----------|-------|\n| Blobs per point | 20 |\n| Doubles per point | 20 |\n| Indexes per point | 1 |\n| Blob/Index size | 16KB |\n| Write rate (no sampling) | ~1M/min |\n| Retention | 90 days |\n| Query timeout | 30s |\n\n## Best Practices\n\n✅ Pre-aggregate at high volumes  \n✅ Use index for high-cardinality (millions)  \n✅ Always include time filter in queries  \n✅ Design schema before coding  \n\n❌ Don't await writeDataPoint  \n❌ Don't use index for low-cardinality  \n❌ Don't query without time range  \n❌ Don't assume all writes succeed\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/analytics-engine/patterns.md",
    "content": "# Analytics Engine Patterns\n\n## Use Cases\n\n| Use Case | Key Metrics | Index On |\n|----------|-------------|----------|\n| API Metering | requests, bytes, compute_units | api_key |\n| Feature Usage | feature, action, duration | user_id |\n| Error Tracking | error_type, endpoint, count | customer_id |\n| Performance | latency_ms, cache_status | endpoint |\n| A/B Testing | variant, conversions | user_id |\n\n## API Metering (Billing)\n\n```typescript\nenv.ANALYTICS.writeDataPoint({\n  blobs: [pathname, method, status, tier],\n  doubles: [1, computeUnits, bytes, latencyMs],\n  indexes: [apiKey]\n});\n\n// Query: Monthly usage by customer\n// SELECT index1 AS api_key, SUM(double2) AS compute_units\n// FROM usage WHERE timestamp >= DATE_TRUNC('month', NOW()) GROUP BY index1\n```\n\n## Error Tracking\n\n```typescript\nenv.ANALYTICS.writeDataPoint({\n  blobs: [endpoint, method, errorName, errorMessage.slice(0, 1000)],\n  doubles: [1, timeToErrorMs],\n  indexes: [customerId]\n});\n```\n\n## Performance Monitoring\n\n```typescript\nenv.ANALYTICS.writeDataPoint({\n  blobs: [pathname, method, cacheStatus, status],\n  doubles: [latencyMs, 1],\n  indexes: [userId]\n});\n\n// Query: P95 latency by endpoint\n// SELECT blob1, quantile(0.95)(double1) AS p95_ms FROM perf GROUP BY blob1\n```\n\n## Anti-Patterns\n\n| ❌ Wrong | ✅ Correct |\n|----------|-----------|\n| `await writeDataPoint()` | `writeDataPoint()` (fire-and-forget) |\n| `indexes: [method]` (low cardinality) | `blobs: [method]`, `indexes: [userId]` |\n| `blobs: [JSON.stringify(obj)]` | Store ID in blob, full object in D1/KV |\n| Write every request at 10M/min | Pre-aggregate per second |\n| Query from Worker | Query from external service/API |\n\n## Best Practices\n\n1. **Design schema upfront** - Document blob/double/index assignments\n2. **Always include count metric** - `doubles: [latency, 1]` for AVG calculations\n3. **Use enums for blobs** - Consistent values like `Status.SUCCESS`\n4. **Handle sampling** - Use ratios (avg_latency = SUM(latency)/SUM(count))\n5. **Test queries early** - Validate schema before heavy writes\n\n## Schema Template\n\n```typescript\n/**\n * Dataset: my_metrics\n * \n * Blobs:\n *   blob1: endpoint, blob2: method, blob3: status\n * \n * Doubles:\n *   double1: latency_ms, double2: count (always 1)\n * \n * Indexes:\n *   index1: customer_id (high cardinality)\n */\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/api/README.md",
    "content": "# Cloudflare API Integration\n\nGuide for working with Cloudflare's REST API - authentication, SDK usage, common patterns, and troubleshooting.\n\n## Quick Decision Tree\n\n```\nHow are you calling the Cloudflare API?\n├─ From Workers runtime → Use bindings, not REST API (see ../bindings/)\n├─ Server-side (Node/Python/Go) → Official SDK (see api.md)\n├─ CLI/scripts → Wrangler or curl (see configuration.md)\n├─ Infrastructure-as-code → See ../pulumi/ or ../terraform/\n└─ One-off requests → curl examples (see api.md)\n```\n\n## SDK Selection\n\n| Language | Package | Best For | Default Retries |\n|----------|---------|----------|-----------------|\n| TypeScript | `cloudflare` | Node.js, Bun, Next.js, Workers | 2 |\n| Python | `cloudflare` | FastAPI, Django, scripts | 2 |\n| Go | `cloudflare-go/v4` | CLI tools, microservices | 10 |\n\nAll SDKs are Stainless-generated from OpenAPI spec (consistent APIs).\n\n## Authentication Methods\n\n| Method | Security | Use Case | Scope |\n|--------|----------|----------|-------|\n| **API Token** ✓ | Scoped, rotatable | Production | Per-zone or account |\n| API Key + Email | Full account access | Legacy only | Everything |\n| User Service Key | Limited | Origin CA certs only | Origin CA |\n\n**Always use API tokens** for new projects.\n\n## Rate Limits\n\n| Limit | Value |\n|-------|-------|\n| Per user/token | 1200 requests / 5 minutes |\n| Per IP | 200 requests / second |\n| GraphQL | 320 / 5 minutes (cost-based) |\n\n## Reading Order\n\n| Task | Files to Read |\n|------|---------------|\n| Initialize SDK client | api.md |\n| Configure auth/timeout/retry | configuration.md |\n| Find usage patterns | patterns.md |\n| Debug errors/rate limits | gotchas.md |\n| Product-specific APIs | ../workers/, ../r2/, ../kv/, etc. |\n\n## In This Reference\n\n- **[api.md](api.md)** - SDK client initialization, pagination, error handling, examples\n- **[configuration.md](configuration.md)** - Environment variables, SDK config, Wrangler setup\n- **[patterns.md](patterns.md)** - Real-world patterns, batch operations, workflows\n- **[gotchas.md](gotchas.md)** - Rate limits, SDK-specific issues, troubleshooting\n\n## See Also\n\n- [Cloudflare API Docs](https://developers.cloudflare.com/api/)\n- [Bindings Reference](../bindings/) - Workers runtime bindings (preferred over REST API)\n- [Wrangler Reference](../wrangler/) - CLI tool for Cloudflare development\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/api/api.md",
    "content": "# API Reference\n\n## Client Initialization\n\n### TypeScript\n\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst client = new Cloudflare({\n  apiToken: process.env.CLOUDFLARE_API_TOKEN,\n});\n```\n\n### Python\n\n```python\nfrom cloudflare import Cloudflare\n\nclient = Cloudflare(api_token=os.environ.get(\"CLOUDFLARE_API_TOKEN\"))\n\n# For async:\nfrom cloudflare import AsyncCloudflare\nclient = AsyncCloudflare(api_token=os.environ[\"CLOUDFLARE_API_TOKEN\"])\n```\n\n### Go\n\n```go\nimport (\n    \"github.com/cloudflare/cloudflare-go/v4\"\n    \"github.com/cloudflare/cloudflare-go/v4/option\"\n)\n\nclient := cloudflare.NewClient(\n    option.WithAPIToken(os.Getenv(\"CLOUDFLARE_API_TOKEN\")),\n)\n```\n\n## Authentication\n\n### API Token (Recommended)\n\n**Create token**: Dashboard → My Profile → API Tokens → Create Token\n\n```bash\nexport CLOUDFLARE_API_TOKEN='your-token-here'\n\ncurl \"https://api.cloudflare.com/client/v4/zones\" \\\n  --header \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\"\n```\n\n**Token scopes**: Always use minimal permissions (zone-specific, time-limited).\n\n### API Key (Legacy)\n\n```bash\ncurl \"https://api.cloudflare.com/client/v4/zones\" \\\n  --header \"X-Auth-Email: user@example.com\" \\\n  --header \"X-Auth-Key: $CLOUDFLARE_API_KEY\"\n```\n\n**Not recommended:** Full account access, cannot scope permissions.\n\n## Auto-Pagination\n\nAll SDKs support automatic pagination for list operations.\n\n```typescript\n// TypeScript: for await...of\nfor await (const zone of client.zones.list()) {\n  console.log(zone.id);\n}\n```\n\n```python\n# Python: iterator protocol\nfor zone in client.zones.list():\n    print(zone.id)\n```\n\n```go\n// Go: ListAutoPaging\niter := client.Zones.ListAutoPaging(ctx, cloudflare.ZoneListParams{})\nfor iter.Next() {\n    zone := iter.Current()\n    fmt.Println(zone.ID)\n}\n```\n\n## Error Handling\n\n```typescript\ntry {\n  const zone = await client.zones.get({ zone_id: 'xxx' });\n} catch (err) {\n  if (err instanceof Cloudflare.NotFoundError) {\n    // 404\n  } else if (err instanceof Cloudflare.RateLimitError) {\n    // 429 - SDK auto-retries with backoff\n  } else if (err instanceof Cloudflare.APIError) {\n    console.log(err.status, err.message);\n  }\n}\n```\n\n**Common Error Types:**\n- `AuthenticationError` (401) - Invalid token\n- `PermissionDeniedError` (403) - Insufficient scope\n- `NotFoundError` (404) - Resource not found\n- `RateLimitError` (429) - Rate limit exceeded\n- `InternalServerError` (≥500) - Cloudflare error\n\n## Zone Management\n\n```typescript\n// List zones\nconst zones = await client.zones.list({\n  account: { id: 'account-id' },\n  status: 'active',\n});\n\n// Create zone\nconst zone = await client.zones.create({\n  account: { id: 'account-id' },\n  name: 'example.com',\n  type: 'full', // or 'partial'\n});\n\n// Update zone\nawait client.zones.edit('zone-id', {\n  paused: false,\n});\n\n// Delete zone\nawait client.zones.delete('zone-id');\n```\n\n```go\n// Go: requires cloudflare.F() wrapper\nzone, err := client.Zones.New(ctx, cloudflare.ZoneNewParams{\n    Account: cloudflare.F(cloudflare.ZoneNewParamsAccount{\n        ID: cloudflare.F(\"account-id\"),\n    }),\n    Name: cloudflare.F(\"example.com\"),\n    Type: cloudflare.F(cloudflare.ZoneNewParamsTypeFull),\n})\n```\n\n## DNS Management\n\n```typescript\n// Create DNS record\nawait client.dns.records.create({\n  zone_id: 'zone-id',\n  type: 'A',\n  name: 'subdomain.example.com',\n  content: '192.0.2.1',\n  ttl: 1, // auto\n  proxied: true, // Orange cloud\n});\n\n// List DNS records (with auto-pagination)\nfor await (const record of client.dns.records.list({\n  zone_id: 'zone-id',\n  type: 'A',\n})) {\n  console.log(record.name, record.content);\n}\n\n// Update DNS record\nawait client.dns.records.update({\n  zone_id: 'zone-id',\n  dns_record_id: 'record-id',\n  type: 'A',\n  name: 'subdomain.example.com',\n  content: '203.0.113.1',\n  proxied: true,\n});\n\n// Delete DNS record\nawait client.dns.records.delete({\n  zone_id: 'zone-id',\n  dns_record_id: 'record-id',\n});\n```\n\n```python\n# Python example\nclient.dns.records.create(\n    zone_id=\"zone-id\",\n    type=\"A\",\n    name=\"subdomain.example.com\",\n    content=\"192.0.2.1\",\n    ttl=1,\n    proxied=True,\n)\n```\n\n## See Also\n\n- [configuration.md](./configuration.md) - SDK configuration, environment variables\n- [patterns.md](./patterns.md) - Real-world patterns and workflows\n- [gotchas.md](./gotchas.md) - Rate limits, troubleshooting\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/api/configuration.md",
    "content": "# Configuration\n\n## Environment Variables\n\n### Set Variables\n\n| Platform | Command |\n|----------|---------|\n| Linux/macOS | `export CLOUDFLARE_API_TOKEN='token'` |\n| PowerShell | `$env:CLOUDFLARE_API_TOKEN = 'token'` |\n| Windows CMD | `set CLOUDFLARE_API_TOKEN=token` |\n\n**Security:** Never commit tokens. Use `.env` files (gitignored) or secret managers.\n\n### .env File Pattern\n\n```bash\n# .env (add to .gitignore)\nCLOUDFLARE_API_TOKEN=your-token-here\nCLOUDFLARE_ACCOUNT_ID=your-account-id\n```\n\n```typescript\n// TypeScript\nimport 'dotenv/config';\n\nconst client = new Cloudflare({\n  apiToken: process.env.CLOUDFLARE_API_TOKEN,\n});\n```\n\n```python\n# Python\nfrom dotenv import load_dotenv\nload_dotenv()\n\nclient = Cloudflare(api_token=os.environ[\"CLOUDFLARE_API_TOKEN\"])\n```\n\n## SDK Configuration\n\n### TypeScript\n\n```typescript\nconst client = new Cloudflare({\n  apiToken: process.env.CLOUDFLARE_API_TOKEN,\n  timeout: 120000,        // 2 min (default 60s), in milliseconds\n  maxRetries: 5,          // default 2\n  baseURL: 'https://...', // proxy (rare)\n});\n\n// Per-request overrides\nawait client.zones.get(\n  { zone_id: 'zone-id' },\n  { timeout: 5000, maxRetries: 0 }\n);\n```\n\n### Python\n\n```python\nclient = Cloudflare(\n    api_token=os.environ[\"CLOUDFLARE_API_TOKEN\"],\n    timeout=120,         # seconds (default 60)\n    max_retries=5,       # default 2\n    base_url=\"https://...\",  # proxy (rare)\n)\n\n# Per-request overrides\nclient.with_options(timeout=5, max_retries=0).zones.get(zone_id=\"zone-id\")\n```\n\n### Go\n\n```go\nclient := cloudflare.NewClient(\n    option.WithAPIToken(os.Getenv(\"CLOUDFLARE_API_TOKEN\")),\n    option.WithMaxRetries(5),  // default 10 (higher than TS/Python)\n    option.WithRequestTimeout(2 * time.Minute),  // default 60s\n    option.WithBaseURL(\"https://...\"),  // proxy (rare)\n)\n\n// Per-request overrides\nclient.Zones.Get(ctx, \"zone-id\", option.WithMaxRetries(0))\n```\n\n## Configuration Options\n\n| Option | TypeScript | Python | Go | Default |\n|--------|-----------|--------|-----|---------|\n| Timeout | `timeout` (ms) | `timeout` (s) | `WithRequestTimeout` | 60s |\n| Retries | `maxRetries` | `max_retries` | `WithMaxRetries` | 2 (Go: 10) |\n| Base URL | `baseURL` | `base_url` | `WithBaseURL` | api.cloudflare.com |\n\n**Note:** Go SDK has higher default retries (10) than TypeScript/Python (2).\n\n## Timeout Configuration\n\n**When to increase:**\n- Large zone transfers\n- Bulk DNS operations\n- Worker script uploads\n\n```typescript\nconst client = new Cloudflare({\n  timeout: 300000, // 5 minutes\n});\n```\n\n## Retry Configuration\n\n**When to increase:** Rate-limit-heavy workflows, flaky network\n\n**When to decrease:** Fast-fail requirements, user-facing requests\n\n```typescript\n// Increase retries for batch operations\nconst client = new Cloudflare({ maxRetries: 10 });\n\n// Disable retries for fast-fail\nconst fastClient = new Cloudflare({ maxRetries: 0 });\n```\n\n## Wrangler CLI Integration\n\n```bash\n# Configure authentication\nwrangler login\n# Or\nexport CLOUDFLARE_API_TOKEN='token'\n\n# Common commands that use API\nwrangler deploy              # Uploads worker via API\nwrangler kv:key put          # KV operations\nwrangler r2 bucket create    # R2 operations\nwrangler d1 execute          # D1 operations\nwrangler pages deploy        # Pages operations\n\n# Get API configuration\nwrangler whoami              # Shows authenticated user\n```\n\n### wrangler.toml\n\n```toml\nname = \"my-worker\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2024-01-01\"\naccount_id = \"your-account-id\"\n\n# Can also use env vars:\n# CLOUDFLARE_ACCOUNT_ID\n# CLOUDFLARE_API_TOKEN\n```\n\n## See Also\n\n- [api.md](./api.md) - Client initialization, authentication\n- [gotchas.md](./gotchas.md) - Rate limits, timeout errors\n- [Wrangler Reference](../wrangler/) - CLI tool details\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/api/gotchas.md",
    "content": "# Gotchas & Troubleshooting\n\n## Rate Limits & 429 Errors\n\n**Actual Limits:**\n- **1200 requests / 5 minutes** per user/token (global)\n- **200 requests / second** per IP address\n- **GraphQL: 320 / 5 minutes** (cost-based)\n\n**SDK Behavior:**\n- Auto-retry with exponential backoff (default 2 retries, Go: 10)\n- Respects `Retry-After` header\n- Throws `RateLimitError` after exhausting retries\n\n**Solution:**\n\n```typescript\n// Increase retries for rate-limit-heavy workflows\nconst client = new Cloudflare({ maxRetries: 5 });\n\n// Add application-level throttling\nimport pLimit from 'p-limit';\nconst limit = pLimit(10); // Max 10 concurrent requests\n```\n\n## SDK-Specific Issues\n\n### Go: Required Field Wrapper\n\n**Problem:** Go SDK requires `cloudflare.F()` wrapper for optional fields.\n\n```go\n// ❌ WRONG - Won't compile or send field\nclient.Zones.New(ctx, cloudflare.ZoneNewParams{\n    Name: \"example.com\",\n})\n\n// ✅ CORRECT\nclient.Zones.New(ctx, cloudflare.ZoneNewParams{\n    Name: cloudflare.F(\"example.com\"),\n    Account: cloudflare.F(cloudflare.ZoneNewParamsAccount{\n        ID: cloudflare.F(\"account-id\"),\n    }),\n})\n```\n\n**Why:** Distinguishes between zero value, null, and omitted fields.\n\n### Python: Async vs Sync Clients\n\n**Problem:** Using sync client in async context or vice versa.\n\n```python\n# ❌ WRONG - Can't await sync client\nfrom cloudflare import Cloudflare\nclient = Cloudflare()\nawait client.zones.list()  # TypeError\n\n# ✅ CORRECT - Use AsyncCloudflare\nfrom cloudflare import AsyncCloudflare\nclient = AsyncCloudflare()\nawait client.zones.list()\n```\n\n## Token Permission Errors (403)\n\n**Problem:** API returns 403 Forbidden despite valid token.\n\n**Cause:** Token lacks required permissions (scope).\n\n**Scopes Required:**\n\n| Operation | Required Scope |\n|-----------|----------------|\n| List zones | Zone:Read (zone-level or account-level) |\n| Create zone | Zone:Edit (account-level) |\n| Edit DNS | DNS:Edit (zone-level) |\n| Deploy Worker | Workers Script:Edit (account-level) |\n| Read KV | Workers KV Storage:Read |\n| Write KV | Workers KV Storage:Edit |\n\n**Solution:** Re-create token with correct permissions in Dashboard → My Profile → API Tokens.\n\n## Pagination Truncation\n\n**Problem:** Only getting first 20 results (default page size).\n\n**Solution:** Use auto-pagination iterators.\n\n```typescript\n// ❌ WRONG - Only first page (20 items)\nconst page = await client.zones.list();\n\n// ✅ CORRECT - All results\nconst zones = [];\nfor await (const zone of client.zones.list()) {\n  zones.push(zone);\n}\n```\n\n## Workers Subrequests\n\n**Problem:** Rate limit hit faster than expected in Workers.\n\n**Cause:** Workers subrequests count as separate API calls.\n\n**Solution:** Use bindings instead of REST API in Workers (see ../bindings/).\n\n```typescript\n// ❌ WRONG - REST API in Workers (counts against rate limit)\nconst client = new Cloudflare({ apiToken: env.CLOUDFLARE_API_TOKEN });\nconst zones = await client.zones.list();\n\n// ✅ CORRECT - Use bindings (no rate limit)\n// Access via env.MY_BINDING\n```\n\n## Authentication Errors (401)\n\n**Problem:** \"Authentication failed\" or \"Invalid token\"\n\n**Causes:**\n- Token expired\n- Token deleted/revoked\n- Token not set in environment\n- Wrong token format\n\n**Solution:**\n\n```typescript\n// Verify token is set\nif (!process.env.CLOUDFLARE_API_TOKEN) {\n  throw new Error('CLOUDFLARE_API_TOKEN not set');\n}\n\n// Test token\nconst user = await client.user.tokens.verify();\nconsole.log('Token valid:', user.status);\n```\n\n## Timeout Errors\n\n**Problem:** Request times out (default 60s).\n\n**Cause:** Large operations (bulk DNS, zone transfers).\n\n**Solution:** Increase timeout or split operations.\n\n```typescript\n// Increase timeout\nconst client = new Cloudflare({\n  timeout: 300000, // 5 minutes\n});\n\n// Or split operations\nconst batchSize = 100;\nfor (let i = 0; i < records.length; i += batchSize) {\n  const batch = records.slice(i, i + batchSize);\n  await processBatch(batch);\n}\n```\n\n## Zone Not Found (404)\n\n**Problem:** Zone ID valid but returns 404.\n\n**Causes:**\n- Zone not in account associated with token\n- Zone deleted\n- Wrong zone ID format\n\n**Solution:**\n\n```typescript\n// List all zones to find correct ID\nfor await (const zone of client.zones.list()) {\n  console.log(zone.id, zone.name);\n}\n```\n\n## Limits Reference\n\n| Resource/Limit | Value | Notes |\n|----------------|-------|-------|\n| API rate limit | 1200/5min | Per user/token |\n| IP rate limit | 200/sec | Per IP |\n| GraphQL rate limit | 320/5min | Cost-based |\n| Parallel requests (recommended) | < 10 | Avoid overwhelming API |\n| Default page size | 20 | Use auto-pagination |\n| Max page size | 50 | Some endpoints |\n\n## Best Practices\n\n**Security:**\n- Never commit tokens\n- Use minimal permissions\n- Rotate tokens regularly\n- Set token expiration\n\n**Performance:**\n- Batch operations\n- Use pagination wisely\n- Cache responses\n- Handle rate limits\n\n**Code Organization:**\n\n```typescript\n// Create reusable client instance\nexport const cfClient = new Cloudflare({\n  apiToken: process.env.CLOUDFLARE_API_TOKEN,\n  maxRetries: 5,\n});\n\n// Wrap common operations\nexport async function getZoneDetails(zoneId: string) {\n  return await cfClient.zones.get({ zone_id: zoneId });\n}\n```\n\n## See Also\n\n- [api.md](./api.md) - Error types, authentication\n- [configuration.md](./configuration.md) - Timeout/retry configuration\n- [patterns.md](./patterns.md) - Error handling patterns\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/api/patterns.md",
    "content": "# Common Patterns\n\n## List All with Auto-Pagination\n\n**Problem:** API returns paginated results. Default page size is 20.\n\n**Solution:** Use SDK auto-pagination to iterate all results.\n\n```typescript\n// TypeScript\nfor await (const zone of client.zones.list()) {\n  console.log(zone.name);\n}\n```\n\n```python\n# Python\nfor zone in client.zones.list():\n    print(zone.name)\n```\n\n```go\n// Go\niter := client.Zones.ListAutoPaging(ctx, cloudflare.ZoneListParams{})\nfor iter.Next() {\n    fmt.Println(iter.Current().Name)\n}\n```\n\n## Error Handling with Retry\n\n**Problem:** Rate limits (429) and transient errors need retry.\n\n**Solution:** SDKs auto-retry with exponential backoff. Customize as needed.\n\n```typescript\n// Increase retries for rate-limit-heavy operations\nconst client = new Cloudflare({ maxRetries: 5 });\n\ntry {\n  const zone = await client.zones.create({ /* ... */ });\n} catch (err) {\n  if (err instanceof Cloudflare.RateLimitError) {\n    // Already retried 5 times with backoff\n    const retryAfter = err.headers['retry-after'];\n    console.log(`Rate limited. Retry after ${retryAfter}s`);\n  }\n}\n```\n\n## Batch Parallel Operations\n\n**Problem:** Need to create multiple resources quickly.\n\n**Solution:** Use `Promise.all()` for parallel requests (respect rate limits).\n\n```typescript\n// Create multiple DNS records in parallel\nconst records = ['www', 'api', 'cdn'].map(subdomain =>\n  client.dns.records.create({\n    zone_id: 'zone-id',\n    type: 'A',\n    name: `${subdomain}.example.com`,\n    content: '192.0.2.1',\n  })\n);\nawait Promise.all(records);\n```\n\n**Controlled concurrency** (avoid rate limits):\n\n```typescript\nimport pLimit from 'p-limit';\nconst limit = pLimit(10); // Max 10 concurrent\n\nconst subdomains = ['www', 'api', 'cdn', /* many more */];\nconst records = subdomains.map(subdomain =>\n  limit(() => client.dns.records.create({\n    zone_id: 'zone-id',\n    type: 'A',\n    name: `${subdomain}.example.com`,\n    content: '192.0.2.1',\n  }))\n);\nawait Promise.all(records);\n```\n\n## Zone CRUD Workflow\n\n```typescript\n// Create\nconst zone = await client.zones.create({\n  account: { id: 'account-id' },\n  name: 'example.com',\n  type: 'full',\n});\n\n// Read\nconst fetched = await client.zones.get({ zone_id: zone.id });\n\n// Update\nawait client.zones.edit(zone.id, { paused: false });\n\n// Delete\nawait client.zones.delete(zone.id);\n```\n\n## DNS Bulk Update\n\n```typescript\n// Fetch all A records\nconst records = [];\nfor await (const record of client.dns.records.list({\n  zone_id: 'zone-id',\n  type: 'A',\n})) {\n  records.push(record);\n}\n\n// Update all to new IP\nawait Promise.all(records.map(record =>\n  client.dns.records.update({\n    zone_id: 'zone-id',\n    dns_record_id: record.id,\n    type: 'A',\n    name: record.name,\n    content: '203.0.113.1', // New IP\n    proxied: record.proxied,\n    ttl: record.ttl,\n  })\n));\n```\n\n## Filter and Collect Results\n\n```typescript\n// Find all proxied A records\nconst proxiedRecords = [];\nfor await (const record of client.dns.records.list({\n  zone_id: 'zone-id',\n  type: 'A',\n})) {\n  if (record.proxied) {\n    proxiedRecords.push(record);\n  }\n}\n```\n\n## Error Recovery Pattern\n\n```typescript\nasync function createZoneWithRetry(name: string, maxAttempts = 3) {\n  for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n    try {\n      return await client.zones.create({\n        account: { id: 'account-id' },\n        name,\n        type: 'full',\n      });\n    } catch (err) {\n      if (err instanceof Cloudflare.RateLimitError && attempt < maxAttempts) {\n        const retryAfter = parseInt(err.headers['retry-after'] || '5');\n        console.log(`Rate limited, waiting ${retryAfter}s (retry ${attempt}/${maxAttempts})`);\n        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));\n      } else {\n        throw err;\n      }\n    }\n  }\n}\n```\n\n## Conditional Update Pattern\n\n```typescript\n// Only update if zone is active\nconst zone = await client.zones.get({ zone_id: 'zone-id' });\nif (zone.status === 'active') {\n  await client.zones.edit(zone.id, { paused: false });\n}\n```\n\n## Batch with Error Handling\n\n```typescript\n// Process multiple zones, continue on errors\nconst results = await Promise.allSettled(\n  zoneIds.map(id => client.zones.get({ zone_id: id }))\n);\n\nresults.forEach((result, i) => {\n  if (result.status === 'fulfilled') {\n    console.log(`Zone ${i}: ${result.value.name}`);\n  } else {\n    console.error(`Zone ${i} failed:`, result.reason.message);\n  }\n});\n```\n\n## See Also\n\n- [api.md](./api.md) - SDK client initialization, basic operations\n- [gotchas.md](./gotchas.md) - Rate limits, common errors\n- [configuration.md](./configuration.md) - SDK configuration options\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/api-shield/README.md",
    "content": "# Cloudflare API Shield Reference\n\nExpert guidance for API Shield - comprehensive API security suite for discovery, protection, and monitoring.\n\n## Reading Order\n\n| Task | Files to Read |\n|------|---------------|\n| Initial setup | README → configuration.md |\n| Implement JWT validation | configuration.md → api.md |\n| Add schema validation | configuration.md → patterns.md |\n| Detect API attacks | patterns.md → api.md |\n| Debug issues | gotchas.md |\n\n## Feature Selection\n\nWhat protection do you need?\n\n```\n├─ Validate request/response structure → Schema Validation 2.0 (configuration.md)\n├─ Verify auth tokens → JWT Validation (configuration.md)\n├─ Client certificates → mTLS (configuration.md)\n├─ Detect BOLA attacks → BOLA Detection (patterns.md)\n├─ Track auth coverage → Auth Posture (patterns.md)\n├─ Stop volumetric abuse → Abuse Detection (patterns.md)\n└─ Discover shadow APIs → API Discovery (api.md)\n```\n\n## In This Reference\n\n- **[configuration.md](configuration.md)** - Setup, session identifiers, rules, token/mTLS configs\n- **[api.md](api.md)** - Endpoint management, discovery, validation APIs, GraphQL operations\n- **[patterns.md](patterns.md)** - Common patterns, progressive rollout, OWASP mappings, workflows\n- **[gotchas.md](gotchas.md)** - Troubleshooting, false positives, performance, best practices\n\n## Quick Start\n\nAPI Shield: Enterprise-grade API security (Discovery, Schema Validation 2.0, JWT, mTLS, BOLA Detection, Auth Posture). Available as Enterprise add-on with preview access.\n\n## See Also\n\n- [API Shield Docs](https://developers.cloudflare.com/api-shield/)\n- [API Reference](https://developers.cloudflare.com/api/resources/api_gateway/)\n- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/api-shield/api.md",
    "content": "# API Reference\n\nBase: `/zones/{zone_id}/api_gateway`\n\n## Endpoints\n\n```bash\nGET /operations                    # List\nGET /operations/{op_id}            # Get single\nPOST /operations/item              # Create: {endpoint,host,method}\nPOST /operations                   # Bulk: {operations:[{endpoint,host,method}]}\nDELETE /operations/{op_id}         # Delete\nDELETE /operations                 # Bulk delete: {operation_ids:[...]}\n```\n\n## Discovery\n\n```bash\nGET /discovery/operations                    # List discovered\nPATCH /discovery/operations/{op_id}          # Update: {state:\"saved\"|\"ignored\"}\nPATCH /discovery/operations                  # Bulk: {operation_ids:{id:{state}}}\nGET /discovery                               # OpenAPI export\n```\n\n## Config\n\n```bash\nGET /configuration        # Get session ID config\nPUT /configuration        # Update: {auth_id_characteristics:[{name,type:\"header\"|\"cookie\"}]}\n```\n\n## Token Validation\n\n```bash\nGET /token_validation                  # List\nPOST /token_validation                 # Create: {name,location:{header:\"...\"},jwks:\"...\"}\nPOST /jwt_validation_rules             # Rule: {name,hostname,token_validation_id,action:\"block\"}\n```\n\n## Workers Integration\n\n### Access JWT Claims\n```js\nexport default {\n  async fetch(req, env) {\n    // Access validated JWT payload\n    const jwt = req.cf?.jwt?.payload?.[env.JWT_CONFIG_ID]?.[0];\n    if (jwt) {\n      const userId = jwt.sub;\n      const role = jwt.role;\n    }\n  }\n}\n```\n\n### Access mTLS Info\n```js\nexport default {\n  async fetch(req, env) {\n    const tls = req.cf?.tlsClientAuth;\n    if (tls?.certVerified === 'SUCCESS') {\n      const fingerprint = tls.certFingerprintSHA256;\n      // Authenticated client\n    }\n  }\n}\n```\n\n### Dynamic JWKS Update\n```js\nexport default {\n  async scheduled(event, env) {\n    const jwks = await (await fetch('https://auth.example.com/.well-known/jwks.json')).json();\n    await fetch(`https://api.cloudflare.com/client/v4/zones/${env.ZONE_ID}/api_gateway/token_validation/${env.CONFIG_ID}`, {\n      method: 'PATCH',\n      headers: {'Authorization': `Bearer ${env.CF_API_TOKEN}`, 'Content-Type': 'application/json'},\n      body: JSON.stringify({jwks: JSON.stringify(jwks)})\n    });\n  }\n}\n```\n\n## Firewall Fields\n\n### Core Fields\n```js\ncf.api_gateway.auth_id_present           // Session ID present\ncf.api_gateway.request_violates_schema   // Schema violation\ncf.api_gateway.fallthrough_triggered     // No endpoint match\ncf.tls_client_auth.cert_verified         // mTLS cert valid\ncf.tls_client_auth.cert_fingerprint_sha256\n```\n\n### JWT Validation (2026)\n```js\n// Modern validation syntax\nis_jwt_valid(http.request.jwt.payload[\"{config_id}\"][0])\n\n// Legacy (still supported)\ncf.api_gateway.jwt_claims_valid\n\n// Extract claims\nlookup_json_string(http.request.jwt.payload[\"{config_id}\"][0], \"claim_name\")\n```\n\n### Risk Labels (2026)\n```js\n// BOLA detection\ncf.api_gateway.cf-risk-bola-enumeration  // Sequential resource access detected\ncf.api_gateway.cf-risk-bola-pollution    // Parameter pollution detected\n\n// Authentication posture\ncf.api_gateway.cf-risk-missing-auth      // Endpoint lacks authentication\ncf.api_gateway.cf-risk-mixed-auth        // Inconsistent auth patterns\n```\n\n## BOLA Detection\n\n```bash\nGET /user_schemas/{schema_id}/bola             # Get BOLA config\nPATCH /user_schemas/{schema_id}/bola           # Update: {enabled:true}\n```\n\n## Auth Posture\n\n```bash\nGET /discovery/authentication_posture          # List unprotected endpoints\n```\n\n## GraphQL Protection\n\n```bash\nGET /settings/graphql_protection               # Get limits\nPUT /settings/graphql_protection               # Set: {max_depth,max_size}\n```\n\n## See Also\n\n- [configuration.md](configuration.md) - Setup guides for all features\n- [patterns.md](patterns.md) - Firewall rules and common patterns\n- [API Gateway API Docs](https://developers.cloudflare.com/api/resources/api_gateway/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/api-shield/configuration.md",
    "content": "# Configuration\n\n## Schema Validation 2.0 Setup\n\n> ⚠️ **Classic Schema Validation deprecated.** Use Schema Validation 2.0.\n\n**Upload schema (Dashboard):**\n```\nSecurity > API Shield > Schema Validation > Add validation\n- Upload .yml/.yaml/.json (OpenAPI v3.0)\n- Endpoints auto-added to Endpoint Management\n- Action: Log | Block | None\n- Body inspection: JSON payloads\n```\n\n**Change validation action:**\n```\nSecurity > API Shield > Settings > Schema Validation\nPer-endpoint: Filter → ellipses → Change action\nDefault action: Set global mitigation action\n```\n\n**Migration from Classic:**\n```\n1. Export existing schema (if available)\n2. Delete all Classic schema validation rules\n3. Wait 5 min for cache clear\n4. Re-upload via Schema Validation 2.0 interface\n5. Verify in Security > Events\n```\n\n**Fallthrough rule** (catch-all unknown endpoints):\n```\nSecurity > API Shield > Settings > Fallthrough > Use Template\n- Select hostnames\n- Create rule with cf.api_gateway.fallthrough_triggered\n- Action: Log (discover) or Block (strict)\n```\n\n**Body inspection:** Supports `application/json`, `*/*`, `application/*`. Disable origin MIME sniffing to prevent bypasses.\n\n## JWT Validation\n\n**Setup token config:**\n```\nSecurity > API Shield > Settings > JWT Settings > Add configuration\n- Name: \"Auth0 JWT Config\"\n- Location: Header/Cookie + name (e.g., \"Authorization\")\n- JWKS: Paste public keys from IdP\n```\n\n**Create validation rule:**\n```\nSecurity > API Shield > API Rules > Add rule\n- Hostname: api.example.com\n- Deselect endpoints to ignore\n- Token config: Select config\n- Enforce presence: Ignore or Mark as non-compliant\n- Action: Log/Block/Challenge\n```\n\n**Rate limit by JWT claim:**\n```wirefilter\nlookup_json_string(http.request.jwt.claims[\"{config_id}\"][0], \"sub\")\n```\n\n**Special cases:**\n- Two JWTs, different IdPs: Create 2 configs, select both, \"Validate all\"\n- IdP migration: 2 configs + 2 rules, adjust actions per state\n- Bearer prefix: API Shield handles with/without\n- Nested claims: Dot notation `user.email`\n\n## Mutual TLS (mTLS)\n\n**Setup:**\n```\nSSL/TLS > Client Certificates > Create Certificate\n- Generate CF-managed CA (all plans)\n- Upload custom CA (Enterprise, max 5)\n```\n\n**Configure mTLS rule:**\n```\nSecurity > API Shield > mTLS\n- Select hostname(s)\n- Choose certificate(s)\n- Action: Block/Log/Challenge\n```\n\n**Test:**\n```bash\nopenssl req -x509 -newkey rsa:4096 -keyout client-key.pem -out client-cert.pem -days 365\ncurl https://api.example.com/endpoint --cert client-cert.pem --key client-key.pem\n```\n\n## Session Identifiers\n\nCritical for BOLA Detection, Sequence Mitigation, and analytics. Configure header/cookie that uniquely IDs API users.\n\n**Examples:** JWT sub claim, session token, API key, custom user ID header\n\n**Configure:**\n```\nSecurity > API Shield > Settings > Session Identifiers\n- Type: Header/Cookie\n- Name: \"X-User-ID\" or \"Authorization\"\n```\n\n## BOLA Detection\n\nDetects Broken Object Level Authorization attacks (enumeration + parameter pollution).\n\n**Enable:**\n```\nSecurity > API Shield > Schema Validation > [Select Schema] > BOLA Detection\n- Enable detection\n- Threshold: Sensitivity level (Low/Medium/High)\n- Action: Log or Block\n```\n\n**Requirements:**\n- Schema Validation 2.0 enabled\n- Session identifiers configured\n- Minimum traffic: 1000+ requests/day per endpoint\n\n## Authentication Posture\n\nIdentifies unprotected or inconsistently protected endpoints.\n\n**View report:**\n```\nSecurity > API Shield > Authentication Posture\n- Shows endpoints lacking JWT/mTLS\n- Highlights mixed authentication patterns\n```\n\n**Remediate:**\n1. Review flagged endpoints\n2. Add JWT validation rules\n3. Configure mTLS for sensitive endpoints\n4. Monitor posture score\n\n## Volumetric Abuse + GraphQL\n\n**Volumetric Abuse Detection:**\n`Security > API Shield > Settings > Volumetric Abuse Detection`\n- Enable per-endpoint monitoring, set thresholds, action: Log | Challenge | Block\n\n**GraphQL Protection:**\n`Security > API Shield > Settings > GraphQL Protection`\n- Max query depth: 10, max size: 100KB, block introspection (production)\n\n## Terraform\n\n```hcl\n# Session identifier\nresource \"cloudflare_api_shield\" \"main\" {\n  zone_id = var.zone_id\n  auth_id_characteristics {\n    type = \"header\"\n    name = \"Authorization\"\n  }\n}\n\n# Add endpoint\nresource \"cloudflare_api_shield_operation\" \"users_get\" {\n  zone_id  = var.zone_id\n  method   = \"GET\"\n  host     = \"api.example.com\"\n  endpoint = \"/api/users/{id}\"\n}\n\n# JWT validation rule\nresource \"cloudflare_ruleset\" \"jwt_validation\" {\n  zone_id = var.zone_id\n  name    = \"API JWT Validation\"\n  kind    = \"zone\"\n  phase   = \"http_request_firewall_custom\"\n\n  rules {\n    action = \"block\"\n    expression = \"(http.host eq \\\"api.example.com\\\" and not is_jwt_valid(http.request.jwt.payload[\\\"{config_id}\\\"][0]))\"\n    description = \"Block invalid JWTs\"\n  }\n}\n```\n\n## See Also\n\n- [api.md](api.md) - API endpoints and Workers integration\n- [patterns.md](patterns.md) - Firewall rules and deployment patterns\n- [gotchas.md](gotchas.md) - Troubleshooting and limits\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/api-shield/gotchas.md",
    "content": "# Gotchas & Troubleshooting\n\n## Common Errors\n\n### \"Schema Validation 2.0 not working after migration\"\n\n**Cause:** Classic rules still active, conflicting with new system\n**Solution:**\n1. Delete ALL Classic schema validation rules\n2. Clear Cloudflare cache (wait 5 min)\n3. Re-upload schema via new Schema Validation 2.0 interface\n4. Verify in Security > Events\n5. Check action is set (Log/Block)\n\n### \"Schema validation blocking valid requests\"\n\n**Cause:** Schema too restrictive, missing fields, or incorrect types\n**Solution:** \n1. Check Firewall Events for violation details\n2. Review schema in Settings\n3. Test schema in Swagger Editor\n4. Use Log mode to validate before blocking\n5. Update schema with correct specifications\n6. Ensure Schema Validation 2.0 (not Classic)\n\n### \"JWT validation failing\"\n\n**Cause:** JWKS mismatch with IdP, expired token, wrong header/cookie name, or clock skew\n**Solution:** \n1. Verify JWKS matches IdP configuration\n2. Check token `exp` claim is valid\n3. Confirm header/cookie name matches config\n4. Test token at jwt.io\n5. Account for clock skew (±5 min tolerance)\n6. Use modern syntax: `is_jwt_valid(http.request.jwt.payload[\"{config_id}\"][0])`\n\n### \"BOLA detection false positives\"\n\n**Cause:** Legitimate sequential access patterns, bulk operations, or sensitivity too high\n**Solution:**\n1. Review BOLA events in Security > Events\n2. Lower sensitivity threshold (High → Medium → Low)\n3. Exclude legitimate bulk operations from detection\n4. Ensure session identifiers uniquely identify users\n5. Verify minimum traffic requirements met (1000+ req/day)\n\n### \"Risk labels not appearing in firewall rules\"\n\n**Cause:** Feature not enabled, insufficient traffic, or missing session identifiers\n**Solution:**\n1. Verify Schema Validation 2.0 enabled\n2. Enable BOLA Detection in schema settings\n3. Configure session identifiers (required for BOLA)\n4. Wait 24-48h for ML model training\n5. Check minimum traffic thresholds met\n\n### \"Endpoint discovery not finding APIs\"\n\n**Cause:** Insufficient traffic (<500 reqs/10d), non-2xx responses, Worker direct requests, or incorrect session ID config\n**Solution:** Ensure 500+ requests in 10 days, 2xx responses from edge (not Workers direct), configure session IDs correctly. ML updates daily.\n\n### \"Sequence detection false positives\"\n\n**Cause:** Lookback window issues, non-unique session IDs, or model sensitivity\n**Solution:** \n1. Review lookback settings (10 reqs to managed endpoints, 10min window)\n2. Ensure session ID uniqueness per user (not shared tokens)\n3. Adjust positive/negative model balance\n4. Exclude legitimate workflows from detection\n\n### \"GraphQL protection blocking valid queries\"\n\n**Cause:** Query depth/size limits too restrictive, complex but legitimate queries\n**Solution:**\n1. Review blocked query patterns in Security > Events\n2. Increase max_depth (default: 10) if needed\n3. Increase max_size (default: 100KB) for complex queries\n4. Whitelist specific query signatures\n5. Use Log mode to tune before blocking\n\n### \"Token invalid\"\n\n**Cause:** Configuration error, JWKS mismatch, or expired token\n**Solution:** Verify config matches IdP, update JWKS, check token expiration\n\n### \"Schema violation\"\n\n**Cause:** Missing required fields, wrong data types, or spec mismatch\n**Solution:** Review schema against actual requests, ensure all required fields present, validate types match spec\n\n### \"Fallthrough\"\n\n**Cause:** Unknown endpoint or pattern mismatch\n**Solution:** Update schema with all endpoints, check path pattern matching\n\n### \"mTLS failed\"\n\n**Cause:** Certificate untrusted/expired or wrong CA\n**Solution:** Verify cert chain, check expiration, confirm correct CA uploaded\n\n## Limits (2026)\n\n| Resource/Limit | Value | Notes |\n|----------------|-------|-------|\n| OpenAPI version | v3.0.x only | No external refs, must be valid |\n| Schema operations | 10K (Enterprise) | Contact for higher limits |\n| JWT validation sources | Headers/cookies only | No query params/body |\n| Endpoint discovery | 500+ reqs/10d | Minimum for ML model |\n| Path normalization | Automatic | `/profile/238` → `/profile/{var1}` |\n| Schema parameters | No `content` field | No object param validation |\n| BOLA detection | 1000+ reqs/day/endpoint | Per-endpoint minimum |\n| Session ID uniqueness | Required | BOLA/Sequence need unique IDs |\n| GraphQL max depth | 1-50 | Default: 10 |\n| GraphQL max size | 1KB-1MB | Default: 100KB |\n| JWT claim nesting | 10 levels max | Use dot notation |\n| mTLS CA certificates | 5 custom max | CF-managed unlimited |\n| Schema upload size | 5MB max | Compressed OpenAPI spec |\n| Volumetric abuse baseline | 7 days training | Initial ML period |\n| Auth Posture refresh | Daily | Updated nightly |\n\n## See Also\n\n- [configuration.md](configuration.md) - Setup guides to avoid common issues\n- [patterns.md](patterns.md) - Best practices and progressive rollout\n- [API Shield Docs](https://developers.cloudflare.com/api-shield/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/api-shield/patterns.md",
    "content": "# Patterns & Use Cases\n\n## Protect API with Schema + JWT\n\n```bash\n# 1. Upload OpenAPI schema\nPOST /zones/{zone_id}/api_gateway/user_schemas\n\n# 2. Configure JWT validation\nPOST /zones/{zone_id}/api_gateway/token_validation\n{\n  \"name\": \"Auth0\",\n  \"location\": {\"header\": \"Authorization\"},\n  \"jwks\": \"{...}\"\n}\n\n# 3. Create JWT rule\nPOST /zones/{zone_id}/api_gateway/jwt_validation_rules\n\n# 4. Set schema validation action\nPUT /zones/{zone_id}/api_gateway/settings/schema_validation\n{\"validation_default_mitigation_action\": \"block\"}\n```\n\n## Progressive Rollout\n\n```\n1. Log mode: Observe false positives\n   - Schema: Action = Log\n   - JWT: Action = Log\n\n2. Block subset: Protect critical endpoints\n   - Change specific endpoint actions to Block\n   - Monitor firewall events\n\n3. Full enforcement: Block all violations\n   - Change default action to Block\n   - Handle fallthrough with custom rule\n```\n\n## BOLA Detection\n\n### Enumeration Detection\nDetects sequential resource access (e.g., `/users/1`, `/users/2`, `/users/3`).\n\n```javascript\n// Block BOLA enumeration attempts\n(cf.api_gateway.cf-risk-bola-enumeration and http.host eq \"api.example.com\")\n// Action: Block or Challenge\n```\n\n### Parameter Pollution\nDetects duplicate/excessive parameters in requests.\n\n```javascript\n// Block parameter pollution\n(cf.api_gateway.cf-risk-bola-pollution and http.host eq \"api.example.com\")\n// Action: Block\n```\n\n### Combined BOLA Protection\n```javascript\n// Comprehensive BOLA rule\n(cf.api_gateway.cf-risk-bola-enumeration or cf.api_gateway.cf-risk-bola-pollution)\nand http.host eq \"api.example.com\"\n// Action: Block\n```\n\n## Authentication Posture\n\n### Detect Missing Auth\n```javascript\n// Log endpoints lacking authentication\n(cf.api_gateway.cf-risk-missing-auth and http.host eq \"api.example.com\")\n// Action: Log (for audit)\n```\n\n### Detect Mixed Auth\n```javascript\n// Alert on inconsistent auth patterns\n(cf.api_gateway.cf-risk-mixed-auth and http.host eq \"api.example.com\")\n// Action: Log (review required)\n```\n\n## Fallthrough Detection (Shadow APIs)\n\n```javascript\n// WAF Custom Rule\n(cf.api_gateway.fallthrough_triggered and http.host eq \"api.example.com\")\n// Action: Log (discover unknown) or Block (strict)\n```\n\n## Rate Limiting by User\n\n```javascript\n// Rate Limiting Rule (modern syntax)\n(http.host eq \"api.example.com\" and\n is_jwt_valid(http.request.jwt.payload[\"{config_id}\"][0]))\n\n// Rate: 100 req/60s\n// Counting expression: lookup_json_string(http.request.jwt.payload[\"{config_id}\"][0], \"sub\")\n```\n\n## Volumetric Abuse Response\n\n```javascript\n// Detect abnormal traffic spikes\n(cf.api_gateway.volumetric_abuse_detected and http.host eq \"api.example.com\")\n// Action: Challenge or Rate Limit\n\n// Combined with rate limiting\n(cf.api_gateway.volumetric_abuse_detected or\n cf.threat_score gt 50) and http.host eq \"api.example.com\"\n// Action: JS Challenge\n```\n\n## GraphQL Protection\n\n```javascript\n// Block oversized queries\n(http.request.uri.path eq \"/graphql\" and\n cf.api_gateway.graphql_query_size gt 100000)\n// Action: Block\n\n// Block deep nested queries\n(http.request.uri.path eq \"/graphql\" and\n cf.api_gateway.graphql_query_depth gt 10)\n// Action: Block\n```\n\n## Architecture Patterns\n\n**Public API:** Discovery + Schema Validation 2.0 + JWT + Rate Limiting + Bot Management  \n**Partner API:** mTLS + Schema Validation + Sequence Mitigation  \n**Internal API:** Discovery + Schema Learning + Auth Posture\n\n## OWASP API Security Top 10 Mapping (2026)\n\n| OWASP Issue | API Shield Solutions |\n|-------------|---------------------|\n| API1:2023 Broken Object Level Authorization | **BOLA Detection** (enumeration + pollution), Sequence mitigation, Schema, JWT, Rate Limiting |\n| API2:2023 Broken Authentication | **Auth Posture**, mTLS, JWT validation, Bot Management |\n| API3:2023 Broken Object Property Auth | Schema validation, JWT validation |\n| API4:2023 Unrestricted Resource Access | Rate Limiting, **Volumetric Abuse Detection**, **GraphQL Protection**, Bot Management |\n| API5:2023 Broken Function Level Auth | Schema validation, JWT validation, Auth Posture |\n| API6:2023 Unrestricted Business Flows | Sequence mitigation, Bot Management |\n| API7:2023 SSRF | Schema validation, WAF managed rules |\n| API8:2023 Security Misconfiguration | **Schema Validation 2.0**, Auth Posture, WAF rules |\n| API9:2023 Improper Inventory Management | **API Discovery**, Schema learning, Auth Posture |\n| API10:2023 Unsafe API Consumption | JWT validation, Schema validation, WAF managed |\n\n## Monitoring\n\n**Security Events:** `Security > Events` → Filter: Action = block, Service = API Shield  \n**Firewall Analytics:** `Analytics > Security` → Filter by `cf.api_gateway.*` fields  \n**Logpush fields:** APIGatewayAuthIDPresent, APIGatewayRequestViolatesSchema, APIGatewayFallthroughDetected, JWTValidationResult\n\n## Availability (2026)\n\n| Feature | Availability | Notes |\n|---------|-------------|-------|\n| mTLS (CF-managed CA) | All plans | Self-service |\n| Endpoint Management | All plans | Limited operations |\n| Schema Validation 2.0 | All plans | Limited operations |\n| API Discovery | Enterprise | 10K+ ops |\n| JWT Validation | Enterprise add-on | Full validation |\n| BOLA Detection | Enterprise add-on | Requires session IDs |\n| Auth Posture | Enterprise add-on | Security audit |\n| Volumetric Abuse Detection | Enterprise add-on | Traffic analysis |\n| GraphQL Protection | Enterprise add-on | Query limits |\n| Sequence Mitigation | Enterprise (beta) | Contact team |\n| Full Suite | Enterprise add-on | All features |\n\n**Enterprise limits:** 10K operations (contact for higher). Preview access available for non-contract evaluation.\n\n## See Also\n\n- [configuration.md](configuration.md) - Setup all features before creating rules\n- [api.md](api.md) - Firewall field reference and API endpoints\n- [gotchas.md](gotchas.md) - Common issues and limits\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/argo-smart-routing/README.md",
    "content": "# Cloudflare Argo Smart Routing Skill Reference\n\n## Overview\n\nCloudflare Argo Smart Routing is a performance optimization service that detects real-time network issues and routes web traffic across the most efficient network path. It continuously monitors network conditions and intelligently routes traffic through the fastest, most reliable routes in Cloudflare's network.\n\n**Note on Smart Shield:** Argo Smart Routing is being integrated into Cloudflare's Smart Shield product for enhanced DDoS protection and performance. Existing Argo customers maintain full functionality with gradual migration to Smart Shield features.\n\n## Quick Start\n\n### Enable via cURL\n```bash\ncurl -X PATCH \"https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"value\": \"on\"}'\n```\n\n### Enable via TypeScript SDK\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN });\n\nconst result = await client.argo.smartRouting.edit({\n  zone_id: 'your-zone-id',\n  value: 'on',\n});\n\nconsole.log(`Argo enabled: ${result.value}`);\n```\n\n## Core Concepts\n\n### What It Does\n- **Intelligent routing**: Detects congestion, outages, packet loss in real-time\n- **Global optimization**: Routes across 300+ Cloudflare data centers\n- **Automatic failover**: Switches paths when issues detected (typically <1s)\n- **Works with existing setup**: No origin changes required\n\n### Billing Model\n- Usage-based: Charged per GB of traffic (excluding DDoS/WAF mitigated traffic)\n- Requires billing configuration before enabling\n- Available on Enterprise+ plans (check zone eligibility)\n\n### When to Use\n- **High-traffic production sites** with global user base\n- **Latency-sensitive applications** (APIs, real-time services)\n- **Sites behind Cloudflare proxy** (orange-clouded DNS records)\n- **Combined with Tiered Cache** for maximum performance gains\n\n### When NOT to Use\n- Development/staging environments (cost control)\n- Low-traffic sites (<1TB/month) where cost may exceed benefit\n- Sites with primarily single-region traffic\n\n## Should I Enable Argo?\n\n| Your Situation | Recommendation |\n|----------------|----------------|\n| Global production app, >1TB/month traffic | ✅ Enable - likely ROI positive |\n| Enterprise plan, latency-critical APIs | ✅ Enable - performance matters |\n| Regional site, <100GB/month traffic | ⚠️ Evaluate - cost may not justify |\n| Development/staging environment | ❌ Disable - use in production only |\n| Not yet configured billing | ❌ Configure billing first |\n\n## Reading Order by Task\n\n| Your Goal | Start With | Then Read |\n|-----------|------------|-----------|\n| Enable Argo for first time | Quick Start above → [configuration.md](configuration.md) | [gotchas.md](gotchas.md) |\n| Use TypeScript/Python SDK | [api.md](api.md) | [patterns.md](patterns.md) |\n| Terraform/IaC setup | [configuration.md](configuration.md) | - |\n| Enable for Spectrum TCP app | [patterns.md](patterns.md) → Spectrum section | [api.md](api.md) |\n| Troubleshoot enablement issue | [gotchas.md](gotchas.md) | [api.md](api.md) |\n| Manage billing/usage | [patterns.md](patterns.md) → Billing section | [gotchas.md](gotchas.md) |\n\n## In This Reference\n\n- **[api.md](api.md)** - API endpoints, SDK methods, error handling, Python/TypeScript examples\n- **[configuration.md](configuration.md)** - Terraform setup, environment config, billing configuration\n- **[patterns.md](patterns.md)** - Tiered Cache integration, Spectrum TCP apps, billing management, validation patterns\n- **[gotchas.md](gotchas.md)** - Common errors, permission issues, limits, best practices\n\n## See Also\n\n- [Cloudflare Argo Smart Routing Docs](https://developers.cloudflare.com/argo-smart-routing/)\n- [Cloudflare Smart Shield](https://developers.cloudflare.com/smart-shield/)\n- [Spectrum Documentation](https://developers.cloudflare.com/spectrum/)\n- [Tiered Cache](https://developers.cloudflare.com/cache/how-to/tiered-cache/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/argo-smart-routing/api.md",
    "content": "## API Reference\n\n**Note on Smart Shield:** Argo Smart Routing is being integrated into Cloudflare's Smart Shield product. API endpoints remain stable; existing integrations continue to work without changes.\n\n### Base Endpoint\n```\nhttps://api.cloudflare.com/client/v4\n```\n\n### Authentication\nUse API tokens with Zone:Argo Smart Routing:Edit permissions:\n\n```bash\n# Headers required\nX-Auth-Email: user@example.com\nAuthorization: Bearer YOUR_API_TOKEN\n```\n\n### Get Argo Smart Routing Status\n\n**Endpoint:** `GET /zones/{zone_id}/argo/smart_routing`\n\n**Description:** Retrieves current Argo Smart Routing enablement status.\n\n**cURL Example:**\n```bash\ncurl -X GET \"https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n  -H \"Content-Type: application/json\"\n```\n\n**Response:**\n```json\n{\n  \"result\": {\n    \"id\": \"smart_routing\",\n    \"value\": \"on\",\n    \"editable\": true,\n    \"modified_on\": \"2024-01-11T12:00:00Z\"\n  },\n  \"success\": true,\n  \"errors\": [],\n  \"messages\": []\n}\n```\n\n**TypeScript SDK Example:**\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst client = new Cloudflare({\n  apiToken: process.env.CLOUDFLARE_API_TOKEN\n});\n\nconst status = await client.argo.smartRouting.get({ zone_id: 'your-zone-id' });\nconsole.log(`Argo status: ${status.value}, editable: ${status.editable}`);\n```\n\n**Python SDK Example:**\n```python\nfrom cloudflare import Cloudflare\n\nclient = Cloudflare(api_token=os.environ.get('CLOUDFLARE_API_TOKEN'))\n\nstatus = client.argo.smart_routing.get(zone_id='your-zone-id')\nprint(f\"Argo status: {status.value}, editable: {status.editable}\")\n```\n\n### Update Argo Smart Routing Status\n\n**Endpoint:** `PATCH /zones/{zone_id}/argo/smart_routing`\n\n**Description:** Enable or disable Argo Smart Routing for a zone.\n\n**Request Body:**\n```json\n{\n  \"value\": \"on\"  // or \"off\"\n}\n```\n\n**cURL Example:**\n```bash\ncurl -X PATCH \"https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"value\": \"on\"}'\n```\n\n**TypeScript SDK Example:**\n```typescript\nconst result = await client.argo.smartRouting.edit({\n  zone_id: 'your-zone-id',\n  value: 'on',\n});\nconsole.log(`Updated: ${result.value} at ${result.modified_on}`);\n```\n\n**Python SDK Example:**\n```python\nresult = client.argo.smart_routing.edit(\n    zone_id='your-zone-id',\n    value='on'\n)\nprint(f\"Updated: {result.value} at {result.modified_on}\")\n```\n\n## Checking Editability Before Updates\n\n**Critical:** Always check the `editable` field before attempting to enable/disable Argo. When `editable: false`, the zone has restrictions (billing not configured, insufficient permissions, or plan limitations).\n\n**Pattern:**\n```typescript\nasync function safelyEnableArgo(client: Cloudflare, zoneId: string): Promise<boolean> {\n  const status = await client.argo.smartRouting.get({ zone_id: zoneId });\n  \n  if (!status.editable) {\n    console.error('Cannot modify Argo: editable=false (check billing/permissions)');\n    return false;\n  }\n  \n  if (status.value === 'on') {\n    console.log('Argo already enabled');\n    return true;\n  }\n  \n  await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });\n  console.log('Argo enabled successfully');\n  return true;\n}\n```\n\n**Python Pattern:**\n```python\ndef safely_enable_argo(client: Cloudflare, zone_id: str) -> bool:\n    status = client.argo.smart_routing.get(zone_id=zone_id)\n    \n    if not status.editable:\n        print('Cannot modify Argo: editable=false (check billing/permissions)')\n        return False\n    \n    if status.value == 'on':\n        print('Argo already enabled')\n        return True\n    \n    client.argo.smart_routing.edit(zone_id=zone_id, value='on')\n    print('Argo enabled successfully')\n    return True\n```\n\n## Error Handling\n\nThe TypeScript SDK provides typed error classes for robust error handling:\n\n```typescript\nimport Cloudflare from 'cloudflare';\nimport { APIError, APIConnectionError, RateLimitError } from 'cloudflare';\n\nasync function enableArgoWithErrorHandling(client: Cloudflare, zoneId: string) {\n  try {\n    const result = await client.argo.smartRouting.edit({\n      zone_id: zoneId,\n      value: 'on',\n    });\n    return result;\n  } catch (error) {\n    if (error instanceof RateLimitError) {\n      console.error('Rate limited. Retry after:', error.response?.headers.get('retry-after'));\n      // Implement exponential backoff\n    } else if (error instanceof APIError) {\n      console.error('API error:', error.status, error.message);\n      if (error.status === 403) {\n        console.error('Permission denied - check API token scopes');\n      } else if (error.status === 400) {\n        console.error('Bad request - verify zone_id and payload');\n      }\n    } else if (error instanceof APIConnectionError) {\n      console.error('Connection failed:', error.message);\n      // Retry with exponential backoff\n    } else {\n      console.error('Unexpected error:', error);\n    }\n    throw error;\n  }\n}\n```\n\n**Python Error Handling:**\n```python\nfrom cloudflare import Cloudflare, APIError, RateLimitError\n\ndef enable_argo_with_error_handling(client: Cloudflare, zone_id: str):\n    try:\n        result = client.argo.smart_routing.edit(zone_id=zone_id, value='on')\n        return result\n    except RateLimitError as e:\n        print(f\"Rate limited. Retry after: {e.response.headers.get('retry-after')}\")\n        raise\n    except APIError as e:\n        print(f\"API error: {e.status} - {e.message}\")\n        if e.status == 403:\n            print('Permission denied - check API token scopes')\n        elif e.status == 400:\n            print('Bad request - verify zone_id and payload')\n        raise\n    except Exception as e:\n        print(f\"Unexpected error: {e}\")\n        raise\n```\n\n## Response Schema\n\nAll Argo Smart Routing API responses follow this structure:\n\n```typescript\ninterface ArgoSmartRoutingResponse {\n  result: {\n    id: 'smart_routing';\n    value: 'on' | 'off';\n    editable: boolean;\n    modified_on: string; // ISO 8601 timestamp\n  };\n  success: boolean;\n  errors: Array<{\n    code: number;\n    message: string;\n  }>;\n  messages: Array<string>;\n}\n```\n\n## Key Response Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `value` | `\"on\" \\| \"off\"` | Current enablement status |\n| `editable` | `boolean` | Whether changes are allowed (check before PATCH) |\n| `modified_on` | `string` | ISO timestamp of last modification |\n| `success` | `boolean` | Whether request succeeded |\n| `errors` | `Array` | Error details if `success: false`"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/argo-smart-routing/configuration.md",
    "content": "## Configuration Management\n\n**Note on Smart Shield Evolution:** Argo Smart Routing is being integrated into Smart Shield. Configuration methods below remain valid; Terraform and IaC patterns unchanged.\n\n### Infrastructure as Code (Terraform)\n\n```hcl\n# terraform/argo.tf\n# Note: Use Cloudflare Terraform provider\n\nresource \"cloudflare_argo\" \"example\" {\n  zone_id        = var.zone_id\n  smart_routing  = \"on\"\n  tiered_caching = \"on\"\n}\n\nvariable \"zone_id\" {\n  description = \"Cloudflare Zone ID\"\n  type        = string\n}\n\noutput \"argo_enabled\" {\n  value       = cloudflare_argo.example.smart_routing\n  description = \"Argo Smart Routing status\"\n}\n```\n\n### Environment-Based Configuration\n\n```typescript\n// config/argo.ts\ninterface ArgoEnvironmentConfig {\n  enabled: boolean;\n  tieredCache: boolean;\n  monitoring: {\n    usageAlerts: boolean;\n    threshold: number;\n  };\n}\n\nconst configs: Record<string, ArgoEnvironmentConfig> = {\n  production: {\n    enabled: true,\n    tieredCache: true,\n    monitoring: {\n      usageAlerts: true,\n      threshold: 1000, // GB\n    },\n  },\n  staging: {\n    enabled: true,\n    tieredCache: false,\n    monitoring: {\n      usageAlerts: false,\n      threshold: 100, // GB\n    },\n  },\n  development: {\n    enabled: false,\n    tieredCache: false,\n    monitoring: {\n      usageAlerts: false,\n      threshold: 0,\n    },\n  },\n};\n\nexport function getArgoConfig(env: string): ArgoEnvironmentConfig {\n  return configs[env] || configs.development;\n}\n```\n\n### Pulumi Configuration\n\n```typescript\n// pulumi/argo.ts\nimport * as cloudflare from '@pulumi/cloudflare';\n\nconst zone = new cloudflare.Zone('example-zone', {\n  zone: 'example.com',\n  plan: 'enterprise',\n});\n\nconst argoSettings = new cloudflare.Argo('argo-config', {\n  zoneId: zone.id,\n  smartRouting: 'on',\n  tieredCaching: 'on',\n});\n\nexport const argoEnabled = argoSettings.smartRouting;\nexport const zoneId = zone.id;\n```\n\n## Billing Configuration\n\nBefore enabling Argo Smart Routing, ensure billing is configured for the account:\n\n**Prerequisites:**\n1. Valid payment method on file\n2. Enterprise or higher plan\n3. Zone must have billing enabled\n\n**Check Billing Status via Dashboard:**\n1. Navigate to Account → Billing\n2. Verify payment method configured\n3. Check zone subscription status\n\n**Note:** Attempting to enable Argo without billing configured will result in `editable: false` in API responses.\n\n## Environment Variable Setup\n\n**Required Environment Variables:**\n```bash\n# .env\nCLOUDFLARE_API_TOKEN=your_api_token_here\nCLOUDFLARE_ZONE_ID=your_zone_id_here\nCLOUDFLARE_ACCOUNT_ID=your_account_id_here\n\n# Optional\nARGO_ENABLED=true\nARGO_TIERED_CACHE=true\n```\n\n**TypeScript Configuration Loader:**\n```typescript\n// config/env.ts\nimport { z } from 'zod';\n\nconst envSchema = z.object({\n  CLOUDFLARE_API_TOKEN: z.string().min(1),\n  CLOUDFLARE_ZONE_ID: z.string().min(1),\n  CLOUDFLARE_ACCOUNT_ID: z.string().min(1),\n  ARGO_ENABLED: z.string().optional().default('false'),\n  ARGO_TIERED_CACHE: z.string().optional().default('false'),\n});\n\nexport const env = envSchema.parse(process.env);\n\nexport const argoConfig = {\n  enabled: env.ARGO_ENABLED === 'true',\n  tieredCache: env.ARGO_TIERED_CACHE === 'true',\n};\n```\n\n## CI/CD Integration\n\n**GitHub Actions Example:**\n```yaml\n# .github/workflows/deploy-argo.yml\nname: Deploy Argo Configuration\n\non:\n  push:\n    branches: [main]\n    paths:\n      - 'terraform/argo.tf'\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      \n      - name: Setup Terraform\n        uses: hashicorp/setup-terraform@v2\n        \n      - name: Terraform Init\n        run: terraform init\n        working-directory: ./terraform\n        \n      - name: Terraform Apply\n        run: terraform apply -auto-approve\n        working-directory: ./terraform\n        env:\n          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n          TF_VAR_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }}\n```\n\n## Enterprise Preview Program\n\nFor early access to Argo Smart Routing features and Smart Shield integration:\n\n**Eligibility:**\n- Enterprise plan customers\n- Active Cloudflare support contract\n- Production traffic >100GB/month\n\n**How to Join:**\n1. Contact Cloudflare account team or support\n2. Request Argo/Smart Shield preview access\n3. Receive preview zone configuration\n\n**Preview Features:**\n- Enhanced analytics and reporting\n- Smart Shield DDoS integration\n- Advanced routing policies\n- Priority support for routing issues"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/argo-smart-routing/gotchas.md",
    "content": "## Best Practices Summary\n\n**Smart Shield Note:** Argo Smart Routing evolving into Smart Shield. Best practices below remain applicable; monitor Cloudflare changelog for Smart Shield updates.\n\n1. **Always check editability** before attempting to enable/disable Argo\n2. **Set up billing notifications** to avoid unexpected costs\n3. **Combine with Tiered Cache** for maximum performance benefit\n4. **Use in production only** - disable for dev/staging to control costs\n5. **Monitor analytics** - require 500+ requests in 48h for detailed metrics\n6. **Handle errors gracefully** - check for billing, permissions, zone compatibility\n7. **Test configuration changes** in staging before production\n8. **Use TypeScript SDK** for type safety and better developer experience\n9. **Implement retry logic** for API calls in production systems\n10. **Document zone-specific settings** for team visibility\n\n## Common Errors\n\n### \"Argo unavailable\"\n\n**Problem:** API returns error \"Argo Smart Routing is unavailable for this zone\"\n\n**Cause:** Zone not eligible or billing not set up\n\n**Solution:**\n1. Verify zone has Enterprise or higher plan\n2. Check billing is configured in Account → Billing\n3. Ensure payment method is valid and current\n4. Contact Cloudflare support if eligibility unclear\n\n### \"Cannot enable/disable\"\n\n**Problem:** API call succeeds but status remains unchanged, or `editable: false` in GET response\n\n**Cause:** Insufficient permissions or zone restrictions\n\n**Solution:**\n1. Check API token has `Zone:Argo Smart Routing:Edit` permission\n2. Verify `editable: true` in GET response before attempting PATCH\n3. If `editable: false`, check:\n   - Billing configured for account\n   - Zone plan includes Argo (Enterprise+)\n   - No active zone holds or suspensions\n   - API token has correct scopes\n\n### `editable: false` Error\n\n**Problem:** GET request returns `\"editable\": false`, preventing enable/disable\n\n**Cause:** Zone-level restrictions from billing, plan, or permissions\n\n**Solution Pattern:**\n```typescript\nconst status = await client.argo.smartRouting.get({ zone_id: zoneId });\n\nif (!status.editable) {\n  // Don't attempt to modify - will fail\n  console.error('Cannot modify Argo settings:');\n  console.error('- Check billing is configured');\n  console.error('- Verify zone has Enterprise+ plan');\n  console.error('- Confirm API token has Edit permission');\n  throw new Error('Argo is not editable for this zone');\n}\n\n// Safe to proceed with enable/disable\nawait client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });\n```\n\n### Rate Limiting\n\n**Problem:** `429 Too Many Requests` error from API\n\n**Cause:** Exceeded API rate limits (typically 1200 requests per 5 minutes)\n\n**Solution:**\n```typescript\nimport { RateLimitError } from 'cloudflare';\n\ntry {\n  await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });\n} catch (error) {\n  if (error instanceof RateLimitError) {\n    const retryAfter = error.response?.headers.get('retry-after');\n    console.log(`Rate limited. Retry after ${retryAfter} seconds`);\n    \n    // Implement exponential backoff\n    await new Promise(resolve => setTimeout(resolve, (retryAfter || 60) * 1000));\n    // Retry request\n  }\n}\n```\n\n## Limits\n\n| Resource/Limit | Value | Notes |\n|----------------|-------|-------|\n| Min requests for analytics | 500 in 48h | For detailed metrics via GraphQL |\n| Zones supported | Enterprise+ | Check zone plan in dashboard |\n| Billing requirement | Must be configured | Before enabling; verify payment method |\n| API rate limit | 1200 req / 5 min | Per API token across all endpoints |\n| Spectrum apps | No hard limit | Each app can enable Argo independently |\n| Traffic counting | Proxied only | Only orange-clouded DNS records count |\n| DDoS/WAF exemption | Yes | Mitigated traffic excluded from billing |\n| Analytics latency | 1-5 minutes | Real-time metrics not available |\n\n## Additional Resources\n\n- [Official Argo Smart Routing Docs](https://developers.cloudflare.com/argo-smart-routing/)\n- [Cloudflare Smart Shield](https://developers.cloudflare.com/smart-shield/)\n- [API Authentication](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)\n- [Cloudflare TypeScript SDK](https://github.com/cloudflare/cloudflare-typescript)\n- [Cloudflare Python SDK](https://github.com/cloudflare/cloudflare-python)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/argo-smart-routing/patterns.md",
    "content": "# Integration Patterns\n\n## Enable Argo + Tiered Cache\n\n```typescript\nasync function enableOptimalPerformance(client: Cloudflare, zoneId: string) {\n  await Promise.all([\n    client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }),\n    client.argo.tieredCaching.edit({ zone_id: zoneId, value: 'on' }),\n  ]);\n}\n```\n\n**Flow:** Visitor → Edge (Lower-Tier) → [Cache Miss] → Upper-Tier → [Cache Miss + Argo] → Origin\n\n**Impact:** Argo ~30% latency reduction + Tiered Cache 50-80% origin offload\n\n## Usage Analytics (GraphQL)\n\n```graphql\nquery ArgoAnalytics($zoneTag: string!) {\n  viewer {\n    zones(filter: { zoneTag: $zoneTag }) {\n      httpRequestsAdaptiveGroups(limit: 1000) {\n        sum { argoBytes, bytes }\n      }\n    }\n  }\n}\n```\n\n**Billing:** ~$0.10/GB. DDoS-mitigated and WAF-blocked traffic NOT charged.\n\n## Spectrum TCP Integration\n\nEnable Argo for non-HTTP traffic (databases, game servers, IoT):\n\n```typescript\n// Update existing app\nawait client.spectrum.apps.update(appId, { zone_id: zoneId, argo_smart_routing: true });\n\n// Create new app with Argo\nawait client.spectrum.apps.create({\n  zone_id: zoneId,\n  dns: { type: 'CNAME', name: 'tcp.example.com' },\n  origin_direct: ['tcp://origin.example.com:3306'],\n  protocol: 'tcp/3306',\n  argo_smart_routing: true,\n});\n```\n\n**Use cases:** MySQL/PostgreSQL (3306/5432), game servers, MQTT (1883), SSH (22)\n\n## Pre-Flight Validation\n\n```typescript\nasync function validateArgoEligibility(client: Cloudflare, zoneId: string) {\n  const status = await client.argo.smartRouting.get({ zone_id: zoneId });\n  const zone = await client.zones.get({ zone_id: zoneId });\n  \n  const issues: string[] = [];\n  if (!status.editable) issues.push('Zone not editable');\n  if (['free', 'pro'].includes(zone.plan.legacy_id)) issues.push('Requires Business+ plan');\n  if (zone.status !== 'active') issues.push('Zone not active');\n  \n  return { canEnable: issues.length === 0, issues };\n}\n```\n\n## Post-Enable Verification\n\n```typescript\nasync function verifyArgoEnabled(client: Cloudflare, zoneId: string): Promise<boolean> {\n  await new Promise(r => setTimeout(r, 2000)); // Wait for propagation\n  const status = await client.argo.smartRouting.get({ zone_id: zoneId });\n  return status.value === 'on';\n}\n```\n\n## Full Setup Pattern\n\n```typescript\nasync function setupArgo(client: Cloudflare, zoneId: string) {\n  // 1. Validate\n  const { canEnable, issues } = await validateArgoEligibility(client, zoneId);\n  if (!canEnable) throw new Error(issues.join(', '));\n  \n  // 2. Enable both features\n  await Promise.all([\n    client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }),\n    client.argo.tieredCaching.edit({ zone_id: zoneId, value: 'on' }),\n  ]);\n  \n  // 3. Verify\n  const [argo, cache] = await Promise.all([\n    client.argo.smartRouting.get({ zone_id: zoneId }),\n    client.argo.tieredCaching.get({ zone_id: zoneId }),\n  ]);\n  \n  return { argo: argo.value === 'on', tieredCache: cache.value === 'on' };\n}\n```\n\n**When to combine:** High-traffic sites (>1TB/mo), global users, cacheable content.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/bindings/README.md",
    "content": "# Cloudflare Bindings Skill Reference\n\nExpert guidance on Cloudflare Workers Bindings - the runtime APIs that connect Workers to Cloudflare platform resources.\n\n## What Are Bindings?\n\nBindings are how Workers access Cloudflare resources (storage, compute, services) via the `env` object. They're configured in `wrangler.jsonc`, type-safe via TypeScript, and zero-overhead at runtime.\n\n## Reading Order\n\n1. **This file** - Binding catalog and selection guide\n2. **[api.md](api.md)** - TypeScript types and env access patterns\n3. **[configuration.md](configuration.md)** - Complete wrangler.jsonc examples\n4. **[patterns.md](patterns.md)** - Best practices and common patterns\n5. **[gotchas.md](gotchas.md)** - Critical pitfalls and troubleshooting\n\n## Binding Catalog\n\n### Storage Bindings\n\n| Binding | Use Case | Access Pattern |\n|---------|----------|----------------|\n| **KV** | Key-value cache, CDN-backed reads | `env.MY_KV.get(key)` |\n| **R2** | Object storage (S3-compatible) | `env.MY_BUCKET.get(key)` |\n| **D1** | SQL database (SQLite) | `env.DB.prepare(sql).all()` |\n| **Durable Objects** | Coordination, real-time state | `env.MY_DO.get(id)` |\n| **Vectorize** | Vector embeddings search | `env.VECTORIZE.query(vector)` |\n| **Queues** | Async message processing | `env.MY_QUEUE.send(msg)` |\n\n### Compute Bindings\n\n| Binding | Use Case | Access Pattern |\n|---------|----------|----------------|\n| **Service** | Worker-to-Worker RPC | `env.MY_SERVICE.fetch(req)` |\n| **Workers AI** | LLM inference | `env.AI.run(model, input)` |\n| **Browser Rendering** | Headless Chrome | `env.BROWSER.fetch(url)` |\n\n### Platform Bindings\n\n| Binding | Use Case | Access Pattern |\n|---------|----------|----------------|\n| **Analytics Engine** | Custom metrics | `env.ANALYTICS.writeDataPoint(data)` |\n| **mTLS** | Client certificates | `env.MY_CERT` (string) |\n| **Hyperdrive** | Database pooling | `env.HYPERDRIVE.connectionString` |\n| **Rate Limiting** | Request throttling | `env.RATE_LIMITER.limit(id)` |\n| **Workflows** | Long-running workflows | `env.MY_WORKFLOW.create()` |\n\n### Configuration Bindings\n\n| Binding | Use Case | Access Pattern |\n|---------|----------|----------------|\n| **Environment Variables** | Non-sensitive config | `env.API_URL` (string) |\n| **Secrets** | Sensitive values | `env.API_KEY` (string) |\n| **Text/Data Blobs** | Static files | `env.MY_BLOB` (string) |\n| **WASM** | WebAssembly modules | `env.MY_WASM` (WebAssembly.Module) |\n\n## Quick Selection Guide\n\n**Need persistent storage?**\n- Key-value < 25MB → **KV**\n- Files/objects → **R2**\n- Relational data → **D1**\n- Real-time coordination → **Durable Objects**\n\n**Need AI/compute?**\n- LLM inference → **Workers AI**\n- Scraping/PDFs → **Browser Rendering**\n- Call another Worker → **Service binding**\n\n**Need async processing?**\n- Background jobs → **Queues**\n\n**Need config?**\n- Public values → **Environment Variables**\n- Secrets → **Secrets** (never commit)\n\n## Quick Start\n\n1. **Add binding to wrangler.jsonc:**\n```jsonc\n{\n  \"kv_namespaces\": [\n    { \"binding\": \"MY_KV\", \"id\": \"your-kv-id\" }\n  ]\n}\n```\n\n2. **Generate types:**\n```bash\nnpx wrangler types\n```\n\n3. **Access in Worker:**\n```typescript\nexport default {\n  async fetch(request, env, ctx) {\n    await env.MY_KV.put('key', 'value');\n    return new Response('OK');\n  }\n}\n```\n\n## Type Safety\n\nBindings are fully typed via `wrangler types`. See [api.md](api.md) for details.\n\n## Limits\n\n- 64 bindings max per Worker (all types combined)\n- See [gotchas.md](gotchas.md) for per-binding limits\n\n## Key Concepts\n\n**Zero-overhead access:** Bindings compiled into Worker, no network calls to access\n**Type-safe:** Full TypeScript support via `wrangler types`\n**Per-environment:** Different IDs for dev/staging/production\n**Secrets vs Vars:** Secrets encrypted at rest, never in config files\n\n## See Also\n\n- [Cloudflare Docs: Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/)\n- [Wrangler Configuration](https://developers.cloudflare.com/workers/wrangler/configuration/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/bindings/api.md",
    "content": "# Bindings API Reference\n\n## TypeScript Types\n\nCloudflare generates binding types via `npx wrangler types`. This creates `.wrangler/types/runtime.d.ts` with your Env interface.\n\n### Generated Env Interface\n\nAfter running `wrangler types`, TypeScript knows your bindings:\n\n```typescript\ninterface Env {\n  // From wrangler.jsonc bindings\n  MY_KV: KVNamespace;\n  MY_BUCKET: R2Bucket;\n  DB: D1Database;\n  MY_SERVICE: Fetcher;\n  AI: Ai;\n  \n  // From vars\n  API_URL: string;\n  \n  // From secrets (set via wrangler secret put)\n  API_KEY: string;\n}\n```\n\n### Binding Types\n\n| Config | TypeScript Type | Package |\n|--------|-----------------|---------|\n| `kv_namespaces` | `KVNamespace` | `@cloudflare/workers-types` |\n| `r2_buckets` | `R2Bucket` | `@cloudflare/workers-types` |\n| `d1_databases` | `D1Database` | `@cloudflare/workers-types` |\n| `durable_objects.bindings` | `DurableObjectNamespace` | `@cloudflare/workers-types` |\n| `vectorize` | `VectorizeIndex` | `@cloudflare/workers-types` |\n| `queues.producers` | `Queue` | `@cloudflare/workers-types` |\n| `services` | `Fetcher` | `@cloudflare/workers-types` |\n| `ai` | `Ai` | `@cloudflare/workers-types` |\n| `browser` | `Fetcher` | `@cloudflare/workers-types` |\n| `analytics_engine_datasets` | `AnalyticsEngineDataset` | `@cloudflare/workers-types` |\n| `hyperdrive` | `Hyperdrive` | `@cloudflare/workers-types` |\n| `rate_limiting` | `RateLimit` | `@cloudflare/workers-types` |\n| `workflows` | `Workflow` | `@cloudflare/workers-types` |\n| `mtls_certificates` / `vars` / `text_blobs` / `data_blobs` | `string` | Built-in |\n| `wasm_modules` | `WebAssembly.Module` | Built-in |\n\n## Accessing Bindings\n\n### Method 1: fetch() Handler (Recommended)\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n    const value = await env.MY_KV.get('key');\n    return new Response(value);\n  }\n}\n```\n\n**Why:** Type-safe, aligns with Workers API, supports ctx for waitUntil/passThroughOnException.\n\n### Method 2: Hono Framework\n\n```typescript\nimport { Hono } from 'hono';\n\nconst app = new Hono<{ Bindings: Env }>();\n\napp.get('/', async (c) => {\n  const value = await c.env.MY_KV.get('key');\n  return c.json({ value });\n});\n\nexport default app;\n```\n\n**Why:** c.env auto-typed, ergonomic for routing-heavy apps.\n\n### Method 3: Module Workers (Legacy)\n\n```typescript\nexport async function handleRequest(request: Request, env: Env): Promise<Response> {\n  const value = await env.MY_KV.get('key');\n  return new Response(value);\n}\n\naddEventListener('fetch', (event) => {\n  // env not directly available - requires workarounds\n});\n```\n\n**Avoid:** Use fetch() handler instead (Method 1).\n\n## Type Generation Workflow\n\n### Initial Setup\n\n```bash\n# Install wrangler\nnpm install -D wrangler\n\n# Generate types from wrangler.jsonc\nnpx wrangler types\n```\n\n### After Changing Bindings\n\n```bash\n# Added/modified binding in wrangler.jsonc\nnpx wrangler types\n\n# TypeScript now sees updated Env interface\n```\n\n**Note:** `wrangler types` outputs to `.wrangler/types/runtime.d.ts`. TypeScript picks this up automatically if `@cloudflare/workers-types` is in `tsconfig.json` `\"types\"` array.\n\n## Key Binding Methods\n\n**KV:**\n```typescript\nawait env.MY_KV.get(key, { type: 'json' });  // text|json|arrayBuffer|stream\nawait env.MY_KV.put(key, value, { expirationTtl: 3600 });\nawait env.MY_KV.delete(key);\nawait env.MY_KV.list({ prefix: 'user:' });\n```\n\n**R2:**\n```typescript\nawait env.BUCKET.get(key);\nawait env.BUCKET.put(key, value);\nawait env.BUCKET.delete(key);\nawait env.BUCKET.list({ prefix: 'images/' });\n```\n\n**D1:**\n```typescript\nawait env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();\nawait env.DB.batch([stmt1, stmt2]);\n```\n\n**Service:**\n```typescript\nawait env.MY_SERVICE.fetch(new Request('https://fake/path'));\n```\n\n**Workers AI:**\n```typescript\nawait env.AI.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' });\n```\n\n**Queues:**\n```typescript\nawait env.MY_QUEUE.send({ userId: 123, action: 'process' });\n```\n\n**Durable Objects:**\n```typescript\nconst id = env.MY_DO.idFromName('user-123');\nconst stub = env.MY_DO.get(id);\nawait stub.fetch(new Request('https://fake/increment'));\n```\n\n## Runtime vs Build-Time Types\n\n| Type Source | When Generated | Use Case |\n|-------------|----------------|----------|\n| `@cloudflare/workers-types` | npm install | Base Workers APIs (Request, Response, etc.) |\n| `wrangler types` | After config change | Your specific bindings (Env interface) |\n\n**Install both:**\n```bash\nnpm install -D @cloudflare/workers-types\nnpx wrangler types\n```\n\n## Type Safety Best Practices\n\n1. **Never use `any` for env:**\n```typescript\n// ❌ BAD\nasync fetch(request: Request, env: any) { }\n\n// ✅ GOOD\nasync fetch(request: Request, env: Env) { }\n```\n\n2. **Run wrangler types after config changes:**\n```bash\n# After editing wrangler.jsonc\nnpx wrangler types\n```\n\n3. **Check generated types match config:**\n```bash\n# View generated Env interface\ncat .wrangler/types/runtime.d.ts\n```\n\n## See Also\n\n- [Workers Types Package](https://www.npmjs.com/package/@cloudflare/workers-types)\n- [Wrangler Types Command](https://developers.cloudflare.com/workers/wrangler/commands/#types)"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/bindings/configuration.md",
    "content": "# Binding Configuration Reference\n\n## Storage Bindings\n\n```jsonc\n{\n  \"kv_namespaces\": [{ \"binding\": \"MY_KV\", \"id\": \"...\" }],\n  \"r2_buckets\": [{ \"binding\": \"MY_BUCKET\", \"bucket_name\": \"my-bucket\" }],\n  \"d1_databases\": [{ \"binding\": \"DB\", \"database_name\": \"my-db\", \"database_id\": \"...\" }],\n  \"durable_objects\": { \"bindings\": [{ \"name\": \"MY_DO\", \"class_name\": \"MyDO\" }] },\n  \"vectorize\": [{ \"binding\": \"VECTORIZE\", \"index_name\": \"my-index\" }],\n  \"queues\": { \"producers\": [{ \"binding\": \"MY_QUEUE\", \"queue\": \"my-queue\" }] }\n}\n```\n\n**Create commands:**\n```bash\nnpx wrangler kv namespace create MY_KV\nnpx wrangler r2 bucket create my-bucket\nnpx wrangler d1 create my-db\nnpx wrangler vectorize create my-index --dimensions=768 --metric=cosine\nnpx wrangler queues create my-queue\n\n# List existing resources\nnpx wrangler kv namespace list\nnpx wrangler r2 bucket list\nnpx wrangler d1 list\nnpx wrangler vectorize list\nnpx wrangler queues list\n```\n\n## Compute Bindings\n\n```jsonc\n{\n  \"services\": [{ \n    \"binding\": \"MY_SERVICE\", \n    \"service\": \"other-worker\",\n    \"environment\": \"production\"  // Optional: target specific env\n  }],\n  \"ai\": { \"binding\": \"AI\" },\n  \"browser\": { \"binding\": \"BROWSER\" },\n  \"workflows\": [{ \"binding\": \"MY_WORKFLOW\", \"name\": \"my-workflow\" }]\n}\n```\n\n**Create workflows:**\n```bash\nnpx wrangler workflows create my-workflow\n```\n\n## Platform Bindings\n\n```jsonc\n{\n  \"analytics_engine_datasets\": [{ \"binding\": \"ANALYTICS\" }],\n  \"mtls_certificates\": [{ \"binding\": \"MY_CERT\", \"certificate_id\": \"...\" }],\n  \"hyperdrive\": [{ \"binding\": \"HYPERDRIVE\", \"id\": \"...\" }],\n  \"unsafe\": {\n    \"bindings\": [{ \"name\": \"RATE_LIMITER\", \"type\": \"ratelimit\", \"namespace_id\": \"...\" }]\n  }\n}\n```\n\n## Configuration Bindings\n\n```jsonc\n{\n  \"vars\": {\n    \"API_URL\": \"https://api.example.com\",\n    \"MAX_RETRIES\": \"3\"\n  },\n  \"text_blobs\": { \"MY_TEXT\": \"./data/template.html\" },\n  \"data_blobs\": { \"MY_DATA\": \"./data/config.bin\" },\n  \"wasm_modules\": { \"MY_WASM\": \"./build/module.wasm\" }\n}\n```\n\n**Secrets (never in config):**\n```bash\nnpx wrangler secret put API_KEY\n```\n\n## Environment-Specific Configuration\n\n```jsonc\n{\n  \"name\": \"my-worker\",\n  \"vars\": { \"ENV\": \"production\" },\n  \"kv_namespaces\": [{ \"binding\": \"CACHE\", \"id\": \"prod-kv-id\" }],\n  \n  \"env\": {\n    \"staging\": {\n      \"vars\": { \"ENV\": \"staging\" },\n      \"kv_namespaces\": [{ \"binding\": \"CACHE\", \"id\": \"staging-kv-id\" }]\n    }\n  }\n}\n```\n\n**Deploy:**\n```bash\nnpx wrangler deploy              # Production\nnpx wrangler deploy --env staging\n```\n\n## Local Development\n\n```jsonc\n{\n  \"kv_namespaces\": [{\n    \"binding\": \"MY_KV\",\n    \"id\": \"prod-id\",\n    \"preview_id\": \"dev-id\"  // Used in wrangler dev\n  }]\n}\n```\n\n**Or use remote:**\n```bash\nnpx wrangler dev --remote  # Uses production bindings\n```\n\n## Complete Example\n\n```jsonc\n{\n  \"$schema\": \"./node_modules/wrangler/config-schema.json\",\n  \"name\": \"my-app\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\",\n  \n  \"vars\": { \"API_URL\": \"https://api.example.com\" },\n  \"kv_namespaces\": [{ \"binding\": \"CACHE\", \"id\": \"abc123\" }],\n  \"r2_buckets\": [{ \"binding\": \"ASSETS\", \"bucket_name\": \"my-assets\" }],\n  \"d1_databases\": [{ \"binding\": \"DB\", \"database_name\": \"my-db\", \"database_id\": \"xyz789\" }],\n  \"services\": [{ \"binding\": \"AUTH\", \"service\": \"auth-worker\" }],\n  \"ai\": { \"binding\": \"AI\" }\n}\n```\n\n## Binding-Specific Configuration\n\n### Durable Objects with Class Export\n\n```jsonc\n{\n  \"durable_objects\": {\n    \"bindings\": [\n      { \"name\": \"COUNTER\", \"class_name\": \"Counter\", \"script_name\": \"my-worker\" }\n    ]\n  }\n}\n```\n\n```typescript\n// In same Worker or script_name Worker\nexport class Counter {\n  constructor(private state: DurableObjectState, private env: Env) {}\n  async fetch(request: Request) { /* ... */ }\n}\n```\n\n### Queue Consumers\n\n```jsonc\n{\n  \"queues\": {\n    \"producers\": [{ \"binding\": \"MY_QUEUE\", \"queue\": \"my-queue\" }],\n    \"consumers\": [{ \"queue\": \"my-queue\", \"max_batch_size\": 10 }]\n  }\n}\n```\n\nQueue consumer handler: `export default { async queue(batch, env) { /* process batch.messages */ } }`\n\n## Key Points\n\n- **64 binding limit** (all types combined)\n- **Secrets**: Always use `wrangler secret put`, never commit\n- **Types**: Run `npx wrangler types` after config changes\n- **Environments**: Use `env` field for staging/production variants\n- **Development**: Use `preview_id` or `--remote` flag\n- **IDs vs Names**: Some bindings use `id` (KV, D1), others use `name` (R2, Queues)\n\n## See Also\n\n- [Wrangler Configuration](https://developers.cloudflare.com/workers/wrangler/configuration/)"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/bindings/gotchas.md",
    "content": "# Binding Gotchas and Troubleshooting\n\n## Critical: Global Scope Mutation\n\n### ❌ THE #1 GOTCHA: Caching env in Global Scope\n\n```typescript\n// ❌ DANGEROUS - env cached at deploy time\nconst apiKey = env.API_KEY;  // ERROR: env not available in global scope\n\nexport default {\n  async fetch(request: Request, env: Env) {\n    // Uses undefined or stale value!\n  }\n}\n```\n\n**Why it breaks:**\n- `env` not available in global scope\n- If using workarounds, secrets may not update without redeployment\n- Leads to \"Cannot read property 'X' of undefined\" errors\n\n**✅ Always access env per-request:**\n```typescript\nexport default {\n  async fetch(request: Request, env: Env) {\n    const apiKey = env.API_KEY;  // Fresh every request\n  }\n}\n```\n\n## Common Errors\n\n### \"env.MY_KV is undefined\"\n\n**Cause:** Name mismatch or not configured  \n**Solution:** Check wrangler.jsonc (case-sensitive), run `npx wrangler types`, verify `npx wrangler kv namespace list`\n\n### \"Property 'MY_KV' does not exist on type 'Env'\"\n\n**Cause:** Types not generated  \n**Solution:** `npx wrangler types`\n\n### \"preview_id is required for --remote\"\n\n**Cause:** Missing preview binding  \n**Solution:** Add `\"preview_id\": \"dev-id\"` or use `npx wrangler dev` (local mode)\n\n### \"Secret updated but Worker still uses old value\"\n\n**Cause:** Cached in global scope or not redeployed  \n**Solution:** Avoid global caching, redeploy after secret change\n\n### \"KV get() returns null for existing key\"\n\n**Cause:** Eventual consistency (60s), wrong namespace, wrong environment  \n**Solution:**\n```bash\n# Check key exists\nnpx wrangler kv key get --binding=MY_KV \"your-key\"\n\n# Verify namespace ID\nnpx wrangler kv namespace list\n\n# Check environment\nnpx wrangler deployments list\n```\n\n### \"D1 database not found\"\n\n**Solution:** `npx wrangler d1 list`, verify ID in wrangler.jsonc\n\n### \"Service binding returns 'No such service'\"\n\n**Cause:** Target Worker not deployed, name mismatch, environment mismatch  \n**Solution:**\n```bash\n# List deployed Workers\nnpx wrangler deployments list --name=target-worker\n\n# Check service binding config\ncat wrangler.jsonc | grep -A2 services\n\n# Deploy target first\ncd ../target-worker && npx wrangler deploy\n```\n\n### \"Rate limit exceeded\" on KV writes\n\n**Cause:** >1 write/second per key  \n**Solution:** Use different keys, Durable Objects, or Queues\n\n## Type Safety Gotchas\n\n### Missing @cloudflare/workers-types\n\n**Error:** `Cannot find name 'Request'`  \n**Solution:** `npm install -D @cloudflare/workers-types`, add to tsconfig.json `\"types\"`\n\n### Binding Type Mismatches\n\n```typescript\n// ❌ Wrong - KV returns string | null\nconst value: string = await env.MY_KV.get('key');\n\n// ✅ Handle null\nconst value = await env.MY_KV.get('key');\nif (!value) return new Response('Not found', { status: 404 });\n```\n\n## Environment Gotchas\n\n### Wrong Environment Deployed\n\n**Solution:** Check `npx wrangler deployments list`, use `--env` flag\n\n### Secrets Not Per-Environment\n\n**Solution:** Set per environment: `npx wrangler secret put API_KEY --env staging`\n\n## Development Gotchas\n\n**wrangler dev vs deploy:**\n- dev: Uses `preview_id` or local bindings, secrets not available\n- deploy: Uses production `id`, secrets available\n\n**Access secrets in dev:** `npx wrangler dev --remote`  \n**Persist local data:** `npx wrangler dev --persist`\n\n## Performance Gotchas\n\n### Sequential Binding Calls\n\n```typescript\n// ❌ Slow\nconst user = await env.DB.prepare('...').first();\nconst config = await env.MY_KV.get('config');\n\n// ✅ Parallel\nconst [user, config] = await Promise.all([\n  env.DB.prepare('...').first(),\n  env.MY_KV.get('config')\n]);\n```\n\n## Security Gotchas\n\n**❌ Secrets in logs:** `console.log('Key:', env.API_KEY)` - visible in dashboard  \n**✅** `console.log('Key:', env.API_KEY ? '***' : 'missing')`\n\n**❌ Exposing env:** `return Response.json(env)` - exposes all bindings  \n**✅** Never return env object in responses\n\n## Limits Reference\n\n| Resource | Limit | Impact | Plan |\n|----------|-------|--------|------|\n| **Bindings per Worker** | 64 total | All binding types combined | All |\n| **Environment variables** | 64 max, 5KB each | Per Worker | All |\n| **Secret size** | 1KB | Per secret | All |\n| **KV key size** | 512 bytes | UTF-8 encoded | All |\n| **KV value size** | 25 MB | Per value | All |\n| **KV writes per key** | 1/second | Per key; exceeding = 429 error | All |\n| **KV list() results** | 1000 keys | Per call; use cursor for more | All |\n| **KV operations** | 1000 reads/day | Free tier only | Free |\n| **R2 object size** | 5 TB | Per object | All |\n| **R2 operations** | 1M Class A/month free | Writes | All |\n| **D1 database size** | 10 GB | Per database | All |\n| **D1 rows per query** | 100,000 | Result set limit | All |\n| **D1 databases** | 10 | Free tier | Free |\n| **Queue batch size** | 100 messages | Per consumer batch | All |\n| **Queue message size** | 128 KB | Per message | All |\n| **Service binding calls** | Unlimited | Counts toward CPU time | All |\n| **Durable Objects** | 1M requests/month free | First 1M | Free |\n\n## Debugging Tips\n\n```bash\n# Check configuration\nnpx wrangler deploy --dry-run       # Validate config without deploying\nnpx wrangler kv namespace list      # List KV namespaces\nnpx wrangler secret list            # List secrets (not values)\nnpx wrangler deployments list       # Recent deployments\n\n# Inspect bindings\nnpx wrangler kv key list --binding=MY_KV\nnpx wrangler kv key get --binding=MY_KV \"key-name\"\nnpx wrangler r2 object get my-bucket/file.txt\nnpx wrangler d1 execute my-db --command=\"SELECT * FROM sqlite_master\"\n\n# Test locally\nnpx wrangler dev                  # Local mode\nnpx wrangler dev --remote         # Production bindings\nnpx wrangler dev --persist        # Persist data across restarts\n\n# Verify types\nnpx wrangler types\ncat .wrangler/types/runtime.d.ts | grep \"interface Env\"\n\n# Debug specific binding issues\nnpx wrangler tail                 # Stream logs in real-time\nnpx wrangler tail --format=pretty # Formatted logs\n```\n\n## See Also\n\n- [Workers Limits](https://developers.cloudflare.com/workers/platform/limits/)\n- [Wrangler Commands](https://developers.cloudflare.com/workers/wrangler/commands/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/bindings/patterns.md",
    "content": "# Binding Patterns and Best Practices\n\n## Service Binding Patterns\n\n### RPC via Service Bindings\n\n```typescript\n// auth-worker\nexport default {\n  async fetch(request: Request, env: Env) {\n    const token = request.headers.get('Authorization');\n    return new Response(JSON.stringify({ valid: await validateToken(token) }));\n  }\n}\n\n// api-worker\nconst response = await env.AUTH_SERVICE.fetch(\n  new Request('https://fake-host/validate', {\n    headers: { 'Authorization': token }\n  })\n);\n```\n\n**Why RPC?** Zero latency (same datacenter), no DNS, free, type-safe.\n\n**HTTP vs Service:**\n```typescript\n// ❌ HTTP (slow, paid, cross-region latency)\nawait fetch('https://auth-worker.example.com/validate');\n\n// ✅ Service binding (fast, free, same isolate)\nawait env.AUTH_SERVICE.fetch(new Request('https://fake-host/validate'));\n```\n\n**URL doesn't matter:** Service bindings ignore hostname/protocol, routing happens via binding name.\n\n### Typed Service RPC\n\n```typescript\n// shared-types.ts\nexport interface AuthRequest { token: string; }\nexport interface AuthResponse { valid: boolean; userId?: string; }\n\n// auth-worker\nexport default {\n  async fetch(request: Request): Promise<Response> {\n    const body: AuthRequest = await request.json();\n    const response: AuthResponse = { valid: true, userId: '123' };\n    return Response.json(response);\n  }\n}\n\n// api-worker\nconst response = await env.AUTH_SERVICE.fetch(\n  new Request('https://fake/validate', {\n    method: 'POST',\n    body: JSON.stringify({ token } satisfies AuthRequest)\n  })\n);\nconst data: AuthResponse = await response.json();\n```\n\n## Secrets Management\n\n```bash\n# Set secret\nnpx wrangler secret put API_KEY\ncat api-key.txt | npx wrangler secret put API_KEY\nnpx wrangler secret put API_KEY --env staging\n```\n\n```typescript\n// Use secret\nconst response = await fetch('https://api.example.com', {\n  headers: { 'Authorization': `Bearer ${env.API_KEY}` }\n});\n```\n\n**Never commit secrets:**\n```jsonc\n// ❌ NEVER\n{ \"vars\": { \"API_KEY\": \"sk_live_abc123\" } }\n```\n\n## Testing with Mock Bindings\n\n### Vitest Mock\n\n```typescript\nimport { vi } from 'vitest';\n\nconst mockKV: KVNamespace = {\n  get: vi.fn(async (key) => key === 'test' ? 'value' : null),\n  put: vi.fn(async () => {}),\n  delete: vi.fn(async () => {}),\n  list: vi.fn(async () => ({ keys: [], list_complete: true, cursor: '' })),\n  getWithMetadata: vi.fn(),\n} as unknown as KVNamespace;\n\nconst mockEnv: Env = { MY_KV: mockKV };\nconst mockCtx: ExecutionContext = {\n  waitUntil: vi.fn(),\n  passThroughOnException: vi.fn(),\n};\n\nconst response = await worker.fetch(\n  new Request('http://localhost/test'),\n  mockEnv,\n  mockCtx\n);\n```\n\n## Binding Access Patterns\n\n### Lazy Access\n\n```typescript\n// ✅ Access only when needed\nif (url.pathname === '/cached') {\n  const cached = await env.MY_KV.get('data');\n  if (cached) return new Response(cached);\n}\n```\n\n### Parallel Access\n\n```typescript\n// ✅ Parallelize independent calls\nconst [user, config, cache] = await Promise.all([\n  env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(),\n  env.MY_KV.get('config'),\n  env.CACHE.get('data')\n]);\n```\n\n## Storage Selection\n\n### KV: CDN-Backed Reads\n\n```typescript\nconst config = await env.MY_KV.get('app-config', { type: 'json' });\n```\n\n**Use when:** Read-heavy, <25MB, global distribution, eventual consistency OK  \n**Latency:** <10ms reads (cached), writes eventually consistent (60s)\n\n### D1: Relational Queries\n\n```typescript\nconst results = await env.DB.prepare(`\n  SELECT u.name, COUNT(o.id) FROM users u\n  LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id\n`).all();\n```\n\n**Use when:** Relational data, JOINs, ACID transactions  \n**Limits:** 10GB database size, 100k rows per query\n\n### R2: Large Objects\n\n```typescript\nconst object = await env.MY_BUCKET.get('large-file.zip');\nreturn new Response(object.body);\n```\n\n**Use when:** Files >25MB, S3-compatible API needed  \n**Limits:** 5TB per object, unlimited storage\n\n### Durable Objects: Coordination\n\n```typescript\nconst id = env.COUNTER.idFromName('global');\nconst stub = env.COUNTER.get(id);\nawait stub.fetch(new Request('https://fake/increment'));\n```\n\n**Use when:** Strong consistency, real-time coordination, WebSocket state  \n**Guarantees:** Single-threaded execution, transactional storage\n\n## Anti-Patterns\n\n**❌ Hardcoding credentials:** `const apiKey = 'sk_live_abc123'`  \n**✅** `npx wrangler secret put API_KEY`\n\n**❌ Using REST API:** `fetch('https://api.cloudflare.com/.../kv/...')`  \n**✅** `env.MY_KV.get('key')`\n\n**❌ Polling storage:** `setInterval(() => env.KV.get('config'), 1000)`  \n**✅** Use Durable Objects for real-time state\n\n**❌ Large data in vars:** `{ \"vars\": { \"HUGE_CONFIG\": \"...\" } }` (5KB max)  \n**✅** `env.MY_KV.put('config', data)`\n\n**❌ Caching env globally:** `const apiKey = env.API_KEY` outside fetch()  \n**✅** Access `env.API_KEY` per-request inside fetch()\n\n## See Also\n\n- [Service Bindings Docs](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/)\n- [Miniflare Testing](https://miniflare.dev/)"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/bot-management/README.md",
    "content": "# Cloudflare Bot Management\n\nEnterprise-grade bot detection, protection, and mitigation using ML/heuristics, bot scores, JavaScript detections, and verified bot handling.\n\n## Overview\n\nBot Management provides multi-tier protection:\n- **Free (Bot Fight Mode)**: Auto-blocks definite bots, no config\n- **Pro/Business (Super Bot Fight Mode)**: Configurable actions, static resource protection, analytics groupings\n- **Enterprise (Bot Management)**: Granular 1-99 scores, WAF integration, JA3/JA4 fingerprinting, Workers API, Advanced Analytics\n\n## Quick Start\n\n```txt\n# Dashboard: Security > Bots\n# Enterprise: Deploy rule template\n(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot) → Block\n(cf.bot_management.score le 29 and not cf.bot_management.verified_bot) → Managed Challenge\n```\n\n## What Do You Need?\n\n```txt\n├─ Initial setup → configuration.md\n│   ├─ Free tier → \"Bot Fight Mode\"\n│   ├─ Pro/Business → \"Super Bot Fight Mode\"\n│   └─ Enterprise → \"Bot Management for Enterprise\"\n├─ Workers API integration → api.md\n├─ WAF rules → patterns.md\n├─ Debugging → gotchas.md\n└─ Analytics → api.md#bot-analytics\n```\n\n## Reading Order\n\n| Task | Files to Read |\n|------|---------------|\n| Enable bot protection | README → configuration.md |\n| Workers bot detection | README → api.md |\n| WAF rule templates | README → patterns.md |\n| Debug bot issues | gotchas.md |\n| Advanced analytics | api.md#bot-analytics |\n\n## Core Concepts\n\n**Bot Scores**: 1-99 (1 = definitely automated, 99 = definitely human). Threshold: <30 indicates bot traffic. Enterprise gets granular 1-99; Pro/Business get groupings only.\n\n**Detection Engines**: Heuristics (known fingerprints, assigns score=1), ML (majority of detections, supervised learning on billions of requests), Anomaly Detection (optional, baseline traffic analysis), JavaScript Detections (headless browser detection).\n\n**Verified Bots**: Allowlisted good bots (search engines, AI crawlers) verified via reverse DNS or Web Bot Auth. Access via `cf.bot_management.verified_bot` or `cf.verified_bot_category`.\n\n## Platform Limits\n\n| Plan | Bot Scores | JA3/JA4 | Custom Rules | Analytics Retention |\n|------|------------|---------|--------------|---------------------|\n| Free | No (auto-block only) | No | 5 | N/A (no analytics) |\n| Pro/Business | Groupings only | No | 20/100 | 30 days (72h at a time) |\n| Enterprise | 1-99 granular | Yes | 1,000+ | 30 days (1 week at a time) |\n\n## Basic Patterns\n\n```typescript\n// Workers: Check bot score\nexport default {\n  async fetch(request: Request): Promise<Response> {\n    const botScore = request.cf?.botManagement?.score;\n    if (botScore && botScore < 30 && !request.cf?.botManagement?.verifiedBot) {\n      return new Response('Bot detected', { status: 403 });\n    }\n    return fetch(request);\n  }\n};\n```\n\n```txt\n# WAF: Block definite bots\n(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot)\n\n# WAF: Protect sensitive endpoints\n(cf.bot_management.score lt 50 and http.request.uri.path in {\"/login\" \"/checkout\"} and not cf.bot_management.verified_bot)\n```\n\n## In This Reference\n\n- [configuration.md](./configuration.md) - Product tiers, WAF rule setup, JavaScript Detections, ML auto-updates\n- [api.md](./api.md) - Workers BotManagement interface, WAF fields, JA4 Signals\n- [patterns.md](./patterns.md) - E-commerce, API protection, mobile app allowlisting, SEO-friendly handling\n- [gotchas.md](./gotchas.md) - False positives/negatives, score=0 issues, JSD limitations, CSP requirements\n\n## See Also\n\n- [waf](../waf/) - WAF custom rules for bot enforcement\n- [workers](../workers/) - Workers request.cf.botManagement API\n- [api-shield](../api-shield/) - API-specific bot protection\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/bot-management/api.md",
    "content": "# Bot Management API\n\n## Workers: BotManagement Interface\n\n```typescript\ninterface BotManagement {\n  score: number;              // 1-99 (Enterprise), 0 if not computed\n  verifiedBot: boolean;       // Is verified bot\n  staticResource: boolean;    // Serves static resource\n  ja3Hash: string;            // JA3 fingerprint (Enterprise, HTTPS only)\n  ja4: string;                // JA4 fingerprint (Enterprise, HTTPS only)\n  jsDetection?: {\n    passed: boolean;          // Passed JS detection (if enabled)\n  };\n  detectionIds: number[];     // Heuristic detection IDs\n  corporateProxy?: boolean;   // From corporate proxy (Enterprise)\n}\n\n// DEPRECATED: Use botManagement.score instead\n// request.cf.clientTrustScore (legacy, duplicate of botManagement.score)\n\n// Access via request.cf\nimport type { IncomingRequestCfProperties } from '@cloudflare/workers-types';\n\nexport default {\n  async fetch(request: Request): Promise<Response> {\n    const cf = request.cf as IncomingRequestCfProperties | undefined;\n    const botMgmt = cf?.botManagement;\n    \n    if (!botMgmt) return fetch(request);\n    if (botMgmt.verifiedBot) return fetch(request); // Allow verified bots\n    if (botMgmt.score === 1) return new Response('Blocked', { status: 403 });\n    if (botMgmt.score < 30) return new Response('Challenge required', { status: 429 });\n    \n    return fetch(request);\n  }\n};\n```\n\n## WAF Fields Reference\n\n```txt\n# Score fields\ncf.bot_management.score                    # 0-99 (0 = not computed)\ncf.bot_management.verified_bot             # boolean\ncf.bot_management.static_resource          # boolean\ncf.bot_management.ja3_hash                 # string (Enterprise)\ncf.bot_management.ja4                      # string (Enterprise)\ncf.bot_management.detection_ids            # array\ncf.bot_management.js_detection.passed      # boolean\ncf.bot_management.corporate_proxy          # boolean (Enterprise)\ncf.verified_bot_category                   # string\n\n# Workers equivalent\nrequest.cf.botManagement.score\nrequest.cf.botManagement.verifiedBot\nrequest.cf.botManagement.ja3Hash\nrequest.cf.botManagement.ja4\nrequest.cf.botManagement.jsDetection.passed\nrequest.cf.verifiedBotCategory\n```\n\n## JA4 Signals (Enterprise)\n\n```typescript\nimport type { IncomingRequestCfProperties } from '@cloudflare/workers-types';\n\ninterface JA4Signals {\n  // Ratios (0.0-1.0)\n  heuristic_ratio_1h?: number;  // Fraction flagged by heuristics\n  browser_ratio_1h?: number;    // Fraction from real browsers  \n  cache_ratio_1h?: number;      // Fraction hitting cache\n  h2h3_ratio_1h?: number;       // Fraction using HTTP/2 or HTTP/3\n  // Ranks (relative position in distribution)\n  uas_rank_1h?: number;         // User-Agent diversity rank\n  paths_rank_1h?: number;       // Path diversity rank\n  reqs_rank_1h?: number;        // Request volume rank\n  ips_rank_1h?: number;         // IP diversity rank\n  // Quantiles (0.0-1.0, percentile in distribution)\n  reqs_quantile_1h?: number;    // Request volume quantile\n  ips_quantile_1h?: number;     // IP count quantile\n}\n\nexport default {\n  async fetch(request: Request): Promise<Response> {\n    const cf = request.cf as IncomingRequestCfProperties | undefined;\n    const ja4Signals = cf?.ja4Signals as JA4Signals | undefined;\n    \n    if (!ja4Signals) return fetch(request); // Not available for HTTP or Worker routing\n    \n    // Check for anomalous behavior\n    // High heuristic_ratio or low browser_ratio = suspicious\n    const heuristicRatio = ja4Signals.heuristic_ratio_1h ?? 0;\n    const browserRatio = ja4Signals.browser_ratio_1h ?? 0;\n    \n    if (heuristicRatio > 0.5 || browserRatio < 0.3) {\n      return new Response('Suspicious traffic', { status: 403 });\n    }\n    \n    return fetch(request);\n  }\n};\n```\n\n## Common Patterns\n\nSee [patterns.md](./patterns.md) for Workers examples: mobile app allowlisting, corporate proxy exemption, datacenter detection, conditional delay, and more.\n\n## Bot Analytics\n\n### Access Locations\n- Dashboard: Security > Bots (old) or Security > Analytics > Bot analysis (new)\n- GraphQL API for programmatic access\n- Security Events & Security Analytics\n- Logpush/Logpull\n\n### Available Data\n- **Enterprise BM**: Bot scores (1-99), bot score source, distribution\n- **Pro/Business**: Bot groupings (automated, likely automated, likely human)\n- Top attributes: IPs, paths, user agents, countries\n- Detection sources: Heuristics, ML, AD, JSD\n- Verified bot categories\n\n### Time Ranges\n- **Enterprise BM**: Up to 1 week at a time, 30 days history\n- **Pro/Business**: Up to 72 hours at a time, 30 days history\n- Real-time in most cases, adaptive sampling (1-10% depending on volume)\n\n## Logpush Fields\n\n```txt\nBotScore              # 1-99 or 0 if not computed\nBotScoreSrc           # Detection engine (ML, Heuristics, etc.)\nBotTags               # Classification tags\nBotDetectionIDs       # Heuristic detection IDs\n```\n\n**BotScoreSrc values:**\n- `\"Heuristics\"` - Known fingerprint\n- `\"Machine Learning\"` - ML model\n- `\"Anomaly Detection\"` - Baseline anomaly\n- `\"JS Detection\"` - JavaScript check\n- `\"Cloudflare Service\"` - Zero Trust\n- `\"Not Computed\"` - Score = 0\n\nAccess via Logpush (stream to cloud storage/SIEM), Logpull (API to fetch logs), or GraphQL API (query analytics data).\n\n## Testing with Miniflare\n\nMiniflare provides mock botManagement data for local development:\n\n**Default values:**\n- `score: 99` (human)\n- `verifiedBot: false`\n- `corporateProxy: false`\n- `ja3Hash: \"25b4882c2bcb50cd6b469ff28c596742\"`\n- `staticResource: false`\n- `detectionIds: []`\n\n**Override in tests:**\n```typescript\nimport { getPlatformProxy } from 'wrangler';\n\nconst { cf, dispose } = await getPlatformProxy();\n// cf.botManagement is frozen mock object\nexpect(cf.botManagement.score).toBe(99);\n```\n\nFor custom test data, mock request.cf in your test setup.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/bot-management/configuration.md",
    "content": "# Bot Management Configuration\n\n## Product Tiers\n\n**Note:** Dashboard paths differ between old and new UI:\n- **New:** Security > Settings > Filter \"Bot traffic\"\n- **Old:** Security > Bots\n\nBoth UIs access same settings.\n\n### Bot Score Groupings (Pro/Business)\n\nPro/Business users see bot score groupings instead of granular 1-99 scores:\n\n| Score | Grouping | Meaning |\n|-------|----------|---------|\n| 0 | Not computed | Bot Management didn't run |\n| 1 | Automated | Definite bot (heuristic match) |\n| 2-29 | Likely automated | Probably bot (ML detection) |\n| 30-99 | Likely human | Probably human |\n| N/A | Verified bot | Allowlisted good bot |\n\nEnterprise plans get granular 1-99 scores for custom thresholds.\n\n### Bot Fight Mode (Free)\n- Auto-blocks definite bots (score=1), excludes verified bots by default\n- JavaScript Detections always enabled, no configuration options\n\n### Super Bot Fight Mode (Pro/Business)\n```txt\nDashboard: Security > Bots > Configure\n- Definitely automated: Block/Challenge\n- Likely automated: Challenge/Allow  \n- Verified bots: Allow (recommended)\n- Static resource protection: ON (may block mail clients)\n- JavaScript Detections: Optional\n```\n\n### Bot Management for Enterprise\n```txt\nDashboard: Security > Bots > Configure > Auto-updates: ON (recommended)\n\n# Template 1: Block definite bots\n(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot and not cf.bot_management.static_resource)\nAction: Block\n\n# Template 2: Challenge likely bots\n(cf.bot_management.score ge 2 and cf.bot_management.score le 29 and not cf.bot_management.verified_bot and not cf.bot_management.static_resource)\nAction: Managed Challenge\n```\n\n## JavaScript Detections Setup\n\n### Enable via Dashboard\n```txt\nSecurity > Bots > Configure Bot Management > JS Detections: ON\n\nUpdate CSP: script-src 'self' /cdn-cgi/challenge-platform/;\n```\n\n### Manual JS Injection (API)\n```html\n<script>\nfunction jsdOnload() {\n  window.cloudflare.jsd.executeOnce({ callback: function(result) { console.log('JSD:', result); } });\n}\n</script>\n<script src=\"/cdn-cgi/challenge-platform/scripts/jsd/api.js?onload=jsdOnload\" async></script>\n```\n\n**Use API for**: Selective deployment on specific pages  \n**Don't combine**: Zone-wide toggle + manual injection\n\n### WAF Rules for JSD\n```txt\n# NEVER use on first page visit (needs HTML page first)\n(not cf.bot_management.js_detection.passed and http.request.uri.path eq \"/api/user/create\" and http.request.method eq \"POST\" and not cf.bot_management.verified_bot)\nAction: Managed Challenge (always use Managed Challenge, not Block)\n```\n\n### Limitations\n- First request won't have JSD data (needs HTML page first)\n- Strips ETags from HTML responses\n- Not supported with CSP via `<meta>` tags\n- Websocket endpoints not supported\n- Native mobile apps won't pass\n- cf_clearance cookie: 15-minute lifespan, max 4096 bytes\n\n## __cf_bm Cookie\n\nCloudflare sets `__cf_bm` cookie to smooth bot scores across user sessions:\n\n- **Purpose:** Reduces false positives from score volatility\n- **Scope:** Per-domain, HTTP-only\n- **Lifespan:** Session duration\n- **Privacy:** No PII—only session classification\n- **Automatic:** No configuration required\n\nBot scores for repeat visitors consider session history via this cookie.\n\n## Static Resource Protection\n\n**File Extensions**: ico, jpg, png, jpeg, gif, css, js, tif, tiff, bmp, pict, webp, svg, svgz, class, jar, txt, csv, doc, docx, xls, xlsx, pdf, ps, pls, ppt, pptx, ttf, otf, woff, woff2, eot, eps, ejs, swf, torrent, midi, mid, m3u8, m4a, mp3, ogg, ts  \n**Plus**: `/.well-known/` path (all files)\n\n```txt\n# Exclude static resources from bot rules\n(cf.bot_management.score lt 30 and not cf.bot_management.static_resource)\n```\n\n**WARNING**: May block mail clients fetching static images\n\n## JA3/JA4 Fingerprinting (Enterprise)\n\n```txt\n# Block specific attack fingerprint\n(cf.bot_management.ja3_hash eq \"8b8e3d5e3e8b3d5e\")\n\n# Allow mobile app by fingerprint\n(cf.bot_management.ja4 eq \"your_mobile_app_fingerprint\")\n```\n\nOnly available for HTTPS/TLS traffic. Missing for Worker-routed traffic or HTTP requests.\n\n## Verified Bot Categories\n\n```txt\n# Allow search engines only\n(cf.verified_bot_category eq \"Search Engine Crawler\")\n\n# Block AI crawlers\n(cf.verified_bot_category eq \"AI Crawler\")\nAction: Block\n\n# Or use dashboard: Security > Settings > Bot Management > Block AI Bots\n```\n\n| Category | String Value | Example |\n|----------|--------------|---------|\n| AI Crawler | `AI Crawler` | GPTBot, Claude-Web |\n| AI Assistant | `AI Assistant` | Perplexity-User, DuckAssistBot |\n| AI Search | `AI Search` | OAI-SearchBot |\n| Accessibility | `Accessibility` | Accessible Web Bot |\n| Academic Research | `Academic Research` | Library of Congress |\n| Advertising & Marketing | `Advertising & Marketing` | Google Adsbot |\n| Aggregator | `Aggregator` | Pinterest, Indeed |\n| Archiver | `Archiver` | Internet Archive, CommonCrawl |\n| Feed Fetcher | `Feed Fetcher` | RSS/Podcast updaters |\n| Monitoring & Analytics | `Monitoring & Analytics` | Uptime monitors |\n| Page Preview | `Page Preview` | Facebook/Slack link preview |\n| SEO | `Search Engine Optimization` | Google Lighthouse |\n| Security | `Security` | Vulnerability scanners |\n| Social Media Marketing | `Social Media Marketing` | Brandwatch |\n| Webhooks | `Webhooks` | Payment processors |\n| Other | `Other` | Uncategorized bots |\n\n## Best Practices\n\n- **ML Auto-Updates**: Enable on Enterprise for latest models\n- **Start with Managed Challenge**: Test before blocking\n- **Always exclude verified bots**: Use `not cf.bot_management.verified_bot`\n- **Exempt corporate proxies**: For B2B traffic via `cf.bot_management.corporate_proxy`\n- **Use static resource exception**: Improves performance, reduces overhead\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/bot-management/gotchas.md",
    "content": "# Bot Management Gotchas\n\n## Common Errors\n\n### \"Bot Score = 0\"\n\n**Cause:** Bot Management didn't run (internal Cloudflare request, Worker routing to zone (Orange-to-Orange), or request handled before BM (Redirect Rules, etc.))  \n**Solution:** Check request flow and ensure Bot Management runs in request lifecycle\n\n### \"JavaScript Detections Not Working\"\n\n**Cause:** `js_detection.passed` always false or undefined due to: CSP headers don't allow `/cdn-cgi/challenge-platform/`, using on first page visit (needs HTML page first), ad blockers or disabled JS, JSD not enabled in dashboard, or using Block action (must use Managed Challenge)  \n**Solution:** Add CSP header `Content-Security-Policy: script-src 'self' /cdn-cgi/challenge-platform/;` and ensure JSD is enabled with Managed Challenge action\n\n### \"False Positives (Legitimate Users Blocked)\"\n\n**Cause:** Bot detection incorrectly flagging legitimate users  \n**Solution:** Check Bot Analytics for affected IPs/paths, identify detection source (ML, Heuristics, etc.), create exception rule like `(cf.bot_management.score lt 30 and http.request.uri.path eq \"/problematic-path\")` with Action: Skip (Bot Management), or allowlist by IP/ASN/country\n\n### \"False Negatives (Bots Not Caught)\"\n\n**Cause:** Bots bypassing detection  \n**Solution:** Lower score threshold (30 → 50), enable JavaScript Detections, add JA3/JA4 fingerprinting rules, or use rate limiting as fallback\n\n### \"Verified Bot Blocked\"\n\n**Cause:** Search engine bot blocked by WAF Managed Rules (not just Bot Management)  \n**Solution:** Create WAF exception for specific rule ID and verify bot via reverse DNS\n\n### \"Yandex Bot Blocked During IP Update\"\n\n**Cause:** Yandex updates bot IPs; new IPs unrecognized for 48h during propagation  \n**Solution:** \n1. Check Security Events for specific WAF rule ID blocking Yandex\n2. Create WAF exception:\n   ```txt\n   (http.user_agent contains \"YandexBot\" and ip.src in {<yandex-ip-range>})\n   Action: Skip (WAF Managed Ruleset)\n   ```\n3. Monitor Bot Analytics for 48h\n4. Remove exception after propagation completes\n\nIssue resolves automatically after 48h. Contact Cloudflare Support if persists.\n\n### \"JA3/JA4 Missing\"\n\n**Cause:** Non-HTTPS traffic, Worker routing traffic, Orange-to-Orange traffic via Worker, or Bot Management skipped  \n**Solution:** JA3/JA4 only available for HTTPS/TLS traffic; check request routing\n\n**JA3/JA4 Not User-Unique:** Same browser/library version = same fingerprint\n- Don't use for user identification\n- Use for client profiling only\n- Fingerprints change with browser updates\n\n## Bot Verification Methods\n\nCloudflare verifies bots via:\n\n1. **Reverse DNS (IP validation):** Traditional method—bot IP resolves to expected domain\n2. **Web Bot Auth:** Modern cryptographic verification—faster propagation\n\nWhen `verifiedBot=true`, bot passed at least one method.\n\n**Inactive verified bots:** IPs removed after 24h of no traffic.\n\n## Detection Engine Behavior\n\n| Engine | Score | Timing | Plan | Notes |\n|--------|-------|--------|------|-------|\n| Heuristics | Always 1 | Immediate | All | Known fingerprints—overrides ML |\n| ML | 1-99 | Immediate | All | Majority of detections |\n| Anomaly Detection | Influences | After baseline | Enterprise | Optional, baseline analysis |\n| JavaScript Detections | Pass/fail | After JS | Pro+ | Headless browser detection |\n| Cloudflare Service | N/A | N/A | Enterprise | Zero Trust internal source |\n\n**Priority:** Heuristics > ML—if heuristic matches, score=1 regardless of ML.\n\n## Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Bot Score = 0 | Means not computed | Not score = 100 |\n| First request JSD data | May not be available | JSD data appears on subsequent requests |\n| Score accuracy | Not 100% guaranteed | False positives/negatives possible |\n| JSD on first HTML page visit | Not supported | Requires subsequent page load |\n| JSD requirements | JavaScript-enabled browser | Won't work with JS disabled or ad blockers |\n| JSD ETag stripping | Strips ETags from HTML responses | May affect caching behavior |\n| JSD CSP compatibility | Requires specific CSP | Not compatible with some CSP configurations |\n| JSD meta CSP tags | Not supported | Must use HTTP headers |\n| JSD WebSocket support | Not supported | WebSocket endpoints won't work with JSD |\n| JSD mobile app support | Native apps won't pass | Only works in browsers |\n| JA3/JA4 traffic type | HTTPS/TLS only | Not available for non-HTTPS traffic |\n| JA3/JA4 Worker routing | Missing for Worker-routed traffic | Check request routing |\n| JA3/JA4 uniqueness | Not unique per user | Shared by clients with same browser/library |\n| JA3/JA4 stability | Can change with updates | Browser/library updates affect fingerprints |\n| WAF custom rules (Free) | 5 | Varies by plan |\n| WAF custom rules (Pro) | 20 | Varies by plan |\n| WAF custom rules (Business) | 100 | Varies by plan |\n| WAF custom rules (Enterprise) | 1,000+ | Varies by plan |\n| Workers CPU time | Varies by plan | Applies to bot logic |\n| Bot Analytics sampling | 1-10% adaptive | High-volume zones sampled more aggressively |\n| Bot Analytics history | 30 days max | Historical data retention limit |\n| CSP requirements for JSD | Must allow `/cdn-cgi/challenge-platform/` | Required for JSD to function |\n\n### Plan Restrictions\n\n| Feature | Free | Pro/Business | Enterprise |\n|---------|------|--------------|------------|\n| Granular scores (1-99) | No | No | Yes |\n| JA3/JA4 | No | No | Yes |\n| Anomaly Detection | No | No | Yes |\n| Corporate Proxy detection | No | No | Yes |\n| Verified bot categories | Limited | Limited | Full |\n| Custom WAF rules | 5 | 20/100 | 1,000+ |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/bot-management/patterns.md",
    "content": "# Bot Management Patterns\n\n## E-commerce Protection\n\n```txt\n# High security for checkout\n(cf.bot_management.score lt 50 and http.request.uri.path in {\"/checkout\" \"/cart/add\"} and not cf.bot_management.verified_bot and not cf.bot_management.corporate_proxy)\nAction: Managed Challenge\n```\n\n## API Protection\n\n```txt\n# Protect API with JS detection + score\n(http.request.uri.path matches \"^/api/\" and (cf.bot_management.score lt 30 or not cf.bot_management.js_detection.passed) and not cf.bot_management.verified_bot)\nAction: Block\n```\n\n## SEO-Friendly Bot Handling\n\n```txt\n# Allow search engine crawlers\n(cf.bot_management.score lt 30 and not cf.verified_bot_category in {\"Search Engine Crawler\"})\nAction: Managed Challenge\n```\n\n## Block AI Scrapers\n\n```txt\n# Block training crawlers only (allow AI assistants/search)\n(cf.verified_bot_category eq \"AI Crawler\")\nAction: Block\n\n# Block all AI-related bots (training + assistants + search)\n(cf.verified_bot_category in {\"AI Crawler\" \"AI Assistant\" \"AI Search\"})\nAction: Block\n\n# Allow AI Search, block AI Crawler and AI Assistant\n(cf.verified_bot_category in {\"AI Crawler\" \"AI Assistant\"})\nAction: Block\n\n# Or use dashboard: Security > Settings > Bot Management > Block AI Bots\n```\n\n## Rate Limiting by Bot Score\n\n```txt\n# Stricter limits for suspicious traffic\n(cf.bot_management.score lt 50)\nRate: 10 requests per 10 seconds\n\n(cf.bot_management.score ge 50)\nRate: 100 requests per 10 seconds\n```\n\n## Mobile App Allowlisting\n\n```txt\n# Identify mobile app by JA3/JA4\n(cf.bot_management.ja4 in {\"fingerprint1\" \"fingerprint2\"})\nAction: Skip (all remaining rules)\n```\n\n## Datacenter Detection\n\n```typescript\nimport type { IncomingRequestCfProperties } from '@cloudflare/workers-types';\n\n// Low score + not corporate proxy = likely datacenter bot\nexport default {\n  async fetch(request: Request): Promise<Response> {\n    const cf = request.cf as IncomingRequestCfProperties | undefined;\n    const botMgmt = cf?.botManagement;\n    \n    if (botMgmt?.score && botMgmt.score < 30 && \n        !botMgmt.corporateProxy && !botMgmt.verifiedBot) {\n      return new Response('Datacenter traffic blocked', { status: 403 });\n    }\n    \n    return fetch(request);\n  }\n};\n```\n\n## Conditional Delay (Tarpit)\n\n```typescript\nimport type { IncomingRequestCfProperties } from '@cloudflare/workers-types';\n\n// Add delay proportional to bot suspicion\nexport default {\n  async fetch(request: Request): Promise<Response> {\n    const cf = request.cf as IncomingRequestCfProperties | undefined;\n    const botMgmt = cf?.botManagement;\n    \n    if (botMgmt?.score && botMgmt.score < 50 && !botMgmt.verifiedBot) {\n      // Delay: 0-2 seconds for scores 50-0\n      const delayMs = Math.max(0, (50 - botMgmt.score) * 40);\n      await new Promise(r => setTimeout(r, delayMs));\n    }\n    \n    return fetch(request);\n  }\n};\n```\n\n## Layered Defense\n\n```txt\n1. Bot Management (score-based)\n2. JavaScript Detections (for JS-capable clients)\n3. Rate Limiting (fallback protection)\n4. WAF Managed Rules (OWASP, etc.)\n```\n\n## Progressive Enhancement\n\n```txt\nPublic content: High threshold (score < 10)\nAuthenticated: Medium threshold (score < 30)\nSensitive: Low threshold (score < 50) + JSD\n```\n\n## Zero Trust for Bots\n\n```txt\n1. Default deny (all scores < 30)\n2. Allowlist verified bots\n3. Allowlist mobile apps (JA3/JA4)\n4. Allowlist corporate proxies\n5. Allowlist static resources\n```\n\n## Workers: Score + JS Detection\n\n```typescript\nimport type { IncomingRequestCfProperties } from '@cloudflare/workers-types';\n\nexport default {\n  async fetch(request: Request): Promise<Response> {\n    const cf = request.cf as IncomingRequestCfProperties | undefined;\n    const botMgmt = cf?.botManagement;\n    const url = new URL(request.url);\n    \n    if (botMgmt?.staticResource) return fetch(request); // Skip static\n    \n    // API endpoints: require JS detection + good score\n    if (url.pathname.startsWith('/api/')) {\n      const jsDetectionPassed = botMgmt?.jsDetection?.passed ?? false;\n      const score = botMgmt?.score ?? 100;\n      \n      if (!jsDetectionPassed || score < 30) {\n        return new Response('Unauthorized', { status: 401 });\n      }\n    }\n    \n    return fetch(request);\n  }\n};\n```\n\n## Rate Limiting by JWT Claim + Bot Score\n\n```txt\n# Enterprise: Combine bot score with JWT validation\nRate limiting > Custom rules\n- Field: lookup_json_string(http.request.jwt.claims[\"{config_id}\"][0], \"sub\")\n- Matches: user ID claim\n- Additional condition: cf.bot_management.score lt 50\n```\n\n## WAF Integration Points\n\n- **WAF Custom Rules**: Primary enforcement mechanism\n- **Rate Limiting Rules**: Bot score as dimension, stricter limits for low scores\n- **Transform Rules**: Pass score to origin via custom header\n- **Workers**: Programmatic bot logic, custom scoring algorithms\n- **Page Rules / Configuration Rules**: Zone-level overrides, path-specific settings\n\n## See Also\n\n- [gotchas.md](./gotchas.md) - Common errors, false positives/negatives, limitations\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/browser-rendering/README.md",
    "content": "# Cloudflare Browser Rendering Skill Reference\n\n**Description**: Expert knowledge for Cloudflare Browser Rendering - control headless Chrome on Cloudflare's global network for browser automation, screenshots, PDFs, web scraping, testing, and content generation.\n\n**When to use**: Any task involving Cloudflare Browser Rendering including: taking screenshots, generating PDFs, web scraping, browser automation, testing web applications, extracting structured data, capturing page metrics, or automating browser interactions.\n\n## Decision Tree\n\n### REST API vs Workers Bindings\n\n**Use REST API when:**\n- One-off, stateless tasks (screenshot, PDF, content fetch)\n- No Workers infrastructure yet\n- Simple integrations from external services\n- Need quick prototyping without deployment\n\n**Use Workers Bindings when:**\n- Complex browser automation workflows\n- Need session reuse for performance\n- Multiple page interactions per request\n- Custom scripting and logic required\n- Building production applications\n\n### Puppeteer vs Playwright\n\n| Feature | Puppeteer | Playwright |\n|---------|-----------|------------|\n| API Style | Chrome DevTools Protocol | High-level abstractions |\n| Selectors | CSS, XPath | CSS, text, role, test-id |\n| Best for | Advanced control, CDP access | Quick automation, testing |\n| Learning curve | Steeper | Gentler |\n\n**Use Puppeteer:** Need CDP protocol access, Chrome-specific features, migration from existing Puppeteer code\n**Use Playwright:** Modern selector APIs, cross-browser patterns, faster development\n\n## Tier Limits Summary\n\n| Limit | Free Tier | Paid Tier |\n|-------|-----------|-----------|\n| Daily browser time | 10 minutes | Unlimited* |\n| Concurrent sessions | 3 | 30 |\n| Requests per minute | 6 | 180 |\n\n*Subject to fair-use policy. See [gotchas.md](gotchas.md) for details.\n\n## Reading Order\n\n**New to Browser Rendering:**\n1. [configuration.md](configuration.md) - Setup and deployment\n2. [patterns.md](patterns.md) - Common use cases with examples\n3. [api.md](api.md) - API reference\n4. [gotchas.md](gotchas.md) - Avoid common pitfalls\n\n**Specific task:**\n- **Setup/deployment** → [configuration.md](configuration.md)\n- **API reference/endpoints** → [api.md](api.md)\n- **Example code/patterns** → [patterns.md](patterns.md)\n- **Debugging/troubleshooting** → [gotchas.md](gotchas.md)\n\n**REST API users:**\n- Start with [api.md](api.md) REST API section\n- Check [gotchas.md](gotchas.md) for rate limits\n\n**Workers users:**\n- Start with [configuration.md](configuration.md)\n- Review [patterns.md](patterns.md) for session management\n- Reference [api.md](api.md) for Workers Bindings\n\n## In This Reference\n\n- **[configuration.md](configuration.md)** - Setup, deployment, wrangler config, compatibility\n- **[api.md](api.md)** - REST API endpoints + Workers Bindings (Puppeteer/Playwright)\n- **[patterns.md](patterns.md)** - Common patterns, use cases, real examples\n- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, tier limits, common errors\n\n## See Also\n\n- [Cloudflare Docs](https://developers.cloudflare.com/browser-rendering/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/browser-rendering/api.md",
    "content": "# Browser Rendering API\n\n## REST API\n\n**Base:** `https://api.cloudflare.com/client/v4/accounts/{accountId}/browser-rendering`  \n**Auth:** `Authorization: Bearer <token>` (Browser Rendering - Edit permission)\n\n### Endpoints\n\n| Endpoint | Description | Key Options |\n|----------|-------------|-------------|\n| `/content` | Get rendered HTML | `url`, `waitUntil` |\n| `/screenshot` | Capture image | `screenshotOptions: {type, fullPage, clip}` |\n| `/pdf` | Generate PDF | `pdfOptions: {format, landscape, margin}` |\n| `/snapshot` | HTML + inlined resources | `url` |\n| `/scrape` | Extract by selectors | `selectors: [\"h1\", \".price\"]` |\n| `/json` | AI-structured extraction | `schema: {name: \"string\", price: \"number\"}` |\n| `/links` | Get all links | `url` |\n| `/markdown` | Convert to markdown | `url` |\n\n```bash\ncurl -X POST '.../browser-rendering/screenshot' \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -d '{\"url\":\"https://example.com\",\"screenshotOptions\":{\"fullPage\":true}}'\n```\n\n## Workers Binding\n\n```jsonc\n// wrangler.jsonc\n{ \"browser\": { \"binding\": \"MYBROWSER\" } }\n```\n\n## Puppeteer\n\n```typescript\nimport puppeteer from \"@cloudflare/puppeteer\";\n\nconst browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 600000 });\nconst page = await browser.newPage();\nawait page.goto('https://example.com', { waitUntil: 'networkidle0' });\n\n// Content\nconst html = await page.content();\nconst title = await page.title();\n\n// Screenshot/PDF\nawait page.screenshot({ fullPage: true, type: 'png' });\nawait page.pdf({ format: 'A4', printBackground: true });\n\n// Interaction\nawait page.click('#button');\nawait page.type('#input', 'text');\nawait page.evaluate(() => document.querySelector('h1')?.textContent);\n\n// Session management\nconst sessions = await puppeteer.sessions(env.MYBROWSER);\nconst limits = await puppeteer.limits(env.MYBROWSER);\n\nawait browser.close();\n```\n\n## Playwright\n\n```typescript\nimport { launch, connect } from \"@cloudflare/playwright\";\n\nconst browser = await launch(env.MYBROWSER, { keep_alive: 600000 });\nconst page = await browser.newPage();\n\nawait page.goto('https://example.com', { waitUntil: 'networkidle' });\n\n// Modern selectors\nawait page.locator('.button').click();\nawait page.getByText('Submit').click();\nawait page.getByTestId('search').fill('query');\n\n// Context for isolation\nconst context = await browser.newContext({\n  viewport: { width: 1920, height: 1080 },\n  userAgent: 'custom'\n});\n\nawait browser.close();\n```\n\n## Session Management\n\n```typescript\n// List sessions\nawait puppeteer.sessions(env.MYBROWSER);\n\n// Connect to existing\nawait puppeteer.connect(env.MYBROWSER, sessionId);\n\n// Check limits\nawait puppeteer.limits(env.MYBROWSER);\n// { remaining: ms, total: ms, concurrent: n }\n```\n\n## Key Options\n\n| Option | Values |\n|--------|--------|\n| `waitUntil` | `load`, `domcontentloaded`, `networkidle0`, `networkidle2` |\n| `keep_alive` | Max 600000ms (10 min) |\n| `screenshot.type` | `png`, `jpeg` |\n| `pdf.format` | `A4`, `Letter`, `Legal` |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/browser-rendering/configuration.md",
    "content": "# Configuration & Setup\n\n## Installation\n\n```bash\nnpm install @cloudflare/puppeteer  # or @cloudflare/playwright\n```\n\n**Use Cloudflare packages** - standard `puppeteer`/`playwright` won't work in Workers.\n\n## wrangler.json\n\n```json\n{\n  \"name\": \"browser-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\",\n  \"compatibility_flags\": [\"nodejs_compat\"],\n  \"browser\": {\n    \"binding\": \"MYBROWSER\"\n  }\n}\n```\n\n**Required:** `nodejs_compat` flag and `browser.binding`.\n\n## TypeScript\n\n```typescript\ninterface Env {\n  MYBROWSER: Fetcher;\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // ...\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## Development\n\n```bash\nwrangler dev --remote  # --remote required for browser binding\n```\n\n**Local mode does NOT support Browser Rendering** - must use `--remote`.\n\n## REST API\n\nNo wrangler config needed. Get API token with \"Browser Rendering - Edit\" permission.\n\n```bash\ncurl -X POST \\\n  'https://api.cloudflare.com/client/v4/accounts/{accountId}/browser-rendering/screenshot' \\\n  -H 'Authorization: Bearer TOKEN' \\\n  -d '{\"url\": \"https://example.com\"}' --output screenshot.png\n```\n\n## Requirements\n\n| Requirement | Value |\n|-------------|-------|\n| Node.js compatibility | `nodejs_compat` flag |\n| Compatibility date | 2023-03-01+ |\n| Module format | ES modules only |\n| Browser | Chromium 119+ (no Firefox/Safari) |\n\n**Not supported:** WebGL, WebRTC, extensions, `file://` protocol, Service Worker syntax.\n\n## Troubleshooting\n\n| Error | Solution |\n|-------|----------|\n| `MYBROWSER is undefined` | Use `wrangler dev --remote` |\n| `nodejs_compat not enabled` | Add to `compatibility_flags` |\n| `Module not found` | `npm install @cloudflare/puppeteer` |\n| `Browser Rendering not available` | Enable in dashboard |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/browser-rendering/gotchas.md",
    "content": "# Browser Rendering Gotchas\n\n## Tier Limits\n\n| Limit | Free | Paid |\n|-------|------|------|\n| Daily browser time | 10 min | Unlimited* |\n| Concurrent sessions | 3 | 30 |\n| Requests/minute | 6 | 180 |\n| Session keep-alive | 10 min max | 10 min max |\n\n*Subject to fair-use policy.\n\n**Check quota:**\n```typescript\nconst limits = await puppeteer.limits(env.MYBROWSER);\n// { remaining: 540000, total: 600000, concurrent: 2 }\n```\n\n## Always Close Browsers\n\n```typescript\nconst browser = await puppeteer.launch(env.MYBROWSER);\ntry {\n  const page = await browser.newPage();\n  await page.goto(\"https://example.com\");\n  return new Response(await page.content());\n} finally {\n  await browser.close(); // ALWAYS in finally\n}\n```\n\n**Workers vs REST:** REST auto-closes after timeout. Workers must call `close()` or session stays open until `keep_alive` expires.\n\n## Optimize Concurrency\n\n```typescript\n// ❌ 3 sessions (hits free tier limit)\nconst browser1 = await puppeteer.launch(env.MYBROWSER);\nconst browser2 = await puppeteer.launch(env.MYBROWSER);\n\n// ✅ 1 session, multiple pages\nconst browser = await puppeteer.launch(env.MYBROWSER);\nconst page1 = await browser.newPage();\nconst page2 = await browser.newPage();\n```\n\n## Common Errors\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| Session limit exceeded | Too many concurrent | Close unused browsers, use pages not browsers |\n| Page navigation timeout | Slow page or `networkidle` on busy page | Increase timeout, use `waitUntil: \"load\"` |\n| Session not found | Expired session | Catch error, launch new session |\n| Evaluation failed | DOM element missing | Use `?.` optional chaining |\n| Protocol error: Target closed | Page closed during operation | Await all ops before closing |\n\n## page.evaluate() Gotchas\n\n```typescript\n// ❌ Outer scope not available\nconst selector = \"h1\";\nawait page.evaluate(() => document.querySelector(selector));\n\n// ✅ Pass as argument\nawait page.evaluate((sel) => document.querySelector(sel)?.textContent, selector);\n```\n\n## Performance\n\n**waitUntil options (fastest to slowest):**\n1. `domcontentloaded` - DOM ready\n2. `load` - load event (default)\n3. `networkidle0` - no network for 500ms\n\n**Block unnecessary resources:**\n```typescript\nawait page.setRequestInterception(true);\npage.on(\"request\", (req) => {\n  if ([\"image\", \"stylesheet\", \"font\"].includes(req.resourceType())) {\n    req.abort();\n  } else {\n    req.continue();\n  }\n});\n```\n\n**Session reuse:** Cold start ~1-2s, warm connect ~100-200ms. Store sessionId in KV for reuse.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/browser-rendering/patterns.md",
    "content": "# Browser Rendering Patterns\n\n## Basic Worker\n\n```typescript\nimport puppeteer from \"@cloudflare/puppeteer\";\n\nexport default {\n  async fetch(request, env) {\n    const browser = await puppeteer.launch(env.MYBROWSER);\n    try {\n      const page = await browser.newPage();\n      await page.goto(\"https://example.com\");\n      return new Response(await page.content());\n    } finally {\n      await browser.close(); // ALWAYS in finally\n    }\n  }\n};\n```\n\n## Session Reuse\n\nKeep sessions alive for performance:\n```typescript\nlet sessionId = await env.SESSION_KV.get(\"browser-session\");\nif (sessionId) {\n  browser = await puppeteer.connect(env.MYBROWSER, sessionId);\n} else {\n  browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 600000 });\n  await env.SESSION_KV.put(\"browser-session\", browser.sessionId(), { expirationTtl: 600 });\n}\n// Don't close browser to keep session alive\n```\n\n## Common Operations\n\n| Task | Code |\n|------|------|\n| Screenshot | `await page.screenshot({ type: \"png\", fullPage: true })` |\n| PDF | `await page.pdf({ format: \"A4\", printBackground: true })` |\n| Extract data | `await page.evaluate(() => document.querySelector('h1').textContent)` |\n| Fill form | `await page.type('#input', 'value'); await page.click('button')` |\n| Wait nav | `await Promise.all([page.waitForNavigation(), page.click('a')])` |\n\n## Parallel Scraping\n\n```typescript\nconst pages = await Promise.all(urls.map(() => browser.newPage()));\nawait Promise.all(pages.map((p, i) => p.goto(urls[i])));\nconst titles = await Promise.all(pages.map(p => p.title()));\n```\n\n## Playwright Selectors\n\n```typescript\nimport { launch } from \"@cloudflare/playwright\";\nconst browser = await launch(env.MYBROWSER);\nawait page.getByRole(\"button\", { name: \"Sign in\" }).click();\nawait page.getByLabel(\"Email\").fill(\"user@example.com\");\nawait page.getByTestId(\"submit-button\").click();\n```\n\n## Incognito Contexts\n\nIsolated sessions without multiple browsers:\n```typescript\nconst ctx1 = await browser.createIncognitoBrowserContext();\nconst ctx2 = await browser.createIncognitoBrowserContext();\n// Each has isolated cookies/storage\n```\n\n## Quota Check\n\n```typescript\nconst limits = await puppeteer.limits(env.MYBROWSER);\nif (limits.remaining < 60000) return new Response(\"Quota low\", { status: 429 });\n```\n\n## Error Handling\n\n```typescript\ntry {\n  await page.goto(url, { timeout: 30000, waitUntil: \"networkidle0\" });\n} catch (e) {\n  if (e.message.includes(\"timeout\")) return new Response(\"Timeout\", { status: 504 });\n  if (e.message.includes(\"Session limit\")) return new Response(\"Too many sessions\", { status: 429 });\n} finally {\n  if (browser) await browser.close();\n}\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/c3/README.md",
    "content": "# C3 (create-cloudflare)\n\nOfficial CLI for scaffolding Cloudflare Workers and Pages projects with templates, TypeScript, and instant deployment.\n\n## Quick Start\n\n```bash\n# Interactive (recommended for first-time)\nnpm create cloudflare@latest my-app\n\n# Worker (API/WebSocket/Cron)\nnpm create cloudflare@latest my-api -- --type=hello-world --ts\n\n# Pages (static/SSG/full-stack)\nnpm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages\n```\n\n## Platform Decision Tree\n\n```\nWhat are you building?\n\n├─ API / WebSocket / Cron / Email handler\n│   └─ Workers (default) - no --platform flag needed\n│       npm create cloudflare@latest my-api -- --type=hello-world\n\n├─ Static site / SSG / Documentation\n│   └─ Pages - requires --platform=pages\n│       npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages\n\n├─ Full-stack app (Next.js/Remix/SvelteKit)\n│   ├─ Need Durable Objects, Queues, or Workers-only features?\n│   │   └─ Workers (default)\n│   └─ Otherwise use Pages for git integration and branch previews\n│       └─ Add --platform=pages\n\n└─ Convert existing project\n    └─ npm create cloudflare@latest . -- --type=pre-existing --existing-script=./src/worker.ts\n```\n\n**Critical:** Pages projects require `--platform=pages` flag. Without it, C3 defaults to Workers.\n\n## Interactive Flow\n\nWhen run without flags, C3 prompts in this order:\n\n1. **Project name** - Directory to create (defaults to current dir with `.`)\n2. **Application type** - `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template`\n3. **Platform** - `workers` (default) or `pages` (for web apps only)\n4. **Framework** - If web-app: `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, etc.\n5. **TypeScript** - `yes` (recommended) or `no`\n6. **Git** - Initialize repository? `yes` or `no`\n7. **Deploy** - Deploy now? `yes` or `no` (requires `wrangler login`)\n\n## Installation Methods\n\n```bash\n# NPM\nnpm create cloudflare@latest\n\n# Yarn\nyarn create cloudflare\n\n# PNPM\npnpm create cloudflare@latest\n```\n\n## In This Reference\n\n| File | Purpose | Use When |\n|------|---------|----------|\n| **api.md** | Complete CLI flag reference | Scripting, CI/CD, advanced usage |\n| **configuration.md** | Generated files, bindings, types | Understanding output, customization |\n| **patterns.md** | Workflows, CI/CD, monorepos | Real-world integration |\n| **gotchas.md** | Troubleshooting failures | Deployment blocked, errors |\n\n## Reading Order\n\n| Task | Read |\n|------|------|\n| Create first project | README only |\n| Set up CI/CD | README → api → patterns |\n| Debug failed deploy | gotchas |\n| Understand generated files | configuration |\n| Full CLI reference | api |\n| Create custom template | patterns → configuration |\n| Convert existing project | README → patterns |\n\n## Post-Creation\n\n```bash\ncd my-app\n\n# Local dev with hot reload\nnpm run dev\n\n# Generate TypeScript types for bindings\nnpm run cf-typegen\n\n# Deploy to Cloudflare\nnpm run deploy\n```\n\n## See Also\n\n- **workers/README.md** - Workers runtime, bindings, APIs\n- **workers-ai/README.md** - AI/ML models\n- **pages/README.md** - Pages-specific features\n- **wrangler/README.md** - Wrangler CLI beyond initial setup\n- **d1/README.md** - SQLite database\n- **r2/README.md** - Object storage\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/c3/api.md",
    "content": "# C3 CLI Reference\n\n## Invocation\n\n```bash\nnpm create cloudflare@latest [name] [-- flags]  # NPM requires --\nyarn create cloudflare [name] [flags]\npnpm create cloudflare@latest [name] [-- flags]\n```\n\n## Core Flags\n\n| Flag | Values | Description |\n|------|--------|-------------|\n| `--type` | `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template` | Application type |\n| `--platform` | `workers` (default), `pages` | Target platform |\n| `--framework` | `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, `qwik`, `vue`, `angular`, `hono` | Web framework (requires `--type=web-app`) |\n| `--lang` | `ts`, `js`, `python` | Language (for `--type=hello-world`) |\n| `--ts` / `--no-ts` | - | TypeScript for web apps |\n\n## Deployment Flags\n\n| Flag | Description |\n|------|-------------|\n| `--deploy` / `--no-deploy` | Deploy immediately (prompts interactive, skips in CI) |\n| `--git` / `--no-git` | Initialize git (default: yes) |\n| `--open` | Open browser after deploy |\n\n## Advanced Flags\n\n| Flag | Description |\n|------|-------------|\n| `--template=user/repo` | GitHub template or local path |\n| `--existing-script=./src/worker.ts` | Existing script (requires `--type=pre-existing`) |\n| `--category=ai\\|database\\|realtime` | Demo filter (requires `--type=demo`) |\n| `--experimental` | Enable experimental features |\n| `--wrangler-defaults` | Skip wrangler prompts |\n\n## Environment Variables\n\n```bash\nCLOUDFLARE_API_TOKEN=xxx    # For deployment\nCLOUDFLARE_ACCOUNT_ID=xxx   # Account ID\nCF_TELEMETRY_DISABLED=1     # Disable telemetry\n```\n\n## Exit Codes\n\n`0` success, `1` user abort, `2` error\n\n## Examples\n\n```bash\n# TypeScript Worker\nnpm create cloudflare@latest my-api -- --type=hello-world --lang=ts --no-deploy\n\n# Next.js on Pages\nnpm create cloudflare@latest my-app -- --type=web-app --framework=next --platform=pages --ts\n\n# Astro blog\nnpm create cloudflare@latest my-blog -- --type=web-app --framework=astro --ts --deploy\n\n# CI: non-interactive\nnpm create cloudflare@latest my-app -- --type=web-app --framework=next --ts --no-git --no-deploy\n\n# GitHub template\nnpm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi\n\n# Convert existing project\nnpm create cloudflare@latest . -- --type=pre-existing --existing-script=./build/worker.js\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/c3/configuration.md",
    "content": "# C3 Generated Configuration\n\n## Output Structure\n\n```\nmy-app/\n├── src/index.ts          # Worker entry point\n├── wrangler.jsonc        # Cloudflare config\n├── package.json          # Scripts\n├── tsconfig.json\n└── .gitignore\n```\n\n## wrangler.jsonc\n\n```jsonc\n{\n  \"$schema\": \"https://raw.githubusercontent.com/cloudflare/workers-sdk/main/packages/wrangler/config-schema.json\",\n  \"name\": \"my-app\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2026-01-27\"\n}\n```\n\n## Binding Placeholders\n\nC3 generates **placeholder IDs** that must be replaced before deploy:\n\n```jsonc\n{\n  \"kv_namespaces\": [{ \"binding\": \"MY_KV\", \"id\": \"placeholder_kv_id\" }],\n  \"d1_databases\": [{ \"binding\": \"DB\", \"database_id\": \"00000000-...\" }]\n}\n```\n\n**Replace with real IDs:**\n```bash\nnpx wrangler kv namespace create MY_KV   # Returns real ID\nnpx wrangler d1 create my-database       # Returns real database_id\n```\n\n**Deployment error if not replaced:**\n```\nError: Invalid KV namespace ID \"placeholder_kv_id\"\n```\n\n## Scripts\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"wrangler dev\",\n    \"deploy\": \"wrangler deploy\",\n    \"cf-typegen\": \"wrangler types\"\n  }\n}\n```\n\n## Type Generation\n\nRun after adding bindings:\n```bash\nnpm run cf-typegen\n```\n\nGenerates `.wrangler/types/runtime.d.ts`:\n```typescript\ninterface Env {\n  MY_KV: KVNamespace;\n  DB: D1Database;\n}\n```\n\n## Post-Creation Checklist\n\n1. Review `wrangler.jsonc` - check name, compatibility_date\n2. Replace placeholder binding IDs with real resource IDs\n3. Run `npm run cf-typegen`\n4. Test: `npm run dev`\n5. Deploy: `npm run deploy`\n6. Add secrets: `npx wrangler secret put SECRET_NAME`\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/c3/gotchas.md",
    "content": "# C3 Troubleshooting\n\n## Deployment Issues\n\n### Placeholder IDs\n\n**Error:** \"Invalid namespace ID\"  \n**Fix:** Replace placeholders in wrangler.jsonc with real IDs:\n```bash\nnpx wrangler kv namespace create MY_KV  # Get real ID\n```\n\n### Authentication\n\n**Error:** \"Not authenticated\"  \n**Fix:** `npx wrangler login` or set `CLOUDFLARE_API_TOKEN`\n\n### Name Conflict\n\n**Error:** \"Worker already exists\"  \n**Fix:** Change `name` in wrangler.jsonc\n\n## Platform Selection\n\n| Need | Platform |\n|------|----------|\n| Git integration, branch previews | `--platform=pages` |\n| Durable Objects, D1, Queues | Workers (default) |\n\nWrong platform? Recreate with correct `--platform` flag.\n\n## TypeScript Issues\n\n**\"Cannot find name 'KVNamespace'\"**\n```bash\nnpm run cf-typegen  # Regenerate types\n# Restart TS server in editor\n```\n\n**Missing types after config change:** Re-run `npm run cf-typegen`\n\n## Package Manager\n\n**Multiple lockfiles causing issues:**\n```bash\nrm pnpm-lock.yaml  # If using npm\nrm package-lock.json  # If using pnpm\n```\n\n## CI/CD\n\n**CI hangs on prompts:**\n```bash\nnpm create cloudflare@latest my-app -- \\\n  --type=hello-world --lang=ts --no-git --no-deploy\n```\n\n**Auth in CI:**\n```yaml\nenv:\n  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n  CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}\n```\n\n## Framework-Specific\n\n| Framework | Issue | Fix |\n|-----------|-------|-----|\n| Next.js | create-next-app failed | `npm cache clean --force`, retry |\n| Astro | Adapter missing | Install `@astrojs/cloudflare` |\n| Remix | Module errors | Update `@remix-run/cloudflare*` |\n\n## Compatibility Date\n\n**\"Feature X requires compatibility_date >= ...\"**  \n**Fix:** Update `compatibility_date` in wrangler.jsonc to today's date\n\n## Node.js Version\n\n**\"Node.js version not supported\"**  \n**Fix:** Install Node.js 18+ (`nvm install 20`)\n\n## Quick Reference\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| Invalid namespace ID | Placeholder binding | Create resource, update config |\n| Not authenticated | No login | `npx wrangler login` |\n| Cannot find KVNamespace | Missing types | `npm run cf-typegen` |\n| Worker already exists | Name conflict | Change `name` |\n| CI hangs | Missing flags | Add --type, --lang, --no-deploy |\n| Template not found | Bad name | Check cloudflare/templates |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/c3/patterns.md",
    "content": "# C3 Usage Patterns\n\n## Quick Workflows\n\n```bash\n# TypeScript API Worker\nnpm create cloudflare@latest my-api -- --type=hello-world --lang=ts --deploy\n\n# Next.js on Pages\nnpm create cloudflare@latest my-app -- --type=web-app --framework=next --platform=pages --ts --deploy\n\n# Astro static site  \nnpm create cloudflare@latest my-blog -- --type=web-app --framework=astro --platform=pages --ts\n```\n\n## CI/CD (GitHub Actions)\n\n```yaml\n- name: Deploy\n  run: npm run deploy\n  env:\n    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n    CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}\n```\n\n**Non-interactive requires:**\n```bash\n--type=<value>       # Required\n--no-git             # Recommended (CI already in git)\n--no-deploy          # Deploy separately with secrets\n--framework=<value>  # For web-app\n--ts / --no-ts       # Required\n```\n\n## Monorepo\n\nC3 detects workspace config (`package.json` workspaces or `pnpm-workspace.yaml`).\n\n```bash\ncd packages/\nnpm create cloudflare@latest my-worker -- --type=hello-world --lang=ts --no-deploy\n```\n\n## Custom Templates\n\n```bash\n# GitHub repo\nnpm create cloudflare@latest -- --template=username/repo\nnpm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi\n\n# Local path\nnpm create cloudflare@latest my-app -- --template=../my-template\n```\n\n**Template requires `c3.config.json`:**\n```json\n{\n  \"name\": \"my-template\",\n  \"category\": \"hello-world\",\n  \"copies\": [{ \"path\": \"src/\" }, { \"path\": \"wrangler.jsonc\" }],\n  \"transforms\": [{ \"path\": \"package.json\", \"jsonc\": { \"name\": \"{{projectName}}\" }}]\n}\n```\n\n## Existing Projects\n\n```bash\n# Add Cloudflare to existing Worker\nnpm create cloudflare@latest . -- --type=pre-existing --existing-script=./dist/index.js\n\n# Add to existing framework app\nnpm create cloudflare@latest . -- --type=web-app --framework=next --platform=pages --ts\n```\n\n## Post-Creation Checklist\n\n1. Review `wrangler.jsonc` - set `compatibility_date`, verify `name`\n2. Create bindings: `wrangler kv namespace create`, `wrangler d1 create`, `wrangler r2 bucket create`\n3. Generate types: `npm run cf-typegen`\n4. Test: `npm run dev`\n5. Deploy: `npm run deploy`\n6. Set secrets: `wrangler secret put SECRET_NAME`\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/cache-reserve/README.md",
    "content": "# Cloudflare Cache Reserve\n\n**Persistent cache storage built on R2 for long-term content retention**\n\n## Smart Shield Integration\n\nCache Reserve is part of **Smart Shield**, Cloudflare's comprehensive security and performance suite:\n\n- **Smart Shield Advanced tier**: Includes 2TB Cache Reserve storage\n- **Standalone purchase**: Available separately if not using Smart Shield\n- **Migration**: Existing standalone customers can migrate to Smart Shield bundles\n\n**Decision**: Already on Smart Shield Advanced? Cache Reserve is included. Otherwise evaluate standalone purchase vs Smart Shield upgrade.\n\n## Overview\n\nCache Reserve is Cloudflare's persistent, large-scale cache storage layer built on R2. It acts as the ultimate upper-tier cache, storing cacheable content for extended periods (30+ days) to maximize cache hits, reduce origin egress fees, and shield origins from repeated requests for long-tail content.\n\n## Core Concepts\n\n### What is Cache Reserve?\n\n- **Persistent storage layer**: Built on R2, sits above tiered cache hierarchy\n- **Long-term retention**: 30-day default retention, extended on each access\n- **Automatic operation**: Works seamlessly with existing CDN, no code changes required\n- **Origin shielding**: Dramatically reduces origin egress by serving cached content longer\n- **Usage-based pricing**: Pay only for storage + read/write operations\n\n### Cache Hierarchy\n\n```\nVisitor Request\n    ↓\nLower-Tier Cache (closest to visitor)\n    ↓ (on miss)\nUpper-Tier Cache (closest to origin)\n    ↓ (on miss)\nCache Reserve (R2 persistent storage)\n    ↓ (on miss)\nOrigin Server\n```\n\n### How It Works\n\n1. **On cache miss**: Content fetched from origin �� written to Cache Reserve + edge caches simultaneously\n2. **On edge eviction**: Content may be evicted from edge cache but remains in Cache Reserve\n3. **On subsequent request**: If edge cache misses but Cache Reserve hits → content restored to edge caches\n4. **Retention**: Assets remain in Cache Reserve for 30 days since last access (configurable via TTL)\n\n## When to Use Cache Reserve\n\n```\nNeed persistent caching?\n├─ High origin egress costs → Cache Reserve ✓\n├─ Long-tail content (archives, media libraries) → Cache Reserve ✓\n├─ Already using Smart Shield Advanced → Included! ✓\n├─ Video streaming with seeking (range requests) → ✗ Not supported\n├─ Dynamic/personalized content → ✗ Use edge cache only\n├─ Need per-request cache control from Workers → ✗ Use R2 directly\n└─ Frequently updated content (< 10hr lifetime) → ✗ Not eligible\n```\n\n## Asset Eligibility\n\nCache Reserve only stores assets meeting **ALL** criteria:\n\n- Cacheable per Cloudflare's standard rules\n- Minimum 10-hour TTL (36000 seconds)\n- `Content-Length` header present\n- Original files only (not transformed images)\n\n### Eligibility Checklist\n\nUse this checklist to verify if an asset is eligible:\n\n- [ ] Zone has Cache Reserve enabled\n- [ ] Zone has Tiered Cache enabled (required)\n- [ ] Asset TTL ≥ 10 hours (36,000 seconds)\n- [ ] `Content-Length` header present on origin response\n- [ ] No `Set-Cookie` header (or uses private directive)\n- [ ] `Vary` header is NOT `*` (can be `Accept-Encoding`)\n- [ ] Not an image transformation variant (original images OK)\n- [ ] Not a range request (no HTTP 206 support)\n- [ ] Not O2O (Orange-to-Orange) proxied request\n\n**All boxes must be checked for Cache Reserve eligibility.**\n\n### Not Eligible\n\n- Assets with TTL < 10 hours\n- Responses without `Content-Length` header\n- Image transformation variants (original images are eligible)\n- Responses with `Set-Cookie` headers\n- Responses with `Vary: *` header\n- Assets from R2 public buckets on same zone\n- O2O (Orange-to-Orange) setup requests\n- **Range requests** (video seeking, partial content downloads)\n\n## Quick Start\n\n```bash\n# Enable via Dashboard\nhttps://dash.cloudflare.com/caching/cache-reserve\n# Click \"Enable Storage Sync\" or \"Purchase\" button\n```\n\n**Prerequisites:**\n- Paid Cache Reserve plan or Smart Shield Advanced required\n- Tiered Cache required for optimal performance\n\n## Essential Commands\n\n```bash\n# Check Cache Reserve status\ncurl -X GET \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve\" \\\n  -H \"Authorization: Bearer $API_TOKEN\"\n\n# Enable Cache Reserve\ncurl -X PATCH \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve\" \\\n  -H \"Authorization: Bearer $API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"value\": \"on\"}'\n\n# Check asset cache status\ncurl -I https://example.com/asset.jpg | grep -i cache\n```\n\n## In This Reference\n\n| Task | Files |\n|------|-------|\n| Evaluate if Cache Reserve fits your use case | README.md (this file) |\n| Enable Cache Reserve for your zone | README.md + [configuration.md](./configuration.md) |\n| Use with Workers (understand limitations) | [api.md](./api.md) |\n| Setup via SDKs or IaC (TypeScript, Python, Terraform) | [configuration.md](./configuration.md) |\n| Optimize costs and debug issues | [patterns.md](./patterns.md) + [gotchas.md](./gotchas.md) |\n| Understand eligibility and troubleshoot | [gotchas.md](./gotchas.md) → [patterns.md](./patterns.md) |\n\n**Files:**\n- [configuration.md](./configuration.md) - Setup, API, SDKs, and Cache Rules\n- [api.md](./api.md) - Purging, monitoring, Workers integration\n- [patterns.md](./patterns.md) - Best practices, cost optimization, debugging\n- [gotchas.md](./gotchas.md) - Common issues, limitations, troubleshooting\n\n## See Also\n- [r2](../r2/) - Cache Reserve built on R2 storage\n- [workers](../workers/) - Workers integration with Cache API\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/cache-reserve/api.md",
    "content": "# Cache Reserve API\n\n## Workers Integration\n\n```\n┌────────────────────────────────────────────────────────────────┐\n│ CRITICAL: Workers Cache API ≠ Cache Reserve                   │\n│                                                                │\n│ • Workers caches.default / cache.put() → edge cache ONLY      │\n│ • Cache Reserve → zone-level setting, automatic, no per-req   │\n│ • You CANNOT selectively write to Cache Reserve from Workers  │\n│ • Cache Reserve works with standard fetch(), not cache.put()  │\n└────────────────────────────────────────────────────────────────┘\n```\n\nCache Reserve is a **zone-level configuration**, not a per-request API. It works automatically when enabled for the zone:\n\n### Standard Fetch (Recommended)\n\n```typescript\n// Cache Reserve works automatically via standard fetch\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // Standard fetch uses Cache Reserve automatically\n    return await fetch(request);\n  }\n};\n```\n\n### Cache API Limitations\n\n**IMPORTANT**: `cache.put()` is **NOT compatible** with Cache Reserve or Tiered Cache.\n\n```typescript\n// ❌ WRONG: cache.put() bypasses Cache Reserve\nconst cache = caches.default;\nlet response = await cache.match(request);\nif (!response) {\n  response = await fetch(request);\n  await cache.put(request, response.clone()); // Bypasses Cache Reserve!\n}\n\n// ✅ CORRECT: Use standard fetch for Cache Reserve compatibility\nreturn await fetch(request);\n\n// ✅ CORRECT: Use Cache API only for custom cache namespaces\nconst customCache = await caches.open('my-custom-cache');\nlet response = await customCache.match(request);\nif (!response) {\n  response = await fetch(request);\n  await customCache.put(request, response.clone()); // Custom cache OK\n}\n```\n\n## Purging and Cache Management\n\n### Purge by URL (Instant)\n\n```typescript\n// Purge specific URL from Cache Reserve immediately\nconst purgeCacheReserveByURL = async (\n  zoneId: string,\n  apiToken: string,\n  urls: string[]\n) => {\n  const response = await fetch(\n    `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,\n    {\n      method: 'POST',\n      headers: {\n        'Authorization': `Bearer ${apiToken}`,\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ files: urls })\n    }\n  );\n  return await response.json();\n};\n\n// Example usage\nawait purgeCacheReserveByURL('zone123', 'token456', [\n  'https://example.com/image.jpg',\n  'https://example.com/video.mp4'\n]);\n```\n\n### Purge by Tag/Host/Prefix (Revalidation)\n\n```typescript\n// Purge by cache tag - forces revalidation, not immediate removal\nawait fetch(\n  `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,\n  {\n    method: 'POST',\n    headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },\n    body: JSON.stringify({ tags: ['tag1', 'tag2'] })\n  }\n);\n```\n\n**Purge behavior:**\n- **By URL**: Immediate removal from Cache Reserve + edge cache\n- **By tag/host/prefix**: Revalidation only, assets remain in storage (costs continue)\n\n### Clear All Cache Reserve Data\n\n```typescript\n// Requires Cache Reserve OFF first\nawait fetch(\n  `https://api.cloudflare.com/client/v4/zones/${zoneId}/cache/cache_reserve_clear`,\n  { method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}` } }\n);\n\n// Check status: GET same endpoint returns { state: \"In-progress\" | \"Completed\" }\n```\n\n**Process**: Disable Cache Reserve → Call clear endpoint → Wait up to 24hr → Re-enable\n\n## Monitoring and Analytics\n\n### Dashboard Analytics\n\nNavigate to **Caching > Cache Reserve** to view:\n\n- **Egress Savings**: Total bytes served from Cache Reserve vs origin egress cost saved\n- **Requests Served**: Cache Reserve hits vs misses breakdown\n- **Storage Used**: Current GB stored in Cache Reserve (billed monthly)\n- **Operations**: Class A (writes) and Class B (reads) operation counts\n- **Cost Tracking**: Estimated monthly costs based on current usage\n\n### Logpush Integration\n\n```typescript\n// Logpush field: CacheReserveUsed (boolean) - filter for Cache Reserve hits\n// Query Cache Reserve hits in analytics\nconst logpushQuery = `\n  SELECT \n    ClientRequestHost, \n    COUNT(*) as requests, \n    SUM(EdgeResponseBytes) as bytes_served,\n    COUNT(CASE WHEN CacheReserveUsed = true THEN 1 END) as cache_reserve_hits,\n    COUNT(CASE WHEN CacheReserveUsed = false THEN 1 END) as cache_reserve_misses\n  FROM http_requests \n  WHERE Timestamp >= NOW() - INTERVAL '24 hours'\n  GROUP BY ClientRequestHost \n  ORDER BY requests DESC\n`;\n\n// Filter only Cache Reserve hits\nconst crHitsQuery = `\n  SELECT ClientRequestHost, COUNT(*) as requests, SUM(EdgeResponseBytes) as bytes\n  FROM http_requests \n  WHERE CacheReserveUsed = true AND Timestamp >= NOW() - INTERVAL '7 days'\n  GROUP BY ClientRequestHost \n  ORDER BY bytes DESC\n`;\n```\n\n### GraphQL Analytics\n\n```graphql\nquery CacheReserveAnalytics($zoneTag: string, $since: string, $until: string) {\n  viewer {\n    zones(filter: { zoneTag: $zoneTag }) {\n      httpRequests1dGroups(\n        filter: { datetime_geq: $since, datetime_leq: $until }\n        limit: 1000\n      ) {\n        dimensions { date }\n        sum {\n          cachedBytes\n          cachedRequests\n          bytes\n          requests\n        }\n      }\n    }\n  }\n}\n```\n\n## Pricing\n\n```typescript\n// Storage: $0.015/GB-month | Class A (writes): $4.50/M | Class B (reads): $0.36/M\n// Cache miss: 1A + 1B | Cache hit: 1B | Assets >1GB: proportionally more ops\n```\n\n## See Also\n\n- [README](./README.md) - Overview and core concepts\n- [Configuration](./configuration.md) - Setup and Cache Rules\n- [Patterns](./patterns.md) - Best practices and optimization\n- [Gotchas](./gotchas.md) - Common issues and troubleshooting\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/cache-reserve/configuration.md",
    "content": "# Cache Reserve Configuration\n\n## Dashboard Setup\n\n**Minimum steps to enable:**\n\n```bash\n# Navigate to dashboard\nhttps://dash.cloudflare.com/caching/cache-reserve\n\n# Click \"Enable Storage Sync\" or \"Purchase\" button\n```\n\n**Prerequisites:**\n- Paid Cache Reserve plan or Smart Shield Advanced required\n- Tiered Cache **required** for Cache Reserve to function optimally\n\n## API Configuration\n\n### REST API\n\n```bash\n# Enable\ncurl -X PATCH \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve\" \\\n  -H \"Authorization: Bearer $API_TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"value\": \"on\"}'\n\n# Check status\ncurl -X GET \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve\" \\\n  -H \"Authorization: Bearer $API_TOKEN\"\n```\n\n### TypeScript SDK\n\n```bash\nnpm install cloudflare\n```\n\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst client = new Cloudflare({\n  apiToken: process.env.CLOUDFLARE_API_TOKEN,\n});\n\n// Enable Cache Reserve\nawait client.cache.cacheReserve.edit({\n  zone_id: 'abc123',\n  value: 'on',\n});\n\n// Get Cache Reserve status\nconst status = await client.cache.cacheReserve.get({\n  zone_id: 'abc123',\n});\nconsole.log(status.value); // 'on' or 'off'\n```\n\n### Python SDK\n\n```bash\npip install cloudflare\n```\n\n```python\nfrom cloudflare import Cloudflare\n\nclient = Cloudflare(api_token=os.environ.get(\"CLOUDFLARE_API_TOKEN\"))\n\n# Enable Cache Reserve\nclient.cache.cache_reserve.edit(\n    zone_id=\"abc123\",\n    value=\"on\"\n)\n\n# Get Cache Reserve status\nstatus = client.cache.cache_reserve.get(zone_id=\"abc123\")\nprint(status.value)  # 'on' or 'off'\n```\n\n### Terraform\n\n```hcl\nterraform {\n  required_providers {\n    cloudflare = {\n      source  = \"cloudflare/cloudflare\"\n      version = \"~> 4.0\"\n    }\n  }\n}\n\nprovider \"cloudflare\" {\n  api_token = var.cloudflare_api_token\n}\n\nresource \"cloudflare_zone_cache_reserve\" \"example\" {\n  zone_id = var.zone_id\n  enabled = true\n}\n\n# Tiered Cache is required for Cache Reserve\nresource \"cloudflare_tiered_cache\" \"example\" {\n  zone_id    = var.zone_id\n  cache_type = \"smart\"\n}\n```\n\n### Pulumi\n\n```typescript\nimport * as cloudflare from \"@pulumi/cloudflare\";\n\n// Enable Cache Reserve\nconst cacheReserve = new cloudflare.ZoneCacheReserve(\"example\", {\n  zoneId: zoneId,\n  enabled: true,\n});\n\n// Enable Tiered Cache (required)\nconst tieredCache = new cloudflare.TieredCache(\"example\", {\n  zoneId: zoneId,\n  cacheType: \"smart\",\n});\n```\n\n### Required API Token Permissions\n\n- `Zone Settings Read`\n- `Zone Settings Write`\n- `Zone Read`\n- `Zone Write`\n\n## Cache Rules Integration\n\nControl Cache Reserve eligibility via Cache Rules:\n\n```typescript\n// Enable for static assets\n{\n  action: 'set_cache_settings',\n  action_parameters: {\n    cache_reserve: { eligible: true, minimum_file_ttl: 86400 },\n    edge_ttl: { mode: 'override_origin', default: 86400 },\n    cache: true\n  },\n  expression: '(http.request.uri.path matches \"\\\\.(jpg|png|webp|pdf|zip)$\")'\n}\n\n// Disable for APIs\n{\n  action: 'set_cache_settings',\n  action_parameters: { cache_reserve: { eligible: false } },\n  expression: '(http.request.uri.path matches \"^/api/\")'\n}\n\n// Create via API: PUT to zones/{zone_id}/rulesets/phases/http_request_cache_settings/entrypoint\n```\n\n## Wrangler Integration\n\nCache Reserve works automatically with Workers deployed via Wrangler. No special wrangler.jsonc configuration needed - enable Cache Reserve via Dashboard or API for the zone.\n\n## See Also\n\n- [README](./README.md) - Overview and core concepts\n- [API Reference](./api.md) - Purging and monitoring APIs\n- [Patterns](./patterns.md) - Best practices and optimization\n- [Gotchas](./gotchas.md) - Common issues and troubleshooting\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/cache-reserve/gotchas.md",
    "content": "# Cache Reserve Gotchas\n\n## Common Errors\n\n### \"Assets Not Being Cached in Cache Reserve\"\n\n**Cause:** Asset is not cacheable, TTL < 10 hours, Content-Length header missing, or blocking headers present (Set-Cookie, Vary: *)  \n**Solution:** Ensure minimum TTL of 10+ hours (`Cache-Control: public, max-age=36000`), add Content-Length header, remove Set-Cookie header, and set `Vary: Accept-Encoding` (not *)\n\n### \"Range Requests Not Working\" (Video Seeking Fails)\n\n**Cause:** Cache Reserve does **NOT** support range requests (HTTP 206 Partial Content)  \n**Solution:** Range requests bypass Cache Reserve entirely. For video streaming with seeking:\n- Use edge cache only (shorter TTLs)\n- Consider R2 with direct access for range-heavy workloads\n- Accept that seekable content won't benefit from Cache Reserve persistence\n\n### \"Origin Bandwidth Higher Than Expected\"\n\n**Cause:** Cache Reserve fetches **uncompressed** content from origin, even though it serves compressed to visitors  \n**Solution:** \n- If origin charges by bandwidth, factor in uncompressed transfer costs\n- Cache Reserve compresses for visitors automatically (saves visitor bandwidth)\n- Compare: origin egress savings vs higher uncompressed fetch costs\n\n### \"Cloudflare Images Not Caching with Cache Reserve\"\n\n**Cause:** Cloudflare Images with `Vary: Accept` header (format negotiation) is incompatible with Cache Reserve  \n**Solution:** \n- Cache Reserve silently skips images with Vary for format negotiation\n- Original images (non-transformed) may still be eligible\n- Use Cloudflare Images variants or edge cache for transformed images\n\n### \"High Class A Operations Costs\"\n\n**Cause:** Frequent cache misses, short TTLs, or frequent revalidation  \n**Solution:** Increase TTL for stable content (24+ hours), enable Tiered Cache to reduce direct Cache Reserve misses, or use stale-while-revalidate\n\n### \"Purge Not Working as Expected\"\n\n**Cause:** Purge by tag only triggers revalidation but doesn't remove from Cache Reserve storage  \n**Solution:** Use purge by URL for immediate removal, or disable Cache Reserve then clear all data for complete removal\n\n### \"O2O (Orange-to-Orange) Assets Not Caching\"\n\n**Cause:** Orange-to-Orange (proxied zone requesting another proxied zone on Cloudflare) bypasses Cache Reserve  \n**Solution:** \n- **What is O2O**: Zone A (proxied) → Zone B (proxied), both on Cloudflare\n- **Detection**: Check `cf-cache-status` for `BYPASS` and review request path\n- **Workaround**: Use R2 or direct origin access instead of O2O proxy chains\n\n### \"Cache Reserve must be OFF before clearing data\"\n\n**Cause:** Attempting to clear Cache Reserve data while it's still enabled  \n**Solution:** Disable Cache Reserve first, wait briefly for propagation (5s), then clear data (can take up to 24 hours)\n\n## Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Minimum TTL | 10 hours (36000 seconds) | Assets with shorter TTL not eligible |\n| Default retention | 30 days (2592000 seconds) | Configurable |\n| Maximum file size | Same as R2 limits | No practical limit |\n| Purge/clear time | Up to 24 hours | Complete propagation time |\n| Plan requirement | Paid Cache Reserve or Smart Shield | Not available on free plans |\n| Content-Length header | Required | Must be present for eligibility |\n| Set-Cookie header | Blocks caching | Must not be present (or use private directive) |\n| Vary header | Cannot be * | Can use Vary: Accept-Encoding |\n| Image transformations | Variants not eligible | Original images only |\n| Range requests | NOT supported | HTTP 206 bypasses Cache Reserve |\n| Compression | Fetches uncompressed | Serves compressed to visitors |\n| Worker control | Zone-level only | Cannot control per-request |\n| O2O requests | Bypassed | Orange-to-Orange not eligible |\n\n## Additional Resources\n\n- **Official Docs**: https://developers.cloudflare.com/cache/advanced-configuration/cache-reserve/\n- **API Reference**: https://developers.cloudflare.com/api/resources/cache/subresources/cache_reserve/\n- **Cache Rules**: https://developers.cloudflare.com/cache/how-to/cache-rules/\n- **Workers Cache API**: https://developers.cloudflare.com/workers/runtime-apis/cache/\n- **R2 Documentation**: https://developers.cloudflare.com/r2/\n- **Smart Shield**: https://developers.cloudflare.com/smart-shield/\n- **Tiered Cache**: https://developers.cloudflare.com/cache/how-to/tiered-cache/\n\n## Troubleshooting Flowchart\n\nAsset not caching in Cache Reserve?\n\n```\n1. Is Cache Reserve enabled for zone?\n   → No: Enable via Dashboard or API\n   → Yes: Continue to step 2\n\n2. Is Tiered Cache enabled?\n   → No: Enable Tiered Cache (required!)\n   → Yes: Continue to step 3\n\n3. Does asset have TTL ≥ 10 hours?\n   → No: Increase via Cache Rules (edge_ttl override)\n   → Yes: Continue to step 4\n\n4. Is Content-Length header present?\n   → No: Fix origin to include Content-Length\n   → Yes: Continue to step 5\n\n5. Is Set-Cookie header present?\n   → Yes: Remove Set-Cookie or scope appropriately\n   → No: Continue to step 6\n\n6. Is Vary header set to *?\n   → Yes: Change to specific value (e.g., Accept-Encoding)\n   → No: Continue to step 7\n\n7. Is this a range request?\n   → Yes: Range requests bypass Cache Reserve (not supported)\n   → No: Continue to step 8\n\n8. Is this an O2O (Orange-to-Orange) request?\n   → Yes: O2O bypasses Cache Reserve\n   → No: Continue to step 9\n\n9. Check Logpush CacheReserveUsed field\n   → Filter logs to see if assets ever hit Cache Reserve\n   → Verify cf-cache-status header (should be HIT after first request)\n```\n\n## See Also\n\n- [README](./README.md) - Overview and core concepts\n- [Configuration](./configuration.md) - Setup and Cache Rules\n- [API Reference](./api.md) - Purging and monitoring\n- [Patterns](./patterns.md) - Best practices and optimization\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/cache-reserve/patterns.md",
    "content": "# Cache Reserve Patterns\n\n## Best Practices\n\n### 1. Always Enable Tiered Cache\n\n```typescript\n// Cache Reserve is designed for use WITH Tiered Cache\nconst configuration = {\n  tieredCache: 'enabled',    // Required for optimal performance\n  cacheReserve: 'enabled',   // Works best with Tiered Cache\n  \n  hierarchy: [\n    'Lower-Tier Cache (visitor)',\n    'Upper-Tier Cache (origin region)',\n    'Cache Reserve (persistent)',\n    'Origin'\n  ]\n};\n```\n\n### 2. Set Appropriate Cache-Control Headers\n\n```typescript\n// Origin response headers for Cache Reserve eligibility\nconst originHeaders = {\n  'Cache-Control': 'public, max-age=86400', // 24hr (minimum 10hr)\n  'Content-Length': '1024000', // Required\n  'Cache-Tag': 'images,product-123', // Optional: purging\n  'ETag': '\"abc123\"', // Optional: revalidation\n  // Avoid: 'Set-Cookie' and 'Vary: *' prevent caching\n};\n```\n\n### 3. Use Cache Rules for Fine-Grained Control\n\n```typescript\n// Different TTLs for different content types\nconst cacheRules = [\n  {\n    description: 'Long-term cache for immutable assets',\n    expression: '(http.request.uri.path matches \"^/static/.*\\\\.[a-f0-9]{8}\\\\.\")',\n    action_parameters: {\n      cache_reserve: { eligible: true },\n      edge_ttl: { mode: 'override_origin', default: 2592000 }, // 30 days\n      cache: true\n    }\n  },\n  {\n    description: 'Moderate cache for regular images',\n    expression: '(http.request.uri.path matches \"\\\\.(jpg|png|webp)$\")',\n    action_parameters: {\n      cache_reserve: { eligible: true },\n      edge_ttl: { mode: 'override_origin', default: 86400 }, // 24 hours\n      cache: true\n    }\n  },\n  {\n    description: 'Exclude API from Cache Reserve',\n    expression: '(http.request.uri.path matches \"^/api/\")',\n    action_parameters: { cache_reserve: { eligible: false }, cache: false }\n  }\n];\n```\n\n### 4. Making Assets Cache Reserve Eligible from Workers\n\n**Note**: This modifies response headers to meet eligibility criteria but does NOT directly control Cache Reserve storage (which is zone-level automatic).\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const response = await fetch(request);\n    if (!response.ok) return response;\n    \n    const headers = new Headers(response.headers);\n    headers.set('Cache-Control', 'public, max-age=36000'); // 10hr minimum\n    headers.delete('Set-Cookie'); // Blocks caching\n    \n    // Ensure Content-Length present\n    if (!headers.has('Content-Length')) {\n      const blob = await response.blob();\n      headers.set('Content-Length', blob.size.toString());\n      return new Response(blob, { status: response.status, headers });\n    }\n    \n    return new Response(response.body, { status: response.status, headers });\n  }\n};\n```\n\n### 5. Hostname Best Practices\n\nUse Worker's hostname for efficient caching - avoid overriding hostname unnecessarily.\n\n## Architecture Patterns\n\n### Multi-Tier Caching + Immutable Assets\n\n```typescript\n// Optimal: L1 (visitor) → L2 (region) → L3 (Cache Reserve) → Origin\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const url = new URL(request.url);\n    const isImmutable = /\\.[a-f0-9]{8,}\\.(js|css|jpg|png|woff2)$/.test(url.pathname);\n    const response = await fetch(request);\n    \n    if (isImmutable) {\n      const headers = new Headers(response.headers);\n      headers.set('Cache-Control', 'public, max-age=31536000, immutable');\n      return new Response(response.body, { status: response.status, headers });\n    }\n    return response;\n  }\n};\n```\n\n## Cost Optimization\n\n### Cost Calculator\n\n```typescript\ninterface CacheReserveEstimate {\n  avgAssetSizeGB: number;\n  uniqueAssets: number;\n  monthlyReads: number;\n  monthlyWrites: number;\n  originEgressCostPerGB: number; // e.g., AWS: $0.09/GB\n}\n\nfunction estimateMonthlyCost(input: CacheReserveEstimate) {\n  // Cache Reserve pricing\n  const storageCostPerGBMonth = 0.015;\n  const classAPerMillion = 4.50; // writes\n  const classBPerMillion = 0.36; // reads\n  \n  // Calculate Cache Reserve costs\n  const totalStorageGB = input.avgAssetSizeGB * input.uniqueAssets;\n  const storageCost = totalStorageGB * storageCostPerGBMonth;\n  const writeCost = (input.monthlyWrites / 1_000_000) * classAPerMillion;\n  const readCost = (input.monthlyReads / 1_000_000) * classBPerMillion;\n  \n  const cacheReserveCost = storageCost + writeCost + readCost;\n  \n  // Calculate origin egress cost (what you'd pay without Cache Reserve)\n  const totalTrafficGB = (input.monthlyReads * input.avgAssetSizeGB);\n  const originEgressCost = totalTrafficGB * input.originEgressCostPerGB;\n  \n  // Savings calculation\n  const savings = originEgressCost - cacheReserveCost;\n  const savingsPercent = ((savings / originEgressCost) * 100).toFixed(1);\n  \n  return {\n    cacheReserveCost: `$${cacheReserveCost.toFixed(2)}`,\n    originEgressCost: `$${originEgressCost.toFixed(2)}`,\n    monthlySavings: `$${savings.toFixed(2)}`,\n    savingsPercent: `${savingsPercent}%`,\n    breakdown: {\n      storage: `$${storageCost.toFixed(2)}`,\n      writes: `$${writeCost.toFixed(2)}`,\n      reads: `$${readCost.toFixed(2)}`,\n    }\n  };\n}\n\n// Example: Media library\nconst mediaLibrary = estimateMonthlyCost({\n  avgAssetSizeGB: 0.005, // 5MB images\n  uniqueAssets: 10_000,\n  monthlyReads: 5_000_000,\n  monthlyWrites: 50_000,\n  originEgressCostPerGB: 0.09, // AWS S3\n});\n\nconsole.log(mediaLibrary);\n// {\n//   cacheReserveCost: \"$9.98\",\n//   originEgressCost: \"$25.00\",\n//   monthlySavings: \"$15.02\",\n//   savingsPercent: \"60.1%\",\n//   breakdown: { storage: \"$0.75\", writes: \"$0.23\", reads: \"$9.00\" }\n// }\n```\n\n### Optimization Guidelines\n\n- **Set appropriate TTLs**: 10hr minimum, 24hr+ optimal for stable content, 30d max cautiously\n- **Cache high-value stable assets**: Images, media, fonts, archives, documentation\n- **Exclude frequently changing**: APIs, user-specific content, real-time data\n- **Compression note**: Cache Reserve fetches uncompressed from origin, serves compressed to visitors - factor in origin egress costs\n\n## See Also\n\n- [README](./README.md) - Overview and core concepts\n- [Configuration](./configuration.md) - Setup and Cache Rules\n- [API Reference](./api.md) - Purging and monitoring\n- [Gotchas](./gotchas.md) - Common issues and troubleshooting\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/containers/README.md",
    "content": "# Cloudflare Containers Skill Reference\n\n**APPLIES TO: Cloudflare Containers ONLY - NOT general Cloudflare Workers**\n\nUse when working with Cloudflare Containers: deploying containerized apps on Workers platform, configuring container-enabled Durable Objects, managing container lifecycle, or implementing stateful/stateless container patterns.\n\n## Beta Status\n\n⚠️ Containers is currently in **beta**. API may change without notice. No SLA guarantees. Custom instance types added Jan 2026.\n\n## Core Concepts\n\n**Container as Durable Object:** Each container is a Durable Object with persistent identity. Accessed via `getByName(id)` or `getRandom()`.\n\n**Image deployment:** Images pre-fetched globally. Deployments use rolling strategy (not instant like Workers).\n\n**Lifecycle:** cold start (2-3s) → running → `sleepAfter` timeout → stopped. No autoscaling - manual load balancing via `getRandom()`.\n\n**Persistent identity, ephemeral disk:** Container ID persists, but disk resets on stop. Use Durable Object storage for persistence.\n\n## Quick Start\n\n```typescript\nimport { Container } from \"@cloudflare/containers\";\n\nexport class MyContainer extends Container {\n  defaultPort = 8080;\n  sleepAfter = \"30m\";\n}\n\nexport default {\n  async fetch(request: Request, env: Env) {\n    const container = env.MY_CONTAINER.getByName(\"instance-1\");\n    await container.startAndWaitForPorts();\n    return container.fetch(request);\n  }\n};\n```\n\n## Reading Order\n\n| Task | Files |\n|------|-------|\n| Setup new container project | README → configuration.md |\n| Implement container logic | README → api.md → patterns.md |\n| Choose routing pattern | patterns.md (routing section) |\n| Debug issues | gotchas.md |\n| Production hardening | gotchas.md → patterns.md (lifecycle) |\n\n## Routing Decision Tree\n\n**How should requests reach containers?**\n\n- **Same user/session → same container:** Use `getByName(sessionId)` for session affinity\n- **Stateless, spread load:** Use `getRandom()` for load balancing\n- **Job per container:** Use `getByName(jobId)` + explicit lifecycle management\n- **Single global instance:** Use `getByName(\"singleton\")`\n\n## When to Use Containers vs Workers\n\n**Use Containers when:**\n- Need stateful, long-lived processes (sessions, WebSockets, games)\n- Running existing containerized apps (Node.js, Python, custom binaries)\n- Need filesystem access or specific system dependencies\n- Per-user/session isolation with dedicated compute\n\n**Use Workers when:**\n- Stateless HTTP handlers\n- Sub-millisecond cold starts required\n- Auto-scaling to zero critical\n- Simple request/response patterns\n\n## In This Reference\n\n- **[configuration.md](configuration.md)** - Wrangler config, instance types, Container class properties, environment variables, account limits\n- **[api.md](api.md)** - Container class API, startup methods, communication (HTTP/TCP/WebSocket), routing helpers, lifecycle hooks, scheduling, state inspection\n- **[patterns.md](patterns.md)** - Routing patterns (session affinity, load balancing, singleton), WebSocket forwarding, graceful shutdown, Workflow/Queue integration\n- **[gotchas.md](gotchas.md)** - Critical gotchas (WebSocket, startup methods), common errors with solutions, specific limits, beta caveats\n\n## See Also\n\n- [Durable Objects](../durable-objects/) - Containers extend Durable Objects\n- [Workflows](../workflows/) - Orchestrate container operations\n- [Queues](../queues/) - Trigger containers from queue messages\n- [Cloudflare Docs](https://developers.cloudflare.com/containers/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/containers/api.md",
    "content": "## Container Class API\n\n```typescript\nimport { Container } from \"@cloudflare/containers\";\n\nexport class MyContainer extends Container {\n  defaultPort = 8080;\n  requiredPorts = [8080];\n  sleepAfter = \"30m\";\n  enableInternet = true;\n  pingEndpoint = \"/health\";\n  envVars = {};\n  entrypoint = [];\n\n  onStart() { /* container started */ }\n  onStop() { /* container stopping */ }\n  onError(error: Error) { /* container error */ }\n  onActivityExpired(): boolean { /* timeout, return true to stay alive */ }\n  async alarm() { /* scheduled task */ }\n}\n```\n\n## Routing\n\n**getByName(id)** - Named instance for session affinity, per-user state\n**getRandom()** - Random instance for load balancing stateless services\n\n```typescript\nconst container = env.MY_CONTAINER.getByName(\"user-123\");\nconst container = env.MY_CONTAINER.getRandom();\n```\n\n## Startup Methods\n\n### start() - Basic start (8s timeout)\n\n```typescript\nawait container.start();\nawait container.start({ envVars: { KEY: \"value\" } });\n```\n\nReturns when **process starts**, NOT when ports ready. Use for fire-and-forget.\n\n### startAndWaitForPorts() - Recommended (20s timeout)\n\n```typescript\nawait container.startAndWaitForPorts();  // Uses requiredPorts\nawait container.startAndWaitForPorts({ ports: [8080, 9090] });\nawait container.startAndWaitForPorts({ \n  ports: [8080],\n  startOptions: { envVars: { KEY: \"value\" } }\n});\n```\n\nReturns when **ports listening**. Use before HTTP/TCP requests.\n\n**Port resolution:** explicit ports → requiredPorts → defaultPort → port 33\n\n### waitForPort() - Wait for specific port\n\n```typescript\nawait container.waitForPort(8080);\nawait container.waitForPort(8080, { timeout: 30000 });\n```\n\n## Communication\n\n### fetch() - HTTP with WebSocket support\n\n```typescript\n// ✅ Supports WebSocket upgrades\nconst response = await container.fetch(request);\nconst response = await container.fetch(\"http://container/api\", {\n  method: \"POST\",\n  body: JSON.stringify({ data: \"value\" })\n});\n```\n\n**Use for:** All HTTP, especially WebSocket.\n\n### containerFetch() - HTTP only (no WebSocket)\n\n```typescript\n// ❌ No WebSocket support\nconst response = await container.containerFetch(request);\n```\n\n**⚠️ Critical:** Use `fetch()` for WebSocket, not `containerFetch()`.\n\n### TCP Connections\n\n```typescript\nconst port = this.ctx.container.getTcpPort(8080);\nconst conn = port.connect();\nawait conn.opened;\n\nif (request.body) await request.body.pipeTo(conn.writable);\nreturn new Response(conn.readable);\n```\n\n### switchPort() - Change default port\n\n```typescript\nthis.switchPort(8081);  // Subsequent fetch() uses this port\n```\n\n## Lifecycle Hooks\n\n### onStart()\n\nCalled when container process starts (ports may not be ready). Runs in `blockConcurrencyWhile` - no concurrent requests.\n\n```typescript\nonStart() {\n  console.log(\"Container starting\");\n}\n```\n\n### onStop()\n\nCalled when SIGTERM received. 15 minutes until SIGKILL. Use for graceful shutdown.\n\n```typescript\nonStop() {\n  // Save state, close connections, flush logs\n}\n```\n\n### onError()\n\nCalled when container crashes or fails to start.\n\n```typescript\nonError(error: Error) {\n  console.error(\"Container error:\", error);\n}\n```\n\n### onActivityExpired()\n\nCalled when `sleepAfter` timeout reached. Return `true` to stay alive, `false` to stop.\n\n```typescript\nonActivityExpired(): boolean {\n  if (this.hasActiveConnections()) return true;  // Keep alive\n  return false;  // OK to stop\n}\n```\n\n## Scheduling\n\n```typescript\nexport class ScheduledContainer extends Container {\n  async fetch(request: Request) {\n    await this.schedule(Date.now() + 60000);  // 1 minute\n    await this.schedule(\"2026-01-28T00:00:00Z\");  // ISO string\n    return new Response(\"Scheduled\");\n  }\n\n  async alarm() {\n    // Called when schedule fires (SQLite-backed, survives restarts)\n  }\n}\n```\n\n**⚠️ Don't override `alarm()` directly when using `schedule()` helper.**\n\n## State Inspection\n\n### External state check\n\n```typescript\nconst state = await container.getState();\n// state.status: \"starting\" | \"running\" | \"stopping\" | \"stopped\"\n```\n\n### Internal state check\n\n```typescript\nexport class MyContainer extends Container {\n  async fetch(request: Request) {\n    if (this.ctx.container.running) { ... }\n  }\n}\n```\n\n**⚠️ Use `getState()` for external checks, `ctx.container.running` for internal.**\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/containers/configuration.md",
    "content": "## Wrangler Configuration\n\n### Basic Container Config\n\n```jsonc\n{\n  \"name\": \"my-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2026-01-10\",\n  \"containers\": [\n    {\n      \"class_name\": \"MyContainer\",\n      \"image\": \"./Dockerfile\",  // Path to Dockerfile or directory with Dockerfile\n      \"instance_type\": \"standard-1\",  // Predefined or custom (see below)\n      \"max_instances\": 10\n    }\n  ],\n  \"durable_objects\": {\n    \"bindings\": [\n      {\n        \"name\": \"MY_CONTAINER\",\n        \"class_name\": \"MyContainer\"\n      }\n    ]\n  },\n  \"migrations\": [\n    {\n      \"tag\": \"v1\",\n      \"new_sqlite_classes\": [\"MyContainer\"]  // Must use new_sqlite_classes\n    }\n  ]\n}\n```\n\nKey config requirements:\n- `image` - Path to Dockerfile or directory containing Dockerfile\n- `class_name` - Must match Container class export name\n- `max_instances` - Max concurrent container instances\n- Must configure Durable Objects binding AND migrations\n\n### Instance Types\n\n#### Predefined Types\n\n| Type | vCPU | Memory | Disk |\n|------|------|--------|------|\n| lite | 1/16 | 256 MiB | 2 GB |\n| basic | 1/4 | 1 GiB | 4 GB |\n| standard-1 | 1/2 | 4 GiB | 8 GB |\n| standard-2 | 1 | 6 GiB | 12 GB |\n| standard-3 | 2 | 8 GiB | 16 GB |\n| standard-4 | 4 | 12 GiB | 20 GB |\n\n```jsonc\n{\n  \"containers\": [\n    {\n      \"class_name\": \"MyContainer\",\n      \"image\": \"./Dockerfile\",\n      \"instance_type\": \"standard-2\"  // Use predefined type\n    }\n  ]\n}\n```\n\n#### Custom Types (Jan 2026 Feature)\n\n```jsonc\n{\n  \"containers\": [\n    {\n      \"class_name\": \"MyContainer\",\n      \"image\": \"./Dockerfile\",\n      \"instance_type_custom\": {\n        \"vcpu\": 2,              // 1-4 vCPU\n        \"memory_mib\": 8192,     // 512-12288 MiB (up to 12 GiB)\n        \"disk_mib\": 16384       // 2048-20480 MiB (up to 20 GB)\n      }\n    }\n  ]\n}\n```\n\n**Custom type constraints:**\n- Minimum 3 GiB memory per vCPU\n- Maximum 2 GB disk per 1 GiB memory\n- Max 4 vCPU, 12 GiB memory, 20 GB disk per container\n\n### Account Limits\n\n| Resource | Limit | Notes |\n|----------|-------|-------|\n| Total memory (all containers) | 400 GiB | Across all running containers |\n| Total vCPU (all containers) | 100 | Across all running containers |\n| Total disk (all containers) | 2 TB | Across all running containers |\n| Image storage per account | 50 GB | Stored container images |\n\n### Container Class Properties\n\n```typescript\nimport { Container } from \"@cloudflare/containers\";\n\nexport class MyContainer extends Container {\n  // Port Configuration\n  defaultPort = 8080;             // Default port for fetch() calls\n  requiredPorts = [8080, 9090];   // Ports to wait for in startAndWaitForPorts()\n\n  // Lifecycle\n  sleepAfter = \"30m\";             // Inactivity timeout (5m, 30m, 2h, etc.)\n\n  // Network\n  enableInternet = true;          // Allow outbound internet access\n\n  // Health Check\n  pingEndpoint = \"/health\";       // Health check endpoint path\n\n  // Environment\n  envVars = {                     // Environment variables passed to container\n    NODE_ENV: \"production\",\n    LOG_LEVEL: \"info\"\n  };\n\n  // Startup\n  entrypoint = [\"/bin/start.sh\"]; // Override image entrypoint (optional)\n}\n```\n\n**Property details:**\n\n- **`defaultPort`**: Port used when calling `container.fetch()` without explicit port. Falls back to port 33 if not set.\n\n- **`requiredPorts`**: Array of ports that must be listening before `startAndWaitForPorts()` returns. First port becomes default if `defaultPort` not set.\n\n- **`sleepAfter`**: Duration string (e.g., \"5m\", \"30m\", \"2h\"). Container stops after this period of inactivity. Timer resets on each request.\n\n- **`enableInternet`**: Boolean. If `true`, container can make outbound HTTP/TCP requests.\n\n- **`pingEndpoint`**: Path used for health checks. Should respond with 2xx status.\n\n- **`envVars`**: Object of environment variables. Merged with runtime-provided vars (see below).\n\n- **`entrypoint`**: Array of strings. Overrides container image's CMD/ENTRYPOINT.\n\n### Runtime Environment Variables\n\nCloudflare automatically provides these environment variables to containers:\n\n| Variable | Description |\n|----------|-------------|\n| `CLOUDFLARE_APPLICATION_ID` | Worker application ID |\n| `CLOUDFLARE_COUNTRY_A2` | Two-letter country code of request origin |\n| `CLOUDFLARE_LOCATION` | Cloudflare data center location |\n| `CLOUDFLARE_REGION` | Region identifier |\n| `CLOUDFLARE_DURABLE_OBJECT_ID` | Container's Durable Object ID |\n\nCustom `envVars` from Container class are merged with these. Custom vars override runtime vars if names conflict.\n\n### Image Management\n\n**Distribution model:** Images pre-fetched to all global locations before deployment. Ensures fast cold starts (2-3s typical).\n\n**Rolling deploys:** Unlike Workers (instant), container deployments roll out gradually. Old versions continue running during rollout.\n\n**Ephemeral disk:** Container disk is ephemeral and resets on each stop. Use Durable Object storage (`this.ctx.storage`) for persistence.\n\n## wrangler.toml Format\n\n```toml\nname = \"my-worker\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2026-01-10\"\n\n[[containers]]\nclass_name = \"MyContainer\"\nimage = \"./Dockerfile\"\ninstance_type = \"standard-2\"\nmax_instances = 10\n\n[[durable_objects.bindings]]\nname = \"MY_CONTAINER\"\nclass_name = \"MyContainer\"\n\n[[migrations]]\ntag = \"v1\"\nnew_sqlite_classes = [\"MyContainer\"]\n```\n\nBoth `wrangler.jsonc` and `wrangler.toml` are supported. Use `wrangler.jsonc` for comments and better IDE support.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/containers/gotchas.md",
    "content": "## Critical Gotchas\n\n### ⚠️ WebSocket: fetch() vs containerFetch()\n\n**Problem:** WebSocket connections fail silently\n\n**Cause:** `containerFetch()` doesn't support WebSocket upgrades\n\n**Fix:** Always use `fetch()` for WebSocket\n\n```typescript\n// ❌ WRONG\nreturn container.containerFetch(request);\n\n// ✅ CORRECT\nreturn container.fetch(request);\n```\n\n### ⚠️ startAndWaitForPorts() vs start()\n\n**Problem:** \"connection refused\" after `start()`\n\n**Cause:** `start()` returns when process starts, NOT when ports ready\n\n**Fix:** Use `startAndWaitForPorts()` before requests\n\n```typescript\n// ❌ WRONG\nawait container.start();\nreturn container.fetch(request);\n\n// ✅ CORRECT\nawait container.startAndWaitForPorts();\nreturn container.fetch(request);\n```\n\n### ⚠️ Activity Timeout on Long Operations\n\n**Problem:** Container stops during long work\n\n**Cause:** `sleepAfter` based on request activity, not internal work\n\n**Fix:** Renew timeout by touching storage\n\n```typescript\nconst interval = setInterval(() => {\n  this.ctx.storage.put(\"keepalive\", Date.now());\n}, 60000);\n\ntry {\n  await this.doLongWork(data);\n} finally {\n  clearInterval(interval);\n}\n```\n\n### ⚠️ blockConcurrencyWhile for Startup\n\n**Problem:** Race conditions during initialization\n\n**Fix:** Use `blockConcurrencyWhile` for atomic initialization\n\n```typescript\nawait this.ctx.blockConcurrencyWhile(async () => {\n  if (!this.initialized) {\n    await this.startAndWaitForPorts();\n    this.initialized = true;\n  }\n});\n```\n\n### ⚠️ Lifecycle Hooks Block Requests\n\n**Problem:** Container unresponsive during `onStart()`\n\n**Cause:** Hooks run in `blockConcurrencyWhile` - no concurrent requests\n\n**Fix:** Keep hooks fast, avoid long operations\n\n### ⚠️ Don't Override alarm() When Using schedule()\n\n**Problem:** Scheduled tasks don't execute\n\n**Cause:** `schedule()` uses `alarm()` internally\n\n**Fix:** Implement `alarm()` to handle scheduled tasks\n\n## Common Errors\n\n### \"Container start timeout\"\n\n**Cause:** Container took >8s (`start()`) or >20s (`startAndWaitForPorts()`)\n\n**Solutions:**\n- Optimize image (smaller base, fewer layers)\n- Check `entrypoint` correct\n- Verify app listens on correct ports\n- Increase timeout if needed\n\n### \"Port not available\"\n\n**Cause:** Calling `fetch()` before port ready\n\n**Solution:** Use `startAndWaitForPorts()`\n\n### \"Container memory exceeded\"\n\n**Cause:** Using more memory than instance type allows\n\n**Solutions:**\n- Use larger instance type (standard-2, standard-3, standard-4)\n- Optimize app memory usage\n- Use custom instance type\n\n```jsonc\n\"instance_type_custom\": {\n  \"vcpu\": 2,\n  \"memory_mib\": 8192\n}\n```\n\n### \"Max instances reached\"\n\n**Cause:** All `max_instances` slots in use\n\n**Solutions:**\n- Increase `max_instances`\n- Implement proper `sleepAfter`\n- Use `getRandom()` for distribution\n- Check for instance leaks\n\n### \"No container instance available\"\n\n**Cause:** Account capacity limits reached\n\n**Solutions:**\n- Check account limits\n- Review instance types across containers\n- Contact Cloudflare support\n\n## Limits\n\n| Resource | Limit | Notes |\n|----------|-------|-------|\n| Cold start | 2-3s | Image pre-fetched globally |\n| Graceful shutdown | 15 min | SIGTERM → SIGKILL |\n| `start()` timeout | 8s | Process start |\n| `startAndWaitForPorts()` timeout | 20s | Port ready |\n| Max vCPU per container | 4 | standard-4 or custom |\n| Max memory per container | 12 GiB | standard-4 or custom |\n| Max disk per container | 20 GB | Ephemeral, resets |\n| Account total memory | 400 GiB | All containers |\n| Account total vCPU | 100 | All containers |\n| Account total disk | 2 TB | All containers |\n| Image storage | 50 GB | Per account |\n| Disk persistence | None | Use DO storage |\n\n## Best Practices\n\n1. **Use `startAndWaitForPorts()` by default** - Prevents port errors\n2. **Set appropriate `sleepAfter`** - Balance resources vs cold starts\n3. **Use `fetch()` for WebSocket** - Not `containerFetch()`\n4. **Design for restarts** - Ephemeral disk, implement graceful shutdown\n5. **Monitor resources** - Stay within account limits\n6. **Keep hooks fast** - Run in `blockConcurrencyWhile`\n7. **Renew activity for long ops** - Touch storage to prevent timeout\n\n## Beta Caveats\n\n⚠️ Containers in **beta**:\n\n- **API may change** without notice\n- **No SLA** guarantees\n- **Limited regions** initially\n- **No autoscaling** - manual via `getRandom()`\n- **Rolling deploys** only (not instant like Workers)\n\nPlan for API changes, test thoroughly before production.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/containers/patterns.md",
    "content": "## Routing Patterns\n\n### Session Affinity (Stateful)\n\n```typescript\nexport class SessionBackend extends Container {\n  defaultPort = 3000;\n  sleepAfter = \"30m\";\n}\n\nexport default {\n  async fetch(request: Request, env: Env) {\n    const sessionId = request.headers.get(\"X-Session-ID\") || crypto.randomUUID();\n    const container = env.SESSION_BACKEND.getByName(sessionId);\n    await container.startAndWaitForPorts();\n    return container.fetch(request);\n  }\n};\n```\n\n**Use:** User sessions, WebSocket, stateful games, per-user caching.\n\n### Load Balancing (Stateless)\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env) {\n    const container = env.STATELESS_API.getRandom();\n    await container.startAndWaitForPorts();\n    return container.fetch(request);\n  }\n};\n```\n\n**Use:** Stateless HTTP APIs, CPU-intensive work, read-only queries.\n\n### Singleton Pattern\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env) {\n    const container = env.GLOBAL_SERVICE.getByName(\"singleton\");\n    await container.startAndWaitForPorts();\n    return container.fetch(request);\n  }\n};\n```\n\n**Use:** Global cache, centralized coordinator, single source of truth.\n\n## WebSocket Forwarding\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env) {\n    if (request.headers.get(\"Upgrade\") === \"websocket\") {\n      const sessionId = request.headers.get(\"X-Session-ID\") || crypto.randomUUID();\n      const container = env.WS_BACKEND.getByName(sessionId);\n      await container.startAndWaitForPorts();\n      \n      // ⚠️ MUST use fetch(), not containerFetch()\n      return container.fetch(request);\n    }\n    return new Response(\"Not a WebSocket request\", { status: 400 });\n  }\n};\n```\n\n**⚠️ Critical:** Always use `fetch()` for WebSocket.\n\n## Graceful Shutdown\n\n```typescript\nexport class GracefulContainer extends Container {\n  private connections = new Set<WebSocket>();\n\n  onStop() {\n    // SIGTERM received, 15 minutes until SIGKILL\n    for (const ws of this.connections) {\n      ws.close(1001, \"Server shutting down\");\n    }\n    this.ctx.storage.put(\"shutdown-time\", Date.now());\n  }\n\n  onActivityExpired(): boolean {\n    return this.connections.size > 0;  // Keep alive if connections\n  }\n}\n```\n\n## Concurrent Request Handling\n\n```typescript\nexport class SafeContainer extends Container {\n  private initialized = false;\n\n  async fetch(request: Request) {\n    await this.ctx.blockConcurrencyWhile(async () => {\n      if (!this.initialized) {\n        await this.startAndWaitForPorts();\n        this.initialized = true;\n      }\n    });\n    return super.fetch(request);\n  }\n}\n```\n\n**Use:** One-time initialization, preventing concurrent startup.\n\n## Activity Timeout Renewal\n\n```typescript\nexport class LongRunningContainer extends Container {\n  sleepAfter = \"5m\";\n\n  async processLongJob(data: unknown) {\n    const interval = setInterval(() => {\n      this.ctx.storage.put(\"keepalive\", Date.now());\n    }, 60000);\n\n    try {\n      await this.doLongWork(data);\n    } finally {\n      clearInterval(interval);\n    }\n  }\n}\n```\n\n**Use:** Long operations exceeding `sleepAfter`.\n\n## Multiple Port Routing\n\n```typescript\nexport class MultiPortContainer extends Container {\n  requiredPorts = [8080, 8081, 9090];\n\n  async fetch(request: Request) {\n    const path = new URL(request.url).pathname;\n    if (path.startsWith(\"/grpc\")) this.switchPort(8081);\n    else if (path.startsWith(\"/metrics\")) this.switchPort(9090);\n    return super.fetch(request);\n  }\n}\n```\n\n**Use:** Multi-protocol services (HTTP + gRPC), separate metrics endpoints.\n\n## Workflow Integration\n\n```typescript\nimport { WorkflowEntrypoint } from \"cloudflare:workers\";\n\nexport class ProcessingWorkflow extends WorkflowEntrypoint {\n  async run(event, step) {\n    const container = this.env.PROCESSOR.getByName(event.payload.jobId);\n    \n    await step.do(\"start\", async () => {\n      await container.startAndWaitForPorts();\n    });\n    \n    const result = await step.do(\"process\", async () => {\n      return container.fetch(\"/process\", {\n        method: \"POST\",\n        body: JSON.stringify(event.payload.data)\n      }).then(r => r.json());\n    });\n    \n    return result;\n  }\n}\n```\n\n**Use:** Orchestrating multi-step container operations, durable execution.\n\n## Queue Consumer Integration\n\n```typescript\nexport default {\n  async queue(batch, env) {\n    for (const msg of batch.messages) {\n      try {\n        const container = env.PROCESSOR.getByName(msg.body.jobId);\n        await container.startAndWaitForPorts();\n        \n        const response = await container.fetch(\"/process\", {\n          method: \"POST\",\n          body: JSON.stringify(msg.body)\n        });\n        \n        response.ok ? msg.ack() : msg.retry();\n      } catch (err) {\n        console.error(\"Queue processing error:\", err);\n        msg.retry();\n      }\n    }\n  }\n};\n```\n\n**Use:** Asynchronous job processing, batch operations, event-driven execution.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/cron-triggers/README.md",
    "content": "# Cloudflare Cron Triggers\n\nSchedule Workers execution using cron expressions. Runs on Cloudflare's global network during underutilized periods.\n\n## Key Features\n\n- **UTC-only execution** - All schedules run on UTC time\n- **5-field cron syntax** - Quartz scheduler extensions (L, W, #)\n- **Global propagation** - 15min deployment delay\n- **At-least-once delivery** - Rare duplicate executions possible\n- **Workflow integration** - Trigger long-running multi-step tasks\n- **Green Compute** - Optional carbon-aware scheduling during low-carbon periods\n\n## Cron Syntax\n\n```\n ┌─────────── minute (0-59)\n │ ┌───────── hour (0-23)\n │ │ ┌─────── day of month (1-31)\n │ │ │ ┌───── month (1-12, JAN-DEC)\n │ │ │ │ ┌─── day of week (1-7, SUN-SAT, 1=Sunday)\n * * * * *\n```\n\n**Special chars:** `*` (any), `,` (list), `-` (range), `/` (step), `L` (last), `W` (weekday), `#` (nth)\n\n## Common Schedules\n\n```bash\n*/5 * * * *        # Every 5 minutes\n0 * * * *          # Hourly\n0 2 * * *          # Daily 2am UTC (off-peak)\n0 9 * * MON-FRI    # Weekdays 9am UTC\n0 0 1 * *          # Monthly 1st midnight UTC\n0 9 L * *          # Last day of month 9am UTC\n0 10 * * MON#2     # 2nd Monday 10am UTC\n*/10 9-17 * * MON-FRI  # Every 10min, 9am-5pm weekdays\n```\n\n## Quick Start\n\n**wrangler.jsonc:**\n```jsonc\n{\n  \"name\": \"my-cron-worker\",\n  \"triggers\": {\n    \"crons\": [\"*/5 * * * *\", \"0 2 * * *\"]\n  }\n}\n```\n\n**Handler:**\n```typescript\nexport default {\n  async scheduled(\n    controller: ScheduledController,\n    env: Env,\n    ctx: ExecutionContext,\n  ): Promise<void> {\n    console.log(\"Cron:\", controller.cron);\n    console.log(\"Time:\", new Date(controller.scheduledTime));\n    \n    ctx.waitUntil(asyncTask(env)); // Non-blocking\n  },\n};\n```\n\n**Test locally:**\n```bash\nnpx wrangler dev\ncurl \"http://localhost:8787/__scheduled?cron=*/5+*+*+*+*\"\n```\n\n## Limits\n\n- **Free:** 3 triggers/worker, 10ms CPU\n- **Paid:** Unlimited triggers, 50ms CPU\n- **Propagation:** 15min global deployment\n- **Timezone:** UTC only\n\n## Reading Order\n\n**New to cron triggers?** Start here:\n1. This README - Overview and quick start\n2. [configuration.md](./configuration.md) - Set up your first cron trigger\n3. [api.md](./api.md) - Understand the handler API\n4. [patterns.md](./patterns.md) - Common use cases and examples\n\n**Troubleshooting?** Jump to [gotchas.md](./gotchas.md)\n\n## In This Reference\n- [configuration.md](./configuration.md) - wrangler config, env-specific schedules, Green Compute\n- [api.md](./api.md) - ScheduledController, noRetry(), waitUntil, testing patterns\n- [patterns.md](./patterns.md) - Use cases, monitoring, queue integration, Durable Objects\n- [gotchas.md](./gotchas.md) - Timezone issues, idempotency, security, testing\n\n## See Also\n- [workflows](../workflows/) - Alternative for long-running scheduled tasks\n- [workers](../workers/) - Worker runtime documentation\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/cron-triggers/api.md",
    "content": "# Cron Triggers API\n\n## Basic Handler\n\n```typescript\nexport default {\n  async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {\n    console.log(\"Cron executed:\", new Date(controller.scheduledTime));\n  },\n};\n```\n\n**JavaScript:** Same signature without types  \n**Python:** `class Default(WorkerEntrypoint): async def scheduled(self, controller, env, ctx)`\n\n## ScheduledController\n\n```typescript\ninterface ScheduledController {\n  scheduledTime: number;  // Unix ms when scheduled to run\n  cron: string;           // Expression that triggered (e.g., \"*/5 * * * *\")\n  type: string;           // Always \"scheduled\"\n  noRetry(): void;        // Prevent automatic retry on failure\n}\n```\n\n**Prevent retry on failure:**\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    try {\n      await riskyOperation(env);\n    } catch (error) {\n      // Don't retry - failure is expected/acceptable\n      controller.noRetry();\n      console.error(\"Operation failed, not retrying:\", error);\n    }\n  },\n};\n```\n\n**When to use noRetry():**\n- External API failures outside your control (avoid hammering failed services)\n- Rate limit errors (retry would fail again immediately)\n- Duplicate execution detected (idempotency check failed)\n- Non-critical operations where skip is acceptable (analytics, caching)\n- Validation errors that won't resolve on retry\n\n## Handler Parameters\n\n**`controller: ScheduledController`**\n- Access cron expression and scheduled time\n\n**`env: Env`**\n- All bindings: KV, R2, D1, secrets, service bindings\n\n**`ctx: ExecutionContext`**\n- `ctx.waitUntil(promise)` - Extend execution for async tasks (logging, cleanup, external APIs)\n- First `waitUntil` failure recorded in Cron Events\n\n## Multiple Schedules\n\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    switch (controller.cron) {\n      case \"*/3 * * * *\": ctx.waitUntil(updateRecentData(env)); break;\n      case \"0 * * * *\": ctx.waitUntil(processHourlyAggregation(env)); break;\n      case \"0 2 * * *\": ctx.waitUntil(performDailyMaintenance(env)); break;\n      default: console.warn(`Unhandled: ${controller.cron}`);\n    }\n  },\n};\n```\n\n## ctx.waitUntil Usage\n\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    const data = await fetchCriticalData(); // Critical path\n    \n    // Non-blocking background tasks\n    ctx.waitUntil(Promise.all([\n      logToAnalytics(data),\n      cleanupOldRecords(env.DB),\n      notifyWebhook(env.WEBHOOK_URL, data),\n    ]));\n  },\n};\n```\n\n## Workflow Integration\n\n```typescript\nimport { WorkflowEntrypoint } from \"cloudflare:workers\";\n\nexport class DataProcessingWorkflow extends WorkflowEntrypoint {\n  async run(event, step) {\n    const data = await step.do(\"fetch-data\", () => fetchLargeDataset());\n    const processed = await step.do(\"process-data\", () => processDataset(data));\n    await step.do(\"store-results\", () => storeResults(processed));\n  }\n}\n\nexport default {\n  async scheduled(controller, env, ctx) {\n    const instance = await env.MY_WORKFLOW.create({\n      params: { scheduledTime: controller.scheduledTime, cron: controller.cron },\n    });\n    console.log(`Started workflow: ${instance.id}`);\n  },\n};\n```\n\n## Testing Handler\n\n**Local development (/__scheduled endpoint):**\n```bash\n# Start dev server\nnpx wrangler dev\n\n# Trigger any cron\ncurl \"http://localhost:8787/__scheduled?cron=*/5+*+*+*+*\"\n\n# Trigger specific cron with custom time\ncurl \"http://localhost:8787/__scheduled?cron=0+2+*+*+*&scheduledTime=1704067200000\"\n```\n\n**Query parameters:**\n- `cron` - Required. URL-encoded cron expression (use `+` for spaces)\n- `scheduledTime` - Optional. Unix timestamp in milliseconds (defaults to current time)\n\n**Production security:** The `/__scheduled` endpoint is available in production and can be triggered by anyone. Block it or implement authentication - see [gotchas.md](./gotchas.md#security-concerns)\n\n**Unit testing (Vitest):**\n```typescript\n// test/scheduled.test.ts\nimport { describe, it, expect } from \"vitest\";\nimport { env } from \"cloudflare:test\";\nimport worker from \"../src/index\";\n\ndescribe(\"Scheduled Handler\", () => {\n  it(\"processes scheduled event\", async () => {\n    const controller = { scheduledTime: Date.now(), cron: \"*/5 * * * *\", type: \"scheduled\" as const, noRetry: () => {} };\n    const ctx = { waitUntil: (p: Promise<any>) => p, passThroughOnException: () => {} };\n    await worker.scheduled(controller, env, ctx);\n    expect(await env.MY_KV.get(\"last_run\")).toBeDefined();\n  });\n  \n  it(\"handles multiple crons\", async () => {\n    const ctx = { waitUntil: () => {}, passThroughOnException: () => {} };\n    await worker.scheduled({ scheduledTime: Date.now(), cron: \"*/5 * * * *\", type: \"scheduled\", noRetry: () => {} }, env, ctx);\n    expect(await env.MY_KV.get(\"last_type\")).toBe(\"frequent\");\n  });\n});\n```\n\n## Error Handling\n\n**Automatic retries:**\n- Failed cron executions are retried automatically unless `noRetry()` is called\n- Retry happens after a delay (typically minutes)\n- Only first `waitUntil()` failure is recorded in Cron Events\n\n**Best practices:**\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    try {\n      await criticalOperation(env);\n    } catch (error) {\n      // Log error details\n      console.error(\"Cron failed:\", {\n        cron: controller.cron,\n        scheduledTime: controller.scheduledTime,\n        error: error.message,\n        stack: error.stack,\n      });\n      \n      // Decide: retry or skip\n      if (error.message.includes(\"rate limit\")) {\n        controller.noRetry(); // Skip retry for rate limits\n      }\n      // Otherwise allow automatic retry\n      throw error;\n    }\n  },\n};\n```\n\n## See Also\n\n- [README.md](./README.md) - Overview\n- [patterns.md](./patterns.md) - Use cases, examples\n- [gotchas.md](./gotchas.md) - Common errors, testing issues\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/cron-triggers/configuration.md",
    "content": "# Cron Triggers Configuration\n\n## wrangler.jsonc\n\n```jsonc\n{\n  \"$schema\": \"./node_modules/wrangler/config-schema.json\",\n  \"name\": \"my-cron-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\", // Use current date for new projects\n  \n  \"triggers\": {\n    \"crons\": [\n      \"*/5 * * * *\",     // Every 5 minutes\n      \"0 */2 * * *\",     // Every 2 hours\n      \"0 9 * * MON-FRI\", // Weekdays at 9am UTC\n      \"0 2 1 * *\"        // Monthly on 1st at 2am UTC\n    ]\n  }\n}\n```\n\n## Green Compute (Beta)\n\nSchedule crons during low-carbon periods for carbon-aware execution:\n\n```jsonc\n{\n  \"name\": \"eco-cron-worker\",\n  \"triggers\": {\n    \"crons\": [\"0 2 * * *\"]\n  },\n  \"placement\": {\n    \"mode\": \"smart\"  // Runs during low-carbon periods\n  }\n}\n```\n\n**Modes:**\n- `\"smart\"` - Carbon-aware scheduling (may delay up to 24h for optimal window)\n- Default (no placement config) - Standard scheduling (no delay)\n\n**How it works:**\n- Cloudflare delays execution until grid carbon intensity is lower\n- Maximum delay: 24 hours from scheduled time\n- Ideal for batch jobs with flexible timing requirements\n\n**Use cases:** \n- Nightly data processing and ETL pipelines\n- Weekly/monthly report generation\n- Database backups and maintenance\n- Analytics aggregation\n- ML model training\n\n**Not suitable for:** \n- Time-sensitive operations (SLA requirements)\n- User-facing features requiring immediate execution\n- Real-time monitoring and alerting\n- Compliance tasks with strict time windows\n\n## Environment-Specific Schedules\n\n```jsonc\n{\n  \"name\": \"my-cron-worker\",\n  \"triggers\": {\n    \"crons\": [\"0 */6 * * *\"]  // Prod: every 6 hours\n  },\n  \"env\": {\n    \"staging\": {\n      \"triggers\": {\n        \"crons\": [\"*/15 * * * *\"]  // Staging: every 15min\n      }\n    },\n    \"dev\": {\n      \"triggers\": {\n        \"crons\": [\"*/5 * * * *\"]  // Dev: every 5min\n      }\n    }\n  }\n}\n```\n\n## Schedule Format\n\n**Structure:** `minute hour day-of-month month day-of-week`\n\n**Special chars:** `*` (any), `,` (list), `-` (range), `/` (step), `L` (last), `W` (weekday), `#` (nth)\n\n## Managing Triggers\n\n**Remove all:** `\"triggers\": { \"crons\": [] }`  \n**Preserve existing:** Omit `\"triggers\"` field entirely\n\n## Deployment\n\n```bash\n# Deploy with config crons\nnpx wrangler deploy\n\n# Deploy specific environment\nnpx wrangler deploy --env production\n\n# View deployments\nnpx wrangler deployments list\n```\n\n**⚠️ Changes take up to 15 minutes to propagate globally**\n\n## API Management\n\n**Get triggers:**\n```bash\ncurl \"https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules\" \\\n  -H \"Authorization: Bearer {api_token}\"\n```\n\n**Update triggers:**\n```bash\ncurl -X PUT \"https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules\" \\\n  -H \"Authorization: Bearer {api_token}\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"crons\": [\"*/5 * * * *\", \"0 2 * * *\"]}'\n```\n\n**Delete all:**\n```bash\ncurl -X PUT \"https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules\" \\\n  -H \"Authorization: Bearer {api_token}\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"crons\": []}'\n```\n\n## Combining Multiple Workers\n\nFor complex schedules, use multiple workers:\n\n```jsonc\n// worker-frequent.jsonc\n{\n  \"name\": \"data-sync-frequent\",\n  \"triggers\": { \"crons\": [\"*/5 * * * *\"] }\n}\n\n// worker-daily.jsonc\n{\n  \"name\": \"reports-daily\",\n  \"triggers\": { \"crons\": [\"0 2 * * *\"] },\n  \"placement\": { \"mode\": \"smart\" }\n}\n\n// worker-weekly.jsonc\n{\n  \"name\": \"cleanup-weekly\",\n  \"triggers\": { \"crons\": [\"0 3 * * SUN\"] }\n}\n```\n\n**Benefits:**\n- Separate CPU limits per worker\n- Independent error isolation\n- Different Green Compute policies\n- Easier to maintain and debug\n\n## Validation\n\n**Test cron syntax:**\n- [crontab.guru](https://crontab.guru/) - Interactive validator\n- Wrangler validates on deploy but won't catch logic errors\n\n**Common mistakes:**\n- `0 0 * * *` runs daily at midnight UTC, not your local timezone\n- `*/60 * * * *` is invalid (use `0 * * * *` for hourly)\n- `0 2 31 * *` only runs on months with 31 days\n\n## See Also\n\n- [README.md](./README.md) - Overview, quick start\n- [api.md](./api.md) - Handler implementation\n- [patterns.md](./patterns.md) - Multi-cron routing examples\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/cron-triggers/gotchas.md",
    "content": "# Cron Triggers Gotchas\n\n## Common Errors\n\n### \"Timezone Issues\"\n\n**Problem:** Cron runs at wrong time relative to local timezone  \n**Cause:** All crons execute in UTC, no local timezone support  \n**Solution:** Convert local time to UTC manually\n\n**Conversion formula:** `utcHour = (localHour - utcOffset + 24) % 24`\n\n**Examples:**\n- 9am PST (UTC-8) → `(9 - (-8) + 24) % 24 = 17` → `0 17 * * *`\n- 2am EST (UTC-5) → `(2 - (-5) + 24) % 24 = 7` → `0 7 * * *`\n- 6pm JST (UTC+9) → `(18 - 9 + 24) % 24 = 33 % 24 = 9` → `0 9 * * *`\n\n**Daylight Saving Time:** Adjust manually when DST changes, or schedule at times unaffected by DST (e.g., 2am-4am local time usually safe)\n\n### \"Cron Not Executing\"\n\n**Cause:** Missing `scheduled()` export, invalid syntax, propagation delay (<15min), or plan limits  \n**Solution:** Verify export exists, validate at crontab.guru, wait 15+ min after deploy, check plan limits\n\n### \"Duplicate Executions\"\n\n**Cause:** At-least-once delivery  \n**Solution:** Track execution IDs in KV - see idempotency pattern below\n\n### \"Execution Failures\"\n\n**Cause:** CPU exceeded, unhandled exceptions, network timeouts, binding errors  \n**Solution:** Use try-catch, AbortController timeouts, `ctx.waitUntil()` for long ops, or Workflows for heavy tasks\n\n### \"Local Testing Not Working\"\n\n**Problem:** `/__scheduled` endpoint returns 404 or doesn't trigger handler  \n**Cause:** Missing `scheduled()` export, wrangler not running, or incorrect endpoint format  \n**Solution:**\n\n1. Verify `scheduled()` is exported:\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    console.log(\"Cron triggered\");\n  },\n};\n```\n\n2. Start dev server:\n```bash\nnpx wrangler dev\n```\n\n3. Use correct endpoint format (URL-encode spaces as `+`):\n```bash\n# Correct\ncurl \"http://localhost:8787/__scheduled?cron=*/5+*+*+*+*\"\n\n# Wrong (will fail)\ncurl \"http://localhost:8787/__scheduled?cron=*/5 * * * *\"\n```\n\n4. Update Wrangler if outdated:\n```bash\nnpm install -g wrangler@latest\n```\n\n### \"waitUntil() Tasks Not Completing\"\n\n**Problem:** Background tasks in `ctx.waitUntil()` fail silently or don't execute  \n**Cause:** Promises rejected without error handling, or handler returns before promise settles  \n**Solution:** Always await or handle errors in waitUntil promises:\n\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    // BAD: Silent failures\n    ctx.waitUntil(riskyOperation());\n    \n    // GOOD: Explicit error handling\n    ctx.waitUntil(\n      riskyOperation().catch(err => {\n        console.error(\"Background task failed:\", err);\n        return logError(err, env);\n      })\n    );\n  },\n};\n```\n\n### \"Idempotency Issues\"\n\n**Problem:** At-least-once delivery causes duplicate side effects (double charges, duplicate emails)  \n**Cause:** No deduplication mechanism  \n**Solution:** Use KV to track execution IDs:\n\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    const executionId = `${controller.cron}-${controller.scheduledTime}`;\n    const existing = await env.EXECUTIONS.get(executionId);\n    \n    if (existing) {\n      console.log(\"Already executed, skipping\");\n      controller.noRetry();\n      return;\n    }\n    \n    await env.EXECUTIONS.put(executionId, \"1\", { expirationTtl: 86400 }); // 24h TTL\n    await performIdempotentOperation(env);\n  },\n};\n```\n\n### \"Security Concerns\"\n\n**Problem:** `__scheduled` endpoint exposed in production allows unauthorized cron triggering  \n**Cause:** Testing endpoint available in deployed Workers  \n**Solution:** Block `__scheduled` in production:\n\n```typescript\nexport default {\n  async fetch(request, env, ctx) {\n    const url = new URL(request.url);\n    \n    // Block __scheduled in production\n    if (url.pathname === \"/__scheduled\" && env.ENVIRONMENT === \"production\") {\n      return new Response(\"Not Found\", { status: 404 });\n    }\n    \n    return handleRequest(request, env, ctx);\n  },\n  \n  async scheduled(controller, env, ctx) {\n    // Your cron logic\n  },\n};\n```\n\n**Also:** Use `env.API_KEY` for secrets (never hardcode)\n\n**Alternative:** Add middleware to verify request origin:\n```typescript\nexport default {\n  async fetch(request, env, ctx) {\n    const url = new URL(request.url);\n    \n    if (url.pathname === \"/__scheduled\") {\n      // Check Cloudflare headers to verify internal request\n      const cfRay = request.headers.get(\"cf-ray\");\n      if (!cfRay && env.ENVIRONMENT === \"production\") {\n        return new Response(\"Not Found\", { status: 404 });\n      }\n    }\n    \n    return handleRequest(request, env, ctx);\n  },\n  \n  async scheduled(controller, env, ctx) {\n    // Your cron logic\n  },\n};\n```\n\n## Limits & Quotas\n\n| Limit | Free | Paid | Notes |\n|-------|------|------|-------|\n| Triggers per Worker | 3 | Unlimited | Maximum cron schedules per Worker |\n| CPU time | 10ms | 50ms | May need `ctx.waitUntil()` or Workflows |\n| Execution guarantee | At-least-once | At-least-once | Duplicates possible - use idempotency |\n| Propagation delay | Up to 15 minutes | Up to 15 minutes | Time for changes to take effect globally |\n| Min interval | 1 minute | 1 minute | Cannot schedule more frequently |\n| Cron accuracy | ±1 minute | ±1 minute | Execution may drift slightly |\n\n## Testing Best Practices\n\n**Unit tests:**\n- Mock `ScheduledController`, `ExecutionContext`, and bindings\n- Test each cron expression separately\n- Verify `noRetry()` is called when expected\n- Use Vitest with `@cloudflare/vitest-pool-workers` for realistic env\n\n**Integration tests:**\n- Test via `/__scheduled` endpoint in dev environment\n- Verify idempotency logic with duplicate `scheduledTime` values\n- Test error handling and retry behavior\n\n**Production:** Start with long intervals (`*/30 * * * *`), monitor Cron Events for 24h, set up alerts before reducing interval\n\n## Resources\n\n- [Cron Triggers Docs](https://developers.cloudflare.com/workers/configuration/cron-triggers/)\n- [Scheduled Handler API](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/)\n- [Cloudflare Workflows](https://developers.cloudflare.com/workflows/)\n- [Workers Limits](https://developers.cloudflare.com/workers/platform/limits/)\n- [Crontab Guru](https://crontab.guru/) - Validator\n- [Vitest Pool Workers](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/cron-triggers/patterns.md",
    "content": "# Cron Triggers Patterns\n\n## API Data Sync\n\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    const response = await fetch(\"https://api.example.com/data\", {headers: { \"Authorization\": `Bearer ${env.API_KEY}` }});\n    if (!response.ok) throw new Error(`API error: ${response.status}`);\n    ctx.waitUntil(env.MY_KV.put(\"cached_data\", JSON.stringify(await response.json()), {expirationTtl: 3600}));\n  },\n};\n```\n\n## Database Cleanup\n\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    const result = await env.DB.prepare(`DELETE FROM sessions WHERE expires_at < datetime('now')`).run();\n    console.log(`Deleted ${result.meta.changes} expired sessions`);\n    ctx.waitUntil(env.DB.prepare(\"VACUUM\").run());\n  },\n};\n```\n\n## Report Generation\n\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    const startOfWeek = new Date(); startOfWeek.setDate(startOfWeek.getDate() - 7);\n    const { results } = await env.DB.prepare(`SELECT date, revenue, orders FROM daily_stats WHERE date >= ? ORDER BY date`).bind(startOfWeek.toISOString()).all();\n    const report = {period: \"weekly\", totalRevenue: results.reduce((sum, d) => sum + d.revenue, 0), totalOrders: results.reduce((sum, d) => sum + d.orders, 0), dailyBreakdown: results};\n    const reportKey = `reports/weekly-${Date.now()}.json`;\n    await env.REPORTS_BUCKET.put(reportKey, JSON.stringify(report));\n    ctx.waitUntil(env.SEND_EMAIL.fetch(\"https://example.com/send\", {method: \"POST\", body: JSON.stringify({to: \"team@example.com\", subject: \"Weekly Report\", reportUrl: `https://reports.example.com/${reportKey}`})}));\n  },\n};\n```\n\n## Health Checks\n\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    const services = [{name: \"API\", url: \"https://api.example.com/health\"}, {name: \"CDN\", url: \"https://cdn.example.com/health\"}];\n    const checks = await Promise.all(services.map(async (service) => {\n      const start = Date.now();\n      try {\n        const response = await fetch(service.url, { signal: AbortSignal.timeout(5000) });\n        return {name: service.name, status: response.ok ? \"up\" : \"down\", responseTime: Date.now() - start};\n      } catch (error) {\n        return {name: service.name, status: \"down\", responseTime: Date.now() - start, error: error.message};\n      }\n    }));\n    ctx.waitUntil(env.STATUS_KV.put(\"health_status\", JSON.stringify(checks)));\n    const failures = checks.filter(c => c.status === \"down\");\n    if (failures.length > 0) ctx.waitUntil(fetch(env.ALERT_WEBHOOK, {method: \"POST\", body: JSON.stringify({text: `${failures.length} service(s) down: ${failures.map(f => f.name).join(\", \")}`})}));\n  },\n};\n```\n\n## Batch Processing (Rate-Limited)\n\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    const queueData = await env.QUEUE_KV.get(\"pending_items\", \"json\");\n    if (!queueData || queueData.length === 0) return;\n    const batch = queueData.slice(0, 100);\n    const results = await Promise.allSettled(batch.map(item => fetch(\"https://api.example.com/process\", {method: \"POST\", headers: {\"Authorization\": `Bearer ${env.API_KEY}`, \"Content-Type\": \"application/json\"}, body: JSON.stringify(item)})));\n    console.log(`Processed ${results.filter(r => r.status === \"fulfilled\").length}/${batch.length} items`);\n    ctx.waitUntil(env.QUEUE_KV.put(\"pending_items\", JSON.stringify(queueData.slice(100))));\n  },\n};\n```\n\n## Queue Integration\n\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    const batch = await env.MY_QUEUE.receive({ batchSize: 100 });\n    const results = await Promise.allSettled(batch.messages.map(async (msg) => {\n      await processMessage(msg.body, env);\n      await msg.ack();\n    }));\n    console.log(`Processed ${results.filter(r => r.status === \"fulfilled\").length}/${batch.messages.length}`);\n  },\n};\n```\n\n## Monitoring & Observability\n\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    const startTime = Date.now();\n    const meta = { cron: controller.cron, scheduledTime: controller.scheduledTime };\n    console.log(\"[START]\", meta);\n    try {\n      const result = await performTask(env);\n      console.log(\"[SUCCESS]\", { ...meta, duration: Date.now() - startTime, count: result.count });\n      ctx.waitUntil(env.METRICS.put(`cron:${controller.scheduledTime}`, JSON.stringify({ ...meta, status: \"success\" }), { expirationTtl: 2592000 }));\n    } catch (error) {\n      console.error(\"[ERROR]\", { ...meta, duration: Date.now() - startTime, error: error.message });\n      ctx.waitUntil(fetch(env.ALERT_WEBHOOK, { method: \"POST\", body: JSON.stringify({ text: `Cron failed: ${controller.cron}`, error: error.message }) }));\n      throw error;\n    }\n  },\n};\n```\n\n**View logs:** `npx wrangler tail` or Dashboard → Workers & Pages → Worker → Logs\n\n## Durable Objects Coordination\n\n```typescript\nexport default {\n  async scheduled(controller, env, ctx) {\n    const stub = env.COORDINATOR.get(env.COORDINATOR.idFromName(\"cron-lock\"));\n    const acquired = await stub.tryAcquireLock(controller.scheduledTime);\n    if (!acquired) {\n      controller.noRetry();\n      return;\n    }\n    try {\n      await performTask(env);\n    } finally {\n      await stub.releaseLock();\n    }\n  },\n};\n```\n\n## Python Handler\n\n```python\nfrom workers import WorkerEntrypoint\n\nclass Default(WorkerEntrypoint):\n    async def scheduled(self, controller, env, ctx):\n        data = await env.MY_KV.get(\"key\")\n        ctx.waitUntil(env.DB.execute(\"DELETE FROM logs WHERE created_at < datetime('now', '-7 days')\"))\n```\n\n## Testing Patterns\n\n**Local testing with /__scheduled:**\n```bash\n# Start dev server\nnpx wrangler dev\n\n# Test specific cron\ncurl \"http://localhost:8787/__scheduled?cron=*/5+*+*+*+*\"\n\n# Test with specific time\ncurl \"http://localhost:8787/__scheduled?cron=0+2+*+*+*&scheduledTime=1704067200000\"\n```\n\n**Unit tests:**\n```typescript\n// test/scheduled.test.ts\nimport { describe, it, expect, vi } from \"vitest\";\nimport { env } from \"cloudflare:test\";\nimport worker from \"../src/index\";\n\ndescribe(\"Scheduled Handler\", () => {\n  it(\"executes cron\", async () => {\n    const controller = { scheduledTime: Date.now(), cron: \"*/5 * * * *\", type: \"scheduled\" as const, noRetry: vi.fn() };\n    const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn() };\n    await worker.scheduled(controller, env, ctx);\n    expect(await env.MY_KV.get(\"last_run\")).toBeDefined();\n  });\n  \n  it(\"calls noRetry on duplicate\", async () => {\n    const controller = { scheduledTime: 1704067200000, cron: \"0 2 * * *\", type: \"scheduled\" as const, noRetry: vi.fn() };\n    await env.EXECUTIONS.put(\"0 2 * * *-1704067200000\", \"1\");\n    await worker.scheduled(controller, env, { waitUntil: vi.fn(), passThroughOnException: vi.fn() });\n    expect(controller.noRetry).toHaveBeenCalled();\n  });\n});\n```\n\n## See Also\n\n- [README.md](./README.md) - Overview\n- [api.md](./api.md) - Handler implementation\n- [gotchas.md](./gotchas.md) - Troubleshooting\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/d1/README.md",
    "content": "# Cloudflare D1 Database\n\nExpert guidance for Cloudflare D1, a serverless SQLite database designed for horizontal scale-out across multiple databases.\n\n## Overview\n\nD1 is Cloudflare's managed, serverless database with:\n- SQLite SQL semantics and compatibility\n- Built-in disaster recovery via Time Travel (30-day point-in-time recovery)\n- Horizontal scale-out architecture (10 GB per database)\n- Worker and HTTP API access\n- Pricing based on query and storage costs only\n\n**Architecture Philosophy**: D1 is optimized for per-user, per-tenant, or per-entity database patterns rather than single large databases.\n\n## Quick Start\n\n```bash\n# Create database\nwrangler d1 create <database-name>\n\n# Execute migration\nwrangler d1 migrations apply <db-name> --remote\n\n# Local development\nwrangler dev\n```\n\n## Core Query Methods\n\n```typescript\n// .all() - Returns all rows; .first() - First row or null; .first(col) - Single column value\n// .run() - INSERT/UPDATE/DELETE; .raw() - Array of arrays (efficient)\nconst { results, success, meta } = await env.DB.prepare('SELECT * FROM users WHERE active = ?').bind(true).all();\nconst user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();\n```\n\n## Batch Operations\n\n```typescript\n// Multiple queries in single round trip (atomic transaction)\nconst results = await env.DB.batch([\n  env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(1),\n  env.DB.prepare('SELECT * FROM posts WHERE author_id = ?').bind(1),\n  env.DB.prepare('UPDATE users SET last_access = ? WHERE id = ?').bind(Date.now(), 1)\n]);\n```\n\n## Sessions API (Paid Plans)\n\n```typescript\n// Create long-running session for analytics/migrations (up to 15 minutes)\nconst session = env.DB.withSession();\ntry {\n  await session.prepare('CREATE INDEX idx_heavy ON large_table(column)').run();\n  await session.prepare('ANALYZE').run();\n} finally {\n  session.close(); // Always close to release resources\n}\n```\n\n## Read Replication (Paid Plans)\n\n```typescript\n// Read from nearest replica for lower latency (automatic failover)\nconst user = await env.DB_REPLICA.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();\n\n// Writes always go to primary\nawait env.DB.prepare('UPDATE users SET last_login = ? WHERE id = ?').bind(Date.now(), userId).run();\n```\n\n## Platform Limits\n\n| Limit | Free Tier | Paid Plans |\n|-------|-----------|------------|\n| Database size | 500 MB | 10 GB per database |\n| Row size | 1 MB max | 1 MB max |\n| Query timeout | 30 seconds | 30 seconds |\n| Batch size | 1,000 statements | 10,000 statements |\n| Time Travel retention | 7 days | 30 days |\n| Read replicas | Not available | Yes (paid add-on) |\n\n**Pricing**: $5/month per database beyond free tier + $0.001 per 1K reads + $1 per 1M writes + $0.75/GB storage/month\n\n## CLI Commands\n\n```bash\n# Database management\nwrangler d1 create <db-name>\nwrangler d1 list\nwrangler d1 delete <db-name>\n\n# Migrations\nwrangler d1 migrations create <db-name> <migration-name>    # Create new migration file\nwrangler d1 migrations apply <db-name> --remote             # Apply pending migrations\nwrangler d1 migrations apply <db-name> --local              # Apply locally\nwrangler d1 migrations list <db-name> --remote              # Show applied migrations\n\n# Direct SQL execution\nwrangler d1 execute <db-name> --remote --command=\"SELECT * FROM users\"\nwrangler d1 execute <db-name> --local --file=./schema.sql\n\n# Backups & Import/Export\nwrangler d1 export <db-name> --remote --output=./backup.sql  # Full export with schema\nwrangler d1 export <db-name> --remote --no-schema --output=./data.sql  # Data only\nwrangler d1 time-travel restore <db-name> --timestamp=\"2024-01-15T14:30:00Z\"  # Point-in-time recovery\n\n# Development\nwrangler dev --persist-to=./.wrangler/state\n```\n\n## Reading Order\n\n**Start here**: Quick Start above → configuration.md (setup) → api.md (queries)\n\n**Common tasks**:\n- First time setup: configuration.md → Run migrations\n- Adding queries: api.md → Prepared statements\n- Pagination/caching: patterns.md\n- Production optimization: Read Replication + Sessions API (this file)\n- Debugging: gotchas.md\n\n## In This Reference\n\n- [configuration.md](./configuration.md) - wrangler.jsonc setup, migrations, TypeScript types, ORMs, local dev\n- [api.md](./api.md) - Query methods (.all/.first/.run/.raw), batch, sessions, read replicas, error handling\n- [patterns.md](./patterns.md) - Pagination, bulk operations, caching, multi-tenant, sessions, analytics\n- [gotchas.md](./gotchas.md) - SQL injection, limits by plan tier, performance, common errors\n\n## See Also\n\n- [workers](../workers/) - Worker runtime and fetch handler patterns\n- [hyperdrive](../hyperdrive/) - Connection pooling for external databases\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/d1/api.md",
    "content": "# D1 API Reference\n\n## Prepared Statements (Required for Security)\n\n```typescript\n// ❌ NEVER: Direct string interpolation (SQL injection risk)\nconst result = await env.DB.prepare(`SELECT * FROM users WHERE id = ${userId}`).all();\n\n// ✅ CORRECT: Prepared statements with bind()\nconst result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).all();\n\n// Multiple parameters\nconst result = await env.DB.prepare('SELECT * FROM users WHERE email = ? AND active = ?').bind(email, true).all();\n```\n\n## Query Execution Methods\n\n```typescript\n// .all() - Returns all rows\nconst { results, success, meta } = await env.DB.prepare('SELECT * FROM users WHERE active = ?').bind(true).all();\n// results: Array of row objects; success: boolean\n// meta: { duration: number, rows_read: number, rows_written: number }\n\n// .first() - Returns first row or null\nconst user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();\n\n// .first(columnName) - Returns single column value\nconst email = await env.DB.prepare('SELECT email FROM users WHERE id = ?').bind(userId).first('email');\n// Returns string | number | null\n\n// .run() - For INSERT/UPDATE/DELETE (no row data returned)\nconst result = await env.DB.prepare('UPDATE users SET last_login = ? WHERE id = ?').bind(Date.now(), userId).run();\n// result.meta: { duration, rows_read, rows_written, last_row_id, changes }\n\n// .raw() - Returns array of arrays (efficient for large datasets)\nconst rawResults = await env.DB.prepare('SELECT id, name FROM users').raw();\n// [[1, 'Alice'], [2, 'Bob']]\n```\n\n## Batch Operations\n\n```typescript\n// Execute multiple queries in single round trip (atomic transaction)\nconst results = await env.DB.batch([\n  env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(1),\n  env.DB.prepare('SELECT * FROM posts WHERE author_id = ?').bind(1),\n  env.DB.prepare('UPDATE users SET last_access = ? WHERE id = ?').bind(Date.now(), 1)\n]);\n// results is array: [result1, result2, result3]\n\n// Batch with same prepared statement, different params\nconst userIds = [1, 2, 3];\nconst stmt = env.DB.prepare('SELECT * FROM users WHERE id = ?');\nconst results = await env.DB.batch(userIds.map(id => stmt.bind(id)));\n```\n\n## Transactions (via batch)\n\n```typescript\n// D1 executes batch() as atomic transaction - all succeed or all fail\nconst results = await env.DB.batch([\n  env.DB.prepare('INSERT INTO accounts (id, balance) VALUES (?, ?)').bind(1, 100),\n  env.DB.prepare('INSERT INTO accounts (id, balance) VALUES (?, ?)').bind(2, 200),\n  env.DB.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?').bind(50, 1),\n  env.DB.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?').bind(50, 2)\n]);\n```\n\n## Sessions API (Paid Plans)\n\nLong-running sessions for operations exceeding 30s timeout (up to 15 min).\n\n```typescript\nconst session = env.DB.withSession({ timeout: 600 }); // 10 min (1-900s)\ntry {\n  await session.prepare('CREATE INDEX idx_large ON big_table(column)').run();\n  await session.prepare('ANALYZE').run();\n} finally {\n  session.close(); // CRITICAL: always close to prevent leaks\n}\n```\n\n**Use cases**: Migrations, ANALYZE, large index creation, bulk transformations\n\n## Read Replication (Paid Plans)\n\nRoutes queries to nearest replica for lower latency. Writes always go to primary.\n\n```typescript\ninterface Env {\n  DB: D1Database;          // Primary (writes)\n  DB_REPLICA: D1Database;  // Replica (reads)\n}\n\n// Reads: use replica\nconst user = await env.DB_REPLICA.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();\n\n// Writes: use primary\nawait env.DB.prepare('UPDATE users SET last_login = ? WHERE id = ?').bind(Date.now(), userId).run();\n\n// Read-after-write: use primary for consistency (replication lag <100ms-2s)\nawait env.DB.prepare('INSERT INTO posts (title) VALUES (?)').bind(title).run();\nconst post = await env.DB.prepare('SELECT * FROM posts WHERE title = ?').bind(title).first(); // Primary\n```\n\n## Error Handling\n\n```typescript\nasync function getUser(userId: number, env: Env): Promise<Response> {\n  try {\n    const result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).all();\n    if (!result.success) return new Response('Database error', { status: 500 });\n    if (result.results.length === 0) return new Response('User not found', { status: 404 });\n    return Response.json(result.results[0]);\n  } catch (error) {\n    return new Response('Internal error', { status: 500 });\n  }\n}\n\n// Constraint violations\ntry {\n  await env.DB.prepare('INSERT INTO users (email, name) VALUES (?, ?)').bind(email, name).run();\n} catch (error) {\n  if (error.message?.includes('UNIQUE constraint failed')) return new Response('Email exists', { status: 409 });\n  throw error;\n}\n```\n\n## REST API (HTTP) Access\n\nAccess D1 from external services (non-Worker contexts) using Cloudflare API.\n\n```typescript\n// Single query\nconst response = await fetch(\n  `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/d1/database/${DATABASE_ID}/query`,\n  {\n    method: 'POST',\n    headers: {\n      'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,\n      'Content-Type': 'application/json'\n    },\n    body: JSON.stringify({\n      sql: 'SELECT * FROM users WHERE id = ?',\n      params: [userId]\n    })\n  }\n);\n\nconst { result, success, errors } = await response.json();\n// result: [{ results: [...], success: true, meta: {...} }]\n\n// Batch queries via HTTP\nconst response = await fetch(\n  `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/d1/database/${DATABASE_ID}/query`,\n  {\n    method: 'POST',\n    headers: {\n      'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,\n      'Content-Type': 'application/json'\n    },\n    body: JSON.stringify([\n      { sql: 'SELECT * FROM users WHERE id = ?', params: [1] },\n      { sql: 'SELECT * FROM posts WHERE author_id = ?', params: [1] }\n    ])\n  }\n);\n```\n\n**Use cases**: Server-side scripts, CI/CD migrations, administrative tools, non-Worker integrations\n\n## Testing & Debugging\n\n```typescript\n// Vitest with unstable_dev\nimport { unstable_dev } from 'wrangler';\ndescribe('D1', () => {\n  let worker: Awaited<ReturnType<typeof unstable_dev>>;\n  beforeAll(async () => { worker = await unstable_dev('src/index.ts'); });\n  afterAll(async () => { await worker.stop(); });\n  it('queries users', async () => { expect((await worker.fetch('/users')).status).toBe(200); });\n});\n\n// Debug query performance\nconst result = await env.DB.prepare('SELECT * FROM users').all();\nconsole.log('Duration:', result.meta.duration, 'ms');\n\n// Query plan analysis\nconst plan = await env.DB.prepare('EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?').bind(email).all();\n```\n\n```bash\n# Inspect local database\nsqlite3 .wrangler/state/v3/d1/<database-id>.sqlite\n.tables; .schema users; PRAGMA table_info(users);\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/d1/configuration.md",
    "content": "# D1 Configuration\n\n## wrangler.jsonc Setup\n\n```jsonc\n{\n  \"name\": \"your-worker-name\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\", // Use current date for new projects\n  \"d1_databases\": [\n    {\n      \"binding\": \"DB\",                    // Env variable name\n      \"database_name\": \"your-db-name\",    // Human-readable name\n      \"database_id\": \"your-database-id\",  // UUID from dashboard/CLI\n      \"migrations_dir\": \"migrations\"      // Optional: default is \"migrations\"\n    },\n    // Read replica (paid plans only)\n    {\n      \"binding\": \"DB_REPLICA\",\n      \"database_name\": \"your-db-name\",\n      \"database_id\": \"your-database-id\"   // Same ID, different binding\n    },\n    // Multiple databases\n    {\n      \"binding\": \"ANALYTICS_DB\",\n      \"database_name\": \"analytics-db\",\n      \"database_id\": \"yyy-yyy-yyy\"\n    }\n  ]\n}\n```\n\n## TypeScript Types\n\n```typescript\ninterface Env { DB: D1Database; ANALYTICS_DB?: D1Database; }\n\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n    const result = await env.DB.prepare('SELECT * FROM users').all();\n    return Response.json(result.results);\n  }\n}\n```\n\n## Migrations\n\nFile structure: `migrations/0001_initial_schema.sql`, `0002_add_posts.sql`, etc.\n\n### Example Migration\n\n```sql\n-- migrations/0001_initial_schema.sql\nCREATE TABLE IF NOT EXISTS users (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  email TEXT UNIQUE NOT NULL,\n  name TEXT NOT NULL,\n  created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n  updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX idx_users_email ON users(email);\n\nCREATE TABLE IF NOT EXISTS posts (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  user_id INTEGER NOT NULL,\n  title TEXT NOT NULL,\n  content TEXT,\n  published BOOLEAN DEFAULT 0,\n  created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE\n);\n\nCREATE INDEX idx_posts_user_id ON posts(user_id);\nCREATE INDEX idx_posts_published ON posts(published);\n```\n\n### Running Migrations\n\n```bash\n# Create new migration file\nwrangler d1 migrations create <db-name> add_users_table\n# Creates: migrations/0001_add_users_table.sql\n\n# Apply migrations\nwrangler d1 migrations apply <db-name> --local     # Apply to local DB\nwrangler d1 migrations apply <db-name> --remote    # Apply to production DB\n\n# List applied migrations\nwrangler d1 migrations list <db-name> --remote\n\n# Direct SQL execution (bypasses migration tracking)\nwrangler d1 execute <db-name> --remote --command=\"SELECT * FROM users\"\nwrangler d1 execute <db-name> --local --file=./schema.sql\n```\n\n**Migration tracking**: Wrangler creates `d1_migrations` table automatically to track applied migrations\n\n## Indexing Strategy\n\n```sql\n-- Index frequently queried columns\nCREATE INDEX idx_users_email ON users(email);\n\n-- Composite indexes for multi-column queries\nCREATE INDEX idx_posts_user_published ON posts(user_id, published);\n\n-- Covering indexes (include queried columns)\nCREATE INDEX idx_users_email_name ON users(email, name);\n\n-- Partial indexes for filtered queries\nCREATE INDEX idx_active_users ON users(email) WHERE active = 1;\n\n-- Check if query uses index\nEXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?;\n```\n\n## Drizzle ORM\n\n```typescript\n// drizzle.config.ts\nexport default {\n  schema: './src/schema.ts', out: './migrations', dialect: 'sqlite', driver: 'd1-http',\n  dbCredentials: { accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, databaseId: process.env.D1_DATABASE_ID!, token: process.env.CLOUDFLARE_API_TOKEN! }\n} satisfies Config;\n\n// schema.ts\nimport { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';\nexport const users = sqliteTable('users', {\n  id: integer('id').primaryKey({ autoIncrement: true }),\n  email: text('email').notNull().unique(),\n  name: text('name').notNull()\n});\n\n// worker.ts\nimport { drizzle } from 'drizzle-orm/d1';\nimport { users } from './schema';\nexport default {\n  async fetch(request: Request, env: Env) {\n    const db = drizzle(env.DB);\n    return Response.json(await db.select().from(users));\n  }\n}\n```\n\n## Import & Export\n\n```bash\n# Export full database (schema + data)\nwrangler d1 export <db-name> --remote --output=./backup.sql\n\n# Export data only (no schema)\nwrangler d1 export <db-name> --remote --no-schema --output=./data-only.sql\n\n# Export with foreign key constraints preserved\n# (Default: foreign keys are disabled during export for import compatibility)\n\n# Import SQL file\nwrangler d1 execute <db-name> --remote --file=./backup.sql\n\n# Limitations\n# - BLOB data may not export correctly (use R2 for binary files)\n# - Very large exports (>1GB) may timeout (split into chunks)\n# - Import is NOT atomic (use batch() for transactional imports in Workers)\n```\n\n## Plan Tiers\n\n| Feature | Free | Paid |\n|---------|------|------|\n| Database size | 500 MB | 10 GB |\n| Batch size | 1,000 statements | 10,000 statements |\n| Time Travel | 7 days | 30 days |\n| Read replicas | ❌ | ✅ |\n| Sessions API | ❌ | ✅ (up to 15 min) |\n| Pricing | Free | $5/mo + usage |\n\n**Usage pricing** (paid plans): $0.001 per 1K reads + $1 per 1M writes + $0.75/GB storage/month\n\n## Local Development\n\n```bash\nwrangler dev --persist-to=./.wrangler/state  # Persist across restarts\n# Local DB: .wrangler/state/v3/d1/<database-id>.sqlite\nsqlite3 .wrangler/state/v3/d1/<database-id>.sqlite  # Inspect\n\n# Local dev uses free tier limits by default\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/d1/gotchas.md",
    "content": "# D1 Gotchas & Troubleshooting\n\n## Common Errors\n\n### \"SQL Injection Vulnerability\"\n\n**Cause:** Using string interpolation instead of prepared statements with bind()  \n**Solution:** ALWAYS use prepared statements: `env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).all()` instead of string interpolation which allows attackers to inject malicious SQL\n\n### \"no such table\"\n\n**Cause:** Table doesn't exist because migrations haven't been run, or using wrong database binding  \n**Solution:** Run migrations using `wrangler d1 migrations apply <db-name> --remote` and verify binding name in wrangler.jsonc matches code\n\n### \"UNIQUE constraint failed\"\n\n**Cause:** Attempting to insert duplicate value in column with UNIQUE constraint  \n**Solution:** Catch error and return 409 Conflict status code\n\n### \"Query Timeout (30s exceeded)\"\n\n**Cause:** Query execution exceeds 30 second timeout limit  \n**Solution:** Break into smaller queries, add indexes to speed up queries, or reduce dataset size\n\n### \"N+1 Query Problem\"\n\n**Cause:** Making multiple individual queries in a loop instead of single optimized query  \n**Solution:** Use JOIN to fetch related data in single query or use `batch()` method for multiple queries\n\n### \"Missing Indexes\"\n\n**Cause:** Queries performing full table scans without indexes  \n**Solution:** Use `EXPLAIN QUERY PLAN` to check if index is used, then create index with `CREATE INDEX idx_users_email ON users(email)`\n\n### \"Boolean Type Issues\"\n\n**Cause:** SQLite uses INTEGER (0/1) not native boolean type  \n**Solution:** Bind 1 or 0 instead of true/false when working with boolean values\n\n### \"Date/Time Type Issues\"\n\n**Cause:** SQLite doesn't have native DATE/TIME types  \n**Solution:** Use TEXT (ISO 8601 format) or INTEGER (unix timestamp) for date/time values\n\n## Plan Tier Limits\n\n| Limit | Free Tier | Paid Plans | Notes |\n|-------|-----------|------------|-------|\n| Database size | 500 MB | 10 GB | Design for multiple DBs per tenant on paid |\n| Row size | 1 MB | 1 MB | Store large files in R2, not D1 |\n| Query timeout | 30s | 30s (900s with sessions) | Use sessions API for migrations |\n| Batch size | 1,000 statements | 10,000 statements | Split large batches accordingly |\n| Time Travel | 7 days | 30 days | Point-in-time recovery window |\n| Read replicas | ❌ Not available | ✅ Available | Paid add-on for lower latency |\n| Sessions API | ❌ Not available | ✅ Up to 15 min | For migrations and heavy operations |\n| Concurrent requests | 10,000/min | Higher | Contact support for custom limits |\n\n## Production Gotchas\n\n### \"Batch size exceeded\"\n\n**Cause:** Attempting to send >1,000 statements on free tier or >10,000 on paid  \n**Solution:** Chunk batches: `for (let i = 0; i < stmts.length; i += MAX_BATCH) await env.DB.batch(stmts.slice(i, i + MAX_BATCH))`\n\n### \"Session not closed / resource leak\"\n\n**Cause:** Forgot to call `session.close()` after using sessions API  \n**Solution:** Always use try/finally block: `try { await session.prepare(...) } finally { session.close() }`\n\n### \"Replication lag causing stale reads\"\n\n**Cause:** Reading from replica immediately after write - replication lag can be 100ms-2s  \n**Solution:** Use primary for read-after-write: `await env.DB.prepare(...)` not `env.DB_REPLICA`\n\n### \"Migration applied to local but not remote\"\n\n**Cause:** Forgot `--remote` flag when applying migrations  \n**Solution:** Always run `wrangler d1 migrations apply <db-name> --remote` for production\n\n### \"Foreign key constraint failed\"\n\n**Cause:** Inserting row with FK to non-existent parent, or deleting parent before children  \n**Solution:** Enable FK enforcement: `PRAGMA foreign_keys = ON;` and use ON DELETE CASCADE in schema\n\n### \"BLOB data corrupted on export\"\n\n**Cause:** D1 export may not handle BLOB correctly  \n**Solution:** Store binary files in R2, only store R2 URLs/keys in D1\n\n### \"Database size approaching limit\"\n\n**Cause:** Storing too much data in single database  \n**Solution:** Horizontal scale-out: create per-tenant/per-user databases, archive old data, or upgrade to paid plan\n\n### \"Local dev vs production behavior differs\"\n\n**Cause:** Local uses SQLite file, production uses distributed D1 - different performance/limits  \n**Solution:** Always test migrations on remote with `--remote` flag before production rollout\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/d1/patterns.md",
    "content": "# D1 Patterns & Best Practices\n\n## Pagination\n\n```typescript\nasync function getUsers({ page, pageSize }: { page: number; pageSize: number }, env: Env) {\n  const offset = (page - 1) * pageSize;\n  const [countResult, dataResult] = await env.DB.batch([\n    env.DB.prepare('SELECT COUNT(*) as total FROM users'),\n    env.DB.prepare('SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?').bind(pageSize, offset)\n  ]);\n  return { data: dataResult.results, total: countResult.results[0].total, page, pageSize, totalPages: Math.ceil(countResult.results[0].total / pageSize) };\n}\n```\n\n## Conditional Queries\n\n```typescript\nasync function searchUsers(filters: { name?: string; email?: string; active?: boolean }, env: Env) {\n  const conditions: string[] = [], params: (string | number | boolean | null)[] = [];\n  if (filters.name) { conditions.push('name LIKE ?'); params.push(`%${filters.name}%`); }\n  if (filters.email) { conditions.push('email = ?'); params.push(filters.email); }\n  if (filters.active !== undefined) { conditions.push('active = ?'); params.push(filters.active ? 1 : 0); }\n  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n  return await env.DB.prepare(`SELECT * FROM users ${whereClause}`).bind(...params).all();\n}\n```\n\n## Bulk Insert\n\n```typescript\nasync function bulkInsertUsers(users: Array<{ name: string; email: string }>, env: Env) {\n  const stmt = env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)');\n  const batch = users.map(user => stmt.bind(user.name, user.email));\n  return await env.DB.batch(batch);\n}\n```\n\n## Caching with KV\n\n```typescript\nasync function getCachedUser(userId: number, env: { DB: D1Database; CACHE: KVNamespace }) {\n  const cacheKey = `user:${userId}`;\n  const cached = await env.CACHE?.get(cacheKey, 'json');\n  if (cached) return cached;\n  const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();\n  if (user) await env.CACHE?.put(cacheKey, JSON.stringify(user), { expirationTtl: 300 });\n  return user;\n}\n```\n\n## Query Optimization\n\n```typescript\n// ✅ Use indexes in WHERE clauses\nconst users = await env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(email).all();\n\n// ✅ Limit result sets\nconst recentPosts = await env.DB.prepare('SELECT * FROM posts ORDER BY created_at DESC LIMIT 100').all();\n\n// ✅ Use batch() for multiple independent queries\nconst [user, posts, comments] = await env.DB.batch([\n  env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId),\n  env.DB.prepare('SELECT * FROM posts WHERE user_id = ?').bind(userId),\n  env.DB.prepare('SELECT * FROM comments WHERE user_id = ?').bind(userId)\n]);\n\n// ❌ Avoid N+1 queries\nfor (const post of posts) {\n  const author = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(post.user_id).first(); // Bad: multiple round trips\n}\n\n// ✅ Use JOINs instead\nconst postsWithAuthors = await env.DB.prepare(`\n  SELECT posts.*, users.name as author_name\n  FROM posts\n  JOIN users ON posts.user_id = users.id\n`).all();\n```\n\n## Multi-Tenant SaaS\n\n```typescript\n// Each tenant gets own database\nexport default {\n  async fetch(request: Request, env: { [key: `TENANT_${string}`]: D1Database }) {\n    const tenantId = request.headers.get('X-Tenant-ID');\n    const data = await env[`TENANT_${tenantId}`].prepare('SELECT * FROM records').all();\n    return Response.json(data.results);\n  }\n}\n```\n\n## Session Storage\n\n```typescript\nasync function createSession(userId: number, token: string, env: Env) {\n  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();\n  return await env.DB.prepare('INSERT INTO sessions (user_id, token, expires_at) VALUES (?, ?, ?)').bind(userId, token, expiresAt).run();\n}\n\nasync function validateSession(token: string, env: Env) {\n  return await env.DB.prepare('SELECT s.*, u.email FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.token = ? AND s.expires_at > CURRENT_TIMESTAMP').bind(token).first();\n}\n```\n\n## Analytics/Events\n\n```typescript\nasync function logEvent(event: { type: string; userId?: number; metadata: object }, env: Env) {\n  return await env.DB.prepare('INSERT INTO events (type, user_id, metadata) VALUES (?, ?, ?)').bind(event.type, event.userId || null, JSON.stringify(event.metadata)).run();\n}\n\nasync function getEventStats(startDate: string, endDate: string, env: Env) {\n  return await env.DB.prepare('SELECT type, COUNT(*) as count FROM events WHERE timestamp BETWEEN ? AND ? GROUP BY type ORDER BY count DESC').bind(startDate, endDate).all();\n}\n```\n\n## Read Replication Pattern (Paid Plans)\n\n```typescript\ninterface Env { DB: D1Database; DB_REPLICA: D1Database; }\n\nexport default {\n  async fetch(request: Request, env: Env) {\n    if (request.method === 'GET') {\n      // Reads: use replica for lower latency\n      const users = await env.DB_REPLICA.prepare('SELECT * FROM users WHERE active = 1').all();\n      return Response.json(users.results);\n    }\n    \n    if (request.method === 'POST') {\n      const { name, email } = await request.json();\n      const result = await env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind(name, email).run();\n      \n      // Read-after-write: use primary for consistency (replication lag <100ms-2s)\n      const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(result.meta.last_row_id).first();\n      return Response.json(user, { status: 201 });\n    }\n  }\n}\n```\n\n**Use replicas for**: Analytics dashboards, search results, public queries (eventual consistency OK)  \n**Use primary for**: Read-after-write, financial transactions, authentication (consistency required)\n\n## Sessions API Pattern (Paid Plans)\n\n```typescript\n// Migration with long-running session (up to 15 min)\nasync function runMigration(env: Env) {\n  const session = env.DB.withSession({ timeout: 600 }); // 10 min\n  try {\n    await session.prepare('CREATE INDEX idx_users_email ON users(email)').run();\n    await session.prepare('CREATE INDEX idx_posts_user ON posts(user_id)').run();\n    await session.prepare('ANALYZE').run();\n  } finally {\n    session.close(); // Always close to prevent leaks\n  }\n}\n\n// Bulk transformation with batching\nasync function transformLargeDataset(env: Env) {\n  const session = env.DB.withSession({ timeout: 900 }); // 15 min max\n  try {\n    const BATCH_SIZE = 1000;\n    let offset = 0;\n    while (true) {\n      const rows = await session.prepare('SELECT id, data FROM legacy LIMIT ? OFFSET ?').bind(BATCH_SIZE, offset).all();\n      if (rows.results.length === 0) break;\n      const updates = rows.results.map(row => \n        session.prepare('UPDATE legacy SET new_data = ? WHERE id = ?').bind(transform(row.data), row.id)\n      );\n      await session.batch(updates);\n      offset += BATCH_SIZE;\n    }\n  } finally { session.close(); }\n}\n```\n\n## Time Travel & Backups\n\n```bash\nwrangler d1 time-travel restore <db-name> --timestamp=\"2024-01-15T14:30:00Z\"  # Point-in-time\nwrangler d1 time-travel info <db-name>  # List restore points (7 days free, 30 days paid)\nwrangler d1 export <db-name> --remote --output=./backup.sql  # Full export\nwrangler d1 export <db-name> --remote --no-schema --output=./data.sql  # Data only\nwrangler d1 execute <db-name> --remote --file=./backup.sql  # Import\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ddos/README.md",
    "content": "# Cloudflare DDoS Protection\n\nAutonomous, always-on protection against DDoS attacks across L3/4 and L7.\n\n## Protection Types\n\n- **HTTP DDoS (L7)**: Protects HTTP/HTTPS traffic, phase `ddos_l7`, zone/account level\n- **Network DDoS (L3/4)**: UDP/SYN/DNS floods, phase `ddos_l4`, account level only\n- **Adaptive DDoS**: Learns 7-day baseline, detects deviations, 4 profile types (Origins, User-Agents, Locations, Protocols)\n\n## Plan Availability\n\n| Feature | Free | Pro | Business | Enterprise | Enterprise Advanced |\n|---------|------|-----|----------|------------|---------------------|\n| HTTP DDoS (L7) | ✓ | ✓ | ✓ | ✓ | ✓ |\n| Network DDoS (L3/4) | ✓ | ✓ | ✓ | ✓ | ✓ |\n| Override rules | 1 | 1 | 1 | 1 | 10 |\n| Custom expressions | ✗ | ✗ | ✗ | ✗ | ✓ |\n| Log action | ✗ | ✗ | ✗ | ✗ | ✓ |\n| Adaptive DDoS | ✗ | ✗ | ✗ | ✓ | ✓ |\n| Alert filters | Basic | Basic | Basic | Advanced | Advanced |\n\n## Actions & Sensitivity\n\n- **Actions**: `block`, `managed_challenge`, `challenge`, `log` (Enterprise Advanced only)\n- **Sensitivity**: `default` (high), `medium`, `low`, `eoff` (essentially off)\n- **Override**: By category/tag or individual rule ID\n- **Scope**: Zone-level overrides take precedence over account-level\n\n## Reading Order\n\n| File | Purpose | Start Here If... |\n|------|---------|------------------|\n| [configuration.md](./configuration.md) | Dashboard setup, rule structure, adaptive profiles | You're setting up DDoS protection for the first time |\n| [api.md](./api.md) | API endpoints, SDK usage, ruleset ID discovery | You're automating configuration or need programmatic access |\n| [patterns.md](./patterns.md) | Protection strategies, defense-in-depth, dynamic response | You need implementation patterns or layered security |\n| [gotchas.md](./gotchas.md) | False positives, tuning, error handling | You're troubleshooting or optimizing existing protection |\n\n## See Also\n- [waf](../waf/) - Application-layer security rules\n- [bot-management](../bot-management/) - Bot detection and mitigation\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ddos/api.md",
    "content": "# DDoS API\n\n## Endpoints\n\n### HTTP DDoS (L7)\n\n```typescript\n// Zone-level\nPUT /zones/{zoneId}/rulesets/phases/ddos_l7/entrypoint\nGET /zones/{zoneId}/rulesets/phases/ddos_l7/entrypoint\n\n// Account-level (Enterprise Advanced)\nPUT /accounts/{accountId}/rulesets/phases/ddos_l7/entrypoint\nGET /accounts/{accountId}/rulesets/phases/ddos_l7/entrypoint\n```\n\n### Network DDoS (L3/4)\n\n```typescript\n// Account-level only\nPUT /accounts/{accountId}/rulesets/phases/ddos_l4/entrypoint\nGET /accounts/{accountId}/rulesets/phases/ddos_l4/entrypoint\n```\n\n## TypeScript SDK\n\n**SDK Version**: Requires `cloudflare` >= 3.0.0 for ruleset phase methods.\n\n```typescript\nimport Cloudflare from \"cloudflare\";\n\nconst client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN });\n\n// STEP 1: Discover managed ruleset ID (required for overrides)\nconst allRulesets = await client.rulesets.list({ zone_id: zoneId });\nconst ddosRuleset = allRulesets.result.find(\n  (r) => r.kind === \"managed\" && r.phase === \"ddos_l7\"\n);\nif (!ddosRuleset) throw new Error(\"DDoS managed ruleset not found\");\nconst managedRulesetId = ddosRuleset.id;\n\n// STEP 2: Get current HTTP DDoS configuration\nconst entrypointRuleset = await client.zones.rulesets.phases.entrypoint.get(\"ddos_l7\", {\n  zone_id: zoneId,\n});\n\n// STEP 3: Update HTTP DDoS ruleset with overrides\nawait client.zones.rulesets.phases.entrypoint.update(\"ddos_l7\", {\n  zone_id: zoneId,\n  rules: [\n    {\n      action: \"execute\",\n      expression: \"true\",\n      action_parameters: {\n        id: managedRulesetId, // From discovery step\n        overrides: {\n          sensitivity_level: \"medium\",\n          action: \"managed_challenge\",\n        },\n      },\n    },\n  ],\n});\n\n// Network DDoS (account level, L3/4)\nconst l4Rulesets = await client.rulesets.list({ account_id: accountId });\nconst l4DdosRuleset = l4Rulesets.result.find(\n  (r) => r.kind === \"managed\" && r.phase === \"ddos_l4\"\n);\nconst l4Ruleset = await client.accounts.rulesets.phases.entrypoint.get(\"ddos_l4\", {\n  account_id: accountId,\n});\n```\n\n## Alert Configuration\n\n```typescript\ninterface DDoSAlertConfig {\n  name: string;\n  enabled: boolean;\n  alert_type: \"http_ddos_attack_alert\" | \"layer_3_4_ddos_attack_alert\" \n    | \"advanced_http_ddos_attack_alert\" | \"advanced_layer_3_4_ddos_attack_alert\";\n  filters?: {\n    zones?: string[];\n    hostnames?: string[];\n    requests_per_second?: number;\n    packets_per_second?: number;\n    megabits_per_second?: number;\n    ip_prefixes?: string[]; // CIDR\n    ip_addresses?: string[];\n    protocols?: string[];\n  };\n  mechanisms: {\n    email?: Array<{ id: string }>;\n    webhooks?: Array<{ id: string }>;\n    pagerduty?: Array<{ id: string }>;\n  };\n}\n\n// Create alert\nawait fetch(\n  `https://api.cloudflare.com/client/v4/accounts/${accountId}/alerting/v3/policies`,\n  {\n    method: \"POST\",\n    headers: {\n      Authorization: `Bearer ${apiToken}`,\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify(alertConfig),\n  }\n);\n```\n\n## Typed Override Examples\n\n```typescript\n// Override by category\ninterface CategoryOverride {\n  action: \"execute\";\n  expression: string;\n  action_parameters: {\n    id: string;\n    overrides: {\n      categories?: Array<{\n        category: \"http-flood\" | \"http-anomaly\" | \"udp-flood\" | \"syn-flood\";\n        sensitivity_level?: \"default\" | \"medium\" | \"low\" | \"eoff\";\n        action?: \"block\" | \"managed_challenge\" | \"challenge\" | \"log\";\n      }>;\n    };\n  };\n}\n\n// Override by rule ID\ninterface RuleOverride {\n  action: \"execute\";\n  expression: string;\n  action_parameters: {\n    id: string;\n    overrides: {\n      rules?: Array<{\n        id: string;\n        action?: \"block\" | \"managed_challenge\" | \"challenge\" | \"log\";\n        sensitivity_level?: \"default\" | \"medium\" | \"low\" | \"eoff\";\n      }>;\n    };\n  };\n}\n\n// Example: Override specific adaptive rule\nconst adaptiveOverride: RuleOverride = {\n  action: \"execute\",\n  expression: \"true\",\n  action_parameters: {\n    id: managedRulesetId,\n    overrides: {\n      rules: [\n        { id: \"...adaptive-origins-rule-id...\", sensitivity_level: \"low\" },\n      ],\n    },\n  },\n};\n```\n\nSee [patterns.md](./patterns.md) for complete implementation patterns.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ddos/configuration.md",
    "content": "# DDoS Configuration\n\n## Dashboard Setup\n\n1. Navigate to Security > DDoS\n2. Select HTTP DDoS or Network-layer DDoS\n3. Configure sensitivity & action per ruleset/category/rule\n4. Apply overrides with optional expressions (Enterprise Advanced)\n5. Enable Adaptive DDoS toggle (Enterprise/Enterprise Advanced, requires 7 days traffic history)\n\n## Rule Structure\n\n```typescript\ninterface DDoSOverride {\n  description: string;\n  rules: Array<{\n    action: \"execute\";\n    expression: string; // Custom expression (Enterprise Advanced) or \"true\" for all\n    action_parameters: {\n      id: string; // Managed ruleset ID (discover via api.md)\n      overrides: {\n        sensitivity_level?: \"default\" | \"medium\" | \"low\" | \"eoff\";\n        action?: \"block\" | \"managed_challenge\" | \"challenge\" | \"log\"; // log = Enterprise Advanced only\n        categories?: Array<{\n          category: string; // e.g., \"http-flood\", \"udp-flood\"\n          sensitivity_level?: string;\n        }>;\n        rules?: Array<{\n          id: string;\n          action?: string;\n          sensitivity_level?: string;\n        }>;\n      };\n    };\n  }>;\n}\n```\n\n## Expression Availability\n\n| Plan | Custom Expressions | Example |\n|------|-------------------|---------|\n| Free/Pro/Business | ✗ | Use `\"true\"` only |\n| Enterprise | ✗ | Use `\"true\"` only |\n| Enterprise Advanced | ✓ | `ip.src in {...}`, `http.request.uri.path matches \"...\"` |\n\n## Sensitivity Mapping\n\n| UI | API | Threshold |\n|----|-----|-----------|\n| High | `default` | Most aggressive |\n| Medium | `medium` | Balanced |\n| Low | `low` | Less aggressive |\n| Essentially Off | `eoff` | Minimal mitigation |\n\n## Common Categories\n\n- `http-flood`, `http-anomaly` (L7)\n- `udp-flood`, `syn-flood`, `dns-flood` (L3/4)\n\n## Override Precedence\n\nMultiple override layers apply in this order (higher precedence wins):\n\n```\nZone-level > Account-level\nIndividual Rule > Category > Global sensitivity/action\n```\n\n**Example**: Zone rule for `/api/*` overrides account-level global settings.\n\n## Adaptive DDoS Profiles\n\n**Availability**: Enterprise, Enterprise Advanced  \n**Learning period**: 7 days of traffic history required\n\n| Profile Type | Description | Detects |\n|--------------|-------------|---------|\n| **Origins** | Traffic patterns per origin server | Anomalous requests to specific origins |\n| **User-Agents** | Traffic patterns per User-Agent | Malicious/anomalous user agent strings |\n| **Locations** | Traffic patterns per geo-location | Attacks from specific countries/regions |\n| **Protocols** | Traffic patterns per protocol (L3/4) | Protocol-specific flood attacks |\n\nConfigure by targeting specific adaptive rule IDs via API (see api.md#typed-override-examples).\n\n## Alerting\n\nConfigure via Notifications:\n- Alert types: `http_ddos_attack_alert`, `layer_3_4_ddos_attack_alert`, `advanced_*` variants\n- Filters: zones, hostnames, RPS/PPS/Mbps thresholds, IPs, protocols\n- Mechanisms: email, webhooks, PagerDuty\n\nSee [api.md](./api.md#alert-configuration) for API examples.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ddos/gotchas.md",
    "content": "# DDoS Gotchas\n\n## Common Errors\n\n### \"False positives blocking legitimate traffic\"\n\n**Cause**: Sensitivity too high, wrong action, or missing exceptions  \n**Solution**:\n1. Lower sensitivity for specific rule/category\n2. Use `log` action first to validate (Enterprise Advanced)\n3. Add exception with custom expression (e.g., allowlist IPs)\n4. Query flagged requests via GraphQL Analytics API to identify patterns\n\n### \"Attacks getting through\"\n\n**Cause**: Sensitivity too low or wrong action  \n**Solution**: Increase to `default` sensitivity and use `block` action:\n```typescript\nconst config = {\n  rules: [{\n    expression: \"true\",\n    action: \"execute\",\n    action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: \"default\", action: \"block\" } },\n  }],\n};\n```\n\n### \"Adaptive rules not working\"\n\n**Cause**: Insufficient traffic history (needs 7 days)  \n**Solution**: Wait for baseline to establish, check dashboard for adaptive rule status\n\n### \"Zone override ignored\"\n\n**Cause**: Account overrides conflict with zone overrides  \n**Solution**: Configure at zone level OR remove zone overrides to use account-level\n\n### \"Log action not available\"\n\n**Cause**: Not on Enterprise Advanced DDoS plan  \n**Solution**: Use `managed_challenge` with low sensitivity for testing\n\n### \"Rule limit exceeded\"\n\n**Cause**: Too many override rules (Free/Pro/Business: 1, Enterprise Advanced: 10)  \n**Solution**: Combine conditions in single expression using `and`/`or`\n\n### \"Cannot override rule\"\n\n**Cause**: Rule is read-only  \n**Solution**: Check API response for read-only indicator, use different rule\n\n### \"Cannot disable DDoS protection\"\n\n**Cause**: DDoS managed rulesets cannot be fully disabled (always-on protection)  \n**Solution**: Set `sensitivity_level: \"eoff\"` for minimal mitigation\n\n### \"Expression not allowed\"\n\n**Cause**: Custom expressions require Enterprise Advanced plan  \n**Solution**: Use `expression: \"true\"` for all traffic, or upgrade plan\n\n### \"Managed ruleset not found\"\n\n**Cause**: Zone/account doesn't have DDoS managed ruleset, or incorrect phase  \n**Solution**: Verify ruleset exists via `client.rulesets.list()`, check phase name (`ddos_l7` or `ddos_l4`)\n\n## API Error Codes\n\n| Error Code | Message | Cause | Solution |\n|------------|---------|-------|----------|\n| 10000 | Authentication error | Invalid/missing API token | Check token has DDoS permissions |\n| 81000 | Ruleset validation failed | Invalid rule structure | Verify `action_parameters.id` is managed ruleset ID |\n| 81020 | Expression not allowed | Custom expressions on wrong plan | Use `\"true\"` or upgrade to Enterprise Advanced |\n| 81021 | Rule limit exceeded | Too many override rules | Reduce rules or upgrade (Enterprise Advanced: 10) |\n| 81022 | Invalid sensitivity level | Wrong sensitivity value | Use: `default`, `medium`, `low`, `eoff` |\n| 81023 | Invalid action | Wrong action for plan | Enterprise Advanced only: `log` action |\n\n## Limits\n\n| Resource/Limit | Free/Pro/Business | Enterprise | Enterprise Advanced |\n|----------------|-------------------|------------|---------------------|\n| Override rules per zone | 1 | 1 | 10 |\n| Custom expressions | ✗ | ✗ | ✓ |\n| Log action | ✗ | ✗ | ✓ |\n| Adaptive DDoS | ✗ | ✓ | ✓ |\n| Traffic history required | - | 7 days | 7 days |\n\n## Tuning Strategy\n\n1. Start with `log` action + `medium` sensitivity\n2. Monitor for 24-48 hours\n3. Identify false positives, add exceptions\n4. Gradually increase to `default` sensitivity\n5. Change action from `log` → `managed_challenge` → `block`\n6. Document all adjustments\n\n## Best Practices\n\n- Test during low-traffic periods\n- Use zone-level for per-site tuning\n- Reference IP lists for easier management\n- Set appropriate alert thresholds (avoid noise)\n- Combine with WAF for layered defense\n- Avoid over-tuning (keep config simple)\n\nSee [patterns.md](./patterns.md) for progressive rollout examples.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/ddos/patterns.md",
    "content": "# DDoS Protection Patterns\n\n## Allowlist Trusted IPs\n\n```typescript\nconst config = {\n  description: \"Allowlist trusted IPs\",\n  rules: [{\n    expression: \"ip.src in { 203.0.113.0/24 192.0.2.1 }\",\n    action: \"execute\",\n    action_parameters: {\n      id: managedRulesetId,\n      overrides: { sensitivity_level: \"eoff\" },\n    },\n  }],\n};\n\nawait client.accounts.rulesets.phases.entrypoint.update(\"ddos_l7\", {\n  account_id: accountId,\n  ...config,\n});\n```\n\n## Route-specific Sensitivity\n\n```typescript\nconst config = {\n  description: \"Route-specific protection\",\n  rules: [\n    {\n      expression: \"not http.request.uri.path matches \\\"^/api/\\\"\",\n      action: \"execute\",\n      action_parameters: {\n        id: managedRulesetId,\n        overrides: { sensitivity_level: \"default\", action: \"block\" },\n      },\n    },\n    {\n      expression: \"http.request.uri.path matches \\\"^/api/\\\"\",\n      action: \"execute\",\n      action_parameters: {\n        id: managedRulesetId,\n        overrides: { sensitivity_level: \"low\", action: \"managed_challenge\" },\n      },\n    },\n  ],\n};\n```\n\n## Progressive Enhancement\n\n```typescript\nenum ProtectionLevel { MONITORING = \"monitoring\", LOW = \"low\", MEDIUM = \"medium\", HIGH = \"high\" }\n\nconst levelConfig = {\n  [ProtectionLevel.MONITORING]: { action: \"log\", sensitivity: \"eoff\" },\n  [ProtectionLevel.LOW]: { action: \"managed_challenge\", sensitivity: \"low\" },\n  [ProtectionLevel.MEDIUM]: { action: \"managed_challenge\", sensitivity: \"medium\" },\n  [ProtectionLevel.HIGH]: { action: \"block\", sensitivity: \"default\" },\n} as const;\n\nasync function setProtectionLevel(zoneId: string, level: ProtectionLevel, rulesetId: string, client: Cloudflare) {\n  const settings = levelConfig[level];\n  return client.zones.rulesets.phases.entrypoint.update(\"ddos_l7\", {\n    zone_id: zoneId,\n    rules: [{\n      expression: \"true\",\n      action: \"execute\",\n      action_parameters: { id: rulesetId, overrides: { action: settings.action, sensitivity_level: settings.sensitivity } },\n    }],\n  });\n}\n```\n\n## Dynamic Response to Attacks\n\n```typescript\ninterface Env { CLOUDFLARE_API_TOKEN: string; ZONE_ID: string; KV: KVNamespace; }\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    if (request.url.includes(\"/attack-detected\")) {\n      const attackData = await request.json();\n      await env.KV.put(`attack:${Date.now()}`, JSON.stringify(attackData), { expirationTtl: 86400 });\n      const recentAttacks = await getRecentAttacks(env.KV);\n      if (recentAttacks.length > 5) {\n        await setProtectionLevel(env.ZONE_ID, ProtectionLevel.HIGH, managedRulesetId, client);\n        return new Response(\"Protection increased\");\n      }\n    }\n    return new Response(\"OK\");\n  },\n  async scheduled(event: ScheduledEvent, env: Env): Promise<void> {\n    const recentAttacks = await getRecentAttacks(env.KV);\n    if (recentAttacks.length === 0) await setProtectionLevel(env.ZONE_ID, ProtectionLevel.MEDIUM, managedRulesetId, client);\n  },\n};\n```\n\n## Multi-rule Tiered Protection (Enterprise Advanced)\n\n```typescript\nconst config = {\n  description: \"Multi-tier DDoS protection\",\n  rules: [\n    {\n      expression: \"not ip.src in $known_ips and not cf.bot_management.score gt 30\",\n      action: \"execute\",\n      action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: \"default\", action: \"block\" } },\n    },\n    {\n      expression: \"cf.bot_management.verified_bot\",\n      action: \"execute\",\n      action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: \"medium\", action: \"managed_challenge\" } },\n    },\n    {\n      expression: \"ip.src in $trusted_ips\",\n      action: \"execute\",\n      action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: \"low\" } },\n    },\n  ],\n};\n```\n\n## Defense in Depth\n\nLayered security stack: DDoS + WAF + Rate Limiting + Bot Management.\n\n```typescript\n// Layer 1: DDoS (volumetric attacks)\nawait client.zones.rulesets.phases.entrypoint.update(\"ddos_l7\", {\n  zone_id: zoneId,\n  rules: [{ expression: \"true\", action: \"execute\", action_parameters: { id: ddosRulesetId, overrides: { sensitivity_level: \"medium\" } } }],\n});\n\n// Layer 2: WAF (exploit protection)\nawait client.zones.rulesets.phases.entrypoint.update(\"http_request_firewall_managed\", {\n  zone_id: zoneId,\n  rules: [{ expression: \"true\", action: \"execute\", action_parameters: { id: wafRulesetId } }],\n});\n\n// Layer 3: Rate Limiting (abuse prevention)\nawait client.zones.rulesets.phases.entrypoint.update(\"http_ratelimit\", {\n  zone_id: zoneId,\n  rules: [{ expression: \"http.request.uri.path eq \\\"/api/login\\\"\", action: \"block\", ratelimit: { characteristics: [\"ip.src\"], period: 60, requests_per_period: 5 } }],\n});\n\n// Layer 4: Bot Management (automation detection)\nawait client.zones.rulesets.phases.entrypoint.update(\"http_request_sbfm\", {\n  zone_id: zoneId,\n  rules: [{ expression: \"cf.bot_management.score lt 30\", action: \"managed_challenge\" }],\n});\n```\n\n## Cache Strategy for DDoS Mitigation\n\nExclude query strings from cache key to counter randomized query parameter attacks.\n\n```typescript\nconst cacheRule = {\n  expression: \"http.request.uri.path matches \\\"^/api/\\\"\",\n  action: \"set_cache_settings\",\n  action_parameters: {\n    cache: true,\n    cache_key: { ignore_query_strings_order: true, custom_key: { query_string: { exclude: { all: true } } } },\n  },\n};\n\nawait client.zones.rulesets.phases.entrypoint.update(\"http_request_cache_settings\", { zone_id: zoneId, rules: [cacheRule] });\n```\n\n**Rationale**: Attackers randomize query strings (`?random=123456`) to bypass cache. Excluding query params ensures cache hits absorb attack traffic.\n\nSee [configuration.md](./configuration.md) for rule structure details.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/do-storage/README.md",
    "content": "# Cloudflare Durable Objects Storage\n\nPersistent storage API for Durable Objects with SQLite and KV backends, PITR, and automatic concurrency control.\n\n## Overview\n\nDO Storage provides:\n- SQLite-backed (recommended) or KV-backed\n- SQL API + synchronous/async KV APIs\n- Automatic input/output gates (race-free)\n- 30-day point-in-time recovery (PITR)\n- Transactions and alarms\n\n**Use cases:** Stateful coordination, real-time collaboration, counters, sessions, rate limiters\n\n**Billing:** Charged by request, GB-month storage, and rowsRead/rowsWritten for SQL operations\n\n## Quick Start\n\n```typescript\nexport class Counter extends DurableObject {\n  sql: SqlStorage;\n  \n  constructor(ctx: DurableObjectState, env: Env) {\n    super(ctx, env);\n    this.sql = ctx.storage.sql;\n    this.sql.exec('CREATE TABLE IF NOT EXISTS data(key TEXT PRIMARY KEY, value INTEGER)');\n  }\n  \n  async increment(): Promise<number> {\n    const result = this.sql.exec(\n      'INSERT INTO data VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = value + 1 RETURNING value',\n      'counter', 1\n    ).one();\n    return result?.value || 1;\n  }\n}\n```\n\n## Storage Backends\n\n| Backend | Create Method | APIs | PITR |\n|---------|---------------|------|------|\n| SQLite (recommended) | `new_sqlite_classes` | SQL + sync KV + async KV | ✅ |\n| KV (legacy) | `new_classes` | async KV only | ❌ |\n\n## Core APIs\n\n- **SQL API** (`ctx.storage.sql`): Full SQLite with extensions (FTS5, JSON, math)\n- **Sync KV** (`ctx.storage.kv`): Synchronous key-value (SQLite only)\n- **Async KV** (`ctx.storage`): Asynchronous key-value (both backends)\n- **Transactions** (`transactionSync()`, `transaction()`)\n- **PITR** (`getBookmarkForTime()`, `onNextSessionRestoreBookmark()`)\n- **Alarms** (`setAlarm()`, `alarm()` handler)\n\n## Reading Order\n\n**New to DO storage:** configuration.md → api.md → patterns.md → gotchas.md  \n**Building features:** patterns.md → api.md → gotchas.md  \n**Debugging issues:** gotchas.md → api.md  \n**Writing tests:** testing.md\n\n## In This Reference\n\n- [configuration.md](./configuration.md) - wrangler.jsonc migrations, SQLite vs KV setup, RPC binding\n- [api.md](./api.md) - SQL exec/cursors, KV methods, storage options, transactions, alarms, PITR\n- [patterns.md](./patterns.md) - Schema migrations, caching, rate limiting, batch processing, parent-child coordination\n- [gotchas.md](./gotchas.md) - Concurrency gates, INTEGER precision, transaction rules, SQL limits\n- [testing.md](./testing.md) - vitest-pool-workers setup, testing DOs with SQL/alarms/PITR\n\n## See Also\n\n- [durable-objects](../durable-objects/) - DO fundamentals and coordination patterns\n- [workers](../workers/) - Worker runtime for DO stubs\n- [d1](../d1/) - Shared database alternative to per-DO storage\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/do-storage/api.md",
    "content": "# DO Storage API Reference\n\n## SQL API\n\n```typescript\nconst cursor = this.sql.exec('SELECT * FROM users WHERE email = ?', email);\nfor (let row of cursor) {} // Objects: { id, name, email }\ncursor.toArray(); cursor.one(); // Single row (throws if != 1)\nfor (let row of cursor.raw()) {} // Arrays: [1, \"Alice\", \"...\"]\n\n// Manual iteration\nconst iter = cursor[Symbol.iterator]();\nconst first = iter.next(); // { value: {...}, done: false }\n\ncursor.columnNames; // [\"id\", \"name\", \"email\"]\ncursor.rowsRead; cursor.rowsWritten; // Billing\n\ntype User = { id: number; name: string; email: string };\nconst user = this.sql.exec<User>('...', userId).one();\n```\n\n## Sync KV API (SQLite only)\n\n```typescript\nthis.ctx.storage.kv.get(\"counter\"); // undefined if missing\nthis.ctx.storage.kv.put(\"counter\", 42);\nthis.ctx.storage.kv.put(\"user\", { name: \"Alice\", age: 30 });\nthis.ctx.storage.kv.delete(\"counter\"); // true if existed\n\nfor (let [key, value] of this.ctx.storage.kv.list()) {}\n\n// List options: start, prefix, reverse, limit\nthis.ctx.storage.kv.list({ start: \"user:\", prefix: \"user:\", reverse: true, limit: 100 });\n```\n\n## Async KV API (Both backends)\n\n```typescript\nawait this.ctx.storage.get(\"key\"); // Single\nawait this.ctx.storage.get([\"key1\", \"key2\"]); // Multiple (max 128)\nawait this.ctx.storage.put(\"key\", value); // Single\nawait this.ctx.storage.put({ \"key1\": \"v1\", \"key2\": { nested: true } }); // Multiple (max 128)\nawait this.ctx.storage.delete(\"key\");\nawait this.ctx.storage.delete([\"key1\", \"key2\"]);\nawait this.ctx.storage.list({ prefix: \"user:\", limit: 100 });\n\n// Options: allowConcurrency, noCache, allowUnconfirmed\nawait this.ctx.storage.get(\"key\", { allowConcurrency: true, noCache: true });\nawait this.ctx.storage.put(\"key\", value, { allowUnconfirmed: true, noCache: true });\n```\n\n### Storage Options\n\n| Option | Methods | Effect | Use Case |\n|--------|---------|--------|----------|\n| `allowConcurrency` | get, list | Skip input gate; allow concurrent requests during read | Read-heavy metrics that don't need strict consistency |\n| `noCache` | get, put, list | Skip in-memory cache; always read from disk | Rarely-accessed data or testing storage directly |\n| `allowUnconfirmed` | put, delete | Return before write confirms (still protected by output gate) | Non-critical writes where latency matters more than confirmation |\n\n## Transactions\n\n```typescript\n// Sync (SQL/sync KV only)\nthis.ctx.storage.transactionSync(() => {\n  this.sql.exec('UPDATE accounts SET balance = balance - ? WHERE id = ?', 100, 1);\n  this.sql.exec('UPDATE accounts SET balance = balance + ? WHERE id = ?', 100, 2);\n  return \"result\";\n});\n\n// Async\nawait this.ctx.storage.transaction(async () => {\n  const value = await this.ctx.storage.get(\"counter\");\n  await this.ctx.storage.put(\"counter\", value + 1);\n  if (value > 100) this.ctx.storage.rollback(); // Explicit rollback\n});\n```\n\n## Point-in-Time Recovery\n\n```typescript\nawait this.ctx.storage.getCurrentBookmark();\nawait this.ctx.storage.getBookmarkForTime(Date.now() - 2 * 24 * 60 * 60 * 1000);\nawait this.ctx.storage.onNextSessionRestoreBookmark(bookmark);\nthis.ctx.abort(); // Restart to apply; bookmarks lexically comparable (earlier < later)\n```\n\n## Alarms\n\n```typescript\nawait this.ctx.storage.setAlarm(Date.now() + 60000); // Timestamp or Date\nawait this.ctx.storage.getAlarm();\nawait this.ctx.storage.deleteAlarm();\n\nasync alarm() { await this.doScheduledWork(); }\n```\n\n## Misc\n\n```typescript\nawait this.ctx.storage.deleteAll(); // Atomic for SQLite; alarm NOT included\nthis.ctx.storage.sql.databaseSize; // Bytes\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/do-storage/configuration.md",
    "content": "# DO Storage Configuration\n\n## SQLite-backed (Recommended)\n\n**wrangler.jsonc:**\n```jsonc\n{\n  \"migrations\": [\n    {\n      \"tag\": \"v1\",\n      \"new_sqlite_classes\": [\"Counter\", \"Session\", \"RateLimiter\"]\n    }\n  ]\n}\n```\n\n**Migration lifecycle:** Migrations run once per deployment. Existing DO instances get new storage backend on next invocation. Renaming/removing classes requires `renamed_classes` or `deleted_classes` entries.\n\n## KV-backed (Legacy)\n\n**wrangler.jsonc:**\n```jsonc\n{\n  \"migrations\": [\n    {\n      \"tag\": \"v1\",\n      \"new_classes\": [\"OldCounter\"]\n    }\n  ]\n}\n```\n\n## TypeScript Setup\n\n```typescript\nexport class MyDurableObject extends DurableObject {\n  sql: SqlStorage;\n  \n  constructor(ctx: DurableObjectState, env: Env) {\n    super(ctx, env);\n    this.sql = ctx.storage.sql;\n    \n    // Initialize schema\n    this.sql.exec(`\n      CREATE TABLE IF NOT EXISTS users(\n        id INTEGER PRIMARY KEY,\n        name TEXT NOT NULL,\n        email TEXT UNIQUE\n      );\n    `);\n  }\n}\n\n// Binding\ninterface Env {\n  MY_DO: DurableObjectNamespace;\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const id = env.MY_DO.idFromName('singleton');\n    const stub = env.MY_DO.get(id);\n    \n    // Modern RPC: call methods directly (recommended)\n    const result = await stub.someMethod();\n    return Response.json(result);\n    \n    // Legacy: forward request (still works)\n    // return stub.fetch(request);\n  }\n}\n```\n\n## CPU Limits\n\n```jsonc\n{\n  \"limits\": {\n    \"cpu_ms\": 300000  // 5 minutes (default 30s)\n  }\n}\n```\n\n## Location Control\n\n```typescript\n// Jurisdiction (GDPR/FedRAMP)\nconst euNamespace = env.MY_DO.jurisdiction(\"eu\");\nconst id = euNamespace.newUniqueId();\nconst stub = euNamespace.get(id);\n\n// Location hint (best effort)\nconst stub = env.MY_DO.get(id, { locationHint: \"enam\" });\n// Hints: wnam, enam, sam, weur, eeur, apac, oc, afr, me\n```\n\n## Initialization\n\n```typescript\nexport class Counter extends DurableObject {\n  value: number;\n  \n  constructor(ctx: DurableObjectState, env: Env) {\n    super(ctx, env);\n    \n    // Block concurrent requests during init\n    ctx.blockConcurrencyWhile(async () => {\n      this.value = (await ctx.storage.get(\"value\")) || 0;\n    });\n  }\n}\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/do-storage/gotchas.md",
    "content": "# DO Storage Gotchas & Troubleshooting\n\n## Concurrency Model (CRITICAL)\n\nDurable Objects use **input/output gates** to prevent race conditions:\n\n### Input Gates\nBlock new requests during storage reads from CURRENT request:\n\n```typescript\n// SAFE: Input gate active during await\nasync increment() {\n  const val = await this.ctx.storage.get(\"counter\"); // Input gate blocks other requests\n  await this.ctx.storage.put(\"counter\", val + 1);\n  return val;\n}\n```\n\n### Output Gates\nHold response until ALL writes from current request confirm:\n\n```typescript\n// SAFE: Output gate waits for put() to confirm before returning response\nasync increment() {\n  const val = await this.ctx.storage.get(\"counter\");\n  this.ctx.storage.put(\"counter\", val + 1); // No await\n  return new Response(String(val)); // Response delayed until write confirms\n}\n```\n\n### Write Coalescing\nMultiple writes to same key = atomic (last write wins):\n\n```typescript\n// SAFE: All three writes coalesce atomically\nthis.ctx.storage.put(\"key\", 1);\nthis.ctx.storage.put(\"key\", 2);\nthis.ctx.storage.put(\"key\", 3); // Final value: 3\n```\n\n### Breaking Gates (DANGER)\n\n**fetch() breaks input/output gates** → allows request interleaving:\n\n```typescript\n// UNSAFE: fetch() allows another request to interleave\nasync unsafe() {\n  const val = await this.ctx.storage.get(\"counter\");\n  await fetch(\"https://api.example.com\"); // Gate broken!\n  await this.ctx.storage.put(\"counter\", val + 1); // Race condition possible\n}\n```\n\n**Solution:** Use `blockConcurrencyWhile()` or `transaction()`:\n\n```typescript\n// SAFE: Block concurrent requests explicitly\nasync safe() {\n  return await this.ctx.blockConcurrencyWhile(async () => {\n    const val = await this.ctx.storage.get(\"counter\");\n    await fetch(\"https://api.example.com\");\n    await this.ctx.storage.put(\"counter\", val + 1);\n    return val;\n  });\n}\n```\n\n### allowConcurrency Option\n\nOpt out of input gate for reads that don't need protection:\n\n```typescript\n// Allow concurrent reads (no consistency guarantee)\nconst val = await this.ctx.storage.get(\"metrics\", { allowConcurrency: true });\n```\n\n## Common Errors\n\n### \"Race Condition in Concurrent Calls\"\n\n**Cause:** Multiple concurrent storage operations initiated from same event (e.g., `Promise.all()`) are not protected by input gate  \n**Solution:** Avoid concurrent storage operations within single event; input gate only serializes requests from different events, not operations within same event\n\n### \"Direct SQL Transaction Statements\"\n\n**Cause:** Using `BEGIN TRANSACTION` directly instead of transaction methods  \n**Solution:** Use `this.ctx.storage.transactionSync()` for sync operations or `this.ctx.storage.transaction()` for async operations\n\n### \"Async in transactionSync\"\n\n**Cause:** Using async operations inside `transactionSync()` callback  \n**Solution:** Use async `transaction()` method instead of `transactionSync()` when async operations needed\n\n### \"TypeScript Type Mismatch at Runtime\"\n\n**Cause:** Query doesn't return all fields specified in TypeScript type  \n**Solution:** Ensure SQL query selects all columns that match the TypeScript type definition\n\n### \"Silent Data Corruption with Large IDs\"\n\n**Cause:** JavaScript numbers have 53-bit precision; SQLite INTEGER is 64-bit  \n**Symptom:** IDs > 9007199254740991 (Number.MAX_SAFE_INTEGER) silently truncate/corrupt  \n**Solution:** Store large IDs as TEXT:\n\n```typescript\n// BAD: Snowflake/Twitter IDs will corrupt\nthis.sql.exec(\"CREATE TABLE events(id INTEGER PRIMARY KEY)\");\nthis.sql.exec(\"INSERT INTO events VALUES (?)\", 1234567890123456789n); // Corrupts!\n\n// GOOD: Store as TEXT\nthis.sql.exec(\"CREATE TABLE events(id TEXT PRIMARY KEY)\");\nthis.sql.exec(\"INSERT INTO events VALUES (?)\", \"1234567890123456789\");\n```\n\n### \"Alarm Not Deleted with deleteAll()\"\n\n**Cause:** `deleteAll()` doesn't delete alarms automatically  \n**Solution:** Call `deleteAlarm()` explicitly before `deleteAll()` to remove alarm\n\n### \"Slow Performance\"\n\n**Cause:** Using async KV API instead of sync API  \n**Solution:** Use sync KV API (`ctx.storage.kv`) for better performance with simple key-value operations\n\n### \"High Billing from Storage Operations\"\n\n**Cause:** Excessive `rowsRead`/`rowsWritten` or unused objects not cleaned up  \n**Solution:** Monitor `rowsRead`/`rowsWritten` metrics and ensure unused objects call `deleteAll()`\n\n### \"Durable Object Overloaded\"\n\n**Cause:** Single DO exceeding ~1K req/sec soft limit  \n**Solution:** Shard across multiple DOs with random IDs or other distribution strategy\n\n## Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Max columns per table | 100 | SQL limitation |\n| Max string/BLOB per row | 2 MB | SQL limitation |\n| Max row size | 2 MB | SQL limitation |\n| Max SQL statement size | 100 KB | SQL limitation |\n| Max SQL parameters | 100 | SQL limitation |\n| Max LIKE/GLOB pattern | 50 B | SQL limitation |\n| SQLite storage per object | 10 GB | SQLite-backed storage |\n| SQLite key+value size | 2 MB | SQLite-backed storage |\n| KV storage per object | Unlimited | KV-style storage |\n| KV key size | 2 KiB | KV-style storage |\n| KV value size | 128 KiB | KV-style storage |\n| Request throughput | ~1K req/sec | Soft limit per DO |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/do-storage/patterns.md",
    "content": "# DO Storage Patterns & Best Practices\n\n## Schema Migration\n\n```typescript\nexport class MyDurableObject extends DurableObject {\n  constructor(ctx: DurableObjectState, env: Env) {\n    super(ctx, env);\n    this.sql = ctx.storage.sql;\n    \n    // Use SQLite's built-in user_version pragma\n    const ver = this.sql.exec(\"PRAGMA user_version\").one()?.user_version || 0;\n    \n    if (ver === 0) {\n      this.sql.exec(`CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT)`);\n      this.sql.exec(\"PRAGMA user_version = 1\");\n    }\n    if (ver === 1) {\n      this.sql.exec(`ALTER TABLE users ADD COLUMN email TEXT`);\n      this.sql.exec(\"PRAGMA user_version = 2\");\n    }\n  }\n}\n```\n\n## In-Memory Caching\n\n```typescript\nexport class UserCache extends DurableObject {\n  cache = new Map<string, User>();\n  async getUser(id: string): Promise<User | undefined> {\n    if (this.cache.has(id)) {\n      const cached = this.cache.get(id);\n      if (cached) return cached;\n    }\n    const user = await this.ctx.storage.get<User>(`user:${id}`);\n    if (user) this.cache.set(id, user);\n    return user;\n  }\n  async updateUser(id: string, data: Partial<User>) {\n    const updated = { ...await this.getUser(id), ...data };\n    this.cache.set(id, updated);\n    await this.ctx.storage.put(`user:${id}`, updated);\n    return updated;\n  }\n}\n```\n\n## Rate Limiting\n\n```typescript\nexport class RateLimiter extends DurableObject {\n  async checkLimit(key: string, limit: number, window: number): Promise<boolean> {\n    const now = Date.now();\n    this.sql.exec('DELETE FROM requests WHERE key = ? AND timestamp < ?', key, now - window);\n    const count = this.sql.exec('SELECT COUNT(*) as count FROM requests WHERE key = ?', key).one().count;\n    if (count >= limit) return false;\n    this.sql.exec('INSERT INTO requests (key, timestamp) VALUES (?, ?)', key, now);\n    return true;\n  }\n}\n```\n\n## Batch Processing with Alarms\n\n```typescript\nexport class BatchProcessor extends DurableObject {\n  pending: string[] = [];\n  async addItem(item: string) {\n    this.pending.push(item);\n    if (!await this.ctx.storage.getAlarm()) await this.ctx.storage.setAlarm(Date.now() + 5000);\n  }\n  async alarm() {\n    const items = [...this.pending];\n    this.pending = [];\n    this.sql.exec(`INSERT INTO processed_items (item, timestamp) VALUES ${items.map(() => \"(?, ?)\").join(\", \")}`, ...items.flatMap(item => [item, Date.now()]));\n  }\n}\n```\n\n## Initialization Pattern\n\n```typescript\nexport class Counter extends DurableObject {\n  value: number;\n  constructor(ctx: DurableObjectState, env: Env) {\n    super(ctx, env);\n    ctx.blockConcurrencyWhile(async () => { this.value = (await ctx.storage.get(\"value\")) || 0; });\n  }\n  async increment() {\n    this.value++;\n    this.ctx.storage.put(\"value\", this.value); // Don't await (output gate protects)\n    return this.value;\n  }\n}\n```\n\n## Safe Counter / Optimized Write\n\n```typescript\n// Input gate blocks other requests\nasync getUniqueNumber(): Promise<number> {\n  let val = await this.ctx.storage.get(\"counter\");\n  await this.ctx.storage.put(\"counter\", val + 1);\n  return val;\n}\n\n// No await on write - output gate delays response until write confirms\nasync increment(): Promise<Response> {\n  let val = await this.ctx.storage.get(\"counter\");\n  this.ctx.storage.put(\"counter\", val + 1);\n  return new Response(String(val));\n}\n```\n\n## Parent-Child Coordination\n\nHierarchical DO pattern where parent manages child DOs:\n\n```typescript\n// Parent DO coordinates children\nexport class Workspace extends DurableObject {\n  async createDocument(name: string): Promise<string> {\n    const docId = crypto.randomUUID();\n    const childId = this.env.DOCUMENT.idFromName(`${this.ctx.id.toString()}:${docId}`);\n    const childStub = this.env.DOCUMENT.get(childId);\n    await childStub.initialize(name);\n    \n    // Track child in parent storage\n    this.sql.exec('INSERT INTO documents (id, name, created) VALUES (?, ?, ?)', \n      docId, name, Date.now());\n    return docId;\n  }\n  \n  async listDocuments(): Promise<string[]> {\n    return this.sql.exec('SELECT id FROM documents').toArray().map(r => r.id);\n  }\n}\n\n// Child DO\nexport class Document extends DurableObject {\n  async initialize(name: string) {\n    this.sql.exec('CREATE TABLE IF NOT EXISTS content(key TEXT PRIMARY KEY, value TEXT)');\n    this.sql.exec('INSERT INTO content VALUES (?, ?)', 'name', name);\n  }\n}\n```\n\n## Write Coalescing Pattern\n\nMultiple writes to same key coalesce atomically (last write wins):\n\n```typescript\nasync updateMetrics(userId: string, actions: Action[]) {\n  // All writes coalesce - no await needed\n  for (const action of actions) {\n    this.ctx.storage.put(`user:${userId}:lastAction`, action.type);\n    this.ctx.storage.put(`user:${userId}:count`, \n      await this.ctx.storage.get(`user:${userId}:count`) + 1);\n  }\n  // Output gate ensures all writes confirm before response\n  return new Response(\"OK\");\n}\n\n// Atomic batch with SQL\nasync batchUpdate(items: Item[]) {\n  this.sql.exec('BEGIN');\n  for (const item of items) {\n    this.sql.exec('INSERT OR REPLACE INTO items VALUES (?, ?)', item.id, item.value);\n  }\n  this.sql.exec('COMMIT');\n}\n```\n\n## Cleanup\n\n```typescript\nasync cleanup() {\n  await this.ctx.storage.deleteAlarm(); // Separate from deleteAll\n  await this.ctx.storage.deleteAll();\n}\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/do-storage/testing.md",
    "content": "# DO Storage Testing\n\nTesting Durable Objects with storage using `vitest-pool-workers`.\n\n## Setup\n\n**vitest.config.ts:**\n```typescript\nimport { defineWorkersConfig } from \"@cloudflare/vitest-pool-workers/config\";\n\nexport default defineWorkersConfig({\n  test: {\n    poolOptions: {\n      workers: { wrangler: { configPath: \"./wrangler.toml\" } }\n    }\n  }\n});\n```\n\n**package.json:** Add `@cloudflare/vitest-pool-workers` and `vitest` to devDependencies\n\n## Basic Testing\n\n```typescript\nimport { env, runInDurableObject } from \"cloudflare:test\";\nimport { describe, it, expect } from \"vitest\";\n\ndescribe(\"Counter DO\", () => {\n  it(\"increments counter\", async () => {\n    const id = env.COUNTER.idFromName(\"test\");\n    const result = await runInDurableObject(env.COUNTER, id, async (instance, state) => {\n      const val1 = await instance.increment();\n      const val2 = await instance.increment();\n      return { val1, val2 };\n    });\n    expect(result.val1).toBe(1);\n    expect(result.val2).toBe(2);\n  });\n});\n```\n\n## Testing SQL Storage\n\n```typescript\nit(\"creates and queries users\", async () => {\n  const id = env.USER_MANAGER.idFromName(\"test\");\n  await runInDurableObject(env.USER_MANAGER, id, async (instance, state) => {\n    await instance.createUser(\"alice@example.com\", \"Alice\");\n    const user = await instance.getUser(\"alice@example.com\");\n    expect(user).toEqual({ email: \"alice@example.com\", name: \"Alice\" });\n  });\n});\n\nit(\"handles schema migrations\", async () => {\n  const id = env.USER_MANAGER.idFromName(\"migration-test\");\n  await runInDurableObject(env.USER_MANAGER, id, async (instance, state) => {\n    const version = state.storage.sql.exec(\n      \"SELECT value FROM _meta WHERE key = 'schema_version'\"\n    ).one()?.value;\n    expect(version).toBe(\"1\");\n  });\n});\n```\n\n## Testing Alarms\n\n```typescript\nimport { runDurableObjectAlarm } from \"cloudflare:test\";\n\nit(\"processes batch on alarm\", async () => {\n  const id = env.BATCH_PROCESSOR.idFromName(\"test\");\n  \n  // Add items\n  await runInDurableObject(env.BATCH_PROCESSOR, id, async (instance) => {\n    await instance.addItem(\"item1\");\n    await instance.addItem(\"item2\");\n  });\n  \n  // Trigger alarm\n  await runDurableObjectAlarm(env.BATCH_PROCESSOR, id);\n  \n  // Verify processed\n  await runInDurableObject(env.BATCH_PROCESSOR, id, async (instance, state) => {\n    const count = state.storage.sql.exec(\n      \"SELECT COUNT(*) as count FROM processed_items\"\n    ).one().count;\n    expect(count).toBe(2);\n  });\n});\n```\n\n## Testing Concurrency\n\n```typescript\nit(\"handles concurrent increments safely\", async () => {\n  const id = env.COUNTER.idFromName(\"concurrent-test\");\n  \n  // Parallel increments\n  const results = await Promise.all([\n    runInDurableObject(env.COUNTER, id, (i) => i.increment()),\n    runInDurableObject(env.COUNTER, id, (i) => i.increment()),\n    runInDurableObject(env.COUNTER, id, (i) => i.increment())\n  ]);\n  \n  // All should get unique values\n  expect(new Set(results).size).toBe(3);\n  expect(Math.max(...results)).toBe(3);\n});\n```\n\n## Test Isolation\n\n```typescript\n// Per-test unique IDs\nlet testId: string;\nbeforeEach(() => { testId = crypto.randomUUID(); });\n\nit(\"isolated test\", async () => {\n  const id = env.MY_DO.idFromName(testId);\n  // Uses unique DO instance\n});\n\n// Cleanup pattern\nit(\"with cleanup\", async () => {\n  const id = env.MY_DO.idFromName(\"cleanup-test\");\n  try {\n    await runInDurableObject(env.MY_DO, id, async (instance) => {});\n  } finally {\n    await runInDurableObject(env.MY_DO, id, async (instance, state) => {\n      await state.storage.deleteAll();\n    });\n  }\n});\n```\n\n## Testing PITR\n\n```typescript\nit(\"restores from bookmark\", async () => {\n  const id = env.MY_DO.idFromName(\"pitr-test\");\n  \n  // Create checkpoint\n  const bookmark = await runInDurableObject(env.MY_DO, id, async (instance, state) => {\n    await state.storage.put(\"value\", 1);\n    return await state.storage.getCurrentBookmark();\n  });\n  \n  // Modify and restore\n  await runInDurableObject(env.MY_DO, id, async (instance, state) => {\n    await state.storage.put(\"value\", 2);\n    await state.storage.onNextSessionRestoreBookmark(bookmark);\n    state.abort();\n  });\n  \n  // Verify restored\n  await runInDurableObject(env.MY_DO, id, async (instance, state) => {\n    const value = await state.storage.get(\"value\");\n    expect(value).toBe(1);\n  });\n});\n```\n\n## Testing Transactions\n\n```typescript\nit(\"rolls back on error\", async () => {\n  const id = env.BANK.idFromName(\"transaction-test\");\n  \n  await runInDurableObject(env.BANK, id, async (instance, state) => {\n    await state.storage.put(\"balance\", 100);\n    \n    await expect(\n      state.storage.transaction(async () => {\n        await state.storage.put(\"balance\", 50);\n        throw new Error(\"Cancel\");\n      })\n    ).rejects.toThrow(\"Cancel\");\n    \n    const balance = await state.storage.get(\"balance\");\n    expect(balance).toBe(100); // Rolled back\n  });\n});\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/durable-objects/README.md",
    "content": "# Cloudflare Durable Objects\n\nExpert guidance for building stateful applications with Cloudflare Durable Objects.\n\n## Reading Order\n\n1. **First time?** Read this overview + Quick Start\n2. **Setting up?** See [Configuration](./configuration.md)\n3. **Building features?** Use decision trees below → [Patterns](./patterns.md)\n4. **Debugging issues?** Check [Gotchas](./gotchas.md)\n5. **Deep dive?** [API](./api.md) and [DO Storage](../do-storage/README.md)\n\n## Overview\n\nDurable Objects combine compute with storage in globally-unique, strongly-consistent packages:\n- **Globally unique instances**: Each DO has unique ID for multi-client coordination\n- **Co-located storage**: Fast, strongly-consistent storage with compute\n- **Automatic placement**: Objects spawn near first request location\n- **Stateful serverless**: In-memory state + persistent storage\n- **Single-threaded**: Serial request processing (no race conditions)\n\n## Rules of Durable Objects\n\nCritical rules preventing most production issues:\n\n1. **One alarm per DO** - Schedule multiple events via queue pattern\n2. **~1K req/s per DO max** - Shard for higher throughput\n3. **Constructor runs every wake** - Keep initialization light; use lazy loading\n4. **Hibernation clears memory** - In-memory state lost; persist critical data\n5. **Use `ctx.waitUntil()` for cleanup** - Ensures completion after response sent\n6. **No setTimeout for persistence** - Use `setAlarm()` for reliable scheduling\n\n## Core Concepts\n\n### Class Structure\nAll DOs extend `DurableObject` base class with constructor receiving `DurableObjectState` (storage, WebSockets, alarms) and `Env` (bindings).\n\n### Lifecycle States\n\n```\n[Not Created] → [Active] ⇄ [Hibernated] → [Evicted]\n                   ↓\n              [Destroyed]\n```\n\n- **Not Created**: DO ID exists but instance never spawned\n- **Active**: Processing requests, in-memory state valid, billed per GB-hour\n- **Hibernated**: WebSocket connections open but zero compute, zero cost\n- **Evicted**: Removed from memory; next request triggers cold start\n- **Destroyed**: Data deleted via migration or manual deletion\n\n### Accessing from Workers\nWorkers use bindings to get stubs, then call RPC methods directly (recommended) or use fetch handler (legacy).\n\n**RPC vs fetch() decision:**\n```\n├─ New project + compat ≥2024-04-03 → RPC (type-safe, simpler)\n├─ Need HTTP semantics (headers, status) → fetch()\n├─ Proxying requests to DO → fetch()\n└─ Legacy compatibility → fetch()\n```\n\nSee [Patterns: RPC vs fetch()](./patterns.md) for examples.\n\n### ID Generation\n- `idFromName()`: Deterministic, named coordination (rate limiting, locks)\n- `newUniqueId()`: Random IDs for sharding high-throughput workloads\n- `idFromString()`: Derive from existing IDs\n- Jurisdiction option: Data locality compliance\n\n### Storage Options\n\n**Which storage API?**\n```\n├─ Structured data, relations, transactions → SQLite (recommended)\n├─ Simple KV on SQLite DO → ctx.storage.kv (sync KV)\n└─ Legacy KV-only DO → ctx.storage (async KV)\n```\n\n- **SQLite** (recommended): Structured data, transactions, 10GB/DO\n- **Synchronous KV API**: Simple key-value on SQLite objects\n- **Asynchronous KV API**: Legacy/advanced use cases\n\nSee [DO Storage](../do-storage/README.md) for deep dive.\n\n### Special Features\n- **Alarms**: Schedule future execution per-DO (1 per DO - use queue pattern for multiple)\n- **WebSocket Hibernation**: Zero-cost idle connections (memory cleared on hibernation)\n- **Point-in-Time Recovery**: Restore to any point in 30 days (SQLite only)\n\n## Quick Start\n\n```typescript\nimport { DurableObject } from \"cloudflare:workers\";\n\nexport class Counter extends DurableObject<Env> {\n  async increment(): Promise<number> {\n    const result = this.ctx.storage.sql.exec(\n      `INSERT INTO counters (id, value) VALUES (1, 1)\n       ON CONFLICT(id) DO UPDATE SET value = value + 1\n       RETURNING value`\n    ).one();\n    return result.value;\n  }\n}\n\n// Worker access\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const id = env.COUNTER.idFromName(\"global\");\n    const stub = env.COUNTER.get(id);\n    const count = await stub.increment();\n    return new Response(`Count: ${count}`);\n  }\n};\n```\n\n## Decision Trees\n\n### What do you need?\n\n```\n├─ Coordinate requests (rate limit, lock, session)\n│   → idFromName(identifier) → [Patterns: Rate Limiting/Locks](./patterns.md)\n│\n├─ High throughput (>1K req/s)\n│   → Sharding with newUniqueId() or hash → [Patterns: Sharding](./patterns.md)\n│\n├─ Real-time updates (WebSocket, chat, collab)\n│   → WebSocket hibernation + room pattern → [Patterns: Real-time](./patterns.md)\n│\n├─ Background work (cleanup, notifications, scheduled tasks)\n│   → Alarms + queue pattern (1 alarm/DO) → [Patterns: Multiple Events](./patterns.md)\n│\n└─ User sessions with expiration\n    → Session pattern + alarm cleanup → [Patterns: Session Management](./patterns.md)\n```\n\n### Which access pattern?\n\n```\n├─ New project + typed methods → RPC (compat ≥2024-04-03)\n├─ Need HTTP semantics → fetch()\n├─ Proxying to DO → fetch()\n└─ Legacy compat → fetch()\n```\n\nSee [Patterns: RPC vs fetch()](./patterns.md) for examples.\n\n### Which storage?\n\n```\n├─ Structured data, SQL queries, transactions → SQLite (recommended)\n├─ Simple KV on SQLite DO → ctx.storage.kv (sync API)\n└─ Legacy KV-only DO → ctx.storage (async API)\n```\n\nSee [DO Storage](../do-storage/README.md) for complete guide.\n\n## Essential Commands\n\n```bash\nnpx wrangler dev              # Local dev with DOs\nnpx wrangler dev --remote     # Test against prod DOs\nnpx wrangler deploy           # Deploy + auto-apply migrations\n```\n\n## Resources\n\n**Docs**: https://developers.cloudflare.com/durable-objects/  \n**API Reference**: https://developers.cloudflare.com/durable-objects/api/  \n**Examples**: https://developers.cloudflare.com/durable-objects/examples/\n\n## In This Reference\n\n- **[Configuration](./configuration.md)** - wrangler.jsonc setup, migrations, bindings, environments\n- **[API](./api.md)** - Class structure, ctx methods, alarms, WebSocket hibernation\n- **[Patterns](./patterns.md)** - Sharding, rate limiting, locks, real-time, sessions\n- **[Gotchas](./gotchas.md)** - Limits, hibernation caveats, common errors\n\n## See Also\n\n- **[DO Storage](../do-storage/README.md)** - SQLite, KV, transactions (detailed storage guide)\n- **[Workers](../workers/README.md)** - Core Workers runtime features\n- **[WebSockets](../websockets/README.md)** - WebSocket APIs and patterns\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/durable-objects/api.md",
    "content": "# Durable Objects API\n\n## Class Structure\n\n```typescript\nimport { DurableObject } from \"cloudflare:workers\";\n\nexport class MyDO extends DurableObject<Env> {\n  constructor(ctx: DurableObjectState, env: Env) {\n    super(ctx, env);\n    // Runs on EVERY wake - keep light!\n  }\n  \n  // RPC methods (called directly from worker)\n  async myMethod(arg: string): Promise<string> { return arg; }\n  \n  // fetch handler (legacy/HTTP semantics)\n  async fetch(req: Request): Promise<Response> { /* ... */ }\n  \n  // Lifecycle handlers\n  async alarm() { /* alarm fired */ }\n  async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) { /* ... */ }\n  async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) { /* ... */ }\n  async webSocketError(ws: WebSocket, error: unknown) { /* ... */ }\n}\n```\n\n## DurableObjectState Context Methods\n\n### Concurrency Control\n\n```typescript\n// Complete work after response sent (e.g., cleanup, logging)\nthis.ctx.waitUntil(promise: Promise<any>): void\n\n// Critical section - blocks all other requests until complete\nawait this.ctx.blockConcurrencyWhile(async () => {\n  // No other requests processed during this block\n  // Use for initialization or critical operations\n})\n```\n\n**When to use:**\n- `waitUntil()`: Background cleanup, logging, non-critical work after response\n- `blockConcurrencyWhile()`: First-time init, schema migration, critical state setup\n\n### Lifecycle\n\n```typescript\nthis.ctx.id              // DurableObjectId of this instance\nthis.ctx.abort()         // Force eviction (use after PITR restore to reload state)\n```\n\n### Storage Access\n\n```typescript\nthis.ctx.storage.sql     // SQLite API (recommended)\nthis.ctx.storage.kv      // Sync KV API (SQLite DOs only)\nthis.ctx.storage         // Async KV API (legacy/KV-only DOs)\n```\n\nSee **[DO Storage](../do-storage/README.md)** for complete storage API reference.\n\n### WebSocket Management\n\n```typescript\nthis.ctx.acceptWebSocket(ws: WebSocket, tags?: string[])  // Enable hibernation\nthis.ctx.getWebSockets(tag?: string): WebSocket[]         // Get by tag or all\nthis.ctx.getTags(ws: WebSocket): string[]                 // Get tags for connection\n```\n\n### Alarms\n\n```typescript\nawait this.ctx.storage.setAlarm(timestamp: number | Date)  // Schedule (overwrites existing)\nawait this.ctx.storage.getAlarm(): number | null           // Get next alarm time\nawait this.ctx.storage.deleteAlarm(): void                 // Cancel alarm\n```\n\n**Limit:** 1 alarm per DO. Use queue pattern for multiple events (see [Patterns](./patterns.md)).\n\n## Storage APIs\n\nFor detailed storage documentation including SQLite queries, KV operations, transactions, and Point-in-Time Recovery, see **[DO Storage](../do-storage/README.md)**.\n\nQuick reference:\n\n```typescript\n// SQLite (recommended)\nthis.ctx.storage.sql.exec(\"SELECT * FROM users WHERE id = ?\", userId).one()\n\n// Sync KV (SQLite DOs only)\nthis.ctx.storage.kv.get(\"key\")\n\n// Async KV (legacy)\nawait this.ctx.storage.get(\"key\")\n```\n\n## Alarms\n\nSchedule future work that survives eviction:\n\n```typescript\n// Set alarm (overwrites any existing alarm)\nawait this.ctx.storage.setAlarm(Date.now() + 3600000)  // 1 hour from now\nawait this.ctx.storage.setAlarm(new Date(\"2026-02-01\"))  // Absolute time\n\n// Check next alarm\nconst nextRun = await this.ctx.storage.getAlarm()  // null if none\n\n// Cancel alarm\nawait this.ctx.storage.deleteAlarm()\n\n// Handler called when alarm fires\nasync alarm() {\n  // Runs once alarm triggers\n  // DO wakes from hibernation if needed\n  // Use for cleanup, notifications, scheduled tasks\n}\n```\n\n**Limitations:**\n- 1 alarm per DO maximum\n- Overwrites previous alarm when set\n- Use queue pattern for multiple scheduled events (see [Patterns](./patterns.md))\n\n**Reliability:**\n- Alarms survive DO eviction/restart\n- Cloudflare retries failed alarms automatically\n- Not guaranteed exactly-once (handle idempotently)\n\n## WebSocket Hibernation\n\nHibernation allows DOs with open WebSocket connections to consume zero compute/memory until message arrives.\n\n```typescript\nasync fetch(req: Request): Promise<Response> {\n  const [client, server] = Object.values(new WebSocketPair());\n  this.ctx.acceptWebSocket(server, [\"room:123\"]);  // Tags for filtering\n  server.serializeAttachment({ userId: \"abc\" });    // Persisted metadata\n  return new Response(null, { status: 101, webSocket: client });\n}\n\n// Called when message arrives (DO wakes from hibernation)\nasync webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) {\n  const data = ws.deserializeAttachment();          // Retrieve metadata\n  for (const c of this.ctx.getWebSockets(\"room:123\")) c.send(msg);\n}\n\n// Called on close (optional handler)\nasync webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {\n  // Cleanup logic, remove from lists, etc.\n}\n\n// Called on error (optional handler)\nasync webSocketError(ws: WebSocket, error: unknown) {\n  console.error(\"WebSocket error:\", error);\n  // Handle error, close connection, etc.\n}\n```\n\n**Key concepts:**\n- **Auto-hibernation:** DO hibernates when no active requests/alarms\n- **Zero cost:** Hibernated DOs incur no charges while preserving connections\n- **Memory cleared:** All in-memory state lost on hibernation\n- **Attachment persistence:** Use `serializeAttachment()` for per-connection metadata that survives hibernation\n- **Tags for filtering:** Group connections by room/channel/user for targeted broadcasts\n\n**Handler lifecycle:**\n- `webSocketMessage`: DO wakes, processes message, may hibernate after\n- `webSocketClose`: Called when client closes (optional - implement for cleanup)\n- `webSocketError`: Called on connection error (optional - implement for error handling)\n\n**Metadata persistence:**\n```typescript\n// Store connection metadata (survives hibernation)\nws.serializeAttachment({ userId: \"abc\", room: \"lobby\" })\n\n// Retrieve after hibernation\nconst { userId, room } = ws.deserializeAttachment()\n```\n\n## See Also\n\n- **[DO Storage](../do-storage/README.md)** - Complete storage API reference\n- **[Patterns](./patterns.md)** - Real-world usage patterns\n- **[Gotchas](./gotchas.md)** - Hibernation caveats and limits\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/durable-objects/configuration.md",
    "content": "# Durable Objects Configuration\n\n## Basic Setup\n\n```jsonc\n{\n  \"name\": \"my-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\",  // Use latest; ≥2024-04-03 for RPC\n  \"durable_objects\": {\n    \"bindings\": [\n      { \n        \"name\": \"MY_DO\",                // Env binding name\n        \"class_name\": \"MyDO\"            // Class exported from this worker\n      },\n      { \n        \"name\": \"EXTERNAL\",             // Access DO from another worker\n        \"class_name\": \"ExternalDO\", \n        \"script_name\": \"other-worker\"\n      }\n    ]\n  },\n  \"migrations\": [\n    { \"tag\": \"v1\", \"new_sqlite_classes\": [\"MyDO\"] }  // Prefer SQLite\n  ]\n}\n```\n\n## Binding Options\n\n```jsonc\n{\n  \"name\": \"BINDING_NAME\",\n  \"class_name\": \"ClassName\",\n  \"script_name\": \"other-worker\",        // Optional: external DO\n  \"environment\": \"production\"           // Optional: isolate by env\n}\n```\n\n## Jurisdiction (Data Locality)\n\nSpecify jurisdiction at ID creation for data residency compliance:\n\n```typescript\n// EU data residency\nconst id = env.MY_DO.idFromName(\"user:123\", { jurisdiction: \"eu\" })\n\n// Available jurisdictions\nconst jurisdictions = [\"eu\", \"fedramp\"]  // More may be added\n\n// All operations on this DO stay within jurisdiction\nconst stub = env.MY_DO.get(id)\nawait stub.someMethod()  // Data stays in EU\n```\n\n**Key points:**\n- Set at ID creation time, immutable afterward\n- DO instance physically located within jurisdiction\n- Storage and compute guaranteed within boundary\n- Use for GDPR, FedRAMP, other compliance requirements\n- No cross-jurisdiction access (requests fail if DO in different jurisdiction)\n\n## Migrations\n\n```jsonc\n{\n  \"migrations\": [\n    { \"tag\": \"v1\", \"new_sqlite_classes\": [\"MyDO\"] },            // Create SQLite (recommended)\n    // { \"tag\": \"v1\", \"new_classes\": [\"MyDO\"] },                // Create KV (paid only)\n    { \"tag\": \"v2\", \"renamed_classes\": [{ \"from\": \"Old\", \"to\": \"New\" }] },\n    { \"tag\": \"v3\", \"transferred_classes\": [{ \"from\": \"Src\", \"from_script\": \"old\", \"to\": \"Dest\" }] },\n    { \"tag\": \"v4\", \"deleted_classes\": [\"Obsolete\"] }           // Destroys ALL data!\n  ]\n}\n```\n\n**Migration rules:**\n- Tags must be unique and sequential (v1, v2, v3...)\n- No rollback supported (test with `--dry-run` first)\n- Auto-applied on deploy\n- `new_sqlite_classes` recommended over `new_classes` (SQLite vs KV)\n- `deleted_classes` immediately destroys ALL data (irreversible)\n\n## Environment Isolation\n\nSeparate DO namespaces per environment (staging/production have distinct object instances):\n\n```jsonc\n{\n  \"durable_objects\": {\n    \"bindings\": [{ \"name\": \"MY_DO\", \"class_name\": \"MyDO\" }]\n  },\n  \"env\": {\n    \"production\": {\n      \"durable_objects\": {\n        \"bindings\": [\n          { \"name\": \"MY_DO\", \"class_name\": \"MyDO\", \"environment\": \"production\" }\n        ]\n      }\n    }\n  }\n}\n```\n\nDeploy: `npx wrangler deploy --env production`\n\n## Limits & Settings\n\n```jsonc\n{\n  \"limits\": { \n    \"cpu_ms\": 300000  // Max CPU time: 30s default, 300s max\n  }\n}\n```\n\nSee [Gotchas](./gotchas.md) for complete limits table.\n\n## Types\n\n```typescript\nimport { DurableObject } from \"cloudflare:workers\";\n\ninterface Env {\n  MY_DO: DurableObjectNamespace<MyDO>;\n}\n\nexport class MyDO extends DurableObject<Env> {}\n\ntype DurableObjectNamespace<T> = {\n  newUniqueId(options?: { jurisdiction?: string }): DurableObjectId;\n  idFromName(name: string): DurableObjectId;\n  idFromString(id: string): DurableObjectId;\n  get(id: DurableObjectId): DurableObjectStub<T>;\n};\n```\n\n## Commands\n\n```bash\n# Development\nnpx wrangler dev                    # Local dev\nnpx wrangler dev --remote           # Test against production DOs\n\n# Deployment\nnpx wrangler deploy                 # Deploy + auto-apply migrations\nnpx wrangler deploy --dry-run       # Validate migrations without deploying\nnpx wrangler deploy --env production\n\n# Management\nnpx wrangler durable-objects list                      # List namespaces\nnpx wrangler durable-objects info <namespace> <id>     # Inspect specific DO\nnpx wrangler durable-objects delete <namespace> <id>   # Delete DO (destroys data)\n```\n\n## See Also\n\n- **[API](./api.md)** - DurableObjectState and lifecycle handlers\n- **[Patterns](./patterns.md)** - Multi-environment patterns\n- **[Gotchas](./gotchas.md)** - Migration caveats, limits\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/durable-objects/gotchas.md",
    "content": "# Durable Objects Gotchas\n\n## Common Errors\n\n### \"Hibernation Cleared My In-Memory State\"\n\n**Problem:** Variables lost after hibernation  \n**Cause:** DO auto-hibernates when idle; in-memory state not persisted  \n**Solution:** Use `ctx.storage` for critical data, `ws.serializeAttachment()` for per-connection metadata\n\n```typescript\n// ❌ Wrong - lost on hibernation\nprivate userCount = 0;\nasync webSocketMessage(ws: WebSocket, msg: string) {\n  this.userCount++;  // Lost!\n}\n\n// ✅ Right - persisted\nasync webSocketMessage(ws: WebSocket, msg: string) {\n  const count = this.ctx.storage.kv.get(\"userCount\") || 0;\n  this.ctx.storage.kv.put(\"userCount\", count + 1);\n}\n```\n\n### \"setTimeout Didn't Fire After Restart\"\n\n**Problem:** Scheduled work lost on eviction  \n**Cause:** `setTimeout` in-memory only; eviction clears timers  \n**Solution:** Use `ctx.storage.setAlarm()` for reliable scheduling\n\n```typescript\n// ❌ Wrong - lost on eviction\nsetTimeout(() => this.cleanup(), 3600000);\n\n// ✅ Right - survives eviction\nawait this.ctx.storage.setAlarm(Date.now() + 3600000);\nasync alarm() { await this.cleanup(); }\n```\n\n### \"Constructor Runs on Every Wake\"\n\n**Problem:** Expensive init logic slows all requests  \n**Cause:** Constructor runs on every wake (first request after eviction OR after hibernation)  \n**Solution:** Lazy initialization or cache in storage\n\n**Critical understanding:** Constructor runs in two scenarios:\n1. **Cold start** - DO evicted from memory, first request creates new instance\n2. **Wake from hibernation** - DO with WebSockets hibernated, message/alarm wakes it\n\n```typescript\n// ❌ Wrong - expensive on every wake\nconstructor(ctx: DurableObjectState, env: Env) {\n  super(ctx, env);\n  this.heavyData = this.loadExpensiveData();  // Slow!\n}\n\n// ✅ Right - lazy load\nprivate heavyData?: HeavyData;\nprivate getHeavyData() {\n  if (!this.heavyData) this.heavyData = this.loadExpensiveData();\n  return this.heavyData;\n}\n```\n\n### \"Durable Object Overloaded (503 errors)\"\n\n**Problem:** 503 errors under load  \n**Cause:** Single DO exceeding ~1K req/s throughput limit  \n**Solution:** Shard across multiple DOs (see [Patterns: Sharding](./patterns.md))\n\n### \"Storage Quota Exceeded (Write failures)\"\n\n**Problem:** Write operations failing  \n**Cause:** DO storage exceeding 10GB limit or account quota  \n**Solution:** Cleanup with alarms, use `deleteAll()` for old data, upgrade plan\n\n### \"CPU Time Exceeded (Terminated)\"\n\n**Problem:** Request terminated mid-execution  \n**Cause:** Processing exceeding 30s CPU time default limit  \n**Solution:** Increase `limits.cpu_ms` in wrangler.jsonc (max 300s) or chunk work\n\n### \"WebSockets Disconnect on Eviction\"\n\n**Problem:** Connections drop unexpectedly  \n**Cause:** DO evicted from memory without hibernation API  \n**Solution:** Use WebSocket hibernation handlers + client reconnection logic\n\n### \"Migration Failed (Deploy error)\"\n\n**Cause:** Non-unique tags, non-sequential tags, or invalid class names in migration  \n**Solution:** Check tag uniqueness/sequential ordering and verify class names are correct\n\n### \"RPC Method Not Found\"\n\n**Cause:** compatibility_date < 2024-04-03 preventing RPC usage  \n**Solution:** Update compatibility_date to >= 2024-04-03 or use fetch() instead of RPC\n\n### \"Only One Alarm Allowed\"\n\n**Cause:** Need multiple scheduled tasks but only one alarm supported per DO  \n**Solution:** Use event queue pattern to schedule multiple tasks with single alarm\n\n### \"Race Condition Despite Single-Threading\"\n\n**Problem:** Concurrent requests see inconsistent state  \n**Cause:** Async operations allow request interleaving (await = yield point)  \n**Solution:** Use `blockConcurrencyWhile()` for critical sections or atomic storage ops\n\n```typescript\n// ❌ Wrong - race condition\nasync incrementCounter() {\n  const count = await this.ctx.storage.get(\"count\") || 0;\n  // ⚠️ Another request could execute here during await\n  await this.ctx.storage.put(\"count\", count + 1);\n}\n\n// ✅ Right - atomic operation\nasync incrementCounter() {\n  return this.ctx.storage.sql.exec(\n    \"INSERT INTO counters (id, value) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET value = value + 1 RETURNING value\"\n  ).one().value;\n}\n\n// ✅ Right - explicit locking\nasync criticalOperation() {\n  await this.ctx.blockConcurrencyWhile(async () => {\n    const count = await this.ctx.storage.get(\"count\") || 0;\n    await this.ctx.storage.put(\"count\", count + 1);\n  });\n}\n```\n\n### \"Migration Rollback Not Supported\"\n\n**Cause:** Attempting to rollback a migration after deployment  \n**Solution:** Test with `--dry-run` before deploying; migrations cannot be rolled back\n\n### \"deleted_classes Destroys Data\"\n\n**Problem:** Migration deleted all data  \n**Cause:** `deleted_classes` migration immediately destroys all DO instances and data  \n**Solution:** Test with `--dry-run`; use `transferred_classes` to preserve data during moves\n\n### \"Cold Starts Are Slow\"\n\n**Problem:** First request after eviction takes longer  \n**Cause:** DO constructor + initial storage access on cold start  \n**Solution:** Expected behavior; optimize constructor, use connection pooling in clients, consider warming strategy for critical DOs\n\n```typescript\n// Warming strategy (periodically ping critical DOs)\nexport default {\n  async scheduled(event: ScheduledEvent, env: Env) {\n    const criticalIds = [\"auth\", \"sessions\", \"locks\"];\n    await Promise.all(criticalIds.map(name => {\n      const id = env.MY_DO.idFromName(name);\n      const stub = env.MY_DO.get(id);\n      return stub.ping();  // Keep warm\n    }));\n  }\n};\n```\n\n## Limits\n\n| Limit | Free | Paid | Notes |\n|-------|------|------|-------|\n| SQLite storage per DO | 10 GB | 10 GB | Per Durable Object instance |\n| SQLite total storage | 5 GB | Unlimited | Account-wide quota |\n| Key+value size | 2 MB | 2 MB | Single KV pair (SQLite/async) |\n| CPU time default | 30s | 30s | Per request; configurable |\n| CPU time max | 300s | 300s | Set via `limits.cpu_ms` |\n| DO classes | 100 | 500 | Distinct DO class definitions |\n| SQL columns | 100 | 100 | Per table |\n| SQL statement size | 100 KB | 100 KB | Max SQL query size |\n| WebSocket message size | 32 MiB | 32 MiB | Per message |\n| Request throughput | ~1K req/s | ~1K req/s | Per DO (soft limit - shard for more) |\n| Alarms per DO | 1 | 1 | Use queue pattern for multiple events |\n| Total DOs | Unlimited | Unlimited | Create as many instances as needed |\n| WebSockets | Unlimited | Unlimited | Within 128MB memory limit per DO |\n| Memory per DO | 128 MB | 128 MB | In-memory state + WebSocket buffers |\n\n## Hibernation Caveats\n\n1. **Memory cleared** - All in-memory variables lost; reconstruct from storage or `deserializeAttachment()`\n2. **Constructor reruns** - Runs on wake; avoid expensive operations, use lazy initialization\n3. **No guarantees** - DO may evict instead of hibernate; design for both\n4. **Attachment limit** - `serializeAttachment()` data must be JSON-serializable, keep small\n5. **Alarm wakes DO** - Alarm prevents hibernation until handler completes\n6. **WebSocket state not automatic** - Must explicitly persist with `serializeAttachment()` or storage\n\n## See Also\n\n- **[Patterns](./patterns.md)** - Workarounds for common limitations\n- **[API](./api.md)** - Storage limits and quotas\n- **[Configuration](./configuration.md)** - Setting CPU limits\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/durable-objects/patterns.md",
    "content": "# Durable Objects Patterns\n\n## When to Use Which Pattern\n\n| Need | Pattern | ID Strategy |\n|------|---------|-------------|\n| Rate limit per user/IP | Rate Limiting | `idFromName(identifier)` |\n| Mutual exclusion | Distributed Lock | `idFromName(resource)` |\n| >1K req/s throughput | Sharding | `newUniqueId()` or hash |\n| Real-time updates | WebSocket Collab | `idFromName(room)` |\n| User sessions | Session Management | `idFromName(sessionId)` |\n| Background cleanup | Alarm-based | Any |\n\n## RPC vs fetch()\n\n**RPC** (compat ≥2024-04-03): Type-safe, simpler, default for new projects  \n**fetch()**: Legacy compat, HTTP semantics, proxying\n\n```typescript\nconst count = await stub.increment();  // RPC\nconst count = await (await stub.fetch(req)).json();  // fetch()\n```\n\n## Sharding (High Throughput)\n\nSingle DO ~1K req/s max. Shard for higher throughput:\n\n```typescript\nexport default {\n  async fetch(req: Request, env: Env): Promise<Response> {\n    const userId = new URL(req.url).searchParams.get(\"user\");\n    const hash = hashCode(userId) % 100;  // 100 shards\n    const id = env.COUNTER.idFromName(`shard:${hash}`);\n    return env.COUNTER.get(id).fetch(req);\n  }\n};\n\nfunction hashCode(str: string): number {\n  let hash = 0;\n  for (let i = 0; i < str.length; i++) hash = ((hash << 5) - hash) + str.charCodeAt(i);\n  return Math.abs(hash);\n}\n```\n\n**Decisions:**\n- **Shard count**: 10-1000 typical (start with 100, measure, adjust)\n- **Shard key**: User ID, IP, session - must distribute evenly (use hash)\n- **Aggregation**: Coordinator DO or external system (D1, R2)\n\n## Rate Limiting\n\n```typescript\nasync checkLimit(key: string, limit: number, windowMs: number): Promise<boolean> {\n  const req = this.ctx.storage.sql.exec(\"SELECT COUNT(*) as count FROM requests WHERE key = ? AND timestamp > ?\", key, Date.now() - windowMs).one();\n  if (req.count >= limit) return false;\n  this.ctx.storage.sql.exec(\"INSERT INTO requests (key, timestamp) VALUES (?, ?)\", key, Date.now());\n  return true;\n}\n```\n\n## Distributed Lock\n\n```typescript\nprivate held = false;\nasync acquire(timeoutMs = 5000): Promise<boolean> {\n  if (this.held) return false;\n  this.held = true;\n  await this.ctx.storage.setAlarm(Date.now() + timeoutMs);\n  return true;\n}\nasync release() { this.held = false; await this.ctx.storage.deleteAlarm(); }\nasync alarm() { this.held = false; }  // Auto-release on timeout\n```\n\n## Hibernation-Aware Pattern\n\nPreserve state across hibernation:\n\n```typescript\nasync fetch(req: Request): Promise<Response> {\n  const [client, server] = Object.values(new WebSocketPair());\n  const userId = new URL(req.url).searchParams.get(\"user\");\n  server.serializeAttachment({ userId });  // Survives hibernation\n  this.ctx.acceptWebSocket(server, [\"room:lobby\"]);\n  server.send(JSON.stringify({ type: \"init\", state: this.ctx.storage.kv.get(\"state\") }));\n  return new Response(null, { status: 101, webSocket: client });\n}\n\nasync webSocketMessage(ws: WebSocket, msg: string) {\n  const { userId } = ws.deserializeAttachment();  // Retrieve after wake\n  const state = this.ctx.storage.kv.get(\"state\") || {};\n  state[userId] = JSON.parse(msg);\n  this.ctx.storage.kv.put(\"state\", state);\n  for (const c of this.ctx.getWebSockets(\"room:lobby\")) c.send(msg);\n}\n```\n\n## Real-time Collaboration\n\nBroadcast updates to all connected clients:\n\n```typescript\nasync webSocketMessage(ws: WebSocket, msg: string) {\n  const data = JSON.parse(msg);\n  this.ctx.storage.kv.put(\"doc\", data.content);  // Persist\n  for (const c of this.ctx.getWebSockets()) if (c !== ws) c.send(msg);  // Broadcast\n}\n```\n\n### WebSocket Reconnection\n\n**Client-side** (exponential backoff):\n```typescript\nclass ResilientWS {\n  private delay = 1000;\n  connect(url: string) {\n    const ws = new WebSocket(url);\n    ws.onclose = () => setTimeout(() => {\n      this.connect(url);\n      this.delay = Math.min(this.delay * 2, 30000);\n    }, this.delay);\n  }\n}\n```\n\n**Server-side** (cleanup on close):\n```typescript\nasync webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {\n  const { userId } = ws.deserializeAttachment();\n  this.ctx.storage.sql.exec(\"UPDATE users SET online = false WHERE id = ?\", userId);\n  for (const c of this.ctx.getWebSockets()) c.send(JSON.stringify({ type: \"user_left\", userId }));\n}\n```\n\n## Session Management\n\n```typescript\nasync createSession(userId: string, data: object): Promise<string> {\n  const id = crypto.randomUUID(), exp = Date.now() + 86400000;\n  this.ctx.storage.sql.exec(\"INSERT INTO sessions VALUES (?, ?, ?, ?)\", id, userId, JSON.stringify(data), exp);\n  await this.ctx.storage.setAlarm(exp);\n  return id;\n}\n\nasync getSession(id: string): Promise<object | null> {\n  const row = this.ctx.storage.sql.exec(\"SELECT data FROM sessions WHERE id = ? AND expires_at > ?\", id, Date.now()).one();\n  return row ? JSON.parse(row.data) : null;\n}\n\nasync alarm() { this.ctx.storage.sql.exec(\"DELETE FROM sessions WHERE expires_at <= ?\", Date.now()); }\n```\n\n## Multiple Events (Single Alarm)\n\nQueue pattern to schedule multiple events:\n\n```typescript\nasync scheduleEvent(id: string, runAt: number) {\n  await this.ctx.storage.put(`event:${id}`, { id, runAt });\n  const curr = await this.ctx.storage.getAlarm();\n  if (!curr || runAt < curr) await this.ctx.storage.setAlarm(runAt);\n}\n\nasync alarm() {\n  const events = await this.ctx.storage.list({ prefix: \"event:\" }), now = Date.now();\n  let next = null;\n  for (const [key, ev] of events) {\n    if (ev.runAt <= now) {\n      await this.processEvent(ev);\n      await this.ctx.storage.delete(key);\n    } else if (!next || ev.runAt < next) next = ev.runAt;\n  }\n  if (next) await this.ctx.storage.setAlarm(next);\n}\n```\n\n## Graceful Cleanup\n\nUse `ctx.waitUntil()` to complete work after response:\n\n```typescript\nasync myMethod() {\n  const response = { success: true };\n  this.ctx.waitUntil(this.ctx.storage.sql.exec(\"DELETE FROM old_data WHERE timestamp < ?\", cutoff));\n  return response;\n}\n```\n\n## Best Practices\n\n- **Design**: Use `idFromName()` for coordination, `newUniqueId()` for sharding, minimize constructor work\n- **Storage**: Prefer SQLite, batch with transactions, set alarms for cleanup, use PITR before risky ops\n- **Performance**: ~1K req/s per DO max - shard for more, cache in memory, use alarms for deferred work\n- **Reliability**: Handle 503 with retry+backoff, design for cold starts, test migrations with `--dry-run`\n- **Security**: Validate inputs in Workers, rate limit DO creation, use jurisdiction for compliance\n\n## See Also\n\n- **[API](./api.md)** - ctx methods, WebSocket handlers\n- **[Gotchas](./gotchas.md)** - Hibernation caveats, common errors\n- **[DO Storage](../do-storage/README.md)** - Storage patterns and transactions\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/email-routing/README.md",
    "content": "# Cloudflare Email Routing Skill Reference\n\n## Overview\n\nCloudflare Email Routing enables custom email addresses for your domain that route to verified destination addresses. It's free, privacy-focused (no storage/access), and includes Email Workers for programmatic email processing.\n\n**Available to all Cloudflare customers using Cloudflare as authoritative nameserver.**\n\n## Quick Start\n\n```typescript\n// Basic email handler\nexport default {\n  async email(message, env, ctx) {\n    // CRITICAL: Must consume stream before response\n    const parser = new PostalMime.default();\n    const email = await parser.parse(await message.raw.arrayBuffer());\n    \n    // Process email\n    console.log(`From: ${message.from}, Subject: ${email.subject}`);\n    \n    // Forward or reject\n    await message.forward(\"verified@destination.com\");\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## Reading Order\n\n**Start here based on your goal:**\n\n1. **New to Email Routing?** → [configuration.md](configuration.md) → [patterns.md](patterns.md)\n2. **Adding Workers?** → [api.md](api.md) § Worker Runtime API → [patterns.md](patterns.md)\n3. **Sending emails?** → [api.md](api.md) § SendEmail Binding\n4. **Managing via API?** → [api.md](api.md) § REST API Operations\n5. **Debugging issues?** → [gotchas.md](gotchas.md)\n\n## Decision Tree\n\n```\nNeed to receive emails?\n├─ Simple forwarding only? → Dashboard rules (configuration.md)\n├─ Complex logic/filtering? → Email Workers (api.md + patterns.md)\n└─ Parse attachments/body? → postal-mime library (patterns.md § Parse Email)\n\nNeed to send emails?\n├─ From Worker? → SendEmail binding (api.md § SendEmail)\n└─ From external app? → Use external SMTP/API service\n\nHaving issues?\n├─ Email not arriving? → gotchas.md § Mail Authentication\n├─ Worker crashing? → gotchas.md § Stream Consumption\n└─ Forward failing? → gotchas.md § Destination Verification\n```\n\n## Key Concepts\n\n**Routing Rules**: Pattern-based forwarding configured via Dashboard/API. Simple but limited.\n\n**Email Workers**: Custom TypeScript handlers with full email access. Handles complex logic, parsing, storage, rejection.\n\n**SendEmail Binding**: Outbound email API for Workers. Transactional email only (no marketing/bulk).\n\n**ForwardableEmailMessage**: Runtime interface for incoming emails. Provides headers, raw stream, forward/reject methods.\n\n## In This Reference\n\n- **[configuration.md](configuration.md)** - Setup, deployment, wrangler config\n- **[api.md](api.md)** - REST API + Worker runtime API + types\n- **[patterns.md](patterns.md)** - Common patterns with working examples\n- **[gotchas.md](gotchas.md)** - Critical pitfalls, troubleshooting, limits\n\n## Architecture\n\n```\nInternet → MX Records → Cloudflare Email Routing\n                            ├─ Routing Rules (dashboard)\n                            └─ Email Worker (your code)\n                                ├─ Forward to destination\n                                ├─ Reject with reason\n                                ├─ Store in R2/KV/D1\n                                └─ Send outbound (SendEmail)\n```\n\n## See Also\n\n- [Cloudflare Docs: Email Routing](https://developers.cloudflare.com/email-routing/)\n- [Cloudflare Docs: Email Workers](https://developers.cloudflare.com/email-routing/email-workers/)\n- [postal-mime npm package](https://www.npmjs.com/package/postal-mime)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/email-routing/api.md",
    "content": "# Email Routing API Reference\n\n## Worker Runtime API\n\n### Email Handler Interface\n\n```typescript\ninterface ExportedHandler<Env = unknown> {\n  email?(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): void | Promise<void>;\n}\n```\n\n### ForwardableEmailMessage\n\nMain interface for incoming emails:\n\n```typescript\ninterface ForwardableEmailMessage {\n  readonly from: string;          // Envelope sender (e.g., \"sender@example.com\")\n  readonly to: string;             // Envelope recipient (e.g., \"you@yourdomain.com\")\n  readonly headers: Headers;       // Web API Headers object\n  readonly raw: ReadableStream;    // Raw MIME message stream\n  \n  setReject(reason: string): void;\n  forward(rcptTo: string, headers?: Headers): Promise<void>;\n}\n```\n\n**Key Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `from` | `string` | Envelope sender (MAIL FROM), not header From |\n| `to` | `string` | Envelope recipient (RCPT TO), not header To |\n| `headers` | `Headers` | Email headers (Subject, From, To, etc.) |\n| `raw` | `ReadableStream` | Raw MIME message (consume once only) |\n\n**Methods:**\n\n- `setReject(reason)`: Reject email with bounce message\n- `forward(rcptTo, headers?)`: Forward to verified destination, optionally add headers\n\n### Headers Object\n\nStandard Web API Headers interface:\n\n```typescript\n// Access headers\nconst subject = message.headers.get(\"subject\");\nconst from = message.headers.get(\"from\");\nconst messageId = message.headers.get(\"message-id\");\n\n// Check spam score\nconst spamScore = parseFloat(message.headers.get(\"x-cf-spamh-score\") || \"0\");\nif (spamScore > 5) {\n  message.setReject(\"Spam detected\");\n}\n```\n\n### Common Headers\n\n`subject`, `from`, `to`, `x-cf-spamh-score` (spam score), `message-id` (deduplication), `dkim-signature` (auth)\n\n### Envelope vs Header Addresses\n\n**Critical distinction:**\n\n```typescript\n// Envelope addresses (routing, auth checks)\nmessage.from // \"bounce@sender.com\" (actual sender)\nmessage.to   // \"you@yourdomain.com\" (your address)\n\n// Header addresses (display, user-facing)\nmessage.headers.get(\"from\") // \"Alice <alice@sender.com>\"\nmessage.headers.get(\"to\")   // \"Bob <you@yourdomain.com>\"\n```\n\n**Use envelope addresses for:**\n- Authentication/SPF checks\n- Routing decisions\n- Bounce handling\n\n**Use header addresses for:**\n- Display to users\n- Reply-To logic\n- User-facing filtering\n\n## SendEmail Binding\n\nOutbound email API for transactional messages.\n\n### Configuration\n\n```jsonc\n// wrangler.jsonc\n{\n  \"send_email\": [\n    { \"name\": \"EMAIL\" }\n  ]\n}\n```\n\n### TypeScript Types\n\n```typescript\ninterface Env {\n  EMAIL: SendEmail;\n}\n\ninterface SendEmail {\n  send(message: EmailMessage): Promise<void>;\n}\n\ninterface EmailMessage {\n  from: string | { name?: string; email: string };\n  to: string | { name?: string; email: string } | Array<string | { name?: string; email: string }>;\n  subject: string;\n  text?: string;\n  html?: string;\n  headers?: Headers;\n  reply_to?: string | { name?: string; email: string };\n}\n```\n\n### Send Email Example\n\n```typescript\ninterface Env {\n  EMAIL: SendEmail;\n}\n\nexport default {\n  async fetch(request, env, ctx): Promise<Response> {\n    await env.EMAIL.send({\n      from: { name: \"Acme Corp\", email: \"noreply@yourdomain.com\" },\n      to: [\n        { name: \"Alice\", email: \"alice@example.com\" },\n        \"bob@example.com\"\n      ],\n      subject: \"Your order #12345 has shipped\",\n      text: \"Track your package at: https://track.example.com/12345\",\n      html: \"<p>Track your package at: <a href='https://track.example.com/12345'>View tracking</a></p>\",\n      reply_to: { name: \"Support\", email: \"support@yourdomain.com\" }\n    });\n    \n    return new Response(\"Email sent\");\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n### SendEmail Constraints\n\n- **From address**: Must be on verified domain (your domain with Email Routing enabled)\n- **Volume limits**: Transactional only, no bulk/marketing email\n- **Rate limits**: 100 emails/minute on Free plan, higher on Paid\n- **No attachments**: Use links to hosted files instead\n- **No DKIM control**: Cloudflare signs automatically\n\n## REST API Operations\n\nBase URL: `https://api.cloudflare.com/client/v4`\n\n### Authentication\n\n```bash\ncurl -H \"Authorization: Bearer $API_TOKEN\" https://api.cloudflare.com/client/v4/...\n```\n\n### Key Endpoints\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Enable routing | POST | `/zones/{zone_id}/email/routing/enable` |\n| Disable routing | POST | `/zones/{zone_id}/email/routing/disable` |\n| List rules | GET | `/zones/{zone_id}/email/routing/rules` |\n| Create rule | POST | `/zones/{zone_id}/email/routing/rules` |\n| Verify destination | POST | `/zones/{zone_id}/email/routing/addresses` |\n| List destinations | GET | `/zones/{zone_id}/email/routing/addresses` |\n\n### Create Routing Rule Example\n\n```bash\ncurl -X POST \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/email/routing/rules\" \\\n  -H \"Authorization: Bearer $API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"enabled\": true,\n    \"name\": \"Forward sales\",\n    \"matchers\": [{\"type\": \"literal\", \"field\": \"to\", \"value\": \"sales@yourdomain.com\"}],\n    \"actions\": [{\"type\": \"forward\", \"value\": [\"alice@company.com\"]}],\n    \"priority\": 0\n  }'\n```\n\nMatcher types: `literal` (exact match), `all` (catch-all).\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/email-routing/configuration.md",
    "content": "# Email Routing Configuration\n\n## Wrangler Configuration\n\n### Basic Email Worker\n\n```jsonc\n// wrangler.jsonc\n{\n  \"name\": \"email-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\",\n  \"send_email\": [{ \"name\": \"EMAIL\" }]\n}\n```\n\n```typescript\n// src/index.ts\nexport default {\n  async email(message, env, ctx) {\n    await message.forward(\"destination@example.com\");\n  }\n} satisfies ExportedHandler;\n```\n\n### With Storage Bindings\n\n```jsonc\n{\n  \"name\": \"email-processor\",\n  \"send_email\": [{ \"name\": \"EMAIL\" }],\n  \"kv_namespaces\": [{ \"binding\": \"KV\", \"id\": \"abc123\" }],\n  \"r2_buckets\": [{ \"binding\": \"R2\", \"bucket_name\": \"emails\" }],\n  \"d1_databases\": [{ \"binding\": \"DB\", \"database_id\": \"def456\" }]\n}\n```\n\n```typescript\ninterface Env {\n  EMAIL: SendEmail;\n  KV: KVNamespace;\n  R2: R2Bucket;\n  DB: D1Database;\n}\n```\n\n## Local Development\n\n```bash\nnpx wrangler dev\n\n# Test with curl\ncurl -X POST 'http://localhost:8787/__email' \\\n  --header 'content-type: message/rfc822' \\\n  --data 'From: test@example.com\nTo: you@yourdomain.com\nSubject: Test\n\nBody'\n```\n\n## Deployment\n\n```bash\nnpx wrangler deploy\n```\n\n**Connect to Email Routing:**\n\nDashboard: Email > Email Routing > [domain] > Settings > Email Workers > Select worker\n\nAPI:\n```bash\ncurl -X PUT \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/email/routing/settings\" \\\n  -H \"Authorization: Bearer $API_TOKEN\" \\\n  -d '{\"enabled\": true, \"worker\": \"email-worker\"}'\n```\n\n## DNS (Auto-Created)\n\n```dns\nyourdomain.com. IN MX 1 isaac.mx.cloudflare.net.\nyourdomain.com. IN MX 2 linda.mx.cloudflare.net.\nyourdomain.com. IN MX 3 amir.mx.cloudflare.net.\nyourdomain.com. IN TXT \"v=spf1 include:_spf.mx.cloudflare.net ~all\"\n```\n\n## Secrets & Variables\n\n```bash\n# Secrets (encrypted)\nnpx wrangler secret put API_KEY\n\n# Variables (plain)\n# wrangler.jsonc\n{ \"vars\": { \"THRESHOLD\": \"5.0\" } }\n```\n\n```typescript\ninterface Env {\n  API_KEY: string;\n  THRESHOLD: string;\n}\n```\n\n## TypeScript Setup\n\n```bash\nnpm install --save-dev @cloudflare/workers-types\n```\n\n```json\n// tsconfig.json\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"lib\": [\"ES2022\"],\n    \"types\": [\"@cloudflare/workers-types\"],\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true\n  }\n}\n```\n\n```typescript\nimport type { ForwardableEmailMessage } from \"@cloudflare/workers-types\";\n\nexport default {\n  async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): Promise<void> {\n    await message.forward(\"dest@example.com\");\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## Dependencies\n\n```bash\nnpm install postal-mime\n```\n\n```typescript\nimport PostalMime from 'postal-mime';\n\nexport default {\n  async email(message, env, ctx) {\n    const parser = new PostalMime();\n    const email = await parser.parse(await message.raw.arrayBuffer());\n    console.log(email.subject);\n    await message.forward(\"inbox@corp.com\");\n  }\n} satisfies ExportedHandler;\n```\n\n## Multi-Environment\n\n```bash\n# wrangler.dev.jsonc\n{ \"name\": \"worker-dev\", \"vars\": { \"ENV\": \"dev\" } }\n\n# wrangler.prod.jsonc\n{ \"name\": \"worker-prod\", \"vars\": { \"ENV\": \"prod\" } }\n\nnpx wrangler deploy --config wrangler.dev.jsonc\nnpx wrangler deploy --config wrangler.prod.jsonc\n```\n\n## CI/CD (GitHub Actions)\n\n```yaml\n# .github/workflows/deploy.yml\nname: Deploy\non:\n  push:\n    branches: [main]\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n      - run: npm ci\n      - run: npx wrangler deploy\n        env:\n          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/email-routing/gotchas.md",
    "content": "# Gotchas & Troubleshooting\n\n## Critical Pitfalls\n\n### Stream Consumption (MOST COMMON)\n\n**Problem:** \"stream already consumed\" or worker hangs\n\n**Cause:** `message.raw` is `ReadableStream` - consume once only\n\n**Solution:**\n```typescript\n// ❌ WRONG\nconst email1 = await parser.parse(await message.raw.arrayBuffer());\nconst email2 = await parser.parse(await message.raw.arrayBuffer()); // FAILS\n\n// ✅ CORRECT\nconst raw = await message.raw.arrayBuffer();\nconst email = await parser.parse(raw);\n```\n\nConsume `message.raw` immediately before any async operations.\n\n### Destination Verification\n\n**Problem:** Emails not forwarding\n\n**Cause:** Destination unverified\n\n**Solution:** Add destination, check inbox for verification email, click link. Verify status: `GET /zones/{id}/email/routing/addresses`\n\n### Mail Authentication\n\n**Problem:** Legitimate emails rejected\n\n**Cause:** Missing SPF/DKIM/DMARC on sender domain\n\n**Solution:** Configure sender DNS:\n```dns\nexample.com. IN TXT \"v=spf1 include:_spf.example.com ~all\"\nselector._domainkey.example.com. IN TXT \"v=DKIM1; k=rsa; p=...\"\n_dmarc.example.com. IN TXT \"v=DMARC1; p=quarantine\"\n```\n\n### Envelope vs Header\n\n**Problem:** Filtering on wrong address\n\n**Solution:**\n```typescript\n// Routing/auth: envelope\nif (message.from === \"trusted@example.com\") { }\n\n// Display: headers\nconst display = message.headers.get(\"from\");\n```\n\n### SendEmail Limits\n\n| Issue | Limit | Solution |\n|-------|-------|----------|\n| From domain | Must own | Use Email Routing domain |\n| Volume | ~100/min Free | Upgrade or throttle |\n| Attachments | Not supported | Link to R2 |\n| Type | Transactional | No bulk |\n\n## Common Errors\n\n### CPU Time Exceeded\n\n**Cause:** Heavy parsing, large emails\n\n**Solution:**\n```typescript\nconst size = parseInt(message.headers.get(\"content-length\") || \"0\") / 1024 / 1024;\nif (size > 20) {\n  message.setReject(\"Too large\");\n  return;\n}\n\nctx.waitUntil(expensiveWork());\nawait message.forward(\"dest@example.com\");\n```\n\n### Rule Not Triggering\n\n**Causes:** Priority conflict, matcher error, catch-all override\n\n**Solution:** Check priority (lower=first), verify exact match, confirm destination verified\n\n### Undefined Property\n\n**Cause:** Missing header\n\n**Solution:**\n```typescript\n// ❌ WRONG\nconst subj = message.headers.get(\"subject\").toLowerCase();\n\n// ✅ CORRECT\nconst subj = message.headers.get(\"subject\")?.toLowerCase() || \"\";\n```\n\n## Limits\n\n| Resource | Free | Paid |\n|----------|------|------|\n| Email size | 25 MB | 25 MB |\n| Rules | 200 | 200 |\n| Destinations | 200 | 200 |\n| CPU time | 10ms | 50ms |\n| SendEmail | ~100/min | Higher |\n\n## Debugging\n\n### Local\n\n```bash\nnpx wrangler dev\n\ncurl -X POST 'http://localhost:8787/__email' \\\n  --header 'content-type: message/rfc822' \\\n  --data 'From: test@example.com\nTo: you@yourdomain.com\nSubject: Test\n\nBody'\n```\n\n### Production\n\n```bash\nnpx wrangler tail\n```\n\n### Pattern\n\n```typescript\nexport default {\n  async email(message, env, ctx) {\n    try {\n      console.log(\"From:\", message.from);\n      await process(message, env);\n    } catch (err) {\n      console.error(err);\n      message.setReject(err.message);\n    }\n  }\n} satisfies ExportedHandler;\n```\n\n## Auth Troubleshooting\n\n### Check Status\n\n```typescript\nconst auth = message.headers.get(\"authentication-results\") || \"\";\nconsole.log({\n  spf: auth.includes(\"spf=pass\"),\n  dkim: auth.includes(\"dkim=pass\"),\n  dmarc: auth.includes(\"dmarc=pass\")\n});\n\nif (!auth.includes(\"pass\")) {\n  message.setReject(\"Failed auth\");\n  return;\n}\n```\n\n### SPF Issues\n\n**Causes:** Forwarding breaks SPF, too many lookups (>10), missing includes\n\n**Solution:**\n```dns\n; ✅ Good\nexample.com. IN TXT \"v=spf1 include:_spf.google.com ~all\"\n\n; ❌ Bad - too many\nexample.com. IN TXT \"v=spf1 include:a.com include:b.com ... ~all\"\n```\n\n### DMARC Alignment\n\n**Cause:** From domain must match SPF/DKIM domain\n\n## Best Practices\n\n1. Consume `message.raw` immediately\n2. Verify destinations\n3. Handle missing headers (`?.`)\n4. Use envelope for routing\n5. Check spam scores\n6. Test locally first\n7. Use `ctx.waitUntil` for background work\n8. Size-check early\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/email-routing/patterns.md",
    "content": "# Common Patterns\n\n## 1. Allowlist/Blocklist\n\n```typescript\n// Allowlist\nconst allowed = [\"user@example.com\", \"trusted@corp.com\"];\nif (!allowed.includes(message.from)) {\n  message.setReject(\"Not allowed\");\n  return;\n}\nawait message.forward(\"inbox@corp.com\");\n```\n\n## 2. Parse Email Body\n\n```typescript\nimport PostalMime from 'postal-mime';\n\nexport default {\n  async email(message, env, ctx) {\n    // CRITICAL: Consume stream immediately\n    const raw = await message.raw.arrayBuffer();\n    \n    const parser = new PostalMime();\n    const email = await parser.parse(raw);\n    \n    console.log({\n      subject: email.subject,\n      text: email.text,\n      html: email.html,\n      from: email.from.address,\n      attachments: email.attachments.length\n    });\n    \n    await message.forward(\"inbox@corp.com\");\n  }\n} satisfies ExportedHandler;\n```\n\n## 3. Spam Filter\n\n```typescript\nconst score = parseFloat(message.headers.get(\"x-cf-spamh-score\") || \"0\");\nif (score > 5) {\n  message.setReject(\"Spam detected\");\n  return;\n}\nawait message.forward(\"inbox@corp.com\");\n```\n\n## 4. Archive to R2\n\n```typescript\ninterface Env { R2: R2Bucket; }\n\nexport default {\n  async email(message, env, ctx) {\n    const raw = await message.raw.arrayBuffer();\n    \n    const key = `${new Date().toISOString()}-${message.from}.eml`;\n    await env.R2.put(key, raw, { \n      httpMetadata: { contentType: \"message/rfc822\" }\n    });\n    \n    await message.forward(\"inbox@corp.com\");\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## 5. Store Metadata in KV\n\n```typescript\nimport PostalMime from 'postal-mime';\n\ninterface Env { KV: KVNamespace; }\n\nexport default {\n  async email(message, env, ctx) {\n    const raw = await message.raw.arrayBuffer();\n    const parser = new PostalMime();\n    const email = await parser.parse(raw);\n    \n    const metadata = {\n      from: email.from.address,\n      subject: email.subject,\n      timestamp: new Date().toISOString(),\n      size: raw.byteLength\n    };\n    \n    await env.KV.put(`email:${Date.now()}`, JSON.stringify(metadata));\n    await message.forward(\"inbox@corp.com\");\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## 6. Subject-Based Routing\n\n```typescript\nexport default {\n  async email(message, env, ctx) {\n    const subject = message.headers.get(\"subject\")?.toLowerCase() || \"\";\n    \n    if (subject.includes(\"[urgent]\")) {\n      await message.forward(\"oncall@corp.com\");\n    } else if (subject.includes(\"[billing]\")) {\n      await message.forward(\"billing@corp.com\");\n    } else if (subject.includes(\"[support]\")) {\n      await message.forward(\"support@corp.com\");\n    } else {\n      await message.forward(\"general@corp.com\");\n    }\n  }\n} satisfies ExportedHandler;\n```\n\n## 7. Auto-Reply\n\n```typescript\ninterface Env {\n  EMAIL: SendEmail;\n  REPLIED: KVNamespace;\n}\n\nexport default {\n  async email(message, env, ctx) {\n    const msgId = message.headers.get(\"message-id\");\n    \n    if (msgId && await env.REPLIED.get(msgId)) {\n      await message.forward(\"archive@corp.com\");\n      return;\n    }\n    \n    ctx.waitUntil((async () => {\n      await env.EMAIL.send({\n        from: \"noreply@yourdomain.com\",\n        to: message.from,\n        subject: \"Re: \" + (message.headers.get(\"subject\") || \"\"),\n        text: \"Thank you. We'll respond within 24h.\"\n      });\n      if (msgId) await env.REPLIED.put(msgId, \"1\", { expirationTtl: 604800 });\n    })());\n    \n    await message.forward(\"support@corp.com\");\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## 8. Extract Attachments\n\n```typescript\nimport PostalMime from 'postal-mime';\n\ninterface Env { ATTACHMENTS: R2Bucket; }\n\nexport default {\n  async email(message, env, ctx) {\n    const parser = new PostalMime();\n    const email = await parser.parse(await message.raw.arrayBuffer());\n    \n    for (const att of email.attachments) {\n      const key = `${Date.now()}-${att.filename}`;\n      await env.ATTACHMENTS.put(key, att.content, {\n        httpMetadata: { contentType: att.mimeType }\n      });\n    }\n    \n    await message.forward(\"inbox@corp.com\");\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## 9. Log to D1\n\n```typescript\nimport PostalMime from 'postal-mime';\n\ninterface Env { DB: D1Database; }\n\nexport default {\n  async email(message, env, ctx) {\n    const parser = new PostalMime();\n    const email = await parser.parse(await message.raw.arrayBuffer());\n    \n    ctx.waitUntil(\n      env.DB.prepare(\"INSERT INTO log (ts, from_addr, subj) VALUES (?, ?, ?)\")\n        .bind(new Date().toISOString(), email.from.address, email.subject || \"\")\n        .run()\n    );\n    \n    await message.forward(\"inbox@corp.com\");\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## 10. Multi-Tenant\n\n```typescript\ninterface Env { TENANTS: KVNamespace; }\n\nexport default {\n  async email(message, env, ctx) {\n    const subdomain = message.to.split(\"@\")[1].split(\".\")[0];\n    const config = await env.TENANTS.get(subdomain, \"json\") as { forward: string } | null;\n    \n    if (!config) {\n      message.setReject(\"Unknown tenant\");\n      return;\n    }\n    \n    await message.forward(config.forward);\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## Summary\n\n| Pattern | Use Case | Storage |\n|---------|----------|---------|\n| Allowlist | Security | None |\n| Parse | Body/attachments | None |\n| Spam Filter | Reduce spam | None |\n| R2 Archive | Email storage | R2 |\n| KV Meta | Analytics | KV |\n| Subject Route | Dept routing | None |\n| Auto-Reply | Support | KV |\n| Attachments | Doc mgmt | R2 |\n| D1 Log | Audit trail | D1 |\n| Multi-Tenant | SaaS | KV |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/email-workers/README.md",
    "content": "# Cloudflare Email Workers\n\nProcess incoming emails programmatically using Cloudflare Workers runtime.\n\n## Overview\n\nEmail Workers enable custom email processing logic at the edge. Build spam filters, auto-responders, ticket systems, notification handlers, and more using the same Workers runtime you use for HTTP requests.\n\n**Key capabilities**:\n- Process inbound emails with full message access\n- Forward to verified destinations\n- Send replies with proper threading\n- Parse MIME content and attachments\n- Integrate with KV, R2, D1, and external APIs\n\n## Quick Start\n\n### Minimal ES Modules Handler\n\n```typescript\nexport default {\n  async email(message, env, ctx) {\n    // Reject spam\n    if (message.from.includes('spam.com')) {\n      message.setReject('Blocked');\n      return;\n    }\n    \n    // Forward to inbox\n    await message.forward('inbox@example.com');\n  }\n};\n```\n\n### Core Operations\n\n| Operation | Method | Use Case |\n|-----------|--------|----------|\n| Forward | `message.forward(to, headers?)` | Route to verified destination |\n| Reject | `message.setReject(reason)` | Block with SMTP error |\n| Reply | `message.reply(emailMessage)` | Auto-respond with threading |\n| Parse | postal-mime library | Extract subject, body, attachments |\n\n## Reading Order\n\nFor comprehensive understanding, read files in this order:\n\n1. **README.md** (this file) - Overview and quick start\n2. **configuration.md** - Setup, deployment, bindings\n3. **api.md** - Complete API reference\n4. **patterns.md** - Real-world implementation examples\n5. **gotchas.md** - Critical pitfalls and debugging\n\n## In This Reference\n\n| File | Description | Key Topics |\n|------|-------------|------------|\n| [api.md](./api.md) | Complete API reference | ForwardableEmailMessage, SendEmail bindings, reply() method, postal-mime/mimetext APIs |\n| [configuration.md](./configuration.md) | Setup and configuration | wrangler.jsonc, bindings, deployment, dependencies |\n| [patterns.md](./patterns.md) | Real-world examples | Allowlists from KV, auto-reply with threading, attachment extraction, webhook notifications |\n| [gotchas.md](./gotchas.md) | Pitfalls and debugging | Stream consumption, ctx.waitUntil errors, security, limits |\n\n## Architecture\n\n```\nIncoming Email → Email Routing → Email Worker\n                                    ↓\n                              Process + Decide\n                                    ↓\n                    ┌───────────────┼───────────────┐\n                    ↓               ↓               ↓\n                Forward          Reply          Reject\n```\n\n**Event flow**:\n1. Email arrives at your domain\n2. Email Routing matches route (e.g., `support@example.com`)\n3. Bound Email Worker receives `ForwardableEmailMessage`\n4. Worker processes and takes action (forward/reply/reject)\n5. Email delivered or rejected based on worker logic\n\n## Key Concepts\n\n### Envelope vs Headers\n\n- **Envelope addresses** (`message.from`, `message.to`): SMTP transport addresses (trusted)\n- **Header addresses** (parsed from body): Display addresses (can be spoofed)\n\nUse envelope addresses for security decisions.\n\n### Single-Use Streams\n\n`message.raw` is a ReadableStream that can only be read once. Buffer to ArrayBuffer for multiple uses.\n\n```typescript\n// Buffer first\nconst buffer = await new Response(message.raw).arrayBuffer();\nconst email = await PostalMime.parse(buffer);\n```\n\nSee [gotchas.md](./gotchas.md#readablestream-can-only-be-consumed-once) for details.\n\n### Verified Destinations\n\n`forward()` only works with addresses verified in the Cloudflare Email Routing dashboard. Add destinations before deployment.\n\n## Use Cases\n\n- **Spam filtering**: Block based on sender, content, or reputation\n- **Auto-responders**: Send acknowledgment replies with threading\n- **Ticket creation**: Parse emails and create support tickets\n- **Email archival**: Store in KV, R2, or D1\n- **Notification routing**: Forward to Slack, Discord, or webhooks\n- **Attachment processing**: Extract files to R2 storage\n- **Multi-tenant routing**: Route based on recipient subdomain\n- **Size filtering**: Reject oversized attachments\n\n## Limits\n\n| Limit | Value |\n|-------|-------|\n| Max message size | 25 MiB |\n| Max routing rules | 200 |\n| Max destinations | 200 |\n| CPU time (free tier) | 10ms |\n| CPU time (paid tier) | 50ms |\n\nSee [gotchas.md](./gotchas.md#limits-reference) for complete limits table.\n\n## Prerequisites\n\nBefore deploying Email Workers:\n\n1. **Enable Email Routing** in Cloudflare dashboard for your domain\n2. **Verify destination addresses** for forwarding\n3. **Configure DMARC/SPF** for sending domains (required for replies)\n4. **Set up wrangler.jsonc** with SendEmail binding\n\nSee [configuration.md](./configuration.md) for detailed setup.\n\n## Service Worker Syntax (Deprecated)\n\nModern projects should use ES modules format shown above. Service Worker syntax (`addEventListener('email', ...)`) is deprecated but still supported.\n\n## See Also\n\n- [Email Routing Documentation](https://developers.cloudflare.com/email-routing/)\n- [Workers Platform](https://developers.cloudflare.com/workers/)\n- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/)\n- [postal-mime on npm](https://www.npmjs.com/package/postal-mime)\n- [mimetext on npm](https://www.npmjs.com/package/mimetext)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/email-workers/api.md",
    "content": "# Email Workers API Reference\n\nComplete API reference for Cloudflare Email Workers runtime.\n\n## ForwardableEmailMessage Interface\n\nThe main interface passed to email handlers.\n\n```typescript\ninterface ForwardableEmailMessage {\n  readonly from: string;        // Envelope MAIL FROM (SMTP sender)\n  readonly to: string;          // Envelope RCPT TO (SMTP recipient)\n  readonly headers: Headers;    // Web-standard Headers object\n  readonly raw: ReadableStream; // Raw MIME message (single-use stream)\n  readonly rawSize: number;     // Total message size in bytes\n  \n  setReject(reason: string): void;\n  forward(rcptTo: string, headers?: Headers): Promise<void>;\n  reply(message: EmailMessage): Promise<void>;\n}\n```\n\n### Properties\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `from` | string | Envelope sender (SMTP MAIL FROM) - use for security |\n| `to` | string | Envelope recipient (SMTP RCPT TO) |\n| `headers` | Headers | Message headers (Subject, Message-ID, etc.) |\n| `raw` | ReadableStream | Raw MIME message (**single-use**, buffer first) |\n| `rawSize` | number | Message size in bytes |\n\n### Methods\n\n#### setReject(reason: string): void\n\nReject with permanent SMTP 5xx error. Email not delivered, sender may receive bounce.\n\n```typescript\nif (blockList.includes(message.from)) {\n  message.setReject('Sender blocked');\n}\n```\n\n#### forward(rcptTo: string, headers?: Headers): Promise<void>\n\nForward to verified destination. Only `X-*` custom headers allowed.\n\n```typescript\nawait message.forward('inbox@example.com');\n\n// With custom headers\nconst h = new Headers();\nh.set('X-Processed-By', 'worker');\nawait message.forward('inbox@example.com', h);\n```\n\n#### reply(message: EmailMessage): Promise<void>\n\nSend a reply to the original sender (March 2025 feature).\n\n```typescript\nimport { EmailMessage } from 'cloudflare:email';\nimport { createMimeMessage } from 'mimetext';\n\nconst msg = createMimeMessage();\nmsg.setSender({ name: 'Support', addr: 'support@example.com' });\nmsg.setRecipient(message.from);\nmsg.setSubject(`Re: ${message.headers.get('Subject')}`);\nmsg.setHeader('In-Reply-To', message.headers.get('Message-ID'));\nmsg.setHeader('References', message.headers.get('References') || '');\nmsg.addMessage({\n  contentType: 'text/plain',\n  data: 'Thank you for your message.'\n});\n\nawait message.reply(new EmailMessage(\n  'support@example.com',\n  message.from,\n  msg.asRaw()\n));\n```\n\n**Requirements**:\n- Incoming email needs valid DMARC\n- Reply once per event, recipient = `message.from`\n- Sender domain = receiving domain, with DMARC/SPF/DKIM\n- Max 100 `References` entries\n- Threading: `In-Reply-To` (original Message-ID), `References`, new `Message-ID`\n\n## EmailMessage Constructor\n\n```typescript\nimport { EmailMessage } from 'cloudflare:email';\n\nnew EmailMessage(from: string, to: string, raw: ReadableStream | string)\n```\n\nUsed for sending emails (replies or via SendEmail binding). Domain must be verified.\n\n## SendEmail Interface\n\n```typescript\ninterface SendEmail {\n  send(message: EmailMessage): Promise<void>;\n}\n\n// Usage\nawait env.EMAIL.send(new EmailMessage(from, to, mimeContent));\n```\n\n## SendEmail Binding Types\n\n```jsonc\n{\n  \"send_email\": [\n    { \"name\": \"EMAIL\" },  // Type 1: Any verified address\n    { \"name\": \"LOGS\", \"destination_address\": \"logs@example.com\" },  // Type 2: Single dest\n    { \"name\": \"TEAM\", \"allowed_destination_addresses\": [\"a@ex.com\", \"b@ex.com\"] },  // Type 3: Dest allowlist\n    { \"name\": \"NOREPLY\", \"allowed_sender_addresses\": [\"noreply@ex.com\"] }  // Type 4: Sender allowlist\n  ]\n}\n```\n\n## postal-mime Parsed Output\n\npostal-mime v2.7.3 parses incoming emails into structured data.\n\n```typescript\ninterface ParsedEmail {\n  headers: Array<{ key: string; value: string }>;\n  from: { name: string; address: string } | null;\n  to: Array<{ name: string; address: string }> | { name: string; address: string } | null;\n  cc: Array<{ name: string; address: string }> | null;\n  bcc: Array<{ name: string; address: string }> | null;\n  subject: string;\n  messageId: string | null;\n  inReplyTo: string | null;\n  references: string | null;\n  date: string | null;\n  html: string | null;\n  text: string | null;\n  attachments: Array<{\n    filename: string;\n    mimeType: string;\n    disposition: string | null;\n    related: boolean;\n    contentId: string | null;\n    content: Uint8Array;\n  }>;\n}\n```\n\n### Usage\n\n```typescript\nimport PostalMime from 'postal-mime';\n\nconst buffer = await new Response(message.raw).arrayBuffer();\nconst email = await PostalMime.parse(buffer);\n\nconsole.log(email.subject);\nconsole.log(email.from?.address);\nconsole.log(email.text);\nconsole.log(email.attachments.length);\n```\n\n## mimetext API Quick Reference\n\nmimetext v3.0.27 composes outgoing emails.\n\n```typescript\nimport { createMimeMessage } from 'mimetext';\n\nconst msg = createMimeMessage();\n\n// Sender\nmsg.setSender({ name: 'John Doe', addr: 'john@example.com' });\n\n// Recipients\nmsg.setRecipient('alice@example.com');\nmsg.setRecipients(['bob@example.com', 'carol@example.com']);\nmsg.setCc('manager@example.com');\nmsg.setBcc(['audit@example.com']);\n\n// Headers\nmsg.setSubject('Meeting Notes');\nmsg.setHeader('In-Reply-To', '<previous-message-id>');\nmsg.setHeader('References', '<msg1> <msg2>');\nmsg.setHeader('Message-ID', `<${crypto.randomUUID()}@example.com>`);\n\n// Content\nmsg.addMessage({\n  contentType: 'text/plain',\n  data: 'Plain text content'\n});\n\nmsg.addMessage({\n  contentType: 'text/html',\n  data: '<p>HTML content</p>'\n});\n\n// Attachments\nmsg.addAttachment({\n  filename: 'report.pdf',\n  contentType: 'application/pdf',\n  data: pdfBuffer // Uint8Array or base64 string\n});\n\n// Generate raw MIME\nconst raw = msg.asRaw(); // Returns string\n```\n\n## TypeScript Types\n\n```typescript\nimport { \n  ForwardableEmailMessage,\n  EmailMessage \n} from 'cloudflare:email';\n\ninterface Env {\n  EMAIL: SendEmail;\n  EMAIL_ARCHIVE: KVNamespace;\n  ALLOWED_SENDERS: KVNamespace;\n}\n\nexport default {\n  async email(\n    message: ForwardableEmailMessage,\n    env: Env,\n    ctx: ExecutionContext\n  ): Promise<void> {\n    // Fully typed\n  }\n};\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/email-workers/configuration.md",
    "content": "# Email Workers Configuration\n\n## wrangler.jsonc\n\n```jsonc\n{\n  \"name\": \"email-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-27\",\n  \"send_email\": [\n    { \"name\": \"EMAIL\" },                                    // Unrestricted\n    { \"name\": \"EMAIL_LOGS\", \"destination_address\": \"logs@example.com\" },  // Single dest\n    { \"name\": \"EMAIL_TEAM\", \"allowed_destination_addresses\": [\"a@ex.com\", \"b@ex.com\"] },\n    { \"name\": \"EMAIL_NOREPLY\", \"allowed_sender_addresses\": [\"noreply@ex.com\"] }\n  ],\n  \"kv_namespaces\": [{ \"binding\": \"ARCHIVE\", \"id\": \"xxx\" }],\n  \"r2_buckets\": [{ \"binding\": \"ATTACHMENTS\", \"bucket_name\": \"email-attachments\" }],\n  \"vars\": { \"WEBHOOK_URL\": \"https://hooks.example.com\" }\n}\n```\n\n## TypeScript Types\n\n```typescript\ninterface Env {\n  EMAIL: SendEmail;\n  ARCHIVE: KVNamespace;\n  ATTACHMENTS: R2Bucket;\n  WEBHOOK_URL: string;\n}\n\nexport default {\n  async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) {}\n};\n```\n\n## Dependencies\n\n```bash\nnpm install postal-mime mimetext\nnpm install -D @cloudflare/workers-types wrangler typescript\n```\n\nUse postal-mime v2.x, mimetext v3.x.\n\n## tsconfig.json\n\n```json\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\", \"module\": \"ES2022\", \"lib\": [\"ES2022\"],\n    \"types\": [\"@cloudflare/workers-types\"],\n    \"moduleResolution\": \"bundler\", \"strict\": true\n  }\n}\n```\n\n## Local Development\n\n```bash\nnpx wrangler dev\n\n# Test receiving\ncurl --request POST 'http://localhost:8787/cdn-cgi/handler/email' \\\n  --url-query 'from=sender@example.com' --url-query 'to=recipient@example.com' \\\n  --header 'Content-Type: text/plain' --data-raw 'Subject: Test\\n\\nHello'\n```\n\nSent emails write to local `.eml` files.\n\n## Deployment Checklist\n\n- [ ] Enable Email Routing in dashboard\n- [ ] Verify destination addresses\n- [ ] Configure DMARC/SPF/DKIM for sending\n- [ ] Create KV/R2 resources if needed\n- [ ] Update wrangler.jsonc with production IDs\n\n```bash\nnpx wrangler deploy\nnpx wrangler deployments list\n```\n\n## Dashboard Setup\n\n1. **Email Routing:** Domain → Email → Enable Email Routing\n2. **Verify addresses:** Email → Destination addresses → Add & verify\n3. **Bind Worker:** Email → Email Workers → Create route → Select pattern & Worker\n4. **DMARC:** Add TXT `_dmarc.domain.com`: `v=DMARC1; p=quarantine;`\n\n## Secrets\n\n```bash\nnpx wrangler secret put API_KEY\n# Access: env.API_KEY\n```\n\n## Monitoring\n\n```bash\nnpx wrangler tail\nnpx wrangler tail --status error\nnpx wrangler tail --format json\n```\n\n## Troubleshooting\n\n| Error | Fix |\n|-------|-----|\n| \"Binding not found\" | Check `send_email` name matches code |\n| \"Invalid destination\" | Verify in Email Routing dashboard |\n| Type errors | Install `@cloudflare/workers-types` |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/email-workers/gotchas.md",
    "content": "# Email Workers Gotchas\n\n## Critical Issues\n\n### ReadableStream Single-Use\n\n```typescript\n// ❌ WRONG: Stream consumed twice\nconst email = await PostalMime.parse(await new Response(message.raw).arrayBuffer());\nconst rawText = await new Response(message.raw).text(); // EMPTY!\n\n// ✅ CORRECT: Buffer first\nconst buffer = await new Response(message.raw).arrayBuffer();\nconst email = await PostalMime.parse(buffer);\nconst rawText = new TextDecoder().decode(buffer);\n```\n\n### ctx.waitUntil() Errors Silent\n\n```typescript\n// ❌ Errors dropped silently\nctx.waitUntil(fetch(webhookUrl, { method: 'POST', body: data }));\n\n// ✅ Catch and log\nctx.waitUntil(\n  fetch(webhookUrl, { method: 'POST', body: data })\n    .catch(err => env.ERROR_LOG.put(`error:${Date.now()}`, err.message))\n);\n```\n\n## Security\n\n### Envelope vs Header From (Spoofing)\n\n```typescript\nconst envelopeFrom = message.from;               // SMTP MAIL FROM (trusted)\nconst headerFrom = (await PostalMime.parse(buffer)).from?.address; // (untrusted)\n// Use envelope for security decisions\n```\n\n### Input Validation\n\n```typescript\nif (message.rawSize > 5_000_000) { message.setReject('Too large'); return; }\nif ((message.headers.get('Subject') || '').length > 1000) {\n  message.setReject('Invalid subject'); return;\n}\n```\n\n### DMARC for Replies\n\nReplies fail silently without DMARC. Verify: `dig TXT _dmarc.example.com`\n\n## Parsing\n\n### Address Parsing\n\n```typescript\nconst email = await PostalMime.parse(buffer);\nconst fromAddress = email.from?.address || 'unknown';\nconst toAddresses = Array.isArray(email.to) ? email.to.map(t => t.address) : [email.to?.address];\n```\n\n### Character Encoding\n\nLet postal-mime handle decoding - `email.subject`, `email.text`, `email.html` are UTF-8.\n\n## API Behavior\n\n### setReject() vs throw\n\n```typescript\n// setReject() for SMTP rejection\nif (blockList.includes(message.from)) { message.setReject('Blocked'); return; }\n\n// throw for worker errors\nif (!env.KV) throw new Error('KV not configured');\n```\n\n### forward() Only X-* Headers\n\n```typescript\nheaders.set('X-Processed-By', 'worker');  // ✅ Works\nheaders.set('Subject', 'Modified');        // ❌ Dropped\n```\n\n### Reply Requires Verified Domain\n\n```typescript\n// Use same domain as receiving address\nconst receivingDomain = message.to.split('@')[1];\nawait message.reply(new EmailMessage(`noreply@${receivingDomain}`, message.from, rawMime));\n```\n\n## Performance\n\n### CPU Limit\n\n```typescript\n// Skip parsing large emails\nif (message.rawSize > 5_000_000) {\n  await message.forward('inbox@example.com');\n  return;\n}\n```\n\nMonitor: `npx wrangler tail`\n\n## Limits\n\n| Limit | Value |\n|-------|-------|\n| Max message size | 25 MiB |\n| Max rules/zone | 200 |\n| CPU time (free/paid) | 10ms / 50ms |\n| Reply References | 100 |\n\n## Common Errors\n\n| Error | Fix |\n|-------|-----|\n| \"Address not verified\" | Add in Email Routing dashboard |\n| \"Exceeded CPU time\" | Use `ctx.waitUntil()` or upgrade |\n| \"Stream is locked\" | Buffer `message.raw` first |\n| Silent reply failure | Check DMARC records |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/email-workers/patterns.md",
    "content": "# Email Workers Patterns\n\n## Parse Email\n\n```typescript\nimport PostalMime from 'postal-mime';\n\nexport default {\n  async email(message, env, ctx) {\n    const buffer = await new Response(message.raw).arrayBuffer();\n    const email = await PostalMime.parse(buffer);\n    console.log(email.from, email.subject, email.text, email.attachments.length);\n    await message.forward('inbox@example.com');\n  }\n};\n```\n\n## Filtering\n\n```typescript\n// Allowlist from KV\nconst allowList = await env.ALLOWED_SENDERS.get('list', 'json') || [];\nif (!allowList.includes(message.from)) {\n  message.setReject('Not allowed');\n  return;\n}\n\n// Size check (avoid parsing large emails)\nif (message.rawSize > 5_000_000) {\n  await message.forward('inbox@example.com'); // Forward without parsing\n  return;\n}\n```\n\n## Auto-Reply with Threading\n\n```typescript\nimport { EmailMessage } from 'cloudflare:email';\nimport { createMimeMessage } from 'mimetext';\n\nconst msg = createMimeMessage();\nmsg.setSender({ addr: 'support@example.com' });\nmsg.setRecipient(message.from);\nmsg.setSubject(`Re: ${message.headers.get('Subject')}`);\nmsg.setHeader('In-Reply-To', message.headers.get('Message-ID') || '');\nmsg.addMessage({ contentType: 'text/plain', data: 'Thank you. We will respond.' });\n\nawait message.reply(new EmailMessage('support@example.com', message.from, msg.asRaw()));\n```\n\n## Rate-Limited Auto-Reply\n\n```typescript\nconst rateKey = `rate:${message.from}`;\nif (!await env.RATE_LIMIT.get(rateKey)) {\n  // Send reply...\n  ctx.waitUntil(env.RATE_LIMIT.put(rateKey, '1', { expirationTtl: 3600 }));\n}\n```\n\n## Subject-Based Routing\n\n```typescript\nconst subject = (message.headers.get('Subject') || '').toLowerCase();\nif (subject.includes('billing')) await message.forward('billing@example.com');\nelse if (subject.includes('support')) await message.forward('support@example.com');\nelse await message.forward('general@example.com');\n```\n\n## Multi-Tenant Routing\n\n```typescript\n// support+tenant123@example.com → tenant123\nconst tenantId = message.to.split('@')[0].match(/\\+(.+)$/)?.[1] || 'default';\nconst config = await env.TENANT_CONFIG.get(tenantId, 'json');\nconfig?.forwardTo ? await message.forward(config.forwardTo) : message.setReject('Unknown');\n```\n\n## Archive & Extract Attachments\n\n```typescript\n// Archive to KV\nctx.waitUntil(env.ARCHIVE.put(`email:${Date.now()}`, JSON.stringify({\n  from: message.from, subject: email.subject\n})));\n\n// Attachments to R2\nfor (const att of email.attachments) {\n  ctx.waitUntil(env.R2.put(`${Date.now()}-${att.filename}`, att.content));\n}\n```\n\n## Webhook Integration\n\n```typescript\nctx.waitUntil(\n  fetch(env.WEBHOOK_URL, {\n    method: 'POST',\n    body: JSON.stringify({ from: message.from, subject: message.headers.get('Subject') })\n  }).catch(err => console.error(err))\n);\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/hyperdrive/README.md",
    "content": "# Hyperdrive\n\nAccelerates database queries from Workers via connection pooling, edge setup, query caching.\n\n## Key Features\n\n- **Connection Pooling**: Persistent connections eliminate TCP/TLS/auth handshakes (~7 round-trips)\n- **Edge Setup**: Connection negotiation at edge, pooling near origin\n- **Query Caching**: Auto-cache non-mutating queries (default 60s TTL)\n- **Support**: PostgreSQL, MySQL + compatibles (CockroachDB, Timescale, PlanetScale, Neon, Supabase)\n\n## Architecture\n\n```\nWorker → Edge (setup) → Pool (near DB) → Origin\n         ↓ cached reads\n         Cache\n```\n\n## Quick Start\n\n```bash\n# Create config\nnpx wrangler hyperdrive create my-db \\\n  --connection-string=\"postgres://user:pass@host:5432/db\"\n\n# wrangler.jsonc\n{\n  \"compatibility_flags\": [\"nodejs_compat\"],\n  \"hyperdrive\": [{\"binding\": \"HYPERDRIVE\", \"id\": \"<ID>\"}]\n}\n```\n\n```typescript\nimport { Client } from \"pg\";\n\nexport default {\n  async fetch(req: Request, env: Env): Promise<Response> {\n    const client = new Client({\n      connectionString: env.HYPERDRIVE.connectionString,\n    });\n    await client.connect();\n    const result = await client.query(\"SELECT * FROM users WHERE id = $1\", [123]);\n    await client.end();\n    return Response.json(result.rows);\n  },\n};\n```\n\n## When to Use\n\n✅ Global access to single-region DBs, high read ratios, popular queries, connection-heavy loads\n❌ Write-heavy, real-time data (<1s), single-region apps close to DB\n\n**💡 Pair with Smart Placement** for Workers making multiple queries - executes near DB to minimize latency.\n\n## Driver Choice\n\n| Driver | Use When | Notes |\n|--------|----------|-------|\n| **pg** (recommended) | General use, TypeScript, ecosystem compatibility | Stable, widely used, works with most ORMs |\n| **postgres.js** | Advanced features, template literals, streaming | Lighter than pg, `prepare: true` is default |\n| **mysql2** | MySQL/MariaDB/PlanetScale | MySQL only, less mature support |\n\n## Reading Order\n\n| New to Hyperdrive | Implementing | Troubleshooting |\n|-------------------|--------------|-----------------|\n| 1. README (this) | 1. [configuration.md](./configuration.md) | 1. [gotchas.md](./gotchas.md) |\n| 2. [configuration.md](./configuration.md) | 2. [api.md](./api.md) | 2. [patterns.md](./patterns.md) |\n| 3. [api.md](./api.md) | 3. [patterns.md](./patterns.md) | 3. [api.md](./api.md) |\n\n## In This Reference\n- [configuration.md](./configuration.md) - Setup, wrangler config, Smart Placement\n- [api.md](./api.md) - Binding APIs, query patterns, driver usage\n- [patterns.md](./patterns.md) - Use cases, ORMs, multi-query optimization\n- [gotchas.md](./gotchas.md) - Limits, troubleshooting, connection management\n\n## See Also\n- [smart-placement](../smart-placement/) - Optimize multi-query Workers near databases\n- [d1](../d1/) - Serverless SQLite alternative for edge-native apps\n- [workers](../workers/) - Worker runtime with database bindings\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/hyperdrive/api.md",
    "content": "# API Reference\n\nSee [README.md](./README.md) for overview, [configuration.md](./configuration.md) for setup.\n\n## Binding Interface\n\n```typescript\ninterface Hyperdrive {\n  connectionString: string;  // PostgreSQL\n  // MySQL properties:\n  host: string;\n  port: number;\n  user: string;\n  password: string;\n  database: string;\n}\n\ninterface Env {\n  HYPERDRIVE: Hyperdrive;\n}\n```\n\n**Generate types:** `npx wrangler types` (auto-creates worker-configuration.d.ts from wrangler.jsonc)\n\n## PostgreSQL (node-postgres) - RECOMMENDED\n\n```typescript\nimport { Client } from \"pg\";  // pg@^8.17.2\n\nexport default {\n  async fetch(req: Request, env: Env): Promise<Response> {\n    const client = new Client({connectionString: env.HYPERDRIVE.connectionString});\n    try {\n      await client.connect();\n      const result = await client.query(\"SELECT * FROM users WHERE id = $1\", [123]);\n      return Response.json(result.rows);\n    } finally {\n      await client.end();\n    }\n  },\n};\n```\n\n**⚠️ Workers connection limit: 6 per Worker invocation** - use connection pooling wisely.\n\n## PostgreSQL (postgres.js)\n\n```typescript\nimport postgres from \"postgres\";  // postgres@^3.4.8\n\nconst sql = postgres(env.HYPERDRIVE.connectionString, {\n  max: 5,             // Limit per Worker (Workers max: 6)\n  prepare: true,      // Enabled by default, required for caching\n  fetch_types: false, // Reduce latency if not using arrays\n});\n\nconst users = await sql`SELECT * FROM users WHERE active = ${true} LIMIT 10`;\n```\n\n**⚠️ `prepare: true` is enabled by default and required for Hyperdrive caching.** Setting to `false` disables prepared statements + cache.\n\n## MySQL (mysql2)\n\n```typescript\nimport { createConnection } from \"mysql2/promise\";  // mysql2@^3.16.2\n\nconst conn = await createConnection({\n  host: env.HYPERDRIVE.host,\n  user: env.HYPERDRIVE.user,\n  password: env.HYPERDRIVE.password,\n  database: env.HYPERDRIVE.database,\n  port: env.HYPERDRIVE.port,\n  disableEval: true,  // ⚠️ REQUIRED for Workers\n});\n\nconst [results] = await conn.query(\"SELECT * FROM users WHERE active = ? LIMIT ?\", [true, 10]);\nctx.waitUntil(conn.end());\n```\n\n**⚠️ MySQL support is less mature than PostgreSQL** - expect fewer optimizations and potential edge cases.\n\n## Query Caching\n\n**Cacheable:**\n```sql\nSELECT * FROM posts WHERE published = true;\nSELECT COUNT(*) FROM users;\n```\n\n**NOT cacheable:**\n```sql\n-- Writes\nINSERT/UPDATE/DELETE\n\n-- Volatile functions\nSELECT NOW();\nSELECT random();\nSELECT LASTVAL();  -- PostgreSQL\nSELECT UUID();     -- MySQL\n```\n\n**Cache config:**\n- Default: `max_age=60s`, `swr=15s`\n- Max `max_age`: 3600s\n- Disable: `--caching-disabled=true`\n\n**Multiple configs pattern:**\n```typescript\n// Reads: cached\nconst sqlCached = postgres(env.HYPERDRIVE_CACHED.connectionString);\nconst posts = await sqlCached`SELECT * FROM posts ORDER BY views DESC LIMIT 10`;\n\n// Writes/time-sensitive: no cache\nconst sqlNoCache = postgres(env.HYPERDRIVE_NO_CACHE.connectionString);\nconst orders = await sqlNoCache`SELECT * FROM orders WHERE created_at > NOW() - INTERVAL 5 MINUTE`;\n```\n\n## ORMs\n\n**Drizzle:**\n```typescript\nimport { drizzle } from \"drizzle-orm/postgres-js\";  // drizzle-orm@^0.45.1\nimport postgres from \"postgres\";\n\nconst client = postgres(env.HYPERDRIVE.connectionString, {max: 5, prepare: true});\nconst db = drizzle(client);\nconst users = await db.select().from(users).where(eq(users.active, true)).limit(10);\n```\n\n**Kysely:**\n```typescript\nimport { Kysely, PostgresDialect } from \"kysely\";  // kysely@^0.27+\nimport postgres from \"postgres\";\n\nconst db = new Kysely({\n  dialect: new PostgresDialect({\n    postgres: postgres(env.HYPERDRIVE.connectionString, {max: 5, prepare: true}),\n  }),\n});\nconst users = await db.selectFrom(\"users\").selectAll().where(\"active\", \"=\", true).execute();\n```\n\nSee [patterns.md](./patterns.md) for use cases, [gotchas.md](./gotchas.md) for limits.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/hyperdrive/configuration.md",
    "content": "# Configuration\n\nSee [README.md](./README.md) for overview.\n\n## Create Config\n\n**PostgreSQL:**\n```bash\n# Basic\nnpx wrangler hyperdrive create my-db \\\n  --connection-string=\"postgres://user:pass@host:5432/db\"\n\n# Custom cache\nnpx wrangler hyperdrive create my-db \\\n  --connection-string=\"postgres://...\" \\\n  --max-age=120 --swr=30\n\n# No cache\nnpx wrangler hyperdrive create my-db \\\n  --connection-string=\"postgres://...\" \\\n  --caching-disabled=true\n```\n\n**MySQL:**\n```bash\nnpx wrangler hyperdrive create my-db \\\n  --connection-string=\"mysql://user:pass@host:3306/db\"\n```\n\n## wrangler.jsonc\n\n```jsonc\n{\n  \"compatibility_date\": \"2025-01-01\", // Use latest for new projects\n  \"compatibility_flags\": [\"nodejs_compat\"],\n  \"hyperdrive\": [\n    {\n      \"binding\": \"HYPERDRIVE\",\n      \"id\": \"<HYPERDRIVE_ID>\",\n      \"localConnectionString\": \"postgres://user:pass@localhost:5432/dev\"\n    }\n  ]\n}\n```\n\n**Generate TypeScript types:** Run `npx wrangler types` to auto-generate `worker-configuration.d.ts` from your wrangler.jsonc.\n\n**Multiple configs:**\n```jsonc\n{\n  \"hyperdrive\": [\n    {\"binding\": \"HYPERDRIVE_CACHED\", \"id\": \"<ID1>\"},\n    {\"binding\": \"HYPERDRIVE_NO_CACHE\", \"id\": \"<ID2>\"}\n  ]\n}\n```\n\n## Management\n\n```bash\nnpx wrangler hyperdrive list\nnpx wrangler hyperdrive get <ID>\nnpx wrangler hyperdrive update <ID> --max-age=180\nnpx wrangler hyperdrive delete <ID>\n```\n\n## Config Options\n\nHyperdrive create/update CLI flags:\n\n| Option | Default | Notes |\n|--------|---------|-------|\n| `--caching-disabled` | `false` | Disable caching |\n| `--max-age` | `60` | Cache TTL (max 3600s) |\n| `--swr` | `15` | Stale-while-revalidate |\n| `--origin-connection-limit` | 20/100 | Free/paid |\n| `--access-client-id` | - | Tunnel auth |\n| `--access-client-secret` | - | Tunnel auth |\n| `--sslmode` | `require` | PostgreSQL only |\n\n## Smart Placement Integration\n\nFor Workers making **multiple queries** per request, enable Smart Placement to execute near your database:\n\n```jsonc\n{\n  \"compatibility_date\": \"2025-01-01\",\n  \"compatibility_flags\": [\"nodejs_compat\"],\n  \"placement\": {\n    \"mode\": \"smart\"\n  },\n  \"hyperdrive\": [\n    {\n      \"binding\": \"HYPERDRIVE\",\n      \"id\": \"<HYPERDRIVE_ID>\"\n    }\n  ]\n}\n```\n\n**Benefits:** Multi-query Workers run closer to DB, reducing round-trip latency. See [patterns.md](./patterns.md) for examples.\n\n## Private DB via Tunnel\n\n```\nWorker → Hyperdrive → Access → Tunnel → Private Network → DB\n```\n\n**Setup:**\n```bash\n# 1. Create tunnel\ncloudflared tunnel create my-db-tunnel\n\n# 2. Configure hostname in Zero Trust dashboard\n#    Domain: db-tunnel.example.com\n#    Service: TCP -> localhost:5432\n\n# 3. Create service token (Zero Trust > Service Auth)\n#    Save Client ID/Secret\n\n# 4. Create Access app (db-tunnel.example.com)\n#    Policy: Service Auth token from step 3\n\n# 5. Create Hyperdrive\nnpx wrangler hyperdrive create my-private-db \\\n  --host=db-tunnel.example.com \\\n  --user=dbuser --password=dbpass --database=prod \\\n  --access-client-id=<ID> --access-client-secret=<SECRET>\n```\n\n**⚠️ Don't specify `--port` with Tunnel** - port configured in tunnel service settings.\n\n## Local Dev\n\n**Option 1: Local (RECOMMENDED):**\n```bash\n# Env var (takes precedence)\nexport CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE=\"postgres://user:pass@localhost:5432/dev\"\nnpx wrangler dev\n\n# wrangler.jsonc\n{\"hyperdrive\": [{\"binding\": \"HYPERDRIVE\", \"localConnectionString\": \"postgres://...\"}]}\n```\n\n**Remote DB locally:**\n```bash\n# PostgreSQL\nexport CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE=\"postgres://user:pass@remote:5432/db?sslmode=require\"\n\n# MySQL\nexport CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE=\"mysql://user:pass@remote:3306/db?sslMode=REQUIRED\"\n```\n\n**Option 2: Remote execution:**\n```bash\nnpx wrangler dev --remote  # Uses deployed config, affects production\n```\n\nSee [api.md](./api.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md).\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/hyperdrive/gotchas.md",
    "content": "# Gotchas\n\nSee [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md).\n\n## Common Errors\n\n### \"Too many open connections\" / \"Connection limit exceeded\"\n\n**Cause:** Workers have a hard limit of **6 concurrent connections per invocation**  \n**Solution:** Set `max: 5` in driver config, reuse connections, ensure proper cleanup with `client.end()` or `ctx.waitUntil(conn.end())`\n\n### \"Failed to acquire a connection (Pool exhausted)\"\n\n**Cause:** All connections in pool are in use, often due to long-running transactions  \n**Solution:** Reduce transaction duration, avoid queries >60s, don't hold connections during external calls, or upgrade to paid plan for more connections\n\n### \"connection_refused\"\n\n**Cause:** Database refusing connections due to firewall, connection limits, or service down  \n**Solution:** Check firewall allows Cloudflare IPs, verify DB listening on port, confirm service running, and validate credentials\n\n### \"Query timeout (deadline exceeded)\"\n\n**Cause:** Query execution exceeding 60s timeout limit  \n**Solution:** Optimize with indexes, reduce dataset with LIMIT, break into smaller queries, or use async processing\n\n### \"password authentication failed\"\n\n**Cause:** Invalid credentials in Hyperdrive configuration  \n**Solution:** Check username and password in Hyperdrive config match database credentials\n\n### \"SSL/TLS connection error\"\n\n**Cause:** SSL/TLS configuration mismatch between Hyperdrive and database  \n**Solution:** Add `sslmode=require` (Postgres) or `sslMode=REQUIRED` (MySQL), upload CA cert if self-signed, verify DB has SSL enabled, and check cert expiry\n\n### \"Queries not being cached\"\n\n**Cause:** Query is mutating (INSERT/UPDATE/DELETE), contains volatile functions (NOW(), RANDOM()), or caching disabled  \n**Solution:** Verify query is non-mutating SELECT, avoid volatile functions, confirm caching enabled, use `wrangler dev --remote` to test, and set `prepare=true` for postgres.js\n\n### \"Slow multi-query Workers despite Hyperdrive\"\n\n**Cause:** Worker executing at edge, each query round-trips to DB region  \n**Solution:** Enable Smart Placement (`\"placement\": {\"mode\": \"smart\"}` in wrangler.jsonc) to execute Worker near DB. See [patterns.md](./patterns.md) Multi-Query pattern.\n\n### \"Local database connection failed\"\n\n**Cause:** `localConnectionString` incorrect or database not running  \n**Solution:** Verify `localConnectionString` correct, check DB running, confirm env var name matches binding, and test with psql/mysql client\n\n### \"Environment variable not working\"\n\n**Cause:** Environment variable format incorrect or not exported  \n**Solution:** Use format `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_<BINDING>`, ensure binding matches wrangler.jsonc, export variable in shell, and restart wrangler dev\n\n## Limits\n\n| Limit | Free | Paid | Notes |\n|-------|------|------|-------|\n| Max configs | 10 | 25 | Hyperdrive configurations per account |\n| Worker connections | 6 | 6 | Max concurrent connections per Worker invocation |\n| Username/DB name | 63 bytes | 63 bytes | Maximum length |\n| Connection timeout | 15s | 15s | Time to establish connection |\n| Idle timeout | 10 min | 10 min | Connection idle timeout |\n| Max origin connections | ~20 | ~100 | Connections to origin database |\n| Query duration max | 60s | 60s | Queries >60s terminated |\n| Cached response max | 50 MB | 50 MB | Responses >50MB returned but not cached |\n\n## Resources\n\n- [Docs](https://developers.cloudflare.com/hyperdrive/)\n- [Getting Started](https://developers.cloudflare.com/hyperdrive/get-started/)\n- [Wrangler Reference](https://developers.cloudflare.com/hyperdrive/reference/wrangler-commands/)\n- [Supported DBs](https://developers.cloudflare.com/hyperdrive/reference/supported-databases-and-features/)\n- [Discord #hyperdrive](https://discord.cloudflare.com)\n- [Limit Increase Form](https://forms.gle/ukpeZVLWLnKeixDu7)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/hyperdrive/patterns.md",
    "content": "# Patterns\n\nSee [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md).\n\n## High-Traffic Read-Heavy\n\n```typescript\nconst sql = postgres(env.HYPERDRIVE.connectionString, {max: 5, prepare: true});\n\n// Cacheable: popular content\nconst posts = await sql`SELECT * FROM posts WHERE published = true ORDER BY views DESC LIMIT 20`;\n\n// Cacheable: user profiles\nconst [user] = await sql`SELECT id, username, bio FROM users WHERE id = ${userId}`;\n```\n\n**Benefits:** Trending/profiles cached (60s), connection pooling handles spikes.\n\n## Mixed Read/Write\n\n```typescript\ninterface Env {\n  HYPERDRIVE_CACHED: Hyperdrive;    // max_age=120\n  HYPERDRIVE_REALTIME: Hyperdrive;  // caching disabled\n}\n\n// Reads: cached\nif (req.method === \"GET\") {\n  const sql = postgres(env.HYPERDRIVE_CACHED.connectionString, {prepare: true});\n  const products = await sql`SELECT * FROM products WHERE category = ${cat}`;\n}\n\n// Writes: no cache (immediate consistency)\nif (req.method === \"POST\") {\n  const sql = postgres(env.HYPERDRIVE_REALTIME.connectionString, {prepare: true});\n  await sql`INSERT INTO orders ${sql(data)}`;\n}\n```\n\n## Analytics Dashboard\n\n```typescript\nconst client = new Client({connectionString: env.HYPERDRIVE.connectionString});\nawait client.connect();\n\n// Aggregate queries cached (use fixed timestamps for caching)\nconst thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();\nconst dailyStats = await client.query(`\n  SELECT DATE(created_at) as date, COUNT(*) as orders, SUM(amount) as revenue\n  FROM orders WHERE created_at >= $1\n  GROUP BY DATE(created_at) ORDER BY date DESC\n`, [thirtyDaysAgo]);\n\nconst sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();\nconst topProducts = await client.query(`\n  SELECT p.name, COUNT(oi.id) as count, SUM(oi.quantity * oi.price) as revenue\n  FROM order_items oi JOIN products p ON oi.product_id = p.id\n  WHERE oi.created_at >= $1\n  GROUP BY p.id, p.name ORDER BY revenue DESC LIMIT 10\n`, [sevenDaysAgo]);\n```\n\n**Benefits:** Expensive aggregations cached (avoid NOW() for cacheability), dashboard instant, reduced DB load.\n\n## Multi-Tenant\n\n```typescript\nconst tenantId = req.headers.get(\"X-Tenant-ID\");\nconst sql = postgres(env.HYPERDRIVE.connectionString, {prepare: true});\n\n// Tenant-scoped queries cached separately\nconst docs = await sql`\n  SELECT * FROM documents \n  WHERE tenant_id = ${tenantId} AND deleted_at IS NULL\n  ORDER BY updated_at DESC LIMIT 50\n`;\n```\n\n**Benefits:** Per-tenant caching, shared connection pool, protects DB from multi-tenant load.\n\n## Geographically Distributed\n\n```typescript\n// Worker runs at edge nearest user\n// Connection setup at edge (fast), pooling near DB (efficient)\nconst sql = postgres(env.HYPERDRIVE.connectionString, {prepare: true});\nconst [user] = await sql`SELECT * FROM users WHERE id = ${userId}`;\n\nreturn Response.json({\n  user,\n  serverRegion: req.cf?.colo,  // Edge location\n});\n```\n\n**Benefits:** Edge setup + DB pooling = global → single-region DB without replication.\n\n## Multi-Query + Smart Placement\n\nFor Workers making **multiple queries** per request, enable Smart Placement to execute near DB:\n\n```jsonc\n// wrangler.jsonc\n{\n  \"placement\": {\"mode\": \"smart\"},\n  \"hyperdrive\": [{\"binding\": \"HYPERDRIVE\", \"id\": \"<ID>\"}]\n}\n```\n\n```typescript\nconst sql = postgres(env.HYPERDRIVE.connectionString, {prepare: true});\n\n// Multiple queries benefit from Smart Placement\nconst [user] = await sql`SELECT * FROM users WHERE id = ${userId}`;\nconst orders = await sql`SELECT * FROM orders WHERE user_id = ${userId} ORDER BY created_at DESC LIMIT 10`;\nconst stats = await sql`SELECT COUNT(*) as total, SUM(amount) as spent FROM orders WHERE user_id = ${userId}`;\n\nreturn Response.json({user, orders, stats});\n```\n\n**Benefits:** Worker executes near DB → reduces latency for each query. Without Smart Placement, each query round-trips from edge.\n\n## Connection Pooling\n\nOperates in **transaction mode**: connection acquired per transaction, `RESET` on return.\n\n**SET statements:**\n```typescript\n// ✅ Within transaction\nawait client.query(\"BEGIN\");\nawait client.query(\"SET work_mem = '256MB'\");\nawait client.query(\"SELECT * FROM large_table\");  // Uses SET\nawait client.query(\"COMMIT\");  // RESET after\n\n// ✅ Single statement\nawait client.query(\"SET work_mem = '256MB'; SELECT * FROM large_table\");\n\n// ❌ Across queries (may get different connection)\nawait client.query(\"SET work_mem = '256MB'\");\nawait client.query(\"SELECT * FROM large_table\");  // SET not applied\n```\n\n**Best practices:**\n```typescript\n// ❌ Long transactions block pooling\nawait client.query(\"BEGIN\");\nawait processThousands();  // Connection held entire time\nawait client.query(\"COMMIT\");\n\n// ✅ Short transactions\nawait client.query(\"BEGIN\");\nawait client.query(\"UPDATE users SET status = $1 WHERE id = $2\", [status, id]);\nawait client.query(\"COMMIT\");\n\n// ✅ SET LOCAL within transaction\nawait client.query(\"BEGIN\");\nawait client.query(\"SET LOCAL work_mem = '256MB'\");\nawait client.query(\"SELECT * FROM large_table\");\nawait client.query(\"COMMIT\");\n```\n\n## Performance Tips\n\n**Enable prepared statements (required for caching):**\n```typescript\nconst sql = postgres(connectionString, {prepare: true});  // Default, enables caching\n```\n\n**Optimize connection settings:**\n```typescript\nconst sql = postgres(connectionString, {\n  max: 5,             // Stay under Workers' 6 connection limit\n  fetch_types: false, // Reduce latency if not using arrays\n  idle_timeout: 60,   // Match Worker lifetime\n});\n```\n\n**Write cache-friendly queries:**\n```typescript\n// ✅ Cacheable (deterministic)\nawait sql`SELECT * FROM products WHERE category = 'electronics' LIMIT 10`;\n\n// ❌ Not cacheable (volatile NOW())\nawait sql`SELECT * FROM logs WHERE created_at > NOW()`;\n\n// ✅ Cacheable (parameterized timestamp)\nconst ts = Date.now();\nawait sql`SELECT * FROM logs WHERE created_at > ${ts}`;\n```\n\nSee [gotchas.md](./gotchas.md) for limits, troubleshooting.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/images/README.md",
    "content": "# Cloudflare Images Skill Reference\n\n**Cloudflare Images** is an end-to-end image management solution providing storage, transformation, optimization, and delivery at scale via Cloudflare's global network.\n\n## Quick Decision Tree\n\n**Need to:**\n- **Transform in Worker?** → [api.md](api.md#workers-binding-api-2026-primary-method) (Workers Binding API)\n- **Upload from Worker?** → [api.md](api.md#upload-from-worker) (REST API)\n- **Upload from client?** → [patterns.md](patterns.md#upload-from-client-direct-creator-upload) (Direct Creator Upload)\n- **Set up variants?** → [configuration.md](configuration.md#variants-configuration)\n- **Serve responsive images?** → [patterns.md](patterns.md#responsive-images)\n- **Add watermarks?** → [patterns.md](patterns.md#watermarking)\n- **Fix errors?** → [gotchas.md](gotchas.md#common-errors)\n\n## Reading Order\n\n**For building image upload/transform feature:**\n1. [configuration.md](configuration.md) - Setup Workers binding\n2. [api.md](api.md#workers-binding-api-2026-primary-method) - Learn transform API\n3. [patterns.md](patterns.md#upload-from-client-direct-creator-upload) - Direct upload pattern\n4. [gotchas.md](gotchas.md) - Check limits and errors\n\n**For URL-based transforms:**\n1. [configuration.md](configuration.md#variants-configuration) - Create variants\n2. [api.md](api.md#url-transform-api) - URL syntax\n3. [patterns.md](patterns.md#responsive-images) - Responsive patterns\n\n**For troubleshooting:**\n1. [gotchas.md](gotchas.md#common-errors) - Error messages\n2. [gotchas.md](gotchas.md#limits) - Size/format limits\n\n## Core Methods\n\n| Method | Use Case | Location |\n|--------|----------|----------|\n| `env.IMAGES.input().transform()` | Transform in Worker | [api.md:11](api.md) |\n| REST API `/images/v1` | Upload images | [api.md:57](api.md) |\n| Direct Creator Upload | Client-side upload | [api.md:127](api.md) |\n| URL transforms | Static image delivery | [api.md:112](api.md) |\n\n## In This Reference\n\n- **[api.md](api.md)** - Complete API: Workers binding, REST endpoints, URL transforms\n- **[configuration.md](configuration.md)** - Setup: wrangler.toml, variants, auth, signed URLs\n- **[patterns.md](patterns.md)** - Patterns: responsive images, watermarks, format negotiation, caching\n- **[gotchas.md](gotchas.md)** - Troubleshooting: limits, errors, best practices\n\n## Key Features\n\n- **Automatic Optimization** - AVIF/WebP format negotiation\n- **On-the-fly Transforms** - Resize, crop, blur, sharpen via URL or API\n- **Workers Binding** - Transform images in Workers (2026 primary method)\n- **Direct Upload** - Secure client-side uploads without backend proxy\n- **Global Delivery** - Cached at 300+ Cloudflare data centers\n- **Watermarking** - Overlay images programmatically\n\n## See Also\n\n- [Official Docs](https://developers.cloudflare.com/images/)\n- [Workers Examples](https://developers.cloudflare.com/images/tutorials/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/images/api.md",
    "content": "# API Reference\n\n## Workers Binding API\n\n```toml\n# wrangler.toml\n[images]\nbinding = \"IMAGES\"\n```\n\n### Transform Images\n\n```typescript\nconst imageResponse = await env.IMAGES\n  .input(fileBuffer)\n  .transform({ width: 800, height: 600, fit: \"cover\", quality: 85, format: \"avif\" })\n  .output();\nreturn imageResponse.response();\n```\n\n### Transform Options\n\n```typescript\ninterface TransformOptions {\n  width?: number;        height?: number;\n  fit?: \"scale-down\" | \"contain\" | \"cover\" | \"crop\" | \"pad\";\n  quality?: number;      // 1-100\n  format?: \"avif\" | \"webp\" | \"jpeg\" | \"png\";\n  dpr?: number;          // 1-3\n  gravity?: \"auto\" | \"left\" | \"right\" | \"top\" | \"bottom\" | \"face\" | string;\n  sharpen?: number;      // 0-10\n  blur?: number;         // 1-250\n  rotate?: 90 | 180 | 270;\n  background?: string;   // CSS color for pad\n  metadata?: \"none\" | \"copyright\" | \"keep\";\n  brightness?: number;   contrast?: number;   gamma?: number;  // 0-2\n}\n```\n\n### Draw/Watermark\n\n```typescript\nawait env.IMAGES.input(baseImage)\n  .draw(env.IMAGES.input(watermark).transform({ width: 100 }), { top: 10, left: 10, opacity: 0.8 })\n  .output();\n```\n\n## REST API\n\n### Upload Image\n\n```bash\ncurl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 \\\n  -H \"Authorization: Bearer {token}\" -F file=@image.jpg -F metadata='{\"key\":\"value\"}'\n```\n\n### Other Operations\n\n```bash\nGET  /accounts/{account_id}/images/v1/{image_id}      # Get details\nDELETE /accounts/{account_id}/images/v1/{image_id}   # Delete\nGET  /accounts/{account_id}/images/v1?page=1         # List\n```\n\n## URL Transform API\n\n```\nhttps://imagedelivery.net/{hash}/{id}/width=800,height=600,fit=cover,format=avif\n```\n\n**Params:** `w=`, `h=`, `fit=`, `q=`, `f=`, `dpr=`, `gravity=`, `sharpen=`, `blur=`, `rotate=`, `background=`, `metadata=`\n\n## Direct Creator Upload\n\n```typescript\n// 1. Get upload URL (backend)\nconst { result } = await fetch(\n  `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v2/direct_upload`,\n  { method: 'POST', headers: { 'Authorization': `Bearer ${token}` },\n    body: JSON.stringify({ requireSignedURLs: false }) }\n).then(r => r.json());\n\n// 2. Client uploads to result.uploadURL\nconst formData = new FormData();\nformData.append('file', file);\nawait fetch(result.uploadURL, { method: 'POST', body: formData });\n```\n\n## Error Codes\n\n| Code | Message | Solution |\n|------|---------|----------|\n| 5400 | Invalid format | Use JPEG, PNG, GIF, WebP |\n| 5401 | Too large | Max 100MB |\n| 5403 | Invalid transform | Check params |\n| 9413 | Rate limit | Implement backoff |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/images/configuration.md",
    "content": "# Configuration\n\n## Wrangler Integration\n\n### Workers Binding Setup\n\nAdd to `wrangler.toml`:\n\n```toml\nname = \"my-image-worker\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2024-01-01\"\n\n[images]\nbinding = \"IMAGES\"\n```\n\nAccess in Worker:\n\n```typescript\ninterface Env {\n  IMAGES: ImageBinding;\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    return await env.IMAGES\n      .input(imageBuffer)\n      .transform({ width: 800 })\n      .output()\n      .response();\n  }\n};\n```\n\n### Upload via Script\n\nWrangler doesn't have built-in Images commands, use REST API:\n\n```typescript\n// scripts/upload-image.ts\nimport fs from 'fs';\nimport FormData from 'form-data';\n\nasync function uploadImage(filePath: string) {\n  const accountId = process.env.CLOUDFLARE_ACCOUNT_ID!;\n  const apiToken = process.env.CLOUDFLARE_API_TOKEN!;\n  \n  const formData = new FormData();\n  formData.append('file', fs.createReadStream(filePath));\n  \n  const response = await fetch(\n    `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1`,\n    {\n      method: 'POST',\n      headers: {\n        'Authorization': `Bearer ${apiToken}`,\n      },\n      body: formData,\n    }\n  );\n  \n  const result = await response.json();\n  console.log('Uploaded:', result);\n}\n\nuploadImage('./photo.jpg');\n```\n\n### Environment Variables\n\nStore account hash for URL construction:\n\n```toml\n[vars]\nIMAGES_ACCOUNT_HASH = \"your-account-hash\"\nACCOUNT_ID = \"your-account-id\"\n```\n\nAccess in Worker:\n\n```typescript\nconst imageUrl = `https://imagedelivery.net/${env.IMAGES_ACCOUNT_HASH}/${imageId}/public`;\n```\n\n## Variants Configuration\n\nVariants are named presets for transformations.\n\n### Create Variant (Dashboard)\n\n1. Navigate to Images → Variants\n2. Click \"Create Variant\"\n3. Set name (e.g., `thumbnail`)\n4. Configure: `width=200,height=200,fit=cover`\n\n### Create Variant (API)\n\n```bash\ncurl -X POST \\\n  https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1/variants \\\n  -H \"Authorization: Bearer {api_token}\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"id\": \"thumbnail\",\n    \"options\": {\n      \"width\": 200,\n      \"height\": 200,\n      \"fit\": \"cover\"\n    },\n    \"neverRequireSignedURLs\": true\n  }'\n```\n\n### Use Variant\n\n```\nhttps://imagedelivery.net/{account_hash}/{image_id}/thumbnail\n```\n\n### Common Variant Presets\n\n```json\n{\n  \"thumbnail\": {\n    \"width\": 200,\n    \"height\": 200,\n    \"fit\": \"cover\"\n  },\n  \"avatar\": {\n    \"width\": 128,\n    \"height\": 128,\n    \"fit\": \"cover\",\n    \"gravity\": \"face\"\n  },\n  \"hero\": {\n    \"width\": 1920,\n    \"height\": 1080,\n    \"fit\": \"cover\",\n    \"quality\": 90\n  },\n  \"mobile\": {\n    \"width\": 640,\n    \"fit\": \"scale-down\",\n    \"quality\": 80,\n    \"format\": \"avif\"\n  }\n}\n```\n\n## Authentication\n\n### API Token (Recommended)\n\nGenerate at: Dashboard → My Profile → API Tokens\n\nRequired permissions:\n- Account → Cloudflare Images → Edit\n\n```bash\ncurl -H \"Authorization: Bearer {api_token}\" \\\n  https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1\n```\n\n### API Key (Legacy)\n\n```bash\ncurl -H \"X-Auth-Email: {email}\" \\\n     -H \"X-Auth-Key: {api_key}\" \\\n  https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1\n```\n\n## Signed URLs\n\nFor private images, enable signed URLs:\n\n```bash\n# Upload with signed URLs required\ncurl -X POST \\\n  https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 \\\n  -H \"Authorization: Bearer {api_token}\" \\\n  -F file=@private.jpg \\\n  -F requireSignedURLs=true\n```\n\nGenerate signed URL:\n\n```typescript\nimport { createHmac } from 'crypto';\n\nfunction signUrl(imageId: string, variant: string, expiry: number, key: string): string {\n  const path = `/${imageId}/${variant}`;\n  const toSign = `${path}${expiry}`;\n  const signature = createHmac('sha256', key)\n    .update(toSign)\n    .digest('hex');\n  \n  return `https://imagedelivery.net/{hash}${path}?exp=${expiry}&sig=${signature}`;\n}\n\n// Sign URL valid for 1 hour\nconst signedUrl = signUrl('image-id', 'public', Date.now() + 3600, env.SIGNING_KEY);\n```\n\n## Local Development\n\n```bash\nnpx wrangler dev --remote\n```\n\nMust use `--remote` for Images binding access.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/images/gotchas.md",
    "content": "# Gotchas & Best Practices\n\n## Fit Modes\n\n| Mode | Best For | Behavior |\n|------|----------|----------|\n| `cover` | Hero images, thumbnails | Fills space, crops excess |\n| `contain` | Product images, artwork | Preserves full image, may add padding |\n| `scale-down` | User uploads | Never enlarges |\n| `crop` | Precise crops | Uses gravity |\n| `pad` | Fixed aspect ratio | Adds background |\n\n## Format Selection\n\n```typescript\nformat: 'auto' // Recommended - negotiates best format\n```\n\n**Support:** AVIF (Chrome 85+, Firefox 93+, Safari 16.4+), WebP (Chrome 23+, Firefox 65+, Safari 14+)\n\n## Quality Settings\n\n| Use Case | Quality |\n|----------|---------|\n| Thumbnails | 75-80 |\n| Standard | 85 (default) |\n| High-quality | 90-95 |\n\n## Common Errors\n\n### 5403: \"Image transformation failed\"\n- Verify `width`/`height` ≤ 12000\n- Check `quality` 1-100, `dpr` 1-3\n- Don't combine incompatible options\n\n### 9413: \"Rate limit exceeded\"\nImplement caching and exponential backoff:\n```typescript\nfor (let i = 0; i < 3; i++) {\n  try { return await env.IMAGES.input(buffer).transform({...}).output(); }\n  catch { await new Promise(r => setTimeout(r, 2 ** i * 1000)); }\n}\n```\n\n### 5401: \"Image too large\"\nPre-process images before upload (max 100MB, 12000×12000px)\n\n### 5400: \"Invalid image format\"\nSupported: JPEG, PNG, GIF, WebP, AVIF, SVG\n\n### 401/403: \"Unauthorized\"\nVerify API token has `Cloudflare Images → Edit` permission\n\n## Limits\n\n| Resource | Limit |\n|----------|-------|\n| Max input size | 100MB |\n| Max dimensions | 12000×12000px |\n| Quality range | 1-100 |\n| DPR range | 1-3 |\n| API rate limit | ~1200 req/min |\n\n## AVIF Gotchas\n\n- **Slower encoding**: First request may have higher latency\n- **Browser detection**:\n```typescript\nconst format = /image\\/avif/.test(request.headers.get('Accept') || '') ? 'avif' : 'webp';\n```\n\n## Anti-Patterns\n\n```typescript\n// ❌ No caching - transforms every request\nreturn env.IMAGES.input(buffer).transform({...}).output().response();\n\n// ❌ cover without both dimensions\ntransform({ width: 800, fit: 'cover' })\n\n// ✅ Always set both for cover\ntransform({ width: 800, height: 600, fit: 'cover' })\n\n// ❌ Exposes API token to client\n// ✅ Use Direct Creator Upload (patterns.md)\n```\n\n## Debugging\n\n```typescript\n// Check response headers\nconsole.log('Content-Type:', response.headers.get('Content-Type'));\n\n// Test with curl\n// curl -I \"https://imagedelivery.net/{hash}/{id}/width=800,format=avif\"\n\n// Monitor logs\n// npx wrangler tail\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/images/patterns.md",
    "content": "# Common Patterns\n\n## URL Transform Options\n\n```\nwidth=<PX>   height=<PX>   fit=scale-down|contain|cover|crop|pad\nquality=85   format=auto|webp|avif|jpeg|png   dpr=2\ngravity=auto|face|left|right|top|bottom   sharpen=2   blur=10\nrotate=90|180|270   background=white   metadata=none|copyright|keep\n```\n\n## Responsive Images (srcset)\n\n```html\n<img src=\"https://imagedelivery.net/{hash}/{id}/width=800\"\n  srcset=\".../{id}/width=400 400w, .../{id}/width=800 800w, .../{id}/width=1200 1200w\"\n  sizes=\"(max-width: 600px) 400px, 800px\" />\n```\n\n## Format Negotiation\n\n```typescript\nasync fetch(request: Request, env: Env): Promise<Response> {\n  const accept = request.headers.get('Accept') || '';\n  const format = /image\\/avif/.test(accept) ? 'avif' : /image\\/webp/.test(accept) ? 'webp' : 'jpeg';\n  return env.IMAGES.input(buffer).transform({ format, quality: 85 }).output().response();\n}\n```\n\n## Direct Creator Upload\n\n```typescript\n// Backend: Generate upload URL\nconst response = await fetch(\n  `https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/images/v2/direct_upload`,\n  { method: 'POST', headers: { 'Authorization': `Bearer ${env.API_TOKEN}` },\n    body: JSON.stringify({ requireSignedURLs: false, metadata: { userId } }) }\n);\n\n// Frontend: Upload to returned uploadURL\nconst formData = new FormData();\nformData.append('file', file);\nawait fetch(result.uploadURL, { method: 'POST', body: formData });\n// Use: https://imagedelivery.net/{hash}/${result.id}/public\n```\n\n## Transform & Store to R2\n\n```typescript\nasync fetch(request: Request, env: Env): Promise<Response> {\n  const file = (await request.formData()).get('image') as File;\n  const transformed = await env.IMAGES\n    .input(await file.arrayBuffer())\n    .transform({ width: 800, format: 'avif', quality: 80 })\n    .output();\n  await env.R2.put(`images/${Date.now()}.avif`, transformed.response().body);\n  return Response.json({ success: true });\n}\n```\n\n## Watermarking\n\n```typescript\nconst watermark = await env.ASSETS.fetch(new URL('/watermark.png', request.url));\nconst result = await env.IMAGES\n  .input(await image.arrayBuffer())\n  .draw(env.IMAGES.input(watermark.body).transform({ width: 100 }), { bottom: 20, right: 20, opacity: 0.7 })\n  .transform({ format: 'avif' })\n  .output();\nreturn result.response();\n```\n\n## Device-Based Transforms\n\n```typescript\nconst ua = request.headers.get('User-Agent') || '';\nconst isMobile = /Mobile|Android|iPhone/i.test(ua);\nreturn env.IMAGES.input(buffer)\n  .transform({ width: isMobile ? 400 : 1200, quality: isMobile ? 75 : 85, format: 'avif' })\n  .output().response();\n```\n\n## Caching Strategy\n\n```typescript\nasync fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n  const cache = caches.default;\n  let response = await cache.match(request);\n  if (!response) {\n    response = await env.IMAGES.input(buffer).transform({ width: 800, format: 'avif' }).output().response();\n    response = new Response(response.body, { headers: { ...response.headers, 'Cache-Control': 'public, max-age=86400' } });\n    ctx.waitUntil(cache.put(request, response.clone()));\n  }\n  return response;\n}\n```\n\n## Batch Processing\n\n```typescript\nconst results = await Promise.all(images.map(buffer =>\n  env.IMAGES.input(buffer).transform({ width: 800, fit: 'cover', format: 'avif' }).output()\n));\n```\n\n## Error Handling\n\n```typescript\ntry {\n  return (await env.IMAGES.input(buffer).transform({ width: 800 }).output()).response();\n} catch (error) {\n  console.error('Transform failed:', error);\n  return new Response('Image processing failed', { status: 500 });\n}\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/kv/README.md",
    "content": "# Cloudflare Workers KV\n\nGlobally-distributed, eventually-consistent key-value store optimized for high read volume and low latency.\n\n## Overview\n\nKV provides:\n- Eventual consistency (60s global propagation)\n- Read-optimized performance\n- 25 MiB value limit per key\n- Auto-replication to Cloudflare edge\n- Metadata support (1024 bytes)\n\n**Use cases:** Config storage, user sessions, feature flags, caching, A/B testing\n\n## When to Use KV\n\n| Need | Recommendation |\n|------|----------------|\n| Strong consistency | → [Durable Objects](../durable-objects/) |\n| SQL queries | → [D1](../d1/) |\n| Object storage (files) | → [R2](../r2/) |\n| High read, low write volume | → KV ✅ |\n| Sub-10ms global reads | → KV ✅ |\n\n**Quick comparison:**\n\n| Feature | KV | D1 | Durable Objects |\n|---------|----|----|-----------------|\n| Consistency | Eventual | Strong | Strong |\n| Read latency | <10ms | ~50ms | <1ms |\n| Write limit | 1/s per key | Unlimited | Unlimited |\n| Use case | Config, cache | Relational data | Coordination |\n\n## Quick Start\n\n```bash\nwrangler kv namespace create MY_NAMESPACE\n# Add binding to wrangler.jsonc\n```\n\n```typescript\n// Write\nawait env.MY_KV.put(\"key\", \"value\", { expirationTtl: 300 });\n\n// Read\nconst value = await env.MY_KV.get(\"key\");\nconst json = await env.MY_KV.get<Config>(\"config\", \"json\");\n```\n\n## Core Operations\n\n| Method | Purpose | Returns |\n|--------|---------|---------|\n| `get(key, type?)` | Single read | `string \\| null` |\n| `get(keys, type?)` | Bulk read (≤100) | `Map<string, T \\| null>` |\n| `put(key, value, options?)` | Write | `Promise<void>` |\n| `delete(key)` | Delete | `Promise<void>` |\n| `list(options?)` | List keys | `{ keys, list_complete, cursor? }` |\n| `getWithMetadata(key)` | Get + metadata | `{ value, metadata }` |\n\n## Consistency Model\n\n- **Write visibility:** Immediate in same location, ≤60s globally\n- **Read path:** Eventually consistent\n- **Write rate:** 1 write/second per key (429 on exceed)\n\n## Reading Order\n\n| Task | Files to Read |\n|------|---------------|\n| Quick start | README → configuration.md |\n| Implement feature | README → api.md → patterns.md |\n| Debug issues | gotchas.md → api.md |\n| Batch operations | api.md (bulk section) → patterns.md |\n| Performance tuning | gotchas.md (performance) → patterns.md (caching) |\n\n## In This Reference\n\n- [configuration.md](./configuration.md) - wrangler.jsonc setup, namespace creation, TypeScript types\n- [api.md](./api.md) - KV methods, bulk operations, cacheTtl, content types\n- [patterns.md](./patterns.md) - Caching, sessions, rate limiting, A/B testing\n- [gotchas.md](./gotchas.md) - Eventual consistency, concurrent writes, value limits\n\n## See Also\n\n- [workers](../workers/) - Worker runtime for KV access\n- [d1](../d1/) - Use D1 for strong consistency needs\n- [durable-objects](../durable-objects/) - Strongly consistent alternative\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/kv/api.md",
    "content": "# KV API Reference\n\n## Read Operations\n\n```typescript\n// Single key (string)\nconst value = await env.MY_KV.get(\"user:123\");\n\n// JSON type (auto-parsed)\nconst config = await env.MY_KV.get<AppConfig>(\"config\", \"json\");\n\n// ArrayBuffer for binary\nconst buffer = await env.MY_KV.get(\"image\", \"arrayBuffer\");\n\n// Stream for large values\nconst stream = await env.MY_KV.get(\"large-file\", \"stream\");\n\n// With cache TTL (min 60s)\nconst value = await env.MY_KV.get(\"key\", { type: \"text\", cacheTtl: 300 });\n\n// Bulk get (max 100 keys, counts as 1 operation)\nconst keys = [\"user:1\", \"user:2\", \"user:3\", \"missing:key\"];\nconst results = await env.MY_KV.get(keys);\n// Returns Map<string, string | null>\n\nconsole.log(results.get(\"user:1\"));     // \"John\" (if exists)\nconsole.log(results.get(\"missing:key\")); // null\n\n// Process results with null handling\nfor (const [key, value] of results) {\n  if (value !== null) {\n    // Handle found keys\n    console.log(`${key}: ${value}`);\n  }\n}\n\n// TypeScript with generics (type-safe JSON parsing)\ninterface UserProfile { name: string; email: string; }\nconst profile = await env.USERS.get<UserProfile>(\"user:123\", \"json\");\n// profile is typed as UserProfile | null\nif (profile) {\n  console.log(profile.name); // Type-safe access\n}\n\n// Bulk get with type\nconst configs = await env.MY_KV.get<Config>([\"config:app\", \"config:feature\"], \"json\");\n// Map<string, Config | null>\n```\n\n## Write Operations\n\n```typescript\n// Basic put\nawait env.MY_KV.put(\"key\", \"value\");\nawait env.MY_KV.put(\"config\", JSON.stringify({ theme: \"dark\" }));\n\n// With expiration (UNIX timestamp)\nawait env.MY_KV.put(\"session\", token, {\n  expiration: Math.floor(Date.now() / 1000) + 3600\n});\n\n// With TTL (seconds from now, min 60)\nawait env.MY_KV.put(\"cache\", data, { expirationTtl: 300 });\n\n// With metadata (max 1024 bytes)\nawait env.MY_KV.put(\"user:profile\", userData, {\n  metadata: { version: 2, lastUpdated: Date.now() }\n});\n\n// Combined\nawait env.MY_KV.put(\"temp\", value, {\n  expirationTtl: 3600,\n  metadata: { temporary: true }\n});\n```\n\n## Get with Metadata\n\n```typescript\n// Single key\nconst result = await env.MY_KV.getWithMetadata(\"user:profile\");\n// { value: string | null, metadata: any | null }\n\nif (result.value && result.metadata) {\n  const { version, lastUpdated } = result.metadata;\n}\n\n// Multiple keys (bulk)\nconst keys = [\"key1\", \"key2\", \"key3\"];\nconst results = await env.MY_KV.getWithMetadata(keys);\n// Returns Map<string, { value, metadata, cacheStatus? }>\n\nfor (const [key, result] of results) {\n  if (result.value) {\n    console.log(`${key}: ${result.value}`);\n    console.log(`Metadata: ${JSON.stringify(result.metadata)}`);\n    // cacheStatus field indicates cache hit/miss (when available)\n  }\n}\n\n// With type\nconst result = await env.MY_KV.getWithMetadata<UserData>(\"user:123\", \"json\");\n// result: { value: UserData | null, metadata: any | null, cacheStatus?: string }\n```\n\n## Delete Operations\n\n```typescript\nawait env.MY_KV.delete(\"key\"); // Always succeeds (even if key missing)\n```\n\n## List Operations\n\n```typescript\n// List all\nconst keys = await env.MY_KV.list();\n// { keys: [...], list_complete: boolean, cursor?: string }\n\n// With prefix\nconst userKeys = await env.MY_KV.list({ prefix: \"user:\" });\n\n// Pagination\nlet cursor: string | undefined;\nlet allKeys = [];\ndo {\n  const result = await env.MY_KV.list({ cursor, limit: 1000 });\n  allKeys.push(...result.keys);\n  cursor = result.cursor;\n} while (!result.list_complete);\n```\n\n## Performance Considerations\n\n### Type Selection\n\n| Type | Use Case | Performance |\n|------|----------|-------------|\n| `stream` | Large values (>1MB) | Fastest - no buffering |\n| `arrayBuffer` | Binary data | Fast - single allocation |\n| `text` | String values | Medium |\n| `json` | Objects (parse overhead) | Slowest - parsing cost |\n\n### Parallel Reads\n\n```typescript\n// Efficient parallel reads with Promise.all()\nconst [user, settings, cache] = await Promise.all([\n  env.USERS.get(\"user:123\", \"json\"),\n  env.SETTINGS.get(\"config:app\", \"json\"),\n  env.CACHE.get(\"data:latest\")\n]);\n```\n\n## Error Handling\n\n- **Missing keys:** Return `null` (not an error)\n- **Rate limit (429):** Retry with exponential backoff (see gotchas.md)\n- **Response too large (413):** Values >25MB fail with 413 error\n\nSee [gotchas.md](./gotchas.md) for detailed error patterns and solutions.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/kv/configuration.md",
    "content": "# KV Configuration\n\n## Create Namespace\n\n```bash\nwrangler kv namespace create MY_NAMESPACE\n# Output: { binding = \"MY_NAMESPACE\", id = \"abc123...\" }\n\nwrangler kv namespace create MY_NAMESPACE --preview  # For local dev\n```\n\n## Workers Binding\n\n**wrangler.jsonc:**\n```jsonc\n{\n  \"kv_namespaces\": [\n    {\n      \"binding\": \"MY_KV\",\n      \"id\": \"abc123xyz789\"\n    },\n    // Optional: Different namespace for preview/development\n    {\n      \"binding\": \"MY_KV\",\n      \"preview_id\": \"preview-abc123\"\n    }\n  ]\n}\n```\n\n## TypeScript Types\n\n**env.d.ts:**\n```typescript\ninterface Env {\n  MY_KV: KVNamespace;\n  SESSIONS: KVNamespace;\n  CACHE: KVNamespace;\n}\n```\n\n**worker.ts:**\n```typescript\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n    // env.MY_KV is now typed as KVNamespace\n    const value = await env.MY_KV.get(\"key\");\n    return new Response(value || \"Not found\");\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n**Type-safe JSON operations:**\n```typescript\ninterface UserProfile {\n  name: string;\n  email: string;\n  role: \"admin\" | \"user\";\n}\n\nconst profile = await env.USERS.get<UserProfile>(\"user:123\", \"json\");\n// profile: UserProfile | null (type-safe!)\nif (profile) {\n  console.log(profile.name); // TypeScript knows this is a string\n}\n```\n\n## CLI Operations\n\n```bash\n# Put\nwrangler kv key put --binding=MY_KV \"key\" \"value\"\nwrangler kv key put --binding=MY_KV \"key\" --path=./file.json --ttl=3600\n\n# Get\nwrangler kv key get --binding=MY_KV \"key\"\n\n# Delete\nwrangler kv key delete --binding=MY_KV \"key\"\n\n# List\nwrangler kv key list --binding=MY_KV --prefix=\"user:\"\n\n# Bulk operations (max 10,000 keys per file)\nwrangler kv bulk put data.json --binding=MY_KV\nwrangler kv bulk get keys.json --binding=MY_KV\nwrangler kv bulk delete keys.json --binding=MY_KV --force\n```\n\n## Local Development\n\n```bash\nwrangler dev                # Local KV (isolated)\nwrangler dev --remote       # Remote KV (production)\n\n# Or in wrangler.jsonc:\n# \"kv_namespaces\": [{ \"binding\": \"MY_KV\", \"id\": \"...\", \"remote\": true }]\n```\n\n## REST API\n\n### Single Operations\n\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst client = new Cloudflare({\n  apiEmail: process.env.CLOUDFLARE_EMAIL,\n  apiKey: process.env.CLOUDFLARE_API_KEY\n});\n\n// Single key operations\nawait client.kv.namespaces.values.update(namespaceId, 'key', {\n  account_id: accountId,\n  value: 'value',\n  expiration_ttl: 3600\n});\n```\n\n### Bulk Operations\n\n```typescript\n// Bulk update (up to 10,000 keys, max 100MB total)\nawait client.kv.namespaces.bulkUpdate(namespaceId, {\n  account_id: accountId,\n  body: [\n    { key: \"key1\", value: \"value1\", expiration_ttl: 3600 },\n    { key: \"key2\", value: \"value2\", metadata: { version: 1 } },\n    { key: \"key3\", value: \"value3\" }\n  ]\n});\n\n// Bulk get (up to 100 keys)\nconst results = await client.kv.namespaces.bulkGet(namespaceId, {\n  account_id: accountId,\n  keys: [\"key1\", \"key2\", \"key3\"]\n});\n\n// Bulk delete (up to 10,000 keys)\nawait client.kv.namespaces.bulkDelete(namespaceId, {\n  account_id: accountId,\n  keys: [\"key1\", \"key2\", \"key3\"]\n});\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/kv/gotchas.md",
    "content": "# KV Gotchas & Troubleshooting\n\n## Common Errors\n\n### \"Stale Read After Write\"\n\n**Cause:** Eventual consistency means writes may not be immediately visible in other regions  \n**Solution:** Don't read immediately after write; return confirmation without reading or use the local value you just wrote. Writes visible immediately in same location, ≤60s globally\n\n```typescript\n// ❌ BAD: Read immediately after write\nawait env.KV.put(\"key\", \"value\");\nconst value = await env.KV.get(\"key\"); // May be null in other regions!\n\n// ✅ GOOD: Use the value you just wrote\nconst newValue = \"value\";\nawait env.KV.put(\"key\", newValue);\nreturn new Response(newValue); // Don't re-read\n```\n\n### \"429 Rate Limit on Concurrent Writes\"\n\n**Cause:** Multiple concurrent writes to same key exceeding 1 write/second limit  \n**Solution:** Use sequential writes, unique keys for concurrent operations, or implement retry with exponential backoff\n\n```typescript\nasync function putWithRetry(\n  kv: KVNamespace,\n  key: string,\n  value: string,\n  maxAttempts = 5\n): Promise<void> {\n  let delay = 1000;\n  for (let i = 0; i < maxAttempts; i++) {\n    try {\n      await kv.put(key, value);\n      return;\n    } catch (err) {\n      if (err instanceof Error && err.message.includes(\"429\")) {\n        if (i === maxAttempts - 1) throw err;\n        await new Promise(r => setTimeout(r, delay));\n        delay *= 2; // Exponential backoff\n      } else {\n        throw err;\n      }\n    }\n  }\n}\n```\n\n### \"Inefficient Multiple Gets\"\n\n**Cause:** Making multiple individual get() calls instead of bulk operation  \n**Solution:** Use bulk get with array of keys: `env.USERS.get([\"user:1\", \"user:2\", \"user:3\"])` to reduce to 1 operation\n\n### \"Null Reference Error\"\n\n**Cause:** Attempting to use value without checking for null when key doesn't exist  \n**Solution:** Always handle null returns - KV returns `null` for missing keys, not undefined\n\n```typescript\n// ❌ BAD: Assumes value exists\nconst config = await env.KV.get(\"config\", \"json\");\nreturn config.theme; // TypeError if null!\n\n// ✅ GOOD: Null checks\nconst config = await env.KV.get(\"config\", \"json\");\nreturn config?.theme ?? \"default\";\n\n// ✅ GOOD: Early return\nconst config = await env.KV.get(\"config\", \"json\");\nif (!config) return new Response(\"Not found\", { status: 404 });\nreturn new Response(config.theme);\n```\n\n### \"Negative Lookup Caching\"\n\n**Cause:** Keys that don't exist are cached as \"not found\" for up to 60s  \n**Solution:** Creating a key after checking won't be visible until cache expires\n\n```typescript\n// Check → create pattern has race condition\nconst exists = await env.KV.get(\"key\"); // null, cached as \"not found\"\nif (!exists) {\n  await env.KV.put(\"key\", \"value\");\n  // Next get() may still return null for ~60s due to negative cache\n}\n\n// Alternative: Always assume key may not exist, use defaults\nconst value = await env.KV.get(\"key\") ?? \"default-value\";\n```\n\n## Performance Tips\n\n| Scenario | Recommendation | Why |\n|----------|----------------|-----|\n| Large values (>1MB) | Use `stream` type | Avoids buffering entire value in memory |\n| Many small keys | Coalesce into one JSON object | Reduces operations, improves cache hit rate |\n| High write volume | Spread across different keys | Avoid 1 write/second per-key limit |\n| Cold reads | Increase `cacheTtl` parameter | Reduces latency for frequently-read data |\n| Bulk operations | Use array form of get() | Single operation, better performance |\n\n## Cost Examples\n\n**Free tier:**\n- 100K reads/day = 3M/month ✅\n- 1K writes/day = 30K/month ✅\n- 1GB storage ✅\n\n**Example paid workload:**\n- 10M reads/month = $5.00\n- 100K writes/month = $0.50\n- 1GB storage = $0.50\n- **Total: ~$6/month**\n\n## Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Key size | 512 bytes | Maximum key length |\n| Value size | 25 MiB | Maximum value; 413 error if exceeded |\n| Metadata size | 1024 bytes | Maximum metadata per key |\n| cacheTtl minimum | 60s | Minimum cache TTL |\n| Write rate per key | 1 write/second | All plans; 429 error if exceeded |\n| Propagation time | ≤60s | Global propagation time |\n| Bulk get max | 100 keys | Maximum keys per bulk operation |\n| Operations per Worker | 1,000 | Per request (bulk counts as 1) |\n| Reads pricing | $0.50 per 10M | Per million reads |\n| Writes pricing | $5.00 per 1M | Per million writes |\n| Deletes pricing | $5.00 per 1M | Per million deletes |\n| Storage pricing | $0.50 per GB-month | Per GB per month |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/kv/patterns.md",
    "content": "# KV Patterns & Best Practices\n\n## Multi-Tier Caching\n\n```typescript\n// Memory → KV → Origin (3-tier cache)\nconst memoryCache = new Map<string, { data: any; expires: number }>();\n\nasync function getCached(env: Env, key: string): Promise<any> {\n  const now = Date.now();\n  \n  // L1: Memory cache (fastest)\n  const cached = memoryCache.get(key);\n  if (cached && cached.expires > now) {\n    return cached.data;\n  }\n  \n  // L2: KV cache (fast)\n  const kvValue = await env.CACHE.get(key, \"json\");\n  if (kvValue) {\n    memoryCache.set(key, { data: kvValue, expires: now + 60000 }); // 1min in memory\n    return kvValue;\n  }\n  \n  // L3: Origin (slow)\n  const origin = await fetch(`https://api.example.com/${key}`).then(r => r.json());\n  \n  // Backfill caches\n  await env.CACHE.put(key, JSON.stringify(origin), { expirationTtl: 300 }); // 5min in KV\n  memoryCache.set(key, { data: origin, expires: now + 60000 });\n  \n  return origin;\n}\n```\n\n## API Response Caching\n\n```typescript\nasync function getCachedData(env: Env, key: string, fetcher: () => Promise<any>): Promise<any> {\n  const cached = await env.MY_KV.get(key, \"json\");\n  if (cached) return cached;\n  \n  const data = await fetcher();\n  await env.MY_KV.put(key, JSON.stringify(data), { expirationTtl: 300 });\n  return data;\n}\n\nconst apiData = await getCachedData(\n  env,\n  \"cache:users\",\n  () => fetch(\"https://api.example.com/users\").then(r => r.json())\n);\n```\n\n## Session Management\n\n```typescript\ninterface Session { userId: string; expiresAt: number; }\n\nasync function createSession(env: Env, userId: string): Promise<string> {\n  const sessionId = crypto.randomUUID();\n  const expiresAt = Date.now() + (24 * 60 * 60 * 1000);\n  \n  await env.SESSIONS.put(\n    `session:${sessionId}`,\n    JSON.stringify({ userId, expiresAt }),\n    { expirationTtl: 86400, metadata: { createdAt: Date.now() } }\n  );\n  \n  return sessionId;\n}\n\nasync function getSession(env: Env, sessionId: string): Promise<Session | null> {\n  const data = await env.SESSIONS.get<Session>(`session:${sessionId}`, \"json\");\n  if (!data || data.expiresAt < Date.now()) return null;\n  return data;\n}\n```\n\n## Coalesce Cold Keys\n\n```typescript\n// ❌ BAD: Many individual keys\nawait env.KV.put(\"user:123:name\", \"John\");\nawait env.KV.put(\"user:123:email\", \"john@example.com\");\n\n// ✅ GOOD: Single coalesced object\nawait env.USERS.put(\"user:123:profile\", JSON.stringify({\n  name: \"John\",\n  email: \"john@example.com\",\n  role: \"admin\"\n}));\n\n// Benefits: Hot key cache, single read, reduced operations\n// Trade-off: Harder to update individual fields\n```\n\n## Prefix-Based Namespacing\n\n```typescript\n// Logical partitioning within single namespace\nconst PREFIXES = {\n  users: \"user:\",\n  sessions: \"session:\",\n  cache: \"cache:\",\n  features: \"feature:\"\n} as const;\n\n// Write with prefix\nasync function setUser(env: Env, id: string, data: any) {\n  await env.KV.put(`${PREFIXES.users}${id}`, JSON.stringify(data));\n}\n\n// Read with prefix\nasync function getUser(env: Env, id: string) {\n  return await env.KV.get(`${PREFIXES.users}${id}`, \"json\");\n}\n\n// List by prefix\nasync function listUserIds(env: Env): Promise<string[]> {\n  const result = await env.KV.list({ prefix: PREFIXES.users });\n  return result.keys.map(k => k.name.replace(PREFIXES.users, \"\"));\n}\n\n// Example hierarchy\n\"user:123:profile\"\n\"user:123:settings\"\n\"cache:api:users\"\n\"session:abc-def\"\n\"feature:flags:beta\"\n```\n\n## Metadata Versioning\n\n```typescript\ninterface VersionedData {\n  version: number;\n  data: any;\n}\n\nasync function migrateIfNeeded(env: Env, key: string) {\n  const result = await env.DATA.getWithMetadata(key, \"json\");\n  \n  if (!result.value) return null;\n  \n  const currentVersion = result.metadata?.version || 1;\n  const targetVersion = 2;\n  \n  if (currentVersion < targetVersion) {\n    // Migrate data format\n    const migrated = migrate(result.value, currentVersion, targetVersion);\n    \n    // Store with new version\n    await env.DATA.put(key, JSON.stringify(migrated), {\n      metadata: { version: targetVersion, migratedAt: Date.now() }\n    });\n    \n    return migrated;\n  }\n  \n  return result.value;\n}\n\nfunction migrate(data: any, from: number, to: number): any {\n  if (from === 1 && to === 2) {\n    // V1 → V2: Rename field\n    return { ...data, userName: data.name };\n  }\n  return data;\n}\n```\n\n## Error Boundary Pattern\n\n```typescript\n// Resilient get with fallback\nasync function resilientGet<T>(\n  env: Env,\n  key: string,\n  fallback: T\n): Promise<T> {\n  try {\n    const value = await env.KV.get<T>(key, \"json\");\n    return value ?? fallback;\n  } catch (err) {\n    console.error(`KV error for ${key}:`, err);\n    return fallback;\n  }\n}\n\n// Usage\nconst config = await resilientGet(env, \"config:app\", {\n  theme: \"light\",\n  maxItems: 10\n});\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/miniflare/README.md",
    "content": "# Miniflare\n\nLocal simulator for Cloudflare Workers development/testing. Runs Workers in workerd sandbox implementing runtime APIs - no internet required.\n\n## Features\n\n- Full-featured: KV, Durable Objects, R2, D1, WebSockets, Queues\n- Fully-local: test without internet, instant reload\n- TypeScript-native: detailed logging, source maps\n- Advanced testing: dispatch events without HTTP, simulate Worker connections\n\n## When to Use\n\n**Decision tree for testing Workers:**\n\n```\nNeed to test Workers?\n│\n├─ Unit tests for business logic only?\n│  └─ getPlatformProxy (Vitest/Jest) → [patterns.md](./patterns.md#getplatformproxy)\n│     Fast, no HTTP, direct binding access\n│\n├─ Integration tests with full runtime?\n│  ├─ Single Worker?\n│  │  └─ Miniflare API → [Quick Start](#quick-start)\n│  │     Full control, programmatic access\n│  │\n│  ├─ Multiple Workers + service bindings?\n│  │  └─ Miniflare workers array → [configuration.md](./configuration.md#multiple-workers)\n│  │     Shared storage, inter-worker calls\n│  │\n│  └─ Vitest test runner integration?\n│     └─ vitest-pool-workers → [patterns.md](./patterns.md#vitest-pool-workers)\n│        Full Workers env in Vitest\n│\n└─ Local dev server?\n   └─ wrangler dev (not Miniflare)\n      Hot reload, automatic config\n```\n\n**Use Miniflare for:**\n- Integration tests with full Worker runtime\n- Testing bindings/storage locally\n- Multiple Workers with service bindings\n- Programmatic event dispatch (fetch, queue, scheduled)\n\n**Use getPlatformProxy for:**\n- Fast unit tests of business logic\n- Testing without HTTP overhead\n- Vitest/Jest environments\n\n**Use Wrangler for:**\n- Local development workflow\n- Production deployments\n\n## Setup\n\n```bash\nnpm i -D miniflare\n```\n\nRequires ES modules in `package.json`:\n```json\n{\"type\": \"module\"}\n```\n\n## Quick Start\n\n```js\nimport { Miniflare } from \"miniflare\";\n\nconst mf = new Miniflare({\n  modules: true,\n  script: `\n    export default {\n      async fetch(request, env, ctx) {\n        return new Response(\"Hello Miniflare!\");\n      }\n    }\n  `,\n});\n\nconst res = await mf.dispatchFetch(\"http://localhost:8787/\");\nconsole.log(await res.text()); // Hello Miniflare!\nawait mf.dispose();\n```\n\n## Reading Order\n\n**New to Miniflare?** Start here:\n1. [Quick Start](#quick-start) - Running in 2 minutes\n2. [When to Use](#when-to-use) - Choose your testing approach\n3. [patterns.md](./patterns.md) - Testing patterns (getPlatformProxy, Vitest, node:test)\n4. [configuration.md](./configuration.md) - Configure bindings, storage, multiple workers\n\n**Troubleshooting:**\n- [gotchas.md](./gotchas.md) - Common errors and debugging\n\n**API reference:**\n- [api.md](./api.md) - Complete method reference\n\n## See Also\n- [wrangler](../wrangler/) - CLI tool that embeds Miniflare for `wrangler dev`\n- [workerd](../workerd/) - Runtime that powers Miniflare\n- [workers](../workers/) - Workers runtime API documentation\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/miniflare/api.md",
    "content": "# Programmatic API\n\n## Miniflare Class\n\n```typescript\nclass Miniflare {\n  constructor(options: MiniflareOptions);\n  \n  // Lifecycle\n  ready: Promise<URL>; // Resolves when server ready, returns URL\n  dispose(): Promise<void>; // Cleanup resources\n  setOptions(options: MiniflareOptions): Promise<void>; // Reload config\n  \n  // Event dispatching\n  dispatchFetch(url: string | URL | Request, init?: RequestInit): Promise<Response>;\n  getWorker(name?: string): Promise<Worker>;\n  \n  // Bindings access\n  getBindings<Bindings = Record<string, unknown>>(name?: string): Promise<Bindings>;\n  getCf(name?: string): Promise<IncomingRequestCfProperties | undefined>;\n  getKVNamespace(name: string): Promise<KVNamespace>;\n  getR2Bucket(name: string): Promise<R2Bucket>;\n  getDurableObjectNamespace(name: string): Promise<DurableObjectNamespace>;\n  getDurableObjectStorage(id: DurableObjectId): Promise<DurableObjectStorage>;\n  getD1Database(name: string): Promise<D1Database>;\n  getCaches(): Promise<CacheStorage>;\n  getQueueProducer(name: string): Promise<QueueProducer>;\n  \n  // Debugging\n  getInspectorURL(): Promise<URL>; // Chrome DevTools inspector URL\n}\n```\n\n## Event Dispatching\n\n**Fetch (no HTTP server):**\n```js\nconst res = await mf.dispatchFetch(\"http://localhost:8787/path\", {\n  method: \"POST\",\n  headers: { \"Authorization\": \"Bearer token\" },\n  body: JSON.stringify({ data: \"value\" }),\n});\n```\n\n**Custom Host routing:**\n```js\nconst res = await mf.dispatchFetch(\"http://localhost:8787/\", {\n  headers: { \"Host\": \"api.example.com\" },\n});\n```\n\n**Scheduled:**\n```js\nconst worker = await mf.getWorker();\nconst result = await worker.scheduled({ cron: \"30 * * * *\" });\n// result: { outcome: \"ok\", noRetry: false }\n```\n\n**Queue:**\n```js\nconst worker = await mf.getWorker();\nconst result = await worker.queue(\"queue-name\", [\n  { id: \"msg1\", timestamp: new Date(), body: \"data\", attempts: 1 },\n]);\n// result: { outcome: \"ok\", retryAll: false, ackAll: false, ... }\n```\n\n## Bindings Access\n\n**Environment variables:**\n```js\n// Basic usage\nconst bindings = await mf.getBindings();\nconsole.log(bindings.SECRET_KEY);\n\n// With type safety (recommended):\ninterface Env {\n  SECRET_KEY: string;\n  API_URL: string;\n  KV: KVNamespace;\n}\nconst env = await mf.getBindings<Env>();\nenv.SECRET_KEY; // string (typed!)\nenv.KV.get(\"key\"); // KVNamespace methods available\n```\n\n**Request.cf object:**\n```js\nconst cf = await mf.getCf();\nconsole.log(cf?.colo); // \"DFW\"\nconsole.log(cf?.country); // \"US\"\n```\n\n**KV:**\n```js\nconst ns = await mf.getKVNamespace(\"TEST_NAMESPACE\");\nawait ns.put(\"key\", \"value\");\nconst value = await ns.get(\"key\");\n```\n\n**R2:**\n```js\nconst bucket = await mf.getR2Bucket(\"BUCKET\");\nawait bucket.put(\"file.txt\", \"content\");\nconst object = await bucket.get(\"file.txt\");\n```\n\n**Durable Objects:**\n```js\nconst ns = await mf.getDurableObjectNamespace(\"COUNTER\");\nconst id = ns.idFromName(\"test\");\nconst stub = ns.get(id);\nconst res = await stub.fetch(\"http://localhost/\");\n\n// Access storage directly:\nconst storage = await mf.getDurableObjectStorage(id);\nawait storage.put(\"key\", \"value\");\n```\n\n**D1:**\n```js\nconst db = await mf.getD1Database(\"DB\");\nawait db.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)`);\nawait db.prepare(\"INSERT INTO users (name) VALUES (?)\").bind(\"Alice\").run();\n```\n\n**Cache:**\n```js\nconst caches = await mf.getCaches();\nconst defaultCache = caches.default;\nawait defaultCache.put(\"http://example.com\", new Response(\"cached\"));\n```\n\n**Queue producer:**\n```js\nconst producer = await mf.getQueueProducer(\"QUEUE\");\nawait producer.send({ body: \"message data\" });\n```\n\n## Lifecycle\n\n**Reload:**\n```js\nawait mf.setOptions({\n  scriptPath: \"worker.js\",\n  bindings: { VERSION: \"2.0\" },\n});\n```\n\n**Watch (manual):**\n```js\nimport { watch } from \"fs\";\n\nconst config = { scriptPath: \"worker.js\" };\nconst mf = new Miniflare(config);\n\nwatch(\"worker.js\", async () => {\n  console.log(\"Reloading...\");\n  await mf.setOptions(config);\n});\n```\n\n**Cleanup:**\n```js\nawait mf.dispose();\n```\n\n## Debugging\n\n**Inspector URL for DevTools:**\n```js\nconst url = await mf.getInspectorURL();\nconsole.log(`DevTools: ${url}`);\n// Open in Chrome DevTools for breakpoints, profiling\n```\n\n**Wait for server ready:**\n```js\nconst mf = new Miniflare({ scriptPath: \"worker.js\" });\nconst url = await mf.ready; // Promise<URL>\nconsole.log(`Server running at ${url}`); // http://127.0.0.1:8787\n\n// Note: dispatchFetch() waits automatically, no need to await ready\nconst res = await mf.dispatchFetch(\"http://localhost/\"); // Works immediately\n```\n\nSee [configuration.md](./configuration.md) for all constructor options.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/miniflare/configuration.md",
    "content": "# Configuration\n\n## Script Loading\n\n```js\n// Inline\nnew Miniflare({ modules: true, script: `export default { ... }` });\n\n// File-based\nnew Miniflare({ scriptPath: \"worker.js\" });\n\n// Multi-module\nnew Miniflare({\n  scriptPath: \"src/index.js\",\n  modules: true,\n  modulesRules: [\n    { type: \"ESModule\", include: [\"**/*.js\"] },\n    { type: \"Text\", include: [\"**/*.txt\"] },\n  ],\n});\n```\n\n## Compatibility\n\n```js\nnew Miniflare({\n  compatibilityDate: \"2026-01-01\", // Use recent date for latest features\n  compatibilityFlags: [\n    \"nodejs_compat\",        // Node.js APIs (process, Buffer, etc)\n    \"streams_enable_constructors\", // Stream constructors\n  ],\n  upstream: \"https://example.com\", // Fallback for unhandled requests\n});\n```\n\n**Critical:** Use `compatibilityDate: \"2026-01-01\"` or latest to match production runtime. Old dates limit available APIs.\n\n## HTTP Server & Request.cf\n\n```js\nnew Miniflare({\n  port: 8787,              // Default: 8787\n  host: \"127.0.0.1\",\n  https: true,             // Self-signed cert\n  liveReload: true,        // Auto-reload HTML\n  \n  cf: true,                // Fetch live Request.cf data (cached)\n  // cf: \"./cf.json\",      // Or load from file\n  // cf: { colo: \"DFW\" },  // Or inline mock\n});\n```\n\n**Note:** For tests, use `dispatchFetch()` (no port conflicts).\n\n## Storage Bindings\n\n```js\nnew Miniflare({\n  // KV\n  kvNamespaces: [\"TEST_NAMESPACE\", \"CACHE\"],\n  kvPersist: \"./kv-data\", // Optional: persist to disk\n  \n  // R2\n  r2Buckets: [\"BUCKET\", \"IMAGES\"],\n  r2Persist: \"./r2-data\",\n  \n  // Durable Objects\n  modules: true,\n  durableObjects: {\n    COUNTER: \"Counter\", // className\n    API_OBJECT: { className: \"ApiObject\", scriptName: \"api-worker\" },\n  },\n  durableObjectsPersist: \"./do-data\",\n  \n  // D1\n  d1Databases: [\"DB\"],\n  d1Persist: \"./d1-data\",\n  \n  // Cache\n  cache: true, // Default\n  cachePersist: \"./cache-data\",\n});\n```\n\n## Bindings\n\n```js\nnew Miniflare({\n  // Environment variables\n  bindings: {\n    SECRET_KEY: \"my-secret-value\",\n    API_URL: \"https://api.example.com\",\n    DEBUG: true,\n  },\n  \n  // Other bindings\n  wasmBindings: { ADD_MODULE: \"./add.wasm\" },\n  textBlobBindings: { TEXT: \"./data.txt\" },\n  queueProducers: [\"QUEUE\"],\n});\n```\n\n## Multiple Workers\n\n```js\nnew Miniflare({\n  workers: [\n    {\n      name: \"main\",\n      kvNamespaces: { DATA: \"shared\" },\n      serviceBindings: { API: \"api-worker\" },\n      script: `export default { ... }`,\n    },\n    {\n      name: \"api-worker\",\n      kvNamespaces: { DATA: \"shared\" }, // Shared storage\n      script: `export default { ... }`,\n    },\n  ],\n});\n```\n\n**With routing:**\n```js\nworkers: [\n  { name: \"api\", scriptPath: \"./api.js\", routes: [\"api.example.com/*\"] },\n  { name: \"web\", scriptPath: \"./web.js\", routes: [\"example.com/*\"] },\n],\n```\n\n## Logging & Performance\n\n```js\nimport { Log, LogLevel } from \"miniflare\";\n\nnew Miniflare({\n  log: new Log(LogLevel.DEBUG), // DEBUG | INFO | WARN | ERROR | NONE\n  scriptTimeout: 30000,         // CPU limit (ms)\n  workersConcurrencyLimit: 10,  // Max concurrent workers\n});\n```\n\n## Workers Sites\n\n```js\nnew Miniflare({\n  sitePath: \"./public\",\n  siteInclude: [\"**/*.html\", \"**/*.css\"],\n  siteExclude: [\"**/*.map\"],\n});\n```\n\n## From wrangler.toml\n\nMiniflare doesn't auto-read `wrangler.toml`:\n\n```toml\n# wrangler.toml\nname = \"my-worker\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2026-01-01\"\n[[kv_namespaces]]\nbinding = \"KV\"\n```\n\n```js\n// Miniflare equivalent\nnew Miniflare({\n  scriptPath: \"src/index.ts\",\n  compatibilityDate: \"2026-01-01\",\n  kvNamespaces: [\"KV\"],\n});\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/miniflare/gotchas.md",
    "content": "# Gotchas & Troubleshooting\n\n## Miniflare Limitations\n\n**Not supported:**\n- Analytics Engine (use mocks)\n- Cloudflare Images/Stream\n- Browser Rendering API\n- Tail Workers\n- Workers for Platforms (partial support)\n\n**Behavior differences from production:**\n- Runs workerd locally, not Cloudflare edge\n- Storage is local (filesystem/memory), not distributed\n- `Request.cf` is cached/mocked, not real edge data\n- Performance differs from edge\n- Caching implementation may vary slightly\n\n## Common Errors\n\n### \"Cannot find module\"\n**Cause:** Module path wrong or `modulesRules` not configured  \n**Solution:**\n```js\nnew Miniflare({\n  modules: true,\n  modulesRules: [{ type: \"ESModule\", include: [\"**/*.js\"] }],\n});\n```\n\n### \"Data not persisting\"\n**Cause:** Persist paths are files, not directories  \n**Solution:**\n```js\nkvPersist: \"./data/kv\",  // Directory, not file\n```\n\n### \"Cannot run TypeScript\"\n**Cause:** Miniflare doesn't transpile TypeScript  \n**Solution:** Build first with esbuild/tsc, then run compiled JS\n\n### \"`request.cf` is undefined\"\n**Cause:** CF data not configured  \n**Solution:**\n```js\nnew Miniflare({ cf: true }); // Or cf: \"./cf.json\"\n```\n\n### \"EADDRINUSE\" port conflict\n**Cause:** Multiple instances using same port  \n**Solution:** Use `dispatchFetch()` (no HTTP server) or `port: 0` for auto-assign\n\n### \"Durable Object not found\"\n**Cause:** Class export doesn't match config name  \n**Solution:**\n```js\nexport class Counter {} // Must match\nnew Miniflare({ durableObjects: { COUNTER: \"Counter\" } });\n```\n\n## Debugging\n\n**Enable verbose logging:**\n```js\nimport { Log, LogLevel } from \"miniflare\";\nnew Miniflare({ log: new Log(LogLevel.DEBUG) });\n```\n\n**Chrome DevTools:**\n```js\nconst url = await mf.getInspectorURL();\nconsole.log(`DevTools: ${url}`); // Open in Chrome\n```\n\n**Inspect bindings:**\n```js\nconst env = await mf.getBindings();\nconsole.log(Object.keys(env));\n```\n\n**Verify storage:**\n```js\nconst ns = await mf.getKVNamespace(\"TEST\");\nconst { keys } = await ns.list();\n```\n\n## Best Practices\n\n**✓ Do:**\n- Use `dispatchFetch()` for tests (no HTTP server)\n- In-memory storage for CI (omit persist options)\n- New instances per test for isolation\n- Type-safe bindings with interfaces\n- `await mf.dispose()` in cleanup\n\n**✗ Avoid:**\n- HTTP server in tests\n- Shared instances without cleanup\n- Old compatibility dates (use 2026+)\n\n## Migration Guides\n\n### From Miniflare 2.x to 3+\n\nBreaking changes in v3+:\n\n| v2 | v3+ |\n|----|-----|\n| `getBindings()` sync | `getBindings()` returns Promise |\n| `ready` is void | `ready` returns `Promise<URL>` |\n| service-worker-mock | Built on workerd |\n| Different options | Restructured constructor |\n\n**Example migration:**\n```js\n// v2\nconst bindings = mf.getBindings();\nmf.ready; // void\n\n// v3+\nconst bindings = await mf.getBindings();\nconst url = await mf.ready; // Promise<URL>\n```\n\n### From unstable_dev to Miniflare\n\n```js\n// Old (deprecated)\nimport { unstable_dev } from \"wrangler\";\nconst worker = await unstable_dev(\"src/index.ts\");\n\n// New\nimport { Miniflare } from \"miniflare\";\nconst mf = new Miniflare({ scriptPath: \"src/index.ts\" });\n```\n\n### From Wrangler Dev\n\nMiniflare doesn't auto-read `wrangler.toml`:\n\n```js\n// Translate manually:\nnew Miniflare({\n  scriptPath: \"dist/worker.js\",\n  compatibilityDate: \"2026-01-01\",\n  kvNamespaces: [\"KV\"],\n  bindings: { API_KEY: process.env.API_KEY },\n});\n```\n\n## Resource Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| CPU time | 30s default | Configurable via `scriptTimeout` |\n| Storage | Filesystem | Performance varies by disk |\n| Memory | System dependent | No artificial limits |\n| Request.cf | Cached/mocked | Not live edge data |\n\nSee [patterns.md](./patterns.md) for testing examples.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/miniflare/patterns.md",
    "content": "# Testing Patterns\n\n## Choosing a Testing Approach\n\n| Approach | Use Case | Speed | Setup | Runtime |\n|----------|----------|-------|-------|---------|\n| **getPlatformProxy** | Unit tests, logic testing | Fast | Low | Miniflare |\n| **Miniflare API** | Integration tests, full control | Medium | Medium | Miniflare |\n| **vitest-pool-workers** | Vitest runner integration | Medium | Medium | workerd |\n\n**Quick guide:**\n- Unit tests → getPlatformProxy\n- Integration tests → Miniflare API\n- Vitest workflows → vitest-pool-workers\n\n## getPlatformProxy\n\nLightweight unit testing - provides bindings without full Worker runtime.\n\n```js\n// vitest.config.js\nexport default { test: { environment: \"node\" } };\n```\n\n```js\nimport { env } from \"cloudflare:test\";\nimport { describe, it, expect } from \"vitest\";\n\ndescribe(\"Business logic\", () => {\n  it(\"processes data with KV\", async () => {\n    await env.KV.put(\"test\", \"value\");\n    expect(await env.KV.get(\"test\")).toBe(\"value\");\n  });\n});\n```\n\n**Pros:** Fast, simple  \n**Cons:** No full runtime, can't test fetch handler\n\n## vitest-pool-workers\n\nFull Workers runtime in Vitest. Reads `wrangler.toml`.\n\n```bash\nnpm i -D @cloudflare/vitest-pool-workers\n```\n\n```js\n// vitest.config.js\nimport { defineWorkersConfig } from \"@cloudflare/vitest-pool-workers/config\";\n\nexport default defineWorkersConfig({\n  test: {\n    poolOptions: { workers: { wrangler: { configPath: \"./wrangler.toml\" } } },\n  },\n});\n```\n\n```js\nimport { env, SELF } from \"cloudflare:test\";\nimport { it, expect } from \"vitest\";\n\nit(\"handles fetch\", async () => {\n  const res = await SELF.fetch(\"http://example.com/\");\n  expect(res.status).toBe(200);\n});\n```\n\n**Pros:** Full runtime, uses wrangler.toml  \n**Cons:** Requires Wrangler config\n\n## Miniflare API (node:test)\n\n```js\nimport assert from \"node:assert\";\nimport test, { after, before } from \"node:test\";\nimport { Miniflare } from \"miniflare\";\n\nlet mf;\nbefore(() => {\n  mf = new Miniflare({ scriptPath: \"src/index.js\", kvNamespaces: [\"TEST_KV\"] });\n});\n\ntest(\"fetch\", async () => {\n  const res = await mf.dispatchFetch(\"http://localhost/\");\n  assert.strictEqual(await res.text(), \"Hello\");\n});\n\nafter(() => mf.dispose());\n```\n\n## Testing Durable Objects & Events\n\n```js\n// Durable Objects\nconst ns = await mf.getDurableObjectNamespace(\"COUNTER\");\nconst stub = ns.get(ns.idFromName(\"test-counter\"));\nawait stub.fetch(\"http://localhost/increment\");\n\n// Direct storage\nconst storage = await mf.getDurableObjectStorage(ns.idFromName(\"test-counter\"));\nconst count = await storage.get(\"count\");\n\n// Queue\nconst worker = await mf.getWorker();\nawait worker.queue(\"my-queue\", [\n  { id: \"msg1\", timestamp: new Date(), body: { userId: 123 }, attempts: 1 },\n]);\n\n// Scheduled\nawait worker.scheduled({ cron: \"0 0 * * *\" });\n```\n\n## Test Isolation & Mocking\n\n```js\n// Per-test isolation\nbeforeEach(() => { mf = new Miniflare({ kvNamespaces: [\"TEST\"] }); });\nafterEach(() => mf.dispose());\n\n// Mock external APIs\nnew Miniflare({\n  workers: [\n    { name: \"main\", serviceBindings: { API: \"mock-api\" }, script: `...` },\n    { name: \"mock-api\", script: `export default { async fetch() { return Response.json({mock: true}); } }` },\n  ],\n});\n```\n\n## Type Safety\n\n```ts\nimport type { KVNamespace } from \"@cloudflare/workers-types\";\n\ninterface Env {\n  KV: KVNamespace;\n  API_KEY: string;\n}\n\nconst env = await mf.getBindings<Env>();\nawait env.KV.put(\"key\", \"value\"); // Typed!\n\nexport default {\n  async fetch(req: Request, env: Env) {\n    return new Response(await env.KV.get(\"key\"));\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## WebSocket Testing\n\n```js\nconst res = await mf.dispatchFetch(\"http://localhost/ws\", {\n  headers: { Upgrade: \"websocket\" },\n});\nassert.strictEqual(res.status, 101);\n```\n\n## Migration from unstable_dev\n\n```js\n// Old (deprecated)\nimport { unstable_dev } from \"wrangler\";\nconst worker = await unstable_dev(\"src/index.ts\");\n\n// New\nimport { Miniflare } from \"miniflare\";\nconst mf = new Miniflare({ scriptPath: \"src/index.ts\" });\n```\n\n## CI/CD Tips\n\n```js\n// In-memory storage (faster)\nnew Miniflare({ kvNamespaces: [\"TEST\"] }); // No persist = in-memory\n\n// Use dispatchFetch (no port conflicts)\nawait mf.dispatchFetch(\"http://localhost/\");\n```\n\nSee [gotchas.md](./gotchas.md) for troubleshooting.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/network-interconnect/README.md",
    "content": "# Cloudflare Network Interconnect (CNI)\n\nPrivate, high-performance connectivity to Cloudflare's network. **Enterprise-only**.\n\n## Connection Types\n\n**Direct**: Physical fiber in shared datacenter. 10/100 Gbps. You order cross-connect.\n\n**Partner**: Virtual via Console Connect, Equinix, Megaport, etc. Managed via partner SDN.\n\n**Cloud**: AWS Direct Connect or GCP Cloud Interconnect. Magic WAN only.\n\n## Dataplane Versions\n\n**v1 (Classic)**: GRE tunnel support, VLAN/BFD/LACP, asymmetric MTU (1500↓/1476↑), peering support.\n\n**v2 (Beta)**: No GRE, 1500 MTU both ways, no VLAN/BFD/LACP yet, ECMP instead.\n\n## Use Cases\n\n- **Magic Transit DSR**: DDoS protection, egress via ISP (v1/v2)\n- **Magic Transit + Egress**: DDoS + egress via CF (v1/v2)\n- **Magic WAN + Zero Trust**: Private backbone (v1 needs GRE, v2 native)\n- **Peering**: Public routes at PoP (v1 only)\n- **App Security**: WAF/Cache/LB (v1/v2 over Magic Transit)\n\n## Prerequisites\n\n- Enterprise plan\n- IPv4 /24+ or IPv6 /48+ prefixes\n- BGP ASN for v1\n- See [locations PDF](https://developers.cloudflare.com/network-interconnect/static/cni-locations-2026-01.pdf)\n\n## Specs\n\n- /31 point-to-point subnets\n- 10km max optical distance\n- 10G: 10GBASE-LR single-mode\n- 100G: 100GBASE-LR4 single-mode\n- **No SLA** (free service)\n- Backup Internet required\n\n## Throughput\n\n| Direction | 10G | 100G |\n|-----------|-----|------|\n| CF → Customer | 10 Gbps | 100 Gbps |\n| Customer → CF (peering) | 10 Gbps | 100 Gbps |\n| Customer → CF (Magic) | 1 Gbps/tunnel or CNI | 1 Gbps/tunnel or CNI |\n\n## Timeline\n\n2-4 weeks typical. Steps: request → config review → order connection → configure → test → enable health checks → activate → monitor.\n\n## In This Reference\n- [configuration.md](./configuration.md) - BGP, routing, setup\n- [api.md](./api.md) - API endpoints, SDKs\n- [patterns.md](./patterns.md) - HA, hybrid cloud, failover\n- [gotchas.md](./gotchas.md) - Troubleshooting, limits\n\n## Reading Order by Task\n\n| Task | Files to Load |\n|------|---------------|\n| Initial setup | README → configuration.md → api.md |\n| Create interconnect via API | api.md → gotchas.md |\n| Design HA architecture | patterns.md → README |\n| Troubleshoot connection | gotchas.md → configuration.md |\n| Cloud integration (AWS/GCP) | configuration.md → patterns.md |\n| Monitor + alerts | configuration.md |\n\n## Automation Boundary\n\n**API-Automatable:**\n- List/create/delete interconnects (Direct, Partner)\n- List available slots\n- Get interconnect status\n- Download LOA PDF\n- Create/update CNI objects (BGP config)\n- Query settings\n\n**Requires Account Team:**\n- Initial request approval\n- AWS Direct Connect setup (send LOA+VLAN to CF)\n- GCP Cloud Interconnect final activation\n- Partner interconnect acceptance (Equinix, Megaport)\n- VLAN assignment (v1)\n- Configuration document generation (v1)\n- Escalations + troubleshooting support\n\n**Cannot Be Automated:**\n- Physical cross-connect installation (Direct)\n- Partner portal operations (virtual circuit ordering)\n- AWS/GCP portal operations\n- Maintenance window coordination\n\n## See Also\n- [tunnel](../tunnel/) - Alternative for private network connectivity\n- [spectrum](../spectrum/) - Layer 4 proxy for TCP/UDP traffic\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/network-interconnect/api.md",
    "content": "# CNI API Reference\n\nSee [README.md](README.md) for overview.\n\n## Base\n\n```\nhttps://api.cloudflare.com/client/v4\nAuth: Authorization: Bearer <token>\n```\n\n## SDK Namespaces\n\n**Primary (recommended):**\n```typescript\nclient.networkInterconnects.interconnects.*\nclient.networkInterconnects.cnis.*\nclient.networkInterconnects.slots.*\n```\n\n**Alternate (deprecated):**\n```typescript\nclient.magicTransit.cfInterconnects.*\n```\n\nUse `networkInterconnects` namespace for all new code.\n\n## Interconnects\n\n```http\nGET    /accounts/{account_id}/cni/interconnects              # Query: page, per_page\nPOST   /accounts/{account_id}/cni/interconnects              # Query: validate_only=true (optional)\nGET    /accounts/{account_id}/cni/interconnects/{icon}\nGET    /accounts/{account_id}/cni/interconnects/{icon}/status\nGET    /accounts/{account_id}/cni/interconnects/{icon}/loa   # Returns PDF\nDELETE /accounts/{account_id}/cni/interconnects/{icon}\n```\n\n**Create Body:** `account`, `slot_id`, `type`, `facility`, `speed`, `name`, `description`  \n**Status Values:** `active` | `healthy` | `unhealthy` | `pending` | `down`\n\n**Response Example:**\n```json\n{\"result\": [{\"id\": \"icon_abc\", \"name\": \"prod\", \"type\": \"direct\", \"facility\": \"EWR1\", \"speed\": \"10G\", \"status\": \"active\"}]}\n```\n\n## CNI Objects (BGP config)\n\n```http\nGET    /accounts/{account_id}/cni/cnis\nPOST   /accounts/{account_id}/cni/cnis\nGET    /accounts/{account_id}/cni/cnis/{cni}\nPUT    /accounts/{account_id}/cni/cnis/{cni}\nDELETE /accounts/{account_id}/cni/cnis/{cni}\n```\n\nBody: `account`, `cust_ip`, `cf_ip`, `bgp_asn`, `bgp_password`, `vlan`\n\n## Slots\n\n```http\nGET /accounts/{account_id}/cni/slots\nGET /accounts/{account_id}/cni/slots/{slot}\n```\n\nQuery: `facility`, `occupied`, `speed`\n\n## Health Checks\n\nConfigure via Magic Transit/WAN tunnel endpoints (CNI v2).\n\n```typescript\nawait client.magicTransit.tunnels.update(accountId, tunnelId, {\n  health_check: { enabled: true, target: '192.0.2.1', rate: 'high', type: 'request' },\n});\n```\n\nRates: `high` | `medium` | `low`. Types: `request` | `reply`. See [Magic Transit docs](https://developers.cloudflare.com/magic-transit/how-to/configure-tunnel-endpoints/#add-tunnels).\n\n## Settings\n\n```http\nGET /accounts/{account_id}/cni/settings\nPUT /accounts/{account_id}/cni/settings\n```\n\nBody: `default_asn`\n\n## TypeScript SDK\n\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst client = new Cloudflare({ apiToken: process.env.CF_TOKEN });\n\n// List\nawait client.networkInterconnects.interconnects.list({ account_id: id });\n\n// Create with validation\nawait client.networkInterconnects.interconnects.create({\n  account_id: id,\n  account: id,\n  slot_id: 'slot_abc',\n  type: 'direct',\n  facility: 'EWR1',\n  speed: '10G',\n  name: 'prod-interconnect',\n}, {\n  query: { validate_only: true }, // Dry-run validation\n});\n\n// Create without validation\nawait client.networkInterconnects.interconnects.create({\n  account_id: id,\n  account: id,\n  slot_id: 'slot_abc',\n  type: 'direct',\n  facility: 'EWR1',\n  speed: '10G',\n  name: 'prod-interconnect',\n});\n\n// Status\nawait client.networkInterconnects.interconnects.get(accountId, iconId);\n\n// LOA (use fetch)\nconst res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${id}/cni/interconnects/${iconId}/loa`, {\n  headers: { Authorization: `Bearer ${token}` },\n});\nawait fs.writeFile('loa.pdf', Buffer.from(await res.arrayBuffer()));\n\n// CNI object\nawait client.networkInterconnects.cnis.create({\n  account_id: id,\n  account: id,\n  cust_ip: '192.0.2.1/31',\n  cf_ip: '192.0.2.0/31',\n  bgp_asn: 65000,\n  vlan: 100,\n});\n\n// Slots (filter by facility and speed)\nawait client.networkInterconnects.slots.list({\n  account_id: id,\n  occupied: false,\n  facility: 'EWR1',\n  speed: '10G',\n});\n```\n\n## Python SDK\n\n```python\nfrom cloudflare import Cloudflare\n\nclient = Cloudflare(api_token=os.environ[\"CF_TOKEN\"])\n\n# List, create, status (same pattern as TypeScript)\nclient.network_interconnects.interconnects.list(account_id=id)\nclient.network_interconnects.interconnects.create(account_id=id, account=id, slot_id=\"slot_abc\", type=\"direct\", facility=\"EWR1\", speed=\"10G\")\nclient.network_interconnects.interconnects.get(account_id=id, icon=icon_id)\n\n# CNI objects and slots\nclient.network_interconnects.cnis.create(account_id=id, cust_ip=\"192.0.2.1/31\", cf_ip=\"192.0.2.0/31\", bgp_asn=65000)\nclient.network_interconnects.slots.list(account_id=id, occupied=False)\n```\n\n## cURL\n\n```bash\n# List interconnects\ncurl \"https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cni/interconnects\" \\\n  -H \"Authorization: Bearer ${CF_TOKEN}\"\n\n# Create interconnect\ncurl -X POST \"https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cni/interconnects?validate_only=true\" \\\n  -H \"Authorization: Bearer ${CF_TOKEN}\" -H \"Content-Type: application/json\" \\\n  -d '{\"account\": \"id\", \"slot_id\": \"slot_abc\", \"type\": \"direct\", \"facility\": \"EWR1\", \"speed\": \"10G\"}'\n\n# LOA PDF\ncurl \"https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cni/interconnects/${ICON_ID}/loa\" \\\n  -H \"Authorization: Bearer ${CF_TOKEN}\" --output loa.pdf\n```\n\n## Not Available via API\n\n**Missing Capabilities:**\n- BGP session state query (use Dashboard or BGP logs)\n- Bandwidth utilization metrics (use external monitoring)\n- Traffic statistics per interconnect\n- Historical uptime/downtime data\n- Light level readings (contact account team)\n- Maintenance window scheduling (notifications only)\n\n## Resources\n\n- [API Docs](https://developers.cloudflare.com/api/resources/network_interconnects/)\n- [TypeScript SDK](https://github.com/cloudflare/cloudflare-typescript)\n- [Python SDK](https://github.com/cloudflare/cloudflare-python)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/network-interconnect/configuration.md",
    "content": "# CNI Configuration\n\nSee [README.md](README.md) for overview.\n\n## Workflow (2-4 weeks)\n\n1. **Submit request** (Week 1): Contact account team, provide type/location/use case\n2. **Review config** (Week 1-2, v1 only): Approve IP/VLAN/spec doc\n3. **Order connection** (Week 2-3):\n   - **Direct**: Get LOA, order cross-connect from facility\n   - **Partner**: Order virtual circuit in partner portal\n   - **Cloud**: Order Direct Connect/Cloud Interconnect, send LOA+VLAN to CF\n4. **Configure** (Week 3): Both sides configure per doc\n5. **Test** (Week 3-4): Ping, verify BGP, check routes\n6. **Health checks** (Week 4): Configure [Magic Transit](https://developers.cloudflare.com/magic-transit/how-to/configure-tunnel-endpoints/#add-tunnels) or [Magic WAN](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-tunnel-endpoints/#add-tunnels) health checks\n7. **Activate** (Week 4): Route traffic, verify flow\n8. **Monitor**: Enable [maintenance notifications](https://developers.cloudflare.com/network-interconnect/monitoring-and-alerts/#enable-cloudflare-status-maintenance-notification)\n\n## BGP Configuration\n\n**v1 Requirements:**\n- BGP ASN (provide during setup)\n- /31 subnet for peering\n- Optional: BGP password\n\n**v2:** Simplified, less BGP config needed.\n\n**BGP over CNI (Dec 2024):** Magic WAN/Transit can now peer BGP directly over CNI v2 (no GRE tunnel required).\n\n**Example v1 BGP:**\n```\nRouter ID: 192.0.2.1\nPeer IP: 192.0.2.0\nRemote ASN: 13335\nLocal ASN: 65000\nPassword: [optional]\nVLAN: 100\n```\n\n## Cloud Interconnect Setup\n\n### AWS Direct Connect (Beta)\n\n**Requirements:** Magic WAN, AWS Dedicated Direct Connect 1/10 Gbps.\n\n**Process:**\n1. Contact CF account team\n2. Choose location\n3. Order in AWS portal\n4. AWS provides LOA + VLAN ID\n5. Send to CF account team\n6. Wait ~4 weeks\n\n**Post-setup:** Add [static routes](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-routes/#configure-static-routes) to Magic WAN. Enable [bidirectional health checks](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-tunnel-endpoints/#legacy-bidirectional-health-checks).\n\n### GCP Cloud Interconnect (Beta)\n\n**Setup via Dashboard:**\n1. Interconnects → Create → Cloud Interconnect → Google\n2. Provide name, MTU (match GCP VLAN attachment), speed (50M-50G granular options available for partner interconnects)\n3. Enter VLAN attachment pairing key\n4. Confirm order\n\n**Routing to GCP:** Add [static routes](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-routes/#configure-static-routes). BGP routes from GCP Cloud Router **ignored**.\n\n**Routing to CF:** Configure [custom learned routes](https://cloud.google.com/network-connectivity/docs/router/how-to/configure-custom-learned-routes) in Cloud Router. Request prefixes from CF account team.\n\n## Monitoring\n\n**Dashboard Status:**\n\n| Status | Meaning |\n|--------|---------|\n| **Healthy** | Link operational, traffic flowing, health checks passing |\n| **Active** | Link up, sufficient light, Ethernet negotiated |\n| **Unhealthy** | Link down, no/low light (<-20 dBm), can't negotiate |\n| **Pending** | Cross-connect incomplete, device unresponsive, RX/TX swapped |\n| **Down** | Physical link down, no connectivity |\n\n**Alerts:**\n\n**CNI Connection Maintenance** (Magic Networking only):\n```\nDashboard → Notifications → Add\nProduct: Cloudflare Network Interconnect\nType: Connection Maintenance Alert\n```\nWarnings up to 2 weeks advance. 6hr delay for new additions.\n\n**Cloudflare Status Maintenance** (entire PoP):\n```\nDashboard → Notifications → Add\nProduct: Cloudflare Status\nFilter PoPs: gru,fra,lhr\n```\n\n**Find PoP code:**\n```\nDashboard → Magic Transit/WAN → Configuration → Interconnects\nSelect CNI → Note Data Center (e.g., \"gru-b\")\nUse first 3 letters: \"gru\"\n```\n\n## Best Practices\n\n**Critical config-specific practices:**\n- /31 subnets required for BGP\n- BGP passwords recommended\n- BFD for fast failover (v1 only)\n- Test ping connectivity before BGP\n- Enable maintenance notifications immediately after activation\n- Monitor status programmatically via API\n\nFor design patterns, HA architecture, and security best practices, see [patterns.md](./patterns.md).\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/network-interconnect/gotchas.md",
    "content": "# CNI Gotchas & Troubleshooting\n\n## Common Errors\n\n### \"Status: Pending\"\n\n**Cause:** Cross-connect not installed, RX/TX fibers reversed, wrong fiber type, or low light levels\n**Solution:**\n1. Verify cross-connect installed\n2. Check fiber at patch panel\n3. Swap RX/TX fibers\n4. Check light with optical power meter (target > -20 dBm)\n5. Contact account team\n\n### \"Status: Unhealthy\"\n\n**Cause:** Physical issue, low light (<-20 dBm), optic mismatch, or dirty connectors\n**Solution:**\n1. Check physical connections\n2. Clean fiber connectors\n3. Verify optic types (10GBASE-LR/100GBASE-LR4)\n4. Test with known-good optics\n5. Check patch panel\n6. Contact account team\n\n### \"BGP Session Down\"\n\n**Cause:** Wrong IP addressing, wrong ASN, password mismatch, or firewall blocking TCP/179\n**Solution:**\n1. Verify IPs match CNI object\n2. Confirm ASN correct\n3. Check BGP password\n4. Verify no firewall on TCP/179\n5. Check BGP logs\n6. Review BGP timers\n\n### \"Low Throughput\"\n\n**Cause:** MTU mismatch, fragmentation, single GRE tunnel (v1), or routing inefficiency\n**Solution:**\n1. Check MTU (1500↓/1476↑ for v1, 1500 both for v2)\n2. Test various packet sizes\n3. Add more GRE tunnels (v1)\n4. Consider upgrading to v2\n5. Review routing tables\n6. Use LACP for bundling (v1)\n\n## API Errors\n\n### 400 Bad Request: \"slot_id already occupied\"\n\n**Cause:** Another interconnect already uses this slot  \n**Solution:** Use `occupied=false` filter when listing slots:\n```typescript\nawait client.networkInterconnects.slots.list({\n  account_id: id,\n  occupied: false,\n  facility: 'EWR1',\n});\n```\n\n### 400 Bad Request: \"invalid facility code\"\n\n**Cause:** Typo or unsupported facility  \n**Solution:** Check [locations PDF](https://developers.cloudflare.com/network-interconnect/static/cni-locations-2026-01.pdf) for valid codes\n\n### 403 Forbidden: \"Enterprise plan required\"\n\n**Cause:** Account not enterprise-level  \n**Solution:** Contact account team to upgrade\n\n### 422 Unprocessable: \"validate_only request failed\"\n\n**Cause:** Dry-run validation found issues (wrong slot, invalid config)  \n**Solution:** Review error message details, fix config before real creation\n\n### Rate Limiting\n\n**Limit:** 1200 requests/5min per token  \n**Solution:** Implement exponential backoff, cache slot listings\n\n## Cloud-Specific Issues\n\n### AWS Direct Connect: \"VLAN not matching\"\n\n**Cause:** VLAN ID from AWS LOA doesn't match CNI config  \n**Solution:**\n1. Get VLAN from AWS Console after ordering\n2. Send exact VLAN to CF account team\n3. Verify match in CNI object config\n\n### AWS: \"Connection stuck in Pending\"\n\n**Cause:** LOA not provided to CF or AWS connection not accepted  \n**Solution:**\n1. Verify AWS connection status is \"Available\"\n2. Confirm LOA sent to CF account team\n3. Wait for CF team acceptance (can take days)\n\n### GCP: \"BGP routes not propagating\"\n\n**Cause:** BGP routes from GCP Cloud Router **ignored by design**  \n**Solution:** Use [static routes](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-routes/#configure-static-routes) in Magic WAN instead\n\n### GCP: \"Cannot query VLAN attachment status via API\"\n\n**Cause:** GCP Cloud Interconnect Dashboard-only (no API yet)  \n**Solution:** Check status in CF Dashboard or GCP Console\n\n## Partner Interconnect Issues\n\n### Equinix: \"Virtual circuit not appearing\"\n\n**Cause:** CF hasn't accepted Equinix connection request  \n**Solution:**\n1. Verify VC created in Equinix Fabric Portal\n2. Contact CF account team to accept\n3. Allow 2-3 business days\n\n### Console Connect/Megaport: \"API creation fails\"\n\n**Cause:** Partner interconnects require partner portal + CF approval  \n**Solution:** Cannot fully automate. Order in partner portal, notify CF account team.\n\n## Anti-Patterns\n\n| Anti-Pattern | Why Bad | Solution |\n|--------------|---------|----------|\n| Single interconnect for production | No SLA, single point of failure | Use ≥2 with device diversity |\n| No backup Internet | CNI fails = total outage | Always maintain alternate path |\n| Polling status every second | Rate limits, wastes API calls | Poll every 30-60s max |\n| Using v1 for Magic WAN v2 workloads | GRE overhead, complexity | Use v2 for simplified routing |\n| Assuming BGP session = traffic flowing | BGP up ≠ routes installed | Verify routing tables + test traffic |\n| Not enabling maintenance alerts | Surprise downtime during maintenance | Enable notifications immediately |\n| Hardcoding VLAN in automation | VLAN assigned by CF (v1) | Get VLAN from CNI object response |\n| Using Direct without colocation | Can't access cross-connect | Use Partner or Cloud interconnect |\n\n## What's Not Queryable via API\n\n**Cannot retrieve:**\n- BGP session state (use Dashboard or BGP logs)\n- Light levels (contact account team)\n- Historical metrics (uptime, traffic)\n- Bandwidth utilization per interconnect\n- Maintenance window schedules (notifications only)\n- Fiber path details\n- Cross-connect installation status\n\n**Workarounds:**\n- External monitoring for BGP state\n- Log aggregation for historical data\n- Notifications for maintenance windows\n\n## Limits\n\n| Resource/Limit | Value | Notes |\n|----------------|-------|-------|\n| Max optical distance | 10km | Physical limit |\n| MTU (v1) | 1500↓ / 1476↑ | Asymmetric |\n| MTU (v2) | 1500 both | Symmetric |\n| GRE tunnel throughput | 1 Gbps | Per tunnel (v1) |\n| Recovery time | Days | No formal SLA |\n| Light level minimum | -20 dBm | Target threshold |\n| API rate limit | 1200 req/5min | Per token |\n| Health check delay | 6 hours | New maintenance alert subscriptions |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/network-interconnect/patterns.md",
    "content": "# CNI Patterns\n\nSee [README.md](README.md) for overview.\n\n## High Availability\n\n**Critical:** Design for resilience from day one.\n\n**Requirements:**\n- Device-level diversity (separate hardware)\n- Backup Internet connectivity (no SLA on CNI)\n- Network-resilient locations preferred\n- Regular failover testing\n\n**Architecture:**\n```\nYour Network A ──10G CNI v2──> CF CCR Device 1\n                                     │\nYour Network B ──10G CNI v2──> CF CCR Device 2\n                                     │\n                            CF Global Network (AS13335)\n```\n\n**Capacity Planning:**\n- Plan across all links\n- Account for failover scenarios\n- Your responsibility\n\n## Pattern: Magic Transit + CNI v2\n\n**Use Case:** DDoS protection, private connectivity, no GRE overhead.\n\n```typescript\n// 1. Create interconnect\nconst ic = await client.networkInterconnects.interconnects.create({\n  account_id: id,\n  type: 'direct',\n  facility: 'EWR1',\n  speed: '10G',\n  name: 'magic-transit-primary',\n});\n\n// 2. Poll until active\nconst status = await pollUntilActive(id, ic.id);\n\n// 3. Configure Magic Transit tunnel via Dashboard/API\n```\n\n**Benefits:** 1500 MTU both ways, simplified routing.\n\n## Pattern: Multi-Cloud Hybrid\n\n**Use Case:** AWS/GCP workloads with Cloudflare.\n\n**AWS Direct Connect:**\n```typescript\n// 1. Order Direct Connect in AWS Console\n// 2. Get LOA + VLAN from AWS\n// 3. Send to CF account team (no API)\n// 4. Configure static routes in Magic WAN\n\nawait configureStaticRoutes(id, {\n  prefix: '10.0.0.0/8',\n  nexthop: 'aws-direct-connect',\n});\n```\n\n**GCP Cloud Interconnect:**\n```\n1. Get VLAN attachment pairing key from GCP Console\n2. Create via Dashboard: Interconnects → Create → Cloud Interconnect → Google\n   - Enter pairing key, name, MTU, speed\n3. Configure static routes in Magic WAN (BGP routes from GCP ignored)\n4. Configure custom learned routes in GCP Cloud Router\n```\n\n**Note:** Dashboard-only. No API/SDK support yet.\n\n## Pattern: Multi-Location HA\n\n**Use Case:** 99.99%+ uptime.\n\n```typescript\n// Primary (NY)\nconst primary = await client.networkInterconnects.interconnects.create({\n  account_id: id,\n  type: 'direct',\n  facility: 'EWR1',\n  speed: '10G',\n  name: 'primary-ewr1',\n});\n\n// Secondary (NY, different hardware)\nconst secondary = await client.networkInterconnects.interconnects.create({\n  account_id: id,\n  type: 'direct',\n  facility: 'EWR2',\n  speed: '10G',\n  name: 'secondary-ewr2',\n});\n\n// Tertiary (LA, different geography)\nconst tertiary = await client.networkInterconnects.interconnects.create({\n  account_id: id,\n  type: 'partner',\n  facility: 'LAX1',\n  speed: '10G',\n  name: 'tertiary-lax1',\n});\n\n// BGP local preferences:\n// Primary: 200\n// Secondary: 150\n// Tertiary: 100\n// Internet: Last resort\n```\n\n## Pattern: Partner Interconnect (Equinix)\n\n**Use Case:** Quick deployment, no colocation.\n\n**Setup:**\n1. Order virtual circuit in Equinix Fabric Portal\n2. Select Cloudflare as destination\n3. Choose facility\n4. Send details to CF account team\n5. CF accepts in portal\n6. Configure BGP\n\n**No API automation** – partner portals managed separately.\n\n## Failover & Security\n\n**Failover Best Practices:**\n- Use BGP local preferences for priority\n- Configure BFD for fast detection (v1)\n- Test regularly with traffic shift\n- Document runbooks\n\n**Security:**\n- BGP password authentication\n- BGP route filtering\n- Monitor unexpected routes\n- Magic Firewall for DDoS/threats\n- Minimum API token permissions\n- Rotate credentials periodically\n\n## Decision Matrix\n\n| Requirement | Recommended |\n|-------------|-------------|\n| Collocated with CF | Direct |\n| Not collocated | Partner |\n| AWS/GCP workloads | Cloud |\n| 1500 MTU both ways | v2 |\n| VLAN tagging | v1 |\n| Public peering | v1 |\n| Simplest config | v2 |\n| BFD fast failover | v1 |\n| LACP bundling | v1 |\n\n## Resources\n\n- [Magic Transit Docs](https://developers.cloudflare.com/magic-transit/)\n- [Magic WAN Docs](https://developers.cloudflare.com/magic-wan/)\n- [Argo Smart Routing](https://developers.cloudflare.com/argo/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/observability/README.md",
    "content": "# Cloudflare Observability Skill Reference\n\n**Purpose**: Comprehensive guidance for implementing observability in Cloudflare Workers, covering traces, logs, metrics, and analytics.\n\n**Scope**: Cloudflare Observability features ONLY - Workers Logs, Traces, Analytics Engine, Logpush, Metrics & Analytics, and OpenTelemetry exports.\n\n---\n\n## Decision Tree: Which File to Load?\n\nUse this to route to the correct file without loading all content:\n\n```\n├─ \"How do I enable/configure X?\"           → configuration.md\n├─ \"What's the API/method/binding for X?\"   → api.md\n├─ \"How do I implement X pattern?\"          → patterns.md\n│   ├─ Usage tracking/billing               → patterns.md\n│   ├─ Error tracking                       → patterns.md\n│   ├─ Performance monitoring               → patterns.md\n│   ├─ Multi-tenant tracking                → patterns.md\n│   ├─ Tail Worker filtering                → patterns.md\n│   └─ OpenTelemetry export                 → patterns.md\n└─ \"Why isn't X working?\" / \"Limits?\"       → gotchas.md\n```\n\n## Reading Order\n\nLoad files in this order based on task:\n\n| Task Type | Load Order | Reason |\n|-----------|------------|--------|\n| **Initial setup** | configuration.md → gotchas.md | Setup first, avoid pitfalls |\n| **Implement feature** | patterns.md → api.md → gotchas.md | Pattern → API details → edge cases |\n| **Debug issue** | gotchas.md → configuration.md | Common issues first |\n| **Query data** | api.md → patterns.md | API syntax → query examples |\n\n## Product Overview\n\n### Workers Logs\n- **What:** Console output from Workers (console.log/warn/error)\n- **Access:** Dashboard (Real-time Logs), Logpush, Tail Workers\n- **Cost:** Free (included with all Workers)\n- **Retention:** Real-time only (no historical storage in dashboard)\n\n### Workers Traces\n- **What:** Execution traces with timing, CPU usage, outcome\n- **Access:** Dashboard (Workers Analytics → Traces), Logpush\n- **Cost:** $0.10/1M spans (GA pricing starts March 1, 2026), 10M free/month\n- **Retention:** 14 days included\n\n### Analytics Engine\n- **What:** High-cardinality event storage and SQL queries\n- **Access:** SQL API, Dashboard (Analytics → Analytics Engine)\n- **Cost:** $0.25/1M writes beyond 10M free/month\n- **Retention:** 90 days (configurable up to 1 year)\n\n### Tail Workers\n- **What:** Workers that receive logs/traces from other Workers\n- **Use Cases:** Log filtering, transformation, external export\n- **Cost:** Standard Workers pricing\n\n### Logpush\n- **What:** Stream logs to external storage (S3, R2, Datadog, etc.)\n- **Access:** Dashboard, API\n- **Cost:** Requires Business/Enterprise plan\n\n## Pricing Summary (2026)\n\n| Feature | Free Tier | Cost Beyond Free Tier | Plan Requirement |\n|---------|-----------|----------------------|------------------|\n| Workers Logs | Unlimited | Free | Any |\n| Workers Traces | 10M spans/month | $0.10/1M spans | Paid Workers (GA: March 1, 2026) |\n| Analytics Engine | 10M writes/month | $0.25/1M writes | Paid Workers |\n| Logpush | N/A | Included in plan | Business/Enterprise |\n\n## In This Reference\n\n- **[configuration.md](configuration.md)** - Setup, deployment, configuration (Logs, Traces, Analytics Engine, Tail Workers, Logpush)\n- **[api.md](api.md)** - API endpoints, methods, interfaces (GraphQL, SQL, bindings, types)\n- **[patterns.md](patterns.md)** - Common patterns, use cases, examples (billing, monitoring, error tracking, exports)\n- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations (common errors, performance gotchas, pricing)\n\n## See Also\n\n- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)\n- [Analytics Engine Docs](https://developers.cloudflare.com/analytics/analytics-engine/)\n- [Workers Traces Docs](https://developers.cloudflare.com/workers/observability/traces/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/observability/api.md",
    "content": "## API Reference\n\n### GraphQL Analytics API\n\n**Endpoint**: `https://api.cloudflare.com/client/v4/graphql`\n\n**Query Workers Metrics**:\n```graphql\nquery {\n  viewer {\n    accounts(filter: { accountTag: $accountId }) {\n      workersInvocationsAdaptive(\n        limit: 100\n        filter: {\n          datetime_geq: \"2025-01-01T00:00:00Z\"\n          datetime_leq: \"2025-01-31T23:59:59Z\"\n          scriptName: \"my-worker\"\n        }\n      ) {\n        sum {\n          requests\n          errors\n          subrequests\n        }\n        quantiles {\n          cpuTimeP50\n          cpuTimeP99\n          wallTimeP50\n          wallTimeP99\n        }\n      }\n    }\n  }\n}\n```\n\n### Analytics Engine SQL API\n\n**Endpoint**: `https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql`\n\n**Authentication**: `Authorization: Bearer <API_TOKEN>` (Account Analytics Read permission)\n\n**Common Queries**:\n\n```sql\n-- List all datasets\nSHOW TABLES;\n\n-- Time-series aggregation (5-minute buckets)\nSELECT\n  intDiv(toUInt32(timestamp), 300) * 300 AS time_bucket,\n  blob1 AS endpoint,\n  SUM(_sample_interval) AS total_requests,\n  AVG(double1) AS avg_response_time_ms\nFROM api_metrics\nWHERE timestamp >= NOW() - INTERVAL '24' HOUR\nGROUP BY time_bucket, endpoint\nORDER BY time_bucket DESC;\n\n-- Top customers by usage\nSELECT\n  index1 AS customer_id,\n  SUM(_sample_interval * double1) AS total_api_calls,\n  AVG(double2) AS avg_response_time_ms\nFROM api_usage\nWHERE timestamp >= NOW() - INTERVAL '7' DAY\nGROUP BY customer_id\nORDER BY total_api_calls DESC\nLIMIT 100;\n\n-- Error rate analysis\nSELECT\n  blob1 AS error_type,\n  COUNT(*) AS occurrences,\n  MAX(timestamp) AS last_seen\nFROM error_tracking\nWHERE timestamp >= NOW() - INTERVAL '1' HOUR\nGROUP BY error_type\nORDER BY occurrences DESC;\n```\n\n### Console Logging API\n\n**Methods**:\n```typescript\n// Standard methods (all appear in Workers Logs)\nconsole.log('info message');\nconsole.info('info message');\nconsole.warn('warning message');\nconsole.error('error message');\nconsole.debug('debug message');\n\n// Structured logging (recommended)\nconsole.log({\n  level: 'info',\n  user_id: '123',\n  action: 'checkout',\n  amount: 99.99,\n  currency: 'USD'\n});\n```\n\n**Log Levels**: All console methods produce logs; use structured fields for filtering:\n```typescript\nconsole.log({ \n  level: 'error', \n  message: 'Payment failed', \n  error_code: 'CARD_DECLINED' \n});\n```\n\n### Analytics Engine Binding Types\n\n```typescript\ninterface AnalyticsEngineDataset {\n  writeDataPoint(event: AnalyticsEngineDataPoint): void;\n}\n\ninterface AnalyticsEngineDataPoint {\n  // Indexed strings (use for filtering/grouping)\n  indexes?: string[];\n  \n  // Non-indexed strings (metadata, IDs, URLs)\n  blobs?: string[];\n  \n  // Numeric values (counts, durations, amounts)\n  doubles?: number[];\n}\n```\n\n**Field Limits**:\n- Max 20 indexes\n- Max 20 blobs\n- Max 20 doubles\n- Max 25 `writeDataPoint` calls per request\n\n### Tail Consumer Event Type\n\n```typescript\ninterface TraceItem {\n  event: TraceEvent;\n  logs: TraceLog[];\n  exceptions: TraceException[];\n  scriptName?: string;\n}\n\ninterface TraceEvent {\n  outcome: 'ok' | 'exception' | 'exceededCpu' | 'exceededMemory' | 'unknown';\n  cpuTime: number; // microseconds\n  wallTime: number; // microseconds\n}\n\ninterface TraceLog {\n  timestamp: number;\n  level: 'log' | 'info' | 'debug' | 'warn' | 'error';\n  message: any; // string or structured object\n}\n\ninterface TraceException {\n  name: string;\n  message: string;\n  timestamp: number;\n}\n```"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/observability/configuration.md",
    "content": "## Configuration Patterns\n\n### Enable Workers Logs\n\n```jsonc\n{\n  \"observability\": {\n    \"enabled\": true,\n    \"head_sampling_rate\": 1  // 100% sampling (default)\n  }\n}\n```\n\n**Best Practice**: Use structured JSON logging for better indexing\n\n```typescript\n// Good - structured logging\nconsole.log({ \n  user_id: 123, \n  action: \"login\", \n  status: \"success\",\n  duration_ms: 45\n});\n\n// Avoid - unstructured string\nconsole.log(\"user_id: 123 logged in successfully in 45ms\");\n```\n\n### Enable Workers Traces\n\n```jsonc\n{\n  \"observability\": {\n    \"traces\": {\n      \"enabled\": true,\n      \"head_sampling_rate\": 0.05  // 5% sampling\n    }\n  }\n}\n```\n\n**Note**: Default sampling is 100%. For high-traffic Workers, use lower sampling (0.01-0.1).\n\n### Configure Analytics Engine\n\n**Bind to Worker**:\n```toml\n# wrangler.toml\nanalytics_engine_datasets = [\n  { binding = \"ANALYTICS\", dataset = \"api_metrics\" }\n]\n```\n\n**Write Data Points**:\n```typescript\nexport interface Env {\n  ANALYTICS: AnalyticsEngineDataset;\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // Track metrics\n    env.ANALYTICS.writeDataPoint({\n      blobs: ['customer_123', 'POST', '/api/v1/users'],\n      doubles: [1, 245.5], // request_count, response_time_ms\n      indexes: ['customer_123'] // for efficient filtering\n    });\n    \n    return new Response('OK');\n  }\n}\n```\n\n### Configure Tail Workers\n\nTail Workers receive logs/traces from other Workers for filtering, transformation, or export.\n\n**Setup**:\n```toml\n# wrangler.toml\nname = \"log-processor\"\nmain = \"src/tail.ts\"\n\n[[tail_consumers]]\nservice = \"my-worker\" # Worker to tail\n```\n\n**Tail Worker Example**:\n```typescript\nexport default {\n  async tail(events: TraceItem[], env: Env, ctx: ExecutionContext) {\n    // Filter errors only\n    const errors = events.filter(event => \n      event.outcome === 'exception' || event.outcome === 'exceededCpu'\n    );\n    \n    if (errors.length > 0) {\n      // Send to external monitoring\n      ctx.waitUntil(\n        fetch('https://monitoring.example.com/errors', {\n          method: 'POST',\n          body: JSON.stringify(errors)\n        })\n      );\n    }\n  }\n}\n```\n\n### Configure Logpush\n\nSend logs to external storage (S3, R2, GCS, Azure, Datadog, etc.). Requires Business/Enterprise plan.\n\n**Via Dashboard**:\n1. Navigate to Analytics → Logs → Logpush\n2. Select destination type\n3. Provide credentials and bucket/endpoint\n4. Choose dataset (e.g., Workers Trace Events)\n5. Configure filters and fields\n\n**Via API**:\n```bash\ncurl -X POST \"https://api.cloudflare.com/client/v4/accounts/{account_id}/logpush/jobs\" \\\n  -H \"Authorization: Bearer <API_TOKEN>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"workers-logs-to-s3\",\n    \"destination_conf\": \"s3://my-bucket/logs?region=us-east-1\",\n    \"dataset\": \"workers_trace_events\",\n    \"enabled\": true,\n    \"frequency\": \"high\",\n    \"filter\": \"{\\\"where\\\":{\\\"and\\\":[{\\\"key\\\":\\\"ScriptName\\\",\\\"operator\\\":\\\"eq\\\",\\\"value\\\":\\\"my-worker\\\"}]}}\"\n  }'\n```\n\n### Environment-Specific Configuration\n\n**Development** (verbose logs, full sampling):\n```jsonc\n// wrangler.dev.jsonc\n{\n  \"observability\": {\n    \"enabled\": true,\n    \"head_sampling_rate\": 1.0,\n    \"traces\": {\n      \"enabled\": true\n    }\n  }\n}\n```\n\n**Production** (reduced sampling, structured logs):\n```jsonc\n// wrangler.prod.jsonc\n{\n  \"observability\": {\n    \"enabled\": true,\n    \"head_sampling_rate\": 0.1, // 10% sampling\n    \"traces\": {\n      \"enabled\": true\n    }\n  }\n}\n```\n\nDeploy with env-specific config:\n```bash\nwrangler deploy --config wrangler.prod.jsonc --env production\n```"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/observability/gotchas.md",
    "content": "## Common Errors\n\n### \"Logs not appearing\"\n\n**Cause:** Observability disabled, Worker not redeployed, no traffic, low sampling rate, or log size exceeds 256 KB\n**Solution:** \n```bash\n# Verify config\ncat wrangler.jsonc | jq '.observability'\n\n# Check deployment\nwrangler deployments list <WORKER_NAME>\n\n# Test with curl\ncurl https://your-worker.workers.dev\n```\nEnsure `observability.enabled = true`, redeploy Worker, check `head_sampling_rate`, verify traffic\n\n### \"Traces not being captured\"\n\n**Cause:** Traces not enabled, incorrect sampling rate, Worker not redeployed, or destination unavailable\n**Solution:**\n```jsonc\n// Temporarily set to 100% sampling for debugging\n{\n  \"observability\": {\n    \"enabled\": true,\n    \"head_sampling_rate\": 1.0,\n    \"traces\": {\n      \"enabled\": true\n    }\n  }\n}\n```\nEnsure `observability.traces.enabled = true`, set `head_sampling_rate` to 1.0 for testing, redeploy, check destination status\n\n## Limits\n\n| Resource/Limit | Value | Notes |\n|----------------|-------|-------|\n| Max log size | 256 KB | Logs exceeding this are truncated |\n| Default sampling rate | 1.0 (100%) | Reduce for high-traffic Workers |\n| Max destinations | Varies by plan | Check dashboard |\n| Trace context propagation | 100 spans max | Deep call chains may lose spans |\n| Analytics Engine write rate | 25 writes/request | Excess writes dropped silently |\n\n## Performance Gotchas\n\n### Spectre Mitigation Timing\n\n**Problem:** `Date.now()` and `performance.now()` have reduced precision (coarsened to 100μs)\n**Cause:** Spectre vulnerability mitigation in V8\n**Solution:** Accept reduced precision or use Workers Traces for accurate timing\n```typescript\n// Date.now() is coarsened - trace spans are accurate\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n    // For user-facing timing, Date.now() is fine\n    const start = Date.now();\n    const response = await processRequest(request);\n    const duration = Date.now() - start;\n    \n    // For detailed performance analysis, use Workers Traces instead\n    return response;\n  }\n}\n```\n\n### Analytics Engine _sample_interval Aggregation\n\n**Problem:** Queries return incorrect totals when not multiplying by `_sample_interval`\n**Cause:** Analytics Engine stores sampled data points, each representing multiple events\n**Solution:** Always multiply counts/sums by `_sample_interval` in aggregations\n```sql\n-- WRONG: Undercounts actual events\nSELECT blob1 AS customer_id, COUNT(*) AS total_calls\nFROM api_usage GROUP BY customer_id;\n\n-- CORRECT: Accounts for sampling\nSELECT blob1 AS customer_id, SUM(_sample_interval) AS total_calls\nFROM api_usage GROUP BY customer_id;\n```\n\n### Trace Context Propagation Limits\n\n**Problem:** Deep call chains lose trace context after 100 spans\n**Cause:** Cloudflare limits trace depth to prevent performance impact\n**Solution:** Design for flatter architectures or use custom correlation IDs for deep chains\n```typescript\n// For deep call chains, add custom correlation ID\nconst correlationId = crypto.randomUUID();\nconsole.log({ correlationId, event: 'request_start' });\n\n// Pass correlationId through headers to downstream services\nawait fetch('https://api.example.com', {\n  headers: { 'X-Correlation-ID': correlationId }\n});\n```\n\n## Pricing (2026)\n\n### Workers Traces\n- **GA Pricing (starts March 1, 2026):**\n  - $0.10 per 1M trace spans captured\n  - Retention: 14 days included\n- **Free tier:** 10M trace spans/month\n- **Note:** Beta usage (before March 1, 2026) is free\n\n### Workers Logs\n- **Included:** Free for all Workers\n- **Logpush:** Requires Business/Enterprise plan\n\n### Analytics Engine\n- **Included:** 10M writes/month on Paid Workers plan\n- **Additional:** $0.25 per 1M writes beyond included quota\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/observability/patterns.md",
    "content": "# Observability Patterns\n\n## Usage-Based Billing\n\n```typescript\nenv.ANALYTICS.writeDataPoint({\n  blobs: [customerId, request.url, request.method],\n  doubles: [1], // request_count\n  indexes: [customerId]\n});\n```\n\n```sql\nSELECT blob1 AS customer_id, SUM(_sample_interval * double1) AS total_calls\nFROM api_usage WHERE timestamp >= DATE_TRUNC('month', NOW())\nGROUP BY customer_id\n```\n\n## Performance Monitoring\n\n```typescript\nconst start = Date.now();\nconst response = await fetch(url);\nenv.ANALYTICS.writeDataPoint({\n  blobs: [url, response.status.toString()],\n  doubles: [Date.now() - start, response.status]\n});\n```\n\n```sql\nSELECT blob1 AS url, AVG(double1) AS avg_ms, percentile(double1, 0.95) AS p95_ms\nFROM fetch_metrics WHERE timestamp >= NOW() - INTERVAL '1' HOUR\nGROUP BY url\n```\n\n## Error Tracking\n\n```typescript\nenv.ANALYTICS.writeDataPoint({\n  blobs: [error.name, request.url, request.method],\n  doubles: [1],\n  indexes: [error.name]\n});\n```\n\n## Multi-Tenant Tracking\n\n```typescript\nenv.ANALYTICS.writeDataPoint({\n  indexes: [tenantId], // efficient filtering\n  blobs: [tenantId, url.pathname, method, status],\n  doubles: [1, duration, bytesSize]\n});\n```\n\n## Tail Worker Log Filtering\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    const critical = events.filter(e => \n      e.exceptions.length > 0 || e.event.wallTime > 1000000\n    );\n    if (critical.length === 0) return;\n    \n    ctx.waitUntil(\n      fetch('https://logging.example.com/ingest', {\n        method: 'POST',\n        headers: { 'Authorization': `Bearer ${env.API_KEY}` },\n        body: JSON.stringify(critical.map(e => ({\n          outcome: e.event.outcome,\n          cpu_ms: e.event.cpuTime / 1000,\n          errors: e.exceptions\n        })))\n      })\n    );\n  }\n};\n```\n\n## OpenTelemetry Export\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    const otelSpans = events.map(e => ({\n      traceId: generateId(32),\n      spanId: generateId(16),\n      name: e.scriptName || 'worker.request',\n      attributes: [\n        { key: 'worker.outcome', value: { stringValue: e.event.outcome } },\n        { key: 'worker.cpu_time_us', value: { intValue: String(e.event.cpuTime) } }\n      ]\n    }));\n    \n    ctx.waitUntil(\n      fetch('https://api.honeycomb.io/v1/traces', {\n        method: 'POST',\n        headers: { 'X-Honeycomb-Team': env.HONEYCOMB_KEY },\n        body: JSON.stringify({ resourceSpans: [{ scopeSpans: [{ spans: otelSpans }] }] })\n      })\n    );\n  }\n};\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pages/README.md",
    "content": "# Cloudflare Pages\n\nJAMstack platform for full-stack apps on Cloudflare's global network.\n\n## Key Features\n\n- **Git-based deploys**: Auto-deploy from GitHub/GitLab\n- **Preview deployments**: Unique URL per branch/PR\n- **Pages Functions**: File-based serverless routing (Workers runtime)\n- **Static + dynamic**: Smart asset caching + edge compute\n- **Smart Placement**: Automatic function optimization based on traffic patterns\n- **Framework optimized**: SvelteKit, Astro, Nuxt, Qwik, Solid Start\n\n## Deployment Methods\n\n### 1. Git Integration (Production)\nDashboard → Workers & Pages → Create → Connect to Git → Configure build\n\n### 2. Direct Upload\n```bash\nnpx wrangler pages deploy ./dist --project-name=my-project\nnpx wrangler pages deploy ./dist --project-name=my-project --branch=staging\n```\n\n### 3. C3 CLI\n```bash\nnpm create cloudflare@latest my-app\n# Select framework → auto-setup + deploy\n```\n\n## vs Workers\n\n- **Pages**: Static sites, JAMstack, frameworks, git workflow, file-based routing\n- **Workers**: Pure APIs, complex routing, WebSockets, scheduled tasks, email handlers\n- **Combine**: Pages Functions use Workers runtime, can bind to Workers\n\n## Quick Start\n\n```bash\n# Create\nnpm create cloudflare@latest\n\n# Local dev\nnpx wrangler pages dev ./dist\n\n# Deploy\nnpx wrangler pages deploy ./dist --project-name=my-project\n\n# Types\nnpx wrangler types --path='./functions/types.d.ts'\n\n# Secrets\necho \"value\" | npx wrangler pages secret put KEY --project-name=my-project\n\n# Logs\nnpx wrangler pages deployment tail --project-name=my-project\n```\n\n## Resources\n\n- [Pages Docs](https://developers.cloudflare.com/pages/)\n- [Functions API](https://developers.cloudflare.com/pages/functions/api-reference/)\n- [Framework Guides](https://developers.cloudflare.com/pages/framework-guides/)\n- [Discord #functions](https://discord.com/channels/595317990191398933/910978223968518144)\n\n## Reading Order\n\n**New to Pages?** Start here:\n1. README.md (you are here) - Overview & quick start\n2. [configuration.md](./configuration.md) - Project setup, wrangler.jsonc, bindings\n3. [api.md](./api.md) - Functions API, routing, context\n4. [patterns.md](./patterns.md) - Common implementations\n5. [gotchas.md](./gotchas.md) - Troubleshooting & pitfalls\n\n**Quick reference?** Jump to relevant file above.\n\n## In This Reference\n\n- [configuration.md](./configuration.md) - wrangler.jsonc, build, env vars, Smart Placement\n- [api.md](./api.md) - Functions API, bindings, context, advanced mode\n- [patterns.md](./patterns.md) - Full-stack patterns, framework integration\n- [gotchas.md](./gotchas.md) - Build issues, limits, debugging, framework warnings\n\n## See Also\n\n- [pages-functions](../pages-functions/) - File-based routing, middleware\n- [d1](../d1/) - SQL database for Pages Functions\n- [kv](../kv/) - Key-value storage for caching/state\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pages/api.md",
    "content": "# Functions API\n\n## File-Based Routing\n\n```\n/functions/index.ts              → example.com/\n/functions/api/users.ts          → example.com/api/users\n/functions/api/users/[id].ts     → example.com/api/users/:id\n/functions/api/users/[[path]].ts → example.com/api/users/* (catchall)\n/functions/_middleware.ts        → Runs before all routes\n```\n\n**Rules**: `[param]` = single segment, `[[param]]` = multi-segment catchall, more specific wins.\n\n## Request Handlers\n\n```typescript\nimport type { PagesFunction } from '@cloudflare/workers-types';\n\ninterface Env {\n  DB: D1Database;\n  KV: KVNamespace;\n}\n\n// All methods\nexport const onRequest: PagesFunction<Env> = async (context) => {\n  return new Response('All methods');\n};\n\n// Method-specific\nexport const onRequestGet: PagesFunction<Env> = async (context) => {\n  const { request, env, params, data } = context;\n  \n  const user = await env.DB.prepare(\n    'SELECT * FROM users WHERE id = ?'\n  ).bind(params.id).first();\n  \n  return Response.json(user);\n};\n\nexport const onRequestPost: PagesFunction<Env> = async (context) => {\n  const body = await context.request.json();\n  return Response.json({ success: true });\n};\n\n// Also: onRequestPut, onRequestPatch, onRequestDelete, onRequestHead, onRequestOptions\n```\n\n## Context Object\n\n```typescript\ninterface EventContext<Env, Params, Data> {\n  request: Request;              // HTTP request\n  env: Env;                      // Bindings (KV, D1, R2, etc.)\n  params: Params;                // Route parameters\n  data: Data;                    // Middleware-shared data\n  waitUntil: (promise: Promise<any>) => void;  // Background tasks\n  next: () => Promise<Response>; // Next handler\n  passThroughOnException: () => void;  // Error fallback (not in advanced mode)\n}\n```\n\n## Dynamic Routes\n\n```typescript\n// Single segment: functions/users/[id].ts\nexport const onRequestGet: PagesFunction = async ({ params }) => {\n  // /users/123 → params.id = \"123\"\n  return Response.json({ userId: params.id });\n};\n\n// Multi-segment: functions/files/[[path]].ts\nexport const onRequestGet: PagesFunction = async ({ params }) => {\n  // /files/docs/api/v1.md → params.path = [\"docs\", \"api\", \"v1.md\"]\n  const filePath = (params.path as string[]).join('/');\n  return new Response(filePath);\n};\n```\n\n## Middleware\n\n```typescript\n// functions/_middleware.ts\n// Single\nexport const onRequest: PagesFunction = async (context) => {\n  const response = await context.next();\n  response.headers.set('X-Custom-Header', 'value');\n  return response;\n};\n\n// Chained (runs in order)\nconst errorHandler: PagesFunction = async (context) => {\n  try {\n    return await context.next();\n  } catch (err) {\n    return new Response(err.message, { status: 500 });\n  }\n};\n\nconst auth: PagesFunction = async (context) => {\n  const token = context.request.headers.get('Authorization');\n  if (!token) return new Response('Unauthorized', { status: 401 });\n  context.data.userId = await verifyToken(token);\n  return context.next();\n};\n\nexport const onRequest = [errorHandler, auth];\n```\n\n**Scope**: `functions/_middleware.ts` → all; `functions/api/_middleware.ts` → `/api/*` only\n\n## Bindings Usage\n\n```typescript\nexport const onRequestGet: PagesFunction<Env> = async ({ env }) => {\n  // KV\n  const cached = await env.KV.get('key', 'json');\n  await env.KV.put('key', JSON.stringify({data: 'value'}), {expirationTtl: 3600});\n  \n  // D1\n  const result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();\n  \n  // R2, Queue, AI - see respective reference docs\n  \n  return Response.json({success: true});\n};\n```\n\n## Advanced Mode\n\nFull Workers API, bypasses file-based routing:\n\n```javascript\n// functions/_worker.js\nexport default {\n  async fetch(request, env, ctx) {\n    const url = new URL(request.url);\n    \n    // Custom routing\n    if (url.pathname.startsWith('/api/')) {\n      return new Response('API response');\n    }\n    \n    // REQUIRED: Serve static assets\n    return env.ASSETS.fetch(request);\n  }\n};\n```\n\n**When to use**: WebSockets, complex routing, scheduled handlers, email handlers.\n\n## Smart Placement\n\nAutomatically optimizes function execution location based on traffic patterns.\n\n**Configuration** (in wrangler.jsonc):\n```jsonc\n{\n  \"placement\": {\n    \"mode\": \"smart\"  // Enables optimization (default: off)\n  }\n}\n```\n\n**How it works**: Analyzes traffic patterns over time and places functions closer to users or data sources (e.g., D1 databases). Requires no code changes.\n\n**Trade-offs**: Initial requests may see slightly higher latency during learning period (hours-days). Performance improves as system optimizes.\n\n**When to use**: Global apps with centralized databases or geographically concentrated traffic sources.\n\n## getRequestContext (Framework SSR)\n\nAccess bindings in framework code:\n\n```typescript\n// SvelteKit\nimport type { RequestEvent } from '@sveltejs/kit';\nexport async function load({ platform }: RequestEvent) {\n  const data = await platform.env.DB.prepare('SELECT * FROM users').all();\n  return { users: data.results };\n}\n\n// Astro\nconst { DB } = Astro.locals.runtime.env;\nconst data = await DB.prepare('SELECT * FROM users').all();\n\n// Solid Start (server function)\nimport { getRequestEvent } from 'solid-js/web';\nconst event = getRequestEvent();\nconst data = await event.locals.runtime.env.DB.prepare('SELECT * FROM users').all();\n```\n\n**✅ Supported adapters** (2026):\n- **SvelteKit**: `@sveltejs/adapter-cloudflare`\n- **Astro**: Built-in Cloudflare adapter\n- **Nuxt**: Set `nitro.preset: 'cloudflare-pages'` in `nuxt.config.ts`\n- **Qwik**: Built-in Cloudflare adapter\n- **Solid Start**: `@solidjs/start-cloudflare-pages`\n\n**❌ Deprecated/Unsupported**:\n- **Next.js**: Official adapter (`@cloudflare/next-on-pages`) deprecated. Use Vercel or self-host on Workers.\n- **Remix**: Official adapter (`@remix-run/cloudflare-pages`) deprecated. Migrate to supported frameworks.\n\nSee [gotchas.md](./gotchas.md#framework-specific) for migration guidance.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pages/configuration.md",
    "content": "# Configuration\n\n## wrangler.jsonc\n\n```jsonc\n{\n  \"name\": \"my-pages-project\",\n  \"pages_build_output_dir\": \"./dist\",\n  \"compatibility_date\": \"2026-01-01\", // Use current date for new projects\n  \"compatibility_flags\": [\"nodejs_compat\"],\n  \"placement\": {\n    \"mode\": \"smart\"  // Optional: Enable Smart Placement\n  },\n  \"kv_namespaces\": [{\"binding\": \"KV\", \"id\": \"abcd1234...\"}],\n  \"d1_databases\": [{\"binding\": \"DB\", \"database_id\": \"xxxx-xxxx\", \"database_name\": \"production-db\"}],\n  \"r2_buckets\": [{\"binding\": \"BUCKET\", \"bucket_name\": \"my-bucket\"}],\n  \"durable_objects\": {\"bindings\": [{\"name\": \"COUNTER\", \"class_name\": \"Counter\", \"script_name\": \"counter-worker\"}]},\n  \"services\": [{\"binding\": \"API\", \"service\": \"api-worker\"}],\n  \"queues\": {\"producers\": [{\"binding\": \"QUEUE\", \"queue\": \"my-queue\"}]},\n  \"vectorize\": [{\"binding\": \"VECTORIZE\", \"index_name\": \"my-index\"}],\n  \"ai\": {\"binding\": \"AI\"},\n  \"analytics_engine_datasets\": [{\"binding\": \"ANALYTICS\"}],\n  \"vars\": {\"API_URL\": \"https://api.example.com\", \"ENVIRONMENT\": \"production\"},\n  \"env\": {\n    \"preview\": {\n      \"vars\": {\"API_URL\": \"https://staging-api.example.com\"},\n      \"kv_namespaces\": [{\"binding\": \"KV\", \"id\": \"preview-namespace-id\"}]\n    }\n  }\n}\n```\n\n## Build Config\n\n**Git deployment**: Dashboard → Project → Settings → Build settings  \nSet build command, output dir, env vars. Framework auto-detection configures automatically.\n\n## Environment Variables\n\n### Local (.dev.vars)\n```bash\n# .dev.vars (never commit)\nSECRET_KEY=\"local-secret-key\"\nAPI_TOKEN=\"dev-token-123\"\n```\n\n### Production\n```bash\necho \"secret-value\" | npx wrangler pages secret put SECRET_KEY --project-name=my-project\nnpx wrangler pages secret list --project-name=my-project\nnpx wrangler pages secret delete SECRET_KEY --project-name=my-project\n```\n\nAccess: `env.SECRET_KEY`\n\n## Static Config Files\n\n### _redirects\nPlace in build output (e.g., `dist/_redirects`):\n\n```txt\n/old-page /new-page 301          # 301 redirect\n/blog/* /news/:splat 301         # Splat wildcard\n/users/:id /members/:id 301      # Placeholders\n/api/* /api-v2/:splat 200        # Proxy (no redirect)\n```\n\n**Limits**: 2,100 total (2,000 static + 100 dynamic), 1,000 char/line  \n**Note**: Functions take precedence\n\n### _headers\n```txt\n/secure/*\n  X-Frame-Options: DENY\n  X-Content-Type-Options: nosniff\n\n/api/*\n  Access-Control-Allow-Origin: *\n\n/static/*\n  Cache-Control: public, max-age=31536000, immutable\n```\n\n**Limits**: 100 rules, 2,000 char/line  \n**Note**: Only static assets; Functions set headers in Response\n\n### _routes.json\nControls which requests invoke Functions (auto-generated for most frameworks):\n\n```json\n{\n  \"version\": 1,\n  \"include\": [\"/*\"],\n  \"exclude\": [\"/build/*\", \"/static/*\", \"/assets/*\", \"/*.{ico,png,jpg,css,js}\"]\n}\n```\n\n**Purpose**: Functions are metered; static requests are free. `exclude` takes precedence. Max 100 rules, 100 char/rule.\n\n## TypeScript\n\n```bash\nnpx wrangler types --path='./functions/types.d.ts'\n```\n\nPoint `types` in `functions/tsconfig.json` to generated file.\n\n## Smart Placement\n\nAutomatically optimizes function execution location based on request patterns.\n\n```jsonc\n{\n  \"placement\": {\n    \"mode\": \"smart\"  // Enable optimization (default: off)\n  }\n}\n```\n\n**How it works**: System analyzes traffic over hours/days and places function execution closer to:\n- User clusters (e.g., regional traffic)\n- Data sources (e.g., D1 database primary location)\n\n**Benefits**: \n- Lower latency for read-heavy apps with centralized databases\n- Better performance for apps with regional traffic patterns\n\n**Trade-offs**:\n- Initial learning period: First requests may be slower while system optimizes\n- Optimization time: Performance improves over 24-48 hours\n\n**When to enable**: Global apps with D1/Durable Objects in specific regions, or apps with concentrated geographic traffic.\n\n**When to skip**: Evenly distributed global traffic with no data locality constraints.\n\n## Remote Bindings (Local Dev)\n\nConnect local dev server to production bindings instead of local mocks:\n\n```bash\n# All bindings remote\nnpx wrangler pages dev ./dist --remote\n\n# Specific bindings remote (others local)\nnpx wrangler pages dev ./dist --remote --kv=KV --d1=DB\n```\n\n**Use cases**:\n- Test against production data (read-only operations)\n- Debug binding-specific behavior\n- Validate changes before deployment\n\n**⚠️ Warning**: \n- Writes affect **real production data**\n- Use only for read-heavy debugging or with non-production accounts\n- Consider creating separate preview environments instead\n\n**Requirements**: Must be logged in (`npx wrangler login`) with access to bindings.\n\n## Local Dev\n\n```bash\n# Basic\nnpx wrangler pages dev ./dist\n\n# With bindings\nnpx wrangler pages dev ./dist --kv KV --d1 DB=local-db-id\n\n# Remote bindings (production data)\nnpx wrangler pages dev ./dist --remote\n\n# Persistence\nnpx wrangler pages dev ./dist --persist-to=./.wrangler/state/v3\n\n# Proxy mode (SSR frameworks)\nnpx wrangler pages dev -- npm run dev\n```\n\n## Limits (as of Jan 2026)\n\n| Resource | Free | Paid |\n|----------|------|------|\n| **Functions Requests** | 100k/day | Unlimited (metered) |\n| **Function CPU Time** | 10ms/req | 30ms/req (Workers Paid) |\n| **Function Memory** | 128MB | 128MB |\n| **Script Size** | 1MB compressed | 10MB compressed |\n| **Deployments** | 500/month | 5,000/month |\n| **Files per Deploy** | 20,000 | 20,000 |\n| **File Size** | 25MB | 25MB |\n| **Build Time** | 20min | 20min |\n| **Redirects** | 2,100 (2k static + 100 dynamic) | Same |\n| **Header Rules** | 100 | 100 |\n| **Route Rules** | 100 | 100 |\n| **Subrequests** | 50/request | 1,000/request (Workers Paid) |\n\n**Notes**:\n- Functions use Workers runtime; Workers Paid plan increases limits\n- Free plan sufficient for most projects\n- Static requests always free (not counted toward limits)\n\n[Full limits](https://developers.cloudflare.com/pages/platform/limits/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pages/gotchas.md",
    "content": "# Gotchas\n\n## Functions Not Running\n\n**Problem**: Function endpoints return 404 or don't execute  \n**Causes**: `_routes.json` excludes path; wrong file extension (`.jsx`/`.tsx`); Functions dir not at output root  \n**Solution**: Check `_routes.json`, rename to `.ts`/`.js`, verify build output structure\n\n## 404 on Static Assets\n\n**Problem**: Static files not serving  \n**Causes**: Build output dir misconfigured; Functions catching requests; Advanced mode missing `env.ASSETS.fetch()`  \n**Solution**: Verify output dir, add exclusions to `_routes.json`, call `env.ASSETS.fetch()` in `_worker.js`\n\n## Bindings Not Working\n\n**Problem**: `env.BINDING` undefined or errors  \n**Causes**: wrangler.jsonc syntax error; wrong binding IDs; missing `.dev.vars`; out-of-sync types  \n**Solution**: Validate config, verify IDs, create `.dev.vars`, run `npx wrangler types`\n\n## Build Failures\n\n**Problem**: Deployment fails during build  \n**Causes**: Wrong build command/output dir; Node version incompatibility; missing env vars; 20min timeout; OOM  \n**Solution**: Check Dashboard → Deployments → Build log; verify settings; add `.nvmrc`; optimize build\n\n## Middleware Not Running\n\n**Problem**: Middleware doesn't execute  \n**Causes**: Wrong filename (not `_middleware.ts`); missing `onRequest` export; didn't call `next()`  \n**Solution**: Rename file with underscore prefix; export handler; call `next()` or return Response\n\n## Headers/Redirects Not Working\n\n**Problem**: `_headers` or `_redirects` not applying  \n**Causes**: Only work for static assets; Functions override; syntax errors; exceeded limits  \n**Solution**: Set headers in Response object for Functions; verify syntax; check limits (100 headers, 2,100 redirects)\n\n## TypeScript Errors\n\n**Problem**: Type errors in Functions code  \n**Causes**: Types not generated; Env interface doesn't match wrangler.jsonc  \n**Solution**: Run `npx wrangler types --path='./functions/types.d.ts'`; update Env interface\n\n## Local Dev Issues\n\n**Problem**: Dev server errors or bindings don't work  \n**Causes**: Port conflict; bindings not passed; local vs HTTPS differences  \n**Solution**: Use `--port=3000`; pass bindings via CLI or wrangler.jsonc; account for HTTP/HTTPS differences\n\n## Performance Issues\n\n**Problem**: Slow responses or CPU limit errors  \n**Causes**: Functions invoked for static assets; cold starts; 10ms CPU limit; large bundle  \n**Solution**: Exclude static via `_routes.json`; optimize hot paths; keep bundle < 1MB\n\n## Framework-Specific\n\n### ⚠️ Deprecated Frameworks\n\n**Next.js**: Official adapter (`@cloudflare/next-on-pages`) **deprecated** and unmaintained.\n- **Problem**: No updates since 2024; incompatible with Next.js 15+; missing App Router features\n- **Cause**: Cloudflare discontinued official support; community fork exists but limited\n- **Solutions**:\n  1. **Recommended**: Use Vercel (official Next.js host)\n  2. **Advanced**: Self-host on Workers using custom adapter (complex, unsupported)\n  3. **Migration**: Switch to SvelteKit/Nuxt (similar DX, full Pages support)\n\n**Remix**: Official adapter (`@remix-run/cloudflare-pages`) **deprecated**.\n- **Problem**: No maintenance from Remix team; compatibility issues with Remix v2+\n- **Cause**: Remix team deprecated all framework adapters\n- **Solutions**:\n  1. **Recommended**: Migrate to SvelteKit (similar file-based routing, better DX)\n  2. **Alternative**: Use Astro (static-first with optional SSR)\n  3. **Workaround**: Continue using deprecated adapter (no future support)\n\n### ✅ Supported Frameworks\n\n**SvelteKit**:\n- Use `@sveltejs/adapter-cloudflare`\n- Access bindings via `platform.env` in server load functions\n- Set `platform: 'cloudflare'` in `svelte.config.js`\n\n**Astro**:\n- Built-in Cloudflare adapter\n- Access bindings via `Astro.locals.runtime.env`\n\n**Nuxt**:\n- Set `nitro.preset: 'cloudflare-pages'` in `nuxt.config.ts`\n- Access bindings via `event.context.cloudflare.env`\n\n**Qwik, Solid Start**:\n- Built-in or official Cloudflare adapters available\n- Check respective framework docs for binding access\n\n## Debugging\n\n```typescript\n// Log request details\nconsole.log('Request:', { method: request.method, url: request.url });\nconsole.log('Env:', Object.keys(env));\nconsole.log('Params:', params);\n```\n\n**View logs**: `npx wrangler pages deployment tail --project-name=my-project`\n\n## Smart Placement Issues\n\n### Increased Cold Start Latency\n\n**Problem**: First requests slower after enabling Smart Placement  \n**Cause**: Initial optimization period while system learns traffic patterns  \n**Solution**: Expected behavior during first 24-48 hours; monitor latency trends over time\n\n### Inconsistent Response Times\n\n**Problem**: Latency varies significantly across requests during initial deployment  \n**Cause**: Smart Placement testing different execution locations to find optimal placement  \n**Solution**: Normal during learning phase; stabilizes after traffic patterns emerge (1-2 days)\n\n### No Performance Improvement\n\n**Problem**: Smart Placement enabled but no latency reduction observed  \n**Cause**: Traffic evenly distributed globally, or no data locality constraints  \n**Solution**: Smart Placement most effective with centralized data (D1/DO) or regional traffic; disable if no benefit\n\n## Remote Bindings Issues\n\n### Accidentally Modified Production Data\n\n**Problem**: Local dev with `--remote` altered production database/KV  \n**Cause**: Remote bindings connect directly to production resources; writes are real  \n**Solution**: \n- Use `--remote` only for read-heavy debugging\n- Create separate preview environments for testing\n- Never use `--remote` for write operations during development\n\n### Remote Binding Auth Errors\n\n**Problem**: `npx wrangler pages dev --remote` fails with \"Unauthorized\" or auth error  \n**Cause**: Not logged in, session expired, or insufficient account permissions  \n**Solution**: \n1. Run `npx wrangler login` to re-authenticate\n2. Verify account has access to project and bindings\n3. Check binding IDs match production configuration\n\n### Slow Local Dev with Remote Bindings\n\n**Problem**: Local dev server slow when using `--remote`  \n**Cause**: Every request makes network calls to production bindings  \n**Solution**: Use local bindings for development; reserve `--remote` for final validation\n\n## Common Errors\n\n### \"Module not found\"\n**Cause**: Dependencies not bundled or build output incorrect  \n**Solution**: Check build output directory, ensure dependencies bundled\n\n### \"Binding not found\"\n**Cause**: Binding not configured or types out of sync  \n**Solution**: Verify wrangler.jsonc, run `npx wrangler types`\n\n### \"Request exceeded CPU limit\"\n**Cause**: Code execution too slow or heavy compute  \n**Solution**: Optimize hot paths, upgrade to Workers Paid\n\n### \"Script too large\"\n**Cause**: Bundle size exceeds limit  \n**Solution**: Tree-shake, use dynamic imports, code-split\n\n### \"Too many subrequests\"\n**Cause**: Exceeded 50 subrequest limit  \n**Solution**: Batch or reduce fetch calls\n\n### \"KV key not found\"\n**Cause**: Key doesn't exist or wrong namespace  \n**Solution**: Check namespace matches environment\n\n### \"D1 error\"\n**Cause**: Wrong database_id or missing migrations  \n**Solution**: Verify config, run `wrangler d1 migrations list`\n\n## Limits Reference (Jan 2026)\n\n| Resource | Free | Paid |\n|----------|------|------|\n| Functions Requests | 100k/day | Unlimited |\n| CPU Time | 10ms/req | 30ms/req |\n| Memory | 128MB | 128MB |\n| Script Size | 1MB | 10MB |\n| Subrequests | 50/req | 1,000/req |\n| Deployments | 500/month | 5,000/month |\n\n**Tip**: Hitting CPU limit? Optimize hot paths or upgrade to Workers Paid plan.\n\n[Full limits](https://developers.cloudflare.com/pages/platform/limits/)\n\n## Getting Help\n\n1. Check [Pages Docs](https://developers.cloudflare.com/pages/)\n2. Search [Discord #functions](https://discord.com/channels/595317990191398933/910978223968518144)\n3. Review [Workers Examples](https://developers.cloudflare.com/workers/examples/)\n4. Check framework-specific docs/adapters\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pages/patterns.md",
    "content": "# Patterns\n\n## API Routes\n\n```typescript\n// functions/api/todos/[id].ts\nexport const onRequestGet: PagesFunction<Env> = async ({ env, params }) => {\n  const todo = await env.DB.prepare('SELECT * FROM todos WHERE id = ?').bind(params.id).first();\n  if (!todo) return new Response('Not found', { status: 404 });\n  return Response.json(todo);\n};\n\nexport const onRequestPut: PagesFunction<Env> = async ({ env, params, request }) => {\n  const body = await request.json();\n  await env.DB.prepare('UPDATE todos SET title = ?, completed = ? WHERE id = ?')\n    .bind(body.title, body.completed, params.id).run();\n  return Response.json({ success: true });\n};\n// Also: onRequestDelete, onRequestPost\n```\n\n## Auth Middleware\n\n```typescript\n// functions/_middleware.ts\nconst auth: PagesFunction<Env> = async (context) => {\n  if (context.request.url.includes('/public/')) return context.next();\n  const authHeader = context.request.headers.get('Authorization');\n  if (!authHeader?.startsWith('Bearer ')) {\n    return new Response('Unauthorized', { status: 401 });\n  }\n  \n  try {\n    const payload = await verifyJWT(authHeader.substring(7), context.env.JWT_SECRET);\n    context.data.user = payload;\n    return context.next();\n  } catch (err) {\n    return new Response('Invalid token', { status: 401 });\n  }\n};\nexport const onRequest = [auth];\n```\n\n## CORS\n\n```typescript\n// functions/api/_middleware.ts\nconst corsHeaders = {\n  'Access-Control-Allow-Origin': '*',\n  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',\n  'Access-Control-Allow-Headers': 'Content-Type, Authorization'\n};\n\nexport const onRequest: PagesFunction = async (context) => {\n  if (context.request.method === 'OPTIONS') {\n    return new Response(null, {headers: corsHeaders});\n  }\n  const response = await context.next();\n  Object.entries(corsHeaders).forEach(([k, v]) => response.headers.set(k, v));\n  return response;\n};\n```\n\n## Form Handling\n\n```typescript\n// functions/api/contact.ts\nexport const onRequestPost: PagesFunction<Env> = async ({ request, env }) => {\n  const formData = await request.formData();\n  await env.QUEUE.send({name: formData.get('name'), email: formData.get('email')});\n  return new Response('<h1>Thanks!</h1>', { headers: { 'Content-Type': 'text/html' } });\n};\n```\n\n## Background Tasks\n\n```typescript\nexport const onRequestPost: PagesFunction = async ({ request, waitUntil }) => {\n  const data = await request.json();\n  waitUntil(fetch('https://api.example.com/webhook', {\n    method: 'POST', body: JSON.stringify(data)\n  }));\n  return Response.json({ queued: true });\n};\n```\n\n## Error Handling\n\n```typescript\n// functions/_middleware.ts\nconst errorHandler: PagesFunction = async (context) => {\n  try {\n    return await context.next();\n  } catch (error) {\n    console.error('Error:', error);\n    if (context.request.url.includes('/api/')) {\n      return Response.json({ error: error.message }, { status: 500 });\n    }\n    return new Response(`<h1>Error</h1><p>${error.message}</p>`, { \n      status: 500, headers: { 'Content-Type': 'text/html' } \n    });\n  }\n};\nexport const onRequest = [errorHandler];\n```\n\n## Caching\n\n```typescript\n// functions/api/data.ts\nexport const onRequestGet: PagesFunction<Env> = async ({ env, request }) => {\n  const cacheKey = `data:${new URL(request.url).pathname}`;\n  const cached = await env.KV.get(cacheKey, 'json');\n  if (cached) return Response.json(cached, { headers: { 'X-Cache': 'HIT' } });\n  \n  const data = await env.DB.prepare('SELECT * FROM data').first();\n  await env.KV.put(cacheKey, JSON.stringify(data), {expirationTtl: 3600});\n  return Response.json(data, {headers: {'X-Cache': 'MISS'}});\n};\n```\n\n## Smart Placement for Database Apps\n\nEnable Smart Placement for apps with D1 or centralized data sources:\n\n```jsonc\n// wrangler.jsonc\n{\n  \"name\": \"global-app\",\n  \"placement\": {\n    \"mode\": \"smart\"\n  },\n  \"d1_databases\": [{\n    \"binding\": \"DB\",\n    \"database_id\": \"your-db-id\"\n  }]\n}\n```\n\n```typescript\n// functions/api/data.ts\nexport const onRequestGet: PagesFunction<Env> = async ({ env }) => {\n  // Smart Placement optimizes execution location over time\n  // Balances user location vs database location\n  const data = await env.DB.prepare('SELECT * FROM products LIMIT 10').all();\n  return Response.json(data);\n};\n```\n\n**Best for**: Read-heavy apps with D1/Durable Objects in specific regions.  \n**Not needed**: Apps without data locality constraints or with evenly distributed traffic.\n\n## Framework Integration\n\n**Supported** (2026): SvelteKit, Astro, Nuxt, Qwik, Solid Start\n\n```bash\nnpm create cloudflare@latest my-app -- --framework=svelte\n```\n\n### SvelteKit\n```typescript\n// src/routes/+page.server.ts\nexport const load = async ({ platform }) => {\n  const todos = await platform.env.DB.prepare('SELECT * FROM todos').all();\n  return { todos: todos.results };\n};\n```\n\n### Astro\n```astro\n---\nconst { DB } = Astro.locals.runtime.env;\nconst todos = await DB.prepare('SELECT * FROM todos').all();\n---\n<ul>{todos.results.map(t => <li>{t.title}</li>)}</ul>\n```\n\n### Nuxt\n```typescript\n// server/api/todos.get.ts\nexport default defineEventHandler(async (event) => {\n  const { DB } = event.context.cloudflare.env;\n  return await DB.prepare('SELECT * FROM todos').all();\n});\n```\n\n**⚠️ Framework Status** (2026):\n- ✅ **Supported**: SvelteKit, Astro, Nuxt, Qwik, Solid Start\n- ❌ **Deprecated**: Next.js (`@cloudflare/next-on-pages`), Remix (`@remix-run/cloudflare-pages`)\n\nFor deprecated frameworks, see [gotchas.md](./gotchas.md#framework-specific) for migration options.\n\n[Framework Guides](https://developers.cloudflare.com/pages/framework-guides/)\n\n## Monorepo\n\nDashboard → Settings → Build → Root directory. Set to subproject (e.g., `apps/web`).\n\n## Best Practices\n\n**Performance**: Exclude static via `_routes.json`; cache with KV; keep bundle < 1MB  \n**Security**: Use secrets (not vars); validate inputs; rate limit with KV/DO  \n**Workflow**: Preview per branch; local dev with `wrangler pages dev`; instant rollbacks in Dashboard\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pages-functions/README.md",
    "content": "# Cloudflare Pages Functions\n\nServerless functions on Cloudflare Pages using Workers runtime. Full-stack dev with file-based routing.\n\n## Quick Navigation\n\n**Need to...**\n| Task | Go to |\n|------|-------|\n| Set up TypeScript types | [configuration.md](./configuration.md) - TypeScript Setup |\n| Configure bindings (KV, D1, R2) | [configuration.md](./configuration.md) - wrangler.jsonc |\n| Access request/env/params | [api.md](./api.md) - EventContext |\n| Add middleware or auth | [patterns.md](./patterns.md) - Middleware, Auth |\n| Background tasks (waitUntil) | [patterns.md](./patterns.md) - Background Tasks |\n| Debug errors or check limits | [gotchas.md](./gotchas.md) - Common Errors, Limits |\n\n## Decision Tree: Is This Pages Functions?\n\n```\nNeed serverless backend? \n├─ Yes, for a static site → Pages Functions\n├─ Yes, standalone API → Workers\n└─ Just static hosting → Pages (no functions)\n\nHave existing Worker?\n├─ Complex routing logic → Use _worker.js (Advanced Mode)\n└─ Simple routes → Migrate to /functions (File-Based)\n\nFramework-based?\n├─ Next.js/SvelteKit/Remix → Uses _worker.js automatically\n└─ Vanilla/HTML/React SPA → Use /functions\n```\n\n## File-Based Routing\n\n```\n/functions\n  ├── index.js              → /\n  ├── api.js                → /api\n  ├── users/\n  │   ├── index.js          → /users/\n  │   ├── [user].js         → /users/:user\n  │   └── [[catchall]].js   → /users/*\n  └── _middleware.js        → runs on all routes\n```\n\n**Rules:**\n- `index.js` → directory root\n- Trailing slash optional\n- Specific routes precede catch-alls\n- Falls back to static if no match\n\n## Dynamic Routes\n\n**Single segment** `[param]` → string:\n```js\n// /functions/users/[user].js\nexport function onRequest(context) {\n  return new Response(`Hello ${context.params.user}`);\n}\n// Matches: /users/nevi\n```\n\n**Multi-segment** `[[param]]` → array:\n```js\n// /functions/users/[[catchall]].js\nexport function onRequest(context) {\n  return new Response(JSON.stringify(context.params.catchall));\n}\n// Matches: /users/nevi/foobar → [\"nevi\", \"foobar\"]\n```\n\n## Key Features\n\n- **Method handlers:** `onRequestGet`, `onRequestPost`, etc.\n- **Middleware:** `_middleware.js` for cross-cutting concerns\n- **Bindings:** KV, D1, R2, Durable Objects, Workers AI, Service bindings\n- **TypeScript:** Full type support via `wrangler types` command\n- **Advanced mode:** Use `_worker.js` for custom routing logic\n\n## Reading Order\n\n**New to Pages Functions?** Start here:\n1. [README.md](./README.md) - Overview, routing, decision tree (you are here)\n2. [configuration.md](./configuration.md) - TypeScript setup, wrangler.jsonc, bindings\n3. [api.md](./api.md) - EventContext, handlers, bindings reference\n4. [patterns.md](./patterns.md) - Middleware, auth, CORS, rate limiting, caching\n5. [gotchas.md](./gotchas.md) - Common errors, debugging, limits\n\n**Quick reference lookup:**\n- Bindings table → [api.md](./api.md)\n- Error diagnosis → [gotchas.md](./gotchas.md)\n- TypeScript setup → [configuration.md](./configuration.md)\n\n## See Also\n- [pages](../pages/) - Pages platform overview and static site deployment\n- [workers](../workers/) - Workers runtime API reference\n- [d1](../d1/) - D1 database integration with Pages Functions\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pages-functions/api.md",
    "content": "# Function API\n\n## EventContext\n\n```typescript\ninterface EventContext<Env = any> {\n  request: Request;              // Incoming request\n  functionPath: string;          // Request path\n  waitUntil(promise: Promise<any>): void;  // Background tasks (non-blocking)\n  passThroughOnException(): void;          // Fallback to static on error\n  next(input?: Request | string, init?: RequestInit): Promise<Response>;\n  env: Env;                      // Bindings, vars, secrets\n  params: Record<string, string | string[]>;  // Route params ([user] or [[catchall]])\n  data: any;                     // Middleware shared state\n}\n```\n\n**TypeScript:** See [configuration.md](./configuration.md) for `wrangler types` setup\n\n## Handlers\n\n```typescript\n// Generic (fallback for any method)\nexport async function onRequest(ctx: EventContext): Promise<Response> {\n  return new Response('Any method');\n}\n\n// Method-specific (takes precedence over generic)\nexport async function onRequestGet(ctx: EventContext): Promise<Response> {\n  return Response.json({ message: 'GET' });\n}\n\nexport async function onRequestPost(ctx: EventContext): Promise<Response> {\n  const body = await ctx.request.json();\n  return Response.json({ received: body });\n}\n// Also: onRequestPut, onRequestPatch, onRequestDelete, onRequestHead, onRequestOptions\n```\n\n## Bindings Reference\n\n| Binding Type | Interface | Config Key | Use Case |\n|--------------|-----------|------------|----------|\n| KV | `KVNamespace` | `kv_namespaces` | Key-value cache, sessions, config |\n| D1 | `D1Database` | `d1_databases` | Relational data, SQL queries |\n| R2 | `R2Bucket` | `r2_buckets` | Large files, user uploads, assets |\n| Durable Objects | `DurableObjectNamespace` | `durable_objects.bindings` | Stateful coordination, websockets |\n| Workers AI | `Ai` | `ai.binding` | LLM inference, embeddings |\n| Vectorize | `VectorizeIndex` | `vectorize` | Vector search, embeddings |\n| Service Binding | `Fetcher` | `services` | Worker-to-worker RPC |\n| Analytics Engine | `AnalyticsEngineDataset` | `analytics_engine_datasets` | Event logging, metrics |\n| Environment Vars | `string` | `vars` | Non-sensitive config |\n\nSee [configuration.md](./configuration.md) for wrangler.jsonc examples.\n\n## Bindings\n\n### KV\n\n```typescript\ninterface Env { KV: KVNamespace; }\nexport const onRequest: PagesFunction<Env> = async (ctx) => {\n  await ctx.env.KV.put('key', 'value', { expirationTtl: 3600 });\n  const val = await ctx.env.KV.get('key', { type: 'json' });\n  const keys = await ctx.env.KV.list({ prefix: 'user:' });\n  return Response.json({ val });\n};\n```\n\n### D1\n\n```typescript\ninterface Env { DB: D1Database; }\nexport const onRequest: PagesFunction<Env> = async (ctx) => {\n  const user = await ctx.env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(123).first();\n  return Response.json(user);\n};\n```\n\n### R2\n\n```typescript\ninterface Env { BUCKET: R2Bucket; }\nexport const onRequest: PagesFunction<Env> = async (ctx) => {\n  const obj = await ctx.env.BUCKET.get('file.txt');\n  if (!obj) return new Response('Not found', { status: 404 });\n  await ctx.env.BUCKET.put('file.txt', ctx.request.body);\n  return new Response(obj.body);\n};\n```\n\n### Durable Objects\n\n```typescript\ninterface Env { COUNTER: DurableObjectNamespace; }\nexport const onRequest: PagesFunction<Env> = async (ctx) => {\n  const stub = ctx.env.COUNTER.get(ctx.env.COUNTER.idFromName('global'));\n  return stub.fetch(ctx.request);\n};\n```\n\n### Workers AI\n\n```typescript\ninterface Env { AI: Ai; }\nexport const onRequest: PagesFunction<Env> = async (ctx) => {\n  const resp = await ctx.env.AI.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' });\n  return Response.json(resp);\n};\n```\n\n### Service Bindings & Env Vars\n\n```typescript\ninterface Env { AUTH: Fetcher; API_KEY: string; }\nexport const onRequest: PagesFunction<Env> = async (ctx) => {\n  // Service binding: forward to another Worker\n  return ctx.env.AUTH.fetch(ctx.request);\n  \n  // Environment variable\n  return Response.json({ key: ctx.env.API_KEY });\n};\n```\n\n## Advanced Mode (env.ASSETS)\n\nWhen using `_worker.js`, access static assets via `env.ASSETS.fetch()`:\n\n```typescript\ninterface Env { ASSETS: Fetcher; KV: KVNamespace; }\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const url = new URL(request.url);\n    if (url.pathname.startsWith('/api/')) {\n      return Response.json({ data: await env.KV.get('key') });\n    }\n    return env.ASSETS.fetch(request); // Fallback to static\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n**See also:** [configuration.md](./configuration.md) for TypeScript setup and wrangler.jsonc | [patterns.md](./patterns.md) for middleware and auth patterns\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pages-functions/configuration.md",
    "content": "# Configuration\n\n## TypeScript Setup\n\n**Generate types from wrangler.jsonc** (replaces deprecated `@cloudflare/workers-types`):\n\n```bash\nnpx wrangler types\n```\n\nCreates `worker-configuration.d.ts` with typed `Env` interface based on your bindings.\n\n```typescript\n// functions/api.ts\nexport const onRequest: PagesFunction<Env> = async (ctx) => {\n  // ctx.env.KV, ctx.env.DB, etc. are fully typed\n  return Response.json({ ok: true });\n};\n```\n\n**Manual types** (if not using wrangler types):\n\n```typescript\ninterface Env {\n  KV: KVNamespace;\n  DB: D1Database;\n  API_KEY: string;\n}\nexport const onRequest: PagesFunction<Env> = async (ctx) => { /* ... */ };\n```\n\n## wrangler.jsonc\n\n```jsonc\n{\n  \"$schema\": \"./node_modules/wrangler/config-schema.json\",\n  \"name\": \"my-pages-app\",\n  \"pages_build_output_dir\": \"./dist\",\n  \"compatibility_date\": \"2025-01-01\",\n  \"compatibility_flags\": [\"nodejs_compat\"],\n  \n  \"vars\": { \"API_URL\": \"https://api.example.com\" },\n  \"kv_namespaces\": [{ \"binding\": \"KV\", \"id\": \"abc123\" }],\n  \"d1_databases\": [{ \"binding\": \"DB\", \"database_name\": \"prod-db\", \"database_id\": \"xyz789\" }],\n  \"r2_buckets\": [{ \"binding\": \"BUCKET\", \"bucket_name\": \"my-bucket\" }],\n  \"durable_objects\": { \"bindings\": [{ \"name\": \"COUNTER\", \"class_name\": \"Counter\", \"script_name\": \"counter-worker\" }] },\n  \"services\": [{ \"binding\": \"AUTH\", \"service\": \"auth-worker\" }],\n  \"ai\": { \"binding\": \"AI\" },\n  \"vectorize\": [{ \"binding\": \"VECTORIZE\", \"index_name\": \"my-index\" }],\n  \"analytics_engine_datasets\": [{ \"binding\": \"ANALYTICS\" }]\n}\n```\n\n## Environment Overrides\n\nTop-level → local dev, `env.preview` → preview, `env.production` → production\n\n```jsonc\n{\n  \"vars\": { \"API_URL\": \"http://localhost:8787\" },\n  \"env\": {\n    \"production\": { \"vars\": { \"API_URL\": \"https://api.example.com\" } }\n  }\n}\n```\n\n**Note:** If overriding `vars`, `kv_namespaces`, `d1_databases`, etc., ALL must be redefined (non-inheritable)\n\n## Local Secrets (.dev.vars)\n\n**Local dev only** - NOT deployed:\n\n```bash\n# .dev.vars (add to .gitignore)\nSECRET_KEY=\"my-secret-value\"\n```\n\nAccessed via `ctx.env.SECRET_KEY`. Set production secrets:\n```bash\necho \"value\" | npx wrangler pages secret put SECRET_KEY --project-name=my-app\n```\n\n## Static Config Files\n\n**_routes.json** - Custom routing:\n```json\n{ \"version\": 1, \"include\": [\"/api/*\"], \"exclude\": [\"/static/*\"] }\n```\n\n**_headers** - Static headers:\n```\n/static/*\n  Cache-Control: public, max-age=31536000\n```\n\n**_redirects** - Redirects:\n```\n/old  /new  301\n```\n\n## Local Dev & Deployment\n\n```bash\n# Dev server\nnpx wrangler pages dev ./dist\n\n# With bindings\nnpx wrangler pages dev ./dist --kv=KV --d1=DB=db-id --r2=BUCKET\n\n# Durable Objects (2 terminals)\ncd do-worker && npx wrangler dev\ncd pages-project && npx wrangler pages dev ./dist --do COUNTER=Counter@do-worker\n\n# Deploy\nnpx wrangler pages deploy ./dist\nnpx wrangler pages deploy ./dist --branch preview\n\n# Download config\nnpx wrangler pages download config my-project\n```\n\n**See also:** [api.md](./api.md) for binding usage examples\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pages-functions/gotchas.md",
    "content": "# Gotchas & Debugging\n\n## Error Diagnosis\n\n| Symptom | Likely Cause | Solution |\n|---------|--------------|----------|\n| **Function not invoking** | Wrong `/functions` location, wrong extension, or `_routes.json` excludes path | Check `pages_build_output_dir`, use `.js`/`.ts`, verify `_routes.json` |\n| **`ctx.env.BINDING` undefined** | Binding not configured or name mismatch | Add to `wrangler.jsonc`, verify exact name (case-sensitive), redeploy |\n| **TypeScript errors on `ctx.env`** | Missing type definition | Run `wrangler types` or define `interface Env {}` |\n| **Middleware not running** | Wrong filename/location or missing `ctx.next()` | Name exactly `_middleware.js`, export `onRequest`, call `ctx.next()` |\n| **Secrets missing in production** | `.dev.vars` not deployed | `.dev.vars` is local only - set production secrets via dashboard or `wrangler secret put` |\n| **Type mismatch on binding** | Wrong interface type | See [api.md](./api.md) bindings table for correct types |\n| **\"KV key not found\" but exists** | Key in wrong namespace or env | Verify namespace binding, check preview vs production env |\n| **Function times out** | Synchronous wait or missing `await` | All I/O must be async/await, use `ctx.waitUntil()` for background tasks |\n\n## Common Errors\n\n### TypeScript type errors\n\n**Problem:** `ctx.env.MY_BINDING` shows type error  \n**Cause:** No type definition for `Env`  \n**Solution:** Run `npx wrangler types` or manually define:\n```typescript\ninterface Env { MY_BINDING: KVNamespace; }\nexport const onRequest: PagesFunction<Env> = async (ctx) => { /* ... */ };\n```\n\n### Secrets not available in production\n\n**Problem:** `ctx.env.SECRET_KEY` is undefined in production  \n**Cause:** `.dev.vars` is local-only, not deployed  \n**Solution:** Set production secrets:\n```bash\necho \"value\" | npx wrangler pages secret put SECRET_KEY --project-name=my-app\n```\n\n## Debugging\n\n```typescript\n// Console logging\nexport async function onRequest(ctx) {\n  console.log('Request:', ctx.request.method, ctx.request.url);\n  const res = await ctx.next();\n  console.log('Status:', res.status);\n  return res;\n}\n```\n\n```bash\n# Stream real-time logs\nnpx wrangler pages deployment tail\nnpx wrangler pages deployment tail --status error\n```\n\n```jsonc\n// Source maps (wrangler.jsonc)\n{ \"upload_source_maps\": true }\n```\n\n## Limits\n\n| Resource | Free | Paid |\n|----------|------|------|\n| CPU time | 10ms | 50ms |\n| Memory | 128 MB | 128 MB |\n| Script size | 10 MB compressed | 10 MB compressed |\n| Env vars | 5 KB per var, 64 max | 5 KB per var, 64 max |\n| Requests | 100k/day | Unlimited ($0.50/million) |\n\n## Best Practices\n\n**Performance:** Minimize deps (cold start), use KV for cache/D1 for relational/R2 for large files, set `Cache-Control` headers, batch DB ops, handle errors gracefully\n\n**Security:** Never commit secrets (use `.dev.vars` + gitignore), validate input, sanitize before DB, implement auth middleware, set CORS headers, rate limit per-IP\n\n## Migration\n\n**Workers → Pages Functions:**\n- `export default { fetch(req, env) {} }` → `export function onRequest(ctx) { const { request, env } = ctx; }`\n- Use `_worker.js` for complex routing: `env.ASSETS.fetch(request)` for static files\n\n**Other platforms → Pages:**\n- File-based routing: `/functions/api/users.js` → `/api/users`\n- Dynamic routes: `[param]` not `:param`\n- Replace Node.js deps with Workers APIs or add `nodejs_compat` flag\n\n## Resources\n\n- [Official Docs](https://developers.cloudflare.com/pages/functions/)\n- [Workers APIs](https://developers.cloudflare.com/workers/runtime-apis/)\n- [Examples](https://github.com/cloudflare/pages-example-projects)\n- [Discord](https://discord.gg/cloudflaredev)\n\n**See also:** [configuration.md](./configuration.md) for TypeScript setup | [patterns.md](./patterns.md) for middleware/auth | [api.md](./api.md) for bindings\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pages-functions/patterns.md",
    "content": "# Common Patterns\n\n## Background Tasks (waitUntil)\n\nNon-blocking tasks after response sent (analytics, cleanup, webhooks):\n\n```typescript\nexport async function onRequest(ctx: EventContext<Env>) {\n  const res = Response.json({ success: true });\n  \n  ctx.waitUntil(ctx.env.KV.put('last-visit', new Date().toISOString()));\n  ctx.waitUntil(Promise.all([\n    ctx.env.ANALYTICS.writeDataPoint({ event: 'view' }),\n    fetch('https://webhook.site/...', { method: 'POST' })\n  ]));\n  \n  return res; // Returned immediately\n}\n```\n\n## Middleware & Auth\n\n```typescript\n// functions/_middleware.js (global) or functions/users/_middleware.js (scoped)\nexport async function onRequest(ctx) {\n  try { return await ctx.next(); } \n  catch (err) { return new Response(err.message, { status: 500 }); }\n}\n\n// Chained: export const onRequest = [errorHandler, auth, logger];\n\n// Auth\nasync function auth(ctx: EventContext<Env>) {\n  const token = ctx.request.headers.get('authorization')?.replace('Bearer ', '');\n  if (!token) return new Response('Unauthorized', { status: 401 });\n  const session = await ctx.env.KV.get(`session:${token}`);\n  if (!session) return new Response('Invalid', { status: 401 });\n  ctx.data.user = JSON.parse(session);\n  return ctx.next();\n}\n```\n\n## CORS & Rate Limiting\n\n```typescript\n// CORS middleware\nconst cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST' };\nexport async function onRequestOptions() { return new Response(null, { headers: cors }); }\nexport async function onRequest(ctx) {\n  const res = await ctx.next();\n  Object.entries(cors).forEach(([k, v]) => res.headers.set(k, v));\n  return res;\n}\n\n// Rate limiting (KV-based)\nasync function rateLimit(ctx: EventContext<Env>) {\n  const ip = ctx.request.headers.get('CF-Connecting-IP') || 'unknown';\n  const count = parseInt(await ctx.env.KV.get(`rate:${ip}`) || '0');\n  if (count >= 100) return new Response('Rate limited', { status: 429 });\n  await ctx.env.KV.put(`rate:${ip}`, (count + 1).toString(), { expirationTtl: 3600 });\n  return ctx.next();\n}\n```\n\n## Forms, Caching, Redirects\n\n```typescript\n// JSON & file upload\nexport async function onRequestPost(ctx) {\n  const ct = ctx.request.headers.get('content-type') || '';\n  if (ct.includes('application/json')) return Response.json(await ctx.request.json());\n  if (ct.includes('multipart/form-data')) {\n    const file = (await ctx.request.formData()).get('file') as File;\n    await ctx.env.BUCKET.put(file.name, file.stream());\n    return Response.json({ uploaded: file.name });\n  }\n}\n\n// Cache API\nexport async function onRequest(ctx) {\n  let res = await caches.default.match(ctx.request);\n  if (!res) {\n    res = new Response('Data');\n    res.headers.set('Cache-Control', 'public, max-age=3600');\n    ctx.waitUntil(caches.default.put(ctx.request, res.clone()));\n  }\n  return res;\n}\n\n// Redirects\nexport async function onRequest(ctx) {\n  if (new URL(ctx.request.url).pathname === '/old') {\n    return Response.redirect(new URL('/new', ctx.request.url), 301);\n  }\n  return ctx.next();\n}\n```\n\n## Testing\n\n**Unit tests** (Vitest + cloudflare:test):\n```typescript\nimport { env } from 'cloudflare:test';\nimport { it, expect } from 'vitest';\nimport { onRequest } from '../functions/api';\n\nit('returns JSON', async () => {\n  const req = new Request('http://localhost/api');\n  const ctx = { request: req, env, params: {}, data: {} } as EventContext;\n  const res = await onRequest(ctx);\n  expect(res.status).toBe(200);\n});\n```\n\n**Integration:** `wrangler pages dev` + Playwright/Cypress\n\n## Advanced Mode (_worker.js)\n\nUse `_worker.js` for complex routing (replaces `/functions`):\n\n```typescript\ninterface Env { ASSETS: Fetcher; KV: KVNamespace; }\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const url = new URL(request.url);\n    if (url.pathname.startsWith('/api/')) {\n      return Response.json({ data: await env.KV.get('key') });\n    }\n    return env.ASSETS.fetch(request); // Static files\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n**When:** Existing Worker, framework-generated (Next.js/SvelteKit), custom routing logic\n\n**See also:** [api.md](./api.md) for `env.ASSETS.fetch()` | [gotchas.md](./gotchas.md) for debugging\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pipelines/README.md",
    "content": "# Cloudflare Pipelines\n\nETL streaming platform for ingesting, transforming, and loading data into R2 with SQL transformations.\n\n## Overview\n\nPipelines provides:\n- **Streams**: Durable event buffers (HTTP/Workers ingestion)\n- **Pipelines**: SQL-based transformations\n- **Sinks**: R2 destinations (Iceberg tables or Parquet/JSON files)\n\n**Status**: Open beta (Workers Paid plan)  \n**Pricing**: No charge beyond standard R2 storage/operations\n\n## Architecture\n\n```\nData Sources → Streams → Pipelines (SQL) → Sinks → R2\n                 ↑          ↓                ↓\n            HTTP/Workers  Transform     Iceberg/Parquet\n```\n\n| Component | Purpose | Key Feature |\n|-----------|---------|-------------|\n| Streams | Event ingestion | Structured (validated) or unstructured |\n| Pipelines | Transform with SQL | Immutable after creation |\n| Sinks | Write to R2 | Exactly-once delivery |\n\n## Quick Start\n\n```bash\n# Interactive setup (recommended)\nnpx wrangler pipelines setup\n```\n\n**Minimal Worker example:**\n```typescript\ninterface Env {\n  STREAM: Pipeline;\n}\n\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n    const event = { user_id: \"123\", event_type: \"purchase\", amount: 29.99 };\n    \n    // Fire-and-forget pattern\n    ctx.waitUntil(env.STREAM.send([event]));\n    \n    return new Response('OK');\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## Which Sink Type?\n\n```\nNeed SQL queries on data?\n  → R2 Data Catalog (Iceberg)\n    ✅ ACID transactions, time-travel, schema evolution\n    ❌ More setup complexity (namespace, table, catalog token)\n\nJust file storage/archival?\n  → R2 Storage (Parquet)\n    ✅ Simple, direct file access\n    ❌ No built-in SQL queries\n\nUsing external tools (Spark/Athena)?\n  → R2 Storage (Parquet with partitioning)\n    ✅ Standard format, partition pruning for performance\n    ❌ Must manage schema compatibility yourself\n```\n\n## Common Use Cases\n\n- **Analytics pipelines**: Clickstream, telemetry, server logs\n- **Data warehousing**: ETL into queryable Iceberg tables\n- **Event processing**: Mobile/IoT with enrichment\n- **Ecommerce analytics**: User events, purchases, views\n\n## Reading Order\n\n**New to Pipelines?** Start here:\n1. [configuration.md](./configuration.md) - Setup streams, sinks, pipelines\n2. [api.md](./api.md) - Send events, TypeScript types, SQL functions\n3. [patterns.md](./patterns.md) - Best practices, integrations, complete example\n4. [gotchas.md](./gotchas.md) - Critical warnings, troubleshooting\n\n**Task-based routing:**\n- Setup pipeline → [configuration.md](./configuration.md)\n- Send/query data → [api.md](./api.md)\n- Implement pattern → [patterns.md](./patterns.md)\n- Debug issue → [gotchas.md](./gotchas.md)\n\n## In This Reference\n\n- [configuration.md](./configuration.md) - wrangler.jsonc bindings, schema definition, sink options, CLI commands\n- [api.md](./api.md) - Pipeline binding interface, send() method, HTTP ingest, SQL function reference\n- [patterns.md](./patterns.md) - Fire-and-forget, schema validation with Zod, integrations, performance tuning\n- [gotchas.md](./gotchas.md) - Silent validation failures, immutable pipelines, latency expectations, limits\n\n## See Also\n\n- [r2](../r2/) - R2 storage backend for sinks\n- [queues](../queues/) - Compare with Queues for async processing\n- [workers](../workers/) - Worker runtime for event ingestion\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pipelines/api.md",
    "content": "# Pipelines API Reference\n\n## Pipeline Binding Interface\n\n```typescript\n// From @cloudflare/workers-types\ninterface Pipeline {\n  send(data: object | object[]): Promise<void>;\n}\n\ninterface Env {\n  STREAM: Pipeline;\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // send() returns Promise<void> - no result data\n    await env.STREAM.send([event]);\n    return new Response('OK');\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n**Key points:**\n- `send()` accepts single object or array\n- Always returns `Promise<void>` (no confirmation data)\n- Throws on network/validation errors (wrap in try/catch)\n- Use `ctx.waitUntil()` for fire-and-forget pattern\n\n## Writing Events\n\n### Single Event\n\n```typescript\nawait env.STREAM.send([{\n  user_id: \"12345\",\n  event_type: \"purchase\",\n  product_id: \"widget-001\",\n  amount: 29.99\n}]);\n```\n\n### Batch Events\n\n```typescript\nconst events = [\n  { user_id: \"user1\", event_type: \"view\" },\n  { user_id: \"user2\", event_type: \"purchase\", amount: 50 }\n];\nawait env.STREAM.send(events);\n```\n\n**Limits:**\n- Max 1 MB per request\n- 5 MB/s per stream\n\n### Fire-and-Forget Pattern\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n    const event = { /* ... */ };\n    \n    // Don't block response on send\n    ctx.waitUntil(env.STREAM.send([event]));\n    \n    return new Response('OK');\n  }\n};\n```\n\n### Error Handling\n\n```typescript\ntry {\n  await env.STREAM.send([event]);\n} catch (error) {\n  console.error('Pipeline send failed:', error);\n  // Log to another system, retry, or return error response\n  return new Response('Failed to track event', { status: 500 });\n}\n```\n\n## HTTP Ingest API\n\n### Endpoint Format\n\n```\nhttps://{stream-id}.ingest.cloudflare.com\n```\n\nGet `{stream-id}` from: `npx wrangler pipelines streams list`\n\n### Request Format\n\n**CRITICAL:** Must send array, not single object\n\n```bash\n# ✅ Correct\ncurl -X POST https://{stream-id}.ingest.cloudflare.com \\\n  -H \"Content-Type: application/json\" \\\n  -d '[{\"user_id\": \"123\", \"event_type\": \"purchase\"}]'\n\n# ❌ Wrong - will fail\ncurl -X POST https://{stream-id}.ingest.cloudflare.com \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"user_id\": \"123\", \"event_type\": \"purchase\"}'\n```\n\n### Authentication\n\n```bash\ncurl -X POST https://{stream-id}.ingest.cloudflare.com \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n  -d '[{\"event\": \"data\"}]'\n```\n\n**Required permission:** Workers Pipeline Send\n\nCreate token: Dashboard → Workers → API tokens → Create with Pipeline Send permission\n\n### Response Codes\n\n| Code | Meaning | Action |\n|------|---------|--------|\n| 200 | Accepted | Success |\n| 400 | Invalid format | Check JSON array, schema match |\n| 401 | Auth failed | Verify token valid |\n| 413 | Payload too large | Split into smaller batches (<1 MB) |\n| 429 | Rate limited | Back off, retry with delay |\n| 5xx | Server error | Retry with exponential backoff |\n\n## SQL Functions Quick Reference\n\nAvailable in `INSERT INTO sink SELECT ... FROM stream` transformations:\n\n| Function | Example | Use Case |\n|----------|---------|----------|\n| `UPPER(s)` | `UPPER(event_type)` | Normalize strings |\n| `LOWER(s)` | `LOWER(email)` | Case-insensitive matching |\n| `CONCAT(...)` | `CONCAT(user_id, '_', product_id)` | Generate composite keys |\n| `CASE WHEN ... THEN ... END` | `CASE WHEN amount > 100 THEN 'high' ELSE 'low' END` | Conditional enrichment |\n| `CAST(x AS type)` | `CAST(timestamp AS string)` | Type conversion |\n| `COALESCE(x, y)` | `COALESCE(amount, 0.0)` | Default values |\n| Math operators | `amount * 1.1`, `price / quantity` | Calculations |\n| Comparison | `amount > 100`, `status IN ('active', 'pending')` | Filtering |\n\n**String types for CAST:** `string`, `int32`, `int64`, `float32`, `float64`, `bool`, `timestamp`\n\nFull reference: [Pipelines SQL Reference](https://developers.cloudflare.com/pipelines/sql-reference/)\n\n## SQL Transform Examples\n\n### Filter Events\n\n```sql\nINSERT INTO my_sink\nSELECT * FROM my_stream\nWHERE event_type = 'purchase' AND amount > 100\n```\n\n### Select Specific Fields\n\n```sql\nINSERT INTO my_sink\nSELECT user_id, event_type, timestamp, amount\nFROM my_stream\n```\n\n### Transform and Enrich\n\n```sql\nINSERT INTO my_sink\nSELECT\n  user_id,\n  UPPER(event_type) as event_type,\n  timestamp,\n  amount * 1.1 as amount_with_tax,\n  CONCAT(user_id, '_', product_id) as unique_key,\n  CASE\n    WHEN amount > 1000 THEN 'high_value'\n    WHEN amount > 100 THEN 'medium_value'\n    ELSE 'low_value'\n  END as customer_tier\nFROM my_stream\nWHERE event_type IN ('purchase', 'refund')\n```\n\n## Querying Results (R2 Data Catalog)\n\n```bash\nexport WRANGLER_R2_SQL_AUTH_TOKEN=YOUR_CATALOG_TOKEN\n\nnpx wrangler r2 sql query \"warehouse_name\" \"\nSELECT \n  event_type,\n  COUNT(*) as event_count,\n  SUM(amount) as total_revenue\nFROM default.my_table\nWHERE event_type = 'purchase'\n  AND timestamp >= '2025-01-01'\nGROUP BY event_type\nORDER BY total_revenue DESC\nLIMIT 100\"\n```\n\n**Note:** Iceberg tables support standard SQL queries with GROUP BY, JOINs, WHERE, ORDER BY, etc.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pipelines/configuration.md",
    "content": "# Pipelines Configuration\n\n## Worker Binding\n\n```jsonc\n// wrangler.jsonc\n{\n  \"pipelines\": [\n    { \"pipeline\": \"<STREAM_ID>\", \"binding\": \"STREAM\" }\n  ]\n}\n```\n\nGet stream ID: `npx wrangler pipelines streams list`\n\n## Schema (Structured Streams)\n\n```json\n{\n  \"fields\": [\n    { \"name\": \"user_id\", \"type\": \"string\", \"required\": true },\n    { \"name\": \"event_type\", \"type\": \"string\", \"required\": true },\n    { \"name\": \"amount\", \"type\": \"float64\", \"required\": false },\n    { \"name\": \"timestamp\", \"type\": \"timestamp\", \"required\": true }\n  ]\n}\n```\n\n**Types:** `string`, `int32`, `int64`, `float32`, `float64`, `bool`, `timestamp`, `json`, `binary`, `list`, `struct`\n\n## Stream Setup\n\n```bash\n# With schema\nnpx wrangler pipelines streams create my-stream --schema-file schema.json\n\n# Unstructured (no validation)\nnpx wrangler pipelines streams create my-stream\n\n# List/get/delete\nnpx wrangler pipelines streams list\nnpx wrangler pipelines streams get <ID>\nnpx wrangler pipelines streams delete <ID>\n```\n\n## Sink Configuration\n\n**R2 Data Catalog (Iceberg):**\n```bash\nnpx wrangler pipelines sinks create my-sink \\\n  --type r2-data-catalog \\\n  --bucket my-bucket --namespace default --table events \\\n  --catalog-token $TOKEN \\\n  --compression zstd --roll-interval 60\n```\n\n**R2 Raw (Parquet):**\n```bash\nnpx wrangler pipelines sinks create my-sink \\\n  --type r2 --bucket my-bucket --format parquet \\\n  --path analytics/events \\\n  --partitioning \"year=%Y/month=%m/day=%d\" \\\n  --access-key-id $KEY --secret-access-key $SECRET\n```\n\n| Option | Values | Guidance |\n|--------|--------|----------|\n| `--compression` | `zstd`, `snappy`, `gzip` | `zstd` best ratio, `snappy` fastest |\n| `--roll-interval` | Seconds | Low latency: 10-60, Query perf: 300 |\n| `--roll-size` | MB | Larger = better compression |\n\n## Pipeline Creation\n\n```bash\nnpx wrangler pipelines create my-pipeline \\\n  --sql \"INSERT INTO my_sink SELECT * FROM my_stream WHERE event_type = 'purchase'\"\n```\n\n**⚠️ Pipelines are immutable** - cannot modify SQL. Must delete/recreate.\n\n## Credentials\n\n| Type | Permission | Get From |\n|------|------------|----------|\n| Catalog token | R2 Admin Read & Write | Dashboard → R2 → API tokens |\n| R2 credentials | Object Read & Write | `wrangler r2 bucket create` output |\n| HTTP ingest token | Workers Pipeline Send | Dashboard → Workers → API tokens |\n\n## Complete Example\n\n```bash\nnpx wrangler r2 bucket create my-bucket\nnpx wrangler r2 bucket catalog enable my-bucket\nnpx wrangler pipelines streams create my-stream --schema-file schema.json\nnpx wrangler pipelines sinks create my-sink --type r2-data-catalog --bucket my-bucket ...\nnpx wrangler pipelines create my-pipeline --sql \"INSERT INTO my_sink SELECT * FROM my_stream\"\nnpx wrangler deploy\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pipelines/gotchas.md",
    "content": "# Pipelines Gotchas\n\n## Critical Issues\n\n### Events Silently Dropped\n\n**Most common issue.** Events accepted (HTTP 200) but never appear in sink.\n\n**Causes:**\n1. Schema validation fails - structured streams drop invalid events silently\n2. Waiting for roll interval (10-300s) - expected behavior\n\n**Solution:** Validate client-side with Zod:\n```typescript\nconst EventSchema = z.object({ user_id: z.string(), amount: z.number() });\ntry {\n  const validated = EventSchema.parse(rawEvent);\n  await env.STREAM.send([validated]);\n} catch (e) { /* get immediate feedback */ }\n```\n\n### Pipelines Are Immutable\n\nCannot modify SQL after creation. Must delete and recreate.\n\n```bash\nnpx wrangler pipelines delete old-pipeline\nnpx wrangler pipelines create new-pipeline --sql \"...\"\n```\n\n**Tip:** Use version naming (`events-pipeline-v1`) and keep SQL in version control.\n\n### Worker Binding Not Found\n\n**`env.STREAM is undefined`**\n\n1. Use **stream ID** (not pipeline ID) in `wrangler.jsonc`\n2. Redeploy after adding binding\n\n```bash\nnpx wrangler pipelines streams list  # Get stream ID\nnpx wrangler deploy\n```\n\n## Common Errors\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| Events not in R2 | Roll interval not elapsed | Wait 10-300s, check `roll_interval` |\n| Schema validation failures | Type mismatch, missing fields | Validate client-side |\n| Rate limit (429) | >5 MB/s per stream | Batch events, request increase |\n| Payload too large (413) | >1 MB request | Split into smaller batches |\n| Cannot delete stream | Pipeline references it | Delete pipelines first |\n| Sink credential errors | Token expired | Recreate sink with new credentials |\n\n## Limits (Open Beta)\n\n| Resource | Limit |\n|----------|-------|\n| Streams/Sinks/Pipelines per account | 20 each |\n| Payload size | 1 MB |\n| Ingest rate per stream | 5 MB/s |\n| Event retention | 24 hours |\n| Recommended batch size | 100 events |\n\n## SQL Limitations\n\n- **No JOINs** - single stream per pipeline\n- **No window functions** - basic SQL only\n- **No subqueries** - must use `INSERT INTO ... SELECT ... FROM`\n- **No schema evolution** - cannot modify after creation\n\n## Debug Checklist\n\n- [ ] Stream exists: `npx wrangler pipelines streams list`\n- [ ] Pipeline healthy: `npx wrangler pipelines get <ID>`\n- [ ] SQL syntax matches schema\n- [ ] Worker redeployed after binding added\n- [ ] Waited for roll interval\n- [ ] Accepted vs processed count matches (no validation drops)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pipelines/patterns.md",
    "content": "# Pipelines Patterns\n\n## Fire-and-Forget\n\n```typescript\nexport default {\n  async fetch(request, env, ctx) {\n    const event = { user_id: '...', event_type: 'page_view', timestamp: new Date().toISOString() };\n    ctx.waitUntil(env.STREAM.send([event])); // Don't block response\n    return new Response('OK');\n  }\n};\n```\n\n## Schema Validation with Zod\n\n```typescript\nimport { z } from 'zod';\n\nconst EventSchema = z.object({\n  user_id: z.string(),\n  event_type: z.enum(['purchase', 'view']),\n  amount: z.number().positive().optional()\n});\n\nconst validated = EventSchema.parse(rawEvent); // Throws on invalid\nawait env.STREAM.send([validated]);\n```\n\n**Why:** Structured streams drop invalid events silently. Client validation gives immediate feedback.\n\n## SQL Transform Patterns\n\n```sql\n-- Filter early (reduce storage)\nINSERT INTO my_sink\nSELECT user_id, event_type, amount\nFROM my_stream\nWHERE event_type = 'purchase' AND amount > 10\n\n-- Select only needed fields\nINSERT INTO my_sink\nSELECT user_id, event_type, timestamp FROM my_stream\n\n-- Enrich with CASE\nINSERT INTO my_sink\nSELECT user_id, amount,\n  CASE WHEN amount > 1000 THEN 'vip' ELSE 'standard' END as tier\nFROM my_stream\n```\n\n## Pipelines + Queues Fan-out\n\n```typescript\nawait Promise.all([\n  env.ANALYTICS_STREAM.send([event]),  // Long-term storage\n  env.PROCESS_QUEUE.send(event)        // Immediate processing\n]);\n```\n\n| Need | Use |\n|------|-----|\n| Long-term storage, SQL queries | Pipelines |\n| Immediate processing, retries | Queues |\n| Both | Fan-out pattern |\n\n## Performance Tuning\n\n| Goal | Config |\n|------|--------|\n| Low latency | `--roll-interval 10` |\n| Query performance | `--roll-interval 300 --roll-size 100` |\n| Cost optimal | `--compression zstd --roll-interval 300` |\n\n## Schema Evolution\n\nPipelines are immutable. Use versioning:\n\n```bash\n# Create v2 stream/sink/pipeline\nnpx wrangler pipelines streams create events-v2 --schema-file v2.json\n\n# Dual-write during transition\nawait Promise.all([env.EVENTS_V1.send([event]), env.EVENTS_V2.send([event])]);\n\n# Query across versions with UNION ALL\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pulumi/README.md",
    "content": "# Cloudflare Pulumi Provider\n\nExpert guidance for Cloudflare Pulumi Provider (@pulumi/cloudflare).\n\n## Overview\n\nProgrammatic management of Cloudflare resources: Workers, Pages, D1, KV, R2, DNS, Queues, etc.\n\n**Packages:**\n- TypeScript/JS: `@pulumi/cloudflare`\n- Python: `pulumi-cloudflare`\n- Go: `github.com/pulumi/pulumi-cloudflare/sdk/v6/go/cloudflare`\n- .NET: `Pulumi.Cloudflare`\n\n**Version:** v6.x\n\n## Core Principles\n\n1. Use API tokens (not legacy API keys)\n2. Store accountId in stack config\n3. Match binding names across code/config\n4. Use `module: true` for ES modules\n5. Set `compatibilityDate` to lock behavior\n\n## Authentication\n\n```typescript\nimport * as cloudflare from \"@pulumi/cloudflare\";\n\n// API Token (recommended): CLOUDFLARE_API_TOKEN env\nconst provider = new cloudflare.Provider(\"cf\", { apiToken: process.env.CLOUDFLARE_API_TOKEN });\n\n// API Key (legacy): CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL env\nconst provider = new cloudflare.Provider(\"cf\", { apiKey: process.env.CLOUDFLARE_API_KEY, email: process.env.CLOUDFLARE_EMAIL });\n\n// API User Service Key: CLOUDFLARE_API_USER_SERVICE_KEY env\nconst provider = new cloudflare.Provider(\"cf\", { apiUserServiceKey: process.env.CLOUDFLARE_API_USER_SERVICE_KEY });\n```\n\n## Setup\n\n**Pulumi.yaml:**\n```yaml\nname: my-cloudflare-app\nruntime: nodejs\nconfig:\n  cloudflare:apiToken:\n    value: ${CLOUDFLARE_API_TOKEN}\n```\n\n**Pulumi.<stack>.yaml:**\n```yaml\nconfig:\n  cloudflare:accountId: \"abc123...\"\n```\n\n**index.ts:**\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as cloudflare from \"@pulumi/cloudflare\";\nconst accountId = new pulumi.Config(\"cloudflare\").require(\"accountId\");\n```\n\n## Common Resource Types\n- `Provider` - Provider config\n- `WorkerScript` - Worker\n- `WorkersKvNamespace` - KV\n- `R2Bucket` - R2\n- `D1Database` - D1\n- `Queue` - Queue\n- `PagesProject` - Pages\n- `DnsRecord` - DNS\n- `WorkerRoute` - Worker route\n- `WorkersDomain` - Custom domain\n\n## Key Properties\n- `accountId` - Required for most resources\n- `zoneId` - Required for DNS/domain\n- `name`/`title` - Resource identifier\n- `*Bindings` - Connect resources to Workers\n\n## Reading Order\n\n| Order | File | What | When to Read |\n|-------|------|------|--------------|\n| 1 | [configuration.md](./configuration.md) | Resource config for Workers/KV/D1/R2/Queues/Pages | First time setup, resource reference |\n| 2 | [patterns.md](./patterns.md) | Architecture patterns, multi-env, component resources | Building complex apps, best practices |\n| 3 | [api.md](./api.md) | Outputs, dependencies, imports, dynamic providers | Advanced features, integrations |\n| 4 | [gotchas.md](./gotchas.md) | Common errors, troubleshooting, limits | Debugging, deployment issues |\n\n## In This Reference\n- [configuration.md](./configuration.md) - Provider config, stack setup, Workers/bindings\n- [api.md](./api.md) - Resource types, Workers script, KV/D1/R2/queues/Pages\n- [patterns.md](./patterns.md) - Multi-env, secrets, CI/CD, stack management\n- [gotchas.md](./gotchas.md) - State issues, deployment failures, limits\n\n## See Also\n- [terraform](../terraform/) - Alternative IaC for Cloudflare\n- [wrangler](../wrangler/) - CLI deployment alternative\n- [workers](../workers/) - Worker runtime documentation\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pulumi/api.md",
    "content": "# API & Data Sources\n\n## Outputs and Exports\n\nExport resource identifiers:\n\n```typescript\nexport const kvId = kv.id;\nexport const bucketName = bucket.name;\nexport const workerUrl = worker.subdomain;\nexport const dbId = db.id;\n```\n\n## Resource Dependencies\n\nImplicit dependencies via outputs:\n\n```typescript\nconst kv = new cloudflare.WorkersKvNamespace(\"kv\", {\n    accountId: accountId,\n    title: \"my-kv\",\n});\n\n// Worker depends on KV (implicit via kv.id)\nconst worker = new cloudflare.WorkerScript(\"worker\", {\n    accountId: accountId,\n    name: \"my-worker\",\n    content: code,\n    kvNamespaceBindings: [{name: \"MY_KV\", namespaceId: kv.id}], // Creates dependency\n});\n```\n\nExplicit dependencies:\n\n```typescript\nconst migration = new command.local.Command(\"migration\", {\n    create: pulumi.interpolate`wrangler d1 execute ${db.name} --file ./schema.sql`,\n}, {dependsOn: [db]});\n\nconst worker = new cloudflare.WorkerScript(\"worker\", {\n    accountId: accountId,\n    name: \"worker\",\n    content: code,\n    d1DatabaseBindings: [{name: \"DB\", databaseId: db.id}],\n}, {dependsOn: [migration]}); // Ensure migrations run first\n```\n\n## Using Outputs with API Calls\n\n```typescript\nconst db = new cloudflare.D1Database(\"db\", {accountId, name: \"my-db\"});\n\ndb.id.apply(async (dbId) => {\n    const response = await fetch(\n        `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${dbId}/query`,\n        {method: \"POST\", headers: {\"Authorization\": `Bearer ${apiToken}`, \"Content-Type\": \"application/json\"},\n         body: JSON.stringify({sql: \"CREATE TABLE users (id INT)\"})}\n    );\n    return response.json();\n});\n```\n\n## Custom Dynamic Providers\n\nFor resources not in provider:\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\n\nclass D1MigrationProvider implements pulumi.dynamic.ResourceProvider {\n    async create(inputs: any): Promise<pulumi.dynamic.CreateResult> {\n        const response = await fetch(\n            `https://api.cloudflare.com/client/v4/accounts/${inputs.accountId}/d1/database/${inputs.databaseId}/query`,\n            {method: \"POST\", headers: {\"Authorization\": `Bearer ${inputs.apiToken}`, \"Content-Type\": \"application/json\"},\n             body: JSON.stringify({sql: inputs.sql})}\n        );\n        return {id: `${inputs.databaseId}-${Date.now()}`, outs: await response.json()};\n    }\n    async update(id: string, olds: any, news: any): Promise<pulumi.dynamic.UpdateResult> {\n        if (olds.sql !== news.sql) await this.create(news);\n        return {};\n    }\n    async delete(id: string, props: any): Promise<void> {}\n}\n\nclass D1Migration extends pulumi.dynamic.Resource {\n    constructor(name: string, args: any, opts?: pulumi.CustomResourceOptions) {\n        super(new D1MigrationProvider(), name, args, opts);\n    }\n}\n\nconst migration = new D1Migration(\"migration\", {\n    accountId, databaseId: db.id, apiToken, sql: \"CREATE TABLE users (id INT)\",\n}, {dependsOn: [db]});\n```\n\n## Data Sources\n\n**Get Zone:**\n```typescript\nconst zone = cloudflare.getZone({name: \"example.com\"});\nconst zoneId = zone.then(z => z.id);\n```\n\n**Get Accounts (via API):**\nUse Cloudflare API directly or custom dynamic resources.\n\n## Import Existing Resources\n\n```bash\n# Import worker\npulumi import cloudflare:index/workerScript:WorkerScript my-worker <account_id>/<worker_name>\n\n# Import KV namespace\npulumi import cloudflare:index/workersKvNamespace:WorkersKvNamespace my-kv <namespace_id>\n\n# Import R2 bucket\npulumi import cloudflare:index/r2Bucket:R2Bucket my-bucket <account_id>/<bucket_name>\n\n# Import D1 database\npulumi import cloudflare:index/d1Database:D1Database my-db <account_id>/<database_id>\n\n# Import DNS record\npulumi import cloudflare:index/dnsRecord:DnsRecord my-record <zone_id>/<record_id>\n```\n\n## Secrets Management\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\n\nconst config = new pulumi.Config();\nconst apiKey = config.requireSecret(\"apiKey\"); // Encrypted in state\n\nconst worker = new cloudflare.WorkerScript(\"worker\", {\n    accountId: accountId,\n    name: \"my-worker\",\n    content: code,\n    secretTextBindings: [{name: \"API_KEY\", text: apiKey}],\n});\n```\n\nStore secrets:\n```bash\npulumi config set --secret apiKey \"secret-value\"\n```\n\n## Transform Pattern\n\nModify resource args before creation:\n\n```typescript\nimport {Transform} from \"@pulumi/pulumi\";\n\ninterface BucketArgs {\n    accountId: pulumi.Input<string>;\n    transform?: {bucket?: Transform<cloudflare.R2BucketArgs>};\n}\n\nfunction createBucket(name: string, args: BucketArgs) {\n    const bucketArgs: cloudflare.R2BucketArgs = {\n        accountId: args.accountId,\n        name: name,\n        location: \"auto\",\n    };\n    const finalArgs = args.transform?.bucket?.(bucketArgs) ?? bucketArgs;\n    return new cloudflare.R2Bucket(name, finalArgs);\n}\n```\n\n## v6.x Worker Versioning Resources\n\n**Worker** - Container for versions:\n```typescript\nconst worker = new cloudflare.Worker(\"api\", {accountId, name: \"api-worker\"});\nexport const workerId = worker.id;\n```\n\n**WorkerVersion** - Immutable code + config:\n```typescript\nconst version = new cloudflare.WorkerVersion(\"v1\", {\n    accountId, workerId: worker.id,\n    content: fs.readFileSync(\"./dist/worker.js\", \"utf8\"),\n    compatibilityDate: \"2025-01-01\",\n});\nexport const versionId = version.id;\n```\n\n**WorkersDeployment** - Active deployment with bindings:\n```typescript\nconst deployment = new cloudflare.WorkersDeployment(\"prod\", {\n    accountId, workerId: worker.id, versionId: version.id,\n    kvNamespaceBindings: [{name: \"MY_KV\", namespaceId: kv.id}],\n});\n```\n\n**Use:** Advanced deployments (canary, blue-green). Most apps should use `WorkerScript` (auto-versioning).\n\n---\nSee: [README.md](./README.md), [configuration.md](./configuration.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pulumi/configuration.md",
    "content": "# Resource Configuration\n\n## Workers (cloudflare.WorkerScript)\n\n```typescript\nimport * as cloudflare from \"@pulumi/cloudflare\";\nimport * as fs from \"fs\";\n\nconst worker = new cloudflare.WorkerScript(\"my-worker\", {\n    accountId: accountId,\n    name: \"my-worker\",\n    content: fs.readFileSync(\"./dist/worker.js\", \"utf8\"),\n    module: true, // ES modules\n    compatibilityDate: \"2025-01-01\",\n    compatibilityFlags: [\"nodejs_compat\"],\n    \n    // v6.x: Observability\n    logpush: true, // Enable Workers Logpush\n    tailConsumers: [{service: \"log-consumer\"}], // Stream logs to Worker\n    \n    // v6.x: Placement\n    placement: {mode: \"smart\"}, // Smart placement for latency optimization\n    \n    // Bindings\n    kvNamespaceBindings: [{name: \"MY_KV\", namespaceId: kv.id}],\n    r2BucketBindings: [{name: \"MY_BUCKET\", bucketName: bucket.name}],\n    d1DatabaseBindings: [{name: \"DB\", databaseId: db.id}],\n    queueBindings: [{name: \"MY_QUEUE\", queue: queue.id}],\n    serviceBindings: [{name: \"OTHER_SERVICE\", service: other.name}],\n    plainTextBindings: [{name: \"ENV_VAR\", text: \"value\"}],\n    secretTextBindings: [{name: \"API_KEY\", text: secret}],\n    \n    // v6.x: Advanced bindings\n    analyticsEngineBindings: [{name: \"ANALYTICS\", dataset: \"my-dataset\"}],\n    browserBinding: {name: \"BROWSER\"}, // Browser Rendering\n    aiBinding: {name: \"AI\"}, // Workers AI\n    hyperdriveBindings: [{name: \"HYPERDRIVE\", id: hyperdriveConfig.id}],\n});\n```\n\n## Workers KV (cloudflare.WorkersKvNamespace)\n\n```typescript\nconst kv = new cloudflare.WorkersKvNamespace(\"my-kv\", {\n    accountId: accountId,\n    title: \"my-kv-namespace\",\n});\n\n// Write values\nconst kvValue = new cloudflare.WorkersKvValue(\"config\", {\n    accountId: accountId,\n    namespaceId: kv.id,\n    key: \"config\",\n    value: JSON.stringify({foo: \"bar\"}),\n});\n```\n\n## R2 Buckets (cloudflare.R2Bucket)\n\n```typescript\nconst bucket = new cloudflare.R2Bucket(\"my-bucket\", {\n    accountId: accountId,\n    name: \"my-bucket\",\n    location: \"auto\", // or \"wnam\", etc.\n});\n```\n\n## D1 Databases (cloudflare.D1Database)\n\n```typescript\nconst db = new cloudflare.D1Database(\"my-db\", {accountId, name: \"my-database\"});\n\n// Migrations via wrangler\nimport * as command from \"@pulumi/command\";\nconst migration = new command.local.Command(\"d1-migration\", {\n    create: pulumi.interpolate`wrangler d1 execute ${db.name} --file ./schema.sql`,\n}, {dependsOn: [db]});\n```\n\n## Queues (cloudflare.Queue)\n\n```typescript\nconst queue = new cloudflare.Queue(\"my-queue\", {accountId, name: \"my-queue\"});\n\n// Producer\nconst producer = new cloudflare.WorkerScript(\"producer\", {\n    accountId, name: \"producer\", content: code,\n    queueBindings: [{name: \"MY_QUEUE\", queue: queue.id}],\n});\n\n// Consumer\nconst consumer = new cloudflare.WorkerScript(\"consumer\", {\n    accountId, name: \"consumer\", content: code,\n    queueConsumers: [{queue: queue.name, maxBatchSize: 10, maxRetries: 3}],\n});\n```\n\n## Pages Projects (cloudflare.PagesProject)\n\n```typescript\nconst pages = new cloudflare.PagesProject(\"my-site\", {\n    accountId, name: \"my-site\", productionBranch: \"main\",\n    buildConfig: {buildCommand: \"npm run build\", destinationDir: \"dist\"},\n    source: {\n        type: \"github\",\n        config: {owner: \"my-org\", repoName: \"my-repo\", productionBranch: \"main\"},\n    },\n    deploymentConfigs: {\n        production: {\n            environmentVariables: {NODE_VERSION: \"18\"},\n            kvNamespaces: {MY_KV: kv.id},\n            d1Databases: {DB: db.id},\n        },\n    },\n});\n```\n\n## DNS Records (cloudflare.DnsRecord)\n\n```typescript\nconst zone = cloudflare.getZone({name: \"example.com\"});\nconst record = new cloudflare.DnsRecord(\"www\", {\n    zoneId: zone.then(z => z.id), name: \"www\", type: \"A\",\n    content: \"192.0.2.1\", ttl: 3600, proxied: true,\n});\n```\n\n## Workers Domains/Routes\n\n```typescript\n// Route (pattern-based)\nconst route = new cloudflare.WorkerRoute(\"my-route\", {\n    zoneId: zoneId,\n    pattern: \"example.com/api/*\",\n    scriptName: worker.name,\n});\n\n// Domain (dedicated subdomain)\nconst domain = new cloudflare.WorkersDomain(\"my-domain\", {\n    accountId: accountId,\n    hostname: \"api.example.com\",\n    service: worker.name,\n    zoneId: zoneId,\n});\n```\n\n## Assets Configuration (v6.x)\n\nServe static assets from Workers:\n\n```typescript\nconst worker = new cloudflare.WorkerScript(\"app\", {\n    accountId: accountId,\n    name: \"my-app\",\n    content: code,\n    assets: {\n        path: \"./public\", // Local directory\n        // Assets uploaded and served from Workers\n    },\n});\n```\n\n## v6.x Versioned Deployments (Advanced)\n\nFor gradual rollouts, use 3-resource pattern:\n\n```typescript\n// 1. Worker (container for versions)\nconst worker = new cloudflare.Worker(\"api\", {\n    accountId: accountId,\n    name: \"api-worker\",\n});\n\n// 2. Version (immutable code + config)\nconst version = new cloudflare.WorkerVersion(\"v1\", {\n    accountId: accountId,\n    workerId: worker.id,\n    content: fs.readFileSync(\"./dist/worker.js\", \"utf8\"),\n    compatibilityDate: \"2025-01-01\",\n    compatibilityFlags: [\"nodejs_compat\"],\n    // Note: Bindings configured at deployment level\n});\n\n// 3. Deployment (version + bindings + traffic split)\nconst deployment = new cloudflare.WorkersDeployment(\"prod\", {\n    accountId: accountId,\n    workerId: worker.id,\n    versionId: version.id,\n    // Bindings applied to deployment\n    kvNamespaceBindings: [{name: \"MY_KV\", namespaceId: kv.id}],\n});\n```\n\n**When to use:** Blue-green deployments, canary releases, gradual rollouts  \n**When NOT to use:** Simple single-version deployments (use WorkerScript)\n\n---\nSee: [README.md](./README.md), [api.md](./api.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pulumi/gotchas.md",
    "content": "# Troubleshooting & Best Practices\n\n## Common Errors\n\n### \"No bundler/build step\" - Pulumi uploads raw code\n\n**Problem:** Worker fails with \"Cannot use import statement outside a module\"  \n**Cause:** Pulumi doesn't bundle Worker code - uploads exactly what you provide  \n**Solution:** Build Worker BEFORE Pulumi deploy\n\n```typescript\n// WRONG: Pulumi won't bundle this\nconst worker = new cloudflare.WorkerScript(\"worker\", {\n    content: fs.readFileSync(\"./src/index.ts\", \"utf8\"), // Raw TS file\n});\n\n// RIGHT: Build first, then deploy\nimport * as command from \"@pulumi/command\";\nconst build = new command.local.Command(\"build\", {\n    create: \"npm run build\",\n    dir: \"./worker\",\n});\nconst worker = new cloudflare.WorkerScript(\"worker\", {\n    content: build.stdout.apply(() => fs.readFileSync(\"./worker/dist/index.js\", \"utf8\")),\n}, {dependsOn: [build]});\n```\n\n### \"wrangler.toml not consumed\" - Config drift\n\n**Problem:** Local wrangler dev works, Pulumi deploy fails  \n**Cause:** Pulumi ignores wrangler.toml - must duplicate config  \n**Solution:** Generate wrangler.toml from Pulumi or keep synced manually\n\n```typescript\n// Pattern: Export Pulumi config to wrangler.toml\nconst workerConfig = {\n    name: \"my-worker\",\n    compatibilityDate: \"2025-01-01\",\n    compatibilityFlags: [\"nodejs_compat\"],\n};\n\nnew command.local.Command(\"generate-wrangler\", {\n    create: pulumi.interpolate`cat > wrangler.toml <<EOF\nname = \"${workerConfig.name}\"\ncompatibility_date = \"${workerConfig.compatibilityDate}\"\ncompatibility_flags = ${JSON.stringify(workerConfig.compatibilityFlags)}\nEOF`,\n});\n```\n\n### \"False no-changes detection\" - Content SHA unchanged\n\n**Problem:** Worker code updated, Pulumi says \"no changes\"  \n**Cause:** Content hash identical (whitespace/comment-only change)  \n**Solution:** Add build timestamp or version to force update\n\n```typescript\nconst version = Date.now().toString();\nconst worker = new cloudflare.WorkerScript(\"worker\", {\n    content: code,\n    plainTextBindings: [{name: \"VERSION\", text: version}], // Forces new deployment\n});\n```\n\n### \"D1 migrations don't run on pulumi up\"\n\n**Problem:** Database schema not applied after D1 database created  \n**Cause:** Pulumi creates database but doesn't run migrations  \n**Solution:** Use Command resource with dependsOn\n\n```typescript\nconst db = new cloudflare.D1Database(\"db\", {accountId, name: \"mydb\"});\n\n// Run migrations after DB created\nconst migration = new command.local.Command(\"migrate\", {\n    create: pulumi.interpolate`wrangler d1 execute ${db.name} --file ./schema.sql`,\n}, {dependsOn: [db]});\n\n// Worker depends on migrations\nconst worker = new cloudflare.WorkerScript(\"worker\", {\n    d1DatabaseBindings: [{name: \"DB\", databaseId: db.id}],\n}, {dependsOn: [migration]});\n```\n\n### \"Missing required property 'accountId'\"\n\n**Problem:** `Error: Missing required property 'accountId'`  \n**Cause:** Account ID not provided in resource configuration  \n**Solution:** Add to stack config\n\n```yaml\n# Pulumi.<stack>.yaml\nconfig:\n  cloudflare:accountId: \"abc123...\"\n```\n\n### \"Binding name mismatch\"\n\n**Problem:** Worker fails with \"env.MY_KV is undefined\"  \n**Cause:** Binding name in Pulumi != name in Worker code  \n**Solution:** Match exactly (case-sensitive)\n\n```typescript\n// Pulumi\nkvNamespaceBindings: [{name: \"MY_KV\", namespaceId: kv.id}]\n\n// Worker code\nexport default { async fetch(request, env) { await env.MY_KV.get(\"key\"); }}\n```\n\n### \"API token permissions insufficient\"\n\n**Problem:** `Error: authentication error (10000)`  \n**Cause:** Token lacks required permissions  \n**Solution:** Grant token permissions: Account.Workers Scripts:Edit, Account.Account Settings:Read\n\n### \"Resource not found after import\"\n\n**Problem:** Imported resource shows as changed on next `pulumi up`  \n**Cause:** State mismatch between actual resource and Pulumi config  \n**Solution:** Check property names/types match exactly\n\n```bash\npulumi import cloudflare:index/workerScript:WorkerScript my-worker <account_id>/<worker_name>\npulumi preview # If shows changes, adjust Pulumi code to match actual resource\n```\n\n### \"v6.x Worker versioning confusion\"\n\n**Problem:** Worker deployed but not receiving traffic  \n**Cause:** v6.x requires Worker + WorkerVersion + WorkersDeployment (3 resources)  \n**Solution:** Use WorkerScript (auto-versioning) OR full versioning pattern\n\n```typescript\n// SIMPLE: WorkerScript auto-versions (default behavior)\nconst worker = new cloudflare.WorkerScript(\"worker\", {\n    accountId, name: \"my-worker\", content: code,\n});\n\n// ADVANCED: Manual versioning for gradual rollouts (v6.x)\nconst worker = new cloudflare.Worker(\"worker\", {accountId, name: \"my-worker\"});\nconst version = new cloudflare.WorkerVersion(\"v1\", {\n    accountId, workerId: worker.id, content: code, compatibilityDate: \"2025-01-01\",\n});\nconst deployment = new cloudflare.WorkersDeployment(\"prod\", {\n    accountId, workerId: worker.id, versionId: version.id,\n});\n```\n\n## Best Practices\n\n1. **Always set compatibilityDate** - Locks Worker behavior, prevents breaking changes\n2. **Build before deploy** - Pulumi doesn't bundle; use Command resource or CI build step\n3. **Match binding names** - Case-sensitive, must match between Pulumi and Worker code\n4. **Use dependsOn for migrations** - Ensure D1 migrations run before Worker deploys\n5. **Version Worker content** - Add VERSION binding to force redeployment on content changes\n6. **Store secrets in stack config** - Use `pulumi config set --secret` for API keys\n\n## Limits\n\n| Resource | Limit | Notes |\n|----------|-------|-------|\n| Worker script size | 10 MB | Includes all dependencies, after compression |\n| Worker CPU time | 50ms (free), 30s (paid) | Per request |\n| KV keys per namespace | Unlimited | 1000 ops/sec write, 100k ops/sec read |\n| R2 storage | Unlimited | Class A ops: 1M/mo free, Class B: 10M/mo free |\n| D1 databases | 50,000 per account | Free: 10 per account, 5 GB each |\n| Queues | 10,000 per account | Free: 1M ops/day |\n| Pages projects | 500 per account | Free: 100 projects |\n| API requests | Varies by plan | ~1200 req/5min on free |\n\n## Resources\n\n- **Pulumi Registry:** https://www.pulumi.com/registry/packages/cloudflare/\n- **API Docs:** https://www.pulumi.com/registry/packages/cloudflare/api-docs/\n- **GitHub:** https://github.com/pulumi/pulumi-cloudflare\n- **Cloudflare Docs:** https://developers.cloudflare.com/\n- **Workers Docs:** https://developers.cloudflare.com/workers/\n\n---\nSee: [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/pulumi/patterns.md",
    "content": "# Architecture Patterns\n\n## Component Resources\n\n```typescript\nclass WorkerApp extends pulumi.ComponentResource {\n    constructor(name: string, args: WorkerAppArgs, opts?) {\n        super(\"custom:cloudflare:WorkerApp\", name, {}, opts);\n        const defaultOpts = {parent: this};\n\n        this.kv = new cloudflare.WorkersKvNamespace(`${name}-kv`, {accountId: args.accountId, title: `${name}-kv`}, defaultOpts);\n        this.worker = new cloudflare.WorkerScript(`${name}-worker`, {\n            accountId: args.accountId, name: `${name}-worker`, content: args.workerCode,\n            module: true, kvNamespaceBindings: [{name: \"KV\", namespaceId: this.kv.id}],\n        }, defaultOpts);\n        this.domain = new cloudflare.WorkersDomain(`${name}-domain`, {\n            accountId: args.accountId, hostname: args.domain, service: this.worker.name,\n        }, defaultOpts);\n    }\n}\n```\n\n## Full-Stack Worker App\n\n```typescript\nconst kv = new cloudflare.WorkersKvNamespace(\"cache\", {accountId, title: \"api-cache\"});\nconst db = new cloudflare.D1Database(\"db\", {accountId, name: \"app-database\"});\nconst bucket = new cloudflare.R2Bucket(\"assets\", {accountId, name: \"app-assets\"});\n\nconst apiWorker = new cloudflare.WorkerScript(\"api\", {\n    accountId, name: \"api-worker\", content: fs.readFileSync(\"./dist/api.js\", \"utf8\"),\n    module: true, kvNamespaceBindings: [{name: \"CACHE\", namespaceId: kv.id}],\n    d1DatabaseBindings: [{name: \"DB\", databaseId: db.id}],\n    r2BucketBindings: [{name: \"ASSETS\", bucketName: bucket.name}],\n});\n```\n\n## Multi-Environment Setup\n\n```typescript\nconst stack = pulumi.getStack();\nconst worker = new cloudflare.WorkerScript(`worker-${stack}`, {\n    accountId, name: `my-worker-${stack}`, content: code,\n    plainTextBindings: [{name: \"ENVIRONMENT\", text: stack}],\n});\n```\n\n## Queue-Based Processing\n\n```typescript\nconst queue = new cloudflare.Queue(\"processing-queue\", {accountId, name: \"image-processing\"});\n\n// Producer: API receives requests\nconst apiWorker = new cloudflare.WorkerScript(\"api\", {\n    accountId, name: \"api-worker\", content: apiCode,\n    queueBindings: [{name: \"PROCESSING_QUEUE\", queue: queue.id}],\n});\n\n// Consumer: Process async\nconst processorWorker = new cloudflare.WorkerScript(\"processor\", {\n    accountId, name: \"processor-worker\", content: processorCode,\n    queueConsumers: [{queue: queue.name, maxBatchSize: 10, maxRetries: 3, maxWaitTimeMs: 5000}],\n    r2BucketBindings: [{name: \"OUTPUT_BUCKET\", bucketName: outputBucket.name}],\n});\n```\n\n## Microservices with Service Bindings\n\n```typescript\nconst authWorker = new cloudflare.WorkerScript(\"auth\", {accountId, name: \"auth-service\", content: authCode});\nconst apiWorker = new cloudflare.WorkerScript(\"api\", {\n    accountId, name: \"api-service\", content: apiCode,\n    serviceBindings: [{name: \"AUTH\", service: authWorker.name}],\n});\n```\n\n## Event-Driven Architecture\n\n```typescript\nconst eventQueue = new cloudflare.Queue(\"events\", {accountId, name: \"event-bus\"});\nconst producer = new cloudflare.WorkerScript(\"producer\", {\n    accountId, name: \"api-producer\", content: producerCode,\n    queueBindings: [{name: \"EVENTS\", queue: eventQueue.id}],\n});\nconst consumer = new cloudflare.WorkerScript(\"consumer\", {\n    accountId, name: \"email-consumer\", content: consumerCode,\n    queueConsumers: [{queue: eventQueue.name, maxBatchSize: 10}],\n});\n```\n\n## v6.x Versioned Deployments (Blue-Green/Canary)\n\n```typescript\nconst worker = new cloudflare.Worker(\"api\", {accountId, name: \"api-worker\"});\nconst v1 = new cloudflare.WorkerVersion(\"v1\", {accountId, workerId: worker.id, content: fs.readFileSync(\"./dist/v1.js\", \"utf8\"), compatibilityDate: \"2025-01-01\"});\nconst v2 = new cloudflare.WorkerVersion(\"v2\", {accountId, workerId: worker.id, content: fs.readFileSync(\"./dist/v2.js\", \"utf8\"), compatibilityDate: \"2025-01-01\"});\n\n// Gradual rollout: 10% v2, 90% v1\nconst deployment = new cloudflare.WorkersDeployment(\"canary\", {\n    accountId, workerId: worker.id,\n    versions: [{versionId: v2.id, percentage: 10}, {versionId: v1.id, percentage: 90}],\n    kvNamespaceBindings: [{name: \"MY_KV\", namespaceId: kv.id}],\n});\n```\n\n**Use:** Canary releases, A/B testing, blue-green. Most apps use `WorkerScript` (auto-versioning).\n\n## Wrangler.toml Generation (Bridge IaC with Local Dev)\n\nGenerate wrangler.toml from Pulumi config to keep local dev in sync:\n\n```typescript\nimport * as command from \"@pulumi/command\";\n\nconst workerConfig = {\n    name: \"my-worker\",\n    compatibilityDate: \"2025-01-01\",\n    compatibilityFlags: [\"nodejs_compat\"],\n};\n\n// Create resources\nconst kv = new cloudflare.WorkersKvNamespace(\"kv\", {accountId, title: \"my-kv\"});\nconst db = new cloudflare.D1Database(\"db\", {accountId, name: \"my-db\"});\nconst bucket = new cloudflare.R2Bucket(\"bucket\", {accountId, name: \"my-bucket\"});\n\n// Generate wrangler.toml after resources created\nconst wranglerGen = new command.local.Command(\"gen-wrangler\", {\n    create: pulumi.interpolate`cat > wrangler.toml <<EOF\nname = \"${workerConfig.name}\"\nmain = \"src/index.ts\"\ncompatibility_date = \"${workerConfig.compatibilityDate}\"\ncompatibility_flags = ${JSON.stringify(workerConfig.compatibilityFlags)}\n\n[[kv_namespaces]]\nbinding = \"MY_KV\"\nid = \"${kv.id}\"\n\n[[d1_databases]]\nbinding = \"DB\"\ndatabase_id = \"${db.id}\"\ndatabase_name = \"${db.name}\"\n\n[[r2_buckets]]\nbinding = \"MY_BUCKET\"\nbucket_name = \"${bucket.name}\"\nEOF`,\n}, {dependsOn: [kv, db, bucket]});\n\n// Deploy worker after wrangler.toml generated\nconst worker = new cloudflare.WorkerScript(\"worker\", {\n    accountId, name: workerConfig.name, content: code,\n    compatibilityDate: workerConfig.compatibilityDate,\n    compatibilityFlags: workerConfig.compatibilityFlags,\n    kvNamespaceBindings: [{name: \"MY_KV\", namespaceId: kv.id}],\n    d1DatabaseBindings: [{name: \"DB\", databaseId: db.id}],\n    r2BucketBindings: [{name: \"MY_BUCKET\", bucketName: bucket.name}],\n}, {dependsOn: [wranglerGen]});\n```\n\n**Benefits:**\n- `wrangler dev` uses same bindings as production\n- No config drift between Pulumi and local dev\n- Single source of truth (Pulumi config)\n\n**Alternative:** Read wrangler.toml in Pulumi (reverse direction) if wrangler is source of truth\n\n## Build + Deploy Pattern\n\n```typescript\nimport * as command from \"@pulumi/command\";\nconst build = new command.local.Command(\"build\", {create: \"npm run build\", dir: \"./worker\"});\nconst worker = new cloudflare.WorkerScript(\"worker\", {\n    accountId, name: \"my-worker\",\n    content: build.stdout.apply(() => fs.readFileSync(\"./worker/dist/index.js\", \"utf8\")),\n}, {dependsOn: [build]});\n```\n\n## Content SHA Pattern (Force Updates)\n\nPrevent false \"no changes\" detections:\n\n```typescript\nconst version = Date.now().toString();\nconst worker = new cloudflare.WorkerScript(\"worker\", {\n    accountId, name: \"my-worker\", content: code,\n    plainTextBindings: [{name: \"VERSION\", text: version}], // Forces deployment\n});\n```\n\n---\nSee: [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [gotchas.md](./gotchas.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/queues/README.md",
    "content": "# Cloudflare Queues\n\nFlexible message queuing for async task processing with guaranteed at-least-once delivery and configurable batching.\n\n## Overview\n\nQueues provide:\n- At-least-once delivery guarantee\n- Push-based (Worker) and pull-based (HTTP) consumers\n- Configurable batching and retries\n- Dead Letter Queues (DLQ)\n- Delays up to 12 hours\n\n**Use cases:** Async processing, API buffering, rate limiting, event workflows, deferred jobs\n\n## Quick Start\n\n```bash\nwrangler queues create my-queue\nwrangler queues consumer add my-queue my-worker\n```\n\n```typescript\n// Producer\nawait env.MY_QUEUE.send({ userId: 123, action: 'notify' });\n\n// Consumer (with proper error handling)\nexport default {\n  async queue(batch: MessageBatch, env: Env): Promise<void> {\n    for (const msg of batch.messages) {\n      try {\n        await process(msg.body);\n        msg.ack();\n      } catch (error) {\n        msg.retry({ delaySeconds: 60 });\n      }\n    }\n  }\n};\n```\n\n## Critical Warnings\n\n**Before using Queues, understand these production mistakes:**\n\n1. **Uncaught errors retry ENTIRE batch** (not just failed message). Always use per-message try/catch.\n2. **Messages not ack'd/retry'd will auto-retry forever** until max_retries. Always explicitly handle each message.\n\nSee [gotchas.md](./gotchas.md) for detailed solutions.\n\n## Core Operations\n\n| Operation | Purpose | Limit |\n|-----------|---------|-------|\n| `send(body, options?)` | Publish message | 128 KB |\n| `sendBatch(messages)` | Bulk publish | 100 msgs/256 KB |\n| `message.ack()` | Acknowledge success | - |\n| `message.retry(options?)` | Retry with delay | - |\n| `batch.ackAll()` | Ack entire batch | - |\n\n## Architecture\n\n```\n[Producer Worker] → [Queue] → [Consumer Worker/HTTP] → [Processing]\n```\n\n- Max 10,000 queues per account\n- 5,000 msgs/second per queue\n- 4-14 day retention (configurable)\n\n## Reading Order\n\n**New to Queues?** Start here:\n1. [configuration.md](./configuration.md) - Set up queues, bindings, consumers\n2. [api.md](./api.md) - Send messages, handle batches, ack/retry patterns\n3. [patterns.md](./patterns.md) - Real-world examples and integrations\n4. [gotchas.md](./gotchas.md) - Critical warnings and troubleshooting\n\n**Task-based routing:**\n- Setup queue → [configuration.md](./configuration.md)\n- Send/receive messages → [api.md](./api.md)\n- Implement specific pattern → [patterns.md](./patterns.md)\n- Debug/troubleshoot → [gotchas.md](./gotchas.md)\n\n## In This Reference\n\n- [configuration.md](./configuration.md) - wrangler.jsonc setup, producer/consumer config, DLQ, content types\n- [api.md](./api.md) - Send/batch methods, queue handler, ack/retry rules, type-safe patterns\n- [patterns.md](./patterns.md) - Async tasks, buffering, rate limiting, D1/Workflows/DO integrations\n- [gotchas.md](./gotchas.md) - Critical batch error handling, idempotency, error classification\n\n## See Also\n\n- [workers](../workers/) - Worker runtime for producers/consumers\n- [r2](../r2/) - Process R2 event notifications via queues\n- [d1](../d1/) - Batch write to D1 from queue consumers\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/queues/api.md",
    "content": "# Queues API Reference\n\n## Producer: Send Messages\n\n```typescript\n// Basic send\nawait env.MY_QUEUE.send({ url: request.url, timestamp: Date.now() });\n\n// Options: delay (max 43200s), contentType (json|text|bytes|v8)\nawait env.MY_QUEUE.send(message, { delaySeconds: 600 });\nawait env.MY_QUEUE.send(message, { delaySeconds: 0 }); // Override queue default\n\n// Batch (up to 100 msgs or 256 KB)\nawait env.MY_QUEUE.sendBatch([\n  { body: 'msg1' },\n  { body: 'msg2' },\n  { body: 'msg3', options: { delaySeconds: 300 } }\n]);\n\n// Non-blocking with ctx.waitUntil - send continues after response\nctx.waitUntil(env.MY_QUEUE.send({ data: 'async' }));\n\n// Background tasks in queue consumer\nexport default {\n  async queue(batch: MessageBatch, env: Env, ctx: ExecutionContext): Promise<void> {\n    for (const msg of batch.messages) {\n      await processMessage(msg.body);\n      \n      // Fire-and-forget analytics (doesn't block ack)\n      ctx.waitUntil(\n        env.ANALYTICS_QUEUE.send({ messageId: msg.id, processedAt: Date.now() })\n      );\n      \n      msg.ack();\n    }\n  }\n};\n```\n\n## Consumer: Push-based (Worker)\n\n```typescript\n// Type-safe handler with ExportedHandler\ninterface Env {\n  MY_QUEUE: Queue;\n  DB: D1Database;\n}\n\nexport default {\n  async queue(batch: MessageBatch<MessageBody>, env: Env, ctx: ExecutionContext): Promise<void> {\n    // batch.queue, batch.messages.length\n    for (const msg of batch.messages) {\n      // msg.id, msg.body, msg.timestamp, msg.attempts\n      try {\n        await processMessage(msg.body);\n        msg.ack();\n      } catch (error) {\n        msg.retry({ delaySeconds: 600 });\n      }\n    }\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n**CRITICAL WARNINGS:**\n\n1. **Messages not explicitly ack'd or retry'd will auto-retry indefinitely** until `max_retries` is reached. Always call `msg.ack()` or `msg.retry()` for each message.\n\n2. **Throwing uncaught errors retries the ENTIRE batch**, not just the failed message. Always wrap individual message processing in try/catch and call `msg.retry()` explicitly per message.\n\n```typescript\n// ❌ BAD: Uncaught error retries entire batch\nasync queue(batch: MessageBatch): Promise<void> {\n  for (const msg of batch.messages) {\n    await riskyOperation(msg.body); // If this throws, entire batch retries\n    msg.ack();\n  }\n}\n\n// ✅ GOOD: Catch per message, handle individually\nasync queue(batch: MessageBatch): Promise<void> {\n  for (const msg of batch.messages) {\n    try {\n      await riskyOperation(msg.body);\n      msg.ack();\n    } catch (error) {\n      msg.retry({ delaySeconds: 60 });\n    }\n  }\n}\n```\n\n## Ack/Retry Precedence Rules\n\n1. **Per-message calls take precedence**: If you call both `msg.ack()` and `msg.retry()`, last call wins\n2. **Batch calls don't override**: `batch.ackAll()` only affects messages without explicit ack/retry\n3. **No action = automatic retry**: Messages with no explicit action retry with configured delay\n\n```typescript\nasync queue(batch: MessageBatch): Promise<void> {\n  for (const msg of batch.messages) {\n    msg.ack();        // Message marked for ack\n    msg.retry();      // Overrides ack - message will retry\n  }\n  \n  batch.ackAll();     // Only affects messages not explicitly handled above\n}\n```\n\n## Batch Operations\n\n```typescript\n// Acknowledge entire batch\ntry {\n  await bulkProcess(batch.messages);\n  batch.ackAll();\n} catch (error) {\n  batch.retryAll({ delaySeconds: 300 });\n}\n```\n\n## Exponential Backoff\n\n```typescript\nasync queue(batch: MessageBatch, env: Env): Promise<void> {\n  for (const msg of batch.messages) {\n    try {\n      await processMessage(msg.body);\n      msg.ack();\n    } catch (error) {\n      // 30s, 60s, 120s, 240s, 480s, ... up to 12h max\n      const delay = Math.min(30 * (2 ** msg.attempts), 43200);\n      msg.retry({ delaySeconds: delay });\n    }\n  }\n}\n```\n\n## Multiple Queues, Single Consumer\n\n```typescript\nexport default {\n  async queue(batch: MessageBatch, env: Env): Promise<void> {\n    switch (batch.queue) {\n      case 'high-priority': await processUrgent(batch.messages); break;\n      case 'low-priority': await processDeferred(batch.messages); break;\n      case 'email': await sendEmails(batch.messages); break;\n      default: batch.retryAll();\n    }\n  }\n};\n```\n\n## Consumer: Pull-based (HTTP)\n\n```typescript\n// Pull messages\nconst response = await fetch(\n  `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/queues/${QUEUE_ID}/messages/pull`,\n  {\n    method: 'POST',\n    headers: { 'authorization': `Bearer ${API_TOKEN}`, 'content-type': 'application/json' },\n    body: JSON.stringify({ visibility_timeout_ms: 6000, batch_size: 50 })\n  }\n);\n\nconst data = await response.json();\n\n// Acknowledge\nawait fetch(\n  `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/queues/${QUEUE_ID}/messages/ack`,\n  {\n    method: 'POST',\n    headers: { 'authorization': `Bearer ${API_TOKEN}`, 'content-type': 'application/json' },\n    body: JSON.stringify({\n      acks: [{ lease_id: msg.lease_id }],\n      retries: [{ lease_id: msg2.lease_id, delay_seconds: 600 }]\n    })\n  }\n);\n```\n\n## Interfaces\n\n```typescript\ninterface MessageBatch<Body = unknown> {\n  readonly queue: string;\n  readonly messages: Message<Body>[];\n  ackAll(): void;\n  retryAll(options?: QueueRetryOptions): void;\n}\n\ninterface Message<Body = unknown> {\n  readonly id: string;\n  readonly timestamp: Date;\n  readonly body: Body;\n  readonly attempts: number;\n  ack(): void;\n  retry(options?: QueueRetryOptions): void;\n}\n\ninterface QueueSendOptions {\n  contentType?: 'text' | 'bytes' | 'json' | 'v8';\n  delaySeconds?: number; // 0-43200\n}\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/queues/configuration.md",
    "content": "# Queues Configuration\n\n## Create Queue\n\n```bash\nwrangler queues create my-queue\nwrangler queues create my-queue --retention-period-hours=336  # 14 days\nwrangler queues create my-queue --delivery-delay-secs=300\n```\n\n## Producer Binding\n\n**wrangler.jsonc:**\n```jsonc\n{\n  \"queues\": {\n    \"producers\": [\n      {\n        \"queue\": \"my-queue-name\",\n        \"binding\": \"MY_QUEUE\",\n        \"delivery_delay\": 60  // Optional: default delay in seconds\n      }\n    ]\n  }\n}\n```\n\n## Consumer Configuration (Push-based)\n\n**wrangler.jsonc:**\n```jsonc\n{\n  \"queues\": {\n    \"consumers\": [\n      {\n        \"queue\": \"my-queue-name\",\n        \"max_batch_size\": 10,           // 1-100, default 10\n        \"max_batch_timeout\": 5,         // 0-60s, default 5\n        \"max_retries\": 3,               // default 3, max 100\n        \"dead_letter_queue\": \"my-dlq\",  // optional\n        \"retry_delay\": 300              // optional: delay retries in seconds\n      }\n    ]\n  }\n}\n```\n\n## Consumer Configuration (Pull-based)\n\n**wrangler.jsonc:**\n```jsonc\n{\n  \"queues\": {\n    \"consumers\": [\n      {\n        \"queue\": \"my-queue-name\",\n        \"type\": \"http_pull\",\n        \"visibility_timeout_ms\": 5000,  // default 30000, max 12h\n        \"max_retries\": 5,\n        \"dead_letter_queue\": \"my-dlq\"\n      }\n    ]\n  }\n}\n```\n\n## TypeScript Types\n\n```typescript\ninterface Env {\n  MY_QUEUE: Queue<MessageBody>;\n  ANALYTICS_QUEUE: Queue<AnalyticsEvent>;\n}\n\ninterface MessageBody {\n  id: string;\n  action: 'create' | 'update' | 'delete';\n  data: Record<string, any>;\n}\n\nexport default {\n  async queue(batch: MessageBatch<MessageBody>, env: Env): Promise<void> {\n    for (const msg of batch.messages) {\n      console.log(msg.body.action);\n      msg.ack();\n    }\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## Content Type Selection\n\nChoose content type based on consumer type and data requirements:\n\n| Content Type | Use When | Readable By | Supports | Size |\n|--------------|----------|-------------|----------|------|\n| `json` | Pull consumers, dashboard visibility, simple objects | All (push/pull/dashboard) | JSON-serializable types only | Medium |\n| `v8` | Push consumers only, complex JS objects | Push consumers only | Date, Map, Set, BigInt, typed arrays | Small |\n| `text` | String-only payloads | All | Strings only | Smallest |\n| `bytes` | Binary data (images, files) | All | ArrayBuffer, Uint8Array | Variable |\n\n**Decision tree:**\n1. Need to view in dashboard or use pull consumer? → Use `json`\n2. Need Date, Map, Set, or other V8 types? → Use `v8` (push consumers only)\n3. Just strings? → Use `text`\n4. Binary data? → Use `bytes`\n\n```typescript\n// JSON: Good for simple objects, pull consumers, dashboard visibility\nawait env.QUEUE.send({ id: 123, name: 'test' }, { contentType: 'json' });\n\n// V8: Good for Date, Map, Set (push consumers only)\nawait env.QUEUE.send({ \n  created: new Date(), \n  tags: new Set(['a', 'b']) \n}, { contentType: 'v8' });\n\n// Text: Simple strings\nawait env.QUEUE.send('process-user-123', { contentType: 'text' });\n\n// Bytes: Binary data\nawait env.QUEUE.send(imageBuffer, { contentType: 'bytes' });\n```\n\n**Default behavior:** If not specified, Cloudflare auto-selects `json` for JSON-serializable objects and `v8` for complex types.\n\n**IMPORTANT:** `v8` messages cannot be read by pull consumers or viewed in the dashboard. Use `json` if you need visibility or pull-based consumption.\n\n## CLI Commands\n\n```bash\n# Consumer management\nwrangler queues consumer add my-queue my-worker --batch-size=50 --max-retries=5\nwrangler queues consumer http add my-queue\nwrangler queues consumer worker remove my-queue my-worker\nwrangler queues consumer http remove my-queue\n\n# Queue operations\nwrangler queues list\nwrangler queues pause my-queue\nwrangler queues resume my-queue\nwrangler queues purge my-queue\nwrangler queues delete my-queue\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/queues/gotchas.md",
    "content": "# Queues Gotchas & Troubleshooting\n\n## CRITICAL: Top Production Mistakes\n\n### 1. \"Entire Batch Retried After Single Error\"\n\n**Problem:** Throwing uncaught error in queue handler retries the entire batch, not just the failed message  \n**Cause:** Uncaught exceptions propagate to the runtime, triggering batch-level retry  \n**Solution:** Always wrap individual message processing in try/catch and call `msg.retry()` explicitly\n\n```typescript\n// ❌ BAD: Throws error, retries entire batch\nasync queue(batch: MessageBatch): Promise<void> {\n  for (const msg of batch.messages) {\n    await riskyOperation(msg.body); // If this throws, entire batch retries\n    msg.ack();\n  }\n}\n\n// ✅ GOOD: Catch per message, handle individually\nasync queue(batch: MessageBatch): Promise<void> {\n  for (const msg of batch.messages) {\n    try {\n      await riskyOperation(msg.body);\n      msg.ack();\n    } catch (error) {\n      msg.retry({ delaySeconds: 60 });\n    }\n  }\n}\n```\n\n### 2. \"Messages Retry Forever\"\n\n**Problem:** Messages not explicitly ack'd or retry'd will auto-retry indefinitely  \n**Cause:** Runtime default behavior retries unhandled messages until `max_retries` reached  \n**Solution:** Always call `msg.ack()` or `msg.retry()` for each message. Never leave messages unhandled.\n\n```typescript\n// ❌ BAD: Skipped messages auto-retry forever\nasync queue(batch: MessageBatch): Promise<void> {\n  for (const msg of batch.messages) {\n    if (shouldProcess(msg.body)) {\n      await process(msg.body);\n      msg.ack();\n    }\n    // Missing: msg.ack() for skipped messages - they will retry!\n  }\n}\n\n// ✅ GOOD: Explicitly handle all messages\nasync queue(batch: MessageBatch): Promise<void> {\n  for (const msg of batch.messages) {\n    if (shouldProcess(msg.body)) {\n      await process(msg.body);\n      msg.ack();\n    } else {\n      msg.ack(); // Explicitly ack even if not processing\n    }\n  }\n}\n```\n\n## Common Errors\n\n### \"Duplicate Message Processing\"\n\n**Problem:** Same message processed multiple times  \n**Cause:** At-least-once delivery guarantee means duplicates are possible during retries  \n**Solution:** Design consumers to be idempotent by tracking processed message IDs in KV with expiration TTL\n\n```typescript\nasync queue(batch: MessageBatch, env: Env): Promise<void> {\n  for (const msg of batch.messages) {\n    const processed = await env.PROCESSED_KV.get(msg.id);\n    if (processed) {\n      msg.ack();\n      continue;\n    }\n    \n    await processMessage(msg.body);\n    await env.PROCESSED_KV.put(msg.id, '1', { expirationTtl: 86400 });\n    msg.ack();\n  }\n}\n```\n\n### \"Pull Consumer Can't Decode Messages\"\n\n**Problem:** Pull consumer or dashboard shows unreadable message bodies  \n**Cause:** Messages sent with `v8` content type are only decodable by Workers push consumers  \n**Solution:** Use `json` content type for pull consumers or dashboard visibility\n\n```typescript\n// Use json for pull consumers\nawait env.MY_QUEUE.send(data, { contentType: 'json' });\n\n// Use v8 only for push consumers with complex JS types\nawait env.MY_QUEUE.send({ date: new Date(), tags: new Set() }, { contentType: 'v8' });\n```\n\n### \"Messages Not Being Delivered\"\n\n**Problem:** Messages sent but consumer not processing  \n**Cause:** Queue paused, consumer not configured, or consumer errors  \n**Solution:** Check queue status with `wrangler queues list`, verify consumer configured with `wrangler queues consumer add`, and check logs with `wrangler tail`\n\n### \"High Dead Letter Queue Rate\"\n\n**Problem:** Many messages ending up in DLQ  \n**Cause:** Consumer repeatedly failing to process messages after max retries  \n**Solution:** Review consumer error logs, check external dependency availability, verify message format matches expectations, or increase retry delay\n\n## Error Classification Patterns\n\nClassify errors to decide whether to retry or DLQ:\n\n```typescript\nasync queue(batch: MessageBatch, env: Env): Promise<void> {\n  for (const msg of batch.messages) {\n    try {\n      await processMessage(msg.body);\n      msg.ack();\n    } catch (error) {\n      // Transient errors: retry with backoff\n      if (isRetryable(error)) {\n        const delay = Math.min(30 * (2 ** msg.attempts), 43200);\n        msg.retry({ delaySeconds: delay });\n      } \n      // Permanent errors: ack to avoid infinite retries\n      else {\n        console.error('Permanent error, sending to DLQ:', error);\n        await env.ERROR_LOG.put(msg.id, JSON.stringify({ msg: msg.body, error: String(error) }));\n        msg.ack(); // Prevent further retries\n      }\n    }\n  }\n}\n\nfunction isRetryable(error: unknown): boolean {\n  if (error instanceof Response) {\n    // Retry: rate limits, timeouts, server errors\n    return error.status === 429 || error.status >= 500;\n  }\n  if (error instanceof Error) {\n    // Don't retry: validation, auth, not found\n    return !error.message.includes('validation') && \n           !error.message.includes('unauthorized') &&\n           !error.message.includes('not found');\n  }\n  return false; // Unknown errors don't retry\n}\n```\n\n### \"CPU Time Exceeded in Consumer\"\n\n**Problem:** Consumer fails with CPU time limit exceeded  \n**Cause:** Consumer processing exceeding 30s default CPU time limit  \n**Solution:** Increase CPU limit in wrangler.jsonc: `{ \"limits\": { \"cpu_ms\": 300000 } }` (5 minutes max)\n\n## Content Type Decision Guide\n\n**When to use each content type:**\n\n| Content Type | Use When | Readable By | Supports |\n|--------------|----------|-------------|----------|\n| `json` (default) | Pull consumers, dashboard visibility, simple objects | All (push/pull/dashboard) | JSON-serializable types only |\n| `v8` | Push consumers only, complex JS objects | Push consumers only | Date, Map, Set, BigInt, typed arrays |\n| `text` | String-only payloads | All | Strings only |\n| `bytes` | Binary data (images, files) | All | ArrayBuffer, Uint8Array |\n\n**Decision tree:**\n1. Need to view in dashboard or use pull consumer? → Use `json`\n2. Need Date, Map, Set, or other V8 types? → Use `v8` (push consumers only)\n3. Just strings? → Use `text`\n4. Binary data? → Use `bytes`\n\n```typescript\n// Dashboard/pull: use json\nawait env.QUEUE.send({ id: 123, name: 'test' }, { contentType: 'json' });\n\n// Complex JS types (push only): use v8\nawait env.QUEUE.send({ \n  created: new Date(), \n  tags: new Set(['a', 'b']) \n}, { contentType: 'v8' });\n```\n\n## Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Max queues | 10,000 | Per account |\n| Message size | 128 KB | Maximum per message |\n| Batch size (consumer) | 100 messages | Maximum messages per batch |\n| Batch size (sendBatch) | 100 msgs or 256 KB | Whichever limit reached first |\n| Throughput | 5,000 msgs/sec | Per queue |\n| Retention | 4-14 days | Configurable retention period |\n| Max backlog | 25 GB | Maximum queue backlog size |\n| Max delay | 12 hours (43,200s) | Maximum message delay |\n| Max retries | 100 | Maximum retry attempts |\n| CPU time default | 30s | Per consumer invocation |\n| CPU time max | 300s (5 min) | Configurable via `limits.cpu_ms` |\n| Operations per message | 3 (write + read + delete) | Base cost per message |\n| Pricing | $0.40 per 1M operations | After 1M free operations |\n| Message charging | Per 64 KB chunk | Messages charged in 64 KB increments |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/queues/patterns.md",
    "content": "# Queues Patterns & Best Practices\n\n## Async Task Processing\n\n```typescript\n// Producer: Accept request, queue work\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const { userId, reportType } = await request.json();\n    await env.REPORT_QUEUE.send({ userId, reportType, requestedAt: Date.now() });\n    return Response.json({ message: 'Report queued', status: 'pending' });\n  }\n};\n\n// Consumer: Process reports\nexport default {\n  async queue(batch: MessageBatch, env: Env): Promise<void> {\n    for (const msg of batch.messages) {\n      const { userId, reportType } = msg.body;\n      const report = await generateReport(userId, reportType, env);\n      await env.REPORTS_BUCKET.put(`${userId}/${reportType}.pdf`, report);\n      msg.ack();\n    }\n  }\n};\n```\n\n## Buffering API Calls\n\n```typescript\n// Producer: Queue log entries\nctx.waitUntil(env.LOGS_QUEUE.send({\n  method: request.method,\n  url: request.url,\n  timestamp: Date.now()\n}));\n\n// Consumer: Batch write to external API\nasync queue(batch: MessageBatch, env: Env): Promise<void> {\n  const logs = batch.messages.map(m => m.body);\n  await fetch(env.LOG_ENDPOINT, { method: 'POST', body: JSON.stringify({ logs }) });\n  batch.ackAll();\n}\n```\n\n## Rate Limiting Upstream\n\n```typescript\nasync queue(batch: MessageBatch, env: Env): Promise<void> {\n  for (const msg of batch.messages) {\n    try {\n      await callRateLimitedAPI(msg.body);\n      msg.ack();\n    } catch (error) {\n      if (error.status === 429) {\n        const retryAfter = parseInt(error.headers.get('Retry-After') || '60');\n        msg.retry({ delaySeconds: retryAfter });\n      } else throw error;\n    }\n  }\n}\n```\n\n## Event-Driven Workflows\n\n```typescript\n// R2 event → Queue → Worker\nexport default {\n  async queue(batch: MessageBatch, env: Env): Promise<void> {\n    for (const msg of batch.messages) {\n      const event = msg.body;\n      if (event.action === 'PutObject') {\n        await processNewFile(event.object.key, env);\n      } else if (event.action === 'DeleteObject') {\n        await cleanupReferences(event.object.key, env);\n      }\n      msg.ack();\n    }\n  }\n};\n```\n\n## Dead Letter Queue Pattern\n\n```typescript\n// Main queue: After max_retries, goes to DLQ automatically\nexport default {\n  async queue(batch: MessageBatch, env: Env): Promise<void> {\n    for (const msg of batch.messages) {\n      try {\n        await riskyOperation(msg.body);\n        msg.ack();\n      } catch (error) {\n        console.error(`Failed after ${msg.attempts} attempts:`, error);\n      }\n    }\n  }\n};\n\n// DLQ consumer: Log and store failed messages\nexport default {\n  async queue(batch: MessageBatch, env: Env): Promise<void> {\n    for (const msg of batch.messages) {\n      await env.FAILED_KV.put(msg.id, JSON.stringify(msg.body));\n      msg.ack();\n    }\n  }\n};\n```\n\n## Priority Queues\n\nHigh priority: `max_batch_size: 5, max_batch_timeout: 1`. Low priority: `max_batch_size: 100, max_batch_timeout: 30`.\n\n## Delayed Job Processing\n\n```typescript\nawait env.EMAIL_QUEUE.send({ to, template, userId }, { delaySeconds: 3600 });\n```\n\n## Fan-out Pattern\n\n```typescript\nasync fetch(request: Request, env: Env): Promise<Response> {\n  const event = await request.json();\n  \n  // Send to multiple queues for parallel processing\n  await Promise.all([\n    env.ANALYTICS_QUEUE.send(event),\n    env.NOTIFICATIONS_QUEUE.send(event),\n    env.AUDIT_LOG_QUEUE.send(event)\n  ]);\n  \n  return Response.json({ status: 'processed' });\n}\n```\n\n## Idempotency Pattern\n\n```typescript\nasync queue(batch: MessageBatch, env: Env): Promise<void> {\n  for (const msg of batch.messages) {\n    // Check if already processed\n    const processed = await env.PROCESSED_KV.get(msg.id);\n    if (processed) {\n      msg.ack();\n      continue;\n    }\n    \n    await processMessage(msg.body);\n    await env.PROCESSED_KV.put(msg.id, '1', { expirationTtl: 86400 });\n    msg.ack();\n  }\n}\n```\n\n## Integration: D1 Batch Writes\n\n```typescript\nasync queue(batch: MessageBatch, env: Env): Promise<void> {\n  // Collect all inserts for single D1 batch\n  const statements = batch.messages.map(msg => \n    env.DB.prepare('INSERT INTO events (id, data, created) VALUES (?, ?, ?)')\n      .bind(msg.id, JSON.stringify(msg.body), Date.now())\n  );\n  \n  try {\n    await env.DB.batch(statements);\n    batch.ackAll();\n  } catch (error) {\n    console.error('D1 batch failed:', error);\n    batch.retryAll({ delaySeconds: 60 });\n  }\n}\n```\n\n## Integration: Workflows\n\n```typescript\n// Queue triggers Workflow for long-running tasks\nasync queue(batch: MessageBatch, env: Env): Promise<void> {\n  for (const msg of batch.messages) {\n    try {\n      const instance = await env.MY_WORKFLOW.create({\n        id: msg.id,\n        params: msg.body\n      });\n      console.log('Workflow started:', instance.id);\n      msg.ack();\n    } catch (error) {\n      msg.retry({ delaySeconds: 30 });\n    }\n  }\n}\n```\n\n## Integration: Durable Objects\n\n```typescript\n// Queue distributes work to Durable Objects by ID\nasync queue(batch: MessageBatch, env: Env): Promise<void> {\n  for (const msg of batch.messages) {\n    const { userId, action } = msg.body;\n    \n    // Route to user-specific DO\n    const id = env.USER_DO.idFromName(userId);\n    const stub = env.USER_DO.get(id);\n    \n    try {\n      await stub.fetch(new Request('https://do/process', {\n        method: 'POST',\n        body: JSON.stringify({ action, messageId: msg.id })\n      }));\n      msg.ack();\n    } catch (error) {\n      msg.retry({ delaySeconds: 60 });\n    }\n  }\n}\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2/README.md",
    "content": "# Cloudflare R2 Object Storage\n\nS3-compatible object storage with zero egress fees, optimized for large file storage and delivery.\n\n## Overview\n\nR2 provides:\n- S3-compatible API (Workers API + S3 REST)\n- Zero egress fees globally\n- Strong consistency for writes/deletes\n- Storage classes (Standard/Infrequent Access)\n- SSE-C encryption support\n\n**Use cases:** Media storage, backups, static assets, user uploads, data lakes\n\n## Quick Start\n\n```bash\nwrangler r2 bucket create my-bucket --location=enam\nwrangler r2 object put my-bucket/file.txt --file=./local.txt\n```\n\n```typescript\n// Upload\nawait env.MY_BUCKET.put(key, data, {\n  httpMetadata: { contentType: 'image/jpeg' }\n});\n\n// Download\nconst object = await env.MY_BUCKET.get(key);\nif (object) return new Response(object.body);\n```\n\n## Core Operations\n\n| Method | Purpose | Returns |\n|--------|---------|---------|\n| `put(key, value, options?)` | Upload object | `R2Object \\| null` |\n| `get(key, options?)` | Download object | `R2ObjectBody \\| R2Object \\| null` |\n| `head(key)` | Get metadata only | `R2Object \\| null` |\n| `delete(keys)` | Delete object(s) | `Promise<void>` |\n| `list(options?)` | List objects | `R2Objects` |\n\n## Storage Classes\n\n- **Standard**: Frequent access, low latency reads\n- **InfrequentAccess**: 30-day minimum storage, retrieval fees, lower storage cost\n\n## Event Notifications\n\nR2 integrates with Cloudflare Queues for reactive workflows:\n\n```typescript\n// wrangler.jsonc\n{\n  \"event_notifications\": [{\n    \"queue\": \"r2-notifications\",\n    \"actions\": [\"PutObject\", \"DeleteObject\"]\n  }]\n}\n\n// Consumer\nasync queue(batch: MessageBatch, env: Env) {\n  for (const message of batch.messages) {\n    const event = message.body; // { action, bucket, object, timestamps }\n    if (event.action === 'PutObject') {\n      // Process upload: thumbnail generation, virus scan, etc.\n    }\n  }\n}\n```\n\n## Reading Order\n\n**First-time users:** README → configuration.md → api.md → patterns.md  \n**Specific tasks:**\n- Setup: configuration.md\n- Client uploads: patterns.md (presigned URLs)\n- Public static site: patterns.md (public access + custom domain)\n- Processing uploads: README (event notifications) + queues reference\n- Debugging: gotchas.md\n\n## In This Reference\n\n- [configuration.md](./configuration.md) - Bindings, S3 SDK, CORS, lifecycles, token scopes\n- [api.md](./api.md) - Workers API, multipart, conditional requests, presigned URLs\n- [patterns.md](./patterns.md) - Streaming, caching, client uploads, public buckets\n- [gotchas.md](./gotchas.md) - List truncation, etag format, stream length, S3 SDK region\n\n## See Also\n\n- [workers](../workers/) - Worker runtime and fetch handlers\n- [kv](../kv/) - Metadata storage for R2 objects\n- [d1](../d1/) - Store R2 URLs in relational database\n- [queues](../queues/) - Process R2 uploads asynchronously\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2/api.md",
    "content": "# R2 API Reference\n\n## PUT (Upload)\n\n```typescript\n// Basic\nawait env.MY_BUCKET.put(key, value);\n\n// With metadata\nawait env.MY_BUCKET.put(key, value, {\n  httpMetadata: {\n    contentType: 'image/jpeg',\n    contentDisposition: 'attachment; filename=\"photo.jpg\"',\n    cacheControl: 'max-age=3600'\n  },\n  customMetadata: { userId: '123', version: '2' },\n  storageClass: 'Standard', // or 'InfrequentAccess'\n  sha256: arrayBufferOrHex, // Integrity check\n  ssecKey: arrayBuffer32bytes // SSE-C encryption\n});\n\n// Value types: ReadableStream | ArrayBuffer | string | Blob\n```\n\n## GET (Download)\n\n```typescript\nconst object = await env.MY_BUCKET.get(key);\nif (!object) return new Response('Not found', { status: 404 });\n\n// Body: arrayBuffer(), text(), json(), blob(), body (ReadableStream)\n\n// Ranged reads\nconst object = await env.MY_BUCKET.get(key, { range: { offset: 0, length: 1024 } });\n\n// Conditional GET\nconst object = await env.MY_BUCKET.get(key, { onlyIf: { etagMatches: '\"abc123\"' } });\n```\n\n## HEAD (Metadata Only)\n\n```typescript\nconst object = await env.MY_BUCKET.head(key); // Returns R2Object without body\n```\n\n## DELETE\n\n```typescript\nawait env.MY_BUCKET.delete(key);\nawait env.MY_BUCKET.delete([key1, key2, key3]); // Batch (max 1000)\n```\n## LIST\n\n```typescript\nconst listed = await env.MY_BUCKET.list({\n  limit: 1000,\n  prefix: 'photos/',\n  cursor: cursorFromPrevious,\n  delimiter: '/',\n  include: ['httpMetadata', 'customMetadata']\n});\n\n// Pagination (always use truncated flag)\nwhile (listed.truncated) {\n  const next = await env.MY_BUCKET.list({ cursor: listed.cursor });\n  listed.objects.push(...next.objects);\n  listed.truncated = next.truncated;\n  listed.cursor = next.cursor;\n}\n```\n\n## Multipart Uploads\n\n```typescript\nconst multipart = await env.MY_BUCKET.createMultipartUpload(key, {\n  httpMetadata: { contentType: 'video/mp4' }\n});\n\nconst uploadedParts: R2UploadedPart[] = [];\nfor (let i = 0; i < partCount; i++) {\n  const part = await multipart.uploadPart(i + 1, partData);\n  uploadedParts.push(part);\n}\n\nconst object = await multipart.complete(uploadedParts);\n// OR: await multipart.abort();\n\n// Resume\nconst multipart = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);\n```\n\n## Presigned URLs (S3 SDK)\n\n```typescript\nimport { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\n\nconst s3 = new S3Client({\n  region: 'auto',\n  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n  credentials: { accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY }\n});\n\nconst uploadUrl = await getSignedUrl(s3, new PutObjectCommand({ Bucket: 'my-bucket', Key: key }), { expiresIn: 3600 });\nreturn Response.json({ uploadUrl });\n```\n\n## TypeScript Interfaces\n\n```typescript\ninterface R2Bucket {\n  head(key: string): Promise<R2Object | null>;\n  get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null>;\n  put(key: string, value: ReadableStream | ArrayBuffer | string | Blob, options?: R2PutOptions): Promise<R2Object | null>;\n  delete(keys: string | string[]): Promise<void>;\n  list(options?: R2ListOptions): Promise<R2Objects>;\n  createMultipartUpload(key: string, options?: R2MultipartOptions): Promise<R2MultipartUpload>;\n  resumeMultipartUpload(key: string, uploadId: string): R2MultipartUpload;\n}\n\ninterface R2Object {\n  key: string; version: string; size: number;\n  etag: string; httpEtag: string; // httpEtag is quoted, use for headers\n  uploaded: Date; httpMetadata?: R2HTTPMetadata;\n  customMetadata?: Record<string, string>;\n  storageClass: 'Standard' | 'InfrequentAccess';\n  checksums: R2Checksums;\n  writeHttpMetadata(headers: Headers): void;\n}\n\ninterface R2ObjectBody extends R2Object {\n  body: ReadableStream; bodyUsed: boolean;\n  arrayBuffer(): Promise<ArrayBuffer>; text(): Promise<string>;\n  json<T>(): Promise<T>; blob(): Promise<Blob>;\n}\n\ninterface R2HTTPMetadata {\n  contentType?: string; contentDisposition?: string;\n  contentEncoding?: string; contentLanguage?: string;\n  cacheControl?: string; cacheExpiry?: Date;\n}\n\ninterface R2PutOptions {\n  httpMetadata?: R2HTTPMetadata | Headers;\n  customMetadata?: Record<string, string>;\n  sha256?: ArrayBuffer | string; // Only ONE checksum allowed\n  storageClass?: 'Standard' | 'InfrequentAccess';\n  ssecKey?: ArrayBuffer;\n}\n\ninterface R2GetOptions {\n  onlyIf?: R2Conditional | Headers;\n  range?: R2Range | Headers;\n  ssecKey?: ArrayBuffer;\n}\n\ninterface R2ListOptions {\n  limit?: number; prefix?: string; cursor?: string; delimiter?: string;\n  startAfter?: string; include?: ('httpMetadata' | 'customMetadata')[];\n}\n\ninterface R2Objects {\n  objects: R2Object[]; truncated: boolean;\n  cursor?: string; delimitedPrefixes: string[];\n}\n\ninterface R2Conditional {\n  etagMatches?: string; etagDoesNotMatch?: string;\n  uploadedBefore?: Date; uploadedAfter?: Date;\n}\n\ninterface R2Range { offset?: number; length?: number; suffix?: number; }\n\ninterface R2Checksums {\n  md5?: ArrayBuffer; sha1?: ArrayBuffer; sha256?: ArrayBuffer;\n  sha384?: ArrayBuffer; sha512?: ArrayBuffer;\n}\n\ninterface R2MultipartUpload {\n  key: string;\n  uploadId: string;\n  uploadPart(partNumber: number, value: ReadableStream | ArrayBuffer | string | Blob): Promise<R2UploadedPart>;\n  abort(): Promise<void>;\n  complete(uploadedParts: R2UploadedPart[]): Promise<R2Object>;\n}\n\ninterface R2UploadedPart {\n  partNumber: number;\n  etag: string;\n}\n```\n\n## CLI Operations\n\n```bash\nwrangler r2 object put my-bucket/file.txt --file=./local.txt\nwrangler r2 object get my-bucket/file.txt --file=./download.txt\nwrangler r2 object delete my-bucket/file.txt\nwrangler r2 object list my-bucket --prefix=photos/\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2/configuration.md",
    "content": "# R2 Configuration\n\n## Workers Binding\n\n**wrangler.jsonc:**\n```jsonc\n{\n  \"r2_buckets\": [\n    {\n      \"binding\": \"MY_BUCKET\",\n      \"bucket_name\": \"my-bucket-name\"\n    }\n  ]\n}\n```\n\n## TypeScript Types\n\n```typescript\ninterface Env { MY_BUCKET: R2Bucket; }\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const object = await env.MY_BUCKET.get('file.txt');\n    return new Response(object?.body);\n  }\n}\n```\n\n## S3 SDK Setup\n\n```typescript\nimport { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';\n\nconst s3 = new S3Client({\n  region: 'auto',\n  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n  credentials: {\n    accessKeyId: env.R2_ACCESS_KEY_ID,\n    secretAccessKey: env.R2_SECRET_ACCESS_KEY\n  }\n});\n\nawait s3.send(new PutObjectCommand({\n  Bucket: 'my-bucket',\n  Key: 'file.txt',\n  Body: data,\n  StorageClass: 'STANDARD' // or 'STANDARD_IA'\n}));\n```\n\n## Location Hints\n\n```bash\nwrangler r2 bucket create my-bucket --location=enam\n\n# Hints: wnam, enam, weur, eeur, apac, oc\n# Jurisdictions (override hint): --jurisdiction=eu (or fedramp)\n```\n\n## CORS Configuration\n\nCORS must be configured via S3 SDK or dashboard (not available in Workers API):\n\n```typescript\nimport { S3Client, PutBucketCorsCommand } from '@aws-sdk/client-s3';\n\nconst s3 = new S3Client({\n  region: 'auto',\n  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n  credentials: {\n    accessKeyId: env.R2_ACCESS_KEY_ID,\n    secretAccessKey: env.R2_SECRET_ACCESS_KEY\n  }\n});\n\nawait s3.send(new PutBucketCorsCommand({\n  Bucket: 'my-bucket',\n  CORSConfiguration: {\n    CORSRules: [{\n      AllowedOrigins: ['https://example.com'],\n      AllowedMethods: ['GET', 'PUT', 'HEAD'],\n      AllowedHeaders: ['*'],\n      ExposeHeaders: ['ETag'],\n      MaxAgeSeconds: 3600\n    }]\n  }\n}));\n```\n\n## Object Lifecycles\n\n```typescript\nimport { PutBucketLifecycleConfigurationCommand } from '@aws-sdk/client-s3';\n\nawait s3.send(new PutBucketLifecycleConfigurationCommand({\n  Bucket: 'my-bucket',\n  LifecycleConfiguration: {\n    Rules: [\n      {\n        ID: 'expire-old-logs',\n        Status: 'Enabled',\n        Prefix: 'logs/',\n        Expiration: { Days: 90 }\n      },\n      {\n        ID: 'transition-to-ia',\n        Status: 'Enabled',\n        Prefix: 'archives/',\n        Transitions: [{ Days: 30, StorageClass: 'STANDARD_IA' }]\n      }\n    ]\n  }\n}));\n```\n\n## API Token Scopes\n\nWhen creating R2 tokens, set minimal permissions:\n\n| Permission | Use Case |\n|------------|----------|\n| Object Read | Public serving, downloads |\n| Object Write | Uploads only |\n| Object Read & Write | Full object operations |\n| Admin Read & Write | Bucket management, CORS, lifecycles |\n\n**Best practice:** Separate tokens for Workers (read/write) vs admin tasks (CORS, lifecycles).\n\n## Event Notifications\n\n```jsonc\n// wrangler.jsonc\n{\n  \"r2_buckets\": [\n    {\n      \"binding\": \"MY_BUCKET\",\n      \"bucket_name\": \"my-bucket\",\n      \"event_notifications\": [\n        {\n          \"queue\": \"r2-events\",\n          \"actions\": [\"PutObject\", \"DeleteObject\", \"CompleteMultipartUpload\"]\n        }\n      ]\n    }\n  ],\n  \"queues\": {\n    \"producers\": [{ \"binding\": \"R2_EVENTS\", \"queue\": \"r2-events\" }],\n    \"consumers\": [{ \"queue\": \"r2-events\", \"max_batch_size\": 10 }]\n  }\n}\n```\n\n## Bucket Management\n\n```bash\nwrangler r2 bucket create my-bucket --location=enam --storage-class=Standard\nwrangler r2 bucket list\nwrangler r2 bucket info my-bucket\nwrangler r2 bucket delete my-bucket  # Must be empty\nwrangler r2 bucket update-storage-class my-bucket --storage-class=InfrequentAccess\n\n# Public bucket via dashboard\nwrangler r2 bucket domain add my-bucket --domain=files.example.com\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2/gotchas.md",
    "content": "# R2 Gotchas & Troubleshooting\n\n## List Truncation\n\n```typescript\n// ❌ WRONG: Don't compare object count when using include\nwhile (listed.objects.length < options.limit) { ... }\n\n// ✅ CORRECT: Always use truncated property\nwhile (listed.truncated) {\n  const next = await env.MY_BUCKET.list({ cursor: listed.cursor });\n  // ...\n}\n```\n\n**Reason:** `include` with metadata may return fewer objects per page to fit metadata.\n\n## ETag Format\n\n```typescript\n// ❌ WRONG: Using etag (unquoted) in headers\nheaders.set('etag', object.etag); // Missing quotes\n\n// ✅ CORRECT: Use httpEtag (quoted)\nheaders.set('etag', object.httpEtag);\n```\n\n## Checksum Limits\n\nOnly ONE checksum algorithm allowed per PUT:\n\n```typescript\n// ❌ WRONG: Multiple checksums\nawait env.MY_BUCKET.put(key, data, { md5: hash1, sha256: hash2 }); // Error\n\n// ✅ CORRECT: Pick one\nawait env.MY_BUCKET.put(key, data, { sha256: hash });\n```\n\n## Multipart Requirements\n\n- All parts must be uniform size (except last part)\n- Part numbers start at 1 (not 0)\n- Uncompleted uploads auto-abort after 7 days\n- `resumeMultipartUpload` doesn't validate uploadId existence\n\n## Conditional Operations\n\n```typescript\n// Precondition failure returns object WITHOUT body\nconst object = await env.MY_BUCKET.get(key, {\n  onlyIf: { etagMatches: '\"wrong\"' }\n});\n\n// Check for body, not just null\nif (!object) return new Response('Not found', { status: 404 });\nif (!object.body) return new Response(null, { status: 304 }); // Precondition failed\n```\n\n## Key Validation\n\n```typescript\n// ❌ DANGEROUS: Path traversal\nconst key = url.pathname.slice(1); // Could be ../../../etc/passwd\nawait env.MY_BUCKET.get(key);\n\n// ✅ SAFE: Validate keys\nif (!key || key.includes('..') || key.startsWith('/')) {\n  return new Response('Invalid key', { status: 400 });\n}\n```\n\n## Storage Class Pitfalls\n\n- InfrequentAccess: 30-day minimum billing (even if deleted early)\n- Can't transition IA → Standard via lifecycle (use S3 CopyObject)\n- Retrieval fees apply for IA reads\n\n## Stream Length Requirement\n\n```typescript\n// ❌ WRONG: Streaming unknown length fails silently\nconst response = await fetch(url);\nawait env.MY_BUCKET.put(key, response.body); // May fail without error\n\n// ✅ CORRECT: Buffer or use Content-Length\nconst data = await response.arrayBuffer();\nawait env.MY_BUCKET.put(key, data);\n\n// OR: Pass Content-Length if known\nconst object = await env.MY_BUCKET.put(key, request.body, {\n  httpMetadata: {\n    contentLength: parseInt(request.headers.get('content-length') || '0')\n  }\n});\n```\n\n**Reason:** R2 requires known length for streams. Unknown length may cause silent truncation.\n\n## S3 SDK Region Configuration\n\n```typescript\n// ❌ WRONG: Missing region breaks ALL S3 SDK calls\nconst s3 = new S3Client({\n  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n  credentials: { ... }\n});\n\n// ✅ CORRECT: MUST set region='auto'\nconst s3 = new S3Client({\n  region: 'auto', // REQUIRED\n  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n  credentials: { ... }\n});\n```\n\n**Reason:** S3 SDK requires region. R2 uses 'auto' as placeholder.\n\n## Local Development Limits\n\n```typescript\n// ❌ Miniflare/wrangler dev: Limited R2 support\n// - No multipart uploads\n// - No presigned URLs (requires S3 SDK + network)\n// - Memory-backed storage (lost on restart)\n\n// ✅ Use remote bindings for full features\nwrangler dev --remote\n\n// OR: Conditional logic\nif (env.ENVIRONMENT === 'development') {\n  // Fallback for local dev\n} else {\n  // Full R2 features\n}\n```\n\n## Presigned URL Expiry\n\n```typescript\n// ❌ WRONG: URL expires but no client validation\nconst url = await getSignedUrl(s3, command, { expiresIn: 60 });\n// 61 seconds later: 403 Forbidden\n\n// ✅ CORRECT: Return expiry to client\nreturn Response.json({\n  uploadUrl: url,\n  expiresAt: new Date(Date.now() + 60000).toISOString()\n});\n```\n\n## Limits\n\n| Limit | Value |\n|-------|-------|\n| Object size | 5 TB |\n| Multipart part count | 10,000 |\n| Multipart part min size | 5 MB (except last) |\n| Batch delete | 1,000 keys |\n| List limit | 1,000 per request |\n| Key size | 1024 bytes |\n| Custom metadata | 2 KB per object |\n| Presigned URL max expiry | 7 days |\n\n## Common Errors\n\n### \"Stream upload failed\" / Silent Truncation\n\n**Cause:** Stream length unknown or Content-Length missing  \n**Solution:** Buffer data or pass explicit Content-Length\n\n### \"Invalid credentials\" / S3 SDK\n\n**Cause:** Missing `region: 'auto'` in S3Client config  \n**Solution:** Always set `region: 'auto'` for R2\n\n### \"Object not found\"\n\n**Cause:** Object key doesn't exist or was deleted  \n**Solution:** Verify object key correct, check if object was deleted, ensure bucket correct\n\n### \"List compatibility error\"\n\n**Cause:** Missing or old compatibility_date, or flag not enabled  \n**Solution:** Set `compatibility_date >= 2022-08-04` or enable `r2_list_honor_include` flag\n\n### \"Multipart upload failed\"\n\n**Cause:** Part sizes not uniform or incorrect part number  \n**Solution:** Ensure uniform size except final part, verify part numbers start at 1\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2/patterns.md",
    "content": "# R2 Patterns & Best Practices\n\n## Streaming Large Files\n\n```typescript\nconst object = await env.MY_BUCKET.get(key);\nif (!object) return new Response('Not found', { status: 404 });\n\nconst headers = new Headers();\nobject.writeHttpMetadata(headers);\nheaders.set('etag', object.httpEtag);\n\nreturn new Response(object.body, { headers });\n```\n\n## Conditional GET (304 Not Modified)\n\n```typescript\nconst ifNoneMatch = request.headers.get('if-none-match');\nconst object = await env.MY_BUCKET.get(key, {\n  onlyIf: { etagDoesNotMatch: ifNoneMatch?.replace(/\"/g, '') || '' }\n});\n\nif (!object) return new Response('Not found', { status: 404 });\nif (!object.body) return new Response(null, { status: 304, headers: { 'etag': object.httpEtag } });\n\nreturn new Response(object.body, { headers: { 'etag': object.httpEtag } });\n```\n\n## Upload with Validation\n\n```typescript\nconst key = url.pathname.slice(1);\nif (!key || key.includes('..')) return new Response('Invalid key', { status: 400 });\n\nconst object = await env.MY_BUCKET.put(key, request.body, {\n  httpMetadata: { contentType: request.headers.get('content-type') || 'application/octet-stream' },\n  customMetadata: { uploadedAt: new Date().toISOString(), ip: request.headers.get('cf-connecting-ip') || 'unknown' }\n});\n\nreturn Response.json({ key: object.key, size: object.size, etag: object.httpEtag });\n```\n\n## Multipart with Progress\n\n```typescript\nconst PART_SIZE = 5 * 1024 * 1024; // 5MB\nconst partCount = Math.ceil(file.size / PART_SIZE);\nconst multipart = await env.MY_BUCKET.createMultipartUpload(key, { httpMetadata: { contentType: file.type } });\n\nconst uploadedParts: R2UploadedPart[] = [];\ntry {\n  for (let i = 0; i < partCount; i++) {\n    const start = i * PART_SIZE;\n    const part = await multipart.uploadPart(i + 1, file.slice(start, start + PART_SIZE));\n    uploadedParts.push(part);\n    onProgress?.(Math.round(((i + 1) / partCount) * 100));\n  }\n  return await multipart.complete(uploadedParts);\n} catch (error) {\n  await multipart.abort();\n  throw error;\n}\n```\n\n## Batch Delete\n\n```typescript\nasync function deletePrefix(prefix: string, env: Env) {\n  let cursor: string | undefined;\n  let truncated = true;\n\n  while (truncated) {\n    const listed = await env.MY_BUCKET.list({ prefix, limit: 1000, cursor });\n    if (listed.objects.length > 0) {\n      await env.MY_BUCKET.delete(listed.objects.map(o => o.key));\n    }\n    truncated = listed.truncated;\n    cursor = listed.cursor;\n  }\n}\n```\n\n## Checksum Validation & Storage Transitions\n\n```typescript\n// Upload with checksum\nconst hash = await crypto.subtle.digest('SHA-256', data);\nawait env.MY_BUCKET.put(key, data, { sha256: hash });\n\n// Transition storage class (requires S3 SDK)\nimport { S3Client, CopyObjectCommand } from '@aws-sdk/client-s3';\nawait s3.send(new CopyObjectCommand({\n  Bucket: 'my-bucket', Key: key,\n  CopySource: `/my-bucket/${key}`,\n  StorageClass: 'STANDARD_IA'\n}));\n```\n\n## Client-Side Uploads (Presigned URLs)\n\n```typescript\nimport { S3Client } from '@aws-sdk/client-s3';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport { PutObjectCommand } from '@aws-sdk/client-s3';\n\n// Worker: Generate presigned upload URL\nconst s3 = new S3Client({\n  region: 'auto',\n  endpoint: `https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com`,\n  credentials: { accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY }\n});\n\nconst url = await getSignedUrl(s3, new PutObjectCommand({ Bucket: 'my-bucket', Key: key }), { expiresIn: 3600 });\nreturn Response.json({ uploadUrl: url });\n\n// Client: Upload directly\nconst { uploadUrl } = await fetch('/api/upload-url').then(r => r.json());\nawait fetch(uploadUrl, { method: 'PUT', body: file });\n```\n\n## Caching with Cache API\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n    const cache = caches.default;\n    const url = new URL(request.url);\n    const cacheKey = new Request(url.toString(), request);\n\n    // Check cache first\n    let response = await cache.match(cacheKey);\n    if (response) return response;\n\n    // Fetch from R2\n    const key = url.pathname.slice(1);\n    const object = await env.MY_BUCKET.get(key);\n    if (!object) return new Response('Not found', { status: 404 });\n\n    const headers = new Headers();\n    object.writeHttpMetadata(headers);\n    headers.set('etag', object.httpEtag);\n    headers.set('cache-control', 'public, max-age=31536000, immutable');\n\n    response = new Response(object.body, { headers });\n\n    // Cache for subsequent requests\n    ctx.waitUntil(cache.put(cacheKey, response.clone()));\n\n    return response;\n  }\n};\n```\n\n## Public Bucket with Custom Domain\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // CORS preflight\n    if (request.method === 'OPTIONS') {\n      return new Response(null, {\n        headers: {\n          'access-control-allow-origin': '*',\n          'access-control-allow-methods': 'GET, HEAD',\n          'access-control-max-age': '86400'\n        }\n      });\n    }\n\n    const key = new URL(request.url).pathname.slice(1);\n    if (!key) return Response.redirect('/index.html', 302);\n\n    const object = await env.MY_BUCKET.get(key);\n    if (!object) return new Response('Not found', { status: 404 });\n\n    const headers = new Headers();\n    object.writeHttpMetadata(headers);\n    headers.set('etag', object.httpEtag);\n    headers.set('access-control-allow-origin', '*');\n    headers.set('cache-control', 'public, max-age=31536000, immutable');\n\n    return new Response(object.body, { headers });\n  }\n};\n```\n\n## r2.dev Public URLs\n\nEnable r2.dev in dashboard for simple public access: `https://pub-${hashId}.r2.dev/${key}`  \nOr add custom domain via dashboard: `https://files.example.com/${key}`\n\n**Limitations:** No auth, bucket-level CORS, no cache override.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2-data-catalog/README.md",
    "content": "# Cloudflare R2 Data Catalog Skill Reference\n\nExpert guidance for Cloudflare R2 Data Catalog - Apache Iceberg catalog built into R2 buckets.\n\n## Reading Order\n\n**New to R2 Data Catalog?** Start here:\n1. Read \"What is R2 Data Catalog?\" and \"When to Use\" below\n2. [configuration.md](configuration.md) - Enable catalog, create tokens\n3. [patterns.md](patterns.md) - PyIceberg setup and common patterns\n4. [api.md](api.md) - REST API reference as needed\n5. [gotchas.md](gotchas.md) - Troubleshooting when issues arise\n\n**Quick reference?** Jump to:\n- [Enable catalog on bucket](configuration.md#enable-catalog-on-bucket)\n- [PyIceberg connection pattern](patterns.md#pyiceberg-connection-pattern)\n- [Permission errors](gotchas.md#permission-errors)\n\n## What is R2 Data Catalog?\n\nR2 Data Catalog is a **managed Apache Iceberg REST catalog** built directly into R2 buckets. It provides:\n\n- **Apache Iceberg tables** - ACID transactions, schema evolution, time-travel queries\n- **Zero-egress costs** - Query from any cloud/region without data transfer fees\n- **Standard REST API** - Works with Spark, PyIceberg, Snowflake, Trino, DuckDB\n- **No infrastructure** - Fully managed, no catalog servers to run\n- **Public beta** - Available to all R2 subscribers, no extra cost beyond R2 storage\n\n### What is Apache Iceberg?\n\nOpen table format for analytics datasets in object storage. Features:\n- **ACID transactions** - Safe concurrent reads/writes\n- **Metadata optimization** - Fast queries without full scans\n- **Schema evolution** - Add/rename/delete columns without rewrites\n- **Time-travel** - Query historical snapshots\n- **Partitioning** - Organize data for efficient queries\n\n## When to Use\n\n**Use R2 Data Catalog for:**\n- **Log analytics** - Store and query application/system logs\n- **Data lakes/warehouses** - Analytical datasets queried by multiple engines\n- **BI pipelines** - Aggregate data for dashboards and reports\n- **Multi-cloud analytics** - Share data across clouds without egress fees\n- **Time-series data** - Event streams, metrics, sensor data\n\n**Don't use for:**\n- **Transactional workloads** - Use D1 or external database instead\n- **Sub-second latency** - Iceberg optimized for batch/analytical queries\n- **Small datasets (<1GB)** - Setup overhead not worth it\n- **Unstructured data** - Store files directly in R2, not as Iceberg tables\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────┐\n│  Query Engines                                  │\n│  (PyIceberg, Spark, Trino, Snowflake, DuckDB)  │\n└────────────────┬────────────────────────────────┘\n                 │\n                 │ REST API (OAuth2 token)\n                 ▼\n┌─────────────────────────────────────────────────┐\n│  R2 Data Catalog (Managed Iceberg REST Catalog)│\n│  • Namespace/table metadata                     │\n│  • Transaction coordination                     │\n│  • Snapshot management                          │\n└────────────────┬────────────────────────────────┘\n                 │\n                 │ Vended credentials\n                 ▼\n┌─────────────────────────────────────────────────┐\n│  R2 Bucket Storage                              │\n│  • Parquet data files                           │\n│  • Metadata files                               │\n│  • Manifest files                               │\n└─────────────────────────────────────────────────┘\n```\n\n**Key concepts:**\n- **Catalog URI** - REST endpoint for catalog operations (e.g., `https://<account-id>.r2.cloudflarestorage.com/iceberg/<bucket>`)\n- **Warehouse** - Logical grouping of tables (typically same as bucket name)\n- **Namespace** - Schema/database containing tables (e.g., `logs`, `analytics`)\n- **Table** - Iceberg table with schema, data files, snapshots\n- **Vended credentials** - Temporary S3 credentials catalog provides for data access\n\n## Limits\n\n| Resource | Limit | Notes |\n|----------|-------|-------|\n| Namespaces per catalog | No hard limit | Organize tables logically |\n| Tables per namespace | <10,000 recommended | Performance degrades beyond this |\n| Files per table | <100,000 recommended | Run compaction regularly |\n| Snapshots per table | Configurable retention | Expire >7 days old |\n| Partitions per table | 100-1,000 optimal | Too many = slow metadata ops |\n| Table size | Same as R2 bucket | 10GB-10TB+ common |\n| API rate limits | Standard R2 API limits | Shared with R2 storage operations |\n| Target file size | 128-512 MB | After compaction |\n\n## Current Status\n\n**Public Beta** (as of Jan 2026)\n- Available to all R2 subscribers\n- No extra cost beyond standard R2 storage/operations\n- Production-ready, but breaking changes possible\n- Supports: namespaces, tables, snapshots, compaction, time-travel, table maintenance\n\n## Decision Tree: Is R2 Data Catalog Right For You?\n\n```\nStart → Need analytics on object storage data?\n         │\n         ├─ No → Use R2 directly for object storage\n         │\n         └─ Yes → Dataset >1GB with structured schema?\n                  │\n                  ├─ No → Too small, use R2 + ad-hoc queries\n                  │\n                  └─ Yes → Need ACID transactions or schema evolution?\n                           │\n                           ├─ No → Consider simpler solutions (Parquet on R2)\n                           │\n                           └─ Yes → Need multi-cloud/multi-tool access?\n                                    │\n                                    ├─ No → D1 or external DB may be simpler\n                                    │\n                                    └─ Yes → ✅ Use R2 Data Catalog\n```\n\n**Quick check:** If you answer \"yes\" to all:\n- Dataset >1GB and growing\n- Structured/tabular data (logs, events, metrics)\n- Multiple query tools or cloud environments\n- Need versioning, schema changes, or concurrent access\n\n→ R2 Data Catalog is a good fit.\n\n## In This Reference\n\n- **[configuration.md](configuration.md)** - Enable catalog, create API tokens, connect clients\n- **[api.md](api.md)** - REST endpoints, operations, maintenance\n- **[patterns.md](patterns.md)** - PyIceberg examples, common use cases\n- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations\n\n## See Also\n\n- [Cloudflare R2 Data Catalog Docs](https://developers.cloudflare.com/r2/data-catalog/)\n- [Apache Iceberg Docs](https://iceberg.apache.org/)\n- [PyIceberg Docs](https://py.iceberg.apache.org/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2-data-catalog/api.md",
    "content": "# API Reference\n\nR2 Data Catalog exposes standard [Apache Iceberg REST Catalog API](https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml).\n\n## Quick Reference\n\n**Most common operations:**\n\n| Task | PyIceberg Code |\n|------|----------------|\n| Connect | `RestCatalog(name=\"r2\", warehouse=bucket, uri=uri, token=token)` |\n| List namespaces | `catalog.list_namespaces()` |\n| Create namespace | `catalog.create_namespace(\"logs\")` |\n| Create table | `catalog.create_table((\"ns\", \"table\"), schema=schema)` |\n| Load table | `catalog.load_table((\"ns\", \"table\"))` |\n| Append data | `table.append(pyarrow_table)` |\n| Query data | `table.scan().to_pandas()` |\n| Compact files | `table.rewrite_data_files(target_file_size_bytes=128*1024*1024)` |\n| Expire snapshots | `table.expire_snapshots(older_than=timestamp_ms, retain_last=10)` |\n\n## REST Endpoints\n\nBase: `https://<account-id>.r2.cloudflarestorage.com/iceberg/<bucket-name>`\n\n| Operation | Method | Path |\n|-----------|--------|------|\n| Catalog config | GET | `/v1/config` |\n| List namespaces | GET | `/v1/namespaces` |\n| Create namespace | POST | `/v1/namespaces` |\n| Delete namespace | DELETE | `/v1/namespaces/{ns}` |\n| List tables | GET | `/v1/namespaces/{ns}/tables` |\n| Create table | POST | `/v1/namespaces/{ns}/tables` |\n| Load table | GET | `/v1/namespaces/{ns}/tables/{table}` |\n| Update table | POST | `/v1/namespaces/{ns}/tables/{table}` |\n| Delete table | DELETE | `/v1/namespaces/{ns}/tables/{table}` |\n| Rename table | POST | `/v1/tables/rename` |\n\n**Authentication:** Bearer token in header: `Authorization: Bearer <token>`\n\n## PyIceberg Client API\n\nMost users use PyIceberg, not raw REST.\n\n### Connection\n\n```python\nfrom pyiceberg.catalog.rest import RestCatalog\n\ncatalog = RestCatalog(\n    name=\"my_catalog\",\n    warehouse=\"<bucket-name>\",\n    uri=\"<catalog-uri>\",\n    token=\"<api-token>\",\n)\n```\n\n### Namespace Operations\n\n```python\nfrom pyiceberg.exceptions import NamespaceAlreadyExistsError\n\nnamespaces = catalog.list_namespaces()  # [('default',), ('logs',)]\ncatalog.create_namespace(\"logs\", properties={\"owner\": \"team\"})\ncatalog.drop_namespace(\"logs\")  # Must be empty\n```\n\n### Table Operations\n\n```python\nfrom pyiceberg.schema import Schema\nfrom pyiceberg.types import NestedField, StringType, IntegerType\n\nschema = Schema(\n    NestedField(1, \"id\", IntegerType(), required=True),\n    NestedField(2, \"name\", StringType(), required=False),\n)\ntable = catalog.create_table((\"logs\", \"app_logs\"), schema=schema)\ntables = catalog.list_tables(\"logs\")\ntable = catalog.load_table((\"logs\", \"app_logs\"))\ncatalog.rename_table((\"logs\", \"old\"), (\"logs\", \"new\"))\n```\n\n### Data Operations\n\n```python\nimport pyarrow as pa\n\ndata = pa.table({\"id\": [1, 2], \"name\": [\"Alice\", \"Bob\"]})\ntable.append(data)\ntable.overwrite(data)\n\n# Read with filters\nscan = table.scan(row_filter=\"id > 100\", selected_fields=[\"id\", \"name\"])\ndf = scan.to_pandas()\n```\n\n### Schema Evolution\n\n```python\nfrom pyiceberg.types import IntegerType, LongType\n\nwith table.update_schema() as update:\n    update.add_column(\"user_id\", IntegerType(), doc=\"User ID\")\n    update.rename_column(\"msg\", \"message\")\n    update.delete_column(\"old_field\")\n    update.update_column(\"id\", field_type=LongType())  # int→long only\n```\n\n### Time-Travel\n\n```python\nfrom datetime import datetime, timedelta\n\n# Query specific snapshot or timestamp\nscan = table.scan(snapshot_id=table.snapshots()[-2].snapshot_id)\nyesterday_ms = int((datetime.now() - timedelta(days=1)).timestamp() * 1000)\nscan = table.scan(as_of_timestamp=yesterday_ms)\n```\n\n### Partitioning\n\n```python\nfrom pyiceberg.partitioning import PartitionSpec, PartitionField\nfrom pyiceberg.transforms import DayTransform\nfrom pyiceberg.types import TimestampType\n\npartition_spec = PartitionSpec(\n    PartitionField(source_id=1, field_id=1000, transform=DayTransform(), name=\"day\")\n)\ntable = catalog.create_table((\"events\", \"actions\"), schema=schema, partition_spec=partition_spec)\nscan = table.scan(row_filter=\"day = '2026-01-27'\")  # Prunes partitions\n```\n\n## Table Maintenance\n\n### Compaction\n\n```python\nfiles = table.scan().plan_files()\navg_mb = sum(f.file_size_in_bytes for f in files) / len(files) / (1024**2)\nprint(f\"Files: {len(files)}, Avg: {avg_mb:.1f} MB\")\n\ntable.rewrite_data_files(target_file_size_bytes=128 * 1024 * 1024)\n```\n\n**When:** Avg <10MB or >1000 files. **Frequency:** High-write daily, medium weekly.\n\n### Snapshot Expiration\n\n```python\nfrom datetime import datetime, timedelta\n\nseven_days_ms = int((datetime.now() - timedelta(days=7)).timestamp() * 1000)\ntable.expire_snapshots(older_than=seven_days_ms, retain_last=10)\n```\n\n**Retention:** Production 7-30d, dev 1-7d, audit 90+d.\n\n### Orphan Cleanup\n\n```python\nthree_days_ms = int((datetime.now() - timedelta(days=3)).timestamp() * 1000)\ntable.delete_orphan_files(older_than=three_days_ms)\n```\n\n⚠️ Always expire snapshots first, use 3+ day threshold, run during low traffic.\n\n### Full Maintenance\n\n```python\n# Compact → Expire → Cleanup (in order)\nif len(table.scan().plan_files()) > 1000:\n    table.rewrite_data_files(target_file_size_bytes=128 * 1024 * 1024)\nseven_days_ms = int((datetime.now() - timedelta(days=7)).timestamp() * 1000)\ntable.expire_snapshots(older_than=seven_days_ms, retain_last=10)\nthree_days_ms = int((datetime.now() - timedelta(days=3)).timestamp() * 1000)\ntable.delete_orphan_files(older_than=three_days_ms)\n```\n\n## Metadata Inspection\n\n```python\ntable = catalog.load_table((\"logs\", \"app_logs\"))\nprint(table.schema())\nprint(table.current_snapshot())\nprint(table.properties)\nprint(f\"Files: {len(table.scan().plan_files())}\")\n```\n\n## Error Codes\n\n| Code | Meaning | Common Causes |\n|------|---------|---------------|\n| 401 | Unauthorized | Invalid/missing token |\n| 404 | Not Found | Catalog not enabled, namespace/table missing |\n| 409 | Conflict | Already exists, concurrent update |\n| 422 | Validation | Invalid schema, incompatible type |\n\nSee [gotchas.md](gotchas.md) for detailed troubleshooting.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2-data-catalog/configuration.md",
    "content": "# Configuration\n\nHow to enable R2 Data Catalog and configure authentication.\n\n## Prerequisites\n\n- Cloudflare account with [R2 subscription](https://developers.cloudflare.com/r2/pricing/)\n- R2 bucket created\n- Access to Cloudflare dashboard or Wrangler CLI\n\n## Enable Catalog on Bucket\n\nChoose one method:\n\n### Via Wrangler (Recommended)\n\n```bash\nnpx wrangler r2 bucket catalog enable <BUCKET_NAME>\n```\n\n**Output:**\n```\n✅ Data Catalog enabled for bucket 'my-bucket'\n   Catalog URI: https://<account-id>.r2.cloudflarestorage.com/iceberg/my-bucket\n   Warehouse: my-bucket\n```\n\n### Via Dashboard\n\n1. Navigate to **R2** → Select your bucket → **Settings** tab\n2. Scroll to \"R2 Data Catalog\" section → Click **Enable**\n3. Note the **Catalog URI** and **Warehouse name** shown\n\n**Result:**\n- Catalog URI: `https://<account-id>.r2.cloudflarestorage.com/iceberg/<bucket-name>`\n- Warehouse: `<bucket-name>` (same as bucket name)\n\n### Via API (Programmatic)\n\n```bash\ncurl -X POST \\\n  \"https://api.cloudflare.com/client/v4/accounts/<account-id>/r2/buckets/<bucket>/catalog\" \\\n  -H \"Authorization: Bearer <api-token>\" \\\n  -H \"Content-Type: application/json\"\n```\n\n**Response:**\n```json\n{\n  \"result\": {\n    \"catalog_uri\": \"https://<account-id>.r2.cloudflarestorage.com/iceberg/<bucket>\",\n    \"warehouse\": \"<bucket>\"\n  },\n  \"success\": true\n}\n```\n\n## Check Catalog Status\n\n```bash\nnpx wrangler r2 bucket catalog status <BUCKET_NAME>\n```\n\n**Output:**\n```\nCatalog Status: enabled\nCatalog URI: https://<account-id>.r2.cloudflarestorage.com/iceberg/my-bucket\nWarehouse: my-bucket\n```\n\n## Disable Catalog (If Needed)\n\n```bash\nnpx wrangler r2 bucket catalog disable <BUCKET_NAME>\n```\n\n⚠️ **Warning:** Disabling does NOT delete tables/data. Files remain in bucket. Metadata becomes inaccessible until re-enabled.\n\n## API Token Creation\n\nR2 Data Catalog requires API token with **both** R2 Storage + R2 Data Catalog permissions.\n\n### Dashboard Method (Recommended)\n\n1. Go to **R2** → **Manage R2 API Tokens** → **Create API Token**\n2. Select permission level:\n   - **Admin Read & Write** - Full catalog + storage access (read/write)\n   - **Admin Read only** - Read-only access (for query engines)\n3. Copy token value immediately (shown only once)\n\n**Permission groups included:**\n- `Workers R2 Data Catalog Write` (or Read)\n- `Workers R2 Storage Bucket Item Write` (or Read)\n\n### API Method (Programmatic)\n\nUse Cloudflare API to create tokens programmatically. Required permissions:\n- `Workers R2 Data Catalog Write` (or Read)\n- `Workers R2 Storage Bucket Item Write` (or Read)\n\n## Client Configuration\n\n### PyIceberg\n\n```python\nfrom pyiceberg.catalog.rest import RestCatalog\n\ncatalog = RestCatalog(\n    name=\"my_catalog\",\n    warehouse=\"<bucket-name>\",           # Same as bucket name\n    uri=\"<catalog-uri>\",                 # From enable command\n    token=\"<api-token>\",                 # From token creation\n)\n```\n\n**Full example with credentials:**\n```python\nimport os\nfrom pyiceberg.catalog.rest import RestCatalog\n\n# Store credentials in environment variables\nWAREHOUSE = os.getenv(\"R2_WAREHOUSE\")      # e.g., \"my-bucket\"\nCATALOG_URI = os.getenv(\"R2_CATALOG_URI\")  # e.g., \"https://abc123.r2.cloudflarestorage.com/iceberg/my-bucket\"\nTOKEN = os.getenv(\"R2_TOKEN\")              # API token\n\ncatalog = RestCatalog(\n    name=\"r2_catalog\",\n    warehouse=WAREHOUSE,\n    uri=CATALOG_URI,\n    token=TOKEN,\n)\n\n# Test connection\nprint(catalog.list_namespaces())\n```\n\n### Spark / Trino / DuckDB\n\nSee [patterns.md](patterns.md) for integration examples with other query engines.\n\n## Connection String Format\n\nFor quick reference:\n\n```\nCatalog URI:  https://<account-id>.r2.cloudflarestorage.com/iceberg/<bucket>\nWarehouse:    <bucket-name>\nToken:        <r2-api-token>\n```\n\n**Where to find values:**\n\n| Value | Source |\n|-------|--------|\n| `<account-id>` | Dashboard URL or `wrangler whoami` |\n| `<bucket>` | R2 bucket name |\n| Catalog URI | Output from `wrangler r2 bucket catalog enable` |\n| Token | R2 API Token creation page |\n\n## Security Best Practices\n\n1. **Store tokens securely** - Use environment variables or secret managers, never hardcode\n2. **Use least privilege** - Read-only tokens for query engines, write tokens only where needed\n3. **Rotate tokens regularly** - Create new tokens, test, then revoke old ones\n4. **One token per application** - Easier to track and revoke if compromised\n5. **Monitor token usage** - Check R2 analytics for unexpected patterns\n6. **Bucket-scoped tokens** - Create tokens per bucket, not account-wide\n\n## Environment Variables Pattern\n\n```bash\n# .env (never commit)\nR2_CATALOG_URI=https://<account-id>.r2.cloudflarestorage.com/iceberg/<bucket>\nR2_WAREHOUSE=<bucket-name>\nR2_TOKEN=<api-token>\n```\n\n```python\nimport os\nfrom pyiceberg.catalog.rest import RestCatalog\n\ncatalog = RestCatalog(\n    name=\"r2\",\n    uri=os.getenv(\"R2_CATALOG_URI\"),\n    warehouse=os.getenv(\"R2_WAREHOUSE\"),\n    token=os.getenv(\"R2_TOKEN\"),\n)\n```\n\n## Troubleshooting\n\n| Problem | Solution |\n|---------|----------|\n| 404 \"catalog not found\" | Run `wrangler r2 bucket catalog enable <bucket>` |\n| 401 \"unauthorized\" | Check token has both Catalog + Storage permissions |\n| 403 on data files | Token needs both permission groups |\n\nSee [gotchas.md](gotchas.md) for detailed troubleshooting.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2-data-catalog/gotchas.md",
    "content": "# Gotchas & Troubleshooting\n\nCommon problems → causes → solutions.\n\n## Permission Errors\n\n### 401 Unauthorized\n\n**Error:** `\"401 Unauthorized\"`  \n**Cause:** Token missing R2 Data Catalog permissions.  \n**Solution:** Use \"Admin Read & Write\" token (includes catalog + storage permissions). Test with `catalog.list_namespaces()`.\n\n### 403 Forbidden\n\n**Error:** `\"403 Forbidden\"` on data files  \n**Cause:** Token lacks storage permissions.  \n**Solution:** Token needs both R2 Data Catalog + R2 Storage Bucket Item permissions.\n\n### Token Rotation Issues\n\n**Error:** New token fails after rotation.  \n**Solution:** Create new token → test in staging → update prod → monitor 24h → revoke old.\n\n## Catalog URI Issues\n\n### 404 Not Found\n\n**Error:** `\"404 Catalog not found\"`  \n**Cause:** Catalog not enabled or wrong URI.  \n**Solution:** Run `wrangler r2 bucket catalog enable <bucket>`. URI must be HTTPS with `/iceberg/` and case-sensitive bucket name.\n\n### Wrong Warehouse\n\n**Error:** Cannot create/load tables.  \n**Cause:** Warehouse ≠ bucket name.  \n**Solution:** Set `warehouse=\"bucket-name\"` to match bucket exactly.\n\n## Table and Schema Issues\n\n### Table/Namespace Already Exists\n\n**Error:** `\"TableAlreadyExistsError\"`  \n**Solution:** Use try/except to load existing or check first.\n\n### Namespace Not Found\n\n**Error:** Cannot create table.  \n**Solution:** Create namespace first: `catalog.create_namespace(\"ns\")`\n\n### Schema Evolution Errors\n\n**Error:** `\"422 Validation\"` on schema update.  \n**Cause:** Incompatible change (required field, type shrink).  \n**Solution:** Only add nullable columns, compatible type widening (int→long, float→double).\n\n## Data and Query Issues\n\n### Empty Scan Results\n\n**Error:** Scan returns no data.  \n**Cause:** Incorrect filter or partition column.  \n**Solution:** Test without filter first: `table.scan().to_pandas()`. Verify partition column names.\n\n### Slow Queries\n\n**Error:** Performance degrades over time.  \n**Cause:** Too many small files.  \n**Solution:** Check file count, compact if >1000 or avg <10MB. See [api.md](api.md#compaction).\n\n### Type Mismatch\n\n**Error:** `\"Cannot cast\"` on append.  \n**Cause:** PyArrow types don't match Iceberg schema.  \n**Solution:** Cast to int64 (Iceberg default), not int32. Check `table.schema()`.\n\n## Compaction Issues\n\n### Compaction Issues\n\n**Problem:** File count unchanged or compaction takes hours.  \n**Cause:** Target size too large, or table too big for PyIceberg.  \n**Solution:** Only compact if avg <50MB. For >1TB tables, use Spark. Run during low-traffic periods.\n\n## Maintenance Issues\n\n### Snapshot/Orphan Issues\n\n**Problem:** Expiration fails or orphan cleanup deletes active data.  \n**Cause:** Too aggressive retention or wrong order.  \n**Solution:** Always expire snapshots first with `retain_last=10`, then cleanup orphans with 3+ day threshold.\n\n## Concurrency Issues\n\n### Concurrent Write Conflicts\n\n**Problem:** `CommitFailedException` with multiple writers.  \n**Cause:** Optimistic locking - simultaneous commits.  \n**Solution:** Add retry with exponential backoff (see [patterns.md](patterns.md#pattern-6-concurrent-writes-with-retry)).\n\n### Stale Metadata\n\n**Problem:** Old schema/data after external update.  \n**Cause:** Cached metadata.  \n**Solution:** Reload table: `table = catalog.load_table((\"ns\", \"table\"))`\n\n## Performance Optimization\n\n### Performance Tips\n\n**Scans:** Use `row_filter` and `selected_fields` to reduce data scanned.  \n**Partitions:** 100-1000 optimal. Avoid high cardinality (millions) or low (<10).  \n**Files:** Keep 100-500MB avg. Compact if <10MB or >10k files.\n\n## Limits\n\n| Resource | Recommended | Impact if Exceeded |\n|----------|-------------|-------------------|\n| Tables/namespace | <10k | Slow list ops |\n| Files/table | <100k | Slow query planning |\n| Partitions/table | 100-1k | Metadata overhead |\n| Snapshots/table | Expire >7d | Metadata bloat |\n\n## Common Error Messages Reference\n\n| Error Message | Likely Cause | Fix |\n|---------------|--------------|-----|\n| `401 Unauthorized` | Missing/invalid token | Check token has catalog+storage permissions |\n| `403 Forbidden` | Token lacks storage permissions | Add R2 Storage Bucket Item permission |\n| `404 Not Found` | Catalog not enabled or wrong URI | Run `wrangler r2 bucket catalog enable` |\n| `409 Conflict` | Table/namespace already exists | Use try/except or load existing |\n| `422 Unprocessable Entity` | Schema validation failed | Check type compatibility, required fields |\n| `CommitFailedException` | Concurrent write conflict | Add retry logic with backoff |\n| `NamespaceAlreadyExistsError` | Namespace exists | Use try/except or load existing |\n| `NoSuchTableError` | Table doesn't exist | Check namespace+table name, create first |\n| `TypeError: Cannot cast` | PyArrow type mismatch | Cast data to match Iceberg schema |\n\n## Debugging Checklist\n\nWhen things go wrong, check in order:\n\n1. ✅ **Catalog enabled:** `npx wrangler r2 bucket catalog status <bucket>`\n2. ✅ **Token permissions:** Both R2 Data Catalog + R2 Storage in dashboard\n3. ✅ **Connection test:** `catalog.list_namespaces()` succeeds\n4. ✅ **URI format:** HTTPS, includes `/iceberg/`, correct bucket name\n5. ✅ **Warehouse name:** Matches bucket name exactly\n6. ✅ **Namespace exists:** Create before `create_table()`\n7. ✅ **Enable debug logging:** `logging.basicConfig(level=logging.DEBUG)`\n8. ✅ **PyIceberg version:** `pip install --upgrade pyiceberg` (≥0.5.0)\n9. ✅ **File health:** Compact if >1000 files or avg <10MB\n10. ✅ **Snapshot count:** Expire if >100 snapshots\n\n## Enable Debug Logging\n\n```python\nimport logging\nlogging.basicConfig(level=logging.DEBUG)\n# Now operations show HTTP requests/responses\n```\n\n## Resources\n\n- [Cloudflare Community](https://community.cloudflare.com/c/developers/workers/40)\n- [Cloudflare Discord](https://discord.cloudflare.com) - #r2 channel\n- [PyIceberg GitHub](https://github.com/apache/iceberg-python/issues)\n- [Apache Iceberg Slack](https://iceberg.apache.org/community/)\n\n## Next Steps\n\n- [patterns.md](patterns.md) - Working examples\n- [api.md](api.md) - API reference\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2-data-catalog/patterns.md",
    "content": "# Common Patterns\n\nPractical patterns for R2 Data Catalog with PyIceberg.\n\n## PyIceberg Connection\n\n```python\nimport os\nfrom pyiceberg.catalog.rest import RestCatalog\nfrom pyiceberg.exceptions import NamespaceAlreadyExistsError\n\ncatalog = RestCatalog(\n    name=\"r2_catalog\",\n    warehouse=os.getenv(\"R2_WAREHOUSE\"),      # bucket name\n    uri=os.getenv(\"R2_CATALOG_URI\"),          # catalog endpoint\n    token=os.getenv(\"R2_TOKEN\"),              # API token\n)\n\n# Create namespace (idempotent)\ntry:\n    catalog.create_namespace(\"default\")\nexcept NamespaceAlreadyExistsError:\n    pass\n```\n\n## Pattern 1: Log Analytics Pipeline\n\nIngest logs incrementally, query by time/level.\n\n```python\nimport pyarrow as pa\nfrom datetime import datetime\nfrom pyiceberg.schema import Schema\nfrom pyiceberg.types import NestedField, TimestampType, StringType, IntegerType\nfrom pyiceberg.partitioning import PartitionSpec, PartitionField\nfrom pyiceberg.transforms import DayTransform\n\n# Create partitioned table (once)\nschema = Schema(\n    NestedField(1, \"timestamp\", TimestampType(), required=True),\n    NestedField(2, \"level\", StringType(), required=True),\n    NestedField(3, \"service\", StringType(), required=True),\n    NestedField(4, \"message\", StringType(), required=False),\n)\n\npartition_spec = PartitionSpec(\n    PartitionField(source_id=1, field_id=1000, transform=DayTransform(), name=\"day\")\n)\n\ncatalog.create_namespace(\"logs\")\ntable = catalog.create_table((\"logs\", \"app_logs\"), schema=schema, partition_spec=partition_spec)\n\n# Append logs (incremental)\ndata = pa.table({\n    \"timestamp\": [datetime(2026, 1, 27, 10, 30, 0)],\n    \"level\": [\"ERROR\"],\n    \"service\": [\"auth-service\"],\n    \"message\": [\"Failed login\"],\n})\ntable.append(data)\n\n# Query by time + level (leverages partitioning)\nscan = table.scan(row_filter=\"level = 'ERROR' AND day = '2026-01-27'\")\nerrors = scan.to_pandas()\n```\n\n## Pattern 2: Time-Travel Queries\n\n```python\nfrom datetime import datetime, timedelta\n\ntable = catalog.load_table((\"logs\", \"app_logs\"))\n\n# Query specific snapshot\nsnapshot_id = table.current_snapshot().snapshot_id\ndata = table.scan(snapshot_id=snapshot_id).to_pandas()\n\n# Query as of timestamp (yesterday)\nyesterday_ms = int((datetime.now() - timedelta(days=1)).timestamp() * 1000)\ndata = table.scan(as_of_timestamp=yesterday_ms).to_pandas()\n```\n\n## Pattern 3: Schema Evolution\n\n```python\nfrom pyiceberg.types import StringType\n\ntable = catalog.load_table((\"users\", \"profiles\"))\n\nwith table.update_schema() as update:\n    update.add_column(\"email\", StringType(), required=False)\n    update.rename_column(\"name\", \"full_name\")\n# Old readers ignore new columns, new readers see nulls for old data\n```\n\n## Pattern 4: Partitioned Tables\n\n```python\nfrom pyiceberg.partitioning import PartitionSpec, PartitionField\nfrom pyiceberg.transforms import DayTransform, IdentityTransform\n\n# Partition by day + country\npartition_spec = PartitionSpec(\n    PartitionField(source_id=1, field_id=1000, transform=DayTransform(), name=\"day\"),\n    PartitionField(source_id=2, field_id=1001, transform=IdentityTransform(), name=\"country\"),\n)\ntable = catalog.create_table((\"events\", \"user_events\"), schema=schema, partition_spec=partition_spec)\n\n# Queries prune partitions automatically\nscan = table.scan(row_filter=\"country = 'US' AND day = '2026-01-27'\")\n```\n\n## Pattern 5: Table Maintenance\n\n```python\nfrom datetime import datetime, timedelta\n\ntable = catalog.load_table((\"logs\", \"app_logs\"))\n\n# Compact → expire → cleanup (in order)\ntable.rewrite_data_files(target_file_size_bytes=128 * 1024 * 1024)\nseven_days_ms = int((datetime.now() - timedelta(days=7)).timestamp() * 1000)\ntable.expire_snapshots(older_than=seven_days_ms, retain_last=10)\nthree_days_ms = int((datetime.now() - timedelta(days=3)).timestamp() * 1000)\ntable.delete_orphan_files(older_than=three_days_ms)\n```\n\nSee [api.md](api.md#table-maintenance) for detailed parameters.\n\n## Pattern 6: Concurrent Writes with Retry\n\n```python\nfrom pyiceberg.exceptions import CommitFailedException\nimport time\n\ndef append_with_retry(table, data, max_retries=3):\n    for attempt in range(max_retries):\n        try:\n            table.append(data)\n            return\n        except CommitFailedException:\n            if attempt == max_retries - 1:\n                raise\n            time.sleep(2 ** attempt)\n```\n\n## Pattern 7: Upsert Simulation\n\n```python\nimport pandas as pd\nimport pyarrow as pa\n\n# Read → merge → overwrite (not atomic, use Spark MERGE INTO for production)\nexisting = table.scan().to_pandas()\nnew_data = pd.DataFrame({\"id\": [1, 3], \"value\": [100, 300]})\nmerged = pd.concat([existing, new_data]).drop_duplicates(subset=[\"id\"], keep=\"last\")\ntable.overwrite(pa.Table.from_pandas(merged))\n```\n\n## Pattern 8: DuckDB Integration\n\n```python\nimport duckdb\n\narrow_table = table.scan().to_arrow()\ncon = duckdb.connect()\ncon.register(\"logs\", arrow_table)\nresult = con.execute(\"SELECT level, COUNT(*) FROM logs GROUP BY level\").fetchdf()\n```\n\n## Pattern 9: Monitor Table Health\n\n```python\nfiles = table.scan().plan_files()\navg_mb = sum(f.file_size_in_bytes for f in files) / len(files) / (1024**2)\nprint(f\"Files: {len(files)}, Avg: {avg_mb:.1f}MB, Snapshots: {len(table.snapshots())}\")\n\nif avg_mb < 10 or len(files) > 1000:\n    print(\"⚠️ Needs compaction\")\n```\n\n## Best Practices\n\n| Area | Guideline |\n|------|-----------|\n| **Partitioning** | Use day/hour for time-series; 100-1000 partitions; avoid high cardinality |\n| **File sizes** | Target 128-512MB; compact when avg <10MB or >10k files |\n| **Schema** | Add columns as nullable (`required=False`); batch changes |\n| **Maintenance** | Compact high-write daily/weekly; expire snapshots 7-30d; cleanup orphans after |\n| **Concurrency** | Reads automatic; writes to different partitions safe; retry same partition |\n| **Performance** | Filter on partitions; select only needed columns; batch appends 100MB+ |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2-sql/README.md",
    "content": "# Cloudflare R2 SQL Skill Reference\n\nExpert guidance for Cloudflare R2 SQL - serverless distributed query engine for Apache Iceberg tables.\n\n## Reading Order\n\n**New to R2 SQL?** Start here:\n1. Read \"What is R2 SQL?\" and \"When to Use\" below\n2. [configuration.md](configuration.md) - Enable catalog, create tokens\n3. [patterns.md](patterns.md) - Wrangler CLI and integration examples\n4. [api.md](api.md) - SQL syntax and query reference\n5. [gotchas.md](gotchas.md) - Limitations and troubleshooting\n\n**Quick reference?** Jump to:\n- [Run a query via Wrangler](patterns.md#wrangler-cli-query)\n- [SQL syntax reference](api.md#sql-syntax)\n- [ORDER BY limitations](gotchas.md#order-by-limitations)\n\n## What is R2 SQL?\n\nR2 SQL is Cloudflare's **serverless distributed analytics query engine** for querying Apache Iceberg tables in R2 Data Catalog. Features:\n\n- **Serverless** - No clusters to manage, no infrastructure\n- **Distributed** - Leverages Cloudflare's global network for parallel execution\n- **SQL interface** - Familiar SQL syntax for analytics queries\n- **Zero egress fees** - Query from any cloud/region without data transfer costs\n- **Open beta** - Free during beta (standard R2 storage costs apply)\n\n### What is Apache Iceberg?\n\nOpen table format for large-scale analytics datasets in object storage:\n- **ACID transactions** - Safe concurrent reads/writes\n- **Metadata optimization** - Fast queries without full table scans\n- **Schema evolution** - Add/rename/drop columns without rewrites\n- **Partitioning** - Organize data for efficient pruning\n\n## When to Use\n\n**Use R2 SQL for:**\n- **Log analytics** - Query application/system logs with WHERE filters and aggregations\n- **BI dashboards** - Generate reports from large analytical datasets\n- **Fraud detection** - Analyze transaction patterns with GROUP BY/HAVING\n- **Multi-cloud analytics** - Query data from any cloud without egress fees\n- **Ad-hoc exploration** - Run SQL queries on Iceberg tables via Wrangler CLI\n\n**Don't use R2 SQL for:**\n- **Workers/Pages runtime** - R2 SQL has no Workers binding, use HTTP API from external systems\n- **Real-time queries (<100ms)** - Optimized for analytical batch queries, not OLTP\n- **Complex joins/CTEs** - Limited SQL feature set (no JOINs, subqueries, CTEs currently)\n- **Small datasets (<1GB)** - Setup overhead not justified\n\n## Decision Tree: Need to Query R2 Data?\n\n```\nDo you need to query structured data in R2?\n├─ YES, data is in Iceberg tables\n│  ├─ Need SQL interface? → Use R2 SQL (this reference)\n│  ├─ Need Python API? → See r2-data-catalog reference (PyIceberg)\n│  └─ Need other engine? → See r2-data-catalog reference (Spark, Trino, etc.)\n│\n├─ YES, but not in Iceberg format\n│  ├─ Streaming data? → Use Pipelines to write to Data Catalog, then R2 SQL\n│  └─ Static files? → Use PyIceberg to create Iceberg tables, then R2 SQL\n│\n└─ NO, just need object storage → Use R2 reference (not R2 SQL)\n```\n\n## Architecture Overview\n\n**Query Planner:**\n- Top-down metadata investigation with multi-layer pruning\n- Partition-level, column-level, and row-group pruning\n- Streaming pipeline - execution starts before planning completes\n- Early termination with LIMIT - stops when result complete\n\n**Query Execution:**\n- Coordinator distributes work to workers across Cloudflare network\n- Workers run Apache DataFusion for parallel query execution\n- Parquet column pruning - reads only required columns\n- Ranged reads from R2 for efficiency\n\n**Aggregation Strategies:**\n- Scatter-gather - simple aggregations (SUM, COUNT, AVG)\n- Shuffling - ORDER BY/HAVING on aggregates via hash partitioning\n\n## Quick Start\n\n```bash\n# 1. Enable R2 Data Catalog on bucket\nnpx wrangler r2 bucket catalog enable my-bucket\n\n# 2. Create API token (Admin Read & Write)\n# Dashboard: R2 → Manage API tokens → Create API token\n\n# 3. Set environment variable\nexport WRANGLER_R2_SQL_AUTH_TOKEN=<your-token>\n\n# 4. Run query\nnpx wrangler r2 sql query \"my-bucket\" \"SELECT * FROM default.my_table LIMIT 10\"\n```\n\n## Important Limitations\n\n**CRITICAL: No Workers Binding**\n- R2 SQL cannot be called directly from Workers/Pages code\n- For programmatic access, use HTTP API from external systems\n- Or query via PyIceberg, Spark, etc. (see r2-data-catalog reference)\n\n**SQL Feature Set:**\n- No JOINs, CTEs, subqueries, window functions\n- ORDER BY supports aggregation columns (not just partition keys)\n- LIMIT max 10,000 (default 500)\n- See [gotchas.md](gotchas.md) for complete limitations\n\n## In This Reference\n\n- **[configuration.md](configuration.md)** - Enable catalog, create API tokens\n- **[api.md](api.md)** - SQL syntax, functions, operators, data types\n- **[patterns.md](patterns.md)** - Wrangler CLI, HTTP API, Pipelines, PyIceberg\n- **[gotchas.md](gotchas.md)** - Limitations, troubleshooting, performance tips\n\n## See Also\n\n- [r2-data-catalog](../r2-data-catalog/) - PyIceberg, REST API, external engines\n- [pipelines](../pipelines/) - Streaming ingestion to Iceberg tables\n- [r2](../r2/) - R2 object storage fundamentals\n- [Cloudflare R2 SQL Docs](https://developers.cloudflare.com/r2-sql/)\n- [R2 SQL Deep Dive Blog](https://blog.cloudflare.com/r2-sql-deep-dive/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2-sql/api.md",
    "content": "# R2 SQL API Reference\n\nSQL syntax, functions, operators, and data types for R2 SQL queries.\n\n## SQL Syntax\n\n```sql\nSELECT column_list | aggregation_function\nFROM [namespace.]table_name\nWHERE conditions\n[GROUP BY column_list]\n[HAVING conditions]\n[ORDER BY column | aggregation_function [DESC | ASC]]\n[LIMIT number]\n```\n\n## Schema Discovery\n\n```sql\nSHOW DATABASES;           -- List namespaces\nSHOW NAMESPACES;          -- Alias for SHOW DATABASES\nSHOW SCHEMAS;             -- Alias for SHOW DATABASES\nSHOW TABLES IN namespace; -- List tables in namespace\nDESCRIBE namespace.table; -- Show table schema, partition keys\n```\n\n## SELECT Clause\n\n```sql\n-- All columns\nSELECT * FROM logs.http_requests;\n\n-- Specific columns\nSELECT user_id, timestamp, status FROM logs.http_requests;\n```\n\n**Limitations:** No column aliases, expressions, or nested column access\n\n## WHERE Clause\n\n### Operators\n\n| Operator | Example |\n|----------|---------|\n| `=`, `!=`, `<`, `<=`, `>`, `>=` | `status = 200` |\n| `LIKE` | `user_agent LIKE '%Chrome%'` |\n| `BETWEEN` | `timestamp BETWEEN '2025-01-01T00:00:00Z' AND '2025-01-31T23:59:59Z'` |\n| `IS NULL`, `IS NOT NULL` | `email IS NOT NULL` |\n| `AND`, `OR` | `status = 200 AND method = 'GET'` |\n\nUse parentheses for precedence: `(status = 404 OR status = 500) AND method = 'POST'`\n\n## Aggregation Functions\n\n| Function | Description |\n|----------|-------------|\n| `COUNT(*)` | Count all rows |\n| `COUNT(column)` | Count non-null values |\n| `COUNT(DISTINCT column)` | Count unique values |\n| `SUM(column)`, `AVG(column)` | Numeric aggregations |\n| `MIN(column)`, `MAX(column)` | Min/max values |\n\n```sql\n-- Multiple aggregations with GROUP BY\nSELECT region, COUNT(*), SUM(amount), AVG(amount)\nFROM sales.transactions\nWHERE sale_date >= '2024-01-01'\nGROUP BY region;\n```\n\n## HAVING Clause\n\nFilter aggregated results (after GROUP BY):\n\n```sql\nSELECT category, SUM(amount)\nFROM sales.transactions\nGROUP BY category\nHAVING SUM(amount) > 10000;\n```\n\n## ORDER BY Clause\n\nSort results by:\n- **Partition key columns** - Always supported\n- **Aggregation functions** - Supported via shuffle strategy\n\n```sql\n-- Order by partition key\nSELECT * FROM logs.requests ORDER BY timestamp DESC LIMIT 100;\n\n-- Order by aggregation (repeat function, aliases not supported)\nSELECT region, SUM(amount)\nFROM sales.transactions\nGROUP BY region\nORDER BY SUM(amount) DESC;\n```\n\n**Limitations:** Cannot order by non-partition columns. See [gotchas.md](gotchas.md#order-by-limitations)\n\n## LIMIT Clause\n\n```sql\nSELECT * FROM logs.requests LIMIT 100;\n```\n\n| Setting | Value |\n|---------|-------|\n| Min | 1 |\n| Max | 10,000 |\n| Default | 500 |\n\n**Always use LIMIT** to enable early termination optimization.\n\n## Data Types\n\n| Type | SQL Literal | Example |\n|------|-------------|---------|\n| `integer` | Unquoted number | `42`, `-10` |\n| `float` | Decimal number | `3.14`, `-0.5` |\n| `string` | Single quotes | `'hello'`, `'GET'` |\n| `boolean` | Keyword | `true`, `false` |\n| `timestamp` | RFC3339 string | `'2025-01-01T00:00:00Z'` |\n| `date` | ISO 8601 date | `'2025-01-01'` |\n\n### Type Safety\n\n- Quote strings with single quotes: `'value'`\n- Timestamps must be RFC3339: `'2025-01-01T00:00:00Z'` (include timezone)\n- Dates must be ISO 8601: `'2025-01-01'` (YYYY-MM-DD)\n- No implicit conversions\n\n```sql\n-- ✅ Correct\nWHERE status = 200 AND method = 'GET' AND timestamp > '2025-01-01T00:00:00Z'\n\n-- ❌ Wrong\nWHERE status = '200'              -- string instead of integer\nWHERE timestamp > '2025-01-01'    -- missing time/timezone\nWHERE method = GET                -- unquoted string\n```\n\n## Query Result Format\n\nJSON array of objects:\n\n```json\n[\n  {\"user_id\": \"user_123\", \"timestamp\": \"2025-01-15T10:30:00Z\", \"status\": 200},\n  {\"user_id\": \"user_456\", \"timestamp\": \"2025-01-15T10:31:00Z\", \"status\": 404}\n]\n```\n\n## See Also\n\n- [patterns.md](patterns.md) - Query examples and use cases\n- [gotchas.md](gotchas.md) - SQL limitations and error handling\n- [configuration.md](configuration.md) - Setup and authentication\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2-sql/configuration.md",
    "content": "# R2 SQL Configuration\n\nSetup and configuration for R2 SQL queries.\n\n## Prerequisites\n\n- R2 bucket with Data Catalog enabled\n- API token with R2 permissions\n- Wrangler CLI installed (for CLI queries)\n\n## Enable R2 Data Catalog\n\nR2 SQL queries Apache Iceberg tables in R2 Data Catalog. Must enable catalog on bucket first.\n\n### Via Wrangler CLI\n\n```bash\nnpx wrangler r2 bucket catalog enable <bucket-name>\n```\n\nOutput includes:\n- **Warehouse name** - Typically same as bucket name\n- **Catalog URI** - REST endpoint for catalog operations\n\nExample output:\n```\nCatalog enabled successfully\nWarehouse: my-bucket\nCatalog URI: https://abc123.r2.cloudflarestorage.com/iceberg/my-bucket\n```\n\n### Via Dashboard\n\n1. Navigate to **R2 Object Storage** → Select your bucket\n2. Click **Settings** tab\n3. Scroll to **R2 Data Catalog** section\n4. Click **Enable**\n5. Note the **Catalog URI** and **Warehouse** name\n\n**Important:** Enabling catalog creates metadata directories in bucket but does not modify existing objects.\n\n## Create API Token\n\nR2 SQL requires API token with R2 permissions.\n\n### Required Permission\n\n**R2 Admin Read & Write** (includes R2 SQL Read permission)\n\n### Via Dashboard\n\n1. Navigate to **R2 Object Storage**\n2. Click **Manage API tokens** (top right)\n3. Click **Create API token**\n4. Select **Admin Read & Write** permission\n5. Click **Create API Token**\n6. **Copy token value** - shown only once\n\n### Permission Scope\n\n| Permission | Grants Access To |\n|------------|------------------|\n| R2 Admin Read & Write | R2 storage operations + R2 SQL queries + Data Catalog operations |\n| R2 SQL Read | SQL queries only (no storage writes) |\n\n**Note:** R2 SQL Read permission not yet available via Dashboard - use Admin Read & Write.\n\n## Configure Environment\n\n### Wrangler CLI\n\nSet environment variable for Wrangler to use:\n\n```bash\nexport WRANGLER_R2_SQL_AUTH_TOKEN=<your-token>\n```\n\nOr create `.env` file in project directory:\n\n```\nWRANGLER_R2_SQL_AUTH_TOKEN=<your-token>\n```\n\nWrangler automatically loads `.env` file when running commands.\n\n### HTTP API\n\nFor programmatic access (non-Wrangler), pass token in Authorization header:\n\n```bash\ncurl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/r2/sql/query \\\n  -H \"Authorization: Bearer <your-token>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"warehouse\": \"my-bucket\",\n    \"query\": \"SELECT * FROM default.my_table LIMIT 10\"\n  }'\n```\n\n**Note:** HTTP API endpoint URL may vary - see [patterns.md](patterns.md#http-api-query) for current endpoint.\n\n## Verify Setup\n\nTest configuration by querying system tables:\n\n```bash\n# List namespaces\nnpx wrangler r2 sql query \"my-bucket\" \"SHOW DATABASES\"\n\n# List tables in namespace\nnpx wrangler r2 sql query \"my-bucket\" \"SHOW TABLES IN default\"\n```\n\nIf successful, returns JSON array of results.\n\n## Troubleshooting\n\n### \"Token authentication failed\"\n\n**Cause:** Invalid or missing token\n\n**Solution:**\n- Verify `WRANGLER_R2_SQL_AUTH_TOKEN` environment variable set\n- Check token has Admin Read & Write permission\n- Create new token if expired\n\n### \"Catalog not enabled on bucket\"\n\n**Cause:** Data Catalog not enabled\n\n**Solution:**\n- Run `npx wrangler r2 bucket catalog enable <bucket-name>`\n- Or enable via Dashboard (R2 → bucket → Settings → R2 Data Catalog)\n\n### \"Permission denied\"\n\n**Cause:** Token lacks required permissions\n\n**Solution:**\n- Verify token has **Admin Read & Write** permission\n- Create new token with correct permissions\n\n## See Also\n\n- [r2-data-catalog/configuration.md](../r2-data-catalog/configuration.md) - Detailed token setup and PyIceberg connection\n- [patterns.md](patterns.md) - Query examples using configuration\n- [gotchas.md](gotchas.md) - Common configuration errors\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2-sql/gotchas.md",
    "content": "# R2 SQL Gotchas\n\nLimitations, troubleshooting, and common pitfalls for R2 SQL.\n\n## Critical Limitations\n\n### No Workers Binding\n\n**Cannot call R2 SQL from Workers/Pages code** - no binding exists.\n\n```typescript\n// ❌ This doesn't exist\nexport default {\n  async fetch(request, env) {\n    const result = await env.R2_SQL.query(\"SELECT * FROM table\");  // Not possible\n    return Response.json(result);\n  }\n};\n```\n\n**Solutions:**\n- HTTP API from external systems (not Workers)\n- PyIceberg/Spark via r2-data-catalog REST API\n- For Workers, use D1 or external databases\n\n### ORDER BY Limitations\n\nCan only order by:\n1. **Partition key columns** - Always supported\n2. **Aggregation functions** - Supported via shuffle strategy\n\n**Cannot order by** regular non-partition columns.\n\n```sql\n-- ✅ Valid: ORDER BY partition key\nSELECT * FROM logs.requests ORDER BY timestamp DESC LIMIT 100;\n\n-- ✅ Valid: ORDER BY aggregation\nSELECT region, SUM(amount) FROM sales.transactions\nGROUP BY region ORDER BY SUM(amount) DESC;\n\n-- ❌ Invalid: ORDER BY non-partition column\nSELECT * FROM logs.requests ORDER BY user_id;\n\n-- ❌ Invalid: ORDER BY alias (must repeat function)\nSELECT region, SUM(amount) as total FROM sales.transactions\nGROUP BY region ORDER BY total;  -- Use ORDER BY SUM(amount)\n```\n\nCheck partition spec: `DESCRIBE namespace.table_name`\n\n## SQL Feature Limitations\n\n| Feature | Supported | Notes |\n|---------|-----------|-------|\n| SELECT, WHERE, GROUP BY, HAVING | ✅ | Standard support |\n| COUNT, SUM, AVG, MIN, MAX | ✅ | Standard aggregations |\n| ORDER BY partition/aggregation | ✅ | See above |\n| LIMIT | ✅ | Max 10,000 |\n| Column aliases | ❌ | No AS alias |\n| Expressions in SELECT | ❌ | No col1 + col2 |\n| ORDER BY non-partition | ❌ | Fails at runtime |\n| JOINs, subqueries, CTEs | ❌ | Denormalize at write time |\n| Window functions, UNION | ❌ | Use external engines |\n| INSERT/UPDATE/DELETE | ❌ | Use PyIceberg/Pipelines |\n| Nested columns, arrays, JSON | ❌ | Flatten at write time |\n\n**Workarounds:**\n- No JOINs: Denormalize data or use Spark/PyIceberg\n- No subqueries: Split into multiple queries\n- No aliases: Accept generated names, transform in app\n\n## Common Errors\n\n### \"Column not found\"\n**Cause:** Typo, column doesn't exist, or case mismatch  \n**Solution:** `DESCRIBE namespace.table_name` to check schema\n\n### \"Type mismatch\"\n```sql\n-- ❌ Wrong types\nWHERE status = '200'              -- string instead of integer\nWHERE timestamp > '2025-01-01'    -- missing time/timezone\n\n-- ✅ Correct types\nWHERE status = 200\nWHERE timestamp > '2025-01-01T00:00:00Z'\n```\n\n### \"ORDER BY column not in partition key\"\n**Cause:** Ordering by non-partition column  \n**Solution:** Use partition key, aggregation, or remove ORDER BY. Check: `DESCRIBE table`\n\n### \"Token authentication failed\"\n```bash\n# Check/set token\necho $WRANGLER_R2_SQL_AUTH_TOKEN\nexport WRANGLER_R2_SQL_AUTH_TOKEN=<your-token>\n\n# Or .env file\necho \"WRANGLER_R2_SQL_AUTH_TOKEN=<your-token>\" > .env\n```\n\n### \"Table not found\"\n```sql\n-- Verify catalog and tables\nSHOW DATABASES;\nSHOW TABLES IN namespace_name;\n```\n\nEnable catalog: `npx wrangler r2 bucket catalog enable <bucket>`\n\n### \"LIMIT exceeds maximum\"\nMax LIMIT is 10,000. For pagination, use WHERE filters with partition keys.\n\n### \"No data returned\" (unexpected)\n**Debug steps:**\n1. `SELECT COUNT(*) FROM table` - verify data exists\n2. Remove WHERE filters incrementally\n3. `SELECT * FROM table LIMIT 10` - inspect actual data/types\n\n## Performance Issues\n\n### Slow Queries\n\n**Causes:** Too many partitions, large LIMIT, no filters, small files\n\n```sql\n-- ❌ Slow: No filters\nSELECT * FROM logs.requests LIMIT 10000;\n\n-- ✅ Fast: Filter on partition key\nSELECT * FROM logs.requests \nWHERE timestamp >= '2025-01-15T00:00:00Z' AND timestamp < '2025-01-16T00:00:00Z'\nLIMIT 1000;\n\n-- ✅ Faster: Multiple filters\nSELECT * FROM logs.requests \nWHERE timestamp >= '2025-01-15T00:00:00Z' AND status = 404 AND method = 'GET'\nLIMIT 1000;\n```\n\n**File optimization:**\n- Target Parquet size: 100-500MB compressed\n- Pipelines roll interval: 300+ sec (prod), 10 sec (dev)\n- Run compaction to merge small files\n\n### Query Timeout\n\n**Solution:** Add restrictive WHERE filters, reduce time range, query smaller intervals\n\n```sql\n-- ❌ Times out: Year-long aggregation\nSELECT status, COUNT(*) FROM logs.requests \nWHERE timestamp >= '2024-01-01T00:00:00Z' GROUP BY status;\n\n-- ✅ Faster: Month-long aggregation\nSELECT status, COUNT(*) FROM logs.requests \nWHERE timestamp >= '2025-01-01T00:00:00Z' AND timestamp < '2025-02-01T00:00:00Z'\nGROUP BY status;\n```\n\n## Best Practices\n\n### Partitioning\n- **Time-series:** Partition by day/hour on timestamp\n- **Avoid:** High-cardinality keys (user_id), >10,000 partitions\n\n```python\nfrom pyiceberg.partitioning import PartitionSpec, PartitionField\nfrom pyiceberg.transforms import DayTransform\n\nPartitionSpec(PartitionField(source_id=1, field_id=1000, transform=DayTransform(), name=\"day\"))\n```\n\n### Query Writing\n- **Always use LIMIT** for early termination\n- **Filter on partition keys first** for pruning\n- **Combine filters with AND** for more pruning\n\n```sql\n-- Good\nWHERE timestamp >= '2025-01-15T00:00:00Z' AND status = 404 AND method = 'GET' LIMIT 100\n```\n\n### Type Safety\n- Quote strings: `'GET'` not `GET`\n- RFC3339 timestamps: `'2025-01-01T00:00:00Z'` not `'2025-01-01'`\n- ISO dates: `'2025-01-15'` not `'01/15/2025'`\n\n### Data Organization\n- **Pipelines:** Dev `roll_file_time: 10`, Prod `roll_file_time: 300+`\n- **Compression:** Use `zstd`\n- **Maintenance:** Compaction for small files, expire old snapshots\n\n## Debugging Checklist\n\n1. `npx wrangler r2 bucket catalog enable <bucket>` - Verify catalog\n2. `echo $WRANGLER_R2_SQL_AUTH_TOKEN` - Check token\n3. `SHOW DATABASES` - List namespaces\n4. `SHOW TABLES IN namespace` - List tables\n5. `DESCRIBE namespace.table` - Check schema\n6. `SELECT COUNT(*) FROM namespace.table` - Verify data\n7. `SELECT * FROM namespace.table LIMIT 10` - Test simple query\n8. Add filters incrementally\n\n## See Also\n\n- [api.md](api.md) - SQL syntax\n- [patterns.md](patterns.md) - Query optimization\n- [configuration.md](configuration.md) - Setup\n- [Cloudflare R2 SQL Docs](https://developers.cloudflare.com/r2-sql/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/r2-sql/patterns.md",
    "content": "# R2 SQL Patterns\n\nCommon patterns, use cases, and integration examples for R2 SQL.\n\n## Wrangler CLI Query\n\n```bash\n# Basic query\nnpx wrangler r2 sql query \"my-bucket\" \"SELECT * FROM default.logs LIMIT 10\"\n\n# Multi-line query\nnpx wrangler r2 sql query \"my-bucket\" \"\n  SELECT status, COUNT(*), AVG(response_time)\n  FROM logs.http_requests\n  WHERE timestamp >= '2025-01-01T00:00:00Z'\n  GROUP BY status\n  ORDER BY COUNT(*) DESC\n  LIMIT 100\n\"\n\n# Use environment variable\nexport R2_SQL_WAREHOUSE=\"my-bucket\"\nnpx wrangler r2 sql query \"$R2_SQL_WAREHOUSE\" \"SELECT * FROM default.logs\"\n```\n\n## HTTP API Query\n\nFor programmatic access from external systems (not Workers - see gotchas.md).\n\n```bash\ncurl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/r2/sql/query \\\n  -H \"Authorization: Bearer <your-token>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"warehouse\": \"my-bucket\",\n    \"query\": \"SELECT * FROM default.my_table WHERE status = 200 LIMIT 100\"\n  }'\n```\n\nResponse:\n```json\n{\n  \"success\": true,\n  \"result\": [{\"user_id\": \"user_123\", \"timestamp\": \"2025-01-15T10:30:00Z\", \"status\": 200}],\n  \"errors\": []\n}\n```\n\n## Pipelines Integration\n\nStream data to Iceberg tables via Pipelines, then query with R2 SQL.\n\n```bash\n# Setup pipeline (select Data Catalog Table destination)\nnpx wrangler pipelines setup\n\n# Key settings:\n# - Destination: Data Catalog Table\n# - Compression: zstd (recommended)\n# - Roll file time: 300+ sec (production), 10 sec (dev)\n\n# Send data to pipeline\ncurl -X POST https://{stream-id}.ingest.cloudflare.com \\\n  -H \"Content-Type: application/json\" \\\n  -d '[{\"user_id\": \"user_123\", \"event_type\": \"purchase\", \"timestamp\": \"2025-01-15T10:30:00Z\", \"amount\": 29.99}]'\n\n# Query ingested data (wait for roll interval)\nnpx wrangler r2 sql query \"my-bucket\" \"\n  SELECT event_type, COUNT(*), SUM(amount)\n  FROM default.events\n  WHERE timestamp >= '2025-01-15T00:00:00Z'\n  GROUP BY event_type\n\"\n```\n\nSee [pipelines/patterns.md](../pipelines/patterns.md) for detailed setup.\n\n## PyIceberg Integration\n\nCreate and populate Iceberg tables with PyIceberg, then query with R2 SQL.\n\n```python\nfrom pyiceberg.catalog.rest import RestCatalog\nimport pyarrow as pa\nimport pandas as pd\n\n# Setup catalog\ncatalog = RestCatalog(\n    name=\"my_catalog\",\n    warehouse=\"my-bucket\",\n    uri=\"https://<account-id>.r2.cloudflarestorage.com/iceberg/my-bucket\",\n    token=\"<your-token>\",\n)\ncatalog.create_namespace_if_not_exists(\"analytics\")\n\n# Create table\nschema = pa.schema([\n    pa.field(\"user_id\", pa.string(), nullable=False),\n    pa.field(\"event_time\", pa.timestamp(\"us\", tz=\"UTC\"), nullable=False),\n    pa.field(\"page_views\", pa.int64(), nullable=False),\n])\ntable = catalog.create_table((\"analytics\", \"user_metrics\"), schema=schema)\n\n# Append data\ndf = pd.DataFrame({\n    \"user_id\": [\"user_1\", \"user_2\"],\n    \"event_time\": pd.to_datetime([\"2025-01-15 10:00:00\", \"2025-01-15 11:00:00\"], utc=True),\n    \"page_views\": [10, 25],\n})\ntable.append(pa.Table.from_pandas(df, schema=schema))\n```\n\nQuery with R2 SQL:\n```bash\nnpx wrangler r2 sql query \"my-bucket\" \"\n  SELECT user_id, SUM(page_views)\n  FROM analytics.user_metrics\n  WHERE event_time >= '2025-01-15T00:00:00Z'\n  GROUP BY user_id\n\"\n```\n\nSee [r2-data-catalog/patterns.md](../r2-data-catalog/patterns.md) for advanced PyIceberg patterns.\n\n## Use Cases\n\n### Log Analytics\n```sql\n-- Error rate by endpoint\nSELECT path, COUNT(*), SUM(CASE WHEN status >= 400 THEN 1 ELSE 0 END) as errors\nFROM logs.http_requests\nWHERE timestamp BETWEEN '2025-01-01T00:00:00Z' AND '2025-01-31T23:59:59Z'\nGROUP BY path ORDER BY errors DESC LIMIT 20;\n\n-- Response time stats\nSELECT method, MIN(response_time_ms), AVG(response_time_ms), MAX(response_time_ms)\nFROM logs.http_requests WHERE timestamp >= '2025-01-15T00:00:00Z' GROUP BY method;\n\n-- Traffic by status\nSELECT status, COUNT(*) FROM logs.http_requests\nWHERE timestamp >= '2025-01-15T00:00:00Z' AND method = 'GET'\nGROUP BY status ORDER BY COUNT(*) DESC;\n```\n\n### Fraud Detection\n```sql\n-- High-value transactions\nSELECT location, COUNT(*), SUM(amount), AVG(amount)\nFROM fraud.transactions WHERE transaction_timestamp >= '2025-01-01T00:00:00Z' AND amount > 1000.0\nGROUP BY location ORDER BY SUM(amount) DESC LIMIT 20;\n\n-- Flagged transactions\nSELECT merchant_category, COUNT(*), AVG(amount) FROM fraud.transactions\nWHERE is_fraud_flag = true AND transaction_timestamp >= '2025-01-01T00:00:00Z'\nGROUP BY merchant_category HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC;\n```\n\n### Business Intelligence\n```sql\n-- Sales by department\nSELECT department, SUM(revenue), AVG(revenue), COUNT(*) FROM sales.transactions\nWHERE sale_date >= '2024-01-01' GROUP BY department ORDER BY SUM(revenue) DESC LIMIT 10;\n\n-- Product performance\nSELECT category, COUNT(DISTINCT product_id), SUM(units_sold), SUM(revenue)\nFROM sales.product_sales WHERE sale_date BETWEEN '2024-10-01' AND '2024-12-31'\nGROUP BY category ORDER BY SUM(revenue) DESC;\n```\n\n## Connecting External Engines\n\nR2 Data Catalog exposes Iceberg REST API. Connect Spark, Snowflake, Trino, DuckDB, etc.\n\n```scala\n// Apache Spark example\nval spark = SparkSession.builder()\n  .config(\"spark.sql.catalog.my_catalog\", \"org.apache.iceberg.spark.SparkCatalog\")\n  .config(\"spark.sql.catalog.my_catalog.catalog-impl\", \"org.apache.iceberg.rest.RESTCatalog\")\n  .config(\"spark.sql.catalog.my_catalog.uri\", \"https://<account-id>.r2.cloudflarestorage.com/iceberg/my-bucket\")\n  .config(\"spark.sql.catalog.my_catalog.token\", \"<token>\")\n  .getOrCreate()\n\nspark.sql(\"SELECT * FROM my_catalog.default.my_table LIMIT 10\").show()\n```\n\nSee [r2-data-catalog/patterns.md](../r2-data-catalog/patterns.md) for more engines.\n\n## Performance Optimization\n\n### Partitioning\n- **Time-series:** day/hour on timestamp\n- **Geographic:** region/country\n- **Avoid:** High-cardinality keys (user_id)\n\n```python\nfrom pyiceberg.partitioning import PartitionSpec, PartitionField\nfrom pyiceberg.transforms import DayTransform\n\nPartitionSpec(PartitionField(source_id=1, field_id=1000, transform=DayTransform(), name=\"day\"))\n```\n\n### Query Optimization\n- **Always use LIMIT** for early termination\n- **Filter on partition keys first**\n- **Multiple filters** for better pruning\n\n```sql\n-- Better: Multiple filters on partition key\nSELECT * FROM logs.requests \nWHERE timestamp >= '2025-01-15T00:00:00Z' AND status = 404 AND method = 'GET' LIMIT 100;\n```\n\n### File Organization\n- **Pipelines roll:** Dev 10-30s, Prod 300+s\n- **Target Parquet:** 100-500MB compressed\n\n## See Also\n\n- [api.md](api.md) - SQL syntax reference\n- [gotchas.md](gotchas.md) - Limitations and troubleshooting\n- [r2-data-catalog/patterns.md](../r2-data-catalog/patterns.md) - PyIceberg advanced patterns\n- [pipelines/patterns.md](../pipelines/patterns.md) - Streaming ingestion patterns\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/realtime-sfu/README.md",
    "content": "# Cloudflare Realtime SFU Reference\n\nExpert guidance for building real-time audio/video/data applications using Cloudflare Realtime SFU (Selective Forwarding Unit).\n\n## Reading Order\n\n| Task | Files | ~Tokens |\n|------|-------|---------|\n| New project | README → configuration | ~1200 |\n| Implement publish/subscribe | README → api | ~1600 |\n| Add PartyTracks | patterns (PartyTracks section) | ~800 |\n| Build presence system | patterns (DO section) | ~800 |\n| Debug connection issues | gotchas | ~700 |\n| Scale to millions | patterns (Cascading section) | ~600 |\n| Add simulcast | patterns (Advanced section) | ~500 |\n| Configure TURN | configuration (TURN section) | ~400 |\n\n## In This Reference\n\n- **[configuration.md](configuration.md)** - Setup, deployment, environment variables, Wrangler config\n- **[api.md](api.md)** - Sessions, tracks, endpoints, request/response patterns\n- **[patterns.md](patterns.md)** - Architecture patterns, use cases, integration examples\n- **[gotchas.md](gotchas.md)** - Common issues, debugging, performance, security\n\n## Quick Start\n\nCloudflare Realtime SFU: WebRTC infrastructure on global network (310+ cities). Anycast routing, no regional constraints, pub/sub model.\n\n**Core concepts:**\n- **Sessions:** WebRTC PeerConnection to Cloudflare edge\n- **Tracks:** Audio/video/data channels you publish or subscribe to\n- **No rooms:** Build presence layer yourself via track sharing (see patterns.md)\n\n**Mental model:** Your client establishes one WebRTC session, publishes tracks (audio/video), shares track IDs via your backend, others subscribe to your tracks using track IDs + your session ID.\n\n## Choose Your Approach\n\n| Approach | When to Use | Complexity |\n|----------|-------------|------------|\n| **PartyTracks** | Production apps with device switching, React | Low - Observable-based, handles reconnections |\n| **Raw API** | Custom requirements, non-browser, learning | Medium - Full control, manual WebRTC lifecycle |\n| **RealtimeKit** | End-to-end SDK with UI components | Lowest - Managed state, React hooks |\n\n**Recommendation:** Start with PartyTracks for most production applications. See patterns.md for PartyTracks examples.\n\n## SFU vs RealtimeKit\n\n- **Realtime SFU:** WebRTC infrastructure (this reference). Build your own signaling, presence, UI.\n- **RealtimeKit:** SDK layer on top of SFU. Includes React hooks, state management, UI components. Part of Cloudflare AI platform.\n\nUse SFU directly when you need custom signaling or non-React framework. Use RealtimeKit for faster development with React.\n\n## Setup\n\nDashboard: https://dash.cloudflare.com/?to=/:account/calls\n\nGet `CALLS_APP_ID` and `CALLS_APP_SECRET` from dashboard, then see configuration.md for deployment.\n\n## See Also\n\n- [Orange Meets Demo](https://demo.orange.cloudflare.dev/)\n- [Orange Source](https://github.com/cloudflare/orange)\n- [Calls Examples](https://github.com/cloudflare/calls-examples)\n- [API Reference](https://developers.cloudflare.com/api/resources/calls/)\n- [RealtimeKit Docs](https://developers.cloudflare.com/workers-ai/realtimekit/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/realtime-sfu/api.md",
    "content": "# API Reference\n\n## Authentication\n\n```bash\ncurl -X POST 'https://rtc.live/v1/apps/${CALLS_APP_ID}/sessions/new' \\\n  -H \"Authorization: Bearer ${CALLS_APP_SECRET}\"\n```\n\n## Core Concepts\n\n**Sessions:** PeerConnection to Cloudflare edge  \n**Tracks:** Media/data channels (audio/video/datachannel)  \n**No rooms:** Build presence via track sharing\n\n## Client Libraries\n\n**PartyTracks (Recommended):** Observable-based client library for production use. Handles device changes, network switches, ICE restarts automatically. Push/pull API with React hooks. See patterns.md for full examples.\n\n```bash\nnpm install partytracks @cloudflare/calls\n```\n\n**Raw API:** Direct HTTP + WebRTC for custom requirements (documented below).\n\n## Endpoints\n\n### Create Session\n```http\nPOST /v1/apps/{appId}/sessions/new\n→ {sessionId, sessionDescription}\n```\n\n### Add Track (Publish)\n```http\nPOST /v1/apps/{appId}/sessions/{sessionId}/tracks/new\nBody: {\n  sessionDescription: {sdp, type: \"offer\"},\n  tracks: [{location: \"local\", trackName: \"my-video\"}]\n}\n→ {sessionDescription, tracks: [{trackName}]}\n```\n\n### Add Track (Subscribe)\n```http\nPOST /v1/apps/{appId}/sessions/{sessionId}/tracks/new\nBody: {\n  tracks: [{\n    location: \"remote\",\n    trackName: \"remote-track-id\",\n    sessionId: \"other-session-id\"\n  }]\n}\n→ {sessionDescription} (server offer)\n```\n\n### Renegotiate\n```http\nPUT /v1/apps/{appId}/sessions/{sessionId}/renegotiate\nBody: {sessionDescription: {sdp, type: \"answer\"}}\n```\n\n### Close Tracks\n```http\nPUT /v1/apps/{appId}/sessions/{sessionId}/tracks/close\nBody: {tracks: [{trackName}]}\n→ {requiresImmediateRenegotiation: boolean}\n```\n\n### Get Session\n```http\nGET /v1/apps/{appId}/sessions/{sessionId}\n→ {sessionId, tracks: TrackMetadata[]}\n```\n\n## TypeScript Types\n\n```typescript\ninterface TrackMetadata {\n  trackName: string;\n  location: \"local\" | \"remote\";\n  sessionId?: string; // For remote tracks\n  mid?: string; // WebRTC mid\n}\n```\n\n## WebRTC Flow\n\n```typescript\n// 1. Create PeerConnection\nconst pc = new RTCPeerConnection({\n  iceServers: [{urls: 'stun:stun.cloudflare.com:3478'}]\n});\n\n// 2. Add tracks\nconst stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});\nstream.getTracks().forEach(track => pc.addTrack(track, stream));\n\n// 3. Create offer\nconst offer = await pc.createOffer();\nawait pc.setLocalDescription(offer);\n\n// 4. Send to backend → Cloudflare API\nconst response = await fetch('/api/new-session', {\n  method: 'POST',\n  body: JSON.stringify({sdp: offer.sdp})\n});\n\n// 5. Set remote answer\nconst {sessionDescription} = await response.json();\nawait pc.setRemoteDescription(sessionDescription);\n```\n\n## Publishing\n\n```typescript\nconst offer = await pc.createOffer();\nawait pc.setLocalDescription(offer);\n\nconst res = await fetch(`/api/sessions/${sessionId}/tracks`, {\n  method: 'POST',\n  body: JSON.stringify({\n    sdp: offer.sdp,\n    tracks: [{location: 'local', trackName: 'my-video'}]\n  })\n});\n\nconst {sessionDescription, tracks} = await res.json();\nawait pc.setRemoteDescription(sessionDescription);\nconst publishedTrackId = tracks[0].trackName; // Share with others\n```\n\n## Subscribing\n\n```typescript\nconst res = await fetch(`/api/sessions/${sessionId}/tracks`, {\n  method: 'POST',\n  body: JSON.stringify({\n    tracks: [{location: 'remote', trackName: remoteTrackId, sessionId: remoteSessionId}]\n  })\n});\n\nconst {sessionDescription} = await res.json();\nawait pc.setRemoteDescription(sessionDescription);\n\nconst answer = await pc.createAnswer();\nawait pc.setLocalDescription(answer);\n\nawait fetch(`/api/sessions/${sessionId}/renegotiate`, {\n  method: 'PUT',\n  body: JSON.stringify({sdp: answer.sdp})\n});\n\npc.ontrack = (event) => {\n  const [remoteStream] = event.streams;\n  videoElement.srcObject = remoteStream;\n};\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/realtime-sfu/configuration.md",
    "content": "# Configuration & Deployment\n\n## Dashboard Setup\n\n1. Navigate to https://dash.cloudflare.com/?to=/:account/calls\n2. Click \"Create Application\" (or use existing app)\n3. Copy `CALLS_APP_ID` from dashboard\n4. Generate and copy `CALLS_APP_SECRET` (treat as sensitive credential)\n5. Use credentials in Wrangler config or environment variables below\n\n## Dependencies\n\n**Backend (Workers):** Built-in fetch API, no additional packages required\n\n**Client (PartyTracks):**\n```bash\nnpm install partytracks @cloudflare/calls\n```\n\n**Client (React + PartyTracks):**\n```bash\nnpm install partytracks @cloudflare/calls observable-hooks\n# Observable hooks: useObservableAsValue, useValueAsObservable\n```\n\n**Client (Raw API):** Native browser WebRTC API only\n\n## Wrangler Setup\n\n```jsonc\n{\n  \"name\": \"my-calls-app\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\", // Use current date for new projects\n  \"vars\": {\n    \"CALLS_APP_ID\": \"your-app-id\",\n    \"MAX_WEBCAM_BITRATE\": \"1200000\",\n    \"MAX_WEBCAM_FRAMERATE\": \"24\",\n    \"MAX_WEBCAM_QUALITY_LEVEL\": \"1080\"\n  },\n  // Set secret: wrangler secret put CALLS_APP_SECRET\n  \"durable_objects\": {\n    \"bindings\": [\n      {\n        \"name\": \"ROOM\",\n        \"class_name\": \"Room\"\n      }\n    ]\n  }\n}\n```\n\n## Deploy\n\n```bash\nwrangler login\nwrangler secret put CALLS_APP_SECRET\nwrangler deploy\n```\n\n## Environment Variables\n\n**Required:**\n- `CALLS_APP_ID`: From dashboard\n- `CALLS_APP_SECRET`: From dashboard (secret)\n\n**Optional:**\n- `MAX_WEBCAM_BITRATE` (default: 1200000)\n- `MAX_WEBCAM_FRAMERATE` (default: 24)\n- `MAX_WEBCAM_QUALITY_LEVEL` (default: 1080)\n- `TURN_SERVICE_ID`: TURN service\n- `TURN_SERVICE_TOKEN`: TURN auth (secret)\n\n## TURN Configuration\n\n```javascript\nconst pc = new RTCPeerConnection({\n  iceServers: [\n    { urls: 'stun:stun.cloudflare.com:3478' },\n    {\n      urls: [\n        'turn:turn.cloudflare.com:3478?transport=udp',\n        'turn:turn.cloudflare.com:3478?transport=tcp',\n        'turns:turn.cloudflare.com:5349?transport=tcp'\n      ],\n      username: turnUsername,\n      credential: turnCredential\n    }\n  ],\n  bundlePolicy: 'max-bundle', // Recommended: reduces overhead\n  iceTransportPolicy: 'all'    // Use 'relay' to force TURN (testing only)\n});\n```\n\n**Ports:** 3478 (UDP/TCP), 53 (UDP), 80 (TCP), 443 (TLS), 5349 (TLS)\n\n**When to use TURN:** Required for restrictive corporate firewalls/networks that block UDP. ~5-10% of connections fallback to TURN. STUN works for most users.\n\n**ICE candidate filtering:** Cloudflare handles candidate filtering automatically. No need to manually filter candidates.\n\n## Durable Object Boilerplate\n\nMinimal presence system:\n\n```typescript\nexport class Room {\n  private sessions = new Map<string, {userId: string, tracks: string[]}>();\n\n  async fetch(req: Request) {\n    const {pathname} = new URL(req.url);\n    const body = await req.json();\n    \n    if (pathname === '/join') {\n      this.sessions.set(body.sessionId, {userId: body.userId, tracks: []});\n      return Response.json({participants: this.sessions.size});\n    }\n    \n    if (pathname === '/publish') {\n      this.sessions.get(body.sessionId)?.tracks.push(...body.tracks);\n      // Broadcast to others via WebSocket (not shown)\n      return new Response('OK');\n    }\n    \n    return new Response('Not found', {status: 404});\n  }\n}\n```\n\n## Environment Validation\n\nCheck credentials before first API call:\n\n```typescript\nif (!env.CALLS_APP_ID || !env.CALLS_APP_SECRET) {\n  throw new Error('CALLS_APP_ID and CALLS_APP_SECRET required');\n}\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/realtime-sfu/gotchas.md",
    "content": "# Gotchas & Troubleshooting\n\n## Common Errors\n\n### \"Slow initial connect (~1.8s)\"\n\n**Cause:** First STUN delayed during consensus forming (normal behavior)\n**Solution:** Subsequent connections are faster. CF detects DTLS ClientHello early to compensate.\n\n### \"No media flow\"\n\n**Cause:** SDP exchange incomplete, connection not established, tracks not added before offer, browser permissions missing\n**Solution:** \n1. Verify SDP exchange complete\n2. Check `pc.connectionState === 'connected'`\n3. Ensure tracks added before creating offer\n4. Confirm browser permissions granted\n5. Use `chrome://webrtc-internals` for debugging\n\n### \"Track not receiving\"\n\n**Cause:** Track not published, track ID not shared, session IDs mismatch, `pc.ontrack` not set, renegotiation needed\n**Solution:** \n1. Verify track published successfully\n2. Confirm track ID shared between peers\n3. Check session IDs match\n4. Set `pc.ontrack` handler before answer\n5. Trigger renegotiation if needed\n\n### \"ICE connection failed\"\n\n**Cause:** Network changed, firewall blocked UDP, TURN needed, transient network issue\n**Solution:**\n```typescript\npc.oniceconnectionstatechange = async () => {\n  if (pc.iceConnectionState === 'failed') {\n    console.warn('ICE failed, attempting restart');\n    await pc.restartIce(); // Triggers new ICE gathering\n    \n    // Create new offer with ICE restart flag\n    const offer = await pc.createOffer({iceRestart: true});\n    await pc.setLocalDescription(offer);\n    \n    // Send to backend → Cloudflare API\n    await fetch(`/api/sessions/${sessionId}/renegotiate`, {\n      method: 'PUT',\n      body: JSON.stringify({sdp: offer.sdp})\n    });\n  }\n};\n```\n\n### \"Track stuck/frozen\"\n\n**Cause:** Sender paused track, network congestion, codec mismatch, mobile browser backgrounded\n**Solution:**\n1. Check `track.enabled` and `track.readyState === 'live'`\n2. Verify sender active: `pc.getSenders().find(s => s.track === track)`\n3. Check stats for packet loss/jitter (see patterns.md)\n4. On mobile: Re-acquire tracks when app foregrounded\n5. Test with different codecs if persistent\n\n### \"Network change disconnects call\"\n\n**Cause:** Mobile switching WiFi↔cellular, laptop changing networks\n**Solution:**\n```typescript\n// Listen for network changes\nif ('connection' in navigator) {\n  (navigator as any).connection.addEventListener('change', async () => {\n    console.log('Network changed');\n    await pc.restartIce(); // Use ICE restart pattern above\n  });\n}\n\n// Or use PartyTracks (handles automatically)\n```\n\n## Retry with Exponential Backoff\n\n```typescript\nasync function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {\n  for (let i = 0; i < maxRetries; i++) {\n    try {\n      const res = await fetch(url, options);\n      if (res.ok) return res;\n      if (res.status >= 500) throw new Error('Server error');\n      return res; // Client error, don't retry\n    } catch (err) {\n      if (i === maxRetries - 1) throw err;\n      const delay = Math.min(1000 * 2 ** i, 10000); // Cap at 10s\n      await new Promise(resolve => setTimeout(resolve, delay));\n    }\n  }\n}\n```\n\n## Debugging with chrome://webrtc-internals\n\n1. Open `chrome://webrtc-internals` in Chrome/Edge\n2. Find your PeerConnection in the list\n3. Check **Stats graphs** for packet loss, jitter, bandwidth\n4. Check **ICE candidate pairs**: Look for `succeeded` state, relay vs host candidates\n5. Check **getStats**: Raw metrics for inbound/outbound RTP\n6. Look for errors in **Event log**: `iceConnectionState`, `connectionState` changes\n7. Export data with \"Download the PeerConnection updates and stats data\" button\n8. Common issues visible here: ICE failures, high packet loss, bitrate drops\n\n## Limits\n\n| Resource/Limit | Value | Notes |\n|----------------|-------|-------|\n| Egress (Free) | 1TB/month | Per account |\n| Egress (Paid) | $0.05/GB | After free tier |\n| Inbound traffic | Free | All plans |\n| TURN service | Free | Included with SFU |\n| Participants | No hard limit | Client bandwidth/CPU bound (typically 10-50 tracks) |\n| Tracks per session | No hard limit | Client resources limited |\n| Session duration | No hard limit | Production calls run for hours |\n| WebRTC ports | UDP 1024-65535 | Outbound only, required for media |\n| API rate limit | 600 req/min | Per app, burst allowed |\n\n## Security Checklist\n\n- ✅ **Never expose** `CALLS_APP_SECRET` to client\n- ✅ **Validate user identity** in backend before creating sessions\n- ✅ **Implement auth tokens** for session access (JWT in custom header)\n- ✅ **Rate limit** session creation endpoints\n- ✅ **Expire sessions** server-side after inactivity\n- ✅ **Validate track IDs** before subscribing (prevent unauthorized access)\n- ✅ **Use HTTPS** for all signaling (API calls)\n- ✅ **Enable DTLS-SRTP** (automatic with Cloudflare, encrypts media)\n- ⚠️ **Consider E2EE** for sensitive content (implement client-side with Insertable Streams API)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/realtime-sfu/patterns.md",
    "content": "# Patterns & Use Cases\n\n## Architecture\n\n```\nClient (WebRTC) <---> CF Edge <---> Backend (HTTP)\n                           |\n                    CF Backbone (310+ DCs)\n                           |\n                    Other Edges <---> Other Clients\n```\n\nAnycast: Last-mile <50ms (95%), no region select, NACK shield, distributed consensus\n\nCascading trees auto-scale to millions:\n```\nPublisher -> Edge A -> Edge B -> Sub1\n                    \\-> Edge C -> Sub2,3\n```\n\n## Use Cases\n\n**1:1:** A creates session+publishes, B creates+subscribes to A+publishes, A subscribes to B\n**N:N:** All create session+publish, backend broadcasts track IDs, all subscribe to others\n**1:N:** Publisher creates+publishes, viewers each create+subscribe (no fan-out limit)\n**Breakout:** Same PeerConnection! Backend closes/adds tracks, no recreation\n\n## PartyTracks (Recommended)\n\nObservable-based client with automatic device/network handling:\n\n```typescript\nimport {PartyTracks} from 'partytracks';\n\n// Create client\nconst pt = new PartyTracks({\n  apiUrl: '/api/calls',\n  sessionId: 'my-session',\n  onTrack: (track, peer) => {\n    const video = document.getElementById(`video-${peer.id}`) as HTMLVideoElement;\n    video.srcObject = new MediaStream([track]);\n  }\n});\n\n// Publish camera (push API)\nconst camera = await pt.getCamera(); // Auto-requests permissions, handles device changes\nawait pt.publishTrack(camera, {trackName: 'my-camera'});\n\n// Subscribe to remote track (pull API)\nawait pt.subscribeToTrack({trackName: 'remote-camera', sessionId: 'other-session'});\n\n// React hook example\nimport {useObservableAsValue} from 'observable-hooks';\n\nfunction VideoCall() {\n  const localTracks = useObservableAsValue(pt.localTracks$);\n  const remoteTracks = useObservableAsValue(pt.remoteTracks$);\n  \n  return <div>{/* Render tracks */}</div>;\n}\n\n// Screenshare\nconst screen = await pt.getScreenshare();\nawait pt.publishTrack(screen, {trackName: 'my-screen'});\n\n// Handle device changes (automatic)\n// PartyTracks detects device changes (e.g., Bluetooth headset) and renegotiates\n```\n\n## Backend\n\nExpress:\n```js\napp.post('/api/new-session', async (req, res) => {\n  const r = await fetch(`${CALLS_API}/apps/${process.env.CALLS_APP_ID}/sessions/new`,\n    {method: 'POST', headers: {'Authorization': `Bearer ${process.env.CALLS_APP_SECRET}`}});\n  res.json(await r.json());\n});\n```\n\nWorkers: Same pattern, use `env.CALLS_APP_ID` and `env.CALLS_APP_SECRET`\n\nDO Presence: See configuration.md for boilerplate\n\n## Audio Level Detection\n\n```typescript\n// Attach analyzer to audio track\nfunction attachAudioLevelDetector(track: MediaStreamTrack) {\n  const ctx = new AudioContext();\n  const analyzer = ctx.createAnalyser();\n  const src = ctx.createMediaStreamSource(new MediaStream([track]));\n  src.connect(analyzer);\n  \n  const data = new Uint8Array(analyzer.frequencyBinCount);\n  const checkLevel = () => {\n    analyzer.getByteFrequencyData(data);\n    const level = data.reduce((a, b) => a + b) / data.length;\n    if (level > 30) console.log('Speaking:', level); // Trigger UI update\n    requestAnimationFrame(checkLevel);\n  };\n  checkLevel();\n}\n```\n\n## Connection Quality Monitoring\n\n```typescript\npc.getStats().then(stats => {\n  stats.forEach(report => {\n    if (report.type === 'inbound-rtp' && report.kind === 'video') {\n      const {packetsLost, packetsReceived, jitter} = report;\n      const lossRate = packetsLost / (packetsLost + packetsReceived);\n      if (lossRate > 0.05) console.warn('High packet loss:', lossRate);\n      if (jitter > 100) console.warn('High jitter:', jitter);\n    }\n  });\n});\n```\n\n## Stage Management (Limit Visible Participants)\n\n```typescript\n// Subscribe to top 6 active speakers only\nlet activeSubscriptions = new Set<string>();\n\nfunction updateStage(topSpeakers: string[]) {\n  const toAdd = topSpeakers.filter(id => !activeSubscriptions.has(id)).slice(0, 6);\n  const toRemove = [...activeSubscriptions].filter(id => !topSpeakers.includes(id));\n  \n  toRemove.forEach(id => {\n    pc.getSenders().find(s => s.track?.id === id)?.track?.stop();\n    activeSubscriptions.delete(id);\n  });\n  \n  toAdd.forEach(async id => {\n    await fetch(`/api/subscribe`, {method: 'POST', body: JSON.stringify({trackId: id})});\n    activeSubscriptions.add(id);\n  });\n}\n```\n\n## Advanced\n\nBandwidth mgmt:\n```ts\nconst s = pc.getSenders().find(s => s.track?.kind === 'video');\nconst p = s.getParameters();\nif (!p.encodings) p.encodings = [{}];\np.encodings[0].maxBitrate = 1200000; p.encodings[0].maxFramerate = 24;\nawait s.setParameters(p);\n```\n\nSimulcast (CF auto-forwards best layer):\n```ts\npc.addTransceiver('video', {direction: 'sendonly', sendEncodings: [\n  {rid: 'high', maxBitrate: 1200000},\n  {rid: 'med', maxBitrate: 600000, scaleResolutionDownBy: 2},\n  {rid: 'low', maxBitrate: 200000, scaleResolutionDownBy: 4}\n]});\n```\n\nDataChannel:\n```ts\nconst dc = pc.createDataChannel('chat', {ordered: true, maxRetransmits: 3});\ndc.onopen = () => dc.send(JSON.stringify({type: 'chat', text: 'Hi'}));\ndc.onmessage = (e) => console.log('RX:', JSON.parse(e.data));\n```\n\n**WHIP/WHEP:** For streaming interop (OBS → SFU, SFU → video players), use WHIP (ingest) and WHEP (egress) protocols. See Cloudflare Stream integration docs.\n\nIntegrations: R2 for recording `env.R2_BUCKET.put(...)`, Queues for analytics\n\nPerf: 100-250ms connect, ~50ms latency (95%), 200-400ms glass-to-glass, no participant limit (client: 10-50 tracks)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/realtimekit/README.md",
    "content": "# Cloudflare RealtimeKit\n\nExpert guidance for building real-time video and audio applications using **Cloudflare RealtimeKit** - a comprehensive SDK suite for adding customizable live video and voice to web or mobile applications.\n\n## Overview\n\nRealtimeKit is Cloudflare's SDK suite built on Realtime SFU, abstracting WebRTC complexity with fast integration, pre-built UI components, global performance (300+ cities), and production features (recording, transcription, chat, polls).\n\n**Use cases**: Team meetings, webinars, social video, audio calls, interactive plugins\n\n## Core Concepts\n\n- **App**: Workspace grouping meetings, participants, presets, recordings. Use separate Apps for staging/production\n- **Meeting**: Re-usable virtual room. Each join creates new **Session**\n- **Session**: Live meeting instance. Created on first join, ends after last leave\n- **Participant**: User added via REST API. Returns `authToken` for client SDK. **Do not reuse tokens**\n- **Preset**: Reusable permission/UI template (permissions, meeting type, theme). Applied at participant creation\n- **Peer ID** (`id`): Unique per session, changes on rejoin\n- **Participant ID** (`userId`): Persistent across sessions\n\n## Quick Start\n\n### 1. Create App & Meeting (Backend)\n\n```bash\n# Create app\ncurl -X POST 'https://api.cloudflare.com/client/v4/accounts/<account_id>/realtime/kit/apps' \\\n  -H 'Authorization: Bearer <api_token>' \\\n  -d '{\"name\": \"My RealtimeKit App\"}'\n\n# Create meeting\ncurl -X POST 'https://api.cloudflare.com/client/v4/accounts/<account_id>/realtime/kit/<app_id>/meetings' \\\n  -H 'Authorization: Bearer <api_token>' \\\n  -d '{\"title\": \"Team Standup\"}'\n\n# Add participant\ncurl -X POST 'https://api.cloudflare.com/client/v4/accounts/<account_id>/realtime/kit/<app_id>/meetings/<meeting_id>/participants' \\\n  -H 'Authorization: Bearer <api_token>' \\\n  -d '{\"name\": \"Alice\", \"preset_name\": \"host\"}'\n# Returns: { authToken }\n```\n\n### 2. Client Integration\n\n**React**:\n```tsx\nimport { RtkMeeting } from '@cloudflare/realtimekit-react-ui';\n\nfunction App() {\n  return <RtkMeeting authToken=\"<participant_auth_token>\" onLeave={() => {}} />;\n}\n```\n\n**Core SDK**:\n```typescript\nimport RealtimeKitClient from '@cloudflare/realtimekit';\n\nconst meeting = new RealtimeKitClient({ authToken: '<token>', video: true, audio: true });\nawait meeting.join();\n```\n\n## Reading Order\n\n| Task | Files |\n|------|-------|\n| Quick integration | README only |\n| Custom UI | README → patterns → api |\n| Backend setup | README → configuration |\n| Debug issues | gotchas |\n| Advanced features | patterns → api |\n\n## RealtimeKit vs Realtime SFU\n\n| Choose | When |\n|--------|------|\n| **RealtimeKit** | Need pre-built UI, fast integration, React/Angular/HTML |\n| **Realtime SFU** | Building from scratch, custom WebRTC, full control |\n\nRealtimeKit is built on Realtime SFU but abstracts WebRTC complexity with UI components and SDKs.\n\n## Which Package?\n\nNeed pre-built meeting UI?\n- React → `@cloudflare/realtimekit-react-ui` (`<RtkMeeting>`)\n- Angular → `@cloudflare/realtimekit-angular-ui`\n- HTML/Vanilla → `@cloudflare/realtimekit-ui`\n\nNeed custom UI?\n- Core SDK → `@cloudflare/realtimekit` (RealtimeKitClient) - full control\n\nNeed raw WebRTC control?\n- See `realtime-sfu/` reference\n\n## In This Reference\n\n- [Configuration](./configuration.md) - Setup, installation, wrangler config\n- [API](./api.md) - Meeting object, REST API, SDK methods\n- [Patterns](./patterns.md) - Common workflows, code examples\n- [Gotchas](./gotchas.md) - Common issues, troubleshooting\n\n## See Also\n\n- [Workers](../workers/) - Backend integration\n- [D1](../d1/) - Meeting metadata storage\n- [R2](../r2/) - Recording storage\n- [KV](../kv/) - Session management\n\n## Reference Links\n\n- **Official Docs**: https://developers.cloudflare.com/realtime/realtimekit/\n- **API Reference**: https://developers.cloudflare.com/api/resources/realtime_kit/\n- **Examples**: https://github.com/cloudflare/realtimekit-web-examples\n- **Dashboard**: https://dash.cloudflare.com/?to=/:account/realtime/kit\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/realtimekit/api.md",
    "content": "# RealtimeKit API Reference\n\nComplete API reference for Meeting object, REST endpoints, and SDK methods.\n\n## Meeting Object API\n\n### `meeting.self` - Local Participant\n\n```typescript\n// Properties: id, userId, name, audioEnabled, videoEnabled, screenShareEnabled, audioTrack, videoTrack, screenShareTracks, roomJoined, roomState\n// Methods\nawait meeting.self.enableAudio() / disableAudio() / enableVideo() / disableVideo() / enableScreenShare() / disableScreenShare()\nawait meeting.self.setName(\"Name\")  // Before join only\nawait meeting.self.setDevice(device)\nconst devices = await meeting.self.getAllDevices() / getAudioDevices() / getVideoDevices() / getSpeakerDevices()\n// Events: 'roomJoined', 'audioUpdate', 'videoUpdate', 'screenShareUpdate', 'deviceUpdate', 'deviceListUpdate'\nmeeting.self.on('roomJoined', () => {})\nmeeting.self.on('audioUpdate', ({ audioEnabled, audioTrack }) => {})\n```\n\n### `meeting.participants` - Remote Participants\n\n**Collections**:\n```typescript\nmeeting.participants.joined / active / waitlisted / pinned  // Maps\nconst participants = meeting.participants.joined.toArray()\nconst count = meeting.participants.joined.size()\nconst p = meeting.participants.joined.get('peer-id')\n```\n\n**Participant Properties**:\n```typescript\nparticipant.id / userId / name\nparticipant.audioEnabled / videoEnabled / screenShareEnabled\nparticipant.audioTrack / videoTrack / screenShareTracks\n```\n\n**Events**:\n```typescript\nmeeting.participants.joined.on('participantJoined', (participant) => {})\nmeeting.participants.joined.on('participantLeft', (participant) => {})\n```\n\n### `meeting.meta` - Metadata\n```typescript\nmeeting.meta.meetingId / meetingTitle / meetingStartedTimestamp\n```\n\n### `meeting.chat` - Chat\n```typescript\nmeeting.chat.messages  // Array\nawait meeting.chat.sendTextMessage(\"Hello\") / sendImageMessage(file)\nmeeting.chat.on('chatUpdate', ({ message, messages }) => {})\n```\n\n### `meeting.polls` - Polling\n```typescript\nmeeting.polls.items  // Array\nawait meeting.polls.create(question, options, anonymous, hideVotes)\nawait meeting.polls.vote(pollId, optionIndex)\n```\n\n### `meeting.plugins` - Collaborative Apps\n```typescript\nmeeting.plugins.all  // Array\nawait meeting.plugins.activate(pluginId) / deactivate()\n```\n\n### `meeting.ai` - AI Features\n```typescript\nmeeting.ai.transcripts  // Live transcriptions (when enabled in Preset)\n```\n\n### Core Methods\n```typescript\nawait meeting.join()   // Emits 'roomJoined' on meeting.self\nawait meeting.leave()\n```\n\n## TypeScript Types\n\n```typescript\nimport type { RealtimeKitClient, States, UIConfig, Participant } from '@cloudflare/realtimekit';\n\n// Main interface\ninterface RealtimeKitClient {\n  self: SelfState;          // Local participant (id, userId, name, audioEnabled, videoEnabled, roomJoined, roomState)\n  participants: { joined, active, waitlisted, pinned };  // Reactive Maps\n  chat: ChatNamespace;      // messages[], sendTextMessage(), sendImageMessage()\n  polls: PollsNamespace;    // items[], create(), vote()\n  plugins: PluginsNamespace;  // all[], activate(), deactivate()\n  ai: AINamespace;          // transcripts[]\n  meta: MetaState;          // meetingId, meetingTitle, meetingStartedTimestamp\n  join(): Promise<void>;\n  leave(): Promise<void>;\n}\n\n// Participant (self & remote share same shape)\ninterface Participant {\n  id: string;                      // Peer ID (changes on rejoin)\n  userId: string;                  // Persistent participant ID\n  name: string;\n  audioEnabled: boolean;\n  videoEnabled: boolean;\n  screenShareEnabled: boolean;\n  audioTrack: MediaStreamTrack | null;\n  videoTrack: MediaStreamTrack | null;\n  screenShareTracks: MediaStreamTrack[];\n}\n```\n\n## Store Architecture\n\nRealtimeKit uses reactive store (event-driven updates, live Maps):\n\n```typescript\n// Subscribe to state changes\nmeeting.self.on('audioUpdate', ({ audioEnabled, audioTrack }) => {});\nmeeting.participants.joined.on('participantJoined', (p) => {});\n\n// Access current state synchronously\nconst isAudioOn = meeting.self.audioEnabled;\nconst count = meeting.participants.joined.size();\n```\n\n**Key principles:** State updates emit events after changes. Use `.toArray()` sparingly. Collections are live Maps.\n\n## REST API\n\nBase: `https://api.cloudflare.com/client/v4/accounts/{account_id}/realtime/kit/{app_id}`\n\n### Meetings\n```bash\nGET    /meetings                                    # List all\nGET    /meetings/{meeting_id}                       # Get details\nPOST   /meetings                                    # Create: {\"title\": \"...\"}\nPATCH  /meetings/{meeting_id}                       # Update: {\"title\": \"...\", \"record_on_start\": true}\n```\n\n### Participants\n```bash\nGET    /meetings/{meeting_id}/participants                          # List all\nGET    /meetings/{meeting_id}/participants/{participant_id}         # Get details\nPOST   /meetings/{meeting_id}/participants                          # Add: {\"name\": \"...\", \"preset_name\": \"...\", \"custom_participant_id\": \"...\"}\nPATCH  /meetings/{meeting_id}/participants/{participant_id}         # Update: {\"name\": \"...\", \"preset_name\": \"...\"}\nDELETE /meetings/{meeting_id}/participants/{participant_id}         # Delete\nPOST   /meetings/{meeting_id}/participants/{participant_id}/token   # Refresh token\n```\n\n### Active Session\n```bash\nGET  /meetings/{meeting_id}/active-session               # Get active session\nPOST /meetings/{meeting_id}/active-session/kick          # Kick users: {\"user_ids\": [\"id1\", \"id2\"]}\nPOST /meetings/{meeting_id}/active-session/kick-all      # Kick all\nPOST /meetings/{meeting_id}/active-session/poll          # Create poll: {\"question\": \"...\", \"options\": [...], \"anonymous\": false}\n```\n\n### Recording\n```bash\nGET  /recordings?meeting_id={meeting_id}                 # List recordings\nGET  /recordings/active-recording/{meeting_id}           # Get active recording\nPOST /recordings                                         # Start: {\"meeting_id\": \"...\", \"type\": \"composite\"} (or \"track\")\nPUT  /recordings/{recording_id}                          # Control: {\"action\": \"pause\"} (or \"resume\", \"stop\")\nPOST /recordings/track                                   # Track recording: {\"meeting_id\": \"...\", \"layers\": [...]}\n```\n\n### Livestreaming\n```bash\nGET  /livestreams?exclude_meetings=false                                # List all\nGET  /livestreams/{livestream_id}                                       # Get details\nPOST /meetings/{meeting_id}/livestreams                                 # Start for meeting\nPOST /meetings/{meeting_id}/active-livestream/stop                      # Stop\nPOST /livestreams                                                       # Create independent: returns {ingest_server, stream_key, playback_url}\n```\n\n### Sessions & Analytics\n```bash\nGET  /sessions                                                          # List all\nGET  /sessions/{session_id}                                             # Get details\nGET  /sessions/{session_id}/participants                                # List participants\nGET  /sessions/{session_id}/participants/{participant_id}               # Call stats\nGET  /sessions/{session_id}/chat                                        # Download chat CSV\nGET  /sessions/{session_id}/transcript                                  # Download transcript CSV\nGET  /sessions/{session_id}/summary                                     # Get summary\nPOST /sessions/{session_id}/summary                                     # Generate summary\nGET  /analytics/daywise?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD      # Day-wise analytics\nGET  /analytics/livestreams/overall                                     # Livestream analytics\n```\n\n### Webhooks\n```bash\nGET    /webhooks                    # List all\nPOST   /webhooks                    # Create: {\"url\": \"https://...\", \"events\": [\"session.started\", \"session.ended\"]}\nPATCH  /webhooks/{webhook_id}       # Update\nDELETE /webhooks/{webhook_id}       # Delete\n```\n\n## Session Lifecycle\n\n```\nInitialization → Join Intent → [Waitlist?] → Meeting Screen (Stage) → Ended\n                                   ↓ Approved\n                               [Rejected → Ended]\n```\n\nUI Kit handles state transitions automatically.\n\n## See Also\n\n- [Configuration](./configuration.md) - Setup and installation\n- [Patterns](./patterns.md) - Usage examples\n- [README](./README.md) - Overview and quick start\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/realtimekit/configuration.md",
    "content": "# RealtimeKit Configuration\n\nConfiguration guide for RealtimeKit setup, client SDKs, and wrangler integration.\n\n## Installation\n\n### React\n```bash\nnpm install @cloudflare/realtimekit @cloudflare/realtimekit-react-ui\n```\n\n### Angular\n```bash\nnpm install @cloudflare/realtimekit @cloudflare/realtimekit-angular-ui\n```\n\n### Web Components/HTML\n```bash\nnpm install @cloudflare/realtimekit @cloudflare/realtimekit-ui\n```\n\n## Client SDK Configuration\n\n### React UI Kit\n```tsx\nimport { RtkMeeting } from '@cloudflare/realtimekit-react-ui';\n<RtkMeeting authToken=\"<token>\" onLeave={() => {}} />\n```\n\n### Angular UI Kit\n```typescript\n@Component({ template: `<rtk-meeting [authToken]=\"authToken\" (rtkLeave)=\"onLeave($event)\"></rtk-meeting>` })\nexport class AppComponent { authToken = '<token>'; onLeave() {} }\n```\n\n### Web Components\n```html\n<script type=\"module\" src=\"https://cdn.jsdelivr.net/npm/@cloudflare/realtimekit-ui/dist/realtimekit-ui/realtimekit-ui.esm.js\"></script>\n<rtk-meeting id=\"meeting\"></rtk-meeting>\n<script>\n  document.getElementById('meeting').authToken = '<token>';\n</script>\n```\n\n### Core SDK Configuration\n```typescript\nimport RealtimeKitClient from '@cloudflare/realtimekit';\n\nconst meeting = new RealtimeKitClient({\n  authToken: '<token>',\n  video: true, audio: true, autoSwitchAudioDevice: true,\n  mediaConfiguration: {\n    video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } },\n    audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },\n    screenshare: { width: { max: 1920 }, height: { max: 1080 }, frameRate: { ideal: 15 } }\n  }\n});\nawait meeting.join();\n```\n\n## Backend Setup\n\n### Create App & Credentials\n\n**Dashboard**: https://dash.cloudflare.com/?to=/:account/realtime/kit\n\n**API**:\n```bash\ncurl -X POST 'https://api.cloudflare.com/client/v4/accounts/<account_id>/realtime/kit/apps' \\\n  -H 'Content-Type: application/json' \\\n  -H 'Authorization: Bearer <api_token>' \\\n  -d '{\"name\": \"My RealtimeKit App\"}'\n```\n\n**Required Permissions**: API token with **Realtime / Realtime Admin** permissions\n\n### Create Presets\n\n```bash\ncurl -X POST 'https://api.cloudflare.com/client/v4/accounts/<account_id>/realtime/kit/<app_id>/presets' \\\n  -H 'Authorization: Bearer <api_token>' \\\n  -d '{\n    \"name\": \"host\",\n    \"permissions\": {\n      \"canShareAudio\": true,\n      \"canShareVideo\": true,\n      \"canRecord\": true,\n      \"canLivestream\": true,\n      \"canStartStopRecording\": true\n    }\n  }'\n```\n\n## Wrangler Configuration\n\n### Basic Configuration\n```jsonc\n// wrangler.jsonc\n{\n  \"name\": \"realtimekit-app\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\",  // Use current date\n  \"vars\": {\n    \"CLOUDFLARE_ACCOUNT_ID\": \"abc123\",\n    \"REALTIMEKIT_APP_ID\": \"xyz789\"\n  }\n  // Secrets: wrangler secret put CLOUDFLARE_API_TOKEN\n}\n```\n\n### With Database & Storage\n```jsonc\n{\n  \"d1_databases\": [{ \"binding\": \"DB\", \"database_name\": \"meetings\", \"database_id\": \"d1-id\" }],\n  \"r2_buckets\": [{ \"binding\": \"RECORDINGS\", \"bucket_name\": \"recordings\" }],\n  \"kv_namespaces\": [{ \"binding\": \"SESSIONS\", \"id\": \"kv-id\" }]\n}\n```\n\n### Multi-Environment\n```bash\n# Deploy to environments\nwrangler deploy --env staging\nwrangler deploy --env production\n```\n\n## TURN Service Configuration\n\nRealtimeKit can use Cloudflare's TURN service for connectivity through restrictive networks:\n\n```jsonc\n// wrangler.jsonc\n{\n  \"vars\": {\n    \"TURN_SERVICE_ID\": \"your_turn_service_id\"\n  }\n  // Set secret: wrangler secret put TURN_SERVICE_TOKEN\n}\n```\n\nTURN automatically configured when enabled in account - no client-side changes needed.\n\n## Theming & Design Tokens\n\n```typescript\nimport type { UIConfig } from '@cloudflare/realtimekit';\n\nconst uiConfig: UIConfig = {\n  designTokens: {\n    colors: {\n      brand: { 500: '#0066ff', 600: '#0052cc' },\n      background: { 1000: '#1A1A1A', 900: '#2D2D2D' },\n      text: { 1000: '#FFFFFF', 900: '#E0E0E0' }\n    },\n    borderRadius: 'extra-rounded',  // 'rounded' | 'extra-rounded' | 'sharp'\n    theme: 'dark'  // 'light' | 'dark'\n  },\n  logo: { url: 'https://example.com/logo.png', altText: 'Company' }\n};\n\n// Apply to React\n<RtkMeeting authToken={token} config={uiConfig} onLeave={() => {}} />\n\n// Or use CSS variables\n// :root { --rtk-color-brand-500: #0066ff; --rtk-border-radius: 12px; }\n```\n\n## Internationalization (i18n)\n\n### Custom Language Strings\n```typescript\nimport { useLanguage } from '@cloudflare/realtimekit-ui';\n\nconst customLanguage = {\n  'join': 'Entrar',\n  'leave': 'Salir',\n  'mute': 'Silenciar',\n  'unmute': 'Activar audio',\n  'turn_on_camera': 'Encender cámara',\n  'turn_off_camera': 'Apagar cámara',\n  'share_screen': 'Compartir pantalla',\n  'stop_sharing': 'Dejar de compartir'\n};\n\nconst t = useLanguage(customLanguage);\n\n// React usage\n<RtkMeeting authToken={token} t={t} onLeave={() => {}} />\n```\n\n### Supported Locales\nDefault locales available: `en`, `es`, `fr`, `de`, `pt`, `ja`, `zh`\n\n```typescript\nimport { setLocale } from '@cloudflare/realtimekit-ui';\nsetLocale('es');  // Switch to Spanish\n```\n\n## See Also\n\n- [API](./api.md) - Meeting APIs, REST endpoints\n- [Patterns](./patterns.md) - Backend integration examples\n- [README](./README.md) - Overview and quick start\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/realtimekit/gotchas.md",
    "content": "# RealtimeKit Gotchas & Troubleshooting\n\n## Common Errors\n\n### \"Cannot connect to meeting\"\n\n**Cause:** Auth token invalid/expired, API credentials lack permissions, or network blocks WebRTC\n**Solution:**\nVerify token validity, check API token has **Realtime / Realtime Admin** permissions, enable TURN service for restrictive networks\n\n### \"No video/audio tracks\"\n\n**Cause:** Browser permissions not granted, video/audio not enabled, device in use, or device unavailable\n**Solution:**\nRequest browser permissions explicitly, verify initialization config, use `meeting.self.getAllDevices()` to debug, close other apps using device\n\n### \"Participant count mismatched\"\n\n**Cause:** `meeting.participants` doesn't include `meeting.self`\n**Solution:** Total count = `meeting.participants.joined.size() + 1`\n\n### \"Events not firing\"\n\n**Cause:** Listeners registered after actions, incorrect event name, or wrong namespace\n**Solution:**\nRegister listeners before calling `meeting.join()`, check event names against docs, verify correct namespace\n\n### \"CORS errors in API calls\"\n\n**Cause:** Making REST API calls from client-side\n**Solution:** All REST API calls **must** be server-side (Workers, backend). Never expose API tokens to clients.\n\n### \"Preset not applying\"\n\n**Cause:** Preset doesn't exist, name mismatch (case-sensitive), or participant created before preset\n**Solution:**\nVerify preset exists via Dashboard or API, check exact spelling and case, create preset before adding participants\n\n### \"Token reuse error\"\n\n**Cause:** Reusing participant tokens across sessions\n**Solution:** Generate fresh token per session. Use refresh endpoint if token expires during session.\n\n### \"Video quality poor\"\n\n**Cause:** Insufficient bandwidth, resolution/bitrate too high, or CPU overload\n**Solution:**\nLower `mediaConfiguration.video` resolution/frameRate, monitor network conditions, reduce participant count or grid size\n\n### \"Echo or audio feedback\"\n\n**Cause:** Multiple devices picking up same audio source\n**Solution:**\n- Lower `mediaConfiguration.video` resolution/frameRate\n- Monitor network conditions\n- Reduce participant count or grid size\n\n### Issue: Echo or audio feedback\n**Cause**: Multiple devices picking up same audio source\n\n**Solutions**:\nEnable `echoCancellation: true` in `mediaConfiguration.audio`, use headphones, mute when not speaking\n\n### \"Screen share not working\"\n\n**Cause:** Browser doesn't support screen sharing API, permission denied, or wrong `displaySurface` config\n**Solution:**\nUse Chrome/Edge/Firefox (Safari limited support), check browser permissions, try different `displaySurface` values ('window', 'monitor', 'browser')\n\n### \"How do I schedule meetings?\"\n\n**Cause:** RealtimeKit has no built-in scheduling system\n**Solution:**\nStore meeting IDs in your database with timestamps. Generate participant tokens only when user should join. Example:\n```typescript\n// Store in DB\n{ meetingId: 'abc123', scheduledFor: '2026-02-15T10:00:00Z', userId: 'user456' }\n\n// Generate token when user clicks \"Join\" near scheduled time\nconst response = await fetch('/api/join-meeting', {\n  method: 'POST',\n  body: JSON.stringify({ meetingId: 'abc123' })\n});\nconst { authToken } = await response.json();\n```\n\n### \"Recording not starting\"\n\n**Cause:** Preset lacks recording permissions, no active session, or API call from client\n**Solution:**\nVerify preset has `canRecord: true` and `canStartStopRecording: true`, ensure session is active (at least one participant), make recording API calls server-side only\n\n## Limits\n\n| Resource | Limit |\n|----------|-------|\n| Max participants per session | 100 |\n| Max concurrent sessions per App | 1000 |\n| Max recording duration | 6 hours |\n| Max meeting duration | 24 hours |\n| Max chat message length | 4000 characters |\n| Max preset name length | 64 characters |\n| Max meeting title length | 256 characters |\n| Max participant name length | 256 characters |\n| Token expiration | 24 hours (default) |\n| WebRTC ports required | UDP 1024-65535 |\n\n## Network Requirements\n\n### Firewall Rules\nAllow outbound UDP/TCP to:\n- `*.cloudflare.com` ports 443, 80\n- UDP ports 1024-65535 (WebRTC media)\n\n### TURN Service\nEnable for users behind restrictive firewalls/proxies:\n```jsonc\n// wrangler.jsonc\n{\n  \"vars\": {\n    \"TURN_SERVICE_ID\": \"your_turn_service_id\"\n  }\n  // Set secret: wrangler secret put TURN_SERVICE_TOKEN\n}\n```\n\nTURN automatically configured in SDK when enabled in account.\n\n## Debugging Tips\n\n```typescript\n// Check devices\nconst devices = await meeting.self.getAllDevices();\nmeeting.self.on('deviceListUpdate', ({ added, removed, devices }) => console.log('Devices:', { added, removed, devices }));\n\n// Monitor participants\nmeeting.participants.joined.on('participantJoined', (p) => console.log(`${p.name} joined:`, { id: p.id, userId: p.userId, audioEnabled: p.audioEnabled, videoEnabled: p.videoEnabled }));\n\n// Check room state\nmeeting.self.on('roomJoined', () => console.log('Room:', { meetingId: meeting.meta.meetingId, meetingTitle: meeting.meta.meetingTitle, participantCount: meeting.participants.joined.size() + 1, audioEnabled: meeting.self.audioEnabled, videoEnabled: meeting.self.videoEnabled }));\n\n// Log all events\n['roomJoined', 'audioUpdate', 'videoUpdate', 'screenShareUpdate', 'deviceUpdate', 'deviceListUpdate'].forEach(event => meeting.self.on(event, (data) => console.log(`[self] ${event}:`, data)));\n['participantJoined', 'participantLeft'].forEach(event => meeting.participants.joined.on(event, (data) => console.log(`[participants] ${event}:`, data)));\nmeeting.chat.on('chatUpdate', (data) => console.log('[chat] chatUpdate:', data));\n```\n\n## Security & Performance\n\n### Security: Do NOT\n- Expose `CLOUDFLARE_API_TOKEN` in client code, hardcode credentials in frontend\n- Reuse participant tokens, store tokens in localStorage without encryption\n- Allow client-side meeting creation\n\n### Security: DO\n- Generate tokens server-side only, use HTTPS, implement rate limiting\n- Validate user auth before generating tokens, use `custom_participant_id` to map to your user system\n- Set appropriate preset permissions per user role, rotate API tokens regularly\n\n### Performance\n- **CPU**: Lower video resolution/frameRate, disable video for audio-only, use `meeting.participants.active` for large meetings, implement virtual scrolling\n- **Bandwidth**: Set max resolution in `mediaConfiguration`, disable screenshare audio if unneeded, use audio-only mode, implement adaptive bitrate\n- **Memory**: Clean up event listeners on unmount, call `meeting.leave()` when done, don't store large participant arrays\n\n## In This Reference\n- [README.md](README.md) - Overview, core concepts, quick start\n- [configuration.md](configuration.md) - SDK config, presets, wrangler setup\n- [api.md](api.md) - Client SDK APIs, REST endpoints\n- [patterns.md](patterns.md) - Common patterns, React hooks, backend integration\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/realtimekit/patterns.md",
    "content": "# RealtimeKit Patterns\n\n## UI Kit (Minimal Code)\n\n```tsx\n// React\nimport { RtkMeeting } from '@cloudflare/realtimekit-react-ui';\n<RtkMeeting authToken=\"<token>\" onLeave={() => console.log('Left')} />\n\n// Angular\n@Component({ template: `<rtk-meeting [authToken]=\"authToken\" (rtkLeave)=\"onLeave($event)\"></rtk-meeting>` })\nexport class AppComponent { authToken = '<token>'; onLeave(event: unknown) {} }\n\n// HTML/Web Components\n<script type=\"module\" src=\"https://cdn.jsdelivr.net/npm/@cloudflare/realtimekit-ui/dist/realtimekit-ui/realtimekit-ui.esm.js\"></script>\n<rtk-meeting id=\"meeting\"></rtk-meeting>\n<script>document.getElementById('meeting').authToken = '<token>';</script>\n```\n\n## UI Components\n\nRealtimeKit provides 133+ pre-built Stencil.js Web Components with framework wrappers:\n\n### Layout Components\n- `<RtkMeeting>` - Full meeting UI (all-in-one)\n- `<RtkHeader>`, `<RtkStage>`, `<RtkControlbar>` - Layout sections\n- `<RtkSidebar>` - Chat/participants sidebar\n- `<RtkGrid>` - Adaptive video grid\n\n### Control Components  \n- `<RtkMicToggle>`, `<RtkCameraToggle>` - Media controls\n- `<RtkScreenShareToggle>` - Screen sharing\n- `<RtkLeaveButton>` - Leave meeting\n- `<RtkSettingsModal>` - Device settings\n\n### Grid Variants\n- `<RtkSpotlightGrid>` - Active speaker focus\n- `<RtkAudioGrid>` - Audio-only mode\n- `<RtkPaginatedGrid>` - Paginated layout\n\n**See full catalog**: https://docs.realtime.cloudflare.com/ui-kit\n\n## Core SDK Patterns\n\n### Basic Setup\n```typescript\nimport RealtimeKitClient from '@cloudflare/realtimekit';\n\nconst meeting = new RealtimeKitClient({ authToken, video: true, audio: true });\nmeeting.self.on('roomJoined', () => console.log('Joined:', meeting.meta.meetingTitle));\nmeeting.participants.joined.on('participantJoined', (p) => console.log(`${p.name} joined`));\nawait meeting.join();\n```\n\n### Video Grid & Device Selection\n```typescript\n// Video grid\nfunction VideoGrid({ meeting }) {\n  const [participants, setParticipants] = useState([]);\n  useEffect(() => {\n    const update = () => setParticipants(meeting.participants.joined.toArray());\n    meeting.participants.joined.on('participantJoined', update);\n    meeting.participants.joined.on('participantLeft', update);\n    update();\n    return () => { meeting.participants.joined.off('participantJoined', update); meeting.participants.joined.off('participantLeft', update); };\n  }, [meeting]);\n  return <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))' }}>\n    {participants.map(p => <VideoTile key={p.id} participant={p} />)}\n  </div>;\n}\n\nfunction VideoTile({ participant }) {\n  const videoRef = useRef<HTMLVideoElement>(null);\n  useEffect(() => {\n    if (videoRef.current && participant.videoTrack) videoRef.current.srcObject = new MediaStream([participant.videoTrack]);\n  }, [participant.videoTrack]);\n  return <div><video ref={videoRef} autoPlay playsInline muted /><div>{participant.name}</div></div>;\n}\n\n// Device selection\nconst devices = await meeting.self.getAllDevices();\nconst switchCamera = (deviceId: string) => {\n  const device = devices.find(d => d.deviceId === deviceId);\n  if (device) await meeting.self.setDevice(device);\n};\n```\n\n## React Hooks (Official)\n\n```typescript\nimport { useRealtimeKitClient, useRealtimeKitSelector } from '@cloudflare/realtimekit-react-ui';\n\nfunction MyComponent() {\n  const [meeting, initMeeting] = useRealtimeKitClient();\n  const audioEnabled = useRealtimeKitSelector(m => m.self.audioEnabled);\n  const participantCount = useRealtimeKitSelector(m => m.participants.joined.size());\n  \n  useEffect(() => { initMeeting({ authToken: '<token>' }); }, []);\n  \n  return <div>\n    <button onClick={() => meeting?.self.enableAudio()}>{audioEnabled ? 'Mute' : 'Unmute'}</button>\n    <span>{participantCount} participants</span>\n  </div>;\n}\n```\n\n**Benefits:** Automatic re-renders, memoized selectors, type-safe\n\n## Waitlist Handling\n\n```typescript\n// Monitor waitlist\nmeeting.participants.waitlisted.on('participantJoined', (participant) => {\n  console.log(`${participant.name} is waiting`);\n  // Show admin UI to approve/reject\n});\n\n// Approve from waitlist (backend only)\nawait fetch(\n  `https://api.cloudflare.com/client/v4/accounts/${accountId}/realtime/kit/${appId}/meetings/${meetingId}/active-session/waitlist/approve`,\n  {\n    method: 'POST',\n    headers: { 'Authorization': `Bearer ${apiToken}` },\n    body: JSON.stringify({ user_ids: [participant.userId] })\n  }\n);\n\n// Client receives automatic transition when approved\nmeeting.self.on('roomJoined', () => console.log('Approved and joined'));\n```\n\n## Audio-Only Mode\n\n```typescript\nconst meeting = new RealtimeKitClient({\n  authToken: '<token>',\n  video: false,  // Disable video\n  audio: true,\n  mediaConfiguration: {\n    audio: {\n      echoCancellation: true,\n      noiseSuppression: true,\n      autoGainControl: true\n    }\n  }\n});\n\n// Use audio grid component\nimport { RtkAudioGrid } from '@cloudflare/realtimekit-react-ui';\n<RtkAudioGrid meeting={meeting} />\n```\n\n## Addon System\n\n```typescript\n// List available addons\nmeeting.plugins.all.forEach(plugin => {\n  console.log(plugin.id, plugin.name, plugin.active);\n});\n\n// Activate collaborative app\nawait meeting.plugins.activate('whiteboard-addon-id');\n\n// Listen for activations\nmeeting.plugins.on('pluginActivated', ({ plugin }) => {\n  console.log(`${plugin.name} activated`);\n});\n\n// Deactivate\nawait meeting.plugins.deactivate();\n```\n\n## Backend Integration\n\n### Token Generation (Workers)\n```typescript\nexport interface Env { CLOUDFLARE_API_TOKEN: string; CLOUDFLARE_ACCOUNT_ID: string; REALTIMEKIT_APP_ID: string; }\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const url = new URL(request.url);\n    \n    if (url.pathname === '/api/join-meeting') {\n      const { meetingId, userName, presetName } = await request.json();\n      const response = await fetch(\n        `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/realtime/kit/${env.REALTIMEKIT_APP_ID}/meetings/${meetingId}/participants`,\n        {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${env.CLOUDFLARE_API_TOKEN}` },\n          body: JSON.stringify({ name: userName, preset_name: presetName })\n        }\n      );\n      const data = await response.json();\n      return Response.json({ authToken: data.result.authToken });\n    }\n    \n    return new Response('Not found', { status: 404 });\n  }\n};\n```\n\n## Best Practices\n\n### Security\n1. **Never expose API tokens client-side** - Generate participant tokens server-side only\n2. **Don't reuse participant tokens** - Generate fresh token per session, use refresh endpoint if expired\n3. **Use custom participant IDs** - Map to your user system for cross-session tracking\n\n### Performance\n1. **Event-driven updates** - Listen to events, don't poll. Use `toArray()` only when needed\n2. **Media quality constraints** - Set appropriate resolution/bitrate limits based on network conditions\n3. **Device management** - Enable `autoSwitchAudioDevice` for better UX, handle device list updates\n\n### Architecture\n1. **Separate Apps for environments** - staging vs production to prevent data mixing\n2. **Preset strategy** - Create presets at App level, reuse across meetings\n3. **Token management** - Backend generates tokens, frontend receives via authenticated endpoint\n\n## In This Reference\n- [README.md](README.md) - Overview, core concepts, quick start\n- [configuration.md](configuration.md) - SDK config, presets, wrangler setup\n- [api.md](api.md) - Client SDK APIs, REST endpoints\n- [gotchas.md](gotchas.md) - Common issues, troubleshooting, limits\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/sandbox/README.md",
    "content": "# Cloudflare Sandbox SDK\n\nSecure isolated code execution in containers on Cloudflare's edge. Run untrusted code, manage files, expose services, integrate with AI agents.\n\n**Use cases**: AI code execution, interactive dev environments, data analysis, CI/CD, code interpreters, multi-tenant execution.\n\n## Architecture\n\n- Each sandbox = Durable Object + Container\n- Persistent across requests (same ID = same sandbox)\n- Isolated filesystem/processes/network\n- Configurable sleep/wake for cost optimization\n\n## Quick Start\n\n```typescript\nimport { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox';\nexport { Sandbox } from '@cloudflare/sandbox';\n\ntype Env = { Sandbox: DurableObjectNamespace<Sandbox>; };\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // CRITICAL: proxyToSandbox MUST be called first for preview URLs\n    const proxyResponse = await proxyToSandbox(request, env);\n    if (proxyResponse) return proxyResponse;\n\n    const sandbox = getSandbox(env.Sandbox, 'my-sandbox');\n    const result = await sandbox.exec('python3 -c \"print(2 + 2)\"');\n    return Response.json({ output: result.stdout });\n  }\n};\n```\n\n**wrangler.jsonc**:\n```jsonc\n{\n  \"name\": \"my-sandbox-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\", // Use current date for new projects\n  \n  \"containers\": [{\n    \"class_name\": \"Sandbox\",\n    \"image\": \"./Dockerfile\",\n    \"instance_type\": \"lite\",        // lite | standard | heavy\n    \"max_instances\": 5\n  }],\n  \n  \"durable_objects\": {\n    \"bindings\": [{ \"class_name\": \"Sandbox\", \"name\": \"Sandbox\" }]\n  },\n  \n  \"migrations\": [{\n    \"tag\": \"v1\",\n    \"new_sqlite_classes\": [\"Sandbox\"]\n  }]\n}\n```\n\n**Dockerfile**:\n```dockerfile\nFROM docker.io/cloudflare/sandbox:latest\nRUN pip3 install --no-cache-dir pandas numpy matplotlib\nEXPOSE 8080 3000  # Required for wrangler dev\n```\n\n## Core APIs\n\n- `getSandbox(namespace, id, options?)` → Get/create sandbox\n- `sandbox.exec(command, options?)` → Execute command\n- `sandbox.readFile(path)` / `writeFile(path, content)` → File ops\n- `sandbox.startProcess(command, options)` → Background process\n- `sandbox.exposePort(port, options)` → Get preview URL\n- `sandbox.createSession(options)` → Isolated session\n- `sandbox.wsConnect(request, port)` → WebSocket proxy\n- `sandbox.destroy()` → Terminate container\n- `sandbox.mountBucket(bucket, path, options)` → Mount S3 storage\n\n## Critical Rules\n\n- ALWAYS call `proxyToSandbox()` first\n- Same ID = reuse sandbox\n- Use `/workspace` for persistent files\n- `normalizeId: true` for preview URLs\n- Retry on `CONTAINER_NOT_READY`\n\n## In This Reference\n- [configuration.md](./configuration.md) - Config, CLI, environment setup\n- [api.md](./api.md) - Programmatic API, testing patterns\n- [patterns.md](./patterns.md) - Common workflows, CI/CD integration\n- [gotchas.md](./gotchas.md) - Issues, limits, best practices\n\n## See Also\n- [durable-objects](../durable-objects/) - Sandbox runs on DO infrastructure\n- [containers](../containers/) - Container runtime fundamentals\n- [workers](../workers/) - Entry point for sandbox requests\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/sandbox/api.md",
    "content": "# API Reference\n\n## Command Execution\n\n```typescript\n// Basic\nconst result = await sandbox.exec('python3 script.py');\n// Returns: { stdout, stderr, exitCode, success, duration }\n\n// With options\nawait sandbox.exec('python3 test.py', {\n  cwd: '/workspace/project',\n  env: { API_KEY: 'secret' },\n  stream: true,\n  onOutput: (stream, data) => console.log(data)\n});\n```\n\n## File Operations\n\n```typescript\n// Read/Write\nconst { content } = await sandbox.readFile('/workspace/data.txt');\nawait sandbox.writeFile('/workspace/file.txt', 'content');  // Auto-creates dirs\n\n// List/Delete\nconst files = await sandbox.listFiles('/workspace');\nawait sandbox.deleteFile('/workspace/temp.txt');\nawait sandbox.deleteFile('/workspace/dir', { recursive: true });\n\n// Utils\nawait sandbox.mkdir('/workspace/dir', { recursive: true });\nawait sandbox.pathExists('/workspace/file.txt');\n```\n\n## Background Processes\n\n```typescript\n// Start\nconst process = await sandbox.startProcess('python3 -m http.server 8080', {\n  processId: 'web-server',\n  cwd: '/workspace/public',\n  env: { PORT: '8080' }\n});\n// Returns: { id, pid, command }\n\n// Wait for readiness\nawait process.waitForPort(8080);  // Wait for port to listen\nawait process.waitForLog(/Server running/);  // Wait for log pattern\nawait process.waitForExit();  // Wait for completion\n\n// Management\nconst processes = await sandbox.listProcesses();\nconst info = await sandbox.getProcess('web-server');\nawait sandbox.stopProcess('web-server');\nconst logs = await sandbox.getProcessLogs('web-server');\n```\n\n## Port Exposure\n\n```typescript\n// Expose port\nconst { url } = await sandbox.exposePort(8080, {\n  name: 'web-app',\n  hostname: request.hostname\n});\n\n// Management\nawait sandbox.isPortExposed(8080);\nawait sandbox.getExposedPorts(request.hostname);\nawait sandbox.unexposePort(8080);\n```\n\n## Sessions (Isolated Contexts)\n\nEach session maintains own shell state, env vars, cwd, process namespace.\n\n```typescript\n// Create with context\nconst session = await sandbox.createSession({\n  id: 'user-123',\n  cwd: '/workspace/user123',\n  env: { USER_ID: '123' }\n});\n\n// Use (full sandbox API)\nawait session.exec('echo $USER_ID');\nawait session.writeFile('config.txt', 'data');\n\n// Manage\nawait sandbox.getSession('user-123');\nawait sandbox.deleteSession('user-123');\n```\n\n## Code Interpreter\n\n```typescript\n// Create context with variables\nconst ctx = await sandbox.createCodeContext({\n  language: 'python',\n  variables: {\n    data: [1, 2, 3, 4, 5],\n    config: { verbose: true }\n  }\n});\n\n// Execute code with rich outputs\nconst result = await ctx.runCode(`\nimport matplotlib.pyplot as plt\nplt.plot(data, [x**2 for x in data])\nplt.savefig('plot.png')\nprint(f\"Processed {len(data)} points\")\n`);\n// Returns: { outputs: [{ type: 'text'|'image'|'html', content }], error }\n\n// Context persists variables across runs\nconst result2 = await ctx.runCode('print(data[0])');  // Still has 'data'\n```\n\n## WebSocket Connections\n\n```typescript\n// Proxy WebSocket to sandbox service\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const proxyResponse = await proxyToSandbox(request, env);\n    if (proxyResponse) return proxyResponse;\n\n    if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') {\n      const sandbox = getSandbox(env.Sandbox, 'realtime');\n      return await sandbox.wsConnect(request, 8080);\n    }\n    \n    return new Response('Not a WebSocket request', { status: 400 });\n  }\n};\n```\n\n## Bucket Mounting (S3 Storage)\n\n```typescript\n// Mount R2 bucket (production only, not wrangler dev)\nawait sandbox.mountBucket(env.DATA_BUCKET, '/data', {\n  readOnly: false\n});\n\n// Access files in mounted bucket\nawait sandbox.exec('ls /data');\nawait sandbox.writeFile('/data/output.txt', 'result');\n\n// Unmount\nawait sandbox.unmountBucket('/data');\n```\n\n**Note**: Bucket mounting only works in production. Mounted buckets are sandbox-scoped (visible to all sessions in that sandbox).\n\n## Lifecycle Management\n\n```typescript\n// Terminate container immediately\nawait sandbox.destroy();\n\n// REQUIRED when using keepAlive: true\nconst sandbox = getSandbox(env.Sandbox, 'temp', { keepAlive: true });\ntry {\n  await sandbox.writeFile('/tmp/code.py', code);\n  const result = await sandbox.exec('python /tmp/code.py');\n  return result.stdout;\n} finally {\n  await sandbox.destroy();  // Free resources\n}\n```\n\nDeletes: files, processes, sessions, network connections, exposed ports.\n\n## Error Handling\n\n```typescript\n// Command errors\nconst result = await sandbox.exec('python3 invalid.py');\nif (!result.success) {\n  console.error('Exit code:', result.exitCode);\n  console.error('Stderr:', result.stderr);\n}\n\n// SDK errors\ntry {\n  await sandbox.readFile('/nonexistent');\n} catch (error) {\n  if (error.code === 'FILE_NOT_FOUND') { /* ... */ }\n  else if (error.code === 'CONTAINER_NOT_READY') { /* retry */ }\n  else if (error.code === 'TIMEOUT') { /* ... */ }\n}\n\n// Retry pattern (see gotchas.md for full implementation)\n```\n\n\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/sandbox/configuration.md",
    "content": "# Configuration\n\n## getSandbox Options\n\n```typescript\nconst sandbox = getSandbox(env.Sandbox, 'sandbox-id', {\n  normalizeId: true,         // lowercase ID (required for preview URLs)\n  sleepAfter: '10m',         // sleep after inactivity: '5m', '1h', '2d' (default: '10m')\n  keepAlive: false,          // false = auto-timeout, true = never sleep\n  \n  containerTimeouts: {\n    instanceGetTimeoutMS: 30000,  // 30s for provisioning (default: 30000)\n    portReadyTimeoutMS: 90000     // 90s for container startup (default: 90000)\n  }\n});\n```\n\n**Sleep Config**:\n- `sleepAfter`: Duration string (e.g., '5m', '10m', '1h') - default: '10m'\n- `keepAlive: false`: Auto-sleep (default, cost-optimized)\n- `keepAlive: true`: Never sleep (higher cost, requires explicit `destroy()`)\n- Sleeping sandboxes wake automatically (cold start)\n\n## Instance Types\n\nwrangler.jsonc `instance_type`:\n- `lite`: 256MB RAM, 0.5 vCPU (default)\n- `standard`: 512MB RAM, 1 vCPU\n- `heavy`: 1GB RAM, 2 vCPU\n\n## Dockerfile Patterns\n\n**Basic**:\n```dockerfile\nFROM docker.io/cloudflare/sandbox:latest\nRUN pip3 install --no-cache-dir pandas numpy\nEXPOSE 8080  # Required for wrangler dev\n```\n\n**Scientific**:\n```dockerfile\nFROM docker.io/cloudflare/sandbox:latest\nRUN pip3 install --no-cache-dir \\\n    jupyter-server ipykernel matplotlib \\\n    pandas seaborn plotly scipy scikit-learn\n```\n\n**Node.js**:\n```dockerfile\nFROM docker.io/cloudflare/sandbox:latest\nRUN npm install -g typescript ts-node\n```\n\n**CRITICAL**: `EXPOSE` required for `wrangler dev` port access. Production auto-exposes all ports.\n\n## CLI Commands\n\n```bash\n# Dev\nwrangler dev                    # Start local dev server\nwrangler deploy                 # Deploy to production\nwrangler tail                   # Monitor logs\nwrangler containers list        # Check container status\nwrangler secret put KEY         # Set secret\n```\n\n## Environment & Secrets\n\n**wrangler.jsonc**:\n```jsonc\n{\n  \"vars\": {\n    \"ENVIRONMENT\": \"production\",\n    \"API_URL\": \"https://api.example.com\"\n  },\n  \"r2_buckets\": [{\n    \"binding\": \"DATA_BUCKET\",\n    \"bucket_name\": \"my-data-bucket\"\n  }]\n}\n```\n\n**Usage**:\n```typescript\nconst token = env.GITHUB_TOKEN;  // From wrangler secret\nawait sandbox.exec('git clone ...', {\n  env: { GIT_TOKEN: token }\n});\n```\n\n## Preview URL Setup\n\n**Prerequisites**:\n- Custom domain with wildcard DNS: `*.yourdomain.com → worker.yourdomain.com`\n- `.workers.dev` domains NOT supported\n- `normalizeId: true` in getSandbox\n- `proxyToSandbox()` called first in fetch handler\n\n## Cron Triggers (Pre-warming)\n\n```jsonc\n{\n  \"triggers\": {\n    \"crons\": [\"*/5 * * * *\"]  // Every 5 minutes\n  }\n}\n```\n\n```typescript\nexport default {\n  async scheduled(event: ScheduledEvent, env: Env) {\n    const sandbox = getSandbox(env.Sandbox, 'main');\n    await sandbox.exec('echo \"keepalive\"');  // Wake sandbox\n  }\n};\n```\n\n## Logging Configuration\n\n**wrangler.jsonc**:\n```jsonc\n{\n  \"vars\": {\n    \"SANDBOX_LOG_LEVEL\": \"debug\",  // debug | info | warn | error (default: info)\n    \"SANDBOX_LOG_FORMAT\": \"pretty\" // json | pretty (default: json)\n  }\n}\n```\n\n**Dev**: `debug` + `pretty`. **Production**: `info`/`warn` + `json`.\n\n## Timeout Environment Overrides\n\nOverride default timeouts via environment variables:\n\n```jsonc\n{\n  \"vars\": {\n    \"SANDBOX_INSTANCE_TIMEOUT_MS\": \"60000\",  // Override instanceGetTimeoutMS\n    \"SANDBOX_PORT_TIMEOUT_MS\": \"120000\"      // Override portReadyTimeoutMS\n  }\n}\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/sandbox/gotchas.md",
    "content": "# Gotchas & Best Practices\n\n## Common Errors\n\n### \"Container running indefinitely\"\n\n**Cause:** `keepAlive: true` without calling `destroy()`\n**Solution:** Always call `destroy()` when done with keepAlive containers\n\n```typescript\nconst sandbox = getSandbox(env.Sandbox, 'temp', { keepAlive: true });\ntry {\n  const result = await sandbox.exec('python script.py');\n  return result.stdout;\n} finally {\n  await sandbox.destroy();  // REQUIRED to free resources\n}\n```\n\n### \"CONTAINER_NOT_READY\"\n\n**Cause:** Container still provisioning (first request or after sleep)\n**Solution:** Retry after 2-3s\n\n```typescript\nasync function execWithRetry(sandbox, cmd) {\n  for (let i = 0; i < 3; i++) {\n    try {\n      return await sandbox.exec(cmd);\n    } catch (e) {\n      if (e.code === 'CONTAINER_NOT_READY') {\n        await new Promise(r => setTimeout(r, 2000));\n        continue;\n      }\n      throw e;\n    }\n  }\n}\n```\n\n### \"Connection refused: container port not found\"\n\n**Cause:** Missing `EXPOSE` directive in Dockerfile\n**Solution:** Add `EXPOSE <port>` to Dockerfile (only needed for `wrangler dev`, production auto-exposes)\n\n### \"Preview URLs not working\"\n\n**Cause:** Custom domain not configured, wildcard DNS missing, `normalizeId` not set, or `proxyToSandbox()` not called\n**Solution:** Check:\n1. Custom domain configured? (not `.workers.dev`)\n2. Wildcard DNS set up? (`*.domain.com → worker.domain.com`)\n3. `normalizeId: true` in getSandbox?\n4. `proxyToSandbox()` called first in fetch?\n\n### \"Slow first request\"\n\n**Cause:** Cold start (container provisioning)\n**Solution:**\n- Use `sleepAfter` instead of creating new sandboxes\n- Pre-warm with cron triggers\n- Set `keepAlive: true` for critical sandboxes\n\n### \"File not persisting\"\n\n**Cause:** Files in `/tmp` or other ephemeral paths\n**Solution:** Use `/workspace` for persistent files\n\n### \"Bucket mounting doesn't work locally\"\n\n**Cause:** Bucket mounting requires FUSE, not available in `wrangler dev`\n**Solution:** Test bucket mounting in production only. Use mock data locally.\n\n### \"Different normalizeId = different sandbox\"\n\n**Cause:** Changing `normalizeId` option changes Durable Object ID\n**Solution:** Set `normalizeId` consistently. `normalizeId: true` lowercases the ID.\n\n```typescript\n// These create DIFFERENT sandboxes:\ngetSandbox(env.Sandbox, 'MyApp');              // DO ID: hash('MyApp')\ngetSandbox(env.Sandbox, 'MyApp', { normalizeId: true });  // DO ID: hash('myapp')\n```\n\n### \"Code context variables disappeared\"\n\n**Cause:** Container restart clears code context state\n**Solution:** Code contexts are ephemeral. Recreate context after container sleep/wake.\n\n## Performance Optimization\n\n### Sandbox ID Strategy\n\n```typescript\n// ❌ BAD: New sandbox every time (slow)\nconst sandbox = getSandbox(env.Sandbox, `user-${Date.now()}`);\n\n// ✅ GOOD: Reuse per user\nconst sandbox = getSandbox(env.Sandbox, `user-${userId}`);\n```\n\n### Sleep & Traffic Config\n\n```typescript\n// Cost-optimized\ngetSandbox(env.Sandbox, 'id', { sleepAfter: '30m', keepAlive: false });\n\n// Always-on (requires destroy())\ngetSandbox(env.Sandbox, 'id', { keepAlive: true });\n```\n\n```jsonc\n// High traffic: increase max_instances\n{ \"containers\": [{ \"class_name\": \"Sandbox\", \"max_instances\": 50 }] }\n```\n\n## Security Best Practices\n\n### Sandbox Isolation\n- Each sandbox = isolated container (filesystem, network, processes)\n- Use unique sandbox IDs per tenant for multi-tenant apps\n- Sandboxes cannot communicate directly\n\n### Input Validation\n\n```typescript\n// ❌ DANGEROUS: Command injection\nconst result = await sandbox.exec(`python3 -c \"${userCode}\"`);\n\n// ✅ SAFE: Write to file, execute file\nawait sandbox.writeFile('/workspace/user_code.py', userCode);\nconst result = await sandbox.exec('python3 /workspace/user_code.py');\n```\n\n### Resource Limits\n\n```typescript\n// Timeout long-running commands\nconst result = await sandbox.exec('python3 script.py', {\n  timeout: 30000  // 30 seconds\n});\n```\n\n### Secrets Management\n\n```typescript\n// ❌ NEVER hardcode secrets\nconst token = 'ghp_abc123';\n\n// ✅ Use environment secrets\nconst token = env.GITHUB_TOKEN;\n\n// Pass to sandbox via exec env\nconst result = await sandbox.exec('git clone ...', {\n  env: { GIT_TOKEN: token }\n});\n```\n\n### Preview URL Security\nPreview URLs include auto-generated tokens:\n```\nhttps://8080-sandbox-abc123def456.yourdomain.com\n```\nToken changes on each expose operation, preventing unauthorized access.\n\n## Limits\n\n| Resource | Lite | Standard | Heavy |\n|----------|------|----------|-------|\n| RAM | 256MB | 512MB | 1GB |\n| vCPU | 0.5 | 1 | 2 |\n\n| Operation | Default Timeout | Override |\n|-----------|----------------|----------|\n| Container provisioning | 30s | `SANDBOX_INSTANCE_TIMEOUT_MS` |\n| Port readiness | 90s | `SANDBOX_PORT_TIMEOUT_MS` |\n| exec() | 120s | `timeout` option |\n| sleepAfter | 10m | `sleepAfter` option |\n\n**Performance**:\n- **First deploy**: 2-3 min for container build\n- **Cold start**: 2-3s when waking from sleep\n- **Bucket mounting**: Production only (FUSE not in dev)\n\n## Production Guide\n\nSee: https://developers.cloudflare.com/sandbox/guides/production-deployment/\n\n## Resources\n\n- [Official Docs](https://developers.cloudflare.com/sandbox/)\n- [API Reference](https://developers.cloudflare.com/sandbox/api/)\n- [Examples](https://github.com/cloudflare/sandbox-sdk/tree/main/examples)\n- [npm Package](https://www.npmjs.com/package/@cloudflare/sandbox)\n- [Discord Support](https://discord.cloudflare.com)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/sandbox/patterns.md",
    "content": "# Common Patterns\n\n## AI Code Execution with Code Context\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const { code, variables } = await request.json();\n    const sandbox = getSandbox(env.Sandbox, 'ai-agent');\n    \n    // Create context with persistent variables\n    const ctx = await sandbox.createCodeContext({\n      language: 'python',\n      variables: variables || {}\n    });\n    \n    // Execute with rich outputs (text, images, HTML)\n    const result = await ctx.runCode(code);\n    \n    return Response.json({\n      outputs: result.outputs,  // [{ type: 'text'|'image'|'html', content }]\n      error: result.error,\n      success: !result.error\n    });\n  }\n};\n```\n\n## Interactive Dev Environment\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const proxyResponse = await proxyToSandbox(request, env);\n    if (proxyResponse) return proxyResponse;\n    \n    const sandbox = getSandbox(env.Sandbox, 'ide', { normalizeId: true });\n    \n    if (request.url.endsWith('/start')) {\n      await sandbox.exec('curl -fsSL https://code-server.dev/install.sh | sh');\n      await sandbox.startProcess('code-server --bind-addr 0.0.0.0:8080', {\n        processId: 'vscode'\n      });\n      \n      const exposed = await sandbox.exposePort(8080);\n      return Response.json({ url: exposed.url });\n    }\n    \n    return new Response('Try /start');\n  }\n};\n```\n\n## WebSocket Real-Time Service\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const proxyResponse = await proxyToSandbox(request, env);\n    if (proxyResponse) return proxyResponse;\n\n    if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') {\n      const sandbox = getSandbox(env.Sandbox, 'realtime-service');\n      return await sandbox.wsConnect(request, 8080);\n    }\n\n    // Non-WebSocket: expose preview URL\n    const sandbox = getSandbox(env.Sandbox, 'realtime-service');\n    const { url } = await sandbox.exposePort(8080, {\n      hostname: new URL(request.url).hostname\n    });\n    return Response.json({ wsUrl: url.replace('https', 'wss') });\n  }\n};\n```\n\n**Dockerfile**:\n```dockerfile\nFROM docker.io/cloudflare/sandbox:latest\nRUN npm install -g ws\nEXPOSE 8080\n```\n\n## Process Readiness Pattern\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const sandbox = getSandbox(env.Sandbox, 'app-server');\n    \n    // Start server\n    const process = await sandbox.startProcess(\n      'node server.js',\n      { processId: 'server' }\n    );\n    \n    // Wait for server to be ready\n    await process.waitForPort(8080);  // Wait for port listening\n    \n    // Now safe to expose\n    const { url } = await sandbox.exposePort(8080);\n    return Response.json({ url });\n  }\n};\n```\n\n## Persistent Data with Bucket Mounting\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const sandbox = getSandbox(env.Sandbox, 'data-processor');\n    \n    // Mount R2 bucket (production only)\n    await sandbox.mountBucket(env.DATA_BUCKET, '/data', {\n      readOnly: false\n    });\n    \n    // Process files in bucket\n    const result = await sandbox.exec('python3 /workspace/process.py', {\n      env: { DATA_DIR: '/data/input' }\n    });\n    \n    // Results written to /data/output are persisted in R2\n    return Response.json({ success: result.success });\n  }\n};\n```\n\n## CI/CD Pipeline\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const { repo, branch } = await request.json();\n    const sandbox = getSandbox(env.Sandbox, `ci-${repo}-${Date.now()}`);\n    \n    await sandbox.exec(`git clone -b ${branch} ${repo} /workspace/repo`);\n    \n    const install = await sandbox.exec('npm install', {\n      cwd: '/workspace/repo',\n      stream: true,\n      onOutput: (stream, data) => console.log(data)\n    });\n    \n    if (!install.success) {\n      return Response.json({ success: false, error: 'Install failed' });\n    }\n    \n    const test = await sandbox.exec('npm test', { cwd: '/workspace/repo' });\n    \n    return Response.json({\n      success: test.success,\n      output: test.stdout,\n      exitCode: test.exitCode\n    });\n  }\n};\n```\n\n\n\n\n\n## Multi-Tenant Pattern\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const userId = request.headers.get('X-User-ID');\n    const sandbox = getSandbox(env.Sandbox, 'multi-tenant');\n    \n    // Each user gets isolated session\n    let session;\n    try {\n      session = await sandbox.getSession(userId);\n    } catch {\n      session = await sandbox.createSession({\n        id: userId,\n        cwd: `/workspace/users/${userId}`,\n        env: { USER_ID: userId }\n      });\n    }\n    \n    const code = await request.text();\n    const result = await session.exec(`python3 -c \"${code}\"`);\n    \n    return Response.json({ output: result.stdout });\n  }\n};\n```\n\n## Git Operations\n\n```typescript\n// Clone repo\nawait sandbox.exec('git clone https://github.com/user/repo.git /workspace/repo');\n\n// Authenticated (use env secrets)\nawait sandbox.exec(`git clone https://${env.GITHUB_TOKEN}@github.com/user/repo.git`);\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/secrets-store/README.md",
    "content": "# Cloudflare Secrets Store\n\nAccount-level encrypted secret management for Workers and AI Gateway.\n\n## Overview\n\n**Secrets Store**: Centralized, account-level secrets, reusable across Workers\n**Worker Secrets**: Per-Worker secrets (`wrangler secret put`)\n\n### Architecture\n\n- **Store**: Container (1/account in beta)\n- **Secret**: String ≤1024 bytes\n- **Scopes**: Permission boundaries controlling access\n  - `workers`: For Workers runtime access\n  - `ai-gateway`: For AI Gateway access\n  - Secrets must have correct scope for binding to work\n- **Bindings**: Connect secrets via `env` object\n\n**Regional Availability**: Global except China Network (unavailable)\n\n### Access Control\n\n- **Super Admin**: Full access\n- **Admin**: Create/edit/delete secrets, view metadata\n- **Deployer**: View metadata + bindings\n- **Reporter**: View metadata only\n\nAPI Token permissions: `Account Secrets Store Edit/Read`\n\n### Limits (Beta)\n\n- 100 secrets/account\n- 1 store/account\n- 1024 bytes max/secret\n- Production secrets count toward limit\n\n## When to Use\n\n**Use Secrets Store when:**\n- Multiple Workers share same credential\n- Centralized management needed\n- Compliance requires audit trail\n- Team collaboration on secrets\n\n**Use Worker Secrets when:**\n- Secret unique to one Worker\n- Simple single-Worker project\n- No cross-Worker sharing needed\n\n## In This Reference\n\n### Reading Order by Task\n\n| Task | Start Here | Then Read |\n|------|------------|-----------|\n| Quick overview | README.md | - |\n| First-time setup | README.md → configuration.md | api.md |\n| Add secret to Worker | configuration.md | api.md |\n| Implement access pattern | api.md | patterns.md |\n| Debug errors | gotchas.md | api.md |\n| Secret rotation | patterns.md | configuration.md |\n| Best practices | gotchas.md | patterns.md |\n\n### Files\n\n- [configuration.md](./configuration.md) - Wrangler commands, binding config\n- [api.md](./api.md) - Binding API, get/put/delete operations\n- [patterns.md](./patterns.md) - Rotation, encryption, access control\n- [gotchas.md](./gotchas.md) - Security issues, limits, best practices\n\n## See Also\n- [workers](../workers/) - Worker bindings integration\n- [wrangler](../wrangler/) - CLI secret management commands\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/secrets-store/api.md",
    "content": "# API Reference\n\n## Binding API\n\n### Basic Access\n\n**CRITICAL**: Async `.get()` required - secrets NOT directly available.\n\n**`.get()` throws on error** - does NOT return null. Always use try/catch.\n\n```typescript\ninterface Env {\n  API_KEY: { get(): Promise<string> };\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const apiKey = await env.API_KEY.get();\n    return fetch(\"https://api.example.com\", {\n      headers: { \"Authorization\": `Bearer ${apiKey}` }\n    });\n  }\n}\n```\n\n### Error Handling\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    try {\n      const apiKey = await env.API_KEY.get();\n      return fetch(\"https://api.example.com\", {\n        headers: { \"Authorization\": `Bearer ${apiKey}` }\n      });\n    } catch (error) {\n      console.error(\"Secret access failed:\", error);\n      return new Response(\"Configuration error\", { status: 500 });\n    }\n  }\n}\n```\n\n### Multiple Secrets & Patterns\n\n```typescript\n// Parallel fetch\nconst [stripeKey, sendgridKey] = await Promise.all([\n  env.STRIPE_KEY.get(),\n  env.SENDGRID_KEY.get()\n]);\n\n// ❌ Missing .get()\nconst key = env.API_KEY;\n\n// ❌ Module-level cache\nconst CACHED_KEY = await env.API_KEY.get(); // Fails\n\n// ✅ Request-scope cache\nconst key = await env.API_KEY.get(); // OK - reuse within request\n```\n\n## REST API\n\nBase: `https://api.cloudflare.com/client/v4`\n\n### Auth\n\n```bash\ncurl -H \"Authorization: Bearer $CF_TOKEN\" \\\n  https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/secrets_store/stores\n```\n\n### Store Operations\n\n```bash\n# List\nGET /accounts/{account_id}/secrets_store/stores\n\n# Create\nPOST /accounts/{account_id}/secrets_store/stores\n{\"name\": \"my-store\"}\n\n# Delete\nDELETE /accounts/{account_id}/secrets_store/stores/{store_id}\n```\n\n### Secret Operations\n\n```bash\n# List\nGET /accounts/{account_id}/secrets_store/stores/{store_id}/secrets\n\n# Create (single)\nPOST /accounts/{account_id}/secrets_store/stores/{store_id}/secrets\n{\n  \"name\": \"my_secret\",\n  \"value\": \"secret_value\",\n  \"scopes\": [\"workers\"],\n  \"comment\": \"Optional\"\n}\n\n# Create (batch)\nPOST /accounts/{account_id}/secrets_store/stores/{store_id}/secrets\n[\n  {\"name\": \"secret_one\", \"value\": \"val1\", \"scopes\": [\"workers\"]},\n  {\"name\": \"secret_two\", \"value\": \"val2\", \"scopes\": [\"workers\", \"ai-gateway\"]}\n]\n\n# Get metadata\nGET /accounts/{account_id}/secrets_store/stores/{store_id}/secrets/{secret_id}\n\n# Update\nPATCH /accounts/{account_id}/secrets_store/stores/{store_id}/secrets/{secret_id}\n{\"value\": \"new_value\", \"comment\": \"Updated\"}\n\n# Delete (single)\nDELETE /accounts/{account_id}/secrets_store/stores/{store_id}/secrets/{secret_id}\n\n# Delete (batch)\nDELETE /accounts/{account_id}/secrets_store/stores/{store_id}/secrets\n{\"secret_ids\": [\"id-1\", \"id-2\"]}\n\n# Duplicate\nPOST /accounts/{account_id}/secrets_store/stores/{store_id}/secrets/{secret_id}/duplicate\n{\"name\": \"new_name\"}\n\n# Quota\nGET /accounts/{account_id}/secrets_store/quota\n```\n\n### Responses\n\nSuccess:\n```json\n{\n  \"success\": true,\n  \"result\": {\n    \"id\": \"secret-id-123\",\n    \"name\": \"my_secret\",\n    \"created\": \"2025-01-11T12:00:00Z\",\n    \"scopes\": [\"workers\"]\n  }\n}\n```\n\nError:\n```json\n{\n  \"success\": false,\n  \"errors\": [{\"code\": 10000, \"message\": \"Name exists\"}]\n}\n```\n\n## TypeScript Helpers\n\nOfficial types available via `@cloudflare/workers-types`:\n\n```typescript\nimport type { SecretsStoreSecret } from \"@cloudflare/workers-types\";\n\ninterface Env {\n  STRIPE_API_KEY: SecretsStoreSecret;\n  DATABASE_URL: SecretsStoreSecret;\n  WORKER_SECRET: string; // Regular Worker secret (direct access)\n}\n```\n\nCustom helper type:\n\n```typescript\ninterface SecretsStoreBinding {\n  get(): Promise<string>;\n}\n\n// Fallback helper\nasync function getSecretWithFallback(\n  primary: SecretsStoreBinding,\n  fallback?: SecretsStoreBinding\n): Promise<string> {\n  try {\n    return await primary.get();\n  } catch (error) {\n    if (fallback) return await fallback.get();\n    throw error;\n  }\n}\n\n// Batch helper\nasync function getAllSecrets(\n  secrets: Record<string, SecretsStoreBinding>\n): Promise<Record<string, string>> {\n  const entries = await Promise.all(\n    Object.entries(secrets).map(async ([k, v]) => [k, await v.get()])\n  );\n  return Object.fromEntries(entries);\n}\n```\n\nSee: [configuration.md](./configuration.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/secrets-store/configuration.md",
    "content": "# Configuration\n\n## Wrangler Config\n\n### Basic Binding\n\n**wrangler.jsonc**:\n\n```jsonc\n{\n  \"secrets_store_secrets\": [\n    {\n      \"binding\": \"API_KEY\",\n      \"store_id\": \"abc123\",\n      \"secret_name\": \"stripe_api_key\"\n    }\n  ]\n}\n```\n\n**wrangler.toml** (alternative):\n\n```toml\n[[secrets_store_secrets]]\nbinding = \"API_KEY\"\nstore_id = \"abc123\"\nsecret_name = \"stripe_api_key\"\n```\n\nFields:\n- `binding`: Variable name for `env` access\n- `store_id`: From `wrangler secrets-store store list`\n- `secret_name`: Identifier (no spaces)\n\n### Environment-Specific\n\n**wrangler.jsonc**:\n\n```jsonc\n{\n  \"env\": {\n    \"production\": {\n      \"secrets_store_secrets\": [\n        {\n          \"binding\": \"API_KEY\",\n          \"store_id\": \"prod-store\",\n          \"secret_name\": \"prod_api_key\"\n        }\n      ]\n    },\n    \"staging\": {\n      \"secrets_store_secrets\": [\n        {\n          \"binding\": \"API_KEY\",\n          \"store_id\": \"staging-store\",\n          \"secret_name\": \"staging_api_key\"\n        }\n      ]\n    }\n  }\n}\n```\n\n**wrangler.toml** (alternative):\n\n```toml\n[env.production]\n[[env.production.secrets_store_secrets]]\nbinding = \"API_KEY\"\nstore_id = \"prod-store\"\nsecret_name = \"prod_api_key\"\n\n[env.staging]\n[[env.staging.secrets_store_secrets]]\nbinding = \"API_KEY\"\nstore_id = \"staging-store\"\nsecret_name = \"staging_api_key\"\n```\n\n## Wrangler Commands\n\n### Store Management\n\n```bash\nwrangler secrets-store store list\nwrangler secrets-store store create my-store --remote\nwrangler secrets-store store delete <store-id> --remote\n```\n\n### Secret Management (Production)\n\n```bash\n# Create (interactive)\nwrangler secrets-store secret create <store-id> \\\n  --name MY_SECRET --scopes workers --remote\n\n# Create (piped)\ncat secret.txt | wrangler secrets-store secret create <store-id> \\\n  --name MY_SECRET --scopes workers --remote\n\n# List/get/update/delete\nwrangler secrets-store secret list <store-id> --remote\nwrangler secrets-store secret get <store-id> --name MY_SECRET --remote\nwrangler secrets-store secret update <store-id> --name MY_SECRET --new-value \"val\" --remote\nwrangler secrets-store secret delete <store-id> --name MY_SECRET --remote\n\n# Duplicate\nwrangler secrets-store secret duplicate <store-id> \\\n  --name ORIG --new-name COPY --remote\n```\n\n### Local Development\n\n**CRITICAL**: Production secrets (`--remote`) NOT accessible in local dev.\n\n```bash\n# Create local-only (no --remote)\nwrangler secrets-store secret create <store-id> --name DEV_KEY --scopes workers\n\nwrangler dev    # Uses local secrets\nwrangler deploy # Uses production secrets\n```\n\nBest practice: Separate names for local/prod:\n\n```jsonc\n{\n  \"env\": {\n    \"development\": {\n      \"secrets_store_secrets\": [\n        { \"binding\": \"API_KEY\", \"store_id\": \"store\", \"secret_name\": \"dev_api_key\" }\n      ]\n    },\n    \"production\": {\n      \"secrets_store_secrets\": [\n        { \"binding\": \"API_KEY\", \"store_id\": \"store\", \"secret_name\": \"prod_api_key\" }\n      ]\n    }\n  }\n}\n```\n\n## Dashboard\n\n### Creating Secrets\n\n1. **Secrets Store** → **Create secret**\n2. Fill: Name (no spaces), Value, Scope (`Workers`), Comment\n3. **Save** (value hidden after)\n\n### Adding Bindings\n\n**Method 1**: Worker → Settings → Bindings → Add → Secrets Store\n**Method 2**: Create secret directly from Worker settings dropdown\n\nDeploy options:\n- **Deploy**: Immediate 100%\n- **Save version**: Gradual rollout\n\n## CI/CD\n\n### GitHub Actions\n\n```yaml\n- name: Create secret\n  env:\n    CLOUDFLARE_API_TOKEN: ${{ secrets.CF_TOKEN }}\n  run: |\n    echo \"${{ secrets.API_KEY }}\" | \\\n    npx wrangler secrets-store secret create $STORE_ID \\\n      --name API_KEY --scopes workers --remote\n\n- name: Deploy\n  run: npx wrangler deploy\n```\n\n### GitLab CI\n\n```yaml\nscript:\n  - echo \"$API_KEY_VALUE\" | npx wrangler secrets-store secret create $STORE_ID --name API_KEY --scopes workers --remote\n  - npx wrangler deploy\n```\n\nSee: [api.md](./api.md), [patterns.md](./patterns.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/secrets-store/gotchas.md",
    "content": "# Gotchas\n\n## Common Errors\n\n### \".get() Throws on Error\"\n\n**Cause:** Assuming `.get()` returns null on failure instead of throwing  \n**Solution:** Always wrap `.get()` calls in try/catch blocks to handle errors gracefully\n\n```typescript\ntry {\n  const key = await env.API_KEY.get();\n} catch (error) {\n  return new Response(\"Configuration error\", { status: 500 });\n}\n```\n\n### \"Logging Secret Values\"\n\n**Cause:** Accidentally logging secret values in console or error messages  \n**Solution:** Only log metadata (e.g., \"Retrieved API_KEY\") never the actual secret value\n\n### \"Module-Level Secret Access\"\n\n**Cause:** Attempting to access secrets during module initialization before env is available  \n**Solution:** Cache secrets in request scope only, not at module level\n\n### \"Secret not found in store\"\n\n**Cause:** Secret name doesn't exist, case mismatch, missing workers scope, or incorrect store_id  \n**Solution:** Verify secret exists with `wrangler secrets-store secret list <store-id> --remote`, check name matches exactly (case-sensitive), ensure secret has `workers` scope, and verify correct store_id\n\n### \"Scope Mismatch\"\n\n**Cause:** Secret exists but missing `workers` scope (only has `ai-gateway` scope)  \n**Solution:** Update secret scopes: `wrangler secrets-store secret update <store-id> --name SECRET --scopes workers --remote` or add via Dashboard\n\n### \"JSON Parsing Failure\"\n\n**Cause:** Storing invalid JSON in secret, then failing to parse during runtime  \n**Solution:** Validate JSON before storing:\n\n```bash\n# Validate before storing\necho '{\"key\":\"value\"}' | jq . && \\\n  echo '{\"key\":\"value\"}' | wrangler secrets-store secret create <store-id> \\\n    --name CONFIG --scopes workers --remote\n```\n\nRuntime parsing with error handling:\n\n```typescript\ntry {\n  const configStr = await env.CONFIG.get();\n  const config = JSON.parse(configStr);\n} catch (error) {\n  console.error(\"Invalid config JSON:\", error);\n  return new Response(\"Invalid configuration\", { status: 500 });\n}\n```\n\n### \"Cannot access secret in local dev\"\n\n**Cause:** Attempting to access production secrets in local development environment  \n**Solution:** Create local-only secrets (without `--remote` flag) for development: `wrangler secrets-store secret create <store-id> --name API_KEY --scopes workers`\n\n### \"Property 'get' does not exist\"\n\n**Cause:** Missing TypeScript type definition for secret binding  \n**Solution:** Define interface with get method: `interface Env { API_KEY: { get(): Promise<string> }; }`\n\n### \"Binding already exists\"\n\n**Cause:** Duplicate binding in dashboard or conflict between wrangler.jsonc and dashboard  \n**Solution:** Remove duplicate from dashboard Settings → Bindings, check for conflicts, or delete old Worker secret with `wrangler secret delete API_KEY`\n\n### \"Account secret quota exceeded\"\n\n**Cause:** Account has reached 100 secret limit (beta)  \n**Solution:** Check quota with `wrangler secrets-store quota --remote`, delete unused secrets, consolidate duplicates, or contact Cloudflare for increase\n\n## Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Max secrets per account | 100 | Beta limit |\n| Max stores per account | 1 | Beta limit |\n| Max secret size | 1024 bytes | Per secret |\n| Local secrets | Don't count toward limit | Only production secrets count |\n| Scopes available | `workers`, `ai-gateway` | Must have correct scope for access |\n| Scope | Account-level | Can be reused across multiple Workers |\n| Access method | `await env.BINDING.get()` | Async only, throws on error |\n| Management | Centralized | Via secrets-store commands |\n| Local dev | Separate local secrets | Use without `--remote` flag |\n| Regional availability | Global except China Network | Unavailable in China Network |\n\nSee: [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/secrets-store/patterns.md",
    "content": "# Patterns\n\n## Secret Rotation\n\nZero-downtime rotation with versioned naming (`api_key_v1`, `api_key_v2`):\n\n```typescript\ninterface Env {\n  PRIMARY_KEY: { get(): Promise<string> };\n  FALLBACK_KEY?: { get(): Promise<string> };\n}\n\nasync function fetchWithAuth(url: string, key: string) {\n  return fetch(url, { headers: { \"Authorization\": `Bearer ${key}` } });\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    let resp = await fetchWithAuth(\"https://api.example.com\", await env.PRIMARY_KEY.get());\n    \n    // Fallback during rotation\n    if (!resp.ok && env.FALLBACK_KEY) {\n      resp = await fetchWithAuth(\"https://api.example.com\", await env.FALLBACK_KEY.get());\n    }\n    \n    return resp;\n  }\n}\n```\n\nWorkflow: Create `api_key_v2` → add fallback binding → deploy → swap primary → deploy → remove `v1`\n\n## Encryption with KV\n\n```typescript\ninterface Env {\n  CACHE: KVNamespace;\n  ENCRYPTION_KEY: { get(): Promise<string> };\n}\n\nasync function encryptValue(value: string, key: string): Promise<string> {\n  const enc = new TextEncoder();\n  const keyMaterial = await crypto.subtle.importKey(\n    \"raw\", enc.encode(key), { name: \"AES-GCM\" }, false, [\"encrypt\"]\n  );\n  const iv = crypto.getRandomValues(new Uint8Array(12));\n  const encrypted = await crypto.subtle.encrypt(\n    { name: \"AES-GCM\", iv }, keyMaterial, enc.encode(value)\n  );\n  \n  const combined = new Uint8Array(iv.length + encrypted.byteLength);\n  combined.set(iv);\n  combined.set(new Uint8Array(encrypted), iv.length);\n  return btoa(String.fromCharCode(...combined));\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const key = await env.ENCRYPTION_KEY.get();\n    const encrypted = await encryptValue(\"sensitive-data\", key);\n    await env.CACHE.put(\"user:123:data\", encrypted);\n    return Response.json({ ok: true });\n  }\n}\n```\n\n## HMAC Signing\n\n```typescript\ninterface Env {\n  HMAC_SECRET: { get(): Promise<string> };\n}\n\nasync function signRequest(data: string, secret: string): Promise<string> {\n  const enc = new TextEncoder();\n  const key = await crypto.subtle.importKey(\n    \"raw\", enc.encode(secret), { name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"]\n  );\n  const sig = await crypto.subtle.sign(\"HMAC\", key, enc.encode(data));\n  return btoa(String.fromCharCode(...new Uint8Array(sig)));\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const secret = await env.HMAC_SECRET.get();\n    const payload = await request.text();\n    const signature = await signRequest(payload, secret);\n    return Response.json({ signature });\n  }\n}\n```\n\n## Audit & Monitoring\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext) {\n    const startTime = Date.now();\n    try {\n      const apiKey = await env.API_KEY.get();\n      const resp = await fetch(\"https://api.example.com\", {\n        headers: { \"Authorization\": `Bearer ${apiKey}` }\n      });\n      \n      ctx.waitUntil(\n        fetch(\"https://log.example.com/log\", {\n          method: \"POST\",\n          body: JSON.stringify({\n            event: \"secret_used\",\n            secret_name: \"API_KEY\",\n            timestamp: new Date().toISOString(),\n            duration_ms: Date.now() - startTime,\n            success: resp.ok\n          })\n        })\n      );\n      return resp;\n    } catch (error) {\n      ctx.waitUntil(\n        fetch(\"https://log.example.com/log\", {\n          method: \"POST\",\n          body: JSON.stringify({\n            event: \"secret_access_failed\",\n            secret_name: \"API_KEY\",\n            error: error instanceof Error ? error.message : \"Unknown\"\n          })\n        })\n      );\n      return new Response(\"Error\", { status: 500 });\n    }\n  }\n}\n```\n\n## Migration from Worker Secrets\n\nChange `env.SECRET` (direct) to `await env.SECRET.get()` (async).\n\nSteps:\n1. Create in Secrets Store: `wrangler secrets-store secret create <store-id> --name API_KEY --scopes workers --remote`\n2. Add binding to `wrangler.jsonc`: `{\"binding\": \"API_KEY\", \"store_id\": \"abc123\", \"secret_name\": \"api_key\"}`\n3. Update code: `const key = await env.API_KEY.get();`\n4. Test staging, deploy\n5. Remove old: `wrangler secret delete API_KEY`\n\n## Sharing Across Workers\n\nSame secret, different binding names:\n\n```jsonc\n// worker-1: binding=\"SHARED_DB\", secret_name=\"postgres_url\"\n// worker-2: binding=\"DB_CONN\", secret_name=\"postgres_url\"\n```\n\n## JSON Secret Parsing\n\nStore structured config as JSON secrets:\n\n```typescript\ninterface Env {\n  DB_CONFIG: { get(): Promise<string> };\n}\n\ninterface DbConfig {\n  host: string;\n  port: number;\n  username: string;\n  password: string;\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    try {\n      const configStr = await env.DB_CONFIG.get();\n      const config: DbConfig = JSON.parse(configStr);\n      \n      // Use parsed config\n      const dbUrl = `postgres://${config.username}:${config.password}@${config.host}:${config.port}`;\n      \n      return Response.json({ connected: true });\n    } catch (error) {\n      if (error instanceof SyntaxError) {\n        return new Response(\"Invalid config JSON\", { status: 500 });\n      }\n      throw error;\n    }\n  }\n}\n```\n\nStore JSON secret:\n\n```bash\necho '{\"host\":\"db.example.com\",\"port\":5432,\"username\":\"app\",\"password\":\"secret\"}' | \\\n  wrangler secrets-store secret create <store-id> \\\n    --name DB_CONFIG --scopes workers --remote\n```\n\n## Integration\n\n### Service Bindings\n\nAuth Worker signs JWT with Secrets Store; API Worker verifies via service binding.\n\nSee: [workers](../workers/) for service binding patterns.\n\nSee: [api.md](./api.md), [gotchas.md](./gotchas.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/smart-placement/README.md",
    "content": "# Cloudflare Workers Smart Placement\n\nAutomatic workload placement optimization to minimize latency by running Workers closer to backend infrastructure rather than end users.\n\n## Core Concept\n\nSmart Placement automatically analyzes Worker request duration across Cloudflare's global network and intelligently routes requests to optimal data center locations. Instead of defaulting to the location closest to the end user, Smart Placement can forward requests to locations closer to backend infrastructure when this reduces overall request duration.\n\n### When to Use\n\n**Enable Smart Placement when:**\n- Worker makes multiple round trips to backend services/databases\n- Backend infrastructure is geographically concentrated\n- Request duration dominated by backend latency rather than network latency from user\n- Running backend logic in Workers (APIs, data aggregation, SSR with DB calls)\n- Worker uses `fetch` handler (not RPC methods)\n\n**Do NOT enable for:**\n- Workers serving only static content or cached responses\n- Workers without significant backend communication\n- Pure edge logic (auth checks, redirects, simple transformations)\n- Workers without fetch event handlers\n- Workers with RPC methods or named entrypoints (only `fetch` handlers are affected)\n- Pages/Assets Workers with `run_worker_first = true` (degrades asset serving)\n\n### Decision Tree\n\n```\nDoes your Worker have a fetch handler?\n├─ No → Smart Placement won't work (skip)\n└─ Yes\n   │\n   Does it make multiple backend calls (DB/API)?\n   ├─ No → Don't enable (won't help)\n   └─ Yes\n      │\n      Is backend geographically concentrated?\n      ├─ No (globally distributed) → Probably won't help\n      └─ Yes or uncertain\n         │\n         Does it serve static assets with run_worker_first=true?\n         ├─ Yes → Don't enable (will hurt performance)\n         └─ No → Enable Smart Placement\n            │\n            After 15min, check placement_status\n            ├─ SUCCESS → Monitor metrics\n            ├─ INSUFFICIENT_INVOCATIONS → Need more traffic\n            └─ UNSUPPORTED_APPLICATION → Disable (hurting performance)\n```\n\n### Key Architecture Pattern\n\n**Recommended:** Split full-stack applications into separate Workers:\n```\nUser → Frontend Worker (at edge, close to user)\n         ↓ Service Binding\n       Backend Worker (Smart Placement enabled, close to DB/API)\n         ↓\n       Database/Backend Service\n```\n\nThis maintains fast, reactive frontends while optimizing backend latency.\n\n## Quick Start\n\n```jsonc\n// wrangler.jsonc\n{\n  \"placement\": {\n    \"mode\": \"smart\"  // or \"off\" to explicitly disable\n  }\n}\n```\n\nDeploy and wait 15 minutes for analysis. Check status via API or dashboard metrics.\n\n**To disable:** Set `\"mode\": \"off\"` or remove `placement` field entirely (both equivalent).\n\n## Requirements\n\n- Wrangler 2.20.0+\n- Analysis time: Up to 15 minutes after enabling\n- Traffic requirements: Consistent traffic from multiple global locations\n- Available on all Workers plans (Free, Paid, Enterprise)\n\n## Placement Status Values\n\n```typescript\ntype PlacementStatus = \n  | undefined  // Not yet analyzed\n  | 'SUCCESS'  // Successfully optimized\n  | 'INSUFFICIENT_INVOCATIONS'  // Not enough traffic\n  | 'UNSUPPORTED_APPLICATION';  // Made Worker slower (reverted)\n```\n\n## CLI Commands\n\n```bash\n# Deploy with Smart Placement\nwrangler deploy\n\n# Check placement status\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/services/$WORKER_NAME \\\n  | jq .result.placement_status\n\n# Monitor\nwrangler tail your-worker-name --header cf-placement\n```\n\n## Reading Order\n\n**First time?** Start here:\n1. This README - understand core concepts and when to use Smart Placement\n2. [configuration.md](./configuration.md) - set up wrangler.jsonc and understand limitations\n3. [patterns.md](./patterns.md) - see practical examples for your use case\n4. [api.md](./api.md) - monitor and verify Smart Placement is working\n5. [gotchas.md](./gotchas.md) - troubleshoot common issues\n\n**Quick lookup:**\n- \"Should I enable Smart Placement?\" → See \"When to Use\" above\n- \"How do I configure it?\" → [configuration.md](./configuration.md)\n- \"How do I split frontend/backend?\" → [patterns.md](./patterns.md)\n- \"Why isn't it working?\" → [gotchas.md](./gotchas.md)\n\n## In This Reference\n\n- [configuration.md](./configuration.md) - wrangler.jsonc setup, mode values, validation rules\n- [api.md](./api.md) - Placement Status API, cf-placement header, monitoring\n- [patterns.md](./patterns.md) - Frontend/backend split, database workers, SSR patterns\n- [gotchas.md](./gotchas.md) - Troubleshooting INSUFFICIENT_INVOCATIONS, performance issues\n\n## See Also\n\n- [workers](../workers/) - Worker runtime and fetch handlers\n- [d1](../d1/) - D1 database that benefits from Smart Placement\n- [durable-objects](../durable-objects/) - Durable Objects with backend logic\n- [bindings](../bindings/) - Service bindings for frontend/backend split\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/smart-placement/api.md",
    "content": "# Smart Placement API\n\n## Placement Status API\n\nQuery Worker placement status via Cloudflare API:\n\n```bash\ncurl -X GET \"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/workers/services/{WORKER_NAME}\" \\\n  -H \"Authorization: Bearer <TOKEN>\" \\\n  -H \"Content-Type: application/json\"\n```\n\nResponse includes `placement_status` field:\n\n```typescript\ntype PlacementStatus = \n  | undefined  // Not yet analyzed\n  | 'SUCCESS'  // Successfully optimized\n  | 'INSUFFICIENT_INVOCATIONS'  // Not enough traffic\n  | 'UNSUPPORTED_APPLICATION';  // Made Worker slower (reverted)\n```\n\n## Status Meanings\n\n**`undefined` (not present)**\n- Worker not yet analyzed\n- Always runs at default edge location closest to user\n\n**`SUCCESS`**\n- Analysis complete, Smart Placement active\n- Worker runs in optimal location (may be edge or remote)\n\n**`INSUFFICIENT_INVOCATIONS`**\n- Not enough requests to make placement decision\n- Requires consistent multi-region traffic\n- Always runs at default edge location\n\n**`UNSUPPORTED_APPLICATION`** (rare, <1% of Workers)\n- Smart Placement made Worker slower\n- Placement decision reverted\n- Always runs at edge location\n- Won't be re-analyzed until redeployed\n\n## cf-placement Header (Beta)\n\nSmart Placement adds response header indicating routing decision:\n\n```typescript\n// Remote placement (Smart Placement routed request)\n\"cf-placement: remote-LHR\"  // Routed to London\n\n// Local placement (default edge routing)  \n\"cf-placement: local-EWR\"   // Stayed at Newark edge\n```\n\nFormat: `{placement-type}-{IATA-code}`\n- `remote-*` = Smart Placement routed to remote location\n- `local-*` = Stayed at default edge location\n- IATA code = nearest airport to data center\n\n**Warning:** Beta feature, may be removed before GA.\n\n## Detecting Smart Placement in Code\n\n**Note:** `cf-placement` header is a beta feature and may change or be removed.\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const placementHeader = request.headers.get('cf-placement');\n    \n    if (placementHeader?.startsWith('remote-')) {\n      const location = placementHeader.split('-')[1];\n      console.log(`Smart Placement routed to ${location}`);\n    } else if (placementHeader?.startsWith('local-')) {\n      const location = placementHeader.split('-')[1];\n      console.log(`Running at edge location ${location}`);\n    }\n    \n    return new Response('OK');\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## Request Duration Metrics\n\nAvailable in Cloudflare dashboard when Smart Placement enabled:\n\n**Workers & Pages → [Your Worker] → Metrics → Request Duration**\n\nShows histogram comparing:\n- Request duration WITH Smart Placement (99% of traffic)\n- Request duration WITHOUT Smart Placement (1% baseline)\n\n**Request Duration vs Execution Duration:**\n- **Request duration:** Total time from request arrival to response delivery (includes network latency)\n- **Execution duration:** Time Worker code actively executing (excludes network waits)\n\nUse request duration to measure Smart Placement impact.\n\n### Interpreting Metrics\n\n| Metric Comparison | Interpretation | Action |\n|-------------------|----------------|--------|\n| WITH < WITHOUT | Smart Placement helping | Keep enabled |\n| WITH ≈ WITHOUT | Neutral impact | Consider disabling to free resources |\n| WITH > WITHOUT | Smart Placement hurting | Disable with `mode: \"off\"` |\n\n**Why Smart Placement might hurt performance:**\n- Worker primarily serves static assets or cached content\n- Backend services are globally distributed (no single optimal location)\n- Worker has minimal backend communication\n- Using Pages with `assets.run_worker_first = true`\n\n**Typical improvements when Smart Placement helps:**\n- 20-50% reduction in request duration for database-heavy Workers\n- 30-60% reduction for Workers making multiple backend API calls\n- Larger improvements when backend is geographically concentrated\n\n## Monitoring Commands\n\n```bash\n# Tail Worker logs\nwrangler tail your-worker-name\n\n# Tail with filters\nwrangler tail your-worker-name --status error\nwrangler tail your-worker-name --header cf-placement\n\n# Check placement status via API\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/services/$WORKER_NAME \\\n  | jq .result.placement_status\n```\n\n## TypeScript Types\n\n```typescript\n// Placement status returned by API (field may be absent)\ntype PlacementStatus = \n  | 'SUCCESS'\n  | 'INSUFFICIENT_INVOCATIONS'\n  | 'UNSUPPORTED_APPLICATION'\n  | undefined;\n\n// Placement configuration in wrangler.jsonc\ntype PlacementMode = 'smart' | 'off';\n\ninterface PlacementConfig {\n  mode: PlacementMode;\n  // Legacy fields (deprecated/removed):\n  // hint?: string;  // REMOVED - no longer supported\n}\n\n// Explicit placement (separate feature from Smart Placement)\ninterface ExplicitPlacementConfig {\n  region?: string;\n  host?: string;\n  hostname?: string;\n  // Cannot combine with mode field\n}\n\n// Worker metadata from API response\ninterface WorkerMetadata {\n  placement?: PlacementConfig | ExplicitPlacementConfig;\n  placement_status?: PlacementStatus;\n}\n\n// Service Binding for backend Worker\ninterface Env {\n  BACKEND_SERVICE: Fetcher;  // Service Binding to backend Worker\n  DATABASE: D1Database;\n}\n\n// Example Worker with Service Binding\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // Forward to backend Worker with Smart Placement enabled\n    const response = await env.BACKEND_SERVICE.fetch(request);\n    return response;\n  }\n} satisfies ExportedHandler<Env>;\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/smart-placement/configuration.md",
    "content": "# Smart Placement Configuration\n\n## wrangler.jsonc Setup\n\n```jsonc\n{\n  \"$schema\": \"./node_modules/wrangler/config-schema.json\",\n  \"placement\": {\n    \"mode\": \"smart\"\n  }\n}\n```\n\n## Placement Mode Values\n\n| Mode | Behavior |\n|------|----------|\n| `\"smart\"` | Enable Smart Placement - automatic optimization based on traffic analysis |\n| `\"off\"` | Explicitly disable Smart Placement - always run at edge closest to user |\n| Not specified | Default behavior - run at edge closest to user (same as `\"off\"`) |\n\n**Note:** Smart Placement vs Explicit Placement are separate features. Smart Placement (`mode: \"smart\"`) uses automatic analysis. For manual placement control, see explicit placement options (`region`, `host`, `hostname` fields - not covered in this reference).\n\n## Frontend + Backend Split Configuration\n\n### Frontend Worker (No Smart Placement)\n\n```jsonc\n// frontend-worker/wrangler.jsonc\n{\n  \"name\": \"frontend\",\n  \"main\": \"frontend-worker.ts\",\n  // No \"placement\" - runs at edge\n  \"services\": [\n    {\n      \"binding\": \"BACKEND\",\n      \"service\": \"backend-api\"\n    }\n  ]\n}\n```\n\n### Backend Worker (Smart Placement Enabled)\n\n```jsonc\n// backend-api/wrangler.jsonc\n{\n  \"name\": \"backend-api\",\n  \"main\": \"backend-worker.ts\",\n  \"placement\": {\n    \"mode\": \"smart\"\n  },\n  \"d1_databases\": [\n    {\n      \"binding\": \"DATABASE\",\n      \"database_id\": \"xxx\"\n    }\n  ]\n}\n```\n\n## Requirements & Limitations\n\n### Requirements\n- **Wrangler version:** 2.20.0+\n- **Analysis time:** Up to 15 minutes\n- **Traffic requirements:** Consistent multi-location traffic\n- **Workers plan:** All plans (Free, Paid, Enterprise)\n\n### What Smart Placement Affects\n\n**CRITICAL LIMITATION - Smart Placement ONLY Affects `fetch` Handlers:**\n\nSmart Placement is fundamentally limited to Workers with default `fetch` handlers. This is a key architectural constraint.\n\n- ✅ **Affects:** `fetch` event handlers ONLY (the default export's fetch method)\n- ❌ **Does NOT affect:** \n  - RPC methods (Service Bindings with `WorkerEntrypoint` - see example below)\n  - Named entrypoints (exports other than `default`)\n  - Workers without `fetch` handlers\n  - Queue consumers, scheduled handlers, or other event types\n\n**Example - Smart Placement ONLY affects `fetch`:**\n```typescript\n// ✅ Smart Placement affects this:\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // This runs close to backend when Smart Placement enabled\n    const data = await env.DATABASE.prepare('SELECT * FROM users').all();\n    return Response.json(data);\n  }\n}\n\n// ❌ Smart Placement DOES NOT affect these:\nexport class MyRPC extends WorkerEntrypoint {\n  async myMethod() { \n    // This ALWAYS runs at edge, Smart Placement has NO EFFECT\n    const data = await this.env.DATABASE.prepare('SELECT * FROM users').all();\n    return data;\n  }\n}\n\nexport async function scheduled(event: ScheduledEvent, env: Env) {\n  // NOT affected by Smart Placement\n}\n```\n\n**Consequence:** If your backend logic uses RPC methods (`WorkerEntrypoint`), Smart Placement cannot optimize those calls. You must use fetch-based patterns for Smart Placement to work.\n\n**Solution:** Convert RPC methods to fetch endpoints, or use a wrapper Worker with `fetch` handler that calls your backend RPC (though this adds latency).\n\n### Baseline Traffic\nSmart Placement automatically routes 1% of requests WITHOUT optimization as baseline for performance comparison.\n\n### Validation Rules\n\n**Mutually exclusive fields:**\n- `mode` cannot be used with explicit placement fields (`region`, `host`, `hostname`)\n- Choose either Smart Placement OR explicit placement, not both\n\n```jsonc\n// ✅ Valid - Smart Placement\n{ \"placement\": { \"mode\": \"smart\" } }\n\n// ✅ Valid - Explicit Placement (different feature)\n{ \"placement\": { \"region\": \"us-east1\" } }\n\n// ❌ Invalid - Cannot combine\n{ \"placement\": { \"mode\": \"smart\", \"region\": \"us-east1\" } }\n```\n\n## Dashboard Configuration\n\n**Workers & Pages** → Select Worker → **Settings** → **General** → **Placement: Smart** → Wait 15min → Check **Metrics**\n\n## TypeScript Types\n\n```typescript\ninterface Env {\n  BACKEND: Fetcher;\n  DATABASE: D1Database;\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const data = await env.DATABASE.prepare('SELECT * FROM table').all();\n    return Response.json(data);\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## Cloudflare Pages/Assets Warning\n\n**CRITICAL PERFORMANCE ISSUE:** Enabling Smart Placement with `assets.run_worker_first = true` in Pages projects **severely degrades asset serving performance**. This is one of the most common misconfigurations.\n\n**Why this is bad:**\n- Smart Placement routes ALL requests (including static assets) away from edge to remote locations\n- Static assets (HTML, CSS, JS, images) should ALWAYS be served from edge closest to user\n- Result: 2-5x slower asset loading times, poor user experience\n\n**Problem:** Smart Placement routes asset requests away from edge, but static assets should always be served from edge closest to user.\n\n**Solutions (in order of preference):**\n1. **Recommended:** Split into separate Workers (frontend at edge + backend with Smart Placement)\n2. Set `\"mode\": \"off\"` to explicitly disable Smart Placement for Pages/Assets Workers\n3. Use `assets.run_worker_first = false` (serves assets first, bypasses Worker for static content)\n\n```jsonc\n// ❌ BAD - Degrades asset performance by 2-5x\n{\n  \"name\": \"pages-app\",\n  \"placement\": { \"mode\": \"smart\" },\n  \"assets\": { \"run_worker_first\": true }\n}\n\n// ✅ GOOD - Frontend at edge, backend optimized\n// frontend-worker/wrangler.jsonc\n{\n  \"name\": \"frontend\",\n  \"assets\": { \"run_worker_first\": true }\n  // No placement - runs at edge\n}\n\n// backend-worker/wrangler.jsonc\n{\n  \"name\": \"backend-api\",\n  \"placement\": { \"mode\": \"smart\" },\n  \"d1_databases\": [{ \"binding\": \"DB\", \"database_id\": \"xxx\" }]\n}\n```\n\n**Key takeaway:** Never enable Smart Placement on Workers that serve static assets with `run_worker_first = true`.\n\n## Local Development\n\nSmart Placement does NOT work in `wrangler dev` (local only). Test by deploying: `wrangler deploy --env staging`\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/smart-placement/gotchas.md",
    "content": "# Smart Placement Gotchas\n\n## Common Errors\n\n### \"INSUFFICIENT_INVOCATIONS\"\n\n**Cause:** Not enough traffic for Smart Placement to analyze\n**Solution:**\n- Ensure Worker receives consistent global traffic\n- Wait longer (analysis takes up to 15 minutes)\n- Send test traffic from multiple global locations\n- Check Worker has fetch event handler\n\n### \"UNSUPPORTED_APPLICATION\"\n\n**Cause:** Smart Placement made Worker slower rather than faster\n**Reasons:**\n- Worker doesn't make backend calls (runs faster at edge)\n- Backend calls are cached (network latency to user more important)\n- Backend service has good global distribution\n- Worker serves static assets or Pages content\n\n**Solutions:**\n- Disable Smart Placement: `{ \"placement\": { \"mode\": \"off\" } }`\n- Review whether Worker actually benefits from Smart Placement\n- Consider caching strategy to reduce backend calls\n- For Pages/Assets Workers, use separate backend Worker with Smart Placement\n\n### \"No request duration metrics\"\n\n**Cause:** Smart Placement not enabled, insufficient time passed, insufficient traffic, or analysis incomplete\n**Solution:**\n- Ensure Smart Placement enabled in config\n- Wait 15+ minutes after deployment\n- Verify Worker has sufficient traffic\n- Check `placement_status` is `SUCCESS`\n\n### \"cf-placement header missing\"\n\n**Cause:** Smart Placement not enabled, beta feature removed, or Worker not analyzed yet\n**Solution:** Verify Smart Placement enabled, wait for analysis (15min), check if beta feature still available\n\n## Pages/Assets + Smart Placement Performance Degradation\n\n**Problem:** Static assets load 2-5x slower when Smart Placement enabled with `run_worker_first = true`.\n\n**Cause:** Smart Placement routes ALL requests (including static assets like HTML, CSS, JS, images) to remote locations. Static content should ALWAYS be served from edge closest to user.\n\n**Solution:** Split into separate Workers OR disable Smart Placement:\n```jsonc\n// ❌ BAD - Assets routed away from user\n{\n  \"name\": \"pages-app\",\n  \"placement\": { \"mode\": \"smart\" },\n  \"assets\": { \"run_worker_first\": true }\n}\n\n// ✅ GOOD - Assets at edge, API optimized\n// frontend/wrangler.jsonc\n{\n  \"name\": \"frontend\",\n  \"assets\": { \"run_worker_first\": true }\n  // No placement field - stays at edge\n}\n\n// backend/wrangler.jsonc\n{\n  \"name\": \"backend-api\",\n  \"placement\": { \"mode\": \"smart\" }\n}\n```\n\nThis is one of the most common and impactful Smart Placement misconfigurations.\n\n## Monolithic Full-Stack Worker\n\n**Problem:** Frontend and backend logic in single Worker with Smart Placement enabled.\n\n**Cause:** Smart Placement optimizes for backend latency but increases user-facing response time.\n\n**Solution:** Split into two Workers:\n```jsonc\n// frontend/wrangler.jsonc\n{\n  \"name\": \"frontend\",\n  \"placement\": { \"mode\": \"off\" },  // Explicit: stay at edge\n  \"services\": [{ \"binding\": \"BACKEND\", \"service\": \"backend-api\" }]\n}\n\n// backend/wrangler.jsonc\n{\n  \"name\": \"backend-api\",\n  \"placement\": { \"mode\": \"smart\" },\n  \"d1_databases\": [{ \"binding\": \"DB\", \"database_id\": \"xxx\" }]\n}\n```\n\n## Local Development Confusion\n\n**Issue:** Smart Placement doesn't work in `wrangler dev`.\n\n**Explanation:** Smart Placement only activates in production deployments, not local development.\n\n**Solution:** Test Smart Placement in staging environment: `wrangler deploy --env staging`\n\n## Baseline Traffic & Analysis Time\n\n**Note:** Smart Placement routes 1% of requests WITHOUT optimization for comparison (expected).\n\n**Analysis time:** Up to 15 minutes. During analysis, Worker runs at edge. Monitor `placement_status`.\n\n## RPC Methods Not Affected (Critical Limitation)\n\n**Problem:** Enabled Smart Placement on backend but RPC calls still slow.\n\n**Cause:** Smart Placement ONLY affects `fetch` handlers. RPC methods (Service Bindings with `WorkerEntrypoint`) are NEVER affected.\n\n**Why:** RPC bypasses `fetch` handler - Smart Placement can only route `fetch` requests.\n\n**Solution:** Convert to fetch-based Service Bindings:\n\n```typescript\n// ❌ RPC - Smart Placement has NO EFFECT\nexport class BackendRPC extends WorkerEntrypoint {\n  async getData() {\n    // ALWAYS runs at edge\n    return await this.env.DATABASE.prepare('SELECT * FROM table').all();\n  }\n}\n\n// ✅ Fetch - Smart Placement WORKS\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // Runs close to DATABASE when Smart Placement enabled\n    const data = await env.DATABASE.prepare('SELECT * FROM table').all();\n    return Response.json(data);\n  }\n}\n```\n\n## Requirements\n\n- **Wrangler 2.20.0+** required\n- **Consistent multi-region traffic** needed for analysis\n- **Only affects fetch handlers** - RPC methods and named entrypoints not affected\n\n## Limits\n\n| Resource/Limit | Value | Notes |\n|----------------|-------|-------|\n| Analysis time | Up to 15 minutes | After enabling |\n| Baseline traffic | 1% | Routed without optimization |\n| Min Wrangler version | 2.20.0+ | Required |\n| Traffic requirement | Multi-region | Consistent needed |\n\n## Disabling Smart Placement\n\n```jsonc\n{ \"placement\": { \"mode\": \"off\" } }  // Explicit disable\n// OR remove \"placement\" field entirely (same effect)\n```\n\nBoth behaviors identical - Worker runs at edge closest to user.\n\n## When NOT to Use Smart Placement\n\n- Workers serving only static content or cached responses\n- Workers without significant backend communication\n- Pure edge logic (auth checks, redirects, simple transformations)\n- Workers without fetch event handlers\n- Pages/Assets Workers with `run_worker_first = true`\n- Workers using RPC methods instead of fetch handlers\n\nThese scenarios won't benefit and may perform worse with Smart Placement.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/smart-placement/patterns.md",
    "content": "# Smart Placement Patterns\n\n## Backend Worker with Database Access\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const user = await env.DATABASE.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();\n    const orders = await env.DATABASE.prepare('SELECT * FROM orders WHERE user_id = ?').bind(userId).all();\n    return Response.json({ user, orders });\n  }\n};\n```\n\n```jsonc\n{ \"placement\": { \"mode\": \"smart\" }, \"d1_databases\": [{ \"binding\": \"DATABASE\", \"database_id\": \"xxx\" }] }\n```\n\n## Frontend + Backend Split (Service Bindings)\n\n**Frontend:** Runs at edge for fast user response\n**Backend:** Smart Placement runs close to database\n\n```typescript\n// Frontend Worker - routes requests to backend\ninterface Env {\n  BACKEND: Fetcher;  // Service Binding to backend Worker\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    if (new URL(request.url).pathname.startsWith('/api/')) {\n      return env.BACKEND.fetch(request);  // Forward to backend\n    }\n    return new Response('Frontend content');\n  }\n};\n\n// Backend Worker - database operations\ninterface BackendEnv {\n  DATABASE: D1Database;\n}\n\nexport default {\n  async fetch(request: Request, env: BackendEnv): Promise<Response> {\n    const data = await env.DATABASE.prepare('SELECT * FROM table').all();\n    return Response.json(data);\n  }\n};\n```\n\n**CRITICAL:** Use fetch-based Service Bindings (shown above). If using RPC with `WorkerEntrypoint`, Smart Placement will NOT optimize those method calls - only `fetch` handlers are affected.\n\n**RPC vs Fetch - CRITICAL:** Smart Placement ONLY works with fetch-based bindings, NOT RPC.\n\n```typescript\n// ❌ RPC - Smart Placement has NO EFFECT on backend RPC methods\nexport class BackendRPC extends WorkerEntrypoint {\n  async getData() {\n    // ALWAYS runs at edge, Smart Placement ignored\n    return await this.env.DATABASE.prepare('SELECT * FROM table').all();\n  }\n}\n\n// ✅ Fetch - Smart Placement WORKS\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // Runs close to DATABASE when Smart Placement enabled\n    const data = await env.DATABASE.prepare('SELECT * FROM table').all();\n    return Response.json(data);\n  }\n};\n```\n\n## External API Integration\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const apiUrl = 'https://api.partner.com';\n    const headers = { 'Authorization': `Bearer ${env.API_KEY}` };\n    \n    const [profile, transactions] = await Promise.all([\n      fetch(`${apiUrl}/profile`, { headers }),\n      fetch(`${apiUrl}/transactions`, { headers })\n    ]);\n    \n    return Response.json({ \n      profile: await profile.json(), \n      transactions: await transactions.json()\n    });\n  }\n};\n```\n\n## SSR / API Gateway Pattern\n\n```typescript\n// Frontend (edge) - auth/routing close to user\nexport default {\n  async fetch(request: Request, env: Env) {\n    if (!request.headers.get('Authorization')) {\n      return new Response('Unauthorized', { status: 401 });\n    }\n    const data = await env.BACKEND.fetch(request);\n    return new Response(renderPage(await data.json()), { \n      headers: { 'Content-Type': 'text/html' } \n    });\n  }\n};\n\n// Backend (Smart Placement) - DB operations close to data\nexport default {\n  async fetch(request: Request, env: Env) {\n    const data = await env.DATABASE.prepare('SELECT * FROM pages WHERE id = ?').bind(pageId).first();\n    return Response.json(data);\n  }\n};\n```\n\n## Durable Objects with Smart Placement\n\n**Key principle:** Smart Placement does NOT control WHERE Durable Objects run. DOs always run in their designated region (based on jurisdiction or smart location hints).\n\n**What Smart Placement DOES affect:** The location of the coordinator Worker's `fetch` handler that makes calls to multiple DOs.\n\n**Pattern:** Enable Smart Placement on coordinator Worker that aggregates data from multiple DOs:\n\n```typescript\n// Worker with Smart Placement - aggregates data from multiple DOs\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const userId = new URL(request.url).searchParams.get('user');\n    \n    // Get DO stubs\n    const userDO = env.USER_DO.get(env.USER_DO.idFromName(userId));\n    const analyticsID = env.ANALYTICS_DO.idFromName(`analytics-${userId}`);\n    const analyticsDO = env.ANALYTICS_DO.get(analyticsID);\n    \n    // Fetch from multiple DOs\n    const [userData, analyticsData] = await Promise.all([\n      userDO.fetch(new Request('https://do/profile')),\n      analyticsDO.fetch(new Request('https://do/stats'))\n    ]);\n    \n    return Response.json({\n      user: await userData.json(),\n      analytics: await analyticsData.json()\n    });\n  }\n};\n```\n\n```jsonc\n// wrangler.jsonc\n{\n  \"placement\": { \"mode\": \"smart\" },\n  \"durable_objects\": {\n    \"bindings\": [\n      { \"name\": \"USER_DO\", \"class_name\": \"UserDO\" },\n      { \"name\": \"ANALYTICS_DO\", \"class_name\": \"AnalyticsDO\" }\n    ]\n  }\n}\n```\n\n**When this helps:** \n- Worker's `fetch` handler runs closer to DO regions, reducing network latency for multiple DO calls\n- Most beneficial when DOs are geographically concentrated or in specific jurisdictions\n- Helps when coordinator makes many sequential or parallel DO calls\n\n**When this DOESN'T help:**\n- DOs are globally distributed (no single optimal Worker location)\n- Worker only calls a single DO\n- DO calls are infrequent or cached\n\n## Best Practices\n\n- Split full-stack apps: frontend at edge, backend with Smart Placement\n- Use fetch-based Service Bindings (not RPC)\n- Enable for backend logic: APIs, data aggregation, DB operations\n- Don't enable for: static content, edge logic, RPC methods, Pages with `run_worker_first`\n- Wait 15+ min for analysis, verify `placement_status = SUCCESS`\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/snippets/README.md",
    "content": "# Cloudflare Snippets Skill Reference\n\n## Description\nExpert guidance for **Cloudflare Snippets ONLY** - a lightweight JavaScript-based edge logic platform for modifying HTTP requests and responses. Snippets run as part of the Ruleset Engine and are included at no additional cost on paid plans (Pro, Business, Enterprise).\n\n## What Are Snippets?\nSnippets are JavaScript functions executed at the edge as part of Cloudflare's Ruleset Engine. Key characteristics:\n- **Execution time**: 5ms CPU limit per request\n- **Size limit**: 32KB per snippet\n- **Runtime**: V8 isolate (subset of Workers APIs)\n- **Subrequests**: 2-5 fetch calls depending on plan\n- **Cost**: Included with Pro/Business/Enterprise plans\n\n## Snippets vs Workers Decision Matrix\n\n| Factor | Choose Snippets If... | Choose Workers If... |\n|--------|----------------------|---------------------|\n| **Complexity** | Simple request/response modifications | Complex business logic, routing, middleware |\n| **Execution time** | <5ms sufficient | Need >5ms or variable time |\n| **Subrequests** | 2-5 fetch calls sufficient | Need >5 subrequests or complex orchestration |\n| **Code size** | <32KB sufficient | Need >32KB or npm dependencies |\n| **Cost** | Want zero additional cost | Can afford $5/mo + usage |\n| **APIs** | Need basic fetch, headers, URL | Need KV, D1, R2, Durable Objects, cron triggers |\n| **Deployment** | Need rule-based triggers | Want custom routing logic |\n\n**Rule of thumb**: Use Snippets for modifications, Workers for applications.\n\n## Execution Model\n1. Request arrives at Cloudflare edge\n2. Ruleset Engine evaluates snippet rules (filter expressions)\n3. If rule matches, snippet executes within 5ms limit\n4. Modified request/response continues through pipeline\n5. Response returned to client\n\nSnippets execute synchronously in the request path - performance is critical.\n\n## Reading Order\n1. **[configuration.md](configuration.md)** - Start here: setup, deployment methods (Dashboard/API/Terraform)\n2. **[api.md](api.md)** - Core APIs: Request, Response, headers, `request.cf` properties\n3. **[patterns.md](patterns.md)** - Real-world examples: geo-routing, A/B tests, security headers\n4. **[gotchas.md](gotchas.md)** - Troubleshooting: common errors, performance tips, API limitations\n\n## In This Reference\n\n- **[configuration.md](configuration.md)** - Setup, deployment, configuration\n- **[api.md](api.md)** - API endpoints, methods, interfaces\n- **[patterns.md](patterns.md)** - Common patterns, use cases, examples\n- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations\n\n## Quick Start\n```javascript\n// Snippet: Add security headers\nexport default {\n  async fetch(request) {\n    const response = await fetch(request);\n    const newResponse = new Response(response.body, response);\n    newResponse.headers.set(\"X-Frame-Options\", \"DENY\");\n    newResponse.headers.set(\"X-Content-Type-Options\", \"nosniff\");\n    return newResponse;\n  }\n}\n```\n\nDeploy via Dashboard (Rules → Snippets) or API/Terraform. See configuration.md for details.\n\n## See Also\n\n- [Cloudflare Docs](https://developers.cloudflare.com/rules/snippets/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/snippets/api.md",
    "content": "# Snippets API Reference\n\n## Request Object\n\n### HTTP Properties\n```javascript\nrequest.method    // GET, POST, PUT, DELETE, etc.\nrequest.url       // Full URL string\nrequest.headers   // Headers object\nrequest.body      // ReadableStream (for POST/PUT)\nrequest.cf        // Cloudflare properties (see below)\n```\n\n### URL Operations\n```javascript\nconst url = new URL(request.url);\nurl.hostname             // \"example.com\"\nurl.pathname             // \"/path/to/page\"\nurl.search               // \"?query=value\"\nurl.searchParams.get(\"q\") // \"value\"\nurl.searchParams.set(\"q\", \"new\")\nurl.searchParams.delete(\"q\")\n```\n\n### Header Operations\n```javascript\n// Read headers\nrequest.headers.get(\"User-Agent\")\nrequest.headers.has(\"Authorization\")\nrequest.headers.getSetCookie() // Get all Set-Cookie headers\n\n// Modify headers (create new request)\nconst modifiedRequest = new Request(request);\nmodifiedRequest.headers.set(\"X-Custom\", \"value\")\nmodifiedRequest.headers.delete(\"X-Remove\")\n```\n\n### Cloudflare Properties (`request.cf`)\nAccess Cloudflare-specific metadata about the request:\n\n```javascript\n// Geolocation\nrequest.cf.city            // \"San Francisco\"\nrequest.cf.continent       // \"NA\"\nrequest.cf.country         // \"US\"\nrequest.cf.region          // \"California\" or \"CA\"\nrequest.cf.regionCode      // \"CA\"\nrequest.cf.postalCode      // \"94102\"\nrequest.cf.latitude        // \"37.7749\"\nrequest.cf.longitude       // \"-122.4194\"\nrequest.cf.timezone        // \"America/Los_Angeles\"\nrequest.cf.metroCode       // \"807\" (DMA code)\n\n// Network\nrequest.cf.colo            // \"SFO\" (airport code of datacenter)\nrequest.cf.asn             // 13335 (ASN number)\nrequest.cf.asOrganization  // \"Cloudflare, Inc.\"\n\n// Bot Management (if enabled)\nrequest.cf.botManagement.score        // 1-99 (1=bot, 99=human)\nrequest.cf.botManagement.verified_bot // true/false\nrequest.cf.botManagement.static_resource // true/false\n\n// TLS/HTTP version\nrequest.cf.tlsVersion      // \"TLSv1.3\"\nrequest.cf.tlsCipher       // \"AEAD-AES128-GCM-SHA256\"\nrequest.cf.httpProtocol    // \"HTTP/2\"\n\n// Request metadata\nrequest.cf.requestPriority // \"weight=192;exclusive=0\"\n```\n\n**Use cases**: Geo-routing, bot detection, security decisions, analytics.\n\n## Response Object\n\n### Response Constructors\n```javascript\n// Plain text\nnew Response(\"Hello\", { status: 200 })\n\n// JSON\nResponse.json({ key: \"value\" }, { status: 200 })\n\n// HTML\nnew Response(\"<h1>Hi</h1>\", { \n  status: 200,\n  headers: { \"Content-Type\": \"text/html\" }\n})\n\n// Redirect\nResponse.redirect(\"https://example.com\", 301) // or 302\n\n// Stream (pass through)\nnew Response(response.body, response)\n```\n\n### Response Headers\n```javascript\n// Create modified response\nconst newResponse = new Response(response.body, response);\n\n// Set/modify headers\nnewResponse.headers.set(\"X-Custom\", \"value\")\nnewResponse.headers.append(\"Set-Cookie\", \"session=abc; Path=/\")\nnewResponse.headers.delete(\"Server\")\n\n// Common headers\nnewResponse.headers.set(\"Cache-Control\", \"public, max-age=3600\")\nnewResponse.headers.set(\"Content-Type\", \"application/json\")\n```\n\n### Response Properties\n```javascript\nresponse.status       // 200, 404, 500, etc.\nresponse.statusText   // \"OK\", \"Not Found\", etc.\nresponse.headers      // Headers object\nresponse.body         // ReadableStream\nresponse.ok           // true if status 200-299\nresponse.redirected   // true if redirected\n```\n\n## REST API Operations\n\n### List Snippets\n```bash\nGET /zones/{zone_id}/snippets\n```\n\n### Get Snippet\n```bash\nGET /zones/{zone_id}/snippets/{snippet_name}\n```\n\n### Create/Update Snippet\n```bash\nPUT /zones/{zone_id}/snippets/{snippet_name}\nContent-Type: multipart/form-data\n\nfiles=@snippet.js\nmetadata={\"main_module\":\"snippet.js\"}\n```\n\n### Delete Snippet\n```bash\nDELETE /zones/{zone_id}/snippets/{snippet_name}\n```\n\n### List Snippet Rules\n```bash\nGET /zones/{zone_id}/rulesets/phases/http_request_snippets/entrypoint\n```\n\n### Update Snippet Rules\n```bash\nPUT /zones/{zone_id}/snippets/snippet_rules\nContent-Type: application/json\n\n{\n  \"rules\": [{\n    \"description\": \"Apply snippet\",\n    \"enabled\": true,\n    \"expression\": \"http.host eq \\\"example.com\\\"\",\n    \"snippet_name\": \"my_snippet\"\n  }]\n}\n```\n\n## Available APIs in Snippets\n\n### ✅ Supported\n- `fetch()` - HTTP requests (2-5 subrequests per plan)\n- `Request` / `Response` - Standard Web APIs\n- `URL` / `URLSearchParams` - URL manipulation\n- `Headers` - Header manipulation\n- `TextEncoder` / `TextDecoder` - Text encoding\n- `crypto.subtle` - Web Crypto API (hashing, signing)\n- `crypto.randomUUID()` - UUID generation\n\n### ❌ Not Supported in Snippets\n- `caches` API - Not available (use Workers)\n- `KV`, `D1`, `R2` - Storage APIs (use Workers)\n- `Durable Objects` - Stateful objects (use Workers)\n- `WebSocket` - WebSocket upgrades (use Workers)\n- `HTMLRewriter` - HTML parsing (use Workers)\n- `import` statements - No module imports\n- `addEventListener` - Use `export default { async fetch() {}` pattern\n\n## Snippet Structure\n```javascript\nexport default {\n  async fetch(request) {\n    // Your logic here\n    const response = await fetch(request);\n    return response; // or modified response\n  }\n}\n```"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/snippets/configuration.md",
    "content": "# Snippets Configuration Guide\n\n## Configuration Methods\n\n### 1. Dashboard (GUI)\n**Best for**: Quick tests, single snippets, visual rule building\n\n```\n1. Go to zone → Rules → Snippets\n2. Click \"Create Snippet\" or select template\n3. Enter snippet name (a-z, 0-9, _ only, cannot change later)\n4. Write JavaScript code (32KB max)\n5. Configure snippet rule:\n   - Expression Builder (visual) or Expression Editor (text)\n   - Use Ruleset Engine filter expressions\n6. Test with Preview/HTTP tabs\n7. Deploy or Save as Draft\n```\n\n### 2. REST API\n**Best for**: CI/CD, automation, programmatic management\n\n```bash\n# Create/update snippet\ncurl \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/snippets/$SNIPPET_NAME\" \\\n  --request PUT \\\n  --header \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\" \\\n  --form \"files=@example.js\" \\\n  --form \"metadata={\\\"main_module\\\": \\\"example.js\\\"}\"\n\n# Create snippet rule\ncurl \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/snippets/snippet_rules\" \\\n  --request PUT \\\n  --header \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"rules\": [\n      {\n        \"description\": \"Trigger snippet on /api paths\",\n        \"enabled\": true,\n        \"expression\": \"starts_with(http.request.uri.path, \\\"/api/\\\")\",\n        \"snippet_name\": \"api_snippet\"\n      }\n    ]\n  }'\n\n# List snippets\ncurl \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/snippets\" \\\n  --header \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\"\n\n# Delete snippet\ncurl \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/snippets/$SNIPPET_NAME\" \\\n  --request DELETE \\\n  --header \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\"\n```\n\n### 3. Terraform\n**Best for**: Infrastructure-as-code, multi-zone deployments\n\n```hcl\n# Configure Terraform provider\nterraform {\n  required_providers {\n    cloudflare = {\n      source  = \"cloudflare/cloudflare\"\n      version = \"~> 4.0\"\n    }\n  }\n}\n\nprovider \"cloudflare\" {\n  api_token = var.cloudflare_api_token\n}\n\n# Create snippet\nresource \"cloudflare_snippet\" \"security_headers\" {\n  zone_id = var.zone_id\n  name    = \"security_headers\"\n  \n  main_module = \"security_headers.js\"\n  files {\n    name    = \"security_headers.js\"\n    content = file(\"${path.module}/snippets/security_headers.js\")\n  }\n}\n\n# Create snippet rule\nresource \"cloudflare_snippet_rules\" \"security_rules\" {\n  zone_id = var.zone_id\n  \n  rules {\n    description  = \"Apply security headers to all requests\"\n    enabled      = true\n    expression   = \"true\"\n    snippet_name = cloudflare_snippet.security_headers.name\n  }\n}\n```\n\n### 4. Pulumi\n**Best for**: Multi-cloud IaC, TypeScript/Python/Go workflows\n\n```typescript\nimport * as cloudflare from \"@pulumi/cloudflare\";\nimport * as fs from \"fs\";\n\n// Create snippet\nconst securitySnippet = new cloudflare.Snippet(\"security-headers\", {\n  zoneId: zoneId,\n  name: \"security_headers\",\n  mainModule: \"security_headers.js\",\n  files: [{\n    name: \"security_headers.js\",\n    content: fs.readFileSync(\"./snippets/security_headers.js\", \"utf8\"),\n  }],\n});\n\n// Create snippet rule\nconst snippetRule = new cloudflare.SnippetRules(\"security-rules\", {\n  zoneId: zoneId,\n  rules: [{\n    description: \"Apply security headers\",\n    enabled: true,\n    expression: \"true\",\n    snippetName: securitySnippet.name,\n  }],\n});\n```\n\n## Filter Expressions\n\nSnippets use Cloudflare's Ruleset Engine expression language to determine when to execute.\n\n### Common Expression Patterns\n\n```javascript\n// Host matching\nhttp.host eq \"example.com\"\nhttp.host in {\"example.com\" \"www.example.com\"}\nhttp.host contains \"example\"\n\n// Path matching\nhttp.request.uri.path eq \"/api/users\"\nstarts_with(http.request.uri.path, \"/api/\")\nends_with(http.request.uri.path, \".json\")\nmatches(http.request.uri.path, \"^/api/v[0-9]+/\")\n\n// Query parameters\nhttp.request.uri.query contains \"debug=true\"\n\n// Headers\nhttp.headers[\"user-agent\"] contains \"Mobile\"\nhttp.headers[\"accept-language\"] eq \"en-US\"\n\n// Cookies\nhttp.cookie contains \"session=\"\n\n// Geolocation\nip.geoip.country eq \"US\"\nip.geoip.continent eq \"EU\"\n\n// Bot detection (requires Bot Management)\ncf.bot_management.score lt 30\n\n// Method\nhttp.request.method eq \"POST\"\nhttp.request.method in {\"POST\" \"PUT\" \"PATCH\"}\n\n// Combine with logical operators\nhttp.host eq \"example.com\" and starts_with(http.request.uri.path, \"/api/\")\nip.geoip.country eq \"US\" or ip.geoip.country eq \"CA\"\nnot http.headers[\"user-agent\"] contains \"bot\"\n```\n\n### Expression Functions\n\n| Function | Example | Description |\n|----------|---------|-------------|\n| `starts_with()` | `starts_with(http.request.uri.path, \"/api/\")` | Check prefix |\n| `ends_with()` | `ends_with(http.request.uri.path, \".json\")` | Check suffix |\n| `contains()` | `contains(http.headers[\"user-agent\"], \"Mobile\")` | Check substring |\n| `matches()` | `matches(http.request.uri.path, \"^/api/\")` | Regex match |\n| `lower()` | `lower(http.host) eq \"example.com\"` | Convert to lowercase |\n| `upper()` | `upper(http.headers[\"x-api-key\"])` | Convert to uppercase |\n| `len()` | `len(http.request.uri.path) gt 100` | String length |\n\n## Deployment Workflow\n\n### Development\n1. Write snippet code locally\n2. Test syntax with `node snippet.js` or TypeScript compiler\n3. Deploy to Dashboard or use API with `Save as Draft`\n4. Test with Preview/HTTP tabs in Dashboard\n5. Enable rule when ready\n\n### Production\n1. Store snippet code in version control\n2. Use Terraform/Pulumi for reproducible deployments\n3. Deploy to staging zone first\n4. Test with real traffic (use low-traffic subdomain)\n5. Apply to production zone\n6. Monitor with Analytics/Logpush\n\n## Limits & Requirements\n\n| Resource | Limit | Notes |\n|----------|-------|-------|\n| Snippet size | 32 KB | Per snippet, compressed |\n| Snippet name | 64 chars | `a-z`, `0-9`, `_` only, immutable |\n| Snippets per zone | 20 | Soft limit, contact support for more |\n| Rules per zone | 20 | One rule per snippet typical |\n| Expression length | 4096 chars | Per rule expression |\n\n## Authentication\n\n### API Token (Recommended)\n```bash\n# Create token at: https://dash.cloudflare.com/profile/api-tokens\n# Required permissions: Zone.Snippets:Edit, Zone.Rules:Edit\nexport CLOUDFLARE_API_TOKEN=\"your_token_here\"\n```\n\n### API Key (Legacy)\n```bash\nexport CLOUDFLARE_EMAIL=\"your@email.com\"\nexport CLOUDFLARE_API_KEY=\"your_global_api_key\"\n``` "
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/snippets/gotchas.md",
    "content": "# Gotchas & Best Practices\n\n## Common Errors\n\n### 1000: \"Snippet execution failed\"\nRuntime error or syntax error. Wrap code in try/catch:\n```javascript\ntry { return await fetch(request); }\ncatch (error) { return new Response(`Error: ${error.message}`, { status: 500 }); }\n```\n\n### 1100: \"Exceeded execution limit\"\nCode takes >5ms CPU. Simplify logic or move to Workers.\n\n### 1201: \"Multiple origin fetches\"\nCall `fetch(request)` exactly once:\n```javascript\n// ❌ Multiple origin fetches\nconst r1 = await fetch(request); const r2 = await fetch(request);\n// ✅ Single fetch, reuse response\nconst response = await fetch(request);\n```\n\n### 1202: \"Subrequest limit exceeded\"\nPro: 2 subrequests, Business/Enterprise: 5. Reduce fetch calls.\n\n### \"Cannot set property on immutable object\"\nClone before modifying:\n```javascript\nconst modifiedRequest = new Request(request);\nmodifiedRequest.headers.set(\"X-Custom\", \"value\");\n```\n\n### \"caches is not defined\"\nCache API NOT available in Snippets. Use Workers.\n\n### \"Module not found\"\nSnippets don't support `import`. Use inline code or Workers.\n\n## Best Practices\n\n### Performance\n- Keep code <10KB (32KB limit)\n- Optimize for 5ms CPU\n- Clone only when modifying\n- Minimize subrequests\n\n### Security\n- Validate all inputs\n- Use Web Crypto API for hashing\n- Sanitize headers before origin\n- Don't log secrets\n\n### Debugging\n```javascript\nnewResponse.headers.set(\"X-Debug-Country\", request.cf.country);\n```\n```bash\ncurl -H \"X-Test: true\" https://example.com -v\n```\n\n## Available APIs\n\n**✅ Available:** `fetch()`, `Request`, `Response`, `Headers`, `URL`, `crypto.subtle`, `crypto.randomUUID()`, `atob()`/`btoa()`, `JSON`\n\n**❌ NOT Available:** `caches`, `KV`, `D1`, `R2`, `Durable Objects`, `WebSocket`, `HTMLRewriter`, `import`, Node.js APIs\n\n## Limits\n\n| Resource | Limit |\n|----------|-------|\n| Snippet size | 32KB |\n| Execution time | 5ms CPU |\n| Subrequests (Pro/Biz) | 2/5 |\n| Snippets/zone | 20 |\n\n## Performance Benchmarks\n\n| Operation | Time |\n|-----------|------|\n| Header set | <0.1ms |\n| URL parsing | <0.2ms |\n| fetch() | 1-3ms |\n| SHA-256 | 0.5-1ms |\n\n**Migrate to Workers when:** >5ms needed, >5 subrequests, need storage (KV/D1/R2), need npm packages, >32KB code\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/snippets/patterns.md",
    "content": "# Snippets Patterns\n\n## Security Headers\n\n```javascript\nexport default {\n  async fetch(request) {\n    const response = await fetch(request);\n    const newResponse = new Response(response.body, response);\n    newResponse.headers.set(\"X-Frame-Options\", \"DENY\");\n    newResponse.headers.set(\"X-Content-Type-Options\", \"nosniff\");\n    newResponse.headers.delete(\"X-Powered-By\");\n    return newResponse;\n  }\n}\n```\n\n**Rule:** `true` (all requests)\n\n## Geo-Based Routing\n\n```javascript\nexport default {\n  async fetch(request) {\n    const country = request.cf.country;\n    if ([\"GB\", \"DE\", \"FR\"].includes(country)) {\n      const url = new URL(request.url);\n      url.hostname = url.hostname.replace(\".com\", \".eu\");\n      return Response.redirect(url.toString(), 302);\n    }\n    return fetch(request);\n  }\n}\n```\n\n## A/B Testing\n\n```javascript\nexport default {\n  async fetch(request) {\n    const cookies = request.headers.get(\"Cookie\") || \"\";\n    let variant = cookies.match(/ab_test=([AB])/)?.[1] || (Math.random() < 0.5 ? \"A\" : \"B\");\n    \n    const req = new Request(request);\n    req.headers.set(\"X-Variant\", variant);\n    const response = await fetch(req);\n    \n    if (!cookies.includes(\"ab_test=\")) {\n      const newResponse = new Response(response.body, response);\n      newResponse.headers.append(\"Set-Cookie\", `ab_test=${variant}; Path=/; Secure`);\n      return newResponse;\n    }\n    return response;\n  }\n}\n```\n\n## Bot Detection\n\n```javascript\nexport default {\n  async fetch(request) {\n    const botScore = request.cf.botManagement?.score;\n    if (botScore && botScore < 30) return new Response(\"Denied\", { status: 403 });\n    return fetch(request);\n  }\n}\n```\n\n**Requires:** Bot Management plan\n\n## API Auth Header Injection\n\n```javascript\nexport default {\n  async fetch(request) {\n    if (new URL(request.url).pathname.startsWith(\"/api/\")) {\n      const req = new Request(request);\n      req.headers.set(\"X-Internal-Auth\", \"secret_token\");\n      req.headers.delete(\"Authorization\");\n      return fetch(req);\n    }\n    return fetch(request);\n  }\n}\n```\n\n## CORS Headers\n\n```javascript\nexport default {\n  async fetch(request) {\n    if (request.method === \"OPTIONS\") {\n      return new Response(null, {\n        status: 204,\n        headers: {\n          \"Access-Control-Allow-Origin\": \"*\",\n          \"Access-Control-Allow-Methods\": \"GET, POST, PUT, DELETE\",\n          \"Access-Control-Allow-Headers\": \"Content-Type, Authorization\"\n        }\n      });\n    }\n    const response = await fetch(request);\n    const newResponse = new Response(response.body, response);\n    newResponse.headers.set(\"Access-Control-Allow-Origin\", \"*\");\n    return newResponse;\n  }\n}\n```\n\n## Maintenance Mode\n\n```javascript\nexport default {\n  async fetch(request) {\n    if (request.headers.get(\"X-Bypass-Token\") === \"admin\") return fetch(request);\n    return new Response(\"<h1>Maintenance</h1>\", {\n      status: 503,\n      headers: { \"Content-Type\": \"text/html\", \"Retry-After\": \"3600\" }\n    });\n  }\n}\n```\n\n## Pattern Selection\n\n| Pattern | Complexity | Use Case |\n|---------|-----------|----------|\n| Security Headers | Low | All sites |\n| Geo-Routing | Low | Regional content |\n| A/B Testing | Medium | Experiments |\n| Bot Detection | Medium | Requires Bot Management |\n| API Auth | Low | Backend protection |\n| CORS | Low | API endpoints |\n| Maintenance | Low | Deployments |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/spectrum/README.md",
    "content": "# Cloudflare Spectrum Skill Reference\n\n## Overview\n\nCloudflare Spectrum provides security and acceleration for ANY TCP or UDP-based application. It's a global Layer 4 (L4) reverse proxy running on Cloudflare's edge nodes that routes MQTT, email, file transfer, version control, games, and more through Cloudflare to mask origins and protect from DDoS attacks.\n\n**When to Use Spectrum**: When your protocol isn't HTTP/HTTPS (use Cloudflare proxy for HTTP). Spectrum handles everything else: SSH, gaming, databases, MQTT, SMTP, RDP, custom protocols.\n\n## Plan Capabilities\n\n| Capability | Pro/Business | Enterprise |\n|------------|--------------|------------|\n| TCP protocols | Selected ports only | All ports (1-65535) |\n| UDP protocols | Selected ports only | All ports (1-65535) |\n| Port ranges | ❌ | ✅ |\n| Argo Smart Routing | ✅ | ✅ |\n| IP Firewall | ✅ | ✅ |\n| Load balancer origins | ✅ | ✅ |\n\n## Decision Tree\n\n**What are you trying to do?**\n\n1. **Create/manage Spectrum app**\n   - Via Dashboard → See [Cloudflare Dashboard](https://dash.cloudflare.com)\n   - Via API → See [api.md](api.md) - REST endpoints\n   - Via SDK → See [api.md](api.md) - TypeScript/Python/Go examples\n   - Via IaC → See [configuration.md](configuration.md) - Terraform/Pulumi\n\n2. **Protect specific protocol**\n   - SSH → See [patterns.md](patterns.md#1-ssh-server-protection)\n   - Gaming (Minecraft, etc) → See [patterns.md](patterns.md#2-game-server)\n   - MQTT/IoT → See [patterns.md](patterns.md#3-mqtt-broker)\n   - SMTP/Email → See [patterns.md](patterns.md#4-smtp-relay)\n   - Database → See [patterns.md](patterns.md#5-database-proxy)\n   - RDP → See [patterns.md](patterns.md#6-rdp-remote-desktop)\n\n3. **Choose origin type**\n   - Direct IP (single server) → See [configuration.md](configuration.md#direct-ip-origin)\n   - CNAME (hostname) → See [configuration.md](configuration.md#cname-origin)\n   - Load balancer (HA/failover) → See [configuration.md](configuration.md#load-balancer-origin)\n\n## Reading Order\n\n1. Start with [patterns.md](patterns.md) for your specific protocol\n2. Then [configuration.md](configuration.md) for your origin type\n3. Check [gotchas.md](gotchas.md) before going to production\n4. Use [api.md](api.md) for programmatic access\n\n## See Also\n\n- [Cloudflare Docs](https://developers.cloudflare.com/spectrum/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/spectrum/api.md",
    "content": "## REST API Endpoints\n\n```\nGET    /zones/{zone_id}/spectrum/apps                    # List apps\nPOST   /zones/{zone_id}/spectrum/apps                    # Create app\nGET    /zones/{zone_id}/spectrum/apps/{app_id}           # Get app\nPUT    /zones/{zone_id}/spectrum/apps/{app_id}           # Update app\nDELETE /zones/{zone_id}/spectrum/apps/{app_id}           # Delete app\n\nGET    /zones/{zone_id}/spectrum/analytics/aggregate/current\nGET    /zones/{zone_id}/spectrum/analytics/events/bytime\nGET    /zones/{zone_id}/spectrum/analytics/events/summary\n```\n\n## Request/Response Schemas\n\n### CreateSpectrumAppRequest\n\n```typescript\ninterface CreateSpectrumAppRequest {\n  protocol: string;                    // \"tcp/22\", \"udp/53\"\n  dns: {\n    type: \"CNAME\" | \"ADDRESS\";\n    name: string;                      // \"ssh.example.com\"\n  };\n  origin_direct?: string[];            // [\"tcp://192.0.2.1:22\"]\n  origin_dns?: { name: string };       // {\"name\": \"origin.example.com\"}\n  origin_port?: number | { start: number; end: number };\n  proxy_protocol?: \"off\" | \"v1\" | \"v2\" | \"simple\";\n  ip_firewall?: boolean;\n  tls?: \"off\" | \"flexible\" | \"full\" | \"strict\";\n  edge_ips?: {\n    type: \"dynamic\" | \"static\";\n    connectivity: \"all\" | \"ipv4\" | \"ipv6\";\n  };\n  traffic_type?: \"direct\" | \"http\" | \"https\";\n  argo_smart_routing?: boolean;\n}\n```\n\n### SpectrumApp Response\n\n```typescript\ninterface SpectrumApp {\n  id: string;\n  protocol: string;\n  dns: { type: string; name: string };\n  origin_direct?: string[];\n  origin_dns?: { name: string };\n  origin_port?: number | { start: number; end: number };\n  proxy_protocol: string;\n  ip_firewall: boolean;\n  tls: string;\n  edge_ips: { type: string; connectivity: string; ips?: string[] };\n  argo_smart_routing: boolean;\n  created_on: string;\n  modified_on: string;\n}\n```\n\n## TypeScript SDK\n\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN });\n\n// Create\nconst app = await client.spectrum.apps.create({\n  zone_id: 'your-zone-id',\n  protocol: 'tcp/22',\n  dns: { type: 'CNAME', name: 'ssh.example.com' },\n  origin_direct: ['tcp://192.0.2.1:22'],\n  ip_firewall: true,\n  tls: 'off',\n});\n\n// List\nconst apps = await client.spectrum.apps.list({ zone_id: 'your-zone-id' });\n\n// Get\nconst appDetails = await client.spectrum.apps.get({ zone_id: 'your-zone-id', app_id: app.id });\n\n// Update\nawait client.spectrum.apps.update({ zone_id: 'your-zone-id', app_id: app.id, tls: 'full' });\n\n// Delete\nawait client.spectrum.apps.delete({ zone_id: 'your-zone-id', app_id: app.id });\n\n// Analytics\nconst analytics = await client.spectrum.analytics.aggregate({\n  zone_id: 'your-zone-id',\n  metrics: ['bytesIngress', 'bytesEgress'],\n  since: new Date(Date.now() - 3600000).toISOString(),\n});\n```\n\n## Python SDK\n\n```python\nfrom cloudflare import Cloudflare\n\nclient = Cloudflare(api_token=\"your-api-token\")\n\n# Create\napp = client.spectrum.apps.create(\n    zone_id=\"your-zone-id\",\n    protocol=\"tcp/22\",\n    dns={\"type\": \"CNAME\", \"name\": \"ssh.example.com\"},\n    origin_direct=[\"tcp://192.0.2.1:22\"],\n    ip_firewall=True,\n    tls=\"off\",\n)\n\n# List\napps = client.spectrum.apps.list(zone_id=\"your-zone-id\")\n\n# Get\napp_details = client.spectrum.apps.get(zone_id=\"your-zone-id\", app_id=app.id)\n\n# Update\nclient.spectrum.apps.update(zone_id=\"your-zone-id\", app_id=app.id, tls=\"full\")\n\n# Delete\nclient.spectrum.apps.delete(zone_id=\"your-zone-id\", app_id=app.id)\n\n# Analytics\nanalytics = client.spectrum.analytics.aggregate(\n    zone_id=\"your-zone-id\",\n    metrics=[\"bytesIngress\", \"bytesEgress\"],\n    since=datetime.now() - timedelta(hours=1),\n)\n```\n\n## Go SDK\n\n```go\nimport \"github.com/cloudflare/cloudflare-go\"\n\napi, _ := cloudflare.NewWithAPIToken(\"your-api-token\")\n\n// Create\napp, _ := api.CreateSpectrumApplication(ctx, \"zone-id\", cloudflare.SpectrumApplication{\n    Protocol:         \"tcp/22\",\n    DNS:              cloudflare.SpectrumApplicationDNS{Type: \"CNAME\", Name: \"ssh.example.com\"},\n    OriginDirect:     []string{\"tcp://192.0.2.1:22\"},\n    IPFirewall:       true,\n    ArgoSmartRouting: true,\n})\n\n// List\napps, _ := api.SpectrumApplications(ctx, \"zone-id\")\n\n// Delete\n_ = api.DeleteSpectrumApplication(ctx, \"zone-id\", app.ID)\n```\n\n## Analytics API\n\n**Metrics:**\n- `bytesIngress` - Bytes received from clients\n- `bytesEgress` - Bytes sent to clients\n- `count` - Number of connections\n- `duration` - Connection duration (seconds)\n\n**Dimensions:**\n- `event` - Connection event type\n- `appID` - Spectrum application ID\n- `coloName` - Datacenter name\n- `ipVersion` - IPv4 or IPv6\n\n**Example:**\n```bash\ncurl \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/spectrum/analytics/aggregate/current?metrics=bytesIngress,bytesEgress,count&dimensions=appID\" \\\n  --header \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\"\n```\n\n## See Also\n\n- [configuration.md](configuration.md) - Terraform/Pulumi\n- [patterns.md](patterns.md) - Protocol examples\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/spectrum/configuration.md",
    "content": "## Origin Types\n\n### Direct IP Origin\n\nUse when origin is a single server with static IP.\n\n**TypeScript SDK:**\n```typescript\nconst app = await client.spectrum.apps.create({\n  zone_id: 'your-zone-id',\n  protocol: 'tcp/22',\n  dns: { type: 'CNAME', name: 'ssh.example.com' },\n  origin_direct: ['tcp://192.0.2.1:22'],\n  ip_firewall: true,\n  tls: 'off',\n});\n```\n\n**Terraform:**\n```hcl\nresource \"cloudflare_spectrum_application\" \"ssh\" {\n  zone_id  = var.zone_id\n  protocol = \"tcp/22\"\n\n  dns {\n    type = \"CNAME\"\n    name = \"ssh.example.com\"\n  }\n\n  origin_direct      = [\"tcp://192.0.2.1:22\"]\n  ip_firewall        = true\n  tls                = \"off\"\n  argo_smart_routing = true\n}\n```\n\n### CNAME Origin\n\nUse when origin is a hostname (not static IP). Spectrum resolves DNS dynamically.\n\n**TypeScript SDK:**\n```typescript\nconst app = await client.spectrum.apps.create({\n  zone_id: 'your-zone-id',\n  protocol: 'tcp/3306',\n  dns: { type: 'CNAME', name: 'db.example.com' },\n  origin_dns: { name: 'db-primary.internal.example.com' },\n  origin_port: 3306,\n  tls: 'full',\n});\n```\n\n**Terraform:**\n```hcl\nresource \"cloudflare_spectrum_application\" \"database\" {\n  zone_id  = var.zone_id\n  protocol = \"tcp/3306\"\n\n  dns {\n    type = \"CNAME\"\n    name = \"db.example.com\"\n  }\n\n  origin_dns {\n    name = \"db-primary.internal.example.com\"\n  }\n\n  origin_port        = 3306\n  tls                = \"full\"\n  argo_smart_routing = true\n}\n```\n\n### Load Balancer Origin\n\nUse for high availability and failover.\n\n**Terraform:**\n```hcl\nresource \"cloudflare_load_balancer\" \"game_lb\" {\n  zone_id          = var.zone_id\n  name             = \"game-lb.example.com\"\n  default_pool_ids = [cloudflare_load_balancer_pool.game_pool.id]\n}\n\nresource \"cloudflare_load_balancer_pool\" \"game_pool\" {\n  name    = \"game-primary\"\n  origins { name = \"game-1\"; address = \"192.0.2.1\" }\n  monitor = cloudflare_load_balancer_monitor.tcp_monitor.id\n}\n\nresource \"cloudflare_load_balancer_monitor\" \"tcp_monitor\" {\n  type = \"tcp\"; port = 25565; interval = 60; timeout = 5\n}\n\nresource \"cloudflare_spectrum_application\" \"game\" {\n  zone_id  = var.zone_id\n  protocol = \"tcp/25565\"\n  dns { type = \"CNAME\"; name = \"game.example.com\" }\n  origin_dns { name = cloudflare_load_balancer.game_lb.name }\n  origin_port = 25565\n}\n```\n\n## TLS Configuration\n\n| Mode | Description | Use Case | Origin Cert |\n|------|-------------|----------|-------------|\n| `off` | No TLS | Non-encrypted (SSH, gaming) | No |\n| `flexible` | TLS client→CF, plain CF→origin | Testing | No |\n| `full` | TLS end-to-end, self-signed OK | Production | Yes (any) |\n| `strict` | Full + valid cert verification | Max security | Yes (CA) |\n\n**Example:**\n```typescript\nconst app = await client.spectrum.apps.create({\n  zone_id: 'your-zone-id',\n  protocol: 'tcp/3306',\n  dns: { type: 'CNAME', name: 'db.example.com' },\n  origin_direct: ['tcp://192.0.2.1:3306'],\n  tls: 'strict',  // Validates origin certificate\n});\n```\n\n## Proxy Protocol\n\nForwards real client IP to origin. Origin must support parsing.\n\n| Version | Protocol | Use Case |\n|---------|----------|----------|\n| `off` | - | Origin doesn't need client IP |\n| `v1` | TCP | Most TCP apps (SSH, databases) |\n| `v2` | TCP | High-performance TCP |\n| `simple` | UDP | UDP applications |\n\n**Compatibility:**\n- **v1**: HAProxy, nginx, SSH, most databases\n- **v2**: HAProxy 1.5+, nginx 1.11+\n- **simple**: Cloudflare-specific UDP format\n\n**Enable:**\n```typescript\nconst app = await client.spectrum.apps.create({\n  // ...\n  proxy_protocol: 'v1',  // Origin must parse PROXY header\n});\n```\n\n**Origin Config (nginx):**\n```nginx\nstream {\n    server {\n        listen 22 proxy_protocol;\n        proxy_pass backend:22;\n    }\n}\n```\n\n## IP Access Rules\n\nEnable `ip_firewall: true` then configure zone-level firewall rules.\n\n```typescript\nconst app = await client.spectrum.apps.create({\n  // ...\n  ip_firewall: true,  // Applies zone firewall rules\n});\n```\n\n## Port Ranges (Enterprise Only)\n\n```hcl\nresource \"cloudflare_spectrum_application\" \"game_cluster\" {\n  zone_id  = var.zone_id\n  protocol = \"tcp/25565-25575\"\n\n  dns {\n    type = \"CNAME\"\n    name = \"games.example.com\"\n  }\n\n  origin_direct = [\"tcp://192.0.2.1\"]\n  \n  origin_port {\n    start = 25565\n    end   = 25575\n  }\n}\n```\n\n## See Also\n\n- [patterns.md](patterns.md) - Protocol-specific examples\n- [api.md](api.md) - REST/SDK reference\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/spectrum/gotchas.md",
    "content": "## Common Issues\n\n### Connection Timeouts\n\n**Problem:** Connections fail or timeout  \n**Cause:** Origin firewall blocking Cloudflare IPs, origin service not running, incorrect DNS  \n**Solution:**\n1. Verify origin firewall allows Cloudflare IP ranges\n2. Check origin service running on correct port\n3. Ensure DNS record is CNAME (not A/AAAA)\n4. Verify origin IP/hostname is correct\n\n```bash\n# Test connectivity\nnc -zv app.example.com 22\ndig app.example.com\n```\n\n### Client IP Showing Cloudflare IP\n\n**Problem:** Origin logs show Cloudflare IPs not real client IPs  \n**Cause:** Proxy Protocol not enabled or origin not configured  \n**Solution:**\n```typescript\n// Enable in Spectrum app\nconst app = await client.spectrum.apps.create({\n  // ...\n  proxy_protocol: 'v1',  // TCP: v1/v2; UDP: simple\n});\n```\n\n**Origin config:**\n- **nginx**: `listen 22 proxy_protocol;`\n- **HAProxy**: `bind :22 accept-proxy`\n\n### TLS Errors\n\n**Problem:** TLS handshake failures, 525 errors  \n**Cause:** TLS mode mismatch\n\n| Error | TLS Mode | Problem | Solution |\n|-------|----------|---------|----------|\n| Connection refused | `full`/`strict` | Origin not TLS | Use `tls: \"off\"` or enable TLS |\n| 525 cert invalid | `strict` | Self-signed cert | Use `tls: \"full\"` or valid cert |\n| Handshake timeout | `flexible` | Origin expects TLS | Use `tls: \"full\"` |\n\n**Debug:**\n```bash\nopenssl s_client -connect app.example.com:443 -showcerts\n```\n\n### SMTP Reverse DNS\n\n**Problem:** Email servers reject SMTP via Spectrum  \n**Cause:** Spectrum IPs lack PTR (reverse DNS) records  \n**Impact:** Many mail servers require valid rDNS for anti-spam\n\n**Solution:**\n- Outbound SMTP: NOT recommended through Spectrum\n- Inbound SMTP: Use Cloudflare Email Routing\n- Internal relay: Whitelist Spectrum IPs on destination\n\n### Proxy Protocol Compatibility\n\n**Problem:** Connection works but app behaves incorrectly  \n**Cause:** Origin doesn't support Proxy Protocol\n\n**Solution:**\n1. Verify origin supports version (v1: widely supported, v2: HAProxy 1.5+/nginx 1.11+)\n2. Test with `proxy_protocol: 'off'` first\n3. Configure origin to parse headers\n\n**nginx TCP:**\n```nginx\nstream {\n    server {\n        listen 22 proxy_protocol;\n        proxy_pass backend:22;\n    }\n}\n```\n\n**HAProxy:**\n```\nfrontend ft_ssh\n    bind :22 accept-proxy\n```\n\n### Analytics Data Retention\n\n**Problem:** Historical data not available  \n**Cause:** Retention varies by plan\n\n| Plan | Real-time | Historical |\n|------|-----------|------------|\n| Pro | Last hour | ❌ |\n| Business | Last hour | Limited |\n| Enterprise | Last hour | 90+ days |\n\n**Solution:** Query within retention window or export to external system\n\n### Enterprise-Only Features\n\n**Problem:** Feature unavailable/errors  \n**Cause:** Requires Enterprise plan\n\n**Enterprise-only:**\n- Port ranges (`tcp/25565-25575`)\n- All TCP/UDP ports (Pro/Business: selected only)\n- Extended analytics retention\n- Advanced load balancing\n\n### IPv6 Considerations\n\n**Problem:** IPv6 clients can't connect or origin doesn't support IPv6  \n**Solution:** Configure `edge_ips.connectivity`\n\n```typescript\nconst app = await client.spectrum.apps.create({\n  // ...\n  edge_ips: {\n    type: 'dynamic',\n    connectivity: 'ipv4',  // Options: 'all', 'ipv4', 'ipv6'\n  },\n});\n```\n\n**Options:**\n- `all`: Dual-stack (default, requires origin support both)\n- `ipv4`: IPv4 only (use if origin lacks IPv6)\n- `ipv6`: IPv6 only (rare)\n\n## Limits\n\n| Resource | Pro/Business | Enterprise |\n|----------|--------------|------------|\n| Max apps | ~10-15 | 100+ |\n| Protocols | Selected | All TCP/UDP |\n| Port ranges | ❌ | ✅ |\n| Analytics | ~1 hour | 90+ days |\n\n## See Also\n\n- [patterns.md](patterns.md) - Protocol examples\n- [configuration.md](configuration.md) - TLS/Proxy setup\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/spectrum/patterns.md",
    "content": "## Common Use Cases\n\n### 1. SSH Server Protection\n\n**Terraform:**\n```hcl\nresource \"cloudflare_spectrum_application\" \"ssh\" {\n  zone_id  = var.zone_id\n  protocol = \"tcp/22\"\n\n  dns {\n    type = \"CNAME\"\n    name = \"ssh.example.com\"\n  }\n\n  origin_direct      = [\"tcp://10.0.1.5:22\"]\n  ip_firewall        = true\n  argo_smart_routing = true\n}\n```\n\n**Benefits:** Hide origin IP, DDoS protection, IP firewall, Argo reduces latency\n\n### 2. Game Server\n\n**TypeScript (Minecraft):**\n```typescript\nconst app = await client.spectrum.apps.create({\n  zone_id: 'your-zone-id',\n  protocol: 'tcp/25565',\n  dns: { type: 'CNAME', name: 'mc.example.com' },\n  origin_direct: ['tcp://192.168.1.10:25565'],\n  proxy_protocol: 'v1',  // Preserves player IPs\n  argo_smart_routing: true,\n});\n```\n\n**Benefits:** DDoS protection, hide origin IP, Proxy Protocol for player IPs/bans, Argo reduces latency\n\n### 3. MQTT Broker\n\nIoT device communication.\n\n**TypeScript:**\n```typescript\nconst mqttApp = await client.spectrum.apps.create({\n  zone_id: 'your-zone-id',\n  protocol: 'tcp/8883',  // Use 1883 for plain MQTT\n  dns: { type: 'CNAME', name: 'mqtt.example.com' },\n  origin_direct: ['tcp://mqtt-broker.internal:8883'],\n  tls: 'full',  // Use 'off' for plain MQTT\n});\n```\n\n**Benefits:** DDoS protection, hide broker IP, TLS termination at edge\n\n### 4. SMTP Relay\n\nEmail submission (port 587). **WARNING**: See [gotchas.md](gotchas.md#smtp-reverse-dns)\n\n**Terraform:**\n```hcl\nresource \"cloudflare_spectrum_application\" \"smtp\" {\n  zone_id  = var.zone_id\n  protocol = \"tcp/587\"\n\n  dns {\n    type = \"CNAME\"\n    name = \"smtp.example.com\"\n  }\n\n  origin_direct = [\"tcp://mail-server.internal:587\"]\n  tls           = \"full\"  # STARTTLS support\n}\n```\n\n**Limitations:**\n- Spectrum IPs lack reverse DNS (PTR records)\n- Many mail servers reject without valid rDNS\n- Best for internal/trusted relay only\n\n### 5. Database Proxy\n\nMySQL/PostgreSQL. **Use with caution** - security critical.\n\n**PostgreSQL:**\n```typescript\nconst postgresApp = await client.spectrum.apps.create({\n  zone_id: 'your-zone-id',\n  protocol: 'tcp/5432',\n  dns: { type: 'CNAME', name: 'postgres.example.com' },\n  origin_dns: { name: 'db-primary.internal.example.com' },\n  origin_port: 5432,\n  tls: 'strict',      // REQUIRED\n  ip_firewall: true,  // REQUIRED\n});\n```\n\n**MySQL:**\n```hcl\nresource \"cloudflare_spectrum_application\" \"mysql\" {\n  zone_id  = var.zone_id\n  protocol = \"tcp/3306\"\n\n  dns {\n    type = \"CNAME\"\n    name = \"mysql.example.com\"\n  }\n\n  origin_dns {\n    name = \"mysql-primary.internal.example.com\"\n  }\n\n  origin_port = 3306\n  tls         = \"strict\"\n  ip_firewall = true\n}\n```\n\n**Security:**\n- ALWAYS use `tls: \"strict\"`\n- ALWAYS use `ip_firewall: true`\n- Restrict to known IPs via zone firewall\n- Use strong DB authentication\n- Consider VPN or Cloudflare Access instead\n\n### 6. RDP (Remote Desktop)\n\n**Requires IP firewall.**\n\n**Terraform:**\n```hcl\nresource \"cloudflare_spectrum_application\" \"rdp\" {\n  zone_id  = var.zone_id\n  protocol = \"tcp/3389\"\n\n  dns {\n    type = \"CNAME\"\n    name = \"rdp.example.com\"\n  }\n\n  origin_direct = [\"tcp://windows-server.internal:3389\"]\n  tls           = \"off\"       # RDP has own encryption\n  ip_firewall   = true        # REQUIRED\n}\n```\n\n**Security:** ALWAYS `ip_firewall: true`, whitelist admin IPs, RDP is DDoS/brute-force target\n\n### 7. Multi-Origin Failover\n\nHigh availability with load balancer.\n\n**Terraform:**\n```hcl\nresource \"cloudflare_load_balancer\" \"database_lb\" {\n  zone_id          = var.zone_id\n  name             = \"db-lb.example.com\"\n  default_pool_ids = [cloudflare_load_balancer_pool.db_primary.id]\n  fallback_pool_id = cloudflare_load_balancer_pool.db_secondary.id\n}\n\nresource \"cloudflare_load_balancer_pool\" \"db_primary\" {\n  name    = \"db-primary-pool\"\n  origins { name = \"db-1\"; address = \"192.0.2.1\" }\n  monitor = cloudflare_load_balancer_monitor.postgres_monitor.id\n}\n\nresource \"cloudflare_load_balancer_pool\" \"db_secondary\" {\n  name    = \"db-secondary-pool\"\n  origins { name = \"db-2\"; address = \"192.0.2.2\" }\n  monitor = cloudflare_load_balancer_monitor.postgres_monitor.id\n}\n\nresource \"cloudflare_load_balancer_monitor\" \"postgres_monitor\" {\n  type = \"tcp\"; port = 5432; interval = 30; timeout = 5\n}\n\nresource \"cloudflare_spectrum_application\" \"postgres_ha\" {\n  zone_id     = var.zone_id\n  protocol    = \"tcp/5432\"\n  dns         { type = \"CNAME\"; name = \"postgres.example.com\" }\n  origin_dns  { name = cloudflare_load_balancer.database_lb.name }\n  origin_port = 5432\n  tls         = \"strict\"\n  ip_firewall = true\n}\n```\n\n**Benefits:** Automatic failover, health monitoring, traffic distribution, zero-downtime deployments\n\n## See Also\n\n- [configuration.md](configuration.md) - Origin type setup\n- [gotchas.md](gotchas.md) - Protocol limitations\n- [api.md](api.md) - SDK reference\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/static-assets/README.md",
    "content": "# Cloudflare Static Assets Skill Reference\n\nExpert guidance for deploying and configuring static assets with Cloudflare Workers. This skill covers configuration patterns, routing architectures, asset binding usage, and best practices for SPAs, SSG sites, and full-stack applications.\n\n## Quick Start\n\n```jsonc\n// wrangler.jsonc\n{\n  \"name\": \"my-app\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\",\n  \"assets\": {\n    \"directory\": \"./dist\"\n  }\n}\n```\n\n```typescript\n// src/index.ts\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    return env.ASSETS.fetch(request);\n  }\n};\n```\n\nDeploy: `wrangler deploy`\n\n## When to Use Workers Static Assets vs Pages\n\n| Factor | Workers Static Assets | Cloudflare Pages |\n|--------|----------------------|------------------|\n| **Use case** | Hybrid apps (static + dynamic API) | Static sites, SSG |\n| **Worker control** | Full control over routing | Limited (Functions) |\n| **Configuration** | Code-first, flexible | Git-based, opinionated |\n| **Dynamic routing** | Worker-first patterns | Functions (_functions/) |\n| **Best for** | Full-stack apps, SPAs with APIs | Jamstack, static docs |\n\n**Decision tree:**\n\n- Need custom routing logic? → Workers Static Assets\n- Pure static site or SSG? → Pages\n- API routes + SPA? → Workers Static Assets\n- Framework (Next, Nuxt, Remix)? → Pages\n\n## Reading Order\n\n1. **configuration.md** - Setup, wrangler.jsonc options, routing patterns\n2. **api.md** - ASSETS binding API, request/response handling\n3. **patterns.md** - Common patterns (SPA, API routes, auth, A/B testing)\n4. **gotchas.md** - Limits, errors, performance tips\n\n## In This Reference\n\n- **[configuration.md](configuration.md)** - Setup, deployment, configuration\n- **[api.md](api.md)** - API endpoints, methods, interfaces\n- **[patterns.md](patterns.md)** - Common patterns, use cases, examples\n- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations\n\n## See Also\n\n- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)\n- [Static Assets Docs](https://developers.cloudflare.com/workers/static-assets/)\n- [Cloudflare Pages](https://developers.cloudflare.com/pages/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/static-assets/api.md",
    "content": "# API Reference\n\n## ASSETS Binding\n\nThe `ASSETS` binding provides access to static assets via the `Fetcher` interface.\n\n### Type Definition\n\n```typescript\ninterface Env {\n  ASSETS: Fetcher;\n}\n\ninterface Fetcher {\n  fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;\n}\n```\n\n### Method Signatures\n\n```typescript\n// 1. Forward entire request\nawait env.ASSETS.fetch(request);\n\n// 2. String path (hostname ignored, only path matters)\nawait env.ASSETS.fetch(\"https://any-host/path/to/asset.png\");\n\n// 3. URL object\nawait env.ASSETS.fetch(new URL(\"/index.html\", request.url));\n\n// 4. Constructed Request object\nawait env.ASSETS.fetch(new Request(new URL(\"/logo.png\", request.url), {\n  method: \"GET\",\n  headers: request.headers\n}));\n```\n\n**Key behaviors:**\n\n- Host/origin is ignored for string/URL inputs (only path is used)\n- Method must be GET (others return 405)\n- Request headers pass through (affects response)\n- Returns standard `Response` object\n\n## Request Handling\n\n### Path Resolution\n\n```typescript\n// All resolve to same asset:\nenv.ASSETS.fetch(\"https://example.com/logo.png\")\nenv.ASSETS.fetch(\"https://ignored.host/logo.png\")\nenv.ASSETS.fetch(\"/logo.png\")\n```\n\nAssets are resolved relative to configured `assets.directory`.\n\n### Headers\n\nRequest headers that affect response:\n\n| Header | Effect |\n|--------|--------|\n| `Accept-Encoding` | Controls compression (gzip, brotli) |\n| `Range` | Enables partial content (206 responses) |\n| `If-None-Match` | Conditional request via ETag |\n| `If-Modified-Since` | Conditional request via modification date |\n\nCustom headers pass through but don't affect asset serving.\n\n### Method Support\n\n| Method | Supported | Response |\n|--------|-----------|----------|\n| `GET` | ✅ Yes | Asset content |\n| `HEAD` | ✅ Yes | Headers only, no body |\n| `POST`, `PUT`, etc. | ❌ No | 405 Method Not Allowed |\n\n## Response Behavior\n\n### Content-Type Inference\n\nAutomatically set based on file extension:\n\n| Extension | Content-Type |\n|-----------|--------------|\n| `.html` | `text/html; charset=utf-8` |\n| `.css` | `text/css` |\n| `.js` | `application/javascript` |\n| `.json` | `application/json` |\n| `.png` | `image/png` |\n| `.jpg`, `.jpeg` | `image/jpeg` |\n| `.svg` | `image/svg+xml` |\n| `.woff2` | `font/woff2` |\n\n### Default Headers\n\nResponses include:\n\n```\nContent-Type: <inferred>\nETag: \"<hash>\"\nCache-Control: public, max-age=3600\nContent-Encoding: br  (if supported and beneficial)\n```\n\n**Cache-Control defaults:**\n\n- 1 hour (`max-age=3600`) for most assets\n- Override via Worker response transformation (see patterns.md:27-35)\n\n### Compression\n\nAutomatic compression based on `Accept-Encoding`:\n\n- **Brotli** (`br`): Preferred, best compression\n- **Gzip** (`gzip`): Fallback\n- **None**: If client doesn't support or asset too small\n\n### ETag Generation\n\nETags are content-based hashes:\n\n```\nETag: \"a3b2c1d4e5f6...\"\n```\n\nUsed for conditional requests (`If-None-Match`). Returns `304 Not Modified` if match.\n\n## Error Responses\n\n| Status | Condition | Behavior |\n|--------|-----------|----------|\n| `404` | Asset not found | Body depends on `not_found_handling` config |\n| `405` | Non-GET/HEAD method | `{ \"error\": \"Method not allowed\" }` |\n| `416` | Invalid Range header | Range not satisfiable |\n\n### 404 Handling\n\nDepends on configuration (see configuration.md:45-52):\n\n```typescript\n// not_found_handling: \"single-page-application\"\n// Returns /index.html with 200 status\n\n// not_found_handling: \"404-page\"\n// Returns /404.html if exists, else 404 response\n\n// not_found_handling: \"none\"\n// Returns 404 response\n```\n\n## Advanced Usage\n\n### Modifying Responses\n\n```typescript\nconst response = await env.ASSETS.fetch(request);\n\n// Clone and modify\nreturn new Response(response.body, {\n  status: response.status,\n  headers: {\n    ...Object.fromEntries(response.headers),\n    'Cache-Control': 'public, max-age=31536000',\n    'X-Custom': 'value'\n  }\n});\n```\n\nSee patterns.md:27-35 for full example.\n\n### Error Handling\n\n```typescript\nconst response = await env.ASSETS.fetch(request);\n\nif (!response.ok) {\n  // Asset not found or error\n  return new Response('Custom error page', { status: 404 });\n}\n\nreturn response;\n```\n\n### Conditional Serving\n\n```typescript\nconst url = new URL(request.url);\n\n// Serve different assets based on conditions\nif (url.pathname === '/') {\n  return env.ASSETS.fetch('/index.html');\n}\n\nreturn env.ASSETS.fetch(request);\n```\n\nSee patterns.md for complete patterns.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/static-assets/configuration.md",
    "content": "## Configuration\n\n### Basic Setup\n\nMinimal configuration requires only `assets.directory`:\n\n```jsonc\n{\n  \"name\": \"my-worker\",\n  \"compatibility_date\": \"2025-01-01\",  // Use current date for new projects\n  \"assets\": {\n    \"directory\": \"./dist\"\n  }\n}\n```\n\n### Full Configuration Options\n\n```jsonc\n{\n  \"name\": \"my-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\",\n  \"assets\": {\n    \"directory\": \"./dist\",\n    \"binding\": \"ASSETS\",\n    \"not_found_handling\": \"single-page-application\",\n    \"html_handling\": \"auto-trailing-slash\",\n    \"run_worker_first\": [\"/api/*\", \"!/api/docs/*\"]\n  }\n}\n```\n\n**Configuration keys:**\n\n- `directory` (string, required): Path to assets folder (e.g. `./dist`, `./public`, `./build`)\n- `binding` (string, optional): Name to access assets in Worker code (e.g. `env.ASSETS`). Default: `\"ASSETS\"`\n- `not_found_handling` (string, optional): Behavior when asset not found\n  - `\"single-page-application\"`: Serve `/index.html` for non-asset paths (default for SPAs)\n  - `\"404-page\"`: Serve `/404.html` if present, otherwise 404\n  - `\"none\"`: Return 404 for missing assets\n- `html_handling` (string, optional): URL trailing slash behavior\n- `run_worker_first` (boolean | string[], optional): Routes that invoke Worker before checking assets\n\n### not_found_handling Modes\n\n| Mode | Behavior | Use Case |\n|------|----------|----------|\n| `\"single-page-application\"` | Serve `/index.html` for non-asset requests | React, Vue, Angular SPAs |\n| `\"404-page\"` | Serve `/404.html` if exists, else 404 | Static sites with custom error page |\n| `\"none\"` | Return 404 for missing assets | API-first or custom routing |\n\n### html_handling Modes\n\nControls trailing slash behavior for HTML files:\n\n| Mode | `/page` | `/page/` | Use Case |\n|------|---------|----------|----------|\n| `\"auto-trailing-slash\"` | Redirect to `/page/` if `/page/index.html` exists | Serve `/page/index.html` | Default, SEO-friendly |\n| `\"force-trailing-slash\"` | Always redirect to `/page/` | Serve if exists | Consistent trailing slashes |\n| `\"drop-trailing-slash\"` | Serve if exists | Redirect to `/page` | Cleaner URLs |\n| `\"none\"` | No modification | No modification | Custom routing logic |\n\n**Default:** `\"auto-trailing-slash\"`\n\n### run_worker_first Configuration\n\nControls which requests invoke Worker before checking assets.\n\n**Boolean syntax:**\n\n```jsonc\n{\n  \"assets\": {\n    \"run_worker_first\": true  // ALL requests invoke Worker\n  }\n}\n```\n\n**Array syntax (recommended):**\n\n```jsonc\n{\n  \"assets\": {\n    \"run_worker_first\": [\n      \"/api/*\",           // Positive pattern: match API routes\n      \"/admin/*\",         // Match admin routes\n      \"!/admin/assets/*\"  // Negative pattern: exclude admin assets\n    ]\n  }\n}\n```\n\n**Pattern rules:**\n\n- Glob patterns: `*` (any chars), `**` (any path segments)\n- Negative patterns: Prefix with `!` to exclude\n- Precedence: Negative patterns override positive patterns\n- Default: `false` (assets served directly)\n\n**Decision guidance:**\n\n- Use `true` for API-first apps (few static assets)\n- Use array patterns for hybrid apps (APIs + static assets)\n- Use `false` for static-first sites (minimal dynamic routes)\n\n### .assetsignore File\n\nExclude files from upload using `.assetsignore` (same syntax as `.gitignore`):\n\n```\n# .assetsignore\n_worker.js\n*.map\n*.md\nnode_modules/\n.git/\n```\n\n**Common patterns:**\n\n- `_worker.js` - Exclude Worker code from assets\n- `*.map` - Exclude source maps\n- `*.md` - Exclude markdown files\n- Development artifacts\n\n### Vite Plugin Integration\n\nFor Vite-based projects, use `@cloudflare/vite-plugin`:\n\n```typescript\n// vite.config.ts\nimport { defineConfig } from 'vite';\nimport { cloudflare } from '@cloudflare/vite-plugin';\n\nexport default defineConfig({\n  plugins: [\n    cloudflare({\n      assets: {\n        directory: './dist',\n        binding: 'ASSETS'\n      }\n    })\n  ]\n});\n```\n\n**Features:**\n\n- Automatic asset detection during dev\n- Hot module replacement for assets\n- Production build integration\n- Requires: Wrangler 4.0.0+, `@cloudflare/vite-plugin` 1.0.0+\n\n### Key Compatibility Dates\n\n| Date | Feature | Impact |\n|------|---------|--------|\n| `2025-04-01` | Navigation request optimization | SPAs skip Worker for navigation, reducing costs |\n\nUse current date for new projects. See [Compatibility Dates](https://developers.cloudflare.com/workers/configuration/compatibility-dates/) for full list.\n\n### Environment-Specific Configuration\n\nUse `wrangler.jsonc` environments for different configs:\n\n```jsonc\n{\n  \"name\": \"my-worker\",\n  \"assets\": { \"directory\": \"./dist\" },\n  \"env\": {\n    \"staging\": {\n      \"assets\": {\n        \"not_found_handling\": \"404-page\"\n      }\n    },\n    \"production\": {\n      \"assets\": {\n        \"not_found_handling\": \"single-page-application\"\n      }\n    }\n  }\n}\n```\n\nDeploy with: `wrangler deploy --env staging`\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/static-assets/gotchas.md",
    "content": "## Best Practices\n\n### 1. Use Selective Worker-First Routing\n\nInstead of `run_worker_first = true`, use array patterns:\n\n```jsonc\n{\n  \"assets\": {\n    \"run_worker_first\": [\n      \"/api/*\",           // API routes\n      \"/admin/*\",         // Admin area\n      \"!/admin/assets/*\"  // Except admin assets\n    ]\n  }\n}\n```\n\n**Benefits:**\n- Reduces Worker invocations\n- Lowers costs\n- Improves asset delivery performance\n\n### 2. Leverage Navigation Request Optimization\n\nFor SPAs, use `compatibility_date = \"2025-04-01\"` or later:\n\n```jsonc\n{\n  \"compatibility_date\": \"2025-04-01\",\n  \"assets\": {\n    \"not_found_handling\": \"single-page-application\"\n  }\n}\n```\n\nNavigation requests skip Worker invocation, reducing costs.\n\n### 3. Type Safety with Bindings\n\nAlways type your environment:\n\n```typescript\ninterface Env {\n  ASSETS: Fetcher;\n}\n```\n\n## Common Errors\n\n### \"Asset not found\"\n\n**Cause:** Asset not in assets directory, wrong path, or assets not deployed  \n**Solution:** Verify asset exists, check path case-sensitivity, redeploy if needed\n\n### \"Worker not invoked for asset\"\n\n**Cause:** Asset served directly, `run_worker_first` not configured  \n**Solution:** Configure `run_worker_first` patterns to include asset routes (see configuration.md:66-106)\n\n### \"429 Too Many Requests on free tier\"\n\n**Cause:** `run_worker_first` patterns invoke Worker for many requests, hitting free tier limits (100k req/day)  \n**Solution:** Use more selective patterns with negative exclusions, or upgrade to paid plan\n\n### \"Smart Placement increases latency\"\n\n**Cause:** `run_worker_first=true` + Smart Placement routes all requests through single smart-placed location  \n**Solution:** Use selective patterns (array syntax) or disable Smart Placement for asset-heavy apps\n\n### \"CF-Cache-Status header unreliable\"\n\n**Cause:** Header is probabilistically added for privacy reasons  \n**Solution:** Don't rely on `CF-Cache-Status` for critical routing logic. Use other signals (ETag, age).\n\n### \"JWT expired during deployment\"\n\n**Cause:** Large asset deployments exceed JWT token lifetime  \n**Solution:** Update to Wrangler 4.34.0+ (automatic token refresh), or reduce asset count\n\n### \"Cannot use 'assets' with 'site'\"\n\n**Cause:** Legacy `site` config conflicts with new `assets` config  \n**Solution:** Migrate from `site` to `assets` (see configuration.md). Remove `site` key from wrangler.jsonc.\n\n### \"Assets not updating after deployment\"\n\n**Cause:** Browser or CDN cache serving old assets  \n**Solution:** \n- Hard refresh browser (Cmd+Shift+R / Ctrl+F5)\n- Use cache-busting (hashed filenames)\n- Verify deployment completed: `wrangler tail`\n\n## Limits\n\n| Resource/Limit | Free | Paid | Notes |\n|----------------|------|------|-------|\n| Max asset size | 25 MiB | 25 MiB | Per file |\n| Total assets | 20,000 | **100,000** | Requires Wrangler 4.34.0+ (Sep 2025) |\n| Worker invocations | 100k/day | 10M/month | Optimize with `run_worker_first` patterns |\n| Asset storage | Unlimited | Unlimited | Included |\n\n### Version Requirements\n\n| Feature | Minimum Wrangler Version |\n|---------|--------------------------|\n| 100k file limit (paid) | 4.34.0 |\n| Vite plugin | 4.0.0 + @cloudflare/vite-plugin 1.0.0 |\n| Navigation optimization | 4.0.0 + compatibility_date: \"2025-04-01\" |\n\n## Performance Tips\n\n### 1. Use Hashed Filenames\n\nEnable long-term caching with content-hashed filenames:\n\n```\napp.a3b2c1d4.js\nstyles.e5f6g7h8.css\n```\n\nMost bundlers (Vite, Webpack, Parcel) do this automatically.\n\n### 2. Minimize Worker Invocations\n\nServe assets directly when possible:\n\n```jsonc\n{\n  \"assets\": {\n    // Only invoke Worker for dynamic routes\n    \"run_worker_first\": [\"/api/*\", \"/auth/*\"]\n  }\n}\n```\n\n### 3. Leverage Browser Cache\n\nSet appropriate `Cache-Control` headers:\n\n```typescript\n// Versioned assets\n'Cache-Control': 'public, max-age=31536000, immutable'\n\n// HTML (revalidate often)\n'Cache-Control': 'public, max-age=0, must-revalidate'\n```\n\nSee patterns.md:169-189 for implementation.\n\n### 4. Use .assetsignore\n\nReduce upload time by excluding unnecessary files:\n\n```\n*.map\n*.md\n.DS_Store\nnode_modules/\n```\n\nSee configuration.md:107-126 for details.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/static-assets/patterns.md",
    "content": "### Common Patterns\n\n**1. Forward request to assets:**\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    return env.ASSETS.fetch(request);\n  }\n};\n```\n\n**2. Fetch specific asset by path:**\n\n```typescript\nconst response = await env.ASSETS.fetch(\"https://assets.local/logo.png\");\n```\n\n**3. Modify request before fetching asset:**\n\n```typescript\nconst url = new URL(request.url);\nurl.pathname = \"/index.html\";\nreturn env.ASSETS.fetch(new Request(url, request));\n```\n\n**4. Transform asset response:**\n\n```typescript\nconst response = await env.ASSETS.fetch(request);\nconst modifiedResponse = new Response(response.body, response);\nmodifiedResponse.headers.set(\"X-Custom-Header\", \"value\");\nmodifiedResponse.headers.set(\"Cache-Control\", \"public, max-age=3600\");\nreturn modifiedResponse;\n```\n\n**5. Conditional asset serving:**\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const url = new URL(request.url);\n    if (url.pathname === '/') {\n      return env.ASSETS.fetch('/index.html');\n    }\n    return env.ASSETS.fetch(request);\n  }\n};\n```\n\n**6. SPA with API routes:**\n\nMost common full-stack pattern - static SPA with backend API:\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const url = new URL(request.url);\n    if (url.pathname.startsWith('/api/')) {\n      return handleAPI(request, env);\n    }\n    return env.ASSETS.fetch(request);\n  }\n};\n\nasync function handleAPI(request: Request, env: Env): Promise<Response> {\n  return new Response(JSON.stringify({ status: 'ok' }), {\n    headers: { 'Content-Type': 'application/json' }\n  });\n}\n```\n\n**Config:** Set `run_worker_first: [\"/api/*\"]` (see configuration.md:66-106)\n\n**7. Auth gating for protected assets:**\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const url = new URL(request.url);\n    if (url.pathname.startsWith('/admin/')) {\n      const session = await validateSession(request, env);\n      if (!session) {\n        return Response.redirect('/login', 302);\n      }\n    }\n    return env.ASSETS.fetch(request);\n  }\n};\n```\n\n**Config:** Set `run_worker_first: [\"/admin/*\"]`\n\n**8. Custom headers for security:**\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const response = await env.ASSETS.fetch(request);\n    const secureResponse = new Response(response.body, response);\n    secureResponse.headers.set('X-Frame-Options', 'DENY');\n    secureResponse.headers.set('X-Content-Type-Options', 'nosniff');\n    secureResponse.headers.set('Content-Security-Policy', \"default-src 'self'\");\n    return secureResponse;\n  }\n};\n```\n\n**9. A/B testing via cookies:**\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const cookies = request.headers.get('Cookie') || '';\n    const variant = cookies.includes('variant=b') ? 'b' : 'a';\n    const url = new URL(request.url);\n    if (url.pathname === '/') {\n      return env.ASSETS.fetch(`/index-${variant}.html`);\n    }\n    return env.ASSETS.fetch(request);\n  }\n};\n```\n\n**10. Locale-based routing:**\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const locale = request.headers.get('Accept-Language')?.split(',')[0] || 'en';\n    const url = new URL(request.url);\n    if (url.pathname === '/') {\n      return env.ASSETS.fetch(`/${locale}/index.html`);\n    }\n    if (!url.pathname.startsWith(`/${locale}/`)) {\n      url.pathname = `/${locale}${url.pathname}`;\n    }\n    return env.ASSETS.fetch(url);\n  }\n};\n```\n\n**11. OAuth callback handling:**\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const url = new URL(request.url);\n    if (url.pathname === '/auth/callback') {\n      const code = url.searchParams.get('code');\n      if (code) {\n        const session = await exchangeCode(code, env);\n        return new Response(null, {\n          status: 302,\n          headers: {\n            'Location': '/',\n            'Set-Cookie': `session=${session}; HttpOnly; Secure; SameSite=Lax`\n          }\n        });\n      }\n    }\n    return env.ASSETS.fetch(request);\n  }\n};\n```\n\n**Config:** Set `run_worker_first: [\"/auth/*\"]`\n\n**12. Cache control override:**\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const response = await env.ASSETS.fetch(request);\n    const url = new URL(request.url);\n    // Immutable assets (hashed filenames)\n    if (/\\.[a-f0-9]{8,}\\.(js|css|png|jpg)$/.test(url.pathname)) {\n      return new Response(response.body, {\n        ...response,\n        headers: {\n          ...Object.fromEntries(response.headers),\n          'Cache-Control': 'public, max-age=31536000, immutable'\n        }\n      });\n    }\n    return response;\n  }\n};\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/stream/README.md",
    "content": "# Cloudflare Stream\n\nServerless live and on-demand video streaming platform with one API.\n\n## Overview\n\nCloudflare Stream provides video upload, storage, encoding, and delivery without managing infrastructure. Runs on Cloudflare's global network.\n\n### Key Features\n- **On-demand video**: Upload, encode, store, deliver\n- **Live streaming**: RTMPS/SRT ingestion with ABR\n- **Direct creator uploads**: End users upload without API keys\n- **Signed URLs**: Token-based access control\n- **Analytics**: Server-side metrics via GraphQL\n- **Webhooks**: Processing notifications\n- **Captions**: Upload or AI-generate subtitles\n- **Watermarks**: Apply branding to videos\n- **Downloads**: Enable MP4 offline viewing\n\n## Core Concepts\n\n### Video Upload Methods\n1. **API Upload (TUS protocol)**: Direct server upload\n2. **Upload from URL**: Import from external source\n3. **Direct Creator Uploads**: User-generated content (recommended)\n\n### Playback Options\n1. **Stream Player (iframe)**: Built-in, optimized player\n2. **Custom Player (HLS/DASH)**: Video.js, HLS.js integration\n3. **Thumbnails**: Static or animated previews\n\n### Access Control\n- **Public**: No restrictions\n- **requireSignedURLs**: Token-based access\n- **allowedOrigins**: Domain restrictions\n- **Access Rules**: Geo/IP restrictions in tokens\n\n### Live Streaming\n- RTMPS/SRT ingest from OBS, FFmpeg\n- Automatic recording to on-demand\n- Simulcast to YouTube, Twitch, etc.\n- WebRTC support for browser streaming\n\n## Quick Start\n\n**Upload video via API**\n```bash\ncurl -X POST \\\n  \"https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/copy\" \\\n  -H \"Authorization: Bearer <TOKEN>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"https://example.com/video.mp4\"}'\n```\n\n**Embed player**\n```html\n<iframe\n  src=\"https://customer-<CODE>.cloudflarestream.com/<VIDEO_ID>/iframe\"\n  style=\"border: none;\"\n  height=\"720\" width=\"1280\"\n  allow=\"accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;\"\n  allowfullscreen=\"true\"\n></iframe>\n```\n\n**Create live input**\n```bash\ncurl -X POST \\\n  \"https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/live_inputs\" \\\n  -H \"Authorization: Bearer <TOKEN>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"recording\": {\"mode\": \"automatic\"}}'\n```\n\n## Limits\n\n- Max file size: 30 GB\n- Max frame rate: 60 fps (recommended)\n- Supported formats: MP4, MKV, MOV, AVI, FLV, MPEG-2 TS/PS, MXF, LXF, GXF, 3GP, WebM, MPG, QuickTime\n\n## Pricing\n\n- $5/1000 min stored\n- $1/1000 min delivered\n\n## Resources\n\n- Dashboard: https://dash.cloudflare.com/?to=/:account/stream\n- API Docs: https://developers.cloudflare.com/api/resources/stream/\n- Stream Docs: https://developers.cloudflare.com/stream/\n\n## Reading Order\n\n| Order | File | Purpose | When to Use |\n|-------|------|---------|-------------|\n| 1 | [configuration.md](./configuration.md) | Setup SDKs, env vars, signing keys | Starting new project |\n| 2 | [api.md](./api.md) | On-demand video APIs | Implementing uploads/playback |\n| 3 | [api-live.md](./api-live.md) | Live streaming APIs | Building live streaming |\n| 4 | [patterns.md](./patterns.md) | Full-stack flows, TUS, JWT signing | Implementing workflows |\n| 5 | [gotchas.md](./gotchas.md) | Errors, limits, troubleshooting | Debugging issues |\n\n## In This Reference\n\n- [configuration.md](./configuration.md) - Setup, environment variables, wrangler config\n- [api.md](./api.md) - On-demand video upload, playback, management APIs\n- [api-live.md](./api-live.md) - Live streaming (RTMPS/SRT/WebRTC), simulcast\n- [patterns.md](./patterns.md) - Full-stack flows, state management, best practices\n- [gotchas.md](./gotchas.md) - Error codes, troubleshooting, limits\n\n## See Also\n\n- [workers](../workers/) - Deploy Stream APIs in Workers\n- [pages](../pages/) - Integrate Stream with Pages\n- [workers-ai](../workers-ai/) - AI-generate captions\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/stream/api-live.md",
    "content": "# Stream Live Streaming API\n\nLive input creation, status checking, simulcast, and WebRTC streaming.\n\n## Create Live Input\n\n### Using Cloudflare SDK\n\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst client = new Cloudflare({ apiToken: env.CF_API_TOKEN });\n\nconst liveInput = await client.stream.liveInputs.create({\n  account_id: env.CF_ACCOUNT_ID,\n  recording: { mode: 'automatic', timeoutSeconds: 30 },\n  deleteRecordingAfterDays: 30\n});\n\n// Returns: { uid, rtmps, srt, webRTC }\n```\n\n### Raw fetch API\n\n```typescript\nasync function createLiveInput(accountId: string, apiToken: string) {\n  const response = await fetch(\n    `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/live_inputs`,\n    {\n      method: 'POST',\n      headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        recording: { mode: 'automatic', timeoutSeconds: 30 },\n        deleteRecordingAfterDays: 30\n      })\n    }\n  );\n  const { result } = await response.json();\n  return {\n    uid: result.uid,\n    rtmps: { url: result.rtmps.url, streamKey: result.rtmps.streamKey },\n    srt: { url: result.srt.url, streamId: result.srt.streamId, passphrase: result.srt.passphrase },\n    webRTC: result.webRTC\n  };\n}\n```\n\n## Check Live Status\n\n```typescript\nasync function getLiveStatus(accountId: string, liveInputId: string, apiToken: string) {\n  const response = await fetch(\n    `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/live_inputs/${liveInputId}`,\n    { headers: { 'Authorization': `Bearer ${apiToken}` } }\n  );\n  const { result } = await response.json();\n  return {\n    isLive: result.status?.current?.state === 'connected',\n    recording: result.recording,\n    status: result.status\n  };\n}\n```\n\n## Simulcast (Live Outputs)\n\n### Create Output\n\n```typescript\nasync function createLiveOutput(\n  accountId: string, liveInputId: string, apiToken: string,\n  outputUrl: string, streamKey: string\n) {\n  return fetch(\n    `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/live_inputs/${liveInputId}/outputs`,\n    {\n      method: 'POST',\n      headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        url: `${outputUrl}/${streamKey}`,\n        enabled: true,\n        streamKey // For platforms like YouTube, Twitch\n      })\n    }\n  ).then(r => r.json());\n}\n```\n\n### Example: Simulcast to YouTube + Twitch\n\n```typescript\nconst liveInput = await createLiveInput(accountId, apiToken);\n\n// Add YouTube output\nawait createLiveOutput(\n  accountId, liveInput.uid, apiToken,\n  'rtmp://a.rtmp.youtube.com/live2',\n  'your-youtube-stream-key'\n);\n\n// Add Twitch output\nawait createLiveOutput(\n  accountId, liveInput.uid, apiToken,\n  'rtmp://live.twitch.tv/app',\n  'your-twitch-stream-key'\n);\n```\n\n## WebRTC Streaming (WHIP/WHEP)\n\n### Browser to Stream (WHIP)\n\n```typescript\nasync function startWebRTCBroadcast(liveInputId: string) {\n  const pc = new RTCPeerConnection();\n  \n  // Add local media tracks\n  const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });\n  stream.getTracks().forEach(track => pc.addTrack(track, stream));\n  \n  // Create offer\n  const offer = await pc.createOffer();\n  await pc.setLocalDescription(offer);\n  \n  // Send to Stream via WHIP\n  const response = await fetch(\n    `https://customer-<CODE>.cloudflarestream.com/${liveInputId}/webRTC/publish`,\n    {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/sdp' },\n      body: offer.sdp\n    }\n  );\n  \n  const answer = await response.text();\n  await pc.setRemoteDescription({ type: 'answer', sdp: answer });\n}\n```\n\n### Stream to Browser (WHEP)\n\n```typescript\nasync function playWebRTCStream(videoId: string) {\n  const pc = new RTCPeerConnection();\n  \n  pc.addTransceiver('video', { direction: 'recvonly' });\n  pc.addTransceiver('audio', { direction: 'recvonly' });\n  \n  const offer = await pc.createOffer();\n  await pc.setLocalDescription(offer);\n  \n  const response = await fetch(\n    `https://customer-<CODE>.cloudflarestream.com/${videoId}/webRTC/play`,\n    {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/sdp' },\n      body: offer.sdp\n    }\n  );\n  \n  const answer = await response.text();\n  await pc.setRemoteDescription({ type: 'answer', sdp: answer });\n  \n  return pc;\n}\n```\n\n## Recording Settings\n\n| Mode | Behavior |\n|------|----------|\n| `automatic` | Record all live streams |\n| `off` | No recording |\n| `timeoutSeconds` | Stop recording after N seconds of inactivity |\n\n```typescript\nconst recordingConfig = {\n  mode: 'automatic',\n  timeoutSeconds: 30, // Auto-stop 30s after stream ends\n  requireSignedURLs: true, // Require token for VOD playback\n  allowedOrigins: ['https://yourdomain.com']\n};\n```\n\n## In This Reference\n\n- [README.md](./README.md) - Overview and quick start\n- [api.md](./api.md) - On-demand video APIs\n- [configuration.md](./configuration.md) - Setup and config\n- [patterns.md](./patterns.md) - Full-stack flows, best practices\n- [gotchas.md](./gotchas.md) - Error codes, troubleshooting\n\n## See Also\n\n- [workers](../workers/) - Deploy live APIs in Workers\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/stream/api.md",
    "content": "# Stream API Reference\n\nUpload, playback, live streaming, and management APIs.\n\n## Upload APIs\n\n### Direct Creator Upload (Recommended)\n\n**Backend: Create upload URL (SDK)**\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst client = new Cloudflare({ apiToken: env.CF_API_TOKEN });\n\nconst uploadData = await client.stream.directUpload.create({\n  account_id: env.CF_ACCOUNT_ID,\n  maxDurationSeconds: 3600,\n  requireSignedURLs: true,\n  meta: { creator: 'user-123' }\n});\n// Returns: { uploadURL: string, uid: string }\n```\n\n**Frontend: Upload file**\n```typescript\nasync function uploadVideo(file: File, uploadURL: string) {\n  const formData = new FormData();\n  formData.append('file', file);\n  return fetch(uploadURL, { method: 'POST', body: formData }).then(r => r.json());\n}\n```\n\n### Upload from URL\n\n```typescript\nconst video = await client.stream.copy.create({\n  account_id: env.CF_ACCOUNT_ID,\n  url: 'https://example.com/video.mp4',\n  meta: { name: 'My Video' },\n  requireSignedURLs: false\n});\n```\n\n## Playback APIs\n\n### Embed Player (iframe)\n\n```html\n<iframe\n  src=\"https://customer-<CODE>.cloudflarestream.com/<VIDEO_ID>/iframe?autoplay=true&muted=true\"\n  style=\"border: none;\" height=\"720\" width=\"1280\"\n  allow=\"accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;\"\n  allowfullscreen=\"true\"\n></iframe>\n```\n\n### HLS/DASH Manifest URLs\n\n```typescript\n// HLS\nconst hlsUrl = `https://customer-<CODE>.cloudflarestream.com/${videoId}/manifest/video.m3u8`;\n\n// DASH\nconst dashUrl = `https://customer-<CODE>.cloudflarestream.com/${videoId}/manifest/video.mpd`;\n```\n\n### Thumbnails\n\n```typescript\n// At specific time (seconds)\nconst thumb = `https://customer-<CODE>.cloudflarestream.com/${videoId}/thumbnails/thumbnail.jpg?time=10s`;\n\n// By percentage\nconst thumbPct = `https://customer-<CODE>.cloudflarestream.com/${videoId}/thumbnails/thumbnail.jpg?time=50%`;\n\n// Animated GIF\nconst gif = `https://customer-<CODE>.cloudflarestream.com/${videoId}/thumbnails/thumbnail.gif`;\n```\n\n## Signed URLs\n\n```typescript\n// Low volume (<1k/day): Use API\nasync function getSignedToken(accountId: string, videoId: string, apiToken: string) {\n  const response = await fetch(\n    `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/${videoId}/token`,\n    {\n      method: 'POST',\n      headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        exp: Math.floor(Date.now() / 1000) + 3600,\n        accessRules: [{ type: 'ip.geoip.country', action: 'allow', country: ['US'] }]\n      })\n    }\n  );\n  return (await response.json()).result.token;\n}\n\n// High volume: Self-sign with RS256 JWT (see \"Self-Sign JWT\" in patterns.md)\n```\n\n## Captions & Clips\n\n### Upload Captions\n\n```typescript\nasync function uploadCaption(\n  accountId: string, videoId: string, apiToken: string,\n  language: string, captionFile: File\n) {\n  const formData = new FormData();\n  formData.append('file', captionFile);\n  return fetch(\n    `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/${videoId}/captions/${language}`,\n    {\n      method: 'PUT',\n      headers: { 'Authorization': `Bearer ${apiToken}` },\n      body: formData\n    }\n  ).then(r => r.json());\n}\n```\n\n### Generate AI Captions\n\n```typescript\n// TODO: Requires Workers AI integration - see workers-ai reference\nasync function generateAICaptions(accountId: string, videoId: string, apiToken: string) {\n  return fetch(\n    `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/${videoId}/captions/generate`,\n    {\n      method: 'POST',\n      headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },\n      body: JSON.stringify({ language: 'en' })\n    }\n  ).then(r => r.json());\n}\n```\n\n### Clip Video\n\n```typescript\nasync function clipVideo(\n  accountId: string, videoId: string, apiToken: string,\n  startTime: number, endTime: number\n) {\n  return fetch(\n    `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/clip`,\n    {\n      method: 'POST',\n      headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        clippedFromVideoUID: videoId,\n        startTimeSeconds: startTime,\n        endTimeSeconds: endTime\n      })\n    }\n  ).then(r => r.json());\n}\n```\n\n## Video Management\n\n```typescript\n// List videos\nconst videos = await client.stream.videos.list({\n  account_id: env.CF_ACCOUNT_ID,\n  search: 'keyword' // optional\n});\n\n// Get video details\nconst video = await client.stream.videos.get(videoId, {\n  account_id: env.CF_ACCOUNT_ID\n});\n\n// Update video\nawait client.stream.videos.update(videoId, {\n  account_id: env.CF_ACCOUNT_ID,\n  meta: { title: 'New Title' },\n  requireSignedURLs: true\n});\n\n// Delete video\nawait client.stream.videos.delete(videoId, {\n  account_id: env.CF_ACCOUNT_ID\n});\n```\n\n## In This Reference\n\n- [README.md](./README.md) - Overview and quick start\n- [configuration.md](./configuration.md) - Setup and config\n- [api-live.md](./api-live.md) - Live streaming APIs (RTMPS/SRT/WebRTC)\n- [patterns.md](./patterns.md) - Full-stack flows, best practices\n- [gotchas.md](./gotchas.md) - Error codes, troubleshooting\n\n## See Also\n\n- [workers](../workers/) - Deploy Stream APIs in Workers\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/stream/configuration.md",
    "content": "# Stream Configuration\n\nSetup, environment variables, and wrangler configuration.\n\n## Installation\n\n```bash\n# Official Cloudflare SDK (Node.js, Workers, Pages)\nnpm install cloudflare\n\n# React component library\nnpm install @cloudflare/stream-react\n\n# TUS resumable uploads (large files)\nnpm install tus-js-client\n```\n\n## Environment Variables\n\n```bash\n# Required\nCF_ACCOUNT_ID=your-account-id\nCF_API_TOKEN=your-api-token\n\n# For signed URLs (high volume)\nSTREAM_KEY_ID=your-key-id\nSTREAM_JWK=base64-encoded-jwk\n\n# For webhooks\nWEBHOOK_SECRET=your-webhook-secret\n\n# Customer subdomain (from dashboard)\nSTREAM_CUSTOMER_CODE=your-customer-code\n```\n\n## Wrangler Configuration\n\n```jsonc\n{\n  \"name\": \"stream-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\", // Use current date for new projects\n  \"vars\": {\n    \"CF_ACCOUNT_ID\": \"your-account-id\"\n  }\n  // Store secrets: wrangler secret put CF_API_TOKEN\n  // wrangler secret put STREAM_KEY_ID\n  // wrangler secret put STREAM_JWK\n  // wrangler secret put WEBHOOK_SECRET\n}\n```\n\n## Signing Keys (High Volume)\n\nCreate once for self-signing tokens (thousands of daily users).\n\n**Create key**\n```bash\ncurl -X POST \\\n  \"https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/keys\" \\\n  -H \"Authorization: Bearer <API_TOKEN>\"\n\n# Save `id` and `jwk` (base64) from response\n```\n\n**Store in secrets**\n```bash\nwrangler secret put STREAM_KEY_ID\nwrangler secret put STREAM_JWK\n```\n\n## Webhooks\n\n**Setup webhook URL**\n```bash\ncurl -X PUT \\\n  \"https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/webhook\" \\\n  -H \"Authorization: Bearer <API_TOKEN>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"notificationUrl\": \"https://your-worker.workers.dev/webhook\"}'\n\n# Save the returned `secret` for signature verification\n```\n\n**Store secret**\n```bash\nwrangler secret put WEBHOOK_SECRET\n```\n\n## Direct Upload / Live / Watermark Config\n\n```typescript\n// Direct upload\nconst uploadConfig = {\n  maxDurationSeconds: 3600,\n  expiry: new Date(Date.now() + 3600000).toISOString(),\n  requireSignedURLs: true,\n  allowedOrigins: ['https://yourdomain.com'],\n  meta: { creator: 'user-123' }\n};\n\n// Live input\nconst liveConfig = {\n  recording: { mode: 'automatic', timeoutSeconds: 30 },\n  deleteRecordingAfterDays: 30\n};\n\n// Watermark\nconst watermark = {\n  name: 'Logo', opacity: 0.7, padding: 20,\n  position: 'lowerRight', scale: 0.15\n};\n```\n\n## Access Rules & Player Config\n\n```typescript\n// Access rules: allow US/CA, block CN/RU, or IP allowlist\nconst geoRestrict = [\n  { type: 'ip.geoip.country', action: 'allow', country: ['US', 'CA'] },\n  { type: 'any', action: 'block' }\n];\n\n// Player params for iframe\nconst playerParams = new URLSearchParams({\n  autoplay: 'true', muted: 'true', preload: 'auto', defaultTextTrack: 'en'\n});\n```\n\n## In This Reference\n\n- [README.md](./README.md) - Overview and quick start\n- [api.md](./api.md) - On-demand video APIs\n- [api-live.md](./api-live.md) - Live streaming APIs\n- [patterns.md](./patterns.md) - Full-stack flows, best practices\n- [gotchas.md](./gotchas.md) - Error codes, troubleshooting\n\n## See Also\n\n- [wrangler](../wrangler/) - Wrangler CLI and configuration\n- [workers](../workers/) - Deploy Stream APIs in Workers\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/stream/gotchas.md",
    "content": "# Stream Gotchas\n\n## Common Errors\n\n### \"ERR_NON_VIDEO\"\n\n**Cause:** Uploaded file is not a valid video format\n**Solution:** Ensure file is in supported format (MP4, MKV, MOV, AVI, FLV, MPEG-2 TS/PS, MXF, LXF, GXF, 3GP, WebM, MPG, QuickTime)\n\n### \"ERR_DURATION_EXCEED_CONSTRAINT\"\n\n**Cause:** Video duration exceeds `maxDurationSeconds` constraint\n**Solution:** Increase `maxDurationSeconds` in direct upload config or trim video before upload\n\n### \"ERR_FETCH_ORIGIN_ERROR\"\n\n**Cause:** Failed to download video from URL (upload from URL)\n**Solution:** Ensure URL is publicly accessible, uses HTTPS, and video file is available\n\n### \"ERR_MALFORMED_VIDEO\"\n\n**Cause:** Video file is corrupted or improperly encoded\n**Solution:** Re-encode video using FFmpeg or check source file integrity\n\n### \"ERR_DURATION_TOO_SHORT\"\n\n**Cause:** Video must be at least 0.1 seconds long\n**Solution:** Ensure video has valid duration (not a single frame)\n\n## Troubleshooting\n\n### Video stuck in \"inprogress\" state\n- **Cause**: Processing large/complex video\n- **Solution**: Wait up to 5 minutes for processing; use webhooks instead of polling\n\n### Signed URL returns 403\n- **Cause**: Token expired or invalid signature\n- **Solution**: Check expiration timestamp, verify JWK is correct, ensure clock sync\n\n### Live stream not connecting\n- **Cause**: Invalid RTMPS URL or stream key\n- **Solution**: Use exact URL/key from API, ensure firewall allows outbound 443\n\n### Webhook signature verification fails\n- **Cause**: Incorrect secret or timestamp window\n- **Solution**: Use exact secret from webhook setup, allow 5-minute timestamp drift\n\n### Video uploads but isn't visible\n- **Cause**: `requireSignedURLs` enabled without providing token\n- **Solution**: Generate signed token or set `requireSignedURLs: false` for public videos\n\n### Player shows infinite loading\n- **Cause**: CORS issue with allowedOrigins\n- **Solution**: Add your domain to `allowedOrigins` array\n\n## Limits\n\n| Resource | Limit |\n|----------|-------|\n| Max file size | 30 GB |\n| Max frame rate | 60 fps (recommended) |\n| Max duration per direct upload | Configurable via `maxDurationSeconds` |\n| Token generation (API endpoint) | 1,000/day recommended (use signing keys for higher) |\n| Live input outputs (simulcast) | 5 per live input |\n| Webhook retry attempts | 5 (exponential backoff) |\n| Webhook timeout | 30 seconds |\n| Caption file size | 5 MB |\n| Watermark image size | 2 MB |\n| Metadata keys per video | Unlimited |\n| Search results per page | Max 1,000 |\n\n## Performance Issues\n\n### Upload is slow\n- **Cause**: Large file size or network constraints\n- **Solution**: Use TUS resumable upload, compress video before upload, check bandwidth\n\n### Playback buffering\n- **Cause**: Network congestion or low bandwidth\n- **Solution**: Use ABR (adaptive bitrate) with HLS/DASH, reduce max bitrate\n\n### High processing time\n- **Cause**: Complex video codec, high resolution\n- **Solution**: Pre-encode with H.264 (most efficient), reduce resolution\n\n## Type Safety\n\n```typescript\n// Error response type\ninterface StreamError {\n  success: false;\n  errors: Array<{\n    code: number;\n    message: string;\n  }>;\n}\n\n// Handle errors\nasync function uploadWithErrorHandling(url: string, file: File) {\n  const formData = new FormData();\n  formData.append('file', file);\n  const response = await fetch(url, { method: 'POST', body: formData });\n  const result = await response.json();\n  \n  if (!result.success) {\n    throw new Error(result.errors[0]?.message || 'Upload failed');\n  }\n  return result;\n}\n```\n\n## Security Gotchas\n\n1. **Never expose API token in frontend** - Use direct creator uploads\n2. **Always verify webhook signatures** - Prevent spoofed notifications\n3. **Set appropriate token expiration** - Short-lived for security\n4. **Use requireSignedURLs for private content** - Prevent unauthorized access\n5. **Whitelist allowedOrigins** - Prevent hotlinking/embedding on unauthorized sites\n\n## In This Reference\n\n- [README.md](./README.md) - Overview and quick start\n- [configuration.md](./configuration.md) - Setup and config\n- [api.md](./api.md) - On-demand video APIs\n- [api-live.md](./api-live.md) - Live streaming APIs\n- [patterns.md](./patterns.md) - Full-stack flows, best practices\n\n## See Also\n\n- [workers](../workers/) - Deploy Stream APIs securely\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/stream/patterns.md",
    "content": "# Stream Patterns\n\nCommon workflows, full-stack flows, and best practices.\n\n## React Stream Player\n\n`npm install @cloudflare/stream-react`\n\n```tsx\nimport { Stream } from '@cloudflare/stream-react';\n\nexport function VideoPlayer({ videoId, token }: { videoId: string; token?: string }) {\n  return <Stream controls src={token ? `${videoId}?token=${token}` : videoId} responsive />;\n}\n```\n\n## Full-Stack Upload Flow\n\n**Backend API (Workers/Pages)**\n```typescript\nimport Cloudflare from 'cloudflare';\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const { videoName } = await request.json();\n    const client = new Cloudflare({ apiToken: env.CF_API_TOKEN });\n    const { uploadURL, uid } = await client.stream.directUpload.create({\n      account_id: env.CF_ACCOUNT_ID,\n      maxDurationSeconds: 3600,\n      requireSignedURLs: true,\n      meta: { name: videoName }\n    });\n    return Response.json({ uploadURL, uid });\n  }\n};\n```\n\n**Frontend component**\n```tsx\nimport { useState } from 'react';\n\nexport function VideoUploader() {\n  const [uploading, setUploading] = useState(false);\n  const [progress, setProgress] = useState(0);\n  \n  async function handleUpload(file: File) {\n    setUploading(true);\n    const { uploadURL, uid } = await fetch('/api/upload-url', {\n      method: 'POST',\n      body: JSON.stringify({ videoName: file.name })\n    }).then(r => r.json());\n    \n    const xhr = new XMLHttpRequest();\n    xhr.upload.onprogress = (e) => setProgress((e.loaded / e.total) * 100);\n    xhr.onload = () => { setUploading(false); window.location.href = `/videos/${uid}`; };\n    xhr.open('POST', uploadURL);\n    const formData = new FormData();\n    formData.append('file', file);\n    xhr.send(formData);\n  }\n  \n  return (\n    <div>\n      <input type=\"file\" accept=\"video/*\" onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])} disabled={uploading} />\n      {uploading && <progress value={progress} max={100} />}\n    </div>\n  );\n}\n```\n\n## TUS Resumable Upload\n\nFor large files (>500MB). `npm install tus-js-client`\n\n```typescript\nimport * as tus from 'tus-js-client';\n\nasync function uploadWithTUS(file: File, uploadURL: string, onProgress?: (pct: number) => void) {\n  return new Promise<string>((resolve, reject) => {\n    const upload = new tus.Upload(file, {\n      endpoint: uploadURL,\n      retryDelays: [0, 3000, 5000, 10000, 20000],\n      chunkSize: 50 * 1024 * 1024,\n      metadata: { filename: file.name, filetype: file.type },\n      onError: reject,\n      onProgress: (up, total) => onProgress?.((up / total) * 100),\n      onSuccess: () => resolve(upload.url?.split('/').pop() || '')\n    });\n    upload.start();\n  });\n}\n```\n\n## Video State Polling\n\n```typescript\nasync function waitForVideoReady(client: Cloudflare, accountId: string, videoId: string) {\n  for (let i = 0; i < 60; i++) {\n    const video = await client.stream.videos.get(videoId, { account_id: accountId });\n    if (video.readyToStream || video.status.state === 'error') return video;\n    await new Promise(resolve => setTimeout(resolve, 5000));\n  }\n  throw new Error('Video processing timeout');\n}\n```\n\n## Webhook Handler\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const signature = request.headers.get('Webhook-Signature');\n    const body = await request.text();\n    if (!signature || !await verifyWebhook(signature, body, env.WEBHOOK_SECRET)) {\n      return new Response('Unauthorized', { status: 401 });\n    }\n    const payload = JSON.parse(body);\n    if (payload.readyToStream) console.log(`Video ${payload.uid} ready`);\n    return new Response('OK');\n  }\n};\n\nasync function verifyWebhook(sig: string, body: string, secret: string): Promise<boolean> {\n  const parts = Object.fromEntries(sig.split(',').map(p => p.split('=')));\n  const timestamp = parseInt(parts.time || '0', 10);\n  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;\n  \n  const key = await crypto.subtle.importKey(\n    'raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']\n  );\n  const computed = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${timestamp}.${body}`));\n  const hex = Array.from(new Uint8Array(computed), b => b.toString(16).padStart(2, '0')).join('');\n  return hex === parts.sig1;\n}\n```\n\n## Self-Sign JWT (High Volume Tokens)\n\nFor >1k tokens/day. Prerequisites: Create signing key (see configuration.md).\n\n```typescript\nasync function selfSignToken(keyId: string, jwkBase64: string, videoId: string, expiresIn = 3600) {\n  const key = await crypto.subtle.importKey(\n    'jwk', JSON.parse(atob(jwkBase64)), { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign']\n  );\n  const now = Math.floor(Date.now() / 1000);\n  const header = btoa(JSON.stringify({ alg: 'RS256', kid: keyId })).replace(/=/g, '').replace(/\\+/g, '-').replace(/\\//g, '_');\n  const payload = btoa(JSON.stringify({ sub: videoId, kid: keyId, exp: now + expiresIn, nbf: now }))\n    .replace(/=/g, '').replace(/\\+/g, '-').replace(/\\//g, '_');\n  const message = `${header}.${payload}`;\n  const sig = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', key, new TextEncoder().encode(message));\n  const b64Sig = btoa(String.fromCharCode(...new Uint8Array(sig))).replace(/=/g, '').replace(/\\+/g, '-').replace(/\\//g, '_');\n  return `${message}.${b64Sig}`;\n}\n\n// With access rules (geo-restriction)\nconst payloadWithRules = {\n  sub: videoId, kid: keyId, exp: now + 3600, nbf: now,\n  accessRules: [{ type: 'ip.geoip.country', action: 'allow', country: ['US'] }]\n};\n```\n\n## Best Practices\n\n- **Use Direct Creator Uploads** - Avoid proxying through servers\n- **Enable requireSignedURLs** - Control private content access\n- **Self-sign tokens at scale** - Use signing keys for >1k/day\n- **Set allowedOrigins** - Prevent hotlinking\n- **Use webhooks over polling** - Efficient status updates\n- **Set maxDurationSeconds** - Prevent abuse\n- **Enable live recordings** - Auto VOD after stream\n\n## In This Reference\n\n- [README.md](./README.md) - Overview and quick start\n- [configuration.md](./configuration.md) - Setup and config\n- [api.md](./api.md) - On-demand video APIs\n- [api-live.md](./api-live.md) - Live streaming APIs\n- [gotchas.md](./gotchas.md) - Error codes, troubleshooting\n\n## See Also\n\n- [workers](../workers/) - Deploy Stream APIs in Workers\n- [pages](../pages/) - Integrate Stream with Pages\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/tail-workers/README.md",
    "content": "# Cloudflare Tail Workers\n\nSpecialized Workers that consume execution events from producer Workers for logging, debugging, analytics, and observability.\n\n## When to Use This Reference\n\n- Implementing observability/logging for Cloudflare Workers\n- Processing Worker execution events, logs, exceptions\n- Building custom analytics or error tracking\n- Configuring real-time event streaming\n- Working with tail handlers or tail consumers\n\n## Core Concepts\n\n### What Are Tail Workers?\n\nTail Workers automatically process events from producer Workers (the Workers being monitored). They receive:\n- HTTP request/response info\n- Console logs (`console.log/error/warn/debug`)\n- Uncaught exceptions\n- Execution outcomes (`ok`, `exception`, `exceededCpu`, etc.)\n- Diagnostic channel events\n\n**Key characteristics:**\n- Invoked AFTER producer finishes executing\n- Capture entire request lifecycle including Service Bindings and Dynamic Dispatch sub-requests\n- Billed by CPU time, not request count\n- Available on Workers Paid and Enterprise tiers\n\n### Alternative: OpenTelemetry Export\n\n**Before using Tail Workers, consider OpenTelemetry:**\n\nFor batch exports to observability tools (Sentry, Grafana, Honeycomb):\n- OTEL export sends logs/traces in batches (more efficient)\n- Built-in integrations with popular platforms\n- Lower overhead than Tail Workers\n- **Use Tail Workers only for custom real-time processing**\n\n## Decision Tree\n\n```\nNeed observability for Workers?\n├─ Batch export to known tools (Sentry/Grafana/Honeycomb)?\n│  └─ Use OpenTelemetry export (not Tail Workers)\n├─ Custom real-time processing needed?\n│  ├─ Aggregated metrics?\n│  │  └─ Use Tail Worker + Analytics Engine\n│  ├─ Error tracking?\n│  │  └─ Use Tail Worker + external service\n│  ├─ Custom logging/debugging?\n│  │  └─ Use Tail Worker + KV/HTTP endpoint\n│  └─ Complex event processing?\n│     └─ Use Tail Worker + Durable Objects\n└─ Quick debugging?\n   └─ Use `wrangler tail` (different from Tail Workers)\n```\n\n## Reading Order\n\n1. **[configuration.md](configuration.md)** - Set up Tail Workers\n2. **[api.md](api.md)** - Handler signature, types, redaction\n3. **[patterns.md](patterns.md)** - Common use cases and integrations\n4. **[gotchas.md](gotchas.md)** - Pitfalls and debugging tips\n\n## Quick Example\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    // Process events from producer Worker\n    ctx.waitUntil(\n      fetch(env.LOG_ENDPOINT, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify(events),\n      })\n    );\n  }\n};\n```\n\n## Related Skills\n\n- **observability** - General Workers observability patterns, OTEL export\n- **analytics-engine** - Aggregated metrics storage for tail event data\n- **durable-objects** - Stateful event processing, batching tail events\n- **logpush** - Alternative for batch log export (non-real-time)\n- **workers-for-platforms** - Dynamic dispatch with tail consumers\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/tail-workers/api.md",
    "content": "# Tail Workers API Reference\n\n## Handler Signature\n\n```typescript\nexport default {\n  async tail(\n    events: TraceItem[],\n    env: Env,\n    ctx: ExecutionContext\n  ): Promise<void> {\n    // Process events\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n**Parameters:**\n- `events`: Array of `TraceItem` objects (one per producer invocation)\n- `env`: Bindings (KV, D1, R2, env vars, etc.)\n- `ctx`: Context with `waitUntil()` for async work\n\n**CRITICAL:** Tail handlers don't return values. Use `ctx.waitUntil()` for async operations.\n\n## TraceItem Type\n\n```typescript\ninterface TraceItem {\n  scriptName: string;           // Producer Worker name\n  eventTimestamp: number;        // Epoch milliseconds\n  outcome: 'ok' | 'exception' | 'exceededCpu' | 'exceededMemory' \n         | 'canceled' | 'scriptNotFound' | 'responseStreamDisconnected' | 'unknown';\n  \n  event?: {\n    request?: {\n      url: string;               // Redacted by default\n      method: string;\n      headers: Record<string, string>;  // Sensitive headers redacted\n      cf?: IncomingRequestCfProperties;\n      getUnredacted(): TraceRequest;    // Bypass redaction (use carefully)\n    };\n    response?: {\n      status: number;\n    };\n  };\n  \n  logs: Array<{\n    timestamp: number;           // Epoch milliseconds\n    level: 'debug' | 'info' | 'log' | 'warn' | 'error';\n    message: unknown[];          // Args passed to console function\n  }>;\n  \n  exceptions: Array<{\n    timestamp: number;           // Epoch milliseconds\n    name: string;                // Error type (Error, TypeError, etc.)\n    message: string;             // Error description\n  }>;\n  \n  diagnosticsChannelEvents: Array<{\n    channel: string;\n    message: unknown;\n    timestamp: number;           // Epoch milliseconds\n  }>;\n}\n```\n\n**Note:** Official SDK uses `TraceItem`, not `TailItem`. Use `@cloudflare/workers-types` for accurate types.\n\n## Timestamp Handling\n\nAll timestamps are **epoch milliseconds**, not seconds:\n\n```typescript\n// ✅ CORRECT - use directly with Date\nconst date = new Date(event.eventTimestamp);\n\n// ❌ WRONG - don't multiply by 1000\nconst date = new Date(event.eventTimestamp * 1000);\n```\n\n## Automatic Redaction\n\nBy default, sensitive data is redacted from `TraceRequest`:\n\n### Header Redaction\n\nHeaders containing these substrings (case-insensitive):\n- `auth`, `key`, `secret`, `token`, `jwt`\n- `cookie`, `set-cookie`\n\nRedacted values show as `\"REDACTED\"`.\n\n### URL Redaction\n\n- **Hex IDs:** 32+ hex digits → `\"REDACTED\"`\n- **Base-64 IDs:** 21+ chars with 2+ upper, 2+ lower, 2+ digits → `\"REDACTED\"`\n\n## Bypassing Redaction\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    for (const event of events) {\n      // ⚠️ Use with extreme caution\n      const unredacted = event.event?.request?.getUnredacted();\n      // unredacted.url and unredacted.headers contain raw values\n    }\n  }\n};\n```\n\n**Best practices:**\n- Only call `getUnredacted()` when absolutely necessary\n- Never log unredacted sensitive data\n- Implement additional filtering before external transmission\n- Use environment variables for API keys, never hardcode\n\n## Type-Safe Handler\n\n```typescript\ninterface Env {\n  LOGS_KV: KVNamespace;\n  ANALYTICS: AnalyticsEngineDataset;\n  LOG_ENDPOINT: string;\n  API_TOKEN: string;\n}\n\nexport default {\n  async tail(\n    events: TraceItem[],\n    env: Env,\n    ctx: ExecutionContext\n  ): Promise<void> {\n    const payload = events.map(event => ({\n      script: event.scriptName,\n      timestamp: event.eventTimestamp,\n      outcome: event.outcome,\n      url: event.event?.request?.url,\n      status: event.event?.response?.status,\n    }));\n    \n    ctx.waitUntil(\n      fetch(env.LOG_ENDPOINT, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify(payload),\n      })\n    );\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## Outcome vs HTTP Status\n\n**IMPORTANT:** `outcome` is script execution status, NOT HTTP status.\n\n- Worker returns 500 → `outcome='ok'` if script completed successfully\n- Uncaught exception → `outcome='exception'` regardless of HTTP status\n- CPU limit exceeded → `outcome='exceededCpu'`\n\n```typescript\n// ✅ Check outcome for script execution status\nif (event.outcome === 'exception') {\n  // Script threw uncaught exception\n}\n\n// ✅ Check HTTP status separately\nif (event.event?.response?.status === 500) {\n  // HTTP 500 returned (script may have handled error)\n}\n```\n\n## Serialization Considerations\n\n`log.message` is `unknown[]` and may contain non-serializable objects:\n\n```typescript\n// ❌ May fail with circular references or BigInt\nJSON.stringify(events);\n\n// ✅ Safe serialization\nconst safePayload = events.map(event => ({\n  ...event,\n  logs: event.logs.map(log => ({\n    ...log,\n    message: log.message.map(m => {\n      try {\n        return JSON.parse(JSON.stringify(m));\n      } catch {\n        return String(m);\n      }\n    })\n  }))\n}));\n```\n\n**Common serialization issues:**\n- Circular references in logged objects\n- `BigInt` values (not JSON-serializable)\n- Functions or symbols in console.log arguments\n- Large objects exceeding body size limits\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/tail-workers/configuration.md",
    "content": "# Tail Workers Configuration\n\n## Setup Steps\n\n### 1. Create Tail Worker\n\nCreate a Worker with a `tail()` handler:\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    // Process events from producer Worker\n    ctx.waitUntil(\n      fetch(env.LOG_ENDPOINT, {\n        method: \"POST\",\n        body: JSON.stringify(events),\n      })\n    );\n  }\n};\n```\n\n### 2. Configure Producer Worker\n\nIn producer's `wrangler.jsonc`:\n\n```jsonc\n{\n  \"name\": \"my-producer-worker\",\n  \"tail_consumers\": [\n    {\n      \"service\": \"my-tail-worker\"\n    }\n  ]\n}\n```\n\n### 3. Deploy Both Workers\n\n```bash\n# Deploy Tail Worker first\ncd tail-worker\nwrangler deploy\n\n# Then deploy producer Worker\ncd ../producer-worker\nwrangler deploy\n```\n\n## Wrangler Configuration\n\n### Single Tail Consumer\n\n```jsonc\n{\n  \"name\": \"producer-worker\",\n  \"tail_consumers\": [\n    {\n      \"service\": \"logging-tail-worker\"\n    }\n  ]\n}\n```\n\n### Multiple Tail Consumers\n\n```jsonc\n{\n  \"name\": \"producer-worker\",\n  \"tail_consumers\": [\n    {\n      \"service\": \"logging-tail-worker\"\n    },\n    {\n      \"service\": \"metrics-tail-worker\"\n    }\n  ]\n}\n```\n\n**Note:** Each consumer receives ALL events independently.\n\n### Remove Tail Consumer\n\n```jsonc\n{\n  \"tail_consumers\": []\n}\n```\n\nThen redeploy producer Worker.\n\n## Environment Variables\n\nTail Workers use same binding syntax as regular Workers:\n\n```jsonc\n{\n  \"name\": \"my-tail-worker\",\n  \"vars\": {\n    \"LOG_ENDPOINT\": \"https://logs.example.com/ingest\"\n  },\n  \"kv_namespaces\": [\n    {\n      \"binding\": \"LOGS_KV\",\n      \"id\": \"abc123...\"\n    }\n  ]\n}\n```\n\n## Testing & Development\n\n### Local Testing\n\n**Tail Workers cannot be fully tested with `wrangler dev`.** Deploy to staging environment for testing.\n\n### Testing Strategy\n\n1. Deploy producer Worker to staging\n2. Deploy Tail Worker to staging\n3. Configure `tail_consumers` in producer\n4. Trigger producer Worker requests\n5. Verify Tail Worker receives events (check destination logs/storage)\n\n### Wrangler Tail Command\n\n```bash\n# Stream logs to terminal (NOT Tail Workers)\nwrangler tail my-producer-worker\n```\n\n**This is different from Tail Workers:**\n- `wrangler tail` streams logs to your terminal\n- Tail Workers are Workers that process events programmatically\n\n## Deployment Checklist\n\n- [ ] Tail Worker has `tail()` handler\n- [ ] Tail Worker deployed before producer\n- [ ] Producer's `wrangler.jsonc` has correct `tail_consumers`\n- [ ] Environment variables configured\n- [ ] Tested with staging environment\n- [ ] Monitoring configured for Tail Worker itself\n\n## Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Max tail consumers per producer | 10 | Each receives all events independently |\n| Events batch size | Up to 100 events per invocation | Larger batches split across invocations |\n| Tail Worker CPU time | Same as regular Workers | 10ms (free), 30ms (paid), 50ms (paid bundle) |\n| Pricing tier | Workers Paid or Enterprise | Not available on free plan |\n| Request body size | 100 MB max | When sending to external endpoints |\n| Event retention | None | Events not retried if tail handler fails |\n\n## Workers for Platforms\n\nFor dynamic dispatch Workers, both dispatch and user Worker events sent to tail consumer:\n\n```jsonc\n{\n  \"name\": \"dispatch-worker\",\n  \"tail_consumers\": [\n    {\n      \"service\": \"platform-tail-worker\"\n    }\n  ]\n}\n```\n\nTail Worker receives TWO `TraceItem` elements per request:\n1. Dynamic dispatch Worker event\n2. User Worker event\n\nSee [patterns.md](patterns.md) for handling.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/tail-workers/gotchas.md",
    "content": "# Tail Workers Gotchas & Debugging\n\n## Critical Pitfalls\n\n### 1. Not Using `ctx.waitUntil()`\n\n**Problem:** Async work doesn't complete or tail Worker times out  \n**Cause:** Handlers exit immediately; awaiting blocks processing  \n**Solution:**\n\n```typescript\n// ❌ WRONG - fire and forget\nexport default {\n  async tail(events) {\n    fetch(endpoint, { body: JSON.stringify(events) });\n  }\n};\n\n// ❌ WRONG - blocking await\nexport default {\n  async tail(events, env, ctx) {\n    await fetch(endpoint, { body: JSON.stringify(events) });\n  }\n};\n\n// ✅ CORRECT\nexport default {\n  async tail(events, env, ctx) {\n    ctx.waitUntil(\n      (async () => {\n        await fetch(endpoint, { body: JSON.stringify(events) });\n        await processMore();\n      })()\n    );\n  }\n};\n```\n\n### 2. Missing `tail()` Handler\n\n**Problem:** Producer deployment fails  \n**Cause:** Worker in `tail_consumers` doesn't export `tail()` handler  \n**Solution:** Ensure `export default { async tail(events, env, ctx) { ... } }`\n\n### 3. Outcome vs HTTP Status\n\n**Problem:** Filtering by wrong status  \n**Cause:** `outcome` is script execution status, not HTTP status\n\n```typescript\n// ❌ WRONG\nif (event.outcome === 500) { /* never matches */ }\n\n// ✅ CORRECT\nif (event.outcome === 'exception') { /* script threw */ }\nif (event.event?.response?.status === 500) { /* HTTP 500 */ }\n```\n\n### 4. Timestamp Units\n\n**Problem:** Dates off by 1000x  \n**Cause:** Timestamps are epoch milliseconds, not seconds\n\n```typescript\n// ❌ WRONG: const date = new Date(event.eventTimestamp * 1000);\n// ✅ CORRECT: const date = new Date(event.eventTimestamp);\n```\n\n### 5. Type Name Mismatch\n\n**Problem:** Using `TailItem` type  \n**Cause:** Old docs used `TailItem`, SDK uses `TraceItem`\n\n```typescript\nimport type { TraceItem } from '@cloudflare/workers-types';\nexport default {\n  async tail(events: TraceItem[], env, ctx) { /* ... */ }\n};\n```\n\n### 6. Excessive Logging Volume\n\n**Problem:** Unexpected high costs  \n**Cause:** Invoked on EVERY producer request  \n**Solution:** Sample events\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    if (Math.random() > 0.1) return;  // 10% sample\n    ctx.waitUntil(sendToEndpoint(events));\n  }\n};\n```\n\n### 7. Serialization Issues\n\n**Problem:** `JSON.stringify()` fails  \n**Cause:** `log.message` is `unknown[]` with non-serializable values  \n**Solution:**\n\n```typescript\nconst safePayload = events.map(e => ({\n  ...e,\n  logs: e.logs.map(log => ({\n    ...log,\n    message: log.message.map(m => {\n      try { return JSON.parse(JSON.stringify(m)); }\n      catch { return String(m); }\n    })\n  }))\n}));\n```\n\n### 8. Missing Error Handling\n\n**Problem:** Tail Worker silently fails  \n**Cause:** No try/catch  \n**Solution:**\n\n```typescript\nctx.waitUntil((async () => {\n  try {\n    await fetch(env.ENDPOINT, { body: JSON.stringify(events) });\n  } catch (error) {\n    console.error(\"Tail error:\", error);\n    await env.FALLBACK_KV.put(`failed:${Date.now()}`, JSON.stringify(events));\n  }\n})());\n```\n\n### 9. Deployment Order\n\n**Problem:** Producer deployment fails  \n**Cause:** Tail consumer not deployed yet  \n**Solution:** Deploy tail consumer FIRST\n\n```bash\ncd tail-worker && wrangler deploy\ncd ../producer && wrangler deploy\n```\n\n### 10. No Event Retry\n\n**Problem:** Events lost when handler fails  \n**Cause:** Failed invocations NOT retried  \n**Solution:** Implement fallback storage (see #8)\n\n## Debugging\n\n**View logs:** `wrangler tail my-tail-worker`\n\n**Incremental testing:**\n1. Verify receipt: `console.log('Events:', events.length)`\n2. Inspect structure: `console.log(JSON.stringify(events[0], null, 2))`\n3. Add external call with `ctx.waitUntil()`\n\n**Monitor dashboard:** Check invocation count (matches producer?), error rate, CPU time\n\n## Testing\n\nAdd test endpoint to producer:\n\n```typescript\nexport default {\n  async fetch(request) {\n    if (request.url.includes('/test')) {\n      console.log('Test log');\n      throw new Error('Test error');\n    }\n    return new Response('OK');\n  }\n};\n```\n\nTrigger: `curl https://producer.example.workers.dev/test`\n\n## Common Errors\n\n| Error | Cause | Solution |\n|-------|-------|----------|\n| \"Tail consumer not found\" | Not deployed | Deploy tail Worker first |\n| \"No tail handler\" | Missing `tail()` | Add to default export |\n| \"waitUntil is not a function\" | Missing `ctx` | Add `ctx` parameter |\n| Timeout | Blocking await | Use `ctx.waitUntil()` |\n\n## Performance Notes\n\n- Max 100 events per invocation\n- Each consumer receives all events independently\n- CPU limits same as regular Workers\n- For high volume, use Durable Objects batching\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/tail-workers/patterns.md",
    "content": "# Tail Workers Common Patterns\n\n## Community Libraries\n\nWhile most tail Worker implementations are custom, these libraries may help:\n\n**Logging/Observability:**\n- **Axiom** - `axiom-cloudflare-workers` (npm) - Direct Axiom integration\n- **Baselime** - SDK for Baselime observability platform\n- **LogFlare** - Structured log aggregation\n\n**Type Definitions:**\n- **@cloudflare/workers-types** - Official TypeScript types (use `TraceItem`)\n\n**Note:** Most integrations require custom tail handler implementation. See integration examples below.\n\n## Basic Patterns\n\n### HTTP Endpoint Logging\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    const payload = events.map(event => ({\n      script: event.scriptName,\n      timestamp: event.eventTimestamp,\n      outcome: event.outcome,\n      url: event.event?.request?.url,\n      status: event.event?.response?.status,\n      logs: event.logs,\n      exceptions: event.exceptions,\n    }));\n    \n    ctx.waitUntil(\n      fetch(env.LOG_ENDPOINT, {\n        method: \"POST\",\n        body: JSON.stringify(payload),\n      })\n    );\n  }\n};\n```\n\n### Error Tracking Only\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    const errors = events.filter(e => \n      e.outcome === 'exception' || e.exceptions.length > 0\n    );\n    \n    if (errors.length === 0) return;\n    \n    ctx.waitUntil(\n      fetch(env.ERROR_ENDPOINT, {\n        method: \"POST\",\n        body: JSON.stringify(errors),\n      })\n    );\n  }\n};\n```\n\n## Storage Integration\n\n### KV Storage with TTL\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    ctx.waitUntil(\n      Promise.all(events.map(event =>\n        env.LOGS_KV.put(\n          `log:${event.scriptName}:${event.eventTimestamp}`,\n          JSON.stringify(event),\n          { expirationTtl: 86400 }  // 24 hours\n        )\n      ))\n    );\n  }\n};\n```\n\n### Analytics Engine Metrics\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    ctx.waitUntil(\n      Promise.all(events.map(event =>\n        env.ANALYTICS.writeDataPoint({\n          blobs: [event.scriptName, event.outcome],\n          doubles: [1, event.event?.response?.status ?? 0],\n          indexes: [event.event?.request?.cf?.colo ?? 'unknown'],\n        })\n      ))\n    );\n  }\n};\n```\n\n## Filtering & Routing\n\nFilter by route, outcome, or other criteria:\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    // Route filtering\n    const apiEvents = events.filter(e => \n      e.event?.request?.url?.includes('/api/')\n    );\n    \n    // Multi-destination routing\n    const errors = events.filter(e => e.outcome === 'exception');\n    const success = events.filter(e => e.outcome === 'ok');\n    \n    const tasks = [];\n    if (errors.length > 0) {\n      tasks.push(fetch(env.ERROR_ENDPOINT, {\n        method: \"POST\",\n        body: JSON.stringify(errors),\n      }));\n    }\n    if (success.length > 0) {\n      tasks.push(fetch(env.SUCCESS_ENDPOINT, {\n        method: \"POST\",\n        body: JSON.stringify(success),\n      }));\n    }\n    \n    ctx.waitUntil(Promise.all(tasks));\n  }\n};\n```\n\n## Sampling\n\nReduce costs by processing only a percentage of events:\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    if (Math.random() > 0.1) return;  // 10% sample rate\n    ctx.waitUntil(fetch(env.LOG_ENDPOINT, {\n      method: \"POST\",\n      body: JSON.stringify(events),\n    }));\n  }\n};\n```\n\n## Advanced Patterns\n\n### Batching with Durable Objects\n\nAccumulate events before sending:\n\n```typescript\nexport default {\n  async tail(events, env, ctx) {\n    const batch = env.BATCH_DO.get(env.BATCH_DO.idFromName(\"batch\"));\n    ctx.waitUntil(batch.fetch(\"https://batch/add\", {\n      method: \"POST\",\n      body: JSON.stringify(events),\n    }));\n  }\n};\n```\n\nSee durable-objects skill for full implementation.\n\n### Workers for Platforms\n\nDynamic dispatch sends TWO events per request. Filter by `scriptName` to distinguish dispatch vs user Worker events.\n\n### Error Handling\n\nAlways wrap external calls. See gotchas.md for fallback storage pattern.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/terraform/README.md",
    "content": "# Cloudflare Terraform Provider\n\n**Expert guidance for Cloudflare Terraform Provider - infrastructure as code for Cloudflare resources.**\n\n## Core Principles\n\n- **Provider-first**: Use Terraform provider for ALL infrastructure - never mix with wrangler.jsonc for the same resources\n- **State management**: Always use remote state (S3, Terraform Cloud, etc.) for team environments\n- **Modular architecture**: Create reusable modules for common patterns (zones, workers, pages)\n- **Version pinning**: Always pin provider version with `~>` for predictable upgrades\n- **Secret management**: Use variables + environment vars for sensitive data - never hardcode API tokens\n\n## Provider Version\n\n| Version | Status | Notes |\n|---------|--------|-------|\n| 5.x | Current | Auto-generated from OpenAPI, breaking changes from v4 |\n| 4.x | Legacy | Manual maintenance, deprecated |\n\n**Critical:** v5 renamed many resources (`cloudflare_record` → `cloudflare_dns_record`, `cloudflare_worker_*` → `cloudflare_workers_*`). See [gotchas.md](./gotchas.md#v5-breaking-changes) for migration details.\n\n## Provider Setup\n\n### Basic Configuration\n\n```hcl\nterraform {\n  required_version = \">= 1.0\"\n  \n  required_providers {\n    cloudflare = {\n      source  = \"cloudflare/cloudflare\"\n      version = \"~> 5.15.0\"\n    }\n  }\n}\n\nprovider \"cloudflare\" {\n  api_token = var.cloudflare_api_token  # or CLOUDFLARE_API_TOKEN env var\n}\n```\n\n### Authentication Methods (priority order)\n\n1. **API Token** (RECOMMENDED): `api_token` or `CLOUDFLARE_API_TOKEN`\n   - Create: Dashboard → My Profile → API Tokens\n   - Scope to specific accounts/zones for security\n   \n2. **Global API Key** (LEGACY): `api_key` + `api_email` or `CLOUDFLARE_API_KEY` + `CLOUDFLARE_EMAIL`\n   - Less secure, use tokens instead\n   \n3. **User Service Key**: `user_service_key` for Origin CA certificates\n\n\n\n## Quick Reference: Common Commands\n\n```bash\nterraform init          # Initialize provider\nterraform plan          # Plan changes\nterraform apply         # Apply changes\nterraform destroy       # Destroy resources\nterraform import cloudflare_zone.example <zone-id>  # Import existing\nterraform state list    # List resources in state\nterraform output        # Show outputs\nterraform fmt -recursive  # Format code\nterraform validate      # Validate configuration\n```\n\n## Import Existing Resources\n\nUse cf-terraforming to generate configs from existing Cloudflare resources:\n\n```bash\n# Install\nbrew install cloudflare/cloudflare/cf-terraforming\n\n# Generate HCL from existing resources\ncf-terraforming generate --resource-type cloudflare_dns_record --zone <zone-id>\n\n# Import into Terraform state\ncf-terraforming import --resource-type cloudflare_dns_record --zone <zone-id>\n```\n\n## Reading Order\n\n1. Start with [README.md](./README.md) for provider setup and authentication\n2. Review [configuration.md](./configuration.md) for resource configurations\n3. Check [api.md](./api.md) for data sources and existing resource queries\n4. See [patterns.md](./patterns.md) for multi-environment and CI/CD patterns\n5. Read [gotchas.md](./gotchas.md) for state drift, v5 breaking changes, and troubleshooting\n\n## In This Reference\n- [configuration.md](./configuration.md) - Resources for zones, DNS, workers, KV, R2, D1, Pages, rulesets\n- [api.md](./api.md) - Data sources for existing resources\n- [patterns.md](./patterns.md) - Architecture patterns, multi-env setup, CI/CD integration\n- [gotchas.md](./gotchas.md) - Common issues, security, best practices\n\n## See Also\n- [pulumi](../pulumi/) - Alternative IaC tool for Cloudflare\n- [wrangler](../wrangler/) - CLI deployment alternative\n- [workers](../workers/) - Worker runtime documentation\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/terraform/api.md",
    "content": "# Terraform Data Sources Reference\n\nQuery existing Cloudflare resources to reference in your configurations.\n\n## v5 Data Source Names\n\n| v4 Name | v5 Name | Notes |\n|---------|---------|-------|\n| `cloudflare_record` | `cloudflare_dns_record` | |\n| `cloudflare_worker_script` | `cloudflare_workers_script` | Note: plural |\n| `cloudflare_access_*` | `cloudflare_zero_trust_*` | Access → Zero Trust |\n\n## Zone Data Sources\n\n```hcl\n# Get zone by name\ndata \"cloudflare_zone\" \"example\" {\n  name = \"example.com\"\n}\n\n# Use in resources\nresource \"cloudflare_dns_record\" \"www\" {\n  zone_id = data.cloudflare_zone.example.id\n  name = \"www\"\n  # ...\n}\n```\n\n## Account Data Sources\n\n```hcl\n# List all accounts\ndata \"cloudflare_accounts\" \"main\" {\n  name = \"My Account\"\n}\n\n# Use account ID\nresource \"cloudflare_worker_script\" \"api\" {\n  account_id = data.cloudflare_accounts.main.accounts[0].id\n  # ...\n}\n```\n\n## Worker Data Sources\n\n```hcl\n# Get existing worker script (v5: cloudflare_workers_script)\ndata \"cloudflare_workers_script\" \"existing\" {\n  account_id = var.account_id\n  name = \"existing-worker\"\n}\n\n# Reference in service bindings\nresource \"cloudflare_workers_script\" \"consumer\" {\n  service_binding {\n    name = \"UPSTREAM\"\n    service = data.cloudflare_workers_script.existing.name\n  }\n}\n```\n\n## KV Data Sources\n\n```hcl\n# Get KV namespace\ndata \"cloudflare_workers_kv_namespace\" \"existing\" {\n  account_id = var.account_id\n  namespace_id = \"abc123\"\n}\n\n# Use in worker binding\nresource \"cloudflare_workers_script\" \"api\" {\n  kv_namespace_binding {\n    name = \"KV\"\n    namespace_id = data.cloudflare_workers_kv_namespace.existing.id\n  }\n}\n```\n\n## Lists Data Source\n\n```hcl\n# Get IP lists for WAF rules\ndata \"cloudflare_list\" \"blocked_ips\" {\n  account_id = var.account_id\n  name = \"blocked_ips\"\n}\n```\n\n## IP Ranges Data Source\n\n```hcl\n# Get Cloudflare IP ranges (for firewall rules)\ndata \"cloudflare_ip_ranges\" \"cloudflare\" {}\n\noutput \"ipv4_cidrs\" {\n  value = data.cloudflare_ip_ranges.cloudflare.ipv4_cidr_blocks\n}\n\noutput \"ipv6_cidrs\" {\n  value = data.cloudflare_ip_ranges.cloudflare.ipv6_cidr_blocks\n}\n\n# Use in security group rules (AWS example)\nresource \"aws_security_group_rule\" \"allow_cloudflare\" {\n  type = \"ingress\"\n  from_port = 443\n  to_port = 443\n  protocol = \"tcp\"\n  cidr_blocks = data.cloudflare_ip_ranges.cloudflare.ipv4_cidr_blocks\n  security_group_id = aws_security_group.web.id\n}\n```\n\n## Common Patterns\n\n### Import ID Formats\n\n| Resource | Import ID Format |\n|----------|------------------|\n| `cloudflare_zone` | `<zone-id>` |\n| `cloudflare_dns_record` | `<zone-id>/<record-id>` |\n| `cloudflare_workers_script` | `<account-id>/<script-name>` |\n| `cloudflare_workers_kv_namespace` | `<account-id>/<namespace-id>` |\n| `cloudflare_r2_bucket` | `<account-id>/<bucket-name>` |\n| `cloudflare_d1_database` | `<account-id>/<database-id>` |\n| `cloudflare_pages_project` | `<account-id>/<project-name>` |\n\n```bash\n# Example: Import DNS record\nterraform import cloudflare_dns_record.example <zone-id>/<record-id>\n```\n\n### Reference Across Modules\n\n```hcl\n# modules/worker/main.tf\ndata \"cloudflare_zone\" \"main\" {\n  name = var.domain\n}\n\nresource \"cloudflare_worker_route\" \"api\" {\n  zone_id = data.cloudflare_zone.main.id\n  pattern = \"api.${var.domain}/*\"\n  script_name = cloudflare_worker_script.api.name\n}\n```\n\n### Output Important Values\n\n```hcl\noutput \"zone_id\" {\n  value = cloudflare_zone.main.id\n  description = \"Zone ID for DNS management\"\n}\n\noutput \"worker_url\" {\n  value = \"https://${cloudflare_worker_domain.api.hostname}\"\n  description = \"Worker API endpoint\"\n}\n\noutput \"kv_namespace_id\" {\n  value = cloudflare_workers_kv_namespace.app.id\n  sensitive = false\n}\n\noutput \"name_servers\" {\n  value = cloudflare_zone.main.name_servers\n  description = \"Name servers for domain registration\"\n}\n```\n\n## See Also\n\n- [README](./README.md) - Provider setup\n- [Configuration Reference](./configuration.md) - All resource types\n- [Patterns](./patterns.md) - Architecture patterns\n- [Troubleshooting](./gotchas.md) - Common issues\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/terraform/configuration.md",
    "content": "# Terraform Configuration Reference\n\nComplete resource configurations for Cloudflare infrastructure.\n\n## Zone & DNS\n\n```hcl\n# Zone + settings\nresource \"cloudflare_zone\" \"example\" { account = { id = var.account_id }; name = \"example.com\"; type = \"full\" }\nresource \"cloudflare_zone_settings_override\" \"example\" {\n  zone_id = cloudflare_zone.example.id\n  settings { ssl = \"strict\"; always_use_https = \"on\"; min_tls_version = \"1.2\"; tls_1_3 = \"on\"; http3 = \"on\" }\n}\n\n# DNS records (A, CNAME, MX, TXT)\nresource \"cloudflare_dns_record\" \"www\" {\n  zone_id = cloudflare_zone.example.id; name = \"www\"; content = \"192.0.2.1\"; type = \"A\"; proxied = true\n}\nresource \"cloudflare_dns_record\" \"mx\" {\n  for_each = { \"10\" = \"mail1.example.com\", \"20\" = \"mail2.example.com\" }\n  zone_id = cloudflare_zone.example.id; name = \"@\"; content = each.value; type = \"MX\"; priority = each.key\n}\n```\n\n## Workers\n\n### Simple Pattern (Legacy - Still Works)\n\n```hcl\nresource \"cloudflare_workers_script\" \"api\" {\n  account_id = var.account_id; name = \"api-worker\"; content = file(\"worker.js\")\n  module = true; compatibility_date = \"2025-01-01\"\n  kv_namespace_binding { name = \"KV\"; namespace_id = cloudflare_workers_kv_namespace.cache.id }\n  r2_bucket_binding { name = \"BUCKET\"; bucket_name = cloudflare_r2_bucket.assets.name }\n  d1_database_binding { name = \"DB\"; database_id = cloudflare_d1_database.app.id }\n  secret_text_binding { name = \"SECRET\"; text = var.secret }\n}\n```\n\n### Gradual Rollouts (Recommended for Production)\n\n```hcl\nresource \"cloudflare_worker\" \"api\" { account_id = var.account_id; name = \"api-worker\" }\nresource \"cloudflare_worker_version\" \"api_v1\" {\n  account_id = var.account_id; worker_name = cloudflare_worker.api.name\n  content = file(\"worker.js\"); content_sha256 = filesha256(\"worker.js\")\n  compatibility_date = \"2025-01-01\"\n  bindings {\n    kv_namespace { name = \"KV\"; namespace_id = cloudflare_workers_kv_namespace.cache.id }\n    r2_bucket { name = \"BUCKET\"; bucket_name = cloudflare_r2_bucket.assets.name }\n  }\n}\nresource \"cloudflare_workers_deployment\" \"api\" {\n  account_id = var.account_id; worker_name = cloudflare_worker.api.name\n  versions { version_id = cloudflare_worker_version.api_v1.id; percentage = 100 }\n}\n```\n\n### Worker Binding Types (v5)\n\n| Binding | Attribute | Example |\n|---------|-----------|---------|\n| KV | `kv_namespace_binding` | `{ name = \"KV\", namespace_id = \"...\" }` |\n| R2 | `r2_bucket_binding` | `{ name = \"BUCKET\", bucket_name = \"...\" }` |\n| D1 | `d1_database_binding` | `{ name = \"DB\", database_id = \"...\" }` |\n| Service | `service_binding` | `{ name = \"AUTH\", service = \"auth-worker\" }` |\n| Secret | `secret_text_binding` | `{ name = \"API_KEY\", text = \"...\" }` |\n| Queue | `queue_binding` | `{ name = \"QUEUE\", queue_name = \"...\" }` |\n| Vectorize | `vectorize_binding` | `{ name = \"INDEX\", index_name = \"...\" }` |\n| Hyperdrive | `hyperdrive_binding` | `{ name = \"DB\", id = \"...\" }` |\n| AI | `ai_binding` | `{ name = \"AI\" }` |\n| Browser | `browser_binding` | `{ name = \"BROWSER\" }` |\n| Analytics | `analytics_engine_binding` | `{ name = \"ANALYTICS\", dataset = \"...\" }` |\n| mTLS | `mtls_certificate_binding` | `{ name = \"CERT\", certificate_id = \"...\" }` |\n\n### Routes & Triggers\n\n```hcl\nresource \"cloudflare_worker_route\" \"api\" {\n  zone_id = cloudflare_zone.example.id; pattern = \"api.example.com/*\"\n  script_name = cloudflare_workers_script.api.name\n}\nresource \"cloudflare_worker_cron_trigger\" \"task\" {\n  account_id = var.account_id; script_name = cloudflare_workers_script.api.name\n  schedules = [\"*/5 * * * *\"]\n}\n```\n\n## Storage (KV, R2, D1)\n\n```hcl\n# KV\nresource \"cloudflare_workers_kv_namespace\" \"cache\" { account_id = var.account_id; title = \"cache\" }\nresource \"cloudflare_workers_kv\" \"config\" {\n  account_id = var.account_id; namespace_id = cloudflare_workers_kv_namespace.cache.id\n  key_name = \"config\"; value = jsonencode({ version = \"1.0\" })\n}\n\n# R2\nresource \"cloudflare_r2_bucket\" \"assets\" { account_id = var.account_id; name = \"assets\"; location = \"WNAM\" }\n\n# D1 (migrations via wrangler) & Queues\nresource \"cloudflare_d1_database\" \"app\" { account_id = var.account_id; name = \"app-db\" }\nresource \"cloudflare_queue\" \"events\" { account_id = var.account_id; name = \"events-queue\" }\n```\n\n## Pages\n\n```hcl\nresource \"cloudflare_pages_project\" \"site\" {\n  account_id = var.account_id; name = \"site\"; production_branch = \"main\"\n  deployment_configs {\n    production {\n      compatibility_date = \"2025-01-01\"\n      environment_variables = { NODE_ENV = \"production\" }\n      kv_namespaces = { KV = cloudflare_workers_kv_namespace.cache.id }\n      d1_databases = { DB = cloudflare_d1_database.app.id }\n    }\n  }\n  build_config { build_command = \"npm run build\"; destination_dir = \"dist\" }\n  source { type = \"github\"; config { owner = \"org\"; repo_name = \"site\"; production_branch = \"main\" }}\n}\n\nresource \"cloudflare_pages_domain\" \"custom\" {\n  account_id = var.account_id; project_name = cloudflare_pages_project.site.name; domain = \"site.example.com\"\n}\n```\n\n## Rulesets (WAF, Redirects, Cache)\n\n```hcl\n# WAF\nresource \"cloudflare_ruleset\" \"waf\" {\n  zone_id = cloudflare_zone.example.id; name = \"WAF\"; kind = \"zone\"; phase = \"http_request_firewall_custom\"\n  rules { action = \"block\"; enabled = true; expression = \"(cf.client.bot) and not (cf.verified_bot)\" }\n}\n\n# Redirects\nresource \"cloudflare_ruleset\" \"redirects\" {\n  zone_id = cloudflare_zone.example.id; name = \"Redirects\"; kind = \"zone\"; phase = \"http_request_dynamic_redirect\"\n  rules {\n    action = \"redirect\"; enabled = true; expression = \"(http.request.uri.path eq \\\"/old\\\")\"\n    action_parameters { from_value { status_code = 301; target_url { value = \"https://example.com/new\" }}}\n  }\n}\n\n# Cache rules\nresource \"cloudflare_ruleset\" \"cache\" {\n  zone_id = cloudflare_zone.example.id; name = \"Cache\"; kind = \"zone\"; phase = \"http_request_cache_settings\"\n  rules {\n    action = \"set_cache_settings\"; enabled = true; expression = \"(http.request.uri.path matches \\\"\\\\.(jpg|png|css|js)$\\\")\"\n    action_parameters { cache = true; edge_ttl { mode = \"override_origin\"; default = 86400 }}\n  }\n}\n```\n\n## Load Balancers\n\n```hcl\nresource \"cloudflare_load_balancer_monitor\" \"http\" {\n  account_id = var.account_id; type = \"http\"; path = \"/health\"; interval = 60; timeout = 5\n}\nresource \"cloudflare_load_balancer_pool\" \"api\" {\n  account_id = var.account_id; name = \"api-pool\"; monitor = cloudflare_load_balancer_monitor.http.id\n  origins { name = \"api-1\"; address = \"192.0.2.1\" }\n  origins { name = \"api-2\"; address = \"192.0.2.2\" }\n}\nresource \"cloudflare_load_balancer\" \"api\" {\n  zone_id = cloudflare_zone.example.id; name = \"api.example.com\"\n  default_pool_ids = [cloudflare_load_balancer_pool.api.id]; steering_policy = \"geo\"\n}\n```\n\n## Access (Zero Trust)\n\n```hcl\nresource \"cloudflare_access_application\" \"admin\" {\n  account_id = var.account_id; name = \"Admin\"; domain = \"admin.example.com\"; type = \"self_hosted\"\n  session_duration = \"24h\"; allowed_idps = [cloudflare_access_identity_provider.github.id]\n}\nresource \"cloudflare_access_policy\" \"allow\" {\n  account_id = var.account_id; application_id = cloudflare_access_application.admin.id\n  name = \"Allow\"; decision = \"allow\"; precedence = 1\n  include { email = [\"admin@example.com\"] }\n}\nresource \"cloudflare_access_identity_provider\" \"github\" {\n  account_id = var.account_id; name = \"GitHub\"; type = \"github\"\n  config { client_id = var.github_id; client_secret = var.github_secret }\n}\n```\n\n## See Also\n\n- [README](./README.md) - Provider setup\n- [API](./api.md) - Data sources\n- [Patterns](./patterns.md) - Use cases\n- [Troubleshooting](./gotchas.md) - Issues\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/terraform/gotchas.md",
    "content": "# Terraform Troubleshooting & Best Practices\n\nCommon issues, security considerations, and best practices.\n\n## State Drift Issues\n\nSome resources have known state drift. Add lifecycle blocks to prevent perpetual diffs:\n\n| Resource | Drift Attributes | Workaround |\n|----------|------------------|------------|\n| `cloudflare_pages_project` | `deployment_configs.*` | `ignore_changes = [deployment_configs]` |\n| `cloudflare_workers_script` | secrets returned as REDACTED | `ignore_changes = [secret_text_binding]` |\n| `cloudflare_load_balancer` | `adaptive_routing`, `random_steering` | `ignore_changes = [adaptive_routing, random_steering]` |\n| `cloudflare_workers_kv` | special chars in keys (< 5.16.0) | Upgrade to 5.16.0+ |\n\n```hcl\n# Example: Ignore secret drift\nresource \"cloudflare_workers_script\" \"api\" {\n  account_id = var.account_id\n  name = \"api-worker\"\n  content = file(\"worker.js\")\n  secret_text_binding { name = \"API_KEY\"; text = var.api_key }\n  \n  lifecycle {\n    ignore_changes = [secret_text_binding]\n  }\n}\n```\n\n## v5 Breaking Changes\n\nProvider v5 is current (auto-generated from OpenAPI). v4→v5 has breaking changes:\n\n**Resource Renames:**\n\n| v4 Resource | v5 Resource | Notes |\n|-------------|-------------|-------|\n| `cloudflare_record` | `cloudflare_dns_record` | |\n| `cloudflare_worker_script` | `cloudflare_workers_script` | Note: plural |\n| `cloudflare_worker_*` | `cloudflare_workers_*` | All worker resources |\n| `cloudflare_access_*` | `cloudflare_zero_trust_*` | Access → Zero Trust |\n\n**Attribute Changes:**\n\n| v4 Attribute | v5 Attribute | Resources |\n|--------------|--------------|-----------|\n| `zone` | `name` | zone |\n| `account_id` | `account.id` | zone (object syntax) |\n| `key` | `key_name` | KV |\n| `location_hint` | `location` | R2 |\n\n**State Migration:**\n\n```bash\n# Rename resources in state after v5 upgrade\nterraform state mv cloudflare_record.example cloudflare_dns_record.example\nterraform state mv cloudflare_worker_script.api cloudflare_workers_script.api\n```\n\n## Resource-Specific Gotchas\n\n### R2 Location Case Sensitivity\n\n**Problem:** Terraform creates R2 bucket but fails on subsequent applies  \n**Cause:** Location must be UPPERCASE  \n**Solution:** Use `WNAM`, `ENAM`, `WEUR`, `EEUR`, `APAC` (not `wnam`, `enam`, etc.)\n\n```hcl\nresource \"cloudflare_r2_bucket\" \"assets\" {\n  account_id = var.account_id\n  name = \"assets\"\n  location = \"WNAM\"  # UPPERCASE required\n}\n```\n\n### KV Special Characters (< 5.16.0)\n\n**Problem:** Keys with `+`, `#`, `%` cause encoding issues  \n**Cause:** URL encoding bug in provider < 5.16.0  \n**Solution:** Upgrade to 5.16.0+ or avoid special chars in keys\n\n### D1 Migrations\n\n**Problem:** Terraform creates database but schema is empty  \n**Cause:** Terraform only creates D1 resource, not schema  \n**Solution:** Run migrations via wrangler after Terraform apply\n\n```bash\n# After terraform apply\nwrangler d1 migrations apply <db-name>\n```\n\n### Worker Script Size Limit\n\n**Problem:** Worker deployment fails with \"script too large\"  \n**Cause:** Worker script + dependencies exceed 10 MB limit  \n**Solution:** Use code splitting, external dependencies, or minification\n\n### Pages Project Drift\n\n**Problem:** Pages project shows perpetual diff on `deployment_configs`  \n**Cause:** Cloudflare API adds default values not in Terraform state  \n**Solution:** Add lifecycle ignore block (see State Drift table above)\n\n## Common Errors\n\n### \"Error: couldn't find resource\"\n\n**Cause:** Resource was deleted outside Terraform  \n**Solution:** Import resource back into state with `terraform import cloudflare_zone.example <zone-id>` or remove from state with `terraform state rm cloudflare_zone.example`\n\n### \"409 Conflict on worker deployment\"\n\n**Cause:** Worker being deployed by both Terraform and wrangler simultaneously  \n**Solution:** Choose one deployment method; if using Terraform, remove wrangler deployments\n\n### \"DNS record already exists\"\n\n**Cause:** Existing DNS record not imported into Terraform state  \n**Solution:** Find record ID in Cloudflare dashboard and import with `terraform import cloudflare_dns_record.example <zone-id>/<record-id>`\n\n### \"Invalid provider configuration\"\n\n**Cause:** API token missing, invalid, or lacking required permissions  \n**Solution:** Set `CLOUDFLARE_API_TOKEN` environment variable or check token permissions in dashboard\n\n### \"State locking errors\"\n\n**Cause:** Multiple concurrent Terraform runs or stale lock from crashed process  \n**Solution:** Remove stale lock with `terraform force-unlock <lock-id>` (use with caution)\n\n## Limits\n\n| Resource | Limit | Notes |\n|----------|-------|-------|\n| API token rate limit | Varies by plan | Use `api_client_logging = true` to debug\n| Worker script size | 10 MB | Includes all dependencies\n| KV keys per namespace | Unlimited | Pay per operation\n| R2 storage | Unlimited | Pay per GB\n| D1 databases | 50,000 per account | Free tier: 10\n| Pages projects | 500 per account | 100 for free accounts\n| DNS records | 3,500 per zone | Free plan\n\n## See Also\n\n- [README](./README.md) - Provider setup\n- [Configuration](./configuration.md) - Resources\n- [API](./api.md) - Data sources\n- [Patterns](./patterns.md) - Use cases\n- Provider docs: https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/terraform/patterns.md",
    "content": "# Terraform Patterns & Use Cases\n\nArchitecture patterns, multi-environment setups, and real-world use cases.\n\n## Recommended Directory Structure\n\n```\nterraform/\n├── environments/\n│   ├── production/\n│   │   ├── main.tf\n│   │   └── terraform.tfvars\n│   └── staging/\n│       ├── main.tf\n│       └── terraform.tfvars\n├── modules/\n│   ├── zone/\n│   ├── worker/\n│   └── dns/\n└── shared/          # Shared resources across envs\n    └── main.tf\n```\n\n**Note:** Cloudflare recommends avoiding modules for provider resources due to v5 auto-generation complexity. Prefer environment directories + shared state instead.\n\n## Multi-Environment Setup\n\n```hcl\n# Directory: environments/{production,staging}/main.tf + modules/{zone,worker,pages}\nmodule \"zone\" {\n  source = \"../../modules/zone\"; account_id = var.account_id; zone_name = \"example.com\"; environment = \"production\"\n}\nmodule \"api_worker\" {\n  source = \"../../modules/worker\"; account_id = var.account_id; zone_id = module.zone.zone_id\n  name = \"api-worker-prod\"; script = file(\"../../workers/api.js\"); environment = \"production\"\n}\n```\n\n## R2 State Backend\n\n```hcl\nterraform {\n  backend \"s3\" {\n    bucket = \"terraform-state\"\n    key = \"cloudflare.tfstate\"\n    region = \"auto\"\n    endpoints = { s3 = \"https://<account_id>.r2.cloudflarestorage.com\" }\n    skip_credentials_validation = true\n    skip_region_validation = true\n    skip_requesting_account_id = true\n    skip_metadata_api_check = true\n    skip_s3_checksum = true\n  }\n}\n```\n\n## Worker with All Bindings\n\n```hcl\nlocals { worker_name = \"full-stack-worker\" }\nresource \"cloudflare_workers_kv_namespace\" \"app\" { account_id = var.account_id; title = \"${local.worker_name}-kv\" }\nresource \"cloudflare_r2_bucket\" \"app\" { account_id = var.account_id; name = \"${local.worker_name}-bucket\" }\nresource \"cloudflare_d1_database\" \"app\" { account_id = var.account_id; name = \"${local.worker_name}-db\" }\n\nresource \"cloudflare_worker_script\" \"app\" {\n  account_id = var.account_id; name = local.worker_name; content = file(\"worker.js\"); module = true\n  compatibility_date = \"2025-01-01\"\n  kv_namespace_binding { name = \"KV\"; namespace_id = cloudflare_workers_kv_namespace.app.id }\n  r2_bucket_binding { name = \"BUCKET\"; bucket_name = cloudflare_r2_bucket.app.name }\n  d1_database_binding { name = \"DB\"; database_id = cloudflare_d1_database.app.id }\n  secret_text_binding { name = \"API_KEY\"; text = var.api_key }\n}\n```\n\n## Wrangler Integration\n\n**CRITICAL**: Wrangler and Terraform must NOT manage same resources.\n\n**Terraform**: Zones, DNS, security rules, Access, load balancers, worker deployments (CI/CD), KV/R2/D1 resource creation  \n**Wrangler**: Local dev (`wrangler dev`), manual deploys, D1 migrations, KV bulk ops, log streaming (`wrangler tail`)\n\n### CI/CD Pattern\n\n```hcl\n# Terraform creates infrastructure\nresource \"cloudflare_workers_kv_namespace\" \"app\" { account_id = var.account_id; title = \"app-kv\" }\nresource \"cloudflare_d1_database\" \"app\" { account_id = var.account_id; name = \"app-db\" }\noutput \"kv_namespace_id\" { value = cloudflare_workers_kv_namespace.app.id }\noutput \"d1_database_id\" { value = cloudflare_d1_database.app.id }\n```\n\n```yaml\n# GitHub Actions: terraform apply → envsubst wrangler.jsonc.template → wrangler deploy\n- run: terraform apply -auto-approve\n- run: |\n    export KV_NAMESPACE_ID=$(terraform output -raw kv_namespace_id)\n    envsubst < wrangler.jsonc.template > wrangler.jsonc\n- run: wrangler deploy\n```\n\n## Use Cases\n\n### Static Site + API Worker\n\n```hcl\nresource \"cloudflare_pages_project\" \"frontend\" {\n  account_id = var.account_id; name = \"frontend\"; production_branch = \"main\"\n  build_config { build_command = \"npm run build\"; destination_dir = \"dist\" }\n}\nresource \"cloudflare_worker_script\" \"api\" {\n  account_id = var.account_id; name = \"api\"; content = file(\"api-worker.js\")\n  d1_database_binding { name = \"DB\"; database_id = cloudflare_d1_database.api_db.id }\n}\nresource \"cloudflare_dns_record\" \"frontend\" {\n  zone_id = cloudflare_zone.main.id; name = \"app\"; content = cloudflare_pages_project.frontend.subdomain; type = \"CNAME\"; proxied = true\n}\nresource \"cloudflare_worker_route\" \"api\" {\n  zone_id = cloudflare_zone.main.id; pattern = \"api.example.com/*\"; script_name = cloudflare_worker_script.api.name\n}\n```\n\n### Multi-Region Load Balancing\n\n```hcl\nresource \"cloudflare_load_balancer_pool\" \"us\" {\n  account_id = var.account_id; name = \"us-pool\"; monitor = cloudflare_load_balancer_monitor.http.id\n  origins { name = \"us-east\"; address = var.us_east_ip }\n}\nresource \"cloudflare_load_balancer_pool\" \"eu\" {\n  account_id = var.account_id; name = \"eu-pool\"; monitor = cloudflare_load_balancer_monitor.http.id\n  origins { name = \"eu-west\"; address = var.eu_west_ip }\n}\nresource \"cloudflare_load_balancer\" \"global\" {\n  zone_id = cloudflare_zone.main.id; name = \"api.example.com\"; steering_policy = \"geo\"\n  default_pool_ids = [cloudflare_load_balancer_pool.us.id]\n  region_pools { region = \"WNAM\"; pool_ids = [cloudflare_load_balancer_pool.us.id] }\n  region_pools { region = \"WEU\"; pool_ids = [cloudflare_load_balancer_pool.eu.id] }\n}\n```\n\n### Secure Admin with Access\n\n```hcl\nresource \"cloudflare_pages_project\" \"admin\" { account_id = var.account_id; name = \"admin\"; production_branch = \"main\" }\nresource \"cloudflare_access_application\" \"admin\" {\n  account_id = var.account_id; name = \"Admin\"; domain = \"admin.example.com\"; type = \"self_hosted\"; session_duration = \"24h\"\n  allowed_idps = [cloudflare_access_identity_provider.google.id]\n}\nresource \"cloudflare_access_policy\" \"allow\" {\n  account_id = var.account_id; application_id = cloudflare_access_application.admin.id\n  name = \"Allow admins\"; decision = \"allow\"; precedence = 1; include { email = var.admin_emails }\n}\n```\n\n### Reusable Module\n\n```hcl\n# modules/cloudflare-zone/main.tf\nvariable \"account_id\" { type = string }; variable \"domain\" { type = string }; variable \"ssl_mode\" { default = \"strict\" }\nresource \"cloudflare_zone\" \"main\" { account = { id = var.account_id }; name = var.domain }\nresource \"cloudflare_zone_settings_override\" \"main\" {\n  zone_id = cloudflare_zone.main.id; settings { ssl = var.ssl_mode; always_use_https = \"on\" }\n}\noutput \"zone_id\" { value = cloudflare_zone.main.id }\n\n# Usage: module \"prod\" { source = \"./modules/cloudflare-zone\"; account_id = var.account_id; domain = \"example.com\" }\n```\n\n## See Also\n\n- [README](./README.md) - Provider setup\n- [Configuration Reference](./configuration.md) - All resource types\n- [API Reference](./api.md) - Data sources\n- [Troubleshooting](./gotchas.md) - Best practices, common issues\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/tunnel/README.md",
    "content": "# Cloudflare Tunnel\n\nSecure outbound-only connections between infrastructure and Cloudflare's global network.\n\n## Overview\n\nCloudflare Tunnel (formerly Argo Tunnel) enables:\n- **Outbound-only connections** - No inbound ports or firewall changes\n- **Public hostname routing** - Expose local services to internet\n- **Private network access** - Connect internal networks via WARP\n- **Zero Trust integration** - Built-in access policies\n\n**Architecture**: Tunnel (persistent object) → Replica (`cloudflared` process) → Origin services\n\n**Terminology:**\n- **Tunnel**: Named persistent object with UUID\n- **Replica**: Individual `cloudflared` process connected to tunnel\n- **Config Source**: Where ingress rules stored (local file vs Cloudflare dashboard)\n- **Connector**: Legacy term for replica\n\n## Quick Start\n\n### Local Config\n```bash\n# Install cloudflared\nbrew install cloudflared  # macOS\n\n# Authenticate\ncloudflared tunnel login\n\n# Create tunnel\ncloudflared tunnel create my-tunnel\n\n# Route DNS\ncloudflared tunnel route dns my-tunnel app.example.com\n\n# Run tunnel\ncloudflared tunnel run my-tunnel\n```\n\n### Dashboard Config (Recommended)\n1. **Zero Trust** > **Networks** > **Tunnels** > **Create**\n2. Name tunnel, copy token\n3. Configure routes in dashboard\n4. Run: `cloudflared tunnel --no-autoupdate run --token <TOKEN>`\n\n## Decision Tree\n\n**Choose config source:**\n```\nNeed centralized config updates?\n├─ Yes → Token-based (dashboard config)\n└─ No → Local config file\n\nMultiple environments (dev/staging/prod)?\n├─ Yes → Local config (version controlled)\n└─ No → Either works\n\nNeed firewall approval?\n└─ See networking.md first\n```\n\n## Core Commands\n\n```bash\n# Tunnel lifecycle\ncloudflared tunnel create <name>\ncloudflared tunnel list\ncloudflared tunnel info <name>\ncloudflared tunnel delete <name>\n\n# DNS routing\ncloudflared tunnel route dns <tunnel> <hostname>\ncloudflared tunnel route list\n\n# Private network\ncloudflared tunnel route ip add 10.0.0.0/8 <tunnel>\n\n# Run tunnel\ncloudflared tunnel run <name>\n```\n\n## Configuration Example\n\n```yaml\n# ~/.cloudflared/config.yml\ntunnel: 6ff42ae2-765d-4adf-8112-31c55c1551ef\ncredentials-file: /root/.cloudflared/6ff42ae2-765d-4adf-8112-31c55c1551ef.json\n\ningress:\n  - hostname: app.example.com\n    service: http://localhost:8000\n  - hostname: api.example.com\n    service: https://localhost:8443\n    originRequest:\n      noTLSVerify: true\n  - service: http_status:404\n```\n\n## Reading Order\n\n**New to Cloudflare Tunnel:**\n1. This README (overview, quick start)\n2. [networking.md](./networking.md) - Firewall rules, connectivity pre-checks\n3. [configuration.md](./configuration.md) - Config file options, ingress rules\n4. [patterns.md](./patterns.md) - Docker, Kubernetes, production deployment\n5. [gotchas.md](./gotchas.md) - Troubleshooting, best practices\n\n**Enterprise deployment:**\n1. [networking.md](./networking.md) - Corporate firewall requirements\n2. [gotchas.md](./gotchas.md) - HA setup, security best practices\n3. [patterns.md](./patterns.md) - Kubernetes, rolling updates\n\n**Programmatic control:**\n1. [api.md](./api.md) - REST API, TypeScript SDK\n\n## In This Reference\n\n- [networking.md](./networking.md) - Firewall rules, ports, connectivity pre-checks\n- [configuration.md](./configuration.md) - Config file options, ingress rules, TLS settings\n- [api.md](./api.md) - REST API, TypeScript SDK, token-based tunnels\n- [patterns.md](./patterns.md) - Docker, Kubernetes, Terraform, HA, use cases\n- [gotchas.md](./gotchas.md) - Troubleshooting, limitations, best practices\n\n## See Also\n\n- [workers](../workers/) - Workers with Tunnel integration\n- [access](../access/) - Zero Trust access policies\n- [warp](../warp/) - WARP client for private networks\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/tunnel/api.md",
    "content": "# Tunnel API\n\n## Cloudflare API Access\n\n**Base URL**: `https://api.cloudflare.com/client/v4`\n\n**Authentication**:\n```bash\nAuthorization: Bearer ${CF_API_TOKEN}\n```\n\n## TypeScript SDK\n\nInstall: `npm install cloudflare`\n\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst cf = new Cloudflare({\n  apiToken: process.env.CF_API_TOKEN,\n});\n\nconst accountId = process.env.CF_ACCOUNT_ID;\n```\n\n## Create Tunnel\n\n### cURL\n```bash\ncurl -X POST \"https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels\" \\\n  -H \"Authorization: Bearer ${CF_API_TOKEN}\" \\\n  -H \"Content-Type: application/json\" \\\n  --data '{\n    \"name\": \"my-tunnel\",\n    \"tunnel_secret\": \"<base64-secret>\"\n  }'\n```\n\n### TypeScript\n```typescript\nconst tunnel = await cf.zeroTrust.tunnels.create({\n  account_id: accountId,\n  name: 'my-tunnel',\n  tunnel_secret: Buffer.from(crypto.randomBytes(32)).toString('base64'),\n});\n\nconsole.log(`Tunnel ID: ${tunnel.id}`);\n```\n\n## List Tunnels\n\n### cURL\n```bash\ncurl -X GET \"https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels\" \\\n  -H \"Authorization: Bearer ${CF_API_TOKEN}\"\n```\n\n### TypeScript\n```typescript\nconst tunnels = await cf.zeroTrust.tunnels.list({\n  account_id: accountId,\n});\n\nfor (const tunnel of tunnels.result) {\n  console.log(`${tunnel.name}: ${tunnel.id}`);\n}\n```\n\n## Get Tunnel Info\n\n### cURL\n```bash\ncurl -X GET \"https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}\" \\\n  -H \"Authorization: Bearer ${CF_API_TOKEN}\"\n```\n\n### TypeScript\n```typescript\nconst tunnel = await cf.zeroTrust.tunnels.get(tunnelId, {\n  account_id: accountId,\n});\n\nconsole.log(`Status: ${tunnel.status}`);\nconsole.log(`Connections: ${tunnel.connections?.length || 0}`);\n```\n\n## Update Tunnel Config\n\n### cURL\n```bash\ncurl -X PUT \"https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}/configurations\" \\\n  -H \"Authorization: Bearer ${CF_API_TOKEN}\" \\\n  -H \"Content-Type: application/json\" \\\n  --data '{\n    \"config\": {\n      \"ingress\": [\n        {\"hostname\": \"app.example.com\", \"service\": \"http://localhost:8000\"},\n        {\"service\": \"http_status:404\"}\n      ]\n    }\n  }'\n```\n\n### TypeScript\n```typescript\nconst config = await cf.zeroTrust.tunnels.configurations.update(\n  tunnelId,\n  {\n    account_id: accountId,\n    config: {\n      ingress: [\n        { hostname: 'app.example.com', service: 'http://localhost:8000' },\n        { service: 'http_status:404' },\n      ],\n    },\n  }\n);\n```\n\n## Delete Tunnel\n\n### cURL\n```bash\ncurl -X DELETE \"https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}\" \\\n  -H \"Authorization: Bearer ${CF_API_TOKEN}\"\n```\n\n### TypeScript\n```typescript\nawait cf.zeroTrust.tunnels.delete(tunnelId, {\n  account_id: accountId,\n});\n```\n\n## Token-Based Tunnels (Config Source: Cloudflare)\n\nToken-based tunnels store config in Cloudflare dashboard instead of local files.\n\n### Via Dashboard\n1. **Zero Trust** > **Networks** > **Tunnels**\n2. **Create a tunnel** > **Cloudflared**\n3. Configure routes in dashboard\n4. Copy token\n5. Run on origin:\n```bash\ncloudflared service install <TOKEN>\n```\n\n### Via Token\n```bash\n# Run with token (no config file needed)\ncloudflared tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}\n\n# Docker\ndocker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}\n```\n\n### Get Tunnel Token (TypeScript)\n```typescript\n// Get tunnel to retrieve token\nconst tunnel = await cf.zeroTrust.tunnels.get(tunnelId, {\n  account_id: accountId,\n});\n\n// Token available in tunnel.token (only for config source: cloudflare)\nconst token = tunnel.token;\n```\n\n## DNS Routes API\n\n```bash\n# Create DNS route\ncurl -X POST \"https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}/connections\" \\\n  -H \"Authorization: Bearer ${CF_API_TOKEN}\" \\\n  --data '{\"hostname\": \"app.example.com\"}'\n\n# Delete route\ncurl -X DELETE \"https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}/connections/{route_id}\" \\\n  -H \"Authorization: Bearer ${CF_API_TOKEN}\"\n```\n\n## Private Network Routes API\n\n```bash\n# Add IP route\ncurl -X POST \"https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}/routes\" \\\n  -H \"Authorization: Bearer ${CF_API_TOKEN}\" \\\n  --data '{\"ip_network\": \"10.0.0.0/8\"}'\n\n# List IP routes\ncurl -X GET \"https://api.cloudflare.com/client/v4/accounts/{account_id}/tunnels/{tunnel_id}/routes\" \\\n  -H \"Authorization: Bearer ${CF_API_TOKEN}\"\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/tunnel/configuration.md",
    "content": "# Tunnel Configuration\n\n## Config Source\n\nTunnels use one of two config sources:\n\n| Config Source | Storage | Updates | Use Case |\n|---------------|---------|---------|----------|\n| Local | `config.yml` file | Edit file, restart | Dev, multi-env, version control |\n| Cloudflare | Dashboard/API | Instant, no restart | Production, centralized management |\n\n**Token-based tunnels** = config source: Cloudflare\n**Locally-managed tunnels** = config source: local\n\n## Config File Location\n\n```\n~/.cloudflared/config.yml          # User config\n/etc/cloudflared/config.yml        # System-wide (Linux)\n```\n\n## Basic Structure\n\n```yaml\ntunnel: <UUID>\ncredentials-file: /path/to/<UUID>.json\n\ningress:\n  - hostname: app.example.com\n    service: http://localhost:8000\n  - service: http_status:404  # Required catch-all\n```\n\n## Ingress Rules\n\nRules evaluated **top to bottom**, first match wins.\n\n```yaml\ningress:\n  # Exact hostname + path regex\n  - hostname: static.example.com\n    path: \\.(jpg|png|css|js)$\n    service: https://localhost:8001\n  \n  # Wildcard hostname\n  - hostname: \"*.example.com\"\n    service: https://localhost:8002\n  \n  # Path only (all hostnames)\n  - path: /api/.*\n    service: http://localhost:9000\n  \n  # Catch-all (required)\n  - service: http_status:404\n```\n\n**Validation**:\n```bash\ncloudflared tunnel ingress validate\ncloudflared tunnel ingress rule https://foo.example.com\n```\n\n## Service Types\n\n| Protocol | Format | Client Requirement |\n|----------|--------|-------------------|\n| HTTP | `http://localhost:8000` | Browser |\n| HTTPS | `https://localhost:8443` | Browser |\n| TCP | `tcp://localhost:2222` | `cloudflared access tcp` |\n| SSH | `ssh://localhost:22` | `cloudflared access ssh` |\n| RDP | `rdp://localhost:3389` | `cloudflared access rdp` |\n| Unix | `unix:/path/to/socket` | Browser |\n| Test | `hello_world` | Browser |\n\n## Origin Configuration\n\n### Connection Settings\n```yaml\noriginRequest:\n  connectTimeout: 30s\n  tlsTimeout: 10s\n  tcpKeepAlive: 30s\n  keepAliveTimeout: 90s\n  keepAliveConnections: 100\n```\n\n### TLS Settings\n```yaml\noriginRequest:\n  noTLSVerify: true                      # Disable cert verification\n  originServerName: \"app.internal\"       # Override SNI\n  caPool: /path/to/ca.pem                # Custom CA\n```\n\n### HTTP Settings\n```yaml\noriginRequest:\n  disableChunkedEncoding: true\n  httpHostHeader: \"app.internal\"\n  http2Origin: true\n```\n\n## Private Network Mode\n\n```yaml\ntunnel: <UUID>\ncredentials-file: /path/to/creds.json\n\nwarp-routing:\n  enabled: true\n```\n\n```bash\ncloudflared tunnel route ip add 10.0.0.0/8 my-tunnel\ncloudflared tunnel route ip add 192.168.1.100/32 my-tunnel\n```\n\n## Config Source Comparison\n\n### Local Config\n```yaml\n# config.yml\ntunnel: <UUID>\ncredentials-file: /path/to/<UUID>.json\n\ningress:\n  - hostname: app.example.com\n    service: http://localhost:8000\n  - service: http_status:404\n```\n\n```bash\ncloudflared tunnel run my-tunnel\n```\n\n**Pros:** Version control, multi-environment, offline edits\n**Cons:** Requires file distribution, manual restarts\n\n### Cloudflare Config (Token-Based)\n```bash\n# No config file needed\ncloudflared tunnel --no-autoupdate run --token <TOKEN>\n```\n\nConfigure routes in dashboard: **Zero Trust** > **Networks** > **Tunnels** > [Tunnel] > **Public Hostname**\n\n**Pros:** Centralized updates, no file management, instant route changes\n**Cons:** Requires dashboard/API access, less portable\n\n## Environment Variables\n\n```bash\nTUNNEL_TOKEN=<token>                    # Token for config source: cloudflare\nTUNNEL_ORIGIN_CERT=/path/to/cert.pem   # Override cert path (local config)\nNO_AUTOUPDATE=true                      # Disable auto-updates\nTUNNEL_LOGLEVEL=debug                   # Log level\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/tunnel/gotchas.md",
    "content": "# Tunnel Gotchas\n\n## Common Errors\n\n### \"Error 1016 (Origin DNS Error)\"\n\n**Cause:** Tunnel not running or not connected\n**Solution:**\n```bash\ncloudflared tunnel info my-tunnel     # Check status\nps aux | grep cloudflared             # Verify running\njournalctl -u cloudflared -n 100      # Check logs\n```\n\n### \"Self-signed certificate rejected\"\n\n**Cause:** Origin using self-signed certificate\n**Solution:**\n```yaml\noriginRequest:\n  noTLSVerify: true      # Dev only\n  caPool: /path/to/ca.pem  # Custom CA\n```\n\n### \"Connection timeout\"\n\n**Cause:** Origin slow to respond or timeout settings too low\n**Solution:**\n```yaml\noriginRequest:\n  connectTimeout: 60s\n  tlsTimeout: 20s\n  keepAliveTimeout: 120s\n```\n\n### \"Tunnel not starting\"\n\n**Cause:** Invalid config, missing credentials, or tunnel doesn't exist\n**Solution:**\n```bash\ncloudflared tunnel ingress validate  # Validate config\nls -la ~/.cloudflared/*.json         # Verify credentials\ncloudflared tunnel list              # Verify tunnel exists\n```\n\n### \"Connection already registered\"\n\n**Cause:** Multiple replicas with same connector ID or stale connection\n**Solution:**\n```bash\n# Check active connections\ncloudflared tunnel info my-tunnel\n\n# Wait 60s for stale connection cleanup, or restart with new connector ID\ncloudflared tunnel run my-tunnel\n```\n\n### \"Tunnel credentials rotated but connections fail\"\n\n**Cause:** Old cloudflared processes using expired credentials\n**Solution:**\n```bash\n# Stop all cloudflared processes\npkill cloudflared\n\n# Verify stopped\nps aux | grep cloudflared\n\n# Restart with new credentials\ncloudflared tunnel run my-tunnel\n```\n\n## Limits\n\n| Resource/Limit | Value | Notes |\n|----------------|-------|-------|\n| Free tier | Unlimited tunnels | Unlimited traffic |\n| Tunnel replicas | 1000 per tunnel | Max concurrent |\n| Connection duration | No hard limit | Hours to days |\n| Long-lived connections | May drop during updates | WebSocket, SSH, UDP |\n| Replica registration | ~5s TTL | Old replica dropped after 5s no heartbeat |\n| Token rotation grace | 24 hours | Old tokens work during grace period |\n\n## Best Practices\n\n### Security\n1. Use token-based tunnels (config source: cloudflare) for centralized control\n2. Enable Access policies for sensitive services\n3. Rotate tunnel credentials regularly\n4. After rotation: stop all old cloudflared processes within 24h grace period\n5. Verify TLS certs (`noTLSVerify: false`)\n6. Restrict `bastion` service type\n\n### Performance\n1. Run multiple replicas for HA (2-4 typical, load balanced automatically)\n2. Replicas share same tunnel UUID, get unique connector IDs\n3. Place `cloudflared` close to origin (same network)\n4. Use HTTP/2 for gRPC (`http2Origin: true`)\n5. Tune keepalive for long-lived connections\n6. Monitor connection counts\n\n### Configuration\n1. Use environment variables for secrets\n2. Version control config files\n3. Validate before deploying (`cloudflared tunnel ingress validate`)\n4. Test rules (`cloudflared tunnel ingress rule <URL>`)\n5. Document rule order (first match wins)\n\n### Operations\n1. Monitor tunnel health in dashboard (shows active replicas)\n2. Set up disconnect alerts (when replica count drops to 0)\n3. Graceful shutdown for config updates\n4. Update replicas in rolling fashion (update 1, wait, update next)\n5. Keep `cloudflared` updated (1 year support window)\n6. Use `--no-autoupdate` in prod; control updates manually\n\n## Debug Mode\n\n```bash\ncloudflared tunnel --loglevel debug run my-tunnel\ncloudflared tunnel ingress rule https://app.example.com\n```\n\n## Migration Strategies\n\n### From Ngrok\n```yaml\n# Ngrok: ngrok http 8000\n# Cloudflare Tunnel:\ningress:\n  - hostname: app.example.com\n    service: http://localhost:8000\n  - service: http_status:404\n```\n\n### From VPN\n```yaml\n# Replace VPN with private network routing\nwarp-routing:\n  enabled: true\n```\n\n```bash\ncloudflared tunnel route ip add 10.0.0.0/8 my-tunnel\n```\n\nUsers install WARP client instead of VPN.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/tunnel/networking.md",
    "content": "# Tunnel Networking\n\n## Connectivity Requirements\n\n### Outbound Ports\n\nCloudflared requires outbound access on:\n\n| Port | Protocol | Purpose | Required |\n|------|----------|---------|----------|\n| 7844 | TCP/UDP | Primary tunnel protocol (QUIC) | Yes |\n| 443 | TCP | Fallback (HTTP/2) | Yes |\n\n**Network path:**\n```\ncloudflared → edge.argotunnel.com:7844 (preferred)\ncloudflared → region.argotunnel.com:443 (fallback)\n```\n\n### Firewall Rules\n\n#### Minimal (Production)\n```bash\n# Outbound only\nALLOW tcp/udp 7844 to *.argotunnel.com\nALLOW tcp 443 to *.argotunnel.com\n```\n\n#### Full (Recommended)\n```bash\n# Tunnel connectivity\nALLOW tcp/udp 7844 to *.argotunnel.com\nALLOW tcp 443 to *.argotunnel.com\n\n# API access (for token-based tunnels)\nALLOW tcp 443 to api.cloudflare.com\n\n# Updates (optional)\nALLOW tcp 443 to github.com\nALLOW tcp 443 to objects.githubusercontent.com\n```\n\n### IP Ranges\n\nCloudflare Anycast IPs (tunnel endpoints):\n```\n# IPv4\n198.41.192.0/24\n198.41.200.0/24\n\n# IPv6\n2606:4700::/32\n```\n\n**Note:** Use DNS resolution for `*.argotunnel.com` rather than hardcoding IPs. Cloudflare may add edge locations.\n\n## Pre-Flight Check\n\nTest connectivity before deploying:\n\n```bash\n# Test DNS resolution\ndig edge.argotunnel.com +short\n\n# Test port 7844 (QUIC/UDP)\nnc -zvu edge.argotunnel.com 7844\n\n# Test port 443 (HTTP/2 fallback)\nnc -zv edge.argotunnel.com 443\n\n# Test with cloudflared\ncloudflared tunnel --loglevel debug run my-tunnel\n# Look for \"Registered tunnel connection\"\n```\n\n### Common Connectivity Errors\n\n| Error | Cause | Solution |\n|-------|-------|----------|\n| \"no such host\" | DNS blocked | Allow port 53 UDP/TCP |\n| \"context deadline exceeded\" | Port 7844 blocked | Allow UDP/TCP 7844 |\n| \"TLS handshake timeout\" | Port 443 blocked | Allow TCP 443, disable SSL inspection |\n\n## Protocol Selection\n\nCloudflared automatically selects protocol:\n\n| Protocol | Port | Priority | Use Case |\n|----------|------|----------|----------|\n| QUIC | 7844 UDP | 1st (preferred) | Low latency, best performance |\n| HTTP/2 | 443 TCP | 2nd (fallback) | QUIC blocked by firewall |\n\n**Force HTTP/2 fallback:**\n```bash\ncloudflared tunnel --protocol http2 run my-tunnel\n```\n\n**Verify active protocol:**\n```bash\ncloudflared tunnel info my-tunnel\n# Shows \"connections\" with protocol type\n```\n\n## Private Network Routing\n\n### WARP Client Requirements\n\nUsers accessing private IPs via WARP need:\n\n```bash\n# Outbound (WARP client)\nALLOW udp 500,4500 to 162.159.*.* (IPsec)\nALLOW udp 2408 to 162.159.*.* (WireGuard)\nALLOW tcp 443 to *.cloudflareclient.com\n```\n\n### Split Tunnel Configuration\n\nRoute only private networks through tunnel:\n\n```yaml\n# warp-routing config\nwarp-routing:\n  enabled: true\n```\n\n```bash\n# Add specific routes\ncloudflared tunnel route ip add 10.0.0.0/8 my-tunnel\ncloudflared tunnel route ip add 172.16.0.0/12 my-tunnel\ncloudflared tunnel route ip add 192.168.0.0/16 my-tunnel\n```\n\nWARP users can access these IPs without VPN.\n\n## Network Diagnostics\n\n### Connection Diagnostics\n\n```bash\n# Check edge selection and connection health\ncloudflared tunnel info my-tunnel --output json | jq '.connections[]'\n\n# Enable metrics endpoint\ncloudflared tunnel --metrics localhost:9090 run my-tunnel\ncurl localhost:9090/metrics | grep cloudflared_tunnel\n\n# Test latency\ncurl -w \"time_total: %{time_total}\\n\" -o /dev/null https://myapp.example.com\n```\n\n## Corporate Network Considerations\n\nCloudflared honors proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`).\n\nIf corporate proxy intercepts TLS, add corporate root CA to system trust store.\n\n## Bandwidth and Rate Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Request size | 100 MB | Single HTTP request |\n| Upload speed | No hard limit | Governed by network/plan |\n| Concurrent connections | 1000 per tunnel | Across all replicas |\n| Requests per second | No limit | Subject to DDoS detection |\n\n**Large file transfers:**\nUse R2 or Workers with chunked uploads instead of streaming through tunnel.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/tunnel/patterns.md",
    "content": "# Tunnel Patterns\n\n## Docker Deployment\n\n### Token-Based (Recommended)\n```yaml\nservices:\n  cloudflared:\n    image: cloudflare/cloudflared:latest\n    command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}\n    restart: unless-stopped\n```\n\n### Local Config\n```yaml\nservices:\n  cloudflared:\n    image: cloudflare/cloudflared:latest\n    volumes:\n      - ./config.yml:/etc/cloudflared/config.yml:ro\n      - ./credentials.json:/etc/cloudflared/credentials.json:ro\n    command: tunnel run\n```\n\n## Kubernetes Deployment\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: cloudflared\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: cloudflared\n  template:\n    metadata:\n      labels:\n        app: cloudflared\n    spec:\n      containers:\n      - name: cloudflared\n        image: cloudflare/cloudflared:latest\n        args:\n        - tunnel\n        - --no-autoupdate\n        - run\n        - --token\n        - $(TUNNEL_TOKEN)\n        env:\n        - name: TUNNEL_TOKEN\n          valueFrom:\n            secretKeyRef:\n              name: tunnel-credentials\n              key: token\n```\n\n## High Availability\n\n```yaml\n# Same config on multiple servers\ntunnel: <UUID>\ncredentials-file: /path/to/creds.json\n\ningress:\n  - hostname: app.example.com\n    service: http://localhost:8000\n  - service: http_status:404\n```\n\nRun same config on multiple machines. Cloudflare automatically load balances. Long-lived connections (WebSocket, SSH) may drop during updates.\n\n## Use Cases\n\n### Web Application\n```yaml\ningress:\n  - hostname: myapp.example.com\n    service: http://localhost:3000\n  - service: http_status:404\n```\n\n### SSH Access\n```yaml\ningress:\n  - hostname: ssh.example.com\n    service: ssh://localhost:22\n  - service: http_status:404\n```\n\nClient: `cloudflared access ssh --hostname ssh.example.com`\n\n### gRPC Service\n```yaml\ningress:\n  - hostname: grpc.example.com\n    service: http://localhost:50051\n    originRequest:\n      http2Origin: true\n  - service: http_status:404\n```\n\n## Infrastructure as Code\n\n### Terraform\n\n```hcl\nresource \"random_id\" \"tunnel_secret\" {\n  byte_length = 32\n}\n\nresource \"cloudflare_tunnel\" \"app\" {\n  account_id = var.cloudflare_account_id\n  name       = \"app-tunnel\"\n  secret     = random_id.tunnel_secret.b64_std\n}\n\nresource \"cloudflare_tunnel_config\" \"app\" {\n  account_id = var.cloudflare_account_id\n  tunnel_id  = cloudflare_tunnel.app.id\n  config {\n    ingress_rule {\n      hostname = \"app.example.com\"\n      service  = \"http://localhost:8000\"\n    }\n    ingress_rule { service = \"http_status:404\" }\n  }\n}\n\nresource \"cloudflare_record\" \"app\" {\n  zone_id = var.cloudflare_zone_id\n  name    = \"app\"\n  value   = cloudflare_tunnel.app.cname\n  type    = \"CNAME\"\n  proxied = true\n}\n\noutput \"tunnel_token\" {\n  value     = cloudflare_tunnel.app.tunnel_token\n  sensitive = true\n}\n```\n\n### Pulumi\n\n```typescript\nimport * as cloudflare from \"@pulumi/cloudflare\";\nimport * as random from \"@pulumi/random\";\n\nconst secret = new random.RandomId(\"secret\", { byteLength: 32 });\n\nconst tunnel = new cloudflare.ZeroTrustTunnelCloudflared(\"tunnel\", {\n  accountId: accountId,\n  name: \"app-tunnel\",\n  secret: secret.b64Std,\n});\n\nconst config = new cloudflare.ZeroTrustTunnelCloudflaredConfig(\"config\", {\n  accountId: accountId,\n  tunnelId: tunnel.id,\n  config: {\n    ingressRules: [\n      { hostname: \"app.example.com\", service: \"http://localhost:8000\" },\n      { service: \"http_status:404\" },\n    ],\n  },\n});\n\nnew cloudflare.Record(\"dns\", {\n  zoneId: zoneId,\n  name: \"app\",\n  value: tunnel.cname,\n  type: \"CNAME\",\n  proxied: true,\n});\n```\n\n## Service Installation\n\n### Linux systemd\n```bash\ncloudflared service install\nsystemctl start cloudflared && systemctl enable cloudflared\njournalctl -u cloudflared -f  # Logs\n```\n\n### macOS launchd\n```bash\nsudo cloudflared service install\nsudo launchctl start com.cloudflare.cloudflared\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/turn/README.md",
    "content": "# Cloudflare TURN Service\n\nExpert guidance for implementing Cloudflare TURN Service in WebRTC applications.\n\n## Overview\n\nCloudflare TURN (Traversal Using Relays around NAT) Service is a managed relay service for WebRTC applications. TURN acts as a relay point for traffic between WebRTC clients and SFUs, particularly when direct peer-to-peer communication is obstructed by NATs or firewalls. The service runs on Cloudflare's global anycast network across 310+ cities.\n\n## Key Characteristics\n\n- **Anycast Architecture**: Automatically connects clients to the closest Cloudflare location\n- **Global Network**: Available across Cloudflare's entire network (excluding China Network)\n- **Zero Configuration**: No need to manually select regions or servers\n- **Protocol Support**: STUN/TURN over UDP, TCP, and TLS\n- **Free Tier**: Free when used with Cloudflare Calls SFU, otherwise $0.05/GB outbound\n\n## In This Reference\n\n| File | Purpose |\n|------|---------|\n| [api.md](./api.md) | Credentials API, TURN key management, types, constraints |\n| [configuration.md](./configuration.md) | Worker setup, wrangler.jsonc, env vars, IP allowlisting |\n| [patterns.md](./patterns.md) | Implementation patterns, use cases, integration examples |\n| [gotchas.md](./gotchas.md) | Troubleshooting, limits, security, common mistakes |\n\n## Reading Order\n\n| Task | Files to Read | Est. Tokens |\n|------|---------------|-------------|\n| Quick start | README only | ~500 |\n| Generate credentials | README → api | ~1300 |\n| Worker integration | README → configuration → patterns | ~2000 |\n| Debug connection | gotchas | ~700 |\n| Security review | api → gotchas | ~1500 |\n| Enterprise firewall | configuration | ~600 |\n\n## Service Addresses and Ports\n\n### STUN over UDP\n- **Primary**: `stun.cloudflare.com:3478/udp`\n- **Alternate**: `stun.cloudflare.com:53/udp` (blocked by browsers, not recommended)\n\n### TURN over UDP\n- **Primary**: `turn.cloudflare.com:3478/udp`\n- **Alternate**: `turn.cloudflare.com:53/udp` (blocked by browsers)\n\n### TURN over TCP\n- **Primary**: `turn.cloudflare.com:3478/tcp`\n- **Alternate**: `turn.cloudflare.com:80/tcp`\n\n### TURN over TLS\n- **Primary**: `turn.cloudflare.com:5349/tcp`\n- **Alternate**: `turn.cloudflare.com:443/tcp`\n\n## Quick Start\n\n1. **Create TURN key via API**: see [api.md#create-turn-key](./api.md#create-turn-key)\n2. **Generate credentials**: see [api.md#generate-temporary-credentials](./api.md#generate-temporary-credentials)\n3. **Configure Worker**: see [configuration.md#cloudflare-worker-integration](./configuration.md#cloudflare-worker-integration)\n4. **Implement client**: see [patterns.md#basic-turn-configuration-browser](./patterns.md#basic-turn-configuration-browser)\n\n## When to Use TURN\n\n- **Restrictive NATs**: Symmetric NATs that block direct connections\n- **Corporate firewalls**: Environments blocking WebRTC ports\n- **Mobile networks**: Carrier-grade NAT scenarios\n- **Predictable connectivity**: When reliability > efficiency\n\n## Related Cloudflare Services\n\n- **Cloudflare Calls SFU**: Managed Selective Forwarding Unit (TURN free when used with SFU)\n- **Cloudflare Stream**: Video streaming with WHIP/WHEP support\n- **Cloudflare Workers**: Backend for credential generation\n- **Cloudflare KV**: Credential caching\n- **Cloudflare Durable Objects**: Session state management\n\n## Additional Resources\n\n- [Cloudflare Calls Documentation](https://developers.cloudflare.com/calls/)\n- [Cloudflare TURN Service Docs](https://developers.cloudflare.com/realtime/turn/)\n- [Cloudflare API Reference](https://developers.cloudflare.com/api/resources/calls/subresources/turn/)\n- [Orange Meets (Open Source Example)](https://github.com/cloudflare/orange)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/turn/api.md",
    "content": "# TURN API Reference\n\nComplete API documentation for Cloudflare TURN service credentials and key management.\n\n## Authentication\n\nAll endpoints require Cloudflare API token with \"Calls Write\" permission.\n\nBase URL: `https://api.cloudflare.com/client/v4`\n\n## TURN Key Management\n\n### List TURN Keys\n\n```\nGET /accounts/{account_id}/calls/turn_keys\n```\n\n### Get TURN Key Details\n\n```\nGET /accounts/{account_id}/calls/turn_keys/{key_id}\n```\n\n### Create TURN Key\n\n```\nPOST /accounts/{account_id}/calls/turn_keys\nContent-Type: application/json\n\n{\n  \"name\": \"my-turn-key\"\n}\n```\n\n**Response includes**:\n- `uid`: Key identifier\n- `key`: The actual secret key (only returned on creation—save immediately)\n- `name`: Human-readable name\n- `created`: ISO 8601 timestamp\n- `modified`: ISO 8601 timestamp\n\n### Update TURN Key\n\n```\nPUT /accounts/{account_id}/calls/turn_keys/{key_id}\nContent-Type: application/json\n\n{\n  \"name\": \"updated-name\"\n}\n```\n\n### Delete TURN Key\n\n```\nDELETE /accounts/{account_id}/calls/turn_keys/{key_id}\n```\n\n## Generate Temporary Credentials\n\n```\nPOST https://rtc.live.cloudflare.com/v1/turn/keys/{key_id}/credentials/generate\nAuthorization: Bearer {key_secret}\nContent-Type: application/json\n\n{\n  \"ttl\": 86400\n}\n```\n\n### Credential Constraints\n\n| Parameter | Min | Max | Default | Notes |\n|-----------|-----|-----|---------|-------|\n| ttl | 1 | 172800 (48hrs) | varies | API rejects values >172800 |\n\n**CRITICAL**: Maximum TTL is 48 hours (172800 seconds). API will reject requests exceeding this limit.\n\n### Response Schema\n\n```json\n{\n  \"iceServers\": {\n    \"urls\": [\n      \"stun:stun.cloudflare.com:3478\",\n      \"turn:turn.cloudflare.com:3478?transport=udp\",\n      \"turn:turn.cloudflare.com:3478?transport=tcp\",\n      \"turn:turn.cloudflare.com:53?transport=udp\",\n      \"turn:turn.cloudflare.com:80?transport=tcp\",\n      \"turns:turn.cloudflare.com:5349?transport=tcp\",\n      \"turns:turn.cloudflare.com:443?transport=tcp\"\n    ],\n    \"username\": \"1738035200:user123\",\n    \"credential\": \"base64encodedhmac==\"\n  }\n}\n```\n\n**Port 53 Warning**: Filter port 53 URLs for browser clients—blocked by Chrome/Firefox. See [gotchas.md](./gotchas.md#using-port-53-in-browsers).\n\n## Revoke Credentials\n\n```\nPOST https://rtc.live.cloudflare.com/v1/turn/keys/{key_id}/credentials/revoke\nAuthorization: Bearer {key_secret}\nContent-Type: application/json\n\n{\n  \"username\": \"1738035200:user123\"\n}\n```\n\n**Response**: 204 No Content\n\nBilling stops immediately. Active connection drops after short delay (~seconds).\n\n## TypeScript Types\n\n```typescript\ninterface CloudflareTURNConfig {\n  keyId: string;\n  keySecret: string;\n  ttl?: number; // Max 172800 (48 hours)\n}\n\ninterface TURNCredentialsRequest {\n  ttl?: number; // Max 172800 seconds\n}\n\ninterface TURNCredentialsResponse {\n  iceServers: {\n    urls: string[];\n    username: string;\n    credential: string;\n  };\n}\n\ninterface RTCIceServer {\n  urls: string | string[];\n  username?: string;\n  credential?: string;\n  credentialType?: \"password\";\n}\n\ninterface TURNKeyResponse {\n  uid: string;\n  key: string; // Only present on creation\n  name: string;\n  created: string;\n  modified: string;\n}\n```\n\n## Validation Function\n\n```typescript\nfunction validateRTCIceServer(obj: unknown): obj is RTCIceServer {\n  if (!obj || typeof obj !== 'object') {\n    return false;\n  }\n\n  const server = obj as Record<string, unknown>;\n\n  if (typeof server.urls !== 'string' && !Array.isArray(server.urls)) {\n    return false;\n  }\n\n  if (server.username && typeof server.username !== 'string') {\n    return false;\n  }\n\n  if (server.credential && typeof server.credential !== 'string') {\n    return false;\n  }\n\n  return true;\n}\n```\n\n## Type-Safe Credential Generation\n\n```typescript\nasync function fetchTURNServers(\n  config: CloudflareTURNConfig\n): Promise<RTCIceServer[]> {\n  // Validate TTL constraint\n  const ttl = config.ttl ?? 3600;\n  if (ttl > 172800) {\n    throw new Error('TTL cannot exceed 172800 seconds (48 hours)');\n  }\n\n  const response = await fetch(\n    `https://rtc.live.cloudflare.com/v1/turn/keys/${config.keyId}/credentials/generate`,\n    {\n      method: 'POST',\n      headers: {\n        'Authorization': `Bearer ${config.keySecret}`,\n        'Content-Type': 'application/json'\n      },\n      body: JSON.stringify({ ttl })\n    }\n  );\n\n  if (!response.ok) {\n    throw new Error(`TURN credential generation failed: ${response.status}`);\n  }\n\n  const data = await response.json();\n  \n  // Filter port 53 for browser clients\n  const filteredUrls = data.iceServers.urls.filter(\n    (url: string) => !url.includes(':53')\n  );\n\n  const iceServers = [\n    { urls: 'stun:stun.cloudflare.com:3478' },\n    {\n      urls: filteredUrls,\n      username: data.iceServers.username,\n      credential: data.iceServers.credential,\n      credentialType: 'password' as const\n    }\n  ];\n\n  // Validate before returning\n  if (!iceServers.every(validateRTCIceServer)) {\n    throw new Error('Invalid ICE server configuration received');\n  }\n\n  return iceServers;\n}\n```\n\n## See Also\n\n- [configuration.md](./configuration.md) - Worker setup, environment variables\n- [patterns.md](./patterns.md) - Implementation examples using these APIs\n- [gotchas.md](./gotchas.md) - Security best practices, common mistakes\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/turn/configuration.md",
    "content": "# TURN Configuration\n\nSetup and configuration for Cloudflare TURN service in Workers and applications.\n\n## Environment Variables\n\n```bash\n# .env\nCLOUDFLARE_ACCOUNT_ID=your_account_id\nCLOUDFLARE_API_TOKEN=your_api_token\nTURN_KEY_ID=your_turn_key_id\nTURN_KEY_SECRET=your_turn_key_secret\n```\n\nValidate with zod:\n\n```typescript\nimport { z } from 'zod';\n\nconst envSchema = z.object({\n  CLOUDFLARE_ACCOUNT_ID: z.string().min(1),\n  CLOUDFLARE_API_TOKEN: z.string().min(1),\n  TURN_KEY_ID: z.string().min(1),\n  TURN_KEY_SECRET: z.string().min(1)\n});\n\nexport const config = envSchema.parse(process.env);\n```\n\n## wrangler.jsonc\n\n```jsonc\n{\n  \"name\": \"turn-credentials-api\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\",\n  \"vars\": {\n    \"TURN_KEY_ID\": \"your-turn-key-id\"  // Non-sensitive, can be in vars\n  },\n  \"env\": {\n    \"production\": {\n      \"kv_namespaces\": [\n        {\n          \"binding\": \"CREDENTIALS_CACHE\",\n          \"id\": \"your-kv-namespace-id\"\n        }\n      ]\n    }\n  }\n}\n```\n\n**Store secrets separately**:\n```bash\nwrangler secret put TURN_KEY_SECRET\n```\n\n## Cloudflare Worker Integration\n\n### Worker Binding Types\n\n```typescript\ninterface Env {\n  TURN_KEY_ID: string;\n  TURN_KEY_SECRET: string;\n  CREDENTIALS_CACHE?: KVNamespace;\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // See patterns.md for implementation\n  }\n}\n```\n\n### Basic Worker Example\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    if (request.url.endsWith('/turn-credentials')) {\n      // Validate client auth\n      const authHeader = request.headers.get('Authorization');\n      if (!authHeader) {\n        return new Response('Unauthorized', { status: 401 });\n      }\n\n      const response = await fetch(\n        `https://rtc.live.cloudflare.com/v1/turn/keys/${env.TURN_KEY_ID}/credentials/generate`,\n        {\n          method: 'POST',\n          headers: {\n            'Authorization': `Bearer ${env.TURN_KEY_SECRET}`,\n            'Content-Type': 'application/json'\n          },\n          body: JSON.stringify({ ttl: 3600 })\n        }\n      );\n\n      if (!response.ok) {\n        return new Response('Failed to generate credentials', { status: 500 });\n      }\n\n      const data = await response.json();\n\n      // Filter port 53 for browser clients\n      const filteredUrls = data.iceServers.urls.filter(\n        (url: string) => !url.includes(':53')\n      );\n\n      return Response.json({\n        iceServers: [\n          { urls: 'stun:stun.cloudflare.com:3478' },\n          {\n            urls: filteredUrls,\n            username: data.iceServers.username,\n            credential: data.iceServers.credential\n          }\n        ]\n      });\n    }\n\n    return new Response('Not found', { status: 404 });\n  }\n};\n```\n\n## IP Allowlisting (Enterprise/Firewall)\n\nFor strict firewalls, allowlist these IPs for `turn.cloudflare.com`:\n\n| Type | Address | Protocol |\n|------|---------|----------|\n| IPv4 | 141.101.90.1/32 | All |\n| IPv4 | 162.159.207.1/32 | All |\n| IPv6 | 2a06:98c1:3200::1/128 | All |\n| IPv6 | 2606:4700:48::1/128 | All |\n\n**IMPORTANT**: These IPs may change with 14-day notice. Monitor DNS:\n\n```bash\n# Check A and AAAA records\ndig turn.cloudflare.com A\ndig turn.cloudflare.com AAAA\n```\n\nSet up automated monitoring to detect IP changes and update allowlists within 14 days.\n\n## IPv6 Support\n\n- **Client-to-TURN**: Both IPv4 and IPv6 supported\n- **Relay addresses**: IPv4 only (no RFC 6156 support)\n- **TCP relaying**: Not supported (RFC 6062)\n\nClients can connect via IPv6, but relayed traffic uses IPv4 addresses.\n\n## TLS Configuration\n\n### Supported TLS Versions\n- TLS 1.1\n- TLS 1.2\n- TLS 1.3\n\n### Recommended Ciphers (TLS 1.3)\n- AEAD-AES128-GCM-SHA256\n- AEAD-AES256-GCM-SHA384\n- AEAD-CHACHA20-POLY1305-SHA256\n\n### Recommended Ciphers (TLS 1.2)\n- ECDHE-ECDSA-AES128-GCM-SHA256\n- ECDHE-RSA-AES128-GCM-SHA256\n- ECDHE-RSA-AES128-SHA (also TLS 1.1)\n- AES128-GCM-SHA256\n\n## See Also\n\n- [api.md](./api.md) - TURN key creation, credential generation API\n- [patterns.md](./patterns.md) - Full Worker implementation patterns\n- [gotchas.md](./gotchas.md) - Security best practices, troubleshooting\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/turn/gotchas.md",
    "content": "# TURN Gotchas & Troubleshooting\n\nCommon mistakes, security best practices, and troubleshooting for Cloudflare TURN.\n\n## Quick Reference\n\n| Issue | Solution | Details |\n|-------|----------|---------|\n| Credentials not working | Check TTL ≤ 48hrs | [See Troubleshooting](#issue-turn-credentials-not-working) |\n| Connection drops after ~48hrs | Implement credential refresh | [See Connection Drops](#issue-connection-drops-after-48-hours) |\n| Port 53 fails in browser | Filter server-side | [See Port 53](#using-port-53-in-browsers) |\n| High packet loss | Check rate limits | [See Rate Limits](#limits-per-turn-allocation) |\n| Connection fails after maintenance | Implement ICE restart | [See ICE Restart](#ice-restart-required-scenarios) |\n\n## Critical Constraints\n\n| Constraint | Value | Consequence if Violated |\n|------------|-------|-------------------------|\n| Max credential TTL | 48 hours (172800s) | API rejects request |\n| Credential revocation delay | ~seconds | Billing stops immediately, connection drops shortly |\n| IP allowlist update window | 14 days (if IPs change) | Connection fails if IPs change |\n| Packet rate | 5-10k pps per allocation | Packet drops |\n| Data rate | 50-100 Mbps per allocation | Packet drops |\n| Unique IP rate | >5 new IPs/sec | Packet drops |\n\n## Limits Per TURN Allocation\n\n**Per user** (not account-wide):\n\n- **IP addresses**: >5 new unique IPs per second\n- **Packet rate**: 5-10k packets per second (inbound/outbound)\n- **Data rate**: 50-100 Mbps (inbound/outbound)\n- **MTU**: No specific limit\n- **Burst rates**: Higher than documented\n\nExceeding limits results in **packet drops**.\n\n## Common Mistakes\n\n### Setting TTL > 48 hours\n\n```typescript\n// ❌ BAD: API will reject\nconst creds = await generate({ ttl: 604800 });  // 7 days\n\n// ✅ GOOD:\nconst creds = await generate({ ttl: 86400 });   // 24 hours\n```\n\n### Hardcoding IPs without monitoring\n\n```typescript\n// ❌ BAD: IPs can change with 14-day notice\nconst iceServers = [{ urls: 'turn:141.101.90.1:3478' }];\n\n// ✅ GOOD: Use DNS\nconst iceServers = [{ urls: 'turn:turn.cloudflare.com:3478' }];\n```\n\n### Using port 53 in browsers\n\n```typescript\n// ❌ BAD: Blocked by Chrome/Firefox\nurls: ['turn:turn.cloudflare.com:53']\n\n// ✅ GOOD: Filter port 53\nurls: urls.filter(url => !url.includes(':53'))\n```\n\n### Not handling credential expiry\n\n```typescript\n// ❌ BAD: Credentials expire but call continues → connection drops\nconst creds = await fetchCreds();\nconst pc = new RTCPeerConnection({ iceServers: creds });\n\n// ✅ GOOD: Refresh before expiry\nsetInterval(() => refreshCredentials(pc), 3000000);  // 50 min\n```\n\n### Missing ICE restart support\n\n```typescript\n// ❌ BAD: No recovery from TURN maintenance\npc.addEventListener('iceconnectionstatechange', () => {\n  console.log('State changed:', pc.iceConnectionState);\n});\n\n// ✅ GOOD: Implement ICE restart\npc.addEventListener('iceconnectionstatechange', async () => {\n  if (pc.iceConnectionState === 'failed') {\n    await refreshCredentials(pc);\n    pc.restartIce();\n  }\n});\n```\n\n### Exposing TURN key secret client-side\n\n```typescript\n// ❌ BAD: Secret exposed to client\nconst secret = 'your-turn-key-secret';\nconst response = await fetch(`https://rtc.live.cloudflare.com/v1/turn/...`, {\n  headers: { 'Authorization': `Bearer ${secret}` }\n});\n\n// ✅ GOOD: Generate credentials server-side\nconst response = await fetch('/api/turn-credentials');\n```\n\n## ICE Restart Required Scenarios\n\nThese events require ICE restart (see [patterns.md](./patterns.md#ice-restart-pattern)):\n\n1. **TURN server maintenance** (occasional on Cloudflare's network)\n2. **Network topology changes** (anycast routing changes)\n3. **Credential refresh** during long sessions (>1 hour)\n4. **Connection failure** (iceConnectionState === 'failed')\n\nImplement in all production apps:\n\n```typescript\npc.addEventListener('iceconnectionstatechange', async () => {\n  if (pc.iceConnectionState === 'failed' || \n      pc.iceConnectionState === 'disconnected') {\n    await refreshTURNCredentials(pc);\n    pc.restartIce();\n    const offer = await pc.createOffer({ iceRestart: true });\n    await pc.setLocalDescription(offer);\n    // Send offer to peer via signaling...\n  }\n});\n```\n\nReference: [RFC 8445 Section 2.4](https://datatracker.ietf.org/doc/html/rfc8445#section-2.4)\n\n## Security Checklist\n\n- [ ] Credentials generated server-side only (never client-side)\n- [ ] TURN_KEY_SECRET in wrangler secrets, not vars\n- [ ] TTL ≤ expected session duration (and ≤ 48 hours)\n- [ ] Rate limiting on credential generation endpoint\n- [ ] Client authentication before issuing credentials\n- [ ] Credential revocation API for compromised sessions\n- [ ] No hardcoded IPs (or DNS monitoring in place)\n- [ ] Port 53 filtered for browser clients\n\n## Troubleshooting\n\n### Issue: TURN credentials not working\n\n**Check:**\n- Key ID and secret are correct\n- Credentials haven't expired (check TTL)\n- TTL doesn't exceed 172800 seconds (48 hours)\n- Server can reach rtc.live.cloudflare.com\n- Network allows outbound HTTPS\n\n**Solution:**\n```typescript\n// Validate before using\nif (ttl > 172800) {\n  throw new Error('TTL cannot exceed 48 hours');\n}\n```\n\n### Issue: Slow connection establishment\n\n**Solutions:**\n- Ensure proper ICE candidate gathering\n- Check network latency to Cloudflare edge\n- Verify firewall allows WebRTC ports (3478, 5349, 443)\n- Consider using TURN over TLS (port 443) for corporate networks\n\n### Issue: High packet loss\n\n**Check:**\n- Not exceeding rate limits (5-10k pps)\n- Not exceeding bandwidth limits (50-100 Mbps)\n- Not connecting to too many unique IPs (>5/sec)\n- Client network quality\n\n### Issue: Connection drops after ~48 hours\n\n**Cause**: Credentials expired (48hr max)\n\n**Solution**: \n- Set TTL to expected session duration\n- Implement credential refresh with setConfiguration()\n- Use ICE restart if connection fails\n\n```typescript\n// Refresh credentials before expiry\nconst refreshInterval = ttl * 1000 - 60000; // 1 min early\nsetInterval(async () => {\n  await refreshTURNCredentials(pc);\n}, refreshInterval);\n```\n\n### Issue: Port 53 URLs in browser fail silently\n\n**Cause**: Chrome/Firefox block port 53\n\n**Solution**: Filter port 53 URLs server-side:\n\n```typescript\nconst filtered = urls.filter(url => !url.includes(':53'));\n```\n\n### Issue: Hardcoded IPs stop working\n\n**Cause**: Cloudflare changed IP addresses (14-day notice)\n\n**Solution**: \n- Use DNS hostnames (`turn.cloudflare.com`)\n- Monitor DNS changes with automated alerts\n- Update allowlists within 14 days if using IP allowlisting\n\n## Cost Optimization\n\n1. Use appropriate TTLs (don't over-provision)\n2. Implement credential caching\n3. Set `iceTransportPolicy: 'all'` to try direct first (use `'relay'` only when necessary)\n4. Monitor bandwidth usage\n5. Free when used with Cloudflare Calls SFU\n\n## See Also\n\n- [api.md](./api.md) - Credential generation API, revocation\n- [configuration.md](./configuration.md) - IP allowlisting, monitoring\n- [patterns.md](./patterns.md) - ICE restart, credential refresh patterns\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/turn/patterns.md",
    "content": "# TURN Implementation Patterns\n\nProduction-ready patterns for implementing Cloudflare TURN in WebRTC applications.\n\n## Prerequisites\n\nBefore implementing these patterns, ensure you have:\n- TURN key created: see [api.md#create-turn-key](./api.md#create-turn-key)\n- Worker configured: see [configuration.md#cloudflare-worker-integration](./configuration.md#cloudflare-worker-integration)\n\n## Basic TURN Configuration (Browser)\n\n```typescript\ninterface RTCIceServer {\n  urls: string | string[];\n  username?: string;\n  credential?: string;\n  credentialType?: \"password\" | \"oauth\";\n}\n\nasync function getTURNConfig(): Promise<RTCIceServer[]> {\n  const response = await fetch('/api/turn-credentials');\n  const data = await response.json();\n  \n  return [\n    {\n      urls: 'stun:stun.cloudflare.com:3478'\n    },\n    {\n      urls: [\n        'turn:turn.cloudflare.com:3478?transport=udp',\n        'turn:turn.cloudflare.com:3478?transport=tcp',\n        'turns:turn.cloudflare.com:5349?transport=tcp',\n        'turns:turn.cloudflare.com:443?transport=tcp'\n      ],\n      username: data.username,\n      credential: data.credential,\n      credentialType: 'password'\n    }\n  ];\n}\n\n// Use in RTCPeerConnection\nconst iceServers = await getTURNConfig();\nconst peerConnection = new RTCPeerConnection({ iceServers });\n```\n\n## Port Selection Strategy\n\nRecommended order for browser clients:\n\n1. **3478/udp** (primary, lowest latency)\n2. **3478/tcp** (fallback for UDP-blocked networks)\n3. **5349/tls** (corporate firewalls, most reliable)\n4. **443/tls** (alternate TLS port, firewall-friendly)\n\n**Avoid port 53**—blocked by Chrome and Firefox.\n\n```typescript\nfunction filterICEServersForBrowser(urls: string[]): string[] {\n  return urls\n    .filter(url => !url.includes(':53'))  // Remove port 53\n    .sort((a, b) => {\n      // Prioritize UDP over TCP over TLS\n      if (a.includes('transport=udp')) return -1;\n      if (b.includes('transport=udp')) return 1;\n      if (a.includes('transport=tcp') && !a.startsWith('turns:')) return -1;\n      if (b.includes('transport=tcp') && !b.startsWith('turns:')) return 1;\n      return 0;\n    });\n}\n```\n\n## Credential Refresh (Mid-Session)\n\nWhen credentials expire during long calls:\n\n```typescript\nasync function refreshTURNCredentials(pc: RTCPeerConnection): Promise<void> {\n  const newCreds = await fetch('/turn-credentials').then(r => r.json());\n  const config = pc.getConfiguration();\n  config.iceServers = newCreds.iceServers;\n  pc.setConfiguration(config);\n  // Note: setConfiguration() does NOT trigger ICE restart\n  // Combine with restartIce() if connection fails\n}\n\n// Auto-refresh before expiry\nsetInterval(async () => {\n  await refreshTURNCredentials(peerConnection);\n}, 3000000);  // 50 minutes if TTL is 1 hour\n```\n\n## ICE Restart Pattern\n\nAfter network change, TURN server maintenance, or credential expiry:\n\n```typescript\npc.addEventListener('iceconnectionstatechange', async () => {\n  if (pc.iceConnectionState === 'failed') {\n    console.warn('ICE connection failed, restarting...');\n    \n    // Refresh credentials\n    await refreshTURNCredentials(pc);\n    \n    // Trigger ICE restart\n    pc.restartIce();\n    const offer = await pc.createOffer({ iceRestart: true });\n    await pc.setLocalDescription(offer);\n    \n    // Send offer to peer via signaling channel...\n  }\n});\n```\n\n## Credentials Caching Pattern\n\n```typescript\nclass TURNCredentialsManager {\n  private creds: { username: string; credential: string; urls: string[]; expiresAt: number; } | null = null;\n\n  async getCredentials(keyId: string, keySecret: string): Promise<RTCIceServer[]> {\n    const now = Date.now();\n    \n    if (this.creds && this.creds.expiresAt > now) {\n      return this.buildIceServers(this.creds);\n    }\n\n    const ttl = 3600;\n    if (ttl > 172800) throw new Error('TTL max 48hrs');\n\n    const res = await fetch(\n      `https://rtc.live.cloudflare.com/v1/turn/keys/${keyId}/credentials/generate`,\n      {\n        method: 'POST',\n        headers: { 'Authorization': `Bearer ${keySecret}`, 'Content-Type': 'application/json' },\n        body: JSON.stringify({ ttl })\n      }\n    );\n\n    const data = await res.json();\n    const filteredUrls = data.iceServers.urls.filter((url: string) => !url.includes(':53'));\n\n    this.creds = {\n      username: data.iceServers.username,\n      credential: data.iceServers.credential,\n      urls: filteredUrls,\n      expiresAt: now + (ttl * 1000) - 60000\n    };\n\n    return this.buildIceServers(this.creds);\n  }\n\n  private buildIceServers(c: { username: string; credential: string; urls: string[] }): RTCIceServer[] {\n    return [\n      { urls: 'stun:stun.cloudflare.com:3478' },\n      { urls: c.urls, username: c.username, credential: c.credential, credentialType: 'password' as const }\n    ];\n  }\n}\n```\n\n## Common Use Cases\n\n```typescript\n// Video conferencing: TURN as fallback\nconst config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'all' };\n\n// IoT/predictable connectivity: force TURN\nconst config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'relay' };\n\n// Screen sharing: reduce overhead\nconst pc = new RTCPeerConnection({ iceServers: await getTURNConfig(), bundlePolicy: 'max-bundle' });\n```\n\n## Integration with Cloudflare Calls SFU\n\n```typescript\n// TURN is automatically used when needed\n// Cloudflare Calls handles TURN + SFU coordination\nconst session = await callsClient.createSession({\n  appId: 'your-app-id',\n  sessionId: 'meeting-123'\n});\n```\n\n## Debugging ICE Connectivity\n\n```typescript\npc.addEventListener('icecandidate', (event) => {\n  if (event.candidate) {\n    console.log('ICE candidate:', event.candidate.type, event.candidate.protocol);\n  }\n});\n\npc.addEventListener('iceconnectionstatechange', () => {\n  console.log('ICE state:', pc.iceConnectionState);\n});\n\n// Check selected candidate pair\nconst stats = await pc.getStats();\nstats.forEach(report => {\n  if (report.type === 'candidate-pair' && report.selected) {\n    console.log('Selected:', report);\n  }\n});\n```\n\n## See Also\n\n- [api.md](./api.md) - Credential generation API, types\n- [configuration.md](./configuration.md) - Worker setup, environment variables\n- [gotchas.md](./gotchas.md) - Common mistakes, troubleshooting\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/turnstile/README.md",
    "content": "# Cloudflare Turnstile Implementation Skill Reference\n\nExpert guidance for implementing Cloudflare Turnstile - a smart CAPTCHA alternative that protects websites from bots without showing traditional CAPTCHA puzzles.\n\n## Overview\n\nTurnstile is a user-friendly CAPTCHA alternative that runs challenges in the background without user interaction. It validates visitors automatically using signals like browser behavior, device fingerprinting, and machine learning.\n\n## Widget Types\n\n| Type | Interaction | Use Case |\n|------|-------------|----------|\n| **Managed** (default) | Shows checkbox when needed | Forms, logins - balance UX and security |\n| **Non-Interactive** | Invisible, runs automatically | Frictionless UX, low-risk actions |\n| **Invisible** | Hidden, triggered programmatically | Pre-clearance, API calls, headless |\n\n## Quick Start\n\n### Implicit Rendering (HTML-based)\n```html\n<!-- 1. Add script -->\n<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js\" async defer></script>\n\n<!-- 2. Add widget to form -->\n<form action=\"/submit\" method=\"POST\">\n  <div class=\"cf-turnstile\" data-sitekey=\"YOUR_SITE_KEY\"></div>\n  <button type=\"submit\">Submit</button>\n</form>\n```\n\n### Explicit Rendering (JavaScript-based)\n```html\n<div id=\"turnstile-container\"></div>\n<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit\"></script>\n<script>\nwindow.turnstile.render('#turnstile-container', {\n  sitekey: 'YOUR_SITE_KEY',\n  callback: (token) => console.log('Token:', token)\n});\n</script>\n```\n\n### Server Validation (Required)\n```javascript\n// Cloudflare Workers\nexport default {\n  async fetch(request) {\n    const formData = await request.formData();\n    const token = formData.get('cf-turnstile-response');\n    \n    const result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        secret: env.TURNSTILE_SECRET,\n        response: token,\n        remoteip: request.headers.get('CF-Connecting-IP')\n      })\n    });\n    \n    const validation = await result.json();\n    if (!validation.success) {\n      return new Response('Invalid CAPTCHA', { status: 400 });\n    }\n    // Process form...\n  }\n}\n```\n\n## Testing Keys\n\n**Critical for development/testing:**\n\n| Type | Key | Behavior |\n|------|-----|----------|\n| **Site Key (Always Passes)** | `1x00000000000000000000AA` | Widget succeeds, token validates |\n| **Site Key (Always Blocks)** | `2x00000000000000000000AB` | Widget fails visibly |\n| **Site Key (Force Challenge)** | `3x00000000000000000000FF` | Always shows interactive challenge |\n| **Secret Key (Testing)** | `1x0000000000000000000000000000000AA` | Validates test tokens |\n\n**Note:** Test keys work on `localhost` and any domain. Do NOT use in production.\n\n## Key Constraints\n\n- **Token expiry:** 5 minutes after generation\n- **Single-use:** Each token can only be validated once\n- **Server validation required:** Client-side checks are insufficient\n\n## Reading Order\n\n1. **[configuration.md](configuration.md)** - Setup, widget options, script loading\n2. **[api.md](api.md)** - JavaScript API, siteverify endpoints, TypeScript types\n3. **[patterns.md](patterns.md)** - Form integration, framework examples, validation patterns\n4. **[gotchas.md](gotchas.md)** - Common errors, debugging, limitations\n\n## See Also\n\n- [Cloudflare Turnstile Docs](https://developers.cloudflare.com/turnstile/)\n- [Dashboard](https://dash.cloudflare.com/?to=/:account/turnstile)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/turnstile/api.md",
    "content": "# API Reference\n\n## Client-Side JavaScript API\n\nThe Turnstile JavaScript API is available at `window.turnstile` after loading the script.\n\n### `turnstile.render(container, options)`\n\nRenders a Turnstile widget into a container element.\n\n**Parameters:**\n- `container` (string | HTMLElement): CSS selector or DOM element\n- `options` (TurnstileOptions): Configuration object (see [configuration.md](configuration.md))\n\n**Returns:** `string` - Widget ID for use with other API methods\n\n**Example:**\n```javascript\nconst widgetId = window.turnstile.render('#my-container', {\n  sitekey: 'YOUR_SITE_KEY',\n  callback: (token) => console.log('Success:', token),\n  'error-callback': (code) => console.error('Error:', code)\n});\n```\n\n### `turnstile.reset(widgetId)`\n\nResets a widget (clears token, resets challenge state). Useful when form validation fails.\n\n**Parameters:**\n- `widgetId` (string): Widget ID from `render()`, or container element\n\n**Returns:** `void`\n\n**Example:**\n```javascript\n// Reset on form error\nif (!validateForm()) {\n  window.turnstile.reset(widgetId);\n}\n```\n\n### `turnstile.remove(widgetId)`\n\nRemoves a widget from the DOM completely.\n\n**Parameters:**\n- `widgetId` (string): Widget ID from `render()`\n\n**Returns:** `void`\n\n**Example:**\n```javascript\n// Cleanup on navigation\nwindow.turnstile.remove(widgetId);\n```\n\n### `turnstile.getResponse(widgetId)`\n\nGets the current token from a widget (if challenge completed).\n\n**Parameters:**\n- `widgetId` (string): Widget ID from `render()`, or container element\n\n**Returns:** `string | undefined` - Token string, or undefined if not ready\n\n**Example:**\n```javascript\nconst token = window.turnstile.getResponse(widgetId);\nif (token) {\n  submitForm(token);\n}\n```\n\n### `turnstile.isExpired(widgetId)`\n\nChecks if a widget's token has expired (>5 minutes old).\n\n**Parameters:**\n- `widgetId` (string): Widget ID from `render()`\n\n**Returns:** `boolean` - True if expired\n\n**Example:**\n```javascript\nif (window.turnstile.isExpired(widgetId)) {\n  window.turnstile.reset(widgetId);\n}\n```\n\n## Callback Signatures\n\n```typescript\ntype TurnstileCallback = (token: string) => void;\ntype ErrorCallback = (errorCode: string) => void;\ntype TimeoutCallback = () => void;\ntype ExpiredCallback = () => void;\ntype BeforeInteractiveCallback = () => void;\ntype AfterInteractiveCallback = () => void;\ntype UnsupportedCallback = () => void;\n```\n\n## Siteverify API (Server-Side)\n\n**Endpoint:** `https://challenges.cloudflare.com/turnstile/v0/siteverify`\n\n### Request\n\n**Method:** POST  \n**Content-Type:** `application/json` or `application/x-www-form-urlencoded`\n\n```typescript\ninterface SiteverifyRequest {\n  secret: string;    // Your secret key (never expose client-side)\n  response: string;  // Token from cf-turnstile-response\n  remoteip?: string; // User's IP (optional but recommended)\n  idempotency_key?: string; // Unique key for idempotent validation\n}\n```\n\n**Example:**\n```javascript\n// Cloudflare Workers\nconst result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    secret: env.TURNSTILE_SECRET,\n    response: token,\n    remoteip: request.headers.get('CF-Connecting-IP')\n  })\n});\nconst data = await result.json();\n```\n\n### Response\n\n```typescript\ninterface SiteverifyResponse {\n  success: boolean;           // Validation result\n  challenge_ts?: string;      // ISO timestamp of challenge\n  hostname?: string;          // Hostname where widget was solved\n  'error-codes'?: string[];   // Error codes if success=false\n  action?: string;            // Action name from widget config\n  cdata?: string;             // Custom data from widget config\n}\n```\n\n**Example Success:**\n```json\n{\n  \"success\": true,\n  \"challenge_ts\": \"2024-01-15T10:30:00Z\",\n  \"hostname\": \"example.com\",\n  \"action\": \"login\",\n  \"cdata\": \"user123\"\n}\n```\n\n**Example Failure:**\n```json\n{\n  \"success\": false,\n  \"error-codes\": [\"timeout-or-duplicate\"]\n}\n```\n\n## Error Codes\n\n| Code | Cause | Solution |\n|------|-------|----------|\n| `missing-input-secret` | Secret key not provided | Include `secret` in request |\n| `invalid-input-secret` | Secret key is wrong | Check secret key in dashboard |\n| `missing-input-response` | Token not provided | Include `response` token |\n| `invalid-input-response` | Token is invalid/malformed | Verify token from widget |\n| `timeout-or-duplicate` | Token expired (>5min) or reused | Generate new token, validate once |\n| `internal-error` | Cloudflare server error | Retry with exponential backoff |\n| `bad-request` | Malformed request | Check JSON/form encoding |\n\n## TypeScript Types\n\n```typescript\ninterface TurnstileOptions {\n  sitekey: string;\n  action?: string;\n  cData?: string;\n  callback?: (token: string) => void;\n  'error-callback'?: (errorCode: string) => void;\n  'expired-callback'?: () => void;\n  'timeout-callback'?: () => void;\n  'before-interactive-callback'?: () => void;\n  'after-interactive-callback'?: () => void;\n  'unsupported-callback'?: () => void;\n  theme?: 'light' | 'dark' | 'auto';\n  size?: 'normal' | 'compact' | 'flexible';\n  tabindex?: number;\n  'response-field'?: boolean;\n  'response-field-name'?: string;\n  retry?: 'auto' | 'never';\n  'retry-interval'?: number;\n  language?: string;\n  execution?: 'render' | 'execute';\n  appearance?: 'always' | 'execute' | 'interaction-only';\n  'refresh-expired'?: 'auto' | 'manual' | 'never';\n}\n\ninterface Turnstile {\n  render(container: string | HTMLElement, options: TurnstileOptions): string;\n  reset(widgetId: string): void;\n  remove(widgetId: string): void;\n  getResponse(widgetId: string): string | undefined;\n  isExpired(widgetId: string): boolean;\n  execute(container?: string | HTMLElement, options?: TurnstileOptions): void;\n}\n\ndeclare global {\n  interface Window {\n    turnstile: Turnstile;\n    onloadTurnstileCallback?: () => void;\n  }\n}\n```\n\n## Script Loading\n\n```html\n<!-- Standard -->\n<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js\" async defer></script>\n\n<!-- Explicit render mode -->\n<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit\"></script>\n\n<!-- With load callback -->\n<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback\"></script>\n<script>\nwindow.onloadTurnstileCallback = () => {\n  window.turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY' });\n};\n</script>\n```"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/turnstile/configuration.md",
    "content": "# Configuration\n\n## Script Loading\n\n### Basic (Implicit Rendering)\n```html\n<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js\" async defer></script>\n```\nAutomatically renders widgets with `class=\"cf-turnstile\"` on page load.\n\n### Explicit Rendering\n```html\n<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit\"></script>\n```\nManual control over when/where widgets render via `window.turnstile.render()`.\n\n### With Load Callback\n```html\n<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js?onload=myCallback\"></script>\n<script>\nfunction myCallback() {\n  // API ready\n  window.turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY' });\n}\n</script>\n```\n\n### Compatibility Mode\n```html\n<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js?compat=recaptcha\"></script>\n```\nProvides `grecaptcha` API for Google reCAPTCHA drop-in replacement.\n\n## Widget Configuration\n\n### Complete Options Object\n\n```javascript\n{\n  // Required\n  sitekey: 'YOUR_SITE_KEY',        // Widget sitekey from dashboard\n\n  // Callbacks\n  callback: (token) => {},          // Success - token ready\n  'error-callback': (code) => {},   // Error occurred\n  'expired-callback': () => {},     // Token expired (>5min)\n  'timeout-callback': () => {},     // Challenge timeout\n  'before-interactive-callback': () => {}, // Before showing checkbox\n  'after-interactive-callback': () => {},  // After user interacts\n  'unsupported-callback': () => {}, // Browser doesn't support Turnstile\n\n  // Appearance\n  theme: 'auto',                    // 'light' | 'dark' | 'auto'\n  size: 'normal',                   // 'normal' | 'compact' | 'flexible'\n  tabindex: 0,                      // Tab order (accessibility)\n  language: 'auto',                 // ISO 639-1 code or 'auto'\n\n  // Behavior\n  execution: 'render',              // 'render' (auto) | 'execute' (manual)\n  appearance: 'always',             // 'always' | 'execute' | 'interaction-only'\n  retry: 'auto',                    // 'auto' | 'never'\n  'retry-interval': 8000,           // Retry interval (ms), default 8000\n  'refresh-expired': 'auto',        // 'auto' | 'manual' | 'never'\n\n  // Form Integration\n  'response-field': true,           // Add hidden input (default: true)\n  'response-field-name': 'cf-turnstile-response', // Hidden input name\n\n  // Analytics & Data\n  action: 'login',                  // Action name (for analytics)\n  cData: 'user-session-123',        // Custom data (returned in siteverify)\n}\n```\n\n### Key Options Explained\n\n**`execution`:**\n- `'render'` (default): Challenge starts immediately on render\n- `'execute'`: Wait for `turnstile.execute()` call\n\n**`appearance`:**\n- `'always'` (default): Widget always visible\n- `'execute'`: Hidden until `execute()` called\n- `'interaction-only'`: Hidden until user interaction needed\n\n**`refresh-expired`:**\n- `'auto'` (default): Auto-refresh expired tokens\n- `'manual'`: App must call `reset()` after expiry\n- `'never'`: No refresh, expired-callback triggered\n\n**`retry`:**\n- `'auto'` (default): Auto-retry failed challenges\n- `'never'`: Don't retry, trigger error-callback\n\n## HTML Data Attributes\n\nFor implicit rendering, use data attributes on `<div class=\"cf-turnstile\">`:\n\n| JavaScript Property | HTML Data Attribute | Example |\n|---------------------|---------------------|---------|\n| `sitekey` | `data-sitekey` | `data-sitekey=\"YOUR_KEY\"` |\n| `action` | `data-action` | `data-action=\"login\"` |\n| `cData` | `data-cdata` | `data-cdata=\"session-123\"` |\n| `callback` | `data-callback` | `data-callback=\"onSuccess\"` |\n| `error-callback` | `data-error-callback` | `data-error-callback=\"onError\"` |\n| `expired-callback` | `data-expired-callback` | `data-expired-callback=\"onExpired\"` |\n| `timeout-callback` | `data-timeout-callback` | `data-timeout-callback=\"onTimeout\"` |\n| `theme` | `data-theme` | `data-theme=\"dark\"` |\n| `size` | `data-size` | `data-size=\"compact\"` |\n| `tabindex` | `data-tabindex` | `data-tabindex=\"0\"` |\n| `response-field` | `data-response-field` | `data-response-field=\"false\"` |\n| `response-field-name` | `data-response-field-name` | `data-response-field-name=\"token\"` |\n| `retry` | `data-retry` | `data-retry=\"never\"` |\n| `retry-interval` | `data-retry-interval` | `data-retry-interval=\"5000\"` |\n| `language` | `data-language` | `data-language=\"en\"` |\n| `execution` | `data-execution` | `data-execution=\"execute\"` |\n| `appearance` | `data-appearance` | `data-appearance=\"interaction-only\"` |\n| `refresh-expired` | `data-refresh-expired` | `data-refresh-expired=\"manual\"` |\n\n**Example:**\n```html\n<div class=\"cf-turnstile\"\n     data-sitekey=\"YOUR_SITE_KEY\"\n     data-theme=\"dark\"\n     data-callback=\"onTurnstileSuccess\"\n     data-error-callback=\"onTurnstileError\"></div>\n```\n\n## Content Security Policy\n\nAdd these directives to CSP header/meta tag:\n\n```\nscript-src https://challenges.cloudflare.com;\nframe-src https://challenges.cloudflare.com;\n```\n\n**Full Example:**\n```html\n<meta http-equiv=\"Content-Security-Policy\" \n      content=\"default-src 'self'; \n               script-src 'self' https://challenges.cloudflare.com; \n               frame-src https://challenges.cloudflare.com;\">\n```\n\n## Framework-Specific Setup\n\n### React\n```bash\nnpm install @marsidev/react-turnstile\n```\n```jsx\nimport Turnstile from '@marsidev/react-turnstile';\n\n<Turnstile\n  siteKey=\"YOUR_SITE_KEY\"\n  onSuccess={(token) => console.log(token)}\n/>\n```\n\n### Vue\n```bash\nnpm install vue-turnstile\n```\n```vue\n<template>\n  <VueTurnstile site-key=\"YOUR_SITE_KEY\" @success=\"onSuccess\" />\n</template>\n<script setup>\nimport VueTurnstile from 'vue-turnstile';\n</script>\n```\n\n### Svelte\n```bash\nnpm install svelte-turnstile\n```\n```svelte\n<script>\nimport Turnstile from 'svelte-turnstile';\n</script>\n<Turnstile siteKey=\"YOUR_SITE_KEY\" on:turnstile-callback={handleToken} />\n```\n\n### Next.js (App Router)\n```tsx\n// app/components/TurnstileWidget.tsx\n'use client';\nimport { useEffect, useRef } from 'react';\n\nexport default function TurnstileWidget({ sitekey, onSuccess }) {\n  const ref = useRef<HTMLDivElement>(null);\n  \n  useEffect(() => {\n    if (ref.current && window.turnstile) {\n      const widgetId = window.turnstile.render(ref.current, {\n        sitekey,\n        callback: onSuccess\n      });\n      return () => window.turnstile.remove(widgetId);\n    }\n  }, [sitekey, onSuccess]);\n  \n  return <div ref={ref} />;\n}\n```\n\n## Cloudflare Pages Plugin\n\n```bash\nnpm install @cloudflare/pages-plugin-turnstile\n```\n\n```typescript\n// functions/_middleware.ts\nimport turnstilePlugin from '@cloudflare/pages-plugin-turnstile';\n\nexport const onRequest = turnstilePlugin({\n  secret: 'YOUR_SECRET_KEY',\n  onError: () => new Response('CAPTCHA failed', { status: 403 })\n});\n```"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/turnstile/gotchas.md",
    "content": "# Troubleshooting & Gotchas\n\n## Critical Rules\n\n### ❌ Skipping Server-Side Validation\n**Problem:** Client-only validation is easily bypassed.\n\n**Solution:** Always validate on server.\n```javascript\n// CORRECT - Server validates token\napp.post('/submit', async (req, res) => {\n  const token = req.body['cf-turnstile-response'];\n  const validation = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {\n    method: 'POST',\n    body: JSON.stringify({ secret: SECRET, response: token })\n  }).then(r => r.json());\n  \n  if (!validation.success) return res.status(403).json({ error: 'CAPTCHA failed' });\n});\n```\n\n### ❌ Exposing Secret Key\n**Problem:** Secret key leaked in client-side code.\n\n**Solution:** Server-side validation only. Never send secret to client.\n\n### ❌ Reusing Tokens (Single-Use Rule)\n**Problem:** Tokens are single-use. Revalidation fails with `timeout-or-duplicate`.\n\n**Solution:** Generate new token for each submission. Reset widget on error.\n```javascript\nif (!response.ok) window.turnstile.reset(widgetId);\n```\n\n### ❌ Not Handling Token Expiry\n**Problem:** Tokens expire after 5 minutes.\n\n**Solution:** Handle expiry callback or use auto-refresh.\n```javascript\nwindow.turnstile.render('#container', {\n  sitekey: 'YOUR_SITE_KEY',\n  'refresh-expired': 'auto', // or 'manual' with expired-callback\n  'expired-callback': () => window.turnstile.reset(widgetId)\n});\n```\n\n## Common Errors\n\n| Error | Cause | Solution |\n|-------|-------|----------|\n| **Widget not rendering** | Incorrect sitekey, CSP blocking, file:// protocol | Check sitekey, add CSP for challenges.cloudflare.com, use http:// |\n| **timeout-or-duplicate** | Token expired (>5min) or reused | Generate fresh token, don't cache >5min |\n| **invalid-input-secret** | Wrong secret key | Verify secret from dashboard, check env vars |\n| **missing-input-response** | Token not sent | Check form field name is 'cf-turnstile-response' |\n\n## Framework Gotchas\n\n### React: Widget Re-mounting\n**Problem:** Widget re-renders on state change, losing token.\n\n**Solution:** Control lifecycle with useRef.\n```tsx\nfunction TurnstileWidget({ onToken }) {\n  const containerRef = useRef(null);\n  const widgetIdRef = useRef(null);\n  \n  useEffect(() => {\n    if (containerRef.current && !widgetIdRef.current) {\n      widgetIdRef.current = window.turnstile.render(containerRef.current, {\n        sitekey: 'YOUR_SITE_KEY',\n        callback: onToken\n      });\n    }\n    return () => {\n      if (widgetIdRef.current) {\n        window.turnstile.remove(widgetIdRef.current);\n        widgetIdRef.current = null;\n      }\n    };\n  }, []);\n  \n  return <div ref={containerRef} />;\n}\n```\n\n### React StrictMode: Double Render\n**Problem:** Widget renders twice in dev due to StrictMode.\n\n**Solution:** Use cleanup function.\n```tsx\nuseEffect(() => {\n  const widgetId = window.turnstile.render('#container', { sitekey });\n  return () => window.turnstile.remove(widgetId);\n}, []);\n```\n\n### Next.js: SSR Hydration\n**Problem:** `window.turnstile` undefined during SSR.\n\n**Solution:** Use `'use client'` or dynamic import with `ssr: false`.\n```tsx\n'use client';\nexport default function Turnstile() { /* component */ }\n```\n\n### SPA: Navigation Without Cleanup\n**Problem:** Navigating leaves orphaned widgets.\n\n**Solution:** Remove widget in cleanup.\n```javascript\n// Vue\nonBeforeUnmount(() => window.turnstile.remove(widgetId));\n\n// React\nuseEffect(() => () => window.turnstile.remove(widgetId), []);\n```\n\n## Network & Security\n\n### CSP Blocking\n**Problem:** Content Security Policy blocks script/iframe.\n\n**Solution:** Add CSP directives.\n```html\n<meta http-equiv=\"Content-Security-Policy\" \n      content=\"script-src 'self' https://challenges.cloudflare.com; \n               frame-src https://challenges.cloudflare.com;\">\n```\n\n### IP Address Forwarding\n**Problem:** Server receives proxy IP instead of client IP.\n\n**Solution:** Use correct header.\n```javascript\n// Cloudflare Workers\nconst ip = request.headers.get('CF-Connecting-IP');\n\n// Behind proxy\nconst ip = request.headers.get('X-Forwarded-For')?.split(',')[0];\n```\n\n### CORS (Siteverify)\n**Problem:** CORS error calling siteverify from browser.\n\n**Solution:** Never call siteverify client-side. Call your backend, backend calls siteverify.\n\n## Limits & Constraints\n\n| Limit | Value | Impact |\n|-------|-------|--------|\n| Token validity | 5 minutes | Must regenerate after expiry |\n| Token use | Single-use | Cannot revalidate same token |\n| Widget size | 300x65px (normal), 130x120px (compact) | Plan layout |\n\n## Debugging\n\n### Console Logging\n```javascript\nwindow.turnstile.render('#container', {\n  sitekey: 'YOUR_SITE_KEY',\n  callback: (token) => console.log('✓ Token:', token),\n  'error-callback': (code) => console.error('✗ Error:', code),\n  'expired-callback': () => console.warn('⏱ Expired'),\n  'timeout-callback': () => console.warn('⏱ Timeout')\n});\n```\n\n### Check Token State\n```javascript\nconst token = window.turnstile.getResponse(widgetId);\nconsole.log('Token:', token || 'NOT READY');\nconsole.log('Expired:', window.turnstile.isExpired(widgetId));\n```\n\n### Test Keys (Use First)\nAlways develop with test keys before production:\n- Site: `1x00000000000000000000AA`\n- Secret: `1x0000000000000000000000000000000AA`\n\n### Network Tab\n- Verify `api.js` loads (200 OK)\n- Check siteverify request/response\n- Look for 4xx/5xx errors\n\n## Misconfigurations\n\n### Wrong Key Pairing\n**Problem:** Site key from one widget, secret from another.\n\n**Solution:** Verify site key and secret are from same widget in dashboard.\n\n### Test Keys in Production\n**Problem:** Using test keys in production.\n\n**Solution:** Environment-based keys.\n```javascript\nconst SITE_KEY = process.env.NODE_ENV === 'production'\n  ? process.env.TURNSTILE_SITE_KEY\n  : '1x00000000000000000000AA';\n```\n\n### Missing Environment Variables\n**Problem:** Secret undefined on server.\n\n**Solution:** Check .env and verify loading.\n```bash\n# .env\nTURNSTILE_SECRET=your_secret_here\n\n# Verify\nconsole.log('Secret loaded:', !!process.env.TURNSTILE_SECRET);\n```\n\n## Reference\n\n- [Turnstile Docs](https://developers.cloudflare.com/turnstile/)\n- [Dashboard](https://dash.cloudflare.com/?to=/:account/turnstile)\n- [Error Codes](https://developers.cloudflare.com/turnstile/troubleshooting/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/turnstile/patterns.md",
    "content": "# Common Patterns\n\n## Form Integration\n\n### Basic Form (Implicit Rendering)\n\n```html\n<!DOCTYPE html>\n<html>\n<head>\n  <script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js\" async defer></script>\n</head>\n<body>\n  <form action=\"/submit\" method=\"POST\">\n    <input type=\"email\" name=\"email\" required>\n    <div class=\"cf-turnstile\" data-sitekey=\"YOUR_SITE_KEY\"></div>\n    <button type=\"submit\">Submit</button>\n  </form>\n</body>\n</html>\n```\n\n### Controlled Form (Explicit Rendering)\n\n```javascript\n<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit\"></script>\n<script>\nlet widgetId = window.turnstile.render('#container', {\n  sitekey: 'YOUR_SITE_KEY',\n  callback: (token) => console.log('Token:', token)\n});\n\nform.addEventListener('submit', async (e) => {\n  e.preventDefault();\n  const token = window.turnstile.getResponse(widgetId);\n  if (!token) return;\n  \n  const response = await fetch('/submit', {\n    method: 'POST',\n    body: JSON.stringify({ 'cf-turnstile-response': token })\n  });\n  \n  if (!response.ok) window.turnstile.reset(widgetId);\n});\n</script>\n```\n\n## Framework Patterns\n\n### React\n\n```tsx\nimport { useState } from 'react';\nimport Turnstile from '@marsidev/react-turnstile';\n\nexport default function Form() {\n  const [token, setToken] = useState<string | null>(null);\n\n  return (\n    <form onSubmit={async (e) => {\n      e.preventDefault();\n      if (!token) return;\n      await fetch('/api/submit', { \n        method: 'POST',\n        body: JSON.stringify({ 'cf-turnstile-response': token })\n      });\n    }}>\n      <Turnstile siteKey=\"YOUR_SITE_KEY\" onSuccess={setToken} />\n      <button disabled={!token}>Submit</button>\n    </form>\n  );\n}\n```\n\n### Vue / Svelte\n\n```vue\n<!-- Vue: npm install vue-turnstile -->\n<VueTurnstile :site-key=\"SITE_KEY\" @success=\"token = $event\" />\n\n<!-- Svelte: npm install svelte-turnstile -->\n<Turnstile siteKey={SITE_KEY} on:turnstile-callback={(e) => token = e.detail.token} />\n```\n\n## Server Validation\n\n### Cloudflare Workers\n\n```typescript\ninterface Env {\n  TURNSTILE_SECRET: string;\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    if (request.method !== 'POST') {\n      return new Response('Method not allowed', { status: 405 });\n    }\n    \n    const formData = await request.formData();\n    const token = formData.get('cf-turnstile-response');\n    \n    if (!token) {\n      return new Response('Missing token', { status: 400 });\n    }\n    \n    // Validate token\n    const ip = request.headers.get('CF-Connecting-IP');\n    const result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        secret: env.TURNSTILE_SECRET,\n        response: token,\n        remoteip: ip\n      })\n    });\n    \n    const validation = await result.json();\n    \n    if (!validation.success) {\n      return new Response('CAPTCHA validation failed', { status: 403 });\n    }\n    \n    // Process form...\n    return new Response('Success');\n  }\n};\n```\n\n### Pages Functions\n\n```typescript\n// functions/submit.ts - same pattern as Workers, use ctx.env and ctx.request\nexport const onRequestPost: PagesFunction<{ TURNSTILE_SECRET: string }> = async (ctx) => {\n  const token = (await ctx.request.formData()).get('cf-turnstile-response');\n  // Validate with ctx.env.TURNSTILE_SECRET (same as Workers pattern above)\n};\n```\n\n## Advanced Patterns\n\n### Pre-Clearance (Invisible)\n\n```html\n<div id=\"turnstile-precheck\"></div>\n<form id=\"protected-form\" style=\"display: none;\">\n  <button type=\"submit\">Submit</button>\n</form>\n\n<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit\"></script>\n<script>\nlet cachedToken = null;\n\nwindow.onload = () => {\n  window.turnstile.render('#turnstile-precheck', {\n    sitekey: 'YOUR_SITE_KEY',\n    size: 'invisible',\n    callback: (token) => {\n      cachedToken = token;\n      document.getElementById('protected-form').style.display = 'block';\n    }\n  });\n};\n</script>\n```\n\n### Token Refresh on Expiry\n\n```javascript\nlet widgetId = window.turnstile.render('#container', {\n  sitekey: 'YOUR_SITE_KEY',\n  'refresh-expired': 'manual',\n  'expired-callback': () => {\n    console.log('Token expired, refreshing...');\n    window.turnstile.reset(widgetId);\n  }\n});\n```\n\n## Testing\n\n### Environment-Based Keys\n\n```javascript\nconst SITE_KEY = process.env.NODE_ENV === 'production'\n  ? 'YOUR_PRODUCTION_SITE_KEY'\n  : '1x00000000000000000000AA'; // Always passes\n\nconst SECRET_KEY = process.env.NODE_ENV === 'production'\n  ? process.env.TURNSTILE_SECRET\n  : '1x0000000000000000000000000000000AA';\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/vectorize/README.md",
    "content": "# Cloudflare Vectorize\n\nGlobally distributed vector database for AI applications. Store and query vector embeddings for semantic search, recommendations, RAG, and classification.\n\n**Status:** Generally Available (GA) | **Last Updated:** 2026-01-27\n\n## Quick Start\n\n```typescript\n// 1. Create index\n// npx wrangler vectorize create my-index --dimensions=768 --metric=cosine\n\n// 2. Configure binding (wrangler.jsonc)\n// { \"vectorize\": [{ \"binding\": \"VECTORIZE\", \"index_name\": \"my-index\" }] }\n\n// 3. Query vectors\nconst matches = await env.VECTORIZE.query(queryVector, { topK: 5 });\n```\n\n## Key Features\n\n- **10M vectors per index** (V2)\n- Dimensions up to 1536 (32-bit float)\n- Three distance metrics: cosine, euclidean, dot-product\n- Metadata filtering (up to 10 indexes)\n- Namespace support (50K namespaces paid, 1K free)\n- Seamless Workers AI integration\n- Global distribution\n\n## Reading Order\n\n| Task | Files to Read |\n|------|---------------|\n| New to Vectorize | README only |\n| Implement feature | README + api + patterns |\n| Setup/configure | README + configuration |\n| Debug issues | gotchas |\n| Integrate with AI | README + patterns |\n| RAG implementation | README + patterns |\n\n## File Guide\n\n- **README.md** (this file): Overview, quick decisions\n- **api.md**: Runtime API, types, operations (query/insert/upsert)\n- **configuration.md**: Setup, CLI, metadata indexes\n- **patterns.md**: RAG, Workers AI, OpenAI, LangChain, multi-tenant\n- **gotchas.md**: Limits, pitfalls, troubleshooting\n\n## Distance Metric Selection\n\nChoose based on your use case:\n\n```\nWhat are you building?\n├─ Text/semantic search → cosine (most common)\n├─ Image similarity → euclidean\n├─ Recommendation system → dot-product\n└─ Pre-normalized vectors → dot-product\n```\n\n| Metric | Best For | Score Interpretation |\n|--------|----------|---------------------|\n| `cosine` | Text embeddings, semantic similarity | Higher = closer (1.0 = identical) |\n| `euclidean` | Absolute distance, spatial data | Lower = closer (0.0 = identical) |\n| `dot-product` | Recommendations, normalized vectors | Higher = closer |\n\n**Note:** Index configuration is immutable. Cannot change dimensions or metric after creation.\n\n## Multi-Tenancy Strategy\n\n```\nHow many tenants?\n├─ < 50K tenants → Use namespaces (recommended)\n│   ├─ Fastest (filter before vector search)\n│   └─ Strict isolation\n├─ > 50K tenants → Use metadata filtering\n│   ├─ Slower (post-filter after vector search)\n│   └─ Requires metadata index\n└─ Per-tenant indexes → Only if compliance mandated\n    └─ 50K index limit per account (paid plan)\n```\n\n## Common Workflows\n\n### Semantic Search\n\n```typescript\n// 1. Generate embedding\nconst result = await env.AI.run(\"@cf/baai/bge-base-en-v1.5\", { text: [query] });\n\n// 2. Query Vectorize\nconst matches = await env.VECTORIZE.query(result.data[0], {\n  topK: 5,\n  returnMetadata: \"indexed\"\n});\n```\n\n### RAG Pattern\n\n```typescript\n// 1. Generate query embedding\nconst embedding = await env.AI.run(\"@cf/baai/bge-base-en-v1.5\", { text: [query] });\n\n// 2. Search Vectorize\nconst matches = await env.VECTORIZE.query(embedding.data[0], { topK: 5 });\n\n// 3. Fetch full documents from R2/D1/KV\nconst docs = await Promise.all(matches.matches.map(m => \n  env.R2.get(m.metadata.key).then(obj => obj?.text())\n));\n\n// 4. Generate LLM response with context\nconst answer = await env.AI.run(\"@cf/meta/llama-3-8b-instruct\", {\n  prompt: `Context: ${docs.join(\"\\n\\n\")}\\n\\nQuestion: ${query}\\n\\nAnswer:`\n});\n```\n\n## Critical Gotchas\n\nSee `gotchas.md` for details. Most important:\n\n1. **Async mutations**: Inserts take 5-10s to be queryable\n2. **500 batch limit**: Workers API enforces 500 vectors per call (undocumented)\n3. **Metadata truncation**: `\"indexed\"` returns first 64 bytes only\n4. **topK with metadata**: Max 20 (not 100) when using returnValues or returnMetadata: \"all\"\n5. **Metadata indexes first**: Must create before inserting vectors\n\n## Resources\n\n- [Official Docs](https://developers.cloudflare.com/vectorize/)\n- [Client API Reference](https://developers.cloudflare.com/vectorize/reference/client-api/)\n- [Workers AI Models](https://developers.cloudflare.com/workers-ai/models/#text-embeddings)\n- [Discord: #vectorize](https://discord.cloudflare.com)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/vectorize/api.md",
    "content": "# Vectorize API Reference\n\n## Types\n\n```typescript\ninterface VectorizeVector {\n  id: string;                    // Max 64 bytes\n  values: number[];              // Must match index dimensions\n  namespace?: string;            // Optional partition (max 64 bytes)\n  metadata?: Record<string, any>; // Max 10 KiB\n}\n```\n\n## Query\n\n```typescript\nconst matches = await env.VECTORIZE.query(queryVector, {\n  topK: 10,                        // Max 100 (or 20 with returnValues/returnMetadata:\"all\")\n  returnMetadata: \"indexed\",       // \"none\" | \"indexed\" | \"all\"\n  returnValues: false,\n  namespace: \"tenant-123\",\n  filter: { category: \"docs\" }\n});\n// matches.matches[0] = { id, score, metadata? }\n```\n\n**returnMetadata:** `\"none\"` (fastest) → `\"indexed\"` (recommended) → `\"all\"` (topK max 20)\n\n**queryById (V2 only):** Search using existing vector as query.\n```typescript\nawait env.VECTORIZE.queryById(\"doc-123\", { topK: 5 });\n```\n\n## Insert/Upsert\n\n```typescript\n// Insert: ignores duplicates (keeps first)\nawait env.VECTORIZE.insert([{ id, values, metadata }]);\n\n// Upsert: overwrites duplicates (keeps last)\nawait env.VECTORIZE.upsert([{ id, values, metadata }]);\n```\n\n**Max 500 vectors per call.** Queryable after 5-10 seconds.\n\n## Other Operations\n\n```typescript\n// Get by IDs\nconst vectors = await env.VECTORIZE.getByIds([\"id1\", \"id2\"]);\n\n// Delete (max 1000 IDs per call)\nawait env.VECTORIZE.deleteByIds([\"id1\", \"id2\"]);\n\n// Index info\nconst info = await env.VECTORIZE.describe();\n// { dimensions, metric, vectorCount }\n```\n\n## Filtering\n\nRequires metadata index. Filter operators:\n\n| Operator | Example |\n|----------|---------|\n| `$eq` (implicit) | `{ category: \"docs\" }` |\n| `$ne` | `{ status: { $ne: \"deleted\" } }` |\n| `$in` / `$nin` | `{ tag: { $in: [\"sale\"] } }` |\n| `$lt`, `$lte`, `$gt`, `$gte` | `{ price: { $lt: 100 } }` |\n\n**Constraints:** Max 2048 bytes, no dots/`$` in keys, values: string/number/boolean/null.\n\n## Performance\n\n| Configuration | topK Limit | Speed |\n|--------------|------------|-------|\n| No metadata | 100 | Fastest |\n| `returnMetadata: \"indexed\"` | 100 | Fast |\n| `returnMetadata: \"all\"` | 20 | Slower |\n| `returnValues: true` | 20 | Slower |\n\n**Batch operations:** Always batch (500/call) for optimal throughput.\n\n```typescript\nfor (let i = 0; i < vectors.length; i += 500) {\n  await env.VECTORIZE.upsert(vectors.slice(i, i + 500));\n}\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/vectorize/configuration.md",
    "content": "# Vectorize Configuration\n\n## Create Index\n\n```bash\nnpx wrangler vectorize create my-index --dimensions=768 --metric=cosine\n```\n\n**⚠️ Dimensions and metric are immutable** - cannot change after creation.\n\n## Worker Binding\n\n```jsonc\n// wrangler.jsonc\n{\n  \"vectorize\": [\n    { \"binding\": \"VECTORIZE\", \"index_name\": \"my-index\" }\n  ]\n}\n```\n\n```typescript\ninterface Env {\n  VECTORIZE: Vectorize;\n}\n```\n\n## Metadata Indexes\n\n**Must create BEFORE inserting vectors** - existing vectors not retroactively indexed.\n\n```bash\nwrangler vectorize create-metadata-index my-index --property-name=category --type=string\nwrangler vectorize create-metadata-index my-index --property-name=price --type=number\n```\n\n| Type | Use For |\n|------|---------|\n| `string` | Categories, tags (first 64 bytes indexed) |\n| `number` | Prices, timestamps |\n| `boolean` | Flags |\n\n## CLI Commands\n\n```bash\n# Index management\nwrangler vectorize list\nwrangler vectorize info <index-name>\nwrangler vectorize delete <index-name>\n\n# Vector operations\nwrangler vectorize insert <index-name> --file=embeddings.ndjson\nwrangler vectorize get <index-name> --ids=id1,id2\nwrangler vectorize delete-by-ids <index-name> --ids=id1,id2\n\n# Metadata indexes\nwrangler vectorize list-metadata-index <index-name>\nwrangler vectorize delete-metadata-index <index-name> --property-name=field\n```\n\n## Bulk Upload (NDJSON)\n\n```json\n{\"id\": \"1\", \"values\": [0.1, 0.2, ...], \"metadata\": {\"category\": \"docs\"}}\n{\"id\": \"2\", \"values\": [0.4, 0.5, ...], \"namespace\": \"tenant-abc\"}\n```\n\n**Limits:** 5000 vectors per file, 100 MB max\n\n## Cardinality Best Practice\n\nBucket high-cardinality data:\n```typescript\n// ❌ Millisecond timestamps\nmetadata: { timestamp: Date.now() }\n\n// ✅ 5-minute buckets\nmetadata: { timestamp_bucket: Math.floor(Date.now() / 300000) * 300000 }\n```\n\n## Production Checklist\n\n1. Create index with correct dimensions\n2. Create metadata indexes FIRST\n3. Test bulk upload\n4. Configure bindings\n5. Deploy Worker\n6. Verify queries\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/vectorize/gotchas.md",
    "content": "# Vectorize Gotchas\n\n## Critical Warnings\n\n### Async Mutations\nInsert/upsert/delete return immediately but vectors aren't queryable for 5-10 seconds.\n\n### Batch Size Limit\n**Workers API: 500 vectors max per call** (undocumented, silently truncates)\n\n```typescript\n// ✅ Chunk into 500\nfor (let i = 0; i < vectors.length; i += 500) {\n  await env.VECTORIZE.upsert(vectors.slice(i, i + 500));\n}\n```\n\n### Metadata Truncation\n`returnMetadata: \"indexed\"` returns only first 64 bytes of strings. Use `\"all\"` for complete metadata (but max topK drops to 20).\n\n### topK Limits\n\n| returnMetadata | returnValues | Max topK |\n|----------------|--------------|----------|\n| `\"none\"` / `\"indexed\"` | `false` | 100 |\n| `\"all\"` | any | **20** |\n| any | `true` | **20** |\n\n### Metadata Indexes First\nCreate BEFORE inserting - existing vectors not retroactively indexed.\n\n```bash\n# ✅ Create index FIRST\nwrangler vectorize create-metadata-index my-index --property-name=category --type=string\nwrangler vectorize insert my-index --file=data.ndjson\n```\n\n### Index Config Immutable\nCannot change dimensions/metric after creation. Must create new index and migrate.\n\n## Limits (V2)\n\n| Resource | Limit |\n|----------|-------|\n| Vectors per index | 10,000,000 |\n| Max dimensions | 1536 |\n| Batch upsert (Workers) | **500** |\n| Indexed string metadata | **64 bytes** |\n| Metadata indexes | 10 |\n| Namespaces | 50,000 (paid) / 1,000 (free) |\n\n## Common Mistakes\n\n1. **Wrong embedding shape:** Extract `result.data[0]` from Workers AI\n2. **Metadata index after data:** Re-upsert all vectors\n3. **Insert vs upsert:** `insert` ignores duplicates, `upsert` overwrites\n4. **Not batching:** Individual inserts ~1K/min, batched ~200K+/min\n\n## Troubleshooting\n\n**No results?**\n- Wait 5-10s after insert\n- Check namespace spelling (case-sensitive)\n- Verify metadata index exists\n- Check dimension mismatch\n\n**Metadata filter not working?**\n- Index must exist before data insert\n- Strings >64 bytes truncated\n- Use dot notation for nested: `\"product.category\"`\n\n## Model Dimensions\n\n- `@cf/baai/bge-small-en-v1.5`: 384\n- `@cf/baai/bge-base-en-v1.5`: 768\n- `@cf/baai/bge-large-en-v1.5`: 1024\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/vectorize/patterns.md",
    "content": "# Vectorize Patterns\n\n## Workers AI Integration\n\n```typescript\n// Generate embedding + query\nconst result = await env.AI.run(\"@cf/baai/bge-base-en-v1.5\", { text: [query] });\nconst matches = await env.VECTORIZE.query(result.data[0], { topK: 5 }); // Pass data[0]!\n```\n\n| Model | Dimensions |\n|-------|------------|\n| `@cf/baai/bge-small-en-v1.5` | 384 |\n| `@cf/baai/bge-base-en-v1.5` | 768 (recommended) |\n| `@cf/baai/bge-large-en-v1.5` | 1024 |\n\n## OpenAI Integration\n\n```typescript\nconst response = await openai.embeddings.create({ model: \"text-embedding-ada-002\", input: query });\nconst matches = await env.VECTORIZE.query(response.data[0].embedding, { topK: 5 });\n```\n\n## RAG Pattern\n\n```typescript\n// 1. Embed query\nconst emb = await env.AI.run(\"@cf/baai/bge-base-en-v1.5\", { text: [query] });\n\n// 2. Search vectors\nconst matches = await env.VECTORIZE.query(emb.data[0], { topK: 5, returnMetadata: \"indexed\" });\n\n// 3. Fetch full docs from R2/D1/KV\nconst docs = await Promise.all(matches.matches.map(m => env.R2.get(m.metadata.key).then(o => o?.text())));\n\n// 4. Generate with context\nconst answer = await env.AI.run(\"@cf/meta/llama-3-8b-instruct\", {\n  prompt: `Context:\\n${docs.filter(Boolean).join(\"\\n\\n\")}\\n\\nQuestion: ${query}\\n\\nAnswer:`\n});\n```\n\n## Multi-Tenant\n\n### Namespaces (< 50K tenants, fastest)\n\n```typescript\nawait env.VECTORIZE.upsert([{ id: \"1\", values: emb, namespace: `tenant-${id}` }]);\nawait env.VECTORIZE.query(vec, { namespace: `tenant-${id}`, topK: 10 });\n```\n\n### Metadata Filter (> 50K tenants)\n\n```bash\nwrangler vectorize create-metadata-index my-index --property-name=tenantId --type=string\n```\n\n```typescript\nawait env.VECTORIZE.upsert([{ id: \"1\", values: emb, metadata: { tenantId: id } }]);\nawait env.VECTORIZE.query(vec, { filter: { tenantId: id }, topK: 10 });\n```\n\n## Hybrid Search\n\n```typescript\nconst matches = await env.VECTORIZE.query(vec, {\n  topK: 20,\n  filter: {\n    category: { $in: [\"tech\", \"science\"] },\n    published: { $gte: lastMonthTimestamp }\n  }\n});\n```\n\n## Batch Ingestion\n\n```typescript\nconst BATCH = 500;\nfor (let i = 0; i < vectors.length; i += BATCH) {\n  await env.VECTORIZE.upsert(vectors.slice(i, i + BATCH));\n}\n```\n\n## Best Practices\n\n1. **Pass `data[0]`** not `data` or full response\n2. **Batch 500** vectors per upsert\n3. **Create metadata indexes** before inserting\n4. **Use namespaces** for tenant isolation (faster than filters)\n5. **`returnMetadata: \"indexed\"`** for best speed/data balance\n6. **Handle 5-10s mutation delay** in async operations\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/waf/README.md",
    "content": "# Cloudflare WAF Expert Skill Reference\n\n**Expertise**: Cloudflare Web Application Firewall (WAF) configuration, custom rules, managed rulesets, rate limiting, attack detection, and API integration\n\n## Overview\n\nCloudflare WAF protects web applications from attacks through managed rulesets and custom rules.\n\n**Detection (Managed Rulesets)**\n- Pre-configured rules maintained by Cloudflare\n- CVE-based rules, OWASP Top 10 coverage\n- Three main rulesets: Cloudflare Managed, OWASP CRS, Exposed Credentials\n- Actions: log, block, challenge, js_challenge, managed_challenge\n\n**Mitigation (Custom Rules & Rate Limiting)**\n- Custom expressions using Wirefilter syntax\n- Attack score-based blocking (`cf.waf.score`)\n- Rate limiting with per-IP, per-user, or custom characteristics\n- Actions: block, challenge, js_challenge, managed_challenge, log, skip\n\n## Quick Start\n\n### Deploy Cloudflare Managed Ruleset\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst client = new Cloudflare({ apiToken: process.env.CF_API_TOKEN });\n\n// Deploy managed ruleset to zone\nawait client.rulesets.create({\n  zone_id: 'zone_id',\n  kind: 'zone',\n  phase: 'http_request_firewall_managed',\n  name: 'Deploy Cloudflare Managed Ruleset',\n  rules: [{\n    action: 'execute',\n    action_parameters: {\n      id: 'efb7b8c949ac4650a09736fc376e9aee', // Cloudflare Managed Ruleset\n    },\n    expression: 'true',\n    enabled: true,\n  }],\n});\n```\n\n### Create Custom Rule\n```typescript\n// Block requests with attack score >= 40\nawait client.rulesets.create({\n  zone_id: 'zone_id',\n  kind: 'zone',\n  phase: 'http_request_firewall_custom',\n  name: 'Custom WAF Rules',\n  rules: [{\n    action: 'block',\n    expression: 'cf.waf.score gt 40',\n    description: 'Block high attack scores',\n    enabled: true,\n  }],\n});\n```\n\n### Create Rate Limit\n```typescript\nawait client.rulesets.create({\n  zone_id: 'zone_id',\n  kind: 'zone',\n  phase: 'http_ratelimit',\n  name: 'API Rate Limits',\n  rules: [{\n    action: 'block',\n    expression: 'http.request.uri.path eq \"/api/login\"',\n    action_parameters: {\n      ratelimit: {\n        characteristics: ['cf.colo.id', 'ip.src'],\n        period: 60,\n        requests_per_period: 10,\n        mitigation_timeout: 600,\n      },\n    },\n    enabled: true,\n  }],\n});\n```\n\n## Managed Ruleset Quick Reference\n\n| Ruleset Name | ID | Coverage |\n|--------------|----|---------| \n| Cloudflare Managed | `efb7b8c949ac4650a09736fc376e9aee` | OWASP Top 10, CVEs |\n| OWASP Core Ruleset | `4814384a9e5d4991b9815dcfc25d2f1f` | OWASP ModSecurity CRS |\n| Exposed Credentials Check | `c2e184081120413c86c3ab7e14069605` | Credential stuffing |\n\n## Phases\n\nWAF rules execute in specific phases:\n- `http_request_firewall_managed` - Managed rulesets\n- `http_request_firewall_custom` - Custom rules\n- `http_ratelimit` - Rate limiting rules\n- `http_request_sbfm` - Super Bot Fight Mode (Pro+)\n\n## Reading Order\n\n1. **[api.md](api.md)** - SDK methods, expressions, actions, parameters\n2. **[configuration.md](configuration.md)** - Setup with Wrangler, Terraform, Pulumi\n3. **[patterns.md](patterns.md)** - Common patterns: deploy managed, rate limiting, skip, override\n4. **[gotchas.md](gotchas.md)** - Execution order, limits, expression errors\n\n## See Also\n\n- [Cloudflare WAF Docs](https://developers.cloudflare.com/waf/)\n- [Ruleset Engine](https://developers.cloudflare.com/ruleset-engine/)\n- [Expression Reference](https://developers.cloudflare.com/ruleset-engine/rules-language/)"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/waf/api.md",
    "content": "# API Reference\n\n## SDK Setup\n\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst client = new Cloudflare({\n  apiToken: process.env.CF_API_TOKEN,\n});\n```\n\n## Core Methods\n\n```typescript\n// List rulesets\nawait client.rulesets.list({ zone_id: 'zone_id', phase: 'http_request_firewall_managed' });\n\n// Get ruleset\nawait client.rulesets.get({ zone_id: 'zone_id', ruleset_id: 'ruleset_id' });\n\n// Create ruleset\nawait client.rulesets.create({\n  zone_id: 'zone_id',\n  kind: 'zone',\n  phase: 'http_request_firewall_custom',\n  name: 'Custom WAF Rules',\n  rules: [{ action: 'block', expression: 'cf.waf.score gt 40', enabled: true }],\n});\n\n// Update ruleset (include rule id to keep existing, omit id for new rules)\nawait client.rulesets.update({\n  zone_id: 'zone_id',\n  ruleset_id: 'ruleset_id',\n  rules: [\n    { id: 'rule_id', action: 'block', expression: 'cf.waf.score gt 40', enabled: true },\n    { action: 'challenge', expression: 'http.request.uri.path contains \"/admin\"', enabled: true },\n  ],\n});\n\n// Delete ruleset\nawait client.rulesets.delete({ zone_id: 'zone_id', ruleset_id: 'ruleset_id' });\n```\n\n## Actions & Phases\n\n### Actions by Phase\n\n| Action | Custom | Managed | Rate Limit | Description |\n|--------|--------|---------|------------|-------------|\n| `block` | ✅ | ❌ | ✅ | Block request with 403 |\n| `challenge` | ✅ | ❌ | ✅ | Show CAPTCHA challenge |\n| `js_challenge` | ✅ | ❌ | ✅ | JS-based challenge |\n| `managed_challenge` | ✅ | ❌ | ✅ | Smart challenge (recommended) |\n| `log` | ✅ | ❌ | ✅ | Log only, don't block |\n| `skip` | ✅ | ❌ | ❌ | Skip rule evaluation |\n| `execute` | ❌ | ✅ | ❌ | Deploy managed ruleset |\n\n### Phases (Execution Order)\n\n1. `http_request_firewall_custom` - Custom rules (first line of defense)\n2. `http_request_firewall_managed` - Managed rulesets (pre-configured protection)\n3. `http_ratelimit` - Rate limiting (request throttling)\n4. `http_request_sbfm` - Super Bot Fight Mode (Pro+ only)\n\n## Expression Syntax\n\n### Fields\n\n```typescript\n// Request properties\nhttp.request.method          // GET, POST, etc.\nhttp.request.uri.path        // /api/users\nhttp.host                    // example.com\n\n// IP and Geolocation\nip.src                       // 192.0.2.1\nip.geoip.country            // US, GB, etc.\nip.geoip.continent          // NA, EU, etc.\n\n// Attack detection\ncf.waf.score                 // 0-100 attack score\ncf.waf.score.sqli           // SQL injection score\ncf.waf.score.xss            // XSS score\n\n// Headers & Cookies\nhttp.request.headers[\"authorization\"][0]\nhttp.request.cookies[\"session\"][0]\nlower(http.user_agent)      // Lowercase user agent\n```\n\n### Operators\n\n```typescript\n// Comparison\neq      // Equal\nne      // Not equal\nlt      // Less than\nle      // Less than or equal\ngt      // Greater than\nge      // Greater than or equal\n\n// String matching\ncontains        // Substring match\nmatches         // Regex match (use carefully)\nstarts_with     // Prefix match\nends_with       // Suffix match\n\n// List operations\nin              // Value in list\nnot             // Logical NOT\nand             // Logical AND\nor              // Logical OR\n```\n\n### Expression Examples\n\n```typescript\n'cf.waf.score gt 40' // Attack score\n'http.request.uri.path eq \"/api/login\" and http.request.method eq \"POST\"' // Path + method\n'ip.src in {192.0.2.0/24 203.0.113.0/24}' // IP blocking\n'ip.geoip.country in {\"CN\" \"RU\" \"KP\"}' // Country blocking\n'http.user_agent contains \"bot\"' // User agent\n'not http.request.headers[\"authorization\"][0]' // Header check\n'(cf.waf.score.sqli gt 20 or cf.waf.score.xss gt 20) and http.request.uri.path starts_with \"/api\"' // Complex\n```\n\n## Rate Limiting Configuration\n\n```typescript\n{\n  action: 'block',\n  expression: 'http.request.uri.path starts_with \"/api\"',\n  action_parameters: {\n    ratelimit: {\n      // Characteristics define uniqueness: 'ip.src', 'cf.colo.id', \n      // 'http.request.headers[\"key\"][0]', 'http.request.cookies[\"session\"][0]'\n      characteristics: ['cf.colo.id', 'ip.src'], // Recommended: per-IP per-datacenter\n      period: 60,                      // Time window in seconds\n      requests_per_period: 100,        // Max requests in period\n      mitigation_timeout: 600,         // Block duration in seconds\n      counting_expression: 'http.request.method ne \"GET\"', // Optional: filter counted requests\n      requests_to_origin: false,       // Count all requests (not just origin hits)\n    },\n  },\n  enabled: true,\n}\n```\n\n## Managed Ruleset Deployment\n\n```typescript\n{\n  action: 'execute',\n  action_parameters: {\n    id: 'efb7b8c949ac4650a09736fc376e9aee', // Cloudflare Managed\n    overrides: {\n      // Override specific rules\n      rules: [\n        { id: '5de7edfa648c4d6891dc3e7f84534ffa', action: 'log', enabled: true },\n      ],\n      // Override categories: 'wordpress', 'sqli', 'xss', 'rce', etc.\n      categories: [\n        { category: 'wordpress', enabled: false },\n        { category: 'sqli', action: 'log' },\n      ],\n    },\n  },\n  expression: 'true',\n  enabled: true,\n}\n```\n\n## Skip Rules\n\nSkip rules bypass subsequent rule evaluation. Two skip types:\n\n**Skip current ruleset**: Skip remaining rules in current phase only\n```typescript\n{\n  action: 'skip',\n  action_parameters: {\n    ruleset: 'current', // Skip rest of current ruleset\n  },\n  expression: 'http.request.uri.path ends_with \".jpg\" or http.request.uri.path ends_with \".css\"',\n  enabled: true,\n}\n```\n\n**Skip entire phases**: Skip one or more phases completely\n```typescript\n{\n  action: 'skip',\n  action_parameters: {\n    phases: ['http_request_firewall_managed', 'http_ratelimit'], // Skip multiple phases\n  },\n  expression: 'ip.src in {192.0.2.0/24 203.0.113.0/24}',\n  enabled: true,\n}\n```\n\n**Note**: Skip rules in custom phase can skip managed/ratelimit phases, but not vice versa (execution order)."
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/waf/configuration.md",
    "content": "# Configuration\n\n## Prerequisites\n\n**API Token**: Create at https://dash.cloudflare.com/profile/api-tokens\n- Permission: `Zone.WAF Edit` or `Zone.Firewall Services Edit`\n- Zone Resources: Include specific zones or all zones\n\n**Zone ID**: Found in dashboard > Overview > API section (right sidebar)\n\n```bash\n# Set environment variables\nexport CF_API_TOKEN=\"your_api_token_here\"\nexport ZONE_ID=\"your_zone_id_here\"\n```\n\n## TypeScript SDK Usage\n\n```bash\nnpm install cloudflare\n```\n\n```typescript\nimport Cloudflare from 'cloudflare';\n\nconst client = new Cloudflare({ apiToken: process.env.CF_API_TOKEN });\n\n// Custom rules\nawait client.rulesets.create({\n  zone_id: process.env.ZONE_ID,\n  kind: 'zone',\n  phase: 'http_request_firewall_custom',\n  name: 'Custom WAF',\n  rules: [\n    { action: 'block', expression: 'cf.waf.score gt 50', enabled: true },\n    { action: 'challenge', expression: 'http.request.uri.path eq \"/admin\"', enabled: true },\n  ],\n});\n\n// Managed ruleset\nawait client.rulesets.create({\n  zone_id: process.env.ZONE_ID,\n  phase: 'http_request_firewall_managed',\n  rules: [{\n    action: 'execute',\n    action_parameters: { id: 'efb7b8c949ac4650a09736fc376e9aee' },\n    expression: 'true',\n  }],\n});\n\n// Rate limiting\nawait client.rulesets.create({\n  zone_id: process.env.ZONE_ID,\n  phase: 'http_ratelimit',\n  rules: [{\n    action: 'block',\n    expression: 'http.request.uri.path starts_with \"/api\"',\n    action_parameters: {\n      ratelimit: {\n        characteristics: ['cf.colo.id', 'ip.src'],\n        period: 60,\n        requests_per_period: 100,\n        mitigation_timeout: 600,\n      },\n    },\n  }],\n});\n```\n\n## Terraform Configuration\n\n```hcl\nprovider \"cloudflare\" {\n  api_token = var.cloudflare_api_token\n}\n\nresource \"cloudflare_ruleset\" \"waf_custom\" {\n  zone_id = var.zone_id\n  kind    = \"zone\"\n  phase   = \"http_request_firewall_custom\"\n\n  rules {\n    action     = \"block\"\n    expression = \"cf.waf.score gt 50\"\n  }\n}\n```\n\n**Managed Ruleset & Rate Limiting**:\n```hcl\nresource \"cloudflare_ruleset\" \"waf_managed\" {\n  zone_id = var.zone_id\n  name    = \"Managed Ruleset\"\n  kind    = \"zone\"\n  phase   = \"http_request_firewall_managed\"\n\n  rules {\n    action = \"execute\"\n    action_parameters {\n      id = \"efb7b8c949ac4650a09736fc376e9aee\"\n      overrides {\n        rules {\n          id = \"5de7edfa648c4d6891dc3e7f84534ffa\"\n          action = \"log\"\n        }\n      }\n    }\n    expression = \"true\"\n  }\n}\n\nresource \"cloudflare_ruleset\" \"rate_limiting\" {\n  zone_id = var.zone_id\n  phase   = \"http_ratelimit\"\n\n  rules {\n    action = \"block\"\n    expression = \"http.request.uri.path starts_with \\\"/api\\\"\"\n    ratelimit {\n      characteristics     = [\"cf.colo.id\", \"ip.src\"]\n      period              = 60\n      requests_per_period = 100\n      mitigation_timeout  = 600\n    }\n  }\n}\n```\n\n## Pulumi Configuration\n\n```typescript\nimport * as cloudflare from '@pulumi/cloudflare';\n\nconst zoneId = 'zone_id';\n\n// Custom rules\nconst wafCustom = new cloudflare.Ruleset('waf-custom', {\n  zoneId,\n  phase: 'http_request_firewall_custom',\n  rules: [\n    { action: 'block', expression: 'cf.waf.score gt 50', enabled: true },\n    { action: 'challenge', expression: 'http.request.uri.path eq \"/admin\"', enabled: true },\n  ],\n});\n\n// Managed ruleset\nconst wafManaged = new cloudflare.Ruleset('waf-managed', {\n  zoneId,\n  phase: 'http_request_firewall_managed',\n  rules: [{\n    action: 'execute',\n    actionParameters: { id: 'efb7b8c949ac4650a09736fc376e9aee' },\n    expression: 'true',\n  }],\n});\n\n// Rate limiting\nconst rateLimiting = new cloudflare.Ruleset('rate-limiting', {\n  zoneId,\n  phase: 'http_ratelimit',\n  rules: [{\n    action: 'block',\n    expression: 'http.request.uri.path starts_with \"/api\"',\n    ratelimit: {\n      characteristics: ['cf.colo.id', 'ip.src'],\n      period: 60,\n      requestsPerPeriod: 100,\n      mitigationTimeout: 600,\n    },\n  }],\n});\n```\n\n## Dashboard Configuration\n\n1. Navigate to: **Security** > **WAF**\n2. Select tab:\n   - **Managed rules** - Deploy/configure managed rulesets\n   - **Custom rules** - Create custom rules\n   - **Rate limiting rules** - Configure rate limits\n3. Click **Deploy** or **Create rule**\n\n**Testing**: Use Security Events to test expressions before deploying.\n\n## Wrangler Integration\n\nWAF configuration is zone-level (not Worker-specific). Configuration methods:\n- Dashboard UI\n- Cloudflare API via SDK\n- Terraform/Pulumi (IaC)\n\n**Workers benefit from WAF automatically** - no Worker code changes needed.\n\n**Example: Query WAF API from Worker**:\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    return fetch(`https://api.cloudflare.com/client/v4/zones/${env.ZONE_ID}/rulesets`, {\n      headers: { 'Authorization': `Bearer ${env.CF_API_TOKEN}` },\n    });\n  },\n};\n```"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/waf/gotchas.md",
    "content": "# Gotchas & Troubleshooting\n\n## Execution Order\n\n**Problem:** Rules execute in unexpected order\n**Cause:** Misunderstanding phase execution\n**Solution:**\n\nPhases execute sequentially (can't be changed):\n1. `http_request_firewall_custom` - Custom rules\n2. `http_request_firewall_managed` - Managed rulesets\n3. `http_ratelimit` - Rate limiting\n\nWithin phase: top-to-bottom, first match wins (unless `skip`)\n\n```typescript\n// WRONG: Can't mix phase-specific actions\nawait client.rulesets.create({\n  phase: 'http_request_firewall_custom',\n  rules: [\n    { action: 'block', expression: 'cf.waf.score gt 50' },\n    { action: 'execute', action_parameters: { id: 'managed_id' } }, // WRONG\n  ],\n});\n\n// CORRECT: Separate rulesets per phase\nawait client.rulesets.create({ phase: 'http_request_firewall_custom', rules: [...] });\nawait client.rulesets.create({ phase: 'http_request_firewall_managed', rules: [...] });\n```\n\n## Expression Errors\n\n**Problem:** Syntax errors prevent deployment\n**Cause:** Invalid field/operator/syntax\n**Solution:**\n\n```typescript\n// Common mistakes\n'http.request.path' → 'http.request.uri.path' // Correct field\n'ip.geoip.country eq US' → 'ip.geoip.country eq \"US\"' // Quote strings\n'http.user_agent eq \"Mozilla\"' → 'lower(http.user_agent) contains \"mozilla\"' // Case sensitivity\n'matches \".*[.jpg\"' → 'matches \".*\\\\.jpg$\"' // Valid regex\n```\n\nTest expressions in Security Events before deploying.\n\n## Skip Rule Pitfalls\n\n**Problem:** Skip rules don't work as expected\n**Cause:** Misunderstanding skip scope\n**Solution:**\n\nSkip types:\n- `ruleset: 'current'` - Skip remaining rules in current ruleset only\n- `phases: ['phase_name']` - Skip entire phases\n\n```typescript\n// WRONG: Trying to skip managed rules from custom phase\n// In http_request_firewall_custom:\n{\n  action: 'skip',\n  action_parameters: { ruleset: 'current' },\n  expression: 'ip.src in {192.0.2.0/24}',\n}\n// This only skips remaining custom rules, not managed rules\n\n// CORRECT: Skip specific phases\n{\n  action: 'skip',\n  action_parameters: {\n    phases: ['http_request_firewall_managed', 'http_ratelimit'],\n  },\n  expression: 'ip.src in {192.0.2.0/24}',\n}\n```\n\n## Update Replaces All Rules\n\n**Problem:** Updating ruleset deletes other rules\n**Cause:** `update()` replaces entire rule list\n**Solution:**\n\n```typescript\n// WRONG: This deletes all existing rules!\nawait client.rulesets.update({\n  zone_id: 'zone_id',\n  ruleset_id: 'ruleset_id',\n  rules: [{ action: 'block', expression: 'cf.waf.score gt 50' }],\n});\n\n// CORRECT: Get existing rules first\nconst ruleset = await client.rulesets.get({ zone_id: 'zone_id', ruleset_id: 'ruleset_id' });\nawait client.rulesets.update({\n  zone_id: 'zone_id',\n  ruleset_id: 'ruleset_id',\n  rules: [...ruleset.rules, { action: 'block', expression: 'cf.waf.score gt 50' }],\n});\n```\n\n## Override Conflicts\n\n**Problem:** Managed ruleset overrides don't apply\n**Cause:** Rule ID doesn't exist or category name incorrect\n**Solution:**\n\n```typescript\n// List managed ruleset rules to find IDs\nconst ruleset = await client.rulesets.get({\n  zone_id: 'zone_id',\n  ruleset_id: 'efb7b8c949ac4650a09736fc376e9aee',\n});\nconsole.log(ruleset.rules.map(r => ({ id: r.id, description: r.description })));\n\n// Use correct IDs in overrides\n{ action: 'execute', action_parameters: { id: 'efb7b8c949ac4650a09736fc376e9aee', \n  overrides: { rules: [{ id: '5de7edfa648c4d6891dc3e7f84534ffa', action: 'log' }] } } }\n```\n\n## False Positives\n\n**Problem:** Legitimate traffic blocked\n**Cause:** Aggressive rules/thresholds\n**Solution:**\n\n1. Start with log mode: `overrides: { action: 'log' }`\n2. Review Security Events to identify false positives\n3. Override specific rules: `overrides: { rules: [{ id: 'rule_id', action: 'log' }] }`\n\n## Rate Limiting NAT Issues\n\n**Problem:** Users behind NAT hit rate limits too quickly\n**Cause:** Multiple users sharing single IP\n**Solution:**\n\nAdd more characteristics: User-Agent, session cookie, or authorization header\n```typescript\n{\n  action: 'block',\n  expression: 'http.request.uri.path starts_with \"/api\"',\n  action_parameters: {\n    ratelimit: {\n      characteristics: ['cf.colo.id', 'ip.src', 'http.request.cookies[\"session\"][0]'],\n      period: 60,\n      requests_per_period: 100,\n    },\n  },\n}\n```\n\n## Performance Issues\n\n**Problem:** Increased latency\n**Cause:** Complex expressions, excessive rules\n**Solution:**\n\n1. Skip static assets early: `action: 'skip'` for `\\\\.(jpg|css|js)$`\n2. Path-based deployment: Only run managed on `/api` or `/admin`\n3. Disable unused categories: `{ category: 'wordpress', enabled: false }`\n4. Prefer string operators over regex: `starts_with` vs `matches`\n\n## Limits & Quotas\n\n| Resource | Free | Pro | Business | Enterprise |\n|----------|------|-----|----------|------------|\n| Custom rules | 5 | 20 | 100 | 1000 |\n| Rate limiting rules | 1 | 10 | 25 | 100 |\n| Rule expression length | 4096 chars | 4096 chars | 4096 chars | 4096 chars |\n| Rules per ruleset | 75 | 75 | 400 | 1000 |\n| Managed rulesets | Yes | Yes | Yes | Yes |\n| Rate limit characteristics | 2 | 3 | 5 | 5 |\n\n**Important Notes:**\n- Rules execute in order; first match wins (except skip rules)\n- Expression evaluation stops at first `false` in AND chains\n- `matches` regex operator is slower than string operators\n- Rate limit counting happens before mitigation\n\n## API Errors\n\n**Problem:** API calls fail with cryptic errors\n**Cause:** Invalid parameters or permissions\n**Solution:**\n\n```typescript\n// Error: \"Invalid phase\" → Use exact phase name\nphase: 'http_request_firewall_custom'\n\n// Error: \"Ruleset already exists\" → Use update() or list first\nconst rulesets = await client.rulesets.list({ zone_id, phase: 'http_request_firewall_custom' });\nif (rulesets.result.length > 0) {\n  await client.rulesets.update({ zone_id, ruleset_id: rulesets.result[0].id, rules: [...] });\n}\n\n// Error: \"Action not supported\" → Check phase/action compatibility\n// 'execute' only in http_request_firewall_managed\n// Rate limit config only in http_ratelimit phase\n\n// Error: \"Expression parse error\" → Common fixes:\n'ip.geoip.country eq \"US\"'   // Quote strings\n'cf.waf.score gt 40'         // Use 'gt' not '>'\n'http.request.uri.path'      // Not 'http.request.path'\n```\n\n**Tip**: Test expressions in dashboard Security Events before deploying.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/waf/patterns.md",
    "content": "# Common Patterns\n\n## Deploy Managed Rulesets\n\n```typescript\n// Deploy Cloudflare Managed Ruleset (default)\nawait client.rulesets.create({\n  zone_id: 'zone_id',\n  kind: 'zone',\n  phase: 'http_request_firewall_managed',\n  name: 'Cloudflare Managed Ruleset',\n  rules: [{\n    action: 'execute',\n    action_parameters: {\n      id: 'efb7b8c949ac4650a09736fc376e9aee', // Cloudflare Managed\n      // Or: '4814384a9e5d4991b9815dcfc25d2f1f' for OWASP CRS\n      // Or: 'c2e184081120413c86c3ab7e14069605' for Exposed Credentials\n    },\n    expression: 'true', // All requests\n    // Or: 'http.request.uri.path starts_with \"/api\"' for specific paths\n    enabled: true,\n  }],\n});\n```\n\n## Override Managed Ruleset\n\n```typescript\nawait client.rulesets.create({\n  zone_id: 'zone_id',\n  phase: 'http_request_firewall_managed',\n  rules: [{\n    action: 'execute',\n    action_parameters: {\n      id: 'efb7b8c949ac4650a09736fc376e9aee',\n      overrides: {\n        // Override specific rules\n        rules: [\n          { id: '5de7edfa648c4d6891dc3e7f84534ffa', action: 'log' },\n          { id: '75a0060762034b9dad4e883afc121b4c', enabled: false },\n        ],\n        // Override categories: wordpress, sqli, xss, rce, etc.\n        categories: [\n          { category: 'wordpress', enabled: false },\n          { category: 'sqli', action: 'log' },\n        ],\n      },\n    },\n    expression: 'true',\n  }],\n});\n```\n\n## Custom Rules\n\n```typescript\nawait client.rulesets.create({\n  zone_id: 'zone_id',\n  kind: 'zone',\n  phase: 'http_request_firewall_custom',\n  name: 'Custom WAF Rules',\n  rules: [\n    // Attack score-based\n    { action: 'block', expression: 'cf.waf.score gt 50', enabled: true },\n    { action: 'challenge', expression: 'cf.waf.score gt 20', enabled: true },\n    \n    // Specific attack types\n    { action: 'block', expression: 'cf.waf.score.sqli gt 30 or cf.waf.score.xss gt 30', enabled: true },\n    \n    // Geographic blocking\n    { action: 'block', expression: 'ip.geoip.country in {\"CN\" \"RU\"}', enabled: true },\n  ],\n});\n```\n\n## Rate Limiting\n\n```typescript\nawait client.rulesets.create({\n  zone_id: 'zone_id',\n  kind: 'zone',\n  phase: 'http_ratelimit',\n  name: 'Rate Limits',\n  rules: [\n    // Per-IP global limit\n    {\n      action: 'block',\n      expression: 'true',\n      action_parameters: {\n        ratelimit: {\n          characteristics: ['cf.colo.id', 'ip.src'],\n          period: 60,\n          requests_per_period: 100,\n          mitigation_timeout: 600,\n        },\n      },\n    },\n    \n    // Login endpoint (stricter)\n    {\n      action: 'block',\n      expression: 'http.request.uri.path eq \"/api/login\"',\n      action_parameters: {\n        ratelimit: {\n          characteristics: ['ip.src'],\n          period: 60,\n          requests_per_period: 5,\n          mitigation_timeout: 600,\n        },\n      },\n    },\n    \n    // API writes only (using counting_expression)\n    {\n      action: 'block',\n      expression: 'http.request.uri.path starts_with \"/api\"',\n      action_parameters: {\n        ratelimit: {\n          characteristics: ['cf.colo.id', 'ip.src'],\n          period: 60,\n          requests_per_period: 50,\n          counting_expression: 'http.request.method ne \"GET\"',\n        },\n      },\n    },\n  ],\n});\n```\n\n## Skip Rules\n\n```typescript\nawait client.rulesets.create({\n  zone_id: 'zone_id',\n  kind: 'zone',\n  phase: 'http_request_firewall_custom',\n  name: 'Skip Rules',\n  rules: [\n    // Skip static assets (current ruleset only)\n    {\n      action: 'skip',\n      action_parameters: { ruleset: 'current' },\n      expression: 'http.request.uri.path matches \"\\\\.(jpg|css|js|woff2?)$\"',\n    },\n    \n    // Skip all WAF phases for trusted IPs\n    {\n      action: 'skip',\n      action_parameters: {\n        phases: ['http_request_firewall_managed', 'http_ratelimit'],\n      },\n      expression: 'ip.src in {192.0.2.0/24}',\n    },\n  ],\n});\n```\n\n## Complete Setup Example\n\nCombine all three phases for comprehensive protection:\n\n```typescript\nconst client = new Cloudflare({ apiToken: process.env.CF_API_TOKEN });\nconst zoneId = process.env.ZONE_ID;\n\n// 1. Custom rules (execute first)\nawait client.rulesets.create({\n  zone_id: zoneId,\n  phase: 'http_request_firewall_custom',\n  rules: [\n    { action: 'skip', action_parameters: { phases: ['http_request_firewall_managed', 'http_ratelimit'] }, expression: 'ip.src in {192.0.2.0/24}' },\n    { action: 'block', expression: 'cf.waf.score gt 50' },\n    { action: 'managed_challenge', expression: 'cf.waf.score gt 20' },\n  ],\n});\n\n// 2. Managed ruleset (execute second)\nawait client.rulesets.create({\n  zone_id: zoneId,\n  phase: 'http_request_firewall_managed',\n  rules: [{\n    action: 'execute',\n    action_parameters: { id: 'efb7b8c949ac4650a09736fc376e9aee', overrides: { categories: [{ category: 'wordpress', enabled: false }] } },\n    expression: 'true',\n  }],\n});\n\n// 3. Rate limiting (execute third)\nawait client.rulesets.create({\n  zone_id: zoneId,\n  phase: 'http_ratelimit',\n  rules: [\n    { action: 'block', expression: 'true', action_parameters: { ratelimit: { characteristics: ['cf.colo.id', 'ip.src'], period: 60, requests_per_period: 100, mitigation_timeout: 600 } } },\n    { action: 'block', expression: 'http.request.uri.path eq \"/api/login\"', action_parameters: { ratelimit: { characteristics: ['ip.src'], period: 60, requests_per_period: 5, mitigation_timeout: 600 } } },\n  ],\n});\n```"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/web-analytics/README.md",
    "content": "# Cloudflare Web Analytics\n\nPrivacy-first web analytics providing Core Web Vitals, traffic metrics, and user insights without compromising visitor privacy.\n\n## Overview\n\nCloudflare Web Analytics provides:\n- **Core Web Vitals** - LCP, FID, CLS, INP, TTFB monitoring\n- **Page views & visits** - Traffic patterns without cookies\n- **Referrers & paths** - Traffic sources and popular pages\n- **Device & browser data** - User agent breakdown\n- **Geographic data** - Country-level visitor distribution\n- **Privacy-first** - No cookies, fingerprinting, or PII collection\n- **Free** - No cost, unlimited pageviews\n\n**Important:** Web Analytics is **dashboard-only**. No API exists for programmatic data access.\n\n## Quick Start Decision Tree\n\n```\nIs your site proxied through Cloudflare?\n├─ YES → Use automatic injection (configuration.md)\n│   ├─ Enable auto-injection in dashboard\n│   └─ No code changes needed (unless Cache-Control: no-transform)\n│\n└─ NO → Use manual beacon integration (integration.md)\n    ├─ Add JS snippet to HTML\n    ├─ Use spa: true for React/Vue/Next.js\n    └─ Configure CSP if needed\n```\n\n## Reading Order\n\n1. **[configuration.md](configuration.md)** - Setup for proxied vs non-proxied sites\n2. **[integration.md](integration.md)** - Framework-specific beacon integration (React, Next.js, Vue, Nuxt, etc.)\n3. **[patterns.md](patterns.md)** - Common use cases (performance monitoring, GDPR consent, multi-site tracking)\n4. **[gotchas.md](gotchas.md)** - Troubleshooting (SPA tracking, CSP issues, hash routing limitations)\n\n## When to Use Each File\n\n- **Setting up for first time?** → Start with configuration.md\n- **Using React/Next.js/Vue/Nuxt?** → Go to integration.md for framework code\n- **Need GDPR consent loading?** → See patterns.md\n- **Beacon not loading or no data?** → Check gotchas.md\n- **SPA not tracking navigation?** → See integration.md for `spa: true` config\n\n## Key Concepts\n\n### Proxied vs Non-Proxied Sites\n\n| Type | Description | Beacon Injection | Limit |\n|------|-------------|------------------|-------|\n| **Proxied** | DNS through Cloudflare (orange cloud) | Automatic or manual | Unlimited |\n| **Non-proxied** | External hosting, manual beacon | Manual only | 10 sites max |\n\n### SPA Mode\n\n**Critical for modern frameworks:**\n```json\n{\"token\": \"YOUR_TOKEN\", \"spa\": true}\n```\n\nWithout `spa: true`, client-side navigation (React Router, Vue Router, Next.js routing) will NOT be tracked. Only initial page loads will register.\n\n### CSP Requirements\n\nIf using Content Security Policy, allow both domains:\n```\nscript-src https://static.cloudflareinsights.com https://cloudflareinsights.com;\n```\n\n## Features\n\n### Core Web Vitals Debugging\n- **LCP (Largest Contentful Paint)** - Identifies slow-loading hero images/elements\n- **FID (First Input Delay)** - Interaction responsiveness (legacy metric)\n- **INP (Interaction to Next Paint)** - Modern interaction responsiveness metric\n- **CLS (Cumulative Layout Shift)** - Visual stability issues\n- **TTFB (Time to First Byte)** - Server response performance\n\nDashboard shows top 5 problematic elements with CSS selectors for debugging.\n\n### Traffic Filters\n- **Bot filtering** - Exclude automated traffic from metrics\n- **Date ranges** - Custom time period analysis\n- **Geographic** - Country-level filtering\n- **Device type** - Desktop, mobile, tablet breakdown\n- **Browser/OS** - User agent filtering\n\n### Rules (Advanced - Plan-dependent)\n\nCreate custom tracking rules for advanced configurations:\n\n**Sample Rate Rules:**\n- Reduce data collection percentage for high-traffic sites\n- Example: Track only 50% of visitors to reduce volume\n\n**Path-Based Rules:**\n- Different behavior per route\n- Example: Exclude `/admin/*` or `/internal/*` from tracking\n\n**Host-Based Rules:**\n- Multi-domain configurations\n- Example: Separate tracking for staging vs production subdomains\n\n**Availability:** Rules feature depends on your Cloudflare plan. Check dashboard under Web Analytics → Rules to see if available. Free plans may have limited or no access.\n\n## Plan Limits\n\n| Feature | Free | Notes |\n|---------|------|-------|\n| Proxied sites | Unlimited | DNS through Cloudflare |\n| Non-proxied sites | 10 | External hosting |\n| Pageviews | Unlimited | No volume limits |\n| Data retention | 6 months | Rolling window |\n| Rules | Plan-dependent | Check dashboard |\n\n## Privacy & Compliance\n\n- **No cookies** - Zero client-side storage\n- **No fingerprinting** - No tracking across sites\n- **No PII** - IP addresses not stored\n- **GDPR-friendly** - Minimal data collection\n- **CCPA-compliant** - No personal data sale\n\n**EU opt-out:** Dashboard option to exclude EU visitor data entirely.\n\n## Limitations\n\n- **Dashboard-only** - No API for programmatic access\n- **No real-time** - 5-10 minute data delay\n- **No custom events** - Automatic pageview/navigation tracking only\n- **History API only** - Hash-based routing (`#/path`) not supported\n- **No session replay** - Metrics only, no user recordings\n- **No form tracking** - Page navigation tracking only\n\n## See Also\n\n- [Cloudflare Web Analytics Docs](https://developers.cloudflare.com/analytics/web-analytics/)\n- [Core Web Vitals Guide](https://web.dev/vitals/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/web-analytics/configuration.md",
    "content": "# Configuration\n\n## Setup Methods\n\n### Proxied Sites (Automatic)\n\nDashboard → Web Analytics → Add site → Select hostname → Done\n\n| Injection Option | Description |\n|------------------|-------------|\n| Enable | Auto-inject for all visitors (default) |\n| Enable, excluding EU | No injection for EU (GDPR) |\n| Enable with manual snippet | You add beacon manually |\n| Disable | Pause tracking |\n\n**Fails if response has:** `Cache-Control: public, no-transform`\n\n**CSP required:**\n```\nscript-src https://static.cloudflareinsights.com https://cloudflareinsights.com;\n```\n\n### Non-Proxied Sites (Manual)\n\nDashboard → Web Analytics → Add site → Enter hostname → Copy snippet\n\n```html\n<script defer src='https://static.cloudflareinsights.com/beacon.min.js' \n        data-cf-beacon='{\"token\": \"YOUR_TOKEN\", \"spa\": true}'></script>\n```\n\n**Limits:** 10 non-proxied sites per account\n\n## SPA Mode\n\n**Enable `spa: true` for:** React Router, Next.js, Vue Router, Nuxt, SvelteKit, Angular\n\n**Keep `spa: false` for:** Traditional multi-page apps, static sites, WordPress\n\n**Hash routing (`#/path`) NOT supported** - use History API routing.\n\n## Token Management\n\n- Found in: Dashboard → Web Analytics → Manage site\n- **Not secrets** - domain-locked, safe to expose in HTML\n- Each site gets unique token\n\n## Environment Config\n\n```typescript\n// Only load in production\nif (process.env.NODE_ENV === 'production') {\n  // Load beacon\n}\n```\n\nOr use environment-specific tokens via env vars.\n\n## Verify Installation\n\n1. DevTools Network → filter `cloudflareinsights` → see `beacon.min.js` + data request\n2. No CSP/CORS errors in console\n3. Dashboard shows pageviews after 5-10 min delay\n\n## Rules (Plan-dependent)\n\nConfigure in dashboard for:\n- **Sample rate** - reduce collection % for high-traffic\n- **Path-based** - different behavior per route\n- **Host-based** - separate tracking per domain\n\n## Data Retention\n\n- 6 months rolling window\n- 1-hour bucket granularity\n- No raw export, dashboard only\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/web-analytics/gotchas.md",
    "content": "# Web Analytics Gotchas\n\n## Critical Issues\n\n### SPA Navigation Not Tracked\n\n**Symptom:** Only initial pageload counted  \n**Fix:** Add `spa: true`:\n```html\n<script data-cf-beacon='{\"token\": \"TOKEN\", \"spa\": true}' ...></script>\n```\n\n### CSP Blocking Beacon\n\n**Symptom:** Console error \"Refused to load script\"  \n**Fix:** Allow both domains:\n```\nscript-src 'self' https://static.cloudflareinsights.com https://cloudflareinsights.com;\n```\n\n### Hash-Based Routing Unsupported\n\n**Symptom:** `#/path` URLs not tracked  \n**Fix:** Migrate to History API (`BrowserRouter`, not `HashRouter`). No workaround for hash routing.\n\n### No Data Appearing\n\n**Causes & Fixes:**\n1. **Delay** - Wait 5-15 minutes\n2. **Wrong token** - Verify matches dashboard exactly\n3. **Script blocked** - Check DevTools Network tab for beacon.min.js\n4. **Domain mismatch** - Dashboard site must match actual URL\n\n### Auto-Injection Fails\n\n**Cause:** `Cache-Control: no-transform` header  \n**Fix:** Remove `no-transform` or install beacon manually\n\n### Duplicate Pageviews\n\n**Cause:** Multiple beacon scripts  \n**Fix:** Keep only one beacon per page\n\n## Configuration Issues\n\n| Issue | Fix |\n|-------|-----|\n| 10-site limit reached | Delete old sites or proxy through CF (unlimited) |\n| Token not recognized | Use exact alphanumeric token from dashboard |\n\n## Framework-Specific\n\n### Next.js Hydration Warning\n\n```tsx\n<script suppressHydrationWarning ... />\n```\n\n### Gatsby Window Undefined\n\nUse `gatsby-browser.js` to load client-side only.\n\n## Limits\n\n| Resource | Limit |\n|----------|-------|\n| Non-proxied sites | 10 |\n| Proxied sites | Unlimited |\n| Data retention | 6 months |\n| Ingestion delay | 5-10 min |\n| API access | None (dashboard only) |\n\n## When NOT to Use Web Analytics\n\nUse alternatives if you need:\n- Custom event tracking\n- Real-time data\n- User-level tracking\n- Conversion funnels\n- Data export/API access\n\n**Web Analytics excels at:** Core Web Vitals, basic traffic, privacy compliance, free unlimited pageviews.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/web-analytics/integration.md",
    "content": "# Framework Integration\n\n**Web Analytics is dashboard-only** - no programmatic API. This covers beacon integration.\n\n## Basic HTML\n\n```html\n<script defer src='https://static.cloudflareinsights.com/beacon.min.js' \n        data-cf-beacon='{\"token\": \"YOUR_TOKEN\", \"spa\": true}'></script>\n```\n\nPlace before closing `</body>` tag.\n\n## Framework Examples\n\n| Framework | Location | Notes |\n|-----------|----------|-------|\n| React/Vite | `public/index.html` | Add `spa: true` |\n| Next.js App Router | `app/layout.tsx` | Use `<Script strategy=\"afterInteractive\">` |\n| Next.js Pages | `pages/_document.tsx` | Use `<Script>` |\n| Nuxt 3 | `app.vue` with `useHead()` | Or use plugin |\n| Vue 3/Vite | `index.html` | Add `spa: true` |\n| Gatsby | `gatsby-browser.js` | `onClientEntry` hook |\n| SvelteKit | `src/app.html` | Before `</body>` |\n| Astro | Layout component | Before `</body>` |\n| Angular | `src/index.html` | Add `spa: true` |\n| Docusaurus | `docusaurus.config.js` | In `scripts` array |\n\n## Configuration\n\n```json\n{\n  \"token\": \"YOUR_TOKEN\",\n  \"spa\": true\n}\n```\n\n**Use `spa: true` for:** React Router, Vue Router, Next.js, Nuxt, Gatsby, SvelteKit, Angular\n\n**Use `spa: false` for:** Traditional server-rendered (PHP, Django, Rails, WordPress)\n\n## CSP Headers\n\n```\nscript-src 'self' https://static.cloudflareinsights.com;\nconnect-src 'self' https://cloudflareinsights.com;\n```\n\n## GDPR Consent\n\n```typescript\n// Load conditionally based on consent\nif (localStorage.getItem('analytics-consent') === 'true') {\n  const script = document.createElement('script');\n  script.src = 'https://static.cloudflareinsights.com/beacon.min.js';\n  script.defer = true;\n  script.setAttribute('data-cf-beacon', '{\"token\": \"YOUR_TOKEN\", \"spa\": true}');\n  document.body.appendChild(script);\n}\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/web-analytics/patterns.md",
    "content": "# Web Analytics Patterns\n\n## Core Web Vitals Debugging\n\nDashboard → Core Web Vitals → Click metric → Debug View shows top 5 problematic elements.\n\n### LCP Fixes\n\n```html\n<!-- Priority hints -->\n<img src=\"hero.jpg\" loading=\"eager\" fetchpriority=\"high\" />\n<link rel=\"preload\" as=\"image\" href=\"/hero.jpg\" fetchpriority=\"high\" />\n```\n\n### CLS Fixes\n\n```css\n/* Reserve space */\n.ad-container { min-height: 250px; }\nimg { width: 400px; height: 300px; } /* Explicit dimensions */\n```\n\n### INP Fixes\n\n```typescript\n// Debounce expensive operations\nconst handleInput = debounce(search, 300);\n\n// Yield to main thread\nawait task(); await new Promise(r => setTimeout(r, 0)); await task2();\n\n// Move to Web Worker for heavy computation\n```\n\n| Metric | Good | Poor |\n|--------|------|------|\n| LCP | ≤2.5s | >4s |\n| INP | ≤200ms | >500ms |\n| CLS | ≤0.1 | >0.25 |\n\n## GDPR Consent\n\n```typescript\n// Load beacon only after consent\nconst consent = localStorage.getItem('analytics-consent');\nif (consent === 'accepted') {\n  const script = document.createElement('script');\n  script.src = 'https://static.cloudflareinsights.com/beacon.min.js';\n  script.setAttribute('data-cf-beacon', '{\"token\": \"TOKEN\", \"spa\": true}');\n  document.body.appendChild(script);\n}\n```\n\nAlternative: Dashboard → \"Enable, excluding visitor data in the EU\"\n\n## SPA Navigation\n\n```html\n<!-- REQUIRED for React/Vue/etc routing -->\n<script data-cf-beacon='{\"token\": \"TOKEN\", \"spa\": true}' ...></script>\n```\n\nWithout `spa: true`: only initial pageload tracked.\n\n## Staging/Production Separation\n\n```typescript\n// Use env-specific tokens\nconst token = process.env.NEXT_PUBLIC_CF_ANALYTICS_TOKEN;\n// .env.production: production token\n// .env.staging: staging token (or empty to disable)\n```\n\n## Bot Filtering\n\nDashboard → Filters → \"Exclude Bot Traffic\"\n\nFilters: Search crawlers, monitoring services, known bots.  \nNot filtered: Headless browsers (Playwright/Puppeteer).\n\n## Ad-Blocker Impact\n\n~25-40% of users may block `cloudflareinsights.com`. No official workaround.\nDashboard shows minimum baseline; use server logs for complete picture.\n\n## Limitations\n\n- No UTM parameter tracking\n- No webhooks/alerts/API\n- No custom beacon domains\n- Max 10 non-proxied sites\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workerd/README.md",
    "content": "# Workerd Runtime\n\nV8-based JS/Wasm runtime powering Cloudflare Workers. Use as app server, dev tool, or HTTP proxy.\n\n## ⚠️ IMPORTANT SECURITY NOTICE\n**workerd is NOT a hardened sandbox.** Do not run untrusted code. It's designed for deploying YOUR code locally/self-hosted, not multi-tenant SaaS. Cloudflare production adds security layers not present in open-source workerd.\n\n## Decision Tree: When to Use What\n\n**95% of users:** Use Wrangler\n- Local development: `wrangler dev` (uses workerd internally)\n- Deployment: `wrangler deploy` (deploys to Cloudflare)\n- Types: `wrangler types` (generates TypeScript types)\n\n**Use raw workerd directly only if:**\n- Self-hosting Workers runtime in production\n- Embedding runtime in C++ application\n- Custom tooling/testing infrastructure\n- Debugging workerd-specific behavior\n\n**Never use workerd for:**\n- Running untrusted/user-submitted code\n- Multi-tenant isolation (not hardened)\n- Production without additional security layers\n\n## Key Features\n- **Standards-based**: Fetch API, Web Crypto, Streams, WebSocket\n- **Nanoservices**: Service bindings with local call performance\n- **Capability security**: Explicit bindings prevent SSRF\n- **Backwards compatible**: Version = max compat date supported\n\n## Architecture\n```\nConfig (workerd.capnp)\n├── Services (workers/endpoints)\n├── Sockets (HTTP/HTTPS listeners)\n└── Extensions (global capabilities)\n```\n\n## Quick Start\n```bash\nworkerd serve config.capnp\nworkerd compile config.capnp myConfig -o binary\nworkerd test config.capnp\n```\n\n## Platform Support & Beta Status\n\n| Platform | Status | Notes |\n|----------|--------|-------|\n| Linux (x64) | Stable | Primary platform |\n| macOS (x64/ARM) | Stable | Full support |\n| Windows | Beta | Use WSL2 for best results |\n| Linux (ARM64) | Experimental | Limited testing |\n\nworkerd is in **active development**. Breaking changes possible. Pin versions in production.\n\n## Core Concepts\n- **Service**: Named endpoint (worker/network/disk/external)\n- **Binding**: Capability-based resource access (KV/DO/R2/services)\n- **Compatibility date**: Feature gate (always set!)\n- **Modules**: ES modules (recommended) or service worker syntax\n\n## Reading Order (Progressive Disclosure)\n\n**Start here:**\n1. This README (overview, decision tree)\n2. [patterns.md](./patterns.md) - Common workflows, framework examples\n\n**When you need details:**\n3. [configuration.md](./configuration.md) - Config format, services, bindings\n4. [api.md](./api.md) - Runtime APIs, TypeScript types\n5. [gotchas.md](./gotchas.md) - Common errors, debugging\n\n## Related References\n- [workers](../workers/) - Workers runtime API documentation\n- [miniflare](../miniflare/) - Testing tool built on workerd\n- [wrangler](../wrangler/) - CLI that uses workerd for local dev\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workerd/api.md",
    "content": "# Workerd APIs\n\n## Worker Code (JS/TS)\n\n### ES Modules (Recommended)\n```javascript\nexport default {\n  async fetch(request, env, ctx) {\n    const value = await env.KV.get(\"key\");           // Bindings in env\n    const response = await env.API.fetch(request);   // Service binding\n    ctx.waitUntil(logRequest(request));              // Background task\n    return new Response(\"OK\");\n  },\n  async adminApi(request, env, ctx) { /* Named entrypoint */ },\n  async queue(batch, env, ctx) { /* Queue consumer */ },\n  async scheduled(event, env, ctx) { /* Cron handler */ }\n};\n```\n\n### TypeScript Types\n\n**Generate from wrangler.toml (Recommended):**\n```bash\nwrangler types  # Output: worker-configuration.d.ts\n```\n\n**Manual types:**\n```typescript\ninterface Env {\n  API: Fetcher;\n  CACHE: KVNamespace;\n  STORAGE: R2Bucket;\n  ROOMS: DurableObjectNamespace;\n  API_KEY: string;\n}\n\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n    return new Response(await env.CACHE.get(\"key\"));\n  }\n};\n```\n\n**Setup:**\n```bash\nnpm install -D @cloudflare/workers-types\n```\n\n```json\n// tsconfig.json\n{\"compilerOptions\": {\"types\": [\"@cloudflare/workers-types\"]}}\n```\n\n### Service Worker Syntax (Legacy)\n```javascript\naddEventListener('fetch', event => {\n  event.respondWith(handleRequest(event.request));\n});\n\nasync function handleRequest(request) {\n  const value = await KV.get(\"key\");  // Bindings as globals\n  return new Response(\"OK\");\n}\n```\n\n### Durable Objects\n```javascript\nexport class Room {\n  constructor(state, env) { this.state = state; this.env = env; }\n  \n  async fetch(request) {\n    const url = new URL(request.url);\n    if (url.pathname === \"/increment\") {\n      const value = (await this.state.storage.get(\"counter\")) || 0;\n      await this.state.storage.put(\"counter\", value + 1);\n      return new Response(String(value + 1));\n    }\n    return new Response(\"Not found\", {status: 404});\n  }\n}\n```\n\n### RPC Between Services\n```javascript\n// Caller: env.AUTH.validateToken(token) returns structured data\nconst user = await env.AUTH.validateToken(request.headers.get(\"Authorization\"));\n\n// Callee: export methods that return data\nexport default {\n  async validateToken(token) { return {id: 123, name: \"Alice\"}; }\n};\n```\n\n## Web Platform APIs\n\n### Fetch\n- `fetch()`, `Request`, `Response`, `Headers`\n- `AbortController`, `AbortSignal`\n\n### Streams\n- `ReadableStream`, `WritableStream`, `TransformStream`\n- Byte streams, BYOB readers\n\n### Web Crypto\n- `crypto.subtle` (encrypt/decrypt/sign/verify)\n- `crypto.randomUUID()`, `crypto.getRandomValues()`\n\n### Encoding\n- `TextEncoder`, `TextDecoder`\n- `atob()`, `btoa()`\n\n### Web Standards\n- `URL`, `URLSearchParams`\n- `Blob`, `File`, `FormData`\n- `WebSocket`\n\n### Server-Sent Events (EventSource)\n```javascript\n// Server-side SSE\nconst { readable, writable } = new TransformStream();\nconst writer = writable.getWriter();\nwriter.write(new TextEncoder().encode('data: Hello\\n\\n'));\nreturn new Response(readable, {headers: {'Content-Type': 'text/event-stream'}});\n```\n\n### HTMLRewriter (HTML Parsing/Transformation)\n```javascript\nconst response = await fetch('https://example.com');\nreturn new HTMLRewriter()\n  .on('a[href]', {\n    element(el) {\n      el.setAttribute('href', `/proxy?url=${encodeURIComponent(el.getAttribute('href'))}`);\n    }\n  })\n  .on('script', { element(el) { el.remove(); } })\n  .transform(response);\n```\n\n### TCP Sockets (Experimental)\n```javascript\nconst socket = await connect({ hostname: 'example.com', port: 80 });\nconst writer = socket.writable.getWriter();\nawait writer.write(new TextEncoder().encode('GET / HTTP/1.1\\r\\n\\r\\n'));\nconst reader = socket.readable.getReader();\nconst { value } = await reader.read();\nreturn new Response(value);\n```\n\n### Performance\n- `performance.now()`, `performance.timeOrigin`\n- `setTimeout()`, `setInterval()`, `queueMicrotask()`\n\n### Console\n- `console.log()`, `console.error()`, `console.warn()`\n\n### Node.js Compat (`nodejs_compat` flag)\n```javascript\nimport { Buffer } from 'node:buffer';\nimport { randomBytes } from 'node:crypto';\n\nconst buf = Buffer.from('Hello');\nconst random = randomBytes(16);\n```\n\n**Available:** `node:buffer`, `node:crypto`, `node:stream`, `node:util`, `node:events`, `node:assert`, `node:path`, `node:querystring`, `node:url`\n**NOT available:** `node:fs`, `node:http`, `node:net`, `node:child_process`\n\n## CLI Commands\n\n```bash\nworkerd serve config.capnp [constantName]          # Start server\nworkerd serve config.capnp --socket-addr http=*:3000 --verbose\nworkerd compile config.capnp constantName -o binary  # Compile to binary\nworkerd test config.capnp [--test-only=test.js]    # Run tests\n```\n\n## Wrangler Integration\n\nUse Wrangler for development:\n```bash\nwrangler dev     # Uses workerd internally\nwrangler types   # Generate TypeScript types from wrangler.toml\n```\n\nSee [patterns.md](./patterns.md) for usage examples, [configuration.md](./configuration.md) for config details.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workerd/configuration.md",
    "content": "# Workerd Configuration\n\n## Basic Structure\n```capnp\nusing Workerd = import \"/workerd/workerd.capnp\";\n\nconst config :Workerd.Config = (\n  services = [(name = \"main\", worker = .mainWorker)],\n  sockets = [(name = \"http\", address = \"*:8080\", http = (), service = \"main\")]\n);\n\nconst mainWorker :Workerd.Worker = (\n  modules = [(name = \"index.js\", esModule = embed \"src/index.js\")],\n  compatibilityDate = \"2024-01-15\",\n  bindings = [...]\n);\n```\n\n## Services\n**Worker**: Run JS/Wasm code\n```capnp\n(name = \"api\", worker = (\n  modules = [(name = \"index.js\", esModule = embed \"index.js\")],\n  compatibilityDate = \"2024-01-15\",\n  bindings = [...]\n))\n```\n\n**Network**: Internet access\n```capnp\n(name = \"internet\", network = (allow = [\"public\"], tlsOptions = (trustBrowserCas = true)))\n```\n\n**External**: Reverse proxy\n```capnp\n(name = \"backend\", external = (address = \"api.com:443\", http = (style = tls)))\n```\n\n**Disk**: Static files\n```capnp\n(name = \"assets\", disk = (path = \"/var/www\", writable = false))\n```\n\n## Sockets\n```capnp\n(name = \"http\", address = \"*:8080\", http = (), service = \"main\")\n(name = \"https\", address = \"*:443\", https = (options = (), tlsOptions = (keypair = (...))), service = \"main\")\n(name = \"app\", address = \"unix:/tmp/app.sock\", http = (), service = \"main\")\n```\n\n## Worker Formats\n```capnp\n# ES Modules (recommended)\nmodules = [(name = \"index.js\", esModule = embed \"src/index.js\"), (name = \"wasm.wasm\", wasm = embed \"build/module.wasm\")]\n\n# Service Worker (legacy)\nserviceWorkerScript = embed \"worker.js\"\n\n# CommonJS\n(name = \"legacy.js\", commonJsModule = embed \"legacy.js\", namedExports = [\"foo\"])\n```\n\n## Bindings\nBindings expose resources to workers. ES modules: `env.BINDING`, Service workers: globals.\n\n### Primitive Types\n```capnp\n(name = \"API_KEY\", text = \"secret\")                    # String\n(name = \"CONFIG\", json = '{\"key\":\"val\"}')              # Parsed JSON\n(name = \"DATA\", data = embed \"data.bin\")               # ArrayBuffer\n(name = \"DATABASE_URL\", fromEnvironment = \"DB_URL\")    # System env var\n```\n\n### Service Binding\n```capnp\n(name = \"AUTH\", service = \"auth-worker\")               # Basic\n(name = \"API\", service = (\n  name = \"backend\",\n  entrypoint = \"adminApi\",                             # Named export\n  props = (json = '{\"role\":\"admin\"}')                  # ctx.props\n))\n```\n\n### Storage\n```capnp\n(name = \"CACHE\", kvNamespace = \"kv-service\")           # KV\n(name = \"STORAGE\", r2Bucket = \"r2-service\")            # R2\n(name = \"ROOMS\", durableObjectNamespace = (\n  serviceName = \"room-service\",\n  className = \"Room\"\n))\n(name = \"FAST\", memoryCache = (\n  id = \"cache-id\",\n  limits = (maxKeys = 1000, maxValueSize = 1048576)\n))\n```\n\n### Other\n```capnp\n(name = \"TASKS\", queue = \"queue-service\")\n(name = \"ANALYTICS\", analyticsEngine = \"analytics\")\n(name = \"LOADER\", workerLoader = (id = \"dynamic\"))\n(name = \"KEY\", cryptoKey = (format = raw, algorithm = (name = \"HMAC\", hash = \"SHA-256\"), keyData = embed \"key.bin\", usages = [sign, verify], extractable = false))\n(name = \"TRACED\", wrapped = (moduleName = \"tracing\", entrypoint = \"makeTracer\", innerBindings = [(name = \"backend\", service = \"backend\")]))\n```\n\n## Compatibility\n```capnp\ncompatibilityDate = \"2024-01-15\"                       # Always set!\ncompatibilityFlags = [\"nodejs_compat\", \"streams_enable_constructors\"]\n```\n\nVersion = max compat date. Update carefully after testing.\n\n## Parameter Bindings (Inheritance)\n```capnp\nconst base :Workerd.Worker = (\n  modules = [...], compatibilityDate = \"2024-01-15\",\n  bindings = [(name = \"API_URL\", parameter = (type = text)), (name = \"DB\", parameter = (type = service))]\n);\n\nconst derived :Workerd.Worker = (\n  inherit = \"base-service\",\n  bindings = [(name = \"API_URL\", text = \"https://api.com\"), (name = \"DB\", service = \"postgres\")]\n);\n```\n\n## Durable Objects Config\n```capnp\nconst worker :Workerd.Worker = (\n  modules = [...],\n  compatibilityDate = \"2024-01-15\",\n  bindings = [(name = \"ROOMS\", durableObjectNamespace = \"Room\")],\n  durableObjectNamespaces = [(className = \"Room\", uniqueKey = \"v1\")],\n  durableObjectStorage = (localDisk = \"/var/do\")\n);\n```\n\n## Remote Bindings (Development)\n\nConnect local workerd to production Cloudflare resources:\n\n```capnp\nbindings = [\n  # Remote KV (requires API token)\n  (name = \"PROD_KV\", kvNamespace = (\n    remote = (\n      accountId = \"your-account-id\",\n      namespaceId = \"your-namespace-id\",\n      apiToken = .envVar(\"CF_API_TOKEN\")\n    )\n  )),\n  \n  # Remote R2\n  (name = \"PROD_R2\", r2Bucket = (\n    remote = (\n      accountId = \"your-account-id\",\n      bucketName = \"my-bucket\",\n      apiToken = .envVar(\"CF_API_TOKEN\")\n    )\n  )),\n  \n  # Remote Durable Object\n  (name = \"PROD_DO\", durableObjectNamespace = (\n    remote = (\n      accountId = \"your-account-id\",\n      scriptName = \"my-worker\",\n      className = \"MyDO\",\n      apiToken = .envVar(\"CF_API_TOKEN\")\n    )\n  ))\n]\n```\n\n**Note:** Remote bindings require network access and valid Cloudflare API credentials.\n\n## Logging & Debugging\n```capnp\nlogging = (structuredLogging = true, stdoutPrefix = \"OUT: \", stderrPrefix = \"ERR: \")\nv8Flags = [\"--expose-gc\", \"--max-old-space-size=2048\"]  # ⚠️ Unsupported in production\n```\n\nSee [patterns.md](./patterns.md) for multi-service examples, [gotchas.md](./gotchas.md) for config errors.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workerd/gotchas.md",
    "content": "# Workerd Gotchas\n\n## Common Errors\n\n### \"Missing compatibility date\"\n**Cause:** Compatibility date not set\n**Solution:**\n❌ Wrong:\n```capnp\nconst worker :Workerd.Worker = (\n  serviceWorkerScript = embed \"worker.js\"\n)\n```\n\n✅ Correct:\n```capnp\nconst worker :Workerd.Worker = (\n  serviceWorkerScript = embed \"worker.js\",\n  compatibilityDate = \"2024-01-15\"  # Always set!\n)\n```\n\n### Wrong Binding Type\n**Problem:** JSON not parsed\n**Cause:** Using `text = '{\"key\":\"value\"}'` instead of `json`\n**Solution:** Use `json = '{\"key\":\"value\"}'` for parsed objects\n\n### Service vs Namespace\n**Problem:** Cannot create DO instance\n**Cause:** Using `service = \"room-service\"` for Durable Object\n**Solution:** Use `durableObjectNamespace = \"Room\"` for DO bindings\n\n### Module Name Mismatch\n**Problem:** Import fails\n**Cause:** Module name includes path: `name = \"src/index.js\"`\n**Solution:** Use simple names: `name = \"index.js\"`, embed with path\n\n## Network Access\n\n**Problem:** Fetch fails with network error\n**Cause:** No network service configured (workerd has no global fetch)\n**Solution:** Add network service binding:\n```capnp\nservices = [(name = \"internet\", network = (allow = [\"public\"]))]\nbindings = [(name = \"NET\", service = \"internet\")]\n```\n\nOr external service:\n```capnp\nbindings = [(name = \"API\", service = (external = (address = \"api.com:443\", http = (style = tls))))]\n```\n\n### \"Worker not responding\"\n**Cause:** Socket misconfigured, no fetch handler, or port unavailable\n**Solution:** Verify socket `address` matches, worker exports `fetch()`, port available\n\n### \"Binding not found\"\n**Cause:** Name mismatch or service doesn't exist\n**Solution:** Check binding name in config matches code (`env.BINDING` for ES modules)\n\n### \"Module not found\"\n**Cause:** Module name doesn't match import or bad embed path\n**Solution:** Module `name` must match import path exactly, verify `embed` path\n\n### \"Compatibility error\"\n**Cause:** Date not set or API unavailable on that date\n**Solution:** Set `compatibilityDate`, verify API available on that date\n\n## Performance Issues\n\n**Problem:** High memory usage\n**Cause:** Large caches or many isolates\n**Solution:** Set cache limits, reduce isolate count, or use V8 flags (caution)\n\n**Problem:** Slow startup\n**Cause:** Many modules or complex config\n**Solution:** Compile to binary (`workerd compile`), reduce imports\n\n**Problem:** Request timeouts\n**Cause:** External service issues or DNS problems\n**Solution:** Check connectivity, DNS resolution, TLS handshake\n\n## Build Issues\n\n**Problem:** Cap'n Proto syntax errors\n**Cause:** Invalid config or missing schema\n**Solution:** Install capnproto tools, validate: `capnp compile -I. config.capnp`\n\n**Problem:** Embed path not found\n**Cause:** Path relative to config file\n**Solution:** Use correct relative path or absolute path\n\n**Problem:** V8 flags cause crashes\n**Cause:** Unsafe V8 flags\n**Solution:** ⚠️ V8 flags unsupported in production. Test thoroughly before use.\n\n## Security Issues\n\n**Problem:** Hardcoded secrets in config\n**Cause:** `text` binding with secret value\n**Solution:** Use `fromEnvironment` to load from env vars\n\n**Problem:** Overly broad network access\n**Cause:** `network = (allow = [\"*\"])`\n**Solution:** Restrict to `allow = [\"public\"]` or specific hosts\n\n**Problem:** Extractable crypto keys\n**Cause:** `cryptoKey = (extractable = true, ...)`\n**Solution:** Set `extractable = false` unless export required\n\n## Compatibility Changes\n\n**Problem:** Breaking changes after compat date update\n**Cause:** New flags enabled between dates\n**Solution:** Review [compat dates docs](https://developers.cloudflare.com/workers/configuration/compatibility-dates/), test locally first\n\n**Problem:** \"Compatibility date not supported\"\n**Cause:** Workerd version older than compat date\n**Solution:** Update workerd binary (version = max compat date supported)\n\n## Limits\n\n| Resource/Limit | Value | Notes |\n|----------------|-------|-------|\n| V8 flags | Unsupported in production | Use with caution |\n| Compatibility date | Must match workerd version | Update if mismatch |\n| Module count | Affects startup time | Many imports slow |\n\n## Troubleshooting Steps\n\n1. **Enable verbose logging**: `workerd serve config.capnp --verbose`\n2. **Check logs**: Look for error messages, stack traces\n3. **Validate config**: `capnp compile -I. config.capnp`\n4. **Test bindings**: Log `Object.keys(env)` to verify\n5. **Check versions**: Workerd version vs compat date\n6. **Isolate issue**: Minimal repro config\n7. **Review schema**: [workerd.capnp](https://github.com/cloudflare/workerd/blob/main/src/workerd/server/workerd.capnp)\n\nSee [configuration.md](./configuration.md) for config details, [patterns.md](./patterns.md) for working examples, [api.md](./api.md) for runtime APIs.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workerd/patterns.md",
    "content": "# Workerd Patterns\n\n## Multi-Service Architecture\n```capnp\nconst config :Workerd.Config = (\n  services = [\n    (name = \"frontend\", worker = (\n      modules = [(name = \"index.js\", esModule = embed \"frontend/index.js\")],\n      compatibilityDate = \"2024-01-15\",\n      bindings = [(name = \"API\", service = \"api\")]\n    )),\n    (name = \"api\", worker = (\n      modules = [(name = \"index.js\", esModule = embed \"api/index.js\")],\n      compatibilityDate = \"2024-01-15\",\n      bindings = [(name = \"DB\", service = \"postgres\"), (name = \"CACHE\", kvNamespace = \"kv\")]\n    )),\n    (name = \"postgres\", external = (address = \"db.internal:5432\", http = ())),\n    (name = \"kv\", disk = (path = \"/var/kv\", writable = true))\n  ],\n  sockets = [(name = \"http\", address = \"*:8080\", http = (), service = \"frontend\")]\n);\n```\n\n## Durable Objects\n```capnp\nconst worker :Workerd.Worker = (\n  modules = [(name = \"index.js\", esModule = embed \"index.js\"), (name = \"room.js\", esModule = embed \"room.js\")],\n  compatibilityDate = \"2024-01-15\",\n  bindings = [(name = \"ROOMS\", durableObjectNamespace = \"Room\")],\n  durableObjectNamespaces = [(className = \"Room\", uniqueKey = \"v1\")],\n  durableObjectStorage = (localDisk = \"/var/do\")\n);\n```\n\n## Dev vs Prod Configs\n```capnp\n# Use parameter bindings for env-specific config\nconst baseWorker :Workerd.Worker = (\n  modules = [(name = \"index.js\", esModule = embed \"src/index.js\")],\n  compatibilityDate = \"2024-01-15\",\n  bindings = [(name = \"API_URL\", parameter = (type = text))]\n);\n\nconst prodWorker :Workerd.Worker = (\n  inherit = \"base-service\",\n  bindings = [(name = \"API_URL\", text = \"https://api.prod.com\")]\n);\n```\n\n## HTTP Reverse Proxy\n```capnp\nservices = [\n  (name = \"proxy\", worker = (serviceWorkerScript = embed \"proxy.js\", compatibilityDate = \"2024-01-15\", bindings = [(name = \"BACKEND\", service = \"backend\")])),\n  (name = \"backend\", external = (address = \"internal:8080\", http = ()))\n]\n```\n\n## Local Development\n\n**Recommended:** Use Wrangler\n```bash\nwrangler dev  # Uses workerd internally\n```\n\n**Direct workerd:**\n```bash\nworkerd serve config.capnp --socket-addr http=*:3000 --verbose\n```\n\n**Environment variables:**\n```capnp\nbindings = [(name = \"DATABASE_URL\", fromEnvironment = \"DATABASE_URL\")]\n```\n\n## Testing\n```bash\nworkerd test config.capnp\nworkerd test config.capnp --test-only=test.js\n```\n\nTest files must be included in `modules = [...]` config.\n\n## Production Deployment\n\n### Compiled Binary (Recommended)\n```bash\nworkerd compile config.capnp myConfig -o production-server\n./production-server\n```\n\n### Docker\n```dockerfile\nFROM debian:bookworm-slim\nRUN apt-get update && apt-get install -y ca-certificates\nCOPY workerd /usr/local/bin/\nCOPY config.capnp /etc/workerd/\nCOPY src/ /etc/workerd/src/\nEXPOSE 8080\nCMD [\"workerd\", \"serve\", \"/etc/workerd/config.capnp\"]\n```\n\n### Systemd\n```ini\n# /etc/systemd/system/workerd.service\n[Service]\nExecStart=/usr/bin/workerd serve /etc/workerd/config.capnp --socket-fd http=3\nRestart=always\nUser=nobody\n```\n\nSee systemd socket activation docs for complete setup.\n\n## Framework Integration\n\n### Hono\n```javascript\nimport { Hono } from 'hono';\n\nconst app = new Hono();\n\napp.get('/', (c) => c.text('Hello Hono!'));\napp.get('/api/:id', async (c) => {\n  const id = c.req.param('id');\n  const data = await c.env.KV.get(id);\n  return c.json({ id, data });\n});\n\nexport default app;\n```\n\n### itty-router\n```javascript\nimport { Router } from 'itty-router';\n\nconst router = Router();\n\nrouter.get('/', () => new Response('Hello itty!'));\nrouter.get('/api/:id', async (request, env) => {\n  const { id } = request.params;\n  const data = await env.KV.get(id);\n  return Response.json({ id, data });\n});\n\nexport default {\n  fetch: (request, env, ctx) => router.handle(request, env, ctx)\n};\n```\n\n## Best Practices\n\n1. **Use ES modules** over service worker syntax\n2. **Explicit bindings** - no global namespace assumptions\n3. **Type safety** - define `Env` interfaces (use `wrangler types`)\n4. **Service isolation** - split concerns into multiple services\n5. **Pin compat date** in production after testing\n6. **Use ctx.waitUntil()** for background tasks\n7. **Handle errors gracefully** with try/catch\n8. **Configure resource limits** on caches/storage\n\n## Common Patterns\n\n### Error Handling\n```javascript\nexport default {\n  async fetch(request, env, ctx) {\n    try {\n      return await handleRequest(request, env);\n    } catch (error) {\n      console.error(\"Request failed\", error);\n      return new Response(\"Internal Error\", {status: 500});\n    }\n  }\n};\n```\n\n### Background Tasks\n```javascript\nexport default {\n  async fetch(request, env, ctx) {\n    const response = new Response(\"OK\");\n    \n    // Fire-and-forget background work\n    ctx.waitUntil(\n      env.ANALYTICS.put(request.url, Date.now())\n    );\n    \n    return response;\n  }\n};\n```\n\nSee [configuration.md](./configuration.md) for config syntax, [api.md](./api.md) for runtime APIs, [gotchas.md](./gotchas.md) for common errors.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers/README.md",
    "content": "# Cloudflare Workers\n\nExpert guidance for building, deploying, and optimizing Cloudflare Workers applications.\n\n## Overview\n\nCloudflare Workers run on V8 isolates (NOT containers/VMs):\n- Extremely fast cold starts (< 1ms)\n- Global deployment across 300+ locations\n- Web standards compliant (fetch, URL, Headers, Request, Response)\n- Support JS/TS, Python, Rust, and WebAssembly\n\n**Key principle**: Workers use web platform APIs wherever possible for portability.\n\n## Module Worker Pattern (Recommended)\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n    return new Response('Hello World!');\n  },\n};\n```\n\n**Handler parameters**:\n- `request`: Incoming HTTP request (standard Request object)\n- `env`: Environment bindings (KV, D1, R2, secrets, vars)\n- `ctx`: Execution context (`waitUntil`, `passThroughOnException`)\n\n## Essential Commands\n\n```bash\nnpx wrangler dev                    # Local dev\nnpx wrangler dev --remote           # Remote dev (actual resources)\nnpx wrangler deploy                 # Production\nnpx wrangler deploy --env staging   # Specific environment\nnpx wrangler tail                   # Stream logs\nnpx wrangler secret put API_KEY     # Set secret\n```\n\n## When to Use Workers\n\n- API endpoints at the edge\n- Request/response transformation\n- Authentication/authorization layers\n- Static asset optimization\n- A/B testing and feature flags\n- Rate limiting and security\n- Proxy/routing logic\n- WebSocket applications\n\n## Quick Start\n\n```bash\nnpm create cloudflare@latest my-worker -- --type hello-world\ncd my-worker\nnpx wrangler dev\n```\n\n## Handler Signatures\n\n```typescript\n// HTTP requests\nasync fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response>\n\n// Cron triggers\nasync scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void>\n\n// Queue consumer\nasync queue(batch: MessageBatch, env: Env, ctx: ExecutionContext): Promise<void>\n\n// Tail consumer\nasync tail(events: TraceItem[], env: Env, ctx: ExecutionContext): Promise<void>\n```\n\n## Resources\n\n**Docs**: https://developers.cloudflare.com/workers/  \n**Examples**: https://developers.cloudflare.com/workers/examples/  \n**Runtime APIs**: https://developers.cloudflare.com/workers/runtime-apis/\n\n## In This Reference\n\n- [Configuration](./configuration.md) - wrangler.jsonc setup, bindings, environments\n- [API](./api.md) - Runtime APIs, bindings, execution context\n- [Patterns](./patterns.md) - Common workflows, testing, optimization\n- [Frameworks](./frameworks.md) - Hono, routing, validation\n- [Gotchas](./gotchas.md) - Common issues, limits, troubleshooting\n\n## Reading Order\n\n| Task | Start With | Then Read |\n|------|------------|-----------|\n| First Worker | README → Configuration → API | Patterns |\n| Add framework | Frameworks | Configuration (bindings) |\n| Add storage/bindings | Configuration → API (binding usage) | See Also links |\n| Debug issues | Gotchas | API (specific binding docs) |\n| Production optimization | Patterns | API (caching, streaming) |\n| Type safety | Configuration (TypeScript) | Frameworks (Hono typing) |\n\n## See Also\n\n- [KV](../kv/README.md) - Key-value storage\n- [D1](../d1/README.md) - SQL database\n- [R2](../r2/README.md) - Object storage\n- [Durable Objects](../durable-objects/README.md) - Stateful coordination\n- [Queues](../queues/README.md) - Message queues\n- [Wrangler](../wrangler/README.md) - CLI tool reference\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers/api.md",
    "content": "# Workers Runtime APIs\n\n## Fetch Handler\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n    const url = new URL(request.url);\n    if (request.method === 'POST' && url.pathname === '/api') {\n      const body = await request.json();\n      return new Response(JSON.stringify({ id: 1 }), {\n        headers: { 'Content-Type': 'application/json' }\n      });\n    }\n    return fetch(request);  // Subrequest to origin\n  },\n};\n```\n\n## Execution Context\n\n```typescript\nctx.waitUntil(logAnalytics(request));  // Background work, don't block response\nctx.passThroughOnException();  // Failover to origin on error\n```\n\n**Never** `await` background operations - use `ctx.waitUntil()`.\n\n## Bindings\n\n```typescript\n// KV\nawait env.MY_KV.get('key');\nawait env.MY_KV.put('key', 'value', { expirationTtl: 3600 });\n\n// R2\nconst obj = await env.MY_BUCKET.get('file.txt');\nawait env.MY_BUCKET.put('file.txt', 'content');\n\n// D1\nconst result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(1).first();\n\n// D1 Sessions (2024+) - read-after-write consistency\nconst session = env.DB.withSession();\nawait session.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice').run();\nconst user = await session.prepare('SELECT * FROM users WHERE name = ?').bind('Alice').first(); // Guaranteed fresh\n\n// Queues\nawait env.MY_QUEUE.send({ timestamp: Date.now() });\n\n// Secrets/vars\nconst key = env.API_KEY;\n```\n\n## Cache API\n\n```typescript\nconst cache = caches.default;\nlet response = await cache.match(request);\n\nif (!response) {\n  response = await fetch(request);\n  response = new Response(response.body, response);\n  response.headers.set('Cache-Control', 'max-age=3600');\n  ctx.waitUntil(cache.put(request, response.clone()));  // Clone before caching\n}\n```\n\n## HTMLRewriter\n\n```typescript\nreturn new HTMLRewriter()\n  .on('a[href]', {\n    element(el) {\n      const href = el.getAttribute('href');\n      if (href?.startsWith('http://')) {\n        el.setAttribute('href', href.replace('http://', 'https://'));\n      }\n    }\n  })\n  .transform(response);\n```\n\n**Use cases**: A/B testing, analytics injection, link rewriting\n\n## WebSockets\n\n### Standard WebSocket\n\n```typescript\nconst [client, server] = Object.values(new WebSocketPair());\n\nserver.accept();\nserver.addEventListener('message', event => {\n  server.send(`Echo: ${event.data}`);\n});\n\nreturn new Response(null, { status: 101, webSocket: client });\n```\n\n### WebSocket Hibernation (Recommended for idle connections)\n\n```typescript\n// In Durable Object\nexport class WebSocketDO {\n  async webSocketMessage(ws: WebSocket, message: string) {\n    ws.send(`Echo: ${message}`);\n  }\n  \n  async webSocketClose(ws: WebSocket, code: number, reason: string) {\n    // Cleanup on close\n  }\n  \n  async webSocketError(ws: WebSocket, error: Error) {\n    console.error('WebSocket error:', error);\n  }\n}\n```\n\nHibernation automatically suspends inactive connections (no CPU cost), wakes on events\n\n## Durable Objects\n\n### RPC Pattern (Recommended 2024+)\n\n```typescript\nexport class Counter {\n  private value = 0;\n  \n  constructor(private state: DurableObjectState) {\n    state.blockConcurrencyWhile(async () => {\n      this.value = (await state.storage.get('value')) || 0;\n    });\n  }\n  \n  // Export methods directly - called via RPC (type-safe, zero serialization)\n  async increment(): Promise<number> {\n    this.value++;\n    await this.state.storage.put('value', this.value);\n    return this.value;\n  }\n  \n  async getValue(): Promise<number> {\n    return this.value;\n  }\n}\n\n// Worker usage:\nconst stub = env.COUNTER.get(env.COUNTER.idFromName('global'));\nconst count = await stub.increment(); // Direct method call, full type safety\n```\n\n### Legacy Fetch Pattern (Pre-2024)\n\n```typescript\nasync fetch(request: Request): Promise<Response> {\n  const url = new URL(request.url);\n  if (url.pathname === '/increment') {\n    await this.state.storage.put('value', ++this.value);\n  }\n  return new Response(String(this.value));\n}\n// Usage: await stub.fetch('http://x/increment')\n```\n\n**When to use DOs**: Real-time collaboration, rate limiting, strongly consistent state\n\n## Other Handlers\n\n```typescript\n// Cron: async scheduled(event, env, ctx) { ctx.waitUntil(doCleanup(env)); }\n// Queue: async queue(batch) { for (const msg of batch.messages) { await process(msg.body); msg.ack(); } }\n// Tail: async tail(events, env) { for (const e of events) if (e.outcome === 'exception') await log(e); }\n```\n\n## Service Bindings\n\n```typescript\n// Worker-to-worker RPC (zero latency, no internet round-trip)\nreturn env.SERVICE_B.fetch(request);\n\n// With RPC (2024+) - same as Durable Objects RPC\nexport class ServiceWorker {\n  async getData() { return { data: 'value' }; }\n}\n// Usage: const data = await env.SERVICE_B.getData();\n```\n\n**Benefits**: Type-safe method calls, no HTTP overhead, share code between Workers\n\n## See Also\n\n- [Configuration](./configuration.md) - Binding setup\n- [Patterns](./patterns.md) - Common workflows\n- [KV](../kv/README.md), [D1](../d1/README.md), [R2](../r2/README.md), [Durable Objects](../durable-objects/README.md), [Queues](../queues/README.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers/configuration.md",
    "content": "# Workers Configuration\n\n## wrangler.jsonc (Recommended)\n\n```jsonc\n{\n  \"$schema\": \"./node_modules/wrangler/config-schema.json\",\n  \"name\": \"my-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\", // Use current date for new projects\n  \n  // Bindings (non-inheritable)\n  \"vars\": { \"ENVIRONMENT\": \"production\" },\n  \"kv_namespaces\": [{ \"binding\": \"MY_KV\", \"id\": \"abc123\" }],\n  \"r2_buckets\": [{ \"binding\": \"MY_BUCKET\", \"bucket_name\": \"my-bucket\" }],\n  \"d1_databases\": [{ \"binding\": \"DB\", \"database_name\": \"my-db\", \"database_id\": \"xyz789\" }],\n  \n  // Environments\n  \"env\": {\n    \"staging\": {\n      \"vars\": { \"ENVIRONMENT\": \"staging\" },\n      \"kv_namespaces\": [{ \"binding\": \"MY_KV\", \"id\": \"staging-id\" }]\n    }\n  }\n}\n```\n\n## Configuration Rules\n\n**Inheritable**: `name`, `main`, `compatibility_date`, `routes`, `workers_dev`  \n**Non-inheritable**: All bindings (`vars`, `kv_namespaces`, `r2_buckets`, etc.)  \n**Top-level only**: `migrations`, `keep_vars`, `send_metrics`\n\n**ALWAYS set `compatibility_date` to current date for new projects**\n\n## Bindings\n\n```jsonc\n{\n  // Environment variables - access via env.VAR_NAME\n  \"vars\": { \"ENVIRONMENT\": \"production\" },\n  \n  // KV (key-value storage)\n  \"kv_namespaces\": [{ \"binding\": \"MY_KV\", \"id\": \"abc123\" }],\n  \n  // R2 (object storage)\n  \"r2_buckets\": [{ \"binding\": \"MY_BUCKET\", \"bucket_name\": \"my-bucket\" }],\n  \n  // D1 (SQL database)\n  \"d1_databases\": [{ \"binding\": \"DB\", \"database_name\": \"my-db\", \"database_id\": \"xyz789\" }],\n  \n  // Durable Objects (stateful coordination)\n  \"durable_objects\": {\n    \"bindings\": [{ \"name\": \"COUNTER\", \"class_name\": \"Counter\" }]\n  },\n  \n  // Queues (message queues)\n  \"queues\": {\n    \"producers\": [{ \"binding\": \"MY_QUEUE\", \"queue\": \"my-queue\" }],\n    \"consumers\": [{ \"queue\": \"my-queue\", \"max_batch_size\": 10 }]\n  },\n  \n  // Service bindings (worker-to-worker RPC)\n  \"services\": [{ \"binding\": \"SERVICE_B\", \"service\": \"service-b\" }],\n  \n  // Analytics Engine\n  \"analytics_engine_datasets\": [{ \"binding\": \"ANALYTICS\" }]\n}\n```\n\n### Secrets\n\nSet via CLI (never in config):\n\n```bash\nnpx wrangler secret put API_KEY\n```\n\nAccess: `env.API_KEY`\n\n### Automatic Provisioning (Beta)\n\nBindings without IDs are auto-created:\n\n```jsonc\n{ \"kv_namespaces\": [{ \"binding\": \"MY_KV\" }] }  // ID added on deploy\n```\n\n## Routes & Triggers\n\n```jsonc\n{\n  \"routes\": [\n    { \"pattern\": \"example.com/*\", \"zone_name\": \"example.com\" }\n  ],\n  \"triggers\": {\n    \"crons\": [\"0 */6 * * *\"]  // Every 6 hours\n  }\n}\n```\n\n## TypeScript Setup\n\n### Automatic Type Generation (Recommended)\n\n```bash\nnpm install -D @cloudflare/workers-types\nnpx wrangler types  # Generates .wrangler/types/runtime.d.ts from wrangler.jsonc\n```\n\n`tsconfig.json`:\n\n```jsonc\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2022\"],\n    \"types\": [\"@cloudflare/workers-types\"]\n  },\n  \"include\": [\".wrangler/types/**/*.ts\", \"src/**/*\"]\n}\n```\n\nImport generated types:\n\n```typescript\nimport type { Env } from './.wrangler/types/runtime';\n\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n    await env.MY_KV.get('key');  // Fully typed, autocomplete works\n    return new Response('OK');\n  },\n};\n```\n\nRe-run `npx wrangler types` after changing bindings in wrangler.jsonc\n\n### Manual Type Definition (Legacy)\n\n```typescript\ninterface Env {\n  MY_KV: KVNamespace;\n  DB: D1Database;\n  API_KEY: string;\n}\n```\n\n## Advanced Options\n\n```jsonc\n{\n  // Auto-locate compute near data sources\n  \"placement\": { \"mode\": \"smart\" },\n  \n  // Enable Node.js built-ins (Buffer, process, path, etc.)\n  \"compatibility_flags\": [\"nodejs_compat_v2\"],\n  \n  // Observability (10% sampling)\n  \"observability\": { \"enabled\": true, \"head_sampling_rate\": 0.1 }\n}\n```\n\n### Node.js Compatibility\n\n`nodejs_compat_v2` enables:\n- `Buffer`, `process.env`, `path`, `stream`\n- CommonJS `require()` for Node modules\n- `node:` imports (e.g., `import { Buffer } from 'node:buffer'`)\n\n**Note:** Adds ~1-2ms cold start overhead. Use Workers APIs (R2, KV) when possible\n\n## Deployment Commands\n\n```bash\nnpx wrangler deploy              # Production\nnpx wrangler deploy --env staging\nnpx wrangler deploy --dry-run    # Validate only\n```\n\n## See Also\n\n- [API](./api.md) - Runtime APIs and bindings usage\n- [Patterns](./patterns.md) - Deployment strategies\n- [Wrangler](../wrangler/README.md) - CLI reference\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers/frameworks.md",
    "content": "# Workers Frameworks\n\n## Hono (Recommended)\n\nWorkers-native web framework with excellent TypeScript support and middleware ecosystem.\n\n```bash\nnpm install hono\n```\n\n### Basic Setup\n\n```typescript\nimport { Hono } from 'hono';\n\nconst app = new Hono();\n\napp.get('/', (c) => c.text('Hello World!'));\napp.post('/api/users', async (c) => {\n  const body = await c.req.json();\n  return c.json({ id: 1, ...body }, 201);\n});\n\nexport default app;\n```\n\n### Typed Environment\n\n```typescript\nimport type { Env } from './.wrangler/types/runtime';\n\nconst app = new Hono<{ Bindings: Env }>();\n\napp.get('/data', async (c) => {\n  const value = await c.env.MY_KV.get('key');  // Fully typed\n  return c.text(value || 'Not found');\n});\n```\n\n### Middleware\n\n```typescript\nimport { cors } from 'hono/cors';\nimport { logger } from 'hono/logger';\n\napp.use('*', logger());\napp.use('/api/*', cors({ origin: '*' }));\n\n// Custom middleware\napp.use('/protected/*', async (c, next) => {\n  const auth = c.req.header('Authorization');\n  if (!auth?.startsWith('Bearer ')) return c.text('Unauthorized', 401);\n  await next();\n});\n```\n\n### Request Validation (Zod)\n\n```typescript\nimport { zValidator } from '@hono/zod-validator';\nimport { z } from 'zod';\n\nconst schema = z.object({\n  name: z.string().min(1),\n  email: z.string().email(),\n});\n\napp.post('/users', zValidator('json', schema), async (c) => {\n  const validated = c.req.valid('json');  // Type-safe, validated data\n  return c.json({ id: 1, ...validated });\n});\n```\n\n**Error handling**: Automatic 400 response with validation errors\n\n### Route Groups\n\n```typescript\nconst api = new Hono().basePath('/api');\n\napi.get('/users', (c) => c.json([]));\napi.post('/users', (c) => c.json({ id: 1 }));\n\napp.route('/', api);  // Mounts at /api/*\n```\n\n### Error Handling\n\n```typescript\napp.onError((err, c) => {\n  console.error(err);\n  return c.json({ error: err.message }, 500);\n});\n\napp.notFound((c) => c.json({ error: 'Not Found' }, 404));\n```\n\n### Accessing ExecutionContext\n\n```typescript\nexport default {\n  fetch(request: Request, env: Env, ctx: ExecutionContext) {\n    return app.fetch(request, env, ctx);\n  },\n};\n\n// In route handlers:\napp.get('/log', (c) => {\n  c.executionCtx.waitUntil(logRequest(c.req));\n  return c.text('OK');\n});\n```\n\n### OpenAPI/Swagger (Hono OpenAPI)\n\n```typescript\nimport { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';\n\nconst app = new OpenAPIHono();\n\nconst route = createRoute({\n  method: 'get',\n  path: '/users/{id}',\n  request: { params: z.object({ id: z.string() }) },\n  responses: {\n    200: { description: 'User found', content: { 'application/json': { schema: z.object({ id: z.string() }) } } },\n  },\n});\n\napp.openapi(route, (c) => {\n  const { id } = c.req.valid('param');\n  return c.json({ id });\n});\n\napp.doc('/openapi.json', { openapi: '3.0.0', info: { version: '1.0.0', title: 'API' } });\n```\n\n### Testing with Hono\n\n```typescript\nimport { describe, it, expect } from 'vitest';\nimport app from '../src/index';\n\ndescribe('API', () => {\n  it('GET /', async () => {\n    const res = await app.request('/');\n    expect(res.status).toBe(200);\n    expect(await res.text()).toBe('Hello World!');\n  });\n});\n```\n\n## Other Frameworks\n\n### itty-router (Minimalist)\n\n```typescript\nimport { Router } from 'itty-router';\n\nconst router = Router();\n\nrouter.get('/users/:id', ({ params }) => new Response(params.id));\n\nexport default { fetch: router.handle };\n```\n\n**Use case**: Tiny bundle size (~500 bytes), simple routing needs\n\n### Worktop (Advanced)\n\n```typescript\nimport { Router } from 'worktop';\n\nconst router = new Router();\n\nrouter.add('GET', '/users/:id', (req, res) => {\n  res.send(200, { id: req.params.id });\n});\n\nrouter.listen();\n```\n\n**Use case**: Advanced routing, built-in CORS/cache utilities\n\n## Framework Comparison\n\n| Framework | Bundle Size | TypeScript | Middleware | Validation | Best For |\n|-----------|-------------|------------|------------|------------|----------|\n| Hono | ~12KB | Excellent | Rich | Zod | Production apps |\n| itty-router | ~500B | Good | Basic | Manual | Minimal APIs |\n| Worktop | ~8KB | Good | Advanced | Manual | Complex routing |\n\n## See Also\n\n- [Patterns](./patterns.md) - Common workflows\n- [API](./api.md) - Runtime APIs\n- [Gotchas](./gotchas.md) - Framework-specific issues\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers/gotchas.md",
    "content": "# Workers Gotchas\n\n## Common Errors\n\n### \"Too much CPU time used\"\n\n**Cause:** Worker exceeded CPU time limit (10ms standard, 30ms unbound)  \n**Solution:** Use `ctx.waitUntil()` for background work, offload heavy compute to Durable Objects, or consider Workers AI for ML workloads\n\n### \"Module-Level State Lost\"\n\n**Cause:** Workers are stateless between requests; module-level variables reset unpredictably  \n**Solution:** Use KV, D1, or Durable Objects for persistent state; don't rely on module-level variables\n\n### \"Body has already been used\"\n\n**Cause:** Attempting to read response body twice (bodies are streams)  \n**Solution:** Clone response before reading: `response.clone()` or read once and create new Response with the text\n\n### \"Node.js module not found\"\n\n**Cause:** Node.js built-ins not available by default  \n**Solution:** Use Workers APIs (e.g., R2 for file storage) or enable Node.js compat with `\"compatibility_flags\": [\"nodejs_compat_v2\"]`\n\n### \"Cannot fetch in global scope\"\n\n**Cause:** Attempting to use fetch during module initialization  \n**Solution:** Move fetch calls inside handler functions (fetch, scheduled, etc.) where they're allowed\n\n### \"Subrequest depth limit exceeded\"\n\n**Cause:** Too many nested subrequests creating deep call chain  \n**Solution:** Flatten request chain or use service bindings for direct Worker-to-Worker communication\n\n### \"D1 read-after-write inconsistency\"\n\n**Cause:** D1 is eventually consistent; reads may not reflect recent writes  \n**Solution:** Use D1 Sessions (2024+) to guarantee read-after-write consistency within a session:\n\n```typescript\nconst session = env.DB.withSession();\nawait session.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice').run();\nconst user = await session.prepare('SELECT * FROM users WHERE name = ?').bind('Alice').first(); // Guaranteed to see Alice\n```\n\n**When to use sessions:** Write → Read patterns, transactions requiring consistency\n\n### \"wrangler types not generating TypeScript definitions\"\n\n**Cause:** Type generation not configured or outdated  \n**Solution:** Run `npx wrangler types` after changing bindings in wrangler.jsonc:\n\n```bash\nnpx wrangler types  # Generates .wrangler/types/runtime.d.ts\n```\n\nAdd to `tsconfig.json`: `\"include\": [\".wrangler/types/**/*.ts\"]`\n\nThen import: `import type { Env } from './.wrangler/types/runtime';`\n\n### \"Durable Object RPC errors with deprecated fetch pattern\"\n\n**Cause:** Using old `stub.fetch()` pattern instead of RPC (2024+)  \n**Solution:** Export methods directly, call via RPC:\n\n```typescript\n// ❌ Old fetch pattern\nexport class MyDO {\n  async fetch(request: Request) {\n    const { method } = await request.json();\n    if (method === 'increment') return new Response(String(await this.increment()));\n  }\n  async increment() { return ++this.value; }\n}\nconst stub = env.DO.get(id);\nconst res = await stub.fetch('http://x', { method: 'POST', body: JSON.stringify({ method: 'increment' }) });\n\n// ✅ RPC pattern (type-safe, no serialization overhead)\nexport class MyDO {\n  async increment() { return ++this.value; }\n}\nconst stub = env.DO.get(id);\nconst count = await stub.increment(); // Direct method call\n```\n\n### \"WebSocket connection closes unexpectedly\"\n\n**Cause:** Worker reaches CPU limit while maintaining WebSocket connection  \n**Solution:** Use WebSocket hibernation (2024+) to offload idle connections:\n\n```typescript\nexport class WebSocketDO {\n  async webSocketMessage(ws: WebSocket, message: string) {\n    // Handle message\n  }\n  async webSocketClose(ws: WebSocket, code: number) {\n    // Cleanup\n  }\n}\n```\n\nHibernation automatically suspends inactive connections, wakes on events\n\n### \"Framework middleware not working with Workers\"\n\n**Cause:** Framework expects Node.js primitives (e.g., Express uses Node streams)  \n**Solution:** Use Workers-native frameworks (Hono, itty-router, Worktop) or adapt middleware:\n\n```typescript\n// ✅ Hono (Workers-native)\nimport { Hono } from 'hono';\nconst app = new Hono();\napp.use('*', async (c, next) => { /* middleware */ await next(); });\n```\n\nSee [frameworks.md](./frameworks.md) for full patterns\n\n## Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Request size | 100 MB | Maximum incoming request size |\n| Response size | Unlimited | Supports streaming |\n| CPU time (standard) | 10ms | Standard Workers |\n| CPU time (unbound) | 30ms | Unbound Workers |\n| Subrequests | 1000 | Per request |\n| KV reads | 1000 | Per request |\n| KV write size | 25 MB | Maximum per write |\n| Environment size | 5 MB | Total size of env bindings |\n\n## See Also\n\n- [Patterns](./patterns.md) - Best practices\n- [API](./api.md) - Runtime APIs\n- [Configuration](./configuration.md) - Setup\n- [Frameworks](./frameworks.md) - Hono, routing, validation\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers/patterns.md",
    "content": "# Workers Patterns\n\n## Error Handling\n\n```typescript\nclass HTTPError extends Error {\n  constructor(public status: number, message: string) { super(message); }\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    try {\n      return await handleRequest(request, env);\n    } catch (error) {\n      if (error instanceof HTTPError) {\n        return new Response(JSON.stringify({ error: error.message }), {\n          status: error.status, headers: { 'Content-Type': 'application/json' }\n        });\n      }\n      return new Response('Internal Server Error', { status: 500 });\n    }\n  },\n};\n```\n\n## CORS\n\n```typescript\nconst corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS' };\nif (request.method === 'OPTIONS') return new Response(null, { headers: corsHeaders });\n```\n\n## Routing\n\n```typescript\nconst router = { 'GET /api/users': handleGetUsers, 'POST /api/users': handleCreateUser };\n\nconst handler = router[`${request.method} ${url.pathname}`];\nreturn handler ? handler(request, env) : new Response('Not Found', { status: 404 });\n```\n\n**Production**: Use Hono, itty-router, or Worktop (see [frameworks.md](./frameworks.md))\n\n## Request Validation (Zod)\n\n```typescript\nimport { z } from 'zod';\n\nconst userSchema = z.object({\n  name: z.string().min(1).max(100),\n  email: z.string().email(),\n  age: z.number().int().positive().optional(),\n});\n\nasync function handleCreateUser(request: Request) {\n  try {\n    const body = await request.json();\n    const validated = userSchema.parse(body);  // Throws on invalid data\n    return new Response(JSON.stringify({ id: 1, ...validated }), {\n      status: 201,\n      headers: { 'Content-Type': 'application/json' },\n    });\n  } catch (err) {\n    if (err instanceof z.ZodError) {\n      return new Response(JSON.stringify({ errors: err.errors }), { status: 400 });\n    }\n    throw err;\n  }\n}\n```\n\n**With Hono**: Use `@hono/zod-validator` for automatic validation (see [frameworks.md](./frameworks.md))\n\n## Performance\n\n```typescript\n// ❌ Sequential\nconst user = await fetch('/api/user/1');\nconst posts = await fetch('/api/posts?user=1');\n\n// ✅ Parallel\nconst [user, posts] = await Promise.all([fetch('/api/user/1'), fetch('/api/posts?user=1')]);\n```\n\n## Streaming\n\n```typescript\nconst stream = new ReadableStream({\n  async start(controller) {\n    for (let i = 0; i < 1000; i++) {\n      controller.enqueue(new TextEncoder().encode(`Item ${i}\\n`));\n      if (i % 100 === 0) await new Promise(r => setTimeout(r, 0));\n    }\n    controller.close();\n  }\n});\n```\n\n## Transform Streams\n\n```typescript\nresponse.body.pipeThrough(new TextDecoderStream()).pipeThrough(\n  new TransformStream({ transform(chunk, c) { c.enqueue(chunk.toUpperCase()); } })\n).pipeThrough(new TextEncoderStream());\n```\n\n## Testing\n\n```typescript\nimport { describe, it, expect } from 'vitest';\nimport worker from '../src/index';\n\ndescribe('Worker', () => {\n  it('returns 200', async () => {\n    const req = new Request('http://localhost/');\n    const env = { MY_VAR: 'test' };\n    const ctx = { waitUntil: () => {}, passThroughOnException: () => {} };\n    expect((await worker.fetch(req, env, ctx)).status).toBe(200);\n  });\n});\n```\n\n## Deployment\n\n```bash\nnpx wrangler deploy              # production\nnpx wrangler deploy --env staging\nnpx wrangler versions upload --message \"Add feature\"\nnpx wrangler rollback\n```\n\n## Monitoring\n\n```typescript\nconst start = Date.now();\nconst response = await handleRequest(request, env);\nctx.waitUntil(env.ANALYTICS.writeDataPoint({\n  doubles: [Date.now() - start], blobs: [request.url, String(response.status)]\n}));\n```\n\n## Security & Rate Limiting\n\n```typescript\n// Security headers\nconst security = { 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY' };\n\n// Auth\nconst auth = request.headers.get('Authorization');\nif (!auth?.startsWith('Bearer ')) return new Response('Unauthorized', { status: 401 });\n\n// Gradual rollouts (deterministic user bucketing)\nconst hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(userId));\nif (new Uint8Array(hash)[0] % 100 < rolloutPercent) return newFeature(request);\n```\n\nRate limiting: See [Durable Objects](../durable-objects/README.md)\n\n## R2 Multipart Upload\n\n```typescript\n// For files > 100MB\nconst upload = await env.MY_BUCKET.createMultipartUpload('large-file.bin');\ntry {\n  const parts = [];\n  for (let i = 0; i < chunks.length; i++) {\n    parts.push(await upload.uploadPart(i + 1, chunks[i]));\n  }\n  await upload.complete(parts);\n} catch (err) { await upload.abort(); throw err; }\n```\n\nParallel uploads, resume on failure, handle files > 5GB\n\n## Workflows (Step Orchestration)\n\n```typescript\nimport { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';\n\nexport class MyWorkflow extends WorkflowEntrypoint {\n  async run(event: WorkflowEvent<{ userId: string }>, step: WorkflowStep) {\n    const user = await step.do('fetch-user', async () => \n      fetch(`/api/users/${event.payload.userId}`).then(r => r.json())\n    );\n    await step.sleep('wait', '1 hour');\n    await step.do('notify', async () => sendEmail(user.email));\n  }\n}\n```\n\nMulti-step jobs with automatic retries, state persistence, resume from failure\n\n## See Also\n\n- [API](./api.md) - Runtime APIs\n- [Gotchas](./gotchas.md) - Common issues\n- [Configuration](./configuration.md) - Setup\n- [Frameworks](./frameworks.md) - Hono, routing, validation\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-ai/README.md",
    "content": "# Cloudflare Workers AI\n\nExpert guidance for Cloudflare Workers AI - serverless GPU-powered AI inference at the edge.\n\n## Overview\n\nWorkers AI provides:\n- 50+ pre-trained models (LLMs, embeddings, image generation, speech-to-text, translation)\n- Native Workers binding (no external API calls)\n- Pay-per-use pricing (neurons consumed per inference)\n- OpenAI-compatible REST API\n- Streaming support for text generation\n- Function calling with compatible models\n\n**Architecture**: Inference runs on Cloudflare's GPU network. Models load on first request (cold start 1-3s), subsequent requests are faster.\n\n## Quick Start\n\n```typescript\ninterface Env {\n  AI: Ai;\n}\n\nexport default {\n  async fetch(request: Request, env: Env) {\n    const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {\n      messages: [{ role: 'user', content: 'What is Cloudflare?' }]\n    });\n    return Response.json(response);\n  }\n};\n```\n\n```bash\n# Setup - add binding to wrangler.jsonc\nwrangler dev --remote  # Must use --remote for AI\nwrangler deploy\n```\n\n## Model Selection Decision Tree\n\n### Text Generation (Chat/Completion)\n\n**Quality Priority**:\n- **Best quality**: `@cf/meta/llama-3.1-70b-instruct` (expensive, ~2000 neurons)\n- **Balanced**: `@cf/meta/llama-3.1-8b-instruct` (good quality, ~200 neurons)\n- **Fastest/cheapest**: `@cf/mistral/mistral-7b-instruct-v0.1` (~50 neurons)\n\n**Function Calling**:\n- Use `@cf/meta/llama-3.1-8b-instruct` or `@cf/meta/llama-3.1-70b-instruct` (native tool support)\n\n**Code Generation**:\n- Use `@cf/deepseek-ai/deepseek-coder-6.7b-instruct` (specialized for code)\n\n### Embeddings (Semantic Search/RAG)\n\n**English text**:\n- **Best**: `@cf/baai/bge-large-en-v1.5` (1024 dims, highest quality)\n- **Balanced**: `@cf/baai/bge-base-en-v1.5` (768 dims, good quality)\n- **Fast**: `@cf/baai/bge-small-en-v1.5` (384 dims, lower quality but fast)\n\n**Multilingual**:\n- Use `@hf/sentence-transformers/paraphrase-multilingual-minilm-l12-v2`\n\n### Image Generation\n\n- **Stable Diffusion**: `@cf/stabilityai/stable-diffusion-xl-base-1.0` (~10,000 neurons)\n- **Portraits**: `@cf/lykon/dreamshaper-8-lcm` (optimized for faces)\n\n### Other Tasks\n\n- **Speech-to-text**: `@cf/openai/whisper`\n- **Translation**: `@cf/meta/m2m100-1.2b` (100 languages)\n- **Image classification**: `@cf/microsoft/resnet-50`\n\n## SDK Approach Decision Tree\n\n### Native Binding (Recommended)\n\n**When**: Building Workers/Pages with TypeScript  \n**Why**: Zero external dependencies, best performance, native types\n\n```typescript\nawait env.AI.run(model, input);\n```\n\n### REST API\n\n**When**: External services, non-Workers environments, testing  \n**Why**: Standard HTTP, works anywhere\n\n```bash\ncurl https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/run/@cf/meta/llama-3.1-8b-instruct \\\n  -H \"Authorization: Bearer <API_TOKEN>\" \\\n  -d '{\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}'\n```\n\n### Vercel AI SDK Integration\n\n**When**: Using Vercel AI SDK features (streaming UI, tool calling abstractions)  \n**Why**: Unified interface across providers\n\n```typescript\nimport { openai } from '@ai-sdk/openai';\n\nconst model = openai('model-name', {\n  baseURL: 'https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/v1',\n  headers: { Authorization: 'Bearer <API_TOKEN>' }\n});\n```\n\n## RAG vs Direct Generation\n\n### Use RAG (Vectorize + Workers AI) When:\n- Answering questions about specific documents/data\n- Need factual accuracy from known corpus\n- Context exceeds model's window (>4K tokens)\n- Building knowledge base chat\n\n### Use Direct Generation When:\n- Creative writing, brainstorming\n- General knowledge questions\n- Small context fits in prompt (<4K tokens)\n- Cost optimization (RAG adds embedding + vector search costs)\n\n## Platform Limits\n\n| Limit | Free Tier | Paid Plans |\n|-------|-----------|------------|\n| Neurons/day | 10,000 | Pay per use |\n| Rate limit | Varies by model | Higher (contact support) |\n| Context window | Model dependent (2K-8K) | Same |\n| Streaming | ✅ Supported | ✅ Supported |\n| Function calling | ✅ Supported (select models) | ✅ Supported |\n\n**Pricing**: Free 10K neurons/day, then pay per neuron consumed (varies by model)\n\n## Common Tasks\n\n```typescript\n// Streaming text generation\nconst stream = await env.AI.run(model, { messages, stream: true });\nfor await (const chunk of stream) {\n  console.log(chunk.response);\n}\n\n// Embeddings for RAG\nconst { data } = await env.AI.run('@cf/baai/bge-base-en-v1.5', {\n  text: ['Query text', 'Document 1', 'Document 2']\n});\n\n// Function calling\nconst response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {\n  messages: [{ role: 'user', content: 'What is the weather?' }],\n  tools: [{\n    type: 'function',\n    function: { name: 'getWeather', parameters: { ... } }\n  }]\n});\n```\n\n## Development Workflow\n\n```bash\n# Always use --remote for AI (local doesn't have models)\nwrangler dev --remote\n\n# Deploy to production\nwrangler deploy\n\n# View model catalog\n# https://developers.cloudflare.com/workers-ai/models/\n```\n\n## Reading Order\n\n**Start here**: Quick Start above → configuration.md (setup)\n\n**Common tasks**:\n- First time setup: configuration.md → Add binding + deploy\n- Choose model: Model Selection Decision Tree (above) → api.md\n- Build RAG: patterns.md → Vectorize integration\n- Optimize costs: Model Selection + gotchas.md (rate limits)\n- Debugging: gotchas.md → Common errors\n\n## In This Reference\n\n- [configuration.md](./configuration.md) - wrangler.jsonc setup, TypeScript types, bindings, environment variables\n- [api.md](./api.md) - env.AI.run(), streaming, function calling, REST API, response types\n- [patterns.md](./patterns.md) - RAG with Vectorize, prompt engineering, batching, error handling, caching\n- [gotchas.md](./gotchas.md) - Deprecated @cloudflare/ai package, rate limits, pricing, common errors\n\n## See Also\n\n- [vectorize](../vectorize/) - Vector database for RAG patterns\n- [ai-gateway](../ai-gateway/) - Caching, rate limiting, analytics for AI requests\n- [workers](../workers/) - Worker runtime and fetch handler patterns\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-ai/api.md",
    "content": "# Workers AI API Reference\n\n## Core Method\n\n```typescript\nconst response = await env.AI.run(model, input);\n```\n\n## Text Generation\n\n```typescript\nconst result = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {\n  messages: [\n    { role: 'system', content: 'You are helpful' },\n    { role: 'user', content: 'Hello' }\n  ],\n  temperature: 0.7,  // 0-1\n  max_tokens: 100\n});\nconsole.log(result.response);\n```\n\n**Streaming:**\n```typescript\nconst stream = await env.AI.run(model, { messages, stream: true });\nreturn new Response(stream, { headers: { 'Content-Type': 'text/event-stream' } });\n```\n\n## Embeddings\n\n```typescript\nconst result = await env.AI.run('@cf/baai/bge-base-en-v1.5', {\n  text: ['Query', 'Doc 1', 'Doc 2'] // Batch for efficiency\n});\nconst [queryEmbed, doc1Embed, doc2Embed] = result.data; // 768-dim vectors\n```\n\n## Function Calling\n\n```typescript\nconst tools = [{\n  type: 'function',\n  function: {\n    name: 'getWeather',\n    description: 'Get weather for location',\n    parameters: {\n      type: 'object',\n      properties: { location: { type: 'string' } },\n      required: ['location']\n    }\n  }\n}];\n\nconst response = await env.AI.run(model, { messages, tools });\nif (response.tool_calls) {\n  const args = JSON.parse(response.tool_calls[0].function.arguments);\n  // Execute function, send result back\n}\n```\n\n## Image Generation\n\n```typescript\nconst image = await env.AI.run('@cf/stabilityai/stable-diffusion-xl-base-1.0', {\n  prompt: 'Mountain sunset',\n  num_steps: 20,   // 1-20\n  guidance: 7.5    // 1-20\n});\nreturn new Response(image, { headers: { 'Content-Type': 'image/png' } });\n```\n\n## Speech Recognition\n\n```typescript\nconst audioArray = Array.from(new Uint8Array(await request.arrayBuffer()));\nconst result = await env.AI.run('@cf/openai/whisper', { audio: audioArray });\nconsole.log(result.text);\n```\n\n## Translation\n\n```typescript\nconst result = await env.AI.run('@cf/meta/m2m100-1.2b', {\n  text: 'Hello',\n  source_lang: 'en',\n  target_lang: 'es'\n});\nconsole.log(result.translated_text);\n```\n\n## REST API\n\n```bash\ncurl https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/@cf/meta/llama-3.1-8b-instruct \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -d '{\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}'\n```\n\n## Error Codes\n\n| Code | Meaning | Fix |\n|------|---------|-----|\n| 7502 | Model not found | Check spelling |\n| 7504 | Validation failed | Verify input schema |\n| 7505 | Rate limited | Reduce rate or upgrade |\n| 7506 | Context exceeded | Reduce input size |\n\n## Performance Tips\n\n1. **Batch embeddings** - single request for multiple texts\n2. **Stream long responses** - reduce perceived latency\n3. **Accept cold starts** - first request ~1-3s, subsequent ~100-500ms\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-ai/configuration.md",
    "content": "# Workers AI Configuration\n\n## wrangler.jsonc\n\n```jsonc\n{\n  \"name\": \"my-ai-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2024-01-01\",\n  \"ai\": {\n    \"binding\": \"AI\"\n  }\n}\n```\n\n## TypeScript\n\n```bash\nnpm install --save-dev @cloudflare/workers-types\n```\n\n```typescript\ninterface Env {\n  AI: Ai;\n}\n\nexport default {\n  async fetch(request: Request, env: Env) {\n    const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {\n      messages: [{ role: 'user', content: 'Hello' }]\n    });\n    return Response.json(response);\n  }\n};\n```\n\n## Local Development\n\n```bash\nwrangler dev --remote  # Required for AI - no local inference\n```\n\n## REST API\n\n```typescript\nconst response = await fetch(\n  `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/ai/run/@cf/meta/llama-3.1-8b-instruct`,\n  {\n    method: 'POST',\n    headers: { 'Authorization': `Bearer ${API_TOKEN}` },\n    body: JSON.stringify({ messages: [{ role: 'user', content: 'Hello' }] })\n  }\n);\n```\n\nCreate API token at: dash.cloudflare.com/profile/api-tokens (Workers AI - Read permission)\n\n## SDK Compatibility\n\n**OpenAI SDK:**\n```typescript\nimport OpenAI from 'openai';\nconst client = new OpenAI({\n  apiKey: env.CLOUDFLARE_API_TOKEN,\n  baseURL: `https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/ai/v1`\n});\n```\n\n## Multi-Model Setup\n\n```typescript\nconst MODELS = {\n  chat: '@cf/meta/llama-3.1-8b-instruct',\n  embed: '@cf/baai/bge-base-en-v1.5',\n  image: '@cf/stabilityai/stable-diffusion-xl-base-1.0'\n};\n```\n\n## RAG Setup (with Vectorize)\n\n```jsonc\n{\n  \"ai\": { \"binding\": \"AI\" },\n  \"vectorize\": {\n    \"bindings\": [{ \"binding\": \"VECTORIZE\", \"index_name\": \"embeddings-index\" }]\n  }\n}\n```\n\n## Troubleshooting\n\n| Error | Fix |\n|-------|-----|\n| `env.AI is undefined` | Check `ai` binding in wrangler.jsonc |\n| Local AI doesn't work | Use `wrangler dev --remote` |\n| Type 'Ai' not found | Install `@cloudflare/workers-types` |\n| @cloudflare/ai package error | Don't install - use native binding |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-ai/gotchas.md",
    "content": "# Workers AI Gotchas\n\n## Critical: @cloudflare/ai is DEPRECATED\n\n```typescript\n// ❌ WRONG - Don't install @cloudflare/ai\nimport Ai from '@cloudflare/ai';\n\n// ✅ CORRECT - Use native binding\nexport default {\n  async fetch(request: Request, env: Env) {\n    await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: [...] });\n  }\n}\n```\n\n## Development\n\n### \"AI inference doesn't work locally\"\n```bash\n# ❌ Local AI doesn't work\nwrangler dev\n# ✅ Use remote\nwrangler dev --remote\n```\n\n### \"env.AI is undefined\"\nAdd binding to wrangler.jsonc:\n```jsonc\n{ \"ai\": { \"binding\": \"AI\" } }\n```\n\n## API Responses\n\n### Embedding response shape varies\n```typescript\n// @cf/baai/bge-base-en-v1.5 returns: { data: [[0.1, 0.2, ...]] }\nconst embedding = response.data[0]; // Get first element\n```\n\n### Stream returns ReadableStream\n```typescript\nconst stream = await env.AI.run(model, { messages: [...], stream: true });\nfor await (const chunk of stream) { console.log(chunk.response); }\n```\n\n## Rate Limits & Pricing\n\n| Model Type | Neurons/Request |\n|------------|-----------------|\n| Small text (7B) | ~50-200 |\n| Large text (70B) | ~500-2000 |\n| Embeddings | ~5-20 |\n| Image gen | ~10,000+ |\n\n**Free tier**: 10,000 neurons/day\n\n```typescript\n// ❌ EXPENSIVE - 70B model\nawait env.AI.run('@cf/meta/llama-3.1-70b-instruct', ...);\n// ✅ CHEAPER - Use smallest that works\nawait env.AI.run('@cf/meta/llama-3.1-8b-instruct', ...);\n```\n\n## Model-Specific\n\n### Function calling\nOnly `@cf/meta/llama-3.1-*` and `mistral-7b-instruct-v0.2` support tools.\n\n### Empty response\nCheck context limits (2K-8K tokens). Validate input structure.\n\n### Inconsistent responses\nSet `temperature: 0` for deterministic outputs.\n\n### Cold start latency\nFirst request: 1-3s. Use AI Gateway caching for frequent prompts.\n\n## TypeScript\n\n```typescript\ninterface Env {\n  AI: Ai; // From @cloudflare/workers-types\n}\n\ninterface TextGenerationResponse { response: string; }\ninterface EmbeddingResponse { data: number[][]; shape: number[]; }\n```\n\n## Common Errors\n\n### 7502: Model not found\nCheck exact model name at developers.cloudflare.com/workers-ai/models/\n\n### 7504: Input validation failed\n```typescript\n// Text gen requires messages array\nawait env.AI.run('@cf/meta/llama-3.1-8b-instruct', {\n  messages: [{ role: 'user', content: 'Hello' }]  // ✅\n});\n\n// Embeddings require text\nawait env.AI.run('@cf/baai/bge-base-en-v1.5', { text: 'Hello' });  // ✅\n```\n\n## Vercel AI SDK Integration\n\n```typescript\nimport { openai } from '@ai-sdk/openai';\nconst model = openai('gpt-3.5-turbo', {\n  baseURL: 'https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/v1',\n  headers: { Authorization: 'Bearer <API_TOKEN>' }\n});\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-ai/patterns.md",
    "content": "# Workers AI Patterns\n\n## RAG (Retrieval-Augmented Generation)\n\n```typescript\n// 1. Embed query\nconst embedding = await env.AI.run('@cf/baai/bge-base-en-v1.5', { text: query });\n\n// 2. Search vectors\nconst results = await env.VECTORIZE.query(embedding.data[0], {\n  topK: 5, returnMetadata: true\n});\n\n// 3. Build context\nconst context = results.matches.map(m => m.metadata?.text).join('\\n\\n');\n\n// 4. Generate with context\nconst response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {\n  messages: [\n    { role: 'system', content: `Answer based on:\\n\\n${context}` },\n    { role: 'user', content: query }\n  ]\n});\n```\n\n## Streaming (SSE)\n\n```typescript\nconst stream = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {\n  messages, stream: true\n});\n\nconst { readable, writable } = new TransformStream();\nconst writer = writable.getWriter();\n\n(async () => {\n  for await (const chunk of stream) {\n    await writer.write(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\\n\\n`));\n  }\n  await writer.write(new TextEncoder().encode('data: [DONE]\\n\\n'));\n  await writer.close();\n})();\n\nreturn new Response(readable, {\n  headers: { 'Content-Type': 'text/event-stream' }\n});\n```\n\n## Error Handling & Retry\n\n```typescript\nasync function runWithRetry(env, model, input, maxRetries = 3) {\n  for (let attempt = 0; attempt < maxRetries; attempt++) {\n    try {\n      return await env.AI.run(model, input);\n    } catch (error) {\n      if (error.message?.includes('7505') && attempt < maxRetries - 1) {\n        await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));\n        continue;\n      }\n      throw error;\n    }\n  }\n}\n```\n\n## Model Fallback\n\n```typescript\ntry {\n  return await env.AI.run('@cf/meta/llama-3.1-70b-instruct', { messages });\n} catch {\n  return await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages });\n}\n```\n\n## Prompt Patterns\n\n```typescript\n// System prompts\nconst PROMPTS = {\n  json: 'Respond with valid JSON only.',\n  concise: 'Keep responses brief.',\n  cot: 'Think step by step before answering.'\n};\n\n// Few-shot\nmessages: [\n  { role: 'system', content: 'Extract as JSON' },\n  { role: 'user', content: 'John bought 3 apples for $5' },\n  { role: 'assistant', content: '{\"name\":\"John\",\"item\":\"apples\",\"qty\":3}' },\n  { role: 'user', content: actualInput }\n]\n```\n\n## Parallel Execution\n\n```typescript\nconst [sentiment, summary, embedding] = await Promise.all([\n  env.AI.run('@cf/mistral/mistral-7b-instruct-v0.1', { messages: sentimentPrompt }),\n  env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: summaryPrompt }),\n  env.AI.run('@cf/baai/bge-base-en-v1.5', { text })\n]);\n```\n\n## Cost Optimization\n\n| Task | Model | Neurons |\n|------|-------|---------|\n| Classify | `@cf/mistral/mistral-7b-instruct-v0.1` | ~50 |\n| Chat | `@cf/meta/llama-3.1-8b-instruct` | ~200 |\n| Complex | `@cf/meta/llama-3.1-70b-instruct` | ~2000 |\n| Embed | `@cf/baai/bge-base-en-v1.5` | ~10 |\n\n```typescript\n// Batch embeddings\nconst response = await env.AI.run('@cf/baai/bge-base-en-v1.5', {\n  text: textsArray // Process multiple at once\n});\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-for-platforms/README.md",
    "content": "# Cloudflare Workers for Platforms\n\nMulti-tenant platform with isolated customer code execution at scale.\n\n## Use Cases\n\n- Multi-tenant SaaS running customer code\n- AI-generated code execution in secure sandboxes\n- Programmable platforms with isolated compute\n- Edge functions/serverless platforms\n- Website builders with static + dynamic content\n- Unlimited app deployment at scale\n\n**NOT for general Workers** - only for Workers for Platforms architecture.\n\n## Quick Start\n\n**One-click deploy:** [Platform Starter Kit](https://github.com/cloudflare/workers-for-platforms-example) deploys complete WfP setup with dispatch namespace, dispatch worker, and user worker example.\n\n[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/workers-for-platforms-example)\n\n**Manual setup:** See [configuration.md](./configuration.md) for namespace creation and dispatch worker configuration.\n\n## Key Features\n\n- Unlimited Workers per namespace (no script limits)\n- Automatic tenant isolation\n- Custom CPU/subrequest limits per customer\n- Hostname routing (subdomains/vanity domains)\n- Egress/ingress control\n- Static assets support\n- Tags for bulk operations\n\n## Architecture\n\n**4 Components:**\n1. **Dispatch Namespace** - Container for unlimited customer Workers, automatic isolation (untrusted mode by default - no request.cf access, no shared cache)\n2. **Dynamic Dispatch Worker** - Entry point, routes requests, enforces platform logic (auth, limits, validation)\n3. **User Workers** - Customer code in isolated sandboxes, API-deployed, optional bindings (KV/D1/R2/DO)\n4. **Outbound Worker** (optional) - Intercepts external fetch, controls egress, logs subrequests (blocks TCP socket connect() API)\n\n**Request Flow:**\n```\nRequest → Dispatch Worker → Determines user Worker → env.DISPATCHER.get(\"customer\") \n→ User Worker executes (Outbound Worker for external fetch) → Response → Dispatch Worker → Client\n```\n\n## Decision Trees\n\n### When to Use Workers for Platforms\n```\nNeed to run code?\n├─ Your code only → Regular Workers\n├─ Customer/AI code → Workers for Platforms\n└─ Untrusted code in sandbox → Workers for Platforms OR Sandbox API\n```\n\n### Routing Strategy Selection\n```\nHostname routing needed?\n├─ Subdomains only (*.saas.com) → `*.saas.com/*` route + subdomain extraction\n├─ Custom domains → `*/*` wildcard + Cloudflare for SaaS + KV/metadata routing\n└─ Path-based (/customer/app) → Any route + path parsing\n```\n\n### Isolation Mode Selection\n```\nWorker mode?\n├─ Running customer code → Untrusted (default)\n├─ Need request.cf geolocation → Trusted mode\n├─ Internal platform, controlled code → Trusted mode with cache key prefixes\n└─ Maximum isolation → Untrusted + unique resources per customer\n```\n\n## In This Reference\n\n| File | Purpose | When to Read |\n|------|---------|--------------|\n| [configuration.md](./configuration.md) | Namespace setup, dispatch worker config | First-time setup, changing limits |\n| [api.md](./api.md) | User worker API, dispatch API, outbound worker | Deploying workers, SDK integration |\n| [patterns.md](./patterns.md) | Multi-tenancy, routing, egress control | Planning architecture, scaling |\n| [gotchas.md](./gotchas.md) | Limits, isolation issues, best practices | Debugging, production prep |\n\n## See Also\n- [workers](../workers/) - Core Workers runtime documentation\n- [durable-objects](../durable-objects/) - Stateful multi-tenant patterns\n- [sandbox](../sandbox/) - Alternative for untrusted code execution\n- [Reference Architecture: Programmable Platforms](https://developers.cloudflare.com/reference-architecture/diagrams/serverless/programmable-platforms/)\n- [Reference Architecture: AI Vibe Coding Platform](https://developers.cloudflare.com/reference-architecture/diagrams/ai/ai-vibe-coding-platform/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-for-platforms/api.md",
    "content": "# API Operations\n\n## Deploy User Worker\n\n```bash\ncurl -X PUT \\\n  \"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/dispatch/namespaces/$NAMESPACE/scripts/$SCRIPT_NAME\" \\\n  -H \"Authorization: Bearer $API_TOKEN\" \\\n  -F 'metadata={\"main_module\": \"worker.mjs\"};type=application/json' \\\n  -F 'worker.mjs=@worker.mjs;type=application/javascript+module'\n```\n\n### TypeScript SDK\n```typescript\nimport Cloudflare from \"cloudflare\";\n\nconst client = new Cloudflare({ apiToken: process.env.API_TOKEN });\n\nconst scriptFile = new File([scriptContent], `${scriptName}.mjs`, {\n  type: \"application/javascript+module\",\n});\n\nawait client.workersForPlatforms.dispatch.namespaces.scripts.update(\n  namespace, scriptName,\n  {\n    account_id: accountId,\n    metadata: { main_module: `${scriptName}.mjs` },\n    files: [scriptFile],\n  }\n);\n```\n\n## TypeScript Types\n\n```typescript\nimport type { DispatchNamespace } from '@cloudflare/workers-types';\n\ninterface DispatchNamespace {\n  get(name: string, options?: Record<string, unknown>, dispatchOptions?: DynamicDispatchOptions): Fetcher;\n}\n\ninterface DynamicDispatchOptions {\n  limits?: DynamicDispatchLimits;\n  outbound?: Record<string, unknown>;\n}\n\ninterface DynamicDispatchLimits {\n  cpuMs?: number;        // Max CPU milliseconds\n  subRequests?: number;  // Max fetch() calls\n}\n\n// Usage\nconst userWorker = env.DISPATCHER.get('customer-123', {}, {\n  limits: { cpuMs: 50, subRequests: 20 },\n  outbound: { customerId: '123', url: request.url }\n});\n```\n\n## Deploy with Bindings\n```bash\ncurl -X PUT \".../scripts/$SCRIPT_NAME\" \\\n  -F 'metadata={\n    \"main_module\": \"worker.mjs\",\n    \"bindings\": [\n      {\"type\": \"kv_namespace\", \"name\": \"MY_KV\", \"namespace_id\": \"'$KV_ID'\"}\n    ],\n    \"tags\": [\"customer-123\", \"production\"],\n    \"compatibility_date\": \"2026-01-01\"  // Use current date for new projects\n  };type=application/json' \\\n  -F 'worker.mjs=@worker.mjs;type=application/javascript+module'\n```\n\n## List/Delete Workers\n\n```bash\n# List\ncurl \"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/dispatch/namespaces/$NAMESPACE/scripts\" \\\n  -H \"Authorization: Bearer $API_TOKEN\"\n\n# Delete by name\ncurl -X DELETE \".../scripts/$SCRIPT_NAME\" -H \"Authorization: Bearer $API_TOKEN\"\n\n# Delete by tag\ncurl -X DELETE \".../scripts?tags=customer-123%3Ayes\" -H \"Authorization: Bearer $API_TOKEN\"\n```\n\n**Pagination:** SDK supports async iteration. Manual: add `?per_page=100&page=1` query params.\n\n## Static Assets\n\n**3-step process:** Create session → Upload files → Deploy Worker\n\n### 1. Create Upload Session\n```bash\ncurl -X POST \".../scripts/$SCRIPT_NAME/assets-upload-session\" \\\n  -H \"Authorization: Bearer $API_TOKEN\" \\\n  -d '{\n    \"manifest\": {\n      \"/index.html\": {\"hash\": \"08f1dfda4574284ab3c21666d1ee8c7d4\", \"size\": 1234}\n    }\n  }'\n# Returns: jwt, buckets\n```\n\n**Hash:** SHA-256 truncated to first 16 bytes (32 hex characters)\n\n### 2. Upload Files\n```bash\ncurl -X POST \".../workers/assets/upload?base64=true\" \\\n  -H \"Authorization: Bearer $UPLOAD_JWT\" \\\n  -F '08f1dfda4574284ab3c21666d1ee8c7d4=<BASE64_CONTENT>'\n# Returns: completion jwt\n```\n\n**Multiple buckets:** Upload to all returned bucket URLs (typically 2 for redundancy) using same JWT and hash.\n\n### 3. Deploy with Assets\n```bash\ncurl -X PUT \".../scripts/$SCRIPT_NAME\" \\\n  -F 'metadata={\n    \"main_module\": \"index.js\",\n    \"assets\": {\"jwt\": \"<COMPLETION_TOKEN>\"},\n    \"bindings\": [{\"type\": \"assets\", \"name\": \"ASSETS\"}]\n  };type=application/json' \\\n  -F 'index.js=export default {...};type=application/javascript+module'\n```\n\n**Asset Isolation:** Assets shared across namespace by default. For customer isolation, salt hash: `sha256(customerId + fileContents).slice(0, 32)`\n\n## Dispatch Workers\n\n### Subdomain Routing\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const userWorkerName = new URL(request.url).hostname.split(\".\")[0];\n    const userWorker = env.DISPATCHER.get(userWorkerName);\n    return await userWorker.fetch(request);\n  },\n};\n```\n\n### Path Routing\n```typescript\nconst pathParts = new URL(request.url).pathname.split(\"/\").filter(Boolean);\nconst userWorker = env.DISPATCHER.get(pathParts[0]);\nreturn await userWorker.fetch(request);\n```\n\n### KV Routing\n```typescript\nconst hostname = new URL(request.url).hostname;\nconst userWorkerName = await env.ROUTING_KV.get(hostname);\nconst userWorker = env.DISPATCHER.get(userWorkerName);\nreturn await userWorker.fetch(request);\n```\n\n## Outbound Workers\n\nControl external fetch from user Workers:\n\n### Configure\n```typescript\nconst userWorker = env.DISPATCHER.get(\n  workerName, {},\n  { outbound: { customer_context: { customer_name: workerName, url: request.url } } }\n);\n```\n\n### Implement\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const customerName = env.customer_name;\n    const url = new URL(request.url);\n    \n    // Block domains\n    if ([\"malicious.com\"].some(d => url.hostname.includes(d))) {\n      return new Response(\"Blocked\", { status: 403 });\n    }\n    \n    // Inject auth\n    if (url.hostname === \"api.example.com\") {\n      const headers = new Headers(request.headers);\n      headers.set(\"Authorization\", `Bearer ${generateJWT(customerName)}`);\n      return fetch(new Request(request, { headers }));\n    }\n    \n    return fetch(request);\n  },\n};\n```\n\n**Note:** Doesn't intercept DO/mTLS fetch.\n\nSee [README.md](./README.md), [configuration.md](./configuration.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-for-platforms/configuration.md",
    "content": "# Configuration\n\n## Dispatch Namespace Binding\n\n### wrangler.jsonc\n```jsonc\n{\n  \"$schema\": \"./node_modules/wrangler/config-schema.json\",\n  \"dispatch_namespaces\": [{\n    \"binding\": \"DISPATCHER\",\n    \"namespace\": \"production\"\n  }]\n}\n```\n\n## Worker Isolation Mode\n\nWorkers in a namespace run in **untrusted mode** by default for security:\n- No access to `request.cf` object\n- Isolated cache per Worker (no shared cache)\n- `caches.default` disabled\n\n### Enable Trusted Mode\n\nFor internal platforms where you control all code:\n\n```bash\ncurl -X PUT \\\n  \"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/dispatch/namespaces/$NAMESPACE\" \\\n  -H \"Authorization: Bearer $API_TOKEN\" \\\n  -d '{\"name\": \"'$NAMESPACE'\", \"trusted_workers\": true}'\n```\n\n**Caveats:**\n- Workers share cache within namespace (use cache key prefixes: `customer-${id}:${key}`)\n- `request.cf` object accessible\n- Redeploy existing Workers after enabling trusted mode\n\n**When to use:** Internal platforms, A/B testing platforms, need geolocation data\n\n\n### With Outbound Worker\n```jsonc\n{\n  \"dispatch_namespaces\": [{\n    \"binding\": \"DISPATCHER\",\n    \"namespace\": \"production\",\n    \"outbound\": {\n      \"service\": \"outbound-worker\",\n      \"parameters\": [\"customer_context\"]\n    }\n  }]\n}\n```\n\n## Wrangler Commands\n\n```bash\nwrangler dispatch-namespace list\nwrangler dispatch-namespace get production\nwrangler dispatch-namespace create production\nwrangler dispatch-namespace delete staging\nwrangler dispatch-namespace rename old new\n```\n\n## Custom Limits\n\nSet CPU time and subrequest limits per invocation:\n\n```typescript\nconst userWorker = env.DISPATCHER.get(\n  workerName,\n  {},\n  {\n    limits: { \n      cpuMs: 10,        // Max CPU ms\n      subRequests: 5    // Max fetch() calls\n    }\n  }\n);\n```\n\nHandle limit violations:\n```typescript\ntry {\n  return await userWorker.fetch(request);\n} catch (e) {\n  if (e.message.includes(\"CPU time limit\")) {\n    return new Response(\"CPU limit exceeded\", { status: 429 });\n  }\n  throw e;\n}\n```\n\n## Static Assets\n\nDeploy HTML/CSS/images with Workers. See [api.md](./api.md#static-assets) for upload process.\n\n### Wrangler\n```jsonc\n{\n  \"name\": \"customer-site\",\n  \"main\": \"./src/index.js\",\n  \"assets\": {\n    \"directory\": \"./public\",\n    \"binding\": \"ASSETS\"\n  }\n}\n```\n\n```bash\nnpx wrangler deploy --name customer-site --dispatch-namespace production\n```\n\n### Dashboard Deployment\n\nAlternative to CLI:\n\n1. Upload Worker file in dashboard\n2. Add `--dispatch-namespace` flag: `wrangler deploy --dispatch-namespace production`\n3. Or configure in wrangler.jsonc under `dispatch_namespaces`\n\nSee [api.md](./api.md) for programmatic deployment via REST API or SDK.\n\n## Tags\n\nOrganize/search Workers (max 8/script):\n\n```bash\n# Set tags\ncurl -X PUT \".../tags\" -d '[\"customer-123\", \"pro\", \"production\"]'\n\n# Filter by tag\ncurl \".../scripts?tags=production%3Ayes\"\n\n# Delete by tag\ncurl -X DELETE \".../scripts?tags=customer-123%3Ayes\"\n```\n\nCommon patterns: `customer-123`, `free|pro|enterprise`, `production|staging`\n\n## Bindings\n\n**Supported binding types:** 29 total including KV, D1, R2, Durable Objects, Analytics Engine, Service, Assets, Queue, Vectorize, Hyperdrive, Workflow, AI, Browser, and more.\n\nAdd via API metadata (see [api.md](./api.md#deploy-with-bindings)):\n```json\n{\n  \"bindings\": [\n    {\"type\": \"kv_namespace\", \"name\": \"USER_KV\", \"namespace_id\": \"...\"},\n    {\"type\": \"r2_bucket\", \"name\": \"STORAGE\", \"bucket_name\": \"...\"},\n    {\"type\": \"d1\", \"name\": \"DB\", \"id\": \"...\"}\n  ]\n}\n```\n\nPreserve existing bindings:\n```json\n{\n  \"bindings\": [{\"type\": \"r2_bucket\", \"name\": \"STORAGE\", \"bucket_name\": \"new\"}],\n  \"keep_bindings\": [\"kv_namespace\", \"d1\"]  // Preserves existing bindings of these types\n}\n```\n\nFor complete binding type reference, see [bindings](../bindings/) documentation\n\nSee [README.md](./README.md), [api.md](./api.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-for-platforms/gotchas.md",
    "content": "# Gotchas & Limits\n\n## Common Errors\n\n### \"Worker not found\"\n\n**Cause:** Attempting to get Worker that doesn't exist in namespace  \n**Solution:** Catch error and return 404:\n\n```typescript\ntry {\n  const userWorker = env.DISPATCHER.get(workerName);\n  return userWorker.fetch(request);\n} catch (e) {\n  if (e.message.startsWith(\"Worker not found\")) {\n    return new Response(\"Worker not found\", { status: 404 });\n  }\n  throw e;  // Re-throw unexpected errors\n}\n```\n\n### \"CPU time limit exceeded\"\n\n**Cause:** User Worker exceeded configured CPU time limit  \n**Solution:** Track violations in Analytics Engine and return 429 response; consider adjusting limits per customer tier\n\n### \"Hostname Routing Issues\"\n\n**Cause:** DNS proxy settings causing routing problems  \n**Solution:** Use `*/*` wildcard route which works regardless of proxy settings for orange-to-orange routing\n\n### \"Bindings Lost on Update\"\n\n**Cause:** Not using `keep_bindings` flag when updating Worker  \n**Solution:** Use `keep_bindings: true` in API requests to preserve existing bindings during updates\n\n### \"Tag Filtering Not Working\"\n\n**Cause:** Special characters not URL encoded in tag filters  \n**Solution:** URL encode tags (e.g., `tags=production%3Ayes`) and avoid special chars like `,` and `&`\n\n### \"Deploy Failures with ES Modules\"\n\n**Cause:** Incorrect upload format for ES modules  \n**Solution:** Use multipart form upload, specify `main_module` in metadata, and set file type to `application/javascript+module`\n\n### \"Static Asset Upload Failed\"\n\n**Cause:** Invalid hash format, expired token, or incorrect encoding  \n**Solution:** Hash must be first 16 bytes (32 hex chars) of SHA-256, upload within 1 hour of session creation, deploy within 1 hour of upload completion, and Base64 encode file contents\n\n### \"Outbound Worker Not Intercepting Calls\"\n\n**Cause:** Outbound Workers don't intercept Durable Object or mTLS binding fetch  \n**Solution:** Plan egress control accordingly; not all fetch calls are intercepted\n\n### \"TCP Socket Connection Failed\"\n\n**Cause:** Outbound Worker enabled blocks `connect()` API for TCP sockets  \n**Solution:** Outbound Workers only intercept `fetch()` calls; TCP socket connections unavailable when outbound configured. Remove outbound if TCP needed, or use proxy pattern.\n\n### \"API Rate Limit Exceeded\"\n\n**Cause:** Exceeded Cloudflare API rate limits (1200 requests per 5 minutes per account, 200 requests per second per IP)  \n**Solution:** Implement exponential backoff:\n\n```typescript\nasync function deployWithBackoff(deploy: () => Promise<void>, maxRetries = 3) {\n  for (let i = 0; i < maxRetries; i++) {\n    try {\n      return await deploy();\n    } catch (e) {\n      if (e.status === 429 && i < maxRetries - 1) {\n        await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));\n        continue;\n      }\n      throw e;\n    }\n  }\n}\n```\n\n### \"Gradual Deployment Not Supported\"\n\n**Cause:** Attempted to use gradual deployments with user Workers  \n**Solution:** Gradual deployments not supported for Workers in dispatch namespaces. Use all-at-once deployment with staged rollout via dispatch worker logic (feature flags, percentage-based routing).\n\n### \"Asset Session Expired\"\n\n**Cause:** Upload JWT expired (1 hour validity) or completion token expired (1 hour after upload)  \n**Solution:** Complete asset upload within 1 hour of session creation, and deploy Worker within 1 hour of upload completion. For large uploads, batch files or increase upload parallelism.\n\n## Platform Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Workers per namespace | Unlimited | Unlike regular Workers (500 per account) |\n| Namespaces per account | Unlimited | Best practice: 1 production + 1 staging |\n| Max tags per Worker | 8 | For filtering and organization |\n| Worker mode | Untrusted (default) | No `request.cf` access unless trusted mode |\n| Cache isolation | Per-Worker (untrusted) | Shared in trusted mode with key prefixes |\n| Durable Object namespaces | Unlimited | No per-account limit for WfP |\n| Gradual Deployments | Not supported | All-at-once only |\n| `caches.default` | Disabled (untrusted) | Use Cache API with custom keys |\n\n## Asset Upload Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Upload session JWT validity | 1 hour | Must complete upload within this time |\n| Completion token validity | 1 hour | Must deploy within this time after upload |\n| Asset hash format | First 16 bytes SHA-256 | 32 hex characters |\n| Base64 encoding | Required | For binary files |\n\n## API Rate Limits\n\n| Limit Type | Value | Scope |\n|------------|-------|-------|\n| Client API | 1200 requests / 5 min | Per account |\n| Client API | 200 requests / sec | Per IP address |\n| GraphQL | Varies by query cost | Query complexity |\n\nSee [Cloudflare API Rate Limits](https://developers.cloudflare.com/fundamentals/api/reference/limits/) for details.\n\n## Operational Limits\n\n| Operation | Limit | Notes |\n|-----------|-------|-------|\n| CPU time (custom limits) | Up to Workers plan limit | Set per-invocation in dispatch worker |\n| Subrequests (custom limits) | Up to Workers plan limit | Set per-invocation in dispatch worker |\n| Outbound Worker subrequests | Not intercepted for DO/mTLS | Only regular fetch() calls |\n| TCP sockets with outbound | Disabled | `connect()` API unavailable |\n\nSee [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-for-platforms/patterns.md",
    "content": "# Multi-Tenant Patterns\n\n## Billing by Plan\n\n```typescript\ninterface Env {\n  DISPATCHER: DispatchNamespace;\n  CUSTOMERS_KV: KVNamespace;\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const userWorkerName = new URL(request.url).hostname.split(\".\")[0];\n    const customerPlan = await env.CUSTOMERS_KV.get(userWorkerName);\n    \n    const plans = {\n      enterprise: { cpuMs: 50, subRequests: 50 },\n      pro: { cpuMs: 20, subRequests: 20 },\n      free: { cpuMs: 10, subRequests: 5 },\n    };\n    const limits = plans[customerPlan as keyof typeof plans] || plans.free;\n    \n    const userWorker = env.DISPATCHER.get(userWorkerName, {}, { limits });\n    return await userWorker.fetch(request);\n  },\n};\n```\n\n## Resource Isolation\n\n**Complete isolation:** Create unique resources per customer\n- KV namespace per customer\n- D1 database per customer\n- R2 bucket per customer\n\n```typescript\nconst bindings = [{\n  type: \"kv_namespace\",\n  name: \"USER_KV\",\n  namespace_id: `customer-${customerId}-kv`\n}];\n```\n\n## Hostname Routing\n\n### Wildcard Route (Recommended)\nConfigure `*/*` route on SaaS domain → dispatch Worker\n\n**Benefits:**\n- Supports subdomains + custom vanity domains\n- No per-route limits (regular Workers limited to 100 routes)\n- Programmatic control\n- Works with any DNS proxy settings\n\n**Setup:**\n1. Cloudflare for SaaS custom hostnames\n2. Fallback origin (dummy `A 192.0.2.0` if Worker is origin)\n3. DNS CNAME to SaaS domain\n4. `*/*` route → dispatch Worker\n5. Routing logic in dispatch Worker\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const hostname = new URL(request.url).hostname;\n    const hostnameData = await env.ROUTING_KV.get(`hostname:${hostname}`, { type: \"json\" });\n    \n    if (!hostnameData?.workerName) {\n      return new Response(\"Hostname not configured\", { status: 404 });\n    }\n    \n    const userWorker = env.DISPATCHER.get(hostnameData.workerName);\n    return await userWorker.fetch(request);\n  },\n};\n```\n\n### Subdomain-Only\n1. Wildcard DNS: `*.saas.com` → origin\n2. Route: `*.saas.com/*` → dispatch Worker\n3. Extract subdomain for routing\n\n### Orange-to-Orange (O2O) Behavior\n\nWhen customers use Cloudflare and CNAME to your Workers domain:\n\n| Scenario | Behavior | Route Pattern |\n|----------|----------|---------------|\n| Customer not on Cloudflare | Standard routing | `*/*` or `*.domain.com/*` |\n| Customer on Cloudflare (proxied CNAME) | Invokes Worker at edge | `*/*` required |\n| Customer on Cloudflare (DNS-only CNAME) | Standard routing | Any route works |\n\n**Recommendation:** Always use `*/*` wildcard for consistent O2O behavior.\n\n### Custom Metadata Routing\n\nFor Cloudflare for SaaS: Store worker name in custom hostname `custom_metadata`, retrieve in dispatch worker to route requests. Requires custom hostnames as subdomains of your domain.\n\n## Observability\n\n### Logpush\n- Enable on dispatch Worker → captures all user Worker logs\n- Filter by `Outcome` or `Script Name`\n\n### Tail Workers\n- Real-time logs with custom formatting\n- Receives HTTP status, `console.log()`, exceptions, diagnostics\n\n### Analytics Engine\n```typescript\n// Track violations\nenv.ANALYTICS.writeDataPoint({\n  indexes: [customerName],\n  blobs: [\"cpu_limit_exceeded\"],\n});\n```\n\n### GraphQL\n```graphql\nquery {\n  viewer {\n    accounts(filter: {accountTag: $accountId}) {\n      workersInvocationsAdaptive(filter: {dispatchNamespaceName: \"production\"}) {\n        sum { requests errors cpuTime }\n      }\n    }\n  }\n}\n```\n\n## Use Case Implementations\n\n### AI Code Execution\n```typescript\nasync function deployGeneratedCode(name: string, code: string) {\n  const file = new File([code], `${name}.mjs`, { type: \"application/javascript+module\" });\n  await client.workersForPlatforms.dispatch.namespaces.scripts.update(\"production\", name, {\n    account_id: accountId,\n    metadata: { main_module: `${name}.mjs`, tags: [name, \"ai-generated\"] },\n    files: [file],\n  });\n}\n\n// Short limits for untrusted code\nconst userWorker = env.DISPATCHER.get(sessionId, {}, { limits: { cpuMs: 5, subRequests: 3 } });\n```\n\n**VibeSDK:** For AI-powered code generation + deployment platforms, see [VibeSDK](https://github.com/cloudflare/vibesdk) - handles AI generation, sandbox execution, live preview, and deployment.\n\nReference: [AI Vibe Coding Platform Architecture](https://developers.cloudflare.com/reference-architecture/diagrams/ai/ai-vibe-coding-platform/)\n\n### Edge Functions Platform\n```typescript\n// Route: /customer-id/function-name\nconst [customerId, functionName] = new URL(request.url).pathname.split(\"/\").filter(Boolean);\nconst workerName = `${customerId}-${functionName}`;\nconst userWorker = env.DISPATCHER.get(workerName);\n```\n\n### Website Builder\n- Deploy static assets + Worker code\n- See [api.md](./api.md#static-assets) for full implementation\n- Salt hashes for asset isolation\n\n## Best Practices\n\n### Architecture\n- One namespace per environment (production, staging)\n- Platform logic in dispatch Worker (auth, rate limiting, validation)\n- Isolation automatic (no shared cache, untrusted mode)\n\n### Routing\n- Use `*/*` wildcard routes\n- Store mappings in KV\n- Handle missing Workers gracefully\n\n### Limits & Security\n- Set custom limits by plan\n- Track violations with Analytics Engine\n- Use outbound Workers for egress control\n- Sanitize responses\n\n### Tags\n- Tag all Workers: customer ID, plan, environment\n- Enable bulk operations\n- Filter efficiently\n\nSee [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [gotchas.md](./gotchas.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-playground/README.md",
    "content": "# Cloudflare Workers Playground Skill Reference\n\n## Overview\n\nCloudflare Workers Playground is a browser-based sandbox for instantly experimenting with, testing, and deploying Cloudflare Workers without authentication or setup. This skill provides patterns, APIs, and best practices specifically for Workers Playground development.\n\n**URL:** [workers.cloudflare.com/playground](https://workers.cloudflare.com/playground)\n\n## ⚠️ Playground Constraints\n\n**Playground is NOT production-equivalent:**\n- ✅ Real Workers runtime, instant testing, shareable URLs\n- ❌ No TypeScript (JavaScript only)\n- ❌ No bindings (KV, D1, R2, Durable Objects)\n- ❌ No environment variables or secrets\n- ❌ ES modules only (no Service Worker format)\n- ⚠️ Safari broken (use Chrome/Firefox)\n\n**For production:** Use `wrangler` CLI. Playground is for rapid prototyping.\n\n## Quick Start\n\nMinimal Worker:\n\n```javascript\nexport default {\n  async fetch(request, env, ctx) {\n    return new Response('Hello World');\n  }\n};\n```\n\nJSON API:\n\n```javascript\nexport default {\n  async fetch(request, env, ctx) {\n    const data = { message: 'Hello', timestamp: Date.now() };\n    return Response.json(data);\n  }\n};\n```\n\nProxy with modification:\n\n```javascript\nexport default {\n  async fetch(request, env, ctx) {\n    const response = await fetch('https://example.com');\n    const modified = new Response(response.body, response);\n    modified.headers.set('X-Custom-Header', 'added-by-worker');\n    return modified;\n  }\n};\n```\n\nImport from CDN:\n\n```javascript\nimport { Hono } from 'https://esm.sh/hono@3';\n\nexport default {\n  async fetch(request) {\n    const app = new Hono();\n    app.get('/', (c) => c.text('Hello Hono!'));\n    return app.fetch(request);\n  }\n};\n```\n\n## Reading Order\n\n1. **[configuration.md](configuration.md)** - Start here: playground setup, constraints, deployment\n2. **[api.md](api.md)** - Core APIs: Request, Response, ExecutionContext, fetch, Cache\n3. **[patterns.md](patterns.md)** - Common use cases: routing, proxying, A/B testing, multi-module code\n4. **[gotchas.md](gotchas.md)** - Troubleshooting: errors, browser issues, limits, best practices\n\n## In This Reference\n\n- **[configuration.md](configuration.md)** - Setup, deployment, configuration\n- **[api.md](api.md)** - API endpoints, methods, interfaces\n- **[patterns.md](patterns.md)** - Common patterns, use cases, examples\n- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations\n\n## Key Features\n\n**No Setup Required:**\n- Open URL and start coding\n- No CLI, no account, no config files\n- Code executes in real Cloudflare Workers runtime\n\n**Instant Preview:**\n- Live preview pane with browser tab or HTTP tester\n- Auto-reload on code changes\n- DevTools integration (right-click → Inspect)\n\n**Share & Deploy:**\n- Copy Link generates permanent shareable URL\n- Deploy button publishes to production in ~30 seconds\n- Get `*.workers.dev` subdomain immediately\n\n## Common Use Cases\n\n- **API development:** Test endpoints before wrangler setup\n- **Learning Workers:** Experiment with APIs without local environment\n- **Prototyping:** Quick POCs for edge logic\n- **Sharing examples:** Generate shareable links for bug reports or demos\n- **Framework testing:** Import from CDN (Hono, itty-router, etc.)\n\n## Limitations vs Production\n\n| Feature | Playground | Production (wrangler) |\n|---------|------------|----------------------|\n| Language | JavaScript only | JS + TypeScript |\n| Bindings | None | KV, D1, R2, DO, AI, etc. |\n| Environment vars | None | Full support |\n| Module format | ES only | ES + Service Worker |\n| CPU time | 10ms (Free plan) | 10ms Free / 50ms Paid |\n| Custom domains | No | Yes |\n| Analytics | No | Yes |\n\n## See Also\n\n- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)\n- [Workers Examples](https://developers.cloudflare.com/workers/examples/)\n- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/)\n- [Workers API Reference](https://developers.cloudflare.com/workers/runtime-apis/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-playground/api.md",
    "content": "# Workers Playground API\n\n## Handler\n\n```javascript\nexport default {\n  async fetch(request, env, ctx) {\n    // request: Request, env: {} (empty in playground), ctx: ExecutionContext\n    return new Response('Hello');\n  }\n};\n```\n\n## Request\n\n```javascript\nconst method = request.method;       // \"GET\", \"POST\"\nconst url = new URL(request.url);    // Parse URL\nconst headers = request.headers;     // Headers object\nconst body = await request.json();   // Read body (consumes stream)\nconst clone = request.clone();       // Clone before reading body\n\n// Query params\nurl.searchParams.get('page');        // Single value\nurl.searchParams.getAll('tag');      // Array\n\n// Cloudflare metadata\nrequest.cf.country;                  // \"US\"\nrequest.cf.colo;                     // \"SFO\"\n```\n\n## Response\n\n```javascript\n// Text\nreturn new Response('Hello', { status: 200 });\n\n// JSON\nreturn Response.json({ data }, { status: 200, headers: {...} });\n\n// Redirect\nreturn Response.redirect('/new-path', 301);\n\n// Modify existing\nconst modified = new Response(response.body, response);\nmodified.headers.set('X-Custom', 'value');\n```\n\n## ExecutionContext\n\n```javascript\n// Background work (after response sent)\nctx.waitUntil(fetch('https://logs.example.com', { method: 'POST', body: '...' }));\nreturn new Response('OK'); // Returns immediately\n```\n\n## Fetch\n\n```javascript\nconst response = await fetch('https://api.example.com');\nconst data = await response.json();\n\n// With options\nawait fetch(url, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ name: 'Alice' })\n});\n```\n\n## Cache\n\n```javascript\nconst cache = caches.default;\n\n// Check cache\nlet response = await cache.match(request);\nif (!response) {\n  response = await fetch(origin);\n  await cache.put(request, response.clone()); // Clone before put!\n}\nreturn response;\n```\n\n## Crypto\n\n```javascript\ncrypto.randomUUID();                 // UUID v4\ncrypto.getRandomValues(new Uint8Array(16));\n\n// SHA-256 hash\nconst hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data));\n```\n\n## Limits (Playground = Free Plan)\n\n| Resource | Limit |\n|----------|-------|\n| CPU time | 10ms |\n| Subrequests | 50 |\n| Memory | 128 MB |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-playground/configuration.md",
    "content": "# Configuration\n\n## Getting Started\n\nNavigate to [workers.cloudflare.com/playground](https://workers.cloudflare.com/playground)\n\n- **No account required** for testing\n- **No CLI or local setup** needed\n- Code executes in real Cloudflare Workers runtime\n- Share code via URL (never expires)\n\n## Playground Constraints\n\n⚠️ **Important Limitations**\n\n| Constraint | Playground | Production Workers |\n|------------|------------|-------------------|\n| **Module Format** | ES modules only | ES modules or Service Worker |\n| **TypeScript** | Not supported (JS only) | Supported via build step |\n| **Bindings** | Not available | KV, D1, R2, Durable Objects, etc. |\n| **wrangler.toml** | Not used | Required for config |\n| **Environment Variables** | Not available | Full support |\n| **Secrets** | Not available | Full support |\n| **Custom Domains** | Not available | Full support |\n\n**Playground is for rapid prototyping only.** For production apps, use `wrangler` CLI.\n\n## Code Editor\n\n### Syntax Requirements\n\nMust export default object with `fetch` handler:\n\n```javascript\nexport default {\n  async fetch(request, env, ctx) {\n    return new Response('Hello World');\n  }\n};\n```\n\n**Key Points:**\n- Must use ES modules (`export default`)\n- `fetch` method receives `(request, env, ctx)`\n- Must return `Response` object\n- TypeScript not supported (use plain JavaScript)\n\n### Multi-Module Code\n\nImport from external URLs or inline modules:\n\n```javascript\n// Import from CDN\nimport { Hono } from 'https://esm.sh/hono@3';\n\n// Or paste library code and import relatively\n// (See patterns.md for multi-module examples)\n\nexport default {\n  async fetch(request) {\n    const app = new Hono();\n    app.get('/', (c) => c.text('Hello'));\n    return app.fetch(request);\n  }\n};\n```\n\n## Preview Panel\n\n### Browser Tab\n\nDefault interactive preview with address bar:\n- Enter custom URL paths\n- Automatic reload on code changes\n- DevTools available (right-click → Inspect)\n\n### HTTP Test Panel\n\nSwitch to **HTTP** tab for raw HTTP testing:\n- Change HTTP method (GET, POST, PUT, DELETE, PATCH, etc.)\n- Add/edit request headers\n- Modify request body (JSON, form data, text)\n- View response headers and body\n- Test different content types\n\nExample HTTP test:\n```\nMethod: POST\nURL: /api/users\nHeaders:\n  Content-Type: application/json\n  Authorization: Bearer token123\nBody:\n{\n  \"name\": \"Alice\",\n  \"email\": \"alice@example.com\"\n}\n```\n\n## Sharing Code\n\n**Copy Link** button generates shareable URL:\n- Code embedded in URL fragment\n- Links never expire\n- No account required\n- Can be bookmarked for later\n\nExample: `https://workers.cloudflare.com/playground#abc123...`\n\n## Deploying from Playground\n\nClick **Deploy** button to move code to production:\n\n1. **Log in** to Cloudflare account (creates free account if needed)\n2. **Review** Worker name and code\n3. **Deploy** to global network (takes ~30 seconds)\n4. **Get URL**: Deployed to `<name>.workers.dev` subdomain\n5. **Manage** from dashboard: add bindings, custom domains, analytics\n\n**After deploy:**\n- Code runs on Cloudflare's global network (300+ cities)\n- Can add KV, D1, R2, Durable Objects bindings\n- Configure custom domains and routes\n- View analytics and logs\n- Set environment variables and secrets\n\n**Note:** Deployed Workers are production-ready but start on Free plan (100k requests/day).\n\n## Browser Compatibility\n\n| Browser | Status | Notes |\n|---------|--------|-------|\n| Chrome/Edge | ✅ Full support | Recommended |\n| Firefox | ✅ Full support | Works well |\n| Safari | ⚠️ Broken | Preview fails with \"PreviewRequestFailed\" |\n\n**Safari users:** Use Chrome, Firefox, or Edge for Workers Playground.\n\n## DevTools Integration\n\n1. **Open preview** in browser tab\n2. **Right-click** → Inspect Element\n3. **Console tab** shows Worker logs:\n   - `console.log()` output\n   - Uncaught errors\n   - Network requests (subrequests)\n\n**Note:** DevTools show client-side console, not Worker execution logs. For production logging, use Logpush or Tail Workers.\n\n## Limits in Playground\n\nSame as production Free plan:\n\n| Resource | Limit | Notes |\n|----------|-------|-------|\n| CPU time | 10ms | Per request |\n| Memory | 128 MB | Per request |\n| Script size | 1 MB | After compression |\n| Subrequests | 50 | Outbound fetch calls |\n| Request size | 100 MB | Incoming |\n| Response size | Unlimited | Outgoing (streamed) |\n\n**Exceeding CPU time** throws error immediately. Optimize hot paths or upgrade to Paid plan (50ms CPU).\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-playground/gotchas.md",
    "content": "# Workers Playground Gotchas\n\n## Platform Limitations\n\n| Limitation | Impact | Workaround |\n|------------|--------|------------|\n| Safari broken | Preview fails | Use Chrome/Firefox/Edge |\n| TypeScript unsupported | TS syntax errors | Write plain JS or use JSDoc |\n| No bindings | `env` always `{}` | Mock data or use external APIs |\n| No env vars | Can't access secrets | Hardcode for testing |\n\n## Common Runtime Errors\n\n### \"Response body already read\"\n\n```javascript\n// ❌ Body consumed twice\nconst body = await request.text();\nawait fetch(url, { body: request.body }); // Error!\n\n// ✅ Clone first\nconst clone = request.clone();\nconst body = await request.text();\nawait fetch(url, { body: clone.body });\n```\n\n### \"Worker exceeded CPU time\"\n\n**Limit:** 10ms (free), 50ms (paid)\n\n```javascript\n// ✅ Move slow work to background\nctx.waitUntil(fetch('https://analytics.example.com', {...}));\nreturn new Response('OK'); // Return immediately\n```\n\n### \"Too many subrequests\"\n\n**Limit:** 50 (free), 1000 (paid)\n\n```javascript\n// ❌ 100 individual fetches\n// ✅ Batch into single API call\nawait fetch('https://api.example.com/batch', {\n  body: JSON.stringify({ ids: [...] })\n});\n```\n\n## Best Practices\n\n```javascript\n// Clone before caching\nawait cache.put(request, response.clone());\nreturn response;\n\n// Validate input early\nif (request.method !== 'POST') return new Response('', { status: 405 });\n\n// Handle errors\ntry { ... } catch (e) {\n  return Response.json({ error: e.message }, { status: 500 });\n}\n```\n\n## Limits\n\n| Resource | Free | Paid |\n|----------|------|------|\n| CPU time | 10ms | 50ms |\n| Memory | 128 MB | 128 MB |\n| Subrequests | 50 | 1000 |\n\n## Browser Support\n\n| Browser | Status |\n|---------|--------|\n| Chrome | ✅ Recommended |\n| Firefox | ✅ Works |\n| Edge | ✅ Works |\n| Safari | ❌ Broken |\n\n## Debugging\n\n```javascript\nconsole.log('URL:', request.url); // View in browser DevTools Console\n```\n\n**Note:** `console.log` works in playground. For production, use Logpush or Tail Workers.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-playground/patterns.md",
    "content": "# Workers Playground Patterns\n\n## JSON API\n\n```javascript\nexport default {\n  async fetch(request) {\n    const url = new URL(request.url);\n    if (url.pathname === '/api/hello') return Response.json({ message: 'Hello' });\n    if (url.pathname === '/api/echo' && request.method === 'POST') {\n      return Response.json({ received: await request.json() });\n    }\n    return Response.json({ error: 'Not found' }, { status: 404 });\n  }\n};\n```\n\n## Router Pattern\n\n```javascript\nconst routes = {\n  '/': () => new Response('Home'),\n  '/api/users': () => Response.json([{ id: 1, name: 'Alice' }])\n};\n\nexport default {\n  async fetch(request) {\n    const handler = routes[new URL(request.url).pathname];\n    return handler ? handler() : new Response('Not Found', { status: 404 });\n  }\n};\n```\n\n## Proxy Pattern\n\n```javascript\nexport default {\n  async fetch(request) {\n    const url = new URL(request.url);\n    url.hostname = 'api.example.com';\n    return fetch(url.toString(), {\n      method: request.method, headers: request.headers, body: request.body\n    });\n  }\n};\n```\n\n## CORS Handling\n\n```javascript\nexport default {\n  async fetch(request) {\n    if (request.method === 'OPTIONS') {\n      return new Response(null, {\n        headers: {\n          'Access-Control-Allow-Origin': '*',\n          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',\n          'Access-Control-Allow-Headers': 'Content-Type, Authorization'\n        }\n      });\n    }\n    const response = await fetch('https://api.example.com', request);\n    const modified = new Response(response.body, response);\n    modified.headers.set('Access-Control-Allow-Origin', '*');\n    return modified;\n  }\n};\n```\n\n## Caching\n\n```javascript\nexport default {\n  async fetch(request) {\n    if (request.method !== 'GET') return fetch(request);\n    const cache = caches.default;\n    let response = await cache.match(request);\n    if (!response) {\n      response = await fetch('https://api.example.com');\n      if (response.status === 200) await cache.put(request, response.clone());\n    }\n    return response;\n  }\n};\n```\n\n## Hono Framework\n\n```javascript\nimport { Hono } from 'https://esm.sh/hono@3';\nconst app = new Hono();\napp.get('/', (c) => c.text('Hello'));\napp.get('/api/users/:id', (c) => c.json({ id: c.req.param('id') }));\napp.notFound((c) => c.json({ error: 'Not found' }, 404));\nexport default app;\n```\n\n## Authentication\n\n```javascript\nexport default {\n  async fetch(request) {\n    const auth = request.headers.get('Authorization');\n    if (!auth?.startsWith('Bearer ')) {\n      return Response.json({ error: 'Unauthorized' }, { status: 401 });\n    }\n    const token = auth.substring(7);\n    if (token !== 'secret-token') {\n      return Response.json({ error: 'Invalid token' }, { status: 403 });\n    }\n    return Response.json({ message: 'Authenticated' });\n  }\n};\n```\n\n## Error Handling\n\n```javascript\nexport default {\n  async fetch(request) {\n    try {\n      const response = await fetch('https://api.example.com');\n      if (!response.ok) throw new Error(`API returned ${response.status}`);\n      return response;\n    } catch (error) {\n      return Response.json({ error: error.message }, { status: 500 });\n    }\n  }\n};\n```\n\n**Note:** In-memory state (Maps, variables) resets on Worker cold start. Use Durable Objects or KV for persistence.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-vpc/README.md",
    "content": "# Workers VPC Connectivity\n\nConnect Cloudflare Workers to private networks and internal infrastructure using TCP Sockets.\n\n## Overview\n\nWorkers VPC connectivity enables outbound TCP connections from Workers to private resources in AWS, Azure, GCP, on-premises datacenters, or any private network. This is achieved through the **TCP Sockets API** (`cloudflare:sockets`), which provides low-level network access for custom protocols and services.\n\n**Key capabilities:**\n- Direct TCP connections to private IPs and hostnames\n- TLS/StartTLS support for encrypted connections\n- Integration with Cloudflare Tunnel for secure private network access\n- Full control over wire protocols (database protocols, SSH, MQTT, custom TCP)\n\n**Note:** This reference documents the TCP Sockets API. For the newer Workers VPC Services product (HTTP-only service bindings with built-in SSRF protection), refer to separate documentation when available. VPC Services is currently in beta (2025+).\n\n## Quick Decision: Which Technology?\n\nNeed private network connectivity from Workers?\n\n| Requirement | Use | Why |\n|------------|-----|-----|\n| HTTP/HTTPS APIs in private network | VPC Services (beta, separate docs) | SSRF-safe, declarative bindings |\n| PostgreSQL/MySQL databases | [Hyperdrive](../hyperdrive/) | Connection pooling, caching, optimized |\n| Custom TCP protocols (SSH, MQTT, proprietary) | **TCP Sockets (this doc)** | Full protocol control |\n| Simple HTTP with lowest latency | TCP Sockets + [Smart Placement](../smart-placement/) | Manual optimization |\n| Expose on-prem to internet (inbound) | [Cloudflare Tunnel](../tunnel/) | Not Worker-specific |\n\n## When to Use TCP Sockets\n\n**Use TCP Sockets when you need:**\n- ✅ Direct control over wire protocols (e.g., Postgres wire protocol, SSH, Redis RESP)\n- ✅ Non-HTTP protocols (MQTT, SMTP, custom binary protocols)\n- ✅ StartTLS or custom TLS negotiation\n- ✅ Streaming binary data over TCP\n\n**Don't use TCP Sockets when:**\n- ❌ You just need HTTP/HTTPS (use `fetch()` or VPC Services)\n- ❌ You need PostgreSQL/MySQL (use Hyperdrive for pooling)\n- ❌ You need WebSocket (use native Workers WebSocket)\n\n## Quick Start\n\n```typescript\nimport { connect } from 'cloudflare:sockets';\n\nexport default {\n  async fetch(req: Request): Promise<Response> {\n    // Connect to private service\n    const socket = connect(\n      { hostname: \"db.internal.company.net\", port: 5432 },\n      { secureTransport: \"on\" }\n    );\n\n    try {\n      await socket.opened; // Wait for connection\n      \n      const writer = socket.writable.getWriter();\n      await writer.write(new TextEncoder().encode(\"QUERY\\r\\n\"));\n      await writer.close();\n\n      const reader = socket.readable.getReader();\n      const { value } = await reader.read();\n      \n      return new Response(value);\n    } finally {\n      await socket.close();\n    }\n  }\n};\n```\n\n## Architecture Pattern: Workers + Tunnel\n\nMost private network connectivity combines TCP Sockets with Cloudflare Tunnel:\n\n```\n┌─────────┐     ┌─────────────┐     ┌──────────────┐     ┌─────────────┐\n│ Worker  │────▶│ TCP Socket  │────▶│   Tunnel     │────▶│   Private   │\n│         │     │ (this API)  │     │ (cloudflared)│     │   Network   │\n└─────────┘     └─────────────┘     └──────────────┘     └─────────────┘\n```\n\n1. Worker opens TCP socket to Tunnel hostname\n2. Tunnel endpoint routes to private IP\n3. Response flows back through Tunnel to Worker\n\nSee [configuration.md](./configuration.md) for Tunnel setup details.\n\n## Reading Order\n\n1. **Start here (README.md)** - Overview and decision guide\n2. **[api.md](./api.md)** - Socket interface, types, methods\n3. **[configuration.md](./configuration.md)** - Wrangler setup, Tunnel integration\n4. **[patterns.md](./patterns.md)** - Real-world examples (databases, protocols, error handling)\n5. **[gotchas.md](./gotchas.md)** - Limits, blocked ports, common errors\n\n## Key Limits\n\n| Limit | Value |\n|-------|-------|\n| Max concurrent sockets per request | 6 |\n| Blocked destinations | Cloudflare IPs, localhost, port 25 |\n| Scope requirement | Must create in handler (not global) |\n\nSee [gotchas.md](./gotchas.md) for complete limits and troubleshooting.\n\n## Best Practices\n\n1. **Always close sockets** - Use try/finally blocks\n2. **Validate destinations** - Prevent SSRF by allowlisting hosts\n3. **Use Hyperdrive for databases** - Better performance than raw TCP\n4. **Prefer fetch() for HTTP** - Only use TCP when necessary\n5. **Combine with Smart Placement** - Reduce latency to private networks\n\n## Related Technologies\n\n- **[Hyperdrive](../hyperdrive/)** - PostgreSQL/MySQL with connection pooling\n- **[Cloudflare Tunnel](../tunnel/)** - Secure private network access\n- **[Smart Placement](../smart-placement/)** - Auto-locate Workers near backends\n- **VPC Services (beta)** - HTTP-only service bindings with SSRF protection (separate docs)\n\n## Reference\n\n- [TCP Sockets API Documentation](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/)\n- [Connect to databases guide](https://developers.cloudflare.com/workers/tutorials/connect-to-postgres/)\n- [Cloudflare Tunnel setup](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-vpc/api.md",
    "content": "# TCP Sockets API Reference\n\nComplete API reference for the Cloudflare Workers TCP Sockets API (`cloudflare:sockets`).\n\n## Core Function: `connect()`\n\n```typescript\nfunction connect(\n  address: SocketAddress,\n  options?: SocketOptions\n): Socket\n```\n\nCreates an outbound TCP connection to the specified address.\n\n### Parameters\n\n#### `SocketAddress`\n\n```typescript\ninterface SocketAddress {\n  hostname: string; // DNS hostname or IP address\n  port: number;     // TCP port (1-65535, excluding blocked ports)\n}\n```\n\n| Field | Type | Description | Example |\n|-------|------|-------------|---------|\n| `hostname` | `string` | Target hostname or IP | `\"db.internal.net\"`, `\"10.0.1.50\"` |\n| `port` | `number` | TCP port number | `5432`, `443`, `22` |\n\nDNS names are resolved at connection time. IPv4, IPv6, and private IPs (10.x, 172.16.x, 192.168.x) supported.\n\n#### `SocketOptions`\n\n```typescript\ninterface SocketOptions {\n  secureTransport?: \"off\" | \"on\" | \"starttls\";\n  allowHalfOpen?: boolean;\n}\n```\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `secureTransport` | `\"off\" \\| \"on\" \\| \"starttls\"` | `\"off\"` | TLS mode |\n| `allowHalfOpen` | `boolean` | `false` | Allow half-closed connections |\n\n**`secureTransport` modes:**\n\n| Mode | Behavior | Use Case |\n|------|----------|----------|\n| `\"off\"` | Plain TCP, no encryption | Testing, internal trusted networks |\n| `\"on\"` | Immediate TLS handshake | HTTPS, secure databases, SSH |\n| `\"starttls\"` | Start plain, upgrade later with `startTls()` | Postgres, SMTP, IMAP |\n\n**`allowHalfOpen`:** When `false` (default), closing read stream auto-closes write stream. When `true`, streams are independent.\n\n### Returns\n\nA `Socket` object with readable/writable streams.\n\n## Socket Interface\n\n```typescript\ninterface Socket {\n  // Streams\n  readable: ReadableStream<Uint8Array>;\n  writable: WritableStream<Uint8Array>;\n  \n  // Connection state\n  opened: Promise<SocketInfo>;\n  closed: Promise<void>;\n  \n  // Methods\n  close(): Promise<void>;\n  startTls(): Socket;\n}\n```\n\n### Properties\n\n#### `readable: ReadableStream<Uint8Array>`\n\nStream for reading data from the socket. Use `getReader()` to consume data.\n\n```typescript\nconst reader = socket.readable.getReader();\nconst { done, value } = await reader.read(); // Read one chunk\n```\n\n#### `writable: WritableStream<Uint8Array>`\n\nStream for writing data to the socket. Use `getWriter()` to send data.\n\n```typescript\nconst writer = socket.writable.getWriter();\nawait writer.write(new TextEncoder().encode(\"HELLO\\r\\n\"));\nawait writer.close();\n```\n\n#### `opened: Promise<SocketInfo>`\n\nPromise that resolves when connection succeeds, rejects on failure.\n\n```typescript\ninterface SocketInfo {\n  remoteAddress?: string; // May be undefined\n  localAddress?: string;  // May be undefined\n}\n\ntry {\n  const info = await socket.opened;\n} catch (error) {\n  // Connection failed\n}\n```\n\n#### `closed: Promise<void>`\n\nPromise that resolves when socket is fully closed (both directions).\n\n### Methods\n\n#### `close(): Promise<void>`\n\nCloses the socket gracefully, waiting for pending writes to complete.\n\n```typescript\nconst socket = connect({ hostname: \"api.internal\", port: 443 });\ntry {\n  // Use socket\n} finally {\n  await socket.close(); // Always call in finally block\n}\n```\n\n#### `startTls(): Socket`\n\nUpgrades connection to TLS. Only available when `secureTransport: \"starttls\"` was specified.\n\n```typescript\nconst socket = connect(\n  { hostname: \"db.internal\", port: 5432 },\n  { secureTransport: \"starttls\" }\n);\n\n// Send protocol-specific StartTLS command\nconst writer = socket.writable.getWriter();\nawait writer.write(new TextEncoder().encode(\"STARTTLS\\r\\n\"));\n\n// Upgrade to TLS - use returned socket, not original\nconst secureSocket = socket.startTls();\nconst secureWriter = secureSocket.writable.getWriter();\n```\n\n## Complete Example\n\n```typescript\nimport { connect } from 'cloudflare:sockets';\n\nexport default {\n  async fetch(req: Request): Promise<Response> {\n    const socket = connect({ hostname: \"echo.example.com\", port: 7 }, { secureTransport: \"on\" });\n\n    try {\n      await socket.opened;\n      \n      const writer = socket.writable.getWriter();\n      await writer.write(new TextEncoder().encode(\"Hello, TCP!\\n\"));\n      await writer.close();\n\n      const reader = socket.readable.getReader();\n      const { value } = await reader.read();\n      \n      return new Response(value);\n    } finally {\n      await socket.close();\n    }\n  }\n};\n```\n\nSee [patterns.md](./patterns.md) for multi-chunk reading, error handling, and protocol implementations.\n\n## Quick Reference\n\n| Task | Code |\n|------|------|\n| Import | `import { connect } from 'cloudflare:sockets';` |\n| Connect | `connect({ hostname: \"host\", port: 443 })` |\n| With TLS | `connect(addr, { secureTransport: \"on\" })` |\n| StartTLS | `socket.startTls()` after handshake |\n| Write | `await writer.write(data); await writer.close();` |\n| Read | `const { value } = await reader.read();` |\n| Error handling | `try { await socket.opened; } catch { }` |\n| Always close | `try { } finally { await socket.close(); }` |\n\n## See Also\n\n- [patterns.md](./patterns.md) - Real-world protocol implementations\n- [configuration.md](./configuration.md) - Wrangler setup and environment variables\n- [gotchas.md](./gotchas.md) - Limits and error handling\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-vpc/configuration.md",
    "content": "# Configuration\n\nSetup and configuration for TCP Sockets in Cloudflare Workers.\n\n## Wrangler Configuration\n\n### Basic Setup\n\nTCP Sockets are available by default in Workers runtime. No special configuration required in `wrangler.jsonc`:\n\n```jsonc\n{\n  \"name\": \"private-network-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\"\n}\n```\n\n### Environment Variables\n\nStore connection details as env vars:\n\n```jsonc\n{\n  \"vars\": { \"DB_HOST\": \"10.0.1.50\", \"DB_PORT\": \"5432\" }\n}\n```\n\n```typescript\ninterface Env { DB_HOST: string; DB_PORT: string; }\n\nexport default {\n  async fetch(req: Request, env: Env): Promise<Response> {\n    const socket = connect({ hostname: env.DB_HOST, port: parseInt(env.DB_PORT) });\n  }\n};\n```\n\n### Per-Environment Configuration\n\n```jsonc\n{\n  \"vars\": { \"DB_HOST\": \"localhost\" },\n  \"env\": {\n    \"staging\": { \"vars\": { \"DB_HOST\": \"staging-db.internal.net\" } },\n    \"production\": { \"vars\": { \"DB_HOST\": \"prod-db.internal.net\" } }\n  }\n}\n```\n\nDeploy: `wrangler deploy --env staging` or `wrangler deploy --env production`\n\n## Integration with Cloudflare Tunnel\n\nTo connect Workers to private networks, combine TCP Sockets with Cloudflare Tunnel:\n\n```\nWorker (TCP Socket) → Tunnel hostname → cloudflared → Private Network\n```\n\n### Quick Setup\n\n1. **Install cloudflared** on a server inside your private network\n2. **Create tunnel**: `cloudflared tunnel create my-private-network`\n3. **Configure routing** in `config.yml`:\n\n```yaml\ntunnel: <TUNNEL_ID>\ncredentials-file: /path/to/<TUNNEL_ID>.json\ningress:\n  - hostname: db.internal.example.com\n    service: tcp://10.0.1.50:5432\n  - service: http_status:404  # Required catch-all\n```\n\n4. **Run tunnel**: `cloudflared tunnel run my-private-network`\n5. **Connect from Worker**:\n\n```typescript\nconst socket = connect(\n  { hostname: \"db.internal.example.com\", port: 5432 },  // Tunnel hostname\n  { secureTransport: \"on\" }\n);\n```\n\nFor detailed Tunnel setup, see [Tunnel configuration reference](../tunnel/configuration.md).\n\n## Smart Placement Integration\n\nReduce latency by auto-placing Workers near backends:\n\n```jsonc\n{ \"placement\": { \"mode\": \"smart\" } }\n```\n\nWorkers automatically relocate closer to TCP socket destinations after observing connection latency. See [Smart Placement reference](../smart-placement/).\n\n## Secrets Management\n\nStore sensitive credentials as secrets (not in wrangler.jsonc):\n\n```bash\nwrangler secret put DB_PASSWORD  # Enter value when prompted\n```\n\nAccess in Worker via `env.DB_PASSWORD`. Use in protocol handshake or authentication.\n\n## Local Development\n\nTest with `wrangler dev`. Note: Local mode may not access private networks. Use public endpoints or mock servers for development:\n\n```typescript\nconst config = process.env.NODE_ENV === 'dev' \n  ? { hostname: 'localhost', port: 5432 }  // Mock\n  : { hostname: 'db.internal.example.com', port: 5432 };  // Production\n```\n\n## Connection String Patterns\n\nParse connection strings to extract host and port:\n\n```typescript\nfunction parseConnectionString(connStr: string): SocketAddress {\n  const url = new URL(connStr); // e.g., \"postgres://10.0.1.50:5432/mydb\"\n  return { hostname: url.hostname, port: parseInt(url.port) || 5432 };\n}\n```\n\n## Hyperdrive Integration\n\nFor PostgreSQL/MySQL, prefer Hyperdrive over raw TCP sockets (includes connection pooling):\n\n```jsonc\n{ \"hyperdrive\": [{ \"binding\": \"DB\", \"id\": \"<HYPERDRIVE_ID>\" }] }\n```\n\nSee [Hyperdrive reference](../hyperdrive/) for complete setup.\n\n## Compatibility\n\nTCP Sockets available in all modern Workers. Use current date: `\"compatibility_date\": \"2025-01-01\"`. No special flags required.\n\n## Related Configuration\n\n- **[Tunnel Configuration](../tunnel/configuration.md)** - Detailed cloudflared setup\n- **[Smart Placement](../smart-placement/configuration.md)** - Placement mode options\n- **[Hyperdrive](../hyperdrive/configuration.md)** - Database connection pooling setup\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-vpc/gotchas.md",
    "content": "# Gotchas and Troubleshooting\n\nCommon pitfalls, limitations, and solutions for TCP Sockets in Cloudflare Workers.\n\n## Platform Limits\n\n### Connection Limits\n\n| Limit | Value |\n|-------|-------|\n| Max concurrent sockets per request | 6 (hard limit) |\n| Socket lifetime | Request duration |\n| Connection timeout | Platform-dependent, no setting |\n\n**Problem:** Exceeding 6 connections throws error\n\n**Solution:** Process in batches of 6\n\n```typescript\nfor (let i = 0; i < hosts.length; i += 6) {\n  const batch = hosts.slice(i, i + 6).map(h => connect({ hostname: h, port: 443 }));\n  await Promise.all(batch.map(async s => { /* use */ await s.close(); }));\n}\n```\n\n### Blocked Destinations\n\nCloudflare IPs (1.1.1.1), localhost (127.0.0.1), port 25 (SMTP), Worker's own URL blocked for security.\n\n**Solution:** Use public IPs or Tunnel hostnames: `connect({ hostname: \"db.internal.company.net\", port: 5432 })`\n\n### Scope Requirements\n\n**Problem:** Sockets created in global scope fail\n\n**Cause:** Sockets tied to request lifecycle\n\n**Solution:** Create inside handler: `export default { async fetch() { const socket = connect(...); } }`\n\n## Common Errors\n\n### Error: \"proxy request failed\"\n\n**Causes:** Blocked destination (Cloudflare IP, localhost, port 25), DNS failure, network unreachable\n\n**Solution:** Validate destinations, use Tunnel hostnames, catch errors with try/catch\n\n### Error: \"TCP Loop detected\"\n\n**Cause:** Worker connecting to itself\n\n**Solution:** Connect to external service, not Worker's own hostname\n\n### Error: \"Port 25 prohibited\"\n\n**Cause:** SMTP port blocked\n\n**Solution:** Use Email Workers API for email\n\n### Error: \"socket is not open\"\n\n**Cause:** Read/write after close\n\n**Solution:** Always use try/finally to ensure proper closure order\n\n### Error: Connection timeout\n\n**Cause:** No built-in timeout\n\n**Solution:** Use `Promise.race()`:\n\n```typescript\nconst socket = connect(addr, opts);\nconst timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000));\nawait Promise.race([socket.opened, timeout]);\n```\n\n## TLS/SSL Issues\n\n### StartTLS Timing\n\n**Problem:** Calling `startTls()` too early\n\n**Solution:** Send protocol-specific STARTTLS command, wait for server OK, then call `socket.startTls()`\n\n### Certificate Validation\n\n**Problem:** Self-signed certs fail\n\n**Solution:** Use proper certs or Tunnel (handles TLS termination)\n\n## Performance Issues\n\n### Not Using Connection Pooling\n\n**Problem:** New connection overhead per request\n\n**Solution:** Use [Hyperdrive](../hyperdrive/) for databases (built-in pooling)\n\n### Not Using Smart Placement\n\n**Problem:** High latency to backend\n\n**Solution:** Enable: `{ \"placement\": { \"mode\": \"smart\" } }` in wrangler.jsonc\n\n### Forgetting to Close Sockets\n\n**Problem:** Resource leaks\n\n**Solution:** Always use try/finally:\n\n```typescript\nconst socket = connect({ hostname: \"api.internal\", port: 443 });\ntry {\n  // Use socket\n} finally {\n  await socket.close();\n}\n```\n\n## Data Handling Issues\n\n### Assuming Single Read Gets All Data\n\n**Problem:** Only reading once may miss chunked data\n\n**Solution:** Loop `reader.read()` until `done === true` (see patterns.md)\n\n### Text Encoding Issues\n\n**Problem:** Using wrong encoding\n\n**Solution:** Specify encoding: `new TextDecoder('iso-8859-1').decode(data)`\n\n## Security Issues\n\n### SSRF Vulnerability\n\n**Problem:** User-controlled destinations allow access to internal services\n\n**Solution:** Validate against strict allowlist:\n\n```typescript\nconst ALLOWED = ['api1.internal.net', 'api2.internal.net'];\nconst host = new URL(req.url).searchParams.get('host');\nif (!host || !ALLOWED.includes(host)) return new Response('Forbidden', { status: 403 });\n```\n\n## When to Use Alternatives\n\n| Use Case | Alternative | Reason |\n|----------|-------------|--------|\n| PostgreSQL/MySQL | [Hyperdrive](../hyperdrive/) | Connection pooling, caching |\n| HTTP/HTTPS | `fetch()` | Simpler, built-in |\n| HTTP with SSRF protection | VPC Services (beta 2025+) | Declarative bindings |\n\n## Debugging Tips\n\n1. **Log connection details:** `const info = await socket.opened; console.log(info.remoteAddress);`\n2. **Test with public services first:** Use tcpbin.com:4242 echo server\n3. **Verify Tunnel:** `cloudflared tunnel info <name>` and `cloudflared tunnel route ip list`\n\n## Related\n\n- [Hyperdrive](../hyperdrive/) - Database connections\n- [Smart Placement](../smart-placement/) - Latency optimization\n- [Tunnel Troubleshooting](../tunnel/gotchas.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workers-vpc/patterns.md",
    "content": "# Common Patterns\n\nReal-world patterns and examples for TCP Sockets in Cloudflare Workers.\n\n```typescript\nimport { connect } from 'cloudflare:sockets';\n```\n\n## Basic Patterns\n\n### Simple Request-Response\n\n```typescript\nconst socket = connect({ hostname: \"echo.example.com\", port: 7 }, { secureTransport: \"on\" });\ntry {\n  await socket.opened;\n  const writer = socket.writable.getWriter();\n  await writer.write(new TextEncoder().encode(\"Hello\\n\"));\n  await writer.close();\n  \n  const reader = socket.readable.getReader();\n  const { value } = await reader.read();\n  return new Response(value);\n} finally {\n  await socket.close();\n}\n```\n\n### Reading All Data\n\n```typescript\nasync function readAll(socket: Socket): Promise<Uint8Array> {\n  const reader = socket.readable.getReader();\n  const chunks: Uint8Array[] = [];\n  while (true) {\n    const { done, value } = await reader.read();\n    if (done) break;\n    chunks.push(value);\n  }\n  const total = chunks.reduce((sum, c) => sum + c.length, 0);\n  const result = new Uint8Array(total);\n  let offset = 0;\n  for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; }\n  return result;\n}\n```\n\n### Streaming Response\n\n```typescript\n// Stream socket data directly to HTTP response\nconst socket = connect({ hostname: \"stream.internal\", port: 9000 }, { secureTransport: \"on\" });\nconst writer = socket.writable.getWriter();\nawait writer.write(new TextEncoder().encode(\"STREAM\\n\"));\nawait writer.close();\nreturn new Response(socket.readable);\n```\n\n## Protocol Examples\n\n### Redis RESP\n\n```typescript\n// Send: *2\\r\\n$3\\r\\nGET\\r\\n$<keylen>\\r\\n<key>\\r\\n\n// Recv: $<len>\\r\\n<data>\\r\\n or $-1\\r\\n for null\nconst socket = connect({ hostname: \"redis.internal\", port: 6379 });\nconst writer = socket.writable.getWriter();\nawait writer.write(new TextEncoder().encode(`*2\\r\\n$3\\r\\nGET\\r\\n$3\\r\\nkey\\r\\n`));\n```\n\n### PostgreSQL\n\n**Use [Hyperdrive](../hyperdrive/) for production.** Raw Postgres protocol is complex (startup, auth, query messages).\n\n### MQTT\n\n```typescript\nconst socket = connect({ hostname: \"mqtt.broker\", port: 1883 });\nconst writer = socket.writable.getWriter();\n// CONNECT: 0x10 <len> 0x00 0x04 \"MQTT\" 0x04 <flags> ...\n// PUBLISH: 0x30 <len> <topic_len> <topic> <message>\n```\n\n## Error Handling Patterns\n\n### Retry with Backoff\n\n```typescript\nasync function connectWithRetry(addr: SocketAddress, opts: SocketOptions, maxRetries = 3): Promise<Socket> {\n  for (let i = 1; i <= maxRetries; i++) {\n    try {\n      const socket = connect(addr, opts);\n      await socket.opened;\n      return socket;\n    } catch (error) {\n      if (i === maxRetries) throw error;\n      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i - 1))); // Exponential backoff\n    }\n  }\n  throw new Error('Unreachable');\n}\n```\n\n### Timeout\n\n```typescript\nasync function connectWithTimeout(addr: SocketAddress, opts: SocketOptions, ms = 5000): Promise<Socket> {\n  const socket = connect(addr, opts);\n  const timeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms));\n  await Promise.race([socket.opened, timeout]);\n  return socket;\n}\n```\n\n### Fallback\n\n```typescript\nasync function connectWithFallback(primary: string, fallback: string, port: number): Promise<Socket> {\n  try {\n    const socket = connect({ hostname: primary, port }, { secureTransport: \"on\" });\n    await socket.opened;\n    return socket;\n  } catch {\n    return connect({ hostname: fallback, port }, { secureTransport: \"on\" });\n  }\n}\n```\n\n## Security Patterns\n\n### Destination Allowlist (Prevent SSRF)\n\n```typescript\nconst ALLOWED_HOSTS = ['db.internal.company.net', 'api.internal.company.net', /^10\\.0\\.1\\.\\d+$/];\n\nfunction isAllowed(hostname: string): boolean {\n  return ALLOWED_HOSTS.some(p => p instanceof RegExp ? p.test(hostname) : p === hostname);\n}\n\nexport default {\n  async fetch(req: Request): Promise<Response> {\n    const target = new URL(req.url).searchParams.get('host');\n    if (!target || !isAllowed(target)) return new Response('Forbidden', { status: 403 });\n    const socket = connect({ hostname: target, port: 443 });\n    // Use socket...\n  }\n};\n```\n\n### Connection Pooling\n\n```typescript\nclass SocketPool {\n  private pool = new Map<string, Socket[]>();\n  \n  async acquire(hostname: string, port: number): Promise<Socket> {\n    const key = `${hostname}:${port}`;\n    const sockets = this.pool.get(key) || [];\n    if (sockets.length > 0) return sockets.pop()!;\n    const socket = connect({ hostname, port }, { secureTransport: \"on\" });\n    await socket.opened;\n    return socket;\n  }\n  \n  release(hostname: string, port: number, socket: Socket): void {\n    const key = `${hostname}:${port}`;\n    const sockets = this.pool.get(key) || [];\n    if (sockets.length < 3) { sockets.push(socket); this.pool.set(key, sockets); }\n    else socket.close();\n  }\n}\n```\n\n## Multi-Protocol Gateway\n\n```typescript\ninterface Protocol { name: string; defaultPort: number; test(host: string, port: number): Promise<string>; }\n\nconst PROTOCOLS: Record<string, Protocol> = {\n  redis: {\n    name: 'redis',\n    defaultPort: 6379,\n    async test(host, port) {\n      const socket = connect({ hostname: host, port });\n      try {\n        const writer = socket.writable.getWriter();\n        await writer.write(new TextEncoder().encode('*1\\r\\n$4\\r\\nPING\\r\\n'));\n        writer.releaseLock();\n        const reader = socket.readable.getReader();\n        const { value } = await reader.read();\n        return new TextDecoder().decode(value || new Uint8Array());\n      } finally { await socket.close(); }\n    }\n  }\n};\n\nexport default {\n  async fetch(req: Request): Promise<Response> {\n    const url = new URL(req.url);\n    const proto = url.pathname.slice(1);  // /redis\n    const host = url.searchParams.get('host');\n    if (!host || !PROTOCOLS[proto]) return new Response('Invalid', { status: 400 });\n    const result = await PROTOCOLS[proto].test(host, parseInt(url.searchParams.get('port') || '') || PROTOCOLS[proto].defaultPort);\n    return new Response(result);\n  }\n};\n```\n\n\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workflows/README.md",
    "content": "# Cloudflare Workflows\n\nDurable multi-step applications with automatic retries, state persistence, and long-running execution.\n\n## What It Does\n\n- Chain steps with automatic retry logic\n- Persist state between steps (minutes → weeks)\n- Handle failures without losing progress\n- Wait for external events/approvals\n- Sleep without consuming resources\n\n**Available:** Free & Paid Workers plans\n\n## Core Concepts\n\n**Workflow**: Class extending `WorkflowEntrypoint` with `run` method\n**Instance**: Single execution with unique ID & independent state\n**Steps**: Independently retriable units via `step.do()` - API calls, DB queries, AI invocations\n**State**: Persisted from step returns; step name = cache key\n\n## Quick Start\n\n```typescript\nimport { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';\n\ntype Env = { MY_WORKFLOW: Workflow; DB: D1Database };\ntype Params = { userId: string };\n\nexport class MyWorkflow extends WorkflowEntrypoint<Env, Params> {\n  async run(event: WorkflowEvent<Params>, step: WorkflowStep) {\n    const user = await step.do('fetch user', async () => {\n      return await this.env.DB.prepare('SELECT * FROM users WHERE id = ?')\n        .bind(event.params.userId).first();\n    });\n    \n    await step.sleep('wait 7 days', '7 days');\n    \n    await step.do('send reminder', async () => {\n      await sendEmail(user.email, 'Reminder!');\n    });\n  }\n}\n```\n\n## Key Features\n\n- **Durability**: Failed steps don't re-run successful ones\n- **Retries**: Configurable backoff (constant/linear/exponential)\n- **Events**: `waitForEvent()` for webhooks/approvals (timeout: 1h → 365d)\n- **Sleep**: `sleep()` / `sleepUntil()` for scheduling (max 365d)\n- **Parallel**: `Promise.all()` for concurrent steps\n- **Idempotency**: Check-then-execute patterns\n\n## Reading Order\n\n**Getting Started:** configuration.md → api.md → patterns.md  \n**Troubleshooting:** gotchas.md\n\n## In This Reference\n- [configuration.md](./configuration.md) - wrangler.jsonc setup, step config, bindings\n- [api.md](./api.md) - Step APIs, instance management, sleep/parameters\n- [patterns.md](./patterns.md) - Common workflows, testing, orchestration\n- [gotchas.md](./gotchas.md) - Timeouts, limits, debugging strategies\n\n## See Also\n- [durable-objects](../durable-objects/) - Alternative stateful approach\n- [queues](../queues/) - Message-driven workflows\n- [workers](../workers/) - Entry point for workflow instances\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workflows/api.md",
    "content": "# Workflow APIs\n\n## Step APIs\n\n```typescript\n// step.do()\nconst result = await step.do('step name', async () => { /* logic */ });\nconst result = await step.do('step name', { retries, timeout }, async () => {});\n\n// step.sleep()\nawait step.sleep('description', '1 hour');\nawait step.sleep('description', 5000); // ms\n\n// step.sleepUntil()\nawait step.sleepUntil('description', Date.parse('2024-12-31'));\n\n// step.waitForEvent()\nconst data = await step.waitForEvent<PayloadType>('wait', {event: 'webhook-type', timeout: '24h'}); // Default 24h, max 365d\ntry { const event = await step.waitForEvent('wait', { event: 'approval', timeout: '1h' }); } catch (e) { /* Timeout */ }\n```\n\n## Instance Management\n\n```typescript\n// Create single\nconst instance = await env.MY_WORKFLOW.create({id: crypto.randomUUID(), params: { userId: 'user123' }}); // id optional, auto-generated if omitted\n\n// Create with custom retention (default: 3 days free, 30 days paid)\nconst instance = await env.MY_WORKFLOW.create({\n  id: crypto.randomUUID(),\n  params: { userId: 'user123' },\n  retention: '30 days'  // Override default retention period\n});\n\n// Batch (max 100, idempotent: skips existing IDs)\nconst instances = await env.MY_WORKFLOW.createBatch([{id: 'user1', params: {name: 'John'}}, {id: 'user2', params: {name: 'Jane'}}]);\n\n// Get & Status\nconst instance = await env.MY_WORKFLOW.get('instance-id');\nconst status = await instance.status(); // {status: 'queued' | 'running' | 'paused' | 'errored' | 'terminated' | 'complete' | 'waiting' | 'waitingForPause' | 'unknown', error?, output?}\n\n// Control\nawait instance.pause(); await instance.resume(); await instance.terminate(); await instance.restart();\n\n// Send Events\nawait instance.sendEvent({type: 'approval', payload: { approved: true }}); // Must match waitForEvent type\n```\n\n## Triggering Workflows\n\n```typescript\n// From Worker\nexport default { async fetch(req, env) { const instance = await env.MY_WORKFLOW.create({id: crypto.randomUUID(), params: { userId: 'user123' }}); return Response.json({ id: instance.id }); }};\n\n// From Queue\nexport default { async queue(batch, env) { for (const msg of batch.messages) { await env.MY_WORKFLOW.create({id: `job-${msg.id}`, params: msg.body}); } }};\n\n// From Cron\nexport default { async scheduled(event, env) { await env.CLEANUP_WORKFLOW.create({id: `cleanup-${Date.now()}`, params: { timestamp: event.scheduledTime }}); }};\n\n// From Another Workflow (non-blocking)\nexport class ParentWorkflow extends WorkflowEntrypoint<Env, Params> {\n  async run(event, step) {\n    const child = await step.do('start child', async () => await this.env.CHILD_WORKFLOW.create({id: `child-${event.instanceId}`, params: {}}));\n  }\n}\n```\n\n## Error Handling\n\n```typescript\nimport { NonRetryableError } from 'cloudflare:workers';\n\n// NonRetryableError\nawait step.do('validate', async () => {\n  if (!event.params.paymentMethod) throw new NonRetryableError('Payment method required');\n  const res = await fetch('https://api.example.com/charge', { method: 'POST' });\n  if (res.status === 401) throw new NonRetryableError('Invalid credentials'); // Don't retry\n  if (!res.ok) throw new Error('Retryable failure'); // Will retry\n  return res.json();\n});\n\n// Catching Errors\ntry { await step.do('risky op', async () => { throw new NonRetryableError('Failed'); }); } catch (e) { await step.do('cleanup', async () => {}); }\n\n// Idempotency\nawait step.do('charge', async () => {\n  const sub = await fetch(`https://api/subscriptions/${id}`).then(r => r.json());\n  if (sub.charged) return sub; // Already done\n  return await fetch(`https://api/subscriptions/${id}`, {method: 'POST', body: JSON.stringify({ amount: 10.0 })}).then(r => r.json());\n});\n```\n\n## Type Constraints\n\nParams and step returns must be `Rpc.Serializable<T>`:\n\n```typescript\n// ✅ Valid types\ntype ValidParams = {\n  userId: string;\n  count: number;\n  tags: string[];\n  metadata: Record<string, unknown>;\n};\n\n// ❌ Invalid types\ntype InvalidParams = {\n  callback: () => void;      // Functions not serializable\n  symbol: symbol;            // Symbols not serializable\n  circular: any;             // Circular references not allowed\n};\n\n// Step returns follow same rules\nconst result = await step.do('fetch', async () => {\n  return { userId: '123', data: [1, 2, 3] }; // ✅ Plain object\n});\n```\n\n## Sleep & Scheduling\n\n```typescript\n// Relative\nawait step.sleep('wait 1 hour', '1 hour');\nawait step.sleep('wait 30 days', '30 days');\nawait step.sleep('wait 5s', 5000); // ms\n\n// Absolute\nawait step.sleepUntil('launch date', Date.parse('24 Oct 2024 13:00:00 UTC'));\nawait step.sleepUntil('deadline', new Date('2024-12-31T23:59:59Z'));\n```\n\nUnits: second, minute, hour, day, week, month, year. Max: 365 days.\nSleeping instances don't count toward concurrency.\n\n## Parameters\n\n**Pass from Worker:**\n```typescript\nconst instance = await env.MY_WORKFLOW.create({\n  id: crypto.randomUUID(),\n  params: { userId: 'user123', email: 'user@example.com' }\n});\n```\n\n**Access in Workflow:**\n```typescript\nasync run(event: WorkflowEvent<Params>, step: WorkflowStep) {\n  const userId = event.params.userId;\n  const instanceId = event.instanceId;\n  const createdAt = event.timestamp;\n}\n```\n\n**CLI Trigger:**\n```bash\nnpx wrangler workflows trigger my-workflow '{\"userId\":\"user123\"}'\n```\n\n## Wrangler CLI\n\n```bash\nnpm create cloudflare@latest my-workflow -- --template \"cloudflare/workflows-starter\"\nnpx wrangler deploy\nnpx wrangler workflows list\nnpx wrangler workflows trigger my-workflow '{\"userId\":\"user123\"}'\nnpx wrangler workflows instances list my-workflow\nnpx wrangler workflows instances describe my-workflow instance-id\nnpx wrangler workflows instances pause/resume/terminate my-workflow instance-id\n```\n\n## REST API\n\n```bash\n# Create\ncurl -X POST \"https://api.cloudflare.com/client/v4/accounts/{account_id}/workflows/{workflow_name}/instances\" -H \"Authorization: Bearer {token}\" -d '{\"id\":\"custom-id\",\"params\":{\"userId\":\"user123\"}}'\n\n# Status\ncurl \"https://api.cloudflare.com/client/v4/accounts/{account_id}/workflows/{workflow_name}/instances/{instance_id}/status\" -H \"Authorization: Bearer {token}\"\n\n# Send Event\ncurl -X POST \"https://api.cloudflare.com/client/v4/accounts/{account_id}/workflows/{workflow_name}/instances/{instance_id}/events\" -H \"Authorization: Bearer {token}\" -d '{\"type\":\"approval\",\"payload\":{\"approved\":true}}'\n```\n\nSee: [configuration.md](./configuration.md), [patterns.md](./patterns.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workflows/configuration.md",
    "content": "# Workflow Configuration\n\n## wrangler.jsonc Setup\n\n```jsonc\n{\n  \"name\": \"my-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\",  // Use current date for new projects\n  \"observability\": {\n    \"enabled\": true  // Enables Workflows dashboard + structured logs\n  },\n  \"workflows\": [\n    {\n      \"name\": \"my-workflow\",           // Workflow name\n      \"binding\": \"MY_WORKFLOW\",        // Env binding\n      \"class_name\": \"MyWorkflow\"      // TS class name\n      // \"script_name\": \"other-worker\" // For cross-script calls\n    }\n  ],\n  \"limits\": {\n    \"cpu_ms\": 300000  // 5 min max (default 30s)\n  }\n}\n```\n\n## Step Configuration\n\n```typescript\n// Basic step\nconst data = await step.do('step name', async () => ({ result: 'value' }));\n\n// With retry config\nawait step.do('api call', {\n  retries: {\n    limit: 10,              // Default: 5, or Infinity\n    delay: '10 seconds',    // Default: 10000ms\n    backoff: 'exponential'  // constant | linear | exponential\n  },\n  timeout: '30 minutes'     // Per-attempt timeout (default: 10min)\n}, async () => {\n  const res = await fetch('https://api.example.com/data');\n  if (!res.ok) throw new Error('Failed');\n  return res.json();\n});\n```\n\n### Parallel Steps\n```typescript\nconst [user, settings] = await Promise.all([\n  step.do('fetch user', async () => this.env.KV.get(`user:${id}`)),\n  step.do('fetch settings', async () => this.env.KV.get(`settings:${id}`))\n]);\n```\n\n### Conditional Steps\n```typescript\nconst config = await step.do('fetch config', async () => \n  this.env.KV.get('flags', { type: 'json' })\n);\n\n// ✅ Deterministic (based on step output)\nif (config.enableEmail) {\n  await step.do('send email', async () => sendEmail());\n}\n\n// ❌ Non-deterministic (Date.now outside step)\nif (Date.now() > deadline) { /* BAD */ }\n```\n\n### Dynamic Steps (Loops)\n```typescript\nconst files = await step.do('list files', async () => \n  this.env.BUCKET.list()\n);\n\nfor (const file of files.objects) {\n  await step.do(`process ${file.key}`, async () => {\n    const obj = await this.env.BUCKET.get(file.key);\n    return processData(await obj.arrayBuffer());\n  });\n}\n```\n\n## Multiple Workflows\n\n```jsonc\n{\n  \"workflows\": [\n    {\"name\": \"user-onboarding\", \"binding\": \"USER_ONBOARDING\", \"class_name\": \"UserOnboarding\"},\n    {\"name\": \"data-processing\", \"binding\": \"DATA_PROCESSING\", \"class_name\": \"DataProcessing\"}\n  ]\n}\n```\n\nEach class extends `WorkflowEntrypoint` with its own `Params` type.\n\n## Cross-Script Bindings\n\nWorker A defines workflow. Worker B calls it by adding `script_name`:\n\n```jsonc\n// Worker B (caller)\n{\n  \"workflows\": [{\n    \"name\": \"billing-workflow\",\n    \"binding\": \"BILLING\",\n    \"script_name\": \"billing-worker\"  // Points to Worker A\n  }]\n}\n```\n\n## Bindings\n\nWorkflows access Cloudflare bindings via `this.env`:\n\n```typescript\ntype Env = {\n  MY_WORKFLOW: Workflow;\n  KV: KVNamespace;\n  DB: D1Database;\n  BUCKET: R2Bucket;\n  AI: Ai;\n  VECTORIZE: VectorizeIndex;\n};\n\nawait step.do('use bindings', async () => {\n  const kv = await this.env.KV.get('key');\n  const db = await this.env.DB.prepare('SELECT * FROM users').first();\n  const file = await this.env.BUCKET.get('file.txt');\n  const ai = await this.env.AI.run('@cf/meta/llama-2-7b-chat-int8', { prompt: 'Hi' });\n});\n```\n\n## Pages Functions Binding\n\nPages Functions can trigger Workflows via service bindings:\n\n```typescript\n// functions/_middleware.ts\nexport const onRequest: PagesFunction<Env> = async ({ env, request }) => {\n  const instance = await env.MY_WORKFLOW.create({\n    params: { url: request.url }\n  });\n  return new Response(`Started ${instance.id}`);\n};\n```\n\nConfigure in wrangler.jsonc under `service_bindings`.\n\nSee: [api.md](./api.md), [patterns.md](./patterns.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workflows/gotchas.md",
    "content": "# Gotchas & Debugging\n\n## Common Errors\n\n### \"Step Timeout\"\n\n**Cause:** Step execution exceeding 10 minute default timeout or configured timeout  \n**Solution:** Set custom timeout with `step.do('long operation', {timeout: '30 minutes'}, async () => {...})` or increase CPU limit in wrangler.jsonc (max 5min CPU time)\n\n### \"waitForEvent Timeout\"\n\n**Cause:** Event not received within timeout period (default 24h, max 365d)  \n**Solution:** Wrap in try-catch to handle timeout gracefully and proceed with default behavior\n\n### \"Non-Deterministic Step Names\"\n\n**Cause:** Using dynamic values like `Date.now()` in step names causes replay issues  \n**Solution:** Use deterministic values like `event.instanceId` for step names\n\n### \"State Lost in Variables\"\n\n**Cause:** Using module-level or local variables to store state which is lost on hibernation  \n**Solution:** Return values from `step.do()` which are automatically persisted: `const total = await step.do('step 1', async () => 10)`\n\n### \"Non-Deterministic Conditionals\"\n\n**Cause:** Using non-deterministic logic (like `Date.now()`) outside steps in conditionals  \n**Solution:** Move non-deterministic operations inside steps: `const isLate = await step.do('check', async () => Date.now() > deadline)`\n\n### \"Large Step Returns Exceeding Limit\"\n\n**Cause:** Returning data >1 MiB from step  \n**Solution:** Store large data in R2 and return only reference: `{ key: 'r2-object-key' }`\n\n### \"Step Exceeded CPU Limit But Ran for < 30s\"\n\n**Cause:** Confusion between CPU time (active compute) and wall-clock time (includes I/O waits)  \n**Solution:** Network requests, database queries, and sleeps don't count toward CPU. 30s limit = 30s of active processing\n\n### \"Idempotency Violation\"\n\n**Cause:** Step operations not idempotent, causing duplicate charges or actions on retry  \n**Solution:** Check if operation already completed before executing (e.g., check if customer already charged)\n\n### \"Instance ID Collision\"\n\n**Cause:** Reusing instance IDs causing conflicts  \n**Solution:** Use unique IDs with timestamp: `await env.MY_WORKFLOW.create({ id: \\`${userId}-${Date.now()}\\`, params: {} })`\n\n### \"Instance Data Disappeared After Completion\"\n\n**Cause:** Completed/errored instances are automatically deleted after retention period (3 days free / 30 days paid)  \n**Solution:** Export critical data to KV/R2/D1 before workflow completes\n\n### \"Missing await on step.do\"\n\n**Cause:** Forgetting to await step.do() causing fire-and-forget behavior  \n**Solution:** Always await step operations: `await step.do('task', ...)`\n\n## Limits\n\n| Limit | Free | Paid | Notes |\n|-------|------|------|-------|\n| CPU per step | 10ms | 30s (default), 5min (max) | Set via `limits.cpu_ms` in wrangler.jsonc |\n| Step state | 1 MiB | 1 MiB | Per step return value |\n| Instance state | 100 MB | 1 GB | Total state per workflow instance |\n| Steps per workflow | 1,024 | 1,024 | `step.sleep()` doesn't count |\n| Executions per day | 100k | Unlimited | Daily execution limit |\n| Concurrent instances | 25 | 10k | Maximum concurrent workflows; waiting state excluded |\n| Queued instances | 100k | 1M | Maximum queued workflow instances |\n| Subrequests per step | 50 | 1,000 | Maximum outbound requests per step |\n| State retention | 3 days | 30 days | How long completed instances kept |\n| Step timeout default | 10 min | 10 min | Per attempt |\n| waitForEvent timeout default | 24h | 24h | Maximum 365 days |\n| waitForEvent timeout max | 365 days | 365 days | Maximum wait time |\n\n**Note:** Instances in `waiting` state (from `step.sleep` or `step.waitForEvent`) don't count toward concurrent instance limit, allowing millions of sleeping workflows.\n\n## Pricing\n\n| Metric | Free | Paid | Notes |\n|--------|------|------|-------|\n| Requests | 100k/day | 10M/mo + $0.30/M | Workflow invocations |\n| CPU time | 10ms/invoke | 30M CPU-ms/mo + $0.02/M CPU-ms | Actual CPU usage |\n| Storage | 1 GB | 1 GB/mo + $0.20/GB-mo | All instances (running/errored/sleeping/completed) |\n\n## References\n\n- [Official Docs](https://developers.cloudflare.com/workflows/)\n- [Get Started Guide](https://developers.cloudflare.com/workflows/get-started/guide/)\n- [Workers API](https://developers.cloudflare.com/workflows/build/workers-api/)\n- [REST API](https://developers.cloudflare.com/api/resources/workflows/)\n- [Examples](https://developers.cloudflare.com/workflows/examples/)\n- [Limits](https://developers.cloudflare.com/workflows/reference/limits/)\n- [Pricing](https://developers.cloudflare.com/workflows/reference/pricing/)\n\nSee: [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/workflows/patterns.md",
    "content": "# Workflow Patterns\n\n## Image Processing Pipeline\n\n```typescript\nexport class ImageProcessingWorkflow extends WorkflowEntrypoint<Env, Params> {\n  async run(event, step) {\n    const imageData = await step.do('fetch', async () => (await this.env.BUCKET.get(event.params.imageKey)).arrayBuffer());\n    const description = await step.do('generate description', async () => \n      await this.env.AI.run('@cf/llava-hf/llava-1.5-7b-hf', {image: Array.from(new Uint8Array(imageData)), prompt: 'Describe this image', max_tokens: 50})\n    );\n    await step.waitForEvent('await approval', { event: 'approved', timeout: '24h' });\n    await step.do('publish', async () => await this.env.BUCKET.put(`public/${event.params.imageKey}`, imageData));\n  }\n}\n```\n\n## User Lifecycle\n\n```typescript\nexport class UserLifecycleWorkflow extends WorkflowEntrypoint<Env, Params> {\n  async run(event, step) {\n    await step.do('welcome email', async () => await sendEmail(event.params.email, 'Welcome!'));\n    await step.sleep('trial period', '7 days');\n    const hasConverted = await step.do('check conversion', async () => {\n      const user = await this.env.DB.prepare('SELECT subscription_status FROM users WHERE id = ?').bind(event.params.userId).first();\n      return user.subscription_status === 'active';\n    });\n    if (!hasConverted) await step.do('trial expiration email', async () => await sendEmail(event.params.email, 'Trial ending'));\n  }\n}\n```\n\n## Data Pipeline\n\n```typescript\nexport class DataPipelineWorkflow extends WorkflowEntrypoint<Env, Params> {\n  async run(event, step) {\n    const rawData = await step.do('extract', {retries: { limit: 10, delay: '30s', backoff: 'exponential' }}, async () => {\n      const res = await fetch(event.params.sourceUrl);\n      if (!res.ok) throw new Error('Fetch failed');\n      return res.json();\n    });\n    const transformed = await step.do('transform', async () => \n      rawData.map(item => ({ id: item.id, normalized: normalizeData(item) }))\n    );\n    const dataRef = await step.do('store', async () => {\n      const key = `processed/${Date.now()}.json`;\n      await this.env.BUCKET.put(key, JSON.stringify(transformed));\n      return { key };\n    });\n    await step.do('load', async () => {\n      const data = await (await this.env.BUCKET.get(dataRef.key)).json();\n      for (let i = 0; i < data.length; i += 100) {\n        await this.env.DB.batch(data.slice(i, i + 100).map(item => \n          this.env.DB.prepare('INSERT INTO records VALUES (?, ?)').bind(item.id, item.normalized)\n        ));\n      }\n    });\n  }\n}\n```\n\n## Human-in-the-Loop Approval\n\n```typescript\nexport class ApprovalWorkflow extends WorkflowEntrypoint<Env, Params> {\n  async run(event, step) {\n    await step.do('create approval', async () => await this.env.DB.prepare('INSERT INTO approvals (id, user_id, status) VALUES (?, ?, ?)').bind(event.instanceId, event.params.userId, 'pending').run());\n    try {\n      const approval = await step.waitForEvent<{ approved: boolean }>('wait for approval', { event: 'approval-response', timeout: '48h' });\n      if (approval.approved) { await step.do('process approval', async () => {}); } \n      else { await step.do('handle rejection', async () => {}); }\n    } catch (e) {\n      await step.do('auto reject', async () => await this.env.DB.prepare('UPDATE approvals SET status = ? WHERE id = ?').bind('auto-rejected', event.instanceId).run());\n    }\n  }\n}\n```\n\n## Testing Workflows\n\n### Setup\n\n```typescript\n// vitest.config.ts\nimport { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';\n\nexport default defineWorkersConfig({\n  test: {\n    poolOptions: {\n      workers: {\n        wrangler: { configPath: './wrangler.jsonc' }\n      }\n    }\n  }\n});\n```\n\n### Introspection API\n\n```typescript\nimport { introspectWorkflowInstance } from 'cloudflare:test';\n\nconst instance = await env.MY_WORKFLOW.create({ params: { userId: '123' } });\nconst introspector = await introspectWorkflowInstance(env.MY_WORKFLOW, instance.id);\n\n// Wait for step completion\nconst result = await introspector.waitForStepResult({ name: 'fetch user', index: 0 });\n\n// Mock step behavior\nawait introspector.modify(async (m) => {\n  await m.mockStepResult({ name: 'api call' }, { mocked: true });\n});\n```\n\n## Best Practices\n\n### ✅ DO\n\n1. **Granular steps**: One API call per step (unless proving idempotency)\n2. **Idempotency**: Check-then-execute; use idempotency keys\n3. **Deterministic names**: Use static or step-output-based names\n4. **Return state**: Persist via step returns, not variables\n5. **Always await**: `await step.do()`, avoid dangling promises\n6. **Deterministic conditionals**: Base on `event.payload` or step outputs\n7. **Store large data externally**: R2/KV for >1 MiB, return refs\n8. **Batch creation**: `createBatch()` for multiple instances\n\n### ❌ DON'T\n\n1. **One giant step**: Breaks durability & retry control\n2. **State outside steps**: Lost on hibernation\n3. **Mutate events**: Events immutable, return new state\n4. **Non-deterministic logic outside steps**: `Math.random()`, `Date.now()` must be in steps\n5. **Side effects outside steps**: May duplicate on restart\n6. **Non-deterministic step names**: Prevents caching\n7. **Ignore timeouts**: `waitForEvent` throws, use try-catch\n8. **Reuse instance IDs**: Must be unique within retention\n\n## Orchestration Patterns\n\n### Fan-Out (Parallel Processing)\n```typescript\nconst files = await step.do('list', async () => this.env.BUCKET.list());\nawait Promise.all(files.objects.map((file, i) => step.do(`process ${i}`, async () => processFile(await (await this.env.BUCKET.get(file.key)).arrayBuffer()))));\n```\n\n### Parent-Child Workflows\n```typescript\nconst child = await step.do('start child', async () => await this.env.CHILD_WORKFLOW.create({id: `child-${event.instanceId}`, params: { data: result.data }}));\nawait step.do('other work', async () => console.log(`Child started: ${child.id}`));\n```\n\n### Race Pattern\n```typescript\nconst winner = await Promise.race([\n  step.do('option A', async () => slowOperation()),\n  step.do('option B', async () => fastOperation())\n]);\n```\n\n### Scheduled Workflow Chain\n```typescript\nexport default { async scheduled(event, env) { await env.DAILY_WORKFLOW.create({id: `daily-${event.scheduledTime}`, params: { timestamp: event.scheduledTime }}); }};\nexport class DailyWorkflow extends WorkflowEntrypoint<Env, Params> {\n  async run(event, step) {\n    await step.do('daily task', async () => {});\n    await step.sleep('wait 7 days', '7 days');\n    await step.do('weekly followup', async () => {});\n  }\n}\n```\n\nSee: [configuration.md](./configuration.md), [api.md](./api.md), [gotchas.md](./gotchas.md)\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/wrangler/README.md",
    "content": "# Cloudflare Wrangler\n\nOfficial CLI for Cloudflare Workers - develop, manage, and deploy Workers from the command line.\n\n## What is Wrangler?\n\nWrangler is the Cloudflare Developer Platform CLI that allows you to:\n- Create, develop, and deploy Workers\n- Manage bindings (KV, D1, R2, Durable Objects, etc.)\n- Configure routing and environments\n- Run local development servers\n- Execute migrations and manage resources\n- Perform integration testing\n\n## Installation\n\n```bash\nnpm install wrangler --save-dev\n# or globally\nnpm install -g wrangler\n```\n\nRun commands: `npx wrangler <command>` (or `pnpm`/`yarn wrangler`)\n\n## Reading Order\n\n| If you want to... | Start here |\n|-------------------|------------|\n| Create/deploy Worker quickly | Essential Commands below → [patterns.md](./patterns.md) §New Worker |\n| Configure bindings (KV, D1, R2) | [configuration.md](./configuration.md) §Bindings |\n| Write integration tests | [api.md](./api.md) §startWorker |\n| Debug production issues | [gotchas.md](./gotchas.md) + Essential Commands §Monitoring |\n| Set up multi-environment workflow | [configuration.md](./configuration.md) §Environments |\n\n## Essential Commands\n\n### Project & Development\n```bash\nwrangler init [name]              # Create new project\nwrangler dev                      # Local dev server (fast, simulated)\nwrangler dev --remote             # Dev with remote resources (production-like)\nwrangler deploy                   # Deploy to production\nwrangler deploy --env staging     # Deploy to environment\nwrangler versions list            # List versions\nwrangler rollback [id]            # Rollback deployment\nwrangler login                    # OAuth login\nwrangler whoami                   # Check auth status\n```\n\n## Resource Management\n\n### KV\n```bash\nwrangler kv namespace create NAME\nwrangler kv key put \"key\" \"value\" --namespace-id=<id>\nwrangler kv key get \"key\" --namespace-id=<id>\n```\n\n### D1\n```bash\nwrangler d1 create NAME\nwrangler d1 execute NAME --command \"SQL\"\nwrangler d1 migrations create NAME \"description\"\nwrangler d1 migrations apply NAME\n```\n\n### R2\n```bash\nwrangler r2 bucket create NAME\nwrangler r2 object put BUCKET/key --file path\nwrangler r2 object get BUCKET/key\n```\n\n### Other Resources\n```bash\nwrangler queues create NAME\nwrangler vectorize create NAME --dimensions N --metric cosine\nwrangler hyperdrive create NAME --connection-string \"...\"\nwrangler workflows create NAME\nwrangler constellation create NAME\nwrangler pages project create NAME\nwrangler pages deployment create --project NAME --branch main\n```\n\n### Secrets\n```bash\nwrangler secret put NAME          # Set Worker secret\nwrangler secret list              # List Worker secrets\nwrangler secret delete NAME       # Delete Worker secret\nwrangler secret bulk FILE.json    # Bulk upload from JSON\n\n# Secrets Store (centralized, reusable across Workers)\nwrangler secret-store:secret put STORE_NAME SECRET_NAME\nwrangler secret-store:secret list STORE_NAME\n```\n\n### Monitoring\n```bash\nwrangler tail                     # Real-time logs\nwrangler tail --env production    # Tail specific env\nwrangler tail --status error      # Filter by status\n```\n\n## In This Reference\n\n- [auth.md](./auth.md) - Authentication setup (`wrangler login`, API tokens)\n- [configuration.md](./configuration.md) - wrangler.jsonc setup, environments, bindings\n- [api.md](./api.md) - Programmatic API (`startWorker`, `getPlatformProxy`, events)\n- [patterns.md](./patterns.md) - Common workflows and development patterns\n- [gotchas.md](./gotchas.md) - Common pitfalls, limits, and troubleshooting\n\n## Quick Decision Tree\n\n```\nNeed to test your Worker?\n├─ Testing full Worker with bindings → api.md §startWorker\n├─ Testing individual functions → api.md §getPlatformProxy\n└─ Testing with Vitest → patterns.md §Testing with Vitest\n\nNeed to configure something?\n├─ Bindings (KV, D1, R2, etc.) → configuration.md §Bindings\n├─ Multiple environments → configuration.md §Environments\n├─ Static files → configuration.md §Workers Assets\n└─ Routing → configuration.md §Routing\n\nDevelopment not working?\n├─ Local differs from production → Use `wrangler dev --remote`\n├─ Bindings not available → gotchas.md §Binding Not Available\n└─ Auth issues → auth.md\n\nAuthentication issues?\n├─ \"Not logged in\" / \"Unauthorized\" → auth.md\n├─ First time deploying → `wrangler login` (one-time OAuth)\n└─ CI/CD setup → auth.md §API Token\n```\n\n## See Also\n\n- [workers](../workers/) - Workers runtime API reference\n- [miniflare](../miniflare/) - Local testing with Miniflare\n- [workerd](../workerd/) - Runtime that powers `wrangler dev`\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/wrangler/api.md",
    "content": "# Wrangler Programmatic API\n\nNode.js APIs for testing and development.\n\n## startWorker (Testing)\n\nStarts Worker with real local bindings for integration tests. Stable API (replaces `unstable_startWorker`).\n\n```typescript\nimport { startWorker } from \"wrangler\";\nimport { describe, it, before, after } from \"node:test\";\nimport assert from \"node:assert\";\n\ndescribe(\"worker\", () => {\n  let worker;\n  \n  before(async () => {\n    worker = await startWorker({\n      config: \"wrangler.jsonc\",\n      environment: \"development\"\n    });\n  });\n  \n  after(async () => {\n    await worker.dispose();\n  });\n  \n  it(\"responds with 200\", async () => {\n    const response = await worker.fetch(\"http://example.com\");\n    assert.strictEqual(response.status, 200);\n  });\n});\n```\n\n### Options\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `config` | `string` | Path to wrangler.jsonc |\n| `environment` | `string` | Environment name from config |\n| `persist` | `boolean \\| { path: string }` | Enable persistent state |\n| `bundle` | `boolean` | Enable bundling (default: true) |\n| `remote` | `false \\| true \\| \"minimal\"` | Remote mode: `false` (local), `true` (full remote), `\"minimal\"` (remote bindings only) |\n\n### Remote Mode\n\n```typescript\n// Local mode (default) - fast, simulated\nconst worker = await startWorker({ config: \"wrangler.jsonc\" });\n\n// Full remote mode - production-like, slower\nconst worker = await startWorker({ \n  config: \"wrangler.jsonc\",\n  remote: true \n});\n\n// Minimal remote mode - remote bindings, local Worker\nconst worker = await startWorker({ \n  config: \"wrangler.jsonc\",\n  remote: \"minimal\"\n});\n```\n\n## getPlatformProxy\n\nEmulate bindings in Node.js without starting Worker.\n\n```typescript\nimport { getPlatformProxy } from \"wrangler\";\n\nconst { env, dispose, caches } = await getPlatformProxy<Env>({\n  configPath: \"wrangler.jsonc\",\n  environment: \"production\",\n  persist: { path: \".wrangler/state\" }\n});\n\n// Use bindings\nconst value = await env.MY_KV.get(\"key\");\nawait env.DB.prepare(\"SELECT * FROM users\").all();\nawait env.ASSETS.put(\"file.txt\", \"content\");\n\n// Platform APIs\nawait caches.default.put(\"https://example.com\", new Response(\"cached\"));\n\nawait dispose();\n```\n\nUse for unit tests (test functions, not full Worker) or scripts that need bindings.\n\n## Type Generation\n\nGenerate types from config: `wrangler types` → creates `worker-configuration.d.ts`\n\n## Event System\n\nListen to Worker lifecycle events for advanced workflows.\n\n```typescript\nimport { startWorker } from \"wrangler\";\n\nconst worker = await startWorker({\n  config: \"wrangler.jsonc\",\n  bundle: true\n});\n\n// Bundle events\nworker.on(\"bundleStart\", (details) => {\n  console.log(\"Bundling started:\", details.config);\n});\n\nworker.on(\"bundleComplete\", (details) => {\n  console.log(\"Bundle ready:\", details.duration);\n});\n\n// Reconfiguration events\nworker.on(\"reloadStart\", () => {\n  console.log(\"Worker reloading...\");\n});\n\nworker.on(\"reloadComplete\", () => {\n  console.log(\"Worker reloaded\");\n});\n\nawait worker.dispose();\n```\n\n### Dynamic Reconfiguration\n\n```typescript\nimport { startWorker } from \"wrangler\";\n\nconst worker = await startWorker({ config: \"wrangler.jsonc\" });\n\n// Replace entire config\nawait worker.setConfig({\n  config: \"wrangler.staging.jsonc\",\n  environment: \"staging\"\n});\n\n// Patch specific fields\nawait worker.patchConfig({\n  vars: { DEBUG: \"true\" }\n});\n\nawait worker.dispose();\n```\n\n## unstable_dev (Deprecated)\n\nUse `startWorker` instead.\n\n## Multi-Worker Registry\n\nTest multiple Workers with service bindings.\n\n```typescript\nimport { startWorker } from \"wrangler\";\n\nconst auth = await startWorker({ config: \"./auth/wrangler.jsonc\" });\nconst api = await startWorker({\n  config: \"./api/wrangler.jsonc\",\n  bindings: { AUTH: auth }  // Service binding\n});\n\nconst response = await api.fetch(\"http://example.com/api/login\");\n// API Worker calls AUTH Worker via env.AUTH.fetch()\n\nawait api.dispose();\nawait auth.dispose();\n```\n\n## Best Practices\n\n- Use `startWorker` for integration tests (tests full Worker)\n- Use `getPlatformProxy` for unit tests (tests individual functions)\n- Use `remote: true` when debugging production-specific issues\n- Use `remote: \"minimal\"` for faster tests with real bindings\n- Enable `persist: true` for debugging (state survives runs)\n- Run `wrangler types` after config changes\n- Always `dispose()` to prevent resource leaks\n- Listen to bundle events for build monitoring\n- Use multi-worker registry for testing service bindings\n\n## See Also\n\n- [README.md](./README.md) - CLI commands\n- [configuration.md](./configuration.md) - Config\n- [patterns.md](./patterns.md) - Testing patterns\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/wrangler/auth.md",
    "content": "# Authentication\n\nAuthenticate with Cloudflare before deploying Workers or Pages.\n\n## Quick Decision Tree\n\n```\nNeed to authenticate?\n├─ Interactive/local dev → wrangler login (recommended)\n├─ CI/CD or headless → CLOUDFLARE_API_TOKEN env var\n└─ Terraform/Pulumi → See respective references\n```\n\n## wrangler login (Recommended)\n\nOne-time OAuth flow for local development:\n\n```bash\nnpx wrangler login     # Opens browser, completes OAuth\nnpx wrangler whoami    # Verify: shows email + account ID\n```\n\nCredentials stored locally. Works for all subsequent commands.\n\n## API Token (CI/CD)\n\nFor automated pipelines or environments without browser access:\n\n1. Go to: **https://dash.cloudflare.com/profile/api-tokens**\n2. Click **Create Token**\n3. Use template: **\"Edit Cloudflare Workers\"** (covers Workers, Pages, KV, D1, R2)\n4. Copy the token (shown only once)\n5. Set environment variable:\n\n```bash\nexport CLOUDFLARE_API_TOKEN=\"your-token-here\"\n```\n\n### Minimal Permissions by Task\n\n| Task | Template / Permissions |\n|------|------------------------|\n| Deploy Workers/Pages | \"Edit Cloudflare Workers\" template |\n| Read-only access | \"Read All Resources\" template |\n| Custom scope | Account:Read + Workers Scripts:Edit + specific resources |\n\n## Troubleshooting\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| \"Not logged in\" | No credentials | `wrangler login` or set `CLOUDFLARE_API_TOKEN` |\n| \"Authentication error\" | Invalid/expired token | Regenerate token in dashboard |\n| \"Missing account\" | Wrong account selected | `wrangler whoami` to check, add `account_id` to wrangler.jsonc |\n| Token works locally, fails CI | Token scoped to wrong account | Verify account ID matches in both places |\n| \"Insufficient permissions\" | Token lacks required scope | Create new token with correct permissions |\n\n## Verifying Authentication\n\n```bash\nnpx wrangler whoami\n```\n\nOutput shows:\n- Email (if OAuth login)\n- Account ID and name\n- Token scopes (if API token)\n\nNon-zero exit code means not authenticated.\n\n## See Also\n\n- [terraform/README.md](../terraform/README.md) - Terraform provider auth\n- [pulumi/README.md](../pulumi/README.md) - Pulumi provider auth\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/wrangler/configuration.md",
    "content": "# Wrangler Configuration\n\nConfiguration reference for wrangler.jsonc (recommended).\n\n## Config Format\n\n**wrangler.jsonc recommended** (v3.91.0+) - provides schema validation.\n\n```jsonc\n{\n  \"$schema\": \"./node_modules/wrangler/config-schema.json\",\n  \"name\": \"my-worker\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2025-01-01\",  // Use current date\n  \"vars\": { \"API_KEY\": \"dev-key\" },\n  \"kv_namespaces\": [{ \"binding\": \"MY_KV\", \"id\": \"abc123\" }]\n}\n```\n\n## Field Inheritance\n\nInheritable: `name`, `main`, `compatibility_date`, `routes`, `triggers`\nNon-inheritable (define per env): `vars`, bindings (KV, D1, R2, etc.)\n\n## Environments\n\n```jsonc\n{\n  \"name\": \"my-worker\",\n  \"vars\": { \"ENV\": \"dev\" },\n  \"env\": {\n    \"production\": {\n      \"name\": \"my-worker-prod\",\n      \"vars\": { \"ENV\": \"prod\" },\n      \"route\": { \"pattern\": \"example.com/*\", \"zone_name\": \"example.com\" }\n    }\n  }\n}\n```\n\nDeploy: `wrangler deploy --env production`\n\n## Routing\n\n```jsonc\n// Custom domain (recommended)\n{ \"routes\": [{ \"pattern\": \"api.example.com\", \"custom_domain\": true }] }\n\n// Zone-based\n{ \"routes\": [{ \"pattern\": \"api.example.com/*\", \"zone_name\": \"example.com\" }] }\n\n// workers.dev\n{ \"workers_dev\": true }\n```\n\n## Bindings\n\n```jsonc\n// Variables\n{ \"vars\": { \"API_URL\": \"https://api.example.com\" } }\n\n// KV\n{ \"kv_namespaces\": [{ \"binding\": \"CACHE\", \"id\": \"abc123\" }] }\n\n// D1\n{ \"d1_databases\": [{ \"binding\": \"DB\", \"database_id\": \"abc-123\" }] }\n\n// R2\n{ \"r2_buckets\": [{ \"binding\": \"ASSETS\", \"bucket_name\": \"my-assets\" }] }\n\n// Durable Objects\n{ \"durable_objects\": { \n  \"bindings\": [{ \n    \"name\": \"COUNTER\", \n    \"class_name\": \"Counter\",\n    \"script_name\": \"my-worker\"  // Required for external DOs\n  }] \n} }\n{ \"migrations\": [{ \"tag\": \"v1\", \"new_sqlite_classes\": [\"Counter\"] }] }\n\n// Service Bindings\n{ \"services\": [{ \"binding\": \"AUTH\", \"service\": \"auth-worker\" }] }\n\n// Queues\n{ \"queues\": {\n  \"producers\": [{ \"binding\": \"TASKS\", \"queue\": \"task-queue\" }],\n  \"consumers\": [{ \"queue\": \"task-queue\", \"max_batch_size\": 10 }]\n} }\n\n// Vectorize\n{ \"vectorize\": [{ \"binding\": \"VECTORS\", \"index_name\": \"embeddings\" }] }\n\n// Hyperdrive (requires nodejs_compat_v2 for pg/postgres)\n{ \"hyperdrive\": [{ \"binding\": \"HYPERDRIVE\", \"id\": \"hyper-id\" }] }\n{ \"compatibility_flags\": [\"nodejs_compat_v2\"] }  // For pg/postgres\n\n// Workers AI\n{ \"ai\": { \"binding\": \"AI\" } }\n\n// Workflows\n{ \"workflows\": [{ \"binding\": \"WORKFLOW\", \"name\": \"my-workflow\", \"class_name\": \"MyWorkflow\" }] }\n\n// Secrets Store (centralized secrets)\n{ \"secrets_store\": [{ \"binding\": \"SECRETS\", \"id\": \"store-id\" }] }\n\n// Constellation (AI inference)\n{ \"constellation\": [{ \"binding\": \"MODEL\", \"project_id\": \"proj-id\" }] }\n```\n\n## Workers Assets (Static Files)\n\nRecommended for serving static files (replaces old `site` config).\n\n```jsonc\n{\n  \"assets\": {\n    \"directory\": \"./public\",\n    \"binding\": \"ASSETS\",\n    \"html_handling\": \"auto-trailing-slash\",  // or \"none\", \"force-trailing-slash\"\n    \"not_found_handling\": \"single-page-application\"  // or \"404-page\", \"none\"\n  }\n}\n```\n\nAccess in Worker:\n```typescript\nexport default {\n  async fetch(request, env) {\n    // Try serving static asset first\n    const asset = await env.ASSETS.fetch(request);\n    if (asset.status !== 404) return asset;\n    \n    // Custom logic for non-assets\n    return new Response(\"API response\");\n  }\n}\n```\n\n## Placement\n\nControl where Workers run geographically.\n\n```jsonc\n{\n  \"placement\": {\n    \"mode\": \"smart\"  // or \"off\"\n  }\n}\n```\n\n- `\"smart\"`: Run Worker near data sources (D1, Durable Objects) to reduce latency\n- `\"off\"`: Default distribution (run everywhere)\n\n## Auto-Provisioning (Beta)\n\nOmit resource IDs - Wrangler creates them and writes back to config on deploy.\n\n```jsonc\n{ \"kv_namespaces\": [{ \"binding\": \"MY_KV\" }] }  // No id - auto-provisioned\n```\n\nAfter deploy, ID is added to config automatically.\n\n## Advanced\n\n```jsonc\n// Cron Triggers\n{ \"triggers\": { \"crons\": [\"0 0 * * *\"] } }\n\n// Observability (tracing)\n{ \"observability\": { \"enabled\": true, \"head_sampling_rate\": 0.1 } }\n\n// Runtime Limits\n{ \"limits\": { \"cpu_ms\": 100 } }\n\n// Browser Rendering\n{ \"browser\": { \"binding\": \"BROWSER\" } }\n\n// mTLS Certificates\n{ \"mtls_certificates\": [{ \"binding\": \"CERT\", \"certificate_id\": \"cert-uuid\" }] }\n\n// Logpush (stream logs to R2/S3)\n{ \"logpush\": true }\n\n// Tail Consumers (process logs with another Worker)\n{ \"tail_consumers\": [{ \"service\": \"log-worker\" }] }\n\n// Unsafe bindings (access to arbitrary bindings)\n{ \"unsafe\": { \"bindings\": [{ \"name\": \"MY_BINDING\", \"type\": \"plain_text\", \"text\": \"value\" }] } }\n```\n\n## See Also\n\n- [README.md](./README.md) - Overview and commands\n- [api.md](./api.md) - Programmatic API\n- [patterns.md](./patterns.md) - Workflows\n- [gotchas.md](./gotchas.md) - Common issues\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/wrangler/gotchas.md",
    "content": "# Wrangler Common Issues\n\n## Common Errors\n\n### \"Binding ID vs name mismatch\"\n\n**Cause:** Confusion between binding name (code) and resource ID\n**Solution:** Bindings use `binding` (code name) and `id`/`database_id`/`bucket_name` (resource ID). Preview bindings need separate IDs: `preview_id`, `preview_database_id`\n\n### \"Environment not inheriting config\"\n\n**Cause:** Non-inheritable keys not redefined per environment\n**Solution:** Non-inheritable keys (bindings, vars) must be redefined per environment. Inheritable keys (routes, compatibility_date) can be overridden\n\n### \"Local dev behavior differs from production\"\n\n**Cause:** Using local simulation instead of remote execution\n**Solution:** Choose appropriate remote mode:\n- `wrangler dev` (default): Local simulation, fast, limited accuracy\n- `wrangler dev --remote`: Full remote execution, production-accurate, slower\n- Use `remote: \"minimal\"` in tests for fast tests with real remote bindings\n\n### \"startWorker doesn't match production\"\n\n**Cause:** Using local mode when remote resources needed\n**Solution:** Use `remote` option:\n```typescript\nconst worker = await startWorker({ \n  config: \"wrangler.jsonc\",\n  remote: true  // or \"minimal\" for faster tests\n});\n```\n\n### \"Unexpected runtime changes\"\n\n**Cause:** Missing compatibility_date\n**Solution:** Always set `compatibility_date`:\n```jsonc\n{ \"compatibility_date\": \"2025-01-01\" }\n```\n\n### \"Durable Object binding not working\"\n\n**Cause:** Missing script_name for external DOs\n**Solution:** Always specify `script_name` for external Durable Objects:\n```jsonc\n{\n  \"durable_objects\": {\n    \"bindings\": [\n      { \"name\": \"MY_DO\", \"class_name\": \"MyDO\", \"script_name\": \"my-worker\" }\n    ]\n  }\n}\n```\n\nFor local DOs in same Worker, `script_name` is optional.\n\n### \"Auto-provisioned resources not appearing\"\n\n**Cause:** IDs written back to config on first deploy, but config not reloaded\n**Solution:** After first deploy with auto-provisioning, config file is updated with IDs. Commit the updated config. On subsequent deploys, existing resources are reused.\n\n### \"Secrets not available in local dev\"\n\n**Cause:** Secrets set with `wrangler secret put` only work in deployed Workers\n**Solution:** For local dev, use `.dev.vars`\n\n### \"Node.js compatibility error\"\n\n**Cause:** Missing Node.js compatibility flag\n**Solution:** Some bindings (Hyperdrive with `pg`) require:\n```jsonc\n{ \"compatibility_flags\": [\"nodejs_compat_v2\"] }\n```\n\n### \"Workers Assets 404 errors\"\n\n**Cause:** Asset path mismatch or incorrect `html_handling`\n**Solution:** \n- Check `assets.directory` points to correct build output\n- Set `html_handling: \"auto-trailing-slash\"` for SPAs\n- Use `not_found_handling: \"single-page-application\"` to serve index.html for 404s\n```jsonc\n{\n  \"assets\": {\n    \"directory\": \"./dist\",\n    \"html_handling\": \"auto-trailing-slash\",\n    \"not_found_handling\": \"single-page-application\"\n  }\n}\n```\n\n### \"Placement not reducing latency\"\n\n**Cause:** Misunderstanding of Smart Placement\n**Solution:** Smart Placement only helps when Worker accesses D1 or Durable Objects. It doesn't affect KV, R2, or external API latency.\n```jsonc\n{ \"placement\": { \"mode\": \"smart\" } }  // Only beneficial with D1/DOs\n```\n\n### \"unstable_startWorker not found\"\n\n**Cause:** Using outdated API\n**Solution:** Use stable `startWorker` instead:\n```typescript\nimport { startWorker } from \"wrangler\";  // Not unstable_startWorker\n```\n\n### \"outboundService not mocking fetch\"\n\n**Cause:** Mock function not returning Response\n**Solution:** Always return Response, use `fetch(req)` for passthrough:\n```typescript\nconst worker = await startWorker({\n  outboundService: (req) => {\n    if (shouldMock(req)) {\n      return new Response(\"mocked\");\n    }\n    return fetch(req);  // Required for non-mocked requests\n  }\n});\n```\n\n## Limits\n\n| Resource/Limit | Value | Notes |\n|----------------|-------|-------|\n| Bindings per Worker | 64 | Total across all types |\n| Environments | Unlimited | Named envs in config |\n| Config file size | ~1MB | Keep reasonable |\n| Workers Assets size | 25 MB | Per deployment |\n| Workers Assets files | 20,000 | Max number of files |\n| Script size (compressed) | 1 MB | Free, 10 MB paid |\n| CPU time | 10-50ms | Free, 50-500ms paid |\n| Subrequest limit | 50 | Free, 1000 paid |\n\n## Troubleshooting\n\n### Authentication Issues\n```bash\nwrangler logout\nwrangler login\nwrangler whoami\n```\n\n### Configuration Errors\n```bash\nwrangler check  # Validate config\n```\nUse wrangler.jsonc with `$schema` for validation.\n\n### Binding Not Available\n- Check binding exists in config\n- For environments, ensure binding defined for that env\n- Local dev: some bindings need `--remote`\n\n### Deployment Failures\n```bash\nwrangler tail              # Check logs\nwrangler deploy --dry-run  # Validate\nwrangler whoami            # Check account limits\n```\n\n### Local Development Issues\n```bash\nrm -rf .wrangler/state     # Clear local state\nwrangler dev --remote      # Use remote bindings\nwrangler dev --persist-to ./local-state  # Custom persist location\nwrangler dev --inspector-port 9229  # Enable debugging\n```\n\n### Testing Issues\n```bash\n# If tests hang, ensure dispose() is called\nworker.dispose()  // Always cleanup\n\n# If bindings don't work in tests\nconst worker = await startWorker({ \n  config: \"wrangler.jsonc\",\n  remote: \"minimal\"  // Use remote bindings\n});\n```\n\n## Resources\n\n- Docs: https://developers.cloudflare.com/workers/wrangler/\n- Config: https://developers.cloudflare.com/workers/wrangler/configuration/\n- Commands: https://developers.cloudflare.com/workers/wrangler/commands/\n- Examples: https://github.com/cloudflare/workers-sdk/tree/main/templates\n- Discord: https://discord.gg/cloudflaredev\n\n## See Also\n\n- [README.md](./README.md) - Commands\n- [configuration.md](./configuration.md) - Config\n- [api.md](./api.md) - Programmatic API\n- [patterns.md](./patterns.md) - Workflows\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/wrangler/patterns.md",
    "content": "# Wrangler Development Patterns\n\nCommon workflows and best practices.\n\n## New Worker Project\n\n```bash\nwrangler init my-worker && cd my-worker\nwrangler dev              # Develop locally\nwrangler deploy           # Deploy\n```\n\n## Local Development\n\n```bash\nwrangler dev              # Local mode (fast, simulated)\nwrangler dev --remote     # Remote mode (production-accurate)\nwrangler dev --env staging --port 8787\nwrangler dev --inspector-port 9229  # Enable debugging\n```\n\nDebug: chrome://inspect → Configure → localhost:9229\n\n## Secrets\n\n```bash\n# Production\necho \"secret-value\" | wrangler secret put SECRET_KEY\n\n# Local: use .dev.vars (gitignored)\n# SECRET_KEY=local-dev-key\n```\n\n## Adding KV\n\n```bash\nwrangler kv namespace create MY_KV\nwrangler kv namespace create MY_KV --preview\n# Add to wrangler.jsonc: { \"binding\": \"MY_KV\", \"id\": \"abc123\" }\nwrangler deploy\n```\n\n## Adding D1\n\n```bash\nwrangler d1 create my-db\nwrangler d1 migrations create my-db \"initial_schema\"\n# Edit migration file in migrations/, then:\nwrangler d1 migrations apply my-db --local\nwrangler deploy\nwrangler d1 migrations apply my-db --remote\n\n# Time Travel (restore to point in time)\nwrangler d1 time-travel restore my-db --timestamp 2025-01-01T12:00:00Z\n```\n\n## Multi-Environment\n\n```bash\nwrangler deploy --env staging\nwrangler deploy --env production\n```\n\n```jsonc\n{ \"env\": { \"staging\": { \"vars\": { \"ENV\": \"staging\" } } } }\n```\n\n## Testing\n\n### Integration Tests with Node.js Test Runner\n\n```typescript\nimport { startWorker } from \"wrangler\";\nimport { describe, it, before, after } from \"node:test\";\nimport assert from \"node:assert\";\n\ndescribe(\"API\", () => {\n  let worker;\n  \n  before(async () => {\n    worker = await startWorker({ \n      config: \"wrangler.jsonc\",\n      remote: \"minimal\"  // Fast tests with real bindings\n    });\n  });\n  \n  after(async () => await worker.dispose());\n  \n  it(\"creates user\", async () => {\n    const response = await worker.fetch(\"http://example.com/api/users\", {\n      method: \"POST\",\n      body: JSON.stringify({ name: \"Alice\" })\n    });\n    assert.strictEqual(response.status, 201);\n  });\n});\n```\n\n### Testing with Vitest\n\nInstall: `npm install -D vitest @cloudflare/vitest-pool-workers`\n\n**vitest.config.ts:**\n```typescript\nimport { defineWorkersConfig } from \"@cloudflare/vitest-pool-workers/config\";\nexport default defineWorkersConfig({\n  test: { poolOptions: { workers: { wrangler: { configPath: \"./wrangler.jsonc\" } } } }\n});\n```\n\n**tests/api.test.ts:**\n```typescript\nimport { env, SELF } from \"cloudflare:test\";\nimport { describe, it, expect } from \"vitest\";\n\nit(\"fetches users\", async () => {\n  const response = await SELF.fetch(\"https://example.com/api/users\");\n  expect(response.status).toBe(200);\n});\n\nit(\"uses bindings\", async () => {\n  await env.MY_KV.put(\"key\", \"value\");\n  expect(await env.MY_KV.get(\"key\")).toBe(\"value\");\n});\n```\n\n### Multi-Worker Development (Service Bindings)\n\n```typescript\nconst authWorker = await startWorker({ config: \"./auth/wrangler.jsonc\" });\nconst apiWorker = await startWorker({\n  config: \"./api/wrangler.jsonc\",\n  bindings: { AUTH: authWorker }  // Service binding\n});\n\n// Test API calling AUTH\nconst response = await apiWorker.fetch(\"http://example.com/api/protected\");\nawait authWorker.dispose();\nawait apiWorker.dispose();\n```\n\n### Mock External APIs\n\n```typescript\nconst worker = await startWorker({ \n  config: \"wrangler.jsonc\",\n  outboundService: (req) => {\n    const url = new URL(req.url);\n    if (url.hostname === \"api.external.com\") {\n      return new Response(JSON.stringify({ mocked: true }), {\n        headers: { \"content-type\": \"application/json\" }\n      });\n    }\n    return fetch(req);  // Pass through other requests\n  }\n});\n\n// Test Worker that calls external API\nconst response = await worker.fetch(\"http://example.com/proxy\");\n// Worker internally fetches api.external.com - gets mocked response\n```\n\n## Monitoring & Versions\n\n```bash\nwrangler tail                 # Real-time logs\nwrangler tail --status error  # Filter errors\nwrangler versions list\nwrangler rollback [id]\n```\n\n## TypeScript\n\n```bash\nwrangler types  # Generate types from config\n```\n\n```typescript\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    return Response.json({ value: await env.MY_KV.get(\"key\") });\n  }\n} satisfies ExportedHandler<Env>;\n```\n\n## Workers Assets\n\n```jsonc\n{ \"assets\": { \"directory\": \"./dist\", \"binding\": \"ASSETS\" } }\n```\n\n```typescript\nexport default {\n  async fetch(request, env) {\n    // API routes first\n    if (new URL(request.url).pathname.startsWith(\"/api/\")) {\n      return Response.json({ data: \"from API\" });\n    }\n    return env.ASSETS.fetch(request);  // Static assets\n  }\n}\n```\n\n## See Also\n\n- [README.md](./README.md) - Commands\n- [configuration.md](./configuration.md) - Config\n- [api.md](./api.md) - Programmatic API\n- [gotchas.md](./gotchas.md) - Issues\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/zaraz/IMPLEMENTATION_SUMMARY.md",
    "content": "# Zaraz Reference Implementation Summary\n\n## Files Created\n\n| File | Lines | Purpose |\n|------|-------|---------|\n| README.md | 111 | Navigation, decision tree, quick start |\n| api.md | 287 | Web API reference, Zaraz Context |\n| configuration.md | 307 | Dashboard setup, triggers, tools, consent |\n| patterns.md | 430 | SPA, e-commerce, Worker integration |\n| gotchas.md | 317 | Troubleshooting, limits, tool-specific issues |\n| **Total** | **1,452** | **vs 366 original** |\n\n## Key Improvements Applied\n\n### Structure\n- ✅ Created 5-file progressive disclosure system\n- ✅ Added navigation table in README\n- ✅ Added decision tree for routing\n- ✅ Added \"Reading Order by Task\" guide\n- ✅ Cross-referenced files throughout\n\n### New Content Added\n- ✅ Zaraz Context (system/client properties)\n- ✅ History Change trigger for SPA tracking\n- ✅ Context Enrichers pattern\n- ✅ Worker Variables pattern\n- ✅ Consent management deep dive\n- ✅ Tool-specific quirks (GA4, Facebook, Google Ads)\n- ✅ GTM migration guide\n- ✅ Comprehensive troubleshooting\n- ✅ \"When NOT to use Zaraz\" section\n- ✅ TypeScript type definitions\n\n### Preserved Content\n- ✅ All original API methods\n- ✅ E-commerce tracking examples\n- ✅ Consent management\n- ✅ Workers integration (expanded)\n- ✅ Common patterns (expanded)\n- ✅ Debugging tools\n- ✅ Reference links\n\n## Progressive Disclosure Impact\n\n### Before (Monolithic)\nAll tasks loaded 366 lines regardless of need.\n\n### After (Progressive)\n- **Track event task**: README (111) + api.md (287) = 398 lines\n- **Debug issue**: gotchas.md (317) = 317 lines (13% reduction)\n- **Configure tool**: configuration.md (307) = 307 lines (16% reduction)\n- **SPA tracking**: README + patterns.md (SPA section) ~180 lines (51% reduction)\n\n**Net effect:** Task-specific loading reduces unnecessary content by 13-51% depending on use case.\n\n## File Summary\n\n### README.md (111 lines)\n- Overview and core concepts\n- Quick start guide\n- When to use Zaraz vs Workers\n- Navigation table\n- Reading order by task\n- Decision tree\n\n### api.md (287 lines)\n- zaraz.track()\n- zaraz.set()\n- zaraz.ecommerce()\n- Zaraz Context (system/client properties)\n- zaraz.consent API\n- zaraz.debug\n- Cookie methods\n- TypeScript definitions\n\n### configuration.md (307 lines)\n- Dashboard setup flow\n- Trigger types (including History Change)\n- Tool configuration (GA4, Facebook, Google Ads)\n- Actions and action rules\n- Selective loading\n- Consent management setup\n- Privacy features\n- Testing workflow\n\n### patterns.md (430 lines)\n- SPA tracking (React, Vue, Next.js)\n- User identification flows\n- Complete e-commerce funnel\n- A/B testing\n- Worker integration (Context Enrichers, Worker Variables, HTML injection)\n- Multi-tool coordination\n- GTM migration\n- Best practices\n\n### gotchas.md (317 lines)\n- Events not firing (5-step debug process)\n- Consent issues\n- SPA tracking pitfalls\n- Performance issues\n- Tool-specific quirks\n- Data layer issues\n- Limits table\n- When NOT to use Zaraz\n- Debug checklist\n\n## Quality Metrics\n\n- ✅ All files use consistent markdown formatting\n- ✅ Code examples include language tags\n- ✅ Tables for structured data (limits, parameters, comparisons)\n- ✅ Problem → Cause → Solution format in gotchas\n- ✅ Cross-references between files\n- ✅ No \"see documentation\" placeholders\n- ✅ Real, actionable examples throughout\n- ✅ Verified API syntax for Workers\n\n## Original Backup\n\nOriginal SKILL.md preserved as `_SKILL_old.md` for reference.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/zaraz/README.md",
    "content": "# Cloudflare Zaraz\n\nExpert guidance for Cloudflare Zaraz - server-side tag manager for loading third-party tools at the edge.\n\n## What is Zaraz?\n\nZaraz offloads third-party scripts (analytics, ads, chat, marketing) to Cloudflare's edge, improving site speed, privacy, and security. Zero client-side performance impact.\n\n**Core Concepts:**\n- **Server-side execution** - Scripts run on Cloudflare, not user's browser\n- **Single HTTP request** - All tools loaded via one endpoint\n- **Privacy-first** - Control data sent to third parties\n- **No client-side JS overhead** - Minimal browser impact\n\n## Quick Start\n\n1. Navigate to domain > Zaraz in Cloudflare dashboard\n2. Click \"Start setup\"\n3. Add tools (Google Analytics, Facebook Pixel, etc.)\n4. Configure triggers (when tools fire)\n5. Add tracking code to your site:\n\n```javascript\n// Track page view\nzaraz.track('page_view');\n\n// Track custom event\nzaraz.track('button_click', { button_id: 'cta' });\n\n// Set user properties\nzaraz.set('userId', 'user_123');\n```\n\n## When to Use Zaraz\n\n**Use Zaraz when:**\n- Adding multiple third-party tools (analytics, ads, marketing)\n- Site performance is critical (no client-side JS overhead)\n- Privacy compliance required (GDPR, CCPA)\n- Non-technical teams need to manage tools\n\n**Use Workers directly when:**\n- Building custom server-side tracking logic\n- Need full control over data processing\n- Integrating with complex backend systems\n- Zaraz's tool library doesn't meet needs\n\n## In This Reference\n\n| File | Purpose | When to Read |\n|------|---------|--------------|\n| [api.md](./api.md) | Web API, zaraz object, consent methods | Implementing tracking calls |\n| [configuration.md](./configuration.md) | Dashboard setup, triggers, tools | Initial setup, adding tools |\n| [patterns.md](./patterns.md) | SPA, e-commerce, Worker integration | Best practices, common scenarios |\n| [gotchas.md](./gotchas.md) | Troubleshooting, limits, pitfalls | Debugging issues |\n\n## Reading Order by Task\n\n| Task | Files to Read |\n|------|---------------|\n| Add analytics to site | README → configuration.md |\n| Track custom events | README → api.md |\n| Debug tracking issues | gotchas.md |\n| SPA tracking | api.md → patterns.md (SPA section) |\n| E-commerce tracking | api.md#ecommerce → patterns.md#ecommerce |\n| Worker integration | patterns.md#worker-integration |\n| GDPR compliance | api.md#consent → configuration.md#consent |\n\n## Decision Tree\n\n```\nWhat do you need?\n\n├─ Track events in browser → api.md\n│   ├─ Page views, clicks → zaraz.track()\n│   ├─ User properties → zaraz.set()\n│   └─ E-commerce → zaraz.ecommerce()\n│\n├─ Configure Zaraz → configuration.md\n│   ├─ Add GA4/Facebook → tools setup\n│   ├─ When tools fire → triggers\n│   └─ GDPR consent → consent purposes\n│\n├─ Integrate with Workers → patterns.md#worker-integration\n│   ├─ Enrich context → Context Enrichers\n│   └─ Inject tracking → HTML rewriting\n│\n└─ Debug issues → gotchas.md\n    ├─ Events not firing → troubleshooting\n    ├─ Consent issues → consent debugging\n    └─ Performance → debugging tools\n```\n\n## Key Features\n\n- **100+ Pre-built Tools** - GA4, Facebook, Google Ads, TikTok, etc.\n- **Zero Client Impact** - Runs at Cloudflare's edge, not browser\n- **Privacy Controls** - Consent management, data filtering\n- **Custom Tools** - Build Managed Components for proprietary systems\n- **Worker Integration** - Enrich context, compute dynamic values\n- **Debug Mode** - Real-time event inspection\n\n## Reference\n\n- [Zaraz Docs](https://developers.cloudflare.com/zaraz/)\n- [Web API](https://developers.cloudflare.com/zaraz/web-api/)\n- [Managed Components](https://developers.cloudflare.com/zaraz/advanced/load-custom-managed-component/)\n\n---\n\nThis skill focuses exclusively on Zaraz. For Workers development, see `cloudflare-workers` skill.\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/zaraz/api.md",
    "content": "# Zaraz Web API\n\nClient-side JavaScript API for tracking events, setting properties, and managing consent.\n\n## zaraz.track()\n\n```javascript\nzaraz.track('button_click');\nzaraz.track('purchase', { value: 99.99, currency: 'USD', item_id: '12345' });\nzaraz.track('pageview', { page_path: '/products', page_title: 'Products' }); // SPA\n```\n\n**Params:** `eventName` (string), `properties` (object, optional). Fire-and-forget.\n\n## zaraz.set()\n\n```javascript\nzaraz.set('userId', 'user_12345');\nzaraz.set({ email: '[email protected]', plan: 'premium', country: 'US' });\n```\n\nProperties persist for page session. Use for user identification and segmentation.\n\n## zaraz.ecommerce()\n\n```javascript\nzaraz.ecommerce('Product Viewed', { product_id: 'SKU123', name: 'Widget', price: 49.99 });\nzaraz.ecommerce('Product Added', { product_id: 'SKU123', quantity: 2, price: 49.99 });\nzaraz.ecommerce('Order Completed', {\n  order_id: 'ORD-789', total: 149.98, currency: 'USD',\n  products: [{ product_id: 'SKU123', quantity: 2, price: 49.99 }]\n});\n```\n\n**Events:** `Product Viewed`, `Product Added`, `Product Removed`, `Cart Viewed`, `Checkout Started`, `Order Completed`\n\nTools auto-map to GA4, Facebook CAPI, etc.\n\n## System Properties (Triggers)\n\n```\n{{system.page.url}}   {{system.page.title}}   {{system.page.referrer}}\n{{system.device.ip}}  {{system.device.userAgent}}  {{system.device.language}}\n{{system.cookies.name}}  {{client.__zarazTrack.userId}}\n```\n\n## zaraz.consent\n\n```javascript\n// Check\nconst purposes = zaraz.consent.getAll(); // { analytics: true, marketing: false }\n\n// Set\nzaraz.consent.modal = true; // Show modal\nzaraz.consent.setAll({ analytics: true, marketing: false });\nzaraz.consent.set('marketing', true);\n\n// Listen\nzaraz.consent.addEventListener('consentChanged', () => {\n  if (zaraz.consent.getAll().marketing) zaraz.track('marketing_consent_granted');\n});\n```\n\n**Flow:** Configure purposes in dashboard → Map tools to purposes → Show modal/set programmatically → Tools fire when allowed\n\n## zaraz.debug\n\n```javascript\nzaraz.debug = true;\nzaraz.track('test_event');\nconsole.log(zaraz.tools); // View loaded tools\n```\n\n## Cookie Methods\n\n```javascript\nzaraz.getCookie('session_id');  // Zaraz namespace\nzaraz.readCookie('_ga');        // Any cookie\n```\n\n## Async Behavior\n\nAll methods fire-and-forget. Events batched and sent asynchronously:\n\n```javascript\nzaraz.track('event1');\nzaraz.set('prop', 'value');\nzaraz.track('event2'); // All batched\n```\n\n## TypeScript Types\n\n```typescript\ninterface Zaraz {\n  track(event: string, properties?: Record<string, unknown>): void;\n  set(key: string, value: unknown): void;\n  set(properties: Record<string, unknown>): void;\n  ecommerce(event: string, properties: Record<string, unknown>): void;\n  consent: {\n    getAll(): Record<string, boolean>;\n    setAll(purposes: Record<string, boolean>): void;\n    set(purpose: string, value: boolean): void;\n    addEventListener(event: 'consentChanged', callback: () => void): void;\n    modal: boolean;\n  };\n  debug: boolean;\n  tools?: string[];\n  getCookie(name: string): string | undefined;\n  readCookie(name: string): string | undefined;\n}\ndeclare global { interface Window { zaraz: Zaraz; } }\n```\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/zaraz/configuration.md",
    "content": "# Zaraz Configuration\n\n## Dashboard Setup\n\n1. Domain → Zaraz → Start setup\n2. Add tool (e.g., Google Analytics 4)\n3. Enter credentials (GA4: `G-XXXXXXXXXX`)\n4. Configure triggers\n5. Save and Publish\n\n## Triggers\n\n| Type | When | Use Case |\n|------|------|----------|\n| Pageview | Page load | Track page views |\n| Click | Element clicked | Button tracking |\n| Form Submission | Form submitted | Lead capture |\n| History Change | URL changes (SPA) | React/Vue routing |\n| Variable Match | Custom condition | Conditional firing |\n\n### History Change (SPA)\n\n```\nType: History Change\nEvent: pageview\n```\n\nFires on `pushState`, `replaceState`, hash changes. **No manual tracking needed.**\n\n### Click Trigger\n\n```\nType: Click\nCSS Selector: .buy-button\nEvent: purchase_intent\nProperties:\n  button_text: {{system.clickElement.text}}\n```\n\n## Tool Configuration\n\n**GA4:**\n```\nMeasurement ID: G-XXXXXXXXXX\nEvents: page_view, purchase, user_engagement\n```\n\n**Facebook Pixel:**\n```\nPixel ID: 1234567890123456\nEvents: PageView, Purchase, AddToCart\n```\n\n**Google Ads:**\n```\nConversion ID: AW-XXXXXXXXX\nConversion Label: YYYYYYYYYY\n```\n\n## Consent Management\n\n1. Settings → Consent → Create purposes (analytics, marketing)\n2. Map tools to purposes\n3. Set behavior: \"Do not load until consent granted\"\n\n**Programmatic consent:**\n```javascript\nzaraz.consent.setAll({ analytics: true, marketing: true });\n```\n\n## Privacy Features\n\n| Feature | Default |\n|---------|---------|\n| IP Anonymization | Enabled |\n| Cookie Control | Via consent purposes |\n| GDPR/CCPA | Consent modal |\n\n## Testing\n\n1. **Preview Mode** - test without publishing\n2. **Debug Mode** - `zaraz.debug = true`\n3. **Network tab** - filter \"zaraz\"\n\n## Limits\n\n| Resource | Limit |\n|----------|-------|\n| Event properties | 100KB |\n| Consent purposes | 20 |\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/zaraz/gotchas.md",
    "content": "# Zaraz Gotchas\n\n## Events Not Firing\n\n**Check:**\n1. Tool enabled in dashboard (green dot)\n2. Trigger conditions met\n3. Consent granted for tool's purpose\n4. Tool credentials correct (GA4: `G-XXXXXXXXXX`, FB: numeric only)\n\n**Debug:**\n```javascript\nzaraz.debug = true;\nconsole.log('Tools:', zaraz.tools);\nconsole.log('Consent:', zaraz.consent.getAll());\n```\n\n## Consent Issues\n\n**Modal not showing:**\n```javascript\n// Clear consent cookie\ndocument.cookie = 'zaraz-consent=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';\nlocation.reload();\n```\n\n**Tools firing before consent:** Map tool to consent purpose with \"Do not load until consent granted\".\n\n## SPA Tracking\n\n**Route changes not tracked:**\n1. Configure History Change trigger in dashboard\n2. Hash routing (`#/path`) requires manual tracking:\n```javascript\nwindow.addEventListener('hashchange', () => {\n  zaraz.track('pageview', { page_path: location.pathname + location.hash });\n});\n```\n\n**React fix:**\n```javascript\nconst location = useLocation();\nuseEffect(() => {\n  zaraz.track('pageview', { page_path: location.pathname });\n}, [location]); // Include dependency\n```\n\n## Performance\n\n**Slow page load:**\n- Audit tool count (50+ degrades performance)\n- Disable blocking triggers unless required\n- Reduce event payload size (<100KB)\n\n## Tool-Specific Issues\n\n| Tool | Issue | Fix |\n|------|-------|-----|\n| GA4 | Events not in real-time | Wait 5-10 min, use DebugView |\n| Facebook | Invalid Pixel ID | Use numeric only (no `fbpx_` prefix) |\n| Google Ads | Conversions not attributed | Include `send_to: 'AW-XXX/LABEL'` |\n\n## Data Layer\n\n- Properties persist per page only - set on each page load\n- Nested access: `{{client.__zarazTrack.user.plan}}`\n\n## Limits\n\n| Resource | Limit |\n|----------|-------|\n| Request size | 100KB |\n| Consent purposes | 20 |\n| API rate | 1000 req/sec |\n\n## When NOT to Use Zaraz\n\n- Server-to-server tracking (use Workers)\n- Real-time bidirectional communication\n- Binary data transmission\n- Authentication flows\n"
  },
  {
    "path": "skills/.curated/cloudflare-deploy/references/zaraz/patterns.md",
    "content": "# Zaraz Patterns\n\n## SPA Tracking\n\n**History Change Trigger (Recommended):** Configure in dashboard - no code needed, Zaraz auto-detects route changes.\n\n**Manual tracking (React/Vue/Next.js):**\n```javascript\n// On route change\nzaraz.track('pageview', { page_path: pathname, page_title: document.title });\n```\n\n## User Identification\n\n```javascript\n// Login\nzaraz.set({ userId: user.id, email: user.email, plan: user.plan });\nzaraz.track('login', { method: 'oauth' });\n\n// Logout - set to null (cannot clear)\nzaraz.set('userId', null);\n```\n\n## E-commerce Funnel\n\n| Event | Method |\n|-------|--------|\n| View | `zaraz.ecommerce('Product Viewed', { product_id, name, price })` |\n| Add to cart | `zaraz.ecommerce('Product Added', { product_id, quantity })` |\n| Checkout | `zaraz.ecommerce('Checkout Started', { cart_id, products: [...] })` |\n| Purchase | `zaraz.ecommerce('Order Completed', { order_id, total, products })` |\n\n## A/B Testing\n\n```javascript\nzaraz.set('experiment_checkout', variant);\nzaraz.track('experiment_viewed', { experiment_id: 'checkout', variant });\n// On conversion\nzaraz.track('experiment_conversion', { experiment_id, variant, value });\n```\n\n## Worker Integration\n\n**Context Enricher** - Modify context before tools execute:\n```typescript\nexport default {\n  async fetch(request, env) {\n    const body = await request.json();\n    body.system.userRegion = request.cf?.region;\n    return Response.json(body);\n  }\n};\n```\nConfigure: Zaraz > Settings > Context Enrichers\n\n**Worker Variables** - Compute dynamic values server-side, use as `{{worker.variable_name}}`.\n\n## GTM Migration\n\n| GTM | Zaraz |\n|-----|-------|\n| `dataLayer.push({event: 'purchase'})` | `zaraz.ecommerce('Order Completed', {...})` |\n| `{{Page URL}}` | `{{system.page.url}}` |\n| `{{Page Title}}` | `{{system.page.title}}` |\n| Page View trigger | Pageview trigger |\n| Click trigger | Click (selector: `*`) |\n\n## Best Practices\n\n1. Use dashboard triggers over inline code\n2. Enable History Change for SPAs (no manual code)\n3. Debug with `zaraz.debug = true`\n4. Implement consent early (GDPR/CCPA)\n5. Use Context Enrichers for sensitive/server data\n"
  },
  {
    "path": "skills/.curated/develop-web-game/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/develop-web-game/SKILL.md",
    "content": "---\nname: \"develop-web-game\"\ndescription: \"Use when Codex is building or iterating on a web game (HTML/JS) and needs a reliable development + testing loop: implement small changes, run a Playwright-based test script with short input bursts and intentional pauses, inspect screenshots/text, and review console errors with render_game_to_text.\"\n---\n\n\n# Develop Web Game\n\nBuild games in small steps and validate every change. Treat each iteration as: implement → act → pause → observe → adjust.\n\n## Skill paths (set once)\n\n```bash\nexport CODEX_HOME=\"${CODEX_HOME:-$HOME/.codex}\"\nexport WEB_GAME_CLIENT=\"$CODEX_HOME/skills/develop-web-game/scripts/web_game_playwright_client.js\"\nexport WEB_GAME_ACTIONS=\"$CODEX_HOME/skills/develop-web-game/references/action_payloads.json\"\n```\n\nUser-scoped skills install under `$CODEX_HOME/skills` (default: `~/.codex/skills`).\n\n## Workflow\n\n1. **Pick a goal.** Define a single feature or behavior to implement.\n2. **Implement small.** Make the smallest change that moves the game forward.\n3. **Ensure integration points.** Provide a single canvas and `window.render_game_to_text` so the test loop can read state.\n4. **Add `window.advanceTime(ms)`.** Strongly prefer a deterministic step hook so the Playwright script can advance frames reliably; without it, automated tests can be flaky.\n5. **Initialize progress.md.** If `progress.md` exists, read it first and confirm the original user prompt is recorded at the top (prefix with `Original prompt:`). Also note any TODOs and suggestions left by the previous agent. If missing, create it and write `Original prompt: <prompt>` at the top before appending updates.\n6. **Verify Playwright availability.** Ensure `playwright` is available (local dependency or global install). If unsure, check `npx` first.\n7. **Run the Playwright test script.** You must run `$WEB_GAME_CLIENT` after each meaningful change; do not invent a new client unless required.\n8. **Use the payload reference.** Base actions on `$WEB_GAME_ACTIONS` to avoid guessing keys.\n9. **Inspect state.** Capture screenshots and text state after each burst.\n10. **Inspect screenshots.** Open the latest screenshot, verify expected visuals, fix any issues, and rerun the script. Repeat until correct.\n11. **Verify controls and state (multi-step focus).** Exhaustively exercise all important interactions. For each, think through the full multi-step sequence it implies (cause → intermediate states → outcome) and verify the entire chain works end-to-end. Confirm `render_game_to_text` reflects the same state shown on screen. If anything is off, fix and rerun.\n    Examples of important interactions: move, jump, shoot/attack, interact/use, select/confirm/cancel in menus, pause/resume, restart, and any special abilities or puzzle actions defined by the request. Multi-step examples: shooting an enemy should reduce its health; when health reaches 0 it should disappear and update the score; collecting a key should unlock a door and allow level progression.\n12. **Check errors.** Review console errors and fix the first new issue before continuing.\n13. **Reset between scenarios.** Avoid cross-test state when validating distinct features.\n14. **Iterate with small deltas.** Change one variable at a time (frames, inputs, timing, positions), then repeat steps 7–13 until stable.\n\nExample command (actions required):\n```\nnode \"$WEB_GAME_CLIENT\" --url http://localhost:5173 --actions-file \"$WEB_GAME_ACTIONS\" --click-selector \"#start-btn\" --iterations 3 --pause-ms 250\n```\n\nExample actions (inline JSON):\n```json\n{\n  \"steps\": [\n    { \"buttons\": [\"left_mouse_button\"], \"frames\": 2, \"mouse_x\": 120, \"mouse_y\": 80 },\n    { \"buttons\": [], \"frames\": 6 },\n    { \"buttons\": [\"right\"], \"frames\": 8 },\n    { \"buttons\": [\"space\"], \"frames\": 4 }\n  ]\n}\n```\n\n## Test Checklist\n\nTest any new features added for the request and any areas your logic changes could affect. Identify issues, fix them, and re-run the tests to confirm they’re resolved.\n\nExamples of things to test:\n- Primary movement/interaction inputs (e.g., move, jump, shoot, confirm/select).\n- Win/lose or success/fail transitions.\n- Score/health/resource changes.\n- Boundary conditions (collisions, walls, screen edges).\n- Menu/pause/start flow if present.\n- Any special actions tied to the request (powerups, combos, abilities, puzzles, timers).\n\n## Test Artifacts to Review\n\n- Latest screenshots from the Playwright run.\n- Latest `render_game_to_text` JSON output.\n- Console error logs (fix the first new error before continuing).\nYou must actually open and visually inspect the latest screenshots after running the Playwright script, not just generate them. Ensure everything that should be visible on screen is actually visible. Go beyond the start screen and capture gameplay screenshots that cover all newly added features. Treat the screenshots as the source of truth; if something is missing, it is missing in the build. If you suspect a headless/WebGL capture issue, rerun the Playwright script in headed mode and re-check. Fix and rerun in a tight loop until the screenshots and text state look correct. Once fixes are verified, re-test all important interactions and controls, confirm they work, and ensure your changes did not introduce regressions. If they did, fix them and rerun everything in a loop until interactions, text state, and controls all work as expected. Be exhaustive in testing controls; broken games are not acceptable.\n\n## Core Game Guidelines\n\n### Canvas + Layout\n- Prefer a single canvas centered in the window.\n\n### Visuals\n- Keep on-screen text minimal; show controls on a start/menu screen rather than overlaying them during play.\n- Avoid overly dark scenes unless the design calls for it. Make key elements easy to see.\n- Draw the background on the canvas itself instead of relying on CSS backgrounds.\n\n### Text State Output (render_game_to_text)\nExpose a `window.render_game_to_text` function that returns a concise JSON string representing the current game state. The text should include enough information to play the game without visuals.\n\nMinimal pattern:\n```js\nfunction renderGameToText() {\n  const payload = {\n    mode: state.mode,\n    player: { x: state.player.x, y: state.player.y, r: state.player.r },\n    entities: state.entities.map((e) => ({ x: e.x, y: e.y, r: e.r })),\n    score: state.score,\n  };\n  return JSON.stringify(payload);\n}\nwindow.render_game_to_text = renderGameToText;\n```\n\nKeep the payload succinct and biased toward on-screen/interactive elements. Prefer current, visible entities over full history.\nInclude a clear coordinate system note (origin and axis directions), and encode all player-relevant state: player position/velocity, active obstacles/enemies, collectibles, timers/cooldowns, score, and any mode/state flags needed to make correct decisions. Avoid large histories; only include what's currently relevant and visible.\n\n### Time Stepping Hook\nProvide a deterministic time-stepping hook so the Playwright client can advance the game in controlled increments. Expose `window.advanceTime(ms)` (or a thin wrapper that forwards to your game update loop) and have the game loop use it when present.\nThe Playwright test script uses this hook to step frames deterministically during automated testing.\n\nMinimal pattern:\n```js\nwindow.advanceTime = (ms) => {\n  const steps = Math.max(1, Math.round(ms / (1000 / 60)));\n  for (let i = 0; i < steps; i++) update(1 / 60);\n  render();\n};\n```\n\n### Fullscreen Toggle\n- Use a single key (prefer `f`) to toggle fullscreen on/off.\n- Allow `Esc` to exit fullscreen.\n- When fullscreen toggles, resize the canvas/rendering so visuals and input mapping stay correct.\n\n## Progress Tracking\n\nCreate a `progress.md` file if it doesn't exist, and append TODOs, notes, gotchas, and loose ends as you go so another agent can pick up seamlessly.\nIf a `progress.md` file already exists, read it first, including the original user prompt at the top (you may be continuing another agent's work). Do not overwrite the original prompt; preserve it.\nUpdate `progress.md` after each meaningful chunk of work (feature added, bug found, test run, or decision made).\nAt the end of your work, leave TODOs and suggestions for the next agent in `progress.md`.\n\n## Playwright Prerequisites\n\n- Prefer a local `playwright` dependency if the project already has it.\n- If unsure whether Playwright is available, check for `npx`:\n  ```\n  command -v npx >/dev/null 2>&1\n  ```\n- If `npx` is missing, install Node/npm and then install Playwright globally:\n  ```\n  npm install -g @playwright/mcp@latest\n  ```\n- Do not switch to `@playwright/test` unless explicitly asked; stick to the client script.\n\n## Scripts\n\n- `$WEB_GAME_CLIENT` (installed default: `$CODEX_HOME/skills/develop-web-game/scripts/web_game_playwright_client.js`) — Playwright-based action loop with virtual-time stepping, screenshot capture, and console error buffering. You must pass an action burst via `--actions-file`, `--actions-json`, or `--click`.\n\n## References\n\n- `$WEB_GAME_ACTIONS` (installed default: `$CODEX_HOME/skills/develop-web-game/references/action_payloads.json`) — example action payloads (keyboard + mouse, per-frame capture). Use these to build your burst.\n"
  },
  {
    "path": "skills/.curated/develop-web-game/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Develop Web Game\"\n  short_description: \"Web game dev + Playwright test loop\"\n  icon_small: \"./assets/game-small.svg\"\n  icon_large: \"./assets/game.png\"\n  default_prompt: \"Build and iterate a playable web game in this workspace, validating changes with a Playwright loop.\"\n"
  },
  {
    "path": "skills/.curated/develop-web-game/references/action_payloads.json",
    "content": "{\n  \"steps\": [\n    { \"buttons\": [\"left\"], \"frames\": 6 },\n    { \"buttons\": [], \"frames\": 4 },\n    { \"buttons\": [\"space\"], \"frames\": 3 }\n  ]\n}\n"
  },
  {
    "path": "skills/.curated/develop-web-game/scripts/web_game_playwright_client.js",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { chromium } from \"playwright\";\n\nfunction parseArgs(argv) {\n  const args = {\n    url: null,\n    iterations: 3,\n    pauseMs: 250,\n    headless: true,\n    screenshotDir: \"output/web-game\",\n    actionsFile: null,\n    actionsJson: null,\n    click: null,\n    clickSelector: null,\n  };\n  for (let i = 2; i < argv.length; i++) {\n    const arg = argv[i];\n    const next = argv[i + 1];\n    if (arg === \"--url\" && next) {\n      args.url = next;\n      i++;\n    } else if (arg === \"--iterations\" && next) {\n      args.iterations = parseInt(next, 10);\n      i++;\n    } else if (arg === \"--pause-ms\" && next) {\n      args.pauseMs = parseInt(next, 10);\n      i++;\n    } else if (arg === \"--headless\" && next) {\n      args.headless = next !== \"0\" && next !== \"false\";\n      i++;\n    } else if (arg === \"--screenshot-dir\" && next) {\n      args.screenshotDir = next;\n      i++;\n    } else if (arg === \"--actions-file\" && next) {\n      args.actionsFile = next;\n      i++;\n    } else if (arg === \"--actions-json\" && next) {\n      args.actionsJson = next;\n      i++;\n    } else if (arg === \"--click\" && next) {\n      const parts = next.split(\",\").map((v) => parseFloat(v.trim()));\n      if (parts.length === 2 && parts.every((v) => Number.isFinite(v))) {\n        args.click = { x: parts[0], y: parts[1] };\n      }\n      i++;\n    } else if (arg === \"--click-selector\" && next) {\n      args.clickSelector = next;\n      i++;\n    }\n  }\n  if (!args.url) {\n    throw new Error(\"--url is required\");\n  }\n  return args;\n}\n\nconst buttonNameToKey = {\n  up: \"ArrowUp\",\n  down: \"ArrowDown\",\n  left: \"ArrowLeft\",\n  right: \"ArrowRight\",\n  enter: \"Enter\",\n  space: \"Space\",\n  a: \"KeyA\",\n  b: \"KeyB\",\n};\n\nasync function sleep(ms) {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction ensureDir(p) {\n  fs.mkdirSync(p, { recursive: true });\n}\n\nfunction makeVirtualTimeShim() {\n  return `(() => {\n    const pending = new Set();\n    const origSetTimeout = window.setTimeout.bind(window);\n    const origSetInterval = window.setInterval.bind(window);\n    const origRequestAnimationFrame = window.requestAnimationFrame.bind(window);\n\n    window.__vt_pending = pending;\n\n    window.setTimeout = (fn, t, ...rest) => {\n      const task = {};\n      pending.add(task);\n      return origSetTimeout(() => {\n        pending.delete(task);\n        fn(...rest);\n      }, t);\n    };\n\n    window.setInterval = (fn, t, ...rest) => {\n      const task = {};\n      pending.add(task);\n      return origSetInterval(() => {\n        fn(...rest);\n      }, t);\n    };\n\n    window.requestAnimationFrame = (fn) => {\n      const task = {};\n      pending.add(task);\n      return origRequestAnimationFrame((ts) => {\n        pending.delete(task);\n        fn(ts);\n      });\n    };\n\n    window.advanceTime = (ms) => {\n      return new Promise((resolve) => {\n        const start = performance.now();\n        function step(now) {\n          if (now - start >= ms) return resolve();\n          origRequestAnimationFrame(step);\n        }\n        origRequestAnimationFrame(step);\n      });\n    };\n\n    window.__drainVirtualTimePending = () => pending.size;\n  })();`;\n}\n\nasync function getCanvasHandle(page) {\n  const handle = await page.evaluateHandle(() => {\n    let best = null;\n    let bestArea = 0;\n    for (const canvas of document.querySelectorAll(\"canvas\")) {\n      const area = (canvas.width || canvas.clientWidth || 0) * (canvas.height || canvas.clientHeight || 0);\n      if (area > bestArea) {\n        bestArea = area;\n        best = canvas;\n      }\n    }\n    return best;\n  });\n  return handle.asElement();\n}\n\nasync function captureCanvasPngBase64(canvas) {\n  return canvas.evaluate((c) => {\n    if (!c || typeof c.toDataURL !== \"function\") return \"\";\n    const data = c.toDataURL(\"image/png\");\n    const idx = data.indexOf(\",\");\n    return idx === -1 ? \"\" : data.slice(idx + 1);\n  });\n}\n\nasync function isCanvasTransparent(canvas) {\n  if (!canvas) return true;\n  return canvas.evaluate((c) => {\n    try {\n      const w = c.width || c.clientWidth || 0;\n      const h = c.height || c.clientHeight || 0;\n      if (!w || !h) return true;\n      const size = Math.max(1, Math.min(16, w, h));\n      const probe = document.createElement(\"canvas\");\n      probe.width = size;\n      probe.height = size;\n      const ctx = probe.getContext(\"2d\");\n      if (!ctx) return true;\n      ctx.drawImage(c, 0, 0, size, size);\n      const data = ctx.getImageData(0, 0, size, size).data;\n      for (let i = 3; i < data.length; i += 4) {\n        if (data[i] !== 0) return false;\n      }\n      return true;\n    } catch {\n      return false;\n    }\n  });\n}\n\nasync function captureScreenshot(page, canvas, outPath) {\n  let buffer = null;\n  let base64 = canvas ? await captureCanvasPngBase64(canvas) : \"\";\n  if (base64) {\n    buffer = Buffer.from(base64, \"base64\");\n    const transparent = canvas ? await isCanvasTransparent(canvas) : false;\n    if (transparent) buffer = null;\n  }\n  if (!buffer && canvas) {\n    try {\n      buffer = await canvas.screenshot({ type: \"png\" });\n    } catch {\n      buffer = null;\n    }\n  }\n  if (!buffer) {\n    const bbox = canvas ? await canvas.boundingBox() : null;\n    if (bbox) {\n      buffer = await page.screenshot({\n        type: \"png\",\n        omitBackground: false,\n        clip: bbox,\n      });\n    } else {\n      buffer = await page.screenshot({ type: \"png\", omitBackground: false });\n    }\n  }\n  fs.writeFileSync(outPath, buffer);\n}\n\nclass ConsoleErrorTracker {\n  constructor() {\n    this._seen = new Set();\n    this._errors = [];\n  }\n\n  ingest(err) {\n    const key = JSON.stringify(err);\n    if (this._seen.has(key)) return;\n    this._seen.add(key);\n    this._errors.push(err);\n  }\n\n  drain() {\n    const next = [...this._errors];\n    this._errors = [];\n    return next;\n  }\n}\n\nasync function doChoreography(page, canvas, steps) {\n  for (const step of steps) {\n    const buttons = new Set(step.buttons || []);\n    for (const button of buttons) {\n      if (button === \"left_mouse_button\" || button === \"right_mouse_button\") {\n        const bbox = canvas ? await canvas.boundingBox() : null;\n        if (!bbox) continue;\n        const x = typeof step.mouse_x === \"number\" ? step.mouse_x : bbox.width / 2;\n        const y = typeof step.mouse_y === \"number\" ? step.mouse_y : bbox.height / 2;\n        await page.mouse.move(bbox.x + x, bbox.y + y);\n        await page.mouse.down({ button: button === \"left_mouse_button\" ? \"left\" : \"right\" });\n      } else if (buttonNameToKey[button]) {\n        await page.keyboard.down(buttonNameToKey[button]);\n      }\n    }\n\n    const frames = step.frames || 1;\n    for (let i = 0; i < frames; i++) {\n      await page.evaluate(async () => {\n        if (typeof window.advanceTime === \"function\") {\n          await window.advanceTime(1000 / 60);\n        }\n      });\n    }\n\n    for (const button of buttons) {\n      if (button === \"left_mouse_button\" || button === \"right_mouse_button\") {\n        await page.mouse.up({ button: button === \"left_mouse_button\" ? \"left\" : \"right\" });\n      } else if (buttonNameToKey[button]) {\n        await page.keyboard.up(buttonNameToKey[button]);\n      }\n    }\n  }\n}\n\nasync function main() {\n  const args = parseArgs(process.argv);\n  ensureDir(args.screenshotDir);\n\n  const browser = await chromium.launch({\n    headless: args.headless,\n    args: [\"--use-gl=angle\", \"--use-angle=swiftshader\"],\n  });\n  const page = await browser.newPage();\n  const consoleErrors = new ConsoleErrorTracker();\n\n  page.on(\"console\", (msg) => {\n    if (msg.type() !== \"error\") return;\n    consoleErrors.ingest({ type: \"console.error\", text: msg.text() });\n  });\n  page.on(\"pageerror\", (err) => {\n    consoleErrors.ingest({ type: \"pageerror\", text: String(err) });\n  });\n\n  await page.addInitScript({ content: makeVirtualTimeShim() });\n  await page.goto(args.url, { waitUntil: \"domcontentloaded\" });\n  await page.waitForTimeout(500);\n  await page.evaluate(() => {\n    window.dispatchEvent(new Event(\"resize\"));\n  });\n\n  let canvas = await getCanvasHandle(page);\n\n  if (args.clickSelector) {\n    try {\n      await page.click(args.clickSelector, { timeout: 5000 });\n      await page.waitForTimeout(250);\n    } catch (err) {\n      console.warn(\"Failed to click selector\", args.clickSelector, err);\n    }\n  }\n  let steps = null;\n  if (args.actionsFile) {\n    const raw = fs.readFileSync(args.actionsFile, \"utf-8\");\n    const parsed = JSON.parse(raw);\n    if (Array.isArray(parsed)) steps = parsed;\n    if (parsed && Array.isArray(parsed.steps)) steps = parsed.steps;\n  } else if (args.actionsJson) {\n    const parsed = JSON.parse(args.actionsJson);\n    if (Array.isArray(parsed)) steps = parsed;\n    if (parsed && Array.isArray(parsed.steps)) steps = parsed.steps;\n  } else if (args.click) {\n    steps = [\n      {\n        buttons: [\"left_mouse_button\"],\n        frames: 2,\n        mouse_x: args.click.x,\n        mouse_y: args.click.y,\n      },\n    ];\n  }\n  if (!steps) {\n    throw new Error(\"Actions are required. Use --actions-file, --actions-json, or --click.\");\n  }\n\n  for (let i = 0; i < args.iterations; i++) {\n    if (!canvas) canvas = await getCanvasHandle(page);\n    await doChoreography(page, canvas, steps);\n    await sleep(args.pauseMs);\n\n    const shotPath = path.join(args.screenshotDir, `shot-${i}.png`);\n    await captureScreenshot(page, canvas, shotPath);\n\n    const text = await page.evaluate(() => {\n      if (typeof window.render_game_to_text === \"function\") {\n        return window.render_game_to_text();\n      }\n      return null;\n    });\n    if (text) {\n      fs.writeFileSync(path.join(args.screenshotDir, `state-${i}.json`), text);\n    }\n\n    const freshErrors = consoleErrors.drain();\n    if (freshErrors.length) {\n      fs.writeFileSync(\n        path.join(args.screenshotDir, `errors-${i}.json`),\n        JSON.stringify(freshErrors, null, 2)\n      );\n      break;\n    }\n  }\n\n  await browser.close();\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "skills/.curated/doc/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/doc/SKILL.md",
    "content": "---\nname: \"doc\"\ndescription: \"Use when the task involves reading, creating, or editing `.docx` documents, especially when formatting or layout fidelity matters; prefer `python-docx` plus the bundled `scripts/render_docx.py` for visual checks.\"\n---\n\n\n# DOCX Skill\n\n## When to use\n- Read or review DOCX content where layout matters (tables, diagrams, pagination).\n- Create or edit DOCX files with professional formatting.\n- Validate visual layout before delivery.\n\n## Workflow\n1. Prefer visual review (layout, tables, diagrams).\n   - If `soffice` and `pdftoppm` are available, convert DOCX -> PDF -> PNGs.\n   - Or use `scripts/render_docx.py` (requires `pdf2image` and Poppler).\n   - If these tools are missing, install them or ask the user to review rendered pages locally.\n2. Use `python-docx` for edits and structured creation (headings, styles, tables, lists).\n3. After each meaningful change, re-render and inspect the pages.\n4. If visual review is not possible, extract text with `python-docx` as a fallback and call out layout risk.\n5. Keep intermediate outputs organized and clean up after final approval.\n\n## Temp and output conventions\n- Use `tmp/docs/` for intermediate files; delete when done.\n- Write final artifacts under `output/doc/` when working in this repo.\n- Keep filenames stable and descriptive.\n\n## Dependencies (install if missing)\nPrefer `uv` for dependency management.\n\nPython packages:\n```\nuv pip install python-docx pdf2image\n```\nIf `uv` is unavailable:\n```\npython3 -m pip install python-docx pdf2image\n```\nSystem tools (for rendering):\n```\n# macOS (Homebrew)\nbrew install libreoffice poppler\n\n# Ubuntu/Debian\nsudo apt-get install -y libreoffice poppler-utils\n```\n\nIf installation isn't possible in this environment, tell the user which dependency is missing and how to install it locally.\n\n## Environment\nNo required environment variables.\n\n## Rendering commands\nDOCX -> PDF:\n```\nsoffice -env:UserInstallation=file:///tmp/lo_profile_$$ --headless --convert-to pdf --outdir $OUTDIR $INPUT_DOCX\n```\n\nPDF -> PNGs:\n```\npdftoppm -png $OUTDIR/$BASENAME.pdf $OUTDIR/$BASENAME\n```\n\nBundled helper:\n```\npython3 scripts/render_docx.py /path/to/file.docx --output_dir /tmp/docx_pages\n```\n\n## Quality expectations\n- Deliver a client-ready document: consistent typography, spacing, margins, and clear hierarchy.\n- Avoid formatting defects: clipped/overlapping text, broken tables, unreadable characters, or default-template styling.\n- Charts, tables, and visuals must be legible in rendered pages with correct alignment.\n- Use ASCII hyphens only. Avoid U+2011 (non-breaking hyphen) and other Unicode dashes.\n- Citations and references must be human-readable; never leave tool tokens or placeholder strings.\n\n## Final checks\n- Re-render and inspect every page at 100% zoom before final delivery.\n- Fix any spacing, alignment, or pagination issues and repeat the render loop.\n- Confirm there are no leftovers (temp files, duplicate renders) unless the user asks to keep them.\n"
  },
  {
    "path": "skills/.curated/doc/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Word Docs\"\n  short_description: \"Edit and review docx files\"\n  icon_small: \"./assets/doc-small.svg\"\n  icon_large: \"./assets/doc.png\"\n  default_prompt: \"Edit or review this .docx file and return the updated file plus a concise change summary.\"\n"
  },
  {
    "path": "skills/.curated/doc/scripts/render_docx.py",
    "content": "import argparse\nimport os\nimport re\nimport subprocess\nimport tempfile\nimport xml.etree.ElementTree as ET\nfrom os import makedirs, replace\nfrom os.path import abspath, basename, exists, expanduser, join, splitext\nfrom shutil import which\nimport sys\nfrom typing import Sequence, cast\nfrom zipfile import ZipFile\n\nfrom pdf2image import convert_from_path, pdfinfo_from_path\n\nTWIPS_PER_INCH: int = 1440\n\n\ndef ensure_system_tools() -> None:\n    missing: list[str] = []\n    for tool in (\"soffice\", \"pdftoppm\"):\n        if which(tool) is None:\n            missing.append(tool)\n    if missing:\n        tools = \", \".join(missing)\n        raise RuntimeError(\n            f\"Missing required system tool(s): {tools}. Install LibreOffice and Poppler, then retry.\"\n        )\n\n\ndef calc_dpi_via_ooxml_docx(input_path: str, max_w_px: int, max_h_px: int) -> int:\n    \"\"\"Calculate DPI from OOXML `word/document.xml` page size (w:pgSz in twips).\n\n    DOCX stores page dimensions in section properties as twips (1/1440 inch).\n    We read the first encountered section's page size and compute an isotropic DPI\n    that fits within the target max pixel dimensions.\n    \"\"\"\n    with ZipFile(input_path, \"r\") as zf:\n        xml = zf.read(\"word/document.xml\")\n    root = ET.fromstring(xml)\n    ns = {\"w\": \"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"}\n\n    # Common placements: w:body/w:sectPr or w:body/w:p/w:pPr/w:sectPr\n    sect_pr = root.find(\".//w:sectPr\", ns)\n    if sect_pr is None:\n        raise RuntimeError(\"Section properties not found in document.xml\")\n    pg_sz = sect_pr.find(\"w:pgSz\", ns)\n    if pg_sz is None:\n        raise RuntimeError(\"Page size not found in section properties\")\n\n    # Values are in twips\n    w_twips_str = pg_sz.get(\n        \"{http://schemas.openxmlformats.org/wordprocessingml/2006/main}w\"\n    ) or pg_sz.get(\"w\")\n    h_twips_str = pg_sz.get(\n        \"{http://schemas.openxmlformats.org/wordprocessingml/2006/main}h\"\n    ) or pg_sz.get(\"h\")\n\n    if not w_twips_str or not h_twips_str:\n        raise RuntimeError(\"Page size attributes missing in pgSz\")\n\n    width_in = int(w_twips_str) / TWIPS_PER_INCH\n    height_in = int(h_twips_str) / TWIPS_PER_INCH\n    if width_in <= 0 or height_in <= 0:\n        raise RuntimeError(\"Invalid page size values in document.xml\")\n    return round(min(max_w_px / width_in, max_h_px / height_in))\n\n\ndef calc_dpi_via_pdf(input_path: str, max_w_px: int, max_h_px: int) -> int:\n    \"\"\"Convert input to PDF and compute DPI from its page size.\"\"\"\n    with tempfile.TemporaryDirectory(prefix=\"soffice_profile_\") as user_profile:\n        with tempfile.TemporaryDirectory(prefix=\"soffice_convert_\") as convert_tmp_dir:\n            stem = splitext(basename(input_path))[0]\n            pdf_path = convert_to_pdf(input_path, user_profile, convert_tmp_dir, stem)\n            if not (pdf_path and exists(pdf_path)):\n                raise RuntimeError(\"Failed to convert input to PDF for DPI computation.\")\n\n            info = pdfinfo_from_path(pdf_path)\n            size_val = info.get(\"Page size\")\n            if not size_val:\n                for k, v in info.items():\n                    if isinstance(v, str) and \"size\" in k.lower() and \"pts\" in v:\n                        size_val = v\n                        break\n            if not isinstance(size_val, str):\n                raise RuntimeError(\"Failed to read PDF page size for DPI computation.\")\n\n            m = re.search(r\"(\\d+)\\s*x\\s*(\\d+)\\s*pts\", size_val)\n            if not m:\n                raise RuntimeError(\"Unrecognized PDF page size format.\")\n            width_pts = int(m.group(1))\n            height_pts = int(m.group(2))\n            width_in = width_pts / 72.0\n            height_in = height_pts / 72.0\n            if width_in <= 0 or height_in <= 0:\n                raise RuntimeError(\"Invalid PDF page size values.\")\n            return round(min(max_w_px / width_in, max_h_px / height_in))\n\n\ndef run_cmd_no_check(cmd: list[str]) -> None:\n    subprocess.run(\n        cmd,\n        check=False,\n        stdout=subprocess.DEVNULL,\n        stderr=subprocess.DEVNULL,\n        env=os.environ.copy(),\n    )\n\n\ndef convert_to_pdf(\n    doc_path: str,\n    user_profile: str,\n    convert_tmp_dir: str,\n    stem: str,\n) -> str:\n    # Try direct DOC(X) -> PDF\n    cmd_pdf = [\n        \"soffice\",\n        \"-env:UserInstallation=file://\" + user_profile,\n        \"--invisible\",\n        \"--headless\",\n        \"--norestore\",\n        \"--convert-to\",\n        \"pdf\",\n        \"--outdir\",\n        convert_tmp_dir,\n        doc_path,\n    ]\n    run_cmd_no_check(cmd_pdf)\n\n    pdf_path = join(convert_tmp_dir, f\"{stem}.pdf\")\n    if exists(pdf_path):\n        return pdf_path\n\n    # Fallback: DOCX -> ODT, then ODT -> PDF\n    cmd_odt = [\n        \"soffice\",\n        \"-env:UserInstallation=file://\" + user_profile,\n        \"--invisible\",\n        \"--headless\",\n        \"--norestore\",\n        \"--convert-to\",\n        \"odt\",\n        \"--outdir\",\n        convert_tmp_dir,\n        doc_path,\n    ]\n    run_cmd_no_check(cmd_odt)\n\n    odt_path = join(convert_tmp_dir, f\"{stem}.odt\")\n\n    if exists(odt_path):\n        cmd_odt_pdf = [\n            \"soffice\",\n            \"-env:UserInstallation=file://\" + user_profile,\n            \"--invisible\",\n            \"--headless\",\n            \"--norestore\",\n            \"--convert-to\",\n            \"pdf\",\n            \"--outdir\",\n            convert_tmp_dir,\n            odt_path,\n        ]\n        run_cmd_no_check(cmd_odt_pdf)\n        if exists(pdf_path):\n            return pdf_path\n\n    return \"\"\n\n\ndef rasterize(\n    doc_path: str,\n    out_dir: str,\n    dpi: int,\n) -> Sequence[str]:\n    \"\"\"Rasterise DOCX (or similar) to images placed in out_dir and return their paths.\n\n    Images are named as page-<N>.<ext> with pages starting at 1.\n    \"\"\"\n    makedirs(out_dir, exist_ok=True)\n    doc_path = abspath(doc_path)\n    stem = splitext(basename(doc_path))[0]\n\n    # Use a unique user profile to avoid LibreOffice profile lock when running concurrently\n    with tempfile.TemporaryDirectory(prefix=\"soffice_profile_\") as user_profile:\n        # Write conversion outputs into a temp directory to avoid any IO oddities\n        with tempfile.TemporaryDirectory(prefix=\"soffice_convert_\") as convert_tmp_dir:\n            pdf_path = convert_to_pdf(\n                doc_path,\n                user_profile,\n                convert_tmp_dir,\n                stem,\n            )\n\n            if not pdf_path or not exists(pdf_path):\n                raise RuntimeError(\n                    \"Failed to produce PDF for rasterization (direct and ODT fallback).\"\n                )\n            paths_raw = cast(\n                list[str],\n                convert_from_path(\n                    pdf_path,\n                    dpi=dpi,\n                    fmt=\"png\",\n                    thread_count=8,\n                    output_folder=out_dir,\n                    paths_only=True,\n                    output_file=\"page\",\n                ),\n            )\n\n    # Rename convert_from_path's output format f'page{thread_id:04d}-{page_num:02d}.<ext>' to 'page-<num>.<ext>'\n    pages: list[tuple[int, str]] = []\n    for src_path in paths_raw:\n        base = splitext(basename(src_path))[0]\n        page_num_str = base.split(\"-\")[-1]\n        page_num = int(page_num_str)\n        dst_path = join(out_dir, f\"page-{page_num}.png\")\n        replace(src_path, dst_path)\n        pages.append((page_num, dst_path))\n    pages.sort(key=lambda t: t[0])\n    final_paths = [path for _, path in pages]\n    return final_paths\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(description=\"Render DOCX-like file to PNG images.\")\n    parser.add_argument(\n        \"input_path\",\n        type=str,\n        help=\"Path to the input DOCX file (or compatible).\",\n    )\n    parser.add_argument(\n        \"--output_dir\",\n        type=str,\n        default=None,\n        help=(\n            \"Output directory for the rendered images. \"\n            \"Defaults to a folder next to the input named after the input file (without extension).\"\n        ),\n    )\n    parser.add_argument(\n        \"--width\",\n        type=int,\n        default=1600,\n        help=(\n            \"Approximate maximum width in pixels after isotropic scaling (default 1600). \"\n            \"The actual value may exceed slightly.\"\n        ),\n    )\n    parser.add_argument(\n        \"--height\",\n        type=int,\n        default=2000,\n        help=(\n            \"Approximate maximum height in pixels after isotropic scaling (default 2000). \"\n            \"The actual value may exceed slightly.\"\n        ),\n    )\n    parser.add_argument(\n        \"--dpi\",\n        type=int,\n        default=None,\n        help=(\"Override computed DPI. If provided, skips DOCX/PDF-based DPI calculation.\"),\n    )\n    args = parser.parse_args()\n\n    try:\n        ensure_system_tools()\n\n        input_path = abspath(expanduser(args.input_path))\n        out_dir = (\n            abspath(expanduser(args.output_dir)) if args.output_dir else splitext(input_path)[0]\n        )\n\n        if args.dpi is not None:\n            dpi = int(args.dpi)\n        else:\n            try:\n                if input_path.lower().endswith((\".docx\", \".docm\", \".dotx\", \".dotm\")):\n                    dpi = calc_dpi_via_ooxml_docx(input_path, args.width, args.height)\n                else:\n                    raise RuntimeError(\"Skip OOXML DPI; not a DOCX container\")\n            except Exception:\n                dpi = calc_dpi_via_pdf(input_path, args.width, args.height)\n\n        rasterize(input_path, out_dir, dpi)\n        print(\"Pages rendered to \" + out_dir)\n    except RuntimeError as exc:\n        print(f\"Error: {exc}\", file=sys.stderr)\n        raise SystemExit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/figma/LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "skills/.curated/figma/SKILL.md",
    "content": "---\nname: figma\ndescription: Use the Figma MCP server to fetch design context, screenshots, variables, and assets from Figma, and to translate Figma nodes into production code. Trigger when a task involves Figma URLs, node IDs, design-to-code implementation, or Figma MCP setup and troubleshooting.\n---\n\n# Figma MCP\n\nUse the Figma MCP server for Figma-driven implementation. For setup and debugging details (env vars, config, verification), see `references/figma-mcp-config.md`.\n\n## Figma MCP Integration Rules\nThese rules define how to translate Figma inputs into code for this project and must be followed for every Figma-driven change.\n\n### Required flow (do not skip)\n1. Run get_design_context first to fetch the structured representation for the exact node(s).\n2. If the response is too large or truncated, run get_metadata to get the high-level node map and then re-fetch only the required node(s) with get_design_context.\n3. Run get_screenshot for a visual reference of the node variant being implemented.\n4. Only after you have both get_design_context and get_screenshot, download any assets needed and start implementation.\n5. Translate the output (usually React + Tailwind) into this project's conventions, styles and framework. Reuse the project's color tokens, components, and typography wherever possible.\n6. Validate against Figma for 1:1 look and behavior before marking complete.\n\n### Implementation rules\n- Treat the Figma MCP output (React + Tailwind) as a representation of design and behavior, not as final code style.\n- Replace Tailwind utility classes with the project's preferred utilities/design-system tokens when applicable.\n- Reuse existing components (e.g., buttons, inputs, typography, icon wrappers) instead of duplicating functionality.\n- Use the project's color system, typography scale, and spacing tokens consistently.\n- Respect existing routing, state management, and data-fetch patterns already adopted in the repo.\n- Strive for 1:1 visual parity with the Figma design. When conflicts arise, prefer design-system tokens and adjust spacing or sizes minimally to match visuals.\n- Validate the final UI against the Figma screenshot for both look and behavior.\n\n### Asset handling\n- The Figma MCP Server provides an assets endpoint which can serve image and SVG assets.\n- IMPORTANT: If the Figma MCP Server returns a localhost source for an image or an SVG, use that image or SVG source directly.\n- IMPORTANT: DO NOT import/add new icon packages, all the assets should be in the Figma payload.\n- IMPORTANT: do NOT use or create placeholders if a localhost source is provided.\n\n### Link-based prompting\n- The server is link-based: copy the Figma frame/layer link and give that URL to the MCP client when asking for implementation help.\n- The client cannot browse the URL but extracts the node ID from the link; always ensure the link points to the exact node/variant you want.\n\n## References\n- `references/figma-mcp-config.md` — setup, verification, troubleshooting, and link-based usage reminders.\n- `references/figma-tools-and-prompts.md` — tool catalog and prompt patterns for selecting frameworks/components and fetching metadata.\n"
  },
  {
    "path": "skills/.curated/figma/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Figma\"\n  short_description: \"Use Figma MCP for design-to-code work\"\n  icon_small: \"./assets/figma-small.svg\"\n  icon_large: \"./assets/figma.png\"\n  default_prompt: \"Use Figma MCP to inspect the target design and translate it into implementable UI decisions.\"\n\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"figma\"\n      description: \"Figma MCP server\"\n      transport: \"streamable_http\"\n      url: \"https://mcp.figma.com/mcp\"\n"
  },
  {
    "path": "skills/.curated/figma/references/figma-mcp-config.md",
    "content": "# Figma MCP config reference\n\nUse this snippet to register the Figma MCP server in `~/.codex/config.toml` as a streamable HTTP server with bearer auth pulled from your env.\n\n```toml\n[mcp_servers.figma]\nurl = \"https://mcp.figma.com/mcp\"\nbearer_token_env_var = \"FIGMA_OAUTH_TOKEN\"\nhttp_headers = { \"X-Figma-Region\" = \"us-east-1\" }\n```\n\n## Notes and options\n- The bearer token must be available as `FIGMA_OAUTH_TOKEN` in the environment that launches Codex.\n- Keep the region header aligned with your Figma region. If your org uses another region, update `X-Figma-Region` consistently.\n- OAuth on streamable HTTP requires the RMCP client: set `[features].rmcp_client = true` (or `experimental_use_rmcp_client = true` on older builds) at the top level of `config.toml`.\n- Optional per-server timeouts: `startup_timeout_sec` (default 10) and `tool_timeout_sec` (default 60) can be set inside `[mcp_servers.figma]` if needed.\n\n## Env var setup (if missing)\n- One-time set for current shell: `export FIGMA_OAUTH_TOKEN=\"<token>\"`\n- Persist for future sessions: add the export line to your shell profile (e.g., `~/.zshrc` or `~/.bashrc`), then restart the shell or your IDE.\n- Verify before launching Codex: `echo $FIGMA_OAUTH_TOKEN` should print a non-empty token.\n\n## Setup + verification checklist\n- Add the snippet above to `~/.codex/config.toml` under `[mcp_servers.figma]`, and enable `[features].rmcp_client = true` (or `experimental_use_rmcp_client = true` on older releases).\n- Restart Codex (CLI/IDE) after updating config and env vars.\n- Ask Codex to list Figma tools or run a simple call to confirm the server is reachable.\n\n## Troubleshooting\n- Token not picked up: Export `FIGMA_OAUTH_TOKEN` in the same shell that launches Codex, or add it to your shell profile and restart.\n- OAuth errors: Verify `rmcp_client` is enabled and the bearer token is valid. Tokens copied from Figma should not include surrounding quotes.\n- Network/headers: Keep the `X-Figma-Region` header; if your org uses another region, update the header consistently across config and requests.\n\n## Usage reminders\n- The server is link-based: copy the Figma frame or layer link, then ask the MCP client to implement that URL. The client will extract the node ID from the link (it does not browse the page).\n- If output feels generic, restate the project-specific rules from the main skill and ensure you follow the required flow (get_design_context → get_metadata if needed → get_screenshot).\n"
  },
  {
    "path": "skills/.curated/figma/references/figma-tools-and-prompts.md",
    "content": "# Figma MCP tools and prompt patterns\n\nQuick reference for the Figma MCP toolset, when to use each tool, and prompt examples to steer output toward your stack.\n\n## Core tools\n- `get_design_context` (Figma Design, Figma Make): Primary tool. Returns structured design data and default React + Tailwind code. Selection-based prompting works on desktop; the remote server uses a frame/layer link to extract the node ID.\n- `get_variable_defs` (Figma Design): Lists variables/styles (colors, spacing, typography) used in the selection. Useful to align with tokens.\n- `get_metadata` (Figma Design): Sparse XML outline of layer IDs/names/types/positions/sizes. Use before re-calling `get_design_context` on large nodes to avoid truncation.\n- `get_screenshot` (Figma Design, FigJam): Screenshot of the selection for visual fidelity checks.\n- `get_figjam` (FigJam): XML + screenshots for FigJam diagrams (architecture, flows).\n- `create_design_system_rules` (no file context): Generates a rule file with design-to-code guidance for your stack. Save it where the agent can read it.\n- `get_code_connect_map` (Figma Design): Returns mapping of Figma node IDs to code components (`codeConnectSrc`, `codeConnectName`). Use to reuse existing components.\n- `add_code_connect_map` (Figma Design): Adds/updates a mapping between a Figma node and a code component to improve reuse.\n- `get_strategy_for_mapping` (alpha, local only): Figma-prompted tool to decide mapping strategy for connecting a node to a code component.\n- `send_get_strategy_response` (alpha, local only): Sends the response after `get_strategy_for_mapping`.\n- `whoami` (remote only): Returns the authenticated Figma user identity (email, plans, seat types).\n\n## Prompt patterns (design context)\n- Change framework: “generate my Figma selection in Vue” or “in plain HTML + CSS” or “for iOS”.\n- Use my components: “generate my Figma selection using components from `src/components/ui`”.\n- Combine: “generate my Figma selection using components from `src/ui` and style with Tailwind”.\n- Note: On the remote server, selection-based prompting requires a frame/layer link; the server extracts the node ID from the URL.\n\n## Prompt patterns (variables/styles)\n- “get the variables used in my Figma selection”\n- “what color and spacing variables are used in my Figma selection?”\n- “list the variable names and their values used in my Figma selection”\n\n## Prompt patterns (code connect)\n- “show the code connect map for this selection”\n- “map this node to `src/components/ui/Button.tsx` with name `Button`”\n\n## Best-practice flow reminder\nUse `get_design_context` → (optionally `get_metadata` for large nodes) → `get_screenshot`, and keep project rules from `SKILL.md` in mind when applying the generated output.\n"
  },
  {
    "path": "skills/.curated/figma-implement-design/LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "skills/.curated/figma-implement-design/SKILL.md",
    "content": "---\nname: \"figma-implement-design\"\ndescription: \"Translate Figma nodes into production-ready code with 1:1 visual fidelity using the Figma MCP workflow (design context, screenshots, assets, and project-convention translation). Trigger when the user provides Figma URLs or node IDs, or asks to implement designs or components that must match Figma specs. Requires a working Figma MCP server connection.\"\n---\n\n\n# Implement Design\n\n## Overview\n\nThis skill provides a structured workflow for translating Figma designs into production-ready code with pixel-perfect accuracy. It ensures consistent integration with the Figma MCP server, proper use of design tokens, and 1:1 visual parity with designs.\n\n## Prerequisites\n\n- Figma MCP server must be connected and accessible\n- User must provide a Figma URL in the format: `https://figma.com/design/:fileKey/:fileName?node-id=1-2`\n  - `:fileKey` is the file key\n  - `1-2` is the node ID (the specific component or frame to implement)\n- **OR** when using `figma-desktop` MCP: User can select a node directly in the Figma desktop app (no URL required)\n- Project should have an established design system or component library (preferred)\n\n## Required Workflow\n\n**Follow these steps in order. Do not skip steps.**\n\n### Step 0: Set up Figma MCP (if not already configured)\n\nIf any MCP call fails because Figma MCP is not connected, pause and set it up:\n\n1. Add the Figma MCP:\n   - `codex mcp add figma --url https://mcp.figma.com/mcp`\n2. Enable remote MCP client:\n   - Set `[features].rmcp_client = true` in `config.toml` **or** run `codex --enable rmcp_client`\n3. Log in with OAuth:\n   - `codex mcp login figma`\n\nAfter successful login, the user will have to restart codex. You should finish your answer and tell them so when they try again they can continue with Step 1.\n\n### Step 1: Get Node ID\n\n#### Option A: Parse from Figma URL\n\nWhen the user provides a Figma URL, extract the file key and node ID to pass as arguments to MCP tools.\n\n**URL format:** `https://figma.com/design/:fileKey/:fileName?node-id=1-2`\n\n**Extract:**\n\n- **File key:** `:fileKey` (the segment after `/design/`)\n- **Node ID:** `1-2` (the value of the `node-id` query parameter)\n\n**Note:** When using the local desktop MCP (`figma-desktop`), `fileKey` is not passed as a parameter to tool calls. The server automatically uses the currently open file, so only `nodeId` is needed.\n\n**Example:**\n\n- URL: `https://figma.com/design/kL9xQn2VwM8pYrTb4ZcHjF/DesignSystem?node-id=42-15`\n- File key: `kL9xQn2VwM8pYrTb4ZcHjF`\n- Node ID: `42-15`\n\n#### Option B: Use Current Selection from Figma Desktop App (figma-desktop MCP only)\n\nWhen using the `figma-desktop` MCP and the user has NOT provided a URL, the tools automatically use the currently selected node from the open Figma file in the desktop app.\n\n**Note:** Selection-based prompting only works with the `figma-desktop` MCP server. The remote server requires a link to a frame or layer to extract context. The user must have the Figma desktop app open with a node selected.\n\n### Step 2: Fetch Design Context\n\nRun `get_design_context` with the extracted file key and node ID.\n\n```\nget_design_context(fileKey=\":fileKey\", nodeId=\"1-2\")\n```\n\nThis provides the structured data including:\n\n- Layout properties (Auto Layout, constraints, sizing)\n- Typography specifications\n- Color values and design tokens\n- Component structure and variants\n- Spacing and padding values\n\n**If the response is too large or truncated:**\n\n1. Run `get_metadata(fileKey=\":fileKey\", nodeId=\"1-2\")` to get the high-level node map\n2. Identify the specific child nodes needed from the metadata\n3. Fetch individual child nodes with `get_design_context(fileKey=\":fileKey\", nodeId=\":childNodeId\")`\n\n### Step 3: Capture Visual Reference\n\nRun `get_screenshot` with the same file key and node ID for a visual reference.\n\n```\nget_screenshot(fileKey=\":fileKey\", nodeId=\"1-2\")\n```\n\nThis screenshot serves as the source of truth for visual validation. Keep it accessible throughout implementation.\n\n### Step 4: Download Required Assets\n\nDownload any assets (images, icons, SVGs) returned by the Figma MCP server.\n\n**IMPORTANT:** Follow these asset rules:\n\n- If the Figma MCP server returns a `localhost` source for an image or SVG, use that source directly\n- DO NOT import or add new icon packages - all assets should come from the Figma payload\n- DO NOT use or create placeholders if a `localhost` source is provided\n- Assets are served through the Figma MCP server's built-in assets endpoint\n\n### Step 5: Translate to Project Conventions\n\nTranslate the Figma output into this project's framework, styles, and conventions.\n\n**Key principles:**\n\n- Treat the Figma MCP output (typically React + Tailwind) as a representation of design and behavior, not as final code style\n- Replace Tailwind utility classes with the project's preferred utilities or design system tokens\n- Reuse existing components (buttons, inputs, typography, icon wrappers) instead of duplicating functionality\n- Use the project's color system, typography scale, and spacing tokens consistently\n- Respect existing routing, state management, and data-fetch patterns\n\n### Step 6: Achieve 1:1 Visual Parity\n\nStrive for pixel-perfect visual parity with the Figma design.\n\n**Guidelines:**\n\n- Prioritize Figma fidelity to match designs exactly\n- Avoid hardcoded values - use design tokens from Figma where available\n- When conflicts arise between design system tokens and Figma specs, prefer design system tokens but adjust spacing or sizes minimally to match visuals\n- Follow WCAG requirements for accessibility\n- Add component documentation as needed\n\n### Step 7: Validate Against Figma\n\nBefore marking complete, validate the final UI against the Figma screenshot.\n\n**Validation checklist:**\n\n- [ ] Layout matches (spacing, alignment, sizing)\n- [ ] Typography matches (font, size, weight, line height)\n- [ ] Colors match exactly\n- [ ] Interactive states work as designed (hover, active, disabled)\n- [ ] Responsive behavior follows Figma constraints\n- [ ] Assets render correctly\n- [ ] Accessibility standards met\n\n## Implementation Rules\n\n### Component Organization\n\n- Place UI components in the project's designated design system directory\n- Follow the project's component naming conventions\n- Avoid inline styles unless truly necessary for dynamic values\n\n### Design System Integration\n\n- ALWAYS use components from the project's design system when possible\n- Map Figma design tokens to project design tokens\n- When a matching component exists, extend it rather than creating a new one\n- Document any new components added to the design system\n\n### Code Quality\n\n- Avoid hardcoded values - extract to constants or design tokens\n- Keep components composable and reusable\n- Add TypeScript types for component props\n- Include JSDoc comments for exported components\n\n## Examples\n\n### Example 1: Implementing a Button Component\n\nUser says: \"Implement this Figma button component: https://figma.com/design/kL9xQn2VwM8pYrTb4ZcHjF/DesignSystem?node-id=42-15\"\n\n**Actions:**\n\n1. Parse URL to extract fileKey=`kL9xQn2VwM8pYrTb4ZcHjF` and nodeId=`42-15`\n2. Run `get_design_context(fileKey=\"kL9xQn2VwM8pYrTb4ZcHjF\", nodeId=\"42-15\")`\n3. Run `get_screenshot(fileKey=\"kL9xQn2VwM8pYrTb4ZcHjF\", nodeId=\"42-15\")` for visual reference\n4. Download any button icons from the assets endpoint\n5. Check if project has existing button component\n6. If yes, extend it with new variant; if no, create new component using project conventions\n7. Map Figma colors to project design tokens (e.g., `primary-500`, `primary-hover`)\n8. Validate against screenshot for padding, border radius, typography\n\n**Result:** Button component matching Figma design, integrated with project design system.\n\n### Example 2: Building a Dashboard Layout\n\nUser says: \"Build this dashboard: https://figma.com/design/pR8mNv5KqXzGwY2JtCfL4D/Dashboard?node-id=10-5\"\n\n**Actions:**\n\n1. Parse URL to extract fileKey=`pR8mNv5KqXzGwY2JtCfL4D` and nodeId=`10-5`\n2. Run `get_metadata(fileKey=\"pR8mNv5KqXzGwY2JtCfL4D\", nodeId=\"10-5\")` to understand the page structure\n3. Identify main sections from metadata (header, sidebar, content area, cards) and their child node IDs\n4. Run `get_design_context(fileKey=\"pR8mNv5KqXzGwY2JtCfL4D\", nodeId=\":childNodeId\")` for each major section\n5. Run `get_screenshot(fileKey=\"pR8mNv5KqXzGwY2JtCfL4D\", nodeId=\"10-5\")` for the full page\n6. Download all assets (logos, icons, charts)\n7. Build layout using project's layout primitives\n8. Implement each section using existing components where possible\n9. Validate responsive behavior against Figma constraints\n\n**Result:** Complete dashboard matching Figma design with responsive layout.\n\n## Best Practices\n\n### Always Start with Context\n\nNever implement based on assumptions. Always fetch `get_design_context` and `get_screenshot` first.\n\n### Incremental Validation\n\nValidate frequently during implementation, not just at the end. This catches issues early.\n\n### Document Deviations\n\nIf you must deviate from the Figma design (e.g., for accessibility or technical constraints), document why in code comments.\n\n### Reuse Over Recreation\n\nAlways check for existing components before creating new ones. Consistency across the codebase is more important than exact Figma replication.\n\n### Design System First\n\nWhen in doubt, prefer the project's design system patterns over literal Figma translation.\n\n## Common Issues and Solutions\n\n### Issue: Figma output is truncated\n\n**Cause:** The design is too complex or has too many nested layers to return in a single response.\n**Solution:** Use `get_metadata` to get the node structure, then fetch specific nodes individually with `get_design_context`.\n\n### Issue: Design doesn't match after implementation\n\n**Cause:** Visual discrepancies between the implemented code and the original Figma design.\n**Solution:** Compare side-by-side with the screenshot from Step 3. Check spacing, colors, and typography values in the design context data.\n\n### Issue: Assets not loading\n\n**Cause:** The Figma MCP server's assets endpoint is not accessible or the URLs are being modified.\n**Solution:** Verify the Figma MCP server's assets endpoint is accessible. The server serves assets at `localhost` URLs. Use these directly without modification.\n\n### Issue: Design token values differ from Figma\n\n**Cause:** The project's design system tokens have different values than those specified in the Figma design.\n**Solution:** When project tokens differ from Figma values, prefer project tokens for consistency but adjust spacing/sizing to maintain visual fidelity.\n\n## Understanding Design Implementation\n\nThe Figma implementation workflow establishes a reliable process for translating designs to code:\n\n**For designers:** Confidence that implementations will match their designs with pixel-perfect accuracy.\n**For developers:** A structured approach that eliminates guesswork and reduces back-and-forth revisions.\n**For teams:** Consistent, high-quality implementations that maintain design system integrity.\n\nBy following this workflow, you ensure that every Figma design is implemented with the same level of care and attention to detail.\n\n## Additional Resources\n\n- [Figma MCP Server Documentation](https://developers.figma.com/docs/figma-mcp-server/)\n- [Figma MCP Server Tools and Prompts](https://developers.figma.com/docs/figma-mcp-server/tools-and-prompts/)\n- [Figma Variables and Design Tokens](https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma)\n"
  },
  {
    "path": "skills/.curated/figma-implement-design/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Figma Implement Design\"\n  short_description: \"Turn Figma designs into production-ready code\"\n  icon_small: \"./assets/figma-small.svg\"\n  icon_large: \"./assets/figma.png\"\n  default_prompt: \"Implement this Figma design in this codebase, matching layout, states, and responsive behavior.\"\n\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"figma\"\n      description: \"Figma MCP server\"\n      transport: \"streamable_http\"\n      url: \"https://mcp.figma.com/mcp\"\n"
  },
  {
    "path": "skills/.curated/gh-address-comments/LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "skills/.curated/gh-address-comments/SKILL.md",
    "content": "---\nname: gh-address-comments\ndescription: Help address review/issue comments on the open GitHub PR for the current branch using gh CLI; verify gh auth first and prompt the user to authenticate if not logged in.\nmetadata:\n  short-description: Address comments in a GitHub PR review\n---\n\n# PR Comment Handler\n\nGuide to find the open PR for the current branch and address its comments with gh CLI. Run all `gh` commands with elevated network access.\n\nPrereq: ensure `gh` is authenticated (for example, run `gh auth login` once), then run `gh auth status` with escalated permissions (include workflow/repo scopes) so `gh` commands succeed. If sandboxing blocks `gh auth status`, rerun it with `sandbox_permissions=require_escalated`.\n\n## 1) Inspect comments needing attention\n- Run scripts/fetch_comments.py which will print out all the comments and review threads on the PR\n\n## 2) Ask the user for clarification\n- Number all the review threads and comments and provide a short summary of what would be required to apply a fix for it\n- Ask the user which numbered comments should be addressed\n\n## 3) If user chooses comments\n- Apply fixes for the selected comments\n\nNotes:\n- If gh hits auth/rate issues mid-run, prompt the user to re-authenticate with `gh auth login`, then retry.\n"
  },
  {
    "path": "skills/.curated/gh-address-comments/agents/openai.yaml",
    "content": "interface:\n  display_name: \"GitHub Address Comments\"\n  short_description: Address comments in a GitHub PR review\"\n  icon_small: \"./assets/github-small.svg\"\n  icon_large: \"./assets/github.png\"\n  default_prompt: \"Address all actionable GitHub PR review comments in this branch and summarize the updates.\"\n"
  },
  {
    "path": "skills/.curated/gh-address-comments/scripts/fetch_comments.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nFetch all PR conversation comments + reviews + review threads (inline threads)\nfor the PR associated with the current git branch, by shelling out to:\n\n  gh api graphql\n\nRequires:\n  - `gh auth login` already set up\n  - current branch has an associated (open) PR\n\nUsage:\n  python fetch_comments.py > pr_comments.json\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport subprocess\nimport sys\nfrom typing import Any\n\nQUERY = \"\"\"\\\nquery(\n  $owner: String!,\n  $repo: String!,\n  $number: Int!,\n  $commentsCursor: String,\n  $reviewsCursor: String,\n  $threadsCursor: String\n) {\n  repository(owner: $owner, name: $repo) {\n    pullRequest(number: $number) {\n      number\n      url\n      title\n      state\n\n      # Top-level \"Conversation\" comments (issue comments on the PR)\n      comments(first: 100, after: $commentsCursor) {\n        pageInfo { hasNextPage endCursor }\n        nodes {\n          id\n          body\n          createdAt\n          updatedAt\n          author { login }\n        }\n      }\n\n      # Review submissions (Approve / Request changes / Comment), with body if present\n      reviews(first: 100, after: $reviewsCursor) {\n        pageInfo { hasNextPage endCursor }\n        nodes {\n          id\n          state\n          body\n          submittedAt\n          author { login }\n        }\n      }\n\n      # Inline review threads (grouped), includes resolved state\n      reviewThreads(first: 100, after: $threadsCursor) {\n        pageInfo { hasNextPage endCursor }\n        nodes {\n          id\n          isResolved\n          isOutdated\n          path\n          line\n          diffSide\n          startLine\n          startDiffSide\n          originalLine\n          originalStartLine\n          resolvedBy { login }\n          comments(first: 100) {\n            nodes {\n              id\n              body\n              createdAt\n              updatedAt\n              author { login }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\"\"\"\n\n\ndef _run(cmd: list[str], stdin: str | None = None) -> str:\n    p = subprocess.run(cmd, input=stdin, capture_output=True, text=True)\n    if p.returncode != 0:\n        raise RuntimeError(f\"Command failed: {' '.join(cmd)}\\n{p.stderr}\")\n    return p.stdout\n\n\ndef _run_json(cmd: list[str], stdin: str | None = None) -> dict[str, Any]:\n    out = _run(cmd, stdin=stdin)\n    try:\n        return json.loads(out)\n    except json.JSONDecodeError as e:\n        raise RuntimeError(f\"Failed to parse JSON from command output: {e}\\nRaw:\\n{out}\") from e\n\n\ndef _ensure_gh_authenticated() -> None:\n    try:\n        _run([\"gh\", \"auth\", \"status\"])\n    except RuntimeError:\n        print(\"run `gh auth login` to authenticate the GitHub CLI\", file=sys.stderr)\n        raise RuntimeError(\"gh auth status failed; run `gh auth login` to authenticate the GitHub CLI\") from None\n\n\ndef gh_pr_view_json(fields: str) -> dict[str, Any]:\n    # fields is a comma-separated list like: \"number,headRepositoryOwner,headRepository\"\n    return _run_json([\"gh\", \"pr\", \"view\", \"--json\", fields])\n\n\ndef get_current_pr_ref() -> tuple[str, str, int]:\n    \"\"\"\n    Resolve the PR for the current branch (whatever gh considers associated).\n    Works for cross-repo PRs too, by reading head repository owner/name.\n    \"\"\"\n    pr = gh_pr_view_json(\"number,headRepositoryOwner,headRepository\")\n    owner = pr[\"headRepositoryOwner\"][\"login\"]\n    repo = pr[\"headRepository\"][\"name\"]\n    number = int(pr[\"number\"])\n    return owner, repo, number\n\n\ndef gh_api_graphql(\n    owner: str,\n    repo: str,\n    number: int,\n    comments_cursor: str | None = None,\n    reviews_cursor: str | None = None,\n    threads_cursor: str | None = None,\n) -> dict[str, Any]:\n    \"\"\"\n    Call `gh api graphql` using -F variables, avoiding JSON blobs with nulls.\n    Query is passed via stdin using query=@- to avoid shell newline/quoting issues.\n    \"\"\"\n    cmd = [\n        \"gh\",\n        \"api\",\n        \"graphql\",\n        \"-F\",\n        \"query=@-\",\n        \"-F\",\n        f\"owner={owner}\",\n        \"-F\",\n        f\"repo={repo}\",\n        \"-F\",\n        f\"number={number}\",\n    ]\n    if comments_cursor:\n        cmd += [\"-F\", f\"commentsCursor={comments_cursor}\"]\n    if reviews_cursor:\n        cmd += [\"-F\", f\"reviewsCursor={reviews_cursor}\"]\n    if threads_cursor:\n        cmd += [\"-F\", f\"threadsCursor={threads_cursor}\"]\n\n    return _run_json(cmd, stdin=QUERY)\n\n\ndef fetch_all(owner: str, repo: str, number: int) -> dict[str, Any]:\n    conversation_comments: list[dict[str, Any]] = []\n    reviews: list[dict[str, Any]] = []\n    review_threads: list[dict[str, Any]] = []\n\n    comments_cursor: str | None = None\n    reviews_cursor: str | None = None\n    threads_cursor: str | None = None\n\n    pr_meta: dict[str, Any] | None = None\n\n    while True:\n        payload = gh_api_graphql(\n            owner=owner,\n            repo=repo,\n            number=number,\n            comments_cursor=comments_cursor,\n            reviews_cursor=reviews_cursor,\n            threads_cursor=threads_cursor,\n        )\n\n        if \"errors\" in payload and payload[\"errors\"]:\n            raise RuntimeError(f\"GitHub GraphQL errors:\\n{json.dumps(payload['errors'], indent=2)}\")\n\n        pr = payload[\"data\"][\"repository\"][\"pullRequest\"]\n        if pr_meta is None:\n            pr_meta = {\n                \"number\": pr[\"number\"],\n                \"url\": pr[\"url\"],\n                \"title\": pr[\"title\"],\n                \"state\": pr[\"state\"],\n                \"owner\": owner,\n                \"repo\": repo,\n            }\n\n        c = pr[\"comments\"]\n        r = pr[\"reviews\"]\n        t = pr[\"reviewThreads\"]\n\n        conversation_comments.extend(c.get(\"nodes\") or [])\n        reviews.extend(r.get(\"nodes\") or [])\n        review_threads.extend(t.get(\"nodes\") or [])\n\n        comments_cursor = c[\"pageInfo\"][\"endCursor\"] if c[\"pageInfo\"][\"hasNextPage\"] else None\n        reviews_cursor = r[\"pageInfo\"][\"endCursor\"] if r[\"pageInfo\"][\"hasNextPage\"] else None\n        threads_cursor = t[\"pageInfo\"][\"endCursor\"] if t[\"pageInfo\"][\"hasNextPage\"] else None\n\n        if not (comments_cursor or reviews_cursor or threads_cursor):\n            break\n\n    assert pr_meta is not None\n    return {\n        \"pull_request\": pr_meta,\n        \"conversation_comments\": conversation_comments,\n        \"reviews\": reviews,\n        \"review_threads\": review_threads,\n    }\n\n\ndef main() -> None:\n    _ensure_gh_authenticated()\n    owner, repo, number = get_current_pr_ref()\n    result = fetch_all(owner, repo, number)\n    print(json.dumps(result, indent=2))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/gh-fix-ci/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/gh-fix-ci/SKILL.md",
    "content": "---\nname: \"gh-fix-ci\"\ndescription: \"Use when a user asks to debug or fix failing GitHub PR checks that run in GitHub Actions; use `gh` to inspect checks and logs, summarize failure context, draft a fix plan, and implement only after explicit approval. Treat external providers (for example Buildkite) as out of scope and report only the details URL.\"\n---\n\n\n# Gh Pr Checks Plan Fix\n\n## Overview\n\nUse gh to locate failing PR checks, fetch GitHub Actions logs for actionable failures, summarize the failure snippet, then propose a fix plan and implement after explicit approval.\n- If a plan-oriented skill (for example `create-plan`) is available, use it; otherwise draft a concise plan inline and request approval before implementing.\n\nPrereq: authenticate with the standard GitHub CLI once (for example, run `gh auth login`), then confirm with `gh auth status` (repo + workflow scopes are typically required).\n\n## Inputs\n\n- `repo`: path inside the repo (default `.`)\n- `pr`: PR number or URL (optional; defaults to current branch PR)\n- `gh` authentication for the repo host\n\n## Quick start\n\n- `python \"<path-to-skill>/scripts/inspect_pr_checks.py\" --repo \".\" --pr \"<number-or-url>\"`\n- Add `--json` if you want machine-friendly output for summarization.\n\n## Workflow\n\n1. Verify gh authentication.\n   - Run `gh auth status` in the repo.\n   - If unauthenticated, ask the user to run `gh auth login` (ensuring repo + workflow scopes) before proceeding.\n2. Resolve the PR.\n   - Prefer the current branch PR: `gh pr view --json number,url`.\n   - If the user provides a PR number or URL, use that directly.\n3. Inspect failing checks (GitHub Actions only).\n   - Preferred: run the bundled script (handles gh field drift and job-log fallbacks):\n     - `python \"<path-to-skill>/scripts/inspect_pr_checks.py\" --repo \".\" --pr \"<number-or-url>\"`\n     - Add `--json` for machine-friendly output.\n   - Manual fallback:\n     - `gh pr checks <pr> --json name,state,bucket,link,startedAt,completedAt,workflow`\n       - If a field is rejected, rerun with the available fields reported by `gh`.\n     - For each failing check, extract the run id from `detailsUrl` and run:\n       - `gh run view <run_id> --json name,workflowName,conclusion,status,url,event,headBranch,headSha`\n       - `gh run view <run_id> --log`\n     - If the run log says it is still in progress, fetch job logs directly:\n       - `gh api \"/repos/<owner>/<repo>/actions/jobs/<job_id>/logs\" > \"<path>\"`\n4. Scope non-GitHub Actions checks.\n   - If `detailsUrl` is not a GitHub Actions run, label it as external and only report the URL.\n   - Do not attempt Buildkite or other providers; keep the workflow lean.\n5. Summarize failures for the user.\n   - Provide the failing check name, run URL (if any), and a concise log snippet.\n   - Call out missing logs explicitly.\n6. Create a plan.\n   - Use the `create-plan` skill to draft a concise plan and request approval.\n7. Implement after approval.\n   - Apply the approved plan, summarize diffs/tests, and ask about opening a PR.\n8. Recheck status.\n   - After changes, suggest re-running the relevant tests and `gh pr checks` to confirm.\n\n## Bundled Resources\n\n### scripts/inspect_pr_checks.py\n\nFetch failing PR checks, pull GitHub Actions logs, and extract a failure snippet. Exits non-zero when failures remain so it can be used in automation.\n\nUsage examples:\n- `python \"<path-to-skill>/scripts/inspect_pr_checks.py\" --repo \".\" --pr \"123\"`\n- `python \"<path-to-skill>/scripts/inspect_pr_checks.py\" --repo \".\" --pr \"https://github.com/org/repo/pull/123\" --json`\n- `python \"<path-to-skill>/scripts/inspect_pr_checks.py\" --repo \".\" --max-lines 200 --context 40`\n"
  },
  {
    "path": "skills/.curated/gh-fix-ci/agents/openai.yaml",
    "content": "interface:\n  display_name: \"GitHub Fix CI\"\n  short_description: \"Debug failing GitHub Actions CI\"\n  icon_small: \"./assets/github-small.svg\"\n  icon_large: \"./assets/github.png\"\n  default_prompt: \"Inspect failing GitHub Actions checks in this repo, summarize root cause, and propose a focused fix plan.\"\n"
  },
  {
    "path": "skills/.curated/gh-fix-ci/scripts/inspect_pr_checks.py",
    "content": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport re\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom shutil import which\nfrom typing import Any, Iterable, Sequence\n\nFAILURE_CONCLUSIONS = {\n    \"failure\",\n    \"cancelled\",\n    \"timed_out\",\n    \"action_required\",\n}\n\nFAILURE_STATES = {\n    \"failure\",\n    \"error\",\n    \"cancelled\",\n    \"timed_out\",\n    \"action_required\",\n}\n\nFAILURE_BUCKETS = {\"fail\"}\n\nFAILURE_MARKERS = (\n    \"error\",\n    \"fail\",\n    \"failed\",\n    \"traceback\",\n    \"exception\",\n    \"assert\",\n    \"panic\",\n    \"fatal\",\n    \"timeout\",\n    \"segmentation fault\",\n)\n\nDEFAULT_MAX_LINES = 160\nDEFAULT_CONTEXT_LINES = 30\nPENDING_LOG_MARKERS = (\n    \"still in progress\",\n    \"log will be available when it is complete\",\n)\n\n\nclass GhResult:\n    def __init__(self, returncode: int, stdout: str, stderr: str):\n        self.returncode = returncode\n        self.stdout = stdout\n        self.stderr = stderr\n\n\ndef run_gh_command(args: Sequence[str], cwd: Path) -> GhResult:\n    process = subprocess.run(\n        [\"gh\", *args],\n        cwd=cwd,\n        text=True,\n        capture_output=True,\n    )\n    return GhResult(process.returncode, process.stdout, process.stderr)\n\n\ndef run_gh_command_raw(args: Sequence[str], cwd: Path) -> tuple[int, bytes, str]:\n    process = subprocess.run(\n        [\"gh\", *args],\n        cwd=cwd,\n        capture_output=True,\n    )\n    stderr = process.stderr.decode(errors=\"replace\")\n    return process.returncode, process.stdout, stderr\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=(\n            \"Inspect failing GitHub PR checks, fetch GitHub Actions logs, and extract a \"\n            \"failure snippet.\"\n        ),\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n    )\n    parser.add_argument(\"--repo\", default=\".\", help=\"Path inside the target Git repository.\")\n    parser.add_argument(\n        \"--pr\", default=None, help=\"PR number or URL (defaults to current branch PR).\"\n    )\n    parser.add_argument(\"--max-lines\", type=int, default=DEFAULT_MAX_LINES)\n    parser.add_argument(\"--context\", type=int, default=DEFAULT_CONTEXT_LINES)\n    parser.add_argument(\"--json\", action=\"store_true\", help=\"Emit JSON instead of text output.\")\n    return parser.parse_args()\n\n\ndef main() -> int:\n    args = parse_args()\n    repo_root = find_git_root(Path(args.repo))\n    if repo_root is None:\n        print(\"Error: not inside a Git repository.\", file=sys.stderr)\n        return 1\n\n    if not ensure_gh_available(repo_root):\n        return 1\n\n    pr_value = resolve_pr(args.pr, repo_root)\n    if pr_value is None:\n        return 1\n\n    checks = fetch_checks(pr_value, repo_root)\n    if checks is None:\n        return 1\n\n    failing = [c for c in checks if is_failing(c)]\n    if not failing:\n        print(f\"PR #{pr_value}: no failing checks detected.\")\n        return 0\n\n    results = []\n    for check in failing:\n        results.append(\n            analyze_check(\n                check,\n                repo_root=repo_root,\n                max_lines=max(1, args.max_lines),\n                context=max(1, args.context),\n            )\n        )\n\n    if args.json:\n        print(json.dumps({\"pr\": pr_value, \"results\": results}, indent=2))\n    else:\n        render_results(pr_value, results)\n\n    return 1\n\n\ndef find_git_root(start: Path) -> Path | None:\n    result = subprocess.run(\n        [\"git\", \"rev-parse\", \"--show-toplevel\"],\n        cwd=start,\n        text=True,\n        capture_output=True,\n    )\n    if result.returncode != 0:\n        return None\n    return Path(result.stdout.strip())\n\n\ndef ensure_gh_available(repo_root: Path) -> bool:\n    if which(\"gh\") is None:\n        print(\"Error: gh is not installed or not on PATH.\", file=sys.stderr)\n        return False\n    result = run_gh_command([\"auth\", \"status\"], cwd=repo_root)\n    if result.returncode == 0:\n        return True\n    message = (result.stderr or result.stdout or \"\").strip()\n    print(message or \"Error: gh not authenticated.\", file=sys.stderr)\n    return False\n\n\ndef resolve_pr(pr_value: str | None, repo_root: Path) -> str | None:\n    if pr_value:\n        return pr_value\n    result = run_gh_command([\"pr\", \"view\", \"--json\", \"number\"], cwd=repo_root)\n    if result.returncode != 0:\n        message = (result.stderr or result.stdout or \"\").strip()\n        print(message or \"Error: unable to resolve PR.\", file=sys.stderr)\n        return None\n    try:\n        data = json.loads(result.stdout or \"{}\")\n    except json.JSONDecodeError:\n        print(\"Error: unable to parse PR JSON.\", file=sys.stderr)\n        return None\n    number = data.get(\"number\")\n    if not number:\n        print(\"Error: no PR number found.\", file=sys.stderr)\n        return None\n    return str(number)\n\n\ndef fetch_checks(pr_value: str, repo_root: Path) -> list[dict[str, Any]] | None:\n    primary_fields = [\"name\", \"state\", \"conclusion\", \"detailsUrl\", \"startedAt\", \"completedAt\"]\n    result = run_gh_command(\n        [\"pr\", \"checks\", pr_value, \"--json\", \",\".join(primary_fields)],\n        cwd=repo_root,\n    )\n    if result.returncode != 0:\n        message = \"\\n\".join(filter(None, [result.stderr, result.stdout])).strip()\n        available_fields = parse_available_fields(message)\n        if available_fields:\n            fallback_fields = [\n                \"name\",\n                \"state\",\n                \"bucket\",\n                \"link\",\n                \"startedAt\",\n                \"completedAt\",\n                \"workflow\",\n            ]\n            selected_fields = [field for field in fallback_fields if field in available_fields]\n            if not selected_fields:\n                print(\"Error: no usable fields available for gh pr checks.\", file=sys.stderr)\n                return None\n            result = run_gh_command(\n                [\"pr\", \"checks\", pr_value, \"--json\", \",\".join(selected_fields)],\n                cwd=repo_root,\n            )\n            if result.returncode != 0:\n                message = (result.stderr or result.stdout or \"\").strip()\n                print(message or \"Error: gh pr checks failed.\", file=sys.stderr)\n                return None\n        else:\n            print(message or \"Error: gh pr checks failed.\", file=sys.stderr)\n            return None\n    try:\n        data = json.loads(result.stdout or \"[]\")\n    except json.JSONDecodeError:\n        print(\"Error: unable to parse checks JSON.\", file=sys.stderr)\n        return None\n    if not isinstance(data, list):\n        print(\"Error: unexpected checks JSON shape.\", file=sys.stderr)\n        return None\n    return data\n\n\ndef is_failing(check: dict[str, Any]) -> bool:\n    conclusion = normalize_field(check.get(\"conclusion\"))\n    if conclusion in FAILURE_CONCLUSIONS:\n        return True\n    state = normalize_field(check.get(\"state\") or check.get(\"status\"))\n    if state in FAILURE_STATES:\n        return True\n    bucket = normalize_field(check.get(\"bucket\"))\n    return bucket in FAILURE_BUCKETS\n\n\ndef analyze_check(\n    check: dict[str, Any],\n    repo_root: Path,\n    max_lines: int,\n    context: int,\n) -> dict[str, Any]:\n    url = check.get(\"detailsUrl\") or check.get(\"link\") or \"\"\n    run_id = extract_run_id(url)\n    job_id = extract_job_id(url)\n    base: dict[str, Any] = {\n        \"name\": check.get(\"name\", \"\"),\n        \"detailsUrl\": url,\n        \"runId\": run_id,\n        \"jobId\": job_id,\n    }\n\n    if run_id is None:\n        base[\"status\"] = \"external\"\n        base[\"note\"] = \"No GitHub Actions run id detected in detailsUrl.\"\n        return base\n\n    metadata = fetch_run_metadata(run_id, repo_root)\n    log_text, log_error, log_status = fetch_check_log(\n        run_id=run_id,\n        job_id=job_id,\n        repo_root=repo_root,\n    )\n\n    if log_status == \"pending\":\n        base[\"status\"] = \"log_pending\"\n        base[\"note\"] = log_error or \"Logs are not available yet.\"\n        if metadata:\n            base[\"run\"] = metadata\n        return base\n\n    if log_error:\n        base[\"status\"] = \"log_unavailable\"\n        base[\"error\"] = log_error\n        if metadata:\n            base[\"run\"] = metadata\n        return base\n\n    snippet = extract_failure_snippet(log_text, max_lines=max_lines, context=context)\n    base[\"status\"] = \"ok\"\n    base[\"run\"] = metadata or {}\n    base[\"logSnippet\"] = snippet\n    base[\"logTail\"] = tail_lines(log_text, max_lines)\n    return base\n\n\ndef extract_run_id(url: str) -> str | None:\n    if not url:\n        return None\n    for pattern in (r\"/actions/runs/(\\d+)\", r\"/runs/(\\d+)\"):\n        match = re.search(pattern, url)\n        if match:\n            return match.group(1)\n    return None\n\n\ndef extract_job_id(url: str) -> str | None:\n    if not url:\n        return None\n    match = re.search(r\"/actions/runs/\\d+/job/(\\d+)\", url)\n    if match:\n        return match.group(1)\n    match = re.search(r\"/job/(\\d+)\", url)\n    if match:\n        return match.group(1)\n    return None\n\n\ndef fetch_run_metadata(run_id: str, repo_root: Path) -> dict[str, Any] | None:\n    fields = [\n        \"conclusion\",\n        \"status\",\n        \"workflowName\",\n        \"name\",\n        \"event\",\n        \"headBranch\",\n        \"headSha\",\n        \"url\",\n    ]\n    result = run_gh_command([\"run\", \"view\", run_id, \"--json\", \",\".join(fields)], cwd=repo_root)\n    if result.returncode != 0:\n        return None\n    try:\n        data = json.loads(result.stdout or \"{}\")\n    except json.JSONDecodeError:\n        return None\n    if not isinstance(data, dict):\n        return None\n    return data\n\n\ndef fetch_check_log(\n    run_id: str,\n    job_id: str | None,\n    repo_root: Path,\n) -> tuple[str, str, str]:\n    log_text, log_error = fetch_run_log(run_id, repo_root)\n    if not log_error:\n        return log_text, \"\", \"ok\"\n\n    if is_log_pending_message(log_error) and job_id:\n        job_log, job_error = fetch_job_log(job_id, repo_root)\n        if job_log:\n            return job_log, \"\", \"ok\"\n        if job_error and is_log_pending_message(job_error):\n            return \"\", job_error, \"pending\"\n        if job_error:\n            return \"\", job_error, \"error\"\n        return \"\", log_error, \"pending\"\n\n    if is_log_pending_message(log_error):\n        return \"\", log_error, \"pending\"\n\n    return \"\", log_error, \"error\"\n\n\ndef fetch_run_log(run_id: str, repo_root: Path) -> tuple[str, str]:\n    result = run_gh_command([\"run\", \"view\", run_id, \"--log\"], cwd=repo_root)\n    if result.returncode != 0:\n        error = (result.stderr or result.stdout or \"\").strip()\n        return \"\", error or \"gh run view failed\"\n    return result.stdout, \"\"\n\n\ndef fetch_job_log(job_id: str, repo_root: Path) -> tuple[str, str]:\n    repo_slug = fetch_repo_slug(repo_root)\n    if not repo_slug:\n        return \"\", \"Error: unable to resolve repository name for job logs.\"\n    endpoint = f\"/repos/{repo_slug}/actions/jobs/{job_id}/logs\"\n    returncode, stdout_bytes, stderr = run_gh_command_raw([\"api\", endpoint], cwd=repo_root)\n    if returncode != 0:\n        message = (stderr or stdout_bytes.decode(errors=\"replace\")).strip()\n        return \"\", message or \"gh api job logs failed\"\n    if is_zip_payload(stdout_bytes):\n        return \"\", \"Job logs returned a zip archive; unable to parse.\"\n    return stdout_bytes.decode(errors=\"replace\"), \"\"\n\n\ndef fetch_repo_slug(repo_root: Path) -> str | None:\n    result = run_gh_command([\"repo\", \"view\", \"--json\", \"nameWithOwner\"], cwd=repo_root)\n    if result.returncode != 0:\n        return None\n    try:\n        data = json.loads(result.stdout or \"{}\")\n    except json.JSONDecodeError:\n        return None\n    name_with_owner = data.get(\"nameWithOwner\")\n    if not name_with_owner:\n        return None\n    return str(name_with_owner)\n\n\ndef normalize_field(value: Any) -> str:\n    if value is None:\n        return \"\"\n    return str(value).strip().lower()\n\n\ndef parse_available_fields(message: str) -> list[str]:\n    if \"Available fields:\" not in message:\n        return []\n    fields: list[str] = []\n    collecting = False\n    for line in message.splitlines():\n        if \"Available fields:\" in line:\n            collecting = True\n            continue\n        if not collecting:\n            continue\n        field = line.strip()\n        if not field:\n            continue\n        fields.append(field)\n    return fields\n\n\ndef is_log_pending_message(message: str) -> bool:\n    lowered = message.lower()\n    return any(marker in lowered for marker in PENDING_LOG_MARKERS)\n\n\ndef is_zip_payload(payload: bytes) -> bool:\n    return payload.startswith(b\"PK\")\n\n\ndef extract_failure_snippet(log_text: str, max_lines: int, context: int) -> str:\n    lines = log_text.splitlines()\n    if not lines:\n        return \"\"\n\n    marker_index = find_failure_index(lines)\n    if marker_index is None:\n        return \"\\n\".join(lines[-max_lines:])\n\n    start = max(0, marker_index - context)\n    end = min(len(lines), marker_index + context)\n    window = lines[start:end]\n    if len(window) > max_lines:\n        window = window[-max_lines:]\n    return \"\\n\".join(window)\n\n\ndef find_failure_index(lines: Sequence[str]) -> int | None:\n    for idx in range(len(lines) - 1, -1, -1):\n        lowered = lines[idx].lower()\n        if any(marker in lowered for marker in FAILURE_MARKERS):\n            return idx\n    return None\n\n\ndef tail_lines(text: str, max_lines: int) -> str:\n    if max_lines <= 0:\n        return \"\"\n    lines = text.splitlines()\n    return \"\\n\".join(lines[-max_lines:])\n\n\ndef render_results(pr_number: str, results: Iterable[dict[str, Any]]) -> None:\n    results_list = list(results)\n    print(f\"PR #{pr_number}: {len(results_list)} failing checks analyzed.\")\n    for result in results_list:\n        print(\"-\" * 60)\n        print(f\"Check: {result.get('name', '')}\")\n        if result.get(\"detailsUrl\"):\n            print(f\"Details: {result['detailsUrl']}\")\n        run_id = result.get(\"runId\")\n        if run_id:\n            print(f\"Run ID: {run_id}\")\n        job_id = result.get(\"jobId\")\n        if job_id:\n            print(f\"Job ID: {job_id}\")\n        status = result.get(\"status\", \"unknown\")\n        print(f\"Status: {status}\")\n\n        run_meta = result.get(\"run\", {})\n        if run_meta:\n            branch = run_meta.get(\"headBranch\", \"\")\n            sha = (run_meta.get(\"headSha\") or \"\")[:12]\n            workflow = run_meta.get(\"workflowName\") or run_meta.get(\"name\") or \"\"\n            conclusion = run_meta.get(\"conclusion\") or run_meta.get(\"status\") or \"\"\n            print(f\"Workflow: {workflow} ({conclusion})\")\n            if branch or sha:\n                print(f\"Branch/SHA: {branch} {sha}\")\n            if run_meta.get(\"url\"):\n                print(f\"Run URL: {run_meta['url']}\")\n\n        if result.get(\"note\"):\n            print(f\"Note: {result['note']}\")\n\n        if result.get(\"error\"):\n            print(f\"Error fetching logs: {result['error']}\")\n            continue\n\n        snippet = result.get(\"logSnippet\") or \"\"\n        if snippet:\n            print(\"Failure snippet:\")\n            print(indent_block(snippet, prefix=\"  \"))\n        else:\n            print(\"No snippet available.\")\n    print(\"-\" * 60)\n\n\ndef indent_block(text: str, prefix: str = \"  \") -> str:\n    return \"\\n\".join(f\"{prefix}{line}\" for line in text.splitlines())\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "skills/.curated/imagegen/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/imagegen/SKILL.md",
    "content": "---\nname: \"imagegen\"\ndescription: \"Use when the user asks to generate or edit images via the OpenAI Image API (for example: generate image, edit/inpaint/mask, background removal or replacement, transparent background, product shots, concept art, covers, or batch variants); run the bundled CLI (`scripts/image_gen.py`) and require `OPENAI_API_KEY` for live calls.\"\n---\n\n\n# Image Generation Skill\n\nGenerates or edits images for the current project (e.g., website assets, game assets, UI mockups, product mockups, wireframes, logo design, photorealistic images, infographics). Defaults to `gpt-image-1.5` and the OpenAI Image API, and prefers the bundled CLI for deterministic, reproducible runs.\n\n## When to use\n- Generate a new image (concept art, product shot, cover, website hero)\n- Edit an existing image (inpainting, masked edits, lighting or weather transformations, background replacement, object removal, compositing, transparent background)\n- Batch runs (many prompts, or many variants across prompts)\n\n## Decision tree (generate vs edit vs batch)\n- If the user provides an input image (or says “edit/retouch/inpaint/mask/translate/localize/change only X”) → **edit**\n- Else if the user needs many different prompts/assets → **generate-batch**\n- Else → **generate**\n\n## Workflow\n1. Decide intent: generate vs edit vs batch (see decision tree above).\n2. Collect inputs up front: prompt(s), exact text (verbatim), constraints/avoid list, and any input image(s)/mask(s). For multi-image edits, label each input by index and role; for edits, list invariants explicitly.\n3. If batch: write a temporary JSONL under tmp/ (one job per line), run once, then delete the JSONL.\n4. Augment prompt into a short labeled spec (structure + constraints) without inventing new creative requirements.\n5. Run the bundled CLI (`scripts/image_gen.py`) with sensible defaults (see references/cli.md).\n6. For complex edits/generations, inspect outputs (open/view images) and validate: subject, style, composition, text accuracy, and invariants/avoid items.\n7. Iterate: make a single targeted change (prompt or mask), re-run, re-check.\n8. Save/return final outputs and note the final prompt + flags used.\n\n## Temp and output conventions\n- Use `tmp/imagegen/` for intermediate files (for example JSONL batches); delete when done.\n- Write final artifacts under `output/imagegen/` when working in this repo.\n- Use `--out` or `--out-dir` to control output paths; keep filenames stable and descriptive.\n\n## Dependencies (install if missing)\nPrefer `uv` for dependency management.\n\nPython packages:\n```\nuv pip install openai pillow\n```\nIf `uv` is unavailable:\n```\npython3 -m pip install openai pillow\n```\n\n## Environment\n- `OPENAI_API_KEY` must be set for live API calls.\n\nIf the key is missing, give the user these steps:\n1. Create an API key in the OpenAI platform UI: https://platform.openai.com/api-keys\n2. Set `OPENAI_API_KEY` as an environment variable in their system.\n3. Offer to guide them through setting the environment variable for their OS/shell if needed.\n- Never ask the user to paste the full key in chat. Ask them to set it locally and confirm when ready.\n\nIf installation isn't possible in this environment, tell the user which dependency is missing and how to install it locally.\n\n## Defaults & rules\n- Use `gpt-image-1.5` unless the user explicitly asks for `gpt-image-1-mini` or explicitly prefers a cheaper/faster model.\n- Assume the user wants a new image unless they explicitly ask for an edit.\n- Require `OPENAI_API_KEY` before any live API call.\n- Use the OpenAI Python SDK (`openai` package) for all API calls; do not use raw HTTP.\n- If the user requests edits, use `client.images.edit(...)` and include input images (and mask if provided).\n- Prefer the bundled CLI (`scripts/image_gen.py`) over writing new one-off scripts.\n- Never modify `scripts/image_gen.py`. If something is missing, ask the user before doing anything else.\n- If the result isn’t clearly relevant or doesn’t satisfy constraints, iterate with small targeted prompt changes; only ask a question if a missing detail blocks success.\n\n## Prompt augmentation\nReformat user prompts into a structured, production-oriented spec. Only make implicit details explicit; do not invent new requirements.\n\n## Use-case taxonomy (exact slugs)\nClassify each request into one of these buckets and keep the slug consistent across prompts and references.\n\nGenerate:\n- photorealistic-natural — candid/editorial lifestyle scenes with real texture and natural lighting.\n- product-mockup — product/packaging shots, catalog imagery, merch concepts.\n- ui-mockup — app/web interface mockups that look shippable.\n- infographic-diagram — diagrams/infographics with structured layout and text.\n- logo-brand — logo/mark exploration, vector-friendly.\n- illustration-story — comics, children’s book art, narrative scenes.\n- stylized-concept — style-driven concept art, 3D/stylized renders.\n- historical-scene — period-accurate/world-knowledge scenes.\n\nEdit:\n- text-localization — translate/replace in-image text, preserve layout.\n- identity-preserve — try-on, person-in-scene; lock face/body/pose.\n- precise-object-edit — remove/replace a specific element (incl. interior swaps).\n- lighting-weather — time-of-day/season/atmosphere changes only.\n- background-extraction — transparent background / clean cutout.\n- style-transfer — apply reference style while changing subject/scene.\n- compositing — multi-image insert/merge with matched lighting/perspective.\n- sketch-to-render — drawing/line art to photoreal render.\n\nQuick clarification (augmentation vs invention):\n- If the user says “a hero image for a landing page”, you may add *layout/composition constraints* that are implied by that use (e.g., “generous negative space on the right for headline text”).\n- Do not introduce new creative elements the user didn’t ask for (e.g., adding a mascot, changing the subject, inventing brand names/logos).\n\nTemplate (include only relevant lines):\n```\nUse case: <taxonomy slug>\nAsset type: <where the asset will be used>\nPrimary request: <user's main prompt>\nScene/background: <environment>\nSubject: <main subject>\nStyle/medium: <photo/illustration/3D/etc>\nComposition/framing: <wide/close/top-down; placement>\nLighting/mood: <lighting + mood>\nColor palette: <palette notes>\nMaterials/textures: <surface details>\nQuality: <low/medium/high/auto>\nInput fidelity (edits): <low/high>\nText (verbatim): \"<exact text>\"\nConstraints: <must keep/must avoid>\nAvoid: <negative constraints>\n```\n\nAugmentation rules:\n- Keep it short; add only details the user already implied or provided elsewhere.\n- Always classify the request into a taxonomy slug above and tailor constraints/composition/quality to that bucket. Use the slug to find the matching example in `references/sample-prompts.md`.\n- If the user gives a broad request (e.g., \"Generate images for this website\"), use judgment to propose tasteful, context-appropriate assets and map each to a taxonomy slug.\n- For edits, explicitly list invariants (\"change only X; keep Y unchanged\").\n- If any critical detail is missing and blocks success, ask a question; otherwise proceed.\n\n## Examples\n\n### Generation example (hero image)\n```\nUse case: stylized-concept\nAsset type: landing page hero\nPrimary request: a minimal hero image of a ceramic coffee mug\nStyle/medium: clean product photography\nComposition/framing: centered product, generous negative space on the right\nLighting/mood: soft studio lighting\nConstraints: no logos, no text, no watermark\n```\n\n### Edit example (invariants)\n```\nUse case: precise-object-edit\nAsset type: product photo background replacement\nPrimary request: replace the background with a warm sunset gradient\nConstraints: change only the background; keep the product and its edges unchanged; no text; no watermark\n```\n\n## Prompting best practices (short list)\n- Structure prompt as scene -> subject -> details -> constraints.\n- Include intended use (ad, UI mock, infographic) to set the mode and polish level.\n- Use camera/composition language for photorealism.\n- Quote exact text and specify typography + placement.\n- For tricky words, spell them letter-by-letter and require verbatim rendering.\n- For multi-image inputs, reference images by index and describe how to combine them.\n- For edits, repeat invariants every iteration to reduce drift.\n- Iterate with single-change follow-ups.\n- For latency-sensitive runs, start with quality=low; use quality=high for text-heavy or detail-critical outputs.\n- For strict edits (identity/layout lock), consider input_fidelity=high.\n- If results feel “tacky”, add a brief “Avoid:” line (stock-photo vibe; cheesy lens flare; oversaturated neon; harsh bloom; oversharpening; clutter) and specify restraint (“editorial”, “premium”, “subtle”).\n\nMore principles: `references/prompting.md`. Copy/paste specs: `references/sample-prompts.md`.\n\n## Guidance by asset type\nAsset-type templates (website assets, game assets, wireframes, logo) are consolidated in `references/sample-prompts.md`.\n\n## CLI + environment notes\n- CLI commands + examples: `references/cli.md`\n- API parameter quick reference: `references/image-api.md`\n- If network approvals / sandbox settings are getting in the way: `references/codex-network.md`\n\n## Reference map\n- **`references/cli.md`**: how to *run* image generation/edits/batches via `scripts/image_gen.py` (commands, flags, recipes).\n- **`references/image-api.md`**: what knobs exist at the API level (parameters, sizes, quality, background, edit-only fields).\n- **`references/prompting.md`**: prompting principles (structure, constraints/invariants, iteration patterns).\n- **`references/sample-prompts.md`**: copy/paste prompt recipes (generate + edit workflows; examples only).\n- **`references/codex-network.md`**: environment/sandbox/network-approval troubleshooting.\n"
  },
  {
    "path": "skills/.curated/imagegen/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Image Gen\"\n  short_description: \"Generate and edit images using OpenAI\"\n  icon_small: \"./assets/imagegen-small.svg\"\n  icon_large: \"./assets/imagegen.png\"\n  default_prompt: \"Generate or edit images for this task and return the final prompt plus selected outputs.\"\n"
  },
  {
    "path": "skills/.curated/imagegen/references/cli.md",
    "content": "# CLI reference (`scripts/image_gen.py`)\n\nThis file contains the “command catalog” for the bundled image generation CLI. Keep `SKILL.md` as overview-first; put verbose CLI details here.\n\n## What this CLI does\n- `generate`: generate new images from a prompt\n- `edit`: edit an existing image (optionally with a mask) — inpainting / background replacement / “change only X”\n- `generate-batch`: run many jobs from a JSONL file (one job per line)\n\nReal API calls require **network access** + `OPENAI_API_KEY`. `--dry-run` does not.\n\n## Quick start (works from any repo)\nSet a stable path to the skill CLI (default `CODEX_HOME` is `~/.codex`):\n\n```\nexport CODEX_HOME=\"${CODEX_HOME:-$HOME/.codex}\"\nexport IMAGE_GEN=\"$CODEX_HOME/skills/imagegen/scripts/image_gen.py\"\n```\n\nDry-run (no API call; no network required; does not require the `openai` package):\n\n```\npython \"$IMAGE_GEN\" generate --prompt \"Test\" --dry-run\n```\n\nGenerate (requires `OPENAI_API_KEY` + network):\n\n```\nuv run --with openai python \"$IMAGE_GEN\" generate --prompt \"A cozy alpine cabin at dawn\" --size 1024x1024\n```\n\nNo `uv` installed? Use your active Python env:\n\n```\npython \"$IMAGE_GEN\" generate --prompt \"A cozy alpine cabin at dawn\" --size 1024x1024\n```\n\n## Guardrails (important)\n- Use `python \"$IMAGE_GEN\" ...` (or equivalent full path) for generations/edits/batch work.\n- Do **not** create one-off runners (e.g. `gen_images.py`) unless the user explicitly asks for a custom wrapper.\n- **Never modify** `scripts/image_gen.py`. If something is missing, ask the user before doing anything else.\n\n## Defaults (unless overridden by flags)\n- Model: `gpt-image-1.5`\n- Size: `1024x1024`\n- Quality: `auto`\n- Output format: `png`\n- Background: unspecified (API default). If you set `--background transparent`, also set `--output-format png` or `webp`.\n\n## Quality + input fidelity\n- `--quality` works for `generate`, `edit`, and `generate-batch`: `low|medium|high|auto`.\n- `--input-fidelity` is **edit-only**: `low|high` (use `high` for strict edits like identity or layout lock).\n\nExample:\n```\npython \"$IMAGE_GEN\" edit --image input.png --prompt \"Change only the background\" --quality high --input-fidelity high\n```\n\n## Masks (edits)\n- Use a **PNG** mask; an alpha channel is strongly recommended.\n- The mask should match the input image dimensions.\n- In the edit prompt, repeat invariants (e.g., “change only the background; keep the subject unchanged”) to reduce drift.\n\n## Optional deps\nPrefer `uv run --with ...` for an out-of-the-box run without changing the current project env; otherwise install into your active env:\n\n```\nuv pip install openai\n```\n\n## Common recipes\n\nGenerate + also write a downscaled copy for fast web loading:\n\n```\nuv run --with openai --with pillow python \"$IMAGE_GEN\" generate \\\n  --prompt \"A cozy alpine cabin at dawn\" \\\n  --size 1024x1024 \\\n  --downscale-max-dim 1024\n```\n\nNotes:\n- Downscaling writes an extra file next to the original (default suffix `-web`, e.g. `output-web.png`).\n- Downscaling requires Pillow (use `uv run --with pillow ...` or install it into your env).\n\nGenerate with augmentation fields:\n\n```\npython \"$IMAGE_GEN\" generate \\\n  --prompt \"A minimal hero image of a ceramic coffee mug\" \\\n  --use-case \"landing page hero\" \\\n  --style \"clean product photography\" \\\n  --composition \"centered product, generous negative space\" \\\n  --constraints \"no logos, no text\"\n```\n\nGenerate multiple prompts concurrently (async batch):\n\n```\nmkdir -p tmp/imagegen\ncat > tmp/imagegen/prompts.jsonl << 'EOF'\n{\"prompt\":\"Cavernous hangar interior with a compact shuttle parked center-left, open bay door\",\"use_case\":\"game concept art environment\",\"composition\":\"wide-angle, low-angle, cinematic framing\",\"lighting\":\"volumetric light rays through drifting fog\",\"constraints\":\"no logos or trademarks; no watermark\",\"size\":\"1536x1024\"}\n{\"prompt\":\"Gray wolf in profile in a snowy forest, crisp fur texture\",\"use_case\":\"wildlife photography print\",\"composition\":\"100mm, eye-level, shallow depth of field\",\"constraints\":\"no logos or trademarks; no watermark\",\"size\":\"1024x1024\"}\nEOF\n\npython \"$IMAGE_GEN\" generate-batch --input tmp/imagegen/prompts.jsonl --out-dir out --concurrency 5\n\n# Cleanup (recommended)\nrm -f tmp/imagegen/prompts.jsonl\n```\n\nNotes:\n- Use `--concurrency` to control parallelism (default `5`). Higher concurrency can hit rate limits; the CLI retries on transient errors.\n- Per-job overrides are supported in JSONL (e.g., `size`, `quality`, `background`, `output_format`, `n`, and prompt-augmentation fields).\n- `--n` generates multiple variants for a single prompt; `generate-batch` is for many different prompts.\n- Treat the JSONL file as temporary: write it under `tmp/` and delete it after the run (don’t commit it).\n\nEdit:\n\n```\npython \"$IMAGE_GEN\" edit --image input.png --mask mask.png --prompt \"Replace the background with a warm sunset\"\n```\n\n## CLI notes\n- Supported sizes: `1024x1024`, `1536x1024`, `1024x1536`, or `auto`.\n- Transparent backgrounds require `output_format` to be `png` or `webp`.\n- Default output is `output.png`; multiple images become `output-1.png`, `output-2.png`, etc.\n- Use `--no-augment` to skip prompt augmentation.\n\n## See also\n- API parameter quick reference: `references/image-api.md`\n- Prompt examples: `references/sample-prompts.md`\n"
  },
  {
    "path": "skills/.curated/imagegen/references/codex-network.md",
    "content": "# Codex network approvals / sandbox notes\n\nThis guidance is intentionally isolated from `SKILL.md` because it can vary by environment and may become stale. Prefer the defaults in your environment when in doubt.\n\n## Why am I asked to approve every image generation call?\nImage generation uses the OpenAI Image API, so the CLI needs outbound network access. In many Codex setups, network access is disabled by default (especially under stricter sandbox modes), and/or the approval policy may require confirmation before networked commands run.\n\n## How do I reduce repeated approval prompts (network)?\nIf you trust the repo and want fewer prompts, enable network access for the relevant sandbox mode and relax the approval policy.\n\nExample `~/.codex/config.toml` pattern:\n\n```\napproval_policy = \"never\"\nsandbox_mode = \"workspace-write\"\n\n[sandbox_workspace_write]\nnetwork_access = true\n```\n\nOr for a single session:\n\n```\ncodex --sandbox workspace-write --ask-for-approval never\n```\n\n## Safety note\nUse caution: enabling network and disabling approvals reduces friction but increases risk if you run untrusted code or work in an untrusted repository.\n"
  },
  {
    "path": "skills/.curated/imagegen/references/image-api.md",
    "content": "# Image API quick reference\n\n## Endpoints\n- Generate: `POST /v1/images/generations` (`client.images.generate(...)`)\n- Edit: `POST /v1/images/edits` (`client.images.edit(...)`)\n\n## Models\n- Default: `gpt-image-1.5`\n- Alternatives: `gpt-image-1-mini` (for faster, lower-cost generation)\n\n## Core parameters (generate + edit)\n- `prompt`: text prompt\n- `model`: image model\n- `n`: number of images (1-10)\n- `size`: `1024x1024`, `1536x1024`, `1024x1536`, or `auto`\n- `quality`: `low`, `medium`, `high`, or `auto`\n- `background`: `transparent`, `opaque`, or `auto` (transparent requires `png`/`webp`)\n- `output_format`: `png` (default), `jpeg`, `webp`\n- `output_compression`: 0-100 (jpeg/webp only)\n- `moderation`: `auto` (default) or `low`\n\n## Edit-specific parameters\n- `image`: one or more input images (first image is primary)\n- `mask`: optional mask image (same size, alpha channel required)\n- `input_fidelity`: `low` (default) or `high` (support varies by model) - set it to `high` if the user needs a very specific edit and you can't achieve it with the default `low` fidelity.\n\n## Output\n- `data[]` list with `b64_json` per image\n\n## Limits & notes\n- Input images and masks must be under 50MB.\n- Use edits endpoint when the user requests changes to an existing image.\n- Masking is prompt-guided; exact shapes are not guaranteed.\n- Large sizes and high quality increase latency and cost.\n- For fast iteration or latency-sensitive runs, start with `quality=low`; raise to `high` for text-heavy or detail-critical outputs.\n- Use `input_fidelity=high` for strict edits (identity preservation, layout lock, or precise compositing).\n"
  },
  {
    "path": "skills/.curated/imagegen/references/prompting.md",
    "content": "# Prompting best practices (gpt-image-1.5)\n\n## Contents\n- [Structure](#structure)\n- [Specificity](#specificity)\n- [Avoiding “tacky” outputs](#avoiding-tacky-outputs)\n- [Composition & layout](#composition--layout)\n- [Constraints & invariants](#constraints--invariants)\n- [Text in images](#text-in-images)\n- [Multi-image inputs](#multi-image-inputs)\n- [Iterate deliberately](#iterate-deliberately)\n- [Quality vs latency](#quality-vs-latency)\n- [Use-case tips](#use-case-tips)\n- [Where to find copy/paste recipes](#where-to-find-copypaste-recipes)\n\n## Structure\n- Use a consistent order: scene/background -> subject -> key details -> constraints -> output intent.\n- Include intended use (ad, UI mock, infographic) to set the mode and polish level.\n- For complex requests, use short labeled lines instead of a long paragraph.\n\n## Specificity\n- Name materials, textures, and visual medium (photo, watercolor, 3D render).\n- For photorealism, include camera/composition language (lens, framing, lighting).\n- Add targeted quality cues only when needed (film grain, textured brushstrokes, macro detail); avoid generic \"8K\" style prompts.\n\n## Avoiding “tacky” outputs\n- Don’t use vibe-only buzzwords (“epic”, “cinematic”, “trending”, “8k”, “award-winning”, “unreal engine”, “artstation”) unless the user explicitly wants that look.\n- Specify restraint: “minimal”, “editorial”, “premium”, “subtle”, “natural color grading”, “soft contrast”, “no harsh bloom”, “no oversharpening”.\n- For 3D/illustration, name the finish you want: “matte”, “paper grain”, “ink texture”, “flat color with soft shadow”; avoid “glossy plastic” unless requested.\n- Add a short negative line when needed (especially for marketing art): “Avoid: stock-photo vibe; cheesy lens flare; oversaturated neon; excessive bokeh; fake-looking smiles; clutter”.\n\n## Composition & layout\n- Specify framing and viewpoint (close-up, wide, top-down) and placement (\"logo top-right\").\n- Call out negative space if you need room for UI or overlays.\n\n## Constraints & invariants\n- State what must not change (\"keep background unchanged\").\n- For edits, say \"change only X; keep Y unchanged\" and repeat invariants on every iteration to reduce drift.\n\n## Text in images\n- Put literal text in quotes or ALL CAPS and specify typography (font style, size, color, placement).\n- Spell uncommon words letter-by-letter if accuracy matters.\n- For in-image copy, require verbatim rendering and no extra characters.\n\n## Multi-image inputs\n- Reference inputs by index and role (\"Image 1: product, Image 2: style\").\n- Describe how to combine them (\"apply Image 2's style to Image 1\").\n- For compositing, specify what moves where and what must remain unchanged.\n\n## Iterate deliberately\n- Start with a clean base prompt, then make small single-change edits.\n- Re-specify critical constraints when you iterate.\n\n## Quality vs latency\n- For latency-sensitive runs, start at `quality=low` and only raise it if needed.\n- Use `quality=high` for text-heavy or detail-critical images.\n- For strict edits (identity preservation, layout lock), consider `input_fidelity=high`.\n\n## Use-case tips\nGenerate:\n- photorealistic-natural: Prompt as if a real photo is captured in the moment; use photography language (lens, lighting, framing); call for real texture (pores, wrinkles, fabric wear, imperfections); avoid studio polish or staging; use `quality=high` when detail matters.\n- product-mockup: Describe the product/packaging and materials; ensure clean silhouette and label clarity; if in-image text is needed, require verbatim rendering and specify typography.\n- ui-mockup: Describe a real product; focus on layout, hierarchy, and common UI elements; avoid concept-art language so it looks shippable.\n- infographic-diagram: Define the audience and layout flow; label parts explicitly; require verbatim text; use `quality=high`.\n- logo-brand: Keep it simple and scalable; ask for a strong silhouette and balanced negative space; avoid gradients and fine detail.\n- illustration-story: Define panels or scene beats; keep each action concrete; for continuity, restate character traits and outfit each time.\n- stylized-concept: Specify style cues, material finish, and rendering approach (3D, painterly, clay); add a short \"Avoid\" line to prevent tacky effects.\n- historical-scene: State the location/date and required period accuracy; constrain clothing, props, and environment to match the era.\n\nEdit:\n- text-localization: Change only the text; preserve layout, typography, spacing, and hierarchy; no extra words or reflow unless needed.\n- identity-preserve: Lock identity (face, body, pose, hair, expression); change only the specified elements; match lighting and shadows; use `input_fidelity=high` if likeness drifts.\n- precise-object-edit: Specify exactly what to remove/replace; preserve surrounding texture and lighting; keep everything else unchanged.\n- lighting-weather: Change only environmental conditions (light, shadows, atmosphere, precipitation); keep geometry, framing, and subject identity.\n- background-extraction: Request transparent background; crisp silhouette; no halos; preserve label text exactly; optionally add a subtle contact shadow.\n- style-transfer: Specify style cues to preserve (palette, texture, brushwork) and what must change; add \"no extra elements\" to prevent drift.\n- compositing: Reference inputs by index; specify what moves where; match lighting, perspective, and scale; keep background and framing unchanged.\n- sketch-to-render: Preserve layout, proportions, and perspective; add plausible materials, lighting, and environment; \"do not add new elements or text.\"\n\n## Where to find copy/paste recipes\nFor copy/paste prompt specs (examples only), see `references/sample-prompts.md`. This file focuses on principles, structure, and iteration patterns.\n"
  },
  {
    "path": "skills/.curated/imagegen/references/sample-prompts.md",
    "content": "# Sample prompts (copy/paste)\n\nUse these as starting points (recipes only). Keep user-provided requirements; do not invent new creative elements.\n\nFor prompting principles (structure, invariants, iteration), see `references/prompting.md`.\n\n## Generate\n\n### photorealistic-natural\n```\nUse case: photorealistic-natural\nPrimary request: candid photo of an elderly sailor on a small fishing boat adjusting a net\nScene/background: coastal water with soft haze\nSubject: weathered skin with wrinkles and sun texture; a calm dog on deck nearby\nStyle/medium: photorealistic candid photo\nComposition/framing: medium close-up, eye-level, 50mm lens\nLighting/mood: soft coastal daylight, shallow depth of field, subtle film grain\nMaterials/textures: real skin texture, worn fabric, salt-worn wood\nConstraints: natural color balance; no heavy retouching; no glamorization; no watermark\nAvoid: studio polish; staged look\nQuality: high\n```\n\n### product-mockup\n```\nUse case: product-mockup\nPrimary request: premium product photo of a matte black shampoo bottle with a minimal label\nScene/background: clean studio gradient from light gray to white\nSubject: single bottle centered with subtle reflection\nStyle/medium: premium product photography\nComposition/framing: centered, slight three-quarter angle, generous padding\nLighting/mood: softbox lighting, clean highlights, controlled shadows\nMaterials/textures: matte plastic, crisp label printing\nConstraints: no logos or trademarks; no watermark\nQuality: high\n```\n\n### ui-mockup\n```\nUse case: ui-mockup\nPrimary request: mobile app UI for a local farmers market with vendors and specials\nScene/background: clean white background with subtle natural accents\nSubject: header, vendor list with small photos, \"Today's specials\" section, location and hours\nStyle/medium: realistic product UI, not concept art\nComposition/framing: iPhone frame, balanced spacing and hierarchy\nConstraints: practical layout, clear typography, no logos or trademarks, no watermark\n```\n\n### infographic-diagram\n```\nUse case: infographic-diagram\nPrimary request: detailed infographic of an automatic coffee machine flow\nScene/background: clean, light neutral background\nSubject: bean hopper -> grinder -> brew group -> boiler -> water tank -> drip tray\nStyle/medium: clean vector-like infographic with clear callouts and arrows\nComposition/framing: vertical poster layout, top-to-bottom flow\nText (verbatim): \"Bean Hopper\", \"Grinder\", \"Brew Group\", \"Boiler\", \"Water Tank\", \"Drip Tray\"\nConstraints: clear labels, strong contrast, no logos or trademarks, no watermark\nQuality: high\n```\n\n### logo-brand\n```\nUse case: logo-brand\nPrimary request: original logo for \"Field & Flour\", a local bakery\nStyle/medium: vector logo mark; flat colors; minimal\nComposition/framing: single centered logo on plain background with padding\nConstraints: strong silhouette, balanced negative space; original design only; no gradients unless essential; no trademarks; no watermark\n```\n\n### illustration-story\n```\nUse case: illustration-story\nPrimary request: 4-panel comic about a pet left alone at home\nScene/background: cozy living room across panels\nSubject: pet reacting to the owner leaving, then relaxing, then returning to a composed pose\nStyle/medium: comic illustration with clear panels\nComposition/framing: 4 equal-sized vertical panels, readable actions per panel\nConstraints: no text; no logos or trademarks; no watermark\n```\n\n### stylized-concept\n```\nUse case: stylized-concept\nPrimary request: cavernous hangar interior with tall support beams and drifting fog\nScene/background: industrial hangar interior, deep scale, light haze\nSubject: compact shuttle, parked center-left, bay door open\nStyle/medium: cinematic concept art, industrial realism\nComposition/framing: wide-angle, low-angle, cinematic framing\nLighting/mood: volumetric light rays cutting through fog\nConstraints: no logos or trademarks; no watermark\n```\n\n### historical-scene\n```\nUse case: historical-scene\nPrimary request: outdoor crowd scene in Bethel, New York on August 16, 1969\nScene/background: open field, temporary stages, period-accurate tents and signage\nSubject: crowd in period-accurate clothing, authentic staging and environment\nStyle/medium: photorealistic photo\nComposition/framing: wide shot, eye-level\nConstraints: period-accurate details; no modern objects; no logos or trademarks; no watermark\n```\n\n## Asset type templates (taxonomy-aligned)\n\n### Website assets template\n```\nUse case: <photorealistic-natural|stylized-concept|product-mockup|infographic-diagram|ui-mockup>\nAsset type: <hero image / section illustration / blog header>\nPrimary request: <short description>\nScene/background: <environment or abstract background>\nSubject: <main subject>\nStyle/medium: <photo/illustration/3D>\nComposition/framing: <wide/centered; specify negative space side>\nLighting/mood: <soft/bright/neutral>\nColor palette: <brand colors or neutral>\nConstraints: <no text; no logos; no watermark; leave space for UI>\n```\n\n### Website assets example: minimal hero background\n```\nUse case: stylized-concept\nAsset type: landing page hero background\nPrimary request: minimal abstract background with a soft gradient and subtle texture (calm, modern)\nStyle/medium: matte illustration / soft-rendered abstract background (not glossy 3D)\nComposition/framing: wide composition; large negative space on the right for headline\nLighting/mood: gentle studio glow\nColor palette: cool neutrals with a restrained blue accent\nConstraints: no text; no logos; no watermark\n```\n\n### Website assets example: feature section illustration\n```\nUse case: stylized-concept\nAsset type: feature section illustration\nPrimary request: simple abstract shapes suggesting connection and flow (tasteful, minimal)\nScene/background: subtle light-gray backdrop with faint texture\nStyle/medium: flat illustration; soft shadows; restrained contrast\nComposition/framing: centered cluster; open margins for UI\nColor palette: muted teal and slate, low contrast accents\nConstraints: no text; no logos; no watermark\n```\n\n### Website assets example: blog header image\n```\nUse case: photorealistic-natural\nAsset type: blog header image\nPrimary request: overhead desk scene with notebook, pen, and coffee cup\nScene/background: warm wooden tabletop\nStyle/medium: photorealistic photo\nComposition/framing: wide crop; subject placed left; right side left empty\nLighting/mood: soft morning light\nConstraints: no text; no logos; no watermark\n```\n\n### Game assets template\n```\nUse case: stylized-concept\nAsset type: <game environment concept art / game character concept / game UI icon / tileable game texture>\nPrimary request: <biome/scene/character/icon/material>\nScene/background: <location + set dressing> (if applicable)\nSubject: <main focal element(s)>\nStyle/medium: <realistic/stylized>; <concept art / character render / UI icon / texture>\nComposition/framing: <wide/establishing/top-down>; <camera angle>; <focal point placement>\nLighting/mood: <time of day>; <mood>; <volumetric/fog/etc>\nConstraints: no logos or trademarks; no watermark\n```\n\n### Game assets example: environment concept art\n```\nUse case: stylized-concept\nAsset type: game environment concept art\nPrimary request: cavernous hangar interior with tall support beams and drifting fog\nScene/background: industrial hangar interior, deep scale, light haze\nSubject: compact shuttle, parked center-left, bay door open\nForeground: painted floor markings; cables; tool carts along edges\nStyle/medium: cinematic concept art, industrial realism\nComposition/framing: wide-angle, low-angle, cinematic framing\nLighting/mood: volumetric light rays cutting through fog\nConstraints: no logos or trademarks; no watermark\n```\n\n### Game assets example: character concept\n```\nUse case: stylized-concept\nAsset type: game character concept\nPrimary request: desert scout character with layered travel gear\nSilhouette: long coat with hood, wide boots, satchel\nOutfit/gear: dusty canvas, leather straps, brass buckles\nFace/hair: windworn face, short cropped hair\nStyle/medium: character render; stylized realism\nPose: neutral hero pose\nBackground: simple neutral backdrop\nConstraints: no logos or trademarks; no watermark\n```\n\n### Game assets example: UI icon\n```\nUse case: stylized-concept\nAsset type: game UI icon\nPrimary request: round shield icon with a subtle rune pattern\nStyle/medium: painted game UI icon\nComposition/framing: centered icon; generous padding; clear silhouette\nBackground: transparent\nLighting/mood: subtle highlights; crisp edges\nConstraints: no text; no logos or trademarks; no watermark\n```\n\n### Game assets example: tileable texture\n```\nUse case: stylized-concept\nAsset type: tileable game texture\nPrimary request: worn sandstone blocks\nStyle/medium: seamless tileable texture; PBR-ish look\nScale: medium tiling\nLighting: neutral / flat lighting\nConstraints: seamless edges; no obvious focal elements; no text; no logos or trademarks; no watermark\n```\n\n### Wireframe template\n```\nUse case: ui-mockup\nAsset type: website wireframe\nPrimary request: <page or flow to sketch>\nFidelity: low-fi grayscale wireframe; hand-drawn feel; simple boxes\nLayout: <sections in order; grid/columns>\nAnnotations: <labels for key blocks>\nResolution/orientation: <landscape or portrait to match expected device>\nConstraints: no color; no logos; no real photos; no watermark\n```\n\n### Wireframe example: homepage (desktop)\n```\nUse case: ui-mockup\nAsset type: website wireframe\nPrimary request: SaaS homepage layout with clear hierarchy\nFidelity: low-fi grayscale wireframe; hand-drawn feel; simple boxes\nLayout: top nav; hero with headline and CTA; three feature cards; testimonial strip; pricing preview; footer\nAnnotations: label each block (\"Nav\", \"Hero\", \"CTA\", \"Feature\", \"Testimonial\", \"Pricing\", \"Footer\")\nResolution/orientation: landscape (wide) for desktop\nConstraints: no color; no logos; no real photos; no watermark\n```\n\n### Wireframe example: pricing page\n```\nUse case: ui-mockup\nAsset type: website wireframe\nPrimary request: pricing page layout with comparison table\nFidelity: low-fi grayscale wireframe; sketchy lines; simple boxes\nLayout: header; plan toggle; 3 pricing cards; comparison table; FAQ accordion; footer\nAnnotations: label key areas (\"Toggle\", \"Plan Card\", \"Table\", \"FAQ\")\nResolution/orientation: landscape for desktop or portrait for tablet\nConstraints: no color; no logos; no real photos; no watermark\n```\n\n### Wireframe example: mobile onboarding flow\n```\nUse case: ui-mockup\nAsset type: website wireframe\nPrimary request: three-screen mobile onboarding flow\nFidelity: low-fi grayscale wireframe; hand-drawn feel; simple boxes\nLayout: screen 1 (logo placeholder, headline, illustration placeholder, CTA); screen 2 (feature bullets); screen 3 (form fields + CTA)\nAnnotations: label each block and screen number\nResolution/orientation: portrait (tall) for mobile\nConstraints: no color; no logos; no real photos; no watermark\n```\n\n### Logo template\n```\nUse case: logo-brand\nAsset type: logo concept\nPrimary request: <brand idea or symbol concept>\nStyle/medium: vector logo mark; flat colors; minimal\nComposition/framing: centered mark; clear silhouette; generous margin\nColor palette: <1-2 colors; high contrast>\nText (verbatim): \"<exact name>\" (only if needed)\nConstraints: no gradients; no mockups; no 3D; no watermark\n```\n\n### Logo example: abstract symbol mark\n```\nUse case: logo-brand\nAsset type: logo concept\nPrimary request: geometric leaf symbol suggesting sustainability and growth\nStyle/medium: vector logo mark; flat colors; minimal\nComposition/framing: centered mark; clear silhouette\nColor palette: deep green and off-white\nConstraints: no text; no gradients; no mockups; no 3D; no watermark\n```\n\n### Logo example: monogram mark\n```\nUse case: logo-brand\nAsset type: logo concept\nPrimary request: interlocking monogram of the letters \"AV\"\nStyle/medium: vector logo mark; flat colors; minimal\nComposition/framing: centered mark; balanced spacing\nColor palette: black on white\nConstraints: no gradients; no mockups; no 3D; no watermark\n```\n\n### Logo example: wordmark\n```\nUse case: logo-brand\nAsset type: logo concept\nPrimary request: clean wordmark for a modern studio\nStyle/medium: vector wordmark; flat colors; minimal\nText (verbatim): \"Studio North\"\nComposition/framing: centered text; even letter spacing\nColor palette: charcoal on white\nConstraints: no gradients; no mockups; no 3D; no watermark\n```\n\n## Edit\n\n### text-localization\n```\nUse case: text-localization\nInput images: Image 1: original infographic\nPrimary request: translate all in-image text to Spanish\nConstraints: change only the text; preserve layout, typography, spacing, and hierarchy; no extra words; do not alter logos or imagery\n```\n\n### identity-preserve\n```\nUse case: identity-preserve\nInput images: Image 1: person photo; Image 2..N: clothing items\nPrimary request: replace only the clothing with the provided garments\nConstraints: preserve face, body shape, pose, hair, expression, and identity; match lighting and shadows; keep background unchanged; no accessories or text\nInput fidelity (edits): high\n```\n\n### precise-object-edit\n```\nUse case: precise-object-edit\nInput images: Image 1: room photo\nPrimary request: replace ONLY the white chairs with wooden chairs\nConstraints: preserve camera angle, room lighting, floor shadows, and surrounding objects; keep all other aspects unchanged\n```\n\n### lighting-weather\n```\nUse case: lighting-weather\nInput images: Image 1: original photo\nPrimary request: make it look like a winter evening with gentle snowfall\nConstraints: preserve subject identity, geometry, camera angle, and composition; change only lighting, atmosphere, and weather\nQuality: high\n```\n\n### background-extraction\n```\nUse case: background-extraction\nInput images: Image 1: product photo\nPrimary request: extract the product on a transparent background\nOutput: transparent background (RGBA PNG)\nConstraints: crisp silhouette, no halos/fringing; preserve label text exactly; no restyling\n```\n\n### style-transfer\n```\nUse case: style-transfer\nInput images: Image 1: style reference\nPrimary request: apply Image 1's visual style to a man riding a motorcycle on a white background\nConstraints: preserve palette, texture, and brushwork; no extra elements; plain white background\n```\n\n### compositing\n```\nUse case: compositing\nInput images: Image 1: base scene; Image 2: subject to insert\nPrimary request: place the subject from Image 2 next to the person in Image 1\nConstraints: match lighting, perspective, and scale; keep background and framing unchanged; no extra elements\nInput fidelity (edits): high\n```\n\n### sketch-to-render\n```\nUse case: sketch-to-render\nInput images: Image 1: drawing\nPrimary request: turn the drawing into a photorealistic image\nConstraints: preserve layout, proportions, and perspective; choose realistic materials and lighting; do not add new elements or text\nQuality: high\n```\n"
  },
  {
    "path": "skills/.curated/imagegen/scripts/image_gen.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Generate or edit images with the OpenAI Image API.\n\nDefaults to gpt-image-1.5 and a structured prompt augmentation workflow.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport asyncio\nimport base64\nimport json\nimport os\nfrom pathlib import Path\nimport re\nimport sys\nimport time\nfrom typing import Any, Dict, Iterable, List, Optional, Tuple\n\nfrom io import BytesIO\n\nDEFAULT_MODEL = \"gpt-image-1.5\"\nDEFAULT_SIZE = \"1024x1024\"\nDEFAULT_QUALITY = \"auto\"\nDEFAULT_OUTPUT_FORMAT = \"png\"\nDEFAULT_CONCURRENCY = 5\nDEFAULT_DOWNSCALE_SUFFIX = \"-web\"\n\nALLOWED_SIZES = {\"1024x1024\", \"1536x1024\", \"1024x1536\", \"auto\"}\nALLOWED_QUALITIES = {\"low\", \"medium\", \"high\", \"auto\"}\nALLOWED_BACKGROUNDS = {\"transparent\", \"opaque\", \"auto\", None}\n\nMAX_IMAGE_BYTES = 50 * 1024 * 1024\nMAX_BATCH_JOBS = 500\n\n\ndef _die(message: str, code: int = 1) -> None:\n    print(f\"Error: {message}\", file=sys.stderr)\n    raise SystemExit(code)\n\n\ndef _warn(message: str) -> None:\n    print(f\"Warning: {message}\", file=sys.stderr)\n\n\ndef _ensure_api_key(dry_run: bool) -> None:\n    if os.getenv(\"OPENAI_API_KEY\"):\n        print(\"OPENAI_API_KEY is set.\", file=sys.stderr)\n        return\n    if dry_run:\n        _warn(\"OPENAI_API_KEY is not set; dry-run only.\")\n        return\n    _die(\"OPENAI_API_KEY is not set. Export it before running.\")\n\n\ndef _read_prompt(prompt: Optional[str], prompt_file: Optional[str]) -> str:\n    if prompt and prompt_file:\n        _die(\"Use --prompt or --prompt-file, not both.\")\n    if prompt_file:\n        path = Path(prompt_file)\n        if not path.exists():\n            _die(f\"Prompt file not found: {path}\")\n        return path.read_text(encoding=\"utf-8\").strip()\n    if prompt:\n        return prompt.strip()\n    _die(\"Missing prompt. Use --prompt or --prompt-file.\")\n    return \"\"  # unreachable\n\n\ndef _check_image_paths(paths: Iterable[str]) -> List[Path]:\n    resolved: List[Path] = []\n    for raw in paths:\n        path = Path(raw)\n        if not path.exists():\n            _die(f\"Image file not found: {path}\")\n        if path.stat().st_size > MAX_IMAGE_BYTES:\n            _warn(f\"Image exceeds 50MB limit: {path}\")\n        resolved.append(path)\n    return resolved\n\n\ndef _normalize_output_format(fmt: Optional[str]) -> str:\n    if not fmt:\n        return DEFAULT_OUTPUT_FORMAT\n    fmt = fmt.lower()\n    if fmt not in {\"png\", \"jpeg\", \"jpg\", \"webp\"}:\n        _die(\"output-format must be png, jpeg, jpg, or webp.\")\n    return \"jpeg\" if fmt == \"jpg\" else fmt\n\n\ndef _validate_size(size: str) -> None:\n    if size not in ALLOWED_SIZES:\n        _die(\n            \"size must be one of 1024x1024, 1536x1024, 1024x1536, or auto for GPT image models.\"\n        )\n\n\ndef _validate_quality(quality: str) -> None:\n    if quality not in ALLOWED_QUALITIES:\n        _die(\"quality must be one of low, medium, high, or auto.\")\n\n\ndef _validate_background(background: Optional[str]) -> None:\n    if background not in ALLOWED_BACKGROUNDS:\n        _die(\"background must be one of transparent, opaque, or auto.\")\n\n\ndef _validate_transparency(background: Optional[str], output_format: str) -> None:\n    if background == \"transparent\" and output_format not in {\"png\", \"webp\"}:\n        _die(\"transparent background requires output-format png or webp.\")\n\n\ndef _validate_generate_payload(payload: Dict[str, Any]) -> None:\n    n = int(payload.get(\"n\", 1))\n    if n < 1 or n > 10:\n        _die(\"n must be between 1 and 10\")\n    size = str(payload.get(\"size\", DEFAULT_SIZE))\n    quality = str(payload.get(\"quality\", DEFAULT_QUALITY))\n    background = payload.get(\"background\")\n    _validate_size(size)\n    _validate_quality(quality)\n    _validate_background(background)\n    oc = payload.get(\"output_compression\")\n    if oc is not None and not (0 <= int(oc) <= 100):\n        _die(\"output_compression must be between 0 and 100\")\n\n\ndef _build_output_paths(\n    out: str,\n    output_format: str,\n    count: int,\n    out_dir: Optional[str],\n) -> List[Path]:\n    ext = \".\" + output_format\n\n    if out_dir:\n        out_base = Path(out_dir)\n        out_base.mkdir(parents=True, exist_ok=True)\n        return [out_base / f\"image_{i}{ext}\" for i in range(1, count + 1)]\n\n    out_path = Path(out)\n    if out_path.exists() and out_path.is_dir():\n        out_path.mkdir(parents=True, exist_ok=True)\n        return [out_path / f\"image_{i}{ext}\" for i in range(1, count + 1)]\n\n    if out_path.suffix == \"\":\n        out_path = out_path.with_suffix(ext)\n    elif output_format and out_path.suffix.lstrip(\".\").lower() != output_format:\n        _warn(\n            f\"Output extension {out_path.suffix} does not match output-format {output_format}.\"\n        )\n\n    if count == 1:\n        return [out_path]\n\n    return [\n        out_path.with_name(f\"{out_path.stem}-{i}{out_path.suffix}\")\n        for i in range(1, count + 1)\n    ]\n\n\ndef _augment_prompt(args: argparse.Namespace, prompt: str) -> str:\n    fields = _fields_from_args(args)\n    return _augment_prompt_fields(args.augment, prompt, fields)\n\n\ndef _augment_prompt_fields(augment: bool, prompt: str, fields: Dict[str, Optional[str]]) -> str:\n    if not augment:\n        return prompt\n\n    sections: List[str] = []\n    if fields.get(\"use_case\"):\n        sections.append(f\"Use case: {fields['use_case']}\")\n    sections.append(f\"Primary request: {prompt}\")\n    if fields.get(\"scene\"):\n        sections.append(f\"Scene/background: {fields['scene']}\")\n    if fields.get(\"subject\"):\n        sections.append(f\"Subject: {fields['subject']}\")\n    if fields.get(\"style\"):\n        sections.append(f\"Style/medium: {fields['style']}\")\n    if fields.get(\"composition\"):\n        sections.append(f\"Composition/framing: {fields['composition']}\")\n    if fields.get(\"lighting\"):\n        sections.append(f\"Lighting/mood: {fields['lighting']}\")\n    if fields.get(\"palette\"):\n        sections.append(f\"Color palette: {fields['palette']}\")\n    if fields.get(\"materials\"):\n        sections.append(f\"Materials/textures: {fields['materials']}\")\n    if fields.get(\"text\"):\n        sections.append(f\"Text (verbatim): \\\"{fields['text']}\\\"\")\n    if fields.get(\"constraints\"):\n        sections.append(f\"Constraints: {fields['constraints']}\")\n    if fields.get(\"negative\"):\n        sections.append(f\"Avoid: {fields['negative']}\")\n\n    return \"\\n\".join(sections)\n\n\ndef _fields_from_args(args: argparse.Namespace) -> Dict[str, Optional[str]]:\n    return {\n        \"use_case\": getattr(args, \"use_case\", None),\n        \"scene\": getattr(args, \"scene\", None),\n        \"subject\": getattr(args, \"subject\", None),\n        \"style\": getattr(args, \"style\", None),\n        \"composition\": getattr(args, \"composition\", None),\n        \"lighting\": getattr(args, \"lighting\", None),\n        \"palette\": getattr(args, \"palette\", None),\n        \"materials\": getattr(args, \"materials\", None),\n        \"text\": getattr(args, \"text\", None),\n        \"constraints\": getattr(args, \"constraints\", None),\n        \"negative\": getattr(args, \"negative\", None),\n    }\n\n\ndef _print_request(payload: dict) -> None:\n    print(json.dumps(payload, indent=2, sort_keys=True))\n\n\ndef _decode_and_write(images: List[str], outputs: List[Path], force: bool) -> None:\n    for idx, image_b64 in enumerate(images):\n        if idx >= len(outputs):\n            break\n        out_path = outputs[idx]\n        if out_path.exists() and not force:\n            _die(f\"Output already exists: {out_path} (use --force to overwrite)\")\n        out_path.parent.mkdir(parents=True, exist_ok=True)\n        out_path.write_bytes(base64.b64decode(image_b64))\n        print(f\"Wrote {out_path}\")\n\n\ndef _derive_downscale_path(path: Path, suffix: str) -> Path:\n    if suffix and not suffix.startswith(\"-\") and not suffix.startswith(\"_\"):\n        suffix = \"-\" + suffix\n    return path.with_name(f\"{path.stem}{suffix}{path.suffix}\")\n\n\ndef _downscale_image_bytes(image_bytes: bytes, *, max_dim: int, output_format: str) -> bytes:\n    try:\n        from PIL import Image\n    except Exception:\n        _die(\n            \"Downscaling requires Pillow. Install with `uv pip install pillow` (then re-run).\"\n        )\n\n    if max_dim < 1:\n        _die(\"--downscale-max-dim must be >= 1\")\n\n    with Image.open(BytesIO(image_bytes)) as img:\n        img.load()\n        w, h = img.size\n        scale = min(1.0, float(max_dim) / float(max(w, h)))\n        target = (max(1, int(round(w * scale))), max(1, int(round(h * scale))))\n\n        resized = img if target == (w, h) else img.resize(target, Image.Resampling.LANCZOS)\n\n        fmt = output_format.lower()\n        if fmt == \"jpg\":\n            fmt = \"jpeg\"\n\n        if fmt == \"jpeg\":\n            if resized.mode in (\"RGBA\", \"LA\") or (\"transparency\" in getattr(resized, \"info\", {})):\n                bg = Image.new(\"RGB\", resized.size, (255, 255, 255))\n                bg.paste(resized.convert(\"RGBA\"), mask=resized.convert(\"RGBA\").split()[-1])\n                resized = bg\n            else:\n                resized = resized.convert(\"RGB\")\n\n        out = BytesIO()\n        resized.save(out, format=fmt.upper())\n        return out.getvalue()\n\n\ndef _decode_write_and_downscale(\n    images: List[str],\n    outputs: List[Path],\n    *,\n    force: bool,\n    downscale_max_dim: Optional[int],\n    downscale_suffix: str,\n    output_format: str,\n) -> None:\n    for idx, image_b64 in enumerate(images):\n        if idx >= len(outputs):\n            break\n        out_path = outputs[idx]\n        if out_path.exists() and not force:\n            _die(f\"Output already exists: {out_path} (use --force to overwrite)\")\n        out_path.parent.mkdir(parents=True, exist_ok=True)\n\n        raw = base64.b64decode(image_b64)\n        out_path.write_bytes(raw)\n        print(f\"Wrote {out_path}\")\n\n        if downscale_max_dim is None:\n            continue\n\n        derived = _derive_downscale_path(out_path, downscale_suffix)\n        if derived.exists() and not force:\n            _die(f\"Output already exists: {derived} (use --force to overwrite)\")\n        derived.parent.mkdir(parents=True, exist_ok=True)\n        resized = _downscale_image_bytes(raw, max_dim=downscale_max_dim, output_format=output_format)\n        derived.write_bytes(resized)\n        print(f\"Wrote {derived}\")\n\n\ndef _create_client():\n    try:\n        from openai import OpenAI\n    except ImportError as exc:\n        _die(\"openai SDK not installed. Install with `uv pip install openai`.\")\n    return OpenAI()\n\n\ndef _create_async_client():\n    try:\n        from openai import AsyncOpenAI\n    except ImportError:\n        try:\n            import openai as _openai  # noqa: F401\n        except ImportError:\n            _die(\"openai SDK not installed. Install with `uv pip install openai`.\")\n        _die(\n            \"AsyncOpenAI not available in this openai SDK version. Upgrade with `uv pip install -U openai`.\"\n        )\n    return AsyncOpenAI()\n\n\ndef _slugify(value: str) -> str:\n    value = value.strip().lower()\n    value = re.sub(r\"[^a-z0-9]+\", \"-\", value)\n    value = re.sub(r\"-{2,}\", \"-\", value).strip(\"-\")\n    return value[:60] if value else \"job\"\n\n\ndef _normalize_job(job: Any, idx: int) -> Dict[str, Any]:\n    if isinstance(job, str):\n        prompt = job.strip()\n        if not prompt:\n            _die(f\"Empty prompt at job {idx}\")\n        return {\"prompt\": prompt}\n    if isinstance(job, dict):\n        if \"prompt\" not in job or not str(job[\"prompt\"]).strip():\n            _die(f\"Missing prompt for job {idx}\")\n        return job\n    _die(f\"Invalid job at index {idx}: expected string or object.\")\n    return {}  # unreachable\n\n\ndef _read_jobs_jsonl(path: str) -> List[Dict[str, Any]]:\n    p = Path(path)\n    if not p.exists():\n        _die(f\"Input file not found: {p}\")\n    jobs: List[Dict[str, Any]] = []\n    for line_no, raw in enumerate(p.read_text(encoding=\"utf-8\").splitlines(), start=1):\n        line = raw.strip()\n        if not line or line.startswith(\"#\"):\n            continue\n        try:\n            item: Any\n            if line.startswith(\"{\"):\n                item = json.loads(line)\n            else:\n                item = line\n            jobs.append(_normalize_job(item, idx=line_no))\n        except json.JSONDecodeError as exc:\n            _die(f\"Invalid JSON on line {line_no}: {exc}\")\n    if not jobs:\n        _die(\"No jobs found in input file.\")\n    if len(jobs) > MAX_BATCH_JOBS:\n        _die(f\"Too many jobs ({len(jobs)}). Max is {MAX_BATCH_JOBS}.\")\n    return jobs\n\n\ndef _merge_non_null(dst: Dict[str, Any], src: Dict[str, Any]) -> Dict[str, Any]:\n    merged = dict(dst)\n    for k, v in src.items():\n        if v is not None:\n            merged[k] = v\n    return merged\n\n\ndef _job_output_paths(\n    *,\n    out_dir: Path,\n    output_format: str,\n    idx: int,\n    prompt: str,\n    n: int,\n    explicit_out: Optional[str],\n) -> List[Path]:\n    out_dir.mkdir(parents=True, exist_ok=True)\n    ext = \".\" + output_format\n\n    if explicit_out:\n        base = Path(explicit_out)\n        if base.suffix == \"\":\n            base = base.with_suffix(ext)\n        elif base.suffix.lstrip(\".\").lower() != output_format:\n            _warn(\n                f\"Job {idx}: output extension {base.suffix} does not match output-format {output_format}.\"\n            )\n        base = out_dir / base.name\n    else:\n        slug = _slugify(prompt[:80])\n        base = out_dir / f\"{idx:03d}-{slug}{ext}\"\n\n    if n == 1:\n        return [base]\n    return [\n        base.with_name(f\"{base.stem}-{i}{base.suffix}\")\n        for i in range(1, n + 1)\n    ]\n\n\ndef _extract_retry_after_seconds(exc: Exception) -> Optional[float]:\n    # Best-effort: openai SDK errors vary by version. Prefer a conservative fallback.\n    for attr in (\"retry_after\", \"retry_after_seconds\"):\n        val = getattr(exc, attr, None)\n        if isinstance(val, (int, float)) and val >= 0:\n            return float(val)\n    msg = str(exc)\n    m = re.search(r\"retry[- ]after[:= ]+([0-9]+(?:\\\\.[0-9]+)?)\", msg, re.IGNORECASE)\n    if m:\n        try:\n            return float(m.group(1))\n        except Exception:\n            return None\n    return None\n\n\ndef _is_rate_limit_error(exc: Exception) -> bool:\n    name = exc.__class__.__name__.lower()\n    if \"ratelimit\" in name or \"rate_limit\" in name:\n        return True\n    msg = str(exc).lower()\n    return \"429\" in msg or \"rate limit\" in msg or \"too many requests\" in msg\n\n\ndef _is_transient_error(exc: Exception) -> bool:\n    if _is_rate_limit_error(exc):\n        return True\n    name = exc.__class__.__name__.lower()\n    if \"timeout\" in name or \"timedout\" in name or \"tempor\" in name:\n        return True\n    msg = str(exc).lower()\n    return \"timeout\" in msg or \"timed out\" in msg or \"connection reset\" in msg\n\n\nasync def _generate_one_with_retries(\n    client: Any,\n    payload: Dict[str, Any],\n    *,\n    attempts: int,\n    job_label: str,\n) -> Any:\n    last_exc: Optional[Exception] = None\n    for attempt in range(1, attempts + 1):\n        try:\n            return await client.images.generate(**payload)\n        except Exception as exc:\n            last_exc = exc\n            if not _is_transient_error(exc):\n                raise\n            if attempt == attempts:\n                raise\n            sleep_s = _extract_retry_after_seconds(exc)\n            if sleep_s is None:\n                sleep_s = min(60.0, 2.0**attempt)\n            print(\n                f\"{job_label} attempt {attempt}/{attempts} failed ({exc.__class__.__name__}); retrying in {sleep_s:.1f}s\",\n                file=sys.stderr,\n            )\n            await asyncio.sleep(sleep_s)\n    raise last_exc or RuntimeError(\"unknown error\")\n\n\nasync def _run_generate_batch(args: argparse.Namespace) -> int:\n    jobs = _read_jobs_jsonl(args.input)\n    out_dir = Path(args.out_dir)\n\n    base_fields = _fields_from_args(args)\n    base_payload = {\n        \"model\": args.model,\n        \"n\": args.n,\n        \"size\": args.size,\n        \"quality\": args.quality,\n        \"background\": args.background,\n        \"output_format\": args.output_format,\n        \"output_compression\": args.output_compression,\n        \"moderation\": args.moderation,\n    }\n\n    if args.dry_run:\n        for i, job in enumerate(jobs, start=1):\n            prompt = str(job[\"prompt\"]).strip()\n            fields = _merge_non_null(base_fields, job.get(\"fields\", {}))\n            # Allow flat job keys as well (use_case, scene, etc.)\n            fields = _merge_non_null(fields, {k: job.get(k) for k in base_fields.keys()})\n            augmented = _augment_prompt_fields(args.augment, prompt, fields)\n\n            job_payload = dict(base_payload)\n            job_payload[\"prompt\"] = augmented\n            job_payload = _merge_non_null(job_payload, {k: job.get(k) for k in base_payload.keys()})\n            job_payload = {k: v for k, v in job_payload.items() if v is not None}\n\n            _validate_generate_payload(job_payload)\n            effective_output_format = _normalize_output_format(job_payload.get(\"output_format\"))\n            _validate_transparency(job_payload.get(\"background\"), effective_output_format)\n            if \"output_format\" in job_payload:\n                job_payload[\"output_format\"] = effective_output_format\n\n            n = int(job_payload.get(\"n\", 1))\n            outputs = _job_output_paths(\n                out_dir=out_dir,\n                output_format=effective_output_format,\n                idx=i,\n                prompt=prompt,\n                n=n,\n                explicit_out=job.get(\"out\"),\n            )\n            downscaled = None\n            if args.downscale_max_dim is not None:\n                downscaled = [\n                    str(_derive_downscale_path(p, args.downscale_suffix)) for p in outputs\n                ]\n            _print_request(\n                {\n                    \"endpoint\": \"/v1/images/generations\",\n                    \"job\": i,\n                    \"outputs\": [str(p) for p in outputs],\n                    \"outputs_downscaled\": downscaled,\n                    **job_payload,\n                }\n            )\n        return 0\n\n    client = _create_async_client()\n    sem = asyncio.Semaphore(args.concurrency)\n\n    any_failed = False\n\n    async def run_job(i: int, job: Dict[str, Any]) -> Tuple[int, Optional[str]]:\n        nonlocal any_failed\n        prompt = str(job[\"prompt\"]).strip()\n        job_label = f\"[job {i}/{len(jobs)}]\"\n\n        fields = _merge_non_null(base_fields, job.get(\"fields\", {}))\n        fields = _merge_non_null(fields, {k: job.get(k) for k in base_fields.keys()})\n        augmented = _augment_prompt_fields(args.augment, prompt, fields)\n\n        payload = dict(base_payload)\n        payload[\"prompt\"] = augmented\n        payload = _merge_non_null(payload, {k: job.get(k) for k in base_payload.keys()})\n        payload = {k: v for k, v in payload.items() if v is not None}\n\n        n = int(payload.get(\"n\", 1))\n        _validate_generate_payload(payload)\n        effective_output_format = _normalize_output_format(payload.get(\"output_format\"))\n        _validate_transparency(payload.get(\"background\"), effective_output_format)\n        if \"output_format\" in payload:\n            payload[\"output_format\"] = effective_output_format\n        outputs = _job_output_paths(\n            out_dir=out_dir,\n            output_format=effective_output_format,\n            idx=i,\n            prompt=prompt,\n            n=n,\n            explicit_out=job.get(\"out\"),\n        )\n        try:\n            async with sem:\n                print(f\"{job_label} starting\", file=sys.stderr)\n                started = time.time()\n                result = await _generate_one_with_retries(\n                    client,\n                    payload,\n                    attempts=args.max_attempts,\n                    job_label=job_label,\n                )\n                elapsed = time.time() - started\n                print(f\"{job_label} completed in {elapsed:.1f}s\", file=sys.stderr)\n            images = [item.b64_json for item in result.data]\n            _decode_write_and_downscale(\n                images,\n                outputs,\n                force=args.force,\n                downscale_max_dim=args.downscale_max_dim,\n                downscale_suffix=args.downscale_suffix,\n                output_format=effective_output_format,\n            )\n            return i, None\n        except Exception as exc:\n            any_failed = True\n            print(f\"{job_label} failed: {exc}\", file=sys.stderr)\n            if args.fail_fast:\n                raise\n            return i, str(exc)\n\n    tasks = [asyncio.create_task(run_job(i, job)) for i, job in enumerate(jobs, start=1)]\n\n    try:\n        await asyncio.gather(*tasks)\n    except Exception:\n        for t in tasks:\n            if not t.done():\n                t.cancel()\n        raise\n\n    return 1 if any_failed else 0\n\n\ndef _generate_batch(args: argparse.Namespace) -> None:\n    exit_code = asyncio.run(_run_generate_batch(args))\n    if exit_code:\n        raise SystemExit(exit_code)\n\n\ndef _generate(args: argparse.Namespace) -> None:\n    prompt = _read_prompt(args.prompt, args.prompt_file)\n    prompt = _augment_prompt(args, prompt)\n\n    payload = {\n        \"model\": args.model,\n        \"prompt\": prompt,\n        \"n\": args.n,\n        \"size\": args.size,\n        \"quality\": args.quality,\n        \"background\": args.background,\n        \"output_format\": args.output_format,\n        \"output_compression\": args.output_compression,\n        \"moderation\": args.moderation,\n    }\n    payload = {k: v for k, v in payload.items() if v is not None}\n\n    output_format = _normalize_output_format(args.output_format)\n    _validate_transparency(args.background, output_format)\n    if \"output_format\" in payload:\n        payload[\"output_format\"] = output_format\n    output_paths = _build_output_paths(args.out, output_format, args.n, args.out_dir)\n\n    if args.dry_run:\n        _print_request({\"endpoint\": \"/v1/images/generations\", **payload})\n        return\n\n    print(\n        \"Calling Image API (generation). This can take up to a couple of minutes.\",\n        file=sys.stderr,\n    )\n    started = time.time()\n    client = _create_client()\n    result = client.images.generate(**payload)\n    elapsed = time.time() - started\n    print(f\"Generation completed in {elapsed:.1f}s.\", file=sys.stderr)\n\n    images = [item.b64_json for item in result.data]\n    _decode_write_and_downscale(\n        images,\n        output_paths,\n        force=args.force,\n        downscale_max_dim=args.downscale_max_dim,\n        downscale_suffix=args.downscale_suffix,\n        output_format=output_format,\n    )\n\n\ndef _edit(args: argparse.Namespace) -> None:\n    prompt = _read_prompt(args.prompt, args.prompt_file)\n    prompt = _augment_prompt(args, prompt)\n\n    image_paths = _check_image_paths(args.image)\n    mask_path = Path(args.mask) if args.mask else None\n    if mask_path:\n        if not mask_path.exists():\n            _die(f\"Mask file not found: {mask_path}\")\n        if mask_path.suffix.lower() != \".png\":\n            _warn(f\"Mask should be a PNG with an alpha channel: {mask_path}\")\n        if mask_path.stat().st_size > MAX_IMAGE_BYTES:\n            _warn(f\"Mask exceeds 50MB limit: {mask_path}\")\n\n    payload = {\n        \"model\": args.model,\n        \"prompt\": prompt,\n        \"n\": args.n,\n        \"size\": args.size,\n        \"quality\": args.quality,\n        \"background\": args.background,\n        \"output_format\": args.output_format,\n        \"output_compression\": args.output_compression,\n        \"input_fidelity\": args.input_fidelity,\n        \"moderation\": args.moderation,\n    }\n    payload = {k: v for k, v in payload.items() if v is not None}\n\n    output_format = _normalize_output_format(args.output_format)\n    _validate_transparency(args.background, output_format)\n    if \"output_format\" in payload:\n        payload[\"output_format\"] = output_format\n    output_paths = _build_output_paths(args.out, output_format, args.n, args.out_dir)\n\n    if args.dry_run:\n        payload_preview = dict(payload)\n        payload_preview[\"image\"] = [str(p) for p in image_paths]\n        if mask_path:\n            payload_preview[\"mask\"] = str(mask_path)\n        _print_request({\"endpoint\": \"/v1/images/edits\", **payload_preview})\n        return\n\n    print(\n        f\"Calling Image API (edit) with {len(image_paths)} image(s).\",\n        file=sys.stderr,\n    )\n    started = time.time()\n    client = _create_client()\n\n    with _open_files(image_paths) as image_files, _open_mask(mask_path) as mask_file:\n        request = dict(payload)\n        request[\"image\"] = image_files if len(image_files) > 1 else image_files[0]\n        if mask_file is not None:\n            request[\"mask\"] = mask_file\n        result = client.images.edit(**request)\n\n    elapsed = time.time() - started\n    print(f\"Edit completed in {elapsed:.1f}s.\", file=sys.stderr)\n    images = [item.b64_json for item in result.data]\n    _decode_write_and_downscale(\n        images,\n        output_paths,\n        force=args.force,\n        downscale_max_dim=args.downscale_max_dim,\n        downscale_suffix=args.downscale_suffix,\n        output_format=output_format,\n    )\n\n\ndef _open_files(paths: List[Path]):\n    return _FileBundle(paths)\n\n\ndef _open_mask(mask_path: Optional[Path]):\n    if mask_path is None:\n        return _NullContext()\n    return _SingleFile(mask_path)\n\n\nclass _NullContext:\n    def __enter__(self):\n        return None\n\n    def __exit__(self, exc_type, exc, tb):\n        return False\n\n\nclass _SingleFile:\n    def __init__(self, path: Path):\n        self._path = path\n        self._handle = None\n\n    def __enter__(self):\n        self._handle = self._path.open(\"rb\")\n        return self._handle\n\n    def __exit__(self, exc_type, exc, tb):\n        if self._handle:\n            try:\n                self._handle.close()\n            except Exception:\n                pass\n        return False\n\n\nclass _FileBundle:\n    def __init__(self, paths: List[Path]):\n        self._paths = paths\n        self._handles: List[object] = []\n\n    def __enter__(self):\n        self._handles = [p.open(\"rb\") for p in self._paths]\n        return self._handles\n\n    def __exit__(self, exc_type, exc, tb):\n        for handle in self._handles:\n            try:\n                handle.close()\n            except Exception:\n                pass\n        return False\n\n\ndef _add_shared_args(parser: argparse.ArgumentParser) -> None:\n    parser.add_argument(\"--model\", default=DEFAULT_MODEL)\n    parser.add_argument(\"--prompt\")\n    parser.add_argument(\"--prompt-file\")\n    parser.add_argument(\"--n\", type=int, default=1)\n    parser.add_argument(\"--size\", default=DEFAULT_SIZE)\n    parser.add_argument(\"--quality\", default=DEFAULT_QUALITY)\n    parser.add_argument(\"--background\")\n    parser.add_argument(\"--output-format\")\n    parser.add_argument(\"--output-compression\", type=int)\n    parser.add_argument(\"--moderation\")\n    parser.add_argument(\"--out\", default=\"output.png\")\n    parser.add_argument(\"--out-dir\")\n    parser.add_argument(\"--force\", action=\"store_true\")\n    parser.add_argument(\"--dry-run\", action=\"store_true\")\n    parser.add_argument(\"--augment\", dest=\"augment\", action=\"store_true\")\n    parser.add_argument(\"--no-augment\", dest=\"augment\", action=\"store_false\")\n    parser.set_defaults(augment=True)\n\n    # Prompt augmentation hints\n    parser.add_argument(\"--use-case\")\n    parser.add_argument(\"--scene\")\n    parser.add_argument(\"--subject\")\n    parser.add_argument(\"--style\")\n    parser.add_argument(\"--composition\")\n    parser.add_argument(\"--lighting\")\n    parser.add_argument(\"--palette\")\n    parser.add_argument(\"--materials\")\n    parser.add_argument(\"--text\")\n    parser.add_argument(\"--constraints\")\n    parser.add_argument(\"--negative\")\n\n    # Post-processing (optional): generate an additional downscaled copy for fast web loading.\n    parser.add_argument(\"--downscale-max-dim\", type=int)\n    parser.add_argument(\"--downscale-suffix\", default=DEFAULT_DOWNSCALE_SUFFIX)\n\n\ndef main() -> int:\n    parser = argparse.ArgumentParser(description=\"Generate or edit images via the Image API\")\n    subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n    gen_parser = subparsers.add_parser(\"generate\", help=\"Create a new image\")\n    _add_shared_args(gen_parser)\n    gen_parser.set_defaults(func=_generate)\n\n    batch_parser = subparsers.add_parser(\n        \"generate-batch\",\n        help=\"Generate multiple prompts concurrently (JSONL input)\",\n    )\n    _add_shared_args(batch_parser)\n    batch_parser.add_argument(\"--input\", required=True, help=\"Path to JSONL file (one job per line)\")\n    batch_parser.add_argument(\"--concurrency\", type=int, default=DEFAULT_CONCURRENCY)\n    batch_parser.add_argument(\"--max-attempts\", type=int, default=3)\n    batch_parser.add_argument(\"--fail-fast\", action=\"store_true\")\n    batch_parser.set_defaults(func=_generate_batch)\n\n    edit_parser = subparsers.add_parser(\"edit\", help=\"Edit an existing image\")\n    _add_shared_args(edit_parser)\n    edit_parser.add_argument(\"--image\", action=\"append\", required=True)\n    edit_parser.add_argument(\"--mask\")\n    edit_parser.add_argument(\"--input-fidelity\")\n    edit_parser.set_defaults(func=_edit)\n\n    args = parser.parse_args()\n    if args.n < 1 or args.n > 10:\n        _die(\"--n must be between 1 and 10\")\n    if getattr(args, \"concurrency\", 1) < 1 or getattr(args, \"concurrency\", 1) > 25:\n        _die(\"--concurrency must be between 1 and 25\")\n    if getattr(args, \"max_attempts\", 3) < 1 or getattr(args, \"max_attempts\", 3) > 10:\n        _die(\"--max-attempts must be between 1 and 10\")\n    if args.output_compression is not None and not (0 <= args.output_compression <= 100):\n        _die(\"--output-compression must be between 0 and 100\")\n    if args.command == \"generate-batch\" and not args.out_dir:\n        _die(\"generate-batch requires --out-dir\")\n    if getattr(args, \"downscale_max_dim\", None) is not None and args.downscale_max_dim < 1:\n        _die(\"--downscale-max-dim must be >= 1\")\n\n    _validate_size(args.size)\n    _validate_quality(args.quality)\n    _validate_background(args.background)\n    _ensure_api_key(args.dry_run)\n\n    args.func(args)\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "skills/.curated/jupyter-notebook/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/jupyter-notebook/SKILL.md",
    "content": "---\nname: \"jupyter-notebook\"\ndescription: \"Use when the user asks to create, scaffold, or edit Jupyter notebooks (`.ipynb`) for experiments, explorations, or tutorials; prefer the bundled templates and run the helper script `new_notebook.py` to generate a clean starting notebook.\"\n---\n\n\n# Jupyter Notebook Skill\n\nCreate clean, reproducible Jupyter notebooks for two primary modes:\n\n- Experiments and exploratory analysis\n- Tutorials and teaching-oriented walkthroughs\n\nPrefer the bundled templates and the helper script for consistent structure and fewer JSON mistakes.\n\n## When to use\n- Create a new `.ipynb` notebook from scratch.\n- Convert rough notes or scripts into a structured notebook.\n- Refactor an existing notebook to be more reproducible and skimmable.\n- Build experiments or tutorials that will be read or re-run by other people.\n\n## Decision tree\n- If the request is exploratory, analytical, or hypothesis-driven, choose `experiment`.\n- If the request is instructional, step-by-step, or audience-specific, choose `tutorial`.\n- If editing an existing notebook, treat it as a refactor: preserve intent and improve structure.\n\n## Skill path (set once)\n\n```bash\nexport CODEX_HOME=\"${CODEX_HOME:-$HOME/.codex}\"\nexport JUPYTER_NOTEBOOK_CLI=\"$CODEX_HOME/skills/jupyter-notebook/scripts/new_notebook.py\"\n```\n\nUser-scoped skills install under `$CODEX_HOME/skills` (default: `~/.codex/skills`).\n\n## Workflow\n1. Lock the intent.\nIdentify the notebook kind: `experiment` or `tutorial`.\nCapture the objective, audience, and what \"done\" looks like.\n\n2. Scaffold from the template.\nUse the helper script to avoid hand-authoring raw notebook JSON.\n\n```bash\nuv run --python 3.12 python \"$JUPYTER_NOTEBOOK_CLI\" \\\n  --kind experiment \\\n  --title \"Compare prompt variants\" \\\n  --out output/jupyter-notebook/compare-prompt-variants.ipynb\n```\n\n```bash\nuv run --python 3.12 python \"$JUPYTER_NOTEBOOK_CLI\" \\\n  --kind tutorial \\\n  --title \"Intro to embeddings\" \\\n  --out output/jupyter-notebook/intro-to-embeddings.ipynb\n```\n\n3. Fill the notebook with small, runnable steps.\nKeep each code cell focused on one step.\nAdd short markdown cells that explain the purpose and expected result.\nAvoid large, noisy outputs when a short summary works.\n\n4. Apply the right pattern.\nFor experiments, follow `references/experiment-patterns.md`.\nFor tutorials, follow `references/tutorial-patterns.md`.\n\n5. Edit safely when working with existing notebooks.\nPreserve the notebook structure; avoid reordering cells unless it improves the top-to-bottom story.\nPrefer targeted edits over full rewrites.\nIf you must edit raw JSON, review `references/notebook-structure.md` first.\n\n6. Validate the result.\nRun the notebook top-to-bottom when the environment allows.\nIf execution is not possible, say so explicitly and call out how to validate locally.\nUse the final pass checklist in `references/quality-checklist.md`.\n\n## Templates and helper script\n- Templates live in `assets/experiment-template.ipynb` and `assets/tutorial-template.ipynb`.\n- The helper script loads a template, updates the title cell, and writes a notebook.\n\nScript path:\n- `$JUPYTER_NOTEBOOK_CLI` (installed default: `$CODEX_HOME/skills/jupyter-notebook/scripts/new_notebook.py`)\n\n## Temp and output conventions\n- Use `tmp/jupyter-notebook/` for intermediate files; delete when done.\n- Write final artifacts under `output/jupyter-notebook/` when working in this repo.\n- Use stable, descriptive filenames (for example, `ablation-temperature.ipynb`).\n\n## Dependencies (install only when needed)\nPrefer `uv` for dependency management.\n\nOptional Python packages for local notebook execution:\n\n```bash\nuv pip install jupyterlab ipykernel\n```\n\nThe bundled scaffold script uses only the Python standard library and does not require extra dependencies.\n\n## Environment\nNo required environment variables.\n\n## Reference map\n- `references/experiment-patterns.md`: experiment structure and heuristics.\n- `references/tutorial-patterns.md`: tutorial structure and teaching flow.\n- `references/notebook-structure.md`: notebook JSON shape and safe editing rules.\n- `references/quality-checklist.md`: final validation checklist.\n"
  },
  {
    "path": "skills/.curated/jupyter-notebook/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Jupyter Notebooks\"\n  short_description: \"Create Jupyter notebooks for experiments and tutorials\"\n  icon_small: \"./assets/jupyter-small.svg\"\n  icon_large: \"./assets/jupyter.png\"\n  default_prompt: \"Create a Jupyter notebook for this task with clear sections, runnable cells, and concise takeaways.\"\n"
  },
  {
    "path": "skills/.curated/jupyter-notebook/assets/experiment-template.ipynb",
    "content": "{\n  \"cells\": [\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"# Experiment: TITLE\\n\",\n        \"\\n\",\n        \"Objective:\\n\",\n        \"- State the question you want to answer.\\n\",\n        \"- Define the success criteria.\\n\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": null,\n      \"metadata\": {},\n      \"outputs\": [],\n      \"source\": [\n        \"# Setup: imports and reproducibility\\n\",\n        \"from __future__ import annotations\\n\",\n        \"\\n\",\n        \"import random\\n\",\n        \"import statistics\\n\",\n        \"\\n\",\n        \"SEED = 7\\n\",\n        \"random.seed(SEED)\\n\",\n        \"SEED\\n\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## Plan\\n\",\n        \"\\n\",\n        \"- Hypothesis:\\n\",\n        \"- Variables to sweep:\\n\",\n        \"- Metrics to record:\\n\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": null,\n      \"metadata\": {},\n      \"outputs\": [],\n      \"source\": [\n        \"# Define parameters and lightweight helpers\\n\",\n        \"sample_size = 20\\n\",\n        \"values = [random.random() for _ in range(sample_size)]\\n\",\n        \"summary = {\\n\",\n        \"    \\\"count\\\": len(values),\\n\",\n        \"    \\\"mean\\\": statistics.fmean(values),\\n\",\n        \"    \\\"min\\\": min(values),\\n\",\n        \"    \\\"max\\\": max(values),\\n\",\n        \"}\\n\",\n        \"summary\\n\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## Results\\n\",\n        \"\\n\",\n        \"- Key observations:\\n\",\n        \"- Surprises or failure modes:\\n\",\n        \"- Decision: continue, pivot, or stop:\\n\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": null,\n      \"metadata\": {},\n      \"outputs\": [],\n      \"source\": [\n        \"# Record findings in a minimal, copy-pasteable structure\\n\",\n        \"result = {\\n\",\n        \"    \\\"seed\\\": SEED,\\n\",\n        \"    \\\"mean\\\": summary[\\\"mean\\\"],\\n\",\n        \"    \\\"range\\\": summary[\\\"max\\\"] - summary[\\\"min\\\"],\\n\",\n        \"}\\n\",\n        \"result\\n\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## Next steps\\n\",\n        \"\\n\",\n        \"- What to try next:\\n\",\n        \"- What to document elsewhere (PRD, notes, issue):\\n\"\n      ]\n    }\n  ],\n  \"metadata\": {\n    \"kernelspec\": {\n      \"display_name\": \"Python 3\",\n      \"language\": \"python\",\n      \"name\": \"python3\"\n    },\n    \"language_info\": {\n      \"name\": \"python\",\n      \"version\": \"3.12\"\n    }\n  },\n  \"nbformat\": 4,\n  \"nbformat_minor\": 5\n}\n"
  },
  {
    "path": "skills/.curated/jupyter-notebook/assets/tutorial-template.ipynb",
    "content": "{\n  \"cells\": [\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"# Tutorial: TITLE\\n\",\n        \"\\n\",\n        \"Audience:\\n\",\n        \"- Describe who this is for.\\n\",\n        \"\\n\",\n        \"Prerequisites:\\n\",\n        \"- List required concepts or setup.\\n\",\n        \"\\n\",\n        \"Learning goals:\\n\",\n        \"- By the end, the reader can...\\n\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## Outline\\n\",\n        \"\\n\",\n        \"1. Setup\\n\",\n        \"2. A minimal working example\\n\",\n        \"3. Variations and pitfalls\\n\",\n        \"4. Exercises\\n\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": null,\n      \"metadata\": {},\n      \"outputs\": [],\n      \"source\": [\n        \"# Setup cell: keep it short and deterministic\\n\",\n        \"from __future__ import annotations\\n\",\n        \"\\n\",\n        \"import math\\n\",\n        \"import random\\n\",\n        \"\\n\",\n        \"SEED = 21\\n\",\n        \"random.seed(SEED)\\n\",\n        \"SEED\\n\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## Step 1 - Start with a tiny example\\n\",\n        \"\\n\",\n        \"Explain what the next cell does in plain language.\\n\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": null,\n      \"metadata\": {},\n      \"outputs\": [],\n      \"source\": [\n        \"# Minimal working example\\n\",\n        \"angles = [0, math.pi / 4, math.pi / 2]\\n\",\n        \"sines = [math.sin(a) for a in angles]\\n\",\n        \"list(zip(angles, sines))\\n\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## Exercises\\n\",\n        \"\\n\",\n        \"- Try a different input.\\n\",\n        \"- Predict the output before running the code.\\n\",\n        \"- Note one common mistake and how to fix it.\\n\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": null,\n      \"metadata\": {},\n      \"outputs\": [],\n      \"source\": [\n        \"# Exercise answer scaffold\\n\",\n        \"def describe(values: list[float]) -> dict[str, float]:\\n\",\n        \"    return {\\\"min\\\": min(values), \\\"max\\\": max(values)}\\n\",\n        \"\\n\",\n        \"describe(sines)\\n\"\n      ]\n    }\n  ],\n  \"metadata\": {\n    \"kernelspec\": {\n      \"display_name\": \"Python 3\",\n      \"language\": \"python\",\n      \"name\": \"python3\"\n    },\n    \"language_info\": {\n      \"name\": \"python\",\n      \"version\": \"3.12\"\n    }\n  },\n  \"nbformat\": 4,\n  \"nbformat_minor\": 5\n}\n"
  },
  {
    "path": "skills/.curated/jupyter-notebook/references/experiment-patterns.md",
    "content": "# Experiment Patterns\n\nUse this structure for exploratory and experimental work:\n\n- Title and objective: state the question and the success criteria.\n- Setup and reproducibility: import only what you need, set a seed early, and keep configuration in one short cell.\n- Plan: list hypotheses, sweeps, and metrics before running code.\n- Minimal baseline: start with the smallest runnable example and confirm it runs end-to-end before adding complexity.\n- Results and notes: summarize findings in markdown near the relevant code and record key metrics in a small dictionary or table-like structure.\n- Next steps: decide whether to continue, pivot, or stop, and capture follow-up ideas as short bullets.\n"
  },
  {
    "path": "skills/.curated/jupyter-notebook/references/notebook-structure.md",
    "content": "# Notebook Structure\n\nJupyter notebooks are JSON documents with this high-level shape:\n\n- `nbformat` and `nbformat_minor`\n- `metadata`\n- `cells` (a list of markdown and code cells)\n\nWhen editing `.ipynb` files programmatically:\n\n- Preserve `nbformat` and `nbformat_minor` from the template.\n- Keep `cells` as an ordered list; do not reorder unless intentional.\n- For code cells, set `execution_count` to `null` when unknown.\n- For code cells, set `outputs` to an empty list when scaffolding.\n- For markdown cells, keep `cell_type=\"markdown\"` and `metadata={}`.\n\nPrefer scaffolding from the bundled templates or `new_notebook.py` (for example, `$CODEX_HOME/skills/jupyter-notebook/scripts/new_notebook.py`) instead of hand-authoring raw notebook JSON.\n"
  },
  {
    "path": "skills/.curated/jupyter-notebook/references/quality-checklist.md",
    "content": "# Quality Checklist\n\nBefore delivering a notebook:\n\n- Run it top-to-bottom at least once (or as much as the environment allows).\n- Ensure early cells set all required state; avoid hidden state from prior runs.\n- Keep outputs tidy. Avoid giant outputs when a short summary works.\n- Prefer small tables, key metrics, or short printouts.\n- Keep the narrative skimmable. Use headings and short bullets, and avoid long paragraphs.\n- Leave helpful TODOs only when necessary, and label them clearly.\n- If execution is not possible, call out the risk and how to validate locally.\n"
  },
  {
    "path": "skills/.curated/jupyter-notebook/references/tutorial-patterns.md",
    "content": "# Tutorial Patterns\n\nUse this structure for teaching and walkthroughs:\n\n- Audience, prerequisites, and learning goals: say who it is for, list what they should already know, and state what they will be able to do by the end.\n- Outline: provide a short numbered outline so readers can skim.\n- Step-by-step flow: pair a short markdown explanation with a small code cell that runs on its own and a brief interpretation of the result.\n- Exercises: include at least one exercise that reinforces the key concept and provide an answer scaffold in the next cell.\n- Pitfalls and extensions: call out one common mistake and how to fix it, and suggest one optional extension for curious readers.\n"
  },
  {
    "path": "skills/.curated/jupyter-notebook/scripts/new_notebook.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport json\nimport re\nfrom pathlib import Path\nfrom typing import Any\n\n\ndef slugify(text: str) -> str:\n    lowered = text.strip().lower()\n    cleaned = re.sub(r\"[^a-z0-9]+\", \"-\", lowered)\n    collapsed = re.sub(r\"-+\", \"-\", cleaned).strip(\"-\")\n    return collapsed or \"notebook\"\n\n\ndef find_repo_root(start: Path) -> Path:\n    for candidate in (start, *start.parents):\n        if (candidate / \".git\").exists():\n            return candidate\n    return start\n\n\ndef load_template(skill_dir: Path, kind: str) -> dict[str, Any]:\n    asset_name = \"experiment-template.ipynb\" if kind == \"experiment\" else \"tutorial-template.ipynb\"\n    template_path = skill_dir / \"assets\" / asset_name\n    if not template_path.exists():\n        raise SystemExit(f\"Missing template: {template_path}\")\n    with template_path.open(\"r\", encoding=\"utf-8\") as f:\n        data = json.load(f)\n    if not isinstance(data, dict):\n        raise SystemExit(f\"Unexpected template shape: {template_path}\")\n    return data\n\n\ndef update_title(notebook: dict[str, Any], kind: str, title: str) -> None:\n    prefix = \"Experiment\" if kind == \"experiment\" else \"Tutorial\"\n    expected = f\"# {prefix}: {title}\\n\"\n\n    cells = notebook.get(\"cells\")\n    if not isinstance(cells, list) or not cells:\n        raise SystemExit(\"Template notebook has no cells\")\n\n    first_cell = cells[0]\n    if not isinstance(first_cell, dict) or first_cell.get(\"cell_type\") != \"markdown\":\n        raise SystemExit(\"Template notebook must start with a markdown title cell\")\n\n    source = first_cell.get(\"source\", [])\n    if isinstance(source, str):\n        source_lines = [source]\n    elif isinstance(source, list):\n        source_lines = [str(line) for line in source]\n    else:\n        source_lines = []\n\n    if source_lines:\n        source_lines[0] = expected\n    else:\n        source_lines = [expected]\n\n    first_cell[\"source\"] = source_lines\n\n    metadata = notebook.setdefault(\"metadata\", {})\n    if not isinstance(metadata, dict):\n        raise SystemExit(\"Notebook metadata must be a mapping\")\n\n    language_info = metadata.setdefault(\"language_info\", {})\n    if isinstance(language_info, dict):\n        language_info.setdefault(\"name\", \"python\")\n        language_info.setdefault(\"version\", \"3.12\")\n\n\ndef default_output(repo_root: Path, title: str) -> Path:\n    filename = f\"{slugify(title)}.ipynb\"\n    return repo_root / \"output\" / \"jupyter-notebook\" / filename\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(description=\"Scaffold a Jupyter notebook for experiments or tutorials.\")\n    parser.add_argument(\n        \"--kind\",\n        choices=[\"experiment\", \"tutorial\"],\n        default=\"experiment\",\n        help=\"Notebook style to scaffold (default: experiment).\",\n    )\n    parser.add_argument(\n        \"--title\",\n        required=True,\n        help=\"Human-readable notebook title used in the first markdown cell.\",\n    )\n    parser.add_argument(\n        \"--out\",\n        type=Path,\n        default=None,\n        help=\"Output path for the notebook. Defaults to output/jupyter-notebook/<slug>.ipynb.\",\n    )\n    parser.add_argument(\n        \"--force\",\n        action=\"store_true\",\n        help=\"Overwrite the output file if it already exists.\",\n    )\n    return parser.parse_args()\n\n\ndef main() -> None:\n    args = parse_args()\n\n    script_path = Path(__file__).resolve()\n    skill_dir = script_path.parents[1]\n    repo_root = find_repo_root(skill_dir)\n\n    notebook = load_template(skill_dir, args.kind)\n    update_title(notebook, args.kind, args.title)\n\n    out_path = args.out or default_output(repo_root, args.title)\n    out_path = out_path.resolve()\n\n    if out_path.exists() and not args.force:\n        raise SystemExit(f\"Refusing to overwrite existing file without --force: {out_path}\")\n\n    out_path.parent.mkdir(parents=True, exist_ok=True)\n    with out_path.open(\"w\", encoding=\"utf-8\") as f:\n        json.dump(notebook, f, indent=2)\n        f.write(\"\\n\")\n\n    print(f\"Wrote {out_path} using kind={args.kind}.\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/linear/LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "skills/.curated/linear/SKILL.md",
    "content": "---\nname: linear\ndescription: Manage issues, projects & team workflows in Linear. Use when the user wants to read, create or updates tickets in Linear.\nmetadata:\n  short-description: Manage Linear issues in Codex\n---\n\n# Linear\n\n## Overview\n\nThis skill provides a structured workflow for managing issues, projects & team workflows in Linear. It ensures consistent integration with the Linear MCP server, which offers natural-language project management for issues, projects, documentation, and team collaboration.\n\n## Prerequisites\n- Linear MCP server must be connected and accessible via OAuth\n- Confirm access to the relevant Linear workspace, teams, and projects\n\n## Required Workflow\n\n**Follow these steps in order. Do not skip steps.**\n\n### Step 0: Set up Linear MCP (if not already configured)\n\nIf any MCP call fails because Linear MCP is not connected, pause and set it up:\n\n1. Add the Linear MCP:\n   - `codex mcp add linear --url https://mcp.linear.app/mcp`\n2. Enable remote MCP client:\n   - Set `[features] rmcp_client = true` in `config.toml` **or** run `codex --enable rmcp_client`\n3. Log in with OAuth:\n   - `codex mcp login linear`\n\nAfter successful login, the user will have to restart codex. You should finish your answer and tell them so when they try again they can continue with Step 1.\n\n**Windows/WSL note:** If you see connection errors on Windows, try configuring the Linear MCP to run via WSL:\n```json\n{\"mcpServers\": {\"linear\": {\"command\": \"wsl\", \"args\": [\"npx\", \"-y\", \"mcp-remote\", \"https://mcp.linear.app/sse\", \"--transport\", \"sse-only\"]}}}\n```\n\n### Step 1\nClarify the user's goal and scope (e.g., issue triage, sprint planning, documentation audit, workload balance). Confirm team/project, priority, labels, cycle, and due dates as needed.\n\n### Step 2\nSelect the appropriate workflow (see Practical Workflows below) and identify the Linear MCP tools you will need. Confirm required identifiers (issue ID, project ID, team key) before calling tools.\n\n### Step 3\nExecute Linear MCP tool calls in logical batches:\n- Read first (list/get/search) to build context.\n- Create or update next (issues, projects, labels, comments) with all required fields.\n- For bulk operations, explain the grouping logic before applying changes.\n\n### Step 4\nSummarize results, call out remaining gaps or blockers, and propose next actions (additional issues, label changes, assignments, or follow-up comments).\n\n## Available Tools\n\nIssue Management: `list_issues`, `get_issue`, `create_issue`, `update_issue`, `list_my_issues`, `list_issue_statuses`, `list_issue_labels`, `create_issue_label`\n\nProject & Team: `list_projects`, `get_project`, `create_project`, `update_project`, `list_teams`, `get_team`, `list_users`\n\nDocumentation & Collaboration: `list_documents`, `get_document`, `search_documentation`, `list_comments`, `create_comment`, `list_cycles`\n\n## Practical Workflows\n\n- Sprint Planning: Review open issues for a target team, pick top items by priority, and create a new cycle (e.g., \"Q1 Performance Sprint\") with assignments.\n- Bug Triage: List critical/high-priority bugs, rank by user impact, and move the top items to \"In Progress.\"\n- Documentation Audit: Search documentation (e.g., API auth), then open labeled \"documentation\" issues for gaps or outdated sections with detailed fixes.\n- Team Workload Balance: Group active issues by assignee, flag anyone with high load, and suggest or apply redistributions.\n- Release Planning: Create a project (e.g., \"v2.0 Release\") with milestones (feature freeze, beta, docs, launch) and generate issues with estimates.\n- Cross-Project Dependencies: Find all \"blocked\" issues, identify blockers, and create linked issues if missing.\n- Automated Status Updates: Find your issues with stale updates and add status comments based on current state/blockers.\n- Smart Labeling: Analyze unlabeled issues, suggest/apply labels, and create missing label categories.\n- Sprint Retrospectives: Generate a report for the last completed cycle, note completed vs. pushed work, and open discussion issues for patterns.\n\n## Tips for Maximum Productivity\n\n- Batch operations for related changes; consider smart templates for recurring issue structures.\n- Use natural queries when possible (\"Show me what John is working on this week\").\n- Leverage context: reference prior issues in new requests.\n- Break large updates into smaller batches to avoid rate limits; cache or reuse filters when listing frequently.\n\n## Troubleshooting\n\n- Authentication: Clear browser cookies, re-run OAuth, verify workspace permissions, ensure API access is enabled.\n- Tool Calling Errors: Confirm the model supports multiple tool calls, provide all required fields, and split complex requests.\n- Missing Data: Refresh token, verify workspace access, check for archived projects, and confirm correct team selection.\n- Performance: Remember Linear API rate limits; batch bulk operations, use specific filters, or cache frequent queries.\n"
  },
  {
    "path": "skills/.curated/linear/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Linear\"\n  short_description: \"Manage Linear issues in Codex\"\n  icon_small: \"./assets/linear-small.svg\"\n  icon_large: \"./assets/linear.png\"\n  default_prompt: \"Use Linear context to triage or update relevant issues for this task, with clear next actions.\"\n\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"linear\"\n      description: \"Linear MCP server\"\n      transport: \"streamable_http\"\n      url: \"https://mcp.linear.app/mcp\"\n"
  },
  {
    "path": "skills/.curated/netlify-deploy/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/netlify-deploy/SKILL.md",
    "content": "---\nname: netlify-deploy\ndescription: Deploy web projects to Netlify using the Netlify CLI (`npx netlify`). Use when the user asks to deploy, host, publish, or link a site/repo on Netlify, including preview and production deploys.\n---\n\n# Netlify Deployment Skill\n\nDeploy web projects to Netlify using the Netlify CLI with intelligent detection of project configuration and deployment context.\n\n## Overview\n\nThis skill automates Netlify deployments by:\n- Verifying Netlify CLI authentication\n- Detecting project configuration and framework\n- Linking to existing sites or creating new ones\n- Deploying to production or preview environments\n\n## Prerequisites\n\n- **Netlify CLI**: Installed via npx (no global install required)\n- **Authentication**: Netlify account with active login session\n- **Project**: Valid web project in current directory\n- When sandboxing blocks the deployment network calls, rerun with `sandbox_permissions=require_escalated`.\n- The deployment might take a few minutes. Use appropriate timeout values.\n\n## Authentication Pattern\n\nThe skill uses the **pre-authenticated Netlify CLI** approach:\n\n1. Check authentication status with `npx netlify status`\n2. If not authenticated, guide user through `npx netlify login`\n3. Fail gracefully if authentication cannot be established\n\nAuthentication uses either:\n- **Browser-based OAuth** (primary): `netlify login` opens browser for authentication\n- **API Key** (alternative): Set `NETLIFY_AUTH_TOKEN` environment variable\n\n## Workflow\n\n### 1. Verify Netlify CLI Authentication\n\nCheck if the user is logged into Netlify:\n\n```bash\nnpx netlify status\n```\n\n**Expected output patterns**:\n- ✅ Authenticated: Shows logged-in user email and site link status\n- ❌ Not authenticated: \"Not logged into any site\" or authentication error\n\n**If not authenticated**, guide the user:\n\n```bash\nnpx netlify login\n```\n\nThis opens a browser window for OAuth authentication. Wait for user to complete login, then verify with `netlify status` again.\n\n**Alternative: API Key authentication**\n\nIf browser authentication isn't available, users can set:\n\n```bash\nexport NETLIFY_AUTH_TOKEN=your_token_here\n```\n\nTokens can be generated at: https://app.netlify.com/user/applications#personal-access-tokens\n\n### 2. Detect Site Link Status\n\nFrom `netlify status` output, determine:\n- **Linked**: Site already connected to Netlify (shows site name/URL)\n- **Not linked**: Need to link or create site\n\n### 3. Link to Existing Site or Create New\n\n**If already linked** → Skip to step 4\n\n**If not linked**, attempt to link by Git remote:\n\n```bash\n# Check if project is Git-based\ngit remote show origin\n\n# If Git-based, extract remote URL\n# Format: https://github.com/username/repo or git@github.com:username/repo.git\n\n# Try to link by Git remote\nnpx netlify link --git-remote-url <REMOTE_URL>\n```\n\n**If link fails** (site doesn't exist on Netlify):\n\n```bash\n# Create new site interactively\nnpx netlify init\n```\n\nThis guides user through:\n1. Choosing team/account\n2. Setting site name\n3. Configuring build settings\n4. Creating netlify.toml if needed\n\n### 4. Verify Dependencies\n\nBefore deploying, ensure project dependencies are installed:\n\n```bash\n# For npm projects\nnpm install\n\n# For other package managers, detect and use appropriate command\n# yarn install, pnpm install, etc.\n```\n\n### 5. Deploy to Netlify\n\nChoose deployment type based on context:\n\n**Preview/Draft Deploy** (default for existing sites):\n\n```bash\nnpx netlify deploy\n```\n\nThis creates a deploy preview with a unique URL for testing.\n\n**Production Deploy** (for new sites or explicit production deployments):\n\n```bash\nnpx netlify deploy --prod\n```\n\nThis deploys to the live production URL.\n\n**Deployment process**:\n1. CLI detects build settings (from netlify.toml or prompts user)\n2. Builds the project locally\n3. Uploads built assets to Netlify\n4. Returns deployment URL\n\n### 6. Report Results\n\nAfter deployment, report to user:\n- **Deploy URL**: Unique URL for this deployment\n- **Site URL**: Production URL (if production deploy)\n- **Deploy logs**: Link to Netlify dashboard for logs\n- **Next steps**: Suggest `netlify open` to view site or dashboard\n\n## Handling netlify.toml\n\nIf a `netlify.toml` file exists, the CLI uses it automatically. If not, the CLI will prompt for:\n- **Build command**: e.g., `npm run build`, `next build`\n- **Publish directory**: e.g., `dist`, `build`, `.next`\n\nCommon framework defaults:\n- **Next.js**: build command `npm run build`, publish `.next`\n- **React (Vite)**: build command `npm run build`, publish `dist`\n- **Static HTML**: no build command, publish current directory\n\nThe skill should detect framework from `package.json` if possible and suggest appropriate settings.\n\n## Example Full Workflow\n\n```bash\n# 1. Check authentication\nnpx netlify status\n\n# If not authenticated:\nnpx netlify login\n\n# 2. Link site (if needed)\n# Try Git-based linking first\ngit remote show origin\nnpx netlify link --git-remote-url https://github.com/user/repo\n\n# If no site exists, create new one:\nnpx netlify init\n\n# 3. Install dependencies\nnpm install\n\n# 4. Deploy (preview for testing)\nnpx netlify deploy\n\n# 5. Deploy to production (when ready)\nnpx netlify deploy --prod\n```\n\n## Error Handling\n\nCommon issues and solutions:\n\n**\"Not logged in\"**\n→ Run `npx netlify login`\n\n**\"No site linked\"**\n→ Run `npx netlify link` or `npx netlify init`\n\n**\"Build failed\"**\n→ Check build command and publish directory in netlify.toml or CLI prompts\n→ Verify dependencies are installed\n→ Review build logs for specific errors\n\n**\"Publish directory not found\"**\n→ Verify build command ran successfully\n→ Check publish directory path is correct\n\n## Troubleshooting\n\n### Escalated Network Access\n\nIf deployment fails due to network issues (timeouts, DNS errors, connection resets), rerun the deploy with escalated permissions (use `sandbox_permissions=require_escalated`). The deploy requires escalated network access when sandbox networking blocks outbound requests.\n\nExample guidance to the user:\n\n```\nThe deploy needs escalated network access to deploy to Netlify. I can rerun the command with escalated permissions—want me to proceed?\n```\n\n## Environment Variables\n\nFor secrets and configuration:\n\n1. Never commit secrets to Git\n2. Set in Netlify dashboard: Site Settings → Environment Variables\n3. Access in builds via `process.env.VARIABLE_NAME`\n\n## Tips\n\n- Use `netlify deploy` (no `--prod`) first to test before production\n- Run `netlify open` to view site in Netlify dashboard\n- Run `netlify logs` to view function logs (if using Netlify Functions)\n- Use `netlify dev` for local development with Netlify Functions\n\n## Reference\n\n- Netlify CLI Docs: https://docs.netlify.com/cli/get-started/\n- netlify.toml Reference: https://docs.netlify.com/configure-builds/file-based-configuration/\n\n## Bundled References (Load As Needed)\n\n- [CLI commands](references/cli-commands.md)\n- [Deployment patterns](references/deployment-patterns.md)\n- [netlify.toml guide](references/netlify-toml.md)\n"
  },
  {
    "path": "skills/.curated/netlify-deploy/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Netlify Deploy\"\n  short_description: \"Deploy web projects to Netlify with the Netlify CLI\"\n  icon_small: \"./assets/netlify-small.svg\"\n  icon_large: \"./assets/netlify.png\"\n  default_prompt: \"Deploy this project to Netlify and return the preview URL, build settings, and any required fixes.\"\n"
  },
  {
    "path": "skills/.curated/netlify-deploy/references/cli-commands.md",
    "content": "# Netlify CLI Commands Reference\n\nQuick reference for common Netlify CLI commands used in deployments.\n\n## Authentication\n\n```bash\n# Login via browser OAuth\nnpx netlify login\n\n# Check authentication status and site link\nnpx netlify status\n\n# Logout\nnpx netlify logout\n```\n\n## Site Management\n\n```bash\n# Link current directory to existing site\nnpx netlify link\n\n# Link by Git remote URL\nnpx netlify link --git-remote-url <url>\n\n# Create and link new site\nnpx netlify init\n\n# Unlink from current site\nnpx netlify unlink\n\n# Open site in Netlify dashboard\nnpx netlify open\n\n# Open site admin panel\nnpx netlify open:admin\n\n# Open site in browser\nnpx netlify open:site\n```\n\n## Deployment\n\n```bash\n# Deploy preview/draft (safe for testing)\nnpx netlify deploy\n\n# Deploy to production\nnpx netlify deploy --prod\n\n# Deploy with specific directory\nnpx netlify deploy --dir=dist\n\n# Deploy with message\nnpx netlify deploy --message=\"Deploy message\"\n\n# List all deploys\nnpx netlify deploy:list\n```\n\n## Development\n\n```bash\n# Run local dev server with Netlify features\nnpx netlify dev\n\n# Run local dev server on specific port\nnpx netlify dev --port 3000\n```\n\n## Site Information\n\n```bash\n# Get site info\nnpx netlify sites:list\n\n# Get current site info\nnpx netlify api getSite --data '{\"site_id\": \"YOUR_SITE_ID\"}'\n```\n\n## Environment Variables\n\n```bash\n# List environment variables\nnpx netlify env:list\n\n# Set environment variable\nnpx netlify env:set KEY value\n\n# Get environment variable value\nnpx netlify env:get KEY\n\n# Import env vars from file\nnpx netlify env:import .env\n```\n\n## Build\n\n```bash\n# Show build settings\nnpx netlify build --dry\n\n# Run build locally\nnpx netlify build\n```\n\n## Functions (Serverless)\n\n```bash\n# List functions\nnpx netlify functions:list\n\n# Invoke function locally\nnpx netlify functions:invoke FUNCTION_NAME\n\n# Create new function\nnpx netlify functions:create FUNCTION_NAME\n```\n\n## Logs\n\n```bash\n# Stream function logs\nnpx netlify logs\n\n# View logs for specific function\nnpx netlify logs:function FUNCTION_NAME\n```\n\n## Troubleshooting Commands\n\n```bash\n# Check CLI version\nnpx netlify --version\n\n# Get help for any command\nnpx netlify help [command]\n\n# Check status with verbose output\nnpx netlify status --verbose\n```\n\n## Exit Codes\n\n- `0` - Success\n- `1` - General error\n- `2` - Authentication error\n- `3` - Site not found\n- `4` - Build failed\n\n## Common Flags\n\n- `--json` - Output as JSON\n- `--silent` - Suppress output\n- `--debug` - Show debug information\n- `--force` - Skip confirmation prompts\n\n## Resources\n\n- Full CLI documentation: https://docs.netlify.com/cli/get-started/\n- CLI GitHub repository: https://github.com/netlify/cli\n"
  },
  {
    "path": "skills/.curated/netlify-deploy/references/deployment-patterns.md",
    "content": "# Netlify Deployment Patterns\n\nCommon deployment scenarios and best practices for the Netlify skill.\n\n## Deployment Decision Tree\n\n```\nIs user authenticated?\n├─ No → Run `netlify login`\n└─ Yes → Is site linked?\n    ├─ No → Is it a Git repo?\n    │   ├─ Yes → Try `netlify link --git-remote-url`\n    │   │   ├─ Success → Continue to deploy\n    │   │   └─ Fail → Run `netlify init`\n    │   └─ No → Run `netlify init`\n    └─ Yes → Is this first deploy or existing site?\n        ├─ First deploy/new site → `netlify deploy --prod`\n        └─ Existing site → `netlify deploy` (preview)\n```\n\n## Scenario 1: First-Time Deployment (New Project)\n\n**Context**: User has a project that has never been deployed to Netlify.\n\n**Steps**:\n1. Check authentication: `npx netlify status`\n2. If not authenticated: `npx netlify login`\n3. Initialize new site: `npx netlify init`\n   - This guides user through setup\n   - Creates netlify.toml if needed\n4. Install dependencies: `npm install`\n5. Deploy to production: `npx netlify deploy --prod`\n\n**Example**:\n```bash\nnpx netlify status\n# Not linked to a site\n\nnpx netlify login\n# Opens browser for authentication\n\nnpx netlify init\n# Walks through site creation\n\nnpm install\nnpx netlify deploy --prod\n```\n\n## Scenario 2: Linking Existing Git Repository to Existing Site\n\n**Context**: User has a site already on Netlify and wants to link their local repo.\n\n**Steps**:\n1. Check authentication: `npx netlify status`\n2. Get Git remote: `git remote show origin`\n3. Extract URL (e.g., `https://github.com/user/repo.git`)\n4. Link by remote: `npx netlify link --git-remote-url <URL>`\n5. If found, linked. If not, run `netlify init`\n\n**Example**:\n```bash\ngit remote show origin\n# * remote origin\n#   Fetch URL: https://github.com/user/my-app.git\n\nnpx netlify link --git-remote-url https://github.com/user/my-app.git\n# Site linked successfully\n```\n\n## Scenario 3: Preview Deployment (Testing Changes)\n\n**Context**: User wants to test changes before pushing to production.\n\n**Steps**:\n1. Ensure site is linked: `npx netlify status`\n2. Make code changes\n3. Deploy preview: `npx netlify deploy`\n4. Review preview URL\n5. If approved, deploy to prod: `npx netlify deploy --prod`\n\n**Example**:\n```bash\n# Make changes to code\n\nnpx netlify deploy\n# Draft deploy URL: https://507f1f77bcf86cd799439011-my-app.netlify.app\n\n# Test the preview, then:\nnpx netlify deploy --prod\n```\n\n## Scenario 4: Framework-Specific Deployments\n\n### Next.js\n\n```bash\n# Next.js typically uses .next as output\nnpx netlify deploy --prod\n\n# netlify.toml should have:\n# [build]\n#   command = \"npm run build\"\n#   publish = \".next\"\n```\n\n### React (Vite)\n\n```bash\n# Vite outputs to dist by default\nnpm run build\nnpx netlify deploy --dir=dist --prod\n\n# netlify.toml:\n# [build]\n#   command = \"npm run build\"\n#   publish = \"dist\"\n```\n\n### Static HTML\n\n```bash\n# No build step needed\nnpx netlify deploy --dir=. --prod\n```\n\n## Scenario 5: Monorepo Deployment\n\n**Context**: Project is in a subdirectory of a monorepo.\n\n**Steps**:\n1. Navigate to project subdirectory: `cd packages/frontend`\n2. Or set base in netlify.toml:\n   ```toml\n   [build]\n     base = \"packages/frontend\"\n     command = \"npm run build\"\n     publish = \"dist\"\n   ```\n3. Deploy normally: `npx netlify deploy --prod`\n\n## Scenario 6: Environment Variables\n\n**Context**: Project needs secrets or environment-specific config.\n\n**Steps**:\n1. Never commit secrets to Git\n2. Set in Netlify dashboard or CLI:\n   ```bash\n   npx netlify env:set API_KEY \"secret_value\"\n   npx netlify env:set NODE_ENV \"production\"\n   ```\n3. Access in code: `process.env.API_KEY`\n4. Deploy: `npx netlify deploy --prod`\n\n## Scenario 7: Custom Domain Setup\n\n**Context**: User wants to use a custom domain.\n\n**Steps**:\n1. Deploy site first: `npx netlify deploy --prod`\n2. Add domain via dashboard or CLI:\n   ```bash\n   npx netlify open:admin\n   # Navigate to Domain settings\n   ```\n3. Update DNS records as instructed by Netlify\n4. Wait for DNS propagation (can take up to 48 hours)\n\n## Best Practices\n\n### 1. Always Preview First\n\n```bash\n# Deploy preview\nnpx netlify deploy\n\n# Test thoroughly\n# Then deploy to production\nnpx netlify deploy --prod\n```\n\n### 2. Use netlify.toml for Consistency\n\nCreate a `netlify.toml` file in your repo root:\n\n```toml\n[build]\n  command = \"npm run build\"\n  publish = \"dist\"\n\n[[redirects]]\n  from = \"/*\"\n  to = \"/index.html\"\n  status = 200\n```\n\nThis ensures consistent builds across all deployments.\n\n### 3. Framework Detection\n\nLet Netlify auto-detect when possible. Only specify build settings if:\n- Netlify can't detect your framework\n- You need custom build commands\n- Your project has a non-standard structure\n\n### 4. Dependency Installation\n\nAlways ensure dependencies are installed before deploying:\n\n```bash\nnpm install  # or yarn install, pnpm install\nnpx netlify deploy\n```\n\n### 5. Build Locally First\n\nTest builds locally before deploying:\n\n```bash\nnpm run build\n# Check that build output exists\n\nnpx netlify deploy --dir=dist\n```\n\n### 6. Use Deploy Messages\n\nAdd context to deployments:\n\n```bash\nnpx netlify deploy --prod --message=\"Fix login bug\"\n```\n\n## Error Recovery Patterns\n\n### \"Publish directory not found\"\n\n**Cause**: Build command didn't create expected output directory.\n\n**Fix**:\n1. Run build locally: `npm run build`\n2. Check output directory name\n3. Update netlify.toml or CLI prompts with correct path\n\n### \"Command failed with exit code 1\"\n\n**Cause**: Build command failed.\n\n**Fix**:\n1. Check build logs for specific error\n2. Run build locally to reproduce: `npm run build`\n3. Fix the build error\n4. Deploy again\n\n### \"Not logged in\"\n\n**Cause**: Authentication token expired or missing.\n\n**Fix**:\n```bash\nnpx netlify logout\nnpx netlify login\n```\n\n### \"No site linked\"\n\n**Cause**: Project not connected to a Netlify site.\n\n**Fix**:\n```bash\n# Try linking to existing site\nnpx netlify link\n\n# Or create new site\nnpx netlify init\n```\n\n## Performance Tips\n\n1. **Enable processing** in netlify.toml for auto-optimization:\n   ```toml\n   [build.processing.css]\n     bundle = true\n     minify = true\n   ```\n\n2. **Use caching headers** for static assets:\n   ```toml\n   [[headers]]\n     for = \"/assets/*\"\n     [headers.values]\n       Cache-Control = \"public, max-age=31536000, immutable\"\n   ```\n\n3. **Optimize images** before deploying or use Netlify Image CDN\n\n4. **Use Netlify Functions** for serverless backend (avoid external API calls when possible)\n\n## Resources\n\n- Netlify CLI Documentation: https://docs.netlify.com/cli/get-started/\n- Framework Integration Guides: https://docs.netlify.com/frameworks/\n- Build Configuration: https://docs.netlify.com/configure-builds/\n"
  },
  {
    "path": "skills/.curated/netlify-deploy/references/netlify-toml.md",
    "content": "# netlify.toml Configuration Reference\n\nConfiguration file for Netlify builds and deployments.\n\n## Basic Structure\n\n```toml\n[build]\n  command = \"npm run build\"\n  publish = \"dist\"\n```\n\n## Build Settings\n\n### Common Configuration\n\n```toml\n[build]\n  # Command to build your site\n  command = \"npm run build\"\n\n  # Directory to publish (relative to repo root)\n  publish = \"dist\"\n\n  # Functions directory\n  functions = \"netlify/functions\"\n\n  # Base directory (if not repo root)\n  base = \"packages/frontend\"\n\n  # Ignore builds for specific conditions\n  ignore = \"git diff --quiet HEAD^ HEAD package.json\"\n```\n\n## Environment Variables\n\n```toml\n[build.environment]\n  NODE_VERSION = \"18\"\n  NPM_FLAGS = \"--prefix=/dev/null\"\n\n[context.production.environment]\n  NODE_ENV = \"production\"\n```\n\n## Framework Detection\n\nNetlify auto-detects frameworks, but you can override:\n\n### Next.js\n\n```toml\n[build]\n  command = \"npm run build\"\n  publish = \".next\"\n```\n\n### React (Vite)\n\n```toml\n[build]\n  command = \"npm run build\"\n  publish = \"dist\"\n```\n\n### Vue\n\n```toml\n[build]\n  command = \"npm run build\"\n  publish = \"dist\"\n```\n\n### Astro\n\n```toml\n[build]\n  command = \"npm run build\"\n  publish = \"dist\"\n```\n\n### SvelteKit\n\n```toml\n[build]\n  command = \"npm run build\"\n  publish = \"build\"\n```\n\n## Redirects and Rewrites\n\n```toml\n[[redirects]]\n  from = \"/old-path\"\n  to = \"/new-path\"\n  status = 301\n\n[[redirects]]\n  from = \"/api/*\"\n  to = \"https://api.example.com/:splat\"\n  status = 200\n\n# SPA fallback (for client-side routing)\n[[redirects]]\n  from = \"/*\"\n  to = \"/index.html\"\n  status = 200\n```\n\n## Headers\n\n```toml\n[[headers]]\n  for = \"/*\"\n  [headers.values]\n    X-Frame-Options = \"DENY\"\n    X-XSS-Protection = \"1; mode=block\"\n    Content-Security-Policy = \"default-src 'self'\"\n\n[[headers]]\n  for = \"/assets/*\"\n  [headers.values]\n    Cache-Control = \"public, max-age=31536000, immutable\"\n```\n\n## Context-Specific Configuration\n\nDeploy different settings per context:\n\n```toml\n# Production\n[context.production]\n  command = \"npm run build:prod\"\n  [context.production.environment]\n    NODE_ENV = \"production\"\n\n# Deploy previews\n[context.deploy-preview]\n  command = \"npm run build:preview\"\n\n# Branch deploys\n[context.branch-deploy]\n  command = \"npm run build:staging\"\n\n# Specific branch\n[context.staging]\n  command = \"npm run build:staging\"\n```\n\n## Functions Configuration\n\n```toml\n[functions]\n  directory = \"netlify/functions\"\n  node_bundler = \"esbuild\"\n\n[[functions]]\n  path = \"/api/*\"\n  function = \"api\"\n```\n\n## Build Plugins\n\n```toml\n[[plugins]]\n  package = \"@netlify/plugin-lighthouse\"\n\n  [plugins.inputs]\n    output_path = \"reports/lighthouse.html\"\n\n[[plugins]]\n  package = \"netlify-plugin-submit-sitemap\"\n\n  [plugins.inputs]\n    baseUrl = \"https://example.com\"\n    sitemapPath = \"/sitemap.xml\"\n```\n\n## Edge Functions\n\n```toml\n[[edge_functions]]\n  function = \"geolocation\"\n  path = \"/api/location\"\n```\n\n## Processing\n\n```toml\n[build.processing]\n  skip_processing = false\n\n[build.processing.css]\n  bundle = true\n  minify = true\n\n[build.processing.js]\n  bundle = true\n  minify = true\n\n[build.processing.html]\n  pretty_urls = true\n\n[build.processing.images]\n  compress = true\n```\n\n## Common Patterns\n\n### Single Page Application (SPA)\n\n```toml\n[build]\n  command = \"npm run build\"\n  publish = \"dist\"\n\n[[redirects]]\n  from = \"/*\"\n  to = \"/index.html\"\n  status = 200\n```\n\n### Monorepo with Base Directory\n\n```toml\n[build]\n  base = \"packages/web\"\n  command = \"npm run build\"\n  publish = \"dist\"\n```\n\n### Multiple Redirects with Country-Based Routing\n\n```toml\n[[redirects]]\n  from = \"/\"\n  to = \"/uk\"\n  status = 302\n  conditions = {Country = [\"GB\"]}\n\n[[redirects]]\n  from = \"/\"\n  to = \"/us\"\n  status = 302\n  conditions = {Country = [\"US\"]}\n```\n\n## Validation\n\nValidate your netlify.toml:\n\n```bash\nnpx netlify build --dry\n```\n\n## Resources\n\n- Full configuration reference: https://docs.netlify.com/configure-builds/file-based-configuration/\n- Framework-specific guides: https://docs.netlify.com/frameworks/\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/LICENSE.txt",
    "content": "Copyright 2025 Notion Labs, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/SKILL.md",
    "content": "---\nname: notion-knowledge-capture\ndescription: Capture conversations and decisions into structured Notion pages; use when turning chats/notes into wiki entries, how-tos, decisions, or FAQs with proper linking.\nmetadata:\n  short-description: Capture conversations into structured Notion pages\n---\n\n# Knowledge Capture\n\nConvert conversations and notes into structured, linkable Notion pages for easy reuse.\n\n## Quick start\n1) Clarify what to capture (decision, how-to, FAQ, learning, documentation) and target audience.\n2) Identify the right database/template in `reference/` (team wiki, how-to, FAQ, decision log, learning, documentation).\n3) Pull any prior context from Notion with `Notion:notion-search` → `Notion:notion-fetch` (existing pages to update/link).\n4) Draft the page with `Notion:notion-create-pages` using the database’s schema; include summary, context, source links, and tags/owners.\n5) Link from hub pages and related records; update status/owners with `Notion:notion-update-page` as the source evolves.\n\n## Workflow\n### 0) If any MCP call fails because Notion MCP is not connected, pause and set it up:\n1. Add the Notion MCP:\n   - `codex mcp add notion --url https://mcp.notion.com/mcp`\n2. Enable remote MCP client:\n   - Set `[features].rmcp_client = true` in `config.toml` **or** run `codex --enable rmcp_client`\n3. Log in with OAuth:\n   - `codex mcp login notion`\n\nAfter successful login, the user will have to restart codex. You should finish your answer and tell them so when they try again they can continue with Step 1.\n\n### 1) Define the capture\n- Ask purpose, audience, freshness, and whether this is new or an update.\n- Determine content type: decision, how-to, FAQ, concept/wiki entry, learning/note, documentation page.\n\n### 2) Locate destination\n- Pick the correct database using `reference/*-database.md` guides; confirm required properties (title, tags, owner, status, date, relations).\n- If multiple candidate databases, ask the user which to use; otherwise, create in the primary wiki/documentation DB.\n\n### 3) Extract and structure\n- Extract facts, decisions, actions, and rationale from the conversation.\n- For decisions, record alternatives, rationale, and outcomes.\n- For how-tos/docs, capture steps, pre-reqs, links to assets/code, and edge cases.\n- For FAQs, phrase as Q&A with concise answers and links to deeper docs.\n\n### 4) Create/update in Notion\n- Use `Notion:notion-create-pages` with the correct `data_source_id`; set properties (title, tags, owner, status, dates, relations).\n- Use templates in `reference/` to structure content (section headers, checklists).\n- If updating an existing page, fetch then edit via `Notion:notion-update-page`.\n\n### 5) Link and surface\n- Add relations/backlinks to hub pages, related specs/docs, and teams.\n- Add a short summary/changelog for future readers.\n- If follow-up tasks exist, create tasks in the relevant database and link them.\n\n## References and examples\n- `reference/` — database schemas and templates (e.g., `team-wiki-database.md`, `how-to-guide-database.md`, `faq-database.md`, `decision-log-database.md`, `documentation-database.md`, `learning-database.md`, `database-best-practices.md`).\n- `examples/` — capture patterns in practice (e.g., `decision-capture.md`, `how-to-guide.md`, `conversation-to-faq.md`).\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Notion Knowledge Capture\"\n  short_description: \"Capture conversations into structured Notion pages\"\n  icon_small: \"./assets/notion-small.svg\"\n  icon_large: \"./assets/notion.png\"\n  default_prompt: \"Capture this conversation into structured Notion pages with decisions, action items, and owners when known.\"\n\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"notion\"\n      description: \"Notion MCP server\"\n      transport: \"streamable_http\"\n      url: \"https://mcp.notion.com/mcp\"\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/evaluations/README.md",
    "content": "# Knowledge Capture Skill Evaluations\n\nEvaluation scenarios for testing the Knowledge Capture skill across different Codex models.\n\n## Purpose\n\nThese evaluations ensure the Knowledge Capture skill:\n- Correctly identifies content types (how-to guides, FAQs, decision records, wikis)\n- Extracts relevant information from conversations\n- Structures content appropriately for each type\n- Searches and places content in the right Notion location\n- Works consistently across Haiku, Sonnet, and Opus\n\n## Evaluation Files\n\n### conversation-to-wiki.json\nTests capturing conversation content as a how-to guide for the team wiki.\n\n**Scenario**: Save deployment discussion to wiki  \n**Key Behaviors**:\n- Extracts steps, gotchas, and best practices from conversation\n- Identifies content as How-To Guide\n- Structures with proper sections (Overview, Prerequisites, Steps, Troubleshooting)\n- Searches for team wiki location\n- Preserves technical details (commands, configs)\n\n### decision-record.json\nTests capturing architectural or technical decisions with full context.\n\n**Scenario**: Document database migration decision  \n**Key Behaviors**:\n- Extracts decision context, alternatives, and rationale\n- Follows decision record structure (Context, Decision, Alternatives, Consequences)\n- Captures both selected and rejected options with reasoning\n- Places in decision log or ADR database\n- Links to related technical documentation\n\n## Running Evaluations\n\n1. Enable the `knowledge-capture` skill\n2. Submit the query from the evaluation file\n3. Provide conversation context as specified\n4. Verify all expected behaviors are met\n5. Check success criteria for quality\n6. Test with Haiku, Sonnet, and Opus\n\n## Expected Skill Behaviors\n\nKnowledge Capture evaluations should verify:\n\n### Content Extraction\n- Accurately captures key points from conversation context\n- Preserves specific technical details, not generic placeholders\n- Maintains context and nuance from discussion\n\n### Content Type Selection\n- Correctly identifies appropriate content type (how-to, FAQ, decision record, wiki page)\n- Uses matching structure from reference documentation\n- Applies proper Notion markdown formatting\n\n### Notion Integration\n- Searches for appropriate target location (wiki, decision log, etc.)\n- Creates well-structured pages with clear titles\n- Uses proper parent placement\n- Includes discoverable titles and metadata\n\n### Quality Standards\n- Content is actionable and future-reference ready\n- Technical accuracy is preserved\n- Organization aids discoverability\n- Formatting enhances readability\n\n## Creating New Evaluations\n\nWhen adding Knowledge Capture evaluations:\n\n1. **Use realistic conversation content** - Include actual technical details, decisions, or processes\n2. **Test different content types** - How-to guides, FAQs, decision records, meeting notes, learnings\n3. **Vary complexity** - Simple captures vs. complex technical discussions\n4. **Test discovery** - Finding the right wiki section or database\n5. **Include edge cases** - Unclear content types, minimal context, overlapping categories\n\n## Example Success Criteria\n\n**Good** (specific, testable):\n- \"Structures content using How-To format with numbered steps\"\n- \"Preserves exact bash commands from conversation\"\n- \"Creates page with title format 'How to [Action]'\"\n- \"Places in Engineering Wiki → Deployment section\"\n\n**Bad** (vague, untestable):\n- \"Creates good documentation\"\n- \"Uses appropriate structure\"\n- \"Saves to the right place\"\n\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/evaluations/conversation-to-wiki.json",
    "content": "{\n  \"name\": \"Save Conversation to Wiki\",\n  \"skills\": [\"knowledge-capture\"],\n  \"query\": \"Save this conversation about deploying our application to production to the team wiki\",\n  \"context\": \"Preceding conversation contains discussion about deployment process, including steps, gotchas, and best practices\",\n  \"expected_behavior\": [\n    \"Extracts key information from conversation context (deployment steps, gotchas, best practices)\",\n    \"Identifies content type as How-To Guide based on procedural nature\",\n    \"Structures content using How-To structure: Overview → Prerequisites → Steps (numbered) → Verification → Troubleshooting → Related\",\n    \"Organizes information into clear sections with proper headings\",\n    \"Includes specific commands, configurations, or examples from conversation\",\n    \"Adds context about why/when to use this process in Overview section\",\n    \"Notes common issues and solutions mentioned in discussion in Troubleshooting section\",\n    \"Uses Notion:notion-search to find team wiki location or asks user\",\n    \"Creates page using Notion:notion-create-pages with structured content and appropriate parent\",\n    \"Uses clear, descriptive title like 'How to Deploy to Production'\",\n    \"Applies Notion markdown formatting (headings, code blocks, bullets)\",\n    \"Suggests tags/categories for discoverability if wiki database\"\n  ],\n  \"success_criteria\": [\n    \"Content is structured using How-To format from SKILL.md content types\",\n    \"Key points from conversation are captured accurately (not generic)\",\n    \"Information is organized with proper Notion markdown (##, ###, bullets, code blocks)\",\n    \"Specific technical details (commands, configs) are preserved from conversation\",\n    \"Document is written for future reference with clear step-by-step instructions\",\n    \"Title is searchable and descriptive (e.g., 'How to Deploy to Production')\",\n    \"Page is placed in appropriate wiki location (general wiki or specific section)\",\n    \"Uses correct tool name (Notion:notion-create-pages)\"\n  ]\n}\n\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/evaluations/decision-record.json",
    "content": "{\n  \"name\": \"Create Decision Record\",\n  \"skills\": [\"knowledge-capture\"],\n  \"query\": \"Document our decision to use PostgreSQL instead of MongoDB for our new service\",\n  \"context\": \"User has just explained the decision with rationale, options considered, and trade-offs\",\n  \"expected_behavior\": [\n    \"Recognizes this as a decision record (architectural decision) from conversation context\",\n    \"Uses Decision structure: Context → Decision → Rationale → Options Considered (with Pros/Cons) → Consequences → Implementation\",\n    \"Extracts from context: decision made, options considered (PostgreSQL vs MongoDB), rationale, trade-offs\",\n    \"Creates document with proper structure including Date, Status (Accepted), and Deciders\",\n    \"Includes both positive and negative consequences (trade-offs) in Consequences section\",\n    \"Uses Notion:notion-search to check if decision log database exists\",\n    \"If database exists, asks whether to add there or create standalone page\",\n    \"If creating in database, fetches schema using Notion:notion-fetch and sets properties: Decision title, Date, Status, Domain (Architecture), Deciders, Impact\",\n    \"Uses Notion:notion-create-pages with parent: { data_source_id } for database or { page_id } for parent page\",\n    \"Applies proper Notion markdown formatting with sections\",\n    \"Suggests linking from architecture docs or project pages\"\n  ],\n  \"success_criteria\": [\n    \"Document follows Decision structure from SKILL.md content types\",\n    \"All key sections present: Context, Decision, Rationale, Options Considered (with Pros/Cons for each), Consequences, Implementation\",\n    \"Decision is clearly stated (PostgreSQL chosen over MongoDB)\",\n    \"Options that were considered are documented with pros/cons structure\",\n    \"Rationale explains why PostgreSQL was chosen based on conversation context\",\n    \"Consequences include both positive (benefits) and negative (trade-offs)\",\n    \"If in database, properties are set correctly from schema (Decision, Date, Status: Accepted, Domain: Architecture, Impact)\",\n    \"Document is dated and has status 'Accepted'\",\n    \"Uses correct tool names (Notion:notion-search, Notion:notion-fetch, Notion:notion-create-pages)\"\n  ]\n}\n\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/examples/conversation-to-faq.md",
    "content": "# Example: Conversation to FAQ\n\n## User Request\n\n> \"Save this conversation about deployment troubleshooting to the FAQ\"\n\n**Context**: User just had a conversation explaining how to troubleshoot common deployment errors.\n\n## Conversation Summary\n\nThe conversation covered:\n1. **Question**: \"Why does deployment fail with 'port already in use' error?\"\n2. **Answer**: Process from previous deployment still running, need to kill it\n3. **Question**: \"How do I find and kill the process?\"\n4. **Commands shared**: `lsof -ti:3000 | xargs kill -9` or use `pm2 restart app`\n5. **Question**: \"What about 'cannot connect to database' errors?\"\n6. **Answer**: Check database credentials, verify database is running, check network connectivity\n7. **Best practice**: Always check logs first (`pm2 logs` or `docker logs`)\n\n## Workflow Execution\n\n### Step 1: Identify Content Type\n\nThis is Q&A content → **FAQ Entry** format is appropriate\n\n### Step 2: Structure FAQ Entries\n\nExtract distinct Q&A pairs from conversation:\n\n**FAQ 1**: Port already in use error\n**FAQ 2**: Cannot connect to database error\n**FAQ 3**: General troubleshooting approach\n\n### Step 3: Determine Destination\n\n```\nNotion:notion-search\nquery: \"FAQ deployment\"\nquery_type: \"internal\"\n```\n\nFound: \"Deployment FAQ\" database in Engineering Docs\n\n```\nNotion:notion-fetch\nid: \"deployment-faq-database-id\"\n```\n\n**Schema**:\n- Question (title)\n- Category (select): Deployment, Configuration, Troubleshooting, etc.\n- Tags (multi_select)\n- Last Reviewed (date)\n\n### Step 4: Create FAQ Entries\n\n**FAQ Entry 1: Port Already in Use**\n\n```\nNotion:notion-create-pages\n\nparent: { data_source_id: \"collection://faq-db-uuid\" }\n\npages: [{\n  properties: {\n    \"Question\": \"Why does deployment fail with 'port already in use' error?\",\n    \"Category\": \"Troubleshooting\",\n    \"Tags\": \"deployment, errors, ports\",\n    \"date:Last Reviewed:start\": \"2025-10-14\",\n    \"date:Last Reviewed:is_datetime\": 0\n  },\n  content: \"## Short Answer\\n\\nThe deployment port (usually 3000) is still occupied by a process from a previous deployment. You need to kill the existing process before deploying again.\\n\\n## Detailed Explanation\\n\\nWhen you deploy the application, it tries to bind to a specific port (e.g., port 3000). If a previous deployment didn't shut down cleanly, that process may still be running and holding the port. This prevents the new deployment from starting.\\n\\n**Common causes**:\\n- Previous deployment crashed without cleanup\\n- Manual `node` process started and forgotten\\n- PM2 or other process manager didn't restart properly\\n- Multiple deployments attempted simultaneously\\n\\n## Solution\\n\\n### Option 1: Kill the process using the port\\n\\n```bash\\n# Find and kill process on port 3000\\nlsof -ti:3000 | xargs kill -9\\n```\\n\\nReplace `3000` with your application's port number.\\n\\n### Option 2: If using PM2\\n\\n```bash\\n# Restart the application\\npm2 restart app\\n\\n# Or delete and restart\\npm2 delete app\\npm2 start app.js --name app\\n```\\n\\n### Option 3: Check all node processes\\n\\n```bash\\n# List all node processes\\nps aux | grep node\\n\\n# Kill specific process by PID\\nkill -9 <PID>\\n```\\n\\n## Prevention\\n\\n1. **Use process managers**: PM2, systemd, or Docker handle cleanup automatically\\n2. **Graceful shutdown**: Implement proper shutdown handlers in your app\\n3. **Health checks**: Monitor if previous deployment shut down before starting new one\\n\\n```javascript\\n// Graceful shutdown example\\nprocess.on('SIGTERM', () => {\\n  server.close(() => {\\n    console.log('Server closed');\\n    process.exit(0);\\n  });\\n});\\n```\\n\\n## Verification\\n\\nAfter killing the process, verify the port is free:\\n\\n```bash\\nlsof -ti:3000\\n# Should return nothing if port is free\\n```\\n\\n## Related Questions\\n- <mention-page url=\\\"...\\\">How do I check what's using a port?</mention-page>\\n- <mention-page url=\\\"...\\\">How do I configure the application port?</mention-page>\\n- <mention-page url=\\\"...\\\">PM2 deployment best practices</mention-page>\\n\\n## Last Updated\\nOctober 14, 2025\"\n}]\n```\n\n**FAQ Entry 2: Database Connection Errors**\n\n```\nNotion:notion-create-pages\n\nparent: { data_source_id: \"collection://faq-db-uuid\" }\n\npages: [{\n  properties: {\n    \"Question\": \"Why do I get 'cannot connect to database' errors during deployment?\",\n    \"Category\": \"Troubleshooting\",\n    \"Tags\": \"deployment, database, errors\",\n    \"date:Last Reviewed:start\": \"2025-10-14\",\n    \"date:Last Reviewed:is_datetime\": 0\n  },\n  content: \"## Short Answer\\n\\nDatabase connection errors usually mean either the database isn't running, credentials are incorrect, or there's a network connectivity issue. Check database status, verify credentials, and test connectivity.\\n\\n## Detailed Explanation\\n\\nThe application can't establish a connection to the database during startup. This prevents the application from initializing properly.\\n\\n**Common causes**:\\n- Database service isn't running\\n- Incorrect connection credentials\\n- Network connectivity issues (firewall, security groups)\\n- Database host/port misconfigured\\n- Database is at connection limit\\n- SSL/TLS configuration mismatch\\n\\n## Troubleshooting Steps\\n\\n### Step 1: Check database status\\n\\n```bash\\n# For local PostgreSQL\\npg_isready -h localhost -p 5432\\n\\n# For Docker\\ndocker ps | grep postgres\\n\\n# For MongoDB\\nmongosh --eval \\\"db.adminCommand('ping')\\\"\\n```\\n\\n### Step 2: Verify credentials\\n\\nCheck your `.env` or configuration file:\\n\\n```bash\\n# Common environment variables\\nDB_HOST=localhost\\nDB_PORT=5432\\nDB_NAME=myapp_production\\nDB_USER=myapp_user\\nDB_PASSWORD=***********\\n```\\n\\nTest connection manually:\\n\\n```bash\\n# PostgreSQL\\npsql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME\\n\\n# MongoDB\\nmongosh \\\"mongodb://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME\\\"\\n```\\n\\n### Step 3: Check network connectivity\\n\\n```bash\\n# Test if port is reachable\\ntelnet $DB_HOST $DB_PORT\\n\\n# Or using nc\\nnc -zv $DB_HOST $DB_PORT\\n\\n# Check firewall rules (if applicable)\\nsudo iptables -L\\n```\\n\\n### Step 4: Check application logs\\n\\n```bash\\n# PM2 logs\\npm2 logs app\\n\\n# Docker logs\\ndocker logs container-name\\n\\n# Application logs\\ntail -f /var/log/app/error.log\\n```\\n\\nLook for specific error messages:\\n- `ECONNREFUSED`: Database not running or wrong host/port\\n- `Authentication failed`: Wrong credentials\\n- `Timeout`: Network/firewall issue\\n- `Too many connections`: Database connection limit reached\\n\\n## Solutions by Error Type\\n\\n### Database Not Running\\n\\n```bash\\n# Start PostgreSQL\\nsudo systemctl start postgresql\\n\\n# Start via Docker\\ndocker start postgres-container\\n```\\n\\n### Wrong Credentials\\n\\n1. Reset database password\\n2. Update `.env` file\\n3. Restart application\\n\\n### Connection Limit Reached\\n\\n```sql\\n-- Check current connections (PostgreSQL)\\nSELECT count(*) FROM pg_stat_activity;\\n\\n-- Increase max connections\\nALTER SYSTEM SET max_connections = 200;\\n```\\n\\n### SSL/TLS Issues\\n\\nAdd to connection string:\\n```\\nssl=true&sslmode=require\\n```\\n\\nOr disable SSL for dev:\\n```\\nsslmode=disable\\n```\\n\\n## Prevention\\n\\n1. **Connection pooling**: Limits concurrent connections\\n```javascript\\npool: {\\n  min: 2,\\n  max: 10\\n}\\n```\\n\\n2. **Health checks**: Verify database before starting app\\n```javascript\\nconst healthCheck = async () => {\\n  try {\\n    await db.raw('SELECT 1');\\n    console.log('Database connected');\\n  } catch (err) {\\n    console.error('Database connection failed:', err);\\n    process.exit(1);\\n  }\\n};\\n```\\n\\n3. **Retry logic**: Auto-reconnect on temporary failures\\n```javascript\\nretry: {\\n  max: 3,\\n  backoff: 1000\\n}\\n```\\n\\n4. **Environment validation**: Check all required env vars exist\\n```javascript\\nconst required = ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD'];\\nfor (const key of required) {\\n  if (!process.env[key]) {\\n    throw new Error(`Missing required env var: ${key}`);\\n  }\\n}\\n```\\n\\n## Related Questions\\n- <mention-page url=\\\"...\\\">How do I configure database connection pooling?</mention-page>\\n- <mention-page url=\\\"...\\\">Database backup and restore procedures</mention-page>\\n- <mention-page url=\\\"...\\\">Database performance optimization</mention-page>\\n\\n## Last Updated\\nOctober 14, 2025\"\n}]\n```\n\n**FAQ Entry 3: General Troubleshooting**\n\n```\nNotion:notion-create-pages\n\nparent: { data_source_id: \"collection://faq-db-uuid\" }\n\npages: [{\n  properties: {\n    \"Question\": \"What's the first thing I should check when deployment fails?\",\n    \"Category\": \"Troubleshooting\",\n    \"Tags\": \"deployment, debugging, best-practices\",\n    \"date:Last Reviewed:start\": \"2025-10-14\",\n    \"date:Last Reviewed:is_datetime\": 0\n  },\n  content: \"## Short Answer\\n\\n**Always check the logs first.** Logs contain error messages that point you directly to the problem. Use `pm2 logs`, `docker logs`, or check your application's log files.\\n\\n## Detailed Explanation\\n\\nLogs are your first and most important debugging tool. They show:\\n- Exact error messages\\n- Stack traces\\n- Timing information\\n- Configuration issues\\n- Dependency problems\\n\\nMost deployment issues can be diagnosed and fixed by reading the logs carefully.\\n\\n## How to Check Logs\\n\\n### PM2\\n\\n```bash\\n# View all logs\\npm2 logs\\n\\n# View logs for specific app\\npm2 logs app-name\\n\\n# View only errors\\npm2 logs --err\\n\\n# Follow logs in real-time\\npm2 logs --lines 100\\n```\\n\\n### Docker\\n\\n```bash\\n# View logs\\ndocker logs container-name\\n\\n# Follow logs\\ndocker logs -f container-name\\n\\n# Last 100 lines\\ndocker logs --tail 100 container-name\\n\\n# With timestamps\\ndocker logs -t container-name\\n```\\n\\n### Application Logs\\n\\n```bash\\n# Tail application logs\\ntail -f /var/log/app/app.log\\ntail -f /var/log/app/error.log\\n\\n# Search logs for errors\\ngrep -i error /var/log/app/*.log\\n\\n# View logs with context\\ngrep -B 5 -A 5 \\\"ERROR\\\" app.log\\n```\\n\\n## Systematic Troubleshooting Approach\\n\\n### 1. Check the logs\\n- Read error messages carefully\\n- Note the exact error type and message\\n- Check timestamps to find when error occurred\\n\\n### 2. Verify configuration\\n- Environment variables set correctly?\\n- Configuration files present and valid?\\n- Paths and file permissions correct?\\n\\n### 3. Check dependencies\\n- All packages installed? (`node_modules` present?)\\n- Correct versions installed?\\n- Any native module compilation errors?\\n\\n### 4. Verify environment\\n- Required services running (database, Redis, etc.)?\\n- Ports available?\\n- Network connectivity working?\\n\\n### 5. Test components individually\\n- Can you connect to database manually?\\n- Can you run application locally?\\n- Do health check endpoints work?\\n\\n### 6. Check recent changes\\n- What changed since last successful deployment?\\n- New dependencies added?\\n- Configuration modified?\\n- Environment differences?\\n\\n## Common Error Patterns\\n\\n### \\\"Module not found\\\"\\n```bash\\n# Solution: Install dependencies\\nnpm install\\n# or\\nnpm ci\\n```\\n\\n### \\\"Permission denied\\\"\\n```bash\\n# Solution: Fix file permissions\\nchmod +x start.sh\\nsudo chown -R appuser:appuser /app\\n```\\n\\n### \\\"Address already in use\\\"\\n```bash\\n# Solution: Kill process on port\\nlsof -ti:3000 | xargs kill -9\\n```\\n\\n### \\\"Cannot connect to...\\\"\\n```bash\\n# Solution: Verify service is running and reachable\\ntelnet service-host port\\n```\\n\\n## Debugging Tools\\n\\n### Log Aggregation\\n- **PM2**: Built-in log management\\n- **Docker**: Centralized logging with log drivers\\n- **ELK Stack**: Elasticsearch, Logstash, Kibana for large scale\\n- **CloudWatch**: For AWS deployments\\n\\n### Monitoring\\n- **PM2 Monit**: `pm2 monit` for real-time metrics\\n- **Docker Stats**: `docker stats` for resource usage\\n- **System metrics**: `top`, `htop` for CPU/memory\\n\\n### Network Debugging\\n```bash\\n# Check open connections\\nnetstat -tulpn\\n\\n# Check specific port\\nlsof -i :3000\\n\\n# Test connectivity\\ncurl http://localhost:3000/health\\n```\\n\\n## When to Escalate\\n\\nEscalate to senior team member if:\\n- Error message is cryptic and not Google-able\\n- Problem persists after trying obvious solutions\\n- Production is affected\\n- Data integrity may be compromised\\n- Security-related errors\\n\\n## Prevention\\n\\n1. **Automated health checks** before deployment\\n2. **Staged deployments** (dev → staging → production)\\n3. **Rollback plan** ready for quick recovery\\n4. **Monitoring and alerts** for early problem detection\\n5. **Deployment checklist** to ensure all steps followed\\n\\n## Related Questions\\n- <mention-page url=\\\"...\\\">How do I set up log aggregation?</mention-page>\\n- <mention-page url=\\\"...\\\">Deployment checklist and best practices</mention-page>\\n- <mention-page url=\\\"...\\\">How to roll back a failed deployment?</mention-page>\\n\\n## Last Updated\\nOctober 14, 2025\"\n}]\n```\n\n### Step 5: Update FAQ Index\n\n```\nNotion:notion-fetch\nid: \"faq-index-page-id\"\n```\n\n```\nNotion:notion-update-page\npage_id: \"faq-index-page-id\"\ncommand: \"insert_content_after\"\nselection_with_ellipsis: \"## Deployment & Troubleshooting...\"\nnew_str: \"\n- <mention-page url=\\\"...\\\">Why does deployment fail with 'port already in use' error?</mention-page>\n- <mention-page url=\\\"...\\\">Why do I get 'cannot connect to database' errors?</mention-page>\n- <mention-page url=\\\"...\\\">What's the first thing I should check when deployment fails?</mention-page>\n\"\n```\n\n## Summary Provided to User\n\n```markdown\n## FAQ Entries Created ✅\n\n### Conversation Captured\nTransformed deployment troubleshooting conversation into 3 comprehensive FAQ entries.\n\n### FAQ Entries Created\n\n1. **Port Already in Use Error**\n   - Problem: Deployment fails due to occupied port\n   - Solutions: Kill process, use PM2, check node processes\n   - Prevention: Use process managers, graceful shutdown\n   - Link: <mention-page url=\"...\">Port already in use FAQ</mention-page>\n\n2. **Database Connection Errors**\n   - Problem: Cannot connect to database during deployment\n   - Troubleshooting: 4-step systematic approach\n   - Solutions: By error type (credentials, network, limits)\n   - Prevention: Connection pooling, health checks, retry logic\n   - Link: <mention-page url=\"...\">Database connection FAQ</mention-page>\n\n3. **General Troubleshooting Approach**\n   - Best practice: Always check logs first\n   - Tools: PM2 logs, Docker logs, application logs\n   - Systematic approach: 6-step debugging process\n   - Common errors: Module not found, permissions, ports\n   - Link: <mention-page url=\"...\">Troubleshooting first steps FAQ</mention-page>\n\n### Content Structure\n\nEach FAQ includes:\n✅ Short answer (quick reference)\n✅ Detailed explanation (full context)\n✅ Multiple solution options\n✅ Code examples and commands\n✅ Prevention strategies\n✅ Links to related FAQs\n✅ Last updated date\n\n### Database Integration\n\n- ✅ Added to FAQ database (not standalone pages)\n- ✅ Properties set: Category (Troubleshooting), Tags, Date\n- ✅ FAQ index page updated with links\n- ✅ Cross-references between related questions\n\n### Value Added\n\n**From conversation** → **To structured knowledge**:\n- Casual Q&A → Professional FAQ entries\n- Implicit knowledge → Explicit documentation\n- One-time help → Reusable resource\n- 5-minute conversation → Permanent team knowledge\n\n**Future benefit**:\n- Team can self-serve on common issues\n- New team members can onboard faster\n- Reduces repeated questions\n- Improves troubleshooting speed\n\n**Knowledge captured and preserved! 📚**\n```\n\n## Key Features Demonstrated\n\n### Content Extraction\n- Identified distinct Q&A pairs from conversation\n- Extracted commands, solutions, and best practices\n- Preserved troubleshooting workflows\n\n### FAQ Structure\n- Short answer for quick reference\n- Detailed explanation for understanding\n- Multiple solutions (Options 1, 2, 3)\n- Code examples with comments\n- Prevention strategies\n- Related questions linking\n\n### Database Integration\n- Created in FAQ database (not standalone)\n- Set appropriate properties (category, tags, date)\n- Updated index page with links\n- Cross-referenced related questions\n\n### Value Transformation\n- Converted informal conversation to professional docs\n- Added structure, examples, and prevention tips\n- Made knowledge searchable and discoverable\n- Created permanent team resource\n\nPerfect for: FAQ creation, knowledge preservation, team documentation\n\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/examples/decision-capture.md",
    "content": "# Example: Decision Record Capture\n\n**User Request**: \"Document our decision to move from REST to GraphQL API\"\n\n## Workflow\n\n### 1. Extract Decision from Context\nFrom conversation, identified:\n- **Decision**: Migrate customer-facing API from REST to GraphQL\n- **Context**: REST endpoints becoming unwieldy (50+ endpoints)\n- **Alternatives**: Keep REST, try gRPC, adopt GraphQL\n- **Rationale**: Better client experience, type safety, single endpoint\n\n### 2. Find Decision Log\n```\nNotion:notion-search\nquery: \"architecture decisions\" or \"ADR\"\n```\n\nFound: \"Architecture Decision Records\" database\n\n### 3. Fetch Schema\n```\nNotion:notion-fetch\n```\n\nGot properties: Decision (title), Date, Status, Domain, Impact, Deciders, Stakeholders\n\n### 4. Create Decision Record\n```\nNotion:notion-create-pages\nparent: { data_source_id: \"decision-log-collection-id\" }\npages: [{\n  properties: {\n    \"Decision\": \"Migrate to GraphQL API\",\n    \"date:Date:start\": \"2025-10-16\",\n    \"date:Date:is_datetime\": 0,\n    \"Status\": \"Accepted\",\n    \"Domain\": \"Architecture\",\n    \"Impact\": \"High\"\n  },\n  content: \"[Full decision record with context, rationale, alternatives...]\"\n}]\n```\n\n**Content sample**:\n\n```markdown\n# Migrate Customer-Facing API to GraphQL\n\n## Context\nOur REST API has grown to 50+ endpoints with inconsistent patterns. Frontend and mobile teams request new endpoints frequently, leading to:\n- API bloat and maintenance burden\n- Over-fetching/under-fetching data\n- Slow iteration on client features\n- Inconsistent error handling\n\n## Decision\nMigrate customer-facing API from REST to GraphQL while maintaining REST for internal services.\n\n## Rationale\n**Why GraphQL**:\n- Clients fetch exactly what they need (no over/under-fetching)\n- Single endpoint, self-documenting schema\n- Type safety with code generation\n- Better developer experience\n- Industry standard for client-facing APIs\n\n## Options Considered\n\n### Option A: Keep REST (Status Quo)\n**Pros**: No migration cost, team familiar\n**Cons**: Doesn't solve core problems, maintenance keeps growing\n**Decision**: Rejected - doesn't address root cause\n\n### Option B: gRPC\n**Pros**: Performance, strong typing\n**Cons**: Browser support issues, steeper learning curve, not designed for client-facing\n**Decision**: Rejected - better for internal services\n\n### Option C: GraphQL\n**Pros**: Solves over/under-fetching, great DX, strong ecosystem\n**Cons**: Learning curve, caching complexity, migration effort\n**Decision**: **Accepted**\n\n## Consequences\n\n**Positive**:\n- Faster frontend/mobile development\n- Reduced API maintenance burden\n- Better type safety and tooling\n- Single endpoint simplifies deployment\n\n**Negative**:\n- 3-4 month migration timeline\n- Team needs training\n- Need to solve caching strategy\n- Monitoring/debugging patterns different\n\n## Implementation Plan\n1. GraphQL server setup (Apollo Server)\n2. Schema design workshop\n3. Gradual migration (start with new features)\n4. Dual-run period (both REST and GraphQL)\n5. Deprecate old REST endpoints\n\n**Timeline**: Start Q4 2025, complete Q1 2026\n**Owner**: Backend team lead\n\n## Success Metrics\n- API response times improve 30%\n- Client fetch efficiency (less data transferred)\n- Reduced new endpoint requests\n- Developer satisfaction scores\n```\n\n### 5. Make Discoverable\nAdded link from Architecture Wiki and notified team in Slack.\n\n## Key Success Factors\n- Captured decision while context fresh\n- Documented alternatives considered\n- Included both pros and cons\n- Clear implementation plan\n- Saved to decision log for future reference\n- Made discoverable for team\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/examples/how-to-guide.md",
    "content": "# Example: How-To Guide from Discussion\n\n**User Request**: \"Save our discussion about deploying to production as a how-to guide\"\n\n## Workflow\n\n### 1. Extract Content from Chat\nFrom conversation, identified:\n- Deployment prerequisites\n- Step-by-step procedure\n- Common issues and solutions\n- Best practices and tips\n\n### 2. Structure as How-To\nOrganized into:\n- Overview & prerequisites\n- Numbered deployment steps\n- Verification steps\n- Troubleshooting section\n- Related resources\n\n### 3. Find Location\n```\nNotion:notion-search\nquery: \"deployment documentation\"\n```\nFound: Engineering Wiki → Deployment section\n\n### 4. Create Guide\n```\nNotion:notion-create-pages\nparent: { page_id: \"deployment-section-id\" }\n```\n\n## Output\n\n```markdown\n# How to Deploy to Production\n\n## Overview\nProduction deployment using GitHub Actions with zero-downtime rolling updates.\n**Time Required**: 15-20 minutes | **Complexity**: Intermediate\n\n## Prerequisites\n- [ ] PR approved and merged to main\n- [ ] All CI tests passing\n- [ ] Database migrations reviewed\n- [ ] Feature flags configured\n\n## Deployment Steps\n\n1. **Verify main branch is ready**\n   ```bash\n   git checkout main && git pull\n   ```\n\n2. **Tag release**\n   ```bash\n   git tag -a v1.2.3 -m \"Release v1.2.3\"\n   git push origin v1.2.3\n   ```\n\n3. **Trigger deployment**\n   - GitHub Actions auto-starts from tag push\n   - Monitor: https://github.com/org/repo/actions\n\n4. **Database migrations** (if needed)\n   - Auto-run in GitHub Actions\n   - Check logs for completion\n\n5. **Verify deployment**\n   - Wait for health checks (2-3 min)\n   - Test key endpoints\n   - Check error rates in Datadog\n\n## Verification Checklist\n- [ ] All pods healthy in k8s dashboard\n- [ ] Error rate < 0.1% in last 10 min\n- [ ] Response time p95 < 500ms\n- [ ] Test login flow\n- [ ] Check Slack #alerts channel\n\n## Troubleshooting\n\n**Health checks failing**\n→ Check pod logs: `kubectl logs -f deployment/api -n production`\n\n**Migration errors**\n→ Rollback: Revert tag, migrations auto-rollback\n\n**High error rate**\n→ Emergency rollback: Previous tag auto-deploys via GitHub Actions\n\n## Best Practices\n- Deploy during low-traffic hours (2-4am PST)\n- Have 2 engineers available\n- Monitor for 30 min post-deploy\n- Update #engineering Slack with deploy notice\n\n## Related Docs\n- <mention-page url=\"...\">Rollback Procedure</mention-page>\n- <mention-page url=\"...\">Database Migration Guide</mention-page>\n```\n\n### 5. Make Discoverable\n```\nNotion:notion-update-page\npage_id: \"engineering-wiki-homepage\"\ncommand: \"insert_content_after\"\n```\nAdded link in Engineering Wiki → How-To Guides section\n\n## Key Success Factors\n- Captured tribal knowledge from discussion\n- Structured as actionable steps\n- Included troubleshooting from experience\n- Made discoverable by linking from wiki index\n- Added metadata (time, complexity)\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/reference/database-best-practices.md",
    "content": "# Database Best Practices\n\nGeneral guidance for creating and maintaining knowledge capture databases.\n\n## Core Principles\n\n### 1. Keep It Simple\n- Start with core properties\n- Add more only when needed\n- Don't over-engineer\n\n### 2. Use Consistent Naming\n- Title property for main identifier\n- Status for lifecycle tracking\n- Tags for flexible categorization\n- Owner for accountability\n\n### 3. Include Metadata\n- Created/Updated timestamps\n- Owner or maintainer\n- Last reviewed dates\n- Status indicators\n\n### 4. Enable Discovery\n- Use tags liberally\n- Create helpful views\n- Link related content\n- Use clear titles\n\n### 5. Plan for Scale\n- Consider filters early\n- Use relations for connections\n- Think about search\n- Organize with categories\n\n## Creating a Database\n\n### Using `Notion:notion-create-database`\n\nExample for documentation database:\n\n```javascript\n{\n  \"parent\": {\"page_id\": \"wiki-page-id\"},\n  \"title\": [{\"text\": {\"content\": \"Team Documentation\"}}],\n  \"properties\": {\n    \"Type\": {\n      \"select\": {\n        \"options\": [\n          {\"name\": \"How-To\", \"color\": \"blue\"},\n          {\"name\": \"Concept\", \"color\": \"green\"},\n          {\"name\": \"Reference\", \"color\": \"gray\"},\n          {\"name\": \"FAQ\", \"color\": \"yellow\"}\n        ]\n      }\n    },\n    \"Category\": {\n      \"select\": {\n        \"options\": [\n          {\"name\": \"Engineering\", \"color\": \"red\"},\n          {\"name\": \"Product\", \"color\": \"purple\"},\n          {\"name\": \"Design\", \"color\": \"pink\"}\n        ]\n      }\n    },\n    \"Tags\": {\"multi_select\": {\"options\": []}},\n    \"Owner\": {\"people\": {}},\n    \"Status\": {\n      \"select\": {\n        \"options\": [\n          {\"name\": \"Draft\", \"color\": \"gray\"},\n          {\"name\": \"Final\", \"color\": \"green\"},\n          {\"name\": \"Deprecated\", \"color\": \"red\"}\n        ]\n      }\n    }\n  }\n}\n```\n\n### Fetching Database Schema\n\nBefore creating pages, always fetch database to get schema:\n\n```\nNotion:notion-fetch\nid: \"database-url-or-id\"\n```\n\nThis returns the exact property names and types to use.\n\n## Database Selection Guide\n\n| Need | Use This Database |\n|------|-------------------|\n| General documentation | [Documentation Database](documentation-database.md) |\n| Track decisions | [Decision Log](decision-log-database.md) |\n| Q&A knowledge base | [FAQ Database](faq-database.md) |\n| Team-specific content | [Team Wiki](team-wiki-database.md) |\n| Step-by-step guides | [How-To Guide Database](how-to-guide-database.md) |\n| Incident/project learnings | [Learning Database](learning-database.md) |\n\n## Tips\n\n1. **Start with general documentation database** - most flexible\n2. **Add specialized databases** as needs emerge (FAQ, Decisions)\n3. **Use relations** to connect related docs\n4. **Create views** for common use cases\n5. **Review properties** quarterly - remove unused ones\n6. **Document the schema** in database description\n7. **Train team** on property usage and conventions\n\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/reference/decision-log-database.md",
    "content": "# Decision Log Database (ADR - Architecture Decision Records)\n\n**Purpose**: Track important decisions with context and rationale.\n\n## Schema\n\n| Property | Type | Options | Purpose |\n|----------|------|---------|---------|\n| **Decision** | title | - | What was decided |\n| **Date** | date | - | When decision was made |\n| **Status** | select | Proposed, Accepted, Superseded, Deprecated | Current decision status |\n| **Domain** | select | Architecture, Product, Business, Design, Operations | Decision category |\n| **Impact** | select | High, Medium, Low | Expected impact level |\n| **Deciders** | people | - | Who made the decision |\n| **Stakeholders** | people | - | Who's affected by decision |\n| **Related Decisions** | relation | Links to other decisions | Context and dependencies |\n\n## Usage\n\n```\nCreate decision records with properties:\n{\n  \"Decision\": \"Use PostgreSQL for Primary Database\",\n  \"Date\": \"2025-10-15\",\n  \"Status\": \"Accepted\",\n  \"Domain\": \"Architecture\",\n  \"Impact\": \"High\",\n  \"Deciders\": [tech_lead, architect],\n  \"Stakeholders\": [eng_team]\n}\n```\n\n## Content Template\n\nEach decision page should include:\n- **Context**: Why this decision was needed\n- **Decision**: What was decided\n- **Rationale**: Why this option was chosen\n- **Options Considered**: Alternatives and trade-offs\n- **Consequences**: Expected outcomes (positive and negative)\n- **Implementation**: How decision will be executed\n\n## Views\n\n**Recent Decisions**: Sort by Date descending\n**Active Decisions**: Filter where Status = \"Accepted\"\n**By Domain**: Group by Domain\n**High Impact**: Filter where Impact = \"High\"\n**Pending**: Filter where Status = \"Proposed\"\n\n## Best Practices\n\n1. **Document immediately**: Record decisions when made, while context is fresh\n2. **Include alternatives**: Show what was considered and why it wasn't chosen\n3. **Track superseded decisions**: Update status when decisions change\n4. **Link related decisions**: Use relations to show dependencies\n5. **Review periodically**: Check if old decisions are still valid\n\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/reference/documentation-database.md",
    "content": "# General Documentation Database\n\n**Purpose**: Store all types of documentation in a searchable, organized database.\n\n## Schema\n\n| Property | Type | Options | Purpose |\n|----------|------|---------|---------|\n| **Title** | title | - | Document name |\n| **Type** | select | How-To, Concept, Reference, FAQ, Decision, Post-Mortem | Categorize content type |\n| **Category** | select | Engineering, Product, Design, Operations, General | Organize by department/topic |\n| **Tags** | multi_select | - | Additional categorization (languages, tools, topics) |\n| **Status** | select | Draft, In Review, Final, Deprecated | Track document lifecycle |\n| **Owner** | people | - | Document maintainer |\n| **Created** | created_time | - | Auto-populated creation date |\n| **Last Updated** | last_edited_time | - | Auto-populated last edit |\n| **Last Reviewed** | date | - | Manual review tracking |\n\n## Usage\n\n```\nCreate pages with properties:\n{\n  \"Title\": \"How to Deploy to Production\",\n  \"Type\": \"How-To\",\n  \"Category\": \"Engineering\",\n  \"Tags\": \"deployment, production, DevOps\",\n  \"Status\": \"Final\",\n  \"Owner\": [current_user],\n  \"Last Reviewed\": \"2025-10-01\"\n}\n```\n\n## Views\n\n**By Type**: Group by Type property\n**By Category**: Group by Category property  \n**Recent Updates**: Sort by Last Updated descending\n**Needs Review**: Filter where Last Reviewed > 90 days ago\n**Draft Docs**: Filter where Status = \"Draft\"\n\n## Creating This Database\n\nUse `Notion:notion-create-database`:\n\n```javascript\n{\n  \"parent\": {\"page_id\": \"wiki-page-id\"},\n  \"title\": [{\"text\": {\"content\": \"Team Documentation\"}}],\n  \"properties\": {\n    \"Type\": {\n      \"select\": {\n        \"options\": [\n          {\"name\": \"How-To\", \"color\": \"blue\"},\n          {\"name\": \"Concept\", \"color\": \"green\"},\n          {\"name\": \"Reference\", \"color\": \"gray\"},\n          {\"name\": \"FAQ\", \"color\": \"yellow\"}\n        ]\n      }\n    },\n    \"Category\": {\n      \"select\": {\n        \"options\": [\n          {\"name\": \"Engineering\", \"color\": \"red\"},\n          {\"name\": \"Product\", \"color\": \"purple\"},\n          {\"name\": \"Design\", \"color\": \"pink\"}\n        ]\n      }\n    },\n    \"Tags\": {\"multi_select\": {\"options\": []}},\n    \"Owner\": {\"people\": {}},\n    \"Status\": {\n      \"select\": {\n        \"options\": [\n          {\"name\": \"Draft\", \"color\": \"gray\"},\n          {\"name\": \"Final\", \"color\": \"green\"},\n          {\"name\": \"Deprecated\", \"color\": \"red\"}\n        ]\n      }\n    }\n  }\n}\n```\n\n## Best Practices\n\n1. **Start with this schema** - most flexible for general documentation\n2. **Use relations** to connect related docs\n3. **Create views** for common use cases\n4. **Review properties** quarterly - remove unused ones\n5. **Document the schema** in database description\n6. **Train team** on property usage and conventions\n\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/reference/faq-database.md",
    "content": "# FAQ Database\n\n**Purpose**: Organize frequently asked questions with answers.\n\n## Schema\n\n| Property | Type | Options | Purpose |\n|----------|------|---------|---------|\n| **Question** | title | - | The question being asked |\n| **Category** | select | Product, Engineering, Support, HR, General | Question topic |\n| **Tags** | multi_select | - | Specific topics (auth, billing, onboarding, etc.) |\n| **Answer Type** | select | Quick Answer, Detailed Guide, Link to Docs | Response format |\n| **Last Reviewed** | date | - | When answer was verified |\n| **Helpful Count** | number | - | Track usefulness (optional) |\n| **Audience** | select | Internal, External, All | Who should see this |\n| **Related Questions** | relation | Links to related FAQs | Connect similar topics |\n\n## Usage\n\n```\nCreate FAQ entries with properties:\n{\n  \"Question\": \"How do I reset my password?\",\n  \"Category\": \"Support\",\n  \"Tags\": \"authentication, password, login\",\n  \"Answer Type\": \"Quick Answer\",\n  \"Last Reviewed\": \"2025-10-01\",\n  \"Audience\": \"External\"\n}\n```\n\n## Content Template\n\nEach FAQ page should include:\n- **Short Answer**: 1-2 sentence quick response\n- **Detailed Explanation**: Full answer with context\n- **Steps** (if applicable): Numbered procedure\n- **Screenshots** (if helpful): Visual guidance\n- **Related Questions**: Links to similar FAQs\n- **Additional Resources**: External docs or videos\n\n## Views\n\n**By Category**: Group by Category\n**Recently Updated**: Sort by Last Reviewed descending\n**Needs Review**: Filter where Last Reviewed > 180 days ago\n**External FAQs**: Filter where Audience contains \"External\"\n**Popular**: Sort by Helpful Count descending (if tracking)\n\n## Best Practices\n\n1. **Use clear questions**: Write questions as users would ask them\n2. **Provide quick answers**: Lead with the direct answer, then elaborate\n3. **Link related FAQs**: Help users discover related information\n4. **Review regularly**: Keep answers current and accurate\n5. **Track what's helpful**: Use feedback to improve frequently accessed FAQs\n\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/reference/how-to-guide-database.md",
    "content": "# How-To Guide Database\n\n**Purpose**: Procedural documentation for common tasks.\n\n## Schema\n\n| Property | Type | Options | Purpose |\n|----------|------|---------|---------|\n| **Title** | title | - | \"How to [Task]\" |\n| **Complexity** | select | Beginner, Intermediate, Advanced | Skill level required |\n| **Time Required** | number | - | Estimated minutes to complete |\n| **Prerequisites** | relation | Links to other guides | Required knowledge |\n| **Category** | select | Development, Deployment, Testing, Tools | Task category |\n| **Last Tested** | date | - | When procedure was verified |\n| **Tags** | multi_select | - | Technology/tool tags |\n\n## Usage\n\n```\nCreate how-to guides with properties:\n{\n  \"Title\": \"How to Set Up Local Development Environment\",\n  \"Complexity\": \"Beginner\",\n  \"Time Required\": 30,\n  \"Category\": \"Development\",\n  \"Last Tested\": \"2025-10-01\",\n  \"Tags\": \"setup, environment, docker\"\n}\n```\n\n## Best Practices\n\n1. **Use consistent naming**: Always start with \"How to...\"\n2. **Test procedures**: Verify steps work before publishing\n3. **Include time estimates**: Help users plan their time\n4. **Link prerequisites**: Make dependencies clear\n5. **Update regularly**: Re-test procedures when tools/systems change\n\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/reference/learning-database.md",
    "content": "# Learning/Post-Mortem Database\n\n**Purpose**: Capture learnings from incidents, projects, or experiences.\n\n## Schema\n\n| Property | Type | Options | Purpose |\n|----------|------|---------|---------|\n| **Title** | title | - | Event or project name |\n| **Date** | date | - | When it happened |\n| **Type** | select | Incident, Project, Experiment, Retrospective | Learning type |\n| **Severity** | select | Critical, Major, Minor | Impact level (for incidents) |\n| **Team** | people | - | Who was involved |\n| **Key Learnings** | number | - | Count of learnings |\n| **Action Items** | relation | Links to tasks | Follow-up actions |\n\n## Content Template\n\nEach learning page should include:\n- **What Happened**: Situation description\n- **What Went Well**: Success factors\n- **What Didn't Go Well**: Problems encountered\n- **Root Causes**: Why things happened\n- **Learnings**: Key takeaways\n- **Action Items**: Improvements to implement\n\n## Best Practices\n\n1. **Blameless approach**: Focus on systems and processes, not individuals\n2. **Document quickly**: Capture while memory is fresh\n3. **Identify root causes**: Go beyond surface-level problems\n4. **Create action items**: Turn learnings into improvements\n5. **Follow up**: Track that action items are completed\n6. **Share widely**: Make learnings accessible to entire team\n\n"
  },
  {
    "path": "skills/.curated/notion-knowledge-capture/reference/team-wiki-database.md",
    "content": "# Team Wiki Database\n\n**Purpose**: Centralized team knowledge and resources.\n\n## Schema\n\n| Property | Type | Options | Purpose |\n|----------|------|---------|---------|\n| **Title** | title | - | Page name |\n| **Section** | select | Getting Started, Processes, Tools, Reference, Onboarding | Wiki organization |\n| **Tags** | multi_select | - | Topic tags |\n| **Owner** | people | - | Page maintainer |\n| **Last Updated** | last_edited_time | - | Auto-tracked |\n| **Visibility** | select | Public, Team Only, Confidential | Access level |\n\n## Usage\n\nUse for team-specific documentation that doesn't fit other databases.\n\n## Best Practices\n\n1. **Organize by sections**: Use clear top-level organization\n2. **Assign owners**: Every page should have a maintainer\n3. **Control visibility**: Set appropriate access levels\n4. **Link extensively**: Connect related pages\n5. **Keep current**: Regular reviews to remove outdated content\n\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/LICENSE.txt",
    "content": "Copyright 2025 Notion Labs, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/SKILL.md",
    "content": "---\nname: notion-meeting-intelligence\ndescription: Prepare meeting materials with Notion context and Codex research; use when gathering context, drafting agendas/pre-reads, and tailoring materials to attendees.\nmetadata:\n  short-description: Prep meetings with Notion context and tailored agendas\n---\n\n# Meeting Intelligence\n\nPrep meetings by pulling Notion context, tailoring agendas/pre-reads, and enriching with Codex research.\n\n## Quick start\n1) Confirm meeting goal, attendees, date/time, and decisions needed.\n2) Gather context: search with `Notion:notion-search`, then fetch with `Notion:notion-fetch` (prior notes, specs, OKRs, decisions).\n3) Pick the right template via `reference/template-selection-guide.md` (status, decision, planning, retro, 1:1, brainstorming).\n4) Draft agenda/pre-read in Notion with `Notion:notion-create-pages`, embedding source links and owner/timeboxes.\n5) Enrich with Codex research (industry insights, benchmarks, risks) and update the page with `Notion:notion-update-page` as plans change.\n\n## Workflow\n### 0) If any MCP call fails because Notion MCP is not connected, pause and set it up:\n1. Add the Notion MCP:\n   - `codex mcp add notion --url https://mcp.notion.com/mcp`\n2. Enable remote MCP client:\n   - Set `[features].rmcp_client = true` in `config.toml` **or** run `codex --enable rmcp_client`\n3. Log in with OAuth:\n   - `codex mcp login notion`\n\nAfter successful login, the user will have to restart codex. You should finish your answer and tell them so when they try again they can continue with Step 1.\n\n### 1) Gather inputs\n- Ask for objective, desired outcomes/decisions, attendees, duration, date/time, and prior materials.\n- Search Notion for relevant docs, past notes, specs, and action items (`Notion:notion-search`), then fetch key pages (`Notion:notion-fetch`).\n- Capture blockers/risks and open questions up front.\n\n### 2) Choose format\n- Status/update → status template.\n- Decision/approval → decision template.\n- Planning (sprint/project) → planning template.\n- Retro/feedback → retrospective template.\n- 1:1 → one-on-one template.\n- Ideation → brainstorming template.\n- Use `reference/template-selection-guide.md` to confirm.\n\n### 3) Build the agenda/pre-read\n- Start from the chosen template in `reference/` and adapt sections (context, goals, agenda, owner/time per item, decisions, risks, prep asks).\n- Include links to pulled Notion pages and any required pre-reading.\n- Assign owners for each agenda item; call out timeboxes and expected outputs.\n\n### 4) Enrich with research\n- Add concise Codex research where helpful: market/industry facts, benchmarks, risks, best practices.\n- Keep claims cited with source links; separate fact from opinion.\n\n### 5) Finalize and share\n- Add next steps and owners for follow-ups.\n- If tasks arise, create/link tasks in the relevant Notion database.\n- Update the page via `Notion:notion-update-page` when details change; keep a brief changelog if multiple edits.\n\n## References and examples\n- `reference/` — template picker and meeting templates (e.g., `template-selection-guide.md`, `status-update-template.md`, `decision-meeting-template.md`, `sprint-planning-template.md`, `one-on-one-template.md`, `retrospective-template.md`, `brainstorming-template.md`).\n- `examples/` — end-to-end meeting preps (e.g., `executive-review.md`, `project-decision.md`, `sprint-planning.md`, `customer-meeting.md`).\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Notion Meeting Intelligence\"\n  short_description: \"Prep meetings with Notion context and tailored agendas\"\n  icon_small: \"./assets/notion-small.svg\"\n  icon_large: \"./assets/notion.png\"\n  default_prompt: \"Prepare this meeting from Notion context with a brief, agenda, decisions needed, and open questions.\"\n\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"notion\"\n      description: \"Notion MCP server\"\n      transport: \"streamable_http\"\n      url: \"https://mcp.notion.com/mcp\"\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/evaluations/README.md",
    "content": "# Meeting Intelligence Skill Evaluations\n\nEvaluation scenarios for testing the Meeting Intelligence skill across different Codex models.\n\n## Purpose\n\nThese evaluations ensure the Meeting Intelligence skill:\n- Gathers context from Notion workspace\n- Enriches with Codex research appropriately\n- Creates both internal pre-reads and external agendas\n- Distinguishes between Notion facts and Codex insights\n- Works consistently across Haiku, Sonnet, and Opus\n\n## Evaluation Files\n\n### decision-meeting-prep.json\nTests preparation for a decision-making meeting.\n\n**Scenario**: Prep for database migration decision meeting  \n**Key Behaviors**:\n- Searches Notion for migration context (specs, discussions, options)\n- Fetches 2-3 relevant pages\n- Enriches with Codex research (decision frameworks, migration best practices)\n- Creates comprehensive internal pre-read with recommendation\n- Creates clean, professional external agenda\n- Clearly distinguishes Notion facts from Codex insights\n- Cross-links both documents\n\n### status-meeting-prep.json\nTests preparation for a status update or review meeting.\n\n**Scenario**: Prep for project status review  \n**Key Behaviors**:\n- Gathers project metrics and progress from Notion\n- Fetches relevant pages (roadmap, tasks, milestones)\n- Adds Codex context (industry benchmarks, best practices)\n- Creates internal pre-read with honest assessment\n- Creates external agenda with structured flow\n- Includes source citations using mention-page tags\n- Time-boxes agenda items\n\n## Running Evaluations\n\n1. Enable the `meeting-intelligence` skill\n2. Submit the query from the evaluation file\n3. Verify the skill searches Notion first (not Codex research)\n4. Check that TWO documents are created (internal + external)\n5. Verify Codex enrichment adds value without replacing Notion content\n6. Test with Haiku, Sonnet, and Opus\n\n## Expected Skill Behaviors\n\nMeeting Intelligence evaluations should verify:\n\n### Notion Context Gathering\n- Searches workspace for relevant context first\n- Fetches specific pages (not generic)\n- Extracts key information from Notion content\n- Cites sources using mention-page tags\n\n### Codex Research Integration\n- Adds industry context, frameworks, or best practices\n- Enrichment is relevant and valuable (not filler)\n- Clearly distinguishes Notion facts from Codex insights\n- Research complements (doesn't replace) Notion content\n\n### Two-Document Creation\n- **Internal Pre-Read**: Comprehensive, includes strategy, recommendations, detailed pros/cons\n- **External Agenda**: Professional, focused on meeting flow, no internal strategy\n- Both documents are clearly labeled\n- Documents are cross-linked\n\n### Document Quality\n- Pre-read follows structure: Overview → Background → Current Status → Context & Insights → Discussion Points\n- Agenda follows structure: Details → Objective → Agenda Items (with times) → Decisions → Actions → Resources\n- Titles include date or meeting context\n- Content is actionable and meeting-ready\n\n## Creating New Evaluations\n\nWhen adding Meeting Intelligence evaluations:\n\n1. **Test different meeting types** - Decision, status, brainstorm, 1:1, sprint planning, retrospective\n2. **Vary complexity** - Simple updates vs. complex strategic decisions\n3. **Test with/without Notion content** - Rich workspace vs. minimal existing pages\n4. **Verify enrichment value** - Is Codex research genuinely helpful?\n5. **Check internal/external distinction** - Is sensitive info kept in pre-read only?\n\n## Example Success Criteria\n\n**Good** (specific, testable):\n- \"Creates TWO documents (internal pre-read + external agenda)\"\n- \"Internal pre-read marked 'INTERNAL ONLY' or 'For team only'\"\n- \"Cites at least 2-3 Notion pages using mention-page tags\"\n- \"Agenda includes time allocations for each section\"\n- \"Codex enrichment includes decision frameworks or best practices\"\n\n**Bad** (vague, untestable):\n- \"Creates meeting materials\"\n- \"Gathers context effectively\"\n- \"Prepares well\"\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/evaluations/decision-meeting-prep.json",
    "content": "{\n  \"name\": \"Decision Meeting Preparation\",\n  \"skills\": [\"meeting-intelligence\"],\n  \"query\": \"Prep for tomorrow's meeting where we need to decide on our database migration approach. Create both an internal pre-read for the team and an agenda for the meeting.\",\n  \"expected_behavior\": [\n    \"Step 1: Uses Notion:notion-search to find context about database migration (project pages, technical specs, previous discussions, options analysis)\",\n    \"Step 2: Fetches at least 2-3 relevant pages using Notion:notion-fetch to gather information from Notion\",\n    \"Step 3: Identifies the decision to be made and available options from fetched Notion content\",\n    \"Step 4: Enriches with Codex research - adds decision-making frameworks (e.g., cost-benefit analysis, risk assessment), technical context for migration approaches, best practices for database migrations\",\n    \"Step 5: Distinguishes Notion facts from Codex insights in synthesis\",\n    \"Step 6: Creates INTERNAL PRE-READ using Notion:notion-create-pages with title like 'INTERNAL: Database Migration Decision - Pre-Read - [Date]'\",\n    \"Step 6a: Internal pre-read includes: Meeting overview, background context (from Notion), current status and technical details, context & insights (from Codex research on migration best practices), decision options with detailed pros/cons, recommendation with rationale, what we need from meeting\",\n    \"Step 6b: Internal pre-read marked clearly as 'INTERNAL ONLY' or 'For team only'\",\n    \"Step 7: Creates EXTERNAL AGENDA using Notion:notion-create-pages with title like 'Meeting Agenda: Database Migration Decision - [Date]'\",\n    \"Step 7a: External agenda includes: Meeting details, objective (clear decision to make), agenda items with time allocations, discussion topics, decisions needed, action items section (empty), related resources with link to pre-read\",\n    \"Step 7b: External agenda is clean, professional, focused (no internal strategy or detailed pros/cons)\",\n    \"Step 8: Links both documents together (agenda mentions pre-read, pre-read mentions agenda)\",\n    \"Both documents link to source pages using <mention-page url='...'>\"\n  ],\n  \"success_criteria\": [\n    \"TWO documents are created (internal pre-read + external agenda), not just one\",\n    \"Internal pre-read is comprehensive with: Notion context + Codex insights + detailed pros/cons + recommendation\",\n    \"Internal pre-read is clearly marked 'INTERNAL' or 'For team only'\",\n    \"External agenda is professional, structured, focused on meeting flow (not internal strategy)\",\n    \"Codex enrichment is present and adds value (decision frameworks, migration best practices, risk patterns)\",\n    \"Notion facts are clearly sourced, Codex insights are distinguished\",\n    \"At least 2-3 Notion source pages are cited using mention-page tags\",\n    \"Internal pre-read follows structure from SKILL.md Step 5 (Meeting Overview → Background → Current Status → Context & Insights → Key Discussion Points → What We Need)\",\n    \"External agenda follows structure from SKILL.md Step 6 (Meeting Details → Objective → Agenda Items → Discussion Topics → Decisions Needed → Action Items → Related Resources)\",\n    \"Documents are cross-linked (pre-read mentions agenda, agenda mentions pre-read)\",\n    \"Meeting date is included in both titles\",\n    \"Uses correct tool names (Notion:notion-search, Notion:notion-fetch, Notion:notion-create-pages for BOTH documents)\"\n  ]\n}\n\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/evaluations/status-meeting-prep.json",
    "content": "{\n  \"name\": \"Status Update Meeting Preparation\",\n  \"skills\": [\"meeting-intelligence\", \"task-manager\"],\n  \"query\": \"Prep for Friday's project status meeting on the Mobile App Redesign project. Create both an internal pre-read and an external agenda.\",\n  \"expected_behavior\": [\n    \"Step 1: Uses Notion:notion-search to find Mobile App Redesign project page\",\n    \"Step 2: Fetches project page using Notion:notion-fetch to get current status and context\",\n    \"Step 3: Uses Notion:notion-search to find tasks database\",\n    \"Step 4: Queries task database using Notion:notion-query-data-sources for project tasks (WHERE Project = 'Mobile App Redesign')\",\n    \"Step 5: Analyzes task data: calculates completion %, identifies completed work, in-progress items, and blockers\",\n    \"Step 6: Enriches with Codex research - adds project management insights (velocity trends, risk patterns, common project pitfalls), suggests discussion frameworks if risks identified, provides context on timeline implications\",\n    \"Step 7: Creates INTERNAL PRE-READ using Notion:notion-create-pages with title 'INTERNAL: Mobile App Redesign Status - Pre-Read - [Date]'\",\n    \"Step 7a: Internal pre-read includes: Project overview, current status with metrics (from Notion/tasks), progress summary with specifics, context & insights (Codex research on project health patterns), honest assessment of challenges/risks, what we need from meeting\",\n    \"Step 7b: Internal pre-read contains detailed metrics, blockers, and strategic considerations\",\n    \"Step 8: Creates EXTERNAL AGENDA using Notion:notion-create-pages with title 'Meeting Agenda: Mobile App Redesign Status Update - [Date]'\",\n    \"Step 8a: External agenda uses Status Update structure: Meeting Details → Objective → Agenda Items (timed) → Discussion Topics → Action Items\",\n    \"Step 8b: External agenda is concise, professional, focuses on meeting flow (summary-level metrics only)\",\n    \"Step 9: Links both documents together\",\n    \"Both documents link to project page and task database using <mention-page> and <mention-database>\"\n  ],\n  \"success_criteria\": [\n    \"TWO documents are created (internal pre-read + external agenda)\",\n    \"Internal pre-read contains: Detailed metrics from task query, honest assessment of blockers/risks, Codex insights on project health patterns, strategic considerations\",\n    \"Internal pre-read is marked 'INTERNAL' or 'For team only'\",\n    \"External agenda is professional status update format with: Overall status, progress summary (high-level), upcoming work, discussion topics, action items section\",\n    \"External agenda does NOT contain detailed risk analysis or internal strategic discussion (that's in pre-read)\",\n    \"Progress metrics are accurate based on Notion:notion-query-data-sources results (e.g., X complete, Y in progress, Z blocked)\",\n    \"Blockers identified from task query are included in internal pre-read with full context\",\n    \"Codex enrichment adds value (project management insights, risk patterns, velocity analysis)\",\n    \"Links to project page and task database using mention tags\",\n    \"Documents are cross-linked for easy navigation\",\n    \"Uses correct tool names (Notion:notion-search, Notion:notion-fetch, Notion:notion-query-data-sources, Notion:notion-create-pages for BOTH documents)\"\n  ]\n}\n\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/examples/customer-meeting.md",
    "content": "# Example: Customer Meeting Prep\n\n**User Request**: \"Prep for sales meeting with Acme Corp on Thursday\"\n\n## Workflow\n\n### 1. Search Customer Info\n```\nNotion:notion-search\nquery: \"Acme Corp\"\n```\n\nFound:\n- CRM notes from initial call\n- Product demo feedback\n- Pricing proposal\n- Competitor analysis\n\n### 2. Fetch Details\n```\nNotion:notion-fetch (4 pages)\n```\n\n**Key context**:\n- **Company**: 500-person fintech startup, Series B\n- **Pain Points**: Manual workflows, data silos, scaling issues\n- **Decision Makers**: CTO (technical), VP Product (user needs), CFO (budget)\n- **Timeline**: Need solution by Q1\n- **Budget**: $50-75K/year range\n- **Competition**: Evaluating us vs. Competitor A and Competitor B\n\n### 3. Add Research Context\nCodex research added:\n- Fintech industry compliance requirements\n- Common integration needs for financial products\n- Typical procurement processes for Series B companies\n\n### 4. Create Internal Pre-Read\n```\nNotion:notion-create-pages\npages: [{\n  properties: {\n    title: \"Acme Corp Meeting - Pre-Read (Internal)\"\n  },\n  content: \"# Acme Corp Meeting - Pre-Read (Internal)\n\n## Company Overview\n**Company**: Acme Corp (500-person fintech, Series B)\n**Decision Makers**: CTO, VP Product, CFO\nSource: <mention-page url='...'>CRM Notes</mention-page>\n\n## Their Pain Points → Our Solutions\n- Manual workflows → Workflow automation\n- Data silos → API integrations\n- Scaling issues → Enterprise features\n\n## Competitive Position\n**Our advantages over Competitor A/B**:\n- Better integration ecosystem (Stripe, Plaid)\n- Stronger compliance features\n- Faster implementation (4 weeks vs 12 weeks)\n\n## Pricing Strategy\nRecommend: $60K/year (mid-range of their budget)\n\n## Potential Objections & Responses\n...\"\n}]\n```\n\n### 5. Create Meeting Agenda\n```\nNotion:notion-create-pages\npages: [{\n  properties: {\n    title: \"Acme Corp - Product Demo & Q&A\"\n  },\n  content: \"# Acme Corp - Product Demo & Q&A\n\n## Meeting Details\n**Date**: Thursday, Oct 17, 2025 @ 2pm PST\n**Attendees**: CTO, VP Product, CFO (Acme) | Sales Lead, Solutions Engineer (Us)\n**Duration**: 60 minutes\n\n## Objective\nDemonstrate how our platform solves Acme's workflow automation and integration challenges.\n\n## Agenda\n\n**1. Introductions** (5 min)\n\n**2. Understand Current Workflow** (10 min)\n- Current pain points\n- Integration requirements\n- Success criteria\n\n**3. Product Demo** (25 min)\n- Workflow automation capabilities\n- API integrations (Stripe, Plaid)\n- Security & compliance features\n\n**4. Pricing & Implementation** (10 min)\n\n**5. Next Steps** (10 min)\n\"\n}]\n```\n\n### 6. Link Resources\nConnected agenda to CRM page, pricing sheet, and technical integration docs.\n\n## Outputs\n\n**Internal Pre-Read**: Full context for sales team\n**Customer Agenda**: Professional meeting structure\n**Both in Notion** with links to supporting materials\n\n## Key Success Factors\n- Understood customer's specific pain points\n- Researched industry context (fintech compliance)\n- Mapped features to their needs\n- Prepared competitive differentiators\n- Structured demo around their use cases\n- Pre-planned objection responses\n- Clear next steps in agenda\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/examples/executive-review.md",
    "content": "# Example: Executive Review Prep\n\n**User Request**: \"Prep for quarterly executive review on Friday\"\n\n## Workflow\n\n### 1. Search for Context\n```\nNotion:notion-search\nquery: \"Q4 objectives\" + \"KPIs\" + \"quarterly results\"\n```\n\nFound:\n- Q4 OKRs and progress\n- Product metrics dashboard\n- Engineering velocity reports\n- Customer feedback summary\n\n### 2. Fetch & Analyze\n```\nNotion:notion-fetch (5 pages)\n```\n\n**Key metrics**:\n- **Revenue**: $2.4M ARR (96% of Q4 target)\n- **Customer Growth**: 145 new customers (exceeds 120 target)\n- **Churn**: 3.2% (below 5% target)\n- **Product**: 3 major features shipped, 2 in beta\n- **Engineering**: 94% uptime (above 95% SLA)\n\n### 3. Add Codex Research Context\nAdded context on:\n- Industry benchmarks for SaaS metrics\n- Typical Q4 sales patterns\n- Best practices for executive presentations\n\n### 4. Create Pre-Read (Internal)\n```\nNotion:notion-create-pages\ntitle: \"Q4 Review - Pre-Read (Internal)\"\n```\n\n**Pre-read sections**:\n- **Executive Summary**: Strong quarter, missed revenue by 4% but exceeded customer growth\n- **Detailed Metrics**: All KPIs with trend lines\n- **Wins**: Product launches, key customer acquisitions\n- **Challenges**: Sales pipeline conversion, engineering hiring\n- **Q1 Preview**: Strategic priorities\n\n### 5. Create Presentation Agenda\n```\nNotion:notion-create-pages\ntitle: \"Q4 Executive Review - Agenda\"\n```\n\n**Agenda** (90 min):\n- Q4 Results Overview (15 min)\n- Revenue & Growth Deep Dive (20 min)\n- Product & Engineering Update (20 min)\n- Customer Success Highlights (15 min)\n- Q1 Strategic Plan (15 min)\n- Discussion & Questions (15 min)\n\n### 6. Link Supporting Docs\nConnected to OKRs, metrics dashboards, and Q1 planning docs.\n\n## Outputs\n\n**Internal Pre-Read**: Comprehensive context with honest assessment\n**Executive Agenda**: Structured 90-min presentation\n**Both in Notion** with links to supporting data\n\n## Key Success Factors\n- Synthesized data from multiple sources (OKRs, metrics, feedback)\n- Added industry context and benchmarks\n- Created honest internal assessment (not just wins)\n- Structured agenda with time allocations\n- Linked to source data for drill-down during Q&A\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/examples/project-decision.md",
    "content": "# Example: Project Decision Meeting Prep\n\n## User Request\n\n> \"Prep for tomorrow's architecture meeting where we need to decide on database migration strategy\"\n\n## Workflow Execution\n\n### Step 1: Search for Context\n\n```\nNotion:notion-search\nquery: \"database migration\"\nquery_type: \"internal\"\nteamspace_id: \"engineering-teamspace-id\"\nfilters: {\n  created_date_range: {\n    start_date: \"2024-01-01\"\n  }\n}\n```\n\n**Results Found:**\n- \"Database Migration Proposal\" (Engineering)\n- \"Current Database Performance Issues\" (Engineering)\n- \"Migration Options Analysis\" (Architecture)\n- \"Previous Migration Post-mortem\" (Engineering)\n\n### Step 2: Fetch Relevant Pages\n\n```\nNotion:notion-fetch\nid: \"database-migration-proposal-page-id\"\n```\n\n**Extracted**: Proposes migrating from MongoDB to PostgreSQL, outlines timeline and risks\n\n```\nNotion:notion-fetch\nid: \"database-performance-issues-page-id\"\n```\n\n**Extracted**: Current pain points - slow queries, schema flexibility issues, transaction limitations\n\n```\nNotion:notion-fetch\nid: \"migration-options-analysis-page-id\"\n```\n\n**Extracted**: Three options analyzed:\n1. Stay on MongoDB with optimizations\n2. Migrate to PostgreSQL\n3. Hybrid approach (MongoDB + PostgreSQL)\n\n```\nNotion:notion-fetch\nid: \"previous-migration-postmortem-page-id\"\n```\n\n**Extracted**: Lessons from 2023 Redis migration - importance of rollback plan, testing strategy\n\n### Step 3: Synthesize Pre-Read\n\n**Key Information Identified:**\n- **Problem**: MongoDB performance degrading, schema flexibility causing data consistency issues\n- **Options**: Three approaches with different trade-offs\n- **Recommendation**: PostgreSQL migration favored by engineering team\n- **Concerns**: Downtime requirements, data migration complexity, application changes needed\n- **Historical context**: Previous migration took 6 weeks, had rollback issues\n\n### Step 4: Create Meeting Agenda\n\n```\nNotion:notion-create-pages\n\nparent: { page_id: \"meetings-parent-id\" }\n\npages: [{\n  properties: {\n    \"title\": \"Architecture Decision: Database Migration Strategy - Oct 15, 2025\"\n  },\n  content: \"[Decision meeting agenda]\"\n}]\n```\n\n**Agenda Created:**\n\n```markdown\n# Architecture Decision: Database Migration Strategy\n\n## Meeting Details\n**Date & Time**: October 15, 2025 at 2:00 PM  \n**Duration**: 90 minutes  \n**Location**: Conference Room B / Zoom  \n**Facilitator**: Sarah Chen (Engineering Lead)  \n**Attendees**:\n- Sarah Chen (Engineering Lead)\n- David Kim (CTO)\n- Alice Wang (Backend Team Lead)\n- Bob Martinez (DevOps Lead)\n- Carol Johnson (Product Director)\n\n## Pre-Read Summary\n\n### Background\n\nOur MongoDB database is experiencing performance degradation as we scale. Query response times have increased from 50ms to 400ms average over the past 6 months. The schema-less nature that initially provided flexibility is now causing data consistency issues and making application logic more complex.\n\n**Source**: <mention-page url=\"...\">Current Database Performance Issues</mention-page>\n\n### Current Situation\n\n**Performance metrics**:\n- Average query time: 400ms (was 50ms 6 months ago)\n- p95 query time: 1.2s (was 200ms)\n- Database size: 500GB (growing 20GB/month)\n- Connection pool exhaustion during peak traffic\n\n**Technical debt**:\n- 15+ application-layer validation rules compensating for lack of schema\n- Complex data migration scripts for schema changes\n- Limited transaction support causing race conditions\n\n**Source**: <mention-page url=\"...\">Database Migration Proposal</mention-page>\n\n### Historical Context\n\nWe successfully migrated from Redis to Memcached in 2023, which took 6 weeks. Key learnings:\n- Underestimated application code changes (3 weeks instead of 1 week)\n- Rollback plan was crucial when we discovered compatibility issues\n- Parallel running period (dual writes) was essential for safe migration\n\n**Source**: <mention-page url=\"...\">Previous Migration Post-mortem</mention-page>\n\n## Decision Required\n\n**Question**: Which database migration strategy should we adopt?\n\n**Timeline**: Need decision by end of week to include in Q4 planning\n\n**Impact**: \n- Engineering team (4-8 weeks of work)\n- Application architecture\n- Operations & monitoring\n- Future feature development velocity\n\n## Options Analysis\n\n### Option A: Stay on MongoDB with Optimizations\n\n**Description**: Invest in MongoDB performance tuning, add indexes, upgrade to latest version, implement better query patterns.\n\n**Pros**:\n- ✅ No migration complexity\n- ✅ Team familiar with MongoDB\n- ✅ Can implement immediately\n- ✅ Lower risk\n- ✅ Estimated 2 weeks effort\n\n**Cons**:\n- ❌ Doesn't solve fundamental schema flexibility issues\n- ❌ Still limited transaction support\n- ❌ Performance improvements may be temporary\n- ❌ Continues technical debt accumulation\n\n**Cost/Effort**: 2 weeks engineering + $5K/year additional MongoDB infrastructure\n\n**Risk**: Medium - Improvements may not be sufficient\n\n**Source**: <mention-page url=\"...\">Migration Options Analysis</mention-page>\n\n### Option B: Migrate to PostgreSQL\n\n**Description**: Full migration from MongoDB to PostgreSQL. Redesign schema with proper constraints, implement dual-write period, then cut over.\n\n**Pros**:\n- ✅ Solves schema consistency issues\n- ✅ Full ACID transactions\n- ✅ Better performance for relational queries\n- ✅ Lower long-term complexity\n- ✅ Industry standard, easier hiring\n\n**Cons**:\n- ❌ High migration effort (6-8 weeks)\n- ❌ Requires schema redesign\n- ❌ Application code changes extensive\n- ❌ Risk of data loss during migration\n- ❌ Downtime required (4-6 hours estimated)\n\n**Cost/Effort**: 8 weeks engineering + $8K migration costs - $15K/year MongoDB savings = net $7K/year savings\n\n**Risk**: High - Complex migration, application changes required\n\n**Recommendation**: ✅ **Favored by engineering team**\n\n**Source**: <mention-page url=\"...\">Database Migration Proposal</mention-page>\n\n### Option C: Hybrid Approach\n\n**Description**: Keep MongoDB for document-heavy data (logs, analytics), migrate transactional data to PostgreSQL. Run both databases.\n\n**Pros**:\n- ✅ Phased migration (lower risk)\n- ✅ Use best tool for each data type\n- ✅ Can migrate incrementally\n- ✅ Smaller initial scope (4 weeks)\n\n**Cons**:\n- ❌ Increased operational complexity\n- ❌ Two databases to maintain\n- ❌ Data consistency between databases challenging\n- ❌ Higher infrastructure costs\n- ❌ Complex application logic\n\n**Cost/Effort**: 4 weeks initial + ongoing complexity + $10K/year additional infrastructure\n\n**Risk**: Medium - Operational complexity increases\n\n**Source**: <mention-page url=\"...\">Migration Options Analysis</mention-page>\n\n### Option D: Do Nothing\n\n**Description**: Accept current performance and continue with MongoDB as-is.\n\n**Implications**:\n- Performance continues to degrade\n- Technical debt increases\n- Feature development slows\n- Customer experience suffers\n- Eventually forced into emergency migration\n\n**Not recommended**\n\n## Discussion Topics\n\n### Technical Feasibility\n1. Can we achieve < 4 hours downtime for Option B?\n2. What's the rollback plan if PostgreSQL migration fails?\n3. How do we handle data migration for 500GB?\n4. Schema design - what constraints do we need?\n\n### Business Impact\n5. What's the customer impact of 4-6 hours downtime?\n6. Can we schedule migration during low-traffic period?\n7. How does this affect Q4 feature roadmap?\n8. Cost-benefit analysis over 2-year horizon?\n\n### Risk Management\n9. What are the biggest risks with Option B?\n10. How do we test thoroughly before cutover?\n11. What's the rollback procedure and time?\n12. Do we have necessary expertise on team?\n\n### Timeline & Resources\n13. Can we allocate 2 engineers full-time for 8 weeks?\n14. Do we need external consultants?\n15. What's the impact on other Q4 projects?\n16. When could we realistically complete this?\n\n## Decision Framework\n\n**Evaluation criteria**:\n1. **Performance improvement**: Will this solve our performance issues?\n2. **Technical debt**: Does this reduce or increase complexity?\n3. **Risk**: What's the probability and impact of failure?\n4. **Cost**: Total cost of ownership (effort + infrastructure)\n5. **Timeline**: Can we complete in Q4?\n6. **Reversibility**: Can we roll back if needed?\n\n**Decision makers**:\n- Primary: David Kim (CTO)\n- Consulted: Sarah Chen (Engineering), Bob Martinez (DevOps)\n- Informed: Product, Executive team\n\n## Decision\n\n*[To be filled during meeting]*\n\n**Selected Option**: ___________________\n\n**Rationale**:\n\n**Timeline**:\n\n**Owner**: ___________________\n\n**Success Criteria**:\n\n## Action Items\n\n*[To be filled during meeting]*\n\n- [ ] [Action item] - @[Owner] - Due: [Date]\n- [ ] [Action item] - @[Owner] - Due: [Date]\n\n## Next Steps\n\n**Immediate**:\n- Document decision rationale\n- Create detailed implementation plan\n- Schedule kickoff meeting\n\n**Follow-up meeting**: [Date] to review migration plan\n\n## Related Resources\n\n### Primary Sources\n- <mention-page url=\"...\">Database Migration Proposal</mention-page>\n- <mention-page url=\"...\">Current Database Performance Issues</mention-page>\n- <mention-page url=\"...\">Migration Options Analysis</mention-page>\n- <mention-page url=\"...\">Previous Migration Post-mortem</mention-page>\n\n### Supporting Documents\n- MongoDB performance metrics dashboard\n- PostgreSQL evaluation results\n- Cost analysis spreadsheet\n```\n\n### Step 5: Share Agenda\n\n```\nNotion:notion-create-comment\nparent: { page_id: \"agenda-page-id\" }\nrich_text: [{\n  type: \"text\",\n  text: { content: \"📋 Meeting agenda ready! Please review the pre-read before tomorrow's meeting. cc: @Sarah @David @Alice @Bob @Carol\" }\n}]\n```\n\n## Summary Provided to User\n\n```markdown\n## Meeting Prep Complete ✅\n\n### Meeting Details\n- **Topic**: Database Migration Strategy Decision\n- **Date**: October 15, 2025 at 2:00 PM\n- **Duration**: 90 minutes\n- **Attendees**: 5 (Engineering leads + CTO + Product)\n\n### Agenda Created\n<mention-page url=\"...\">Architecture Decision: Database Migration Strategy</mention-page>\n\n### Pre-Read Summary\n\n**Problem**: MongoDB performance degrading (50ms → 400ms queries), schema flexibility causing issues\n\n**Decision needed**: Choose between:\n1. Stay on MongoDB (optimizations) - 2 weeks, low risk\n2. Migrate to PostgreSQL - 8 weeks, high value, high risk ✅ Recommended\n3. Hybrid approach - 4 weeks, medium complexity\n4. Do nothing - Not recommended\n\n**Recommendation**: Engineering team favors PostgreSQL migration\n\n### Key Information Included\n\n✅ **Background context** from 4 relevant pages\n✅ **Options analysis** with pros/cons for each\n✅ **Cost-benefit** breakdown\n✅ **Risk assessment** for each option\n✅ **Discussion topics** (16 questions organized by theme)\n✅ **Decision framework** with evaluation criteria\n✅ **Historical context** from previous migration\n✅ **Empty sections** for decision and action items\n\n### Discussion Topics Prepared\n\n**Technical** (4 questions):\n- Downtime achievability\n- Rollback planning\n- Data migration approach\n- Schema design\n\n**Business** (4 questions):\n- Customer impact\n- Scheduling\n- Roadmap impact\n- Cost analysis\n\n**Risk** (4 questions):\n- Risk identification\n- Testing strategy\n- Rollback procedure\n- Expertise availability\n\n**Resources** (4 questions):\n- Team allocation\n- External help needs\n- Project trade-offs\n- Timeline realism\n\n### Success Factors\n\n1. **Comprehensive research**: 4 relevant pages analyzed\n2. **Clear options**: Pros/cons for each approach\n3. **Structured discussion**: 16 questions organized by theme\n4. **Decision framework**: Clear criteria for evaluation\n5. **Historical context**: Lessons from previous migration\n6. **Ready for outcomes**: Sections prepared for decision and actions\n\n**Meeting is well-prepared for productive decision-making! 📊**\n```\n\n## Key Features Demonstrated\n\n### Context Gathering\n- **Targeted search**: Engineering teamspace, recent documents\n- **Multiple perspectives**: Performance data, proposals, analysis, historical lessons\n- **Comprehensive synthesis**: Combined technical, business, and historical context\n\n### Options Analysis\n- **Structured comparison**: Pros/cons for each option\n- **Cost-benefit**: Effort and infrastructure costs included\n- **Risk assessment**: Probability and impact noted\n- **Recommendation**: Clear engineering preference stated\n\n### Decision Support\n- **Discussion topics**: 16 questions organized by theme\n- **Decision framework**: Evaluation criteria defined\n- **Decision makers**: Roles and responsibilities clear\n- **Outcome capture**: Sections ready for decision and actions\n\n### Meeting Structure\n- **Pre-read**: Comprehensive background (can be read in 10 minutes)\n- **Options**: Clear comparison for quick decision\n- **Discussion**: Structured topics prevent rambling\n- **Capture**: Templates for decision and actions\n\nPerfect for: Architecture decisions, technical trade-offs, strategic choices\n\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/examples/sprint-planning.md",
    "content": "# Example: Sprint Planning Meeting Prep\n\n**User Request**: \"Prepare for tomorrow's sprint planning meeting\"\n\n## Workflow\n\n### 1. Search for Context\n```\nNotion:notion-search\nquery: \"sprint planning\" + \"product backlog\"\nteamspace_id: \"engineering-team\"\n```\n\nFound:\n- Last sprint retrospective\n- Product backlog (prioritized)\n- Current sprint progress\n- Team capacity notes\n\n### 2. Fetch Details\n```\nNotion:notion-fetch (4 pages)\n```\n\n**Key context**:\n- **Last Sprint**: Completed 32/35 story points (91%)\n- **Velocity**: Consistent 30-35 points over last 3 sprints\n- **Team**: 5 engineers, 1 on vacation next sprint (80% capacity)\n- **Top Backlog Items**: User auth improvements, API performance, mobile responsive fixes\n\n### 3. Query Current Sprint Tasks\n```\nNotion:notion-query-data-sources\nquery: \"SELECT * FROM tasks WHERE Sprint = 'Sprint 24' AND Status != 'Done'\"\n```\n\n3 tasks carrying over (technical debt items)\n\n### 4. Create Pre-Read (Internal)\n```\nNotion:notion-create-pages\ntitle: \"Sprint 25 Planning - Pre-Read (Internal)\"\n```\n\n**Pre-read included**:\n- Sprint 24 summary (velocity, what carried over)\n- Team capacity for Sprint 25\n- Top backlog candidates with story points\n- Technical dependencies\n- Risk items (auth changes need QA time)\n\n### 5. Create Agenda\n```\nNotion:notion-create-pages  \ntitle: \"Sprint 25 Planning - Agenda\"\n```\n\n**Agenda**:\n- Review Sprint 24 completion (5 min)\n- Discuss carryover items (5 min)\n- Review capacity (28 points available)\n- Select backlog items (30 min)\n- Identify dependencies & risks (10 min)\n- Confirm commitments (10 min)\n\n### 6. Link Documents\nCross-linked pre-read and agenda, referenced last retro and backlog.\n\n## Output Summary\n\n**Internal Pre-Read**: Team context, capacity, blockers\n**External Agenda**: Meeting structure, discussion topics\n**Both saved to Notion** and linked to project pages\n\n## Key Success Factors\n- Gathered sprint history for velocity trends\n- Calculated realistic capacity (account for PTO)\n- Identified carryover items upfront\n- Pre-read gave team context before meeting\n- Agenda kept meeting focused and timeboxed\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/reference/brainstorming-template.md",
    "content": "# Brainstorming Meeting Template\n\nUse this template for creative ideation and brainstorming sessions.\n\n```markdown\n# [Topic] Brainstorming - [Date]\n\n## Meeting Details\n**Date**: [Date]\n**Facilitator**: [Name]\n**Note-taker**: [Name]\n**Attendees**: [List]\n\n## Objective\n\n[Clear statement of what we're brainstorming]\n\n**Success looks like**: [How we'll know brainstorming was successful]\n\n## Background & Context\n\n[Context from research - 2-3 paragraphs]\n\n**Related Pages**:\n- <mention-page url=\"...\">Context Page 1</mention-page>\n- <mention-page url=\"...\">Context Page 2</mention-page>\n\n## Constraints\n\n- [Constraint]\n- [Constraint]\n- [Constraint]\n\n## Seed Ideas\n\n[Starting ideas from research to spark discussion]:\n\n1. **[Idea]**: [Brief description]\n2. **[Idea]**: [Brief description]\n\n## Ground Rules\n\n- No criticism during ideation\n- Build on others' ideas\n- Quantity over quality initially\n- Wild ideas welcome\n\n## Brainstorming Notes\n\n### Ideas Generated\n\n[To be filled during meeting]\n\n1. [Idea with brief description]\n2. [Idea with brief description]\n\n### Themes/Patterns\n\n[Groupings that emerge]\n\n## Evaluation\n\n[If time permits, evaluate top ideas]\n\n### Top Ideas\n\n| Idea | Feasibility | Impact | Effort | Score |\n|------|-------------|---------|--------|-------|\n| [Idea] | [H/M/L] | [H/M/L] | [H/M/L] | [#] |\n\n## Next Steps\n\n- [ ] [Action to explore idea]\n- [ ] [Action to prototype]\n- [ ] [Action to research]\n\n## Follow-up\n\n**Next meeting**: [Date to reconvene]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/reference/decision-meeting-template.md",
    "content": "# Decision Meeting Template\n\nUse this template when you need to make an important decision with your team.\n\n```markdown\n# [Decision Topic] - [Date]\n\n## Meeting Details\n**Date & Time**: [Date and time]\n**Duration**: [Length]\n**Attendees**: [List of attendees with roles]\n**Location**: [Physical location or video link]\n**Facilitator**: [Name]\n\n## Pre-Read Summary\n\n### Background\n[2-3 sentences providing context from related project pages]\n\n**Related Pages**:\n- <mention-page url=\"...\">Project Overview</mention-page>\n- <mention-page url=\"...\">Previous Discussion</mention-page>\n\n### Current Situation\n[What brings us to this decision point]\n\n## Decision Required\n\n**Question**: [Clear statement of decision needed]\n\n**Timeline**: [When decision needs to be made]\n\n**Impact**: [Who/what is affected by this decision]\n\n## Options Analysis\n\n### Option A: [Name]\n**Description**: [What this option entails]\n\n**Pros**:\n- [Advantage]\n- [Advantage]\n\n**Cons**:\n- [Disadvantage]\n- [Disadvantage]\n\n**Cost/Effort**: [Estimate]\n**Risk**: [Risk assessment]\n\n### Option B: [Name]\n[Repeat structure]\n\n### Option C: Do Nothing\n**Description**: What happens if we don't decide\n**Implications**: [Consequences]\n\n## Recommendation\n\n[If there is a recommended option, state it with rationale]\n\n## Discussion Topics\n\n1. [Topic to discuss]\n2. [Clarification needed on]\n3. [Trade-offs to consider]\n\n## Decision Framework\n\n**Criteria for evaluation**:\n- [Criterion 1]\n- [Criterion 2]\n- [Criterion 3]\n\n## Decision\n\n[To be filled during meeting]\n\n**Selected Option**: [Option chosen]\n**Rationale**: [Why]\n**Owner**: [Who will implement]\n**Timeline**: [When]\n\n## Action Items\n\n- [ ] [Action] - @[Owner] - Due: [Date]\n- [ ] [Action] - @[Owner] - Due: [Date]\n\n## Follow-up\n\n**Next review**: [Date]\n**Success metrics**: [How we'll know this worked]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/reference/one-on-one-template.md",
    "content": "# 1:1 Meeting Template\n\nUse this template for manager/report one-on-one meetings.\n\n```markdown\n# 1:1: [Manager] & [Report] - [Date]\n\n## Meeting Details\n**Date**: [Date]\n**Last meeting**: <mention-page url=\"...\">Previous 1:1</mention-page>\n\n## Agenda\n\n### [Report]'s Topics\n1. [Topic to discuss]\n2. [Question or concern]\n\n### [Manager]'s Topics\n1. [Topic to cover]\n2. [Feedback or update]\n\n## Discussion Notes\n\n### [Topic 1]\n[Discussion points]\n\n**Action items**:\n- [ ] [Action] - @[Owner]\n\n### [Topic 2]\n[Discussion points]\n\n## Career Development\n\n**Current focus**: [Development goal]\n**Progress**: [Update on progress]\n\n## Feedback\n\n**What's going well**:\n- [Positive feedback]\n\n**Areas for growth**:\n- [Developmental feedback]\n\n## Action Items\n\n- [ ] [Action] - @[Report] - Due: [Date]\n- [ ] [Action] - @[Manager] - Due: [Date]\n\n## Next Meeting\n\n**Date**: [Date]\n**Topics to cover**:\n- [Carry-over topic]\n- [Upcoming topic]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/reference/retrospective-template.md",
    "content": "# Retrospective Template\n\nUse this template for sprint retrospectives and team retrospectives.\n\n```markdown\n# Sprint [#] Retrospective - [Date]\n\n## Meeting Details\n**Date**: [Date]\n**Team**: [Team]\n**Sprint**: [Sprint dates]\n**Facilitator**: [Name]\n\n## Sprint Summary\n\n**Sprint Goal**: [Goal]\n**Goal Met**: Yes / Partially / No\n\n**Completed**: [#] points\n**Velocity**: [#] points\n**Planned**: [#] points\n\n## Pre-Read\n\n**Sprint Metrics**:\n- Tasks completed: [#]\n- Tasks carried over: [#]\n- Bugs found: [#]\n- Blockers encountered: [#]\n\n## Discussion\n\n### What Went Well (Keep)\n\n[Team input during meeting]\n\n### What Didn't Go Well (Stop)\n\n[Team input during meeting]\n\n### What To Try (Start)\n\n[Team input during meeting]\n\n### Shout-outs\n\n[Team recognition]\n\n## Action Items\n\n- [ ] [Improvement to implement] - @[Owner] - Due: [Date]\n- [ ] [Process change] - @[Owner] - Due: [Date]\n\n## Follow-up\n\n**Review actions in**: [Next retro date]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/reference/sprint-planning-template.md",
    "content": "# Sprint Planning Template\n\nUse this template for agile sprint planning meetings.\n\n```markdown\n# Sprint [#] Planning - [Date]\n\n## Meeting Details\n**Date**: [Date]\n**Team**: [Team name]\n**Sprint Duration**: [Dates]\n\n## Sprint Goal\n\n[Clear statement of what this sprint aims to accomplish]\n\n## Capacity\n\n| Team Member | Availability | Capacity (points) |\n|-------------|--------------|-------------------|\n| [Name] | [%] | [#] |\n| **Total** | | [#] |\n\n## Backlog Review\n\n### High Priority Items\n\n[From product backlog, linked from task database]\n\n- <mention-page url=\"...\">Task 1</mention-page> - [Points]\n- <mention-page url=\"...\">Task 2</mention-page> - [Points]\n\n## Sprint Backlog\n\n### Committed Items\n\n- [x] <mention-page url=\"...\">Task</mention-page> - [Points] - @[Owner]\n- [ ] <mention-page url=\"...\">Task</mention-page> - [Points] - @[Owner]\n\n**Total committed**: [Points]\n\n### Stretch Goals\n\n- [ ] <mention-page url=\"...\">Task</mention-page> - [Points]\n\n## Dependencies & Risks\n\n**Dependencies**:\n- [Dependency]\n\n**Risks**:\n- [Risk]\n\n## Definition of Done\n\n- [ ] Code complete and reviewed\n- [ ] Tests written and passing\n- [ ] Documentation updated\n- [ ] Deployed to staging\n- [ ] QA approved\n\n## Next Steps\n\n- Team begins sprint work\n- Daily standups at [Time]\n- Sprint review on [Date]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/reference/status-update-template.md",
    "content": "# Status Update Meeting Template\n\nUse this template for regular project status updates and check-ins.\n\n```markdown\n# [Project Name] Status Update - [Date]\n\n## Meeting Details\n**Date**: [Date and time]\n**Attendees**: [List]\n**Project**: <mention-page url=\"...\">Project Page</mention-page>\n\n## Executive Summary\n\n**Status**: 🟢 On Track / 🟡 At Risk / 🔴 Behind\n\n**Progress**: [Percentage] complete\n**Timeline**: [Status vs original plan]\n\n## Progress Since Last Meeting\n\n### Completed\n- [Accomplishment with specifics]\n- [Accomplishment with specifics]\n\n### In Progress\n- [Work item and status]\n- [Work item and status]\n\n## Metrics\n\n| Metric | Current | Target | Status |\n|--------|---------|--------|--------|\n| [Metric] | [Value] | [Value] | [Icon] |\n| [Metric] | [Value] | [Value] | [Icon] |\n\n## Upcoming Work\n\n**Next 2 Weeks**:\n- [Planned work]\n- [Planned work]\n\n**Next Month**:\n- [Milestone or major work]\n\n## Blockers & Risks\n\n### Active Blockers\n- **[Blocker]**: [Description and impact]\n  - Action: [What's being done]\n\n### Risks\n- **[Risk]**: [Description]\n  - Mitigation: [Strategy]\n\n## Discussion Topics\n\n1. [Topic requiring input]\n2. [Topic for alignment]\n\n## Decisions Needed\n\n- [Decision] or None\n\n## Action Items\n\n- [ ] [Action] - @[Owner] - Due: [Date]\n\n## Next Meeting\n\n**Date**: [Date]\n**Focus**: [What next meeting will cover]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-meeting-intelligence/reference/template-selection-guide.md",
    "content": "# Meeting Template Selection Guide\n\nChoose the right template for your meeting type.\n\n## Template Overview\n\n| Meeting Type | Use This Template | When to Use |\n|--------------|-------------------|-------------|\n| Make a decision | [Decision Meeting](decision-meeting-template.md) | Need to evaluate options and reach a decision |\n| Project update | [Status Update](status-update-template.md) | Regular check-ins, progress reviews |\n| Generate ideas | [Brainstorming](brainstorming-template.md) | Creative ideation, problem-solving |\n| Sprint planning | [Sprint Planning](sprint-planning-template.md) | Planning agile sprint work |\n| Sprint retro | [Retrospective](retrospective-template.md) | Reflecting on completed work |\n| Manager/report | [1:1 Meeting](one-on-one-template.md) | Regular one-on-one check-ins |\n| Weekly team sync | [Status Update](status-update-template.md) (simplified) | Routine team synchronization |\n\n## Quick Decision Tree\n\n```\nWhat's the primary purpose?\n\n├─ Make a decision\n│  └─ Use: Decision Meeting Template\n│\n├─ Update on progress\n│  └─ Use: Status Update Template\n│\n├─ Generate ideas\n│  └─ Use: Brainstorming Template\n│\n├─ Plan sprint work\n│  └─ Use: Sprint Planning Template\n│\n├─ Reflect on past work\n│  └─ Use: Retrospective Template\n│\n└─ Manager/report check-in\n   └─ Use: 1:1 Meeting Template\n```\n\n## Template Customization\n\nAll templates can be customized:\n- **Simplify** for shorter meetings\n- **Add sections** for specific needs\n- **Combine elements** from multiple templates\n- **Adapt language** for your team culture\n\n## Best Practices\n\n1. **Choose template first**: Select before gathering context\n2. **Gather Notion content**: Search and fetch relevant pages\n3. **Enrich with research**: Add Codex insights where valuable\n4. **Customize as needed**: Adapt template to specific situation\n5. **Share early**: Give attendees time to review\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/LICENSE.txt",
    "content": "Copyright 2025 Notion Labs, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/SKILL.md",
    "content": "---\nname: notion-research-documentation\ndescription: Research across Notion and synthesize into structured documentation; use when gathering info from multiple Notion sources to produce briefs, comparisons, or reports with citations.\nmetadata:\n  short-description: Research Notion content and produce briefs/reports\n---\n\n# Research & Documentation\n\nPull relevant Notion pages, synthesize findings, and publish clear briefs or reports (with citations and links to sources).\n\n## Quick start\n1) Find sources with `Notion:notion-search` using targeted queries; confirm scope with the user.\n2) Fetch pages via `Notion:notion-fetch`; note key sections and capture citations (`reference/citations.md`).\n3) Choose output format (brief, summary, comparison, comprehensive report) using `reference/format-selection-guide.md`.\n4) Draft in Notion with `Notion:notion-create-pages` using the matching template (quick, summary, comparison, comprehensive).\n5) Link sources and add a references/citations section; update as new info arrives with `Notion:notion-update-page`.\n\n## Workflow\n### 0) If any MCP call fails because Notion MCP is not connected, pause and set it up:\n1. Add the Notion MCP:\n   - `codex mcp add notion --url https://mcp.notion.com/mcp`\n2. Enable remote MCP client:\n   - Set `[features].rmcp_client = true` in `config.toml` **or** run `codex --enable rmcp_client`\n3. Log in with OAuth:\n   - `codex mcp login notion`\n\nAfter successful login, the user will have to restart codex. You should finish your answer and tell them so when they try again they can continue with Step 1.\n\n### 1) Gather sources\n- Search first (`Notion:notion-search`); refine queries, and ask the user to confirm if multiple results appear.\n- Fetch relevant pages (`Notion:notion-fetch`), skim for facts, metrics, claims, constraints, and dates.\n- Track each source URL/ID for later citation; prefer direct quotes for critical facts.\n\n### 2) Select the format\n- Quick readout → quick brief.\n- Single-topic dive → research summary.\n- Option tradeoffs → comparison.\n- Deep dive / exec-ready → comprehensive report.\n- See `reference/format-selection-guide.md` for when to pick each.\n\n### 3) Synthesize\n- Outline before writing; group findings by themes/questions.\n- Note evidence with source IDs; flag gaps or contradictions.\n- Keep user goal in view (decision, summary, plan, recommendation).\n\n### 4) Create the doc\n- Pick the matching template in `reference/` (brief, summary, comparison, comprehensive) and adapt it.\n- Create the page with `Notion:notion-create-pages`; include title, summary, key findings, supporting evidence, and recommendations/next steps when relevant.\n- Add citations inline and a references section; link back to source pages.\n\n### 5) Finalize & handoff\n- Add highlights, risks, and open questions.\n- If the user needs follow-ups, create tasks or a checklist in the page; link any task database entries if applicable.\n- Share a short changelog or status using `Notion:notion-update-page` when updating.\n\n## References and examples\n- `reference/` — search tactics, format selection, templates, and citation rules (e.g., `advanced-search.md`, `format-selection-guide.md`, `research-summary-template.md`, `comparison-template.md`, `citations.md`).\n- `examples/` — end-to-end walkthroughs (e.g., `competitor-analysis.md`, `technical-investigation.md`, `market-research.md`, `trip-planning.md`).\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Notion Research & Documentation\"\n  short_description: \"Research Notion content and produce briefs/reports\"\n  icon_small: \"./assets/notion-small.svg\"\n  icon_large: \"./assets/notion.png\"\n  default_prompt: \"Research this topic in Notion and produce a sourced brief with clear recommendations.\"\n\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"notion\"\n      description: \"Notion MCP server\"\n      transport: \"streamable_http\"\n      url: \"https://mcp.notion.com/mcp\"\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/evaluations/README.md",
    "content": "# Research & Documentation Skill Evaluations\n\nEvaluation scenarios for testing the Research & Documentation skill across different Codex models.\n\n## Purpose\n\nThese evaluations ensure the Research & Documentation skill:\n- Searches across Notion workspace effectively\n- Synthesizes information from multiple sources\n- Selects appropriate research report format\n- Creates comprehensive documentation with proper citations\n- Works consistently across Haiku, Sonnet, and Opus\n\n## Evaluation Files\n\n### basic-research.json\nTests basic research workflow with synthesis across multiple Notion pages.\n\n**Scenario**: Research Q4 product roadmap and create summary  \n**Key Behaviors**:\n- Searches Notion for roadmap-related pages\n- Fetches multiple relevant pages (roadmap, product docs, meeting notes)\n- Synthesizes information from different sources\n- Selects appropriate format (Research Summary)\n- Includes citations linking back to source pages\n- Creates structured document with clear sections\n\n### research-to-database.json\nTests creating research documentation in a Notion database with properties.\n\n**Scenario**: Research competitor landscape and save to Research database  \n**Key Behaviors**:\n- Searches for existing competitive intelligence in Notion\n- Identifies Research database as target\n- Fetches database schema to understand properties\n- Creates page with correct property values (Research Type, Status, Date, etc.)\n- Structures content with comparison format\n- Includes source citations for both Notion pages and external research\n\n## Running Evaluations\n\n1. Enable the `research-documentation` skill\n2. Submit the query from the evaluation file\n3. Verify the skill searches Notion workspace comprehensively\n4. Check that multiple source pages are fetched and synthesized\n5. Verify appropriate format is selected (Research Summary, Comprehensive Report, Quick Brief, Comparison)\n6. Confirm citations link back to sources\n7. Test with Haiku, Sonnet, and Opus\n\n## Expected Skill Behaviors\n\nResearch & Documentation evaluations should verify:\n\n### Notion Search & Synthesis\n- Searches workspace with relevant queries\n- Fetches multiple source pages (3-5+)\n- Synthesizes information across sources\n- Identifies patterns and insights\n- Handles conflicting information appropriately\n\n### Format Selection\n- Chooses correct format based on scope and depth:\n  - **Research Summary**: Quick overview with key findings\n  - **Comprehensive Report**: Deep analysis with multiple sections\n  - **Quick Brief**: Fast facts and takeaways\n  - **Comparison**: Side-by-side analysis\n- Applies format structure consistently\n- Uses appropriate sections and headings\n\n### Citation & Attribution\n- Includes citations for all Notion sources\n- Uses mention-page tags: `<mention-page url=\"...\">`\n- Attributes findings to specific sources\n- Distinguishes between Notion content and Codex research\n- Links related documents\n\n### Document Quality\n- Title clearly indicates research topic and date\n- Executive summary or key findings upfront\n- Organized with clear hierarchy\n- Actionable insights and recommendations\n- Appropriate depth for the query\n\n## Creating New Evaluations\n\nWhen adding Research & Documentation evaluations:\n\n1. **Test different research types** - Product research, competitive analysis, technical investigation, market research\n2. **Vary source count** - Synthesis of 2-3 pages vs. 10+ pages\n3. **Test format selection** - Does it choose the right format for the scope?\n4. **Include database targets** - Not just standalone pages\n5. **Test citation accuracy** - Are all sources properly attributed?\n6. **Cross-workspace search** - Testing search across teamspaces if applicable\n\n## Example Success Criteria\n\n**Good** (specific, testable):\n- \"Searches Notion for 'roadmap' and 'Q4' and 'product'\"\n- \"Fetches at least 3 different source pages\"\n- \"Includes citation for each key finding using mention-page tags\"\n- \"Creates page with title format 'Research: [Topic] - [Date]'\"\n- \"Uses Research Summary format with sections: Executive Summary → Key Findings → Details → Recommendations → Sources\"\n\n**Bad** (vague, untestable):\n- \"Searches Notion effectively\"\n- \"Creates comprehensive research\"\n- \"Uses sources appropriately\"\n- \"Good documentation\"\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/evaluations/basic-research.json",
    "content": "{\n  \"name\": \"Basic Research and Documentation\",\n  \"skills\": [\"research-documentation\"],\n  \"query\": \"Research our API authentication approach and create a summary document in Notion\",\n  \"expected_behavior\": [\n    \"Searches Notion workspace for authentication-related pages using Notion:notion-search\",\n    \"Uses appropriate search terms like 'API authentication', 'auth', 'API security'\",\n    \"Applies filters if relevant (e.g., created_date_range, creator filters)\",\n    \"Fetches at least 2-3 relevant pages using Notion:notion-fetch to get detailed content\",\n    \"Analyzes the fetched content to extract key information about authentication approach\",\n    \"Creates a structured research summary document using Research Summary format (see reference/formats.md)\",\n    \"Includes sections: Executive Summary, Key Findings, Detailed Analysis, Recommendations, Sources\",\n    \"Cites source pages using <mention-page> tags for proper linking\",\n    \"Uses Notion:notion-create-pages to save the document to Notion\",\n    \"Applies Notion-flavored markdown with headings, bullets, and clear structure\",\n    \"Places document appropriately (asks user or uses project/research area)\"\n  ],\n  \"success_criteria\": [\n    \"Document contains synthesized information from multiple searched pages\",\n    \"At least 2-3 source pages are cited with mention-page tags\",\n    \"Document follows Research Summary format structure from reference/formats.md\",\n    \"Title is descriptive with topic and date (e.g., 'API Authentication Research - Oct 2025')\",\n    \"Content is concise but comprehensive with clear findings\",\n    \"Uses Notion markdown correctly (headings, lists, mentions)\",\n    \"Document is placed in appropriate location or user is consulted\"\n  ]\n}\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/evaluations/research-to-database.json",
    "content": "{\n  \"name\": \"Research with Database Integration\",\n  \"skills\": [\"research-documentation\"],\n  \"query\": \"Research competitor pricing strategies and add to our Research database\",\n  \"expected_behavior\": [\n    \"Searches for competitor and pricing information using Notion:notion-search\",\n    \"Applies appropriate search strategy (see reference/advanced-search.md patterns)\",\n    \"Fetches relevant pages using Notion:notion-fetch and synthesizes findings\",\n    \"Recognizes need to add to database (mentioned in query)\",\n    \"Searches for or asks about the Research database location\",\n    \"Fetches database using Notion:notion-fetch to get schema, data sources, and properties\",\n    \"Identifies correct data source from <data-source> tags if multiple exist\",\n    \"Creates page with appropriate database properties (Type: Competitor Analysis, Category, Tags, Date, Status, etc.)\",\n    \"Uses parent: { data_source_id: 'collection://...' } for correct database placement\",\n    \"Includes research content using Competitor Analysis format (see reference/formats.md)\",\n    \"Sets all required properties from schema with correct values\",\n    \"Cites sources using mention-page tags per reference/citations.md\"\n  ],\n  \"success_criteria\": [\n    \"Page is created in correct database using data_source_id as parent\",\n    \"All required database properties are set correctly\",\n    \"Property values match available options from fetched schema\",\n    \"Content follows Competitor Analysis format structure\",\n    \"Sources are cited with proper mention-page tags\",\n    \"Title is descriptive (e.g., 'Competitor Pricing Analysis')\",\n    \"Research synthesizes findings rather than just listing sources\"\n  ]\n}\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/examples/competitor-analysis.md",
    "content": "# Example: Competitor Analysis\n\n## User Request\n\n> \"Research competitor pricing models and create a comparison document\"\n\n## Workflow Execution\n\n### Step 1: Search for Competitor Information\n\n```\nNotion:notion-search\nquery: \"competitor pricing\"\nquery_type: \"internal\"\nfilters: {\n  created_date_range: {\n    start_date: \"2024-01-01\"\n  }\n}\n```\n\n**Results Found:**\n- \"Competitor Analysis - Stripe\" (Product)\n- \"Market Research Q3 2024\" (Strategy)\n- \"Pricing Strategy Discussion\" (Product)\n- \"Competitor Feature Matrix\" (Product)\n\n### Step 2: Fetch and Analyze\n\n```\nNotion:notion-fetch for each page\n```\n\n**Extracted Information:**\n\n**Stripe**:\n- Pay-as-you-go: 2.9% + $0.30 per transaction\n- No monthly fees\n- Volume discounts available\n\n**Square**:\n- Pay-as-you-go: 2.6% + $0.10 per transaction\n- Monthly plan: $29/month with lower rates\n- Hardware costs separate\n\n**PayPal**:\n- Standard: 3.49% + $0.49 per transaction\n- Advanced: $30/month + 2.59% + $0.49\n- Multi-currency fees\n\n**Adyen**:\n- Interchange++ pricing model\n- Custom enterprise pricing\n- Setup and monthly fees apply\n\n### Step 3: Create Comparison Document\n\n```\nNotion:notion-create-pages\n\npages: [{\n  properties: {\n    \"title\": \"Competitor Pricing Analysis - Payment Processors\"\n  },\n  content: \"[Comparison document using template]\"\n}]\n```\n\n## Output Document\n\n```markdown\n# Competitor Pricing Analysis - Payment Processors\n\n**Date**: October 14, 2025  \n**Research Team**: Product Strategy\n\n## Executive Summary\n\nWe analyzed four major payment processor competitors: Stripe, Square, PayPal, and Adyen. Pricing models vary from simple pay-as-you-go (Stripe) to complex interchange-plus (Adyen). Key insight: All competitors offer volume discounts for high-transaction merchants, with breakpoints typically at $100K/month processing volume.\n\n## Comparison Matrix\n\n| Feature | Stripe | Square | PayPal | Adyen |\n|---------|--------|--------|--------|-------|\n| **Base Rate** | 2.9% + $0.30 | 2.6% + $0.10 | 3.49% + $0.49 | Interchange++ |\n| **Monthly Fee** | $0 | $0-29 | $0-30 | Custom |\n| **Volume Discounts** | Yes, >$80K | Yes, >$250K | Yes, >$100K | Yes, custom |\n| **Setup Fee** | $0 | $0 | $0 | $1,000-5,000 |\n| **Multi-currency** | 1% extra | 3% extra | 3-4% extra | Included |\n| **Chargeback Fee** | $15 | $15-25 | $20 | Custom |\n| **Target Market** | Startups-Enterprise | Small-Medium | Small-Medium | Enterprise |\n\n## Detailed Analysis\n\n### Stripe\n\n**Pricing Structure**:\n- **Standard**: 2.9% + $0.30 per successful card charge\n- **Volume discounts**: Available for businesses processing >$80,000/month\n- **International cards**: +1% fee\n- **Currency conversion**: 1% above market rate\n\n**Strengths**:\n- Simple, transparent pricing\n- No setup fees or monthly minimums\n- Excellent developer experience\n- Quick onboarding\n\n**Weaknesses**:\n- Higher per-transaction fee for high volume\n- Volume discounts less aggressive than Adyen\n\n**Best for**: Startups and growth-stage companies needing quick integration\n\n**Source**: <mention-page url=\"...\">Competitor Analysis - Stripe</mention-page>\n\n### Square\n\n**Pricing Structure**:\n- **Pay-as-you-go**: 2.6% + $0.10 per tap, dip, or swipe\n- **Keyed-in**: 3.5% + $0.15\n- **Plus plan**: $29/month for lower rates (2.5% + $0.10)\n- **Premium plan**: Custom pricing\n\n**Strengths**:\n- Lowest per-transaction fee for in-person\n- All-in-one hardware + software\n- No long-term contracts\n\n**Weaknesses**:\n- Higher rates for online/keyed transactions\n- Hardware costs ($49-$299)\n- Less suitable for online-only businesses\n\n**Best for**: Brick-and-mortar retail and restaurants\n\n**Source**: <mention-page url=\"...\">Market Research Q3 2024</mention-page>\n\n### PayPal\n\n**Pricing Structure**:\n- **Standard**: 3.49% + $0.49 per transaction\n- **Advanced**: $30/month + 2.59% + $0.49\n- **Payments Pro**: Additional $30/month for direct credit card processing\n\n**Strengths**:\n- Huge customer base (PayPal checkout)\n- Buyer protection increases trust\n- International reach (200+ countries)\n\n**Weaknesses**:\n- Highest per-transaction fees\n- Complex fee structure\n- Account holds and reserves common\n\n**Best for**: Businesses where PayPal brand trust matters (e-commerce, marketplaces)\n\n**Source**: <mention-page url=\"...\">Pricing Strategy Discussion</mention-page>\n\n### Adyen\n\n**Pricing Structure**:\n- **Interchange++**: Actual interchange + scheme fees + fixed markup\n- **Setup fee**: $1,000-5,000 (negotiable)\n- **Monthly minimum**: Typically $10,000+ processing volume\n- **Per-transaction**: Interchange + 0.6% + $0.12 (example)\n\n**Strengths**:\n- Most transparent cost structure at scale\n- Lowest effective rate for high volume\n- True multi-currency (100+ currencies)\n- Direct connections to schemes\n\n**Weaknesses**:\n- Complex pricing requires analysis\n- High minimums ($10K+/month)\n- Longer integration time\n- Not suitable for small businesses\n\n**Best for**: Enterprise with $1M+/month processing volume\n\n**Source**: <mention-page url=\"...\">Competitor Feature Matrix</mention-page>\n\n## Pricing Trends & Insights\n\n### Volume-Based Discounting\nAll competitors offer discounts at scale:\n- **Entry point**: $80K-$250K/month processing\n- **Typical discount**: 10-30 basis points reduction\n- **Negotiation leverage**: Begins at $500K/month+\n\n### Hidden Costs to Consider\n\n| Cost Type | Stripe | Square | PayPal | Adyen |\n|-----------|--------|--------|--------|-------|\n| Chargeback | $15 | $15-25 | $20 | $15-25 |\n| Account verification | $0 | $0 | $0 | Varies |\n| PCI compliance | $0 | $0 | $0 | $0 |\n| Currency conversion | 1% | 3% | 3-4% | 0% |\n| Refund fees | Returned | Returned | Not returned | Varies |\n\n### Market Positioning\n\n```\nHigh Volume / Enterprise\n    ↑\n    |                    Adyen\n    |                      \n    |         Stripe             \n    |    \n    |  Square    PayPal\n    |\n    └──────────────────→\n      Small / Simple        Complex / International\n```\n\n## Strategic Implications\n\n### For Startups (<$100K/month)\n**Recommended**: Stripe\n- Lowest friction to start\n- No upfront costs\n- Great documentation\n- Acceptable rates at this scale\n\n### For Growing Companies ($100K-$1M/month)\n**Recommended**: Stripe or Square\n- Negotiate volume discounts\n- Evaluate interchange++ if international\n- Consider Square if in-person dominant\n\n### For Enterprises (>$1M/month)\n**Recommended**: Adyen or Negotiated Stripe\n- Interchange++ models save significantly\n- Direct scheme connections\n- Multi-region capabilities matter\n- ROI on integration complexity\n\n## Recommendations\n\n1. **Immediate**: Benchmark our current 2.8% + $0.25 against Stripe's standard\n2. **Short-term**: Request volume discount quote from Stripe at our current $150K/month\n3. **Long-term**: Evaluate Adyen when we cross $1M/month threshold\n\n## Next Steps\n\n- [ ] Request detailed pricing proposal from Stripe for volume discounts\n- [ ] Create pricing calculator comparing all 4 at different volume levels\n- [ ] Interview customers about payment method preferences\n- [ ] Analyze our transaction mix (domestic vs international, card types)\n\n## Sources\n\n### Primary Research\n- <mention-page url=\"...\">Competitor Analysis - Stripe</mention-page>\n- <mention-page url=\"...\">Market Research Q3 2024</mention-page>\n- <mention-page url=\"...\">Pricing Strategy Discussion</mention-page>\n- <mention-page url=\"...\">Competitor Feature Matrix</mention-page>\n\n### External References\n- Stripe.com pricing page (Oct 2025)\n- Square pricing documentation\n- PayPal merchant fees\n- Adyen pricing transparency report\n```\n\n## Key Success Factors\n\n1. **Structured comparison**: Matrix format for quick scanning\n2. **Multiple dimensions**: Price, features, target market\n3. **Strategic recommendations**: Not just data, but implications\n4. **Visual elements**: Table and positioning diagram\n5. **Actionable next steps**: Clear recommendations\n6. **Comprehensive sources**: Internal research + external validation\n\n## Workflow Pattern Demonstrated\n\n- **Date-filtered search** (recent information only)\n- **Multiple competitor synthesis** (4 different companies)\n- **Comparison template** (matrix + detailed analysis)\n- **Strategic layer** (implications and recommendations)\n- **Action-oriented** (next steps included)\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/examples/market-research.md",
    "content": "# Example: Market Research\n\n**User Request**: \"Research the current state of AI coding assistants market and create a summary document in Notion\"\n\n## Workflow\n\n### 1. Search\n```\nNotion:notion-search\nquery: \"AI coding assistants market\"\n```\nFound 3 relevant pages across Engineering, Strategy, and Product teamspaces.\n\n### 2. Fetch & Analyze\n```\nNotion:notion-fetch (3x)\n```\nExtracted market size, competitive landscape, technology trends from source pages.\n\n### 3. Create Documentation\n```\nNotion:notion-create-pages\n```\n\n## Output (Condensed)\n\n```markdown\n# AI Coding Assistants Market Research - Oct 2025\n\n## Executive Summary\nThe AI coding assistant market is experiencing 150%+ YoY growth. GitHub Copilot dominates with 60% share, but specialized tools are gaining traction in specific niches.\n\n## Key Findings\n\n### Market Size and Growth\n$800M in 2024 → $2.5B projected by 2026. Developer adoption: 23% (2023) → 47% (2024).\nSource: <mention-page url=\"...\">Market Trends Q3 2025</mention-page>\n\n### Competitive Landscape\n- GitHub Copilot: 60% (strong IDE integration)\n- Cursor: 15% (rapid growth, full IDE)\n- Tabnine: 10% (enterprise, on-premise)\n- Cody: 5% (codebase-aware)\n- CodeWhisperer: 8% (AWS integration)\nSource: <mention-page url=\"...\">AI Tools Competitive Analysis</mention-page>\n\n### Technology Trends\nKey differentiators: context awareness, customization, multi-modal interfaces, code verification.\nSource: <mention-page url=\"...\">Developer Tools Landscape</mention-page>\n\n## Next Steps\n1. Monitor Cursor growth and feature releases\n2. Evaluate Cody's codebase-aware capabilities\n3. Document enterprise security/compliance requirements\n4. Track pricing trends\n```\n\n## Key Takeaways\n- Found relevant pages across multiple teamspaces\n- Synthesized competitive, market, and technical perspectives\n- Used proper citations linking to source pages\n- Created actionable recommendations\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/examples/technical-investigation.md",
    "content": "# Example: Technical Investigation\n\n## User Request\n\n> \"Research our current caching strategy and create a technical summary\"\n\n## Workflow Execution\n\n### Step 1: Search for Caching Information\n\n```\nNotion:notion-search\nquery: \"caching strategy architecture\"\nquery_type: \"internal\"\nteamspace_id: \"engineering-teamspace-id\"\n```\n\n**Results Found:**\n- \"System Architecture Overview\" (Engineering)\n- \"Redis Implementation Guide\" (Backend Docs)\n- \"Performance Optimization - Q3 2024\" (Engineering)\n- \"API Caching Decision Record\" (Architecture)\n\n### Step 2: Fetch Relevant Pages\n\n```\nNotion:notion-fetch\nid: \"system-architecture-page-url\"\n```\n\nExtracted: Current caching architecture uses Redis for API responses, Memcached for session storage.\n\n```\nNotion:notion-fetch\nid: \"redis-implementation-guide-url\"\n```\n\nExtracted: Implementation details, TTL settings, invalidation strategies.\n\n```\nNotion:notion-fetch\nid: \"api-caching-decision-record-url\"\n```\n\nExtracted: Why Redis was chosen over alternatives, trade-offs considered.\n\n### Step 3: Synthesize Findings\n\n**Key findings identified:**\n- Two-tier caching: Redis (API responses) + Memcached (sessions)\n- TTL strategy: 5 minutes for dynamic data, 1 hour for static\n- Invalidation: Event-driven for critical updates\n- Performance impact: 75% reduction in database load\n- Known issues: Cache stampede on popular endpoints\n\n### Step 4: Create Technical Summary\n\n```\nNotion:notion-create-pages\n\nparent: { page_id: \"engineering-docs-parent-id\" }\n\npages: [{\n  properties: {\n    \"title\": \"Technical Summary: Caching Strategy - Oct 2025\"\n  },\n  content: \"[Structured technical summary using template]\"\n}]\n```\n\n## Output Document\n\n```markdown\n# Technical Summary: Caching Strategy - Oct 2025\n\n## Executive Summary\n\nOur caching infrastructure uses a two-tier approach with Redis for API response caching and Memcached for session management. This strategy has reduced database load by 75% and improved API response times from 200ms to 50ms average.\n\n## Architecture Overview\n\n### Layer 1: API Response Caching (Redis)\n**Technology**: Redis 7.0 cluster (3 nodes)\n**Purpose**: Cache GET endpoint responses\n**TTL Strategy**:\n- Dynamic content: 5 minutes\n- Static content: 1 hour\n- User-specific: 15 minutes\n\n**Source**: <mention-page url=\"...\">System Architecture Overview</mention-page>\n\n### Layer 2: Session Storage (Memcached)\n**Technology**: Memcached 1.6\n**Purpose**: User session data, temporary state\n**TTL**: 24 hours (session lifetime)\n\n## Implementation Details\n\n### Cache Key Format\n```\napi:v1:{endpoint}:{params_hash}\nsession:{user_id}:{session_id}\n```\n\n### Invalidation Strategy\n- **Event-driven**: Critical data changes trigger immediate invalidation\n- **Time-based**: TTL expiration for non-critical data\n- **Manual**: Admin tools for emergency cache clear\n\n**Source**: <mention-page url=\"...\">Redis Implementation Guide</mention-page>\n\n## Decision Rationale\n\n### Why Redis for API Caching?\n\n**Pros**:\n- Advanced data structures (sorted sets, hashes)\n- Built-in TTL with automatic eviction\n- Pub/sub for cache invalidation events\n- Persistence options for durability\n\n**Cons**:\n- Higher memory usage than Memcached\n- More complex cluster management\n\n**Decision**: Chosen for flexibility and rich feature set needed for API caching.\n\n**Source**: <mention-page url=\"...\">API Caching Decision Record</mention-page>\n\n### Why Memcached for Sessions?\n\n**Pros**:\n- Simpler, lighter weight\n- Excellent for key-value storage\n- Lower memory footprint\n\n**Cons**:\n- No persistence\n- Limited data structures\n\n**Decision**: Perfect fit for ephemeral session data where simplicity is valued.\n\n## Performance Impact\n\n| Metric | Before Caching | After Caching | Improvement |\n|--------|----------------|---------------|-------------|\n| Avg Response Time | 200ms | 50ms | 75% faster |\n| Database Load | 100% | 25% | 75% reduction |\n| Cache Hit Rate | - | 85% | - |\n| Peak RPS Handled | 1,000 | 4,000 | 4x increase |\n\n**Source**: <mention-page url=\"...\">Performance Optimization - Q3 2024</mention-page>\n\n## Known Issues & Limitations\n\n### Cache Stampede\n**Issue**: When popular cache entries expire, multiple requests hit database simultaneously\n\n**Mitigation**: Implemented probabilistic early expiration and request coalescing\n\n**Status**: Reduced by 90% but not eliminated\n\n### Stale Data Risk\n**Issue**: Cached data can be up to TTL duration stale\n\n**Mitigation**: Event-driven invalidation for critical data paths\n\n**Status**: Acceptable trade-off for performance gains\n\n## Monitoring & Observability\n\n**Metrics tracked**:\n- Cache hit/miss rates per endpoint\n- Memory usage and eviction rates\n- Response time distributions\n- Invalidation event frequency\n\n**Tools**: DataDog dashboards, CloudWatch alarms\n\n## Future Considerations\n\n1. **Edge Caching**: Evaluate CDN integration for static assets\n2. **Cache Warming**: Pre-populate cache for predictable traffic spikes\n3. **Adaptive TTLs**: Adjust TTLs based on data change frequency\n4. **Regional Caching**: Multi-region cache replication for global performance\n\n## Related Documentation\n\n- <mention-page url=\"...\">System Architecture Overview</mention-page>\n- <mention-page url=\"...\">Redis Implementation Guide</mention-page>\n- <mention-page url=\"...\">API Caching Decision Record</mention-page>\n- <mention-page url=\"...\">Performance Optimization - Q3 2024</mention-page>\n\n## Appendix: Configuration Examples\n\n### Redis Configuration\n```yaml\nmaxmemory: 8gb\nmaxmemory-policy: allkeys-lru\ntcp-keepalive: 60\n```\n\n### Common Cache Operations\n```python\n# Set with TTL\ncache.set(key, value, ttl=300)\n\n# Get with fallback\nvalue = cache.get(key) or fetch_from_db(key)\n\n# Invalidate pattern\ncache.delete_pattern(\"api:v1:users:*\")\n```\n```\n\n## Key Success Factors\n\n1. **Multiple source integration**: Combined architecture docs, implementation guides, and decision records\n2. **Technical depth**: Included configuration, code examples, metrics\n3. **Decision context**: Explained why choices were made, not just what\n4. **Practical focus**: Real performance numbers and known issues\n5. **Future-looking**: Noted areas for improvement\n6. **Well-cited**: Every major point links back to source material\n\n## Workflow Pattern Demonstrated\n\nThis example shows the complete research workflow:\n- **Scoped search** (teamspace filter for engineering)\n- **Multi-page synthesis** (4 different sources)\n- **Technical template** (architecture-focused format)\n- **Proper placement** (under engineering docs)\n- **Comprehensive citations** (links to all sources)\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/examples/trip-planning.md",
    "content": "# Example: Group Trip Research & Planning\n\n**User Request**: \"Research and plan our friends' trip to Japan in March - we're 6 people looking for 10 days\"\n\n## Workflow\n\n### 1. Search Existing Notes\n```\nNotion:notion-search\nquery: \"Japan travel\"\n```\nFound: Japan Travel Guide (from friend), Tokyo Restaurants, Kyoto Temple Guide\n\n### 2. Fetch & Extract Tips\n```\nNotion:notion-fetch (3x)\n```\n**Key info from previous travelers:**\n- Best time: March-April (cherry blossoms)\n- Must-see: Tokyo, Kyoto, Osaka\n- Budget: $200-300/day (mid-range)\n- Book accommodations 3 months ahead\n- Get JR Pass before arrival\n- Top restaurants: Sushi Dai, Ichiran Ramen, Tsunahachi Tempura\n\n### 3. Research & Synthesize\nCombined previous traveler insights with:\n- Flight options and prices\n- Accommodation types (hotels/ryokans/Airbnb)\n- Transportation (JR Pass essential)\n- 10-day itinerary structure\n- Budget breakdown\n\n### 4. Create Comprehensive Plan\n```\nNotion:notion-create-pages\nparent: { page_id: \"travel-plans-parent-id\" }\npages: [{\n  properties: {\n    title: \"Japan Trip 2026 - March 15-25 (10 Days)\"\n  },\n  content: \"[Full trip plan with itinerary, budget, tips...]\"\n}]\n```\n\n## Output Sample\n\n```markdown\n# Japan Trip 2026 - March 15-25 (10 Days)\n\n## Trip Overview\n**Dates**: March 15-25, 2026 (Cherry Blossom Season 🌸)\n**Group**: 6 people | **Budget**: $3,000-4,000/person\n\n## Itinerary Summary\n\n**Days 1-3: Tokyo**\n- Arrive, explore Shibuya, Harajuku, Shinjuku\n- Visit Tsukiji Market, Imperial Palace, Sensoji Temple\n- Experience Tokyo nightlife, teamLab Borderless\n\n**Days 4-5: Hakone**  \n- Day trip from Tokyo\n- Hot springs, Mt. Fuji views, Lake Ashi cruise\n\n**Days 6-8: Kyoto**\n- Bullet train from Tokyo\n- Fushimi Inari, Kinkaku-ji, Arashiyama Bamboo Grove\n- Geisha district (Gion), traditional tea ceremony\n\n**Days 9-10: Osaka + Depart**\n- Osaka Castle, Dotonbori food district\n- Nara day trip (deer park, Todai-ji Temple)\n- Return flight from Osaka (KIX)\n\n## Budget Breakdown (per person)\n- **Flights**: $900-1,200 (round-trip)\n- **Accommodations**: $700-1,000 (9 nights)\n- **JR Pass**: $280 (7-day pass)\n- **Food**: $500-700 ($50-70/day)\n- **Activities**: $300-400\n- **Local transport**: $100\n**Total**: $2,780-3,680\n\n## Key Bookings\n- **Flights**: Book 3-4 months ahead\n- **Hotels**: Book now (cherry blossom season)\n  - Tokyo: Shinjuku area (2 nights) + Asakusa (1 night)\n  - Kyoto: Gion area (3 nights)\n  - Osaka: Namba area (2 nights)\n- **JR Pass**: Order 2-3 weeks before travel\n- **Restaurants**: Reserve 1 week ahead (Sushi Dai, high-end spots)\n\n## Essential Tips\nSource: <mention-page url=\"...\">Japan Travel Guide</mention-page>\n- Get pocket WiFi or eSIM on arrival\n- Download: Google Translate, Hyperdia (train routes), Tabelog (restaurants)\n- Cash-heavy country - withdraw at 7-Eleven ATMs\n- Shoes off in temples, ryokans, some restaurants\n- Trains extremely punctual - don't be late\n- Learn basic phrases: arigatou, sumimasen, itadakimasu\n\n## Packing List\n- Comfortable walking shoes (10k+ steps/day)\n- Light jacket (March 55-65°F)\n- Backpack for day trips\n- Cash pouch\n- Portable charger\n\n## Next Steps\n- [ ] Book flights (target: <$1,100/person)\n- [ ] Order JR Passes\n- [ ] Book hotels (Tokyo → Kyoto → Osaka)\n- [ ] Create shared expense tracker\n- [ ] Schedule group planning call\n\n## Sources\n- <mention-page url=\"...\">Japan Travel Guide</mention-page> (Sarah's 2024 trip)\n- <mention-page url=\"...\">Tokyo Restaurant Recommendations</mention-page>\n- <mention-page url=\"...\">Kyoto Temple Guide</mention-page>\n```\n\n## Key Takeaways\n- Leveraged previous traveler notes from Notion\n- Combined personal insights with research\n- Created actionable itinerary with budget breakdown\n- Included practical tips from experienced travelers\n- Set clear next steps for group coordination\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/reference/advanced-search.md",
    "content": "# Advanced Search Techniques\n\n## Search Filtering\n\n### By Date Range\n\nUse `created_date_range` to find recent content:\n\n```\nfilters: {\n  created_date_range: {\n    start_date: \"2024-01-01\",\n    end_date: \"2025-01-01\"\n  }\n}\n```\n\n**When to use**:\n- Finding recent updates on a topic\n- Focusing on current information\n- Excluding outdated content\n\n### By Creator\n\nUse `created_by_user_ids` to find content from specific people:\n\n```\nfilters: {\n  created_by_user_ids: [\"user-id-1\", \"user-id-2\"]\n}\n```\n\n**When to use**:\n- Research from subject matter experts\n- Team-specific information\n- Attribution tracking\n\n### Combined Filters\n\nStack filters for precision:\n\n```\nfilters: {\n  created_date_range: {\n    start_date: \"2024-10-01\"\n  },\n  created_by_user_ids: [\"expert-user-id\"]\n}\n```\n\n## Scoped Searches\n\n### Teamspace Scoping\n\nRestrict search to specific teamspace:\n\n```\nteamspace_id: \"teamspace-uuid\"\n```\n\n**When to use**:\n- Project-specific research\n- Department-focused information\n- Reducing noise from irrelevant results\n\n### Page Scoping\n\nSearch within a specific page and its subpages:\n\n```\npage_url: \"https://notion.so/workspace/Page-Title-uuid\"\n```\n\n**When to use**:\n- Research within a project hierarchy\n- Documentation updates\n- Focused investigation\n\n### Database Scoping\n\nSearch within a database's content:\n\n```\ndata_source_url: \"collection://data-source-uuid\"\n```\n\n**When to use**:\n- Task/project database research\n- Structured data investigation\n- Finding specific entries\n\n## Search Strategies\n\n### Broad to Narrow\n\n1. Start with general search term\n2. Review results for relevant teamspaces/pages\n3. Re-search with scope filters\n4. Fetch detailed content from top results\n\n**Example**:\n```\nSearch 1: query=\"API integration\" → 50 results across workspace\nSearch 2: query=\"API integration\", teamspace_id=\"engineering\" → 12 results\nFetch: Top 3-5 most relevant pages\n```\n\n### Multi-Query Approach\n\nRun parallel searches with related terms:\n\n```\nQuery 1: \"API integration\"\nQuery 2: \"API authentication\"\nQuery 3: \"API documentation\"\n```\n\nCombine results to build comprehensive picture.\n\n### Temporal Research\n\nSearch across time periods to track evolution:\n\n```\nSearch 1: created_date_range 2023 → Historical context\nSearch 2: created_date_range 2024 → Recent developments\nSearch 3: created_date_range 2025 → Current state\n```\n\n## Result Processing\n\n### Identifying Relevant Results\n\nLook for:\n- **High semantic match**: Result summary closely matches query intent\n- **Recent updates**: Last-edited date is recent\n- **Authoritative sources**: Created by known experts or in official locations\n- **Comprehensive content**: Result summary suggests detailed information\n\n### Prioritizing Fetches\n\nFetch pages in order of relevance:\n\n1. **Primary sources**: Direct documentation, official pages\n2. **Recent updates**: Newly edited content\n3. **Related context**: Supporting information\n4. **Historical reference**: Background and context\n\nDon't fetch everything - be selective based on research needs.\n\n### Handling Too Many Results\n\nIf search returns 20+ results:\n\n1. **Add filters**: Narrow by date, creator, or teamspace\n2. **Refine query**: Use more specific terms\n3. **Use page scoping**: Search within relevant parent page\n4. **Sample strategically**: Fetch diverse results (recent, popular, authoritative)\n\n### Handling Too Few Results\n\nIf search returns < 3 results:\n\n1. **Broaden query**: Use more general terms\n2. **Remove filters**: Search full workspace\n3. **Try synonyms**: Alternative terminology\n4. **Search in related areas**: Adjacent teamspaces or pages\n\n## Search Quality\n\n### Effective Search Queries\n\n**Good queries** (specific, semantic):\n- \"Q4 product roadmap\"\n- \"authentication implementation guide\"\n- \"customer feedback themes\"\n\n**Weak queries** (too vague):\n- \"roadmap\"\n- \"guide\"\n- \"feedback\"\n\n**Over-specific queries** (too narrow):\n- \"Q4 2024 product roadmap for mobile app version 3.2 feature X\"\n\n### User Context\n\nAlways use available user context:\n- Query should match their terminology\n- Scope to their relevant teamspaces\n- Consider their role/department\n- Reference their recent pages\n\n## Connected Sources\n\n### Notion Integrations\n\nSearch extends beyond Notion pages to:\n- Slack messages (if connected)\n- Google Drive documents (if connected)\n- GitHub issues/PRs (if connected)\n- Jira tickets (if connected)\n\nBe aware results may come from these sources.\n\n### Source Attribution\n\nWhen citing results from connected sources:\n- Note the source type in documentation\n- Use appropriate mention format\n- Verify user has access to the source system\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/reference/citations.md",
    "content": "# Citation Styles\n\n## Basic Page Citation\n\nAlways cite sources using Notion page mentions:\n\n```markdown\n<mention-page url=\"https://notion.so/workspace/Page-Title-uuid\">Page Title</mention-page>\n```\n\nThe URL must be provided. The title is optional but improves readability:\n\n```markdown\n<mention-page url=\"https://notion.so/workspace/Page-Title-uuid\"/>\n```\n\n## Inline Citations\n\nCite immediately after referenced information:\n\n```markdown\nThe Q4 revenue increased by 23% quarter-over-quarter (<mention-page url=\"...\">Q4 Financial Report</mention-page>).\n```\n\n## Multiple Sources\n\nWhen information comes from multiple sources:\n\n```markdown\nCustomer satisfaction has improved across all metrics (<mention-page url=\"...\">Q3 Survey Results</mention-page>, <mention-page url=\"...\">Support Analysis</mention-page>).\n```\n\n## Section-Level Citations\n\nFor longer sections derived from one source:\n\n```markdown\n### Engineering Priorities\n\nAccording to the <mention-page url=\"...\">Engineering Roadmap 2025</mention-page>:\n\n- Focus on API scalability\n- Improve developer experience\n- Migrate to microservices architecture\n```\n\n## Sources Section\n\nAlways include a \"Sources\" section at document end:\n\n```markdown\n## Sources\n\n- <mention-page url=\"...\">Strategic Plan 2025</mention-page>\n- <mention-page url=\"...\">Market Analysis Report</mention-page>\n- <mention-page url=\"...\">Competitor Research: Q3</mention-page>\n- <mention-page url=\"...\">Customer Interview Notes</mention-page>\n```\n\nGroup by category for long lists:\n\n```markdown\n## Sources\n\n### Primary Sources\n- <mention-page url=\"...\">Official Roadmap</mention-page>\n- <mention-page url=\"...\">Strategy Document</mention-page>\n\n### Supporting Research\n- <mention-page url=\"...\">Market Trends</mention-page>\n- <mention-page url=\"...\">Customer Feedback</mention-page>\n\n### Background Context\n- <mention-page url=\"...\">Historical Analysis</mention-page>\n```\n\n## Quoting Content\n\nWhen quoting directly from source:\n\n```markdown\nThe product team noted: \"We need to prioritize mobile experience improvements\" (<mention-page url=\"...\">Product Meeting Notes</mention-page>).\n```\n\nFor block quotes:\n\n```markdown\n> We need to prioritize mobile experience improvements to meet our Q4 goals. This includes performance optimization and UI refresh.\n>\n> — <mention-page url=\"...\">Product Meeting Notes - Oct 2025</mention-page>\n```\n\n## Data Citations\n\nWhen presenting data, cite the source:\n\n```markdown\n| Metric | Q3 | Q4 | Change |\n|--------|----|----|--------|\n| Revenue | $2.3M | $2.8M | +21.7% |\n| Users | 12.4K | 15.1K | +21.8% |\n\nSource: <mention-page url=\"...\">Financial Dashboard</mention-page>\n```\n\n## Database Citations\n\nWhen referencing database content:\n\n```markdown\nBased on analysis of the <mention-database url=\"...\">Projects Database</mention-database>, 67% of projects are on track.\n```\n\n## User Citations\n\nWhen attributing information to specific people:\n\n```markdown\n<mention-user url=\"...\">Sarah Chen</mention-user> noted in <mention-page url=\"...\">Architecture Review</mention-page> that the microservices migration is ahead of schedule.\n```\n\n## Citation Frequency\n\n**Over-citing** (every sentence):\n```markdown\nThe revenue increased (<mention-page url=\"...\">Report</mention-page>). \nCosts decreased (<mention-page url=\"...\">Report</mention-page>). \nMargin improved (<mention-page url=\"...\">Report</mention-page>).\n```\n\n**Under-citing** (no attribution):\n```markdown\nThe revenue increased, costs decreased, and margin improved.\n```\n\n**Right balance** (grouped citation):\n```markdown\nThe revenue increased, costs decreased, and margin improved (<mention-page url=\"...\">Q4 Financial Report</mention-page>).\n```\n\n## Outdated Information\n\nNote when source information might be outdated:\n\n```markdown\nThe original API design (<mention-page url=\"...\">API Spec v1</mention-page>, last updated January 2024) has been superseded by the new architecture in <mention-page url=\"...\">API Spec v2</mention-page>.\n```\n\n## Cross-References\n\nLink to related research documents:\n\n```markdown\n## Related Research\n\nThis research builds on previous findings:\n- <mention-page url=\"...\">Market Analysis - Q2 2025</mention-page>\n- <mention-page url=\"...\">Competitor Landscape Review</mention-page>\n\nFor implementation details, see:\n- <mention-page url=\"...\">Technical Implementation Guide</mention-page>\n```\n\n## Citation Validation\n\nBefore finalizing research:\n\n✓ Every key claim has a source citation\n✓ All page mentions have valid URLs\n✓ Sources section includes all cited pages\n✓ Outdated sources are noted as such\n✓ Direct quotes are clearly marked\n✓ Data sources are attributed\n\n## Citation Style Consistency\n\nChoose one citation style and use throughout:\n\n**Inline style** (lightweight):\n```markdown\nRevenue grew 23% (Financial Report). Customer count increased 18% (Metrics Dashboard).\n```\n\n**Formal style** (full mentions):\n```markdown\nRevenue grew 23% (<mention-page url=\"...\">Q4 Financial Report</mention-page>). Customer count increased 18% (<mention-page url=\"...\">Metrics Dashboard</mention-page>).\n```\n\n**Recommend formal style** for most research documentation as it provides clickable navigation.\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/reference/comparison-format.md",
    "content": "# Comparison Format\n\n**When to use**:\n- Evaluating multiple options\n- Tool/vendor selection\n- Approach comparison\n- Decision support\n\n## Characteristics\n\n**Length**: 800-1200 words typically\n\n**Structure**:\n- Overview of what's being compared\n- Comparison matrix table\n- Detailed analysis per option (pros/cons)\n- Clear recommendation with rationale\n- Sources\n\n## Template\n\nSee [comparison-template.md](comparison-template.md) for the full template.\n\n## Best For\n\n- Decision support with multiple options\n- Tool or vendor selection\n- Comparing different technical approaches\n- Evaluating trade-offs between alternatives\n\n## Example Use Cases\n\n- \"Compare the three database options discussed in our tech docs\"\n- \"What are the pros and cons of each deployment approach?\"\n- \"Compare the vendor proposals\"\n- \"Evaluate the different authentication methods we've documented\"\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/reference/comparison-template.md",
    "content": "# Comparison Template\n\nUse when researching multiple options or alternatives. See [comparison-format.md](comparison-format.md) for when to use this format.\n\n```markdown\n# [Topic] Comparison\n\n## Overview\n[Brief introduction to what's being compared and why]\n\n## Comparison Matrix\n\n| Criteria | Option A | Option B | Option C |\n|----------|----------|----------|----------|\n| [Criterion 1] | [Rating/Details] | [Rating/Details] | [Rating/Details] |\n| [Criterion 2] | [Rating/Details] | [Rating/Details] | [Rating/Details] |\n\n## Detailed Analysis\n\n### Option A\n**Pros**:\n- [Advantage]\n- [Advantage]\n\n**Cons**:\n- [Disadvantage]\n- [Disadvantage]\n\n**Best for**: [Use case]\n\n**Source**: <mention-page url=\"...\">Source Page</mention-page>\n\n[Repeat for each option]\n\n## Recommendation\n\n**Selected option**: [Choice]\n\n**Rationale**: [Why this option is best given the context]\n\n## Sources\n[List all consulted pages]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/reference/comprehensive-report-format.md",
    "content": "# Comprehensive Report Format\n\n**When to use**: \n- Formal documentation requirements\n- Strategic decision support\n- Complex topics requiring extensive analysis\n- Multiple stakeholders need alignment\n\n## Characteristics\n\n**Length**: 1500+ words\n\n**Structure**:\n- Executive summary\n- Background & context\n- Methodology\n- Detailed findings with subsections\n- Data & evidence section\n- Implications (short and long-term)\n- Prioritized recommendations\n- Appendix\n\n## Template\n\nSee [comprehensive-report-template.md](comprehensive-report-template.md) for the full template.\n\n## Best For\n\n- Deep analysis and strategic decisions\n- Formal documentation requirements\n- Complex topics with multiple facets\n- When stakeholders need extensive context\n- Board presentations or executive briefings\n\n## Example Use Cases\n\n- \"Create a comprehensive analysis of our market position\"\n- \"Document the full technical investigation of the database migration\"\n- \"Prepare an in-depth report on vendor options for executive review\"\n- \"Analyze the pros and cons of different architectural approaches\"\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/reference/comprehensive-report-template.md",
    "content": "# Comprehensive Report Template\n\nUse for in-depth research requiring extensive analysis. See [comprehensive-report-format.md](comprehensive-report-format.md) for when to use this format.\n\n```markdown\n# [Report Title]\n\n## Executive Summary\n[One paragraph summarizing the entire report]\n\n## Background & Context\n[Why this research was conducted, what questions it addresses]\n\n## Methodology\n- Sources consulted: [number] Notion pages across [teamspaces]\n- Time period: [if relevant]\n- Scope: [what was included/excluded]\n\n## Key Findings\n\n### [Major Theme 1]\n**Summary**: [One sentence]\n\n**Details**:\n- [Supporting point with evidence]\n- [Supporting point with evidence]\n- [Supporting point with evidence]\n\n**Sources**: [Page mentions]\n\n### [Major Theme 2]\n[Repeat structure]\n\n## Data & Evidence\n\n[Tables, quotes, specific data points]\n\n## Implications\n\n### Short-term\n[Immediate implications]\n\n### Long-term\n[Strategic implications]\n\n## Recommendations\n\n### Priority 1: [High priority action]\n- **What**: [Specific action]\n- **Why**: [Rationale]\n- **How**: [Implementation approach]\n\n### Priority 2: [Medium priority action]\n[Repeat structure]\n\n## Appendix\n\n### Additional Resources\n- [Related pages]\n\n### Open Questions\n- [Unanswered questions for future research]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/reference/format-selection-guide.md",
    "content": "# Format Selection Guide\n\nChoose the right output format for your research needs.\n\n## Decision Tree\n\n```\nIs this comparing multiple options?\n  ├─ YES → Use Comparison Format\n  └─ NO ↓\n\nIs this time-sensitive or simple?\n  ├─ YES → Use Quick Brief\n  └─ NO ↓\n\nDoes this require formal/extensive documentation?\n  ├─ YES → Use Comprehensive Report\n  └─ NO → Use Research Summary (default)\n```\n\n## Format Overview\n\n| Format | Length | When to Use | Template |\n|--------|--------|-------------|----------|\n| [Research Summary](research-summary-format.md) | 500-1000 words | Most research requests (default) | [Template](research-summary-template.md) |\n| [Comprehensive Report](comprehensive-report-format.md) | 1500+ words | Formal docs, strategic decisions | [Template](comprehensive-report-template.md) |\n| [Quick Brief](quick-brief-format.md) | 200-400 words | Time-sensitive, simple topics | [Template](quick-brief-template.md) |\n| [Comparison](comparison-format.md) | 800-1200 words | Evaluating options | [Template](comparison-template.md) |\n\n## Formatting Guidelines\n\n### Headings\n- Use `#` for title\n- Use `##` for major sections\n- Use `###` for subsections\n- Keep heading hierarchy consistent\n\n### Lists\n- Use `-` for bullet points\n- Use `1.` for numbered lists\n- Keep list items parallel in structure\n\n### Emphasis\n- Use `**bold**` for key terms and section labels\n- Use `*italic*` for emphasis\n- Use sparingly for maximum impact\n\n### Citations\n- Always use `<mention-page url=\"...\">Page Title</mention-page>` for source pages\n- Include citation immediately after referenced information\n- Group all sources in a \"Sources\" section at the end\n\n### Tables\n- Use for structured data comparison\n- Keep columns to 3-5 for readability\n- Include header row\n- Align content appropriately\n\n### Code Blocks\nUse when including:\n- Technical specifications\n- Configuration examples\n- Command examples\n\n```\nExample code or configuration here\n```\n\n## Content Guidelines\n\n### Executive Summaries\n- Lead with the most important finding\n- Include 1-2 key implications\n- Make it standalone (reader gets value without reading further)\n- Target 2-3 sentences for summaries, 1 paragraph for reports\n\n### Key Findings\n- Start with a clear headline\n- Support with specific evidence\n- Include relevant data points or quotes\n- Cite source immediately\n- Focus on actionable insights\n\n### Recommendations\n- Make them specific and actionable\n- Explain the \"why\" behind each recommendation\n- Prioritize clearly (Priority 1, 2, 3 or High/Medium/Low)\n- Include implementation hints when relevant\n\n### Source Citations\n- Link to original pages using mentions\n- Note if information is outdated (check last-edited dates)\n- Credit specific sections when quoting\n- Group related sources together\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/reference/quick-brief-format.md",
    "content": "# Quick Brief Format\n\n**When to use**:\n- Time-sensitive requests\n- Simple topics\n- Status updates\n- Quick reference needs\n\n## Characteristics\n\n**Length**: 200-400 words\n\n**Structure**:\n- 3-4 sentence summary\n- 3-5 bullet key points\n- Short action items list\n- Brief source list\n\n## Template\n\nSee [quick-brief-template.md](quick-brief-template.md) for the full template.\n\n## Best For\n\n- Fast turnaround requests\n- Simple, straightforward topics\n- Quick status updates\n- When time is more important than depth\n- Initial exploration before deeper research\n\n## Example Use Cases\n\n- \"Quick summary of what's in our API docs\"\n- \"Fast brief on the meeting notes from yesterday\"\n- \"What are the key points from that spec?\"\n- \"Give me a quick overview of the project status\"\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/reference/quick-brief-template.md",
    "content": "# Quick Brief Template\n\nUse for fast turnaround requests or simple topics. See [quick-brief-format.md](quick-brief-format.md) for when to use this format.\n\n```markdown\n# [Topic] - Quick Brief\n\n**Date**: [Current date]\n\n## Summary\n[3-4 sentences covering the essentials]\n\n## Key Points\n- **Point 1**: [Details]\n- **Point 2**: [Details]\n- **Point 3**: [Details]\n\n## Action Items\n1. [Immediate next step]\n2. [Follow-up action]\n\n## Sources\n[Brief list of pages consulted]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/reference/research-summary-format.md",
    "content": "# Research Summary Format\n\n**When to use**: General research requests, most common format\n\n## Characteristics\n\n**Length**: 500-1000 words typically\n\n**Structure**:\n- Executive summary (2-3 sentences)\n- 3-5 key findings with supporting evidence\n- Detailed analysis section\n- Conclusions and next steps\n- Source citations\n\n## Template\n\nSee [research-summary-template.md](research-summary-template.md) for the full template.\n\n## Best For\n\n- Most general-purpose research requests\n- Standard documentation needs\n- Balanced depth and readability\n- When you need comprehensive but accessible information\n\n## Example Use Cases\n\n- \"Research our authentication options\"\n- \"What does our project documentation say about the API redesign?\"\n- \"Summarize the team's discussion about mobile strategy\"\n- \"Compile information about our deployment process\"\n\n"
  },
  {
    "path": "skills/.curated/notion-research-documentation/reference/research-summary-template.md",
    "content": "# Research Summary Template\n\nUse this for most research requests. See [research-summary-format.md](research-summary-format.md) for when to use this format.\n\n```markdown\n# [Topic Name]\n\n## Executive Summary\n[2-3 sentence overview of key findings and implications]\n\n## Key Findings\n\n### Finding 1: [Clear headline]\n[Details and supporting evidence]\n- Source: <mention-page url=\"...\">Original Page</mention-page>\n\n### Finding 2: [Clear headline]\n[Details and supporting evidence]\n- Source: <mention-page url=\"...\">Original Page</mention-page>\n\n### Finding 3: [Clear headline]\n[Details and supporting evidence]\n- Source: <mention-page url=\"...\">Original Page</mention-page>\n\n## Detailed Analysis\n\n### [Section 1]\n[In-depth discussion of first major theme]\n\n### [Section 2]\n[In-depth discussion of second major theme]\n\n## Conclusions\n\n[Summary of implications and insights]\n\n## Next Steps\n\n1. [Actionable recommendation]\n2. [Actionable recommendation]\n3. [Actionable recommendation]\n\n## Sources\n\n- <mention-page url=\"...\">Page Title</mention-page>\n- <mention-page url=\"...\">Page Title</mention-page>\n- <mention-page url=\"...\">Page Title</mention-page>\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/LICENSE.txt",
    "content": "Copyright 2025 Notion Labs, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/SKILL.md",
    "content": "---\nname: notion-spec-to-implementation\ndescription: Turn Notion specs into implementation plans, tasks, and progress tracking; use when implementing PRDs/feature specs and creating Notion plans + tasks from them.\nmetadata:\n  short-description: Turn Notion specs into implementation plans, tasks, and progress tracking\n---\n\n# Spec to Implementation\n\nConvert a Notion spec into linked implementation plans, tasks, and ongoing status updates.\n\n## Quick start\n1) Locate the spec with `Notion:notion-search`, then fetch it with `Notion:notion-fetch`.\n2) Parse requirements and ambiguities using `reference/spec-parsing.md`.\n3) Create a plan page with `Notion:notion-create-pages` (pick a template: quick vs. full).\n4) Find the task database, confirm schema, then create tasks with `Notion:notion-create-pages`.\n5) Link spec ↔ plan ↔ tasks; keep status current with `Notion:notion-update-page`.\n\n## Workflow\n\n### 0) If any MCP call fails because Notion MCP is not connected, pause and set it up:\n1. Add the Notion MCP:\n   - `codex mcp add notion --url https://mcp.notion.com/mcp`\n2. Enable remote MCP client:\n   - Set `[features].rmcp_client = true` in `config.toml` **or** run `codex --enable rmcp_client`\n3. Log in with OAuth:\n   - `codex mcp login notion`\n\nAfter successful login, the user will have to restart codex. You should finish your answer and tell them so when they try again they can continue with Step 1.\n\n### 1) Locate and read the spec\n- Search first (`Notion:notion-search`); if multiple hits, ask the user which to use.\n- Fetch the page (`Notion:notion-fetch`) and scan for requirements, acceptance criteria, constraints, and priorities. See `reference/spec-parsing.md` for extraction patterns.\n- Capture gaps/assumptions in a clarifications block before proceeding.\n\n### 2) Choose plan depth\n- Simple change → use `reference/quick-implementation-plan.md`.\n- Multi-phase feature/migration → use `reference/standard-implementation-plan.md`.\n- Create the plan via `Notion:notion-create-pages`, include: overview, linked spec, requirements summary, phases, dependencies/risks, and success criteria. Link back to the spec.\n\n### 3) Create tasks\n- Find the task database (`Notion:notion-search` → `Notion:notion-fetch` to confirm the data source and required properties). Patterns in `reference/task-creation.md`.\n- Size tasks to 1–2 days. Use `reference/task-creation-template.md` for content (context, objective, acceptance criteria, dependencies, resources).\n- Set properties: title/action verb, status, priority, relations to spec + plan, due date/story points/assignee if provided.\n- Create pages with `Notion:notion-create-pages` using the database’s `data_source_id`.\n\n### 4) Link artifacts\n- Plan links to spec; tasks link to both plan and spec.\n- Optionally update the spec with a short “Implementation” section pointing to the plan and tasks using `Notion:notion-update-page`.\n\n### 5) Track progress\n- Use the cadence in `reference/progress-tracking.md`.\n- Post updates with `reference/progress-update-template.md`; close phases with `reference/milestone-summary-template.md`.\n- Keep checklists and status fields in plan/tasks in sync; note blockers and decisions.\n\n## References and examples\n- `reference/` — parsing patterns, plan/task templates, progress cadence (e.g., `spec-parsing.md`, `standard-implementation-plan.md`, `task-creation.md`, `progress-tracking.md`).\n- `examples/` — end-to-end walkthroughs (e.g., `ui-component.md`, `api-feature.md`, `database-migration.md`).\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Notion Spec to Implementation\"\n  short_description: \"Turn Notion specs into implementation plans, tasks, and progress tracking\"\n  icon_small: \"./assets/notion-small.svg\"\n  icon_large: \"./assets/notion.png\"\n  default_prompt: \"Turn this Notion spec into an implementation plan with milestones, tasks, and dependencies.\"\n\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"notion\"\n      description: \"Notion MCP server\"\n      transport: \"streamable_http\"\n      url: \"https://mcp.notion.com/mcp\"\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/evaluations/README.md",
    "content": "# Spec to Implementation Skill Evaluations\n\nEvaluation scenarios for testing the Spec to Implementation skill across different Codex models.\n\n## Purpose\n\nThese evaluations ensure the Spec to Implementation skill:\n- Finds and parses specification pages accurately\n- Breaks down specs into actionable implementation plans\n- Creates tasks that Codex can implement with clear acceptance criteria\n- Tracks progress and updates implementation status\n- Works consistently across Haiku, Sonnet, and Opus\n\n## Evaluation Files\n\n### basic-spec-implementation.json\nTests basic workflow of turning a spec into an implementation plan.\n\n**Scenario**: Implement user authentication feature from spec  \n**Key Behaviors**:\n- Searches for and finds the authentication spec page\n- Fetches spec and extracts requirements\n- Parses requirements into phases (setup, core features, polish)\n- Creates implementation plan page linked to original spec\n- Breaks down into clear phases with deliverables\n- Includes timeline and dependencies\n\n### spec-to-tasks.json\nTests creating concrete tasks from a specification in a task database.\n\n**Scenario**: Create tasks from API redesign spec  \n**Key Behaviors**:\n- Finds spec page in Notion\n- Extracts specific requirements and acceptance criteria\n- Searches for or creates task database\n- Fetches task database schema\n- Creates multiple tasks with proper properties (Status, Priority, Sprint, etc.)\n- Each task has clear title, description, and acceptance criteria\n- Tasks have dependencies where appropriate\n- Links all tasks back to original spec\n\n## Running Evaluations\n\n1. Enable the `spec-to-implementation` skill\n2. Submit the query from the evaluation file\n3. Verify the skill finds the spec page via search\n4. Check that requirements are accurately parsed\n5. Confirm implementation plan is created with phases\n6. Verify tasks have clear, implementable acceptance criteria\n7. Check that tasks link back to spec\n8. Test with Haiku, Sonnet, and Opus\n\n## Expected Skill Behaviors\n\nSpec to Implementation evaluations should verify:\n\n### Spec Discovery & Parsing\n- Searches Notion for specification pages\n- Fetches complete spec content\n- Extracts all requirements accurately\n- Identifies technical dependencies\n- Understands acceptance criteria\n- Notes any ambiguities or missing details\n\n### Implementation Planning\n- Creates implementation plan page\n- Breaks work into logical phases:\n  - Phase 1: Foundation/Setup\n  - Phase 2: Core Implementation\n  - Phase 3: Testing & Polish\n- Includes timeline estimates\n- Identifies dependencies between phases\n- Links back to original spec\n\n### Task Creation\n- Finds or identifies task database\n- Fetches database schema for property names\n- Creates tasks with correct properties\n- Each task has:\n  - Clear, specific title\n  - Context and description\n  - Acceptance criteria (checklist format)\n  - Appropriate priority and status\n  - Link to spec page\n- Tasks are right-sized (not too big, not too small)\n- Dependencies between tasks are noted\n\n### Progress Tracking\n- Implementation plan includes progress markers\n- Tasks can be updated as work progresses\n- Status updates link to completed work\n- Blockers or changes are noted\n\n## Creating New Evaluations\n\nWhen adding Spec to Implementation evaluations:\n\n1. **Test different spec types** - Features, migrations, refactors, API changes, UI components\n2. **Vary complexity** - Simple 1-phase vs. complex multi-phase implementations\n3. **Test task granularity** - Does it create appropriately-sized tasks?\n4. **Include edge cases** - Vague specs, conflicting requirements, missing details\n5. **Test database integration** - Creating tasks in existing task databases with various schemas\n6. **Progress tracking** - Updating implementation plans as tasks complete\n\n## Example Success Criteria\n\n**Good** (specific, testable):\n- \"Searches Notion for spec page using feature name\"\n- \"Creates implementation plan with 3 phases: Setup → Core → Polish\"\n- \"Creates 5-8 tasks in task database with properties: Task (title), Status, Priority, Sprint\"\n- \"Each task has acceptance criteria in checklist format (- [ ] ...)\"\n- \"Tasks link back to spec using mention-page tag\"\n- \"Task titles are specific and actionable (e.g., 'Create login API endpoint' not 'Authentication')\"\n\n**Bad** (vague, untestable):\n- \"Creates good implementation plan\"\n- \"Tasks are well-structured\"\n- \"Breaks down spec appropriately\"\n- \"Links to spec\"\n\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/evaluations/basic-spec-implementation.json",
    "content": "{\n  \"name\": \"Create Implementation Plan from Spec\",\n  \"skills\": [\"spec-to-implementation\"],\n  \"query\": \"Create an implementation plan for the User Authentication spec page\",\n  \"expected_behavior\": [\n    \"Step 1: Uses Notion:notion-search to find 'User Authentication spec' with keywords like 'User Authentication' or 'auth spec'\",\n    \"Step 2: If not found or ambiguous, asks user for spec page URL/ID\",\n    \"Step 3: Fetches spec page using Notion:notion-fetch with URL/ID from search results\",\n    \"Step 4: Parses spec using patterns from reference/spec-parsing.md to extract requirements, acceptance criteria, constraints\",\n    \"Step 5: Identifies functional requirements (user stories, features, workflows) and non-functional requirements (performance, security)\",\n    \"Step 6: Creates implementation plan following structure from reference/templates.md\",\n    \"Step 7: Includes sections: Overview, Linked Spec, Requirements Summary, Technical Approach, Implementation Phases\",\n    \"Step 8: Breaks work into logical phases with Goal, Tasks checklist, Estimated effort per phase\",\n    \"Step 9: Identifies dependencies and risks from spec content\",\n    \"Step 10: Links plan back to original spec page using <mention-page url='...'>\",\n    \"Step 11: Creates plan page using Notion:notion-create-pages with appropriate title (e.g., 'Implementation Plan: User Authentication')\",\n    \"Step 12: Places plan appropriately (asks user or suggests under project/spec parent)\"\n  ],\n  \"success_criteria\": [\n    \"Spec is found using Notion:notion-search before attempting to fetch (or user is asked for URL if not found)\",\n    \"Spec is fetched using Notion:notion-fetch with correct URL/ID from search results\",\n    \"Plan includes clear overview and spec link with mention-page tag\",\n    \"Requirements are extracted from actual spec content (not generic) using spec-parsing patterns\",\n    \"Work is broken into multiple phases (typically 3-5) following template structure\",\n    \"Each phase has Goal, Tasks (as checkboxes), and Estimated effort\",\n    \"Dependencies and risks sections are included with specific details from spec\",\n    \"Plan follows Implementation Plan structure from reference/templates.md\",\n    \"Success criteria or acceptance criteria from spec are referenced in plan\",\n    \"Uses correct tool names and sequence: Notion:notion-search → Notion:notion-fetch → Notion:notion-create-pages\"\n  ]\n}\n\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/evaluations/spec-to-tasks.json",
    "content": "{\n  \"name\": \"Create Tasks from Specification\",\n  \"skills\": [\"spec-to-implementation\", \"task-manager\"],\n  \"query\": \"Read the Payment Integration spec and create implementation tasks in our Tasks database\",\n  \"expected_behavior\": [\n    \"Step 1: Uses Notion:notion-search to find Payment Integration spec or asks for URL\",\n    \"Step 2: Fetches spec page using Notion:notion-fetch to read full content\",\n    \"Step 3: Parses spec using reference/spec-parsing.md patterns to identify work items\",\n    \"Step 4: Breaks down into appropriately-sized tasks using breakdown patterns from reference/task-creation.md\",\n    \"Step 5: Uses Notion:notion-search to find Tasks database location\",\n    \"Step 6: Fetches Tasks database using Notion:notion-fetch to get schema, property names, and data sources\",\n    \"Step 7: Identifies correct data source from <data-source> tags in fetch results\",\n    \"Step 8: Optionally creates implementation plan page first (recommended per workflow)\",\n    \"Step 9: For each task - creates task page using Notion:notion-create-pages with parent: { data_source_id: 'collection://...' }\",\n    \"Step 10: Sets task properties from schema: Title, Status (To Do), Priority, Related Tasks (link to spec)\",\n    \"Step 11: Includes task description with context, acceptance criteria from spec, dependencies\",\n    \"Step 12: Links tasks to spec page using <mention-page> and to each other for dependencies\",\n    \"Step 13: Sequences tasks appropriately (setup → implementation → testing per reference/task-creation.md)\",\n    \"Step 14: Reports summary: 'Created X tasks for Payment Integration: [task list with links]'\"\n  ],\n  \"success_criteria\": [\n    \"Spec is found using Notion:notion-search before attempting to fetch\",\n    \"Task database is found using Notion:notion-search before attempting to fetch schema\",\n    \"Database schema is fetched and data source identified from <data-source> tags\",\n    \"Multiple tasks created (at least 3-5) covering spec scope\",\n    \"Tasks are right-sized per reference/task-creation.md (1-2 days each)\",\n    \"Each task has clear acceptance criteria extracted from spec\",\n    \"Tasks are properly sequenced with dependencies using relation properties\",\n    \"All tasks link back to original spec using mention-page tags\",\n    \"Task properties match exact schema from fetched database\",\n    \"Tasks use correct parent (data_source_id: 'collection://...') for database placement\",\n    \"Uses correct tool names and sequence: Notion:notion-search (2x) → Notion:notion-fetch (2x) → Notion:notion-create-pages (Nx)\"\n  ]\n}\n\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/examples/api-feature.md",
    "content": "# Example: API Feature Implementation\n\n## User Request\n\n> \"Create an implementation plan for the User Profile API spec\"\n\n## Workflow Execution\n\n### Step 1: Fetch Specification\n\n```\nNotion:notion-search\nquery: \"User Profile API spec\"\nquery_type: \"internal\"\n```\n\nFound: \"User Profile API Specification\" in Engineering Specs\n\n```\nNotion:notion-fetch\nid: \"user-profile-api-spec-page-id\"\n```\n\n### Step 2: Parse Specification\n\n**Spec Content Extracted:**\n\n```markdown\n# User Profile API Specification\n\n## Overview\nRESTful API for user profile management\n\n## Requirements\n\n### Functional\n- FR-1: Get user profile by ID\n- FR-2: Update user profile (name, bio, avatar)\n- FR-3: Upload profile avatar\n- FR-4: Get user's public profile (limited fields)\n- FR-5: Search users by name\n\n### Non-Functional\n- NFR-1: Response time < 200ms (p95)\n- NFR-2: Support 1000 concurrent users\n- NFR-3: Avatar upload < 5MB\n- NFR-4: GDPR compliant (data portability)\n\n## API Endpoints\n\nGET /api/v1/users/:id\nPUT /api/v1/users/:id\nPOST /api/v1/users/:id/avatar\nGET /api/v1/users/:id/public\nGET /api/v1/users/search\n\n## Data Model\n- id (UUID)\n- email (string, unique)\n- name (string)\n- bio (text, max 500 chars)\n- avatar_url (string)\n- created_at (timestamp)\n- updated_at (timestamp)\n\n## Security\n- Authentication: JWT bearer token\n- Authorization: Users can only update own profile\n- Rate limiting: 100 req/min per user\n\n## Acceptance Criteria\n- AC-1: All endpoints return proper HTTP status codes\n- AC-2: Validation errors return 400 with error details\n- AC-3: Unauthorized access returns 401\n- AC-4: Rate limit exceeded returns 429\n- AC-5: Avatar images stored in S3\n```\n\n### Step 3: Create Implementation Plan\n\n```\nNotion:notion-create-pages\n\nparent: { page_id: \"engineering-plans-parent-id\" }\n\npages: [{\n  properties: {\n    \"title\": \"Implementation Plan: User Profile API\"\n  },\n  content: \"[Implementation plan]\"\n}]\n```\n\n**Implementation Plan Created:**\n\n```markdown\n# Implementation Plan: User Profile API\n\n## Overview\nBuild RESTful API for user profile management with CRUD operations, avatar upload, and search functionality.\n\n## Linked Specification\n<mention-page url=\"...\">User Profile API Specification</mention-page>\n\n## Requirements Summary\n\n### Functional Requirements\n- ✅ Get user profile (authenticated)\n- ✅ Update profile fields (name, bio)\n- ✅ Upload avatar with image processing\n- ✅ Public profile view (limited fields)\n- ✅ User search by name\n\n### Non-Functional Requirements\n- **Performance**: < 200ms response time (p95)\n- **Scalability**: 1000 concurrent users\n- **Storage**: Avatar files < 5MB to S3\n- **Compliance**: GDPR data portability\n\n### Acceptance Criteria\n- All endpoints with proper status codes\n- Input validation with error details\n- JWT authentication required\n- Rate limiting enforced\n- Avatars stored in S3\n\n## Technical Approach\n\n### Architecture\n- **Framework**: Express.js (Node.js)\n- **Database**: PostgreSQL\n- **Storage**: AWS S3 for avatars\n- **Cache**: Redis for profile data\n- **Search**: PostgreSQL full-text search\n\n### Key Design Decisions\n1. **JWT Authentication**: Stateless auth, scales horizontally\n2. **S3 for Avatars**: Offload storage, CDN integration ready\n3. **Redis Caching**: Reduce DB load for frequently accessed profiles\n4. **Rate Limiting**: Token bucket algorithm, per-user limits\n\n## Implementation Phases\n\n### Phase 1: Foundation (Days 1-2)\n**Goal**: Set up core infrastructure\n\n**Tasks**:\n- [ ] <mention-page url=\"...\">Setup database schema</mention-page>\n- [ ] <mention-page url=\"...\">Configure S3 bucket</mention-page>\n- [ ] <mention-page url=\"...\">Setup Redis cache</mention-page>\n- [ ] <mention-page url=\"...\">Create API scaffolding</mention-page>\n\n**Deliverables**: Working skeleton with DB, storage, cache ready  \n**Estimated effort**: 2 days\n\n### Phase 2: Core Endpoints (Days 3-5)\n**Goal**: Implement main CRUD operations\n\n**Tasks**:\n- [ ] <mention-page url=\"...\">Implement GET user profile</mention-page>\n- [ ] <mention-page url=\"...\">Implement PUT update profile</mention-page>\n- [ ] <mention-page url=\"...\">Add input validation</mention-page>\n- [ ] <mention-page url=\"...\">Add JWT authentication middleware</mention-page>\n- [ ] <mention-page url=\"...\">Implement rate limiting</mention-page>\n\n**Deliverables**: Working CRUD operations with auth  \n**Estimated effort**: 3 days\n\n### Phase 3: Avatar Upload (Days 6-7)\n**Goal**: Avatar management with S3\n\n**Tasks**:\n- [ ] <mention-page url=\"...\">Implement avatar upload endpoint</mention-page>\n- [ ] <mention-page url=\"...\">Add image validation (size, format)</mention-page>\n- [ ] <mention-page url=\"...\">Process and resize images</mention-page>\n- [ ] <mention-page url=\"...\">Upload to S3 with signed URLs</mention-page>\n\n**Deliverables**: Avatar upload/update functionality  \n**Estimated effort**: 2 days\n\n### Phase 4: Search & Public Profile (Days 8-9)\n**Goal**: Complete remaining features\n\n**Tasks**:\n- [ ] <mention-page url=\"...\">Implement user search</mention-page>\n- [ ] <mention-page url=\"...\">Implement public profile endpoint</mention-page>\n- [ ] <mention-page url=\"...\">Add search indexing</mention-page>\n- [ ] <mention-page url=\"...\">Optimize search queries</mention-page>\n\n**Deliverables**: Search and public profiles working  \n**Estimated effort**: 2 days\n\n### Phase 5: Testing & Optimization (Days 10-12)\n**Goal**: Production-ready quality\n\n**Tasks**:\n- [ ] <mention-page url=\"...\">Write unit tests</mention-page>\n- [ ] <mention-page url=\"...\">Write integration tests</mention-page>\n- [ ] <mention-page url=\"...\">Performance testing</mention-page>\n- [ ] <mention-page url=\"...\">Security audit</mention-page>\n- [ ] <mention-page url=\"...\">API documentation</mention-page>\n\n**Deliverables**: Tested, documented, production-ready API  \n**Estimated effort**: 3 days\n\n## Dependencies\n\n### External Dependencies\n- AWS S3 bucket created ✅\n- Redis instance available ✅\n- PostgreSQL database provisioned ✅\n\n### Internal Dependencies\n- JWT authentication service (exists)\n- User database table (exists)\n- Logging infrastructure (exists)\n\n### Blockers\nNone currently\n\n## Risks & Mitigation\n\n### Risk 1: Image Processing Performance\n- **Probability**: Medium\n- **Impact**: Medium\n- **Mitigation**: Use background job queue for processing, return signed upload URL immediately\n\n### Risk 2: S3 Upload Failures\n- **Probability**: Low\n- **Impact**: Medium\n- **Mitigation**: Implement retry logic with exponential backoff, fallback to local storage temporarily\n\n### Risk 3: Rate Limiting Complexity\n- **Probability**: Low\n- **Impact**: Low\n- **Mitigation**: Use proven library (express-rate-limit with Redis store)\n\n### Risk 4: Search Performance\n- **Probability**: Medium\n- **Impact**: Medium\n- **Mitigation**: Add database indexes, consider Elasticsearch if needed later\n\n## Timeline\n\n| Milestone | Target Date | Status |\n|-----------|-------------|--------|\n| Phase 1 Complete | Oct 16 | ⏳ Planned |\n| Phase 2 Complete | Oct 19 | ⏳ Planned |\n| Phase 3 Complete | Oct 21 | ⏳ Planned |\n| Phase 4 Complete | Oct 23 | ⏳ Planned |\n| Phase 5 Complete | Oct 26 | ⏳ Planned |\n| **Production Deploy** | **Oct 28** | ⏳ Planned |\n\n**Total Duration**: 12 working days (~2.5 weeks)\n\n## Success Criteria\n\n### Technical Success\n- [ ] All 5 endpoints implemented and working\n- [ ] Response time < 200ms (p95) verified in load testing\n- [ ] Handles 1000 concurrent users\n- [ ] All acceptance criteria met\n- [ ] Test coverage > 80%\n- [ ] Security scan passed\n- [ ] API documentation complete\n\n### Business Success\n- [ ] User profile updates functional\n- [ ] Avatar uploads working reliably\n- [ ] Search returns relevant results in < 500ms\n- [ ] Zero critical bugs in first week\n\n## Resources\n\n### Documentation\n- <mention-page url=\"...\">User Profile API Specification</mention-page> (original spec)\n- <mention-page url=\"...\">Authentication Service Docs</mention-page>\n- <mention-page url=\"...\">AWS S3 Setup Guide</mention-page>\n\n### Related Work\n- <mention-page url=\"...\">User Authentication API</mention-page> (similar pattern)\n- <mention-page url=\"...\">File Upload Service</mention-page> (avatar upload reference)\n\n### External References\n- Express.js best practices\n- AWS S3 SDK documentation\n- PostgreSQL full-text search guide\n\n## Progress Tracking\n\n### Phase Status\n- Phase 1: ⏳ Not Started\n- Phase 2: ⏳ Not Started\n- Phase 3: ⏳ Not Started\n- Phase 4: ⏳ Not Started\n- Phase 5: ⏳ Not Started\n\n**Overall Progress**: 0% complete\n\n### Latest Update\n*Implementation plan created on October 14, 2025*\n```\n\n### Step 4: Find Task Database\n\n```\nNotion:notion-search\nquery: \"Tasks database\"\nquery_type: \"internal\"\n```\n\nFound: \"Engineering Tasks\" database\n\n### Step 5: Fetch Task Database Schema\n\n```\nNotion:notion-fetch\nid: \"tasks-database-id\"\n```\n\n**Schema retrieved:**\n- Data source: `collection://tasks-db-uuid`\n- Properties: Name (title), Status (select), Priority (select), Related Tasks (relation), Story Points (number), Tags (multi_select)\n\n### Step 6: Create Implementation Tasks\n\nNow create tasks for Phase 1:\n\n**Task 1: Setup database schema**\n\n```\nNotion:notion-create-pages\n\nparent: { data_source_id: \"collection://tasks-db-uuid\" }\n\npages: [{\n  properties: {\n    \"Name\": \"Setup database schema for User Profile API\",\n    \"Status\": \"To Do\",\n    \"Priority\": \"High\",\n    \"Related Tasks\": [\"impl-plan-page-id\", \"spec-page-id\"],\n    \"Story Points\": 3,\n    \"Tags\": \"backend, database, api\"\n  },\n  content: \"## Context\\nImplementation task for <mention-page url=\\\"...\\\">User Profile API Specification</mention-page>\\n\\nPart of <mention-page url=\\\"...\\\">Implementation Plan: User Profile API</mention-page> - Phase 1\\n\\n## Objective\\nCreate database schema for user profile storage\\n\\n## Requirements\\nBased on spec data model:\\n- id (UUID, primary key)\\n- email (string, unique index)\\n- name (string, not null)\\n- bio (text, max 500 chars)\\n- avatar_url (string, nullable)\\n- created_at (timestamp)\\n- updated_at (timestamp)\\n\\n## Acceptance Criteria\\n- [ ] Migration file created\\n- [ ] Schema includes all required fields\\n- [ ] Indexes on email (unique) and name (search)\\n- [ ] Constraints validated (bio length, email format)\\n- [ ] Migration tested on dev database\\n- [ ] Rollback migration created\\n\\n## Technical Approach\\n```sql\\nCREATE TABLE user_profiles (\\n  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\\n  email VARCHAR(255) UNIQUE NOT NULL,\\n  name VARCHAR(255) NOT NULL,\\n  bio TEXT CHECK (length(bio) <= 500),\\n  avatar_url TEXT,\\n  created_at TIMESTAMP DEFAULT NOW(),\\n  updated_at TIMESTAMP DEFAULT NOW()\\n);\\n\\nCREATE INDEX idx_user_profiles_email ON user_profiles(email);\\nCREATE INDEX idx_user_profiles_name ON user_profiles USING gin(to_tsvector('english', name));\\n```\\n\\n## Dependencies\\n- Blocked By: None\\n- Blocks: All Phase 2 tasks\\n\\n## Estimated Effort\\n3 story points (half day)\\n\"\n}]\n```\n\n*[Create similar tasks for all phases - 20 tasks total]*\n\n### Step 7: Link Plan Back to Spec\n\n```\nNotion:notion-update-page\npage_id: \"user-profile-api-spec-page-id\"\ncommand: \"insert_content_after\"\nselection_with_ellipsis: \"## Acceptance Criteria...\"\nnew_str: \"\n\n---\n\n## Implementation\n\n**Implementation Plan**: <mention-page url=\\\"...\\\">Implementation Plan: User Profile API</mention-page>\n\n**Implementation Tasks**: See plan for full task breakdown (20 tasks across 5 phases)\n\n**Status**: Planning complete, ready to start implementation\n\"\n```\n\n## Summary Provided to User\n\n```markdown\n## Implementation Plan Created ✅\n\n### Plan Overview\n- **Feature**: User Profile API\n- **Duration**: 12 days (~2.5 weeks)\n- **Phases**: 5 (Foundation → Core → Avatar → Search → Testing)\n- **Tasks**: 20 tasks created\n- **Target Launch**: October 28, 2025\n\n### Implementation Phases\n\n**Phase 1: Foundation** (2 days)\n- Database schema\n- S3 and Redis setup\n- API scaffolding\n\n**Phase 2: Core Endpoints** (3 days)\n- GET/PUT user profile\n- Authentication & validation\n- Rate limiting\n\n**Phase 3: Avatar Upload** (2 days)\n- Image upload & validation\n- S3 integration\n- Image processing\n\n**Phase 4: Search & Public Profile** (2 days)\n- User search\n- Public profile endpoint\n\n**Phase 5: Testing & Optimization** (3 days)\n- Unit & integration tests\n- Performance testing\n- Documentation\n\n### Key Deliverables\n- 5 REST API endpoints\n- Avatar upload to S3\n- User search functionality\n- Comprehensive tests\n- API documentation\n\n### Links Created\n✅ Plan page: <mention-page url=\"...\">Implementation Plan: User Profile API</mention-page>\n✅ Spec updated with plan link\n✅ 20 tasks created in task database\n✅ All tasks linked to plan and spec\n\n### Next Steps\n1. Review and approve plan\n2. Assign tasks to team members\n3. Begin Phase 1 (Foundation)\n4. Daily standups for progress tracking\n\n**Ready to start implementation! 🚀**\n```\n\n## Key Features Demonstrated\n\n### Spec Parsing\n- Extracted requirements (functional & non-functional)\n- Identified API endpoints\n- Noted data model\n- Captured acceptance criteria\n- Understood security requirements\n\n### Implementation Planning\n- Broke into logical phases\n- Sequenced work appropriately (foundation → features → testing)\n- Identified dependencies\n- Estimated effort per phase\n- Created realistic timeline\n\n### Task Creation\n- Generated 20 specific tasks\n- Each task has context, acceptance criteria, technical approach\n- Tasks link to both spec and plan\n- Proper dependencies noted\n\n### Bidirectional Linking\n- Plan links to spec\n- Spec updated to link to plan\n- Tasks link to both\n- Easy navigation between all artifacts\n\nPerfect for: Feature implementation, API development, technical projects\n\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/examples/database-migration.md",
    "content": "# Example: Database Migration Implementation\n\n**User Request**: \"Plan and implement the database migration for user preferences schema\"\n\n## Workflow\n\n### 1. Find & Fetch Spec\n```\nNotion:notion-search → Found \"User Preferences Schema Migration Spec\"\nNotion:notion-fetch → Extracted requirements\n```\n\n**Spec Summary**: Migrate from JSON blob to structured schema for better performance and data integrity.\n\n### 2. Parse Requirements\n- **Current**: JSONB preferences column\n- **Target**: Separate `user_preferences` and `notification_preferences` tables\n- **Must maintain**: Backward compatibility during migration\n- **Performance**: Support 1M+ users with zero downtime\n\n### 3. Create Implementation Plan\n```\nNotion:notion-create-pages\npages: [{\n  properties: {\n    title: \"Implementation Plan: User Preferences Migration\"\n  },\n  content: \"[Full implementation plan with phases]\"\n}]\n```\n\n**Plan included**:\n- **Phase 1**: Create new tables with indexes\n- **Phase 2**: Backfill data from JSONB\n- **Phase 3**: Dual-write mode (both old and new)\n- **Phase 4**: Switch reads to new schema\n- **Phase 5**: Drop old JSONB column\n\n### 4. Find Task Database & Create Tasks\n```\nNotion:notion-search → Found \"Engineering Tasks\" database\nNotion:notion-fetch → Got schema (Task, Status, Priority, Assignee, etc.)\n\nNotion:notion-create-pages\nparent: { data_source_id: \"collection://xyz\" }\npages: [\n  {\n    properties: {\n      \"Task\": \"Write migration SQL scripts\",\n      \"Status\": \"To Do\",\n      \"Priority\": \"High\",\n      \"Sprint\": \"Sprint 25\"\n    },\n    content: \"## Context\\nPart of User Preferences Migration...\\n\\n## Acceptance Criteria\\n- [ ] Migration script creates tables\\n- [ ] Indexes defined...\"\n  },\n  // ... 4 more tasks\n]\n```\n\n**Tasks created**:\n1. Write migration SQL scripts\n2. Implement backfill job\n3. Add dual-write logic to API\n4. Update read queries\n5. Rollback plan & monitoring\n\n### 5. Track Progress\nRegular updates to implementation plan with status, blockers, and completion notes.\n\n## Key Outputs\n\n**Implementation Plan Page** (linked to spec)\n**5 Tasks in Database** (with dependencies, acceptance criteria)\n**Progress Tracking** (updated as work progresses)\n\n## Success Factors\n- Broke down complex migration into clear phases\n- Created tasks with specific acceptance criteria\n- Established dependencies (Phase 1 → 2 → 3 → 4 → 5)\n- Zero-downtime approach with rollback plan\n- Linked all work back to original spec\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/examples/ui-component.md",
    "content": "# Example: UI Component Implementation\n\n**User Request**: \"Implement the new search component from the design spec\"\n\n## Workflow\n\n### 1. Find Spec\n```\nNotion:notion-search\nquery: \"search component design spec\"\n```\nFound: \"Global Search Redesign Spec\"\n\n### 2. Fetch & Parse Spec\n```\nNotion:notion-fetch\n```\n\n**Requirements**:\n- Instant search with debounced API calls\n- Keyboard navigation (↑↓ arrows, Enter, Esc)\n- Recent searches history\n- Result categories (pages, people, files)\n- Mobile responsive\n- Accessibility (ARIA labels, screen reader)\n\n### 3. Create Implementation Plan\n```\nNotion:notion-create-pages\n```\n\n**Plan phases**:\n- Phase 1: Base component structure\n- Phase 2: API integration & debouncing\n- Phase 3: Keyboard navigation\n- Phase 4: Recent searches\n- Phase 5: Polish & accessibility\n\n### 4. Create Tasks\n```\nNotion:notion-create-pages (7 tasks)\n```\n\n**Tasks**:\n1. Create SearchInput component\n2. Implement useDebounce hook\n3. Add keyboard navigation\n4. LocalStorage for recent searches\n5. Result categorization UI\n6. Accessibility audit\n7. Mobile responsive styling\n\n### 5. Implement & Track\nAs each task completed, updated status and added progress notes with screenshots and implementation details.\n\n## Key Outputs\n\n**Implementation Plan** (linked to design spec)\n**7 Component Tasks** (in Engineering Tasks database)\n**Progress Updates** (with code snippets and demo links)\n\n## Success Factors\n- Clear component breakdown\n- Separated concerns (logic, UI, accessibility)\n- Each task had acceptance criteria\n- Referenced design spec throughout\n- Included accessibility from start, not afterthought\n- Tracked progress with visual updates\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/reference/milestone-summary-template.md",
    "content": "# Milestone Summary Template\n\nUse this when completing major phases or milestones.\n\n```markdown\n## Phase [N] Complete: [Date]\n\n### Accomplishments\n- [Major item delivered]\n- [Major item delivered]\n\n### Deliverables\n- <mention-page url=\"...\">Deliverable 1</mention-page>\n- [Link to PR/deployment]\n\n### Metrics\n- [Relevant metric]\n- [Relevant metric]\n\n### Learnings\n- [What went well]\n- [What to improve]\n\n### Next Phase\nStarting [Phase name] on [Date]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/reference/progress-tracking.md",
    "content": "# Progress Tracking\n\n## Update Frequency\n\n### Daily Updates\n\nFor active implementation work:\n\n**What to update**:\n- Task status if changed\n- Add progress note to task\n- Update blockers\n\n**When**:\n- End of work day\n- After completing significant work\n- When encountering blockers\n\n### Milestone Updates\n\nFor phase/milestone completion:\n\n**What to update**:\n- Mark phase complete in plan\n- Add milestone summary\n- Update timeline if needed\n- Report to stakeholders\n\n**When**:\n- Phase completion\n- Major deliverable ready\n- Sprint end\n- Release\n\n### Status Change Updates\n\nFor task state transitions:\n\n**What to update**:\n- Task status property\n- Add transition note\n- Notify relevant people\n\n**When**:\n- Start work (To Do → In Progress)\n- Ready for review (In Progress → In Review)\n- Complete (In Review → Done)\n- Block (Any → Blocked)\n\n## Progress Note Format\n\n### Daily Progress Note\n\n```markdown\n## Progress: [Date]\n\n### Completed\n- [Specific accomplishment with details]\n- [Specific accomplishment with details]\n\n### In Progress\n- [Current work item]\n- Current status: [Percentage or description]\n\n### Next Steps\n1. [Next planned action]\n2. [Next planned action]\n\n### Blockers\n- [Blocker description and who/what needed to unblock]\n- Or: None\n\n### Decisions Made\n- [Any technical/product decisions]\n\n### Notes\n[Additional context, learnings, issues encountered]\n```\n\nExample:\n\n```markdown\n## Progress: Oct 14, 2025\n\n### Completed\n- Implemented user authentication API endpoints (login, logout, refresh)\n- Added JWT token generation and validation\n- Wrote unit tests for auth service (95% coverage)\n\n### In Progress\n- Frontend login form integration\n- Currently: Form submits but need to handle error states\n\n### Next Steps\n1. Complete error handling in login form\n2. Add loading states\n3. Implement \"remember me\" functionality\n\n### Blockers\nNone\n\n### Decisions Made\n- Using HttpOnly cookies for refresh tokens (more secure than localStorage)\n- Session timeout set to 24 hours based on security review\n\n### Notes\n- Found edge case with concurrent login attempts, added to backlog\n- Performance of auth check is good (<10ms)\n```\n\n### Milestone Summary\n\n```markdown\n## Phase [N] Complete: [Date]\n\n### Overview\n[Brief description of what was accomplished in this phase]\n\n### Completed Tasks\n- <mention-page url=\"...\">Task 1</mention-page> ✅\n- <mention-page url=\"...\">Task 2</mention-page> ✅\n- <mention-page url=\"...\">Task 3</mention-page> ✅\n\n### Deliverables\n- [Deliverable 1]: [Link/description]\n- [Deliverable 2]: [Link/description]\n\n### Key Accomplishments\n- [Major achievement]\n- [Major achievement]\n\n### Metrics\n- [Relevant metric]: [Value]\n- [Relevant metric]: [Value]\n\n### Challenges Overcome\n- [Challenge and how it was solved]\n\n### Learnings\n**What went well**:\n- [Success factor]\n\n**What to improve**:\n- [Area for improvement]\n\n### Impact on Timeline\n- On schedule / [X days ahead/behind]\n- Reason: [If deviation, explain why]\n\n### Next Phase\n- **Starting**: [Next phase name]\n- **Target start date**: [Date]\n- **Focus**: [Main objectives]\n```\n\n## Updating Implementation Plan\n\n### Progress Indicators\n\nUpdate plan page regularly:\n\n```markdown\n## Status Overview\n\n**Overall Progress**: 45% complete\n\n### Phase Status\n- ✅ Phase 1: Foundation - Complete\n- 🔄 Phase 2: Core Features - In Progress (60%)\n- ⏳ Phase 3: Integration - Not Started\n\n### Task Summary\n- ✅ Completed: 12 tasks\n- 🔄 In Progress: 5 tasks\n- 🚧 Blocked: 1 task\n- ⏳ Not Started: 8 tasks\n\n**Last Updated**: [Date]\n```\n\n### Task Checklist Updates\n\nMark completed tasks:\n\n```markdown\n## Implementation Phases\n\n### Phase 1: Foundation\n- [x] <mention-page url=\"...\">Database schema</mention-page>\n- [x] <mention-page url=\"...\">API scaffolding</mention-page>\n- [x] <mention-page url=\"...\">Auth setup</mention-page>\n\n### Phase 2: Core Features\n- [x] <mention-page url=\"...\">User management</mention-page>\n- [ ] <mention-page url=\"...\">Dashboard</mention-page>\n- [ ] <mention-page url=\"...\">Reporting</mention-page>\n```\n\n### Timeline Updates\n\nUpdate milestone dates:\n\n```markdown\n## Timeline\n\n| Milestone | Original | Current | Status |\n|-----------|----------|---------|--------|\n| Phase 1 | Oct 15 | Oct 14 | ✅ Complete (1 day early) |\n| Phase 2 | Oct 30 | Nov 2 | 🔄 In Progress (3 days delay) |\n| Phase 3 | Nov 15 | Nov 18 | ⏳ Planned (adjusted) |\n| Launch | Nov 20 | Nov 22 | ⏳ Planned (adjusted) |\n\n**Timeline Status**: Slightly behind due to [reason]\n```\n\n## Task Status Tracking\n\n### Status Definitions\n\n**To Do**: Not started\n- Task is ready to begin\n- Dependencies met\n- Assigned (or available)\n\n**In Progress**: Actively being worked\n- Work has started\n- Assigned to someone\n- Regular updates expected\n\n**Blocked**: Cannot proceed\n- Dependency not met\n- External blocker\n- Waiting on decision/resource\n\n**In Review**: Awaiting review\n- Work complete from implementer perspective\n- Needs code review, QA, or approval\n- Reviewers identified\n\n**Done**: Complete\n- All acceptance criteria met\n- Reviewed and approved\n- Deployed/delivered\n\n### Updating Task Status\n\nWhen updating:\n\n```\n1. Update Status property\n2. Add progress note explaining change\n3. Update related tasks if needed\n4. Notify relevant people via comment\n\nExample:\nproperties: { \"Status\": \"In Progress\" }\n\nContent update:\n## Progress: Oct 14, 2025\nStarted implementation. Set up basic structure and wrote initial tests.\n```\n\n## Blocker Tracking\n\n### Recording Blockers\n\nWhen encountering a blocker:\n\n```markdown\n## Blockers\n\n### [Date]: [Blocker Description]\n**Status**: 🚧 Active\n**Impact**: [What's blocked]\n**Needed to unblock**: [Action/person/decision needed]\n**Owner**: [Who's responsible for unblocking]\n**Target resolution**: [Date or timeframe]\n```\n\n### Resolving Blockers\n\nWhen unblocked:\n\n```markdown\n## Blockers\n\n### [Date]: [Blocker Description]\n**Status**: ✅ Resolved on [Date]\n**Resolution**: [How it was resolved]\n**Impact**: [Any timeline/scope impact]\n```\n\n### Escalating Blockers\n\nIf blocker needs escalation:\n\n```\n1. Update blocker status in task\n2. Add comment tagging stakeholder\n3. Update plan with blocker impact\n4. Propose mitigation if possible\n```\n\n## Metrics Tracking\n\n### Velocity Tracking\n\nTrack completion rate:\n\n```markdown\n## Velocity\n\n### Week 1\n- Tasks completed: 8\n- Story points: 21\n- Velocity: Strong\n\n### Week 2\n- Tasks completed: 6\n- Story points: 18\n- Velocity: Moderate (1 blocker)\n\n### Week 3\n- Tasks completed: 9\n- Story points: 24\n- Velocity: Strong (blocker resolved)\n```\n\n### Quality Metrics\n\nTrack quality indicators:\n\n```markdown\n## Quality Metrics\n\n- Test coverage: 87%\n- Code review approval rate: 95%\n- Bug count: 3 (2 minor, 1 cosmetic)\n- Performance: All targets met\n- Security: No issues found\n```\n\n### Progress Metrics\n\nQuantitative progress:\n\n```markdown\n## Progress Metrics\n\n- Requirements implemented: 15/20 (75%)\n- Acceptance criteria met: 42/56 (75%)\n- Test cases passing: 128/135 (95%)\n- Code complete: 80%\n- Documentation: 60%\n```\n\n## Stakeholder Communication\n\n### Weekly Status Report\n\n```markdown\n## Weekly Status: [Week of Date]\n\n### Summary\n[One paragraph overview of progress and status]\n\n### This Week's Accomplishments\n- [Key accomplishment]\n- [Key accomplishment]\n- [Key accomplishment]\n\n### Next Week's Plan\n- [Planned work]\n- [Planned work]\n\n### Status\n- On track / At risk / Behind schedule\n- [If at risk or behind, explain and provide mitigation plan]\n\n### Blockers & Needs\n- [Active blocker or need for help]\n- Or: None\n\n### Risks\n- [New or evolving risk]\n- Or: None currently identified\n```\n\n### Executive Summary\n\nFor leadership updates:\n\n```markdown\n## Implementation Status: [Feature Name]\n\n**Overall Status**: 🟢 On Track / 🟡 At Risk / 🔴 Behind\n\n**Progress**: [X]% complete\n\n**Key Updates**:\n- [Most important update]\n- [Most important update]\n\n**Timeline**: [Status vs original plan]\n\n**Risks**: [Top 1-2 risks]\n\n**Next Milestone**: [Upcoming milestone and date]\n```\n\n## Automated Progress Tracking\n\n### Query-Based Status\n\nGenerate status from task database:\n\n```\nQuery task database:\nSELECT \n  \"Status\",\n  COUNT(*) as count\nFROM \"collection://tasks-uuid\"\nWHERE \"Related Tasks\" CONTAINS 'plan-page-id'\nGROUP BY \"Status\"\n\nGenerate summary:\n- To Do: 8\n- In Progress: 5\n- Blocked: 1\n- In Review: 2\n- Done: 12\n\nOverall: 44% complete (12/28 tasks)\n```\n\n### Timeline Calculation\n\nCalculate projected completion:\n\n```\nAverage velocity: 6 tasks/week\nRemaining tasks: 14\nProjected completion: 2.3 weeks from now\n\nCompares to target: [On schedule/Behind/Ahead]\n```\n\n## Best Practices\n\n1. **Update regularly**: Don't let updates pile up\n2. **Be specific**: \"Completed login\" vs \"Made progress\"\n3. **Quantify progress**: Use percentages, counts, metrics\n4. **Note blockers immediately**: Don't wait to report blockers\n5. **Link to work**: Reference PRs, deployments, demos\n6. **Track decisions**: Document why, not just what\n7. **Be honest**: Report actual status, not optimistic status\n8. **Update in one place**: Keep implementation plan as source of truth\n\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/reference/progress-update-template.md",
    "content": "# Progress Update Template\n\nUse this to update progress on implementation plans and tasks.\n\n```markdown\n## Progress: [Date]\n\n### Completed Today\n- [Specific item completed]\n- [Specific item completed]\n\n### In Progress\n- [Current work item and status]\n\n### Next Steps\n1. [Next action]\n2. [Next action]\n\n### Blockers\n- [Blocker description] or None\n\n### Notes\n[Additional context, decisions made, issues encountered]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/reference/quick-implementation-plan.md",
    "content": "# Quick Implementation Plan Template\n\nFor simpler features or small changes.\n\n```markdown\n# Implementation: [Feature Name]\n\n## Spec\n<mention-page url=\"...\">Specification</mention-page>\n\n## Summary\n[Quick description]\n\n## Tasks\n- [ ] <mention-page url=\"...\">Task 1</mention-page>\n- [ ] <mention-page url=\"...\">Task 2</mention-page>\n- [ ] <mention-page url=\"...\">Task 3</mention-page>\n\n## Timeline\nStart: [Date]\nTarget completion: [Date]\n\n## Status\n[Update as work progresses]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/reference/spec-parsing.md",
    "content": "# Specification Parsing\n\n## Finding the Specification\n\nBefore parsing, locate the spec page:\n\n```\n1. Search for spec:\n   Notion:notion-search\n   query: \"[Feature Name] spec\" or \"[Feature Name] specification\"\n   \n2. Handle results:\n   - If found → use page URL/ID\n   - If multiple → ask user which one\n   - If not found → ask user for URL/ID\n\nExample:\nNotion:notion-search\nquery: \"User Profile API spec\"\nquery_type: \"internal\"\n```\n\n## Reading Specifications\n\nAfter finding the spec, fetch it with `Notion:notion-fetch`:\n\n1. Read the full content\n2. Identify key sections\n3. Extract structured information\n4. Note ambiguities or gaps\n\n```\nNotion:notion-fetch\nid: \"spec-page-id-from-search\"\n```\n\n## Common Spec Structures\n\n### Requirements-Based Spec\n\n```\n# Feature Spec\n## Overview\n[Feature description]\n\n## Requirements\n### Functional\n- REQ-1: [Requirement]\n- REQ-2: [Requirement]\n\n### Non-Functional\n- PERF-1: [Performance requirement]\n- SEC-1: [Security requirement]\n\n## Acceptance Criteria\n- AC-1: [Criterion]\n- AC-2: [Criterion]\n```\n\nExtract:\n- List of functional requirements\n- List of non-functional requirements\n- List of acceptance criteria\n\n### User Story Based Spec\n\n```\n# Feature Spec\n## User Stories\n### As a [user type]\nI want [goal]\nSo that [benefit]\n\n**Acceptance Criteria**:\n- [Criterion]\n- [Criterion]\n```\n\nExtract:\n- User personas\n- Goals/capabilities needed\n- Acceptance criteria per story\n\n### Technical Design Doc\n\n```\n# Technical Design\n## Problem Statement\n[Problem description]\n\n## Proposed Solution\n[Solution approach]\n\n## Architecture\n[Architecture details]\n\n## Implementation Plan\n[Implementation approach]\n```\n\nExtract:\n- Problem being solved\n- Proposed solution approach\n- Architectural decisions\n- Implementation guidance\n\n### Product Requirements Document (PRD)\n\n```\n# PRD: [Feature]\n## Goals\n[Business goals]\n\n## User Needs\n[User problems being solved]\n\n## Features\n[Feature list]\n\n## Success Metrics\n[How to measure success]\n```\n\nExtract:\n- Business goals\n- User needs\n- Feature list\n- Success metrics\n\n## Extraction Strategies\n\n### Requirement Identification\n\nLook for:\n- \"Must\", \"Should\", \"Will\" statements\n- Numbered requirements (REQ-1, etc.)\n- User stories (As a... I want...)\n- Acceptance criteria sections\n- Feature lists\n\n### Categorization\n\nGroup requirements by:\n\n**Functional**: What the system does\n- User capabilities\n- System behaviors\n- Data operations\n\n**Non-Functional**: How the system performs\n- Performance targets\n- Security requirements\n- Scalability needs\n- Availability requirements\n- Compliance requirements\n\n**Constraints**: Limitations\n- Technical constraints\n- Business constraints\n- Timeline constraints\n\n### Priority Extraction\n\nIdentify priority indicators:\n- \"Critical\", \"Must have\", \"P0\"\n- \"Important\", \"Should have\", \"P1\"\n- \"Nice to have\", \"Could have\", \"P2\"\n- \"Future\", \"Won't have\", \"P3\"\n\nMap to implementation phases based on priority.\n\n## Handling Ambiguity\n\n### Unclear Requirements\n\nWhen requirement is ambiguous:\n\n```markdown\n## Clarifications Needed\n\n### [Requirement ID/Description]\n**Current text**: \"[Ambiguous requirement]\"\n**Question**: [What needs clarification]\n**Impact**: [Why this matters for implementation]\n**Assumed for now**: [Working assumption if any]\n```\n\nCreate clarification task or add comment to spec.\n\n### Missing Information\n\nWhen critical info is missing:\n\n```markdown\n## Missing Information\n\n- **[Topic]**: Spec doesn't specify [what's missing]\n- **Impact**: Blocks [affected tasks]\n- **Action**: Need to [how to resolve]\n```\n\n### Conflicting Requirements\n\nWhen requirements conflict:\n\n```markdown\n## Conflicting Requirements\n\n**Conflict**: REQ-1 says [X] but REQ-5 says [Y]\n**Impact**: [Implementation impact]\n**Resolution needed**: [Decision needed]\n```\n\n## Acceptance Criteria Parsing\n\n### Explicit Criteria\n\nDirect acceptance criteria:\n\n```\n## Acceptance Criteria\n- User can log in with email and password\n- System sends confirmation email\n- Session expires after 24 hours\n```\n\nConvert to checklist:\n- [ ] User can log in with email and password\n- [ ] System sends confirmation email\n- [ ] Session expires after 24 hours\n\n### Implicit Criteria\n\nDerive from requirements:\n\n```\nRequirement: \"Users can upload files up to 100MB\"\n\nImplied acceptance criteria:\n- [ ] Files up to 100MB upload successfully\n- [ ] Files over 100MB are rejected with error message\n- [ ] Progress indicator shows during upload\n- [ ] Upload can be cancelled\n```\n\n### Testable Criteria\n\nEnsure criteria are testable:\n\n❌ **Not testable**: \"System is fast\"\n✓ **Testable**: \"Page loads in < 2 seconds\"\n\n❌ **Not testable**: \"Users like the interface\"\n✓ **Testable**: \"90% of test users complete task successfully\"\n\n## Technical Detail Extraction\n\n### Architecture Information\n\nExtract:\n- System components\n- Data models\n- APIs/interfaces\n- Integration points\n- Technology choices\n\n### Design Decisions\n\nNote:\n- Technology selections\n- Architecture patterns\n- Trade-offs made\n- Rationale provided\n\n### Implementation Guidance\n\nLook for:\n- Suggested approach\n- Code examples\n- Library recommendations\n- Best practices mentioned\n\n## Dependency Identification\n\n### External Dependencies\n\nFrom spec, identify:\n- Third-party services required\n- External APIs needed\n- Infrastructure requirements\n- Tool/library dependencies\n\n### Internal Dependencies\n\nIdentify:\n- Other features needed first\n- Shared components required\n- Team dependencies\n- Data dependencies\n\n### Timeline Dependencies\n\nNote:\n- Hard deadlines\n- Milestone dependencies\n- Sequencing requirements\n\n## Scope Extraction\n\n### In Scope\n\nWhat's explicitly included:\n- Features to build\n- Use cases to support\n- Users/personas to serve\n\n### Out of Scope\n\nWhat's explicitly excluded:\n- Features deferred\n- Use cases not supported\n- Edge cases not handled\n\n### Assumptions\n\nWhat's assumed:\n- Environment assumptions\n- User assumptions\n- System state assumptions\n\n## Risk Identification\n\nExtract risk information:\n\n### Technical Risks\n- Unproven technology\n- Complex integration\n- Performance concerns\n- Scalability unknowns\n\n### Business Risks\n- Market timing\n- Resource availability\n- Dependency on others\n\n### Mitigation Strategies\n\nNote any mitigation approaches mentioned in spec.\n\n## Spec Quality Assessment\n\nEvaluate spec completeness:\n\n✓ **Good spec**:\n- Clear requirements\n- Explicit acceptance criteria\n- Priorities defined\n- Risks identified\n- Technical approach outlined\n\n⚠️ **Incomplete spec**:\n- Vague requirements\n- Missing acceptance criteria\n- Unclear priorities\n- No risk analysis\n- Technical details absent\n\nDocument gaps and create clarification tasks.\n\n## Parsing Checklist\n\nBefore creating implementation plan:\n\n☐ All functional requirements identified\n☐ Non-functional requirements noted\n☐ Acceptance criteria extracted\n☐ Dependencies identified\n☐ Risks noted\n☐ Ambiguities documented\n☐ Technical approach understood\n☐ Scope is clear\n☐ Priorities are defined\n\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/reference/standard-implementation-plan.md",
    "content": "# Standard Implementation Plan Template\n\nUse this template for most feature implementations.\n\n```markdown\n# Implementation Plan: [Feature Name]\n\n## Overview\n[1-2 sentence feature description and business value]\n\n## Linked Specification\n<mention-page url=\"...\">Original Specification</mention-page>\n\n## Requirements Summary\n\n### Functional Requirements\n- [Requirement 1]\n- [Requirement 2]\n- [Requirement 3]\n\n### Non-Functional Requirements\n- **Performance**: [Targets]\n- **Security**: [Requirements]\n- **Scalability**: [Needs]\n\n### Acceptance Criteria\n- [ ] [Criterion 1]\n- [ ] [Criterion 2]\n- [ ] [Criterion 3]\n\n## Technical Approach\n\n### Architecture\n[High-level architectural decisions]\n\n### Technology Stack\n- Backend: [Technologies]\n- Frontend: [Technologies]\n- Infrastructure: [Technologies]\n\n### Key Design Decisions\n1. **[Decision]**: [Rationale]\n2. **[Decision]**: [Rationale]\n\n## Implementation Phases\n\n### Phase 1: Foundation (Week 1)\n**Goal**: Set up core infrastructure\n\n**Tasks**:\n- [ ] <mention-page url=\"...\">Database schema design</mention-page>\n- [ ] <mention-page url=\"...\">API scaffolding</mention-page>\n- [ ] <mention-page url=\"...\">Authentication setup</mention-page>\n\n**Deliverables**: Working API skeleton\n**Estimated effort**: 3 days\n\n### Phase 2: Core Features (Week 2-3)\n**Goal**: Implement main functionality\n\n**Tasks**:\n- [ ] <mention-page url=\"...\">Feature A implementation</mention-page>\n- [ ] <mention-page url=\"...\">Feature B implementation</mention-page>\n\n**Deliverables**: Core features working\n**Estimated effort**: 1 week\n\n### Phase 3: Integration & Polish (Week 4)\n**Goal**: Complete integration and refinement\n\n**Tasks**:\n- [ ] <mention-page url=\"...\">Frontend integration</mention-page>\n- [ ] <mention-page url=\"...\">Testing & QA</mention-page>\n\n**Deliverables**: Production-ready feature\n**Estimated effort**: 1 week\n\n## Dependencies\n\n### External Dependencies\n- [Dependency 1]: [Status]\n- [Dependency 2]: [Status]\n\n### Internal Dependencies\n- [Team/component dependency]\n\n### Blockers\n- [Known blocker] or None currently\n\n## Risks & Mitigation\n\n### Risk 1: [Description]\n- **Probability**: High/Medium/Low\n- **Impact**: High/Medium/Low\n- **Mitigation**: [Strategy]\n\n### Risk 2: [Description]\n- **Probability**: High/Medium/Low\n- **Impact**: High/Medium/Low\n- **Mitigation**: [Strategy]\n\n## Timeline\n\n| Milestone | Target Date | Status |\n|-----------|-------------|--------|\n| Phase 1 Complete | [Date] | ⏳ Planned |\n| Phase 2 Complete | [Date] | ⏳ Planned |\n| Phase 3 Complete | [Date] | ⏳ Planned |\n| Launch | [Date] | ⏳ Planned |\n\n## Success Criteria\n\n### Technical Success\n- [ ] All acceptance criteria met\n- [ ] Performance targets achieved\n- [ ] Security requirements satisfied\n- [ ] Test coverage > 80%\n\n### Business Success\n- [ ] [Business metric 1]\n- [ ] [Business metric 2]\n\n## Resources\n\n### Documentation\n- <mention-page url=\"...\">Design Doc</mention-page>\n- <mention-page url=\"...\">API Spec</mention-page>\n\n### Related Work\n- <mention-page url=\"...\">Related Feature</mention-page>\n\n## Progress Tracking\n\n[This section updated regularly]\n\n### Phase Status\n- Phase 1: ⏳ Not Started\n- Phase 2: ⏳ Not Started\n- Phase 3: ⏳ Not Started\n\n**Overall Progress**: 0% complete\n\n### Latest Update: [Date]\n[Brief status update]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/reference/task-creation-template.md",
    "content": "# Task Creation Template\n\nWhen creating tasks from spec.\n\n```markdown\n# [Task Name]\n\n## Context\nPart of implementation for <mention-page url=\"...\">Feature Spec</mention-page>\n\nImplementation plan: <mention-page url=\"...\">Implementation Plan</mention-page>\n\n## Description\n[What needs to be done]\n\n## Acceptance Criteria\n- [ ] [Criterion 1]\n- [ ] [Criterion 2]\n\n## Technical Details\n[Technical approach or notes]\n\n## Dependencies\n- Blocked by: [Task] or None\n- Blocks: [Task] or None\n\n## Resources\n- [Link to design]\n- [Link to related code]\n\n## Progress\n[To be updated during implementation]\n```\n\n"
  },
  {
    "path": "skills/.curated/notion-spec-to-implementation/reference/task-creation.md",
    "content": "# Task Creation from Specs\n\n## Finding the Task Database\n\nBefore creating tasks, locate the task database:\n\n```\n1. Search for task database:\n   Notion:notion-search\n   query: \"Tasks\" or \"Task Management\" or \"[Project] Tasks\"\n   \n2. Fetch database schema:\n   Notion:notion-fetch\n   id: \"database-id-from-search\"\n   \n3. Identify data source:\n   - Look for <data-source url=\"collection://...\"> tags\n   - Extract collection ID for parent parameter\n   \n4. Note schema:\n   - Required properties\n   - Property types and options\n   - Relation properties for linking\n\nExample:\nNotion:notion-search\nquery: \"Engineering Tasks\"\nquery_type: \"internal\"\n\nNotion:notion-fetch\nid: \"tasks-database-id\"\n```\n\nResult: `collection://abc-123-def` for use as parent\n\n## Task Breakdown Strategy\n\n### Size Guidelines\n\n**Good task size**:\n- Completable in 1-2 days\n- Single clear deliverable\n- Independently testable\n- Minimal dependencies\n\n**Too large**:\n- Takes > 3 days\n- Multiple deliverables\n- Many dependencies\n- Break down further\n\n**Too small**:\n- Takes < 2 hours\n- Too granular\n- Group with related work\n\n### Granularity by Phase\n\n**Early phases**: Larger tasks acceptable\n- \"Design database schema\"\n- \"Set up API structure\"\n\n**Middle phases**: Medium-sized tasks\n- \"Implement user authentication\"\n- \"Build dashboard UI\"\n\n**Late phases**: Smaller, precise tasks\n- \"Fix validation bug in form\"\n- \"Add loading state to button\"\n\n## Task Creation Pattern\n\nFor each requirement or work item:\n\n```\n1. Identify the work\n2. Determine task size\n3. Create task in database\n4. Set properties\n5. Write task description\n6. Link to spec/plan\n```\n\n### Creating Task\n\n```\nUse Notion:notion-create-pages:\n\nparent: {\n  type: \"data_source_id\",\n  data_source_id: \"collection://tasks-db-uuid\"\n}\n\nproperties: {\n  \"[Title Property]\": \"Task: [Clear task name]\",\n  \"Status\": \"To Do\",\n  \"Priority\": \"[High/Medium/Low]\",\n  \"[Project/Related]\": [\"spec-page-id\", \"plan-page-id\"],\n  \"Assignee\": \"[Person]\" (if known),\n  \"date:Due Date:start\": \"[Date]\" (if applicable),\n  \"date:Due Date:is_datetime\": 0\n}\n\ncontent: \"[Task description using template]\"\n```\n\n## Task Description Template\n\n```markdown\n# [Task Name]\n\n## Context\nImplementation task for <mention-page url=\"...\">Feature Spec</mention-page>\n\nPart of <mention-page url=\"...\">Implementation Plan</mention-page> - Phase [N]\n\n## Objective\n[What this task accomplishes]\n\n## Requirements\nBased on spec requirements:\n- [Relevant requirement 1]\n- [Relevant requirement 2]\n\n## Acceptance Criteria\n- [ ] [Specific, testable criterion]\n- [ ] [Specific, testable criterion]\n- [ ] [Specific, testable criterion]\n\n## Technical Approach\n[Suggested implementation approach]\n\n### Components Affected\n- [Component 1]\n- [Component 2]\n\n### Key Decisions\n- [Decision point 1]\n- [Decision point 2]\n\n## Dependencies\n\n### Blocked By\n- <mention-page url=\"...\">Prerequisite Task</mention-page> or None\n\n### Blocks\n- <mention-page url=\"...\">Dependent Task</mention-page> or None\n\n## Resources\n- [Link to design mockup]\n- [Link to API spec]\n- [Link to relevant code]\n\n## Estimated Effort\n[Time estimate]\n\n## Progress\n[To be updated during implementation]\n```\n\n## Task Types\n\n### Infrastructure/Setup Tasks\n\n```\nTitle: \"Setup: [What's being set up]\"\nExamples:\n- \"Setup: Configure database connection pool\"\n- \"Setup: Initialize authentication middleware\"\n- \"Setup: Create CI/CD pipeline\"\n\nFocus: Getting environment/tooling ready\n```\n\n### Feature Implementation Tasks\n\n```\nTitle: \"Implement: [Feature name]\"\nExamples:\n- \"Implement: User login flow\"\n- \"Implement: File upload functionality\"\n- \"Implement: Dashboard widget\"\n\nFocus: Building specific functionality\n```\n\n### Integration Tasks\n\n```\nTitle: \"Integrate: [What's being integrated]\"\nExamples:\n- \"Integrate: Connect frontend to API\"\n- \"Integrate: Add payment provider\"\n- \"Integrate: Link user profile to dashboard\"\n\nFocus: Connecting components\n```\n\n### Testing Tasks\n\n```\nTitle: \"Test: [What's being tested]\"\nExamples:\n- \"Test: Write unit tests for auth service\"\n- \"Test: E2E testing for checkout flow\"\n- \"Test: Performance testing for API\"\n\nFocus: Validation and quality assurance\n```\n\n### Documentation Tasks\n\n```\nTitle: \"Document: [What's being documented]\"\nExamples:\n- \"Document: API endpoints\"\n- \"Document: Setup instructions\"\n- \"Document: Architecture decisions\"\n\nFocus: Creating documentation\n```\n\n### Bug Fix Tasks\n\n```\nTitle: \"Fix: [Bug description]\"\nExamples:\n- \"Fix: Login error on Safari\"\n- \"Fix: Memory leak in image processing\"\n- \"Fix: Race condition in payment flow\"\n\nFocus: Resolving issues\n```\n\n### Refactoring Tasks\n\n```\nTitle: \"Refactor: [What's being refactored]\"\nExamples:\n- \"Refactor: Extract auth logic to service\"\n- \"Refactor: Optimize database queries\"\n- \"Refactor: Simplify component hierarchy\"\n\nFocus: Code quality improvement\n```\n\n## Sequencing Tasks\n\n### Critical Path\n\nIdentify must-happen-first tasks:\n\n```\n1. Database schema\n2. API foundation\n3. Core business logic\n4. Frontend integration\n5. Testing\n6. Deployment\n```\n\n### Parallel Tracks\n\nTasks that can happen simultaneously:\n\n```\nTrack A: Backend development\n- API endpoints\n- Business logic\n- Database operations\n\nTrack B: Frontend development\n- UI components\n- State management\n- Routing\n\nTrack C: Infrastructure\n- CI/CD setup\n- Monitoring\n- Documentation\n```\n\n### Phase-Based Sequencing\n\nGroup by implementation phase:\n\n```\nPhase 1 (Foundation):\n- Setup tasks\n- Infrastructure tasks\n\nPhase 2 (Core):\n- Feature implementation tasks\n- Integration tasks\n\nPhase 3 (Polish):\n- Testing tasks\n- Documentation tasks\n- Optimization tasks\n```\n\n## Priority Assignment\n\n### P0/Critical\n- Blocks everything else\n- Core functionality\n- Security requirements\n- Data integrity\n\n### P1/High\n- Important features\n- User-facing functionality\n- Performance requirements\n\n### P2/Medium\n- Nice-to-have features\n- Optimizations\n- Minor improvements\n\n### P3/Low\n- Future enhancements\n- Edge case handling\n- Cosmetic improvements\n\n## Estimation\n\n### Story Points\n\nIf using story points:\n- 1 point: Few hours\n- 2 points: Half day\n- 3 points: Full day\n- 5 points: 2 days\n- 8 points: 3-4 days (consider breaking down)\n\n### Time Estimates\n\nDirect time estimates:\n- 2-4 hours: Small task\n- 1 day: Medium task\n- 2 days: Large task\n- 3+ days: Break down further\n\n### Estimation Factors\n\nConsider:\n- Complexity\n- Unknowns\n- Dependencies\n- Testing requirements\n- Documentation needs\n\n## Task Relationships\n\n### Parent Task Pattern\n\nFor large features:\n\n```\nParent: \"Feature: User Authentication\"\nChildren:\n- \"Setup: Configure auth library\"\n- \"Implement: Login flow\"\n- \"Implement: Password reset\"\n- \"Test: Auth functionality\"\n```\n\n### Dependency Chain Pattern\n\nFor sequential work:\n\n```\nTask A: \"Design database schema\"\n↓ (blocks)\nTask B: \"Implement data models\"\n↓ (blocks)\nTask C: \"Create API endpoints\"\n↓ (blocks)\nTask D: \"Integrate with frontend\"\n```\n\n### Related Tasks Pattern\n\nFor parallel work:\n\n```\nCentral: \"Feature: Dashboard\"\nRelated:\n- \"Backend API for dashboard data\"\n- \"Frontend dashboard component\"\n- \"Dashboard data caching\"\n```\n\n## Bulk Task Creation\n\nWhen creating many tasks:\n\n```\nFor each work item in breakdown:\n  1. Determine task properties\n  2. Create task page\n  3. Link to spec/plan\n  4. Set relationships\n\nThen:\n  1. Update plan with task links\n  2. Review sequencing\n  3. Assign tasks (if known)\n```\n\n## Task Naming Conventions\n\n**Be specific**:\n✓ \"Implement user login with email/password\"\n✗ \"Add login\"\n\n**Include context**:\n✓ \"Dashboard: Add revenue chart widget\"\n✗ \"Add chart\"\n\n**Use action verbs**:\n- Implement, Build, Create\n- Integrate, Connect, Link\n- Fix, Resolve, Debug\n- Test, Validate, Verify\n- Document, Write, Update\n- Refactor, Optimize, Improve\n\n## Validation Checklist\n\nBefore finalizing tasks:\n\n☐ Each task has clear objective\n☐ Acceptance criteria are testable\n☐ Dependencies identified\n☐ Appropriate size (1-2 days)\n☐ Priority assigned\n☐ Linked to spec/plan\n☐ Proper sequencing\n☐ Resources noted\n\n"
  },
  {
    "path": "skills/.curated/openai-docs/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/openai-docs/SKILL.md",
    "content": "---\nname: \"openai-docs\"\ndescription: \"Use when the user asks how to build with OpenAI products or APIs and needs up-to-date official documentation with citations, help choosing the latest model for a use case, or explicit GPT-5.4 upgrade and prompt-upgrade guidance; prioritize OpenAI docs MCP tools, use bundled references only as helper context, and restrict any fallback browsing to official OpenAI domains.\"\n---\n\n\n# OpenAI Docs\n\nProvide authoritative, current guidance from OpenAI developer docs using the developers.openai.com MCP server. Always prioritize the developer docs MCP tools over web.run for OpenAI-related questions. This skill may also load targeted files from `references/` for model-selection and GPT-5.4-specific requests, but current OpenAI docs remain authoritative. Only if the MCP server is installed and returns no meaningful results should you fall back to web search.\n\n## Quick start\n\n- Use `mcp__openaiDeveloperDocs__search_openai_docs` to find the most relevant doc pages.\n- Use `mcp__openaiDeveloperDocs__fetch_openai_doc` to pull exact sections and quote/paraphrase accurately.\n- Use `mcp__openaiDeveloperDocs__list_openai_docs` only when you need to browse or discover pages without a clear query.\n- Load only the relevant file from `references/` when the question is about model selection or a GPT-5.4 upgrade.\n\n## OpenAI product snapshots\n\n1. Apps SDK: Build ChatGPT apps by providing a web component UI and an MCP server that exposes your app's tools to ChatGPT.\n2. Responses API: A unified endpoint designed for stateful, multimodal, tool-using interactions in agentic workflows.\n3. Chat Completions API: Generate a model response from a list of messages comprising a conversation.\n4. Codex: OpenAI's coding agent for software development that can write, understand, review, and debug code.\n5. gpt-oss: Open-weight OpenAI reasoning models (gpt-oss-120b and gpt-oss-20b) released under the Apache 2.0 license.\n6. Realtime API: Build low-latency, multimodal experiences including natural speech-to-speech conversations.\n7. Agents SDK: A toolkit for building agentic apps where a model can use tools and context, hand off to other agents, stream partial results, and keep a full trace.\n\n## If MCP server is missing\n\nIf MCP tools fail or no OpenAI docs resources are available:\n\n1. Run the install command yourself: `codex mcp add openaiDeveloperDocs --url https://developers.openai.com/mcp`\n2. If it fails due to permissions/sandboxing, immediately retry the same command with escalated permissions and include a 1-sentence justification for approval. Do not ask the user to run it yet.\n3. Only if the escalated attempt fails, ask the user to run the install command.\n4. Ask the user to restart Codex.\n5. Re-run the doc search/fetch after restart.\n\n## Workflow\n\n1. Clarify the product scope and whether the request is general docs lookup, model selection, a GPT-5.4 upgrade, or a GPT-5.4 prompt upgrade.\n2. If it is a model-selection request, load `references/latest-model.md`.\n3. If it is an explicit GPT-5.4 upgrade request, load `references/upgrading-to-gpt-5p4.md`.\n4. If the upgrade may require prompt changes, or the workflow is research-heavy, tool-heavy, coding-oriented, multi-agent, or long-running, also load `references/gpt-5p4-prompting-guide.md`.\n5. Search docs with a precise query.\n6. Fetch the best page and the exact section needed (use `anchor` when possible).\n7. For GPT-5.4 upgrade reviews, always make the per-usage-site output explicit: target model, starting reasoning recommendation, `phase` assessment when relevant, prompt blocks, and compatibility status.\n8. Answer with concise guidance and cite the doc source, using the reference files only as helper context.\n\n## Reference map\n\nRead only what you need:\n\n- `references/latest-model.md` -> model-selection and \"best/latest/current model\" questions; verify every recommendation against current OpenAI docs before answering.\n- `references/upgrading-to-gpt-5p4.md` -> only for explicit GPT-5.4 upgrade and upgrade-planning requests; verify the checklist and compatibility guidance against current OpenAI docs before answering.\n- `references/gpt-5p4-prompting-guide.md` -> prompt rewrites and prompt-behavior upgrades for GPT-5.4; verify prompting guidance against current OpenAI docs before answering.\n\n## Quality rules\n\n- Treat OpenAI docs as the source of truth; avoid speculation.\n- Keep quotes short and within policy limits; prefer paraphrase with citations.\n- If multiple pages differ, call out the difference and cite both.\n- Reference files are convenience guides only; for volatile guidance such as recommended models, upgrade instructions, or prompting advice, current OpenAI docs always win.\n- If docs do not cover the user’s need, say so and offer next steps.\n\n## Tooling notes\n\n- Always use MCP doc tools before any web search for OpenAI-related questions.\n- If the MCP server is installed but returns no meaningful results, then use web search as a fallback.\n- When falling back to web search, restrict to official OpenAI domains (developers.openai.com, platform.openai.com) and cite sources.\n"
  },
  {
    "path": "skills/.curated/openai-docs/agents/openai.yaml",
    "content": "interface:\n  display_name: \"OpenAI Docs\"\n  short_description: \"Reference official OpenAI docs, including upgrade guidance\"\n  icon_small: \"./assets/openai-small.svg\"\n  icon_large: \"./assets/openai.png\"\n  default_prompt: \"Look up official OpenAI docs, load relevant GPT-5.4 upgrade references when applicable, and answer with concise, cited guidance.\"\n\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"openaiDeveloperDocs\"\n      description: \"OpenAI Developer Docs MCP server\"\n      transport: \"streamable_http\"\n      url: \"https://developers.openai.com/mcp\"\n"
  },
  {
    "path": "skills/.curated/openai-docs/references/gpt-5p4-prompting-guide.md",
    "content": "# GPT-5.4 prompting upgrade guide\n\nUse this guide when prompts written for older models need to be adapted for GPT-5.4 during an upgrade. Start lean: keep the model-string change narrow, preserve the original task intent, and add only the smallest prompt changes needed to recover behavior.\n\n## Default upgrade posture\n\n- Start with `model string only` whenever the old prompt is already short, explicit, and task-bounded.\n- Move to `model string + light prompt rewrite` only when regressions appear in completeness, persistence, citation quality, verification, or verbosity.\n- Prefer one or two targeted prompt additions over a broad rewrite.\n- Treat reasoning effort as a last-mile knob. Start lower, then increase only after prompt-level fixes and evals.\n- Before increasing reasoning effort, first add a completeness contract, a verification loop, and tool persistence rules - depending on the usage case.\n- If the workflow clearly depends on implementation changes rather than prompt changes, treat it as blocked for prompt-only upgrade guidance.\n- Do not classify a case as blocked just because the workflow uses tools; block only if the upgrade requires changing tool definitions, wiring, or other implementation details.\n\n## Behavioral differences to account for\n\nCurrent GPT-5.4 upgrade guidance suggests these strengths:\n\n- stronger personality and tone adherence, with less drift over long answers\n- better long-horizon and agentic workflow stamina\n- stronger spreadsheet, finance, and formatting tasks\n- more efficient tool selection and fewer unnecessary calls by default\n- stronger structured generation and classification reliability\n\nThe main places where prompt guidance still helps are:\n\n- retrieval-heavy workflows that need persistent tool use and explicit completeness\n- research and citation discipline\n- verification before irreversible or high-impact actions\n- terminal and tool workflow hygiene\n- defaults and implied follow-through\n- verbosity control for compact, information-dense answers\n\nStart with the smallest set of instructions that preserves correctness. Add the prompt blocks below only for workflows that actually need them.\n\n## Prompt rewrite patterns\n\n| Older prompt pattern | GPT-5.4 adjustment | Why | Example addition |\n| --- | --- | --- | --- |\n| Long, repetitive instructions that compensate for weaker instruction following | Remove duplicate scaffolding and keep only the constraints that materially change behavior | GPT-5.4 usually needs less repeated steering | Replace repeated reminders with one concise rule plus a verification block |\n| Fast assistant prompt with no verbosity control | Keep the prompt as-is first; add a verbosity clamp only if outputs become too long | Many GPT-4o or GPT-4.1 upgrades work with just a model-string swap | Add `output_verbosity_spec` only after a verbosity regression |\n| Tool-heavy agent prompt that assumes the model will keep searching until complete | Add persistence and verification rules | GPT-5.4 may use fewer tool calls by default for efficiency | Add `tool_persistence_rules` and `verification_loop` |\n| Tool-heavy workflow where later actions depend on earlier lookup or retrieval | Add prerequisite and missing-context rules before action steps | GPT-5.4 benefits from explicit dependency-aware routing when context is still thin | Add `dependency_checks` and `missing_context_gating` |\n| Retrieval workflow with several independent lookups | Add selective parallelism guidance | GPT-5.4 is strong at parallel tool use, but should not parallelize dependent steps | Add `parallel_tool_calling` |\n| Batch workflow prompt that often misses items | Add an explicit completeness contract | Item accounting benefits from direct instruction | Add `completeness_contract` |\n| Research prompt that needs grounding and citation discipline | Add research, citation, and empty-result recovery blocks | Multi-pass retrieval is stronger when the model is told how to react to weak or empty search results | Add `research_mode`, `citation_rules`, and `empty_result_handling`; add `tool_persistence_rules` when retrieval tools are already in use |\n| Coding or terminal prompt with shell misuse or early stop failures | Keep the same tool surface and add terminal hygiene and verification instructions | Tool-using coding workflows are not blocked just because tools exist; they usually need better prompt steering, not host rewiring | Add `terminal_tool_hygiene` and `verification_loop`, optionally `tool_persistence_rules` |\n| Multi-agent or support-triage workflow with escalation or completeness requirements | Add one lightweight control block for persistence, completeness, or verification | GPT-5.4 can be more efficient by default, so multi-step support flows benefit from an explicit completion or verification contract | Add at least one of `tool_persistence_rules`, `completeness_contract`, or `verification_loop` |\n\n## Prompt blocks\n\nUse these selectively. Do not add all of them by default.\n\n### `output_verbosity_spec`\n\nUse when:\n\n- the upgraded model gets too wordy\n- the host needs compact, information-dense answers\n- the workflow benefits from a short overview plus a checklist\n\n```text\n<output_verbosity_spec>\n- Default: 3-6 sentences or up to 6 bullets.\n- If the user asked for a doc or report, use headings with short bullets.\n- For multi-step tasks:\n  - Start with 1 short overview paragraph.\n  - Then provide a checklist with statuses: [done], [todo], or [blocked].\n- Avoid repeating the user's request.\n- Prefer compact, information-dense writing.\n</output_verbosity_spec>\n```\n\n### `default_follow_through_policy`\n\nUse when:\n\n- the host expects the model to proceed on reversible, low-risk steps\n- the upgraded model becomes too conservative or asks for confirmation too often\n\n```text\n<default_follow_through_policy>\n- If the user's intent is clear and the next step is reversible and low-risk, proceed without asking permission.\n- Only ask permission if the next step is:\n  (a) irreversible,\n  (b) has external side effects, or\n  (c) requires missing sensitive information or a choice that materially changes outcomes.\n- If proceeding, state what you did and what remains optional.\n</default_follow_through_policy>\n```\n\n### `instruction_priority`\n\nUse when:\n\n- users often change task shape, format, or tone mid-conversation\n- the host needs an explicit override policy instead of relying on defaults\n\n```text\n<instruction_priority>\n- User instructions override default style, tone, formatting, and initiative preferences.\n- Safety, honesty, privacy, and permission constraints do not yield.\n- If a newer user instruction conflicts with an earlier one, follow the newer instruction.\n- Preserve earlier instructions that do not conflict.\n</instruction_priority>\n```\n\n### `tool_persistence_rules`\n\nUse when:\n\n- the workflow needs multiple retrieval or verification steps\n- the model starts stopping too early because it is trying to save tool calls\n\n```text\n<tool_persistence_rules>\n- Use tools whenever they materially improve correctness, completeness, or grounding.\n- Do not stop early just to save tool calls.\n- Keep calling tools until:\n  (1) the task is complete, and\n  (2) verification passes.\n- If a tool returns empty or partial results, retry with a different strategy.\n</tool_persistence_rules>\n```\n\n### `dig_deeper_nudge`\n\nUse when:\n\n- the model is too literal or stops at the first plausible answer\n- the task is safety- or accuracy-sensitive and needs a small initiative nudge before raising reasoning effort\n\n```text\n<dig_deeper_nudge>\n- Do not stop at the first plausible answer.\n- Look for second-order issues, edge cases, and missing constraints.\n- If the task is safety- or accuracy-critical, perform at least one verification step.\n</dig_deeper_nudge>\n```\n\n### `dependency_checks`\n\nUse when:\n\n- later actions depend on prerequisite lookup, memory retrieval, or discovery steps\n- the model may be tempted to skip prerequisite work because the intended end state seems obvious\n\n```text\n<dependency_checks>\n- Before taking an action, check whether prerequisite discovery, lookup, or memory retrieval is required.\n- Do not skip prerequisite steps just because the intended final action seems obvious.\n- If a later step depends on the output of an earlier one, resolve that dependency first.\n</dependency_checks>\n```\n\n### `parallel_tool_calling`\n\nUse when:\n\n- the workflow has multiple independent retrieval steps\n- wall-clock time matters but some steps still need sequencing\n\n```text\n<parallel_tool_calling>\n- When multiple retrieval or lookup steps are independent, prefer parallel tool calls to reduce wall-clock time.\n- Do not parallelize steps with prerequisite dependencies or where one result determines the next action.\n- After parallel retrieval, pause to synthesize before making more calls.\n- Prefer selective parallelism: parallelize independent evidence gathering, not speculative or redundant tool use.\n</parallel_tool_calling>\n```\n\n### `completeness_contract`\n\nUse when:\n\n- the task involves batches, lists, enumerations, or multiple deliverables\n- missing items are a common failure mode\n\n```text\n<completeness_contract>\n- Deliver all requested items.\n- Maintain an itemized checklist of deliverables.\n- For lists or batches:\n  - state the expected count,\n  - enumerate items 1..N,\n  - confirm that none are missing before finalizing.\n- If any item is blocked by missing data, mark it [blocked] and state exactly what is missing.\n</completeness_contract>\n```\n\n### `empty_result_handling`\n\nUse when:\n\n- the workflow frequently performs search, CRM, logs, or retrieval steps\n- no-results failures are often false negatives\n\n```text\n<empty_result_handling>\nIf a lookup returns empty or suspiciously small results:\n- Do not conclude that no results exist immediately.\n- Try at least 2 fallback strategies, such as a broader query, alternate filters, or another source.\n- Only then report that no results were found, along with what you tried.\n</empty_result_handling>\n```\n\n### `verification_loop`\n\nUse when:\n\n- the workflow has downstream impact\n- accuracy, formatting, or completeness regressions matter\n\n```text\n<verification_loop>\nBefore finalizing:\n- Check correctness: does the output satisfy every requirement?\n- Check grounding: are factual claims backed by retrieved sources or tool output?\n- Check formatting: does the output match the requested schema or style?\n- Check safety and irreversibility: if the next step has external side effects, ask permission first.\n</verification_loop>\n```\n\n### `missing_context_gating`\n\nUse when:\n\n- required context is sometimes missing early in the workflow\n- the model should prefer retrieval over guessing\n\n```text\n<missing_context_gating>\n- If required context is missing, do not guess.\n- Prefer the appropriate lookup tool when the context is retrievable; ask a minimal clarifying question only when it is not.\n- If you must proceed, label assumptions explicitly and choose a reversible action.\n</missing_context_gating>\n```\n\n### `action_safety`\n\nUse when:\n\n- the agent will actively take actions through tools\n- the host benefits from a short pre-flight and post-flight execution frame\n\n```text\n<action_safety>\n- Pre-flight: summarize the intended action and parameters in 1-2 lines.\n- Execute via tool.\n- Post-flight: confirm the outcome and any validation that was performed.\n</action_safety>\n```\n\n### `citation_rules`\n\nUse when:\n\n- the workflow produces cited answers\n- fabricated citations or wrong citation formats are costly\n\n```text\n<citation_rules>\n- Only cite sources that were actually retrieved in this session.\n- Never fabricate citations, URLs, IDs, or quote spans.\n- If you cannot find a source for a claim, say so and either:\n  - soften the claim, or\n  - explain how to verify it with tools.\n- Use exactly the citation format required by the host application.\n</citation_rules>\n```\n\n### `research_mode`\n\nUse when:\n\n- the workflow is research-heavy\n- the host uses web search or retrieval tools\n\n```text\n<research_mode>\n- Do research in 3 passes:\n  1) Plan: list 3-6 sub-questions to answer.\n  2) Retrieve: search each sub-question and follow 1-2 second-order leads.\n  3) Synthesize: resolve contradictions and write the final answer with citations.\n- Stop only when more searching is unlikely to change the conclusion.\n</research_mode>\n```\n\nIf your host environment uses a specific research tool or requires a submit step, combine this with the host's finalization contract.\n\n### `structured_output_contract`\n\nUse when:\n\n- the host depends on strict JSON, SQL, or other structured output\n\n```text\n<structured_output_contract>\n- Output only the requested format.\n- Do not add prose or markdown fences unless they were requested.\n- Validate that parentheses and brackets are balanced.\n- Do not invent tables or fields.\n- If required schema information is missing, ask for it or return an explicit error object.\n</structured_output_contract>\n```\n\n### `bbox_extraction_spec`\n\nUse when:\n\n- the workflow extracts OCR boxes, document regions, or other coordinates\n- layout drift or missed dense regions are common failure modes\n\n```text\n<bbox_extraction_spec>\n- Use the specified coordinate format exactly, such as [x1,y1,x2,y2] normalized to 0..1.\n- For each box, include page, label, text snippet, and confidence.\n- Add a vertical-drift sanity check so boxes stay aligned with the correct line of text.\n- If the layout is dense, process page by page and do a second pass for missed items.\n</bbox_extraction_spec>\n```\n\n### `terminal_tool_hygiene`\n\nUse when:\n\n- the prompt belongs to a terminal-based or coding-agent workflow\n- tool misuse or shell misuse has been observed\n\n```text\n<terminal_tool_hygiene>\n- Only run shell commands through the terminal tool.\n- Never try to \"run\" tool names as shell commands.\n- If a patch or edit tool exists, use it directly instead of emulating it in bash.\n- After changes, run a lightweight verification step such as ls, tests, or a build before declaring the task done.\n</terminal_tool_hygiene>\n```\n\n### `user_updates_spec`\n\nUse when:\n\n- the workflow is long-running and user updates matter\n\n```text\n<user_updates_spec>\n- Only update the user when starting a new major phase or when the plan changes.\n- Each update should contain:\n  - 1 sentence on what changed,\n  - 1 sentence on the next step.\n- Do not narrate routine tool calls.\n- Keep the user-facing update short, even when the actual work is exhaustive.\n</user_updates_spec>\n```\n\nIf you are using [Compaction](https://developers.openai.com/api/docs/guides/compaction) in the Responses API, compact after major milestones, treat compacted items as opaque state, and keep prompts functionally identical after compaction.\n\n## Responses `phase` guidance\n\nFor long-running Responses workflows, preambles, or tool-heavy agents that replay assistant items, review whether `phase` is already preserved.\n\n- If the host already round-trips `phase`, keep it intact during the upgrade.\n- If the host uses `previous_response_id` and does not manually replay assistant items, note that this may reduce manual `phase` handling needs.\n- If reliable GPT-5.4 behavior would require adding or preserving `phase` and that would need code edits, treat the case as blocked for prompt-only or model-string-only migration guidance.\n\n## Example upgrade profiles\n\n### GPT-5.2\n\n- Use `gpt-5.4`\n- Match the current reasoning effort first\n- Preserve the existing latency and quality profile before tuning prompt blocks\n- If the repo does not expose the exact setting, emit `same` as the starting recommendation\n\n### GPT-5.3-Codex\n\n- Use `gpt-5.4`\n- Match the current reasoning effort first\n- If you need Codex-style speed and efficiency, add verification blocks before increasing reasoning effort\n- If the repo does not expose the exact setting, emit `same` as the starting recommendation\n\n### GPT-4o or GPT-4.1 assistant\n\n- Use `gpt-5.4`\n- Start with `none` reasoning effort\n- Add `output_verbosity_spec` only if output becomes too verbose\n\n### Long-horizon agent\n\n- Use `gpt-5.4`\n- Start with `medium` reasoning effort\n- Add `tool_persistence_rules`\n- Add `completeness_contract`\n- Add `verification_loop`\n\n### Research workflow\n\n- Use `gpt-5.4`\n- Start with `medium` reasoning effort\n- Add `research_mode`\n- Add `citation_rules`\n- Add `empty_result_handling`\n- Add `tool_persistence_rules` when the host already uses web or retrieval tools\n- Add `parallel_tool_calling` when the retrieval steps are independent\n\n### Support triage or multi-agent workflow\n\n- Use `gpt-5.4`\n- Prefer `model string + light prompt rewrite` over `model string only`\n- Add at least one of `tool_persistence_rules`, `completeness_contract`, or `verification_loop`\n- Add more only if evals show a real regression\n\n### Coding or terminal workflow\n\n- Use `gpt-5.4`\n- Keep the model-string change narrow\n- Match the current reasoning effort first if you are upgrading from GPT-5.3-Codex\n- Add `terminal_tool_hygiene`\n- Add `verification_loop`\n- Add `dependency_checks` when actions depend on prerequisite lookup or discovery\n- Add `tool_persistence_rules` if the agent stops too early\n- Review whether `phase` is already preserved for long-running Responses flows or assistant preambles\n- Do not classify this as blocked just because the workflow uses tools; block only if the upgrade requires changing tool definitions or wiring\n- If the repo already uses Responses plus tools and no required host-side change is shown, prefer `model_string_plus_light_prompt_rewrite` over `blocked`\n\n## Prompt regression checklist\n\n- Check whether the upgraded prompt still preserves the original task intent.\n- Check whether the new prompt is leaner, not just longer.\n- Check completeness, citation quality, dependency handling, verification behavior, and verbosity.\n- For long-running Responses agents, check whether `phase` handling is already in place or needs implementation work.\n- Confirm that each added prompt block addresses an observed regression.\n- Remove prompt blocks that are not earning their keep.\n"
  },
  {
    "path": "skills/.curated/openai-docs/references/latest-model.md",
    "content": "# Latest model guide\n\nThis file is a curated helper. Every recommendation here must be verified against current OpenAI docs before it is repeated to a user.\n\n## Current model map\n\n| Model ID | Use for |\n| --- | --- |\n| `gpt-5.4` | Default text plus reasoning for most new apps, including for coding use-cases |\n| `gpt-5.4-pro` | Only when the user explicitly asks for maximum reasoning or quality; substantially slower and more expensive |\n| `gpt-5.4-mini` | Cheaper and faster reasoning with good quality, including for coding use-cases |\n| `gpt-5.4-nano` | High-throughput simple tasks and classification |\n| `gpt-image-1.5` | Best image generation and edit quality |\n| `gpt-image-1-mini` | Cost-optimized image generation |\n| `gpt-4o-mini-tts` | Text-to-speech |\n| `gpt-4o-mini-transcribe` | Speech-to-text, fast and cost-efficient |\n| `gpt-realtime-1.5` | Realtime voice and multimodal sessions |\n| `gpt-realtime-mini` | Cheaper realtime sessions |\n| `gpt-audio` | Chat Completions audio input and output |\n| `gpt-audio-mini` | Cheaper Chat Completions audio workflows |\n| `sora-2` | Faster iteration and draft video generation |\n| `sora-2-pro` | Higher-quality production video |\n| `omni-moderation-latest` | Text and image moderation |\n| `text-embedding-3-large` | Higher-quality retrieval embeddings; default in this skill because no best-specific row exists |\n| `text-embedding-3-small` | Lower-cost embeddings |\n\n## Maintenance notes\n\n- This file will drift unless it is periodically re-verified against current OpenAI docs.\n- If this file conflicts with current docs, the docs win.\n"
  },
  {
    "path": "skills/.curated/openai-docs/references/upgrading-to-gpt-5p4.md",
    "content": "# Upgrading to GPT-5.4\n\nUse this guide when the user explicitly asks to upgrade an existing integration to GPT-5.4. Pair it with current OpenAI docs lookups. The default target string is `gpt-5.4`.\n\n## Upgrade posture\n\nUpgrade with the narrowest safe change set:\n\n- replace the model string first\n- update only the prompts that are directly tied to that model usage\n- prefer prompt-only upgrades when possible\n- if the upgrade would require API-surface changes, parameter rewrites, tool rewiring, or broader code edits, mark it as blocked instead of stretching the scope\n\n## Upgrade workflow\n\n1. Inventory current model usage.\n   - Search for model strings, client calls, and prompt-bearing files.\n   - Include inline prompts, prompt templates, YAML or JSON configs, Markdown docs, and saved prompts when they are clearly tied to a model usage site.\n2. Pair each model usage with its prompt surface.\n   - Prefer the closest prompt surface first: inline system or developer text, then adjacent prompt files, then shared templates.\n   - If you cannot confidently tie a prompt to the model usage, say so instead of guessing.\n3. Classify the source model family.\n   - Common buckets: `gpt-4o` or `gpt-4.1`, `o1` or `o3` or `o4-mini`, early `gpt-5`, later `gpt-5.x`, or mixed and unclear.\n4. Decide the upgrade class.\n   - `model string only`\n   - `model string + light prompt rewrite`\n   - `blocked without code changes`\n5. Run the no-code compatibility gate.\n   - Check whether the current integration can accept `gpt-5.4` without API-surface changes or implementation changes.\n   - For long-running Responses or tool-heavy agents, check whether `phase` is already preserved or round-tripped when the host replays assistant items or uses preambles.\n   - If compatibility depends on code changes, return `blocked`.\n   - If compatibility is unclear, return `unknown` rather than improvising.\n6. Recommend the upgrade.\n   - Default replacement string: `gpt-5.4`\n   - Keep the intervention small and behavior-preserving.\n7. Deliver a structured recommendation.\n   - `Current model usage`\n   - `Recommended model-string updates`\n   - `Starting reasoning recommendation`\n   - `Prompt updates`\n   - `Phase assessment` when the flow is long-running, replayed, or tool-heavy\n   - `No-code compatibility check`\n   - `Validation plan`\n   - `Launch-day refresh items`\n\nOutput rule:\n\n- Always emit a starting `reasoning_effort_recommendation` for each usage site.\n- If the repo exposes the current reasoning setting, preserve it first unless the source guide says otherwise.\n- If the repo does not expose the current setting, use the source-family starting mapping instead of returning `null`.\n\n## Upgrade outcomes\n\n### `model string only`\n\nChoose this when:\n\n- the existing prompts are already short, explicit, and task-bounded\n- the workflow is not strongly research-heavy, tool-heavy, multi-agent, batch or completeness-sensitive, or long-horizon\n- there are no obvious compatibility blockers\n\nDefault action:\n\n- replace the model string with `gpt-5.4`\n- keep prompts unchanged\n- validate behavior with existing evals or spot checks\n\n### `model string + light prompt rewrite`\n\nChoose this when:\n\n- the old prompt was compensating for weaker instruction following\n- the workflow needs more persistence than the default tool-use behavior will likely provide\n- the task needs stronger completeness, citation discipline, or verification\n- the upgraded model becomes too verbose or under-complete unless instructed otherwise\n- the workflow is research-heavy and needs stronger handling of sparse or empty retrieval results\n- the workflow is coding-oriented, tool-heavy, or multi-agent, but the existing API surface and tool definitions can remain unchanged\n\nDefault action:\n\n- replace the model string with `gpt-5.4`\n- add one or two targeted prompt blocks\n- read `references/gpt-5p4-prompting-guide.md` to choose the smallest prompt changes that recover the old behavior\n- avoid broad prompt cleanup unrelated to the upgrade\n- for research workflows, default to `research_mode` + `citation_rules` + `empty_result_handling`; add `tool_persistence_rules` when the host already uses retrieval tools\n- for dependency-aware or tool-heavy workflows, default to `tool_persistence_rules` + `dependency_checks` + `verification_loop`; add `parallel_tool_calling` only when retrieval steps are truly independent\n- for coding or terminal workflows, default to `terminal_tool_hygiene` + `verification_loop`\n- for multi-agent support or triage workflows, default to at least one of `tool_persistence_rules`, `completeness_contract`, or `verification_loop`\n- for long-running Responses agents with preambles or multiple assistant messages, explicitly review whether `phase` is already handled; if adding or preserving `phase` would require code edits, mark the path as `blocked`\n- do not classify a coding or tool-using Responses workflow as `blocked` just because the visible snippet is minimal; prefer `model string + light prompt rewrite` unless the repo clearly shows that a safe GPT-5.4 path would require host-side code changes\n\n### `blocked`\n\nChoose this when:\n\n- the upgrade appears to require API-surface changes\n- the upgrade appears to require parameter rewrites or reasoning-setting changes that are not exposed outside implementation code\n- the upgrade would require changing tool definitions, tool handler wiring, or schema contracts\n- you cannot confidently identify the prompt surface tied to the model usage\n\nDefault action:\n\n- do not improvise a broader upgrade\n- report the blocker and explain that the fix is out of scope for this guide\n\n## No-code compatibility checklist\n\nBefore recommending a no-code upgrade, check:\n\n1. Can the current host accept the `gpt-5.4` model string without changing client code or API surface?\n2. Are the related prompts identifiable and editable?\n3. Does the host depend on behavior that likely needs API-surface changes, parameter rewrites, or tool rewiring?\n4. Would the likely fix be prompt-only, or would it need implementation changes?\n5. Is the prompt surface close enough to the model usage that you can make a targeted change instead of a broad cleanup?\n6. For long-running Responses or tool-heavy agents, is `phase` already preserved if the host relies on preambles, replayed assistant items, or multiple assistant messages?\n\nIf item 1 is no, items 3 through 4 point to implementation work, or item 6 is no and the fix needs code changes, return `blocked`.\n\nIf item 2 is no, return `unknown` unless the user can point to the prompt location.\n\nImportant:\n\n- Existing use of tools, agents, or multiple usage sites is not by itself a blocker.\n- If the current host can keep the same API surface and the same tool definitions, prefer `model string + light prompt rewrite` over `blocked`.\n- Reserve `blocked` for cases that truly require implementation changes, not cases that only need stronger prompt steering.\n\n## Scope boundaries\n\nThis guide may:\n\n- update or recommend updated model strings\n- update or recommend updated prompts\n- inspect code and prompt files to understand where those changes belong\n- inspect whether existing Responses flows already preserve `phase`\n- flag compatibility blockers\n\nThis guide may not:\n\n- move Chat Completions code to Responses\n- move Responses code to another API surface\n- rewrite parameter shapes\n- change tool definitions or tool-call handling\n- change structured-output wiring\n- add or retrofit `phase` handling in implementation code\n- edit business logic, orchestration logic, or SDK usage beyond a literal model-string replacement\n\nIf a safe GPT-5.4 upgrade requires any of those changes, mark the path as blocked and out of scope.\n\n## Validation plan\n\n- Validate each upgraded usage site with existing evals or realistic spot checks.\n- Check whether the upgraded model still matches expected latency, output shape, and quality.\n- If prompt edits were added, confirm each block is doing real work instead of adding noise.\n- If the workflow has downstream impact, add a lightweight verification pass before finalization.\n\n## Launch-day refresh items\n\nWhen final GPT-5.4 guidance changes:\n\n1. Replace release-candidate assumptions with final GPT-5.4 guidance where appropriate.\n2. Re-check whether the default target string should stay `gpt-5.4` for all source families.\n3. Re-check any prompt-block recommendations whose semantics may have changed.\n4. Re-check research, citation, and compatibility guidance against the final model behavior.\n5. Re-run the same upgrade scenarios and confirm the blocked-versus-viable boundaries still hold.\n"
  },
  {
    "path": "skills/.curated/pdf/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/pdf/SKILL.md",
    "content": "---\nname: \"pdf\"\ndescription: \"Use when tasks involve reading, creating, or reviewing PDF files where rendering and layout matter; prefer visual checks by rendering pages (Poppler) and use Python tools such as `reportlab`, `pdfplumber`, and `pypdf` for generation and extraction.\"\n---\n\n\n# PDF Skill\n\n## When to use\n- Read or review PDF content where layout and visuals matter.\n- Create PDFs programmatically with reliable formatting.\n- Validate final rendering before delivery.\n\n## Workflow\n1. Prefer visual review: render PDF pages to PNGs and inspect them.\n   - Use `pdftoppm` if available.\n   - If unavailable, install Poppler or ask the user to review the output locally.\n2. Use `reportlab` to generate PDFs when creating new documents.\n3. Use `pdfplumber` (or `pypdf`) for text extraction and quick checks; do not rely on it for layout fidelity.\n4. After each meaningful update, re-render pages and verify alignment, spacing, and legibility.\n\n## Temp and output conventions\n- Use `tmp/pdfs/` for intermediate files; delete when done.\n- Write final artifacts under `output/pdf/` when working in this repo.\n- Keep filenames stable and descriptive.\n\n## Dependencies (install if missing)\nPrefer `uv` for dependency management.\n\nPython packages:\n```\nuv pip install reportlab pdfplumber pypdf\n```\nIf `uv` is unavailable:\n```\npython3 -m pip install reportlab pdfplumber pypdf\n```\nSystem tools (for rendering):\n```\n# macOS (Homebrew)\nbrew install poppler\n\n# Ubuntu/Debian\nsudo apt-get install -y poppler-utils\n```\n\nIf installation isn't possible in this environment, tell the user which dependency is missing and how to install it locally.\n\n## Environment\nNo required environment variables.\n\n## Rendering command\n```\npdftoppm -png $INPUT_PDF $OUTPUT_PREFIX\n```\n\n## Quality expectations\n- Maintain polished visual design: consistent typography, spacing, margins, and section hierarchy.\n- Avoid rendering issues: clipped text, overlapping elements, broken tables, black squares, or unreadable glyphs.\n- Charts, tables, and images must be sharp, aligned, and clearly labeled.\n- Use ASCII hyphens only. Avoid U+2011 (non-breaking hyphen) and other Unicode dashes.\n- Citations and references must be human-readable; never leave tool tokens or placeholder strings.\n\n## Final checks\n- Do not deliver until the latest PNG inspection shows zero visual or formatting defects.\n- Confirm headers/footers, page numbering, and section transitions look polished.\n- Keep intermediate files organized or remove them after final approval.\n"
  },
  {
    "path": "skills/.curated/pdf/agents/openai.yaml",
    "content": "interface:\n  display_name: \"PDF Skill\"\n  short_description: \"Create, edit, and review PDFs\"\n  icon_large: \"./assets/pdf.png\"\n  default_prompt: \"Create, edit, or review this PDF and summarize the key output or changes.\"\n"
  },
  {
    "path": "skills/.curated/playwright/LICENSE.txt",
    "content": "                                 Apache License\r\n                           Version 2.0, January 2004\r\n                        http://www.apache.org/licenses/\r\n\r\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n   1. Definitions.\r\n\r\n      \"License\" shall mean the terms and conditions for use, reproduction,\r\n      and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n      \"Licensor\" shall mean the copyright owner or entity authorized by\r\n      the copyright owner that is granting the License.\r\n\r\n      \"Legal Entity\" shall mean the union of the acting entity and all\r\n      other entities that control, are controlled by, or are under common\r\n      control with that entity. For the purposes of this definition,\r\n      \"control\" means (i) the power, direct or indirect, to cause the\r\n      direction or management of such entity, whether by contract or\r\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n      outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n      exercising permissions granted by this License.\r\n\r\n      \"Source\" form shall mean the preferred form for making modifications,\r\n      including but not limited to software source code, documentation\r\n      source, and configuration files.\r\n\r\n      \"Object\" form shall mean any form resulting from mechanical\r\n      transformation or translation of a Source form, including but\r\n      not limited to compiled object code, generated documentation,\r\n      and conversions to other media types.\r\n\r\n      \"Work\" shall mean the work of authorship, whether in Source or\r\n      Object form, made available under the License, as indicated by a\r\n      copyright notice that is included in or attached to the work\r\n      (an example is provided in the Appendix below).\r\n\r\n      \"Derivative Works\" shall mean any work, whether in Source or Object\r\n      form, that is based on (or derived from) the Work and for which the\r\n      editorial revisions, annotations, elaborations, or other modifications\r\n      represent, as a whole, an original work of authorship. For the purposes\r\n      of this License, Derivative Works shall not include works that remain\r\n      separable from, or merely link (or bind by name) to the interfaces of,\r\n      the Work and Derivative Works thereof.\r\n\r\n      \"Contribution\" shall mean any work of authorship, including\r\n      the original version of the Work and any modifications or additions\r\n      to that Work or Derivative Works thereof, that is intentionally\r\n      submitted to Licensor for inclusion in the Work by the copyright owner\r\n      or by an individual or Legal Entity authorized to submit on behalf of\r\n      the copyright owner. For the purposes of this definition, \"submitted\"\r\n      means any form of electronic, verbal, or written communication sent\r\n      to the Licensor or its representatives, including but not limited to\r\n      communication on electronic mailing lists, source code control systems,\r\n      and issue tracking systems that are managed by, or on behalf of, the\r\n      Licensor for the purpose of discussing and improving the Work, but\r\n      excluding communication that is conspicuously marked or otherwise\r\n      designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n      on behalf of whom a Contribution has been received by Licensor and\r\n      subsequently incorporated within the Work.\r\n\r\n   2. Grant of Copyright License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      copyright license to reproduce, prepare Derivative Works of,\r\n      publicly display, publicly perform, sublicense, and distribute the\r\n      Work and such Derivative Works in Source or Object form.\r\n\r\n   3. Grant of Patent License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      (except as stated in this section) patent license to make, have made,\r\n      use, offer to sell, sell, import, and otherwise transfer the Work,\r\n      where such license applies only to those patent claims licensable\r\n      by such Contributor that are necessarily infringed by their\r\n      Contribution(s) alone or by combination of their Contribution(s)\r\n      with the Work to which such Contribution(s) was submitted. If You\r\n      institute patent litigation against any entity (including a\r\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n      or a Contribution incorporated within the Work constitutes direct\r\n      or contributory patent infringement, then any patent licenses\r\n      granted to You under this License for that Work shall terminate\r\n      as of the date such litigation is filed.\r\n\r\n   4. Redistribution. You may reproduce and distribute copies of the\r\n      Work or Derivative Works thereof in any medium, with or without\r\n      modifications, and in Source or Object form, provided that You\r\n      meet the following conditions:\r\n\r\n      (a) You must give any other recipients of the Work or\r\n          Derivative Works a copy of this License; and\r\n\r\n      (b) You must cause any modified files to carry prominent notices\r\n          stating that You changed the files; and\r\n\r\n      (c) You must retain, in the Source form of any Derivative Works\r\n          that You distribute, all copyright, patent, trademark, and\r\n          attribution notices from the Source form of the Work,\r\n          excluding those notices that do not pertain to any part of\r\n          the Derivative Works; and\r\n\r\n      (d) If the Work includes a \"NOTICE\" text file as part of its\r\n          distribution, then any Derivative Works that You distribute must\r\n          include a readable copy of the attribution notices contained\r\n          within such NOTICE file, excluding those notices that do not\r\n          pertain to any part of the Derivative Works, in at least one\r\n          of the following places: within a NOTICE text file distributed\r\n          as part of the Derivative Works; within the Source form or\r\n          documentation, if provided along with the Derivative Works; or,\r\n          within a display generated by the Derivative Works, if and\r\n          wherever such third-party notices normally appear. The contents\r\n          of the NOTICE file are for informational purposes only and\r\n          do not modify the License. You may add Your own attribution\r\n          notices within Derivative Works that You distribute, alongside\r\n          or as an addendum to the NOTICE text from the Work, provided\r\n          that such additional attribution notices cannot be construed\r\n          as modifying the License.\r\n\r\n      You may add Your own copyright statement to Your modifications and\r\n      may provide additional or different license terms and conditions\r\n      for use, reproduction, or distribution of Your modifications, or\r\n      for any such Derivative Works as a whole, provided Your use,\r\n      reproduction, and distribution of the Work otherwise complies with\r\n      the conditions stated in this License.\r\n\r\n   5. Submission of Contributions. Unless You explicitly state otherwise,\r\n      any Contribution intentionally submitted for inclusion in the Work\r\n      by You to the Licensor shall be under the terms and conditions of\r\n      this License, without any additional terms or conditions.\r\n      Notwithstanding the above, nothing herein shall supersede or modify\r\n      the terms of any separate license agreement you may have executed\r\n      with Licensor regarding such Contributions.\r\n\r\n   6. Trademarks. This License does not grant permission to use the trade\r\n      names, trademarks, service marks, or product names of the Licensor,\r\n      except as required for reasonable and customary use in describing the\r\n      origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n   7. Disclaimer of Warranty. Unless required by applicable law or\r\n      agreed to in writing, Licensor provides the Work (and each\r\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n      implied, including, without limitation, any warranties or conditions\r\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n      PARTICULAR PURPOSE. You are solely responsible for determining the\r\n      appropriateness of using or redistributing the Work and assume any\r\n      risks associated with Your exercise of permissions under this License.\r\n\r\n   8. Limitation of Liability. In no event and under no legal theory,\r\n      whether in tort (including negligence), contract, or otherwise,\r\n      unless required by applicable law (such as deliberate and grossly\r\n      negligent acts) or agreed to in writing, shall any Contributor be\r\n      liable to You for damages, including any direct, indirect, special,\r\n      incidental, or consequential damages of any character arising as a\r\n      result of this License or out of the use or inability to use the\r\n      Work (including but not limited to damages for loss of goodwill,\r\n      work stoppage, computer failure or malfunction, or any and all\r\n      other commercial damages or losses), even if such Contributor\r\n      has been advised of the possibility of such damages.\r\n\r\n   9. Accepting Warranty or Additional Liability. While redistributing\r\n      the Work or Derivative Works thereof, You may choose to offer,\r\n      and charge a fee for, acceptance of support, warranty, indemnity,\r\n      or other liability obligations and/or rights consistent with this\r\n      License. However, in accepting such obligations, You may act only\r\n      on Your own behalf and on Your sole responsibility, not on behalf\r\n      of any other Contributor, and only if You agree to indemnify,\r\n      defend, and hold each Contributor harmless for any liability\r\n      incurred by, or claims asserted against, such Contributor by reason\r\n      of your accepting any such warranty or additional liability.\r\n\r\n   END OF TERMS AND CONDITIONS\r\n\r\n   APPENDIX: How to apply the Apache License to your work.\r\n\r\n      To apply the Apache License to your work, attach the following\r\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\r\n      replaced with your own identifying information. (Don't include\r\n      the brackets!)  The text should be enclosed in the appropriate\r\n      comment syntax for the file format. We also recommend that a\r\n      file or class name and description of purpose be included on the\r\n      same \"printed page\" as the copyright notice for easier\r\n      identification within third-party archives.\r\n\r\n   Copyright (c) Microsoft Corporation.\r\n\r\n   Licensed under the Apache License, Version 2.0 (the \"License\");\r\n   you may not use this file except in compliance with the License.\r\n   You may obtain a copy of the License at\r\n\r\n       http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n   Unless required by applicable law or agreed to in writing, software\r\n   distributed under the License is distributed on an \"AS IS\" BASIS,\r\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n   See the License for the specific language governing permissions and\r\n   limitations under the License.\r\n"
  },
  {
    "path": "skills/.curated/playwright/NOTICE.txt",
    "content": "This skill includes material derived from the Microsoft playwright-cli repository.\n\nSource:\n- Repository: microsoft/playwright-cli\n- Path: skills/playwright-cli/SKILL.md\n\nCopyright (c) Microsoft Corporation.\n\nLicensed under the Apache License, Version 2.0.\nSee LICENSE.txt in this directory.\n\nModifications:\n- Adapted for the Codex skill collection.\n- Added a wrapper script and local reference guides.\n"
  },
  {
    "path": "skills/.curated/playwright/SKILL.md",
    "content": "---\nname: \"playwright\"\ndescription: \"Use when the task requires automating a real browser from the terminal (navigation, form filling, snapshots, screenshots, data extraction, UI-flow debugging) via `playwright-cli` or the bundled wrapper script.\"\n---\n\n\n# Playwright CLI Skill\n\nDrive a real browser from the terminal using `playwright-cli`. Prefer the bundled wrapper script so the CLI works even when it is not globally installed.\nTreat this skill as CLI-first automation. Do not pivot to `@playwright/test` unless the user explicitly asks for test files.\n\n## Prerequisite check (required)\n\nBefore proposing commands, check whether `npx` is available (the wrapper depends on it):\n\n```bash\ncommand -v npx >/dev/null 2>&1\n```\n\nIf it is not available, pause and ask the user to install Node.js/npm (which provides `npx`). Provide these steps verbatim:\n\n```bash\n# Verify Node/npm are installed\nnode --version\nnpm --version\n\n# If missing, install Node.js/npm, then:\nnpm install -g @playwright/cli@latest\nplaywright-cli --help\n```\n\nOnce `npx` is present, proceed with the wrapper script. A global install of `playwright-cli` is optional.\n\n## Skill path (set once)\n\n```bash\nexport CODEX_HOME=\"${CODEX_HOME:-$HOME/.codex}\"\nexport PWCLI=\"$CODEX_HOME/skills/playwright/scripts/playwright_cli.sh\"\n```\n\nUser-scoped skills install under `$CODEX_HOME/skills` (default: `~/.codex/skills`).\n\n## Quick start\n\nUse the wrapper script:\n\n```bash\n\"$PWCLI\" open https://playwright.dev --headed\n\"$PWCLI\" snapshot\n\"$PWCLI\" click e15\n\"$PWCLI\" type \"Playwright\"\n\"$PWCLI\" press Enter\n\"$PWCLI\" screenshot\n```\n\nIf the user prefers a global install, this is also valid:\n\n```bash\nnpm install -g @playwright/cli@latest\nplaywright-cli --help\n```\n\n## Core workflow\n\n1. Open the page.\n2. Snapshot to get stable element refs.\n3. Interact using refs from the latest snapshot.\n4. Re-snapshot after navigation or significant DOM changes.\n5. Capture artifacts (screenshot, pdf, traces) when useful.\n\nMinimal loop:\n\n```bash\n\"$PWCLI\" open https://example.com\n\"$PWCLI\" snapshot\n\"$PWCLI\" click e3\n\"$PWCLI\" snapshot\n```\n\n## When to snapshot again\n\nSnapshot again after:\n\n- navigation\n- clicking elements that change the UI substantially\n- opening/closing modals or menus\n- tab switches\n\nRefs can go stale. When a command fails due to a missing ref, snapshot again.\n\n## Recommended patterns\n\n### Form fill and submit\n\n```bash\n\"$PWCLI\" open https://example.com/form\n\"$PWCLI\" snapshot\n\"$PWCLI\" fill e1 \"user@example.com\"\n\"$PWCLI\" fill e2 \"password123\"\n\"$PWCLI\" click e3\n\"$PWCLI\" snapshot\n```\n\n### Debug a UI flow with traces\n\n```bash\n\"$PWCLI\" open https://example.com --headed\n\"$PWCLI\" tracing-start\n# ...interactions...\n\"$PWCLI\" tracing-stop\n```\n\n### Multi-tab work\n\n```bash\n\"$PWCLI\" tab-new https://example.com\n\"$PWCLI\" tab-list\n\"$PWCLI\" tab-select 0\n\"$PWCLI\" snapshot\n```\n\n## Wrapper script\n\nThe wrapper script uses `npx --package @playwright/cli playwright-cli` so the CLI can run without a global install:\n\n```bash\n\"$PWCLI\" --help\n```\n\nPrefer the wrapper unless the repository already standardizes on a global install.\n\n## References\n\nOpen only what you need:\n\n- CLI command reference: `references/cli.md`\n- Practical workflows and troubleshooting: `references/workflows.md`\n\n## Guardrails\n\n- Always snapshot before referencing element ids like `e12`.\n- Re-snapshot when refs seem stale.\n- Prefer explicit commands over `eval` and `run-code` unless needed.\n- When you do not have a fresh snapshot, use placeholder refs like `eX` and say why; do not bypass refs with `run-code`.\n- Use `--headed` when a visual check will help.\n- When capturing artifacts in this repo, use `output/playwright/` and avoid introducing new top-level artifact folders.\n- Default to CLI commands and workflows, not Playwright test specs.\n"
  },
  {
    "path": "skills/.curated/playwright/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Playwright CLI Skill\"\n  short_description: \"Automate real browsers from the terminal\"\n  icon_small: \"./assets/playwright-small.svg\"\n  icon_large: \"./assets/playwright.png\"\n  default_prompt: \"Automate this browser workflow with Playwright and produce a reliable script with run steps.\"\n"
  },
  {
    "path": "skills/.curated/playwright/references/cli.md",
    "content": "# Playwright CLI Reference\n\nUse the wrapper script unless the CLI is already installed globally:\n\n```bash\nexport CODEX_HOME=\"${CODEX_HOME:-$HOME/.codex}\"\nexport PWCLI=\"$CODEX_HOME/skills/playwright/scripts/playwright_cli.sh\"\n\"$PWCLI\" --help\n```\n\nUser-scoped skills install under `$CODEX_HOME/skills` (default: `~/.codex/skills`).\n\nOptional convenience alias:\n\n```bash\nalias pwcli=\"$PWCLI\"\n```\n\n## Core\n\n```bash\npwcli open https://example.com\npwcli close\npwcli snapshot\npwcli click e3\npwcli dblclick e7\npwcli type \"search terms\"\npwcli press Enter\npwcli fill e5 \"user@example.com\"\npwcli drag e2 e8\npwcli hover e4\npwcli select e9 \"option-value\"\npwcli upload ./document.pdf\npwcli check e12\npwcli uncheck e12\npwcli eval \"document.title\"\npwcli eval \"el => el.textContent\" e5\npwcli dialog-accept\npwcli dialog-accept \"confirmation text\"\npwcli dialog-dismiss\npwcli resize 1920 1080\n```\n\n## Navigation\n\n```bash\npwcli go-back\npwcli go-forward\npwcli reload\n```\n\n## Keyboard\n\n```bash\npwcli press Enter\npwcli press ArrowDown\npwcli keydown Shift\npwcli keyup Shift\n```\n\n## Mouse\n\n```bash\npwcli mousemove 150 300\npwcli mousedown\npwcli mousedown right\npwcli mouseup\npwcli mouseup right\npwcli mousewheel 0 100\n```\n\n## Save as\n\n```bash\npwcli screenshot\npwcli screenshot e5\npwcli pdf\n```\n\n## Tabs\n\n```bash\npwcli tab-list\npwcli tab-new\npwcli tab-new https://example.com/page\npwcli tab-close\npwcli tab-close 2\npwcli tab-select 0\n```\n\n## DevTools\n\n```bash\npwcli console\npwcli console warning\npwcli network\npwcli run-code \"await page.waitForTimeout(1000)\"\npwcli tracing-start\npwcli tracing-stop\n```\n\n## Sessions\n\nUse a named session to isolate work:\n\n```bash\npwcli --session todo open https://demo.playwright.dev/todomvc\npwcli --session todo snapshot\n```\n\nOr set an environment variable once:\n\n```bash\nexport PLAYWRIGHT_CLI_SESSION=todo\npwcli open https://demo.playwright.dev/todomvc\n```\n"
  },
  {
    "path": "skills/.curated/playwright/references/workflows.md",
    "content": "# Playwright CLI Workflows\n\nUse the wrapper script and snapshot often.\nAssume `PWCLI` is set and `pwcli` is an alias for `\"$PWCLI\"`.\nIn this repo, run commands from `output/playwright/<label>/` to keep artifacts contained.\n\n## Standard interaction loop\n\n```bash\npwcli open https://example.com\npwcli snapshot\npwcli click e3\npwcli snapshot\n```\n\n## Form submission\n\n```bash\npwcli open https://example.com/form --headed\npwcli snapshot\npwcli fill e1 \"user@example.com\"\npwcli fill e2 \"password123\"\npwcli click e3\npwcli snapshot\npwcli screenshot\n```\n\n## Data extraction\n\n```bash\npwcli open https://example.com\npwcli snapshot\npwcli eval \"document.title\"\npwcli eval \"el => el.textContent\" e12\n```\n\n## Debugging and inspection\n\nCapture console messages and network activity after reproducing an issue:\n\n```bash\npwcli console warning\npwcli network\n```\n\nRecord a trace around a suspicious flow:\n\n```bash\npwcli tracing-start\n# reproduce the issue\npwcli tracing-stop\npwcli screenshot\n```\n\n## Sessions\n\nUse sessions to isolate work across projects:\n\n```bash\npwcli --session marketing open https://example.com\npwcli --session marketing snapshot\npwcli --session checkout open https://example.com/checkout\n```\n\nOr set the session once:\n\n```bash\nexport PLAYWRIGHT_CLI_SESSION=checkout\npwcli open https://example.com/checkout\n```\n\n## Configuration file\n\nBy default, the CLI reads `playwright-cli.json` from the current directory. Use `--config` to point at a specific file.\n\nMinimal example:\n\n```json\n{\n  \"browser\": {\n    \"launchOptions\": {\n      \"headless\": false\n    },\n    \"contextOptions\": {\n      \"viewport\": { \"width\": 1280, \"height\": 720 }\n    }\n  }\n}\n```\n\n## Troubleshooting\n\n- If an element ref fails, run `pwcli snapshot` again and retry.\n- If the page looks wrong, re-open with `--headed` and resize the window.\n- If a flow depends on prior state, use a named `--session`.\n"
  },
  {
    "path": "skills/.curated/playwright/scripts/playwright_cli.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nif ! command -v npx >/dev/null 2>&1; then\n  echo \"Error: npx is required but not found on PATH.\" >&2\n  exit 1\nfi\n\nhas_session_flag=\"false\"\nfor arg in \"$@\"; do\n  case \"$arg\" in\n    --session|--session=*)\n      has_session_flag=\"true\"\n      break\n      ;;\n  esac\ndone\n\ncmd=(npx --yes --package @playwright/cli playwright-cli)\nif [[ \"${has_session_flag}\" != \"true\" && -n \"${PLAYWRIGHT_CLI_SESSION:-}\" ]]; then\n  cmd+=(--session \"${PLAYWRIGHT_CLI_SESSION}\")\nfi\ncmd+=(\"$@\")\n\nexec \"${cmd[@]}\"\n"
  },
  {
    "path": "skills/.curated/playwright-interactive/LICENSE.txt",
    "content": "                                 Apache License\r\n                           Version 2.0, January 2004\r\n                        http://www.apache.org/licenses/\r\n\r\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n   1. Definitions.\r\n\r\n      \"License\" shall mean the terms and conditions for use, reproduction,\r\n      and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n      \"Licensor\" shall mean the copyright owner or entity authorized by\r\n      the copyright owner that is granting the License.\r\n\r\n      \"Legal Entity\" shall mean the union of the acting entity and all\r\n      other entities that control, are controlled by, or are under common\r\n      control with that entity. For the purposes of this definition,\r\n      \"control\" means (i) the power, direct or indirect, to cause the\r\n      direction or management of such entity, whether by contract or\r\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n      outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n      exercising permissions granted by this License.\r\n\r\n      \"Source\" form shall mean the preferred form for making modifications,\r\n      including but not limited to software source code, documentation\r\n      source, and configuration files.\r\n\r\n      \"Object\" form shall mean any form resulting from mechanical\r\n      transformation or translation of a Source form, including but\r\n      not limited to compiled object code, generated documentation,\r\n      and conversions to other media types.\r\n\r\n      \"Work\" shall mean the work of authorship, whether in Source or\r\n      Object form, made available under the License, as indicated by a\r\n      copyright notice that is included in or attached to the work\r\n      (an example is provided in the Appendix below).\r\n\r\n      \"Derivative Works\" shall mean any work, whether in Source or Object\r\n      form, that is based on (or derived from) the Work and for which the\r\n      editorial revisions, annotations, elaborations, or other modifications\r\n      represent, as a whole, an original work of authorship. For the purposes\r\n      of this License, Derivative Works shall not include works that remain\r\n      separable from, or merely link (or bind by name) to the interfaces of,\r\n      the Work and Derivative Works thereof.\r\n\r\n      \"Contribution\" shall mean any work of authorship, including\r\n      the original version of the Work and any modifications or additions\r\n      to that Work or Derivative Works thereof, that is intentionally\r\n      submitted to Licensor for inclusion in the Work by the copyright owner\r\n      or by an individual or Legal Entity authorized to submit on behalf of\r\n      the copyright owner. For the purposes of this definition, \"submitted\"\r\n      means any form of electronic, verbal, or written communication sent\r\n      to the Licensor or its representatives, including but not limited to\r\n      communication on electronic mailing lists, source code control systems,\r\n      and issue tracking systems that are managed by, or on behalf of, the\r\n      Licensor for the purpose of discussing and improving the Work, but\r\n      excluding communication that is conspicuously marked or otherwise\r\n      designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n      on behalf of whom a Contribution has been received by Licensor and\r\n      subsequently incorporated within the Work.\r\n\r\n   2. Grant of Copyright License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      copyright license to reproduce, prepare Derivative Works of,\r\n      publicly display, publicly perform, sublicense, and distribute the\r\n      Work and such Derivative Works in Source or Object form.\r\n\r\n   3. Grant of Patent License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      (except as stated in this section) patent license to make, have made,\r\n      use, offer to sell, sell, import, and otherwise transfer the Work,\r\n      where such license applies only to those patent claims licensable\r\n      by such Contributor that are necessarily infringed by their\r\n      Contribution(s) alone or by combination of their Contribution(s)\r\n      with the Work to which such Contribution(s) was submitted. If You\r\n      institute patent litigation against any entity (including a\r\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n      or a Contribution incorporated within the Work constitutes direct\r\n      or contributory patent infringement, then any patent licenses\r\n      granted to You under this License for that Work shall terminate\r\n      as of the date such litigation is filed.\r\n\r\n   4. Redistribution. You may reproduce and distribute copies of the\r\n      Work or Derivative Works thereof in any medium, with or without\r\n      modifications, and in Source or Object form, provided that You\r\n      meet the following conditions:\r\n\r\n      (a) You must give any other recipients of the Work or\r\n          Derivative Works a copy of this License; and\r\n\r\n      (b) You must cause any modified files to carry prominent notices\r\n          stating that You changed the files; and\r\n\r\n      (c) You must retain, in the Source form of any Derivative Works\r\n          that You distribute, all copyright, patent, trademark, and\r\n          attribution notices from the Source form of the Work,\r\n          excluding those notices that do not pertain to any part of\r\n          the Derivative Works; and\r\n\r\n      (d) If the Work includes a \"NOTICE\" text file as part of its\r\n          distribution, then any Derivative Works that You distribute must\r\n          include a readable copy of the attribution notices contained\r\n          within such NOTICE file, excluding those notices that do not\r\n          pertain to any part of the Derivative Works, in at least one\r\n          of the following places: within a NOTICE text file distributed\r\n          as part of the Derivative Works; within the Source form or\r\n          documentation, if provided along with the Derivative Works; or,\r\n          within a display generated by the Derivative Works, if and\r\n          wherever such third-party notices normally appear. The contents\r\n          of the NOTICE file are for informational purposes only and\r\n          do not modify the License. You may add Your own attribution\r\n          notices within Derivative Works that You distribute, alongside\r\n          or as an addendum to the NOTICE text from the Work, provided\r\n          that such additional attribution notices cannot be construed\r\n          as modifying the License.\r\n\r\n      You may add Your own copyright statement to Your modifications and\r\n      may provide additional or different license terms and conditions\r\n      for use, reproduction, or distribution of Your modifications, or\r\n      for any such Derivative Works as a whole, provided Your use,\r\n      reproduction, and distribution of the Work otherwise complies with\r\n      the conditions stated in this License.\r\n\r\n   5. Submission of Contributions. Unless You explicitly state otherwise,\r\n      any Contribution intentionally submitted for inclusion in the Work\r\n      by You to the Licensor shall be under the terms and conditions of\r\n      this License, without any additional terms or conditions.\r\n      Notwithstanding the above, nothing herein shall supersede or modify\r\n      the terms of any separate license agreement you may have executed\r\n      with Licensor regarding such Contributions.\r\n\r\n   6. Trademarks. This License does not grant permission to use the trade\r\n      names, trademarks, service marks, or product names of the Licensor,\r\n      except as required for reasonable and customary use in describing the\r\n      origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n   7. Disclaimer of Warranty. Unless required by applicable law or\r\n      agreed to in writing, Licensor provides the Work (and each\r\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n      implied, including, without limitation, any warranties or conditions\r\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n      PARTICULAR PURPOSE. You are solely responsible for determining the\r\n      appropriateness of using or redistributing the Work and assume any\r\n      risks associated with Your exercise of permissions under this License.\r\n\r\n   8. Limitation of Liability. In no event and under no legal theory,\r\n      whether in tort (including negligence), contract, or otherwise,\r\n      unless required by applicable law (such as deliberate and grossly\r\n      negligent acts) or agreed to in writing, shall any Contributor be\r\n      liable to You for damages, including any direct, indirect, special,\r\n      incidental, or consequential damages of any character arising as a\r\n      result of this License or out of the use or inability to use the\r\n      Work (including but not limited to damages for loss of goodwill,\r\n      work stoppage, computer failure or malfunction, or any and all\r\n      other commercial damages or losses), even if such Contributor\r\n      has been advised of the possibility of such damages.\r\n\r\n   9. Accepting Warranty or Additional Liability. While redistributing\r\n      the Work or Derivative Works thereof, You may choose to offer,\r\n      and charge a fee for, acceptance of support, warranty, indemnity,\r\n      or other liability obligations and/or rights consistent with this\r\n      License. However, in accepting such obligations, You may act only\r\n      on Your own behalf and on Your sole responsibility, not on behalf\r\n      of any other Contributor, and only if You agree to indemnify,\r\n      defend, and hold each Contributor harmless for any liability\r\n      incurred by, or claims asserted against, such Contributor by reason\r\n      of your accepting any such warranty or additional liability.\r\n\r\n   END OF TERMS AND CONDITIONS\r\n\r\n   APPENDIX: How to apply the Apache License to your work.\r\n\r\n      To apply the Apache License to your work, attach the following\r\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\r\n      replaced with your own identifying information. (Don't include\r\n      the brackets!)  The text should be enclosed in the appropriate\r\n      comment syntax for the file format. We also recommend that a\r\n      file or class name and description of purpose be included on the\r\n      same \"printed page\" as the copyright notice for easier\r\n      identification within third-party archives.\r\n\r\n   Copyright (c) Microsoft Corporation.\r\n\r\n   Licensed under the Apache License, Version 2.0 (the \"License\");\r\n   you may not use this file except in compliance with the License.\r\n   You may obtain a copy of the License at\r\n\r\n       http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n   Unless required by applicable law or agreed to in writing, software\r\n   distributed under the License is distributed on an \"AS IS\" BASIS,\r\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n   See the License for the specific language governing permissions and\r\n   limitations under the License.\r\n"
  },
  {
    "path": "skills/.curated/playwright-interactive/NOTICE.txt",
    "content": "This skill reuses the Playwright icon assets from `.codex/skills/playwright/assets/`.\n\nThe local `playwright` skill attributes those assets to the Microsoft\n`playwright-cli` repository.\n\nCopyright (c) Microsoft Corporation.\n\nLicensed under the Apache License, Version 2.0.\nSee LICENSE.txt in this directory.\n\nModifications:\n- Repackaged the existing repository Playwright assets for this `js_repl`-focused skill.\n- Wrote new skill instructions for persistent browser debugging.\n"
  },
  {
    "path": "skills/.curated/playwright-interactive/SKILL.md",
    "content": "---\nname: \"playwright-interactive\"\ndescription: \"Persistent browser and Electron interaction through `js_repl` for fast iterative UI debugging.\"\n---\n\n# Playwright Interactive Skill\n\nUse a persistent `js_repl` Playwright session to debug local web or Electron apps, keep the same handles alive across iterations, and run functional plus visual QA without restarting the whole toolchain unless the process ownership changed.\n\n## Preconditions\n\n- `js_repl` must be enabled for this skill.\n- If `js_repl` is missing, enable it in `~/.codex/config.toml`:\n\n```toml\n[features]\njs_repl = true\n```\n\n- You can also start a new session with `--enable js_repl` (equivalent to `-c features.js_repl=true`).\n- After enabling `js_repl`, start a new Codex session so the tool list refreshes.\n- For now, run this workflow with sandboxing disabled: start Codex with `--sandbox danger-full-access` (or the equivalent config for `sandbox_mode=danger-full-access`). This is a temporary requirement while `js_repl` + Playwright support inside the sandbox is still being completed.\n- Run setup from the same project directory you need to debug.\n- Treat `js_repl_reset` as a recovery tool, not routine cleanup. Resetting the kernel destroys your Playwright handles.\n\n## One-time setup\n\n```bash\ntest -f package.json || npm init -y\nnpm install playwright\n# Web-only, for headed Chromium or mobile emulation:\n# npx playwright install chromium\n# Electron-only, and only if the target workspace is the app itself:\n# npm install --save-dev electron\nnode -e \"import('playwright').then(() => console.log('playwright import ok')).catch((error) => { console.error(error); process.exit(1); })\"\n```\n\nIf you switch to a different workspace later, repeat setup there.\n\n## Core Workflow\n\n1. Write a brief QA inventory before testing:\n   - Build the inventory from three sources: the user's requested requirements, the user-visible features or behaviors you actually implemented, and the claims you expect to make in the final response.\n   - Anything that appears in any of those three sources must map to at least one QA check before signoff.\n   - List the user-visible claims you intend to sign off on.\n   - List every meaningful user-facing control, mode switch, or implemented interactive behavior.\n   - List the state changes or view changes each control or implemented behavior can cause.\n   - Use this as the shared coverage list for both functional QA and visual QA.\n   - For each claim or control-state pair, note the intended functional check, the specific state where the visual check must happen, and the evidence you expect to capture.\n   - If a requirement is visually central but subjective, convert it into an observable QA check instead of leaving it implicit.\n   - Add at least 2 exploratory or off-happy-path scenarios that could expose fragile behavior.\n2. Run the bootstrap cell once.\n3. Start or confirm any required dev server in a persistent TTY session.\n4. Launch the correct runtime and keep reusing the same Playwright handles.\n5. After each code change, reload for renderer-only changes or relaunch for main-process/startup changes.\n6. Run functional QA with normal user input.\n7. Run a separate visual QA pass.\n8. Verify viewport fit and capture the screenshots needed to support your claims.\n9. Clean up the Playwright session only when the task is actually finished.\n\n## Bootstrap (Run Once)\n\n```javascript\nvar chromium;\nvar electronLauncher;\nvar browser;\nvar context;\nvar page;\nvar mobileContext;\nvar mobilePage;\nvar electronApp;\nvar appWindow;\n\ntry {\n  ({ chromium, _electron: electronLauncher } = await import(\"playwright\"));\n  console.log(\"Playwright loaded\");\n} catch (error) {\n  throw new Error(\n    `Could not load playwright from the current js_repl cwd. Run the setup commands from this workspace first. Original error: ${error}`\n  );\n}\n```\n\nBinding rules:\n\n- Use `var` for the shared top-level Playwright handles because later `js_repl` cells reuse them.\n- The setup cells below are intentionally short happy paths. If a handle looks stale, set that binding to `undefined` and rerun the cell instead of adding recovery logic everywhere.\n- Prefer one named handle per surface you care about (`page`, `mobilePage`, `appWindow`) over repeatedly rediscovering pages from the context.\n\nShared web helpers:\n\n```javascript\nvar resetWebHandles = function () {\n  context = undefined;\n  page = undefined;\n  mobileContext = undefined;\n  mobilePage = undefined;\n};\n\nvar ensureWebBrowser = async function () {\n  if (browser && !browser.isConnected()) {\n    browser = undefined;\n    resetWebHandles();\n  }\n\n  browser ??= await chromium.launch({ headless: false });\n  return browser;\n};\n\nvar reloadWebContexts = async function () {\n  for (const currentContext of [context, mobileContext]) {\n    if (!currentContext) continue;\n    for (const p of currentContext.pages()) {\n      await p.reload({ waitUntil: \"domcontentloaded\" });\n    }\n  }\n  console.log(\"Reloaded existing web tabs\");\n};\n```\n\n## Choose Session Mode\n\nFor web apps, use an explicit viewport by default and treat native-window mode as a separate validation pass.\n\n- Use an explicit viewport for routine iteration, breakpoint checks, reproducible screenshots, snapshot diffs, and model-assisted localization. This is the default because it is stable across machines and avoids host window-manager variability.\n- When you need deterministic high-DPI behavior, keep the explicit viewport and add `deviceScaleFactor` rather than switching straight to native-window mode.\n- Use native-window mode (`viewport: null`) for a separate headed pass when you need to validate launched window size, OS-level DPI behavior, browser chrome interactions, or bugs that may depend on the host display configuration.\n- For Electron, assume native-window behavior all the time. Electron launches through Playwright with `noDefaultViewport`, so treat it like a real desktop window and check the as-launched size and layout before resizing anything.\n- When signoff depends on both layout breakpoints and real desktop behavior, do both passes: explicit viewport first for deterministic QA, then native-window validation for final environment-specific checks.\n- Treat switching modes as a context reset. Do not reuse a viewport-emulated `context` for a native-window pass or vice versa; close the old `page` and `context`, then create a new one for the new mode.\n\n## Start or Reuse Web Session\n\nDesktop and mobile web sessions share the same `browser`, helpers, and QA flow. The main difference is which context and page pair you create.\n\n### Desktop Web Context\n\nSet `TARGET_URL` to the app you are debugging. For local servers, prefer `127.0.0.1` over `localhost`.\n\n```javascript\nvar TARGET_URL = \"http://127.0.0.1:3000\";\n\nif (page?.isClosed()) page = undefined;\n\nawait ensureWebBrowser();\ncontext ??= await browser.newContext({\n  viewport: { width: 1600, height: 900 },\n});\npage ??= await context.newPage();\n\nawait page.goto(TARGET_URL, { waitUntil: \"domcontentloaded\" });\nconsole.log(\"Loaded:\", await page.title());\n```\n\nIf `context` or `page` is stale, set `context = page = undefined` and rerun the cell.\n\n### Mobile Web Context\n\nReuse `TARGET_URL` when it already exists; otherwise set a mobile target directly.\n\n```javascript\nvar MOBILE_TARGET_URL = typeof TARGET_URL === \"string\"\n  ? TARGET_URL\n  : \"http://127.0.0.1:3000\";\n\nif (mobilePage?.isClosed()) mobilePage = undefined;\n\nawait ensureWebBrowser();\nmobileContext ??= await browser.newContext({\n  viewport: { width: 390, height: 844 },\n  isMobile: true,\n  hasTouch: true,\n});\nmobilePage ??= await mobileContext.newPage();\n\nawait mobilePage.goto(MOBILE_TARGET_URL, { waitUntil: \"domcontentloaded\" });\nconsole.log(\"Loaded mobile:\", await mobilePage.title());\n```\n\nIf `mobileContext` or `mobilePage` is stale, set `mobileContext = mobilePage = undefined` and rerun the cell.\n\n### Native-Window Web Pass\n\n```javascript\nvar TARGET_URL = \"http://127.0.0.1:3000\";\n\nawait ensureWebBrowser();\n\nawait page?.close().catch(() => {});\nawait context?.close().catch(() => {});\npage = undefined;\ncontext = undefined;\n\nbrowser ??= await chromium.launch({ headless: false });\ncontext = await browser.newContext({ viewport: null });\npage = await context.newPage();\n\nawait page.goto(TARGET_URL, { waitUntil: \"domcontentloaded\" });\nconsole.log(\"Loaded native window:\", await page.title());\n```\n\n## Start or Reuse Electron Session\n\nSet `ELECTRON_ENTRY` to `.` when the current workspace is the Electron app and `package.json` points `main` to the right entry file. If you need to target a specific main-process file directly, use a path such as `./main.js` instead.\n\n```javascript\nvar ELECTRON_ENTRY = \".\";\n\nif (appWindow?.isClosed()) appWindow = undefined;\n\nif (!appWindow && electronApp) {\n  await electronApp.close().catch(() => {});\n  electronApp = undefined;\n}\n\nelectronApp ??= await electronLauncher.launch({\n  args: [ELECTRON_ENTRY],\n});\n\nappWindow ??= await electronApp.firstWindow();\n\nconsole.log(\"Loaded Electron window:\", await appWindow.title());\n```\n\nIf `js_repl` is not already running from the Electron app workspace, pass `cwd` explicitly when launching.\n\nIf the app process looks stale, set `electronApp = appWindow = undefined` and rerun the cell.\n\nIf you already have an Electron session but need a fresh process after a main-process, preload, or startup change, use the restart cell in the next section instead of rerunning this one.\n\n## Reuse Sessions During Iteration\n\nKeep the same session alive whenever you can.\n\nWeb renderer reload:\n\n```javascript\nawait reloadWebContexts();\n```\n\nElectron renderer-only reload:\n\n```javascript\nawait appWindow.reload({ waitUntil: \"domcontentloaded\" });\nconsole.log(\"Reloaded Electron window\");\n```\n\nElectron restart after main-process, preload, or startup changes:\n\n```javascript\nawait electronApp.close().catch(() => {});\nelectronApp = undefined;\nappWindow = undefined;\n\nelectronApp = await electronLauncher.launch({\n  args: [ELECTRON_ENTRY],\n});\n\nappWindow = await electronApp.firstWindow();\nconsole.log(\"Relaunched Electron window:\", await appWindow.title());\n```\n\nIf your launch requires an explicit `cwd`, include the same `cwd` here.\n\nDefault posture:\n\n- Keep each `js_repl` cell short and focused on one interaction burst.\n- Reuse the existing top-level bindings (`browser`, `context`, `page`, `electronApp`, `appWindow`) instead of redeclaring them.\n- If you need isolation, create a new page or a new context inside the same browser.\n- For Electron, use `electronApp.evaluate(...)` only for main-process inspection or purpose-built diagnostics.\n- Fix helper mistakes in place; do not reset the REPL unless the kernel is actually broken.\n\n## Checklists\n\n### Session Loop\n\n- Bootstrap `js_repl` once, then keep the same Playwright handles alive across iterations.\n- Launch the target runtime from the current workspace.\n- Make the code change.\n- Reload or relaunch using the correct path for that change.\n- Update the shared QA inventory if exploration reveals an additional control, state, or visible claim.\n- Re-run functional QA.\n- Re-run visual QA.\n- Capture final artifacts only after the current state is the one you are evaluating.\n\n### Reload Decision\n\n- Renderer-only change: reload the existing page or Electron window.\n- Main-process, preload, or startup change: relaunch Electron.\n- New uncertainty about process ownership or startup code: relaunch instead of guessing.\n\n### Functional QA\n\n- Use real user controls for signoff: keyboard, mouse, click, touch, or equivalent Playwright input APIs.\n- Verify at least one end-to-end critical flow.\n- Confirm the visible result of that flow, not just internal state.\n- For realtime or animation-heavy apps, verify behavior under actual interaction timing.\n- Work through the shared QA inventory rather than ad hoc spot checks.\n- Cover every obvious visible control at least once before signoff, not only the main happy path.\n- For reversible controls or stateful toggles in the inventory, test the full cycle: initial state, changed state, and return to the initial state.\n- After the scripted checks pass, do a short exploratory pass using normal input for 30-90 seconds instead of following only the intended path.\n- If the exploratory pass reveals a new state, control, or claim, add it to the shared QA inventory and cover it before signoff.\n- `page.evaluate(...)` and `electronApp.evaluate(...)` may inspect or stage state, but they do not count as signoff input.\n\n### Visual QA\n\n- Treat visual QA as separate from functional QA.\n- Use the same shared QA inventory defined before testing and updated during QA; do not start visual coverage from a different implicit list.\n- Restate the user-visible claims and verify each one explicitly; do not assume a functional pass proves a visual claim.\n- A user-visible claim is not signed off until it has been inspected in the specific state where it is meant to be perceived.\n- Inspect the initial viewport before scrolling.\n- Confirm that the initial view visibly supports the interface's primary claims; if a core promised element is not clearly perceptible there, treat that as a bug.\n- Inspect all required visible regions, not just the main interaction surface.\n- Inspect the states and modes already enumerated in the shared QA inventory, including at least one meaningful post-interaction state when the task is interactive.\n- If motion or transitions are part of the experience, inspect at least one in-transition state in addition to the settled endpoints.\n- If labels, overlays, annotations, guides, or highlights are meant to track changing content, verify that relationship after the relevant state change.\n- For dynamic or interaction-dependent visuals, inspect long enough to judge stability, layering, and readability; do not rely on a single screenshot for signoff.\n- For interfaces that can become denser after loading or interaction, inspect the densest realistic state you can reach during QA, not only the empty, loading, or collapsed state.\n- If the product has a defined minimum supported viewport or window size, run a separate visual QA pass there; otherwise, choose a smaller but still realistic size and inspect it explicitly.\n- Distinguish presence from implementation: if an intended affordance is technically there but not clearly perceptible because of weak contrast, occlusion, clipping, or instability, treat that as a visual failure.\n- If any required visible region is clipped, cut off, obscured, or pushed outside the viewport in the state you are evaluating, treat that as a bug even if page-level scroll metrics appear acceptable.\n- Look for clipping, overflow, distortion, layout imbalance, inconsistent spacing, alignment problems, illegible text, weak contrast, broken layering, and awkward motion states.\n- Judge aesthetic quality as well as correctness. The UI should feel intentional, coherent, and visually pleasing for the task.\n- Prefer viewport screenshots for signoff. Use full-page captures only as secondary debugging artifacts, and capture a focused screenshot when a region needs closer inspection.\n- If motion makes a screenshot ambiguous, wait briefly for the UI to settle, then capture the image you are actually evaluating.\n- Before signoff, explicitly ask: what visible part of this interface have I not yet inspected closely?\n- Before signoff, explicitly ask: what visible defect would most likely embarrass this result if the user looked closely?\n\n### Signoff\n\n- The functional path passed with normal user input.\n- Coverage is explicit against the shared QA inventory: note which requirements, implemented features, controls, states, and claims were exercised, and call out any intentional exclusions.\n- The visual QA pass covered the whole relevant interface.\n- Each user-visible claim has a matching visual check and reviewed screenshot artifact from the state and viewport or window size where that claim matters.\n- The viewport-fit checks passed for the intended initial view and any required minimum supported viewport or window size.\n- If the product launches in a window, the as-launched size, placement, and initial layout were checked before any manual resize or repositioning.\n- The UI is not just functional; it is visually coherent and not aesthetically weak for the task.\n- Functional correctness, viewport fit, and visual quality must each pass on their own; one does not imply the others.\n- A short exploratory pass was completed for interactive products, and the response mentions what that pass covered.\n- If screenshot review and numeric checks disagreed at any point, the discrepancy was investigated before signoff; visible clipping in screenshots is a failure to resolve, not something metrics can overrule.\n- Include a brief negative confirmation of the main defect classes you checked for and did not find.\n- Cleanup was executed, or you intentionally kept the session alive for further work.\n\n## Screenshot Examples\n\nIf you plan to emit a screenshot through `codex.emitImage(...)`, use the CSS-normalized paths in the next section by default. Those are the canonical examples for screenshots that will be interpreted by the model or used for coordinate-based follow-up actions. Keep raw captures as an exception for fidelity-sensitive debugging only; the raw exception examples appear after the normalization guidance.\n\n### Model-bound screenshots (default)\n\nIf you will emit a screenshot with `codex.emitImage(...)` for model interpretation, normalize it to CSS pixels for the exact region you captured before emitting. This keeps returned coordinates aligned with Playwright CSS pixels if the reply is later used for clicking, and it also reduces image payload size and model token cost.\n\nDo not emit raw native-window screenshots by default. Skip normalization only when you explicitly need device-pixel fidelity, such as Retina or DPI artifact debugging, pixel-accurate rendering inspection, or another fidelity-sensitive case where raw pixels matter more than payload size. For local-only inspection that will not be emitted to the model, raw capture is fine.\n\nDo not assume `page.screenshot({ scale: \"css\" })` is enough in native-window mode (`viewport: null`). In Chromium on macOS Retina displays, headed native-window screenshots can still come back at device-pixel size even when `scale: \"css\"` is requested. The same caveat applies to Electron windows launched through Playwright because Electron runs with `noDefaultViewport`, and `appWindow.screenshot({ scale: \"css\" })` may still return device-pixel output.\n\nUse separate normalization paths for web pages and Electron windows:\n\n- Web: prefer `page.screenshot({ scale: \"css\" })` directly. If native-window Chromium still returns device-pixel output, resize inside the current page with canvas; no scratch page is required.\n- Electron: do not use `appWindow.context().newPage()` or `electronApp.context().newPage()` as a scratch page. Electron contexts do not support that path reliably. Capture in the main process with `BrowserWindow.capturePage(...)`, resize with `nativeImage.resize(...)`, and emit those bytes directly.\n\nShared helpers and conventions:\n\n```javascript\nvar emitJpeg = async function (bytes) {\n  await codex.emitImage({\n    bytes,\n    mimeType: \"image/jpeg\",\n    detail: \"original\",\n  });\n};\n\nvar emitWebJpeg = async function (surface, options = {}) {\n  await emitJpeg(await surface.screenshot({\n    type: \"jpeg\",\n    quality: 85,\n    scale: \"css\",\n    ...options,\n  }));\n};\n\nvar clickCssPoint = async function ({ surface, x, y, clip }) {\n  await surface.mouse.click(\n    clip ? clip.x + x : x,\n    clip ? clip.y + y : y\n  );\n};\n\nvar tapCssPoint = async function ({ page, x, y, clip }) {\n  await page.touchscreen.tap(\n    clip ? clip.x + x : x,\n    clip ? clip.y + y : y\n  );\n};\n```\n\n- Use `page` or `mobilePage` for web, or `appWindow` for Electron, as the `surface`.\n- Treat `clip` as CSS pixels from `getBoundingClientRect()` in the renderer.\n- Prefer JPEG at `quality: 85` unless lossless fidelity is specifically required.\n- For full-image captures, use returned `{ x, y }` directly.\n- For clipped captures, add the clip origin back when clicking.\n\n### Web CSS normalization\n\nPreferred web path for explicit-viewport contexts, and often for web in general:\n\n```javascript\nawait emitWebJpeg(page);\n```\n\nMobile web uses the same path; substitute `mobilePage` for `page`:\n\n```javascript\nawait emitWebJpeg(mobilePage);\n```\n\nIf the model returns `{ x, y }`, click it directly:\n\n```javascript\nawait clickCssPoint({ surface: page, x, y });\n```\n\nMobile web click path:\n\n```javascript\nawait tapCssPoint({ page: mobilePage, x, y });\n```\n\nFor web `clip` screenshots or element screenshots in this normal path, `scale: \"css\"` usually works directly. Add the region origin back when clicking.\n\n- `await emitWebJpeg(page, { clip })`\n- `await emitWebJpeg(mobilePage, { clip })`\n- `await clickCssPoint({ surface: page, clip, x, y })`\n- `await tapCssPoint({ page: mobilePage, clip, x, y })`\n- `await clickCssPoint({ surface: page, clip: box, x, y })` after `const box = await locator.boundingBox()`\n\nWeb native-window fallback when `scale: \"css\"` still comes back at device-pixel size:\n\n```javascript\nvar emitWebScreenshotCssScaled = async function ({ page, clip, quality = 0.85 } = {}) {\n  var NodeBuffer = (await import(\"node:buffer\")).Buffer;\n  const target = clip\n    ? { width: clip.width, height: clip.height }\n    : await page.evaluate(() => ({\n        width: window.innerWidth,\n        height: window.innerHeight,\n      }));\n\n  const screenshotBuffer = await page.screenshot({\n    type: \"png\",\n    ...(clip ? { clip } : {}),\n  });\n\n  const bytes = await page.evaluate(\n    async ({ imageBase64, targetWidth, targetHeight, quality }) => {\n      const image = new Image();\n      image.src = `data:image/png;base64,${imageBase64}`;\n      await image.decode();\n\n      const canvas = document.createElement(\"canvas\");\n      canvas.width = targetWidth;\n      canvas.height = targetHeight;\n\n      const ctx = canvas.getContext(\"2d\");\n      ctx.imageSmoothingEnabled = true;\n      ctx.drawImage(image, 0, 0, targetWidth, targetHeight);\n\n      const blob = await new Promise((resolve) =>\n        canvas.toBlob(resolve, \"image/jpeg\", quality)\n      );\n\n      return new Uint8Array(await blob.arrayBuffer());\n    },\n    {\n      imageBase64: NodeBuffer.from(screenshotBuffer).toString(\"base64\"),\n      targetWidth: target.width,\n      targetHeight: target.height,\n      quality,\n    }\n  );\n\n  await emitJpeg(bytes);\n};\n```\n\nFor a full viewport fallback capture, treat returned `{ x, y }` as direct CSS coordinates:\n\n```javascript\nawait emitWebScreenshotCssScaled({ page });\nawait clickCssPoint({ surface: page, x, y });\n```\n\nFor a clipped fallback capture, add the clip origin back:\n\n```javascript\nawait emitWebScreenshotCssScaled({ page, clip });\nawait clickCssPoint({ surface: page, clip, x, y });\n```\n\n### Electron CSS normalization\n\nFor Electron, normalize in the main process instead of opening a scratch Playwright page. The helper below returns CSS-scaled bytes for the full content area or for a clipped CSS-pixel region. Treat `clip` as content-area CSS pixels, for example values taken from `getBoundingClientRect()` in the renderer.\n\n```javascript\nvar emitElectronScreenshotCssScaled = async function ({ electronApp, clip, quality = 85 } = {}) {\n  const bytes = await electronApp.evaluate(async ({ BrowserWindow }, { clip, quality }) => {\n    const win = BrowserWindow.getAllWindows()[0];\n    const image = clip ? await win.capturePage(clip) : await win.capturePage();\n\n    const target = clip\n      ? { width: clip.width, height: clip.height }\n      : (() => {\n          const [width, height] = win.getContentSize();\n          return { width, height };\n        })();\n\n    const resized = image.resize({\n      width: target.width,\n      height: target.height,\n      quality: \"best\",\n    });\n\n    return resized.toJPEG(quality);\n  }, { clip, quality });\n\n  await emitJpeg(bytes);\n};\n```\n\nFull Electron window:\n\n```javascript\nawait emitElectronScreenshotCssScaled({ electronApp });\nawait clickCssPoint({ surface: appWindow, x, y });\n```\n\nClipped Electron region using CSS pixels from the renderer:\n\n```javascript\nvar clip = await appWindow.evaluate(() => {\n  const rect = document.getElementById(\"board\").getBoundingClientRect();\n  return {\n    x: Math.round(rect.x),\n    y: Math.round(rect.y),\n    width: Math.round(rect.width),\n    height: Math.round(rect.height),\n  };\n});\n\nawait emitElectronScreenshotCssScaled({ electronApp, clip });\nawait clickCssPoint({ surface: appWindow, clip, x, y });\n```\n\n### Raw Screenshot Exception Examples\n\nUse these only when raw pixels matter more than CSS-coordinate alignment, such as Retina or DPI artifact debugging, pixel-accurate rendering inspection, or other fidelity-sensitive review.\n\nWeb desktop raw emit:\n\n```javascript\nawait codex.emitImage({\n  bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }),\n  mimeType: \"image/jpeg\",\n  detail: \"original\",\n});\n```\n\nElectron raw emit:\n\n```javascript\nawait codex.emitImage({\n  bytes: await appWindow.screenshot({ type: \"jpeg\", quality: 85 }),\n  mimeType: \"image/jpeg\",\n  detail: \"original\",\n});\n```\n\nMobile raw emit after the mobile web context is already running:\n\n```javascript\nawait codex.emitImage({\n  bytes: await mobilePage.screenshot({ type: \"jpeg\", quality: 85 }),\n  mimeType: \"image/jpeg\",\n  detail: \"original\",\n});\n```\n\n## Viewport Fit Checks (Required)\n\nDo not assume a screenshot is acceptable just because the main widget is visible. Before signoff, explicitly verify that the intended initial view matches the product requirement, using both screenshot review and numeric checks.\n\n- Define the intended initial view before signoff. For scrollable pages, this is the above-the-fold experience. For app-like shells, games, editors, dashboards, or tools, this is the full interactive surface plus the controls and status needed to use it.\n- Use screenshots as the primary evidence for fit. Numeric checks support the screenshots; they do not overrule visible clipping.\n- Signoff fails if any required visible region is clipped, cut off, obscured, or pushed outside the viewport in the intended initial view, even if page-level scroll metrics appear acceptable.\n- Scrolling is acceptable when the product is designed to scroll and the initial view still communicates the core experience and exposes the primary call to action or required starting context.\n- For fixed-shell interfaces, scrolling is not an acceptable workaround if it is needed to reach part of the primary interactive surface or essential controls.\n- Do not rely on document scroll metrics alone. Fixed-height shells, internal panes, and hidden-overflow containers can clip required UI while page-level scroll checks still look clean.\n- Check region bounds, not just document bounds. Verify that each required visible region fits within the viewport in the startup state.\n- For Electron or desktop apps, verify both the launched window size and placement and the renderer's initial visible layout before any manual resize or repositioning.\n- Passing viewport-fit checks only proves that the intended initial view is visible without unintended clipping or scrolling. It does not prove that the UI is visually correct or aesthetically successful.\n\nWeb or renderer check:\n\n```javascript\nconsole.log(await page.evaluate(() => ({\n  innerWidth: window.innerWidth,\n  innerHeight: window.innerHeight,\n  clientWidth: document.documentElement.clientWidth,\n  clientHeight: document.documentElement.clientHeight,\n  scrollWidth: document.documentElement.scrollWidth,\n  scrollHeight: document.documentElement.scrollHeight,\n  canScrollX: document.documentElement.scrollWidth > document.documentElement.clientWidth,\n  canScrollY: document.documentElement.scrollHeight > document.documentElement.clientHeight,\n})));\n```\n\nElectron check:\n\n```javascript\nconsole.log(await appWindow.evaluate(() => ({\n  innerWidth: window.innerWidth,\n  innerHeight: window.innerHeight,\n  clientWidth: document.documentElement.clientWidth,\n  clientHeight: document.documentElement.clientHeight,\n  scrollWidth: document.documentElement.scrollWidth,\n  scrollHeight: document.documentElement.scrollHeight,\n  canScrollX: document.documentElement.scrollWidth > document.documentElement.clientWidth,\n  canScrollY: document.documentElement.scrollHeight > document.documentElement.clientHeight,\n})));\n```\n\nAugment the numeric check with `getBoundingClientRect()` checks for the required visible regions in your specific UI when clipping is a realistic failure mode; document-level metrics alone are not sufficient for fixed shells.\n\n## Dev Server\n\nFor local web debugging, keep the app running in a persistent TTY session. Do not rely on one-shot background commands from a short-lived shell.\n\nUse the project's normal start command, for example:\n\n```bash\nnpm start\n```\n\nBefore `page.goto(...)`, verify the chosen port is listening and the app responds.\n\nFor Electron debugging, launch the app from `js_repl` through `_electron.launch(...)` so the same session owns the process. If the Electron renderer depends on a separate dev server (for example Vite or Next), keep that server running in a persistent TTY session and then relaunch or reload the Electron app from `js_repl`.\n\n## Cleanup\n\nOnly run cleanup when the task is actually finished:\n\n- This cleanup is manual. Exiting Codex, closing the terminal, or losing the `js_repl` session does not implicitly run `electronApp.close()`, `context.close()`, or `browser.close()`.\n- For Electron specifically, assume the app may keep running if you leave the session without executing the cleanup cell first.\n\n```javascript\nif (electronApp) {\n  await electronApp.close().catch(() => {});\n}\n\nif (mobileContext) {\n  await mobileContext.close().catch(() => {});\n}\n\nif (context) {\n  await context.close().catch(() => {});\n}\n\nif (browser) {\n  await browser.close().catch(() => {});\n}\n\nbrowser = undefined;\ncontext = undefined;\npage = undefined;\nmobileContext = undefined;\nmobilePage = undefined;\nelectronApp = undefined;\nappWindow = undefined;\n\nconsole.log(\"Playwright session closed\");\n```\n\nIf you plan to exit Codex immediately after debugging, run the cleanup cell first and wait for the `\"Playwright session closed\"` log before quitting.\n\n## Common Failure Modes\n\n- `Cannot find module 'playwright'`: run the one-time setup in the current workspace and verify the import before using `js_repl`.\n- Playwright package is installed but the browser executable is missing: run `npx playwright install chromium`.\n- `page.goto: net::ERR_CONNECTION_REFUSED`: make sure the dev server is still running in a persistent TTY session, recheck the port, and prefer `http://127.0.0.1:<port>`.\n- `electron.launch` hangs, times out, or exits immediately: verify the local `electron` dependency, confirm the `args` target, and make sure any renderer dev server is already running before launch.\n- `Identifier has already been declared`: reuse the existing top-level bindings, choose a new name, or wrap the code in `{ ... }`. Use `js_repl_reset` only when the kernel is genuinely stuck.\n- `browserContext.newPage: Protocol error (Target.createTarget): Not supported` while working with Electron: do not use `appWindow.context().newPage()` or `electronApp.context().newPage()` as a scratch page; use the Electron-specific screenshot normalization flow in the model-bound screenshots section.\n- `js_repl` timed out or reset: rerun the bootstrap cell and recreate the session with shorter, more focused cells.\n- Browser launch or network operations fail immediately: confirm the session was started with `--sandbox danger-full-access` and restart that way if needed.\n"
  },
  {
    "path": "skills/.curated/playwright-interactive/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Playwright Interactive\"\n  short_description: \"Persistent browser and Electron QA\"\n  icon_small: \"./assets/playwright-small.svg\"\n  icon_large: \"./assets/playwright.png\"\n  default_prompt: \"Use $playwright-interactive to debug a local web or Electron app in a persistent Playwright session and capture the QA evidence.\"\n"
  },
  {
    "path": "skills/.curated/render-deploy/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/render-deploy/SKILL.md",
    "content": "---\nname: render-deploy\ndescription: Deploy applications to Render by analyzing codebases, generating render.yaml Blueprints, and providing Dashboard deeplinks. Use when the user wants to deploy, host, publish, or set up their application on Render's cloud platform.\n---\n\n# Deploy to Render\n\nRender supports **Git-backed** services and **prebuilt Docker image** services.\n\nThis skill covers **Git-backed** flows:\n1. **Blueprint Method** - Generate render.yaml for Infrastructure-as-Code deployments\n2. **Direct Creation** - Create services instantly via MCP tools\n\nBlueprints can also run a **prebuilt Docker image** by using `runtime: image`, but the `render.yaml` still must live in a Git repo.\n\nIf there is no Git remote, stop and ask the user to either:\n- Create/push a Git remote (can be minimal if only the Blueprint is needed), or\n- Use the Render Dashboard/API to deploy a prebuilt Docker image (MCP cannot create image-backed services).\n\n## Prerequisites\n\n- When sandboxing blocks the deployment network calls, rerun with `sandbox_permissions=require_escalated`.\n- The deployment might take a few minutes. Use appropriate timeout values.\n\n## When to Use This Skill\n\nActivate this skill when users want to:\n- Deploy an application to Render\n- Create a render.yaml Blueprint file\n- Set up Render deployment for their project\n- Host or publish their application on Render's cloud platform\n- Create databases, cron jobs, or other Render resources\n\n## Happy Path (New Users)\n\nUse this short prompt sequence before deep analysis to reduce friction:\n1. Ask whether they want to deploy from a Git repo or a prebuilt Docker image.\n2. Ask whether Render should provision everything the app needs (based on what seems likely from the user's description) or only the app while they bring their own infra. If dependencies are unclear, ask a short follow-up to confirm whether they need a database, workers, cron, or other services.\n\nThen proceed with the appropriate method below.\n\n## Choose Your Source Path\n\n**Git Repo Path:** Required for both Blueprint and Direct Creation. The repo must be pushed to GitHub, GitLab, or Bitbucket.\n\n**Prebuilt Docker Image Path:** Supported by Render via image-backed services. This is **not** supported by MCP; use the Dashboard/API. Ask for:\n- Image URL (registry + tag)\n- Registry auth (if private)\n- Service type (web/worker) and port\n\nIf the user chooses a Docker image, guide them to the Render Dashboard image deploy flow or ask them to add a Git remote (so you can use a Blueprint with `runtime: image`).\n\n## Choose Your Deployment Method (Git Repo)\n\nBoth methods require a Git repository pushed to GitHub, GitLab, or Bitbucket. (If using `runtime: image`, the repo can be minimal and only contain `render.yaml`.)\n\n| Method | Best For | Pros |\n|--------|----------|------|\n| **Blueprint** | Multi-service apps, IaC workflows | Version controlled, reproducible, supports complex setups |\n| **Direct Creation** | Single services, quick deployments | Instant creation, no render.yaml file needed |\n\n### Method Selection Heuristic\n\nUse this decision rule by default unless the user requests a specific method. Analyze the codebase first; only ask if deployment intent is unclear (e.g., DB, workers, cron).\n\n**Use Direct Creation (MCP) when ALL are true:**\n- Single service (one web app or one static site)\n- No separate worker/cron services\n- No attached databases or Key Value\n- Simple env vars only (no shared env groups)\nIf this path fits and MCP isn't configured yet, stop and guide MCP setup before proceeding.\n\n**Use Blueprint when ANY are true:**\n- Multiple services (web + worker, API + frontend, etc.)\n- Databases, Redis/Key Value, or other datastores are required\n- Cron jobs, background workers, or private services\n- You want reproducible IaC or a render.yaml committed to the repo\n- Monorepo or multi-env setup that needs consistent configuration\n\nIf unsure, ask a quick clarifying question, but default to Blueprint for safety. For a single service, strongly prefer Direct Creation via MCP and guide MCP setup if needed.\n\n## Prerequisites Check\n\nWhen starting a deployment, verify these requirements in order:\n\n**1. Confirm Source Path (Git vs Docker)**\n\nIf using Git-based methods (Blueprint or Direct Creation), the repo must be pushed to GitHub/GitLab/Bitbucket. Blueprints that reference a prebuilt image still require a Git repo with `render.yaml`.\n\n```bash\ngit remote -v\n```\n\n- If no remote exists, stop and ask the user to create/push a remote **or** switch to Docker image deploy.\n\n**2. Check MCP Tools Availability (Preferred for Single-Service)**\n\nMCP tools provide the best experience. Check if available by attempting:\n```\nlist_services()\n```\n\nIf MCP tools are available, you can skip CLI installation for most operations.\n\n**3. Check Render CLI Installation (for Blueprint validation)**\n```bash\nrender --version\n```\nIf not installed, offer to install:\n- macOS: `brew install render`\n- Linux/macOS: `curl -fsSL https://raw.githubusercontent.com/render-oss/cli/main/bin/install.sh | sh`\n\n**4. MCP Setup (if MCP isn't configured)**\n\nIf `list_services()` fails because MCP isn't configured, ask whether they want to set up MCP (preferred) or continue with the CLI fallback. If they choose MCP, ask which AI tool they're using, then provide the matching instructions below. Always use their API key.\n\n### Cursor\n\nWalk the user through these steps:\n\n1) Get a Render API key:\n```\nhttps://dashboard.render.com/u/*/settings#api-keys\n```\n\n2) Add this to `~/.cursor/mcp.json` (replace `<YOUR_API_KEY>`):\n```json\n{\n  \"mcpServers\": {\n    \"render\": {\n      \"url\": \"https://mcp.render.com/mcp\",\n      \"headers\": {\n        \"Authorization\": \"Bearer <YOUR_API_KEY>\"\n      }\n    }\n  }\n}\n```\n\n3) Restart Cursor, then retry `list_services()`.\n\n### Claude Code\n\nWalk the user through these steps:\n\n1) Get a Render API key:\n```\nhttps://dashboard.render.com/u/*/settings#api-keys\n```\n\n2) Add the MCP server with Claude Code (replace `<YOUR_API_KEY>`):\n```bash\nclaude mcp add --transport http render https://mcp.render.com/mcp --header \"Authorization: Bearer <YOUR_API_KEY>\"\n```\n\n3) Restart Claude Code, then retry `list_services()`.\n\n### Codex\n\nWalk the user through these steps:\n\n1) Get a Render API key:\n```\nhttps://dashboard.render.com/u/*/settings#api-keys\n```\n\n2) Set it in their shell:\n```bash\nexport RENDER_API_KEY=\"<YOUR_API_KEY>\"\n```\n\n3) Add the MCP server with the Codex CLI:\n```bash\ncodex mcp add render --url https://mcp.render.com/mcp --bearer-token-env-var RENDER_API_KEY\n```\n\n4) Restart Codex, then retry `list_services()`.\n\n### Other Tools\n\nIf the user is on another AI app, direct them to the Render MCP docs for that tool's setup steps and install method.\n\n### Workspace Selection\n\nAfter MCP is configured, have the user set the active Render workspace with a prompt like:\n\n```\nSet my Render workspace to [WORKSPACE_NAME]\n```\n\n**5. Check Authentication (CLI fallback only)**\n\nIf MCP isn't available, use the CLI instead and verify you can access your account:\n```bash\n# Check if user is logged in (use -o json for non-interactive mode)\nrender whoami -o json\n```\n\nIf `render whoami` fails or returns empty data, the CLI is not authenticated. The CLI won't always prompt automatically, so explicitly prompt the user to authenticate:\n\nIf neither is configured, ask user which method they prefer:\n- **API Key (CLI)**: `export RENDER_API_KEY=\"rnd_xxxxx\"` (Get from https://dashboard.render.com/u/*/settings#api-keys)\n- **Login**: `render login` (Opens browser for OAuth)\n\n**6. Check Workspace Context**\n\nVerify the active workspace:\n```\nget_selected_workspace()\n```\n\nOr via CLI:\n```bash\nrender workspace current -o json\n```\n\nTo list available workspaces:\n```\nlist_workspaces()\n```\n\nIf user needs to switch workspaces, they must do so via Dashboard or CLI (`render workspace set`).\n\nOnce prerequisites are met, proceed with deployment workflow.\n\n---\n\n# Method 1: Blueprint Deployment (Recommended for Complex Apps)\n\n## Blueprint Workflow\n\n### Step 1: Analyze Codebase\n\nAnalyze the codebase to determine framework/runtime, build and start commands, required env vars, datastores, and port binding. Use the detailed checklists in [references/codebase-analysis.md](references/codebase-analysis.md).\n\n### Step 2: Generate render.yaml\n\nCreate a `render.yaml` Blueprint file following the Blueprint specification.\n\nComplete specification: [references/blueprint-spec.md](references/blueprint-spec.md)\n\n**Key Points:**\n- Always use `plan: free` unless user specifies otherwise\n- Include ALL environment variables the app needs\n- Mark secrets with `sync: false` (user fills these in Dashboard)\n- Use appropriate service type: `web`, `worker`, `cron`, `static`, or `pserv`\n- Use appropriate runtime: [references/runtimes.md](references/runtimes.md)\n\n**Basic Structure:**\n```yaml\nservices:\n  - type: web\n    name: my-app\n    runtime: node\n    plan: free\n    buildCommand: npm ci\n    startCommand: npm start\n    envVars:\n      - key: DATABASE_URL\n        fromDatabase:\n          name: postgres\n          property: connectionString\n      - key: JWT_SECRET\n        sync: false  # User fills in Dashboard\n\ndatabases:\n  - name: postgres\n    databaseName: myapp_db\n    plan: free\n```\n\n**Service Types:**\n- `web`: HTTP services, APIs, web applications (publicly accessible)\n- `worker`: Background job processors (not publicly accessible)\n- `cron`: Scheduled tasks that run on a cron schedule\n- `static`: Static sites (HTML/CSS/JS served via CDN)\n- `pserv`: Private services (internal only, within same account)\n\nService type details: [references/service-types.md](references/service-types.md)\nRuntime options: [references/runtimes.md](references/runtimes.md)\nTemplate examples: [assets/](assets/)\n\n### Step 2.5: Immediate Next Steps (Always Provide)\n\nAfter creating `render.yaml`, always give the user a short, explicit checklist and run validation immediately when the CLI is available:\n1. **Authenticate (CLI)**: run `render whoami -o json` (if not logged in, run `render login` or set `RENDER_API_KEY`)\n2. **Validate (recommended)**: run `render blueprints validate`\n   - If the CLI isn't installed, offer to install it and provide the command.\n3. **Commit + push**: `git add render.yaml && git commit -m \"Add Render deployment configuration\" && git push origin main`\n4. **Open Dashboard**: Use the Blueprint deeplink and complete Git OAuth if prompted\n5. **Fill secrets**: Set env vars marked `sync: false`\n6. **Deploy**: Click \"Apply\" and monitor the deploy\n\n### Step 3: Validate Configuration\n\nValidate the render.yaml file to catch errors before deployment. If the CLI is installed, run the commands directly; only prompt the user if the CLI is missing:\n\n```bash\nrender whoami -o json  # Ensure CLI is authenticated (won't always prompt)\nrender blueprints validate\n```\n\nFix any validation errors before proceeding. Common issues:\n- Missing required fields (`name`, `type`, `runtime`)\n- Invalid runtime values\n- Incorrect YAML syntax\n- Invalid environment variable references\n\nConfiguration guide: [references/configuration-guide.md](references/configuration-guide.md)\n\n### Step 4: Commit and Push\n\n**IMPORTANT:** You must merge the `render.yaml` file into your repository before deploying.\n\nEnsure the `render.yaml` file is committed and pushed to your Git remote:\n\n```bash\ngit add render.yaml\ngit commit -m \"Add Render deployment configuration\"\ngit push origin main\n```\n\nIf there is no Git remote yet, stop here and guide the user to create a GitHub/GitLab/Bitbucket repo, add it as `origin`, and push before continuing.\n\n**Why this matters:** The Dashboard deeplink will read the render.yaml from your repository. If the file isn't merged and pushed, Render won't find the configuration and deployment will fail.\n\nVerify the file is in your remote repository before proceeding to the next step.\n\n### Step 5: Generate Deeplink\n\nGet the Git repository URL:\n\n```bash\ngit remote get-url origin\n```\n\nThis will return a URL from your Git provider. **If the URL is SSH format, convert it to HTTPS:**\n\n| SSH Format | HTTPS Format |\n|------------|--------------|\n| `git@github.com:user/repo.git` | `https://github.com/user/repo` |\n| `git@gitlab.com:user/repo.git` | `https://gitlab.com/user/repo` |\n| `git@bitbucket.org:user/repo.git` | `https://bitbucket.org/user/repo` |\n\n**Conversion pattern:** Replace `git@<host>:` with `https://<host>/` and remove `.git` suffix.\n\nFormat the Dashboard deeplink using the HTTPS repository URL:\n```\nhttps://dashboard.render.com/blueprint/new?repo=<REPOSITORY_URL>\n```\n\nExample:\n```\nhttps://dashboard.render.com/blueprint/new?repo=https://github.com/username/repo-name\n```\n\n### Step 6: Guide User\n\n**CRITICAL:** Ensure the user has merged and pushed the render.yaml file to their repository before clicking the deeplink. If the file isn't in the repository, Render cannot read the Blueprint configuration and deployment will fail.\n\nProvide the deeplink to the user with these instructions:\n\n1. **Verify render.yaml is merged** - Confirm the file exists in your repository on GitHub/GitLab/Bitbucket\n2. Click the deeplink to open Render Dashboard\n3. Complete Git provider OAuth if prompted\n4. Name the Blueprint (or use default from render.yaml)\n5. Fill in secret environment variables (marked with `sync: false`)\n6. Review services and databases configuration\n7. Click \"Apply\" to deploy\n\nThe deployment will begin automatically. Users can monitor progress in the Render Dashboard.\n\n### Step 7: Verify Deployment\n\nAfter the user deploys via Dashboard, verify everything is working.\n\n**Check deployment status via MCP:**\n```\nlist_deploys(serviceId: \"<service-id>\", limit: 1)\n```\nLook for `status: \"live\"` to confirm successful deployment.\n\n**Check for runtime errors (wait 2-3 minutes after deploy):**\n```\nlist_logs(resource: [\"<service-id>\"], level: [\"error\"], limit: 20)\n```\n\n**Check service health metrics:**\n```\nget_metrics(\n  resourceId: \"<service-id>\",\n  metricTypes: [\"http_request_count\", \"cpu_usage\", \"memory_usage\"]\n)\n```\n\nIf errors are found, proceed to the **Post-deploy verification and basic triage** section below.\n\n---\n\n# Method 2: Direct Service Creation (Quick Single-Service Deployments)\n\nFor simple deployments without Infrastructure-as-Code, create services directly via MCP tools.\n\n## When to Use Direct Creation\n\n- Single web service or static site\n- Quick prototypes or demos\n- When you don't need a render.yaml file in your repo\n- Adding databases or cron jobs to existing projects\n\n## Prerequisites for Direct Creation\n\n**Repository must be pushed to a Git provider.** Render clones your repository to build and deploy services.\n\n```bash\ngit remote -v  # Verify remote exists\ngit push origin main  # Ensure code is pushed\n```\n\nSupported providers: GitHub, GitLab, Bitbucket\n\nIf no remote exists, stop and ask the user to create/push a remote or switch to Docker image deploy.\n\n**Note:** MCP does not support creating image-backed services. Use the Dashboard/API for prebuilt Docker image deploys.\n\n## Direct Creation Workflow\n\nUse the concise steps below, and refer to [references/direct-creation.md](references/direct-creation.md) for full MCP command examples and follow-on configuration.\n\n### Step 1: Analyze Codebase\nUse [references/codebase-analysis.md](references/codebase-analysis.md) to determine runtime, build/start commands, env vars, and datastores.\n\n### Step 2: Create Resources via MCP\nCreate the service (web or static) and any required databases or key-value stores. See [references/direct-creation.md](references/direct-creation.md).\n\nIf MCP returns an error about missing Git credentials or repo access, stop and guide the user to connect their Git provider in the Render Dashboard, then retry.\n\n### Step 3: Configure Environment Variables\nAdd required env vars via MCP after creation. See [references/direct-creation.md](references/direct-creation.md).\n\nRemind the user that secrets can be set in the Dashboard if they prefer not to pass them via MCP.\n\n### Step 4: Verify Deployment\nCheck deploy status, logs, and metrics. See [references/direct-creation.md](references/direct-creation.md).\n\n---\n\nFor service discovery, configuration details, quick commands, and common issues, see [references/deployment-details.md](references/deployment-details.md).\n\n---\n\n# Post-deploy verification and basic triage (All Methods)\n\nKeep this short and repeatable. If any check fails, fix it before redeploying.\n\n1. Confirm the latest deploy is `live` and serving traffic\n2. Hit the health endpoint (or root) and verify a 200 response\n3. Scan recent error logs for a clear failure signature\n4. Verify required env vars and port binding (`0.0.0.0:$PORT`)\n\nDetailed checklist and commands: [references/post-deploy-checks.md](references/post-deploy-checks.md)\n\nIf the service fails to start or health checks time out, use the basic triage guide:\n[references/troubleshooting-basics.md](references/troubleshooting-basics.md)\n\n## Troubleshooting\n\n### Escalated Network Access\n\nIf deployment fails due to network issues (timeouts, DNS errors, connection resets), rerun the deploy with escalated permissions (use `sandbox_permissions=require_escalated`). The deploy requires escalated network access when sandbox networking blocks outbound requests.\n\nExample guidance to the user:\n\n```\nThe deploy needs escalated network access to deploy to Render. I can rerun the command with escalated permissions—want me to proceed?\n```\n\nOptional: If you need deeper diagnostics (metrics/DB checks/error catalog), suggest installing the\n`render-debug` skill. It is not required for the core deploy flow.\n"
  },
  {
    "path": "skills/.curated/render-deploy/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Render Deploy\"\n  short_description: \"Deploy applications to Render via Blueprints or MCP\"\n  icon_small: \"./assets/render-small.svg\"\n  icon_large: \"./assets/render.png\"\n  default_prompt: \"Deploy this application to Render and provide service URL, env vars, and next checks.\"\n\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"render\"\n      description: \"Render MCP server\"\n      transport: \"streamable_http\"\n      url: \"https://mcp.render.com/mcp\"\n"
  },
  {
    "path": "skills/.curated/render-deploy/assets/docker.yaml",
    "content": "# Docker-based Service\n# Deploy any application using a Dockerfile\n\nservices:\n  - type: web\n    name: docker-app\n    runtime: docker\n    plan: free\n    region: oregon\n    branch: main\n    autoDeploy: true\n    dockerfilePath: ./Dockerfile  # Path to your Dockerfile\n    dockerContext: .              # Build context directory\n    healthCheckPath: /health\n    envVars:\n      - key: PORT\n        value: 10000\n      - key: ENVIRONMENT\n        value: production\n      - key: DATABASE_URL\n        fromDatabase:\n          name: postgres\n          property: connectionString\n      - key: REDIS_URL\n        fromDatabase:\n          name: redis\n          property: connectionString\n      - key: SECRET_KEY\n        sync: false  # User provides in Dashboard\n\ndatabases:\n  - name: postgres\n    databaseName: app_production\n    user: app_user\n    plan: free\n    postgresMajorVersion: \"15\"\n    ipAllowList: []\n\n  - name: redis\n    plan: free\n    maxmemoryPolicy: allkeys-lru\n    ipAllowList: []\n\n# Example multi-stage Dockerfile:\n#\n# # Build stage\n# FROM node:20-alpine AS builder\n# WORKDIR /app\n# COPY package*.json ./\n# RUN npm ci\n# COPY . .\n# RUN npm run build\n#\n# # Production stage\n# FROM node:20-alpine\n# WORKDIR /app\n# COPY --from=builder /app/dist ./dist\n# COPY --from=builder /app/node_modules ./node_modules\n# COPY package*.json ./\n# ENV NODE_ENV=production\n# EXPOSE 10000\n# CMD [\"node\", \"dist/main.js\"]\n"
  },
  {
    "path": "skills/.curated/render-deploy/assets/go-api.yaml",
    "content": "# Go API Service\n# High-performance Go web service with PostgreSQL\n\nservices:\n  - type: web\n    name: go-api\n    runtime: go\n    plan: free\n    region: oregon\n    branch: main\n    autoDeploy: true\n    buildCommand: go build -o bin/app -ldflags=\"-s -w\" .\n    startCommand: ./bin/app\n    healthCheckPath: /health\n    envVars:\n      - key: PORT\n        value: 10000\n      - key: ENVIRONMENT\n        value: production\n      - key: DATABASE_URL\n        fromDatabase:\n          name: postgres\n          property: connectionString\n      - key: JWT_SECRET\n        sync: false  # User provides in Dashboard\n      - key: API_KEY\n        sync: false  # User provides in Dashboard\n\ndatabases:\n  - name: postgres\n    databaseName: go_api_production\n    user: go_api_user\n    plan: free\n    postgresMajorVersion: \"15\"\n    ipAllowList: []  # Internal access only\n"
  },
  {
    "path": "skills/.curated/render-deploy/assets/nextjs-postgres.yaml",
    "content": "# Next.js Application with PostgreSQL\n# Full-stack Next.js app with database\n\nservices:\n  - type: web\n    name: nextjs-app\n    runtime: node\n    plan: free\n    region: oregon\n    branch: main\n    autoDeploy: true\n    buildCommand: npm ci && npm run build\n    startCommand: npm start\n    healthCheckPath: /api/health\n    envVars:\n      - key: NODE_ENV\n        value: production\n      - key: DATABASE_URL\n        fromDatabase:\n          name: postgres\n          property: connectionString\n      - key: NEXTAUTH_URL\n        value: https://nextjs-app.onrender.com\n      - key: NEXTAUTH_SECRET\n        sync: false  # User provides in Dashboard\n      - key: JWT_SECRET\n        generateValue: true\n\ndatabases:\n  - name: postgres\n    databaseName: nextjs_production\n    user: nextjs_user\n    plan: free\n    postgresMajorVersion: \"15\"\n    ipAllowList: []  # Internal access only\n"
  },
  {
    "path": "skills/.curated/render-deploy/assets/node-express.yaml",
    "content": "# Node.js Express API\n# Basic web service with Express.js framework\n\nservices:\n  - type: web\n    name: express-api\n    runtime: node\n    plan: free\n    region: oregon\n    branch: main\n    autoDeploy: true\n    buildCommand: npm ci\n    startCommand: npm start\n    healthCheckPath: /health\n    envVars:\n      - key: NODE_ENV\n        value: production\n      # PORT is automatically provided by Render (default: 10000)\n      # Only uncomment if you need to override:\n      # - key: PORT\n      #   value: 10000\n      - key: LOG_LEVEL\n        value: info\n      - key: API_KEY\n        sync: false  # User provides in Dashboard\n"
  },
  {
    "path": "skills/.curated/render-deploy/assets/python-django.yaml",
    "content": "# Django Application with Worker and Databases\n# Full Django stack with Celery worker, PostgreSQL, and Redis\n\nservices:\n  # Django web service\n  - type: web\n    name: django-web\n    runtime: python\n    plan: free\n    region: oregon\n    branch: main\n    autoDeploy: true\n    buildCommand: pip install -r requirements.txt && python manage.py collectstatic --no-input && python manage.py migrate\n    startCommand: gunicorn config.wsgi:application --bind 0.0.0.0:$PORT --workers 2\n    healthCheckPath: /health/\n    envVars:\n      - key: PYTHON_VERSION\n        value: 3.11.5\n      - key: DJANGO_SETTINGS_MODULE\n        value: config.settings.production\n      - key: DJANGO_SECRET_KEY\n        sync: false  # User provides in Dashboard\n      - key: DJANGO_ALLOWED_HOSTS\n        value: django-web.onrender.com\n      - key: DATABASE_URL\n        fromDatabase:\n          name: postgres\n          property: connectionString\n      - key: REDIS_URL\n        fromDatabase:\n          name: redis\n          property: connectionString\n\n  # Celery worker for background tasks\n  - type: worker\n    name: celery-worker\n    runtime: python\n    plan: free\n    region: oregon\n    branch: main\n    autoDeploy: true\n    buildCommand: pip install -r requirements.txt\n    startCommand: celery -A config.celery_app worker --loglevel=info --concurrency=2\n    envVars:\n      - key: DJANGO_SETTINGS_MODULE\n        value: config.settings.production\n      - key: DJANGO_SECRET_KEY\n        sync: false  # Same as web service\n      - key: DATABASE_URL\n        fromDatabase:\n          name: postgres\n          property: connectionString\n      - key: REDIS_URL\n        fromDatabase:\n          name: redis\n          property: connectionString\n\n  # Celery beat for periodic tasks (optional)\n  - type: worker\n    name: celery-beat\n    runtime: python\n    plan: free\n    region: oregon\n    branch: main\n    autoDeploy: true\n    buildCommand: pip install -r requirements.txt\n    startCommand: celery -A config.celery_app beat --loglevel=info\n    envVars:\n      - key: DJANGO_SETTINGS_MODULE\n        value: config.settings.production\n      - key: REDIS_URL\n        fromDatabase:\n          name: redis\n          property: connectionString\n\ndatabases:\n  # PostgreSQL database\n  - name: postgres\n    databaseName: django_production\n    user: django_user\n    plan: free\n    postgresMajorVersion: \"15\"\n    ipAllowList: []  # Internal access only\n\n  # Redis for Celery and caching\n  - name: redis\n    plan: free\n    maxmemoryPolicy: allkeys-lru\n    ipAllowList: []  # Internal access only\n"
  },
  {
    "path": "skills/.curated/render-deploy/assets/static-site.yaml",
    "content": "# Static Site (React/Vue/Gatsby)\n# SPA with client-side routing\n\nservices:\n  - type: web\n    name: react-app\n    runtime: static\n    plan: free\n    branch: main\n    autoDeploy: true\n    buildCommand: npm ci && npm run build\n    staticPublishPath: ./build  # Change to ./dist for Vue/Vite, ./public for Gatsby\n\n    # SPA routing - rewrite all routes to index.html\n    routes:\n      - type: rewrite\n        source: /*\n        destination: /index.html\n\n    # Cache control headers\n    headers:\n      # Cache static assets aggressively\n      - path: /static/*\n        name: Cache-Control\n        value: public, max-age=31536000, immutable\n\n      # Cache other assets for 1 hour\n      - path: /assets/*\n        name: Cache-Control\n        value: public, max-age=3600\n\n      # Don't cache index.html\n      - path: /index.html\n        name: Cache-Control\n        value: no-cache, no-store, must-revalidate\n\n      # Security headers\n      - path: /*\n        name: X-Frame-Options\n        value: DENY\n\n      - path: /*\n        name: X-Content-Type-Options\n        value: nosniff\n\n      - path: /*\n        name: Referrer-Policy\n        value: strict-origin-when-cross-origin\n\n    # Environment variables for build (if needed)\n    envVars:\n      - key: REACT_APP_API_URL\n        value: https://api.example.com\n      # Add other REACT_APP_ or VITE_ variables here\n"
  },
  {
    "path": "skills/.curated/render-deploy/references/blueprint-spec.md",
    "content": "# Render Blueprint Specification\n\nComplete reference for render.yaml Blueprint files. Blueprints define your infrastructure as code for reproducible deployments on Render.\n\n## Overview\n\nA Blueprint is a YAML file (typically `render.yaml`) placed in your repository root that describes:\n- Services (web, worker, cron, static, private)\n- Databases (PostgreSQL, Redis)\n- Environment variables and secrets\n- Scaling and resource configuration\n- Project organization\n\n## Root-Level Structure\n\n```yaml\n# Top-level fields\nservices: []         # Array of service definitions\ndatabases: []        # Array of PostgreSQL databases\nenvVarGroups: []     # Reusable environment variable groups (optional)\nprojects: []         # Project organization (optional)\nungrouped: []        # Resources outside projects (optional)\npreviews:            # Preview environment configuration (optional)\n  generation: auto_preview | manual | none\n```\n\n## Service Types\n\n### Web Services (`type: web`)\n\nHTTP services, APIs, and web applications. Publicly accessible via HTTPS.\n\n**Required fields:**\n- `name`: Unique service identifier\n- `type`: Must be `web`\n- `runtime`: Language/environment (see Runtimes section)\n- `buildCommand`: Command to build the application\n- `startCommand`: Command to start the server\n\n**Common optional fields:**\n- `plan`: Instance type (default: `free`)\n- `region`: Deployment region (default: `oregon`)\n- `branch`: Git branch to deploy (default: `main`)\n- `autoDeploy`: Auto-deploy on push (default: `true`)\n- `envVars`: Environment variables array\n- `healthCheckPath`: Health check endpoint (default: `/`)\n- `numInstances`: Number of instances (manual scaling)\n- `scaling`: Autoscaling configuration\n\n**Example:**\n```yaml\nservices:\n  - type: web\n    name: api-server\n    runtime: node\n    plan: free\n    buildCommand: npm ci\n    startCommand: npm start\n    branch: main\n    autoDeploy: true\n    envVars:\n      - key: NODE_ENV\n        value: production\n      - key: PORT\n        value: 10000\n```\n\n### Worker Services (`type: worker`)\n\nBackground job processors, queue consumers. Not publicly accessible.\n\n**Required fields:**\n- `name`: Unique service identifier\n- `type`: Must be `worker`\n- `runtime`: Language/environment\n- `buildCommand`: Command to build\n- `startCommand`: Command to start worker process\n\n**Key differences from web services:**\n- No public URL\n- No health checks\n- No port binding required\n\n**Example:**\n```yaml\nservices:\n  - type: worker\n    name: job-processor\n    runtime: python\n    plan: free\n    buildCommand: pip install -r requirements.txt\n    startCommand: celery -A tasks worker --loglevel=info\n    envVars:\n      - key: REDIS_URL\n        fromDatabase:\n          name: redis\n          property: connectionString\n```\n\n### Cron Jobs (`type: cron`)\n\nScheduled tasks that run on a cron schedule.\n\n**Required fields:**\n- `name`: Unique service identifier\n- `type`: Must be `cron`\n- `runtime`: Language/environment\n- `schedule`: Cron expression\n- `buildCommand`: Command to build\n- `startCommand`: Command to execute on schedule\n\n**Schedule format:** Standard cron syntax (minute hour day month weekday)\n\n**Examples:**\n- `0 0 * * *` - Daily at midnight UTC\n- `*/15 * * * *` - Every 15 minutes\n- `0 9 * * 1` - Every Monday at 9 AM UTC\n\n**Example:**\n```yaml\nservices:\n  - type: cron\n    name: daily-backup\n    runtime: node\n    schedule: \"0 2 * * *\"\n    buildCommand: npm ci\n    startCommand: node scripts/backup.js\n    envVars:\n      - key: DATABASE_URL\n        fromDatabase:\n          name: postgres\n          property: connectionString\n```\n\n### Static Sites (`type: static` or `type: web` with `runtime: static`)\n\nServe static HTML/CSS/JS files via CDN.\n\n**Required fields:**\n- `name`: Unique service identifier\n- `type`: `web`\n- `runtime`: `static`\n- `buildCommand`: Command to build static assets\n- `staticPublishPath`: Path to built files (e.g., `./build`, `./dist`)\n\n**Optional configuration:**\n- `routes`: Routing rules for SPAs\n- `headers`: Custom HTTP headers\n- `buildFilter`: Path filters for build triggers\n\n**Example:**\n```yaml\nservices:\n  - type: web\n    name: react-app\n    runtime: static\n    buildCommand: npm ci && npm run build\n    staticPublishPath: ./dist\n    routes:\n      - type: rewrite\n        source: /*\n        destination: /index.html\n    headers:\n      - path: /*\n        name: Cache-Control\n        value: public, max-age=31536000, immutable\n```\n\n### Private Services (`type: pserv`)\n\nInternal services accessible only within your Render account.\n\n**Required fields:**\n- `name`: Unique service identifier\n- `type`: Must be `pserv`\n- `runtime`: Language/environment\n- `buildCommand`: Command to build\n- `startCommand`: Command to start\n\n**Use cases:**\n- Internal APIs\n- Database proxies\n- Microservices not exposed to internet\n\n**Example:**\n```yaml\nservices:\n  - type: pserv\n    name: internal-api\n    runtime: go\n    plan: free\n    buildCommand: go build -o bin/app\n    startCommand: ./bin/app\n```\n\n## Runtimes\n\n### Native Runtimes\n\n**Node.js (`runtime: node`):**\n- Versions: 14, 16, 18, 20, 21\n- Default version: 20\n- Specify version in `package.json` engines field\n\n**Python (`runtime: python`):**\n- Versions: 3.8, 3.9, 3.10, 3.11, 3.12\n- Default version: 3.11\n- Specify version in `runtime.txt` or `Pipfile`\n\n**Go (`runtime: go`):**\n- Versions: 1.20, 1.21, 1.22, 1.23\n- Uses go modules\n- Version from `go.mod`\n\n**Ruby (`runtime: ruby`):**\n- Versions: 3.0, 3.1, 3.2, 3.3\n- Uses Bundler\n- Version from `.ruby-version` or `Gemfile`\n\n**Rust (`runtime: rust`):**\n- Latest stable version\n- Uses Cargo\n\n**Elixir (`runtime: elixir`):**\n- Latest stable version\n- Uses Mix\n\n### Docker Runtime\n\n**Docker (`runtime: docker`):**\nBuild from a Dockerfile in your repository.\n\n**Additional fields:**\n- `dockerfilePath`: Path to Dockerfile (default: `./Dockerfile`)\n- `dockerContext`: Build context directory (default: `.`)\n\n**Example:**\n```yaml\nservices:\n  - type: web\n    name: docker-app\n    runtime: docker\n    dockerfilePath: ./docker/Dockerfile\n    dockerContext: .\n    plan: free\n```\n\n**Image (`runtime: image`):**\nDeploy pre-built Docker images from a registry.\n\n**Additional fields:**\n- `image`: Image URL (e.g., `registry.com/image:tag`)\n- `registryCredential`: Credentials for private registries\n\n**Example:**\n```yaml\nservices:\n  - type: web\n    name: prebuilt-app\n    runtime: image\n    image: myregistry.com/app:v1.2.3\n    plan: free\n```\n\n## Service Plans\n\nAvailable instance types:\n\n| Plan | RAM | CPU | Price |\n|------|-----|-----|-------|\n| `free` | 512 MB | 0.5 | Free (750 hrs/mo) |\n| `starter` | 512 MB | 0.5 | $7/month |\n| `standard` | 2 GB | 1 | $25/month |\n| `pro` | 4 GB | 2 | $85/month |\n| `pro_plus` | 8 GB | 4 | $175/month |\n\n**Always default to `plan: free` unless user specifies otherwise.**\n\n## Regions\n\nAvailable deployment regions:\n\n- `oregon` (US West) - Default\n- `ohio` (US East)\n- `virginia` (US East)\n- `frankfurt` (EU)\n- `singapore` (Asia)\n\n**Example:**\n```yaml\nservices:\n  - type: web\n    name: my-app\n    runtime: node\n    region: frankfurt\n```\n\n## Environment Variables\n\nThree patterns for defining environment variables:\n\n### 1. Hardcoded Values\n\nFor non-sensitive configuration:\n\n```yaml\nenvVars:\n  - key: NODE_ENV\n    value: production\n  - key: API_URL\n    value: https://api.example.com\n  - key: LOG_LEVEL\n    value: info\n```\n\n### 2. Generated Secrets\n\nRender generates a base64-encoded 256-bit random value:\n\n```yaml\nenvVars:\n  - key: SESSION_SECRET\n    generateValue: true\n  - key: ENCRYPTION_KEY\n    generateValue: true\n```\n\n### 3. User-Provided Secrets\n\nPrompt user for values during Blueprint creation:\n\n```yaml\nenvVars:\n  - key: STRIPE_SECRET_KEY\n    sync: false\n  - key: JWT_SECRET\n    sync: false\n  - key: API_KEY\n    sync: false\n```\n\n**The `sync: false` flag means \"user will fill this in the Dashboard\".**\n\n### 4. Database References\n\nLink to database connection strings:\n\n```yaml\nenvVars:\n  - key: DATABASE_URL\n    fromDatabase:\n      name: postgres\n      property: connectionString\n  - key: REDIS_URL\n    fromDatabase:\n      name: redis\n      property: connectionString\n```\n\n**Available properties:**\n- `connectionString`: Full connection URL\n- `host`: Database host\n- `port`: Database port\n- `user`: Database username\n- `password`: Database password\n- `database`: Database name\n- `hostport`: Combined `host:port`\n\n### 5. Service References\n\nLink to other services:\n\n```yaml\nenvVars:\n  - key: API_URL\n    fromService:\n      name: api-server\n      type: web\n      property: host\n```\n\n### 6. Environment Variable Groups\n\nReusable groups shared across services:\n\n```yaml\nenvVarGroups:\n  - name: shared-config\n    envVars:\n      - key: LOG_LEVEL\n        value: info\n      - key: ENVIRONMENT\n        value: production\n\nservices:\n  - type: web\n    name: web-app\n    runtime: node\n    envVars:\n      - fromGroup: shared-config\n      - key: PORT\n        value: 10000\n```\n\n## Databases\n\n### PostgreSQL\n\n```yaml\ndatabases:\n  - name: postgres\n    databaseName: myapp_prod\n    user: myapp_user\n    plan: free\n    postgresMajorVersion: \"15\"\n    ipAllowList: []\n```\n\n**Plans:**\n- `free`: 1 GB storage, 97 MB RAM, 0.1 CPU\n- `basic-256mb`, `basic-512mb`, `basic-1gb`, `basic-4gb`\n- `pro-4gb`, `pro-8gb`, `pro-16gb`, etc.\n- `accelerated-4gb`, `accelerated-8gb`, etc. (SSD-backed)\n\n**Key fields:**\n- `name`: Identifier for references\n- `databaseName`: Actual PostgreSQL database name\n- `user`: Database username\n- `postgresMajorVersion`: PostgreSQL version (11-16)\n- `ipAllowList`: Array of CIDR blocks (empty = internal only)\n- `diskSizeGB`: Storage size (paid plans only)\n\n**High Availability (paid plans):**\n```yaml\ndatabases:\n  - name: postgres\n    databaseName: myapp_prod\n    plan: pro-4gb\n    highAvailabilityEnabled: true\n```\n\n**Read Replicas (paid plans):**\n```yaml\ndatabases:\n  - name: postgres\n    databaseName: myapp_prod\n    plan: pro-4gb\n    readReplicas:\n      - name: read-replica-1\n        region: ohio\n      - name: read-replica-2\n        region: frankfurt\n```\n\n### Redis (Key-Value Store)\n\n```yaml\ndatabases:\n  - name: redis\n    plan: free\n    maxmemoryPolicy: allkeys-lru\n    ipAllowList: []\n```\n\n**Plans:** Same as PostgreSQL\n\n**maxmemoryPolicy options:**\n- `allkeys-lru`: Evict least recently used keys\n- `volatile-lru`: Evict LRU keys with TTL\n- `allkeys-random`: Evict random keys\n- `volatile-random`: Evict random keys with TTL\n- `volatile-ttl`: Evict keys with soonest TTL\n- `noeviction`: Return errors when memory full\n\n## Scaling\n\n### Manual Scaling\n\nFixed number of instances:\n\n```yaml\nservices:\n  - type: web\n    name: my-app\n    runtime: node\n    plan: standard\n    numInstances: 3\n```\n\n### Autoscaling\n\nDynamic scaling based on CPU/memory (Professional workspace required):\n\n```yaml\nservices:\n  - type: web\n    name: my-app\n    runtime: node\n    plan: standard\n    scaling:\n      minInstances: 1\n      maxInstances: 5\n      targetCPUPercent: 60\n      targetMemoryPercent: 70\n```\n\n**Notes:**\n- Autoscaling disabled in preview environments\n- Preview environments run `minInstances` count\n- Requires Professional or higher workspace\n\n## Health Checks\n\nConfigure health check endpoints:\n\n```yaml\nservices:\n  - type: web\n    name: my-app\n    runtime: node\n    healthCheckPath: /health\n```\n\n**Default:** `/` (root path)\n\n**Recommended:** Add a dedicated `/health` endpoint that returns `200 OK`.\n\n## Build Filters\n\nControl when builds are triggered based on changed files:\n\n```yaml\nservices:\n  - type: web\n    name: frontend\n    runtime: static\n    buildFilter:\n      paths:\n        - frontend/**\n      ignoredPaths:\n        - frontend/README.md\n        - frontend/**/*.test.js\n```\n\n**Behavior:**\n- If `paths` specified: Build only when files in those paths change\n- If `ignoredPaths` specified: Don't build when only ignored files change\n\n## Projects and Environments\n\nOrganize services into projects with multiple environments:\n\n```yaml\nprojects:\n  - name: my-application\n    environments:\n      - name: production\n        services:\n          - type: web\n            name: prod-api\n            runtime: node\n            plan: pro\n            buildCommand: npm ci\n            startCommand: npm start\n        databases:\n          - name: prod-postgres\n            plan: pro-4gb\n        networking:\n          isolation: enabled\n        permissions:\n          protection: enabled\n\n      - name: staging\n        services:\n          - type: web\n            name: staging-api\n            runtime: node\n            plan: starter\n            buildCommand: npm ci\n            startCommand: npm start\n        databases:\n          - name: staging-postgres\n            plan: free\n```\n\n**Environment features:**\n- `networking.isolation`: Enable network isolation between environments\n- `permissions.protection`: Require approval for environment changes\n\n## Preview Environments\n\nConfigure automatic preview environments for pull requests:\n\n```yaml\npreviews:\n  generation: auto_preview  # auto_preview | manual | none\n```\n\n**Options:**\n- `auto_preview`: Create preview environment for each PR automatically\n- `manual`: User manually triggers preview creation\n- `none`: Disable preview environments\n\n## Complete Example\n\nFull-featured Blueprint with multiple services and databases:\n\n```yaml\nservices:\n  # Web service\n  - type: web\n    name: web-app\n    runtime: node\n    plan: free\n    region: oregon\n    buildCommand: npm ci && npm run build\n    startCommand: npm start\n    branch: main\n    autoDeploy: true\n    healthCheckPath: /health\n    envVars:\n      - key: NODE_ENV\n        value: production\n      - key: DATABASE_URL\n        fromDatabase:\n          name: postgres\n          property: connectionString\n      - key: REDIS_URL\n        fromDatabase:\n          name: redis\n          property: connectionString\n      - key: JWT_SECRET\n        sync: false\n\n  # Background worker\n  - type: worker\n    name: queue-worker\n    runtime: node\n    plan: free\n    buildCommand: npm ci\n    startCommand: node worker.js\n    envVars:\n      - key: REDIS_URL\n        fromDatabase:\n          name: redis\n          property: connectionString\n\n  # Cron job\n  - type: cron\n    name: daily-cleanup\n    runtime: node\n    schedule: \"0 3 * * *\"\n    buildCommand: npm ci\n    startCommand: node scripts/cleanup.js\n    envVars:\n      - key: DATABASE_URL\n        fromDatabase:\n          name: postgres\n          property: connectionString\n\n  # Static frontend\n  - type: web\n    name: frontend\n    runtime: static\n    buildCommand: npm ci && npm run build\n    staticPublishPath: ./dist\n    routes:\n      - type: rewrite\n        source: /*\n        destination: /index.html\n\ndatabases:\n  - name: postgres\n    databaseName: app_production\n    user: app_user\n    plan: free\n    postgresMajorVersion: \"15\"\n    ipAllowList: []\n\n  - name: redis\n    plan: free\n    maxmemoryPolicy: allkeys-lru\n    ipAllowList: []\n```\n\n## Validation\n\nValidate your Blueprint before deploying (when CLI command is available):\n\n```bash\nrender blueprint validate\n```\n\n**Common validation errors:**\n- Missing required fields\n- Invalid runtime values\n- Incorrect environment variable references\n- Invalid cron expressions\n- Invalid YAML syntax\n\n## Best Practices\n\n1. **Always use `plan: free` by default** - Let users upgrade if needed\n2. **Mark all secrets with `sync: false`** - Never hardcode sensitive values\n3. **Use `fromDatabase` for database URLs** - Automatic internal connection strings\n4. **Add health check endpoints** - Faster deployment detection\n5. **Use non-interactive build commands** - Prevents build hangs\n6. **Bind to `0.0.0.0:$PORT`** - Required for web services\n7. **Use environment variable groups** - Share config across services\n8. **Enable autoDeploy: true** - Deploy automatically on push\n9. **Set appropriate regions** - Choose closest to your users\n10. **Use build filters** - Optimize build triggers in monorepos\n\n## Additional Resources\n\n- Official Blueprint Specification: https://render.com/docs/blueprint-spec\n- Render CLI Documentation: https://render.com/docs/cli\n- Environment Variables Guide: https://render.com/docs/environment-variables\n"
  },
  {
    "path": "skills/.curated/render-deploy/references/codebase-analysis.md",
    "content": "# Codebase Analysis (Deploy)\n\nUse this reference for framework-specific detection and build/start command selection when preparing a Render deployment.\n\n## Node.js Projects\n- Read `package.json` to detect framework (Express, Next.js, Nest.js, Fastify, etc.)\n- Check `scripts` section for build/start commands\n- Look for `engines` field for Node version, or look in `.node-versions` or `.nvmrc`\n- Detect package manager:\n  - `bun.lockb` (Bun) -> `bun install --frozen-lockfile` / `bun run start`\n  - `pnpm-lock.yaml` (pnpm) -> `pnpm install --frozen-lockfile` / `pnpm start`\n  - `yarn.lock` (Yarn) -> `yarn install --frozen-lockfile` / `yarn start`\n  - `package-lock.json` (npm) -> `npm ci` / `npm start`\n  - `package.json` only (npm fallback) -> `npm install` / `npm start`\n\n## Python Projects\n- Check for dependency files and detect package manager:\n  - `uv.lock` (uv) -> `uv sync` / `uv run gunicorn app:app`\n  - `poetry.lock` (Poetry) -> `poetry install --no-dev` / `poetry run gunicorn app:app`\n  - `Pipfile.lock` (pipenv) -> `pipenv install --deploy` / `pipenv run gunicorn app:app`\n  - `requirements.txt` (pip) -> `pip install -r requirements.txt` / `gunicorn app:app`\n  - `pyproject.toml` only -> check for `[tool.uv]`, `[tool.poetry]`, or use pip\n- Detect framework: Django, Flask, FastAPI, Celery, others\n- Check for Python version:\n  - `.python-version` (uv/pyenv)\n  - `runtime.txt` (Render-specific)\n  - `pyproject.toml` (requires-python field)\n\n## Go Projects\n- Read `go.mod` for dependencies\n- Identify web framework (Gin, Echo, Chi, Fiber, net/http)\n- Note Go version from `go.mod`\n\n## Static Sites\n- Look for build output directories (`build/`, `dist/`, `site/`, `public/`)\n- Detect framework: React, Vue, Gatsby, Next.js (static export)\n- Check build scripts in `package.json`\n\n## Docker Projects\n- Look for `Dockerfile`\n- Note exposed ports and build stages\n- Check for `docker-compose.yml` patterns\n\n## Key Information to Extract\n- Build command (e.g., `npm ci`, `pip install -r requirements.txt`, `go build`)\n- Start command (e.g., `npm start`, `gunicorn app:app`, `./bin/app`)\n- Environment variables used in code (API keys, database URLs, secrets)\n- Database requirements (PostgreSQL, Redis, MongoDB)\n- Port binding (check if app uses an environment variable for port to run on)\n"
  },
  {
    "path": "skills/.curated/render-deploy/references/configuration-guide.md",
    "content": "# Render Configuration Guide\n\nCommon configuration patterns, best practices, and troubleshooting for Render deployments.\n\n## Environment Variables\n\n### Required vs Optional Variables\n\n**Always declare ALL environment variables in render.yaml**, even if values are provided by user later.\n\n**Three categories:**\n\n1. **Configuration values** (hardcoded):\n```yaml\nenvVars:\n  - key: NODE_ENV\n    value: production\n  - key: LOG_LEVEL\n    value: info\n  - key: API_URL\n    value: https://api.example.com\n```\n\n2. **Secrets** (user provides):\n```yaml\nenvVars:\n  - key: JWT_SECRET\n    sync: false\n  - key: STRIPE_SECRET_KEY\n    sync: false\n  - key: API_KEY\n    sync: false\n```\n\n3. **Auto-generated** (Render provides):\n```yaml\nenvVars:\n  - key: SESSION_SECRET\n    generateValue: true\n  - key: ENCRYPTION_KEY\n    generateValue: true\n```\n\n### Database Connection Patterns\n\n**PostgreSQL:**\n```yaml\nenvVars:\n  - key: DATABASE_URL\n    fromDatabase:\n      name: postgres\n      property: connectionString\n```\n\n**Redis:**\n```yaml\nenvVars:\n  - key: REDIS_URL\n    fromDatabase:\n      name: redis\n      property: connectionString\n```\n\n**Multiple databases:**\n```yaml\nenvVars:\n  - key: PRIMARY_DB_URL\n    fromDatabase:\n      name: postgres-primary\n      property: connectionString\n  - key: ANALYTICS_DB_URL\n    fromDatabase:\n      name: postgres-analytics\n      property: connectionString\n  - key: CACHE_URL\n    fromDatabase:\n      name: redis\n      property: connectionString\n```\n\n### Cross-Service References\n\nReference other services in your account:\n\n```yaml\nservices:\n  - type: web\n    name: frontend\n    runtime: node\n    envVars:\n      - key: API_URL\n        fromService:\n          name: backend-api\n          type: web\n          property: host  # or hostport, port\n\n  - type: web\n    name: backend-api\n    runtime: node\n```\n\n**Available properties:**\n- `host`: Service hostname\n- `port`: Service port\n- `hostport`: Combined `host:port`\n\n### Environment Variable Groups\n\nShare common configuration across services:\n\n```yaml\nenvVarGroups:\n  - name: common-config\n    envVars:\n      - key: NODE_ENV\n        value: production\n      - key: LOG_LEVEL\n        value: info\n      - key: TZ\n        value: UTC\n\nservices:\n  - type: web\n    name: web-app\n    runtime: node\n    envVars:\n      - fromGroup: common-config\n      - key: PORT\n        value: 10000\n\n  - type: worker\n    name: worker\n    runtime: node\n    envVars:\n      - fromGroup: common-config\n```\n\n---\n\n## Port Binding\n\n### The Port Binding Requirement\n\n**CRITICAL:** Web services must bind to `0.0.0.0:$PORT`\n\n**Why this matters:**\n- Render sets `PORT` environment variable (default: 10000)\n- Services must bind to `0.0.0.0` (not `localhost` or `127.0.0.1`)\n- Health checks fail if port binding is incorrect\n- Deployment will fail or service won't receive traffic\n\n### Code Examples by Language\n\n**Node.js / Express:**\n```javascript\nconst express = require('express');\nconst app = express();\n\nconst PORT = process.env.PORT || 3000;\n\napp.listen(PORT, '0.0.0.0', () => {\n  console.log(`Server running on port ${PORT}`);\n});\n```\n\n**Python / Flask:**\n```python\nimport os\nfrom flask import Flask\n\napp = Flask(__name__)\n\nif __name__ == '__main__':\n    port = int(os.environ.get('PORT', 5000))\n    app.run(host='0.0.0.0', port=port)\n```\n\n**Python / Django:**\n\nIn `settings.py`:\n```python\n# Django runs on port specified by environment\nALLOWED_HOSTS = ['*']\n```\n\nStart command in render.yaml:\n```yaml\nstartCommand: gunicorn config.wsgi:application --bind 0.0.0.0:$PORT\n```\n\n**Python / FastAPI:**\n```python\nimport os\nimport uvicorn\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\nif __name__ == \"__main__\":\n    port = int(os.environ.get(\"PORT\", 8000))\n    uvicorn.run(app, host=\"0.0.0.0\", port=port)\n```\n\nStart command:\n```yaml\nstartCommand: uvicorn main:app --host 0.0.0.0 --port $PORT\n```\n\n**Go:**\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"net/http\"\n    \"os\"\n)\n\nfunc main() {\n    port := os.Getenv(\"PORT\")\n    if port == \"\" {\n        port = \"3000\"\n    }\n\n    http.HandleFunc(\"/\", handler)\n    fmt.Printf(\"Server starting on port %s\\n\", port)\n    http.ListenAndServe(\":\"+port, nil)\n}\n```\n\n**Ruby / Rails:**\n\nIn `config/puma.rb`:\n```ruby\nport ENV.fetch(\"PORT\") { 3000 }\nbind \"tcp://0.0.0.0:#{ENV.fetch('PORT', 3000)}\"\n```\n\n**Rust / Actix:**\n```rust\nuse actix_web::{App, HttpServer};\nuse std::env;\n\n#[actix_web::main]\nasync fn main() -> std::io::Result<()> {\n    let port = env::var(\"PORT\").unwrap_or_else(|_| \"8080\".to_string());\n    let addr = format!(\"0.0.0.0:{}\", port);\n\n    HttpServer::new(|| App::new())\n        .bind(&addr)?\n        .run()\n        .await\n}\n```\n\n---\n\n## Build Commands\n\n### Non-Interactive Flags\n\n**Always use non-interactive flags** to prevent builds from hanging waiting for input.\n\n**npm (Node.js):**\n```yaml\nbuildCommand: npm ci\n# NOT: npm install\n```\n\n**pip (Python):**\n```yaml\nbuildCommand: pip install -r requirements.txt\n# Already non-interactive\n```\n\n**apt (System packages):**\n```yaml\nbuildCommand: apt-get update && apt-get install -y libpq-dev\n# Use -y flag to auto-confirm\n```\n\n**bundler (Ruby):**\n```yaml\nbuildCommand: bundle install --jobs=4 --retry=3\n```\n\n### Build with Additional Steps\n\n**Node.js with build step:**\n```yaml\nbuildCommand: npm ci && npm run build\n```\n\n**Python Django with static files:**\n```yaml\nbuildCommand: pip install -r requirements.txt && python manage.py collectstatic --no-input\n```\n\n**Ruby Rails with assets:**\n```yaml\nbuildCommand: bundle install && bundle exec rails assets:precompile\n```\n\n### Build Timeouts\n\n**Free tier:** 15 minutes\n**Paid tiers:** Configurable\n\n**If builds timeout:**\n1. Optimize dependencies (remove unused packages)\n2. Use build caching\n3. Consider pre-building in CI/CD\n4. Upgrade to paid tier for longer timeouts\n\n---\n\n## Database Connections\n\n### Internal vs External URLs\n\n**Use internal URLs for better performance:**\n\nWhen using `fromDatabase`, Render automatically provides internal `.render-internal.com` URLs:\n\n```yaml\nenvVars:\n  - key: DATABASE_URL\n    fromDatabase:\n      name: postgres\n      property: connectionString\n```\n\nThis provides: `postgresql://user:pass@postgres.render-internal.com:5432/db`\n\n**Benefits:**\n- Lower latency (same data center)\n- No external bandwidth charges\n- Automatic internal DNS\n\n### Connection Pooling\n\n**Node.js / PostgreSQL:**\n```javascript\nconst { Pool } = require('pg');\n\nconst pool = new Pool({\n  connectionString: process.env.DATABASE_URL,\n  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,\n  max: 20, // Maximum pool size\n  idleTimeoutMillis: 30000,\n  connectionTimeoutMillis: 2000,\n});\n```\n\n**Python / PostgreSQL:**\n```python\nimport psycopg2.pool\n\npool = psycopg2.pool.SimpleConnectionPool(\n    minconn=1,\n    maxconn=20,\n    dsn=os.environ['DATABASE_URL']\n)\n```\n\n**Django Settings:**\n```python\nDATABASES = {\n    'default': {\n        'ENGINE': 'django.db.backends.postgresql',\n        'URL': os.environ['DATABASE_URL'],\n        'CONN_MAX_AGE': 600,  # Connection pooling\n    }\n}\n```\n\n### Database Migrations\n\n**Run migrations during build:**\n\n**Django:**\n```yaml\nbuildCommand: pip install -r requirements.txt && python manage.py migrate\n```\n\n**Rails:**\n```yaml\nbuildCommand: bundle install && bundle exec rails db:migrate\n```\n\n**Node.js / Prisma:**\n```yaml\nbuildCommand: npm ci && npx prisma migrate deploy\n```\n\n---\n\n## Free Tier Limitations\n\n### What's Included\n\n**Free tier provides:**\n- 1 web service\n- 1 PostgreSQL database (1 GB storage, 97 MB RAM)\n- 750 hours/month compute\n- 512 MB RAM per service\n- 0.5 CPU per service\n- 100 GB bandwidth/month\n\n### Resource Limits\n\n**Memory (512 MB):**\n- Monitor memory usage in logs\n- Optimize for memory-constrained environments\n- Use lightweight dependencies\n\n**CPU (0.5 cores):**\n- Suitable for low-traffic applications\n- Consider upgrading for higher traffic\n\n**Spin Down (Free services):**\n- Services spin down after 15 minutes of inactivity\n- First request after spin down takes ~30 seconds (cold start)\n- Upgrade to paid tier for always-on services\n\n### When to Upgrade\n\n**Upgrade to paid plan when:**\n- Need more than 1 web service\n- Need always-on services (no spin down)\n- Traffic exceeds free tier limits\n- Need more memory/CPU\n- Need faster build times\n- Need preview environments\n\n---\n\n## Health Checks\n\n### Adding Health Check Endpoints\n\n**Node.js / Express:**\n```javascript\napp.get('/health', (req, res) => {\n  res.status(200).json({\n    status: 'ok',\n    timestamp: new Date().toISOString()\n  });\n});\n```\n\n**Python / Flask:**\n```python\n@app.route('/health')\ndef health():\n    return {'status': 'ok'}, 200\n```\n\n**Python / FastAPI:**\n```python\n@app.get(\"/health\")\nasync def health():\n    return {\"status\": \"ok\"}\n```\n\n**Go:**\n```go\nhttp.HandleFunc(\"/health\", func(w http.ResponseWriter, r *http.Request) {\n    w.WriteHeader(http.StatusOK)\n    w.Write([]byte(`{\"status\":\"ok\"}`))\n})\n```\n\n### Configure in render.yaml\n\n```yaml\nservices:\n  - type: web\n    name: my-app\n    runtime: node\n    healthCheckPath: /health\n```\n\n**Benefits:**\n- Faster deployment detection\n- Better monitoring\n- Automatic restart on health check failures\n\n---\n\n## Common Deployment Issues\n\n### Issue 1: Missing Environment Variables\n\n**Symptom:** Service crashes with \"undefined variable\" errors\n\n**Solution:** Add all required env vars to render.yaml:\n```yaml\nenvVars:\n  - key: DATABASE_URL\n    fromDatabase:\n      name: postgres\n      property: connectionString\n  - key: JWT_SECRET\n    sync: false  # User fills in Dashboard\n```\n\n### Issue 2: Port Binding Errors\n\n**Symptom:** `EADDRINUSE` or health check timeout errors\n\n**Solution:** Ensure app binds to `0.0.0.0:$PORT`:\n```javascript\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, '0.0.0.0');\n```\n\n### Issue 3: Build Hangs\n\n**Symptom:** Build times out after 15 minutes\n\n**Solution:** Use non-interactive build commands:\n```yaml\nbuildCommand: npm ci  # NOT npm install\n```\n\n### Issue 4: Database Connection Fails\n\n**Symptom:** `ECONNREFUSED` on port 5432\n\n**Solutions:**\n1. Use `fromDatabase` for automatic internal URLs\n2. Enable SSL for external connections\n3. Check `ipAllowList` settings\n\n### Issue 5: Static Site 404s\n\n**Symptom:** Client-side routes return 404\n\n**Solution:** Add SPA rewrite rules:\n```yaml\nroutes:\n  - type: rewrite\n    source: /*\n    destination: /index.html\n```\n\n### Issue 6: Out of Memory (OOM)\n\n**Symptom:** Service crashes with `JavaScript heap out of memory`\n\n**Solutions:**\n1. Optimize application memory usage\n2. Reduce dependency size\n3. Upgrade to higher plan with more RAM\n\n---\n\n## Best Practices Checklist\n\n**Environment Variables:**\n- [ ] All env vars declared in render.yaml\n- [ ] Secrets marked with `sync: false`\n- [ ] Database URLs use `fromDatabase` references\n\n**Port Binding:**\n- [ ] App binds to `process.env.PORT`\n- [ ] Bind to `0.0.0.0` (not `localhost`)\n\n**Build Commands:**\n- [ ] Use non-interactive flags (`npm ci`, `-y`, etc.)\n- [ ] Build completes under 15 minutes (free tier)\n\n**Start Commands:**\n- [ ] Command starts HTTP server correctly\n- [ ] Server binds to correct port\n\n**Health Checks:**\n- [ ] `/health` endpoint implemented\n- [ ] Returns 200 status code\n\n**Database:**\n- [ ] Connection pooling configured\n- [ ] Using internal URLs (`.render-internal.com`)\n- [ ] SSL enabled if needed\n\n**Plans:**\n- [ ] Using `plan: free` by default\n- [ ] Documented upgrade path for users\n\n**Git Repository:**\n- [ ] render.yaml committed to repository\n- [ ] Pushed to git remote (GitHub/GitLab/Bitbucket)\n- [ ] Branch specified in render.yaml (if not main)\n\n---\n\n## Additional Resources\n\n- Blueprint Specification: [blueprint-spec.md](blueprint-spec.md)\n- Service Types: [service-types.md](service-types.md)\n- Runtimes: [runtimes.md](runtimes.md)\n- Official Render Docs: https://render.com/docs\n"
  },
  {
    "path": "skills/.curated/render-deploy/references/deployment-details.md",
    "content": "# Deployment Details\n\nUse this reference for service discovery, configuration patterns, quick commands, and common issues.\n\n## Service Discovery\n\n**List all services:**\n```\nlist_services()\n```\nReturns all services with IDs, names, types, and status.\n\n**Get specific service details:**\n```\nget_service(serviceId: \"<id>\")\n```\nReturns full configuration including environment variables and build/start commands.\n\n**List PostgreSQL databases:**\n```\nlist_postgres_instances()\n```\n\n**List Key-Value stores:**\n```\nlist_key_value()\n```\n\n## Configuration Details\n\n### Environment Variables\n\n**All environment variables must be declared in render.yaml.**\n\n**Three patterns for environment variables:**\n\n1. **Hardcoded values** (non-sensitive configuration):\n```yaml\nenvVars:\n  - key: NODE_ENV\n    value: production\n  - key: API_URL\n    value: https://api.example.com\n```\n\n2. **Database connections** (auto-generated):\n```yaml\nenvVars:\n  - key: DATABASE_URL\n    fromDatabase:\n      name: postgres\n      property: connectionString\n  - key: REDIS_URL\n    fromDatabase:\n      name: redis\n      property: connectionString\n```\n\n3. **Secrets** (user fills in Dashboard):\n```yaml\nenvVars:\n  - key: JWT_SECRET\n    sync: false\n  - key: API_KEY\n    sync: false\n  - key: STRIPE_SECRET_KEY\n    sync: false\n```\n\nComplete environment variable guide: [configuration-guide.md](configuration-guide.md)\n\n### Port Binding\n\n**CRITICAL:** Web services must bind to `0.0.0.0:$PORT` (NOT `localhost`). Render sets the `PORT` environment variable.\n\n**Node.js Example:**\n```javascript\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, '0.0.0.0', () => {\n  console.log(`Server running on port ${PORT}`);\n});\n```\n\n**Python Example:**\n```python\nimport os\n\nport = int(os.environ.get('PORT', 5000))\napp.run(host='0.0.0.0', port=port)\n```\n\n**Go Example:**\n```go\nport := os.Getenv(\"PORT\")\nif port == \"\" {\n    port = \"3000\"\n}\nhttp.ListenAndServe(\":\"+port, handler)\n```\n\n### Plan Defaults\n\n**Use `plan: free` unless the user specifies otherwise.** Refer to Render pricing for current limits and capacity.\n\n### Build Commands\n\n**Use non-interactive flags to prevent build hangs:**\n- npm: `npm ci`\n- yarn: `yarn install --frozen-lockfile`\n- pnpm: `pnpm install --frozen-lockfile`\n- bun: `bun install --frozen-lockfile`\n- pip: `pip install -r requirements.txt`\n- uv: `uv sync`\n- apt: `apt-get install -y <package>`\n- bundler: `bundle install --jobs=4 --retry=3`\n\n### Database Connections\n\nWhen services connect to databases in the same Render account, use `fromDatabase` references for internal URLs.\n\n### Health Checks\n\nOptional but recommended: add a `/health` endpoint for faster deployment detection.\n\n## Quick Reference\n\n### MCP Tools (Preferred)\n```\n# Service Discovery\nlist_services()\nget_service(serviceId: \"<id>\")\nlist_postgres_instances()\nlist_key_value()\n\n# Service Creation\ncreate_web_service(name, runtime, buildCommand, startCommand, ...)\ncreate_static_site(name, buildCommand, publishPath, ...)\ncreate_cron_job(name, runtime, schedule, buildCommand, startCommand, ...)\ncreate_postgres(name, plan, region)\ncreate_key_value(name, plan, region)\n\n# Environment Variables\nupdate_environment_variables(serviceId, envVars: [{key, value}, ...])\n\n# Deployment & Monitoring\nlist_deploys(serviceId, limit)\nlist_logs(resource: [\"<id>\"], level: [\"error\"])\nget_metrics(resourceId, metricTypes: [...])\n\n# Workspace\nget_selected_workspace()\nlist_workspaces()\n```\n\n### CLI Commands\n```bash\n# Validate Blueprint\nrender blueprints validate\n\n# Check workspace\nrender workspace current -o json\nrender workspace set\n\n# List services\nrender services -o json\n\n# View deployment logs\nrender logs -r <service-id> -o json\n\n# Create deployment\nrender deploys create <service-id> --wait\n```\n\n### Templates by Framework\n- Node.js Express: [../assets/node-express.yaml](../assets/node-express.yaml)\n- Next.js + Postgres: [../assets/nextjs-postgres.yaml](../assets/nextjs-postgres.yaml)\n- Django + Worker: [../assets/python-django.yaml](../assets/python-django.yaml)\n- Static Site: [../assets/static-site.yaml](../assets/static-site.yaml)\n- Go API: [../assets/go-api.yaml](../assets/go-api.yaml)\n- Docker: [../assets/docker.yaml](../assets/docker.yaml)\n\n### Documentation\n- Full Blueprint specification: [blueprint-spec.md](blueprint-spec.md)\n- Service types explained: [service-types.md](service-types.md)\n- Runtime options: [runtimes.md](runtimes.md)\n- Configuration guide: [configuration-guide.md](configuration-guide.md)\n\n## Common Issues\n\n**Issue:** Deployment fails with port binding error\n\n**Solution:** Ensure app binds to `0.0.0.0:$PORT` (see Port Binding section above)\n\n---\n\n**Issue:** Build hangs or times out\n\n**Solution:** Use non-interactive build commands (see Build Commands section above)\n\n---\n\n**Issue:** Missing environment variables in Dashboard\n\n**Solution:** All env vars must be declared in render.yaml. Add missing vars with `sync: false` for secrets.\n\n---\n\n**Issue:** Database connection fails\n\n**Solution:** Use `fromDatabase` references for internal connection strings.\n\n---\n\n**Issue:** Static site shows 404 for routes\n\n**Solution:** Add rewrite rules to render.yaml for SPA routing:\n```yaml\nroutes:\n  - type: rewrite\n    source: /*\n    destination: /index.html\n```\n\nFor more detailed troubleshooting, see the debug skill or [configuration-guide.md](configuration-guide.md).\n"
  },
  {
    "path": "skills/.curated/render-deploy/references/direct-creation.md",
    "content": "# Direct Creation (MCP) Details\n\nUse this reference for MCP direct-creation examples and follow-on configuration.\n\n## Direct Creation Workflow\n\n### Step 1: Analyze Codebase\n\nUse [codebase-analysis.md](codebase-analysis.md) to determine runtime, build/start commands, env vars, and datastores.\n\n### Step 2: Create Resources via MCP\n\n**Create a Web Service:**\n```\ncreate_web_service(\n  name: \"my-api\",\n  runtime: \"node\",  # or python, go, rust, ruby, elixir, docker\n  repo: \"https://github.com/username/repo\",\n  branch: \"main\",  # optional, defaults to repo default branch\n  buildCommand: \"npm ci\",\n  startCommand: \"npm start\",\n  plan: \"free\",  # free, starter, standard, pro, pro_max, pro_plus, pro_ultra\n  region: \"oregon\",  # oregon, frankfurt, singapore, ohio, virginia\n  envVars: [\n    {\"key\": \"NODE_ENV\", \"value\": \"production\"}\n  ]\n)\n```\n\n**Create a Static Site:**\n```\ncreate_static_site(\n  name: \"my-frontend\",\n  repo: \"https://github.com/username/repo\",\n  branch: \"main\",\n  buildCommand: \"npm run build\",\n  publishPath: \"dist\",  # or build, public, out\n  envVars: [\n    {\"key\": \"VITE_API_URL\", \"value\": \"https://api.example.com\"}\n  ]\n)\n```\n\n**Create a Cron Job:**\n```\ncreate_cron_job(\n  name: \"daily-cleanup\",\n  runtime: \"node\",\n  repo: \"https://github.com/username/repo\",\n  schedule: \"0 0 * * *\",  # Daily at midnight (cron syntax)\n  buildCommand: \"npm ci\",\n  startCommand: \"node scripts/cleanup.js\",\n  plan: \"free\"\n)\n```\n\n**Create a PostgreSQL Database:**\n```\ncreate_postgres(\n  name: \"myapp-db\",\n  plan: \"free\",  # free, basic_256mb, basic_1gb, basic_4gb, pro_4gb, etc.\n  region: \"oregon\"\n)\n```\n\n**Create a Key-Value Store (Redis):**\n```\ncreate_key_value(\n  name: \"myapp-cache\",\n  plan: \"free\",  # free, starter, standard, pro, pro_plus\n  region: \"oregon\",\n  maxmemoryPolicy: \"allkeys_lru\"  # eviction policy\n)\n```\n\n### Step 3: Configure Environment Variables\n\nAfter creating services, add environment variables:\n\n```\nupdate_environment_variables(\n  serviceId: \"<service-id-from-creation>\",\n  envVars: [\n    {\"key\": \"DATABASE_URL\", \"value\": \"<connection-string>\"},\n    {\"key\": \"JWT_SECRET\", \"value\": \"<secret-value>\"},\n    {\"key\": \"API_KEY\", \"value\": \"<api-key>\"}\n  ]\n)\n```\n\n**Note:** For database connection strings, get the internal URL from the database details in Dashboard or via `get_postgres(postgresId: \"<id>\")`.\n\n### Step 4: Verify Deployment\n\nServices with `autoDeploy: \"yes\"` (default) will deploy automatically when created.\n\n**Check deployment status:**\n```\nlist_deploys(serviceId: \"<service-id>\", limit: 1)\n```\n\n**Monitor logs for errors:**\n```\nlist_logs(resource: [\"<service-id>\"], level: [\"error\"], limit: 50)\n```\n\n**Check health metrics:**\n```\nget_metrics(\n  resourceId: \"<service-id>\",\n  metricTypes: [\"http_request_count\", \"cpu_usage\", \"memory_usage\"]\n)\n```\n"
  },
  {
    "path": "skills/.curated/render-deploy/references/error-patterns.md",
    "content": "# Error patterns (compact)\n\nUse this to quickly map log signatures to likely causes and fixes.\n\n| Log pattern | Likely cause | Quick fix |\n| --- | --- | --- |\n| `KeyError`, `not defined`, `missing environment` | Missing env var | Add env var in render.yaml or via MCP, then redeploy |\n| `EADDRINUSE`, `listen EADDRINUSE` | Port binding conflict | Bind to `0.0.0.0:$PORT` |\n| `Cannot find module`, `ModuleNotFoundError` | Missing dependency | Add dependency to manifest and rebuild |\n| `ECONNREFUSED`, `connection refused` | DB not reachable | Verify DATABASE_URL and DB status |\n| `Health check timeout` | No healthy response | Add/verify health endpoint and port |\n| `exit 137`, `out of memory` | OOM | Reduce memory use or upgrade plan |\n| `Command failed`, `build failed` | Bad build command | Fix build command or dependencies |\n"
  },
  {
    "path": "skills/.curated/render-deploy/references/post-deploy-checks.md",
    "content": "# Post-deploy checks\n\nUse this after any deploy or service creation. Keep it short; stop when a check fails.\n\n## 1) Confirm deploy status\n\n```\nlist_deploys(serviceId: \"<service-id>\", limit: 1)\n```\n\n- Expect `status: \"live\"`.\n- If status is failed, inspect build/runtime logs immediately.\n\n## 2) Verify service health\n\n- Hit the health endpoint (preferred) or `/` and confirm a 200 response.\n- If there is no health endpoint, add one and redeploy.\n\n## 3) Scan recent error logs\n\n```\nlist_logs(resource: [\"<service-id>\"], level: [\"error\"], limit: 50)\n```\n\n- If you see a clear error signature, jump to the matching fix in\n  [troubleshooting-basics.md](troubleshooting-basics.md) or\n  [error-patterns.md](error-patterns.md).\n\n## 4) Verify env vars and port binding\n\n- Confirm all required env vars are set (especially secrets marked `sync: false`).\n- Ensure the app binds to `0.0.0.0:$PORT` (not localhost).\n\n## 5) Redeploy only after fixing the first failure\n\n- Avoid repeated deploys without changes; fix one issue at a time.\n"
  },
  {
    "path": "skills/.curated/render-deploy/references/runtimes.md",
    "content": "# Render Runtime Options\n\nComplete guide to available runtimes on Render, including versions, configuration, and best practices for each language.\n\n## Native Language Runtimes\n\n### Node.js (`runtime: node`)\n\n**Supported Versions:** 14, 16, 18, 20, 21\n**Default Version:** 20\n\n**Version Specification:**\n\nSpecify Node version in `package.json`:\n```json\n{\n  \"engines\": {\n    \"node\": \"20.x\"\n  }\n}\n```\n\n**Package Managers:**\n- **npm**: Default, uses `package-lock.json`\n- **Yarn**: Auto-detected if `yarn.lock` exists\n- **pnpm**: Auto-detected if `pnpm-lock.yaml` exists\n\n**Common Build Commands:**\n```bash\nnpm ci                          # Recommended (faster, reproducible)\nnpm ci && npm run build         # Build step included\nyarn install --frozen-lockfile  # Yarn equivalent\npnpm install --frozen-lockfile  # pnpm equivalent\n```\n\n**Common Start Commands:**\n```bash\nnpm start                       # Uses \"start\" script in package.json\nnode server.js                  # Direct file execution\nnode dist/main.js               # Built output\n```\n\n**Popular Frameworks:**\n- Express.js, Fastify, Koa (APIs)\n- Next.js (full-stack React)\n- Nest.js (enterprise TypeScript)\n- Remix (full-stack React)\n- Nuxt.js (full-stack Vue)\n\n**Example Configuration:**\n```yaml\ntype: web\nname: node-app\nruntime: node\nbuildCommand: npm ci && npm run build\nstartCommand: npm start\n```\n\n---\n\n### Python (`runtime: python`)\n\n**Supported Versions:** 3.8, 3.9, 3.10, 3.11, 3.12\n**Default Version:** 3.11\n\n**Version Specification:**\n\nOption 1 - `runtime.txt`:\n```\npython-3.11.5\n```\n\nOption 2 - `Pipfile`:\n```toml\n[requires]\npython_version = \"3.11\"\n```\n\n**Package Managers:**\n- **pip**: Default, uses `requirements.txt`\n- **Poetry**: Auto-detected if `pyproject.toml` exists\n- **Pipenv**: Auto-detected if `Pipfile` exists\n\n**Common Build Commands:**\n```bash\npip install -r requirements.txt\npip install -r requirements.txt && python manage.py collectstatic --no-input\npoetry install --no-dev\npipenv install --deploy\n```\n\n**Common Start Commands:**\n```bash\ngunicorn app:app                                    # Flask\ngunicorn config.wsgi:application                    # Django\nuvicorn main:app --host 0.0.0.0 --port $PORT       # FastAPI\ncelery -A tasks worker                              # Celery worker\n```\n\n**Popular Frameworks:**\n- Django (full-stack web framework)\n- Flask (microframework)\n- FastAPI (modern async API framework)\n- Celery (task queue)\n\n**Example Configuration:**\n```yaml\ntype: web\nname: python-app\nruntime: python\nbuildCommand: pip install -r requirements.txt\nstartCommand: gunicorn app:app --bind 0.0.0.0:$PORT\n```\n\n---\n\n### Go (`runtime: go`)\n\n**Supported Versions:** 1.20, 1.21, 1.22, 1.23\n**Default Version:** Latest stable\n\n**Version Specification:**\n\nSpecify in `go.mod`:\n```go\nmodule myapp\n\ngo 1.22\n```\n\n**Build System:** Uses Go modules\n\n**Common Build Commands:**\n```bash\ngo build -o bin/app .\ngo build -o bin/app cmd/server/main.go\ngo build -tags netgo -ldflags '-s -w' -o bin/app\n```\n\n**Common Start Commands:**\n```bash\n./bin/app\n./bin/server\n```\n\n**Popular Frameworks:**\n- net/http (standard library)\n- Gin (fast web framework)\n- Echo (high performance framework)\n- Chi (lightweight router)\n- Fiber (Express-inspired framework)\n- Gorilla Mux (powerful router)\n\n**Example Configuration:**\n```yaml\ntype: web\nname: go-app\nruntime: go\nbuildCommand: go build -o bin/app .\nstartCommand: ./bin/app\n```\n\n---\n\n### Ruby (`runtime: ruby`)\n\n**Supported Versions:** 3.0, 3.1, 3.2, 3.3\n**Default Version:** 3.3\n\n**Version Specification:**\n\nOption 1 - `.ruby-version`:\n```\n3.3.0\n```\n\nOption 2 - `Gemfile`:\n```ruby\nruby '3.3.0'\n```\n\n**Package Manager:** Bundler (uses `Gemfile` and `Gemfile.lock`)\n\n**Common Build Commands:**\n```bash\nbundle install --jobs=4 --retry=3\nbundle install && bundle exec rails assets:precompile\n```\n\n**Common Start Commands:**\n```bash\nbundle exec rails server -b 0.0.0.0 -p $PORT\nbundle exec puma -C config/puma.rb\nbundle exec rackup -o 0.0.0.0 -p $PORT\nbundle exec sidekiq                                  # Worker\n```\n\n**Popular Frameworks:**\n- Ruby on Rails (full-stack framework)\n- Sinatra (microframework)\n- Sidekiq (background jobs)\n\n**Example Configuration:**\n```yaml\ntype: web\nname: rails-app\nruntime: ruby\nbuildCommand: bundle install && bundle exec rails assets:precompile\nstartCommand: bundle exec puma -C config/puma.rb\n```\n\n---\n\n### Rust (`runtime: rust`)\n\n**Supported Versions:** Latest stable\n**Default Version:** Latest stable\n\n**Build System:** Cargo\n\n**Common Build Commands:**\n```bash\ncargo build --release\ncargo build --release --locked\n```\n\n**Common Start Commands:**\n```bash\n./target/release/myapp\n```\n\n**Popular Frameworks:**\n- Actix Web (powerful, performant)\n- Rocket (web framework with focus on usability)\n- Axum (modern, ergonomic framework)\n- Warp (composable web framework)\n\n**Example Configuration:**\n```yaml\ntype: web\nname: rust-app\nruntime: rust\nbuildCommand: cargo build --release\nstartCommand: ./target/release/myapp\n```\n\n---\n\n### Elixir (`runtime: elixir`)\n\n**Supported Versions:** Latest stable\n**Default Version:** Latest stable\n\n**Build System:** Mix\n\n**Common Build Commands:**\n```bash\nmix deps.get --only prod\nmix deps.get && mix compile\nmix do deps.get, compile, assets.deploy\n```\n\n**Common Start Commands:**\n```bash\nmix phx.server\nelixir --name myapp -S mix phx.server\n```\n\n**Popular Frameworks:**\n- Phoenix (full-stack web framework)\n- Phoenix LiveView (real-time applications)\n\n**Example Configuration:**\n```yaml\ntype: web\nname: elixir-app\nruntime: elixir\nbuildCommand: mix deps.get --only prod && mix compile\nstartCommand: mix phx.server\n```\n\n---\n\n## Container Runtimes\n\n### Docker (`runtime: docker`)\n\nBuild your application from a Dockerfile in your repository.\n\n**Additional Configuration:**\n- `dockerfilePath`: Path to Dockerfile (default: `./Dockerfile`)\n- `dockerContext`: Build context directory (default: `.`)\n\n**Example Configuration:**\n```yaml\ntype: web\nname: docker-app\nruntime: docker\ndockerfilePath: ./Dockerfile\ndockerContext: .\n```\n\n**Multi-stage Dockerfile Example:**\n```dockerfile\n# Build stage\nFROM node:20-alpine AS builder\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\n\n# Production stage\nFROM node:20-alpine\nWORKDIR /app\nCOPY --from=builder /app/dist ./dist\nCOPY package*.json ./\nRUN npm ci --only=production\nEXPOSE 10000\nCMD [\"node\", \"dist/main.js\"]\n```\n\n**Best Practices:**\n- Use multi-stage builds to reduce image size\n- Copy `package.json` before source code (better caching)\n- Use `.dockerignore` to exclude unnecessary files\n- Expose port dynamically via `$PORT` environment variable\n- Run as non-root user for security\n\n---\n\n### Pre-built Image (`runtime: image`)\n\nDeploy pre-built Docker images from a container registry.\n\n**Additional Configuration:**\n- `image`: Full image URL with tag or digest\n- `registryCredential`: Credentials for private registries\n\n**Example with Public Image:**\n```yaml\ntype: web\nname: prebuilt-app\nruntime: image\nimage: ghcr.io/myorg/myapp:v1.2.3\n```\n\n**Example with Private Registry:**\n```yaml\ntype: web\nname: private-app\nruntime: image\nimage: myregistry.com/myapp:latest\nregistryCredential:\n  username: my-username\n  password:\n    sync: false  # User provides in Dashboard\n```\n\n**Use Cases:**\n- Deploy images built in CI/CD pipeline\n- Use images from container registries\n- Deploy Docker Hub images\n- Use private registry images\n\n---\n\n## Static Runtime (`runtime: static`)\n\nServe pre-built static files without a backend runtime. Files are served via CDN.\n\n**Additional Configuration:**\n- `staticPublishPath`: Directory containing built files (e.g., `./dist`, `./build`)\n\n**Common Build Commands by Framework:**\n\n**React (Create React App):**\n```bash\nnpm ci && npm run build\n# Outputs to: ./build\n```\n\n**Vue:**\n```bash\nnpm ci && npm run build\n# Outputs to: ./dist\n```\n\n**Next.js (Static Export):**\n```bash\nnpm ci && npm run build && npm run export\n# Outputs to: ./out\n```\n\n**Gatsby:**\n```bash\nnpm ci && npm run build\n# Outputs to: ./public\n```\n\n**Vite:**\n```bash\nnpm ci && npm run build\n# Outputs to: ./dist\n```\n\n**Example Configuration:**\n```yaml\ntype: web\nname: react-app\nruntime: static\nbuildCommand: npm ci && npm run build\nstaticPublishPath: ./build\n```\n\n---\n\n## Runtime Comparison\n\n| Runtime | Build Speed | Cold Start | Best For |\n|---------|-------------|------------|----------|\n| Node.js | Fast | Fast | APIs, full-stack apps |\n| Python | Medium | Medium | Data apps, APIs, web |\n| Go | Fast | Very Fast | High performance APIs |\n| Ruby | Slow | Medium | Rails apps, traditional web |\n| Rust | Very Slow | Very Fast | Performance-critical services |\n| Elixir | Medium | Fast | Real-time, concurrent apps |\n| Docker | Varies | Medium | Any language, custom setup |\n| Static | Very Fast | N/A | SPAs, documentation, marketing |\n\n---\n\n## Choosing the Right Runtime\n\n**Choose Node.js when:**\n- Building JavaScript-based applications\n- Need rich npm ecosystem\n- Want fast iteration and deployment\n- Building full-stack applications (Next.js, Remix)\n\n**Choose Python when:**\n- Building data-heavy applications\n- Need machine learning libraries\n- Django or Flask expertise\n- Data processing pipelines\n\n**Choose Go when:**\n- Need high performance and low resource usage\n- Building microservices\n- Want simple deployment (single binary)\n- Handling high concurrency\n\n**Choose Ruby when:**\n- Building traditional web applications\n- Ruby on Rails expertise\n- Rapid development priority\n\n**Choose Rust when:**\n- Maximum performance required\n- Systems programming\n- Resource-constrained environments\n\n**Choose Docker when:**\n- Need custom system dependencies\n- Multi-language application\n- Existing Dockerfile\n- Need full control over environment\n\n**Choose Static when:**\n- Building SPAs or static sites\n- No backend processing needed\n- Want CDN caching and fast delivery\n- Documentation or marketing sites\n"
  },
  {
    "path": "skills/.curated/render-deploy/references/service-types.md",
    "content": "# Render Service Types\n\nDetailed explanation of each service type available on Render. Choose the right service type based on your application's needs.\n\n## Web Services (`type: web`)\n\n### Purpose\n\nWeb services are HTTP servers that handle incoming requests from the internet. They're publicly accessible via HTTPS URLs.\n\n### Use Cases\n\n- **REST APIs**: JSON APIs for mobile apps or frontend applications\n- **GraphQL servers**: GraphQL endpoints for client queries\n- **Web applications**: Server-rendered websites (Django, Rails, Express)\n- **Full-stack frameworks**: Next.js, Nuxt.js, Remix, SvelteKit\n- **WebSocket servers**: Real-time communication servers\n- **SSR applications**: Server-side rendered React, Vue, or Angular apps\n\n### Key Characteristics\n\n- **Public URL**: Automatically assigned `https://[service-name].onrender.com`\n- **Port binding required**: Must bind to `0.0.0.0:$PORT`\n- **Health checks**: Render pings your service to verify it's running\n- **HTTPS**: Automatic SSL/TLS certificates\n- **Load balancing**: Traffic distributed across multiple instances\n- **Custom domains**: Support for your own domain names\n\n### Required Configuration\n\n```yaml\ntype: web\nname: my-api\nruntime: node\nbuildCommand: npm ci\nstartCommand: npm start\n```\n\n### Best Practices\n\n1. **Bind to environment PORT**:\n```javascript\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, '0.0.0.0');\n```\n\n2. **Add health check endpoint**:\n```javascript\napp.get('/health', (req, res) => {\n  res.status(200).json({ status: 'ok' });\n});\n```\n\n3. **Use appropriate timeouts**: Web requests should complete within 30 seconds\n\n4. **Implement graceful shutdown**: Handle SIGTERM signals properly\n\n---\n\n## Worker Services (`type: worker`)\n\n### Purpose\n\nWorker services run background tasks without handling HTTP requests. They're not publicly accessible.\n\n### Use Cases\n\n- **Queue processors**: Redis queue, BullMQ, Celery, Sidekiq\n- **Background jobs**: Email sending, image processing, data exports\n- **Event consumers**: Message queue consumers (Kafka, RabbitMQ, etc.)\n- **Data pipeline workers**: ETL processes, data transformation\n- **Scheduled background tasks**: Continuous processes (not cron)\n- **WebSocket backend**: Dedicated WebSocket handler services\n\n### Key Characteristics\n\n- **No public URL**: Not accessible from internet\n- **No port binding**: Doesn't need to listen on a port\n- **No health checks**: Render monitors process health differently\n- **Long-running**: Can run indefinitely\n- **Private communication**: Access via internal networking\n- **Restart on crash**: Automatically restarted if process dies\n\n### Required Configuration\n\n```yaml\ntype: worker\nname: queue-processor\nruntime: python\nbuildCommand: pip install -r requirements.txt\nstartCommand: celery -A tasks worker --loglevel=info\n```\n\n### Best Practices\n\n1. **Connect to message queue**:\n```python\nimport redis\nr = redis.from_url(os.environ['REDIS_URL'])\n```\n\n2. **Implement retry logic**: Handle failures gracefully\n\n3. **Monitor queue depth**: Track pending jobs\n\n4. **Log processing status**: Make debugging easier\n\n5. **Graceful shutdown**: Finish current jobs before exiting\n\n### Common Patterns\n\n**Node.js with BullMQ:**\n```yaml\ntype: worker\nname: job-processor\nruntime: node\nbuildCommand: npm ci\nstartCommand: node worker.js\nenvVars:\n  - key: REDIS_URL\n    fromDatabase:\n      name: redis\n      property: connectionString\n```\n\n**Python with Celery:**\n```yaml\ntype: worker\nname: celery-worker\nruntime: python\nbuildCommand: pip install -r requirements.txt\nstartCommand: celery -A app.celery worker\nenvVars:\n  - key: REDIS_URL\n    fromDatabase:\n      name: redis\n      property: connectionString\n```\n\n---\n\n## Cron Jobs (`type: cron`)\n\n### Purpose\n\nCron jobs run scheduled tasks on a repeating schedule. They execute, complete, and shut down.\n\n### Use Cases\n\n- **Database backups**: Regular automated backups\n- **Report generation**: Daily/weekly reports\n- **Data cleanup**: Delete old records periodically\n- **Cache warming**: Pre-populate caches\n- **Email digests**: Send scheduled email summaries\n- **Data synchronization**: Sync between systems\n- **Batch processing**: Process accumulated data\n\n### Key Characteristics\n\n- **Scheduled execution**: Runs on cron schedule\n- **Automatic shutdown**: Shuts down after completing\n- **No persistent port**: Doesn't maintain listening port\n- **No health checks**: Task either completes or fails\n- **UTC timezone**: All schedules in UTC\n- **Maximum runtime**: Jobs timeout after configured limit\n\n### Required Configuration\n\n```yaml\ntype: cron\nname: daily-backup\nruntime: node\nschedule: \"0 2 * * *\"  # Daily at 2 AM UTC\nbuildCommand: npm ci\nstartCommand: node scripts/backup.js\n```\n\n### Schedule Format\n\nStandard cron syntax: `minute hour day month weekday`\n\n**Common schedules:**\n\n| Schedule | Description |\n|----------|-------------|\n| `*/5 * * * *` | Every 5 minutes |\n| `0 * * * *` | Every hour |\n| `0 0 * * *` | Daily at midnight UTC |\n| `0 9 * * 1-5` | Weekdays at 9 AM UTC |\n| `0 0 1 * *` | First day of each month |\n| `0 9 * * 1` | Every Monday at 9 AM UTC |\n\n### Best Practices\n\n1. **Handle failures gracefully**: Jobs should be idempotent\n\n2. **Log completion status**: Track success/failure\n\n3. **Set appropriate timeouts**: Match expected job duration\n\n4. **Use UTC times**: All schedules are UTC-based\n\n5. **Test thoroughly**: Test with different data scenarios\n\n### Example Use Cases\n\n**Daily Database Backup:**\n```yaml\ntype: cron\nname: db-backup\nruntime: python\nschedule: \"0 1 * * *\"  # 1 AM UTC daily\nbuildCommand: pip install -r requirements.txt\nstartCommand: python scripts/backup.py\nenvVars:\n  - key: DATABASE_URL\n    fromDatabase:\n      name: postgres\n      property: connectionString\n  - key: S3_BUCKET\n    value: my-backups\n```\n\n**Hourly Cache Refresh:**\n```yaml\ntype: cron\nname: cache-refresh\nruntime: node\nschedule: \"0 * * * *\"  # Top of every hour\nbuildCommand: npm ci\nstartCommand: node scripts/refresh-cache.js\n```\n\n---\n\n## Static Sites (`type: web` + `runtime: static`)\n\n### Purpose\n\nServe static HTML, CSS, and JavaScript files via CDN. No backend runtime.\n\n### Use Cases\n\n- **Single Page Applications (SPAs)**: React, Vue, Angular apps\n- **Static site generators**: Gatsby, Next.js (static export), Hugo\n- **Documentation sites**: MkDocs, Docusaurus, VitePress\n- **Landing pages**: Marketing sites\n- **Portfolio sites**: Personal websites\n- **JAMstack sites**: Static sites with API integration\n\n### Key Characteristics\n\n- **CDN delivery**: Global edge caching\n- **No backend runtime**: Only serves built files\n- **Build output only**: Serves contents of build directory\n- **Routing support**: Rewrite rules for SPA routing\n- **Custom headers**: Cache control, security headers\n- **Fast deployment**: Quick to build and deploy\n\n### Required Configuration\n\n```yaml\ntype: web\nname: frontend\nruntime: static\nbuildCommand: npm ci && npm run build\nstaticPublishPath: ./dist  # or ./build, ./out, ./public\n```\n\n### Routing for SPAs\n\nSingle Page Applications need rewrite rules to handle client-side routing:\n\n```yaml\ntype: web\nname: react-app\nruntime: static\nbuildCommand: npm ci && npm run build\nstaticPublishPath: ./build\nroutes:\n  - type: rewrite\n    source: /*\n    destination: /index.html\n```\n\n### Custom Headers\n\nAdd cache control and security headers:\n\n```yaml\ntype: web\nname: static-site\nruntime: static\nbuildCommand: npm ci && npm run build\nstaticPublishPath: ./dist\nheaders:\n  # Cache static assets\n  - path: /static/*\n    name: Cache-Control\n    value: public, max-age=31536000, immutable\n\n  # Security headers\n  - path: /*\n    name: X-Frame-Options\n    value: DENY\n  - path: /*\n    name: X-Content-Type-Options\n    value: nosniff\n```\n\n### Build Filters\n\nFor monorepos, only build when frontend files change:\n\n```yaml\ntype: web\nname: frontend\nruntime: static\nbuildCommand: npm ci && npm run build\nstaticPublishPath: ./dist\nbuildFilter:\n  paths:\n    - frontend/**\n  ignoredPaths:\n    - frontend/**/*.test.js\n    - frontend/README.md\n```\n\n### Best Practices\n\n1. **Optimize build output**: Minify, compress, tree-shake\n\n2. **Use proper cache headers**: Long cache for hashed assets\n\n3. **Add security headers**: Protect against common attacks\n\n4. **Configure SPA routing**: Add rewrite rules for client routing\n\n5. **Handle 404s**: Create custom 404.html page\n\n---\n\n## Private Services (`type: pserv`)\n\n### Purpose\n\nInternal services accessible only within your Render account. Not exposed to the internet.\n\n### Use Cases\n\n- **Internal APIs**: Services accessed only by other services\n- **Database proxies**: Connection pools, read replicas\n- **Microservices**: Service mesh architectures\n- **Admin tools**: Internal dashboards\n- **Cache layers**: Internal caching services\n- **Message brokers**: Internal message queues\n\n### Key Characteristics\n\n- **No public URL**: Only accessible via internal DNS\n- **Internal networking**: Fast, low-latency connections\n- **Port binding required**: Must bind to `0.0.0.0:$PORT`\n- **Private DNS**: `[service-name].render-internal.com`\n- **Same-account only**: Only accessible from same account\n- **No internet access**: Traffic stays within Render network\n\n### Required Configuration\n\n```yaml\ntype: pserv\nname: internal-api\nruntime: node\nbuildCommand: npm ci\nstartCommand: npm start\n```\n\n### Accessing Private Services\n\nFrom other services in the same account:\n\n```javascript\n// Use .render-internal.com domain\nconst API_URL = 'http://internal-api.render-internal.com:10000';\n```\n\nOr use service references:\n\n```yaml\nservices:\n  - type: web\n    name: frontend\n    runtime: node\n    envVars:\n      - key: INTERNAL_API_URL\n        fromService:\n          name: internal-api\n          type: pserv\n          property: hostport\n```\n\n### Best Practices\n\n1. **Use internal DNS**: Always use `.render-internal.com` domains\n\n2. **No authentication needed**: Already isolated to account\n\n3. **Fast communication**: Low latency between services\n\n4. **Simplify architecture**: No need for external load balancers\n\n---\n\n## Comparison Table\n\n| Feature | Web | Worker | Cron | Static | Private |\n|---------|-----|--------|------|--------|---------|\n| Public URL | ✅ Yes | ❌ No | ❌ No | ✅ Yes | ❌ No |\n| Port Binding | ✅ Required | ❌ Not needed | ❌ Not needed | ❌ N/A | ✅ Required |\n| Health Checks | ✅ Yes | ❌ No | ❌ No | ❌ N/A | ✅ Yes |\n| Runtime | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes |\n| Persistent | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes |\n| Scaling | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes |\n| Use Case | HTTP servers | Background jobs | Scheduled tasks | Static files | Internal services |\n\n## Choosing the Right Service Type\n\n**Use Web Service when:**\n- Your app handles HTTP requests\n- Users need to access it via URL\n- You need load balancing and scaling\n\n**Use Worker Service when:**\n- Processing background jobs\n- Consuming from message queues\n- Running long-lived processes without HTTP\n\n**Use Cron Job when:**\n- Running scheduled tasks\n- Processing doesn't need to be always-on\n- Tasks run periodically (hourly, daily, weekly)\n\n**Use Static Site when:**\n- Serving pre-built HTML/CSS/JS\n- No backend processing needed\n- Want CDN caching and fast delivery\n\n**Use Private Service when:**\n- Service only accessed by other services\n- Want internal-only communication\n- Building microservice architectures\n"
  },
  {
    "path": "skills/.curated/render-deploy/references/troubleshooting-basics.md",
    "content": "# Basic troubleshooting (deploy-time and startup)\n\nUse this when a deploy fails, the service crashes on start, or health checks time out.\nKeep fixes minimal and redeploy after each change.\n\n## 1) Classify the failure\n\n- **Build failure**: errors in build logs, missing dependencies, build command issues.\n- **Startup failure**: app exits quickly, crashes, or cannot bind to `$PORT`.\n- **Runtime/health failure**: service is live but health checks fail or 5xx errors.\n\n## 2) Quick checks by class\n\n**Build failure**\n- Confirm the build command is correct for the runtime.\n- Ensure required dependencies are present in `package.json`, `requirements.txt`, etc.\n- Check for missing build-time env vars.\n\n**Startup failure**\n- Confirm the start command and working directory.\n- Ensure port binding is `0.0.0.0:$PORT`.\n- Check for missing runtime env vars (secrets, DB URLs).\n\n**Runtime/health failure**\n- Verify the health endpoint path and response.\n- Confirm the app is actually listening on `$PORT`.\n- Check database connectivity and migrations.\n\n## 3) Map error signatures to fixes\n\nUse [error-patterns.md](error-patterns.md) for a compact catalog of common log messages.\n\n## 4) If still blocked\n\nGather the latest build logs and runtime error logs, then consider the optional\n`render-debug` skill for deeper diagnostics (metrics, DB checks, expanded patterns).\n"
  },
  {
    "path": "skills/.curated/screenshot/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/screenshot/SKILL.md",
    "content": "---\nname: \"screenshot\"\ndescription: \"Use when the user explicitly asks for a desktop or system screenshot (full screen, specific app or window, or a pixel region), or when tool-specific capture capabilities are unavailable and an OS-level capture is needed.\"\n---\n\n\n# Screenshot Capture\n\nFollow these save-location rules every time:\n\n1) If the user specifies a path, save there.\n2) If the user asks for a screenshot without a path, save to the OS default screenshot location.\n3) If Codex needs a screenshot for its own inspection, save to the temp directory.\n\n## Tool priority\n\n- Prefer tool-specific screenshot capabilities when available (for example: a Figma MCP/skill for Figma files, or Playwright/agent-browser tools for browsers and Electron apps).\n- Use this skill when explicitly asked, for whole-system desktop captures, or when a tool-specific capture cannot get what you need.\n- Otherwise, treat this skill as the default for desktop apps without a better-integrated capture tool.\n\n## macOS permission preflight (reduce repeated prompts)\n\nOn macOS, run the preflight helper once before window/app capture. It checks\nScreen Recording permission, explains why it is needed, and requests it in one\nplace.\n\nThe helpers route Swift's module cache to `$TMPDIR/codex-swift-module-cache`\nto avoid extra sandbox module-cache prompts.\n\n```bash\nbash <path-to-skill>/scripts/ensure_macos_permissions.sh\n```\n\nTo avoid multiple sandbox approval prompts, combine preflight + capture in one\ncommand when possible:\n\n```bash\nbash <path-to-skill>/scripts/ensure_macos_permissions.sh && \\\npython3 <path-to-skill>/scripts/take_screenshot.py --app \"Codex\"\n```\n\nFor Codex inspection runs, keep the output in temp:\n\n```bash\nbash <path-to-skill>/scripts/ensure_macos_permissions.sh && \\\npython3 <path-to-skill>/scripts/take_screenshot.py --app \"<App>\" --mode temp\n```\n\nUse the bundled scripts to avoid re-deriving OS-specific commands.\n\n## macOS and Linux (Python helper)\n\nRun the helper from the repo root:\n\n```bash\npython3 <path-to-skill>/scripts/take_screenshot.py\n```\n\nCommon patterns:\n\n- Default location (user asked for \"a screenshot\"):\n\n```bash\npython3 <path-to-skill>/scripts/take_screenshot.py\n```\n\n- Temp location (Codex visual check):\n\n```bash\npython3 <path-to-skill>/scripts/take_screenshot.py --mode temp\n```\n\n- Explicit location (user provided a path or filename):\n\n```bash\npython3 <path-to-skill>/scripts/take_screenshot.py --path output/screen.png\n```\n\n- App/window capture by app name (macOS only; substring match is OK; captures all matching windows):\n\n```bash\npython3 <path-to-skill>/scripts/take_screenshot.py --app \"Codex\"\n```\n\n- Specific window title within an app (macOS only):\n\n```bash\npython3 <path-to-skill>/scripts/take_screenshot.py --app \"Codex\" --window-name \"Settings\"\n```\n\n- List matching window ids before capturing (macOS only):\n\n```bash\npython3 <path-to-skill>/scripts/take_screenshot.py --list-windows --app \"Codex\"\n```\n\n- Pixel region (x,y,w,h):\n\n```bash\npython3 <path-to-skill>/scripts/take_screenshot.py --mode temp --region 100,200,800,600\n```\n\n- Focused/active window (captures only the frontmost window; use `--app` to capture all windows):\n\n```bash\npython3 <path-to-skill>/scripts/take_screenshot.py --mode temp --active-window\n```\n\n- Specific window id (use --list-windows on macOS to discover ids):\n\n```bash\npython3 <path-to-skill>/scripts/take_screenshot.py --window-id 12345\n```\n\nThe script prints one path per capture. When multiple windows or displays match, it prints multiple paths (one per line) and adds suffixes like `-w<windowId>` or `-d<display>`. View each path sequentially with the image viewer tool, and only manipulate images if needed or requested.\n\n### Workflow examples\n\n- \"Take a look at <App> and tell me what you see\": capture to temp, then view each printed path in order.\n\n```bash\nbash <path-to-skill>/scripts/ensure_macos_permissions.sh && \\\npython3 <path-to-skill>/scripts/take_screenshot.py --app \"<App>\" --mode temp\n```\n\n- \"The design from Figma is not matching what is implemented\": use a Figma MCP/skill to capture the design first, then capture the running app with this skill (typically to temp) and compare the raw screenshots before any manipulation.\n\n### Multi-display behavior\n\n- On macOS, full-screen captures save one file per display when multiple monitors are connected.\n- On Linux and Windows, full-screen captures use the virtual desktop (all monitors in one image); use `--region` to isolate a single display when needed.\n\n### Linux prerequisites and selection logic\n\nThe helper automatically selects the first available tool:\n\n1) `scrot`\n2) `gnome-screenshot`\n3) ImageMagick `import`\n\nIf none are available, ask the user to install one of them and retry.\n\nCoordinate regions require `scrot` or ImageMagick `import`.\n\n`--app`, `--window-name`, and `--list-windows` are macOS-only. On Linux, use\n`--active-window` or provide `--window-id` when available.\n\n## Windows (PowerShell helper)\n\nRun the PowerShell helper:\n\n```powershell\npowershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1\n```\n\nCommon patterns:\n\n- Default location:\n\n```powershell\npowershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1\n```\n\n- Temp location (Codex visual check):\n\n```powershell\npowershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1 -Mode temp\n```\n\n- Explicit path:\n\n```powershell\npowershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1 -Path \"C:\\Temp\\screen.png\"\n```\n\n- Pixel region (x,y,w,h):\n\n```powershell\npowershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1 -Mode temp -Region 100,200,800,600\n```\n\n- Active window (ask the user to focus it first):\n\n```powershell\npowershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1 -Mode temp -ActiveWindow\n```\n\n- Specific window handle (only when provided):\n\n```powershell\npowershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1 -WindowHandle 123456\n```\n\n## Direct OS commands (fallbacks)\n\nUse these when you cannot run the helpers.\n\n### macOS\n\n- Full screen to a specific path:\n\n```bash\nscreencapture -x output/screen.png\n```\n\n- Pixel region:\n\n```bash\nscreencapture -x -R100,200,800,600 output/region.png\n```\n\n- Specific window id:\n\n```bash\nscreencapture -x -l12345 output/window.png\n```\n\n- Interactive selection or window pick:\n\n```bash\nscreencapture -x -i output/interactive.png\n```\n\n### Linux\n\n- Full screen:\n\n```bash\nscrot output/screen.png\n```\n\n```bash\ngnome-screenshot -f output/screen.png\n```\n\n```bash\nimport -window root output/screen.png\n```\n\n- Pixel region:\n\n```bash\nscrot -a 100,200,800,600 output/region.png\n```\n\n```bash\nimport -window root -crop 800x600+100+200 output/region.png\n```\n\n- Active window:\n\n```bash\nscrot -u output/window.png\n```\n\n```bash\ngnome-screenshot -w -f output/window.png\n```\n\n## Error handling\n\n- On macOS, run `bash <path-to-skill>/scripts/ensure_macos_permissions.sh` first to request Screen Recording in one place.\n- If you see \"screen capture checks are blocked in the sandbox\", \"could not create image from display\", or Swift `ModuleCache` permission errors in a sandboxed run, rerun the command with escalated permissions.\n- If macOS app/window capture returns no matches, run `--list-windows --app \"AppName\"` and retry with `--window-id`, and make sure the app is visible on screen.\n- If Linux region/window capture fails, check tool availability with `command -v scrot`, `command -v gnome-screenshot`, and `command -v import`.\n- If saving to the OS default location fails with permission errors in a sandbox, rerun the command with escalated permissions.\n- Always report the saved file path in the response.\n"
  },
  {
    "path": "skills/.curated/screenshot/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Screenshot Capture\"\n  short_description: \"Capture screenshots\"\n  icon_small: \"./assets/screenshot-small.svg\"\n  icon_large: \"./assets/screenshot.png\"\n  default_prompt: \"Capture the right screenshot for this task (target, area, and output path).\"\n"
  },
  {
    "path": "skills/.curated/screenshot/scripts/ensure_macos_permissions.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nif [[ \"$(uname)\" != \"Darwin\" ]]; then\n  echo \"ensure_macos_permissions.sh only supports macOS\" >&2\n  exit 1\nfi\n\nif ! command -v swift >/dev/null 2>&1; then\n  echo \"swift is required to check macOS screen capture permissions\" >&2\n  exit 1\nfi\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nPERM_SWIFT=\"$SCRIPT_DIR/macos_permissions.swift\"\nMODULE_CACHE=\"${TMPDIR:-/tmp}/codex-swift-module-cache\"\nmkdir -p \"$MODULE_CACHE\"\n\nscreen_capture_status() {\n  local json\n  json=\"$(swift -module-cache-path \"$MODULE_CACHE\" \"$PERM_SWIFT\" \"$@\")\"\n  python3 -c 'import json, sys; data=json.loads(sys.argv[1]); print(\"1\" if data.get(\"screenCapture\") else \"0\")' \"$json\"\n}\n\nif [[ -n \"${CODEX_SANDBOX:-}\" ]]; then\n  echo \"Screen capture checks are blocked in the sandbox; rerun with escalated permissions.\" >&2\n  exit 3\nfi\n\nif [[ \"$(screen_capture_status)\" == \"1\" ]]; then\n  echo \"Screen Recording permission already granted.\"\n  exit 0\nfi\n\ncat <<'MSG'\nThis workflow needs macOS Screen Recording permission to capture screenshots.\nmacOS will show a single system prompt for Screen Recording. Approve it, then\nreturn here. If macOS opens System Settings instead of prompting, enable Screen\nRecording for your terminal and rerun the command.\nMSG\n\n# Request permission once after explaining why it is needed.\nscreen_capture_status --request >/dev/null || true\n\nif [[ \"$(screen_capture_status)\" != \"1\" ]]; then\n  cat <<'MSG'\nScreen Recording is still not granted.\nOpen System Settings > Privacy & Security > Screen Recording and enable it for\nyour terminal (and Codex if needed), then rerun your screenshot command.\nMSG\n  exit 2\nfi\n\necho \"Screen Recording permission granted.\"\n"
  },
  {
    "path": "skills/.curated/screenshot/scripts/macos_display_info.swift",
    "content": "import AppKit\nimport Foundation\n\nstruct Response: Encodable {\n  let count: Int\n  let displays: [Int]\n}\n\nlet count = max(NSScreen.screens.count, 1)\nlet displays = Array(1...count)\n\nlet response = Response(count: count, displays: displays)\nlet encoder = JSONEncoder()\nencoder.outputFormatting = [.sortedKeys]\n\nif let data = try? encoder.encode(response),\n   let json = String(data: data, encoding: .utf8) {\n  print(json)\n} else {\n  fputs(\"{\\\"count\\\":\\(count)}\\n\", stderr)\n  exit(1)\n}\n"
  },
  {
    "path": "skills/.curated/screenshot/scripts/macos_permissions.swift",
    "content": "import CoreGraphics\nimport Foundation\n\nstruct Status: Encodable {\n  let screenCapture: Bool\n  let requested: Bool\n}\n\nlet shouldRequest = CommandLine.arguments.contains(\"--request\")\n\n@available(macOS 10.15, *)\nfunc screenCaptureGranted(request: Bool) -> Bool {\n  if CGPreflightScreenCaptureAccess() {\n    return true\n  }\n  if request {\n    _ = CGRequestScreenCaptureAccess()\n    return CGPreflightScreenCaptureAccess()\n  }\n  return false\n}\n\nlet granted: Bool\nif #available(macOS 10.15, *) {\n  granted = screenCaptureGranted(request: shouldRequest)\n} else {\n  granted = true\n}\n\nlet status = Status(screenCapture: granted, requested: shouldRequest)\nlet encoder = JSONEncoder()\nencoder.outputFormatting = [.sortedKeys]\n\nif let data = try? encoder.encode(status),\n   let json = String(data: data, encoding: .utf8) {\n  print(json)\n} else {\n  fputs(\"{\\\"requested\\\":\\(shouldRequest),\\\"screenCapture\\\":\\(granted)}\\n\", stderr)\n  exit(1)\n}\n"
  },
  {
    "path": "skills/.curated/screenshot/scripts/macos_window_info.swift",
    "content": "import AppKit\nimport CoreGraphics\nimport Foundation\n\nstruct Bounds: Encodable {\n  let x: Int\n  let y: Int\n  let width: Int\n  let height: Int\n}\n\nstruct WindowInfo: Encodable {\n  let id: Int\n  let owner: String\n  let name: String\n  let layer: Int\n  let bounds: Bounds\n  let area: Int\n}\n\nstruct Response: Encodable {\n  let count: Int\n  let selected: WindowInfo?\n  let windows: [WindowInfo]?\n}\n\nfunc value(for flag: String) -> String? {\n  guard let idx = CommandLine.arguments.firstIndex(of: flag) else {\n    return nil\n  }\n  let next = CommandLine.arguments.index(after: idx)\n  guard next < CommandLine.arguments.endIndex else {\n    return nil\n  }\n  return CommandLine.arguments[next]\n}\n\nlet frontmostFlag = CommandLine.arguments.contains(\"--frontmost\")\nlet explicitApp = value(for: \"--app\")\nlet frontmostName = frontmostFlag ? NSWorkspace.shared.frontmostApplication?.localizedName : nil\nif frontmostFlag && frontmostName == nil {\n  fputs(\"{\\\"count\\\":0}\\n\", stderr)\n  exit(1)\n}\nlet appFilter = (explicitApp ?? frontmostName)?.lowercased()\nlet nameFilter = value(for: \"--window-name\")?.lowercased()\nlet includeList = CommandLine.arguments.contains(\"--list\")\n\nlet options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]\nguard let raw = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else {\n  fputs(\"{\\\"count\\\":0}\\n\", stderr)\n  exit(1)\n}\n\nvar exactMatches: [WindowInfo] = []\nvar partialMatches: [WindowInfo] = []\nexactMatches.reserveCapacity(raw.count)\npartialMatches.reserveCapacity(raw.count)\n\nfor entry in raw {\n  guard let owner = entry[kCGWindowOwnerName as String] as? String else { continue }\n  let ownerLower = owner.lowercased()\n  if let appFilter, !ownerLower.contains(appFilter) { continue }\n\n  let name = (entry[kCGWindowName as String] as? String) ?? \"\"\n  if let nameFilter, !name.lowercased().contains(nameFilter) { continue }\n\n  guard let number = entry[kCGWindowNumber as String] as? Int else { continue }\n  let layer = (entry[kCGWindowLayer as String] as? Int) ?? 0\n\n  guard let boundsDict = entry[kCGWindowBounds as String] as? [String: Any] else { continue }\n  let x = Int((boundsDict[\"X\"] as? Double) ?? 0)\n  let y = Int((boundsDict[\"Y\"] as? Double) ?? 0)\n  let width = Int((boundsDict[\"Width\"] as? Double) ?? 0)\n  let height = Int((boundsDict[\"Height\"] as? Double) ?? 0)\n  if width <= 0 || height <= 0 { continue }\n\n  let bounds = Bounds(x: x, y: y, width: width, height: height)\n  let area = width * height\n  let info = WindowInfo(id: number, owner: owner, name: name, layer: layer, bounds: bounds, area: area)\n  if let appFilter, ownerLower == appFilter {\n    exactMatches.append(info)\n  } else {\n    partialMatches.append(info)\n  }\n}\n\nlet windows: [WindowInfo]\nif appFilter != nil && !exactMatches.isEmpty {\n  windows = exactMatches\n} else {\n  windows = partialMatches\n}\n\nfunc rank(_ window: WindowInfo) -> (Int, Int) {\n  // Prefer normal-layer windows, then larger area.\n  let layerScore = window.layer == 0 ? 0 : 1\n  return (layerScore, -window.area)\n}\n\nlet ordered: [WindowInfo]\nif frontmostFlag {\n  ordered = windows\n} else {\n  ordered = windows.sorted { rank($0) < rank($1) }\n}\nlet selected = ordered.first\n\nlet list: [WindowInfo]?\nif includeList {\n  list = ordered\n} else {\n  list = nil\n}\n\nlet response = Response(count: windows.count, selected: selected, windows: list)\nlet encoder = JSONEncoder()\nencoder.outputFormatting = [.sortedKeys]\n\nif let data = try? encoder.encode(response),\n   let json = String(data: data, encoding: .utf8) {\n  print(json)\n} else {\n  fputs(\"{\\\"count\\\":\\(windows.count)}\\n\", stderr)\n  exit(1)\n}\n"
  },
  {
    "path": "skills/.curated/screenshot/scripts/take_screenshot.ps1",
    "content": "param(\n  [string]$Path,\n  [ValidateSet(\"default\", \"temp\")][string]$Mode = \"default\",\n  [string]$Format = \"png\",\n  [string]$Region,\n  [switch]$ActiveWindow,\n  [int]$WindowHandle\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = \"Stop\"\n\nfunction Get-Timestamp {\n  Get-Date -Format \"yyyy-MM-dd_HH-mm-ss\"\n}\n\nfunction Get-DefaultDirectory {\n  $home = [Environment]::GetFolderPath(\"UserProfile\")\n  $pictures = Join-Path $home \"Pictures\"\n  $screenshots = Join-Path $pictures \"Screenshots\"\n  if (Test-Path $screenshots) { return $screenshots }\n  if (Test-Path $pictures) { return $pictures }\n  return $home\n}\n\nfunction New-DefaultFilename {\n  param([string]$Prefix)\n  if (-not $Prefix) { $Prefix = \"screenshot\" }\n  \"$Prefix-$(Get-Timestamp).$Format\"\n}\n\nfunction Resolve-OutputPath {\n  if ($Path) {\n    $expanded = [Environment]::ExpandEnvironmentVariables($Path)\n    $homeDir = [Environment]::GetFolderPath(\"UserProfile\")\n    if ($expanded -eq \"~\") {\n      $expanded = $homeDir\n    } elseif ($expanded.StartsWith(\"~/\") -or $expanded.StartsWith(\"~\\\\\")) {\n      $expanded = Join-Path $homeDir $expanded.Substring(2)\n    }\n    $full = [System.IO.Path]::GetFullPath($expanded)\n    if ((Test-Path $full) -and (Get-Item $full).PSIsContainer) {\n      $full = Join-Path $full (New-DefaultFilename \"\")\n    } elseif (($expanded.EndsWith(\"\\\") -or $expanded.EndsWith(\"/\")) -and -not (Test-Path $full)) {\n      New-Item -ItemType Directory -Path $full -Force | Out-Null\n      $full = Join-Path $full (New-DefaultFilename \"\")\n    } elseif ([System.IO.Path]::GetExtension($full) -eq \"\") {\n      $full = \"$full.$Format\"\n    }\n    $parent = Split-Path -Parent $full\n    if ($parent) {\n      New-Item -ItemType Directory -Path $parent -Force | Out-Null\n    }\n    return $full\n  }\n\n  if ($Mode -eq \"temp\") {\n    $tmp = [System.IO.Path]::GetTempPath()\n    return Join-Path $tmp (New-DefaultFilename \"codex-shot\")\n  }\n\n  $dest = Get-DefaultDirectory\n  return Join-Path $dest (New-DefaultFilename \"\")\n}\n\nfunction Parse-Region {\n  if (-not $Region) { return $null }\n  $parts = $Region.Split(\",\") | ForEach-Object { $_.Trim() }\n  if ($parts.Length -ne 4) {\n    throw \"Region must be x,y,w,h\"\n  }\n  $values = $parts | ForEach-Object {\n    $out = 0\n    if (-not [int]::TryParse($_, [ref]$out)) {\n      throw \"Region values must be integers\"\n    }\n    $out\n  }\n  if ($values[2] -le 0 -or $values[3] -le 0) {\n    throw \"Region width and height must be positive\"\n  }\n  return $values\n}\n\nif ($Region -and $ActiveWindow) {\n  throw \"Choose either -Region or -ActiveWindow\"\n}\nif ($Region -and $WindowHandle) {\n  throw \"Choose either -Region or -WindowHandle\"\n}\nif ($ActiveWindow -and $WindowHandle) {\n  throw \"Choose either -ActiveWindow or -WindowHandle\"\n}\n\n$regionValues = Parse-Region\n$outputPath = Resolve-OutputPath\n\nAdd-Type -AssemblyName System.Windows.Forms\nAdd-Type -AssemblyName System.Drawing\n\n$imageFormat = switch ($Format.ToLowerInvariant()) {\n  \"png\" { [System.Drawing.Imaging.ImageFormat]::Png }\n  \"jpg\" { [System.Drawing.Imaging.ImageFormat]::Jpeg }\n  \"jpeg\" { [System.Drawing.Imaging.ImageFormat]::Jpeg }\n  \"bmp\" { [System.Drawing.Imaging.ImageFormat]::Bmp }\n  default { throw \"Unsupported format: $Format\" }\n}\n\nAdd-Type @\"\nusing System;\nusing System.Runtime.InteropServices;\npublic static class NativeMethods {\n  [StructLayout(LayoutKind.Sequential)]\n  public struct RECT {\n    public int Left;\n    public int Top;\n    public int Right;\n    public int Bottom;\n  }\n\n  [DllImport(\"user32.dll\")]\n  public static extern IntPtr GetForegroundWindow();\n\n  [DllImport(\"user32.dll\")]\n  public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);\n}\n\"@\n\nif ($regionValues) {\n  $x = $regionValues[0]\n  $y = $regionValues[1]\n  $w = $regionValues[2]\n  $h = $regionValues[3]\n  $bounds = New-Object System.Drawing.Rectangle($x, $y, $w, $h)\n} elseif ($ActiveWindow -or $WindowHandle) {\n  $handle = if ($WindowHandle) { [IntPtr]$WindowHandle } else { [NativeMethods]::GetForegroundWindow() }\n  $rect = New-Object NativeMethods+RECT\n  if (-not [NativeMethods]::GetWindowRect($handle, [ref]$rect)) {\n    throw \"Failed to get window bounds\"\n  }\n  $width = $rect.Right - $rect.Left\n  $height = $rect.Bottom - $rect.Top\n  $bounds = New-Object System.Drawing.Rectangle($rect.Left, $rect.Top, $width, $height)\n} else {\n  $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen\n  $bounds = New-Object System.Drawing.Rectangle($vs.Left, $vs.Top, $vs.Width, $vs.Height)\n}\n\n$bitmap = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)\n$graphics = [System.Drawing.Graphics]::FromImage($bitmap)\n\ntry {\n  $source = New-Object System.Drawing.Point($bounds.Left, $bounds.Top)\n  $target = [System.Drawing.Point]::Empty\n  $size = New-Object System.Drawing.Size($bounds.Width, $bounds.Height)\n  $graphics.CopyFromScreen($source, $target, $size)\n  $bitmap.Save($outputPath, $imageFormat)\n} finally {\n  $graphics.Dispose()\n  $bitmap.Dispose()\n}\n\nWrite-Output $outputPath\n"
  },
  {
    "path": "skills/.curated/screenshot/scripts/take_screenshot.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Cross-platform screenshot helper for Codex skills.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport datetime as dt\nimport json\nimport os\nimport platform\nimport shutil\nimport subprocess\nimport tempfile\nfrom pathlib import Path\n\nSCRIPT_DIR = Path(__file__).resolve().parent\nMAC_PERM_SCRIPT = SCRIPT_DIR / \"macos_permissions.swift\"\nMAC_PERM_HELPER = SCRIPT_DIR / \"ensure_macos_permissions.sh\"\nMAC_WINDOW_SCRIPT = SCRIPT_DIR / \"macos_window_info.swift\"\nMAC_DISPLAY_SCRIPT = SCRIPT_DIR / \"macos_display_info.swift\"\nTEST_MODE_ENV = \"CODEX_SCREENSHOT_TEST_MODE\"\nTEST_PLATFORM_ENV = \"CODEX_SCREENSHOT_TEST_PLATFORM\"\nTEST_WINDOWS_ENV = \"CODEX_SCREENSHOT_TEST_WINDOWS\"\nTEST_DISPLAYS_ENV = \"CODEX_SCREENSHOT_TEST_DISPLAYS\"\nTEST_PNG = (\n    b\"\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01\"\n    b\"\\x08\\x06\\x00\\x00\\x00\\x1f\\x15\\xc4\\x89\\x00\\x00\\x00\\x0cIDAT\\x08\\xd7c\"\n    b\"\\xf8\\xff\\xff?\\x00\\x05\\xfe\\x02\\xfeA\\xad\\x1c\\x1c\\x00\\x00\\x00\\x00IEND\"\n    b\"\\xaeB`\\x82\"\n)\n\n\ndef parse_region(value: str) -> tuple[int, int, int, int]:\n    parts = [p.strip() for p in value.split(\",\")]\n    if len(parts) != 4:\n        raise argparse.ArgumentTypeError(\"region must be x,y,w,h\")\n    try:\n        x, y, w, h = (int(p) for p in parts)\n    except ValueError as exc:\n        raise argparse.ArgumentTypeError(\"region values must be integers\") from exc\n    if w <= 0 or h <= 0:\n        raise argparse.ArgumentTypeError(\"region width and height must be positive\")\n    return x, y, w, h\n\n\ndef test_mode_enabled() -> bool:\n    value = os.environ.get(TEST_MODE_ENV, \"\")\n    return value.lower() in {\"1\", \"true\", \"yes\", \"on\"}\n\n\ndef normalize_platform(value: str) -> str:\n    lowered = value.strip().lower()\n    if lowered in {\"darwin\", \"mac\", \"macos\", \"osx\"}:\n        return \"Darwin\"\n    if lowered in {\"linux\", \"ubuntu\"}:\n        return \"Linux\"\n    if lowered in {\"windows\", \"win\"}:\n        return \"Windows\"\n    return value\n\n\ndef test_platform_override() -> str | None:\n    value = os.environ.get(TEST_PLATFORM_ENV)\n    if value:\n        return normalize_platform(value)\n    return None\n\n\ndef parse_int_list(value: str) -> list[int]:\n    results: list[int] = []\n    for part in value.split(\",\"):\n        part = part.strip()\n        if not part:\n            continue\n        try:\n            results.append(int(part))\n        except ValueError:\n            continue\n    return results\n\n\ndef test_window_ids() -> list[int]:\n    value = os.environ.get(TEST_WINDOWS_ENV, \"101,102\")\n    ids = parse_int_list(value)\n    return ids or [101]\n\n\ndef test_display_ids() -> list[int]:\n    value = os.environ.get(TEST_DISPLAYS_ENV, \"1,2\")\n    ids = parse_int_list(value)\n    return ids or [1]\n\n\ndef write_test_png(path: Path) -> None:\n    ensure_parent(path)\n    path.write_bytes(TEST_PNG)\n\n\ndef timestamp() -> str:\n    return dt.datetime.now().strftime(\"%Y-%m-%d_%H-%M-%S\")\n\n\ndef default_filename(fmt: str, prefix: str = \"screenshot\") -> str:\n    return f\"{prefix}-{timestamp()}.{fmt}\"\n\n\ndef mac_default_dir() -> Path:\n    desktop = Path.home() / \"Desktop\"\n    try:\n        proc = subprocess.run(\n            [\"defaults\", \"read\", \"com.apple.screencapture\", \"location\"],\n            check=False,\n            capture_output=True,\n            text=True,\n        )\n        location = proc.stdout.strip()\n        if location:\n            return Path(location).expanduser()\n    except OSError:\n        pass\n    return desktop\n\n\ndef default_dir(system: str) -> Path:\n    home = Path.home()\n    if system == \"Darwin\":\n        return mac_default_dir()\n    if system == \"Windows\":\n        pictures = home / \"Pictures\"\n        screenshots = pictures / \"Screenshots\"\n        if screenshots.exists():\n            return screenshots\n        if pictures.exists():\n            return pictures\n        return home\n    pictures = home / \"Pictures\"\n    screenshots = pictures / \"Screenshots\"\n    if screenshots.exists():\n        return screenshots\n    if pictures.exists():\n        return pictures\n    return home\n\n\ndef ensure_parent(path: Path) -> None:\n    try:\n        path.parent.mkdir(parents=True, exist_ok=True)\n    except OSError:\n        # Fall back to letting the capture command report a clearer error.\n        pass\n\n\ndef resolve_output_path(\n    requested_path: str | None, mode: str, fmt: str, system: str\n) -> Path:\n    if requested_path:\n        path = Path(requested_path).expanduser()\n        if path.exists() and path.is_dir():\n            path = path / default_filename(fmt)\n        elif requested_path.endswith((\"/\", \"\\\\\")) and not path.exists():\n            path.mkdir(parents=True, exist_ok=True)\n            path = path / default_filename(fmt)\n        elif path.suffix == \"\":\n            path = path.with_suffix(f\".{fmt}\")\n        ensure_parent(path)\n        return path\n\n    if mode == \"temp\":\n        tmp_dir = Path(tempfile.gettempdir())\n        tmp_path = tmp_dir / default_filename(fmt, prefix=\"codex-shot\")\n        ensure_parent(tmp_path)\n        return tmp_path\n\n    dest_dir = default_dir(system)\n    dest_path = dest_dir / default_filename(fmt)\n    ensure_parent(dest_path)\n    return dest_path\n\n\ndef multi_output_paths(base: Path, suffixes: list[str]) -> list[Path]:\n    if len(suffixes) <= 1:\n        return [base]\n    paths: list[Path] = []\n    for suffix in suffixes:\n        candidate = base.with_name(f\"{base.stem}-{suffix}{base.suffix}\")\n        ensure_parent(candidate)\n        paths.append(candidate)\n    return paths\n\n\ndef run(cmd: list[str]) -> None:\n    try:\n        subprocess.run(cmd, check=True)\n    except FileNotFoundError as exc:\n        raise SystemExit(f\"required command not found: {cmd[0]}\") from exc\n    except subprocess.CalledProcessError as exc:\n        raise SystemExit(f\"command failed ({exc.returncode}): {' '.join(cmd)}\") from exc\n\n\ndef swift_json(script: Path, extra_args: list[str] | None = None) -> dict:\n    module_cache = Path(tempfile.gettempdir()) / \"codex-swift-module-cache\"\n    module_cache.mkdir(parents=True, exist_ok=True)\n    cmd = [\"swift\", \"-module-cache-path\", str(module_cache), str(script)]\n    if extra_args:\n        cmd.extend(extra_args)\n    try:\n        proc = subprocess.run(cmd, check=True, capture_output=True, text=True)\n    except FileNotFoundError as exc:\n        raise SystemExit(\"swift not found; install Xcode command line tools\") from exc\n    except subprocess.CalledProcessError as exc:\n        stderr = (exc.stderr or \"\").strip()\n        if \"ModuleCache\" in stderr and \"Operation not permitted\" in stderr:\n            raise SystemExit(\n                \"swift needs module-cache access; rerun with escalated permissions\"\n            ) from exc\n        msg = stderr or (exc.stdout or \"\").strip() or \"swift helper failed\"\n        raise SystemExit(msg) from exc\n    try:\n        return json.loads(proc.stdout)\n    except json.JSONDecodeError as exc:\n        raise SystemExit(f\"swift helper returned invalid JSON: {proc.stdout.strip()}\") from exc\n\n\ndef macos_screen_capture_granted(request: bool = False) -> bool:\n    args = [\"--request\"] if request else []\n    payload = swift_json(MAC_PERM_SCRIPT, args)\n    return bool(payload.get(\"screenCapture\"))\n\n\ndef ensure_macos_permissions() -> None:\n    if os.environ.get(\"CODEX_SANDBOX\"):\n        raise SystemExit(\n            \"screen capture checks are blocked in the sandbox; rerun with escalated permissions\"\n        )\n    if macos_screen_capture_granted():\n        return\n    subprocess.run([\"bash\", str(MAC_PERM_HELPER)], check=False)\n    if not macos_screen_capture_granted():\n        raise SystemExit(\n            \"Screen Recording permission is required; enable it in System Settings and retry\"\n        )\n\n\ndef activate_app(app: str) -> None:\n    safe_app = app.replace('\"', '\\\\\"')\n    script = f'tell application \"{safe_app}\" to activate'\n    subprocess.run([\"osascript\", \"-e\", script], check=False, capture_output=True, text=True)\n\n\ndef macos_window_payload(args: argparse.Namespace, frontmost: bool, include_list: bool) -> dict:\n    flags: list[str] = []\n    if frontmost:\n        flags.append(\"--frontmost\")\n    if args.app:\n        flags.extend([\"--app\", args.app])\n    if args.window_name:\n        flags.extend([\"--window-name\", args.window_name])\n    if include_list:\n        flags.append(\"--list\")\n    return swift_json(MAC_WINDOW_SCRIPT, flags)\n\n\ndef macos_display_indexes() -> list[int]:\n    payload = swift_json(MAC_DISPLAY_SCRIPT)\n    displays = payload.get(\"displays\") or []\n    indexes: list[int] = []\n    for item in displays:\n        try:\n            value = int(item)\n        except (TypeError, ValueError):\n            continue\n        if value > 0:\n            indexes.append(value)\n    return indexes or [1]\n\n\ndef macos_window_ids(args: argparse.Namespace, capture_all: bool) -> list[int]:\n    payload = macos_window_payload(\n        args,\n        frontmost=args.active_window,\n        include_list=capture_all,\n    )\n    if capture_all:\n        windows = payload.get(\"windows\") or []\n        ids: list[int] = []\n        for item in windows:\n            win_id = item.get(\"id\")\n            if win_id is None:\n                continue\n            try:\n                ids.append(int(win_id))\n            except (TypeError, ValueError):\n                continue\n        if ids:\n            return ids\n    selected = payload.get(\"selected\") or {}\n    win_id = selected.get(\"id\")\n    if win_id is not None:\n        try:\n            return [int(win_id)]\n        except (TypeError, ValueError):\n            pass\n    raise SystemExit(\"no matching macOS window found; try --list-windows to inspect ids\")\n\n\ndef list_macos_windows(args: argparse.Namespace) -> None:\n    payload = macos_window_payload(args, frontmost=args.active_window, include_list=True)\n    windows = payload.get(\"windows\") or []\n    if not windows:\n        print(\"no matching windows found\")\n        return\n    for item in windows:\n        bounds = item.get(\"bounds\") or {}\n        name = item.get(\"name\") or \"\"\n        width = bounds.get(\"width\", 0)\n        height = bounds.get(\"height\", 0)\n        x = bounds.get(\"x\", 0)\n        y = bounds.get(\"y\", 0)\n        print(f\"{item.get('id')}\\t{item.get('owner')}\\t{name}\\t{width}x{height}+{x}+{y}\")\n\n\ndef list_test_macos_windows(args: argparse.Namespace) -> None:\n    owner = args.app or \"TestApp\"\n    name = args.window_name or \"\"\n    ids = test_window_ids()\n    if args.active_window and ids:\n        ids = [ids[0]]\n    for idx, win_id in enumerate(ids, start=1):\n        window_name = name or f\"Window {idx}\"\n        print(f\"{win_id}\\t{owner}\\t{window_name}\\t800x600+0+0\")\n\n\ndef resolve_macos_windows(args: argparse.Namespace) -> list[int]:\n    if args.app:\n        activate_app(args.app)\n    capture_all = not args.active_window\n    return macos_window_ids(args, capture_all=capture_all)\n\n\ndef resolve_test_macos_windows(args: argparse.Namespace) -> list[int]:\n    ids = test_window_ids()\n    if args.active_window and ids:\n        return [ids[0]]\n    return ids\n\n\ndef capture_macos(\n    args: argparse.Namespace,\n    output: Path,\n    *,\n    window_id: int | None = None,\n    display: int | None = None,\n) -> None:\n    cmd = [\"screencapture\", \"-x\", f\"-t{args.format}\"]\n    if args.interactive:\n        cmd.append(\"-i\")\n    if display is not None:\n        cmd.append(f\"-D{display}\")\n    effective_window_id = window_id if window_id is not None else args.window_id\n    if effective_window_id is not None:\n        cmd.append(f\"-l{effective_window_id}\")\n    elif args.region is not None:\n        x, y, w, h = args.region\n        cmd.append(f\"-R{x},{y},{w},{h}\")\n    cmd.append(str(output))\n    run(cmd)\n\n\ndef capture_linux(args: argparse.Namespace, output: Path) -> None:\n    scrot = shutil.which(\"scrot\")\n    gnome = shutil.which(\"gnome-screenshot\")\n    imagemagick = shutil.which(\"import\")\n    xdotool = shutil.which(\"xdotool\")\n\n    if args.region is not None:\n        x, y, w, h = args.region\n        if scrot:\n            run([\"scrot\", \"-a\", f\"{x},{y},{w},{h}\", str(output)])\n            return\n        if imagemagick:\n            geometry = f\"{w}x{h}+{x}+{y}\"\n            run([\"import\", \"-window\", \"root\", \"-crop\", geometry, str(output)])\n            return\n        raise SystemExit(\"region capture requires scrot or ImageMagick (import)\")\n\n    if args.window_id is not None:\n        if imagemagick:\n            run([\"import\", \"-window\", str(args.window_id), str(output)])\n            return\n        raise SystemExit(\"window-id capture requires ImageMagick (import)\")\n\n    if args.active_window:\n        if scrot:\n            run([\"scrot\", \"-u\", str(output)])\n            return\n        if gnome:\n            run([\"gnome-screenshot\", \"-w\", \"-f\", str(output)])\n            return\n        if imagemagick and xdotool:\n            win_id = (\n                subprocess.check_output([\"xdotool\", \"getactivewindow\"], text=True)\n                .strip()\n            )\n            run([\"import\", \"-window\", win_id, str(output)])\n            return\n        raise SystemExit(\"active-window capture requires scrot, gnome-screenshot, or import+xdotool\")\n\n    if scrot:\n        run([\"scrot\", str(output)])\n        return\n    if gnome:\n        run([\"gnome-screenshot\", \"-f\", str(output)])\n        return\n    if imagemagick:\n        run([\"import\", \"-window\", \"root\", str(output)])\n        return\n    raise SystemExit(\"no supported screenshot tool found (scrot, gnome-screenshot, or import)\")\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(description=__doc__)\n    parser.add_argument(\n        \"--path\",\n        help=\"output file path or directory; overrides --mode\",\n    )\n    parser.add_argument(\n        \"--mode\",\n        choices=(\"default\", \"temp\"),\n        default=\"default\",\n        help=\"default saves to the OS screenshot location; temp saves to the temp dir\",\n    )\n    parser.add_argument(\n        \"--format\",\n        default=\"png\",\n        help=\"image format/extension (default: png)\",\n    )\n    parser.add_argument(\n        \"--app\",\n        help=\"macOS only: capture all matching on-screen windows for this app name\",\n    )\n    parser.add_argument(\n        \"--window-name\",\n        help=\"macOS only: substring match for a window title (optionally scoped by --app)\",\n    )\n    parser.add_argument(\n        \"--list-windows\",\n        action=\"store_true\",\n        help=\"macOS only: list matching window ids instead of capturing\",\n    )\n    parser.add_argument(\n        \"--region\",\n        type=parse_region,\n        help=\"capture region as x,y,w,h (pixel coordinates)\",\n    )\n    parser.add_argument(\n        \"--window-id\",\n        type=int,\n        help=\"capture a specific window id when supported\",\n    )\n    parser.add_argument(\n        \"--active-window\",\n        action=\"store_true\",\n        help=\"capture the focused/active window only when supported\",\n    )\n    parser.add_argument(\n        \"--interactive\",\n        action=\"store_true\",\n        help=\"use interactive selection where the OS tool supports it\",\n    )\n    args = parser.parse_args()\n\n    if args.region and args.window_id is not None:\n        raise SystemExit(\"choose either --region or --window-id, not both\")\n    if args.region and args.active_window:\n        raise SystemExit(\"choose either --region or --active-window, not both\")\n    if args.window_id is not None and args.active_window:\n        raise SystemExit(\"choose either --window-id or --active-window, not both\")\n    if args.app and args.window_id is not None:\n        raise SystemExit(\"choose either --app or --window-id, not both\")\n    if args.region and args.app:\n        raise SystemExit(\"choose either --region or --app, not both\")\n    if args.region and args.window_name:\n        raise SystemExit(\"choose either --region or --window-name, not both\")\n    if args.interactive and args.app:\n        raise SystemExit(\"choose either --interactive or --app, not both\")\n    if args.interactive and args.window_name:\n        raise SystemExit(\"choose either --interactive or --window-name, not both\")\n    if args.interactive and args.window_id is not None:\n        raise SystemExit(\"choose either --interactive or --window-id, not both\")\n    if args.interactive and args.active_window:\n        raise SystemExit(\"choose either --interactive or --active-window, not both\")\n    if args.list_windows and (args.region or args.window_id is not None or args.interactive):\n        raise SystemExit(\"--list-windows only supports --app, --window-name, and --active-window\")\n\n    test_mode = test_mode_enabled()\n    system = platform.system()\n    if test_mode:\n        override = test_platform_override()\n        if override:\n            system = override\n    window_ids: list[int] = []\n    display_ids: list[int] = []\n\n    if system != \"Darwin\" and (args.app or args.window_name or args.list_windows):\n        raise SystemExit(\"--app/--window-name/--list-windows are supported on macOS only\")\n\n    if system == \"Darwin\":\n        if test_mode:\n            if args.list_windows:\n                list_test_macos_windows(args)\n                return\n            if args.window_id is not None:\n                window_ids = [args.window_id]\n            elif args.app or args.window_name or args.active_window:\n                window_ids = resolve_test_macos_windows(args)\n            elif args.region is None and not args.interactive:\n                display_ids = test_display_ids()\n        else:\n            ensure_macos_permissions()\n            if args.list_windows:\n                list_macos_windows(args)\n                return\n            if args.window_id is not None:\n                window_ids = [args.window_id]\n            elif args.app or args.window_name or args.active_window:\n                window_ids = resolve_macos_windows(args)\n            elif args.region is None and not args.interactive:\n                display_ids = macos_display_indexes()\n\n    output = resolve_output_path(args.path, args.mode, args.format, system)\n\n    if test_mode:\n        if system == \"Darwin\":\n            if window_ids:\n                suffixes = [f\"w{wid}\" for wid in window_ids]\n                paths = multi_output_paths(output, suffixes)\n                for path in paths:\n                    write_test_png(path)\n                for path in paths:\n                    print(path)\n                return\n            if len(display_ids) > 1:\n                suffixes = [f\"d{did}\" for did in display_ids]\n                paths = multi_output_paths(output, suffixes)\n                for path in paths:\n                    write_test_png(path)\n                for path in paths:\n                    print(path)\n                return\n        write_test_png(output)\n        print(output)\n        return\n\n    if system == \"Darwin\":\n        if window_ids:\n            suffixes = [f\"w{wid}\" for wid in window_ids]\n            paths = multi_output_paths(output, suffixes)\n            for wid, path in zip(window_ids, paths):\n                capture_macos(args, path, window_id=wid)\n            for path in paths:\n                print(path)\n            return\n        if len(display_ids) > 1:\n            suffixes = [f\"d{did}\" for did in display_ids]\n            paths = multi_output_paths(output, suffixes)\n            for did, path in zip(display_ids, paths):\n                capture_macos(args, path, display=did)\n            for path in paths:\n                print(path)\n            return\n        capture_macos(args, output)\n    elif system == \"Linux\":\n        capture_linux(args, output)\n    elif system == \"Windows\":\n        raise SystemExit(\n            \"Windows support lives in scripts/take_screenshot.ps1; run it with PowerShell\"\n        )\n    else:\n        raise SystemExit(f\"unsupported platform: {system}\")\n\n    print(output)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/security-best-practices/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/security-best-practices/SKILL.md",
    "content": "---\nname: \"security-best-practices\"\ndescription: \"Perform language and framework specific security best-practice reviews and suggest improvements. Trigger only when the user explicitly requests security best practices guidance, a security review/report, or secure-by-default coding help. Trigger only for supported languages (python, javascript/typescript, go). Do not trigger for general code review, debugging, or non-security tasks.\"\n---\n\n# Security Best Practices\n\n## Overview\n\nThis skill provides a description of how to identify the language and frameworks used by the current context, and then to load information from this skill's references directory about the security best practices for this language and or frameworks.\n\nThis information, if present, can be used to write new secure by default code, or to passively detect major issues within existing code, or (if requested by the user) provide a vulnerability report and suggest fixes.\n\n## Workflow\n\nThe initial step for this skill is to identify ALL languages and ALL frameworks which you are being asked to use or already exist in the scope of the project you are working in. Focus on the primary core frameworks. Often you will want to identify both frontend and backend languages and frameworks.\n\nThen check this skill's references directory to see if there are any relevant documentation for the language and or frameworks. Make sure you read ALL reference files which relate to the specific framework or language. The format of the filenames is `<language>-<framework>-<stack>-security.md`. You should also check if there is a `<language>-general-<stack>-security.md` which is agnostic to the framework you may be using.\n\nIf working on a web application which includes a frontend and a backend, make sure you have checked for reference documents for BOTH the frontend and backend!\n\nIf you are asked to make a web app which will include both a frontend and backend, but the frontend framework is not specified, also check out `javascript-general-web-frontend-security.md`. It is important that you understand how to secure both the frontend and backend.\n\nIf no relevant information is available in the skill's references directory, think a little bit about what you know about the language, the framework, and all well known security best practices for it. If you are unsure you can try to search online for documentation on security best practices.\n\nFrom there it can operate in a few ways.\n\n1. The primary mode is to just use the information to write secure by default code from this point forward. This is useful for starting a new project or when writing new code.\n\n2. The secondary mode is to passively detect vulnerabilities while working in the project and writing code for the user. Critical or very important vulnerabilities or major issues going against security guidance can be flagged and the user can be told about them. This passive mode should focus on the largest impact vulnerabilities and secure defaults.\n\n3. The user can ask for a security report or to improve the security of the codebase. In this case a full report should be produced describe anyways the project fails to follow security best practices guidance. The report should be prioritized and have clear sections of severity and urgency. Then offer to start working on fixes for these issues. See #fixes below.\n\n## Workflow Decision Tree\n\n- If the language/framework is unclear, inspect the repo to determine it and list your evidence.\n- If matching guidance exists in `references/`, load only the relevant files and follow their instructions.\n- If no matching guidance exists, consider if you know any well known security best practices for the chosen language and or frameworks, but if asked to generate a report, let the user know that concrete guidance is not available (you can still generate the report or detect for sure critical vulnerabilities)\n\n# Overrides\n\nWhile these references contain the security best practices for languages and frameworks, customers may have cases where they need to bypass or override these practices. Pay attention to specific rules and instructions in the project's documentation and prompt files which may require you to override certain best practices. When overriding a best practice, you MAY report it to the user, but do not fight with them. If a security best practice needs to be bypassed / ignored for some project specific reason, you can also suggest to add documentation about this to the project so it is clear why the best practice is not being followed and to follow that bypass in the future.\n\n# Report Format\n\nWhen producing a report, you should write the report as a markdown file in `security_best_practices_report.md` or some other location if provided by the user. You can ask the user where they would like the report to be written to.\n\nThe report should have a short executive summary at the top.\n\nThe report should be clearly delineated into multiple sections based on severity of the vulnerability. The report should focus on the most critical findings as these have the highest impact for the user. All findings should be noted with an numeric ID to make them easier to reference.\n\nFor critical findings include a one sentence impact statement.\n\nOnce the report is written, also report it to the user directly, although you may be less verbose. You can offer to explain any of the findings or the reasons behind the security best practices guidance if the user wants more info on any findings.\n\nImportant: When referencing code in the report, make sure to find and include line numbers for the code you are referencing.\n\nAfter you write the report file, summarize the findings to the user.\n\nAlso tell the user where the final report was written to\n\n# Fixes\n\nIf you produced a report, let the user read the report and ask to begin performing fixes.\n\nIf you passively found a critical finding, notify the user and ask if they would like you to fix this finding.\n\nWhen producing fixes, focus on fixing a single finding at a time. The fixes should have concise clear comments explaining that the new code is based on the specific security best practice, and perhaps a very short reason why it would be dangerous to not do it in this way.\n\nAlways consider if the changes you want to make will impact the functionality of the user's code. Consider if the changes may cause regressions with how the project works currently. It is often the case that insecure code is relied on for other reasons (and this is why insecure code lives on for so long). Avoid breaking the user's project as this may make them not want to apply security fixes in the future. It is better to write a well thought out, well informed by the rest of the project, fix, then a quick slapdash change.\n\nAlways follow any normal change or commit flow the user has configured. If making git commits, provide clear commit messages explaining this is to align with security best practices. Try to avoid bunching a number of unrelated findings into a single commit.\n\nAlways follow any normal testing flows the user has configured (if any) to confirm that your changes are not introducing regressions. Consider the second order impacts the changes may have and inform the user before making them if there are any.\n\n# General Security Advice\n\nBelow is a few bits of secure coding advice that applies to almost any language or framework.\n\n### Avoid Using Incrementing IDs for Public IDs of Resources\n\nWhen assigning an ID for some resource, which will then be used by exposed to the internet, avoid using small auto-incrementing IDs. Use longer, random UUID4 or random hex string instead. This will prevent users from learning the quantity of a resource and being able to guess resource IDs.\n\n### A note on TLS\n\nWhile TLS is important for production deployments, most development work will be with TLS disabled or provided by some out-of-scope TLS proxy. Due to this, be very careful about not reporting lack of TLS as a security issue. Also be very careful around use of \"secure\" cookies. They should only be set if the application will actually be over TLS. If they are set on non-TLS applications (such as when deployed for local dev or testing), it will break the application. You can provide a env or other flag to override setting secure as a way to keep it off until on a TLS production deployment. Additionally avoid recommending HSTS. It is dangerous to use without full understanding of the lasting impacts (can cause major outages and user lockout) and it is not generally recommended for the scope of projects being reviewed by codex.\n"
  },
  {
    "path": "skills/.curated/security-best-practices/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Security Best Practices\"\n  short_description: \"Security reviews and secure-by-default guidance\"\n  default_prompt: \"Review this codebase for security best practices and suggest secure-by-default improvements.\"\n"
  },
  {
    "path": "skills/.curated/security-best-practices/references/golang-general-backend-security.md",
    "content": "# Go (Golang) Security Spec (Go 1.25.x, Standard Library, net/http)\n\nThis document is designed as a **security spec** that supports:\n1) **Secure-by-default code generation** for new Go code.\n2) **Security review / vulnerability hunting** in existing Go code (passive “notice issues while working” and active “scan the repo and report findings”).\n\nIt is intentionally written as a set of **normative requirements** (“MUST/SHOULD/MAY”) plus **audit rules** (what bad patterns look like, how to detect them, and how to fix/mitigate them).\n\n--------------------------------------------------------------------\n\n## 0) Safety, boundaries, and anti-abuse constraints (MUST FOLLOW)\n\n- MUST NOT request, output, log, or commit secrets (API keys, passwords, private keys, session cookies, JWTs, database URLs with credentials, signing keys, client secrets).\n- MUST NOT “fix” security by disabling protections (e.g., `InsecureSkipVerify`, `GOSUMDB=off` for public modules, wildcard CORS + credentials, removing auth checks, disabling CSRF defenses on cookie-auth apps).\n- MUST provide **evidence-based findings** during audits: cite file paths, code snippets, build/deploy configs, and concrete values that justify the claim.\n- MUST treat uncertainty honestly: if a control might exist in infrastructure (reverse proxy, WAF, service mesh, platform config), report it as “not visible in app code; verify at runtime/config.”\n- MUST keep fixes minimal, correct, and production-safe; avoid introducing breaking changes without warning (especially around auth/session flows, and proxies).\n\n--------------------------------------------------------------------\n\n## 1) Operating modes\n\n### 1.1 Generation mode (default)\nWhen asked to write new Go code or modify existing code:\n- MUST follow every **MUST** requirement in this spec.\n- SHOULD follow every **SHOULD** requirement unless the user explicitly says otherwise.\n- MUST prefer safe-by-default APIs and proven libraries over custom security code.\n- MUST avoid introducing new risky sinks (shell execution, dynamic template execution, serving user files as HTML, unsafe redirects, weak crypto, unbounded parsing, etc.).\n\n### 1.2 Passive review mode (always on while editing)\nWhile working anywhere in a Go repo (even if the user did not ask for a security scan):\n- MUST “notice” violations of this spec in touched/nearby code.\n- SHOULD mention issues as they come up, with a brief explanation + safe fix.\n\n### 1.3 Active audit mode (explicit scan request)\nWhen the user asks to “scan”, “audit”, or “hunt for vulns”:\n- MUST systematically search the codebase for violations of this spec.\n- MUST output findings in a structured format (see §2.3).\n\nRecommended audit order:\n1) Build/deploy entrypoints: `main.go`, `cmd/*`, Dockerfiles, Kubernetes manifests, systemd units, CI workflows.\n2) Go toolchain & dependency policy: Go version, modules, `go.mod/go.sum`, proxy/sumdb settings, govulncheck usage.\n3) Secret management and config loading (env, files, secret stores) + logging patterns.\n4) HTTP server configuration (timeouts, body limits, proxy trust, security headers).\n5) AuthN/AuthZ boundaries, session/cookie settings, token validation.\n6) CSRF protections for cookie-authenticated state-changing endpoints.\n7) Template usage and output encoding (XSS), and any “render template from string” behavior (SSTI).\n8) File handling (uploads/downloads/path traversal/temp files), static file serving.\n9) Injection sinks: SQL, OS command execution, SSRF/outbound fetch, open redirects.\n10) Concurrency/resource exhaustion (unbounded goroutines/queues, missing timeouts/contexts).\n11) Use of `unsafe` / `cgo` / `reflect` in security-sensitive paths.\n12) Debug/diagnostic endpoints (pprof/expvar/metrics) exposure.\n13) Cryptography usage (randomness, password hashing).\n\n--------------------------------------------------------------------\n\n## 2) Definitions and review guidance\n\n### 2.1 Untrusted input (treat as attacker-controlled unless proven otherwise)\nExamples include:\n- `*http.Request` fields: `r.URL.Path`, `r.URL.RawQuery`, `r.Form`, `r.PostForm`, headers, cookies, `r.Body`\n- Path parameters from routers (including values extracted from URL paths)\n- JSON/XML/YAML bodies, multipart form parts, uploaded files\n- Any data from external systems (webhooks, third-party APIs, message queues)\n- Any persisted user content (DB rows) that originated from users\n- Configuration values that might be attacker-influenced in some deployments (headers set by upstream proxies, environment variables in multi-tenant systems)\n\n### 2.2 State-changing request\nA request is state-changing if it can create/update/delete data, change auth/session state, trigger side effects (purchase, email send, webhook send), or initiate privileged actions.\n\n### 2.3 Required audit finding format\nFor each issue found, output:\n\n- Rule ID:\n- Severity: Critical / High / Medium / Low\n- Location: file path + function/handler name + line(s)\n- Evidence: the exact code/config snippet\n- Impact: what could go wrong, who can exploit it\n- Fix: safe change (prefer minimal diff)\n- Mitigation: defense-in-depth if immediate fix is hard\n- False positive notes: what to verify if uncertain (edge configs, proxy behavior, auth assumptions)\n\n--------------------------------------------------------------------\n\n## 3) Secure baseline: minimum production configuration (MUST in production)\n\nThis is the smallest “production baseline” that prevents common Go misconfigurations.\n\n### 3.1 Toolchain, patching, and dependency hygiene (MUST)\n- MUST run a supported Go major version and keep to the latest patch releases.\n- MUST treat Go standard library patch releases as security-relevant (many security fixes land in stdlib components like `net/http`, `crypto/*`, parsing packages).\n- MUST use Go modules with committed `go.mod` and `go.sum`.\n- MUST NOT disable module authenticity mechanisms for public modules (checksum DB) unless you have a controlled, documented replacement.\n- MUST run `govulncheck` (source scan and/or binary scan) in CI and address findings.\n\n### 3.2 HTTP server baseline (MUST for network-facing services)\nIf the program serves HTTP (directly or via a framework built on `net/http`):\n- MUST configure an `http.Server` with explicit timeouts and header limits.\n- MUST set request body size limits (global and per-route as needed).\n- MUST avoid exposing diagnostic endpoints (pprof/expvar) publicly.\n- SHOULD set a consistent set of security headers (or verify they are set at the edge).\n- MUST set cookie security attributes for any cookies you issue.\n- SHOULD implement rate limiting and abuse controls for auth and expensive endpoints.\n\nIllustrative baseline skeleton (adjust to your project):\n- Create a dedicated mux (avoid implicit global defaults unless intentionally managed).\n- Wrap handlers with: panic-safe error handling, request ID, logging, auth, and limits.\n\n--------------------------------------------------------------------\n\n## 4) Rules (generation + audit)\n\nEach rule contains: required practice, insecure patterns, detection hints, and remediation.\n\n### GO-DEPLOY-001: Keep the Go toolchain and standard library updated (security releases)\nSeverity: Medium\n\nNOTE: Upgrading dependencies and the core Go version can break projects in unexpected ways. Focus on only security-critical dependencies and if noticed, let the user know rather than upgrading automatically.\n\nRequired:\n- MUST run a supported Go major release and apply patch releases promptly.\n- SHOULD treat patch releases as security-relevant, even if your application code didn’t change.\n\nInsecure patterns:\n- Production builds pinned to old Go versions without a patching process.\n- Docker images like `golang:1.xx` or custom base images that are not updated regularly.\n- CI pipelines that intentionally suppress Go updates.\n\nDetection hints:\n- Inspect CI (`.github/workflows`, `gitlab-ci.yml`, etc.) for `go-version:` or toolchain setup.\n- Inspect Dockerfiles for `FROM golang:` tags.\n- Inspect `go.mod` `go` directive and any toolchain pinning.\n\nFix:\n- Upgrade to the latest patch of a supported Go version.\n- Add an automated check (CI) that fails when Go is below an approved minimum.\n\nNotes:\n- Go publishes regular minor releases that frequently include security fixes across standard library packages.\n\n---\n\n### GO-SUPPLY-001: Go module authenticity MUST NOT be disabled for public dependencies\nSeverity: High\n\nRequired:\n- MUST keep module checksum verification enabled for public modules.\n- SHOULD commit `go.sum` and treat changes as security-sensitive.\n- MUST NOT use insecure module fetching settings for public modules.\n- MAY configure private module behavior using `GOPRIVATE`/`GONOSUMDB` for private repos, but must do so narrowly and intentionally.\n\nInsecure patterns:\n- `GOSUMDB=off` in CI or production build environments for public modules.\n- `GONOSUMDB=*` or overly broad patterns that effectively disable verification.\n- `GOINSECURE=*` or broad `GOINSECURE` patterns for public modules.\n- `GOPROXY=direct` everywhere without a clear policy.\n\nDetection hints:\n- Search build configs for `GOSUMDB`, `GONOSUMDB`, `GOINSECURE`, `GOPROXY`, `GOPRIVATE`.\n- Look for documentation/scripts that recommend disabling checksum DB “to make builds work”.\n\nFix:\n- Restore defaults for public module verification.\n- For private modules:\n  - Set `GOPRIVATE=your.private.domain/*`\n  - Configure an internal proxy or direct fetching, and restrict `GONOSUMDB` to private patterns only.\n\nNotes:\n- Disabling checksum verification removes an important integrity layer against targeted or compromised upstream delivery.\n\n---\n\n### GO-CONFIG-001: Secrets must be externalized and never logged or committed\nSeverity: High (Critical if credentials are committed)\n\nRequired:\n- MUST load secrets from environment variables, secret managers, or secure config files with restricted permissions.\n- MUST NOT hard-code secrets in Go source, test fixtures that may reach production, or build args.\n- MUST NOT log secrets or full credential-bearing connection strings.\n- SHOULD fail closed in production if required secrets are missing.\n\nInsecure patterns:\n- String constants containing tokens/keys/passwords.\n- `.env` files or config files with secrets committed to repo.\n- Logging `os.Environ()`, dumping full configs, or printing DSNs.\n\nDetection hints:\n- Search for suspicious literals (`API_KEY`, `SECRET`, `PASSWORD`, `Authorization:`).\n- Inspect config loaders and logging statements.\n- Inspect CI logs or debug print paths.\n\nFix:\n- Move secrets to a secret store / environment variables.\n- Redact sensitive fields in logs.\n- Add secret scanning to CI and pre-commit.\n\n---\n\n### GO-HTTP-001: HTTP servers MUST set timeouts and MaxHeaderBytes\nSeverity: High (DoS risk)\n\nRequired:\n- MUST set: `ReadHeaderTimeout`, and SHOULD set `ReadTimeout`, `WriteTimeout`, `IdleTimeout` as appropriate for the service.\n- MUST set `MaxHeaderBytes` to a justified limit for your application.\n- MUST NOT rely on default zero-values for timeouts in production for internet-facing servers.\n\nInsecure patterns:\n- `http.ListenAndServe(\":8080\", handler)` with a default `http.Server` (no explicit timeouts).\n- `&http.Server{}` with timeouts left at zero.\n- Missing `MaxHeaderBytes`.\n\nDetection hints:\n- Search for `http.ListenAndServe(`, `ListenAndServeTLS(`, `Server{` and inspect configured fields.\n- Check for reverse proxies; even with a proxy, app-level timeouts still matter.\n\nFix:\n- Use `http.Server{ReadHeaderTimeout: ..., ReadTimeout: ..., WriteTimeout: ..., IdleTimeout: ..., MaxHeaderBytes: ...}`.\n- Calibrate timeouts per endpoint type (streaming vs JSON APIs).\n\nNotes:\n- Net/http documents that these timeouts exist and that zero/negative values mean “no timeout”; production services should choose explicit values.\n\n---\n\n### GO-HTTP-002: Request body and multipart parsing MUST be size-bounded\nSeverity: Medium (DoS risk; can be High for upload-heavy apps)\n\nRequired:\n- MUST enforce a global maximum request body size for endpoints that accept bodies.\n- MUST enforce strict multipart upload limits and avoid unbounded form parsing.\n- SHOULD enforce per-route limits when some endpoints legitimately need larger bodies.\n- SHOULD set upstream (proxy) limits as defense-in-depth.\n\nInsecure patterns:\n- Reading `r.Body` with `io.ReadAll(r.Body)` without a size cap.\n- Calling `r.ParseMultipartForm(...)` with overly large limits (or forgetting size controls).\n- Accepting file uploads with no limits on file size, number of parts, or total body size.\n\nDetection hints:\n- Search for `io.ReadAll(r.Body)`, `json.NewDecoder(r.Body)`, `ParseMultipartForm`, `FormFile`, `multipart`.\n- Look for missing `http.MaxBytesReader` or equivalent per-handler limiting.\n- Look for “upload” endpoints and check limits.\n\nFix:\n- Wrap request bodies with `http.MaxBytesReader(w, r.Body, maxBytes)` before parsing.\n- For multipart, set conservative limits and validate file sizes/part counts explicitly.\n- Set proxy limits (e.g., at ingress) in addition to app limits.\n\nNotes:\n- There are known vulnerability classes and advisories related to excessive resource consumption in multipart/form parsing; treat unbounded parsing as a security issue.\n\n---\n\n### GO-DEPLOY-002: Diagnostic endpoints (pprof/expvar/metrics) MUST NOT be publicly exposed\nSeverity: High\n\nNOTE: This only applies to production configurations. These endpoints are often used for debug or dev endpoints. If found, confirm that it would be reachable from the actual production deployment.\n\nRequired:\n- MUST NOT expose `net/http/pprof` handlers on a public internet-facing listener without strong access controls.\n- SHOULD run diagnostics on a separate, internal-only listener (loopback/VPC-only) and require auth.\n- MUST review what diagnostic endpoints reveal (stack traces, memory, command lines, environment, internal URLs).\n\nInsecure patterns:\n- Side-effect import `import _ \"net/http/pprof\"` in a server binary with a public mux.\n- `/debug/pprof/*` reachable without auth.\n- `/debug/vars` (expvar) reachable without auth.\n\nDetection hints:\n- Search for `net/http/pprof` imports (including blank imports).\n- Search for route prefixes `/debug/pprof`, `/debug/vars`.\n- Check whether `http.DefaultServeMux` is used and whether any debug handlers register globally.\n\nFix:\n- Remove diagnostics from production builds, or bind them to an internal-only listener.\n- Add strong authentication/authorization (and ideally network-level restrictions).\n\nNotes:\n- pprof is typically imported for its side effect of registering HTTP handlers under `/debug/pprof/`.\n\n---\n\n### GO-HTTP-003: Reverse proxy and forwarded header trust MUST be explicit\nSeverity: High (auth, URL generation, logging/auditing correctness)\n\nRequired:\n- If behind a reverse proxy, MUST define which proxy is trusted and how client IP/scheme/host are derived.\n- MUST NOT trust `X-Forwarded-For`, `X-Forwarded-Proto`, `Forwarded`, or similar headers from the open internet.\n- MUST ensure “secure cookie” logic, redirects, and absolute URL generation do not rely on spoofable headers.\n\nInsecure patterns:\n- Using `r.Header.Get(\"X-Forwarded-For\")` as the client IP without validating the proxy boundary.\n- Deriving “is HTTPS” from `X-Forwarded-Proto` without confirming it came from a trusted proxy.\n- Using forwarded `Host` values for password reset links without allowlisting.\n\nDetection hints:\n- Search for `X-Forwarded-For`, `X-Forwarded-Proto`, `Forwarded`, `Real-IP`, and any custom “client IP” helpers.\n- Inspect ingress/proxy configs; if not visible, mark as “verify at edge”.\n\nFix:\n- Enforce proxy trust at the edge and in app:\n  - Accept forwarded headers only from known proxy IP ranges.\n  - Prefer platform-provided mechanisms where available.\n- If generating external links, use a configured allowlisted canonical origin (not the request’s Host header).\n\n---\n\n### GO-HTTP-004: Security headers SHOULD be set (in app or at the edge)\nSeverity: Medium\n\nRequired (typical web app serving browsers):\n- SHOULD set:\n  - `Content-Security-Policy` (CSP) appropriate to the app. NOTE: It is most important to set the CSP's script-src. All other directives are not as important and can generally be excluded for the ease of development.\n  - `X-Content-Type-Options: nosniff`\n  - Clickjacking protection (`X-Frame-Options` and/or CSP `frame-ancestors`)\n  - `Referrer-Policy` and `Permissions-Policy` where appropriate\n- MUST ensure cookies have secure attributes (see GO-HTTP-005).\n\nNOTE:\n- These headers may be set via reverse proxy/CDN; if not visible in app code, report as “verify at edge”.\n\nInsecure patterns:\n- No security headers anywhere (app or edge) for a browser-facing app.\n- CSP missing for apps rendering untrusted content.\n\nDetection hints:\n- Search for middleware setting headers: `w.Header().Set(\"Content-Security-Policy\", ...)`, etc.\n- Search for reverse proxy config that sets headers.\n\nFix:\n- Add centralized header middleware in Go, or configure at the edge.\n- Keep CSP realistic; avoid `unsafe-inline` where possible.\n\n---\n\n### GO-HTTP-005: Cookies MUST use secure attributes in production\nSeverity: Medium\n\nRequired (production, HTTPS):\n- MUST set `Secure` on cookies that carry auth/session state. IMPORTANT NOTE: Only set `Secure` in production environment when TLS is configured. When running in a local dev environment over HTTP, do not set `Secure` property on cookies. You should do this conditionally based on if the app is running in production mode. You should also include a property like `SESSION_COOKIE_SECURE` which can be used to disable `Secure` cookies when testing over HTTP.\n- MUST set `HttpOnly` on auth/session cookies.\n- SHOULD set `SameSite=Lax` by default (or `Strict` if compatible), and only use `None` when necessary (and only with `Secure`).\n- SHOULD set bounded lifetimes (`Max-Age`/`Expires`) appropriate to the app.\n\nInsecure patterns:\n- Setting auth/session cookies without `Secure` in HTTPS deployments.\n- Cookies without `HttpOnly` for session identifiers.\n- `SameSite=None` for cookie-authenticated apps without a strong CSRF strategy.\n\nDetection hints:\n- Search for `http.SetCookie`, `&http.Cookie{`, `Set-Cookie`.\n- Inspect cookie flags in auth/session code.\n\nFix:\n- Set the correct fields on `http.Cookie` and centralize cookie creation.\n\nNotes:\n- SameSite is defense-in-depth and does not replace CSRF protections for cookie-auth apps.\n\n---\n\n### GO-HTTP-006: Cookie-authenticated state-changing endpoints MUST be CSRF-protected\nSeverity: High\n\n- IMPORTANT NOTE: If cookies are not used for auth (e.g., pure bearer token in Authorization header with no ambient cookies), CSRF is not a risk for those endpoints.\n\nRequired:\n- MUST protect all state-changing endpoints (POST/PUT/PATCH/DELETE) that rely on cookies for authentication.\n- SHOULD use a well-tested CSRF library/middleware rather than rolling your own.\n- MAY use additional defenses (Origin/Referer checks, Fetch Metadata, SameSite cookies), but tokens remain the primary defense for cookie-authenticated apps.\nIf tokens are impractical, or for small applications:\n* MUST at a minimum require a custom header to be set and set the session cookie SESSION_COOKIE_SAMESITE=lax, as this is the strongest method besides requiring a form token, and may be much easier to implement.\n\n\nInsecure patterns:\n- Cookie-authenticated JSON endpoints that mutate state with no CSRF checks.\n- Using GET for state-changing actions.\n\nDetection hints:\n- Enumerate all non-GET routes and identify auth mechanism.\n- Look for CSRF middleware usage; if absent, treat as suspicious in browser-facing apps.\n\nFix:\n- Add CSRF middleware and ensure it covers all state-changing routes.\n- If the service is an API intended for non-browser clients, avoid cookie auth; use Authorization headers.\n\n---\n\n### GO-HTTP-007: CORS must be explicit and least-privilege\nSeverity: Medium (High if misconfigured with credentials)\n\nRequired:\n- If CORS is not needed, MUST keep it disabled.\n- If CORS is needed:\n  - MUST allowlist trusted origins (do not reflect arbitrary origins)\n  - MUST be careful with credentialed requests; do not combine broad origins with cookies\n  - SHOULD restrict allowed methods/headers\n\nInsecure patterns:\n- `Access-Control-Allow-Origin: *` paired with cookies (`Access-Control-Allow-Credentials: true`).\n- Reflecting `Origin` without validation.\n\nDetection hints:\n- Search for `Access-Control-Allow-` header setting.\n- Search for CORS middleware configuration.\n\nFix:\n- Implement strict origin allowlists and minimal methods/headers.\n- Ensure cookie-auth endpoints are not exposed cross-origin unless required.\n\n---\n\n### GO-XSS-001: Use html/template and avoid bypassing auto-escaping with untrusted data\nSeverity: High\n\nRequired:\n- MUST use `html/template` for HTML rendering (not `text/template`).\n- MUST NOT convert untrusted data into “trusted” template types (`template.HTML`, `template.JS`, `template.URL`, etc.).\n- SHOULD keep templates static and controlled by developers; treat dynamic templates as high risk.\n- MUST NOT serve user-uploaded HTML/JS as active content unless explicitly intended and safely sandboxed.\n\nInsecure patterns:\n- `text/template` used to generate HTML.\n- Using `template.HTML(userInput)` or similar typed wrappers.\n- Directly writing unescaped user content into HTML responses.\n\nDetection hints:\n- Search for `text/template`, `template.New(...).Parse(...)`, and typed wrappers like `template.HTML(`.\n- Inspect handlers that return HTML with string concatenation.\n\nFix:\n- Use `html/template` and pass untrusted data as data, not markup.\n- If you must allow limited HTML, use a vetted HTML sanitizer and still be careful with attributes/URLs.\n\n---\n\n### GO-SSTI-001: Never parse/execute templates from untrusted input (SSTI)\nSeverity: Critical\n\nRequired:\n- MUST NOT call `template.Parse` / `template.ParseFiles` / `template.New(...).Parse(...)` on template text influenced by untrusted input.\n- MUST treat “user-defined templates” as a special high-risk design:\n  - MUST use heavy sandboxing and strict allowlists\n  - MUST isolate execution (process/container boundary) if truly required\n\nInsecure patterns:\n- `tmpl := template.Must(template.New(\"x\").Parse(r.FormValue(\"tmpl\")))`\n- Reading templates from uploads / DB entries and executing them in the same trust domain as server code.\n\nDetection hints:\n- Search for `.Parse(` and trace the origin of the template string.\n- Look for “custom email templates”, “user theming templates”, etc.\n\nFix:\n- Replace with safe substitution mechanisms (no code execution).\n- If templates must be user-controlled, isolate and sandbox aggressively.\n\n---\n\n### GO-PATH-001: Prevent path traversal and unsafe file serving\nSeverity: High\n\nRequired:\n- MUST NOT pass user-controlled paths to `os.Open`, `os.ReadFile`, `http.ServeFile`, or `http.FileServer` without strict validation and base-dir enforcement.\n- MUST treat `..`, absolute paths, and OS-specific path tricks as hostile input.\n- SHOULD store user uploads outside any static web root; serve through controlled handlers.\n- MUST avoid directory listing for sensitive file trees.\n\nInsecure patterns:\n- `http.ServeFile(w, r, r.URL.Query().Get(\"path\"))`\n- `os.Open(filepath.Join(baseDir, userPath))` without checking that the result stays under `baseDir`\n- `http.FileServer(http.Dir(\".\"))` serving the project root or user-writable directories\n\nDetection hints:\n- Search for `ServeFile(`, `FileServer(`, `http.Dir(`, `os.Open(`, `ReadFile(`, `filepath.Join(`.\n- Trace whether path components come from request/DB.\n\nFix:\n- Use an allowlist of file identifiers (e.g., database IDs) mapped to server-side paths.\n- Enforce base directory containment after cleaning and joining.\n- Serve active formats as downloads (`Content-Disposition: attachment`) unless explicitly intended.\n\n---\n\n### GO-UPLOAD-001: File uploads must be validated, stored safely, and served safely\nSeverity: High\n\nRequired:\n- MUST enforce upload size limits (app + edge).\n- MUST validate file type using allowlists and content checks (not only extensions).\n- MUST store uploads outside executable/static roots when possible.\n- SHOULD generate server-side filenames (random IDs) and avoid trusting original names.\n- MUST serve potentially active formats safely (download attachment) unless explicitly intended.\n\nInsecure patterns:\n- Accepting arbitrary file types and serving them back inline.\n- Using user-supplied filename as storage path.\n- Missing size/type validation.\n\nDetection hints:\n- Search for `multipart`, `FormFile`, `ParseMultipartForm`, `io.Copy` to disk.\n- Check where files are stored and how they are served.\n\nFix:\n- Implement allowlist validation + safe storage + safe serving.\n- Add scanning/quarantine workflows where applicable.\n\n---\n\n### GO-INJECT-001: Prevent SQL injection (parameterized queries / ORM)\nSeverity: High\n\nRequired:\n- MUST use parameterized queries or an ORM that parameterizes under the hood.\n- MUST NOT build SQL by string concatenation / `fmt.Sprintf` / string interpolation with untrusted input.\n\nInsecure patterns:\n- `fmt.Sprintf(\"SELECT ... WHERE id=%s\", r.URL.Query().Get(\"id\"))`\n- `query := \"UPDATE users SET role='\" + role + \"' WHERE id=\" + id`\n\nDetection hints:\n- Grep for `SELECT`, `INSERT`, `UPDATE`, `DELETE` and check how query strings are built.\n- Trace untrusted data into `db.Query`, `db.Exec`, `QueryRow`, etc.\n\nFix:\n- Replace with placeholders (`?`, `$1`, etc.) and pass parameters separately.\n- Validate and type-check IDs before use.\n\n---\n\n### GO-INJECT-002: Prevent OS command injection; avoid shelling out with untrusted input\nSeverity: Critical to High (depends on exposure)\n\nRequired:\n- MUST avoid executing external commands with attacker-controlled strings.\n- If subprocess is necessary:\n  - MUST use `exec.CommandContext` with an argument list (not `sh -c`).\n  - MUST NOT pass untrusted input to a shell (`bash -c`, `sh -c`, PowerShell).\n  - SHOULD use strict allowlists for any variable component (subcommand, flags, filenames).\n- MUST assume CLI tools may interpret attacker-controlled args as flags or special values.\n\nInsecure patterns:\n- `exec.Command(\"sh\", \"-c\", userString)`\n- `exec.Command(\"bash\", \"-c\", fmt.Sprintf(\"tool %s\", user))`\n- Calling the shell to get glob expansion for user-supplied globs.\n\nDetection hints:\n- Search for `os/exec`, `exec.Command(`, `CommandContext(`, `\"sh\"`, `\"bash\"`, `\"-c\"`.\n- Trace untrusted input into command name/args.\n\nFix:\n- Use library APIs instead of subprocesses.\n- Hardcode command and allowlist/validate args.\n- If a shell is unavoidable, escape robustly and treat as high risk (prefer avoiding).\n\nNotes:\n- The Go `os/exec` package intentionally does invoke a shell; introducing `sh -c` reintroduces shell injection hazards.\n\n---\n\n### GO-SSRF-001: Prevent SSRF in outbound HTTP requests\nSeverity: Medium (High in cloud/LAN environments)\n\n- Note: For small stand alone projects this is less important. It is most important when deploying into an LAN or with other services listening on the same server.\n\nRequired:\n- MUST treat outbound requests to user-provided URLs as high risk.\n- SHOULD allowlist hosts/domains for any user-influenced URL fetch.\n- SHOULD block access to localhost/private IP ranges/link-local addresses and cloud metadata endpoints.\n- MUST restrict schemes to `http`/`https` (no `file:`, `gopher:`, etc.).\n- MUST set client timeouts and restrict redirects.\n\nInsecure patterns:\n- `http.Get(r.URL.Query().Get(\"url\"))`\n- “URL preview” / “webhook test” endpoints that fetch arbitrary URLs.\n\nDetection hints:\n- Search for `http.Get`, `client.Do`, and URL values derived from requests/DB.\n- Identify features that fetch remote resources.\n\nFix:\n- Parse URLs strictly; enforce scheme and allowlisted hostnames.\n- Resolve DNS and enforce IP-range restrictions (with care for DNS rebinding).\n- Set timeouts, disable redirects unless needed, and cap response sizes.\n\n---\n\n### GO-HTTPCLIENT-001: Outbound HTTP clients MUST set timeouts and close bodies\nSeverity: High (DoS and resource exhaustion)\n\nRequired:\n- MUST set an overall timeout on `http.Client` usage (or equivalent per-request deadlines via context + transport timeouts).\n- MUST ensure `resp.Body.Close()` is called for all successful requests (typically `defer resp.Body.Close()` immediately after error check).\n- SHOULD limit response body reads (do not `io.ReadAll` unbounded responses).\n- SHOULD restrict redirects for security-sensitive fetches (SSRF, auth flows).\n\nInsecure patterns:\n- Using `http.DefaultClient` / `http.Get` for user-influenced destinations with no timeout policy.\n- Missing `defer resp.Body.Close()` leading to resource leaks.\n- `io.ReadAll(resp.Body)` with no limit.\n\nDetection hints:\n- Search for `http.Get(`, `http.Post(`, `client := &http.Client{}` without `Timeout`, `client.Do(` and missing closes.\n- Search for `io.ReadAll(resp.Body)`.\n\nFix:\n- Use a configured client with timeouts.\n- Always close response bodies.\n- Use bounded readers (`io.LimitReader`) for large/untrusted responses.\n\nNotes:\n- The net/http package exposes `DefaultClient` as a zero-valued `http.Client`, which can easily lead to “no timeout” behavior unless configured.\n\n---\n\n### GO-REDIRECT-001: Prevent open redirects\nSeverity: Medium (can be High with auth flows)\n\nRequired:\n- MUST validate redirect targets derived from untrusted input (`next`, `redirect`, `return_to`).\n- SHOULD prefer only same-site relative paths.\n- SHOULD fall back to a safe default on validation failure.\n\nInsecure patterns:\n- `http.Redirect(w, r, r.URL.Query().Get(\"next\"), http.StatusFound)` with no validation.\n\nDetection hints:\n- Search for `http.Redirect(` and check origin of the location.\n\nFix:\n- Allowlist internal paths or known domains.\n- Reject absolute URLs unless explicitly needed and allowlisted.\n\n---\n\n### GO-CRYPTO-001: Cryptographic randomness MUST come from crypto/rand\nSeverity: High (Critical if used for auth/session tokens or keys)\n\nRequired:\n- MUST use `crypto/rand` for:\n  - session IDs, password reset tokens, API keys, CSRF tokens, nonces\n  - encryption keys, signing keys, salts when required\n- MUST NOT use `math/rand` for any security-sensitive value.\n- SHOULD use built-in helpers that produce appropriately strong tokens when available.\n\nInsecure patterns:\n- `math/rand.Seed(time.Now().UnixNano())` followed by token generation for auth or sessions.\n- Using UUIDv4-like constructs built from `math/rand`.\n\nDetection hints:\n- Search for `math/rand`, `rand.Seed`, `rand.Intn` in code that touches auth/session/token flows.\n- Search for custom token generators.\n\nFix:\n- Switch to `crypto/rand` (`rand.Reader`, `rand.Read`, or secure token helpers).\n- Ensure sufficient entropy and use URL-safe encoding.\n\nNotes:\n- The crypto/rand package provides secure randomness APIs and token generation helpers.\n\n---\n\n### GO-AUTH-001: Password storage MUST use adaptive hashing (bcrypt/argon2id) and safe comparisons\nSeverity: High\n\nRequired:\n- MUST hash passwords using an adaptive password hashing function (bcrypt or argon2id).\n- MUST NOT store plaintext passwords or reversible encryption of passwords.\n- MUST compare secrets in constant time when relevant (tokens, MACs, API keys) to reduce timing leaks.\n- SHOULD ensure password policies do not exceed algorithm constraints (e.g., bcrypt has input length limits; handle long passphrases appropriately).\n\nInsecure patterns:\n- `sha256(password)` stored as password hash.\n- Plaintext password storage.\n- Comparing secrets with `==` in timing-sensitive contexts.\n\nDetection hints:\n- Search for `sha1`, `sha256`, `md5` used on passwords.\n- Search for `bcrypt`/`argon2` usage; if absent, suspect.\n- Search for `==` comparisons on tokens/API keys.\n\nFix:\n- Use `bcrypt.GenerateFromPassword` / `CompareHashAndPassword` or argon2id with recommended parameters.\n- Use constant-time compare helpers when comparing MACs/tokens.\n\nNotes:\n- Go provides bcrypt in `golang.org/x/crypto/bcrypt`, and constant-time comparisons in `crypto/subtle`.\n\n---\n\n### GO-CONC-001: Data races and concurrency hazards MUST be treated as security-relevant\nSeverity: Medium to High (depends on what races affect)\n\nRequired:\n- MUST run tests with the race detector (`go test -race`) in CI for security-sensitive services.\n- MUST fix detected races; do not suppress without deep justification.\n- SHOULD treat shared mutable state in handlers as high risk; enforce synchronization or avoid shared mutability.\n\nInsecure patterns:\n- Global maps/slices mutated from multiple goroutines without a mutex.\n- Caches or auth/session state stored in globals without concurrency protection.\n- Racy access to authorization state (can lead to bypasses or inconsistent enforcement).\n\nDetection hints:\n- Search for `var someMap = map[...]...` used in handlers.\n- Look for missing `sync.Mutex`, `sync.Map`, channels, or other synchronization.\n- Ensure CI includes `-race` and that it runs relevant tests.\n\nFix:\n- Add proper synchronization or redesign to avoid shared mutable state.\n- Add race tests and run them continuously.\n\nNotes:\n- The Go race detector only finds races that occur in executed code paths; improve test coverage and run realistic workloads with `-race` where feasible.\n\n---\n\n### GO-UNSAFE-001: Use of unsafe/cgo MUST be minimized and audited like memory-unsafe code\nSeverity: High (Critical in high-risk code paths)\n\nRequired:\n- SHOULD avoid importing `unsafe` in application code unless absolutely necessary.\n- If `unsafe` is used, MUST treat it as “manual memory safety” requiring careful review and test coverage.\n- If `cgo` is used, MUST treat the C/C++ boundary as memory-unsafe; apply secure coding practices on the C side and isolate where possible.\n\nInsecure patterns:\n- Widespread `unsafe.Pointer` casts in parsing, serialization, auth, or network code.\n- `cgo` used for parsing or security boundaries without sandboxing.\n\nDetection hints:\n- Search for `import \"unsafe\"`, `unsafe.Pointer`, `// #cgo`, `import \"C\"`.\n- Prioritize review where unsafe touches untrusted input.\n\nFix:\n- Replace unsafe/cgo usage with safe standard library alternatives where possible.\n- Isolate unsafe code in small, well-tested modules with fuzz/race tests.\n\nNotes:\n- The unsafe package explicitly provides operations that step around Go’s type safety guarantees.\n\n--------------------------------------------------------------------\n\n## 5) Practical scanning heuristics (how to “hunt”)\n\nWhen actively scanning, use these high-signal patterns:\n\nToolchain & dependencies:\n- `FROM golang:` (Dockerfiles), `go-version:` (CI), `toolchain go` (go.mod), pinned old versions\n- `GOSUMDB=off`, `GOINSECURE`, `GONOSUMDB`, `GOPROXY=direct`\n- `replace` directives in `go.mod` to forks/paths\n- `govulncheck` missing in CI\n\nHTTP server hardening:\n- `http.ListenAndServe(`, `ListenAndServeTLS(`, `&http.Server{` with missing timeouts\n- `ReadHeaderTimeout: 0`, `ReadTimeout: 0`, `WriteTimeout: 0`, `IdleTimeout: 0`, missing `MaxHeaderBytes`\n\nBody parsing / DoS:\n- `io.ReadAll(r.Body)`, `json.NewDecoder(r.Body)` without size cap\n- `ParseMultipartForm`, `FormFile`, `multipart.NewReader` without explicit limits\n- Missing `http.MaxBytesReader`\n\nDebug exposure:\n- `import _ \"net/http/pprof\"`\n- `/debug/pprof`, `/debug/vars`\n\nTemplates / XSS / SSTI:\n- `text/template` used for HTML output\n- `template.HTML(`, `template.JS(`, `template.URL(` with user-controlled data\n- `.Parse(` on user-controlled strings\n\nFiles:\n- `http.ServeFile(` with user path\n- `http.FileServer(http.Dir(` pointing at repo root or uploads\n- `os.Open(filepath.Join(base, user))` without containment checks\n\nInjection:\n- SQL building with `fmt.Sprintf`, string concatenation near `db.Query/Exec`\n- `exec.Command(\"sh\",\"-c\", ...)`, `exec.Command(\"bash\",\"-c\", ...)`\n\nSSRF / outbound HTTP:\n- `http.Get(userURL)`, `client.Do(req)` where URL comes from request/DB\n- Missing client timeout, missing `resp.Body.Close()`, unbounded `io.ReadAll(resp.Body)`\n\nCrypto:\n- `math/rand` in token/session generation\n- `InsecureSkipVerify: true`\n- Password hashing with `sha256`/`md5` instead of bcrypt/argon2\n\nConcurrency:\n- Shared maps/slices mutated from handlers without locks\n- CI lacking `go test -race`\n\nAlways try to confirm:\n- data origin (untrusted vs trusted)\n- sink type (template/SQL/subprocess/files/http)\n- protective controls present (limits, validation, allowlists, middleware, network controls)\n\n--------------------------------------------------------------------\n\n## 6) Sources (accessed 2026-01-28)\n\nPrimary Go documentation:\n- Go Security Policy — https://go.dev/doc/security/policy\n- Go Release History (security fixes in patch releases) — https://go.dev/doc/devel/release\n- Go 1.25 Release Notes — https://go.dev/doc/go1.25\n- net/http (server timeouts, MaxHeaderBytes, DefaultClient) — https://pkg.go.dev/net/http\n- html/template (auto-escaping and trusted-template assumptions) — https://pkg.go.dev/html/template\n- crypto/tls (MinVersion defaults, InsecureSkipVerify warnings) — https://pkg.go.dev/crypto/tls\n- crypto/rand (secure randomness, token helpers) — https://pkg.go.dev/crypto/rand\n- crypto/subtle (constant-time comparisons) — https://pkg.go.dev/crypto/subtle\n- os/exec (no shell by default; command execution guidance) — https://pkg.go.dev/os/exec\n- unsafe (bypasses type safety) — https://go.dev/src/unsafe/unsafe.go\n- net/http/pprof (debug endpoints) — https://pkg.go.dev/net/http/pprof\n- cmd/go (module authentication via go.sum/checksum DB; env vars like GOINSECURE) — https://pkg.go.dev/cmd/go\n- Module Mirror and Checksum Database Launched (Go blog) — https://go.dev/blog/module-mirror-launch\n- govulncheck documentation — https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck\n- Go Race Detector documentation — https://go.dev/doc/articles/race_detector\n- bcrypt (password hashing) — https://pkg.go.dev/golang.org/x/crypto/bcrypt\n- Go vulnerability entry example (multipart resource consumption) — https://pkg.go.dev/vuln/GO-2023-1569\n\nOWASP Cheat Sheet Series (general web security):\n- Session Management — https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html\n- CSRF Prevention — https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html\n- SSRF Prevention — https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html\n- XSS Prevention — https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html\n- HTTP Security Response Headers — https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html"
  },
  {
    "path": "skills/.curated/security-best-practices/references/javascript-express-web-server-security.md",
    "content": "# Express (Node.js) Web Security Spec (Express 5.x / 4.19.2+, Node.js LTS)\n\nThis document is designed as a **security spec** that supports:\n\n1. **Secure-by-default code generation** for new Express apps and routes.\n2. **Security review / vulnerability hunting** in existing Express code (passive “notice issues while working” and active “scan the repo and report findings”).\n\nIt is intentionally written as a set of **normative requirements** (“MUST/SHOULD/MAY”) plus **audit rules** (what bad patterns look like, how to detect them, and how to fix/mitigate them).\n\n---\n\n## 0) Safety, boundaries, and anti-abuse constraints (MUST FOLLOW)\n\n* MUST NOT request, output, log, or commit secrets (API keys, passwords, private keys, session secrets, cookies, tokens).\n* MUST NOT “fix” security by disabling protections (e.g., weakening cookie flags, disabling CSRF defenses for cookie-authenticated apps, enabling permissive CORS, trusting proxy headers from the open internet, turning on debugging/stack traces in production, disabling TLS without a replacement).\n* MUST provide **evidence-based findings** during audits: cite file paths, code snippets, middleware/config values, and runtime assumptions that justify the claim.\n* MUST treat uncertainty honestly: if a protection might exist in infrastructure (reverse proxy, gateway, WAF, CDN), report it as “not visible in app code; verify at runtime/config.”\n* MUST prefer vetted libraries and platform controls over “roll your own” crypto/auth/session/CSRF. Express explicitly expects the application to validate/handle user input correctly; it does not do this automatically. ([Express][1])\n\n---\n\n## 1) Operating modes\n\n### 1.1 Generation mode (default)\n\nWhen asked to write new Express code or modify existing code:\n\n* MUST follow every **MUST** requirement in this spec.\n* SHOULD follow every **SHOULD** requirement unless the user explicitly says otherwise.\n* MUST prefer safe-by-default APIs and proven libraries over custom security code.\n* MUST avoid introducing new risky sinks (shell execution, dynamic code evaluation, unsafe redirects, serving user files as HTML, template rendering from untrusted strings, unsafe filesystem paths, SSRF URL fetch endpoints, etc.).\n\n### 1.2 Passive review mode (always on while editing)\n\nWhile working anywhere in an Express repo (even if the user did not ask for a security scan):\n\n* MUST “notice” violations of this spec in touched/nearby code.\n* SHOULD mention issues as they come up, with a brief explanation + safe fix.\n\n### 1.3 Active audit mode (explicit scan request)\n\nWhen the user asks to “scan”, “audit”, or “hunt for vulns”:\n\n* MUST systematically search the codebase for violations of this spec.\n* MUST output findings in a structured format (see §2.3).\n\nRecommended audit order:\n\n1. Entrypoints (server/app bootstrap), deployment manifests, Dockerfiles, process manager config, CI/CD.\n2. Express settings + middleware stack order (helmet, parsers, auth, sessions, CSRF, CORS).\n3. Proxy trust (`trust proxy`) and IP/protocol/host handling. ([Express][2])\n4. Auth flows, sessions, cookies, password reset links, redirect handling. ([Express][1])\n5. State-changing routes + CSRF protections (cookie-authenticated apps). ([OWASP Cheat Sheet Series][3])\n6. Template rendering and XSS defenses (HTML generation, CSP, `res.locals`). ([OWASP Cheat Sheet Series][4])\n7. File handling (uploads + downloads + static files) and path traversal. ([Express][5])\n8. Injection classes (SQL, NoSQL, command execution, unsafe deserialization). ([OWASP Cheat Sheet Series][6])\n9. Outbound requests (SSRF) and webhook/callback delivery. ([OWASP Cheat Sheet Series][7])\n10. Rate limiting / brute-force defenses / abuse controls. ([Express][1])\n11. Dependency hygiene / lockfiles / npm audit / vulnerable Express versions. ([Express][1])\n\n---\n\n## 2) Definitions and review guidance\n\n### 2.1 Untrusted input (treat as attacker-controlled unless proven otherwise)\n\nIn Express, common untrusted inputs include:\n\n* `req.params` (route parameters)\n* `req.query` (query string parameters; can be strings/arrays/objects depending on parsing) ([OWASP Cheat Sheet Series][8])\n* `req.body` from `express.json()`, `express.urlencoded()`, `express.text()`, `express.raw()` ([Express][5])\n* `req.headers` / `req.get(...)`\n* `req.cookies` / `req.signedCookies` (if cookie parsing middleware is used)\n* Upload metadata and filenames (e.g., multer `file.originalname`, `file.mimetype`)\n* Any data from external systems (webhooks, third-party APIs, message queues)\n* Any persisted user content (DB rows) that originated from users\n\nSpecial proxy note:\n\n* If `trust proxy` is enabled, values like `req.ip`, `req.hostname`, and `req.protocol` may be derived from `X-Forwarded-*` headers which **can be attacker-controlled** if your proxy chain is not correctly overwriting/removing them. ([Express][2])\n\n### 2.2 State-changing request\n\nA request is state-changing if it can create/update/delete data, change auth/session state, trigger side effects (purchase, email send, webhook send), or initiate privileged actions.\n\n### 2.3 Required audit finding format\n\nFor each issue found, output:\n\n* Rule ID:\n* Severity: Critical / High / Medium / Low\n* Location: file path + function/route/middleware name + line(s)\n* Evidence: the exact code/config snippet\n* Impact: what could go wrong, who can exploit it\n* Fix: safe change (prefer minimal diff)\n* Mitigation: defense-in-depth if immediate fix is hard\n* False positive notes: what to verify if uncertain\n\n---\n\n## 3) Secure baseline: minimum production configuration (MUST in production)\n\nThis is the smallest “production baseline” that prevents common Express misconfigurations.\n\nMinimum baseline targets:\n\n* `helmet()` is used and configured (especially CSP where applicable), and fingerprinting is reduced (disable `x-powered-by`). ([Express][1])\n* A custom 404 handler and a custom error handler exist, and production does not leak internal stack traces. ([Express][1])\n* Cookie/session usage is deliberate:\n\n  * Not using default session cookie names\n  * Cookies use secure attributes (`Secure`, `HttpOnly`, `SameSite`) as appropriate\n  * Cookie-backed sessions never store secrets (they are readable by the client)\n  * Server-side sessions never use MemoryStore in production. ([Express][1])\n* Request body parsing has explicit limits (`express.json({ limit })`, `express.urlencoded({ limit, parameterLimit, depth })`). ([Express][5])\n* `trust proxy` is configured explicitly to match your proxy topology; not blindly `true`. ([Express][2])\n* Login/auth endpoints have brute-force protection and rate limiting. ([Express][1])\n* Dependencies are regularly audited/updated (`npm audit` + advisory response). ([Express][1])\n\n---\n\n## 4) Rules (generation + audit)\n\nEach rule contains: required practice, insecure patterns, detection hints, and remediation.\n\n### EXPRESS-INPUT-001: Treat all user input as untrusted and validate it\n\nSeverity: High\n\nRequired:\n\n* MUST validate and normalize untrusted inputs before using them in security-sensitive logic or dangerous sinks (DB queries, redirects, filesystem, HTML output, shell commands). Ensure the untrusted inputs are type checked and structure checked before using or passing forward.\n* SHOULD apply allowlists (known-good) rather than blocklists when feasible.\n* MUST reject or safely handle unexpected types/shapes in `req.query`, `req.params`, and `req.body`.\n\nInsecure patterns:\n\n* Passing `req.query`, `req.params`, `req.body` directly into database/query builders, redirects, filesystem paths, or templates.\n* Assuming `req.query.foo` is always a string (it can be an array/object depending on parsing). ([OWASP Cheat Sheet Series][8])\n\nDetection hints:\n\n* Identify “untrusted-to-sink” flows: request → sink (`res.redirect`, SQL execution, `sendFile`, `child_process`, template render, outbound fetch).\n* Search for direct usage of `req.query.*`, `req.body.*`, `req.params.*` in sensitive calls.\n\nFix:\n\n* Add schema validation (e.g., zod/joi/express-validator) at route boundaries.\n* Normalize types (e.g., force IDs to integers; reject arrays when scalar expected).\n\nNotes:\n\n* Express production security guidance explicitly says input validation/handling is the application’s responsibility. ([Express][1])\n\n---\n\n### EXPRESS-REDIRECT-001: Prevent open redirects; validate redirect targets\n\nSeverity: Medium\n\nRequired:\n\n* MUST validate redirect destinations derived from untrusted input (`next`, `return_to`, `url`).\n* SHOULD allowlist only same-site relative paths (preferred) or a strict allowlist of domains.\n* MUST fall back to a safe default when validation fails.\n\nInsecure patterns:\n\n* `res.redirect(req.query.next)` with no validation.\n* `res.redirect(req.body.url)` or `res.location(...)` using untrusted URLs.\n\nDetection hints:\n\n* Search for `res.redirect(` and `res.location(` and trace the source of the target.\n* Look for query params named `next`, `redirect`, `return`, `url`.\n\nFix:\n\n* Only allow relative paths (starting with `/`) and disallow `//`, backslashes, and encoded variants.\n* If cross-domain redirects are required, allowlist exact hosts and enforce `https`.\n\nNotes:\n\n* Express documentation calls out open redirects as dangerous user input and shows validating the host before redirecting. ([Express][1])\n* Keep Express updated: Express has had an open-redirect-related CVE affecting some versions, and upgrades are part of the mitigation posture. ([NVD][9])\n\n---\n\n### EXPRESS-HEADERS-001: Use Helmet (or equivalent) to set essential security headers\n\nSeverity: Medium\n\nRequired:\n\n* SHOULD use `helmet()` to set common security headers.\n* SHOULD configure CSP realistically (avoid `unsafe-inline` where possible) for pages that render user-influenced content.\n* SHOULD set `X-Content-Type-Options: nosniff`, clickjacking defenses (`X-Frame-Options` or CSP `frame-ancestors`), and appropriate referrer policy.\n\nNOTE: It is most important to set the CSP's script-src. All other directives are not as important and can generally be excluded for the ease of development.\n\nInsecure patterns:\n\n* No security headers set in app code and no evidence they are set at the edge.\n* CSP missing on apps that display user content.\n* Misconfigured framing headers that unintentionally allow clickjacking.\n\nDetection hints:\n\n* Search for `helmet(` usage; check if CSP is configured or disabled.\n* Search for `res.setHeader(` / `res.set(` for security header setting.\n* If not visible in app code, check nginx/CDN config; otherwise flag “verify at edge.”\n\nFix:\n\n* Add `helmet()` early in middleware order and configure:\n\n  * CSP (`contentSecurityPolicy`)\n  * Frame protections (`frameguard` or CSP `frame-ancestors`)\n  * `X-Content-Type-Options` (`noSniff`)\n\nNotes:\n\n* Express production security best practices recommend Helmet and list headers Helmet sets by default. ([Express][1])\n* OWASP HTTP Headers guidance is a useful reference when tuning policies. ([OWASP Cheat Sheet Series][10])\n\n---\n\n### EXPRESS-FINGERPRINT-001: Reduce fingerprinting by disabling `x-powered-by` and customizing error/404 responses\n\nSeverity: Low (defense-in-depth)\n\nRequired:\n\n* SHOULD disable `X-Powered-By` using `app.disable('x-powered-by')`.\n* SHOULD provide a custom 404 handler and a custom error handler to avoid distinct default responses and to control information leakage.\n\nInsecure patterns:\n\n* Default `X-Powered-By: Express` header left enabled.\n* Default Express 404/error responses in production with identifiable formatting and/or stack traces.\n\nDetection hints:\n\n* Search for `app.disable('x-powered-by')`.\n* Check middleware tail for a custom 404 (`app.use((req,res)=>...)`) and a custom error handler (`app.use((err,req,res,next)=>...)`).\n* Check if `NODE_ENV` is correctly set for production behavior (see EXPRESS-ERROR-001). ([Express][11])\n\nFix:\n\n* Add:\n\n  * `app.disable('x-powered-by')`\n  * A custom 404 handler\n  * A custom error handler that logs server-side and returns generic messages client-side\n\nNotes:\n\n* Express docs explicitly recommend disabling `x-powered-by` and adding your own not-found and error handlers. ([Express][1])\n\n---\n\n### EXPRESS-COOKIE-001: Cookies must use secure attributes and minimal scope\n\nSeverity: Medium\n\nRequired:\n\n* MUST set cookie flags appropriately for any authentication/session cookie:\n\n  * `Secure` when HTTPS (production) IMPORTANT NOTE: Only set `Secure` in production environment if TLS is configured. When running in a local dev environment over HTTP, do not set `Secure` property on cookies. You should do this conditionally based on if the app is running in production mode. You should also include a property like `SESSION_COOKIE_SECURE` which can be used to disable `Secure` cookies when testing over HTTP.\n  * `HttpOnly` for auth/session cookies\n  * `SameSite` set deliberately (`Lax` is a common baseline; `Strict` if compatible; `None` only with `Secure` and a justified cross-site need)\n* SHOULD avoid setting `domain` broadly (avoid “all subdomains” unless required).\n* SHOULD set bounded expiry appropriate to risk and UX.\n\nInsecure patterns:\n\n* Session/auth cookies without `HttpOnly`.\n* Cookies without `Secure` in production HTTPS.\n* `SameSite=None` + cookie-authenticated state-changing endpoints without CSRF protections.\n\nDetection hints:\n\n* Search for `res.cookie(`, `Set-Cookie`, `cookie: { ... }`, `express-session`, `cookie-session`.\n* Verify cookie flags in session middleware configuration.\n\nFix:\n\n* Set these attributes centrally in session/cookie middleware configuration.\n\nNotes:\n\n* Express production security guidance lists cookie security options (`secure`, `httpOnly`, etc.). ([Express][1])\n* `res.cookie()` ultimately sets `Set-Cookie` with options; defaults follow RFC 6265 behavior when options are omitted. ([Express][5])\n* OWASP session management guidance is relevant for choosing flags and lifetimes. ([OWASP Cheat Sheet Series][12])\n\n---\n\n### EXPRESS-SESS-001: Do not use the default session cookie name; avoid session fingerprinting\n\nSeverity: Low (defense-in-depth)\n\nRequired:\n\n* SHOULD override the default session cookie name (e.g., do not keep `connect.sid` when using `express-session`).\n* SHOULD use a generic name (e.g., `sessionId`) unless you have a compatibility reason.\n\nInsecure patterns:\n\n* `express-session` used with no `name:` configured (default cookie name).\n* Multiple apps on the same domain sharing a cookie name accidentally.\n\nDetection hints:\n\n* Search for `express-session` config blocks; check for `name:`.\n\nFix:\n\n* Set `name: 'sessionId'` (or similar) in `express-session` options.\n\nNotes:\n\n* Express docs explicitly recommend not using the default session cookie name to reduce fingerprinting. ([Express][1])\n\n---\n\n### EXPRESS-SESS-002: Session storage and lifecycle must be production-safe\n\nSeverity: High\n\nRequired:\n\n* MUST NOT use `MemoryStore` in production (it is not designed for production use).\n* MUST store session secrets outside source control and rotate them safely.\n* SHOULD regenerate sessions on login / privilege changes to reduce session fixation risk.\n* MUST NOT store sensitive secrets in client-readable cookie sessions.\n\nInsecure patterns:\n\n* `app.use(session({ store: new MemoryStore(), ... }))` or missing store (defaults to MemoryStore).\n* Hard-coded for example: `secret: 'keyboard cat'` / `secret: 's3Cur3'` in repo.\n* Using `cookie-session` to store access tokens, refresh tokens, or PII.\n\nDetection hints:\n\n* Search for `express-session` and look for `MemoryStore` usage or missing `store`.\n* Search for `secret:` in session config and check if it’s hard-coded.\n* Look for `req.session = ...` patterns and whether sensitive data is stored.\n\nFix:\n\n* Use a production session store (Redis, database-backed, etc.).\n* Load secrets from environment/secret manager.\n* On login: `req.session.regenerate(...)` or equivalent flow with safe privilege re-binding.\n\nNotes:\n\n* `express-session` explicitly warns that `MemoryStore` is not designed for production. ([Express][1])\n* `express-session` documents rotating secrets and session regeneration to guard against fixation. ([Express][1])\n* Express notes that cookie-backed sessions serialize data into the cookie and that cookie data is visible to the client; keep it small and non-secret. ([Express][1])\n\n---\n\n### EXPRESS-CSRF-001: Cookie-authenticated state-changing requests MUST be CSRF-protected\n\nSeverity: High\n\n- IMPORTANT NOTE: If cookies are not being used for auth (ie auth is via Authentication header or other passed token), then there is no CSRF risk.\n\nRequired:\n\n* MUST protect all state-changing endpoints (POST/PUT/PATCH/DELETE) that rely on cookies for authentication.\n* SHOULD use a well-understood CSRF mitigation (token-based is the typical baseline).\n* MAY add defense-in-depth: Origin/Referer validation, Fetch Metadata enforcement, SameSite cookies, custom header requirements for XHR/fetch—**but do not treat these as a full replacement** unless explicitly designed and justified.\n* MUST use at a minimum require a custom HTTP header if form based CRSF tokens are not practical, as this is the second strongest method.\n\nIMPORTANT NOTE:\n\n* If authentication is done via `Authorization: Bearer ...` headers (and not cookies), classic browser CSRF is typically not applicable; \n\nInsecure patterns:\n\n* Cookie-authenticated endpoints that change state with no CSRF protection.\n* Using GET for state-changing actions (amplifies CSRF risk).\n* “CSRF protection” that only checks a user-controlled field.\n\nDetection hints:\n\n* Enumerate routes with methods other than GET/HEAD and identify whether cookies gate auth.\n* Look for presence/absence of CSRF middleware and token checks.\n* Check JSON APIs too, not only HTML forms.\n\nFix:\n\n* Implement CSRF tokens for cookie-authenticated flows.\n* Add Origin/Referer checks where feasible, and ensure SameSite is set appropriately.\n\nNotes:\n\n* OWASP CSRF guidance and OWASP Node.js guidance both recommend anti-CSRF tokens as a standard control for web apps. ([OWASP Cheat Sheet Series][3])\n\n---\n\n### EXPRESS-CORS-001: CORS must be explicit and least-privilege\n\nSeverity: Medium (High if misconfigured with credentials)\n\nRequired:\n\n* If CORS is not needed, MUST keep it disabled.\n* If CORS is needed:\n\n  * MUST allowlist trusted origins (do not reflect arbitrary `Origin` without validation).\n  * MUST NOT combine broad origins with credentialed cookies (`Access-Control-Allow-Credentials: true`).\n  * SHOULD restrict methods, headers, and exposed headers to what’s required.\n\nInsecure patterns:\n\n* `Access-Control-Allow-Origin: *` with `Access-Control-Allow-Credentials: true`.\n* Reflecting `Origin` for all requests without allowlist validation.\n* Applying permissive CORS middleware globally when only a subset needs cross-origin access.\n\nDetection hints:\n\n* Search for `cors(`, `Access-Control-Allow-Origin`, `Access-Control-Allow-Credentials`.\n* Inspect whether cookies are used for auth on endpoints exposed cross-origin.\n\nFix:\n\n* Implement strict origin allowlist and ensure credentialed requests only for intended origins.\n* Consider splitting CORS config per route group rather than global.\n\nNotes:\n\n* OWASP HTTP header guidance covers security implications of response headers, including those that affect browser behavior; use it as a reference when reviewing header posture. ([OWASP Cheat Sheet Series][10])\n\n---\n\n### EXPRESS-PROXY-001: Reverse proxy trust (`trust proxy`) must be configured correctly\n\nSeverity: Medium (High if using IP based authentication)\n\nRequired:\n\n* If behind a reverse proxy/LB, MUST configure `app.set('trust proxy', ...)` to match the real proxy chain.\n* MUST NOT blindly set `trust proxy = true` unless you fully control the proxy behavior and header rewriting.\n* MUST ensure the last trusted proxy overwrites/removes `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto` so clients cannot spoof them.\n\nInsecure patterns:\n\n* `app.set('trust proxy', true)` in an app directly exposed to the internet or behind unknown proxies.\n* Using `req.ip`, `req.protocol`, `req.hostname` for security decisions without correct proxy trust configuration.\n* Rate limiting keyed by `req.ip` with spoofable forwarded headers.\n\nDetection hints:\n\n* Search for `app.set('trust proxy'`.\n* Check infra docs (nginx/LB) for header rewriting behavior.\n* Identify any security logic using `req.ip`, `req.ips`, `req.protocol`, `req.hostname`.\n\nFix:\n\n* Set `trust proxy` to a hop count, explicit IP/subnet list, or a custom function matching your network.\n* Ensure proxies overwrite forwarded headers.\n\nNotes:\n\n* Express explicitly warns that when `trust proxy` is `true`, the client IP is derived from `X-Forwarded-For`, and if proxies don’t overwrite forwarded headers, the client can provide any value. It also describes that enabling trust proxy impacts `req.hostname` and `req.protocol` derived from forwarded headers. ([Express][2])\n\n---\n\n### EXPRESS-BODY-001: Request body size and parsing limits MUST be set appropriately\n\nSeverity: Low\n\nRequired:\n\n* SHOULD set explicit body size limits for:\n\n  * `express.json({ limit })`\n  * `express.urlencoded({ limit, parameterLimit, depth })`\n* SHOULD only enable the parsers you need; do not parse large bodies by default for all routes.\n* SHOULD enforce additional limits at the reverse proxy / gateway level.\n\nInsecure patterns:\n\n* No explicit body limits (accepting arbitrarily large JSON/urlencoded).\n* Global parsers applied to all routes when only some need bodies.\n* `parameterLimit` very high without justification (DoS potential).\n\nDetection hints:\n\n* Search for `express.json(` and confirm `limit` is set (or consciously accepted).\n* Search for `express.urlencoded(` and inspect `limit`, `parameterLimit`, and `depth`.\n* Review upload/webhook endpoints for special parsing needs.\n\nFix:\n\n* Configure parsers with conservative defaults and override per route group when needed.\n\nNotes:\n\n* Express documents `express.json` options (including `limit`, defaulting to 100kb) and explicitly notes `req.body` is untrusted and should be validated. ([Express][5])\n* Express documents `express.urlencoded` options including `limit`, `parameterLimit`, and `depth`. ([Express][5])\n* OWASP Node.js guidance also recommends setting request size limits. ([OWASP Cheat Sheet Series][8])\n\n---\n\n### EXPRESS-INPUT-002: Prevent HTTP Parameter Pollution and type confusion in `req.query`\n\nSeverity: Medium\n\nRequired:\n\n* MUST treat `req.query` values as potentially multi-valued (array/object), depending on query parsing.\n* SHOULD reject ambiguous multi-valued parameters for security-sensitive fields (e.g., `role`, `isAdmin`, `redirect`, `amount`, `userId`).\n* SHOULD consider explicit parsing or dedicated middleware if parameter pollution is a concern.\n\nInsecure patterns:\n\n* `if (req.query.admin) { ... }` without type checks (arrays/objects may coerce truthy).\n* Passing `req.query` directly into ORM/NoSQL query objects.\n\nDetection hints:\n\n* Search for security-sensitive comparisons on `req.query.*` without type enforcement.\n* Look for code that assumes query params are strings.\n\nFix:\n\n* Validate shape: enforce string-only for certain params and reject arrays/objects.\n* Normalize query parsing settings (simple vs extended) where applicable and documented.\n\nNotes:\n\n* OWASP Node.js cheat sheet explicitly highlights that Express query parsing can produce strings, arrays, or objects and recommends preventing HTTP Parameter Pollution. ([OWASP Cheat Sheet Series][8])\n\n---\n\n### EXPRESS-XSS-001: Prevent reflected/stored XSS in HTML responses and templating\n\nSeverity: High\n\nRequired:\n\n* MUST escape untrusted content in HTML output (templates should auto-escape by default; do not bypass).\n* MUST NOT inject untrusted strings into HTML without escaping/sanitization.\n* SHOULD set CSP (via Helmet) for apps rendering user-controlled content.\n* SHOULD keep `res.locals` free of user-controlled input intended for templates unless it is validated/escaped.\n\nInsecure patterns:\n\n* `res.send(\"<div>\" + req.query.q + \"</div>\")`\n* Passing untrusted HTML through “safe” template flags/filters.\n* Writing untrusted strings into `res.locals` and then rendering without escaping.\n\nDetection hints:\n\n* Search for `res.send(` with strings containing user input.\n* Search for template “safe” flags (engine-specific) and trace data origin.\n* Search for assignments to `res.locals` and whether they might contain untrusted data.\n\nFix:\n\n* Use a template engine with autoescaping; pass only validated data.\n* For rich text that must contain HTML, use a trusted sanitizer and an allowlist policy.\n* Add CSP with realistic directives.\n\nNotes:\n\n* Express API docs explicitly warn that `res.locals` “should not contain user-controlled input” and is often used to expose things like CSRF tokens to templates. ([Express][5])\n* OWASP XSS prevention guidance provides standard output-encoding and policy recommendations. ([OWASP Cheat Sheet Series][4])\n* Helmet can mitigate some XSS classes via headers such as CSP. ([Express][1])\n\n---\n\n### EXPRESS-TEMPLATE-001: Never render untrusted templates or template paths (SSTI / LFI risk)\n\nSeverity: Critical (if you can prove template strings/paths are user/attacker-controlled)\n\nRequired:\n\n* MUST NOT render templates whose contents or template path/name is influenced by untrusted input.\n* MUST NOT load templates from user-controlled filesystem locations.\n* SHOULD treat “email template editors”, “theme engines”, and “CMS-like template storage” as high-risk designs requiring sandboxing and isolation.\n\nInsecure patterns:\n\n* `res.render(req.query.view, data)` where `view` is not allowlisted.\n* Rendering a template from a string that includes user input (engine-specific).\n* Loading templates from uploads directories.\n\nDetection hints:\n\n* Search for `res.render(` where the first argument is derived from request/DB without allowlist.\n* Search for template compilation APIs (engine-specific) fed by user content.\n\nFix:\n\n* Use allowlisted template names and a fixed templates directory.\n* If user-defined templates are required, implement strict sandboxing and isolate execution.\n\nNotes:\n\n* Express’s template system depends on the chosen engine; assume unsafe if user input influences template selection or source.\n\n---\n\n### EXPRESS-FILES-001: Prevent path traversal and unsafe file serving (sendFile/download)\n\nSeverity: High\n\nRequired:\n\n* MUST NOT pass user-controlled filesystem paths directly to `res.sendFile()` / `res.download()` / filesystem APIs.\n* SHOULD use `res.sendFile` with a fixed `root` and strict options (e.g., deny dotfiles) when serving user-selected files from a directory.\n* MUST enforce authorization checks before serving user-specific files.\n\nInsecure patterns:\n\n* `res.sendFile(req.query.path)` or `res.download(req.params.file)` with no root restriction.\n* File-serving routes that accept `..` segments, encoded traversal, or absolute paths.\n\nDetection hints:\n\n* Search for `res.sendFile(` and trace the `path` argument origin.\n* Search for `res.download(` and trace the `path` argument origin.\n* Look for `fs.readFile`/`createReadStream` on paths derived from requests.\n\nFix:\n\n* Use an identifier-to-path mapping stored server-side (DB), not raw paths from clients.\n* Use `root: <trusted_base_dir>` and `dotfiles: 'deny'` where appropriate; validate the filename component strictly.\n\nNotes:\n\n* Express’s `res.sendFile` docs show using a `root` option and `dotfiles: 'deny'` as part of a safe serving configuration. ([Express][5])\n* `res.download` transfers the file as an attachment, but you still must control/validate the underlying `path`. ([Express][5])\n\n---\n\n### EXPRESS-STATIC-001: Harden `express.static` / serve-static and never serve untrusted uploads as active content\n\nSeverity: Medium (if serving untrusted user files if there are not robust limits tot eh file extensions)\n\nRequired:\n\n* MUST NOT serve user uploads from a public static directory as active content (especially HTML/JS/SVG) unless explicitly intended and sandboxed. If sure that the content is inactive (png, jpg, other images etc) then it may be safe. It may be good to validate image file extensions are allow-listed before serving them.\n* SHOULD configure static serving to:\n\n  * deny/ignore dotfiles\n  * avoid unintended directory indexes if not needed\n  * apply appropriate cache controls for immutable assets\n\nInsecure patterns:\n\n* `app.use(express.static('uploads'))` where users can upload arbitrary files.\n* Serving uploaded HTML or SVG inline from the same origin as the app.\n\nDetection hints:\n\n* Search for `express.static(` and identify served directories.\n* Compare served directories with upload storage locations.\n* Check for `dotfiles` and `index` options in static middleware.\n\nFix:\n\n* Store uploads outside any static web root and serve via controlled routes that set safe `Content-Type` and `Content-Disposition: attachment` when appropriate.\n* Configure `express.static(root, { dotfiles: 'deny'|'ignore', index: false (if desired) })`.\n\nNotes:\n\n* Express documents `express.static` options, including `dotfiles` behavior and `index`. ([Express][5])\n\n---\n\n### EXPRESS-UPLOAD-001: File uploads must be validated, stored safely, and served safely\n\nSeverity: Low - Medium\n\nRequired:\n\n* SHOULD enforce upload size limits (app + edge).\n* MUST validate file type using allowlists and content checks (not only filename extension).\n* MUST store uploads outside executable/static roots when possible.\n* SHOULD generate server-side filenames (random IDs); do not trust original names.\n* MUST serve potentially active formats safely (download attachment) unless explicitly intended.\n\nInsecure patterns:\n\n* Accepting arbitrary file types and serving them back inline.\n* Using `file.originalname` as the storage path.\n* Missing size/type validation.\n\nDetection hints:\n\n* Look for multer/busboy/formidable usage and check for `limits`.\n* Check where uploaded files are written and how they are served.\n* Check whether uploads end up under `public/` or any `express.static` root.\n\nFix:\n\n* Implement allowlist validation + safe storage + safe serving, per OWASP upload guidance.\n\nNotes:\n\n* OWASP File Upload guidance covers allowlists, content validation, storage, and safe serving patterns. ([OWASP Cheat Sheet Series][13])\n\n---\n\n### EXPRESS-INJECT-001: Prevent SQL injection (use parameterized queries / ORM)\n\nSeverity: High\n\nRequired:\n\n* MUST use parameterized queries or an ORM/query builder that parameterizes under the hood.\n* MUST NOT build SQL via string concatenation/template literals with untrusted input.\n\nInsecure patterns:\n\n* ``db.query(`SELECT * FROM users WHERE id = ${req.query.id}`)``\n* `\"SELECT ... WHERE name = '\" + req.body.name + \"'\"`\n\nDetection hints:\n\n* Grep for `SELECT`, `INSERT`, `UPDATE`, `DELETE` strings in JS/TS.\n* Trace untrusted input into `.query(...)`, `.execute(...)`, or raw SQL APIs.\n\nFix:\n\n* Replace with parameterized queries (placeholders) or ORM query APIs.\n* Validate types (e.g., integer IDs) before querying.\n\nNotes:\n\n* OWASP SQL injection prevention guidance strongly favors parameterized queries. ([OWASP Cheat Sheet Series][6])\n\n---\n\n### EXPRESS-INJECT-002: Prevent NoSQL injection / operator injection (Mongo-style)\n\nSeverity: High (app-dependent)\n\nRequired:\n\n* MUST validate types and schemas for any query object built from untrusted input.\n* MUST prevent operator injection (e.g., `$ne`, `$gt`, `$where`) if user input is merged into query objects.\n* SHOULD consider defensive libraries/middleware when appropriate.\n\nInsecure patterns:\n\n* `collection.find(req.body)` where the body is attacker-controlled.\n* Merging `req.query`/`req.body` into Mongo queries without schema validation.\n\nDetection hints:\n\n* Search for `find(`, `findOne(`, `aggregate(` calls where argument is request-derived.\n* Check for patterns like `{ ...req.query }` or `Object.assign(query, req.body)`.\n\nFix:\n\n* Use schema validation at boundary; explicitly construct query objects from validated fields only.\n\nNotes:\n\n* OWASP Node.js cheat sheet discusses input validation and mentions Node ecosystem modules commonly used for sanitization in NoSQL contexts. ([OWASP Cheat Sheet Series][8])\n\n---\n\n### EXPRESS-CMD-001: Prevent OS command injection (child_process)\n\nSeverity: Critical to High (depends on exposure), please prove it is user/attacker controlled\n\nRequired:\n\n* MUST avoid executing shell commands with untrusted input.\n* If subprocess is necessary:\n\n  * MUST avoid `exec()` / `execSync()` with attacker-influenced strings\n  * MUST NOT use `shell: true` with attacker-influenced data\n  * SHOULD use `spawn()` with an argument array and strict allowlists. Ensure the executable is hardcoded or allow-listed, do not use a user supplied command name.\n  * SHOULD place user-controlled values after `--` when supported by the subcommand to avoid flag injection\n\nInsecure patterns:\n\n* `exec(req.query.cmd)`\n* `exec(`convert ${userPath} ...`)`\n* `spawn('sh', ['-c', userString])`\n* `spawn(userString, ['foo'])`\n\nDetection hints:\n\n* Search for `child_process`, `exec(`, `execSync(`, `spawn(`, `fork(`.\n* Trace request/DB data into command construction.\n\nFix:\n\n* If possible, write the functionality in javascript or use a library instead of subprocess.\n* If unavoidable, hard-code command and strictly allowlist parameters.\n\nNotes:\n\n* OWASP OS command injection defense guidance covers avoid-shell and allowlist patterns. ([OWASP Cheat Sheet Series][14])\n\n---\n\n### EXPRESS-SSRF-001: Prevent server-side request forgery (SSRF) in outbound HTTP\n\nSeverity: Medium (High in cloud/LAN deployments)\n\nNOTE: This is mostly only applicable to apps which will be deployed in a cloud/LAN setup or have other http services on the same box. Sometimes the feature requires this functionality unavoidably (webhooks).\n\nRequired:\n\n* MUST treat outbound requests to user-provided URLs as high risk if there are other reachable private http endpoints.\n* SHOULD validate and restrict destinations (allowlist hosts/domains) for any user-influenced URL fetch.\n* SHOULD block access to:\n\n  * localhost / private IP ranges / link-local\n  * cloud metadata endpoints\n* MUST allow only `http`/`https` for URL fetch features (to avoid schemas such as `file:`,`javascript:`)\n* SHOULD set timeouts and restrict redirects.\n\nInsecure patterns:\n\n* `fetch(req.query.url)`\n* “URL preview” / “import from URL” endpoints that accept arbitrary URLs.\n\nDetection hints:\n\n* Search for `fetch(`, `axios(`, `got(`, `request(`, `node-fetch` usage where URL originates from users/DB.\n* Review webhook testers, previewers, image fetchers.\n\nFix:\n\n* Enforce scheme allowlist, host allowlist, DNS/IP resolution checks, timeouts, and redirect policy.\n* Consider network egress controls at infrastructure level.\n\nNotes:\n\n* OWASP SSRF prevention guidance provides standard controls and common pitfalls. ([OWASP Cheat Sheet Series][7])\n\n---\n\n### EXPRESS-ERROR-001: Error handling MUST not leak sensitive details in production\n\nSeverity: Low\n\nRequired:\n\n* SHOULD define a centralized error handler (`app.use((err, req, res, next) => ...)`) at the end of middleware.\n* MUST avoid returning stack traces, internal error messages, or secrets to clients in production.\n* SHOULD log errors server-side with appropriate redaction.\n* SHOULD ensure the app runs with production settings so default behavior doesn’t leak details.\n* MUST avoid logging or returning sensitive information such as secrets, env vars, sessions, cookies in error messages in production.\n\nInsecure patterns:\n\n* Returning `err.stack` to clients.\n* Using dev-only error middleware in production.\n* `NODE_ENV` left as development, causing verbose error responses.\n\nDetection hints:\n\n* Verify there is a final error-handling middleware.\n* Search for `res.status(500).send(err)` or similar.\n* Check production environment variables and startup scripts.\n\nFix:\n\n* Add a production-safe error handler that returns generic messages and logs details internally.\n* Ensure environment is configured for production behavior.\n\nNotes:\n\n* Express production security guidance recommends custom error handling. ([Express][1])\n* Express error handling docs describe the default error handler behavior and how production mode affects what is exposed. ([Express][11])\n\n---\n\n### EXPRESS-AUTH-001: Prevent brute-force attacks against authorization endpoints\n\nSeverity: Medium\n\nNOTE: This is highly application specific and while it is good to bring to the attention of the user, it is hard to fix without additional complex configurations. Prefer to inform the user and if they request you to help implement a solution, help walk them through possible solutions.\n\nRequired:\n\n* SHOULD protect login/auth endpoints against brute forcing.\n* SHOULD rate-limit by:\n\n  1. consecutive failed attempts per username+IP\n  2. failed attempts per IP over a time window\n\nInsecure patterns:\n\n* Unlimited login attempts.\n\nDetection hints:\n\n* Identify all auth endpoints and check for rate limiting/throttling.\n* Search for `rate-limiter-flexible`, `express-rate-limit`, or gateway policies.\n\nFix:\n\n* Implement rate-limiting/throttling (app or edge). Express docs point to `rate-limiter-flexible` as a tool for this approach. ([Express][1])\n\nNotes:\n\n* OWASP Node.js cheat sheet also recommends precautions against brute forcing. ([OWASP Cheat Sheet Series][8])\n\n---\n\n### EXPRESS-DEPS-001: Dependency and patch hygiene (Express + Node + critical middleware)\n\nSeverity: Medium / Low\n\nNOTE: `npm audit` often returns a large number of insignificant \"vulnerabilities\" which do not actually matter. You should only focus on Express or other extremely critical packages, ignoring ones listed in dev tools, bundlers, etc.\n\nDo not upgrade packages without concent from the user. This may break existing code in unexpected ways. Instead, inform them of the outdated packages.\n\nRequired:\n\n* MUST keep Express on a maintained version line (avoid EOL major versions).\n* MAY use `npm audit` in CI and during maintenance work.\n* SHOULD pin dependencies via lockfiles and review major updates carefully.\n\nInsecure patterns:\n\n* Running EOL Express versions (e.g., very old major lines).\n* Ignoring `npm audit` findings without triage.\n* Unpinned dependency ranges that auto-upgrade into insecure versions.\n\nDetection hints:\n\n* Check `package.json` and lockfiles for `express` version and other critical middleware versions.\n* Inspect CI pipelines for `npm audit`/SCA steps.\n\nFix:\n\n* Upgrade to latest stable Express and apply patches.\n* Add automated dependency scanning and upgrade process.\n\nNotes:\n\n* Express production security guidance emphasizes that dependency vulnerabilities can compromise the app, and recommends `npm audit`. ([Express][1])\n* Track security issues affecting Express versions (including known open-redirect-related CVEs). ([NVD][9])\n\n---\n\n### EXPRESS-DOS-001: Configure DoS protections (timeouts, limits, reverse proxy)\n\nSeverity: Low\n\nNOTE: It may be hard to tell from the provided application context if the application runs behind a reverse proxy. You can inform the user or recommend one, but do not attempt to configure one without them initiating it. This is highly deployment dependant.\n\nRequired:\n\n* SHOULD use a reverse proxy to provide caching, load balancing, and filtering controls when feasible.\n* MAY configure server/proxy timeouts and connection limits to reduce exposure to Slowloris and similar DoS patterns.\n* MUST ensure server/socket errors are handled so malformed connections do not crash the process. (Express should handle exceptions, but there are edgecases)\n\nInsecure patterns:\n\n* No reverse proxy in front of a public Node server, with defaults everywhere.\n* Missing error handlers on server/socket objects.\n* Extremely permissive timeouts and unlimited body sizes.\n\nDetection hints:\n\n* Inspect server creation (`http.createServer`, `https.createServer`) and whether timeouts are set.\n* Check proxy/gateway config for timeouts and max body size.\n\nFix:\n\n* Explain how to configure reverse proxy and timeouts, set request size limits\n* add robust error handling middleware\n\nNotes:\n\n* Node’s security guidance for HTTP DoS discusses using reverse proxies and correctly configuring server timeouts. ([Node.js][15])\n\n---\n\n### EXPRESS-NODE-INSPECT-001: Do not expose the Node inspector in production\n\nSeverity: Critical\n\nNOTE: Ensure that this detection is actually in the production path, and not just being used for local debugging.\n\nRequired:\n\n* MUST NOT run Node with `--inspect` (especially bound to non-loopback) in production.\n* MUST ensure `NODE_OPTIONS` or startup scripts do not enable inspector in prod.\n* SHOULD firewall/debug locally only.\n\nInsecure patterns:\n\n* `node --inspect=0.0.0.0:9229 app.js` in production.\n* Container/PM2/systemd configs enabling inspector.\n\nDetection hints:\n\n* Search for `--inspect` in Dockerfiles, Procfiles, systemd units, PM2 configs, npm scripts.\n* Check `NODE_OPTIONS`.\n\nFix:\n\n* Remove inspector flags from production start commands; restrict to local dev.\n\nNotes:\n\n* Node security guidance discusses inspector exposure risks (e.g., DNS rebinding) and recommends not running inspector in production. ([Node.js][15])\n\n---\n\n### EXPRESS-NODE-HTTP-001: Do not enable insecure HTTP parsing in production\n\nSeverity: High\n\nNOTE: Ensure that this detection is actually in the production path, and not just being used for local dev.\n\nRequired:\n\n* MUST NOT use Node’s `insecureHTTPParser` in production.\n* MAY suggest configuring front-end proxies to normalize ambiguous requests to reduce request smuggling risk.\n\nInsecure patterns:\n\n* Creating an HTTP server with `{ insecureHTTPParser: true }`.\n\nDetection hints:\n\n* Search for `insecureHTTPParser` in server creation code.\n\nFix:\n\n* Remove insecure parsing; rely on spec-compliant parsing and normalize at the edge.\n\nNotes:\n\n* Node security guidance explicitly recommends not using `insecureHTTPParser`. ([Node.js][15])\n\n---\n\n## 5) Practical scanning heuristics (how to “hunt”)\n\nWhen actively scanning an Express repo, these patterns are high-signal:\n\n* TLS / transport:\n\n  * `app.listen(80` without reverse proxy mention; missing `helmet`; cookies missing `secure` ([Express][1]) (NOTE this only applies to web facing applications, internal apps likely won't have TLS)\n* Proxy trust:\n\n  * `app.set('trust proxy', true)`; logic using `req.ip`/`req.protocol`/`req.hostname` ([Express][2])\n* Security headers / fingerprinting:\n\n  * missing `helmet(`; missing `app.disable('x-powered-by')` ([Express][1])\n* Cookies / sessions:\n\n  * `express-session` with missing `store` (MemoryStore risk), hard-coded `secret:`, missing `cookie: { secure/httpOnly/sameSite }` ([Express][1])\n  * `cookie-session` storing large objects or secrets ([Express][1])\n* Body parsing limits:\n\n  * `express.json()` or `express.urlencoded()` without `limit`/`parameterLimit`/`depth` ([Express][5])\n* CSRF:\n\n  * POST/PUT/PATCH/DELETE routes using cookie auth with no CSRF tokens/origin checks ([OWASP Cheat Sheet Series][3])\n* Open redirects:\n\n  * `res.redirect(req.query.next)` or similar ([Express][1])\n* XSS / HTML output:\n\n  * `res.send(` building HTML with user input; template “safe” flags; untrusted values in `res.locals` ([Express][5])\n* File handling:\n\n  * `res.sendFile(` / `res.download(` where path originates from request; `express.static('uploads')` ([Express][5])\n* Injection:\n\n  * SQL strings + template literals into DB calls ([OWASP Cheat Sheet Series][6])\n  * `child_process.exec` / `execSync` / `shell: true` ([OWASP Cheat Sheet Series][14])\n* SSRF:\n\n  * outbound `fetch/axios/got` to user-provided URLs ([OWASP Cheat Sheet Series][7])\n* Brute force / abuse:\n\n  * auth endpoints lacking throttling; no rate limiting middleware ([Express][1])\n* Supply chain:\n\n  * outdated Express versions; no lockfiles; no `npm audit` workflow ([Express][1])\n* Node runtime hazards:\n\n  * `--inspect` in production scripts; `insecureHTTPParser` usage ([Node.js][15])\n\nAlways try to confirm:\n\n* data origin (untrusted vs trusted)\n* sink type (HTML/template, SQL/NoSQL, subprocess, filesystem, redirect, outbound HTTP)\n* protective controls present (validation, allowlists, middleware, proxy config, header policies)\n* whether protections are at the edge vs in app code\n\n---\n\n## 6) Sources (accessed 2026-01-27)\n\nPrimary Express documentation:\n\n* Express: Production Best Practices — Security: `https://expressjs.com/en/advanced/best-practice-security.html` ([Express][1])\n* Express: Behind Proxies (`trust proxy`): `https://expressjs.com/en/guide/behind-proxies.html` ([Express][2])\n* Express 5.x API Reference (parsers, static, sendFile, redirect, cookies): `https://expressjs.com/en/5x/api.html` ([Express][5])\n* Express: Error Handling: `https://expressjs.com/en/guide/error-handling.html` ([Express][11])\n\nSession middleware documentation:\n\n* express-session docs (cookie flags, secret rotation, fixation mitigation, MemoryStore warning): `https://expressjs.com/en/resources/middleware/session.html` ([Express][1])\n\nNode.js and npm official references:\n\n* Node.js — Security Best Practices (DoS, proxy guidance, inspector risks, request smuggling notes): `https://nodejs.org/en/learn/getting-started/security-best-practices` ([Node.js][15])\n* npm Docs — `npm audit`: `https://docs.npmjs.com/cli/v9/commands/npm-audit/` ([npm Docs][16])\n\nOWASP Cheat Sheet Series:\n\n* Session Management: `https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][12])\n* CSRF Prevention: `https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][3])\n* XSS Prevention: `https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][4])\n* Input Validation: `https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][17])\n* SQL Injection Prevention: `https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][6])\n* OS Command Injection Defense: `https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][14])\n* SSRF Prevention: `https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][7])\n* File Upload: `https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][13])\n* Unvalidated Redirects: `https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][18])\n* HTTP Headers: `https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][10])\n\nVersioning / advisories:\n\n* Express package version (npm): `https://www.npmjs.com/package/express`\n* Express open redirect advisory (CVE): `https://nvd.nist.gov/vuln/detail/CVE-2024-29041` ([NVD][9])\n\n[1]: https://expressjs.com/en/advanced/best-practice-security.html \"Security Best Practices for Express in Production\"\n[2]: https://expressjs.com/en/guide/behind-proxies.html \"Express behind proxies\"\n[3]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html \"Cross-Site Request Forgery Prevention - OWASP Cheat Sheet Series\"\n[4]: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html \"Cross Site Scripting Prevention - OWASP Cheat Sheet Series\"\n[5]: https://expressjs.com/en/5x/api.html \"Express 5.x - API Reference\"\n[6]: https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html \"SQL Injection Prevention - OWASP Cheat Sheet Series\"\n[7]: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html \"Server Side Request Forgery Prevention - OWASP Cheat Sheet Series\"\n[8]: https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html \"Nodejs Security - OWASP Cheat Sheet Series\"\n[9]: https://nvd.nist.gov/vuln/detail/cve-2024-29041?utm_source=chatgpt.com \"CVE-2024-29041 Detail - NVD\"\n[10]: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html \"HTTP Headers - OWASP Cheat Sheet Series\"\n[11]: https://expressjs.com/en/guide/error-handling.html \"Express error handling\"\n[12]: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html \"Session Management - OWASP Cheat Sheet Series\"\n[13]: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html \"File Upload - OWASP Cheat Sheet Series\"\n[14]: https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html \"OS Command Injection Defense - OWASP Cheat Sheet Series\"\n[15]: https://nodejs.org/en/learn/getting-started/security-best-practices \"Node.js — Security Best Practices\"\n[16]: https://docs.npmjs.com/cli/v9/commands/npm-audit/ \"npm-audit | npm Docs\"\n[17]: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html \"Input Validation - OWASP Cheat Sheet Series\"\n[18]: https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html \"Unvalidated Redirects and Forwards - OWASP Cheat Sheet Series\"\n"
  },
  {
    "path": "skills/.curated/security-best-practices/references/javascript-general-web-frontend-security.md",
    "content": "# Frontend JavaScript/TypeScript Web Security Spec (Vanilla Browser JS/TS, Modern Browsers)\n\nThis document is designed as a **security spec** that supports:\n\n1. **Secure-by-default code generation** for new frontend JavaScript/TypeScript (no specific framework assumed).\n2. **Security review / vulnerability hunting** in existing frontend code (passive “notice issues while working” and active “scan the repo and report findings”).\n\nIt is intentionally written as a set of **normative requirements** (“MUST/SHOULD/MAY”) plus **audit rules** (what bad patterns look like, how to detect them, and how to fix/mitigate them).\n\n---\n\n## 0) Safety, boundaries, and anti-abuse constraints (MUST FOLLOW)\n\n* MUST NOT request, output, log, hard-code, or commit secrets (API keys intended to be secret, private keys, passwords, OAuth refresh tokens, session tokens, cookies).\n  Notes:\n\n  * Frontend code is inherently observable by end users. If a value must remain secret, it must not be in browser-delivered code.\n  * If the project uses “public” keys (e.g., publishable analytics keys), they MUST be treated as non-secret and scoped accordingly.\n\n* MUST NOT “fix” security by disabling protections (e.g., weakening CSP with `unsafe-inline`/`unsafe-eval` without justification, removing origin checks for `postMessage`, switching to `innerHTML` for convenience, accepting arbitrary redirects/URLs, or turning off sanitization).\n\n* MUST provide **evidence-based findings** during audits: cite file paths, code snippets, and relevant HTML/CSP/config values that justify the claim.\n\n* MUST treat uncertainty honestly:\n\n  * Security headers (CSP, frame-ancestors, etc.) might be set by server/edge/CDN rather than in repo code. If not visible, report as “not visible here; verify at runtime/edge config.” (Also note that `<meta http-equiv=...>` only simulates a subset of headers; don’t assume other security headers exist just because a meta tag exists.) ([MDN Web Docs][1])\n\n---\n\n## 1) Operating modes\n\n### 1.1 Generation mode (default)\n\nWhen asked to write new frontend JS/TS code or modify existing code:\n\n* MUST follow every **MUST** requirement in this spec.\n* SHOULD follow every **SHOULD** requirement unless the user explicitly says otherwise.\n* MUST prefer safe-by-default browser APIs and proven libraries over custom security code (especially for HTML sanitization).\n* MUST avoid introducing new risky sinks (DOM XSS injection sinks like `innerHTML`, navigation to `javascript:` URLs, dynamic code execution via `eval`/`Function`, unsafe `postMessage`, unsafe third-party script loading, etc.). ([OWASP Cheat Sheet Series][2])\n\n### 1.2 Passive review mode (always on while editing)\n\nWhile working anywhere in a frontend repo (even if the user did not ask for a security scan):\n\n* MUST “notice” violations of this spec in touched/nearby code.\n* SHOULD mention issues as they come up, with a brief explanation + safe fix.\n\n### 1.3 Active audit mode (explicit scan request)\n\nWhen the user asks to “scan”, “audit”, or “hunt for vulns”:\n\n* MUST systematically search the codebase for violations of this spec.\n* MUST output findings in a structured format (see §2.3).\n\nRecommended audit order:\n\n1. HTML entrypoints (`index.html`, server-rendered templates), script/style includes, and any CSP delivery (header vs meta). ([W3C][3])\n2. DOM XSS sinks (`innerHTML`, `document.write`, `insertAdjacentHTML`, event-handler attributes) and their data sources (URL params/hash, storage, postMessage, API responses). ([OWASP Cheat Sheet Series][2])\n3. Navigation/redirect handling (`window.location*`, link targets, URL allowlists) including `javascript:` URL hazards. ([MDN Web Docs][4])\n4. Cross-origin communication (`postMessage`, iframe embed patterns, sandboxing). ([MDN Web Docs][5])\n5. Storage of sensitive data (localStorage/sessionStorage) and assumptions about trust. ([OWASP Cheat Sheet Series][6])\n6. Third-party scripts / tag managers / CDNs, and integrity controls (SRI) and policy controls (CSP). ([OWASP Cheat Sheet Series][7])\n7. DOM clobbering gadgets and unsafe reliance on `window`/`document` named properties. ([OWASP Cheat Sheet Series][8])\n\n---\n\n## 2) Definitions and review guidance\n\n### 2.1 Untrusted input (treat as attacker-controlled unless proven otherwise)\n\nExamples include:\n\n* URL-derived data: `location.href`, `location.search`, `location.hash`, `document.baseURI`, `new URLSearchParams(location.search)`, routing fragments. ([OWASP Cheat Sheet Series][2])\n* DOM content that may include user-controlled markup (comments, profiles, CMS content, markdown-to-HTML output, etc.), especially if inserted dynamically. ([OWASP Cheat Sheet Series][2])\n* `postMessage` event data (`event.data`) and metadata (`event.origin`) from other windows/frames. ([MDN Web Docs][5])\n* Browser storage: `localStorage`, `sessionStorage`, IndexedDB (contents can be attacker-influenced via XSS or local machine access; never treat as “trusted”). ([OWASP Cheat Sheet Series][6])\n* Any data returned from network calls (even if from “your API”), because it may contain stored attacker content that becomes dangerous only when inserted into the DOM. ([OWASP Cheat Sheet Series][2])\n\n### 2.2 Dangerous sink (DOM XSS / code execution sink)\n\nA sink is any API/operation that can execute script or interpret attacker-controlled strings as HTML/JS/URL in a security-sensitive way. High-signal sinks include:\n\n* HTML parsing / insertion: `innerHTML`, `outerHTML`, `insertAdjacentHTML`, `document.write`, `document.writeln`. ([OWASP Cheat Sheet Series][2])\n* Dynamic code execution: `eval`, `new Function`, `setTimeout(\"...\")`, `setInterval(\"...\")`. ([MDN Web Docs][10])\n* Navigation to script-bearing URLs (e.g., `javascript:`) via setters like `Location.href`/`window.location` (and via link `href` if attacker-controlled). ([MDN Web Docs][4])\n* Setting event handler attributes from strings, e.g. `setAttribute(\"onclick\", \"...\")`. ([OWASP Cheat Sheet Series][2])\n\n### 2.3 Required audit finding format\n\nFor each issue found, output:\n\n* Rule ID:\n* Severity: Critical / High / Medium / Low\n* Location: file path + function/class/module + line(s)\n* Evidence: the exact code/config snippet\n* Impact: what could go wrong, who can exploit it\n* Fix: safe change (prefer minimal diff)\n* Mitigation: defense-in-depth if immediate fix is hard\n* False positive notes: what to verify if uncertain\n\n---\n\n## 3) Secure baseline: minimum production configuration (MUST in production)\n\nThis is the smallest baseline that prevents common frontend JS/TS security misconfigurations. Some items are “in repo” (HTML/JS) and some may live at the server/edge.\n\n### 3.1 Content Security Policy (CSP) baseline (SHOULD; MUST for high-risk apps)\n\n* SHOULD deliver CSP via HTTP response headers when possible.\n* MAY deliver CSP via an HTML `<meta http-equiv=\"Content-Security-Policy\" ...>` tag when you cannot set headers (e.g., purely static hosting constraints). ([MDN Web Docs][1])\n* If using CSP via `<meta http-equiv>`, MUST understand the limitations:\n\n  * The policy only applies to content that follows the meta element (so it must appear very early, before any scripts/resources you want governed). ([W3C][3])\n  * The following directives are **not supported** in a meta-delivered policy and will be ignored: `report-uri`, `frame-ancestors`, and `sandbox`. ([W3C][3])\n  * “Report-only” CSP cannot be set via a meta element. ([W3C][3])\n\nPractical baseline goals:\n\n* Avoid script sources `unsafe-inline` and `unsafe-eval` (they significantly weaken CSP’s value against XSS). ([MDN Web Docs][10])\n* Prefer nonce- or hash-based script policies if you need inline scripts. ([MDN Web Docs][10])\n* Consider enabling Trusted Types enforcement where feasible. ([MDN Web Docs][11])\n\n### 3.2 Third-party scripts baseline (SHOULD)\n\n* SHOULD minimize third-party script execution and treat it as equivalent privilege to first-party JS (it runs with your origin’s privileges). ([OWASP Cheat Sheet Series][7])\n* SHOULD use Subresource Integrity (SRI) for third-party scripts/styles loaded from CDNs. ([MDN Web Docs][12])\n\n### 3.3 Cross-window communication baseline (SHOULD)\n\n* SHOULD restrict `postMessage` communications to explicit origins, and validate both origin and message shape. ([MDN Web Docs][5])\n\n---\n\n## 4) Rules (generation + audit)\n\nEach rule contains: required practice, insecure patterns, detection hints, and remediation.\n\n### JS-XSS-001: Do not inject untrusted HTML into the DOM (avoid `innerHTML` and friends)\n\nSeverity: Critical if you can prove attacker-controlled input can reach these APIs; otherwise Medium\n\n\nRequired:\n\n* MUST treat `innerHTML`, `outerHTML`, and `insertAdjacentHTML` as dangerous sinks when their input can contain untrusted data. ([OWASP Cheat Sheet Series][2])\n* MUST prefer safe DOM APIs that do not parse HTML:\n\n  * `textContent` for text. ([OWASP Cheat Sheet Series][2])\n  * `document.createElement`, `appendChild`, `setAttribute` for non-event-handler attributes. ([OWASP Cheat Sheet Series][2])\n* If HTML insertion is truly required, SHOULD sanitize with a well-reviewed HTML sanitizer and strongly consider enforcing Trusted Types to confine usage to audited code paths. ([MDN Web Docs][11])\n\nInsecure patterns:\n\n* `el.innerHTML = userInput`\n* `el.insertAdjacentHTML('beforeend', userInput)`\n* `el.outerHTML = userInput`\n\nDetection hints:\n\n* Search for: `.innerHTML`, `.outerHTML`, `insertAdjacentHTML(`.\n* Trace the origin of inserted string: URL params/hash, postMessage, storage, API responses, DOM attributes. ([OWASP Cheat Sheet Series][2])\n\nFix:\n\n* Replace with `textContent` for plain text. ([OWASP Cheat Sheet Series][2])\n* For structured UI, build DOM nodes explicitly.\n* For “rich text” requirements:\n\n  * Sanitize using an allowlist-based sanitizer.\n  * Prefer returning safe “components” instead of arbitrary HTML strings.\n  * Use Trusted Types enforcement to ensure only `TrustedHTML` reaches sinks where supported. ([MDN Web Docs][11])\n\nMitigation:\n\n* Deploy a strict CSP and consider Trusted Types enforcement (`require-trusted-types-for 'script'`). ([MDN Web Docs][10])\n\nFalse positive notes:\n\n* If the string is provably constant or fully generated from trusted constants, it may be safe. Still prefer safer APIs.\n\n---\n\n### JS-XSS-002: Avoid `document.write` / `document.writeln` (XSS + document clobbering hazards)\n\nSeverity: Critical if you can prove attacker-controlled input can reach these APIs; otherwise Medium \n\nRequired:\n\n* MUST avoid `document.write()` and `document.writeln()` in production code (they are XSS vectors and can be abused with crafted HTML even if some browsers block injected `<script>` in certain situations). ([MDN Web Docs][13])\n* If legacy use is unavoidable, MUST ensure no untrusted input reaches these APIs and SHOULD enforce Trusted Types (`TrustedHTML`) where supported. ([MDN Web Docs][14])\n\nInsecure patterns:\n\n* `document.write(userInput)`\n* `document.writeln(getParam('q'))`\n\nDetection hints:\n\n* Search for `document.write(`, `document.writeln(`. ([OWASP Cheat Sheet Series][2])\n\nFix:\n\n* Replace with DOM manipulation (`createElement`, `appendChild`) or safe text insertion (`textContent`). ([OWASP Cheat Sheet Series][2])\n\nMitigation:\n\n* Strict CSP + Trusted Types enforcement reduces blast radius if a sink remains. ([MDN Web Docs][10])\n\n---\n\n### JS-XSS-003: Do not use string-to-code execution (`eval`, `new Function`, string timeouts)\n\nSeverity: Critical if you can prove attacker-controlled input can reach these APIs; otherwise Medium\n\nRequired:\n\n* MUST NOT pass untrusted data to:\n\n  * `eval()`\n  * `new Function(...)`\n  * `setTimeout(\"...\")` / `setInterval(\"...\")` with string arguments ([MDN Web Docs][10])\n* SHOULD avoid these APIs entirely in modern frontend code; refactor to non-eval logic. ([MDN Web Docs][10])\n* MUST NOT “fix CSP breakage” by adding `unsafe-eval` unless there is a documented, reviewed justification and compensating controls. ([MDN Web Docs][10])\n\nInsecure patterns:\n\n* `eval(userInput)`\n* `new Function(\"return \" + userInput)()`\n* `setTimeout(userInput, 0)` where userInput is a string\n\nDetection hints:\n\n* Search for `eval(`, `new Function`, `setTimeout(\"`, `setInterval(\"`.\n* Also search for construction of code strings used later.\n\nFix:\n\n* Replace dynamic code with:\n\n  * structured data + explicit branching/handlers,\n  * JSON parsing (`JSON.parse`) instead of `eval` for JSON. ([OWASP Cheat Sheet Series][2])\n\nMitigation:\n\n* CSP that blocks `eval()`-like APIs by default, and avoid `unsafe-eval`. ([MDN Web Docs][10])\n* Consider Trusted Types for controlled cases, but treat it as a hardening layer, not a license to keep eval patterns. ([MDN Web Docs][10])\n\n---\n\n### JS-XSS-004: Do not set event handler attributes from strings (e.g., `setAttribute(\"onclick\", \"...\")`)\n\nSeverity: High\n\nRequired:\n\n* MUST NOT use `setAttribute(\"on…\", string)` or similar patterns with untrusted data; this coerces strings into executable code in the event-handler context. ([OWASP Cheat Sheet Series][2])\n* SHOULD prefer `addEventListener` with function references.\n\nInsecure patterns:\n\n* `el.setAttribute(\"onclick\", userInput)`\n* `el.onclick = userControlledString` (string assignment)\n\nDetection hints:\n\n* Search for `.setAttribute(\"on`, `.onclick =`, `.onmouseover =`, etc.\n* Trace whether RHS can be influenced by URL/hash/storage/postMessage. ([OWASP Cheat Sheet Series][2])\n\nFix:\n\n* Replace with `addEventListener(\"click\", () => { ... })`.\n* If dynamic dispatch is needed, use an allowlisted mapping from identifiers to functions (no string eval). ([OWASP Cheat Sheet Series][2])\n\n---\n\n### JS-URL-001: Sanitize and allowlist URLs before navigation (especially `window.location` / `location.replace`)\n\nSeverity: Low (High if you can prove an attacker can fully control the URL)\n\nIMPORTANT: This can cause a lot of false positives. Please perform extra analysis to determine if the url is fully attacker controlled. If not fully attacker controlled, then this is informational at best.\n\nNOTE: It may be important functionality to be able to redirect to any given url. If that is the goal of the feature, then at a minimum, ensure it checks the schema even if the origin is allowed to be anything.\n\nRequired:\n\n* MUST treat any assignment to navigation targets as security-sensitive:\n\n  * `window.location = ...`\n  * `location.href = ...`\n  * `location.assign(...)`\n  * `location.replace(...)` ([MDN Web Docs][4])\n* MUST prevent navigation to `javascript:` URLs (and generally other script-bearing/active schemes), especially when input is derived from URL params, storage, or messages. ([MDN Web Docs][4]). Only allow `http:` and `https:`.\n* SHOULD validate/allowlist the destination. A safe baseline is:\n\n  * Allow only same-origin relative paths, OR\n  * Allow only a strict allowlist of origins and protocols (typically `https:` and optionally `http:` for localhost dev). ([OWASP Cheat Sheet Series][8])\n\nInsecure patterns:\n\n* `location.replace(getParam(\"next\"))`\n* `window.location = userSuppliedUrl`\n* `location.assign(window.redirectTo || \"/\")` where `redirectTo` can be clobbered or attacker-set ([OWASP Cheat Sheet Series][8])\n\nDetection hints:\n\n* Search for `window.location`, `location.href`, `location.assign`, `location.replace`.\n* Search for common redirect parameters: `next`, `returnTo`, `redirect`, `url`, `continue`.\n* Search for `javascript:` literal usage. ([MDN Web Docs][4])\n\nFix:\n\n* Parse and validate with `new URL(value, location.origin)` and then enforce:\n\n  * `url.protocol` in `{ \"https:\" }` (and only include `http:` in explicit dev-only code paths),\n  * `url.origin` equals `location.origin` for internal redirects, or in a strict allowlist for external redirects,\n  * optionally allow only specific path prefixes. ([MDN Web Docs][4])\n* If validation fails, navigate to a safe default (home/dashboard).\n\nMitigation:\n\n* Deploy strict CSP and Trusted Types enforcement to reduce the impact of DOM XSS sinks, but note that Trusted Types do not prevent every possible unsafe navigation scenario on their own. ([W3C][15])\n\nFalse positive notes:\n\nIMPORTANT: This can cause a lot of false positives. Please perform extra analysis to determine if the url is fully attacker controlled. If not fully attacker controlled, then this is informational at best.\n\n* Some apps intentionally support external redirects (SSO, payment flows). Those MUST be allowlisted and documented.\n\n---\n\n### JS-URL-002: Sanitize URLs before inserting into DOM URL contexts (`href`, `src`, etc.)\n\nSeverity: Low (High if you can prove an attacker can fully control the URL)\n\nIMPORTANT: This can cause a lot of false positives. Please perform extra analysis to determine if the url is fully attacker controlled. If not fully attacker controlled, then this is informational at best.\n\nRequired:\n\n* MUST treat setting URL-bearing DOM attributes/properties as security-sensitive, especially:\n\n  * `a.href`, `img.src`, `script.src`, `iframe.src`, `form.action`, `link.href`.\n* MUST prevent script-bearing schemes (`javascript:` and other active schemes) when values can be attacker-influenced. ([MDN Web Docs][4])\n* SHOULD prefer setting properties (e.g., `a.href = url.toString()`) after parsing and validation, rather than string concatenation.\n\nInsecure patterns:\n\n* `link.href = getParam(\"u\")`\n* `el.setAttribute(\"href\", userInput)` without validation\n* constructing URLs via concatenation with untrusted pieces\n\nDetection hints:\n\n* Search for `.href =`, `.src =`, `.action =`, `setAttribute(\"href\"`, `setAttribute(\"src\"`.\n* Search for `javascript:` / `data:` usage in URLs. ([MDN Web Docs][4])\n\nIMPORTANT: This can cause a lot of false positives. Please perform extra analysis to determine if the url is fully attacker controlled. If not fully attacker controlled, then this is informational at best.\n\nFix:\n\n* Use `new URL(...)` and validate:\n\n  * protocol allowlist\n  * avoid passing user-provided values into `<script src>` at all (treat as code execution). ([OWASP Cheat Sheet Series][8])\n\n---\n\n### JS-CSP-001: Use CSP; meta delivery is allowed\n\nSeverity: Medium to High (depends on threat model; High when handling untrusted content)\n\nNOTE: It is most important to set the CSP's script-src. All other directives are not as important and can generally be excluded for the ease of development.\n\nRequired:\n\n* SHOULD deploy a CSP as a major defense-in-depth against XSS. ([MDN Web Docs][10])\n* MAY provide CSP via `<meta http-equiv=\"Content-Security-Policy\" ...>` when headers are not available. ([MDN Web Docs][1])\n* If CSP is delivered via meta, MUST:\n\n  * place it early (before scripts/resources you want governed), and\n  * not rely on unsupported directives in meta policies (`report-uri`, `frame-ancestors`, `sandbox`). ([W3C][3])\n* MUST avoid adding `unsafe-inline` as a “quick fix” for CSP issues unless explicitly required and reviewed (it defeats much of CSP’s purpose). ([MDN Web Docs][10])\n* MUST avoid adding `unsafe-eval` unless explicitly required and reviewed (it allows eval-like APIs that are commonly abused). ([MDN Web Docs][10])\n\nInsecure patterns:\n\n* No CSP present anywhere (repo HTML or server/edge) for an app that renders untrusted content.\n* CSP includes `script-src 'unsafe-inline'` and/or `script-src 'unsafe-eval'` without strong justification. ([MDN Web Docs][10])\n* CSP delivered via meta but includes `frame-ancestors` (it will be ignored in meta). ([W3C][3])\n\nDetection hints:\n\n* Search HTML for `<meta http-equiv=\"Content-Security-Policy\"`.\n* Search server/edge configs for `Content-Security-Policy` header.\n* If CSP is only in meta, check it appears before any `<script>` tags you want governed. ([W3C][3])\n\nFix:\n\n* Prefer header-delivered CSP at the server/edge.\n* If constrained to meta, keep a strong allowlist CSP and document the limitations; implement clickjacking protections (e.g., `frame-ancestors`) at the server/edge, not in meta. ([W3C][3])\n\n---\n\n### JS-CSP-002: Prefer strict CSP (nonces/hashes); avoid inline/eval patterns in code\n\nSeverity: Medium\n\nNOTE: It is most important to set the CSP's script-src. All other directives are not as important and can generally be excluded for the ease of development.\n\nRequired:\n\n* SHOULD design frontend code to work under a strict CSP:\n\n  * avoid inline scripts and inline event handlers,\n  * avoid eval-like APIs (see JS-XSS-003),\n  * allow scripts via nonce or hash when needed. ([MDN Web Docs][10])\n\nInsecure patterns:\n\n* Large amounts of inline script blocks and inline `onclick=\"...\"` handlers.\n* Libraries that require `unsafe-eval`.\n\nDetection hints:\n\n* Search for `<script>` blocks with inline code, `onclick=\"`, `onload=\"`, etc.\n* Search for CSP directives containing `unsafe-inline` or `unsafe-eval`. ([MDN Web Docs][10])\n\nFix:\n\n* Move inline scripts into external JS files (same-origin).\n* Use nonces/hashes for any unavoidable inline blocks. ([MDN Web Docs][10])\n\n---\n\n### JS-TT-001: Use Trusted Types to reduce DOM XSS attack surface (where supported)\n\nSeverity: Low\n\nRequired:\n\n* SHOULD consider enabling Trusted Types enforcement with CSP `require-trusted-types-for 'script'` to make many DOM XSS sinks reject raw strings. ([MDN Web Docs][11])\n* If using Trusted Types, SHOULD also use the CSP `trusted-types` directive to restrict which policies can be created (reduces policy sprawl and improves auditability). ([MDN Web Docs][16])\n* MUST keep Trusted Types policy code small, heavily reviewed, and used as the only path to produce trusted values for sinks. ([W3C][15])\n\nInsecure patterns:\n\n* “Trusted Types enabled” but policy simply returns input unchanged (no sanitization/validation).\n* Many ad-hoc policies created across the codebase without restriction.\n* Belief that Trusted Types alone prevents all unsafe navigations or all XSS classes. (It targets DOM injection sinks; it is not a universal sandbox.) ([W3C][15])\n\nDetection hints:\n\n* Search for CSP directives: `require-trusted-types-for` and `trusted-types`.\n* Search code for `trustedTypes.createPolicy(` and inspect policy implementations. ([MDN Web Docs][11])\n\nFix:\n\n* Add a small set of well-reviewed policies (e.g., `createHTML` that sanitizes).\n* Restrict allowed policies via `trusted-types <policyName...>`.\n* Migrate sinks to require `TrustedHTML` / `TrustedScriptURL` as appropriate. ([MDN Web Docs][11])\n\n---\n\n### JS-MSG-001: `postMessage` must use strict origin validation and explicit targetOrigin\n\nSeverity: Medium (High if dangerous behavior can be triggered via postMessage)\n\nRequired:\n\n* When sending messages, MUST set an explicit `targetOrigin` (not `*`) to avoid sending data to an unexpected origin after redirects or window origin changes. ([MDN Web Docs][5])\n* When receiving messages, MUST:\n\n  * Validate `event.origin` exactly against an allowlist of expected origins (no substring matching). ([OWASP Cheat Sheet Series][6])\n  * Consider validating `event.source` (expected window reference) when applicable. ([MDN Web Docs][5])\n  * Validate `event.data` structure (schema/shape) and treat it purely as data (never evaluate it as code and never insert into DOM with `innerHTML`). ([OWASP Cheat Sheet Series][6])\n\nInsecure patterns:\n\n* `otherWindow.postMessage(payload, \"*\")`\n* `window.addEventListener(\"message\", (e) => { doSomething(e.data) })` with no `origin` check\n* `if (e.origin.includes(\"trusted.com\"))` (substring checks)\n* `el.innerHTML = e.data` ([OWASP Cheat Sheet Series][6])\n\nDetection hints:\n\n* Search for `postMessage(`, `addEventListener(\"message\"`, `onmessage =`.\n* Audit all handlers for explicit allowlist checks on `event.origin`. ([OWASP Cheat Sheet Series][6])\n\nFix:\n\n* Define an allowlist:\n\n  * `const ALLOWED = new Set([\"https://app.example.com\", \"https://accounts.example.com\"]);`\n  NOTE: For ease of development, you can use the current page's origin `window.location.origin` as a safe default origin.\n* On receive:\n\n  * `if (!ALLOWED.has(event.origin)) return;`\n  * Validate `event.data` with a strict schema and reject unknown/extra fields.\n* On send:\n\n  * use the exact expected origin string as `targetOrigin`. ([OWASP Cheat Sheet Series][6])\n\nMitigation:\n\n* Combine with a strict CSP and avoid DOM sinks in message paths. ([MDN Web Docs][10])\n\n---\n\n### JS-STORAGE-001: Web Storage is not a safe place for secrets (and is attacker-influencable)\n\nSeverity: Low\n\nRequired:\n\n* MUST NOT store sensitive secrets or session identifiers in `localStorage` (or `sessionStorage`) if compromise would matter; a single XSS can exfiltrate everything in storage. ([OWASP Cheat Sheet Series][6])\n* MUST treat values read from storage as untrusted input (attackers can load malicious values into storage via XSS). ([OWASP Cheat Sheet Series][6])\n* SHOULD prefer server-set cookies with `HttpOnly` for session identifiers (JS cannot set `HttpOnly`, so avoid storing session IDs in JS-accessible storage). ([OWASP Cheat Sheet Series][6])\n* SHOULD avoid hosting multiple unrelated apps on the same origin if they rely on storage separation (storage is origin-wide). ([OWASP Cheat Sheet Series][6])\n\nInsecure patterns:\n\n* `localStorage.setItem(\"access_token\", token)`\n* `localStorage.setItem(\"session\", sessionId)`\n* Assuming `localStorage` is “trusted because same-origin.”\n\nDetection hints:\n\n* Search for `localStorage.getItem`, `localStorage.setItem`, `sessionStorage.*`.\n* Flag storage keys named `token`, `jwt`, `session`, `auth`, `refresh`. ([OWASP Cheat Sheet Series][6])\n\nFix:\n\n* Use server-managed sessions or short-lived tokens delivered and rotated securely, with careful XSS defenses (CSP/Trusted Types) and minimal JS exposure.\n* If storage must be used for non-sensitive state, keep it non-auth and validate/escape before use.\n\n---\n\n### JS-SUPPLY-001: Third-party JavaScript is a major supply-chain risk; minimize and control it\n\nSeverity: Low\n\nRequired:\n\n* MUST treat third-party JS as equivalent to first-party JS in privilege (it can execute arbitrary code in your origin and access DOM data). ([OWASP Cheat Sheet Series][7])\n* SHOULD minimize third-party scripts and prefer:\n\n  * self-hosting / script mirroring,\n  * strict CSP allowlists,\n  * SRI for any CDN-hosted scripts,\n  * ongoing monitoring for unexpected changes. ([OWASP Cheat Sheet Series][7])\n\nInsecure patterns:\n\n* Loading arbitrary remote scripts from many vendors without review.\n* Using tag managers that can dynamically inject scripts with no integrity controls.\n* Allowing scripts from broad wildcards in CSP (e.g., `script-src *`). ([MDN Web Docs][10])\n\nDetection hints:\n\n* Search HTML for `<script src=\"https://...\">` and `tag manager` snippets.\n* Search CSP `script-src` sources for wildcards or overly broad domains.\n* Search for dynamic script injection: `document.createElement(\"script\")`, `script.src = ...`, `appendChild(script)`. ([OWASP Cheat Sheet Series][8])\n\nFix:\n\n* Remove unnecessary third-party tags.\n* Self-host or mirror scripts where possible.\n* Lock down CSP `script-src` to the smallest set of trusted sources.\n* Add SRI for CDN scripts/styles. ([OWASP Cheat Sheet Series][7])\n\n---\n\n### JS-SRI-001: Use Subresource Integrity (SRI) for third-party scripts/styles\n\nSeverity: Low\n\nRequired:\n\n* SHOULD use SRI to ensure browsers only load third-party resources if they match an expected cryptographic hash. ([MDN Web Docs][12])\n* MUST update SRI hashes whenever the underlying resource changes (pin versions; avoid “latest” URLs).\n\nInsecure patterns:\n\n* `<script src=\"https://cdn.example.com/lib.js\"></script>` with no `integrity`.\n* Loading `latest` or unpinned third-party resources.\n\nDetection hints:\n\n* Search for `<script src=\"https://` and `<link rel=\"stylesheet\" href=\"https://` without `integrity=`.\n* Check whether `integrity` is present and uses strong hashes (sha256/384/512 are typical). ([MDN Web Docs][12])\n\nFix:\n\n* Add `integrity=\"sha384-...\"` (or appropriate) and ensure proper CORS mode where needed.\n* Prefer self-hosting critical libraries.\n\n---\n\n### FS-DOMC-001: Prevent DOM clobbering (avoid relying on `window`/`document` named properties)\n\nSeverity: Medium to High (can become Critical if it enables script loading or `javascript:` navigation)\n\nRequired:\n\n* MUST NOT rely on implicit global variables or `window.someName` / `document.someName` lookups that can be clobbered by injected HTML elements with matching `id`/`name`. ([OWASP Cheat Sheet Series][8])\n* MUST avoid patterns like `let x = window.redirectTo || \"/safe\"; location.assign(x);` where `redirectTo` could be clobbered to an `<a>` element whose `href` is attacker-controlled (including `javascript:`). ([OWASP Cheat Sheet Series][8])\n* SHOULD use explicit variable declarations, local scope, and explicit DOM queries (`getElementById`) rather than named property access. ([OWASP Cheat Sheet Series][8])\n* If the app inserts user-controlled markup (even sanitized), SHOULD ensure sanitization strategies consider `id`/`name` collisions. ([OWASP Cheat Sheet Series][8])\n\nInsecure patterns:\n\n* `const cfg = window.config || {};` used for security-sensitive URLs.\n* `const redirect = window.redirectTo || \"/\"; location.assign(redirect);` ([OWASP Cheat Sheet Series][8])\n* Loading scripts from `window.*` config values without strict validation.\n\nDetection hints:\n\n* Search for `window.` and `document.` used as config stores (especially `||` fallback patterns).\n* Search for usage of `location.assign/replace` with variables that come from `window`/`document` properties.\n* Search for dynamic script creation (`createElement('script')`) where `.src` comes from a non-local variable. ([OWASP Cheat Sheet Series][8])\n\nFix:\n\n* Store config in module-scoped constants (not on `window`/`document`) and pass it explicitly.\n* Validate any URL-like config with protocol/origin allowlists (see FEJS-URL-001). ([OWASP Cheat Sheet Series][8])\n* Consider hardening: sanitization, CSP, and (in limited cases) freezing sensitive objects, but treat these as defense-in-depth, not a substitute for safe coding patterns. ([OWASP Cheat Sheet Series][8])\n\n---\n\n## 5) Practical scanning heuristics (how to “hunt”)\n\nWhen actively scanning, use these high-signal patterns:\n\n* DOM XSS sinks:\n\n  * `.innerHTML`, `.outerHTML`, `insertAdjacentHTML(`\n  * `document.write(`, `document.writeln(` ([OWASP Cheat Sheet Series][2])\n\n* Dangerous navigation / URL sinks:\n\n  * `window.location`, `location.href`, `location.assign`, `location.replace`\n  * `javascript:` literals (and other suspicious schemes like `data:text/html`) ([MDN Web Docs][4])\n\n* String-to-code execution:\n\n  * `eval(`, `new Function`, `setTimeout(\"`, `setInterval(\"` ([MDN Web Docs][10])\n\n* Event-handler string injection:\n\n  * `.setAttribute(\"on`, `.onclick =`, `.onload =` with strings ([OWASP Cheat Sheet Series][2])\n\n* `postMessage`:\n\n  * `postMessage(` with `\"*\"` as targetOrigin\n  * `addEventListener(\"message\"` without strict `event.origin` allowlist checks ([MDN Web Docs][5])\n\n* Storage:\n\n  * `localStorage.setItem(` / `getItem(`, `sessionStorage.*`\n  * keys containing `token`, `jwt`, `session`, `auth`, `refresh` ([OWASP Cheat Sheet Series][6])\n\n* CSP and related:\n\n  * `Content-Security-Policy` header config (server/edge)\n  * `<meta http-equiv=\"Content-Security-Policy\" ...>`\n  * CSP containing `unsafe-inline` or `unsafe-eval`\n  * `require-trusted-types-for` / `trusted-types` directives ([MDN Web Docs][1])\n\n* Third-party scripts:\n\n  * `<script src=\"https://...\">` without `integrity=`\n  * Tag manager snippets and dynamic script injection code paths ([MDN Web Docs][12])\n\n\n* DOM clobbering gadgets:\n\n  * `window.<name> || ...` and `document.<name> || ...` patterns\n  * security-sensitive usage of `window`/`document` properties as config sources ([OWASP Cheat Sheet Series][8])\n\nAlways try to confirm:\n\n* data origin (untrusted vs trusted),\n* sink type (HTML parse, navigation, code execution, message handling, storage),\n* protective controls present (CSP, Trusted Types, sanitizers, strict allowlists, schema validation).\n\n---\n\n## 6) Sources (accessed 2026-01-27)\n\nPrimary standards / platform docs:\n\n* W3C Content Security Policy Level 2 (HTML `<meta>` delivery restrictions; unsupported directives in meta CSP): `https://www.w3.org/TR/CSP2/` ([W3C][3])\n* MDN: CSP Guide (strict CSP, nonces/hashes, `unsafe-inline`/`unsafe-eval`, eval blocking): `https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP` ([MDN Web Docs][10])\n* MDN: `<meta http-equiv>` (CSP via meta and warning about meta-based security headers): `https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/http-equiv` ([MDN Web Docs][1])\n* MDN: `frame-ancestors` (and note it’s not supported in `<meta>`): `https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors` ([MDN Web Docs][18])\n\nDOM XSS and dangerous sinks:\n\n* OWASP: DOM Based XSS Prevention Cheat Sheet (dangerous sinks + safe patterns like `textContent`): `https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][2])\n* MDN: `innerHTML` (security considerations): `https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML` ([MDN Web Docs][19])\n* MDN: `insertAdjacentHTML` (security considerations): `https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML` ([MDN Web Docs][20])\n* MDN: `document.write()` / `document.writeln()` (security considerations): `https://developer.mozilla.org/en-US/docs/Web/API/Document/write` and `https://developer.mozilla.org/en-US/docs/Web/API/Document/writeln` ([MDN Web Docs][13])\n\nURL scheme hazards:\n\n* MDN: `javascript:` URLs (execution on navigation; discouraged; references `window.location`): `https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/javascript` ([MDN Web Docs][4])\n\nTrusted Types:\n\n* W3C: Trusted Types spec (DOM XSS sinks include `Element.innerHTML` and `Location.href` setters; goals and limitations): `https://www.w3.org/TR/trusted-types/` ([W3C][15])\n* MDN: `require-trusted-types-for` directive: `https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for` ([MDN Web Docs][11])\n* MDN: `trusted-types` directive: `https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/trusted-types` ([MDN Web Docs][16])\n\nCross-window messaging:\n\n* MDN: `window.postMessage` (security guidance: specify targetOrigin; validate origin): `https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage` ([MDN Web Docs][5])\n* OWASP: HTML5 Security Cheat Sheet (Web Messaging guidance: explicit origin, strict checks, no `innerHTML`): `https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][6])\n\nThird-party scripts and integrity:\n\n* OWASP: Third Party JavaScript Management Cheat Sheet (risks and mitigations including SRI/mirroring): `https://cheatsheetseries.owasp.org/cheatsheets/Third_Party_Javascript_Management_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][7])\n* MDN: Subresource Integrity overview: `https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity` ([MDN Web Docs][12])\n* W3C: Subresource Integrity spec: `https://www.w3.org/TR/sri-2/` ([W3C][21])\n\nDOM clobbering:\n\n* OWASP: DOM Clobbering Prevention Cheat Sheet (named property access risk; example attacks involving `location.assign` and `javascript:`): `https://cheatsheetseries.owasp.org/cheatsheets/DOM_Clobbering_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][8])\n\n[1]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/http-equiv \"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/http-equiv\"\n[2]: https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html\"\n[3]: https://www.w3.org/TR/CSP2/ \"Content Security Policy Level 2\"\n[4]: https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/javascript \"javascript: URLs - URIs | MDN\"\n[5]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage \"https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage\"\n[6]: https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html\"\n[7]: https://cheatsheetseries.owasp.org/cheatsheets/Third_Party_Javascript_Management_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/Third_Party_Javascript_Management_Cheat_Sheet.html\"\n[8]: https://cheatsheetseries.owasp.org/cheatsheets/DOM_Clobbering_Prevention_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/DOM_Clobbering_Prevention_Cheat_Sheet.html\"\n[9]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/noopener \"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/noopener\"\n[10]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP \"https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP\"\n[11]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for \"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for\"\n[12]: https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity \"https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity\"\n[13]: https://developer.mozilla.org/en-US/docs/Web/API/Document/write \"https://developer.mozilla.org/en-US/docs/Web/API/Document/write\"\n[14]: https://developer.mozilla.org/en-US/docs/Web/API/Document/writeln \"https://developer.mozilla.org/en-US/docs/Web/API/Document/writeln\"\n[15]: https://www.w3.org/TR/trusted-types/ \"https://www.w3.org/TR/trusted-types/\"\n[16]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/trusted-types \"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/trusted-types\"\n[18]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors \"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors\"\n[19]: https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML \"https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML\"\n[20]: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML \"https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML\"\n[21]: https://www.w3.org/TR/sri-2/ \"https://www.w3.org/TR/sri-2/\"\n"
  },
  {
    "path": "skills/.curated/security-best-practices/references/javascript-jquery-web-frontend-security.md",
    "content": "# jQuery Frontend Security Spec (jQuery 4.0.x, modern browsers)\n\nThis document is designed as a **security spec** that supports:\n\n1. **Secure-by-default code generation** for new jQuery-based frontend code.\n2. **Security review / vulnerability hunting** in existing jQuery-based code (passive “notice issues while working” and active “scan the repo and report findings”).\n\nIt is intentionally written as a set of **normative requirements** (“MUST/SHOULD/MAY”) plus **audit rules** (what bad patterns look like, how to detect them, and how to fix/mitigate them).\n\n---\n\n## 0) Safety, boundaries, and anti-abuse constraints (MUST FOLLOW)\n\n* MUST NOT request, output, log, or commit secrets (API keys, passwords, private keys, session tokens, refresh tokens, CSRF tokens, session cookies).\n* MUST treat the browser as an attacker-controlled environment:\n\n  * Frontend checks (UI gating, “disable button”, hidden fields, client-side validation) MUST NOT be treated as authorization or a security boundary.\n  * Server-side authorization and validation MUST exist even if frontend is “correct”.\n* MUST NOT “fix” security by disabling protections (e.g., relaxing CSP to allow `unsafe-inline`, enabling JSONP “because it works”, adding broad CORS, disabling sanitization, suppressing security checks).\n* MUST provide evidence-based findings during audits: cite file paths, code snippets, and relevant configuration values.\n* MUST treat uncertainty honestly: if a protection might exist at the edge (CDN/WAF/reverse proxy headers like CSP), report it as “not visible in repo; verify at runtime/config”.\n\n---\n\n## 1) Operating modes\n\n### 1.1 Generation mode (default)\n\nWhen asked to write new jQuery code or modify existing jQuery code:\n\n* MUST follow every **MUST** requirement in this spec.\n* SHOULD follow every **SHOULD** requirement unless the user explicitly says otherwise.\n* MUST prefer safe-by-default patterns: text insertion, DOM node construction, allowlists, and proven sanitization libraries over custom escaping.\n* MUST avoid introducing new risky sinks (HTML string building, dynamic script loading, JSONP, inline script/event-handler attributes, unsafe URL assignment, unsafe object merging).\n\n### 1.2 Passive review mode (always on while editing)\n\nWhile working anywhere in a repo that uses jQuery (even if the user did not ask for a security scan):\n\n* MUST “notice” violations of this spec in touched/nearby code.\n* SHOULD mention issues as they come up, with a brief explanation + safe fix.\n\n### 1.3 Active audit mode (explicit scan request)\n\nWhen the user asks to “scan”, “audit”, or “hunt for vulns”:\n\n* MUST systematically search the codebase for violations of this spec.\n* MUST output findings in the structured format (see §2.3).\n\nRecommended audit order:\n\n1. jQuery sourcing, versions, and dependency hygiene (script tags, lockfiles, CDN usage, SRI).\n2. CSP / Trusted Types / security headers posture (in repo and at runtime if observable).\n3. DOM XSS: untrusted sources → jQuery sinks (`.html`, `.append`, `$(\"<…>\")`, `.load`, etc.).\n4. Script execution sinks: JSONP, `dataType:\"script\"`, `$.getScript`, dynamic `<script>` insertion.\n5. URL/attribute assignment (`href`, `src`, `style`, `on*` attributes).\n6. Prototype pollution / unsafe object merging (`$.extend` patterns).\n7. AJAX auth patterns + CSRF for cookie-based sessions.\n8. Third-party plugins and untrusted content rendering paths (comments, WYSIWYG, markdown-to-HTML).\n\n---\n\n## 2) Definitions and review guidance\n\n### 2.1 Untrusted input (treat as attacker-controlled unless proven otherwise)\n\nExamples include:\n\n* Any data from the server that originates from users (user profiles, comments, “display name”, rich text, filenames).\n* Data from third-party APIs or services.\n* Browser-controlled sources:\n\n  * `location.href`, `location.search`, `location.hash`\n  * `document.URL`, `document.baseURI`, `document.referrer`\n  * `window.name`\n  * `localStorage` / `sessionStorage`\n  * `postMessage` event data (unless strict origin and schema validation exists)\n  * Any DOM content that could have been injected previously (stored XSS)\n\n### 2.2 High-risk “sinks” in jQuery contexts\n\nA sink is a code path where untrusted input can become interpreted as executable code or HTML.\n\nKey jQuery sink categories:\n\n* HTML insertion / parsing:\n\n  * DOM manipulation methods that accept HTML strings such as `.html()`, `.append()`, and related methods (see CVE notes below). ([NVD][1])\n  * `$(htmlString)` (when the argument can be interpreted as HTML markup).\n  * `jQuery.parseHTML(html, …, keepScripts)` especially with `keepScripts=true`. ([jQuery API][2])\n  * `.load(url)` (loads HTML into DOM; has special script execution behavior). ([jQuery API][3])\n* Script execution / dynamic code loading:\n\n  * `$.getScript()` / `$.ajax({ dataType: \"script\" })` (executes fetched JavaScript). ([jQuery API][4])\n  * JSONP (`dataType: \"jsonp\"` or implicit JSONP behavior) (executes remote JavaScript as a response). ([jQuery API][5])\n  * `eval`, `new Function`, `setTimeout(\"…\")`, `setInterval(\"…\")`, `$.globalEval` (if present)\n* Dangerous attribute assignment:\n\n  * Assigning untrusted strings to `href`, `src`, `srcdoc`, `style`, or event-handler attributes (`onload`, `onclick`, etc.)\n  * `javascript:` URLs are particularly dangerous and discouraged. ([MDN Web Docs][6])\n\n### 2.3 Required audit finding format\n\nFor each issue found, output:\n\n* Rule ID:\n* Severity: Critical / High / Medium / Low\n* Location: file path + function/component + line(s)\n* Evidence: the exact code/config snippet\n* Impact: what could go wrong, who can exploit it\n* Fix: safe change (prefer minimal diff)\n* Mitigation: defense-in-depth if immediate fix is hard\n* False positive notes: what to verify if uncertain\n\n---\n\n## 3) Secure baseline: minimum production configuration (MUST in production)\n\nThis is the smallest “production baseline” that prevents common jQuery-related security failures.\n\n### 3.1 Use a supported, patched jQuery version (MUST)\n\n* MUST use a supported jQuery major version and keep it updated.\n* As of 2026-01-27, the jQuery project ships jQuery 4.0.0 as the latest major release. ([blog.jquery.com][7])\n* If you must support very old browsers (notably IE < 11), jQuery 4 does not support them and you may need to stay on jQuery 3.x; treat this as a higher risk posture and patch aggressively. ([blog.jquery.com][7])\n\n### 3.2 Load jQuery safely (MUST)\n\n* MUST load jQuery only from:\n\n  * Your own build pipeline (bundled via npm/yarn + lockfile), or\n  * The official jQuery CDN / a trusted CDN with Subresource Integrity (SRI) enabled.\n* If loading from a CDN, SHOULD use SRI (`integrity`) and correct `crossorigin` settings; the jQuery project explicitly supports and recommends SRI on its CDN. (Retrieved from [jquery.com][8])\n\n### 3.3 CSP + Trusted Types (SHOULD, and MUST where available/required by policy)\n\n* SHOULD deploy a Content Security Policy (CSP) that reduces XSS impact (especially `script-src` restrictions and avoiding `unsafe-inline`). If not done through HTTP server, this can be done through the `<meta http-equiv=\"Content-Security-Policy\" content=\"...\">` tag. ([OWASP Cheat Sheet Series][9]) NOTE: It is most important to set the CSP's script-src. All other directives are not as important and can generally be excluded for the ease of development.\n* SHOULD consider Trusted Types as a strong defense-in-depth against DOM XSS. ([W3C][10])\n* If you deploy the CSP directive `require-trusted-types-for`, then code MUST route DOM-injection through Trusted Types policies. ([MDN Web Docs][11])\n* Note: jQuery 4.0 explicitly added Trusted Types support so that TrustedHTML can be used with jQuery manipulation methods without violating `require-trusted-types-for`. ([blog.jquery.com][7])\n\n### 3.4 Security headers and cookie posture (defense in depth; SHOULD)\n\nEven though these are typically set server-side, they materially reduce the blast radius of jQuery-related mistakes. However if the context is only the frontend web application, these cannot be acted on.\n\n* SHOULD set common security headers (CSP, `X-Content-Type-Options: nosniff`, clickjacking protection via `frame-ancestors` / `X-Frame-Options`, `Referrer-Policy`). ([OWASP Cheat Sheet Series][12])\n* SHOULD avoid storing long-lived secrets/tokens in places accessible to JavaScript (like `localStorage`) unless the threat model explicitly accepts “XSS == account takeover”. This is not jQuery-specific, but jQuery-heavy DOM manipulation increases the chance of DOM XSS regressions; reduce the payoff.\n\n---\n\n## 4) Rules (generation + audit)\n\nEach rule contains: required practice, insecure patterns, detection hints, and remediation.\n\n### JQ-SUPPLY-001: jQuery MUST be patched; do not run known vulnerable versions\n\nSeverity: Medium (High if internet-facing app AND version is known-vulnerable)\n\nNOTE: Before performing an upgrade, get concent from the user and try to understand if they have reasons to keep it back. Upgrading can break applications in unexpected ways. Report and recommend upgrades rather than just performing them.\n\nRequired:\n\n* MUST NOT use jQuery versions with known high-impact vulnerabilities when a patched version exists.\n* MUST upgrade past:\n\n  * CVE-2019-11358 (prototype pollution in jQuery before 3.4.0). ([NVD][13])\n  * CVE-2020-11022 / CVE-2020-11023 (XSS risks in DOM manipulation methods when handling untrusted HTML; patched in 3.5.0). ([NVD][1])\n\nInsecure patterns:\n\n* Script tags or package manifests referencing old jQuery (e.g., `jquery-1.*`, `jquery-2.*`, `jquery-3.3.*`, `jquery-3.4.*`, `jquery-3.4.1`, etc.).\n* Bundled vendor directories containing old minified jQuery without an upgrade path.\n\nDetection hints:\n\n* Search HTML/templates for `jquery-` and parse version strings.\n* Check `package.json`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`.\n* Check `vendor/`, `public/`, `static/`, `assets/`, `wwwroot/` for `jquery*.js`.\n\nFix:\n\n* Upgrade to current jQuery (prefer latest stable major; as of 2026-01-27, 4.0.0 is current). ([blog.jquery.com][7])\n* If upgrade is constrained, at minimum upgrade beyond the CVE thresholds and add compensating controls (strong CSP, strict sanitization, remove risky APIs like JSONP, remove deep-extend of untrusted objects).\n\nNotes:\n\n* If a product requirement forces old versions, report as “accepted risk requiring compensating controls”.\n\n---\n\n### JQ-SUPPLY-002: Third-party script loading SHOULD use integrity and trusted origins\n\nSeverity: High\n\nRequired:\n\n* MUST load jQuery and plugins only from trusted origins.\n* If loaded from CDN, SHOULD use SRI (`integrity`) and correct `crossorigin` handling. ([jquery.com][8])\n\nInsecure patterns:\n\n* `<script src=\"https://…/jquery.min.js\"></script>` with no `integrity`.\n* Loading jQuery from random third-party CDNs without an explicit trust decision.\n\nDetection hints:\n\n* Scan HTML for `<script src=` and check for `integrity=` + `crossorigin=`.\n* Identify dynamic script insertion with untrusted URLs (see JQ-EXEC-001).\n\nFix:\n\n* Prefer bundling via npm + lockfile.\n* If using CDN, copy official script tag (jQuery CDN supports SRI). ([jquery.com][8])\n\nNote: If unable to get the correct SRI tag, skip this step but tell the user. If you end up using the wrong one the app will not function. In that case remove it and inform the user.\n\n---\n\n### JQ-XSS-001: Untrusted data MUST NOT be inserted as HTML via jQuery DOM-manipulation methods\n\nSeverity: High (if attacker-controlled content reaches these sinks)\n\nRequired:\n\n* MUST treat any HTML string insertion as a code execution boundary.\n* MUST use safe alternatives for untrusted text:\n\n  * `.text(untrusted)` (text, not HTML). ([jQuery API][14])\n  * `.val(untrusted)` for form fields. ([jQuery API][15])\n  * Create elements and set text/attributes safely instead of concatenating HTML strings.\n\nInsecure patterns (examples):\n\n* `$(selector).html(untrusted)`\n* `$(selector).append(untrusted)`\n* `$(selector).before(untrusted)` / `.after(untrusted)` / `.replaceWith(untrusted)` / `.wrap(untrusted)` (and similar)\n* Building markup: `\"<div>\" + untrusted + \"</div>\"` then passing to jQuery\n\nDetection hints:\n\n* Grep for: `.html(`, `.append(`, `.prepend(`, `.before(`, `.after(`, `.replaceWith(`, `.wrap(`, `.wrapAll(`, `.wrapInner(`\n* Trace dataflow into these calls from sources in §2.1.\n\nFix:\n\n* Replace with `.text()` / `.val()` or node construction:\n\n  * `const $el = $(\"<span>\").text(untrusted); container.append($el);`\n* If the output must contain limited markup, see JQ-XSS-002 (sanitization).\n\nNotes:\n\n* Older jQuery versions had additional edge cases even when attempting sanitization; patched in 3.5.0+. Still: never rely on “string sanitization” alone—prefer structured creation or proven sanitizers. ([GitHub][16])\n\n---\n\n### JQ-XSS-002: If rendering user-controlled HTML is required, it MUST be sanitized with a proven HTML sanitizer\n\nSeverity: Medium (High if rich HTML is attacker-controlled and sanitizer is weak/misconfigured)\n\nRequired:\n\n* MUST NOT “roll your own” HTML sanitizer with regexes.\n* If user-controlled HTML must be displayed (e.g., rich text comments), MUST sanitize using a well-maintained HTML sanitizer and a restrictive allowlist.\n\n  * DOMPurify is a common choice; use conservative configuration and keep it updated. ([GitHub][17])\n  * Where available, MAY consider the browser HTML Sanitizer API (note: limited browser availability). ([MDN Web Docs][18])\n* SHOULD pair sanitization with CSP and, where feasible, Trusted Types for defense in depth. ([OWASP Cheat Sheet Series][9])\n\nInsecure patterns:\n\n* Regex-based “strip `<script>`” or “escape `<`” attempts followed by `.html()` insertion.\n* DOMPurify (or similar) configured to allow overly broad tags/attributes, or configuration that’s not reviewed.\n\nDetection hints:\n\n* Search for “sanitize” helper functions, regex replacing `<`/`>` patterns, or “allow all tags” configs.\n* Identify features that render user-generated “rich text” or “custom HTML”.\n* Check if sanitizer results are inserted with `.html()` or equivalent sinks.\n\nFix:\n\n* Introduce a sanitizer with strict allowlist.\n* Centralize the “sanitize then inject” pattern into a single reviewed module.\n* Add regression tests covering representative malicious inputs (don’t store payloads in logs or telemetry).\n\nFalse positive notes:\n\n* If content is guaranteed trusted (e.g., compiled templates shipped by you), document the trust boundary and why it is not attacker-controlled.\n\n---\n\n### JQ-XSS-003: `$(untrustedString)` and `jQuery.parseHTML` MUST NOT process attacker-controlled markup\n\nSeverity: High (if attacker-controlled)\n\nRequired:\n\n* MUST NOT pass attacker-controlled strings to `$()` when they might be interpreted as HTML.\n* MUST treat `jQuery.parseHTML(html, …, keepScripts)` as a high-risk primitive; keepScripts MUST be `false` for any untrusted input. ([jQuery API][2])\n\nInsecure patterns:\n\n* `const $node = $(untrusted);`\n* `$.parseHTML(untrusted, /* context */, true)` (scripts preserved)\n\nDetection hints:\n\n* Search for `$(` calls where the argument is not a static selector or static markup.\n* Search for `$.parseHTML(` and inspect the `keepScripts` argument.\n\nFix:\n\n* Use DOM creation with constant tag names and `.text()` for untrusted values.\n* If parsing HTML is necessary, sanitize first (JQ-XSS-002) and keep scripts disabled.\n\n---\n\n### JQ-XSS-004: `.load()` MUST be treated as an HTML+script injection surface\n\nSeverity: Medium (High if URL/content is attacker-controlled)\n\nRequired:\n\n* MUST NOT use `.load()` with attacker-controlled URLs or attacker-controlled HTML fragments.\n* MUST understand jQuery `.load()` script behavior:\n\n  * Without a selector in the URL, content is passed to `.html()` before scripts are removed, which can execute scripts. ([jQuery API][3])\n* SHOULD prefer `fetch()`/XHR to retrieve data, then render with safe DOM creation or sanitize explicitly.\n\nInsecure patterns:\n\n* `$(\"#target\").load(untrustedUrl)`\n* `$(\"#target\").load(\"/path?param=\" + untrusted)`\n\nDetection hints:\n\n* Search for `.load(` across JS/TS files.\n* Identify whether a selector is appended to the URL (the behavior differs). ([jQuery API][3])\n* Trace whether the URL can be influenced by user input.\n\nFix:\n\n* Replace `.load()` with:\n\n  * `fetch()` to retrieve JSON, then render via `.text()` / node construction, or\n  * `fetch()` to retrieve HTML, sanitize it, then inject.\n* If `.load()` must remain, ensure the URL is constant or strictly allowlisted and the returned content is trusted.\n\n---\n\n### JQ-EXEC-001: Dynamic script execution and script fetching MUST NOT be reachable from untrusted input\n\nSeverity: High\n\nRequired:\n\n* MUST NOT fetch-and-execute scripts from untrusted or user-influenced URLs.\n* MUST treat these as code execution primitives:\n\n  * `$.getScript(url)` executes the fetched script in the global context. ([jQuery API][4])\n  * `$.ajax({ dataType: \"script\" })` and other script-typed requests that execute responses.\n* SHOULD remove these patterns unless there is a strong, reviewed justification.\n\nInsecure patterns:\n\n* `$.getScript(untrustedUrl)`\n* `$.ajax({ url: untrustedUrl, dataType: \"script\" })`\n* Dynamic `<script src=...>` injection where `src` is derived from untrusted input.\n\nDetection hints:\n\n* Search for `getScript(`, `dataType: \"script\"`, `globalEval`, `eval`, `new Function`.\n* Look for “plugin loader” or “theme loader” features that accept URLs.\n\nFix:\n\n* Bundle scripts at build time.\n* If runtime-loading is required, restrict to allowlisted, versioned, integrity-checked assets (and ideally still avoid runtime code loading).\n\n---\n\n### JQ-AJAX-001: JSONP MUST be disabled unless the endpoint is fully trusted (and even then, avoid)\n\nSeverity: Medium (High if attacker can influence URL/endpoint)\n\nRequired:\n\n* MUST NOT use JSONP for untrusted endpoints because it executes JavaScript responses.\n* When using `$.ajax`, MUST explicitly disable JSONP for non-fully-trusted targets; jQuery’s own docs recommend setting `jsonp: false` “for security reasons” if you don’t trust the target. ([jQuery API][5])\n* SHOULD prefer CORS with JSON (`dataType: \"json\"`) and explicit origin allowlists server-side.\n\nInsecure patterns:\n\n* `dataType: \"jsonp\"`\n* URLs containing `callback=?` or patterns that trigger JSONP behavior. callback arguments are historically XSS vectors.\n* `$.get(untrustedUrl)` without pinning `dataType` and disabling JSONP (risk depends on options and jQuery behavior)\n\nDetection hints:\n\n* Search for `jsonp`, `dataType: \"jsonp\"`, `callback=?`.\n* Search for cross-domain AJAX where the URL is not hard-coded or allowlisted.\n\nFix:\n\n* Use JSON over HTTPS with CORS configured server-side.\n* Set:\n\n  * `dataType: \"json\"`\n  * `jsonp: false` (defense in depth when URL might be ambiguous) ([jQuery API][5])\n\n---\n\n### JQ-AJAX-002: State-changing AJAX requests using cookie auth MUST be CSRF-protected\n\nSeverity: High\n\nNOTE: This only matters when using cookie based auth. If the request use Authorization header, there is no CSRF potential.\n\nRequired:\n\n* If authentication uses cookies, MUST protect state-changing requests (POST/PUT/PATCH/DELETE) against CSRF.\n* SHOULD use server-verified CSRF tokens; for AJAX calls, tokens are commonly sent in a custom header. ([OWASP Cheat Sheet Series][19])\n* MUST NOT treat “it’s an AJAX request” as CSRF protection by itself.\n\nInsecure patterns:\n\n* `$.post(\"/transfer\", {...})` or `$.ajax({ method: \"POST\", ... })` with cookie auth and no CSRF token/header.\n* “CSRF protection” that only checks for `X-Requested-With` (defense-in-depth only, not primary).\n\nDetection hints:\n\n* Enumerate state-changing AJAX calls and locate whether they include CSRF tokens.\n* Identify how the server expects CSRF validation (meta tag, cookie-to-header double submit, synchronizer token, etc.).\n\nFix:\n\n* Add CSRF token inclusion in a centralized place, e.g., `$.ajaxSetup({ headers: { \"X-CSRF-Token\": token } })`, and ensure server verifies.\n* Follow OWASP CSRF guidance for token properties and validation. ([OWASP Cheat Sheet Series][19])\n\nFalse positive notes:\n\n* If auth is not cookie-based (e.g., Authorization header bearer token) CSRF risk is different; verify actual auth mechanism.\n\n---\n\n### JQ-ATTR-001: Untrusted values MUST NOT be written into dangerous attributes without validation/allowlisting\n\nSeverity: Low (High for events like onclick)\n\nRequired:\n\n* MUST validate/allowlist URLs written into `href`, `src`, `action`, etc.\n* MUST block dangerous schemes; `javascript:` URLs are discouraged because they can execute code. ([MDN Web Docs][6])\n* MUST NOT set event-handler attributes (`onclick`, `onerror`, etc.) from strings.\n* SHOULD avoid writing untrusted strings into `style` attributes; prefer toggling predefined CSS classes.\n\nInsecure patterns:\n\n* `$(\"a\").attr(\"href\", untrustedUrl)`\n* `$(\"img\").attr(\"src\", untrustedUrl)`\n* `$(el).attr(\"style\", untrustedCss)`\n* `$(el).attr(\"onclick\", untrustedJs)`\n\nDetection hints:\n\n* Search for `.attr(\"href\"`, `.attr(\"src\"`, `.attr(\"style\"`, `.prop(\"href\"`, `.prop(\"src\"`.\n* Trace whether inputs come from URL params, server JSON, DOM, or storage.\n\nFix:\n\n* Parse and validate URLs with `new URL(value, location.origin)` and allowlist protocols (`https:` etc.) and hostnames when needed.\n* For navigation targets, prefer relative paths you construct rather than full URLs.\n* Replace `style` strings with `addClass/removeClass` using predefined class names.\n\n---\n\n### JQ-SELECTOR-001: User-controlled selector fragments MUST be escaped with `jQuery.escapeSelector`\n\nSeverity: Medium (can become High if it enables wrong-element selection in security-relevant UI)\n\nRequired:\n\n* If you must select by an ID/class that can contain special CSS characters, SHOULD use `jQuery.escapeSelector()` (available in jQuery 3.0+). ([jQuery API][20])\n* MUST NOT concatenate raw attacker-controlled strings into selector expressions.\n\nInsecure patterns:\n\n* `$(\"#\" + untrustedId)`\n* `$(\"[data-id='\" + untrusted + \"']\")` (especially without strict quoting/escaping)\n\nDetection hints:\n\n* Search for `\"#\" +`, `\". \" +`, or template strings used inside `$(` selectors.\n* Look for “select by user-supplied id”.\n\nFix:\n\n* `$(\"#\" + $.escapeSelector(untrustedId))` ([jQuery API][20])\n* Prefer stable internal IDs over user-derived selectors.\n\nNotes:\n\n* This is often “robustness”, but it can become security-relevant if incorrect selection causes UI to reveal/modify the wrong data or skip security-related prompts.\n\n---\n\n### JQ-PROTOTYPE-001: Do not deep-merge untrusted objects; prevent prototype pollution\n\nSeverity: Medium\n\nRequired:\n\n* MUST NOT deep-merge (`$.extend(true, …)`) attacker-controlled objects into application objects without filtering dangerous keys.\n* MUST ensure jQuery is >= 3.4.0 to avoid CVE-2019-11358 prototype pollution behavior. ([NVD][13])\n\nInsecure patterns:\n\n* `$.extend(true, target, untrustedObj)`\n* `$.extend(true, {}, defaults, untrustedObj)` where untrustedObj comes from URL/JSON/storage\n\nDetection hints:\n\n* Search for `$.extend(true` and inspect sources of merged objects.\n* Search for “merge options” / “apply config” patterns using untrusted JSON.\n\nFix:\n\n* Prefer:\n\n  * Shallow merges with an allowlisted set of keys, or\n  * A safe merge helper that explicitly rejects `__proto__`, `prototype`, `constructor`, and nested occurrences.\n* Keep jQuery patched.\n\n---\n\n### JQ-CSP-001: CSP and Trusted Types SHOULD be used to make DOM XSS harder to introduce and exploit\n\nSeverity: Medium\n\nRequired:\n\n* SHOULD deploy CSP as defense-in-depth against XSS. ([OWASP Cheat Sheet Series][9])\n* If enabling Trusted Types (`require-trusted-types-for`), MUST ensure DOM injection goes through Trusted Types policies. ([MDN Web Docs][11])\n* When using jQuery 4, SHOULD take advantage of its Trusted Types support (TrustedHTML inputs). ([blog.jquery.com][7])\n\nInsecure patterns:\n\n* “Fixing” a jQuery feature by weakening CSP (`script-src 'unsafe-inline'` / `'unsafe-eval'`) without a compensating plan.\n* No CSP on applications that render user content or manipulate DOM heavily.\n\nDetection hints:\n\n* Look for CSP headers (server configs, framework middleware, meta tags).\n* If not visible in repo, flag as “verify at edge/runtime”.\n\nFix:\n\n* Add CSP incrementally; start by eliminating inline scripts and inline event handlers, then tighten `script-src`.\n* Add Trusted Types where supported and feasible.\n\n---\n\n## 5) Practical scanning heuristics (how to “hunt”)\n\nWhen actively scanning, use these high-signal patterns:\n\n* jQuery version / sourcing:\n\n  * `jquery-*.js` in `vendor/` or `static/`\n  * `package.json` dependency `jquery` pinned to old versions\n  * CDN script tags lacking `integrity`/`crossorigin` ([jquery.com][8])\n* HTML injection sinks (DOM XSS):\n\n  * `.html(`, `.append(`, `.prepend(`, `.before(`, `.after(`, `.replaceWith(`, `.wrap(`\n  * `$(` where argument might be HTML / template strings\n  * `$.parseHTML(` especially with `keepScripts=true` ([jQuery API][2])\n  * `.load(` (and whether selector is appended; script behavior differs) ([jQuery API][3])\n* Script execution / dynamic code:\n\n  * `$.getScript(`, `dataType: \"script\"` ([jQuery API][4])\n  * `dataType: \"jsonp\"` or `jsonp:` usage; `callback=?` patterns ([jQuery API][5])\n  * `eval`, `new Function`, `setTimeout(\"…\")`, `$.globalEval`\n* Dangerous attribute writes:\n\n  * `.attr(\"href\", …)`, `.attr(\"src\", …)`, `.attr(\"style\", …)`\n  * Any assignment of `javascript:`-like schemes or suspicious URL construction ([MDN Web Docs][6])\n* Selector construction:\n\n  * `$(\"#\" + user)` and similar; fix via `$.escapeSelector` ([jQuery API][20])\n* Prototype pollution:\n\n  * `$.extend(true, …, userObj)`; ensure jQuery >= 3.4.0 and filter dangerous keys ([NVD][13])\n* CSRF posture for AJAX:\n\n  * `$.post(` / `$.ajax({ method: ... })` with cookies and no CSRF token/header ([OWASP Cheat Sheet Series][19])\n* Defense-in-depth:\n\n  * Absence of CSP/security headers in configs (or not visible; require runtime verification) ([OWASP Cheat Sheet Series][12])\n\nAlways try to confirm:\n\n* data origin (untrusted vs trusted)\n* sink type (HTML insertion / script execution / attribute / selector / object merge)\n* protective controls present (sanitizer, allowlists, CSP, Trusted Types, CSRF validation)\n\n---\n\n## 6) Sources (accessed 2026-01-27)\n\nPrimary jQuery project documentation and release notes:\n\n* jQuery 4.0.0 release notes (Trusted Types/CSP changes; version info): `https://blog.jquery.com/2026/01/17/jquery-4-0-0/`. ([blog.jquery.com][7])\n* Download jQuery (latest version info; CDN + SRI guidance): `https://jquery.com/download/`. ([jquery.com][8])\n* jQuery API: `.html()`: `https://api.jquery.com/html/`. ([jQuery API][21])\n* jQuery API: `.text()`: `https://api.jquery.com/text/`. ([jQuery API][14])\n* jQuery API: `.append()`: `https://api.jquery.com/append/`. ([jQuery API][22])\n* jQuery API: `.load()` (script execution behavior): `https://api.jquery.com/load/`. ([jQuery API][3])\n* jQuery API: `jQuery.parseHTML(…, keepScripts)`: `https://api.jquery.com/jQuery.parseHTML/`. ([jQuery API][2])\n* jQuery API: `$.ajax()` (`jsonp: false` security note): `https://api.jquery.com/jQuery.ajax/`. ([jQuery API][5])\n* jQuery API: `$.getScript()` (executes script): `https://api.jquery.com/jQuery.getScript/`. ([jQuery API][4])\n* jQuery API: `jQuery.escapeSelector()`: `https://api.jquery.com/jQuery.escapeSelector/`. ([jQuery API][20])\n\njQuery vulnerabilities / advisories:\n\n* NVD CVE-2019-11358 (prototype pollution; jQuery < 3.4.0): `https://nvd.nist.gov/vuln/detail/CVE-2019-11358`. ([NVD][13])\n* NVD CVE-2020-11022 (XSS risk in DOM manipulation methods; patched in 3.5.0): `https://nvd.nist.gov/vuln/detail/CVE-2020-11022`. ([NVD][1])\n* NVD CVE-2020-11023 (XSS risk involving `<option>`; patched in 3.5.0): `https://nvd.nist.gov/vuln/detail/CVE-2020-11023`. ([NVD][23])\n* GitHub Security Advisory GHSA-gxr4-xjj5-5px2 (jQuery htmlPrefilter XSS; patched in 3.5.0): `https://github.com/jquery/jquery/security/advisories/GHSA-gxr4-xjj5-5px2`. ([GitHub][16])\n\nOWASP Cheat Sheet Series (web app security foundations relevant to jQuery usage):\n\n* XSS Prevention: `https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html`. ([OWASP Cheat Sheet Series][24])\n* DOM-based XSS Prevention: `https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html`. ([OWASP Cheat Sheet Series][25])\n* CSRF Prevention: `https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html`. ([OWASP Cheat Sheet Series][19])\n* HTTP Security Headers: `https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html`. ([OWASP Cheat Sheet Series][12])\n* Content Security Policy Cheat Sheet: `https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html`. ([OWASP Cheat Sheet Series][9])\n\nBrowser/platform references (SRI, CSP, Trusted Types, and dangerous URL schemes):\n\n* MDN: Subresource Integrity (SRI): `https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity`. ([MDN Web Docs][26])\n* W3C: SRI specification: `https://www.w3.org/TR/sri-2/`. ([W3C][27])\n* MDN: CSP guide: `https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP`. ([MDN Web Docs][28])\n* MDN: `require-trusted-types-for` directive: `https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for`. ([MDN Web Docs][11])\n* MDN: Trusted Types API: `https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API`. ([MDN Web Docs][29])\n* W3C: Trusted Types specification: `https://www.w3.org/TR/trusted-types/`. ([W3C][10])\n* MDN: `javascript:` URL scheme warning: `https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/javascript`. ([MDN Web Docs][6])\n* DOMPurify project documentation: `https://github.com/cure53/DOMPurify`. ([GitHub][17])\n\n[1]: https://nvd.nist.gov/vuln/detail/cve-2020-11022?utm_source=chatgpt.com \"CVE-2020-11022 Detail - NVD\"\n[2]: https://api.jquery.com/jQuery.parseHTML/?utm_source=chatgpt.com \"jQuery.parseHTML()\"\n[3]: https://api.jquery.com/load/?utm_source=chatgpt.com \".load() | jQuery API Documentation\"\n[4]: https://api.jquery.com/jQuery.getScript/?utm_source=chatgpt.com \"jQuery.getScript()\"\n[5]: https://api.jquery.com/jQuery.ajax/?utm_source=chatgpt.com \"jQuery.ajax()\"\n[6]: https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/javascript?utm_source=chatgpt.com \"javascript: URLs - URIs - MDN Web Docs\"\n[7]: https://blog.jquery.com/2026/01/17/jquery-4-0-0/ \"jQuery 4.0.0 | Official jQuery Blog\"\n[8]: https://jquery.com/download/ \"Download jQuery | jQuery\"\n[9]: https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html?utm_source=chatgpt.com \"Content Security Policy - OWASP Cheat Sheet Series\"\n[10]: https://www.w3.org/TR/trusted-types/?utm_source=chatgpt.com \"Trusted Types\"\n[11]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for?utm_source=chatgpt.com \"Content-Security-Policy: require-trusted-types-for directive\"\n[12]: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html?utm_source=chatgpt.com \"HTTP Security Response Headers Cheat Sheet\"\n[13]: https://nvd.nist.gov/vuln/detail/cve-2019-11358?utm_source=chatgpt.com \"CVE-2019-11358 Detail - NVD\"\n[14]: https://api.jquery.com/text/?utm_source=chatgpt.com \".text() | jQuery API Documentation\"\n[15]: https://api.jquery.com/val/?utm_source=chatgpt.com \".val() | jQuery API Documentation\"\n[16]: https://github.com/jquery/jquery/security/advisories/GHSA-gxr4-xjj5-5px2 \"Potential XSS vulnerability in jQuery.htmlPrefilter and related methods · Advisory · jquery/jquery · GitHub\"\n[17]: https://github.com/cure53/DOMPurify?utm_source=chatgpt.com \"DOMPurify - a DOM-only, super-fast, uber-tolerant XSS ...\"\n[18]: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API?utm_source=chatgpt.com \"HTML Sanitizer API - MDN Web Docs\"\n[19]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html?utm_source=chatgpt.com \"Cross-Site Request Forgery Prevention Cheat Sheet\"\n[20]: https://api.jquery.com/jQuery.escapeSelector/?utm_source=chatgpt.com \"jQuery.escapeSelector()\"\n[21]: https://api.jquery.com/html/?utm_source=chatgpt.com \".html() | jQuery API Documentation\"\n[22]: https://api.jquery.com/append/?utm_source=chatgpt.com \".append() | jQuery API Documentation\"\n[23]: https://nvd.nist.gov/vuln/detail/cve-2020-11023?utm_source=chatgpt.com \"CVE-2020-11023 Detail - NVD\"\n[24]: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html?utm_source=chatgpt.com \"Cross Site Scripting Prevention - OWASP Cheat Sheet Series\"\n[25]: https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html?utm_source=chatgpt.com \"DOM based XSS Prevention Cheat Sheet\"\n[26]: https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity?utm_source=chatgpt.com \"Subresource Integrity - Security - MDN Web Docs\"\n[27]: https://www.w3.org/TR/sri-2/?utm_source=chatgpt.com \"Subresource Integrity\"\n[28]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP?utm_source=chatgpt.com \"Content Security Policy (CSP) - HTTP - MDN Web Docs\"\n[29]: https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API?utm_source=chatgpt.com \"Trusted Types API - MDN Web Docs\"\n"
  },
  {
    "path": "skills/.curated/security-best-practices/references/javascript-typescript-nextjs-web-server-security.md",
    "content": "# Next.js (TypeScript/JavaScript) Web Security Spec (Next.js 16.1.x, Node.js 20.9+)\n\nThis document is designed as a **security spec** that supports:\n\n1. **Secure-by-default code generation** for new Next.js backend code (Route Handlers, API Routes, Server Actions, Proxy/Middleware).\n2. **Security review / vulnerability hunting** in existing Next.js repos (passive “notice issues while working” and active “scan the repo and report findings”).\n\nIt is intentionally written as a set of **normative requirements** (“MUST/SHOULD/MAY”) plus **audit rules** (what bad patterns look like, how to detect them, and how to fix/mitigate them).\n\nTarget scope: Next.js **16.1.x** (latest line shown in the App Router docs) ([Next.js][1]), running on Node.js **20.9+** (per Next.js system requirements). ([Next.js][2])\n\n---\n\n## 0) Safety, boundaries, and anti-abuse constraints (MUST FOLLOW)\n\n* MUST NOT request, output, log, or commit secrets (API keys, passwords, private keys, session cookies, OAuth tokens, `process.env` dumps, database URLs with credentials).\n* MUST NOT “fix” security by disabling protections (e.g., disabling origin checks, relaxing CORS to `*`, skipping authz checks, turning off cookie security flags, turning off CSP because it’s “hard”).\n* MUST provide **evidence-based findings** during audits: cite file paths, code snippets, and configuration values that justify each claim.\n* MUST treat uncertainty honestly: if a protection might exist in infrastructure (reverse proxy, CDN, WAF, platform headers), report it as “not visible in app code; verify at runtime/config”.\n* MUST assume all request-facing server code is reachable by attackers unless there is a clearly enforced auth boundary (not just “the UI doesn’t link to it”).\n* MUST treat TypeScript types as **non-security boundaries**: types do not validate runtime input; runtime checks are required. ([Next.js][3])\n\n---\n\n## 1) Operating modes\n\n### 1.1 Generation mode (default)\n\nWhen asked to write new Next.js code or modify existing code:\n\n* MUST follow every **MUST** requirement in this spec.\n* SHOULD follow every **SHOULD** requirement unless the user explicitly says otherwise.\n* MUST prefer safe-by-default APIs and proven libraries over custom security code.\n* MUST avoid introducing new risky sinks (dynamic code execution, unsafe redirects, serving user files as HTML, SSRF URL fetchers, building SQL strings, etc.).\n\n### 1.2 Passive review mode (always on while editing)\n\nWhile working anywhere in a Next.js repo (even if the user did not ask for a security scan):\n\n* MUST “notice” violations of this spec in touched/nearby code.\n* SHOULD mention issues as they come up, with a brief explanation + safe fix.\n\n### 1.3 Active audit mode (explicit scan request)\n\nWhen the user asks to “scan”, “audit”, or “hunt for vulns”:\n\n* MUST systematically search the codebase for violations of this spec.\n* MUST output findings in a structured format (see §2.3).\n\nRecommended audit order:\n\n1. Deployment entrypoints and environment (Dockerfiles, `package.json` scripts, hosting config).\n2. Next.js config (`next.config.*`), Proxy/Middleware, routing patterns.\n3. Authentication, sessions, cookies.\n4. CSRF protections and state-changing endpoints (Server Actions, Route Handlers, API Routes).\n5. XSS (React + CSP) and unsafe HTML rendering.\n6. Cache/data-leak hazards (static rendering + caching + “use cache”).\n7. File handling (uploads/downloads) and path traversal.\n8. Injection classes (SQL/ORM misuse, command execution, unsafe deserialization).\n9. Outbound requests (SSRF).\n10. Redirect handling (open redirects).\n11. CORS and security headers.\n\n---\n\n## 2) Definitions and review guidance\n\n### 2.1 Untrusted input (treat as attacker-controlled unless proven otherwise)\n\nIn Next.js backends, untrusted input includes:\n\nApp Router:\n\n* Route Handler params and request data:\n\n  * `context.params` (dynamic segments), search params (`request.url`, `new URL(request.url).searchParams`)\n  * `request.headers`, `request.cookies`\n  * `await request.json()`, `await request.formData()`, `await request.text()`\n* Dynamic APIs used in Server Components/Server Functions:\n\n  * `headers()` and `cookies()` values ([Next.js][4])\n\nPages Router:\n\n* `req.query`, `req.cookies`, `req.body` in `pages/api/*` handlers ([Next.js][3])\n\nPlus:\n\n* Anything from external systems (webhooks, third-party APIs, message queues)\n* Any persisted user content (DB rows) that originated from users\n\n### 2.2 State-changing request\n\nA request is state-changing if it can create/update/delete data, change auth/session state, trigger side effects (purchase, email send, webhook send), or initiate privileged actions.\n\nSpecial note for Next.js:\n\n* **Server Actions** are invoked via network requests and can mutate state; treat them as state-changing endpoints. ([Next.js][5])\n\n### 2.3 Required audit finding format\n\nFor each issue found, output:\n\n* Rule ID:\n* Severity: Critical / High / Medium / Low\n* Location: file path + function/route name + line(s)\n* Evidence: the exact code/config snippet\n* Impact: what could go wrong, who can exploit it\n* Fix: safe change (prefer minimal diff)\n* Mitigation: defense-in-depth if immediate fix is hard\n* False positive notes: what to verify if uncertain\n\n---\n\n## 3) Secure baseline: minimum production configuration (MUST in production)\n\nThis is the smallest “production baseline” that prevents common Next.js backend misconfigurations.\n\n### 3.1 Run Next.js in production mode (MUST)\n\n* MUST run `next build` + `next start` (or the managed platform equivalent), not `next dev`. Dev mode has different error/reporting behavior and is not designed for production exposure. ([Next.js][6])\n* MUST ensure `NODE_ENV=production` in production (Next.js defaults `NODE_ENV` based on command; verify the runtime environment). ([Next.js][7])\n\n### 3.2 Put a reverse proxy / edge layer in front when self-hosting (MUST for public internet)\n\n* If self-hosting, MUST place a reverse proxy (e.g., nginx) or equivalent edge layer in front of the Next.js server to handle malformed requests, slow attacks, payload size limits, rate limiting, and similar concerns. ([Next.js][8])\n\n### 3.3 Baseline header/cookie posture (SHOULD)\n\n* SHOULD set a baseline of security headers globally (CSP, `X-Content-Type-Options`, clickjacking defense via CSP `frame-ancestors` and/or `X-Frame-Options`, etc.). Next.js provides guidance for implementing CSP via Proxy/headers. ([Next.js][7])\n* MUST ensure auth/session cookies use secure attributes (`Secure`, `HttpOnly`, `SameSite`) as appropriate. ([Next.js][9])\nIMPORTANT NOTE: Only set `Secure` in production environment. When running in a local dev environment over HTTP, do not set `Secure` property on cookies. You should do this conditionally based on if the app is running in production mode. You should also include a property like `SESSION_COOKIE_SECURE` which can be used to disable `Secure` cookies when testing over HTTP.\n\n### 3.4 Clear separation between server-only and client code (MUST)\n\n* MUST prevent secrets and privileged logic from being bundled into client code.\n* MUST treat `NEXT_PUBLIC_*` environment variables as public (browser-exposed and inlined at build time). ([Next.js][7])\n\n---\n\n## 4) Rules (generation + audit)\n\nEach rule contains: required practice, insecure patterns, detection hints, and remediation.\n\n### NEXT-DEPLOY-001: Do not run `next dev` in production; ensure production mode behavior\n\nSeverity: High (if production)\n\nNOTE: If they are deploying to a specific Next.js hosting provider, they do not need to worry about this.\n\nRequired:\n\n* MUST NOT deploy `next dev` or any development server mode to production.\n* MUST ensure production builds and production runtime are used for any public deployment. ([Next.js][6])\n\nInsecure patterns:\n\n* `next dev` in Docker `CMD`, Procfile, platform start command.\n* `NODE_ENV=development` in production environment config.\n* Debug/dev-only endpoints or flags exposed publicly.\n\nDetection hints:\n\n* Search `package.json` scripts and deployment manifests for `next dev`.\n* Search infra for `NODE_ENV=development` or missing `NODE_ENV`.\n* Check Kubernetes/PM2/systemd entrypoints for `next dev`.\n\nFix:\n\n* Use `next build` during CI/build and `next start` at runtime (or platform-native build/run).\n* Ensure environment sets `NODE_ENV=production`.\n\nNote:\n\n* Dev mode is fine for local development. Only flag if it is being used as a production entrypoint.\n\n---\n\n### NEXT-SUPPLY-001: Stay on supported Next.js releases; patch quickly for security advisories\n\nSeverity: High (Critical if known-vulnerable version)\n\nRequired:\n\n* MUST run a supported Next.js version line and apply security updates promptly. Next.js documents an LTS/support policy. ([Next.js][10])\n* MUST treat published advisories as urgent upgrade signals (e.g., update to a patched release). ([GitHub][11])\n\nInsecure patterns:\n\n* Running EOL Next.js major/minor without backported security fixes.\n* Ignoring advisories, or pinning `next` to a vulnerable range.\n\nDetection hints:\n\n* Check `package.json` and lockfiles for `next` version.\n* Compare against Next.js support policy and advisories.\n\nIMPORTANT: Any versions older than these minor versions are vulnerable to \"react2shell\" vulnerability (https://nextjs.org/blog/CVE-2025-66478):\n15.0.5\n15.1.9\n15.2.6\n15.3.6\n15.4.8\n15.5.7\n16.0.7\n\nFix:\n\n* Upgrade `next` to a supported and patched version.\n* Add a dependency update process + CI checks.\n\n\n---\n\n### NEXT-SECRETS-001: Secrets MUST NOT be committed or exposed to the browser\n\nSeverity: High (Critical if secret is client-exposed)\n\nRequired:\n\n* MUST store secrets in environment variables or a secret manager; MUST NOT commit `.env*` files.\n* MUST treat `.env*` as sensitive; Next.js warns you “almost never want to commit these files.” ([Next.js][7])\n* MUST treat any `NEXT_PUBLIC_*` environment variable as public and browser-visible (inlined into the client bundle at build time). ([Next.js][7])\n\nInsecure patterns:\n\n* `.env`, `.env.local`, `.env.production` committed to git.\n* `NEXT_PUBLIC_API_KEY`, `NEXT_PUBLIC_SECRET`, `NEXT_PUBLIC_DATABASE_URL`, etc.\n* Rendering `process.env` values into HTML or returning them from API routes.\n\nDetection hints:\n\n* Scan git history and repo files for `.env` content, `DB_PASS=`, `API_KEY=`, `SECRET=`.\n* Grep for `NEXT_PUBLIC_` and review any sensitive-looking names.\n* Search for `process.env` usage in Client Components (`\"use client\"`) and shared modules.\n\nFix:\n\n* Move secrets to server-only env vars (no `NEXT_PUBLIC_` prefix).\n* Ensure `.env*` is ignored and secrets are injected at deploy time.\n* Rotate leaked keys.\n\n---\n\n### NEXT-SECRETS-002: Avoid server-only → client bundling mistakes (server/client boundary is a security boundary)\n\nSeverity: High\n\nRequired:\n\n* MUST ensure server-only modules (DB clients, secret-dependent code) are not imported into Client Components or other client-bundled code paths.\n* SHOULD use server-only patterns/layers (e.g., a dedicated DAL and server-only modules) and treat boundary violations as security bugs. Next.js explicitly discusses the “server-only” concept for sensitive modules. ([Next.js][6])\n\nInsecure patterns:\n\n* Importing DB clients, admin SDKs, or secret-reading modules into `\"use client\"` components.\n* Shared `lib/` modules imported by both server and client code that reference secrets.\n\nDetection hints:\n\n* Search for `\"use client\"` and examine its imports for server-only dependencies.\n* Look for DB client packages (`pg`, `mysql2`, `mongoose`, `prisma`, admin SDKs) imported from `components/` or other client paths.\n* Search for `process.env` access in UI components.\n\nFix:\n\n* Refactor into `lib/server/*` and only import from server contexts (Route Handlers, Server Components, Server Actions).\n* Add an explicit “server-only” guard pattern (and/or tests) to prevent accidental imports.\n\n---\n\n### NEXT-AUTH-001: Authentication/authorization MUST be enforced server-side for every protected action\n\nSeverity: High\n\nRequired:\n\n* MUST enforce authn/authz in server-side code for:\n\n  * Route Handlers (`app/**/route.ts`) ([Next.js][1])\n  * API Routes (`pages/api/**`) ([Next.js][3])\n  * Server Actions (`\"use server\"` functions invoked by clients) ([Next.js][6])\n* MUST NOT rely on client-side checks (hiding UI, route guards on the client) as the only protection.\n\nInsecure patterns:\n\n* Sensitive Route Handlers with no session verification.\n* Server Actions that mutate data but do not validate user identity/permissions.\n* “Authorization” checks in React components only.\n\nDetection hints:\n\n* Enumerate all Route Handlers and API Routes; for each, identify whether it requires auth.\n* Grep for `\"use server\"` and review all exported actions for auth checks.\n* Search for admin actions triggered by query params / form submits.\n\nFix:\n\n* Centralize auth helpers and call them in every protected endpoint/action.\n* Implement least-privilege authorization checks (role/resource ownership) per action.\n\n---\n\n### NEXT-AUTH-002: Proxy/Middleware-based auth MUST NOT create route coverage gaps\n\nSeverity: High\n\nRequired:\n\n* If using **Proxy** or **Middleware** for authentication checks, MUST ensure it covers every route that needs protection.\n* Next.js documentation notes Proxy can use a `matcher`, and for auth it’s recommended Proxy runs on all routes. ([Next.js][12])\n* MUST treat `matcher` mistakes as an auth bypass risk.\n\nInsecure patterns:\n\n* Proxy/Middleware only matches “pages” but not `/api/*`, or only matches some route groups.\n* “Denylist” style matchers that miss alternative request forms (framework-internal variants, RSC navigations, etc.).\n\nDetection hints:\n\n* Inspect `proxy.ts` / `middleware.ts` and its `matcher`.\n* Compare matchers to the full set of routes (including `app/api/**` and `pages/api/**`).\n* Ensure static assets and Next internals are excluded only intentionally, and that sensitive routes are included.\n\nFix:\n\n* Prefer allowlisting protected route prefixes or running Proxy globally and doing internal allow/deny logic.\n* Add integration tests: request protected route without auth and assert denial.\n\nNotes:\n\n* Proxy is commonly used for “optimistic checks”; it is not a complete authorization system by itself. ([Next.js][12])\n\n---\n\n### NEXT-CSRF-001: Cookie-authenticated state-changing endpoints MUST be CSRF-protected\n\nSeverity: High\n\n- IMPORTANT NOTE: If cookies are not being used for auth (ie auth is via Authentication header or other passed token), then there is no CSRF risk.\n\nRequired:\n\n* MUST protect every state-changing endpoint that relies on cookies for auth (POST/PUT/PATCH/DELETE).\n* For **Server Actions**, Next.js performs an Origin/Host comparison to help prevent CSRF; do not disable or weaken it. ([Next.js][5])\n* If Server Actions must be callable from additional trusted origins (e.g., a trusted proxy domain), MUST use `allowedOrigins` with a strict allowlist. ([Next.js][5])\n* For **Route Handlers** and **API Routes**, MUST implement CSRF protections explicitly (tokens and/or strict Origin/Referer + SameSite + custom headers). Route Handlers are an “escape hatch” and require application-level security decisions. ([Next.js][6])\n\nInsecure patterns:\n\n* POST endpoints (including Server Actions) that mutate state and accept cross-site requests with no token/origin checks.\n* `allowedOrigins: ['*']` (or broad wildcards) or “reflect Origin” logic.\n* Using GET requests to change state.\n\nDetection hints:\n\n* Enumerate all state-changing endpoints and determine auth mechanism.\n* Search for `allowedOrigins` and confirm the list is small, specific, and justified. ([Next.js][5])\n* In Route Handlers/API Routes: look for missing CSRF token validation or missing Origin/Referer checks.\n\nFix:\n\n* Implement a CSRF token strategy for cookie-auth endpoints.\n* Keep cookies `SameSite=Lax` or `Strict` when compatible; don’t treat SameSite alone as sufficient.\n* Use strict Origin validation for JSON API endpoints, especially when not using CSRF tokens.\n\nNotes:\n\n* XSS can defeat CSRF protections; CSRF defenses do not replace XSS prevention.\n\n---\n\n### NEXT-SESS-001: Session cookies MUST use secure attributes in production\n\nSeverity: Medium\n\nRequired (production, HTTPS):\n\n* MUST set session/auth cookies with:\n\n  * `Secure: true` (HTTPS-only) IMPORTANT NOTE: Only set `Secure` in production environment. When running in a local dev environment over HTTP, do not set `Secure` property on cookies. You should do this conditionally based on if the app is running in production mode. You should also include a property like `SESSION_COOKIE_SECURE` which can be used to disable `Secure` cookies when testing over HTTP.\n  * `HttpOnly: true` (not readable by JS)\n  * `SameSite: 'Lax'` (recommended) or `'Strict'` if compatible\n* Only use `SameSite: 'none'` when you truly need cross-site cookies, and then MUST also set `Secure`. Cookie options are supported in Next.js cookie APIs. ([Next.js][9])\n\nInsecure patterns:\n\n* `secure: false` in production.\n* `httpOnly: false` for auth cookies.\n* `sameSite: 'none'` without a clear need, especially on cookie-authenticated state-changing endpoints.\n\nDetection hints:\n\n* Search for cookie setting sites (`cookies().set(...)`, `Set-Cookie` headers, auth library cookie config).\n* Review cookie options used in Route Handlers and Server Actions. ([Next.js][9])\n\nFix:\n\n* Set secure cookie attributes at the auth/session layer.\n* Reduce cookie scope: avoid wide `domain` unless you explicitly need subdomain-wide cookies.\n\n---\n\n### NEXT-SESS-002: Sessions MUST be bounded and resistant to fixation/replay\n\nSeverity: Low\n\nRequired:\n\n* SHOULD set bounded session lifetimes appropriate to the app.\n* SHOULD rotate session identifiers on login and privilege changes.\n* MUST NOT store sensitive secrets directly in client-readable storage (including cookies that are not encrypted).\n\nInsecure patterns:\n\n* Long-lived admin sessions with no rotation.\n* “Remember me forever” for privileged roles without additional risk controls.\n* Storing access tokens/refresh tokens in non-HttpOnly cookies or localStorage.\n\nDetection hints:\n\n* Review auth library configuration for expiration and rotation.\n* Search for `localStorage.setItem('token'...)` and non-HttpOnly cookie usage.\n\nFix:\n\n* Use short lifetimes for privileged sessions; refresh with rotation.\n* Store only opaque session IDs in cookies; keep sensitive material server-side.\n\n---\n\n### NEXT-INPUT-001: Runtime input validation is mandatory (TypeScript is not validation)\n\nSeverity: High\n\nRequired:\n\n* MUST validate and normalize all attacker-controlled input at runtime (schemas, type checks, bounds).\n* Next.js API Routes explicitly note `req.body` is `any` and must be validated before use. ([Next.js][3])\n* MUST validate Server Action arguments (treat as hostile). ([Next.js][6])\n\nInsecure patterns:\n\n* Trusting `req.body` shape directly.\n* Passing `params.id`/`searchParams` directly into DB queries or file paths.\n* Parsing JSON and then assuming types without validation.\n\nDetection hints:\n\n* Identify endpoints that accept JSON/form input and check for schema validation.\n* Grep for `req.body.` usage and for `await request.json()` usage in Route Handlers; verify validation exists.\n\nFix:\n\n* Add schema validation (e.g., zod/yup/valibot) and reject invalid input with 4xx.\n* Validate IDs as strict types (UUID/int) and enforce length/charset constraints.\n\n---\n\n### NEXT-HEADERS-001: Essential security headers MUST be set (in app or at the edge)\n\nSeverity: Low\n\nRequired (typical web app):\n\n* SHOULD set:\n\n  * CSP (`Content-Security-Policy`) (see NEXT-CSP-001)\n  * `X-Content-Type-Options: nosniff`\n  * Clickjacking defense (`frame-ancestors` in CSP and/or `X-Frame-Options`)\n  * `Referrer-Policy` and `Permissions-Policy` when appropriate\n* MUST ensure cookies are set with secure attributes (see NEXT-SESS-001). ([Next.js][9])\n\nInsecure patterns:\n\n* No security headers anywhere (app or edge).\n* Allowing iframing unintentionally.\n* `Content-Type` sniffing possible due to missing `nosniff`.\n\nDetection hints:\n\n* Check `proxy.ts` / middleware for `response.headers.set(...)`. ([Next.js][7])\n* If not visible in app code, flag as “verify at edge/CDN”.\n\nFix:\n\n* Set headers centrally (Proxy/Middleware or other centralized mechanism).\n* Ensure consistent headers across routes.\n\n---\n\n### NEXT-CSP-001: Use a CSP to reduce XSS impact; prefer nonces for scripts\n\nSeverity: Medium\n\nNOTE: It is most important to set the CSP's script-src. All other directives are not as important and can generally be excluded for the ease of development.\n\nRequired:\n\n* SHOULD deploy a CSP, ideally with nonces for scripts.\n* SHOULD follow Next.js guidance for CSP implementation (including nonce generation and header application). ([Next.js][7])\n* MUST avoid loosening CSP as a “fix” (e.g., `script-src 'unsafe-inline'`) without explicit risk acceptance.\n\nInsecure patterns:\n\n* CSP missing on apps that display user-generated HTML/markdown.\n* CSP that broadly enables inline scripts or eval without strict justification.\n\nDetection hints:\n\n* Search for `Content-Security-Policy` header setting and examine its directives.\n* Check use of `next/script` and whether a nonce is provided when CSP requires it.\n\nFix:\n\n* Implement CSP per Next.js guidance; use a nonce and apply it consistently.\n* Reduce inline scripts; avoid `eval`.\n\nNotes:\n\n* CSP is defense-in-depth; it does not replace proper output encoding and sanitization.\n\n---\n\n### NEXT-XSS-001: Prevent reflected/stored XSS in React/Next rendering\n\nSeverity: High\n\nRequired:\n\n* MUST rely on React’s default escaping; MUST NOT insert untrusted HTML into the DOM without sanitization.\n* MUST treat these as high-risk sinks:\n\n  * `dangerouslySetInnerHTML`\n  * rendering user-controlled strings into `<script>` tags or event handler attributes\n* MUST avoid serving uploaded HTML as active HTML (serve as attachment or sanitize/transform).\n\nInsecure patterns:\n\n* `<div dangerouslySetInnerHTML={{ __html: userContent }} />` with no sanitizer.\n* Markdown renderers configured to allow raw HTML with no sanitizer.\n* Returning user content with `Content-Type: text/html` from a Route Handler.\n\nDetection hints:\n\n* Search for `dangerouslySetInnerHTML`, `__html:`.\n* Search for template-like string concatenation that builds HTML.\n* Review any “render HTML” or “preview” features.\n\nFix:\n\n* Sanitize untrusted HTML with a well-maintained sanitizer; prefer strict allowlists.\n* Prefer rendering user content as text, not HTML.\n* Add CSP to reduce impact.\n\n---\n\n### NEXT-ACTION-001: Server Actions MUST be treated like public endpoints\n\nSeverity: High (Critical for privileged actions)\n\nRequired:\n\n* MUST apply the same controls as for Route Handlers:\n\n  * authn/authz\n  * input validation\n  * CSRF/origin protections\n  * rate limiting for sensitive actions\n* MUST NOT assume Server Actions are “not reachable” or “internal”.\n* MUST understand Server Action request protections:\n\n  * Next.js compares Origin with host to mitigate CSRF; extra origins must be explicitly allowlisted via `allowedOrigins`. ([Next.js][5])\n\nInsecure patterns:\n\n* `\"use server\"` functions that update DB state with no auth check.\n* Adding overly broad `allowedOrigins` to “make it work”.\n\nDetection hints:\n\n* Grep for `\"use server\"` and inventory all exported actions.\n* Identify any action doing privileged writes; confirm it checks identity and permission.\n\nFix:\n\n* Wrap actions with an authz helper (fail closed).\n* Keep `allowedOrigins` minimal and audited.\n\n---\n\n### NEXT-ACTION-002: Do not accidentally leak secrets through Server Action closure/binding patterns\n\nSeverity: Medium (High if important secrets are exposed)\n\nRequired:\n\n* MUST treat Server Action closed-over values as sensitive and design intentionally.\n* Next.js notes that closed-over values are encrypted/signed, but values passed through `.bind` are not encrypted; do not rely on `.bind` to protect secrets. ([Next.js][6])\n* If using a stable encryption key for Server Actions across deployments, MUST treat it as a secret and store securely (do not commit/log it). ([Next.js][6])\n\nInsecure patterns:\n\n* `myAction.bind(null, process.env.SECRET)` or binding sensitive tokens/IDs that should not be client-influenced.\n* Logging action arguments that include secrets.\n\nDetection hints:\n\n* Search for `.bind(` on Server Action functions.\n* Search for `process.env` usage near Server Actions.\n\nFix:\n\n* Avoid binding secrets into actions; fetch secrets server-side inside the action.\n* Keep action arguments minimal and validated.\n\n---\n\n### NEXT-CACHE-001: Prevent data leaks via static rendering and shared caching\n\nSeverity: High (Critical if cross-user data leak)\n\nRequired:\n\n* MUST ensure pages/endpoints that return user-specific or sensitive data are not statically generated or cached in a shared way.\n* Route Handlers are not cached by default, but GET handlers can opt into caching/static behavior; do not do this for per-user data. ([Next.js][1])\n* MUST treat `use cache` and similar caching mechanisms as potentially cross-user unless explicitly proven private; do not cache per-user DB results in shared caches. ([Next.js][1])\n* SHOULD set explicit `Cache-Control: no-store` / `private` for sensitive responses (auth/session/user data APIs).\n\nInsecure patterns:\n\n* `export const dynamic = 'force-static'` on a route that returns user-specific data. ([Next.js][1])\n* Using `use cache` around a function that queries user-specific data without a per-user cache key. ([Next.js][1])\n* Returning auth/session responses from GET endpoints with caching enabled.\n\nDetection hints:\n\n* Search for `dynamic = 'force-static'`, `revalidate`, `use cache`, `cacheLife`, `unstable_cache`.\n* Inspect all GET Route Handlers that are cached/static and confirm they only return public data.\n* Confirm that use of `cookies()`/`headers()` (dynamic APIs) is not accidentally removed in ways that make a route static. ([Next.js][1])\n\nFix:\n\n* Mark sensitive routes as dynamic and set `Cache-Control: no-store`.\n* Ensure caching keys include user identity if caching is truly needed (and store it in a user-private cache).\n\n---\n\n### NEXT-FILES-001: User uploads MUST be validated, stored safely, and served safely\n\nSeverity: Medium\n\nRequired:\n\n* MUST enforce upload size limits at the edge and in application logic.\n* MUST validate file type using allowlists and content checks (not only extension).\n* MUST store uploads outside the `public/` directory (anything under `public/` is served as static content by default).\n* MUST serve potentially active formats safely (`Content-Disposition: attachment`) unless explicitly intended.\n\nInsecure patterns:\n\n* Accepting arbitrary file types and serving them back inline.\n* Using user-supplied filename as the storage path.\n* Writing uploads into `public/uploads/` and serving them directly.\n\nDetection hints:\n\n* Search for `formData()` / multipart parsing, `fs.writeFile`, storage SDK usage.\n* Look for any write path under `public/`.\n* Look for “download” endpoints that set `Content-Type: text/html` or serve user files inline.\n\nFix:\n\n* Use a dedicated object store (S3/GCS) or a safe server-side directory outside static roots.\n* Generate random server-side filenames; store metadata separately.\n\n---\n\n### NEXT-PATH-001: Prevent path traversal and unsafe file access\n\nSeverity: High\n\nRequired:\n\n* MUST NOT use user-controlled strings as filesystem paths.\n* MUST validate and normalize identifiers; use allowlists and safe base directories.\n* MUST avoid reading arbitrary files based on request parameters.\n\nInsecure patterns:\n\n* `fs.readFile(request.nextUrl.searchParams.get('path'))`\n* `path.join(base, userPath)` without normalization + boundary checks\n\nDetection hints:\n\n* Search for `fs.` usage in Route Handlers/API Routes.\n* Search for `path.join`/`path.resolve` fed by request params.\n\nFix:\n\n* Use opaque IDs that map to server-side stored paths.\n* Enforce that resolved paths remain within an intended base directory.\n* Sanitize and disallow `..` from being used when creating urls\n\n---\n\n### NEXT-SSRF-001: Outbound requests using user-influenced URLs MUST be restricted\n\nSeverity: Medium (High in internal networks)\n\nNOTE: This is mostly only applicable to apps which will be deployed in a cloud/LAN setup or have other http services on the same box. Sometimes the feature requires this functionality unavoidably (webhooks).\n\nRequired:\n\n* MUST treat any server-side `fetch()` to a user-provided URL as high-risk.\n* SHOULD allowlist destinations (hosts/domains) for URL fetch features.\n* SHOULD block:\n\n  * localhost / private IP ranges / link-local\n  * cloud metadata endpoints\n* MUST restrict protocols to `http:` and `https:`.\n* SHOULD set strict timeouts and restrict redirects.\n\nInsecure patterns:\n\n* `await fetch(req.query.url)` or `await fetch((await request.json()).url)`\n* “URL preview” endpoints that fetch arbitrary URLs.\n\nDetection hints:\n\n* Search for `fetch(` in server code and trace where the URL comes from.\n* Look for “webhook tester”, “preview”, “import from URL” features.\n\nFix:\n\n* Parse URL, enforce `http/https`, allowlist hostnames, re-resolve DNS/IP to block private ranges.\n* Set timeouts (AbortSignal) and limit redirects.\n\n---\n\n### NEXT-REDIRECT-001: Prevent open redirects (including auth flows)\n\nSeverity: Low\n\nRequired:\n\n* MUST validate redirect targets derived from untrusted input (e.g., `next`, `redirect`, `returnTo`).\n* SHOULD prefer redirecting only to same-site relative paths.\n* MUST validate any absolute URL against an allowlist.\n* MUST ensure urls are `http` or `https:` schema, disallowing `javascript:` schema\n\nInsecure patterns:\n\n* `redirect(searchParams.get('next')!)`\n* `NextResponse.redirect(new URL(req.nextUrl.searchParams.get('to')!, req.url))` without checks\n\nDetection hints:\n\n* Search for `redirect(` (server components/actions) and `NextResponse.redirect`.\n* Search for `res.redirect(` in API Routes. ([Next.js][3])\n\nFix:\n\n* Only allow relative paths (`/path`) and reject protocol-relative (`//evil.com`) or absolute URLs.\n* If invalid, fall back to a safe default (home/dashboard).\n\n---\n\n### NEXT-CORS-001: CORS must be explicit and least-privilege\n\nSeverity: Medium (High if misconfigured with credentials)\n\nRequired:\n\n* If CORS is not needed, MUST keep it disabled.\n* Next.js API Routes do not set CORS headers by default, meaning they are same-origin by default; only enable CORS when you truly need it. ([Next.js][3])\n* If enabling CORS:\n\n  * MUST allowlist trusted origins (no reflection of arbitrary Origin)\n  * MUST be careful with credentialed requests (cookies); never combine broad origins with credentials.\n  * SHOULD restrict methods and headers.\n\nInsecure patterns:\n\n* `Access-Control-Allow-Origin: *` with `Access-Control-Allow-Credentials: true`\n* Reflecting `Origin` without validation.\n\nDetection hints:\n\n* Search for `Access-Control-Allow-Origin`, `cors`, “CORS” middleware/wrappers.\n* Review preflight `OPTIONS` handlers.\n\nFix:\n\n* Implement strict origin allowlist and minimal methods/headers.\n* Ensure cookies aren’t exposed cross-origin unless necessary and reviewed.\n\n---\n\n### NEXT-WEBHOOK-001: Webhook endpoints MUST verify authenticity using the raw body\n\nSeverity: Medium\n\nRequired:\n\n* MUST verify webhook signatures using the **raw request body** (not a re-serialized parsed object).\n* Next.js notes a use case for disabling body parsing is verifying the raw body of a webhook request. ([Next.js][3])\n\nInsecure patterns:\n\n* Verifying webhook signatures over `JSON.stringify(req.body)` (can change formatting).\n* Accepting webhooks with no signature verification and no allowlist.\n\nDetection hints:\n\n* Find webhook endpoints (`/api/webhook`, `/app/api/**/webhook`).\n* Check whether they use raw body verification.\n\nFix:\n\n* Disable Next.js automatic body parsing only for those webhook routes, read raw bytes safely, verify signature, then parse.\n\n---\n\n### NEXT-INJECT-001: Prevent SQL injection (use parameterized queries / ORM)\n\nSeverity: High\n\nRequired:\n\n* MUST use parameterized queries or an ORM that parameterizes under the hood.\n* MUST NOT build SQL by string concatenation / template strings with untrusted input.\n\nInsecure patterns:\n\n* ``db.query(`SELECT * FROM users WHERE id = ${id}`)``\n* `\"WHERE name = '\" + user + \"'\"`\n\nDetection hints:\n\n* Grep for `SELECT`, `INSERT`, `UPDATE`, `DELETE` strings.\n* Trace untrusted input (`params`, `searchParams`, `req.query`, `req.body`, `request.json()`) into DB calls.\n\nFix:\n\n* Use prepared statements / ORM query APIs.\n* Validate and coerce types before querying.\n\n---\n\n### NEXT-INJECT-002: Prevent OS command injection and unsafe subprocess use\n\nSeverity: Critical to High\n\nRequired:\n\n* MUST avoid executing OS commands with attacker-controlled input.\n* If subprocess is necessary:\n\n  * MUST pass args as an array (not a single shell string)\n  * MUST NOT use `shell: true` with attacker-influenced strings\n  * SHOULD use strict allowlists for any variable component\n\nInsecure patterns:\n\n* `exec(\"convert \" + filename)`\n* `spawn(\"bash\", [\"-c\", userInput])`\n* `spawn(userInput, [\"foo\"])`\n\nDetection hints:\n\n* Search for `child_process`, `exec`, `spawn`, `shell: true`.\n\nFix:\n\n* Use library APIs instead of shell commands.\n* Hard-code commands and allowlist validated parameters (and use `--` to separate flags where supported).\n\n---\n\n### NEXT-INJECT-003: Avoid dynamic code execution and unsafe deserialization\n\nSeverity: High to Critical\n\nRequired:\n\n* MUST NOT use `eval`, `new Function`, `vm.runIn*` on untrusted strings.\n* MUST treat deserializing complex formats (YAML, XML, custom serialization) as risky; use safe parsers and strict schemas.\n\nInsecure patterns:\n\n* `eval(req.body.code)`\n* Parsing YAML from user input with a non-safe schema.\n\nDetection hints:\n\n* Search for `eval(`, `new Function`, `vm.`, `require(` with non-literals.\n* Search for `js-yaml`, XML parsers, custom serializer usage on untrusted input.\n\nFix:\n\n* Remove dynamic execution; use safe interpreters or strict parsers.\n* Validate and constrain input.\n\n---\n\n### NEXT-LOG-001: Logging MUST NOT leak secrets or sensitive headers\n\nSeverity: Medium\n\nRequired:\n\n* MUST NOT log:\n\n  * `Authorization` headers\n  * cookies / session tokens\n  * request bodies containing credentials\n  * environment variables or configuration dumps\n* SHOULD implement structured logging with redaction.\n\nInsecure patterns:\n\n* `console.log(req.headers)` in auth endpoints\n* `console.log(process.env)` in server code\n\nDetection hints:\n\n* Search for `console.log(`, `logger.info(`, `debug(` in server routes/actions.\n* Check for logs of headers/cookies/body.\n\nFix:\n\n* Redact sensitive fields; log only what is needed for debugging.\n* Use safe error messages for clients; keep detail server-side only.\n\n---\n\n### NEXT-ERROR-001: Error handling MUST avoid leaking implementation details in production\n\nSeverity: Low\n\nRequired:\n\n* MUST not expose stack traces or internal error details to end users in production.\n* Ensure production mode behavior (Next.js production error handling differs from dev). ([Next.js][6])\n\nInsecure patterns:\n\n* Returning `err.stack` in JSON responses.\n* Showing detailed exception data to unauthenticated users.\n\nDetection hints:\n\n* Search for `res.status(500).json(err)` or `return Response.json(err)`.\n* Verify error responses are sanitized.\n\nFix:\n\n* Return generic error messages to clients; log details server-side with redaction.\n\n---\n\n### NEXT-PROXY-001: Proxy/Middleware must not introduce header smuggling or unsafe header forwarding\n\nSeverity: Medium\n\nRequired:\n\n* MUST be careful when copying/forwarding request headers upstream:\n\n  * Do not forward attacker-controlled `x-forwarded-*` headers unless you have a trusted proxy chain.\n  * Do not forward `Authorization`/cookies to unrelated outbound services.\n* Next.js Proxy patterns often mutate headers; ensure this doesn’t create security issues.\n\nInsecure patterns:\n\n* Blindly cloning all request headers to an outbound `fetch()` call.\n* Trusting `x-forwarded-host` or `host` to construct sensitive absolute URLs without allowlisting.\n\nDetection hints:\n\n* Search `headers()` and `request.headers` usage (especially for URL building). ([Next.js][4])\n* Search Proxy/Middleware for header rewrites.\n\nFix:\n\n* Allowlist forwarded headers explicitly.\n* Validate hostnames before using them to build callback URLs or redirects.\n\n---\n\n### NEXT-HOST-001: Host/Origin-derived URL construction MUST be allowlisted\n\nSeverity: Medium\n\nRequired:\n\n* MUST NOT generate security-sensitive absolute URLs (password reset links, OAuth callback URLs, email verification links) directly from unvalidated `Host` headers.\n* For Server Actions, Origin/Host matching is part of CSRF mitigation; do not weaken it. ([Next.js][5])\n\nInsecure patterns:\n\n* `const base = \"https://\" + request.headers.get(\"host\")`\n* Using unvalidated `x-forwarded-host` for absolute URL generation.\n\nDetection hints:\n\n* Grep for `.get('host')`, `.get('x-forwarded-host')`, and absolute URL building.\n* Review auth-related email link generation code.\n\nFix:\n\n* Use a configured, allowlisted canonical app origin (e.g., `APP_ORIGIN=https://example.com`).\n* Allowlist hostnames; fail closed.\n\n---\n\n### NEXT-DOS-001: Rate limiting and resource controls MUST exist for abuse-prone endpoints\n\nSeverity: Medium\n\nRequired:\n\n* SHOULD implement rate limiting/throttling for:\n\n  * login, password reset, signup\n  * expensive Server Actions\n  * webhook ingestion\n* MUST implement request size limits (see NEXT-LIMITS-001).\n* If self-hosting, MUST rely on reverse proxy for additional protections. ([Next.js][8])\n\nInsecure patterns:\n\n* No throttling on login/reset endpoints.\n* Expensive actions callable without auth or with unlimited frequency.\n\nDetection hints:\n\n* Identify auth endpoints and check for rate limiting.\n* Search for “send email”, “charge”, “generate report” flows.\n\nFix:\n\n* Add edge rate limiting and app-level user/IP throttles.\n* Add job queues for heavy work; return 202 when appropriate.\n\n---\n\n## 5) Practical scanning heuristics (how to “hunt”)\n\nWhen actively scanning, use these high-signal patterns:\n\n* Production misconfig:\n\n  * `next dev`, `NODE_ENV=development`, dev-only start commands ([Next.js][7])\n* Secrets exposure:\n\n  * `.env` committed, `NEXT_PUBLIC_` on sensitive variables ([Next.js][7])\n  * `process.env` used in `\"use client\"` modules\n* Auth coverage:\n\n  * `app/**/route.ts` or `pages/api/**` with no auth checks ([Next.js][1])\n  * `\"use server\"` actions with DB writes and no authz ([Next.js][6])\n  * `proxy.ts` / `middleware.ts` matchers that exclude sensitive routes ([Next.js][12])\n* CSRF:\n\n  * cookie-auth POST/PUT/PATCH/DELETE with no token/origin checks\n  * `serverActions.allowedOrigins` too broad ([Next.js][5])\n* XSS:\n\n  * `dangerouslySetInnerHTML`, raw HTML markdown rendering\n  * missing CSP / overly permissive CSP ([Next.js][7])\n* Caching/data leak:\n\n  * `dynamic = 'force-static'` on sensitive GET handlers ([Next.js][1])\n  * `use cache`, `cacheLife`, `unstable_cache` around user-specific data ([Next.js][1])\n* Files:\n\n  * writing uploads under `public/`\n  * `fs.readFile` / `path.join` with request input\n* SSRF:\n\n  * `fetch(userProvidedUrl)` from Route Handlers / Server Actions\n* Redirect:\n\n  * `redirect(searchParams.get('next'))`, `NextResponse.redirect(...)`, `res.redirect(req.query.next)` ([Next.js][3])\n* CORS:\n\n  * wildcard origins, origin reflection, credentials + broad origins ([Next.js][3])\n* Limits:\n\n  * API routes with `bodyParser: false` and no raw-body verification for webhooks ([Next.js][3])\n  * `serverActions.bodySizeLimit` raised without justification ([Next.js][5])\n* Dependency hygiene:\n\n  * old `next` versions that conflict with support policy/advisories ([Next.js][10])\n\nAlways try to confirm:\n\n* data origin (untrusted vs trusted)\n* sink type (HTML/DOM, SQL, subprocess, files, redirect, outbound HTTP)\n* protective controls present (schema validation, allowlists, middleware/proxy checks, authz helpers, edge protections)\n\n---\n\n## 6) Sources (accessed 2026-01-27)\n\nPrimary framework documentation (Next.js):\n\n* Next.js Docs: Installation (system requirements / Node version) — `https://nextjs.org/docs/app/getting-started/installation`\n* Next.js Docs: Route Handlers — `https://nextjs.org/docs/app/getting-started/route-handlers`\n* Next.js Docs: API Routes (Pages Router) — `https://nextjs.org/docs/pages/building-your-application/routing/api-routes`\n* Next.js Docs: Environment Variables — `https://nextjs.org/docs/pages/guides/environment-variables`\n* Next.js Docs: Data Security — `https://nextjs.org/docs/app/guides/data-security`\n* Next.js Docs: Content Security Policy — `https://nextjs.org/docs/app/guides/content-security-policy`\n* Next.js Docs: Proxy — `https://nextjs.org/docs/app/getting-started/proxy`\n* Next.js Docs: `serverActions.allowedOrigins` and `serverActions.bodySizeLimit` — `https://nextjs.org/docs/app/api-reference/config/next-config-js/serverActions`\n* Next.js Docs: `cookies()` — `https://nextjs.org/docs/app/api-reference/functions/cookies`\n* Next.js Docs: `headers()` — `https://nextjs.org/docs/app/api-reference/functions/headers`\n* Next.js Docs: Self-hosting (reverse proxy guidance) — `https://nextjs.org/docs/pages/guides/self-hosting`\n* Next.js Docs: Support policy (supported versions/LTS) — `https://nextjs.org/docs/support-policy`\n\nNext.js security guidance & advisories:\n\n* Next.js Blog: How to think about security in Next.js — `https://nextjs.org/blog/security-nextjs-server-components-actions`\n* GitHub Security Advisory: Next.js DoS via Server Components / Server Actions (CVE-2026-23864) — `https://github.com/advisories/GHSA-fq29-rrrv-cq2m`\n* Next.js Blog: Security update (example security advisory context) — `https://nextjs.org/blog/security-update`\n\nGeneral web security references (recommended baseline):\n\n* OWASP Cheat Sheet Series (CSRF, Session Management, XSS Prevention, SSRF Prevention, File Upload, HTTP Headers) — `https://cheatsheetseries.owasp.org/`\n\n[1]: https://nextjs.org/docs/app/getting-started/route-handlers \"Getting Started: Route Handlers | Next.js\"\n[2]: https://nextjs.org/docs/app/getting-started/deploying?utm_source=chatgpt.com \"Getting Started: Deploying\"\n[3]: https://nextjs.org/docs/pages/building-your-application/routing/api-routes \"Routing: API Routes | Next.js\"\n[4]: https://nextjs.org/docs/app/api-reference/functions/headers \"Functions: headers | Next.js\"\n[5]: https://nextjs.org/docs/app/api-reference/config/next-config-js/serverActions \"next.config.js: serverActions | Next.js\"\n[6]: https://nextjs.org/blog/security-nextjs-server-components-actions \"How to Think About Security in Next.js | Next.js\"\n[7]: https://nextjs.org/docs/pages/guides/environment-variables \"Guides: Environment Variables | Next.js\"\n[8]: https://nextjs.org/docs/pages/guides/self-hosting?utm_source=chatgpt.com \"Guides: Self-Hosting\"\n[9]: https://nextjs.org/docs/app/api-reference/functions/cookies \"Functions: cookies | Next.js\"\n[10]: https://nextjs.org/blog/next-16?utm_source=chatgpt.com \"Next.js 16\"\n[11]: https://github.com/vercel/next.js/security/advisories/GHSA-9g9p-9gw9-jx7f?utm_source=chatgpt.com \"Denial of Service in Image Optimizer · Advisory\"\n[12]: https://nextjs.org/docs/pages/guides/authentication \"Guides: Authentication | Next.js\"\n"
  },
  {
    "path": "skills/.curated/security-best-practices/references/javascript-typescript-react-web-frontend-security.md",
    "content": "# React (JavaScript/TypeScript) Web Security Spec (React 19.x, TypeScript 5.x)\n\nThis document is designed as a **security spec** that supports:\n\n1. **Secure-by-default code generation** for new React code.\n2. **Security review / vulnerability hunting** in existing React code (passive “notice issues while working” and active “scan the repo and report findings”).\n\nIt is intentionally written as a set of **normative requirements** (“MUST/SHOULD/MAY”) plus **audit rules** (what bad patterns look like, how to detect them, and how to fix/mitigate them).\n\n---\n\n## 0) Safety, boundaries, and anti-abuse constraints (MUST FOLLOW)\n\n* MUST NOT request, output, log, or commit secrets (API keys, OAuth client secrets, private keys, session cookies, JWTs, signing keys).\n\n  * Frontend note: anything shipped to the browser is observable by end users and attackers (view-source, devtools, proxies); never treat client code or “env vars in the bundle” as secret. ([create-react-app.dev][1])\n* MUST NOT “fix” security by disabling protections (e.g., turning off CSP to “make it work”, adding `unsafe-inline`/`unsafe-eval` without a documented, constrained plan, disabling CSRF protections when using cookies, widening CORS, skipping sanitization, or “temporary” bypasses that ship). ([OWASP Cheat Sheet Series][2])\n* MUST provide **evidence-based findings** during audits: cite file paths, code snippets, and configuration values that justify the claim.\n* MUST treat uncertainty honestly: if a protection might exist in infra (CDN/WAF/reverse proxy), report it as “not visible in app code; verify via runtime headers / edge config”.\n* MUST assume any data that crosses a trust boundary (URL, storage, network, postMessage, third-party scripts) can be attacker-influenced unless proven otherwise (see §2.1).\n\n---\n\n## 1) Operating modes\n\n### 1.1 Generation mode (default)\n\nWhen asked to write new React code or modify existing code:\n\n* MUST follow every **MUST** requirement in this spec.\n* SHOULD follow every **SHOULD** requirement unless the user explicitly says otherwise.\n* MUST prefer safe-by-default APIs and proven libraries over custom security code.\n* MUST avoid introducing new risky sinks (raw HTML insertion, direct DOM sinks like `innerHTML`, dynamic code execution, untrusted redirects/navigation, third‑party script injection, unsafe token storage, etc.). ([MDN Web Docs][3])\n\n### 1.2 Passive review mode (always on while editing)\n\nWhile working anywhere in a React repo (even if the user did not ask for a security scan):\n\n* MUST “notice” violations of this spec in touched/nearby code.\n* SHOULD mention issues as they come up, with a brief explanation + safe fix.\n\n### 1.3 Active audit mode (explicit scan request)\n\nWhen the user asks to “scan”, “audit”, or “hunt for vulns”:\n\n* MUST systematically search the codebase for violations of this spec.\n* MUST output findings in a structured format (see §2.3).\n\nRecommended audit order:\n\n1. App entrypoints, build tooling (Vite/Webpack/CRA/Next), deployment configs, CDN/static hosting config.\n2. Secrets & configuration exposure (env vars, runtime config injection, source maps).\n3. Rendering of untrusted data (XSS/DOM XSS), especially `dangerouslySetInnerHTML`, markdown/HTML renderers, URL attributes.\n4. Direct DOM usage and dangerous JS execution (`innerHTML`, `eval`, `new Function`, `document.write`, etc.).\n5. Auth & session patterns (token storage, cookies, CSRF interactions, OAuth flows).\n6. Network layer (axios/fetch wrappers, dynamic base URLs, credentialed requests, data exfil risks).\n7. Navigation & redirect handling (open redirects, `window.location`, `target=_blank`, `window.open`).\n8. Third-party scripts/tags/analytics and integrity controls (CSP, SRI).\n9. Service worker/PWA behavior (HTTPS, caching rules, update strategy).\n10. Security headers posture (CSP, clickjacking, nosniff, referrer policy) in app or at the edge. ([OWASP Cheat Sheet Series][2])\n\n---\n\n## 2) Definitions and review guidance\n\n### 2.1 Untrusted input (treat as attacker-controlled unless proven otherwise)\n\nExamples include:\n\n* URL-derived data: `window.location`, query params, hash fragments, route params.\n* Any data from browser storage: `localStorage`, `sessionStorage`, `IndexedDB` (including data previously written by the app—because XSS or extensions can tamper with it). ([OWASP Cheat Sheet Series][4])\n* Any data from cross-window messaging: `window.postMessage` payloads. ([OWASP Cheat Sheet Series][4])\n* Any data from remote APIs, webhooks proxied to the client, GraphQL responses, CMS content, feature flag services.\n* Any persisted user content (profiles, comments, rich text, markdown) rendered in the UI.\n* Any data produced by third-party scripts or tag managers (treat as untrusted unless strongly controlled). ([OWASP Cheat Sheet Series][5])\n\n### 2.2 State-changing request (frontend perspective)\n\nA request is state-changing if it can create/update/delete data, change auth/session state, trigger side effects (purchase, email send, webhook), or initiate privileged actions.\n\nFrontend-specific note:\n\n* State changes are often triggered by `fetch/axios` calls or form submissions. If authentication is cookie-based, these calls can be CSRF-relevant (§4 REACT-CSRF-001). ([OWASP Cheat Sheet Series][6])\n\n### 2.3 Required audit finding format\n\nFor each issue found, output:\n\n* Rule ID:\n* Severity: Critical / High / Medium / Low\n* Location: file path + component/function + line(s)\n* Evidence: the exact code/config snippet\n* Impact: what could go wrong, who can exploit it\n* Fix: safe change (prefer minimal diff)\n* Mitigation: defense-in-depth if immediate fix is hard\n* False positive notes: what to verify if uncertain\n\n---\n\n## 3) Secure baseline: minimum production configuration (MUST in production)\n\nThis is the smallest “production baseline” that prevents common React frontend misconfigurations.\n\n### 3.1 Production build and configuration hygiene (MUST)\n\n* MUST ship a production build (minified, no dev-only overlays/tools, correct mode flags).\n* MUST ensure build-time configuration does not embed secrets into the shipped JS/HTML/CSS. Build-time “environment variables” are not secret; treat them as public. ([create-react-app.dev][1])\n* SHOULD treat source maps as sensitive operational artifacts:\n\n  * Either don’t publish them publicly, or publish them only where intended (e.g., behind auth or to an error-reporting provider), because they can reveal code structure and internal URLs.\n\n### 3.2 Browser-enforced protections (SHOULD, but baseline expectation for modern apps)\n\n* SHOULD deploy a CSP as defense-in-depth against XSS, and keep it compatible with your React build (avoid `unsafe-inline` and `unsafe-eval` unless strictly necessary and documented). ([OWASP Cheat Sheet Series][2])\n* SHOULD use Subresource Integrity (SRI) for any third-party script/style loaded from a CDN (or self-host instead). ([MDN Web Docs][7])\n* SHOULD enable clickjacking defenses via `frame-ancestors` (CSP) and/or `X-Frame-Options`, unless embedding is an explicit product requirement. ([MDN Web Docs][8])\n\n### 3.3 High-risk features baseline (MUST if used)\n\n* If rendering any user-provided HTML/markdown/rich text:\n\n  * MUST sanitize before insertion and avoid raw DOM sinks. ([OWASP Cheat Sheet Series][9])\n* If using service workers / PWA:\n\n  * MUST serve over HTTPS and implement a safe caching/update strategy (service workers are powerful request/response proxies). ([MDN Web Docs][10])\n\n---\n\n## 4) Rules (generation + audit)\n\nEach rule contains: required practice, insecure patterns, detection hints, and remediation.\n\n### REACT-CONFIG-001: Never embed secrets in the client bundle (env vars are public)\n\nSeverity: Critical (if secrets exposed)\n\nRequired:\n\n* MUST NOT place secrets in React code, in `public/` assets, or in build-time environment variables intended for client consumption.\n* MUST assume any value available to the React app at runtime can be extracted by an attacker.\n\nInsecure patterns:\n\n* Using build-time env vars for secrets:\n\n  * `process.env.REACT_APP_*` containing private keys or credentials.\n  * `import.meta.env.VITE_*` containing secrets.\n* Hard-coded secrets in JS/TS, `.env` committed, or secrets in `public/config.json` served to all users.\n\nDetection hints:\n\n* Search for:\n\n  * `REACT_APP_`, `VITE_`, `NEXT_PUBLIC_`, `process.env.`, `import.meta.env.`\n  * `apiKey`, `secret`, `token`, `private`, `password`, `client_secret`\n* Inspect `public/` for runtime config JSON.\n\nFix:\n\n* Move secrets server-side (API, BFF, serverless function).\n* Use a backend to mint short-lived, scoped tokens if the browser needs to call third-party APIs.\n\nNotes:\n\n* CRA explicitly warns not to store secrets and notes env vars are embedded into the build and visible to anyone inspecting files. ([create-react-app.dev][1])\n* Vite explicitly notes that variables exposed to client code end up in the client bundle and should not contain sensitive info. ([vitejs][11])\n\n---\n\n### REACT-XSS-001: Do not use `dangerouslySetInnerHTML` with untrusted content (sanitize or avoid)\n\nSeverity: High (Only if you can prove attacker-controlled HTML reaches it)\n\nRequired:\n\n* MUST avoid `dangerouslySetInnerHTML` unless absolutely necessary.\n* If it must be used:\n\n  * MUST sanitize untrusted HTML with a proven sanitizer (e.g., DOMPurify) and an allowlist-oriented configuration.\n  * MUST keep the sanitization logic centralized and heavily reviewed.\n  * SHOULD add a CSP and consider Trusted Types (see REACT-TT-001).\n\nInsecure patterns:\n\n* `<div dangerouslySetInnerHTML={{ __html: userHtml }} />` where `userHtml` is from API/URL/storage.\n* “Sanitization” done with regexes, ad-hoc stripping, or incomplete allowlists.\n\nDetection hints:\n\n* Grep: `dangerouslySetInnerHTML`, `__html:`\n* Trace the origin of the HTML string (API/CMS/URL/localStorage).\n\nFix:\n\n* Replace with safe rendering:\n\n  * Render structured data as React elements/components instead of HTML strings.\n  * If rich text is required, sanitize with DOMPurify (or equivalent) and render the sanitized output.\n* Add CSP; remove dangerous sinks where possible.\n\nNotes:\n\n* React explicitly warns that `dangerouslySetInnerHTML` is dangerous and can introduce XSS if misused. ([React][12])\n* OWASP explicitly calls out React’s `dangerouslySetInnerHTML` without sanitization as a common framework “escape hatch” pitfall. ([OWASP Cheat Sheet Series][9])\n* DOMPurify describes itself as an XSS sanitizer for HTML/SVG/MathML. ([GitHub][13])\n\n---\n\n### REACT-XSS-002: Rely on React’s escaping-by-default behavior; do not bypass it\n\nSeverity: High (when bypassed)\n\nRequired:\n\n* MUST render untrusted strings via normal JSX interpolation (`{value}`) and React props, which are escaped by default.\n* MUST NOT build HTML strings from untrusted data and then inject them into the DOM via any means.\n* SHOULD treat any “escape hatch” as high risk and require review.\n\nInsecure patterns:\n\n* Converting untrusted text into HTML and injecting it:\n\n  * `element.innerHTML = userValue`\n  * `document.write(userValue)`\n  * `insertAdjacentHTML(..., userValue)`\n\nDetection hints:\n\n* Grep for DOM sinks: `innerHTML`, `outerHTML`, `insertAdjacentHTML`, `document.write`, `DOMParser`, `createContextualFragment`.\n\nFix:\n\n* Render text content through React (JSX) so it is escaped.\n* If you truly need HTML, sanitize and apply REACT-XSS-001 + REACT-TT-001.\n\nNotes:\n\n* React documentation (JSX) states that React DOM escapes values embedded in JSX before rendering to help prevent injection attacks. ([React][14])\n\n---\n\n### REACT-DOM-001: Avoid DOM XSS injection sinks in React code (use safe alternatives)\n\nSeverity: High\n\nRequired:\n\n* MUST avoid direct DOM injection sinks, even outside React rendering, unless strongly controlled.\n* If a DOM sink is required:\n\n  * MUST ensure inputs are trusted/validated/sanitized.\n  * SHOULD enforce Trusted Types (REACT-TT-001).\n\nInsecure patterns:\n\n* `someEl.innerHTML = untrusted`\n* `document.write(untrusted)`\n* `new DOMParser().parseFromString(untrusted, 'text/html')` followed by insertion\n\nDetection hints:\n\n* Grep for: `innerHTML`, `outerHTML`, `document.write`, `DOMParser`, `Range().createContextualFragment`, `insertAdjacentHTML`\n\nFix:\n\n* Prefer:\n\n  * `textContent` for text insertion.\n  * React rendering rather than manual DOM manipulation.\n  * A vetted sanitizer for any required HTML parsing.\n\nNotes:\n\n* Trusted Types documentation defines HTML sinks like `Element.innerHTML` and `document.write()` as injection sinks that can execute script when given attacker-controlled input. ([MDN Web Docs][3])\n* OWASP HTML5 guidance recommends using `textContent` instead of `innerHTML` for assigning untrusted data. ([OWASP Cheat Sheet Series][4])\n\n---\n\n### REACT-URL-001: Validate and constrain untrusted URLs used in `href`, `src`, navigation, and redirects\n\nSeverity: High Only when you can prove they are attacker controlled\n\nRequired:\n\n* MUST treat any URL derived from untrusted input as dangerous.\n* MUST allowlist schemes and (when applicable) hosts:\n\n  * Typically allow only `https:` (and maybe `http:` for localhost/dev) and relative URLs for in-app navigation.\n  * MUST explicitly block `javascript:` and dangerous `data:` uses unless you have specialized validation and a clear use case.\n* SHOULD prefer same-site relative paths (e.g., `/settings`) over absolute URLs.\n* MUST validate “returnTo/next/redirect” parameters (see REACT-REDIRECT-001).\n\nInsecure patterns:\n\n* `<img src={userProvidedUrl}>...` (can be used for tracking / data exfil; also risky if used for scripts/iframes)\n* `window.location = next`\n* `navigate(next)` where `next` comes from query params without validation\n\nDetection hints:\n\n* Search for:\n\n  * `href={`, `src={`, `window.location`, `location.href`, `window.open`, `navigate(`, `redirectTo`, `returnTo`, `next=`\n* Track whether the value is derived from URL/query/storage/API.\n\nFix:\n\n* Implement a shared `safeUrl()` utility:\n\n  * Parse with `new URL(value, base)`\n  * Enforce scheme allowlist and host allowlist (or enforce same-origin)\n  * For redirects: allow only relative paths (starting with `/`) or a strict allowlist of absolute origins.\n* Fall back to a safe default when validation fails.\n\nNotes:\n\n* OWASP explicitly notes React’s `dangerouslySetInnerHTML` risk and also states React cannot safely handle `javascript:` or `data:` URLs without specialized validation. ([OWASP Cheat Sheet Series][9])\n\n---\n\n### REACT-MARKUP-001: Markdown / rich text rendering must be configured safely\n\nSeverity: Medium\n\nRequired:\n\n* MUST assume markdown/rich text can be attacker-controlled if it comes from users or CMS.\n* MUST ensure raw HTML is not rendered unless sanitized.\n* SHOULD prefer markdown renderers that:\n\n  * Do not allow raw HTML by default, or\n  * Can be configured to disallow raw HTML, or\n  * Sanitize HTML output before rendering.\n\nInsecure patterns:\n\n* Markdown rendering with “raw HTML passthrough” enabled (e.g., options/plugins that allow HTML).\n* Rendering user-provided SVG/MathML/HTML inline without sanitization.\n\nDetection hints:\n\n* Search for common libraries and risky options:\n\n  * `marked`, `markdown-it`, `react-markdown`, `rehype-raw`, `sanitize: false`, `allowDangerousHtml`, etc.\n* Look for `dangerouslySetInnerHTML` used with “markdown output”.\n\nFix:\n\n* Disable raw HTML passthrough.\n* Sanitize output with a proven sanitizer (e.g., DOMPurify) before rendering.\n\nNotes:\n\n* OWASP XSS guidance emphasizes that framework escape hatches require output encoding and/or HTML sanitization. ([OWASP Cheat Sheet Series][9])\n\n---\n\n### REACT-TT-001: Use Trusted Types (with CSP) to harden DOM XSS sinks where feasible\n\nSeverity: Low\n\nRequired:\n\n* SHOULD consider enabling Trusted Types in report-only mode first, then enforce once violations are addressed.\n* SHOULD centralize Trusted Types policies and treat them as high-risk code requiring review.\n* MUST NOT create permissive policies that simply “pass through” untrusted strings.\n\nInsecure patterns:\n\n* A Trusted Types policy that returns the raw string without sanitization for HTML sinks.\n* Many scattered policies across the codebase (hard to audit).\n\nDetection hints:\n\n* Search for:\n\n  * `trustedTypes.createPolicy`\n  * CSP directives: `require-trusted-types-for`, `trusted-types`\n* Search for remaining DOM sinks (REACT-DOM-001).\n\nFix:\n\n* Implement a small number of tightly scoped policies:\n\n  * HTML policy uses sanitizer (DOMPurify or equivalent).\n  * Script URL policy uses strict allowlists.\n* Run in report-only mode, fix violations, then enforce.\n\nNotes:\n\n* MDN describes Trusted Types as a way to ensure input is transformed (commonly sanitized) before being passed to injection sinks, and highlights HTML sinks (`innerHTML`, `document.write`) and JS URL sinks (`script.src`). ([MDN Web Docs][3])\n* The W3C Trusted Types spec frames this as reducing DOM XSS risk by locking down sinks to typed values created by reviewed policies. ([W3C][15])\n\n---\n\n### REACT-CSP-001: Deploy and maintain a CSP as defense-in-depth (especially when rendering untrusted content)\n\nSeverity: Medium to High\n\nRequired:\n\n* SHOULD deploy CSP in production; MUST do so for apps that render untrusted content or integrate third-party scripts.\n* SHOULD avoid `unsafe-inline` and `unsafe-eval` when possible.\n* SHOULD use CSP nonces/hashes for inline scripts if needed, and keep policy realistic.\n* SHOULD use CSP to require/encourage SRI where appropriate.\n\nInsecure patterns:\n\n* No CSP at all on the app shell (SPA entry HTML).\n* CSP that relies on `unsafe-inline`/`unsafe-eval` broadly without justification.\n* `script-src *` or overly broad sources.\n\nDetection hints:\n\n* Look for CSP configuration:\n\n  * Server/CDN config, headers in `index.html` responses, or framework config.\n* If absent in repo, mark as “verify at edge”.\n\nFix:\n\n* Add CSP via HTTP response headers (preferred).\n* Start with report-only to reduce breakage, then enforce.\n\nNotes:\n\n* OWASP describes CSP as “defense in depth” against XSS and notes it can help enforce SRI even on static sites, but should not be the only defense. ([OWASP Cheat Sheet Series][2])\n\n---\n\n### REACT-SRI-001: Use Subresource Integrity (SRI) for third-party scripts and styles (or self-host)\n\nSeverity: Low\n\nRequired:\n\n* MUST treat third-party JS as equivalent to running arbitrary code in your origin.\n* If loading from a CDN or third party:\n\n  * SHOULD use SRI (`integrity=...`) and `crossorigin` where applicable.\n  * SHOULD pin exact versions (avoid “latest” URLs).\n  * SHOULD prefer self-hosting for critical code.\n\nInsecure patterns:\n\n* `<script src=\"https://cdn.example.com/lib/latest.js\"></script>` with no integrity.\n* Tag managers that dynamically load arbitrary scripts without governance.\n\nDetection hints:\n\n* Search in `public/index.html`, templates, or SSR wrappers for:\n\n  * `<script src=`, `<link rel=\"stylesheet\" href=`\n  * Tag manager snippets (GTM, Segment, etc.)\n* Identify scripts loaded dynamically in runtime JS.\n\nFix:\n\n* Add SRI hashes for stable third-party assets or self-host.\n* Apply governance controls for tag managers (see REACT-3P-001).\n\nNotes:\n\n* MDN describes SRI as a security feature enabling browsers to verify fetched resources (e.g., from a CDN) haven’t been manipulated by checking a cryptographic hash. ([MDN Web Docs][7])\n* OWASP CSP guidance notes CSP can enforce SRI and is useful even on static sites. ([OWASP Cheat Sheet Series][2])\n\n---\n\n### REACT-3P-001: Third-party JavaScript and tag managers must be minimized and governed\n\nSeverity: High\n\nRequired:\n\n* MUST minimize third-party scripts and treat each as a supply-chain risk.\n* MUST know exactly what third-party JS executes in your origin and why.\n* SHOULD implement governance:\n\n  * Review and pin versions (or mirror in-house).\n  * Restrict data access (data-layer approach).\n  * Use SRI and CSP; consider sandboxing untrusted UI in iframes where possible.\n\nInsecure patterns:\n\n* Unreviewed analytics/ads scripts running with full access to DOM, cookies, storage, and user data.\n* Tag managers that can be changed by non-engineering roles with no change control.\n\nDetection hints:\n\n* Search for common vendor snippets in HTML/JS:\n\n  * GTM, Segment, Hotjar, FullStory, etc.\n* Look for dynamic script insertion:\n\n  * `document.createElement('script')`, `.src = ...`, `.appendChild(script)`\n\nFix:\n\n* Reduce to only necessary vendors.\n* Where feasible:\n\n  * Self-host or mirror scripts.\n  * Use SRI.\n  * Limit data exposure via a controlled data layer.\n\nNotes:\n\n* OWASP notes third-party JS server compromise can inject malicious JS, and highlights risks like arbitrary code execution and disclosure of sensitive info to third parties. ([OWASP Cheat Sheet Series][5])\n\n---\n\n### REACT-AUTH-001: Token and session handling must be resilient to XSS (avoid sensitive storage in Web Storage)\n\nSeverity: Medium\n\nRequired:\n\n* SHOULD avoid storing session identifiers or long-lived tokens in `localStorage` (and generally in Web Storage) because XSS can exfiltrate them.\n* If tokens must exist client-side:\n\n  * SHOULD prefer in-memory storage with short lifetimes and refresh mechanisms.\n  * MUST scope and rotate tokens; avoid long-lived bearer tokens in persistent storage.\n* SHOULD prefer HTTPOnly cookies for session tokens when possible (requires CSRF strategy: see REACT-CSRF-001).\n\nInsecure patterns:\n\n* `localStorage.setItem('token', ...)` / `sessionStorage.setItem('token', ...)` for auth tokens.\n* Persisting refresh tokens in `localStorage`.\n* Treating data from Web Storage as trusted.\n\nDetection hints:\n\n* Grep for: `localStorage.`, `sessionStorage.`, `setItem(`, `getItem(`, `token`, `jwt`, `refresh`\n* Search auth code for “remember me” storing tokens persistently.\n\nFix:\n\n* Move to HTTPOnly cookies (server change) + CSRF protections, or use short-lived in-memory tokens.\n* Reduce token scope and lifetime.\n\nNotes:\n\n* OWASP HTML5 guidance recommends avoiding sensitive info and session identifiers in local storage and warns that a single XSS can steal all data in Web Storage. ([OWASP Cheat Sheet Series][4])\n* OAuth browser-based apps guidance discusses that tokens stored in persistent browser storage like localStorage can be accessible to malicious JS (e.g., via XSS). ([IETF Datatracker][16])\n\n---\n\n### REACT-CSRF-001: Cookie-authenticated, state-changing requests MUST be CSRF-protected\n\nSeverity: High\n\nNOTE: If the application does not use cookie based auth (using Authentication header for example), then CSRF is not a concern.\n\nRequired:\n\n* If the app relies on cookies for authentication:\n\n  * MUST protect state-changing requests (POST/PUT/PATCH/DELETE) against CSRF.\n  * SHOULD include a CSRF token mechanism (synchronizer token or double-submit cookie) or other robust pattern appropriate to the backend.\n  * SHOULD use SameSite cookies as defense-in-depth, not as the sole defense.\n\nInsecure patterns:\n\n* `fetch('/api/transfer', { method: 'POST', credentials: 'include' })` with no CSRF token/header, relying only on cookies.\n* Using GET for state-changing operations.\n\nDetection hints:\n\n* Enumerate state-changing network calls and check:\n\n  * Is `credentials: 'include'` or `withCredentials: true` used?\n  * Is a CSRF token header included (e.g., `X-CSRF-Token`)?\n* Search for “csrf” utilities; if absent, treat as suspicious.\n\nFix:\n\n* Add CSRF token flow:\n\n  * Fetch token from a safe endpoint and attach to state-changing requests.\n  * Validate server-side.\n* Keep SameSite cookies and Origin/Referer validation as defense-in-depth.\n\nNotes:\n\n* OWASP CSRF guidance explains SameSite behavior (Lax/Strict/None) as a defense-in-depth technique and why Lax is often the usability/security balance, but it is not a complete substitute for CSRF protections. ([OWASP Cheat Sheet Series][6])\n\n---\n\n### REACT-AUTHZ-001: Do not rely on frontend-only authorization\n\nSeverity: High (only if used as primary protection)\n\nRequired:\n\n* MUST treat all frontend authorization checks as UX only.\n* MUST enforce authorization on the server for any protected resource or action.\n\nInsecure patterns:\n\n* “Protected” actions hidden in UI but callable by API without server checks.\n* Client checks like `if (user.isAdmin) { showAdminPanel(); }` with no server-side enforcement.\n\nDetection hints:\n\n* Look for UI gating around sensitive actions and verify server endpoints enforce authorization.\n* In a frontend-only audit, report as “client checks are not security; verify backend”.\n\nFix:\n\n* Add/confirm server-side authorization checks.\n* Keep frontend gating only as convenience.\n\nNotes:\n\n* This is a general web app security property; React cannot protect server resources by itself.\n\n---\n\n### REACT-NET-001: Prevent data exfiltration and credential leakage via dynamic outbound requests\n\nSeverity: Medium to High\n\nRequired:\n\n* MUST avoid making authenticated requests to attacker-controlled origins.\n* SHOULD avoid allowing user input to control request destination (scheme/host/port).\n* SHOULD centralize network clients (fetch/axios) with:\n\n  * fixed `baseURL` (or strict allowlist),\n  * strict handling of redirects,\n  * explicit `credentials` usage.\n\nInsecure patterns:\n\n* `fetch(userProvidedUrl, { credentials: 'include' })`\n* `axios.create({ baseURL: userProvidedBase })`\n* “URL fetch/preview” features in the client that hit arbitrary domains with sensitive headers.\n\nDetection hints:\n\n* Search for `fetch(` / `axios(` where the first argument or `baseURL` is derived from:\n\n  * query params, localStorage, API responses, postMessage\n* Search for `credentials: 'include'`, `withCredentials: true`.\n\nFix:\n\n* Enforce destination allowlists; disallow cross-origin requests unless explicitly required.\n* Strip credentials/Authorization headers for any non-allowlisted destination.\n\nNotes:\n\n* Even if the browser limits some cross-origin behavior, leaking tokens/headers to untrusted endpoints is still a common failure mode.\n\n---\n\n### REACT-REDIRECT-001: Prevent open redirects and untrusted navigation\n\nSeverity: Medium\n\nRequired:\n\n* MUST validate redirect/navigation targets derived from untrusted input (`next`, `returnTo`, `redirect`).\n* SHOULD only allow same-site relative paths, or a strict allowlist of trusted origins for absolute URLs.\n\nInsecure patterns:\n\n* `window.location.href = new URLSearchParams(location.search).get('next')`\n* `navigate(next)` where `next` comes from query params.\n\nDetection hints:\n\n* Search for: `next`, `returnTo`, `redirect`, `window.location`, `navigate(`\n* Trace origin of the redirect target.\n\nFix:\n\n* Only allow relative paths (`/^\\/[^\\s]*$/`) or allowlisted origins.\n* Fall back to a safe default (e.g., `/`) when invalid.\n\nNotes:\n\n* Open redirects are frequently used in phishing and can undermine SSO/OAuth flows.\n\n---\n\n### REACT-SW-001: Service workers are high-privilege; require HTTPS and safe caching/update rules\n\nSeverity: Medium\n\nRequired:\n\n* MUST serve service workers over HTTPS (except `localhost` dev), and deploy only in secure contexts.\n* MUST avoid caching sensitive authenticated API responses unless explicitly designed and threat-modeled.\n* SHOULD implement safe update strategy (prompt reload, versioned caches, remove old caches on activate).\n\nInsecure patterns:\n\n* Registering a service worker for an authenticated app and caching “everything” indiscriminately.\n* Long-lived caches containing PII or user-specific content shared across accounts.\n\nDetection hints:\n\n* Search for:\n\n  * `navigator.serviceWorker.register`\n  * `workbox`, `precacheAndRoute`, custom `fetch` handlers\n* Inspect caching patterns (`caches.open`, `cache.put`, `respondWith`).\n\nFix:\n\n* Restrict caching to static assets only (JS/CSS/images) unless you have a designed offline model.\n* Ensure cache keys are user-scoped if user-specific data must be cached.\n* Provide a clear update mechanism.\n\nNotes:\n\n* MDN notes service workers require HTTPS for security reasons and act like a proxy for requests/responses. ([MDN Web Docs][10])\n* “Secure contexts” exist to prevent MITM attackers from accessing powerful APIs; service workers are an example of such a powerful feature. ([MDN Web Docs][18])\n\n---\n\n### REACT-HEADERS-001: Ensure essential security headers are set for the React app shell (app or edge)\n\nSeverity: Medium\n\nRequired (typical SPA served from an origin):\n\n* SHOULD set:\n\n  * CSP (`Content-Security-Policy`)\n  * `X-Content-Type-Options: nosniff`\n  * Clickjacking protection (`frame-ancestors` in CSP and/or `X-Frame-Options`)\n  * `Referrer-Policy`\n  * `Permissions-Policy` as appropriate\n* MUST ensure these are set somewhere (CDN/edge/server), even if not in repo.\n\nInsecure patterns:\n\n* No security headers anywhere (app or edge).\n* CSP missing on apps that render untrusted content or use third-party scripts.\n\nDetection hints:\n\n* Check server/CDN config in repo (nginx, Cloudflare, Vercel config, etc.).\n* If absent, flag as “verify at runtime/edge”.\n\nFix:\n\n* Set headers centrally at the edge.\n* Keep CSP realistic and iterative (report-only → enforce).\n\nNotes:\n\n* MDN clickjacking guidance discusses defenses including `X-Frame-Options` and CSP `frame-ancestors`. ([MDN Web Docs][8])\n* OWASP CSP guidance explains delivery via response headers and recommends headers as the preferred mechanism. ([OWASP Cheat Sheet Series][2])\n\n---\n\n### REACT-POSTMSG-001: `postMessage` must validate origin and treat payload as untrusted data\n\nSeverity: Medium to High (depends on what messages can do)\n\nRequired:\n\n* MUST specify exact `targetOrigin` when sending messages (not `*`) unless there is a strict reason.\n* MUST validate `event.origin` on receipt and validate message shape.\n* MUST NOT evaluate message data as code or insert it into the DOM as HTML.\n\nInsecure patterns:\n\n* `window.postMessage(data, '*')` to unknown targets.\n* Receiving:\n\n  * `window.addEventListener('message', (e) => { eval(e.data) })`\n  * `element.innerHTML = e.data`\n\nDetection hints:\n\n* Search: `postMessage(`, `addEventListener('message'`\n* Check for origin checks and safe handling.\n\nFix:\n\n* Add strict origin allowlists and schema validation (e.g., zod).\n* Treat message payload strictly as data; render safely via React.\n\nNotes:\n\n* OWASP HTML5 guidance recommends specifying expected origin for `postMessage`, checking sender origin, validating data, and avoiding eval/innerHTML with message content. ([OWASP Cheat Sheet Series][4])\n\n---\n\n### REACT-FILE-001: File uploads and previews must not create client-side active content vulnerabilities\n\nSeverity: Medium (can be High if stored-XSS possible)\n\nRequired:\n\n* MUST treat user-uploaded files and previews as potentially malicious.\n* MUST NOT render uploaded HTML/SVG/other active content inline unless sanitized and explicitly required.\n* SHOULD validate file types client-side for UX, but MUST rely on server-side validation for security.\n\nInsecure patterns:\n\n* Rendering user-uploaded HTML as content.\n* Inline rendering of untrusted SVG/HTML via `dangerouslySetInnerHTML` or `<iframe srcdoc=...>` without sanitization.\n\nDetection hints:\n\n* Search for upload components and preview logic:\n\n  * `input type=\"file\"`, `FileReader`, `URL.createObjectURL`, `<iframe>`, `<object>`, `<embed>`.\n* Trace where uploaded content is later displayed.\n\nFix:\n\n* Restrict accepted types, sanitize where needed, and prefer download/attachment flows for risky types.\n* Ensure server enforces the real policy (type checking, renaming, scanning, storing outside webroot).\n\nNotes:\n\n* OWASP file upload guidance highlights allowlisting extensions, validating file type, generating filenames, limiting size, storing outside webroot, and considering “client-side active content (XSS, CSRF, etc.)” when files are publicly retrievable. ([OWASP Cheat Sheet Series][19])\n\n---\n\n### REACT-SUPPLY-001: Dependency and supply-chain hygiene (frontend + build tooling)\n\nSeverity: Low\n\nRequired:\n\n* MUST use a lockfile and enforce reproducible installs in CI.\n* SHOULD regularly audit dependencies and respond quickly to advisories for:\n\n  * React, react-dom, router libs, build tooling (Vite/Webpack), sanitizers, auth libs, etc.\n* SHOULD reduce exposure to install-time script attacks and typosquatting risk.\n\nAudit focus:\n\n* CI should use `npm ci` (or Yarn frozen lockfile / pnpm equivalent) to prevent drift.\n* Use vulnerability scanning (`npm audit`, GitHub Dependabot/alerts, etc.).\n\nInsecure patterns:\n\n* No lockfile or lockfile ignored in CI.\n* `npm install` in CI producing non-reproducible builds.\n* Unpinned or unreviewed high-risk deps; sudden major updates without review.\n* Blindly running install scripts from third-party packages.\n\nDetection hints:\n\n* Check for lockfiles: `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`.\n* Check CI scripts for `npm install` vs `npm ci`.\n* Search for `postinstall` scripts and suspicious build steps.\n\nFix:\n\n* Use lockfile and enforce it in CI (e.g., `npm ci`).\n* Run audits regularly; pin/upgrade responsibly.\n* Consider restricting install scripts where feasible.\n\nNotes:\n\n* npm docs describe `npm audit` as submitting the project dependency tree to the registry to receive a report of known vulnerabilities and (optionally) applying remediations via `npm audit fix`, while noting some vulns require manual review. ([npm Docs][20])\n* npm docs describe `npm ci` as intended for automated/CI environments, requiring an existing lockfile and failing if `package.json` and lockfile do not match. ([npm Docs][21])\n* OWASP NPM security guidance recommends enforcing the lockfile and explicitly calls out `npm ci` / `yarn install --frozen-lockfile` to abort on inconsistencies, and highlights the risk of install-time scripts and the option to use `--ignore-scripts` to reduce attack surface. ([OWASP Cheat Sheet Series][22])\n\n---\n\n## 5) Practical scanning heuristics (how to “hunt”)\n\nWhen actively scanning, use these high-signal patterns:\n\n* Raw HTML / XSS escape hatches:\n\n  * `dangerouslySetInnerHTML`, `__html:`\n  * Markdown HTML passthrough flags: `rehype-raw`, `allowDangerousHtml`, `sanitize: false`\n* DOM XSS sinks:\n\n  * `innerHTML`, `outerHTML`, `insertAdjacentHTML`, `document.write`, `DOMParser`, `createContextualFragment`\n* Dangerous JS execution:\n\n  * `eval(`, `new Function(`, `setTimeout(\"`, `setInterval(\"`\n* Untrusted URL injection / navigation:\n\n  * `href={` / `src={` with untrusted values\n  * `window.location`, `location.href`, `window.open`, `navigate(`\n  * Query params: `next`, `returnTo`, `redirect`\n* Token/session risk:\n\n  * `localStorage.setItem`, `sessionStorage.setItem`, `getItem(` with `token`, `jwt`, `refresh`\n* Cookie/CSRF coupling:\n\n  * `credentials: 'include'`, `withCredentials: true` on state-changing requests without CSRF headers\n* Third-party scripts:\n\n  * `<script src=...>` in `public/index.html`\n  * Tag manager snippets and dynamic script insertion\n* Service workers:\n\n  * `navigator.serviceWorker.register`, Workbox usage, custom `fetch` handlers\n* postMessage:\n\n  * `postMessage(` with `*`, missing `event.origin` checks\n* Supply chain:\n\n  * Missing lockfile, CI uses `npm install`, no audit step, risky postinstall scripts\n\nAlways try to confirm:\n\n* data origin (untrusted vs trusted)\n* sink type (React escape hatch vs DOM sink vs navigation vs storage)\n* protective controls present (sanitization, allowlists, CSP/Trusted Types, CSRF tokens, headers, governance)\n\n---\n\n## 6) Sources (accessed 2026-01-26)\n\nPrimary React documentation:\n\n* React 19 stable announcement — `https://react.dev/blog/2024/12/05/react-19` ([React][23])\n* React DOM docs: `dangerouslySetInnerHTML` warning — `https://react.dev/reference/react-dom/components/common#dangerouslysetting-the-inner-html` ([React][12])\n* React (legacy) JSX escaping statement — `https://legacy.reactjs.org/docs/introducing-jsx.html` ([React][14])\n\nOWASP Cheat Sheet Series:\n\n* Cross Site Scripting Prevention (framework escape hatches; React `dangerouslySetInnerHTML`; URL validation notes) — `https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][9])\n* Content Security Policy — `https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][2])\n* Cross-Site Request Forgery Prevention — `https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][6])\n* HTML5 Security (Web Storage, postMessage, tabnabbing, sandboxed frames) — `https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][4])\n* Third Party JavaScript Management — `https://cheatsheetseries.owasp.org/cheatsheets/Third_Party_Javascript_Management_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][5])\n* File Upload — `https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][19])\n* NPM Security best practices — `https://cheatsheetseries.owasp.org/cheatsheets/NPM_Security_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][22])\n\nBrowser / platform references (MDN, W3C):\n\n* Trusted Types API — `https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API` ([MDN Web Docs][3])\n* W3C Trusted Types spec — `https://www.w3.org/TR/trusted-types/` ([W3C][15])\n* Subresource Integrity — `https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity` ([MDN Web Docs][7])\n* Clickjacking defenses overview — `https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Clickjacking` ([MDN Web Docs][8])\n* Using Service Workers (HTTPS requirement; proxy-like behavior) — `https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers` ([MDN Web Docs][10])\n* Secure contexts (powerful APIs restricted to HTTPS) — `https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts` ([MDN Web Docs][18])\n* Link `rel` values (noopener/noreferrer) — `https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel` ([MDN Web Docs][17])\n\nBuild tooling / env exposure references:\n\n* Create React App env variables warning — `https://create-react-app.dev/docs/adding-custom-environment-variables/` ([create-react-app.dev][1])\n* Vite env variables security notes — `https://vite.dev/guide/env-and-mode` ([vitejs][11])\n\nAuth/token storage guidance:\n\n* OAuth 2.0 for Browser-Based Apps (token storage discussion) — `https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps` ([IETF Datatracker][16])\n\nDependency tooling references:\n\n* npm audit docs — `https://docs.npmjs.com/cli/v10/commands/npm-audit/` ([npm Docs][20])\n* npm ci docs — `https://docs.npmjs.com/cli/v10/commands/npm-ci/` ([npm Docs][21])\n\nSanitizer reference:\n\n* DOMPurify — `https://github.com/cure53/DOMPurify` ([GitHub][13])\n\n[1]: https://create-react-app.dev/docs/adding-custom-environment-variables/ \"Adding Custom Environment Variables | Create React App\"\n[2]: https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html \"Content Security Policy - OWASP Cheat Sheet Series\"\n[3]: https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API \"Trusted Types API - Web APIs | MDN\"\n[4]: https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html \"HTML5 Security - OWASP Cheat Sheet Series\"\n[5]: https://cheatsheetseries.owasp.org/cheatsheets/Third_Party_Javascript_Management_Cheat_Sheet.html \"Third Party Javascript Management - OWASP Cheat Sheet Series\"\n[6]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html \"Cross-Site Request Forgery Prevention - OWASP Cheat Sheet Series\"\n[7]: https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity \"Subresource Integrity - Security | MDN\"\n[8]: https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Clickjacking \"Clickjacking - Security | MDN\"\n[9]: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html \"Cross Site Scripting Prevention - OWASP Cheat Sheet Series\"\n[10]: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers \"Using Service Workers - Web APIs | MDN\"\n[11]: https://vite.dev/guide/env-and-mode \"Env Variables and Modes | Vite\"\n[12]: https://react.dev/reference/react-dom/components/common \"Common components (e.g. <div>) – React\"\n[13]: https://github.com/cure53/DOMPurify \"GitHub - cure53/DOMPurify: DOMPurify - a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. DOMPurify works with a secure default, but offers a lot of configurability and hooks. Demo:\"\n[14]: https://legacy.reactjs.org/docs/introducing-jsx.html \"Introducing JSX – React\"\n[15]: https://www.w3.org/TR/trusted-types/ \"Trusted Types\"\n[16]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps \"\n            \n                draft-ietf-oauth-browser-based-apps-26\n            \n        \"\n[17]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel \"HTML attribute: rel - HTML | MDN\"\n[18]: https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts \"Secure contexts - Security | MDN\"\n[19]: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html \"File Upload - OWASP Cheat Sheet Series\"\n[20]: https://docs.npmjs.com/cli/v10/commands/npm-audit \"npm-audit | npm Docs\"\n[21]: https://docs.npmjs.com/cli/v10/commands/npm-ci \"npm-ci | npm Docs\"\n[22]: https://cheatsheetseries.owasp.org/cheatsheets/NPM_Security_Cheat_Sheet.html \"NPM Security - OWASP Cheat Sheet Series\"\n[23]: https://react.dev/blog/2024/12/05/react-19 \"React v19 – React\"\n"
  },
  {
    "path": "skills/.curated/security-best-practices/references/javascript-typescript-vue-web-frontend-security.md",
    "content": "# Vue.js Web Security Spec (Vue 3.x, TypeScript/JavaScript, common tooling: Vite)\n\nThis document is designed as a **security spec** that supports:\n\n1. **Secure-by-default code generation** for new Vue code.\n2. **Security review / vulnerability hunting** in existing Vue code (passive “notice issues while working” and active “scan the repo and report findings”).\n\nIt is intentionally written as a set of **normative requirements** (“MUST/SHOULD/MAY”) plus **audit rules** (what bad patterns look like, how to detect them, and how to fix/mitigate them).\n\n---\n\n## 0) Safety, boundaries, and anti-abuse constraints (MUST FOLLOW)\n\n* MUST NOT request, output, log, or commit secrets (API keys, passwords, private keys, session cookies, auth tokens).\n* MUST NOT “fix” security by disabling protections (e.g., weakening CSP, turning on unsafe template compilation, using `v-html` as a shortcut, bypassing backend auth, or “just store the token in localStorage”).\n* MUST provide **evidence-based findings** during audits: cite file paths, code snippets, and configuration values that justify the claim.\n* MUST treat uncertainty honestly: if a protection might exist at the edge (CDN, reverse proxy, WAF, server headers), report it as “not visible in repo; verify runtime/infra config”.\n* MUST remember the frontend trust model: **any code shipped to browsers is attacker-readable and attacker-modifiable**. Secrets and “security enforcement” cannot rely on frontend-only logic.\n\n---\n\n## 1) Operating modes\n\n### 1.1 Generation mode (default)\n\nWhen asked to write new Vue code or modify existing code:\n\n* MUST follow every **MUST** requirement in this spec.\n* SHOULD follow every **SHOULD** requirement unless the user explicitly says otherwise.\n* MUST prefer safe-by-default framework features and proven libraries over custom security code.\n* MUST avoid introducing new risky sinks (runtime template compilation, `v-html` / `innerHTML`, unsafe URL navigation, dynamic script injection, etc.). ([Vue.js][1])\n\n### 1.2 Passive review mode (always on while editing)\n\nWhile working anywhere in a Vue repo (even if the user did not ask for a security scan):\n\n* MUST “notice” violations of this spec in touched/nearby code.\n* SHOULD mention issues as they come up, with a brief explanation + safe fix.\n\n### 1.3 Active audit mode (explicit scan request)\n\nWhen the user asks to “scan”, “audit”, or “hunt for vulns”:\n\n* MUST systematically search the codebase for violations of this spec.\n* MUST output findings in a structured format (see §2.3).\n\nRecommended audit order:\n\n1. Build/deploy entrypoints and hosting config (Docker, CI, static hosting, SSR server).\n2. Secrets exposure (env usage, `.env*`, hard-coded keys). ([vitejs][2])\n3. XSS surface: templates, `v-html` / `innerHTML`, URL/style injection, DOM APIs. ([Vue.js][1])\n4. Auth/session handling in the browser (token storage, credentialed requests, CSRF integration). ([Vue.js][1])\n5. Routing/navigation (open redirects, “return_to/next”, unsafe external navigation). ([Vue.js][1])\n6. Third-party scripts and content (CDN assets, analytics, widgets, iframes). ([Vue.js][1])\n7. Security headers and browser hardening expectations (CSP, clickjacking). ([Vue.js][1])\n8. SSR-specific concerns (state serialization, template boundaries) when applicable. ([Vue.js][1])\n\n---\n\n## 2) Definitions and review guidance\n\n### 2.1 Untrusted input (treat as attacker-controlled unless proven otherwise)\n\nIn a Vue app, untrusted input includes (non-exhaustive):\n\n* Anything from APIs: `fetch`, `axios`, GraphQL responses, webhooks, third-party SDKs.\n* Router-controlled data: `route.params`, `route.query`, `route.hash`, and anything derived from `window.location`.\n* User-controlled persisted content: DB-backed content displayed in the UI (comments, profiles, CMS content).\n* Browser-controlled storage: `localStorage`, `sessionStorage`, `IndexedDB`.\n* Cross-window messages: `postMessage` inputs.\n* Anything that can be influenced by an attacker through DOM clobbering or injected HTML (especially if Vue is mounted onto non-sterile DOM). ([Vue.js][1])\n\n### 2.2 State-changing action (frontend perspective)\n\nAn action is state-changing if it can:\n\n* Create/update/delete data via API calls.\n* Change authentication/session state (login, logout, refresh token).\n* Trigger privileged operations (payments, admin actions).\n* Cause side effects (sending emails, triggering webhooks, changing account settings).\n\n### 2.3 Required audit finding format\n\nFor each issue found, output:\n\n* Rule ID:\n* Severity: Critical / High / Medium / Low\n* Location: file path + component/function + line(s)\n* Evidence: the exact code/config snippet\n* Impact: what could go wrong, who can exploit it\n* Fix: safe change (prefer minimal diff)\n* Mitigation: defense-in-depth if immediate fix is hard\n* False positive notes: what to verify if uncertain\n\n---\n\n## 3) Secure baseline: minimum production configuration (MUST in production)\n\nThis is the smallest “production baseline” that prevents common Vue/front-end misconfigurations.\n\n* MUST ship a **production build** (not a development build or dev server). ([Vue.js][3])\n* MUST NOT ship secrets in frontend bundles; treat all client-exposed env variables as public. ([vitejs][2])\n* MUST NOT render non-trusted templates or allow user-provided Vue templates (equivalent to arbitrary JS execution). ([Vue.js][1])\n* SHOULD avoid raw HTML injection (`v-html`, `innerHTML`) unless content is trusted or strongly sandboxed. ([Vue.js][1])\n* SHOULD deploy baseline security headers (especially CSP and clickjacking defenses) at the server/CDN layer. ([OWASP Cheat Sheet Series][4])\n* SHOULD use safe auth patterns (prefer HttpOnly cookies for session tokens; coordinate with backend on CSRF). ([Vue.js][1])\n\n---\n\n## 4) Rules (generation + audit)\n\nEach rule contains: required practice, insecure patterns, detection hints, and remediation.\n\n### VUE-DEPLOY-001: Do not run dev/preview servers in production\n\nSeverity: High\n\nRequired:\n\n* MUST NOT deploy the Vite/Vue dev server (`vite`, `npm run dev`, HMR) as the production server.\n* MUST NOT use `vite preview` as a production server. ([vitejs][5])\n* MUST build (`vite build`) and serve the built assets using a production-grade static server/CDN, or a production SSR server if you are doing SSR. ([vitejs][6])\n\nInsecure patterns:\n\n* Docker/Procfile/systemd running `vite`, `npm run dev`, or `vite preview` as the production entrypoint.\n* Publicly exposed HMR endpoints.\n\nDetection hints:\n\n* Search: `vite`, `npm run dev`, `pnpm dev`, `yarn dev`, `vite preview`, `vue-cli-service serve`.\n* Check Docker `CMD`, `ENTRYPOINT`, CI deploy scripts, platform config.\n\nFix:\n\n* Build artifacts with `vite build`.\n* Serve `dist/` with hardened hosting (CDN/static server) or integrate into your backend server as static assets.\n\nNotes:\n\n* Using dev/preview servers locally is fine; only flag if it is the production entrypoint.\n\n---\n\n### VUE-DEPLOY-002: Use Vue production builds and keep devtools off in production\n\nSeverity: Medium (High if production devtools/debug hooks are enabled)\n\nRequired:\n\n* If loading Vue from CDN/self-host without a bundler, MUST use the `.prod.js` builds in production. ([Vue.js][3])\n* SHOULD ensure production bundles do not enable Vue devtools in production builds, and SHOULD not intentionally enable production devtools flags. ([Vue.js][7])\n\nInsecure patterns:\n\n* Production includes development build artifacts.\n* Explicitly enabling production devtools/diagnostic hooks.\n\nDetection hints:\n\n* Search HTML for `vue.global.js` / non-`.prod.js` variants when using CDN builds.\n* Search build config for Vue feature flags like `__VUE_PROD_DEVTOOLS__`. ([Vue.js][7])\n\nFix:\n\n* Switch to production build artifacts and ensure compile-time flags are configured for production.\n\n---\n\n### VUE-SECRETS-001: Never ship secrets in frontend code or env variables\n\nSeverity: High (Critical if real credentials are exposed)\n\nRequired:\n\n* MUST treat all frontend code and configuration as public.\n* MUST NOT embed secrets in:\n\n  * source code\n  * `.env` files committed to repo\n  * `import.meta.env.*` variables included in the bundle\n* MUST assume any env var that ends up in the client bundle is attacker-readable. ([vitejs][2])\n\nInsecure patterns:\n\n* `VITE_API_KEY=...` containing a true secret (not just a public identifier).\n* Hard-coded API keys, private tokens, service credentials, signing keys in JS/TS.\n\nDetection hints:\n\n* Search: `VITE_`, `import.meta.env`, `.env`, `.env.production`, `.env.*.local`.\n* Grep for `API_KEY`, `SECRET`, `TOKEN`, `PRIVATE_KEY`, `BEGIN`, `sk-`, `AKIA`, etc.\n\nFix:\n\n* Move secrets to backend/edge functions.\n* Use backend-minted short-lived tokens for the browser when needed.\n\nNotes:\n\n* Vite specifically warns that `.env.*.local` should be gitignored and that `VITE_*` vars end up in the client bundle, so they must not contain sensitive info. ([vitejs][2])\n\n---\n\n### VUE-SECRETS-002: Do not broaden Vite env exposure\n\nSeverity: High\n\nRequired:\n\n* MUST NOT configure Vite to expose all environment variables to the client.\n* SHOULD keep `envPrefix` strict and explicit.\n\nInsecure patterns:\n\n* Setting `envPrefix` to overly broad values (or `''`) to “make env vars work”.\n* Custom scripts that inject server secrets into global variables in HTML at build time.\n\nDetection hints:\n\n* Check `vite.config.*` for `envPrefix`.\n* Look for `define: { 'process.env': ... }` or manual injection into `window.__CONFIG__`.\n\nFix:\n\n* Keep secrets server-side.\n* Only expose non-sensitive values intentionally designed to be public.\n\nNotes:\n\n* Vite’s docs explain that only prefixed variables are exposed and that exposed variables land in the client bundle. ([vitejs][2])\n\n---\n\n### VUE-XSS-001: Prefer Vue’s default escaping; avoid raw HTML injection\n\nSeverity: High\n\nRequired:\n\n* MUST rely on Vue’s automatic escaping for text interpolation and attribute binding where possible. ([Vue.js][1])\n* MUST NOT render user-provided HTML via:\n\n  * `v-html`\n  * `innerHTML` in render functions / JSX\n  * direct DOM APIs (`element.innerHTML`, `insertAdjacentHTML`)\n    unless the HTML is trusted or robustly sanitized and the risk is explicitly accepted. ([Vue.js][1])\n\nInsecure patterns:\n\n* `<div v-html=\"userProvidedHtml\"></div>`\n* `h('div', { innerHTML: userProvidedHtml })`\n* `<div innerHTML={userProvidedHtml}></div>`\n* `el.innerHTML = untrusted`\n\nDetection hints:\n\n* Search: `v-html`, `innerHTML`, `insertAdjacentHTML`, `DOMParser`, `document.write`.\n\nFix:\n\n* Render untrusted content as text (interpolation).\n* If HTML rendering is required (e.g., Markdown), sanitize with a well-maintained HTML sanitizer and apply defense-in-depth (CSP, Trusted Types). ([Vue.js][1])\n\nNotes:\n\n* Vue’s docs explicitly warn that user-provided HTML is never “100% safe” unless sandboxed or strictly self-only exposure. ([Vue.js][1])\n\n---\n\n### VUE-XSS-002: Never use non-trusted templates (client-side template/code injection)\n\nSeverity: Critical\n\nRequired:\n\n* MUST NOT use non-trusted content as a Vue component template.\n* MUST treat “user can write a Vue template” as “user can execute arbitrary JavaScript in your app”, and potentially in SSR contexts too. ([Vue.js][1])\n* SHOULD prefer the runtime-only build (templates compiled at build time) and avoid shipping the runtime compiler unless you have a vetted need.\n\nInsecure patterns:\n\n* `createApp({ template: '<div>' + userProvidedString + '</div>' }).mount(...)`\n* Storing templates in DB and compiling/rendering them in the browser.\n* Admin/CMS features that allow entering Vue template syntax.\n\nDetection hints:\n\n* Search: `template:` where the value is not a static string.\n* Search: `@vue/compiler-dom`, `compile(`, “runtime compiler” build selection, dynamic SFC compilation.\n* Search for “template editor”, “custom template”, “theme HTML” features.\n\nFix:\n\n* Treat templates as code: keep them developer-controlled.\n* If end-user customization is required, use a safe format (restricted Markdown subset) rendered via a sanitizer, or isolate in a sandboxed iframe.\n\n---\n\n### VUE-XSS-003: Do not mount Vue onto DOM that may contain user-provided server-rendered HTML\n\nSeverity: Medium\n\nRequired:\n\n* MUST NOT mount Vue on nodes that may contain server-rendered and user-provided content (because attacker-controlled HTML that is “safe as HTML” may become unsafe as a Vue template). ([Vue.js][1])\n* SHOULD mount Vue into a “sterile” root element and render the app’s DOM from Vue-controlled templates/components.\n\nInsecure patterns:\n\n* Server renders user content into `#app`, then Vue mounts on `#app` and compiles/interprets that DOM as a template.\n* “Sprinkling Vue” on large server-rendered pages that include user-generated content.\n\nDetection hints:\n\n* Check server templates (e.g., Rails/Django/Express templates) for user HTML inserted inside the Vue mount root.\n* Look for `mount('#app')` where `#app` includes server-rendered UGC.\n\nFix:\n\n* Move user-rendered HTML outside the Vue mount root, or render it in a safe way (text/sanitized HTML) from Vue components.\n\n---\n\n### VUE-XSS-004: Prevent URL injection in bindings and navigations\n\nSeverity: High\n\nRequired:\n\n* MUST validate/sanitize any user-influenced URL before binding to navigation sinks (`href`, `src`, `action`, `window.location`, `window.open`, router navigation to external).\n* MUST specifically prevent `javascript:` URL execution in bindings like `<a :href=\"userProvidedUrl\">`. ([Vue.js][1])\n* SHOULD validate protocol and destination (allowlist `https:` and expected hosts; allow `mailto:`/`tel:` only if intended).\n\nInsecure patterns:\n\n* `<iframe :src=\"userProvidedUrl\">`\n* `window.location = route.query.next`\n* `window.open(userProvidedUrl)`\n\nDetection hints:\n\n* Search: `:href=`, `:src=`, `window.location`, `location.href`, `window.open`, `router.push(` with untrusted input.\n* Look for `next`, `return_to`, `redirect` query params.\n\nFix:\n\n* Prefer internal navigation via route names/paths you control.\n* For external URLs: parse with `new URL(...)`, allowlist protocol/host, reject `javascript:` and other dangerous schemes.\n* Sanitize and validate on the backend before storing user URLs (Vue docs explicitly recommend backend sanitization). ([Vue.js][1])\n\n---\n\n### VUE-XSS-005: Prevent style/CSS injection and UI redress\n\nSeverity: Low\n\nRequired:\n\n* MUST NOT bind attacker-controlled CSS strings broadly (e.g., `:style=\"userProvidedStyles\"`).\n* SHOULD use Vue’s style object syntax and only allow safe, specific properties if user customization is needed. ([Vue.js][1])\n* SHOULD isolate “user can control layout/CSS” features inside sandboxed iframes.\n\nInsecure patterns:\n\n* `:style=\"userProvidedStyles\"` where styles are attacker-controlled.\n* Rendering user-provided `<style>` content (even if Vue blocks some patterns, don’t try to work around it).\n\nDetection hints:\n\n* Search: `:style=\"` bound to non-constant variables that originate from API/user content.\n* Search for “custom CSS”, “theme editor”, “profile CSS”.\n\nFix:\n\n* Allowlist properties and values; avoid raw style strings.\n* Use sandboxed iframes for rich user customization.\n\n---\n\n### VUE-XSS-006: Never bind user-provided JavaScript into event handler attributes\n\nSeverity: Critical\n\nRequired:\n\n* MUST NOT bind attacker-provided strings into event handler attributes (e.g., `onclick`, `onfocus`, etc.).\n* MUST treat “user-provided JS” as unsafe unless sandboxed and self-only exposure is guaranteed. ([Vue.js][1])\n\nInsecure patterns:\n\n* `<div :onclick=\"userProvidedString\">`\n* `<a :onmouseenter=\"userProvidedString\">`\n\nDetection hints:\n\n* Search: `:on` followed by event attribute names (`:onclick`, `:onload`, etc.).\n* Search for `setAttribute('on` patterns.\n\nFix:\n\n* Use real event listeners with developer-controlled handlers.\n* If you truly need user scripting, isolate it (sandboxed iframe + strict boundaries).\n\n---\n\n### VUE-ROUTER-001: Do not treat client-side route guards as authorization\n\nSeverity: High\n\nRequired:\n\n* MUST NOT rely on Vue Router guards, UI hiding, or client-side checks to enforce authorization.\n* MUST enforce authorization on the backend for every privileged action and sensitive data response. ([OWASP Cheat Sheet Series][8])\n\nInsecure patterns:\n\n* “Admin route is protected because `beforeEach` checks `user.isAdmin`.”\n* Sensitive API endpoints that assume “the frontend won’t call this unless allowed.”\n\nDetection hints:\n\n* Search `router.beforeEach` for role-based gating and see if the backend is also enforcing.\n* Look for “security by route meta” patterns (`meta.requiresAdmin`) with no server corroboration.\n\nFix:\n\n* Keep route guards as UX only (reduce accidental access), but enforce real checks server-side.\n\n---\n\n### VUE-ROUTER-002: Prevent open redirects and unsafe “return_to/next” handling\n\nSeverity: Low\n\nRequired:\n\n* MUST validate redirect destinations derived from untrusted input (`next`, `return_to`, `redirect`).\n* SHOULD allow only same-site relative paths or an explicit allowlist of destinations.\n* MUST NOT allow non `http` / `https` protos (such as `javascript:`)\n\nInsecure patterns:\n\n* `router.push(route.query.next as string)`\n* `window.location.href = route.query.redirect`\n\nDetection hints:\n\n* Search for `route.query.next`, `route.query.redirect`, `return_to`, `continue`, `callback`.\n* Trace the value into router/window navigation sinks.\n\nFix:\n\n* Allow only relative paths starting with `/` (and reject `//host`, `javascript:`, etc.).\n* Prefer redirecting to named routes you control.\n\nNotes:\n\n* Even Vue’s docs note that sanitized URLs still may not guarantee safe destinations. ([Vue.js][1])\n\n---\n\n### VUE-AUTH-001: Token storage must assume XSS is possible\n\nSeverity: Low\n\nRequired:\n\n* MUST assume any token accessible to JavaScript can be stolen via XSS.\n* SHOULD prefer HttpOnly cookies (set by the backend) for session tokens, combined with CSRF protections where relevant. ([Vue.js][1])\n* SHOULD avoid storing long-lived tokens (especially refresh tokens) in `localStorage`/`sessionStorage`.\n\nInsecure patterns:\n\n* `localStorage.setItem('token', ...)` for long-lived bearer tokens.\n* Storing refresh tokens in JS-accessible storage.\n\nDetection hints:\n\n* Search: `localStorage`, `sessionStorage`, `indexedDB`, `persist`, `pinia-plugin-persistedstate`.\n* Identify whether stored values are auth/session material.\n\nFix:\n\n* Prefer backend-managed sessions via HttpOnly cookies.\n* If bearer tokens are unavoidable, keep them short-lived, stored in memory, and rotate frequently; combine with strong XSS mitigations (CSP, Trusted Types, strict sanitization). ([OWASP Cheat Sheet Series][4])\n\n---\n\n### VUE-CSRF-001: Coordinate with the backend for CSRF when using cookies\n\nSeverity: High (for cookie-authenticated state-changing requests)\n\nNOTE: If the application is not using cookie based authentication (for example if it passes an Authorization header), then CSRF is not a concern\n\nRequired:\n\n* If API requests include cookies (`credentials: 'include'` / `withCredentials: true`) and cookies authenticate the user, MUST include CSRF protections coordinated with the backend (token/header patterns, Origin checks, SameSite cookies as defense-in-depth). ([Vue.js][1])\n* MUST NOT “solve CORS/CSRF errors” by disabling protections on the backend or using `mode: 'no-cors'` on the frontend.\n\nInsecure patterns:\n\n* `fetch(url, { credentials: 'include', method: 'POST', body: ... })` with no CSRF token/header usage anywhere.\n* Enabling cross-origin credentialed requests without strict origin allowlists (backend-side).\n\nDetection hints:\n\n* Search: `credentials: 'include'`, `withCredentials`, `xsrf`, `csrf`, `X-CSRF-Token`, `X-XSRF-TOKEN`.\n* Look at API wrapper modules for headers and cookie settings.\n\nFix:\n\n* Implement backend-issued CSRF tokens and require them on state-changing requests.\n* Keep cookies `SameSite=Lax/Strict` where compatible and verify Origin/Referer where appropriate (backend-driven). ([OWASP Cheat Sheet Series][9])\n\nNotes:\n\n* Vue’s docs explicitly say CSRF is primarily backend-addressed but recommends coordinating on CSRF token submission. ([Vue.js][1])\n\n---\n\n### VUE-HTTP-001: Do not put secrets in URLs; avoid leaking sensitive data in navigation/logs\n\nSeverity: Medium\n\nRequired:\n\n* MUST NOT place tokens/secrets in query strings or fragments (they leak via logs, referrers, browser history).\n* SHOULD avoid logging sensitive values to console in production.\n\nInsecure patterns:\n\n* `/?token=...`, `/#access_token=...` used beyond short-lived OAuth handoff.\n* `console.log(userSession)` that includes tokens/PII.\n\nDetection hints:\n\n* Search for `token=` in router parsing, auth callback handlers, and analytics logs.\n* Search for `console.log(` around auth code.\n\nFix:\n\n* Use Authorization headers or HttpOnly cookies.\n* Scrub logs; gate debug logs behind dev-only checks.\n\n---\n\n### VUE-HEADERS-001: Require security headers at the deployment layer\n\nSeverity: Medium\n\nRequired:\n\n* SHOULD deploy a CSP (`Content-Security-Policy`) suitable for your Vue app.\n* SHOULD deploy clickjacking defenses (CSP `frame-ancestors` and/or `X-Frame-Options`) unless intentional embedding is required.\n* SHOULD deploy `X-Content-Type-Options: nosniff`, plus other headers as appropriate (Referrer-Policy, Permissions-Policy). ([OWASP Cheat Sheet Series][4])\n\nInsecure patterns:\n\n* No evidence of headers in server/CDN config for an app with UGC or rich HTML rendering.\n* CSP includes `unsafe-inline`/`unsafe-eval` without strong justification.\n\nDetection hints:\n\n* Look for hosting config: nginx, Netlify/Vercel headers config, CloudFront/Cloudflare rules.\n* If absent in repo, flag as “verify at edge”.\n\nFix:\n\n* Set headers at the edge or in the server. Start with a conservative CSP and tighten.\n\n---\n\n### VUE-CSP-001: Use Trusted Types and DOM XSS hardening when feasible\n\nSeverity: Low\n\nRequired:\n\n* For apps with significant DOM injection surface (rich text, plugins, `v-html`), SHOULD consider enabling Trusted Types to reduce DOM XSS risk. ([web.dev][10])\n* SHOULD treat Trusted Types as defense-in-depth, not a replacement for sanitization.\n\nInsecure patterns:\n\n* Frequent use of `innerHTML`/`v-html` without sanitization or CSP hardening.\n\nDetection hints:\n\n* Search: `v-html`, `innerHTML`, `insertAdjacentHTML`.\n* Check CSP for `require-trusted-types-for 'script'` usage (if headers are in repo).\n\nFix:\n\n* Reduce/centralize HTML injection, sanitize inputs, and add Trusted Types policies where appropriate.\n\n---\n\n### VUE-THIRDPARTY-001: Avoid dynamic third-party script injection; prefer static, vetted loading\n\nSeverity: Low\n\nRequired:\n\n* MUST NOT inject `<script src=\"...\">` where the URL is user-controlled.\n* SHOULD treat third-party widgets/analytics as supply-chain risk; load only from vetted, pinned sources.\n\nInsecure patterns:\n\n* `const s=document.createElement('script'); s.src = userProvidedUrl; ...`\n* “Plugin marketplace” that loads arbitrary remote scripts.\n\nDetection hints:\n\n* Search: `createElement('script')`, `.src =`, `appendChild(script)`.\n* Search for “loadExternalScript”, “injectScript”, “cdnUrl”.\n\nFix:\n\n* Bundle dependencies, or allowlist strict origins and enforce integrity (see SRI rule).\n* Consider sandboxed iframes for untrusted third-party UI.\n\n---\n\n### VUE-SRI-001: Use Subresource Integrity for CDN-hosted scripts/styles\n\nSeverity: Low\n\nRequired:\n\n* If loading scripts/styles from a CDN, SHOULD use Subresource Integrity (`integrity` attribute) with appropriate `crossorigin` configuration. ([MDN Web Docs][11])\n* SHOULD prefer self-hosting or bundling over runtime CDN dependencies for security-critical code.\n\nInsecure patterns:\n\n* `<script src=\"https://cdn.example/...\">` with no `integrity`.\n* Remote script URLs that can change content without version pinning.\n\nDetection hints:\n\n* Search `index.html` and server templates for `https://` script/style tags.\n* Check for `integrity=`.\n\nFix:\n\n* Add SRI hashes (and pin versions), or bundle assets with your build.\n\n---\n\n### VUE-SUPPLY-001: Dependency and patch hygiene is mandatory\n\nSeverity: Low\n\nRequired:\n\n* SHOULD keep Vue and official companion libraries updated; Vue explicitly recommends using latest versions to remain as secure as possible. ([Vue.js][1])\n* MUST respond to security advisories promptly.\n* SHOULD pin dependencies and keep lockfiles committed (to reduce drift in production artifacts).\n\nInsecure patterns:\n\n* Outdated major versions with known CVEs.\n* No lockfile in repo; wide semver ranges for critical deps.\n* Ignoring advisories for template/rendering/compiler packages.\n\nDetection hints:\n\n* Inspect `package.json`, lockfiles, CI install commands.\n* Search for `npm audit` disabled, “ignore vulnerabilities” scripts.\n\nFix:\n\n* Upgrade dependencies and add regression tests around the impacted behavior.\n* Add dependency scanning in CI.\n\n---\n\n### VUE-SSR-001: SSR adds additional trust boundaries; treat state injection as XSS-sensitive\n\nSeverity: Medium\n\nRequired:\n\n* When using SSR, MUST treat anything injected into the HTML document (initial state, serialized data, inline scripts) as XSS-sensitive.\n* MUST keep the “trusted templates only” rule even stricter, because unsafe templates can lead to server-side execution during rendering. ([Vue.js][1])\n* SHOULD follow Vue SSR documentation and best practices for SSR security. ([Vue.js][1])\n\nInsecure patterns:\n\n* Concatenating untrusted strings into SSR templates.\n* Injecting JSON into `<script>` blocks without robust escaping/serialization controls.\n\nDetection hints:\n\n* Search server code for `__INITIAL_STATE__`, `window.__*STATE__`, template concatenation, and SSR render pipelines.\n* Trace untrusted data into those sinks.\n\nFix:\n\n* Use safe serialization patterns recommended by your SSR stack.\n* Avoid rendering untrusted HTML; sanitize or isolate.\n\n---\n\n## 5) Practical scanning heuristics (how to “hunt”)\n\nWhen actively scanning, use these high-signal patterns:\n\n* Dev/preview servers in production:\n\n  * `npm run dev`, `vite`, `vite preview`, `vue-cli-service serve` ([vitejs][5])\n* Secrets exposure:\n\n  * `.env`, `.env.production`, `.env.*.local`, `VITE_`, `import.meta.env`, hard-coded `API_KEY` / `SECRET` ([vitejs][2])\n* XSS sinks:\n\n  * `v-html`, `innerHTML`, `insertAdjacentHTML`, `DOMParser`, `document.write` ([Vue.js][1])\n* Client-side template injection:\n\n  * `template:` concatenation, `compile(`, runtime compiler usage, mounting on non-sterile DOM ([Vue.js][1])\n* URL injection / open redirects:\n\n  * `:href=\"...\"` / `:src=\"...\"` from user data\n  * `javascript:` occurrences\n  * `route.query.next` / `redirect` / `return_to` flowing into `router.push` or `window.location` ([Vue.js][1])\n* Style injection:\n\n  * `:style=\"userProvidedStyles\"` or user-driven theme CSS ([Vue.js][1])\n* Token storage:\n\n  * `localStorage.setItem('token'...)`, persisted auth stores, refresh tokens in JS-accessible storage\n* CSRF integration red flags:\n\n  * `credentials: 'include'` / `withCredentials: true` without any CSRF header/token handling ([Vue.js][1])\n* Third-party scripts:\n\n  * dynamic script injection (`createElement('script')`), CDN scripts without SRI ([MDN Web Docs][11])\n* External links security:\n\n  * `target=\"_blank\"` without `rel=\"noopener\"`/`noreferrer` (still recommended for legacy and explicitness) ([MDN Web Docs][12])\n\nAlways try to confirm:\n\n* data origin (untrusted vs trusted)\n* sink type (HTML/DOM insertion, template compilation, URL navigation, style injection, script injection)\n* protective controls present (sanitization, allowlists, CSP/Trusted Types, backend validation)\n\n---\n\n## 6) Sources (accessed 2026-01-27)\n\nPrimary Vue documentation:\n\n* Vue Docs: Security — `https://vuejs.org/guide/best-practices/security` ([Vue.js][1])\n* Vue Docs: Template Syntax (security warning about in-DOM templates) — `https://vuejs.org/guide/essentials/template-syntax` ([Vue.js][13])\n* Vue Docs: Production Deployment — `https://vuejs.org/guide/best-practices/production-deployment` ([Vue.js][3])\n* Vue Docs: Feature Flags — `https://link.vuejs.org/feature-flags` ([Vue.js][7])\n\nVite documentation (common Vue tooling):\n\n* Vite Docs: Env Variables and Modes (VITE_* exposure + security notes) — `https://vite.dev/guide/env-and-mode` ([vitejs][2])\n* Vite Docs: CLI (`vite preview` not designed for production) — `https://vite.dev/guide/cli` ([vitejs][5])\n* Vite Docs: Server Options (`server.host` can listen on public addresses) — `https://vite.dev/config/server-options` ([vitejs][14])\n\nOWASP and web platform hardening references:\n\n* OWASP Cheat Sheet Series: XSS Prevention — `https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html` ([Vue.js][1])\n* OWASP Cheat Sheet Series: CSRF Prevention — `https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][9])\n* OWASP Cheat Sheet Series: Authorization — `https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][8])\n* OWASP Cheat Sheet Series: HTTP Headers — `https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][4])\n* HTML5 Security Cheat Sheet (referenced by Vue) — `https://html5sec.org/` ([Vue.js][1])\n\nBrowser/platform references:\n\n* MDN: `rel=\"noopener\"` — `https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/noopener` ([MDN Web Docs][12])\n* MDN: Subresource Integrity — `https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity` ([MDN Web Docs][11])\n* web.dev: Trusted Types — `https://web.dev/trusted-types/` ([web.dev][10])\n\n[1]: https://vuejs.org/guide/best-practices/security \"https://vuejs.org/guide/best-practices/security\"\n[2]: https://vite.dev/guide/env-and-mode \"https://vite.dev/guide/env-and-mode\"\n[3]: https://vuejs.org/guide/best-practices/production-deployment \"https://vuejs.org/guide/best-practices/production-deployment\"\n[4]: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html\"\n[5]: https://vite.dev/guide/cli \"https://vite.dev/guide/cli\"\n[6]: https://vite.dev/guide/build \"https://vite.dev/guide/build\"\n[7]: https://vuejs.org/guide/best-practices/production-deployment?utm_source=chatgpt.com \"Production Deployment\"\n[8]: https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html\"\n[9]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html\"\n[10]: https://web.dev/articles/trusted-types \"https://web.dev/articles/trusted-types\"\n[11]: https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity?utm_source=chatgpt.com \"Subresource Integrity - Security - MDN Web Docs\"\n[12]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/noopener \"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/noopener\"\n[13]: https://vuejs.org/guide/essentials/template-syntax \"Template Syntax | Vue.js\"\n[14]: https://vite.dev/config/server-options \"https://vite.dev/config/server-options\"\n"
  },
  {
    "path": "skills/.curated/security-best-practices/references/python-django-web-server-security.md",
    "content": "# Django (Python) Web Security Spec (Django 6.0.x, Python 3.x)\n\nThis document is designed as a **security spec** that supports:\n\n1. **Secure-by-default code generation** for new Django code.\n2. **Security review / vulnerability hunting** in existing Django code (passive “notice issues while working” and active “scan the repo and report findings”).\n\nIt is intentionally written as a set of **normative requirements** (“MUST/SHOULD/MAY”) plus **audit rules** (what bad patterns look like, how to detect them, and how to fix/mitigate them).\n\n---\n\n## 0) Safety, boundaries, and anti-abuse constraints (MUST FOLLOW)\n\n* MUST NOT request, output, log, or commit secrets (API keys, passwords, private keys, session cookies, `SECRET_KEY`, `SECRET_KEY_FALLBACKS`, database passwords).\n* MUST NOT “fix” security by disabling protections (e.g., removing `CsrfViewMiddleware`, sprinkling `@csrf_exempt`, loosening `ALLOWED_HOSTS` to `['*']`, disabling `SecurityMiddleware`, disabling template auto-escaping, disabling permission checks).\n* MUST provide **evidence-based findings** during audits: cite file paths, code snippets, and concrete configuration values that justify the claim.\n* MUST treat uncertainty honestly: if a protection might exist in infrastructure (reverse proxy, WAF, CDN, ingress controller), report it as “not visible in app code; verify at runtime / edge config”.\n* MUST keep fixes compatible with Django’s intended security model: prefer Django’s built-ins (middleware, auth, forms, ORM) over custom security logic whenever possible. Django’s deployment checklist and system checks are part of the intended model. ([Django Project][1])\n\n---\n\n## 1) Operating modes\n\n### 1.1 Generation mode (default)\n\nWhen asked to write new Django code or modify existing code:\n\n* MUST follow every **MUST** requirement in this spec.\n* SHOULD follow every **SHOULD** requirement unless the user explicitly says otherwise.\n* MUST prefer safe-by-default Django APIs and proven libraries over custom security code.\n* MUST avoid introducing new risky sinks (dynamic template rendering from untrusted strings, unsafe redirects, unsafe file serving, shell execution, raw SQL string formatting, SSRF-capable URL fetchers from untrusted input).\n\n### 1.2 Passive review mode (always on while editing)\n\nWhile working anywhere in a Django repo (even if the user did not ask for a security scan):\n\n* MUST “notice” violations of this spec in touched/nearby code.\n* SHOULD mention issues as they come up, with a brief explanation + safe fix.\n\n### 1.3 Active audit mode (explicit scan request)\n\nWhen the user asks to “scan”, “audit”, or “hunt for vulns”:\n\n* MUST systematically search the codebase for violations of this spec.\n* MUST output findings in a structured format (see §2.3).\n\nRecommended audit order:\n\n1. Deployment entrypoints (ASGI/WSGI), Dockerfiles, Procfiles, systemd units, platform manifests.\n2. `settings.py` and environment-specific settings modules.\n3. Middleware ordering and enabled protections.\n4. Authn/authz (login, session management, permissions, admin).\n5. CSRF protections and state-changing endpoints.\n6. Templates and XSS.\n7. File handling (uploads/downloads/static/media) and path traversal.\n8. Injection classes (SQL, command execution, unsafe deserialization).\n9. Outbound requests (SSRF).\n10. Redirect handling (open redirects) + CORS + security headers (CSP, HSTS, etc.).\n11. Dependency/pinning and patch posture.\n\n---\n\n## 2) Definitions and review guidance\n\n### 2.1 Untrusted input (treat as attacker-controlled unless proven otherwise)\n\nExamples include:\n\n* `request.GET`, `request.POST`, `request.FILES`\n* `request.body`, JSON bodies (e.g., `json.loads(request.body)`), DRF `request.data`\n* URL path parameters (e.g., `<int:id>`, `<slug:...>`)\n* `request.headers` / `request.META` (including `HTTP_HOST`, `HTTP_ORIGIN`, `HTTP_REFERER`, `HTTP_X_FORWARDED_*`)\n* `request.COOKIES`\n* Any data from external systems (webhooks, third-party APIs, message queues)\n* Any persisted content that originated from users (DB rows, cached content, file uploads)\n\nDjango explicitly emphasizes “never trust user-controlled data” and recommends using forms/validation. ([Django Project][2])\n\n### 2.2 State-changing request\n\nA request is state-changing if it can create/update/delete data, change auth/session state, trigger side effects (purchase, email send, webhook send), or initiate privileged actions.\n\n### 2.3 Required audit finding format\n\nFor each issue found, output:\n\n* Rule ID:\n* Severity: Critical / High / Medium / Low\n* Location: file path + function/class/view name + line(s)\n* Evidence: the exact code/config snippet\n* Impact: what could go wrong, who can exploit it\n* Fix: safe change (prefer minimal diff)\n* Mitigation: defense-in-depth if immediate fix is hard\n* False positive notes: what to verify if uncertain\n\n---\n\n## 3) Secure baseline: minimum production configuration (MUST in production)\n\nThis is the smallest “production baseline” that prevents common Django misconfigurations. Django provides a “Deployment checklist” and recommends running `manage.py check --deploy` against production settings. ([Django Project][1])\n\n### 3.1 Settings management pattern (SHOULD)\n\n* SHOULD use environment-based configuration (or a secret manager) so production settings are not hard-coded.\n* MUST treat sensitive settings as confidential (e.g., `SECRET_KEY`, DB passwords) and keep them out of source control. Django’s checklist explicitly recommends loading `SECRET_KEY` from env or a file rather than hardcoding. ([Django Project][1])\n* SHOULD separate dev vs prod settings modules, with safe defaults for production (fail closed if critical settings are missing). ([Django Project][1])\n\n### 3.2 Minimum baseline targets (production)\n\n* MUST NOT use `manage.py runserver` as the production entrypoint; use a production-ready WSGI or ASGI server. ([Django Project][1])\n* MUST set `DEBUG = False` in production. ([Django Project][1])\n* MUST set a strong, secret `SECRET_KEY` and keep it secret; MAY use `SECRET_KEY_FALLBACKS` for safe rotation. ([Django Project][1])\n* MUST set `ALLOWED_HOSTS` to expected hosts (no wildcard unless you do your own host validation). ([Django Project][1])\n* MUST enforce HTTPS for authenticated areas (ideally site-wide for any login-capable app) and set `CSRF_COOKIE_SECURE=True` and `SESSION_COOKIE_SECURE=True` when HTTPS is used. ([Django Project][1])\n* SHOULD enable key `SecurityMiddleware` headers/settings: HSTS, Referrer-Policy, COOP, nosniff, SSL redirect (with correct proxy configuration). ([Django Project][3])\n* MUST treat user uploads as untrusted; ensure your web server never interprets them as executable content; keep `MEDIA_ROOT` separate from `STATIC_ROOT`. ([Django Project][1])\n\n---\n\n## 4) Rules (generation + audit)\n\nEach rule contains: required practice, insecure patterns, detection hints, and remediation.\n\n### DJANGO-DEPLOY-001: Do not use Django’s development server in production\n\nSeverity: High (if production)\n\nRequired:\n\n* MUST NOT deploy `manage.py runserver` as the production server.\n* MUST run behind a production-grade WSGI or ASGI server. ([Django Project][1])\n\nInsecure patterns:\n\n* Production docs/scripts using `python manage.py runserver 0.0.0.0:8000`.\n* Docker `CMD`/entrypoint uses `runserver`.\n* Kubernetes/Procfile/systemd units invoking `runserver`.\n\nDetection hints:\n\n* Search for `manage.py runserver`, `runserver 0.0.0.0`, `--insecure`.\n* Check Docker `CMD/ENTRYPOINT`, Procfile, systemd unit files, Helm charts.\n\nFix:\n\n* Use a production server (WSGI/ASGI) as recommended in Django’s deployment checklist. ([Django Project][1])\n\nNote:\n\n* `runserver` is fine for local development. Only flag if it’s used as the production entrypoint.\n\n---\n\n### DJANGO-DEPLOY-002: `DEBUG` MUST be disabled in production\n\nSeverity: High\n\nRequired:\n\n* MUST set `DEBUG = False` in production.\n* MUST treat any mechanism that exposes debug pages/tracebacks to untrusted users as a critical information disclosure risk. Django’s checklist explicitly warns `DEBUG=True` leaks source excerpts, local variables, settings, and more. ([Django Project][1])\n\nInsecure patterns:\n\n* `DEBUG = True` in production settings.\n* Environment defaults to `DEBUG=True` unless explicitly overridden.\n\nDetection hints:\n\n* Search `DEBUG = True`, `DEBUG=os.environ.get(..., True)`, `DJANGO_DEBUG`, `.env` files.\n* Look for “production” settings modules that import from dev defaults.\n\nFix:\n\n* Set `DEBUG=False` in prod settings; use explicit environment config.\n* Ensure error reporting is via safe logging/monitoring, not debug pages. ([Django Project][1])\n\n---\n\n### DJANGO-CONFIG-001: `SECRET_KEY` must be strong, secret, and rotated safely\n\nSeverity: High (Critical if missing in production with signing/sessions)\n\nRequired:\n\n* MUST set a large random `SECRET_KEY` in production and keep it secret. ([Django Project][1])\n* MUST NOT commit it to source control or print/log it. ([Django Project][1])\n* SHOULD load it from env or a file/secret store (not hard-coded). ([Django Project][1])\n* MAY rotate keys using `SECRET_KEY_FALLBACKS` to avoid instantly invalidating all signed data; MUST remove old keys from fallbacks in a timely manner. ([Django Project][1])\n\nInsecure patterns:\n\n* Hard-coded `SECRET_KEY = \"...\"` in repo for production.\n* `SECRET_KEY` reused across environments.\n* `SECRET_KEY_FALLBACKS` contains long-expired keys indefinitely.\n\nDetection hints:\n\n* Search for `SECRET_KEY =`, `SECRET_KEY_FALLBACKS`, `.env` committed files, `print(settings.SECRET_KEY)`.\n\nFix:\n\n* Load from secret manager / environment variable.\n* If rotating:\n\n  * Set new `SECRET_KEY`\n  * Keep old key(s) temporarily in `SECRET_KEY_FALLBACKS`\n  * Remove old key(s) after the rotation window. ([Django Project][1])\n\n---\n\n### DJANGO-HOST-001: Host header must be validated (`ALLOWED_HOSTS` must be strict)\n\nSeverity: Medium\n\nRequired:\n\n* MUST set `ALLOWED_HOSTS` in production to your expected domains/hosts. ([Django Project][1])\n* MUST NOT set `ALLOWED_HOSTS = ['*']` in production unless you also implement your own robust `Host` validation (Django warns that wildcards require your own validation to avoid CSRF-class attacks). ([Django Project][1])\n* SHOULD configure the fronting web server to reject unknown hosts early (defense-in-depth). ([Django Project][1])\n\nInsecure patterns:\n\n* `ALLOWED_HOSTS = ['*']` (or env expands to `*`) in production.\n* `ALLOWED_HOSTS = []` with `DEBUG=False` (site won’t run, or misconfigured deployments attempt workarounds).\n\nDetection hints:\n\n* Search `ALLOWED_HOSTS`.\n* Check platform environment settings that override `ALLOWED_HOSTS`.\n\nFix:\n\n* Set `ALLOWED_HOSTS = ['example.com', 'www.example.com', ...]` for prod.\n* Keep dev hosts separate.\n\nNotes:\n\n* Django uses the Host header for URL construction; fake Host values can lead to CSRF, cache poisoning, and poisoned email links (Django security docs call this out). ([Django Project][2])\n\n---\n\n### DJANGO-HTTPS-001: If TLS is used cookie transport must be secured\n\nSeverity: High (Critical for auth-enabled apps)\n\nNOTE: Only enforce this if TLS is enabled, as it will break non-TLS applications\n\nIf using TLS:\n* MUST set:\n\n  * `CSRF_COOKIE_SECURE = True` ([Django Project][1])\n  * `SESSION_COOKIE_SECURE = True` ([Django Project][1])\n* SHOULD consider enabling:\n\n  * `SECURE_SSL_REDIRECT = True` (with correct proxy config) ([Django Project][3])\n  * HSTS via `SECURE_HSTS_SECONDS` (+ includeSubDomains/preload as appropriate). ([Django Project][3])\n\nInsecure patterns:\n\n* Login pages over HTTP, or mixed HTTP/HTTPS with the same session cookie.\n* `CSRF_COOKIE_SECURE=False` or `SESSION_COOKIE_SECURE=False` in production HTTPS.\n* HSTS enabled incorrectly (can break site for the duration).\n\nDetection hints:\n\n* Inspect `settings.py` for `CSRF_COOKIE_SECURE`, `SESSION_COOKIE_SECURE`, `SECURE_SSL_REDIRECT`, `SECURE_HSTS_SECONDS`.\n* Inspect proxy/ingress config for HTTP->HTTPS redirect behavior.\n\nFix:\n\n* Enable HTTPS redirect and secure cookies.\n* Add HSTS carefully (start with low value, validate, then increase). Django warns misconfig can break your site for the HSTS duration. ([Django Project][3])\n\n---\n\n### DJANGO-PROXY-001: Reverse proxy trust must be configured correctly (`SECURE_PROXY_SSL_HEADER`)\n\nSeverity: Medium (when behind a TLS proxy)\n\nRequired:\n\n* If behind a reverse proxy that terminates TLS, MUST configure Django so `request.is_secure()` reflects the *external* scheme, otherwise CSRF and other logic can break. Django documents using `SECURE_PROXY_SSL_HEADER` for this. ([Django Project][3])\n* MUST only set `SECURE_PROXY_SSL_HEADER` if you control the proxy (or have guarantees) and it strips inbound spoofed headers. Django explicitly warns misconfig can compromise security and lists required conditions. ([Django Project][3])\n\nInsecure patterns:\n\n* `SECURE_PROXY_SSL_HEADER = (\"HTTP_X_FORWARDED_PROTO\", \"https\")` in an environment where the proxy does not strip user-supplied `X-Forwarded-Proto`.\n* Infinite redirect loops after setting `SECURE_SSL_REDIRECT=True` (often indicates proxy HTTPS detection is wrong). ([Django Project][3])\n\nDetection hints:\n\n* Search `SECURE_PROXY_SSL_HEADER`, `SECURE_SSL_REDIRECT`.\n* Inspect ingress/proxy behavior for stripping forwarded headers.\n\nFix:\n\n* Set `SECURE_PROXY_SSL_HEADER` only if the proxy strips and sets the header correctly (per Django’s documented prerequisites). ([Django Project][3])\n\n---\n\n### DJANGO-SESS-001: Session cookies must use secure attributes in production\n\nSeverity: Medium (Only if TLS enabled)\n\nRequired (production, HTTPS):\n\n* MUST set `SESSION_COOKIE_SECURE=True` (only transmit over HTTPS). ([Django Project][3])\n* MUST keep `SESSION_COOKIE_HTTPONLY=True` (Django default is `True`). ([Django Project][3])\n* SHOULD keep `SESSION_COOKIE_SAMESITE='Lax'` (Django default is `Lax`) unless a justified cross-site flow requires `None`. ([Django Project][3])\n* SHOULD avoid setting `SESSION_COOKIE_DOMAIN` unless you truly need cross-subdomain cookies (subdomain-wide cookies expand attack surface).\n\nInsecure patterns:\n\n* `SESSION_COOKIE_SECURE=False` in production HTTPS.\n\nIMPORTANT NOTE: Only set `Secure` in production environment when TLS is configured. When running in a local dev environment over HTTP, do not set `Secure` property on cookies. You should do this conditionally based on if the app is running in production mode. You should also include a property like `SESSION_COOKIE_SECURE` which can be used to disable `Secure` cookies when testing over HTTP.\n\n* `SESSION_COOKIE_HTTPONLY=False`.\n* `SESSION_COOKIE_SAMESITE=None` combined with cookie-authenticated state-changing endpoints (higher CSRF risk).\n\nDetection hints:\n\n* Search for `SESSION_COOKIE_` settings, `response.set_cookie(..., httponly=..., secure=..., samesite=...)`.\n\nFix:\n\n* Set the above explicitly in production settings.\n* Validate compatibility with your auth flows. ([Django Project][3])\n\n---\n\n### DJANGO-SESS-002: CSRF cookie settings must be deliberate (HttpOnly has tradeoffs)\n\nSeverity: Medium\n\nRequired:\n\n* SHOULD set `CSRF_COOKIE_SECURE=True` when using HTTPS/TLS. ([Django Project][3])\n* SHOULD keep `CSRF_COOKIE_SAMESITE='Lax'` unless you have a cross-site requirement. Django default is `Lax`. ([Django Project][3])\n* MAY set `CSRF_COOKIE_HTTPONLY=True` (default is `False`) if your frontend does not need to read the CSRF cookie. If you enable it, your JS must read the CSRF token from the DOM instead (Django documents this). ([Django Project][3])\n\nInsecure patterns:\n\n* `CSRF_COOKIE_SECURE=False` in production HTTPS/TLS.\n* Setting `CSRF_COOKIE_HTTPONLY=True` but still relying on “read csrftoken cookie in JS” patterns (breaks CSRF for AJAX).\n* `CSRF_COOKIE_SAMESITE=None` without a clear reason.\n\nDetection hints:\n\n* Search for `CSRF_COOKIE_` settings.\n* Search JS for `document.cookie` usage to fetch `csrftoken`.\n\nFix:\n\n* Align cookie settings with your CSRF token acquisition method (cookie vs DOM) as Django describes. ([Django Project][4])\n\n---\n\n### DJANGO-CSRF-001: Cookie-authenticated state-changing requests MUST be CSRF-protected\n\nSeverity: High\n\nRequired:\n\n* MUST keep `django.middleware.csrf.CsrfViewMiddleware` enabled (it is activated by default). ([Django Project][4])\n* MUST include `{% csrf_token %}` in internal POST forms; MUST NOT include it in forms that POST to external URLs (Django warns this leaks the token). ([Django Project][4])\n* MUST protect all state-changing endpoints (POST/PUT/PATCH/DELETE) that rely on cookies for authentication.\n* For AJAX/SPA calls, MUST send the CSRF token via the `X-CSRFToken` header (or configured header name) as documented. ([Django Project][4])\n* MUST be very careful with `@csrf_exempt` and use it only when absolutely necessary; if used, MUST replace CSRF with an appropriate alternative control (e.g., request signing for webhooks). Django explicitly warns about `csrf_exempt`. ([Django Project][2])\n\nInsecure patterns:\n\n* Missing `CsrfViewMiddleware` in `MIDDLEWARE`.\n* `@csrf_exempt` on general-purpose authenticated views.\n* POST/PUT/PATCH/DELETE endpoints with session auth and no CSRF tokens.\n* Using GET for state-changing actions (amplifies CSRF risk).\n\nDetection hints:\n\n* Inspect `settings.py` `MIDDLEWARE` for `CsrfViewMiddleware` and its order (Django notes it should come before middleware that assumes CSRF is handled). ([Django Project][4])\n* Search for `csrf_exempt`, `csrf_protect`, `ensure_csrf_cookie`.\n* Enumerate URL patterns for non-GET methods; confirm CSRF coverage.\n\nFix:\n\n* Re-enable `CsrfViewMiddleware`, add CSRF tokens to forms, and add AJAX header handling.\n* For caching decorators: if you cache a view that needs CSRF tokens, apply `@csrf_protect` as Django documents to avoid caching a response without CSRF cookie/Vary headers. ([Django Project][4])\n\nNotes:\n\n* When deployed with HTTPS, Django’s CSRF middleware also checks the Referer header for same-origin (Django security docs mention this). ([Django Project][2])\n\n---\n\n### DJANGO-XSS-001: Prevent reflected/stored XSS in templates and HTML generation\n\nSeverity: High\n\nRequired:\n\n* MUST rely on Django template auto-escaping (safe-by-default) for HTML templates. Django security docs highlight that Django templates escape dangerous characters but have limitations. ([Django Project][2])\n* MUST NOT disable auto-escaping broadly (`{% autoescape off %}`) unless the content is trusted or safely sanitized. ([Django Project][5])\n* MUST NOT mark untrusted content as safe:\n\n  * Avoid `mark_safe(...)` on user data.\n  * Avoid `|safe` on user-controlled content.\n* MUST be careful about HTML context pitfalls (e.g., unquoted attributes); Django explicitly shows an example where escaping does not protect an unquoted attribute context. ([Django Project][2])\n* SHOULD prefer safe HTML construction helpers (e.g., `format_html`) rather than manual concatenation that risks missing escapes. ([Django Project][6])\n\nInsecure patterns:\n\n* `{% autoescape off %}{{ user_input }}{% endautoescape %}`\n* `{{ user_input|safe }}`\n* `mark_safe(request.GET[\"q\"])`\n* Unquoted attribute injections: `<style class={{ var }}>...` (Django’s own example). ([Django Project][2])\n\nDetection hints:\n\n* Search templates for `|safe`, `autoescape off`, `safeseq`.\n* Search Python for `mark_safe`, `SafeString`, or direct HTML concatenation with request/DB values.\n* Review any code returning `HttpResponse(user_value)` where `user_value` contains HTML.\n\nFix:\n\n* Remove unsafe marking; sanitize only when strictly necessary (use an allowlist-based HTML sanitizer).\n* Quote attributes and avoid placing untrusted values into dangerous contexts.\n* Add CSP as defense-in-depth (see DJANGO-CSP-001). ([Django Project][2])\n\n---\n\n### DJANGO-TEMPLATE-001: Never render untrusted template source strings\n\nSeverity: High to Critical (depends on context and exposure)\n\nRequired:\n\n* MUST NOT render templates where the template source string is influenced by untrusted input (request, user content, DB rows editable by untrusted users).\n* MUST treat “template from string” patterns as dangerous, even if Django templates are more constrained than some other engines: they can still leak data from context, bypass escaping, and create XSS or content injection.\n\nInsecure patterns:\n\n* `Template(request.GET[\"tmpl\"]).render(Context(...))`\n* Saving user templates in the DB and rendering them with normal privileges/context.\n\nDetection hints:\n\n* Search for `django.template.Template(`, `Engine.from_string`, `.render(Context(` with non-constant strings.\n* Trace where the template string comes from (admin panels, DB, uploads, requests).\n\nFix:\n\n* Replace with non-executing formatting (e.g., `string.Template`, explicit placeholders) or a strict allowlisted rendering model.\n* If you *must* support user-defined templates, isolate heavily (separate service/tenant context, strict allowlists, and assume bypasses are possible).\n\n---\n\n### DJANGO-SQL-001: Prevent SQL injection (use ORM or parameterized raw SQL)\n\nSeverity: High\n\nRequired:\n\n* MUST use Django ORM/querysets for normal DB access; Django notes querysets are parameterized and protected from SQL injection under typical use. ([Django Project][2])\n* MUST be very careful with raw SQL; if using `raw()`, `cursor.execute()`, `extra()`, or `RawSQL`, MUST pass parameters separately (e.g., `params=`) and MUST NOT string-interpolate untrusted input into SQL. Django’s raw SQL docs warn to escape user-controlled parameters using `params`. ([Django Project][7])\n* MUST NOT quote placeholders in SQL templates (Django docs explicitly warn that quoting `%s` placeholders makes it unsafe). ([Django Project][8])\n* SHOULD avoid `extra()` and `RawSQL` unless necessary; Django security docs call for caution. ([Django Project][2])\n\nInsecure patterns:\n\n* `cursor.execute(f\"SELECT ... WHERE id={request.GET['id']}\")`\n* `Model.objects.raw(\"... %s\" % user_input)` (string formatting)\n* `extra(where=[f\"headline='{q}'\"])`\n* Quoted placeholders: `WHERE othercol = '%s'` (explicitly documented as unsafe). ([Django Project][8])\n\nDetection hints:\n\n* Grep for `.raw(`, `.extra(`, `RawSQL(`, `connection.cursor()`, `.execute(`.\n* Grep for SQL keywords (`SELECT`, `UPDATE`, `DELETE`, `INSERT`) in Python strings.\n* Track untrusted inputs into these call sites.\n\nFix:\n\n* Prefer ORM queries.\n* If raw SQL is unavoidable, use parameters (`params`, DB-API param binding) and do not quote placeholders. ([Django Project][7])\n\n---\n\n### DJANGO-CMD-001: Prevent OS command injection\n\nSeverity: Critical to High (depends on exposure)\n\nRequired:\n\n* MUST avoid executing system commands with attacker-influenced input.\n* If subprocess is necessary:\n\n  * MUST pass args as a list (not a shell string).\n  * MUST NOT use `shell=True` with attacker-influenced content.\n  * SHOULD use strict allowlists for variable components.\n* SHOULD prefer pure-Python libraries instead of shelling out.\n\nInsecure patterns:\n\n* `os.system(request.GET[\"cmd\"])`\n* `subprocess.run(f\"convert {path}\", shell=True)` where `path` is user-controlled.\n\nDetection hints:\n\n* Search `os.system`, `subprocess`, `Popen`, `shell=True`.\n* Trace request/DB inputs into those calls.\n\nFix:\n\n* Replace with library APIs; if unavoidable, hard-code executable and allowlist validated parameters.\n\n---\n\n### DJANGO-UPLOAD-001: File uploads must be validated, stored safely, and served safely\n\nSeverity: High\n\nRequired:\n\n* MUST treat all user uploads as untrusted. Django explicitly warns “Media files are uploaded by your users. They’re untrusted!” ([Django Project][1])\n* MUST ensure the web server never interprets user uploads as executable code (e.g., don’t allow uploaded `.php` or HTML to execute/inline as active content). ([Django Project][1])\n* MUST enforce size limits (at least at the web server; Django security docs recommend limiting upload size at the server to prevent DoS). ([Django Project][2])\n* SHOULD validate file types using allowlists and content checks (not only extensions).\n* SHOULD store uploads outside the application code directory and outside any static root.\n* SHOULD consider serving uploads from a separate top-level/second-level domain to reduce same-origin impact; Django security docs recommend a distinct domain and note that a subdomain may be insufficient for some protections. ([Django Project][2])\n* MUST be aware of polyglot upload risks: Django documents a case where HTML can be uploaded “as an image” by using a valid PNG header (and may be served as HTML depending on the web server). ([Django Project][2])\n\nInsecure patterns:\n\n* Serving uploads inline with `text/html` or without forcing download for potentially active formats.\n* Upload allowlist based only on extension.\n* Upload storage inside static roots or code roots.\n\nDetection hints:\n\n* Search for `request.FILES`, `FileField`, `ImageField`, upload forms/views.\n* Inspect upload serving paths and Nginx/Apache config (media handlers).\n* Check `MEDIA_URL`, `MEDIA_ROOT`, and static config.\n\nFix:\n\n* Configure the web server to serve uploads as inert bytes (no execution), and consider forcing `Content-Disposition: attachment` for risky types.\n* Use a separate domain for user content when warranted. ([Django Project][2])\n\n---\n\n### DJANGO-PATH-001: Prevent path traversal and unsafe file serving (static/media separation)\n\nSeverity: High\n\nRequired:\n\n* MUST NOT treat user input as a filesystem path for reads/writes/serving.\n* MUST keep `MEDIA_ROOT` and `STATIC_ROOT` distinct; Django settings docs explicitly warn they must have different values to avoid security implications. ([Django Project][3])\n* SHOULD prefer using Django storage APIs keyed by server-side identifiers rather than accepting arbitrary relative paths from users.\n\nInsecure patterns:\n\n* `open(os.path.join(MEDIA_ROOT, request.GET[\"path\"]))`\n* Download endpoints that take `?file=../../...` style parameters.\n* Misconfigured `MEDIA_ROOT == STATIC_ROOT`.\n\nDetection hints:\n\n* Grep for `open(`, `Path(`, `os.path.join(` used with request values.\n* Check `MEDIA_ROOT`, `STATIC_ROOT` in settings. ([Django Project][3])\n\nFix:\n\n* Use server-side IDs mapped to known files.\n* Keep static and media separated and ensure the web server treats media as untrusted. ([Django Project][3])\n\n---\n\n### DJANGO-REDIRECT-001: Prevent open redirects (`next`, `return_to`, `redirect`)\n\nSeverity: Medium (High when combined with auth flows)\n\nRequired:\n\n* MUST validate redirect targets derived from untrusted input (e.g., `next`, `return_to`).\n* SHOULD restrict to same-site relative paths or allowlisted hosts/schemes.\n* SHOULD use Django’s safe URL helpers (e.g., `django.utils.http.url_has_allowed_host_and_scheme`) rather than custom parsing.\n\nInsecure patterns:\n\n* `return redirect(request.GET.get(\"next\"))` with no validation.\n* Redirect allowlist implemented with naive string checks.\n\nDetection hints:\n\n* Search for `redirect(` and track origin of the target.\n* Search for parameters named `next`, `return_to`, `redirect`, `url`.\n\nFix:\n\n* Validate with allowlists and default to a safe internal path if validation fails.\n* Ensure host validation via `ALLOWED_HOSTS` remains strict (see DJANGO-HOST-001). ([Django Project][3])\n\n---\n\n### DJANGO-HEADERS-001: Enable essential security headers (SecurityMiddleware + clickjacking protection)\n\nSeverity: Medium to High\n\nRequired:\n\n* SHOULD use `django.middleware.security.SecurityMiddleware` and configure it appropriately (production) for:\n\n  * `X-Content-Type-Options: nosniff` (Django setting `SECURE_CONTENT_TYPE_NOSNIFF`, default `True`). ([Django Project][3])\n  * `Referrer-Policy` (Django setting `SECURE_REFERRER_POLICY`, default `'same-origin'`). ([Django Project][3])\n  * COOP (Django setting `SECURE_CROSS_ORIGIN_OPENER_POLICY`, default `'same-origin'`). ([Django Project][3])\n  * HTTPS redirects and HSTS as appropriate (see DJANGO-HTTPS-001). ([Django Project][3])\n* SHOULD enable clickjacking protection via X-Frame-Options middleware; Django security docs strongly recommend it for sites that don’t need third-party framing. ([Django Project][2])\n\nInsecure patterns:\n\n* Missing SecurityMiddleware.\n* Missing clickjacking protection (or disabling it globally) without a clear framing requirement.\n* Over-broad framing allowances for sensitive endpoints.\n\nDetection hints:\n\n* Inspect `MIDDLEWARE` for SecurityMiddleware and XFrameOptionsMiddleware.\n* Search for per-view disabling of framing/CSRF protections.\n\nFix:\n\n* Add/enable middleware and configure the settings intentionally. ([Django Project][3])\n\nNOTE:\n\n* Some headers may be set at the edge (CDN/reverse proxy). If not visible in app code, flag as “verify at edge”.\n\n---\n\n### DJANGO-CSP-001: Deploy a Content Security Policy (CSP) as defense-in-depth\n\nSeverity: Medium (High for apps rendering untrusted content) \n\nNOTE: It is most important to set the CSP's script-src. All other directives are not as important and can generally be excluded for the ease of development.\n\nRequired:\n\n* SHOULD deploy a CSP to mitigate XSS and content injection classes; Django’s security docs recommend CSP and note it is new in Django 6.0. ([Django Project][2])\n* MUST understand CSP limitations:\n\n  * Avoid excluding routes from CSP coverage; Django warns that an unprotected page can undermine protected pages due to same-origin policy. ([Django Project][2])\n* MAY start with `SECURE_CSP_REPORT_ONLY` to iterate safely (Django provides report-only support). ([Django Project][3])\n\nInsecure patterns:\n\n* No CSP on apps that render user-controlled content.\n* CSP excludes “just a couple pages” (weakens overall protection), especially pages with any injection surface. ([Django Project][2])\n* CSP uses overly permissive directives (e.g., widespread `unsafe-inline`) without justification.\n\nDetection hints:\n\n* Search `SECURE_CSP`, `SECURE_CSP_REPORT_ONLY`, and CSP middleware configuration.\n* Inspect reverse proxy/CDN config for CSP headers.\n\nFix:\n\n* Implement a realistic CSP, ideally report-only first, then enforce. ([Django Project][3])\n\n---\n\n### DJANGO-AUTH-001: Password storage must use Django’s secure hashers; password policy must be configured\n\nSeverity: High\n\nRequired:\n\n* MUST use Django’s built-in password hashing (never store plaintext or reversible encrypted passwords).\n* SHOULD prefer modern hashers and keep defaults updated; Django documents `PASSWORD_HASHERS` and includes modern options (Argon2, bcrypt, scrypt, PBKDF2 variants). ([Django Project][3])\n* SHOULD configure `AUTH_PASSWORD_VALIDATORS` (default is empty) for production password policy. ([Django Project][3])\n\nInsecure patterns:\n\n* Custom password storage or hashing.\n* Plaintext passwords stored in DB fields.\n* No password validation on consumer-facing apps.\n\nDetection hints:\n\n* Search for `.set_password(` usage vs manual hashing.\n* Inspect settings for `PASSWORD_HASHERS` and `AUTH_PASSWORD_VALIDATORS`. ([Django Project][3])\n\nFix:\n\n* Use Django auth user model APIs.\n* Enable password validators appropriate to the product’s risk profile. ([Django Project][3])\n\n---\n\n### DJANGO-AUTHZ-001: Authorization must be explicit and consistent\n\nSeverity: High\n\nRequired:\n\n* MUST enforce authorization checks on every privileged action (view, modify, admin-like operations).\n* MUST NOT rely on UI-only restrictions (e.g., hiding buttons) without server-side permission checks.\n* SHOULD use Django’s permissions/groups and per-object authorization patterns where applicable.\n\nInsecure patterns:\n\n* Views that assume “user is logged in” implies “user may do action”.\n* Missing authorization checks on update/delete endpoints.\n\nDetection hints:\n\n* Enumerate views that modify state; ensure they validate ownership/permission.\n* Look for use of only `is_authenticated` or only `is_staff` without checking object-level access.\n\nFix:\n\n* Add explicit permission checks and tests for unauthorized access.\n\n---\n\n### DJANGO-ADMIN-001: Django admin must be treated as a high-value target\n\nSeverity: High\n\nRequired:\n\n* MUST ensure admin is protected by strong authentication and HTTPS-only transport (see DJANGO-HTTPS-001). ([Django Project][1])\n* SHOULD restrict admin exposure (network allowlists, VPN, SSO, or additional authentication controls) when possible.\n* SHOULD audit installed admin extensions and third-party apps for XSS/CSRF exposure.\n\nInsecure patterns:\n\n* Admin exposed to the internet with weak authentication.\n* Admin served over HTTP.\n\nDetection hints:\n\n* Search `urlpatterns` for `admin.site.urls`.\n* Check deployment config for IP allowlisting or auth gateways.\n\nFix:\n\n* Add network controls and enforce HTTPS.\n\n---\n\n### DJANGO-LOG-001: Logging and error reporting must not leak secrets\n\nSeverity: Medium to High\n\nRequired:\n\n* MUST NOT log secrets (including `SECRET_KEY`, session cookies, auth headers, password reset tokens).\n* MUST configure production logging deliberately; Django’s deployment checklist explicitly calls out reviewing logging before production. ([Django Project][1])\n* MUST ensure `DEBUG=False` in production so exceptions aren’t rendered with sensitive context. ([Django Project][1])\n\nInsecure patterns:\n\n* Logging full request headers or cookies in production.\n* Printing settings dictionaries.\n* Debug error pages.\n\nDetection hints:\n\n* Inspect `LOGGING` config; search for middleware that logs request headers/cookies.\n* Grep for `print(settings` / `logging.info(request.META)` patterns.\n\nFix:\n\n* Redact sensitive values; log IDs not secrets.\n* Use structured logging and a safe error monitoring tool. ([Django Project][1])\n\n---\n\n### DJANGO-SUPPLY-001: Dependency and patch hygiene (Django + security-critical deps)\n\nSeverity: Medium (High if known vulnerable versions)\n\nRequired:\n\n* SHOULD pin and regularly update Django and security-critical dependencies.\n* MUST respond to Django security releases promptly.\n\nDetection hints:\n\n* Check `requirements.txt`, lockfiles, build images.\n* Identify Django version; compare against latest supported release (Django’s download page publishes current stable and supported branches). ([Django Project][9])\n\nFix:\n\n* Upgrade to patched versions; add regression tests for previously vulnerable classes.\n\n---\n\n## 5) Practical scanning heuristics (how to “hunt”)\n\nWhen actively scanning, use these high-signal patterns:\n\n* Deployment/dev server:\n\n  * `manage.py runserver`, `runserver 0.0.0.0`, `--insecure` ([Django Project][1])\n* Debug / settings:\n\n  * `DEBUG = True` ([Django Project][1])\n  * `SECRET_KEY =`, `SECRET_KEY_FALLBACKS` ([Django Project][1])\n* Host validation:\n\n  * `ALLOWED_HOSTS = ['*']` ([Django Project][3])\n* HTTPS and proxy:\n\n  * `SECURE_SSL_REDIRECT`, `SECURE_HSTS_SECONDS`, `SECURE_PROXY_SSL_HEADER` ([Django Project][3])\n* Cookies / sessions:\n\n  * `SESSION_COOKIE_SECURE`, `SESSION_COOKIE_HTTPONLY`, `SESSION_COOKIE_SAMESITE` ([Django Project][3])\n  * `CSRF_COOKIE_SECURE`, `CSRF_COOKIE_HTTPONLY`, `CSRF_COOKIE_SAMESITE` ([Django Project][3])\n* CSRF bypasses:\n\n  * `csrf_exempt`, missing `CsrfViewMiddleware`, POST forms without `{% csrf_token %}` ([Django Project][4])\n* XSS:\n\n  * `|safe`, `autoescape off`, `mark_safe(`, HTML string concatenation ([Django Project][5])\n* SQL injection:\n\n  * `.raw(`, `.extra(`, `RawSQL(`, `cursor.execute(` with formatted SQL strings ([Django Project][7])\n* User uploads / media:\n\n  * `request.FILES`, `MEDIA_ROOT`, `MEDIA_URL`, serving media inline; `MEDIA_ROOT == STATIC_ROOT` ([Django Project][1])\n* Redirects:\n\n  * `redirect(request.GET.get(\"next\"))` patterns; missing allowlist validation\n* Security headers and CSP:\n\n  * Missing `SecurityMiddleware`, missing X-Frame-Options protection, missing `SECURE_CSP` adoption (where appropriate) ([Django Project][2])\n\nAlways try to confirm:\n\n* data origin (untrusted vs trusted)\n* sink type (template/SQL/subprocess/files/redirect/http)\n* protective controls present (middleware, validation, allowlists, authz checks)\n* whether security headers/controls are set in-app vs at the edge\n\n---\n\n## 6) Sources (accessed 2026-01-27)\n\nPrimary Django documentation:\n\n```text\n- Django Downloads (current stable & supported branches): https://www.djangoproject.com/download/\n- Django 6.0 Release Notes: https://docs.djangoproject.com/en/6.0/releases/6.0/\n- Django: Deployment checklist (incl. check --deploy, runserver warning, HTTPS/cookies guidance): https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n- Django: Settings reference (SecurityMiddleware settings, cookies, SECRET_KEY_FALLBACKS, CSP settings): https://docs.djangoproject.com/en/6.0/ref/settings/\n- Django: Security in Django (XSS/CSRF/SQLi/clickjacking/HTTPS/host header validation/uploads/CSP): https://docs.djangoproject.com/en/6.0/topics/security/\n- Django: CSRF how-to (middleware, csrf_token usage, AJAX header patterns, csrf_exempt cautions): https://docs.djangoproject.com/en/6.0/howto/csrf/\n- Django: Performing raw SQL queries (parameterization guidance): https://docs.djangoproject.com/en/6.0/topics/db/sql/\n- Django: QuerySet API reference (extra() cautions; “do not quote placeholders” guidance): https://docs.djangoproject.com/en/6.0/ref/models/querysets/\n- Django: Template built-ins (autoescape tag): https://docs.djangoproject.com/en/6.0/ref/templates/builtins/\n- Django: Template language reference (turning off autoescape & risks): https://docs.djangoproject.com/en/6.0/ref/templates/language/\n- Django: Utilities reference (e.g., format_html): https://docs.djangoproject.com/en/6.0/ref/utils/\n```\n\nOWASP:\n\n```text\n- OWASP Cheat Sheet Series: Django Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Django_Security_Cheat_Sheet.html\n```\n\n[1]: https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ \"https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\"\n[2]: https://docs.djangoproject.com/en/6.0/topics/security/ \"Security in Django | Django documentation | Django\"\n[3]: https://docs.djangoproject.com/en/6.0/ref/settings/ \"Settings | Django documentation | Django\"\n[4]: https://docs.djangoproject.com/en/6.0/howto/csrf/ \"How to use Django’s CSRF protection | Django documentation | Django\"\n[5]: https://docs.djangoproject.com/en/6.0/ref/templates/builtins/ \"https://docs.djangoproject.com/en/6.0/ref/templates/builtins/\"\n[6]: https://docs.djangoproject.com/en/6.0/ref/utils/ \"https://docs.djangoproject.com/en/6.0/ref/utils/\"\n[7]: https://docs.djangoproject.com/en/6.0/topics/db/sql/ \"https://docs.djangoproject.com/en/6.0/topics/db/sql/\"\n[8]: https://docs.djangoproject.com/en/6.0/ref/models/querysets/ \"https://docs.djangoproject.com/en/6.0/ref/models/querysets/\"\n[9]: https://www.djangoproject.com/download/ \"Download Django | Django\"\n"
  },
  {
    "path": "skills/.curated/security-best-practices/references/python-fastapi-web-server-security.md",
    "content": "# FastAPI (Python) Web Security Spec (FastAPI 0.128.x, Python 3.x) ([PyPI][1])\n\nThis document is designed as a **security spec** that supports:\n\n1. **Secure-by-default code generation** for new FastAPI code.\n2. **Security review / vulnerability hunting** in existing FastAPI code (passive “notice issues while working” and active “scan the repo and report findings”).\n\nIt is intentionally written as a set of **normative requirements** (“MUST/SHOULD/MAY”) plus **audit rules** (what bad patterns look like, how to detect them, and how to fix/mitigate them).\n\nFastAPI is commonly deployed with an ASGI server (e.g., Uvicorn) and is built on Starlette + Pydantic, so this spec covers those layers where they affect security. ([PyPI][1])\n\n---\n\n## 0) Safety, boundaries, and anti-abuse constraints (MUST FOLLOW)\n\n* MUST NOT request, output, log, or commit secrets (API keys, passwords, private keys, session cookies, signing keys, database URLs with credentials).\n* MUST NOT “fix” security by disabling protections (e.g., weakening auth, making CORS permissive, skipping signature checks, disabling validation, turning off TLS verification, adding `allow_origins=[\"*\"]` with credentials).\n* MUST provide **evidence-based findings** during audits: cite file paths, code snippets, and configuration values that justify the claim.\n* MUST treat uncertainty honestly: if a protection might exist in infrastructure (reverse proxy, WAF, CDN, service mesh), report it as “not visible in app code; verify at runtime/config”.\n* MUST treat browser controls correctly:\n\n  * CORS is **not** an auth mechanism; it only affects browsers.\n  * CSRF defenses apply when the browser automatically attaches credentials (cookies); they are usually not relevant for purely header-token APIs. ([OWASP Cheat Sheet Series][2])\n\n---\n\n## 1) Operating modes\n\n### 1.1 Generation mode (default)\n\nWhen asked to write new FastAPI code or modify existing code:\n\n* MUST follow every **MUST** requirement in this spec.\n* SHOULD follow every **SHOULD** requirement unless the user explicitly says otherwise.\n* MUST prefer safe-by-default APIs and proven libraries over custom security code.\n* MUST avoid introducing new risky sinks (shell execution, unsafe deserialization, dynamic eval, untrusted template rendering, unsafe file serving, unsafe redirects, arbitrary outbound fetching).\n\n### 1.2 Passive review mode (always on while editing)\n\nWhile working anywhere in a FastAPI repo (even if the user did not ask for a security scan):\n\n* MUST “notice” violations of this spec in touched/nearby code.\n* SHOULD mention issues as they come up, with a brief explanation + safe fix.\n\n### 1.3 Active audit mode (explicit scan request)\n\nWhen the user asks to “scan”, “audit”, or “hunt for vulns”:\n\n* MUST systematically search the codebase for violations of this spec.\n* MUST output findings in a structured format (see §2.3).\n\nRecommended audit order:\n\n1. App entrypoints / deployment scripts / Dockerfiles / Procfiles / Helm/terraform.\n2. ASGI server configuration (Uvicorn/Gunicorn), proxy settings, debug/reload settings.\n3. FastAPI app configuration (docs exposure, middleware, trusted hosts, CORS).\n4. Authn/Authz design (dependencies, JWT/session handling, password storage).\n5. Cookie/session usage + CSRF (if cookies are used).\n6. Input validation and output shaping (Pydantic models, mass assignment, excessive data exposure).\n7. Template rendering and XSS/SSTI (if HTML is served).\n8. File handling (uploads + downloads), StaticFiles, Range support.\n9. Injection classes (SQL, command execution, unsafe deserialization).\n10. Outbound requests (SSRF), redirect handling, WebSockets security.\n\n---\n\n## 2) Definitions and review guidance\n\n### 2.1 Untrusted input (treat as attacker-controlled unless proven otherwise)\n\nExamples include:\n\n* Query parameters / path parameters\n* JSON bodies (including nested fields)\n* Headers (including `Host`, `Origin`, `X-Forwarded-*`)\n* Cookies (including session cookies)\n* File uploads (multipart parts)\n* WebSocket messages, query params, and headers during handshake ([Starlette][3])\n* Any data from external systems (webhooks, third-party APIs, message queues)\n* Any persisted user content (DB rows) that originated from users\n\n### 2.2 State-changing request\n\nA request is state-changing if it can create/update/delete data, change auth/session state, trigger side effects (purchase, email send, webhook send), or initiate privileged actions.\n\n### 2.3 Required audit finding format\n\nFor each issue found, output:\n\n* Rule ID:\n* Severity: Critical / High / Medium / Low\n* Location: file path + function/route name + line(s)\n* Evidence: the exact code/config snippet\n* Impact: what could go wrong, who can exploit it\n* Fix: safe change (prefer minimal diff)\n* Mitigation: defense-in-depth if immediate fix is hard\n* False positive notes: what to verify if uncertain\n\n---\n\n## 3) Secure baseline: minimum production configuration (MUST in production)\n\nThis is the smallest “production baseline” that prevents common FastAPI/ASGI misconfigurations.\n\nBaseline goals:\n\n* No debug tracebacks or auto-reload in production. ([PyPI][4])\n* Run under a production ASGI server configuration (workers, timeouts, resource controls). ([PyPI][4])\n* Host header validation enabled (TrustedHostMiddleware or equivalent). ([PyPI][5])\n* CORS disabled unless explicitly needed; if enabled, it is strict and least-privilege. ([OWASP Cheat Sheet Series][6])\n* Auth is enforced consistently via dependencies (no “oops, forgot auth on this route”). ([FastAPI][7])\n* If cookies/sessions are used, cookie flags are secure and CSRF is addressed. ([OWASP Cheat Sheet Series][8])\n* Request size limits and multipart limits exist at the edge and are validated in app as needed (to mitigate memory/CPU DoS). ([advisories.gitlab.com][9])\n* Dependencies are patched promptly, especially Starlette/python-multipart (multiple DoS and traversal advisories exist historically). ([advisories.gitlab.com][10])\n\n---\n\n## 4) Rules (generation + audit)\n\nEach rule contains: required practice, insecure patterns, detection hints, and remediation.\n\n### FASTAPI-DEPLOY-001: Do not use auto-reload / dev-only server modes in production\n\nSeverity: High (if production)\n\nRequired:\n\n* MUST NOT run production using auto-reload/watch mode (e.g., Uvicorn reload).\n* MUST run with a production process model (e.g., multiple workers where appropriate) and stable server settings. ([PyPI][4])\n\nInsecure patterns:\n\n* `uvicorn ... --reload` (or equivalent “reload=True” configs) in production entrypoints.\n* Docker/Procfile/systemd commands that run with `--reload` in production.\n\nDetection hints:\n\n* Search for `--reload`, `reload=True`, `watchfiles`, `fastapi dev`, “development” run scripts.\n* Check Docker CMD/ENTRYPOINT, Procfile, systemd units, shell scripts.\n\nFix:\n\n* Remove reload in production; run Uvicorn/Gunicorn with stable settings and explicit worker configuration. ([PyPI][4])\n\nNote:\n\n* Reload is fine for local development. Only flag when it is clearly used as a production entrypoint.\n\n---\n\n### FASTAPI-DEPLOY-002: Debug mode MUST be disabled in production\n\nSeverity: Critical\n\nRequired:\n\n* MUST NOT enable debug tracebacks in production (FastAPI/Starlette debug mode can expose sensitive internals and make some exploit chains easier). ([PyPI][5])\n* MUST treat any configuration that returns detailed stack traces to clients as sensitive.\n\nInsecure patterns:\n\n* `app = FastAPI(debug=True)` (or Starlette `debug=True`), or equivalent environment toggles enabling debug in production. ([PyPI][5])\n* Server/log config that exposes tracebacks to end users.\n\nDetection hints:\n\n* Search for `debug=True`, `DEBUG = True`, environment flags mapped to debug.\n* Review exception middleware and error handler setup.\n\nFix:\n\n* Ensure debug is only enabled in local dev/test.\n* Return generic error responses to clients; log details internally.\n\n---\n\n### FASTAPI-OPENAPI-001: OpenAPI and interactive docs MUST be disabled or protected in production\n\nSeverity: Medium (can be High in sensitive/internal apps)\n\nRequired:\n\n* SHOULD disable `/docs`, `/redoc`, and `/openapi.json` in production for public-facing services unless there is an explicit business need.\n* If enabled, MUST protect them (e.g., auth, network allowlists, or internal-only routing).\n* MUST NOT assume “security through obscurity”; treat docs exposure as an information disclosure amplifier.\n\nInsecure patterns:\n\n* Publicly reachable `/docs` and `/openapi.json` for internal/admin APIs.\n* Docs enabled on the same hostname as production without access control.\n\nDetection hints:\n\n* Look for `FastAPI(docs_url=..., redoc_url=..., openapi_url=...)` or defaults.\n* Check reverse proxy routing and allowlists.\n\nFix:\n\n* Disable docs endpoints in prod (`docs_url=None`, `redoc_url=None`, `openapi_url=None`) or restrict access at the edge.\n\n---\n\n### FASTAPI-AUTH-001: Authentication MUST be explicit and consistently enforced via dependencies\n\nSeverity: High\n\nRequired:\n\n* MUST implement authentication as a dependency (or router-level dependency) so that protected endpoints cannot “forget” auth.\n* MUST default to “deny” for privileged routers/endpoints; explicitly mark truly public routes.\n* SHOULD centralize auth enforcement at router boundaries (e.g., protected `APIRouter` for authenticated endpoints). ([FastAPI][7])\n\nInsecure patterns:\n\n* Per-route ad-hoc auth checks scattered through handlers (easy to miss).\n* A mix of protected/unprotected endpoints with no clear policy.\n\nDetection hints:\n\n* Identify routers and endpoints; check whether protected ones include `Depends(...)`/`Security(...)`.\n* Search for patterns like `if user is None: raise ...` inside handlers (instead of dependencies).\n\nFix:\n\n* Move authentication into a dependency and attach it to the router/endpoint consistently using `Depends()`/`Security()`. ([FastAPI][7])\n\n---\n\n### FASTAPI-AUTH-002: Use standard auth transports; avoid secrets in URLs\n\nSeverity: High\n\nRequired:\n\n* SHOULD use the `Authorization: Bearer <token>` header for token auth, not query parameters. ([FastAPI][11])\n* MUST NOT place secrets (tokens, reset links containing long-lived secrets, API keys) in query strings when avoidable.\n\nInsecure patterns:\n\n* `?token=...`, `?api_key=...`, `?auth=...` used for primary auth.\n* Long-lived access tokens embedded in URLs (leak via logs, referrers, caches).\n\nDetection hints:\n\n* Search for parameter names like `token`, `api_key`, `key`, `secret`, `password`.\n* Look for security schemes that use query API keys without justification.\n\nFix:\n\n* Move tokens to Authorization headers; rotate/shorten lifetimes; use POST bodies for sensitive values.\n\n---\n\n### FASTAPI-AUTH-003: Password storage MUST be strongly hashed; never store plaintext passwords\n\nSeverity: Critical\n\nRequired:\n\n* MUST store passwords using a strong, slow password hashing scheme (e.g., Argon2id, bcrypt).\n* MUST NOT store plaintext passwords, or reversible encryption as the primary protection.\n* SHOULD use established libraries for hashing and verification (do not roll your own).\n\nInsecure patterns:\n\n* Storing plaintext passwords in DB.\n* Using fast hashes (e.g., SHA256) without a proper password hashing KDF.\n* Returning password hashes in API responses.\n\nDetection hints:\n\n* Search for `password=` persisted fields, and look for `hashlib.md5/sha1/sha256` usage on passwords.\n* Inspect response models for password/hash fields.\n\nFix:\n\n* Migrate to a proper password hashing library; add a re-hash-on-login upgrade path.\n\n---\n\n### FASTAPI-AUTH-004: JWT validation MUST be strict; JWTs MUST NOT carry secrets\n\nSeverity: High\n\nRequired:\n\n* MUST validate JWT signature and enforce an algorithm allowlist.\n* MUST validate standard claims appropriate to your system (at least `exp`; typically also `iss`/`aud` if multi-service or multi-tenant).\n* MUST treat JWT contents as readable by the client; do not put secrets in JWT payloads. ([FastAPI][12])\n\nInsecure patterns:\n\n* `jwt.decode(..., options={\"verify_signature\": False})` or equivalent.\n* Accepting `alg=none` / algorithm confusion.\n* Using JWT payload to store sensitive secrets (API keys, passwords).\n\nDetection hints:\n\n* Search for `jwt.decode`, `python-jose`, `PyJWT`, `verify_signature`.\n* Check for missing exp validation or long expirations.\n\nFix:\n\n* Enforce strict validation (signature, allowed algorithms, exp, and any required issuer/audience constraints).\n* Store only identifiers/claims you are comfortable exposing to the client. ([FastAPI][12])\n\n---\n\n### FASTAPI-AUTHZ-001: Authorization MUST be enforced per-object and per-property\n\nSeverity: High\n\nRequired:\n\n* MUST perform object-level authorization whenever accessing a resource by user-controlled identifier (ID in path/query/body).\n* MUST perform property-level authorization and response shaping to prevent “excessive data exposure” (e.g., admin-only fields). ([OWASP Foundation][13])\n\nInsecure patterns:\n\n* `GET /users/{id}` returns user record without verifying caller can access that `id`.\n* Response models include internal fields (roles, permissions, billing data, password hashes).\n\nDetection hints:\n\n* Enumerate endpoints that accept IDs; trace whether an authz check is performed.\n* Compare response models for public vs internal fields.\n\nFix:\n\n* Add object-level checks (ownership, ACLs, tenant boundaries).\n* Use dedicated response models that include only allowed fields.\n\n---\n\n### FASTAPI-SESS-001: If using cookie-based sessions and TLS, cookie attributes MUST be secure in production\n\nSeverity: High (only if TLS is enabled)\n\nRequired (production, HTTPS):\n\n* MUST set session cookies to be sent only over HTTPS (secure). IMPORTANT NOTE: Only set `Secure` in production environment when TLS is configured. When running in a local dev environment over HTTP, do not set `Secure` property on cookies. You should do this conditionally based on if the app is running in production mode. You should also include a property like `SESSION_COOKIE_SECURE` which can be used to disable `Secure` cookies when testing over HTTP.\n* MUST set HttpOnly for session cookies (not accessible to JS).\n* SHOULD use `SameSite=Lax` (or `Strict` if UX allows); if you require cross-site cookies, document the CSRF implications and add compensating controls. ([OWASP Cheat Sheet Series][8])\n* If using Starlette `SessionMiddleware`, MUST set `https_only=True` in production and choose an appropriate `same_site`. ([PyPI][5])\n\nInsecure patterns:\n\n* Session cookies without Secure/HttpOnly.\n* `SameSite=None` cookies used for authenticated state-changing endpoints without CSRF protections.\n\nDetection hints:\n\n* Search for `SessionMiddleware(` and inspect parameters like `https_only`, `same_site`.\n* Search for `set_cookie(` usage and cookie flags.\n\nFix:\n\n* Set secure cookie attributes; prefer short lifetimes for high-privilege sessions. ([OWASP Cheat Sheet Series][8])\n\n---\n\n### FASTAPI-SESS-002: Do not store sensitive secrets in signed session cookies\n\nSeverity: High\n\nRequired:\n\n* MUST assume cookie-based session data is readable by the client (signed ≠ encrypted); do not store secrets/PII unless encrypted server-side.\n* Store only opaque identifiers (e.g., session ID) or non-sensitive state in the cookie; store sensitive session state server-side. ([OWASP Cheat Sheet Series][8])\n\nInsecure patterns:\n\n* Storing access tokens, refresh tokens, or PII directly in cookie session payloads.\n* Treating “signed cookies” as confidential storage.\n\nDetection hints:\n\n* Search for `request.session[...] =` or `session[...] =`-equivalent patterns; identify what is stored.\n* Identify use of `SessionMiddleware` or other cookie session mechanisms.\n\nFix:\n\n* Move sensitive values to server-side storage; keep cookie minimal.\n\n---\n\n### FASTAPI-CSRF-001: Cookie-authenticated state-changing requests MUST be CSRF-protected\n\nSeverity: High\n\nNote: This only applies if using cookie based auth. If the application uses header or token based auth such as Authorization header, then CSRF is not an issue.\n\nRequired:\n\n* MUST protect all state-changing endpoints (POST/PUT/PATCH/DELETE) that rely on cookies for authentication.\n* SHOULD use a proven CSRF approach (synchronizer token pattern, or well-reviewed middleware) rather than rolling your own. ([OWASP Cheat Sheet Series][2])\n* MAY add defense-in-depth (Origin/Referer checks, SameSite cookies, Fetch Metadata), but tokens are the primary defense for cookie-authenticated apps. ([OWASP Cheat Sheet Series][2])\n* IMPORTANT NOTE: If cookies are not used for auth (auth is via `Authorization` header), CSRF is usually not applicable. ([FastAPI][11])\n\nInsecure patterns:\n\n* Cookie-authenticated endpoints that change state with no CSRF validation.\n* Using GET for state-changing actions (amplifies CSRF risk).\n\nDetection hints:\n\n* Enumerate routes with methods other than GET; identify whether cookies are used for auth.\n* Look for CSRF token generation/verification or middleware.\n\nFix:\n\n* Add CSRF tokens (and validate them) on state-changing actions when cookie auth is in use. ([OWASP Cheat Sheet Series][2])\n\n---\n\n### FASTAPI-VALID-001: Request parsing and validation MUST be schema-driven; prevent mass assignment\n\nSeverity: Medium (especially for APIs that write to DB)\n\nRequired:\n\n* SHOULD use Pydantic models for request bodies instead of accepting arbitrary `dict`/`Any`.\n* SHOULD configure models to reject unexpected fields where appropriate (prevents “mass assignment” style bugs).\n* MUST validate and normalize identifiers (IDs, email, URLs) before using them for access control or side effects. ([OWASP Cheat Sheet Series][14])\n\nInsecure patterns:\n\n* `payload = await request.json()` followed by `Model(**payload)` or direct DB writes with `payload` (no allowlist).\n* Models that silently accept unknown fields for write endpoints.\n\nDetection hints:\n\n* Search for `await request.json()`, `request.body()`, `dict`-typed bodies, `Any`-typed bodies.\n* Look for endpoints that do `db.update(**payload)` or `Model(**payload)` with unfiltered input.\n\nFix:\n\n* Use explicit Pydantic models with allowlisted fields; reject extras for write endpoints. ([OWASP Cheat Sheet Series][14])\n\n---\n\n### FASTAPI-RESP-001: Prevent excessive data exposure via response models and explicit serialization\n\nSeverity: Medium\n\nRequired:\n\n* MUST define response models that include only intended fields (especially for user objects, auth-related objects, billing objects).\n* SHOULD use separate models for “create input”, “db/internal”, and “public output” to avoid leaking sensitive fields. ([FastAPI][15])\n\nInsecure patterns:\n\n* Returning ORM objects or dicts that include internal columns.\n* Reusing “DB model” as the response model (includes `password_hash`, `is_admin`, etc).\n\nDetection hints:\n\n* Look for endpoints that `return user` where `user` is an ORM instance.\n* Check for `response_model` omissions on endpoints that return sensitive resources.\n\nFix:\n\n* Add explicit response models; create “public” schemas that exclude sensitive fields. ([FastAPI][15])\n\n---\n\n### FASTAPI-XSS-001: Prevent reflected/stored XSS in HTML responses and templates\n\nSeverity: High (if the service serves HTML)\n\nRequired:\n\n* MUST use templating with auto-escaping enabled for HTML.\n* MUST NOT mark untrusted content as safe (no unsafe “raw HTML” rendering of user-controlled data).\n* SHOULD deploy a CSP when serving HTML that includes any user content. ([OWASP Cheat Sheet Series][16])\n\nInsecure patterns:\n\n* Rendering user content directly into HTML without escaping/sanitization.\n* Disabling auto-escaping or using “raw HTML” features without sanitization.\n\nDetection hints:\n\n* Search for template rendering and string concatenation that builds HTML.\n* Review templates for “unsafe” filters/constructs and unquoted attributes.\n\nFix:\n\n* Keep auto-escaping on; sanitize user HTML only if absolutely required using a trusted sanitizer; add CSP. ([OWASP Cheat Sheet Series][16])\n\nNote:\n\n* If the app is a pure JSON API, XSS is usually a client/app concern, but error pages/docs pages might still render HTML.\n\n---\n\n### FASTAPI-SSTI-001: Never render untrusted templates (Server-Side Template Injection)\n\nSeverity: Critical\n\nRequired:\n\n* MUST NOT render templates that contain user-controlled template syntax.\n* MUST treat “template-from-string” rendering as dangerous if influenced by untrusted input.\n* If untrusted templates are absolutely required (rare, high-risk):\n\n  * MUST use a sandboxed templating approach and restrict capabilities.\n  * MUST assume sandbox escapes are possible; add isolation and strict allowlists. ([OWASP Foundation][17])\n\nInsecure patterns:\n\n* Rendering templates loaded from user input or DB via a normal Jinja environment.\n* Building templates dynamically using user-controlled strings.\n\nDetection hints:\n\n* Grep for Jinja `Environment.from_string`, `Template(...)`, or similar.\n* Trace origin of template string (request, DB, uploads, admin panels).\n\nFix:\n\n* Replace with non-executable templating (simple string substitution).\n* If truly needed, use Jinja’s sandbox environment plus strong isolation. ([jinja.palletsprojects.com][18])\n\n---\n\n### FASTAPI-HEADERS-001: Set essential security headers (in app or at the edge)\n\nSeverity: Medium\n\nRequired (typical API/web app):\n\n* SHOULD set:\n\n  * `X-Content-Type-Options: nosniff`\n  * Clickjacking protection (`X-Frame-Options` and/or CSP `frame-ancestors`) if HTML is served\n  * `Referrer-Policy` and `Permissions-Policy` as appropriate\n\nNOTE:\n\n* Headers may be set by a proxy/CDN. If not visible in app code, flag as “verify at edge”. ([OWASP Cheat Sheet Series][6])\n\nInsecure patterns:\n\n* No security headers anywhere (app or edge) for apps serving HTML or sensitive APIs.\n\nDetection hints:\n\n* Search for middleware that sets headers; check reverse proxy config.\n\nFix:\n\n* Set headers centrally (middleware) or via reverse proxy/CDN.\n\n---\n\n### FASTAPI-CORS-001: CORS MUST be explicit and least-privilege\n\nSeverity: Medium (High if misconfigured with credentials)\n\nRequired:\n\n* If CORS is not needed, MUST keep it disabled.\n* If CORS is needed:\n\n  * MUST allowlist trusted origins (do not reflect arbitrary origins).\n  * MUST NOT combine credentialed requests with wildcard origins (this is unsafe and commonly rejected by compliant middleware). ([OWASP Cheat Sheet Series][6])\n  * SHOULD restrict allowed methods and headers.\n\nInsecure patterns:\n\n* `allow_origins=[\"*\"]` together with `allow_credentials=True`.\n* Reflecting `Origin` without validation.\n* `allow_origin_regex=\".*\"` used broadly.\n\nDetection hints:\n\n* Search for `CORSMiddleware` configuration.\n* Look for `allow_origins=[\"*\"]`, `allow_credentials=True`, `allow_origin_regex`.\n\nFix:\n\n* Use an explicit origin allowlist and minimal methods/headers; keep credentials off unless required. ([OWASP Cheat Sheet Series][6])\n\n---\n\n### FASTAPI-HOST-001: Host header MUST be validated in production\n\nSeverity: Low\n\nRequired:\n\n* SHOULD use `TrustedHostMiddleware` (or equivalent at edge) to restrict accepted Host values. ([PyPI][5])\n* MUST NOT trust the `Host` header for security-sensitive decisions without validation.\n\nInsecure patterns:\n\n* No Host validation while generating external URLs (password reset links, callback URLs) from request host.\n* Allowing arbitrary Host headers in apps behind permissive proxies.\n\nDetection hints:\n\n* Search for `TrustedHostMiddleware` usage.\n* Search for logic that uses `request.url`, `request.base_url`, or host-derived values to build external URLs.\n\nFix:\n\n* Configure a strict allowed-hosts list in production; enforce at edge too if possible.\n\n---\n\n### FASTAPI-PROXY-001: Reverse proxy trust MUST be configured correctly\n\nSeverity: High (when behind a proxy)\n\nRequired:\n\n* If behind a reverse proxy, MUST configure forwarded-header trust correctly.\n* MUST NOT blindly trust `X-Forwarded-*` headers from the open internet.\n* If using Uvicorn proxy header support, MUST restrict which IPs are allowed to provide forwarded headers. ([PyPI][4])\n\nInsecure patterns:\n\n* Enabling proxy headers broadly without restricting trusted proxy IPs.\n* Using forwarded headers to decide “is secure” / “is internal” / “client IP” without proper trust boundaries.\n\nDetection hints:\n\n* Search for `--proxy-headers`, `--forwarded-allow-ips`, or equivalent config.\n* Search for security-sensitive use of `request.client.host`, `request.url.scheme`, `request.headers[\"x-forwarded-for\"]`.\n\nFix:\n\n* Configure Uvicorn with proxy headers only when behind a known proxy, and restrict `forwarded_allow_ips` to that proxy. ([PyPI][4])\n* Keep Host allowlisting in place even behind proxies.\n\n---\n\n### FASTAPI-LIMITS-001: Request and multipart limits MUST be enforced to prevent DoS\n\nSeverity: Low\n\nRequired:\n\n* MUST enforce request size limits at the edge (reverse proxy/load balancer) and validate in app where needed.\n* MUST apply special scrutiny to multipart/form-data handling; historical vulnerabilities include unbounded buffering and DoS vectors. ([advisories.gitlab.com][9])\n* SHOULD rate limit and/or add per-IP/per-user throttles for expensive endpoints.\n\nInsecure patterns:\n\n* Accepting arbitrarily large JSON bodies or multipart forms.\n* Parsing multipart forms without size/field-count controls.\n\nDetection hints:\n\n* Identify file upload endpoints and `multipart/form-data` usage.\n* Look for missing proxy-level limits (nginx `client_max_body_size`, ALB limits, etc.) and missing app-level checks.\n\nFix:\n\n* Enforce strict body limits and multipart constraints; keep Starlette and python-multipart updated to patched versions. ([advisories.gitlab.com][9])\n\n---\n\n### FASTAPI-FILES-001: Prevent path traversal and unsafe static file exposure\n\nSeverity: High\n\nRequired:\n\n* MUST NOT pass user-controlled file paths to `FileResponse`/filesystem calls without strict validation and safe base directories.\n* If using `StaticFiles`, MUST keep Starlette updated and understand the security history (path traversal advisory exists for older versions). ([advisories.gitlab.com][10])\n* MUST NOT serve user uploads as executable/active content (especially HTML/JS) from a static root without safe handling.\n\nInsecure patterns:\n\n* `FileResponse(request.query_params[\"path\"])`\n* Mounting `StaticFiles(directory=\"uploads\")` where uploads include HTML/JS/SVG and are served inline.\n\nDetection hints:\n\n* Search for `FileResponse(`, `StaticFiles(`, `open(` in routes.\n* Trace whether the path originates from untrusted input.\n\nFix:\n\n* Use opaque IDs for files; map IDs to server-side stored paths.\n* Serve untrusted content as attachment downloads where appropriate.\n\n---\n\n### FASTAPI-FILES-002: Mitigate Range-header DoS on file-serving endpoints\n\nSeverity: Low (if affected versions and file serving is enabled)\n\nRequired:\n\n* MUST keep Starlette patched against known file-serving DoS issues if using `FileResponse`/`StaticFiles`.\n* MUST treat unusual `Range` header handling and file serving as a DoS surface. ([advisories.gitlab.com][19])\n\nInsecure patterns:\n\n* Serving large files with vulnerable Starlette versions.\n* No rate limiting / CDN shielding for file endpoints.\n\nDetection hints:\n\n* Identify Starlette version; if in affected range, flag.\n* Find uses of `FileResponse` and `StaticFiles`.\n\nFix:\n\n* Upgrade Starlette to a fixed version per advisory guidance. ([advisories.gitlab.com][19])\n* Add edge caching/rate limiting for file endpoints where appropriate.\n\n---\n\n### FASTAPI-UPLOAD-001: File uploads MUST be validated, stored safely, and served safely\n\nSeverity: Medium\n\nRequired:\n\n* MUST enforce upload size limits (app + edge).\n* MUST validate file type using allowlists and content checks (not only extension). ([OWASP Cheat Sheet Series][20])\n* SHOULD generate server-side filenames (random IDs) and avoid trusting original names.\n* MUST serve potentially active formats safely (download attachment) unless explicitly intended.\n\nInsecure patterns:\n\n* Accepting arbitrary file types and serving them back inline.\n* Using user-supplied filename as storage path.\n\nDetection hints:\n\n* Look for upload handlers and where/how files are written.\n* Look for direct exposure of upload directories.\n\nFix:\n\n* Implement allowlist validation + safe storage + safe serving; add scanning/quarantine if applicable. ([OWASP Cheat Sheet Series][20])\n\n---\n\n### FASTAPI-INJECT-001: Prevent SQL injection (use parameterized queries / ORM)\n\nSeverity: High\n\nRequired:\n\n* MUST use parameterized queries or an ORM that parameterizes under the hood.\n* MUST NOT build SQL by string concatenation / f-strings with untrusted input. ([OWASP Cheat Sheet Series][21])\n\nInsecure patterns:\n\n* `f\"SELECT ... WHERE id={user_id}\"`\n* `\"... WHERE name = '%s'\" % user_input`\n\nDetection hints:\n\n* Grep for SQL keywords in Python strings near `.execute(...)`.\n* Trace untrusted data into DB calls.\n\nFix:\n\n* Replace with parameterized queries / ORM query APIs; validate types before querying. ([OWASP Cheat Sheet Series][21])\n\n---\n\n### FASTAPI-INJECT-002: Prevent OS command injection\n\nSeverity: Critical to High (depends on exposure)\n\nRequired:\n\n* MUST avoid executing shell commands with untrusted input.\n* If subprocess is necessary:\n\n  * MUST pass args as a list (not a string)\n  * MUST NOT use `shell=True` with attacker-influenced strings\n  * SHOULD use strict allowlists for any variable component ([OWASP Cheat Sheet Series][22])\n\nInsecure patterns:\n\n* `os.system(user_input)`\n* `subprocess.run(f\"cmd {user}\", shell=True)`\n* Passing user strings into `bash -c`, `sh -c`, PowerShell, etc.\n\nDetection hints:\n\n* Search for `os.system`, `subprocess`, `Popen`, `shell=True`.\n* Trace data from request/DB into these calls.\n\nFix:\n\n* Use library APIs instead of shell commands.\n* If unavoidable, hard-code the command and allowlist validated parameters; use `--` separator where supported. ([OWASP Cheat Sheet Series][22])\n\n---\n\n### FASTAPI-SSRF-001: Prevent server-side request forgery (SSRF) in outbound HTTP\n\nSeverity: Medium (can be High in cloud/VPC environments)\n\n- Note: For small stand alone projects this is less important. It is most important when deploying into an LAN or with other services listening on the same server.\n\nRequired:\n\n* MUST treat outbound requests to user-provided URLs as high risk.\n* SHOULD validate and restrict destinations (allowlist hosts/domains) for any user-influenced URL fetch.\n* SHOULD block access to localhost/private IP ranges/link-local and cloud metadata endpoints.\n* MUST restrict protocols to http/https.\n* SHOULD set timeouts and carefully control redirects. ([OWASP Cheat Sheet Series][23])\n\nInsecure patterns:\n\n* `httpx.get(request.query_params[\"url\"])`\n* “URL preview/import/webhook tester” features that accept arbitrary URLs.\n\nDetection hints:\n\n* Search for `requests`, `httpx`, `urllib`, `aiohttp` calls with URLs derived from requests/DB.\n* Identify endpoints named `fetch`, `preview`, `proxy`, `webhook`, `import`.\n\nFix:\n\n* Implement strict URL parsing + allowlists; add egress controls; set short timeouts; disable redirects if not required. ([OWASP Cheat Sheet Series][23])\n\n---\n\n### FASTAPI-REDIRECT-001: Prevent open redirects\n\nSeverity: Low\n\nRequired:\n\n* MUST validate redirect targets derived from untrusted input (`next`, `redirect`, `return_to`).\n* SHOULD prefer redirecting only to same-site relative paths or an allowlist of domains. ([OWASP Cheat Sheet Series][24])\n\nInsecure patterns:\n\n* Returning `RedirectResponse(next)` where `next` is user-controlled with no validation.\n\nDetection hints:\n\n* Search for `RedirectResponse(` or redirect logic and examine the source of the target.\n\nFix:\n\n* Allow only relative paths or allowlisted domains; fall back to a safe default. ([OWASP Cheat Sheet Series][24])\n\n---\n\n### FASTAPI-WS-001: WebSocket endpoints MUST be authenticated and protected against cross-site abuse\n\nSeverity: Medium to High (depends on data/privilege)\n\nRequired:\n\n* MUST authenticate WebSocket connections for any non-public channel (WebSockets don’t inherently provide auth). ([OWASP Cheat Sheet Series][25])\n* SHOULD enforce origin/CSRF-like protections appropriate for browser-based WebSocket clients (Origin validation is a common control).\n* SHOULD rate limit message frequency and connection attempts; close idle/abusive connections.\n\nInsecure patterns:\n\n* `@app.websocket(...)` accepts and trusts the connection with no auth check.\n* Using query-string tokens for auth without considering leakage/rotation.\n\nDetection hints:\n\n* Search for `@app.websocket` / `websocket_endpoint` and inspect whether auth is performed before accepting sensitive operations.\n* Review origin checks, token parsing, and per-connection authorization.\n\nFix:\n\n* Require authentication during handshake (e.g., a token or session) and enforce authorization for actions/messages.\n* Validate Origin for browser-based clients where appropriate; apply rate limits and timeouts. ([OWASP Cheat Sheet Series][25])\n\n---\n\n### FASTAPI-SUPPLY-001: Dependency and patch hygiene (focus on security-relevant deps)\n\nSeverity: Low\n\nRequired:\n\n* SHOULD pin and regularly update security-critical dependencies (FastAPI, Starlette, Uvicorn, Pydantic, python-multipart, auth/JWT libs).\n* MUST respond to known security advisories promptly.\n* MUST treat file serving and multipart parsing dependencies as security-sensitive due to historical CVEs. ([advisories.gitlab.com][10])\n\nAudit focus examples (historical):\n\n* Starlette StaticFiles path traversal (fixed in 0.27.0). ([advisories.gitlab.com][10])\n* Starlette multipart/form-data DoS (fixed in 0.40.0). ([advisories.gitlab.com][9])\n* Starlette FileResponse Range header DoS (fixed in 0.49.1). ([advisories.gitlab.com][19])\n\nDetection hints:\n\n* Check `requirements.txt`, lockfiles, container images, and runtime environments for actual installed versions.\n* Map file upload/file serving features to dependency versions.\n\nFix:\n\n* Upgrade to patched versions per advisories; add regression tests around affected behavior.\n\n---\n\n## 5) Practical scanning heuristics (how to “hunt”)\n\nWhen actively scanning, use these high-signal patterns:\n\n* Dev server / debug:\n\n  * `--reload`, `reload=True`, `debug=True`, `FastAPI(debug=True)` ([PyPI][4])\n* OpenAPI/docs exposure:\n\n  * `/docs`, `/redoc`, `/openapi.json`, `docs_url=`, `openapi_url=`\n* Auth enforcement gaps:\n\n  * Endpoints missing `Depends()`/`Security()` where expected; routers without a consistent dependency boundary ([FastAPI][7])\n  * Tokens in query params (`token=`, `api_key=`, `key=`) ([FastAPI][11])\n* Session/cookies + CSRF:\n\n  * `SessionMiddleware(` and cookie flags (`https_only`, `same_site`) ([PyPI][5])\n  * POST/PUT/PATCH/DELETE handlers using cookie auth with no CSRF checks ([OWASP Cheat Sheet Series][2])\n* Input validation & mass assignment:\n\n  * `await request.json()` and direct DB writes from dicts; models accepting extra fields ([OWASP Cheat Sheet Series][14])\n* Excessive data exposure:\n\n  * Returning ORM objects or dicts without `response_model`; responses containing password/role/internal fields ([FastAPI][15])\n* CORS:\n\n  * `CORSMiddleware` with `allow_origins=[\"*\"]`, `allow_origin_regex=\".*\"`, `allow_credentials=True` ([OWASP Cheat Sheet Series][6])\n* Files:\n\n  * `FileResponse(` with user-controlled paths; `StaticFiles(` exposing uploads ([advisories.gitlab.com][10])\n* Uploads / multipart:\n\n  * `multipart/form-data` endpoints with no size/field constraints; outdated Starlette/python-multipart ([advisories.gitlab.com][9])\n* Injection:\n\n  * SQL strings with f-strings/concatenation into `.execute(...)` ([OWASP Cheat Sheet Series][21])\n  * `subprocess.*`, `shell=True`, `os.system` ([OWASP Cheat Sheet Series][22])\n* SSRF:\n\n  * `httpx.get/post` or `requests.*` with URL from request/DB, no allowlist/timeouts ([OWASP Cheat Sheet Series][23])\n* Redirect:\n\n  * `RedirectResponse(next)` with no validation ([OWASP Cheat Sheet Series][24])\n* WebSockets:\n\n  * `@app.websocket` handlers without auth/origin checks; use of `ws://` in prod configs ([FastAPI][27])\n\nAlways try to confirm:\n\n* data origin (untrusted vs trusted)\n* sink type (SQL/subprocess/files/template/http/redirect/ws)\n* protective controls present (validation, allowlists, middleware, edge controls)\n* installed dependency versions vs vulnerable ranges ([advisories.gitlab.com][10])\n\n---\n\n## 6) Sources (accessed 2026-01-27)\n\nPrimary framework documentation:\n\n* FastAPI (PyPI metadata, versioning) — `https://pypi.org/project/fastapi/` ([PyPI][1])\n* FastAPI docs: Security “First Steps” (Authorization Bearer header conventions) — `https://fastapi.tiangolo.com/tutorial/security/first-steps/` ([FastAPI][11])\n* FastAPI reference: Dependencies (`Depends`, `Security`) — `https://fastapi.tiangolo.com/reference/dependencies/` ([FastAPI][7])\n* FastAPI reference: APIRouter (router-level dependencies) — `https://fastapi.tiangolo.com/reference/apirouter/` ([FastAPI][28])\n* FastAPI docs: WebSockets — `https://fastapi.tiangolo.com/advanced/websockets/` ([FastAPI][27])\n\nASGI/server stack documentation:\n\n* Starlette (PyPI, general capabilities) — `https://pypi.org/project/starlette/` ([PyPI][5])\n* Starlette docs: WebSockets — `https://starlette.dev/websockets/` ([Starlette][3])\n* Uvicorn (PyPI metadata) — `https://pypi.org/project/uvicorn/` ([PyPI][4])\n* Pydantic docs (v2.12.x) — `https://docs.pydantic.dev/latest/` ([Pydantic][29])\n\nSecurity standards and cheat sheets:\n\n* OWASP Cheat Sheet Series: Session Management — `https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][8])\n* OWASP Cheat Sheet Series: CSRF Prevention — `https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][2])\n* OWASP Cheat Sheet Series: XSS Prevention — `https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][16])\n* OWASP Cheat Sheet Series: Mass Assignment — `https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][14])\n* OWASP API Security Top 10 (2023) — `https://owasp.org/API-Security/editions/2023/en/0x11-t10/` ([OWASP Foundation][13])\n* OWASP Cheat Sheet Series: SQL Injection Prevention — `https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][21])\n* OWASP Cheat Sheet Series: OS Command Injection Defense — `https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][22])\n* OWASP Cheat Sheet Series: SSRF Prevention — `https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][23])\n* OWASP Cheat Sheet Series: File Upload — `https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][20])\n* OWASP Cheat Sheet Series: Unvalidated Redirects and Forwards — `https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][24])\n* OWASP Cheat Sheet Series: HTTP Security Response Headers — `https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][6])\n* OWASP Cheat Sheet Series: WebSocket Security — `https://cheatsheetseries.owasp.org/cheatsheets/WebSocket_Security_Cheat_Sheet.html` ([OWASP Cheat Sheet Series][25])\n* OWASP WSTG: Testing for Server-Side Template Injection — `https://owasp.org/www-project-web-security-testing-guide/v41/4-Web_Application_Security_Testing/07-Input_Validation_Testing/18-Testing_for_Server_Side_Template_Injection` ([OWASP Foundation][17])\n* OWASP WSTG: Testing WebSockets — `https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/10-Testing_WebSockets` ([OWASP Foundation][26])\n\nTemplate safety references:\n\n* Jinja: Sandbox — `https://jinja.palletsprojects.com/en/stable/sandbox/` ([jinja.palletsprojects.com][18])\n\nSelected supply-chain/advisory references (Starlette examples):\n\n* CVE-2023-29159 (StaticFiles path traversal; fixed 0.27.0) — `https://advisories.gitlab.com/pkg/pypi/starlette/CVE-2023-29159/` ([advisories.gitlab.com][10])\n* CVE-2024-47874 (multipart/form-data DoS; fixed 0.40.0) — `https://advisories.gitlab.com/pkg/pypi/starlette/CVE-2024-47874/` ([advisories.gitlab.com][9])\n* CVE-2025-62727 (FileResponse Range header DoS; fixed 0.49.1) — `https://advisories.gitlab.com/pkg/pypi/starlette/CVE-2025-62727/` ([advisories.gitlab.com][19])\n\n[1]: https://pypi.org/project/fastapi/ \"https://pypi.org/project/fastapi/\"\n[2]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html\"\n[3]: https://starlette.dev/websockets/?utm_source=chatgpt.com \"Websockets\"\n[4]: https://pypi.org/project/uvicorn/ \"https://pypi.org/project/uvicorn/\"\n[5]: https://pypi.org/project/starlette/ \"https://pypi.org/project/starlette/\"\n[6]: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html?utm_source=chatgpt.com \"HTTP Security Response Headers Cheat Sheet\"\n[7]: https://fastapi.tiangolo.com/reference/dependencies/?utm_source=chatgpt.com \"Dependencies - Depends() and Security() - FastAPI\"\n[8]: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html\"\n[9]: https://advisories.gitlab.com/pkg/pypi/starlette/CVE-2024-47874/ \"Starlette Denial of service (DoS) via multipart/form-data | GitLab Advisory Database\"\n[10]: https://advisories.gitlab.com/pkg/pypi/starlette/CVE-2023-29159/ \"Starlette has Path Traversal vulnerability in StaticFiles | GitLab Advisory Database\"\n[11]: https://fastapi.tiangolo.com/tutorial/security/first-steps/?utm_source=chatgpt.com \"Security - First Steps - FastAPI\"\n[12]: https://fastapi.tiangolo.com/tutorial/response-model/ \"https://fastapi.tiangolo.com/tutorial/response-model/\"\n[13]: https://owasp.org/API-Security/editions/2023/en/0x11-t10/ \"https://owasp.org/API-Security/editions/2023/en/0x11-t10/\"\n[14]: https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html\"\n[15]: https://fastapi.tiangolo.com/tutorial/extra-models/ \"https://fastapi.tiangolo.com/tutorial/extra-models/\"\n[16]: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html\"\n[17]: https://owasp.org/www-project-web-security-testing-guide/v41/4-Web_Application_Security_Testing/07-Input_Validation_Testing/18-Testing_for_Server_Side_Template_Injection?utm_source=chatgpt.com \"Testing for Server Side Template Injection\"\n[18]: https://jinja.palletsprojects.com/en/stable/sandbox/?utm_source=chatgpt.com \"Sandbox — Jinja Documentation (3.1.x)\"\n[19]: https://advisories.gitlab.com/pkg/pypi/starlette/CVE-2025-62727/ \"Starlette vulnerable to O(n^2) DoS via Range header merging in ``starlette.responses.FileResponse`` | GitLab Advisory Database\"\n[20]: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html\"\n[21]: https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html\"\n[22]: https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html\"\n[23]: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html \"https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html\"\n[24]: https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html?utm_source=chatgpt.com \"Unvalidated Redirects and Forwards Cheat Sheet\"\n[25]: https://cheatsheetseries.owasp.org/cheatsheets/WebSocket_Security_Cheat_Sheet.html?utm_source=chatgpt.com \"WebSocket Security - OWASP Cheat Sheet Series\"\n[26]: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/10-Testing_WebSockets?utm_source=chatgpt.com \"WSTG - Latest | OWASP Foundation\"\n[27]: https://fastapi.tiangolo.com/advanced/websockets/?utm_source=chatgpt.com \"WebSockets - FastAPI\"\n[28]: https://fastapi.tiangolo.com/reference/apirouter/?utm_source=chatgpt.com \"APIRouter class - FastAPI\"\n[29]: https://docs.pydantic.dev/latest/ \"https://docs.pydantic.dev/latest/\"\n"
  },
  {
    "path": "skills/.curated/security-best-practices/references/python-flask-web-server-security.md",
    "content": "# Flask (Python) Web Security Spec (Flask 3.1.x, Python 3.x)\n\nThis document is designed as a **security spec** that supports:\n1) **Secure-by-default code generation** for new Flask code.\n2) **Security review / vulnerability hunting** in existing Flask code (passive “notice issues while working” and active “scan the repo and report findings”).\n\nIt is intentionally written as a set of **normative requirements** (“MUST/SHOULD/MAY”) plus **audit rules** (what bad patterns look like, how to detect them, and how to fix/mitigate them).\n\n--------------------------------------------------------------------\n\n## 0) Safety, boundaries, and anti-abuse constraints (MUST FOLLOW)\n\n- MUST NOT request, output, log, or commit secrets (API keys, passwords, private keys, session cookies, SECRET_KEY).\n- MUST NOT “fix” security by disabling protections (e.g., turning off CSRF, relaxing CORS, disabling escaping, disabling auth checks).\n- MUST provide **evidence-based findings** during audits: cite file paths, code snippets, and configuration values that justify the claim.\n- MUST treat uncertainty honestly: if a protection might exist in infrastructure (reverse proxy, WAF, CDN), report it as “not visible in app code; verify at runtime/config”.\n\n--------------------------------------------------------------------\n\n## 1) Operating modes\n\n### 1.1 Generation mode (default)\nWhen asked to write new Flask code or modify existing code:\n- MUST follow every **MUST** requirement in this spec.\n- SHOULD follow every **SHOULD** requirement unless the user explicitly says otherwise.\n- MUST prefer safe-by-default APIs and proven libraries over custom security code.\n- MUST avoid introducing new risky sinks (template rendering from strings, shell execution, dynamic imports, unsafe redirects, serving user files as HTML, etc.).\n\n### 1.2 Passive review mode (always on while editing)\nWhile working anywhere in a Flask repo (even if the user did not ask for a security scan):\n- MUST “notice” violations of this spec in touched/nearby code.\n- SHOULD mention issues as they come up, with a brief explanation + safe fix.\n\n### 1.3 Active audit mode (explicit scan request)\nWhen the user asks to “scan”, “audit”, or “hunt for vulns”:\n- MUST systematically search the codebase for violations of this spec.\n- MUST output findings in a structured format (see §2.3).\n\nRecommended audit order:\n1) App entrypoints / deployment scripts / Dockerfiles / Procfiles.\n2) Flask configuration and environment handling.\n3) Auth + sessions + cookies.\n4) CSRF protections and state-changing routes.\n5) Template rendering and XSS/SSTI.\n6) File handling (uploads + downloads) and path traversal.\n7) Injection classes (SQL, command execution, unsafe deserialization).\n8) Outbound requests (SSRF).\n9) Redirect handling (open redirects).\n10) CORS and security headers.\n\n--------------------------------------------------------------------\n\n## 2) Definitions and review guidance\n\n### 2.1 Untrusted input (treat as attacker-controlled unless proven otherwise)\nExamples include:\n- `request.args`, `request.form`, `request.values`\n- `request.get_json()`, `request.json`, `request.data`\n- `request.headers`, `request.cookies`\n- URL path parameters (e.g., `/user/<id>`)\n- Any data from external systems (webhooks, third-party APIs, message queues)\n- Any persisted user content (DB rows) that originated from users\n\n### 2.2 State-changing request\nA request is state-changing if it can create/update/delete data, change auth/session state, trigger side effects (purchase, email send, webhook send), or initiate privileged actions.\n\n### 2.3 Required audit finding format\nFor each issue found, output:\n\n- Rule ID:\n- Severity: Critical / High / Medium / Low\n- Location: file path + function/route name + line(s)\n- Evidence: the exact code/config snippet\n- Impact: what could go wrong, who can exploit it\n- Fix: safe change (prefer minimal diff)\n- Mitigation: defense-in-depth if immediate fix is hard\n- False positive notes: what to verify if uncertain\n\n--------------------------------------------------------------------\n\n## 3) Secure baseline: minimum production configuration (MUST in production)\n\nThis is the smallest “production baseline” that prevents common Flask misconfigurations.\n\n### 3.1 App initialization pattern (SHOULD)\nSHOULD use an app factory and environment-based config so production config is not hard-coded.\n\nExample skeleton (illustrative; adjust to your project):\n- Load config from environment / secret store.\n- Fail closed if critical settings are missing in production.\n\nKey baseline config targets:\n- `SECRET_KEY` set and not committed\n- `SESSION_COOKIE_SECURE=True` (when HTTPS) IMPORTANT NOTE: Only set `Secure` in production environment when TLS is configured. When running in a local dev environment over HTTP, do not set `Secure` property on cookies. You should do this conditionally based on if the app is running in production mode. You should also include a property like `SESSION_COOKIE_SECURE` which can be used to disable `Secure` cookies when testing over HTTP.\n- `SESSION_COOKIE_HTTPONLY=True`\n- `SESSION_COOKIE_SAMESITE='Lax'` (or `'Strict'` if compatible)\n- `TRUSTED_HOSTS` set in production\n- Security headers set (CSP, etc.) either in app or at the edge\n\n--------------------------------------------------------------------\n\n## 4) Rules (generation + audit)\n\nEach rule contains: required practice, insecure patterns, detection hints, and remediation.\n\n### FLASK-DEPLOY-001: Do not use Flask’s development server in production\nSeverity: High (if production)\n\nRequired:\n- MUST NOT deploy the built-in development server as the production server.\n- MUST run behind a production-grade WSGI server or managed platform (such as gunicorn)\n\nInsecure patterns:\n- `app.run(...)` in a production entrypoint.\n- Deployment docs/scripts that use `flask run` in production.\n\nDetection hints:\n- Search for `app.run(`, `flask run`, `--debug`, `FLASK_DEBUG`, `FLASK_ENV=development`.\n- Check Docker CMD/ENTRYPOINT, Procfile, systemd units, shell scripts.\n\nFix:\n- Use a production WSGI server (and keep Flask as the app object).\n- Ensure the dev server is only used for local development.\n\nNote:\n- These are often used in dev mode or local testing. This is allowed. Only flag if it is clear that it is being used as the production entrypoint\n\n---\n\n### FLASK-DEPLOY-002: Debug mode MUST be disabled in production\nSeverity: Critical\n\nRequired:\n- MUST NOT enable debug mode in production.\n- MUST treat the interactive debugger as equivalent to remote code execution if exposed.\n\nInsecure patterns:\n- `app.run(debug=True)`\n- `flask run --debug` in production.\n- `DEBUG=True` via env/config in production.\n\nDetection hints:\n- Look for `debug=True`, `FLASK_DEBUG=1`, `DEBUG = True`, `app.debug = True`.\n- Look for `TRAP_HTTP_EXCEPTIONS`/debugger settings enabled in non-test contexts.\n\nFix:\n- Ensure debug is only enabled in local dev/test.\n- Prefer environment-based toggles and safe defaults.\n\nNote:\n- These are often used in dev mode or local testing. This is allowed. Only flag if it is clear that it is being used as the production entrypoint\n\n---\n\n### FLASK-CONFIG-001: SECRET_KEY must be strong, secret, and rotated safely\nSeverity: High (Critical if missing in production with sessions or signing)\n\nRequired:\n- MUST set a strong random `SECRET_KEY` in production.\n- MUST keep `SECRET_KEY` out of source control and out of logs.\n- MAY rotate keys periodically; MAY use `SECRET_KEY_FALLBACKS` to support rotation without instantly invalidating existing sessions, then remove old keys after the rotation window. This likely is not needed for smaller applications but is good practice for larger applications. As this may complicate deployment, suggest that it be implemented rather than implementing it by default.\n\nInsecure patterns:\n- Missing `SECRET_KEY` in production.\n- Hard-coded `SECRET_KEY` in repo (including test keys accidentally used in prod).\n- Logging or printing `SECRET_KEY`.\n\nDetection hints:\n- Search for `SECRET_KEY =`, `app.secret_key =`, `SECRET_KEY_FALLBACKS =`.\n- Check `.env` files committed to repo.\n- Check config modules for constants.\n\nFix:\n- Load from secret manager or environment variable.\n- Add a rotation process:\n  - Set new `SECRET_KEY`\n  - Keep old key(s) temporarily in `SECRET_KEY_FALLBACKS`\n  - Remove old key(s) after the safe window.\n\nNotes:\n- If the application uses Flask sessions (cookie-based by default), `SECRET_KEY` is directly security-critical.\n\n---\n\n### FLASK-SESS-001: Session cookies must use secure attributes in production\nSeverity: Medium\n\nRequired (production, HTTPS):\n- MUST set `SESSION_COOKIE_SECURE=True` (cookies only over HTTPS). NOTE: Only set `Secure` in production environment when TLS is configured. When running in a local dev environment over HTTP, do not set `Secure` property on cookies. You should do this conditionally based on if the app is running in production mode. You should also include a property like `SESSION_COOKIE_SECURE` which can be used to disable `Secure` cookies when testing over HTTP.\n- MUST ensure `SESSION_COOKIE_HTTPONLY=True` (protect from JS access).\n- SHOULD set `SESSION_COOKIE_SAMESITE='Lax'` (recommended) or `'Strict'` if compatible with UX.\n- SHOULD keep `SESSION_COOKIE_DOMAIN=None` unless you explicitly need subdomain-wide cookies.\n- If you need embedded/iframe third-party usage, MAY consider `SESSION_COOKIE_PARTITIONED=True` (requires HTTPS).\n\nInsecure patterns:\n- `SESSION_COOKIE_SECURE=False` in production.\n- `SESSION_COOKIE_HTTPONLY=False`.\n- `SESSION_COOKIE_SAMESITE=None` with cookie-authenticated state-changing endpoints (higher CSRF risk).\n\nDetection hints:\n- Inspect `app.config.update(...)` blocks and config classes.\n- Look for `set_cookie(..., secure=..., httponly=..., samesite=...)` usage on non-session cookies too.\n\nFix:\n- Set these config values explicitly in production config.\n\nNotes:\n- SameSite is defense-in-depth; do not treat it as a full replacement for CSRF tokens.\n\n---\n\n### FLASK-SESS-002: Sessions must be bounded and resistant to fixation/replay\nSeverity: Medium\n\nRequired:\n- SHOULD set a bounded session lifetime appropriate to the app.\n- SHOULD set `session.permanent = True` only when you intend persistent sessions, and set `PERMANENT_SESSION_LIFETIME` to a justified value.\n- SHOULD clear the session on login and privilege changes to reduce session fixation risk.\n- MUST NOT store sensitive secrets in the default Flask session cookie. The default session is signed, not encrypted.\n\nInsecure patterns:\n- Extremely long or unlimited lifetimes for privileged sessions.\n- No session clearing on login.\n- Storing secrets (passwords, access tokens, PII) directly in `session[...]` when using default cookie sessions.\n\nDetection hints:\n- Search for `PERMANENT_SESSION_LIFETIME`, `session.permanent`, `session[...] =`.\n- Identify whether server-side session storage is used; if not, assume default cookie sessions.\n\nFix:\n- Set appropriate lifetimes.\n- Clear/rotate session on login.\n- Store sensitive data server-side; store only identifiers in the session cookie.\n\n---\n\n### FLASK-CSRF-001: State-changing requests using cookie auth MUST be CSRF-protected\nSeverity: High\n\n- IMPORTANT NOTE: If cookies are not being used for auth (ie auth is via Authentication header or other passed token), then there is no CSRF risk.\n\nRequired:\n- MUST protect all state-changing endpoints (POST/PUT/PATCH/DELETE) that rely on cookies for authentication.\n- MAY use a well-tested CSRF library/integration (form framework or middleware) rather than rolling your own.\n- MAY use additional defenses (Origin/Referer checking, SameSite cookies, Fetch Metadata headers, custom headers for AJAX/API), but tokens remain the primary defense for cookie-authenticated apps.\nIf tokens are impractical, or for small applications:\n* MUST at a minimum require a custom header to be set and set the session cookie SESSION_COOKIE_SAMESITE=lax, as this is the strongest method besides requiring a form token, and may be much easier to implement.\n\nInsecure patterns:\n- Cookie-authenticated endpoints that change state with no CSRF protection.\n- Using GET for state-changing actions (amplifies CSRF risk).\n\nDetection hints:\n- Enumerate routes with methods other than GET and identify auth mechanism.\n- Look for CSRF integrations (e.g., Flask-WTF, global CSRF middleware). If absent, treat as suspicious.\n- Check JSON API endpoints too, not only HTML forms.\n\nFix:\n- Add CSRF protection to all state-changing requests.\n- If the app is a pure API and uses Authorization headers (bearer tokens) rather than cookies, document that choice and ensure cookies aren’t used for auth. If cookies are not used for auth, there is no CSRF risk.\n\nNotes:\n- XSS can defeat CSRF protections; CSRF defenses do not replace XSS prevention.\n\n---\n\n### FLASK-XSS-001: Prevent reflected/stored XSS in templates and HTML generation\nSeverity: High\n\nRequired:\n- MUST rely on Jinja auto-escaping for HTML templates.\n- MUST NOT mark untrusted content as safe:\n  - Avoid `Markup(...)` on user data.\n  - Avoid Jinja `|safe` on user-controlled content.\n- MUST quote HTML attributes containing Jinja expressions (`value=\"{{ x }}\"` not `value={{ x }}`).\n- MUST NOT serve uploaded HTML as active HTML; serve as download (`Content-Disposition: attachment`) or transform to a safe format. Note: This is only relevant if it is possible to upload document content such as html, js, css, etc. If it purely is image files, there is no concern.\n- SHOULD deploy a Content Security Policy (CSP) to mitigate XSS classes (including `javascript:` in `href`).\n\nInsecure patterns:\n- `Markup(request.args.get(...))`\n- Template filters: `{{ user_html|safe }}`\n- Unquoted attributes in templates\n- Serving user-uploaded content directly with `text/html` or inline rendering\n\nDetection hints:\n- Search for `Markup(` and investigate origin of the data.\n- Search template files for `|safe`, `|tojson` misuse, and unquoted attributes.\n- Review file-serving routes that might return user uploads without `as_attachment=True`. Note: This is only relevant if it is possible to upload document content such as html, js, css, etc. If it purely is image files, there is no concern.\n\nFix:\n- Remove unsafe marking; sanitize only when strictly necessary using a trusted HTML sanitizer.\n- Always quote attributes.\n- Add CSP and reduce inline scripts.\n\n---\n\n### FLASK-SSTI-001: Never render untrusted templates (Server-Side Template Injection)\nSeverity: Critical\n\nRequired:\n- MUST NOT render templates that contain user-controlled template syntax.\n- MUST treat `render_template_string` and `Environment.from_string(...).render(...)` as dangerous if the template string is influenced by untrusted input.\n- MUST NOT use use `.format()` on user controlled strings\n- If untrusted templates are absolutely required, treat it as a special high-risk design:\n  - MUST use a sandboxed templating approach and restrict capabilities.\n  - MUST keep Jinja updated and assume sandbox escapes are possible; isolate further.\n\nInsecure patterns:\n- `render_template_string(request.args[\"tmpl\"], ...)`\n- Storing user templates in DB and rendering them with the normal Jinja environment.\n- `request.args[\"tmpl\"].format(...)`\n\nDetection hints:\n- Grep for `render_template_string`, `from_string`, `.render(` with dynamic strings.\n- Trace the origin of the template string (DB, request, uploads, admin panels).\n\nFix:\n- Replace with safe templating alternatives that do not evaluate code (e.g., string.Template, str.replace).\n- If templates must be user-defined, use a sandbox plus strict allowlists and heavy isolation.\n\n---\n\n### FLASK-HEADERS-001: Set essential security headers (in app or at the edge)\nSeverity: Medium\n\nRequired (typical web app):\n- SHOULD set:\n  - CSP (`Content-Security-Policy`)\n  - `X-Content-Type-Options: nosniff`\n  - Clickjacking protection (`X-Frame-Options: SAMEORIGIN` and/or CSP `frame-ancestors`) (there may be cases where the user wants to iframe their site elsewhere. If that is the case, work with them to safely allow it)\n- SHOULD consider additional hardening headers depending on app (Referrer-Policy, Permissions-Policy).\n- MUST ensure cookies are set with secure attributes (see FLASK-SESS-001).\n\nNOTE: Security headers may be set via a proxy or other cloud provider. Check to see if there is evidence of that.\n\nInsecure patterns:\n- No security headers anywhere (app or edge).\n- CSP missing on apps that display untrusted content.\n\nDetection hints:\n- Search for `after_request` hooks, Flask-Talisman usage, reverse proxy config.\n- If not visible in app code, flag as “verify at edge”.\n\nFix:\n- Set headers centrally (middleware / after_request) or via reverse proxy/CDN.\n- Keep CSP realistic and compatible; avoid `unsafe-inline` where possible.\n\n---\n\n### FLASK-LIMITS-001: Request size and form parsing limits MUST be set appropriately\nSeverity: Low (Medium if file uploads / large bodies are possible)\n\nRequired:\n- SHOULD set and justify:\n  - `MAX_CONTENT_LENGTH` (global maximum request bytes)\n  - `MAX_FORM_MEMORY_SIZE` (max per non-file form field in multipart)\n  - `MAX_FORM_PARTS` (max number of multipart fields)\n- MUST enforce additional limits at the reverse proxy / WSGI / platform level where possible.\n\nInsecure patterns:\n- Unlimited request body sizes when handling uploads or user content.\n- Accepting arbitrarily large multipart forms or many fields.\n\nDetection hints:\n- Inspect Flask config for these keys.\n- Inspect upload routes and APIs that accept large JSON.\n\nFix:\n- Set conservative defaults, override per-route only when needed.\n- Ensure large uploads use dedicated upload mechanisms.\n\n---\n\n### FLASK-HOST-001: Host header must be validated in production\nSeverity: Low (depends on app’s use of external URLs)\n\nRequired:\n- MUST set `TRUSTED_HOSTS` in production to restrict accepted Host values.\n- MUST NOT rely on `SERVER_NAME` as a host restriction mechanism.\n\nInsecure patterns:\n- `TRUSTED_HOSTS` unset in production.\n- Code that generates external URLs for emails/password resets without host validation.\n\nDetection hints:\n- Find `TRUSTED_HOSTS` config usage.\n- Find `url_for(..., _external=True)` and check how host is determined.\n\nFix:\n- Set `TRUSTED_HOSTS` to your expected domains (and required subdomains).\n- Ensure external URL generation uses trusted host/scheme.\n\n---\n\n### FLASK-PROXY-001: Reverse proxy trust must be configured correctly\nSeverity: Medium (High if relying on IPs for auth)\n\nRequired:\n- If behind a reverse proxy, MUST configure Flask/Werkzeug to trust forwarded headers only from the intended proxy.\n- MUST NOT blindly trust `X-Forwarded-*` headers from the open internet.\n\nInsecure patterns:\n- `ProxyFix` applied with overly broad trust settings, or applied without understanding how many proxies are in front.\n- Relying on forwarded headers for scheme/host without validation.\n\nDetection hints:\n- Search for `ProxyFix`.\n- Search for usage of `request.remote_addr`, `request.scheme`, `request.host` in security-sensitive logic.\n\nFix:\n- Configure `ProxyFix` (or platform-specific settings) with correct hop counts.\n- Keep `TRUSTED_HOSTS` in place even behind proxies.\n\n---\n\n### FLASK-PATH-001: Prevent path traversal and unsafe file serving\nSeverity: High\n\nRequired:\n- MUST NOT pass user-controlled file paths to `send_file` or to direct file I/O.\n- MUST use safe file serving patterns:\n  - `send_from_directory` for user-specified paths under a trusted base directory\n  - `safe_join` for joining a trusted base directory with untrusted path components\n  - `secure_filename` for uploaded filenames (and still generate your own unique storage name)\n- MUST ensure user uploads are not served as executable/active content (especially HTML).\n- SHOULD in general use `safe_join` over `os.path.join` for almost any filesystem path computations.\n\nInsecure patterns:\n- `send_file(request.args[\"path\"])`\n- `open(os.path.join(base_dir, user_path))` where `user_path` is untrusted\n- Serving uploads from within a static web root without restrictions\n\nDetection hints:\n- Search for `send_file(`, `open(`, `os.path.join(`, `pathlib.Path(...)/...` in file routes.\n- Identify where filenames come from (request args, DB, headers).\n\nFix:\n- Serve only from a non-user-controlled directory base.\n- Store uploads outside static roots; serve through controlled routes.\n- Always validate and normalize file identifiers.\n\nNote: `safe_join` is imported from `werkzeug.security`\n\n---\n\n### FLASK-UPLOAD-001: File uploads must be validated, stored safely, and served safely\nSeverity: High\n\nRequired:\n- MUST enforce upload size limits (app + edge).\n- MUST validate file type using allowlists and content checks (not only extension).\n- MUST store uploads outside executable/static roots when possible.\n- SHOULD generate server-side filenames (random IDs) and avoid trusting original names.\n- MUST serve potentially active formats safely (download attachment) unless explicitly intended.\n\nInsecure patterns:\n- Accepting arbitrary file types and serving them back inline.\n- Using user-supplied filename as storage path.\n- Missing size/type validation.\n\nDetection hints:\n- Look for `request.files[...]` handlers.\n- Check for `secure_filename` usage (and whether it’s combined with uniqueness).\n- Check where files are stored and how they are served.\n\nFix:\n- Implement allowlist validation + safe storage + safe serving.\n- Add scanning / quarantine if applicable.\n\n---\n\n### FLASK-INJECT-001: Prevent SQL injection (use parameterized queries / ORM)\nSeverity: High\n\nRequired:\n- MUST use parameterized queries or an ORM that parameterizes under the hood.\n- MUST NOT build SQL by string concatenation / f-strings with untrusted input.\n\nInsecure patterns:\n- `f\"SELECT ... WHERE id={request.args['id']}\"`\n- `\"... WHERE name = '%s'\" % user_input`\n\nDetection hints:\n- Grep for `SELECT`, `INSERT`, `UPDATE`, `DELETE` strings in Python code.\n- Track untrusted data into DB execute calls.\n\nFix:\n- Replace with parameterized queries or ORM query APIs.\n- Validate types (e.g., int IDs) before querying.\n\n---\n\n### FLASK-INJECT-002: Prevent OS command injection\nSeverity: Critical to High (depends on exposure)\n\nRequired:\n- MUST avoid executing shell commands with untrusted input.\n- If subprocess is necessary:\n  - MUST pass args as a list (not a string)\n  - MUST NOT use `shell=True` with attacker-influenced strings\n  - SHOULD use strict allowlists for any variable component\n- If possible, use pure python or a python library rather than using a subprocess or system command\n- Do not assume that arguments to commands will be inherently safe even in `shell=False`. Commands may incorrectly process these arguments as command line flags or other trusted values.\n\nInsecure patterns:\n- `os.system(user_input)`\n- `subprocess.run(f\"cmd {user}\", shell=True)`\n- Passing user strings into `bash -c`, `sh -c`, PowerShell, etc.\n\nDetection hints:\n- Search for `os.system`, `subprocess`, `Popen`, `shell=True`.\n- Trace data from request/DB into these calls.\n\nFix:\n- Use library APIs instead of shell commands.\n- If unavoidable, hard-code the command and allowlist validated parameters. If supported by the subcommand, try to keep user values after `--` to prevent them being processed as command line flags.\n\n---\n\n### FLASK-SSRF-001: Prevent server-side request forgery (SSRF) in outbound HTTP\nSeverity: Medium\n\n- Note: For small stand alone projects this is less important. It is most important when deploying into an LAN or with other services listening on the same server.\n\nRequired:\n- MUST treat outbound requests to user-provided URLs as high risk.\n- SHOULD validate and restrict destinations (allowlist hosts/domains) for any user-influenced URL fetch.\n- SHOULD block access to:\n  - localhost / private IP ranges / link-local addresses\n  - cloud metadata endpoints\n- MUST NOT allow non http/https protocols (ie file: etc)\n- SHOULD set timeouts and restrict redirects.\n\n\n\nInsecure patterns:\n- `requests.get(request.args[\"url\"])`\n- Webhooks/preview/fetch endpoints that accept arbitrary URLs.\n\nDetection hints:\n- Search for `requests.get/post`, `httpx`, `urllib`, `aiohttp` usage with untrusted URL sources.\n- Identify URL fetch features (preview, import, webhook tester).\n\nFix:\n- Ensure URLs are http or https (disallow file: or other protocols)\n- Enforce allowlists and network egress controls.\n- Add strict parsing and IP resolution checks; set timeouts; disable redirects if not needed.\n\n---\n\n### FLASK-REDIRECT-001: Prevent open redirects\nSeverity: Low\n\nRequired:\n- MUST validate redirect targets derived from untrusted input (e.g., `next`, `redirect`, `return_to`).\n- SHOULD use allowlists of internal paths or known domains.\n- SHOULD prefer redirecting only to same-site relative paths.\n\nInsecure patterns:\n- `redirect(request.args.get(\"next\"))` with no validation.\n\nDetection hints:\n- Search for `redirect(` and examine where `location` comes from.\n\nFix:\n- Only allow relative paths or allowlisted domains.\n- Fall back to a safe default if validation fails.\n\n---\n\n### FLASK-HTTP-001: Use HTTP methods safely; do not change state via GET; avoid secrets in URLs\nSeverity: Medium\n\nRequired:\n- MUST NOT perform state-changing actions over GET.\n- MUST NOT put secrets in URLs (query strings are commonly logged and leaked via referrers).\n- SHOULD require POST/PUT/PATCH/DELETE for state change and apply CSRF protections when cookie-authenticated.\n\nInsecure patterns:\n- `/delete?id=...` implemented as GET\n- Password reset tokens or API keys in query params\n\nDetection hints:\n- Enumerate GET routes and inspect whether they mutate state.\n- Look for URL parameters named `token`, `key`, `secret`, `password`, etc.\n\nFix:\n- Move state changes to non-GET methods.\n- Move sensitive values to secure channels (POST bodies, headers) and protect them.\n\n---\n\n### FLASK-CORS-001: CORS must be explicit and least-privilege\nSeverity: Medium (High if misconfigured with credentials)\n\nRequired:\n- If CORS is not needed, MUST keep it disabled.\n- If CORS is needed:\n  - MUST allowlist trusted origins (do not reflect arbitrary origins).\n  - MUST be careful with credentialed requests; do not combine broad origins with cookies.\n  - SHOULD restrict allowed methods and headers.\n\nInsecure patterns:\n- `Access-Control-Allow-Origin: *` paired with credentialed cookies or overly broad access.\n- Reflecting `Origin` without validation.\n- `flask_cors.CORS(app)` with permissive defaults.\n\nDetection hints:\n- Search for `flask_cors`, `CORS(`, `Access-Control-Allow-Origin`.\n- Check for `supports_credentials=True` and wildcard origins.\n\nFix:\n- Use a strict origin allowlist and minimal methods/headers.\n- Ensure cookie-authenticated endpoints are not exposed cross-origin unless necessary.\n\n---\n\n### FLASK-SUPPLY-001: Dependency and patch hygiene (focus on security-relevant deps)\nSeverity: Low\n\nRequired:\n- SHOULD pin and regularly update security-critical dependencies (Flask, Werkzeug, Jinja2, itsdangerous).\n- MUST respond to known security advisories promptly.\n\nAudit focus example:\n- If running on Windows and using file serving with untrusted paths, ensure Werkzeug’s `safe_join` behavior is not vulnerable to Windows device-name edge cases.\n\nDetection hints:\n- Check `requirements.txt`, lockfiles, and runtime environments.\n- Identify where security helpers are used (safe_join, send_from_directory).\n\nFix:\n- Upgrade to patched versions and add regression tests for the impacted behavior.\n\n--------------------------------------------------------------------\n\n## 5) Practical scanning heuristics (how to “hunt”)\n\nWhen actively scanning, use these high-signal patterns:\n\n- Dev server / debug:\n  - `app.run(`, `flask run`, `--debug`, `DEBUG=True`, `FLASK_DEBUG`\n- Secrets:\n  - `SECRET_KEY`, `secret_key`, `.env` committed, `print(config)`\n- Cookies / sessions:\n  - `SESSION_COOKIE_SECURE`, `SESSION_COOKIE_HTTPONLY`, `SESSION_COOKIE_SAMESITE`\n  - `session[...] =` with sensitive values\n- CSRF:\n  - POST/PUT/PATCH/DELETE handlers without CSRF checks in cookie-authenticated apps\n- XSS/SSTI:\n  - `Markup(`, `|safe`, unquoted attributes, `render_template_string`\n- Files:\n  - `send_file(` with user-controlled path; `open(` on user path; `os.path.join` with untrusted\n  - upload handlers using user filename for path\n- Injection:\n  - SQL strings + string formatting into `.execute(...)`\n  - `subprocess.*`, `shell=True`, `os.system`\n- SSRF:\n  - `requests.get/post` or `httpx` with URL from request/DB\n- Redirect:\n  - `redirect(request.args.get(\"next\"))`\n- CORS:\n  - `flask_cors.CORS` permissive configs; wildcard origins with credentials\n\nAlways try to confirm:\n- data origin (untrusted vs trusted)\n- sink type (template/SQL/subprocess/files/redirect/http)\n- protective controls present (validation, allowlists, middleware)\n\n--------------------------------------------------------------------\n\n## 6) Sources (accessed 2026-01-26)\n\nPrimary framework documentation:\n- Flask Docs: Deploying to Production — https://flask.palletsprojects.com/en/stable/deploying/\n- Flask Docs: Debugging Application Errors — https://flask.palletsprojects.com/en/stable/debugging/\n- Flask Docs: Configuration Handling — https://flask.palletsprojects.com/en/stable/config/\n- Flask Docs: Security Considerations — https://flask.palletsprojects.com/en/stable/web-security/\n- Flask Docs: Tell Flask it is Behind a Proxy — https://flask.palletsprojects.com/en/stable/deploying/proxy_fix/\n- Flask API Docs: Sessions — https://flask.palletsprojects.com/en/stable/api/#sessions\n\nWerkzeug documentation & advisories:\n- Werkzeug Docs: Utilities (send_file / send_from_directory / safe_join / secure_filename / password hashing) — https://werkzeug.palletsprojects.com/en/stable/utils/\n- GitHub Advisory: CVE-2025-66221 (Werkzeug safe_join Windows device names) — https://github.com/advisories/GHSA-hgf8-39gv-g3f2\n\nOWASP Cheat Sheet Series:\n- Session Management — https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html\n- CSRF Prevention — https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html\n- XSS Prevention — https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html\n- Input Validation — https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html\n- SQL Injection Prevention — https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html\n- Injection Prevention — https://cheatsheetseries.owasp.org/cheatsheets/Injection_Prevention_Cheat_Sheet.html\n- OS Command Injection Defense — https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html\n- SSRF Prevention — https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html\n- File Upload — https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html\n- Unvalidated Redirects — https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html\n- HTTP Headers — https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html\n\nTemplate safety references:\n- Jinja: Sandbox (rendering untrusted templates) — https://jinja.palletsprojects.com/en/stable/sandbox/\n- OWASP WSTG: Testing for Server-Side Template Injection — https://owasp.org/www-project-web-security-testing-guide/v41/4-Web_Application_Security_Testing/07-Input_Validation_Testing/18-Testing_for_Server_Side_Template_Injection\n- PortSwigger Web Security Academy: Server-side template injection — https://portswigger.net/web-security/server-side-template-injection\n\nHTTP semantics:\n- RFC 9110: HTTP Semantics (safe methods) — https://www.rfc-editor.org/rfc/rfc9110"
  },
  {
    "path": "skills/.curated/security-ownership-map/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/security-ownership-map/SKILL.md",
    "content": "---\nname: \"security-ownership-map\"\ndescription: \"Analyze git repositories to build a security ownership topology (people-to-file), compute bus factor and sensitive-code ownership, and export CSV/JSON for graph databases and visualization. Trigger only when the user explicitly wants a security-oriented ownership or bus-factor analysis grounded in git history (for example: orphaned sensitive code, security maintainers, CODEOWNERS reality checks for risk, sensitive hotspots, or ownership clusters). Do not trigger for general maintainer lists or non-security ownership questions.\"\n---\n\n# Security Ownership Map\n\n## Overview\n\nBuild a bipartite graph of people and files from git history, then compute ownership risk and export graph artifacts for Neo4j/Gephi. Also build a file co-change graph (Jaccard similarity on shared commits) to cluster files by how they move together while ignoring large, noisy commits.\n\n## Requirements\n\n- Python 3\n- `networkx` (required; community detection is enabled by default)\n\nInstall with:\n\n```bash\npip install networkx\n```\n\n## Workflow\n\n1. Scope the repo and time window (optional `--since/--until`).\n2. Decide sensitivity rules (use defaults or provide a CSV config).\n3. Build the ownership map with `scripts/run_ownership_map.py` (co-change graph is on by default; use `--cochange-max-files` to ignore supernode commits).\n4. Communities are computed by default; graphml output is optional (`--graphml`).\n5. Query the outputs with `scripts/query_ownership.py` for bounded JSON slices.\n6. Persist and visualize (see `references/neo4j-import.md`).\n\nBy default, the co-change graph ignores common “glue” files (lockfiles, `.github/*`, editor config) so clusters reflect actual code movement instead of shared infra edits. Override with `--cochange-exclude` or `--no-default-cochange-excludes`. Dependabot commits are excluded by default; override with `--no-default-author-excludes` or add patterns via `--author-exclude-regex`.\n\nIf you want to exclude Linux build glue like `Kbuild` from co-change clustering, pass:\n\n```bash\npython skills/skills/security-ownership-map/scripts/run_ownership_map.py \\\n  --repo /path/to/linux \\\n  --out ownership-map-out \\\n  --cochange-exclude \"**/Kbuild\"\n```\n\n## Quick start\n\nRun from the repo root:\n\n```bash\npython skills/skills/security-ownership-map/scripts/run_ownership_map.py \\\n  --repo . \\\n  --out ownership-map-out \\\n  --since \"12 months ago\" \\\n  --emit-commits\n```\n\nDefaults: author identity, author date, and merge commits excluded. Use `--identity committer`, `--date-field committer`, or `--include-merges` if needed.\n\nExample (override co-change excludes):\n\n```bash\npython skills/skills/security-ownership-map/scripts/run_ownership_map.py \\\n  --repo . \\\n  --out ownership-map-out \\\n  --cochange-exclude \"**/Cargo.lock\" \\\n  --cochange-exclude \"**/.github/**\" \\\n  --no-default-cochange-excludes\n```\n\nCommunities are computed by default. To disable:\n\n```bash\npython skills/skills/security-ownership-map/scripts/run_ownership_map.py \\\n  --repo . \\\n  --out ownership-map-out \\\n  --no-communities\n```\n\n## Sensitivity rules\n\nBy default, the script flags common auth/crypto/secret paths. Override by providing a CSV file:\n\n```\n# pattern,tag,weight\n**/auth/**,auth,1.0\n**/crypto/**,crypto,1.0\n**/*.pem,secrets,1.0\n```\n\nUse it with `--sensitive-config path/to/sensitive.csv`.\n\n## Output artifacts\n\n`ownership-map-out/` contains:\n\n- `people.csv` (nodes: people)\n- `files.csv` (nodes: files)\n- `edges.csv` (edges: touches)\n- `cochange_edges.csv` (file-to-file co-change edges with Jaccard weight; omitted with `--no-cochange`)\n- `summary.json` (security ownership findings)\n- `commits.jsonl` (optional, if `--emit-commits`)\n- `communities.json` (computed by default from co-change edges when available; includes `maintainers` per community; disable with `--no-communities`)\n- `cochange.graph.json` (NetworkX node-link JSON with `community_id` + `community_maintainers`; falls back to `ownership.graph.json` if no co-change edges)\n- `ownership.graphml` / `cochange.graphml` (optional, if `--graphml`)\n\n`people.csv` includes timezone detection based on author commit offsets: `primary_tz_offset`, `primary_tz_minutes`, and `timezone_offsets`.\n\n## LLM query helper\n\nUse `scripts/query_ownership.py` to return small, JSON-bounded slices without loading the full graph into context.\n\nExamples:\n\n```bash\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out people --limit 10\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out files --tag auth --bus-factor-max 1\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out person --person alice@corp --limit 10\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out file --file crypto/tls\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out cochange --file crypto/tls --limit 10\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out summary --section orphaned_sensitive_code\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out community --id 3\n```\n\nUse `--community-top-owners 5` (default) to control how many maintainers are stored per community.\n\n## Basic security queries\n\nRun these to answer common security ownership questions with bounded output:\n\n```bash\n# Orphaned sensitive code (stale + low bus factor)\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out summary --section orphaned_sensitive_code\n\n# Hidden owners for sensitive tags\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out summary --section hidden_owners\n\n# Sensitive hotspots with low bus factor\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out summary --section bus_factor_hotspots\n\n# Auth/crypto files with bus factor <= 1\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out files --tag auth --bus-factor-max 1\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out files --tag crypto --bus-factor-max 1\n\n# Who is touching sensitive code the most\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out people --sort sensitive_touches --limit 10\n\n# Co-change neighbors (cluster hints for ownership drift)\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out cochange --file path/to/file --min-jaccard 0.05 --limit 20\n\n# Community maintainers (for a cluster)\npython skills/skills/security-ownership-map/scripts/query_ownership.py --data-dir ownership-map-out community --id 3\n\n# Monthly maintainers for the community containing a file\npython skills/skills/security-ownership-map/scripts/community_maintainers.py \\\n  --data-dir ownership-map-out \\\n  --file network/card.c \\\n  --since 2025-01-01 \\\n  --top 5\n\n# Quarterly buckets instead of monthly\npython skills/skills/security-ownership-map/scripts/community_maintainers.py \\\n  --data-dir ownership-map-out \\\n  --file network/card.c \\\n  --since 2025-01-01 \\\n  --bucket quarter \\\n  --top 5\n```\n\nNotes:\n- Touches default to one authored commit (not per-file). Use `--touch-mode file` to count per-file touches.\n- Use `--window-days 90` or `--weight recency --half-life-days 180` to smooth churn.\n- Filter bots with `--ignore-author-regex '(bot|dependabot)'`.\n- Use `--min-share 0.1` to show stable maintainers only.\n- Use `--bucket quarter` for calendar quarter groupings.\n- Use `--identity committer` or `--date-field committer` to switch from author attribution.\n- Use `--include-merges` to include merge commits (excluded by default).\n\n### Summary format (default)\n\nUse this structure, add fields if needed:\n\n```json\n{\n  \"orphaned_sensitive_code\": [\n    {\n      \"path\": \"crypto/tls/handshake.rs\",\n      \"last_security_touch\": \"2023-03-12T18:10:04+00:00\",\n      \"bus_factor\": 1\n    }\n  ],\n  \"hidden_owners\": [\n    {\n      \"person\": \"alice@corp\",\n      \"controls\": \"63% of auth code\"\n    }\n  ]\n}\n```\n\n## Graph persistence\n\nUse `references/neo4j-import.md` when you need to load the CSVs into Neo4j. It includes constraints, import Cypher, and visualization tips.\n\n## Notes\n\n- `bus_factor_hotspots` in `summary.json` lists sensitive files with low bus factor; `orphaned_sensitive_code` is the stale subset.\n- If `git log` is too large, narrow with `--since` or `--until`.\n- Compare `summary.json` against CODEOWNERS to highlight ownership drift.\n"
  },
  {
    "path": "skills/.curated/security-ownership-map/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Security Ownership Map\"\n  short_description: \"Map maintainers, bus factor, and sensitive code ownership\"\n  default_prompt: \"Build a security ownership map for this repository and identify bus-factor risks in sensitive code.\"\n"
  },
  {
    "path": "skills/.curated/security-ownership-map/references/neo4j-import.md",
    "content": "# Neo4j Import Notes\n\nUse these steps when persisting the ownership graph to Neo4j.\n\n## Quick import (LOAD CSV)\n\n1. Copy `people.csv`, `files.csv`, and `edges.csv` into the Neo4j import directory.\n2. Run the following Cypher from Neo4j Browser or `cypher-shell`:\n\n```cypher\nCREATE CONSTRAINT person_id IF NOT EXISTS FOR (p:Person) REQUIRE p.id IS UNIQUE;\nCREATE CONSTRAINT file_id IF NOT EXISTS FOR (f:File) REQUIRE f.id IS UNIQUE;\n\nLOAD CSV WITH HEADERS FROM 'file:///people.csv' AS row\nMERGE (p:Person {id: row.person_id})\nSET p.name = row.name,\n    p.email = row.email,\n    p.first_seen = row.first_seen,\n    p.last_seen = row.last_seen,\n    p.commit_count = toInteger(row.commit_count),\n    p.touches = toInteger(row.touches),\n    p.sensitive_touches = toFloat(row.sensitive_touches),\n    p.primary_tz_offset = CASE row.primary_tz_offset WHEN '' THEN null ELSE row.primary_tz_offset END,\n    p.primary_tz_minutes = CASE row.primary_tz_minutes WHEN '' THEN null ELSE toInteger(row.primary_tz_minutes) END,\n    p.timezone_offsets = CASE row.timezone_offsets WHEN '' THEN null ELSE row.timezone_offsets END;\n\nLOAD CSV WITH HEADERS FROM 'file:///files.csv' AS row\nMERGE (f:File {id: row.file_id})\nSET f.path = row.path,\n    f.first_seen = row.first_seen,\n    f.last_seen = row.last_seen,\n    f.commit_count = toInteger(row.commit_count),\n    f.touches = toInteger(row.touches),\n    f.bus_factor = toInteger(row.bus_factor),\n    f.sensitivity_score = toFloat(row.sensitivity_score),\n    f.sensitivity_tags = row.sensitivity_tags;\n\nLOAD CSV WITH HEADERS FROM 'file:///edges.csv' AS row\nMATCH (p:Person {id: row.person_id})\nMATCH (f:File {id: row.file_id})\nMERGE (p)-[r:TOUCHES]->(f)\nSET r.touches = toInteger(row.touches),\n    r.recency_weight = toFloat(row.recency_weight),\n    r.first_seen = row.first_seen,\n    r.last_seen = row.last_seen,\n    r.sensitive_weight = toFloat(row.sensitive_weight);\n\nLOAD CSV WITH HEADERS FROM 'file:///cochange_edges.csv' AS row\nMATCH (f1:File {id: row.file_a})\nMATCH (f2:File {id: row.file_b})\nMERGE (f1)-[r:COCHANGES]->(f2)\nSET r.cochange_count = toInteger(row.cochange_count),\n    r.jaccard = toFloat(row.jaccard);\n```\n\n## Visualization tips\n\n- Use Neo4j Bloom or Browser with `MATCH (p:Person)-[r:TOUCHES]->(f:File) RETURN p,r,f`.\n- Filter by `f.sensitivity_score > 0` to highlight security-relevant clusters.\n- For Gephi, import `edges.csv` as edges and `files.csv` / `people.csv` as nodes.\n"
  },
  {
    "path": "skills/.curated/security-ownership-map/scripts/build_ownership_map.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Build a security ownership map from git history.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport csv\nimport datetime as dt\nimport fnmatch\nimport json\nimport math\nimport os\nimport re\nimport subprocess\nimport sys\nfrom collections import defaultdict\nfrom pathlib import Path\nfrom typing import Iterable\n\nDEFAULT_SENSITIVE_RULES: list[tuple[str, str, float]] = [\n    (\"**/auth/**\", \"auth\", 1.0),\n    (\"**/oauth/**\", \"auth\", 1.0),\n    (\"**/rbac/**\", \"auth\", 1.0),\n    (\"**/session/**\", \"auth\", 1.0),\n    (\"**/token/**\", \"auth\", 1.0),\n    (\"**/crypto/**\", \"crypto\", 1.0),\n    (\"**/tls/**\", \"crypto\", 1.0),\n    (\"**/ssl/**\", \"crypto\", 1.0),\n    (\"**/secrets/**\", \"secrets\", 1.0),\n    (\"**/keys/**\", \"secrets\", 1.0),\n    (\"**/*.pem\", \"secrets\", 1.0),\n    (\"**/*.key\", \"secrets\", 1.0),\n    (\"**/*.p12\", \"secrets\", 1.0),\n    (\"**/*.pfx\", \"secrets\", 1.0),\n    (\"**/iam/**\", \"auth\", 1.0),\n    (\"**/sso/**\", \"auth\", 1.0),\n]\n\nDEFAULT_AUTHOR_EXCLUDE_REGEXES = [\n    \"dependabot\",\n]\n\nDEFAULT_COCHANGE_EXCLUDES = [\n    \"**/Cargo.lock\",\n    \"**/Cargo.toml\",\n    \"**/package-lock.json\",\n    \"**/yarn.lock\",\n    \"**/pnpm-lock.yaml\",\n    \"**/go.sum\",\n    \"**/go.mod\",\n    \"**/Gemfile.lock\",\n    \"**/Pipfile.lock\",\n    \"**/poetry.lock\",\n    \"**/composer.lock\",\n    \"**/.github/**\",\n    \"**/.gitignore\",\n    \"**/.gitattributes\",\n    \"**/.gitmodules\",\n    \"**/.editorconfig\",\n    \"**/.vscode/**\",\n    \"**/.idea/**\",\n]\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Build ownership graphs and security ownership summaries from git history.\"\n    )\n    parser.add_argument(\"--repo\", default=\".\", help=\"Path to the git repo (default: .)\")\n    parser.add_argument(\n        \"--out\",\n        default=\"ownership-map-out\",\n        help=\"Output directory for graph artifacts\",\n    )\n    parser.add_argument(\"--since\", default=None, help=\"Limit git log to commits since date\")\n    parser.add_argument(\"--until\", default=None, help=\"Limit git log to commits until date\")\n    parser.add_argument(\n        \"--identity\",\n        choices=(\"author\", \"committer\"),\n        default=\"author\",\n        help=\"Identity to attribute touches to\",\n    )\n    parser.add_argument(\n        \"--date-field\",\n        choices=(\"author\", \"committer\"),\n        default=\"author\",\n        help=\"Date field to use for recency and bucketing\",\n    )\n    parser.add_argument(\n        \"--include-merges\",\n        action=\"store_true\",\n        help=\"Include merge commits (excluded by default)\",\n    )\n    parser.add_argument(\n        \"--half-life-days\",\n        type=float,\n        default=180.0,\n        help=\"Half life for recency weighting\",\n    )\n    parser.add_argument(\n        \"--sensitive-config\",\n        default=None,\n        help=\"CSV file with pattern,tag,weight for sensitive paths\",\n    )\n    parser.add_argument(\n        \"--owner-threshold\",\n        type=float,\n        default=0.5,\n        help=\"Share threshold for hidden owner detection\",\n    )\n    parser.add_argument(\n        \"--bus-factor-threshold\",\n        type=int,\n        default=1,\n        help=\"Bus factor threshold for hotspots\",\n    )\n    parser.add_argument(\n        \"--stale-days\",\n        type=int,\n        default=365,\n        help=\"Days since last touch to consider stale\",\n    )\n    parser.add_argument(\n        \"--min-touches\",\n        type=int,\n        default=1,\n        help=\"Minimum touches to keep an edge\",\n    )\n    parser.add_argument(\n        \"--emit-commits\",\n        action=\"store_true\",\n        help=\"Write commit list to commits.jsonl\",\n    )\n    parser.add_argument(\n        \"--author-exclude-regex\",\n        action=\"append\",\n        default=[],\n        help=\"Regex for author name/email to exclude (repeatable)\",\n    )\n    parser.add_argument(\n        \"--no-default-author-excludes\",\n        action=\"store_true\",\n        help=\"Disable default author excludes (dependabot)\",\n    )\n    parser.add_argument(\n        \"--no-cochange\",\n        action=\"store_true\",\n        help=\"Disable co-change graph output\",\n    )\n    parser.add_argument(\n        \"--cochange-max-files\",\n        type=int,\n        default=50,\n        help=\"Ignore commits touching more than this many files for co-change graph\",\n    )\n    parser.add_argument(\n        \"--cochange-min-count\",\n        type=int,\n        default=2,\n        help=\"Minimum co-change count to keep file-file edge\",\n    )\n    parser.add_argument(\n        \"--cochange-min-jaccard\",\n        type=float,\n        default=0.05,\n        help=\"Minimum Jaccard similarity to keep file-file edge\",\n    )\n    parser.add_argument(\n        \"--cochange-exclude\",\n        action=\"append\",\n        default=[],\n        help=\"Glob to exclude from co-change graph (repeatable)\",\n    )\n    parser.add_argument(\n        \"--no-default-cochange-excludes\",\n        action=\"store_true\",\n        help=\"Disable default co-change excludes (lockfiles, .github, editor config)\",\n    )\n    parser.add_argument(\n        \"--no-communities\",\n        dest=\"communities\",\n        action=\"store_false\",\n        help=\"Disable community detection (enabled by default, requires networkx)\",\n    )\n    parser.add_argument(\n        \"--graphml\",\n        action=\"store_true\",\n        help=\"Emit ownership.graphml (requires networkx)\",\n    )\n    parser.add_argument(\n        \"--max-community-files\",\n        type=int,\n        default=50,\n        help=\"Max files listed per community\",\n    )\n    parser.add_argument(\n        \"--community-top-owners\",\n        type=int,\n        default=5,\n        help=\"Top maintainers saved per community\",\n    )\n    parser.set_defaults(communities=True)\n    return parser.parse_args()\n\n\ndef load_sensitive_rules(path: str | None) -> list[tuple[str, str, float]]:\n    if not path:\n        return list(DEFAULT_SENSITIVE_RULES)\n    rules: list[tuple[str, str, float]] = []\n    with open(path, \"r\", encoding=\"utf-8\") as handle:\n        for raw in handle:\n            line = raw.strip()\n            if not line or line.startswith(\"#\"):\n                continue\n            parts = [part.strip() for part in line.split(\",\")]\n            if not parts:\n                continue\n            pattern = parts[0]\n            tag = parts[1] if len(parts) > 1 and parts[1] else \"sensitive\"\n            weight = float(parts[2]) if len(parts) > 2 and parts[2] else 1.0\n            rules.append((pattern, tag, weight))\n    return rules\n\n\ndef parse_date(value: str) -> dt.datetime:\n    parsed = dt.datetime.fromisoformat(value)\n    if parsed.tzinfo is None:\n        parsed = parsed.replace(tzinfo=dt.timezone.utc)\n    return parsed\n\n\ndef offset_minutes(timestamp: dt.datetime) -> int | None:\n    offset = timestamp.utcoffset()\n    if offset is None:\n        return None\n    return int(offset.total_seconds() / 60)\n\n\ndef format_offset(minutes: int) -> str:\n    sign = \"+\" if minutes >= 0 else \"-\"\n    minutes = abs(minutes)\n    return f\"{sign}{minutes // 60:02d}:{minutes % 60:02d}\"\n\n\ndef recency_weighted(now: dt.datetime, when: dt.datetime, half_life_days: float) -> float:\n    if half_life_days <= 0:\n        return 1.0\n    age_days = max(0.0, (now - when).total_seconds() / 86400.0)\n    return math.exp(-math.log(2) * age_days / half_life_days)\n\n\ndef match_sensitive(path: str, rules: Iterable[tuple[str, str, float]]) -> dict[str, float]:\n    tags: dict[str, float] = defaultdict(float)\n    posix = path.replace(\"\\\\\", \"/\")\n    for pattern, tag, weight in rules:\n        patterns = [pattern]\n        if pattern.startswith(\"**/\"):\n            patterns.append(pattern[3:])\n        for candidate in patterns:\n            if fnmatch.fnmatchcase(posix, candidate):\n                tags[tag] += weight\n                break\n    return tags\n\n\ndef matches_glob(path: str, pattern: str) -> bool:\n    posix = path.replace(\"\\\\\", \"/\")\n    patterns = [pattern]\n    if pattern.startswith(\"**/\"):\n        patterns.append(pattern[3:])\n    return any(fnmatch.fnmatchcase(posix, candidate) for candidate in patterns)\n\n\ndef is_excluded(path: str, patterns: Iterable[str]) -> bool:\n    return any(matches_glob(path, pattern) for pattern in patterns)\n\n\ndef author_excluded(name: str, email: str, patterns: Iterable[re.Pattern[str]]) -> bool:\n    if not patterns:\n        return False\n    haystack = f\"{name} {email}\".strip()\n    return any(pattern.search(haystack) for pattern in patterns)\n\n\ndef compute_community_owners(\n    community_files: Iterable[str],\n    people: dict[str, dict[str, object]],\n    file_people_touches: dict[str, dict[str, int]],\n    file_people_recency: dict[str, dict[str, float]],\n    file_people_sensitive: dict[str, dict[str, float]],\n    top_n: int,\n) -> dict[str, object]:\n    touches_by_person: dict[str, int] = defaultdict(int)\n    recency_by_person: dict[str, float] = defaultdict(float)\n    sensitive_by_person: dict[str, float] = defaultdict(float)\n\n    for path in community_files:\n        for person, touches in file_people_touches.get(path, {}).items():\n            touches_by_person[person] += touches\n        for person, recency in file_people_recency.get(path, {}).items():\n            recency_by_person[person] += recency\n        for person, weight in file_people_sensitive.get(path, {}).items():\n            sensitive_by_person[person] += weight\n\n    total_touches = sum(touches_by_person.values())\n    total_recency = sum(recency_by_person.values())\n    total_sensitive = sum(sensitive_by_person.values())\n\n    ranked = sorted(touches_by_person.items(), key=lambda item: item[1], reverse=True)\n    owners = []\n    for person_id, touches in ranked[:top_n]:\n        recency = recency_by_person.get(person_id, 0.0)\n        sensitive = sensitive_by_person.get(person_id, 0.0)\n        owners.append(\n            {\n                \"person_id\": person_id,\n                \"name\": people.get(person_id, {}).get(\"name\", person_id),\n                \"touches\": touches,\n                \"touch_share\": round(touches / total_touches, 4) if total_touches else 0.0,\n                \"recency_share\": round(recency / total_recency, 4) if total_recency else 0.0,\n                \"sensitive_share\": round(sensitive / total_sensitive, 4)\n                if total_sensitive\n                else 0.0,\n                \"primary_tz_offset\": people.get(person_id, {}).get(\"primary_tz_offset\", \"\"),\n            }\n        )\n\n    return {\n        \"bus_factor\": len(touches_by_person),\n        \"owner_count\": len(touches_by_person),\n        \"totals\": {\n            \"touches\": total_touches,\n            \"recency_weight\": round(total_recency, 6),\n            \"sensitive_weight\": round(total_sensitive, 2),\n        },\n        \"top_maintainers\": owners,\n    }\n\n\ndef run_git_log(\n    repo: str, since: str | None, until: str | None, include_merges: bool\n) -> Iterable[list[str]]:\n    cmd = [\n        \"git\",\n        \"-C\",\n        repo,\n        \"log\",\n        \"--name-only\",\n        \"--no-renames\",\n        \"--date=iso-strict\",\n        \"--format=---%n%H%n%P%n%an%n%ae%n%ad%n%cn%n%ce%n%cd\",\n    ]\n    if not include_merges:\n        cmd.append(\"--no-merges\")\n    if since:\n        cmd.extend([\"--since\", since])\n    if until:\n        cmd.extend([\"--until\", until])\n\n    proc = subprocess.Popen(\n        cmd,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        text=True,\n    )\n    assert proc.stdout is not None\n\n    batch: list[str] = []\n    for line in proc.stdout:\n        batch.append(line.rstrip(\"\\n\"))\n        if line.rstrip(\"\\n\") == \"---\" and len(batch) > 1:\n            yield batch[:-1]\n            batch = [\"---\"]\n\n    if batch:\n        yield batch\n\n    stderr = proc.stderr.read() if proc.stderr else \"\"\n    exit_code = proc.wait()\n    if exit_code != 0:\n        raise RuntimeError(stderr.strip() or \"git log failed\")\n\n\ndef iter_commits(lines: Iterable[list[str]]) -> Iterable[tuple[dict[str, object], list[str]]]:\n    for chunk in lines:\n        if not chunk or chunk[0] != \"---\":\n            continue\n        header = chunk[1:9]\n        if len(header) < 8:\n            continue\n        parents = [entry for entry in header[1].split(\" \") if entry]\n        commit = {\n            \"hash\": header[0],\n            \"parents\": parents,\n            \"is_merge\": len(parents) > 1,\n            \"author_name\": header[2],\n            \"author_email\": header[3],\n            \"author_date\": header[4],\n            \"committer_name\": header[5],\n            \"committer_email\": header[6],\n            \"committer_date\": header[7],\n        }\n        files = [line for line in chunk[9:] if line.strip()]\n        yield commit, files\n\n\ndef ensure_out_dir(path: str) -> Path:\n    out_dir = Path(path)\n    out_dir.mkdir(parents=True, exist_ok=True)\n    return out_dir\n\n\ndef write_csv(path: Path, header: list[str], rows: Iterable[list[str]]) -> None:\n    with path.open(\"w\", encoding=\"utf-8\", newline=\"\") as handle:\n        writer = csv.writer(handle)\n        writer.writerow(header)\n        for row in rows:\n            writer.writerow(row)\n\n\ndef build_ownership_map(args: argparse.Namespace) -> Path:\n    now = dt.datetime.now(dt.timezone.utc)\n    rules = load_sensitive_rules(args.sensitive_config)\n    out_dir = ensure_out_dir(args.out)\n\n    people: dict[str, dict[str, object]] = {}\n    files: dict[str, dict[str, object]] = {}\n    edges: dict[tuple[str, str], dict[str, object]] = {}\n    file_people_touches: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))\n    file_people_recency: dict[str, dict[str, float]] = defaultdict(lambda: defaultdict(float))\n    file_people_sensitive: dict[str, dict[str, float]] = defaultdict(lambda: defaultdict(float))\n    tag_totals: dict[str, float] = defaultdict(float)\n    tag_person_totals: dict[str, dict[str, float]] = defaultdict(lambda: defaultdict(float))\n    person_timezone_counts: dict[str, dict[int, int]] = defaultdict(lambda: defaultdict(int))\n    cochange_counts: dict[tuple[str, str], int] = defaultdict(int)\n    cochange_file_commits: dict[str, int] = defaultdict(int)\n    cochange_commits_used = 0\n    cochange_commits_skipped = 0\n    cochange_commits_filtered = 0\n    cochange_files_excluded = 0\n\n    commits_path = out_dir / \"commits.jsonl\"\n    commit_handle = None\n    if args.emit_commits:\n        commit_handle = commits_path.open(\"w\", encoding=\"utf-8\")\n\n    total_commits_seen = 0\n    total_commits_included = 0\n    commits_excluded_identities = 0\n    commits_excluded_merges = 0\n    total_edges = 0\n\n    author_exclude_regexes = []\n    if not args.no_default_author_excludes:\n        author_exclude_regexes.extend(DEFAULT_AUTHOR_EXCLUDE_REGEXES)\n    author_exclude_regexes.extend(args.author_exclude_regex)\n    author_exclude_patterns = [\n        re.compile(pattern, re.IGNORECASE) for pattern in author_exclude_regexes\n    ]\n\n    cochange_excludes = []\n    if not args.no_default_cochange_excludes:\n        cochange_excludes.extend(DEFAULT_COCHANGE_EXCLUDES)\n    cochange_excludes.extend(args.cochange_exclude)\n\n    log_lines = run_git_log(args.repo, args.since, args.until, args.include_merges)\n    for commit, touched_files in iter_commits(log_lines):\n        total_commits_seen += 1\n\n        if commit.get(\"is_merge\") and not args.include_merges:\n            commits_excluded_merges += 1\n            continue\n\n        identity_name = commit.get(f\"{args.identity}_name\", \"\")\n        identity_email = commit.get(f\"{args.identity}_email\", \"\")\n        if author_excluded(\n            identity_name,\n            identity_email,\n            author_exclude_patterns,\n        ):\n            commits_excluded_identities += 1\n            continue\n\n        if not touched_files:\n            continue\n\n        total_commits_included += 1\n        if commit_handle:\n            commit_handle.write(json.dumps({**commit, \"files\": touched_files}) + \"\\n\")\n\n        identity_name = commit.get(f\"{args.identity}_name\", \"\")\n        identity_email = commit.get(f\"{args.identity}_email\", \"\") or identity_name\n        commit_date = parse_date(commit.get(f\"{args.date_field}_date\", \"\"))\n        recency = recency_weighted(now, commit_date, args.half_life_days)\n        tz_minutes = offset_minutes(commit_date)\n        if tz_minutes is not None:\n            person_timezone_counts[identity_email][tz_minutes] += 1\n        unique_files = sorted(set(touched_files))\n        if not args.no_cochange and len(unique_files) > 1:\n            if len(unique_files) > args.cochange_max_files:\n                cochange_commits_skipped += 1\n            else:\n                filtered_files = [\n                    path for path in unique_files if not is_excluded(path, cochange_excludes)\n                ]\n                excluded = len(unique_files) - len(filtered_files)\n                if excluded:\n                    cochange_files_excluded += excluded\n                if len(filtered_files) < 2:\n                    cochange_commits_filtered += 1\n                if filtered_files:\n                    for path in filtered_files:\n                        cochange_file_commits[path] += 1\n                if len(filtered_files) >= 2:\n                    cochange_commits_used += 1\n                    for idx, path in enumerate(filtered_files):\n                        for other in filtered_files[idx + 1 :]:\n                            cochange_counts[(path, other)] += 1\n\n        person = people.setdefault(\n            identity_email,\n            {\n                \"name\": identity_name,\n                \"email\": identity_email,\n                \"first_seen\": commit_date,\n                \"last_seen\": commit_date,\n                \"commit_count\": 0,\n                \"touches\": 0,\n                \"sensitive_touches\": 0.0,\n            },\n        )\n        person[\"commit_count\"] = int(person[\"commit_count\"]) + 1\n        person[\"first_seen\"] = min(person[\"first_seen\"], commit_date)\n        person[\"last_seen\"] = max(person[\"last_seen\"], commit_date)\n\n        for path in touched_files:\n            file_entry = files.setdefault(\n                path,\n                {\n                    \"path\": path,\n                    \"first_seen\": commit_date,\n                    \"last_seen\": commit_date,\n                    \"commit_count\": 0,\n                    \"touches\": 0,\n                    \"authors\": set(),\n                    \"sensitive_tags\": {},\n                },\n            )\n            file_entry[\"commit_count\"] = int(file_entry[\"commit_count\"]) + 1\n            file_entry[\"first_seen\"] = min(file_entry[\"first_seen\"], commit_date)\n            file_entry[\"last_seen\"] = max(file_entry[\"last_seen\"], commit_date)\n            file_entry[\"touches\"] = int(file_entry[\"touches\"]) + 1\n            file_entry[\"authors\"].add(identity_email)\n\n            edge = edges.setdefault(\n                (identity_email, path),\n                {\n                    \"touches\": 0,\n                    \"first_seen\": commit_date,\n                    \"last_seen\": commit_date,\n                    \"recency_weight\": 0.0,\n                    \"sensitive_weight\": 0.0,\n                },\n            )\n            edge[\"touches\"] = int(edge[\"touches\"]) + 1\n            edge[\"first_seen\"] = min(edge[\"first_seen\"], commit_date)\n            edge[\"last_seen\"] = max(edge[\"last_seen\"], commit_date)\n            edge[\"recency_weight\"] = float(edge[\"recency_weight\"]) + recency\n\n            tags = match_sensitive(path, rules)\n            if tags:\n                file_entry[\"sensitive_tags\"] = tags\n                sensitive_weight = sum(tags.values())\n                edge[\"sensitive_weight\"] = float(edge[\"sensitive_weight\"]) + sensitive_weight\n                person[\"sensitive_touches\"] = float(person[\"sensitive_touches\"]) + sensitive_weight\n                file_people_sensitive[path][identity_email] += sensitive_weight\n                for tag, weight in tags.items():\n                    tag_totals[tag] += weight\n                    tag_person_totals[tag][identity_email] += weight\n\n            person[\"touches\"] = int(person[\"touches\"]) + 1\n            file_people_touches[path][identity_email] += 1\n            file_people_recency[path][identity_email] += recency\n            total_edges += 1\n\n    if commit_handle:\n        commit_handle.close()\n\n    people_rows = []\n    for email, person in sorted(people.items()):\n        tz_counts = person_timezone_counts.get(email, {})\n        primary_tz_offset = \"\"\n        primary_tz_minutes = \"\"\n        timezone_offsets = \"\"\n        if tz_counts:\n            primary_tz_minutes_value = max(tz_counts.items(), key=lambda item: (item[1], item[0]))[\n                0\n            ]\n            primary_tz_offset = format_offset(primary_tz_minutes_value)\n            primary_tz_minutes = str(primary_tz_minutes_value)\n            timezone_offsets = \";\".join(\n                f\"{format_offset(minutes)}:{count}\"\n                for minutes, count in sorted(tz_counts.items(), key=lambda item: item[0])\n            )\n            person[\"primary_tz_offset\"] = primary_tz_offset\n        people_rows.append(\n            [\n                email,\n                str(person[\"name\"]),\n                email,\n                person[\"first_seen\"].isoformat(),\n                person[\"last_seen\"].isoformat(),\n                str(person[\"commit_count\"]),\n                str(person[\"touches\"]),\n                f\"{person['sensitive_touches']:.2f}\",\n                primary_tz_offset,\n                primary_tz_minutes,\n                timezone_offsets,\n            ]\n        )\n\n    file_rows = []\n    for path, file_entry in sorted(files.items()):\n        authors = file_entry[\"authors\"]\n        bus_factor = len(authors)\n        tags = file_entry[\"sensitive_tags\"]\n        tag_list = \";\".join(sorted(tags.keys()))\n        sensitivity_score = sum(tags.values()) if tags else 0.0\n        file_rows.append(\n            [\n                path,\n                path,\n                file_entry[\"first_seen\"].isoformat(),\n                file_entry[\"last_seen\"].isoformat(),\n                str(file_entry[\"commit_count\"]),\n                str(file_entry[\"touches\"]),\n                str(bus_factor),\n                f\"{sensitivity_score:.2f}\",\n                tag_list,\n            ]\n        )\n\n    edge_rows = []\n    for (email, path), edge in edges.items():\n        if int(edge[\"touches\"]) < args.min_touches:\n            continue\n        edge_rows.append(\n            [\n                email,\n                path,\n                str(edge[\"touches\"]),\n                f\"{edge['recency_weight']:.6f}\",\n                edge[\"first_seen\"].isoformat(),\n                edge[\"last_seen\"].isoformat(),\n                f\"{edge['sensitive_weight']:.2f}\",\n            ]\n        )\n\n    cochange_rows: list[list[str]] = []\n    if not args.no_cochange:\n        for (file_a, file_b), count in cochange_counts.items():\n            if count < args.cochange_min_count:\n                continue\n            commits_a = cochange_file_commits.get(file_a, 0)\n            commits_b = cochange_file_commits.get(file_b, 0)\n            denom = commits_a + commits_b - count\n            if denom <= 0:\n                continue\n            jaccard = count / denom\n            if jaccard < args.cochange_min_jaccard:\n                continue\n            cochange_rows.append([file_a, file_b, str(count), f\"{jaccard:.6f}\"])\n\n    write_csv(\n        out_dir / \"people.csv\",\n        [\n            \"person_id\",\n            \"name\",\n            \"email\",\n            \"first_seen\",\n            \"last_seen\",\n            \"commit_count\",\n            \"touches\",\n            \"sensitive_touches\",\n            \"primary_tz_offset\",\n            \"primary_tz_minutes\",\n            \"timezone_offsets\",\n        ],\n        people_rows,\n    )\n    write_csv(\n        out_dir / \"files.csv\",\n        [\n            \"file_id\",\n            \"path\",\n            \"first_seen\",\n            \"last_seen\",\n            \"commit_count\",\n            \"touches\",\n            \"bus_factor\",\n            \"sensitivity_score\",\n            \"sensitivity_tags\",\n        ],\n        file_rows,\n    )\n    write_csv(\n        out_dir / \"edges.csv\",\n        [\n            \"person_id\",\n            \"file_id\",\n            \"touches\",\n            \"recency_weight\",\n            \"first_seen\",\n            \"last_seen\",\n            \"sensitive_weight\",\n        ],\n        edge_rows,\n    )\n    if not args.no_cochange:\n        write_csv(\n            out_dir / \"cochange_edges.csv\",\n            [\n                \"file_a\",\n                \"file_b\",\n                \"cochange_count\",\n                \"jaccard\",\n            ],\n            cochange_rows,\n        )\n\n    orphaned_sensitive_code = []\n    bus_factor_hotspots = []\n    for path, file_entry in files.items():\n        tags = file_entry[\"sensitive_tags\"]\n        if not tags:\n            continue\n        bus_factor = len(file_entry[\"authors\"])\n        last_seen = file_entry[\"last_seen\"]\n        age_days = (now - last_seen).days\n        top_owner = None\n        if path in file_people_touches:\n            top_owner = max(file_people_touches[path].items(), key=lambda item: item[1])[0]\n        hotspot = {\n            \"path\": path,\n            \"bus_factor\": bus_factor,\n            \"last_touch\": last_seen.isoformat(),\n            \"sensitivity_tags\": sorted(tags.keys()),\n            \"top_owner\": top_owner,\n        }\n        if bus_factor <= args.bus_factor_threshold:\n            bus_factor_hotspots.append(hotspot)\n            if age_days >= args.stale_days:\n                orphaned_sensitive_code.append(\n                    {\n                        **hotspot,\n                        \"last_security_touch\": last_seen.isoformat(),\n                    }\n                )\n\n    hidden_owners = []\n    for tag, total in tag_totals.items():\n        if total <= 0:\n            continue\n        person_totals = tag_person_totals[tag]\n        if not person_totals:\n            continue\n        top_email, top_value = max(person_totals.items(), key=lambda item: item[1])\n        share = top_value / total\n        if share >= args.owner_threshold:\n            person_name = people.get(top_email, {}).get(\"name\", top_email)\n            hidden_owners.append(\n                {\n                    \"person\": top_email,\n                    \"name\": person_name,\n                    \"controls\": f\"{share * 100:.0f}% of {tag} code\",\n                    \"category\": tag,\n                    \"share\": round(share, 4),\n                }\n            )\n\n    summary = {\n        \"generated_at\": now.isoformat(),\n        \"repo\": os.path.abspath(args.repo),\n        \"parameters\": {\n            \"since\": args.since,\n            \"until\": args.until,\n            \"half_life_days\": args.half_life_days,\n            \"bus_factor_threshold\": args.bus_factor_threshold,\n            \"stale_days\": args.stale_days,\n            \"owner_threshold\": args.owner_threshold,\n            \"sensitive_config\": args.sensitive_config,\n            \"identity\": args.identity,\n            \"date_field\": args.date_field,\n            \"include_merges\": args.include_merges,\n            \"cochange_enabled\": not args.no_cochange,\n            \"cochange_max_files\": args.cochange_max_files,\n            \"cochange_min_count\": args.cochange_min_count,\n            \"cochange_min_jaccard\": args.cochange_min_jaccard,\n            \"cochange_default_excludes\": not args.no_default_cochange_excludes,\n            \"cochange_excludes\": cochange_excludes,\n            \"author_default_excludes\": not args.no_default_author_excludes,\n            \"author_exclude_regexes\": author_exclude_regexes,\n            \"community_top_owners\": args.community_top_owners,\n        },\n        \"orphaned_sensitive_code\": orphaned_sensitive_code,\n        \"hidden_owners\": hidden_owners,\n        \"bus_factor_hotspots\": bus_factor_hotspots,\n        \"stats\": {\n            \"commits\": total_commits_included,\n            \"commits_seen\": total_commits_seen,\n            \"commits_excluded_identities\": commits_excluded_identities,\n            \"commits_excluded_merges\": commits_excluded_merges,\n            \"edges\": total_edges,\n            \"people\": len(people),\n            \"files\": len(files),\n            \"cochange_pairs_total\": len(cochange_counts) if not args.no_cochange else 0,\n            \"cochange_edges\": len(cochange_rows) if not args.no_cochange else 0,\n            \"cochange_commits_used\": cochange_commits_used if not args.no_cochange else 0,\n            \"cochange_commits_skipped\": cochange_commits_skipped if not args.no_cochange else 0,\n            \"cochange_commits_filtered\": cochange_commits_filtered if not args.no_cochange else 0,\n            \"cochange_files_excluded\": cochange_files_excluded if not args.no_cochange else 0,\n        },\n    }\n\n    with (out_dir / \"summary.json\").open(\"w\", encoding=\"utf-8\") as handle:\n        json.dump(summary, handle, indent=2)\n\n    if args.communities or args.graphml:\n        try:\n            import networkx as nx\n            from networkx.algorithms import bipartite\n        except ImportError:\n            raise RuntimeError(\n                \"networkx is required for communities/graphml output. Install with: pip install networkx\"\n            )\n        else:\n            graph_bipartite = None\n            graph_cochange = None\n            person_nodes = set()\n            file_nodes = set()\n            community_index: dict[str, int] = {}\n            community_metadata: list[dict[str, object]] = []\n\n            if args.graphml or (args.communities and (args.no_cochange or not cochange_rows)):\n                graph_bipartite = nx.Graph()\n                for (email, path), edge in edges.items():\n                    if int(edge[\"touches\"]) < args.min_touches:\n                        continue\n                    graph_bipartite.add_node(email, node_type=\"person\")\n                    graph_bipartite.add_node(path, node_type=\"file\")\n                    graph_bipartite.add_edge(email, path, weight=float(edge[\"touches\"]))\n                    person_nodes.add(email)\n                    file_nodes.add(path)\n\n            if not args.no_cochange and cochange_rows:\n                graph_cochange = nx.Graph()\n                for file_a, file_b, count, jaccard in cochange_rows:\n                    graph_cochange.add_edge(\n                        file_a,\n                        file_b,\n                        weight=float(jaccard),\n                        count=int(count),\n                    )\n\n            if args.communities:\n                communities_result = None\n                if graph_cochange is not None:\n                    communities_result = list(\n                        nx.algorithms.community.greedy_modularity_communities(\n                            graph_cochange, weight=\"weight\"\n                        )\n                    )\n                elif graph_bipartite is not None and file_nodes:\n                    projected = bipartite.weighted_projected_graph(graph_bipartite, file_nodes)\n                    communities_result = list(\n                        nx.algorithms.community.greedy_modularity_communities(projected)\n                    )\n\n                if communities_result is not None:\n                    serialized = []\n                    for idx, community in enumerate(communities_result, start=1):\n                        files_list = sorted(community)\n                        owners = compute_community_owners(\n                            files_list,\n                            people,\n                            file_people_touches,\n                            file_people_recency,\n                            file_people_sensitive,\n                            args.community_top_owners,\n                        )\n                        for path in files_list:\n                            community_index[path] = idx\n                        entry = {\n                            \"id\": idx,\n                            \"size\": len(files_list),\n                            \"files\": files_list[: args.max_community_files],\n                            \"maintainers\": owners[\"top_maintainers\"],\n                            \"bus_factor\": owners[\"bus_factor\"],\n                            \"owner_count\": owners[\"owner_count\"],\n                            \"totals\": owners[\"totals\"],\n                        }\n                        serialized.append(entry)\n                        metadata = dict(entry)\n                        metadata.pop(\"files\", None)\n                        community_metadata.append(metadata)\n                    with (out_dir / \"communities.json\").open(\"w\", encoding=\"utf-8\") as handle:\n                        json.dump(serialized, handle, indent=2)\n\n            if args.communities:\n                for node, community_id in community_index.items():\n                    if graph_cochange is not None and node in graph_cochange:\n                        graph_cochange.nodes[node][\"community_id\"] = community_id\n                    if graph_bipartite is not None and node in graph_bipartite:\n                        graph_bipartite.nodes[node][\"community_id\"] = community_id\n\n                graph_for_json = graph_cochange or graph_bipartite\n                if graph_for_json is not None:\n                    try:\n                        from networkx.readwrite import json_graph\n                    except ImportError:\n                        pass\n                    else:\n                        data = json_graph.node_link_data(graph_for_json, edges=\"edges\")\n                        data.setdefault(\"graph\", {})\n                        data[\"graph\"][\"community_maintainers\"] = community_metadata\n                        json_name = (\n                            \"cochange.graph.json\"\n                            if graph_for_json is graph_cochange\n                            else \"ownership.graph.json\"\n                        )\n                        with (out_dir / json_name).open(\"w\", encoding=\"utf-8\") as handle:\n                            json.dump(data, handle, indent=2)\n\n            if args.graphml:\n                if graph_bipartite is not None:\n                    nx.write_graphml(graph_bipartite, out_dir / \"ownership.graphml\")\n                if graph_cochange is not None:\n                    nx.write_graphml(graph_cochange, out_dir / \"cochange.graphml\")\n\n    return out_dir\n\n\ndef main() -> int:\n    args = parse_args()\n    try:\n        out_dir = build_ownership_map(args)\n    except RuntimeError as exc:\n        print(str(exc), file=sys.stderr)\n        return 1\n\n    print(f\"Ownership map written to {out_dir}\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "skills/.curated/security-ownership-map/scripts/community_maintainers.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Report monthly maintainers for a file's community.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport csv\nimport datetime as dt\nimport json\nimport math\nimport re\nimport subprocess\nimport sys\nfrom collections import Counter, defaultdict\nfrom pathlib import Path\nfrom typing import Iterable\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Compute maintainers for a file's community over time.\"\n    )\n    parser.add_argument(\n        \"--data-dir\",\n        default=\"ownership-map-out\",\n        help=\"Directory containing graph outputs\",\n    )\n    parser.add_argument(\n        \"--repo\",\n        default=None,\n        help=\"Git repo path (required if commits.jsonl is missing)\",\n    )\n    parser.add_argument(\n        \"--file\",\n        default=None,\n        help=\"File path (exact or substring) to locate community\",\n    )\n    parser.add_argument(\n        \"--community-id\",\n        type=int,\n        default=None,\n        help=\"Community id to analyze\",\n    )\n    parser.add_argument(\n        \"--since\",\n        default=None,\n        help=\"Filter commits since date (ISO or 'YYYY-MM-DD')\",\n    )\n    parser.add_argument(\n        \"--until\",\n        default=None,\n        help=\"Filter commits until date (ISO or 'YYYY-MM-DD')\",\n    )\n    parser.add_argument(\n        \"--identity\",\n        choices=(\"author\", \"committer\"),\n        default=\"author\",\n        help=\"Identity to attribute touches to\",\n    )\n    parser.add_argument(\n        \"--date-field\",\n        choices=(\"author\", \"committer\"),\n        default=\"author\",\n        help=\"Date field to use for bucketing\",\n    )\n    parser.add_argument(\n        \"--include-merges\",\n        action=\"store_true\",\n        help=\"Include merge commits (excluded by default)\",\n    )\n    parser.add_argument(\n        \"--top\",\n        type=int,\n        default=5,\n        help=\"Top maintainers per month\",\n    )\n    parser.add_argument(\n        \"--bucket\",\n        choices=(\"month\", \"quarter\"),\n        default=\"month\",\n        help=\"Time bucket for grouping\",\n    )\n    parser.add_argument(\n        \"--touch-mode\",\n        choices=(\"commit\", \"file\"),\n        default=\"commit\",\n        help=\"Count one touch per commit or one per file touched\",\n    )\n    parser.add_argument(\n        \"--window-days\",\n        type=int,\n        default=0,\n        help=\"Use a rolling window of N days ending each month (0 = calendar month only)\",\n    )\n    parser.add_argument(\n        \"--weight\",\n        choices=(\"touches\", \"recency\"),\n        default=\"touches\",\n        help=\"Weight touches by recency using exponential decay\",\n    )\n    parser.add_argument(\n        \"--half-life-days\",\n        type=float,\n        default=180.0,\n        help=\"Half-life days for recency weighting\",\n    )\n    parser.add_argument(\n        \"--min-share\",\n        type=float,\n        default=0.0,\n        help=\"Minimum share within a month to include a maintainer\",\n    )\n    parser.add_argument(\n        \"--ignore-author-regex\",\n        default=None,\n        help=\"Regex to skip authors by name or email (e.g., '(bot|dependabot)')\",\n    )\n    parser.add_argument(\n        \"--min-touches\",\n        type=int,\n        default=1,\n        help=\"Minimum touches per month to include a maintainer\",\n    )\n    return parser.parse_args()\n\n\ndef parse_date(value: str) -> dt.datetime:\n    try:\n        parsed = dt.datetime.fromisoformat(value)\n    except ValueError:\n        parsed = dt.datetime.fromisoformat(value + \"T00:00:00\")\n    if parsed.tzinfo is None:\n        parsed = parsed.replace(tzinfo=dt.timezone.utc)\n    return parsed\n\n\ndef month_key(timestamp: dt.datetime) -> str:\n    return timestamp.strftime(\"%Y-%m\")\n\n\ndef quarter_key(timestamp: dt.datetime) -> str:\n    quarter = (timestamp.month - 1) // 3 + 1\n    return f\"{timestamp.year}-Q{quarter}\"\n\n\ndef month_end(timestamp: dt.datetime) -> dt.datetime:\n    year = timestamp.year\n    month = timestamp.month\n    if month == 12:\n        next_month = dt.datetime(year + 1, 1, 1, tzinfo=dt.timezone.utc)\n    else:\n        next_month = dt.datetime(year, month + 1, 1, tzinfo=dt.timezone.utc)\n    return next_month - dt.timedelta(seconds=1)\n\n\ndef quarter_start(timestamp: dt.datetime) -> dt.datetime:\n    quarter = (timestamp.month - 1) // 3\n    start_month = quarter * 3 + 1\n    return dt.datetime(timestamp.year, start_month, 1, tzinfo=dt.timezone.utc)\n\n\ndef quarter_end(timestamp: dt.datetime) -> dt.datetime:\n    start = quarter_start(timestamp)\n    end_month = start.month + 2\n    end_year = start.year\n    if end_month > 12:\n        end_month -= 12\n        end_year += 1\n    end_anchor = dt.datetime(end_year, end_month, 1, tzinfo=dt.timezone.utc)\n    return month_end(end_anchor)\n\n\ndef add_months(timestamp: dt.datetime, months: int) -> dt.datetime:\n    year = timestamp.year + (timestamp.month - 1 + months) // 12\n    month = (timestamp.month - 1 + months) % 12 + 1\n    return dt.datetime(year, month, 1, tzinfo=dt.timezone.utc)\n\n\ndef recency_weight(age_days: float, half_life_days: float) -> float:\n    if half_life_days <= 0:\n        return 1.0\n    return math.exp(-age_days / half_life_days)\n\n\ndef read_csv(path: Path) -> Iterable[dict[str, str]]:\n    with path.open(\"r\", encoding=\"utf-8\") as handle:\n        reader = csv.DictReader(handle)\n        yield from reader\n\n\ndef load_people(data_dir: Path) -> dict[str, dict[str, str]]:\n    people_path = data_dir / \"people.csv\"\n    people = {}\n    for row in read_csv(people_path):\n        people[row.get(\"person_id\", \"\")] = {\n            \"name\": row.get(\"name\", \"\"),\n            \"email\": row.get(\"email\", \"\"),\n            \"primary_tz_offset\": row.get(\"primary_tz_offset\", \"\"),\n        }\n    return people\n\n\ndef load_graph_json(data_dir: Path) -> dict[str, object] | None:\n    cochange_path = data_dir / \"cochange.graph.json\"\n    ownership_path = data_dir / \"ownership.graph.json\"\n    if cochange_path.exists():\n        return json.loads(cochange_path.read_text(encoding=\"utf-8\"))\n    if ownership_path.exists():\n        return json.loads(ownership_path.read_text(encoding=\"utf-8\"))\n    return None\n\n\ndef find_file_node(nodes: list[dict[str, object]], query: str) -> dict[str, object]:\n    exact = [node for node in nodes if node.get(\"id\") == query]\n    if exact:\n        return exact[0]\n    contains = [node for node in nodes if query in str(node.get(\"id\", \"\"))]\n    if len(contains) == 1:\n        return contains[0]\n    if not contains:\n        raise ValueError(f\"File not found in graph: {query}\")\n    candidates = \", \".join(str(node.get(\"id\")) for node in contains[:10])\n    raise ValueError(f\"Multiple matches for file {query}: {candidates}\")\n\n\ndef load_community_files(\n    data_dir: Path, file_query: str | None, community_id: int | None\n) -> tuple[int, list[str]]:\n    graph = load_graph_json(data_dir)\n    if graph:\n        nodes = graph.get(\"nodes\", [])\n        if file_query:\n            node = find_file_node(nodes, file_query)\n            community_id = int(node.get(\"community_id\", -1))\n        if community_id is None:\n            raise ValueError(\"Provide --file or --community-id\")\n        files = [node.get(\"id\") for node in nodes if node.get(\"community_id\") == community_id]\n        files = [entry for entry in files if entry]\n        if not files:\n            raise ValueError(f\"No files found for community {community_id}\")\n        return community_id, files\n\n    communities_path = data_dir / \"communities.json\"\n    if not communities_path.exists():\n        raise FileNotFoundError(\"Missing graph json and communities.json\")\n    communities = json.loads(communities_path.read_text(encoding=\"utf-8\"))\n    if file_query:\n        for entry in communities:\n            files = entry.get(\"files\", [])\n            if any(file_query == f or file_query in f for f in files):\n                return int(entry.get(\"id\", -1)), list(files)\n        raise ValueError(\"File not found in communities.json (list may be truncated)\")\n    if community_id is None:\n        raise ValueError(\"Provide --file or --community-id\")\n    for entry in communities:\n        if int(entry.get(\"id\", -1)) == community_id:\n            return community_id, list(entry.get(\"files\", []))\n    raise ValueError(f\"Community id not found: {community_id}\")\n\n\ndef iter_commits_from_json(\n    commits_path: Path,\n    since: dt.datetime | None,\n    until: dt.datetime | None,\n    date_field: str,\n) -> Iterable[dict[str, object]]:\n    with commits_path.open(\"r\", encoding=\"utf-8\") as handle:\n        for line in handle:\n            entry = json.loads(line)\n            author_date = entry.get(\"author_date\") or entry.get(\"date\")\n            committer_date = entry.get(\"committer_date\")\n            if author_date:\n                author_dt = parse_date(author_date)\n            else:\n                author_dt = None\n            if committer_date:\n                committer_dt = parse_date(committer_date)\n            else:\n                committer_dt = None\n            if date_field == \"committer\":\n                commit_date = committer_dt or author_dt\n            else:\n                commit_date = author_dt or committer_dt\n            if commit_date is None:\n                continue\n            if since and commit_date < since:\n                continue\n            if until and commit_date > until:\n                continue\n            yield {\n                \"hash\": entry.get(\"hash\", \"\"),\n                \"parents\": entry.get(\"parents\", []),\n                \"is_merge\": entry.get(\"is_merge\", False),\n                \"author_name\": entry.get(\"author_name\", \"\"),\n                \"author_email\": entry.get(\"author_email\", \"\"),\n                \"author_date\": author_date,\n                \"committer_name\": entry.get(\"committer_name\", \"\"),\n                \"committer_email\": entry.get(\"committer_email\", \"\"),\n                \"committer_date\": committer_date,\n                \"files\": entry.get(\"files\", []),\n            }\n\n\ndef iter_commits_from_git(\n    repo: str, since: str | None, until: str | None, include_merges: bool\n) -> Iterable[dict[str, object]]:\n    cmd = [\n        \"git\",\n        \"-C\",\n        repo,\n        \"log\",\n        \"--name-only\",\n        \"--no-renames\",\n        \"--date=iso-strict\",\n        \"--format=---%n%H%n%P%n%an%n%ae%n%ad%n%cn%n%ce%n%cd\",\n    ]\n    if not include_merges:\n        cmd.append(\"--no-merges\")\n    if since:\n        cmd.extend([\"--since\", since])\n    if until:\n        cmd.extend([\"--until\", until])\n\n    proc = subprocess.Popen(\n        cmd,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        text=True,\n    )\n    assert proc.stdout is not None\n\n    block: list[str] = []\n    for line in proc.stdout:\n        line = line.rstrip(\"\\n\")\n        if line == \"---\":\n            if block:\n                yield from parse_git_block(block)\n                block = []\n        else:\n            block.append(line)\n    if block:\n        yield from parse_git_block(block)\n\n    stderr = proc.stderr.read() if proc.stderr else \"\"\n    exit_code = proc.wait()\n    if exit_code != 0:\n        raise RuntimeError(stderr.strip() or \"git log failed\")\n\n\ndef parse_git_block(block: list[str]) -> Iterable[dict[str, object]]:\n    if len(block) < 8:\n        return []\n    commit_hash = block[0]\n    parents = [entry for entry in block[1].split(\" \") if entry]\n    author_name = block[2]\n    author_email = block[3]\n    author_date = block[4]\n    committer_name = block[5]\n    committer_email = block[6]\n    committer_date = block[7]\n    files = [line for line in block[8:] if line]\n    return [\n        {\n            \"hash\": commit_hash,\n            \"parents\": parents,\n            \"is_merge\": len(parents) > 1,\n            \"author_name\": author_name,\n            \"author_email\": author_email,\n            \"author_date\": author_date,\n            \"committer_name\": committer_name,\n            \"committer_email\": committer_email,\n            \"committer_date\": committer_date,\n            \"files\": files,\n        }\n    ]\n\n\ndef main() -> int:\n    args = parse_args()\n    data_dir = Path(args.data_dir)\n    if not data_dir.exists():\n        print(f\"Data directory not found: {data_dir}\", file=sys.stderr)\n        return 1\n\n    since = parse_date(args.since) if args.since else None\n    until = parse_date(args.until) if args.until else None\n\n    try:\n        community_id, community_files = load_community_files(data_dir, args.file, args.community_id)\n    except (ValueError, FileNotFoundError) as exc:\n        print(str(exc), file=sys.stderr)\n        return 2\n\n    people = load_people(data_dir)\n\n    ignore_re = re.compile(args.ignore_author_regex) if args.ignore_author_regex else None\n\n    commits_path = data_dir / \"commits.jsonl\"\n    if commits_path.exists():\n        commit_iter = iter_commits_from_json(commits_path, since, until, args.date_field)\n    else:\n        if not args.repo:\n            print(\"--repo is required when commits.jsonl is missing\", file=sys.stderr)\n            return 2\n        commit_iter = iter_commits_from_git(args.repo, args.since, args.until, args.include_merges)\n\n    commit_rows: list[tuple[dt.datetime, str, int, str, str]] = []\n    for commit in commit_iter:\n        if commit.get(\"is_merge\") and not args.include_merges:\n            continue\n        files = commit.get(\"files\", [])\n        in_community = sum(1 for path in files if path in community_files)\n        if in_community == 0:\n            continue\n        identity_name = commit.get(f\"{args.identity}_name\", \"\")\n        identity_email = commit.get(f\"{args.identity}_email\", \"\")\n        date_value = commit.get(f\"{args.date_field}_date\")\n        if not date_value:\n            print(\n                \"Missing committer fields in commits.jsonl. Re-run build or pass --repo.\",\n                file=sys.stderr,\n            )\n            return 2\n        commit_date = parse_date(date_value)\n        person_id = identity_email or identity_name\n        if ignore_re and ignore_re.search(identity_name or \"\"):\n            continue\n        if ignore_re and ignore_re.search(identity_email or \"\"):\n            continue\n        touches = 1 if args.touch_mode == \"commit\" else in_community\n        commit_rows.append((commit_date, person_id, touches, identity_name, identity_email))\n        if person_id not in people:\n            people[person_id] = {\n                \"name\": identity_name,\n                \"email\": identity_email,\n                \"primary_tz_offset\": \"\",\n            }\n\n    if not commit_rows:\n        print(\"No commits touching community files for the selected window.\", file=sys.stderr)\n        return 0\n\n    commit_rows.sort(key=lambda row: row[0])\n    period_counts: dict[str, Counter[str]] = defaultdict(Counter)\n    period_totals: dict[str, float] = defaultdict(float)\n\n    min_date = commit_rows[0][0]\n    max_date = commit_rows[-1][0]\n    if args.bucket == \"quarter\":\n        period_cursor = quarter_start(min_date)\n        period_end_anchor = quarter_start(max_date)\n        step_months = 3\n        key_func = quarter_key\n        end_func = quarter_end\n    else:\n        period_cursor = dt.datetime(min_date.year, min_date.month, 1, tzinfo=dt.timezone.utc)\n        period_end_anchor = dt.datetime(max_date.year, max_date.month, 1, tzinfo=dt.timezone.utc)\n        step_months = 1\n        key_func = month_key\n        end_func = month_end\n\n    while period_cursor <= period_end_anchor:\n        bucket_end = end_func(period_cursor)\n        bucket_key = key_func(bucket_end)\n        if args.window_days > 0:\n            window_start = bucket_end - dt.timedelta(days=args.window_days)\n\n            def in_bucket(commit_date: dt.datetime) -> bool:\n                return window_start <= commit_date <= bucket_end\n        else:\n            if args.bucket == \"quarter\":\n                bucket_start = quarter_start(period_cursor)\n\n                def in_bucket(commit_date: dt.datetime) -> bool:\n                    return bucket_start <= commit_date <= bucket_end\n            else:\n\n                def in_bucket(commit_date: dt.datetime) -> bool:\n                    return (\n                        commit_date.year == bucket_end.year\n                        and commit_date.month == bucket_end.month\n                    )\n\n        for commit_date, person_id, touches, _name, _email in commit_rows:\n            if not in_bucket(commit_date):\n                continue\n            weight = 1.0\n            if args.weight == \"recency\":\n                age_days = (bucket_end - commit_date).total_seconds() / 86400.0\n                weight = recency_weight(age_days, args.half_life_days)\n            contribution = touches * weight\n            period_counts[bucket_key][person_id] += contribution\n            period_totals[bucket_key] += contribution\n\n        period_cursor = add_months(period_cursor, step_months)\n\n    writer = csv.writer(sys.stdout)\n    writer.writerow(\n        [\n            \"period\",\n            \"rank\",\n            \"name\",\n            \"email\",\n            \"primary_tz_offset\",\n            \"community_touches\",\n            \"touch_share\",\n        ]\n    )\n\n    for period in sorted(period_counts.keys()):\n        total = period_totals[period]\n        ranked = sorted(period_counts[period].items(), key=lambda item: item[1], reverse=True)\n        rank = 0\n        for person_id, touches in ranked:\n            if touches < args.min_touches:\n                continue\n            share = touches / total if total else 0.0\n            if share < args.min_share:\n                continue\n            rank += 1\n            if rank > args.top:\n                break\n            person = people.get(person_id, {})\n            if args.weight == \"recency\":\n                touches_value = f\"{touches:.4f}\"\n            else:\n                touches_value = f\"{touches:.0f}\"\n            writer.writerow(\n                [\n                    period,\n                    rank,\n                    person.get(\"name\", \"\"),\n                    person.get(\"email\", person_id),\n                    person.get(\"primary_tz_offset\", \"\"),\n                    touches_value,\n                    f\"{share:.4f}\",\n                ]\n            )\n\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "skills/.curated/security-ownership-map/scripts/query_ownership.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Query ownership-map outputs without loading everything into an LLM context.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport csv\nimport json\nimport sys\nfrom collections import defaultdict\nfrom pathlib import Path\nfrom typing import Iterable\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Query ownership-map outputs with bounded JSON results.\"\n    )\n    parser.add_argument(\n        \"--data-dir\",\n        default=\"ownership-map-out\",\n        help=\"Directory containing people.csv, files.csv, edges.csv\",\n    )\n\n    subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n    people = subparsers.add_parser(\"people\", help=\"List people\")\n    people.add_argument(\"--limit\", type=int, default=20)\n    people.add_argument(\"--sort\", default=\"touches\")\n    people.add_argument(\"--email-contains\", default=None)\n    people.add_argument(\"--min-touches\", type=int, default=0)\n    people.add_argument(\"--min-sensitive\", type=float, default=0.0)\n\n    files = subparsers.add_parser(\"files\", help=\"List files\")\n    files.add_argument(\"--limit\", type=int, default=20)\n    files.add_argument(\"--sort\", default=\"sensitivity_score\")\n    files.add_argument(\"--path-contains\", default=None)\n    files.add_argument(\"--tag\", default=None)\n    files.add_argument(\"--bus-factor-max\", type=int, default=None)\n    files.add_argument(\"--sensitivity-min\", type=float, default=0.0)\n\n    person = subparsers.add_parser(\"person\", help=\"Show person details and top files\")\n    person.add_argument(\"--person\", required=True, help=\"Exact email or substring\")\n    person.add_argument(\"--limit\", type=int, default=20)\n    person.add_argument(\"--sort\", default=\"touches\")\n\n    file_cmd = subparsers.add_parser(\"file\", help=\"Show file details and top people\")\n    file_cmd.add_argument(\"--file\", required=True, help=\"Exact path or substring\")\n    file_cmd.add_argument(\"--limit\", type=int, default=20)\n    file_cmd.add_argument(\"--sort\", default=\"touches\")\n\n    cochange = subparsers.add_parser(\"cochange\", help=\"List co-change neighbors for a file\")\n    cochange.add_argument(\"--file\", required=True, help=\"Exact path or substring\")\n    cochange.add_argument(\"--limit\", type=int, default=20)\n    cochange.add_argument(\"--sort\", default=\"jaccard\")\n    cochange.add_argument(\"--min-jaccard\", type=float, default=0.0)\n    cochange.add_argument(\"--min-count\", type=int, default=1)\n\n    tag = subparsers.add_parser(\"tag\", help=\"Show top people/files for a sensitive tag\")\n    tag.add_argument(\"--tag\", required=True)\n    tag.add_argument(\"--limit\", type=int, default=20)\n\n    summary = subparsers.add_parser(\"summary\", help=\"Show summary.json sections\")\n    summary.add_argument(\"--section\", default=None)\n\n    communities = subparsers.add_parser(\"communities\", help=\"List communities\")\n    communities.add_argument(\"--limit\", type=int, default=10)\n    communities.add_argument(\"--id\", type=int, default=None)\n\n    community = subparsers.add_parser(\"community\", help=\"Show community maintainers\")\n    community.add_argument(\"--id\", type=int, required=True)\n    community.add_argument(\"--include-files\", action=\"store_true\")\n    community.add_argument(\"--file-limit\", type=int, default=50)\n\n    return parser.parse_args()\n\n\ndef to_int(value: str) -> int:\n    try:\n        return int(value)\n    except (TypeError, ValueError):\n        return 0\n\n\ndef to_float(value: str) -> float:\n    try:\n        return float(value)\n    except (TypeError, ValueError):\n        return 0.0\n\n\ndef read_csv(path: Path) -> Iterable[dict[str, str]]:\n    with path.open(\"r\", encoding=\"utf-8\") as handle:\n        reader = csv.DictReader(handle)\n        yield from reader\n\n\ndef load_people(data_dir: Path) -> list[dict[str, object]]:\n    people_path = data_dir / \"people.csv\"\n    people = []\n    for row in read_csv(people_path):\n        person = dict(row)\n        person[\"touches\"] = to_int(row.get(\"touches\", \"0\"))\n        person[\"commit_count\"] = to_int(row.get(\"commit_count\", \"0\"))\n        person[\"sensitive_touches\"] = to_float(row.get(\"sensitive_touches\", \"0\"))\n        people.append(person)\n    return people\n\n\ndef load_files(data_dir: Path) -> list[dict[str, object]]:\n    files_path = data_dir / \"files.csv\"\n    files = []\n    for row in read_csv(files_path):\n        file_entry = dict(row)\n        file_entry[\"touches\"] = to_int(row.get(\"touches\", \"0\"))\n        file_entry[\"commit_count\"] = to_int(row.get(\"commit_count\", \"0\"))\n        file_entry[\"bus_factor\"] = to_int(row.get(\"bus_factor\", \"0\"))\n        file_entry[\"sensitivity_score\"] = to_float(row.get(\"sensitivity_score\", \"0\"))\n        tags = row.get(\"sensitivity_tags\", \"\")\n        file_entry[\"sensitivity_tags\"] = [tag for tag in tags.split(\";\") if tag]\n        files.append(file_entry)\n    return files\n\n\ndef load_summary(data_dir: Path) -> dict[str, object]:\n    summary_path = data_dir / \"summary.json\"\n    with summary_path.open(\"r\", encoding=\"utf-8\") as handle:\n        return json.load(handle)\n\n\ndef load_communities(data_dir: Path) -> list[dict[str, object]]:\n    communities_path = data_dir / \"communities.json\"\n    if not communities_path.exists():\n        raise FileNotFoundError(\"communities.json not found; rerun build with --communities\")\n    with communities_path.open(\"r\", encoding=\"utf-8\") as handle:\n        return json.load(handle)\n\n\ndef load_cochange_edges(data_dir: Path) -> Iterable[dict[str, object]]:\n    edges_path = data_dir / \"cochange_edges.csv\"\n    if not edges_path.exists():\n        raise FileNotFoundError(\"cochange_edges.csv not found; rerun build without --no-cochange\")\n    for row in read_csv(edges_path):\n        yield {\n            \"file_a\": row.get(\"file_a\"),\n            \"file_b\": row.get(\"file_b\"),\n            \"cochange_count\": to_int(row.get(\"cochange_count\", \"0\")),\n            \"jaccard\": to_float(row.get(\"jaccard\", \"0\")),\n        }\n\n\ndef select_single(records: list[dict[str, object]], key: str, query: str) -> dict[str, object]:\n    exact = [record for record in records if str(record.get(key, \"\")) == query]\n    if exact:\n        return exact[0]\n    contains = [record for record in records if query in str(record.get(key, \"\"))]\n    if len(contains) == 1:\n        return contains[0]\n    if not contains:\n        raise ValueError(f\"No match for {query}\")\n    candidates = [str(record.get(key, \"\")) for record in contains[:10]]\n    raise ValueError(f\"Multiple matches for {query}: {', '.join(candidates)}\")\n\n\ndef top_edges_for_person(data_dir: Path, person_id: str) -> list[dict[str, object]]:\n    edges_path = data_dir / \"edges.csv\"\n    results = []\n    for row in read_csv(edges_path):\n        if row.get(\"person_id\") != person_id:\n            continue\n        results.append(\n            {\n                \"file_id\": row.get(\"file_id\"),\n                \"touches\": to_int(row.get(\"touches\", \"0\")),\n                \"recency_weight\": to_float(row.get(\"recency_weight\", \"0\")),\n                \"sensitive_weight\": to_float(row.get(\"sensitive_weight\", \"0\")),\n                \"last_seen\": row.get(\"last_seen\"),\n            }\n        )\n    return results\n\n\ndef top_edges_for_file(data_dir: Path, file_id: str) -> list[dict[str, object]]:\n    edges_path = data_dir / \"edges.csv\"\n    results = []\n    for row in read_csv(edges_path):\n        if row.get(\"file_id\") != file_id:\n            continue\n        results.append(\n            {\n                \"person_id\": row.get(\"person_id\"),\n                \"touches\": to_int(row.get(\"touches\", \"0\")),\n                \"recency_weight\": to_float(row.get(\"recency_weight\", \"0\")),\n                \"sensitive_weight\": to_float(row.get(\"sensitive_weight\", \"0\")),\n                \"last_seen\": row.get(\"last_seen\"),\n            }\n        )\n    return results\n\n\ndef sort_records(records: list[dict[str, object]], key: str) -> list[dict[str, object]]:\n    return sorted(records, key=lambda item: item.get(key, 0), reverse=True)\n\n\ndef handle_people(args: argparse.Namespace, data_dir: Path) -> None:\n    people = load_people(data_dir)\n    if args.email_contains:\n        people = [p for p in people if args.email_contains in p.get(\"email\", \"\")]\n    people = [p for p in people if p[\"touches\"] >= args.min_touches]\n    people = [p for p in people if p[\"sensitive_touches\"] >= args.min_sensitive]\n    people = sort_records(people, args.sort)[: args.limit]\n    payload = [\n        {\n            \"person_id\": p.get(\"person_id\"),\n            \"name\": p.get(\"name\"),\n            \"email\": p.get(\"email\"),\n            \"touches\": p.get(\"touches\"),\n            \"commit_count\": p.get(\"commit_count\"),\n            \"sensitive_touches\": p.get(\"sensitive_touches\"),\n            \"primary_tz_offset\": p.get(\"primary_tz_offset\"),\n        }\n        for p in people\n    ]\n    print(json.dumps(payload, indent=2))\n\n\ndef handle_files(args: argparse.Namespace, data_dir: Path) -> None:\n    files = load_files(data_dir)\n    if args.path_contains:\n        files = [f for f in files if args.path_contains in f.get(\"path\", \"\")]\n    if args.tag:\n        files = [f for f in files if args.tag in f.get(\"sensitivity_tags\", [])]\n    if args.bus_factor_max is not None:\n        files = [f for f in files if f[\"bus_factor\"] <= args.bus_factor_max]\n    files = [f for f in files if f[\"sensitivity_score\"] >= args.sensitivity_min]\n    files = sort_records(files, args.sort)[: args.limit]\n    payload = [\n        {\n            \"file_id\": f.get(\"file_id\"),\n            \"path\": f.get(\"path\"),\n            \"touches\": f.get(\"touches\"),\n            \"bus_factor\": f.get(\"bus_factor\"),\n            \"sensitivity_score\": f.get(\"sensitivity_score\"),\n            \"sensitivity_tags\": f.get(\"sensitivity_tags\"),\n            \"last_seen\": f.get(\"last_seen\"),\n        }\n        for f in files\n    ]\n    print(json.dumps(payload, indent=2))\n\n\ndef handle_person(args: argparse.Namespace, data_dir: Path) -> None:\n    people = load_people(data_dir)\n    person = select_single(people, \"person_id\", args.person)\n    files = load_files(data_dir)\n    file_map = {f[\"file_id\"]: f for f in files}\n    edges = top_edges_for_person(data_dir, person[\"person_id\"])\n    edges = sort_records(edges, args.sort)[: args.limit]\n    payload = {\n        \"person\": {\n            \"person_id\": person.get(\"person_id\"),\n            \"name\": person.get(\"name\"),\n            \"email\": person.get(\"email\"),\n            \"touches\": person.get(\"touches\"),\n            \"commit_count\": person.get(\"commit_count\"),\n            \"sensitive_touches\": person.get(\"sensitive_touches\"),\n            \"primary_tz_offset\": person.get(\"primary_tz_offset\"),\n            \"timezone_offsets\": person.get(\"timezone_offsets\"),\n        },\n        \"top_files\": [\n            {\n                \"file_id\": edge.get(\"file_id\"),\n                \"path\": file_map.get(edge.get(\"file_id\"), {}).get(\"path\"),\n                \"touches\": edge.get(\"touches\"),\n                \"recency_weight\": edge.get(\"recency_weight\"),\n                \"sensitive_weight\": edge.get(\"sensitive_weight\"),\n                \"last_seen\": edge.get(\"last_seen\"),\n                \"sensitivity_tags\": file_map.get(edge.get(\"file_id\"), {}).get(\"sensitivity_tags\"),\n            }\n            for edge in edges\n        ],\n    }\n    print(json.dumps(payload, indent=2))\n\n\ndef handle_file(args: argparse.Namespace, data_dir: Path) -> None:\n    files = load_files(data_dir)\n    file_entry = select_single(files, \"file_id\", args.file)\n    people = load_people(data_dir)\n    people_map = {p[\"person_id\"]: p for p in people}\n    edges = top_edges_for_file(data_dir, file_entry[\"file_id\"])\n    edges = sort_records(edges, args.sort)[: args.limit]\n    payload = {\n        \"file\": {\n            \"file_id\": file_entry.get(\"file_id\"),\n            \"path\": file_entry.get(\"path\"),\n            \"touches\": file_entry.get(\"touches\"),\n            \"bus_factor\": file_entry.get(\"bus_factor\"),\n            \"sensitivity_score\": file_entry.get(\"sensitivity_score\"),\n            \"sensitivity_tags\": file_entry.get(\"sensitivity_tags\"),\n            \"last_seen\": file_entry.get(\"last_seen\"),\n        },\n        \"top_people\": [\n            {\n                \"person_id\": edge.get(\"person_id\"),\n                \"name\": people_map.get(edge.get(\"person_id\"), {}).get(\"name\"),\n                \"email\": people_map.get(edge.get(\"person_id\"), {}).get(\"email\"),\n                \"touches\": edge.get(\"touches\"),\n                \"recency_weight\": edge.get(\"recency_weight\"),\n                \"sensitive_weight\": edge.get(\"sensitive_weight\"),\n                \"primary_tz_offset\": people_map.get(edge.get(\"person_id\"), {}).get(\n                    \"primary_tz_offset\"\n                ),\n            }\n            for edge in edges\n        ],\n    }\n    print(json.dumps(payload, indent=2))\n\n\ndef handle_cochange(args: argparse.Namespace, data_dir: Path) -> None:\n    files = load_files(data_dir)\n    file_entry = select_single(files, \"file_id\", args.file)\n\n    neighbors = []\n    for row in load_cochange_edges(data_dir):\n        file_a = row.get(\"file_a\")\n        file_b = row.get(\"file_b\")\n        if file_a == file_entry[\"file_id\"]:\n            other = file_b\n        elif file_b == file_entry[\"file_id\"]:\n            other = file_a\n        else:\n            continue\n\n        if row[\"cochange_count\"] < args.min_count:\n            continue\n        if row[\"jaccard\"] < args.min_jaccard:\n            continue\n\n        neighbors.append(\n            {\n                \"file_id\": other,\n                \"path\": other,\n                \"cochange_count\": row[\"cochange_count\"],\n                \"jaccard\": row[\"jaccard\"],\n            }\n        )\n\n    neighbors = sort_records(neighbors, args.sort)[: args.limit]\n    payload = {\n        \"file\": {\n            \"file_id\": file_entry.get(\"file_id\"),\n            \"path\": file_entry.get(\"path\"),\n        },\n        \"neighbors\": neighbors,\n    }\n    print(json.dumps(payload, indent=2))\n\n\ndef handle_tag(args: argparse.Namespace, data_dir: Path) -> None:\n    files = load_files(data_dir)\n    tagged_files = [f for f in files if args.tag in f.get(\"sensitivity_tags\", [])]\n    tagged_ids = {f[\"file_id\"] for f in tagged_files}\n\n    person_touch = defaultdict(int)\n    edges_path = data_dir / \"edges.csv\"\n    for row in read_csv(edges_path):\n        if row.get(\"file_id\") not in tagged_ids:\n            continue\n        person_touch[row.get(\"person_id\")] += to_int(row.get(\"touches\", \"0\"))\n\n    people = load_people(data_dir)\n    people_map = {p[\"person_id\"]: p for p in people}\n    top_people = [\n        {\n            \"person_id\": person_id,\n            \"name\": people_map.get(person_id, {}).get(\"name\"),\n            \"email\": people_map.get(person_id, {}).get(\"email\"),\n            \"touches\": touches,\n        }\n        for person_id, touches in person_touch.items()\n    ]\n    top_people = sorted(top_people, key=lambda item: item.get(\"touches\", 0), reverse=True)[\n        : args.limit\n    ]\n\n    top_files = sorted(tagged_files, key=lambda item: item.get(\"touches\", 0), reverse=True)[\n        : args.limit\n    ]\n\n    payload = {\n        \"tag\": args.tag,\n        \"top_people\": top_people,\n        \"top_files\": [\n            {\n                \"file_id\": entry.get(\"file_id\"),\n                \"path\": entry.get(\"path\"),\n                \"touches\": entry.get(\"touches\"),\n                \"bus_factor\": entry.get(\"bus_factor\"),\n            }\n            for entry in top_files\n        ],\n    }\n    print(json.dumps(payload, indent=2))\n\n\ndef handle_summary(args: argparse.Namespace, data_dir: Path) -> None:\n    summary = load_summary(data_dir)\n    if args.section:\n        if args.section not in summary:\n            raise ValueError(f\"Section not found: {args.section}\")\n        payload = summary[args.section]\n    else:\n        payload = summary\n    print(json.dumps(payload, indent=2))\n\n\ndef handle_communities(args: argparse.Namespace, data_dir: Path) -> None:\n    communities = load_communities(data_dir)\n    if args.id is not None:\n        matches = [entry for entry in communities if entry.get(\"id\") == args.id]\n        if not matches:\n            raise ValueError(f\"Community id not found: {args.id}\")\n        payload = matches[0]\n    else:\n        payload = sorted(communities, key=lambda item: item.get(\"size\", 0), reverse=True)[\n            : args.limit\n        ]\n    print(json.dumps(payload, indent=2))\n\n\ndef handle_community(args: argparse.Namespace, data_dir: Path) -> None:\n    communities = load_communities(data_dir)\n    matches = [entry for entry in communities if entry.get(\"id\") == args.id]\n    if not matches:\n        raise ValueError(f\"Community id not found: {args.id}\")\n    entry = dict(matches[0])\n    files = entry.pop(\"files\", [])\n    payload = entry\n    if args.include_files:\n        payload[\"files\"] = files[: args.file_limit]\n        payload[\"files_truncated\"] = len(files) > args.file_limit\n    print(json.dumps(payload, indent=2))\n\n\ndef main() -> int:\n    args = parse_args()\n    data_dir = Path(args.data_dir)\n    if not data_dir.exists():\n        print(f\"Data directory not found: {data_dir}\", file=sys.stderr)\n        return 1\n\n    try:\n        if args.command == \"people\":\n            handle_people(args, data_dir)\n        elif args.command == \"files\":\n            handle_files(args, data_dir)\n        elif args.command == \"person\":\n            handle_person(args, data_dir)\n        elif args.command == \"file\":\n            handle_file(args, data_dir)\n        elif args.command == \"cochange\":\n            handle_cochange(args, data_dir)\n        elif args.command == \"tag\":\n            handle_tag(args, data_dir)\n        elif args.command == \"summary\":\n            handle_summary(args, data_dir)\n        elif args.command == \"communities\":\n            handle_communities(args, data_dir)\n        elif args.command == \"community\":\n            handle_community(args, data_dir)\n        else:\n            raise ValueError(f\"Unknown command: {args.command}\")\n    except (FileNotFoundError, ValueError) as exc:\n        print(str(exc), file=sys.stderr)\n        return 2\n\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "skills/.curated/security-ownership-map/scripts/run_ownership_map.py",
    "content": "#!/usr/bin/env python3\n\"\"\"One-shot runner for building the security ownership map.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport subprocess\nimport sys\nfrom pathlib import Path\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Run build_ownership_map.py with sensible defaults.\"\n    )\n    parser.add_argument(\"--repo\", default=\".\", help=\"Path to the git repo (default: .)\")\n    parser.add_argument(\n        \"--out\",\n        default=\"ownership-map-out\",\n        help=\"Output directory for graph artifacts\",\n    )\n    parser.add_argument(\"--since\", default=None, help=\"Limit git log to commits since date\")\n    parser.add_argument(\"--until\", default=None, help=\"Limit git log to commits until date\")\n    parser.add_argument(\n        \"--identity\",\n        choices=(\"author\", \"committer\"),\n        default=\"author\",\n        help=\"Identity to attribute touches to\",\n    )\n    parser.add_argument(\n        \"--date-field\",\n        choices=(\"author\", \"committer\"),\n        default=\"author\",\n        help=\"Date field to use for recency and bucketing\",\n    )\n    parser.add_argument(\n        \"--include-merges\",\n        action=\"store_true\",\n        help=\"Include merge commits (excluded by default)\",\n    )\n    parser.add_argument(\n        \"--emit-commits\",\n        action=\"store_true\",\n        help=\"Write commit list to commits.jsonl\",\n    )\n    parser.add_argument(\n        \"--author-exclude-regex\",\n        action=\"append\",\n        default=[],\n        help=\"Regex for author name/email to exclude (repeatable)\",\n    )\n    parser.add_argument(\n        \"--no-default-author-excludes\",\n        action=\"store_true\",\n        help=\"Disable default author excludes (dependabot)\",\n    )\n    parser.add_argument(\n        \"--graphml\",\n        action=\"store_true\",\n        help=\"Emit GraphML outputs\",\n    )\n    parser.add_argument(\n        \"--sensitive-config\",\n        default=None,\n        help=\"CSV file with pattern,tag,weight for sensitive paths\",\n    )\n    parser.add_argument(\n        \"--cochange-max-files\",\n        type=int,\n        default=50,\n        help=\"Ignore commits touching more than this many files for co-change graph\",\n    )\n    parser.add_argument(\n        \"--cochange-min-count\",\n        type=int,\n        default=2,\n        help=\"Minimum co-change count to keep file-file edge\",\n    )\n    parser.add_argument(\n        \"--cochange-min-jaccard\",\n        type=float,\n        default=0.05,\n        help=\"Minimum Jaccard similarity to keep file-file edge\",\n    )\n    parser.add_argument(\n        \"--cochange-exclude\",\n        action=\"append\",\n        default=[],\n        help=\"Glob to exclude from co-change graph (repeatable)\",\n    )\n    parser.add_argument(\n        \"--no-default-cochange-excludes\",\n        action=\"store_true\",\n        help=\"Disable default co-change excludes (lockfiles, .github, editor config)\",\n    )\n    parser.add_argument(\n        \"--community-top-owners\",\n        type=int,\n        default=5,\n        help=\"Top maintainers saved per community\",\n    )\n    parser.add_argument(\n        \"--bus-factor-threshold\",\n        type=int,\n        default=1,\n        help=\"Bus factor threshold for hotspots\",\n    )\n    parser.add_argument(\n        \"--stale-days\",\n        type=int,\n        default=365,\n        help=\"Days since last touch to consider stale\",\n    )\n    parser.add_argument(\n        \"--owner-threshold\",\n        type=float,\n        default=0.5,\n        help=\"Share threshold for hidden owner detection\",\n    )\n    parser.add_argument(\n        \"--no-cochange\",\n        action=\"store_true\",\n        help=\"Disable co-change graph output\",\n    )\n    parser.add_argument(\n        \"--no-communities\",\n        action=\"store_true\",\n        help=\"Disable community detection (not recommended)\",\n    )\n    return parser.parse_args()\n\n\ndef main() -> int:\n    args = parse_args()\n\n    try:\n        import networkx  # noqa: F401\n    except ImportError:\n        print(\"networkx is required. Install with: pip install networkx\", file=sys.stderr)\n        return 2\n\n    script_path = Path(__file__).resolve().parent / \"build_ownership_map.py\"\n    cmd = [\n        sys.executable,\n        str(script_path),\n        \"--repo\",\n        args.repo,\n        \"--out\",\n        args.out,\n        \"--identity\",\n        args.identity,\n        \"--date-field\",\n        args.date_field,\n        \"--cochange-max-files\",\n        str(args.cochange_max_files),\n        \"--cochange-min-count\",\n        str(args.cochange_min_count),\n        \"--cochange-min-jaccard\",\n        str(args.cochange_min_jaccard),\n        \"--community-top-owners\",\n        str(args.community_top_owners),\n        \"--bus-factor-threshold\",\n        str(args.bus_factor_threshold),\n        \"--stale-days\",\n        str(args.stale_days),\n        \"--owner-threshold\",\n        str(args.owner_threshold),\n    ]\n\n    if args.since:\n        cmd.extend([\"--since\", args.since])\n    if args.until:\n        cmd.extend([\"--until\", args.until])\n    if args.include_merges:\n        cmd.append(\"--include-merges\")\n    if args.emit_commits:\n        cmd.append(\"--emit-commits\")\n    if args.graphml:\n        cmd.append(\"--graphml\")\n    if args.sensitive_config:\n        cmd.extend([\"--sensitive-config\", args.sensitive_config])\n    if args.no_cochange:\n        cmd.append(\"--no-cochange\")\n    if args.no_communities:\n        cmd.append(\"--no-communities\")\n    if args.no_default_cochange_excludes:\n        cmd.append(\"--no-default-cochange-excludes\")\n    for pattern in args.cochange_exclude:\n        cmd.extend([\"--cochange-exclude\", pattern])\n    if args.no_default_author_excludes:\n        cmd.append(\"--no-default-author-excludes\")\n    for pattern in args.author_exclude_regex:\n        cmd.extend([\"--author-exclude-regex\", pattern])\n\n    result = subprocess.run(cmd, check=False)\n    return result.returncode\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "skills/.curated/security-threat-model/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/security-threat-model/SKILL.md",
    "content": "---\nname: \"security-threat-model\"\ndescription: \"Repository-grounded threat modeling that enumerates trust boundaries, assets, attacker capabilities, abuse paths, and mitigations, and writes a concise Markdown threat model. Trigger only when the user explicitly asks to threat model a codebase or path, enumerate threats/abuse paths, or perform AppSec threat modeling. Do not trigger for general architecture summaries, code review, or non-security design work.\"\n---\n\n# Threat Model Source Code Repo\n\nDeliver an actionable AppSec-grade threat model that is specific to the repository or a project path, not a generic checklist. Anchor every architectural claim to evidence in the repo and keep assumptions explicit. Prioritizing realistic attacker goals and concrete impacts over generic checklists.\n\n## Quick start\n\n1) Collect (or infer) inputs:\n- Repo root path and any in-scope paths.\n- Intended usage, deployment model, internet exposure, and auth expectations (if known).\n- Any existing repository summary or architecture spec.\n- Use prompts in `references/prompt-template.md` to generate a repository summary.\n- Follow the required output contract in `references/prompt-template.md`. Use it verbatim when possible.\n\n## Workflow\n\n### 1) Scope and extract the system model\n- Identify primary components, data stores, and external integrations from the repo summary.\n- Identify how the system runs (server, CLI, library, worker) and its entrypoints.\n- Separate runtime behavior from CI/build/dev tooling and from tests/examples.\n- Map the in-scope locations to those components and exclude out-of-scope items explicitly.\n- Do not claim components, flows, or controls without evidence.\n\n### 2) Derive boundaries, assets, and entry points\n- Enumerate trust boundaries as concrete edges between components, noting protocol, auth, encryption, validation, and rate limiting.\n- List assets that drive risk (data, credentials, models, config, compute resources, audit logs).\n- Identify entry points (endpoints, upload surfaces, parsers/decoders, job triggers, admin tooling, logging/error sinks).\n\n### 3) Calibrate assets and attacker capabilities\n- List the assets that drive risk (credentials, PII, integrity-critical state, availability-critical components, build artifacts).\n- Describe realistic attacker capabilities based on exposure and intended usage.\n- Explicitly note non-capabilities to avoid inflated severity.\n\n\n### 4) Enumerate threats as abuse paths\n- Prefer attacker goals that map to assets and boundaries (exfiltration, privilege escalation, integrity compromise, denial of service).\n- Classify each threat and tie it to impacted assets.\n- Keep the number of threats small but high quality.\n\n### 5) Prioritize with explicit likelihood and impact reasoning\n- Use qualitative likelihood and impact (low/medium/high) with short justifications.\n- Set overall priority (critical/high/medium/low) using likelihood x impact, adjusted for existing controls.\n- State which assumptions most influence the ranking.\n\n### 6) Validate service context and assumptions with the user\n- Summarize key assumptions that materially affect threat ranking or scope, then ask the user to confirm or correct them.\n- Ask 1–3 targeted questions to resolve missing context (service owner and environment, scale/users, deployment model, authn/authz, internet exposure, data sensitivity, multi-tenancy).\n- Pause and wait for user feedback before producing the final report.\n- If the user declines or can’t answer, state which assumptions remain and how they influence priority.\n\n### 7) Recommend mitigations and focus paths\n- Distinguish existing mitigations (with evidence) from recommended mitigations.\n- Tie mitigations to concrete locations (component, boundary, or entry point) and control types (authZ checks, input validation, schema enforcement, sandboxing, rate limits, secrets isolation, audit logging).\n- Prefer specific implementation hints over generic advice (e.g., \"enforce schema at gateway for upload payloads\" vs \"validate inputs\").\n- Base recommendations on validated user context; if assumptions remain unresolved, mark recommendations as conditional.\n\n### 8) Run a quality check before finalizing\n- Confirm all discovered entrypoints are covered.\n- Confirm each trust boundary is represented in threats.\n- Confirm runtime vs CI/dev separation.\n- Confirm user clarifications (or explicit non-responses) are reflected.\n- Confirm assumptions and open questions are explicit.\n- Confirm that the format of the report matches closely the required output format defined in prompt template: `references/prompt-template.md`\n- Write the final Markdown to a file named `<repo-or-dir-name>-threat-model.md` (use the basename of the repo root, or the in-scope directory if you were asked to model a subpath).\n\n\n## Risk prioritization guidance (illustrative, not exhaustive)\n- High: pre-auth RCE, auth bypass, cross-tenant access, sensitive data exfiltration, key or token theft, model or config integrity compromise, sandbox escape.\n- Medium: targeted DoS of critical components, partial data exposure, rate-limit bypass with measurable impact, log/metrics poisoning that affects detection.\n- Low: low-sensitivity info leaks, noisy DoS with easy mitigation, issues requiring unlikely preconditions.\n\n## References\n\n- Output contract and full prompt template: `references/prompt-template.md`\n- Optional controls/asset list: `references/security-controls-and-assets.md`\n\nOnly load the reference files you need. Keep the final result concise, grounded, and reviewable.\n"
  },
  {
    "path": "skills/.curated/security-threat-model/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Security Threat Model\"\n  short_description: \"Repo-grounded threat modeling and abuse-path analysis\"\n  default_prompt: \"Create a repository-grounded threat model for this codebase with prioritized abuse paths and mitigations.\"\n"
  },
  {
    "path": "skills/.curated/security-threat-model/references/prompt-template.md",
    "content": "# Threat Modeling Prompt Template for LLMs\n\nThis reference provides a disciplined, repo-grounded prompt that produces AppSec-usable threat models. Use it when you need a reliable output contract and a consistent process to assemble the threat model output\n\n## System prompt\n\nUse this as a stable system prompt:\n\n````text\nYou are a senior application security engineer producing a threat model that will be read by other AppSec engineers.\n\nPrimary objective:\n- Generate a threat model that is specific to THIS repository and its real-world usage.\n- Prefer concrete, evidence-backed findings over generic vulnerability checklists.\n\nEvidence and grounding rules:\n- Do not invent components, data stores, endpoints, flows, or controls.\n- Every architectural claim must be backed by at least one \"Evidence anchor\" referencing a repo path\n  (and a symbol name, config key, or a short quoted snippet if available).\n- If information is missing, state assumptions explicitly and list the open questions needed to validate them.\n\nSecurity hygiene:\n- Never output secrets. If you encounter tokens/keys/passwords, redact them and only describe their presence and location.\n\nThreat modeling approach:\n- Model the system using data flows and trust boundaries.\n- Enumerate threats and produce attack goals and abuse paths\n- Prioritize threats using explicit likelihood and impact reasoning (qualitative is acceptable: low/medium/high).\n\nScope discipline:\n- Clearly separate: production/runtime behavior vs CI/build/dev tooling vs tests/examples.\n- Clearly separate attacker-controlled inputs vs operator-controlled inputs vs developer-controlled inputs.\n- If a vulnerability class requires attacker control that likely does not exist for this repo's real usage, say so and downgrade severity.\n\nCommunication quality:\n- Write for AppSec engineers: concise but specific.\n- Use precise terminology. Include mitigations and residual risks.\n- Avoid restating large blocks of README/spec; summarize and point to evidence.\n\nDiagram requirements:\n- Produce a single compact Mermaid flowchart showing primary components and trust boundaries.\n- Mermaid must render cleanly. Use a conservative subset:\n  - Use `flowchart TD` or `flowchart LR` and only `-->` arrows.\n  - Use simple node IDs (letters/numbers/underscores only) and quoted labels (e.g., `A[\"Label\"]`); avoid `A(Label)` shape syntax.\n  - Do not use Mermaid `title` lines or `style` directives.\n  - Keep edge labels to plain words/spaces only via `-->|label|`; avoid `{}`, `[]`, `()`, or quotes in edge labels (if needed, drop the label).\n  - Keep node labels short and readable: do not include file paths, URLs, or socket paths (put those details in prose outside the diagram).\n- Wrap the diagram in a Markdown fenced block:\n  ```mermaid\n  <mermaid syntax here>\n  ```\n````\n\n## Repository summary prompt\n\n```\nWe have a codebase located at {repo_directory/path}, currently on branch {branch_name}.\n\nPlease produce a security-oriented summary of the repository (or the specified sub-path) with the goal of helping a follow-on security engineer quickly understand the system well enough to build an initial threat model and investigate potential security hypotheses.\n\nObjectives\n1.\tProject overview\n\t•\tIdentify the primary programming languages, frameworks, and build system.\n\t•\tSummarize the project’s core purpose and high-level architecture.\n\t•\tDescribe major components, services, or modules and how they interact.\n2.\tSecurity posture and entry points\n\t•\tIdentify likely user entry points and trust boundaries.\n\t•\tDescribe existing security layers (e.g., authentication, authorization, validation, sandboxing, isolation, privilege boundaries).\n\t•\tCall out security-critical components and assumptions that must hold for the system to remain secure.\n\nGuidance for Security Analysis\n\nStructure the summary so an application security engineer can quickly answer questions such as:\n\t•\tWhere does user input originate?\n\t•\tHow is untrusted data parsed, validated, and handled?\n\t•\tWhat security assumptions should not be violated?\n\t•\tWhere are the most likely choke points for security bugs?\n\nAdapt the analysis to the project type. For example:\n\t•\tWeb applications: where requests enter, how user data is parsed, routed, authenticated, and stored.\n\t•\tCommand-line tools: supported inputs (arguments, files, environment variables, stdin) and how they are processed.\n\t•\tNetwork daemons: exposed ports, supported protocols, message formats, and request handling paths.\n\t•\tOperating system or low-level components: common vulnerability classes (e.g., memory corruption, logic flaws) that could lead to LPE or RCE.\n\nBe thorough but pragmatic: the goal is to help a security engineer quickly determine whether a discovered bug is security-relevant and where deeper investigation should focus.\n\nTooling Notes\n\nIf Ripgrep (rg) is available, use it to explore the codebase. When using grep or rg, always include the -I flag to avoid searching through binary files.\n```\n\n\n\n## User prompt template\n\nUse this as the task prompt, filling in what you know and marking the rest as assumptions:\n\n```text\n# Inputs\nContext (fill as available; otherwise infer and mark assumptions):\n- intended_usage: {intended_usage}\n- deployment_model: {deployment_model}\n- data_sensitivity: {data_sensitivity}\n- internet_exposure: {internet_exposure}\n- authn_authz_expectations: {authn_authz_expectations}\n- out_of_scope: {out_of_scope}\n\nProvided summaries (may be incomplete):\n- repository_summary: {repository_summary}\n\n\nIn-scope code locations (if known):\n- in_scope_paths: {in_scope_paths}\n\n# Task\nConstruct a repo-centric threat model that helps AppSec engineers understand the most important security risks and where to focus manual review.\n\nYou MUST follow this process and reflect outputs in the final document:\n\n## Process\n1) Repo discovery (evidence collection)\n   a. Identify the repo shape:\n      - languages and frameworks\n      - how it runs (server/cli/library), entrypoints, build artifacts\n   b. Identify security-relevant surfaces and controls by searching for evidences, such as:\n      - network listeners/routes/endpoints; RPC handlers; message consumers\n      - authentication, session/token handling, authorization checks, RBAC/ACL logic\n      - parsing/serialization/deserialization (JSON/YAML/XML/protobuf), template rendering, eval/dynamic code\n      - file upload/read paths, archive extraction, image/document parsing\n      - database/queue/cache clients and query construction\n      - secrets/config loading, environment variables, key management\n      - SSRF-capable HTTP clients, webhooks, URL fetchers\n      - sandboxing/isolation, privilege boundaries, subprocess execution\n      - logging/auditing and error handling paths\n      - CI/build/release: pipelines, dependency management, artifact publishing\n   \n2) System model\n   a. Summarize the primary components (runtime plus critical build/CI components when relevant).\n   b. Enumerate data flows and trust boundaries.\n      - For each trust boundary, specify:\n        * source to destination\n        * data types crossing (e.g., credentials, PII, files, tokens, prompts)\n        * channel/protocol (HTTP/gRPC/IPC/file/db)\n        * security guarantees and validation (auth, mTLS, origin checks, schema validation, rate limits)\n   c. Provide a compact Mermaid diagram showing components and trust boundaries.\n\n3) Assets and security objectives\n   - List assets (data, credentials, integrity-critical state, availability-critical components, build artifacts).\n   - For each asset, state why it matters (confidentiality/integrity/availability, compliance, user harm).\n\n4) Attacker model\n   - Capabilities: realistic remote attacker assumptions based on intended usage and exposure.\n   - Non-capabilities: things attacker cannot plausibly do (unless explicitly in scope), to avoid inflated severity.\n\n5) Threat enumeration (concrete, system-specific)\n   - Generate threats as attacker stories tied to:\n     * entry points\n     * trust boundaries\n     * privileged components\n   - Prefer abuse paths (multi-step sequences) over single-line generic threats.\n\n6) Risk prioritization\n   - For each threat:\n     * Likelihood: low/medium/high with a 1 to 2 sentence justification\n     * Impact: low/medium/high with a 1 to 2 sentence justification\n     * Overall priority: critical/high/medium/low (based on likelihood x impact, adjusted for existing controls)\n   - Explicitly state which assumptions most affect risk.\n\n7) Validate assumptions and service context with the user (required before final report)\n   - Summarize key assumptions that materially affect scope or risk ranking.\n   - Ask 1 to 3 targeted questions to resolve missing service meta-context (service owner/environment, scale/users, deployment model, authn/authz, internet exposure, data sensitivity, multi-tenancy).\n   - Pause and wait for user feedback before producing the final report.\n   - If the user cannot answer, proceed with explicit assumptions and mark any conditional conclusions.\n\n8) Mitigations and recommendations\n   - For each high/critical threat:\n     * Existing mitigations (with evidence anchors)\n     * Gaps/weaknesses\n     * Recommended mitigations (code/config/process)\n     * Detection/monitoring ideas (logging, metrics, alerts)\n\n9) Focus paths for manual security review\n   - Output 2 to 30 repo-relative paths (files or directories) that merit deeper review.\n   - For each path, give a one-sentence reason tied to the threat model.\n\n10) Quality check\n   - Provide a short checklist confirming you covered:\n     * all entry points you discovered\n     * each trust boundary at least once in threats\n     * runtime vs CI/dev separation\n     * user clarifications (or explicit non-responses)\n     * assumptions and open questions\n\n## Required output format (exact)\nBefore producing the final Markdown report, first provide an assumption-validation check-in:\n- List the key assumptions in 3 to 6 bullets.\n- Ask 1 to 3 targeted context questions.\n- Wait for the user response, then produce the final report below using the clarified context.\n\nProduce valid Markdown with these sections in this order:\n\n## Executive summary\n- 1 short paragraph on the top risk themes and highest-risk areas.\n\n## Scope and assumptions\n- In-scope paths, out-of-scope items, and explicit assumptions.\n- A short list of open questions that would materially change the risk ranking.\n\n\n## System model\n### Primary components\n### Data flows and trust boundaries\nRepresent the system as a sequence of arrow-style bullets (e.g., Internet → API Server, User Input -> Application Logic, etc). For each boundary, document:\n\t•\tthe primary data types crossing the boundary,\n\t•\tthe communication channel or protocol,\n\t•\tthe security guarantees (e.g., authentication, origin checks, encryption, rate limiting), and\n\t•\tany input validation, normalization, or schema enforcement performed.\n\n#### Diagram\n- Include a single, compact Mermaid diagram (`flowchart TD` or `flowchart LR`) showing primary components and trust boundaries (e.g., separate trust zones via subgraphs). Keep it compact, use only `-->`, avoid `title`/`style`, keep node labels short (no paths/URLs), and keep edge labels to plain words only (avoid `{}`, `[]`, `()`, or quotes).\n\n\n## Assets and security objectives\n- A table: Asset | Why it matters | Security objective (C/I/A)\n\n## Attacker model\n### Capabilities\n### Non-capabilities\n\n## Entry points and attack surfaces\n- A table: Surface | How reached | Trust boundary | Notes | Evidence (repo path / symbol)\n\n## Top abuse paths\n- 5 to 10 short abuse paths, each as a numbered sequence of steps (attacker goal -> steps -> impact).\n\n## Threat model table\n- A Markdown table with columns:\n  Threat ID | Threat source | Prerequisites | Threat action | Impact | Impacted assets | Existing controls (evidence) | Gaps | Recommended mitigations | Detection ideas | Likelihood | Impact severity | Priority\n\nRules:\n- Threat IDs must be stable and formatted: TM-001, TM-002, ...\n- Priority must be one of: critical, high, medium, low.\n- Keep prerequisites to 1 to 2 sentences. Keep recommended mitigations concrete.\n\n## Criticality calibration\n- Define what counts as critical/high/medium/low for THIS repo and context.\n- Include 2 to 3 examples per level (tailored to the repo's assets and exposure).\n\n## Focus paths for security review\n- A table: Path | Why it matters | Related Threat IDs\n\n## Notes on use\n\n- Fill in known context, but allow the model to infer and mark assumptions.\n- Include 1–2 repo-path anchors per major claim; do not dump every match.\n"
  },
  {
    "path": "skills/.curated/security-threat-model/references/security-controls-and-assets.md",
    "content": "# Security Controls and Asset Categories\n\nUse this as a lightweight checklist to keep outputs consistent across teams. Prefer concrete, system-specific items over generic text.\n\n## Asset categories (pick only what applies)\n- User data (PII, content, uploads)\n- Authentication artifacts (passwords, tokens, sessions, cookies)\n- Authorization state (roles, policies, ACLs)\n- Secrets and keys (API keys, signing keys, encryption keys)\n- Configuration and feature flags\n- Models and weights (if ML systems)\n- Source code and build artifacts\n- Audit logs and telemetry\n- Availability-critical resources (queues, caches, rate limits, compute budgets)\n- Tenant isolation boundaries and metadata\n\n## Security control categories\n- Identity and access: authN, authZ, session handling, mTLS, key rotation\n- Input protection: schema validation, parsing hardening, upload scanning, sandboxing\n- Network safeguards: TLS, network policies, WAF, rate limiting, DoS controls\n- Data protection: encryption at rest/in transit, tokenization, redaction\n- Isolation: process sandboxing, container boundaries, tenant isolation, seccomp\n- Observability: audit logs, alerting, anomaly detection, tamper resistance\n- Supply chain: dependency pinning, SBOMs, provenance, signing\n- Change control: CI checks, deployment approvals, config guardrails\n\n## Mitigation phrasing patterns\n- \"Enforce schema at <boundary> for <payload> before <component>.\"\n- \"Require authZ check for <action> on <resource> in <service>.\"\n- \"Isolate <parser/component> in a sandbox with <resource limits>.\"\n- \"Rate limit <endpoint> by <key> and apply burst caps.\"\n- \"Encrypt <data> at rest using <key management> and rotate <keys>.\"\n"
  },
  {
    "path": "skills/.curated/sentry/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/sentry/SKILL.md",
    "content": "---\nname: \"sentry\"\ndescription: \"Use when the user asks to inspect Sentry issues or events, summarize recent production errors, or pull basic Sentry health data via the Sentry API; perform read-only queries with the bundled script and require `SENTRY_AUTH_TOKEN`.\"\n---\n\n\n# Sentry (Read-only Observability)\n\n## Quick start\n\n- If not already authenticated, ask the user to provide a valid `SENTRY_AUTH_TOKEN` (read-only scopes such as `project:read`, `event:read`) or to log in and create one before running commands.\n- Set `SENTRY_AUTH_TOKEN` as an env var.\n- Optional defaults: `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_BASE_URL`.\n- Defaults: org/project `{your-org}`/`{your-project}`, time range `24h`, environment `prod`, limit 20 (max 50).\n- Always call the Sentry API (no heuristics, no caching).\n\nIf the token is missing, give the user these steps:\n1. Create a Sentry auth token: https://sentry.io/settings/account/api/auth-tokens/\n2. Create a token with read-only scopes such as `project:read`, `event:read`, and `org:read`.\n3. Set `SENTRY_AUTH_TOKEN` as an environment variable in their system.\n4. Offer to guide them through setting the environment variable for their OS/shell if needed.\n- Never ask the user to paste the full token in chat. Ask them to set it locally and confirm when ready.\n\n## Core tasks (use bundled script)\n\nUse `scripts/sentry_api.py` for deterministic API calls. It handles pagination and retries once on transient errors.\n\n## Skill path (set once)\n\n```bash\nexport CODEX_HOME=\"${CODEX_HOME:-$HOME/.codex}\"\nexport SENTRY_API=\"$CODEX_HOME/skills/sentry/scripts/sentry_api.py\"\n```\n\nUser-scoped skills install under `$CODEX_HOME/skills` (default: `~/.codex/skills`).\n\n### 1) List issues (ordered by most recent)\n\n```bash\npython3 \"$SENTRY_API\" \\\n  list-issues \\\n  --org {your-org} \\\n  --project {your-project} \\\n  --environment prod \\\n  --time-range 24h \\\n  --limit 20 \\\n  --query \"is:unresolved\"\n```\n\n### 2) Resolve an issue short ID to issue ID\n\n```bash\npython3 \"$SENTRY_API\" \\\n  list-issues \\\n  --org {your-org} \\\n  --project {your-project} \\\n  --query \"ABC-123\" \\\n  --limit 1\n```\n\nUse the returned `id` for issue detail or events.\n\n### 3) Issue detail\n\n```bash\npython3 \"$SENTRY_API\" \\\n  issue-detail \\\n  1234567890\n```\n\n### 4) Issue events\n\n```bash\npython3 \"$SENTRY_API\" \\\n  issue-events \\\n  1234567890 \\\n  --limit 20\n```\n\n### 5) Event detail (no stack traces by default)\n\n```bash\npython3 \"$SENTRY_API\" \\\n  event-detail \\\n  --org {your-org} \\\n  --project {your-project} \\\n  abcdef1234567890\n```\n\n## API requirements\n\nAlways use these endpoints (GET only):\n\n- List issues: `/api/0/projects/{org_slug}/{project_slug}/issues/`\n- Issue detail: `/api/0/issues/{issue_id}/`\n- Events for issue: `/api/0/issues/{issue_id}/events/`\n- Event detail: `/api/0/projects/{org_slug}/{project_slug}/events/{event_id}/`\n\n## Inputs and defaults\n\n- `org_slug`, `project_slug`: default to `{your-org}`/`{your-project}` (avoid non-prod orgs).\n- `time_range`: default `24h` (pass as `statsPeriod`).\n- `environment`: default `prod`.\n- `limit`: default 20, max 50 (paginate until limit reached).\n- `search_query`: optional `query` parameter.\n- `issue_short_id`: resolve via list-issues query first.\n\n## Output formatting rules\n\n- Issue list: show title, short_id, status, first_seen, last_seen, count, environments, top_tags; order by most recent.\n- Event detail: include culprit, timestamp, environment, release, url.\n- If no results, state explicitly.\n- Redact PII in output (emails, IPs). Do not print raw stack traces.\n- Never echo auth tokens.\n\n## Golden test inputs\n\n- Org: `{your-org}`\n- Project: `{your-project}`\n- Issue short ID: `{ABC-123}`\n\nExample prompt: “List the top 10 open issues for prod in the last 24h.”\nExpected: ordered list with titles, short IDs, counts, last seen.\n"
  },
  {
    "path": "skills/.curated/sentry/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Sentry (Read-only Observability)\"\n  short_description: \"Read-only Sentry observability\"\n  icon_small: \"./assets/sentry-small.svg\"\n  icon_large: \"./assets/sentry.png\"\n  default_prompt: \"Investigate this issue in read-only Sentry data and report likely root cause, impact, and next steps.\"\n"
  },
  {
    "path": "skills/.curated/sentry/scripts/sentry_api.py",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport json\nimport os\nimport re\nimport sys\nimport time\nfrom urllib.error import HTTPError, URLError\nfrom urllib.parse import urlencode\nfrom urllib.request import Request, urlopen\n\nDEFAULT_BASE_URL = \"https://sentry.io\"\nDEFAULT_ORG = \"your-org\"\nDEFAULT_PROJECT = \"your-project\"\nMAX_LIMIT = 50\n\nEMAIL_RE = re.compile(r\"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\")\nIP_RE = re.compile(r\"\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b\")\n\n\ndef redact_string(value):\n    value = EMAIL_RE.sub(\"[REDACTED_EMAIL]\", value)\n    value = IP_RE.sub(\"[REDACTED_IP]\", value)\n    return value\n\n\ndef redact_data(value):\n    if isinstance(value, str):\n        return redact_string(value)\n    if isinstance(value, list):\n        return [redact_data(item) for item in value]\n    if isinstance(value, dict):\n        redacted = {}\n        for key, item in value.items():\n            if key.lower() in {\"email\", \"ip\", \"ip_address\"}:\n                redacted[key] = \"[REDACTED]\"\n            else:\n                redacted[key] = redact_data(item)\n        return redacted\n    return value\n\n\ndef next_cursor(link_header):\n    if not link_header:\n        return None\n    for part in link_header.split(\",\"):\n        if 'rel=\"next\"' in part and 'results=\"true\"' in part:\n            match = re.search(r'cursor=\"([^\"]+)\"', part)\n            if match:\n                return match.group(1)\n    return None\n\n\ndef request_json(url, token, retries=1):\n    req = Request(url)\n    req.add_header(\"Authorization\", f\"Bearer {token}\")\n    req.add_header(\"Accept\", \"application/json\")\n\n    attempt = 0\n    while True:\n        try:\n            with urlopen(req) as resp:\n                body = resp.read().decode(\"utf-8\")\n                data = json.loads(body) if body else None\n                return data, resp.headers\n        except HTTPError as err:\n            body = err.read().decode(\"utf-8\", \"ignore\")\n            if attempt < retries and (err.code >= 500 or err.code == 429):\n                attempt += 1\n                time.sleep(1)\n                continue\n            raise RuntimeError(f\"HTTP {err.code} for {url}: {body or 'request failed'}\") from err\n        except URLError as err:\n            if attempt < retries:\n                attempt += 1\n                time.sleep(1)\n                continue\n            raise RuntimeError(f\"Network error for {url}: {err.reason}\") from err\n\n\ndef build_url(base_url, path, params=None):\n    base = base_url.rstrip(\"/\")\n    url = f\"{base}{path}\"\n    if params:\n        url = f\"{url}?{urlencode(params, doseq=True)}\"\n    return url\n\n\ndef paged_get(base_url, path, params, token, limit):\n    results = []\n    cursor = None\n    while len(results) < limit:\n        page_params = dict(params)\n        page_params[\"per_page\"] = min(MAX_LIMIT, limit - len(results))\n        if cursor:\n            page_params[\"cursor\"] = cursor\n        url = build_url(base_url, path, page_params)\n        data, headers = request_json(url, token)\n        if not data:\n            break\n        results.extend(data)\n        cursor = next_cursor(headers.get(\"Link\"))\n        if not cursor:\n            break\n    return results[:limit]\n\n\ndef require_org_project(org, project):\n    if org == DEFAULT_ORG or project == DEFAULT_PROJECT:\n        raise RuntimeError(\n            \"Missing org/project. Set SENTRY_ORG and SENTRY_PROJECT or pass --org/--project.\"\n        )\n\n\ndef handle_list_issues(args, token, base_url):\n    require_org_project(args.org, args.project)\n    limit = min(args.limit, MAX_LIMIT)\n    params = {\n        \"statsPeriod\": args.time_range,\n        \"environment\": args.environment,\n    }\n    if args.query:\n        params[\"query\"] = args.query\n\n    path = f\"/api/0/projects/{args.org}/{args.project}/issues/\"\n    issues = paged_get(base_url, path, params, token, limit)\n    return issues\n\n\ndef handle_issue_detail(args, token, base_url):\n    path = f\"/api/0/issues/{args.issue_id}/\"\n    url = build_url(base_url, path)\n    data, _ = request_json(url, token)\n    return data\n\n\ndef handle_issue_events(args, token, base_url):\n    limit = min(args.limit, MAX_LIMIT)\n    path = f\"/api/0/issues/{args.issue_id}/events/\"\n    events = paged_get(base_url, path, {}, token, limit)\n    return events\n\n\ndef handle_event_detail(args, token, base_url):\n    require_org_project(args.org, args.project)\n    path = f\"/api/0/projects/{args.org}/{args.project}/events/{args.event_id}/\"\n    url = build_url(base_url, path)\n    data, _ = request_json(url, token)\n    if data and not args.include_entries:\n        data = dict(data)\n        data.pop(\"entries\", None)\n    return data\n\n\ndef build_parser():\n    parser = argparse.ArgumentParser(\n        description=\"Read-only Sentry API helper for issues and events\"\n    )\n    parser.add_argument(\n        \"--base-url\",\n        default=os.environ.get(\"SENTRY_BASE_URL\", DEFAULT_BASE_URL),\n        help=\"Sentry base URL (default: https://sentry.io)\",\n    )\n    parser.add_argument(\n        \"--org\",\n        default=os.environ.get(\"SENTRY_ORG\", DEFAULT_ORG),\n        help=\"Sentry org slug\",\n    )\n    parser.add_argument(\n        \"--project\",\n        default=os.environ.get(\"SENTRY_PROJECT\", DEFAULT_PROJECT),\n        help=\"Sentry project slug\",\n    )\n    parser.add_argument(\n        \"--no-redact\",\n        action=\"store_true\",\n        help=\"Do not redact PII in output\",\n    )\n\n    subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n    list_issues = subparsers.add_parser(\"list-issues\", help=\"List issues\")\n    list_issues.add_argument(\"--time-range\", default=\"24h\")\n    list_issues.add_argument(\"--environment\", default=\"prod\")\n    list_issues.add_argument(\"--query\", default=\"\")\n    list_issues.add_argument(\"--limit\", type=int, default=20)\n\n    issue_detail = subparsers.add_parser(\"issue-detail\", help=\"Issue detail\")\n    issue_detail.add_argument(\"issue_id\")\n\n    issue_events = subparsers.add_parser(\"issue-events\", help=\"Issue events\")\n    issue_events.add_argument(\"issue_id\")\n    issue_events.add_argument(\"--limit\", type=int, default=20)\n\n    event_detail = subparsers.add_parser(\"event-detail\", help=\"Event detail\")\n    event_detail.add_argument(\"event_id\")\n    event_detail.add_argument(\n        \"--include-entries\",\n        action=\"store_true\",\n        help=\"Include event entries (may contain stack traces)\",\n    )\n\n    return parser\n\n\ndef main():\n    parser = build_parser()\n    args = parser.parse_args()\n\n    token = os.environ.get(\"SENTRY_AUTH_TOKEN\")\n    if not token:\n        raise RuntimeError(\"Missing SENTRY_AUTH_TOKEN env var.\")\n\n    base_url = args.base_url\n\n    if args.command == \"list-issues\":\n        data = handle_list_issues(args, token, base_url)\n    elif args.command == \"issue-detail\":\n        data = handle_issue_detail(args, token, base_url)\n    elif args.command == \"issue-events\":\n        data = handle_issue_events(args, token, base_url)\n    elif args.command == \"event-detail\":\n        data = handle_event_detail(args, token, base_url)\n    else:\n        raise RuntimeError(f\"Unknown command: {args.command}\")\n\n    if not args.no_redact:\n        data = redact_data(data)\n\n    print(json.dumps(data, indent=2, sort_keys=True))\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except RuntimeError as exc:\n        print(f\"Error: {exc}\", file=sys.stderr)\n        sys.exit(1)\n"
  },
  {
    "path": "skills/.curated/slides/LICENSE.txt",
    "content": "                                 Apache License\r\n                           Version 2.0, January 2004\r\n                        http://www.apache.org/licenses/\r\n\r\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n   1. Definitions.\r\n\r\n      \"License\" shall mean the terms and conditions for use, reproduction,\r\n      and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n      \"Licensor\" shall mean the copyright owner or entity authorized by\r\n      the copyright owner that is granting the License.\r\n\r\n      \"Legal Entity\" shall mean the union of the acting entity and all\r\n      other entities that control, are controlled by, or are under common\r\n      control with that entity. For the purposes of this definition,\r\n      \"control\" means (i) the power, direct or indirect, to cause the\r\n      direction or management of such entity, whether by contract or\r\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n      outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n      exercising permissions granted by this License.\r\n\r\n      \"Source\" form shall mean the preferred form for making modifications,\r\n      including but not limited to software source code, documentation\r\n      source, and configuration files.\r\n\r\n      \"Object\" form shall mean any form resulting from mechanical\r\n      transformation or translation of a Source form, including but\r\n      not limited to compiled object code, generated documentation,\r\n      and conversions to other media types.\r\n\r\n      \"Work\" shall mean the work of authorship, whether in Source or\r\n      Object form, made available under the License, as indicated by a\r\n      copyright notice that is included in or attached to the work\r\n      (an example is provided in the Appendix below).\r\n\r\n      \"Derivative Works\" shall mean any work, whether in Source or Object\r\n      form, that is based on (or derived from) the Work and for which the\r\n      editorial revisions, annotations, elaborations, or other modifications\r\n      represent, as a whole, an original work of authorship. For the purposes\r\n      of this License, Derivative Works shall not include works that remain\r\n      separable from, or merely link (or bind by name) to the interfaces of,\r\n      the Work and Derivative Works thereof.\r\n\r\n      \"Contribution\" shall mean any work of authorship, including\r\n      the original version of the Work and any modifications or additions\r\n      to that Work or Derivative Works thereof, that is intentionally\r\n      submitted to Licensor for inclusion in the Work by the copyright owner\r\n      or by an individual or Legal Entity authorized to submit on behalf of\r\n      the copyright owner. For the purposes of this definition, \"submitted\"\r\n      means any form of electronic, verbal, or written communication sent\r\n      to the Licensor or its representatives, including but not limited to\r\n      communication on electronic mailing lists, source code control systems,\r\n      and issue tracking systems that are managed by, or on behalf of, the\r\n      Licensor for the purpose of discussing and improving the Work, but\r\n      excluding communication that is conspicuously marked or otherwise\r\n      designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n      on behalf of whom a Contribution has been received by Licensor and\r\n      subsequently incorporated within the Work.\r\n\r\n   2. Grant of Copyright License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      copyright license to reproduce, prepare Derivative Works of,\r\n      publicly display, publicly perform, sublicense, and distribute the\r\n      Work and such Derivative Works in Source or Object form.\r\n\r\n   3. Grant of Patent License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      (except as stated in this section) patent license to make, have made,\r\n      use, offer to sell, sell, import, and otherwise transfer the Work,\r\n      where such license applies only to those patent claims licensable\r\n      by such Contributor that are necessarily infringed by their\r\n      Contribution(s) alone or by combination of their Contribution(s)\r\n      with the Work to which such Contribution(s) was submitted. If You\r\n      institute patent litigation against any entity (including a\r\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n      or a Contribution incorporated within the Work constitutes direct\r\n      or contributory patent infringement, then any patent licenses\r\n      granted to You under this License for that Work shall terminate\r\n      as of the date such litigation is filed.\r\n\r\n   4. Redistribution. You may reproduce and distribute copies of the\r\n      Work or Derivative Works thereof in any medium, with or without\r\n      modifications, and in Source or Object form, provided that You\r\n      meet the following conditions:\r\n\r\n      (a) You must give any other recipients of the Work or\r\n          Derivative Works a copy of this License; and\r\n\r\n      (b) You must cause any modified files to carry prominent notices\r\n          stating that You changed the files; and\r\n\r\n      (c) You must retain, in the Source form of any Derivative Works\r\n          that You distribute, all copyright, patent, trademark, and\r\n          attribution notices from the Source form of the Work,\r\n          excluding those notices that do not pertain to any part of\r\n          the Derivative Works; and\r\n\r\n      (d) If the Work includes a \"NOTICE\" text file as part of its\r\n          distribution, then any Derivative Works that You distribute must\r\n          include a readable copy of the attribution notices contained\r\n          within such NOTICE file, excluding those notices that do not\r\n          pertain to any part of the Derivative Works, in at least one\r\n          of the following places: within a NOTICE text file distributed\r\n          as part of the Derivative Works; within the Source form or\r\n          documentation, if provided along with the Derivative Works; or,\r\n          within a display generated by the Derivative Works, if and\r\n          wherever such third-party notices normally appear. The contents\r\n          of the NOTICE file are for informational purposes only and\r\n          do not modify the License. You may add Your own attribution\r\n          notices within Derivative Works that You distribute, alongside\r\n          or as an addendum to the NOTICE text from the Work, provided\r\n          that such additional attribution notices cannot be construed\r\n          as modifying the License.\r\n\r\n      You may add Your own copyright statement to Your modifications and\r\n      may provide additional or different license terms and conditions\r\n      for use, reproduction, or distribution of Your modifications, or\r\n      for any such Derivative Works as a whole, provided Your use,\r\n      reproduction, and distribution of the Work otherwise complies with\r\n      the conditions stated in this License.\r\n\r\n   5. Submission of Contributions. Unless You explicitly state otherwise,\r\n      any Contribution intentionally submitted for inclusion in the Work\r\n      by You to the Licensor shall be under the terms and conditions of\r\n      this License, without any additional terms or conditions.\r\n      Notwithstanding the above, nothing herein shall supersede or modify\r\n      the terms of any separate license agreement you may have executed\r\n      with Licensor regarding such Contributions.\r\n\r\n   6. Trademarks. This License does not grant permission to use the trade\r\n      names, trademarks, service marks, or product names of the Licensor,\r\n      except as required for reasonable and customary use in describing the\r\n      origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n   7. Disclaimer of Warranty. Unless required by applicable law or\r\n      agreed to in writing, Licensor provides the Work (and each\r\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n      implied, including, without limitation, any warranties or conditions\r\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n      PARTICULAR PURPOSE. You are solely responsible for determining the\r\n      appropriateness of using or redistributing the Work and assume any\r\n      risks associated with Your exercise of permissions under this License.\r\n\r\n   8. Limitation of Liability. In no event and under no legal theory,\r\n      whether in tort (including negligence), contract, or otherwise,\r\n      unless required by applicable law (such as deliberate and grossly\r\n      negligent acts) or agreed to in writing, shall any Contributor be\r\n      liable to You for damages, including any direct, indirect, special,\r\n      incidental, or consequential damages of any character arising as a\r\n      result of this License or out of the use or inability to use the\r\n      Work (including but not limited to damages for loss of goodwill,\r\n      work stoppage, computer failure or malfunction, or any and all\r\n      other commercial damages or losses), even if such Contributor\r\n      has been advised of the possibility of such damages.\r\n\r\n   9. Accepting Warranty or Additional Liability. While redistributing\r\n      the Work or Derivative Works thereof, You may choose to offer,\r\n      and charge a fee for, acceptance of support, warranty, indemnity,\r\n      or other liability obligations and/or rights consistent with this\r\n      License. However, in accepting such obligations, You may act only\r\n      on Your own behalf and on Your sole responsibility, not on behalf\r\n      of any other Contributor, and only if You agree to indemnify,\r\n      defend, and hold each Contributor harmless for any liability\r\n      incurred by, or claims asserted against, such Contributor by reason\r\n      of your accepting any such warranty or additional liability.\r\n\r\n   END OF TERMS AND CONDITIONS\r\n\r\n   APPENDIX: How to apply the Apache License to your work.\r\n\r\n      To apply the Apache License to your work, attach the following\r\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\r\n      replaced with your own identifying information. (Don't include\r\n      the brackets!)  The text should be enclosed in the appropriate\r\n      comment syntax for the file format. We also recommend that a\r\n      file or class name and description of purpose be included on the\r\n      same \"printed page\" as the copyright notice for easier\r\n      identification within third-party archives.\r\n\r\n   Copyright (c) Microsoft Corporation.\r\n\r\n   Licensed under the Apache License, Version 2.0 (the \"License\");\r\n   you may not use this file except in compliance with the License.\r\n   You may obtain a copy of the License at\r\n\r\n       http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n   Unless required by applicable law or agreed to in writing, software\r\n   distributed under the License is distributed on an \"AS IS\" BASIS,\r\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n   See the License for the specific language governing permissions and\r\n   limitations under the License.\r\n"
  },
  {
    "path": "skills/.curated/slides/SKILL.md",
    "content": "---\nname: slides\ndescription: Create and edit presentation slide decks (`.pptx`) with PptxGenJS, bundled layout helpers, and render/validation utilities. Use when tasks involve building a new PowerPoint deck, recreating slides from screenshots/PDFs/reference decks, modifying slide content while preserving editable output, adding charts/diagrams/visuals, or diagnosing layout issues such as overflow, overlaps, and font substitution.\n---\n\n# Slides\n\n## Overview\n\nUse PptxGenJS for slide authoring. Do not use `python-pptx` for deck generation unless the task is inspection-only; keep editable output in JavaScript and deliver both the `.pptx` and the source `.js`.\n\nKeep work in a task-local directory. Only copy final artifacts to the requested destination after rendering and validation pass.\n\n## Bundled Resources\n\n- `assets/pptxgenjs_helpers/`: Copy this folder into the deck workspace and import it locally instead of reimplementing helper logic.\n- `scripts/render_slides.py`: Rasterize a `.pptx` or `.pdf` to per-slide PNGs.\n- `scripts/slides_test.py`: Detect content that overflows the slide canvas.\n- `scripts/create_montage.py`: Build a contact-sheet style montage of rendered slides.\n- `scripts/detect_font.py`: Report missing or substituted fonts as LibreOffice resolves them.\n- `scripts/ensure_raster_image.py`: Convert SVG/EMF/HEIC/PDF-like assets into PNGs for quick inspection.\n- `references/pptxgenjs-helpers.md`: Load only when you need API details or dependency notes.\n\n## Workflow\n\n1. Inspect the request and determine whether you are creating a new deck, recreating an existing deck, or editing one.\n2. Set the slide size up front. Default to 16:9 (`LAYOUT_WIDE`) unless the source material clearly uses another aspect ratio.\n3. Copy `assets/pptxgenjs_helpers/` into the working directory and import the helpers from there.\n4. Build the deck in JavaScript with an explicit theme font, stable spacing, and editable PowerPoint-native elements when practical.\n5. Run the bundled scripts from this skill directory or copy the needed ones into the task workspace. Render the result with `render_slides.py`, review the PNGs, and fix layout issues before delivery.\n6. Run `slides_test.py` for overflow checks when slide edges are tight or the deck is dense.\n7. Deliver the `.pptx`, the authoring `.js`, and any generated assets that are required to rebuild the deck.\n\n## Authoring Rules\n\n- Set theme fonts explicitly. Do not rely on PowerPoint defaults if typography matters.\n- Use `autoFontSize`, `calcTextBox`, and related helpers to size text boxes; do not use PptxGenJS `fit` or `autoFit`.\n- Use bullet options, not literal `•` characters.\n- Use `imageSizingCrop` or `imageSizingContain` instead of PptxGenJS built-in image sizing.\n- Use `latexToSvgDataUri()` for equations and `codeToRuns()` for syntax-highlighted code blocks.\n- Prefer native PowerPoint charts for simple bar/line/pie/histogram style visuals so reviewers can edit them later.\n- For charts or diagrams that PptxGenJS cannot express well, render SVG externally and place the SVG in the slide.\n- Include both `warnIfSlideHasOverlaps(slide, pptx)` and `warnIfSlideElementsOutOfBounds(slide, pptx)` in the submitted JavaScript whenever you generate or substantially edit slides.\n- Fix all unintentional overlap and out-of-bounds warnings before delivering. If an overlap is intentional, leave a short code comment near the relevant element.\n\n## Recreate Or Edit Existing Slides\n\n- Render the source deck or reference PDF first so you can compare slide geometry visually.\n- Match the original aspect ratio before rebuilding layout.\n- Preserve editability where possible: text should stay text, and simple charts should stay native charts.\n- If a reference slide uses raster artwork, use `ensure_raster_image.py` to generate debug PNGs from vector or odd image formats before placing them.\n\n## Validation Commands\n\nExamples below assume you copied the needed scripts into the working directory. If not, invoke the same script paths relative to this skill folder.\n\n```bash\n# Render slides to PNGs for review\npython3 scripts/render_slides.py deck.pptx --output_dir rendered\n\n# Build a montage for quick scanning\npython3 scripts/create_montage.py --input_dir rendered --output_file montage.png\n\n# Check for overflow beyond the original slide canvas\npython3 scripts/slides_test.py deck.pptx\n\n# Detect missing or substituted fonts\npython3 scripts/detect_font.py deck.pptx --json\n```\n\nLoad `references/pptxgenjs-helpers.md` if you need the helper API summary or dependency details.\n"
  },
  {
    "path": "skills/.curated/slides/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Slides\"\n  short_description: \"Create and edit PPTX slide decks\"\n  icon_small: \"./assets/slides-small.svg\"\n  icon_large: \"./assets/slides.png\"\n  default_prompt: \"Use $slides to create or update this PPTX slide deck with PptxGenJS and validate the layout.\"\n"
  },
  {
    "path": "skills/.curated/slides/assets/pptxgenjs_helpers/code.js",
    "content": "// Copyright (c) OpenAI. All rights reserved.\n\"use strict\";\n\nconst fs = require(\"fs\");\nconst Prism = require(\"prismjs\");\nlet THEME_MAP;\n\nfunction loadPrismLanguage(lang) {\n  const normalized = String(lang || \"plaintext\").toLowerCase();\n  const known = new Set([\n    \"markup\",\n    \"html\",\n    \"xml\",\n    \"svg\",\n    \"mathml\",\n    \"css\",\n    \"clike\",\n    \"javascript\",\n    \"js\",\n    \"typescript\",\n    \"ts\",\n    \"python\",\n    \"py\",\n    \"bash\",\n    \"sh\",\n    \"json\",\n    \"yaml\",\n    \"yml\",\n  ]);\n  const map = {\n    js: \"javascript\",\n    ts: \"typescript\",\n    py: \"python\",\n    sh: \"bash\",\n    yml: \"yaml\",\n    html: \"markup\",\n    xml: \"markup\",\n  };\n  const id = map[normalized] || normalized;\n  if (!Prism.languages[id]) {\n    try {\n      require(`prismjs/components/prism-${id}`);\n    } catch (_e) {}\n  }\n  return Prism.languages[id] || Prism.languages.plain || {};\n}\n\nfunction buildThemeMap(themeCssModule = \"prismjs/themes/prism-okaidia.css\") {\n  try {\n    const css = fs.readFileSync(require.resolve(themeCssModule), \"utf8\");\n    return Object.fromEntries(\n      [\n        ...css.matchAll(\n          /\\.token\\.([\\w-]+)[^{]*\\{[^}]*color:\\s*([^;\\s]+)[^}]*\\}/g\n        ),\n      ].map(([, t, c]) => [t, c.replace(/#|!important/g, \"\").trim()])\n    );\n  } catch (err) {\n    return { plain: \"FFFFFF\", comment: \"999999\" };\n  }\n}\n\nfunction getThemeMap() {\n  if (!THEME_MAP) THEME_MAP = buildThemeMap();\n  return THEME_MAP;\n}\n\nfunction run(text, type = \"plain\") {\n  const theme = getThemeMap();\n  return {\n    text,\n    options: {\n      fontFace: \"Consolas\",\n      color: theme[type] || theme.plain || \"FFFFFF\",\n      fontSize: 14,\n    },\n  };\n}\n\nfunction tokensToRuns(tokens) {\n  return tokens.flatMap((t) =>\n    typeof t === \"string\"\n      ? [run(t)]\n      : Array.isArray(t.content)\n      ? tokensToRuns(t.content)\n      : [run(t.content, t.type)]\n  );\n}\n\nfunction codeToRuns(code, lang) {\n  const grammar = loadPrismLanguage(lang);\n  const lines = String(code || \"\").split(\"\\n\");\n  const pad = lines.length.toString().length;\n  return lines.flatMap((line, i) => [\n    run(`${(i + 1).toString().padStart(pad, \" \")} `, \"comment\"),\n    ...tokensToRuns(Prism.tokenize(line, grammar)),\n    ...(i < lines.length - 1 ? [run(\"\\n\")] : []),\n  ]);\n}\n\nmodule.exports = {\n  codeToRuns,\n  buildThemeMap,\n};\n"
  },
  {
    "path": "skills/.curated/slides/assets/pptxgenjs_helpers/image.js",
    "content": "// Copyright (c) OpenAI. All rights reserved.\n\"use strict\";\n\nconst fs = require(\"fs\");\n\n// Accept either a filesystem path, a data URI, raw SVG string, or a Buffer\n// and normalize to a Buffer for type/size probing.\nfunction readInputAsBuffer(source) {\n  if (!source) throw new Error(\"Image source is empty\");\n  if (Buffer.isBuffer(source)) return { buffer: source, type: \"buffer\" };\n  if (typeof source === \"string\") {\n    // data URI (we primarily emit base64 data URIs for SVG via helpers)\n    if (source.startsWith(\"data:\")) {\n      const type = \"dataUri\";\n      const comma = source.indexOf(\",\");\n      const payload = comma !== -1 ? source.slice(comma + 1) : source;\n      // Our helpers use base64; if not, try URI decode then treat as raw text\n      try {\n        return { buffer: Buffer.from(payload, \"base64\"), type: type };\n      } catch (_e) {\n        try {\n          return {\n            buffer: Buffer.from(decodeURIComponent(payload), \"utf8\"),\n            type: type,\n          };\n        } catch (_e2) {\n          return { buffer: Buffer.from(payload, \"utf8\"), type: type };\n        }\n      }\n    }\n    // Raw inline SVG string\n    if (source.includes(\"<svg\")) {\n      return { buffer: Buffer.from(source, \"utf8\"), type: \"rawSvg\" };\n    }\n    // Treat as filesystem path\n    return { buffer: fs.readFileSync(source), type: \"path\" };\n  }\n  throw new Error(\"Unsupported image source type\");\n}\n\nfunction isPng(buf) {\n  return (\n    buf.length >= 24 &&\n    buf[0] === 0x89 &&\n    buf[1] === 0x50 &&\n    buf[2] === 0x4e &&\n    buf[3] === 0x47 &&\n    buf[4] === 0x0d &&\n    buf[5] === 0x0a &&\n    buf[6] === 0x1a &&\n    buf[7] === 0x0a\n  );\n}\n\nfunction isJpeg(buf) {\n  return (\n    buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff\n  );\n}\n\nfunction isGif(buf) {\n  return (\n    buf.length >= 10 &&\n    buf[0] === 0x47 &&\n    buf[1] === 0x49 &&\n    buf[2] === 0x46 &&\n    buf[3] === 0x38 &&\n    (buf[4] === 0x39 || buf[4] === 0x37) &&\n    buf[5] === 0x61\n  );\n}\n\nfunction isWebp(buf) {\n  return (\n    buf.length >= 16 &&\n    buf[0] === 0x52 &&\n    buf[1] === 0x49 &&\n    buf[2] === 0x46 &&\n    buf[3] === 0x46 &&\n    buf[8] === 0x57 &&\n    buf[9] === 0x45 &&\n    buf[10] === 0x42 &&\n    buf[11] === 0x50\n  );\n}\n\nfunction isSvg(buf) {\n  const head = buf.slice(0, 200).toString(\"utf8\");\n  return head.includes(\"<svg\");\n}\n\nfunction readPngSize(buf) {\n  // IHDR chunk: width/height at offset 16, big-endian\n  const width = buf.readUInt32BE(16);\n  const height = buf.readUInt32BE(20);\n  return { width, height, type: \"png\" };\n}\n\nfunction readGifSize(buf) {\n  const width = buf.readUInt16LE(6);\n  const height = buf.readUInt16LE(8);\n  return { width, height, type: \"gif\" };\n}\n\nfunction readWebpSize(buf) {\n  // Handle VP8X, VP8L, VP8\n  // Reference: https://developers.google.com/speed/webp/docs/riff_container\n  const riffSize = buf.readUInt32LE(4) + 8;\n  let offset = 12; // start of first chunk\n  while (offset + 8 <= riffSize && offset + 8 <= buf.length) {\n    const chunkTag = buf.slice(offset, offset + 4).toString(\"ascii\");\n    const chunkSize = buf.readUInt32LE(offset + 4);\n    if (chunkTag === \"VP8X\") {\n      // Canvas size stored at bytes 12..17 (6 bytes), 24 bits each minus 1\n      const wMinus1 = buf.readUIntLE(offset + 12, 3);\n      const hMinus1 = buf.readUIntLE(offset + 15, 3);\n      return { width: wMinus1 + 1, height: hMinus1 + 1, type: \"webp\" };\n    }\n    if (chunkTag === \"VP8 \") {\n      // Lossy bitstream: frame header at start of data\n      // Parse minimally for width/height\n      const start = offset + 8;\n      if (start + 10 < buf.length) {\n        const width = buf.readUInt16LE(start + 6) & 0x3fff;\n        const height = buf.readUInt16LE(start + 8) & 0x3fff;\n        return { width, height, type: \"webp\" };\n      }\n    }\n    if (chunkTag === \"VP8L\") {\n      // Lossless bitstream: 14-bit width/height encoded\n      const start = offset + 8;\n      if (start + 5 <= buf.length) {\n        const b0 = buf[start + 1];\n        const b1 = buf[start + 2];\n        const b2 = buf[start + 3];\n        const b3 = buf[start + 4];\n        const width = 1 + (((b1 & 0x3f) << 8) | b0);\n        const height =\n          1 + (((b3 & 0xf) << 10) | (b2 << 2) | ((b1 & 0xc0) >> 6));\n        return { width, height, type: \"webp\" };\n      }\n    }\n    offset += 8 + ((chunkSize + 1) & ~1); // chunks are padded to even size\n  }\n  throw new Error(\"Unsupported WEBP variant for size detection\");\n}\n\nfunction readJpegSize(buf) {\n  let offset = 2;\n  while (offset < buf.length) {\n    if (buf[offset] !== 0xff) {\n      offset++;\n      continue;\n    }\n    const marker = buf[offset + 1];\n    // SOF0..SOF3, SOF5..SOF7, SOF9..SOF11, SOF13..SOF15\n    if (\n      (marker >= 0xc0 && marker <= 0xc3) ||\n      (marker >= 0xc5 && marker <= 0xc7) ||\n      (marker >= 0xc9 && marker <= 0xcb) ||\n      (marker >= 0xcd && marker <= 0xcf)\n    ) {\n      const blockLength = buf.readUInt16BE(offset + 2);\n      const height = buf.readUInt16BE(offset + 5);\n      const width = buf.readUInt16BE(offset + 7);\n      return { width, height, type: \"jpeg\" };\n    }\n    const blockLength = buf.readUInt16BE(offset + 2);\n    if (!Number.isFinite(blockLength) || blockLength < 2) break;\n    offset += 2 + blockLength;\n  }\n  throw new Error(\"JPEG size not found\");\n}\n\nfunction parseSvgSize(buf) {\n  const text = buf.toString(\"utf8\");\n  const a = text.indexOf(\"<svg\");\n  const b = text.indexOf(\"</svg>\");\n  const inner = a !== -1 && b !== -1 ? text.slice(a, b + 6) : text;\n  const widthMatch = inner.match(/\\bwidth\\s*=\\s*\"([^\"]+)\"/i);\n  const heightMatch = inner.match(/\\bheight\\s*=\\s*\"([^\"]+)\"/i);\n  const viewBoxMatch = inner.match(/\\bviewBox\\s*=\\s*\"([^\"]+)\"/i);\n\n  function toPx(v) {\n    if (!v) return null;\n    const m = String(v)\n      .trim()\n      .match(/([0-9.]+)\\s*(px|pt|em|ex|cm|mm|in|%)?/i);\n    if (!m) return null;\n    const n = parseFloat(m[1]);\n    const unit = (m[2] || \"px\").toLowerCase();\n    const dpi = 96;\n    switch (unit) {\n      case \"px\":\n        return n;\n      case \"pt\":\n        return (n * dpi) / 72;\n      case \"in\":\n        return n * dpi;\n      case \"cm\":\n        return (n * dpi) / 2.54;\n      case \"mm\":\n        return (n * dpi) / 25.4;\n      case \"em\":\n      case \"ex\":\n        return n * 16; // rough fallback\n      default:\n        return null;\n    }\n  }\n\n  let widthPx = widthMatch ? toPx(widthMatch[1]) : null;\n  let heightPx = heightMatch ? toPx(heightMatch[1]) : null;\n  if ((widthPx == null || heightPx == null) && viewBoxMatch) {\n    const parts = viewBoxMatch[1].trim().split(/\\s+/).map(Number);\n    if (parts.length === 4) {\n      const vbw = parts[2];\n      const vbh = parts[3];\n      if (!widthPx && vbh) widthPx = vbw;\n      if (!heightPx && vbw) heightPx = vbh;\n    }\n  }\n  if (!widthPx || !heightPx) {\n    // Fallback if sizes missing\n    widthPx = widthPx || 100;\n    heightPx = heightPx || 100;\n  }\n  return { width: widthPx, height: heightPx, type: \"svg\" };\n}\n\nfunction getImageDimensions(pathOrData) {\n  const { buffer: buf, type } = readInputAsBuffer(pathOrData);\n  let meta;\n  if (isPng(buf)) meta = readPngSize(buf);\n  else if (isJpeg(buf)) meta = readJpegSize(buf);\n  else if (isGif(buf)) meta = readGifSize(buf);\n  else if (isWebp(buf)) meta = readWebpSize(buf);\n  else if (isSvg(buf)) meta = parseSvgSize(buf);\n  else {\n    const suffix =\n      type === \"path\" && typeof pathOrData === \"string\"\n        ? ` (path: ${pathOrData})`\n        : \"\";\n    throw new Error(\"Unsupported image format for provided source\" + suffix);\n  }\n\n  const aspectRatio =\n    meta.width > 0 && meta.height > 0 ? meta.width / meta.height : 1;\n  return {\n    width: meta.width,\n    height: meta.height,\n    aspectRatio,\n    type: meta.type,\n  };\n}\n\nfunction imageSizingCrop(source, x, y, w, h, cx, cy, cw, ch) {\n  const { aspectRatio } = getImageDimensions(source);\n  const boxAspect = w / h;\n\n  if (\n    cx === undefined ||\n    cy === undefined ||\n    cw === undefined ||\n    ch === undefined\n  ) {\n    let cropXFrac, cropYFrac, cropWFrac, cropHFrac;\n    if (aspectRatio >= boxAspect) {\n      cropHFrac = 1;\n      cropWFrac = boxAspect / aspectRatio;\n      cropXFrac = (1 - cropWFrac) / 2;\n      cropYFrac = 0;\n    } else {\n      cropWFrac = 1;\n      cropHFrac = aspectRatio / boxAspect;\n      cropXFrac = 0;\n      cropYFrac = (1 - cropHFrac) / 2;\n    }\n    cx = cropXFrac;\n    cy = cropYFrac;\n    cw = cropWFrac;\n    ch = cropHFrac;\n  }\n\n  let virtualW = w / cw;\n  let virtualH = virtualW / aspectRatio;\n  const eps = 1e-6;\n  if (Math.abs(virtualH * ch - h) > eps) {\n    virtualH = h / ch;\n    virtualW = virtualH * aspectRatio;\n  }\n\n  const cropXIn = cx * virtualW;\n  const cropYIn = cy * virtualH;\n  return {\n    x,\n    y,\n    w: virtualW,\n    h: virtualH,\n    sizing: {\n      type: \"crop\",\n      x: cropXIn,\n      y: cropYIn,\n      w: w,\n      h: h,\n    },\n  };\n}\n\nfunction imageSizingContain(source, x, y, w, h) {\n  const { aspectRatio } = getImageDimensions(source);\n  let w2, h2;\n  const boxAspect = w / h;\n  if (aspectRatio >= boxAspect) {\n    w2 = w;\n    h2 = w2 / aspectRatio;\n  } else {\n    h2 = h;\n    w2 = h2 * aspectRatio;\n  }\n  return {\n    x: x + (w - w2) / 2,\n    y: y + (h - h2) / 2,\n    w: w2,\n    h: h2,\n  };\n}\n\nmodule.exports = {\n  getImageDimensions,\n  imageSizingCrop,\n  imageSizingContain,\n};\n"
  },
  {
    "path": "skills/.curated/slides/assets/pptxgenjs_helpers/index.js",
    "content": "// Copyright (c) OpenAI. All rights reserved.\n\"use strict\";\n\nconst VERSION = \"1.2.0\";\n\nconst text = require(\"./text\");\nconst image = require(\"./image\");\nconst svg = require(\"./svg\");\nconst latex = require(\"./latex\");\nconst code = require(\"./code\");\nconst layout = require(\"./layout\");\nconst layoutBuilders = require(\"./layout_builders\");\nconst util = require(\"./util\");\n\nmodule.exports = {\n  VERSION,\n  // text layout\n  ...text,\n  // images\n  ...image,\n  // svg helpers\n  ...svg,\n  // LaTeX -> SVG\n  ...latex,\n  // code block -> pptx text runs\n  ...code,\n  // slide layout analyzers\n  ...layout,\n  // slide layout builders\n  ...layoutBuilders,\n  // text layout helpers and utilities\n  ...util,\n};\n"
  },
  {
    "path": "skills/.curated/slides/assets/pptxgenjs_helpers/latex.js",
    "content": "// Copyright (c) OpenAI. All rights reserved.\n\"use strict\";\n\nlet _mathjax;\nlet _adaptor;\nlet _doc;\n\nfunction ensureMathJax() {\n  if (_mathjax && _adaptor && _doc) return;\n  try {\n    const { mathjax } = require(\"mathjax-full/js/mathjax.js\");\n    const { TeX } = require(\"mathjax-full/js/input/tex.js\");\n    const { SVG } = require(\"mathjax-full/js/output/svg.js\");\n    const { liteAdaptor } = require(\"mathjax-full/js/adaptors/liteAdaptor.js\");\n    const { RegisterHTMLHandler } = require(\"mathjax-full/js/handlers/html.js\");\n    const { AllPackages } = require(\"mathjax-full/js/input/tex/AllPackages.js\");\n\n    _adaptor = liteAdaptor();\n    RegisterHTMLHandler(_adaptor);\n    const tex = new TeX({ packages: AllPackages });\n    const out = new SVG({ fontCache: \"local\" });\n    _doc = mathjax.document(\"\", { InputJax: tex, OutputJax: out });\n    _mathjax = mathjax;\n  } catch (err) {\n    throw new Error(\n      \"mathjax-full is not installed. Run `npm i mathjax-full` or avoid latexToSvgDataUri().\"\n    );\n  }\n}\n\nfunction latexToSvgDataUri(latex, display = true) {\n  ensureMathJax();\n  const html = _adaptor.outerHTML(_doc.convert(latex, { display }));\n  const a = html.indexOf(\"<svg\");\n  const b = html.indexOf(\"</svg>\");\n  let svg = a !== -1 && b !== -1 ? html.slice(a, b + 6) : html;\n  svg = svg.replace(/<\\?xml[^>]*>/g, \"\");\n  if (!/xmlns=\\\"http:\\/\\/www\\.w3\\.org\\/2000\\/svg\\\"/.test(svg)) {\n    svg = svg.replace(/<svg /, '<svg xmlns=\"http://www.w3.org/2000/svg\" ');\n  }\n  svg = svg.replace(/(width|height)=\\\"([0-9.]+)(ex|em)\\\"/g, (_m, attr, num) => {\n    const px = Math.round(parseFloat(num) * 8.5);\n    return `${attr}=\"${px}px\"`;\n  });\n  svg = svg.replace(/currentColor/g, \"#000000\");\n  return \"data:image/svg+xml;base64,\" + Buffer.from(svg).toString(\"base64\");\n}\n\nmodule.exports = {\n  latexToSvgDataUri,\n};\n"
  },
  {
    "path": "skills/.curated/slides/assets/pptxgenjs_helpers/layout.js",
    "content": "// Copyright (c) OpenAI. All rights reserved.\n\"use strict\";\n\nfunction inferElementType(obj) {\n  if (!obj) return \"unknown\";\n  const data = obj.data || obj.options || {};\n  // Distinguish lines explicitly via type only. Many objects have a 'line' style; don't misclassify those.\n  if (obj.type === \"line\") return \"line\";\n  if (obj.type && typeof obj.type === \"string\") return obj.type;\n  if (obj.text || typeof data.text === \"string\") return \"text\";\n  if (data.path || obj.image) return \"image\";\n  if (data.chartType) return \"chart\";\n  if (data.shape || data.line) return \"shape\";\n  if (data.mediaType) return \"media\";\n  if (data.table || Array.isArray(data.rows)) return \"table\";\n  if (data.smartArt) return \"smartart\";\n  return \"unknown\";\n}\n\nconst TEXT_OVERLAP_ERROR_THRESHOLD = 0.1;\nconst RECTIFY_DIRECTION_EQUALITY_TOLERANCE = 0.15;\n\nfunction warnIfSlideHasOverlaps(slide, pptx, options = {}) {\n  if (!slide || !Array.isArray(slide._slideObjects)) {\n    console.warn(\"Invalid slide object passed to warnIfSlideOverlaps()\");\n    return;\n  }\n  const opts = {\n    // By default, containment cases are very common (e.g., full-slide backgrounds)\n    // and usually not actionable. Mute them unless explicitly requested.\n    muteContainment:\n      options.muteContainment !== undefined ? options.muteContainment : true,\n    // Do NOT ignore lines or decorative shapes by default; users want true overlaps.\n    ignoreLines:\n      options.ignoreLines !== undefined ? options.ignoreLines : false,\n    ignoreDecorativeShapes:\n      options.ignoreDecorativeShapes !== undefined\n        ? options.ignoreDecorativeShapes\n        : false,\n  };\n  const slideIndex =\n    pptx && Array.isArray(pptx._slides) ? pptx._slides.indexOf(slide) : -1;\n  const slideLabel =\n    slideIndex >= 0 ? `Slide ${slideIndex + 1}` : \"(Unknown slide index)\";\n  const formatElement = (el) => {\n    const cx = (el.x + el.w / 2).toFixed(3);\n    const cy = (el.y + el.h / 2).toFixed(3);\n    return `element ${el.index} (${el.type}, center_x=${cx}, center_y=${cy})`;\n  };\n  const elements = slide._slideObjects.map((obj, i) => {\n    const {\n      x = 0,\n      y = 0,\n      w = 0,\n      h = 0,\n      fill,\n      line,\n    } = obj.data || obj.options || {};\n    const type = inferElementType(obj);\n    const isDecorative = (() => {\n      if (!opts.ignoreDecorativeShapes) return false;\n      // Border rectangles used as frames: transparent fill (or fully transparent) with a stroke\n      const transparency =\n        typeof fill?.transparency === \"number\" ? fill.transparency : null;\n      const hasOnlyBorder = !!line && (!fill || transparency !== null);\n      const fullyTransparent = transparency !== null && transparency >= 99;\n      return type === \"shape\" && hasOnlyBorder && fullyTransparent;\n    })();\n    const ignorable = (opts.ignoreLines && type === \"line\") || isDecorative;\n    return { index: i, type, x, y, w, h, ignorable };\n  });\n  let overlapCount = 0;\n  let containmentCount = 0;\n  for (let i = 0; i < elements.length; i++) {\n    const a = elements[i];\n    if (a.ignorable) continue;\n    for (let j = i + 1; j < elements.length; j++) {\n      const b = elements[j];\n      if (b.ignorable) continue;\n      const comparison = compareElementPosition(slide, a.index, b.index);\n      if (comparison.relation === \"overlapping\") {\n        // Special-case: diagonal line's bounding box overlapping a rectangle is often a false positive.\n        const EPS = 1e-6;\n        const getBounds = (e) => ({\n          x: e.x,\n          y: e.y,\n          x2: e.x + e.w,\n          y2: e.y + e.h,\n        });\n        const lineRectFalsePositive = (() => {\n          const oneIsLine = (a.type === \"line\") ^ (b.type === \"line\");\n          if (!oneIsLine) return false;\n          const line = a.type === \"line\" ? a : b;\n          const rect = a.type === \"line\" ? b : a;\n          // If line is diagonal, verify actual segment intersects rect; if not, ignore.\n          const isDiagonal = line.w > EPS && line.h > EPS;\n          const lineSeg = {\n            x1: line.x,\n            y1: line.y,\n            x2: line.x + line.w,\n            y2: line.y + line.h,\n          };\n          const rectB = getBounds(rect);\n          const pointInRect = (px, py, rb) =>\n            px >= rb.x - EPS &&\n            px <= rb.x2 + EPS &&\n            py >= rb.y - EPS &&\n            py <= rb.y2 + EPS;\n          const segsIntersect = (p1, p2, q1, q2) => {\n            const cross = (ax, ay, bx, by) => ax * by - ay * bx;\n            const d1x = p2.x - p1.x,\n              d1y = p2.y - p1.y;\n            const d2x = q2.x - q1.x,\n              d2y = q2.y - q1.y;\n            const denom = cross(d1x, d1y, d2x, d2y);\n            if (Math.abs(denom) < EPS) {\n              // Parallel: check colinearity and overlapping projections\n              const crossCol = cross(q1.x - p1.x, q1.y - p1.y, d1x, d1y);\n              if (Math.abs(crossCol) > EPS) return false;\n              const proj = (a, b, c) =>\n                Math.min(Math.max(a, b), Math.max(Math.min(a, b), c));\n              const overlapX = !(\n                Math.max(p1.x, p2.x) < Math.min(q1.x, q2.x) - EPS ||\n                Math.max(q1.x, q2.x) < Math.min(p1.x, p2.x) - EPS\n              );\n              const overlapY = !(\n                Math.max(p1.y, p2.y) < Math.min(q1.y, q2.y) - EPS ||\n                Math.max(q1.y, q2.y) < Math.min(p1.y, p2.y) - EPS\n              );\n              return overlapX && overlapY;\n            }\n            const t = cross(q1.x - p1.x, q1.y - p1.y, d2x, d2y) / denom;\n            const u = cross(q1.x - p1.x, q1.y - p1.y, d1x, d1y) / denom;\n            return t >= -EPS && t <= 1 + EPS && u >= -EPS && u <= 1 + EPS;\n          };\n          const intersectsRect = (seg, rb) => {\n            if (\n              pointInRect(seg.x1, seg.y1, rb) ||\n              pointInRect(seg.x2, seg.y2, rb)\n            )\n              return true;\n            const r1 = { x: rb.x, y: rb.y },\n              r2 = { x: rb.x2, y: rb.y },\n              r3 = { x: rb.x2, y: rb.y2 },\n              r4 = { x: rb.x, y: rb.y2 };\n            const p1 = { x: seg.x1, y: seg.y1 },\n              p2 = { x: seg.x2, y: seg.y2 };\n            return (\n              segsIntersect(p1, p2, r1, r2) ||\n              segsIntersect(p1, p2, r2, r3) ||\n              segsIntersect(p1, p2, r3, r4) ||\n              segsIntersect(p1, p2, r4, r1)\n            );\n          };\n          return isDiagonal && !intersectsRect(lineSeg, rectB);\n        })();\n        if (!lineRectFalsePositive) {\n          overlapCount++;\n\n          const severeTextOverlap = (() => {\n            if (!comparison.intersection) return false;\n            const exceedsThreshold = (element) =>\n              element.type === \"text\" &&\n              comparison.intersection.w >= TEXT_OVERLAP_ERROR_THRESHOLD &&\n              comparison.intersection.h >= TEXT_OVERLAP_ERROR_THRESHOLD;\n            return exceedsThreshold(a) || exceedsThreshold(b);\n          })();\n          if (severeTextOverlap) {\n            const overlapW = comparison.intersection.w;\n            const overlapH = comparison.intersection.h;\n            let rectificationSuggestion = \"\";\n            if (overlapW > EPS && overlapH > EPS) {\n              const maxOverlap = Math.max(overlapW, overlapH);\n              const diffRatio = Math.abs(overlapW - overlapH) / maxOverlap;\n              const directions = [];\n              // Attempt to determine the primary direction of the overlap. This is the direction\n              // in which the overlap is smaller (and so requires the smallest adjustment to rectify).\n              if (diffRatio <= RECTIFY_DIRECTION_EQUALITY_TOLERANCE) {\n                directions.push(\"horizontally\", \"vertically\");\n              } else if (overlapW < overlapH) {\n                directions.push(\"horizontally\");\n              } else {\n                directions.push(\"vertically\");\n              }\n              rectificationSuggestion = `Suggestion: reposition elements ${directions.join(\n                \" and \"\n              )}.`;\n            }\n\n            console.error(\n              `❌ ${slideLabel}: Severe text overlap detected between ${formatElement(\n                a\n              )} and ${formatElement(\n                b\n              )} (overlap_horizontal=${comparison.intersection.w.toFixed(\n                3\n              )}, overlap_vertical=${comparison.intersection.h.toFixed(\n                3\n              )}). THIS MUST BE FIXED. ${rectificationSuggestion}`\n            );\n          } else {\n            console.warn(\n              `⚠️ ${slideLabel}: Overlap detected between ${formatElement(\n                a\n              )} and ${formatElement(b)}.`\n            );\n          }\n        }\n      } else if (comparison.relation === \"contained\") {\n        if (!opts.muteContainment) {\n          containmentCount++;\n          const container = elements[comparison.containerIndex];\n          const contained = elements[comparison.containedIndex];\n          console.warn(\n            `⚠️ ${slideLabel}: ${formatElement(\n              contained\n            )} is fully contained within ${formatElement(container)}`\n          );\n        } else {\n          // Still count internally when muted? We keep for summary only when un-muted\n        }\n      }\n    }\n  }\n  if (!(overlapCount === 0 && (!containmentCount || opts.muteContainment))) {\n    const issues = [];\n    if (overlapCount > 0) issues.push(`${overlapCount} overlapping pair(s)`);\n    if (!opts.muteContainment && containmentCount > 0)\n      issues.push(`${containmentCount} containment case(s)`);\n    console.log(`⚠️ ${slideLabel}: Found ${issues.join(\" and \")}.`);\n  }\n}\n\nfunction compareElementPosition(slide, firstIndex, secondIndex) {\n  if (!slide || !Array.isArray(slide._slideObjects)) {\n    throw new Error(\"Invalid slide object passed to compareElementPosition()\");\n  }\n  if (\n    typeof firstIndex !== \"number\" ||\n    typeof secondIndex !== \"number\" ||\n    !Number.isInteger(firstIndex) ||\n    !Number.isInteger(secondIndex)\n  ) {\n    throw new Error(\"Element indices must be integer values.\");\n  }\n  const elements = slide._slideObjects;\n  if (\n    firstIndex < 0 ||\n    firstIndex >= elements.length ||\n    secondIndex < 0 ||\n    secondIndex >= elements.length\n  ) {\n    throw new Error(\n      \"Element index out of bounds for compareElementPosition().\"\n    );\n  }\n  const EPS = 1e-4;\n  const getBounds = (obj) => {\n    const source = obj?.data || obj?.options || {};\n    let x = typeof source.x === \"number\" ? source.x : 0;\n    let y = typeof source.y === \"number\" ? source.y : 0;\n    let w = typeof source.w === \"number\" ? source.w : 0;\n    let h = typeof source.h === \"number\" ? source.h : 0;\n    if (source.sizing && source.sizing.type === \"crop\") {\n      if (typeof source.sizing.w === \"number\") w = source.sizing.w;\n      if (typeof source.sizing.h === \"number\") h = source.sizing.h;\n    }\n    return { x, y, w, h, x2: x + w, y2: y + h };\n  };\n  const boundsA = getBounds(elements[firstIndex]);\n  const boundsB = getBounds(elements[secondIndex]);\n  const separated =\n    boundsA.x2 < boundsB.x - EPS ||\n    boundsB.x2 < boundsA.x - EPS ||\n    boundsA.y2 < boundsB.y - EPS ||\n    boundsB.y2 < boundsA.y - EPS;\n  if (separated) {\n    return {\n      relation: \"disjoint\",\n      containerIndex: null,\n      containedIndex: null,\n      aBounds: boundsA,\n      bBounds: boundsB,\n      intersection: null,\n    };\n  }\n  const aContainsB =\n    boundsA.x <= boundsB.x + EPS &&\n    boundsA.y <= boundsB.y + EPS &&\n    boundsA.x2 >= boundsB.x2 - EPS &&\n    boundsA.y2 >= boundsB.y2 - EPS;\n  const bContainsA =\n    boundsB.x <= boundsA.x + EPS &&\n    boundsB.y <= boundsA.y + EPS &&\n    boundsB.x2 >= boundsA.x2 - EPS &&\n    boundsB.y2 >= boundsA.y2 - EPS;\n  const ix1 = Math.max(boundsA.x, boundsB.x);\n  const iy1 = Math.max(boundsA.y, boundsB.y);\n  const ix2 = Math.min(boundsA.x2, boundsB.x2);\n  const iy2 = Math.min(boundsA.y2, boundsB.y2);\n  const intersectionWidth = Math.max(0, ix2 - ix1);\n  const intersectionHeight = Math.max(0, iy2 - iy1);\n  const intersection =\n    intersectionWidth > EPS && intersectionHeight > EPS\n      ? { x: ix1, y: iy1, w: intersectionWidth, h: intersectionHeight }\n      : null;\n  if (aContainsB && !bContainsA) {\n    return {\n      relation: \"contained\",\n      containerIndex: firstIndex,\n      containedIndex: secondIndex,\n      aBounds: boundsA,\n      bBounds: boundsB,\n      intersection,\n    };\n  }\n  if (bContainsA && !aContainsB) {\n    return {\n      relation: \"contained\",\n      containerIndex: secondIndex,\n      containedIndex: firstIndex,\n      aBounds: boundsA,\n      bBounds: boundsB,\n      intersection,\n    };\n  }\n  if (intersection) {\n    return {\n      relation: \"overlapping\",\n      containerIndex: null,\n      containedIndex: null,\n      aBounds: boundsA,\n      bBounds: boundsB,\n      intersection,\n    };\n  }\n  return {\n    relation: \"touching\",\n    containerIndex: null,\n    containedIndex: null,\n    aBounds: boundsA,\n    bBounds: boundsB,\n    intersection: null,\n  };\n}\n\nconst VALID_ALIGNMENTS = new Set([\n  \"left\",\n  \"right\",\n  \"top\",\n  \"bottom\",\n  \"verticallyCenter\",\n  \"horizontallyCenter\",\n]);\n\nconst getElementBounds = (obj) => {\n  const source = obj?.data || obj?.options || {};\n  let x = typeof source.x === \"number\" ? source.x : 0;\n  let y = typeof source.y === \"number\" ? source.y : 0;\n  let w = typeof source.w === \"number\" ? source.w : 0;\n  let h = typeof source.h === \"number\" ? source.h : 0;\n  // If an image is placed with crop sizing, pptxgenjs stores a larger virtual image w/h\n  // and a viewport in source.sizing.{w,h}. For visual overlap purposes, use the viewport.\n  if (source.sizing && source.sizing.type === \"crop\") {\n    if (typeof source.sizing.w === \"number\") w = source.sizing.w;\n    if (typeof source.sizing.h === \"number\") h = source.sizing.h;\n  }\n  return { x, y, w, h, x2: x + w, y2: y + h };\n};\n\nconst setElementPosition = (obj, coords) => {\n  const ensureTarget = (targetObj) => {\n    if (!targetObj || typeof targetObj !== \"object\") return null;\n    return targetObj;\n  };\n  const targets = [];\n  const dataTarget = ensureTarget(obj.data);\n  if (dataTarget) targets.push(dataTarget);\n  const optionsTarget =\n    obj.options && obj.options !== obj.data ? ensureTarget(obj.options) : null;\n  if (optionsTarget) targets.push(optionsTarget);\n  if (targets.length === 0) {\n    obj.data = obj.data && typeof obj.data === \"object\" ? obj.data : {};\n    targets.push(obj.data);\n  }\n  targets.forEach((target) => {\n    if (coords.x !== undefined) target.x = coords.x;\n    if (coords.y !== undefined) target.y = coords.y;\n  });\n};\n\nconst dimensionKeyPairs = [\n  [\"width\", \"height\"],\n  [\"w\", \"h\"],\n  [\"cx\", \"cy\"],\n  [\"slideWidth\", \"slideHeight\"],\n  [\"slideWidthInches\", \"slideHeightInches\"],\n  [\"widthInches\", \"heightInches\"],\n];\n\nconst toNumber = (value) => {\n  if (typeof value === \"number\" && Number.isFinite(value)) return value;\n  if (typeof value === \"string\") {\n    const parsed = parseFloat(value);\n    return Number.isFinite(parsed) ? parsed : null;\n  }\n  return null;\n};\n\nconst readDimensionsFromObject = (candidate, seen = new Set()) => {\n  if (!candidate || typeof candidate !== \"object\") return null;\n  if (seen.has(candidate)) return null;\n  seen.add(candidate);\n  for (const [wKey, hKey] of dimensionKeyPairs) {\n    const width = toNumber(candidate[wKey]);\n    const height = toNumber(candidate[hKey]);\n    if (width !== null && height !== null && width > 0 && height > 0) {\n      return { width, height };\n    }\n  }\n  const nestedKeys = [\"size\", \"slideSize\", \"layout\", \"slideLayout\"];\n  for (const key of nestedKeys) {\n    const nested = readDimensionsFromObject(candidate[key], seen);\n    if (nested) return nested;\n  }\n  return null;\n};\n\nconst getSlideDimensions = (slide, pptx) => {\n  const candidates = [\n    slide?._presLayout,\n    slide?._slideLayout,\n    slide?._pres?.layout,\n    slide?._parent?.layout,\n    slide?._layout,\n    pptx?._presLayout,\n    pptx?._layout,\n    pptx?.layout,\n    pptx?.presLayout,\n  ];\n  for (const candidate of candidates) {\n    const dims = readDimensionsFromObject(candidate);\n    if (dims) {\n      // Some internals are in EMUs; convert if values look too large for inches\n      const EMU_PER_IN = 914400;\n      const looksEmu = dims.width > 1000 || dims.height > 1000;\n      if (looksEmu) {\n        return {\n          width: dims.width / EMU_PER_IN,\n          height: dims.height / EMU_PER_IN,\n          source: \"emu_converted\",\n        };\n      }\n      return { ...dims, source: \"detected\" };\n    }\n  }\n  throw new Error(\n    \"getSlideDimensions(): Unable to determine slide dimensions from pptxgenjs internals.\"\n  );\n};\n\nfunction alignSlideElements(slide, indices, alignment) {\n  if (!slide || !Array.isArray(slide._slideObjects)) {\n    throw new Error(\"Invalid slide object passed to alignSlideElements()\");\n  }\n  if (!Array.isArray(indices) || indices.length === 0) {\n    throw new Error(\"indices must be a non-empty array.\");\n  }\n  if (!VALID_ALIGNMENTS.has(alignment)) {\n    throw new Error(`Unsupported alignment option: ${alignment}`);\n  }\n  const uniqueIndices = [...new Set(indices)];\n  const elements = slide._slideObjects;\n  const selected = uniqueIndices.map((idx) => {\n    if (typeof idx !== \"number\" || !Number.isInteger(idx)) {\n      throw new Error(\"Element indices must be integers.\");\n    }\n    if (idx < 0 || idx >= elements.length) {\n      throw new Error(\"Element index out of bounds for alignSlideElements().\");\n    }\n    const obj = elements[idx];\n    const bounds = getElementBounds(obj);\n    return { index: idx, obj, bounds };\n  });\n  if (selected.length < 2) return;\n  const minX = Math.min(...selected.map((item) => item.bounds.x));\n  const maxX2 = Math.max(...selected.map((item) => item.bounds.x2));\n  const minY = Math.min(...selected.map((item) => item.bounds.y));\n  const maxY2 = Math.max(...selected.map((item) => item.bounds.y2));\n  const centerX = (minX + maxX2) / 2;\n  const centerY = (minY + maxY2) / 2;\n  selected.forEach(({ obj, bounds }) => {\n    const { w, h } = bounds;\n    switch (alignment) {\n      case \"left\":\n        setElementPosition(obj, { x: minX });\n        break;\n      case \"right\":\n        setElementPosition(obj, { x: maxX2 - w });\n        break;\n      case \"top\":\n        setElementPosition(obj, { y: minY });\n        break;\n      case \"bottom\":\n        setElementPosition(obj, { y: maxY2 - h });\n        break;\n      case \"horizontallyCenter\":\n        setElementPosition(obj, { x: centerX - w / 2 });\n        break;\n      case \"verticallyCenter\":\n        setElementPosition(obj, { y: centerY - h / 2 });\n        break;\n      default:\n        throw new Error(`Unhandled alignment option: ${alignment}`);\n    }\n  });\n}\n\nfunction distributeSlideElements(slide, indices, direction) {\n  if (!slide || !Array.isArray(slide._slideObjects)) {\n    throw new Error(\"Invalid slide object passed to distributeSlideElements()\");\n  }\n  if (!Array.isArray(indices) || indices.length === 0) {\n    throw new Error(\"indices must be a non-empty array.\");\n  }\n  if (direction !== \"horizontal\" && direction !== \"vertical\") {\n    throw new Error(`Unsupported distribution direction: ${direction}`);\n  }\n  const uniqueIndices = [...new Set(indices)];\n  if (uniqueIndices.length < 2) return;\n  const elements = slide._slideObjects;\n  const selected = uniqueIndices.map((idx) => {\n    if (typeof idx !== \"number\" || !Number.isInteger(idx)) {\n      throw new Error(\"Element indices must be integers.\");\n    }\n    if (idx < 0 || idx >= elements.length) {\n      throw new Error(\n        \"Element index out of bounds for distributeSlideElements().\"\n      );\n    }\n    const obj = elements[idx];\n    const bounds = getElementBounds(obj);\n    return { index: idx, obj, bounds };\n  });\n  const axisStartKey = direction === \"horizontal\" ? \"x\" : \"y\";\n  const axisEndKey = direction === \"horizontal\" ? \"x2\" : \"y2\";\n  const sizeKey = direction === \"horizontal\" ? \"w\" : \"h\";\n  selected.sort((a, b) => {\n    const delta = a.bounds[axisStartKey] - b.bounds[axisStartKey];\n    return Math.abs(delta) > 1e-6 ? delta : a.index - b.index;\n  });\n  const minCoord = Math.min(\n    ...selected.map((item) => item.bounds[axisStartKey])\n  );\n  const maxCoord = Math.max(...selected.map((item) => item.bounds[axisEndKey]));\n  const totalSpan = maxCoord - minCoord;\n  const gaps = selected.length - 1;\n  const totalSize = selected.reduce(\n    (sum, item) => sum + item.bounds[sizeKey],\n    0\n  );\n  const gapSize = gaps > 0 ? (totalSpan - totalSize) / gaps : 0;\n  let cursor = minCoord;\n  selected.forEach(({ obj, bounds }) => {\n    if (direction === \"horizontal\") {\n      setElementPosition(obj, { x: cursor });\n      cursor += bounds.w + gapSize;\n    } else {\n      setElementPosition(obj, { y: cursor });\n      cursor += bounds.h + gapSize;\n    }\n  });\n}\n\nfunction warnIfSlideElementsOutOfBounds(slide, pptx) {\n  if (!slide || !Array.isArray(slide._slideObjects)) {\n    console.warn(\n      \"Invalid slide object passed to warnIfSlideElementsOutOfBounds()\"\n    );\n    return;\n  }\n  const {\n    width: slideWidth,\n    height: slideHeight,\n    source,\n  } = getSlideDimensions(slide, pptx);\n  const slideIndex =\n    pptx && Array.isArray(pptx._slides) ? pptx._slides.indexOf(slide) : -1;\n  const slideLabel =\n    slideIndex >= 0 ? `Slide ${slideIndex + 1}` : \"(Unknown slide index)\";\n  if (source === \"default\") {\n    console.warn(\n      `⚠️ ${slideLabel}: Unable to determine slide dimensions from pptxgenjs internals; assuming width=${slideWidth}, height=${slideHeight}.`\n    );\n  }\n  const EPS = 1e-4;\n  let outOfBoundsCount = 0;\n  const formatElement = (idx, type, bounds) => {\n    const cx = (bounds.x + bounds.w / 2).toFixed(3);\n    const cy = (bounds.y + bounds.h / 2).toFixed(3);\n    return `Element ${idx} (${type}, center_x=${cx}, center_y=${cy})`;\n  };\n  slide._slideObjects.forEach((obj, index) => {\n    const bounds = getElementBounds(obj);\n    const type = inferElementType(obj);\n    const violations = [];\n    if (bounds.x < -EPS) violations.push(`left=${bounds.x.toFixed(3)} < 0`);\n    if (bounds.y < -EPS) violations.push(`top=${bounds.y.toFixed(3)} < 0`);\n    if (bounds.x2 > slideWidth + EPS)\n      violations.push(\n        `right=${bounds.x2.toFixed(3)} > width=${slideWidth.toFixed(3)}`\n      );\n    if (bounds.y2 > slideHeight + EPS)\n      violations.push(\n        `bottom=${bounds.y2.toFixed(3)} > height=${slideHeight.toFixed(3)}`\n      );\n    if (violations.length > 0) {\n      outOfBoundsCount++;\n      console.warn(\n        `⚠️ ${slideLabel}: ${formatElement(\n          index,\n          type,\n          bounds\n        )} exceeds slide bounds (${violations.join(\", \")}).`\n      );\n    }\n  });\n  if (outOfBoundsCount > 0) {\n    console.log(\n      `⚠️ ${slideLabel}: Found ${outOfBoundsCount} element(s) extending beyond the slide bounds.`\n    );\n  }\n}\n\nmodule.exports = {\n  inferElementType,\n  compareElementPosition,\n  warnIfSlideHasOverlaps,\n  alignSlideElements,\n  distributeSlideElements,\n  warnIfSlideElementsOutOfBounds,\n  getSlideDimensions,\n};\n"
  },
  {
    "path": "skills/.curated/slides/assets/pptxgenjs_helpers/layout_builders.js",
    "content": "// Copyright (c) OpenAI. All rights reserved.\n\"use strict\";\n\nconst { calcTextBox, autoFontSize } = require(\"./text\");\nconst { imageSizingCrop, imageSizingContain } = require(\"./image\");\nconst { getSlideDimensions } = require(\"./layout\");\n\nmodule.exports = {\n  addImageTextCard,\n  addCardRow,\n  addThreeLevelTree,\n};\n\nfunction addImageTextCard(slide, opts = {}) {\n  const x = toNumberOr(opts.x, 0);\n  const y = toNumberOr(opts.y, 0);\n  const w = toNumberOr(opts.width, 3.0);\n  const gap = toNumberOr(opts.gap, 0.15);\n  const image = opts.image || {};\n  const text = opts.text || \"\";\n  const textBox = opts.textBox || {};\n\n  const boxH = toNumberOr(image.boxHeight, 2.2);\n  const sizing = (image.sizing || \"crop\").toLowerCase();\n  let imgPlacement;\n  if (image.path || image.data) {\n    const base = image.path ? { path: image.path } : { data: image.data };\n    if (sizing === \"contain\") {\n      imgPlacement = imageSizingContain(\n        image.path || image.data,\n        x,\n        y,\n        w,\n        boxH\n      );\n      slide.addImage({ ...base, ...imgPlacement });\n    } else {\n      const c = image.crop || {};\n      imgPlacement = imageSizingCrop(\n        image.path || image.data,\n        x,\n        y,\n        w,\n        boxH,\n        c.cx,\n        c.cy,\n        c.cw,\n        c.ch\n      );\n      slide.addImage({ ...base, ...imgPlacement });\n    }\n  }\n\n  const textY = y + boxH + gap;\n  const fontSize = toNumberOr(textBox.fontSize, 14);\n  const fontFaceRaw = textBox.fontFace;\n  const fontFace =\n    typeof fontFaceRaw === \"string\" && fontFaceRaw.trim().length > 0\n      ? fontFaceRaw.trim()\n      : null;\n  if (!fontFace) {\n    throw new Error(\n      \"addImageTextCard(): textBox.fontFace is required for text measurement.\"\n    );\n  }\n  let hText;\n  let textOptions;\n\n  if (textBox.h != null && Number.isFinite(toNumberOr(textBox.h, NaN))) {\n    // Layout-first: caller fixed the box height, so adjust font size to fit via autoFontSize.\n    const fixedH = toNumberOr(textBox.h, 0);\n    const baseOpts = {\n      x,\n      y: textY,\n      w,\n      h: fixedH,\n      mode: textBox.mode || \"auto\",\n      fontSize,\n      minFontSize: textBox.minFontSize,\n      maxFontSize: textBox.maxFontSize,\n      margin: textBox.margin,\n      paraSpaceAfter: textBox.paraSpaceAfter,\n    };\n    const autoOpts = autoFontSize(text, fontFace, baseOpts);\n    hText = fixedH;\n    textOptions = {\n      ...autoOpts,\n      fontFace,\n      color: textBox.color,\n      align: textBox.align,\n      valign: textBox.valign || \"top\",\n      fill: opts.background,\n    };\n  } else {\n    // Content-first: fixed font size, let calcTextBox derive the required height.\n    const layout = calcTextBox(fontSize, {\n      text,\n      w,\n      fontFace,\n      margin: textBox.margin,\n      paraSpaceAfter: textBox.paraSpaceAfter,\n    });\n    hText = layout.h;\n    textOptions = {\n      x,\n      y: textY,\n      w,\n      h: hText,\n      fontFace,\n      fontSize,\n      color: textBox.color,\n      align: textBox.align,\n      valign: textBox.valign || \"top\",\n      paraSpaceAfter: textBox.paraSpaceAfter,\n      margin: textBox.margin,\n      fill: opts.background,\n    };\n  }\n\n  slide.addText(text, textOptions);\n\n  return {\n    x,\n    y,\n    w,\n    image: {\n      x: imgPlacement?.x ?? x,\n      y,\n      w: imgPlacement?.w ?? w,\n      h: imgPlacement?.h ?? boxH,\n    },\n    text: { x, y: textY, w, h: hText },\n  };\n}\n\nfunction addCardRow(slide, region, cards = [], options = {}) {\n  const rx = toNumberOr(region.x, 0.4);\n  const ry = toNumberOr(region.y, 1.6);\n  const slideWidth = getSlideDimensions(slide).width;\n  const rw = toNumberOr(region.w, slideWidth - rx * 2);\n  const gap = toNumberOr(options.gap, 0.25);\n  const count = cards.length;\n  if (count === 0) return [];\n\n  let cardW;\n  if (options.widthStrategy === \"fixed\") {\n    cardW = toNumberOr(\n      options.cardWidth,\n      rw / count - (gap * (count - 1)) / count\n    );\n  } else {\n    cardW = (rw - gap * (count - 1)) / count;\n  }\n\n  const totalWidth = cardW * count + gap * (count - 1);\n  const align = options.align || \"left\";\n  const ox =\n    align === \"center\"\n      ? (rw - totalWidth) / 2\n      : align === \"right\"\n      ? rw - totalWidth\n      : 0;\n\n  const placements = [];\n  for (let i = 0; i < count; i++) {\n    const x = rx + ox + i * (cardW + gap);\n    placements.push(\n      addImageTextCard(slide, { ...cards[i], x, y: ry, width: cardW })\n    );\n  }\n  return placements;\n}\n\nfunction addThreeLevelTree(slide, opts = {}) {\n  const slideWidth = getSlideDimensions(slide).width;\n  const cx = toNumberOr(opts.centerX, slideWidth / 2);\n  const topY = toNumberOr(opts.topY, 1.6);\n\n  const rootW = toNumberOr(opts.root?.w, 3.3333333);\n  const rootH = toNumberOr(opts.root?.h, 0.93333333);\n  const rootX = cx - rootW / 2;\n  const rootFontFaceRaw = opts.root?.fontFace;\n  const rootFontFace =\n    typeof rootFontFaceRaw === \"string\" && rootFontFaceRaw.trim().length > 0\n      ? rootFontFaceRaw.trim()\n      : null;\n  if (!rootFontFace) {\n    throw new Error(\n      \"addThreeLevelTree(): opts.root.fontFace is required for text measurement.\"\n    );\n  }\n  const rootFontSize = toNumberOr(opts.root?.fontSize, 16);\n  const rootText = opts.root?.text || \"\";\n  const rootTextOpts = autoFontSize(rootText, rootFontFace, {\n    x: rootX,\n    y: topY,\n    w: rootW,\n    h: rootH,\n    mode: opts.root?.mode || \"shrink\",\n    fontSize: rootFontSize,\n    minFontSize: opts.root?.minFontSize,\n    maxFontSize: opts.root?.maxFontSize,\n  });\n  slide.addText(rootText, {\n    ...rootTextOpts,\n    align: \"center\",\n    valign: \"mid\",\n    fontFace: rootFontFace,\n    color: opts.root?.color || \"FFFFFF\",\n    fill: { color: opts.root?.fill || \"0B0F1A\" },\n    line: { color: opts.root?.line || opts.root?.fill || \"0B0F1A\" },\n  });\n\n  const midLabels = Array.isArray(opts.mid?.labels) ? opts.mid.labels : [];\n  const midFontFaceRaw = opts.mid?.fontFace;\n  const midFontFace =\n    typeof midFontFaceRaw === \"string\" && midFontFaceRaw.trim().length > 0\n      ? midFontFaceRaw.trim()\n      : null;\n  if (!midFontFace) {\n    throw new Error(\n      \"addThreeLevelTree(): opts.mid.fontFace is required for text measurement.\"\n    );\n  }\n  let midW = toNumberOr(opts.mid?.w, NaN);\n  const midH = toNumberOr(opts.mid?.h, rootH);\n  const midY = toNumberOr(opts.mid?.y, topY + rootH + 1.2);\n  const requestedSpacing = toNumberOr(opts.mid?.spacing, NaN); // center-to-center distance if provided\n  const leftRightMargin = toNumberOr(opts.mid?.marginX, 0.6);\n  const availableRowWidth = slideWidth - leftRightMargin * 2;\n  const countMid = midLabels.length;\n  const minGap = 0.4;\n  if (!Number.isFinite(midW) && Number.isFinite(requestedSpacing)) {\n    // Derive midW from spacing and available width\n    const totalSpan = requestedSpacing * (countMid - 1) + 0; // span between first and last centers\n    const maxW = Math.min(rootW, (availableRowWidth - totalSpan) / countMid);\n    midW = Math.max(0.8, maxW);\n  }\n  if (!Number.isFinite(midW)) {\n    // Fit equally within available width with minimum gaps\n    midW = Math.max(\n      0.8,\n      (availableRowWidth - minGap * (countMid - 1)) / countMid\n    );\n  }\n  // Compute gap to center-group horizontally without overlap\n  let gap = Math.max(\n    minGap,\n    (availableRowWidth - midW * countMid) / Math.max(1, countMid - 1)\n  );\n  const totalWidth = midW * countMid + gap * (countMid - 1);\n  const startLeft = cx - totalWidth / 2;\n  for (let i = 0; i < midLabels.length; i++) {\n    const x = startLeft + i * (midW + gap);\n    const midText = midLabels[i] || \"\";\n    const midFontSize = toNumberOr(opts.mid?.fontSize, 16);\n    const midTextOpts = autoFontSize(midText, midFontFace, {\n      x,\n      y: midY,\n      w: midW,\n      h: midH,\n      mode: opts.mid?.mode || \"shrink\",\n      fontSize: midFontSize,\n      minFontSize: opts.mid?.minFontSize,\n      maxFontSize: opts.mid?.maxFontSize,\n    });\n    slide.addText(midText, {\n      ...midTextOpts,\n      align: \"center\",\n      valign: \"mid\",\n      fontFace: midFontFace,\n      color: opts.mid?.color || \"000000\",\n      fill: { color: opts.mid?.fill || \"A0BEC2\" },\n      line: { color: opts.mid?.line || opts.mid?.fill || \"A0BEC2\" },\n    });\n    addConnector(slide, cx, topY + rootH, x + midW / 2, midY, opts.line);\n  }\n\n  const leavesPerMid = Array.isArray(opts.leaf?.labelsPerMid)\n    ? opts.leaf.labelsPerMid\n    : [];\n  const leafFontFaceRaw = opts.leaf?.fontFace;\n  const leafFontFace =\n    typeof leafFontFaceRaw === \"string\" && leafFontFaceRaw.trim().length > 0\n      ? leafFontFaceRaw.trim()\n      : null;\n  if (!leafFontFace) {\n    throw new Error(\n      \"addThreeLevelTree(): opts.leaf.fontFace is required for text measurement.\"\n    );\n  }\n  const leafW = toNumberOr(opts.leaf?.w, 1.05);\n  const leafH = toNumberOr(opts.leaf?.h, 1.0666667);\n  const leafY = toNumberOr(opts.leaf?.y, midY + midH + 1.0);\n  const minLeafGap = 0.2;\n  for (let i = 0; i < midLabels.length; i++) {\n    const xBase = startLeft + i * (midW + gap);\n    const childLabels = Array.isArray(leavesPerMid[i]) ? leavesPerMid[i] : [];\n    const childCount = childLabels.length || 3;\n    // Compute per-mid gap to fit children within midW without overlap\n    const leafGap = Math.max(\n      minLeafGap,\n      (midW - childCount * leafW) / Math.max(1, childCount - 1)\n    );\n    const totalWidth = childCount * leafW + (childCount - 1) * leafGap;\n    const leftX = xBase + (midW - totalWidth) / 2;\n    for (let j = 0; j < childCount; j++) {\n      const x = leftX + j * (leafW + leafGap);\n      const leafText = childLabels[j] || \"\";\n      const leafFontSize = toNumberOr(opts.leaf?.fontSize, 16);\n      const leafTextOpts = autoFontSize(leafText, leafFontFace, {\n        x,\n        y: leafY,\n        w: leafW,\n        h: leafH,\n        mode: opts.leaf?.mode || \"shrink\",\n        fontSize: leafFontSize,\n        minFontSize: opts.leaf?.minFontSize,\n        maxFontSize: opts.leaf?.maxFontSize,\n      });\n      slide.addText(leafText, {\n        ...leafTextOpts,\n        align: \"center\",\n        valign: \"mid\",\n        fontFace: leafFontFace,\n        color: opts.leaf?.color || \"000000\",\n        fill: { color: opts.leaf?.fill || \"A6C1EE\" },\n        line: { color: opts.leaf?.line || opts.leaf?.fill || \"A6C1EE\" },\n      });\n      addConnector(\n        slide,\n        xBase + midW / 2,\n        midY + midH,\n        x + leafW / 2,\n        leafY,\n        opts.line\n      );\n    }\n  }\n}\n\nfunction addConnector(slide, x1, y1, x2, y2, line = {}) {\n  const x = Math.min(x1, x2);\n  const y = Math.min(y1, y2);\n  slide.addShape(\"line\", {\n    x,\n    y,\n    w: Math.abs(x2 - x1),\n    h: Math.abs(y2 - y1),\n    line: { color: line.color || \"000000\", pt: line.pt || 1 },\n    flipH: x2 < x1 ? true : undefined,\n  });\n}\n\nfunction toNumberOr(v, fallback) {\n  const n = typeof v === \"string\" ? parseFloat(v) : v;\n  return Number.isFinite(n) ? n : fallback;\n}\n"
  },
  {
    "path": "skills/.curated/slides/assets/pptxgenjs_helpers/svg.js",
    "content": "// Copyright (c) OpenAI. All rights reserved.\n\"use strict\";\n\nfunction toDataUri(svg) {\n  return \"data:image/svg+xml;base64,\" + Buffer.from(svg).toString(\"base64\");\n}\n\nfunction sanitizeSvg(svg) {\n  let inner = svg;\n  const a = inner.indexOf(\"<svg\");\n  const b = inner.indexOf(\"</svg>\");\n  if (a !== -1 && b !== -1) inner = inner.slice(a, b + 6);\n  inner = inner.replace(/<\\?xml[^>]*>/g, \"\");\n  if (!/xmlns=\\\"http:\\/\\/www\\.w3\\.org\\/2000\\/svg\\\"/.test(inner)) {\n    inner = inner.replace(/<svg /, '<svg xmlns=\"http://www.w3.org/2000/svg\" ');\n  }\n  inner = inner.replace(\n    /(width|height)=\\\"([0-9.]+)(ex|em)\\\"/g,\n    (_m, attr, num) => {\n      const px = Math.round(parseFloat(num) * 8.5);\n      return `${attr}=\"${px}px\"`;\n    }\n  );\n  inner = inner.replace(/currentColor/g, \"#000000\");\n  return inner;\n}\n\nfunction svgToDataUri(svg) {\n  return toDataUri(sanitizeSvg(svg));\n}\n\nmodule.exports = {\n  toDataUri,\n  sanitizeSvg,\n  svgToDataUri,\n};\n"
  },
  {
    "path": "skills/.curated/slides/assets/pptxgenjs_helpers/text.js",
    "content": "// Copyright (c) OpenAI. All rights reserved.\n\"use strict\";\n\nconst { spawnSync } = require(\"child_process\");\nconst { Canvas } = require(\"skia-canvas\");\n// Unicode line-break iterator (UAX #14) so we mimic PPT/LibreOffice wrapping rules.\nconst LineBreaker = require(\"linebreak\");\nconst fontkit = require(\"fontkit\");\nconst TEXT_MEASURER = getTextMeasurer();\nconst registeredFontVariants = new Set();\nconst fontPathCache = new Map();\nconst fontKitCache = new Map();\n\n// Estimate the text box height for a given font size and line count.\n// NOTE: This is an analytical approximation, not an exact reproduction of\n// PowerPoint/LibreOffice layout. Always verify visually and adjust based on\n// actual rendering if precise fit is required.\nfunction calcTextBoxHeightSimple(\n  fontSize,\n  lines = 1,\n  leading = 1.15,\n  padding = 0.3\n) {\n  const lineHeightIn = (fontSize / 72) * leading;\n  return lines * lineHeightIn + padding;\n}\n\n// Compute font size that fits given text within a fixed box.\n// NOTE: autoFontSize uses skia-canvas measurement stack to approximate the font size\n// that will fit in a given box. Rendering engines may differ slightly, so\n// treat the result as an estimate and tweak as needed after visual inspection.\n// Signature:\n//   autoFontSize(textOrRuns, fontFace, opts?)\n//   - fontFace must be provided as the 2nd positional argument and cannot be in opts.\n//   - All modes always respect [minFontSize, maxFontSize] as a CLOSED interval when provided.\n// Modes:\n//   - mode: \"shrink\"  => shrink only (search [minFontSize, min(maxFontSize, fontSize)])\n//   - mode: \"enlarge\" => enlarge only (search [max(minFontSize, fontSize), maxFontSize])\n//   - mode: \"auto\"    => shrink + enlarge (search [minFontSize, maxFontSize]); fontSize optional.\n// In \"auto\" mode fontSize is not required; when omitted we simply search the whole [minFontSize, maxFontSize] range.\n// Returns a cloned options object with computed fontSize. fit: \"shrink\" is appended only when mode === \"shrink\".\nfunction autoFontSize(textOrRuns, fontFace, opts = {}) {\n  const x = toNumber(opts.x, 0);\n  const y = toNumber(opts.y, 0);\n  const w = toNumber(opts.w, 0);\n  const h = toNumber(opts.h, 0);\n  if (!(w > 0 && h > 0)) throw new Error(\"autoFontSize(): non-positive w or h\");\n\n  const face = typeof fontFace === \"string\" ? fontFace.trim() : \"\";\n  if (face.length === 0) {\n    throw new Error(\n      \"autoFontSize(): fontFace is required as the 2nd positional argument.\"\n    );\n  }\n\n  // Fast-path: if there is no visible text content, just return the\n  // (optionally clamped) reference fontSize; there is nothing to fit.\n  const hasAnyText =\n    normalizeText(textOrRuns).trim().length > 0 ||\n    (Array.isArray(textOrRuns) &&\n      textOrRuns.some(\n        (run) => run && typeof run.text === \"string\" && run.text.trim().length\n      ));\n\n  const fontStyle =\n    opts.italic === true || opts.fontStyle === \"italic\" ? \"italic\" : \"normal\";\n  const fontWeight =\n    opts.bold === true || String(opts.fontWeight || \"\").toLowerCase() === \"bold\"\n      ? \"bold\"\n      : \"normal\";\n  const leading = toNumber(opts.leading, 1.15) || 1.15;\n\n  const modeRaw = typeof opts.mode === \"string\" ? opts.mode : \"auto\"; // 'auto' (default) | 'shrink' | 'enlarge'\n  const mode = modeRaw.toLowerCase();\n  const isShrink = mode === \"shrink\";\n  const isEnlarge = mode === \"enlarge\";\n  const isAuto = mode === \"auto\";\n\n  const refPtRaw = toNumber(opts.fontSize, NaN);\n  const hasRefPt = Number.isFinite(refPtRaw);\n  const refPt = hasRefPt ? refPtRaw : NaN;\n\n  // Base bounds (closed interval). Defaults:\n  //   - minFontSize: 1pt\n  //   - maxFontSize: 1000pt (unless the caller provided a tighter bound)\n  let minPt = toNumber(opts.minFontSize, NaN);\n  let maxPt = toNumber(opts.maxFontSize, NaN);\n  const userProvidedMax = Number.isFinite(maxPt);\n  if (!Number.isFinite(minPt)) {\n    minPt = 1;\n  }\n  if (!Number.isFinite(maxPt)) {\n    maxPt = 1000;\n  }\n\n  if (isShrink || isEnlarge) {\n    if (!hasRefPt) {\n      throw new Error(\n        \"autoFontSize(): mode 'shrink' or 'enlarge' requires fontSize\"\n      );\n    }\n  }\n\n  if (isShrink) {\n    // Shrink only: never exceed the requested size (and respect maxFontSize).\n    maxPt = Math.min(maxPt, refPt);\n  } else if (isEnlarge) {\n    // Enlarge only: never go below the requested size (and respect minFontSize).\n    minPt = Math.max(minPt, refPt);\n  } else if (isAuto && hasRefPt && userProvidedMax) {\n    // Auto mode with an explicit maxFontSize: honor [minFontSize, maxFontSize]\n    // as the search band while allowing both shrink and enlarge within it.\n  } else if (!isAuto) {\n    throw new Error(\n      `autoFontSize(): unsupported mode \"${modeRaw}\", expected \"auto\" | \"shrink\" | \"enlarge\"`\n    );\n  }\n\n  if (!(maxPt > 0 && maxPt >= minPt)) {\n    throw new Error(\n      \"autoFontSize(): invalid minFontSize/maxFontSize bounds after normalization\"\n    );\n  }\n\n  // If there is no actual text, we can skip measurement entirely and just\n  // clamp the reference size to [minPt, maxPt].\n  if (!hasAnyText) {\n    const chosen =\n      (hasRefPt && Math.max(minPt, Math.min(maxPt, refPt))) || minPt;\n    const out = { ...opts, x, y, w, h, fontSize: chosen };\n    if (isShrink) out.fit = \"shrink\";\n    return out;\n  }\n\n  // Search the space of candidate font sizes with a small step and a safety\n  // bias baked into the fit test:\n  //   - precision: 0.05pt (~1/20pt) so we land very close to the true max-fit.\n  //   - safetyFactor: we require that the calcTextBox()-measured height is\n  //     within a small margin of the caller-provided box height, so that the\n  //     same layout engine used by calcTextBox drives autoFontSize decisions.\n  const precision = 0.05; // point precision for search (~1/20pt)\n  const safetyFactor = 0.97;\n\n  let lo = minPt;\n  let hi = maxPt;\n  let best = lo;\n  while (hi - lo > precision) {\n    const mid = (lo + hi) / 2;\n    // Delegate measurement to calcTextBox so that autoFontSize and\n    // calcTextBox share the exact same layout pipeline (paragraph modeling,\n    // bullet handling, margins, padding, width scaling, etc.).\n    const layout = calcTextBox(mid, {\n      text: textOrRuns,\n      w,\n      fontFace: face,\n      fontStyle,\n      fontWeight,\n      leading,\n      margin: opts.margin,\n      padding: opts.padding,\n      paraSpaceAfter: opts.paraSpaceAfter,\n    });\n    const fits = layout.h <= h * safetyFactor + 1e-6;\n    if (fits) {\n      best = mid;\n      lo = mid; // try larger\n    } else {\n      hi = mid; // shrink\n    }\n  }\n  // Closed interval: clamp to [minPt, maxPt].\n  const finalPt = Math.max(minPt, Math.min(maxPt, best));\n\n  // Pass through all original options, override fontSize and append fit: \"shrink\"\n  const out = { ...opts, x, y, w, h, fontSize: finalPt };\n  if (isShrink) out.fit = \"shrink\";\n  return out;\n}\n\n// Calculate text box metrics using skia-canvas measurement (lines, height,\n// width) for a given font size and text payload.\n// NOTE: calcTextBox approximates how many lines and how much space text will\n// occupy using our JS measurement pipeline. It is designed to be close to\n// PowerPoint/LibreOffice but is not guaranteed pixel-perfect—always adjust\n// based on actual slide rendering when precision matters.\n// Signature:\n//   calcTextBox(fontSizePt, opts)\n//     - fontSizePt: number (points)\n//     - opts (keywords): {\n//         text?: string | runs[],\n//         w?: number (inches),\n//         h?: number (inches),\n//         lines?: number,\n//         fontFace?: string, // required when measuring by width/height with text\n//         fontStyle?: 'normal' | 'italic', italic?: boolean,\n//         fontWeight?: 'normal' | 'bold', bold?: boolean,\n//         leading?: number (line height multiplier, default 1.15),\n//         padding?: number (inches, default 0.3),\n//         paraSpaceAfter?: number (points, default 0)\n//       }\n// Modes (auto-detected):\n//   a) Given lines -> compute height\n//   b) Given width + text -> compute height and lines\n//   c) Given height + text -> compute width and lines\n// Throws when insufficient info is provided.\nfunction calcTextBox(fontSizePt, opts = {}) {\n  const textInput = opts.text ?? \"\";\n  const text = normalizeText(textInput || \"\");\n  const face =\n    typeof opts.fontFace === \"string\" && opts.fontFace.trim().length > 0\n      ? opts.fontFace.trim()\n      : \"\";\n  const fontStyle =\n    opts.italic === true || opts.fontStyle === \"italic\" ? \"italic\" : \"normal\";\n  const fontWeight =\n    opts.bold === true || String(opts.fontWeight || \"\").toLowerCase() === \"bold\"\n      ? \"bold\"\n      : \"normal\";\n  const leading = toNumber(opts.leading, 1.15) || 1.15;\n  const padding = toNumber(opts.padding, 0.3); // inches (allow 0)\n  const paraSpaceAfterPt = toNumber(opts.paraSpaceAfter, 0) || 0; // points\n  const lineHeightIn = (fontSizePt / 72) * leading;\n  const margins = normalizeMargins(opts.margin);\n  const measurer = TEXT_MEASURER;\n\n  const hasLines = Number.isFinite(toNumber(opts.lines, NaN));\n  const hasWidth = Number.isFinite(toNumber(opts.w, NaN));\n  const hasHeight = Number.isFinite(toNumber(opts.h, NaN));\n  const paragraphs = buildParagraphModels(textInput, {\n    fontSizePt,\n    // Do not silently substitute a default font here; callers measuring by\n    // width/height are required to pass an explicit fontFace so that our\n    // metrics match the actual slide theme.\n    fontFace: face,\n    fontStyle,\n    fontWeight,\n    leading,\n    paraSpaceAfterPt,\n  });\n  const hasAnyText = paragraphs.some((p) => p.text.length > 0);\n\n  // Empirical top inset: PPT text frames render a small gutter above the first line\n  // even with zero margins. Model it as a fraction of the font size so callers can\n  // visually trim by shifting y up and growing h by the same amount.\n  const topInsetIn = (fontSizePt / 72) * 0.2; // ~20% of font size (inches)\n\n  if (hasLines) {\n    // Mode (a): Given lines -> compute height only\n    const lines = toNumber(opts.lines, 1);\n    const contentH = Math.max(0, lines * lineHeightIn + padding);\n    const h = contentH + margins.top + margins.bottom;\n    const passthrough = buildPassthroughOptions(opts, fontSizePt, margins);\n    return {\n      ...passthrough,\n      w: toNumber(opts.w, NaN) || null,\n      h,\n      lines,\n      contentH,\n      margins,\n      topInset: topInsetIn,\n    };\n  }\n\n  if (hasWidth && hasAnyText) {\n    // Mode (b): Given width + text -> compute height and lines\n    if (face.length === 0) {\n      throw new Error(\n        \"calcTextBox(): opts.fontFace is required when measuring by width.\"\n      );\n    }\n    const boxW = toNumber(opts.w, 0);\n    if (!(boxW > 0))\n      throw new Error(\"calcTextBox(): width must be > 0 in mode 'width'\");\n    const innerW = Math.max(0, boxW - margins.left - margins.right);\n    const { lines, heightIn } = layoutGivenWidth(paragraphs, innerW);\n    const contentH = Math.max(0, heightIn + padding);\n    const h = contentH + margins.top + margins.bottom;\n    const passthrough = buildPassthroughOptions(opts, fontSizePt, margins);\n    return {\n      ...passthrough,\n      w: boxW,\n      h,\n      lines,\n      contentH,\n      margins,\n      topInset: topInsetIn,\n    };\n  }\n\n  if (hasHeight && hasAnyText) {\n    // Mode (c): Given height + text -> compute minimal width and lines to fit\n    if (face.length === 0) {\n      throw new Error(\n        \"calcTextBox(): opts.fontFace is required when measuring by height.\"\n      );\n    }\n    const boxH = toNumber(opts.h, 0);\n    if (!(boxH > 0))\n      throw new Error(\"calcTextBox(): height must be > 0 in mode 'height'\");\n    const innerH = Math.max(0, boxH - margins.top - margins.bottom);\n    // Upper bound: single-line width across paragraphs\n    const singleLineWidth = paragraphs.reduce((mx, p) => {\n      const width = measureRunWidth(p, p.text) + p.textIndentIn;\n      return Math.max(mx, width);\n    }, 0);\n    const minHeightOneLine = Math.max(\n      0,\n      paragraphs.reduce((sum, p, idx) => {\n        const lineHeight = (p.fontSizePt / 72) * p.leading;\n        sum += lineHeight;\n        if (idx !== paragraphs.length - 1) sum += p.paraSpaceAfterIn;\n        return sum;\n      }, 0)\n    );\n    if (minHeightOneLine + padding - innerH > 1e-6) {\n      throw new Error(\n        \"calcTextBox(): height too small for one-line layout at this font size\"\n      );\n    }\n    // Lower bound: longest token width\n    const longestTokenWidth = paragraphs.reduce((mx, p) => {\n      const tokens = splitTextIntoTokens(p.text);\n      for (const tk of tokens) {\n        if (tk.length === 0) continue;\n        const wIn = measureRunWidth(p, tk) + p.textIndentIn;\n        if (wIn > mx) mx = wIn;\n      }\n      return mx;\n    }, 0);\n    let lo = Math.max(0.01, longestTokenWidth);\n    let hi = Math.max(lo, singleLineWidth);\n    let best = hi;\n    for (let iter = 0; iter < 32; iter++) {\n      const mid = (lo + hi) / 2;\n      const { lines, heightIn } = layoutGivenWidth(paragraphs, mid);\n      const totalH = heightIn + padding;\n      if (totalH <= innerH + 1e-6) {\n        best = mid;\n        hi = mid;\n      } else {\n        lo = mid;\n      }\n    }\n    const { lines, heightIn } = layoutGivenWidth(paragraphs, best);\n    const contentH = heightIn + padding;\n    const passthrough = buildPassthroughOptions(opts, fontSizePt, margins);\n    return {\n      ...passthrough,\n      w: best + margins.left + margins.right,\n      h: contentH + margins.top + margins.bottom,\n      lines,\n      contentH,\n      margins,\n      topInset: topInsetIn,\n    };\n  }\n\n  throw new Error(\n    \"calcTextBox(): insufficient information. Provide {lines} or ({w,text}) or ({h,text}).\"\n  );\n}\n\nfunction layoutGivenWidth(paragraphs, boxW) {\n  let totalLines = 0;\n  let heightIn = 0;\n  for (let i = 0; i < paragraphs.length; i++) {\n    const para = paragraphs[i];\n    const widthScale = getWidthScaleForParagraph(para);\n    const usableWidth = Math.max(0.01, boxW - para.textIndentIn) * widthScale;\n    const lines = greedyWrap(para, usableWidth);\n    const count = Math.max(1, lines.length);\n    totalLines += count;\n    const lineHeightIn = (para.fontSizePt / 72) * para.leading;\n    heightIn += count * lineHeightIn;\n    if (i !== paragraphs.length - 1) heightIn += para.paraSpaceAfterIn;\n  }\n  return { lines: totalLines, heightIn };\n}\n\nfunction greedyWrap(paragraph, maxWidthIn) {\n  const text = paragraph.text || \"\";\n  if (text.length === 0) return [\"\"];\n  const breaker = new LineBreaker(text);\n  const breakpoints = [];\n  let bk;\n  while ((bk = breaker.nextBreak())) {\n    breakpoints.push({ pos: bk.position, required: bk.required });\n  }\n  const lines = [];\n  let start = skipTextWhitespace(text, 0);\n  let idx = 0;\n  while (start < text.length) {\n    while (idx < breakpoints.length && breakpoints[idx].pos <= start) idx++;\n    let chosen = null;\n    let probe = idx;\n    while (probe < breakpoints.length) {\n      const br = breakpoints[probe];\n      const slice = text.slice(start, br.pos);\n      const width = measureRunWidth(paragraph, trimLineEnd(slice));\n      if (width <= maxWidthIn + 1e-6) {\n        chosen = br;\n        probe++;\n        if (br.required) break;\n      } else {\n        break;\n      }\n    }\n    if (!chosen) {\n      const forced = forceBreakSegment(text, start, maxWidthIn, paragraph);\n      if (forced.segment.length === 0) break;\n      lines.push(trimLineEnd(forced.segment));\n      start = skipTextWhitespace(text, forced.nextIndex);\n      continue;\n    }\n    const lineText = text.slice(start, chosen.pos);\n    lines.push(trimLineEnd(lineText));\n    start = skipTextWhitespace(text, chosen.pos);\n  }\n  if (!lines.length) lines.push(\"\");\n  return lines;\n}\n\nfunction splitTextIntoTokens(text) {\n  if (typeof text !== \"string\") return [\"\"];\n  const tokens = text.split(/(\\s+)/);\n  return tokens.length ? tokens : [\"\"];\n}\n\nfunction trimLineEnd(value) {\n  return typeof value === \"string\" ? value.replace(/\\s+$/u, \"\") : \"\";\n}\n\nfunction measureRunWidth(paragraph, text) {\n  if (!text || text.length === 0) return 0;\n  const fontData = getFontData(\n    paragraph.fontFace,\n    paragraph.fontStyle,\n    paragraph.fontWeight\n  );\n  if (fontData && fontData.font) {\n    const layout = fontData.font.layout(text);\n    const widthPts =\n      (layout.advanceWidth / fontData.font.unitsPerEm) * paragraph.fontSizePt;\n    return Math.max(0, widthPts / 72);\n  }\n  return TEXT_MEASURER(\n    text,\n    paragraph.fontSizePt,\n    paragraph.fontFace,\n    paragraph.fontStyle,\n    paragraph.fontWeight\n  );\n}\n\nfunction forceBreakSegment(text, start, maxWidthIn, paragraph) {\n  const chars = Array.from(text.slice(start));\n  if (chars.length === 0) return { segment: \"\", nextIndex: text.length };\n  let buffer = \"\";\n  let consumedUnits = 0;\n  for (let i = 0; i < chars.length; i++) {\n    const candidate = buffer + chars[i];\n    const width = measureRunWidth(paragraph, trimLineEnd(candidate));\n    if (width <= maxWidthIn + 1e-6) {\n      buffer = candidate;\n      consumedUnits += chars[i].length;\n      continue;\n    }\n    if (buffer.length === 0) {\n      buffer = chars[i];\n      consumedUnits += chars[i].length;\n    }\n    break;\n  }\n  if (buffer.length === 0) {\n    buffer = chars[0] || \"\";\n    consumedUnits = buffer.length;\n  }\n  return { segment: buffer, nextIndex: start + consumedUnits };\n}\n\nfunction skipTextWhitespace(text, index) {\n  let idx = index;\n  while (idx < text.length && /\\s/.test(text[idx])) idx++;\n  return idx;\n}\n\nfunction buildParagraphModels(textOrRuns, baseStyle) {\n  const entries = collectParagraphEntries(textOrRuns);\n  if (entries.length === 0) {\n    return [resolveParagraphStyle({ text: \"\" }, baseStyle)];\n  }\n  return entries.map((entry) => resolveParagraphStyle(entry, baseStyle));\n}\n\nfunction collectParagraphEntries(textOrRuns) {\n  const result = [];\n  if (Array.isArray(textOrRuns)) {\n    for (const entry of textOrRuns) {\n      if (typeof entry === \"string\") {\n        pushParagraphSegments(entry, undefined, result);\n      } else if (entry && typeof entry === \"object\") {\n        pushParagraphSegments(entry.text ?? \"\", entry.options || {}, result);\n      }\n    }\n    return result;\n  }\n  pushParagraphSegments(textOrRuns ?? \"\", undefined, result);\n  return result;\n}\n\nfunction pushParagraphSegments(text, options, target) {\n  const normalized = String(text ?? \"\");\n  const parts = normalized.split(/\\r?\\n/);\n  if (parts.length === 0) {\n    target.push({ text: \"\", options });\n    return;\n  }\n  for (const part of parts) {\n    target.push({ text: part, options });\n  }\n}\n\nfunction resolveParagraphStyle(entry, baseStyle) {\n  const opts = entry.options || {};\n  const fontFace =\n    (opts.fontFace && String(opts.fontFace).trim()) ||\n    baseStyle.fontFace ||\n    \"Arial\";\n  const fontStyle =\n    opts.italic === true || opts.fontStyle === \"italic\"\n      ? \"italic\"\n      : baseStyle.fontStyle || \"normal\";\n  const fontWeight =\n    opts.bold === true || String(opts.fontWeight || \"\").toLowerCase() === \"bold\"\n      ? \"bold\"\n      : baseStyle.fontWeight || \"normal\";\n  const fontSizePt =\n    toNumber(opts.fontSize, baseStyle.fontSizePt) || baseStyle.fontSizePt;\n  const leading =\n    toNumber(opts.leading, baseStyle.leading) || baseStyle.leading || 1.15;\n  const paraSpaceAfterPt =\n    toNumber(opts.paraSpaceAfter, baseStyle.paraSpaceAfterPt) ||\n    baseStyle.paraSpaceAfterPt ||\n    0;\n  const hasBullet = !!opts.bullet;\n  let indentPt = toNumber(opts.indent, NaN);\n  if (!Number.isFinite(indentPt) && hasBullet) {\n    indentPt = toNumber(opts.bullet.indent, NaN);\n  }\n  if (!Number.isFinite(indentPt)) indentPt = 0;\n  const hangingPt = toNumber(opts.hanging, 0) || 0;\n  let textIndentIn = 0;\n  if (indentPt > 0) {\n    if (hasBullet) {\n      // PowerPoint-style bullets: \"indent\" is the distance from the left edge\n      // of the text box to the start of the text (the bullet itself is hung\n      // using the hanging value). This means the available width for the text\n      // is boxWidth - indent, not boxWidth - (indent - hanging). Modeling it\n      // this way matches the manual line counts from PowerPoint/LibreOffice.\n      textIndentIn = indentPt / 72;\n    } else {\n      // Non-bullet paragraphs keep the prior behavior where hanging reduces\n      // the effective indent (similar to CSS text-indent).\n      textIndentIn = Math.max(0, (indentPt - hangingPt) / 72);\n    }\n  }\n  return {\n    text: entry.text || \"\",\n    fontFace,\n    fontStyle,\n    fontWeight,\n    fontSizePt,\n    leading,\n    paraSpaceAfterIn: paraSpaceAfterPt / 72,\n    textIndentIn,\n  };\n}\n\nfunction getFontData(face, fontStyle, fontWeight) {\n  const key = makeFontCacheKey(face, fontStyle, fontWeight);\n  if (fontKitCache.has(key)) return fontKitCache.get(key);\n  const fontPath = findFontPath(face, fontStyle, fontWeight);\n  if (!fontPath) {\n    fontKitCache.set(key, null);\n    return null;\n  }\n  try {\n    let font = fontkit.openSync(fontPath);\n    if (font && typeof font.fonts === \"object\") {\n      font = selectCollectionFont(font, fontStyle, fontWeight);\n    }\n    if (!font || typeof font.layout !== \"function\") {\n      fontKitCache.set(key, null);\n      return null;\n    }\n    registerCanvasFontVariant(fontPath, face, fontStyle, fontWeight, key);\n    const payload = { font, path: fontPath };\n    fontKitCache.set(key, payload);\n    return payload;\n  } catch (err) {\n    fontKitCache.set(key, null);\n    return null;\n  }\n}\n\nfunction makeFontCacheKey(face, fontStyle, fontWeight) {\n  const family = (face || \"Arial\").trim();\n  const style = (fontStyle || \"normal\").toLowerCase();\n  const weight = (fontWeight || \"normal\").toLowerCase();\n  return `${family}::${style}::${weight}`;\n}\n\nfunction registerCanvasFontVariant(\n  fontPath,\n  face,\n  fontStyle,\n  fontWeight,\n  cacheKey\n) {\n  if (registeredFontVariants.has(cacheKey)) return;\n  try {\n    Canvas.registerFont(fontPath, {\n      family: face,\n      style: fontStyle || \"normal\",\n      weight: fontWeight || \"normal\",\n    });\n    registeredFontVariants.add(cacheKey);\n  } catch (err) {\n    // ignore registration failure; measurement will fall back to Skia default\n  }\n}\n\nfunction findFontPath(face, fontStyle, fontWeight) {\n  const family = (face || \"\").trim();\n  if (family.length === 0) return null;\n  const key = makeFontCacheKey(family, fontStyle, fontWeight);\n  if (fontPathCache.has(key)) return fontPathCache.get(key);\n  const styleParts = [];\n  if ((fontWeight || \"\").toLowerCase() === \"bold\") styleParts.push(\"Bold\");\n  if ((fontStyle || \"\").toLowerCase() === \"italic\") styleParts.push(\"Italic\");\n  const styleQuery =\n    styleParts.length > 0 ? `:style=${styleParts.join(\" \")}` : \"\";\n  const query = `${family}${styleQuery}`;\n  const result = spawnSync(\"fc-match\", [\"-f\", \"%{file}\", query], {\n    encoding: \"utf8\",\n  });\n  if (result.status === 0) {\n    const output = String(result.stdout || \"\").trim();\n    if (output.length > 0) {\n      fontPathCache.set(key, output);\n      return output;\n    }\n  }\n  fontPathCache.set(key, null);\n  return null;\n}\n\nfunction selectCollectionFont(collection, fontStyle, fontWeight) {\n  const fonts = collection.fonts || [];\n  if (fonts.length === 0) return null;\n  const wantItalic = (fontStyle || \"\").toLowerCase() === \"italic\";\n  const wantBold = (fontWeight || \"\").toLowerCase() === \"bold\";\n  let best = fonts[0];\n  let bestScore = scoreFontVariant(best, wantItalic, wantBold);\n  for (let i = 1; i < fonts.length; i++) {\n    const candidate = fonts[i];\n    const score = scoreFontVariant(candidate, wantItalic, wantBold);\n    if (score > bestScore) {\n      best = candidate;\n      bestScore = score;\n    }\n  }\n  return best;\n}\n\nfunction scoreFontVariant(font, wantItalic, wantBold) {\n  if (!font) return -1;\n  const name = String(font.fullName || font.postscriptName || \"\").toLowerCase();\n  const isItalic = /italic|oblique/.test(name);\n  const isBold = /bold|black|heavy|semibold|extrabold/.test(name);\n  let score = 0;\n  if (isItalic === wantItalic) score += 1;\n  if (isBold === wantBold) score += 1;\n  return score;\n}\n\n// Empirical width scaling to better match PowerPoint/LibreOffice line breaks.\n// A tiny global shrink (about -1.5%) nudges borderline words to wrap the same\n// way Office does, with per-script tweaks for cases where our measurer\n// systematically under- or over-estimates glyph widths. We intentionally avoid\n// per-font calibration so this helper generalizes beyond the regression deck.\nfunction getWidthScaleForParagraph(paragraph) {\n  if (!paragraph || typeof paragraph.text !== \"string\") return 1;\n  const text = paragraph.text;\n  // Thai script: our measurer tends to slightly over-estimate, which can cause\n  // extra wraps. Give it a bit more room horizontally.\n  if (/[ก-๛]/u.test(text)) {\n    return 1.2;\n  }\n\n  // Arabic: we usually underestimate, so shrink available width a bit more to\n  // encourage earlier breaks.\n  if (/[\\u0600-\\u06FF]/u.test(text)) {\n    return 0.97;\n  }\n\n  // Base shrink for most Latin and other scripts.\n  return 0.985;\n}\n\n// Build options to pass directly to pptx.addText. We exclude measurement-only\n// fields and fill sensible defaults (e.g., fontSize) so callers can spread\n// the result into addText just like the image sizing helpers.\nfunction buildPassthroughOptions(opts, fontSizePt, margins) {\n  const exclude = new Set([\n    \"text\",\n    \"lines\",\n    \"w\", // will be set by calcTextBox\n    \"h\", // will be set by calcTextBox\n    // fontFace/style/weight are useful for addText; allow passthrough\n    \"leading\",\n    \"padding\",\n  ]);\n  const out = {};\n  for (const k of Object.keys(opts)) {\n    if (!exclude.has(k)) out[k] = opts[k];\n  }\n  if (out.fontSize == null) out.fontSize = fontSizePt;\n  if (opts.margin != null) out.margin = margins;\n  return out;\n}\n\nfunction getTextMeasurer() {\n  // Skia-canvas only for accurate shaping and Fontconfig-based resolution.\n  // Throws if skia-canvas is not available.\n  const canvas = new Canvas(2, 2);\n  const ctx = canvas.getContext(\"2d\");\n  const PX_PER_IN = 96;\n  return (text, fontSizePt, fontFace, fontStyle, fontWeight) => {\n    const px = (fontSizePt / 72) * PX_PER_IN;\n    const style = fontStyle || \"normal\";\n    const weight = fontWeight || \"normal\";\n    // CSS shorthand: style weight size family\n    ctx.font = `${style} ${weight} ${px}px ${fontFace || \"Arial\"}`;\n    const metrics = ctx.measureText(text);\n    return (metrics.width || 0) / PX_PER_IN;\n  };\n}\n\nfunction normalizeMargins(m) {\n  const toInches = (value) =>\n    typeof value === \"number\" && Number.isFinite(value) ? value / 72 : 0;\n  if (m && typeof m === \"object\") {\n    if (Number.isFinite(m.left) || Number.isFinite(m.top)) {\n      return {\n        left: toInches(m.left),\n        right: toInches(m.right),\n        top: toInches(m.top),\n        bottom: toInches(m.bottom),\n      };\n    }\n  }\n  const all = toInches(m);\n  return { left: all, right: all, top: all, bottom: all };\n}\n\nfunction normalizeText(textOrRuns) {\n  if (Array.isArray(textOrRuns)) {\n    return textOrRuns\n      .map((item) => {\n        if (typeof item === \"string\") return item;\n        if (item && typeof item.text === \"string\") return item.text;\n        return \"\";\n      })\n      .join(\"\");\n  }\n  return typeof textOrRuns === \"string\" ? textOrRuns : String(textOrRuns ?? \"\");\n}\n\nfunction toNumber(v, fallback) {\n  const n = typeof v === \"string\" ? parseFloat(v) : v;\n  return Number.isFinite(n) ? n : fallback;\n}\n\nmodule.exports = {\n  calcTextBoxHeightSimple,\n  calcTextBox,\n  autoFontSize,\n};\n"
  },
  {
    "path": "skills/.curated/slides/assets/pptxgenjs_helpers/util.js",
    "content": "// Copyright (c) OpenAI. All rights reserved.\n\"use strict\";\n\n// Safe outer shadow helper (avoid inner/outer mix and XML pitfalls)\nfunction safeOuterShadow(\n  color = \"000000\",\n  opacity = 0.25,\n  angle = 45,\n  blur = 3,\n  offset = 2\n) {\n  return {\n    type: \"outer\",\n    color,\n    opacity,\n    angle,\n    blur,\n    offset,\n  };\n}\n\nmodule.exports = {\n  safeOuterShadow,\n};\n"
  },
  {
    "path": "skills/.curated/slides/references/pptxgenjs-helpers.md",
    "content": "# PptxGenJS Helpers\n\n## When To Read This\n\nRead this file when you need helper API details, command examples for the bundled Python scripts, or dependency notes for a slide-generation task.\n\n## Helper Modules\n\n- `autoFontSize(textOrRuns, fontFace, opts)`: Pick a font size that fits a fixed box.\n- `calcTextBox(fontSizePt, opts)`: Estimate text-box geometry from font size and content.\n- `calcTextBoxHeightSimple(fontSizePt, numLines, leading?, padding?)`: Quick text height estimate.\n- `imageSizingCrop(pathOrData, x, y, w, h)`: Center-crop an image into a target box.\n- `imageSizingContain(pathOrData, x, y, w, h)`: Fit an image fully inside a target box.\n- `svgToDataUri(svgString)`: Convert an SVG string into an embeddable data URI.\n- `latexToSvgDataUri(texString)`: Render LaTeX to SVG for crisp equations.\n- `getImageDimensions(pathOrData)`: Read image width, height, type, and aspect ratio.\n- `safeOuterShadow(...)`: Build a safe outer-shadow config for PowerPoint output.\n- `codeToRuns(source, language)`: Convert source code into rich-text runs for `addText`.\n- `warnIfSlideHasOverlaps(slide, pptx)`: Emit overlap warnings for diagnostics.\n- `warnIfSlideElementsOutOfBounds(slide, pptx)`: Emit boundary warnings for diagnostics.\n- `alignSlideElements(slide, indices, alignment)`: Align selected elements precisely.\n- `distributeSlideElements(slide, indices, direction)`: Evenly space selected elements.\n\n## Dependency Notes\n\nJavaScript helpers expect these packages when you use the corresponding features:\n\n- Core authoring: `pptxgenjs`\n- Text measurement: `skia-canvas`, `linebreak`, `fontkit`\n- Syntax highlighting: `prismjs`\n- LaTeX rendering: `mathjax-full`\n\nPython scripts expect these packages:\n\n- `Pillow`\n- `pdf2image`\n- `python-pptx`\n- `numpy`\n\nSystem tools used by the Python scripts:\n\n- `soffice` / LibreOffice for PPTX to PDF conversion\n- Poppler tools for PDF size/raster support used by `pdf2image`\n- `fc-list` for font inspection\n- Optional rasterization tools for `ensure_raster_image.py`: Inkscape, ImageMagick, Ghostscript, `heif-convert`, `JxrDecApp`\n\n## Script Notes\n\n- `render_slides.py`: Convert a deck to PNGs. Good for visual review and diffing.\n- `slides_test.py`: Add a gray border outside the original canvas, render, and check whether any content leaks into the border.\n- `create_montage.py`: Combine multiple rendered slide images into a single overview image.\n- `detect_font.py`: Distinguish between fonts that are missing entirely and fonts that are installed but substituted during rendering.\n- `ensure_raster_image.py`: Produce a PNG from common vector or unusual raster formats so you can inspect or place the asset easily.\n\n## Practical Rules\n\n- Default to `LAYOUT_WIDE` unless the source material says otherwise.\n- Set font families explicitly before measuring text.\n- Use `valign: \"top\"` for content boxes that may grow.\n- Prefer native PowerPoint charts over rendered images when the chart is simple and likely to be edited later.\n- Use SVG instead of PNG for diagrams whenever possible.\n"
  },
  {
    "path": "skills/.curated/slides/scripts/create_montage.py",
    "content": "#!/usr/bin/env python3\n# Copyright (c) OpenAI. All rights reserved.\nimport argparse\nimport re\nimport sys\nimport tempfile\nfrom math import ceil\nfrom os import listdir\nfrom os.path import basename, expanduser, isfile, join, splitext\nfrom pathlib import Path\nfrom typing import Literal\n\nSCRIPT_DIR = Path(__file__).resolve().parent\nif str(SCRIPT_DIR) not in sys.path:\n    sys.path.insert(0, str(SCRIPT_DIR))\n\nfrom ensure_raster_image import SUPPORTED_EXTS, ensure_raster_image  # type: ignore\nfrom PIL import Image, ImageDraw, ImageFont, ImageOps\n\n\ndef _make_placeholder(w: int, h: int) -> Image.Image:\n    \"\"\"Create a visible placeholder tile with a light gray fill and a red X cross.\"\"\"\n    ph = Image.new(\"RGBA\", (w, h), (220, 220, 220, 255))\n    ph_draw = ImageDraw.Draw(ph)\n    line_color = (180, 0, 0, 255)\n    ph_draw.line([(0, 0), (ph.width - 1, ph.height - 1)], fill=line_color, width=3)\n    ph_draw.line([(ph.width - 1, 0), (0, ph.height - 1)], fill=line_color, width=3)\n    return ph\n\n\ndef _load_images_with_placeholders(\n    input_files: list[str], retain_converted_files: bool, fail_on_image_error: bool = False\n) -> tuple[list[str], list[Image.Image | None]]:\n    labels = [basename(p) for p in input_files]\n    images: list[Image.Image | None] = []\n    if retain_converted_files:\n        for p in input_files:\n            try:\n                images.append(Image.open(ensure_raster_image(p)))\n            except Exception as e:\n                if fail_on_image_error:\n                    raise\n                print(f'Warning: Failed to convert or load image \"{p}\": {e}')\n                images.append(None)\n    else:\n        with tempfile.TemporaryDirectory(prefix=\"montage_convert_\") as tmp_conv:\n            for p in input_files:\n                try:\n                    images.append(Image.open(ensure_raster_image(p, tmp_conv)))\n                except Exception as e:\n                    if fail_on_image_error:\n                        raise\n                    print(f'Warning: Failed to convert or load image \"{p}\": {e}')\n                    images.append(None)\n    return labels, images\n\n\ndef _natural_key(s: str) -> list:\n    \"\"\"Key function for natural sorting (e.g., Slide2 before Slide10).\"\"\"\n    return [int(part) if part.isdigit() else part for part in re.split(r\"(\\d+)\", s)]\n\n\ndef create_montage(\n    input_files: list[str],\n    output_file: str,\n    num_col: int,\n    cell_w: int,\n    cell_h: int,\n    gap: int,\n    label_mode: Literal[\"number\", \"filename\", \"none\"],\n    retain_converted_files: bool = False,\n    fail_on_image_error: bool = False,\n) -> None:\n    \"\"\"Build a montage with a fixed number of columns.\n\n    Each cell has size `cell_w` x `cell_h`. Every input image is resized isotropically to fit inside\n    the cell. `gap` controls spacing around and between cells (outer margin equals gap).\n    Label behavior is controlled by `label_mode` which can be one of:\n      - \"none\": no labels are drawn\n      - \"number\": draw a 1-based index beneath each image\n      - \"filename\": draw the filename (no directory) beneath each image\n    \"\"\"\n\n    if num_col <= 0:\n        raise ValueError(\"num_col must be positive\")\n    if cell_w <= 0 or cell_h <= 0:\n        raise ValueError(\"cell_w and cell_h must be positive\")\n\n    labels, images = _load_images_with_placeholders(\n        input_files=input_files,\n        retain_converted_files=retain_converted_files,\n        fail_on_image_error=fail_on_image_error,\n    )\n\n    num_images = len(images)\n    num_valid = sum(1 for im in images if im is not None)\n    if num_valid == 0:\n        raise ValueError(\"No valid images to render.\")\n    if num_valid < num_images:\n        cell_size = round(min(cell_w, cell_h) * 0.6)\n        placeholder = _make_placeholder(cell_size, cell_size)\n    else:\n        placeholder = None\n    cols = num_col\n    rows = ceil(num_images / cols)\n\n    temp_canvas = Image.new(\"RGB\", (10, 10), (255, 255, 255))\n    temp_draw = ImageDraw.Draw(temp_canvas)\n\n    # Choose a readable default font size relative to cell height\n    font: ImageFont.FreeTypeFont | ImageFont.ImageFont\n    try:\n        # Attempt to use a common system font for clarity; fallback to default\n        font_size = max(12, min(36, int(cell_h * 0.12)))\n        font = ImageFont.truetype(\"arial.ttf\", font_size)\n    except Exception:\n        font = ImageFont.load_default()\n        # Adjust default font effect size estimate\n        font_size = 12\n\n    draw_labels = label_mode != \"none\"\n    label_height = 0\n    if draw_labels:\n        # Height is approximately constant across strings for a given font\n        # Use 'Ag' to approximate ascent ('A') and descender ('g') for filename text\n        sample_text = \"1\" if label_mode == \"number\" else \"Ag\"\n        lbbox = temp_draw.textbbox((0, 0), sample_text, font=font)\n        label_height = ceil(lbbox[3] - lbbox[1]) + 6\n\n    row_h = cell_h + label_height\n\n    canvas_w = cols * cell_w + (cols + 1) * gap\n    canvas_h = rows * row_h + (rows + 1) * gap\n    # Light grey canvas background as in typical slide sorter view\n    canvas = Image.new(\"RGB\", (canvas_w, canvas_h), (242, 242, 242))\n    draw = ImageDraw.Draw(canvas)\n\n    for idx, img in enumerate(images):\n        col = idx % cols\n        row = idx // cols\n\n        # Top-left corner of the cell including outer margin and gaps\n        x0 = gap + col * (cell_w + gap)\n        y0 = gap + row * (row_h + gap)\n\n        # Fit the image within the cell while preserving aspect ratio\n        if label_mode == \"number\":\n            label = str(idx + 1)\n        elif label_mode == \"filename\":\n            label = labels[idx]\n        else:\n            label = \"\"\n\n        if draw_labels:\n            bbox = draw.textbbox((0, 0), label, font=font)\n            text_w = bbox[2] - bbox[0]\n        else:\n            text_w = 0\n\n        if img:\n            resized = ImageOps.contain(\n                img.convert(\"RGBA\"),\n                (cell_w, cell_h),\n                method=Image.Resampling.LANCZOS,\n            )\n        else:\n            print(f\"Warning: Using placeholder for invalid image at row={row + 1}, col={col + 1}\")\n            assert placeholder is not None\n            resized = placeholder\n\n        paste_x = x0 + (cell_w - resized.width) // 2\n        paste_y = y0 + (cell_h - resized.height) // 2\n        canvas.paste(\n            resized,\n            (paste_x, paste_y),\n            mask=resized.split()[3] if resized.mode == \"RGBA\" else None,\n        )\n\n        border_color = (160, 160, 160)\n        bw = 1\n        draw.rectangle(\n            [\n                paste_x - bw,\n                paste_y - bw,\n                paste_x + resized.width,\n                paste_y + resized.height,\n            ],\n            outline=border_color,\n            width=bw,\n        )\n\n        if draw_labels:\n            tx = x0 + round((cell_w - text_w) / 2)\n            ty = y0 + cell_h + 3\n            draw.text((tx, ty), label, font=font, fill=(0, 0, 0))\n\n    canvas.save(output_file)\n    print(f\"Montage saved to {output_file}\")\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        description=(\n            \"Create a montage with a fixed number of columns. \"\n            \"Each image is resized isotropically to fit inside a cell of size (cell_width x cell_height).\"\n        )\n    )\n    group = parser.add_mutually_exclusive_group(required=True)\n    group.add_argument(\"--input_files\", nargs=\"+\", help=\"List of input image file paths\")\n    group.add_argument(\"--input_dir\", help=\"Directory containing input images\")\n    parser.add_argument(\n        \"--output_file\",\n        required=True,\n        help=(\n            \"Path to save the output montage image. The format is inferred from the file extension.\"\n        ),\n    )\n    parser.add_argument(\n        \"--num_col\",\n        type=int,\n        default=5,\n        help=\"Number of images per row (default: 5)\",\n    )\n    parser.add_argument(\n        \"--cell_width\",\n        type=int,\n        default=400,\n        help=\"Container width in pixels for each image (default: 400)\",\n    )\n    parser.add_argument(\n        \"--cell_height\",\n        type=int,\n        default=225,\n        help=\"Container height in pixels for each image (default: 225)\",\n    )\n    parser.add_argument(\n        \"--gap\",\n        type=int,\n        default=16,\n        help=\"Gap in pixels between images and canvas margins (default: 16)\",\n    )\n    parser.add_argument(\n        \"--label_mode\",\n        choices=[\"number\", \"filename\", \"none\"],\n        default=\"number\",\n        help=(\n            \"Label mode: 'number' to draw 1-based indices (default), 'filename' to use the \"\n            \"image's filename (no directory), or 'none' for no labels\"\n        ),\n    )\n    parser.add_argument(\n        \"--retain_converted_files\",\n        action=\"store_true\",\n        default=False,\n        help=(\n            \"If set, write converted images (e.g., SVG->PNG, WDP->PNG) next to the original files \"\n            \"instead of a temporary directory.\"\n        ),\n    )\n    parser.add_argument(\n        \"--fail_on_image_error\",\n        action=\"store_true\",\n        default=False,\n        help=(\n            \"If set, fail immediately when any image conversion/loading fails (no placeholders). \"\n            \"By default, failures are tolerated and placeholders are used.\"\n        ),\n    )\n    args = parser.parse_args()\n\n    output_path = expanduser(args.output_file)\n    if args.input_files:\n        input_files = [expanduser(p) for p in args.input_files]\n    else:\n        input_dir = expanduser(args.input_dir)\n        names = sorted(listdir(input_dir), key=_natural_key)\n        dir_entries = [join(input_dir, f) for f in names]\n        input_files = [\n            p for p in dir_entries if isfile(p) and splitext(p)[1].lower() in SUPPORTED_EXTS\n        ]\n        if not input_files:\n            raise ValueError(\n                \"No image files with supported extensions were found in the specified directory.\"\n            )\n\n    create_montage(\n        input_files=input_files,\n        output_file=output_path,\n        num_col=args.num_col,\n        cell_w=args.cell_width,\n        cell_h=args.cell_height,\n        gap=args.gap,\n        label_mode=args.label_mode,\n        retain_converted_files=args.retain_converted_files,\n        fail_on_image_error=args.fail_on_image_error,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/slides/scripts/detect_font.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Copyright (c) OpenAI. All rights reserved.\n\nDetect missing fonts for PPTX rendering by converting to ODP and inspecting the resolved font\nfamilies per slide.\n\nOverview\n========\nPowerPoint files (PPTX) declare requested font families in runs and theme defaults, but the actual\nfont used at render time depends on the renderer (LibreOffice in our pipeline), platform\navailability, and style inheritance. To make detection stable and renderer-accurate, this module:\n\n- Extracts requested families from PPTX per slide (reads a:r/a:rPr plus document defaults, grouped\n  by script: latin/ea/cs/sym). Analysis is done per run: we infer the script from run text and\n  select the matching a:rPr child (e.g., latin/ea/cs). Fonts declared for other scripts in the same\n  run are not counted as used.\n- Converts the PPTX to ODP using headless LibreOffice and parses ODP content.xml and styles.xml to\n  discover which families LibreOffice actually resolved for each slide (including master pages and\n  defaults).\n- Classifies each requested family on each slide into two buckets:\n  - font_missing: the family is not installed on the system (per fontconfig synonyms), so resolution\n    cannot possibly match the request.\n  - font_substituted: the family is installed but was resolved to another family in ODP for the\n    slide (theme/style inheritance or glyph coverage), i.e., installed but substituted.\n\nKey Design\n-----------------------\n1) Inspect the renderer's decision, not only the author's request. Reading PPTX alone tells you what\n   was requested, not what LibreOffice will choose after applying styles and availability checks.\n   Converting to ODP and reading the resolved fo:font-family/style:font-name* values yields a\n   faithful view of what the renderer actually used for each slide.\n\n2) Robust style resolution across ODP structures. Fonts can be specified under multiple layers. We\n   parse office:automatic-styles (both content.xml and styles.xml), office:styles and\n   style:default-style, draw:master-page references used by slides, nested style:text-properties\n   under paragraph-properties, and parent style chains (style:parent-style-name). A text-based\n   fallback parser supplements XML namespace lookups when vendor XML variations occur.\n\n3) Scalable aliasing via fontconfig synonyms, not ad hoc maps. PostScript names, full names, and\n   family names often differ. We build a synonym map from fc-list that unifies those identifiers. We\n   deliberately do NOT use fc-match -s fallback chains for matching, because fallback families\n   (e.g., DejaVu Sans) would mask missing/substitution cases and produce false passes.\n\n4) Clear classification: missing vs substituted.\n   - Missing: no synonym of the requested base family is present in the installed font set (per\n     fontconfig). These require installation.\n   - Substituted: the family is installed, but ODP does not reference it on the slide (LibreOffice\n     chose another family), which is useful for diagnosing style/theme issues or glyph-coverage\n     driven substitutions.\n\nNot Chosen (and why)\n--------------------\n- PDF inspection (e.g., pdffonts): PostScript names don't reliably map back to authoring families;\n  PDFs often reflect subsetted fonts and fallback choices, making robust detection noisy.\n- Ad hoc alias tables: unscalable for large-scale fonts and platform variants; the fontconfig\n  synonym corpus covers family/fullname/PostScript consistently.\n- Treating fallback families as matches (fc-match -s): causes false negatives by accepting generic\n  fallbacks when the requested family is missing.\n- Hardcoding checks in the renderer: we keep detection separate from render_slides to avoid\n  coupling and allow standalone checking.\n\nCLI\n---\n- JSON output exposes two categories by default (and text mode mirrors them): font_missing_overall/\n  font_missing_by_slide and font_substituted_overall/font_substituted_by_slide.\n- Flags include_missing/include_substituted control which categories are emitted (default True/True).\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport shutil\nimport subprocess\nimport tempfile\nimport xml.etree.ElementTree as ET\nfrom functools import lru_cache\nfrom os.path import abspath, basename, exists, expanduser, join, splitext\nfrom zipfile import ZipFile\n\nSTYLE_TOKENS = [\n    \"regular\",\n    \"condensed\",\n    \"compressed\",\n    \"narrow\",\n    \"italic\",\n    \"oblique\",\n    \"semibold\",\n    \"demibold\",\n    \"bold\",\n    \"black\",\n    \"extra light\",\n    \"ultra light\",\n    \"extralight\",\n    \"ultralight\",\n    \"light\",\n    \"thin\",\n    \"medium\",\n]\n\n\ndef normalize_font_family_name(name: str) -> str:\n    s = name.casefold()\n    s = re.sub(r\"\\([^)]*\\)\", \" \", s)\n    s = re.sub(r\"[\\s\\-\\_\\.,/\\'\\\"]+\", \" \", s)\n    return s.strip()\n\n\ndef _or_dummy(node: ET.Element | None) -> ET.Element:\n    \"\"\"Return the element if not None, otherwise a harmless dummy element.\n\n    Avoids deprecated truthiness checks on Element instances (`elem or dummy`).\n    \"\"\"\n    return node if node is not None else ET.Element(\"dummy\")\n\n\n@lru_cache(maxsize=1)\ndef _build_fc_synonym_map() -> dict[str, set[str]]:\n    \"\"\"Build synonym map from fontconfig; raise on failures; memoized (size=1).\"\"\"\n    proc = subprocess.run(\n        [\n            \"fc-list\",\n            \"--format\",\n            \"%{family}\\t%{fullname}\\t%{postscriptname}\\n\",\n        ],\n        capture_output=True,\n        text=True,\n        check=True,\n    )\n    syn: dict[str, set[str]] = {}\n    for line in (proc.stdout or \"\").splitlines():\n        parts = line.split(\"\\t\")\n        if len(parts) != 3:\n            continue\n        fam_field, full_field, ps_field = parts\n        names: set[str] = set()\n        for field in (fam_field, full_field, ps_field):\n            for item in field.split(\",\"):\n                norm = normalize_font_family_name(item)\n                if norm:\n                    names.add(norm)\n                    names.add(norm.replace(\" \", \"\"))\n        for name in list(names):\n            bucket = syn.setdefault(name, set())\n            bucket.update(names)\n    return syn\n\n\ndef _expand_via_fontconfig(family_base_norm: str) -> set[str]:\n    # Accept only true aliases/synonyms (family/fullname/PostScript) — not fallback replacements\n    acceptable: set[str] = {family_base_norm, family_base_norm.replace(\" \", \"\")}\n    syn = _build_fc_synonym_map()\n    if family_base_norm in syn:\n        acceptable.update(syn[family_base_norm])\n    no_space = family_base_norm.replace(\" \", \"\")\n    if no_space in syn:\n        acceptable.update(syn[no_space])\n    return acceptable\n\n\ndef parse_font_family_base_and_styles(name_norm: str) -> tuple[str, set[str]]:\n    tokens = name_norm.split()\n    required: set[str] = set()\n    weight_code_map = {\n        \"25\": \"ultra light\",\n        \"35\": \"thin\",\n        \"45\": \"light\",\n        \"55\": \"regular\",\n        \"65\": \"medium\",\n        \"75\": \"bold\",\n        \"85\": \"black\",\n        \"95\": \"black\",\n    }\n    if tokens and tokens[0].isdigit() and tokens[0] in weight_code_map:\n        required.add(weight_code_map[tokens[0]])\n        tokens = tokens[1:]\n    if len(tokens) == 1:\n        t = tokens[0]\n        fused_map = [\n            (\"extralight\", \"extra light\"),\n            (\"ultralight\", \"ultra light\"),\n            (\"semibold\", \"semibold\"),\n            (\"demibold\", \"semibold\"),\n            (\"condensed\", \"condensed\"),\n            (\"compressed\", \"condensed\"),\n            (\"narrow\", \"condensed\"),\n            (\"italic\", \"italic\"),\n            (\"oblique\", \"italic\"),\n            (\"bold\", \"bold\"),\n            (\"black\", \"black\"),\n            (\"light\", \"light\"),\n            (\"thin\", \"thin\"),\n            (\"medium\", \"medium\"),\n            (\"regular\", \"regular\"),\n        ]\n        changed = True\n        while changed:\n            changed = False\n            for suf, tok in fused_map:\n                if t.endswith(suf) and len(t) > len(suf):\n                    t = t[: -len(suf)]\n                    required.add(tok)\n                    changed = True\n                    break\n        return (t.strip(), required)\n\n    while tokens:\n        tail = \" \".join(tokens[-2:]) if len(tokens) >= 2 else tokens[-1]\n        matched = None\n        for style in STYLE_TOKENS:\n            if tail == style:\n                matched = style\n                break\n        if matched is None and tokens[-1] in STYLE_TOKENS:\n            matched = tokens[-1]\n        if matched is None:\n            break\n        if matched in (\"compressed\", \"narrow\"):\n            required.add(\"condensed\")\n        elif matched == \"roman\":\n            required.add(\"regular\")\n        elif matched == \"demibold\":\n            required.add(\"semibold\")\n        else:\n            required.add(matched)\n        if \" \" in matched:\n            tokens = tokens[:-2]\n        else:\n            tokens = tokens[:-1]\n    return (\" \".join(tokens).strip(), required)\n\n\ndef _split_odf_family_list(value: str) -> list[str]:\n    out: list[str] = []\n    for part in value.split(\",\"):\n        p = part.strip().strip(\"\\\"' \")\n        if p:\n            out.append(normalize_font_family_name(p))\n    return out\n\n\ndef extract_used_fonts_from_pptx(pptx_path: str) -> dict[int, set[str]]:\n    by_slide: dict[int, set[str]] = {}\n    with ZipFile(pptx_path, \"r\") as zf:\n        for name in zf.namelist():\n            if not (name.startswith(\"ppt/slides/slide\") and name.endswith(\".xml\")):\n                continue\n            base = os.path.basename(name)\n            m = re.search(r\"(?i)slide(\\d+)\\.xml$\", base)\n            slide_num = int(m.group(1)) if m else None\n            with zf.open(name) as f:\n                tree = ET.parse(f)\n            root = tree.getroot()\n            ns = {\"a\": \"http://schemas.openxmlformats.org/drawingml/2006/main\"}\n            defaults = _collect_default_font_faces(root)\n            for r in root.findall(\".//a:r\", ns):\n                parts: list[str] = []\n                for t in r.findall(\"a:t\", ns):\n                    if t.text:\n                        parts.append(t.text)\n                text = \"\".join(parts)\n                if not text:\n                    continue\n                script = _detect_script_tag(text)\n                rpr = r.find(\"a:rPr\", ns)\n                face_norm: str | None = None\n                if rpr is not None:\n                    child = rpr.find(f\"a:{script}\", ns)\n                    if child is not None:\n                        face = child.get(\"typeface\")\n                        if face and not face.startswith(\"+\"):\n                            face_norm = normalize_font_family_name(face)\n                bucket = by_slide.setdefault(slide_num or -1, set())\n                if face_norm is None:\n                    for f in defaults.get(script, set()):\n                        bucket.add(f)\n                else:\n                    bucket.add(face_norm)\n    return {k: v for k, v in by_slide.items() if k is not None and k != -1}\n\n\ndef _detect_script_tag(text: str) -> str:\n    for ch in text:\n        cp = ord(ch)\n        if (\n            0x4E00 <= cp <= 0x9FFF\n            or 0x3400 <= cp <= 0x4DBF\n            or 0xF900 <= cp <= 0xFAFF\n            or 0x3040 <= cp <= 0x309F\n            or 0x30A0 <= cp <= 0x30FF\n            or 0x31F0 <= cp <= 0x31FF\n            or 0xAC00 <= cp <= 0xD7AF\n            or 0x3100 <= cp <= 0x312F\n            or 0x3000 <= cp <= 0x303F\n        ):\n            return \"ea\"\n    for ch in text:\n        cp = ord(ch)\n        if (\n            0x0590 <= cp <= 0x05FF\n            or 0x0600 <= cp <= 0x06FF\n            or 0x0700 <= cp <= 0x077F\n            or 0x0780 <= cp <= 0x07BF\n            or 0x0900 <= cp <= 0x0D7F\n            or 0x0E00 <= cp <= 0x0E7F\n            or 0x0E80 <= cp <= 0x0EFF\n            or 0xFB50 <= cp <= 0xFDFF\n            or 0xFE70 <= cp <= 0xFEFF\n        ):\n            return \"cs\"\n    for ch in text:\n        cp = ord(ch)\n        if (\n            (0x0041 <= cp <= 0x005A)\n            or (0x0061 <= cp <= 0x007A)\n            or (0x0030 <= cp <= 0x0039)\n            or (0x00C0 <= cp <= 0x024F)\n            or (0x1E00 <= cp <= 0x1EFF)\n        ):\n            return \"latin\"\n    return \"latin\"\n\n\ndef _collect_default_font_faces(root: ET.Element) -> dict[str, set[str]]:\n    ns = {\"a\": \"http://schemas.openxmlformats.org/drawingml/2006/main\"}\n    defaults: dict[str, set[str]] = {\"latin\": set(), \"ea\": set(), \"cs\": set(), \"sym\": set()}\n    for defrpr in root.findall(\".//a:defRPr\", ns):\n        for tag in (\"latin\", \"ea\", \"cs\", \"sym\"):\n            child = defrpr.find(f\"a:{tag}\", ns)\n            if child is not None:\n                face = child.get(\"typeface\")\n                if face and not face.startswith(\"+\"):\n                    defaults[tag].add(normalize_font_family_name(face))\n    return defaults\n\n\ndef _run_soffice_convert(cmd: list[str]) -> None:\n    subprocess.run(\n        cmd,\n        check=False,\n        stdout=subprocess.DEVNULL,\n        stderr=subprocess.DEVNULL,\n        env=os.environ.copy(),\n    )\n\n\ndef _export_to_odp(pptx_path: str, user_profile: str, out_dir: str, stem: str) -> str:\n    bin_path = shutil.which(\"soffice\") or shutil.which(\"libreoffice\") or \"/usr/bin/libreoffice\"\n    cmd_odp = [\n        bin_path,\n        \"-env:UserInstallation=file://\" + user_profile,\n        \"--invisible\",\n        \"--headless\",\n        \"--norestore\",\n        \"--convert-to\",\n        \"odp\",\n        \"--outdir\",\n        out_dir,\n        pptx_path,\n    ]\n    _run_soffice_convert(cmd_odp)\n    odp_path = join(out_dir, f\"{stem}.odp\")\n    return odp_path if exists(odp_path) else \"\"\n\n\ndef _collect_face_map(root: ET.Element, ns: dict[str, str]) -> dict[str, str]:\n    face_map: dict[str, str] = {}\n    decls = root.find(\"office:font-face-decls\", ns)\n    if decls is None:\n        return face_map\n    for ff in decls.findall(\"style:font-face\", ns):\n        name_attr = ff.get(\"{urn:oasis:names:tc:opendocument:xmlns:style:1.0}name\") or ff.get(\n            \"style:name\"\n        )\n        fam_attr = ff.get(\"{urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0}font-family\")\n        if not name_attr or not fam_attr:\n            continue\n        face_map[normalize_font_family_name(name_attr)] = normalize_font_family_name(fam_attr)\n    return face_map\n\n\ndef _families_from_text_properties(\n    tp: ET.Element, ns: dict[str, str], face_map: dict[str, str]\n) -> set[str]:\n    fams: set[str] = set()\n    # Inspect current node for direct font-family\n    fam_attr = tp.get(\"{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}font-family\")\n    if fam_attr:\n        fams.update(_split_odf_family_list(fam_attr))\n    # Inspect font-name aliases on current node\n    for key in (\n        \"{urn:oasis:names:tc:opendocument:xmlns:style:1.0}font-name\",\n        \"style:font-name\",\n        \"style:font-name-asian\",\n        \"style:font-name-complex\",\n    ):\n        val = tp.get(key)\n        if val:\n            norm_val = normalize_font_family_name(val)\n            mapped = face_map.get(norm_val)\n            if mapped:\n                fams.add(normalize_font_family_name(mapped))\n            else:\n                fams.add(norm_val)\n    # Some styles nest text-properties under paragraph-properties or default-style blocks\n    if not fams:\n        nested = None\n        # paragraph-properties/text-properties\n        pp = tp.find(\"style:paragraph-properties\", ns)\n        if pp is not None:\n            nested = pp.find(\"style:text-properties\", ns)\n        if nested is None:\n            # When tp is actually the style:style node, try finding child text-properties directly\n            nested = tp.find(\"style:text-properties\", ns)\n        if nested is not None and nested is not tp:\n            fams.update(_families_from_text_properties(nested, ns, face_map))\n    return fams\n\n\ndef _extract_styles_from_container(\n    container: ET.Element | None, ns: dict[str, str], face_map: dict[str, str]\n) -> tuple[dict[str, set[str]], set[str]]:\n    styles: dict[str, set[str]] = {}\n    defaults: set[str] = set()\n    if container is None:\n        return styles, defaults\n    for st in container.findall(\"style:style\", ns):\n        name = st.get(\"{urn:oasis:names:tc:opendocument:xmlns:style:1.0}name\") or st.get(\n            \"style:name\"\n        )\n        if not name:\n            continue\n        fams = _families_from_text_properties(\n            _or_dummy(st.find(\"style:text-properties\", ns)), ns, face_map\n        )\n        if fams:\n            styles[name] = fams\n    for ds in container.findall(\"style:default-style\", ns):\n        defaults.update(\n            _families_from_text_properties(\n                _or_dummy(ds.find(\"style:text-properties\", ns)), ns, face_map\n            )\n        )\n    return styles, defaults\n\n\ndef _build_style_map(\n    content: ET.Element,\n    styles_root: ET.Element | None,\n    ns: dict[str, str],\n    face_map: dict[str, str],\n) -> tuple[dict[str, set[str]], set[str]]:\n    style_map: dict[str, set[str]] = {}\n    default_fams: set[str] = set()\n    auto_styles = content.find(\"office:automatic-styles\", ns)\n    styles_part, defaults_part = _extract_styles_from_container(auto_styles, ns, face_map)\n    style_map.update(styles_part)\n    default_fams.update(defaults_part)\n    if styles_root is not None:\n        # Also parse automatic-styles within styles.xml (document-styles)\n        styles_auto = styles_root.find(\"office:automatic-styles\", ns)\n        styles_part, defaults_part = _extract_styles_from_container(styles_auto, ns, face_map)\n        for k, v in styles_part.items():\n            if k not in style_map:\n                style_map[k] = v\n        default_fams.update(defaults_part)\n        common_styles = styles_root.find(\"office:styles\", ns)\n        styles_part, defaults_part = _extract_styles_from_container(common_styles, ns, face_map)\n        for k, v in styles_part.items():\n            if k not in style_map:\n                style_map[k] = v\n        default_fams.update(defaults_part)\n        # top-level default-style under styles_root\n        for ds in styles_root.findall(\"style:default-style\", ns):\n            default_fams.update(\n                _families_from_text_properties(\n                    _or_dummy(ds.find(\"style:text-properties\", ns)), ns, face_map\n                )\n            )\n        # Fallback: include any remaining style:style definitions anywhere in styles.xml\n        for st in styles_root.findall(\".//style:style\", ns):\n            name = st.get(\"{urn:oasis:names:tc:opendocument:xmlns:style:1.0}name\") or st.get(\n                \"style:name\"\n            )\n            if not name or name in style_map:\n                continue\n            fams = _families_from_text_properties(\n                _or_dummy(st.find(\"style:text-properties\", ns)), ns, face_map\n            )\n            if fams:\n                style_map[name] = fams\n    # also check top-level default-style in content root\n    for ds in content.findall(\"style:default-style\", ns):\n        default_fams.update(\n            _families_from_text_properties(\n                _or_dummy(ds.find(\"style:text-properties\", ns)), ns, face_map\n            )\n        )\n    # Fallback: include any remaining style:style definitions anywhere in content.xml\n    for st in content.findall(\".//style:style\", ns):\n        name = st.get(\"{urn:oasis:names:tc:opendocument:xmlns:style:1.0}name\") or st.get(\n            \"style:name\"\n        )\n        if not name or name in style_map:\n            continue\n        fams = _families_from_text_properties(\n            _or_dummy(st.find(\"style:text-properties\", ns)), ns, face_map\n        )\n        if fams:\n            style_map[name] = fams\n    return style_map, default_fams\n\n\ndef _lookup_style_families(\n    style_name: str, ns: dict[str, str], face_map: dict[str, str], roots: list[ET.Element | None]\n) -> set[str]:\n    fams: set[str] = set()\n    if not style_name:\n        return fams\n    visited: set[str] = set()\n\n    def _resolve(name: str) -> None:\n        if not name or name in visited:\n            return\n        visited.add(name)\n        for root in roots:\n            if root is None:\n                continue\n            node = root.find(f\".//style:style[@style:name='{name}']\", ns)\n            if node is None:\n                node = root.find(f\".//style:style[@{{{ns['style']}}}name='{name}']\", ns)\n            if node is None:\n                continue\n            fams.update(\n                _families_from_text_properties(\n                    _or_dummy(node.find(\"style:text-properties\", ns)), ns, face_map\n                )\n            )\n            # Follow parent style chain if present\n            parent = node.get(\n                \"{urn:oasis:names:tc:opendocument:xmlns:style:1.0}parent-style-name\"\n            ) or node.get(\"style:parent-style-name\")\n            if parent:\n                _resolve(parent)\n\n    _resolve(style_name)\n    return fams\n\n\ndef _collect_slide_families(\n    page: ET.Element,\n    ns: dict[str, str],\n    style_map: dict[str, set[str]],\n    face_map: dict[str, str],\n    roots: list[ET.Element | None],\n    text_style_map: dict[str, set[str]] | None = None,\n) -> set[str]:\n    slide_fams: set[str] = set()\n    for el in page.iter():\n        fam_attr = el.get(\n            \"{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}font-family\"\n        )\n        if fam_attr:\n            slide_fams.update(_split_odf_family_list(fam_attr))\n        for attr in (\n            \"{urn:oasis:names:tc:opendocument:xmlns:text:1.0}style-name\",\n            \"text:style-name\",\n            \"{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}text-style-name\",\n            \"draw:text-style-name\",\n            \"draw:style-name\",\n            \"presentation:style-name\",\n        ):\n            style_name = el.get(attr)\n            if not style_name:\n                continue\n            resolved_fams: set[str] = set()\n            if style_name in style_map:\n                resolved_fams.update(style_map[style_name])\n            if not resolved_fams:\n                # Fallback: resolve on the fly from XML if not present in prebuilt style_map\n                resolved_fams.update(_lookup_style_families(style_name, ns, face_map, roots))\n            if not resolved_fams and text_style_map and style_name in text_style_map:\n                resolved_fams.update(text_style_map[style_name])\n            if resolved_fams:\n                slide_fams.update(resolved_fams)\n    return slide_fams\n\n\ndef _build_style_map_text(xml_text: str) -> dict[str, set[str]]:\n    # Best-effort textual extraction for cases missed by XML namespace lookups\n    # Finds style:style name=\"X\" blocks and extracts fo:font-family and style:font-name attributes\n    style_map: dict[str, set[str]] = {}\n    # Non-greedy match of a style:style block\n    for m in re.finditer(\n        r\"<style:style[^>]*?\\bstyle:name=\\\"([^\\\"]+)\\\"[\\s\\S]*?(?:</style:style>)\",\n        xml_text,\n        flags=re.IGNORECASE,\n    ):\n        name = m.group(1).strip()\n        block = m.group(0)\n        fams: set[str] = set()\n        # fo:font-family may be a comma list\n        mff = re.search(r\"fo:font-family=\\\"([^\\\"]+)\\\"\", block, flags=re.IGNORECASE)\n        if mff:\n            for f in _split_odf_family_list(mff.group(1)):\n                fams.add(f)\n        # style:font-name may be a face alias; treat as family directly if present\n        mfn = re.search(r\"style:font-name=\\\"([^\\\"]+)\\\"\", block, flags=re.IGNORECASE)\n        if mfn:\n            fams.add(normalize_font_family_name(mfn.group(1)))\n        if fams:\n            style_map[name] = fams\n    return style_map\n\n\ndef _extract_slide_families_from_odp(odp_path: str) -> dict[int, set[str]]:\n    ns = {\n        \"office\": \"urn:oasis:names:tc:opendocument:xmlns:office:1.0\",\n        \"style\": \"urn:oasis:names:tc:opendocument:xmlns:style:1.0\",\n        \"fo\": \"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0\",\n        \"draw\": \"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0\",\n        \"text\": \"urn:oasis:names:tc:opendocument:xmlns:text:1.0\",\n    }\n    by_slide: dict[int, set[str]] = {}\n    with ZipFile(odp_path, \"r\") as zf:\n        content_bytes = zf.read(\"content.xml\")\n        styles_bytes = zf.read(\"styles.xml\") if \"styles.xml\" in zf.namelist() else None\n        content = ET.fromstring(content_bytes)\n        styles_root = ET.fromstring(styles_bytes) if styles_bytes is not None else None\n        styles_text = (\n            styles_bytes.decode(\"utf-8\", errors=\"ignore\") if styles_bytes is not None else \"\"\n        )\n\n        face_map: dict[str, str] = {}\n        face_map.update(_collect_face_map(content, ns))\n        if styles_root is not None:\n            face_map.update(_collect_face_map(styles_root, ns))\n\n        style_map, default_fams = _build_style_map(content, styles_root, ns, face_map)\n        # Augment style_map with textual parsing fallback (helps with tricky namespace emissions)\n        text_style_map: dict[str, set[str]] = {}\n        if styles_text:\n            text_style_map = _build_style_map_text(styles_text)\n            for k, v in text_style_map.items():\n                if k not in style_map:\n                    style_map[k] = v\n\n        master_map: dict[str, set[str]] = _build_master_page_map(styles_root, ns, style_map)\n\n        pres = content.find(\"office:body\", ns)\n        if pres is not None:\n            pres = pres.find(\"office:presentation\", ns)\n        if pres is None:\n            return {}\n        pages = pres.findall(\"draw:page\", ns)\n        global_fams: set[str] = set()\n        for idx, page in enumerate(pages, start=1):\n            slide_fams = _collect_slide_families(\n                page, ns, style_map, face_map, [content, styles_root], text_style_map\n            )\n            mp_name = page.get(\n                \"{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}master-page-name\"\n            ) or page.get(\"draw:master-page-name\")\n            if mp_name and mp_name in master_map:\n                slide_fams.update(master_map[mp_name])\n            # If theme placeholders like +mn lt are present, augment with defaults\n            if any(f.startswith(\"+\") for f in slide_fams) and default_fams:\n                slide_fams.update(default_fams)\n            if not slide_fams and default_fams:\n                slide_fams.update(default_fams)\n            expanded: set[str] = set()\n            for f in slide_fams:\n                base, _ = parse_font_family_base_and_styles(f)\n                expanded.add(f)\n                expanded.add(base)\n                expanded.add(base.replace(\" \", \"\"))\n            by_slide[idx] = expanded\n            global_fams.update(expanded)\n        # As a last resort, use global families\n        if global_fams:\n            for idx in list(by_slide.keys()):\n                if not by_slide[idx]:\n                    by_slide[idx] = set(global_fams)\n                elif all(f.startswith(\"+\") for f in by_slide[idx]):\n                    by_slide[idx].update(global_fams)\n    return by_slide\n\n\ndef _build_master_page_map(\n    styles_root: ET.Element | None, ns: dict[str, str], style_map: dict[str, set[str]]\n) -> dict[str, set[str]]:\n    master_map: dict[str, set[str]] = {}\n    if styles_root is None:\n        return master_map\n    master_styles = styles_root.find(\"office:master-styles\", ns)\n    if master_styles is None:\n        return master_map\n    for mp in master_styles.findall(\"draw:master-page\", ns):\n        mname = mp.get(\"{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}name\") or mp.get(\n            \"draw:name\"\n        )\n        if not mname:\n            continue\n        fams: set[str] = set()\n        for el in mp.iter():\n            fam_attr = el.get(\n                \"{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}font-family\"\n            )\n            if fam_attr:\n                fams.update(_split_odf_family_list(fam_attr))\n            for attr in (\n                \"{urn:oasis:names:tc:opendocument:xmlns:text:1.0}style-name\",\n                \"text:style-name\",\n                \"{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}text-style-name\",\n                \"draw:text-style-name\",\n                \"draw:style-name\",\n                \"presentation:style-name\",\n            ):\n                sname = el.get(attr)\n                if sname and sname in style_map:\n                    fams.update(style_map[sname])\n        if fams:\n            expanded: set[str] = set()\n            for f in fams:\n                base, _ = parse_font_family_base_and_styles(f)\n                expanded.add(f)\n                expanded.add(base)\n                expanded.add(base.replace(\" \", \"\"))\n            master_map[mname] = expanded\n    return master_map\n\n\ndef detect_missing_fonts_odp(pptx_path: str) -> tuple[set[str], dict[int, list[str]]]:\n    pptx_path = abspath(pptx_path)\n    used = extract_used_fonts_from_pptx(pptx_path)\n    with tempfile.TemporaryDirectory(prefix=\"soffice_profile_\") as prof:\n        with tempfile.TemporaryDirectory(prefix=\"soffice_convert_\") as out:\n            stem = splitext(basename(pptx_path))[0]\n            odp_path = _export_to_odp(pptx_path, prof, out, stem)\n            if not odp_path:\n                return set(), {}\n            slide_fams = _extract_slide_families_from_odp(odp_path)\n\n    missing_overall: set[str] = set()\n    missing_by_slide: dict[int, list[str]] = {}\n    syn_map = _build_fc_synonym_map()\n    for slide_num, req_fams in used.items():\n        odp_fams = slide_fams.get(slide_num, set())\n        slide_missing: list[str] = []\n        for req in req_fams:\n            fam_base, _ = parse_font_family_base_and_styles(req)\n            # Accept fontconfig-resolved aliases and no-space variants for the requested base family\n            acceptable: set[str] = _expand_via_fontconfig(fam_base)\n            # Determine if any acceptable alias is actually installed on system\n            installed = any(alias in syn_map for alias in acceptable)\n            # Missing if not installed at all, or if installed but not resolved in ODP families\n            if (not installed) or ((req not in odp_fams) and not (acceptable & odp_fams)):\n                slide_missing.append(req)\n                missing_overall.add(req)\n        if slide_missing:\n            missing_by_slide[slide_num] = sorted(slide_missing)\n    return missing_overall, missing_by_slide\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        description=(\n            \"Detect missing/substituted fonts for a PPTX by converting to ODP and inspecting resolved families.\"\n        )\n    )\n    parser.add_argument(\"pptx_path\", help=\"Path to .pptx file\")\n    parser.add_argument(\n        \"--json\", dest=\"output_json\", action=\"store_true\", default=False, help=\"Emit JSON output\"\n    )\n    parser.add_argument(\n        \"--include-missing\",\n        dest=\"include_missing\",\n        action=\"store_true\",\n        default=True,\n        help=\"Include missing category\",\n    )\n    parser.add_argument(\n        \"--include-substituted\",\n        dest=\"include_substituted\",\n        action=\"store_true\",\n        default=True,\n        help=\"Include substituted category\",\n    )\n    args = parser.parse_args()\n\n    pptx_path = abspath(expanduser(args.pptx_path))\n    used = extract_used_fonts_from_pptx(pptx_path)\n    # Only build ODP families if we need to report substitutions\n    slide_fams: dict[int, set[str]] = {}\n    odp_available = False\n    if args.include_substituted:\n        with tempfile.TemporaryDirectory(prefix=\"soffice_profile_\") as prof:\n            with tempfile.TemporaryDirectory(prefix=\"soffice_convert_\") as out:\n                stem = splitext(basename(pptx_path))[0]\n                odp_path = _export_to_odp(pptx_path, prof, out, stem)\n                if odp_path:\n                    slide_fams = _extract_slide_families_from_odp(odp_path)\n                    odp_available = True\n\n    syn_map = _build_fc_synonym_map()\n    font_missing_by_slide: dict[int, list[str]] = {}\n    font_substituted_by_slide: dict[int, list[str]] = {}\n    for slide_num, req_fams in used.items():\n        if args.include_substituted and odp_available:\n            odp_fams = slide_fams.get(slide_num, set())\n        else:\n            odp_fams = set()\n        miss_missing: list[str] = []\n        miss_sub: list[str] = []\n        for req in req_fams:\n            fam_base, _ = parse_font_family_base_and_styles(req)\n            acceptable: set[str] = _expand_via_fontconfig(fam_base)\n            installed = any(alias in syn_map for alias in acceptable)\n            if args.include_missing and not installed:\n                miss_missing.append(req)\n            if (\n                args.include_substituted\n                and odp_available\n                and installed\n                and (req not in odp_fams)\n                and not (acceptable & odp_fams)\n            ):\n                miss_sub.append(req)\n        if miss_missing:\n            font_missing_by_slide[slide_num] = sorted(miss_missing)\n        if miss_sub:\n            font_substituted_by_slide[slide_num] = sorted(miss_sub)\n\n    font_missing_overall: set[str] = (\n        set().union(*font_missing_by_slide.values()) if font_missing_by_slide else set()\n    )\n    font_substituted_overall: set[str] = (\n        set().union(*font_substituted_by_slide.values()) if font_substituted_by_slide else set()\n    )\n\n    if args.output_json:\n        payload: dict[str, object] = {}\n        if args.include_missing:\n            payload[\"font_missing_overall\"] = sorted(font_missing_overall)\n            payload[\"font_missing_by_slide\"] = {str(k): v for k, v in font_missing_by_slide.items()}\n        if args.include_substituted:\n            payload[\"font_substituted_overall\"] = sorted(font_substituted_overall)\n            payload[\"font_substituted_by_slide\"] = {\n                str(k): v for k, v in font_substituted_by_slide.items()\n            }\n        print(json.dumps(payload))\n    else:\n        any_missing = args.include_missing and bool(font_missing_overall)\n        any_sub = args.include_substituted and bool(font_substituted_overall)\n        if any_missing or any_sub:\n            if any_missing:\n                print(\"Fonts missing (not installed):\")\n                print(\", \".join(sorted(font_missing_overall)))\n                for slide_num in sorted(font_missing_by_slide.keys()):\n                    print(f\"Slide {slide_num} missing: \", end=\"\")\n                    print(\", \".join(font_missing_by_slide[slide_num]))\n            if any_sub:\n                print(\"Fonts substituted (installed but substituted during rendering):\")\n                print(\", \".join(sorted(font_substituted_overall)))\n                for slide_num in sorted(font_substituted_by_slide.keys()):\n                    print(f\"Slide {slide_num} substituted: \", end=\"\")\n                    print(\", \".join(font_substituted_by_slide[slide_num]))\n        else:\n            print(\"No font issues detected.\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/slides/scripts/ensure_raster_image.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Copyright (c) OpenAI. All rights reserved.\n\nEnsures input images are rasterized, converting to PNG when needed. Primarily used to\npreview image assets extracted from PowerPoint files.\n\n\nDependencies used by this tool:\n- Inkscape: SVG/EMF/WMF rasterization\n- ImageMagick: format bridging (TIFF→PNG, generic convert)\n- Ghostscript: PDF/EPS/PS rasterization (first page)\n- libheif-examples: heif-convert for HEIC/HEIF → PNG\n- jxr-tools (or libjxr-tools on older distros): JxrDecApp for JPEG XR (JXR/WDP)\n\nInstall (Ubuntu/Debian):\n  sudo apt-get update\n  sudo apt-get install -y inkscape imagemagick ghostscript libheif-examples jxr-tools\n  # If jxr-tools not found on your distro, try:\n  # sudo apt-get install -y libjxr-tools\n\nVerify:\n  inkscape --version\n  convert -version | grep -i \"ImageMagick\"\n  gs -v\n  heif-convert -h\n  JxrDecApp -h\n\"\"\"\n\nimport argparse\nimport gzip\nimport shutil\nfrom os import listdir\nfrom os.path import basename, dirname, expanduser, isfile, join, splitext\nfrom subprocess import run\n\nRASTER_EXTS = {\n    \".png\",\n    \".jpg\",\n    \".jpeg\",\n    \".bmp\",\n    \".gif\",\n    \".tif\",\n    \".tiff\",\n    \".webp\",\n}\n\nCONVERTIBLE_EXTS = {\n    # Windows metafiles (and compressed variants)\n    \".emf\",\n    \".wmf\",\n    \".emz\",\n    \".wmz\",\n    # SVG\n    \".svg\",\n    \".svgz\",\n    # JPEG XR / HD Photo\n    \".wdp\",\n    \".jxr\",\n    # HEIF family\n    \".heic\",\n    \".heif\",\n    # Page-description formats (rasterize first page)\n    \".pdf\",\n    \".eps\",\n    \".ps\",\n}\n\nSUPPORTED_EXTS = RASTER_EXTS | CONVERTIBLE_EXTS\n\n\ndef _imagemagick_convert(src_path: str, dst_path: str) -> None:\n    binary = shutil.which(\"magick\") or \"convert\"\n    run([binary, src_path, dst_path], check=True)\n\n\ndef ensure_raster_image(path: str, out_dir: str | None = None) -> str:\n    \"\"\"Return a raster image path for the given input, converting when needed.\n\n    - EMF/WMF/EMZ/WMZ are rasterized via Inkscape (EMZ/WMZ are decompressed first)\n    - SVG/SVGZ are rasterized via Inkscape\n    - WDP/JXR are converted via ImageMagick (if codec available)\n    - Known raster formats are returned as-is\n\n    Raises ValueError if the extension is not supported.\n    \"\"\"\n    base, ext = splitext(path)\n    ext_lower = ext.lower()\n    out_dir = out_dir or dirname(path)\n    out_path = join(out_dir, basename(base) + \".png\")\n\n    # Convertible formats\n    if ext_lower in (\".emf\", \".wmf\"):\n        run([\"inkscape\", path, \"-o\", out_path], check=True)\n        if isfile(out_path):\n            return out_path\n        raise RuntimeError(\"inkscape reported success but output file not found: \" + out_path)\n\n    if ext_lower in (\".emz\", \".wmz\"):\n        # Decompress into EMF/WMF then rasterize with Inkscape\n        decompressed = join(out_dir, basename(base) + (\".emf\" if ext_lower == \".emz\" else \".wmf\"))\n        with gzip.open(path, \"rb\") as zin, open(decompressed, \"wb\") as zout:\n            zout.write(zin.read())\n        run(\n            [\"inkscape\", decompressed, \"-o\", out_path],\n            check=True,\n        )\n        if isfile(out_path):\n            return out_path\n        raise RuntimeError(\"inkscape reported success but output file not found: \" + out_path)\n\n    if ext_lower in (\".svg\", \".svgz\"):\n        run([\"inkscape\", path, \"-o\", out_path], check=True)\n        if isfile(out_path):\n            return out_path\n        raise RuntimeError(\"inkscape reported success but output file not found: \" + out_path)\n\n    if ext_lower in (\".wdp\", \".jxr\"):\n        tmp_tiff = join(out_dir, basename(base) + \".tiff\")\n        run([\"JxrDecApp\", \"-i\", path, \"-o\", tmp_tiff], check=True)\n        _imagemagick_convert(tmp_tiff, out_path)\n        if isfile(out_path):\n            return out_path\n        raise RuntimeError(\"JPEG XR decode succeeded but PNG not found: \" + out_path)\n\n    if ext_lower in (\".heic\", \".heif\"):\n        # Use libheif's CLI for robust conversion\n        heif_convert = shutil.which(\"heif-convert\") or \"heif-convert\"\n        run([heif_convert, path, out_path], check=True)\n        if isfile(out_path):\n            return out_path\n        raise RuntimeError(\"heif-convert reported success but output file not found: \" + out_path)\n\n    if ext_lower in (\".pdf\", \".eps\", \".ps\"):\n        # Rasterize first page via Ghostscript\n        gs = shutil.which(\"gs\") or \"gs\"\n        run(\n            [\n                gs,\n                \"-dSAFER\",\n                \"-dBATCH\",\n                \"-dNOPAUSE\",\n                \"-sDEVICE=pngalpha\",\n                \"-dFirstPage=1\",\n                \"-dLastPage=1\",\n                \"-r200\",\n                \"-o\",\n                out_path,\n                path,\n            ],\n            check=True,\n        )\n        if isfile(out_path):\n            return out_path\n        raise RuntimeError(\"Ghostscript reported success but output file not found: \" + out_path)\n\n    if ext_lower in RASTER_EXTS:\n        return path\n\n    raise ValueError(f\"Unsupported image format for montage: {path}\")\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        description=(\"Ensure input images are rasterized; convert to PNG if needed.\")\n    )\n    group = parser.add_mutually_exclusive_group(required=True)\n    group.add_argument(\"--input_files\", nargs=\"+\", help=\"List of input image file paths\")\n    group.add_argument(\"--input_dir\", help=\"Directory containing input images\")\n    parser.add_argument(\n        \"--output_dir\",\n        default=None,\n        help=(\n            \"Directory to write converted PNGs. If omitted, converted files are written next to inputs.\"\n        ),\n    )\n    args = parser.parse_args()\n\n    if args.input_files:\n        paths = [expanduser(p) for p in args.input_files]\n    else:\n        input_dir = expanduser(args.input_dir)\n        names = listdir(input_dir)\n        paths = [\n            join(input_dir, f)\n            for f in names\n            if isfile(join(input_dir, f)) and splitext(f)[1].lower() in SUPPORTED_EXTS\n        ]\n        if not paths:\n            raise SystemExit(\"No files with supported extensions in input_dir\")\n\n    out_dir = expanduser(args.output_dir) if args.output_dir else None\n    converted_paths = []\n    for p in paths:\n        if ensure_raster_image(p, out_dir) != p:\n            converted_paths.append(p)\n\n    if converted_paths:\n        print(\"Converted the following files to PNG:\\n\" + \"\\n\".join(converted_paths))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/slides/scripts/render_slides.py",
    "content": "#!/usr/bin/env python3\n# Copyright (c) OpenAI. All rights reserved.\nimport argparse\nimport os\nimport re\nimport subprocess\nimport tempfile\nimport xml.etree.ElementTree as ET\nfrom os import makedirs, replace\nfrom os.path import abspath, basename, exists, expanduser, join, splitext\nfrom typing import Sequence, cast\nfrom zipfile import ZipFile\n\nfrom pdf2image import convert_from_path, pdfinfo_from_path\n\nEMU_PER_INCH: int = 914_400\n\n\ndef calc_dpi_via_ooxml(input_path: str, max_w_px: int, max_h_px: int) -> int:\n    \"\"\"Calculate DPI from OOXML `ppt/presentation.xml` slide size (cx/cy in EMUs).\"\"\"\n    with ZipFile(input_path, \"r\") as zf:\n        xml = zf.read(\"ppt/presentation.xml\")\n    root = ET.fromstring(xml)\n    ns = {\"p\": \"http://schemas.openxmlformats.org/presentationml/2006/main\"}\n    sld_sz = root.find(\"p:sldSz\", ns)\n    if sld_sz is None:\n        raise RuntimeError(\"Slide size not found in presentation.xml\")\n    cx = int(sld_sz.get(\"cx\") or 0)\n    cy = int(sld_sz.get(\"cy\") or 0)\n    if cx <= 0 or cy <= 0:\n        raise RuntimeError(\"Invalid slide size values in presentation.xml\")\n    width_in = cx / EMU_PER_INCH\n    height_in = cy / EMU_PER_INCH\n    return round(min(max_w_px / width_in, max_h_px / height_in))\n\n\ndef calc_dpi_via_pdf(input_path: str, max_w_px: int, max_h_px: int) -> int:\n    \"\"\"Compute DPI from PDF page size.\n\n    For non-PDF inputs, first convert to PDF via LibreOffice to read page size.\n    For PDFs, use the PDF directly (avoids unnecessary conversion and failures).\n    \"\"\"\n    is_pdf = input_path.lower().endswith(\".pdf\")\n    with tempfile.TemporaryDirectory(prefix=\"soffice_profile_\") as user_profile:\n        with tempfile.TemporaryDirectory(prefix=\"soffice_convert_\") as convert_tmp_dir:\n            stem = splitext(basename(input_path))[0]\n            pdf_path = (\n                input_path\n                if is_pdf\n                else convert_to_pdf(input_path, user_profile, convert_tmp_dir, stem)\n            )\n            if not (pdf_path and exists(pdf_path)):\n                raise RuntimeError(\"Failed to produce/read PDF for DPI computation.\")\n\n            info = pdfinfo_from_path(pdf_path)\n            size_val = info.get(\"Page size\")\n            if not size_val:\n                for k, v in info.items():\n                    if isinstance(v, str) and \"size\" in k.lower() and \"pts\" in v:\n                        size_val = v\n                        break\n            if not isinstance(size_val, str):\n                raise RuntimeError(\"Failed to read PDF page size for DPI computation.\")\n\n            def _parse_page_size_to_pts(s: str) -> tuple[float, float]:\n                # Common formats from poppler/pdfinfo:\n                # - \"612 x 792 pts (letter)\"\n                # - \"595.276 x 841.89 pts (A4)\"\n                # - sometimes inches: \"8.5 x 11 in\"\n                m_pts = re.search(\n                    r\"([0-9]+(?:\\.[0-9]+)?)\\s*x\\s*([0-9]+(?:\\.[0-9]+)?)\\s*pts\\b\",\n                    s,\n                )\n                if m_pts:\n                    return float(m_pts.group(1)), float(m_pts.group(2))\n                m_in = re.search(\n                    r\"([0-9]+(?:\\.[0-9]+)?)\\s*x\\s*([0-9]+(?:\\.[0-9]+)?)\\s*in\\b\",\n                    s,\n                )\n                if m_in:\n                    w_in = float(m_in.group(1))\n                    h_in = float(m_in.group(2))\n                    return w_in * 72.0, h_in * 72.0\n                # Sometimes poppler returns without an explicit unit; treat as points.\n                m = re.search(r\"([0-9]+(?:\\.[0-9]+)?)\\s*x\\s*([0-9]+(?:\\.[0-9]+)?)\\b\", s)\n                if m:\n                    return float(m.group(1)), float(m.group(2))\n                raise RuntimeError(f\"Unrecognized PDF page size format: {s!r}\")\n\n            width_pts, height_pts = _parse_page_size_to_pts(size_val)\n            width_in = width_pts / 72.0\n            height_in = height_pts / 72.0\n            if width_in <= 0 or height_in <= 0:\n                raise RuntimeError(\"Invalid PDF page size values.\")\n            return round(min(max_w_px / width_in, max_h_px / height_in))\n\n\ndef run_cmd_no_check(cmd: list[str]) -> None:\n    subprocess.run(\n        cmd,\n        check=False,\n        stdout=subprocess.DEVNULL,\n        stderr=subprocess.DEVNULL,\n        env=os.environ.copy(),\n    )\n\n\ndef convert_to_pdf(\n    pptx_path: str,\n    user_profile: str,\n    convert_tmp_dir: str,\n    stem: str,\n) -> str:\n    # Try direct PPTX -> PDF\n    cmd_pdf = [\n        \"soffice\",\n        \"-env:UserInstallation=file://\" + user_profile,\n        \"--invisible\",\n        \"--headless\",\n        \"--norestore\",\n        \"--convert-to\",\n        \"pdf\",\n        \"--outdir\",\n        convert_tmp_dir,\n        pptx_path,\n    ]\n    run_cmd_no_check(cmd_pdf)\n\n    pdf_path = join(convert_tmp_dir, f\"{stem}.pdf\")\n    if exists(pdf_path):\n        return pdf_path\n\n    # Fallback: PPTX -> ODP, then ODP -> PDF\n    # Rationale: Saving as ODP normalizes PPTX-specific constructs via the ODF serializer,\n    # which often bypasses Impress PDF export issues on problematic decks.\n    cmd_odp = [\n        \"soffice\",\n        \"-env:UserInstallation=file://\" + user_profile,\n        \"--invisible\",\n        \"--headless\",\n        \"--norestore\",\n        \"--convert-to\",\n        \"odp\",\n        \"--outdir\",\n        convert_tmp_dir,\n        pptx_path,\n    ]\n    run_cmd_no_check(cmd_odp)\n\n    odp_path = join(convert_tmp_dir, f\"{stem}.odp\")\n\n    if exists(odp_path):\n        # ODP -> PDF\n        cmd_odp_pdf = [\n            \"soffice\",\n            \"-env:UserInstallation=file://\" + user_profile,\n            \"--invisible\",\n            \"--headless\",\n            \"--norestore\",\n            \"--convert-to\",\n            \"pdf\",\n            \"--outdir\",\n            convert_tmp_dir,\n            odp_path,\n        ]\n        run_cmd_no_check(cmd_odp_pdf)\n        if exists(pdf_path):\n            return pdf_path\n\n    return \"\"\n\n\ndef rasterize(\n    input_path: str,\n    out_dir: str,\n    dpi: int,\n) -> Sequence[str]:\n    \"\"\"Rasterise PPTX/PDF to PNG files placed in out_dir and return the image paths.\"\"\"\n    makedirs(out_dir, exist_ok=True)\n    input_path = abspath(input_path)\n    stem = splitext(basename(input_path))[0]\n\n    # Use a unique user profile to avoid LibreOffice profile lock when running concurrently\n    with tempfile.TemporaryDirectory(prefix=\"soffice_profile_\") as user_profile:\n        # Write conversion outputs into a temp directory to avoid any IO oddities\n        with tempfile.TemporaryDirectory(prefix=\"soffice_convert_\") as convert_tmp_dir:\n            is_pdf = input_path.lower().endswith(\".pdf\")\n            pdf_path = (\n                input_path\n                if is_pdf\n                else convert_to_pdf(input_path, user_profile, convert_tmp_dir, stem)\n            )\n\n            if not pdf_path or not exists(pdf_path):\n                raise RuntimeError(\n                    \"Failed to produce PDF for rasterization (direct and ODP fallback).\"\n                )\n\n            # Perform rasterization while the temp PDF still exists\n            paths_raw = cast(\n                list[str],\n                convert_from_path(\n                    pdf_path,\n                    dpi=dpi,\n                    fmt=\"png\",\n                    thread_count=8,\n                    output_folder=out_dir,\n                    paths_only=True,\n                    output_file=\"slide\",\n                ),\n            )\n    # Rename convert_from_path's output format f'slide{thread_id:04d}-{page_num:02d}.png'\n    slides = []\n    for src_path in paths_raw:\n        base = splitext(basename(src_path))[0]\n        slide_num_str = base.split(\"-\")[-1]\n        slide_num = int(slide_num_str)\n        dst_path = join(out_dir, f\"slide-{slide_num}.png\")\n        replace(src_path, dst_path)\n        slides.append((slide_num, dst_path))\n    slides.sort(key=lambda t: t[0])\n    final_paths = [path for _, path in slides]\n    return final_paths\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(description=\"Render slides to images.\")\n    parser.add_argument(\n        \"input_path\",\n        type=str,\n        help=\"Path to the input PowerPoint or PDF file.\",\n    )\n    parser.add_argument(\n        \"--output_dir\",\n        type=str,\n        default=None,\n        help=(\n            \"Output directory for the rendered images. \"\n            \"Defaults to a folder next to the input named after the input file (without extension).\"\n        ),\n    )\n    parser.add_argument(\n        \"--width\",\n        type=int,\n        default=1600,\n        help=(\n            \"Approximate maximum width in pixels after isotropic scaling (default 1600). \"\n            \"The actual value may exceed slightly.\"\n        ),\n    )\n    parser.add_argument(\n        \"--height\",\n        type=int,\n        default=900,\n        help=(\n            \"Approximate maximum height in pixels after isotropic scaling (default 900). \"\n            \"The actual value may exceed slightly.\"\n        ),\n    )\n    args = parser.parse_args()\n\n    input_path = abspath(expanduser(args.input_path))\n    out_dir = abspath(expanduser(args.output_dir)) if args.output_dir else splitext(input_path)[0]\n    if input_path.lower().endswith((\".pptx\", \".ppsx\", \".potx\", \".pptm\", \".ppsm\", \".potm\")):\n        dpi = calc_dpi_via_ooxml(input_path, args.width, args.height)\n    else:\n        dpi = calc_dpi_via_pdf(input_path, args.width, args.height)\n    rasterize(input_path, out_dir, dpi)\n    print(\"Slides rendered to \" + out_dir)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/slides/scripts/slides_test.py",
    "content": "#!/usr/bin/env python3\n# Copyright (c) OpenAI. All rights reserved.\nimport argparse\nimport sys\nimport tempfile\nfrom os.path import abspath, expanduser, join\nfrom pathlib import Path\nfrom typing import Sequence, cast\n\nimport numpy as np\n\nSCRIPT_DIR = Path(__file__).resolve().parent\nif str(SCRIPT_DIR) not in sys.path:\n    sys.path.insert(0, str(SCRIPT_DIR))\n\nimport render_slides  # type: ignore\nfrom PIL import Image\nfrom pptx import Presentation\nfrom pptx.dml.color import RGBColor\nfrom pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE\nfrom pptx.util import Emu\n\n# Configuration specific to overflow checking\nPAD_PX: int = 100  # fixed padding on every side in pixels\nPAD_RGB = (200, 200, 200)\nEMU_PER_INCH: int = 914_400\n\n\ndef px_to_emu(px: int, dpi: int) -> Emu:\n    return Emu(int(px * EMU_PER_INCH // dpi))\n\n\ndef calc_tol(dpi: int) -> int:\n    \"\"\"Calculate per-channel colour tolerance appropriate for *dpi* (anti-aliasing tolerance).\"\"\"\n    if dpi >= 300:\n        return 0\n    # 1 at 250 DPI, 5 at 150 DPI, capped to 10.\n    tol = round((300 - dpi) / 25)\n    return min(max(tol, 1), 10)\n\n\ndef enlarge_deck(src: str, dst: str, pad_emu: Emu) -> tuple[int, int]:\n    \"\"\"Enlarge the input PPTX with a fixed grey padding and return the new page size.\"\"\"\n    prs = Presentation(src)\n    w0 = cast(Emu, prs.slide_width)\n    h0 = cast(Emu, prs.slide_height)\n    w1 = Emu(w0 + 2 * pad_emu)\n    h1 = Emu(h0 + 2 * pad_emu)\n    prs.slide_width = w1\n    prs.slide_height = h1\n\n    for slide in prs.slides:\n        # Shift all shapes so the original canvas sits centred in the new deck.\n        for shp in list(slide.shapes):\n            shp.left = Emu(int(shp.left) + pad_emu)\n            shp.top = Emu(int(shp.top) + pad_emu)\n\n        pads = (\n            (Emu(0), Emu(0), pad_emu, h1),  # left\n            (Emu(int(w1) - int(pad_emu)), Emu(0), pad_emu, h1),  # right\n            (Emu(0), Emu(0), w1, pad_emu),  # top\n            (Emu(0), Emu(int(h1) - int(pad_emu)), w1, pad_emu),  # bottom\n        )\n\n        sp_tree = slide.shapes._spTree  # pylint: disable=protected-access\n\n        for left, top, width, height in pads:\n            pad_shape = slide.shapes.add_shape(\n                MSO_AUTO_SHAPE_TYPE.RECTANGLE, left, top, width, height\n            )\n            pad_shape.fill.solid()\n            pad_shape.fill.fore_color.rgb = RGBColor(*PAD_RGB)\n            pad_shape.line.fill.background()\n\n            # Send pad behind all other shapes (index 2 after mandatory nodes)\n            sp_tree.remove(pad_shape._element)\n            sp_tree.insert(2, pad_shape._element)\n\n    prs.save(dst)\n    return int(w1), int(h1)\n\n\ndef inspect_images(\n    paths: Sequence[str],\n    pad_ratio_w: float,\n    pad_ratio_h: float,\n    dpi: int,\n) -> list[int]:\n    \"\"\"Return 1-based indices of slides that contain pixels outside the pad.\"\"\"\n\n    tol = calc_tol(dpi)\n    failures: list[int] = []\n    pad_colour = np.array(PAD_RGB, dtype=np.uint8)\n\n    for idx, img_path in enumerate(paths, start=1):\n        with Image.open(img_path) as img:\n            rgb = img.convert(\"RGB\")\n            arr = np.asarray(rgb)\n\n        h, w, _ = arr.shape\n        # Exclude the innermost 1-pixel band\n        pad_x = int(w * pad_ratio_w) - 1\n        pad_y = int(h * pad_ratio_h) - 1\n\n        left_margin = arr[:, :pad_x, :]\n        right_margin = arr[:, w - pad_x :, :]\n        top_margin = arr[:pad_y, :, :]\n        bottom_margin = arr[h - pad_y :, :, :]\n\n        def _is_clean(margin: np.ndarray) -> bool:\n            diff = np.abs(margin.astype(np.int16) - pad_colour)\n            matches = np.all(diff <= tol, axis=-1)\n            mismatch_fraction = 1.0 - (np.count_nonzero(matches) / matches.size)\n            if dpi >= 300:\n                max_mismatch = 0.01\n            elif dpi >= 200:\n                max_mismatch = 0.02\n            else:\n                max_mismatch = 0.03\n            return mismatch_fraction <= max_mismatch\n\n        if not (\n            _is_clean(left_margin)\n            and _is_clean(right_margin)\n            and _is_clean(top_margin)\n            and _is_clean(bottom_margin)\n        ):\n            failures.append(idx)\n\n    return failures\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        description=(\n            \"Check a PPTX for content overflowing the original canvas by rendering with padding \"\n            \"and inspecting the margins.\"\n        )\n    )\n    parser.add_argument(\n        \"input_path\",\n        type=str,\n        help=\"Path to the input PPTX file.\",\n    )\n    parser.add_argument(\n        \"--width\",\n        type=int,\n        default=1600,\n        help=(\n            \"Approximate maximum width in pixels after isotropic scaling (default 1600). \"\n            \"The actual value may exceed slightly.\"\n        ),\n    )\n    parser.add_argument(\n        \"--height\",\n        type=int,\n        default=900,\n        help=(\n            \"Approximate maximum height in pixels after isotropic scaling (default 900). \"\n            \"The actual value may exceed slightly.\"\n        ),\n    )\n    parser.add_argument(\n        \"--pad_px\",\n        type=int,\n        default=PAD_PX,\n        help=\"Padding in pixels to add on each side before rasterization.\",\n    )\n    args = parser.parse_args()\n\n    input_path = abspath(expanduser(args.input_path))\n    # Width and height refer to the original, unaltered slide dimensions.\n    dpi = render_slides.calc_dpi_via_ooxml(input_path, args.width, args.height)\n\n    # Not using ``tempfile.TemporaryDirectory(delete=False)`` for Python 3.11 compatibility.\n    tmpdir = tempfile.mkdtemp()\n    enlarged_pptx = join(tmpdir, \"enlarged.pptx\")\n    pad_emu = px_to_emu(args.pad_px, dpi)\n    w1, h1 = enlarge_deck(input_path, enlarged_pptx, pad_emu=pad_emu)\n    pad_ratio_w = pad_emu / w1\n    pad_ratio_h = pad_emu / h1\n\n    img_dir = join(tmpdir, \"imgs\")\n    img_paths = render_slides.rasterize(enlarged_pptx, img_dir, dpi)\n    failing = inspect_images(img_paths, pad_ratio_w, pad_ratio_h, dpi)\n\n    if failing:\n        print(\n            \"ERROR: Slides with content overflowing original canvas (1-based indexing): \"\n            + \", \".join(map(str, failing))\n            + \"\\n\"\n            + \"Rendered images with grey paddings for problematic slides are available at: \"\n        )\n        for i in failing:\n            print(img_paths[i - 1])\n    else:\n        print(\"Test passed. No overflow detected.\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/sora/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/sora/SKILL.md",
    "content": "---\nname: \"sora\"\ndescription: \"Use when the user asks to generate, edit, extend, poll, list, download, or delete Sora videos, create reusable non-human Sora character references, or run local multi-video queues via the bundled CLI (`scripts/sora.py`); includes requests like: (i) generate AI video, (ii) edit this Sora clip, (iii) extend this video, (iv) create a character reference, (v) download video/thumbnail/spritesheet, and (vi) Sora batch planning; requires `OPENAI_API_KEY` and Sora API access.\"\n---\n\n\n# Sora Video Generation Skill\n\nCreates or manages Sora video jobs for the current project (product demos, marketing spots, cinematic shots, social clips, UI mocks). Defaults to `sora-2` with structured prompt augmentation and prefers the bundled CLI for deterministic runs. Note: `$sora` is a skill tag in prompts, not a shell command.\n\n## When to use\n- Generate a new video clip from a prompt\n- Create a reusable character reference from a short non-human source clip\n- Edit an existing generated video with a targeted prompt change\n- Extend a completed video with a continuation prompt\n- Poll status, list jobs, or download assets (video/thumbnail/spritesheet)\n- Run a local multi-job queue now, or plan a true Batch API submission for offline rendering\n\n## Decision tree\n- If the user has a short non-human reference clip they want to reuse across shots → `create-character`\n- If the user has a completed video and wants the next beat/continuation → `extend`\n- If the user has a completed video and wants a targeted change while preserving the shot → `edit`\n- If the user has a video id and wants status or assets → `status`, `poll`, or `download`\n- If the user needs many renders immediately inside Codex → `create-batch` (local fan-out, not the Batch API)\n- If the user needs many renders for offline processing or a studio pipeline → use the official Batch API flow described in `references/video-api.md`\n- Otherwise → `create` (or `create-and-poll` if they need a ready asset in one step)\n\n## Workflow\n1. Decide intent: create vs create-character vs edit vs extend vs status/download vs local queue vs official Batch API.\n2. Collect inputs: prompt, model, size, seconds, any image reference, and any character IDs.\n3. Prefer CLI augmentation flags (`--use-case`, `--scene`, `--camera`, etc.) instead of hand-writing a long structured prompt. If you already have a structured prompt file, pass `--no-augment`.\n4. Run the bundled CLI (`scripts/sora.py`) with sensible defaults. For long prompts, prefer `--prompt-file` to avoid shell-escaping issues.\n5. For async jobs, poll until terminal status (or use `create-and-poll`).\n6. Download assets (video/thumbnail/spritesheet) and save them locally before URLs expire.\n7. If the user wants continuity across many shots, create character assets first, then reference them in later `create` calls.\n8. If the user wants to iterate on a completed shot, prefer `edit`; if they want the shot to continue in time, prefer `extend`.\n9. Use one targeted change per iteration.\n\n## Authentication\n- `OPENAI_API_KEY` must be set for live API calls.\n\nIf the key is missing, give the user these steps:\n1. Create an API key in the OpenAI platform UI: https://platform.openai.com/api-keys\n2. Set `OPENAI_API_KEY` as an environment variable in their system.\n3. Offer to guide them through setting the environment variable for their OS/shell if needed.\n- Never ask the user to paste the full key in chat. Ask them to set it locally and confirm when ready.\n\n## Defaults & rules\n- Default model: `sora-2` (use `sora-2-pro` for higher fidelity).\n- Default size: `1280x720`.\n- Default seconds: `4` (allowed: `\"4\"`, `\"8\"`, `\"12\"`, `\"16\"`, `\"20\"`).\n- Always set size and seconds via API params; prose will not change them.\n- `sora-2-pro` is required for `1920x1080` and `1080x1920`.\n- Use up to two characters per generation.\n- Use the OpenAI Python SDK (`openai` package). If high-level SDK helpers lag the latest Sora guide, use low-level `client.post/get/delete` inside the official SDK rather than standalone HTTP code.\n- Require `OPENAI_API_KEY` before any live API call.\n- If uv cache permissions fail, set `UV_CACHE_DIR=/tmp/uv-cache`.\n- Input reference images must be jpg/png/webp and should match target size.\n- JSON `input_reference` objects use either `file_id` or `image_url`; uploaded file paths use multipart.\n- Download URLs expire after about 1 hour; copy assets to your own storage.\n- Batch-generated videos remain downloadable for up to 24 hours after the batch completes.\n- `create-batch` in `scripts/sora.py` is a local concurrent queue, not the official Batch API.\n- Prefer the bundled CLI and **never modify** `scripts/sora.py` unless the user asks.\n- Sora can generate audio; if a user requests voiceover/audio, specify it explicitly in the `Audio:` and `Dialogue:` lines and keep it short.\n\n## API limitations\n- Models are limited to `sora-2` and `sora-2-pro`.\n- API access to Sora models requires an organization-verified account.\n- Duration must be set via the `seconds` parameter and currently supports `4`, `8`, `12`, `16`, and `20`.\n- Character uploads currently work best with short `2`-`4` second non-human MP4s in `16:9` or `9:16`, at `720p`-`1080p`.\n- Extensions can add up to `20` seconds each, up to six times per source video, for a maximum total length of `120` seconds.\n- Extensions currently do not support characters or image references.\n- This skill supports editing existing generated videos by ID.\n- The official Batch API currently supports `POST /v1/videos` only, with JSON bodies rather than multipart uploads.\n- Output sizes are limited by model (see `references/video-api.md` for the supported sizes).\n- Video creation is async; you must poll for completion before downloading.\n- Rate limits apply by usage tier (do not list specific limits).\n- Content restrictions are enforced by the API (see Guardrails below).\n\n## Guardrails (must enforce)\n- Only content suitable for audiences under 18.\n- No copyrighted characters or copyrighted music.\n- No real people (including public figures).\n- Input images with human faces are rejected.\n- Character uploads in this skill are for non-human subjects only.\n\n## Prompt augmentation\nReformat prompts into a structured, production-oriented spec. Only make implicit details explicit; do not invent new creative requirements.\n\nTemplate (include only relevant lines):\n```\nUse case: <where the clip will be used>\nPrimary request: <user's main prompt>\nScene/background: <location, time of day, atmosphere>\nSubject: <main subject>\nAction: <single clear action>\nCamera: <shot type, angle, motion>\nLighting/mood: <lighting + mood>\nColor palette: <3-5 color anchors>\nStyle/format: <film/animation/format cues>\nTiming/beats: <counts or beats>\nAudio: <ambient cue / music / voiceover if requested>\nText (verbatim): \"<exact text>\"\nDialogue:\n<dialogue>\n- Speaker: \"Short line.\"\n</dialogue>\nConstraints: <must keep/must avoid>\nAvoid: <negative constraints>\n```\n\nAugmentation rules:\n- Keep it short; add only details the user already implied or provided elsewhere.\n- For edits, explicitly list invariants (\"same shot, change only X\").\n- For character-based shots, mention the character name verbatim in the prompt.\n- If any critical detail is missing and blocks success, ask a question; otherwise proceed.\n- If you pass a structured prompt file to the CLI, add `--no-augment` to avoid the tool re-wrapping it.\n\n## Examples\n\n### Generation example (single shot)\n```\nUse case: product teaser\nPrimary request: a close-up of a matte black camera on a pedestal\nAction: slow 30-degree orbit over 4 seconds\nCamera: 85mm, shallow depth of field, gentle handheld drift\nLighting/mood: soft key light, subtle rim, premium studio feel\nConstraints: no logos, no text\n```\n\n### Edit example (invariants)\n```\nPrimary request: same shot and framing, switch palette to teal/sand/rust with warmer backlight\nConstraints: keep the subject and camera move unchanged\n```\n\n### Character consistency example\n```\nPrimary request: Mossy, a moss-covered teapot mascot, hurries through a lantern-lit market at dusk\nCamera: cinematic tracking shot, 35mm, shoulder height\nLighting/mood: warm dusk practicals, soft haze\nConstraints: keep Mossy’s silhouette and moss texture consistent across the shot\n```\n\n## Prompting best practices (short list)\n- One main action + one camera move per shot.\n- Use counts or beats for timing (\"two steps, pause, turn\").\n- Keep text short and the camera locked-off for UI or on-screen text.\n- Add a brief avoid line when artifacts appear (flicker, jitter, fast motion).\n- Shorter prompts are more creative; longer prompts are more controlled.\n- Put dialogue in a dedicated block; keep lines short for 4-8s clips.\n- Mention character names verbatim when using uploaded character IDs.\n- State invariants explicitly for edits (same shot, same camera move).\n- Prefer `edit` for targeted changes and `extend` for timeline continuation.\n- Iterate with single-change follow-ups to preserve continuity.\n\n## Guidance by asset type\nUse these modules when the request is for a specific artifact. They provide targeted templates and defaults.\n- Cinematic shots: `references/cinematic-shots.md`\n- Social ads: `references/social-ads.md`\n\n## CLI + environment notes\n- CLI commands + examples: `references/cli.md`\n- API parameter quick reference: `references/video-api.md`\n- Prompting guidance: `references/prompting.md`\n- Sample prompts: `references/sample-prompts.md`\n- Troubleshooting: `references/troubleshooting.md`\n- Network/sandbox tips: `references/codex-network.md`\n\n## Reference map\n- **`references/cli.md`**: how to run create/edit/extend/create-character/poll/download/local-queue flows via `scripts/sora.py`.\n- **`references/video-api.md`**: API-level knobs (models, sizes, duration, characters, edits, extensions, official Batch API).\n- **`references/prompting.md`**: prompt structure, character continuity, editing, and extension guidance.\n- **`references/sample-prompts.md`**: copy/paste prompt recipes (examples only; no extra theory).\n- **`references/cinematic-shots.md`**: templates for filmic shots.\n- **`references/social-ads.md`**: templates for short social ad beats.\n- **`references/troubleshooting.md`**: common errors and fixes.\n- **`references/codex-network.md`**: network/approval troubleshooting.\n"
  },
  {
    "path": "skills/.curated/sora/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Sora Video Generation Skill\"\n  short_description: \"Generate, edit, extend, and manage Sora videos\"\n  icon_small: \"./assets/sora-small.svg\"\n  icon_large: \"./assets/sora.png\"\n  default_prompt: \"Plan the right Sora workflow for this request, then generate, edit, extend, or manage the video with concrete prompt iterations.\"\n"
  },
  {
    "path": "skills/.curated/sora/references/cinematic-shots.md",
    "content": "# Cinematic shot templates\n\nUse these for filmic, mood-forward clips. Keep one subject, one action, one camera move.\n\n## Shot grammar (pick one)\n- Static wide: locked-off, slow atmosphere changes\n- Dolly-in: slow push toward subject\n- Dolly-out: reveal more context\n- Orbit: 15-45 degree arc around subject\n- Lateral move: smooth left-right slide\n- Crane: subtle vertical rise\n- Handheld drift: gentle, controlled sway\n\n## Default template\n```\nUse case: cinematic shot\nPrimary request: <subject + setting>\nScene/background: <location, time of day, atmosphere>\nSubject: <main subject>\nAction: <one clear action>\nCamera: <shot type, lens, motion>\nLighting/mood: <key light + mood>\nColor palette: <3-5 anchors>\nStyle/format: filmic, natural grain\nConstraints: no logos, no text, no people\nAvoid: jitter; flicker; oversharpening\n```\n\n## Example: moody exterior\n```\nUse case: cinematic shot\nPrimary request: a lone cabin on a cliff above the sea\nScene/background: foggy coastline at dawn, drifting mist\nSubject: small wooden cabin with warm window glow\nAction: light fog rolls past the cabin\nCamera: slow dolly-in, 35mm, steady\nLighting/mood: moody, soft dawn light, subtle contrast\nColor palette: deep blue, slate, warm amber\nConstraints: no logos, no text, no people\n```\n\n## Example: intimate detail\n```\nUse case: cinematic detail\nPrimary request: close-up of a vinyl record spinning\nScene/background: dim room, soft lamp glow\nSubject: record grooves and stylus\nAction: slow rotation, subtle dust motes\nCamera: macro, locked-off\nLighting/mood: warm, low-key, soft highlights\nColor palette: warm amber, deep brown, charcoal\nConstraints: no logos, no text\n```\n"
  },
  {
    "path": "skills/.curated/sora/references/cli.md",
    "content": "# CLI reference (`scripts/sora.py`)\n\nThis file contains the command catalog for the bundled Sora CLI. Keep `SKILL.md` overview-first; put verbose CLI details here.\n\n## What this CLI does\n- `create`: create a new video job\n- `create-and-poll`: create a job, poll until complete, optionally download\n- `create-character`: upload a reusable non-human character reference clip\n- `edit`: edit an existing generated video by ID\n- `extend`: continue a completed video\n- `poll`: wait for an existing job to finish\n- `status`: retrieve job status/details\n- `download`: download video/thumbnail/spritesheet\n- `list`: list recent jobs\n- `delete`: delete a job\n- `remix`: legacy remix endpoint\n- `create-batch`: create multiple video jobs locally from JSONL input\n\nReal API calls require network access and `OPENAI_API_KEY`. `--dry-run` does not.\n\n## Important distinction\n- `create-batch` is a local concurrent fan-out helper.\n- It is not the official Batch API.\n- For the official Batch API, prepare a JSONL file for `POST /v1/videos`, upload it with `purpose=batch`, then create a batch via the Files and Batches APIs.\n\n## Quick start\nSet a stable path to the skill CLI (default `CODEX_HOME` is `~/.codex`):\n\n```bash\nexport CODEX_HOME=\"${CODEX_HOME:-$HOME/.codex}\"\nexport SORA_CLI=\"$CODEX_HOME/skills/sora/scripts/sora.py\"\n```\n\nIf you're in this repo, set the path directly:\n\n```bash\nexport SORA_CLI=\"$(git rev-parse --show-toplevel)/<path-to-skill>/scripts/sora.py\"\n```\n\nIf uv cache fails with permission errors:\n\n```bash\nexport UV_CACHE_DIR=\"/tmp/uv-cache\"\n```\n\nDry-run without calling the API:\n\n```bash\npython \"$SORA_CLI\" create --prompt \"Test\" --dry-run\n```\n\n## Defaults\n- Model: `sora-2`\n- Size: `1280x720`\n- Seconds: `4`\n- Variant: `video`\n- Poll interval: `10` seconds\n\nAllowed seconds: `4`, `8`, `12`, `16`, `20`\n\nAllowed sizes:\n- `sora-2`: `1280x720`, `720x1280`\n- `sora-2-pro`: `1280x720`, `720x1280`, `1024x1792`, `1792x1024`, `1920x1080`, `1080x1920`\n\n## Create\nCreate a job:\n\n```bash\nuv run --with openai python \"$SORA_CLI\" create \\\n  --model sora-2 \\\n  --prompt \"Wide tracking shot of a teal coupe on a desert highway\" \\\n  --size 1280x720 \\\n  --seconds 8\n```\n\nCreate with a file-based first-frame reference:\n\n```bash\nuv run --with openai python \"$SORA_CLI\" create \\\n  --model sora-2-pro \\\n  --prompt \"She turns around and smiles, then slowly walks out of frame.\" \\\n  --size 1280x720 \\\n  --seconds 8 \\\n  --input-reference sample_720p.jpeg\n```\n\nCreate with a stored/remote JSON reference object:\n\n```bash\nuv run --with openai python \"$SORA_CLI\" create \\\n  --prompt \"Slow reveal of a mossy mascot in a lantern-lit market\" \\\n  --input-reference-file-id file_abc123\n```\n\nCreate with characters:\n\n```bash\nuv run --with openai python \"$SORA_CLI\" create \\\n  --model sora-2 \\\n  --prompt \"Mossy, a moss-covered teapot mascot, rushes through a lantern-lit market at dusk.\" \\\n  --character-id char_123 \\\n  --seconds 8\n```\n\nIf the prompt is already structured, disable augmentation:\n\n```bash\nuv run --with openai python \"$SORA_CLI\" create \\\n  --prompt-file prompt.txt \\\n  --no-augment \\\n  --seconds 16\n```\n\n## Create and poll\n\n```bash\nuv run --with openai python \"$SORA_CLI\" create-and-poll \\\n  --model sora-2-pro \\\n  --prompt \"Close-up of a steaming coffee cup on a wooden table\" \\\n  --size 1920x1080 \\\n  --seconds 16 \\\n  --download \\\n  --variant video \\\n  --out coffee.mp4\n```\n\n## Create a character\n\n```bash\nuv run --with openai python \"$SORA_CLI\" create-character \\\n  --name Mossy \\\n  --video-file character.mp4\n```\n\nUse short non-human MP4 source clips and mention the character name verbatim in later prompts.\n\n## Edit\nEdit an existing generated video by ID:\n\n```bash\nuv run --with openai python \"$SORA_CLI\" edit \\\n  --id video_abc123 \\\n  --prompt \"Same shot and camera move; shift the palette to teal, sand, and rust.\"\n```\n\n## Extend\n\n```bash\nuv run --with openai python \"$SORA_CLI\" extend \\\n  --id video_abc123 \\\n  --seconds 8 \\\n  --prompt \"Continue the scene as the camera rises above the rooftops and reveals sunrise.\"\n```\n\n## Poll / status / download\n\n```bash\nuv run --with openai python \"$SORA_CLI\" poll --id video_abc123 --download --out out.mp4\nuv run --with openai python \"$SORA_CLI\" status --id video_abc123\nuv run --with openai python \"$SORA_CLI\" download --id video_abc123 --variant thumbnail --out thumb.webp\nuv run --with openai python \"$SORA_CLI\" download --id video_abc123 --variant spritesheet --out sheet.jpg\n```\n\n## List / delete\n\n```bash\nuv run --with openai python \"$SORA_CLI\" list --limit 20 --after video_123 --order asc\nuv run --with openai python \"$SORA_CLI\" delete --id video_abc123\n```\n\n## Legacy remix\n\n```bash\nuv run --with openai python \"$SORA_CLI\" remix \\\n  --id video_abc123 \\\n  --prompt \"Same shot and framing; change only the palette to teal and sand.\"\n```\n\nUse `edit` for new workflows. `remix` is retained only for legacy compatibility.\n\n## JSON output (`--json-out`)\n- `create`, `status`, `list`, `delete`, `poll`, `remix`, `edit`, `extend`, and `create-character` write the response to a file.\n- `create-and-poll` writes `{ \"create\": ..., \"final\": ... }`.\n- In `--dry-run`, `--json-out` writes the request preview.\n- If the path has no extension, `.json` is added automatically.\n\n## Local batch JSONL schema (`create-batch`)\nEach line is a JSON object (or a raw prompt string). Required key: `prompt`.\n\nCommon top-level keys:\n- `model`, `size`, `seconds`\n- `characters`: list like `[{\"id\":\"char_123\"}]` or `[\"char_123\"]`\n- `character_ids`: alternate list form such as `[\"char_123\"]`\n- `input_reference`: either a file path string or a JSON object with `file_id` or `image_url`\n- `input_reference_path` / `input_reference_file`: file path aliases\n- `input_reference_file_id`\n- `input_reference_url`\n- `out`: optional output filename for the job JSON\n\nPrompt augmentation keys:\n- `use_case`, `scene`, `subject`, `action`, `camera`, `style`, `lighting`, `palette`, `audio`, `dialogue`, `text`, `timing`, `constraints`, `negative`\n\nExample:\n\n```bash\nmkdir -p tmp/sora\ncat > tmp/sora/prompts.jsonl << 'EOB'\n{\"prompt\":\"A neon-lit rainy alley, slow dolly-in\",\"seconds\":\"8\"}\n{\"prompt\":\"Mossy, a moss-covered teapot mascot, jogs through a lantern-lit alley\",\"seconds\":\"16\",\"character_ids\":[\"char_123\"]}\n{\"prompt\":\"A warm sunrise over a misty lake, gentle pan\",\"input_reference\":{\"file_id\":\"file_abc123\"}}\nEOB\n\nuv run --with openai python \"$SORA_CLI\" create-batch \\\n  --input tmp/sora/prompts.jsonl \\\n  --out-dir out \\\n  --concurrency 3\n```\n\nNotes:\n- `create-batch` writes one JSON response per job under `--out-dir`.\n- Output names default to `NNN-<prompt-slug>.json`.\n- Higher concurrency can hit rate limits.\n- Treat the JSONL file as temporary and clean it up after use.\n\n## Guardrails\n- Use `python \"$SORA_CLI\" ...` or `uv run --with openai python \"$SORA_CLI\" ...`.\n- For live API calls, prefer `uv run --with openai ...`.\n- Do not create one-off runners unless the user explicitly asks.\n- `edit` replaces `remix` for new integrations.\n\n## See also\n- API parameter quick reference: `references/video-api.md`\n- Prompt structure and iteration: `references/prompting.md`\n- Sample prompts: `references/sample-prompts.md`\n- Troubleshooting: `references/troubleshooting.md`\n"
  },
  {
    "path": "skills/.curated/sora/references/codex-network.md",
    "content": "# Codex network approvals / sandbox notes\n\nThis guidance is intentionally isolated from `SKILL.md` because it can vary by environment and may become stale. Prefer the defaults in your environment when in doubt.\n\n## Why am I asked to approve every video generation call?\nVideo generation uses the OpenAI Video API, so the CLI needs outbound network access. In many Codex setups, network access is disabled by default (especially under stricter sandbox modes), and/or the approval policy may require confirmation before networked commands run.\n\n## How do I reduce repeated approval prompts (network)?\nIf you trust the repo and want fewer prompts, enable network access for the relevant sandbox mode and relax the approval policy.\n\nExample `~/.codex/config.toml` pattern:\n\n```\napproval_policy = \"never\"\nsandbox_mode = \"workspace-write\"\n\n[sandbox_workspace_write]\nnetwork_access = true\n```\n\nOr for a single session:\n\n```\ncodex --sandbox workspace-write --ask-for-approval never\n```\n\n## Safety note\nUse caution: enabling network and disabling approvals reduces friction but increases risk if you run untrusted code or work in an untrusted repository.\n"
  },
  {
    "path": "skills/.curated/sora/references/prompting.md",
    "content": "# Prompting best practices (Sora)\n\n## Contents\n- [Mindset & tradeoffs](#mindset--tradeoffs)\n- [API-controlled params](#api-controlled-params)\n- [Structure](#structure)\n- [Specificity](#specificity)\n- [Style & visual cues](#style--visual-cues)\n- [Camera & composition](#camera--composition)\n- [Motion & timing](#motion--timing)\n- [Lighting & palette](#lighting--palette)\n- [Character continuity](#character-continuity)\n- [Multi-shot prompts](#multi-shot-prompts)\n- [Ultra-detailed briefs](#ultra-detailed-briefs)\n- [Image input](#image-input)\n- [Constraints & invariants](#constraints--invariants)\n- [Text, dialogue & audio](#text-dialogue--audio)\n- [Avoiding artifacts](#avoiding-artifacts)\n- [Editing & extensions](#editing--extensions)\n- [Iterate deliberately](#iterate-deliberately)\n\n## Mindset & tradeoffs\n- Treat the prompt like a cinematography brief, not a contract.\n- The same prompt can yield different results; rerun for variants.\n- Short prompts give more creative freedom; longer prompts give more control.\n- Shorter clips tend to follow instructions better; even though `16`s and `20`s are available, start shorter when precision matters.\n\n## API-controlled params\n- Model, size, seconds, and character IDs are controlled by API params, not prose.\n- Put desired duration in the `seconds` param; the prompt cannot make a clip longer.\n- `1920x1080` and `1080x1920` require `sora-2-pro`.\n\n## Structure\n- Use short labeled lines; omit sections that do not matter.\n- Keep one main subject and one main action.\n- Put timing in beats or counts if it matters.\n- If you prefer a prose-first template, use:\n```\n<Prose scene description in plain language. Describe subject, setting, time of day, and key visual details.>\n\nCinematography:\nCamera shot: <framing + angle>\nMood: <tone>\n\nActions:\n- <clear action beat>\n- <clear action beat>\n\nDialogue:\n<short lines if needed>\n```\n\n## Specificity\n- Name the subject and materials (metal, fabric, glass).\n- Use camera language (lens, angle, shot type) for stability.\n- Describe the environment with time of day and atmosphere.\n\n## Style & visual cues\n- Set style early (e.g., \"1970s film\", \"IMAX-scale\", \"16mm black-and-white\").\n- Use visible nouns and verbs, not vague adjectives.\n- Weak: \"A beautiful street at night.\"\n- Strong: \"Wet asphalt, zebra crosswalk, neon signs reflecting in puddles.\"\n\n## Camera & composition\n- Prefer one camera move: dolly, orbit, lateral slide, or locked-off.\n- Straight-on framing is best for UI and text.\n- For close-ups, use longer lenses (85mm+); for wide scenes, 24-35mm.\n- Depth of field is a strong lever: shallow for subject isolation, deep for context.\n- Example framings: wide establishing, medium close-up, aerial wide, low angle.\n- Example camera motions: slow tilt, gentle handheld drift, smooth lateral slide.\n\n## Motion & timing\n- Use short beats: \"0-2s\", \"2-4s\", \"4-6s\".\n- Keep actions sequential, not simultaneous.\n- For 4s clips, limit to 1-2 beats.\n- Describe actions as counts or steps when possible (e.g., \"takes four steps, pauses, turns in the final second\").\n\n## Lighting & palette\n- Describe light quality and direction (soft window light, hard rim, backlight).\n- Name 3-5 palette anchors to stabilize color across shots.\n- If continuity matters, keep lighting logic consistent across clips.\n\n## Character continuity\n- Keep character descriptors consistent across shots; reuse phrasing.\n- Avoid mixing competing traits that can shift identity or pose.\n- When using uploaded character assets, mention the character name verbatim in the prompt.\n- Use no more than two characters per generation.\n- Character uploads work best from short non-human MP4 reference clips.\n\n## Multi-shot prompts\n- You can describe multiple shots in one prompt, but keep each shot block distinct.\n- For each shot, specify one camera setup, one action, one lighting recipe.\n- Treat each shot as a creative unit you can later edit or stitch.\n\n## Ultra-detailed briefs\n- Use when you need a specific, filmic look or strict continuity.\n- Call out format/look, lensing/filters, grade/palette, lighting direction, texture, and sound.\n- If needed, include a short shot list with timing beats.\n\n## Image input\n- Use an input image to lock composition, character design, or set dressing.\n- The input image should match the target size and be jpg/png/webp.\n- The image anchors the first frame; the prompt describes what happens next.\n- If you lack a reference, generate one first and pass it as `input_reference`.\n\n## Constraints & invariants\n- State what must not change: \"same shot\", \"same framing\", \"keep background\".\n- Repeat invariants in every edit to reduce drift.\n- Use invariants sparingly in extensions; tell the model what should continue, not just what should stay frozen.\n\n## Text, dialogue & audio\n- Keep text short and specific; quote exact strings.\n- Specify placement and avoid motion blur.\n- For dialogue, use a dedicated block and keep lines short.\n- Label speakers consistently for multi-character scenes.\n- If silent, you can still add a small ambient sound cue to set rhythm.\n- Sora can generate audio; include an `Audio:` line and a short dialogue block when needed.\n- As a rule of thumb, 4s clips fit 1-2 short lines; 8s clips can handle a few more.\n\nExample:\n```\nAudio: soft ambient café noise, clear warm voiceover\nDialogue:\n<dialogue>\n- Speaker: \"Let's get started.\"\n</dialogue>\n```\n\n## Avoiding artifacts\n- Avoid multiple actions in 4-8 seconds.\n- Keep camera motion smooth and limited.\n- Add explicit negatives when needed: \"avoid flicker\", \"avoid jitter\", \"no fast motion\".\n\n## Editing & extensions\n- Prefer edits when the shot is mostly right and you want one targeted change.\n- Prefer extensions when the existing clip should continue forward in time.\n- For edits, change one thing at a time: palette, lighting, or action.\n- For extensions, describe the next beat clearly and preserve motion continuity.\n- If a shot misfires, simplify: freeze the camera, reduce action, clear background, then add complexity back in.\n\n## Iterate deliberately\n- Start simple, then add one constraint per iteration.\n- If results look chaotic, reduce motion and simplify the scene.\n- When a result is close, pin it as a reference and describe only the tweak.\n"
  },
  {
    "path": "skills/.curated/sora/references/sample-prompts.md",
    "content": "# Sample prompts (copy/paste)\n\nUse these as starting points. Keep user-provided requirements and constraints; do not invent new creative elements.\n\nFor prompting principles (structure, invariants, iteration), see `references/prompting.md`.\n\n## Contents\n- [Product teaser (single shot)](#product-teaser-single-shot)\n- [UI demo (screen recording style)](#ui-demo-screen-recording-style)\n- [Cinematic detail shot](#cinematic-detail-shot)\n- [Social ad (6s with beats)](#social-ad-6s-with-beats)\n- [Character continuity shot](#character-continuity-shot)\n- [Edit follow-up](#edit-follow-up)\n- [Extension follow-up](#extension-follow-up)\n- [Motion graphics explainer](#motion-graphics-explainer)\n- [Ambient loop (atmosphere)](#ambient-loop-atmosphere)\n\n## Product teaser (single shot)\n```\nUse case: product teaser\nPrimary request: close-up of a matte black wireless speaker on a stone pedestal\nScene/background: dark studio cyclorama, subtle haze\nSubject: compact speaker with soft fabric texture\nAction: slow 20-degree orbit over 4 seconds\nCamera: 85mm, shallow depth of field, steady dolly\nLighting/mood: soft key, gentle rim, premium studio feel\nColor palette: charcoal, slate, warm amber accents\nConstraints: no logos, no text\nAvoid: harsh bloom; oversharpening; clutter\n```\n\n## UI demo (screen recording style)\n```\nUse case: UI product demo\nPrimary request: a clean mobile budgeting app demo showing a weekly spend chart\nScene/background: neutral gradient backdrop\nSubject: smartphone UI, centered, screen content crisp and legible\nAction: tap the \"Add expense\" button, modal opens, amount typed, save\nCamera: locked-off, straight-on, no tilt\nLighting/mood: soft studio light, minimal reflections\nColor palette: off-white, slate, mint accent\nText (verbatim): \"Add expense\", \"$24.50\", \"Groceries\"\nConstraints: no brand logos; keep UI text readable; avoid motion blur\n```\n\n## Cinematic detail shot\n```\nUse case: cinematic product detail\nPrimary request: macro shot of raindrops sliding across a car hood\nScene/background: night city bokeh, soft rain mist\nSubject: glossy hood surface with water beads\nAction: slow push-in over 4 seconds\nCamera: 100mm macro, shallow depth of field\nLighting/mood: moody, high-contrast reflections, soft speculars\nColor palette: deep navy, teal, silver highlights\nConstraints: no logos, no text\nAvoid: flicker; unstable reflections; excessive noise\n```\n\n## Social ad (6s with beats)\n```\nUse case: social ad\nPrimary request: minimal coffee subscription ad with three quick beats\nScene/background: warm kitchen counter, morning light\nSubject: ceramic mug, coffee bag, steam\nAction: beat 1 (0-2s) pour coffee; beat 2 (2-4s) steam rises; beat 3 (4-6s) mug slides to center\nCamera: 50mm, gentle handheld drift\nLighting/mood: warm, cozy, natural light\nText (verbatim): \"Fresh roast\" (top-left), \"Weekly delivery\" (bottom-right)\nConstraints: no logos; text must be legible; avoid fast motion\n```\n\n## Character continuity shot\n```\nUse case: mascot continuity\nPrimary request: Mossy, a moss-covered teapot mascot, rushes through a lantern-lit market at dusk\nScene/background: narrow alley, hanging lanterns, light haze\nSubject: Mossy the moss-covered teapot mascot\nAction: quick jog through the alley, glances toward camera near the end\nCamera: 35mm, shoulder-height tracking shot, smooth lateral move\nLighting/mood: warm dusk practicals, cinematic glow\nColor palette: moss green, warm amber, charcoal\nConstraints: keep Mossy's silhouette, moss texture, and teapot proportions consistent\nAvoid: flicker; warped limbs; identity drift\n```\n\n## Edit follow-up\n```\nPrimary request: same shot and camera move; change only the palette to teal, sand, and rust with a warmer backlight\nConstraints: keep the subject, framing, and motion unchanged\nAvoid: new objects; reframing; speed changes\n```\n\n## Extension follow-up\n```\nPrimary request: continue the same shot as the camera rises above the rooftops and reveals sunrise over the city\nAction: maintain the existing motion, then gently tilt upward into the skyline reveal\nLighting/mood: dawn light growing warmer through the extension\nConstraints: preserve scene continuity, camera direction, and overall pacing\nAvoid: abrupt cuts; jumpy motion; sudden subject changes\n```\n\n## Motion graphics explainer\n```\nUse case: explainer clip\nPrimary request: clean motion-graphics animation showing data flowing into a dashboard\nScene/background: soft gradient background\nSubject: abstract nodes and lines, simple dashboard cards\nAction: nodes connect, data pulses, cards fill with charts\nCamera: locked-off, no depth, flat design\nLighting/mood: minimal, modern\nColor palette: off-white, graphite, teal, coral accents\nConstraints: no logos; keep shapes simple; avoid heavy texture\n```\n\n## Ambient loop (atmosphere)\n```\nUse case: ambient background loop\nPrimary request: fog drifting through a pine forest at dawn\nScene/background: tall pines, soft fog layers, distant hills\nSubject: drifting fog and light rays\nAction: slow lateral drift, subtle light change\nCamera: wide, locked-off, no tilt\nLighting/mood: calm, soft dawn light\nColor palette: muted greens, cool gray, pale gold\nConstraints: no text, no logos, no people\nAvoid: fast motion; flicker; abrupt lighting shifts\n```\n"
  },
  {
    "path": "skills/.curated/sora/references/social-ads.md",
    "content": "# Social ad templates (4-8s)\n\nShort clips work best with clear beats. Use 2-3 beats and keep text minimal.\n\n## Default template\n```\nUse case: social ad\nPrimary request: <ad concept>\nScene/background: <simple backdrop>\nSubject: <product or scene>\nAction: beat 1 (0-2s) <action>; beat 2 (2-4s) <action>; beat 3 (4-6s) <action>\nCamera: <shot type + motion>\nLighting/mood: <mood>\nText (verbatim): \"<short headline>\", \"<short subhead>\"\nConstraints: no logos; keep text legible; avoid fast motion\n```\n\n## Example: product benefit\n```\nUse case: social ad\nPrimary request: a compact humidifier emphasizing quiet operation\nScene/background: minimal bedroom nightstand\nSubject: matte white humidifier with soft vapor\nAction: beat 1 (0-2s) vapor begins; beat 2 (2-4s) soft glow turns on; beat 3 (4-6s) device slides to center\nCamera: 50mm, gentle push-in\nLighting/mood: calm, warm night light\nText (verbatim): \"Quiet mist\", \"Sleep better\"\nConstraints: no logos; text must be legible; avoid harsh highlights\n```\n\n## Example: before/after\n```\nUse case: social ad\nPrimary request: before/after of a cluttered desk becoming tidy\nScene/background: home office desk, neutral wall\nSubject: desk surface, organizer tray\nAction: beat 1 (0-2s) cluttered desk; beat 2 (2-4s) quick tidy motion; beat 3 (4-6s) clean desk with organizer\nCamera: top-down, locked-off\nLighting/mood: soft daylight\nText (verbatim): \"Before\", \"After\"\nConstraints: no logos; keep motion minimal; avoid blur\n```\n"
  },
  {
    "path": "skills/.curated/sora/references/troubleshooting.md",
    "content": "# Troubleshooting\n\n## Job fails with size or seconds errors\n- Cause: size is not supported by the chosen model, or seconds is outside `4`, `8`, `12`, `16`, `20`.\n- Fix: match size to model; use `sora-2-pro` for `1920x1080` or `1080x1920`.\n\n## Docs and SDK disagree on the latest limits or helpers\n- Cause: the March 2026 Sora guide/changelog is ahead of some typed SDK/API-reference surfaces.\n- Fix: follow the latest guide/changelog and use the bundled CLI, which bridges new flows through the official client’s low-level methods.\n\n## `edit`, `extend`, or `create-character` isn't available in your installed Python SDK\n- Cause: the published SDK may not expose new Sora helpers yet.\n- Fix: use `scripts/sora.py`; it uses the official OpenAI client directly for those endpoints.\n\n## openai SDK not installed\n- Cause: running `python \"$SORA_CLI\" ...` without the OpenAI SDK available.\n- Fix: run with `uv run --with openai python \"$SORA_CLI\" ...`.\n\n## uv cache permission error\n- Cause: uv cache directory is not writable in CI or sandboxed environments.\n- Fix: set `UV_CACHE_DIR=/tmp/uv-cache` (or another writable path) before running `uv`.\n\n## Prompt shell escaping issues\n- Cause: multi-line prompts or quotes break the shell.\n- Fix: use `--prompt-file prompt.txt`.\n\n## Prompt looks double-wrapped (\"Primary request: Use case: ...\")\n- Cause: you structured the prompt manually but left CLI augmentation on.\n- Fix: add `--no-augment`, or use the CLI fields (`--use-case`, `--scene`, etc.) instead of pre-formatting.\n\n## Input reference rejected\n- Cause: the file is not jpg/png/webp, includes a human face, or does not match the target size.\n- Fix: convert to jpg/png/webp, remove faces, and resize to match `--size`.\n\n## Character continuity is weak\n- Cause: the character clip is too long, mismatched in aspect ratio, outside the skill's non-human character workflow, or the prompt never names the character.\n- Fix: use a short non-human MP4, match aspect ratio to the target shot, and mention the character name verbatim in the prompt.\n\n## Extension looks jumpy or drifts\n- Cause: the continuation prompt changes too many things at once, or asks for a hard scene break.\n- Fix: describe the next beat only, preserve motion direction, and avoid introducing unrelated subjects or abrupt camera changes.\n\n## Remix drifts from the original\n- Cause: remix is a legacy endpoint and too many changes were requested at once.\n- Fix: prefer `edit`, state invariants explicitly, and change one element at a time.\n\n## Download fails or returns expired URL\n- Cause: normal download URLs expire after about 1 hour.\n- Fix: re-download while the link is fresh and copy the asset to your own storage promptly.\n\n## Video completes but looks unstable or flickers\n- Cause: multiple actions, aggressive camera motion, or overly long prompt timing for the clip length.\n- Fix: reduce to one main action and one camera move; keep beats simple; add constraints like `avoid flicker` or `stable motion`.\n\n## Text is unreadable\n- Cause: text is too long, too small, or moving.\n- Fix: shorten text, keep the camera locked-off, and avoid fast motion.\n\n## Job stuck in `queued` or `in_progress`\n- Cause: temporary queue delays or slower higher-resolution renders.\n- Fix: increase timeout, poll less aggressively, and expect longer waits for `16`/`20` second or 1080p jobs.\n\n## `create-batch` is not behaving like the Batch API\n- Cause: `create-batch` is a local concurrent helper, not the official Batch API.\n- Fix: use the Files + Batches APIs for true offline batching; use `create-batch` only for immediate local fan-out.\n\n## Cleanup blocked by sandbox policy\n- Cause: some environments block `rm`.\n- Fix: skip cleanup, or truncate temporary files instead of deleting them.\n"
  },
  {
    "path": "skills/.curated/sora/references/video-api.md",
    "content": "# Sora Video API quick reference\n\nKeep this file short; the full source of truth is the latest OpenAI Sora guide plus the API changelog.\n\n## Source-of-truth note\n- The March 2026 changelog and Sora guide added characters, 16s/20s clips, `1920x1080` / `1080x1920` on `sora-2-pro`, extensions, and edits.\n- Some typed SDK and API-reference pages may still show the older `4`/`8`/`12` and pre-1080p enums.\n- If they disagree, follow the latest guide/changelog and use the bundled CLI, which bridges the SDK lag with low-level official-client calls.\n\n## Models\n- `sora-2`: faster, flexible iteration\n- `sora-2-pro`: higher fidelity, slower, more expensive\n\n## Sizes (by model)\n- `sora-2`: `1280x720`, `720x1280`\n- `sora-2-pro`: `1280x720`, `720x1280`, `1024x1792`, `1792x1024`, `1920x1080`, `1080x1920`\n- Use `sora-2-pro` for 1080p exports.\n\n## Duration\n- `seconds`: `\"4\"`, `\"8\"`, `\"12\"`, `\"16\"`, `\"20\"`\n- Use shorter clips first when iterating on motion, timing, or composition.\n\n## Input references\n- `input_reference` guides the first frame of a generation.\n- Multipart requests use an uploaded image file.\n- JSON requests use an object with exactly one of `file_id` or `image_url`.\n- Supported image formats: jpg/jpeg, png, webp.\n- Input references should match the target `size`.\n\n## Characters\n- Create reusable non-human characters via `POST /v1/videos/characters`.\n- Character source clips work best as short MP4s (`2`-`4`s) in `16:9` or `9:16`, at `720p`-`1080p`.\n- Reference up to two characters per generation with `characters: [{\"id\": \"...\"}]`.\n- Mention the character name verbatim in the prompt; the ID alone is not enough.\n- Characters can be combined with `input_reference`.\n- In this skill, character workflows are limited to non-human subjects.\n\n## Edits vs remix\n- Preferred: `POST /v1/videos/edits`\n- Legacy/deprecated: `POST /v1/videos/{video_id}/remix`\n- Use edits for new integrations.\n- In this skill, use edits for existing generated video IDs only.\n\n## Extensions\n- Use `POST /v1/videos/extensions` to continue a completed video.\n- Each extension can add up to `20` seconds.\n- A single video can be extended up to six times, for a maximum total length of `120` seconds.\n- Extensions do not support characters or image references.\n\n## Jobs and status\n- Creation, edit, and extension jobs are async.\n- Common statuses: `queued`, `in_progress`, `completed`, `failed`\n- Poll every `10`-`20`s or use webhooks.\n- Webhook events: `video.completed`, `video.failed`\n\n## Core endpoints\n- `POST /videos`: create\n- `POST /videos/characters`: create a reusable character\n- `POST /videos/edits`: edit an existing generated video by ID\n- `POST /videos/extensions`: extend a completed video\n- `GET /videos/{id}`: retrieve status/details\n- `GET /videos/{id}/content`: download content\n- `GET /videos`: list\n- `DELETE /videos/{id}`: delete\n- `POST /videos/{id}/remix`: legacy/deprecated\n\n## Download variants\n- `video` -> mp4\n- `thumbnail` -> webp\n- `spritesheet` -> jpg\n\nDownload URLs expire after about 1 hour; save assets to your own storage promptly.\n\n## Batch API\n- The official Batch API supports `POST /v1/videos` only.\n- Batch requests must use JSON, not multipart.\n- Upload assets ahead of time and reference them in the JSON body.\n- For image-guided Batch jobs, use JSON `input_reference` with `file_id` or `image_url`.\n- Batch-generated videos remain downloadable for up to 24 hours after the batch completes.\n- The bundled `scripts/sora.py create-batch` command is a local fan-out helper, not the official Batch API.\n\n## Guardrails\n- Only content suitable for audiences under 18\n- No copyrighted characters or copyrighted music\n- No real people (including public figures)\n- Input images with human faces are currently rejected\n"
  },
  {
    "path": "skills/.curated/sora/scripts/sora.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Create and manage Sora videos with the OpenAI Video API.\n\nDefaults to sora-2 and a structured prompt augmentation workflow.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport asyncio\nimport json\nimport os\nfrom pathlib import Path\nimport re\nimport sys\nimport time\nfrom typing import Any, Dict, Iterable, List, Optional, Tuple, Union\n\nDEFAULT_MODEL = \"sora-2\"\nDEFAULT_SIZE = \"1280x720\"\nDEFAULT_SECONDS = \"4\"\nDEFAULT_POLL_INTERVAL = 10.0\nDEFAULT_VARIANT = \"video\"\nDEFAULT_CONCURRENCY = 3\nDEFAULT_MAX_ATTEMPTS = 3\n\nALLOWED_MODELS = {\"sora-2\", \"sora-2-pro\"}\nALLOWED_SIZES_SORA2 = {\"1280x720\", \"720x1280\"}\nALLOWED_SIZES_SORA2_PRO = {\n    \"1280x720\",\n    \"720x1280\",\n    \"1024x1792\",\n    \"1792x1024\",\n    \"1080x1920\",\n    \"1920x1080\",\n}\nALLOWED_SECONDS = {\"4\", \"8\", \"12\", \"16\", \"20\"}\nALLOWED_VARIANTS = {\"video\", \"thumbnail\", \"spritesheet\"}\nALLOWED_ORDERS = {\"asc\", \"desc\"}\nALLOWED_INPUT_EXTS = {\".jpg\", \".jpeg\", \".png\", \".webp\"}\nALLOWED_VIDEO_EXTS = {\".mp4\"}\nTERMINAL_STATUSES = {\"completed\", \"failed\", \"canceled\", \"expired\"}\n\nVARIANT_EXTENSIONS = {\"video\": \".mp4\", \"thumbnail\": \".webp\", \"spritesheet\": \".jpg\"}\n\nMAX_BATCH_JOBS = 200\n\n\ndef _die(message: str, code: int = 1) -> None:\n    print(f\"Error: {message}\", file=sys.stderr)\n    raise SystemExit(code)\n\n\ndef _warn(message: str) -> None:\n    print(f\"Warning: {message}\", file=sys.stderr)\n\n\ndef _ensure_api_key(dry_run: bool) -> None:\n    if os.getenv(\"OPENAI_API_KEY\"):\n        print(\"OPENAI_API_KEY is set.\", file=sys.stderr)\n        return\n    if dry_run:\n        _warn(\"OPENAI_API_KEY is not set; dry-run only.\")\n        return\n    _die(\"OPENAI_API_KEY is not set. Export it before running.\")\n\n\ndef _read_prompt(prompt: Optional[str], prompt_file: Optional[str]) -> str:\n    if prompt and prompt_file:\n        _die(\"Use --prompt or --prompt-file, not both.\")\n    if prompt_file:\n        path = Path(prompt_file)\n        if not path.exists():\n            _die(f\"Prompt file not found: {path}\")\n        return path.read_text(encoding=\"utf-8\").strip()\n    if prompt:\n        return prompt.strip()\n    _die(\"Missing prompt. Use --prompt or --prompt-file.\")\n    return \"\"  # unreachable\n\n\ndef _normalize_model(model: Optional[str]) -> str:\n    value = (model or DEFAULT_MODEL).strip().lower()\n    if value not in ALLOWED_MODELS:\n        _die(\"model must be one of: sora-2, sora-2-pro\")\n    return value\n\n\ndef _normalize_size(size: Optional[str], model: str) -> str:\n    value = (size or DEFAULT_SIZE).strip().lower()\n    allowed = ALLOWED_SIZES_SORA2 if model == \"sora-2\" else ALLOWED_SIZES_SORA2_PRO\n    if value not in allowed:\n        allowed_list = \", \".join(sorted(allowed))\n        _die(f\"size must be one of: {allowed_list} for model {model}\")\n    return value\n\n\ndef _normalize_seconds(seconds: Optional[Union[int, str]]) -> str:\n    if seconds is None:\n        value = DEFAULT_SECONDS\n    elif isinstance(seconds, int):\n        value = str(seconds)\n    else:\n        value = str(seconds).strip()\n    if value not in ALLOWED_SECONDS:\n        _die(\"seconds must be one of: 4, 8, 12, 16, 20\")\n    return value\n\n\ndef _normalize_variant(variant: Optional[str]) -> str:\n    value = (variant or DEFAULT_VARIANT).strip().lower()\n    if value not in ALLOWED_VARIANTS:\n        _die(\"variant must be one of: video, thumbnail, spritesheet\")\n    return value\n\n\ndef _normalize_order(order: Optional[str]) -> Optional[str]:\n    if order is None:\n        return None\n    value = order.strip().lower()\n    if value not in ALLOWED_ORDERS:\n        _die(\"order must be one of: asc, desc\")\n    return value\n\n\ndef _normalize_poll_interval(interval: Optional[float]) -> float:\n    value = float(interval if interval is not None else DEFAULT_POLL_INTERVAL)\n    if value <= 0:\n        _die(\"poll-interval must be > 0\")\n    return value\n\n\ndef _normalize_timeout(timeout: Optional[float]) -> Optional[float]:\n    if timeout is None:\n        return None\n    value = float(timeout)\n    if value <= 0:\n        _die(\"timeout must be > 0\")\n    return value\n\n\ndef _default_out_path(variant: str) -> Path:\n    if variant == \"video\":\n        return Path(\"video.mp4\")\n    if variant == \"thumbnail\":\n        return Path(\"thumbnail.webp\")\n    return Path(\"spritesheet.jpg\")\n\n\ndef _normalize_out_path(out: Optional[str], variant: str) -> Path:\n    expected_ext = VARIANT_EXTENSIONS[variant]\n    if not out:\n        return _default_out_path(variant)\n    path = Path(out)\n    if path.suffix == \"\":\n        return path.with_suffix(expected_ext)\n    if path.suffix.lower() != expected_ext:\n        _warn(f\"Output extension {path.suffix} does not match {expected_ext} for {variant}.\")\n    return path\n\n\ndef _normalize_json_out(out: Optional[str], default_name: str) -> Optional[Path]:\n    if not out:\n        return None\n    raw = str(out)\n    if raw.endswith(\"/\") or raw.endswith(os.sep):\n        return Path(raw) / default_name\n    path = Path(out)\n    if path.exists() and path.is_dir():\n        return path / default_name\n    if path.suffix == \"\":\n        path = path.with_suffix(\".json\")\n    return path\n\n\ndef _normalize_input_reference_object(value: Any) -> Dict[str, str]:\n    if not isinstance(value, dict):\n        _die(\"input_reference object must be a JSON object with file_id or image_url.\")\n\n    file_id = str(value.get(\"file_id\", \"\")).strip()\n    image_url = str(value.get(\"image_url\", \"\")).strip()\n\n    if bool(file_id) == bool(image_url):\n        _die(\"input_reference object must include exactly one of file_id or image_url.\")\n\n    if file_id:\n        return {\"file_id\": file_id}\n    return {\"image_url\": image_url}\n\n\ndef _normalize_input_reference(\n    *,\n    value: Any = None,\n    path: Optional[str] = None,\n    file_id: Optional[str] = None,\n    image_url: Optional[str] = None,\n) -> Tuple[Optional[str], Optional[Dict[str, str]]]:\n    if value is not None:\n        if any(item is not None for item in (path, file_id, image_url)):\n            _die(\n                \"Use either input_reference or explicit input-reference path/file-id/url fields, not both.\"\n            )\n        if isinstance(value, str):\n            path = value\n        elif isinstance(value, dict):\n            return None, _normalize_input_reference_object(value)\n        else:\n            _die(\"input_reference must be a file path string or a JSON object.\")\n\n    provided = [bool(path), bool(file_id), bool(image_url)]\n    if sum(provided) > 1:\n        _die(\"Use only one of --input-reference, --input-reference-file-id, or --input-reference-url.\")\n\n    if path:\n        return str(path), None\n    if file_id:\n        return None, {\"file_id\": str(file_id).strip()}\n    if image_url:\n        return None, {\"image_url\": str(image_url).strip()}\n    return None, None\n\n\ndef _normalize_characters(raw: Any) -> Optional[List[Dict[str, str]]]:\n    if raw is None:\n        return None\n\n    items: List[Any]\n    if isinstance(raw, str):\n        items = [part.strip() for part in raw.split(\",\") if part.strip()]\n    elif isinstance(raw, (list, tuple)):\n        items = list(raw)\n    else:\n        _die(\"characters must be a list of IDs, a comma-separated string, or objects with an id field.\")\n        return None\n\n    if not items:\n        return None\n\n    normalized: List[Dict[str, str]] = []\n    for item in items:\n        if isinstance(item, str):\n            char_id = item.strip()\n        elif isinstance(item, dict):\n            char_id = str(item.get(\"id\", \"\")).strip()\n        else:\n            _die(\"Each character must be a string ID or an object with an id field.\")\n            return None\n\n        if not char_id:\n            _die(\"Character IDs must be non-empty.\")\n        normalized.append({\"id\": char_id})\n\n    if len(normalized) > 2:\n        _die(\"A single video can include at most 2 characters.\")\n\n    return normalized\n\n\ndef _open_input_reference(path: Optional[str]):\n    if not path:\n        return _NullContext()\n    p = Path(path)\n    if not p.exists():\n        _die(f\"Input reference not found: {p}\")\n    if p.suffix.lower() not in ALLOWED_INPUT_EXTS:\n        _warn(\"Input reference should be jpeg, png, or webp.\")\n    return _SingleFile(p)\n\n\ndef _open_video_upload(path: Optional[str], *, label: str) -> Any:\n    if not path:\n        return _NullContext()\n    p = Path(path)\n    if not p.exists():\n        _die(f\"{label} not found: {p}\")\n    if p.suffix.lower() not in ALLOWED_VIDEO_EXTS:\n        _warn(f\"{label} should usually be an MP4 file.\")\n    return _SingleFile(p)\n\n\ndef _create_client():\n    try:\n        from openai import OpenAI\n    except ImportError:\n        _die(\"openai SDK not installed. Run with `uv run --with openai` or install with `uv pip install openai`.\")\n    return OpenAI()\n\n\ndef _create_async_client():\n    try:\n        from openai import AsyncOpenAI\n    except ImportError:\n        try:\n            import openai as _openai  # noqa: F401\n        except ImportError:\n            _die(\"openai SDK not installed. Run with `uv run --with openai` or install with `uv pip install openai`.\")\n        _die(\n            \"AsyncOpenAI not available in this openai SDK version. Upgrade with `uv pip install -U openai`.\"\n        )\n    return AsyncOpenAI()\n\n\ndef _make_request_options(*, multipart: bool) -> Dict[str, Any]:\n    from openai.resources.videos import make_request_options\n\n    headers = {\"Content-Type\": \"multipart/form-data\"} if multipart else None\n    return make_request_options(extra_headers=headers)\n\n\ndef _video_post(\n    client: Any,\n    path: str,\n    payload: Dict[str, Any],\n    *,\n    files: Optional[List[Tuple[str, Any]]] = None,\n) -> Any:\n    return client.post(\n        path,\n        cast_to=dict,\n        body=payload,\n        files=files,\n        options=_make_request_options(multipart=bool(files)),\n    )\n\n\nasync def _async_video_post(\n    client: Any,\n    path: str,\n    payload: Dict[str, Any],\n    *,\n    files: Optional[List[Tuple[str, Any]]] = None,\n) -> Any:\n    return await client.post(\n        path,\n        cast_to=dict,\n        body=payload,\n        files=files,\n        options=_make_request_options(multipart=bool(files)),\n    )\n\n\ndef _to_dict(obj: Any) -> Any:\n    if isinstance(obj, dict):\n        return obj\n    if hasattr(obj, \"model_dump\"):\n        return obj.model_dump()\n    if hasattr(obj, \"dict\"):\n        return obj.dict()\n    if hasattr(obj, \"__dict__\"):\n        return obj.__dict__\n    return obj\n\n\ndef _print_json(obj: Any) -> None:\n    print(json.dumps(_to_dict(obj), indent=2, sort_keys=True))\n\n\ndef _print_request(payload: Dict[str, Any]) -> None:\n    print(json.dumps(payload, indent=2, sort_keys=True))\n\n\ndef _slugify(value: str) -> str:\n    value = value.strip().lower()\n    value = re.sub(r\"[^a-z0-9]+\", \"-\", value)\n    value = re.sub(r\"-{2,}\", \"-\", value).strip(\"-\")\n    return value[:60] if value else \"job\"\n\n\ndef _normalize_job(job: Any, idx: int) -> Dict[str, Any]:\n    if isinstance(job, str):\n        prompt = job.strip()\n        if not prompt:\n            _die(f\"Empty prompt at job {idx}\")\n        return {\"prompt\": prompt}\n    if isinstance(job, dict):\n        if \"prompt\" not in job or not str(job[\"prompt\"]).strip():\n            _die(f\"Missing prompt for job {idx}\")\n        return job\n    _die(f\"Invalid job at index {idx}: expected string or object.\")\n    return {}  # unreachable\n\n\ndef _read_jobs_jsonl(path: str) -> List[Dict[str, Any]]:\n    p = Path(path)\n    if not p.exists():\n        _die(f\"Input file not found: {p}\")\n    jobs: List[Dict[str, Any]] = []\n    for line_no, raw in enumerate(p.read_text(encoding=\"utf-8\").splitlines(), start=1):\n        line = raw.strip()\n        if not line or line.startswith(\"#\"):\n            continue\n        try:\n            item: Any\n            if line.startswith(\"{\"):\n                item = json.loads(line)\n            else:\n                item = line\n            jobs.append(_normalize_job(item, idx=line_no))\n        except json.JSONDecodeError as exc:\n            _die(f\"Invalid JSON on line {line_no}: {exc}\")\n    if not jobs:\n        _die(\"No jobs found in input file.\")\n    if len(jobs) > MAX_BATCH_JOBS:\n        _die(f\"Too many jobs ({len(jobs)}). Max is {MAX_BATCH_JOBS}.\")\n    return jobs\n\n\ndef _merge_non_null(dst: Dict[str, Any], src: Dict[str, Any]) -> Dict[str, Any]:\n    merged = dict(dst)\n    for k, v in src.items():\n        if v is not None:\n            merged[k] = v\n    return merged\n\n\ndef _job_output_path(out_dir: Path, idx: int, prompt: str, explicit_out: Optional[str]) -> Path:\n    out_dir.mkdir(parents=True, exist_ok=True)\n    if explicit_out:\n        path = Path(explicit_out)\n        if path.suffix == \"\":\n            path = path.with_suffix(\".json\")\n        return out_dir / path.name\n    slug = _slugify(prompt[:80])\n    return out_dir / f\"{idx:03d}-{slug}.json\"\n\n\ndef _extract_retry_after_seconds(exc: Exception) -> Optional[float]:\n    for attr in (\"retry_after\", \"retry_after_seconds\"):\n        val = getattr(exc, attr, None)\n        if isinstance(val, (int, float)) and val >= 0:\n            return float(val)\n    msg = str(exc)\n    m = re.search(r\"retry[- ]after[:= ]+([0-9]+(?:\\\\.[0-9]+)?)\", msg, re.IGNORECASE)\n    if m:\n        try:\n            return float(m.group(1))\n        except Exception:\n            return None\n    return None\n\n\ndef _is_rate_limit_error(exc: Exception) -> bool:\n    name = exc.__class__.__name__.lower()\n    if \"ratelimit\" in name or \"rate_limit\" in name:\n        return True\n    msg = str(exc).lower()\n    return \"429\" in msg or \"rate limit\" in msg or \"too many requests\" in msg\n\n\ndef _is_transient_error(exc: Exception) -> bool:\n    if _is_rate_limit_error(exc):\n        return True\n    name = exc.__class__.__name__.lower()\n    if \"timeout\" in name or \"timedout\" in name or \"tempor\" in name:\n        return True\n    msg = str(exc).lower()\n    return \"timeout\" in msg or \"timed out\" in msg or \"connection reset\" in msg\n\n\ndef _fields_from_args(args: argparse.Namespace) -> Dict[str, Optional[str]]:\n    return {\n        \"use_case\": getattr(args, \"use_case\", None),\n        \"scene\": getattr(args, \"scene\", None),\n        \"subject\": getattr(args, \"subject\", None),\n        \"action\": getattr(args, \"action\", None),\n        \"camera\": getattr(args, \"camera\", None),\n        \"style\": getattr(args, \"style\", None),\n        \"lighting\": getattr(args, \"lighting\", None),\n        \"palette\": getattr(args, \"palette\", None),\n        \"audio\": getattr(args, \"audio\", None),\n        \"dialogue\": getattr(args, \"dialogue\", None),\n        \"text\": getattr(args, \"text\", None),\n        \"timing\": getattr(args, \"timing\", None),\n        \"constraints\": getattr(args, \"constraints\", None),\n        \"negative\": getattr(args, \"negative\", None),\n    }\n\n\ndef _augment_prompt_fields(augment: bool, prompt: str, fields: Dict[str, Optional[str]]) -> str:\n    if not augment:\n        return prompt\n\n    sections: List[str] = []\n    if fields.get(\"use_case\"):\n        sections.append(f\"Use case: {fields['use_case']}\")\n    sections.append(f\"Primary request: {prompt}\")\n    if fields.get(\"scene\"):\n        sections.append(f\"Scene/background: {fields['scene']}\")\n    if fields.get(\"subject\"):\n        sections.append(f\"Subject: {fields['subject']}\")\n    if fields.get(\"action\"):\n        sections.append(f\"Action: {fields['action']}\")\n    if fields.get(\"camera\"):\n        sections.append(f\"Camera: {fields['camera']}\")\n    if fields.get(\"lighting\"):\n        sections.append(f\"Lighting/mood: {fields['lighting']}\")\n    if fields.get(\"palette\"):\n        sections.append(f\"Color palette: {fields['palette']}\")\n    if fields.get(\"style\"):\n        sections.append(f\"Style/format: {fields['style']}\")\n    if fields.get(\"timing\"):\n        sections.append(f\"Timing/beats: {fields['timing']}\")\n    if fields.get(\"audio\"):\n        sections.append(f\"Audio: {fields['audio']}\")\n    if fields.get(\"text\"):\n        sections.append(f\"Text (verbatim): \\\"{fields['text']}\\\"\")\n    if fields.get(\"dialogue\"):\n        dialogue = fields[\"dialogue\"].strip()\n        sections.append(\"Dialogue:\\n<dialogue>\\n\" + dialogue + \"\\n</dialogue>\")\n    if fields.get(\"constraints\"):\n        sections.append(f\"Constraints: {fields['constraints']}\")\n    if fields.get(\"negative\"):\n        sections.append(f\"Avoid: {fields['negative']}\")\n\n    return \"\\n\".join(sections)\n\n\ndef _augment_prompt(args: argparse.Namespace, prompt: str) -> str:\n    fields = _fields_from_args(args)\n    return _augment_prompt_fields(args.augment, prompt, fields)\n\n\ndef _get_status(video: Any) -> Optional[str]:\n    if isinstance(video, dict):\n        for key in (\"status\", \"state\"):\n            if key in video and isinstance(video[key], str):\n                return video[key]\n        data = video.get(\"data\") if isinstance(video.get(\"data\"), dict) else None\n        if data:\n            for key in (\"status\", \"state\"):\n                if key in data and isinstance(data[key], str):\n                    return data[key]\n        return None\n    for key in (\"status\", \"state\"):\n        val = getattr(video, key, None)\n        if isinstance(val, str):\n            return val\n    return None\n\n\ndef _get_video_id(video: Any) -> Optional[str]:\n    if isinstance(video, dict):\n        if isinstance(video.get(\"id\"), str):\n            return video[\"id\"]\n        data = video.get(\"data\") if isinstance(video.get(\"data\"), dict) else None\n        if data and isinstance(data.get(\"id\"), str):\n            return data[\"id\"]\n        return None\n    vid = getattr(video, \"id\", None)\n    return vid if isinstance(vid, str) else None\n\n\ndef _poll_video(\n    client: Any,\n    video_id: str,\n    *,\n    poll_interval: float,\n    timeout: Optional[float],\n) -> Any:\n    start = time.time()\n    last_status: Optional[str] = None\n\n    while True:\n        video = client.videos.retrieve(video_id)\n        status = _get_status(video) or \"unknown\"\n        if status != last_status:\n            print(f\"Status: {status}\", file=sys.stderr)\n            last_status = status\n        if status in TERMINAL_STATUSES:\n            return video\n        if timeout is not None and (time.time() - start) > timeout:\n            _die(f\"Timed out after {timeout:.1f}s waiting for {video_id}\")\n        time.sleep(poll_interval)\n\n\ndef _download_content(client: Any, video_id: str, variant: str) -> Any:\n    content = client.videos.download_content(video_id, variant=variant)\n    if hasattr(content, \"write_to_file\"):\n        return content\n    if hasattr(content, \"read\"):\n        return content.read()\n    if isinstance(content, (bytes, bytearray)):\n        return bytes(content)\n    if hasattr(content, \"content\"):\n        return content.content\n    return content\n\n\ndef _write_download(data: Any, out_path: Path, *, force: bool) -> None:\n    if out_path.exists() and not force:\n        _die(f\"Output exists: {out_path} (use --force to overwrite)\")\n    if hasattr(data, \"write_to_file\"):\n        data.write_to_file(out_path)\n        print(f\"Wrote {out_path}\")\n        return\n    if hasattr(data, \"read\"):\n        out_path.write_bytes(data.read())\n        print(f\"Wrote {out_path}\")\n        return\n    out_path.write_bytes(data)\n    print(f\"Wrote {out_path}\")\n\n\ndef _build_create_payload(args: argparse.Namespace, prompt: str) -> Dict[str, Any]:\n    model = _normalize_model(args.model)\n    size = _normalize_size(args.size, model)\n    seconds = _normalize_seconds(args.seconds)\n    payload: Dict[str, Any] = {\n        \"model\": model,\n        \"prompt\": prompt,\n        \"size\": size,\n        \"seconds\": seconds,\n    }\n    characters = _normalize_characters(getattr(args, \"character_id\", None))\n    if characters:\n        payload[\"characters\"] = characters\n\n    _, input_reference_json = _normalize_input_reference(\n        path=getattr(args, \"input_reference\", None),\n        file_id=getattr(args, \"input_reference_file_id\", None),\n        image_url=getattr(args, \"input_reference_url\", None),\n    )\n    if input_reference_json is not None:\n        payload[\"input_reference\"] = input_reference_json\n\n    return payload\n\n\ndef _prepare_job_payload(\n    args: argparse.Namespace,\n    job: Dict[str, Any],\n    base_fields: Dict[str, Optional[str]],\n    base_payload: Dict[str, Any],\n) -> Tuple[Dict[str, Any], Optional[str], str]:\n    prompt = str(job[\"prompt\"]).strip()\n    fields = _merge_non_null(base_fields, job.get(\"fields\", {}))\n    fields = _merge_non_null(fields, {k: job.get(k) for k in base_fields.keys()})\n    augmented = _augment_prompt_fields(args.augment, prompt, fields)\n\n    payload = dict(base_payload)\n    payload[\"prompt\"] = augmented\n    payload = _merge_non_null(payload, {k: job.get(k) for k in base_payload.keys()})\n    payload = {k: v for k, v in payload.items() if v is not None}\n\n    model = _normalize_model(payload.get(\"model\"))\n    size = _normalize_size(payload.get(\"size\"), model)\n    seconds = _normalize_seconds(payload.get(\"seconds\"))\n\n    payload[\"model\"] = model\n    payload[\"size\"] = size\n    payload[\"seconds\"] = seconds\n\n    raw_characters: Any = payload.get(\"characters\")\n    if \"characters\" in job:\n        raw_characters = job.get(\"characters\")\n    elif \"character_ids\" in job:\n        raw_characters = job.get(\"character_ids\")\n\n    characters = _normalize_characters(raw_characters)\n    if characters:\n        payload[\"characters\"] = characters\n    else:\n        payload.pop(\"characters\", None)\n\n    default_input_ref_path, default_input_ref_json = _normalize_input_reference(\n        path=getattr(args, \"input_reference\", None),\n        file_id=getattr(args, \"input_reference_file_id\", None),\n        image_url=getattr(args, \"input_reference_url\", None),\n    )\n    input_ref_path = default_input_ref_path\n    input_ref_json = dict(default_input_ref_json) if default_input_ref_json else None\n\n    if any(\n        key in job\n        for key in (\n            \"input_reference\",\n            \"input_reference_path\",\n            \"input_reference_file\",\n            \"input_reference_file_id\",\n            \"input_reference_url\",\n        )\n    ):\n        input_ref_path, input_ref_json = _normalize_input_reference(\n            value=job.get(\"input_reference\"),\n            path=job.get(\"input_reference_path\") or job.get(\"input_reference_file\"),\n            file_id=job.get(\"input_reference_file_id\"),\n            image_url=job.get(\"input_reference_url\"),\n        )\n\n    if input_ref_json is not None:\n        payload[\"input_reference\"] = input_ref_json\n    else:\n        payload.pop(\"input_reference\", None)\n\n    return payload, input_ref_path, prompt\n\n\ndef _write_json(path: Path, obj: Any) -> None:\n    path.parent.mkdir(parents=True, exist_ok=True)\n    path.write_text(json.dumps(_to_dict(obj), indent=2, sort_keys=True), encoding=\"utf-8\")\n    print(f\"Wrote {path}\")\n\n\ndef _write_json_out(out_path: Optional[Path], obj: Any) -> None:\n    if out_path is None:\n        return\n    _write_json(out_path, obj)\n\n\nasync def _create_one_with_retries(\n    client: Any,\n    payload: Dict[str, Any],\n    *,\n    files: Optional[List[Tuple[str, Any]]] = None,\n    attempts: int,\n    job_label: str,\n) -> Any:\n    last_exc: Optional[Exception] = None\n    for attempt in range(1, attempts + 1):\n        try:\n            return await _async_video_post(client, \"/videos\", payload, files=files)\n        except Exception as exc:\n            last_exc = exc\n            if not _is_transient_error(exc):\n                raise\n            if attempt == attempts:\n                raise\n            sleep_s = _extract_retry_after_seconds(exc)\n            if sleep_s is None:\n                sleep_s = min(60.0, 2.0**attempt)\n            print(\n                f\"{job_label} attempt {attempt}/{attempts} failed ({exc.__class__.__name__}); retrying in {sleep_s:.1f}s\",\n                file=sys.stderr,\n            )\n            await asyncio.sleep(sleep_s)\n    raise last_exc or RuntimeError(\"unknown error\")\n\n\nasync def _run_create_batch(args: argparse.Namespace) -> int:\n    jobs = _read_jobs_jsonl(args.input)\n    out_dir = Path(args.out_dir)\n\n    base_fields = _fields_from_args(args)\n    base_payload = {\n        \"model\": args.model,\n        \"size\": args.size,\n        \"seconds\": args.seconds,\n        \"characters\": _normalize_characters(getattr(args, \"character_id\", None)),\n    }\n\n    if args.dry_run:\n        for i, job in enumerate(jobs, start=1):\n            payload, input_ref, prompt = _prepare_job_payload(args, job, base_fields, base_payload)\n            out_path = _job_output_path(out_dir, i, prompt, job.get(\"out\"))\n            preview = dict(payload)\n            if input_ref:\n                preview[\"input_reference\"] = input_ref\n            _print_request(\n                {\n                    \"endpoint\": \"/v1/videos\",\n                    \"job\": i,\n                    \"output\": str(out_path),\n                    **preview,\n                }\n            )\n        return 0\n\n    client = _create_async_client()\n    sem = asyncio.Semaphore(args.concurrency)\n    any_failed = False\n\n    async def run_job(i: int, job: Dict[str, Any]) -> Tuple[int, Optional[str]]:\n        nonlocal any_failed\n        payload, input_ref, prompt = _prepare_job_payload(args, job, base_fields, base_payload)\n        job_label = f\"[job {i}/{len(jobs)}]\"\n        out_path = _job_output_path(out_dir, i, prompt, job.get(\"out\"))\n\n        try:\n            async with sem:\n                print(f\"{job_label} starting\", file=sys.stderr)\n                started = time.time()\n                with _open_input_reference(input_ref) as ref:\n                    files = [(\"input_reference\", ref)] if ref is not None else None\n                    result = await _create_one_with_retries(\n                        client,\n                        payload,\n                        files=files,\n                        attempts=args.max_attempts,\n                        job_label=job_label,\n                    )\n                elapsed = time.time() - started\n                print(f\"{job_label} completed in {elapsed:.1f}s\", file=sys.stderr)\n            _write_json(out_path, result)\n            return i, None\n        except Exception as exc:\n            any_failed = True\n            print(f\"{job_label} failed: {exc}\", file=sys.stderr)\n            if args.fail_fast:\n                raise\n            return i, str(exc)\n\n    tasks = [asyncio.create_task(run_job(i, job)) for i, job in enumerate(jobs, start=1)]\n\n    try:\n        await asyncio.gather(*tasks)\n    except Exception:\n        for t in tasks:\n            if not t.done():\n                t.cancel()\n        raise\n\n    return 1 if any_failed else 0\n\n\ndef _create_batch(args: argparse.Namespace) -> None:\n    exit_code = asyncio.run(_run_create_batch(args))\n    if exit_code:\n        raise SystemExit(exit_code)\n\n\ndef _cmd_create(args: argparse.Namespace) -> int:\n    prompt = _read_prompt(args.prompt, args.prompt_file)\n    prompt = _augment_prompt(args, prompt)\n\n    payload = _build_create_payload(args, prompt)\n    input_reference_path, _ = _normalize_input_reference(\n        path=args.input_reference,\n        file_id=args.input_reference_file_id,\n        image_url=args.input_reference_url,\n    )\n    json_out = _normalize_json_out(args.json_out, \"create.json\")\n\n    if args.dry_run:\n        preview = dict(payload)\n        if input_reference_path:\n            preview[\"input_reference\"] = input_reference_path\n        _print_request({\"endpoint\": \"/v1/videos\", **preview})\n        _write_json_out(json_out, {\"dry_run\": True, \"request\": {\"endpoint\": \"/v1/videos\", **preview}})\n        return 0\n\n    client = _create_client()\n    with _open_input_reference(input_reference_path) as input_ref:\n        files = [(\"input_reference\", input_ref)] if input_ref is not None else None\n        video = _video_post(client, \"/videos\", payload, files=files)\n    _print_json(video)\n    _write_json_out(json_out, video)\n    return 0\n\n\ndef _cmd_create_and_poll(args: argparse.Namespace) -> int:\n    prompt = _read_prompt(args.prompt, args.prompt_file)\n    prompt = _augment_prompt(args, prompt)\n\n    payload = _build_create_payload(args, prompt)\n    input_reference_path, _ = _normalize_input_reference(\n        path=args.input_reference,\n        file_id=args.input_reference_file_id,\n        image_url=args.input_reference_url,\n    )\n    json_out = _normalize_json_out(args.json_out, \"create-and-poll.json\")\n\n    if args.dry_run:\n        preview = dict(payload)\n        if input_reference_path:\n            preview[\"input_reference\"] = input_reference_path\n        _print_request({\"endpoint\": \"/v1/videos\", **preview})\n        print(\"Would poll for completion.\")\n        if args.download:\n            variant = _normalize_variant(args.variant)\n            out_path = _normalize_out_path(args.out, variant)\n            print(f\"Would download variant={variant} to {out_path}\")\n        if json_out:\n            dry_bundle: Dict[str, Any] = {\n                \"dry_run\": True,\n                \"request\": {\"endpoint\": \"/v1/videos\", **preview},\n                \"poll\": True,\n            }\n            if args.download:\n                dry_bundle[\"download\"] = {\n                    \"variant\": variant,\n                    \"out\": str(out_path),\n                }\n            _write_json_out(json_out, dry_bundle)\n        return 0\n\n    client = _create_client()\n    with _open_input_reference(input_reference_path) as input_ref:\n        files = [(\"input_reference\", input_ref)] if input_ref is not None else None\n        video = _video_post(client, \"/videos\", payload, files=files)\n    _print_json(video)\n\n    video_id = _get_video_id(video)\n    if not video_id:\n        _die(\"Could not determine video id from create response.\")\n\n    poll_interval = _normalize_poll_interval(args.poll_interval)\n    timeout = _normalize_timeout(args.timeout)\n    final_video = _poll_video(\n        client,\n        video_id,\n        poll_interval=poll_interval,\n        timeout=timeout,\n    )\n    _print_json(final_video)\n\n    if args.download:\n        status = _get_status(final_video) or \"unknown\"\n        if status != \"completed\":\n            _die(f\"Video status is {status}; download is available only after completion.\")\n        variant = _normalize_variant(args.variant)\n        out_path = _normalize_out_path(args.out, variant)\n        data = _download_content(client, video_id, variant)\n        _write_download(data, out_path, force=args.force)\n\n    if json_out:\n        _write_json_out(\n            json_out,\n            {\"create\": _to_dict(video), \"final\": _to_dict(final_video)},\n        )\n\n    return 0\n\n\ndef _cmd_poll(args: argparse.Namespace) -> int:\n    poll_interval = _normalize_poll_interval(args.poll_interval)\n    timeout = _normalize_timeout(args.timeout)\n    json_out = _normalize_json_out(args.json_out, \"poll.json\")\n\n    client = _create_client()\n    final_video = _poll_video(\n        client,\n        args.id,\n        poll_interval=poll_interval,\n        timeout=timeout,\n    )\n    _print_json(final_video)\n    _write_json_out(json_out, final_video)\n\n    if args.download:\n        status = _get_status(final_video) or \"unknown\"\n        if status != \"completed\":\n            _die(f\"Video status is {status}; download is available only after completion.\")\n        variant = _normalize_variant(args.variant)\n        out_path = _normalize_out_path(args.out, variant)\n        data = _download_content(client, args.id, variant)\n        _write_download(data, out_path, force=args.force)\n\n    return 0\n\n\ndef _cmd_status(args: argparse.Namespace) -> int:\n    json_out = _normalize_json_out(args.json_out, \"status.json\")\n    client = _create_client()\n    video = client.videos.retrieve(args.id)\n    _print_json(video)\n    _write_json_out(json_out, video)\n    return 0\n\n\ndef _cmd_list(args: argparse.Namespace) -> int:\n    if getattr(args, \"before\", None):\n        _die(\"--before is no longer supported by the Videos API docs. Use --after for pagination.\")\n\n    params: Dict[str, Any] = {\n        \"limit\": args.limit,\n        \"order\": _normalize_order(args.order),\n        \"after\": args.after,\n    }\n    params = {k: v for k, v in params.items() if v is not None}\n    json_out = _normalize_json_out(args.json_out, \"list.json\")\n    client = _create_client()\n    videos = client.videos.list(**params)\n    _print_json(videos)\n    _write_json_out(json_out, videos)\n    return 0\n\n\ndef _cmd_delete(args: argparse.Namespace) -> int:\n    json_out = _normalize_json_out(args.json_out, \"delete.json\")\n    client = _create_client()\n    result = client.videos.delete(args.id)\n    _print_json(result)\n    _write_json_out(json_out, result)\n    return 0\n\n\ndef _cmd_remix(args: argparse.Namespace) -> int:\n    prompt = _read_prompt(args.prompt, args.prompt_file)\n    prompt = _augment_prompt(args, prompt)\n    json_out = _normalize_json_out(args.json_out, \"remix.json\")\n    _warn(\"The remix endpoint is deprecated in the latest Sora docs. Prefer the `edit` command for new workflows.\")\n\n    if args.dry_run:\n        preview = {\"endpoint\": f\"/v1/videos/{args.id}/remix\", \"prompt\": prompt}\n        _print_request(preview)\n        _write_json_out(json_out, {\"dry_run\": True, \"request\": preview})\n        return 0\n\n    client = _create_client()\n    result = client.videos.remix(video_id=args.id, prompt=prompt)\n    _print_json(result)\n    _write_json_out(json_out, result)\n    return 0\n\n\ndef _cmd_download(args: argparse.Namespace) -> int:\n    variant = _normalize_variant(args.variant)\n    out_path = _normalize_out_path(args.out, variant)\n\n    client = _create_client()\n    data = _download_content(client, args.id, variant)\n    _write_download(data, out_path, force=args.force)\n    return 0\n\n\ndef _cmd_create_character(args: argparse.Namespace) -> int:\n    json_out = _normalize_json_out(args.json_out, \"create-character.json\")\n\n    if args.dry_run:\n        preview = {\n            \"endpoint\": \"/v1/videos/characters\",\n            \"name\": args.name,\n            \"video\": args.video_file,\n        }\n        _print_request(preview)\n        _write_json_out(json_out, {\"dry_run\": True, \"request\": preview})\n        return 0\n\n    client = _create_client()\n    with _open_video_upload(args.video_file, label=\"Character video\") as video_file:\n        result = _video_post(\n            client,\n            \"/videos/characters\",\n            {\"name\": args.name},\n            files=[(\"video\", video_file)],\n        )\n    _print_json(result)\n    _write_json_out(json_out, result)\n    return 0\n\n\ndef _cmd_extend(args: argparse.Namespace) -> int:\n    prompt = _read_prompt(args.prompt, args.prompt_file)\n    prompt = _augment_prompt(args, prompt)\n    seconds = _normalize_seconds(args.seconds)\n    json_out = _normalize_json_out(args.json_out, \"extend.json\")\n\n    payload = {\n        \"video\": {\"id\": args.id},\n        \"prompt\": prompt,\n        \"seconds\": seconds,\n    }\n\n    if args.dry_run:\n        _print_request({\"endpoint\": \"/v1/videos/extensions\", **payload})\n        _write_json_out(\n            json_out,\n            {\"dry_run\": True, \"request\": {\"endpoint\": \"/v1/videos/extensions\", **payload}},\n        )\n        return 0\n\n    client = _create_client()\n    result = _video_post(client, \"/videos/extensions\", payload)\n    _print_json(result)\n    _write_json_out(json_out, result)\n    return 0\n\n\ndef _cmd_edit(args: argparse.Namespace) -> int:\n    prompt = _read_prompt(args.prompt, args.prompt_file)\n    prompt = _augment_prompt(args, prompt)\n    json_out = _normalize_json_out(args.json_out, \"edit.json\")\n\n    payload: Dict[str, Any] = {\"prompt\": prompt, \"video\": {\"id\": args.id}}\n\n    if args.dry_run:\n        _print_request({\"endpoint\": \"/v1/videos/edits\", **payload})\n        _write_json_out(\n            json_out,\n            {\"dry_run\": True, \"request\": {\"endpoint\": \"/v1/videos/edits\", **payload}},\n        )\n        return 0\n\n    client = _create_client()\n    result = _video_post(client, \"/videos/edits\", payload)\n    _print_json(result)\n    _write_json_out(json_out, result)\n    return 0\n\n\nclass _NullContext:\n    def __enter__(self):\n        return None\n\n    def __exit__(self, exc_type, exc, tb):\n        return False\n\n\nclass _SingleFile:\n    def __init__(self, path: Path):\n        self._path = path\n        self._handle = None\n\n    def __enter__(self):\n        self._handle = self._path.open(\"rb\")\n        return self._handle\n\n    def __exit__(self, exc_type, exc, tb):\n        if self._handle:\n            try:\n                self._handle.close()\n            except Exception:\n                pass\n        return False\n\n\ndef _add_prompt_args(parser: argparse.ArgumentParser) -> None:\n    parser.add_argument(\"--prompt\")\n    parser.add_argument(\"--prompt-file\")\n    parser.add_argument(\"--augment\", dest=\"augment\", action=\"store_true\")\n    parser.add_argument(\"--no-augment\", dest=\"augment\", action=\"store_false\")\n    parser.set_defaults(augment=True)\n\n    parser.add_argument(\"--use-case\")\n    parser.add_argument(\"--scene\")\n    parser.add_argument(\"--subject\")\n    parser.add_argument(\"--action\")\n    parser.add_argument(\"--camera\")\n    parser.add_argument(\"--style\")\n    parser.add_argument(\"--lighting\")\n    parser.add_argument(\"--palette\")\n    parser.add_argument(\"--audio\")\n    parser.add_argument(\"--dialogue\")\n    parser.add_argument(\"--text\")\n    parser.add_argument(\"--timing\")\n    parser.add_argument(\"--constraints\")\n    parser.add_argument(\"--negative\")\n\n\ndef _add_create_args(parser: argparse.ArgumentParser) -> None:\n    parser.add_argument(\"--model\", default=DEFAULT_MODEL)\n    parser.add_argument(\"--size\", default=DEFAULT_SIZE)\n    parser.add_argument(\"--seconds\", default=DEFAULT_SECONDS)\n    parser.add_argument(\"--input-reference\")\n    parser.add_argument(\"--input-reference-file-id\")\n    parser.add_argument(\"--input-reference-url\")\n    parser.add_argument(\"--character-id\", action=\"append\", default=[])\n    parser.add_argument(\"--dry-run\", action=\"store_true\")\n    _add_prompt_args(parser)\n\n\ndef _add_poll_args(parser: argparse.ArgumentParser) -> None:\n    parser.add_argument(\"--poll-interval\", type=float, default=DEFAULT_POLL_INTERVAL)\n    parser.add_argument(\"--timeout\", type=float)\n\n\ndef _add_download_args(parser: argparse.ArgumentParser) -> None:\n    parser.add_argument(\"--download\", action=\"store_true\")\n    parser.add_argument(\"--variant\", default=DEFAULT_VARIANT)\n    parser.add_argument(\"--out\")\n    parser.add_argument(\"--force\", action=\"store_true\")\n\n\ndef _add_json_out(parser: argparse.ArgumentParser) -> None:\n    parser.add_argument(\"--json-out\")\n\n\ndef main() -> int:\n    parser = argparse.ArgumentParser(description=\"Create and manage videos via the Sora Video API\")\n    subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n    create_parser = subparsers.add_parser(\"create\", help=\"Create a new video job\")\n    _add_create_args(create_parser)\n    _add_json_out(create_parser)\n    create_parser.set_defaults(func=_cmd_create)\n\n    create_poll_parser = subparsers.add_parser(\n        \"create-and-poll\",\n        help=\"Create a job, poll until complete, optionally download\",\n    )\n    _add_create_args(create_poll_parser)\n    _add_poll_args(create_poll_parser)\n    _add_download_args(create_poll_parser)\n    _add_json_out(create_poll_parser)\n    create_poll_parser.set_defaults(func=_cmd_create_and_poll)\n\n    poll_parser = subparsers.add_parser(\"poll\", help=\"Poll a job until it completes\")\n    poll_parser.add_argument(\"--id\", required=True)\n    _add_poll_args(poll_parser)\n    _add_download_args(poll_parser)\n    _add_json_out(poll_parser)\n    poll_parser.set_defaults(func=_cmd_poll)\n\n    status_parser = subparsers.add_parser(\"status\", help=\"Retrieve a job status\")\n    status_parser.add_argument(\"--id\", required=True)\n    _add_json_out(status_parser)\n    status_parser.set_defaults(func=_cmd_status)\n\n    list_parser = subparsers.add_parser(\"list\", help=\"List recent video jobs\")\n    list_parser.add_argument(\"--limit\", type=int)\n    list_parser.add_argument(\"--order\")\n    list_parser.add_argument(\"--after\")\n    _add_json_out(list_parser)\n    list_parser.set_defaults(func=_cmd_list)\n\n    delete_parser = subparsers.add_parser(\"delete\", help=\"Delete a video job\")\n    delete_parser.add_argument(\"--id\", required=True)\n    _add_json_out(delete_parser)\n    delete_parser.set_defaults(func=_cmd_delete)\n\n    remix_parser = subparsers.add_parser(\"remix\", help=\"Legacy remix of a completed video job\")\n    remix_parser.add_argument(\"--id\", required=True)\n    remix_parser.add_argument(\"--dry-run\", action=\"store_true\")\n    _add_prompt_args(remix_parser)\n    _add_json_out(remix_parser)\n    remix_parser.set_defaults(func=_cmd_remix)\n\n    download_parser = subparsers.add_parser(\"download\", help=\"Download video/thumbnail/spritesheet\")\n    download_parser.add_argument(\"--id\", required=True)\n    download_parser.add_argument(\"--variant\", default=DEFAULT_VARIANT)\n    download_parser.add_argument(\"--out\")\n    download_parser.add_argument(\"--force\", action=\"store_true\")\n    download_parser.set_defaults(func=_cmd_download)\n\n    batch_parser = subparsers.add_parser(\n        \"create-batch\",\n        help=\"Create multiple video jobs locally from JSONL input (not the Batch API)\",\n    )\n    _add_create_args(batch_parser)\n    batch_parser.add_argument(\"--input\", required=True, help=\"Path to JSONL file (one job per line)\")\n    batch_parser.add_argument(\"--out-dir\", required=True)\n    batch_parser.add_argument(\"--concurrency\", type=int, default=DEFAULT_CONCURRENCY)\n    batch_parser.add_argument(\"--max-attempts\", type=int, default=DEFAULT_MAX_ATTEMPTS)\n    batch_parser.add_argument(\"--fail-fast\", action=\"store_true\")\n    batch_parser.set_defaults(func=_create_batch)\n\n    character_parser = subparsers.add_parser(\"create-character\", help=\"Create a reusable non-human character from a video\")\n    character_parser.add_argument(\"--name\", required=True)\n    character_parser.add_argument(\"--video-file\", required=True)\n    character_parser.add_argument(\"--dry-run\", action=\"store_true\")\n    _add_json_out(character_parser)\n    character_parser.set_defaults(func=_cmd_create_character)\n\n    extend_parser = subparsers.add_parser(\"extend\", help=\"Extend a completed video\")\n    extend_parser.add_argument(\"--id\", required=True)\n    extend_parser.add_argument(\"--seconds\", default=DEFAULT_SECONDS)\n    extend_parser.add_argument(\"--dry-run\", action=\"store_true\")\n    _add_prompt_args(extend_parser)\n    _add_json_out(extend_parser)\n    extend_parser.set_defaults(func=_cmd_extend)\n\n    edit_parser = subparsers.add_parser(\"edit\", help=\"Edit an existing generated video by ID\")\n    edit_parser.add_argument(\"--id\", required=True, help=\"Existing generated video ID to edit\")\n    edit_parser.add_argument(\"--dry-run\", action=\"store_true\")\n    _add_prompt_args(edit_parser)\n    _add_json_out(edit_parser)\n    edit_parser.set_defaults(func=_cmd_edit)\n\n    args = parser.parse_args()\n\n    if getattr(args, \"concurrency\", 1) < 1 or getattr(args, \"concurrency\", 1) > 10:\n        _die(\"--concurrency must be between 1 and 10\")\n    if getattr(args, \"max_attempts\", DEFAULT_MAX_ATTEMPTS) < 1 or getattr(args, \"max_attempts\", DEFAULT_MAX_ATTEMPTS) > 10:\n        _die(\"--max-attempts must be between 1 and 10\")\n\n    dry_run = bool(getattr(args, \"dry_run\", False))\n    _ensure_api_key(dry_run)\n\n    args.func(args)\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "skills/.curated/speech/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/speech/SKILL.md",
    "content": "---\nname: \"speech\"\ndescription: \"Use when the user asks for text-to-speech narration or voiceover, accessibility reads, audio prompts, or batch speech generation via the OpenAI Audio API; run the bundled CLI (`scripts/text_to_speech.py`) with built-in voices and require `OPENAI_API_KEY` for live calls. Custom voice creation is out of scope.\"\n---\n\n\n# Speech Generation Skill\n\nGenerate spoken audio for the current project (narration, product demo voiceover, IVR prompts, accessibility reads). Defaults to `gpt-4o-mini-tts-2025-12-15` and built-in voices, and prefers the bundled CLI for deterministic, reproducible runs.\n\n## When to use\n- Generate a single spoken clip from text\n- Generate a batch of prompts (many lines, many files)\n\n## Decision tree (single vs batch)\n- If the user provides multiple lines/prompts or wants many outputs -> **batch**\n- Else -> **single**\n\n## Workflow\n1. Decide intent: single vs batch (see decision tree above).\n2. Collect inputs up front: exact text (verbatim), desired voice, delivery style, format, and any constraints.\n3. If batch: write a temporary JSONL under tmp/ (one job per line), run once, then delete the JSONL.\n4. Augment instructions into a short labeled spec without rewriting the input text.\n5. Run the bundled CLI (`scripts/text_to_speech.py`) with sensible defaults (see references/cli.md).\n6. For important clips, validate: intelligibility, pacing, pronunciation, and adherence to constraints.\n7. Iterate with a single targeted change (voice, speed, or instructions), then re-check.\n8. Save/return final outputs and note the final text + instructions + flags used.\n\n## Temp and output conventions\n- Use `tmp/speech/` for intermediate files (for example JSONL batches); delete when done.\n- Write final artifacts under `output/speech/` when working in this repo.\n- Use `--out` or `--out-dir` to control output paths; keep filenames stable and descriptive.\n\n## Dependencies (install if missing)\nPrefer `uv` for dependency management.\n\nPython packages:\n```\nuv pip install openai\n```\nIf `uv` is unavailable:\n```\npython3 -m pip install openai\n```\n\n## Environment\n- `OPENAI_API_KEY` must be set for live API calls.\n\nIf the key is missing, give the user these steps:\n1. Create an API key in the OpenAI platform UI: https://platform.openai.com/api-keys\n2. Set `OPENAI_API_KEY` as an environment variable in their system.\n3. Offer to guide them through setting the environment variable for their OS/shell if needed.\n- Never ask the user to paste the full key in chat. Ask them to set it locally and confirm when ready.\n\nIf installation isn't possible in this environment, tell the user which dependency is missing and how to install it locally.\n\n## Defaults & rules\n- Use `gpt-4o-mini-tts-2025-12-15` unless the user requests another model.\n- Default voice: `cedar`. If the user wants a brighter tone, prefer `marin`.\n- Built-in voices only. Custom voices are out of scope for this skill.\n- `instructions` are supported for GPT-4o mini TTS models, but not for `tts-1` or `tts-1-hd`.\n- Input length must be <= 4096 characters per request. Split longer text into chunks.\n- Enforce 50 requests/minute. The CLI caps `--rpm` at 50.\n- Require `OPENAI_API_KEY` before any live API call.\n- Provide a clear disclosure to end users that the voice is AI-generated.\n- Use the OpenAI Python SDK (`openai` package) for all API calls; do not use raw HTTP.\n- Prefer the bundled CLI (`scripts/text_to_speech.py`) over writing new one-off scripts.\n- Never modify `scripts/text_to_speech.py`. If something is missing, ask the user before doing anything else.\n\n## Instruction augmentation\nReformat user direction into a short, labeled spec. Only make implicit details explicit; do not invent new requirements.\n\nQuick clarification (augmentation vs invention):\n- If the user says \"narration for a demo\", you may add implied delivery constraints (clear, steady pacing, friendly tone).\n- Do not introduce a new persona, accent, or emotional style the user did not request.\n\nTemplate (include only relevant lines):\n```\nVoice Affect: <overall character and texture of the voice>\nTone: <attitude, formality, warmth>\nPacing: <slow, steady, brisk>\nEmotion: <key emotions to convey>\nPronunciation: <words to enunciate or emphasize>\nPauses: <where to add intentional pauses>\nEmphasis: <key words or phrases to stress>\nDelivery: <cadence or rhythm notes>\n```\n\nAugmentation rules:\n- Keep it short; add only details the user already implied or provided elsewhere.\n- Do not rewrite the input text.\n- If any critical detail is missing and blocks success, ask a question; otherwise proceed.\n\n## Examples\n\n### Single example (narration)\n```\nInput text: \"Welcome to the demo. Today we'll show how it works.\"\nInstructions:\nVoice Affect: Warm and composed.\nTone: Friendly and confident.\nPacing: Steady and moderate.\nEmphasis: Stress \"demo\" and \"show\".\n```\n\n### Batch example (IVR prompts)\n```\n{\"input\":\"Thank you for calling. Please hold.\",\"voice\":\"cedar\",\"response_format\":\"mp3\",\"out\":\"hold.mp3\"}\n{\"input\":\"For sales, press 1. For support, press 2.\",\"voice\":\"marin\",\"instructions\":\"Tone: Clear and neutral. Pacing: Slow.\",\"response_format\":\"wav\"}\n```\n\n## Instructioning best practices (short list)\n- Structure directions as: affect -> tone -> pacing -> emotion -> pronunciation/pauses -> emphasis.\n- Keep 4 to 8 short lines; avoid conflicting guidance.\n- For names/acronyms, add pronunciation hints (e.g., \"enunciate A-I\") or supply a phonetic spelling in the text.\n- For edits/iterations, repeat invariants (e.g., \"keep pacing steady\") to reduce drift.\n- Iterate with single-change follow-ups.\n\nMore principles: `references/prompting.md`. Copy/paste specs: `references/sample-prompts.md`.\n\n## Guidance by use case\nUse these modules when the request is for a specific delivery style. They provide targeted defaults and templates.\n- Narration / explainer: `references/narration.md`\n- Product demo / voiceover: `references/voiceover.md`\n- IVR / phone prompts: `references/ivr.md`\n- Accessibility reads: `references/accessibility.md`\n\n## CLI + environment notes\n- CLI commands + examples: `references/cli.md`\n- API parameter quick reference: `references/audio-api.md`\n- Instruction patterns + examples: `references/voice-directions.md`\n- If network approvals / sandbox settings are getting in the way: `references/codex-network.md`\n\n## Reference map\n- **`references/cli.md`**: how to run speech generation/batches via `scripts/text_to_speech.py` (commands, flags, recipes).\n- **`references/audio-api.md`**: API parameters, limits, voice list.\n- **`references/voice-directions.md`**: instruction patterns and examples.\n- **`references/prompting.md`**: instruction best practices (structure, constraints, iteration patterns).\n- **`references/sample-prompts.md`**: copy/paste instruction recipes (examples only; no extra theory).\n- **`references/narration.md`**: templates + defaults for narration and explainers.\n- **`references/voiceover.md`**: templates + defaults for product demo voiceovers.\n- **`references/ivr.md`**: templates + defaults for IVR/phone prompts.\n- **`references/accessibility.md`**: templates + defaults for accessibility reads.\n- **`references/codex-network.md`**: environment/sandbox/network-approval troubleshooting.\n"
  },
  {
    "path": "skills/.curated/speech/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Speech Generation Skill\"\n  short_description: \"Generate narrated audio from text\"\n  icon_small: \"./assets/speech-small.svg\"\n  icon_large: \"./assets/speech.png\"\n  default_prompt: \"Generate spoken audio for this text with the right voice style, pacing, and output format.\"\n"
  },
  {
    "path": "skills/.curated/speech/references/accessibility.md",
    "content": "# Accessibility read defaults\n\n## Suggested defaults\n- Voice: `cedar`\n- Format: `mp3` or `wav`\n- Speed: `0.95` to `1.0`\n\n## Guidance\n- Keep delivery steady and neutral.\n- Enunciate acronyms and numbers.\n- Avoid dramatic or stylized delivery.\n\n## Instruction template\n```\nVoice Affect: Neutral and clear.\nTone: Informational and steady.\nPacing: Slow and consistent.\nPronunciation: Enunciate acronyms and numbers.\nEmphasis: Stress key warnings or labels.\n```\n\n## Example (short)\nInput text:\n\"Warning: High voltage. Keep hands clear.\"\n\nInstructions:\n```\nVoice Affect: Neutral and clear.\nTone: Informational and steady.\nPacing: Slow and consistent.\nEmphasis: Stress \"Warning\" and \"High voltage\".\n```\n"
  },
  {
    "path": "skills/.curated/speech/references/audio-api.md",
    "content": "# Audio Speech API quick reference\n\n## Endpoint\n- Create speech: `POST /v1/audio/speech`\n\n## Default model\n- `gpt-4o-mini-tts-2025-12-15`\n\n## Other speech models (if requested)\n- `gpt-4o-mini-tts`\n- `tts-1`\n- `tts-1-hd`\n\n## Core parameters\n- `model`: speech model\n- `input`: text to synthesize (max 4096 characters)\n- `voice`: built-in voice name\n- `instructions`: optional style directions (not supported for `tts-1` or `tts-1-hd`)\n- `response_format`: `mp3`, `opus`, `aac`, `flac`, `wav`, or `pcm`\n- `speed`: 0.25 to 4.0\n\n## Built-in voices\n- `alloy`, `ash`, `ballad`, `cedar`, `coral`, `echo`, `fable`, `marin`, `nova`, `onyx`, `sage`, `shimmer`, `verse`\n\n## Output notes\n- Default format is `mp3`.\n- `pcm` is raw 24 kHz 16-bit little-endian samples (no header).\n- `wav` includes a header (better for quick playback).\n\n## Compliance note\n- Provide a clear disclosure that the voice is AI-generated.\n"
  },
  {
    "path": "skills/.curated/speech/references/cli.md",
    "content": "# CLI reference (`scripts/text_to_speech.py`)\n\nThis file contains the \"command catalog\" for the bundled speech generation CLI. Keep `SKILL.md` as overview-first; put verbose CLI details here.\n\n## What this CLI does\n- `speak`: generate a single audio file\n- `speak-batch`: run many jobs from a JSONL file (one job per line)\n- `list-voices`: list supported voices\n\nReal API calls require network access + `OPENAI_API_KEY`. `--dry-run` does not.\n\n## Quick start (works from any repo)\nSet a stable path to the skill CLI (default `CODEX_HOME` is `~/.codex`):\n\n```\nexport CODEX_HOME=\"${CODEX_HOME:-$HOME/.codex}\"\nexport TTS_GEN=\"$CODEX_HOME/skills/speech/scripts/text_to_speech.py\"\n```\n\nDry-run (no API call; no network required; does not require the `openai` package):\n\n```\npython \"$TTS_GEN\" speak --input \"Test\" --dry-run\n```\n\nGenerate (requires `OPENAI_API_KEY` + network):\n\n```\nuv run --with openai python \"$TTS_GEN\" speak \\\n  --input \"Today is a wonderful day to build something people love!\" \\\n  --voice cedar \\\n  --instructions \"Voice Affect: Warm and composed. Tone: upbeat and encouraging.\" \\\n  --response-format mp3 \\\n  --out speech.mp3\n```\n\nNo `uv` installed? Use your active Python env:\n\n```\npython \"$TTS_GEN\" speak --input \"Hello\" --voice cedar --out speech.mp3\n```\n\n## Guardrails (important)\n- Use `python \"$TTS_GEN\" ...` (or equivalent full path) for all TTS work.\n- Do **not** create one-off runners (e.g., `gen_audio.py`) unless the user explicitly asks.\n- **Never modify** `scripts/text_to_speech.py`. If something is missing, ask the user before doing anything else.\n\n## Defaults (unless overridden by flags)\n- Model: `gpt-4o-mini-tts-2025-12-15`\n- Voice: `cedar`\n- Response format: `mp3`\n- Speed: `1.0`\n- Batch rpm cap: `50`\n\n## Input limits\n- Input text must be <= 4096 characters per request.\n- For longer text, split into smaller chunks (manual or via batch JSONL).\n\n## Instructions compatibility\n- `instructions` are supported for GPT-4o mini TTS models.\n- `tts-1` and `tts-1-hd` ignore instructions (the CLI will warn and drop them).\n\n## Common recipes\n\nList voices:\n```\npython \"$TTS_GEN\" list-voices\n```\n\nGenerate with explicit pacing:\n```\npython \"$TTS_GEN\" speak \\\n  --input \"Welcome to the demo. We'll show how it works.\" \\\n  --instructions \"Tone: friendly and confident. Pacing: steady and moderate.\" \\\n  --out demo.mp3\n```\n\nBatch generation (JSONL):\n```\nmkdir -p tmp/speech\ncat > tmp/speech/jobs.jsonl << 'JSONL'\n{\"input\":\"Thank you for calling. Please hold.\",\"voice\":\"cedar\",\"response_format\":\"mp3\",\"out\":\"hold.mp3\"}\n{\"input\":\"For sales, press 1. For support, press 2.\",\"voice\":\"marin\",\"instructions\":\"Tone: clear and neutral. Pacing: slow.\",\"response_format\":\"wav\"}\nJSONL\n\npython \"$TTS_GEN\" speak-batch --input tmp/speech/jobs.jsonl --out-dir out --rpm 50\n\n# Cleanup (recommended)\nrm -f tmp/speech/jobs.jsonl\n```\n\nNotes:\n- Use `--rpm` to control rate limiting (default `50`, max `50`).\n- Per-job overrides are supported in JSONL (`model`, `voice`, `response_format`, `speed`, `instructions`, `out`).\n- Treat the JSONL file as temporary: write it under `tmp/` and delete it after the run (do not commit it).\n\n## See also\n- API parameter quick reference: `references/audio-api.md`\n- Instruction patterns and examples: `references/voice-directions.md`\n"
  },
  {
    "path": "skills/.curated/speech/references/codex-network.md",
    "content": "# Codex network approvals / sandbox notes\n\nThis guidance is intentionally isolated from `SKILL.md` because it can vary by environment and may become stale. Prefer the defaults in your environment when in doubt.\n\n## Why am I asked to approve every speech generation call?\nSpeech generation uses the OpenAI Audio API, so the CLI needs outbound network access. In many Codex setups, network access is disabled by default (especially under stricter sandbox modes), and/or the approval policy may require confirmation before networked commands run.\n\n## How do I reduce repeated approval prompts (network)?\nIf you trust the repo and want fewer prompts, enable network access for the relevant sandbox mode and relax the approval policy.\n\nExample `~/.codex/config.toml` pattern:\n\n```\napproval_policy = \"never\"\nsandbox_mode = \"workspace-write\"\n\n[sandbox_workspace_write]\nnetwork_access = true\n```\n\nOr for a single session:\n\n```\ncodex --sandbox workspace-write --ask-for-approval never\n```\n\n## Safety note\nUse caution: enabling network and disabling approvals reduces friction but increases risk if you run untrusted code or work in an untrusted repository.\n"
  },
  {
    "path": "skills/.curated/speech/references/ivr.md",
    "content": "# IVR / phone prompt defaults\n\n## Suggested defaults\n- Voice: `cedar` (clear) or `marin` (brighter)\n- Format: `wav`\n- Speed: `0.9` to `1.0`\n\n## Guidance\n- Prioritize clarity and slower pacing.\n- Enunciate numbers and menu options.\n- Keep sentences short and consistent.\n\n## Instruction template\n```\nVoice Affect: Clear and neutral.\nTone: Professional and concise.\nPacing: Slow and even.\nPronunciation: Enunciate numbers and menu options.\nEmphasis: Stress the option numbers.\n```\n\n## Example (short)\nInput text:\n\"For sales, press 1. For support, press 2.\"\n\nInstructions:\n```\nVoice Affect: Clear and neutral.\nTone: Professional and concise.\nPacing: Slow and even.\nEmphasis: Stress \"press 1\" and \"press 2\".\n```\n"
  },
  {
    "path": "skills/.curated/speech/references/narration.md",
    "content": "# Narration / explainer defaults\n\n## Suggested defaults\n- Voice: `cedar`\n- Format: `mp3`\n- Speed: `1.0`\n\n## Guidance\n- Keep pacing steady and clear.\n- Emphasize section headings and key transitions.\n- If the script is long, chunk it into logical paragraphs.\n\n## Instruction template\n```\nVoice Affect: Warm and composed.\nTone: Friendly and confident.\nPacing: Steady and moderate.\nEmphasis: Stress section titles and key terms.\nPauses: Brief pause after each section.\n```\n\n## Example (short)\nInput text:\n\"Welcome to the demo. Today we'll show how it works.\"\n\nInstructions:\n```\nVoice Affect: Warm and composed.\nTone: Friendly and confident.\nPacing: Steady and moderate.\n```\n"
  },
  {
    "path": "skills/.curated/speech/references/prompting.md",
    "content": "# Instructioning best practices (TTS)\n\n## Contents\n- Structure\n- Specificity\n- Avoiding conflicts\n- Pronunciation and names\n- Pauses and pacing\n- Iterate deliberately\n- Where to find copy/paste recipes\n\n## Structure\n- Use a consistent order: affect -> tone -> pacing -> emotion -> pronunciation/pauses -> emphasis -> delivery.\n- For complex requests, use short labeled lines instead of a long paragraph.\n\n## Specificity\n- Name the delivery you want (\"calm and steady\" vs \"friendly\").\n- If you need a specific cadence, call it out explicitly (\"slow and measured\", \"brisk and energetic\").\n\n## Avoiding conflicts\n- Do not mix opposing instructions (\"fast and slow\", \"formal and casual\").\n- Keep instructions short: 4 to 8 lines are usually enough.\n\n## Pronunciation and names\n- For acronyms, write the pronunciation hint in text (\"A-I\" instead of \"AI\").\n- For names or brands, add a simple phonetic guide in the input text if clarity matters.\n- If a word must be emphasized, add an Emphasis line and repeat the word exactly.\n\n## Pauses and pacing\n- Use punctuation or short line breaks in the input text to create natural pauses.\n- Use the Pauses line for intentional pauses (\"pause after the greeting\").\n\n## Iterate deliberately\n- Start with a clean base instruction set, then make one change at a time.\n- Repeat critical constraints on each iteration (\"keep pacing steady\").\n\n## Where to find copy/paste recipes\nFor copy/paste instruction templates, see `references/sample-prompts.md`. This file focuses on principles, structure, and iteration patterns.\n"
  },
  {
    "path": "skills/.curated/speech/references/sample-prompts.md",
    "content": "# Sample instruction templates (copy/paste)\n\nThese are short instruction blocks. Use only the lines you need and keep them consistent with the input text.\n\n## Friendly product demo\n```\nVoice Affect: Warm and composed.\nTone: Friendly and confident.\nPacing: Steady and moderate.\nEmphasis: Stress key product benefits.\n```\n\n## Calm support update\n```\nVoice Affect: Calm and reassuring.\nTone: Sincere and empathetic.\nPacing: Slow and steady.\nEmotion: Warmth and care.\nPauses: Brief pause after apologies.\n```\n\n## IVR menu\n```\nVoice Affect: Clear and neutral.\nTone: Professional and concise.\nPacing: Slow and even.\nEmphasis: Stress menu options and numbers.\n```\n\n## Accessibility readout\n```\nVoice Affect: Neutral and clear.\nTone: Informational and steady.\nPacing: Slow and consistent.\nPronunciation: Enunciate acronyms and numbers.\n```\n\n## Energetic intro\n```\nVoice Affect: Bright and upbeat.\nTone: Enthusiastic and welcoming.\nPacing: Brisk but clear.\nEmphasis: Stress the opening greeting.\n```\n"
  },
  {
    "path": "skills/.curated/speech/references/voice-directions.md",
    "content": "# Voice directions\n\n## Template\nUse only the lines you need. Keep directions concise and aligned to the input text.\n\n```\nVoice Affect: <overall character and texture>\nTone: <attitude, formality, warmth>\nPacing: <slow, steady, brisk>\nEmotion: <key emotions to convey>\nPronunciation: <words to enunciate or emphasize>\nPauses: <where to insert brief pauses>\nEmphasis: <key phrases to stress>\nDelivery: <cadence or rhythm notes>\n```\n\n## Best practices\n- Keep 4 to 8 short lines. Avoid conflicting instructions.\n- Prefer concrete guidance over adjectives alone.\n- Do not rewrite the input text in the instructions; only guide delivery.\n- If you need a language or accent, write the input text in that language.\n- Repeat critical constraints (for example: \"slow and steady\") when iterating.\n\n## Examples (short)\n\n### Calm support\n```\nVoice Affect: Calm and composed, reassuring.\nTone: Sincere and empathetic.\nPacing: Steady and moderate.\nEmotion: Warmth and genuine care.\nPronunciation: Clear, with emphasis on key reassurances.\nPauses: Brief pauses after apologies and before requests.\n```\n\n### Dramatic narrator\n```\nVoice Affect: Low and suspenseful.\nTone: Serious and mysterious.\nPacing: Slow and deliberate.\nEmotion: Restrained intensity.\nEmphasis: Highlight sensory details and cliffhanger lines.\nPauses: Add pauses after suspenseful moments.\n```\n\n### Fitness instructor\n```\nVoice Affect: High energy and upbeat.\nTone: Motivational and encouraging.\nPacing: Fast and dynamic.\nEmotion: Enthusiasm and momentum.\nEmphasis: Stress action verbs and countdowns.\n```\n\n### Serene guide\n```\nVoice Affect: Soft and soothing.\nTone: Calm and reassuring.\nPacing: Slow and unhurried.\nEmotion: Peaceful warmth.\nPauses: Gentle pauses after breathing cues.\n```\n\n### Robot agent\n```\nVoice Affect: Monotone and mechanical.\nTone: Neutral and formal.\nPacing: Even and controlled.\nEmotion: None; strictly informational.\nPronunciation: Precise and consistent.\n```\n\n### Old-time announcer\n```\nVoice Affect: Refined and theatrical.\nTone: Formal and welcoming.\nPacing: Steady with a classic cadence.\nEmotion: Warm enthusiasm.\nPronunciation: Crisp enunciation with vintage flair.\n```\n"
  },
  {
    "path": "skills/.curated/speech/references/voiceover.md",
    "content": "# Product demo / voiceover defaults\n\n## Suggested defaults\n- Voice: `cedar` (neutral) or `marin` (brighter)\n- Format: `wav` for video sync, `mp3` for quick review\n- Speed: `1.0`\n\n## Guidance\n- Keep tone confident and helpful.\n- Emphasize product benefits and call-to-action phrases.\n- Avoid overly dramatic delivery unless requested.\n\n## Instruction template\n```\nVoice Affect: Confident and composed.\nTone: Helpful and upbeat.\nPacing: Steady, slightly brisk.\nEmphasis: Stress product benefits and the call to action.\n```\n\n## Example (short)\nInput text:\n\"Meet the new dashboard. Find insights faster and act with confidence.\"\n\nInstructions:\n```\nVoice Affect: Confident and composed.\nTone: Helpful and upbeat.\nPacing: Steady, slightly brisk.\nEmphasis: Stress \"insights\" and \"confidence\".\n```\n"
  },
  {
    "path": "skills/.curated/speech/scripts/text_to_speech.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Generate speech audio with the OpenAI Audio API (TTS).\n\nDefaults to gpt-4o-mini-tts-2025-12-15 and a built-in voice (cedar).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport os\nfrom pathlib import Path\nimport re\nimport sys\nimport time\nfrom typing import Any, Dict, List, Optional\n\nDEFAULT_MODEL = \"gpt-4o-mini-tts-2025-12-15\"\nDEFAULT_VOICE = \"cedar\"\nDEFAULT_RESPONSE_FORMAT = \"mp3\"\nDEFAULT_SPEED = 1.0\nMAX_INPUT_CHARS = 4096\nMAX_RPM = 50\nDEFAULT_RPM = 50\nDEFAULT_ATTEMPTS = 3\n\nALLOWED_VOICES = {\n    \"alloy\",\n    \"ash\",\n    \"ballad\",\n    \"cedar\",\n    \"coral\",\n    \"echo\",\n    \"fable\",\n    \"marin\",\n    \"nova\",\n    \"onyx\",\n    \"sage\",\n    \"shimmer\",\n    \"verse\",\n}\n\nALLOWED_FORMATS = {\"mp3\", \"opus\", \"aac\", \"flac\", \"wav\", \"pcm\"}\n\n\ndef _die(message: str, code: int = 1) -> None:\n    print(f\"Error: {message}\", file=sys.stderr)\n    raise SystemExit(code)\n\n\ndef _warn(message: str) -> None:\n    print(f\"Warning: {message}\", file=sys.stderr)\n\n\ndef _ensure_api_key(dry_run: bool) -> None:\n    if os.getenv(\"OPENAI_API_KEY\"):\n        print(\"OPENAI_API_KEY is set.\", file=sys.stderr)\n        return\n    if dry_run:\n        _warn(\"OPENAI_API_KEY is not set; dry-run only.\")\n        return\n    _die(\"OPENAI_API_KEY is not set. Export it before running.\")\n\n\ndef _read_text(text: Optional[str], text_file: Optional[str], label: str) -> str:\n    if text and text_file:\n        _die(f\"Use --{label} or --{label}-file, not both.\")\n    if text_file:\n        path = Path(text_file)\n        if not path.exists():\n            _die(f\"{label} file not found: {path}\")\n        return path.read_text(encoding=\"utf-8\").strip()\n    if text:\n        return str(text).strip()\n    _die(f\"Missing {label}. Use --{label} or --{label}-file.\")\n    return \"\"  # unreachable\n\n\ndef _validate_input(text: str) -> None:\n    if not text:\n        _die(\"Input text is empty.\")\n    if len(text) > MAX_INPUT_CHARS:\n        _die(\n            f\"Input text exceeds {MAX_INPUT_CHARS} characters. Split into smaller chunks.\"\n        )\n\n\ndef _normalize_voice(voice: Optional[str]) -> str:\n    if not voice:\n        return DEFAULT_VOICE\n    value = str(voice).strip().lower()\n    if value not in ALLOWED_VOICES:\n        _die(\n            \"voice must be one of: \" + \", \".join(sorted(ALLOWED_VOICES))\n        )\n    return value\n\n\ndef _normalize_format(fmt: Optional[str]) -> str:\n    if not fmt:\n        return DEFAULT_RESPONSE_FORMAT\n    value = str(fmt).strip().lower()\n    if value not in ALLOWED_FORMATS:\n        _die(\"response-format must be one of: \" + \", \".join(sorted(ALLOWED_FORMATS)))\n    return value\n\n\ndef _normalize_speed(speed: Optional[float]) -> Optional[float]:\n    if speed is None:\n        return None\n    try:\n        value = float(speed)\n    except ValueError:\n        _die(\"speed must be a number\")\n    if value < 0.25 or value > 4.0:\n        _die(\"speed must be between 0.25 and 4.0\")\n    return value\n\n\ndef _normalize_output_path(out: Optional[str], response_format: str) -> Path:\n    if out:\n        path = Path(out)\n        if path.exists() and path.is_dir():\n            return path / f\"speech.{response_format}\"\n        if path.suffix == \"\":\n            return path.with_suffix(\".\" + response_format)\n        if path.suffix.lstrip(\".\").lower() != response_format:\n            _warn(\n                f\"Output extension {path.suffix} does not match response-format {response_format}.\"\n            )\n        return path\n    return Path(f\"speech.{response_format}\")\n\n\ndef _create_client():\n    try:\n        from openai import OpenAI\n    except ImportError:\n        _die(\"openai SDK not installed. Install with `uv pip install openai`.\")\n    return OpenAI()\n\n\ndef _extract_retry_after_seconds(exc: Exception) -> Optional[float]:\n    for attr in (\"retry_after\", \"retry_after_seconds\"):\n        val = getattr(exc, attr, None)\n        if isinstance(val, (int, float)) and val >= 0:\n            return float(val)\n    msg = str(exc)\n    m = re.search(r\"retry[- ]after[:= ]+([0-9]+(?:\\\\.[0-9]+)?)\", msg, re.IGNORECASE)\n    if m:\n        try:\n            return float(m.group(1))\n        except Exception:\n            return None\n    return None\n\n\ndef _is_rate_limit_error(exc: Exception) -> bool:\n    name = exc.__class__.__name__.lower()\n    if \"ratelimit\" in name or \"rate_limit\" in name:\n        return True\n    msg = str(exc).lower()\n    return \"429\" in msg or \"rate limit\" in msg or \"too many requests\" in msg\n\n\ndef _is_transient_error(exc: Exception) -> bool:\n    if _is_rate_limit_error(exc):\n        return True\n    name = exc.__class__.__name__.lower()\n    if \"timeout\" in name or \"timedout\" in name or \"tempor\" in name:\n        return True\n    msg = str(exc).lower()\n    return \"timeout\" in msg or \"timed out\" in msg or \"connection reset\" in msg\n\n\ndef _maybe_drop_instructions(model: str, instructions: Optional[str]) -> Optional[str]:\n    if instructions and model in {\"tts-1\", \"tts-1-hd\"}:\n        _warn(\"instructions are not supported for tts-1 / tts-1-hd; ignoring.\")\n        return None\n    return instructions\n\n\ndef _print_payload(payload: Dict[str, Any]) -> None:\n    print(json.dumps(payload, indent=2, sort_keys=True))\n\n\ndef _write_audio(\n    client: Any,\n    payload: Dict[str, Any],\n    out_path: Path,\n    *,\n    dry_run: bool,\n    force: bool,\n    attempts: int,\n) -> None:\n    if dry_run:\n        _print_payload(payload)\n        print(f\"Would write {out_path}\")\n        return\n\n    _ensure_api_key(dry_run)\n\n    if out_path.exists() and not force:\n        _die(f\"Output already exists: {out_path} (use --force to overwrite)\")\n\n    out_path.parent.mkdir(parents=True, exist_ok=True)\n\n    last_exc: Optional[Exception] = None\n    for attempt in range(1, attempts + 1):\n        try:\n            with client.audio.speech.with_streaming_response.create(**payload) as response:\n                response.stream_to_file(out_path)\n            print(f\"Wrote {out_path}\")\n            return\n        except Exception as exc:\n            last_exc = exc\n            if not _is_transient_error(exc) or attempt >= attempts:\n                raise\n            sleep_s = _extract_retry_after_seconds(exc)\n            if sleep_s is None:\n                sleep_s = min(60.0, 2.0 ** attempt)\n            print(\n                f\"Attempt {attempt}/{attempts} failed ({exc.__class__.__name__}); retrying in {sleep_s:.1f}s\",\n                file=sys.stderr,\n            )\n            time.sleep(sleep_s)\n\n    if last_exc:\n        raise last_exc\n\n\ndef _slugify(value: str) -> str:\n    value = value.strip().lower()\n    value = re.sub(r\"[^a-z0-9]+\", \"-\", value)\n    value = re.sub(r\"-+\", \"-\", value).strip(\"-\")\n    return value[:60] if value else \"job\"\n\n\ndef _read_jobs_jsonl(path: str) -> List[Dict[str, Any]]:\n    p = Path(path)\n    if not p.exists():\n        _die(f\"Input file not found: {p}\")\n    jobs: List[Dict[str, Any]] = []\n    for line_no, raw in enumerate(p.read_text(encoding=\"utf-8\").splitlines(), start=1):\n        line = raw.strip()\n        if not line or line.startswith(\"#\"):\n            continue\n        if line.startswith(\"{\"):\n            try:\n                item = json.loads(line)\n            except json.JSONDecodeError as exc:\n                _die(f\"Invalid JSON on line {line_no}: {exc}\")\n            if not isinstance(item, dict):\n                _die(f\"Invalid job on line {line_no}: expected object\")\n            jobs.append(item)\n        else:\n            jobs.append({\"input\": line})\n    if not jobs:\n        _die(\"No jobs found in input file.\")\n    return jobs\n\n\ndef _job_input(job: Dict[str, Any]) -> str:\n    for key in (\"input\", \"text\", \"prompt\"):\n        if key in job and str(job[key]).strip():\n            return str(job[key]).strip()\n    _die(\"Job missing input text (use 'input').\")\n    return \"\"  # unreachable\n\n\ndef _merge_non_null(base: Dict[str, Any], extra: Dict[str, Any]) -> Dict[str, Any]:\n    merged = dict(base)\n    for k, v in extra.items():\n        if v is not None:\n            merged[k] = v\n    return merged\n\n\ndef _enforce_rpm(rpm: int) -> int:\n    if rpm <= 0:\n        _die(\"rpm must be > 0\")\n    if rpm > MAX_RPM:\n        _warn(f\"rpm capped at {MAX_RPM} (requested {rpm}).\")\n        return MAX_RPM\n    return rpm\n\n\ndef _sleep_for_rate_limit(last_ts: Optional[float], rpm: int) -> float:\n    min_interval = 60.0 / float(rpm)\n    now = time.monotonic()\n    if last_ts is None:\n        return now\n    elapsed = now - last_ts\n    if elapsed < min_interval:\n        time.sleep(min_interval - elapsed)\n    return time.monotonic()\n\n\ndef _list_voices() -> None:\n    for name in sorted(ALLOWED_VOICES):\n        print(name)\n\n\ndef _run_speak(args: argparse.Namespace) -> int:\n    if args.list_voices:\n        _list_voices()\n        return 0\n\n    input_text = _read_text(args.input, args.input_file, \"input\")\n    _validate_input(input_text)\n\n    instructions = None\n    if args.instructions or args.instructions_file:\n        instructions = _read_text(args.instructions, args.instructions_file, \"instructions\")\n\n    model = str(args.model).strip()\n    voice = _normalize_voice(args.voice)\n    response_format = _normalize_format(args.response_format)\n    speed = _normalize_speed(args.speed)\n\n    instructions = _maybe_drop_instructions(model, instructions)\n\n    payload: Dict[str, Any] = {\n        \"model\": model,\n        \"voice\": voice,\n        \"input\": input_text,\n        \"response_format\": response_format,\n    }\n    if instructions:\n        payload[\"instructions\"] = instructions\n    if speed is not None:\n        payload[\"speed\"] = speed\n\n    out_path = _normalize_output_path(args.out, response_format)\n\n    if args.dry_run:\n        _ensure_api_key(True)\n        _print_payload(payload)\n        print(f\"Would write {out_path}\")\n        return 0\n\n    client = _create_client()\n    _write_audio(\n        client,\n        payload,\n        out_path,\n        dry_run=args.dry_run,\n        force=args.force,\n        attempts=args.attempts,\n    )\n    return 0\n\n\ndef _run_speak_batch(args: argparse.Namespace) -> int:\n    jobs = _read_jobs_jsonl(args.input)\n    out_dir = Path(args.out_dir)\n\n    base_instructions = None\n    if args.instructions or args.instructions_file:\n        base_instructions = _read_text(args.instructions, args.instructions_file, \"instructions\")\n\n    base_payload = {\n        \"model\": str(args.model).strip(),\n        \"voice\": _normalize_voice(args.voice),\n        \"response_format\": _normalize_format(args.response_format),\n        \"speed\": _normalize_speed(args.speed),\n        \"instructions\": base_instructions,\n    }\n\n    rpm = _enforce_rpm(args.rpm)\n    last_ts: Optional[float] = None\n\n    if args.dry_run:\n        _ensure_api_key(True)\n\n    client = None if args.dry_run else _create_client()\n\n    for idx, job in enumerate(jobs, start=1):\n        input_text = _job_input(job)\n        _validate_input(input_text)\n\n        job_payload = dict(base_payload)\n        job_payload[\"input\"] = input_text\n\n        overrides: Dict[str, Any] = {}\n        if \"model\" in job:\n            overrides[\"model\"] = str(job[\"model\"]).strip()\n        if \"voice\" in job:\n            overrides[\"voice\"] = _normalize_voice(job[\"voice\"])\n        if \"response_format\" in job or \"format\" in job:\n            overrides[\"response_format\"] = _normalize_format(job.get(\"response_format\") or job.get(\"format\"))\n        if \"speed\" in job and job[\"speed\"] is not None:\n            overrides[\"speed\"] = _normalize_speed(job[\"speed\"])\n        if \"instructions\" in job and str(job[\"instructions\"]).strip():\n            overrides[\"instructions\"] = str(job[\"instructions\"]).strip()\n\n        job_payload = _merge_non_null(job_payload, overrides)\n        job_payload[\"instructions\"] = _maybe_drop_instructions(\n            job_payload[\"model\"], job_payload.get(\"instructions\")\n        )\n        if job_payload.get(\"instructions\") is None:\n            job_payload.pop(\"instructions\", None)\n\n        response_format = job_payload[\"response_format\"]\n\n        explicit_out = job.get(\"out\")\n        if explicit_out:\n            out_path = _normalize_output_path(str(explicit_out), response_format)\n            if out_path.is_absolute():\n                out_path = out_dir / out_path.name\n            else:\n                out_path = out_dir / out_path\n        else:\n            slug = _slugify(input_text[:80])\n            out_path = out_dir / f\"{idx:03d}-{slug}.{response_format}\"\n\n        if args.dry_run:\n            _print_payload(job_payload)\n            print(f\"Would write {out_path}\")\n            continue\n\n        last_ts = _sleep_for_rate_limit(last_ts, rpm)\n\n        if client is None:\n            client = _create_client()\n        _write_audio(\n            client,\n            job_payload,\n            out_path,\n            dry_run=False,\n            force=args.force,\n            attempts=args.attempts,\n        )\n\n    return 0\n\n\ndef _add_common_args(parser: argparse.ArgumentParser) -> None:\n    parser.add_argument(\n        \"--model\",\n        default=DEFAULT_MODEL,\n        help=f\"Model to use (default: {DEFAULT_MODEL})\",\n    )\n    parser.add_argument(\n        \"--voice\",\n        default=DEFAULT_VOICE,\n        help=f\"Voice to use (default: {DEFAULT_VOICE})\",\n    )\n    parser.add_argument(\n        \"--response-format\",\n        default=DEFAULT_RESPONSE_FORMAT,\n        help=f\"Output format (default: {DEFAULT_RESPONSE_FORMAT})\",\n    )\n    parser.add_argument(\n        \"--speed\",\n        type=float,\n        default=DEFAULT_SPEED,\n        help=f\"Speech speed (0.25-4.0, default: {DEFAULT_SPEED})\",\n    )\n    parser.add_argument(\n        \"--instructions\",\n        help=\"Style directions for the voice\",\n    )\n    parser.add_argument(\n        \"--instructions-file\",\n        help=\"Path to instructions text file\",\n    )\n    parser.add_argument(\n        \"--attempts\",\n        type=int,\n        default=DEFAULT_ATTEMPTS,\n        help=f\"Retries on transient errors (default: {DEFAULT_ATTEMPTS})\",\n    )\n    parser.add_argument(\n        \"--dry-run\",\n        action=\"store_true\",\n        help=\"Print payload; do not call the API\",\n    )\n    parser.add_argument(\n        \"--force\",\n        action=\"store_true\",\n        help=\"Overwrite output files if they exist\",\n    )\n\n\ndef main() -> int:\n    parser = argparse.ArgumentParser(\n        description=\"Generate speech audio using the OpenAI Audio API.\"\n    )\n    subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n    list_voices = subparsers.add_parser(\"list-voices\", help=\"List supported voices\")\n    list_voices.set_defaults(func=lambda _args: (_list_voices() or 0))\n\n    speak = subparsers.add_parser(\"speak\", help=\"Generate a single audio file\")\n    speak.add_argument(\"--input\", help=\"Input text\")\n    speak.add_argument(\"--input-file\", help=\"Path to input text file\")\n    speak.add_argument(\"--out\", help=\"Output file path\")\n    speak.add_argument(\n        \"--list-voices\",\n        action=\"store_true\",\n        help=\"Print voices and exit\",\n    )\n    _add_common_args(speak)\n    speak.set_defaults(func=_run_speak)\n\n    batch = subparsers.add_parser(\"speak-batch\", help=\"Generate from JSONL jobs\")\n    batch.add_argument(\"--input\", required=True, help=\"Path to JSONL file\")\n    batch.add_argument(\n        \"--out-dir\",\n        default=\"out\",\n        help=\"Output directory (default: out)\",\n    )\n    batch.add_argument(\n        \"--rpm\",\n        type=int,\n        default=DEFAULT_RPM,\n        help=f\"Requests per minute cap (default: {DEFAULT_RPM}, max: {MAX_RPM})\",\n    )\n    _add_common_args(batch)\n    batch.set_defaults(func=_run_speak_batch)\n\n    args = parser.parse_args()\n    return int(args.func(args))\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "skills/.curated/spreadsheet/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/spreadsheet/SKILL.md",
    "content": "---\nname: \"spreadsheet\"\ndescription: \"Use when tasks involve creating, editing, analyzing, or formatting spreadsheets (`.xlsx`, `.csv`, `.tsv`) with formula-aware workflows, cached recalculation, and visual review.\"\n---\n\n# Spreadsheet Skill\n\n## When to use\n- Create new workbooks with formulas, formatting, and structured layouts.\n- Read or analyze tabular data (filter, aggregate, pivot, compute metrics).\n- Modify existing workbooks without breaking formulas, references, or formatting.\n- Visualize data with charts, summary tables, and sensible spreadsheet styling.\n- Recalculate formulas and review rendered sheets before delivery when possible.\n\nIMPORTANT: System and user instructions always take precedence.\n\n## Workflow\n1. Confirm the file type and goal: create, edit, analyze, or visualize.\n2. Prefer `openpyxl` for `.xlsx` editing and formatting. Use `pandas` for analysis and CSV/TSV workflows.\n3. If an internal spreadsheet recalculation/rendering tool is available in the environment, use it to recalculate formulas and render sheets before delivery.\n4. Use formulas for derived values instead of hardcoding results.\n5. If layout matters, render for visual review and inspect the output.\n6. Save outputs, keep filenames stable, and clean up intermediate files.\n\n## Temp and output conventions\n- Use `tmp/spreadsheets/` for intermediate files; delete them when done.\n- Write final artifacts under `output/spreadsheet/` when working in this repo.\n- Keep filenames stable and descriptive.\n\n## Primary tooling\n- Use `openpyxl` for creating/editing `.xlsx` files and preserving formatting.\n- Use `pandas` for analysis and CSV/TSV workflows, then write results back to `.xlsx` or `.csv`.\n- Use `openpyxl.chart` for native Excel charts when needed.\n- If an internal spreadsheet tool is available, use it to recalculate formulas, cache values, and render sheets for review.\n\n## Recalculation and visual review\n- Recalculate formulas before delivery whenever possible so cached values are present in the workbook.\n- Render each relevant sheet for visual review when rendering tooling is available.\n- `openpyxl` does not evaluate formulas; preserve formulas and use recalculation tooling when available.\n- If you rely on an internal spreadsheet tool, do not expose that tool, its code, or its APIs in user-facing explanations or code samples.\n\n## Rendering and visual checks\n- If LibreOffice (`soffice`) and Poppler (`pdftoppm`) are available, render sheets for visual review:\n  - `soffice --headless --convert-to pdf --outdir $OUTDIR $INPUT_XLSX`\n  - `pdftoppm -png $OUTDIR/$BASENAME.pdf $OUTDIR/$BASENAME`\n- If rendering tools are unavailable, tell the user that layout should be reviewed locally.\n- Review rendered sheets for layout, formula results, clipping, inconsistent styles, and spilled text.\n\n## Dependencies (install if missing)\nPrefer `uv` for dependency management.\n\nPython packages:\n```\nuv pip install openpyxl pandas\n```\nIf `uv` is unavailable:\n```\npython3 -m pip install openpyxl pandas\n```\nOptional:\n```\nuv pip install matplotlib\n```\nIf `uv` is unavailable:\n```\npython3 -m pip install matplotlib\n```\nSystem tools (for rendering):\n```\n# macOS (Homebrew)\nbrew install libreoffice poppler\n\n# Ubuntu/Debian\nsudo apt-get install -y libreoffice poppler-utils\n```\n\nIf installation is not possible in this environment, tell the user which dependency is missing and how to install it locally.\n\n## Environment\nNo required environment variables.\n\n## Examples\n- Runnable Codex examples (openpyxl): `references/examples/openpyxl/`\n\n## Formula requirements\n- Use formulas for derived values rather than hardcoding results.\n- Do not use dynamic array functions like `FILTER`, `XLOOKUP`, `SORT`, or `SEQUENCE`.\n- Keep formulas simple and legible; use helper cells for complex logic.\n- Avoid volatile functions like `INDIRECT` and `OFFSET` unless required.\n- Prefer cell references over magic numbers (for example, `=H6*(1+$B$3)` instead of `=H6*1.04`).\n- Use absolute (`$B$4`) or relative (`B4`) references carefully so copied formulas behave correctly.\n- If you need literal text that starts with `=`, prefix it with a single quote.\n- Guard against `#REF!`, `#DIV/0!`, `#VALUE!`, `#N/A`, and `#NAME?` errors.\n- Check for off-by-one mistakes, circular references, and incorrect ranges.\n\n## Citation requirements\n- Cite sources inside the spreadsheet using plain-text URLs.\n- For financial models, cite model inputs in cell comments.\n- For tabular data sourced externally, add a source column when each row represents a separate item.\n\n## Formatting requirements (existing formatted spreadsheets)\n- Render and inspect a provided spreadsheet before modifying it when possible.\n- Preserve existing formatting and style exactly.\n- Match styles for any newly filled cells that were previously blank.\n- Never overwrite established formatting unless the user explicitly asks for a redesign.\n\n## Formatting requirements (new or unstyled spreadsheets)\n- Use appropriate number and date formats.\n- Dates should render as dates, not plain numbers.\n- Percentages should usually default to one decimal place unless the data calls for something else.\n- Currencies should use the appropriate currency format.\n- Headers should be visually distinct from raw inputs and derived cells.\n- Use fill colors, borders, spacing, and merged cells sparingly and intentionally.\n- Set row heights and column widths so content is readable without excessive whitespace.\n- Do not apply borders around every filled cell.\n- Group related calculations and make totals simple sums of the cells above them.\n- Add whitespace to separate sections.\n- Ensure text does not spill into adjacent cells.\n- Avoid unsupported spreadsheet data-table features such as `=TABLE`.\n\n## Color conventions (if no style guidance)\n- Blue: user input\n- Black: formulas and derived values\n- Green: linked or imported values\n- Gray: static constants\n- Orange: review or caution\n- Light red: error or flag\n- Purple: control or logic\n- Teal: visualization anchors and KPI highlights\n\n## Finance-specific requirements\n- Format zeros as `-`.\n- Negative numbers should be red and in parentheses.\n- Format multiples as `5.2x`.\n- Always specify units in headers (for example, `Revenue ($mm)`).\n- Cite sources for all raw inputs in cell comments.\n- For new financial models with no user-specified style, use blue text for hardcoded inputs, black for formulas, green for internal workbook links, red for external links, and yellow fill for key assumptions that need attention.\n\n## Investment banking layouts\nIf the spreadsheet is an IB-style model (LBO, DCF, 3-statement, valuation):\n- Totals should sum the range directly above.\n- Hide gridlines and use horizontal borders above totals across relevant columns.\n- Section headers should be merged cells with dark fill and white text.\n- Column labels for numeric data should be right-aligned; row labels should be left-aligned.\n- Indent submetrics under their parent line items.\n"
  },
  {
    "path": "skills/.curated/spreadsheet/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Spreadsheet Skill\"\n  short_description: \"Create, edit, and analyze spreadsheets\"\n  icon_small: \"./assets/spreadsheet-small.svg\"\n  icon_large: \"./assets/spreadsheet.png\"\n  default_prompt: \"Use $spreadsheet to create or update a spreadsheet for this task with the right formulas, structure, and formatting.\"\n"
  },
  {
    "path": "skills/.curated/spreadsheet/references/examples/openpyxl/create_basic_spreadsheet.py",
    "content": "\"\"\"Create a basic spreadsheet with two sheets and a simple formula.\n\nUsage:\n  python3 create_basic_spreadsheet.py --output /tmp/basic_spreadsheet.xlsx\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nfrom pathlib import Path\n\nfrom openpyxl import Workbook\nfrom openpyxl.utils import get_column_letter\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(description=\"Create a basic spreadsheet with example data.\")\n    parser.add_argument(\n        \"--output\",\n        type=Path,\n        default=Path(\"basic_spreadsheet.xlsx\"),\n        help=\"Output .xlsx path (default: basic_spreadsheet.xlsx)\",\n    )\n    args = parser.parse_args()\n\n    wb = Workbook()\n    overview = wb.active\n    overview.title = \"Overview\"\n    employees = wb.create_sheet(\"Employees\")\n\n    overview[\"A1\"] = \"Description\"\n    overview[\"A2\"] = \"Awesome Company Report\"\n\n    employees.append([\"Title\", \"Name\", \"Address\", \"Score\"])\n    employees.append([\"Engineer\", \"Vicky\", \"90 50th Street\", 98])\n    employees.append([\"Manager\", \"Alex\", \"500 Market Street\", 92])\n    employees.append([\"Designer\", \"Jordan\", \"200 Pine Street\", 88])\n\n    employees[\"A6\"] = \"Total Score\"\n    employees[\"D6\"] = \"=SUM(D2:D4)\"\n\n    for col in range(1, 5):\n        employees.column_dimensions[get_column_letter(col)].width = 20\n\n    args.output.parent.mkdir(parents=True, exist_ok=True)\n    wb.save(args.output)\n    print(f\"Saved workbook to {args.output}\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/spreadsheet/references/examples/openpyxl/create_spreadsheet_with_styling.py",
    "content": "\"\"\"Generate a styled games scoreboard workbook using openpyxl.\n\nUsage:\n  python3 create_spreadsheet_with_styling.py --output /tmp/GamesSimpleStyling.xlsx\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nfrom pathlib import Path\n\nfrom openpyxl import Workbook\nfrom openpyxl.formatting.rule import FormulaRule\nfrom openpyxl.styles import Alignment, Font, PatternFill\nfrom openpyxl.utils import get_column_letter\n\nHEADER_FILL_HEX = \"B7E1CD\"\nHIGHLIGHT_FILL_HEX = \"FFF2CC\"\n\n\ndef apply_header_style(cell, fill_hex: str) -> None:\n    cell.fill = PatternFill(\"solid\", fgColor=fill_hex)\n    cell.font = Font(bold=True)\n    cell.alignment = Alignment(horizontal=\"center\", vertical=\"center\")\n\n\ndef apply_highlight_style(cell, fill_hex: str) -> None:\n    cell.fill = PatternFill(\"solid\", fgColor=fill_hex)\n    cell.font = Font(bold=True)\n    cell.alignment = Alignment(horizontal=\"center\", vertical=\"center\")\n\n\ndef populate_game_sheet(ws) -> None:\n    ws.title = \"GameX\"\n    ws.row_dimensions[2].height = 24\n\n    widths = {\"B\": 18, \"C\": 14, \"D\": 14, \"E\": 14, \"F\": 40}\n    for col, width in widths.items():\n        ws.column_dimensions[col].width = width\n\n    headers = [\"\", \"Name\", \"Game 1 Score\", \"Game 2 Score\", \"Total Score\", \"Notes\", \"\"]\n    for idx, value in enumerate(headers, start=1):\n        cell = ws.cell(row=2, column=idx, value=value)\n        if value:\n            apply_header_style(cell, HEADER_FILL_HEX)\n\n    players = [\n        (\"Vicky\", 12, 30, \"Dominated the minigames.\"),\n        (\"Yash\", 20, 10, \"Emily main with strong defense.\"),\n        (\"Bobby\", 1000, 1030, \"Numbers look suspiciously high.\"),\n    ]\n    for row_idx, (name, g1, g2, note) in enumerate(players, start=3):\n        ws.cell(row=row_idx, column=2, value=name)\n        ws.cell(row=row_idx, column=3, value=g1)\n        ws.cell(row=row_idx, column=4, value=g2)\n        ws.cell(row=row_idx, column=5, value=f\"=SUM(C{row_idx}:D{row_idx})\")\n        ws.cell(row=row_idx, column=6, value=note)\n\n    ws.cell(row=7, column=2, value=\"Winner\")\n    ws.cell(row=7, column=3, value=\"=INDEX(B3:B5, MATCH(MAX(E3:E5), E3:E5, 0))\")\n    ws.cell(row=7, column=5, value=\"Congrats!\")\n\n    ws.merge_cells(\"C7:D7\")\n    for col in range(2, 6):\n        apply_highlight_style(ws.cell(row=7, column=col), HIGHLIGHT_FILL_HEX)\n\n    rule = FormulaRule(formula=[\"LEN(A2)>0\"], fill=PatternFill(\"solid\", fgColor=HEADER_FILL_HEX))\n    ws.conditional_formatting.add(\"A2:G2\", rule)\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(description=\"Create a styled games scoreboard workbook.\")\n    parser.add_argument(\n        \"--output\",\n        type=Path,\n        default=Path(\"GamesSimpleStyling.xlsx\"),\n        help=\"Output .xlsx path (default: GamesSimpleStyling.xlsx)\",\n    )\n    args = parser.parse_args()\n\n    wb = Workbook()\n    ws = wb.active\n    populate_game_sheet(ws)\n\n    for col in range(1, 8):\n        col_letter = get_column_letter(col)\n        if col_letter not in ws.column_dimensions:\n            ws.column_dimensions[col_letter].width = 12\n\n    args.output.parent.mkdir(parents=True, exist_ok=True)\n    wb.save(args.output)\n    print(f\"Saved workbook to {args.output}\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/spreadsheet/references/examples/openpyxl/read_existing_spreadsheet.py",
    "content": "\"\"\"Read an existing .xlsx and print a small summary.\n\nIf --input is not provided, this script creates a tiny sample workbook in /tmp\nand reads that instead.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport tempfile\nfrom pathlib import Path\n\nfrom openpyxl import Workbook, load_workbook\n\n\ndef create_sample(path: Path) -> Path:\n    wb = Workbook()\n    ws = wb.active\n    ws.title = \"Sample\"\n    ws.append([\"Item\", \"Qty\", \"Price\"])\n    ws.append([\"Apples\", 3, 1.25])\n    ws.append([\"Oranges\", 2, 0.95])\n    ws.append([\"Bananas\", 5, 0.75])\n    ws[\"D1\"] = \"Total\"\n    ws[\"D2\"] = \"=B2*C2\"\n    ws[\"D3\"] = \"=B3*C3\"\n    ws[\"D4\"] = \"=B4*C4\"\n    wb.save(path)\n    return path\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(description=\"Read an existing spreadsheet.\")\n    parser.add_argument(\"--input\", type=Path, help=\"Path to an .xlsx file\")\n    args = parser.parse_args()\n\n    if args.input:\n        input_path = args.input\n    else:\n        tmp_dir = Path(tempfile.gettempdir())\n        input_path = tmp_dir / \"sample_read_existing.xlsx\"\n        create_sample(input_path)\n\n    wb = load_workbook(input_path, data_only=False)\n    print(f\"Loaded: {input_path}\")\n    print(\"Sheet names:\", wb.sheetnames)\n\n    for name in wb.sheetnames:\n        ws = wb[name]\n        max_row = ws.max_row or 0\n        max_col = ws.max_column or 0\n        print(f\"\\n== {name} (rows: {max_row}, cols: {max_col})\")\n        for row in ws.iter_rows(min_row=1, max_row=min(max_row, 5), max_col=min(max_col, 5)):\n            values = [cell.value for cell in row]\n            print(values)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/spreadsheet/references/examples/openpyxl/styling_spreadsheet.py",
    "content": "\"\"\"Create a styled spreadsheet with headers, borders, and a total row.\n\nUsage:\n  python3 styling_spreadsheet.py --output /tmp/styling_spreadsheet.xlsx\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nfrom pathlib import Path\n\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Alignment, Border, Font, PatternFill, Side\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(description=\"Create a styled spreadsheet example.\")\n    parser.add_argument(\n        \"--output\",\n        type=Path,\n        default=Path(\"styling_spreadsheet.xlsx\"),\n        help=\"Output .xlsx path (default: styling_spreadsheet.xlsx)\",\n    )\n    args = parser.parse_args()\n\n    wb = Workbook()\n    ws = wb.active\n    ws.title = \"FirstGame\"\n\n    ws.merge_cells(\"B2:E2\")\n    ws[\"B2\"] = \"Name | Game 1 Score | Game 2 Score | Total Score\"\n\n    header_fill = PatternFill(\"solid\", fgColor=\"B7E1CD\")\n    header_font = Font(bold=True)\n    header_alignment = Alignment(horizontal=\"center\", vertical=\"center\")\n    ws[\"B2\"].fill = header_fill\n    ws[\"B2\"].font = header_font\n    ws[\"B2\"].alignment = header_alignment\n\n    ws[\"B3\"] = \"Vicky\"\n    ws[\"C3\"] = 50\n    ws[\"D3\"] = 60\n    ws[\"E3\"] = \"=C3+D3\"\n\n    ws[\"B4\"] = \"John\"\n    ws[\"C4\"] = 40\n    ws[\"D4\"] = 50\n    ws[\"E4\"] = \"=C4+D4\"\n\n    ws[\"B5\"] = \"Jane\"\n    ws[\"C5\"] = 30\n    ws[\"D5\"] = 40\n    ws[\"E5\"] = \"=C5+D5\"\n\n    ws[\"B6\"] = \"Jim\"\n    ws[\"C6\"] = 20\n    ws[\"D6\"] = 30\n    ws[\"E6\"] = \"=C6+D6\"\n\n    ws.merge_cells(\"B9:E9\")\n    ws[\"B9\"] = \"=SUM(E3:E6)\"\n\n    thin = Side(style=\"thin\")\n    border = Border(top=thin, bottom=thin, left=thin, right=thin)\n    ws[\"B9\"].border = border\n    ws[\"B9\"].alignment = Alignment(horizontal=\"center\")\n    ws[\"B9\"].font = Font(bold=True)\n\n    for col in (\"B\", \"C\", \"D\", \"E\"):\n        ws.column_dimensions[col].width = 18\n    ws.row_dimensions[2].height = 24\n\n    args.output.parent.mkdir(parents=True, exist_ok=True)\n    wb.save(args.output)\n    print(f\"Saved workbook to {args.output}\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/transcribe/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/transcribe/SKILL.md",
    "content": "---\nname: \"transcribe\"\ndescription: \"Transcribe audio files to text with optional diarization and known-speaker hints. Use when a user asks to transcribe speech from audio/video, extract text from recordings, or label speakers in interviews or meetings.\"\n---\n\n\n# Audio Transcribe\n\nTranscribe audio using OpenAI, with optional speaker diarization when requested. Prefer the bundled CLI for deterministic, repeatable runs.\n\n## Workflow\n1. Collect inputs: audio file path(s), desired response format (text/json/diarized_json), optional language hint, and any known speaker references.\n2. Verify `OPENAI_API_KEY` is set. If missing, ask the user to set it locally (do not ask them to paste the key).\n3. Run the bundled `transcribe_diarize.py` CLI with sensible defaults (fast text transcription).\n4. Validate the output: transcription quality, speaker labels, and segment boundaries; iterate with a single targeted change if needed.\n5. Save outputs under `output/transcribe/` when working in this repo.\n\n## Decision rules\n- Default to `gpt-4o-mini-transcribe` with `--response-format text` for fast transcription.\n- If the user wants speaker labels or diarization, use `--model gpt-4o-transcribe-diarize --response-format diarized_json`.\n- If audio is longer than ~30 seconds, keep `--chunking-strategy auto`.\n- Prompting is not supported for `gpt-4o-transcribe-diarize`.\n\n## Output conventions\n- Use `output/transcribe/<job-id>/` for evaluation runs.\n- Use `--out-dir` for multiple files to avoid overwriting.\n\n## Dependencies (install if missing)\nPrefer `uv` for dependency management.\n\n```\nuv pip install openai\n```\nIf `uv` is unavailable:\n```\npython3 -m pip install openai\n```\n\n## Environment\n- `OPENAI_API_KEY` must be set for live API calls.\n- If the key is missing, instruct the user to create one in the OpenAI platform UI and export it in their shell.\n- Never ask the user to paste the full key in chat.\n\n## Skill path (set once)\n\n```bash\nexport CODEX_HOME=\"${CODEX_HOME:-$HOME/.codex}\"\nexport TRANSCRIBE_CLI=\"$CODEX_HOME/skills/transcribe/scripts/transcribe_diarize.py\"\n```\n\nUser-scoped skills install under `$CODEX_HOME/skills` (default: `~/.codex/skills`).\n\n## CLI quick start\nSingle file (fast text default):\n```\npython3 \"$TRANSCRIBE_CLI\" \\\n  path/to/audio.wav \\\n  --out transcript.txt\n```\n\nDiarization with known speakers (up to 4):\n```\npython3 \"$TRANSCRIBE_CLI\" \\\n  meeting.m4a \\\n  --model gpt-4o-transcribe-diarize \\\n  --known-speaker \"Alice=refs/alice.wav\" \\\n  --known-speaker \"Bob=refs/bob.wav\" \\\n  --response-format diarized_json \\\n  --out-dir output/transcribe/meeting\n```\n\nPlain text output (explicit):\n```\npython3 \"$TRANSCRIBE_CLI\" \\\n  interview.mp3 \\\n  --response-format text \\\n  --out interview.txt\n```\n\n## Reference map\n- `references/api.md`: supported formats, limits, response formats, and known-speaker notes.\n"
  },
  {
    "path": "skills/.curated/transcribe/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Audio Transcribe\"\n  short_description: \"Transcribe audio using OpenAI, with optional speaker diarization when requested. Prefer the bundled CLI for deterministic, repeatable runs.\"\n  icon_small: \"./assets/transcribe-small.svg\"\n  icon_large: \"./assets/transcribe.png\"\n  default_prompt: \"Transcribe this audio or video, include speaker labels when possible, and provide a clean summary.\"\n"
  },
  {
    "path": "skills/.curated/transcribe/references/api.md",
    "content": "# gpt-4o-transcribe-diarize quick reference\n\n- Input formats: mp3, mp4, mpeg, mpga, m4a, wav, webm.\n- Max file size: 25 MB per request.\n- response_format options: text, json, diarized_json.\n- For audio longer than ~30 seconds, pass chunking_strategy (use \"auto\" to split into chunks).\n- Known speakers: up to 4 references via extra_body known_speaker_names + known_speaker_references (data URLs).\n- Prompting is not supported for gpt-4o-transcribe-diarize.\n"
  },
  {
    "path": "skills/.curated/transcribe/scripts/transcribe_diarize.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Transcribe audio (optionally with speaker diarization) using OpenAI.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport base64\nimport json\nimport mimetypes\nimport os\nfrom pathlib import Path\nimport sys\nfrom typing import Any, Dict, List, Optional, Tuple\n\nDEFAULT_MODEL = \"gpt-4o-mini-transcribe\"\nDEFAULT_RESPONSE_FORMAT = \"text\"\nDEFAULT_CHUNKING_STRATEGY = \"auto\"\nMAX_AUDIO_BYTES = 25 * 1024 * 1024\nMAX_KNOWN_SPEAKERS = 4\n\nALLOWED_RESPONSE_FORMATS = {\"text\", \"json\", \"diarized_json\"}\n\n\ndef _die(message: str, code: int = 1) -> None:\n    print(f\"Error: {message}\", file=sys.stderr)\n    raise SystemExit(code)\n\n\ndef _warn(message: str) -> None:\n    print(f\"Warning: {message}\", file=sys.stderr)\n\n\ndef _ensure_api_key(dry_run: bool) -> None:\n    if os.getenv(\"OPENAI_API_KEY\"):\n        print(\"OPENAI_API_KEY is set.\", file=sys.stderr)\n        return\n    if dry_run:\n        _warn(\"OPENAI_API_KEY is not set; dry-run only.\")\n        return\n    _die(\"OPENAI_API_KEY is not set. Export it before running.\")\n\n\ndef _normalize_response_format(value: Optional[str]) -> str:\n    if not value:\n        return DEFAULT_RESPONSE_FORMAT\n    fmt = value.strip().lower()\n    if fmt not in ALLOWED_RESPONSE_FORMATS:\n        _die(\n            \"response-format must be one of: \"\n            + \", \".join(sorted(ALLOWED_RESPONSE_FORMATS))\n        )\n    return fmt\n\n\ndef _normalize_chunking_strategy(value: Optional[str]) -> Any:\n    if not value:\n        return DEFAULT_CHUNKING_STRATEGY\n    raw = str(value).strip()\n    if raw.startswith(\"{\"):\n        try:\n            return json.loads(raw)\n        except json.JSONDecodeError:\n            _die(\"chunking-strategy JSON is invalid\")\n    return raw\n\n\ndef _guess_mime_type(path: Path) -> str:\n    mime, _ = mimetypes.guess_type(str(path))\n    if mime:\n        return mime\n    return \"audio/wav\"\n\n\ndef _encode_data_url(path: Path) -> str:\n    data = path.read_bytes()\n    mime = _guess_mime_type(path)\n    encoded = base64.b64encode(data).decode(\"ascii\")\n    return f\"data:{mime};base64,{encoded}\"\n\n\ndef _parse_known_speakers(raw_items: List[str]) -> Tuple[List[str], List[str]]:\n    names: List[str] = []\n    refs: List[str] = []\n    for raw in raw_items:\n        if \"=\" not in raw:\n            _die(\"known-speaker must be NAME=PATH\")\n        name, path_str = raw.split(\"=\", 1)\n        name = name.strip()\n        path = Path(path_str.strip())\n        if not name or not path_str.strip():\n            _die(\"known-speaker must be NAME=PATH\")\n        if not path.exists():\n            _die(f\"Known speaker file not found: {path}\")\n        names.append(name)\n        refs.append(_encode_data_url(path))\n    if len(names) > MAX_KNOWN_SPEAKERS:\n        _die(f\"known speakers must be <= {MAX_KNOWN_SPEAKERS}\")\n    return names, refs\n\n\ndef _output_extension(response_format: str) -> str:\n    return \"txt\" if response_format == \"text\" else \"json\"\n\n\ndef _build_output_path(\n    audio_path: Path,\n    response_format: str,\n    out: Optional[str],\n    out_dir: Optional[str],\n) -> Path:\n    ext = \".\" + _output_extension(response_format)\n    if out:\n        path = Path(out)\n        if path.exists() and path.is_dir():\n            return path / f\"{audio_path.stem}.transcript{ext}\"\n        if path.suffix == \"\":\n            return path.with_suffix(ext)\n        return path\n    if out_dir:\n        base = Path(out_dir)\n        base.mkdir(parents=True, exist_ok=True)\n        return base / f\"{audio_path.stem}.transcript{ext}\"\n    return Path(f\"{audio_path.stem}.transcript{ext}\")\n\n\ndef _create_client():\n    try:\n        from openai import OpenAI\n    except ImportError:\n        _die(\"openai SDK not installed. Install with `uv pip install openai`.\")\n    return OpenAI()\n\n\ndef _format_output(result: Any, response_format: str) -> str:\n    if response_format == \"text\":\n        text = getattr(result, \"text\", None)\n        return text if isinstance(text, str) else str(result)\n    if hasattr(result, \"model_dump\"):\n        return json.dumps(result.model_dump(), indent=2)\n    if isinstance(result, (dict, list)):\n        return json.dumps(result, indent=2)\n    return json.dumps({\"text\": getattr(result, \"text\", str(result))}, indent=2)\n\n\ndef _validate_audio(path: Path) -> None:\n    if not path.exists():\n        _die(f\"Audio file not found: {path}\")\n    size = path.stat().st_size\n    if size > MAX_AUDIO_BYTES:\n        _warn(\n            f\"Audio file exceeds 25MB limit ({size} bytes): {path}\"\n        )\n\n\ndef _build_payload(\n    args: argparse.Namespace,\n    known_speaker_names: List[str],\n    known_speaker_refs: List[str],\n) -> Dict[str, Any]:\n    payload: Dict[str, Any] = {\n        \"model\": args.model,\n        \"response_format\": args.response_format,\n        \"chunking_strategy\": args.chunking_strategy,\n    }\n    if args.language:\n        payload[\"language\"] = args.language\n    if args.prompt:\n        payload[\"prompt\"] = args.prompt\n    if known_speaker_names:\n        payload[\"extra_body\"] = {\n            \"known_speaker_names\": known_speaker_names,\n            \"known_speaker_references\": known_speaker_refs,\n        }\n    return payload\n\n\ndef _run_one(\n    client: Any,\n    audio_path: Path,\n    payload: Dict[str, Any],\n) -> Any:\n    with audio_path.open(\"rb\") as audio_file:\n        return client.audio.transcriptions.create(\n            file=audio_file,\n            **payload,\n        )\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        description=\"Transcribe audio (optionally with speaker diarization) using OpenAI.\"\n    )\n    parser.add_argument(\"audio\", nargs=\"+\", help=\"Audio file(s) to transcribe\")\n    parser.add_argument(\n        \"--model\",\n        default=DEFAULT_MODEL,\n        help=f\"Model to use (default: {DEFAULT_MODEL})\",\n    )\n    parser.add_argument(\n        \"--response-format\",\n        default=DEFAULT_RESPONSE_FORMAT,\n        help=\"Response format: text, json, or diarized_json\",\n    )\n    parser.add_argument(\n        \"--chunking-strategy\",\n        default=DEFAULT_CHUNKING_STRATEGY,\n        help=\"Chunking strategy (use 'auto' for long audio)\",\n    )\n    parser.add_argument(\"--language\", help=\"Optional language hint (e.g. 'en')\")\n    parser.add_argument(\"--prompt\", help=\"Optional prompt to guide transcription\")\n    parser.add_argument(\n        \"--known-speaker\",\n        action=\"append\",\n        default=[],\n        help=\"Known speaker reference as NAME=PATH (repeatable, max 4)\",\n    )\n    parser.add_argument(\"--out\", help=\"Output file path (single audio only)\")\n    parser.add_argument(\"--out-dir\", help=\"Output directory for transcripts\")\n    parser.add_argument(\n        \"--stdout\",\n        action=\"store_true\",\n        help=\"Write transcript to stdout instead of a file\",\n    )\n    parser.add_argument(\n        \"--dry-run\",\n        action=\"store_true\",\n        help=\"Validate inputs and print payload without calling the API\",\n    )\n\n    args = parser.parse_args()\n    args.response_format = _normalize_response_format(args.response_format)\n    args.chunking_strategy = _normalize_chunking_strategy(args.chunking_strategy)\n\n    if args.out and len(args.audio) > 1:\n        _die(\"--out only supports a single audio file\")\n    if args.stdout and (args.out or args.out_dir):\n        _die(\"--stdout cannot be combined with --out or --out-dir\")\n    if args.stdout and len(args.audio) > 1:\n        _die(\"--stdout only supports a single audio file\")\n\n    if args.prompt and \"transcribe-diarize\" in args.model:\n        _die(\"prompt is not supported with gpt-4o-transcribe-diarize\")\n    if args.response_format == \"diarized_json\" and \"transcribe-diarize\" not in args.model:\n        _die(\"diarized_json requires gpt-4o-transcribe-diarize\")\n\n    _ensure_api_key(args.dry_run)\n\n    audio_paths = [Path(p) for p in args.audio]\n    for path in audio_paths:\n        _validate_audio(path)\n\n    known_names, known_refs = _parse_known_speakers(args.known_speaker)\n    if known_names and \"transcribe-diarize\" not in args.model:\n        _warn(\"known-speaker references are only supported for gpt-4o-transcribe-diarize\")\n    payload = _build_payload(args, known_names, known_refs)\n\n    if args.dry_run:\n        print(json.dumps(payload, indent=2))\n        return\n\n    client = _create_client()\n\n    for path in audio_paths:\n        result = _run_one(client, path, payload)\n        output = _format_output(result, args.response_format)\n        if args.stdout:\n            print(output)\n            continue\n        out_path = _build_output_path(path, args.response_format, args.out, args.out_dir)\n        out_path.parent.mkdir(parents=True, exist_ok=True)\n        out_path.write_text(output, encoding=\"utf-8\")\n        print(f\"Wrote {out_path}\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.curated/vercel-deploy/LICENSE.txt",
    "content": "MIT License\n\nCopyright (c) 2026 Vercel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "skills/.curated/vercel-deploy/SKILL.md",
    "content": "---\nname: vercel-deploy\ndescription: Deploy applications and websites to Vercel. Use when the user requests deployment actions like \"deploy my app\", \"deploy and give me the link\", \"push this live\", or \"create a preview deployment\".\n---\n\n# Vercel Deploy\n\nDeploy any project to Vercel instantly. **Always deploy as preview** (not production) unless the user explicitly asks for production.\n\n## Prerequisites\n\n- Check whether the Vercel CLI is installed **without** escalated permissions (for example, `command -v vercel`).\n- Only escalate the actual deploy command if sandboxing blocks the deployment network calls (`sandbox_permissions=require_escalated`).\n- The deployment might take a few minutes. Use appropriate timeout values.\n\n## Quick Start\n\n1. Check whether the Vercel CLI is installed (no escalation for this check):\n\n```bash\ncommand -v vercel\n```\n\n2. If `vercel` is installed, run this (with a 10 minute timeout):\n```bash\nvercel deploy [path] -y\n```\n\n**Important:** Use a 10 minute (600000ms) timeout for the deploy command since builds can take a while.\n\n3. If `vercel` is not installed, or if the CLI fails with \"No existing credentials found\", use the fallback method below.\n\n## Fallback (No Auth)\n\nIf CLI fails with auth error, use the deploy script:\n\n```bash\nskill_dir=\"<path-to-skill>\"\n\n# Deploy current directory\nbash \"$skill_dir/scripts/deploy.sh\"\n\n# Deploy specific project\nbash \"$skill_dir/scripts/deploy.sh\" /path/to/project\n\n# Deploy existing tarball\nbash \"$skill_dir/scripts/deploy.sh\" /path/to/project.tgz\n```\n\nThe script handles framework detection, packaging, and deployment. It waits for the build to complete and returns JSON with `previewUrl` and `claimUrl`.\n\n**Tell the user:** \"Your deployment is ready at [previewUrl]. Claim it at [claimUrl] to manage your deployment.\"\n\n## Production Deploys\n\nOnly if user explicitly asks:\n```bash\nvercel deploy [path] --prod -y\n```\n\n## Output\n\nShow the user the deployment URL. For fallback deployments, also show the claim URL.\n\n**Do not** curl or fetch the deployed URL to verify it works. Just return the link.\n\n## Troubleshooting\n\n### Escalated Network Access\n\nIf deployment fails due to network issues (timeouts, DNS errors, connection resets), rerun the actual deploy command with escalated permissions (use `sandbox_permissions=require_escalated`). Do not escalate the `command -v vercel` installation check. The deploy requires escalated network access when sandbox networking blocks outbound requests.\n\nExample guidance to the user:\n\n```\nThe deploy needs escalated network access to deploy to Vercel. I can rerun the command with escalated permissions—want me to proceed?\n```\n"
  },
  {
    "path": "skills/.curated/vercel-deploy/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Vercel Deploy\"\n  short_description: \"Deploy apps and agents with zero configuration on Vercel. No account required.\"\n  icon_small: \"./assets/vercel-small.svg\"\n  icon_large: \"./assets/vercel.png\"\n  default_prompt: \"Create a Vercel deployment for this project and share the URL.\"\n"
  },
  {
    "path": "skills/.curated/vercel-deploy/scripts/deploy.sh",
    "content": "#!/bin/bash\n\n# Vercel Deployment Script (via claimable deploy endpoint)\n# Usage: ./deploy.sh [project-path]\n# Returns: JSON with previewUrl, claimUrl, deploymentId, projectId\n\nset -euo pipefail\n\nDEPLOY_ENDPOINT=\"https://codex-deploy-skills.vercel.sh/api/deploy\"\n\n# Detect framework from package.json\ndetect_framework() {\n    local pkg_json=\"$1\"\n\n    if [ ! -f \"$pkg_json\" ]; then\n        echo \"null\"\n        return\n    fi\n\n    local content=$(cat \"$pkg_json\")\n\n    # Helper to check if a package exists in dependencies or devDependencies.\n    # Use exact matching by default, with a separate prefix matcher for scoped\n    # package families like \"@remix-run/\".\n    has_dep_exact() {\n        echo \"$content\" | grep -q \"\\\"$1\\\"\"\n    }\n\n    has_dep_prefix() {\n        echo \"$content\" | grep -q \"\\\"$1\"\n    }\n\n    # Order matters - check more specific frameworks first\n\n    # Blitz\n    if has_dep_exact \"blitz\"; then echo \"blitzjs\"; return; fi\n\n    # Next.js\n    if has_dep_exact \"next\"; then echo \"nextjs\"; return; fi\n\n    # Gatsby\n    if has_dep_exact \"gatsby\"; then echo \"gatsby\"; return; fi\n\n    # Remix\n    if has_dep_prefix \"@remix-run/\"; then echo \"remix\"; return; fi\n\n    # React Router (v7 framework mode)\n    if has_dep_prefix \"@react-router/\"; then echo \"react-router\"; return; fi\n\n    # TanStack Start\n    if has_dep_exact \"@tanstack/start\"; then echo \"tanstack-start\"; return; fi\n\n    # Astro\n    if has_dep_exact \"astro\"; then echo \"astro\"; return; fi\n\n    # Hydrogen (Shopify)\n    if has_dep_exact \"@shopify/hydrogen\"; then echo \"hydrogen\"; return; fi\n\n    # SvelteKit\n    if has_dep_exact \"@sveltejs/kit\"; then echo \"sveltekit-1\"; return; fi\n\n    # Svelte (standalone)\n    if has_dep_exact \"svelte\"; then echo \"svelte\"; return; fi\n\n    # Nuxt\n    if has_dep_exact \"nuxt\"; then echo \"nuxtjs\"; return; fi\n\n    # Vue with Vitepress\n    if has_dep_exact \"vitepress\"; then echo \"vitepress\"; return; fi\n\n    # Vue with Vuepress\n    if has_dep_exact \"vuepress\"; then echo \"vuepress\"; return; fi\n\n    # Gridsome\n    if has_dep_exact \"gridsome\"; then echo \"gridsome\"; return; fi\n\n    # SolidStart\n    if has_dep_exact \"@solidjs/start\"; then echo \"solidstart-1\"; return; fi\n\n    # Docusaurus\n    if has_dep_exact \"@docusaurus/core\"; then echo \"docusaurus-2\"; return; fi\n\n    # RedwoodJS\n    if has_dep_prefix \"@redwoodjs/\"; then echo \"redwoodjs\"; return; fi\n\n    # Hexo\n    if has_dep_exact \"hexo\"; then echo \"hexo\"; return; fi\n\n    # Eleventy\n    if has_dep_exact \"@11ty/eleventy\"; then echo \"eleventy\"; return; fi\n\n    # Angular / Ionic Angular\n    if has_dep_exact \"@ionic/angular\"; then echo \"ionic-angular\"; return; fi\n    if has_dep_exact \"@angular/core\"; then echo \"angular\"; return; fi\n\n    # Ionic React\n    if has_dep_exact \"@ionic/react\"; then echo \"ionic-react\"; return; fi\n\n    # Create React App\n    if has_dep_exact \"react-scripts\"; then echo \"create-react-app\"; return; fi\n\n    # Ember\n    if has_dep_exact \"ember-cli\" || has_dep_exact \"ember-source\"; then echo \"ember\"; return; fi\n\n    # Dojo\n    if has_dep_exact \"@dojo/framework\"; then echo \"dojo\"; return; fi\n\n    # Polymer\n    if has_dep_prefix \"@polymer/\"; then echo \"polymer\"; return; fi\n\n    # Preact\n    if has_dep_exact \"preact\"; then echo \"preact\"; return; fi\n\n    # Stencil\n    if has_dep_exact \"@stencil/core\"; then echo \"stencil\"; return; fi\n\n    # UmiJS\n    if has_dep_exact \"umi\"; then echo \"umijs\"; return; fi\n\n    # Sapper (legacy Svelte)\n    if has_dep_exact \"sapper\"; then echo \"sapper\"; return; fi\n\n    # Saber\n    if has_dep_exact \"saber\"; then echo \"saber\"; return; fi\n\n    # Sanity\n    if has_dep_exact \"sanity\"; then echo \"sanity-v3\"; return; fi\n    if has_dep_prefix \"@sanity/\"; then echo \"sanity\"; return; fi\n\n    # Storybook\n    if has_dep_prefix \"@storybook/\"; then echo \"storybook\"; return; fi\n\n    # NestJS\n    if has_dep_exact \"@nestjs/core\"; then echo \"nestjs\"; return; fi\n\n    # Elysia\n    if has_dep_exact \"elysia\"; then echo \"elysia\"; return; fi\n\n    # Hono\n    if has_dep_exact \"hono\"; then echo \"hono\"; return; fi\n\n    # Fastify\n    if has_dep_exact \"fastify\"; then echo \"fastify\"; return; fi\n\n    # h3\n    if has_dep_exact \"h3\"; then echo \"h3\"; return; fi\n\n    # Nitro\n    if has_dep_exact \"nitropack\"; then echo \"nitro\"; return; fi\n\n    # Express\n    if has_dep_exact \"express\"; then echo \"express\"; return; fi\n\n    # Vite (generic - check last among JS frameworks)\n    if has_dep_exact \"vite\"; then echo \"vite\"; return; fi\n\n    # Parcel\n    if has_dep_exact \"parcel\"; then echo \"parcel\"; return; fi\n\n    # No framework detected\n    echo \"null\"\n}\n\n# Parse arguments\nINPUT_PATH=\"${1:-.}\"\n\n# Create temp directory for packaging\nTEMP_DIR=$(mktemp -d)\nTARBALL=\"$TEMP_DIR/project.tgz\"\nSTAGING_DIR=\"$TEMP_DIR/staging\"\nCLEANUP_TEMP=true\n\ncleanup() {\n    if [ \"$CLEANUP_TEMP\" = true ]; then\n        rm -rf \"$TEMP_DIR\"\n    fi\n}\ntrap cleanup EXIT\n\necho \"Preparing deployment...\" >&2\n\n# Check if input is a .tgz file or a directory\nFRAMEWORK=\"null\"\n\nif [ -f \"$INPUT_PATH\" ] && [[ \"$INPUT_PATH\" == *.tgz ]]; then\n    # Input is already a tarball, use it directly\n    echo \"Using provided tarball...\" >&2\n    TARBALL=\"$INPUT_PATH\"\n    CLEANUP_TEMP=false\n    # Can't detect framework from tarball, leave as null\nelif [ -d \"$INPUT_PATH\" ]; then\n    # Input is a directory, need to tar it\n    PROJECT_PATH=$(cd \"$INPUT_PATH\" && pwd)\n\n    # Detect framework from package.json\n    FRAMEWORK=$(detect_framework \"$PROJECT_PATH/package.json\")\n\n    # Stage files into a temporary directory to avoid mutating the source tree.\n    mkdir -p \"$STAGING_DIR\"\n    echo \"Staging project files...\" >&2\n    tar -C \"$PROJECT_PATH\" \\\n        --exclude='node_modules' \\\n        --exclude='.git' \\\n        --exclude='.env' \\\n        --exclude='.env.*' \\\n        -cf - . | tar -C \"$STAGING_DIR\" -xf -\n\n    # Check if this is a static HTML project (no package.json)\n    if [ ! -f \"$PROJECT_PATH/package.json\" ]; then\n        # Find HTML files in root\n        HTML_FILES=$(find \"$STAGING_DIR\" -maxdepth 1 -name \"*.html\" -type f)\n        HTML_COUNT=$(printf '%s\\n' \"$HTML_FILES\" | sed '/^$/d' | wc -l | tr -d '[:space:]')\n\n        # If there's exactly one HTML file and it's not index.html, rename it\n        if [ \"$HTML_COUNT\" -eq 1 ]; then\n            HTML_FILE=$(echo \"$HTML_FILES\" | head -1)\n            BASENAME=$(basename \"$HTML_FILE\")\n            if [ \"$BASENAME\" != \"index.html\" ]; then\n                echo \"Renaming $BASENAME to index.html...\" >&2\n                mv \"$HTML_FILE\" \"$STAGING_DIR/index.html\"\n            fi\n        fi\n    fi\n\n    # Create tarball of the project (excluding node_modules and .git)\n    echo \"Creating deployment package...\" >&2\n    tar -czf \"$TARBALL\" -C \"$STAGING_DIR\" .\nelse\n    echo \"Error: Input must be a directory or a .tgz file\" >&2\n    exit 1\nfi\n\nif [ \"$FRAMEWORK\" != \"null\" ]; then\n    echo \"Detected framework: $FRAMEWORK\" >&2\nfi\n\n# Deploy\necho \"Deploying...\" >&2\nRESPONSE=$(curl -s -X POST \"$DEPLOY_ENDPOINT\" -F \"file=@$TARBALL\" -F \"framework=$FRAMEWORK\")\n\n# Check for error in response\nif echo \"$RESPONSE\" | grep -q '\"error\"'; then\n    ERROR_MSG=$(echo \"$RESPONSE\" | grep -o '\"error\":\"[^\"]*\"' | cut -d'\"' -f4)\n    echo \"Error: $ERROR_MSG\" >&2\n    exit 1\nfi\n\n# Extract URLs from response\nPREVIEW_URL=$(echo \"$RESPONSE\" | grep -o '\"previewUrl\":\"[^\"]*\"' | cut -d'\"' -f4)\nCLAIM_URL=$(echo \"$RESPONSE\" | grep -o '\"claimUrl\":\"[^\"]*\"' | cut -d'\"' -f4)\n\nif [ -z \"$PREVIEW_URL\" ]; then\n    echo \"Error: Could not extract preview URL from response\" >&2\n    echo \"$RESPONSE\" >&2\n    exit 1\nfi\n\necho \"Deployment started. Waiting for build to complete...\" >&2\necho \"Preview URL: $PREVIEW_URL\" >&2\n\n# Poll the preview URL until it returns 200 (not 5xx which indicates still building)\nMAX_ATTEMPTS=60  # 5 minutes max (60 * 5 seconds)\nATTEMPT=0\n\nwhile [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do\n    HTTP_STATUS=$(curl -s -o /dev/null -w \"%{http_code}\" \"$PREVIEW_URL\")\n    \n    if [ \"$HTTP_STATUS\" -eq 200 ]; then\n        echo \"\" >&2\n        echo \"Deployment ready!\" >&2\n        break\n    elif [ \"$HTTP_STATUS\" -ge 500 ]; then\n        # 5xx means still building/deploying\n        echo \"Building... (attempt $((ATTEMPT + 1))/$MAX_ATTEMPTS)\" >&2\n        sleep 5\n        ATTEMPT=$((ATTEMPT + 1))\n    elif [ \"$HTTP_STATUS\" -ge 400 ] && [ \"$HTTP_STATUS\" -lt 500 ]; then\n        # 4xx might be an error or the app itself returns 4xx - check if it's responding\n        echo \"\" >&2\n        echo \"Deployment ready (returned $HTTP_STATUS)!\" >&2\n        break\n    else\n        # Any other status, assume it's ready\n        echo \"\" >&2\n        echo \"Deployment ready!\" >&2\n        break\n    fi\ndone\n\nif [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then\n    echo \"\" >&2\n    echo \"Warning: Timed out waiting for deployment, but it may still be building.\" >&2\nfi\n\necho \"\" >&2\necho \"Preview URL: $PREVIEW_URL\" >&2\necho \"Claim URL:   $CLAIM_URL\" >&2\necho \"\" >&2\n\n# Output JSON for programmatic use\necho \"$RESPONSE\"\n"
  },
  {
    "path": "skills/.curated/winui-app/LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "skills/.curated/winui-app/SKILL.md",
    "content": "---\nname: winui-app\ndescription: Bootstrap, develop, and design modern WinUI 3 desktop applications with C# and the Windows App SDK using official Microsoft guidance, WinUI Gallery patterns, Windows App SDK samples, and CommunityToolkit components. Use when creating a brand new app, preparing a machine for WinUI, reviewing, refactoring, planning, troubleshooting, environment-checking, or setting up WinUI 3 XAML, controls, navigation, windowing, theming, accessibility, responsiveness, performance, deployment, or related Windows app design and development work.\n---\n\n# WinUI App\n\nUse this skill for WinUI 3 and Windows App SDK work that needs grounded setup guidance, app bootstrap, modern Windows UX decisions, or concrete implementation patterns.\n\n## Required Flow\n\n1. Classify the task as environment/setup, new-app bootstrap, design, implementation, review, or troubleshooting.\n2. If the task is about preparing a machine for WinUI, auditing readiness, or creating a brand new app, start with the bundled setup-and-scaffold flow in this skill before broader design, implementation, or troubleshooting work:\n   - Pick the app name when the request is for a new app.\n   - Use the exact name the user gave when it is already a safe folder name.\n   - If the user did not give a name, derive a short PascalCase name from the request and state what you chose.\n   - Create the project in the user's current workspace unless they asked for another location.\n   - Do not use `--force` unless the user explicitly asked to overwrite existing files.\n   - Run the bundled WinGet configuration from the skill directory so the relative path stays exactly `config.yaml`:\n\n```powershell\nwinget configure -f config.yaml --accept-configuration-agreements --disable-interactivity\n```\n\n   - Treat the configuration as intended to enable Developer Mode, install or update Visual Studio Community 2026, and install the Managed Desktop, Universal, and Windows App SDK C# components needed for WinUI development.\n   - Assess the configuration result before continuing. Continue on success. If it fails, inspect the output instead of guessing. If the `winui` template is already available and the toolchain is usable, note the partial failure and continue. If prerequisites are still missing, stop and report the blocker clearly.\n   - Verify the template is available before scaffolding:\n\n```powershell\ndotnet new list winui\n```\n\n   - For diagnostics-only environment requests, explain that the bundled bootstrap may change the machine and get confirmation before running it. If the user declines changes, use the manual verification guidance in `references/foundation-environment-audit-and-remediation.md` and summarize readiness under `present`, `missing`, `uncertain`, and `recommended optional tools`.\n   - For a brand new app, scaffold with `dotnet new winui -o <name>`. Add template options only when the user asked for them. Supported options: `-f|--framework net10.0|net9.0|net8.0`, `-slnx|--use-slnx`, `-cpm|--central-pkg-mgmt`, `-mvvm|--use-mvvm`, `-imt|--include-mvvm-toolkit`, `-un|--unpackaged`, `-nsf|--no-solution-file`, `--force`. Do not invent unsupported flags. If the user asks for packaged behavior, pass `--unpackaged false`. Otherwise keep the template default.\n   - Verify a new scaffold by confirming the expected project file exists and running `dotnet build` against the generated `.csproj`.\n   - Launch a newly scaffolded app through the correct path for its actual packaging model and confirm there is a real top-level window instead of relying only on the launcher process exit code.\n3. Read `references/_sections.md`, then load only the reference files that match the task.\n4. Make the packaging model explicit before creating or refactoring the app. Default to packaged for Store-like product workflows and Visual Studio deploy/F5 flows. Default to unpackaged when the user expects repeatable CLI build-and-run loops or direct `.exe` launches after each change.\n5. When the task is an opaque XAML compiler failure such as `MSB3073` or `XamlCompiler.exe`, read `references/foundation-template-first-recovery.md` and simplify back toward the current `dotnet new winui` scaffold for the chosen packaging model before inventing custom recovery structure.\n6. For any work that creates or changes a WinUI app, make a complete but minimal edit set, then build the app and run it before responding to the user. Do this by default even when the user did not explicitly ask for verification. If a running app instance locks the output while more work remains, stop it, rebuild, relaunch, and continue verification. When the work is complete and launch verification succeeds, leave the final verified app instance running for the user unless they explicitly asked you not to.\n7. Treat launch verification as incomplete until the app shows objective success signals such as a responsive top-level window, expected window title, or other clear startup behavior. A spawned process by itself is not enough.\n8. Prefer Microsoft Learn for requirements, API expectations, and platform guidance.\n9. Prefer WinUI Gallery for concrete control usage, shell composition, and design details.\n10. Prefer WindowsAppSDK-Samples for scenario-level APIs such as windowing, lifecycle, notifications, deployment, and custom controls.\n11. Build toward WinUI and Fluent guidance first. Treat native WinUI shells, controls, interactions, and control chrome as the default implementation path.\n12. For grouped command surfaces such as document actions, editor formatting, view toggles, or page-level toolbars, favor a native `CommandBar` or other stock WinUI command surface before building a custom row with `Grid`, `StackPanel`, `Border`, or ad hoc button groupings.\n13. Do not invent app-specific controls, bespoke component libraries, or custom chrome to replace stock WinUI behavior unless the user explicitly asks for that customization, the existing product design system already requires it, or a verified platform gap leaves no clean native option.\n14. When customization is needed, first compose, template, or restyle built-in WinUI controls and system resources before adding CommunityToolkit dependencies or authoring a new custom control.\n15. Use CommunityToolkit only when built-in WinUI controls or helpers do not cover the need cleanly.\n16. Support both light and dark mode by default. Treat single-theme output as an exception that requires an explicit user request or an existing product constraint.\n17. Use theme-aware resources, system brushes, and WinUI styling hooks instead of hard-coded light-only or dark-only colors when building or revising UI.\n18. Make scroll ownership explicit for collection layouts. When a page already scrolls vertically, do not assume a nested `GridView` or other scroll-owning collection will still render a horizontal poster rail correctly.\n19. Do not add extra `Border` wrappers around sections, lists, or cards unless the border is doing distinct work that the contained control or parent surface does not already provide. Avoid \"double-card\" compositions where a section `Border` wraps child items that already render as cards.\n20. Treat responsiveness as a shell-plus-page problem, not only a control-resize problem. Plan explicit wide, medium, and phone-width behavior for navigation, padding, content density, and footer/tool regions, and simplify or hide nonessential UI as width shrinks.\n\n## Common Routes\n\n| Request | Read first |\n| --- | --- |\n| Check whether this PC can build WinUI apps | `references/foundation-environment-audit-and-remediation.md` |\n| Install missing WinUI prerequisites | `references/foundation-environment-audit-and-remediation.md` |\n| Start a new packaged or unpackaged app | `references/foundation-setup-and-project-selection.md` |\n| Recover from opaque XAML compiler or startup failures while staying anchored to the template scaffold | `references/foundation-template-first-recovery.md` |\n| Build, run, or verify that a WinUI app actually launched | `references/build-run-and-launch-verification.md` |\n| Review app structure, pages, resources, and bindings | `references/foundation-winui-app-structure.md` |\n| Choose shell, navigation, title bar, or multi-window patterns | `references/shell-navigation-and-windowing.md` |\n| Choose controls or responsive layout patterns | `references/controls-layout-and-adaptive-ui.md` |\n| Apply Mica, theming, typography, icons, or Fluent styling | `references/styling-theming-materials-and-icons.md` |\n| Improve accessibility, keyboarding, or localization | `references/accessibility-input-and-localization.md` |\n| Diagnose responsiveness or UI-thread performance | `references/performance-diagnostics-and-responsiveness.md` |\n| Decide whether to use CommunityToolkit | `references/community-toolkit-controls-and-helpers.md` |\n| Handle lifecycle, notifications, or deployment | `references/windows-app-sdk-lifecycle-notifications-and-deployment.md` |\n| Run a review checklist | `references/testing-debugging-and-review-checklists.md` |\n\n## Environment Rules\n\n- Do not guess whether the machine is ready for WinUI development. Verify it.\n- Use the bundled setup-and-scaffold flow in this skill for fresh setup, remediation, and first-project scaffolding instead of delegating to another skill.\n- Treat `config.yaml` in this skill directory as the bundled bootstrap source of truth.\n- Treat uncertain environment signals as uncertain, not as success.\n- If the task is audit-only and the user declines machine changes, use the manual verification guidance in `references/foundation-environment-audit-and-remediation.md` and keep uncertain signals explicit instead of implying success.\n- If `config.yaml` is missing, say so clearly and fall back to the official Microsoft workflow instead of pretending the bundled path exists.\n- Keep environment readiness, packaging choice, and application startup verification as separate checks. Passing one does not prove the others.\n- Fail closed on ambiguous launch results. If the app did not clearly open, keep debugging.\n- After creating or editing a WinUI app, do not stop at a successful build. Launch the app, confirm objective startup behavior, and leave the final verified app instance running before returning control to the user unless they explicitly say not to run it.\n\n## Reference Rules\n\n- Keep C# as the primary path. Mention C++ or C++/WinRT only when the difference is material.\n- Preserve the conventions of an existing codebase instead of forcing a generic sample structure onto it.\n- Treat WinUI design guidance and native controls as the baseline. Do not drift into bespoke component systems or app-specific replacements for standard controls unless the user explicitly requests them or the existing codebase already depends on them.\n- Support light and dark mode by default for app UI work unless the user explicitly asks for a single-theme result or the product already enforces one.\n- Favor built-in WinUI controls and system styling hooks before adding CommunityToolkit dependencies, custom controls, or app-specific surface systems.\n- Put detailed control, theming, shell, scrolling, responsiveness, packaging, and recovery guidance in the matching reference files instead of duplicating those rules here.\n"
  },
  {
    "path": "skills/.curated/winui-app/agents/openai.yaml",
    "content": "interface:\n  display_name: \"WinUI App\"\n  short_description: \"[Windows only] Build native WinUI 3 apps\"\n  icon_large: \"./assets/winui.png\"\n  default_prompt: \"Create a new $winui-app desktop app for me.\"\n"
  },
  {
    "path": "skills/.curated/winui-app/config.yaml",
    "content": "# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2\n\n##################################################################################################################################\n# This configuration installs the tools needed to get started building Windows apps with WinUI.                                  #\n#                                                                                                                                #\n# This will:                                                                                                                     #\n#   * Enable Developer Mode                                                                                                      #\n#   * Install Visual Studio Community 2026                                                                                       #\n#   * Install the WinUI / Windows App SDK workloads                                                                              #\n##################################################################################################################################\nproperties:\n  assertions:\n    - resource: Microsoft.Windows.Developer/OsVersion\n      directives:\n        description: Verify min OS version requirement\n        allowPrerelease: true\n      settings:\n        MinVersion: '10.0.17763'\n  resources:\n    - resource: Microsoft.Windows.Settings/WindowsSettings\n      directives:\n        description: Enable Developer Mode\n        securityContext: elevated\n        allowPrerelease: true\n      settings:\n        DeveloperMode: true\n    - resource: Microsoft.WinGet.DSC/WinGetPackage\n      id: Visual Studio\n      directives:\n        description: Install Visual Studio Community 2026\n        securityContext: elevated\n      settings:\n        id: Microsoft.VisualStudio.Community\n        source: winget\n    - resource: Microsoft.VisualStudio.DSC/VSComponents\n      id: Workloads ManagedDesktop\n      dependsOn:\n        - Visual Studio\n      directives:\n        description: Install required VS workloads (ManagedDesktop, Windows App SDK)\n        allowPrerelease: true\n        securityContext: elevated\n      settings:\n        productId: Microsoft.VisualStudio.Product.Community\n        channelId: VisualStudio.18.Release\n        components:\n          - Microsoft.VisualStudio.Workload.ManagedDesktop\n          - Microsoft.VisualStudio.Workload.Universal\n          - Microsoft.VisualStudio.ComponentGroup.WindowsAppSDK.Cs\n  configurationVersion: 0.2.0\n"
  },
  {
    "path": "skills/.curated/winui-app/references/_sections.md",
    "content": "# Reference Sections\n\nUse this index to choose the narrowest reference file that fits the current task.\n\n## 1. Foundations\n\n- `foundation-setup-and-project-selection.md`\n  - Priority: CRITICAL\n  - Use for first-project setup, packaged vs unpackaged decisions, and core WinUI prerequisites.\n  - Authority: Microsoft Learn WinUI and Windows App SDK setup docs.\n\n- `foundation-environment-audit-and-remediation.md`\n  - Priority: CRITICAL\n  - Use for machine readiness checks, missing prerequisites, and guided remediation.\n  - Authority: Microsoft Learn setup and system requirements docs, plus the bundled bootstrap workflow.\n\n- `foundation-winui-app-structure.md`\n  - Priority: HIGH\n  - Use for solution layout, shell composition, resources, bindings, and C#-first project structure.\n  - Authority: WinUI Gallery source plus Learn XAML guidance.\n\n- `foundation-template-first-recovery.md`\n  - Priority: CRITICAL\n  - Use for opaque `MSB3073`, `XamlCompiler.exe`, and startup failures that should be recovered by comparing against a fresh `dotnet new winui` scaffold instead of applying alternate baseline files.\n  - Authority: Learn packaged and unpackaged deployment guidance plus recurring template-first recovery patterns.\n\n- `build-run-and-launch-verification.md`\n  - Priority: CRITICAL\n  - Use for build/run workflows, actual launch verification, startup crashes, and packaged-vs-unpackaged local execution choices.\n  - Authority: Learn setup and deployment guidance plus recurring WinUI troubleshooting patterns.\n\n## 2. Shell, Navigation, and Windowing\n\n- `shell-navigation-and-windowing.md`\n  - Priority: HIGH\n  - Use for `NavigationView`, page shells, title bars, `AppWindow`, and multi-window design.\n  - Authority: Learn design guidance, WinUI Gallery samples, Windows App SDK Windowing samples.\n\n## 3. Controls, Layout, and Adaptive UI\n\n- `controls-layout-and-adaptive-ui.md`\n  - Priority: HIGH\n  - Use for control selection, command surfaces, responsive layout, and page composition.\n  - Authority: Learn design guidance and WinUI Gallery control pages.\n\n## 4. Styling, Theming, Materials, and Icons\n\n- `styling-theming-materials-and-icons.md`\n  - Priority: HIGH\n  - Use for Fluent styling, theme resources, Mica, Acrylic, typography, and iconography.\n  - Authority: Learn design/material docs, WinUI Gallery backdrop samples, Windows App SDK Mica samples.\n\n- `motion-animations-and-polish.md`\n  - Priority: MEDIUM\n  - Use for transitions, connected animation, subtle polish, and animation discipline.\n  - Authority: Learn motion guidance, WinUI Gallery transition samples, CommunityToolkit animations.\n\n## 5. Accessibility, Input, and Localization\n\n- `accessibility-input-and-localization.md`\n  - Priority: HIGH\n  - Use for keyboarding, Narrator, high contrast, automation properties, and localization concerns.\n  - Authority: Learn accessibility and globalization guidance, WinUI Gallery automation patterns.\n\n## 6. Performance and Diagnostics\n\n- `performance-diagnostics-and-responsiveness.md`\n  - Priority: HIGH\n  - Use for UI-thread responsiveness, large item collections, rendering cost, and diagnostic tooling.\n  - Authority: Learn WinUI performance docs and XAML frame analysis guidance.\n\n## 7. Windows App SDK Scenarios\n\n- `windows-app-sdk-lifecycle-notifications-and-deployment.md`\n  - Priority: HIGH\n  - Use for lifecycle, activation, notifications, packaged vs unpackaged deployment, and runtime initialization.\n  - Authority: Microsoft Learn Windows App SDK docs and WindowsAppSDK-Samples.\n\n## 8. CommunityToolkit Extensions\n\n- `community-toolkit-controls-and-helpers.md`\n  - Priority: MEDIUM\n  - Use when built-in WinUI controls are not enough and Toolkit packages might close the gap cleanly.\n  - Authority: CommunityToolkit/Windows packages and samples.\n\n## 9. Testing, Debugging, and Review\n\n- `testing-debugging-and-review-checklists.md`\n  - Priority: HIGH\n  - Use for final review passes, debugging workflows, and validation checklists.\n  - Authority: Learn tooling docs plus recurring WinUI review patterns.\n\n- `sample-source-map.md`\n  - Priority: MEDIUM\n  - Use when you need to know which canonical repo or doc to inspect first for a given task.\n  - Authority: Curated map across Learn, WinUI Gallery, WindowsAppSDK-Samples, and CommunityToolkit.\n"
  },
  {
    "path": "skills/.curated/winui-app/references/accessibility-input-and-localization.md",
    "content": "---\ntitle: Accessibility, Input, and Localization\npriority: HIGH\ntags: accessibility, keyboard, narrator, automation, localization, high-contrast\nsources:\n  - https://learn.microsoft.com/windows/apps/design/accessibility/accessibility\n  - https://learn.microsoft.com/windows/apps/design/accessibility/keyboard-accessibility\n  - https://learn.microsoft.com/windows/apps/design/accessibility/high-contrast-themes\n  - https://learn.microsoft.com/windows/apps/design/globalizing/globalizing-portal\n  - https://github.com/microsoft/WinUI-Gallery\n---\n\n## What This Reference Is For\n\nUse this file for keyboard accessibility, Narrator support, automation properties, input parity, high contrast, and localization-ready UI.\n\n## Prefer\n\n- Accessible names, help text, and landmarks for meaningful UI elements.\n- Full keyboard reachability for the main workflow.\n- High-contrast-safe visuals.\n- Localizable strings and layouts that tolerate growth.\n- Equal support for mouse, touch, pen, and keyboard where the platform expects it.\n\n## Avoid\n\n- Icon-only interactions without accessible naming.\n- Focus traps, hidden tab stops, or keyboard-only dead ends.\n- Hard-coded strings in XAML or code-behind that block localization.\n- Text layouts that collapse when strings expand.\n\n## Guidance\n\n- Use automation properties intentionally.\n- Preserve visible focus and logical tab order.\n- Verify context menus, flyouts, and dialogs by keyboard as well as mouse.\n- Respect text scaling, contrast changes, and RTL where relevant.\n- Keep touch targets and spacing usable on both mouse and touch hardware.\n\n## WinUI Gallery Anchors\n\n- Accessibility-related control samples\n- Automation helper patterns in shell code\n- Standard WinUI controls that already expose useful accessibility behavior\n\n## Review Checklist\n\n- Can a keyboard-only user complete the task?\n- Does Narrator have enough information to describe the important UI?\n- Does the experience stay legible in high contrast?\n- Are strings and layout ready for localization and RTL growth?\n"
  },
  {
    "path": "skills/.curated/winui-app/references/build-run-and-launch-verification.md",
    "content": "---\ntitle: Build, Run, and Launch Verification\npriority: CRITICAL\ntags: build, run, launch, verification, packaged, unpackaged, debugging\nsources:\n  - https://learn.microsoft.com/windows/apps/get-started/start-here\n  - https://learn.microsoft.com/windows/apps/windows-app-sdk/deploy-packaged-apps\n  - https://learn.microsoft.com/windows/apps/windows-app-sdk/deploy-unpackaged-apps\n---\n\n## What This Reference Is For\n\nUse this file when the task involves building, running, launch failures, startup crashes, or final verification that a WinUI app actually opens on the current machine.\n\n## Required Workflow\n\n1. Identify the real build target:\n   - solution or project file\n   - configuration\n   - platform\n   - packaged or unpackaged model\n2. Build after each meaningful code edit and again at task completion.\n3. Run the app after changes when feasible. Always do it when the user asked for it or when startup, navigation, resources, or packaging changed.\n4. Use the launch path that matches the deployment model:\n   - packaged local dev: normally Visual Studio deploy or another package-aware flow\n   - unpackaged local dev: normally the built executable the user will actually run\n5. Verify real launch with objective evidence such as:\n   - non-zero main window handle\n   - expected window title\n   - responsive process with visible shell\n   - no immediate startup exception or crash\n6. After completing app work, including a first scaffold or a later build-and-fix cycle, leave a successfully verified final app instance running so the user can see that it worked unless they explicitly asked you not to.\n7. If launch fails or verification is ambiguous, debug the failure before saying the app is ready.\n\n## Packaged vs Unpackaged Rules\n\n- Choose one model intentionally before wiring startup, persistence, and launch instructions.\n- Packaged apps can rely on package identity and package-backed storage.\n- Unpackaged apps must not assume package identity. Guard or replace APIs that require it.\n- APIs such as `Windows.Storage.ApplicationData.Current` can fail in unpackaged runs even when the build succeeds.\n- Do not mix packaged-only assumptions into an unpackaged startup path.\n\n## Build and Launch Guidance\n\n- Prefer explicit platform targets when WinUI output is sensitive to architecture defaults. If `AnyCPU` creates ambiguity, use `x64` for local verification.\n- For unpackaged verification, prefer launching the built `.exe` from `bin\\Debug\\...\\win-x64\\` or the project-specific output path.\n- After a successful final launch verification, do not immediately tear the app down just because verification succeeded; keep it open for the user unless it blocks the next required action.\n- If `dotnet run` throws bootstrapper, deployment, or COM activation errors, treat that as a signal that the chosen launch path or packaging setup is wrong for the current app.\n- Stop old app instances before rebuilding if they can lock output files.\n\n## Debugging Startup Failures\n\n- Separate environment problems from app-code startup crashes.\n- If the app exits before showing a window, inspect the startup path first:\n  - `App.xaml`\n  - merged resource dictionaries\n  - converters\n  - `MainWindow`\n  - services used during startup\n- For startup or manifest issues, compare the current app against a fresh `dotnet new winui` scaffold for the same packaging model before broader surgery.\n- For opaque `MSB3073` and `XamlCompiler.exe` failures, simplify back toward the template-generated startup and shared-resource shape before making further structural changes.\n- Restore complex startup pieces incrementally when the failure point is unclear. A minimal `App.xaml` plus minimal `MainWindow` is a valid isolation step.\n- If the diagnostics look stale or inconsistent with the current files, run a clean build once before deeper surgery.\n- Prefer restoring the last known-good template-based shared-resource state over moving styles inline as the long-term fix.\n- When using unpackaged startup, review persistence, notifications, storage, and activation code for hidden package-identity assumptions.\n\n## Exit Criteria\n\n- Build succeeds from the intended local workflow.\n- The app launches from the intended local workflow.\n- A real top-level window or equivalent expected UI is confirmed.\n- No unresolved startup exception remains.\n"
  },
  {
    "path": "skills/.curated/winui-app/references/community-toolkit-controls-and-helpers.md",
    "content": "---\ntitle: CommunityToolkit Controls and Helpers\npriority: MEDIUM\ntags: communitytoolkit, controls, helpers, animations, settingscontrols\nsources:\n  - https://github.com/CommunityToolkit/Windows\n  - https://learn.microsoft.com/dotnet/communitytoolkit/windows/getting-started\n---\n\n## What This Reference Is For\n\nUse this file when deciding whether the Windows Community Toolkit should be added to a WinUI 3 app.\n\n## Prefer\n\n- Platform controls first.\n- Targeted Toolkit package additions for clear gaps such as richer settings surfaces, segmented controls, or focused animation helpers.\n- The smallest package set that solves the problem.\n\n## Avoid\n\n- Adding Toolkit packages because they look convenient without checking whether WinUI already covers the need.\n- Pulling in multiple Toolkit packages for a minor visual difference.\n- Hiding fundamental UX problems behind a new dependency.\n\n## Good Candidate Areas\n\n- `SettingsControls`\n  - useful for settings surfaces and cards\n- `Segmented`\n  - useful when segmented selection is clearer than a tab or radio cluster\n- `HeaderedControls`\n  - useful for labeled control groupings\n- `Animations`\n  - useful when built-in transitions are not enough\n- helpers and extensions\n  - useful when they reduce repetitive WinUI plumbing cleanly\n\n## Package Guidance\n\n- Prefer WinUI 3 compatible Toolkit packages.\n- Add only what the app will actually use.\n- Document why a Toolkit dependency was added and what built-in alternative was rejected.\n\n## Sample and Source Anchors\n\n- CommunityToolkit `components/SettingsControls`\n- CommunityToolkit `components/Segmented`\n- CommunityToolkit `components/HeaderedControls`\n- Toolkit animations and helper packages\n\n## Review Checklist\n\n- Does built-in WinUI already solve the problem?\n- Is the dependency narrowly scoped and justified?\n- Does the new control match the rest of the app’s design language?\n- Will the package meaningfully reduce custom code or improve UX?\n"
  },
  {
    "path": "skills/.curated/winui-app/references/controls-layout-and-adaptive-ui.md",
    "content": "---\ntitle: Controls, Layout, and Adaptive UI\npriority: HIGH\ntags: controls, layout, adaptive-ui, responsive, forms, lists\nsources:\n  - https://learn.microsoft.com/windows/apps/design/layout/responsive-design\n  - https://learn.microsoft.com/windows/apps/design/basics/navigation-basics\n  - https://github.com/microsoft/WinUI-Gallery\n---\n\n## What This Reference Is For\n\nUse this file when choosing controls, composing pages, or making a WinUI layout adapt well to different window sizes and input modes.\n\n## Prefer\n\n- Built-in WinUI controls first.\n- Native command surfaces such as `CommandBar` when the UI is grouping actions, toggles, and lightweight tool controls.\n- Standard controls for common tasks: `TextBox`, `NumberBox`, `ComboBox`, `ListView`, `GridView`, `ContentDialog`, `InfoBar`, `TeachingTip`, `TabView`, `NavigationView`.\n- Explicit scroll ownership for collection layouts. If the page already scrolls vertically, prefer giving a media shelf its own horizontal `ScrollViewer` and a simple horizontal panel.\n- Responsive techniques such as reposition, resize, reflow, and show/hide.\n- Layouts that remain usable when the window becomes narrow.\n- A real phone-width plan when the app may be resized that far: fewer columns, reduced padding, simplified controls, and stacked content instead of compressed desktop rails.\n\n## Avoid\n\n- Replacing standard WinUI controls with custom controls just to change appearance.\n- Building custom toolbar rows out of generic layout panels when a stock `CommandBar` would cover the grouping cleanly.\n- Hard-coded sizes that only look correct at one window width.\n- Dense desktop-only layouts that break touch or keyboard workflows.\n- Adding extra controls for local filtering or sorting when live updates and a simpler layout would better match the workflow.\n- Nesting a scroll-owning `GridView` inside an outer page `ScrollViewer` without deciding which control owns scrolling; this often produces a single vertical column or awkward scroll conflicts instead of a horizontal media shelf.\n- Wrapping list sections or card groups in an extra `Border` when the section header, spacing, and child surfaces already establish grouping.\n\n## Control Selection Guidance\n\n- Forms and settings:\n  - Prefer native controls first; add Toolkit settings controls only if the experience clearly benefits.\n- Command surfaces:\n  - Prefer `CommandBar` for grouped document, formatting, view, and page-level actions before composing a custom bar from `Grid`, `StackPanel`, `Border`, and loose buttons.\n  - Prefer the `CommandBar` overflow model for secondary actions before splitting the command surface into multiple custom rows.\n  - Fall back to a custom command layout only when a verified `CommandBar` limitation, an explicit product design requirement, or unusual content composition makes the native surface a poor fit.\n- Large collections:\n  - Prefer controls with virtualization-friendly behavior.\n  - Use `GridView` when it owns the collection surface and its scrolling behavior is part of the intended experience.\n  - For poster rails or other horizontal shelves inside a vertically scrolling page, prefer a horizontal `ScrollViewer` containing an `ItemsControl` or `ItemsRepeater` with a horizontal panel instead of a nested `GridView`.\n  - Consider `ItemsRepeater` when the layout is custom and performance matters.\n- Search and filtering:\n  - Prefer a single search field with live updates for local or otherwise inexpensive filtering.\n  - Add explicit apply, refresh, or mode-selection controls only when the underlying operation is expensive, remote, asynchronous, or semantically different.\n- Dialogs and transient guidance:\n  - Use `ContentDialog` for modal decisions.\n  - Use `InfoBar` for persistent status.\n  - Use `TeachingTip` for contextual onboarding.\n\n## Adaptive Layout Guidance\n\n- Design with effective pixels, not fixed device assumptions.\n- Make the smallest supported layout fully usable.\n- Add density or multi-column views only when width allows.\n- Use visual states, adaptive triggers, or layout state changes intentionally.\n- Keep commands and primary content reachable after resize.\n- Verify collection orientation and scrolling behavior at runtime. A shelf that looks horizontal in XAML can still render as a vertical stack once nested scroll regions are involved.\n- When simplifying a dense section, remove redundant outer surfaces before adding more adaptive layout rules; fewer layers usually adapt more cleanly across breakpoints.\n- Define breakpoint intent explicitly. Typical questions: when does a shelf become a stacked list, when does a footer drop nonessential controls, and when does the page stop behaving like a desktop canvas and become a single-column phone layout?\n- Simplify as width shrinks. Prefer dropping secondary controls or moving them behind shell affordances over preserving every control at every breakpoint.\n- When a page contains desktop-oriented horizontal shelves, add a phone-width alternative that stacks items vertically instead of relying on clipped rails and horizontal scrolling everywhere.\n\n## WinUI Gallery Anchors\n\n- Control pages for built-in WinUI control usage\n- Gallery home and shell pages for adaptive layout ideas\n- Sample pages for title bar and system backdrop interactions with content layout\n\n## Review Checklist\n\n- Did you choose the simplest built-in control that fits?\n- Are search and filter controls no more complex than the data flow requires?\n- Does the page remain usable when narrow?\n- Can keyboard, mouse, and touch all reach the same core actions?\n- Are spacing and hierarchy consistent across breakpoints?\n- If the page mixes page scrolling with collection scrolling, is it obvious which control owns vertical scrolling and which one, if any, owns horizontal shelf scrolling?\n- Are section containers doing real layout or surface work, or are some outer borders now redundant?\n- At phone width, does the page read as a coherent single-column flow instead of a squeezed desktop layout?\n"
  },
  {
    "path": "skills/.curated/winui-app/references/foundation-environment-audit-and-remediation.md",
    "content": "---\ntitle: Environment Audit and Remediation\npriority: CRITICAL\ntags: setup, audit, install, dotnet, visual-studio, windows-sdk, developer-mode\nsources:\n  - https://learn.microsoft.com/windows/apps/get-started/start-here\n  - https://learn.microsoft.com/windows/apps/windows-app-sdk/system-requirements\n  - https://learn.microsoft.com/windows/apps/get-started/developer-mode-features-and-debugging\n  - https://learn.microsoft.com/dotnet/core/install/windows\n---\n\n## What This Reference Is For\n\nUse this file for machine-readiness checks, build failures caused by missing tools, and any request to install WinUI prerequisites.\n\n## Required Workflow\n\n1. Use the setup-and-scaffold flow in [../SKILL.md](../SKILL.md) for environment readiness, remediation, and initial verification.\n2. If the user asked only for an audit and not for setup, explain that the bundled bootstrap may change the machine and get confirmation before running it.\n3. If the user declines machine changes, run a manual non-mutating audit instead and summarize the result under four headings:\n   - present\n   - missing\n   - uncertain\n   - recommended optional tools\n4. Manual non-mutating audit coverage should focus on:\n   - OS version and build floor\n   - Developer Mode state when relevant to the task\n   - `dotnet --list-sdks`\n   - `dotnet new list winui`\n   - Visual Studio presence and edition\n   - Windows SDK presence\n   - MSBuild availability for XAML compilation\n5. If prerequisites are still missing after the bundled setup flow, stop and report the blocker clearly instead of inventing alternate install recipes.\n\n## Required vs Optional\n\nRequired for normal C# WinUI 3 development:\n\n- Supported Windows build\n- Visual Studio with WinUI C# support\n- Windows SDK 10.0.19041.0 or later\n- MSBuild available for XAML compilation\n- .NET SDK 6 or later\n\nUsually optional, but often recommended:\n\n- Developer Mode for local deploy and debug\n- WinGet for one-command remediation\n- Visual Studio debugging features such as Hot Reload and Live Visual Tree\n\n## Prefer\n\n- The setup-and-scaffold flow in `SKILL.md` over ad hoc manual checks or duplicated setup instructions in this reference.\n- A short manual audit only when the user wants a non-mutating readiness check.\n\n## Avoid\n\n- Rewriting or paraphrasing the bundled setup workflow here when `SKILL.md` already covers the user's goal.\n- Marking workload detection as present when the bootstrap or manual audit leaves uncertainty.\n- Branching into custom per-component install steps unless the user explicitly asks for them.\n- Treating Developer Mode as a hard requirement for every task.\n\n## Remediation Strategy\n\n- Missing any required WinUI prerequisite:\n  - Use the setup-and-scaffold flow in `SKILL.md` after confirmation when the request is audit-only.\n- The bundled setup flow reports a partial failure but the toolchain appears usable:\n  - Note the partial failure and continue when the user's task can proceed.\n- The bundled setup flow fails and prerequisites still appear to be missing:\n  - Use the manual audit checks above for detail if needed, then stop and report the blocker clearly.\n- Windows build unsupported:\n  - Upgrade Windows first. The WinUI bootstrap command does not replace the OS requirement.\n- Developer Mode disabled:\n  - Explain whether the current task needs it.\n  - If it does, prefer the bundled setup flow or let the user enable it manually.\n\n## Review Checklist\n\n- Was the setup-and-scaffold flow in `SKILL.md` used before advice was given?\n- Are missing items clearly separated from uncertain signals?\n- Is the remediation plan the minimum needed for the user's goal?\n- Was post-install verification handled by the bundled setup flow or by a clearly justified fallback?\n"
  },
  {
    "path": "skills/.curated/winui-app/references/foundation-setup-and-project-selection.md",
    "content": "---\ntitle: Setup and Project Selection\npriority: CRITICAL\ntags: setup, prerequisites, packaged, unpackaged, visual-studio, dotnet\nsources:\n  - https://learn.microsoft.com/windows/apps/get-started/start-here\n  - https://learn.microsoft.com/windows/apps/winui/winui3/\n  - https://learn.microsoft.com/windows/apps/windows-app-sdk/\n  - https://learn.microsoft.com/windows/apps/windows-app-sdk/system-requirements\n---\n\n## What This Reference Is For\n\nUse this file when the user is starting from scratch, choosing a project template, or asking what a WinUI machine needs before code work begins.\n\n## Prefer\n\n- The setup-and-scaffold flow in [../SKILL.md](../SKILL.md) for prerequisite setup, template verification, and the first scaffold.\n- A C# WinUI 3 desktop app on the Windows App SDK unless the user has a clear reason to prefer C++ or an existing non-WinUI stack.\n- Official project templates and default packaging choices first.\n- The current supported LTS .NET SDK for new C# work instead of only meeting the bare minimum.\n- A packaged app by default for the smoothest first-project, deployment, and Store-compatible path.\n- An unpackaged app when the user explicitly needs repeatable CLI build-and-run verification or direct executable launches as the normal local workflow.\n\n## Avoid\n\n- Starting project setup before the setup-and-scaffold flow in this skill has finished.\n- Starting with unpackaged deployment unless the user needs repeatable CLI launch, an installer, existing desktop app integration, or a deliberate runtime strategy.\n- Giving machine-readiness advice without verification.\n- Treating old Windows builds, missing SDKs, or partial Visual Studio installs as \"probably fine.\"\n- Deferring the packaging choice until after startup, storage, and launch code are already written.\n\n## Setup Baseline\n\n- Use the setup-and-scaffold flow in [../SKILL.md](../SKILL.md) for prerequisite setup, template verification, and the first scaffold.\n- Treat [../config.yaml](../config.yaml) as the bundled WinGet bootstrap source for setup and remediation.\n- Return to this reference only after that workflow completes or when the task moves beyond initial project creation.\n- Windows 10 version 1809 (build 17763) or later is the floor.\n- Windows SDK 10.0.19041.0 or later is the practical baseline.\n- Visual Studio with the WinUI application development workload is the supported primary IDE path.\n- For C# apps, a supported .NET SDK must be installed.\n- Developer Mode matters for common local deploy and debug flows.\n\n## Project Selection Guidance\n\n- Choose packaged when the user wants the default WinUI 3 path, easy local F5 workflows, or Store-friendly deployment. Keep the scaffold at its default unless the user explicitly asks for unpackaged behavior.\n- Choose packaged when the app needs package identity or package-backed APIs during normal operation.\n- Choose unpackaged when the user expects direct `.exe` launches, agent-driven local verification after each change, or integration with an existing installer or external location. Request that option through the setup flow instead of converting the initial project afterward.\n- For either packaging model, scaffold first through the setup flow in `SKILL.md` and continue from the generated project instead of copying in prebuilt baseline files.\n- If startup or shared resources later become suspect, create a fresh comparison app with the same packaging model and diff against that `dotnet new winui` output before broader restructuring.\n- Once the model is chosen, keep startup and service code consistent with that model.\n- Choose the standard blank app template first, then layer in navigation, title bar, or windowing patterns as the app matures.\n\n## Sample and Source Anchors\n\n- Learn `start-here.md` for the current official setup path.\n- Learn `winui/winui3/index.md` for the framework position and platform benefits.\n- Learn `windows-app-sdk/index.md` for the Windows App SDK feature surface.\n- Learn `system-requirements.md` for tool and OS baselines.\n\n## Review Checklist\n\n- Is the machine baseline actually verified through the setup-and-scaffold flow in `SKILL.md`?\n- Is the chosen packaging model intentional?\n- Does the launch workflow match the chosen packaging model?\n- Is the app still rooted in the standard WinUI template unless there is a real reason not to?\n- Is the recommendation aligned with a C#-first WinUI 3 workflow?\n"
  },
  {
    "path": "skills/.curated/winui-app/references/foundation-template-first-recovery.md",
    "content": "---\ntitle: Template-First Recovery for Startup and XAML Failures\npriority: CRITICAL\ntags: template, recovery, xaml-compiler, msb3073, startup\nsources:\n  - https://learn.microsoft.com/windows/apps/get-started/start-here\n  - https://learn.microsoft.com/windows/apps/windows-app-sdk/deploy-packaged-apps\n  - https://learn.microsoft.com/windows/apps/windows-app-sdk/deploy-unpackaged-apps\n  - https://github.com/microsoft/WinUI-Gallery\n---\n\n## What This Reference Is For\n\nUse this file when a new app should stay close to the `dotnet new winui` scaffold, or when opaque `MSB3073`, `XamlCompiler.exe`, and startup failures make it unclear whether the problem is in app code, shared resources, or the surrounding project structure.\n\n## Prefer\n\n- Scaffold with the standard `dotnet new winui` template first and keep the generated project file, manifests, assets, and startup shape unless the task explicitly requires broader changes.\n- Match any comparison scaffold to the app's actual packaging model.\n- Keep `App.xaml` minimal while isolating startup problems.\n- Prefer explicit `new Window()` and avoid `Window.Current` when customizing WinUI 3 startup.\n- Reintroduce shell, resources, bindings, and services incrementally after a clean build and launch.\n\n## Avoid\n\n- Swapping in alternate baseline files or helper scripts as the first recovery move.\n- Replacing the template-generated `.csproj` or manifests during initial isolation.\n- Flattening all styles into page-local markup as the permanent fix for opaque compiler failures.\n- Treating `MSB3073` as proof that the most recently edited XAML line is the only fault.\n\n## Template-First Recovery Loop\n\n1. Confirm the intended packaging model and launch path.\n2. If the current startup shape is unclear, scaffold a temporary comparison app with the same packaging choice. Example:\n   - `dotnet new winui -n RecoveryReference -o RecoveryReference --use-slnx false --no-solution-file false`\n   - Add `--unpackaged true` when the target app is unpackaged.\n3. Diff only the startup and shared-resource areas against that comparison scaffold:\n   - `App.xaml`\n   - `App.xaml.cs`\n   - `MainWindow.xaml` / `MainWindow.xaml.cs` or the app's actual shell entry point\n   - merged resource dictionaries\n   - startup-related project properties\n4. Revert the suspect area toward the template-generated shape until the app builds cleanly again.\n5. Build explicitly for a concrete architecture. Example:\n   - `dotnet build MyApp.sln -c Debug -p:Platform=x64`\n6. Launch using the correct packaged or unpackaged path and confirm objective startup signals.\n7. Reapply custom changes in small slices, building and running after each meaningful edit.\n\n## Common Recovery Checks\n\n- Confirm `Window.Current` is not used in WinUI 3 startup code.\n- Confirm `x:Class`, namespaces, and code-behind names still match.\n- Confirm merged resource dictionaries load cleanly before adding more layers.\n- Confirm project content items still match any local data or asset files the app expects at runtime.\n- Run one clean build if diagnostics appear stale.\n\n## Exit Criteria\n\n- The current app is still rooted in the generated `dotnet new winui` scaffold rather than an alternate baseline shell.\n- Build succeeds from the intended local workflow.\n- The app launches from the intended local workflow.\n- A real top-level window or equivalent expected UI is confirmed.\n"
  },
  {
    "path": "skills/.curated/winui-app/references/foundation-winui-app-structure.md",
    "content": "---\ntitle: WinUI App Structure\npriority: HIGH\ntags: app-structure, xaml, resources, pages, bindings, csharp\nsources:\n  - https://github.com/microsoft/WinUI-Gallery\n  - https://learn.microsoft.com/windows/apps/winui/\n---\n\n## What This Reference Is For\n\nUse this file when structuring a WinUI 3 app, reviewing project layout, or deciding where shell, pages, controls, resources, and view models should live.\n\n## Prefer\n\n- A clear C#-first folder split such as `Pages`, `Controls`, `ViewModels`, `Services`, `Styles`, and `Assets`.\n- `App.xaml` and shared resource dictionaries for app-wide theme resources and styles.\n- A single main shell window that owns navigation and common chrome.\n- Native command surfaces such as `CommandBar` for grouped window or page actions before inventing a custom toolbar composition.\n- Strongly typed `x:Bind` where it improves compile-time safety and performance.\n\n## Avoid\n\n- Putting shell logic, page logic, and resource definitions into one large window file.\n- Scattering theme brushes and styles across many page-local dictionaries.\n- Introducing MVVM ceremony that the project will not actually maintain.\n\n## Recommended Shape\n\n- `App.xaml` / `App.xaml.cs`\n  - global resources, startup, window creation, app-level exceptions\n- `MainWindow.xaml` / `MainWindow.xaml.cs`\n  - shell, title bar, top-level navigation host\n- `Pages/`\n  - page views and page-specific logic\n- `Controls/`\n  - reusable WinUI user controls\n- `ViewModels/`\n  - state and commands when the app benefits from separation\n- `Styles/`\n  - resource dictionaries, theme tokens, shared control styles\n- `Helpers/` or `Services/`\n  - windowing, navigation, persistence, OS integration helpers\n\n## Binding Guidance\n\n- Prefer `x:Bind` for page-local properties, event handlers, and strongly typed view model access.\n- Use `Binding` where the data context is dynamic or a template must stay flexible.\n- Avoid binding patterns that depend on unclear page lifetime or implicit data contexts.\n\n## WinUI Gallery Anchors\n\n- `App.xaml.cs` shows app-level startup and integration points.\n- `MainWindow.xaml` shows shell composition, title bar usage, and search integration.\n- `Pages/` and `Samples/` show how Microsoft organizes pages, helpers, and styles in a real WinUI companion app.\n\n## Review Checklist\n\n- Are app resources centralized?\n- Is shell logic separated from content pages?\n- Are bindings explicit and maintainable?\n- Is the structure consistent with the scale of the app?\n"
  },
  {
    "path": "skills/.curated/winui-app/references/motion-animations-and-polish.md",
    "content": "---\ntitle: Motion, Animations, and Polish\npriority: MEDIUM\ntags: motion, animations, transitions, connected-animation, polish\nsources:\n  - https://learn.microsoft.com/windows/apps/design/motion/\n  - https://github.com/microsoft/WinUI-Gallery\n  - https://github.com/CommunityToolkit/Windows\n---\n\n## What This Reference Is For\n\nUse this file when adding polish to a WinUI app through motion, transitions, and subtle animated state changes.\n\n## Prefer\n\n- Motion that clarifies hierarchy, continuity, and state changes.\n- Theme transitions, connected animations, and built-in platform behaviors before custom animation systems.\n- Short, purposeful animations that support the task.\n\n## Avoid\n\n- Decorative animation that delays interaction.\n- Multiple overlapping animations for the same state change.\n- Animation that hides focus, selection, or accessibility state.\n\n## Guidance\n\n- Use transitions to explain where content came from and where it went.\n- Keep entrance and exit motion subtle.\n- Use connected animation when there is a real source-to-destination relationship.\n- Reach for CommunityToolkit animation helpers only when built-in transitions are not enough.\n\n## Sample and Source Anchors\n\n- WinUI Gallery animation, transition, and implicit animation pages\n- Learn motion guidance\n- CommunityToolkit animations package and samples\n\n## Review Checklist\n\n- Does the motion improve clarity?\n- Is the app still responsive while the animation runs?\n- Can the transition be simplified to a built-in WinUI behavior?\n- Does the motion preserve accessibility and input clarity?\n"
  },
  {
    "path": "skills/.curated/winui-app/references/performance-diagnostics-and-responsiveness.md",
    "content": "---\ntitle: Performance, Diagnostics, and Responsiveness\npriority: HIGH\ntags: performance, responsiveness, ui-thread, wpr, wpa, diagnostics\nsources:\n  - https://learn.microsoft.com/windows/apps/performance/winui-perf\n  - https://github.com/microsoft/WinUI-Gallery\n---\n\n## What This Reference Is For\n\nUse this file when the user reports sluggish WinUI behavior, dropped frames, long startup, or laggy scrolling and layout.\n\n## Prefer\n\n- Keeping the UI thread free for layout, rendering, and input.\n- Simpler visual trees and lighter templates.\n- Virtualization-friendly controls and item layouts.\n- Measurement before optimization when the issue is not obvious.\n\n## Avoid\n\n- Doing expensive I/O or CPU work directly on the UI thread.\n- Deeply nested XAML trees without a concrete benefit.\n- Re-templating controls in ways that dramatically increase layout work.\n- Guessing at performance causes without profiling.\n\n## Guidance\n\n- Favor platform controls and layouts that virtualize well for long lists.\n- Defer or background heavy work when it does not need to block interaction.\n- Reduce unnecessary layout invalidation and repeated measure/arrange churn.\n- Use WPR and WPA with the XAML Frame Analysis plugin for frame-level investigations.\n- Treat slow-frame findings as a clue to UI-thread overload, not as a reason to micro-optimize blindly.\n\n## Sample and Source Anchors\n\n- Learn `winui-perf.md`\n- WinUI Gallery pages that demonstrate adaptive UI and complex controls without excessive custom infrastructure\n\n## Review Checklist\n\n- Is heavy work running off the UI thread where possible?\n- Are large collections using an appropriate items control?\n- Is the visual tree no more complex than it needs to be?\n- Has profiling been used before claiming a fix?\n"
  },
  {
    "path": "skills/.curated/winui-app/references/sample-source-map.md",
    "content": "---\ntitle: Sample and Source Map\npriority: MEDIUM\ntags: sources, mapping, lookup, gallery, docs, toolkit\nsources:\n  - https://learn.microsoft.com/windows/apps/get-started/samples\n  - https://github.com/microsoft/WinUI-Gallery\n  - https://github.com/microsoft/WindowsAppSDK-Samples\n  - https://github.com/CommunityToolkit/Windows\n---\n\n## What This Reference Is For\n\nUse this file when you know the task but need to identify the best canonical source to inspect first.\n\n| Task | First source | Backup source |\n| --- | --- | --- |\n| Check whether a PC can build WinUI apps | `../SKILL.md` | `foundation-environment-audit-and-remediation.md` |\n| Install missing prerequisites | `../SKILL.md` | `foundation-environment-audit-and-remediation.md` |\n| Start a new packaged or unpackaged app | `../SKILL.md` | `foundation-setup-and-project-selection.md` |\n| Choose packaged vs unpackaged | Learn Windows App SDK deployment docs | WindowsAppSDK-Samples `Samples/Unpackaged` |\n| Build a shell with navigation | WinUI Gallery navigation pages | Learn navigation basics |\n| Design a custom title bar | Learn title bar guidance | WinUI Gallery title bar samples |\n| Add Mica or system backdrops | Learn Mica guidance | WindowsAppSDK-Samples `Samples/Mica` |\n| Design a settings page | WinUI Gallery control pages | CommunityToolkit `SettingsControls` |\n| Pick a control for a list or collection | WinUI Gallery control pages | Learn responsive/layout guidance |\n| Improve accessibility | Learn accessibility docs | WinUI Gallery standard control behavior |\n| Diagnose responsiveness | Learn `winui-perf.md` | WPR/WPA guidance in `testing-debugging-and-review-checklists.md` |\n| Add notifications or activation flows | WindowsAppSDK-Samples | Learn Windows App SDK lifecycle docs |\n| Decide whether to add CommunityToolkit | `community-toolkit-controls-and-helpers.md` | Toolkit component directories |\n\n## Source Preferences\n\n- Learn first for requirements and behavioral guidance.\n- WinUI Gallery first for concrete control usage and shell composition.\n- WindowsAppSDK-Samples first for scenario APIs and platform integration.\n- CommunityToolkit only when the task clearly requires Toolkit-specific functionality.\n"
  },
  {
    "path": "skills/.curated/winui-app/references/shell-navigation-and-windowing.md",
    "content": "---\ntitle: Shell, Navigation, and Windowing\npriority: HIGH\ntags: navigationview, titlebar, appwindow, multi-window, shell\nsources:\n  - https://learn.microsoft.com/windows/apps/design/basics/navigation-basics\n  - https://learn.microsoft.com/windows/apps/design/basics/titlebar-design\n  - https://github.com/microsoft/WinUI-Gallery\n  - https://github.com/microsoft/WindowsAppSDK-Samples/tree/main/Samples/Windowing\n---\n\n## What This Reference Is For\n\nUse this file for top-level app shells, page navigation models, custom title bars, and multi-window decisions.\n\n## Prefer\n\n- `NavigationView` for standard desktop shells with clear top-level destinations.\n- A small, stable set of primary destinations.\n- Built-in back navigation behavior that matches user expectations.\n- `AppWindow` and Windows App SDK windowing APIs for modern window management.\n\n## Avoid\n\n- Overloading the nav surface with every command and secondary action.\n- Turning the `NavigationView` pane into a branded hero area when the user did not ask for custom shell treatment.\n- Custom title bar layouts that break drag regions or caption button clarity.\n- Multi-window designs unless the workflow clearly benefits from them.\n\n## Navigation Guidance\n\n- Use left navigation when the app has several stable, high-level destinations.\n- Use top navigation when there are few peer destinations and width is available.\n- Use a single-page or document-first layout when navigation is shallow and the user mostly stays in one workflow.\n- Keep naming and iconography stable across pages.\n- Treat `NavigationView` as functional shell chrome first. Keep pane headers, footer content, and decorative branding minimal unless the product requirements clearly call for them.\n- Prefer the platform's normal pane structure before adding custom logo blocks, taglines, or non-navigation content that changes the shell's native feel.\n- For narrow or phone-like widths, stop reserving permanent pane width for desktop navigation. Prefer a minimal or overlay navigation mode, show the pane toggle when needed, close the pane by default after navigation, and give content the width back.\n- When a shell enters a phone-width mode, reduce content padding and decorative chrome so the page reads as one primary column instead of a desktop shell with a squeezed content strip.\n\n## Title Bar Guidance\n\n- Treat the title bar as functional chrome first, branding surface second.\n- Keep empty non-interactive areas draggable.\n- Blend title bar visuals with the rest of the app when possible.\n- Respect light, dark, and high-contrast states.\n\n## Windowing Guidance\n\n- Start with one main window.\n- Add secondary windows only for workflows such as document detachment, inspection panes, or tool windows.\n- Use Windows App SDK samples for resizing, placement, and window-specific behaviors instead of inventing custom platform abstractions.\n\n## Sample and Source Anchors\n\n- WinUI Gallery `NavigationView`, `TitleBar`, `AppWindow`, and windowing sample pages\n- WindowsAppSDK-Samples `Samples/Windowing`\n- Learn navigation and title bar guidance\n\n## Review Checklist\n\n- Is the navigation model simple and intentional?\n- Does the shell still look and behave like a normal WinUI `NavigationView` unless there is an explicit reason to diverge?\n- Does the title bar still behave like a Windows title bar?\n- Are back, search, and pane behaviors consistent?\n- Is multi-window use justified by the workflow?\n- Does the shell intentionally switch behavior at narrow or phone widths instead of leaving a full desktop pane open?\n"
  },
  {
    "path": "skills/.curated/winui-app/references/styling-theming-materials-and-icons.md",
    "content": "---\ntitle: Styling, Theming, Materials, and Icons\npriority: HIGH\ntags: styling, theme-resources, mica, acrylic, typography, icons\nsources:\n  - https://learn.microsoft.com/windows/apps/design/style/mica\n  - https://learn.microsoft.com/windows/apps/design/style/acrylic\n  - https://learn.microsoft.com/windows/apps/design/signature-experiences/typography\n  - https://learn.microsoft.com/windows/apps/design/signature-experiences/iconography\n  - https://github.com/microsoft/WinUI-Gallery\n  - https://github.com/microsoft/WindowsAppSDK-Samples/tree/main/Samples/Mica\n---\n\n## What This Reference Is For\n\nUse this file for Fluent styling choices, theme resources, Mica or Acrylic usage, custom title bar visuals, typography, and iconography.\n\n## Prefer\n\n- Theme resources and system brushes over hard-coded colors.\n- Standard WinUI surface resources and default control chrome before custom panel systems.\n- Mica on long-lived surfaces such as the main window background or title bar region.\n- Acrylic on transient or light-dismiss surfaces.\n- Segoe UI Variable or platform-default typography choices.\n- Fluent iconography that matches the platform language.\n- When metadata needs a visual container, prefer small rounded rectangles or subtle badges over bright oval pills.\n\n## Avoid\n\n- Hard-coded light-theme colors that break dark or high-contrast themes.\n- Wrapping every region in a custom `Border` with a bespoke corner radius, stroke, and fill when standard WinUI surfaces would do the job.\n- Adding an outer section `Border` around content that is already visually grouped by card controls, spacing, or headers; this often creates a redundant \"card around cards\" effect.\n- Using Acrylic where Mica or a simple theme-aware surface would be cheaper and clearer.\n- Mixing unrelated icon styles.\n- Filling lists or cards with rows of decorative oval chips for routine metadata. Use tag treatments sparingly, and default to rounded rectangles when they are justified.\n\n## Theming Guidance\n\n- Support light, dark, and high-contrast by default.\n- Centralize brushes, typography, and corner/spacing decisions in shared resource dictionaries.\n- Let built-in controls keep their platform behavior unless there is a strong design reason to customize them.\n- When a grouped surface is needed, prefer system resources such as `CardBackgroundFillColorDefaultBrush`, `CardStrokeColorDefaultBrush`, and `LayerFillColorDefaultBrush` instead of inventing a parallel surface language.\n- If child content already uses card-like surfaces, prefer removing the outer section border and relying on layout spacing and typography for grouping unless the section needs its own distinct background, inset, or stroke.\n\n## Materials Guidance\n\n- Use Mica for long-lived base layers.\n- Use Acrylic for transient surfaces such as flyouts and menus.\n- Verify fallback behavior on older Windows versions or unsupported scenarios.\n\n## Icon and Typography Guidance\n\n- Use standard Windows iconography and keep visual weight consistent.\n- Use typography to create hierarchy instead of adding extra borders or decoration.\n- Keep title bar text and document titles aligned with Windows guidance.\n\n## Sample and Source Anchors\n\n- Learn material, typography, and iconography guidance\n- WinUI Gallery system backdrop and styling pages\n- WindowsAppSDK-Samples `Samples/Mica`\n\n## Review Checklist\n\n- Are colors and brushes theme-aware?\n- Does the app look correct in light, dark, and high contrast?\n- Is the selected material appropriate for the surface lifetime?\n- Are icon and typography choices consistent with Fluent design?\n- Are standard WinUI surfaces doing most of the visual work, with custom borders limited to clearly justified cases?\n- Are there any redundant outer borders that could be removed without losing hierarchy or usability?\n- Are tag or chip treatments sparse, visually quiet, and not rendered as default oval pills unless the product explicitly calls for that style?\n"
  },
  {
    "path": "skills/.curated/winui-app/references/testing-debugging-and-review-checklists.md",
    "content": "---\ntitle: Testing, Debugging, and Review Checklists\npriority: HIGH\ntags: testing, debugging, review, hot-reload, live-visual-tree, checklists\nsources:\n  - https://learn.microsoft.com/windows/apps/get-started/start-here\n  - https://learn.microsoft.com/windows/apps/get-started/developer-mode-features-and-debugging\n  - https://learn.microsoft.com/windows/apps/performance/winui-perf\n---\n\n## What This Reference Is For\n\nUse this file for final review passes, debugging sessions, and \"what should I verify before I call this done?\" prompts.\n\n## Required Verification Loop\n\n- Build after each meaningful edit, not only at the end.\n- Run the app after changes when the user asked for it or when startup-sensitive files changed.\n- Verify actual launch instead of assuming success from a spawned process.\n- If the app fails before showing a window, debug the startup path before continuing feature work.\n\n## Design Review Checklist\n\n- Shell and navigation are simple and predictable.\n- `NavigationView` still reads like standard WinUI shell chrome unless the product explicitly calls for branded pane content or custom shell composition.\n- Layout stays usable when the window is narrow.\n- Layout has been checked at more than one breakpoint, including a genuinely phone-like width when the app can be resized that far.\n- Collection pages with mixed scroll regions have been checked at runtime so shelves still render in the intended direction and do not collapse into a single vertical column.\n- Theme, contrast, hierarchy, and interactive state visibility hold up in both light and dark mode, and typography and iconography still feel native to Windows.\n- Command placement and hierarchy are clear.\n- Default WinUI surfaces and control templates carry most of the layout instead of a custom border/card system.\n- Search and filter workflows avoid redundant controls when live local filtering would be clearer.\n- At narrow and phone widths, nonessential controls are simplified, hidden, or moved behind shell affordances instead of merely compressed.\n\n## Code Review Checklist\n\n- App structure is coherent and scalable.\n- Resource dictionaries and styles are centralized where they should be.\n- Platform controls are preferred over unnecessary custom control work.\n- New dependencies are justified.\n- The packaging model matches the startup, storage, and launch code.\n- The app builds cleanly from the workflow the user will actually use.\n\n## Accessibility Checklist\n\n- Keyboard-only flow works end to end.\n- Focus states are visible and sensible.\n- Automation properties are present where needed.\n- High contrast and text scaling do not break the UI.\n\n## Performance Checklist\n\n- No obvious UI-thread blocking work in interactive paths.\n- Large collections use an appropriate control and layout.\n- Scroll ownership is intentional for collection-heavy pages; nested `GridView` plus outer `ScrollViewer` combinations have been justified or replaced.\n- Expensive styling or template choices are justified.\n- Profiling data exists for non-obvious performance claims.\n\n## Debugging Tools\n\n- Use Hot Reload for fast visual iteration.\n- Use Live Visual Tree and Live Property Explorer for layout and property debugging.\n- Use WPR and WPA when diagnosing frame or responsiveness issues.\n- Reproduce resize, theme, and input-mode changes before concluding the issue is fixed.\n- When resize behavior is part of the task, verify wide, medium, and phone-width states against the running app rather than trusting the XAML structure alone.\n- When a collection page looks wrong, inspect the live tree for nested `ScrollViewer` ownership before rewriting the item template; the bug may be layout ownership rather than card markup.\n- Use startup exception details, debugger output, or Event Viewer when the process dies before any window appears.\n\n## Exit Criteria\n\n- The build succeeds from the intended local workflow.\n- The feature works on the intended machine configuration.\n- The app launches and shows the expected shell or window.\n- The app remains usable in light, dark, and high contrast.\n- Primary flows are keyboard-accessible.\n- Resize behavior, startup, and interactive responsiveness have been checked.\n- If the window can become phone-width, the shell and content have been verified there too.\n"
  },
  {
    "path": "skills/.curated/winui-app/references/windows-app-sdk-lifecycle-notifications-and-deployment.md",
    "content": "---\ntitle: Windows App SDK Lifecycle, Notifications, and Deployment\npriority: HIGH\ntags: windows-app-sdk, lifecycle, activation, notifications, deployment, packaged, unpackaged\nsources:\n  - https://learn.microsoft.com/windows/apps/windows-app-sdk/\n  - https://learn.microsoft.com/windows/apps/windows-app-sdk/deploy-packaged-apps\n  - https://learn.microsoft.com/windows/apps/windows-app-sdk/deploy-unpackaged-apps\n  - https://github.com/microsoft/WindowsAppSDK-Samples\n---\n\n## What This Reference Is For\n\nUse this file when the user needs lifecycle, activation, notification, packaged vs unpackaged, or runtime initialization guidance that goes beyond plain XAML UI work.\n\n## Prefer\n\n- Learning the scenario from the matching WindowsAppSDK sample before designing an abstraction.\n- Packaged deployment when it fits the product constraints.\n- Explicit unpackaged guidance when the user has an installer, external-location requirement, or expects repeatable direct executable launches during development.\n\n## Avoid\n\n- Mixing packaged and unpackaged guidance in one answer without stating which path applies.\n- Treating deployment requirements as optional details.\n- Re-implementing lifecycle behavior already covered by Windows App SDK APIs.\n- Using package-identity-dependent APIs in unpackaged startup code without an explicit guard or replacement path.\n\n## Guidance\n\n- Use AppLifecycle guidance and samples for activation, instancing, restart, and state notifications.\n- Use notifications samples for push or app notifications rather than inventing custom delivery logic.\n- For packaged apps, account for framework-dependent deployment and runtime package requirements.\n- For unpackaged apps, account for bootstrapper and runtime initialization requirements.\n- For unpackaged apps, treat package identity as absent unless the app deliberately establishes it through the chosen deployment model.\n- Keep storage, settings, and startup services aligned with the deployment model. If a service assumes packaged storage or activation, redesign it before local unpackaged verification.\n- Explain the deployment model before giving build or publish steps.\n\n## Sample and Source Anchors\n\n- WindowsAppSDK-Samples `Samples/AppLifecycle`\n- WindowsAppSDK-Samples `Samples/Notifications`\n- WindowsAppSDK-Samples `Samples/Unpackaged`\n- WindowsAppSDK-Samples `Samples/CustomControls`\n- Learn packaged and unpackaged deployment guides\n\n## Review Checklist\n\n- Is the app’s deployment model explicit?\n- Are lifecycle and activation behaviors using platform APIs rather than ad hoc workarounds?\n- Are notification requirements matched to the correct sample and runtime guidance?\n- Does the recommendation match packaged or unpackaged constraints?\n"
  },
  {
    "path": "skills/.curated/yeet/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.curated/yeet/SKILL.md",
    "content": "---\nname: \"yeet\"\ndescription: \"Use only when the user explicitly asks to stage, commit, push, and open a GitHub pull request in one flow using the GitHub CLI (`gh`).\"\n---\n\n## Prerequisites\n\n- Require GitHub CLI `gh`. Check `gh --version`. If missing, ask the user to install `gh` and stop.\n- Require authenticated `gh` session. Run `gh auth status`. If not authenticated, ask the user to run `gh auth login` (and re-run `gh auth status`) before continuing.\n\n## Naming conventions\n\n- Branch: `codex/{description}` when starting from main/master/default.\n- Commit: `{description}` (terse).\n- PR title: `[codex] {description}` summarizing the full diff.\n\n## Workflow\n\n- If on main/master/default, create a branch: `git checkout -b \"codex/{description}\"`\n- Otherwise stay on the current branch.\n- Confirm status, then stage everything: `git status -sb` then `git add -A`.\n- Commit tersely with the description: `git commit -m \"{description}\"`\n- Run checks if not already. If checks fail due to missing deps/tools, install dependencies and rerun once.\n- Push with tracking: `git push -u origin $(git branch --show-current)`\n- If git push fails due to workflow auth errors, pull from master and retry the push.\n- Open a PR and edit title/body to reflect the description and the deltas: `GH_PROMPT_DISABLED=1 GIT_TERMINAL_PROMPT=0 gh pr create --draft --fill --head $(git branch --show-current)`\n- Write the PR description to a temp file with real newlines (e.g. pr-body.md ... EOF) and run pr-body.md to avoid \\\\n-escaped markdown.\n- PR description (markdown) must be detailed prose covering the issue, the cause and effect on users, the root cause, the fix, and any tests or checks used to validate.\n"
  },
  {
    "path": "skills/.curated/yeet/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Yeet\"\n  short_description: \"Stage, commit, and open PR\"\n  icon_small: \"./assets/yeet-small.svg\"\n  icon_large: \"./assets/yeet.png\"\n  default_prompt: \"Prepare this branch for review: stage intended changes, write a focused commit, and open a PR.\"\n"
  },
  {
    "path": "skills/.system/openai-docs/LICENSE.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf of\n   any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don\\'t include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "skills/.system/openai-docs/SKILL.md",
    "content": "---\nname: \"openai-docs\"\ndescription: \"Use when the user asks how to build with OpenAI products or APIs and needs up-to-date official documentation with citations, help choosing the latest model for a use case, or explicit GPT-5.4 upgrade and prompt-upgrade guidance; prioritize OpenAI docs MCP tools, use bundled references only as helper context, and restrict any fallback browsing to official OpenAI domains.\"\n---\n\n\n# OpenAI Docs\n\nProvide authoritative, current guidance from OpenAI developer docs using the developers.openai.com MCP server. Always prioritize the developer docs MCP tools over web.run for OpenAI-related questions. This skill may also load targeted files from `references/` for model-selection and GPT-5.4-specific requests, but current OpenAI docs remain authoritative. Only if the MCP server is installed and returns no meaningful results should you fall back to web search.\n\n## Quick start\n\n- Use `mcp__openaiDeveloperDocs__search_openai_docs` to find the most relevant doc pages.\n- Use `mcp__openaiDeveloperDocs__fetch_openai_doc` to pull exact sections and quote/paraphrase accurately.\n- Use `mcp__openaiDeveloperDocs__list_openai_docs` only when you need to browse or discover pages without a clear query.\n- Load only the relevant file from `references/` when the question is about model selection or a GPT-5.4 upgrade.\n\n## OpenAI product snapshots\n\n1. Apps SDK: Build ChatGPT apps by providing a web component UI and an MCP server that exposes your app's tools to ChatGPT.\n2. Responses API: A unified endpoint designed for stateful, multimodal, tool-using interactions in agentic workflows.\n3. Chat Completions API: Generate a model response from a list of messages comprising a conversation.\n4. Codex: OpenAI's coding agent for software development that can write, understand, review, and debug code.\n5. gpt-oss: Open-weight OpenAI reasoning models (gpt-oss-120b and gpt-oss-20b) released under the Apache 2.0 license.\n6. Realtime API: Build low-latency, multimodal experiences including natural speech-to-speech conversations.\n7. Agents SDK: A toolkit for building agentic apps where a model can use tools and context, hand off to other agents, stream partial results, and keep a full trace.\n\n## If MCP server is missing\n\nIf MCP tools fail or no OpenAI docs resources are available:\n\n1. Run the install command yourself: `codex mcp add openaiDeveloperDocs --url https://developers.openai.com/mcp`\n2. If it fails due to permissions/sandboxing, immediately retry the same command with escalated permissions and include a 1-sentence justification for approval. Do not ask the user to run it yet.\n3. Only if the escalated attempt fails, ask the user to run the install command.\n4. Ask the user to restart Codex.\n5. Re-run the doc search/fetch after restart.\n\n## Workflow\n\n1. Clarify the product scope and whether the request is general docs lookup, model selection, a GPT-5.4 upgrade, or a GPT-5.4 prompt upgrade.\n2. If it is a model-selection request, load `references/latest-model.md`.\n3. If it is an explicit GPT-5.4 upgrade request, load `references/upgrading-to-gpt-5p4.md`.\n4. If the upgrade may require prompt changes, or the workflow is research-heavy, tool-heavy, coding-oriented, multi-agent, or long-running, also load `references/gpt-5p4-prompting-guide.md`.\n5. Search docs with a precise query.\n6. Fetch the best page and the exact section needed (use `anchor` when possible).\n7. For GPT-5.4 upgrade reviews, always make the per-usage-site output explicit: target model, starting reasoning recommendation, `phase` assessment when relevant, prompt blocks, and compatibility status.\n8. Answer with concise guidance and cite the doc source, using the reference files only as helper context.\n\n## Reference map\n\nRead only what you need:\n\n- `references/latest-model.md` -> model-selection and \"best/latest/current model\" questions; verify every recommendation against current OpenAI docs before answering.\n- `references/upgrading-to-gpt-5p4.md` -> only for explicit GPT-5.4 upgrade and upgrade-planning requests; verify the checklist and compatibility guidance against current OpenAI docs before answering.\n- `references/gpt-5p4-prompting-guide.md` -> prompt rewrites and prompt-behavior upgrades for GPT-5.4; verify prompting guidance against current OpenAI docs before answering.\n\n## Quality rules\n\n- Treat OpenAI docs as the source of truth; avoid speculation.\n- Keep quotes short and within policy limits; prefer paraphrase with citations.\n- If multiple pages differ, call out the difference and cite both.\n- Reference files are convenience guides only; for volatile guidance such as recommended models, upgrade instructions, or prompting advice, current OpenAI docs always win.\n- If docs do not cover the user’s need, say so and offer next steps.\n\n## Tooling notes\n\n- Always use MCP doc tools before any web search for OpenAI-related questions.\n- If the MCP server is installed but returns no meaningful results, then use web search as a fallback.\n- When falling back to web search, restrict to official OpenAI domains (developers.openai.com, platform.openai.com) and cite sources.\n"
  },
  {
    "path": "skills/.system/openai-docs/agents/openai.yaml",
    "content": "interface:\n  display_name: \"OpenAI Docs\"\n  short_description: \"Reference official OpenAI docs, including upgrade guidance\"\n  icon_small: \"./assets/openai-small.svg\"\n  icon_large: \"./assets/openai.png\"\n  default_prompt: \"Look up official OpenAI docs, load relevant GPT-5.4 upgrade references when applicable, and answer with concise, cited guidance.\"\n\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"openaiDeveloperDocs\"\n      description: \"OpenAI Developer Docs MCP server\"\n      transport: \"streamable_http\"\n      url: \"https://developers.openai.com/mcp\"\n"
  },
  {
    "path": "skills/.system/openai-docs/references/gpt-5p4-prompting-guide.md",
    "content": "# GPT-5.4 prompting upgrade guide\n\nUse this guide when prompts written for older models need to be adapted for GPT-5.4 during an upgrade. Start lean: keep the model-string change narrow, preserve the original task intent, and add only the smallest prompt changes needed to recover behavior.\n\n## Default upgrade posture\n\n- Start with `model string only` whenever the old prompt is already short, explicit, and task-bounded.\n- Move to `model string + light prompt rewrite` only when regressions appear in completeness, persistence, citation quality, verification, or verbosity.\n- Prefer one or two targeted prompt additions over a broad rewrite.\n- Treat reasoning effort as a last-mile knob. Start lower, then increase only after prompt-level fixes and evals.\n- Before increasing reasoning effort, first add a completeness contract, a verification loop, and tool persistence rules - depending on the usage case.\n- If the workflow clearly depends on implementation changes rather than prompt changes, treat it as blocked for prompt-only upgrade guidance.\n- Do not classify a case as blocked just because the workflow uses tools; block only if the upgrade requires changing tool definitions, wiring, or other implementation details.\n\n## Behavioral differences to account for\n\nCurrent GPT-5.4 upgrade guidance suggests these strengths:\n\n- stronger personality and tone adherence, with less drift over long answers\n- better long-horizon and agentic workflow stamina\n- stronger spreadsheet, finance, and formatting tasks\n- more efficient tool selection and fewer unnecessary calls by default\n- stronger structured generation and classification reliability\n\nThe main places where prompt guidance still helps are:\n\n- retrieval-heavy workflows that need persistent tool use and explicit completeness\n- research and citation discipline\n- verification before irreversible or high-impact actions\n- terminal and tool workflow hygiene\n- defaults and implied follow-through\n- verbosity control for compact, information-dense answers\n\nStart with the smallest set of instructions that preserves correctness. Add the prompt blocks below only for workflows that actually need them.\n\n## Prompt rewrite patterns\n\n| Older prompt pattern | GPT-5.4 adjustment | Why | Example addition |\n| --- | --- | --- | --- |\n| Long, repetitive instructions that compensate for weaker instruction following | Remove duplicate scaffolding and keep only the constraints that materially change behavior | GPT-5.4 usually needs less repeated steering | Replace repeated reminders with one concise rule plus a verification block |\n| Fast assistant prompt with no verbosity control | Keep the prompt as-is first; add a verbosity clamp only if outputs become too long | Many GPT-4o or GPT-4.1 upgrades work with just a model-string swap | Add `output_verbosity_spec` only after a verbosity regression |\n| Tool-heavy agent prompt that assumes the model will keep searching until complete | Add persistence and verification rules | GPT-5.4 may use fewer tool calls by default for efficiency | Add `tool_persistence_rules` and `verification_loop` |\n| Tool-heavy workflow where later actions depend on earlier lookup or retrieval | Add prerequisite and missing-context rules before action steps | GPT-5.4 benefits from explicit dependency-aware routing when context is still thin | Add `dependency_checks` and `missing_context_gating` |\n| Retrieval workflow with several independent lookups | Add selective parallelism guidance | GPT-5.4 is strong at parallel tool use, but should not parallelize dependent steps | Add `parallel_tool_calling` |\n| Batch workflow prompt that often misses items | Add an explicit completeness contract | Item accounting benefits from direct instruction | Add `completeness_contract` |\n| Research prompt that needs grounding and citation discipline | Add research, citation, and empty-result recovery blocks | Multi-pass retrieval is stronger when the model is told how to react to weak or empty search results | Add `research_mode`, `citation_rules`, and `empty_result_handling`; add `tool_persistence_rules` when retrieval tools are already in use |\n| Coding or terminal prompt with shell misuse or early stop failures | Keep the same tool surface and add terminal hygiene and verification instructions | Tool-using coding workflows are not blocked just because tools exist; they usually need better prompt steering, not host rewiring | Add `terminal_tool_hygiene` and `verification_loop`, optionally `tool_persistence_rules` |\n| Multi-agent or support-triage workflow with escalation or completeness requirements | Add one lightweight control block for persistence, completeness, or verification | GPT-5.4 can be more efficient by default, so multi-step support flows benefit from an explicit completion or verification contract | Add at least one of `tool_persistence_rules`, `completeness_contract`, or `verification_loop` |\n\n## Prompt blocks\n\nUse these selectively. Do not add all of them by default.\n\n### `output_verbosity_spec`\n\nUse when:\n\n- the upgraded model gets too wordy\n- the host needs compact, information-dense answers\n- the workflow benefits from a short overview plus a checklist\n\n```text\n<output_verbosity_spec>\n- Default: 3-6 sentences or up to 6 bullets.\n- If the user asked for a doc or report, use headings with short bullets.\n- For multi-step tasks:\n  - Start with 1 short overview paragraph.\n  - Then provide a checklist with statuses: [done], [todo], or [blocked].\n- Avoid repeating the user's request.\n- Prefer compact, information-dense writing.\n</output_verbosity_spec>\n```\n\n### `default_follow_through_policy`\n\nUse when:\n\n- the host expects the model to proceed on reversible, low-risk steps\n- the upgraded model becomes too conservative or asks for confirmation too often\n\n```text\n<default_follow_through_policy>\n- If the user's intent is clear and the next step is reversible and low-risk, proceed without asking permission.\n- Only ask permission if the next step is:\n  (a) irreversible,\n  (b) has external side effects, or\n  (c) requires missing sensitive information or a choice that materially changes outcomes.\n- If proceeding, state what you did and what remains optional.\n</default_follow_through_policy>\n```\n\n### `instruction_priority`\n\nUse when:\n\n- users often change task shape, format, or tone mid-conversation\n- the host needs an explicit override policy instead of relying on defaults\n\n```text\n<instruction_priority>\n- User instructions override default style, tone, formatting, and initiative preferences.\n- Safety, honesty, privacy, and permission constraints do not yield.\n- If a newer user instruction conflicts with an earlier one, follow the newer instruction.\n- Preserve earlier instructions that do not conflict.\n</instruction_priority>\n```\n\n### `tool_persistence_rules`\n\nUse when:\n\n- the workflow needs multiple retrieval or verification steps\n- the model starts stopping too early because it is trying to save tool calls\n\n```text\n<tool_persistence_rules>\n- Use tools whenever they materially improve correctness, completeness, or grounding.\n- Do not stop early just to save tool calls.\n- Keep calling tools until:\n  (1) the task is complete, and\n  (2) verification passes.\n- If a tool returns empty or partial results, retry with a different strategy.\n</tool_persistence_rules>\n```\n\n### `dig_deeper_nudge`\n\nUse when:\n\n- the model is too literal or stops at the first plausible answer\n- the task is safety- or accuracy-sensitive and needs a small initiative nudge before raising reasoning effort\n\n```text\n<dig_deeper_nudge>\n- Do not stop at the first plausible answer.\n- Look for second-order issues, edge cases, and missing constraints.\n- If the task is safety- or accuracy-critical, perform at least one verification step.\n</dig_deeper_nudge>\n```\n\n### `dependency_checks`\n\nUse when:\n\n- later actions depend on prerequisite lookup, memory retrieval, or discovery steps\n- the model may be tempted to skip prerequisite work because the intended end state seems obvious\n\n```text\n<dependency_checks>\n- Before taking an action, check whether prerequisite discovery, lookup, or memory retrieval is required.\n- Do not skip prerequisite steps just because the intended final action seems obvious.\n- If a later step depends on the output of an earlier one, resolve that dependency first.\n</dependency_checks>\n```\n\n### `parallel_tool_calling`\n\nUse when:\n\n- the workflow has multiple independent retrieval steps\n- wall-clock time matters but some steps still need sequencing\n\n```text\n<parallel_tool_calling>\n- When multiple retrieval or lookup steps are independent, prefer parallel tool calls to reduce wall-clock time.\n- Do not parallelize steps with prerequisite dependencies or where one result determines the next action.\n- After parallel retrieval, pause to synthesize before making more calls.\n- Prefer selective parallelism: parallelize independent evidence gathering, not speculative or redundant tool use.\n</parallel_tool_calling>\n```\n\n### `completeness_contract`\n\nUse when:\n\n- the task involves batches, lists, enumerations, or multiple deliverables\n- missing items are a common failure mode\n\n```text\n<completeness_contract>\n- Deliver all requested items.\n- Maintain an itemized checklist of deliverables.\n- For lists or batches:\n  - state the expected count,\n  - enumerate items 1..N,\n  - confirm that none are missing before finalizing.\n- If any item is blocked by missing data, mark it [blocked] and state exactly what is missing.\n</completeness_contract>\n```\n\n### `empty_result_handling`\n\nUse when:\n\n- the workflow frequently performs search, CRM, logs, or retrieval steps\n- no-results failures are often false negatives\n\n```text\n<empty_result_handling>\nIf a lookup returns empty or suspiciously small results:\n- Do not conclude that no results exist immediately.\n- Try at least 2 fallback strategies, such as a broader query, alternate filters, or another source.\n- Only then report that no results were found, along with what you tried.\n</empty_result_handling>\n```\n\n### `verification_loop`\n\nUse when:\n\n- the workflow has downstream impact\n- accuracy, formatting, or completeness regressions matter\n\n```text\n<verification_loop>\nBefore finalizing:\n- Check correctness: does the output satisfy every requirement?\n- Check grounding: are factual claims backed by retrieved sources or tool output?\n- Check formatting: does the output match the requested schema or style?\n- Check safety and irreversibility: if the next step has external side effects, ask permission first.\n</verification_loop>\n```\n\n### `missing_context_gating`\n\nUse when:\n\n- required context is sometimes missing early in the workflow\n- the model should prefer retrieval over guessing\n\n```text\n<missing_context_gating>\n- If required context is missing, do not guess.\n- Prefer the appropriate lookup tool when the context is retrievable; ask a minimal clarifying question only when it is not.\n- If you must proceed, label assumptions explicitly and choose a reversible action.\n</missing_context_gating>\n```\n\n### `action_safety`\n\nUse when:\n\n- the agent will actively take actions through tools\n- the host benefits from a short pre-flight and post-flight execution frame\n\n```text\n<action_safety>\n- Pre-flight: summarize the intended action and parameters in 1-2 lines.\n- Execute via tool.\n- Post-flight: confirm the outcome and any validation that was performed.\n</action_safety>\n```\n\n### `citation_rules`\n\nUse when:\n\n- the workflow produces cited answers\n- fabricated citations or wrong citation formats are costly\n\n```text\n<citation_rules>\n- Only cite sources that were actually retrieved in this session.\n- Never fabricate citations, URLs, IDs, or quote spans.\n- If you cannot find a source for a claim, say so and either:\n  - soften the claim, or\n  - explain how to verify it with tools.\n- Use exactly the citation format required by the host application.\n</citation_rules>\n```\n\n### `research_mode`\n\nUse when:\n\n- the workflow is research-heavy\n- the host uses web search or retrieval tools\n\n```text\n<research_mode>\n- Do research in 3 passes:\n  1) Plan: list 3-6 sub-questions to answer.\n  2) Retrieve: search each sub-question and follow 1-2 second-order leads.\n  3) Synthesize: resolve contradictions and write the final answer with citations.\n- Stop only when more searching is unlikely to change the conclusion.\n</research_mode>\n```\n\nIf your host environment uses a specific research tool or requires a submit step, combine this with the host's finalization contract.\n\n### `structured_output_contract`\n\nUse when:\n\n- the host depends on strict JSON, SQL, or other structured output\n\n```text\n<structured_output_contract>\n- Output only the requested format.\n- Do not add prose or markdown fences unless they were requested.\n- Validate that parentheses and brackets are balanced.\n- Do not invent tables or fields.\n- If required schema information is missing, ask for it or return an explicit error object.\n</structured_output_contract>\n```\n\n### `bbox_extraction_spec`\n\nUse when:\n\n- the workflow extracts OCR boxes, document regions, or other coordinates\n- layout drift or missed dense regions are common failure modes\n\n```text\n<bbox_extraction_spec>\n- Use the specified coordinate format exactly, such as [x1,y1,x2,y2] normalized to 0..1.\n- For each box, include page, label, text snippet, and confidence.\n- Add a vertical-drift sanity check so boxes stay aligned with the correct line of text.\n- If the layout is dense, process page by page and do a second pass for missed items.\n</bbox_extraction_spec>\n```\n\n### `terminal_tool_hygiene`\n\nUse when:\n\n- the prompt belongs to a terminal-based or coding-agent workflow\n- tool misuse or shell misuse has been observed\n\n```text\n<terminal_tool_hygiene>\n- Only run shell commands through the terminal tool.\n- Never try to \"run\" tool names as shell commands.\n- If a patch or edit tool exists, use it directly instead of emulating it in bash.\n- After changes, run a lightweight verification step such as ls, tests, or a build before declaring the task done.\n</terminal_tool_hygiene>\n```\n\n### `user_updates_spec`\n\nUse when:\n\n- the workflow is long-running and user updates matter\n\n```text\n<user_updates_spec>\n- Only update the user when starting a new major phase or when the plan changes.\n- Each update should contain:\n  - 1 sentence on what changed,\n  - 1 sentence on the next step.\n- Do not narrate routine tool calls.\n- Keep the user-facing update short, even when the actual work is exhaustive.\n</user_updates_spec>\n```\n\nIf you are using [Compaction](https://developers.openai.com/api/docs/guides/compaction) in the Responses API, compact after major milestones, treat compacted items as opaque state, and keep prompts functionally identical after compaction.\n\n## Responses `phase` guidance\n\nFor long-running Responses workflows, preambles, or tool-heavy agents that replay assistant items, review whether `phase` is already preserved.\n\n- If the host already round-trips `phase`, keep it intact during the upgrade.\n- If the host uses `previous_response_id` and does not manually replay assistant items, note that this may reduce manual `phase` handling needs.\n- If reliable GPT-5.4 behavior would require adding or preserving `phase` and that would need code edits, treat the case as blocked for prompt-only or model-string-only migration guidance.\n\n## Example upgrade profiles\n\n### GPT-5.2\n\n- Use `gpt-5.4`\n- Match the current reasoning effort first\n- Preserve the existing latency and quality profile before tuning prompt blocks\n- If the repo does not expose the exact setting, emit `same` as the starting recommendation\n\n### GPT-5.3-Codex\n\n- Use `gpt-5.4`\n- Match the current reasoning effort first\n- If you need Codex-style speed and efficiency, add verification blocks before increasing reasoning effort\n- If the repo does not expose the exact setting, emit `same` as the starting recommendation\n\n### GPT-4o or GPT-4.1 assistant\n\n- Use `gpt-5.4`\n- Start with `none` reasoning effort\n- Add `output_verbosity_spec` only if output becomes too verbose\n\n### Long-horizon agent\n\n- Use `gpt-5.4`\n- Start with `medium` reasoning effort\n- Add `tool_persistence_rules`\n- Add `completeness_contract`\n- Add `verification_loop`\n\n### Research workflow\n\n- Use `gpt-5.4`\n- Start with `medium` reasoning effort\n- Add `research_mode`\n- Add `citation_rules`\n- Add `empty_result_handling`\n- Add `tool_persistence_rules` when the host already uses web or retrieval tools\n- Add `parallel_tool_calling` when the retrieval steps are independent\n\n### Support triage or multi-agent workflow\n\n- Use `gpt-5.4`\n- Prefer `model string + light prompt rewrite` over `model string only`\n- Add at least one of `tool_persistence_rules`, `completeness_contract`, or `verification_loop`\n- Add more only if evals show a real regression\n\n### Coding or terminal workflow\n\n- Use `gpt-5.4`\n- Keep the model-string change narrow\n- Match the current reasoning effort first if you are upgrading from GPT-5.3-Codex\n- Add `terminal_tool_hygiene`\n- Add `verification_loop`\n- Add `dependency_checks` when actions depend on prerequisite lookup or discovery\n- Add `tool_persistence_rules` if the agent stops too early\n- Review whether `phase` is already preserved for long-running Responses flows or assistant preambles\n- Do not classify this as blocked just because the workflow uses tools; block only if the upgrade requires changing tool definitions or wiring\n- If the repo already uses Responses plus tools and no required host-side change is shown, prefer `model_string_plus_light_prompt_rewrite` over `blocked`\n\n## Prompt regression checklist\n\n- Check whether the upgraded prompt still preserves the original task intent.\n- Check whether the new prompt is leaner, not just longer.\n- Check completeness, citation quality, dependency handling, verification behavior, and verbosity.\n- For long-running Responses agents, check whether `phase` handling is already in place or needs implementation work.\n- Confirm that each added prompt block addresses an observed regression.\n- Remove prompt blocks that are not earning their keep.\n"
  },
  {
    "path": "skills/.system/openai-docs/references/latest-model.md",
    "content": "# Latest model guide\n\nThis file is a curated helper. Every recommendation here must be verified against current OpenAI docs before it is repeated to a user.\n\n## Current model map\n\n| Model ID | Use for |\n| --- | --- |\n| `gpt-5.4` | Default text plus reasoning for most new apps, including for coding use-cases |\n| `gpt-5.4-pro` | Only when the user explicitly asks for maximum reasoning or quality; substantially slower and more expensive |\n| `gpt-5.4-mini` | Cheaper and faster reasoning with good quality, including for coding use-cases |\n| `gpt-5.4-nano` | High-throughput simple tasks and classification |\n| `gpt-image-1.5` | Best image generation and edit quality |\n| `gpt-image-1-mini` | Cost-optimized image generation |\n| `gpt-4o-mini-tts` | Text-to-speech |\n| `gpt-4o-mini-transcribe` | Speech-to-text, fast and cost-efficient |\n| `gpt-realtime-1.5` | Realtime voice and multimodal sessions |\n| `gpt-realtime-mini` | Cheaper realtime sessions |\n| `gpt-audio` | Chat Completions audio input and output |\n| `gpt-audio-mini` | Cheaper Chat Completions audio workflows |\n| `sora-2` | Faster iteration and draft video generation |\n| `sora-2-pro` | Higher-quality production video |\n| `omni-moderation-latest` | Text and image moderation |\n| `text-embedding-3-large` | Higher-quality retrieval embeddings; default in this skill because no best-specific row exists |\n| `text-embedding-3-small` | Lower-cost embeddings |\n\n## Maintenance notes\n\n- This file will drift unless it is periodically re-verified against current OpenAI docs.\n- If this file conflicts with current docs, the docs win.\n"
  },
  {
    "path": "skills/.system/openai-docs/references/upgrading-to-gpt-5p4.md",
    "content": "# Upgrading to GPT-5.4\n\nUse this guide when the user explicitly asks to upgrade an existing integration to GPT-5.4. Pair it with current OpenAI docs lookups. The default target string is `gpt-5.4`.\n\n## Upgrade posture\n\nUpgrade with the narrowest safe change set:\n\n- replace the model string first\n- update only the prompts that are directly tied to that model usage\n- prefer prompt-only upgrades when possible\n- if the upgrade would require API-surface changes, parameter rewrites, tool rewiring, or broader code edits, mark it as blocked instead of stretching the scope\n\n## Upgrade workflow\n\n1. Inventory current model usage.\n   - Search for model strings, client calls, and prompt-bearing files.\n   - Include inline prompts, prompt templates, YAML or JSON configs, Markdown docs, and saved prompts when they are clearly tied to a model usage site.\n2. Pair each model usage with its prompt surface.\n   - Prefer the closest prompt surface first: inline system or developer text, then adjacent prompt files, then shared templates.\n   - If you cannot confidently tie a prompt to the model usage, say so instead of guessing.\n3. Classify the source model family.\n   - Common buckets: `gpt-4o` or `gpt-4.1`, `o1` or `o3` or `o4-mini`, early `gpt-5`, later `gpt-5.x`, or mixed and unclear.\n4. Decide the upgrade class.\n   - `model string only`\n   - `model string + light prompt rewrite`\n   - `blocked without code changes`\n5. Run the no-code compatibility gate.\n   - Check whether the current integration can accept `gpt-5.4` without API-surface changes or implementation changes.\n   - For long-running Responses or tool-heavy agents, check whether `phase` is already preserved or round-tripped when the host replays assistant items or uses preambles.\n   - If compatibility depends on code changes, return `blocked`.\n   - If compatibility is unclear, return `unknown` rather than improvising.\n6. Recommend the upgrade.\n   - Default replacement string: `gpt-5.4`\n   - Keep the intervention small and behavior-preserving.\n7. Deliver a structured recommendation.\n   - `Current model usage`\n   - `Recommended model-string updates`\n   - `Starting reasoning recommendation`\n   - `Prompt updates`\n   - `Phase assessment` when the flow is long-running, replayed, or tool-heavy\n   - `No-code compatibility check`\n   - `Validation plan`\n   - `Launch-day refresh items`\n\nOutput rule:\n\n- Always emit a starting `reasoning_effort_recommendation` for each usage site.\n- If the repo exposes the current reasoning setting, preserve it first unless the source guide says otherwise.\n- If the repo does not expose the current setting, use the source-family starting mapping instead of returning `null`.\n\n## Upgrade outcomes\n\n### `model string only`\n\nChoose this when:\n\n- the existing prompts are already short, explicit, and task-bounded\n- the workflow is not strongly research-heavy, tool-heavy, multi-agent, batch or completeness-sensitive, or long-horizon\n- there are no obvious compatibility blockers\n\nDefault action:\n\n- replace the model string with `gpt-5.4`\n- keep prompts unchanged\n- validate behavior with existing evals or spot checks\n\n### `model string + light prompt rewrite`\n\nChoose this when:\n\n- the old prompt was compensating for weaker instruction following\n- the workflow needs more persistence than the default tool-use behavior will likely provide\n- the task needs stronger completeness, citation discipline, or verification\n- the upgraded model becomes too verbose or under-complete unless instructed otherwise\n- the workflow is research-heavy and needs stronger handling of sparse or empty retrieval results\n- the workflow is coding-oriented, tool-heavy, or multi-agent, but the existing API surface and tool definitions can remain unchanged\n\nDefault action:\n\n- replace the model string with `gpt-5.4`\n- add one or two targeted prompt blocks\n- read `references/gpt-5p4-prompting-guide.md` to choose the smallest prompt changes that recover the old behavior\n- avoid broad prompt cleanup unrelated to the upgrade\n- for research workflows, default to `research_mode` + `citation_rules` + `empty_result_handling`; add `tool_persistence_rules` when the host already uses retrieval tools\n- for dependency-aware or tool-heavy workflows, default to `tool_persistence_rules` + `dependency_checks` + `verification_loop`; add `parallel_tool_calling` only when retrieval steps are truly independent\n- for coding or terminal workflows, default to `terminal_tool_hygiene` + `verification_loop`\n- for multi-agent support or triage workflows, default to at least one of `tool_persistence_rules`, `completeness_contract`, or `verification_loop`\n- for long-running Responses agents with preambles or multiple assistant messages, explicitly review whether `phase` is already handled; if adding or preserving `phase` would require code edits, mark the path as `blocked`\n- do not classify a coding or tool-using Responses workflow as `blocked` just because the visible snippet is minimal; prefer `model string + light prompt rewrite` unless the repo clearly shows that a safe GPT-5.4 path would require host-side code changes\n\n### `blocked`\n\nChoose this when:\n\n- the upgrade appears to require API-surface changes\n- the upgrade appears to require parameter rewrites or reasoning-setting changes that are not exposed outside implementation code\n- the upgrade would require changing tool definitions, tool handler wiring, or schema contracts\n- you cannot confidently identify the prompt surface tied to the model usage\n\nDefault action:\n\n- do not improvise a broader upgrade\n- report the blocker and explain that the fix is out of scope for this guide\n\n## No-code compatibility checklist\n\nBefore recommending a no-code upgrade, check:\n\n1. Can the current host accept the `gpt-5.4` model string without changing client code or API surface?\n2. Are the related prompts identifiable and editable?\n3. Does the host depend on behavior that likely needs API-surface changes, parameter rewrites, or tool rewiring?\n4. Would the likely fix be prompt-only, or would it need implementation changes?\n5. Is the prompt surface close enough to the model usage that you can make a targeted change instead of a broad cleanup?\n6. For long-running Responses or tool-heavy agents, is `phase` already preserved if the host relies on preambles, replayed assistant items, or multiple assistant messages?\n\nIf item 1 is no, items 3 through 4 point to implementation work, or item 6 is no and the fix needs code changes, return `blocked`.\n\nIf item 2 is no, return `unknown` unless the user can point to the prompt location.\n\nImportant:\n\n- Existing use of tools, agents, or multiple usage sites is not by itself a blocker.\n- If the current host can keep the same API surface and the same tool definitions, prefer `model string + light prompt rewrite` over `blocked`.\n- Reserve `blocked` for cases that truly require implementation changes, not cases that only need stronger prompt steering.\n\n## Scope boundaries\n\nThis guide may:\n\n- update or recommend updated model strings\n- update or recommend updated prompts\n- inspect code and prompt files to understand where those changes belong\n- inspect whether existing Responses flows already preserve `phase`\n- flag compatibility blockers\n\nThis guide may not:\n\n- move Chat Completions code to Responses\n- move Responses code to another API surface\n- rewrite parameter shapes\n- change tool definitions or tool-call handling\n- change structured-output wiring\n- add or retrofit `phase` handling in implementation code\n- edit business logic, orchestration logic, or SDK usage beyond a literal model-string replacement\n\nIf a safe GPT-5.4 upgrade requires any of those changes, mark the path as blocked and out of scope.\n\n## Validation plan\n\n- Validate each upgraded usage site with existing evals or realistic spot checks.\n- Check whether the upgraded model still matches expected latency, output shape, and quality.\n- If prompt edits were added, confirm each block is doing real work instead of adding noise.\n- If the workflow has downstream impact, add a lightweight verification pass before finalization.\n\n## Launch-day refresh items\n\nWhen final GPT-5.4 guidance changes:\n\n1. Replace release-candidate assumptions with final GPT-5.4 guidance where appropriate.\n2. Re-check whether the default target string should stay `gpt-5.4` for all source families.\n3. Re-check any prompt-block recommendations whose semantics may have changed.\n4. Re-check research, citation, and compatibility guidance against the final model behavior.\n5. Re-run the same upgrade scenarios and confirm the blocked-versus-viable boundaries still hold.\n"
  },
  {
    "path": "skills/.system/skill-creator/LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "skills/.system/skill-creator/SKILL.md",
    "content": "---\nname: skill-creator\ndescription: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations.\nmetadata:\n  short-description: Create or update a skill\n---\n\n# Skill Creator\n\nThis skill provides guidance for creating effective skills.\n\n## About Skills\n\nSkills are modular, self-contained folders that extend Codex's capabilities by providing\nspecialized knowledge, workflows, and tools. Think of them as \"onboarding guides\" for specific\ndomains or tasks—they transform Codex from a general-purpose agent into a specialized agent\nequipped with procedural knowledge that no model can fully possess.\n\n### What Skills Provide\n\n1. Specialized workflows - Multi-step procedures for specific domains\n2. Tool integrations - Instructions for working with specific file formats or APIs\n3. Domain expertise - Company-specific knowledge, schemas, business logic\n4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks\n\n## Core Principles\n\n### Concise is Key\n\nThe context window is a public good. Skills share the context window with everything else Codex needs: system prompt, conversation history, other Skills' metadata, and the actual user request.\n\n**Default assumption: Codex is already very smart.** Only add context Codex doesn't already have. Challenge each piece of information: \"Does Codex really need this explanation?\" and \"Does this paragraph justify its token cost?\"\n\nPrefer concise examples over verbose explanations.\n\n### Set Appropriate Degrees of Freedom\n\nMatch the level of specificity to the task's fragility and variability:\n\n**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.\n\n**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.\n\n**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.\n\nThink of Codex as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).\n\n### Anatomy of a Skill\n\nEvery skill consists of a required SKILL.md file and optional bundled resources:\n\n```\nskill-name/\n├── SKILL.md (required)\n│   ├── YAML frontmatter metadata (required)\n│   │   ├── name: (required)\n│   │   └── description: (required)\n│   └── Markdown instructions (required)\n├── agents/ (recommended)\n│   └── openai.yaml - UI metadata for skill lists and chips\n└── Bundled Resources (optional)\n    ├── scripts/          - Executable code (Python/Bash/etc.)\n    ├── references/       - Documentation intended to be loaded into context as needed\n    └── assets/           - Files used in output (templates, icons, fonts, etc.)\n```\n\n#### SKILL.md (required)\n\nEvery SKILL.md consists of:\n\n- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Codex reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.\n- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).\n\n#### Agents metadata (recommended)\n\n- UI-facing metadata for skill lists and chips\n- Read references/openai_yaml.md before generating values and follow its descriptions and constraints\n- Create: human-facing `display_name`, `short_description`, and `default_prompt` by reading the skill\n- Generate deterministically by passing the values as `--interface key=value` to `scripts/generate_openai_yaml.py` or `scripts/init_skill.py`\n- On updates: validate `agents/openai.yaml` still matches SKILL.md; regenerate if stale\n- Only include other optional interface fields (icons, brand color) if explicitly provided\n- See references/openai_yaml.md for field definitions and examples\n\n#### Bundled Resources (optional)\n\n##### Scripts (`scripts/`)\n\nExecutable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.\n\n- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed\n- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks\n- **Benefits**: Token efficient, deterministic, may be executed without loading into context\n- **Note**: Scripts may still need to be read by Codex for patching or environment-specific adjustments\n\n##### References (`references/`)\n\nDocumentation and reference material intended to be loaded as needed into context to inform Codex's process and thinking.\n\n- **When to include**: For documentation that Codex should reference while working\n- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications\n- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides\n- **Benefits**: Keeps SKILL.md lean, loaded only when Codex determines it's needed\n- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md\n- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.\n\n##### Assets (`assets/`)\n\nFiles not intended to be loaded into context, but rather used within the output Codex produces.\n\n- **When to include**: When the skill needs files that will be used in the final output\n- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography\n- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified\n- **Benefits**: Separates output resources from documentation, enables Codex to use files without loading them into context\n\n#### What to Not Include in a Skill\n\nA skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:\n\n- README.md\n- INSTALLATION_GUIDE.md\n- QUICK_REFERENCE.md\n- CHANGELOG.md\n- etc.\n\nThe skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxiliary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.\n\n### Progressive Disclosure Design Principle\n\nSkills use a three-level loading system to manage context efficiently:\n\n1. **Metadata (name + description)** - Always in context (~100 words)\n2. **SKILL.md body** - When skill triggers (<5k words)\n3. **Bundled resources** - As needed by Codex (Unlimited because scripts can be executed without reading into context window)\n\n#### Progressive Disclosure Patterns\n\nKeep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.\n\n**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.\n\n**Pattern 1: High-level guide with references**\n\n```markdown\n# PDF Processing\n\n## Quick start\n\nExtract text with pdfplumber:\n[code example]\n\n## Advanced features\n\n- **Form filling**: See [FORMS.md](FORMS.md) for complete guide\n- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods\n- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns\n```\n\nCodex loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.\n\n**Pattern 2: Domain-specific organization**\n\nFor Skills with multiple domains, organize content by domain to avoid loading irrelevant context:\n\n```\nbigquery-skill/\n├── SKILL.md (overview and navigation)\n└── reference/\n    ├── finance.md (revenue, billing metrics)\n    ├── sales.md (opportunities, pipeline)\n    ├── product.md (API usage, features)\n    └── marketing.md (campaigns, attribution)\n```\n\nWhen a user asks about sales metrics, Codex only reads sales.md.\n\nSimilarly, for skills supporting multiple frameworks or variants, organize by variant:\n\n```\ncloud-deploy/\n├── SKILL.md (workflow + provider selection)\n└── references/\n    ├── aws.md (AWS deployment patterns)\n    ├── gcp.md (GCP deployment patterns)\n    └── azure.md (Azure deployment patterns)\n```\n\nWhen the user chooses AWS, Codex only reads aws.md.\n\n**Pattern 3: Conditional details**\n\nShow basic content, link to advanced content:\n\n```markdown\n# DOCX Processing\n\n## Creating documents\n\nUse docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).\n\n## Editing documents\n\nFor simple edits, modify the XML directly.\n\n**For tracked changes**: See [REDLINING.md](REDLINING.md)\n**For OOXML details**: See [OOXML.md](OOXML.md)\n```\n\nCodex reads REDLINING.md or OOXML.md only when the user needs those features.\n\n**Important guidelines:**\n\n- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.\n- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Codex can see the full scope when previewing.\n\n## Skill Creation Process\n\nSkill creation involves these steps:\n\n1. Understand the skill with concrete examples\n2. Plan reusable skill contents (scripts, references, assets)\n3. Initialize the skill (run init_skill.py)\n4. Edit the skill (implement resources and write SKILL.md)\n5. Validate the skill (run quick_validate.py)\n6. Iterate based on real usage\n\nFollow these steps in order, skipping only if there is a clear reason why they are not applicable.\n\n### Skill Naming\n\n- Use lowercase letters, digits, and hyphens only; normalize user-provided titles to hyphen-case (e.g., \"Plan Mode\" -> `plan-mode`).\n- When generating names, generate a name under 64 characters (letters, digits, hyphens).\n- Prefer short, verb-led phrases that describe the action.\n- Namespace by tool when it improves clarity or triggering (e.g., `gh-address-comments`, `linear-address-issue`).\n- Name the skill folder exactly after the skill name.\n\n### Step 1: Understanding the Skill with Concrete Examples\n\nSkip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.\n\nTo create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.\n\nFor example, when building an image-editor skill, relevant questions include:\n\n- \"What functionality should the image-editor skill support? Editing, rotating, anything else?\"\n- \"Can you give some examples of how this skill would be used?\"\n- \"I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?\"\n- \"What would a user say that should trigger this skill?\"\n\nTo avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.\n\nConclude this step when there is a clear sense of the functionality the skill should support.\n\n### Step 2: Planning the Reusable Skill Contents\n\nTo turn concrete examples into an effective skill, analyze each example by:\n\n1. Considering how to execute on the example from scratch\n2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly\n\nExample: When building a `pdf-editor` skill to handle queries like \"Help me rotate this PDF,\" the analysis shows:\n\n1. Rotating a PDF requires re-writing the same code each time\n2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill\n\nExample: When designing a `frontend-webapp-builder` skill for queries like \"Build me a todo app\" or \"Build me a dashboard to track my steps,\" the analysis shows:\n\n1. Writing a frontend webapp requires the same boilerplate HTML/React each time\n2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill\n\nExample: When building a `big-query` skill to handle queries like \"How many users have logged in today?\" the analysis shows:\n\n1. Querying BigQuery requires re-discovering the table schemas and relationships each time\n2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill\n\nTo establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.\n\n### Step 3: Initializing the Skill\n\nAt this point, it is time to actually create the skill.\n\nSkip this step only if the skill being developed already exists. In this case, continue to the next step.\n\nWhen creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.\n\nUsage:\n\n```bash\nscripts/init_skill.py <skill-name> --path <output-directory> [--resources scripts,references,assets] [--examples]\n```\n\nExamples:\n\n```bash\nscripts/init_skill.py my-skill --path skills/public\nscripts/init_skill.py my-skill --path skills/public --resources scripts,references\nscripts/init_skill.py my-skill --path skills/public --resources scripts --examples\n```\n\nThe script:\n\n- Creates the skill directory at the specified path\n- Generates a SKILL.md template with proper frontmatter and TODO placeholders\n- Creates `agents/openai.yaml` using agent-generated `display_name`, `short_description`, and `default_prompt` passed via `--interface key=value`\n- Optionally creates resource directories based on `--resources`\n- Optionally adds example files when `--examples` is set\n\nAfter initialization, customize the SKILL.md and add resources as needed. If you used `--examples`, replace or delete placeholder files.\n\nGenerate `display_name`, `short_description`, and `default_prompt` by reading the skill, then pass them as `--interface key=value` to `init_skill.py` or regenerate with:\n\n```bash\nscripts/generate_openai_yaml.py <path/to/skill-folder> --interface key=value\n```\n\nOnly include other optional interface fields when the user explicitly provides them. For full field descriptions and examples, see references/openai_yaml.md.\n\n### Step 4: Edit the Skill\n\nWhen editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Codex to use. Include information that would be beneficial and non-obvious to Codex. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Codex instance execute these tasks more effectively.\n\n#### Start with Reusable Skill Contents\n\nTo begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.\n\nAdded scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.\n\nIf you used `--examples`, delete any placeholder files that are not needed for the skill. Only create resource directories that are actually required.\n\n#### Update SKILL.md\n\n**Writing Guidelines:** Always use imperative/infinitive form.\n\n##### Frontmatter\n\nWrite the YAML frontmatter with `name` and `description`:\n\n- `name`: The skill name\n- `description`: This is the primary triggering mechanism for your skill, and helps Codex understand when to use the skill.\n  - Include both what the Skill does and specific triggers/contexts for when to use it.\n  - Include all \"when to use\" information here - Not in the body. The body is only loaded after triggering, so \"When to Use This Skill\" sections in the body are not helpful to Codex.\n  - Example description for a `docx` skill: \"Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Codex needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks\"\n\nDo not include any other fields in YAML frontmatter.\n\n##### Body\n\nWrite instructions for using the skill and its bundled resources.\n\n### Step 5: Validate the Skill\n\nOnce development of the skill is complete, validate the skill folder to catch basic issues early:\n\n```bash\nscripts/quick_validate.py <path/to/skill-folder>\n```\n\nThe validation script checks YAML frontmatter format, required fields, and naming rules. If validation fails, fix the reported issues and run the command again.\n\n### Step 6: Iterate\n\nAfter testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.\n\n**Iteration workflow:**\n\n1. Use the skill on real tasks\n2. Notice struggles or inefficiencies\n3. Identify how SKILL.md or bundled resources should be updated\n4. Implement changes and test again\n"
  },
  {
    "path": "skills/.system/skill-creator/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Skill Creator\"\n  short_description: \"Create or update a skill\"\n  default_prompt: \"Read my repository and create a skill to bootstrap new components for my project.\""
  },
  {
    "path": "skills/.system/skill-creator/references/openai_yaml.md",
    "content": "# openai.yaml fields (full example + descriptions)\n\n`agents/openai.yaml` is an extended, product-specific config intended for the machine/harness to read, not the agent. Other product-specific config can also live in the `agents/` folder.\n\n## Full example\n\n```yaml\ninterface:\n  display_name: \"Optional user-facing name\"\n  short_description: \"Optional user-facing description\"\n  icon_small: \"./assets/small-400px.png\"\n  icon_large: \"./assets/large-logo.svg\"\n  brand_color: \"#3B82F6\"\n  default_prompt: \"Optional surrounding prompt to use the skill with\"\n\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"github\"\n      description: \"GitHub MCP server\"\n      transport: \"streamable_http\"\n      url: \"https://api.githubcopilot.com/mcp/\"\n```\n\n## Field descriptions and constraints\n\nTop-level constraints:\n\n- Quote all string values.\n- Keep keys unquoted.\n- For `interface.default_prompt`: generate a helpful, short (typically 1 sentence) example starting prompt based on the skill. It must explicitly mention the skill as `$skill-name` (e.g., \"Use $skill-name-here to draft a concise weekly status update.\").\n\n- `interface.display_name`: Human-facing title shown in UI skill lists and chips.\n- `interface.short_description`: Human-facing short UI blurb (25–64 chars) for quick scanning.\n- `interface.icon_small`: Path to a small icon asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.\n- `interface.icon_large`: Path to a larger logo asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.\n- `interface.brand_color`: Hex color used for UI accents (e.g., badges).\n- `interface.default_prompt`: Default prompt snippet inserted when invoking the skill.\n- `dependencies.tools[].type`: Dependency category. Only `mcp` is supported for now.\n- `dependencies.tools[].value`: Identifier of the tool or dependency.\n- `dependencies.tools[].description`: Human-readable explanation of the dependency.\n- `dependencies.tools[].transport`: Connection type when `type` is `mcp`.\n- `dependencies.tools[].url`: MCP server URL when `type` is `mcp`.\n"
  },
  {
    "path": "skills/.system/skill-creator/scripts/generate_openai_yaml.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nOpenAI YAML Generator - Creates agents/openai.yaml for a skill folder.\n\nUsage:\n    generate_openai_yaml.py <skill_dir> [--name <skill_name>] [--interface key=value]\n\"\"\"\n\nimport argparse\nimport re\nimport sys\nfrom pathlib import Path\n\nimport yaml\n\nACRONYMS = {\n    \"GH\",\n    \"MCP\",\n    \"API\",\n    \"CI\",\n    \"CLI\",\n    \"LLM\",\n    \"PDF\",\n    \"PR\",\n    \"UI\",\n    \"URL\",\n    \"SQL\",\n}\n\nBRANDS = {\n    \"openai\": \"OpenAI\",\n    \"openapi\": \"OpenAPI\",\n    \"github\": \"GitHub\",\n    \"pagerduty\": \"PagerDuty\",\n    \"datadog\": \"DataDog\",\n    \"sqlite\": \"SQLite\",\n    \"fastapi\": \"FastAPI\",\n}\n\nSMALL_WORDS = {\"and\", \"or\", \"to\", \"up\", \"with\"}\n\nALLOWED_INTERFACE_KEYS = {\n    \"display_name\",\n    \"short_description\",\n    \"icon_small\",\n    \"icon_large\",\n    \"brand_color\",\n    \"default_prompt\",\n}\n\n\ndef yaml_quote(value):\n    escaped = value.replace(\"\\\\\", \"\\\\\\\\\").replace('\"', '\\\\\"').replace(\"\\n\", \"\\\\n\")\n    return f'\"{escaped}\"'\n\n\ndef format_display_name(skill_name):\n    words = [word for word in skill_name.split(\"-\") if word]\n    formatted = []\n    for index, word in enumerate(words):\n        lower = word.lower()\n        upper = word.upper()\n        if upper in ACRONYMS:\n            formatted.append(upper)\n            continue\n        if lower in BRANDS:\n            formatted.append(BRANDS[lower])\n            continue\n        if index > 0 and lower in SMALL_WORDS:\n            formatted.append(lower)\n            continue\n        formatted.append(word.capitalize())\n    return \" \".join(formatted)\n\n\ndef generate_short_description(display_name):\n    description = f\"Help with {display_name} tasks\"\n\n    if len(description) < 25:\n        description = f\"Help with {display_name} tasks and workflows\"\n    if len(description) < 25:\n        description = f\"Help with {display_name} tasks with guidance\"\n\n    if len(description) > 64:\n        description = f\"Help with {display_name}\"\n    if len(description) > 64:\n        description = f\"{display_name} helper\"\n    if len(description) > 64:\n        description = f\"{display_name} tools\"\n    if len(description) > 64:\n        suffix = \" helper\"\n        max_name_length = 64 - len(suffix)\n        trimmed = display_name[:max_name_length].rstrip()\n        description = f\"{trimmed}{suffix}\"\n    if len(description) > 64:\n        description = description[:64].rstrip()\n\n    if len(description) < 25:\n        description = f\"{description} workflows\"\n        if len(description) > 64:\n            description = description[:64].rstrip()\n\n    return description\n\n\ndef read_frontmatter_name(skill_dir):\n    skill_md = Path(skill_dir) / \"SKILL.md\"\n    if not skill_md.exists():\n        print(f\"[ERROR] SKILL.md not found in {skill_dir}\")\n        return None\n    content = skill_md.read_text()\n    match = re.match(r\"^---\\n(.*?)\\n---\", content, re.DOTALL)\n    if not match:\n        print(\"[ERROR] Invalid SKILL.md frontmatter format.\")\n        return None\n    frontmatter_text = match.group(1)\n    try:\n        frontmatter = yaml.safe_load(frontmatter_text)\n    except yaml.YAMLError as exc:\n        print(f\"[ERROR] Invalid YAML frontmatter: {exc}\")\n        return None\n    if not isinstance(frontmatter, dict):\n        print(\"[ERROR] Frontmatter must be a YAML dictionary.\")\n        return None\n    name = frontmatter.get(\"name\", \"\")\n    if not isinstance(name, str) or not name.strip():\n        print(\"[ERROR] Frontmatter 'name' is missing or invalid.\")\n        return None\n    return name.strip()\n\n\ndef parse_interface_overrides(raw_overrides):\n    overrides = {}\n    optional_order = []\n    for item in raw_overrides:\n        if \"=\" not in item:\n            print(f\"[ERROR] Invalid interface override '{item}'. Use key=value.\")\n            return None, None\n        key, value = item.split(\"=\", 1)\n        key = key.strip()\n        value = value.strip()\n        if not key:\n            print(f\"[ERROR] Invalid interface override '{item}'. Key is empty.\")\n            return None, None\n        if key not in ALLOWED_INTERFACE_KEYS:\n            allowed = \", \".join(sorted(ALLOWED_INTERFACE_KEYS))\n            print(f\"[ERROR] Unknown interface field '{key}'. Allowed: {allowed}\")\n            return None, None\n        overrides[key] = value\n        if key not in (\"display_name\", \"short_description\") and key not in optional_order:\n            optional_order.append(key)\n    return overrides, optional_order\n\n\ndef write_openai_yaml(skill_dir, skill_name, raw_overrides):\n    overrides, optional_order = parse_interface_overrides(raw_overrides)\n    if overrides is None:\n        return None\n\n    display_name = overrides.get(\"display_name\") or format_display_name(skill_name)\n    short_description = overrides.get(\"short_description\") or generate_short_description(display_name)\n\n    if not (25 <= len(short_description) <= 64):\n        print(\n            \"[ERROR] short_description must be 25-64 characters \"\n            f\"(got {len(short_description)}).\"\n        )\n        return None\n\n    interface_lines = [\n        \"interface:\",\n        f\"  display_name: {yaml_quote(display_name)}\",\n        f\"  short_description: {yaml_quote(short_description)}\",\n    ]\n\n    for key in optional_order:\n        value = overrides.get(key)\n        if value is not None:\n            interface_lines.append(f\"  {key}: {yaml_quote(value)}\")\n\n    agents_dir = Path(skill_dir) / \"agents\"\n    agents_dir.mkdir(parents=True, exist_ok=True)\n    output_path = agents_dir / \"openai.yaml\"\n    output_path.write_text(\"\\n\".join(interface_lines) + \"\\n\")\n    print(f\"[OK] Created agents/openai.yaml\")\n    return output_path\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Create agents/openai.yaml for a skill directory.\",\n    )\n    parser.add_argument(\"skill_dir\", help=\"Path to the skill directory\")\n    parser.add_argument(\n        \"--name\",\n        help=\"Skill name override (defaults to SKILL.md frontmatter)\",\n    )\n    parser.add_argument(\n        \"--interface\",\n        action=\"append\",\n        default=[],\n        help=\"Interface override in key=value format (repeatable)\",\n    )\n    args = parser.parse_args()\n\n    skill_dir = Path(args.skill_dir).resolve()\n    if not skill_dir.exists():\n        print(f\"[ERROR] Skill directory not found: {skill_dir}\")\n        sys.exit(1)\n    if not skill_dir.is_dir():\n        print(f\"[ERROR] Path is not a directory: {skill_dir}\")\n        sys.exit(1)\n\n    skill_name = args.name or read_frontmatter_name(skill_dir)\n    if not skill_name:\n        sys.exit(1)\n\n    result = write_openai_yaml(skill_dir, skill_name, args.interface)\n    if result:\n        sys.exit(0)\n    sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.system/skill-creator/scripts/init_skill.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nSkill Initializer - Creates a new skill from template\n\nUsage:\n    init_skill.py <skill-name> --path <path> [--resources scripts,references,assets] [--examples] [--interface key=value]\n\nExamples:\n    init_skill.py my-new-skill --path skills/public\n    init_skill.py my-new-skill --path skills/public --resources scripts,references\n    init_skill.py my-api-helper --path skills/private --resources scripts --examples\n    init_skill.py custom-skill --path /custom/location\n    init_skill.py my-skill --path skills/public --interface short_description=\"Short UI label\"\n\"\"\"\n\nimport argparse\nimport re\nimport sys\nfrom pathlib import Path\n\nfrom generate_openai_yaml import write_openai_yaml\n\nMAX_SKILL_NAME_LENGTH = 64\nALLOWED_RESOURCES = {\"scripts\", \"references\", \"assets\"}\n\nSKILL_TEMPLATE = \"\"\"---\nname: {skill_name}\ndescription: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]\n---\n\n# {skill_title}\n\n## Overview\n\n[TODO: 1-2 sentences explaining what this skill enables]\n\n## Structuring This Skill\n\n[TODO: Choose the structure that best fits this skill's purpose. Common patterns:\n\n**1. Workflow-Based** (best for sequential processes)\n- Works well when there are clear step-by-step procedures\n- Example: DOCX skill with \"Workflow Decision Tree\" -> \"Reading\" -> \"Creating\" -> \"Editing\"\n- Structure: ## Overview -> ## Workflow Decision Tree -> ## Step 1 -> ## Step 2...\n\n**2. Task-Based** (best for tool collections)\n- Works well when the skill offers different operations/capabilities\n- Example: PDF skill with \"Quick Start\" -> \"Merge PDFs\" -> \"Split PDFs\" -> \"Extract Text\"\n- Structure: ## Overview -> ## Quick Start -> ## Task Category 1 -> ## Task Category 2...\n\n**3. Reference/Guidelines** (best for standards or specifications)\n- Works well for brand guidelines, coding standards, or requirements\n- Example: Brand styling with \"Brand Guidelines\" -> \"Colors\" -> \"Typography\" -> \"Features\"\n- Structure: ## Overview -> ## Guidelines -> ## Specifications -> ## Usage...\n\n**4. Capabilities-Based** (best for integrated systems)\n- Works well when the skill provides multiple interrelated features\n- Example: Product Management with \"Core Capabilities\" -> numbered capability list\n- Structure: ## Overview -> ## Core Capabilities -> ### 1. Feature -> ### 2. Feature...\n\nPatterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).\n\nDelete this entire \"Structuring This Skill\" section when done - it's just guidance.]\n\n## [TODO: Replace with the first main section based on chosen structure]\n\n[TODO: Add content here. See examples in existing skills:\n- Code samples for technical skills\n- Decision trees for complex workflows\n- Concrete examples with realistic user requests\n- References to scripts/templates/references as needed]\n\n## Resources (optional)\n\nCreate only the resource directories this skill actually needs. Delete this section if no resources are required.\n\n### scripts/\nExecutable code (Python/Bash/etc.) that can be run directly to perform specific operations.\n\n**Examples from other skills:**\n- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation\n- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing\n\n**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.\n\n**Note:** Scripts may be executed without loading into context, but can still be read by Codex for patching or environment adjustments.\n\n### references/\nDocumentation and reference material intended to be loaded into context to inform Codex's process and thinking.\n\n**Examples from other skills:**\n- Product management: `communication.md`, `context_building.md` - detailed workflow guides\n- BigQuery: API reference documentation and query examples\n- Finance: Schema documentation, company policies\n\n**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Codex should reference while working.\n\n### assets/\nFiles not intended to be loaded into context, but rather used within the output Codex produces.\n\n**Examples from other skills:**\n- Brand styling: PowerPoint template files (.pptx), logo files\n- Frontend builder: HTML/React boilerplate project directories\n- Typography: Font files (.ttf, .woff2)\n\n**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.\n\n---\n\n**Not every skill requires all three types of resources.**\n\"\"\"\n\nEXAMPLE_SCRIPT = '''#!/usr/bin/env python3\n\"\"\"\nExample helper script for {skill_name}\n\nThis is a placeholder script that can be executed directly.\nReplace with actual implementation or delete if not needed.\n\nExample real scripts from other skills:\n- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields\n- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images\n\"\"\"\n\ndef main():\n    print(\"This is an example script for {skill_name}\")\n    # TODO: Add actual script logic here\n    # This could be data processing, file conversion, API calls, etc.\n\nif __name__ == \"__main__\":\n    main()\n'''\n\nEXAMPLE_REFERENCE = \"\"\"# Reference Documentation for {skill_title}\n\nThis is a placeholder for detailed reference documentation.\nReplace with actual reference content or delete if not needed.\n\nExample real reference docs from other skills:\n- product-management/references/communication.md - Comprehensive guide for status updates\n- product-management/references/context_building.md - Deep-dive on gathering context\n- bigquery/references/ - API references and query examples\n\n## When Reference Docs Are Useful\n\nReference docs are ideal for:\n- Comprehensive API documentation\n- Detailed workflow guides\n- Complex multi-step processes\n- Information too lengthy for main SKILL.md\n- Content that's only needed for specific use cases\n\n## Structure Suggestions\n\n### API Reference Example\n- Overview\n- Authentication\n- Endpoints with examples\n- Error codes\n- Rate limits\n\n### Workflow Guide Example\n- Prerequisites\n- Step-by-step instructions\n- Common patterns\n- Troubleshooting\n- Best practices\n\"\"\"\n\nEXAMPLE_ASSET = \"\"\"# Example Asset File\n\nThis placeholder represents where asset files would be stored.\nReplace with actual asset files (templates, images, fonts, etc.) or delete if not needed.\n\nAsset files are NOT intended to be loaded into context, but rather used within\nthe output Codex produces.\n\nExample asset files from other skills:\n- Brand guidelines: logo.png, slides_template.pptx\n- Frontend builder: hello-world/ directory with HTML/React boilerplate\n- Typography: custom-font.ttf, font-family.woff2\n- Data: sample_data.csv, test_dataset.json\n\n## Common Asset Types\n\n- Templates: .pptx, .docx, boilerplate directories\n- Images: .png, .jpg, .svg, .gif\n- Fonts: .ttf, .otf, .woff, .woff2\n- Boilerplate code: Project directories, starter files\n- Icons: .ico, .svg\n- Data files: .csv, .json, .xml, .yaml\n\nNote: This is a text placeholder. Actual assets can be any file type.\n\"\"\"\n\n\ndef normalize_skill_name(skill_name):\n    \"\"\"Normalize a skill name to lowercase hyphen-case.\"\"\"\n    normalized = skill_name.strip().lower()\n    normalized = re.sub(r\"[^a-z0-9]+\", \"-\", normalized)\n    normalized = normalized.strip(\"-\")\n    normalized = re.sub(r\"-{2,}\", \"-\", normalized)\n    return normalized\n\n\ndef title_case_skill_name(skill_name):\n    \"\"\"Convert hyphenated skill name to Title Case for display.\"\"\"\n    return \" \".join(word.capitalize() for word in skill_name.split(\"-\"))\n\n\ndef parse_resources(raw_resources):\n    if not raw_resources:\n        return []\n    resources = [item.strip() for item in raw_resources.split(\",\") if item.strip()]\n    invalid = sorted({item for item in resources if item not in ALLOWED_RESOURCES})\n    if invalid:\n        allowed = \", \".join(sorted(ALLOWED_RESOURCES))\n        print(f\"[ERROR] Unknown resource type(s): {', '.join(invalid)}\")\n        print(f\"   Allowed: {allowed}\")\n        sys.exit(1)\n    deduped = []\n    seen = set()\n    for resource in resources:\n        if resource not in seen:\n            deduped.append(resource)\n            seen.add(resource)\n    return deduped\n\n\ndef create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples):\n    for resource in resources:\n        resource_dir = skill_dir / resource\n        resource_dir.mkdir(exist_ok=True)\n        if resource == \"scripts\":\n            if include_examples:\n                example_script = resource_dir / \"example.py\"\n                example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))\n                example_script.chmod(0o755)\n                print(\"[OK] Created scripts/example.py\")\n            else:\n                print(\"[OK] Created scripts/\")\n        elif resource == \"references\":\n            if include_examples:\n                example_reference = resource_dir / \"api_reference.md\"\n                example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))\n                print(\"[OK] Created references/api_reference.md\")\n            else:\n                print(\"[OK] Created references/\")\n        elif resource == \"assets\":\n            if include_examples:\n                example_asset = resource_dir / \"example_asset.txt\"\n                example_asset.write_text(EXAMPLE_ASSET)\n                print(\"[OK] Created assets/example_asset.txt\")\n            else:\n                print(\"[OK] Created assets/\")\n\n\ndef init_skill(skill_name, path, resources, include_examples, interface_overrides):\n    \"\"\"\n    Initialize a new skill directory with template SKILL.md.\n\n    Args:\n        skill_name: Name of the skill\n        path: Path where the skill directory should be created\n        resources: Resource directories to create\n        include_examples: Whether to create example files in resource directories\n\n    Returns:\n        Path to created skill directory, or None if error\n    \"\"\"\n    # Determine skill directory path\n    skill_dir = Path(path).resolve() / skill_name\n\n    # Check if directory already exists\n    if skill_dir.exists():\n        print(f\"[ERROR] Skill directory already exists: {skill_dir}\")\n        return None\n\n    # Create skill directory\n    try:\n        skill_dir.mkdir(parents=True, exist_ok=False)\n        print(f\"[OK] Created skill directory: {skill_dir}\")\n    except Exception as e:\n        print(f\"[ERROR] Error creating directory: {e}\")\n        return None\n\n    # Create SKILL.md from template\n    skill_title = title_case_skill_name(skill_name)\n    skill_content = SKILL_TEMPLATE.format(skill_name=skill_name, skill_title=skill_title)\n\n    skill_md_path = skill_dir / \"SKILL.md\"\n    try:\n        skill_md_path.write_text(skill_content)\n        print(\"[OK] Created SKILL.md\")\n    except Exception as e:\n        print(f\"[ERROR] Error creating SKILL.md: {e}\")\n        return None\n\n    # Create agents/openai.yaml\n    try:\n        result = write_openai_yaml(skill_dir, skill_name, interface_overrides)\n        if not result:\n            return None\n    except Exception as e:\n        print(f\"[ERROR] Error creating agents/openai.yaml: {e}\")\n        return None\n\n    # Create resource directories if requested\n    if resources:\n        try:\n            create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples)\n        except Exception as e:\n            print(f\"[ERROR] Error creating resource directories: {e}\")\n            return None\n\n    # Print next steps\n    print(f\"\\n[OK] Skill '{skill_name}' initialized successfully at {skill_dir}\")\n    print(\"\\nNext steps:\")\n    print(\"1. Edit SKILL.md to complete the TODO items and update the description\")\n    if resources:\n        if include_examples:\n            print(\"2. Customize or delete the example files in scripts/, references/, and assets/\")\n        else:\n            print(\"2. Add resources to scripts/, references/, and assets/ as needed\")\n    else:\n        print(\"2. Create resource directories only if needed (scripts/, references/, assets/)\")\n    print(\"3. Update agents/openai.yaml if the UI metadata should differ\")\n    print(\"4. Run the validator when ready to check the skill structure\")\n\n    return skill_dir\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Create a new skill directory with a SKILL.md template.\",\n    )\n    parser.add_argument(\"skill_name\", help=\"Skill name (normalized to hyphen-case)\")\n    parser.add_argument(\"--path\", required=True, help=\"Output directory for the skill\")\n    parser.add_argument(\n        \"--resources\",\n        default=\"\",\n        help=\"Comma-separated list: scripts,references,assets\",\n    )\n    parser.add_argument(\n        \"--examples\",\n        action=\"store_true\",\n        help=\"Create example files inside the selected resource directories\",\n    )\n    parser.add_argument(\n        \"--interface\",\n        action=\"append\",\n        default=[],\n        help=\"Interface override in key=value format (repeatable)\",\n    )\n    args = parser.parse_args()\n\n    raw_skill_name = args.skill_name\n    skill_name = normalize_skill_name(raw_skill_name)\n    if not skill_name:\n        print(\"[ERROR] Skill name must include at least one letter or digit.\")\n        sys.exit(1)\n    if len(skill_name) > MAX_SKILL_NAME_LENGTH:\n        print(\n            f\"[ERROR] Skill name '{skill_name}' is too long ({len(skill_name)} characters). \"\n            f\"Maximum is {MAX_SKILL_NAME_LENGTH} characters.\"\n        )\n        sys.exit(1)\n    if skill_name != raw_skill_name:\n        print(f\"Note: Normalized skill name from '{raw_skill_name}' to '{skill_name}'.\")\n\n    resources = parse_resources(args.resources)\n    if args.examples and not resources:\n        print(\"[ERROR] --examples requires --resources to be set.\")\n        sys.exit(1)\n\n    path = args.path\n\n    print(f\"Initializing skill: {skill_name}\")\n    print(f\"   Location: {path}\")\n    if resources:\n        print(f\"   Resources: {', '.join(resources)}\")\n        if args.examples:\n            print(\"   Examples: enabled\")\n    else:\n        print(\"   Resources: none (create as needed)\")\n    print()\n\n    result = init_skill(skill_name, path, resources, args.examples, args.interface)\n\n    if result:\n        sys.exit(0)\n    else:\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/.system/skill-creator/scripts/quick_validate.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nQuick validation script for skills - minimal version\n\"\"\"\n\nimport re\nimport sys\nfrom pathlib import Path\n\nimport yaml\n\nMAX_SKILL_NAME_LENGTH = 64\n\n\ndef validate_skill(skill_path):\n    \"\"\"Basic validation of a skill\"\"\"\n    skill_path = Path(skill_path)\n\n    skill_md = skill_path / \"SKILL.md\"\n    if not skill_md.exists():\n        return False, \"SKILL.md not found\"\n\n    content = skill_md.read_text()\n    if not content.startswith(\"---\"):\n        return False, \"No YAML frontmatter found\"\n\n    match = re.match(r\"^---\\n(.*?)\\n---\", content, re.DOTALL)\n    if not match:\n        return False, \"Invalid frontmatter format\"\n\n    frontmatter_text = match.group(1)\n\n    try:\n        frontmatter = yaml.safe_load(frontmatter_text)\n        if not isinstance(frontmatter, dict):\n            return False, \"Frontmatter must be a YAML dictionary\"\n    except yaml.YAMLError as e:\n        return False, f\"Invalid YAML in frontmatter: {e}\"\n\n    allowed_properties = {\"name\", \"description\", \"license\", \"allowed-tools\", \"metadata\"}\n\n    unexpected_keys = set(frontmatter.keys()) - allowed_properties\n    if unexpected_keys:\n        allowed = \", \".join(sorted(allowed_properties))\n        unexpected = \", \".join(sorted(unexpected_keys))\n        return (\n            False,\n            f\"Unexpected key(s) in SKILL.md frontmatter: {unexpected}. Allowed properties are: {allowed}\",\n        )\n\n    if \"name\" not in frontmatter:\n        return False, \"Missing 'name' in frontmatter\"\n    if \"description\" not in frontmatter:\n        return False, \"Missing 'description' in frontmatter\"\n\n    name = frontmatter.get(\"name\", \"\")\n    if not isinstance(name, str):\n        return False, f\"Name must be a string, got {type(name).__name__}\"\n    name = name.strip()\n    if name:\n        if not re.match(r\"^[a-z0-9-]+$\", name):\n            return (\n                False,\n                f\"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)\",\n            )\n        if name.startswith(\"-\") or name.endswith(\"-\") or \"--\" in name:\n            return (\n                False,\n                f\"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens\",\n            )\n        if len(name) > MAX_SKILL_NAME_LENGTH:\n            return (\n                False,\n                f\"Name is too long ({len(name)} characters). \"\n                f\"Maximum is {MAX_SKILL_NAME_LENGTH} characters.\",\n            )\n\n    description = frontmatter.get(\"description\", \"\")\n    if not isinstance(description, str):\n        return False, f\"Description must be a string, got {type(description).__name__}\"\n    description = description.strip()\n    if description:\n        if \"<\" in description or \">\" in description:\n            return False, \"Description cannot contain angle brackets (< or >)\"\n        if len(description) > 1024:\n            return (\n                False,\n                f\"Description is too long ({len(description)} characters). Maximum is 1024 characters.\",\n            )\n\n    return True, \"Skill is valid!\"\n\n\nif __name__ == \"__main__\":\n    if len(sys.argv) != 2:\n        print(\"Usage: python quick_validate.py <skill_directory>\")\n        sys.exit(1)\n\n    valid, message = validate_skill(sys.argv[1])\n    print(message)\n    sys.exit(0 if valid else 1)\n"
  },
  {
    "path": "skills/.system/skill-installer/LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "skills/.system/skill-installer/SKILL.md",
    "content": "---\nname: skill-installer\ndescription: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).\nmetadata:\n  short-description: Install curated skills from openai/skills or other repos\n---\n\n# Skill Installer\n\nHelps install skills. By default these are from https://github.com/openai/skills/tree/main/skills/.curated, but users can also provide other locations.\n\nUse the helper scripts based on the task:\n- List skills when the user asks what is available, or if the user uses this skill without specifying what to do. Default listing is `.curated`, but you can pass `--path skills/.experimental` when they ask about experimental skills.\n- Install from the curated list when the user provides a skill name.\n- Install from another repo when the user provides a GitHub repo/path (including private repos).\n\nInstall skills with the helper scripts.\n\n## Communication\n\nWhen listing skills, output approximately as follows, depending on the context of the user's request. If they ask about experimental skills, list from `.experimental` instead of `.curated` and label the source accordingly:\n\"\"\"\nSkills from {repo}:\n1. skill-1\n2. skill-2 (already installed)\n3. ...\nWhich ones would you like installed?\n\"\"\"\n\nAfter installing a skill, tell the user: \"Restart Codex to pick up new skills.\"\n\n## Scripts\n\nAll of these scripts use network, so when running in the sandbox, request escalation when running them.\n\n- `scripts/list-skills.py` (prints skills list with installed annotations)\n- `scripts/list-skills.py --format json`\n- Example (experimental list): `scripts/list-skills.py --path skills/.experimental`\n- `scripts/install-skill-from-github.py --repo <owner>/<repo> --path <path/to/skill> [<path/to/skill> ...]`\n- `scripts/install-skill-from-github.py --url https://github.com/<owner>/<repo>/tree/<ref>/<path>`\n- Example (experimental skill): `scripts/install-skill-from-github.py --repo openai/skills --path skills/.experimental/<skill-name>`\n\n## Behavior and Options\n\n- Defaults to direct download for public GitHub repos.\n- If download fails with auth/permission errors, falls back to git sparse checkout.\n- Aborts if the destination skill directory already exists.\n- Installs into `$CODEX_HOME/skills/<skill-name>` (defaults to `~/.codex/skills`).\n- Multiple `--path` values install multiple skills in one run, each named from the path basename unless `--name` is supplied.\n- Options: `--ref <ref>` (default `main`), `--dest <path>`, `--method auto|download|git`.\n\n## Notes\n\n- Curated listing is fetched from `https://github.com/openai/skills/tree/main/skills/.curated` via the GitHub API. If it is unavailable, explain the error and exit.\n- Private GitHub repos can be accessed via existing git credentials or optional `GITHUB_TOKEN`/`GH_TOKEN` for download.\n- Git fallback tries HTTPS first, then SSH.\n- The skills at https://github.com/openai/skills/tree/main/skills/.system are preinstalled, so no need to help users install those. If they ask, just explain this. If they insist, you can download and overwrite.\n- Installed annotations come from `$CODEX_HOME/skills`.\n"
  },
  {
    "path": "skills/.system/skill-installer/agents/openai.yaml",
    "content": "interface:\n  display_name: \"Skill Installer\"\n  short_description: \"Install curated skills from openai/skills or other repos\"\n  icon_small: \"./assets/skill-installer-small.svg\"\n  icon_large: \"./assets/skill-installer.png\"\n"
  },
  {
    "path": "skills/.system/skill-installer/scripts/github_utils.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Shared GitHub helpers for skill install scripts.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport urllib.request\n\n\ndef github_request(url: str, user_agent: str) -> bytes:\n    headers = {\"User-Agent\": user_agent}\n    token = os.environ.get(\"GITHUB_TOKEN\") or os.environ.get(\"GH_TOKEN\")\n    if token:\n        headers[\"Authorization\"] = f\"token {token}\"\n    req = urllib.request.Request(url, headers=headers)\n    with urllib.request.urlopen(req) as resp:\n        return resp.read()\n\n\ndef github_api_contents_url(repo: str, path: str, ref: str) -> str:\n    return f\"https://api.github.com/repos/{repo}/contents/{path}?ref={ref}\"\n"
  },
  {
    "path": "skills/.system/skill-installer/scripts/install-skill-from-github.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Install a skill from a GitHub repo path into $CODEX_HOME/skills.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nfrom dataclasses import dataclass\nimport os\nimport shutil\nimport subprocess\nimport sys\nimport tempfile\nimport urllib.error\nimport urllib.parse\nimport zipfile\n\nfrom github_utils import github_request\nDEFAULT_REF = \"main\"\n\n\n@dataclass\nclass Args:\n    url: str | None = None\n    repo: str | None = None\n    path: list[str] | None = None\n    ref: str = DEFAULT_REF\n    dest: str | None = None\n    name: str | None = None\n    method: str = \"auto\"\n\n\n@dataclass\nclass Source:\n    owner: str\n    repo: str\n    ref: str\n    paths: list[str]\n    repo_url: str | None = None\n\n\nclass InstallError(Exception):\n    pass\n\n\ndef _codex_home() -> str:\n    return os.environ.get(\"CODEX_HOME\", os.path.expanduser(\"~/.codex\"))\n\n\ndef _tmp_root() -> str:\n    base = os.path.join(tempfile.gettempdir(), \"codex\")\n    os.makedirs(base, exist_ok=True)\n    return base\n\n\ndef _request(url: str) -> bytes:\n    return github_request(url, \"codex-skill-install\")\n\n\ndef _parse_github_url(url: str, default_ref: str) -> tuple[str, str, str, str | None]:\n    parsed = urllib.parse.urlparse(url)\n    if parsed.netloc != \"github.com\":\n        raise InstallError(\"Only GitHub URLs are supported for download mode.\")\n    parts = [p for p in parsed.path.split(\"/\") if p]\n    if len(parts) < 2:\n        raise InstallError(\"Invalid GitHub URL.\")\n    owner, repo = parts[0], parts[1]\n    ref = default_ref\n    subpath = \"\"\n    if len(parts) > 2:\n        if parts[2] in (\"tree\", \"blob\"):\n            if len(parts) < 4:\n                raise InstallError(\"GitHub URL missing ref or path.\")\n            ref = parts[3]\n            subpath = \"/\".join(parts[4:])\n        else:\n            subpath = \"/\".join(parts[2:])\n    return owner, repo, ref, subpath or None\n\n\ndef _download_repo_zip(owner: str, repo: str, ref: str, dest_dir: str) -> str:\n    zip_url = f\"https://codeload.github.com/{owner}/{repo}/zip/{ref}\"\n    zip_path = os.path.join(dest_dir, \"repo.zip\")\n    try:\n        payload = _request(zip_url)\n    except urllib.error.HTTPError as exc:\n        raise InstallError(f\"Download failed: HTTP {exc.code}\") from exc\n    with open(zip_path, \"wb\") as file_handle:\n        file_handle.write(payload)\n    with zipfile.ZipFile(zip_path, \"r\") as zip_file:\n        _safe_extract_zip(zip_file, dest_dir)\n        top_levels = {name.split(\"/\")[0] for name in zip_file.namelist() if name}\n    if not top_levels:\n        raise InstallError(\"Downloaded archive was empty.\")\n    if len(top_levels) != 1:\n        raise InstallError(\"Unexpected archive layout.\")\n    return os.path.join(dest_dir, next(iter(top_levels)))\n\n\ndef _run_git(args: list[str]) -> None:\n    result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n    if result.returncode != 0:\n        raise InstallError(result.stderr.strip() or \"Git command failed.\")\n\n\ndef _safe_extract_zip(zip_file: zipfile.ZipFile, dest_dir: str) -> None:\n    dest_root = os.path.realpath(dest_dir)\n    for info in zip_file.infolist():\n        extracted_path = os.path.realpath(os.path.join(dest_dir, info.filename))\n        if extracted_path == dest_root or extracted_path.startswith(dest_root + os.sep):\n            continue\n        raise InstallError(\"Archive contains files outside the destination.\")\n    zip_file.extractall(dest_dir)\n\n\ndef _validate_relative_path(path: str) -> None:\n    if os.path.isabs(path) or os.path.normpath(path).startswith(\"..\"):\n        raise InstallError(\"Skill path must be a relative path inside the repo.\")\n\n\ndef _validate_skill_name(name: str) -> None:\n    altsep = os.path.altsep\n    if not name or os.path.sep in name or (altsep and altsep in name):\n        raise InstallError(\"Skill name must be a single path segment.\")\n    if name in (\".\", \"..\"):\n        raise InstallError(\"Invalid skill name.\")\n\n\ndef _git_sparse_checkout(repo_url: str, ref: str, paths: list[str], dest_dir: str) -> str:\n    repo_dir = os.path.join(dest_dir, \"repo\")\n    clone_cmd = [\n        \"git\",\n        \"clone\",\n        \"--filter=blob:none\",\n        \"--depth\",\n        \"1\",\n        \"--sparse\",\n        \"--single-branch\",\n        \"--branch\",\n        ref,\n        repo_url,\n        repo_dir,\n    ]\n    try:\n        _run_git(clone_cmd)\n    except InstallError:\n        _run_git(\n            [\n                \"git\",\n                \"clone\",\n                \"--filter=blob:none\",\n                \"--depth\",\n                \"1\",\n                \"--sparse\",\n                \"--single-branch\",\n                repo_url,\n                repo_dir,\n            ]\n        )\n    _run_git([\"git\", \"-C\", repo_dir, \"sparse-checkout\", \"set\", *paths])\n    _run_git([\"git\", \"-C\", repo_dir, \"checkout\", ref])\n    return repo_dir\n\n\ndef _validate_skill(path: str) -> None:\n    if not os.path.isdir(path):\n        raise InstallError(f\"Skill path not found: {path}\")\n    skill_md = os.path.join(path, \"SKILL.md\")\n    if not os.path.isfile(skill_md):\n        raise InstallError(\"SKILL.md not found in selected skill directory.\")\n\n\ndef _copy_skill(src: str, dest_dir: str) -> None:\n    os.makedirs(os.path.dirname(dest_dir), exist_ok=True)\n    if os.path.exists(dest_dir):\n        raise InstallError(f\"Destination already exists: {dest_dir}\")\n    shutil.copytree(src, dest_dir)\n\n\ndef _build_repo_url(owner: str, repo: str) -> str:\n    return f\"https://github.com/{owner}/{repo}.git\"\n\n\ndef _build_repo_ssh(owner: str, repo: str) -> str:\n    return f\"git@github.com:{owner}/{repo}.git\"\n\n\ndef _prepare_repo(source: Source, method: str, tmp_dir: str) -> str:\n    if method in (\"download\", \"auto\"):\n        try:\n            return _download_repo_zip(source.owner, source.repo, source.ref, tmp_dir)\n        except InstallError as exc:\n            if method == \"download\":\n                raise\n            err_msg = str(exc)\n            if \"HTTP 401\" in err_msg or \"HTTP 403\" in err_msg or \"HTTP 404\" in err_msg:\n                pass\n            else:\n                raise\n    if method in (\"git\", \"auto\"):\n        repo_url = source.repo_url or _build_repo_url(source.owner, source.repo)\n        try:\n            return _git_sparse_checkout(repo_url, source.ref, source.paths, tmp_dir)\n        except InstallError:\n            repo_url = _build_repo_ssh(source.owner, source.repo)\n            return _git_sparse_checkout(repo_url, source.ref, source.paths, tmp_dir)\n    raise InstallError(\"Unsupported method.\")\n\n\ndef _resolve_source(args: Args) -> Source:\n    if args.url:\n        owner, repo, ref, url_path = _parse_github_url(args.url, args.ref)\n        if args.path is not None:\n            paths = list(args.path)\n        elif url_path:\n            paths = [url_path]\n        else:\n            paths = []\n        if not paths:\n            raise InstallError(\"Missing --path for GitHub URL.\")\n        return Source(owner=owner, repo=repo, ref=ref, paths=paths)\n\n    if not args.repo:\n        raise InstallError(\"Provide --repo or --url.\")\n    if \"://\" in args.repo:\n        return _resolve_source(\n            Args(url=args.repo, repo=None, path=args.path, ref=args.ref)\n        )\n\n    repo_parts = [p for p in args.repo.split(\"/\") if p]\n    if len(repo_parts) != 2:\n        raise InstallError(\"--repo must be in owner/repo format.\")\n    if not args.path:\n        raise InstallError(\"Missing --path for --repo.\")\n    paths = list(args.path)\n    return Source(\n        owner=repo_parts[0],\n        repo=repo_parts[1],\n        ref=args.ref,\n        paths=paths,\n    )\n\n\ndef _default_dest() -> str:\n    return os.path.join(_codex_home(), \"skills\")\n\n\ndef _parse_args(argv: list[str]) -> Args:\n    parser = argparse.ArgumentParser(description=\"Install a skill from GitHub.\")\n    parser.add_argument(\"--repo\", help=\"owner/repo\")\n    parser.add_argument(\"--url\", help=\"https://github.com/owner/repo[/tree/ref/path]\")\n    parser.add_argument(\n        \"--path\",\n        nargs=\"+\",\n        help=\"Path(s) to skill(s) inside repo\",\n    )\n    parser.add_argument(\"--ref\", default=DEFAULT_REF)\n    parser.add_argument(\"--dest\", help=\"Destination skills directory\")\n    parser.add_argument(\n        \"--name\", help=\"Destination skill name (defaults to basename of path)\"\n    )\n    parser.add_argument(\n        \"--method\",\n        choices=[\"auto\", \"download\", \"git\"],\n        default=\"auto\",\n    )\n    return parser.parse_args(argv, namespace=Args())\n\n\ndef main(argv: list[str]) -> int:\n    args = _parse_args(argv)\n    try:\n        source = _resolve_source(args)\n        source.ref = source.ref or args.ref\n        if not source.paths:\n            raise InstallError(\"No skill paths provided.\")\n        for path in source.paths:\n            _validate_relative_path(path)\n        dest_root = args.dest or _default_dest()\n        tmp_dir = tempfile.mkdtemp(prefix=\"skill-install-\", dir=_tmp_root())\n        try:\n            repo_root = _prepare_repo(source, args.method, tmp_dir)\n            installed = []\n            for path in source.paths:\n                skill_name = args.name if len(source.paths) == 1 else None\n                skill_name = skill_name or os.path.basename(path.rstrip(\"/\"))\n                _validate_skill_name(skill_name)\n                if not skill_name:\n                    raise InstallError(\"Unable to derive skill name.\")\n                dest_dir = os.path.join(dest_root, skill_name)\n                if os.path.exists(dest_dir):\n                    raise InstallError(f\"Destination already exists: {dest_dir}\")\n                skill_src = os.path.join(repo_root, path)\n                _validate_skill(skill_src)\n                _copy_skill(skill_src, dest_dir)\n                installed.append((skill_name, dest_dir))\n        finally:\n            if os.path.isdir(tmp_dir):\n                shutil.rmtree(tmp_dir, ignore_errors=True)\n        for skill_name, dest_dir in installed:\n            print(f\"Installed {skill_name} to {dest_dir}\")\n        return 0\n    except InstallError as exc:\n        print(f\"Error: {exc}\", file=sys.stderr)\n        return 1\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main(sys.argv[1:]))\n"
  },
  {
    "path": "skills/.system/skill-installer/scripts/list-skills.py",
    "content": "#!/usr/bin/env python3\n\"\"\"List skills from a GitHub repo path.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport os\nimport sys\nimport urllib.error\n\nfrom github_utils import github_api_contents_url, github_request\n\nDEFAULT_REPO = \"openai/skills\"\nDEFAULT_PATH = \"skills/.curated\"\nDEFAULT_REF = \"main\"\n\n\nclass ListError(Exception):\n    pass\n\n\nclass Args(argparse.Namespace):\n    repo: str\n    path: str\n    ref: str\n    format: str\n\n\ndef _request(url: str) -> bytes:\n    return github_request(url, \"codex-skill-list\")\n\n\ndef _codex_home() -> str:\n    return os.environ.get(\"CODEX_HOME\", os.path.expanduser(\"~/.codex\"))\n\n\ndef _installed_skills() -> set[str]:\n    root = os.path.join(_codex_home(), \"skills\")\n    if not os.path.isdir(root):\n        return set()\n    entries = set()\n    for name in os.listdir(root):\n        path = os.path.join(root, name)\n        if os.path.isdir(path):\n            entries.add(name)\n    return entries\n\n\ndef _list_skills(repo: str, path: str, ref: str) -> list[str]:\n    api_url = github_api_contents_url(repo, path, ref)\n    try:\n        payload = _request(api_url)\n    except urllib.error.HTTPError as exc:\n        if exc.code == 404:\n            raise ListError(\n                \"Skills path not found: \"\n                f\"https://github.com/{repo}/tree/{ref}/{path}\"\n            ) from exc\n        raise ListError(f\"Failed to fetch skills: HTTP {exc.code}\") from exc\n    data = json.loads(payload.decode(\"utf-8\"))\n    if not isinstance(data, list):\n        raise ListError(\"Unexpected skills listing response.\")\n    skills = [item[\"name\"] for item in data if item.get(\"type\") == \"dir\"]\n    return sorted(skills)\n\n\ndef _parse_args(argv: list[str]) -> Args:\n    parser = argparse.ArgumentParser(description=\"List skills.\")\n    parser.add_argument(\"--repo\", default=DEFAULT_REPO)\n    parser.add_argument(\n        \"--path\",\n        default=DEFAULT_PATH,\n        help=\"Repo path to list (default: skills/.curated)\",\n    )\n    parser.add_argument(\"--ref\", default=DEFAULT_REF)\n    parser.add_argument(\n        \"--format\",\n        choices=[\"text\", \"json\"],\n        default=\"text\",\n        help=\"Output format\",\n    )\n    return parser.parse_args(argv, namespace=Args())\n\n\ndef main(argv: list[str]) -> int:\n    args = _parse_args(argv)\n    try:\n        skills = _list_skills(args.repo, args.path, args.ref)\n        installed = _installed_skills()\n        if args.format == \"json\":\n            payload = [\n                {\"name\": name, \"installed\": name in installed} for name in skills\n            ]\n            print(json.dumps(payload))\n        else:\n            for idx, name in enumerate(skills, start=1):\n                suffix = \" (already installed)\" if name in installed else \"\"\n                print(f\"{idx}. {name}{suffix}\")\n        return 0\n    except ListError as exc:\n        print(f\"Error: {exc}\", file=sys.stderr)\n        return 1\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main(sys.argv[1:]))\n"
  }
]