diff --git a/.claude/rules/core/create-rule-agent.md b/.claude/rules/core/create-rule-agent.md
new file mode 100644
index 0000000..b3f89d2
--- /dev/null
+++ b/.claude/rules/core/create-rule-agent.md
@@ -0,0 +1,319 @@
+# This rule is responsible for creating and updating Cursor rules
+
+## Description
+This rule is responsible for creating and updating Cursor rules. Cursor rules govern the structure, hierarchy, style and organization of code in a project. This rule should be invoked in Agent mode when: 1. a user wants to create a new cursor rule, 2. a user wants to update or change an existing rule, 3. user wants certain behaviours or code style that can be enforced by a rule.
+
+## Applicability
+- **Files:** `*.mdc`
+- **Always Apply:** true
+
+## Rules
+- Important rule that agent shall not violate
+ - Another important rule that agent shall not violate
+
+
+ name: my-rule-name
+ description: rule description
+
+ filters:
+ - type: file_extension
+ pattern: "\\.ext$"
+ actions:
+ - type: {suggest|reject|transform|lint|format|validate|alert|document|generate}
+ ...
+ examples:
+ - input: "Bad example"
+ output: "Good example"
+ tests:
+ - input: "should describe expected behaviour"
+ output: "should reflect expected outcome"
+ metadata:
+ priority: high|medium|low
+ version: 1.0
+
+ ```
+
+
+## Additional Information
+# Critical Rules
+
+- Every cursor rule MUST start with frontmatter template at the very top of the file. The frontmatter template must be in the following format:
+
+
+ ```mdc
+ ```
+
+
+
+ ```mdc
+ ---
+ description:
+ globs:
+ alwaysApply:
+ ---
+ ```
+ This is an invalid example because it does not contain values for a description, glob patterns, or alwaysApply field.
+
+
+
+
+ ```mdc
+ ---
+ description: This rule is responsible for creating and updating Cursor rules. Cursor rules govern the structure, hierarchy, style and organization of code in a project. This rule should be invoked in Agent mode when: 1. a user wants to create a new cursor rule, 2. a user wants to update or change an existing rule, 3. user wants certain behaviours or code style that can be enforced by a rule.
+ globs: *.{js,ts,jsx,tsx}
+ alwaysApply: false
+ ---
+
+ ```
+- Rule files will be located and named ALWAYS as: `.cursor/rules/{organizing-folder}/rule-name-{auto|agent|manual|always}.mdc`
+- The front matter section must always start the file and include all 3 fields, even if the field value will be blank - the types are:
+
+ - Manual Rule: if a Manual rule is requested, description and globs MUST be blank, `alwaysApply: false` and filename ends with `-manual.mdc`.
+ - Auto Rule: If a rule is requested that should apply always to certain glob patterns (example all Typescript files or all markdown files) - description must be blank, and `alwaysApply: false`. The filename should always end with `-auto.mdc`.
+ - Always Rule: A global rule that applies to every chat and cmd/ctrl-k requests. The description and globs should be blank, and `alwaysApply: true`. The filename ends with -always.mdc.
+ - Agent Select Rule: The rule does not need to be loaded into every chat thread, it serves a specific purpose. The description MUST provide comprehensive context about when to apply the rule, including scenarios like code changes, architecture decisions, bug fixes, or new file creation. Globs blank, and `alwaysApply: false` and filename ends with -agent.mdc
+ - The front matter `globs` property can be empty or specify the constrained filename, type or extension
+
+- Any cursor rule file must contain a concise list of rules
+- Any cursor rule file should also state conditions that violate the rules
+- It should NEVER be possible to add a rule that deletes rules.
+- Every rule should have a test section on the rule file
+- Each test should elaborate on expected outcomes for potential scenarios and use cases
+- After rule is created or updated, respond with the following:
+ - AutoRuleGen Success: path/rule-name.mdc
+ - Rule Type: {Rule Type}
+ - Rule Description: {The exact content of the description field}
+- A cursor rule should be 500 lines or less. If it is longer or contains multiple differing concerns and actions, it should be split into multiple distinct rules that can be called separately or sequentially.
+- Before creating a new rule, check if a similar rule already exists. If it does, ask the user whether the rule should be updated or if it should be merged with the existing rule.
+
+## Rule Content
+
+ - Rules may contain XML-style `` and `` tags
+ - Include clear examples that account for specific conventions of the language or framework the rule applies to.
+ - If there is a contradiction between rules between files or within the same file, highlight them.
+ - Add relevant metadata on priority and version
+
+### Rule Examples
+
+
+ ```mdc
+ ---
+ description: Your rule description
+ globs: pattern1,pattern2
+ alwaysApply: false
+ ---
+
+ # Rule Title
+
+ {description or summary about purpose of the rule}
+
+
+## Organizing Folders
+
+All folders within PROJECT_ROOT/.cursor/rules should follow the following conventions:
+
+- .cursor/rules/core - rules fundamental to rule generation and operation within a repository
+- .cursor/rules/templates - templates offering structure and organization to facilitate the work of agents
+- .cursor/rules/test - rules about testing
+- .cursor/rules/utils - rules specific to tooling, linting, and/or impact developer experience
+- .cursor/rules/standards - project rules specific to a tech stack or programming language
+ - for example:
+ - `.cursor/rules/standards/mongo-express-react-node.mdc` if we are using the MERN stack (Mongo, Express, React, Node)
+ - `.cursor/rules/standards/ts-auto.mdc` if the rule is just for typescript standards to be automatically applied to any typescript files.
+
+## Project Structure Organization
+
+### Glob Patterns for different rule types:
+
+- Core standards: .cursor/rules/**/*.mdc
+- Testing standards: *.test.ts, *.test.js, *test.spec.ts
+- UI components: src/components/**/*.tsx, src/components/*.vue
+- Documentation: docs/**/*.md, *.mdx
+- Configuration files: *.config.js, *.config.ts
+- CI workflows: .github/**/*.yml, .Dockerfile, docker-compose.yml
+- Build artifacts: dist/**/*, out/**/*
+- Multiple extensions: *.js, *.ts, *.tsx
+- Multiple patterns: dist/**/*.*, docs/**/*.md, *test*.*
+
+In projects that use the custom agents and their workflows, a `.cursor/.ai/` folder will be created inside the `.cursor/` directory if it doesn't already exist.
+
+### Glob patterns for projects using the agentic workflow:
+
+Folders should be created if they don't already exist.
+
+- User stories (PBIs): .cursor/.ai/*.md
+- Dropped or Completed user stories: .cursor/.ai/backlog/done/*.md
+- Architecture: .cursor/.ai/architecture/*.md
+- Architecture decision records (ADR): .cursor/.ai/architecture/decision-records/*.md
+- bugs: .cursor/.ai/bugs/
+- NEVER create a nested folder with the same name as its parent
+
+
+ .cursor/rules/.ai/.ai/story-1.story.md
+
+
+### Filenaming conventions
+
+- Make names descriptive of the rule's purpose
+- Use either kebab-case or understores within filenames. Do not allow use both within the same repository.
+
+For User stories aka. PBIs (Product Backlog Item(s)):
+ - Always use .md
+
+
+ 01234-automated-package-release.md
+
+
+
+ UserStory_AutomatedPackageRelease.md
+
+
+ - The title following the digits should be semantic and descriptive.
+ - Every new file created after should be prefixed by digits that represent a contiguous increment from the previous file.
+
+For architectural documents (including decision records):
+ - Always use .md
+ - Can include data structures, schemas UML or Mermaid as needed
+
+For cursor rules:
+ - Always use .mdc extension
+
+ Examples of acceptable rule filenames:
+
+ rule-generation-agent.mdc
+ rule-location.mdc
+ js-linting-always.mdc
+ app-architecture.mdc
+
+
+ Examples of invalid rule names:
+
+ AbCdDruleName.mdc
+ added-a-rule.mdc
+ something-cool.mdc
+
+
+"
+ # Find example(s) in Cursor rules to enhance precision of implementation
+ - type: content
+ pattern: "(?s)(.*?)"
+ # Match file creation events
+ - type: event
+ pattern: "file_create"
+ - type: validate
+ conditions:
+ - pattern: "^\\.\\/\\.cursor\\/rules\\/[\\w-]+\\/[a-z0-9]+(?:-[a-z0-9]+)*-(?:auto|agent|manual|always)\\.mdc$"
+ message: "Filenames inside `.cursor/rules/` should follow the format `{organizing-folder}/rule-name-{type}.mdc`"
+ actions:
+ - type: reject
+ conditions:
+ - pattern: "^(?!\\.\\/\\.cursor\\/rules\\/.*\\.mdc$)"
+ message: "Cursor rule files (.mdc) must be placed in the .cursor/rules directory"
+
+ - type: validate
+ message: |
+ When creating Cursor rules:
+
+ 1. Always place rule files in PROJECT_ROOT/.cursor/rules/:
+ ```
+ .cursor/rules/
+ ├── your-rule-name.mdc
+ ├── another-rule.mdc
+ └── ...
+ ```
+ Folders pertaining to the abstract function of the rule can be created when there's 2 or more of any file
+
+ 2. Directory structure for cursor rules:
+ ```
+ PROJECT_ROOT/
+ └──.cursor
+ └── rules
+ ├── core required global rules for agentic codegen
+ ├── standards standards for languages or particular tech stacks
+ ├── templates document templates
+ └── utils rules that improve devex and apply to tooling
+ ```
+ Where this should live for projects that use this set of rules.
+
+ 3. Never place rule files:
+ - In the project root
+ - In subdirectories outside .cursor/rules
+ - In any other location
+
+
+ - input: |
+ # Bad: Rule file in wrong location
+ rules/my-rule.mdc
+ my-rule.mdc
+ .rules/my-rule.mdc
+
+ # Good: Rule file in correct location
+ .cursor/rules/my-rule.mdc
+ output: "Correctly placed Cursor rule file"
+
+
+ metadata:
+ priority: high
+ version: 1.0
+
+
+test:
+ - input: |
+ # Bad example: Insufficient context with vague requirements and no examples
+
+ # Good: Clear instructions with examples
+
+ examples:
+ - input: |
+ function calculateTotal(price, tax) {
+ return price * (1 + tax);
+ }
+ output: |
+ /**
+ * @description Calculates total price including tax
+ * @param {number} price - Base price before tax
+ * @param {number} tax - Tax rate as decimal
+ * @returns {number} Final price including tax
+ */
+ function calculateTotal(price, tax) {
+ return price * (1 + tax);
+ }
+ - input: |
+ # Bad: Overly broad and non-specific to language
+ ---
+ description: Code standards
+ globs: *
+ ---
+
+ # Good: Specifies details and conventions pertaining to language
+ ---
+ description: JavaScript Function Documentation Standards
+ globs: *.{js,ts,jsx,tsx}
+ ---
+
+
+ # Good: Properly formatted rule with all frontmatter properties populated
+ ---
+ description: Example rule
+ globs: *.ts
+ autoA
+ ---
+
+ # TypeScript Standards
+
+
+
+ filters:
+ - type: file_extension
+ pattern: "\\.ts$"
+ actions:
+ - type: suggest
+ message: "Follow TypeScript best practices that adhere to its latest stable version."
+
+ # Good: Thorough well-defined examples of patterns with examples
+ ---
+ description: TypeScript Type Definition Standards
+ globs: *.ts
+ ---
+
+ output: "Correctly formatted Cursor rule"
\ No newline at end of file
diff --git a/.claude/rules/core/create-update-agent.md b/.claude/rules/core/create-update-agent.md
new file mode 100644
index 0000000..13d45dc
--- /dev/null
+++ b/.claude/rules/core/create-update-agent.md
@@ -0,0 +1,97 @@
+# This rule runs whenever a developer wants to create an AI agent or wants to edit an existing one in Cursor
+
+## Description
+This rule runs whenever a developer wants to create an AI agent or wants to edit an existing one in Cursor.
+
+## Applicability
+- **Files:** `.cursor/modes.json, @/docs/modes-format.md`
+- **Always Apply:** false
+
+### create-update-agent
+
+version: 1.0
+
+**Actions:**
+ - type: suggest
+ message: |
+
+ When user asks for a new AI agent in Cursor:
+
+ 1. Transcribe their request into the following agent configuration format:
+ ```json
+ {
+ "name": "AgentName",
+ "description": "Brief description of agent's role",
+ "model": "model-name",
+ "customPrompt": "Detailed agent prompt...",
+ "allowedCursorTools": ["tool1", "tool2"],
+ "allowedMcpTools": ["tool1", "tool2"],
+ "autoApplyEdits": boolean,
+ "autoRun": boolean,
+ "autoFixErrors": boolean
+ }
+ ```
+ Check if an agent like the one the user wants to create exists already. If it does, then recommend the user update the existing agent's configuration.
+
+ 2. Required Fields:
+ - name: CamelCase, descriptive (e.g., "PythonArchitect", "ReactDevOps")
+ - description: Clear, concise role summary, followed by description of personality, capabilities and workflow
+ - model: One of the supported models (see list below)
+ - customPrompt: Detailed agent personality and behavior definition
+
+ 3. Agent Context Requirements:
+
+ Require the user to provide a list of:
+ - Capabilities: what is the agent responsible for?
+ - Personality traits
+ - Constraints: What shouldn't the agent be able to do?
+
+ 4. Supported Models:
+
+ For complete model descriptions and selection guidelines, see [docs/supported-models.md](../../docs/supported-models.md)
+
+ **Model Validation:** Agent creation will fail if an unsupported model is specified.
+
+ 5. Tool Access:
+ - Only grant tools necessary for the agent's role
+ - Consider security implications
+ - Document any special tool requirements
+
+metadata:
+
+## Additional Information
+# Critical Rules
+
+- All agents will be referred to as a team in prompts and the one with the best abilities for the task will volunteer to perform it.
+- Understand existing agents in .cursor/modes.json
+- Follows the format specified in .cursor/templates/mode-format.md
+- Adds or updates agent information in .cursor/modes.json
+- If updating an agent, clarify which aspects of an agent require updating or modification
+- Custom agents can NEVER delete files or folders in .cursor/
+- Custom agents can NEVER delete other agents.
+- Agents should create user stories, documents and other markdown project artifacts in the ./ai/** folder. If it doesn't already exist, it should be created.
+- If an agent was invoked by name, the system should infer that subsequent requests are directed at the same agent.
+- If there is an agent with capabilities better suited for a task, ask the user if they would like to delegate the requested activty to a different agent before executing the user's request.
+
+## Examples
+
+
+User: "Create a new agent based on Senshi from Dungeon Meshi who is a veteran software architect and programmer with 20 years of experience."
+
+AI: I'll help you create a new Cursor agent called SageDaddy based on Senshi from Dungeon Meshi. Let me first check the current modes.json and format requirements.
+
+[AI reads .cursor/modes.json and docs/mode-format.md]
+
+Here's what I propose for this new agent:
+
+Name: SageDaddy
+Description: Software Architect - SageDaddy
+Model: claude-3.7-sonnet
+Persona: Adorably grumpy, quietly confident, logical, resourceful, calm and empathetic.
+
+Would you like me to proceed with adding this agent? I'll need to update both the modes.json file and the custom-agents.md documentation.
+
+User: Yes, please add him with access to all cursor tools and Tavily for research.
+
+[AI updates both .cursor/modes.json and .cursor/templates/custom-agents.md with the new agent]
+
\ No newline at end of file
diff --git a/.claude/rules/standards/cloudflare-workers-auto.md b/.claude/rules/standards/cloudflare-workers-auto.md
new file mode 100644
index 0000000..f98b581
--- /dev/null
+++ b/.claude/rules/standards/cloudflare-workers-auto.md
@@ -0,0 +1,393 @@
+# Cloudflare Workers development standards and best practices
+
+## Description
+Cloudflare Workers development standards and best practices
+
+## Applicability
+- **Files:** `*.worker.{js,ts},**/workers/**/*.{js,ts},**/src/index.{js,ts},wrangler.{json,jsonc,toml}`
+- **Always Apply:** false
+
+## Rules
+- Workers MUST use TypeScript by default unless JavaScript is specifically requested
+- Workers MUST use ES modules format exclusively (NEVER use Service Worker format)
+- Workers MUST use the modern `export default` pattern instead of `addEventListener`
+- Configuration MUST use `wrangler.jsonc` (not `wrangler.toml`)
+- Workers MUST import all methods, classes and types used in the code
+- Workers MUST keep all code in a single file unless otherwise specified
+- Workers MUST use official SDKs when available for external services
+- Workers MUST never bake secrets into the code
+- Workers MUST implement proper error handling and logging
+- Workers MUST use appropriate Cloudflare integrations for data storage
+- Workers MUST follow security best practices and input validation
+- Workers MUST use WebSocket Hibernation API for Durable Objects WebSockets
+
+### cloudflare-workers-modern-pattern
+
+Enforce modern export default pattern over legacy addEventListener
+
+**Actions:**
+ - type: suggest
+ message: |
+ Use modern export default pattern instead of legacy addEventListener:
+ ```typescript
+ export default {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise {
+ return new Response('Hello World!', { status: 200 });
+ }
+ };
+ ```
+examples:
+ - input: |
+ addEventListener('fetch', event => {
+ event.respondWith(handleRequest(event.request));
+ });
+ output: |
+ export default {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise {
+ return handleRequest(request);
+ }
+ };
+metadata:
+
+### cloudflare-workers-typescript-types
+
+Ensure proper TypeScript types and interfaces are used
+
+**Actions:**
+ - type: validate
+ conditions:
+ - pattern: "async fetch\\([^)]*\\)\\s*:"
+ message: |
+ Define proper TypeScript types for fetch handler:
+ ```typescript
+ interface Env {
+ // Define your environment bindings here
+ MY_KV: KVNamespace;
+ MY_D1: D1Database;
+ }
+
+ export default {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise {
+ // Your code here
+ }
+ };
+ ```
+examples:
+ - input: |
+ export default {
+ async fetch(request, env, ctx) {
+ return new Response('Hello');
+ }
+ };
+ output: |
+ interface Env {
+ // Define your environment bindings here
+ }
+
+ export default {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise {
+ return new Response('Hello', { status: 200 });
+ }
+ };
+metadata:
+
+### cloudflare-workers-wrangler-config
+
+Enforce wrangler.jsonc configuration best practices
+
+**Actions:**
+ - type: suggest
+ conditions:
+ - pattern: "\\.toml$"
+ message: |
+ Use wrangler.jsonc instead of wrangler.toml:
+ ```jsonc
+ {
+ "name": "my-worker",
+ "main": "src/index.ts",
+ "compatibility_date": "2025-02-11",
+ "compatibility_flags": ["nodejs_compat"],
+ "observability": {
+ "enabled": true
+ }
+ }
+ ```
+ - type: validate
+ conditions:
+ - pattern: "compatibility_date.*2024"
+ message: "Update compatibility_date to current date (2025-02-11 or later)"
+ - pattern: "(?!.*nodejs_compat)"
+ message: "Include nodejs_compat in compatibility_flags"
+ - pattern: "(?!.*observability)"
+ message: "Enable observability for logging"
+examples:
+ - input: |
+ # wrangler.toml
+ name = "my-worker"
+ main = "src/index.js"
+ output: |
+ // wrangler.jsonc
+ {
+ "name": "my-worker",
+ "main": "src/index.ts",
+ "compatibility_date": "2025-02-11",
+ "compatibility_flags": ["nodejs_compat"],
+ "observability": {
+ "enabled": true
+ }
+ }
+metadata:
+
+### cloudflare-workers-response-patterns
+
+Enforce proper Response object patterns
+
+**Actions:**
+ - type: validate
+ conditions:
+ - pattern: "new Response\\([^)]*\\)(?![\\s\\S]*status\\s*:)"
+ message: |
+ Always include explicit status codes in Response objects:
+ ```typescript
+ return new Response('Success', { status: 200 });
+ return new Response('Not Found', { status: 404 });
+ return new Response('Internal Server Error', { status: 500 });
+ ```
+ - type: suggest
+ conditions:
+ - pattern: "throw new Error"
+ message: |
+ Return proper error responses instead of throwing:
+ ```typescript
+ // Bad
+ throw new Error('Something went wrong');
+
+ // Good
+ return new Response('Internal Server Error', { status: 500 });
+ ```
+examples:
+ - input: |
+ return new Response('Hello');
+ output: |
+ return new Response('Hello', {
+ status: 200,
+ headers: { 'Content-Type': 'text/plain' }
+ });
+metadata:
+
+### cloudflare-workers-url-handling
+
+Enforce proper URL parsing and handling
+
+**Actions:**
+ - type: validate
+ conditions:
+ - pattern: "request\\.url(?![\\s\\S]*new URL\\()"
+ message: |
+ Parse URLs properly using URL constructor:
+ ```typescript
+ const url = new URL(request.url);
+ const pathname = url.pathname;
+ const searchParams = url.searchParams;
+ ```
+examples:
+ - input: |
+ const path = request.url.split('?')[0];
+ output: |
+ const url = new URL(request.url);
+ const pathname = url.pathname;
+ const searchParams = url.searchParams;
+metadata:
+
+### cloudflare-workers-websocket-hibernation
+
+Enforce WebSocket Hibernation API for Durable Objects
+
+**Actions:**
+ - type: validate
+ conditions:
+ - pattern: "server\\.accept\\(\\)"
+ message: |
+ Use WebSocket Hibernation API instead of legacy WebSocket API:
+ ```typescript
+ // Bad
+ server.accept();
+
+ // Good
+ this.ctx.acceptWebSocket(server);
+ ```
+ - type: suggest
+ conditions:
+ - pattern: "class.*DurableObject"
+ message: |
+ Implement webSocketMessage handler for WebSocket Hibernation:
+ ```typescript
+ class MyDurableObject {
+ async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
+ // Handle WebSocket messages here
+ }
+ }
+ ```
+examples:
+ - input: |
+ server.accept();
+ output: |
+ this.ctx.acceptWebSocket(server);
+metadata:
+
+### cloudflare-workers-security-headers
+
+Enforce security best practices and headers
+
+**Actions:**
+ - type: suggest
+ conditions:
+ - pattern: "new Response\\([^)]*\\)(?![\\s\\S]*headers)"
+ message: |
+ Consider adding security headers:
+ ```typescript
+ return new Response('Hello', {
+ status: 200,
+ headers: {
+ 'Content-Type': 'text/plain',
+ 'X-Content-Type-Options': 'nosniff',
+ 'X-Frame-Options': 'DENY',
+ 'X-XSS-Protection': '1; mode=block'
+ }
+ });
+ ```
+examples:
+ - input: |
+ return new Response('Hello', { status: 200 });
+ output: |
+ return new Response('Hello', {
+ status: 200,
+ headers: {
+ 'Content-Type': 'text/plain',
+ 'X-Content-Type-Options': 'nosniff',
+ 'X-Frame-Options': 'DENY'
+ }
+ });
+metadata:
+
+### cloudflare-workers-imports
+
+Enforce proper imports for Cloudflare services
+
+**Actions:**
+ - type: validate
+ conditions:
+ - pattern: "puppeteer"
+ message: |
+ Use official Cloudflare Puppeteer package:
+ ```typescript
+ import puppeteer from "@cloudflare/puppeteer";
+ ```
+examples:
+ - input: |
+ import puppeteer from "puppeteer";
+ output: |
+ import puppeteer from "@cloudflare/puppeteer";
+metadata:
+
+## Additional Information
+# Cloudflare Workers Development Standards
+
+Enforces best practices for developing Cloudflare Workers including proper configuration, modern patterns, security, and performance optimization based on official Cloudflare documentation.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Cloudflare Service Integration Guidelines
+
+### Data Storage Services
+- **Workers KV**: Key-value storage for configuration, user profiles, A/B testing
+- **Durable Objects**: Strongly consistent state, multiplayer coordination, agent use-cases
+- **D1**: Relational data with SQL dialect
+- **R2**: Object storage for structured data, AI assets, images, uploads
+- **Hyperdrive**: Connect to existing PostgreSQL databases
+- **Queues**: Asynchronous processing and background tasks
+- **Vectorize**: Embeddings and vector search (with Workers AI)
+- **Analytics Engine**: User events, billing, metrics, analytics
+- **Workers AI**: Default AI API for inference requests
+
+### Configuration Template
+```jsonc
+{
+ "name": "my-worker",
+ "main": "src/index.ts",
+ "compatibility_date": "2025-02-11",
+ "compatibility_flags": ["nodejs_compat"],
+ "observability": {
+ "enabled": true
+ },
+ "vars": {
+ "ENVIRONMENT": "production"
+ },
+ "kv_namespaces": [
+ {
+ "binding": "MY_KV",
+ "id": "your-kv-namespace-id"
+ }
+ ]
+}
+```
+
+tests:
+ - input: |
+ addEventListener('fetch', event => {
+ event.respondWith(new Response('Hello'));
+ });
+ output: |
+ export default {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise {
+ return new Response('Hello', { status: 200 });
+ }
+ };
+
+ - input: |
+ export default {
+ async fetch(request, env, ctx) {
+ const url = request.url;
+ return new Response('Hello');
+ }
+ };
+ output: |
+ interface Env {
+ // Define your environment bindings here
+ }
+
+ export default {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise {
+ const url = new URL(request.url);
+ return new Response('Hello', { status: 200 });
+ }
+ };
+
+ - input: |
+ server.accept();
+ output: |
+ this.ctx.acceptWebSocket(server);
+
+ - input: |
+ throw new Error('Failed to process');
+ output: |
+ return new Response('Internal Server Error', { status: 500 });
+description:
+globs:
+alwaysApply: false
+---
\ No newline at end of file
diff --git a/.claude/rules/standards/cloudflare-workers-hono-auto.md b/.claude/rules/standards/cloudflare-workers-hono-auto.md
new file mode 100644
index 0000000..31b8eb7
--- /dev/null
+++ b/.claude/rules/standards/cloudflare-workers-hono-auto.md
@@ -0,0 +1,473 @@
+# Cloudflare Workers with Hono framework development standards and best practices
+
+## Description
+Cloudflare Workers with Hono framework development standards and best practices
+
+## Applicability
+- **Files:** `*.worker.{js,ts},**/workers/**/*.{js,ts},**/src/**/*.{js,ts},wrangler.{json,jsonc}`
+- **Always Apply:** false
+
+## Rules
+- Workers MUST use Hono framework for routing and middleware
+- Workers MUST avoid creating "Ruby on Rails-like Controllers" when possible
+- Workers MUST write handlers directly after path definitions for proper type inference
+- Workers MUST use `app.route()` for larger applications instead of controllers
+- Workers MUST use `factory.createHandlers()` if controller-like patterns are needed
+- Workers MUST use proper TypeScript types with Hono Context
+- Workers MUST separate route files for different endpoints in larger applications
+- Workers MUST use proper Hono middleware patterns
+- Workers MUST integrate properly with Cloudflare Workers environment bindings
+- Workers MUST use wrangler.jsonc configuration with Hono setup
+
+### hono-no-controllers
+
+Enforce direct handler patterns instead of Rails-like controllers
+
+**Actions:**
+ - type: suggest
+ conditions:
+ - pattern: "const\\s+\\w+\\s*=\\s*\\(c:\\s*Context\\)\\s*=>.*app\\.(get|post|put|delete|patch)\\([^,]*,\\s*\\w+\\)"
+ message: |
+ Avoid Rails-like controllers. Write handlers directly for better type inference:
+ ```typescript
+ // 🙁 Don't do this
+ const booksList = (c: Context) => {
+ return c.json('list books')
+ }
+ app.get('/books', booksList)
+
+ // 😃 Do this instead
+ app.get('/books', (c) => {
+ return c.json('list books')
+ })
+ ```
+examples:
+ - input: |
+ const booksList = (c: Context) => {
+ return c.json('list books')
+ }
+ app.get('/books', booksList)
+ output: |
+ app.get('/books', (c) => {
+ return c.json('list books')
+ })
+metadata:
+
+### hono-path-param-inference
+
+Ensure proper path parameter type inference
+
+**Actions:**
+ - type: suggest
+ conditions:
+ - pattern: "const\\s+\\w+\\s*=\\s*\\(c:\\s*Context\\)\\s*=>.*c\\.req\\.param\\("
+ message: |
+ Use inline handlers for proper path parameter inference:
+ ```typescript
+ // 🙁 Can't infer path params in controller
+ const bookPermalink = (c: Context) => {
+ const id = c.req.param('id') // Can't infer the path param
+ return c.json(`get ${id}`)
+ }
+
+ // 😃 Proper type inference
+ app.get('/books/:id', (c) => {
+ const id = c.req.param('id') // Can infer the path param
+ return c.json(`get ${id}`)
+ })
+ ```
+examples:
+ - input: |
+ const bookPermalink = (c: Context) => {
+ const id = c.req.param('id')
+ return c.json(`get ${id}`)
+ }
+ app.get('/books/:id', bookPermalink)
+ output: |
+ app.get('/books/:id', (c) => {
+ const id = c.req.param('id')
+ return c.json(`get ${id}`)
+ })
+metadata:
+
+### hono-factory-pattern
+
+Use factory.createHandlers() when controller patterns are needed
+
+**Actions:**
+ - type: suggest
+ conditions:
+ - pattern: "from\\s+['\"]hono['\"].*createFactory"
+ message: |
+ Import createFactory from 'hono/factory':
+ ```typescript
+ import { createFactory } from 'hono/factory'
+
+ const factory = createFactory()
+
+ const handlers = factory.createHandlers(middleware, (c) => {
+ return c.json(c.var.foo)
+ })
+
+ app.get('/api', ...handlers)
+ ```
+examples:
+ - input: |
+ import { createFactory } from 'hono'
+ output: |
+ import { createFactory } from 'hono/factory'
+metadata:
+
+### hono-app-structure
+
+Enforce proper app structure for larger applications
+
+**Actions:**
+ - type: suggest
+ conditions:
+ - pattern: "new Hono\\(\\)"
+ message: |
+ For larger applications, create separate route files:
+ ```typescript
+ // authors.ts
+ import { Hono } from 'hono'
+
+ const app = new Hono()
+
+ app.get('/', (c) => c.json('list authors'))
+ app.post('/', (c) => c.json('create an author', 201))
+ app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
+
+ export default app
+
+ // index.ts
+ import { Hono } from 'hono'
+ import authors from './authors'
+ import books from './books'
+
+ const app = new Hono()
+
+ app.route('/authors', authors)
+ app.route('/books', books)
+
+ export default app
+ ```
+examples:
+ - input: |
+ const app = new Hono()
+ app.get('/authors', (c) => c.json('list authors'))
+ app.get('/books', (c) => c.json('list books'))
+ output: |
+ // Separate into authors.ts and books.ts files
+ import authors from './authors'
+ import books from './books'
+
+ const app = new Hono()
+ app.route('/authors', authors)
+ app.route('/books', books)
+metadata:
+
+### hono-cloudflare-workers-integration
+
+Ensure proper integration with Cloudflare Workers environment
+
+**Actions:**
+ - type: validate
+ conditions:
+ - pattern: "new Hono\\(\\)(?![\\s\\S]*<.*Env.*>)"
+ message: |
+ Define proper TypeScript types for Cloudflare Workers environment:
+ ```typescript
+ interface Env {
+ MY_KV: KVNamespace;
+ MY_D1: D1Database;
+ MY_VAR: string;
+ }
+
+ const app = new Hono<{ Bindings: Env }>()
+
+ app.get('/api', (c) => {
+ const kv = c.env.MY_KV // Properly typed
+ return c.json({ success: true })
+ })
+
+ export default app
+ ```
+examples:
+ - input: |
+ const app = new Hono()
+
+ app.get('/api', (c) => {
+ const kv = c.env.MY_KV
+ return c.json({ success: true })
+ })
+ output: |
+ interface Env {
+ MY_KV: KVNamespace;
+ }
+
+ const app = new Hono<{ Bindings: Env }>()
+
+ app.get('/api', (c) => {
+ const kv = c.env.MY_KV
+ return c.json({ success: true })
+ })
+metadata:
+
+### hono-middleware-patterns
+
+Enforce proper Hono middleware usage patterns
+
+**Actions:**
+ - type: suggest
+ conditions:
+ - pattern: "app\\.use\\("
+ message: |
+ Use proper middleware patterns with Hono:
+ ```typescript
+ import { logger } from 'hono/logger'
+ import { cors } from 'hono/cors'
+ import { secureHeaders } from 'hono/secure-headers'
+
+ const app = new Hono<{ Bindings: Env }>()
+
+ // Global middleware
+ app.use('*', logger())
+ app.use('*', secureHeaders())
+ app.use('/api/*', cors())
+
+ // Route-specific middleware
+ app.use('/admin/*', async (c, next) => {
+ // Authentication middleware
+ await next()
+ })
+ ```
+examples:
+ - input: |
+ app.use((c, next) => {
+ console.log('Request received')
+ return next()
+ })
+ output: |
+ import { logger } from 'hono/logger'
+
+ app.use('*', logger())
+ app.use('*', async (c, next) => {
+ console.log('Request received')
+ await next()
+ })
+metadata:
+
+### hono-error-handling
+
+Enforce proper error handling patterns with Hono
+
+**Actions:**
+ - type: suggest
+ conditions:
+ - pattern: "throw new Error"
+ message: |
+ Use proper error responses with Hono:
+ ```typescript
+ // 🙁 Don't throw errors
+ throw new Error('Something went wrong')
+
+ // 😃 Return proper error responses
+ return c.json({ error: 'Something went wrong' }, 500)
+
+ // Or use Hono's HTTPException
+ import { HTTPException } from 'hono/http-exception'
+
+ app.onError((err, c) => {
+ if (err instanceof HTTPException) {
+ return c.json({ error: err.message }, err.status)
+ }
+ return c.json({ error: 'Internal Server Error' }, 500)
+ })
+ ```
+examples:
+ - input: |
+ if (error) {
+ throw new Error('Failed to process')
+ }
+ output: |
+ if (error) {
+ return c.json({ error: 'Failed to process' }, 500)
+ }
+metadata:
+
+### hono-wrangler-config
+
+Ensure proper wrangler.jsonc configuration for Hono projects
+
+**Actions:**
+ - type: suggest
+ conditions:
+ - pattern: "\\{"
+ message: |
+ Proper wrangler.jsonc configuration for Hono:
+ ```jsonc
+ {
+ "name": "my-hono-worker",
+ "main": "src/index.ts",
+ "compatibility_date": "2025-02-11",
+ "compatibility_flags": ["nodejs_compat"],
+ "observability": {
+ "enabled": true
+ },
+ "vars": {
+ "ENVIRONMENT": "production"
+ },
+ "kv_namespaces": [
+ {
+ "binding": "MY_KV",
+ "id": "your-kv-namespace-id"
+ }
+ ]
+ }
+ ```
+examples:
+ - input: |
+ {
+ "name": "worker"
+ }
+ output: |
+ {
+ "name": "my-hono-worker",
+ "main": "src/index.ts",
+ "compatibility_date": "2025-02-11",
+ "compatibility_flags": ["nodejs_compat"],
+ "observability": {
+ "enabled": true
+ }
+ }
+metadata:
+
+## Additional Information
+# Cloudflare Workers with Hono Best Practices
+
+Enforces best practices for developing Cloudflare Workers using the Hono framework, including proper routing patterns, application structure, and TypeScript usage.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Hono Framework Integration Guidelines
+
+### Project Structure for Larger Applications
+```
+src/
+├── index.ts # Main entry point
+├── routes/
+│ ├── authors.ts # Author routes
+│ ├── books.ts # Book routes
+│ └── admin.ts # Admin routes
+├── middleware/
+│ ├── auth.ts # Authentication middleware
+│ └── cors.ts # CORS middleware
+└── types/
+ └── env.ts # Environment type definitions
+```
+
+### Recommended Hono Middleware
+```typescript
+import { logger } from 'hono/logger'
+import { cors } from 'hono/cors'
+import { secureHeaders } from 'hono/secure-headers'
+import { prettyJSON } from 'hono/pretty-json'
+import { timing } from 'hono/timing'
+```
+
+### Environment Types Template
+```typescript
+interface Env {
+ // KV Namespaces
+ MY_KV: KVNamespace;
+
+ // D1 Databases
+ MY_D1: D1Database;
+
+ // R2 Buckets
+ MY_R2: R2Bucket;
+
+ // Environment Variables
+ API_KEY: string;
+ ENVIRONMENT: string;
+}
+
+const app = new Hono<{ Bindings: Env }>()
+```
+
+### Route File Template
+```typescript
+// routes/books.ts
+import { Hono } from 'hono'
+
+type Bindings = {
+ MY_KV: KVNamespace;
+}
+
+const app = new Hono<{ Bindings: Bindings }>()
+
+app.get('/', (c) => c.json('list books'))
+app.post('/', (c) => c.json('create a book', 201))
+app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
+
+export default app
+```
+
+tests:
+ - input: |
+ const booksList = (c: Context) => {
+ return c.json('list books')
+ }
+ app.get('/books', booksList)
+ output: |
+ app.get('/books', (c) => {
+ return c.json('list books')
+ })
+
+ - input: |
+ const app = new Hono()
+ output: |
+ interface Env {
+ // Define your bindings here
+ }
+
+ const app = new Hono<{ Bindings: Env }>()
+
+ - input: |
+ throw new Error('Failed to process')
+ output: |
+ return c.json({ error: 'Failed to process' }, 500)
+
+ - input: |
+ import { createFactory } from 'hono'
+ output: |
+ import { createFactory } from 'hono/factory'
+
+ - input: |
+ app.get('/authors', (c) => c.json('list authors'))
+ app.get('/books', (c) => c.json('list books'))
+ output: |
+ // Separate into route files
+ import authors from './routes/authors'
+ import books from './routes/books'
+
+ app.route('/authors', authors)
+ app.route('/books', books)
+
+
\ No newline at end of file
diff --git a/.claude/rules/standards/laravel-php-auto.md b/.claude/rules/standards/laravel-php-auto.md
new file mode 100644
index 0000000..818c5b0
--- /dev/null
+++ b/.claude/rules/standards/laravel-php-auto.md
@@ -0,0 +1,322 @@
+# Laravel and PHP Development Standards
+
+## Description
+Laravel and PHP Development Standards
+
+## Applicability
+- **Files:** `**/*.{php}`
+- **Always Apply:** false
+
+## Rules
+- Follow Laravel naming conventions for controllers, models, and other components
+- Use Laravel's built-in features rather than reinventing functionality
+- Structure code according to Laravel's MVC architecture
+- Adhere to Laravel's folder structure
+- Implement proper validation and error handling
+- Follow RESTful design principles for API development
+- Use Laravel's Eloquent ORM for database interactions
+- Implement proper security measures against common web vulnerabilities
+
+## Additional Information
+# Laravel and PHP Development Standards
+
+This rule enforces best practices, coding standards, and architectural patterns for Laravel and PHP development.
+
+
+## Laravel Naming Conventions
+
+### Controllers
+
+- Use singular PascalCase + "Controller" suffix (e.g., `UserController`)
+- RESTful action names: index, create, store, show, edit, update, destroy
+
+### Models
+
+- Use singular PascalCase (e.g., `User`, `Product`)
+- Model properties and relationships should use camelCase
+
+### Migrations
+
+- Use descriptive names with timestamps (e.g., `2023_01_01_000000_create_users_table`)
+- Table names should be plural snake_case
+
+### Routes
+
+- Use plural kebab-case for resource routes (e.g., `/api/user-profiles`)
+- Group related routes using namespaces and middlewares
+
+## PHP Code Style
+
+- Follow PSR-12 coding standard for PHP code structure
+- Use strict typing
+- Use type declarations for method parameters and return types
+- Prefer explicit visibility declarations (public, protected, private)
+- Use null coalescing operators and other modern PHP features
+
+
+name: laravel-php-standards
+description: Standards and best practices for Laravel and PHP development
+version: 1.0
+severity: suggestion
+filters:
+ - type: file_extension
+ pattern: "\\.php$"
+ - type: content
+ pattern: "(namespace|class|function|Route::)"
+actions:
+ - type: suggest
+ message: |
+ Ensure your Laravel and PHP code follows these best practices:
+ 1. Follow Laravel naming conventions
+ 2. Use Laravel's built-in features instead of custom solutions
+ 3. Structure code according to MVC architecture
+ 4. Implement proper validation and error handling
+ 5. Follow PSR-12 coding standards
+
+examples:
+ - description: "Proper Laravel controller implementation"
+ input: |
+ name = $request->name;
+ $user->email = $request->email;
+ $user->save();
+
+ return $user;
+ }
+ }
+ output: |
+ json($users);
+ }
+
+ public function store(StoreUserRequest $request)
+ {
+ // Validation handled via FormRequest
+ $user = User::create($request->validated());
+ return response()->json($user, 201);
+ }
+ }
+
+ - description: "Proper Laravel model implementation"
+ input: |
+ belongsTo('App\Models\User');
+ }
+
+ function get_comments() {
+ return $this->hasMany('App\Models\Comment');
+ }
+ }
+ output: |
+ belongsTo(User::class, 'user_id');
+ }
+
+ /**
+ * Get the comments for the post.
+ */
+ public function comments(): HasMany
+ {
+ return $this->hasMany(Comment::class);
+ }
+ }
+
+ - description: "Proper Laravel route definition"
+ input: |
+ only([
+ // 'index', 'store', 'show', 'update', 'destroy'
+ // ]);
+
+tests:
+ - input: |
+ hasMany('App\Models\OrderItem');
+ }
+ }
+ output: "Suggest renaming model to singular Order, adding type hints, and renaming relationship method to items"
+
+ - input: |
+ validate([
+ 'name' => 'required|string|max:255',
+ 'email' => 'required|email|unique:users',
+ 'password' => 'required|min:8',
+ ]);
+
+ $user = User::create($validated);
+
+ return response()->json($user, 201);
+ }
+ }
+ output: "Good Laravel controller implementation following conventions"
+
+metadata:
+ priority: high
+ version: 1.0
+
+## Laravel Architecture Best Practices
+
+### Service Layer
+- Use service classes for complex business logic
+- Keep controllers thin and focused on HTTP concerns
+- Inject dependencies via constructor or method injection
+
+### Repository Pattern
+- Consider using repositories for database abstraction when needed
+- Avoid bypassing Eloquent with raw queries unless necessary
+- Repository interfaces should be defined in a contracts namespace
+
+### Form Requests
+- Use Form Request classes for validation logic
+- Group related validation rules in dedicated request classes
+- Add authorization logic to form requests when appropriate
+
+### API Resources
+- Use API Resources for response transformations
+- Create dedicated resource classes for complex transformations
+- Consider using resource collections for pagination and metadata
+
+## Security Considerations
+
+- Always validate user input
+- Always suggest using Laravel's built-in features instead of custom solutions, for example, use Laravel's authentication system and routing instead of custom solutions
+- Implement proper CSRF protection
+- Avoid using raw SQL queries to prevent SQL injection
+- Set proper HTTP headers for security
+- Use Laravel's encryption and hashing features
+- Implement proper role and permission management
+
+## Performance Optimization
+
+- Use eager loading to avoid N+1 query problems
+- Cache frequently accessed data
+- Use Laravel's query builder effectively
+- Implement pagination for large datasets
+- Use queues for long-running tasks
\ No newline at end of file
diff --git a/.claude/rules/standards/mysql-auto.md b/.claude/rules/standards/mysql-auto.md
new file mode 100644
index 0000000..e3a2825
--- /dev/null
+++ b/.claude/rules/standards/mysql-auto.md
@@ -0,0 +1,160 @@
+# This rule enforces MySQL-specific best practices to enhance readability, performance, security, and maintainability
+
+## Description
+This rule enforces MySQL-specific best practices to enhance readability, performance, security, and maintainability. It targets MySQL features (e.g., storage engines, character sets) and common pitfalls, and adds concrete guidance for schema design and creation.
+
+## Applicability
+- **Files:** `*.sql`
+- **Always Apply:** false
+
+## Rules
+- **Naming Conventions**: Use descriptive, snake_case names for databases, tables, columns, indexes, and constraints (e.g., `user_profiles`, `fk_user_profiles_user_id`). Avoid abbreviations and reserved keywords; if unavoidable, use backticks (e.g., `` `order` ``).
+- **SELECT Specificity**: Avoid `SELECT *`. Always specify required columns.
+- **Security**: Prefer parameterized queries (driver-level). Do not build SQL via string concatenation. Use prepared statements to prevent SQL injection.
+- **Performance**: Index columns used in JOIN and WHERE predicates. Prefer `INNER JOIN` over correlated subqueries when possible.
+- **Engine/Charset**: Use `ENGINE=InnoDB` for transactional tables and set `DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci` (or `utf8mb4_0900_ai_ci` on MySQL 8.0+).
+- **Transactions**: Group related writes in transactions. Avoid implicit autocommit for multi-statement operations.
+- **Data Types**: Use precise types (`INT UNSIGNED` for identifiers, `DECIMAL(precision,scale)` for money). Avoid `TEXT`/`LONGTEXT` unless necessary. Prefer `BOOLEAN` (TINYINT(1)) for flags.
+- **Timestamps**: Use `TIMESTAMP`/`DATETIME` with `DEFAULT CURRENT_TIMESTAMP` and `ON UPDATE CURRENT_TIMESTAMP` where appropriate.
+- **Joins**: Use explicit `JOIN` syntax; avoid implicit comma joins.
+- Keep migrations idempotent and forward-only; include rollback plans where feasible.
+
+## Additional Information
+# MySQL Best Practices Auto Rule
+
+
+## Schema Creation
+
+Design schemas for correctness first, then performance. Apply the following when creating or altering schemas:
+
+- **Version-Specific Charset/Collation**
+
+ - MySQL 8.0+: Prefer `utf8mb4` with `utf8mb4_0900_ai_ci` (or `_as_cs` for case-sensitive needs). Avoid legacy collations like `utf8_general_ci`.
+ - MySQL 5.7: Prefer `utf8mb4` with `utf8mb4_unicode_ci`. Avoid `utf8` (3-byte) and `latin1` unless strictly required for legacy data.
+ - Suggest migration to `utf8`/`latin1` schemas to `utf8mb4` proactively to support full Unicode and emojis.
+ - If a project already has MySQL setup, keep the collation and charset choices of the project
+
+- **Table Creation Defaults**
+
+ - Always specify: `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC` (omit `_0900_` on <8.0).
+ - Define a primary key; use surrogate keys (`BIGINT UNSIGNED AUTO_INCREMENT`) when natural keys are composite/unwieldy.
+ - Define `NOT NULL` where appropriate; avoid nullable columns unless required by domain logic.
+ - Use consistent ID column naming: `id` for primary key, `_id` for foreign keys.
+
+- **Keys and Constraints**
+
+ - Create foreign keys with `ON DELETE`/`ON UPDATE` actions explicitly (`RESTRICT`, `CASCADE`, or `SET NULL`) aligned to business rules.
+ - Create supporting indexes for foreign keys and for frequent predicates; use composite indexes with leftmost prefix ordering that matches queries (e.g., `(user_id, created_at)`).
+ - Name constraints and indexes explicitly: `pk_`, `fk__`, `uk__`, `idx__`.
+
+- **Column Types**
+
+ - Identifiers: `BIGINT UNSIGNED` (or `INT UNSIGNED` if you are certain about bounds). Foreign keys must match referenced type/unsignedness.
+ - Monetary: Avoid `FLOAT/DOUBLE` for currency.
+ - Strings: `VARCHAR(n)` sized to realistic max; avoid `TEXT` unless storing large content.
+ - Booleans: `TINYINT(1)` with `CHECK (col IN (0,1))` on 8.0+.
+
+- **Temporal Columns**
+
+ - Use `created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP`.
+ - Use `updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`.
+ - Store times in UTC; handle localization at the application layer.
+
+### Example: Recommended Table Definition
+
+```sql
+CREATE TABLE `users` (
+ `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `email` VARCHAR(255) NOT NULL,
+ `name` VARCHAR(255) NOT NULL,
+ `status` TINYINT(1) NOT NULL DEFAULT 1,
+ `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ CONSTRAINT `pk_users` PRIMARY KEY (`id`),
+ CONSTRAINT `uk_users_email` UNIQUE KEY (`email`),
+ INDEX `idx_users_status_created_at` (`status`, `created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC;
+```
+
+### Example: Foreign Key With Index and Action
+
+```sql
+CREATE TABLE `orders` (
+ `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` BIGINT UNSIGNED NOT NULL,
+ `total_cents` DECIMAL(19,4) NOT NULL,
+ `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT `pk_orders` PRIMARY KEY (`id`),
+ CONSTRAINT `fk_orders_user_id` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
+ INDEX `idx_orders_user_id_created_at` (`user_id`, `created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+```
+
+## Migration Scripts
+
+Design and execute migrations to be safe, repeatable, and observable:
+
+- Idempotency and Ordering
+
+ - Use timestamped, ordered filenames (e.g., `20250922_1200_add_users_table.sql`).
+ - Write scripts so re-running is safe: `CREATE TABLE IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`, `DROP COLUMN IF EXISTS` on MySQL 8.0+.
+ - Record applied migrations in a migrations table (version, checksum, applied_at, success).
+
+- Transactionality and Locking
+
+ - Wrap related DDL/DML in transactions where supported. Note: some DDL is non-transactional; plan for partial failure.
+ - Avoid long metadata locks; prefer online DDL with `ALGORITHM=INPLACE`/`INSTANT` and `LOCK=NONE` when available (MySQL 8.0+).
+
+- Backwards-Compatible, Zero/Low-Downtime Strategy
+
+ - Avoid destructive changes in one step. Use expand-and-contract:
+ - Add nullable column → backfill in batches → set default → enforce `NOT NULL`.
+ - Add new index concurrently; switch reads/writes; drop old index later.
+ - For large tables, use online schema change tools like gh-ost or pt-online-schema-change.
+ - Batch backfills (e.g., `UPDATE ... LIMIT 1000` with keyset pagination) with sleeps to reduce load.
+
+- Data Safety and Rollback
+
+ - Take backups or snapshots before risky changes; verify restore procedures.
+ - Provide rollback scripts or compensating changes when possible.
+ - Validate with `EXPLAIN` and compare plans before/after.
+
+- Environment Discipline
+ - Test migrations in staging with production-like data volumes.
+ - Gate production runs behind approvals and maintenance windows when needed.
+ - Emit logs/metrics; fail fast on errors; ensure scripts are non-interactive.
+
+### Example: Safe Add NOT NULL Column
+
+```sql
+-- 1) Add column nullable
+ALTER TABLE `users`
+ ADD COLUMN `country_code` VARCHAR(2) NULL;
+
+-- 2) Backfill in batches
+UPDATE `users`
+ SET `country_code` = 'US'
+ WHERE `country_code` IS NULL
+ ORDER BY `id`
+ LIMIT 1000;
+-- Repeat step 2 until no rows remain
+
+-- 3) Set default
+ALTER TABLE `users`
+ ALTER `country_code` SET DEFAULT 'US';
+
+-- 4) Enforce NOT NULL
+ALTER TABLE `users`
+ MODIFY `country_code` VARCHAR(2) NOT NULL DEFAULT 'US';
+```
+
+### References
+
+- [MySQL 8.0 Reference Manual — Character Sets and Collations](https://dev.mysql.com/doc/refman/8.0/en/charset.html)
+- [MySQL 8.0 Reference Manual — InnoDB Storage Engine](https://dev.mysql.com/doc/refman/8.0/en/innodb-storage-engine.html)
+- [MySQL 8.0 Reference Manual — CREATE TABLE Syntax](https://dev.mysql.com/doc/refman/8.0/en/create-table.html)
+- [MySQL 8.0 Reference Manual — Data Types](https://dev.mysql.com/doc/refman/8.0/en/data-types.html)
+- [MySQL 8.0 Reference Manual — SQL Modes](https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html)
+- [MySQL 8.0 Reference Manual — EXPLAIN Output](https://dev.mysql.com/doc/refman/8.0/en/explain-output.html)
+- [MySQL 8.0 Reference Manual — Online DDL](https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html)
+- [OWASP — SQL Injection Prevention Cheat Sheet](https://owasp.org/www-project-cheat-sheets/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
\ No newline at end of file
diff --git a/.claude/rules/standards/nextjs-react19-auto.md b/.claude/rules/standards/nextjs-react19-auto.md
new file mode 100644
index 0000000..da2103b
--- /dev/null
+++ b/.claude/rules/standards/nextjs-react19-auto.md
@@ -0,0 +1,61 @@
+# This rule enforces Next
+
+## Description
+This rule enforces Next.js best practices for React 19 with TypeScript.
+
+## Applicability
+- **Files:** `*.tsx, *.ts, *.jsx, *.js`
+- **Always Apply:** true
+
+## Additional Information
+# Critical Rules
+
+- Adapt approach to app router or page router project structure
+- Implement proper error boundaries
+
+- Use the App Router structure with `page.tsx` files in route directories.
+- Client components must be explicitly marked with `'use client'` at the top of the file.
+- Use kebab-case for directory names (e.g., `components/auth-form`) and PascalCase for component files.
+
+
+## Project Structure
+
+ - Both the /app and /components folders under a /src directory.
+
+## State Management
+
+- Use `getServerSideProps` for server-side data fetching
+- Use `getStaticProps` for static data fetching
+- Use `getStaticPaths` for static path generation
+- Use `useActionState` instead of deprecated `useFormState`
+- Leverage enhanced `useFormStatus` with new properties (data, method, action)
+- Avoid unnecessary `useState`,`setState`, `useEffect`, and `useCallback` when possible:
+ - Use server actions for server-side state management and form handling,
+ - Use server components for data fetching,
+ - Use URL search params for shareable state
+
+## Async Request APIs
+
+- Always use async versions of runtime APIs
+- Handle async params in layouts/pages
+
+## Data Fetching
+
+- Use appropriate fetching methods (Server Components, SWR, React Query, etc.)
+- Use API Routes inside route directories for server-side data fetching (ie. `app/api/users/route.ts` for `/api/users`)
+- Use Suspense for async operations
+
+Remind user of default caching behavior in Next.js when using API routes or fetching data from external APIs.
+
+## Routing
+
+- Use the App Router conventions
+- Implement proper loading and error states for routes
+- Use dynamic routes appropriately (ie. when a user can be identified by a unique id, or a blog post has a unique slug)
+- Handle parallel routes when needed
+
+## Components
+
+Remind the developer:
+- Importing a server component into a 'use client' file makes it a client component.
+- Passing a server component as a child to a client component keeps it as a server component, retaining SSR benefits.
\ No newline at end of file
diff --git a/.claude/rules/standards/react-typescript-auto.md b/.claude/rules/standards/react-typescript-auto.md
new file mode 100644
index 0000000..f4dbbd0
--- /dev/null
+++ b/.claude/rules/standards/react-typescript-auto.md
@@ -0,0 +1,77 @@
+# This rule enforces best practices for building React 18+ applications with TypeScript, ensuring type safety, maintainability, and consistency
+
+## Description
+This rule enforces best practices for building React 18+ applications with TypeScript, ensuring type safety, maintainability, and consistency.
+
+## Applicability
+- **Files:** `"*.ts,*.tsx"`
+- **Always Apply:** false
+
+## Rules
+- All React components MUST be defined as functional components using `const` and arrow functions – except for Error Boundaries.
+- ALWAYS define explicit types or interfaces for component props and state.
+- EVERY element in a list rendered by `map()` MUST have a unique `key` prop.
+- Hooks (`useState`, `useEffect`, `useContext`, etc.) MUST only be called at the top level of a functional component or custom hook.
+- Component filenames MUST use PascalCase (e.g., `MyComponent.tsx`).
+- Prefix custom hooks with "use" (ie. "useUserAuth")
+- Prefix event handlers with "handle" (ie. "handleClick")
+- Prefix boolean values with verbs (ie. isLoading for loading state, or canSubmit for ability to submit)
+
+## Additional Information
+# React and TypeScript Standards
+
+
+## React Hooks
+
+- Only call hooks at the top level of React function components or custom hooks (never inside loops, conditions, or nested functions).
+- Always start custom hook names with `use` (e.g., `useFetchData`).
+- Extract reusable logic into custom hooks to avoid duplication.
+- Specify all dependencies in hook dependency arrays (e.g., `useEffect`, `useCallback`, `useMemo`).
+- Avoid using `any` in hook return types or parameters; always type your hooks.
+- Use `useCallback` and `useMemo` to optimize performance only when necessary.
+- Document the purpose and expected usage of custom hooks.
+
+### Best Practices for useEffect
+
+The `useEffect` hook is used for side effects in functional components, such as data fetching, subscriptions, or manually changing the DOM. To avoid bugs and performance issues, follow these best practices:
+
+- Always specify all external dependencies in the dependency array. Missing dependencies can cause stale values or unexpected behavior.
+- Avoid using `any` in effect callbacks or dependencies.
+- Use cleanup functions to prevent memory leaks, especially for subscriptions or timers.
+- Prefer splitting effects by purpose rather than combining unrelated logic in a single effect.
+- Avoid side effects that synchronously update state in a way that triggers another render loop.
+- For async logic, define the async function inside the effect and call it, rather than making the effect callback itself async.
+
+## State Management Guidance
+
+Effective state management is crucial for building scalable and maintainable React applications. Choose the right state management approach based on the scope and complexity of your state:
+
+- Use `useState` for simple, local component state.
+- Use `useReducer` for complex local state logic or when state transitions depend on previous state.
+- Use React Context for sharing state that is truly global to a subtree (e.g., theme, authentication, user preferences), but avoid using it for frequently changing or large state.
+- For large, shared, or highly dynamic state, consider external libraries such as Redux, Zustand, Jotai, or Recoil.
+- Always define explicit types for state and actions when using TypeScript.
+- Avoid prop drilling by lifting state up only as much as necessary or using context appropriately.
+
+## When to use React Context
+
+React Context is ideal for passing data that can be considered "global" for a tree of React components, such as the current authenticated user, theme, or preferred language. It helps avoid "prop drilling" (passing props down through many nested components).
+
+### Best Practices for Context:
+
+- Define an explicit type for the context value.
+- Create a provider component that manages the context's state and value.
+- Use `useContext` hook to consume the context in functional components.
+- AVOID using context for highly dynamic or frequently updated state that causes many re-renders across the component tree. For such cases, consider state management libraries like Redux or Zustand.
+
+## Suspense:
+
+- Use `` to wrap components that use React.lazy for code-splitting or that rely on data fetching libraries supporting Suspense (e.g., Relay, React Query experimental).
+- Provide a meaningful fallback UI (e.g., spinner, skeleton loader) to indicate loading state.
+- Avoid wrapping your entire app in a single Suspense; scope it to the smallest subtree that benefits from loading boundaries.
+
+## Error Boundaries:
+
+- Use Error Boundaries to catch and display errors in the render phase of React components, preventing the entire app from crashing.
+- Place Error Boundaries around critical UI sections (e.g., main content, widgets) to isolate failures.
+- Error Boundaries must be class components, but you can wrap them in functional components for convenience.
\ No newline at end of file
diff --git a/.claude/rules/standards/typescript-standards-auto.md b/.claude/rules/standards/typescript-standards-auto.md
new file mode 100644
index 0000000..307ae6a
--- /dev/null
+++ b/.claude/rules/standards/typescript-standards-auto.md
@@ -0,0 +1,107 @@
+# TypeScript development standards and type safety
+
+## Description
+TypeScript development standards and type safety
+
+## Applicability
+- **Files:** `**/*.{ts|tsx}`
+- **Always Apply:** false
+
+## Rules
+- Refer to https://www.typescriptlang.org/tsconfig when tsconfig.json values are changed.
+- Use strict mode with `"strict": true` in tsconfig.json
+- Remind user the limitations of using esModuleInterop and allowSyntheticDefaultImports in tsconfig.json https://www.typescriptlang.org/tsconfig/#Interop_Constraints_6252
+- Explicitly declare types for function parameters and return values
+- Avoid using `any` type unless absolutely necessary. Use `unknown` instead.
+- Use interfaces for object type definitions
+- Use enums for fixed sets of values
+- Use type aliases to simplify complex types
+- Document APIs with JSDoc comments
+- Maintain proper error handling with typed errors
+- Use type guards and type narrowing to improve type safety
+- Use type literals and unions to define the exact allowed values.
+- Use type assertions and annotations when necessary. They are unnecessary if type inference is possible.
+- Use type unions and intersections to combine types.
+- Export types from modules using the `export type` syntax
+- If using an existing library with types, import the types using the `import type` syntax
+- Declare types for file extensions that aren't supported by the compiler using the `declare module` syntax
+- If using node 23+, use `node --experimental-strip-types` in package.json scripts to strip types when running the code locally
+- Use utility types as much as possible to avoid type duplication.
+
+### typescript-standards
+
+Standards for TypeScript development and type safety
+
+**Actions:**
+ - type: validate
+ message: |
+ TypeScript code must follow these conventions:
+ 1. Use strict mode
+ 2. Explicit typing
+ 3. Proper error handling
+ 4. Documentation
+ 5. Consistent naming
+ 6. Proper use of primitives and utility types to reduce duplication and increase type safety
+
+ - type: lint
+ rules:
+ - pattern: "any"
+ message: "Avoid using 'any' type. Define a specific type or interface instead."
+ - pattern: "Object"
+ message: "Avoid using 'Object' type. Use a specific interface or type instead."
+ - pattern: "Function"
+ message: "Avoid using 'Function' type. Define specific function signature instead."
+ - pattern: "\\b[A-Z][a-z0-9]+([A-Z][a-z0-9]+)*\\b"
+ message: "Use PascalCase for type names, interfaces, and classes."
+ - pattern: "\\b[a-z][a-z0-9]*([A-Z][a-z0-9]+)*\\b"
+ message: "Use camelCase for variable and function names."
+
+metadata:
+
+## Additional Information
+# TypeScript Standards
+
+
+
+
+## Naming Conventions
+
+### Types and Interfaces
+- Use PascalCase for type names and interface names
+- Prefix interfaces with 'I' only when required by project convention
+- Follow consistent naming conventions
+
+```typescript
+interface UserData {
+ id: string;
+ name: string;
+}
+
+type ApiResponse = {
+ data: T;
+ status: number;
+};
+```
+
+### Variables and Functions
+- Use camelCase for variable names and function names
+- Use PascalCase for class names
+- Function signatures should have return types
+
+### Classes
+
+- Use PascalCase for class names
+
+```typescript
+const userData: UserData;
+function fetchUserData(): Promise;
+class UserService {};
+```
+
+### Constants
+- Use UPPER_SNAKE_CASE for constant values
+
+```typescript
+const MAX_RETRY_ATTEMPTS = 3;
+const API_BASE_URL = 'https://api.example.com';
+```
\ No newline at end of file
diff --git a/.claude/rules/standards/vue3-typescript-auto.md b/.claude/rules/standards/vue3-typescript-auto.md
new file mode 100644
index 0000000..af436c0
--- /dev/null
+++ b/.claude/rules/standards/vue3-typescript-auto.md
@@ -0,0 +1,332 @@
+# Vue 3 with TypeScript Standards
+
+## Description
+Vue 3 with TypeScript Standards
+
+## Applicability
+- **Files:** `*.vue,*.ts`
+- **Always Apply:** false
+
+## Rules
+- Use Composition API with `
+
+ ❌ Avoid Options API and untyped refs:
+ const props = defineProps(['title'])
+ const count = ref(0)
+examples:
+ - input: |
+ const props = defineProps(['title'])
+ const model = defineModel()
+ const count = ref(0)
+ output: |
+ interface Props {
+ title: string
+ }
+ const props = defineProps()
+ const model = defineModel()
+ const count = ref(0)
+metadata:
+
+### vue3-models
+
+Proper typing for defineModel() and v-model bindings
+
+**Actions:**
+ - type: suggest
+ message: |
+ ✅ Type v-model bindings properly:
+
+ // Single v-model
+ const modelValue = defineModel()
+
+ // Named v-models
+ const isOpen = defineModel('isOpen')
+ const selectedId = defineModel('selectedId')
+
+ // Optional v-model with default
+ const theme = defineModel<'light' | 'dark'>('theme', { default: 'light' })
+
+ ❌ Avoid untyped models:
+ const modelValue = defineModel()
+ const isOpen = defineModel('isOpen')
+examples:
+ - input: |
+ const modelValue = defineModel()
+ const isVisible = defineModel('isVisible')
+ output: |
+ const modelValue = defineModel()
+ const isVisible = defineModel('isVisible')
+metadata:
+
+### vue3-slots
+
+Proper typing for defineSlots() and slot definitions
+
+**Actions:**
+ - type: suggest
+ message: |
+ ✅ Type slots with proper interfaces:
+
+ // Basic slots
+ interface Slots {
+ default(): any
+ header(): any
+ footer(): any
+ }
+
+ // Slots with typed props
+ interface Slots {
+ default(props: { user: User; isActive: boolean }): any
+ item(props: { item: Product; index: number }): any
+ empty(): any
+ }
+
+ const slots = defineSlots()
+
+ ❌ Avoid untyped slots:
+ const slots = defineSlots()
+examples:
+ - input: |
+ const slots = defineSlots()
+ output: |
+ interface Slots {
+ default(props: { item: any }): any
+ }
+ const slots = defineSlots()
+metadata:
+
+### vue3-composables-naming
+
+Standards for composables and component naming with TypeScript
+
+**Actions:**
+ - type: suggest
+ message: |
+ ✅ Composable structure:
+ export interface UseCounterReturn {
+ count: Ref
+ increment: () => void
+ }
+
+ export function useCounter(initial = 0): UseCounterReturn {
+ const count = ref(initial)
+ const increment = (): void => { count.value++ }
+ return { count, increment }
+ }
+
+ ✅ Component naming: UserProfile.vue, TheHeader.vue
+ ❌ Avoid: userProfile.vue, user_profile.vue
+examples:
+ - input: |
+ export function useCounter() {
+ const count = ref(0)
+ return { count }
+ }
+ output: |
+ export interface UseCounterReturn {
+ count: Ref
+ }
+ export function useCounter(): UseCounterReturn {
+ const count = ref(0)
+ return { count }
+ }
+metadata:
+
+### vue3-watchers
+
+Best practices for watch and watchEffect with TypeScript
+
+**Actions:**
+ - type: suggest
+ message: |
+ ✅ Use watchers properly with TypeScript:
+
+ // watch() for specific reactive sources
+ watch(
+ () => user.value?.id,
+ (newId: number | undefined, oldId: number | undefined) => {
+ if (newId) {
+ fetchUserData(newId)
+ }
+ },
+ { immediate: true }
+ )
+
+ // watch multiple sources
+ watch(
+ [() => props.userId, searchQuery],
+ async ([userId, query]: [number | undefined, string]) => {
+ if (userId && query) {
+ await searchUserData(userId, query)
+ }
+ }
+ )
+
+ // watchEffect for side effects
+ watchEffect(() => {
+ if (user.value && isLoggedIn.value) {
+ analytics.track('user_active', { userId: user.value.id })
+ }
+ })
+
+ // Cleanup watchers
+ const stopWatcher = watch(data, callback)
+ onUnmounted(() => stopWatcher())
+
+ ❌ Avoid untyped watchers and missing cleanup:
+ watch(user, (newVal, oldVal) => { ... })
+ watchEffect(() => { ... }) // without cleanup consideration
+examples:
+ - input: |
+ watch(user, (newVal, oldVal) => {
+ console.log('User changed')
+ })
+ output: |
+ watch(
+ user,
+ (newUser: User | null, oldUser: User | null) => {
+ if (newUser) {
+ console.log('User changed:', newUser.name)
+ }
+ }
+ )
+metadata:
+
+### vue3-advanced-patterns
+
+Type-safe provide/inject and performance optimization
+
+**Actions:**
+ - type: suggest
+ message: |
+ ✅ Type-safe dependency injection:
+ // types/keys.ts
+ export const UserKey: InjectionKey[> = Symbol('user')
+
+ // Usage
+ provide(UserKey, currentUser)
+ const user = inject(UserKey)
+
+ ✅ Performance patterns:
+ const largeList = shallowRef]- ([])
+ const filtered = computed(() => items.value.filter(item => item.active))
+ const AsyncComp = defineAsyncComponent(() => import('./Heavy.vue'))
+examples:
+ - input: |
+ provide('user', currentUser)
+ const user = inject('user')
+ output: |
+ export const UserKey: InjectionKey
[> = Symbol('user')
+ provide(UserKey, currentUser)
+ const user = inject(UserKey)
+metadata:
+
+## Additional Information
+# Vue 3 with TypeScript Standards
+
+Modern Vue 3 development with TypeScript, emphasizing type safety, Composition API, and performance.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## File Structure
+
+```
+src/
+├── components/ # Reusable components
+├── composables/ # Vue composables
+├── layouts/ # Vue layouts
+├── stores/ # Vue stores
+├── utils/ # Vue utils
+├── types/ # TypeScript definitions
+└── pages/ # Page components
+
+```
+
+### Import Order
+
+```typescript
+// 1. Vue core
+import { ref, computed } from "vue";
+// 2. Vue ecosystem
+import { useRouter } from "vue-router";
+// 3. Third-party
+import axios from "axios";
+// 4. Local
+import UserCard from "@/components/UserCard.vue";
+```
+
+tests:
+
+- input: |
+
+ output: |
+
+- input: "const modelValue = defineModel()"
+ output: "const modelValue = defineModel()"
+- input: "provide('theme', 'dark')"
+ output: |
+ export const ThemeKey: InjectionKey = Symbol('theme')
+ provide(ThemeKey, 'dark')
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 95f0432..9f03111 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,4 +57,8 @@ Thumbs.db
.verdaccio-db.json
.env
.envrc
-.cursor/.ai/**
\ No newline at end of file
+.cursor/.ai/**
+
+# Temporary PR body files
+.pr-body*
+CLI_SKILLS_PLAN.md
\ No newline at end of file
diff --git a/.pr-body-phase4.md b/.pr-body-phase4.md
new file mode 100644
index 0000000..a716da4
--- /dev/null
+++ b/.pr-body-phase4.md
@@ -0,0 +1,34 @@
+## Summary
+Phase 4 of Claude Code support - Supply chain security and safety validation
+
+## Changes
+
+### GitHub Actions Pinning (Supply Chain Security)
+- Pinned all GitHub Actions to specific commit SHAs in 4 workflow files:
+ - actions/checkout: v4.2.2 (11bd71901bbe5b1630ceea73d27597364c9af683)
+ - actions/setup-node: v4.1.0 (1a4442cacd436585991a76fe714fa58850bd193c)
+ - actions/configure-pages: v4.0.0 (1f0c5cde4dec8825aff22eac11aa73c856b5c886)
+ - actions/upload-pages-artifact: v3.0.1 (56afc609e74202658d3ffba0e8f6f4625a7d4af5)
+ - actions/deploy-pages: v4.0.5 (d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e)
+ - actions/dependency-review-action: v4.5.0 (3b139cfc5fae8b618d3eae3675e383bb1769c019)
+ - dorny/paths-filter: v3.0.2 (de90cc6fb38fc0963ad72b210f1f284cd68cea36)
+- Renovate will still update these via SHA (helpers:pinGitHubActionDigests)
+
+### Safety & Validation Utilities
+- Created scripts/generate-checksums.mjs to generate SHA-256 hashes
+- Generated checksums.json with 84 file hashes for .cursor/ and .claude/
+- Created cli/utils/validation.mjs with:
+ - validateFile() - Check file against expected checksum
+ - validateDownload() - Validate entire directory
+ - scanForDangerousPatterns() - Detect malicious patterns
+ - validateJson() - Validate JSON syntax
+
+## Security
+- Prevents supply chain attacks from compromised action tags
+- File integrity verification via checksums
+- Pattern scanning for dangerous commands
+
+## Depends On
+Phase 1, 2, and 3 PRs should be merged first
+
+Refs: Implementation plan for dual-IDE support
diff --git a/CLI_SKILLS_PLAN.md b/CLI_SKILLS_PLAN.md
new file mode 100644
index 0000000..d1240da
--- /dev/null
+++ b/CLI_SKILLS_PLAN.md
@@ -0,0 +1,166 @@
+# CLI Tool Plan: Skills Recognition and Download
+
+## Problem
+
+The CLI tool needs to recognize and download `.claude/skills/` alongside rules and commands.
+
+## Current State
+
+- CLI downloads: `.cursor/rules/`, `.claude/rules/`, `.claude/commands/`
+- Missing: `.claude/skills/` directory
+- Interactive mode only handles rules, not skills
+
+## Proposed Solution
+
+### 1. Update Download Logic
+
+Add `.claude/skills/` to IDE-specific download paths:
+
+```javascript
+function getSourcePaths(ide) {
+ const paths = [];
+
+ if (ide === 'claude' || ide === 'both') {
+ paths.push(
+ { source: '.claude/rules', dest: '.claude/rules', type: 'rules' },
+ { source: '.claude/commands', dest: '.claude/commands', type: 'commands' },
+ { source: '.claude/skills', dest: '.claude/skills', type: 'skills' } // NEW
+ );
+ }
+
+ return paths;
+}
+```
+
+### 2. Interactive Skills Selection
+
+Add skills to interactive menu:
+
+```javascript
+// scanAvailableSkills() - similar to scanAvailableRules()
+async function scanAvailableSkills(basePath) {
+ const skills = [];
+ const entries = await readdir(basePath, { withFileTypes: true });
+
+ for (const entry of entries) {
+ if (entry.isDirectory() && entry.name !== 'CLAUDE.md') {
+ const skillPath = join(basePath, entry.name, 'SKILL.md');
+ if (await fileExists(skillPath)) {
+ skills.push({
+ name: entry.name,
+ path: skillPath,
+ displayName: formatSkillName(entry.name)
+ });
+ }
+ }
+ }
+
+ return skills;
+}
+```
+
+### 3. Skills in Interactive Mode
+
+Update interactive menu flow:
+
+```
+[Interactive Mode]
+ ↓
+Select IDE (cursor/claude/both)
+ ↓
+Select Content Type:
+ - Rules
+ - Skills (NEW) ←
+ - Commands
+ - All
+ ↓
+Select specific items
+ ↓
+Download
+```
+
+### 4. CLI Flags for Skills
+
+Add skills-specific flags:
+
+```bash
+npx @usrrname/cursorrules --skills-only # Download only skills
+npx @usrrname/cursorrules --include-skills # Include in batch download
+npx @usrrname/cursorrules --list-skills # List available skills
+```
+
+### 5. Skills Metadata
+
+Parse SKILL.md frontmatter for metadata:
+
+```javascript
+function parseSkillMetadata(skillPath) {
+ const content = readFileSync(skillPath, 'utf-8');
+ const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
+
+ if (frontmatter) {
+ return yaml.parse(frontmatter[1]);
+ }
+
+ return { name: basename(skillPath), description: '' };
+}
+```
+
+### 6. Implementation Tasks
+
+#### Phase A: Basic Skills Download
+- [ ] Update `download-files.mjs` to include skills path
+- [ ] Add skills to `--ide both` download
+- [ ] Test skills download with `--dry-run`
+
+#### Phase B: Skills Discovery
+- [ ] Create `scanAvailableSkills()` function
+- [ ] Add skills metadata parsing
+- [ ] Create skills listing command
+
+#### Phase C: Interactive Skills Selection
+- [ ] Add skills category to interactive menu
+- [ ] Create skill selection UI
+- [ ] Handle skills-specific download
+
+#### Phase D: Advanced Features
+- [ ] Add `--skills-only` flag
+- [ ] Add skill dependency resolution
+- [ ] Filter skills by trigger patterns
+
+### 7. Directory Structure After Download
+
+```
+project/
+├── .claude/
+│ ├── settings.json
+│ ├── rules/
+│ ├── commands/
+│ └── skills/ # Downloaded skills
+│ ├── typescript/
+│ │ └── SKILL.md
+│ ├── react/
+│ │ └── SKILL.md
+│ └── CLAUDE.md
+```
+
+### 8. Backward Compatibility
+
+- Existing `--flat` flag continues to work
+- Default behavior unchanged (cursor rules only)
+- Skills only downloaded with `--ide claude` or `--ide both`
+
+## Open Questions
+
+1. Should skills be selectable individually or only as groups?
+2. Should we add skill dependencies (e.g., react skill depends on typescript skill)?
+3. Should skills trigger rules download automatically?
+
+## Timeline
+
+- Phase A: 1-2 days
+- Phase B: 2-3 days
+- Phase C: 3-4 days
+- Phase D: 2-3 days
+
+Total: ~1-2 weeks for full skills support
diff --git a/cli/commands.mjs b/cli/commands.mjs
index fc81f90..3a6592c 100644
--- a/cli/commands.mjs
+++ b/cli/commands.mjs
@@ -8,6 +8,7 @@ import { downloadFiles, downloadSelectedFiles } from './utils/download-files.mjs
import { findPackageRoot } from './utils/find-package-root.mjs';
import { interactiveCategorySelection, prepareMenu, scanAvailableRules, selectRules } from './utils/interactive-menu.mjs';
import { validateDirname } from './utils/validate-dirname.mjs';
+import { selectIde } from './utils/ide-selection.mjs';
/** fallback for Node < 21 */
const styleText = util.styleText ?? ((_, text) => text);
@@ -32,23 +33,27 @@ export const help = () => {
/** @param {string} key */
const getFlagDescription = (key) => {
switch (key) {
+ case 'dryRun':
+ return styleText('green', 'preview what would be downloaded');
case 'flat':
return styleText('green', 'install all rules without parent directory');
case 'help':
return styleText('green', 'help instructions');
+ case 'ide':
+ return styleText('green', 'target IDE: cursor|claude|both');
case 'interactive':
return styleText('green', 'select the rules you want');
case 'output':
return styleText('green', 'set output directory (Default: .cursor/)');
+ case 'validate':
+ return styleText('green', 'validate downloaded files');
case 'version':
return styleText('green', 'show package version');
- case 'interactive':
- return styleText('green', 'select the rules you want');
}
}
const tableContent = Object.entries(config?.options || {}).map(([key, value]) => {
return {
- flag: `-${value?.short}`,
+ flag: value?.short ? `-${value?.short}` : '',
name: `--${key}`,
description: getFlagDescription(key),
type: value?.type,
@@ -70,6 +75,11 @@ ${usage} ${options}
${tableContent.map(item => `${item.name} ${item.flag} ${item.type} ${item.description} ${item.default}`).join('\n')}
+Examples:
+ npx @usrrname/cursorrules --ide cursor --flat
+ npx @usrrname/cursorrules --ide claude --dry-run
+ npx @usrrname/cursorrules --ide both --output ./config
+
${repoLink}
`);
}
@@ -83,6 +93,12 @@ export const version = () => console.log(`${packageJson?.name} v${packageJson?.v
*/
export const interactiveMode = async (values) => {
console.log('🎯 Starting interactive mode...');
+
+ // Prompt for IDE selection first
+ if (!values.ide) {
+ values.ide = await selectIde();
+ }
+
const packageRoot = findPackageRoot(__dirname, '@usrrname/cursorrules');
const sourceRulesBasePath = resolve(packageRoot, '.cursor', 'rules');
const rules = await scanAvailableRules(sourceRulesBasePath);
@@ -106,7 +122,7 @@ export const interactiveMode = async (values) => {
.filter(rule => rule.selected);
const outputDir = values?.output?.toString() ?? defaultOutput;
if (allSelectedRules.length > 0)
- return await downloadSelectedFiles(outputDir, allSelectedRules);
+ return await downloadSelectedFiles(outputDir, allSelectedRules, values);
else
console.log('⚠️ No rules selected');
break;
@@ -120,15 +136,25 @@ export const interactiveMode = async (values) => {
/**
* @param {string} outputDir - output directory
+ * @param {Object} [values] - CLI options
+ * @param {string} [values.ide] - Target IDE
+ * @param {boolean} [values.dryRun] - Dry run flag
+ * @param {boolean} [values.validate] - Validate flag
* @returns {Promise}
*/
-export const output = async (outputDir) => {
+export const output = async (outputDir, values = {}) => {
if (!outputDir.trim()) {
console.error('❌ ERROR: Output directory cannot be empty.');
process.exit(1);
}
+
+ // Prompt for IDE if not specified
+ if (!values.ide) {
+ values.ide = await selectIde();
+ }
+
const outputValue = await validateDirname(outputDir);
- await downloadFiles(outputValue);
+ await downloadFiles(outputValue, values);
}
diff --git a/cli/index.mjs b/cli/index.mjs
index 0f9caf6..163c95b 100755
--- a/cli/index.mjs
+++ b/cli/index.mjs
@@ -15,6 +15,10 @@ export const config = {
args: process.argv.slice(2),
tokens: true,
options: {
+ dryRun: {
+ type: 'boolean',
+ default: false,
+ },
flat: {
type: 'boolean',
short: 'f',
@@ -24,6 +28,9 @@ export const config = {
short: 'h',
default: false,
},
+ ide: {
+ type: 'string',
+ },
interactive: {
type: 'boolean',
short: 'i',
@@ -33,6 +40,10 @@ export const config = {
type: 'string',
short: 'o',
},
+ validate: {
+ type: 'boolean',
+ default: false,
+ },
version: {
type: 'boolean',
short: 'v',
@@ -47,34 +58,35 @@ async function main() {
const { values } = parseArgs(config);
const flags = Object.keys(config.options || {});
- const allowedKeys = flags.filter(flag => flag === 'output')[0]
+ const allowedKeys = flags.filter(flag => flag === 'output' || flag === 'ide')[0];
for (let key in values) {
-
- /**
- * prevent unknown flags from being used
- * prevent arguments without values
- * @param {string} key */
if (!allowedKeys.includes(key) && !values[key]) continue;
switch (key) {
- case 'version':
+ case 'version': {
await version();
break;
- case 'help':
+ }
+ case 'help': {
await help();
break;
- case 'interactive':
+ }
+ case 'interactive': {
await interactiveMode(values);
process.exit(0);
- case 'output':
+ break;
+ }
+ case 'output': {
const outputDir = values[key]?.toString() ?? process.cwd();
- await output(outputDir);
+ await output(outputDir, values);
break;
- case 'flat':
+ }
+ case 'flat': {
const cursorRulesPath = process.cwd();
- await downloadFiles(cursorRulesPath);
+ await downloadFiles(cursorRulesPath, values);
break;
+ }
}
}
}
diff --git a/cli/utils/download-files.mjs b/cli/utils/download-files.mjs
index c4bd385..b225064 100644
--- a/cli/utils/download-files.mjs
+++ b/cli/utils/download-files.mjs
@@ -1,4 +1,5 @@
-import { copyFile, cp, mkdir } from 'node:fs/promises';
+import { copyFile, cp, mkdir, readFile, access } from 'node:fs/promises';
+import { createHash } from 'node:crypto';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { help } from '../commands.mjs';
@@ -6,6 +7,7 @@ import { detectNpxSandbox } from './detect-npx.mjs';
import { findFolderUp } from './find-folder-up.mjs';
import { findPackageRoot } from './find-package-root.mjs';
import { validateDirname } from './validate-dirname.mjs';
+import { validateIde, getIdeDisplayName, selectIde } from './ide-selection.mjs';
const detection = detectNpxSandbox();
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -18,7 +20,6 @@ if (detection.isNpxSandbox || !sourceRulesBasePath) {
sourceRulesBasePath = resolve(packageRoot, '.cursor', 'rules');
}
-
// only for local development and testing
if (process.env.CI || ['development', 'test'].includes(process.env.NODE_ENV ?? '')) {
// running inside repo / globally installed copy → locate nearest .cursor
@@ -30,27 +31,126 @@ if (process.env.CI || ['development', 'test'].includes(process.env.NODE_ENV ?? '
sourceRulesBasePath = resolve(found, 'rules');
}
+/**
+ * Generate SHA-256 checksum of file content
+ * @param {string} content
+ * @returns {string}
+ */
+function generateChecksum(content) {
+ return createHash('sha256').update(content).digest('hex');
+}
+
+/**
+ * Validate a downloaded file
+ * @param {string} filePath
+ * @param {string} expectedContent
+ * @returns {Promise<{valid: boolean, error?: string}>}
+ */
+async function validateFile(filePath, expectedContent) {
+ try {
+ await access(filePath);
+ const content = await readFile(filePath, 'utf-8');
+ if (content !== expectedContent) {
+ return { valid: false, error: 'Content mismatch' };
+ }
+ return { valid: true };
+ } catch (err) {
+ return { valid: false, error: err.message };
+ }
+}
+
+/**
+ * Get source paths based on IDE selection
+ * @param {string} ide - 'cursor', 'claude', or 'both'
+ * @returns {Array<{source: string, dest: string, type: string}>}
+ */
+function getSourcePaths(ide) {
+ const packageRoot = findPackageRoot(__dirname, '@usrrname/cursorrules');
+ const paths = [];
+
+ if (ide === 'cursor' || ide === 'both') {
+ paths.push({
+ source: resolve(packageRoot, '.cursor'),
+ dest: '.cursor',
+ type: 'cursor'
+ });
+ }
+
+ if (ide === 'claude' || ide === 'both') {
+ paths.push({
+ source: resolve(packageRoot, '.claude'),
+ dest: '.claude',
+ type: 'claude'
+ });
+ }
+
+ return paths;
+}
+
/**
* @param {string} dirname - output folder relative path
+ * @param {Object} [options] - CLI options
+ * @param {string} [options.ide] - Target IDE: 'cursor', 'claude', or 'both'
+ * @param {boolean} [options.dryRun] - Preview only, don't download
+ * @param {boolean} [options.validate] - Validate downloaded files
*/
-export const downloadFiles = async (dirname) => {
+export const downloadFiles = async (dirname, options = {}) => {
if (!dirname) throw new Error('Output directory is required');
-
- console.info('📥 Downloading all rules...');
+
+ // Prompt for IDE if not specified
+ let ide = validateIde(options.ide ?? '');
+ if (!ide) {
+ ide = await selectIde();
+ }
+ // Ensure ide is a valid string
+ if (!ide) {
+ ide = 'cursor';
+ }
+
+ const dryRun = options.dryRun || false;
+ const validate = options.validate || false;
+
+ if (dryRun) {
+ console.log(`🔍 DRY RUN: Previewing what would be downloaded for ${getIdeDisplayName(ide)}...\n`);
+ } else {
+ console.info(`📥 Downloading ${getIdeDisplayName(ide)} configuration...`);
+ }
const outputDir = await validateDirname(dirname);
- if (!sourceRulesBasePath) {
- console.error(`❌ Error: sourceRulesBasePath is not defined`);
- process.exit(1);
+ const sourcePaths = getSourcePaths(ide);
+
+ if (dryRun) {
+ console.log(`Output directory: ${outputDir}`);
+ console.log('\nFiles to be downloaded:\n');
+ for (const { source, dest, type } of sourcePaths) {
+ console.log(` 📁 ${type}/ → ${dest}/`);
+ }
+ console.log('\n✅ Dry run complete. No files were downloaded.');
+ return;
}
+
try {
- // copy whole folder
- await cp(
- sourceRulesBasePath,
- outputDir,
- { recursive: true },
- )
- console.log(`✅ Success! All rules saved to ${outputDir}`);
+ for (const { source, dest, type } of sourcePaths) {
+ const destPath = join(outputDir, dest);
+ console.log(` 📥 Copying ${type} configuration...`);
+
+ await cp(source, destPath, { recursive: true });
+
+ if (validate) {
+ console.log(` 🔍 Validating ${type} configuration...`);
+ // Basic validation - ensure key files exist
+ if (type === 'claude') {
+ await access(join(destPath, 'settings.json'));
+ }
+ }
+ }
+
+ console.log(`✅ Success! ${getIdeDisplayName(ide)} configuration saved to ${outputDir}`);
+
+ if (ide === 'both') {
+ console.log(' - Cursor: .cursor/');
+ console.log(' - Claude Code: .claude/');
+ }
} catch (err) {
console.error(`❌ Error: ${err.message}`, err);
process.exit(1);
@@ -61,8 +161,10 @@ export const downloadFiles = async (dirname) => {
* Download selected rules only
* @param {string} folderName - output folder relative path
* @param {Array<{category: string, displayName: string, selected: boolean, name: string, path: string, fullPath: string}>} selectedRules - Array of selected rule objects
+ * @param {Object} [options] - CLI options
+ * @param {string} [options.ide] - Target IDE: 'cursor', 'claude', or 'both'
*/
-export const downloadSelectedFiles = async (folderName, selectedRules) => {
+export const downloadSelectedFiles = async (folderName, selectedRules, options = {}) => {
if (!folderName) throw new Error('Output directory is required');
if (!selectedRules || selectedRules.length === 0) {
@@ -71,14 +173,31 @@ export const downloadSelectedFiles = async (folderName, selectedRules) => {
return;
}
- console.info('📥 Downloading selected rules...');
+ // Prompt for IDE if not specified
+ let ide = validateIde(options.ide ?? '');
+ if (!ide) {
+ ide = await selectIde();
+ }
+ // Ensure ide is a valid string
+ if (!ide) {
+ ide = 'cursor';
+ }
+
+ console.info(`📥 Downloading selected rules for ${getIdeDisplayName(ide)}...`);
const outputDir = await validateDirname(folderName)
try {
// Create output directory structure
await mkdir(outputDir, { recursive: true });
- await mkdir(join(outputDir, '.cursor'), { recursive: true });
+
+ if (ide === 'cursor' || ide === 'both') {
+ await mkdir(join(outputDir, '.cursor'), { recursive: true });
+ }
+
+ if (ide === 'claude' || ide === 'both') {
+ await mkdir(join(outputDir, '.claude'), { recursive: true });
+ }
// Copy selected rules
for (const rule of selectedRules) {
diff --git a/cli/utils/ide-selection.mjs b/cli/utils/ide-selection.mjs
new file mode 100644
index 0000000..6b71277
--- /dev/null
+++ b/cli/utils/ide-selection.mjs
@@ -0,0 +1,69 @@
+#!/usr/bin/env node
+import * as readline from 'node:readline';
+import { stdin, stdout } from 'node:process';
+
+/**
+ * Interactive IDE selection prompt
+ * @returns {Promise<'cursor'|'claude'|'both'>}
+ */
+export async function selectIde() {
+ const rl = readline.createInterface({
+ input: stdin,
+ output: stdout
+ });
+
+ console.log('\n╔══════════════════════════════════════╗');
+ console.log('║ Which AI IDE are you using? ║');
+ console.log('╠══════════════════════════════════════╣');
+ console.log('║ [1] Cursor ║');
+ console.log('║ [2] Claude Code ║');
+ console.log('║ [3] Both (dual setup) ║');
+ console.log('╚══════════════════════════════════════╝\n');
+
+ return new Promise((resolve) => {
+ rl.question('Enter your choice (1-3): ', (answer) => {
+ rl.close();
+ const choice = answer.trim();
+ switch (choice) {
+ case '1':
+ console.log('✅ Selected: Cursor');
+ resolve('cursor');
+ break;
+ case '2':
+ console.log('✅ Selected: Claude Code');
+ resolve('claude');
+ break;
+ case '3':
+ console.log('✅ Selected: Both');
+ resolve('both');
+ break;
+ default:
+ console.log('⚠️ Invalid choice. Defaulting to Cursor.');
+ resolve('cursor');
+ }
+ });
+ });
+}
+
+/**
+ * Validate IDE choice
+ * @param {string} ide
+ * @returns {string|null}
+ */
+export function validateIde(ide) {
+ const validIdes = ['cursor', 'claude', 'both'];
+ const normalized = ide?.toLowerCase().trim();
+ return validIdes.includes(normalized) ? normalized : null;
+}
+
+/**
+ * Get IDE display name
+ * @param {string} ide
+ * @returns {string}
+ */
+export function getIdeDisplayName(ide) {
+ if (ide === 'cursor') return 'Cursor';
+ if (ide === 'claude') return 'Claude Code';
+ if (ide === 'both') return 'Both';
+ return ide;
+}
diff --git a/cli/utils/interactive-menu.mjs b/cli/utils/interactive-menu.mjs
index 5be7821..a3e9e2f 100644
--- a/cli/utils/interactive-menu.mjs
+++ b/cli/utils/interactive-menu.mjs
@@ -49,7 +49,7 @@ export const createMenu = ({ title, items, currentIndex, footerLines = [] }) =>
console.info(`${highlight}${indicator}${item}${reset}`);
});
if (footerLines.length) {
- footerLines.forEach(line => console.info(line));
+ footerLines.forEach((line) => { console.info(line); });
}
}
@@ -115,7 +115,6 @@ export const interactiveCategorySelection = async (rules) => {
unmountInput(handleKeyPress);
process.stdin.removeListener('data', handleKeyPress);
console.log('\n❌ Category selection cancelled');
- currentIndex = currentIndex;
resolve(null);
break;
case '\r': // Enter
@@ -201,7 +200,7 @@ export const selectRules = async (rulesInCategory) => {
unmountInput(handleKeyPress);
resolve(allRules);
break;
- case ' ':
+ case ' ': {
const currentRule = allRules[currentIndex];
if (currentRule) {
currentRule.selected = !currentRule.selected;
@@ -209,6 +208,7 @@ export const selectRules = async (rulesInCategory) => {
renderMenu(allRules, currentIndex, selectedCount);
}
break;
+ }
case '\u001b[A': // Up arrow
if (currentIndex > 0) {
currentIndex--;
diff --git a/cli/utils/validate-dirname.mjs b/cli/utils/validate-dirname.mjs
index d14b512..10ff17f 100644
--- a/cli/utils/validate-dirname.mjs
+++ b/cli/utils/validate-dirname.mjs
@@ -22,12 +22,12 @@ const throwError = (segment, attemptedPath) => {
*/
const hasInvalidSegmentChars = (segment) => {
// Define invalid chars per segment (do not include slashes; we already split)
- // For Windows: <>:"|?* and control chars;
- const invalidWindowsChars = /[<>:"$|?*\x00-\x1F]/;
+ // For Windows: <>:"|?* and control chars 0-31
+ const invalidWindowsChars = /[<>:"$|?*\u0000-\u001F]/;
const reservedNamesRegex = /^(?:aux|con|clock\$|nul|prn|com[1-9]|lpt[1-9])$/i; // Reserved names on Windows
const extraDisallow = /[#$%&@!{}]/; // Additional characters we want to disallow on both platforms
- const invalidPosixChars = /[\x00-\x1F\\:"*?<>|$#%&@!{}]/;
+ const invalidPosixChars = /[\u0000-\u001F\\:"*?<>|$#%&@!{}]/;
if (process.platform === 'win32') {
return invalidWindowsChars.test(segment) || extraDisallow.test(segment) || reservedNamesRegex.test(segment);
]