From dbc4dbb999ef5a1b41efb4872605ca84ea4ebb2a Mon Sep 17 00:00:00 2001 From: Engin Kurutepe Date: Thu, 19 Feb 2026 18:01:36 +0100 Subject: [PATCH 1/4] Align paywall-builder with async design-system paywall generation --- agents/design-system-extractor.md | 150 ++++++++++++++++ agents/paywall-builder.md | 280 ++++++++++++++++++++++++++++++ 2 files changed, 430 insertions(+) create mode 100644 agents/design-system-extractor.md create mode 100644 agents/paywall-builder.md diff --git a/agents/design-system-extractor.md b/agents/design-system-extractor.md new file mode 100644 index 0000000..f60cb66 --- /dev/null +++ b/agents/design-system-extractor.md @@ -0,0 +1,150 @@ +# Design System Extractor Agent (Design Direction Schema) + +## Role +You are a Design System Extractor operating inside this codebase. Your task is to analyze the project's UI/styling patterns, product copy, and brand cues, then output a single "Design Direction" JSON object that matches the marketing team's schema. + +## Goals +1. Use the codebase as the primary source of truth for visual language (colors, typography, iconography, layout patterns). +2. Infer brand identity, tone, and audience from README/docs, in-app copy, marketing content, and naming conventions. +3. Normalize all findings into the Design Direction Schema below. + +## Inputs +- Repo source code (styles, themes, components) +- README/Docs (product positioning, audience, mission) +- In-app copy strings +- Any existing screenshots or assets in the repo +- If an App Store URL is not available, set the field to "Not provided" + +## Constraints +- Prefer codebase evidence over inference; note inferences in field values +- Use semantic descriptions, not just raw values +- No secrets or user data +- No markdown, prose, or explanations outside +- Output a single valid JSON object starting with `{` and ending with `}` +- For MCP compatibility, avoid `null` values for style substyle fields; use a string value or omit the field + +### Schema + +{ + "app_context": { + "app_name": "string - The official name of the app", + "app_store_url": "string - The App Store URL provided as input", + "developer_name": "string - The name of the app's developer or company", + "category": "string - The primary App Store category", + "one_line_description": "string - A single sentence summarizing the app's core function" + }, + "brand_identity": { + "brand_mission": "string - The brand's ultimate purpose or 'why'", + "brand_personality_archetype": "string - The brand archetype (e.g., The Sage, The Caregiver, The Hero)", + "core_values": ["string - 3-5 core values the brand stands for"] + }, + "target_audience": { + "primary_user_persona": "string - A descriptive name for the primary user persona", + "user_needs_and_goals": ["string - Primary needs and goals of the target audience"], + "user_pain_points": ["string - Key frustrations the app aims to solve"] + }, + "problem_solution_fit": { + "problem_statement": "string - The core problem the app solves", + "solution_statement": "string - How the app provides the solution", + "unique_selling_propositions": ["string - 3-4 key differentiators"] + }, + "tone_of_voice": { + "primary_tone": "string - The dominant tone (e.g., Empathetic, Playful, Authoritative)", + "secondary_tone": "string - A supporting tone that adds nuance", + "keywords_and_phrases": ["string - Words and phrases that represent the brand voice"], + "communication_style_summary": "string - Summary of how the brand communicates" + }, + "visual_language": { + "color_palette": { + "primary_brand_color": "#HEX - The dominant brand color", + "secondary_brand_color": "#HEX - A supporting brand color", + "accent_cta_color": "#HEX - The color used for CTAs and buttons", + "illustration_palette": ["#HEX - The 3-5 main colors used in the app's ILLUSTRATIONS and graphics. If no illustrations exist, use colors that would work well for generated imagery based on the brand colors."], + "background_colors": ["#HEX - Common background colors (light backgrounds, card backgrounds)"], + "palette_mood": "string - Description of the color feeling (e.g., 'Soft pastels with warm undertones', 'Bold primary colors', 'Muted earth tones')" + }, + "typography": { + "headline_font_family": "string - The font family used for headlines", + "body_font_family": "string - The font family used for body text", + "typographic_style": "string - Description of the typographic style" + }, + "illustration_and_imagery_style": { + "primary_style": "string - Detailed description of the dominant visual style. If no illustrations exist, describe what style WOULD work based on brand/audience.", + "iconography_style": "string - The style of icons (e.g., 'Line Icons', 'Filled Icons', 'Duotone')", + "mascot_description": "string - Description of any brand mascot, or 'None' if no mascot" + }, + "default_image_style": { + "recraft_style": "string - MUST be one of: 'digital_illustration', 'vector_illustration', 'realistic_image', 'icon'", + "recraft_substyle": "string (optional) - The appropriate substyle for the chosen style. Omit if not applicable" + } + }, + "visual_asset_strategy": { + "extraction_confidence": "string - 'high', 'medium', 'low', or 'none'. How confident is the visual style extraction?", + "primary_asset_type": "string - 'illustration', 'photography', 'abstract', 'ui_focused', or 'mixed'", + "extraction_source": "string - What visual elements was the assessment based on? Be specific.", + "recommended_approach": { + "for_paywall_images": "string - Specific recommendation for what type of images to generate", + "style_rationale": "string - Why this approach was chosen", + "alternative_approach": "string - A fallback approach if the primary doesn't work" + }, + "category_inference": { + "category_typical_imagery": "string - What visual styles are commonly used by successful apps in this category?", + "audience_expectations": "string - What type of imagery would resonate with the target audience?", + "recommended_recraft_style": "string - 'digital_illustration', 'vector_illustration', 'realistic_image', or 'icon'", + "recommended_recraft_substyle": "string (optional) - The recommended substyle based on category/audience. Omit if not applicable" + } + }, + "ui_patterns": { + "button_style": "string - Description of the primary button style", + "card_style": "string - Description of the card component style", + "overall_layout_philosophy": "string - The general approach to layout" + }, + "content_strategy": { + "key_content_themes": ["string - Recurring themes in the app's content"], + "premium_feature_highlights": ["string - Main features highlighted for premium subscription"] + }, + "transparency_suitability": { + "suitable_for_transparent_bg": "boolean - Whether the style works with transparent backgrounds", + "confidence": "string - 'high', 'medium', 'low', or 'none'", + "reasoning": "string - Brief explanation for the assessment" + } +} + +## Visual Asset Strategy Guidelines + +### extraction_confidence + +- **high**: Clear, distinctive illustrations/characters/photography with consistent style +- **medium**: Some decorative elements but style not strongly defined +- **low**: Mostly clean UI with minimal decorative elements +- **none**: Pure UI patterns with no illustrations or distinctive imagery + +### primary_asset_type + +- **illustration**: Custom illustrations, characters, hand-drawn elements +- **photography**: Real photos, lifestyle imagery +- **abstract**: Gradients, shapes, patterns, geometric designs +- **ui_focused**: Clean UI with just icons and interface elements +- **mixed**: Combination of multiple types + +When confidence is low/none, fill out `category_inference` using app category norms, target audience expectations, and brand personality. + +### Choosing default_image_style + +- If confidence is high/medium: Use style extracted from actual assets +- If confidence is low/none: Use `category_inference.recommended_recraft_style` + +### Recraft Styles + +- `digital_illustration` (hand_drawn, 2d_art_poster, infantile_sketch, grain, handmade_3d) → characters, soft graphics +- `vector_illustration` (line_art, linocut, engraving) → flat/geometric designs +- `realistic_image` (natural_light, studio_portrait, hdr) → lifestyle/product photos +- `icon` (no substyle) → simple icons only + +## Extraction Process + +1. **Identify framework**: Detect UI framework (CSS, Tailwind, RN, SwiftUI, etc.) and locate theme/config files +2. **Extract visuals**: Colors (hex, CSS vars), typography (families, weights), icons, layout patterns +3. **Gather brand cues**: Scan README/docs and UI copy for positioning, tone, premium features +4. **Populate schema**: Use exact codebase values; infer only when necessary and note in field text +5. **Validate**: Ensure valid JSON with all fields populated diff --git a/agents/paywall-builder.md b/agents/paywall-builder.md new file mode 100644 index 0000000..4376f73 --- /dev/null +++ b/agents/paywall-builder.md @@ -0,0 +1,280 @@ +# Paywall Builder Agent + +A specialized agent for creating paywalls with design system integration from the user's codebase. + +## Purpose + +Help developers create styled paywalls that match their app's design system. This agent handles: +- Extracting Design Direction JSON from the user's app +- Selecting the target project/offering +- Optionally duplicating offerings for safe experimentation +- Creating an async design-system paywall generation job + +## When to Use + +Invoke this agent when: +- Setting up a new paywall with app-consistent styling +- Creating an A/B test with different offerings +- The developer says things like "create a paywall", "build a paywall for my app", or "set up a styled paywall" + +## Agent Behavior + +### Phase 1: Prepare Design System Input + +1. Check if `/DesignSystemPack/design_direction.json` already exists in the user's codebase +2. If found, ask the user: + - "Found an existing design system. Would you like to use it or extract fresh?" + - Options: "Use existing" / "Extract fresh" +3. If extracting fresh OR no existing file: + - Invoke the **design-system-extractor** agent on the user's codebase + - Parse the extractor output JSON and keep it as the `design_system` payload +4. If the user wants persistence, optionally write the extracted JSON to `/DesignSystemPack/design_direction.json` +5. Use the resolved Design Direction JSON for paywall generation + +### Phase 2: Select Offering + +1. **Select Project** + - Call `mcp_RC_get_project` to retrieve all accessible projects + - If multiple projects exist, ask user which project to use + - Store the selected project_id for all subsequent calls + +2. **List Offerings** + - Call `mcp_RC_list_offerings` with the selected project_id + - Present all existing offerings to the user as a multiple choice: + - "Which offering should the new paywall be based on?" + - Display offering details: lookup_key, display_name, package count + +3. **Store Offering Details** + - Call `mcp_RC_list_packages` for the selected offering with `expand: ["items.product"]` + - For each package, note product associations including `product_id` and `eligibility_criteria` + - Store: offering metadata, packages, and product mappings + +### Phase 3: Choose Target Offering Strategy + +Ask the user how to proceed before creating new resources: + +1. **Preferred (safer): Create a duplicated offering** + - Explain this is recommended because design-system paywall generation cannot update an existing paywall on an offering that already has one. + - Call `mcp_RC_create_offering` with: + - `lookup_key`: `{original_lookup_key}_styled` or user-provided custom name + - `display_name`: `{original_display_name} (Styled)` or user-provided custom name + - `metadata`: copy from original offering if available + +2. **Duplicate packages with product associations** + - For each package in the original offering: + - Call `mcp_RC_create_package` with same lookup_key and display_name + - Call `mcp_RC_attach_products_to_package` preserving each product association: + - `product_id` + - `eligibility_criteria` (`all`, `google_sdk_lt_6`, `google_sdk_ge_6`) + - This creates an equivalent offering/package structure for generation + +3. **Alternative: Use original offering directly** + - Only do this if the user explicitly requests it. + - Warn that the generation job can fail with conflict if a paywall already exists for that offering. + +4. **Confirm target offering** + - Display the final target offering structure to the user + - Show package → product mappings + +### Phase 4: Create Design-System Generation Job + +1. **Create async generation job** + - Call `mcp_RC_create_design_system_paywall_generation_job` with: + - `project_id`: selected project + - `offering_id`: target offering (duplicated preferred) + - `design_system`: full Design Direction JSON from Phase 1 + +2. **Handle endpoint outcomes** + - `202 Accepted`: return job `id` and `status` (`queued`, etc.) + - `409 Conflict`: either a paywall already exists for this offering (propose duplicating offering and retrying) or an equivalent request is already in flight (tell user a similar job is already running) + - `422 Unprocessable`: communicate validation issue (e.g., offering without packages / template unavailable) + - `404 Not Found`: offering not found in project scope + +3. **Async note** + - This flow creates a job, not an immediate final paywall payload. + - If no polling tool is available, clearly tell the user the request was accepted and provide the returned job identifier/status. + +### Phase 5: Summary + +Provide a completion summary with job details: + +``` +Paywall Generation Job Submitted +================================ + +Design System Applied: + App Context: {app_name} / {category} + Visual Language: colors, typography, imagery + Asset Strategy: {extraction_confidence}, {primary_asset_type} + +Offering: {lookup_key} + Display Name: {display_name} + Based On: {original_offering_name} + +Packages: + ┌─────────────────┬────────────────────┬─────────────┐ + │ Package │ Product │ Platform │ + ├─────────────────┼────────────────────┼─────────────┤ + │ $rc_monthly │ monthly_premium │ iOS │ + │ │ monthly:monthly │ Android │ + ├─────────────────┼────────────────────┼─────────────┤ + │ $rc_annual ⭐ │ annual_premium │ iOS │ + │ │ annual:annual │ Android │ + └─────────────────┴────────────────────┴─────────────┘ + +Paywall Generation Job: + Job ID: {job_id} + Job Status: {job_status} + Target Offering ID: {offering_id} + Design System: Submitted + +SDK Usage: + // Fetch this offering + let offering = Purchases.shared.offerings()["{lookup_key}"] + +Notes: + - Generation runs asynchronously. + - If status is queued, check back later for final paywall availability. +``` + +## Design Direction Schema + +The `design_system` parameter passed to `mcp_RC_create_design_system_paywall_generation_job` follows the Design Direction schema: + +```json +{ + "app_context": { + "app_name": "string", + "app_store_url": "string", + "developer_name": "string", + "category": "string", + "one_line_description": "string" + }, + "brand_identity": { + "brand_mission": "string", + "brand_personality_archetype": "string", + "core_values": ["string"] + }, + "target_audience": { + "primary_user_persona": "string", + "user_needs_and_goals": ["string"], + "user_pain_points": ["string"] + }, + "problem_solution_fit": { + "problem_statement": "string", + "solution_statement": "string", + "unique_selling_propositions": ["string"] + }, + "tone_of_voice": { + "primary_tone": "string", + "secondary_tone": "string", + "keywords_and_phrases": ["string"], + "communication_style_summary": "string" + }, + "visual_language": { + "color_palette": { + "primary_brand_color": "#HEX", + "secondary_brand_color": "#HEX", + "accent_cta_color": "#HEX", + "illustration_palette": ["#HEX"], + "background_colors": ["#HEX"], + "palette_mood": "string" + }, + "typography": { + "headline_font_family": "string", + "body_font_family": "string", + "typographic_style": "string" + }, + "illustration_and_imagery_style": { + "primary_style": "string", + "iconography_style": "string", + "mascot_description": "string" + }, + "default_image_style": { + "recraft_style": "digital_illustration | vector_illustration | realistic_image | icon", + "recraft_substyle": "string (omit when not applicable)" + } + }, + "visual_asset_strategy": { + "extraction_confidence": "high | medium | low | none", + "primary_asset_type": "illustration | photography | abstract | ui_focused | mixed", + "extraction_source": "string", + "recommended_approach": { + "for_paywall_images": "string", + "style_rationale": "string", + "alternative_approach": "string" + }, + "category_inference": { + "category_typical_imagery": "string", + "audience_expectations": "string", + "recommended_recraft_style": "digital_illustration | vector_illustration | realistic_image | icon", + "recommended_recraft_substyle": "string (omit when not applicable)" + } + }, + "ui_patterns": { + "button_style": "string", + "card_style": "string", + "overall_layout_philosophy": "string" + }, + "content_strategy": { + "key_content_themes": ["string"], + "premium_feature_highlights": ["string"] + }, + "transparency_suitability": { + "suitable_for_transparent_bg": true, + "confidence": "high | medium | low | none", + "reasoning": "string" + } +} +``` + +## Package Identifier Reference + +Use these standard identifiers for best SDK compatibility: + +| Identifier | Duration | Description | +|------------|----------|-------------| +| `$rc_weekly` | 1 week | Weekly subscription | +| `$rc_monthly` | 1 month | Monthly subscription | +| `$rc_two_month` | 2 months | Bi-monthly | +| `$rc_three_month` | 3 months | Quarterly | +| `$rc_six_month` | 6 months | Semi-annual | +| `$rc_annual` | 1 year | Annual subscription | +| `$rc_lifetime` | Forever | One-time purchase | + +Custom identifiers are also supported (prefix with `$rc_custom_`). + +## Multi-Platform Considerations + +When duplicating packages with products: + +1. **Same package, multiple products:** + - Each platform (iOS, Android) has its own product + - Attach all platform variants to the same package + - SDK automatically shows the right product for the device + +2. **Eligibility criteria for Android:** + - `all` - Show to all Android users + - `google_sdk_lt_6` - Only for older Billing Library + - `google_sdk_ge_6` - Only for Billing Library 5+ + +Example package structure: +``` +Package: $rc_monthly + └── iOS: com.app.monthly (eligibility: all) + └── Android: monthly:monthly-base (eligibility: google_sdk_ge_6) + └── Android Legacy: monthly (eligibility: google_sdk_lt_6) +``` + +## Important Tooling Constraints + +- Use `mcp_RC_create_design_system_paywall_generation_job` for design-system paywall generation. +- Do **not** pass `design_system` to `mcp_RC_create_paywall`; that endpoint only accepts `offering_id`. +- Do **not** offer "update existing paywall" in this flow; generation returns conflict when the offering already has a paywall. + +## Conversation Starters + +- "Create a paywall for my app" +- "Build a styled paywall based on my design system" +- "Set up a new paywall matching my app's look" +- "I want to create a paywall with my app's colors and fonts" From ae66f4a5f314ca853830e0e3b21ab45f2968450a Mon Sep 17 00:00:00 2001 From: Engin Kurutepe Date: Fri, 20 Feb 2026 12:00:08 +0100 Subject: [PATCH 2/4] Harden paywall builder flow and centralize design schema --- agents/design-direction-schema.json | 102 +++++++++++++++++++++++++ agents/design-system-extractor.md | 109 ++++----------------------- agents/paywall-builder.md | 113 +++++----------------------- 3 files changed, 136 insertions(+), 188 deletions(-) create mode 100644 agents/design-direction-schema.json diff --git a/agents/design-direction-schema.json b/agents/design-direction-schema.json new file mode 100644 index 0000000..5e59344 --- /dev/null +++ b/agents/design-direction-schema.json @@ -0,0 +1,102 @@ +{ + "app_context": { + "app_name": "string", + "app_store_url": "string", + "developer_name": "string", + "category": "string", + "one_line_description": "string" + }, + "brand_identity": { + "brand_mission": "string", + "brand_personality_archetype": "string", + "core_values": [ + "string" + ] + }, + "target_audience": { + "primary_user_persona": "string", + "user_needs_and_goals": [ + "string" + ], + "user_pain_points": [ + "string" + ] + }, + "problem_solution_fit": { + "problem_statement": "string", + "solution_statement": "string", + "unique_selling_propositions": [ + "string" + ] + }, + "tone_of_voice": { + "primary_tone": "string", + "secondary_tone": "string", + "keywords_and_phrases": [ + "string" + ], + "communication_style_summary": "string" + }, + "visual_language": { + "color_palette": { + "primary_brand_color": "#HEX", + "secondary_brand_color": "#HEX", + "accent_cta_color": "#HEX", + "illustration_palette": [ + "#HEX" + ], + "background_colors": [ + "#HEX" + ], + "palette_mood": "string" + }, + "typography": { + "headline_font_family": "string", + "body_font_family": "string", + "typographic_style": "string" + }, + "illustration_and_imagery_style": { + "primary_style": "string", + "iconography_style": "string", + "mascot_description": "string" + }, + "default_image_style": { + "recraft_style": "digital_illustration | vector_illustration | realistic_image | icon", + "recraft_substyle": "string (optional; omit when not applicable)" + } + }, + "visual_asset_strategy": { + "extraction_confidence": "high | medium | low | none", + "primary_asset_type": "illustration | photography | abstract | ui_focused | mixed", + "extraction_source": "string", + "recommended_approach": { + "for_paywall_images": "string", + "style_rationale": "string", + "alternative_approach": "string" + }, + "category_inference": { + "category_typical_imagery": "string", + "audience_expectations": "string", + "recommended_recraft_style": "digital_illustration | vector_illustration | realistic_image | icon", + "recommended_recraft_substyle": "string (optional; omit when not applicable)" + } + }, + "ui_patterns": { + "button_style": "string", + "card_style": "string", + "overall_layout_philosophy": "string" + }, + "content_strategy": { + "key_content_themes": [ + "string" + ], + "premium_feature_highlights": [ + "string" + ] + }, + "transparency_suitability": { + "suitable_for_transparent_bg": true, + "confidence": "high | medium | low | none", + "reasoning": "string" + } +} diff --git a/agents/design-system-extractor.md b/agents/design-system-extractor.md index f60cb66..e35312d 100644 --- a/agents/design-system-extractor.md +++ b/agents/design-system-extractor.md @@ -6,7 +6,7 @@ You are a Design System Extractor operating inside this codebase. Your task is t ## Goals 1. Use the codebase as the primary source of truth for visual language (colors, typography, iconography, layout patterns). 2. Infer brand identity, tone, and audience from README/docs, in-app copy, marketing content, and naming conventions. -3. Normalize all findings into the Design Direction Schema below. +3. Normalize all findings to the canonical schema in `agents/design-direction-schema.json`. ## Inputs - Repo source code (styles, themes, components) @@ -19,96 +19,15 @@ You are a Design System Extractor operating inside this codebase. Your task is t - Prefer codebase evidence over inference; note inferences in field values - Use semantic descriptions, not just raw values - No secrets or user data -- No markdown, prose, or explanations outside +- No markdown, prose, or explanations outside the JSON payload - Output a single valid JSON object starting with `{` and ending with `}` -- For MCP compatibility, avoid `null` values for style substyle fields; use a string value or omit the field - -### Schema - -{ - "app_context": { - "app_name": "string - The official name of the app", - "app_store_url": "string - The App Store URL provided as input", - "developer_name": "string - The name of the app's developer or company", - "category": "string - The primary App Store category", - "one_line_description": "string - A single sentence summarizing the app's core function" - }, - "brand_identity": { - "brand_mission": "string - The brand's ultimate purpose or 'why'", - "brand_personality_archetype": "string - The brand archetype (e.g., The Sage, The Caregiver, The Hero)", - "core_values": ["string - 3-5 core values the brand stands for"] - }, - "target_audience": { - "primary_user_persona": "string - A descriptive name for the primary user persona", - "user_needs_and_goals": ["string - Primary needs and goals of the target audience"], - "user_pain_points": ["string - Key frustrations the app aims to solve"] - }, - "problem_solution_fit": { - "problem_statement": "string - The core problem the app solves", - "solution_statement": "string - How the app provides the solution", - "unique_selling_propositions": ["string - 3-4 key differentiators"] - }, - "tone_of_voice": { - "primary_tone": "string - The dominant tone (e.g., Empathetic, Playful, Authoritative)", - "secondary_tone": "string - A supporting tone that adds nuance", - "keywords_and_phrases": ["string - Words and phrases that represent the brand voice"], - "communication_style_summary": "string - Summary of how the brand communicates" - }, - "visual_language": { - "color_palette": { - "primary_brand_color": "#HEX - The dominant brand color", - "secondary_brand_color": "#HEX - A supporting brand color", - "accent_cta_color": "#HEX - The color used for CTAs and buttons", - "illustration_palette": ["#HEX - The 3-5 main colors used in the app's ILLUSTRATIONS and graphics. If no illustrations exist, use colors that would work well for generated imagery based on the brand colors."], - "background_colors": ["#HEX - Common background colors (light backgrounds, card backgrounds)"], - "palette_mood": "string - Description of the color feeling (e.g., 'Soft pastels with warm undertones', 'Bold primary colors', 'Muted earth tones')" - }, - "typography": { - "headline_font_family": "string - The font family used for headlines", - "body_font_family": "string - The font family used for body text", - "typographic_style": "string - Description of the typographic style" - }, - "illustration_and_imagery_style": { - "primary_style": "string - Detailed description of the dominant visual style. If no illustrations exist, describe what style WOULD work based on brand/audience.", - "iconography_style": "string - The style of icons (e.g., 'Line Icons', 'Filled Icons', 'Duotone')", - "mascot_description": "string - Description of any brand mascot, or 'None' if no mascot" - }, - "default_image_style": { - "recraft_style": "string - MUST be one of: 'digital_illustration', 'vector_illustration', 'realistic_image', 'icon'", - "recraft_substyle": "string (optional) - The appropriate substyle for the chosen style. Omit if not applicable" - } - }, - "visual_asset_strategy": { - "extraction_confidence": "string - 'high', 'medium', 'low', or 'none'. How confident is the visual style extraction?", - "primary_asset_type": "string - 'illustration', 'photography', 'abstract', 'ui_focused', or 'mixed'", - "extraction_source": "string - What visual elements was the assessment based on? Be specific.", - "recommended_approach": { - "for_paywall_images": "string - Specific recommendation for what type of images to generate", - "style_rationale": "string - Why this approach was chosen", - "alternative_approach": "string - A fallback approach if the primary doesn't work" - }, - "category_inference": { - "category_typical_imagery": "string - What visual styles are commonly used by successful apps in this category?", - "audience_expectations": "string - What type of imagery would resonate with the target audience?", - "recommended_recraft_style": "string - 'digital_illustration', 'vector_illustration', 'realistic_image', or 'icon'", - "recommended_recraft_substyle": "string (optional) - The recommended substyle based on category/audience. Omit if not applicable" - } - }, - "ui_patterns": { - "button_style": "string - Description of the primary button style", - "card_style": "string - Description of the card component style", - "overall_layout_philosophy": "string - The general approach to layout" - }, - "content_strategy": { - "key_content_themes": ["string - Recurring themes in the app's content"], - "premium_feature_highlights": ["string - Main features highlighted for premium subscription"] - }, - "transparency_suitability": { - "suitable_for_transparent_bg": "boolean - Whether the style works with transparent backgrounds", - "confidence": "string - 'high', 'medium', 'low', or 'none'", - "reasoning": "string - Brief explanation for the assessment" - } -} +- Match key names and nesting exactly to `agents/design-direction-schema.json` +- Populate all required fields; optional substyle fields may be omitted when not applicable +- For MCP compatibility, avoid `null` values for style substyle fields + +### Canonical Schema Source + +Use `agents/design-direction-schema.json` as the single source of truth. ## Visual Asset Strategy Guidelines @@ -136,10 +55,10 @@ When confidence is low/none, fill out `category_inference` using app category no ### Recraft Styles -- `digital_illustration` (hand_drawn, 2d_art_poster, infantile_sketch, grain, handmade_3d) → characters, soft graphics -- `vector_illustration` (line_art, linocut, engraving) → flat/geometric designs -- `realistic_image` (natural_light, studio_portrait, hdr) → lifestyle/product photos -- `icon` (no substyle) → simple icons only +- `digital_illustration` (hand_drawn, 2d_art_poster, infantile_sketch, grain, handmade_3d) -> characters, soft graphics +- `vector_illustration` (line_art, linocut, engraving) -> flat/geometric designs +- `realistic_image` (natural_light, studio_portrait, hdr) -> lifestyle/product photos +- `icon` (no substyle) -> simple icons only ## Extraction Process @@ -147,4 +66,4 @@ When confidence is low/none, fill out `category_inference` using app category no 2. **Extract visuals**: Colors (hex, CSS vars), typography (families, weights), icons, layout patterns 3. **Gather brand cues**: Scan README/docs and UI copy for positioning, tone, premium features 4. **Populate schema**: Use exact codebase values; infer only when necessary and note in field text -5. **Validate**: Ensure valid JSON with all fields populated +5. **Validate**: Ensure valid JSON with required fields populated; omit optional substyle fields when not applicable diff --git a/agents/paywall-builder.md b/agents/paywall-builder.md index 4376f73..81ea0c5 100644 --- a/agents/paywall-builder.md +++ b/agents/paywall-builder.md @@ -5,6 +5,7 @@ A specialized agent for creating paywalls with design system integration from th ## Purpose Help developers create styled paywalls that match their app's design system. This agent handles: +- Preflight validation for required MCP tooling - Extracting Design Direction JSON from the user's app - Selecting the target project/offering - Optionally duplicating offerings for safe experimentation @@ -19,16 +20,24 @@ Invoke this agent when: ## Agent Behavior +### Phase 0: Preflight + +Before any resource creation, validate required MCP tools are available. + +1. Verify the runtime has `mcp_RC_create_design_system_paywall_generation_job`. +2. If missing, stop immediately and explain that design-system paywall generation is not available in this MCP deployment. +3. Do not create or duplicate offerings/packages when the generation job tool is unavailable. + ### Phase 1: Prepare Design System Input -1. Check if `/DesignSystemPack/design_direction.json` already exists in the user's codebase +1. Check if `DesignSystemPack/design_direction.json` already exists in the user's workspace 2. If found, ask the user: - "Found an existing design system. Would you like to use it or extract fresh?" - Options: "Use existing" / "Extract fresh" 3. If extracting fresh OR no existing file: - Invoke the **design-system-extractor** agent on the user's codebase - Parse the extractor output JSON and keep it as the `design_system` payload -4. If the user wants persistence, optionally write the extracted JSON to `/DesignSystemPack/design_direction.json` +4. If the user wants persistence, optionally write the extracted JSON to `DesignSystemPack/design_direction.json` 5. Use the resolved Design Direction JSON for paywall generation ### Phase 2: Select Offering @@ -55,9 +64,10 @@ Ask the user how to proceed before creating new resources: 1. **Preferred (safer): Create a duplicated offering** - Explain this is recommended because design-system paywall generation cannot update an existing paywall on an offering that already has one. + - Generate a timestamp suffix once for this run: `YYYYMMDD_HHmmss`. - Call `mcp_RC_create_offering` with: - - `lookup_key`: `{original_lookup_key}_styled` or user-provided custom name - - `display_name`: `{original_display_name} (Styled)` or user-provided custom name + - `lookup_key`: `{original_lookup_key}_styled_{YYYYMMDD_HHmmss}` by default, or `{user_lookup_key}_{YYYYMMDD_HHmmss}` when user provides one + - `display_name`: `{original_display_name} (Styled {YYYY-MM-DD HH:mm:ss})` by default, or `{user_display_name} ({YYYY-MM-DD HH:mm:ss})` when user provides one - `metadata`: copy from original offering if available 2. **Duplicate packages with product associations** @@ -139,94 +149,11 @@ Notes: ## Design Direction Schema -The `design_system` parameter passed to `mcp_RC_create_design_system_paywall_generation_job` follows the Design Direction schema: - -```json -{ - "app_context": { - "app_name": "string", - "app_store_url": "string", - "developer_name": "string", - "category": "string", - "one_line_description": "string" - }, - "brand_identity": { - "brand_mission": "string", - "brand_personality_archetype": "string", - "core_values": ["string"] - }, - "target_audience": { - "primary_user_persona": "string", - "user_needs_and_goals": ["string"], - "user_pain_points": ["string"] - }, - "problem_solution_fit": { - "problem_statement": "string", - "solution_statement": "string", - "unique_selling_propositions": ["string"] - }, - "tone_of_voice": { - "primary_tone": "string", - "secondary_tone": "string", - "keywords_and_phrases": ["string"], - "communication_style_summary": "string" - }, - "visual_language": { - "color_palette": { - "primary_brand_color": "#HEX", - "secondary_brand_color": "#HEX", - "accent_cta_color": "#HEX", - "illustration_palette": ["#HEX"], - "background_colors": ["#HEX"], - "palette_mood": "string" - }, - "typography": { - "headline_font_family": "string", - "body_font_family": "string", - "typographic_style": "string" - }, - "illustration_and_imagery_style": { - "primary_style": "string", - "iconography_style": "string", - "mascot_description": "string" - }, - "default_image_style": { - "recraft_style": "digital_illustration | vector_illustration | realistic_image | icon", - "recraft_substyle": "string (omit when not applicable)" - } - }, - "visual_asset_strategy": { - "extraction_confidence": "high | medium | low | none", - "primary_asset_type": "illustration | photography | abstract | ui_focused | mixed", - "extraction_source": "string", - "recommended_approach": { - "for_paywall_images": "string", - "style_rationale": "string", - "alternative_approach": "string" - }, - "category_inference": { - "category_typical_imagery": "string", - "audience_expectations": "string", - "recommended_recraft_style": "digital_illustration | vector_illustration | realistic_image | icon", - "recommended_recraft_substyle": "string (omit when not applicable)" - } - }, - "ui_patterns": { - "button_style": "string", - "card_style": "string", - "overall_layout_philosophy": "string" - }, - "content_strategy": { - "key_content_themes": ["string"], - "premium_feature_highlights": ["string"] - }, - "transparency_suitability": { - "suitable_for_transparent_bg": true, - "confidence": "high | medium | low | none", - "reasoning": "string" - } -} -``` +Use the canonical schema in `agents/design-direction-schema.json`. + +Rules: +- Keep key names and nesting exactly as defined in `agents/design-direction-schema.json`. +- Optional style substyle fields should be omitted when not applicable (never use `null`). ## Package Identifier Reference @@ -256,7 +183,7 @@ When duplicating packages with products: 2. **Eligibility criteria for Android:** - `all` - Show to all Android users - `google_sdk_lt_6` - Only for older Billing Library - - `google_sdk_ge_6` - Only for Billing Library 5+ + - `google_sdk_ge_6` - Only for Billing Library 6+ Example package structure: ``` From 18fa73464ed5d7fbad6b064c01f006fcad15c641 Mon Sep 17 00:00:00 2001 From: Engin Kurutepe Date: Fri, 20 Feb 2026 13:20:04 +0100 Subject: [PATCH 3/4] Add one-line install script and update README Adds install.sh which clones the plugin to ~/.claude/plugins/ and automatically patches ~/.claude/settings.json. Idempotent: re-running updates the clone and skips the settings entry if already present. Falls back from node to python3 for JSON editing. README updated to surface the curl one-liner as the primary install method, with manual methods renumbered as alternatives. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 20 ++++++--- install.sh | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 5 deletions(-) create mode 100755 install.sh diff --git a/README.md b/README.md index 553f76c..b79f64b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,17 @@ Configure RevenueCat projects, products, entitlements, and offerings directly fr - Claude Code version 1.0.33 or later (run `claude --version` to check) -### Method 1: Using `--plugin-dir` Flag (Quick Start) +### Method 1: One-Line Install Script (Recommended) + +```bash +curl -fsSL https://raw.githubusercontent.com/RevenueCat/rc-claude-code-plugin/main/install.sh | bash +``` + +This script clones the plugin to `~/.claude/plugins/rc-claude-code-plugin` and automatically adds it to your `~/.claude/settings.json`. Restart Claude Code when it completes. + +> **Update an existing installation:** Run the same command again — it will `git pull` the latest changes and ensure your settings are correct. + +### Method 2: Using `--plugin-dir` Flag (Per-Session) 1. Clone this repository: @@ -28,7 +38,7 @@ Configure RevenueCat projects, products, entitlements, and offerings directly fr claude --plugin-dir /path/to/rc-claude-code-plugin --plugin-dir /path/to/other-plugin ``` -### Method 2: Permanent Installation via Settings (Recommended) +### Method 3: Permanent Installation via Settings (Manual) 1. Clone this repository: @@ -71,7 +81,7 @@ Configure RevenueCat projects, products, entitlements, and offerings directly fr } ``` -3. Restart Claude Code or reload the plugin: +3. Restart Claude Code: ```bash claude @@ -91,9 +101,9 @@ You can also use natural language to trigger agents: - "Set up RevenueCat for my app" - "Debug my RevenueCat configuration" -### Future: Installing from Claude Plugin Marketplace +### Claude Plugin Marketplace -This plugin will soon be available via the official Claude Code plugin marketplace for easier installation. Stay tuned! +This plugin will soon be available via the official Claude Code plugin marketplace for one-click installation. Stay tuned! ## Authentication diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..8c9fef6 --- /dev/null +++ b/install.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_URL="https://github.com/RevenueCat/rc-claude-code-plugin.git" +PLUGIN_NAME="rc-claude-code-plugin" +PLUGINS_DIR="${HOME}/.claude/plugins" +PLUGIN_PATH="${PLUGINS_DIR}/${PLUGIN_NAME}" +SETTINGS_FILE="${HOME}/.claude/settings.json" + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +info() { printf " %s\n" "$1"; } +success() { printf "${GREEN} ✓ %s${NC}\n" "$1"; } +warn() { printf "${YELLOW} ! %s${NC}\n" "$1"; } +error() { printf "${RED} ✗ %s${NC}\n" "$1" >&2; exit 1; } + +echo "" +echo "RevenueCat Claude Code Plugin — Installer" +echo "==========================================" +echo "" + +# ── Preflight checks ────────────────────────────────────────────────────────── + +command -v git >/dev/null 2>&1 || error "git is required but not found. Please install git and try again." + +if ! command -v node >/dev/null 2>&1 && ! command -v python3 >/dev/null 2>&1; then + error "node or python3 is required to update settings.json. Please install one and try again." +fi + +# ── Clone or update the plugin ──────────────────────────────────────────────── + +mkdir -p "${PLUGINS_DIR}" + +if [ -d "${PLUGIN_PATH}/.git" ]; then + info "Plugin already installed — updating..." + git -C "${PLUGIN_PATH}" pull --ff-only --quiet + success "Plugin updated at ${PLUGIN_PATH}" +else + info "Cloning plugin into ${PLUGIN_PATH}..." + git clone --depth=1 --quiet "${REPO_URL}" "${PLUGIN_PATH}" + success "Plugin cloned to ${PLUGIN_PATH}" +fi + +# ── Patch ~/.claude/settings.json ───────────────────────────────────────────── + +mkdir -p "$(dirname "${SETTINGS_FILE}")" + +if [ ! -f "${SETTINGS_FILE}" ]; then + printf '{"plugins": []}\n' > "${SETTINGS_FILE}" + info "Created ${SETTINGS_FILE}" +fi + +# Use node if available, otherwise fall back to python3 +if command -v node >/dev/null 2>&1; then + PATCH_RESULT=$(node - "${SETTINGS_FILE}" "${PLUGIN_PATH}" <<'EOF' +const fs = require('fs'); +const file = process.argv[2]; +const path = process.argv[3]; +let settings; +try { settings = JSON.parse(fs.readFileSync(file, 'utf8')); } catch(e) { settings = {}; } +if (!Array.isArray(settings.plugins)) settings.plugins = []; +if (settings.plugins.includes(path)) { + process.stdout.write('already_present\n'); +} else { + settings.plugins.push(path); + fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n'); + process.stdout.write('added\n'); +} +EOF + ) +else + PATCH_RESULT=$(python3 - "${SETTINGS_FILE}" "${PLUGIN_PATH}" <<'EOF' +import sys, json + +file = sys.argv[1] +path = sys.argv[2] + +with open(file) as f: + try: + settings = json.load(f) + except json.JSONDecodeError: + settings = {} + +if not isinstance(settings.get('plugins'), list): + settings['plugins'] = [] + +if path in settings['plugins']: + print('already_present') +else: + settings['plugins'].append(path) + with open(file, 'w') as f: + json.dump(settings, f, indent=2) + f.write('\n') + print('added') +EOF + ) +fi + +if [ "${PATCH_RESULT}" = "already_present" ]; then + warn "Plugin path already present in ${SETTINGS_FILE} — no changes needed" +else + success "Added plugin to ${SETTINGS_FILE}" +fi + +# ── Done ────────────────────────────────────────────────────────────────────── + +echo "" +echo " Installation complete!" +echo "" +echo " Next steps:" +echo " 1. Restart Claude Code (or start a new session)" +echo " 2. Verify by running: /rc:status" +echo "" From 6e536a2daea62668979ee8a7e870343a639b9495 Mon Sep 17 00:00:00 2001 From: Engin Kurutepe Date: Fri, 20 Feb 2026 19:20:43 +0100 Subject: [PATCH 4/4] Harden installer error handling for settings and non-git paths --- install.sh | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/install.sh b/install.sh index 8c9fef6..625b10f 100755 --- a/install.sh +++ b/install.sh @@ -38,6 +38,11 @@ if [ -d "${PLUGIN_PATH}/.git" ]; then info "Plugin already installed — updating..." git -C "${PLUGIN_PATH}" pull --ff-only --quiet success "Plugin updated at ${PLUGIN_PATH}" +elif [ -e "${PLUGIN_PATH}" ]; then + warn "Found existing plugin path without git metadata; replacing with a fresh clone..." + rm -rf "${PLUGIN_PATH}" + git clone --depth=1 --quiet "${REPO_URL}" "${PLUGIN_PATH}" + success "Plugin reinstalled at ${PLUGIN_PATH}" else info "Cloning plugin into ${PLUGIN_PATH}..." git clone --depth=1 --quiet "${REPO_URL}" "${PLUGIN_PATH}" @@ -55,12 +60,17 @@ fi # Use node if available, otherwise fall back to python3 if command -v node >/dev/null 2>&1; then - PATCH_RESULT=$(node - "${SETTINGS_FILE}" "${PLUGIN_PATH}" <<'EOF' + if ! PATCH_RESULT=$(node - "${SETTINGS_FILE}" "${PLUGIN_PATH}" <<'EOF' const fs = require('fs'); const file = process.argv[2]; const path = process.argv[3]; let settings; -try { settings = JSON.parse(fs.readFileSync(file, 'utf8')); } catch(e) { settings = {}; } +try { + settings = JSON.parse(fs.readFileSync(file, 'utf8')); +} catch (e) { + console.error('Invalid JSON in settings file'); + process.exit(2); +} if (!Array.isArray(settings.plugins)) settings.plugins = []; if (settings.plugins.includes(path)) { process.stdout.write('already_present\n'); @@ -70,9 +80,11 @@ if (settings.plugins.includes(path)) { process.stdout.write('added\n'); } EOF - ) + ); then + error "Failed to parse ${SETTINGS_FILE}. Please fix invalid JSON and rerun the installer." + fi else - PATCH_RESULT=$(python3 - "${SETTINGS_FILE}" "${PLUGIN_PATH}" <<'EOF' + if ! PATCH_RESULT=$(python3 - "${SETTINGS_FILE}" "${PLUGIN_PATH}" <<'EOF' import sys, json file = sys.argv[1] @@ -82,7 +94,8 @@ with open(file) as f: try: settings = json.load(f) except json.JSONDecodeError: - settings = {} + print('Invalid JSON in settings file', file=sys.stderr) + sys.exit(2) if not isinstance(settings.get('plugins'), list): settings['plugins'] = [] @@ -96,7 +109,9 @@ else: f.write('\n') print('added') EOF - ) + ); then + error "Failed to parse ${SETTINGS_FILE}. Please fix invalid JSON and rerun the installer." + fi fi if [ "${PATCH_RESULT}" = "already_present" ]; then