From ea22d06f61cbb24cb47ff78896e9ce98a02fed7f Mon Sep 17 00:00:00 2001 From: Dennis V <2119348+dzianisv@users.noreply.github.com> Date: Mon, 22 Jun 2026 20:20:44 +0000 Subject: [PATCH] =?UTF-8?q?fix(cua):=20do=20not=20press=20back=20after=20t?= =?UTF-8?q?yping=20=E2=80=94=20adb=20input=20text=20does=20not=20show=20ke?= =?UTF-8?q?yboard,=20back=20navigates=20away?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app-store-optimization/HOW_TO_USE.md | 281 +++++++ .../skills/app-store-optimization/SKILL.md | 492 ++++++++++++ .../assets/aso-audit-template.md | 268 +++++++ .../expected_output.json | 170 ++++ .../references/aso-best-practices.md | 403 ++++++++++ .../references/keyword-research-guide.md | 419 ++++++++++ .../references/platform-requirements.md | 324 ++++++++ .../app-store-optimization/sample_input.json | 30 + .../scripts/ab_test_planner.py | 662 ++++++++++++++++ .../scripts/aso_scorer.py | 482 ++++++++++++ .../scripts/competitor_analyzer.py | 577 ++++++++++++++ .../scripts/keyword_analyzer.py | 406 ++++++++++ .../scripts/launch_checklist.py | 739 ++++++++++++++++++ .../scripts/localization_helper.py | 588 ++++++++++++++ .../scripts/metadata_optimizer.py | 581 ++++++++++++++ .../scripts/review_analyzer.py | 714 +++++++++++++++++ .claude/skills/app-store-optimization | 1 + .../page-2026-06-22T16-55-47-014Z.yml | 95 +++ .../page-2026-06-22T16-55-55-587Z.yml | 74 ++ ...-b7ab-4297-8058-e7f100764903_attempts.json | 1 + ...-bdc8-45b0-b7e4-9fef25893bf5_attempts.json | 1 + .supervisor/current_session | 1 + ..._56dcc5bc-b7ab-4297-8058-e7f100764903.json | 12 + ..._69e7ce6b-bdc8-45b0-b7e4-9fef25893bf5.json | 12 + scripts/android-cua-smoke.py | 10 +- 25 files changed, 7339 insertions(+), 4 deletions(-) create mode 100644 .agents/skills/app-store-optimization/HOW_TO_USE.md create mode 100644 .agents/skills/app-store-optimization/SKILL.md create mode 100644 .agents/skills/app-store-optimization/assets/aso-audit-template.md create mode 100644 .agents/skills/app-store-optimization/expected_output.json create mode 100644 .agents/skills/app-store-optimization/references/aso-best-practices.md create mode 100644 .agents/skills/app-store-optimization/references/keyword-research-guide.md create mode 100644 .agents/skills/app-store-optimization/references/platform-requirements.md create mode 100644 .agents/skills/app-store-optimization/sample_input.json create mode 100644 .agents/skills/app-store-optimization/scripts/ab_test_planner.py create mode 100644 .agents/skills/app-store-optimization/scripts/aso_scorer.py create mode 100644 .agents/skills/app-store-optimization/scripts/competitor_analyzer.py create mode 100644 .agents/skills/app-store-optimization/scripts/keyword_analyzer.py create mode 100644 .agents/skills/app-store-optimization/scripts/launch_checklist.py create mode 100644 .agents/skills/app-store-optimization/scripts/localization_helper.py create mode 100644 .agents/skills/app-store-optimization/scripts/metadata_optimizer.py create mode 100644 .agents/skills/app-store-optimization/scripts/review_analyzer.py create mode 120000 .claude/skills/app-store-optimization create mode 100644 .playwright-mcp/page-2026-06-22T16-55-47-014Z.yml create mode 100644 .playwright-mcp/page-2026-06-22T16-55-55-587Z.yml create mode 100644 .supervisor/56dcc5bc-b7ab-4297-8058-e7f100764903_attempts.json create mode 100644 .supervisor/69e7ce6b-bdc8-45b0-b7e4-9fef25893bf5_attempts.json create mode 100644 .supervisor/current_session create mode 100644 .supervisor/verdict_56dcc5bc-b7ab-4297-8058-e7f100764903.json create mode 100644 .supervisor/verdict_69e7ce6b-bdc8-45b0-b7e4-9fef25893bf5.json diff --git a/.agents/skills/app-store-optimization/HOW_TO_USE.md b/.agents/skills/app-store-optimization/HOW_TO_USE.md new file mode 100644 index 0000000..67e68a8 --- /dev/null +++ b/.agents/skills/app-store-optimization/HOW_TO_USE.md @@ -0,0 +1,281 @@ +# How to Use the App Store Optimization Skill + +Hey Claude—I just added the "app-store-optimization" skill. Can you help me optimize my app's presence on the App Store and Google Play? + +## Example Invocations + +### Keyword Research + +**Example 1: Basic Keyword Research** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you research the best keywords for my productivity app? I'm targeting professionals who need task management and team collaboration features. +``` + +**Example 2: Competitive Keyword Analysis** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you analyze keywords that Todoist, Asana, and Monday.com are using? I want to find gaps and opportunities for my project management app. +``` + +### Metadata Optimization + +**Example 3: Optimize App Title** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you optimize my app title for the Apple App Store? My app is called "TaskFlow" and I want to rank for "task manager", "productivity", and "team collaboration". The title needs to be under 30 characters. +``` + +**Example 4: Full Metadata Package** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you create optimized metadata for both Apple App Store and Google Play Store? Here's my app info: +- Name: TaskFlow +- Category: Productivity +- Key features: AI task prioritization, team collaboration, calendar integration +- Target keywords: task manager, productivity app, team tasks +``` + +### Competitor Analysis + +**Example 5: Analyze Top Competitors** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you analyze the ASO strategies of the top 5 productivity apps in the App Store? I want to understand their title strategies, keyword usage, and visual asset approaches. +``` + +**Example 6: Identify Competitive Gaps** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you compare my app's ASO performance against competitors and identify what I'm missing? Here's my current metadata: [paste metadata] +``` + +### ASO Score Calculation + +**Example 7: Calculate Overall ASO Health** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you calculate my app's ASO health score? Here are my metrics: +- Average rating: 4.2 stars +- Total ratings: 3,500 +- Keywords in top 10: 3 +- Keywords in top 50: 12 +- Conversion rate: 4.5% +``` + +**Example 8: Identify Improvement Areas** +``` +Hey Claude—I just added the "app-store-optimization" skill. My ASO score is 62/100. Can you tell me which areas I should focus on first to improve my rankings and downloads? +``` + +### A/B Testing + +**Example 9: Plan Icon Test** +``` +Hey Claude—I just added the "app-store-optimization" skill. I want to A/B test two different app icons. My current conversion rate is 5%. Can you help me plan the test, calculate required sample size, and determine how long to run it? +``` + +**Example 10: Analyze Test Results** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you analyze my A/B test results? +- Variant A (control): 2,500 visitors, 125 installs +- Variant B (new icon): 2,500 visitors, 150 installs +Is this statistically significant? Should I implement variant B? +``` + +### Localization + +**Example 11: Plan Localization Strategy** +``` +Hey Claude—I just added the "app-store-optimization" skill. I currently only have English metadata. Which markets should I localize for first? I'm a bootstrapped startup with moderate budget. +``` + +**Example 12: Translate Metadata** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you help me translate my app metadata to Spanish for the Mexico market? Here's my English metadata: [paste metadata]. Check if it fits within character limits. +``` + +### Review Analysis + +**Example 13: Analyze User Reviews** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you analyze my recent reviews and tell me: +- Overall sentiment (positive/negative ratio) +- Most common complaints +- Most requested features +- Bugs that need immediate fixing +``` + +**Example 14: Generate Review Response Templates** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you create professional response templates for: +- Users reporting crashes +- Feature requests +- Positive 5-star reviews +- General complaints +``` + +### Launch Planning + +**Example 15: Pre-Launch Checklist** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you generate a comprehensive pre-launch checklist for both Apple App Store and Google Play Store? My launch date is December 1, 2025. +``` + +**Example 16: Optimize Launch Timing** +``` +Hey Claude—I just added the "app-store-optimization" skill. What's the best day and time to launch my fitness app? I want to maximize visibility and downloads in the first week. +``` + +**Example 17: Plan Seasonal Campaign** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you identify seasonal opportunities for my fitness app? It's currently October—what campaigns should I run for the next 6 months? +``` + +## What to Provide + +### For Keyword Research +- App name and category +- Target audience description +- Key features and unique value proposition +- Competitor apps (optional) +- Geographic markets to target + +### For Metadata Optimization +- Current app name +- Platform (Apple, Google, or both) +- Target keywords (prioritized list) +- Key features and benefits +- Target audience +- Current metadata (for optimization) + +### For Competitor Analysis +- Your app category +- List of competitor app names or IDs +- Platform (Apple or Google) +- Specific aspects to analyze (keywords, visuals, ratings) + +### For ASO Score Calculation +- Metadata quality metrics (title length, description length, keyword density) +- Rating data (average rating, total ratings, recent ratings) +- Keyword rankings (top 10, top 50, top 100 counts) +- Conversion metrics (impression-to-install rate, downloads) + +### For A/B Testing +- Test type (icon, screenshot, title, description) +- Control variant details +- Test variant details +- Baseline conversion rate +- For results analysis: visitor and conversion counts for both variants + +### For Localization +- Current market and language +- Budget level (low, medium, high) +- Target number of markets +- Current metadata text for translation + +### For Review Analysis +- Recent reviews (text, rating, date) +- Platform (Apple or Google) +- Time period to analyze +- Specific focus (bugs, features, sentiment) + +### For Launch Planning +- Platform (Apple, Google, or both) +- Target launch date +- App category +- App information (name, features, target audience) + +## What You'll Get + +### Keyword Research Output +- Prioritized keyword list with search volume estimates +- Competition level analysis +- Relevance scores +- Long-tail keyword opportunities +- Strategic recommendations + +### Metadata Optimization Output +- Optimized titles (multiple options) +- Optimized descriptions (short and full) +- Keyword field optimization (Apple) +- Character count validation +- Keyword density analysis +- Before/after comparison + +### Competitor Analysis Output +- Ranked competitors by ASO strength +- Common keyword patterns +- Keyword gaps and opportunities +- Visual asset assessment +- Best practices identified +- Actionable recommendations + +### ASO Score Output +- Overall score (0-100) +- Breakdown by category (metadata, ratings, keywords, conversion) +- Strengths and weaknesses +- Prioritized action items +- Expected impact of improvements + +### A/B Test Output +- Test design with hypothesis +- Required sample size calculation +- Duration estimates +- Statistical significance analysis +- Implementation recommendations +- Learnings and insights + +### Localization Output +- Prioritized target markets +- Estimated translation costs +- ROI projections +- Character limit validation for each language +- Cultural adaptation recommendations +- Phased implementation plan + +### Review Analysis Output +- Sentiment distribution (positive/neutral/negative) +- Common themes and topics +- Top issues requiring fixes +- Most requested features +- Response templates +- Trend analysis over time + +### Launch Planning Output +- Platform-specific checklists (Apple, Google, Universal) +- Timeline with milestones +- Compliance validation +- Optimal launch timing recommendations +- Seasonal campaign opportunities +- Update cadence planning + +## Tips for Best Results + +1. **Be Specific**: Provide as much detail about your app as possible +2. **Include Context**: Share your goals (increase downloads, improve ranking, boost conversion) +3. **Provide Data**: Real metrics enable more accurate analysis +4. **Iterate**: Start with keyword research, then optimize metadata, then test +5. **Track Results**: Monitor changes after implementing recommendations +6. **Stay Compliant**: Always verify recommendations against current App Store/Play Store guidelines +7. **Test First**: Use A/B testing before making major metadata changes +8. **Localize Strategically**: Start with highest-ROI markets first +9. **Respond to Reviews**: Use provided templates to engage with users +10. **Plan Ahead**: Use launch checklists and timelines to avoid last-minute rushes + +## Common Workflows + +### New App Launch +1. Keyword research → Competitor analysis → Metadata optimization → Pre-launch checklist → Launch timing optimization + +### Improving Existing App +1. ASO score calculation → Identify gaps → Metadata optimization → A/B testing → Review analysis → Implement changes + +### International Expansion +1. Localization planning → Market prioritization → Metadata translation → ROI analysis → Phased rollout + +### Ongoing Optimization +1. Monthly keyword ranking tracking → Quarterly metadata updates → Continuous A/B testing → Review monitoring → Seasonal campaigns + +## Need Help? + +If you need clarification on any aspect of ASO or want to combine multiple analyses, just ask! For example: + +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you create a complete ASO strategy for my new productivity app? I need keyword research, optimized metadata for both stores, a pre-launch checklist, and launch timing recommendations. +``` + +The skill can handle comprehensive, multi-phase ASO projects as well as specific tactical optimizations. diff --git a/.agents/skills/app-store-optimization/SKILL.md b/.agents/skills/app-store-optimization/SKILL.md new file mode 100644 index 0000000..4e53605 --- /dev/null +++ b/.agents/skills/app-store-optimization/SKILL.md @@ -0,0 +1,492 @@ +--- +name: app-store-optimization +description: Use this skill when optimizing app store listings, researching keywords, or tracking mobile app performance on Apple App Store and Google Play Store. +metadata: + triggers: + - ASO + - app store optimization + - app store ranking + - app keywords + - app metadata + - play store optimization + - app store listing + - improve app rankings + - app visibility + - app store SEO + - mobile app marketing + - app conversion rate +--- + +# App Store Optimization (ASO) + +## Workspace Context + +Read bootstrap context before asking questions: `strategy/brand.md` for brand, audience, offer, channels, tools, constraints, and metrics; `about/me.md` for personal voice; `content/ideas.md` and `content/calendar.md` for content planning. Use legacy product-marketing context files only as fallback. Save generated drafts to `content//drafts/YYYY-MM-DD_short-topic-slug.md`, and route durable learnings back to `strategy/brand.md`, `about/me.md`, or `content/ideas.md`. + +## Operating Contract + +This skill is self-contained for its frontmatter scope: use its local instructions, references, scripts, and assets as the playbook; ask only for missing task-specific inputs; hand off to adjacent skills instead of expanding scope; and return an actionable artifact, decision, plan, draft, or diagnostic. + + + +ASO tools for researching keywords, optimizing metadata, analyzing competitors, and improving app store visibility on Apple App Store and Google Play Store. + +## Keyword Research Workflow + +Discover and evaluate keywords that drive app store visibility. + +### Workflow: Conduct Keyword Research + +1. Define target audience and core app functions: + - Primary use case (what problem does the app solve) + - Target user demographics + - Competitive category +2. Generate seed keywords from: + - App features and benefits + - User language (not developer terminology) + - App store autocomplete suggestions +3. Expand keyword list using: + - Modifiers (free, best, simple) + - Actions (create, track, organize) + - Audiences (for students, for teams, for business) +4. Evaluate each keyword: + - Search volume (estimated monthly searches) + - Competition (number and quality of ranking apps) + - Relevance (alignment with app function) +5. Score and prioritize keywords: + - Primary: Title and keyword field (iOS) + - Secondary: Subtitle and short description + - Tertiary: Full description only +6. Map keywords to metadata locations +7. Document keyword strategy for tracking +8. **Validation:** Keywords scored; placement mapped; no competitor brand names included; no plurals in iOS keyword field + +### Keyword Evaluation Criteria + +| Factor | Weight | High Score Indicators | +|--------|--------|----------------------| +| Relevance | 35% | Describes core app function | +| Volume | 25% | 10,000+ monthly searches | +| Competition | 25% | Top 10 apps have <4.5 avg rating | +| Conversion | 15% | Transactional intent ("best X app") | + +### Keyword Placement Priority + +| Location | Search Weight | Character Limit | +|----------|---------------|-----------------| +| App Title | Highest | 30 (iOS) / 50 (Android) | +| Subtitle (iOS) | High | 30 | +| Keyword Field (iOS) | High | 100 | +| Short Description (Android) | High | 80 | +| Full Description | Medium | 4,000 | + +See: [references/keyword-research-guide.md](references/keyword-research-guide.md) + +--- + +## Metadata Optimization Workflow + +Optimize app store listing elements for search ranking and conversion. + +### Workflow: Optimize App Metadata + +1. Audit current metadata against platform limits: + - Title character count and keyword presence + - Subtitle/short description usage + - Keyword field efficiency (iOS) + - Description keyword density +2. Optimize title following formula: + ``` + [Brand Name] - [Primary Keyword] [Secondary Keyword] + ``` +3. Write subtitle (iOS) or short description (Android): + - Focus on primary benefit + - Include secondary keyword + - Use action verbs +4. Optimize keyword field (iOS only): + - Remove duplicates from title + - Remove plurals (Apple indexes both forms) + - No spaces after commas + - Prioritize by score +5. Rewrite full description: + - Hook paragraph with value proposition + - Feature bullets with keywords + - Social proof section + - Call to action +6. Validate character counts for each field +7. Calculate keyword density (target 2-3% primary) +8. **Validation:** All fields within character limits; primary keyword in title; no keyword stuffing (>5%); natural language preserved + +### Platform Character Limits + +| Field | Apple App Store | Google Play Store | +|-------|-----------------|-------------------| +| Title | 30 characters | 50 characters | +| Subtitle | 30 characters | N/A | +| Short Description | N/A | 80 characters | +| Keywords | 100 characters | N/A | +| Promotional Text | 170 characters | N/A | +| Full Description | 4,000 characters | 4,000 characters | +| What's New | 4,000 characters | 500 characters | + +### Description Structure + +``` +PARAGRAPH 1: Hook (50-100 words) +├── Address user pain point +├── State main value proposition +└── Include primary keyword + +PARAGRAPH 2-3: Features (100-150 words) +├── Top 5 features with benefits +├── Bullet points for scanability +└── Secondary keywords naturally integrated + +PARAGRAPH 4: Social Proof (50-75 words) +├── Download count or rating +├── Press mentions or awards +└── Summary of user testimonials + +PARAGRAPH 5: Call to Action (25-50 words) +├── Clear next step +└── Reassurance (free trial, no signup) +``` + +See: [references/platform-requirements.md](references/platform-requirements.md) + +--- + +## Competitor Analysis Workflow + +Analyze top competitors to identify keyword gaps and positioning opportunities. + +### Workflow: Analyze Competitor ASO Strategy + +1. Identify top 10 competitors: + - Direct competitors (same core function) + - Indirect competitors (overlapping audience) + - Category leaders (top downloads) +2. Extract competitor keywords from: + - App titles and subtitles + - First 100 words of descriptions + - Visible metadata patterns +3. Build competitor keyword matrix: + - Map which keywords each competitor targets + - Calculate coverage percentage per keyword +4. Identify keyword gaps: + - Keywords with <40% competitor coverage + - High volume terms competitors miss + - Long-tail opportunities +5. Analyze competitor visual assets: + - Icon design patterns + - Screenshot messaging and style + - Video presence and quality +6. Compare ratings and review patterns: + - Average rating by competitor + - Common praise themes + - Common complaint themes +7. Document positioning opportunities +8. **Validation:** 10+ competitors analyzed; keyword matrix complete; gaps identified with volume estimates; visual audit documented + +### Competitor Analysis Matrix + +| Analysis Area | Data Points | +|---------------|-------------| +| Keywords | Title keywords, description frequency | +| Metadata | Character utilization, keyword density | +| Visuals | Icon style, screenshot count/style | +| Ratings | Average rating, total count, velocity | +| Reviews | Top praise, top complaints | + +### Gap Analysis Template + +| Opportunity Type | Example | Action | +|------------------|---------|--------| +| Keyword gap | "habit tracker" (40% coverage) | Add to keyword field | +| Feature gap | Competitor lacks widget | Highlight in screenshots | +| Visual gap | No videos in top 5 | Create app preview | +| Messaging gap | None mention "free" | Test free positioning | + +--- + +## App Launch Workflow + +Execute a structured launch for maximum initial visibility. + +### Workflow: Launch App to Stores + +1. Complete pre-launch preparation (4 weeks before): + - Finalize keywords and metadata + - Prepare all visual assets + - Set up analytics (Firebase, Mixpanel) + - Build press kit and media list +2. Submit for review (2 weeks before): + - Complete all store requirements + - Verify compliance with guidelines + - Prepare launch communications +3. Configure post-launch systems: + - Set up review monitoring + - Prepare response templates + - Configure rating prompt timing +4. Execute launch day: + - Verify app is live in both stores + - Announce across all channels + - Begin review response cycle +5. Monitor initial performance (days 1-7): + - Track download velocity hourly + - Monitor reviews and respond within 24 hours + - Document any issues for quick fixes +6. Conduct 7-day retrospective: + - Compare performance to projections + - Identify quick optimization wins + - Plan first metadata update +7. Schedule first update (2 weeks post-launch) +8. **Validation:** App live in stores; analytics tracking; review responses within 24h; download velocity documented; first update scheduled + +### Pre-Launch Checklist + +| Category | Items | +|----------|-------| +| Metadata | Title, subtitle, description, keywords | +| Visual Assets | Icon, screenshots (all sizes), video | +| Compliance | Age rating, privacy policy, content rights | +| Technical | App binary, signing certificates | +| Analytics | SDK integration, event tracking | +| Marketing | Press kit, social content, email ready | + +### Launch Timing Considerations + +| Factor | Recommendation | +|--------|----------------| +| Day of week | Tuesday-Wednesday (avoid weekends) | +| Time of day | Morning in target market timezone | +| Seasonal | Align with relevant category seasons | +| Competition | Avoid major competitor launch dates | + +See: [references/aso-best-practices.md](references/aso-best-practices.md) + +--- + +## A/B Testing Workflow + +Test metadata and visual elements to improve conversion rates. + +### Workflow: Run A/B Test + +1. Select test element (prioritize by impact): + - Icon (highest impact) + - Screenshot 1 (high impact) + - Title (high impact) + - Short description (medium impact) +2. Form hypothesis: + ``` + If we [change], then [metric] will [improve/increase] by [amount] + because [rationale]. + ``` +3. Create variants: + - Control: Current version + - Treatment: Single variable change +4. Calculate required sample size: + - Baseline conversion rate + - Minimum detectable effect (usually 5%) + - Statistical significance (95%) +5. Launch test: + - Apple: Use Product Page Optimization + - Android: Use Store Listing Experiments +6. Run test for minimum duration: + - At least 7 days + - Until statistical significance reached +7. Analyze results: + - Compare conversion rates + - Check statistical significance + - Document learnings +8. **Validation:** Single variable tested; sample size sufficient; significance reached (95%); results documented; winner implemented + +### A/B Test Prioritization + +| Element | Conversion Impact | Test Complexity | +|---------|-------------------|-----------------| +| App Icon | 10-25% lift possible | Medium (design needed) | +| Screenshot 1 | 15-35% lift possible | Medium | +| Title | 5-15% lift possible | Low | +| Short Description | 5-10% lift possible | Low | +| Video | 10-20% lift possible | High | + +### Sample Size Quick Reference + +| Baseline CVR | Impressions Needed (per variant) | +|--------------|----------------------------------| +| 1% | 31,000 | +| 2% | 15,500 | +| 5% | 6,200 | +| 10% | 3,100 | + +### Test Documentation Template + +``` +TEST ID: ASO-2025-001 +ELEMENT: App Icon +HYPOTHESIS: A bolder color icon will increase conversion by 10% +START DATE: [Date] +END DATE: [Date] + +RESULTS: +├── Control CVR: 4.2% +├── Treatment CVR: 4.8% +├── Lift: +14.3% +├── Significance: 97% +└── Decision: Implement treatment + +LEARNINGS: +- Bold colors outperform muted tones in this category +- Apply to screenshot backgrounds for next test +``` + +--- + +## Before/After Examples + +### Title Optimization + +**Productivity App:** + +| Version | Title | Analysis | +|---------|-------|----------| +| Before | "MyTasks" | No keywords, brand only (8 chars) | +| After | "MyTasks - Todo List & Planner" | Primary + secondary keywords (29 chars) | + +**Fitness App:** + +| Version | Title | Analysis | +|---------|-------|----------| +| Before | "FitTrack Pro" | Generic modifier (12 chars) | +| After | "FitTrack: Workout Log & Gym" | Category keywords (27 chars) | + +### Subtitle Optimization (iOS) + +| Version | Subtitle | Analysis | +|---------|----------|----------| +| Before | "Get Things Done" | Vague, no keywords | +| After | "Daily Task Manager & Planner" | Two keywords, benefit clear | + +### Keyword Field Optimization (iOS) + +**Before (Inefficient - 89 chars, 8 keywords):** +``` +task manager, todo list, productivity app, daily planner, reminder app +``` + +**After (Optimized - 97 chars, 14 keywords):** +``` +task,todo,checklist,reminder,organize,daily,planner,schedule,deadline,goals,habit,widget,sync,team +``` + +**Improvements:** +- Removed spaces after commas (+8 chars) +- Removed duplicates (task manager → task) +- Removed plurals (reminders → reminder) +- Removed words in title +- Added more relevant keywords + +### Description Opening + +**Before:** +``` +MyTasks is a comprehensive task management solution designed +to help busy professionals organize their daily activities +and boost productivity. +``` + +**After:** +``` +Forget missed deadlines. MyTasks keeps every task, reminder, +and project in one place—so you focus on doing, not remembering. +Trusted by 500,000+ professionals. +``` + +**Improvements:** +- Leads with user pain point +- Specific benefit (not generic "boost productivity") +- Social proof included +- Keywords natural, not stuffed + +### Screenshot Caption Evolution + +| Version | Caption | Issue | +|---------|---------|-------| +| Before | "Task List Feature" | Feature-focused, passive | +| Better | "Create Task Lists" | Action verb, but still feature | +| Best | "Never Miss a Deadline" | Benefit-focused, emotional | + +--- + +## Tools and References + +### Scripts + +| Script | Purpose | Usage | +|--------|---------|-------| +| [keyword_analyzer.py](scripts/keyword_analyzer.py) | Analyze keywords for volume and competition | `python keyword_analyzer.py --keywords "todo,task,planner"` | +| [metadata_optimizer.py](scripts/metadata_optimizer.py) | Validate metadata character limits and density | `python metadata_optimizer.py --platform ios --title "App Title"` | +| [competitor_analyzer.py](scripts/competitor_analyzer.py) | Extract and compare competitor keywords | `python competitor_analyzer.py --competitors "App1,App2,App3"` | +| [aso_scorer.py](scripts/aso_scorer.py) | Calculate overall ASO health score | `python aso_scorer.py --app-id com.example.app` | +| [ab_test_planner.py](scripts/ab_test_planner.py) | Plan tests and calculate sample sizes | `python ab_test_planner.py --cvr 0.05 --lift 0.10` | +| [review_analyzer.py](scripts/review_analyzer.py) | Analyze review sentiment and themes | `python review_analyzer.py --app-id com.example.app` | +| [launch_checklist.py](scripts/launch_checklist.py) | Generate platform-specific launch checklists | `python launch_checklist.py --platform ios` | +| [localization_helper.py](scripts/localization_helper.py) | Manage multi-language metadata | `python localization_helper.py --locales "en,es,de,ja"` | + +### References + +| Document | Content | +|----------|---------| +| [platform-requirements.md](references/platform-requirements.md) | iOS and Android metadata specs, visual asset requirements | +| [aso-best-practices.md](references/aso-best-practices.md) | Optimization strategies, rating management, launch tactics | +| [keyword-research-guide.md](references/keyword-research-guide.md) | Research methodology, evaluation framework, tracking | + +### Assets + +| Template | Purpose | +|----------|---------| +| [aso-audit-template.md](assets/aso-audit-template.md) | Structured audit checklist for app store listings | + +--- + +## Platform Limitations + +### Data Constraints + +| Constraint | Impact | +|------------|--------| +| No official keyword volume data | Estimates based on third-party tools | +| Competitor data limited to public info | Cannot see internal metrics | +| Review access limited to public reviews | No access to private feedback | +| Historical data unavailable for new apps | Cannot compare to past performance | + +### Platform Behavior + +| Platform | Behavior | +|----------|----------| +| iOS | Keyword changes require app submission | +| iOS | Promotional text editable without update | +| Android | Metadata changes index in 1-2 hours | +| Android | No separate keyword field (use description) | +| Both | Algorithm changes without notice | + +### When Not to Use This Skill + +| Scenario | Alternative | +|----------|-------------| +| Web apps | Use web SEO skills | +| Enterprise apps (not public) | Internal distribution tools | +| Beta/TestFlight only | Focus on feedback, not ASO | +| Paid advertising strategy | Use paid acquisition skills | + +--- + +## Related Skills + +| Skill | Integration Point | +|-------|-------------------| +| [copywriting-core](../copywriting-core/) | App description copywriting | +| [lead-generation-and-demand](../lead-generation-and-demand/) | Launch promotion campaigns | +| [go-to-market-strategy](../go-to-market-strategy/) | Go-to-market planning | diff --git a/.agents/skills/app-store-optimization/assets/aso-audit-template.md b/.agents/skills/app-store-optimization/assets/aso-audit-template.md new file mode 100644 index 0000000..4a72762 --- /dev/null +++ b/.agents/skills/app-store-optimization/assets/aso-audit-template.md @@ -0,0 +1,268 @@ +# ASO Audit Template + +Use this template to conduct a systematic App Store Optimization audit. + +--- + +## App Information + +| Field | Value | +|-------|-------| +| App Name | | +| Platform | [ ] iOS [ ] Android | +| Category | | +| Current Downloads | | +| Current Rating | | +| Audit Date | | + +--- + +## Metadata Audit + +### Title Analysis + +| Criterion | iOS (30 chars) | Android (50 chars) | +|-----------|----------------|---------------------| +| Current Title | | | +| Character Count | /30 | /50 | +| Primary Keyword Present | [ ] Yes [ ] No | [ ] Yes [ ] No | +| Brand Name Position | | | + +**Title Score:** ___/10 + +**Recommendations:** +- [ ] +- [ ] + +### Subtitle / Short Description + +| Criterion | iOS Subtitle (30 chars) | Android Short Desc (80 chars) | +|-----------|-------------------------|-------------------------------| +| Current Text | | | +| Character Count | /30 | /80 | +| Keywords Included | | | +| Benefit-Focused | [ ] Yes [ ] No | [ ] Yes [ ] No | + +**Score:** ___/10 + +**Recommendations:** +- [ ] +- [ ] + +### Keyword Field (iOS Only) + +| Criterion | Status | +|-----------|--------| +| Current Keywords | | +| Character Count | /100 | +| Duplicates Present | [ ] Yes [ ] No | +| Plurals Included | [ ] Yes [ ] No | +| Brand Names Included | [ ] Yes [ ] No | + +**Score:** ___/10 + +**Recommendations:** +- [ ] +- [ ] + +### Full Description + +| Criterion | iOS | Android | +|-----------|-----|---------| +| Character Count | /4000 | /4000 | +| Primary Keyword Density | % | % | +| Secondary Keywords (count) | | | +| Feature Bullets Present | [ ] Yes [ ] No | [ ] Yes [ ] No | +| Social Proof Included | [ ] Yes [ ] No | [ ] Yes [ ] No | +| CTA Present | [ ] Yes [ ] No | [ ] Yes [ ] No | + +**Score:** ___/10 + +**Recommendations:** +- [ ] +- [ ] + +--- + +## Visual Asset Audit + +### App Icon + +| Criterion | Status | +|-----------|--------| +| Recognizable at 60x60px | [ ] Yes [ ] No | +| Distinct from competitors | [ ] Yes [ ] No | +| Matches app design | [ ] Yes [ ] No | +| No text/words | [ ] Yes [ ] No | + +**Score:** ___/10 + +**Recommendations:** +- [ ] +- [ ] + +### Screenshots + +| Screenshot | Caption | Feature Shown | Score | +|------------|---------|---------------|-------| +| 1 (Hero) | | | /10 | +| 2 | | | /10 | +| 3 | | | /10 | +| 4 | | | /10 | +| 5 | | | /10 | + +| Criterion | Status | +|-----------|--------| +| Total Screenshots | /10 (iOS) or /8 (Android) | +| Captions Present | [ ] Yes [ ] No | +| Consistent Style | [ ] Yes [ ] No | +| First 3 Show Value | [ ] Yes [ ] No | +| Device Frames Used | [ ] Yes [ ] No | + +**Overall Screenshot Score:** ___/10 + +**Recommendations:** +- [ ] +- [ ] + +### App Preview Video + +| Criterion | Status | +|-----------|--------| +| Video Present | [ ] Yes [ ] No | +| Duration | seconds | +| Shows Core Features | [ ] Yes [ ] No | +| Hook in First 5 Seconds | [ ] Yes [ ] No | +| CTA at End | [ ] Yes [ ] No | + +**Score:** ___/10 + +--- + +## Keyword Performance Audit + +### Current Keyword Rankings + +| Keyword | Current Rank | Volume | Competition | Score | +|---------|--------------|--------|-------------|-------| +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | + +### Keyword Opportunities + +| Keyword | Current Rank | Potential | Action | +|---------|--------------|-----------|--------| +| | | | | +| | | | | +| | | | | + +--- + +## Rating & Review Audit + +### Rating Summary + +| Metric | Value | +|--------|-------| +| Current Average Rating | /5.0 | +| Total Ratings | | +| Ratings (Last 30 Days) | | +| 5-Star Percentage | % | +| 1-Star Percentage | % | + +### Review Analysis + +| Category | Count | Common Themes | +|----------|-------|---------------| +| Positive (4-5 stars) | | | +| Neutral (3 stars) | | | +| Negative (1-2 stars) | | | + +### Response Rate + +| Metric | Value | +|--------|-------| +| Reviews Responded | % | +| Avg Response Time | hours | + +**Rating Score:** ___/10 + +**Recommendations:** +- [ ] +- [ ] + +--- + +## Competitor Comparison + +### Top 3 Competitors + +| Metric | Your App | Competitor 1 | Competitor 2 | Competitor 3 | +|--------|----------|--------------|--------------|--------------| +| Name | | | | | +| Rating | | | | | +| Total Ratings | | | | | +| Downloads | | | | | +| Title Keywords | | | | | +| Screenshot Count | | | | | + +### Competitive Gaps + +| Gap Identified | Opportunity | +|----------------|-------------| +| | | +| | | +| | | + +--- + +## Overall ASO Score + +| Category | Weight | Score | Weighted | +|----------|--------|-------|----------| +| Title/Metadata | 25% | /10 | | +| Keywords | 25% | /10 | | +| Visual Assets | 25% | /10 | | +| Ratings/Reviews | 25% | /10 | | +| **TOTAL** | 100% | | **/100** | + +--- + +## Priority Action Items + +### High Priority (This Week) + +1. [ ] +2. [ ] +3. [ ] + +### Medium Priority (This Month) + +1. [ ] +2. [ ] +3. [ ] + +### Low Priority (This Quarter) + +1. [ ] +2. [ ] +3. [ ] + +--- + +## Audit Sign-Off + +| Role | Name | Date | +|------|------|------| +| Auditor | | | +| Reviewer | | | +| App Owner | | | + +--- + +## Notes + +_Additional observations and context:_ diff --git a/.agents/skills/app-store-optimization/expected_output.json b/.agents/skills/app-store-optimization/expected_output.json new file mode 100644 index 0000000..9832693 --- /dev/null +++ b/.agents/skills/app-store-optimization/expected_output.json @@ -0,0 +1,170 @@ +{ + "request_type": "keyword_research", + "app_name": "TaskFlow Pro", + "keyword_analysis": { + "total_keywords_analyzed": 25, + "primary_keywords": [ + { + "keyword": "task manager", + "search_volume": 45000, + "competition_level": "high", + "relevance_score": 0.95, + "difficulty_score": 72.5, + "potential_score": 78.3, + "recommendation": "High priority - target immediately" + }, + { + "keyword": "productivity app", + "search_volume": 38000, + "competition_level": "high", + "relevance_score": 0.90, + "difficulty_score": 68.2, + "potential_score": 75.1, + "recommendation": "High priority - target immediately" + }, + { + "keyword": "todo list", + "search_volume": 52000, + "competition_level": "very_high", + "relevance_score": 0.85, + "difficulty_score": 78.9, + "potential_score": 71.4, + "recommendation": "High priority - target immediately" + } + ], + "secondary_keywords": [ + { + "keyword": "team task manager", + "search_volume": 8500, + "competition_level": "medium", + "relevance_score": 0.88, + "difficulty_score": 42.3, + "potential_score": 68.7, + "recommendation": "Good opportunity - include in metadata" + }, + { + "keyword": "project planning app", + "search_volume": 12000, + "competition_level": "medium", + "relevance_score": 0.75, + "difficulty_score": 48.1, + "potential_score": 64.2, + "recommendation": "Good opportunity - include in metadata" + } + ], + "long_tail_keywords": [ + { + "keyword": "ai task prioritization", + "search_volume": 2800, + "competition_level": "low", + "relevance_score": 0.95, + "difficulty_score": 25.4, + "potential_score": 82.6, + "recommendation": "Excellent long-tail opportunity" + }, + { + "keyword": "team productivity tool", + "search_volume": 3500, + "competition_level": "low", + "relevance_score": 0.85, + "difficulty_score": 28.7, + "potential_score": 79.3, + "recommendation": "Excellent long-tail opportunity" + } + ] + }, + "competitor_insights": { + "competitors_analyzed": 4, + "common_keywords": [ + "task", + "todo", + "list", + "productivity", + "organize", + "manage" + ], + "keyword_gaps": [ + { + "keyword": "ai prioritization", + "used_by": ["None of the major competitors"], + "opportunity": "Unique positioning opportunity" + }, + { + "keyword": "smart task manager", + "used_by": ["Things 3"], + "opportunity": "Underutilized by most competitors" + } + ] + }, + "metadata_recommendations": { + "apple_app_store": { + "title_options": [ + { + "title": "TaskFlow - AI Task Manager", + "length": 26, + "keywords_included": ["task manager", "ai"], + "strategy": "brand_plus_primary" + }, + { + "title": "TaskFlow: Smart Todo & Tasks", + "length": 29, + "keywords_included": ["todo", "tasks"], + "strategy": "brand_plus_multiple" + } + ], + "subtitle_recommendation": "AI-Powered Team Productivity", + "keyword_field": "productivity,organize,planner,schedule,workflow,reminders,collaboration,calendar,sync,priorities", + "description_focus": "Lead with AI differentiation, emphasize team features" + }, + "google_play_store": { + "title_options": [ + { + "title": "TaskFlow - AI Task Manager & Team Productivity", + "length": 48, + "keywords_included": ["task manager", "ai", "team", "productivity"], + "strategy": "keyword_rich" + } + ], + "short_description_recommendation": "AI task manager - Organize, prioritize, and collaborate with your team", + "description_focus": "Keywords naturally integrated throughout 4000 character description" + } + }, + "strategic_recommendations": [ + "Focus on 'AI prioritization' as unique differentiator - low competition, high relevance", + "Target 'team task manager' and 'team productivity' keywords - good search volume, lower competition than generic terms", + "Include long-tail keywords in description for additional discovery opportunities", + "Test title variations with A/B testing after launch", + "Monitor competitor keyword changes quarterly" + ], + "priority_actions": [ + { + "action": "Optimize app title with primary keyword", + "priority": "high", + "expected_impact": "15-25% improvement in search visibility" + }, + { + "action": "Create description highlighting AI features with natural keyword integration", + "priority": "high", + "expected_impact": "10-15% improvement in conversion rate" + }, + { + "action": "Plan A/B tests for icon and screenshots post-launch", + "priority": "medium", + "expected_impact": "5-10% improvement in conversion rate" + } + ], + "aso_health_estimate": { + "current_score": "N/A (pre-launch)", + "potential_score_with_optimizations": "75-80/100", + "key_strengths": [ + "Unique AI differentiation", + "Clear target audience", + "Strong feature set" + ], + "areas_to_develop": [ + "Build rating volume post-launch", + "Monitor and respond to reviews", + "Continuous keyword optimization" + ] + } +} diff --git a/.agents/skills/app-store-optimization/references/aso-best-practices.md b/.agents/skills/app-store-optimization/references/aso-best-practices.md new file mode 100644 index 0000000..9ce7ca2 --- /dev/null +++ b/.agents/skills/app-store-optimization/references/aso-best-practices.md @@ -0,0 +1,403 @@ +# ASO Best Practices Reference + +Optimization strategies for improving app store visibility, conversion, and rankings. + +--- + +## Table of Contents + +- [Keyword Optimization](#keyword-optimization) +- [Metadata Optimization](#metadata-optimization) +- [Visual Asset Optimization](#visual-asset-optimization) +- [Rating and Review Management](#rating-and-review-management) +- [Launch Strategy](#launch-strategy) +- [A/B Testing Framework](#ab-testing-framework) +- [Conversion Optimization](#conversion-optimization) +- [Common Mistakes to Avoid](#common-mistakes-to-avoid) + +--- + +## Keyword Optimization + +### Keyword Research Process + +1. **Brainstorm seed keywords** - Core terms users search for +2. **Expand with variations** - Synonyms, related terms, long-tail +3. **Analyze competition** - Check difficulty scores +4. **Evaluate search volume** - Prioritize high-volume terms +5. **Test and iterate** - Monitor rankings and adjust + +### Keyword Selection Criteria + +| Factor | Weight | Evaluation Method | +|--------|--------|-------------------| +| Relevance | 40% | Does it describe app function? | +| Search Volume | 30% | Monthly search estimates | +| Competition | 20% | Number of ranking apps | +| Conversion Potential | 10% | User intent alignment | + +### Keyword Placement Priority + +| Location | Search Weight | Example | +|----------|---------------|---------| +| App Title | Highest | "TaskMaster - Todo List Manager" | +| Subtitle (iOS) | High | "Organize Your Daily Tasks" | +| Keyword Field (iOS) | High | "planner,reminder,checklist" | +| Short Description (Android) | High | "Simple task manager for busy professionals" | +| Full Description | Medium | Natural keyword usage throughout | + +### Long-Tail Keyword Strategy + +Long-tail keywords have lower volume but higher conversion: + +| Type | Example | Volume | Competition | Conversion | +|------|---------|--------|-------------|------------| +| Short-tail | "todo app" | High | High | Low | +| Mid-tail | "daily task manager" | Medium | Medium | Medium | +| Long-tail | "free todo list with reminders" | Low | Low | High | + +**Formula for keyword priority:** +``` +Score = (Volume × 0.3) + (1/Competition × 0.3) + (Relevance × 0.4) +``` + +--- + +## Metadata Optimization + +### Title Optimization + +**Structure Formula:** +``` +[Brand Name] - [Primary Keyword] [Secondary Keyword/Benefit] +``` + +**Examples by category:** + +| Category | Before | After | +|----------|--------|-------| +| Productivity | "MyTasks" | "MyTasks - Todo List & Planner" | +| Fitness | "FitTrack" | "FitTrack: Workout & Gym Log" | +| Finance | "MoneyApp" | "MoneyApp - Budget Tracker" | +| Photo | "SnapEdit" | "SnapEdit: Photo Editor & AI" | + +**Title Optimization Checklist:** +- [ ] Primary keyword within first 3 words +- [ ] Brand name is memorable and unique +- [ ] Character count matches platform limit +- [ ] No keyword stuffing +- [ ] Readable and natural sounding + +### Description Optimization + +**Full Description Structure:** + +``` +PARAGRAPH 1: Hook + Primary Benefit (50-100 words) +- Address user pain point +- State main value proposition +- Include primary keyword naturally + +PARAGRAPH 2-3: Feature Highlights (100-150 words) +- Top 3-5 features with benefits +- Use bullet points or emojis for scanability +- Include secondary keywords + +PARAGRAPH 4: Social Proof (50-75 words) +- Download numbers or ratings +- Press mentions or awards +- User testimonials (summarized) + +PARAGRAPH 5: Call to Action (25-50 words) +- Clear next step +- Urgency or incentive +- Reassurance (free trial, no credit card) +``` + +**Keyword Density Target:** +- Primary keyword: 2-3% (8-12 mentions in 4000 chars) +- Secondary keywords: 1-2% each (4-8 mentions each) + +### Subtitle Optimization (iOS) + +**Effective Subtitle Formulas:** + +| Formula | Example | +|---------|---------| +| [Verb] + [Benefit] | "Organize Your Life" | +| [Adjective] + [Category] | "Smart Task Manager" | +| [Feature] + [Feature] | "Lists, Reminders & Notes" | +| [Audience] + [Solution] | "For Busy Professionals" | + +--- + +## Visual Asset Optimization + +### App Icon Best Practices + +| Principle | Do | Don't | +|-----------|-----|-------| +| Simplicity | Single focal element | Multiple competing elements | +| Recognizability | Works at 60x60px | Requires large size to read | +| Uniqueness | Distinct from competitors | Generic category icon | +| Color | Bold, contrasting colors | Muted or similar to background | +| Text | None or single letter | Full words or app name | + +**Icon Testing Questions:** +1. Is it recognizable at 29x29px (smallest iOS size)? +2. Does it stand out in search results? +3. Does it communicate app function? +4. Is it distinct from top 10 category competitors? + +### Screenshot Optimization + +**Screenshot Hierarchy:** + +| Position | Purpose | Content Strategy | +|----------|---------|------------------| +| Screenshot 1 | Hook/Hero | Main value proposition + key UI | +| Screenshot 2 | Primary Feature | Most-used feature demonstration | +| Screenshot 3 | Secondary Feature | Differentiating capability | +| Screenshot 4 | Social Proof | Ratings, awards, user count | +| Screenshot 5+ | Additional Features | Supporting functionality | + +**Caption Best Practices:** +- Maximum 5-7 words per caption +- Action-oriented verbs ("Track", "Organize", "Discover") +- Benefit-focused, not feature-focused +- Consistent typography and style + +**Example Caption Evolution:** + +| Weak | Better | Best | +|------|--------|------| +| "Task List Feature" | "Create Task Lists" | "Never Forget a Task Again" | +| "Calendar View" | "See Your Schedule" | "Plan Your Week in Seconds" | +| "Notifications" | "Get Reminders" | "Stay on Top of Deadlines" | + +### Video Preview Strategy + +**Video Structure (30 seconds):** + +| Seconds | Content | +|---------|---------| +| 0-5 | Hook: Show end result or main benefit | +| 5-15 | Demo: Core feature in action | +| 15-25 | Features: Quick feature montage | +| 25-30 | CTA: Logo and download prompt | + +--- + +## Rating and Review Management + +### Review Response Framework + +**For Negative Reviews (1-2 stars):** + +``` +Structure: +1. Acknowledge the issue (1 sentence) +2. Apologize without making excuses (1 sentence) +3. Offer solution or next step (1-2 sentences) +4. Invite direct contact (1 sentence) + +Example: +"We're sorry the syncing issues are affecting your experience. +Our team is actively working on a fix for the next update. +In the meantime, please try logging out and back in, which +resolves this for most users. If issues persist, email us at +support@app.com and we'll prioritize your case." +``` + +**For Positive Reviews (4-5 stars):** + +``` +Structure: +1. Thank sincerely (1 sentence) +2. Acknowledge specific praise (1 sentence) +3. Encourage continued use or sharing (1 sentence) + +Example: +"Thank you for the kind words! We're thrilled the reminder +feature helps you stay organized. If you're enjoying the app, +we'd love if you'd share it with friends who might benefit." +``` + +### Rating Improvement Tactics + +| Tactic | Implementation | Expected Impact | +|--------|----------------|-----------------| +| In-app prompt timing | After positive action (task completed, milestone reached) | +0.3 stars | +| Bug fix velocity | Address 1-star issues within 7 days | +0.2 stars | +| Response rate | Reply to 80%+ of reviews | +0.1 stars | +| Feature requests | Implement top-requested features | +0.2 stars | + +### Review Prompt Best Practices + +**When to prompt:** +- After user completes 5+ successful sessions +- After milestone achievement (first task completed, 7-day streak) +- After positive in-app feedback ("Was this helpful? Yes") + +**When NOT to prompt:** +- First session +- After error or crash +- During critical workflow +- More than once per 30 days + +--- + +## Launch Strategy + +### Pre-Launch Checklist + +**4 Weeks Before Launch:** +- [ ] Finalize app name and keywords +- [ ] Complete all metadata fields +- [ ] Prepare all visual assets +- [ ] Set up analytics (Firebase, Mixpanel) +- [ ] Create press kit and media assets +- [ ] Build email list for launch notification + +**2 Weeks Before Launch:** +- [ ] Submit for app review +- [ ] Prepare social media content +- [ ] Brief press and influencers +- [ ] Set up review response templates +- [ ] Configure in-app rating prompts + +**Launch Day:** +- [ ] Verify app is live in stores +- [ ] Announce across all channels +- [ ] Monitor reviews and respond quickly +- [ ] Track download velocity +- [ ] Document any issues for Day 2 fix + +### Update Cadence + +| Update Type | Frequency | ASO Impact | +|-------------|-----------|------------| +| Bug fixes | As needed | Prevents rating drops | +| Minor features | Every 2-4 weeks | Maintains freshness signal | +| Major features | Every 4-8 weeks | Opportunity for "What's New" | +| Metadata refresh | Every 4-6 weeks | Keyword optimization cycle | + +### Seasonal Optimization + +| Season | Optimization Focus | Example Categories | +|--------|--------------------|--------------------| +| Jan (New Year) | Resolutions, goals | Fitness, Productivity | +| Feb (Valentine's) | Dating, relationships | Dating, Photo | +| Mar-Apr (Tax) | Finance, organization | Finance, Productivity | +| May-Jun (Summer) | Travel, fitness | Travel, Health | +| Aug-Sep (Back to School) | Education, organization | Education, Productivity | +| Nov-Dec (Holidays) | Shopping, social | Shopping, Social | + +--- + +## A/B Testing Framework + +### Test Prioritization Matrix + +| Element | Impact | Ease | Priority | +|---------|--------|------|----------| +| App Icon | High | Medium | 1 | +| Screenshot 1 | High | Medium | 2 | +| Title | High | Easy | 3 | +| Short Description | Medium | Easy | 4 | +| Screenshots 2-5 | Medium | Medium | 5 | +| Video | Medium | Hard | 6 | + +### Sample Size Calculator + +**Formula:** +``` +Sample Size = (2 × (Z² × p × (1-p))) / E² + +Where: +Z = 1.96 (for 95% confidence) +p = baseline conversion rate +E = minimum detectable effect (usually 0.05) +``` + +**Quick Reference:** + +| Baseline CVR | Min. Impressions for 5% Lift | +|--------------|------------------------------| +| 1% | 31,000 per variant | +| 2% | 15,500 per variant | +| 5% | 6,200 per variant | +| 10% | 3,100 per variant | + +### Test Duration Guidelines + +| Daily Impressions | Minimum Test Duration | +|-------------------|----------------------| +| 1,000+ | 7 days | +| 500-1,000 | 14 days | +| 100-500 | 30 days | +| <100 | Not recommended | + +--- + +## Conversion Optimization + +### Conversion Funnel Metrics + +| Stage | Metric | Benchmark | +|-------|--------|-----------| +| Discovery | Impressions | Category dependent | +| Consideration | Page Views | 30-50% of impressions | +| Conversion | Installs | 3-8% of page views | +| Activation | First Open | 70-90% of installs | + +### Conversion Optimization Levers + +| Lever | Typical Lift | Effort | +|-------|--------------|--------| +| Icon redesign | 10-25% | High | +| Screenshot optimization | 15-35% | Medium | +| Title keyword optimization | 5-15% | Low | +| Description rewrite | 5-10% | Low | +| Video addition | 10-20% | High | +| Localization | 20-50% per market | Medium | + +--- + +## Common Mistakes to Avoid + +### Keyword Mistakes + +| Mistake | Problem | Solution | +|---------|---------|----------| +| Keyword stuffing | Spam detection, rejection | Natural usage, 2-3% density | +| Competitor names | Guideline violation | Focus on category terms | +| Duplicate keywords | Wasted character space | Remove duplicates from keyword field | +| Ignoring long-tail | Missing conversion | Include specific phrases | + +### Metadata Mistakes + +| Mistake | Problem | Solution | +|---------|---------|----------| +| Vague descriptions | Low conversion | Specific benefits and features | +| Feature-focused copy | Doesn't resonate | Benefit-focused messaging | +| Outdated information | Misleading users | Update with each release | +| Missing localization | Lost global revenue | Prioritize top 5 markets | + +### Visual Asset Mistakes + +| Mistake | Problem | Solution | +|---------|---------|----------| +| Text-heavy screenshots | Unreadable on phones | Minimal text, clear UI focus | +| Inconsistent style | Unprofessional appearance | Design system for all assets | +| Portrait-only screenshots | Missed tablet users | Include landscape variants | +| No social proof | Lower trust | Add ratings, awards, press | + +### Launch Mistakes + +| Mistake | Problem | Solution | +|---------|---------|----------| +| Launching on Friday | No support over weekend | Launch Tuesday-Wednesday | +| No analytics setup | Can't measure success | Firebase/Mixpanel before launch | +| Immediate rating prompt | Negative ratings | Wait for positive experience | +| Ignoring reviews | Declining ratings | Respond within 24-48 hours | diff --git a/.agents/skills/app-store-optimization/references/keyword-research-guide.md b/.agents/skills/app-store-optimization/references/keyword-research-guide.md new file mode 100644 index 0000000..1110aa6 --- /dev/null +++ b/.agents/skills/app-store-optimization/references/keyword-research-guide.md @@ -0,0 +1,419 @@ +# Keyword Research Guide + +Systematic approach to discovering, evaluating, and selecting keywords for app store optimization. + +--- + +## Table of Contents + +- [Keyword Research Methodology](#keyword-research-methodology) +- [Keyword Evaluation Framework](#keyword-evaluation-framework) +- [Competitor Keyword Analysis](#competitor-keyword-analysis) +- [Keyword Mapping Strategy](#keyword-mapping-strategy) +- [Keyword Tracking and Iteration](#keyword-tracking-and-iteration) + +--- + +## Keyword Research Methodology + +### Phase 1: Seed Keyword Generation + +Start by generating initial keyword ideas from multiple sources. + +**Source 1: Core App Functions** + +List every action or problem the app solves: + +``` +Example for a task management app: +- Create tasks +- Set reminders +- Track deadlines +- Organize projects +- Collaborate with team +- Plan daily schedule +``` + +**Source 2: User Language Mapping** + +Match developer terminology to user searches: + +| Developer Term | User Search Terms | +|----------------|-------------------| +| Task management | todo list, task app, tasks | +| Project organization | project planner, project tracker | +| Deadline tracking | due date reminder, deadline app | +| Time blocking | schedule planner, calendar app | +| GTD methodology | getting things done, productivity system | + +**Source 3: App Store Autocomplete** + +Type seed keywords into App Store/Play Store search and record suggestions: + +``` +"todo" → todo list, todo app, todo list app, todolist widget +"task" → task manager, task planner, task list, tasks to do +"remind" → reminder app, reminder, reminders widget, remind me +``` + +**Source 4: Competitor Analysis** + +Extract keywords from top 10 competitors in category (detailed in section below). + +### Phase 2: Keyword Expansion + +**Expansion Techniques:** + +| Technique | Example (seed: "todo") | +|-----------|------------------------| +| Add modifiers | free todo, best todo, simple todo | +| Add actions | make todo list, create todo, organize todo | +| Add platforms | todo app iphone, todo for mac, todo widget | +| Add audiences | todo for students, business todo, family todo | +| Add features | todo with reminders, todo calendar, todo sync | +| Add problems | forgot tasks todo, procrastination todo | + +**Keyword Matrix Template:** + +| Core Term | Modifier 1 | Modifier 2 | Full Keyword | +|-----------|------------|------------|--------------| +| todo | free | app | free todo app | +| todo | best | iphone | best todo iphone | +| task | manager | simple | simple task manager | +| reminder | daily | widget | daily reminder widget | +| planner | weekly | calendar | weekly planner calendar | + +### Phase 3: Keyword Filtering + +Remove irrelevant or low-quality keywords: + +**Exclusion Criteria:** + +| Criterion | Reason | Example | +|-----------|--------|---------| +| Competitor brand names | Policy violation | "todoist alternative" | +| Unrelated categories | Low conversion | "todo games" | +| Plural duplicates (iOS) | Wasted space | "tasks" when "task" exists | +| Single characters | No search value | "to do" vs "todo" | + +--- + +## Keyword Evaluation Framework + +### Keyword Scoring Model + +Evaluate each keyword on four dimensions: + +**1. Search Volume (0-100)** + +| Volume Level | Score | Monthly Searches | +|--------------|-------|------------------| +| Very High | 80-100 | 50,000+ | +| High | 60-79 | 10,000-49,999 | +| Medium | 40-59 | 1,000-9,999 | +| Low | 20-39 | 100-999 | +| Very Low | 0-19 | <100 | + +**2. Competition (0-100, inverted)** + +| Competition | Score | Top 10 App Ratings | +|-------------|-------|-------------------| +| Very Low | 80-100 | Average <4.0 stars | +| Low | 60-79 | Average 4.0-4.2 stars | +| Medium | 40-59 | Average 4.3-4.5 stars | +| High | 20-39 | Average 4.6-4.8 stars | +| Very High | 0-19 | Average 4.9+ stars | + +**3. Relevance (0-100)** + +| Relevance | Score | Criteria | +|-----------|-------|----------| +| Exact Match | 90-100 | Keyword describes core function | +| Strong Match | 70-89 | Keyword describes major feature | +| Moderate Match | 50-69 | Keyword describes secondary feature | +| Weak Match | 30-49 | Keyword tangentially related | +| No Match | 0-29 | Keyword unrelated to app | + +**4. Conversion Potential (0-100)** + +| Intent | Score | User Query Type | +|--------|-------|-----------------| +| Transactional | 80-100 | "best [app type]", "[app type] app" | +| Commercial | 60-79 | "free [app type]", "[app type] for [use]" | +| Informational | 40-59 | "how to [action]", "what is [concept]" | +| Navigational | 20-39 | "[brand name]", "[specific app]" | + +### Composite Score Calculation + +``` +Keyword Score = (Volume × 0.25) + (Competition × 0.25) + + (Relevance × 0.35) + (Conversion × 0.15) +``` + +**Score Interpretation:** + +| Score Range | Priority | Action | +|-------------|----------|--------| +| 80-100 | Primary | Target in title and keyword field | +| 60-79 | Secondary | Include in subtitle/description | +| 40-59 | Tertiary | Use in long description only | +| 0-39 | Deprioritize | Do not target | + +### Keyword Evaluation Worksheet + +``` +KEYWORD EVALUATION + +Keyword: "task manager app" +Date: [Date] + +SCORES: +├── Search Volume: 72/100 (High - ~25,000/month) +├── Competition: 45/100 (Medium - 4.4 avg rating in top 10) +├── Relevance: 95/100 (Exact match to core function) +└── Conversion: 85/100 (Transactional intent) + +COMPOSITE SCORE: 74.5/100 + +RECOMMENDATION: Secondary Priority +- Include in subtitle or short description +- Not competitive enough for title (dominated by Todoist, Any.do) +- Consider long-tail variant: "simple task manager app" +``` + +--- + +## Competitor Keyword Analysis + +### Competitor Identification + +**Step 1: Direct Competitors** +Apps solving the same problem for the same audience. + +**Step 2: Indirect Competitors** +Apps solving related problems or targeting overlapping audiences. + +**Step 3: Category Leaders** +Top 10-20 apps by downloads in primary category. + +### Competitor Keyword Extraction + +**From App Title:** +``` +Competitor: "Todoist: To-Do List & Tasks" +Keywords: todoist, to-do list, tasks, to do +``` + +**From Subtitle (iOS):** +``` +Competitor subtitle: "Task Manager & Planner" +Keywords: task manager, planner +``` + +**From Description (First 100 words):** +Identify frequently used terms: +``` +"Todoist is the world's favorite task manager and to-do list app. +Organize work and life, hit your goals, and find productivity..." + +Extracted: task manager, to-do list, organize, goals, productivity +``` + +### Competitor Keyword Matrix + +| Keyword | Comp 1 | Comp 2 | Comp 3 | Comp 4 | Comp 5 | Coverage | +|---------|--------|--------|--------|--------|--------|----------| +| task manager | ✓ | ✓ | ✓ | ✓ | ✓ | 100% | +| to-do list | ✓ | ✓ | ✓ | ✓ | | 80% | +| planner | ✓ | ✓ | | ✓ | ✓ | 80% | +| reminder | ✓ | ✓ | ✓ | | | 60% | +| productivity | ✓ | | ✓ | ✓ | | 60% | +| checklist | | ✓ | | ✓ | ✓ | 60% | +| project | ✓ | ✓ | | | | 40% | +| habit | | | ✓ | | ✓ | 40% | + +**Analysis:** +- 100% coverage = Highly competitive, essential keyword +- 60-80% coverage = Important category term +- 40% coverage = Potential differentiator +- <40% coverage = Unique opportunity or irrelevant + +### Keyword Gap Analysis + +Identify keywords competitors miss: + +``` +KEYWORD GAP ANALYSIS + +Underserved Keywords (Low competitor coverage, decent volume): +1. "daily planner widget" - 2/10 competitors, 5,000 searches +2. "task list for teams" - 3/10 competitors, 3,500 searches +3. "todo with calendar sync" - 1/10 competitors, 2,800 searches + +Opportunity Assessment: +- "daily planner widget" → Add widget feature, target keyword +- "task list for teams" → Already have feature, update metadata +- "todo with calendar sync" → Feature gap, add to roadmap +``` + +--- + +## Keyword Mapping Strategy + +### Keyword Placement Map + +Assign each keyword to specific metadata locations: + +``` +KEYWORD PLACEMENT MAP + +PRIMARY (Title + Keyword Field): +├── task manager (Score: 82) +├── todo list (Score: 78) +└── planner (Score: 75) + +SECONDARY (Subtitle + Short Description): +├── reminder app (Score: 68) +├── daily tasks (Score: 65) +└── organize (Score: 62) + +TERTIARY (Full Description): +├── checklist (Score: 55) +├── productivity (Score: 52) +├── schedule (Score: 48) +├── deadline (Score: 45) +└── project management (Score: 42) +``` + +### iOS Keyword Field Strategy + +**100 Character Optimization:** + +``` +STEP 1: List all target keywords +task,manager,todo,list,planner,reminder,organize,daily,checklist, +productivity,schedule,deadline,project,goals,habit,widget,sync, +team,collaborate,notes,calendar + +STEP 2: Remove duplicates from title +Title: "TaskFlow - Todo List Manager" +Remove: task, todo, list, manager + +STEP 3: Remove plurals +Keep: reminder (not reminders) +Keep: goal (not goals) + +STEP 4: Prioritize by score and fit +Final 100 chars: +planner,reminder,organize,daily,checklist,productivity,schedule, +deadline,project,goals,habit,widget,sync,team,collaborate + +Character count: 98/100 +``` + +### Android Description Keyword Integration + +**Natural keyword placement in 4,000 characters:** + +``` +PARAGRAPH 1 (Hook - 300 chars): +Keywords: task manager, todo list, organize +"TaskFlow is the task manager trusted by 2 million users. Create +your perfect todo list and organize everything that matters..." + +PARAGRAPH 2 (Features - 800 chars): +Keywords: reminder, checklist, deadline, project +"Set smart reminders that notify you at the right time. Build +checklists for any project. Never miss a deadline with..." + +PARAGRAPH 3 (Benefits - 600 chars): +Keywords: productivity, schedule, goals +"Boost your productivity with proven planning methods. Schedule +your day in minutes. Track goals and celebrate..." + +PARAGRAPH 4 (Differentiators - 500 chars): +Keywords: widget, sync, team, collaborate +"Beautiful widgets keep tasks visible. Sync across all devices +instantly. Invite your team to collaborate on..." + +Total keyword coverage: 14 keywords naturally integrated +``` + +--- + +## Keyword Tracking and Iteration + +### Ranking Tracking Cadence + +| Frequency | Action | +|-----------|--------| +| Daily | Track top 5-10 primary keywords | +| Weekly | Full keyword set review | +| Monthly | Competitor keyword comparison | +| Quarterly | Full keyword research refresh | + +### Keyword Performance Metrics + +| Metric | Target | Action if Below | +|--------|--------|-----------------| +| Top 10 ranking | 3+ keywords | Increase keyword weight | +| Top 50 ranking | 10+ keywords | Maintain current strategy | +| Ranking velocity | Improving trend | Continue optimization | +| Conversion rate | >5% | Review relevance alignment | + +### Iteration Process + +**Monthly Keyword Audit:** + +``` +1. EXPORT current rankings + - List all tracked keywords + - Record current position + - Note 30-day trend (up/down/stable) + +2. IDENTIFY opportunities + - Keywords improving but not top 10 + - Keywords declining from previous position + - New high-volume keywords in category + +3. PRIORITIZE changes + - Boost: Keywords at position 11-20 + - Maintain: Keywords at position 1-10 + - Replace: Keywords at position 50+ with no improvement + +4. IMPLEMENT updates + - Adjust keyword field (iOS) + - Update description (Android) + - Modify subtitle if needed + +5. DOCUMENT changes + - Record what changed and why + - Set reminder for 2-week check-in +``` + +### Keyword Testing Log Template + +``` +KEYWORD TEST LOG + +Test ID: KW-2025-001 +Date Started: [Date] +Keywords Changed: + - Added: "habit tracker" (replacing "goals app") + - Added: "daily routine" (replacing "schedule planner") + +Rationale: +- "habit tracker" has 3x volume of "goals app" +- "daily routine" trending up 40% in category + +Baseline Rankings: +- "habit tracker": Not ranked +- "daily routine": Position 87 + +30-Day Results: +- "habit tracker": Position 34 (+53) +- "daily routine": Position 28 (+59) + +Conclusion: Test successful - retain new keywords +Next Action: Target subtitle position for "habit tracker" +``` diff --git a/.agents/skills/app-store-optimization/references/platform-requirements.md b/.agents/skills/app-store-optimization/references/platform-requirements.md new file mode 100644 index 0000000..16a0804 --- /dev/null +++ b/.agents/skills/app-store-optimization/references/platform-requirements.md @@ -0,0 +1,324 @@ +# Platform Requirements Reference + +Technical specifications and metadata requirements for Apple App Store and Google Play Store. + +--- + +## Table of Contents + +- [Apple App Store Requirements](#apple-app-store-requirements) +- [Google Play Store Requirements](#google-play-store-requirements) +- [Visual Asset Specifications](#visual-asset-specifications) +- [Localization Requirements](#localization-requirements) +- [Compliance Guidelines](#compliance-guidelines) + +--- + +## Apple App Store Requirements + +### Metadata Character Limits + +| Field | Character Limit | Notes | +|-------|----------------|-------| +| App Name (Title) | 30 characters | Visible in search results | +| Subtitle | 30 characters | iOS 11+ only, appears below title | +| Promotional Text | 170 characters | Editable without app update | +| Description | 4,000 characters | Not indexed for search | +| Keywords Field | 100 characters | Comma-separated, no spaces after commas | +| What's New | 4,000 characters | Release notes for updates | +| Developer Name | 255 characters | Company or individual name | +| Support URL | Required | Must be valid HTTPS URL | +| Privacy Policy URL | Required | Must be valid HTTPS URL | + +### Keyword Field Optimization Rules + +1. **No duplicates** - Words in title are already indexed +2. **No plurals** - Apple indexes both singular and plural forms +3. **No spaces after commas** - Wastes character space +4. **No brand names** - Violates App Store guidelines +5. **No category names** - Already indexed via category selection + +**Example - Efficient keyword field:** +``` +task,todo,checklist,reminder,productivity,organize,schedule,planner,goals,habit +``` + +**Example - Inefficient keyword field (avoid):** +``` +task manager, todo list, productivity app, task tracking +``` + +### App Store Connect Metadata Fields + +| Category | Field | Required | +|----------|-------|----------| +| **App Information** | Name | Yes | +| | Subtitle | No | +| | Category | Yes | +| | Secondary Category | No | +| | Content Rights | Yes | +| | Age Rating | Yes | +| **Version Information** | Description | Yes | +| | Keywords | Yes | +| | Promotional Text | No | +| | What's New | Yes (for updates) | +| | Support URL | Yes | +| | Marketing URL | No | +| **Pricing** | Price Tier | Yes | +| | Availability | Yes | + +### Age Rating Content Descriptors + +| Content Type | None | Infrequent | Frequent | +|--------------|------|------------|----------| +| Cartoon Violence | 4+ | 9+ | 12+ | +| Realistic Violence | 9+ | 12+ | 17+ | +| Sexual Content | 12+ | 17+ | 17+ | +| Profanity | 4+ | 12+ | 17+ | +| Alcohol/Drug Reference | 12+ | 17+ | 17+ | +| Gambling | 12+ | 17+ | 17+ | +| Horror/Fear | 9+ | 12+ | 17+ | + +--- + +## Google Play Store Requirements + +### Metadata Character Limits + +| Field | Character Limit | Notes | +|-------|----------------|-------| +| App Title | 50 characters | Increased from 30 in 2021 | +| Short Description | 80 characters | Visible on store listing | +| Full Description | 4,000 characters | Indexed for search keywords | +| Developer Name | 64 characters | Organization or individual | +| Developer Email | Required | Public support contact | +| Privacy Policy URL | Required | Must be valid HTTPS URL | + +### Description Keyword Strategy + +Google Play has no separate keyword field. Keywords are extracted from: + +1. **App Title** - Highest weight, most important +2. **Short Description** - High weight, visible in search +3. **Full Description** - Medium weight, use naturally throughout +4. **Developer Name** - Low weight but indexed + +**Keyword Density Guidelines:** +- Primary keyword: 2-3% density in full description +- Secondary keywords: 1-2% each +- Avoid keyword stuffing (>5% triggers spam detection) + +### Google Play Console Metadata + +| Category | Field | Required | +|----------|-------|----------| +| **Store Listing** | Title | Yes | +| | Short Description | Yes | +| | Full Description | Yes | +| | App Icon | Yes | +| | Feature Graphic | Yes | +| | Screenshots | Yes (min 2) | +| | Video | No | +| **Store Settings** | App Category | Yes | +| | Tags | No | +| | Contact Email | Yes | +| | Privacy Policy | Yes | +| **Content Rating** | IARC Questionnaire | Yes | + +### Content Rating (IARC) + +| Rating | Age | Description | +|--------|-----|-------------| +| PEGI 3 / Everyone | 3+ | Suitable for all ages | +| PEGI 7 / Everyone 10+ | 7+ | Mild violence, comic mischief | +| PEGI 12 / Teen | 12+ | Moderate violence, mild language | +| PEGI 16 / Mature 17+ | 16+ | Intense violence, strong language | +| PEGI 18 / Adults Only | 18+ | Extreme content | + +--- + +## Visual Asset Specifications + +### App Icon Requirements + +**Apple App Store:** + +| Device | Size | Format | +|--------|------|--------| +| iPhone | 1024x1024 px | PNG, no alpha | +| iPad | 1024x1024 px | PNG, no alpha | +| App Store | 1024x1024 px | PNG, no alpha | +| Spotlight | 120x120 px | PNG | +| Settings | 87x87 px | PNG | + +**Google Play Store:** + +| Asset | Size | Format | +|-------|------|--------| +| App Icon | 512x512 px | PNG, 32-bit | +| Feature Graphic | 1024x500 px | PNG or JPG | +| Promo Graphic | 180x120 px | PNG or JPG | +| TV Banner | 1280x720 px | PNG or JPG | + +### Screenshot Requirements + +**Apple App Store:** + +| Device | Portrait | Landscape | +|--------|----------|-----------| +| iPhone 6.9" | 1320x2868 px | 2868x1320 px | +| iPhone 6.5" | 1290x2796 px | 2796x1290 px | +| iPhone 5.5" | 1242x2208 px | 2208x1242 px | +| iPad Pro 12.9" | 2048x2732 px | 2732x2048 px | +| iPad 10.5" | 1668x2224 px | 2224x1668 px | + +- Minimum: 2 screenshots per device +- Maximum: 10 screenshots per device +- Format: PNG or JPG, no alpha channel +- First 3 screenshots are critical (most users don't scroll) + +**Google Play Store:** + +| Device | Dimensions | Notes | +|--------|------------|-------| +| Phone | 320-3840 px | Min 2:1 aspect ratio | +| 7" Tablet | 320-3840 px | Min 2:1 aspect ratio | +| 10" Tablet | 320-3840 px | Min 2:1 aspect ratio | +| Chromebook | 320-3840 px | Optional | +| TV | 320-3840 px | For TV apps only | + +- Minimum: 2 screenshots +- Maximum: 8 screenshots +- Format: PNG or JPG +- No transparency or borders + +### App Preview Video + +**Apple App Store:** +- Duration: 15-30 seconds +- Resolution: Match device screenshot size +- Format: M4V, MP4, MOV +- Frame rate: 30 fps +- Audio: Optional but recommended + +**Google Play Store:** +- YouTube video link only +- No duration limit (recommend under 2 minutes) +- Landscape orientation preferred +- Must not contain age-restricted content + +--- + +## Localization Requirements + +### Priority Markets by Revenue + +| Rank | Market | Language Code | +|------|--------|---------------| +| 1 | United States | en-US | +| 2 | Japan | ja | +| 3 | United Kingdom | en-GB | +| 4 | Germany | de-DE | +| 5 | China | zh-Hans (iOS), zh-CN (Android) | +| 6 | South Korea | ko | +| 7 | France | fr-FR | +| 8 | Canada | en-CA, fr-CA | +| 9 | Australia | en-AU | +| 10 | Russia | ru | + +### Apple App Store Localization + +Supported localizations: 40+ languages + +| Language | Locale Code | +|----------|-------------| +| English (US) | en-US | +| English (UK) | en-GB | +| Spanish | es-ES | +| Spanish (Mexico) | es-MX | +| French | fr-FR | +| German | de-DE | +| Japanese | ja | +| Korean | ko | +| Simplified Chinese | zh-Hans | +| Traditional Chinese | zh-Hant | + +### Google Play Store Localization + +Supported localizations: 75+ languages + +Each locale requires: +- Title (50 chars) +- Short description (80 chars) +- Full description (4,000 chars) +- Screenshots (can reuse or localize) + +--- + +## Compliance Guidelines + +### Apple App Store Review Guidelines Summary + +| Category | Key Requirements | +|----------|------------------| +| **Safety** | No objectionable content, privacy protection | +| **Performance** | App must work as described, no crashes | +| **Business** | Accurate app description, clear pricing | +| **Design** | Follow Human Interface Guidelines | +| **Legal** | Comply with local laws, proper licensing | + +**Common Rejection Reasons:** +1. Bugs and crashes (50%+ of rejections) +2. Broken links or placeholder content +3. Misleading app descriptions +4. Privacy policy missing or incomplete +5. In-app purchase issues + +### Google Play Developer Policies + +| Policy Area | Requirements | +|-------------|--------------| +| **Restricted Content** | No hate speech, violence, gambling (without license) | +| **Privacy** | Data collection disclosure, privacy policy | +| **Monetization** | Clear pricing, compliant IAPs | +| **Ads** | No deceptive ads, proper disclosure | +| **Store Listing** | Accurate description, no keyword stuffing | + +**Common Suspension Reasons:** +1. Policy violation (content, ads, permissions) +2. Repetitive content (clone apps) +3. Impersonation (fake apps) +4. Intellectual property infringement +5. Malicious behavior + +### Privacy Requirements + +**Apple (App Tracking Transparency):** +- ATT prompt required for tracking +- Privacy nutrition labels mandatory +- Data collection disclosure required + +**Google (Data Safety):** +- Data safety section mandatory +- Data collection and sharing disclosure +- Security practices declaration + +--- + +## Quick Reference Card + +### Apple vs Google Comparison + +| Attribute | Apple App Store | Google Play Store | +|-----------|-----------------|-------------------| +| Title Length | 30 chars | 50 chars | +| Subtitle | 30 chars | N/A | +| Short Description | N/A | 80 chars | +| Full Description | 4,000 chars | 4,000 chars | +| Keywords Field | 100 chars | N/A (in description) | +| Promotional Text | 170 chars | N/A | +| Icon Size | 1024x1024 px | 512x512 px | +| Min Screenshots | 2 | 2 | +| Max Screenshots | 10 | 8 | +| Review Time | 24-48 hours | 1-7 days | +| Metadata Update | Requires review | 1-2 hours to index | diff --git a/.agents/skills/app-store-optimization/sample_input.json b/.agents/skills/app-store-optimization/sample_input.json new file mode 100644 index 0000000..5435a36 --- /dev/null +++ b/.agents/skills/app-store-optimization/sample_input.json @@ -0,0 +1,30 @@ +{ + "request_type": "keyword_research", + "app_info": { + "name": "TaskFlow Pro", + "category": "Productivity", + "target_audience": "Professionals aged 25-45 working in teams", + "key_features": [ + "AI-powered task prioritization", + "Team collaboration tools", + "Calendar integration", + "Cross-platform sync" + ], + "unique_value": "AI automatically prioritizes your tasks based on deadlines and importance" + }, + "target_keywords": [ + "task manager", + "productivity app", + "todo list", + "team collaboration", + "project management" + ], + "competitors": [ + "Todoist", + "Any.do", + "Microsoft To Do", + "Things 3" + ], + "platform": "both", + "language": "en-US" +} diff --git a/.agents/skills/app-store-optimization/scripts/ab_test_planner.py b/.agents/skills/app-store-optimization/scripts/ab_test_planner.py new file mode 100644 index 0000000..06a8016 --- /dev/null +++ b/.agents/skills/app-store-optimization/scripts/ab_test_planner.py @@ -0,0 +1,662 @@ +""" +A/B testing module for App Store Optimization. +Plans and tracks A/B tests for metadata and visual assets. +""" + +from typing import Dict, List, Any, Optional +import math + + +class ABTestPlanner: + """Plans and tracks A/B tests for ASO elements.""" + + # Minimum detectable effect sizes (conservative estimates) + MIN_EFFECT_SIZES = { + 'icon': 0.10, # 10% conversion improvement + 'screenshot': 0.08, # 8% conversion improvement + 'title': 0.05, # 5% conversion improvement + 'description': 0.03 # 3% conversion improvement + } + + # Statistical confidence levels + CONFIDENCE_LEVELS = { + 'high': 0.95, # 95% confidence + 'standard': 0.90, # 90% confidence + 'exploratory': 0.80 # 80% confidence + } + + def __init__(self): + """Initialize A/B test planner.""" + self.active_tests = [] + + def design_test( + self, + test_type: str, + variant_a: Dict[str, Any], + variant_b: Dict[str, Any], + hypothesis: str, + success_metric: str = 'conversion_rate' + ) -> Dict[str, Any]: + """ + Design an A/B test with hypothesis and variables. + + Args: + test_type: Type of test ('icon', 'screenshot', 'title', 'description') + variant_a: Control variant details + variant_b: Test variant details + hypothesis: Expected outcome hypothesis + success_metric: Metric to optimize + + Returns: + Test design with configuration + """ + test_design = { + 'test_id': self._generate_test_id(test_type), + 'test_type': test_type, + 'hypothesis': hypothesis, + 'variants': { + 'a': { + 'name': 'Control', + 'details': variant_a, + 'traffic_split': 0.5 + }, + 'b': { + 'name': 'Variation', + 'details': variant_b, + 'traffic_split': 0.5 + } + }, + 'success_metric': success_metric, + 'secondary_metrics': self._get_secondary_metrics(test_type), + 'minimum_effect_size': self.MIN_EFFECT_SIZES.get(test_type, 0.05), + 'recommended_confidence': 'standard', + 'best_practices': self._get_test_best_practices(test_type) + } + + self.active_tests.append(test_design) + return test_design + + def calculate_sample_size( + self, + baseline_conversion: float, + minimum_detectable_effect: float, + confidence_level: str = 'standard', + power: float = 0.80 + ) -> Dict[str, Any]: + """ + Calculate required sample size for statistical significance. + + Args: + baseline_conversion: Current conversion rate (0-1) + minimum_detectable_effect: Minimum effect size to detect (0-1) + confidence_level: 'high', 'standard', or 'exploratory' + power: Statistical power (typically 0.80 or 0.90) + + Returns: + Sample size calculation with duration estimates + """ + alpha = 1 - self.CONFIDENCE_LEVELS[confidence_level] + beta = 1 - power + + # Expected conversion for variant B + expected_conversion_b = baseline_conversion * (1 + minimum_detectable_effect) + + # Z-scores for alpha and beta + z_alpha = self._get_z_score(1 - alpha / 2) # Two-tailed test + z_beta = self._get_z_score(power) + + # Pooled standard deviation + p_pooled = (baseline_conversion + expected_conversion_b) / 2 + sd_pooled = math.sqrt(2 * p_pooled * (1 - p_pooled)) + + # Sample size per variant + n_per_variant = math.ceil( + ((z_alpha + z_beta) ** 2 * sd_pooled ** 2) / + ((expected_conversion_b - baseline_conversion) ** 2) + ) + + total_sample_size = n_per_variant * 2 + + # Estimate duration based on typical traffic + duration_estimates = self._estimate_test_duration( + total_sample_size, + baseline_conversion + ) + + return { + 'sample_size_per_variant': n_per_variant, + 'total_sample_size': total_sample_size, + 'baseline_conversion': baseline_conversion, + 'expected_conversion_improvement': minimum_detectable_effect, + 'expected_conversion_b': expected_conversion_b, + 'confidence_level': confidence_level, + 'statistical_power': power, + 'duration_estimates': duration_estimates, + 'recommendations': self._generate_sample_size_recommendations( + n_per_variant, + duration_estimates + ) + } + + def calculate_significance( + self, + variant_a_conversions: int, + variant_a_visitors: int, + variant_b_conversions: int, + variant_b_visitors: int + ) -> Dict[str, Any]: + """ + Calculate statistical significance of test results. + + Args: + variant_a_conversions: Conversions for control + variant_a_visitors: Visitors for control + variant_b_conversions: Conversions for variation + variant_b_visitors: Visitors for variation + + Returns: + Significance analysis with decision recommendation + """ + # Calculate conversion rates + rate_a = variant_a_conversions / variant_a_visitors if variant_a_visitors > 0 else 0 + rate_b = variant_b_conversions / variant_b_visitors if variant_b_visitors > 0 else 0 + + # Calculate improvement + if rate_a > 0: + relative_improvement = (rate_b - rate_a) / rate_a + else: + relative_improvement = 0 + + absolute_improvement = rate_b - rate_a + + # Calculate standard error + se_a = math.sqrt(rate_a * (1 - rate_a) / variant_a_visitors) if variant_a_visitors > 0 else 0 + se_b = math.sqrt(rate_b * (1 - rate_b) / variant_b_visitors) if variant_b_visitors > 0 else 0 + se_diff = math.sqrt(se_a**2 + se_b**2) + + # Calculate z-score + z_score = absolute_improvement / se_diff if se_diff > 0 else 0 + + # Calculate p-value (two-tailed) + p_value = 2 * (1 - self._standard_normal_cdf(abs(z_score))) + + # Determine significance + is_significant_95 = p_value < 0.05 + is_significant_90 = p_value < 0.10 + + # Generate decision + decision = self._generate_test_decision( + relative_improvement, + is_significant_95, + is_significant_90, + variant_a_visitors + variant_b_visitors + ) + + return { + 'variant_a': { + 'conversions': variant_a_conversions, + 'visitors': variant_a_visitors, + 'conversion_rate': round(rate_a, 4) + }, + 'variant_b': { + 'conversions': variant_b_conversions, + 'visitors': variant_b_visitors, + 'conversion_rate': round(rate_b, 4) + }, + 'improvement': { + 'absolute': round(absolute_improvement, 4), + 'relative_percentage': round(relative_improvement * 100, 2) + }, + 'statistical_analysis': { + 'z_score': round(z_score, 3), + 'p_value': round(p_value, 4), + 'is_significant_95': is_significant_95, + 'is_significant_90': is_significant_90, + 'confidence_level': '95%' if is_significant_95 else ('90%' if is_significant_90 else 'Not significant') + }, + 'decision': decision + } + + def track_test_results( + self, + test_id: str, + results_data: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Track ongoing test results and provide recommendations. + + Args: + test_id: Test identifier + results_data: Current test results + + Returns: + Test tracking report with next steps + """ + # Find test + test = next((t for t in self.active_tests if t['test_id'] == test_id), None) + if not test: + return {'error': f'Test {test_id} not found'} + + # Calculate significance + significance = self.calculate_significance( + results_data['variant_a_conversions'], + results_data['variant_a_visitors'], + results_data['variant_b_conversions'], + results_data['variant_b_visitors'] + ) + + # Calculate test progress + total_visitors = results_data['variant_a_visitors'] + results_data['variant_b_visitors'] + required_sample = results_data.get('required_sample_size', 10000) + progress_percentage = min((total_visitors / required_sample) * 100, 100) + + # Generate recommendations + recommendations = self._generate_tracking_recommendations( + significance, + progress_percentage, + test['test_type'] + ) + + return { + 'test_id': test_id, + 'test_type': test['test_type'], + 'progress': { + 'total_visitors': total_visitors, + 'required_sample_size': required_sample, + 'progress_percentage': round(progress_percentage, 1), + 'is_complete': progress_percentage >= 100 + }, + 'current_results': significance, + 'recommendations': recommendations, + 'next_steps': self._determine_next_steps( + significance, + progress_percentage + ) + } + + def generate_test_report( + self, + test_id: str, + final_results: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Generate final test report with insights and recommendations. + + Args: + test_id: Test identifier + final_results: Final test results + + Returns: + Comprehensive test report + """ + test = next((t for t in self.active_tests if t['test_id'] == test_id), None) + if not test: + return {'error': f'Test {test_id} not found'} + + significance = self.calculate_significance( + final_results['variant_a_conversions'], + final_results['variant_a_visitors'], + final_results['variant_b_conversions'], + final_results['variant_b_visitors'] + ) + + # Generate insights + insights = self._generate_test_insights( + test, + significance, + final_results + ) + + # Implementation plan + implementation_plan = self._create_implementation_plan( + test, + significance + ) + + return { + 'test_summary': { + 'test_id': test_id, + 'test_type': test['test_type'], + 'hypothesis': test['hypothesis'], + 'duration_days': final_results.get('duration_days', 'N/A') + }, + 'results': significance, + 'insights': insights, + 'implementation_plan': implementation_plan, + 'learnings': self._extract_learnings(test, significance) + } + + def _generate_test_id(self, test_type: str) -> str: + """Generate unique test ID.""" + import time + timestamp = int(time.time()) + return f"{test_type}_{timestamp}" + + def _get_secondary_metrics(self, test_type: str) -> List[str]: + """Get secondary metrics to track for test type.""" + metrics_map = { + 'icon': ['tap_through_rate', 'impression_count', 'brand_recall'], + 'screenshot': ['tap_through_rate', 'time_on_page', 'scroll_depth'], + 'title': ['impression_count', 'tap_through_rate', 'search_visibility'], + 'description': ['time_on_page', 'scroll_depth', 'tap_through_rate'] + } + return metrics_map.get(test_type, ['tap_through_rate']) + + def _get_test_best_practices(self, test_type: str) -> List[str]: + """Get best practices for specific test type.""" + practices_map = { + 'icon': [ + 'Test only one element at a time (color vs. style vs. symbolism)', + 'Ensure icon is recognizable at small sizes (60x60px)', + 'Consider cultural context for global audience', + 'Test against top competitor icons' + ], + 'screenshot': [ + 'Test order of screenshots (users see first 2-3)', + 'Use captions to tell story', + 'Show key features and benefits', + 'Test with and without device frames' + ], + 'title': [ + 'Test keyword variations, not major rebrand', + 'Keep brand name consistent', + 'Ensure title fits within character limits', + 'Test on both search and browse contexts' + ], + 'description': [ + 'Test structure (bullet points vs. paragraphs)', + 'Test call-to-action placement', + 'Test feature vs. benefit focus', + 'Maintain keyword density' + ] + } + return practices_map.get(test_type, ['Test one variable at a time']) + + def _estimate_test_duration( + self, + required_sample_size: int, + baseline_conversion: float + ) -> Dict[str, Any]: + """Estimate test duration based on typical traffic levels.""" + # Assume different daily traffic scenarios + traffic_scenarios = { + 'low': 100, # 100 page views/day + 'medium': 1000, # 1000 page views/day + 'high': 10000 # 10000 page views/day + } + + estimates = {} + for scenario, daily_views in traffic_scenarios.items(): + days = math.ceil(required_sample_size / daily_views) + estimates[scenario] = { + 'daily_page_views': daily_views, + 'estimated_days': days, + 'estimated_weeks': round(days / 7, 1) + } + + return estimates + + def _generate_sample_size_recommendations( + self, + sample_size: int, + duration_estimates: Dict[str, Any] + ) -> List[str]: + """Generate recommendations based on sample size.""" + recommendations = [] + + if sample_size > 50000: + recommendations.append( + "Large sample size required - consider testing smaller effect size or increasing traffic" + ) + + if duration_estimates['medium']['estimated_days'] > 30: + recommendations.append( + "Long test duration - consider higher minimum detectable effect or focus on high-impact changes" + ) + + if duration_estimates['low']['estimated_days'] > 60: + recommendations.append( + "Insufficient traffic for reliable testing - consider user acquisition or broader targeting" + ) + + if not recommendations: + recommendations.append("Sample size and duration are reasonable for this test") + + return recommendations + + def _get_z_score(self, percentile: float) -> float: + """Get z-score for given percentile (approximation).""" + # Common z-scores + z_scores = { + 0.80: 0.84, + 0.85: 1.04, + 0.90: 1.28, + 0.95: 1.645, + 0.975: 1.96, + 0.99: 2.33 + } + return z_scores.get(percentile, 1.96) + + def _standard_normal_cdf(self, z: float) -> float: + """Approximate standard normal cumulative distribution function.""" + # Using error function approximation + t = 1.0 / (1.0 + 0.2316419 * abs(z)) + d = 0.3989423 * math.exp(-z * z / 2.0) + p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274)))) + + if z > 0: + return 1.0 - p + else: + return p + + def _generate_test_decision( + self, + improvement: float, + is_significant_95: bool, + is_significant_90: bool, + total_visitors: int + ) -> Dict[str, Any]: + """Generate test decision and recommendation.""" + if total_visitors < 1000: + return { + 'decision': 'continue', + 'rationale': 'Insufficient data - continue test to reach minimum sample size', + 'action': 'Keep test running' + } + + if is_significant_95: + if improvement > 0: + return { + 'decision': 'implement_b', + 'rationale': f'Variant B shows {improvement*100:.1f}% improvement with 95% confidence', + 'action': 'Implement Variant B' + } + else: + return { + 'decision': 'keep_a', + 'rationale': 'Variant A performs better with 95% confidence', + 'action': 'Keep current version (A)' + } + + elif is_significant_90: + if improvement > 0: + return { + 'decision': 'implement_b_cautiously', + 'rationale': f'Variant B shows {improvement*100:.1f}% improvement with 90% confidence', + 'action': 'Consider implementing B, monitor closely' + } + else: + return { + 'decision': 'keep_a', + 'rationale': 'Variant A performs better with 90% confidence', + 'action': 'Keep current version (A)' + } + + else: + return { + 'decision': 'inconclusive', + 'rationale': 'No statistically significant difference detected', + 'action': 'Either keep A or test different hypothesis' + } + + def _generate_tracking_recommendations( + self, + significance: Dict[str, Any], + progress: float, + test_type: str + ) -> List[str]: + """Generate recommendations for ongoing test.""" + recommendations = [] + + if progress < 50: + recommendations.append( + f"Test is {progress:.0f}% complete - continue collecting data" + ) + + if progress >= 100: + if significance['statistical_analysis']['is_significant_95']: + recommendations.append( + "Sufficient data collected with significant results - ready to conclude test" + ) + else: + recommendations.append( + "Sample size reached but no significant difference - consider extending test or concluding" + ) + + return recommendations + + def _determine_next_steps( + self, + significance: Dict[str, Any], + progress: float + ) -> str: + """Determine next steps for test.""" + if progress < 100: + return f"Continue test until reaching 100% sample size (currently {progress:.0f}%)" + + decision = significance.get('decision', {}).get('decision', 'inconclusive') + + if decision == 'implement_b': + return "Implement Variant B and monitor metrics for 2 weeks" + elif decision == 'keep_a': + return "Keep Variant A and design new test with different hypothesis" + else: + return "Test inconclusive - either keep A or design new test" + + def _generate_test_insights( + self, + test: Dict[str, Any], + significance: Dict[str, Any], + results: Dict[str, Any] + ) -> List[str]: + """Generate insights from test results.""" + insights = [] + + improvement = significance['improvement']['relative_percentage'] + + if significance['statistical_analysis']['is_significant_95']: + insights.append( + f"Strong evidence: Variant B {'improved' if improvement > 0 else 'decreased'} " + f"conversion by {abs(improvement):.1f}% with 95% confidence" + ) + + insights.append( + f"Tested {test['test_type']} changes: {test['hypothesis']}" + ) + + # Add context-specific insights + if test['test_type'] == 'icon' and improvement > 5: + insights.append( + "Icon change had substantial impact - visual first impression is critical" + ) + + return insights + + def _create_implementation_plan( + self, + test: Dict[str, Any], + significance: Dict[str, Any] + ) -> List[Dict[str, str]]: + """Create implementation plan for winning variant.""" + plan = [] + + if significance.get('decision', {}).get('decision') == 'implement_b': + plan.append({ + 'step': '1. Update store listing', + 'details': f"Replace {test['test_type']} with Variant B across all platforms" + }) + plan.append({ + 'step': '2. Monitor metrics', + 'details': 'Track conversion rate for 2 weeks to confirm sustained improvement' + }) + plan.append({ + 'step': '3. Document learnings', + 'details': 'Record insights for future optimization' + }) + + return plan + + def _extract_learnings( + self, + test: Dict[str, Any], + significance: Dict[str, Any] + ) -> List[str]: + """Extract key learnings from test.""" + learnings = [] + + improvement = significance['improvement']['relative_percentage'] + + learnings.append( + f"Testing {test['test_type']} can yield {abs(improvement):.1f}% conversion change" + ) + + if test['test_type'] == 'title': + learnings.append( + "Title changes affect search visibility and user perception" + ) + elif test['test_type'] == 'screenshot': + learnings.append( + "First 2-3 screenshots are critical for conversion" + ) + + return learnings + + +def plan_ab_test( + test_type: str, + variant_a: Dict[str, Any], + variant_b: Dict[str, Any], + hypothesis: str, + baseline_conversion: float +) -> Dict[str, Any]: + """ + Convenience function to plan an A/B test. + + Args: + test_type: Type of test + variant_a: Control variant + variant_b: Test variant + hypothesis: Test hypothesis + baseline_conversion: Current conversion rate + + Returns: + Complete test plan + """ + planner = ABTestPlanner() + + test_design = planner.design_test( + test_type, + variant_a, + variant_b, + hypothesis + ) + + sample_size = planner.calculate_sample_size( + baseline_conversion, + planner.MIN_EFFECT_SIZES.get(test_type, 0.05) + ) + + return { + 'test_design': test_design, + 'sample_size_requirements': sample_size + } diff --git a/.agents/skills/app-store-optimization/scripts/aso_scorer.py b/.agents/skills/app-store-optimization/scripts/aso_scorer.py new file mode 100644 index 0000000..ba4ea6a --- /dev/null +++ b/.agents/skills/app-store-optimization/scripts/aso_scorer.py @@ -0,0 +1,482 @@ +""" +ASO scoring module for App Store Optimization. +Calculates comprehensive ASO health score across multiple dimensions. +""" + +from typing import Dict, List, Any, Optional + + +class ASOScorer: + """Calculates overall ASO health score and provides recommendations.""" + + # Score weights for different components (total = 100) + WEIGHTS = { + 'metadata_quality': 25, + 'ratings_reviews': 25, + 'keyword_performance': 25, + 'conversion_metrics': 25 + } + + # Benchmarks for scoring + BENCHMARKS = { + 'title_keyword_usage': {'min': 1, 'target': 2}, + 'description_length': {'min': 500, 'target': 2000}, + 'keyword_density': {'min': 2, 'optimal': 5, 'max': 8}, + 'average_rating': {'min': 3.5, 'target': 4.5}, + 'ratings_count': {'min': 100, 'target': 5000}, + 'keywords_top_10': {'min': 2, 'target': 10}, + 'keywords_top_50': {'min': 5, 'target': 20}, + 'conversion_rate': {'min': 0.02, 'target': 0.10} + } + + def __init__(self): + """Initialize ASO scorer.""" + self.score_breakdown = {} + + def calculate_overall_score( + self, + metadata: Dict[str, Any], + ratings: Dict[str, Any], + keyword_performance: Dict[str, Any], + conversion: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Calculate comprehensive ASO score (0-100). + + Args: + metadata: Title, description quality metrics + ratings: Rating average and count + keyword_performance: Keyword ranking data + conversion: Impression-to-install metrics + + Returns: + Overall score with detailed breakdown + """ + # Calculate component scores + metadata_score = self.score_metadata_quality(metadata) + ratings_score = self.score_ratings_reviews(ratings) + keyword_score = self.score_keyword_performance(keyword_performance) + conversion_score = self.score_conversion_metrics(conversion) + + # Calculate weighted overall score + overall_score = ( + metadata_score * (self.WEIGHTS['metadata_quality'] / 100) + + ratings_score * (self.WEIGHTS['ratings_reviews'] / 100) + + keyword_score * (self.WEIGHTS['keyword_performance'] / 100) + + conversion_score * (self.WEIGHTS['conversion_metrics'] / 100) + ) + + # Store breakdown + self.score_breakdown = { + 'metadata_quality': { + 'score': metadata_score, + 'weight': self.WEIGHTS['metadata_quality'], + 'weighted_contribution': round(metadata_score * (self.WEIGHTS['metadata_quality'] / 100), 1) + }, + 'ratings_reviews': { + 'score': ratings_score, + 'weight': self.WEIGHTS['ratings_reviews'], + 'weighted_contribution': round(ratings_score * (self.WEIGHTS['ratings_reviews'] / 100), 1) + }, + 'keyword_performance': { + 'score': keyword_score, + 'weight': self.WEIGHTS['keyword_performance'], + 'weighted_contribution': round(keyword_score * (self.WEIGHTS['keyword_performance'] / 100), 1) + }, + 'conversion_metrics': { + 'score': conversion_score, + 'weight': self.WEIGHTS['conversion_metrics'], + 'weighted_contribution': round(conversion_score * (self.WEIGHTS['conversion_metrics'] / 100), 1) + } + } + + # Generate recommendations + recommendations = self.generate_recommendations( + metadata_score, + ratings_score, + keyword_score, + conversion_score + ) + + # Assess overall health + health_status = self._assess_health_status(overall_score) + + return { + 'overall_score': round(overall_score, 1), + 'health_status': health_status, + 'score_breakdown': self.score_breakdown, + 'recommendations': recommendations, + 'priority_actions': self._prioritize_actions(recommendations), + 'strengths': self._identify_strengths(self.score_breakdown), + 'weaknesses': self._identify_weaknesses(self.score_breakdown) + } + + def score_metadata_quality(self, metadata: Dict[str, Any]) -> float: + """ + Score metadata quality (0-100). + + Evaluates: + - Title optimization + - Description quality + - Keyword usage + """ + scores = [] + + # Title score (0-35 points) + title_keywords = metadata.get('title_keyword_count', 0) + title_length = metadata.get('title_length', 0) + + title_score = 0 + if title_keywords >= self.BENCHMARKS['title_keyword_usage']['target']: + title_score = 35 + elif title_keywords >= self.BENCHMARKS['title_keyword_usage']['min']: + title_score = 25 + else: + title_score = 10 + + # Adjust for title length usage + if title_length > 25: # Using most of available space + title_score += 0 + else: + title_score -= 5 + + scores.append(min(title_score, 35)) + + # Description score (0-35 points) + desc_length = metadata.get('description_length', 0) + desc_quality = metadata.get('description_quality', 0.0) # 0-1 scale + + desc_score = 0 + if desc_length >= self.BENCHMARKS['description_length']['target']: + desc_score = 25 + elif desc_length >= self.BENCHMARKS['description_length']['min']: + desc_score = 15 + else: + desc_score = 5 + + # Add quality bonus + desc_score += desc_quality * 10 + scores.append(min(desc_score, 35)) + + # Keyword density score (0-30 points) + keyword_density = metadata.get('keyword_density', 0.0) + + if self.BENCHMARKS['keyword_density']['min'] <= keyword_density <= self.BENCHMARKS['keyword_density']['optimal']: + density_score = 30 + elif keyword_density < self.BENCHMARKS['keyword_density']['min']: + # Too low - proportional scoring + density_score = (keyword_density / self.BENCHMARKS['keyword_density']['min']) * 20 + else: + # Too high (keyword stuffing) - penalty + excess = keyword_density - self.BENCHMARKS['keyword_density']['optimal'] + density_score = max(30 - (excess * 5), 0) + + scores.append(density_score) + + return round(sum(scores), 1) + + def score_ratings_reviews(self, ratings: Dict[str, Any]) -> float: + """ + Score ratings and reviews (0-100). + + Evaluates: + - Average rating + - Total ratings count + - Review velocity + """ + average_rating = ratings.get('average_rating', 0.0) + total_ratings = ratings.get('total_ratings', 0) + recent_ratings = ratings.get('recent_ratings_30d', 0) + + # Rating quality score (0-50 points) + if average_rating >= self.BENCHMARKS['average_rating']['target']: + rating_quality_score = 50 + elif average_rating >= self.BENCHMARKS['average_rating']['min']: + # Proportional scoring between min and target + proportion = (average_rating - self.BENCHMARKS['average_rating']['min']) / \ + (self.BENCHMARKS['average_rating']['target'] - self.BENCHMARKS['average_rating']['min']) + rating_quality_score = 30 + (proportion * 20) + elif average_rating >= 3.0: + rating_quality_score = 20 + else: + rating_quality_score = 10 + + # Rating volume score (0-30 points) + if total_ratings >= self.BENCHMARKS['ratings_count']['target']: + rating_volume_score = 30 + elif total_ratings >= self.BENCHMARKS['ratings_count']['min']: + # Proportional scoring + proportion = (total_ratings - self.BENCHMARKS['ratings_count']['min']) / \ + (self.BENCHMARKS['ratings_count']['target'] - self.BENCHMARKS['ratings_count']['min']) + rating_volume_score = 15 + (proportion * 15) + else: + # Very low volume + rating_volume_score = (total_ratings / self.BENCHMARKS['ratings_count']['min']) * 15 + + # Rating velocity score (0-20 points) + if recent_ratings > 100: + velocity_score = 20 + elif recent_ratings > 50: + velocity_score = 15 + elif recent_ratings > 10: + velocity_score = 10 + else: + velocity_score = 5 + + total_score = rating_quality_score + rating_volume_score + velocity_score + + return round(min(total_score, 100), 1) + + def score_keyword_performance(self, keyword_performance: Dict[str, Any]) -> float: + """ + Score keyword ranking performance (0-100). + + Evaluates: + - Top 10 rankings + - Top 50 rankings + - Ranking trends + """ + top_10_count = keyword_performance.get('top_10', 0) + top_50_count = keyword_performance.get('top_50', 0) + top_100_count = keyword_performance.get('top_100', 0) + improving_keywords = keyword_performance.get('improving_keywords', 0) + + # Top 10 score (0-50 points) - most valuable rankings + if top_10_count >= self.BENCHMARKS['keywords_top_10']['target']: + top_10_score = 50 + elif top_10_count >= self.BENCHMARKS['keywords_top_10']['min']: + proportion = (top_10_count - self.BENCHMARKS['keywords_top_10']['min']) / \ + (self.BENCHMARKS['keywords_top_10']['target'] - self.BENCHMARKS['keywords_top_10']['min']) + top_10_score = 25 + (proportion * 25) + else: + top_10_score = (top_10_count / self.BENCHMARKS['keywords_top_10']['min']) * 25 + + # Top 50 score (0-30 points) + if top_50_count >= self.BENCHMARKS['keywords_top_50']['target']: + top_50_score = 30 + elif top_50_count >= self.BENCHMARKS['keywords_top_50']['min']: + proportion = (top_50_count - self.BENCHMARKS['keywords_top_50']['min']) / \ + (self.BENCHMARKS['keywords_top_50']['target'] - self.BENCHMARKS['keywords_top_50']['min']) + top_50_score = 15 + (proportion * 15) + else: + top_50_score = (top_50_count / self.BENCHMARKS['keywords_top_50']['min']) * 15 + + # Coverage score (0-10 points) - based on top 100 + coverage_score = min((top_100_count / 30) * 10, 10) + + # Trend score (0-10 points) - are rankings improving? + if improving_keywords > 5: + trend_score = 10 + elif improving_keywords > 0: + trend_score = 5 + else: + trend_score = 0 + + total_score = top_10_score + top_50_score + coverage_score + trend_score + + return round(min(total_score, 100), 1) + + def score_conversion_metrics(self, conversion: Dict[str, Any]) -> float: + """ + Score conversion performance (0-100). + + Evaluates: + - Impression-to-install conversion rate + - Download velocity + """ + conversion_rate = conversion.get('impression_to_install', 0.0) + downloads_30d = conversion.get('downloads_last_30_days', 0) + downloads_trend = conversion.get('downloads_trend', 'stable') # 'up', 'stable', 'down' + + # Conversion rate score (0-70 points) + if conversion_rate >= self.BENCHMARKS['conversion_rate']['target']: + conversion_score = 70 + elif conversion_rate >= self.BENCHMARKS['conversion_rate']['min']: + proportion = (conversion_rate - self.BENCHMARKS['conversion_rate']['min']) / \ + (self.BENCHMARKS['conversion_rate']['target'] - self.BENCHMARKS['conversion_rate']['min']) + conversion_score = 35 + (proportion * 35) + else: + conversion_score = (conversion_rate / self.BENCHMARKS['conversion_rate']['min']) * 35 + + # Download velocity score (0-20 points) + if downloads_30d > 10000: + velocity_score = 20 + elif downloads_30d > 1000: + velocity_score = 15 + elif downloads_30d > 100: + velocity_score = 10 + else: + velocity_score = 5 + + # Trend bonus (0-10 points) + if downloads_trend == 'up': + trend_score = 10 + elif downloads_trend == 'stable': + trend_score = 5 + else: + trend_score = 0 + + total_score = conversion_score + velocity_score + trend_score + + return round(min(total_score, 100), 1) + + def generate_recommendations( + self, + metadata_score: float, + ratings_score: float, + keyword_score: float, + conversion_score: float + ) -> List[Dict[str, Any]]: + """Generate prioritized recommendations based on scores.""" + recommendations = [] + + # Metadata recommendations + if metadata_score < 60: + recommendations.append({ + 'category': 'metadata_quality', + 'priority': 'high', + 'action': 'Optimize app title and description', + 'details': 'Add more keywords to title, expand description to 1500-2000 characters, improve keyword density to 3-5%', + 'expected_impact': 'Improve discoverability and ranking potential' + }) + elif metadata_score < 80: + recommendations.append({ + 'category': 'metadata_quality', + 'priority': 'medium', + 'action': 'Refine metadata for better keyword targeting', + 'details': 'Test variations of title/subtitle, optimize keyword field for Apple', + 'expected_impact': 'Incremental ranking improvements' + }) + + # Ratings recommendations + if ratings_score < 60: + recommendations.append({ + 'category': 'ratings_reviews', + 'priority': 'high', + 'action': 'Improve rating quality and volume', + 'details': 'Address top user complaints, implement in-app rating prompts, respond to negative reviews', + 'expected_impact': 'Better conversion rates and trust signals' + }) + elif ratings_score < 80: + recommendations.append({ + 'category': 'ratings_reviews', + 'priority': 'medium', + 'action': 'Increase rating velocity', + 'details': 'Optimize timing of rating requests, encourage satisfied users to rate', + 'expected_impact': 'Sustained rating quality' + }) + + # Keyword performance recommendations + if keyword_score < 60: + recommendations.append({ + 'category': 'keyword_performance', + 'priority': 'high', + 'action': 'Improve keyword rankings', + 'details': 'Target long-tail keywords with lower competition, update metadata with high-potential keywords, build backlinks', + 'expected_impact': 'Significant improvement in organic visibility' + }) + elif keyword_score < 80: + recommendations.append({ + 'category': 'keyword_performance', + 'priority': 'medium', + 'action': 'Expand keyword coverage', + 'details': 'Target additional related keywords, test seasonal keywords, localize for new markets', + 'expected_impact': 'Broader reach and more discovery opportunities' + }) + + # Conversion recommendations + if conversion_score < 60: + recommendations.append({ + 'category': 'conversion_metrics', + 'priority': 'high', + 'action': 'Optimize store listing for conversions', + 'details': 'Improve screenshots and icon, strengthen value proposition in description, add video preview', + 'expected_impact': 'Higher impression-to-install conversion' + }) + elif conversion_score < 80: + recommendations.append({ + 'category': 'conversion_metrics', + 'priority': 'medium', + 'action': 'Test visual asset variations', + 'details': 'A/B test different icon designs and screenshot sequences', + 'expected_impact': 'Incremental conversion improvements' + }) + + return recommendations + + def _assess_health_status(self, overall_score: float) -> str: + """Assess overall ASO health status.""" + if overall_score >= 80: + return "Excellent - Top-tier ASO performance" + elif overall_score >= 65: + return "Good - Competitive ASO with room for improvement" + elif overall_score >= 50: + return "Fair - Needs strategic improvements" + else: + return "Poor - Requires immediate ASO overhaul" + + def _prioritize_actions( + self, + recommendations: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Prioritize actions by impact and urgency.""" + # Sort by priority (high first) and expected impact + priority_order = {'high': 0, 'medium': 1, 'low': 2} + + sorted_recommendations = sorted( + recommendations, + key=lambda x: priority_order[x['priority']] + ) + + return sorted_recommendations[:3] # Top 3 priority actions + + def _identify_strengths(self, score_breakdown: Dict[str, Any]) -> List[str]: + """Identify areas of strength (scores >= 75).""" + strengths = [] + + for category, data in score_breakdown.items(): + if data['score'] >= 75: + strengths.append( + f"{category.replace('_', ' ').title()}: {data['score']}/100" + ) + + return strengths if strengths else ["Focus on building strengths across all areas"] + + def _identify_weaknesses(self, score_breakdown: Dict[str, Any]) -> List[str]: + """Identify areas needing improvement (scores < 60).""" + weaknesses = [] + + for category, data in score_breakdown.items(): + if data['score'] < 60: + weaknesses.append( + f"{category.replace('_', ' ').title()}: {data['score']}/100 - needs improvement" + ) + + return weaknesses if weaknesses else ["All areas performing adequately"] + + +def calculate_aso_score( + metadata: Dict[str, Any], + ratings: Dict[str, Any], + keyword_performance: Dict[str, Any], + conversion: Dict[str, Any] +) -> Dict[str, Any]: + """ + Convenience function to calculate ASO score. + + Args: + metadata: Metadata quality metrics + ratings: Ratings data + keyword_performance: Keyword ranking data + conversion: Conversion metrics + + Returns: + Complete ASO score report + """ + scorer = ASOScorer() + return scorer.calculate_overall_score( + metadata, + ratings, + keyword_performance, + conversion + ) diff --git a/.agents/skills/app-store-optimization/scripts/competitor_analyzer.py b/.agents/skills/app-store-optimization/scripts/competitor_analyzer.py new file mode 100644 index 0000000..9f84575 --- /dev/null +++ b/.agents/skills/app-store-optimization/scripts/competitor_analyzer.py @@ -0,0 +1,577 @@ +""" +Competitor analysis module for App Store Optimization. +Analyzes top competitors' ASO strategies and identifies opportunities. +""" + +from typing import Dict, List, Any, Optional +from collections import Counter +import re + + +class CompetitorAnalyzer: + """Analyzes competitor apps to identify ASO opportunities.""" + + def __init__(self, category: str, platform: str = 'apple'): + """ + Initialize competitor analyzer. + + Args: + category: App category (e.g., "Productivity", "Games") + platform: 'apple' or 'google' + """ + self.category = category + self.platform = platform + self.competitors = [] + + def analyze_competitor( + self, + app_data: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Analyze a single competitor's ASO strategy. + + Args: + app_data: Dictionary with app_name, title, description, rating, ratings_count, keywords + + Returns: + Comprehensive competitor analysis + """ + app_name = app_data.get('app_name', '') + title = app_data.get('title', '') + description = app_data.get('description', '') + rating = app_data.get('rating', 0.0) + ratings_count = app_data.get('ratings_count', 0) + keywords = app_data.get('keywords', []) + + analysis = { + 'app_name': app_name, + 'title_analysis': self._analyze_title(title), + 'description_analysis': self._analyze_description(description), + 'keyword_strategy': self._extract_keyword_strategy(title, description, keywords), + 'rating_metrics': { + 'rating': rating, + 'ratings_count': ratings_count, + 'rating_quality': self._assess_rating_quality(rating, ratings_count) + }, + 'competitive_strength': self._calculate_competitive_strength( + rating, + ratings_count, + len(description) + ), + 'key_differentiators': self._identify_differentiators(description) + } + + self.competitors.append(analysis) + return analysis + + def compare_competitors( + self, + competitors_data: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Compare multiple competitors and identify patterns. + + Args: + competitors_data: List of competitor data dictionaries + + Returns: + Comparative analysis with insights + """ + # Analyze each competitor + analyses = [] + for comp_data in competitors_data: + analysis = self.analyze_competitor(comp_data) + analyses.append(analysis) + + # Extract common keywords across competitors + all_keywords = [] + for analysis in analyses: + all_keywords.extend(analysis['keyword_strategy']['primary_keywords']) + + common_keywords = self._find_common_keywords(all_keywords) + + # Identify keyword gaps (used by some but not all) + keyword_gaps = self._identify_keyword_gaps(analyses) + + # Rank competitors by strength + ranked_competitors = sorted( + analyses, + key=lambda x: x['competitive_strength'], + reverse=True + ) + + # Analyze rating distribution + rating_analysis = self._analyze_rating_distribution(analyses) + + # Identify best practices + best_practices = self._identify_best_practices(ranked_competitors) + + return { + 'category': self.category, + 'platform': self.platform, + 'competitors_analyzed': len(analyses), + 'ranked_competitors': ranked_competitors, + 'common_keywords': common_keywords, + 'keyword_gaps': keyword_gaps, + 'rating_analysis': rating_analysis, + 'best_practices': best_practices, + 'opportunities': self._identify_opportunities( + analyses, + common_keywords, + keyword_gaps + ) + } + + def identify_gaps( + self, + your_app_data: Dict[str, Any], + competitors_data: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Identify gaps between your app and competitors. + + Args: + your_app_data: Your app's data + competitors_data: List of competitor data + + Returns: + Gap analysis with actionable recommendations + """ + # Analyze your app + your_analysis = self.analyze_competitor(your_app_data) + + # Analyze competitors + competitor_comparison = self.compare_competitors(competitors_data) + + # Identify keyword gaps + your_keywords = set(your_analysis['keyword_strategy']['primary_keywords']) + competitor_keywords = set(competitor_comparison['common_keywords']) + missing_keywords = competitor_keywords - your_keywords + + # Identify rating gap + avg_competitor_rating = competitor_comparison['rating_analysis']['average_rating'] + rating_gap = avg_competitor_rating - your_analysis['rating_metrics']['rating'] + + # Identify description length gap + avg_competitor_desc_length = sum( + len(comp['description_analysis']['text']) + for comp in competitor_comparison['ranked_competitors'] + ) / len(competitor_comparison['ranked_competitors']) + your_desc_length = len(your_analysis['description_analysis']['text']) + desc_length_gap = avg_competitor_desc_length - your_desc_length + + return { + 'your_app': your_analysis, + 'keyword_gaps': { + 'missing_keywords': list(missing_keywords)[:10], + 'recommendations': self._generate_keyword_recommendations(missing_keywords) + }, + 'rating_gap': { + 'your_rating': your_analysis['rating_metrics']['rating'], + 'average_competitor_rating': avg_competitor_rating, + 'gap': round(rating_gap, 2), + 'action_items': self._generate_rating_improvement_actions(rating_gap) + }, + 'content_gap': { + 'your_description_length': your_desc_length, + 'average_competitor_length': int(avg_competitor_desc_length), + 'gap': int(desc_length_gap), + 'recommendations': self._generate_content_recommendations(desc_length_gap) + }, + 'competitive_positioning': self._assess_competitive_position( + your_analysis, + competitor_comparison + ) + } + + def _analyze_title(self, title: str) -> Dict[str, Any]: + """Analyze title structure and keyword usage.""" + parts = re.split(r'[-:|]', title) + + return { + 'title': title, + 'length': len(title), + 'has_brand': len(parts) > 0, + 'has_keywords': len(parts) > 1, + 'components': [part.strip() for part in parts], + 'word_count': len(title.split()), + 'strategy': 'brand_plus_keywords' if len(parts) > 1 else 'brand_only' + } + + def _analyze_description(self, description: str) -> Dict[str, Any]: + """Analyze description structure and content.""" + lines = description.split('\n') + word_count = len(description.split()) + + # Check for structural elements + has_bullet_points = '•' in description or '*' in description + has_sections = any(line.isupper() for line in lines if len(line) > 0) + has_call_to_action = any( + cta in description.lower() + for cta in ['download', 'try', 'get', 'start', 'join'] + ) + + # Extract features mentioned + features = self._extract_features(description) + + return { + 'text': description, + 'length': len(description), + 'word_count': word_count, + 'structure': { + 'has_bullet_points': has_bullet_points, + 'has_sections': has_sections, + 'has_call_to_action': has_call_to_action + }, + 'features_mentioned': features, + 'readability': 'good' if 50 <= word_count <= 300 else 'needs_improvement' + } + + def _extract_keyword_strategy( + self, + title: str, + description: str, + explicit_keywords: List[str] + ) -> Dict[str, Any]: + """Extract keyword strategy from metadata.""" + # Extract keywords from title + title_keywords = [word.lower() for word in title.split() if len(word) > 3] + + # Extract frequently used words from description + desc_words = re.findall(r'\b\w{4,}\b', description.lower()) + word_freq = Counter(desc_words) + frequent_words = [word for word, count in word_freq.most_common(15) if count > 2] + + # Combine with explicit keywords + all_keywords = list(set(title_keywords + frequent_words + explicit_keywords)) + + return { + 'primary_keywords': title_keywords, + 'description_keywords': frequent_words[:10], + 'explicit_keywords': explicit_keywords, + 'total_unique_keywords': len(all_keywords), + 'keyword_focus': self._assess_keyword_focus(title_keywords, frequent_words) + } + + def _assess_rating_quality(self, rating: float, ratings_count: int) -> str: + """Assess the quality of ratings.""" + if ratings_count < 100: + return 'insufficient_data' + elif rating >= 4.5 and ratings_count > 1000: + return 'excellent' + elif rating >= 4.0 and ratings_count > 500: + return 'good' + elif rating >= 3.5: + return 'average' + else: + return 'poor' + + def _calculate_competitive_strength( + self, + rating: float, + ratings_count: int, + description_length: int + ) -> float: + """ + Calculate overall competitive strength (0-100). + + Factors: + - Rating quality (40%) + - Rating volume (30%) + - Metadata quality (30%) + """ + # Rating quality score (0-40) + rating_score = (rating / 5.0) * 40 + + # Rating volume score (0-30) + volume_score = min((ratings_count / 10000) * 30, 30) + + # Metadata quality score (0-30) + metadata_score = min((description_length / 2000) * 30, 30) + + total_score = rating_score + volume_score + metadata_score + + return round(total_score, 1) + + def _identify_differentiators(self, description: str) -> List[str]: + """Identify key differentiators from description.""" + differentiator_keywords = [ + 'unique', 'only', 'first', 'best', 'leading', 'exclusive', + 'revolutionary', 'innovative', 'patent', 'award' + ] + + differentiators = [] + sentences = description.split('.') + + for sentence in sentences: + sentence_lower = sentence.lower() + if any(keyword in sentence_lower for keyword in differentiator_keywords): + differentiators.append(sentence.strip()) + + return differentiators[:5] + + def _find_common_keywords(self, all_keywords: List[str]) -> List[str]: + """Find keywords used by multiple competitors.""" + keyword_counts = Counter(all_keywords) + # Return keywords used by at least 2 competitors + common = [kw for kw, count in keyword_counts.items() if count >= 2] + return sorted(common, key=lambda x: keyword_counts[x], reverse=True)[:20] + + def _identify_keyword_gaps(self, analyses: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Identify keywords used by some competitors but not others.""" + all_keywords_by_app = {} + + for analysis in analyses: + app_name = analysis['app_name'] + keywords = analysis['keyword_strategy']['primary_keywords'] + all_keywords_by_app[app_name] = set(keywords) + + # Find keywords used by some but not all + all_keywords_set = set() + for keywords in all_keywords_by_app.values(): + all_keywords_set.update(keywords) + + gaps = [] + for keyword in all_keywords_set: + using_apps = [ + app for app, keywords in all_keywords_by_app.items() + if keyword in keywords + ] + if 1 < len(using_apps) < len(analyses): + gaps.append({ + 'keyword': keyword, + 'used_by': using_apps, + 'usage_percentage': round(len(using_apps) / len(analyses) * 100, 1) + }) + + return sorted(gaps, key=lambda x: x['usage_percentage'], reverse=True)[:15] + + def _analyze_rating_distribution(self, analyses: List[Dict[str, Any]]) -> Dict[str, Any]: + """Analyze rating distribution across competitors.""" + ratings = [a['rating_metrics']['rating'] for a in analyses] + ratings_counts = [a['rating_metrics']['ratings_count'] for a in analyses] + + return { + 'average_rating': round(sum(ratings) / len(ratings), 2), + 'highest_rating': max(ratings), + 'lowest_rating': min(ratings), + 'average_ratings_count': int(sum(ratings_counts) / len(ratings_counts)), + 'total_ratings_in_category': sum(ratings_counts) + } + + def _identify_best_practices(self, ranked_competitors: List[Dict[str, Any]]) -> List[str]: + """Identify best practices from top competitors.""" + if not ranked_competitors: + return [] + + top_competitor = ranked_competitors[0] + practices = [] + + # Title strategy + title_analysis = top_competitor['title_analysis'] + if title_analysis['has_keywords']: + practices.append( + f"Title Strategy: Include primary keyword in title (e.g., '{title_analysis['title']}')" + ) + + # Description structure + desc_analysis = top_competitor['description_analysis'] + if desc_analysis['structure']['has_bullet_points']: + practices.append("Description: Use bullet points to highlight key features") + + if desc_analysis['structure']['has_sections']: + practices.append("Description: Organize content with clear section headers") + + # Rating strategy + rating_quality = top_competitor['rating_metrics']['rating_quality'] + if rating_quality in ['excellent', 'good']: + practices.append( + f"Ratings: Maintain high rating quality ({top_competitor['rating_metrics']['rating']}★) " + f"with significant volume ({top_competitor['rating_metrics']['ratings_count']} ratings)" + ) + + return practices[:5] + + def _identify_opportunities( + self, + analyses: List[Dict[str, Any]], + common_keywords: List[str], + keyword_gaps: List[Dict[str, Any]] + ) -> List[str]: + """Identify ASO opportunities based on competitive analysis.""" + opportunities = [] + + # Keyword opportunities from gaps + if keyword_gaps: + underutilized_keywords = [ + gap['keyword'] for gap in keyword_gaps + if gap['usage_percentage'] < 50 + ] + if underutilized_keywords: + opportunities.append( + f"Target underutilized keywords: {', '.join(underutilized_keywords[:5])}" + ) + + # Rating opportunity + avg_rating = sum(a['rating_metrics']['rating'] for a in analyses) / len(analyses) + if avg_rating < 4.5: + opportunities.append( + f"Category average rating is {avg_rating:.1f} - opportunity to differentiate with higher ratings" + ) + + # Content depth opportunity + avg_desc_length = sum( + a['description_analysis']['length'] for a in analyses + ) / len(analyses) + if avg_desc_length < 1500: + opportunities.append( + "Competitors have relatively short descriptions - opportunity to provide more comprehensive information" + ) + + return opportunities[:5] + + def _extract_features(self, description: str) -> List[str]: + """Extract feature mentions from description.""" + # Look for bullet points or numbered lists + lines = description.split('\n') + features = [] + + for line in lines: + line = line.strip() + # Check if line starts with bullet or number + if line and (line[0] in ['•', '*', '-', '✓'] or line[0].isdigit()): + # Clean the line + cleaned = re.sub(r'^[•*\-✓\d.)\s]+', '', line) + if cleaned: + features.append(cleaned) + + return features[:10] + + def _assess_keyword_focus( + self, + title_keywords: List[str], + description_keywords: List[str] + ) -> str: + """Assess keyword focus strategy.""" + overlap = set(title_keywords) & set(description_keywords) + + if len(overlap) >= 3: + return 'consistent_focus' + elif len(overlap) >= 1: + return 'moderate_focus' + else: + return 'broad_focus' + + def _generate_keyword_recommendations(self, missing_keywords: set) -> List[str]: + """Generate recommendations for missing keywords.""" + if not missing_keywords: + return ["Your keyword coverage is comprehensive"] + + recommendations = [] + missing_list = list(missing_keywords)[:5] + + recommendations.append( + f"Consider adding these competitor keywords: {', '.join(missing_list)}" + ) + recommendations.append( + "Test keyword variations in subtitle/promotional text first" + ) + recommendations.append( + "Monitor competitor keyword changes monthly" + ) + + return recommendations + + def _generate_rating_improvement_actions(self, rating_gap: float) -> List[str]: + """Generate actions to improve ratings.""" + actions = [] + + if rating_gap > 0.5: + actions.append("CRITICAL: Significant rating gap - prioritize user satisfaction improvements") + actions.append("Analyze negative reviews to identify top issues") + actions.append("Implement in-app rating prompts after positive experiences") + actions.append("Respond to all negative reviews professionally") + elif rating_gap > 0.2: + actions.append("Focus on incremental improvements to close rating gap") + actions.append("Optimize timing of rating requests") + else: + actions.append("Ratings are competitive - maintain quality and continue improvements") + + return actions + + def _generate_content_recommendations(self, desc_length_gap: int) -> List[str]: + """Generate content recommendations based on length gap.""" + recommendations = [] + + if desc_length_gap > 500: + recommendations.append( + "Expand description to match competitor detail level" + ) + recommendations.append( + "Add use case examples and success stories" + ) + recommendations.append( + "Include more feature explanations and benefits" + ) + elif desc_length_gap < -500: + recommendations.append( + "Consider condensing description for better readability" + ) + recommendations.append( + "Focus on most important features first" + ) + else: + recommendations.append( + "Description length is competitive" + ) + + return recommendations + + def _assess_competitive_position( + self, + your_analysis: Dict[str, Any], + competitor_comparison: Dict[str, Any] + ) -> str: + """Assess your competitive position.""" + your_strength = your_analysis['competitive_strength'] + competitors = competitor_comparison['ranked_competitors'] + + if not competitors: + return "No comparison data available" + + # Find where you'd rank + better_than_count = sum( + 1 for comp in competitors + if your_strength > comp['competitive_strength'] + ) + + position_percentage = (better_than_count / len(competitors)) * 100 + + if position_percentage >= 75: + return "Strong Position: Top quartile in competitive strength" + elif position_percentage >= 50: + return "Competitive Position: Above average, opportunities for improvement" + elif position_percentage >= 25: + return "Challenging Position: Below average, requires strategic improvements" + else: + return "Weak Position: Bottom quartile, major ASO overhaul needed" + + +def analyze_competitor_set( + category: str, + competitors_data: List[Dict[str, Any]], + platform: str = 'apple' +) -> Dict[str, Any]: + """ + Convenience function to analyze a set of competitors. + + Args: + category: App category + competitors_data: List of competitor data + platform: 'apple' or 'google' + + Returns: + Complete competitive analysis + """ + analyzer = CompetitorAnalyzer(category, platform) + return analyzer.compare_competitors(competitors_data) diff --git a/.agents/skills/app-store-optimization/scripts/keyword_analyzer.py b/.agents/skills/app-store-optimization/scripts/keyword_analyzer.py new file mode 100644 index 0000000..5c3d80b --- /dev/null +++ b/.agents/skills/app-store-optimization/scripts/keyword_analyzer.py @@ -0,0 +1,406 @@ +""" +Keyword analysis module for App Store Optimization. +Analyzes keyword search volume, competition, and relevance for app discovery. +""" + +from typing import Dict, List, Any, Optional, Tuple +import re +from collections import Counter + + +class KeywordAnalyzer: + """Analyzes keywords for ASO effectiveness.""" + + # Competition level thresholds (based on number of competing apps) + COMPETITION_THRESHOLDS = { + 'low': 1000, + 'medium': 5000, + 'high': 10000 + } + + # Search volume categories (monthly searches estimate) + VOLUME_CATEGORIES = { + 'very_low': 1000, + 'low': 5000, + 'medium': 20000, + 'high': 100000, + 'very_high': 500000 + } + + def __init__(self): + """Initialize keyword analyzer.""" + self.analyzed_keywords = {} + + def analyze_keyword( + self, + keyword: str, + search_volume: int = 0, + competing_apps: int = 0, + relevance_score: float = 0.0 + ) -> Dict[str, Any]: + """ + Analyze a single keyword for ASO potential. + + Args: + keyword: The keyword to analyze + search_volume: Estimated monthly search volume + competing_apps: Number of apps competing for this keyword + relevance_score: Relevance to your app (0.0-1.0) + + Returns: + Dictionary with keyword analysis + """ + competition_level = self._calculate_competition_level(competing_apps) + volume_category = self._categorize_search_volume(search_volume) + difficulty_score = self._calculate_keyword_difficulty( + search_volume, + competing_apps + ) + + # Calculate potential score (0-100) + potential_score = self._calculate_potential_score( + search_volume, + competing_apps, + relevance_score + ) + + analysis = { + 'keyword': keyword, + 'search_volume': search_volume, + 'volume_category': volume_category, + 'competing_apps': competing_apps, + 'competition_level': competition_level, + 'relevance_score': relevance_score, + 'difficulty_score': difficulty_score, + 'potential_score': potential_score, + 'recommendation': self._generate_recommendation( + potential_score, + difficulty_score, + relevance_score + ), + 'keyword_length': len(keyword.split()), + 'is_long_tail': len(keyword.split()) >= 3 + } + + self.analyzed_keywords[keyword] = analysis + return analysis + + def compare_keywords(self, keywords_data: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Compare multiple keywords and rank by potential. + + Args: + keywords_data: List of dicts with keyword, search_volume, competing_apps, relevance_score + + Returns: + Comparison report with ranked keywords + """ + analyses = [] + for kw_data in keywords_data: + analysis = self.analyze_keyword( + keyword=kw_data['keyword'], + search_volume=kw_data.get('search_volume', 0), + competing_apps=kw_data.get('competing_apps', 0), + relevance_score=kw_data.get('relevance_score', 0.0) + ) + analyses.append(analysis) + + # Sort by potential score (descending) + ranked_keywords = sorted( + analyses, + key=lambda x: x['potential_score'], + reverse=True + ) + + # Categorize keywords + primary_keywords = [ + kw for kw in ranked_keywords + if kw['potential_score'] >= 70 and kw['relevance_score'] >= 0.8 + ] + + secondary_keywords = [ + kw for kw in ranked_keywords + if 50 <= kw['potential_score'] < 70 and kw['relevance_score'] >= 0.6 + ] + + long_tail_keywords = [ + kw for kw in ranked_keywords + if kw['is_long_tail'] and kw['relevance_score'] >= 0.7 + ] + + return { + 'total_keywords_analyzed': len(analyses), + 'ranked_keywords': ranked_keywords, + 'primary_keywords': primary_keywords[:5], # Top 5 + 'secondary_keywords': secondary_keywords[:10], # Top 10 + 'long_tail_keywords': long_tail_keywords[:10], # Top 10 + 'summary': self._generate_comparison_summary( + primary_keywords, + secondary_keywords, + long_tail_keywords + ) + } + + def find_long_tail_opportunities( + self, + base_keyword: str, + modifiers: List[str] + ) -> List[Dict[str, Any]]: + """ + Generate long-tail keyword variations. + + Args: + base_keyword: Core keyword (e.g., "task manager") + modifiers: List of modifiers (e.g., ["free", "simple", "team"]) + + Returns: + List of long-tail keyword suggestions + """ + long_tail_keywords = [] + + # Generate combinations + for modifier in modifiers: + # Modifier + base + variation1 = f"{modifier} {base_keyword}" + long_tail_keywords.append({ + 'keyword': variation1, + 'pattern': 'modifier_base', + 'estimated_competition': 'low', + 'rationale': f"Less competitive variation of '{base_keyword}'" + }) + + # Base + modifier + variation2 = f"{base_keyword} {modifier}" + long_tail_keywords.append({ + 'keyword': variation2, + 'pattern': 'base_modifier', + 'estimated_competition': 'low', + 'rationale': f"Specific use-case variation of '{base_keyword}'" + }) + + # Add question-based long-tail + question_words = ['how', 'what', 'best', 'top'] + for q_word in question_words: + question_keyword = f"{q_word} {base_keyword}" + long_tail_keywords.append({ + 'keyword': question_keyword, + 'pattern': 'question_based', + 'estimated_competition': 'very_low', + 'rationale': f"Informational search query" + }) + + return long_tail_keywords + + def extract_keywords_from_text( + self, + text: str, + min_word_length: int = 3 + ) -> List[Tuple[str, int]]: + """ + Extract potential keywords from text (descriptions, reviews). + + Args: + text: Text to analyze + min_word_length: Minimum word length to consider + + Returns: + List of (keyword, frequency) tuples + """ + # Clean and normalize text + text = text.lower() + text = re.sub(r'[^\w\s]', ' ', text) + + # Extract words + words = text.split() + + # Filter by length + words = [w for w in words if len(w) >= min_word_length] + + # Remove common stop words + stop_words = { + 'the', 'and', 'for', 'with', 'this', 'that', 'from', 'have', + 'but', 'not', 'you', 'all', 'can', 'are', 'was', 'were', 'been' + } + words = [w for w in words if w not in stop_words] + + # Count frequency + word_counts = Counter(words) + + # Extract 2-word phrases + phrases = [] + for i in range(len(words) - 1): + phrase = f"{words[i]} {words[i+1]}" + phrases.append(phrase) + + phrase_counts = Counter(phrases) + + # Combine and sort + all_keywords = list(word_counts.items()) + list(phrase_counts.items()) + all_keywords.sort(key=lambda x: x[1], reverse=True) + + return all_keywords[:50] # Top 50 + + def calculate_keyword_density( + self, + text: str, + target_keywords: List[str] + ) -> Dict[str, float]: + """ + Calculate keyword density in text. + + Args: + text: Text to analyze (title, description) + target_keywords: Keywords to check density for + + Returns: + Dictionary of keyword: density (percentage) + """ + text_lower = text.lower() + total_words = len(text_lower.split()) + + densities = {} + for keyword in target_keywords: + keyword_lower = keyword.lower() + occurrences = text_lower.count(keyword_lower) + density = (occurrences / total_words) * 100 if total_words > 0 else 0 + densities[keyword] = round(density, 2) + + return densities + + def _calculate_competition_level(self, competing_apps: int) -> str: + """Determine competition level based on number of competing apps.""" + if competing_apps < self.COMPETITION_THRESHOLDS['low']: + return 'low' + elif competing_apps < self.COMPETITION_THRESHOLDS['medium']: + return 'medium' + elif competing_apps < self.COMPETITION_THRESHOLDS['high']: + return 'high' + else: + return 'very_high' + + def _categorize_search_volume(self, search_volume: int) -> str: + """Categorize search volume.""" + if search_volume < self.VOLUME_CATEGORIES['very_low']: + return 'very_low' + elif search_volume < self.VOLUME_CATEGORIES['low']: + return 'low' + elif search_volume < self.VOLUME_CATEGORIES['medium']: + return 'medium' + elif search_volume < self.VOLUME_CATEGORIES['high']: + return 'high' + else: + return 'very_high' + + def _calculate_keyword_difficulty( + self, + search_volume: int, + competing_apps: int + ) -> float: + """ + Calculate keyword difficulty score (0-100). + Higher score = harder to rank. + """ + if competing_apps == 0: + return 0.0 + + # Competition factor (0-1) + competition_factor = min(competing_apps / 50000, 1.0) + + # Volume factor (0-1) - higher volume = more difficulty + volume_factor = min(search_volume / 1000000, 1.0) + + # Difficulty score (weighted average) + difficulty = (competition_factor * 0.7 + volume_factor * 0.3) * 100 + + return round(difficulty, 1) + + def _calculate_potential_score( + self, + search_volume: int, + competing_apps: int, + relevance_score: float + ) -> float: + """ + Calculate overall keyword potential (0-100). + Higher score = better opportunity. + """ + # Volume score (0-40 points) + volume_score = min((search_volume / 100000) * 40, 40) + + # Competition score (0-30 points) - inverse relationship + if competing_apps > 0: + competition_score = max(30 - (competing_apps / 500), 0) + else: + competition_score = 30 + + # Relevance score (0-30 points) + relevance_points = relevance_score * 30 + + total_score = volume_score + competition_score + relevance_points + + return round(min(total_score, 100), 1) + + def _generate_recommendation( + self, + potential_score: float, + difficulty_score: float, + relevance_score: float + ) -> str: + """Generate actionable recommendation for keyword.""" + if relevance_score < 0.5: + return "Low relevance - avoid targeting" + + if potential_score >= 70: + return "High priority - target immediately" + elif potential_score >= 50: + if difficulty_score < 50: + return "Good opportunity - include in metadata" + else: + return "Competitive - use in description, not title" + elif potential_score >= 30: + return "Secondary keyword - use for long-tail variations" + else: + return "Low potential - deprioritize" + + def _generate_comparison_summary( + self, + primary_keywords: List[Dict[str, Any]], + secondary_keywords: List[Dict[str, Any]], + long_tail_keywords: List[Dict[str, Any]] + ) -> str: + """Generate summary of keyword comparison.""" + summary_parts = [] + + summary_parts.append( + f"Identified {len(primary_keywords)} high-priority primary keywords." + ) + + if primary_keywords: + top_keyword = primary_keywords[0]['keyword'] + summary_parts.append( + f"Top recommendation: '{top_keyword}' (potential score: {primary_keywords[0]['potential_score']})." + ) + + summary_parts.append( + f"Found {len(secondary_keywords)} secondary keywords for description and metadata." + ) + + summary_parts.append( + f"Discovered {len(long_tail_keywords)} long-tail opportunities with lower competition." + ) + + return " ".join(summary_parts) + + +def analyze_keyword_set(keywords_data: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Convenience function to analyze a set of keywords. + + Args: + keywords_data: List of keyword data dictionaries + + Returns: + Complete analysis report + """ + analyzer = KeywordAnalyzer() + return analyzer.compare_keywords(keywords_data) diff --git a/.agents/skills/app-store-optimization/scripts/launch_checklist.py b/.agents/skills/app-store-optimization/scripts/launch_checklist.py new file mode 100644 index 0000000..38eea18 --- /dev/null +++ b/.agents/skills/app-store-optimization/scripts/launch_checklist.py @@ -0,0 +1,739 @@ +""" +Launch checklist module for App Store Optimization. +Generates comprehensive pre-launch and update checklists. +""" + +from typing import Dict, List, Any, Optional +from datetime import datetime, timedelta + + +class LaunchChecklistGenerator: + """Generates comprehensive checklists for app launches and updates.""" + + def __init__(self, platform: str = 'both'): + """ + Initialize checklist generator. + + Args: + platform: 'apple', 'google', or 'both' + """ + if platform not in ['apple', 'google', 'both']: + raise ValueError("Platform must be 'apple', 'google', or 'both'") + + self.platform = platform + + def generate_prelaunch_checklist( + self, + app_info: Dict[str, Any], + launch_date: Optional[str] = None + ) -> Dict[str, Any]: + """ + Generate comprehensive pre-launch checklist. + + Args: + app_info: App information (name, category, target_audience) + launch_date: Target launch date (YYYY-MM-DD) + + Returns: + Complete pre-launch checklist + """ + checklist = { + 'app_info': app_info, + 'launch_date': launch_date, + 'checklists': {} + } + + # Generate platform-specific checklists + if self.platform in ['apple', 'both']: + checklist['checklists']['apple'] = self._generate_apple_checklist(app_info) + + if self.platform in ['google', 'both']: + checklist['checklists']['google'] = self._generate_google_checklist(app_info) + + # Add universal checklist items + checklist['checklists']['universal'] = self._generate_universal_checklist(app_info) + + # Generate timeline + if launch_date: + checklist['timeline'] = self._generate_launch_timeline(launch_date) + + # Calculate completion status + checklist['summary'] = self._calculate_checklist_summary(checklist['checklists']) + + return checklist + + def validate_app_store_compliance( + self, + app_data: Dict[str, Any], + platform: str = 'apple' + ) -> Dict[str, Any]: + """ + Validate compliance with app store guidelines. + + Args: + app_data: App data including metadata, privacy policy, etc. + platform: 'apple' or 'google' + + Returns: + Compliance validation report + """ + validation_results = { + 'platform': platform, + 'is_compliant': True, + 'errors': [], + 'warnings': [], + 'recommendations': [] + } + + if platform == 'apple': + self._validate_apple_compliance(app_data, validation_results) + elif platform == 'google': + self._validate_google_compliance(app_data, validation_results) + + # Determine overall compliance + validation_results['is_compliant'] = len(validation_results['errors']) == 0 + + return validation_results + + def create_update_plan( + self, + current_version: str, + planned_features: List[str], + update_frequency: str = 'monthly' + ) -> Dict[str, Any]: + """ + Create update cadence and feature rollout plan. + + Args: + current_version: Current app version + planned_features: List of planned features + update_frequency: 'weekly', 'biweekly', 'monthly', 'quarterly' + + Returns: + Update plan with cadence and feature schedule + """ + # Calculate next versions + next_versions = self._calculate_next_versions( + current_version, + update_frequency, + len(planned_features) + ) + + # Distribute features across versions + feature_schedule = self._distribute_features( + planned_features, + next_versions + ) + + # Generate "What's New" templates + whats_new_templates = [ + self._generate_whats_new_template(version_data) + for version_data in feature_schedule + ] + + return { + 'current_version': current_version, + 'update_frequency': update_frequency, + 'planned_updates': len(feature_schedule), + 'feature_schedule': feature_schedule, + 'whats_new_templates': whats_new_templates, + 'recommendations': self._generate_update_recommendations(update_frequency) + } + + def optimize_launch_timing( + self, + app_category: str, + target_audience: str, + current_date: Optional[str] = None + ) -> Dict[str, Any]: + """ + Recommend optimal launch timing. + + Args: + app_category: App category + target_audience: Target audience description + current_date: Current date (YYYY-MM-DD), defaults to today + + Returns: + Launch timing recommendations + """ + if not current_date: + current_date = datetime.now().strftime('%Y-%m-%d') + + # Analyze launch timing factors + day_of_week_rec = self._recommend_day_of_week(app_category) + seasonal_rec = self._recommend_seasonal_timing(app_category, current_date) + competitive_rec = self._analyze_competitive_timing(app_category) + + # Calculate optimal dates + optimal_dates = self._calculate_optimal_dates( + current_date, + day_of_week_rec, + seasonal_rec + ) + + return { + 'current_date': current_date, + 'optimal_launch_dates': optimal_dates, + 'day_of_week_recommendation': day_of_week_rec, + 'seasonal_considerations': seasonal_rec, + 'competitive_timing': competitive_rec, + 'final_recommendation': self._generate_timing_recommendation( + optimal_dates, + seasonal_rec + ) + } + + def plan_seasonal_campaigns( + self, + app_category: str, + current_month: int = None + ) -> Dict[str, Any]: + """ + Identify seasonal opportunities for ASO campaigns. + + Args: + app_category: App category + current_month: Current month (1-12), defaults to current + + Returns: + Seasonal campaign opportunities + """ + if not current_month: + current_month = datetime.now().month + + # Identify relevant seasonal events + seasonal_opportunities = self._identify_seasonal_opportunities( + app_category, + current_month + ) + + # Generate campaign ideas + campaigns = [ + self._generate_seasonal_campaign(opportunity) + for opportunity in seasonal_opportunities + ] + + return { + 'current_month': current_month, + 'category': app_category, + 'seasonal_opportunities': seasonal_opportunities, + 'campaign_ideas': campaigns, + 'implementation_timeline': self._create_seasonal_timeline(campaigns) + } + + def _generate_apple_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]: + """Generate Apple App Store specific checklist.""" + return [ + { + 'category': 'App Store Connect Setup', + 'items': [ + {'task': 'App Store Connect account created', 'status': 'pending'}, + {'task': 'App bundle ID registered', 'status': 'pending'}, + {'task': 'App Privacy declarations completed', 'status': 'pending'}, + {'task': 'Age rating questionnaire completed', 'status': 'pending'} + ] + }, + { + 'category': 'Metadata (Apple)', + 'items': [ + {'task': 'App title (30 chars max)', 'status': 'pending'}, + {'task': 'Subtitle (30 chars max)', 'status': 'pending'}, + {'task': 'Promotional text (170 chars max)', 'status': 'pending'}, + {'task': 'Description (4000 chars max)', 'status': 'pending'}, + {'task': 'Keywords (100 chars, comma-separated)', 'status': 'pending'}, + {'task': 'Category selection (primary + secondary)', 'status': 'pending'} + ] + }, + { + 'category': 'Visual Assets (Apple)', + 'items': [ + {'task': 'App icon (1024x1024px)', 'status': 'pending'}, + {'task': 'Screenshots (iPhone 6.7" required)', 'status': 'pending'}, + {'task': 'Screenshots (iPhone 5.5" required)', 'status': 'pending'}, + {'task': 'Screenshots (iPad Pro 12.9" if iPad app)', 'status': 'pending'}, + {'task': 'App preview video (optional but recommended)', 'status': 'pending'} + ] + }, + { + 'category': 'Technical Requirements (Apple)', + 'items': [ + {'task': 'Build uploaded to App Store Connect', 'status': 'pending'}, + {'task': 'TestFlight testing completed', 'status': 'pending'}, + {'task': 'App tested on required iOS versions', 'status': 'pending'}, + {'task': 'Crash-free rate > 99%', 'status': 'pending'}, + {'task': 'All links in app/metadata working', 'status': 'pending'} + ] + }, + { + 'category': 'Legal & Privacy (Apple)', + 'items': [ + {'task': 'Privacy Policy URL provided', 'status': 'pending'}, + {'task': 'Terms of Service URL (if applicable)', 'status': 'pending'}, + {'task': 'Data collection declarations accurate', 'status': 'pending'}, + {'task': 'Third-party SDKs disclosed', 'status': 'pending'} + ] + } + ] + + def _generate_google_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]: + """Generate Google Play Store specific checklist.""" + return [ + { + 'category': 'Play Console Setup', + 'items': [ + {'task': 'Google Play Console account created', 'status': 'pending'}, + {'task': 'Developer profile completed', 'status': 'pending'}, + {'task': 'Payment merchant account linked (if paid app)', 'status': 'pending'}, + {'task': 'Content rating questionnaire completed', 'status': 'pending'} + ] + }, + { + 'category': 'Metadata (Google)', + 'items': [ + {'task': 'App title (50 chars max)', 'status': 'pending'}, + {'task': 'Short description (80 chars max)', 'status': 'pending'}, + {'task': 'Full description (4000 chars max)', 'status': 'pending'}, + {'task': 'Category selection', 'status': 'pending'}, + {'task': 'Tags (up to 5)', 'status': 'pending'} + ] + }, + { + 'category': 'Visual Assets (Google)', + 'items': [ + {'task': 'App icon (512x512px)', 'status': 'pending'}, + {'task': 'Feature graphic (1024x500px)', 'status': 'pending'}, + {'task': 'Screenshots (2-8 required, phone)', 'status': 'pending'}, + {'task': 'Screenshots (tablet, if applicable)', 'status': 'pending'}, + {'task': 'Promo video (YouTube link, optional)', 'status': 'pending'} + ] + }, + { + 'category': 'Technical Requirements (Google)', + 'items': [ + {'task': 'APK/AAB uploaded to Play Console', 'status': 'pending'}, + {'task': 'Internal testing completed', 'status': 'pending'}, + {'task': 'App tested on required Android versions', 'status': 'pending'}, + {'task': 'Target API level meets requirements', 'status': 'pending'}, + {'task': 'All permissions justified', 'status': 'pending'} + ] + }, + { + 'category': 'Legal & Privacy (Google)', + 'items': [ + {'task': 'Privacy Policy URL provided', 'status': 'pending'}, + {'task': 'Data safety section completed', 'status': 'pending'}, + {'task': 'Ads disclosure (if applicable)', 'status': 'pending'}, + {'task': 'In-app purchase disclosure (if applicable)', 'status': 'pending'} + ] + } + ] + + def _generate_universal_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]: + """Generate universal (both platforms) checklist.""" + return [ + { + 'category': 'Pre-Launch Marketing', + 'items': [ + {'task': 'Landing page created', 'status': 'pending'}, + {'task': 'Social media accounts setup', 'status': 'pending'}, + {'task': 'Press kit prepared', 'status': 'pending'}, + {'task': 'Beta tester feedback collected', 'status': 'pending'}, + {'task': 'Launch announcement drafted', 'status': 'pending'} + ] + }, + { + 'category': 'ASO Preparation', + 'items': [ + {'task': 'Keyword research completed', 'status': 'pending'}, + {'task': 'Competitor analysis done', 'status': 'pending'}, + {'task': 'A/B test plan created for post-launch', 'status': 'pending'}, + {'task': 'Analytics tracking configured', 'status': 'pending'} + ] + }, + { + 'category': 'Quality Assurance', + 'items': [ + {'task': 'All core features tested', 'status': 'pending'}, + {'task': 'User flows validated', 'status': 'pending'}, + {'task': 'Performance testing completed', 'status': 'pending'}, + {'task': 'Accessibility features tested', 'status': 'pending'}, + {'task': 'Security audit completed', 'status': 'pending'} + ] + }, + { + 'category': 'Support Infrastructure', + 'items': [ + {'task': 'Support email/system setup', 'status': 'pending'}, + {'task': 'FAQ page created', 'status': 'pending'}, + {'task': 'Documentation for users prepared', 'status': 'pending'}, + {'task': 'Team trained on handling reviews', 'status': 'pending'} + ] + } + ] + + def _generate_launch_timeline(self, launch_date: str) -> List[Dict[str, Any]]: + """Generate timeline with milestones leading to launch.""" + launch_dt = datetime.strptime(launch_date, '%Y-%m-%d') + + milestones = [ + { + 'date': (launch_dt - timedelta(days=90)).strftime('%Y-%m-%d'), + 'milestone': '90 days before: Complete keyword research and competitor analysis' + }, + { + 'date': (launch_dt - timedelta(days=60)).strftime('%Y-%m-%d'), + 'milestone': '60 days before: Finalize metadata and visual assets' + }, + { + 'date': (launch_dt - timedelta(days=45)).strftime('%Y-%m-%d'), + 'milestone': '45 days before: Begin beta testing program' + }, + { + 'date': (launch_dt - timedelta(days=30)).strftime('%Y-%m-%d'), + 'milestone': '30 days before: Submit app for review (Apple typically takes 1-2 days, Google instant)' + }, + { + 'date': (launch_dt - timedelta(days=14)).strftime('%Y-%m-%d'), + 'milestone': '14 days before: Prepare launch marketing materials' + }, + { + 'date': (launch_dt - timedelta(days=7)).strftime('%Y-%m-%d'), + 'milestone': '7 days before: Set up analytics and monitoring' + }, + { + 'date': launch_dt.strftime('%Y-%m-%d'), + 'milestone': 'Launch Day: Release app and execute marketing plan' + }, + { + 'date': (launch_dt + timedelta(days=7)).strftime('%Y-%m-%d'), + 'milestone': '7 days after: Monitor metrics, respond to reviews, address critical issues' + }, + { + 'date': (launch_dt + timedelta(days=30)).strftime('%Y-%m-%d'), + 'milestone': '30 days after: Analyze launch metrics, plan first update' + } + ] + + return milestones + + def _calculate_checklist_summary(self, checklists: Dict[str, List[Dict[str, Any]]]) -> Dict[str, Any]: + """Calculate completion summary.""" + total_items = 0 + completed_items = 0 + + for platform, categories in checklists.items(): + for category in categories: + for item in category['items']: + total_items += 1 + if item['status'] == 'completed': + completed_items += 1 + + completion_percentage = (completed_items / total_items * 100) if total_items > 0 else 0 + + return { + 'total_items': total_items, + 'completed_items': completed_items, + 'pending_items': total_items - completed_items, + 'completion_percentage': round(completion_percentage, 1), + 'is_ready_to_launch': completion_percentage == 100 + } + + def _validate_apple_compliance( + self, + app_data: Dict[str, Any], + validation_results: Dict[str, Any] + ) -> None: + """Validate Apple App Store compliance.""" + # Check for required fields + if not app_data.get('privacy_policy_url'): + validation_results['errors'].append("Privacy Policy URL is required") + + if not app_data.get('app_icon'): + validation_results['errors'].append("App icon (1024x1024px) is required") + + # Check metadata character limits + title = app_data.get('title', '') + if len(title) > 30: + validation_results['errors'].append(f"Title exceeds 30 characters ({len(title)})") + + # Warnings for best practices + subtitle = app_data.get('subtitle', '') + if not subtitle: + validation_results['warnings'].append("Subtitle is empty - consider adding for better discoverability") + + keywords = app_data.get('keywords', '') + if len(keywords) < 80: + validation_results['warnings'].append( + f"Keywords field underutilized ({len(keywords)}/100 chars) - add more keywords" + ) + + def _validate_google_compliance( + self, + app_data: Dict[str, Any], + validation_results: Dict[str, Any] + ) -> None: + """Validate Google Play Store compliance.""" + # Check for required fields + if not app_data.get('privacy_policy_url'): + validation_results['errors'].append("Privacy Policy URL is required") + + if not app_data.get('feature_graphic'): + validation_results['errors'].append("Feature graphic (1024x500px) is required") + + # Check metadata character limits + title = app_data.get('title', '') + if len(title) > 50: + validation_results['errors'].append(f"Title exceeds 50 characters ({len(title)})") + + short_desc = app_data.get('short_description', '') + if len(short_desc) > 80: + validation_results['errors'].append(f"Short description exceeds 80 characters ({len(short_desc)})") + + # Warnings + if not short_desc: + validation_results['warnings'].append("Short description is empty") + + def _calculate_next_versions( + self, + current_version: str, + update_frequency: str, + feature_count: int + ) -> List[str]: + """Calculate next version numbers.""" + # Parse current version (assume semantic versioning) + parts = current_version.split('.') + major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2] if len(parts) > 2 else 0) + + versions = [] + for i in range(feature_count): + if update_frequency == 'weekly': + patch += 1 + elif update_frequency == 'biweekly': + patch += 1 + elif update_frequency == 'monthly': + minor += 1 + patch = 0 + else: # quarterly + minor += 1 + patch = 0 + + versions.append(f"{major}.{minor}.{patch}") + + return versions + + def _distribute_features( + self, + features: List[str], + versions: List[str] + ) -> List[Dict[str, Any]]: + """Distribute features across versions.""" + features_per_version = max(1, len(features) // len(versions)) + + schedule = [] + for i, version in enumerate(versions): + start_idx = i * features_per_version + end_idx = start_idx + features_per_version if i < len(versions) - 1 else len(features) + + schedule.append({ + 'version': version, + 'features': features[start_idx:end_idx], + 'release_priority': 'high' if i == 0 else ('medium' if i < len(versions) // 2 else 'low') + }) + + return schedule + + def _generate_whats_new_template(self, version_data: Dict[str, Any]) -> Dict[str, str]: + """Generate What's New template for version.""" + features_list = '\n'.join([f"• {feature}" for feature in version_data['features']]) + + template = f"""Version {version_data['version']} + +{features_list} + +We're constantly improving your experience. Thanks for using [App Name]! + +Have feedback? Contact us at support@[company].com""" + + return { + 'version': version_data['version'], + 'template': template + } + + def _generate_update_recommendations(self, update_frequency: str) -> List[str]: + """Generate recommendations for update strategy.""" + recommendations = [] + + if update_frequency == 'weekly': + recommendations.append("Weekly updates show active development but ensure quality doesn't suffer") + elif update_frequency == 'monthly': + recommendations.append("Monthly updates are optimal for most apps - balance features and stability") + + recommendations.extend([ + "Include bug fixes in every update", + "Update 'What's New' section with each release", + "Respond to reviews mentioning fixed issues" + ]) + + return recommendations + + def _recommend_day_of_week(self, app_category: str) -> Dict[str, Any]: + """Recommend best day of week to launch.""" + # General recommendations based on category + if app_category.lower() in ['games', 'entertainment']: + return { + 'recommended_day': 'Thursday', + 'rationale': 'People download entertainment apps before weekend' + } + elif app_category.lower() in ['productivity', 'business']: + return { + 'recommended_day': 'Tuesday', + 'rationale': 'Business users most active mid-week' + } + else: + return { + 'recommended_day': 'Wednesday', + 'rationale': 'Mid-week provides good balance and review potential' + } + + def _recommend_seasonal_timing(self, app_category: str, current_date: str) -> Dict[str, Any]: + """Recommend seasonal timing considerations.""" + current_dt = datetime.strptime(current_date, '%Y-%m-%d') + month = current_dt.month + + # Avoid certain periods + avoid_periods = [] + if month == 12: + avoid_periods.append("Late December - low user engagement during holidays") + if month in [7, 8]: + avoid_periods.append("Summer months - some categories see lower engagement") + + # Recommend periods + good_periods = [] + if month in [1, 9]: + good_periods.append("New Year/Back-to-school - high user engagement") + if month in [10, 11]: + good_periods.append("Pre-holiday season - good for shopping/gift apps") + + return { + 'current_month': month, + 'avoid_periods': avoid_periods, + 'good_periods': good_periods + } + + def _analyze_competitive_timing(self, app_category: str) -> Dict[str, str]: + """Analyze competitive timing considerations.""" + return { + 'recommendation': 'Research competitor launch schedules in your category', + 'strategy': 'Avoid launching same week as major competitor updates' + } + + def _calculate_optimal_dates( + self, + current_date: str, + day_rec: Dict[str, Any], + seasonal_rec: Dict[str, Any] + ) -> List[str]: + """Calculate optimal launch dates.""" + current_dt = datetime.strptime(current_date, '%Y-%m-%d') + + # Find next occurrence of recommended day + target_day = day_rec['recommended_day'] + days_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4} + target_day_num = days_map.get(target_day, 2) + + days_ahead = (target_day_num - current_dt.weekday()) % 7 + if days_ahead == 0: + days_ahead = 7 + + next_target_date = current_dt + timedelta(days=days_ahead) + + optimal_dates = [ + next_target_date.strftime('%Y-%m-%d'), + (next_target_date + timedelta(days=7)).strftime('%Y-%m-%d'), + (next_target_date + timedelta(days=14)).strftime('%Y-%m-%d') + ] + + return optimal_dates + + def _generate_timing_recommendation( + self, + optimal_dates: List[str], + seasonal_rec: Dict[str, Any] + ) -> str: + """Generate final timing recommendation.""" + if seasonal_rec['avoid_periods']: + return f"Consider launching in {optimal_dates[1]} to avoid {seasonal_rec['avoid_periods'][0]}" + elif seasonal_rec['good_periods']: + return f"Launch on {optimal_dates[0]} to capitalize on {seasonal_rec['good_periods'][0]}" + else: + return f"Recommended launch date: {optimal_dates[0]}" + + def _identify_seasonal_opportunities( + self, + app_category: str, + current_month: int + ) -> List[Dict[str, Any]]: + """Identify seasonal opportunities for category.""" + opportunities = [] + + # Universal opportunities + if current_month == 1: + opportunities.append({ + 'event': 'New Year Resolutions', + 'dates': 'January 1-31', + 'relevance': 'high' if app_category.lower() in ['health', 'fitness', 'productivity'] else 'medium' + }) + + if current_month in [11, 12]: + opportunities.append({ + 'event': 'Holiday Shopping Season', + 'dates': 'November-December', + 'relevance': 'high' if app_category.lower() in ['shopping', 'gifts'] else 'low' + }) + + # Category-specific + if app_category.lower() == 'education' and current_month in [8, 9]: + opportunities.append({ + 'event': 'Back to School', + 'dates': 'August-September', + 'relevance': 'high' + }) + + return opportunities + + def _generate_seasonal_campaign(self, opportunity: Dict[str, Any]) -> Dict[str, Any]: + """Generate campaign idea for seasonal opportunity.""" + return { + 'event': opportunity['event'], + 'campaign_idea': f"Create themed visuals and messaging for {opportunity['event']}", + 'metadata_updates': 'Update app description and screenshots with seasonal themes', + 'promotion_strategy': 'Consider limited-time features or discounts' + } + + def _create_seasonal_timeline(self, campaigns: List[Dict[str, Any]]) -> List[str]: + """Create implementation timeline for campaigns.""" + return [ + f"30 days before: Plan {campaign['event']} campaign strategy" + for campaign in campaigns + ] + + +def generate_launch_checklist( + platform: str, + app_info: Dict[str, Any], + launch_date: Optional[str] = None +) -> Dict[str, Any]: + """ + Convenience function to generate launch checklist. + + Args: + platform: Platform ('apple', 'google', or 'both') + app_info: App information + launch_date: Target launch date + + Returns: + Complete launch checklist + """ + generator = LaunchChecklistGenerator(platform) + return generator.generate_prelaunch_checklist(app_info, launch_date) diff --git a/.agents/skills/app-store-optimization/scripts/localization_helper.py b/.agents/skills/app-store-optimization/scripts/localization_helper.py new file mode 100644 index 0000000..c47003c --- /dev/null +++ b/.agents/skills/app-store-optimization/scripts/localization_helper.py @@ -0,0 +1,588 @@ +""" +Localization helper module for App Store Optimization. +Manages multi-language ASO optimization strategies. +""" + +from typing import Dict, List, Any, Optional, Tuple + + +class LocalizationHelper: + """Helps manage multi-language ASO optimization.""" + + # Priority markets by language (based on app store revenue and user base) + PRIORITY_MARKETS = { + 'tier_1': [ + {'language': 'en-US', 'market': 'United States', 'revenue_share': 0.25}, + {'language': 'zh-CN', 'market': 'China', 'revenue_share': 0.20}, + {'language': 'ja-JP', 'market': 'Japan', 'revenue_share': 0.10}, + {'language': 'de-DE', 'market': 'Germany', 'revenue_share': 0.08}, + {'language': 'en-GB', 'market': 'United Kingdom', 'revenue_share': 0.06} + ], + 'tier_2': [ + {'language': 'fr-FR', 'market': 'France', 'revenue_share': 0.05}, + {'language': 'ko-KR', 'market': 'South Korea', 'revenue_share': 0.05}, + {'language': 'es-ES', 'market': 'Spain', 'revenue_share': 0.03}, + {'language': 'it-IT', 'market': 'Italy', 'revenue_share': 0.03}, + {'language': 'pt-BR', 'market': 'Brazil', 'revenue_share': 0.03} + ], + 'tier_3': [ + {'language': 'ru-RU', 'market': 'Russia', 'revenue_share': 0.02}, + {'language': 'es-MX', 'market': 'Mexico', 'revenue_share': 0.02}, + {'language': 'nl-NL', 'market': 'Netherlands', 'revenue_share': 0.02}, + {'language': 'sv-SE', 'market': 'Sweden', 'revenue_share': 0.01}, + {'language': 'pl-PL', 'market': 'Poland', 'revenue_share': 0.01} + ] + } + + # Character limit multipliers by language (some languages need more/less space) + CHAR_MULTIPLIERS = { + 'en': 1.0, + 'zh': 0.6, # Chinese characters are more compact + 'ja': 0.7, # Japanese uses kanji + 'ko': 0.8, # Korean is relatively compact + 'de': 1.3, # German words are typically longer + 'fr': 1.2, # French tends to be longer + 'es': 1.1, # Spanish slightly longer + 'pt': 1.1, # Portuguese similar to Spanish + 'ru': 1.1, # Russian similar length + 'ar': 1.0, # Arabic varies + 'it': 1.1 # Italian similar to Spanish + } + + def __init__(self, app_category: str = 'general'): + """ + Initialize localization helper. + + Args: + app_category: App category to prioritize relevant markets + """ + self.app_category = app_category + self.localization_plans = [] + + def identify_target_markets( + self, + current_market: str = 'en-US', + budget_level: str = 'medium', + target_market_count: int = 5 + ) -> Dict[str, Any]: + """ + Recommend priority markets for localization. + + Args: + current_market: Current/primary market + budget_level: 'low', 'medium', or 'high' + target_market_count: Number of markets to target + + Returns: + Prioritized market recommendations + """ + # Determine tier priorities based on budget + if budget_level == 'low': + priority_tiers = ['tier_1'] + max_markets = min(target_market_count, 3) + elif budget_level == 'medium': + priority_tiers = ['tier_1', 'tier_2'] + max_markets = min(target_market_count, 8) + else: # high budget + priority_tiers = ['tier_1', 'tier_2', 'tier_3'] + max_markets = target_market_count + + # Collect markets from priority tiers + recommended_markets = [] + for tier in priority_tiers: + for market in self.PRIORITY_MARKETS[tier]: + if market['language'] != current_market: + recommended_markets.append({ + **market, + 'tier': tier, + 'estimated_translation_cost': self._estimate_translation_cost( + market['language'] + ) + }) + + # Sort by revenue share and limit + recommended_markets.sort(key=lambda x: x['revenue_share'], reverse=True) + recommended_markets = recommended_markets[:max_markets] + + # Calculate potential ROI + total_potential_revenue_share = sum(m['revenue_share'] for m in recommended_markets) + + return { + 'recommended_markets': recommended_markets, + 'total_markets': len(recommended_markets), + 'estimated_total_revenue_lift': f"{total_potential_revenue_share*100:.1f}%", + 'estimated_cost': self._estimate_total_localization_cost(recommended_markets), + 'implementation_priority': self._prioritize_implementation(recommended_markets) + } + + def translate_metadata( + self, + source_metadata: Dict[str, str], + source_language: str, + target_language: str, + platform: str = 'apple' + ) -> Dict[str, Any]: + """ + Generate localized metadata with character limit considerations. + + Args: + source_metadata: Original metadata (title, description, etc.) + source_language: Source language code (e.g., 'en') + target_language: Target language code (e.g., 'es') + platform: 'apple' or 'google' + + Returns: + Localized metadata with character limit validation + """ + # Get character multiplier + target_lang_code = target_language.split('-')[0] + char_multiplier = self.CHAR_MULTIPLIERS.get(target_lang_code, 1.0) + + # Platform-specific limits + if platform == 'apple': + limits = {'title': 30, 'subtitle': 30, 'description': 4000, 'keywords': 100} + else: + limits = {'title': 50, 'short_description': 80, 'description': 4000} + + localized_metadata = {} + warnings = [] + + for field, text in source_metadata.items(): + if field not in limits: + continue + + # Estimate target length + estimated_length = int(len(text) * char_multiplier) + limit = limits[field] + + localized_metadata[field] = { + 'original_text': text, + 'original_length': len(text), + 'estimated_target_length': estimated_length, + 'character_limit': limit, + 'fits_within_limit': estimated_length <= limit, + 'translation_notes': self._get_translation_notes( + field, + target_language, + estimated_length, + limit + ) + } + + if estimated_length > limit: + warnings.append( + f"{field}: Estimated length ({estimated_length}) may exceed limit ({limit}) - " + f"condensing may be required" + ) + + return { + 'source_language': source_language, + 'target_language': target_language, + 'platform': platform, + 'localized_fields': localized_metadata, + 'character_multiplier': char_multiplier, + 'warnings': warnings, + 'recommendations': self._generate_translation_recommendations( + target_language, + warnings + ) + } + + def adapt_keywords( + self, + source_keywords: List[str], + source_language: str, + target_language: str, + target_market: str + ) -> Dict[str, Any]: + """ + Adapt keywords for target market (not just direct translation). + + Args: + source_keywords: Original keywords + source_language: Source language code + target_language: Target language code + target_market: Target market (e.g., 'France', 'Japan') + + Returns: + Adapted keyword recommendations + """ + # Cultural adaptation considerations + cultural_notes = self._get_cultural_keyword_considerations(target_market) + + # Search behavior differences + search_patterns = self._get_search_patterns(target_market) + + adapted_keywords = [] + for keyword in source_keywords: + adapted_keywords.append({ + 'source_keyword': keyword, + 'adaptation_strategy': self._determine_adaptation_strategy( + keyword, + target_market + ), + 'cultural_considerations': cultural_notes.get(keyword, []), + 'priority': 'high' if keyword in source_keywords[:3] else 'medium' + }) + + return { + 'source_language': source_language, + 'target_language': target_language, + 'target_market': target_market, + 'adapted_keywords': adapted_keywords, + 'search_behavior_notes': search_patterns, + 'recommendations': [ + 'Use native speakers for keyword research', + 'Test keywords with local users before finalizing', + 'Consider local competitors\' keyword strategies', + 'Monitor search trends in target market' + ] + } + + def validate_translations( + self, + translated_metadata: Dict[str, str], + target_language: str, + platform: str = 'apple' + ) -> Dict[str, Any]: + """ + Validate translated metadata for character limits and quality. + + Args: + translated_metadata: Translated text fields + target_language: Target language code + platform: 'apple' or 'google' + + Returns: + Validation report + """ + # Platform limits + if platform == 'apple': + limits = {'title': 30, 'subtitle': 30, 'description': 4000, 'keywords': 100} + else: + limits = {'title': 50, 'short_description': 80, 'description': 4000} + + validation_results = { + 'is_valid': True, + 'field_validations': {}, + 'errors': [], + 'warnings': [] + } + + for field, text in translated_metadata.items(): + if field not in limits: + continue + + actual_length = len(text) + limit = limits[field] + is_within_limit = actual_length <= limit + + validation_results['field_validations'][field] = { + 'text': text, + 'length': actual_length, + 'limit': limit, + 'is_valid': is_within_limit, + 'usage_percentage': round((actual_length / limit) * 100, 1) + } + + if not is_within_limit: + validation_results['is_valid'] = False + validation_results['errors'].append( + f"{field} exceeds limit: {actual_length}/{limit} characters" + ) + + # Quality checks + quality_issues = self._check_translation_quality( + translated_metadata, + target_language + ) + + validation_results['quality_checks'] = quality_issues + + if quality_issues: + validation_results['warnings'].extend( + [f"Quality issue: {issue}" for issue in quality_issues] + ) + + return validation_results + + def calculate_localization_roi( + self, + target_markets: List[str], + current_monthly_downloads: int, + localization_cost: float, + expected_lift_percentage: float = 0.15 + ) -> Dict[str, Any]: + """ + Estimate ROI of localization investment. + + Args: + target_markets: List of market codes + current_monthly_downloads: Current monthly downloads + localization_cost: Total cost to localize + expected_lift_percentage: Expected download increase (default 15%) + + Returns: + ROI analysis + """ + # Estimate market-specific lift + market_data = [] + total_expected_lift = 0 + + for market_code in target_markets: + # Find market in priority lists + market_info = None + for tier_name, markets in self.PRIORITY_MARKETS.items(): + for m in markets: + if m['language'] == market_code: + market_info = m + break + + if not market_info: + continue + + # Estimate downloads from this market + market_downloads = int(current_monthly_downloads * market_info['revenue_share']) + expected_increase = int(market_downloads * expected_lift_percentage) + total_expected_lift += expected_increase + + market_data.append({ + 'market': market_info['market'], + 'current_monthly_downloads': market_downloads, + 'expected_increase': expected_increase, + 'revenue_potential': market_info['revenue_share'] + }) + + # Calculate payback period (assuming $2 revenue per download) + revenue_per_download = 2.0 + monthly_additional_revenue = total_expected_lift * revenue_per_download + payback_months = (localization_cost / monthly_additional_revenue) if monthly_additional_revenue > 0 else float('inf') + + return { + 'markets_analyzed': len(market_data), + 'market_breakdown': market_data, + 'total_expected_monthly_lift': total_expected_lift, + 'expected_monthly_revenue_increase': f"${monthly_additional_revenue:,.2f}", + 'localization_cost': f"${localization_cost:,.2f}", + 'payback_period_months': round(payback_months, 1) if payback_months != float('inf') else 'N/A', + 'annual_roi': f"{((monthly_additional_revenue * 12 - localization_cost) / localization_cost * 100):.1f}%" if payback_months != float('inf') else 'Negative', + 'recommendation': self._generate_roi_recommendation(payback_months) + } + + def _estimate_translation_cost(self, language: str) -> Dict[str, float]: + """Estimate translation cost for a language.""" + # Base cost per word (professional translation) + base_cost_per_word = 0.12 + + # Language-specific multipliers + multipliers = { + 'zh-CN': 1.5, # Chinese requires specialist + 'ja-JP': 1.5, # Japanese requires specialist + 'ko-KR': 1.3, + 'ar-SA': 1.4, # Arabic (right-to-left) + 'default': 1.0 + } + + multiplier = multipliers.get(language, multipliers['default']) + + # Typical word counts for app store metadata + typical_word_counts = { + 'title': 5, + 'subtitle': 5, + 'description': 300, + 'keywords': 20, + 'screenshots': 50 # Caption text + } + + total_words = sum(typical_word_counts.values()) + estimated_cost = total_words * base_cost_per_word * multiplier + + return { + 'cost_per_word': base_cost_per_word * multiplier, + 'total_words': total_words, + 'estimated_cost': round(estimated_cost, 2) + } + + def _estimate_total_localization_cost(self, markets: List[Dict[str, Any]]) -> str: + """Estimate total cost for multiple markets.""" + total = sum(m['estimated_translation_cost']['estimated_cost'] for m in markets) + return f"${total:,.2f}" + + def _prioritize_implementation(self, markets: List[Dict[str, Any]]) -> List[Dict[str, str]]: + """Create phased implementation plan.""" + phases = [] + + # Phase 1: Top revenue markets + phase_1 = [m for m in markets[:3]] + if phase_1: + phases.append({ + 'phase': 'Phase 1 (First 30 days)', + 'markets': ', '.join([m['market'] for m in phase_1]), + 'rationale': 'Highest revenue potential markets' + }) + + # Phase 2: Remaining tier 1 and top tier 2 + phase_2 = [m for m in markets[3:6]] + if phase_2: + phases.append({ + 'phase': 'Phase 2 (Days 31-60)', + 'markets': ', '.join([m['market'] for m in phase_2]), + 'rationale': 'Strong revenue markets with good ROI' + }) + + # Phase 3: Remaining markets + phase_3 = [m for m in markets[6:]] + if phase_3: + phases.append({ + 'phase': 'Phase 3 (Days 61-90)', + 'markets': ', '.join([m['market'] for m in phase_3]), + 'rationale': 'Complete global coverage' + }) + + return phases + + def _get_translation_notes( + self, + field: str, + target_language: str, + estimated_length: int, + limit: int + ) -> List[str]: + """Get translation-specific notes for field.""" + notes = [] + + if estimated_length > limit: + notes.append(f"Condensing required - aim for {limit - 10} characters to allow buffer") + + if field == 'title' and target_language.startswith('zh'): + notes.append("Chinese characters convey more meaning - may need fewer characters") + + if field == 'keywords' and target_language.startswith('de'): + notes.append("German compound words may be longer - prioritize shorter keywords") + + return notes + + def _generate_translation_recommendations( + self, + target_language: str, + warnings: List[str] + ) -> List[str]: + """Generate translation recommendations.""" + recommendations = [ + "Use professional native speakers for translation", + "Test translations with local users before finalizing" + ] + + if warnings: + recommendations.append("Work with translator to condense text while preserving meaning") + + if target_language.startswith('zh') or target_language.startswith('ja'): + recommendations.append("Consider cultural context and local idioms") + + return recommendations + + def _get_cultural_keyword_considerations(self, target_market: str) -> Dict[str, List[str]]: + """Get cultural considerations for keywords by market.""" + # Simplified example - real implementation would be more comprehensive + considerations = { + 'China': ['Avoid politically sensitive terms', 'Consider local alternatives to blocked services'], + 'Japan': ['Honorific language important', 'Technical terms often use katakana'], + 'Germany': ['Privacy and security terms resonate', 'Efficiency and quality valued'], + 'France': ['French language protection laws', 'Prefer French terms over English'], + 'default': ['Research local search behavior', 'Test with native speakers'] + } + + return considerations.get(target_market, considerations['default']) + + def _get_search_patterns(self, target_market: str) -> List[str]: + """Get search pattern notes for market.""" + patterns = { + 'China': ['Use both simplified characters and romanization', 'Brand names often romanized'], + 'Japan': ['Mix of kanji, hiragana, and katakana', 'English words common in tech'], + 'Germany': ['Compound words common', 'Specific technical terminology'], + 'default': ['Research local search trends', 'Monitor competitor keywords'] + } + + return patterns.get(target_market, patterns['default']) + + def _determine_adaptation_strategy(self, keyword: str, target_market: str) -> str: + """Determine how to adapt keyword for market.""" + # Simplified logic + if target_market in ['China', 'Japan', 'Korea']: + return 'full_localization' # Complete translation needed + elif target_market in ['Germany', 'France', 'Spain']: + return 'adapt_and_translate' # Some adaptation needed + else: + return 'direct_translation' # Direct translation usually sufficient + + def _check_translation_quality( + self, + translated_metadata: Dict[str, str], + target_language: str + ) -> List[str]: + """Basic quality checks for translations.""" + issues = [] + + # Check for untranslated placeholders + for field, text in translated_metadata.items(): + if '[' in text or '{' in text or 'TODO' in text.upper(): + issues.append(f"{field} contains placeholder text") + + # Check for excessive punctuation + for field, text in translated_metadata.items(): + if text.count('!') > 3: + issues.append(f"{field} has excessive exclamation marks") + + return issues + + def _generate_roi_recommendation(self, payback_months: float) -> str: + """Generate ROI recommendation.""" + if payback_months <= 3: + return "Excellent ROI - proceed immediately" + elif payback_months <= 6: + return "Good ROI - recommended investment" + elif payback_months <= 12: + return "Moderate ROI - consider if strategic market" + else: + return "Low ROI - reconsider or focus on higher-priority markets first" + + +def plan_localization_strategy( + current_market: str, + budget_level: str, + monthly_downloads: int +) -> Dict[str, Any]: + """ + Convenience function to plan localization strategy. + + Args: + current_market: Current market code + budget_level: Budget level + monthly_downloads: Current monthly downloads + + Returns: + Complete localization plan + """ + helper = LocalizationHelper() + + target_markets = helper.identify_target_markets( + current_market=current_market, + budget_level=budget_level + ) + + # Extract market codes + market_codes = [m['language'] for m in target_markets['recommended_markets']] + + # Calculate ROI + estimated_cost = float(target_markets['estimated_cost'].replace('$', '').replace(',', '')) + + roi_analysis = helper.calculate_localization_roi( + market_codes, + monthly_downloads, + estimated_cost + ) + + return { + 'target_markets': target_markets, + 'roi_analysis': roi_analysis + } diff --git a/.agents/skills/app-store-optimization/scripts/metadata_optimizer.py b/.agents/skills/app-store-optimization/scripts/metadata_optimizer.py new file mode 100644 index 0000000..7b50614 --- /dev/null +++ b/.agents/skills/app-store-optimization/scripts/metadata_optimizer.py @@ -0,0 +1,581 @@ +""" +Metadata optimization module for App Store Optimization. +Optimizes titles, descriptions, and keyword fields with platform-specific character limit validation. +""" + +from typing import Dict, List, Any, Optional, Tuple +import re + + +class MetadataOptimizer: + """Optimizes app store metadata for maximum discoverability and conversion.""" + + # Platform-specific character limits + CHAR_LIMITS = { + 'apple': { + 'title': 30, + 'subtitle': 30, + 'promotional_text': 170, + 'description': 4000, + 'keywords': 100, + 'whats_new': 4000 + }, + 'google': { + 'title': 50, + 'short_description': 80, + 'full_description': 4000 + } + } + + def __init__(self, platform: str = 'apple'): + """ + Initialize metadata optimizer. + + Args: + platform: 'apple' or 'google' + """ + if platform not in ['apple', 'google']: + raise ValueError("Platform must be 'apple' or 'google'") + + self.platform = platform + self.limits = self.CHAR_LIMITS[platform] + + def optimize_title( + self, + app_name: str, + target_keywords: List[str], + include_brand: bool = True + ) -> Dict[str, Any]: + """ + Optimize app title with keyword integration. + + Args: + app_name: Your app's brand name + target_keywords: List of keywords to potentially include + include_brand: Whether to include brand name + + Returns: + Optimized title options with analysis + """ + max_length = self.limits['title'] + + title_options = [] + + # Option 1: Brand name only + if include_brand: + option1 = app_name[:max_length] + title_options.append({ + 'title': option1, + 'length': len(option1), + 'remaining_chars': max_length - len(option1), + 'keywords_included': [], + 'strategy': 'brand_only', + 'pros': ['Maximum brand recognition', 'Clean and simple'], + 'cons': ['No keyword targeting', 'Lower discoverability'] + }) + + # Option 2: Brand + Primary Keyword + if target_keywords: + primary_keyword = target_keywords[0] + option2 = self._build_title_with_keywords( + app_name, + [primary_keyword], + max_length + ) + if option2: + title_options.append({ + 'title': option2, + 'length': len(option2), + 'remaining_chars': max_length - len(option2), + 'keywords_included': [primary_keyword], + 'strategy': 'brand_plus_primary', + 'pros': ['Targets main keyword', 'Maintains brand identity'], + 'cons': ['Limited keyword coverage'] + }) + + # Option 3: Brand + Multiple Keywords (if space allows) + if len(target_keywords) > 1: + option3 = self._build_title_with_keywords( + app_name, + target_keywords[:2], + max_length + ) + if option3: + title_options.append({ + 'title': option3, + 'length': len(option3), + 'remaining_chars': max_length - len(option3), + 'keywords_included': target_keywords[:2], + 'strategy': 'brand_plus_multiple', + 'pros': ['Multiple keyword targets', 'Better discoverability'], + 'cons': ['May feel cluttered', 'Less brand focus'] + }) + + # Option 4: Keyword-first approach (for new apps) + if target_keywords and not include_brand: + option4 = " ".join(target_keywords[:2])[:max_length] + title_options.append({ + 'title': option4, + 'length': len(option4), + 'remaining_chars': max_length - len(option4), + 'keywords_included': target_keywords[:2], + 'strategy': 'keyword_first', + 'pros': ['Maximum SEO benefit', 'Clear functionality'], + 'cons': ['No brand recognition', 'Generic appearance'] + }) + + return { + 'platform': self.platform, + 'max_length': max_length, + 'options': title_options, + 'recommendation': self._recommend_title_option(title_options) + } + + def optimize_description( + self, + app_info: Dict[str, Any], + target_keywords: List[str], + description_type: str = 'full' + ) -> Dict[str, Any]: + """ + Optimize app description with keyword integration and conversion focus. + + Args: + app_info: Dict with 'name', 'key_features', 'unique_value', 'target_audience' + target_keywords: List of keywords to integrate naturally + description_type: 'full', 'short' (Google), 'subtitle' (Apple) + + Returns: + Optimized description with analysis + """ + if description_type == 'short' and self.platform == 'google': + return self._optimize_short_description(app_info, target_keywords) + elif description_type == 'subtitle' and self.platform == 'apple': + return self._optimize_subtitle(app_info, target_keywords) + else: + return self._optimize_full_description(app_info, target_keywords) + + def optimize_keyword_field( + self, + target_keywords: List[str], + app_title: str = "", + app_description: str = "" + ) -> Dict[str, Any]: + """ + Optimize Apple's 100-character keyword field. + + Rules: + - No spaces between commas + - No plural forms if singular exists + - No duplicates + - Keywords in title/subtitle are already indexed + + Args: + target_keywords: List of target keywords + app_title: Current app title (to avoid duplication) + app_description: Current description (to check coverage) + + Returns: + Optimized keyword field (comma-separated, no spaces) + """ + if self.platform != 'apple': + return {'error': 'Keyword field optimization only applies to Apple App Store'} + + max_length = self.limits['keywords'] + + # Extract words already in title (these don't need to be in keyword field) + title_words = set(app_title.lower().split()) if app_title else set() + + # Process keywords + processed_keywords = [] + for keyword in target_keywords: + keyword_lower = keyword.lower().strip() + + # Skip if already in title + if keyword_lower in title_words: + continue + + # Remove duplicates and process + words = keyword_lower.split() + for word in words: + if word not in processed_keywords and word not in title_words: + processed_keywords.append(word) + + # Remove plurals if singular exists + deduplicated = self._remove_plural_duplicates(processed_keywords) + + # Build keyword field within 100 character limit + keyword_field = self._build_keyword_field(deduplicated, max_length) + + # Calculate keyword density in description + density = self._calculate_coverage(target_keywords, app_description) + + return { + 'keyword_field': keyword_field, + 'length': len(keyword_field), + 'remaining_chars': max_length - len(keyword_field), + 'keywords_included': keyword_field.split(','), + 'keywords_count': len(keyword_field.split(',')), + 'keywords_excluded': [kw for kw in target_keywords if kw.lower() not in keyword_field], + 'description_coverage': density, + 'optimization_tips': [ + 'Keywords in title are auto-indexed - no need to repeat', + 'Use singular forms only (Apple indexes plurals automatically)', + 'No spaces between commas to maximize character usage', + 'Update keyword field with each app update to test variations' + ] + } + + def validate_character_limits( + self, + metadata: Dict[str, str] + ) -> Dict[str, Any]: + """ + Validate all metadata fields against platform character limits. + + Args: + metadata: Dictionary of field_name: value + + Returns: + Validation report with errors and warnings + """ + validation_results = { + 'is_valid': True, + 'errors': [], + 'warnings': [], + 'field_status': {} + } + + for field_name, value in metadata.items(): + if field_name not in self.limits: + validation_results['warnings'].append( + f"Unknown field '{field_name}' for {self.platform} platform" + ) + continue + + max_length = self.limits[field_name] + actual_length = len(value) + remaining = max_length - actual_length + + field_status = { + 'value': value, + 'length': actual_length, + 'limit': max_length, + 'remaining': remaining, + 'is_valid': actual_length <= max_length, + 'usage_percentage': round((actual_length / max_length) * 100, 1) + } + + validation_results['field_status'][field_name] = field_status + + if actual_length > max_length: + validation_results['is_valid'] = False + validation_results['errors'].append( + f"'{field_name}' exceeds limit: {actual_length}/{max_length} chars" + ) + elif remaining > max_length * 0.2: # More than 20% unused + validation_results['warnings'].append( + f"'{field_name}' under-utilizes space: {remaining} chars remaining" + ) + + return validation_results + + def calculate_keyword_density( + self, + text: str, + target_keywords: List[str] + ) -> Dict[str, Any]: + """ + Calculate keyword density in text. + + Args: + text: Text to analyze + target_keywords: Keywords to check + + Returns: + Density analysis + """ + text_lower = text.lower() + total_words = len(text_lower.split()) + + keyword_densities = {} + for keyword in target_keywords: + keyword_lower = keyword.lower() + count = text_lower.count(keyword_lower) + density = (count / total_words * 100) if total_words > 0 else 0 + + keyword_densities[keyword] = { + 'occurrences': count, + 'density_percentage': round(density, 2), + 'status': self._assess_density(density) + } + + # Overall assessment + total_keyword_occurrences = sum(kw['occurrences'] for kw in keyword_densities.values()) + overall_density = (total_keyword_occurrences / total_words * 100) if total_words > 0 else 0 + + return { + 'total_words': total_words, + 'keyword_densities': keyword_densities, + 'overall_keyword_density': round(overall_density, 2), + 'assessment': self._assess_overall_density(overall_density), + 'recommendations': self._generate_density_recommendations(keyword_densities) + } + + def _build_title_with_keywords( + self, + app_name: str, + keywords: List[str], + max_length: int + ) -> Optional[str]: + """Build title combining app name and keywords within limit.""" + separators = [' - ', ': ', ' | '] + + for sep in separators: + for kw in keywords: + title = f"{app_name}{sep}{kw}" + if len(title) <= max_length: + return title + + return None + + def _optimize_short_description( + self, + app_info: Dict[str, Any], + target_keywords: List[str] + ) -> Dict[str, Any]: + """Optimize Google Play short description (80 chars).""" + max_length = self.limits['short_description'] + + # Focus on unique value proposition with primary keyword + unique_value = app_info.get('unique_value', '') + primary_keyword = target_keywords[0] if target_keywords else '' + + # Template: [Primary Keyword] - [Unique Value] + short_desc = f"{primary_keyword.title()} - {unique_value}"[:max_length] + + return { + 'short_description': short_desc, + 'length': len(short_desc), + 'remaining_chars': max_length - len(short_desc), + 'keywords_included': [primary_keyword] if primary_keyword in short_desc.lower() else [], + 'strategy': 'keyword_value_proposition' + } + + def _optimize_subtitle( + self, + app_info: Dict[str, Any], + target_keywords: List[str] + ) -> Dict[str, Any]: + """Optimize Apple App Store subtitle (30 chars).""" + max_length = self.limits['subtitle'] + + # Very concise - primary keyword or key feature + primary_keyword = target_keywords[0] if target_keywords else '' + key_feature = app_info.get('key_features', [''])[0] if app_info.get('key_features') else '' + + options = [ + primary_keyword[:max_length], + key_feature[:max_length], + f"{primary_keyword} App"[:max_length] + ] + + return { + 'subtitle_options': [opt for opt in options if opt], + 'max_length': max_length, + 'recommendation': options[0] if options else '' + } + + def _optimize_full_description( + self, + app_info: Dict[str, Any], + target_keywords: List[str] + ) -> Dict[str, Any]: + """Optimize full app description (4000 chars for both platforms).""" + max_length = self.limits.get('description', self.limits.get('full_description', 4000)) + + # Structure: Hook → Features → Benefits → Social Proof → CTA + sections = [] + + # Hook (with primary keyword) + primary_keyword = target_keywords[0] if target_keywords else '' + unique_value = app_info.get('unique_value', '') + hook = f"{unique_value} {primary_keyword.title()} that helps you achieve more.\n\n" + sections.append(hook) + + # Features (with keywords naturally integrated) + features = app_info.get('key_features', []) + if features: + sections.append("KEY FEATURES:\n") + for i, feature in enumerate(features[:5], 1): + # Integrate keywords naturally + feature_text = f"• {feature}" + if i <= len(target_keywords): + keyword = target_keywords[i-1] + if keyword.lower() not in feature.lower(): + feature_text = f"• {feature} with {keyword}" + sections.append(f"{feature_text}\n") + sections.append("\n") + + # Benefits + target_audience = app_info.get('target_audience', 'users') + sections.append(f"PERFECT FOR:\n{target_audience}\n\n") + + # Social proof placeholder + sections.append("WHY USERS LOVE US:\n") + sections.append("Join thousands of satisfied users who have transformed their workflow.\n\n") + + # CTA + sections.append("Download now and start experiencing the difference!") + + # Combine and validate length + full_description = "".join(sections) + if len(full_description) > max_length: + full_description = full_description[:max_length-3] + "..." + + # Calculate keyword density + density = self.calculate_keyword_density(full_description, target_keywords) + + return { + 'full_description': full_description, + 'length': len(full_description), + 'remaining_chars': max_length - len(full_description), + 'keyword_analysis': density, + 'structure': { + 'has_hook': True, + 'has_features': len(features) > 0, + 'has_benefits': True, + 'has_cta': True + } + } + + def _remove_plural_duplicates(self, keywords: List[str]) -> List[str]: + """Remove plural forms if singular exists.""" + deduplicated = [] + singular_set = set() + + for keyword in keywords: + if keyword.endswith('s') and len(keyword) > 1: + singular = keyword[:-1] + if singular not in singular_set: + deduplicated.append(singular) + singular_set.add(singular) + else: + if keyword not in singular_set: + deduplicated.append(keyword) + singular_set.add(keyword) + + return deduplicated + + def _build_keyword_field(self, keywords: List[str], max_length: int) -> str: + """Build comma-separated keyword field within character limit.""" + keyword_field = "" + + for keyword in keywords: + test_field = f"{keyword_field},{keyword}" if keyword_field else keyword + if len(test_field) <= max_length: + keyword_field = test_field + else: + break + + return keyword_field + + def _calculate_coverage(self, keywords: List[str], text: str) -> Dict[str, int]: + """Calculate how many keywords are covered in text.""" + text_lower = text.lower() + coverage = {} + + for keyword in keywords: + coverage[keyword] = text_lower.count(keyword.lower()) + + return coverage + + def _assess_density(self, density: float) -> str: + """Assess individual keyword density.""" + if density < 0.5: + return "too_low" + elif density <= 2.5: + return "optimal" + else: + return "too_high" + + def _assess_overall_density(self, density: float) -> str: + """Assess overall keyword density.""" + if density < 2: + return "Under-optimized: Consider adding more keyword variations" + elif density <= 5: + return "Optimal: Good keyword integration without stuffing" + elif density <= 8: + return "High: Approaching keyword stuffing - reduce keyword usage" + else: + return "Too High: Keyword stuffing detected - rewrite for natural flow" + + def _generate_density_recommendations( + self, + keyword_densities: Dict[str, Dict[str, Any]] + ) -> List[str]: + """Generate recommendations based on keyword density analysis.""" + recommendations = [] + + for keyword, data in keyword_densities.items(): + if data['status'] == 'too_low': + recommendations.append( + f"Increase usage of '{keyword}' - currently only {data['occurrences']} times" + ) + elif data['status'] == 'too_high': + recommendations.append( + f"Reduce usage of '{keyword}' - appears {data['occurrences']} times (keyword stuffing risk)" + ) + + if not recommendations: + recommendations.append("Keyword density is well-balanced") + + return recommendations + + def _recommend_title_option(self, options: List[Dict[str, Any]]) -> str: + """Recommend best title option based on strategy.""" + if not options: + return "No valid options available" + + # Prefer brand_plus_primary for established apps + for option in options: + if option['strategy'] == 'brand_plus_primary': + return f"Recommended: '{option['title']}' (Balance of brand and SEO)" + + # Fallback to first option + return f"Recommended: '{options[0]['title']}' ({options[0]['strategy']})" + + +def optimize_app_metadata( + platform: str, + app_info: Dict[str, Any], + target_keywords: List[str] +) -> Dict[str, Any]: + """ + Convenience function to optimize all metadata fields. + + Args: + platform: 'apple' or 'google' + app_info: App information dictionary + target_keywords: Target keywords list + + Returns: + Complete metadata optimization package + """ + optimizer = MetadataOptimizer(platform) + + return { + 'platform': platform, + 'title': optimizer.optimize_title( + app_info['name'], + target_keywords + ), + 'description': optimizer.optimize_description( + app_info, + target_keywords, + 'full' + ), + 'keyword_field': optimizer.optimize_keyword_field( + target_keywords + ) if platform == 'apple' else None + } diff --git a/.agents/skills/app-store-optimization/scripts/review_analyzer.py b/.agents/skills/app-store-optimization/scripts/review_analyzer.py new file mode 100644 index 0000000..4ce124d --- /dev/null +++ b/.agents/skills/app-store-optimization/scripts/review_analyzer.py @@ -0,0 +1,714 @@ +""" +Review analysis module for App Store Optimization. +Analyzes user reviews for sentiment, issues, and feature requests. +""" + +from typing import Dict, List, Any, Optional, Tuple +from collections import Counter +import re + + +class ReviewAnalyzer: + """Analyzes user reviews for actionable insights.""" + + # Sentiment keywords + POSITIVE_KEYWORDS = [ + 'great', 'awesome', 'excellent', 'amazing', 'love', 'best', 'perfect', + 'fantastic', 'wonderful', 'brilliant', 'outstanding', 'superb' + ] + + NEGATIVE_KEYWORDS = [ + 'bad', 'terrible', 'awful', 'horrible', 'hate', 'worst', 'useless', + 'broken', 'crash', 'bug', 'slow', 'disappointing', 'frustrating' + ] + + # Issue indicators + ISSUE_KEYWORDS = [ + 'crash', 'bug', 'error', 'broken', 'not working', 'doesnt work', + 'freezes', 'slow', 'laggy', 'glitch', 'problem', 'issue', 'fail' + ] + + # Feature request indicators + FEATURE_REQUEST_KEYWORDS = [ + 'wish', 'would be nice', 'should add', 'need', 'want', 'hope', + 'please add', 'missing', 'lacks', 'feature request' + ] + + def __init__(self, app_name: str): + """ + Initialize review analyzer. + + Args: + app_name: Name of the app + """ + self.app_name = app_name + self.reviews = [] + self.analysis_cache = {} + + def analyze_sentiment( + self, + reviews: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Analyze sentiment across reviews. + + Args: + reviews: List of review dicts with 'text', 'rating', 'date' + + Returns: + Sentiment analysis summary + """ + self.reviews = reviews + + sentiment_counts = { + 'positive': 0, + 'neutral': 0, + 'negative': 0 + } + + detailed_sentiments = [] + + for review in reviews: + text = review.get('text', '').lower() + rating = review.get('rating', 3) + + # Calculate sentiment score + sentiment_score = self._calculate_sentiment_score(text, rating) + sentiment_category = self._categorize_sentiment(sentiment_score) + + sentiment_counts[sentiment_category] += 1 + + detailed_sentiments.append({ + 'review_id': review.get('id', ''), + 'rating': rating, + 'sentiment_score': sentiment_score, + 'sentiment': sentiment_category, + 'text_preview': text[:100] + '...' if len(text) > 100 else text + }) + + # Calculate percentages + total = len(reviews) + sentiment_distribution = { + 'positive': round((sentiment_counts['positive'] / total) * 100, 1) if total > 0 else 0, + 'neutral': round((sentiment_counts['neutral'] / total) * 100, 1) if total > 0 else 0, + 'negative': round((sentiment_counts['negative'] / total) * 100, 1) if total > 0 else 0 + } + + # Calculate average rating + avg_rating = sum(r.get('rating', 0) for r in reviews) / total if total > 0 else 0 + + return { + 'total_reviews_analyzed': total, + 'average_rating': round(avg_rating, 2), + 'sentiment_distribution': sentiment_distribution, + 'sentiment_counts': sentiment_counts, + 'sentiment_trend': self._assess_sentiment_trend(sentiment_distribution), + 'detailed_sentiments': detailed_sentiments[:50] # Limit output + } + + def extract_common_themes( + self, + reviews: List[Dict[str, Any]], + min_mentions: int = 3 + ) -> Dict[str, Any]: + """ + Extract frequently mentioned themes and topics. + + Args: + reviews: List of review dicts + min_mentions: Minimum mentions to be considered common + + Returns: + Common themes analysis + """ + # Extract all words from reviews + all_words = [] + all_phrases = [] + + for review in reviews: + text = review.get('text', '').lower() + # Clean text + text = re.sub(r'[^\w\s]', ' ', text) + words = text.split() + + # Filter out common words + stop_words = { + 'the', 'and', 'for', 'with', 'this', 'that', 'from', 'have', + 'app', 'apps', 'very', 'really', 'just', 'but', 'not', 'you' + } + words = [w for w in words if w not in stop_words and len(w) > 3] + + all_words.extend(words) + + # Extract 2-3 word phrases + for i in range(len(words) - 1): + phrase = f"{words[i]} {words[i+1]}" + all_phrases.append(phrase) + + # Count frequency + word_freq = Counter(all_words) + phrase_freq = Counter(all_phrases) + + # Filter by min_mentions + common_words = [ + {'word': word, 'mentions': count} + for word, count in word_freq.most_common(30) + if count >= min_mentions + ] + + common_phrases = [ + {'phrase': phrase, 'mentions': count} + for phrase, count in phrase_freq.most_common(20) + if count >= min_mentions + ] + + # Categorize themes + themes = self._categorize_themes(common_words, common_phrases) + + return { + 'common_words': common_words, + 'common_phrases': common_phrases, + 'identified_themes': themes, + 'insights': self._generate_theme_insights(themes) + } + + def identify_issues( + self, + reviews: List[Dict[str, Any]], + rating_threshold: int = 3 + ) -> Dict[str, Any]: + """ + Identify bugs, crashes, and other issues from reviews. + + Args: + reviews: List of review dicts + rating_threshold: Only analyze reviews at or below this rating + + Returns: + Issue identification report + """ + issues = [] + + for review in reviews: + rating = review.get('rating', 5) + if rating > rating_threshold: + continue + + text = review.get('text', '').lower() + + # Check for issue keywords + mentioned_issues = [] + for keyword in self.ISSUE_KEYWORDS: + if keyword in text: + mentioned_issues.append(keyword) + + if mentioned_issues: + issues.append({ + 'review_id': review.get('id', ''), + 'rating': rating, + 'date': review.get('date', ''), + 'issue_keywords': mentioned_issues, + 'text': text[:200] + '...' if len(text) > 200 else text + }) + + # Group by issue type + issue_frequency = Counter() + for issue in issues: + for keyword in issue['issue_keywords']: + issue_frequency[keyword] += 1 + + # Categorize issues + categorized_issues = self._categorize_issues(issues) + + # Calculate issue severity + severity_scores = self._calculate_issue_severity( + categorized_issues, + len(reviews) + ) + + return { + 'total_issues_found': len(issues), + 'issue_frequency': dict(issue_frequency.most_common(15)), + 'categorized_issues': categorized_issues, + 'severity_scores': severity_scores, + 'top_issues': self._rank_issues_by_severity(severity_scores), + 'recommendations': self._generate_issue_recommendations( + categorized_issues, + severity_scores + ) + } + + def find_feature_requests( + self, + reviews: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Extract feature requests and desired improvements. + + Args: + reviews: List of review dicts + + Returns: + Feature request analysis + """ + feature_requests = [] + + for review in reviews: + text = review.get('text', '').lower() + rating = review.get('rating', 3) + + # Check for feature request indicators + is_feature_request = any( + keyword in text + for keyword in self.FEATURE_REQUEST_KEYWORDS + ) + + if is_feature_request: + # Extract the specific request + request_text = self._extract_feature_request_text(text) + + feature_requests.append({ + 'review_id': review.get('id', ''), + 'rating': rating, + 'date': review.get('date', ''), + 'request_text': request_text, + 'full_review': text[:200] + '...' if len(text) > 200 else text + }) + + # Cluster similar requests + clustered_requests = self._cluster_feature_requests(feature_requests) + + # Prioritize based on frequency and rating context + prioritized_requests = self._prioritize_feature_requests(clustered_requests) + + return { + 'total_feature_requests': len(feature_requests), + 'clustered_requests': clustered_requests, + 'prioritized_requests': prioritized_requests, + 'implementation_recommendations': self._generate_feature_recommendations( + prioritized_requests + ) + } + + def track_sentiment_trends( + self, + reviews_by_period: Dict[str, List[Dict[str, Any]]] + ) -> Dict[str, Any]: + """ + Track sentiment changes over time. + + Args: + reviews_by_period: Dict of period_name: reviews + + Returns: + Trend analysis + """ + trends = [] + + for period, reviews in reviews_by_period.items(): + sentiment = self.analyze_sentiment(reviews) + + trends.append({ + 'period': period, + 'total_reviews': len(reviews), + 'average_rating': sentiment['average_rating'], + 'positive_percentage': sentiment['sentiment_distribution']['positive'], + 'negative_percentage': sentiment['sentiment_distribution']['negative'] + }) + + # Calculate trend direction + if len(trends) >= 2: + first_period = trends[0] + last_period = trends[-1] + + rating_change = last_period['average_rating'] - first_period['average_rating'] + sentiment_change = last_period['positive_percentage'] - first_period['positive_percentage'] + + trend_direction = self._determine_trend_direction( + rating_change, + sentiment_change + ) + else: + trend_direction = 'insufficient_data' + + return { + 'periods_analyzed': len(trends), + 'trend_data': trends, + 'trend_direction': trend_direction, + 'insights': self._generate_trend_insights(trends, trend_direction) + } + + def generate_response_templates( + self, + issue_category: str + ) -> List[Dict[str, str]]: + """ + Generate response templates for common review scenarios. + + Args: + issue_category: Category of issue ('crash', 'feature_request', 'positive', etc.) + + Returns: + Response templates + """ + templates = { + 'crash': [ + { + 'scenario': 'App crash reported', + 'template': "Thank you for bringing this to our attention. We're sorry you experienced a crash. " + "Our team is investigating this issue. Could you please share more details about when " + "this occurred (device model, iOS/Android version) by contacting support@[company].com? " + "We're committed to fixing this quickly." + }, + { + 'scenario': 'Crash already fixed', + 'template': "Thank you for your feedback. We've identified and fixed this crash issue in version [X.X]. " + "Please update to the latest version. If the problem persists, please reach out to " + "support@[company].com and we'll help you directly." + } + ], + 'bug': [ + { + 'scenario': 'Bug reported', + 'template': "Thanks for reporting this bug. We take these issues seriously. Our team is looking into it " + "and we'll have a fix in an upcoming update. We appreciate your patience and will notify you " + "when it's resolved." + } + ], + 'feature_request': [ + { + 'scenario': 'Feature request received', + 'template': "Thank you for this suggestion! We're always looking to improve [app_name]. We've added your " + "request to our roadmap and will consider it for a future update. Follow us @[social] for " + "updates on new features." + }, + { + 'scenario': 'Feature already planned', + 'template': "Great news! This feature is already on our roadmap and we're working on it. Stay tuned for " + "updates in the coming months. Thanks for your feedback!" + } + ], + 'positive': [ + { + 'scenario': 'Positive review', + 'template': "Thank you so much for your kind words! We're thrilled that you're enjoying [app_name]. " + "Reviews like yours motivate our team to keep improving. If you ever have suggestions, " + "we'd love to hear them!" + } + ], + 'negative_general': [ + { + 'scenario': 'General complaint', + 'template': "We're sorry to hear you're not satisfied with your experience. We'd like to make this right. " + "Please contact us at support@[company].com so we can understand the issue better and help " + "you directly. Thank you for giving us a chance to improve." + } + ] + } + + return templates.get(issue_category, templates['negative_general']) + + def _calculate_sentiment_score(self, text: str, rating: int) -> float: + """Calculate sentiment score (-1 to 1).""" + # Start with rating-based score + rating_score = (rating - 3) / 2 # Convert 1-5 to -1 to 1 + + # Adjust based on text sentiment + positive_count = sum(1 for keyword in self.POSITIVE_KEYWORDS if keyword in text) + negative_count = sum(1 for keyword in self.NEGATIVE_KEYWORDS if keyword in text) + + text_score = (positive_count - negative_count) / 10 # Normalize + + # Weighted average (60% rating, 40% text) + final_score = (rating_score * 0.6) + (text_score * 0.4) + + return max(min(final_score, 1.0), -1.0) + + def _categorize_sentiment(self, score: float) -> str: + """Categorize sentiment score.""" + if score > 0.3: + return 'positive' + elif score < -0.3: + return 'negative' + else: + return 'neutral' + + def _assess_sentiment_trend(self, distribution: Dict[str, float]) -> str: + """Assess overall sentiment trend.""" + positive = distribution['positive'] + negative = distribution['negative'] + + if positive > 70: + return 'very_positive' + elif positive > 50: + return 'positive' + elif negative > 30: + return 'concerning' + elif negative > 50: + return 'critical' + else: + return 'mixed' + + def _categorize_themes( + self, + common_words: List[Dict[str, Any]], + common_phrases: List[Dict[str, Any]] + ) -> Dict[str, List[str]]: + """Categorize themes from words and phrases.""" + themes = { + 'features': [], + 'performance': [], + 'usability': [], + 'support': [], + 'pricing': [] + } + + # Keywords for each category + feature_keywords = {'feature', 'functionality', 'option', 'tool'} + performance_keywords = {'fast', 'slow', 'crash', 'lag', 'speed', 'performance'} + usability_keywords = {'easy', 'difficult', 'intuitive', 'confusing', 'interface', 'design'} + support_keywords = {'support', 'help', 'customer', 'service', 'response'} + pricing_keywords = {'price', 'cost', 'expensive', 'cheap', 'subscription', 'free'} + + for word_data in common_words: + word = word_data['word'] + if any(kw in word for kw in feature_keywords): + themes['features'].append(word) + elif any(kw in word for kw in performance_keywords): + themes['performance'].append(word) + elif any(kw in word for kw in usability_keywords): + themes['usability'].append(word) + elif any(kw in word for kw in support_keywords): + themes['support'].append(word) + elif any(kw in word for kw in pricing_keywords): + themes['pricing'].append(word) + + return {k: v for k, v in themes.items() if v} # Remove empty categories + + def _generate_theme_insights(self, themes: Dict[str, List[str]]) -> List[str]: + """Generate insights from themes.""" + insights = [] + + for category, keywords in themes.items(): + if keywords: + insights.append( + f"{category.title()}: Users frequently mention {', '.join(keywords[:3])}" + ) + + return insights[:5] + + def _categorize_issues(self, issues: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]: + """Categorize issues by type.""" + categories = { + 'crashes': [], + 'bugs': [], + 'performance': [], + 'compatibility': [] + } + + for issue in issues: + keywords = issue['issue_keywords'] + + if 'crash' in keywords or 'freezes' in keywords: + categories['crashes'].append(issue) + elif 'bug' in keywords or 'error' in keywords or 'broken' in keywords: + categories['bugs'].append(issue) + elif 'slow' in keywords or 'laggy' in keywords: + categories['performance'].append(issue) + else: + categories['compatibility'].append(issue) + + return {k: v for k, v in categories.items() if v} + + def _calculate_issue_severity( + self, + categorized_issues: Dict[str, List[Dict[str, Any]]], + total_reviews: int + ) -> Dict[str, Dict[str, Any]]: + """Calculate severity scores for each issue category.""" + severity_scores = {} + + for category, issues in categorized_issues.items(): + count = len(issues) + percentage = (count / total_reviews) * 100 if total_reviews > 0 else 0 + + # Calculate average rating of affected reviews + avg_rating = sum(i['rating'] for i in issues) / count if count > 0 else 0 + + # Severity score (0-100) + severity = min((percentage * 10) + ((5 - avg_rating) * 10), 100) + + severity_scores[category] = { + 'count': count, + 'percentage': round(percentage, 2), + 'average_rating': round(avg_rating, 2), + 'severity_score': round(severity, 1), + 'priority': 'critical' if severity > 70 else ('high' if severity > 40 else 'medium') + } + + return severity_scores + + def _rank_issues_by_severity( + self, + severity_scores: Dict[str, Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Rank issues by severity score.""" + ranked = sorted( + [{'category': cat, **data} for cat, data in severity_scores.items()], + key=lambda x: x['severity_score'], + reverse=True + ) + return ranked + + def _generate_issue_recommendations( + self, + categorized_issues: Dict[str, List[Dict[str, Any]]], + severity_scores: Dict[str, Dict[str, Any]] + ) -> List[str]: + """Generate recommendations for addressing issues.""" + recommendations = [] + + for category, score_data in severity_scores.items(): + if score_data['priority'] == 'critical': + recommendations.append( + f"URGENT: Address {category} issues immediately - affecting {score_data['percentage']}% of reviews" + ) + elif score_data['priority'] == 'high': + recommendations.append( + f"HIGH PRIORITY: Focus on {category} issues in next update" + ) + + return recommendations + + def _extract_feature_request_text(self, text: str) -> str: + """Extract the specific feature request from review text.""" + # Simple extraction - find sentence with feature request keywords + sentences = text.split('.') + for sentence in sentences: + if any(keyword in sentence for keyword in self.FEATURE_REQUEST_KEYWORDS): + return sentence.strip() + return text[:100] # Fallback + + def _cluster_feature_requests( + self, + feature_requests: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Cluster similar feature requests.""" + # Simplified clustering - group by common keywords + clusters = {} + + for request in feature_requests: + text = request['request_text'].lower() + # Extract key words + words = [w for w in text.split() if len(w) > 4] + + # Try to find matching cluster + matched = False + for cluster_key in clusters: + if any(word in cluster_key for word in words[:3]): + clusters[cluster_key].append(request) + matched = True + break + + if not matched and words: + cluster_key = ' '.join(words[:2]) + clusters[cluster_key] = [request] + + return [ + {'feature_theme': theme, 'request_count': len(requests), 'examples': requests[:3]} + for theme, requests in clusters.items() + ] + + def _prioritize_feature_requests( + self, + clustered_requests: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Prioritize feature requests by frequency.""" + return sorted( + clustered_requests, + key=lambda x: x['request_count'], + reverse=True + )[:10] + + def _generate_feature_recommendations( + self, + prioritized_requests: List[Dict[str, Any]] + ) -> List[str]: + """Generate recommendations for feature requests.""" + recommendations = [] + + if prioritized_requests: + top_request = prioritized_requests[0] + recommendations.append( + f"Most requested feature: {top_request['feature_theme']} " + f"({top_request['request_count']} mentions) - consider for next major release" + ) + + if len(prioritized_requests) > 1: + recommendations.append( + f"Also consider: {prioritized_requests[1]['feature_theme']}" + ) + + return recommendations + + def _determine_trend_direction( + self, + rating_change: float, + sentiment_change: float + ) -> str: + """Determine overall trend direction.""" + if rating_change > 0.2 and sentiment_change > 5: + return 'improving' + elif rating_change < -0.2 and sentiment_change < -5: + return 'declining' + else: + return 'stable' + + def _generate_trend_insights( + self, + trends: List[Dict[str, Any]], + trend_direction: str + ) -> List[str]: + """Generate insights from trend analysis.""" + insights = [] + + if trend_direction == 'improving': + insights.append("Positive trend: User satisfaction is increasing over time") + elif trend_direction == 'declining': + insights.append("WARNING: User satisfaction is declining - immediate action needed") + else: + insights.append("Sentiment is stable - maintain current quality") + + # Review velocity insight + if len(trends) >= 2: + recent_reviews = trends[-1]['total_reviews'] + previous_reviews = trends[-2]['total_reviews'] + + if recent_reviews > previous_reviews * 1.5: + insights.append("Review volume increasing - growing user base or recent controversy") + + return insights + + +def analyze_reviews( + app_name: str, + reviews: List[Dict[str, Any]] +) -> Dict[str, Any]: + """ + Convenience function to perform comprehensive review analysis. + + Args: + app_name: App name + reviews: List of review dictionaries + + Returns: + Complete review analysis + """ + analyzer = ReviewAnalyzer(app_name) + + return { + 'sentiment_analysis': analyzer.analyze_sentiment(reviews), + 'common_themes': analyzer.extract_common_themes(reviews), + 'issues_identified': analyzer.identify_issues(reviews), + 'feature_requests': analyzer.find_feature_requests(reviews) + } diff --git a/.claude/skills/app-store-optimization b/.claude/skills/app-store-optimization new file mode 120000 index 0000000..d69a57b --- /dev/null +++ b/.claude/skills/app-store-optimization @@ -0,0 +1 @@ +../../.agents/skills/app-store-optimization \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-22T16-55-47-014Z.yml b/.playwright-mcp/page-2026-06-22T16-55-47-014Z.yml new file mode 100644 index 0000000..b9e3459 --- /dev/null +++ b/.playwright-mcp/page-2026-06-22T16-55-47-014Z.yml @@ -0,0 +1,95 @@ +- generic [active] [ref=e1]: + - navigation [ref=e2]: + - generic [ref=e3]: + - link "opencode /mobile" [ref=e4] [cursor=pointer]: + - /url: / + - text: opencode + - generic [ref=e5]: /mobile + - generic [ref=e6]: + - link "Docs" [ref=e7] [cursor=pointer]: + - /url: https://opencode.ai + - link "GitHub" [ref=e8] [cursor=pointer]: + - /url: https://github.com/dzianisv/opencode-mobile + - link "Beta" [ref=e9] [cursor=pointer]: + - /url: /beta + - link "Download" [ref=e10] [cursor=pointer]: + - /url: https://play.google.com/store/apps/details?id=cc.agentlabs.opencode + - main [ref=e11]: + - generic [ref=e13]: + - heading "The open source AI coding agent, on the go" [level=1] [ref=e14]: + - text: The open source AI coding agent, + - text: on the go + - paragraph [ref=e15]: + - text: Free mobile client for + - link "opencode" [ref=e16] [cursor=pointer]: + - /url: https://opencode.ai + - text: . Connect to your own server, stream diffs in real time, and review AI changes from anywhere. + - generic [ref=e17]: + - link "Download for Android" [ref=e18] [cursor=pointer]: + - /url: https://play.google.com/store/apps/details?id=cc.agentlabs.opencode + - link "GitHub Releases" [ref=e19] [cursor=pointer]: + - /url: https://github.com/dzianisv/opencode-mobile/releases + - link "F-Droid" [ref=e20] [cursor=pointer]: + - /url: https://dzianisv.github.io/opencode-mobile/fdroid/repo + - paragraph [ref=e21]: Android 8.0+ · iOS coming soon + - generic [ref=e24]: + - img "OpenCode Mobile screenshot 1" [ref=e26] + - img "OpenCode Mobile screenshot 2" [ref=e28] + - img "OpenCode Mobile screenshot 3" [ref=e30] + - generic [ref=e32]: + - heading "What you get" [level=2] [ref=e33] + - generic [ref=e34]: + - generic [ref=e35]: + - generic [ref=e36]: "[*]" + - generic [ref=e37]: + - heading "Multi-server" [level=3] [ref=e38] + - paragraph [ref=e39]: Switch between local, cloud, and team servers. Credentials stay on device. + - generic [ref=e40]: + - generic [ref=e41]: "[*]" + - generic [ref=e42]: + - heading "Streaming diffs" [level=3] [ref=e43] + - paragraph [ref=e44]: Watch AI changes in real time with syntax-highlighted diffs. + - generic [ref=e45]: + - generic [ref=e46]: "[*]" + - generic [ref=e47]: + - heading "Biometric unlock" [level=3] [ref=e48] + - paragraph [ref=e49]: Fingerprint or Face ID. No password typing. + - generic [ref=e50]: + - generic [ref=e51]: "[*]" + - generic [ref=e52]: + - heading "Any model" [level=3] [ref=e53] + - paragraph [ref=e54]: Claude, GPT, Gemini, local models — whatever your server runs. + - generic [ref=e55]: + - generic [ref=e56]: "[*]" + - generic [ref=e57]: + - heading "Session management" [level=3] [ref=e58] + - paragraph [ref=e59]: Create, resume, and switch between coding sessions. + - generic [ref=e60]: + - generic [ref=e61]: "[*]" + - generic [ref=e62]: + - heading "MIT licensed" [level=3] [ref=e63] + - paragraph [ref=e64]: Fully open source. Audit, fork, self-build. No telemetry. + - generic [ref=e67]: + - generic [ref=e68]: + - heading "100% open source" [level=2] [ref=e69] + - paragraph [ref=e70]: MIT-licensed. Audit the code, self-build, contribute features, or fork it. No telemetry without consent. No hidden servers. Your data stays yours. + - link "View on GitHub" [ref=e71] [cursor=pointer]: + - /url: https://github.com/dzianisv/opencode-mobile + - generic [ref=e73]: + - heading "Join the beta" [level=2] [ref=e74] + - paragraph [ref=e75]: We need 20 testers to unlock the public Google Play release. Sign up and get early access today. + - link "Join the closed beta" [ref=e76] [cursor=pointer]: + - /url: /beta + - contentinfo [ref=e77]: + - generic [ref=e79]: + - generic [ref=e80]: + - link "GitHub" [ref=e81] [cursor=pointer]: + - /url: https://github.com/dzianisv/opencode-mobile + - link "Docs" [ref=e82] [cursor=pointer]: + - /url: https://opencode.ai + - link "Privacy" [ref=e83] [cursor=pointer]: + - /url: /privacy + - link "Terms" [ref=e84] [cursor=pointer]: + - /url: /terms + - paragraph [ref=e85]: © 2026 VIBE TECHNOLOGIES, LLC + - alert [ref=e86] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-22T16-55-55-587Z.yml b/.playwright-mcp/page-2026-06-22T16-55-55-587Z.yml new file mode 100644 index 0000000..a137981 --- /dev/null +++ b/.playwright-mcp/page-2026-06-22T16-55-55-587Z.yml @@ -0,0 +1,74 @@ +- generic [active] [ref=e1]: + - navigation [ref=e2]: + - generic [ref=e3]: + - link "opencode /mobile" [ref=e4] [cursor=pointer]: + - /url: / + - text: opencode + - generic [ref=e5]: /mobile + - generic [ref=e6]: + - link "Docs" [ref=e7] [cursor=pointer]: + - /url: https://opencode.ai + - link "GitHub" [ref=e8] [cursor=pointer]: + - /url: https://github.com/dzianisv/opencode-mobile + - link "Beta" [ref=e9] [cursor=pointer]: + - /url: /beta + - link "Download" [ref=e10] [cursor=pointer]: + - /url: https://play.google.com/store/apps/details?id=cc.agentlabs.opencode + - main [ref=e11]: + - generic [ref=e13]: + - paragraph [ref=e14]: Closed beta · Limited spots + - heading "Get early access" [level=1] [ref=e15] + - paragraph [ref=e16]: Sign up to join the OpenCode Mobile closed beta on Google Play. We need 20 testers to unlock the public release. + - generic [ref=e17]: + - textbox "your@gmail.com" [ref=e18] + - button "Join beta" [ref=e19] [cursor=pointer] + - paragraph [ref=e20]: Use the Gmail linked to your Google Play account. No spam, no commitment. + - generic [ref=e22]: + - heading "What beta testers get" [level=2] [ref=e23] + - generic [ref=e24]: + - generic [ref=e25]: + - generic [ref=e26]: "[*]" + - generic [ref=e27]: + - heading "Early access" [level=3] [ref=e28] + - paragraph [ref=e29]: Install from Google Play before anyone else. Get updates as we ship them. + - generic [ref=e30]: + - generic [ref=e31]: "[*]" + - generic [ref=e32]: + - heading "Direct feedback channel" [level=3] [ref=e33] + - paragraph [ref=e34]: Report bugs and request features directly to the dev team. + - generic [ref=e35]: + - generic [ref=e36]: "[*]" + - generic [ref=e37]: + - heading "Shape the product" [level=3] [ref=e38] + - paragraph [ref=e39]: Your input decides what we build next. Beta feedback has outsized impact. + - generic [ref=e41]: + - heading "FAQ" [level=2] [ref=e42] + - generic [ref=e43]: + - generic [ref=e44]: + - heading "What is OpenCode Mobile?" [level=3] [ref=e45] + - paragraph [ref=e46]: A free, open-source mobile client for the opencode AI coding agent. Connect to your own server and code from anywhere. + - generic [ref=e47]: + - heading "Why do you need my email?" [level=3] [ref=e48] + - paragraph [ref=e49]: Google Play requires us to add tester email addresses explicitly. We use the same Gmail you use on your Android device. + - generic [ref=e50]: + - heading "Is it free?" [level=3] [ref=e51] + - paragraph [ref=e52]: Yes. Free and open source, MIT license. No in-app purchases, no ads. + - generic [ref=e53]: + - heading "When does the public release happen?" [level=3] [ref=e54] + - paragraph [ref=e55]: Google requires 14 days of closed testing with at least 20 testers. The sooner we hit 20, the sooner we launch. + - generic [ref=e56]: + - heading "What Android version?" [level=3] [ref=e57] + - paragraph [ref=e58]: Android 8.0 (API 26) or newer. + - contentinfo [ref=e59]: + - generic [ref=e61]: + - generic [ref=e62]: + - link "GitHub" [ref=e63] [cursor=pointer]: + - /url: https://github.com/dzianisv/opencode-mobile + - link "Docs" [ref=e64] [cursor=pointer]: + - /url: https://opencode.ai + - link "Privacy" [ref=e65] [cursor=pointer]: + - /url: /privacy + - link "Terms" [ref=e66] [cursor=pointer]: + - /url: /terms + - paragraph [ref=e67]: © 2026 VIBE TECHNOLOGIES, LLC + - alert [ref=e68] \ No newline at end of file diff --git a/.supervisor/56dcc5bc-b7ab-4297-8058-e7f100764903_attempts.json b/.supervisor/56dcc5bc-b7ab-4297-8058-e7f100764903_attempts.json new file mode 100644 index 0000000..70b9367 --- /dev/null +++ b/.supervisor/56dcc5bc-b7ab-4297-8058-e7f100764903_attempts.json @@ -0,0 +1 @@ +{"count":3,"last_iso":"2026-06-21T02:44:56.605Z"} \ No newline at end of file diff --git a/.supervisor/69e7ce6b-bdc8-45b0-b7e4-9fef25893bf5_attempts.json b/.supervisor/69e7ce6b-bdc8-45b0-b7e4-9fef25893bf5_attempts.json new file mode 100644 index 0000000..844ec4d --- /dev/null +++ b/.supervisor/69e7ce6b-bdc8-45b0-b7e4-9fef25893bf5_attempts.json @@ -0,0 +1 @@ +{"count":1,"last_iso":"2026-06-20T09:50:09.188Z"} \ No newline at end of file diff --git a/.supervisor/current_session b/.supervisor/current_session new file mode 100644 index 0000000..c8608ab --- /dev/null +++ b/.supervisor/current_session @@ -0,0 +1 @@ +56dcc5bc-b7ab-4297-8058-e7f100764903 \ No newline at end of file diff --git a/.supervisor/verdict_56dcc5bc-b7ab-4297-8058-e7f100764903.json b/.supervisor/verdict_56dcc5bc-b7ab-4297-8058-e7f100764903.json new file mode 100644 index 0000000..ee210fd --- /dev/null +++ b/.supervisor/verdict_56dcc5bc-b7ab-4297-8058-e7f100764903.json @@ -0,0 +1,12 @@ +{ + "session_id": "56dcc5bc-b7ab-4297-8058-e7f100764903", + "attempt": 3, + "timestamp": "2026-06-21T02:44:56.605Z", + "injected": true, + "status": "in_progress", + "confidence": 0.35, + "complete": false, + "severity": "HIGH", + "reason": "Missing required workflow steps", + "feedback_reason": "assessment_continue" +} \ No newline at end of file diff --git a/.supervisor/verdict_69e7ce6b-bdc8-45b0-b7e4-9fef25893bf5.json b/.supervisor/verdict_69e7ce6b-bdc8-45b0-b7e4-9fef25893bf5.json new file mode 100644 index 0000000..638321d --- /dev/null +++ b/.supervisor/verdict_69e7ce6b-bdc8-45b0-b7e4-9fef25893bf5.json @@ -0,0 +1,12 @@ +{ + "session_id": "69e7ce6b-bdc8-45b0-b7e4-9fef25893bf5", + "attempt": 1, + "timestamp": "2026-06-20T09:50:09.187Z", + "injected": true, + "status": "in_progress", + "confidence": 0.85, + "complete": false, + "severity": "HIGH", + "reason": "Missing required workflow steps", + "feedback_reason": "assessment_continue" +} \ No newline at end of file diff --git a/scripts/android-cua-smoke.py b/scripts/android-cua-smoke.py index 51771a1..fac5b96 100755 --- a/scripts/android-cua-smoke.py +++ b/scripts/android-cua-smoke.py @@ -443,7 +443,9 @@ Rules: - Coordinates are in pixels relative to the screenshot dimensions. - IMPORTANT: In this app, pressing "enter" inserts a newline — it does NOT send the message. To send a message use {"type": "send"} which auto-locates and taps the send/arrow button. - After typing your message, press "back" to dismiss the keyboard, then use {"type": "send"}. + IMPORTANT: ADB's "input text" command does NOT show the on-screen keyboard. + Do NOT press "back" after typing — it will navigate away from the session instead of dismissing the keyboard. + Just type your message, then use {"type": "send"} directly. - Be efficient: skip unnecessary waits, tap directly on visible targets. - When the goal is fully achieved respond with {"type": "done", "summary": "..."}. - If genuinely stuck after 5+ attempts on the same element respond with {"type": "fail", ...}. @@ -719,7 +721,7 @@ def run_onboarding_showcase( f"You are inside a new OpenCode session (chat view with a text input at the bottom). " f"Tap the text input field. " f"Type this exact message: {TYPESCRIPT_TASK!r} " - "Press back to dismiss the keyboard. " + "Do NOT press back (it navigates away). " "Use the send action to submit. " "After sending, wait and watch — opencode will show tool calls and file writes as it works. " "Wait up to 90 seconds total for the session to go idle/complete " @@ -788,8 +790,8 @@ SMOKE_SCENARIOS = [ "goal": ( "You see the OpenCode mobile app. Tap the '+' button (top-right) to create a new session. " "Tap the text input at the bottom. " - f"Type this exact task: {PYTHON_CODING_TASK!r} " - "Press back to dismiss the keyboard. " + f"Type this exact task: {PYTHON_CODING_TASK!r} " + "Do NOT press back (it navigates away). " "Use the send action to submit the task. " "After sending, wait and watch — opencode will think and then produce code. " "Wait up to 120 seconds total for the session to complete "