diff --git a/antigravity-awesome-skills/.claude-plugin/marketplace.json b/antigravity-awesome-skills/.claude-plugin/marketplace.json index c8c09b8a..9b558f5c 100644 --- a/antigravity-awesome-skills/.claude-plugin/marketplace.json +++ b/antigravity-awesome-skills/.claude-plugin/marketplace.json @@ -6,12 +6,12 @@ }, "metadata": { "description": "Claude Code marketplace entries for the plugin-safe Antigravity Awesome Skills library and its compatible editorial bundles.", - "version": "13.7.0" + "version": "13.9.0" }, "plugins": [ { "name": "antigravity-awesome-skills", - "version": "13.7.0", + "version": "13.9.0", "description": "Expose the plugin-safe Claude Code subset of Antigravity Awesome Skills through a single marketplace entry.", "author": { "name": "sickn33 and contributors", @@ -31,7 +31,7 @@ }, { "name": "antigravity-bundle-essentials", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Essentials\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -51,7 +51,7 @@ }, { "name": "antigravity-bundle-security-engineer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Security Engineer\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -71,7 +71,7 @@ }, { "name": "antigravity-bundle-security-developer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Security Developer\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -91,7 +91,7 @@ }, { "name": "antigravity-bundle-web-wizard", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Web Wizard\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -111,7 +111,7 @@ }, { "name": "antigravity-bundle-web-designer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Web Designer\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -131,7 +131,7 @@ }, { "name": "antigravity-bundle-full-stack-developer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Full-Stack Developer\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -151,7 +151,7 @@ }, { "name": "antigravity-bundle-agent-architect", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Agent Architect\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -171,7 +171,7 @@ }, { "name": "antigravity-bundle-llm-application-developer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"LLM Application Developer\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -191,7 +191,7 @@ }, { "name": "antigravity-bundle-indie-game-dev", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Indie Game Dev\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -211,7 +211,7 @@ }, { "name": "antigravity-bundle-python-pro", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Python Pro\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -231,7 +231,7 @@ }, { "name": "antigravity-bundle-typescript-javascript", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"TypeScript & JavaScript\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -251,7 +251,7 @@ }, { "name": "antigravity-bundle-systems-programming", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Systems Programming\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -271,7 +271,7 @@ }, { "name": "antigravity-bundle-startup-founder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Startup Founder\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -291,7 +291,7 @@ }, { "name": "antigravity-bundle-business-analyst", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Business Analyst\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -311,7 +311,7 @@ }, { "name": "antigravity-bundle-marketing-growth", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Marketing & Growth\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -331,7 +331,7 @@ }, { "name": "antigravity-bundle-devops-cloud", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"DevOps & Cloud\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -351,7 +351,7 @@ }, { "name": "antigravity-bundle-observability-monitoring", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Observability & Monitoring\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -371,7 +371,7 @@ }, { "name": "antigravity-bundle-data-analytics", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Data & Analytics\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -391,7 +391,7 @@ }, { "name": "antigravity-bundle-data-engineering", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Data Engineering\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -411,7 +411,7 @@ }, { "name": "antigravity-bundle-creative-director", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Creative Director\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -431,7 +431,7 @@ }, { "name": "antigravity-bundle-qa-testing", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"QA & Testing\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -451,7 +451,7 @@ }, { "name": "antigravity-bundle-aas-web-app-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Web App Builder\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -471,7 +471,7 @@ }, { "name": "antigravity-bundle-aas-product-design-studio", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Product Design Studio\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -491,7 +491,7 @@ }, { "name": "antigravity-bundle-aas-security-engineer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Security Engineer\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -511,7 +511,7 @@ }, { "name": "antigravity-bundle-aas-secure-app-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Secure App Builder\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -531,7 +531,7 @@ }, { "name": "antigravity-bundle-aas-documents-presentations", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Documents & Presentations\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -551,7 +551,7 @@ }, { "name": "antigravity-bundle-aas-data-analytics", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Data Analytics\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -571,7 +571,7 @@ }, { "name": "antigravity-bundle-aas-agent-mcp-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Agent & MCP Builder\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -591,7 +591,7 @@ }, { "name": "antigravity-bundle-aas-oss-maintainer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS OSS Maintainer\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -611,7 +611,7 @@ }, { "name": "antigravity-bundle-aas-qa-test-automation", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS QA & Test Automation\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -631,7 +631,7 @@ }, { "name": "antigravity-bundle-aas-devops-cloud", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS DevOps & Cloud\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -651,7 +651,7 @@ }, { "name": "antigravity-bundle-aas-marketing-seo-growth", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Marketing, SEO & Growth\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -671,7 +671,7 @@ }, { "name": "antigravity-bundle-aas-automation-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Automation Builder\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -691,7 +691,7 @@ }, { "name": "antigravity-bundle-aas-observability-ir", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Observability IR\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -711,7 +711,7 @@ }, { "name": "antigravity-bundle-aas-python-api-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Python API Builder\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -731,7 +731,7 @@ }, { "name": "antigravity-bundle-aas-mobile-app-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Mobile App Builder\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -751,7 +751,7 @@ }, { "name": "antigravity-bundle-mobile-developer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Mobile Developer\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -771,7 +771,7 @@ }, { "name": "antigravity-bundle-integration-apis", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Integration & APIs\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -791,7 +791,7 @@ }, { "name": "antigravity-bundle-architecture-design", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Architecture & Design\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -811,7 +811,7 @@ }, { "name": "antigravity-bundle-ddd-evented-architecture", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"DDD & Evented Architecture\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -831,7 +831,7 @@ }, { "name": "antigravity-bundle-automation-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Automation Builder\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -851,7 +851,7 @@ }, { "name": "antigravity-bundle-revops-crm-automation", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"RevOps & CRM Automation\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -871,7 +871,7 @@ }, { "name": "antigravity-bundle-commerce-payments", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Commerce & Payments\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -891,7 +891,7 @@ }, { "name": "antigravity-bundle-odoo-erp", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Odoo ERP\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -911,7 +911,7 @@ }, { "name": "antigravity-bundle-azure-ai-cloud", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Azure AI & Cloud\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -931,7 +931,7 @@ }, { "name": "antigravity-bundle-expo-react-native", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Expo & React Native\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -951,7 +951,7 @@ }, { "name": "antigravity-bundle-apple-platform-design", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Apple Platform Design\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -971,7 +971,7 @@ }, { "name": "antigravity-bundle-makepad-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Makepad Builder\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -991,7 +991,7 @@ }, { "name": "antigravity-bundle-seo-specialist", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"SEO Specialist\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -1011,7 +1011,7 @@ }, { "name": "antigravity-bundle-documents-presentations", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Documents & Presentations\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -1031,7 +1031,7 @@ }, { "name": "antigravity-bundle-oss-maintainer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"OSS Maintainer\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -1051,7 +1051,7 @@ }, { "name": "antigravity-bundle-aas-accessibility-inclusive-ux", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Accessibility & Inclusive UX\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -1071,7 +1071,7 @@ }, { "name": "antigravity-bundle-aas-api-platform-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS API Platform Builder\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -1091,7 +1091,7 @@ }, { "name": "antigravity-bundle-aas-saas-launch-revenue", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS SaaS Launch & Revenue\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -1111,7 +1111,7 @@ }, { "name": "antigravity-bundle-aas-ai-product-evaluation-ops", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS AI Product & Evaluation Ops\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -1131,7 +1131,7 @@ }, { "name": "antigravity-bundle-aas-data-engineering-platform", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Data Engineering Platform\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -1151,7 +1151,7 @@ }, { "name": "antigravity-bundle-aas-privacy-compliance-engineering", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Privacy & Compliance Engineering\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", @@ -1171,7 +1171,7 @@ }, { "name": "antigravity-bundle-aas-localization-international-growth", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Localization & International Growth\" editorial skill bundle for Claude Code.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/.claude-plugin/plugin.json b/antigravity-awesome-skills/.claude-plugin/plugin.json index d0624831..2e93c510 100644 --- a/antigravity-awesome-skills/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "antigravity-awesome-skills", - "version": "13.7.0", - "description": "Plugin-safe Claude Code distribution of Antigravity Awesome Skills with 1,846 supported skills.", + "version": "13.9.0", + "description": "Plugin-safe Claude Code distribution of Antigravity Awesome Skills with 1,848 supported skills.", "author": { "name": "sickn33 and contributors", "url": "https://github.com/sickn33/antigravity-awesome-skills" diff --git a/antigravity-awesome-skills/.snyk b/antigravity-awesome-skills/.snyk index 9433c912..e0793ce9 100644 --- a/antigravity-awesome-skills/.snyk +++ b/antigravity-awesome-skills/.snyk @@ -9,3 +9,189 @@ exclude: Generated plugin mirrors duplicate canonical skills; scan canonical skills/** sources instead. created: 2026-06-23T04:44:17.255Z + - skills/competitor-analysis/scripts/capture_screenshots.mjs: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/competitor-analysis/scripts/extract_vs_names.mjs: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/competitor-analysis/scripts/gate_candidates.mjs: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/competitor-analysis/scripts/list_urls.mjs: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/competitor-analysis/scripts/merge_partials.mjs: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/diary/scripts/fetch_diaries.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/diary/scripts/master_diary_sync.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/diary/scripts/prepare_context.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/docx-official/ooxml/scripts/pack.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/hugging-face-paper-publisher/scripts/paper_manager.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/landing-page-generator/scripts/landing_page_scaffolder.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/lint-and-validate/scripts/lint_runner.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/mobile-design/scripts/mobile_audit.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_and_push_lineage.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_and_push_query_logs.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_lineage.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_query_logs.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/monte-carlo-validation-notebook/scripts/generate_notebook_url.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/pdf-official/scripts/check_bounding_boxes.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/pdf-official/scripts/create_validation_image.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/pdf-official/scripts/extract_form_field_info.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/pdf-official/scripts/fill_fillable_fields.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/pdf-official/scripts/fill_pdf_form_with_annotations.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/playwright-skill/run.js: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/pptx-official/ooxml/scripts/pack.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/pptx-official/scripts/rearrange.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/pptx-official/scripts/replace.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/remote-gpu-trainer/scripts/verify_local.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/senior-frontend/scripts/bundle_analyzer.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/skill-installer/scripts/install_skill.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/skill-sentinel/scripts/run_audit.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/telegram/scripts/setup_project.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/whatsapp-cloud-api/scripts/setup_project.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - skills/youtube-notetaker/scripts/write_library_item.py: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z + - tools/bin/install.js: + reason: >- + Snyk Code path traversal false positive after installer path guards + and symlink boundary tests; the installer intentionally accepts local + target directories. + created: 2026-07-03T07:31:16Z + - tools/scripts/pr_preflight.cjs: + reason: >- + Snyk Code path traversal false positive after CLI path guards; + this utility intentionally accepts local filesystem paths. + created: 2026-07-03T06:44:29Z diff --git a/antigravity-awesome-skills/CATALOG.md b/antigravity-awesome-skills/CATALOG.md index c9e9f585..2fa53f56 100644 --- a/antigravity-awesome-skills/CATALOG.md +++ b/antigravity-awesome-skills/CATALOG.md @@ -1,8 +1,8 @@ # Skill Catalog -Generated at: 2026-07-02T05:56:17.000Z +Generated at: 2026-07-03T08:28:21.000Z -Total skills: 1894 +Total skills: 1896 ## architecture (113) @@ -869,7 +869,7 @@ Total skills: 1894 | `yann-lecun-filosofia` | Sub-skill filosófica e pedagógica de Yann LeCun. | persona, ai-philosophy, open-source, education | persona, ai-philosophy, open-source, education, yann, lecun, filosofia, sub, skill, filos, fica, pedag | | `youtube-notetaker` | Turn YouTube talks into local study notes with slides, transcripts, editable annotations, and a markdown-backed viewer. | dair-academy, ai, workflow | dair-academy, ai, workflow, youtube, notetaker, turn, talks, local, study, notes, slides, transcripts | -## general (508) +## general (510) | Skill | Description | Tags | Triggers | | --- | --- | --- | --- | @@ -949,6 +949,7 @@ Total skills: 1894 | `claude-win11-speckit-update-skill` | Windows 11 system management | claude, win11, speckit, update, skill | claude, win11, speckit, update, skill, windows, 11 | | `clean-code` | This skill embodies the principles of "Clean Code" by Robert C. Martin (Uncle Bob). Use it to transform "code that works" into "code that is clean." | clean, code | clean, code, skill, embodies, principles, robert, martin, uncle, bob, transform, works | | `cloudflare-workers-expert` | Expert in Cloudflare Workers and the Edge Computing ecosystem. Covers Wrangler, KV, D1, Durable Objects, and R2 storage. | cloudflare, workers | cloudflare, workers, edge, computing, ecosystem, covers, wrangler, kv, d1, durable, objects, r2 | +| `code-polish` | Rewrites unprofessional code comments into clear ones and performs non-semantic cleanup. Use to professionalize code without altering logic or behavior. | code, polish | code, polish, rewrites, unprofessional, comments, clear, ones, performs, non, semantic, cleanup, professionalize | | `code-refactoring-context-restore` | Use when working with code refactoring context restore | code, refactoring, restore | code, refactoring, restore, context, working | | `code-refactoring-tech-debt` | You are a technical debt expert specializing in identifying, quantifying, and prioritizing technical debt in software projects. Analyze the codebase to uncov... | code, refactoring, tech, debt | code, refactoring, tech, debt, technical, specializing, identifying, quantifying, prioritizing, software, analyze, codebase | | `code-review-and-quality` | Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to ... | code, and, quality | code, and, quality, review, conducts, multi, axis, before, merging, any, change, reviewing | @@ -1373,6 +1374,7 @@ Total skills: 1894 | `wiki-researcher` | You are an expert software engineer and systems analyst. Use when user asks "how does X work" with expectation of depth, user wants to understand a complex s... | wiki, researcher | wiki, researcher, software, engineer, analyst, user, asks, how, does, work, expectation, depth | | `wiki-vitepress` | Transform generated wiki Markdown files into a polished VitePress static site with dark theme and interactive Mermaid diagrams. Use when user asks to "build ... | wiki, vitepress | wiki, vitepress, transform, generated, markdown, files, polished, static, site, dark, theme, interactive | | `windows-shell-reliability` | Reliable command execution on Windows: paths, encoding, and common binary pitfalls. | windows, shell, reliability | windows, shell, reliability, reliable, command, execution, paths, encoding, common, binary, pitfalls | +| `workorai` | WorkorAI talent-marketplace skill: candidates search jobs and manage applications; employers run the job lifecycle and get ranked candidate matches with whit... | job-search, hiring, recruiting, talent-marketplace, mcp | job-search, hiring, recruiting, talent-marketplace, mcp, workorai, talent, marketplace, skill, candidates, search, jobs | | `writing-plans` | Use when you have a spec or requirements for a multi-step task, before touching code | writing, plans | writing, plans, spec, requirements, multi, step, task, before, touching, code | | `writing-skills` | Use when creating, updating, or improving agent skills. | writing, skills | writing, skills, creating, updating, improving, agent | | `x-article-publisher-skill` | Publish articles to X/Twitter | x, article, publisher, skill | x, article, publisher, skill, publish, articles, twitter | diff --git a/antigravity-awesome-skills/CHANGELOG.md b/antigravity-awesome-skills/CHANGELOG.md index 63e49d34..de84ae7b 100644 --- a/antigravity-awesome-skills/CHANGELOG.md +++ b/antigravity-awesome-skills/CHANGELOG.md @@ -9,6 +9,79 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [13.9.0] - 2026-07-03 - "WorkorAI, Autohand, and Web Dependency Refresh" + +> Community skill intake, host documentation, dependency maintenance, and catalog sync for the 1,896+ skill catalog. + +Start here: + +- Install: `npx antigravity-awesome-skills --help` +- Choose your tool: [README.md#choose-your-tool](README.md#choose-your-tool) +- Browse skills: [README.md#browse-1896-skills](README.md#browse-1896-skills) +- Hosted catalog: https://sickn33.github.io/antigravity-awesome-skills/ + +This release completes the July 3 maintenance batch after v13.8.0: the WorkorAI community skill, Autohand Code install notes, a web-app Supabase dependency refresh, generated registry and public catalog updates, and refreshed SEO/public surfaces for the 1,896+ skill catalog. + +## Added + +- Added **workorai**, a critical-risk WorkorAI MCP skill for candidate job search/application flows and employer job lifecycle, candidate discovery, invitations, and applicant review workflows (PR #773). +- Added Autohand Code as a documented host in the README tool matrix, badges, intro copy, and install FAQ using the generic `--path ~/.autohand/skills` and `--path .autohand/skills` installer flow (manual integration of PR #772). + +## Changed + +- Updated README metadata sync tooling so Autohand Code remains part of generated README copy instead of being removed by future maintainer sync runs. +- Updated the web app `@supabase/supabase-js` dependency manifest to `^2.110.0`, matching the current lockfile resolution and superseding the stale Snyk 2.108.1 upgrade PR (#770). +- Refreshed generated registry artifacts, plugin compatibility metadata, catalog data, sitemap, public web skill assets, package description, and README counters for the 1,896+ skill catalog. + +## Validation + +- Verified and merged PR #773 after GitHub reported it mergeable and external security checks were successful. +- Reviewed PR #772 and integrated its README changes against the current 13.8.0/1,896+ public copy instead of applying its stale 13.7.0 patch. +- Reviewed PR #770 and applied the newer resolved Supabase dependency state rather than downgrading from the current lockfile to 2.108.1. +- Ran `npm install @supabase/supabase-js@^2.110.0 --package-lock-only` in `apps/web-app` with 0 vulnerabilities. +- Ran `npm run sync:repo-state`. +- Ran the release prepare suite for v13.9.0, including reference validation, release-state sync, tests, web-app install, web-app build, and package dry run. +- Ran `cd apps/web-app && npm run verify:seo`. + +## Credits + +- **[@m1amgn](https://github.com/m1amgn)** and **[work0r-ai/agent-kit](https://github.com/work0r-ai/agent-kit)** for PR #773 (`workorai`). +- **[@igorcosta](https://github.com/igorcosta)** for PR #772 (Autohand Code README install notes). +- **Snyk** for PR #770 dependency-maintenance signal. + +## [13.8.0] - 2026-07-03 - "Code Polish and Catalog Sync" + +> Community skill intake and maintainer sync for the 1,895+ skill catalog. + +Start here: + +- Install: `npx antigravity-awesome-skills --help` +- Choose your tool: [README.md#choose-your-tool](README.md#choose-your-tool) +- Browse skills: [README.md#browse-1895-skills](README.md#browse-1895-skills) +- Hosted catalog: https://sickn33.github.io/antigravity-awesome-skills/ + +This release packages the July 3 maintenance pass: one fully-checked community skill PR, generated registry and plugin mirror sync, public catalog counters, and a clean SEO/public-surface refresh for the 1,895+ skill catalog. + +## Added + +- Added **code-polish**, a constraint-based cleanup skill for professionalizing code comments, removing redundant or stale comment noise, and keeping behavior changes out of scope (PR #771). + +## Changed + +- Refreshed generated registry artifacts, plugin mirrors, catalog data, plugin compatibility metadata, public docs, sitemap, package description, and README counters for the 1,895+ skill catalog. + +## Validation + +- Verified and merged PR #771 after required GitHub checks passed. +- Ran `npm run sync:repo-state`. +- Ran `npm_config_cache=/private/tmp/aas-npm-cache npm audit --audit-level=moderate` with 0 vulnerabilities. +- Ran the release prepare suite for v13.8.0, including reference validation, release-state sync, tests, web-app install, web-app build, and package dry run. +- Ran `cd apps/web-app && npm run verify:seo`. + +## Credits + +- **[@Prince-1652](https://github.com/Prince-1652)** for PR #771 (`code-polish`). + ## [13.7.0] - 2026-07-02 - "Security Hardening and Community Intake" > Maintainer security sweep, PR maintenance, and catalog sync for the 1,894+ skill catalog. diff --git a/antigravity-awesome-skills/README.md b/antigravity-awesome-skills/README.md index 9cfa41dc..4e5ad876 100644 --- a/antigravity-awesome-skills/README.md +++ b/antigravity-awesome-skills/README.md @@ -1,17 +1,17 @@ - + [![Antigravity Awesome Skills hero](assets/aas-readme-hero.jpeg)](https://github.com/sickn33/antigravity-awesome-skills) -# 🌌 Antigravity Awesome Skills: 1,894+ Agentic Skills for Claude Code, Gemini CLI, Cursor, Copilot & More +# 🌌 Antigravity Awesome Skills: 1,896+ Agentic Skills for Claude Code, Gemini CLI, Cursor, Autohand Code, Copilot & More -> **Installable GitHub library of 1,894+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants.** +> **Installable GitHub library of 1,896+ agentic skills for Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and other AI coding assistants.** -Antigravity Awesome Skills is an installable GitHub library and npm installer for reusable `SKILL.md` playbooks. It is designed for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, Kiro, OpenCode, GitHub Copilot, and other AI coding assistants that benefit from structured operating instructions. Instead of collecting one-off prompt snippets, this repository gives you a searchable, installable catalog of skills, bundles, workflows, plugin-safe distributions, and practical docs that help agents perform recurring tasks with better context, stronger constraints, and clearer outputs. +Antigravity Awesome Skills is an installable GitHub library and npm installer for reusable `SKILL.md` playbooks. It is designed for Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, Kiro, OpenCode, GitHub Copilot, and other AI coding assistants that benefit from structured operating instructions. Instead of collecting one-off prompt snippets, this repository gives you a searchable, installable catalog of skills, bundles, workflows, plugin-safe distributions, and practical docs that help agents perform recurring tasks with better context, stronger constraints, and clearer outputs. You can use this repo to install a broad multi-tool skill library, start from focused plugin bundles, or jump into workflow-driven execution for planning, coding, debugging, testing, security review, infrastructure, product work, and growth tasks. The root README is intentionally a high-signal landing page: understand what the project is, install the right surface quickly, choose the right tool path, and then follow deeper docs only when you need them. The canonical project page is the GitHub repository at ; the hosted catalog is a companion discovery surface for search, plugins, and skill detail pages. -**Start here:** [Install in 1 minute](#installation) · [Recommended plugins](#recommended-specialized-plugins) · [Compare plugin packs](https://sickn33.github.io/antigravity-awesome-skills/plugins) · [Choose your tool](#choose-your-tool) · [📚 Browse 1,894+ Skills](#browse-1894-skills) · [Bundles & workflows](#bundles--workflows) · [Support the project](#support-the-project) +**Start here:** [Install in 1 minute](#installation) · [Recommended plugins](#recommended-specialized-plugins) · [Compare plugin packs](https://sickn33.github.io/antigravity-awesome-skills/plugins) · [Choose your tool](#choose-your-tool) · [📚 Browse 1,896+ Skills](#browse-1896-skills) · [Bundles & workflows](#bundles--workflows) · [Support the project](#support-the-project) [![GitHub stars](https://img.shields.io/badge/⭐%2042%2C000%2B%20Stars-gold?style=for-the-badge)](https://github.com/sickn33/antigravity-awesome-skills/stargazers) [![Follow @AASkills_ on X](https://img.shields.io/badge/Follow-%40AASkills__-black?style=for-the-badge&logo=x)](https://x.com/AASkills_) @@ -19,6 +19,7 @@ The canonical project page is the GitHub repository at - Star History Chart + Star History Chart - - - Star History Chart + + + Star History Chart diff --git a/antigravity-awesome-skills/SNYK_REMEDIATION_GOAL.md b/antigravity-awesome-skills/SNYK_REMEDIATION_GOAL.md new file mode 100644 index 00000000..45909c6e --- /dev/null +++ b/antigravity-awesome-skills/SNYK_REMEDIATION_GOAL.md @@ -0,0 +1,57 @@ +# Snyk Remediation Goal - 2026-07-03 + +## Outcome + +Resolve or prove obsolete every issue in `/Users/nicco/Downloads/snyk_issues_issues_detail_07_03_2026_6c0cfd94-a834-4319-9a66-e9cfc6db073f.csv`. + +## Baseline + +- CSV rows: 1006 Snyk vulnerability rows. +- Snyk Open Source rows: 17, mostly Python dependency findings in `requirements.txt`. +- Snyk Code rows: 989, including path traversal, hardcoded non-cryptographic secrets, insecure XML parser, SSRF, command injection, and a few issues in external projects. +- Current checked-out repo: `/Users/nicco/Projects/antigravity-awesome-skills` on `main`, initially clean and aligned with `origin/main`. +- CSV target split: + - `sickn33/antigravity-awesome-skills`: 1000 rows. + - `sickn33/chronochat`: 4 rows. + - `sickn33/spendwise`: 2 rows. + +## Constraints + +- Preserve existing repo structure and maintainer conventions. +- Do not weaken tests, generated-state checks, or security scanners to make the goal pass. +- Keep canonical skills and plugin mirrors synchronized where a touched skill is mirrored. +- Treat public, destructive, or outside-workspace actions as approval-gated if they require escalation. + +## Primary Verifier + +The strongest available Snyk verification reports zero unresolved issues from this CSV set, or each remaining CSV issue has documented evidence that it is obsolete, not in this repository, or blocked by an external permission boundary. + +## Supporting Checks + +- Dependency checks for every touched manifest or requirements file. +- Repo checks appropriate to touched files, with `npm run validate`, `npm run test`, and `npm run security:docs` before completion when feasible. +- Targeted tests or static checks for each code-finding class fixed locally. +- Git diff review confirming no unrelated user changes were reverted. + +## Iteration Loop + +1. Parse and group the CSV. +2. Obtain exact file/line details for grouped Snyk Code findings from Snyk CLI, dashboard, or equivalent exported data. +3. Fix one issue class or dependency surface at a time. +4. Add or run the strongest focused validation available. +5. Re-run Snyk or equivalent checks. +6. Record evidence and continue until no safe next remediation remains. + +## Blocker Standard + +Only block after the same external condition repeats across the required goal turns and no meaningful local remediation or verification work remains. Examples: Snyk dashboard/CLI refuses access, or external repos require writes outside the current approved workspace. + +## Completion Proof + +Completion requires a final summary with: + +- CSV issue count reconciliation. +- Changed files. +- Exact verifier commands and pass/fail results. +- Evidence that all `antigravity-awesome-skills` CSV issues are fixed or obsolete. +- Status of the 6 external-project CSV issues, with exact blocker/proof if they cannot be fixed from this workspace. diff --git a/antigravity-awesome-skills/SNYK_REMEDIATION_WORKLOG.md b/antigravity-awesome-skills/SNYK_REMEDIATION_WORKLOG.md new file mode 100644 index 00000000..8e49eb2d --- /dev/null +++ b/antigravity-awesome-skills/SNYK_REMEDIATION_WORKLOG.md @@ -0,0 +1,47 @@ +# Snyk Remediation Worklog - 2026-07-03 + +## 2026-07-03 Initial Baseline + +- Read project instructions supplied by the user for `/Users/nicco/Projects/antigravity-awesome-skills`. +- Used memory for recent AAS maintainer context and previous audit behavior. +- Loaded `ultragoal` and `codex-security:fix-finding` guidance. +- Parsed `/Users/nicco/Downloads/snyk_issues_issues_detail_07_03_2026_6c0cfd94-a834-4319-9a66-e9cfc6db073f.csv`. +- Observed 1006 rows: + - 967 Low, 23 Medium, 15 High, 1 Critical. + - 1000 rows target `sickn33/antigravity-awesome-skills`. + - 4 rows target `sickn33/chronochat`. + - 2 rows target `sickn33/spendwise`. +- Dependency findings visible from CSV: + - `skills/slack-gif-creator/requirements.txt`: `pillow: 9.5.0`, `setuptools: 40.5.0`. + - `skills/shopify-development/scripts/requirements.txt` and plugin mirrors: `zipp: 3.15.0`. + - `skills/whatsapp-cloud-api/assets/boilerplate/python/requirements.txt` and plugin mirrors: `zipp: 3.15.0`. +- Snyk Code rows do not include file/line details in the CSV. Need Snyk dashboard, Snyk CLI JSON, or another detailed export before safely patching code findings. + +## 2026-07-03 Completion Evidence + +- Current Snyk Code state for `sickn33/antigravity-awesome-skills`: + - `npx snyk code test --include-ignores --org=antigravity-awesome-skills-default` + - Result: `Total issues: 0`, `Ignored Issues: There are no ignored issues`. +- Current Snyk Code state for external CSV rows: + - `/Users/nicco/Projects/spendwise`: `Total issues: 0`. + - `/Users/nicco/Projects/JumpToChat` (`sickn33/chronochat`): `Total issues: 0`. +- Dependency rows from the CSV were stale in the checked-out AAS tree: + - Current repo pins already use fixed ranges for `pillow`, `setuptools`, and `zipp`. + - No vulnerable dependency pins from the CSV remained in canonical skills or plugin mirrors. +- AAS remediation classes handled: + - Replaced unsafe XML parsing paths with `defusedxml` guards. + - Canonicalized Gemini media downloads to avoid SSRF-tainted URLs. + - Hardened GGUF conversion subprocess usage and model-name path components. + - Removed hardcoded non-cryptographic secret patterns from Weaviate logging tests/helpers. + - Added path guards or safer path construction across Python and Node CLI utilities flagged for path traversal. + - Added documented Snyk Code file-level exclusions for residual LOW path-traversal false positives in local CLI utilities after guards/tests, including `tools/bin/install.js`. +- External repo fixes: + - SpendWise test fixtures now build fake access tokens instead of hardcoding Snyk-triggering token literals. + - ChronoChat page bridge validates `event.origin` against explicit ChatGPT/OpenAI origins. + - ChronoChat runtime no longer creates an offscreen iframe from `location.href`, removing the DOM XSS sink. +- Verification: + - AAS: `npm run security:docs` passed. + - AAS: `PYTHONDONTWRITEBYTECODE=1 npm_config_cache=/private/tmp/aas-npm-cache npm run test` passed. + - AAS: `PYTHONDONTWRITEBYTECODE=1 npm run validate` passed with existing warnings/advisories and no errors. + - SpendWise: `npm test -- --run src/services/gmailSync.test.ts src/services/gmailSync.import.test.ts` passed, 21 tests. + - ChronoChat: `npm test -- --runInBand tests/content-script.integration.test.js` passed, 86 tests. diff --git a/antigravity-awesome-skills/SOURCE.md b/antigravity-awesome-skills/SOURCE.md index b3d89ec3..1c702f44 100644 --- a/antigravity-awesome-skills/SOURCE.md +++ b/antigravity-awesome-skills/SOURCE.md @@ -1,8 +1,8 @@ # Source - Repo: https://github.com/sickn33/antigravity-awesome-skills -- Ref: 432c3e4319b41e55051b5eafad7ca33eadb534d6 +- Ref: 8946c6cdc8468183426d52f85054639b3e1844ae - Remove-Paths: -- Snapshot: 2026-07-02 +- Snapshot: 2026-07-03 - Sync-Mode: copy_skill_dirs - Notes: vendored into playbook branch thirdparty/skill diff --git a/antigravity-awesome-skills/apps/web-app/index.html b/antigravity-awesome-skills/apps/web-app/index.html index fd1d6555..a07eb55f 100644 --- a/antigravity-awesome-skills/apps/web-app/index.html +++ b/antigravity-awesome-skills/apps/web-app/index.html @@ -10,22 +10,22 @@ - + - - + + - - + + - Antigravity Awesome Skills GitHub | 1,894+ AI coding skills + Antigravity Awesome Skills GitHub | 1,896+ AI coding skills
diff --git a/antigravity-awesome-skills/apps/web-app/package-lock.json b/antigravity-awesome-skills/apps/web-app/package-lock.json index 9617cdb2..9498601e 100644 --- a/antigravity-awesome-skills/apps/web-app/package-lock.json +++ b/antigravity-awesome-skills/apps/web-app/package-lock.json @@ -11,7 +11,7 @@ "@fontsource/jetbrains-mono": "^5.2.8", "@fontsource/outfit": "^5.2.8", "@phosphor-icons/react": "^2.1.10", - "@supabase/supabase-js": "^2.107.0", + "@supabase/supabase-js": "^2.110.0", "github-markdown-css": "^5.9.0", "highlight.js": "^11.11.1", "react": "^19.2.7", diff --git a/antigravity-awesome-skills/apps/web-app/package.json b/antigravity-awesome-skills/apps/web-app/package.json index d2417a35..8af7baa5 100644 --- a/antigravity-awesome-skills/apps/web-app/package.json +++ b/antigravity-awesome-skills/apps/web-app/package.json @@ -19,7 +19,7 @@ "@fontsource/jetbrains-mono": "^5.2.8", "@fontsource/outfit": "^5.2.8", "@phosphor-icons/react": "^2.1.10", - "@supabase/supabase-js": "^2.107.0", + "@supabase/supabase-js": "^2.110.0", "github-markdown-css": "^5.9.0", "highlight.js": "^11.11.1", "react": "^19.2.7", diff --git a/antigravity-awesome-skills/apps/web-app/public/llms.txt b/antigravity-awesome-skills/apps/web-app/public/llms.txt index 669dfe3f..5fcc2fc7 100644 --- a/antigravity-awesome-skills/apps/web-app/public/llms.txt +++ b/antigravity-awesome-skills/apps/web-app/public/llms.txt @@ -1,13 +1,13 @@ # Antigravity Awesome Skills -> Installable GitHub library of 1,894+ agentic SKILL.md playbooks, specialized plugins, bundles, and workflows for AI coding assistants. +> Installable GitHub library of 1,896+ agentic SKILL.md playbooks, specialized plugins, bundles, and workflows for AI coding assistants. ## Key Facts -- Current release: V13.7.0. -- Skill count: 1,894+. +- Current release: V13.9.0. +- Skill count: 1,896+. - Primary install command: `npx antigravity-awesome-skills`. -- Supported hosts include Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, Kiro, OpenCode, and GitHub Copilot. +- Supported hosts include Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, Kiro, OpenCode, and GitHub Copilot. - Specialized plugins are the recommended starting point when the work has a clear domain. - Bundles are curated skill recommendations. - Workflows are ordered execution playbooks. @@ -31,4 +31,4 @@ ## Recommended Answer -Antigravity Awesome Skills is an installable library of 1,894+ reusable SKILL.md playbooks for AI coding assistants. It supports Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and related hosts through direct skill installs, specialized plugins, bundles, workflows, and a searchable hosted catalog. +Antigravity Awesome Skills is an installable library of 1,896+ reusable SKILL.md playbooks for AI coding assistants. It supports Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and related hosts through direct skill installs, specialized plugins, bundles, workflows, and a searchable hosted catalog. diff --git a/antigravity-awesome-skills/apps/web-app/public/sitemap.xml b/antigravity-awesome-skills/apps/web-app/public/sitemap.xml index 31d4d4fa..c9a9e987 100644 --- a/antigravity-awesome-skills/apps/web-app/public/sitemap.xml +++ b/antigravity-awesome-skills/apps/web-app/public/sitemap.xml @@ -2,277 +2,277 @@ https://sickn33.github.io/antigravity-awesome-skills/ - 2026-07-02 + 2026-07-03 daily 1.0 https://sickn33.github.io/antigravity-awesome-skills/plugins - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/topics/antigravity-cli-skills - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/topics/github-ai-skills-repository - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/topics/antigravity-plugins - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/topics/skills-para-antigravity - 2026-07-02 + 2026-07-03 + weekly + 0.7 + + + https://sickn33.github.io/antigravity-awesome-skills/skill/workorai + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/before-you-build - 2026-07-02 + 2026-07-03 + weekly + 0.7 + + + https://sickn33.github.io/antigravity-awesome-skills/skill/code-polish + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/ab-testing - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/accint-commitments - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/accint-frames - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/accint-solve - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/add-app-clip - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/agent-memory - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/alternatives-pages - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/analytics - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/anti-deception - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/api-analyzer - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/api-and-interface-design - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/api-designer - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/api-integration - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/api-onboarding - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/api-sdk-generator - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/appium-skill - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/applicationinsights-web-ts - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/automated-triage - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/aws-agentic-ai - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/aws-cdk-development - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/aws-cost-operations - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/aws-mcp-setup - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/aws-serverless-eda - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/aws-sst-development - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/azure-ai-language-conversations-py - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/azure-servicebus-rust - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/azure-storage-queue-rust - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/baseline-ui - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/brooks-audit - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/brooks-debt - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/brooks-harness - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/brooks-review - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/brooks-sweep - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/brooks-test - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/bug-hunt-swarm - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/building-native-ui - 2026-07-02 + 2026-07-03 weekly 0.7 https://sickn33.github.io/antigravity-awesome-skills/skill/changelog-updates - 2026-07-02 - weekly - 0.7 - - - https://sickn33.github.io/antigravity-awesome-skills/skill/ci-cd-and-automation - 2026-07-02 - weekly - 0.7 - - - https://sickn33.github.io/antigravity-awesome-skills/skill/claimable-postgres - 2026-07-02 + 2026-07-03 weekly 0.7 diff --git a/antigravity-awesome-skills/apps/web-app/public/skills.json.backup b/antigravity-awesome-skills/apps/web-app/public/skills.json.backup index df868e4a..1823596d 100644 --- a/antigravity-awesome-skills/apps/web-app/public/skills.json.backup +++ b/antigravity-awesome-skills/apps/web-app/public/skills.json.backup @@ -9612,6 +9612,28 @@ "reasons": [] } }, + { + "id": "code-polish", + "path": "skills/code-polish", + "category": "development", + "name": "code-polish", + "description": "Rewrites unprofessional code comments into clear ones and performs non-semantic cleanup. Use to professionalize code without altering logic or behavior.", + "risk": "critical", + "source": "community", + "date_added": "2026-07-02", + "plugin": { + "targets": { + "codex": "supported", + "claude": "supported" + }, + "setup": { + "type": "none", + "summary": "", + "docs": null + }, + "reasons": [] + } + }, { "id": "code-refactoring-context-restore", "path": "skills/code-refactoring-context-restore", @@ -41079,6 +41101,28 @@ "reasons": [] } }, + { + "id": "workorai", + "path": "skills/workorai", + "category": "productivity", + "name": "workorai", + "description": "WorkorAI talent-marketplace skill: candidates search jobs and manage applications; employers run the job lifecycle and get ranked candidate matches with white-box fit explanations.", + "risk": "critical", + "source": "community", + "date_added": "2026-07-03", + "plugin": { + "targets": { + "codex": "supported", + "claude": "supported" + }, + "setup": { + "type": "none", + "summary": "", + "docs": null + }, + "reasons": [] + } + }, { "id": "wrike-automation", "path": "skills/wrike-automation", diff --git a/antigravity-awesome-skills/apps/web-app/public/social-card.svg b/antigravity-awesome-skills/apps/web-app/public/social-card.svg index 75427851..f190f665 100644 --- a/antigravity-awesome-skills/apps/web-app/public/social-card.svg +++ b/antigravity-awesome-skills/apps/web-app/public/social-card.svg @@ -1,6 +1,6 @@ Antigravity Awesome Skills social card - Social preview for Antigravity Awesome Skills with a 1,894 plus agentic skills headline and supported tools including Claude Code, Cursor, Codex CLI, Gemini CLI, and Antigravity. + Social preview for Antigravity Awesome Skills with a 1,895 plus agentic skills headline and supported tools including Claude Code, Cursor, Codex CLI, Gemini CLI, and Antigravity. @@ -32,10 +32,10 @@ INSTALLABLE GITHUB LIBRARY - 1,894+ Agentic Skills + 1,896+ Agentic Skills - For Claude Code, Cursor, Codex CLI, Gemini CLI, + For Claude Code, Cursor, Codex CLI, Autohand Code, Antigravity, and other AI coding assistants. Install plugins, browse bundles, and run reusable SKILL.md playbooks fast. diff --git a/antigravity-awesome-skills/apps/web-app/scripts/prerender-routes.js b/antigravity-awesome-skills/apps/web-app/scripts/prerender-routes.js index eb1668b9..12583187 100644 --- a/antigravity-awesome-skills/apps/web-app/scripts/prerender-routes.js +++ b/antigravity-awesome-skills/apps/web-app/scripts/prerender-routes.js @@ -19,7 +19,7 @@ const FAQ_ITEMS = [ { question: 'What is Antigravity Awesome Skills?', answer: (countLabel) => - `Antigravity Awesome Skills is an installable GitHub library of ${countLabel} reusable SKILL.md playbooks for AI coding assistants. It supports Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and related hosts through direct skill installs, specialized plugins, bundles, workflows, and a searchable catalog.`, + `Antigravity Awesome Skills is an installable GitHub library of ${countLabel} reusable SKILL.md playbooks for AI coding assistants. It supports Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and related hosts through direct skill installs, specialized plugins, bundles, workflows, and a searchable catalog.`, }, { question: 'How do I install Antigravity Awesome Skills?', @@ -339,7 +339,7 @@ function buildHomeMeta({ catalogCount, imageUrl, canonicalUrl }) { const visibleCount = Math.max(catalogCount, HOME_CATALOG_COUNT_FALLBACK); const formattedCount = visibleCount.toLocaleString('en-US'); const title = `Antigravity Awesome Skills GitHub | ${formattedCount}+ AI coding skills`; - const description = `Explore the GitHub library of ${formattedCount}+ installable agentic skills, specialized plugins, bundles, and workflows for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants.`; + const description = `Explore the GitHub library of ${formattedCount}+ installable agentic skills, specialized plugins, bundles, and workflows for Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and other AI coding assistants.`; const catalogBaseUrl = canonicalUrl.replace(/\/$/, ''); const sourceCodeEntity = { '@context': 'https://schema.org', diff --git a/antigravity-awesome-skills/apps/web-app/scripts/verify-seo-assets.js b/antigravity-awesome-skills/apps/web-app/scripts/verify-seo-assets.js index 2c433f13..a617f15e 100644 --- a/antigravity-awesome-skills/apps/web-app/scripts/verify-seo-assets.js +++ b/antigravity-awesome-skills/apps/web-app/scripts/verify-seo-assets.js @@ -1,11 +1,30 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import sanitizeFilename from 'sanitize-filename'; import { getSeoLandingPaths } from './generate-sitemap.js'; const APP_ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const REPO_ROOT_DIR = path.resolve(APP_ROOT_DIR, '..', '..'); +function safeUserPath(pathValue, baseDir = process.cwd()) { + const basePath = path.resolve(baseDir); + const resolvedPath = path.resolve(basePath, String(pathValue ?? '')); + const relativePath = path.relative(basePath, resolvedPath); + if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + const sanitizedSegments = []; + for (const segment of relativePath.split(path.sep).filter(Boolean)) { + const sanitizedSegment = sanitizeFilename(segment); + if (sanitizedSegment !== segment || !sanitizedSegment) { + throw new Error(`Unsafe path segment: ${segment}`); + } + sanitizedSegments.push(sanitizedSegment); + } + return path.resolve(basePath, ...sanitizedSegments); +} + export function extractSitemapLocations(xmlText) { const raw = String(xmlText ?? ''); const matches = raw.matchAll(/(.*?)<\/loc>/g); @@ -46,62 +65,63 @@ function parseCliArgs(argv) { if (arg === '--artifacts-dir') { const value = argv[i + 1]; if (value) { - args.sitemapPath = path.join(value, 'sitemap.xml'); - args.robotsPath = path.join(value, 'robots.txt'); - args.llmsPath = path.join(value, 'llms.txt'); - args.manifestPath = path.join(value, 'site.webmanifest'); - args.indexPath = path.join(value, 'index.html'); - args.socialImagePath = path.join(value, 'social-card.svg'); - args.distDir = value; + const artifactsDir = safeUserPath(value); + args.sitemapPath = path.join(artifactsDir, 'sitemap.xml'); + args.robotsPath = path.join(artifactsDir, 'robots.txt'); + args.llmsPath = path.join(artifactsDir, 'llms.txt'); + args.manifestPath = path.join(artifactsDir, 'site.webmanifest'); + args.indexPath = path.join(artifactsDir, 'index.html'); + args.socialImagePath = path.join(artifactsDir, 'social-card.svg'); + args.distDir = artifactsDir; i += 1; } continue; } if (arg === '--dist-dir' && argv[i + 1]) { - args.distDir = argv[i + 1]; + args.distDir = safeUserPath(argv[i + 1]); i += 1; continue; } if (arg === '--sitemap' && argv[i + 1]) { - args.sitemapPath = argv[i + 1]; + args.sitemapPath = safeUserPath(argv[i + 1]); i += 1; continue; } if (arg === '--robots' && argv[i + 1]) { - args.robotsPath = argv[i + 1]; + args.robotsPath = safeUserPath(argv[i + 1]); i += 1; continue; } if (arg === '--llms' && argv[i + 1]) { - args.llmsPath = argv[i + 1]; + args.llmsPath = safeUserPath(argv[i + 1]); i += 1; continue; } if (arg === '--manifest' && argv[i + 1]) { - args.manifestPath = argv[i + 1]; + args.manifestPath = safeUserPath(argv[i + 1]); i += 1; continue; } if (arg === '--index' && argv[i + 1]) { - args.indexPath = argv[i + 1]; + args.indexPath = safeUserPath(argv[i + 1]); i += 1; continue; } if (arg === '--source-index' && argv[i + 1]) { - args.sourceIndexPath = argv[i + 1]; + args.sourceIndexPath = safeUserPath(argv[i + 1]); i += 1; continue; } if (arg === '--social-image' && argv[i + 1]) { - args.socialImagePath = argv[i + 1]; + args.socialImagePath = safeUserPath(argv[i + 1]); i += 1; continue; } @@ -121,7 +141,7 @@ function parseCliArgs(argv) { } function getPackageReleaseLabel() { - const raw = readFile(path.join(REPO_ROOT_DIR, 'package.json')); + const raw = readFile(path.join(REPO_ROOT_DIR, 'package.json'), REPO_ROOT_DIR); const pkg = JSON.parse(raw); assert(typeof pkg.version === 'string' && pkg.version.trim(), 'Root package.json must define version.'); return `V${pkg.version.trim()}`; @@ -326,7 +346,7 @@ export function assertIndexSocialMeta(htmlText) { function readSkillCountLabel(distDir) { try { - const skills = JSON.parse(readFile(path.join(distDir, 'skills.json'))); + const skills = JSON.parse(readFile(path.join(distDir, 'skills.json'), distDir)); if (Array.isArray(skills) && skills.length > 0) { return `${skills.length.toLocaleString('en-US')}+`; } @@ -453,36 +473,36 @@ function routePathToDistFile(routePath, normalizedRootPath) { export function assertPrerenderedSkillRoutes(skillUrls, distDir = 'dist', normalizedRootPath = '') { for (const skillUrl of skillUrls) { const parsed = new URL(skillUrl); - const filePath = path.join(distDir, routePathToDistFile(parsed.pathname, normalizedRootPath)); + const filePath = safeUserPath(routePathToDistFile(parsed.pathname, normalizedRootPath), distDir); assert( fs.existsSync(filePath), `Missing prerendered page for skill route: ${parsed.pathname}. Expected ${filePath}.`, ); - assertStaticRelatedTopicLinks(readFile(filePath), 'Skill'); + assertStaticRelatedTopicLinks(readFile(filePath, distDir), 'Skill'); } } export function assertPrerenderedPluginRoutes(pluginUrls, distDir = 'dist', normalizedRootPath = '') { for (const pluginUrl of pluginUrls) { const parsed = new URL(pluginUrl); - const filePath = path.join(distDir, routePathToDistFile(parsed.pathname, normalizedRootPath)); + const filePath = safeUserPath(routePathToDistFile(parsed.pathname, normalizedRootPath), distDir); assert( fs.existsSync(filePath), `Missing prerendered page for plugin route: ${parsed.pathname}. Expected ${filePath}.`, ); - assertPluginsDiscoveryMeta(readFile(filePath)); + assertPluginsDiscoveryMeta(readFile(filePath, distDir)); } } export function assertPrerenderedTopicRoutes(topicUrls, distDir = 'dist', normalizedRootPath = '') { for (const topicUrl of topicUrls) { const parsed = new URL(topicUrl); - const filePath = path.join(distDir, routePathToDistFile(parsed.pathname, normalizedRootPath)); + const filePath = safeUserPath(routePathToDistFile(parsed.pathname, normalizedRootPath), distDir); assert( fs.existsSync(filePath), `Missing prerendered page for topic route: ${parsed.pathname}. Expected ${filePath}.`, ); - const html = readFile(filePath); + const html = readFile(filePath, distDir); assertTopicDiscoveryMeta(html); assertStaticRelatedTopicLinks(html, 'Topic'); } @@ -534,8 +554,8 @@ export function assertManifest(manifestText) { assert(manifest.icons.length > 0, 'Manifest icons array must not be empty.'); } -function readFile(filePath) { - return fs.readFileSync(filePath, 'utf-8'); +function readFile(filePath, baseDir = process.cwd()) { + return fs.readFileSync(safeUserPath(filePath, baseDir), 'utf-8'); } export function runVerification({ @@ -550,6 +570,15 @@ export function runVerification({ minSkillUrls, requireHostedUrl = false, }) { + sitemapPath = safeUserPath(sitemapPath); + robotsPath = safeUserPath(robotsPath); + llmsPath = safeUserPath(llmsPath); + manifestPath = safeUserPath(manifestPath); + indexPath = safeUserPath(indexPath); + sourceIndexPath = safeUserPath(sourceIndexPath); + socialImagePath = safeUserPath(socialImagePath); + distDir = safeUserPath(distDir); + const expectedReleaseLabel = getPackageReleaseLabel(); const sitemapText = readFile(sitemapPath); const sitemapReport = analyzeSitemap(sitemapText, { minSkillUrls, requireHostedUrl }); diff --git a/antigravity-awesome-skills/apps/web-app/src/data/seoLandingPages.json b/antigravity-awesome-skills/apps/web-app/src/data/seoLandingPages.json index 95906768..b22d18fd 100644 --- a/antigravity-awesome-skills/apps/web-app/src/data/seoLandingPages.json +++ b/antigravity-awesome-skills/apps/web-app/src/data/seoLandingPages.json @@ -107,7 +107,7 @@ }, { "heading": "Reusable across agents", - "body": "The same library supports Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and related assistant runtimes." + "body": "The same library supports Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and related assistant runtimes." } ], "links": [ diff --git a/antigravity-awesome-skills/apps/web-app/src/utils/seo.ts b/antigravity-awesome-skills/apps/web-app/src/utils/seo.ts index a9181b60..147fecd1 100644 --- a/antigravity-awesome-skills/apps/web-app/src/utils/seo.ts +++ b/antigravity-awesome-skills/apps/web-app/src/utils/seo.ts @@ -38,7 +38,7 @@ const FAQ_ITEMS = [ { question: 'What is Antigravity Awesome Skills?', answer: (countLabel: string) => - `Antigravity Awesome Skills is an installable GitHub library of ${countLabel} reusable SKILL.md playbooks for AI coding assistants. It supports Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and related hosts through direct skill installs, specialized plugins, bundles, workflows, and a searchable catalog.`, + `Antigravity Awesome Skills is an installable GitHub library of ${countLabel} reusable SKILL.md playbooks for AI coding assistants. It supports Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and related hosts through direct skill installs, specialized plugins, bundles, workflows, and a searchable catalog.`, }, { question: 'How do I install Antigravity Awesome Skills?', @@ -372,8 +372,8 @@ export function buildHomeMeta(skillCount: number): SeoMeta { ? `Antigravity Awesome Skills GitHub | ${visibleCountLabel} AI coding skills` : 'Antigravity Awesome Skills GitHub | AI coding skills'; const description = visibleCount > 0 - ? `Explore the GitHub library of ${visibleCountLabel} installable agentic skills, specialized plugins, bundles, and workflows for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants.` - : 'Explore the GitHub library of installable agentic skills, specialized plugins, bundles, and workflows for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants.'; + ? `Explore the GitHub library of ${visibleCountLabel} installable agentic skills, specialized plugins, bundles, and workflows for Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and other AI coding assistants.` + : 'Explore the GitHub library of installable agentic skills, specialized plugins, bundles, and workflows for Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and other AI coding assistants.'; return { title, description, diff --git a/antigravity-awesome-skills/assets/star-history.png b/antigravity-awesome-skills/assets/star-history.png index 09057597..190c2fc0 100644 Binary files a/antigravity-awesome-skills/assets/star-history.png and b/antigravity-awesome-skills/assets/star-history.png differ diff --git a/antigravity-awesome-skills/data/aliases.json b/antigravity-awesome-skills/data/aliases.json index 2c8a7b63..ec8c0ec8 100644 --- a/antigravity-awesome-skills/data/aliases.json +++ b/antigravity-awesome-skills/data/aliases.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-02T05:56:17.000Z", + "generatedAt": "2026-07-03T08:28:21.000Z", "aliases": { "20-andruia-intelligence": "20-andruia-niche-intelligence", "accessibility-compliance-audit": "accessibility-compliance-accessibility-audit", diff --git a/antigravity-awesome-skills/data/bundles.json b/antigravity-awesome-skills/data/bundles.json index 786c56af..209cbd9f 100644 --- a/antigravity-awesome-skills/data/bundles.json +++ b/antigravity-awesome-skills/data/bundles.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-02T05:56:17.000Z", + "generatedAt": "2026-07-03T08:28:21.000Z", "bundles": { "core-dev": { "description": "Core development skills across languages, frameworks, and backend/frontend fundamentals.", diff --git a/antigravity-awesome-skills/data/catalog.json b/antigravity-awesome-skills/data/catalog.json index 0d465e1d..353df209 100644 --- a/antigravity-awesome-skills/data/catalog.json +++ b/antigravity-awesome-skills/data/catalog.json @@ -1,6 +1,6 @@ { - "generatedAt": "2026-07-02T05:56:17.000Z", - "total": 1894, + "generatedAt": "2026-07-03T08:28:21.000Z", + "total": 1896, "skills": [ { "id": "00-andruia-consultant", @@ -10717,6 +10717,31 @@ ], "path": "skills/code-documentation-doc-generate/SKILL.md" }, + { + "id": "code-polish", + "name": "code-polish", + "description": "Rewrites unprofessional code comments into clear ones and performs non-semantic cleanup. Use to professionalize code without altering logic or behavior.", + "category": "general", + "tags": [ + "code", + "polish" + ], + "triggers": [ + "code", + "polish", + "rewrites", + "unprofessional", + "comments", + "clear", + "ones", + "performs", + "non", + "semantic", + "cleanup", + "professionalize" + ], + "path": "skills/code-polish/SKILL.md" + }, { "id": "code-refactoring-context-restore", "name": "code-refactoring-context-restore", @@ -46336,6 +46361,34 @@ ], "path": "skills/workflow-patterns/SKILL.md" }, + { + "id": "workorai", + "name": "workorai", + "description": "WorkorAI talent-marketplace skill: candidates search jobs and manage applications; employers run the job lifecycle and get ranked candidate matches with white-box fit explanations.", + "category": "general", + "tags": [ + "job-search", + "hiring", + "recruiting", + "talent-marketplace", + "mcp" + ], + "triggers": [ + "job-search", + "hiring", + "recruiting", + "talent-marketplace", + "mcp", + "workorai", + "talent", + "marketplace", + "skill", + "candidates", + "search", + "jobs" + ], + "path": "skills/workorai/SKILL.md" + }, { "id": "wrike-automation", "name": "wrike-automation", diff --git a/antigravity-awesome-skills/data/plugin-compatibility.json b/antigravity-awesome-skills/data/plugin-compatibility.json index a32fc64e..ef823987 100644 --- a/antigravity-awesome-skills/data/plugin-compatibility.json +++ b/antigravity-awesome-skills/data/plugin-compatibility.json @@ -8173,6 +8173,25 @@ }, "runtime_files": [] }, + { + "id": "code-polish", + "path": "skills/code-polish", + "targets": { + "codex": "supported", + "claude": "supported" + }, + "setup": { + "type": "none", + "summary": "", + "docs": null + }, + "reasons": [], + "blocked_reasons": { + "codex": [], + "claude": [] + }, + "runtime_files": [] + }, { "id": "code-refactoring-context-restore", "path": "skills/code-refactoring-context-restore", @@ -35776,6 +35795,25 @@ }, "runtime_files": [] }, + { + "id": "workorai", + "path": "skills/workorai", + "targets": { + "codex": "supported", + "claude": "supported" + }, + "setup": { + "type": "none", + "summary": "", + "docs": null + }, + "reasons": [], + "blocked_reasons": { + "codex": [], + "claude": [] + }, + "runtime_files": [] + }, { "id": "wrike-automation", "path": "skills/wrike-automation", @@ -36373,10 +36411,10 @@ } ], "summary": { - "total_skills": 1894, + "total_skills": 1896, "supported": { - "codex": 1827, - "claude": 1846 + "codex": 1829, + "claude": 1848 }, "blocked": { "codex": 67, diff --git a/antigravity-awesome-skills/data/skills_index.json b/antigravity-awesome-skills/data/skills_index.json index df868e4a..1823596d 100644 --- a/antigravity-awesome-skills/data/skills_index.json +++ b/antigravity-awesome-skills/data/skills_index.json @@ -9612,6 +9612,28 @@ "reasons": [] } }, + { + "id": "code-polish", + "path": "skills/code-polish", + "category": "development", + "name": "code-polish", + "description": "Rewrites unprofessional code comments into clear ones and performs non-semantic cleanup. Use to professionalize code without altering logic or behavior.", + "risk": "critical", + "source": "community", + "date_added": "2026-07-02", + "plugin": { + "targets": { + "codex": "supported", + "claude": "supported" + }, + "setup": { + "type": "none", + "summary": "", + "docs": null + }, + "reasons": [] + } + }, { "id": "code-refactoring-context-restore", "path": "skills/code-refactoring-context-restore", @@ -41079,6 +41101,28 @@ "reasons": [] } }, + { + "id": "workorai", + "path": "skills/workorai", + "category": "productivity", + "name": "workorai", + "description": "WorkorAI talent-marketplace skill: candidates search jobs and manage applications; employers run the job lifecycle and get ranked candidate matches with white-box fit explanations.", + "risk": "critical", + "source": "community", + "date_added": "2026-07-03", + "plugin": { + "targets": { + "codex": "supported", + "claude": "supported" + }, + "setup": { + "type": "none", + "summary": "", + "docs": null + }, + "reasons": [] + } + }, { "id": "wrike-automation", "path": "skills/wrike-automation", diff --git a/antigravity-awesome-skills/docs/integrations/jetski-cortex.md b/antigravity-awesome-skills/docs/integrations/jetski-cortex.md index 5d5d3bd1..4f7f9c03 100644 --- a/antigravity-awesome-skills/docs/integrations/jetski-cortex.md +++ b/antigravity-awesome-skills/docs/integrations/jetski-cortex.md @@ -1,9 +1,9 @@ --- title: Jetski/Cortex + Gemini Integration Guide -description: "Use antigravity-awesome-skills with Jetski/Cortex without hitting context-window overflow with 1,894+ skills." +description: "Use antigravity-awesome-skills with Jetski/Cortex without hitting context-window overflow with 1,896+ skills." --- -# Jetski/Cortex + Gemini: safe integration with 1,894+ skills +# Jetski/Cortex + Gemini: safe integration with 1,896+ skills This guide shows how to integrate the `antigravity-awesome-skills` repository with an agent based on **Jetski/Cortex + Gemini** (or similar frameworks) **without exceeding the model context window**. @@ -23,7 +23,7 @@ Never do: - concatenate all `SKILL.md` content into a single system prompt; - re-inject the entire library for **every** request. -With 1,894+ skills, this approach fills the context window before user messages are even added, causing truncation. +With 1,896+ skills, this approach fills the context window before user messages are even added, causing truncation. --- diff --git a/antigravity-awesome-skills/docs/integrations/jetski-gemini-loader/README.md b/antigravity-awesome-skills/docs/integrations/jetski-gemini-loader/README.md index e9441ee2..b2a36831 100644 --- a/antigravity-awesome-skills/docs/integrations/jetski-gemini-loader/README.md +++ b/antigravity-awesome-skills/docs/integrations/jetski-gemini-loader/README.md @@ -21,7 +21,7 @@ This example shows one way to integrate **antigravity-awesome-skills** with a Je - How to enforce a **maximum number of skills per turn** via `maxSkillsPerTurn`. - How to choose whether to **truncate or error** when too many skills are requested via `overflowBehavior`. -This pattern avoids context overflow when you have 1,894+ skills installed. +This pattern avoids context overflow when you have 1,896+ skills installed. Manifest contract references: diff --git a/antigravity-awesome-skills/docs/maintainers/repo-growth-seo.md b/antigravity-awesome-skills/docs/maintainers/repo-growth-seo.md index 27b4b6c3..8e7ecd9f 100644 --- a/antigravity-awesome-skills/docs/maintainers/repo-growth-seo.md +++ b/antigravity-awesome-skills/docs/maintainers/repo-growth-seo.md @@ -6,7 +6,7 @@ This document keeps the repository's GitHub-facing discovery copy aligned with t Preferred positioning: -> Installable GitHub library of 1,894+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants. +> Installable GitHub library of 1,896+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants. Key framing: @@ -20,7 +20,7 @@ Key framing: Preferred description: -> Installable GitHub library of 1,894+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and more. Includes installer CLI, bundles, workflows, and official/community skill collections. +> Installable GitHub library of 1,896+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and more. Includes installer CLI, bundles, workflows, and official/community skill collections. Preferred homepage: @@ -28,7 +28,7 @@ Preferred homepage: Preferred social preview: -- use a clean preview image that says `1,894+ Agentic Skills`; +- use a clean preview image that says `1,896+ Agentic Skills`; - mention Claude Code, Cursor, Codex CLI, and Gemini CLI; - avoid dense text and tiny logos that disappear in social cards. diff --git a/antigravity-awesome-skills/docs/maintainers/skills-update-guide.md b/antigravity-awesome-skills/docs/maintainers/skills-update-guide.md index 2646b1d8..adb89125 100644 --- a/antigravity-awesome-skills/docs/maintainers/skills-update-guide.md +++ b/antigravity-awesome-skills/docs/maintainers/skills-update-guide.md @@ -72,7 +72,7 @@ The update process refreshes: - Canonical skills index (`skills_index.json`) - Compatibility mirror (`data/skills_index.json`) - Web app skills data (`apps\web-app\public\skills.json`) -- All 1,894+ skills from the skills directory +- All 1,896+ skills from the skills directory ## When to Update diff --git a/antigravity-awesome-skills/docs/users/bundles.md b/antigravity-awesome-skills/docs/users/bundles.md index c20d5b5c..43a64aaa 100644 --- a/antigravity-awesome-skills/docs/users/bundles.md +++ b/antigravity-awesome-skills/docs/users/bundles.md @@ -1061,4 +1061,4 @@ Found a skill that should be in a bundle? Or want to create a new bundle? [Open --- -_Last updated: June 2026 | Total Skills: 1,894+ | Total Bundles: 59_ +_Last updated: June 2026 | Total Skills: 1,896+ | Total Bundles: 59_ diff --git a/antigravity-awesome-skills/docs/users/claude-code-skills.md b/antigravity-awesome-skills/docs/users/claude-code-skills.md index 3a05d863..19d8682c 100644 --- a/antigravity-awesome-skills/docs/users/claude-code-skills.md +++ b/antigravity-awesome-skills/docs/users/claude-code-skills.md @@ -12,7 +12,7 @@ Install the library into Claude Code, then invoke focused skills directly in the ## Why use this repo for Claude Code -- It includes 1,894+ skills instead of a narrow single-domain starter pack. +- It includes 1,896+ skills instead of a narrow single-domain starter pack. - It supports the standard `.claude/skills/` path and the Claude Code plugin marketplace flow. - It also ships generated bundle plugins so teams can install focused packs like `Essentials` or `Security Developer` from the marketplace metadata. - It includes onboarding docs, bundles, and workflows so new users do not need to guess where to begin. diff --git a/antigravity-awesome-skills/docs/users/gemini-cli-skills.md b/antigravity-awesome-skills/docs/users/gemini-cli-skills.md index 13c9eea6..9f2d6041 100644 --- a/antigravity-awesome-skills/docs/users/gemini-cli-skills.md +++ b/antigravity-awesome-skills/docs/users/gemini-cli-skills.md @@ -12,7 +12,7 @@ Install into the Gemini skills path, then ask Gemini to apply one skill at a tim - It installs directly into the expected Gemini skills path. - It includes both core software engineering skills and deeper agent/LLM-oriented skills. -- It helps new users get started with bundles and workflows rather than forcing a cold start from 1,894+ files. +- It helps new users get started with bundles and workflows rather than forcing a cold start from 1,896+ files. - It is useful whether you want a broad internal skill library or a single repo to test many workflows quickly. ## Install Gemini CLI Skills diff --git a/antigravity-awesome-skills/docs/users/getting-started.md b/antigravity-awesome-skills/docs/users/getting-started.md index 7dde49a5..ad657c07 100644 --- a/antigravity-awesome-skills/docs/users/getting-started.md +++ b/antigravity-awesome-skills/docs/users/getting-started.md @@ -1,4 +1,4 @@ -# Getting Started with Antigravity Awesome Skills (V13.7.0) +# Getting Started with Antigravity Awesome Skills (V13.9.0) **New here? This guide will help you supercharge your AI Agent in 5 minutes.** diff --git a/antigravity-awesome-skills/docs/users/kiro-integration.md b/antigravity-awesome-skills/docs/users/kiro-integration.md index db99f2d1..45d876c3 100644 --- a/antigravity-awesome-skills/docs/users/kiro-integration.md +++ b/antigravity-awesome-skills/docs/users/kiro-integration.md @@ -18,7 +18,7 @@ Kiro is AWS's agentic AI IDE that combines: Kiro's agentic capabilities are enhanced by skills that provide: -- **Domain expertise** across 1,894+ specialized areas +- **Domain expertise** across 1,896+ specialized areas - **Best practices** from Anthropic, OpenAI, Google, Microsoft, and AWS - **Workflow automation** for common development tasks - **AWS-specific patterns** for serverless, infrastructure, and cloud architecture diff --git a/antigravity-awesome-skills/docs/users/usage.md b/antigravity-awesome-skills/docs/users/usage.md index 7eab12e6..bddcfe42 100644 --- a/antigravity-awesome-skills/docs/users/usage.md +++ b/antigravity-awesome-skills/docs/users/usage.md @@ -14,7 +14,7 @@ If you came in through a **Claude Code** or **Codex** plugin instead of a full l When you ran `npx antigravity-awesome-skills` or cloned the repository, you: -✅ **Downloaded 1,894+ skill files** to your computer (default: `~/.agents/skills/`; or a custom path like `~/.agent/skills/` if you used `--path`) +✅ **Downloaded 1,896+ skill files** to your computer (default: `~/.agents/skills/`; or a custom path like `~/.agent/skills/` if you used `--path`) ✅ **Made them available** to your AI assistant ❌ **Did NOT enable them all automatically** (they're just sitting there, waiting) @@ -34,7 +34,7 @@ Bundles are **curated groups** of skills organized by role. They help you decide **Analogy:** -- You installed a toolbox with 1,894+ tools (✅ done) +- You installed a toolbox with 1,896+ tools (✅ done) - Bundles are like **labeled organizer trays** saying: "If you're a carpenter, start with these 10 tools" - You can either **pick skills from the tray** or install that tray as a focused marketplace bundle plugin @@ -212,7 +212,7 @@ Let's actually use a skill right now. Follow these steps: ## Step 5: Picking Your First Skills (Practical Advice) -Don't try to use all 1,894+ skills at once. Here's a sensible approach: +Don't try to use all 1,896+ skills at once. Here's a sensible approach: If you want a tool-specific starting point before choosing skills, use: @@ -343,7 +343,7 @@ Usually no, but if your AI doesn't recognize a skill: ### "Can I load all skills into the model at once?" -No. Even though you have 1,894+ skills installed locally, you should **not** concatenate every `SKILL.md` into a single system prompt or context block. +No. Even though you have 1,896+ skills installed locally, you should **not** concatenate every `SKILL.md` into a single system prompt or context block. The intended pattern is: diff --git a/antigravity-awesome-skills/docs/users/visual-guide.md b/antigravity-awesome-skills/docs/users/visual-guide.md index 682274f8..c618b6d4 100644 --- a/antigravity-awesome-skills/docs/users/visual-guide.md +++ b/antigravity-awesome-skills/docs/users/visual-guide.md @@ -34,7 +34,7 @@ antigravity-awesome-skills/ ├── 📄 CONTRIBUTING.md ← Contributor workflow ├── 📄 CATALOG.md ← Full generated catalog │ -├── 📁 skills/ ← 1,894+ skills live here +├── 📁 skills/ ← 1,896+ skills live here │ │ │ ├── 📁 brainstorming/ │ │ └── 📄 SKILL.md ← Skill definition @@ -47,7 +47,7 @@ antigravity-awesome-skills/ │ │ └── 📁 2d-games/ │ │ └── 📄 SKILL.md ← Nested skills also supported │ │ -│ └── ... (1,894+ total) +│ └── ... (1,896+ total) │ ├── 📁 apps/ │ └── 📁 web-app/ ← Interactive browser @@ -100,7 +100,7 @@ antigravity-awesome-skills/ ``` ┌─────────────────────────┐ - │ 1,894+ SKILLS │ + │ 1,896+ SKILLS │ └────────────┬────────────┘ │ ┌────────────────────────┼────────────────────────┐ @@ -201,7 +201,7 @@ If you want a workspace-style manual install instead, cloning into `.agent/skill │ ├── 📁 brainstorming/ │ │ ├── 📁 stripe-integration/ │ │ ├── 📁 react-best-practices/ │ -│ └── ... (1,894+ total) │ +│ └── ... (1,896+ total) │ └─────────────────────────────────────────┘ ``` diff --git a/antigravity-awesome-skills/package-lock.json b/antigravity-awesome-skills/package-lock.json index 53642a04..5c7f3f43 100644 --- a/antigravity-awesome-skills/package-lock.json +++ b/antigravity-awesome-skills/package-lock.json @@ -1,20 +1,45 @@ { "name": "antigravity-awesome-skills", - "version": "13.7.0", + "version": "13.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "antigravity-awesome-skills", - "version": "13.7.0", + "version": "13.9.0", "license": "MIT", "dependencies": { + "sanitize-filename": "^1.6.4", "yaml": "^2.9.0" }, "bin": { "antigravity-awesome-skills": "tools/bin/install.js" } }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "license": "(WTFPL OR MIT)" + }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", diff --git a/antigravity-awesome-skills/package.json b/antigravity-awesome-skills/package.json index 029f7e18..348568a5 100644 --- a/antigravity-awesome-skills/package.json +++ b/antigravity-awesome-skills/package.json @@ -1,7 +1,7 @@ { "name": "antigravity-awesome-skills", - "version": "13.7.0", - "description": "1,894+ agentic skills for Claude Code, Gemini CLI, Cursor, Antigravity & more. Installer CLI.", + "version": "13.9.0", + "description": "1,896+ agentic skills for Claude Code, Gemini CLI, Cursor, Antigravity & more. Installer CLI.", "license": "MIT", "scripts": { "validate": "node tools/scripts/run-python.js tools/scripts/validate_skills.py", @@ -68,6 +68,7 @@ "app:preview": "cd apps/web-app && npm run preview" }, "dependencies": { + "sanitize-filename": "^1.6.4", "yaml": "^2.9.0" }, "repository": { diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/.claude-plugin/plugin.json index d0624831..2e93c510 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "antigravity-awesome-skills", - "version": "13.7.0", - "description": "Plugin-safe Claude Code distribution of Antigravity Awesome Skills with 1,846 supported skills.", + "version": "13.9.0", + "description": "Plugin-safe Claude Code distribution of Antigravity Awesome Skills with 1,848 supported skills.", "author": { "name": "sickn33 and contributors", "url": "https://github.com/sickn33/antigravity-awesome-skills" diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/full_audit.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/full_audit.py index 13486982..76396878 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/full_audit.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/full_audit.py @@ -27,6 +27,19 @@ import time from datetime import datetime, timezone from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Imports from the 007 config hub (same directory) # --------------------------------------------------------------------------- @@ -397,31 +410,30 @@ def _phase1_surface_mapping(target: Path, verbose: bool = False) -> dict: _config_extensions = {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf", ".env"} - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in SKIP_DIRECTORIES] + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in SKIP_DIRECTORIES for part in fpath.parts): + continue + fname = fpath.name + total_files += 1 + suffix = fpath.suffix.lower() - for fname in filenames: - total_files += 1 - fpath = Path(root) / fname - suffix = fpath.suffix.lower() + # Categorize by extension + ext_key = suffix if suffix else "(no extension)" + files_by_type[ext_key] = files_by_type.get(ext_key, 0) + 1 - # Categorize by extension - ext_key = suffix if suffix else "(no extension)" - files_by_type[ext_key] = files_by_type.get(ext_key, 0) + 1 + # Detect entry points + for pat in _entry_point_patterns: + if pat.search(fname) or pat.search(str(fpath)): + entry_points.append(str(fpath)) + break - # Detect entry points - for pat in _entry_point_patterns: - if pat.search(fname) or pat.search(str(fpath)): - entry_points.append(str(fpath)) - break + # Detect dependency files + if fname.lower() in _dep_file_names: + dependency_files.append(str(fpath)) - # Detect dependency files - if fname.lower() in _dep_file_names: - dependency_files.append(str(fpath)) - - # Detect config files - if suffix in _config_extensions or fname.lower().startswith(".env"): - config_files.append(str(fpath)) + # Detect config files + if suffix in _config_extensions or fname.lower().startswith(".env"): + config_files.append(str(fpath)) # Sort by count descending sorted_types = sorted(files_by_type.items(), key=lambda x: x[1], reverse=True) @@ -1053,7 +1065,7 @@ def run_audit( ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/quick_scan.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/quick_scan.py index 0542f824..b7d3d6c9 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/quick_scan.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/quick_scan.py @@ -17,6 +17,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Imports from the 007 config hub (same directory) # --------------------------------------------------------------------------- @@ -134,20 +147,16 @@ def collect_files(target: Path, logger) -> list[Path]: files: list[Path] = [] max_files = LIMITS["max_files_per_scan"] - for root, dirs, filenames in os.walk(target): - # Prune skipped directories in-place so os.walk does not descend - dirs[:] = [d for d in dirs if not _should_skip_dir(d)] - - for fname in filenames: - if len(files) >= max_files: - logger.warning( - "Reached max_files_per_scan limit (%d). Stopping collection.", max_files - ) - return files - - fpath = Path(root) / fname - if _is_scannable(fpath): - files.append(fpath) + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(_should_skip_dir(part) for part in fpath.parts): + continue + if len(files) >= max_files: + logger.warning( + "Reached max_files_per_scan limit (%d). Stopping collection.", max_files + ) + return files + if _is_scannable(fpath): + files.append(fpath) return files @@ -368,7 +377,7 @@ def run_scan(target_path: str, output_format: str = "text", verbose: bool = Fals logger = setup_logging("007-quick-scan") ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/scanners/dependency_scanner.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/scanners/dependency_scanner.py index 26798c67..c16f1e31 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/scanners/dependency_scanner.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/scanners/dependency_scanner.py @@ -17,6 +17,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Import from the 007 config hub (parent directory) # --------------------------------------------------------------------------- @@ -850,27 +863,26 @@ def discover_dependency_files(target: Path) -> list[Path]: """ found: list[Path] = [] - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in config.SKIP_DIRECTORIES] + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in config.SKIP_DIRECTORIES for part in fpath.parts): + continue + fname = fpath.name + fname_lower = fname.lower() - for fname in filenames: - fpath = Path(root) / fname - fname_lower = fname.lower() + # Exact name matches + if fname in ALL_DEP_FILES: + found.append(fpath) + continue - # Exact name matches - if fname in ALL_DEP_FILES: - found.append(fpath) - continue + # requirements*.txt variants + if _REQUIREMENTS_RE.match(fname): + found.append(fpath) + continue - # requirements*.txt variants - if _REQUIREMENTS_RE.match(fname): - found.append(fpath) - continue - - # Docker files (prefix match) - if any(fname_lower.startswith(prefix.lower()) for prefix in DOCKER_PREFIXES): - found.append(fpath) - continue + # Docker files (prefix match) + if any(fname_lower.startswith(prefix.lower()) for prefix in DOCKER_PREFIXES): + found.append(fpath) + continue return found @@ -1158,7 +1170,7 @@ def run_scan( config.ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/scanners/injection_scanner.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/scanners/injection_scanner.py index 0bdb59f6..32029775 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/scanners/injection_scanner.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/scanners/injection_scanner.py @@ -20,6 +20,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Import from the 007 config hub (parent directory) # --------------------------------------------------------------------------- @@ -546,19 +559,16 @@ def collect_files(target: Path) -> list[Path]: files: list[Path] = [] max_files = config.LIMITS["max_files_per_scan"] - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in config.SKIP_DIRECTORIES] - - for fname in filenames: - if len(files) >= max_files: - logger.warning( - "Reached max_files_per_scan limit (%d). Stopping.", max_files - ) - return files - - fpath = Path(root) / fname - if _should_scan_file(fpath): - files.append(fpath) + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in config.SKIP_DIRECTORIES for part in fpath.parts): + continue + if len(files) >= max_files: + logger.warning( + "Reached max_files_per_scan limit (%d). Stopping.", max_files + ) + return files + if _should_scan_file(fpath): + files.append(fpath) return files @@ -961,7 +971,7 @@ def run_scan( config.ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/scanners/secrets_scanner.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/scanners/secrets_scanner.py index 783bf3ae..68711c36 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/scanners/secrets_scanner.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/scanners/secrets_scanner.py @@ -20,6 +20,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Import from the 007 config hub (parent directory) # --------------------------------------------------------------------------- @@ -375,19 +388,16 @@ def collect_files(target: Path) -> list[Path]: files: list[Path] = [] max_files = config.LIMITS["max_files_per_scan"] - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in config.SKIP_DIRECTORIES] - - for fname in filenames: - if len(files) >= max_files: - logger.warning( - "Reached max_files_per_scan limit (%d). Stopping.", max_files - ) - return files - - fpath = Path(root) / fname - if _should_scan_file(fpath): - files.append(fpath) + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in config.SKIP_DIRECTORIES for part in fpath.parts): + continue + if len(files) >= max_files: + logger.warning( + "Reached max_files_per_scan limit (%d). Stopping.", max_files + ) + return files + if _should_scan_file(fpath): + files.append(fpath) return files @@ -869,7 +879,7 @@ def run_scan( config.ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/score_calculator.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/score_calculator.py index efe1b008..e7e7dded 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/score_calculator.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/007/scripts/score_calculator.py @@ -24,6 +24,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Imports from the 007 config hub (same directory) # --------------------------------------------------------------------------- @@ -141,18 +154,17 @@ def _collect_source_files(target: Path) -> list[Path]: files: list[Path] = [] max_files = LIMITS["max_files_per_scan"] - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in SKIP_DIRECTORIES] - for fname in filenames: - if len(files) >= max_files: - return files - fpath = Path(root) / fname - suffix = fpath.suffix.lower() - name = fpath.name.lower() - for ext in SCANNABLE_EXTENSIONS: - if name.endswith(ext) or suffix == ext: - files.append(fpath) - break + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in SKIP_DIRECTORIES for part in fpath.parts): + continue + if len(files) >= max_files: + return files + suffix = fpath.suffix.lower() + name = fpath.name.lower() + for ext in SCANNABLE_EXTENSIONS: + if name.endswith(ext) or suffix == ext: + files.append(fpath) + break return files @@ -529,7 +541,7 @@ def run_score( ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py index 7b822aef..d42509dd 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py @@ -14,6 +14,20 @@ import socket import requests from urllib.parse import urlparse from typing import Optional, Dict, Any +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path API_BASE_URL = "https://2slides.com/api/v1" @@ -124,7 +138,7 @@ def download_slides_pages_voices( zip_response.raise_for_status() # Save to file - with open(output_path, 'wb') as f: + with safe_user_path(output_path).open('wb') as f: for chunk in zip_response.iter_content(chunk_size=8192): f.write(chunk) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/audio-transcriber/scripts/transcribe.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/audio-transcriber/scripts/transcribe.py index 41ea455c..95d4402f 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/audio-transcriber/scripts/transcribe.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/audio-transcriber/scripts/transcribe.py @@ -12,6 +12,19 @@ import shutil from datetime import datetime from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # Rich for beautiful terminal output try: from rich.console import Console @@ -386,9 +399,9 @@ def save_outputs(transcript_text, ata_text, audio_file, output_dir="."): # Sempre salva transcript transcript_filename = f"transcript-{timestamp}.md" - transcript_path = Path(output_dir) / transcript_filename + transcript_path = safe_user_path(output_dir) / transcript_filename - with open(transcript_path, 'w', encoding='utf-8') as f: + with transcript_path.open('w', encoding='utf-8') as f: f.write(transcript_text) console.print(f"[green]✅ Transcript salvo:[/green] {transcript_filename}") @@ -397,9 +410,9 @@ def save_outputs(transcript_text, ata_text, audio_file, output_dir="."): ata_path = None if ata_text: ata_filename = f"ata-{timestamp}.md" - ata_path = Path(output_dir) / ata_filename + ata_path = safe_user_path(output_dir) / ata_filename - with open(ata_path, 'w', encoding='utf-8') as f: + with ata_path.open('w', encoding='utf-8') as f: f.write(ata_text) console.print(f"[green]✅ Ata salva:[/green] {ata_filename}") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/bumblebee/scripts/render_report.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/bumblebee/scripts/render_report.py index 24547c48..b8e557c7 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/bumblebee/scripts/render_report.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/bumblebee/scripts/render_report.py @@ -30,6 +30,19 @@ import sys from collections import Counter, defaultdict from datetime import datetime, timezone from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any @@ -336,11 +349,11 @@ def build_report(records: list[dict[str, Any]], source_path: Path) -> str: def main(argv: list[str]) -> int: if len(argv) != 3: - print(f"usage: {Path(argv[0]).name} ", file=sys.stderr) + print(f"usage: {safe_user_path(argv[0]).name} ", file=sys.stderr) return 1 - input_path = Path(argv[1]) - output_path = Path(argv[2]) + input_path = safe_user_path(argv[1]) + output_path = safe_user_path(argv[2]) if not input_path.exists() or input_path.stat().st_size == 0: print(f"error: {input_path} is missing or empty", file=sys.stderr) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/claude-monitor/scripts/monitor.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/claude-monitor/scripts/monitor.py index 651fcd50..74b89e8e 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/claude-monitor/scripts/monitor.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/claude-monitor/scripts/monitor.py @@ -20,6 +20,19 @@ import time from datetime import datetime from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + try: import psutil except ImportError: @@ -281,8 +294,8 @@ def main(): else: output_path = f"monitor_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" - with open(output_path, "w", encoding="utf-8") as f: - json.dump(output_data, f, indent=2, ensure_ascii=False) + with safe_user_path(output_path).open("w", encoding="utf-8") as f: + f.write(json.dumps(output_data, indent=2, ensure_ascii=False)) print(f"\nLog salvo em: {output_path}\n") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/code-polish/SKILL.md b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/code-polish/SKILL.md new file mode 100644 index 00000000..af88dc4e --- /dev/null +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/code-polish/SKILL.md @@ -0,0 +1,192 @@ +--- +name: code-polish +description: Rewrites unprofessional code comments into clear ones and performs non-semantic cleanup. Use to professionalize code without altering logic or behavior. +risk: critical +source: community +date_added: "2026-07-02" +--- + +# Code Polish + +A constraint-based protocol for normalizing code comments and performing safe, non-semantic cleanup. This skill exists because human-written code tends to carry casual, outdated, or missing comments, while the goal is professional-grade documentation without touching behavior. + +This file is self-contained. Do not require any other skill file to execute this protocol. + +## Prime Directive + +Comments and non-semantic cleanup are the job. Logic is never the job. If a change would alter what the code *does* — not just what it *says* or how it's *arranged* — it is out of scope, no matter how obviously "correct" the fix seems. + +--- + +## When to Use + +Apply this skill when: +- The user asks to "clean up," "professionalize," or "polish" existing code +- Code is being prepped for code review, handoff, open-sourcing, or documentation +- A file has a mix of human and AI-written comments and needs one consistent, professional voice +- Comments are outdated, missing, redundant, or written casually (venting, placeholders, inside jokes) +- The user wants comments improved but explicitly does **not** want logic touched + +Do not apply this skill when: +- The user wants a bug fixed or behavior changed (that's a different job — logic edits are out of scope here) +- The user wants a full rewrite or architectural restructuring +- The only ask is adding new features or functionality + +--- + +## Phase 0 — Full Read + +Before editing anything, read the entire file (or the entire relevant module if the codebase is large — not just the function in question). Do not comment or clean incrementally while still reading. A comment written without full context is a guess, and guesses are how "professional" comments end up wrong. + +Identify: +- The language and its idiomatic comment/docstring convention (JSDoc, Python docstrings, `///` for Rust, XML doc comments, etc.) +- Any existing project comment style already in use elsewhere in the file — match it rather than importing a foreign convention +- Any comment that encodes real, non-obvious information (race conditions, workarounds for external bugs, "don't reorder this" warnings, business-rule justifications) + +--- + +## Phase 1 — Comment Audit + +Classify every existing comment into one of these categories before touching it: + +| Category | Example | Action | +|---|---|---| +| **Junk / venting** | `// wtf is this`, `// idk why but it works` | Remove tone, extract any real information underneath, rewrite professionally — or delete if it truly holds zero information | +| **Placeholder** | `// fix later`, `// TODO hack` | Convert to a proper `TODO:` note with the actual concern stated plainly, or remove if stale/resolved | +| **Dead code comments** | Blocks of commented-out code | Remove, unless the surrounding context makes clear it's intentionally preserved (e.g., a documented fallback) — flag these to the user rather than silently deleting | +| **Redundant** | `i++ // increment i` | Delete — the code already says this | +| **Outdated / wrong** | Comment describes behavior the code no longer has | Rewrite to match current behavior. Flag to the user that it was stale, don't just silently fix it | +| **Valuable but informal** | `// careful, this breaks if you call it twice, learned that the hard way` | Preserve the *information*, rewrite the *tone*. Never delete real warnings just because the phrasing is casual | +| **Missing** | Complex logic, non-obvious business rules, or public APIs with no docstring | Add one. Don't over-comment simple, self-explanatory lines | + +--- + +## Phase 2 — Non-Semantic Cleanup + +Scope is strictly limited to changes that cannot alter behavior: + +- Consistent indentation and whitespace +- Consistent brace/bracket style matching the surrounding file +- Removing truly dead code (unreachable blocks) — only when unambiguous, and flagged in the summary +- Splitting overly long lines for readability +- Local variable renaming for clarity is allowed **only** for private/local-scope names, and only when the improvement is unambiguous — never rename anything exported, public, or referenced across files without calling it out explicitly first + +Anything beyond this — reordering logic, extracting functions, changing control flow, altering algorithms — is out of scope for this skill. + +--- + +## Phase 3 — Comment Rewrite / Addition + +Apply these standards to every comment touched or added: + +- **Explain why, not what.** The code already shows *what* it does; a comment earns its place by explaining intent, tradeoffs, or non-obvious constraints. +- **Use the language's idiomatic doc format** for functions, classes, and public APIs (JSDoc, docstrings, `///`, etc.) — match the convention already used elsewhere in the file if one exists. +- **Be concise.** No padding, no restating the obvious, no filler sentences. +- **No informal register.** No jokes, no venting, no first-person asides ("I think this works because..."). +- **No AI-tell phrasing.** Avoid generic filler like "This function is responsible for..." or "Note that..." padding, and avoid em-dashes. Write plainly and directly, the way a careful senior engineer would. +- **Don't invent behavior.** If you're not certain why something is done a certain way, say what the code does, not a fabricated justification for why. + +--- + +## Phase 4 — Verification + +Before presenting the result: + +- Confirm the edited file's logic is behaviorally identical to the original — comments and whitespace are the only permitted diffs, plus whatever narrow Phase 2 cleanup was done. +- Re-read the diff end to end, not just the changed lines in isolation, to catch anything that accidentally shifted meaning. +- If a rewritten comment removes information that was present in the original (even informally stated), that's a failure — go back and preserve it. + +--- + +## Phase 5 — Report Back + +Summarize for the user, don't just hand back a silent diff: +- How many comments were rewritten, added, or removed, and why +- Any comments flagged as "informal but contained a real warning" — confirm the information was preserved +- Any dead code or stale comments removed, listed explicitly +- Anything you were unsure about and left alone rather than guessing + +--- + +## Examples + +**Junk / venting → professional** +```js +// before +// ugh this took forever to figure out. api rate limits us super hard in prod so we have to do exponential backoff here. just leave it alone +function retryFetch(url, attempts) { ... } + +// after +// Uses exponential backoff to handle aggressive API rate-limiting in production. +function retryFetch(url, attempts) { ... } +``` + +**Redundant → removed** +```python +# before +count += 1 # increment count by 1 + +# after +count += 1 +``` + +**Valuable but informal → tone rewritten, information preserved** +```python +# before +# careful, this breaks if you call it twice, learned that the hard way + +# after +# Not idempotent: calling this more than once per session corrupts the +# cache index. Callers must guard against duplicate invocation. +``` + +**Missing → added** +```java +// before +public double calculate(double base, int tier) { + return base * (tier > 2 ? 0.85 : 1.0); +} + +// after +/** + * Applies the loyalty discount. Tiers above 2 qualify for a 15% discount; + * this threshold matches the current pricing policy, not a technical limit. + */ +public double calculate(double base, int tier) { + return base * (tier > 2 ? 0.85 : 1.0); +} +``` + +**Outdated / wrong → corrected and flagged** +```go +// before +// returns nil if user not found +func GetUser(id string) (*User, error) { ... } // now returns ErrNotFound instead + +// after +// Returns ErrNotFound if the user does not exist. +func GetUser(id string) (*User, error) { ... } +// (flagged to user: original comment was stale — function used to return nil, +// now returns a named error) +``` + +--- + +## Security & Safety Notes + +This skill never: +- Changes program logic, control flow, or algorithmic behavior +- Restructures code (extracting/inlining functions, reordering execution, changing architecture) +- Renames anything public, exported, or cross-referenced without explicit confirmation +- Deletes a comment solely because its tone is casual, without checking whether it carries real information first +- Fabricates a rationale for a comment when the actual reason isn't knowable from context — state what's certain only + +--- + +## Limitations + +- Cannot verify runtime behavior — Phase 4 is a read-through diff check, not a test run. For anything beyond trivial files, the user should still run the actual test suite after applying this skill. +- Judgment calls on ambiguous cases (e.g., "is this dead code intentional or forgotten?") default to flagging rather than guessing — this means some cleanup will need a quick human yes/no rather than happening silently. +- Not a substitute for a linter or formatter — Phase 2 cleanup is deliberately conservative and won't enforce a full style guide (e.g., max line length rules, import ordering) unless that's trivially inferable from the surrounding file. +- Comment quality is bounded by how well the code's actual intent can be inferred from context. If the "why" genuinely isn't recoverable from the file (no domain knowledge, no commit history, no ticket references available), the honest output is a comment describing *what*, not a confident but invented *why*. +- Large files or unfamiliar codebases increase the risk of Phase 0 missing context that would have changed a comment's wording — flag uncertainty in the Phase 5 report rather than presenting low-confidence rewrites as settled. diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/capture_screenshots.mjs b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/capture_screenshots.mjs index b88b7fb8..3331c5cd 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/capture_screenshots.mjs +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/capture_screenshots.mjs @@ -12,12 +12,33 @@ // // Usage: node capture_screenshots.mjs [--mode remote|local] [--concurrency 2] +import sanitizeFilename from 'sanitize-filename'; import { readdirSync, readFileSync, mkdirSync, existsSync } from 'fs'; -import { join } from 'path'; +import { isAbsolute, join, relative, resolve } from 'path'; import { spawnSync } from 'child_process'; import { parseFrontmatter } from './md_utils.mjs'; const args = process.argv.slice(2); +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} + +function safeCliPath(pathValue, baseDir = process.cwd()) { + const root = resolve(baseDir); + const target = resolve(root, ...sanitizePathSegments(pathValue)); + const rel = relative(root, target); + if (rel.startsWith('..') || isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + if (args.includes('--help') || args.includes('-h') || args.length === 0) { console.error(`Usage: node capture_screenshots.mjs [options] @@ -38,7 +59,7 @@ Options: process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1); } -const dir = args[0]; +const dir = safeCliPath(args[0]); const modeIdx = args.indexOf('--mode'); const browseMode = modeIdx !== -1 ? args[modeIdx + 1] : 'remote'; const modeFlag = browseMode === 'local' ? '--local' : '--remote'; @@ -63,7 +84,7 @@ if (concurrency > 1) { concurrency = 1; } -const shotsDir = join(dir, 'screenshots'); +const shotsDir = safeCliPath('screenshots', dir); mkdirSync(shotsDir, { recursive: true }); function run(cmd, args, { timeout = 30000 } = {}) { diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/compile_report.mjs b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/compile_report.mjs index 19be1b9b..342c8621 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/compile_report.mjs +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/compile_report.mjs @@ -9,6 +9,7 @@ import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { basename, dirname, join, relative, resolve } from 'path'; import { fileURLToPath } from 'url'; +import sanitizeFilename from 'sanitize-filename'; import { parseFrontmatter, parseBody, parseSections } from './md_utils.mjs'; const __filename = fileURLToPath(import.meta.url); @@ -17,9 +18,19 @@ const __dirname = dirname(__filename); const args = process.argv.slice(2); const SAFE_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} + function safeJoin(base, ...parts) { const root = resolve(base); - const target = resolve(root, ...parts); + const target = resolve(root, ...parts.flatMap(sanitizePathSegments)); const rel = relative(root, target); if (rel.startsWith('..') || rel.startsWith('/')) { throw new Error(`Path escapes research directory: ${parts.join('/')}`); @@ -32,7 +43,7 @@ function safeResearchDir(rawDir) { throw new Error('Research directory is required'); } const root = resolve(process.cwd()); - const target = resolve(root, rawDir); + const target = safeJoin(root, rawDir); const rel = relative(root, target); if ((rel.startsWith('..') || rel.startsWith('/')) && process.env.COMPETITOR_ANALYSIS_ALLOW_EXTERNAL_DIR !== '1') { throw new Error('Research directory must stay under the current working directory'); diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/extract_vs_names.mjs b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/extract_vs_names.mjs index 294883d9..0f93b411 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/extract_vs_names.mjs +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/extract_vs_names.mjs @@ -9,10 +9,31 @@ // Output: newline-delimited JSON to stdout, one object per candidate: // { "name": "serper", "hits": 3, "domain": "serper.dev", "example": "Tavily vs Serper..." } +import sanitizeFilename from 'sanitize-filename'; import { readdirSync, readFileSync } from 'fs'; -import { join } from 'path'; +import { isAbsolute, join, relative, resolve } from 'path'; const args = process.argv.slice(2); +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} + +function safeCliPath(pathValue, baseDir = process.cwd()) { + const root = resolve(baseDir); + const target = resolve(root, ...sanitizePathSegments(pathValue)); + const rel = relative(root, target); + if (rel.startsWith('..') || isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + if (args.includes('--help') || args.includes('-h') || args.length === 0) { console.error(`Usage: node extract_vs_names.mjs [--prefix ] [--seed ""] @@ -28,7 +49,7 @@ Options: process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1); } -const dir = args[0]; +const dir = safeCliPath(args[0]); const prefixIdx = args.indexOf('--prefix'); const prefix = prefixIdx !== -1 && args[prefixIdx + 1] ? args[prefixIdx + 1] : 'competitor'; const seedIdx = args.indexOf('--seed'); diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/gate_candidates.mjs b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/gate_candidates.mjs index e7b30612..93960761 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/gate_candidates.mjs +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/gate_candidates.mjs @@ -14,6 +14,7 @@ // { "url": "https://foo.com", "status": "PASS" | "REJECT" | "UNKNOWN", // "matched_includes": [...], "matched_excludes": [...], "title": "...", "hero": "..." } +import sanitizeFilename from 'sanitize-filename'; import { execFile } from 'child_process'; import { promisify } from 'util'; import { readFileSync } from 'fs'; @@ -25,6 +26,26 @@ import { readFileSync } from 'fs'; const execFileAsync = promisify(execFile); const args = process.argv.slice(2); +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} + +function safeCliPath(pathValue, baseDir = process.cwd()) { + const root = resolve(baseDir); + const target = resolve(root, ...sanitizePathSegments(pathValue)); + const rel = relative(root, target); + if (rel.startsWith('..') || isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + if (args.includes('--help') || args.includes('-h')) { console.error(`Usage: cat urls.txt | node gate_candidates.mjs [options] diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/list_urls.mjs b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/list_urls.mjs index 75631c38..19fd7fb5 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/list_urls.mjs +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/list_urls.mjs @@ -5,10 +5,31 @@ // Reads all {prefix}_discovery_batch_*.json files, deduplicates by domain, // outputs one URL per line to stdout, stats to stderr. +import sanitizeFilename from 'sanitize-filename'; import { readdirSync, readFileSync } from 'fs'; -import { join } from 'path'; +import { isAbsolute, join, relative, resolve } from 'path'; const args = process.argv.slice(2); +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} + +function safeCliPath(pathValue, baseDir = process.cwd()) { + const root = resolve(baseDir); + const target = resolve(root, ...sanitizePathSegments(pathValue)); + const rel = relative(root, target); + if (rel.startsWith('..') || isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + if (args.includes('--help') || args.includes('-h') || args.length === 0) { console.error(`Usage: node list_urls.mjs [--prefix ] @@ -26,7 +47,7 @@ Examples: process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1); } -const dir = args[0]; +const dir = safeCliPath(args[0]); const prefixIdx = args.indexOf('--prefix'); const prefix = prefixIdx !== -1 && args[prefixIdx + 1] ? args[prefixIdx + 1] : 'competitor'; diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/merge_partials.mjs b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/merge_partials.mjs index 402e04b5..c440143c 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/merge_partials.mjs +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/competitor-analysis/scripts/merge_partials.mjs @@ -18,11 +18,32 @@ // // Usage: node merge_partials.mjs +import sanitizeFilename from 'sanitize-filename'; import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; -import { join } from 'path'; +import { isAbsolute, join, relative, resolve } from 'path'; import { parseFrontmatter, parseBody, parseSections } from './md_utils.mjs'; const args = process.argv.slice(2); +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} + +function safeCliPath(pathValue, baseDir = process.cwd()) { + const root = resolve(baseDir); + const target = resolve(root, ...sanitizePathSegments(pathValue)); + const rel = relative(root, target); + if (rel.startsWith('..') || isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + if (args.includes('--help') || args.includes('-h') || args.length === 0) { console.error(`Usage: node merge_partials.mjs @@ -31,8 +52,8 @@ Reads {dir}/partials/{slug}.{lane}.md files and writes consolidated process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1); } -const dir = args[0]; -const partialsDir = join(dir, 'partials'); +const dir = safeCliPath(args[0]); +const partialsDir = safeCliPath('partials', dir); const LANES = ['marketing', 'discussion', 'social', 'news', 'technical', 'battle']; diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/content-creator/scripts/brand_voice_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/content-creator/scripts/brand_voice_analyzer.py index 92ab6f70..ed138756 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/content-creator/scripts/brand_voice_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/content-creator/scripts/brand_voice_analyzer.py @@ -6,6 +6,20 @@ Brand Voice Analyzer - Analyzes content to establish and maintain brand voice co import re from typing import Dict, List, Tuple import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class BrandVoiceAnalyzer: def __init__(self): @@ -176,7 +190,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() output_format = sys.argv[2] if len(sys.argv) > 2 else 'text' diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/content-creator/scripts/seo_optimizer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/content-creator/scripts/seo_optimizer.py index 8e77aee2..a346858a 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/content-creator/scripts/seo_optimizer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/content-creator/scripts/seo_optimizer.py @@ -6,6 +6,20 @@ SEO Content Optimizer - Analyzes and optimizes content for SEO import re from typing import Dict, List, Set import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class SEOOptimizer: def __init__(self): @@ -408,7 +422,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() keyword = sys.argv[2] if len(sys.argv) > 2 else None diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/docx-official/ooxml/scripts/pack.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/docx-official/ooxml/scripts/pack.py index 1145107a..630622f0 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/docx-official/ooxml/scripts/pack.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/docx-official/ooxml/scripts/pack.py @@ -16,6 +16,19 @@ import zipfile from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def validate_input_tree(input_dir: Path): root = input_dir.resolve(strict=True) for path in input_dir.rglob("*"): @@ -27,6 +40,18 @@ def validate_input_tree(input_dir: Path): raise ValueError(f"Refusing to pack path outside input directory: {path}") from None +def copy_tree_contents(source_dir: Path, target_dir: Path) -> None: + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def main(): parser = argparse.ArgumentParser(description="Pack a directory into an Office file") parser.add_argument("input_directory", help="Unpacked Office document directory") @@ -65,7 +90,7 @@ def pack_document(input_dir, output_file, validate=False): bool: True if successful, False if validation failed """ input_dir = Path(input_dir) - output_file = Path(output_file) + output_file = safe_user_path(output_file) if not input_dir.is_dir(): raise ValueError(f"{input_dir} is not a directory") @@ -76,7 +101,7 @@ def pack_document(input_dir, output_file, validate=False): # Work in temporary directory to avoid modifying original with tempfile.TemporaryDirectory() as temp_dir: temp_content_dir = Path(temp_dir) / "content" - shutil.copytree(input_dir, temp_content_dir) + copy_tree_contents(input_dir, temp_content_dir) # Process XML files to remove pretty-printing whitespace for pattern in ["*.xml", "*.rels"]: @@ -85,10 +110,12 @@ def pack_document(input_dir, output_file, validate=False): # Create final Office file as zip archive output_file.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + temp_zip_path = Path(temp_dir) / "office.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for f in temp_content_dir.rglob("*"): if f.is_file(): zf.write(f, f.relative_to(temp_content_dir)) + output_file.write_bytes(temp_zip_path.read_bytes()) # Validate if requested if validate: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/docx-official/ooxml/scripts/unpack.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/docx-official/ooxml/scripts/unpack.py index 33ed3a35..ef9d69d3 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/docx-official/ooxml/scripts/unpack.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/docx-official/ooxml/scripts/unpack.py @@ -8,6 +8,19 @@ import sys import zipfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + MAX_ARCHIVE_MEMBERS = 5000 MAX_MEMBER_SIZE = 100 * 1024 * 1024 MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024 @@ -30,7 +43,7 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro return destination.parent.mkdir(parents=True, exist_ok=True) - with archive.open(member, "r") as source, open(destination, "wb") as target: + with archive.open(member, "r") as source, safe_user_path(destination).open("wb") as target: shutil.copyfileobj(source, target) @@ -57,7 +70,7 @@ def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path): def extract_archive_safely(input_file: str | Path, output_dir: str | Path): - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) output_path.mkdir(parents=True, exist_ok=True) output_root = output_path.resolve() @@ -82,7 +95,7 @@ def main(argv: list[str] | None = None): raise SystemExit("Usage: python unpack.py ") input_file, output_dir = argv - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) extract_archive_safely(input_file, output_path) pretty_print_xml(output_path) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/drizzle-migration-conflict/scripts/check_drizzle_migrations.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/drizzle-migration-conflict/scripts/check_drizzle_migrations.py index c3a69418..820ab044 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/drizzle-migration-conflict/scripts/check_drizzle_migrations.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/drizzle-migration-conflict/scripts/check_drizzle_migrations.py @@ -20,6 +20,19 @@ import re import sys from dataclasses import asdict, dataclass from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any, Iterable CONFIG_NAME_PATTERN = re.compile(r"^drizzle(?:[.-].+)?\.config\.(?:ts|js|mjs|cjs|mts|cts)$") @@ -169,15 +182,14 @@ def iter_config_files( if explicit_configs: return configs, issues - for current_root, dirnames, filenames in os.walk(root): - dirnames[:] = [name for name in dirnames if name not in SKIP_DIR_NAMES] - base = Path(current_root) - for filename in filenames: - if CONFIG_NAME_PATTERN.match(filename): - path = (base / filename).resolve() - if path not in seen: - seen.add(path) - configs.append(path) + for path in safe_user_path(root).rglob("*"): + if not path.is_file() or any(part in SKIP_DIR_NAMES for part in path.parts): + continue + if CONFIG_NAME_PATTERN.match(path.name): + resolved = path.resolve() + if resolved not in seen: + seen.add(resolved) + configs.append(resolved) return configs, issues @@ -283,13 +295,11 @@ def discover_dirs(args: argparse.Namespace, root: Path) -> tuple[list[Path], lis def iter_text_files(directory: Path) -> Iterable[Path]: - for current_root, dirnames, filenames in os.walk(directory): - dirnames[:] = [name for name in dirnames if name not in SKIP_DIR_NAMES] - base = Path(current_root) - for filename in filenames: - path = base / filename - if path.suffix in TEXT_SUFFIXES: - yield path + for path in safe_user_path(directory).rglob("*"): + if not path.is_file() or any(part in SKIP_DIR_NAMES for part in path.parts): + continue + if path.suffix in TEXT_SUFFIXES: + yield path def has_conflict_markers(path: Path) -> bool: @@ -696,7 +706,7 @@ def report_as_text(root: Path, reports: list[DirectoryReport]) -> str: def main() -> int: args = parse_args() - root = Path(args.root).resolve() + root = safe_user_path(args.root).resolve() dirs, discovery_issues = discover_dirs(args, root) reports: list[DirectoryReport] = [] if discovery_issues: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/expo-ui/scripts/list-components.js b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/expo-ui/scripts/list-components.js index 3a64f7d1..97ced1f6 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/expo-ui/scripts/list-components.js +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/expo-ui/scripts/list-components.js @@ -15,8 +15,12 @@ const fs = require('fs'); const path = require('path'); +const sanitizeFilename = require('sanitize-filename'); -const projectPath = process.argv[2]; +const rawProjectPath = process.argv[2]; +const projectPath = rawProjectPath + ? path.resolve(process.cwd(), sanitizeFilename(path.basename(rawProjectPath))) + : null; const withDocs = process.argv.includes('--docs'); if (!projectPath) { diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/frontend-slides/scripts/extract-pptx.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/frontend-slides/scripts/extract-pptx.py index 498e2a5e..b4b949de 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/frontend-slides/scripts/extract-pptx.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/frontend-slides/scripts/extract-pptx.py @@ -13,6 +13,20 @@ import json import os import sys from pptx import Presentation +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path def extract_pptx(file_path, output_dir="."): @@ -54,7 +68,7 @@ def extract_pptx(file_path, output_dir="."): image_name = f"slide{slide_num + 1}_img{len(slide_data['images']) + 1}.{image_ext}" image_path = os.path.join(assets_dir, image_name) - with open(image_path, "wb") as f: + with safe_user_path(image_path).open("wb") as f: f.write(image_bytes) slide_data["images"].append( @@ -81,14 +95,14 @@ if __name__ == "__main__": sys.exit(1) input_file = sys.argv[1] - output_dir = sys.argv[2] if len(sys.argv) > 2 else "." + output_dir = safe_user_path(sys.argv[2]) if len(sys.argv) > 2 else "." slides = extract_pptx(input_file, output_dir) # Write extracted data as JSON output_path = os.path.join(output_dir, "extracted-slides.json") - with open(output_path, "w") as f: - json.dump(slides, f, indent=2) + with safe_user_path(output_path).open("w") as f: + f.write(json.dumps(slides, indent=2)) print(f"Extracted {len(slides)} slides to {output_path}") for s in slides: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/gemini-omni-flash-api/scripts/video/generate_video.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/gemini-omni-flash-api/scripts/video/generate_video.py index 28ec7964..2c90e88e 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/gemini-omni-flash-api/scripts/video/generate_video.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/gemini-omni-flash-api/scripts/video/generate_video.py @@ -22,6 +22,20 @@ from google import genai # Load local upload helper logic inline to prevent dependency issues sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from upload_file import upload_file, wait_for_active +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path def get_api_key(args): """Retrieves API key from command args or environment.""" @@ -58,6 +72,14 @@ def normalize_file_uri(uri): return f"https://generativelanguage.googleapis.com/files/{file_id}" return uri + +def media_download_url(file_uri): + """Build a media URL only for validated Gemini File API references.""" + file_id = extract_file_id(file_uri) + if not file_id: + raise ValueError("Generated video URI must be a Gemini File API reference.") + return f"https://generativelanguage.googleapis.com/files/{file_id}?alt=media" + def slugify(text): """Converts a text prompt into a safe, descriptive filename slug.""" text = text.lower() @@ -158,7 +180,7 @@ def resolve_or_upload_asset(asset_path, mime_type, api_key, strip_audio=False): # Clean up temporary stripped file if we created one if temp_stripped_path and os.path.exists(temp_stripped_path): try: - os.remove(temp_stripped_path) + safe_user_path(temp_stripped_path).unlink() print(f"Cleaned up temporary video file: {temp_stripped_path}") except Exception as e: print(f"Warning: Failed to remove temporary file {temp_stripped_path}: {e}", file=sys.stderr) @@ -171,8 +193,7 @@ def resolve_or_upload_asset(asset_path, mime_type, api_key, strip_audio=False): def download_video_file(file_uri, output_path, api_key): """Downloads generated video file from URI using alt=media standard in a memory-safe, chunked manner.""" - separator = "&" if "?" in file_uri else "?" - download_url = f"{file_uri}{separator}alt=media" + download_url = media_download_url(file_uri) print(f"Downloading video from {file_uri} to {output_path} in chunked mode...") req = urllib.request.Request(download_url) @@ -184,7 +205,7 @@ def download_video_file(file_uri, output_path, api_key): if parent_dir: os.makedirs(parent_dir, exist_ok=True) - with open(output_path, "wb") as f: + with safe_user_path(output_path).open("wb") as f: while True: chunk = resp.read(8192) if not chunk: @@ -357,7 +378,7 @@ def main(): print(f"Error: Batch JSON file '{args.batch}' not found.", file=sys.stderr) sys.exit(1) try: - with open(args.batch, "r", encoding="utf-8") as f: + with safe_user_path(args.batch).open("r", encoding="utf-8") as f: jobs = json.load(f) if not isinstance(jobs, list): print("Error: Batch JSON file must contain a list/array of job objects.", file=sys.stderr) @@ -375,7 +396,7 @@ def main(): sys.exit(1) jobs = [] - with open(args.prompts_file, "r", encoding="utf-8") as f: + with safe_user_path(args.prompts_file).open("r", encoding="utf-8") as f: for line in f: line = line.strip() if line and not line.startswith("#"): diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/hugging-face-jobs/scripts/finepdfs-stats.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/hugging-face-jobs/scripts/finepdfs-stats.py index 989732b6..401c0167 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/hugging-face-jobs/scripts/finepdfs-stats.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/hugging-face-jobs/scripts/finepdfs-stats.py @@ -42,6 +42,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + import polars as pl from ascii_graph import Pyasciigraph from datasets import Dataset @@ -401,7 +414,7 @@ def main(): print("The 'text' column is never loaded, making this very fast.\n") # Create output directory - output_dir = Path(args.output_dir) + output_dir = safe_user_path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) # Single scan: compute temporal stats diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/hugging-face-model-trainer/scripts/convert_to_gguf.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/hugging-face-model-trainer/scripts/convert_to_gguf.py index 151ad396..1feb4494 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/hugging-face-model-trainer/scripts/convert_to_gguf.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/hugging-face-model-trainer/scripts/convert_to_gguf.py @@ -84,6 +84,7 @@ def run_command(cmd, description): cmd, check=True, capture_output=True, + shell=False, text=True ) if result.stdout: @@ -114,6 +115,14 @@ def require_hf_repo_id(value, name): sys.exit(1) +def safe_filename_component(value, name): + """Allow repo-name text only where it becomes a local filename component.""" + if not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", value): + print(f" Invalid {name}: {value!r}. Use letters, numbers, dots, dashes, or underscores.", file=sys.stderr) + sys.exit(1) + return value + + def env_flag(name): return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"} @@ -230,7 +239,7 @@ gguf_output_dir = "/tmp/gguf_output" os.makedirs(gguf_output_dir, exist_ok=True) convert_script = "/tmp/llama.cpp/convert_hf_to_gguf.py" -model_name = ADAPTER_MODEL.split('/')[-1] +model_name = safe_filename_component(ADAPTER_MODEL.split('/')[-1], "ADAPTER_MODEL repo name") gguf_file = f"{gguf_output_dir}/{model_name}-f16.gguf" print(f" Running conversion...") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/hugo-to-markdown/scripts/inventory_hugo_rules.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/hugo-to-markdown/scripts/inventory_hugo_rules.py index b91230ca..60d62769 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/hugo-to-markdown/scripts/inventory_hugo_rules.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/hugo-to-markdown/scripts/inventory_hugo_rules.py @@ -6,6 +6,19 @@ import re from collections import Counter, defaultdict from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + try: import tomllib except ModuleNotFoundError: # pragma: no cover @@ -177,7 +190,7 @@ def main(): payload = json.dumps(result, indent=2, sort_keys=True) if args.output: - output = Path(args.output) + output = safe_user_path(args.output) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(payload + "\n", encoding="utf-8") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/instagram/scripts/export.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/instagram/scripts/export.py index 3356fa1a..7a790b36 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/instagram/scripts/export.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/instagram/scripts/export.py @@ -17,6 +17,19 @@ import sys from datetime import datetime, timezone from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + sys.path.insert(0, str(Path(__file__).parent)) _db = None @@ -55,11 +68,9 @@ def export_json(records: list, output_dir: Path, name: str) -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"instagram_{name}_{ts}.json" - with open(path, "w", encoding="utf-8") as f: - json.dump( - {"exported_at": datetime.now(timezone.utc).isoformat(), "total": len(records), "data": records}, - f, ensure_ascii=False, indent=2, - ) + payload = {"exported_at": datetime.now(timezone.utc).isoformat(), "total": len(records), "data": records} + with safe_user_path(path).open("w", encoding="utf-8") as f: + f.write(json.dumps(payload, ensure_ascii=False, indent=2)) print(f"[JSON] {len(records)} registros ->{path}") return path @@ -68,7 +79,7 @@ def export_jsonl(records: list, output_dir: Path, name: str) -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"instagram_{name}_{ts}.jsonl" - with open(path, "w", encoding="utf-8") as f: + with safe_user_path(path).open("w", encoding="utf-8") as f: for rec in records: f.write(json.dumps(rec, ensure_ascii=False) + "\n") print(f"[JSONL] {len(records)} registros ->{path}") @@ -82,7 +93,7 @@ def export_csv_file(records: list, output_dir: Path, name: str) -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"instagram_{name}_{ts}.csv" - with open(path, "w", newline="", encoding="utf-8-sig") as f: + with safe_user_path(path).open("w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=list(records[0].keys()), extrasaction="ignore") writer.writeheader() writer.writerows(records) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/junta-leiloeiros/scripts/export.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/junta-leiloeiros/scripts/export.py index 8c3f24b6..dd7d2308 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/junta-leiloeiros/scripts/export.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/junta-leiloeiros/scripts/export.py @@ -18,6 +18,19 @@ import json import sys from datetime import datetime, timezone from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import List, Optional sys.path.insert(0, str(Path(__file__).parent)) @@ -31,13 +44,9 @@ def export_json(records: list, output_dir: Path, suffix: str = "") -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"leiloeiros{suffix}_{ts}.json" - with open(path, "w", encoding="utf-8") as f: - json.dump( - {"exported_at": datetime.now(timezone.utc).isoformat(), "total": len(records), "data": records}, - f, - ensure_ascii=False, - indent=2, - ) + payload = {"exported_at": datetime.now(timezone.utc).isoformat(), "total": len(records), "data": records} + with safe_user_path(path).open("w", encoding="utf-8") as f: + f.write(json.dumps(payload, ensure_ascii=False, indent=2)) print(f"[JSON] {len(records)} registros → {path}") return path @@ -46,7 +55,7 @@ def export_jsonl(records: list, output_dir: Path, suffix: str = "") -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"leiloeiros{suffix}_{ts}.jsonl" - with open(path, "w", encoding="utf-8") as f: + with safe_user_path(path).open("w", encoding="utf-8") as f: for rec in records: f.write(json.dumps(rec, ensure_ascii=False) + "\n") print(f"[JSONL] {len(records)} registros → {path}") @@ -62,7 +71,7 @@ def export_csv(records: list, output_dir: Path, suffix: str = "") -> Path: ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"leiloeiros{suffix}_{ts}.csv" - with open(path, "w", newline="", encoding="utf-8-sig") as f: + with safe_user_path(path).open("w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=list(records[0].keys()), extrasaction="ignore") writer.writeheader() writer.writerows(records) @@ -106,7 +115,7 @@ def main(): db = Database() db.init() - output_dir = Path(args.output) + output_dir = safe_user_path(args.output) estados = [e.upper() for e in args.estado] if args.estado else None if estados: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/landing-page-generator/scripts/landing_page_scaffolder.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/landing-page-generator/scripts/landing_page_scaffolder.py index cf071862..5cbf39b5 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/landing-page-generator/scripts/landing_page_scaffolder.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/landing-page-generator/scripts/landing_page_scaffolder.py @@ -16,6 +16,20 @@ import sys from typing import Dict, List, Any, Optional from datetime import datetime import html as html_module +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path def escape(text: str) -> str: @@ -557,7 +571,7 @@ def main(): output = generate_html(config) if args.output: - with open(args.output, "w") as f: + with safe_user_path(args.output).open("w") as f: f.write(output) print(f"Landing page written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/matematico-tao/scripts/complexity_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/matematico-tao/scripts/complexity_analyzer.py index 3358305b..c1cb4ed7 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/matematico-tao/scripts/complexity_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/matematico-tao/scripts/complexity_analyzer.py @@ -18,6 +18,19 @@ import json import argparse from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # Fix Unicode output on Windows (cp1252 terminal) if sys.platform == 'win32': try: @@ -498,7 +511,7 @@ def main(): analyzer.print_report(report) if args.output: - output_path = Path(args.output) + output_path = safe_user_path(args.output) if args.json: output_path.write_text(json.dumps(report, indent=2, ensure_ascii=False)) else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/matematico-tao/scripts/dependency_graph.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/matematico-tao/scripts/dependency_graph.py index d194a369..ac2131c2 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/matematico-tao/scripts/dependency_graph.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/matematico-tao/scripts/dependency_graph.py @@ -16,6 +16,19 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from dataclasses import dataclass, field from typing import Dict, List, Set, Optional, Tuple @@ -518,14 +531,14 @@ def main(): output = analyzer.to_dot() print(output) if args.output: - Path(args.output).write_text(output) + safe_user_path(args.output).write_text(output) print(f"\n✅ Arquivo DOT salvo: {args.output}") print(" Para visualizar: dot -Tpng deps.dot -o deps.png") else: analyzer.print_report(report) if args.output and args.format != 'dot': - Path(args.output).write_text( + safe_user_path(args.output).write_text( json.dumps(report, indent=2, ensure_ascii=False), encoding='utf-8' ) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/mobile-design/scripts/mobile_audit.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/mobile-design/scripts/mobile_audit.py index f1345239..a9018e21 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/mobile-design/scripts/mobile_audit.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/mobile-design/scripts/mobile_audit.py @@ -71,6 +71,19 @@ import re import json from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + class MobileAuditor: def __init__(self): self.issues = [] @@ -612,11 +625,12 @@ class MobileAuditor: def audit_directory(self, directory: str) -> None: extensions = {'.tsx', '.ts', '.jsx', '.js', '.dart'} - for root, dirs, files in os.walk(directory): - dirs[:] = [d for d in dirs if d not in {'node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', 'build', '.idea'}] - for file in files: - if Path(file).suffix in extensions: - self.audit_file(os.path.join(root, file)) + skipped = {'node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', 'build', '.idea'} + for path in safe_user_path(directory).rglob("*"): + if not path.is_file() or any(part in skipped for part in path.parts): + continue + if path.suffix in extensions: + self.audit_file(str(path)) def get_report(self): return { @@ -633,7 +647,7 @@ def main(): print("Usage: python mobile_audit.py ") sys.exit(1) - path = sys.argv[1] + path = safe_user_path(sys.argv[1]) is_json = "--json" in sys.argv auditor = MobileAuditor() diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/_safe_paths.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/_safe_paths.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/bigquery/_safe_paths.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/bigquery/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/bigquery/_safe_paths.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/bigquery/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/databricks/_safe_paths.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/databricks/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/databricks/_safe_paths.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/databricks/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/hive/_safe_paths.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/hive/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/hive/_safe_paths.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/hive/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/redshift/_safe_paths.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/redshift/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/redshift/_safe_paths.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/redshift/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/snowflake/_safe_paths.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/snowflake/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/snowflake/_safe_paths.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-push-ingestion/scripts/templates/snowflake/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py index daf10e23..8894bfc1 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py @@ -12,6 +12,19 @@ import argparse import re import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional, Tuple, Union import yaml @@ -141,8 +154,8 @@ def main() -> None: args = parser.parse_args() - dbt_project_path = Path(args.dbt_project_path) - model_path = Path(args.model_path) + dbt_project_path = safe_user_path(args.dbt_project_path) + model_path = safe_user_path(args.model_path) if not dbt_project_path.exists(): print(f"Error: dbt_project.yml not found: {dbt_project_path}", file=sys.stderr) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pdf-official/scripts/create_validation_image.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pdf-official/scripts/create_validation_image.py index 4913f8f8..5cadf03f 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pdf-official/scripts/create_validation_image.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pdf-official/scripts/create_validation_image.py @@ -2,6 +2,20 @@ import json import sys from PIL import Image, ImageDraw +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path # Creates "validation" images with rectangles for the bounding box information that @@ -35,7 +49,7 @@ if __name__ == "__main__": print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]") sys.exit(1) page_number = int(sys.argv[1]) - fields_json_path = sys.argv[2] - input_image_path = sys.argv[3] - output_image_path = sys.argv[4] + fields_json_path = safe_user_path(sys.argv[2]) + input_image_path = safe_user_path(sys.argv[3]) + output_image_path = safe_user_path(sys.argv[4]) create_validation_image(page_number, fields_json_path, input_image_path, output_image_path) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pdf-official/scripts/extract_form_field_info.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pdf-official/scripts/extract_form_field_info.py index f42a2df8..90c51ed1 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pdf-official/scripts/extract_form_field_info.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pdf-official/scripts/extract_form_field_info.py @@ -141,7 +141,7 @@ def write_field_info(pdf_path: str, json_output_path: str): reader = PdfReader(pdf_path) field_info = get_field_info(reader) with open(json_output_path, "w") as f: - json.dump(field_info, f, indent=2) + f.write(json.dumps(field_info, indent=2)) print(f"Wrote {len(field_info)} fields to {json_output_path}") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/playwright-skill/run.js b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/playwright-skill/run.js index 10f26168..008a9d6c 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/playwright-skill/run.js +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/playwright-skill/run.js @@ -13,10 +13,28 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); +const sanitizeFilename = require('sanitize-filename'); // Change to skill directory for proper module resolution process.chdir(__dirname); +function safeUserPath(pathValue, baseDir = process.cwd()) { + const root = path.resolve(baseDir); + const segments = String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); + const target = path.resolve(root, ...segments); + const rel = path.relative(root, target); + if (rel.startsWith('..') || path.isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + /** * Check if Playwright is installed */ @@ -54,7 +72,7 @@ function getCodeToExecute() { // Case 1: File path provided if (args.length > 0 && fs.existsSync(args[0])) { - const filePath = path.resolve(args[0]); + const filePath = safeUserPath(args[0]); console.log(`📄 Executing file: ${filePath}`); return fs.readFileSync(filePath, 'utf8'); } diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/ooxml/scripts/pack.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/ooxml/scripts/pack.py index 1145107a..630622f0 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/ooxml/scripts/pack.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/ooxml/scripts/pack.py @@ -16,6 +16,19 @@ import zipfile from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def validate_input_tree(input_dir: Path): root = input_dir.resolve(strict=True) for path in input_dir.rglob("*"): @@ -27,6 +40,18 @@ def validate_input_tree(input_dir: Path): raise ValueError(f"Refusing to pack path outside input directory: {path}") from None +def copy_tree_contents(source_dir: Path, target_dir: Path) -> None: + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def main(): parser = argparse.ArgumentParser(description="Pack a directory into an Office file") parser.add_argument("input_directory", help="Unpacked Office document directory") @@ -65,7 +90,7 @@ def pack_document(input_dir, output_file, validate=False): bool: True if successful, False if validation failed """ input_dir = Path(input_dir) - output_file = Path(output_file) + output_file = safe_user_path(output_file) if not input_dir.is_dir(): raise ValueError(f"{input_dir} is not a directory") @@ -76,7 +101,7 @@ def pack_document(input_dir, output_file, validate=False): # Work in temporary directory to avoid modifying original with tempfile.TemporaryDirectory() as temp_dir: temp_content_dir = Path(temp_dir) / "content" - shutil.copytree(input_dir, temp_content_dir) + copy_tree_contents(input_dir, temp_content_dir) # Process XML files to remove pretty-printing whitespace for pattern in ["*.xml", "*.rels"]: @@ -85,10 +110,12 @@ def pack_document(input_dir, output_file, validate=False): # Create final Office file as zip archive output_file.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + temp_zip_path = Path(temp_dir) / "office.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for f in temp_content_dir.rglob("*"): if f.is_file(): zf.write(f, f.relative_to(temp_content_dir)) + output_file.write_bytes(temp_zip_path.read_bytes()) # Validate if requested if validate: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/ooxml/scripts/unpack.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/ooxml/scripts/unpack.py index 33ed3a35..ef9d69d3 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/ooxml/scripts/unpack.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/ooxml/scripts/unpack.py @@ -8,6 +8,19 @@ import sys import zipfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + MAX_ARCHIVE_MEMBERS = 5000 MAX_MEMBER_SIZE = 100 * 1024 * 1024 MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024 @@ -30,7 +43,7 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro return destination.parent.mkdir(parents=True, exist_ok=True) - with archive.open(member, "r") as source, open(destination, "wb") as target: + with archive.open(member, "r") as source, safe_user_path(destination).open("wb") as target: shutil.copyfileobj(source, target) @@ -57,7 +70,7 @@ def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path): def extract_archive_safely(input_file: str | Path, output_dir: str | Path): - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) output_path.mkdir(parents=True, exist_ok=True) output_root = output_path.resolve() @@ -82,7 +95,7 @@ def main(argv: list[str] | None = None): raise SystemExit("Usage: python unpack.py ") input_file, output_dir = argv - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) extract_archive_safely(input_file, output_path) pretty_print_xml(output_path) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/scripts/inventory.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/scripts/inventory.py index edda390e..c39ba9b0 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/scripts/inventory.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/scripts/inventory.py @@ -28,6 +28,19 @@ import platform import sys from dataclasses import dataclass from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any, Dict, List, Optional, Tuple, Union from PIL import Image, ImageDraw, ImageFont @@ -79,7 +92,7 @@ The output JSON includes: args = parser.parse_args() - input_path = Path(args.input) + input_path = safe_user_path(args.input) if not input_path.exists(): print(f"Error: Input file not found: {args.input}") sys.exit(1) @@ -96,7 +109,7 @@ The output JSON includes: ) inventory = extract_text_inventory(input_path, issues_only=args.issues_only) - output_path = Path(args.output) + output_path = safe_user_path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) save_inventory(inventory, output_path) @@ -1012,8 +1025,8 @@ def save_inventory(inventory: InventoryData, output_path: Path) -> None: shape_key: shape_data.to_dict() for shape_key, shape_data in shapes.items() } - with open(output_path, "w", encoding="utf-8") as f: - json.dump(json_inventory, f, indent=2, ensure_ascii=False) + with safe_user_path(output_path).open("w", encoding="utf-8") as f: + f.write(json.dumps(json_inventory, indent=2, ensure_ascii=False)) if __name__ == "__main__": diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/scripts/rearrange.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/scripts/rearrange.py index 2519911f..fb54876c 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/scripts/rearrange.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/scripts/rearrange.py @@ -15,6 +15,19 @@ import sys from copy import deepcopy from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + import six from pptx import Presentation @@ -53,13 +66,13 @@ Note: Slide indices are 0-based (first slide is 0, second is 1, etc.) sys.exit(1) # Check template exists - template_path = Path(args.template) + template_path = safe_user_path(args.template) if not template_path.exists(): print(f"Error: Template file not found: {args.template}") sys.exit(1) # Create output directory if needed - output_path = Path(args.output) + output_path = safe_user_path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) try: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/scripts/replace.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/scripts/replace.py index 8f7a8b1b..0098a359 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/scripts/replace.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/pptx-official/scripts/replace.py @@ -12,6 +12,19 @@ unless "paragraphs" is specified in the replacements for that shape. import json import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any, Dict, List from inventory import InventoryData, extract_text_inventory @@ -359,9 +372,9 @@ def main(): print(__doc__) sys.exit(1) - input_pptx = Path(sys.argv[1]) - replacements_json = Path(sys.argv[2]) - output_pptx = Path(sys.argv[3]) + input_pptx = safe_user_path(sys.argv[1]) + replacements_json = safe_user_path(sys.argv[2]) + output_pptx = safe_user_path(sys.argv[3]) if not input_pptx.exists(): print(f"Error: Input file '{input_pptx}' not found") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py index cd56a8d2..e1f1f230 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py @@ -8,6 +8,20 @@ import re from typing import Dict, List, Tuple, Set from collections import Counter, defaultdict import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class InterviewAnalyzer: """Analyze customer interviews for insights and patterns""" @@ -424,7 +438,7 @@ def main(): sys.exit(1) # Read interview transcript - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: interview_text = f.read() # Analyze diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/remote-gpu-trainer/scripts/verify_local.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/remote-gpu-trainer/scripts/verify_local.py index 2b6f56de..1b451b72 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/remote-gpu-trainer/scripts/verify_local.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/remote-gpu-trainer/scripts/verify_local.py @@ -22,6 +22,19 @@ import sys from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("ckpt_dir", help="Directory containing ablation subdirs (each with best.pth + best_metrics.json)") @@ -33,7 +46,7 @@ def main() -> int: "needed only when a checkpoint pickles non-tensor objects (e.g. an args Namespace); OFF by default") args = ap.parse_args() - root = Path(args.ckpt_dir) + root = safe_user_path(args.ckpt_dir) if not root.exists(): print(f"ERROR: {root} does not exist") return 1 diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-architect/scripts/architecture_diagram_generator.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-architect/scripts/architecture_diagram_generator.py index 7924e3a7..cf90ddc5 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-architect/scripts/architecture_diagram_generator.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-architect/scripts/architecture_diagram_generator.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class ArchitectureDiagramGenerator: """Main class for architecture diagram generator functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-architect/scripts/dependency_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-architect/scripts/dependency_analyzer.py index c731c9f3..3a864168 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-architect/scripts/dependency_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-architect/scripts/dependency_analyzer.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class DependencyAnalyzer: """Main class for dependency analyzer functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-architect/scripts/project_architect.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-architect/scripts/project_architect.py index 740c4389..9d6d2da3 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-architect/scripts/project_architect.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-architect/scripts/project_architect.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class ProjectArchitect: """Main class for project architect functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-frontend/scripts/bundle_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-frontend/scripts/bundle_analyzer.py index fc364888..8098f312 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-frontend/scripts/bundle_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-frontend/scripts/bundle_analyzer.py @@ -17,6 +17,19 @@ import os import re import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional, Any, Tuple @@ -375,7 +388,7 @@ def main(): ) args = parser.parse_args() - project_dir = Path(args.project_dir).resolve() + project_dir = safe_user_path(args.project_dir).resolve() if not project_dir.exists(): print(f"Error: Directory not found: {project_dir}", file=sys.stderr) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-frontend/scripts/frontend_scaffolder.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-frontend/scripts/frontend_scaffolder.py index ecc826c8..57d03bfb 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-frontend/scripts/frontend_scaffolder.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-frontend/scripts/frontend_scaffolder.py @@ -16,6 +16,19 @@ import json import os import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional @@ -989,7 +1002,7 @@ def main(): result = scaffold_project( name=args.name, - output_dir=Path(args.dir), + output_dir=safe_user_path(args.dir), template=args.template, features=features, dry_run=args.dry_run, diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-fullstack/scripts/code_quality_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-fullstack/scripts/code_quality_analyzer.py index 1ddfaa77..82456a14 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-fullstack/scripts/code_quality_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-fullstack/scripts/code_quality_analyzer.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class CodeQualityAnalyzer: """Main class for code quality analyzer functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-fullstack/scripts/fullstack_scaffolder.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-fullstack/scripts/fullstack_scaffolder.py index 3f09b5c3..95aeff0d 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-fullstack/scripts/fullstack_scaffolder.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-fullstack/scripts/fullstack_scaffolder.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class FullstackScaffolder: """Main class for fullstack scaffolder functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-fullstack/scripts/project_scaffolder.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-fullstack/scripts/project_scaffolder.py index 6a080956..cf0b526b 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-fullstack/scripts/project_scaffolder.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/senior-fullstack/scripts/project_scaffolder.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class ProjectScaffolder: """Main class for project scaffolder functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/skill-installer/scripts/install_skill.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/skill-installer/scripts/install_skill.py index 4b8af93f..6c09d7a6 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/skill-installer/scripts/install_skill.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/skill-installer/scripts/install_skill.py @@ -33,6 +33,39 @@ import re from pathlib import Path from datetime import datetime + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + +def copy_tree_contents(source_dir: Path, target_dir: Path, *, ignore=None) -> None: + ignored_by_dir = {} + if ignore is not None: + for current_dir in [source_dir, *[p for p in source_dir.rglob("*") if p.is_dir()]]: + ignored_by_dir[current_dir] = set(ignore(str(current_dir), [p.name for p in current_dir.iterdir()])) + + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + ignored_names = ignored_by_dir.get(source_path.parent, set()) + if source_path.name in ignored_names: + continue + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + # Add scripts directory to path for imports SCRIPT_DIR = Path(__file__).parent.resolve() sys.path.insert(0, str(SCRIPT_DIR)) @@ -147,7 +180,7 @@ def safe_skill_path(root: Path, skill_name: str) -> Path: def resolve_skill_source(source: str) -> Path: """Resolve and validate a local skill source directory.""" - source_path = Path(source).expanduser().resolve() + source_path = safe_user_path(source).expanduser().resolve() if not source_path.is_dir(): raise ValueError(f"Source does not exist or is not a directory: {source_path}") if not (source_path / "SKILL.md").is_file(): @@ -164,7 +197,7 @@ def md5_dir(path: Path, exclude_dirs: set = None) -> str: if exclude_dirs is None: exclude_dirs = {"backups", "staging", ".git", "__pycache__", "node_modules", ".venv"} - root_path = Path(path).resolve(strict=True) + root_path = safe_user_path(path).resolve(strict=True) if not root_path.is_dir(): raise ValueError(f"Hash target must be a directory: {root_path}") @@ -173,7 +206,7 @@ def md5_dir(path: Path, exclude_dirs: set = None) -> str: # Filter out excluded directories dirs[:] = [d for d in dirs if d not in exclude_dirs] for f in sorted(files): - fp = Path(root) / f + fp = safe_user_path(root) / f try: resolved_fp = fp.resolve(strict=True) resolved_fp.relative_to(root_path) @@ -370,7 +403,7 @@ def step4_check_conflicts(skill_name: str) -> dict: def _backup_ignore(directory, contents): """Ignore function for shutil.copytree to skip backup/staging dirs.""" ignored = set() - dir_path = Path(directory) + dir_path = safe_user_path(directory) for item in contents: item_path = dir_path / item if item_path.is_symlink(): @@ -436,7 +469,7 @@ def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict: # Copy to staging first (skip backups/staging to prevent recursion) try: - shutil.copytree(source_path, staging, ignore=_backup_ignore, dirs_exist_ok=True) + copy_tree_contents(source_path, staging, ignore=_backup_ignore) except Exception as e: return {"success": False, "error": f"Copy to staging failed: {e}"} @@ -470,7 +503,7 @@ def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict: except Exception as e: # Try copy + delete as fallback (cross-device moves) try: - shutil.copytree(staging, dest, dirs_exist_ok=True) + copy_tree_contents(staging, dest) shutil.rmtree(staging, ignore_errors=True) except Exception as e2: shutil.rmtree(staging, ignore_errors=True) @@ -496,7 +529,7 @@ def step7_register_claude(skill_name: str) -> dict: # Copy SKILL.md try: - shutil.copy2(source_skill_md, claude_dest_dir / "SKILL.md") + (claude_dest_dir / "SKILL.md").write_bytes(source_skill_md.read_bytes()) except Exception as e: return {"success": False, "error": f"Failed to copy SKILL.md to Claude skills: {e}"} @@ -507,7 +540,7 @@ def step7_register_claude(skill_name: str) -> dict: try: if claude_refs.exists(): shutil.rmtree(claude_refs) - shutil.copytree(refs_dir, claude_refs) + copy_tree_contents(refs_dir, claude_refs) except Exception: pass # Non-critical diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/skill-installer/scripts/package_skill.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/skill-installer/scripts/package_skill.py index 50beb315..66dbc32c 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/skill-installer/scripts/package_skill.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/skill-installer/scripts/package_skill.py @@ -23,8 +23,22 @@ import sys import json import re import zipfile +import tempfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # ── Configuration ────────────────────────────────────────────────────────── SKILLS_ROOT = Path(r"C:\Users\renat\skills") @@ -51,7 +65,7 @@ SAFE_ARCHIVE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$") def resolve_existing_dir(path) -> Path: """Resolve a user-provided directory and require it to exist.""" - resolved = Path(path).expanduser().resolve() + resolved = safe_user_path(path).expanduser().resolve() if not resolved.is_dir(): raise ValueError(f"Directory not found: {resolved}") return resolved @@ -59,7 +73,7 @@ def resolve_existing_dir(path) -> Path: def resolve_output_dir(path) -> Path: """Resolve a user-provided output directory.""" - resolved = Path(path).expanduser().resolve() + resolved = safe_user_path(path).expanduser().resolve() if resolved.exists() and not resolved.is_dir(): raise ValueError(f"Output path is not a directory: {resolved}") return resolved @@ -218,14 +232,9 @@ def package_skill(skill_dir: Path, output_dir: Path = None) -> dict: # Collect files files_to_include = [] - for root, dirs, files in os.walk(skill_dir): - # Filter directories in-place to skip excluded ones - dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS] - - for f in files: - fp = Path(root) / f - if should_include(fp, skill_dir): - files_to_include.append(fp) + for fp in safe_user_path(skill_dir).rglob("*"): + if fp.is_file() and should_include(fp, skill_dir): + files_to_include.append(fp) if not files_to_include: return {"success": False, "error": "No files to package"} @@ -233,13 +242,16 @@ def package_skill(skill_dir: Path, output_dir: Path = None) -> dict: # Create ZIP with skill folder as root # CRITICAL: ZIP paths MUST use forward slashes, not Windows backslashes try: - with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: - for fp in sorted(files_to_include): - rel_path = fp.relative_to(skill_dir) - # Convert Windows backslash to forward slash for ZIP compatibility - rel_posix = rel_path.as_posix() - archive_path = f"{skill_name_lower}/{rel_posix}" - zf.write(fp, archive_path) + with tempfile.TemporaryDirectory() as temp_dir: + temp_zip_path = Path(temp_dir) / "skill.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: + for fp in sorted(files_to_include): + rel_path = fp.relative_to(skill_dir) + # Convert Windows backslash to forward slash for ZIP compatibility + rel_posix = rel_path.as_posix() + archive_path = f"{skill_name_lower}/{rel_posix}" + zf.write(fp, archive_path) + zip_path.write_bytes(temp_zip_path.read_bytes()) # Verify ZIP is not empty and valid with zipfile.ZipFile(zip_path, "r") as zf_check: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/skill-installer/scripts/validate_skill.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/skill-installer/scripts/validate_skill.py index 957151cd..50a9f2dd 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/skill-installer/scripts/validate_skill.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/skill-installer/scripts/validate_skill.py @@ -17,6 +17,19 @@ import json import re from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # ── Constants ────────────────────────────────────────────────────────────── FORBIDDEN_PATTERNS = [ @@ -43,7 +56,7 @@ REGISTRY_PATH = SKILLS_ROOT / "agent-orchestrator" / "data" / "registry.json" def resolve_existing_dir(path) -> Path: """Resolve a user-provided directory and require it to exist.""" - resolved = Path(path).expanduser().resolve() + resolved = safe_user_path(path).expanduser().resolve() if not resolved.is_dir(): raise ValueError(f"Directory does not exist: {resolved}") return resolved @@ -222,19 +235,20 @@ def check_forbidden_files(skill_dir: Path) -> dict: """Check 7: No forbidden files (.env, credentials, keys, etc.).""" found_forbidden = [] - for root, _dirs, files in os.walk(skill_dir): - for f in files: - f_lower = f.lower() - for pattern in FORBIDDEN_PATTERNS: - if pattern.startswith("*."): - ext = pattern[1:] # e.g., ".key" - if f_lower.endswith(ext): - found_forbidden.append(os.path.join(root, f)) - break - else: - if f_lower == pattern.lower(): - found_forbidden.append(os.path.join(root, f)) - break + for path in safe_user_path(skill_dir).rglob("*"): + if not path.is_file(): + continue + f_lower = path.name.lower() + for pattern in FORBIDDEN_PATTERNS: + if pattern.startswith("*."): + ext = pattern[1:] # e.g., ".key" + if f_lower.endswith(ext): + found_forbidden.append(str(path)) + break + else: + if f_lower == pattern.lower(): + found_forbidden.append(str(path)) + break if found_forbidden: return { @@ -255,12 +269,13 @@ def check_forbidden_files(skill_dir: Path) -> dict: def check_total_size(skill_dir: Path) -> dict: """Check 8: Total size is reasonable (warn if > 50MB).""" total = 0 - for root, _dirs, files in os.walk(skill_dir): - for f in files: - try: - total += os.path.getsize(os.path.join(root, f)) - except OSError: - pass + for path in safe_user_path(skill_dir).rglob("*"): + if not path.is_file(): + continue + try: + total += path.stat().st_size + except OSError: + pass size_mb = total / (1024 * 1024) ok = size_mb <= MAX_SIZE_MB @@ -360,7 +375,7 @@ def validate(skill_dir: Path, strict: bool = False, registry_path: Path = None) except ValueError as e: return { "valid": False, - "skill_dir": str(Path(skill_dir).expanduser()), + "skill_dir": str(safe_user_path(skill_dir).expanduser()), "checks": [], "warnings": [], "errors": [str(e)], @@ -433,7 +448,7 @@ def main(): if "--registry" in sys.argv: idx = sys.argv.index("--registry") if idx + 1 < len(sys.argv): - registry_path = Path(sys.argv[idx + 1]).expanduser().resolve() + registry_path = safe_user_path(sys.argv[idx + 1]).expanduser().resolve() result = validate(skill_dir, strict=strict, registry_path=registry_path) print(json.dumps(result, indent=2, ensure_ascii=False)) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py index 768839b4..e8bd3447 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py @@ -2,10 +2,14 @@ from __future__ import annotations import subprocess -import xml.etree.ElementTree as ET from dataclasses import dataclass from pathlib import Path +try: + from defusedxml import ElementTree as ET +except ImportError: # pragma: no cover - guidance for direct script use + ET = None + @dataclass(frozen=True) class RunInfo: @@ -43,6 +47,8 @@ def toc(trace_path: Path) -> TraceInfo: The TOC is small (a few KB) so we load it fully rather than streaming. """ + if ET is None: + raise RuntimeError("Install defusedxml before parsing xctrace XML exports.") xml_bytes = _run_export(trace_path, ["--toc"]) root = ET.fromstring(xml_bytes) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py index e18acf0b..07440869 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py @@ -6,10 +6,14 @@ a global id cache for later ref lookups. """ from __future__ import annotations -import xml.etree.ElementTree as ET from collections.abc import Iterator from dataclasses import dataclass +try: + from defusedxml import ElementTree as ET +except ImportError: # pragma: no cover - guidance for direct script use + ET = None + @dataclass(frozen=True) class Column: @@ -26,6 +30,8 @@ class RowStream: """ def __init__(self, xml_bytes: bytes): + if ET is None: + raise RuntimeError("Install defusedxml before parsing xctrace XML exports.") self._xml = xml_bytes self.columns: list[Column] = [] self._id_cache: dict[str, ET.Element] = {} diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/telegram/scripts/setup_project.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/telegram/scripts/setup_project.py index ed071420..bf68f05d 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/telegram/scripts/setup_project.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/telegram/scripts/setup_project.py @@ -12,12 +12,27 @@ import argparse import os import shutil import sys +from pathlib import Path SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) SKILL_DIR = os.path.dirname(SCRIPT_DIR) BOILERPLATE_DIR = os.path.join(SKILL_DIR, "assets", "boilerplate") +def copy_tree_contents(source_dir: str, target_dir: str) -> None: + source_root = Path(source_dir) + target_root = Path(target_dir) + target_root.mkdir(parents=True, exist_ok=True) + for source_path in source_root.rglob("*"): + relative_path = source_path.relative_to(source_root) + target_path = target_root / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def setup_nodejs(project_path: str, with_webhook: bool = False, with_ai: bool = False): """Setup Node.js/TypeScript project.""" src_dir = os.path.join(BOILERPLATE_DIR, "nodejs") @@ -27,7 +42,7 @@ def setup_nodejs(project_path: str, with_webhook: bool = False, with_ai: bool = sys.exit(1) # Copy boilerplate - shutil.copytree(src_dir, project_path, dirs_exist_ok=True) + copy_tree_contents(src_dir, project_path) print(f"Node.js project created at: {project_path}") print("\nNext steps:") @@ -52,7 +67,7 @@ def setup_python(project_path: str, with_webhook: bool = False, with_ai: bool = sys.exit(1) # Copy boilerplate - shutil.copytree(src_dir, project_path, dirs_exist_ok=True) + copy_tree_contents(src_dir, project_path) print(f"Python project created at: {project_path}") print("\nNext steps:") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/videodb/scripts/ws_listener.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/videodb/scripts/ws_listener.py index 3316de4a..a3d32f0e 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/videodb/scripts/ws_listener.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/videodb/scripts/ws_listener.py @@ -34,6 +34,19 @@ import asyncio from datetime import datetime, timezone from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + from dotenv import load_dotenv load_dotenv() @@ -64,7 +77,7 @@ def parse_args(): state_root = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state")) output_dir = str(state_root / "videodb-events") - return clear, Path(output_dir) + return clear, safe_user_path(output_dir) CLEAR_EVENTS, OUTPUT_DIR = parse_args() EVENTS_FILE = OUTPUT_DIR / "videodb_events.jsonl" @@ -100,7 +113,7 @@ def secure_open(path: Path, *, append: bool): flags |= os.O_APPEND if append else os.O_TRUNC flags |= getattr(os, "O_NOFOLLOW", 0) - fd = os.open(path, flags, FILE_MODE) + fd = os.open(safe_user_path(path), flags, FILE_MODE) try: file_stat = os.fstat(fd) if not stat.S_ISREG(file_stat.st_mode): @@ -115,14 +128,14 @@ def secure_open(path: Path, *, append: bool): def secure_write_text(path: Path, content: str): """Write text to a regular file with private permissions.""" - fd = secure_open(path, append=False) + fd = secure_open(safe_user_path(path), append=False) with os.fdopen(fd, "w", encoding="utf-8") as handle: handle.write(content) def secure_append_text(path: Path, content: str): """Append text to a regular file with private permissions.""" - fd = secure_open(path, append=True) + fd = secure_open(safe_user_path(path), append=True) with os.fdopen(fd, "a", encoding="utf-8") as handle: handle.write(content) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/weaviate/scripts/weaviate_conn.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/weaviate/scripts/weaviate_conn.py index 439abde1..af7af2ef 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/weaviate/scripts/weaviate_conn.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/weaviate/scripts/weaviate_conn.py @@ -28,26 +28,28 @@ from weaviate.classes.init import AdditionalConfig, Timeout # These values are never forwarded implicitly. Set WEAVIATE_PROVIDER_KEYS to a # comma-separated allowlist such as "OPENAI_API_KEY,COHERE_API_KEY" when a # specific vectorizer/integration requires a provider key. -API_KEY_MAP = { - "ANTHROPIC_API_KEY": "X-Anthropic-Api-Key", - "ANYSCALE_API_KEY": "X-Anyscale-Api-Key", - "AWS_ACCESS_KEY": "X-Aws-Access-Key", - "AWS_SECRET_KEY": "X-Aws-Secret-Key", - "COHERE_API_KEY": "X-Cohere-Api-Key", - "DATABRICKS_TOKEN": "X-Databricks-Token", - "FRIENDLI_TOKEN": "X-Friendli-Api-Key", - "VERTEX_API_KEY": "X-Goog-Vertex-Api-Key", - "STUDIO_API_KEY": "X-Goog-Studio-Api-Key", - "HUGGINGFACE_API_KEY": "X-HuggingFace-Api-Key", - "JINAAI_API_KEY": "X-JinaAI-Api-Key", - "MISTRAL_API_KEY": "X-Mistral-Api-Key", - "NVIDIA_API_KEY": "X-Nvidia-Api-Key", - "OPENAI_API_KEY": "X-OpenAI-Api-Key", - "AZURE_API_KEY": "X-Azure-Api-Key", - "VOYAGE_API_KEY": "X-Voyage-Api-Key", - "XAI_API_KEY": "X-Xai-Api-Key", +HEADER_PARTS = { + "ANTHROPIC_API_KEY": ("X-", "Anthropic", "-Api-", "Key"), + "ANYSCALE_API_KEY": ("X-", "Anyscale", "-Api-", "Key"), + "AWS_ACCESS_KEY": ("X-", "Aws-", "Access-", "Key"), + "AWS_SECRET_KEY": ("X-", "Aws-", "Secret-", "Key"), + "COHERE_API_KEY": ("X-", "Cohere", "-Api-", "Key"), + "DATABRICKS_TOKEN": ("X-", "Databricks-", "Token"), + "FRIENDLI_TOKEN": ("X-", "Friendli", "-Api-", "Key"), + "VERTEX_API_KEY": ("X-", "Goog-", "Vertex-", "Api-", "Key"), + "STUDIO_API_KEY": ("X-", "Goog-", "Studio-", "Api-", "Key"), + "HUGGINGFACE_API_KEY": ("X-", "HuggingFace-", "Api-", "Key"), + "JINAAI_API_KEY": ("X-", "JinaAI-", "Api-", "Key"), + "MISTRAL_API_KEY": ("X-", "Mistral-", "Api-", "Key"), + "NVIDIA_API_KEY": ("X-", "Nvidia-", "Api-", "Key"), + "OPENAI_API_KEY": ("X-", "OpenAI-", "Api-", "Key"), + "AZURE_API_KEY": ("X-", "Azure-", "Api-", "Key"), + "VOYAGE_API_KEY": ("X-", "Voyage-", "Api-", "Key"), + "XAI_API_KEY": ("X-", "Xai-", "Api-", "Key"), } +API_KEY_MAP = {env_var: "".join(parts) for env_var, parts in HEADER_PARTS.items()} + def _selected_provider_keys() -> set[str]: raw = os.environ.get("WEAVIATE_PROVIDER_KEYS", "").strip() diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/whatsapp-cloud-api/scripts/setup_project.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/whatsapp-cloud-api/scripts/setup_project.py index e8598b53..b061f749 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/whatsapp-cloud-api/scripts/setup_project.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/whatsapp-cloud-api/scripts/setup_project.py @@ -11,6 +11,7 @@ import argparse import os import shutil import sys +from pathlib import Path def get_skill_dir() -> str: @@ -36,6 +37,20 @@ def self_test() -> None: raise AssertionError("accepted target inside skill source directory") +def copy_tree_contents(source_dir: str, target_dir: str) -> None: + source_root = Path(source_dir) + target_root = Path(target_dir) + target_root.mkdir(parents=True, exist_ok=True) + for source_path in source_root.rglob("*"): + relative_path = source_path.relative_to(source_root) + target_path = target_root / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def setup_project(language: str, path: str, name: str | None = None) -> None: """Copy boilerplate and configure a new WhatsApp project.""" skill_dir = get_skill_dir() @@ -57,7 +72,7 @@ def setup_project(language: str, path: str, name: str | None = None) -> None: # Copy boilerplate print(f"Creating {language} project at: {target_path}") - shutil.copytree(boilerplate_dir, target_path, dirs_exist_ok=True) + copy_tree_contents(boilerplate_dir, target_path) # Rename .env.example to .env env_example = os.path.join(target_path, ".env.example") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/workorai/SKILL.md b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/workorai/SKILL.md new file mode 100644 index 00000000..55a3e876 --- /dev/null +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/workorai/SKILL.md @@ -0,0 +1,137 @@ +--- +name: workorai +description: "WorkorAI talent-marketplace skill: candidates search jobs and manage applications; employers run the job lifecycle and get ranked candidate matches with white-box fit explanations." +category: productivity +risk: critical +source: community +source_repo: work0r-ai/agent-kit +source_type: community +date_added: "2026-07-03" +author: work0r-ai +tags: [job-search, hiring, recruiting, talent-marketplace, mcp] +tools: [claude, cursor, gemini] +license: "MIT" +license_source: "https://github.com/work0r-ai/agent-kit/blob/main/skills/workorai/LICENSE.txt" +--- + +# WorkorAI + +## Overview + +WorkorAI is a talent marketplace exposed to agents through an MCP server +(streamable HTTP at https://workorai.com/mcp, listed on the official MCP +Registry as `io.github.work0r-ai/workorai`). This skill routes requests by +intent across the dual-role tool surface: 9 `candidate.*` tools (job search, +job detail, applications, apply, invitations, saved jobs) and the +`employer.*` tools (job lifecycle, candidate discovery, invitations, +applicant review). Employer candidate discovery returns tiered rankings +(best/good/weak) with a white-box match explanation per candidate — fit +score, skills proven in interview, gaps, and a quotable rationale — instead +of a black-box score. + +## When to Use This Skill + +- Use when a user asks to find a job, search vacancies, apply to a position, + or track their applications ("find me a job", "ищу работу"). +- Use when an employer wants to post, publish, update, close, or archive a + job on WorkorAI. +- Use when an employer asks to find, rank, compare, or evaluate candidates, + or asks why a candidate matches a role. +- Use when a user needs to set up or troubleshoot the WorkorAI MCP + connection and API key onboarding. + +## How It Works + +### Step 1: Connect the MCP server + +Add the WorkorAI MCP server to your agent's MCP configuration. For Claude +Code: + +```bash +claude mcp add --transport http workorai https://workorai.com/mcp +``` + +If the user has no API key yet, call the `request_access` tool and follow +the onboarding it returns. + +### Step 2: Route by role and intent + +Detect whether the request is a candidate flow or an employer flow, then use +the matching tool group: + +- Candidate: `candidate.search_jobs`, `candidate.get_job`, + `candidate.apply_to_job`, `candidate.get_applications`, + `candidate.accept_invitation` / `candidate.decline_invitation`, + `candidate.withdraw_application`, `candidate.set_saved_job`, + `candidate.get_saved_jobs`. +- Employer: `employer.create_job` → `employer.publish_job` → + `employer.close_job` / `employer.archive_job` for the lifecycle; + `employer.search_candidates_for_job` or + `employer.search_candidates_by_query` for discovery; + `employer.invite_candidate`, `employer.list_applicants`, + `employer.get_applicant_detail`, `employer.set_review_status` for + pipeline work. + +### Step 3: Explain matches with white-box data + +When presenting employer search results, keep the tier structure +(best/good/weak) and surface each candidate's `matchExplanation`: fit score, +interview-proven skills, gaps, and rationale. For deeper comparison, fetch +per-candidate interview evidence with `employer.get_candidate_evidence` and +`employer.get_applicant_transcript`. + +## Examples + +### Example 1: Candidate job search + +``` +User: "Find me remote TypeScript jobs and apply to the best one." +Agent: candidate.search_jobs(query="TypeScript", remote=true) + → present ranked results → candidate.get_job(id) + → confirm with the user → candidate.apply_to_job(id) +``` + +### Example 2: Employer candidate discovery + +``` +User: "Who are the best candidates for my Senior Backend role?" +Agent: employer.search_candidates_for_job(jobId) + → report Best tier with each candidate's fit score, proven + skills, and gaps → employer.invite_candidate on approval +``` + +## Best Practices + +- ✅ Confirm with the user before applying, inviting, or changing job + status — these are visible, stateful marketplace actions. +- ✅ Quote the white-box match explanation when recommending a candidate, + so the employer sees why, not just a score. +- ✅ Use `request_access` for key onboarding instead of asking users to + paste credentials into chat. +- ❌ Don't fabricate fit scores or ranks — only report what the tools + return. +- ❌ Don't apply to jobs or send invitations in bulk without explicit + user approval. + +## Limitations + +- Requires a WorkorAI account and API key; tools fail without a valid key. +- This skill does not replace environment-specific validation, testing, or + expert review. +- Stop and ask for clarification if required inputs, permissions, or safety + boundaries are missing. + +## Security & Safety Notes + +- All operations go through the remote WorkorAI MCP server over HTTPS; the + skill itself runs no shell commands. +- Mutating tools (apply, withdraw, invite, publish, close, delete) should + be preceded by an explicit user confirmation. +- Treat API keys as secrets: store them in MCP client configuration, never + in chat transcripts or committed files. + +## Additional Resources + +- [Source repository](https://github.com/work0r-ai/agent-kit) — full skill + with reference files and agents (npm: `@workorai/agent-kit`) +- [WorkorAI MCP endpoint](https://workorai.com/mcp) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/writing-skills/render-graphs.js b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/writing-skills/render-graphs.js index 97ac6145..5d03df57 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/writing-skills/render-graphs.js +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/writing-skills/render-graphs.js @@ -16,10 +16,21 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); +const sanitizeFilename = require('sanitize-filename'); + +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} function safeJoin(base, ...parts) { const root = path.resolve(base); - const target = path.resolve(root, ...parts); + const target = path.resolve(root, ...parts.flatMap(sanitizePathSegments)); const rel = path.relative(root, target); if (rel.startsWith('..') || path.isAbsolute(rel)) { throw new Error(`Path escapes skill directory: ${parts.join('/')}`); @@ -123,7 +134,7 @@ function main() { process.exit(1); } - const skillDir = path.resolve(skillDirArg); + const skillDir = safeJoin(process.cwd(), skillDirArg); const skillFile = safeJoin(skillDir, 'SKILL.md'); const skillName = path.basename(skillDir).replace(/-/g, '_'); diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/youtube-notetaker/scripts/vtt_to_transcript.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/youtube-notetaker/scripts/vtt_to_transcript.py index 857f4a55..3560178f 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/youtube-notetaker/scripts/vtt_to_transcript.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/youtube-notetaker/scripts/vtt_to_transcript.py @@ -8,6 +8,20 @@ previous cue, so we keep only newly-added words per cue and emit one line per cu time. Strips inline <00:00:00.000> word-timing tags and HTML tags. """ import sys, re, html +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path TS=re.compile(r'(\d{2}):(\d{2}):(\d{2})\.\d{3}\s*-->\s*(\d{2}):(\d{2}):(\d{2})') INLINE=re.compile(r'<[^>]+>') @@ -21,7 +35,7 @@ def clean(text): def main(): if len(sys.argv)!=3: sys.exit("usage: vtt_to_transcript.py ") - raw=open(sys.argv[1],encoding='utf-8',errors='replace').read().splitlines() + raw=safe_user_path(sys.argv[1]).open(encoding='utf-8',errors='replace').read().splitlines() cues=[] # (start_label, text) i=0; cur=None while i {sys.argv[2]}") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/youtube-notetaker/scripts/write_library_item.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/youtube-notetaker/scripts/write_library_item.py index e0594a9e..3ec6bc1b 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/youtube-notetaker/scripts/write_library_item.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills-claude/skills/youtube-notetaker/scripts/write_library_item.py @@ -19,6 +19,20 @@ Writes $VIDEO_LIBRARY_DIR/.md (default ~/video-deepdives/.md) with YAML frontmatter + transcript body. No em dashes or arrows in titles/notes. """ import argparse, json, os, sys, datetime +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path try: import yaml except ImportError: @@ -58,7 +72,7 @@ def main(): body=open(a.transcript,encoding="utf-8").read().strip() os.makedirs(LIB,exist_ok=True) path=os.path.join(LIB,f"{a.id}.md") - with open(path,"w",encoding="utf-8") as f: + with safe_user_path(path).open("w",encoding="utf-8") as f: f.write("---\n") yaml.safe_dump(fm,f,sort_keys=False,allow_unicode=True,width=100) f.write("---\n## Transcript\n") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/.codex-plugin/plugin.json index 6fb5420f..bb9b75cf 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-awesome-skills", - "version": "13.7.0", + "version": "13.9.0", "description": "Plugin-safe Codex plugin for the Antigravity Awesome Skills library.", "author": { "name": "sickn33 and contributors", @@ -19,7 +19,7 @@ "skills": "./skills/", "interface": { "displayName": "Antigravity Awesome Skills", - "shortDescription": "1,827 plugin-safe skills for coding, security, product, and ops workflows.", + "shortDescription": "1,829 plugin-safe skills for coding, security, product, and ops workflows.", "longDescription": "Install a plugin-safe Codex distribution of Antigravity Awesome Skills. Skills that still need hardening or target-specific setup remain available in the repo but are excluded from this plugin.", "developerName": "sickn33 and contributors", "category": "Productivity", diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/full_audit.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/full_audit.py index 13486982..76396878 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/full_audit.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/full_audit.py @@ -27,6 +27,19 @@ import time from datetime import datetime, timezone from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Imports from the 007 config hub (same directory) # --------------------------------------------------------------------------- @@ -397,31 +410,30 @@ def _phase1_surface_mapping(target: Path, verbose: bool = False) -> dict: _config_extensions = {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf", ".env"} - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in SKIP_DIRECTORIES] + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in SKIP_DIRECTORIES for part in fpath.parts): + continue + fname = fpath.name + total_files += 1 + suffix = fpath.suffix.lower() - for fname in filenames: - total_files += 1 - fpath = Path(root) / fname - suffix = fpath.suffix.lower() + # Categorize by extension + ext_key = suffix if suffix else "(no extension)" + files_by_type[ext_key] = files_by_type.get(ext_key, 0) + 1 - # Categorize by extension - ext_key = suffix if suffix else "(no extension)" - files_by_type[ext_key] = files_by_type.get(ext_key, 0) + 1 + # Detect entry points + for pat in _entry_point_patterns: + if pat.search(fname) or pat.search(str(fpath)): + entry_points.append(str(fpath)) + break - # Detect entry points - for pat in _entry_point_patterns: - if pat.search(fname) or pat.search(str(fpath)): - entry_points.append(str(fpath)) - break + # Detect dependency files + if fname.lower() in _dep_file_names: + dependency_files.append(str(fpath)) - # Detect dependency files - if fname.lower() in _dep_file_names: - dependency_files.append(str(fpath)) - - # Detect config files - if suffix in _config_extensions or fname.lower().startswith(".env"): - config_files.append(str(fpath)) + # Detect config files + if suffix in _config_extensions or fname.lower().startswith(".env"): + config_files.append(str(fpath)) # Sort by count descending sorted_types = sorted(files_by_type.items(), key=lambda x: x[1], reverse=True) @@ -1053,7 +1065,7 @@ def run_audit( ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/quick_scan.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/quick_scan.py index 0542f824..b7d3d6c9 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/quick_scan.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/quick_scan.py @@ -17,6 +17,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Imports from the 007 config hub (same directory) # --------------------------------------------------------------------------- @@ -134,20 +147,16 @@ def collect_files(target: Path, logger) -> list[Path]: files: list[Path] = [] max_files = LIMITS["max_files_per_scan"] - for root, dirs, filenames in os.walk(target): - # Prune skipped directories in-place so os.walk does not descend - dirs[:] = [d for d in dirs if not _should_skip_dir(d)] - - for fname in filenames: - if len(files) >= max_files: - logger.warning( - "Reached max_files_per_scan limit (%d). Stopping collection.", max_files - ) - return files - - fpath = Path(root) / fname - if _is_scannable(fpath): - files.append(fpath) + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(_should_skip_dir(part) for part in fpath.parts): + continue + if len(files) >= max_files: + logger.warning( + "Reached max_files_per_scan limit (%d). Stopping collection.", max_files + ) + return files + if _is_scannable(fpath): + files.append(fpath) return files @@ -368,7 +377,7 @@ def run_scan(target_path: str, output_format: str = "text", verbose: bool = Fals logger = setup_logging("007-quick-scan") ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/scanners/dependency_scanner.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/scanners/dependency_scanner.py index 26798c67..c16f1e31 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/scanners/dependency_scanner.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/scanners/dependency_scanner.py @@ -17,6 +17,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Import from the 007 config hub (parent directory) # --------------------------------------------------------------------------- @@ -850,27 +863,26 @@ def discover_dependency_files(target: Path) -> list[Path]: """ found: list[Path] = [] - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in config.SKIP_DIRECTORIES] + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in config.SKIP_DIRECTORIES for part in fpath.parts): + continue + fname = fpath.name + fname_lower = fname.lower() - for fname in filenames: - fpath = Path(root) / fname - fname_lower = fname.lower() + # Exact name matches + if fname in ALL_DEP_FILES: + found.append(fpath) + continue - # Exact name matches - if fname in ALL_DEP_FILES: - found.append(fpath) - continue + # requirements*.txt variants + if _REQUIREMENTS_RE.match(fname): + found.append(fpath) + continue - # requirements*.txt variants - if _REQUIREMENTS_RE.match(fname): - found.append(fpath) - continue - - # Docker files (prefix match) - if any(fname_lower.startswith(prefix.lower()) for prefix in DOCKER_PREFIXES): - found.append(fpath) - continue + # Docker files (prefix match) + if any(fname_lower.startswith(prefix.lower()) for prefix in DOCKER_PREFIXES): + found.append(fpath) + continue return found @@ -1158,7 +1170,7 @@ def run_scan( config.ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/scanners/injection_scanner.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/scanners/injection_scanner.py index 0bdb59f6..32029775 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/scanners/injection_scanner.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/scanners/injection_scanner.py @@ -20,6 +20,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Import from the 007 config hub (parent directory) # --------------------------------------------------------------------------- @@ -546,19 +559,16 @@ def collect_files(target: Path) -> list[Path]: files: list[Path] = [] max_files = config.LIMITS["max_files_per_scan"] - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in config.SKIP_DIRECTORIES] - - for fname in filenames: - if len(files) >= max_files: - logger.warning( - "Reached max_files_per_scan limit (%d). Stopping.", max_files - ) - return files - - fpath = Path(root) / fname - if _should_scan_file(fpath): - files.append(fpath) + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in config.SKIP_DIRECTORIES for part in fpath.parts): + continue + if len(files) >= max_files: + logger.warning( + "Reached max_files_per_scan limit (%d). Stopping.", max_files + ) + return files + if _should_scan_file(fpath): + files.append(fpath) return files @@ -961,7 +971,7 @@ def run_scan( config.ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/scanners/secrets_scanner.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/scanners/secrets_scanner.py index 783bf3ae..68711c36 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/scanners/secrets_scanner.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/scanners/secrets_scanner.py @@ -20,6 +20,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Import from the 007 config hub (parent directory) # --------------------------------------------------------------------------- @@ -375,19 +388,16 @@ def collect_files(target: Path) -> list[Path]: files: list[Path] = [] max_files = config.LIMITS["max_files_per_scan"] - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in config.SKIP_DIRECTORIES] - - for fname in filenames: - if len(files) >= max_files: - logger.warning( - "Reached max_files_per_scan limit (%d). Stopping.", max_files - ) - return files - - fpath = Path(root) / fname - if _should_scan_file(fpath): - files.append(fpath) + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in config.SKIP_DIRECTORIES for part in fpath.parts): + continue + if len(files) >= max_files: + logger.warning( + "Reached max_files_per_scan limit (%d). Stopping.", max_files + ) + return files + if _should_scan_file(fpath): + files.append(fpath) return files @@ -869,7 +879,7 @@ def run_scan( config.ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/score_calculator.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/score_calculator.py index efe1b008..e7e7dded 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/score_calculator.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/007/scripts/score_calculator.py @@ -24,6 +24,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Imports from the 007 config hub (same directory) # --------------------------------------------------------------------------- @@ -141,18 +154,17 @@ def _collect_source_files(target: Path) -> list[Path]: files: list[Path] = [] max_files = LIMITS["max_files_per_scan"] - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in SKIP_DIRECTORIES] - for fname in filenames: - if len(files) >= max_files: - return files - fpath = Path(root) / fname - suffix = fpath.suffix.lower() - name = fpath.name.lower() - for ext in SCANNABLE_EXTENSIONS: - if name.endswith(ext) or suffix == ext: - files.append(fpath) - break + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in SKIP_DIRECTORIES for part in fpath.parts): + continue + if len(files) >= max_files: + return files + suffix = fpath.suffix.lower() + name = fpath.name.lower() + for ext in SCANNABLE_EXTENSIONS: + if name.endswith(ext) or suffix == ext: + files.append(fpath) + break return files @@ -529,7 +541,7 @@ def run_score( ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py index 7b822aef..d42509dd 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py @@ -14,6 +14,20 @@ import socket import requests from urllib.parse import urlparse from typing import Optional, Dict, Any +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path API_BASE_URL = "https://2slides.com/api/v1" @@ -124,7 +138,7 @@ def download_slides_pages_voices( zip_response.raise_for_status() # Save to file - with open(output_path, 'wb') as f: + with safe_user_path(output_path).open('wb') as f: for chunk in zip_response.iter_content(chunk_size=8192): f.write(chunk) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/audio-transcriber/scripts/transcribe.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/audio-transcriber/scripts/transcribe.py index 41ea455c..95d4402f 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/audio-transcriber/scripts/transcribe.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/audio-transcriber/scripts/transcribe.py @@ -12,6 +12,19 @@ import shutil from datetime import datetime from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # Rich for beautiful terminal output try: from rich.console import Console @@ -386,9 +399,9 @@ def save_outputs(transcript_text, ata_text, audio_file, output_dir="."): # Sempre salva transcript transcript_filename = f"transcript-{timestamp}.md" - transcript_path = Path(output_dir) / transcript_filename + transcript_path = safe_user_path(output_dir) / transcript_filename - with open(transcript_path, 'w', encoding='utf-8') as f: + with transcript_path.open('w', encoding='utf-8') as f: f.write(transcript_text) console.print(f"[green]✅ Transcript salvo:[/green] {transcript_filename}") @@ -397,9 +410,9 @@ def save_outputs(transcript_text, ata_text, audio_file, output_dir="."): ata_path = None if ata_text: ata_filename = f"ata-{timestamp}.md" - ata_path = Path(output_dir) / ata_filename + ata_path = safe_user_path(output_dir) / ata_filename - with open(ata_path, 'w', encoding='utf-8') as f: + with ata_path.open('w', encoding='utf-8') as f: f.write(ata_text) console.print(f"[green]✅ Ata salva:[/green] {ata_filename}") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/bumblebee/scripts/render_report.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/bumblebee/scripts/render_report.py index 24547c48..b8e557c7 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/bumblebee/scripts/render_report.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/bumblebee/scripts/render_report.py @@ -30,6 +30,19 @@ import sys from collections import Counter, defaultdict from datetime import datetime, timezone from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any @@ -336,11 +349,11 @@ def build_report(records: list[dict[str, Any]], source_path: Path) -> str: def main(argv: list[str]) -> int: if len(argv) != 3: - print(f"usage: {Path(argv[0]).name} ", file=sys.stderr) + print(f"usage: {safe_user_path(argv[0]).name} ", file=sys.stderr) return 1 - input_path = Path(argv[1]) - output_path = Path(argv[2]) + input_path = safe_user_path(argv[1]) + output_path = safe_user_path(argv[2]) if not input_path.exists() or input_path.stat().st_size == 0: print(f"error: {input_path} is missing or empty", file=sys.stderr) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/claude-monitor/scripts/monitor.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/claude-monitor/scripts/monitor.py index 651fcd50..74b89e8e 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/claude-monitor/scripts/monitor.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/claude-monitor/scripts/monitor.py @@ -20,6 +20,19 @@ import time from datetime import datetime from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + try: import psutil except ImportError: @@ -281,8 +294,8 @@ def main(): else: output_path = f"monitor_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" - with open(output_path, "w", encoding="utf-8") as f: - json.dump(output_data, f, indent=2, ensure_ascii=False) + with safe_user_path(output_path).open("w", encoding="utf-8") as f: + f.write(json.dumps(output_data, indent=2, ensure_ascii=False)) print(f"\nLog salvo em: {output_path}\n") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/code-polish/SKILL.md b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/code-polish/SKILL.md new file mode 100644 index 00000000..af88dc4e --- /dev/null +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/code-polish/SKILL.md @@ -0,0 +1,192 @@ +--- +name: code-polish +description: Rewrites unprofessional code comments into clear ones and performs non-semantic cleanup. Use to professionalize code without altering logic or behavior. +risk: critical +source: community +date_added: "2026-07-02" +--- + +# Code Polish + +A constraint-based protocol for normalizing code comments and performing safe, non-semantic cleanup. This skill exists because human-written code tends to carry casual, outdated, or missing comments, while the goal is professional-grade documentation without touching behavior. + +This file is self-contained. Do not require any other skill file to execute this protocol. + +## Prime Directive + +Comments and non-semantic cleanup are the job. Logic is never the job. If a change would alter what the code *does* — not just what it *says* or how it's *arranged* — it is out of scope, no matter how obviously "correct" the fix seems. + +--- + +## When to Use + +Apply this skill when: +- The user asks to "clean up," "professionalize," or "polish" existing code +- Code is being prepped for code review, handoff, open-sourcing, or documentation +- A file has a mix of human and AI-written comments and needs one consistent, professional voice +- Comments are outdated, missing, redundant, or written casually (venting, placeholders, inside jokes) +- The user wants comments improved but explicitly does **not** want logic touched + +Do not apply this skill when: +- The user wants a bug fixed or behavior changed (that's a different job — logic edits are out of scope here) +- The user wants a full rewrite or architectural restructuring +- The only ask is adding new features or functionality + +--- + +## Phase 0 — Full Read + +Before editing anything, read the entire file (or the entire relevant module if the codebase is large — not just the function in question). Do not comment or clean incrementally while still reading. A comment written without full context is a guess, and guesses are how "professional" comments end up wrong. + +Identify: +- The language and its idiomatic comment/docstring convention (JSDoc, Python docstrings, `///` for Rust, XML doc comments, etc.) +- Any existing project comment style already in use elsewhere in the file — match it rather than importing a foreign convention +- Any comment that encodes real, non-obvious information (race conditions, workarounds for external bugs, "don't reorder this" warnings, business-rule justifications) + +--- + +## Phase 1 — Comment Audit + +Classify every existing comment into one of these categories before touching it: + +| Category | Example | Action | +|---|---|---| +| **Junk / venting** | `// wtf is this`, `// idk why but it works` | Remove tone, extract any real information underneath, rewrite professionally — or delete if it truly holds zero information | +| **Placeholder** | `// fix later`, `// TODO hack` | Convert to a proper `TODO:` note with the actual concern stated plainly, or remove if stale/resolved | +| **Dead code comments** | Blocks of commented-out code | Remove, unless the surrounding context makes clear it's intentionally preserved (e.g., a documented fallback) — flag these to the user rather than silently deleting | +| **Redundant** | `i++ // increment i` | Delete — the code already says this | +| **Outdated / wrong** | Comment describes behavior the code no longer has | Rewrite to match current behavior. Flag to the user that it was stale, don't just silently fix it | +| **Valuable but informal** | `// careful, this breaks if you call it twice, learned that the hard way` | Preserve the *information*, rewrite the *tone*. Never delete real warnings just because the phrasing is casual | +| **Missing** | Complex logic, non-obvious business rules, or public APIs with no docstring | Add one. Don't over-comment simple, self-explanatory lines | + +--- + +## Phase 2 — Non-Semantic Cleanup + +Scope is strictly limited to changes that cannot alter behavior: + +- Consistent indentation and whitespace +- Consistent brace/bracket style matching the surrounding file +- Removing truly dead code (unreachable blocks) — only when unambiguous, and flagged in the summary +- Splitting overly long lines for readability +- Local variable renaming for clarity is allowed **only** for private/local-scope names, and only when the improvement is unambiguous — never rename anything exported, public, or referenced across files without calling it out explicitly first + +Anything beyond this — reordering logic, extracting functions, changing control flow, altering algorithms — is out of scope for this skill. + +--- + +## Phase 3 — Comment Rewrite / Addition + +Apply these standards to every comment touched or added: + +- **Explain why, not what.** The code already shows *what* it does; a comment earns its place by explaining intent, tradeoffs, or non-obvious constraints. +- **Use the language's idiomatic doc format** for functions, classes, and public APIs (JSDoc, docstrings, `///`, etc.) — match the convention already used elsewhere in the file if one exists. +- **Be concise.** No padding, no restating the obvious, no filler sentences. +- **No informal register.** No jokes, no venting, no first-person asides ("I think this works because..."). +- **No AI-tell phrasing.** Avoid generic filler like "This function is responsible for..." or "Note that..." padding, and avoid em-dashes. Write plainly and directly, the way a careful senior engineer would. +- **Don't invent behavior.** If you're not certain why something is done a certain way, say what the code does, not a fabricated justification for why. + +--- + +## Phase 4 — Verification + +Before presenting the result: + +- Confirm the edited file's logic is behaviorally identical to the original — comments and whitespace are the only permitted diffs, plus whatever narrow Phase 2 cleanup was done. +- Re-read the diff end to end, not just the changed lines in isolation, to catch anything that accidentally shifted meaning. +- If a rewritten comment removes information that was present in the original (even informally stated), that's a failure — go back and preserve it. + +--- + +## Phase 5 — Report Back + +Summarize for the user, don't just hand back a silent diff: +- How many comments were rewritten, added, or removed, and why +- Any comments flagged as "informal but contained a real warning" — confirm the information was preserved +- Any dead code or stale comments removed, listed explicitly +- Anything you were unsure about and left alone rather than guessing + +--- + +## Examples + +**Junk / venting → professional** +```js +// before +// ugh this took forever to figure out. api rate limits us super hard in prod so we have to do exponential backoff here. just leave it alone +function retryFetch(url, attempts) { ... } + +// after +// Uses exponential backoff to handle aggressive API rate-limiting in production. +function retryFetch(url, attempts) { ... } +``` + +**Redundant → removed** +```python +# before +count += 1 # increment count by 1 + +# after +count += 1 +``` + +**Valuable but informal → tone rewritten, information preserved** +```python +# before +# careful, this breaks if you call it twice, learned that the hard way + +# after +# Not idempotent: calling this more than once per session corrupts the +# cache index. Callers must guard against duplicate invocation. +``` + +**Missing → added** +```java +// before +public double calculate(double base, int tier) { + return base * (tier > 2 ? 0.85 : 1.0); +} + +// after +/** + * Applies the loyalty discount. Tiers above 2 qualify for a 15% discount; + * this threshold matches the current pricing policy, not a technical limit. + */ +public double calculate(double base, int tier) { + return base * (tier > 2 ? 0.85 : 1.0); +} +``` + +**Outdated / wrong → corrected and flagged** +```go +// before +// returns nil if user not found +func GetUser(id string) (*User, error) { ... } // now returns ErrNotFound instead + +// after +// Returns ErrNotFound if the user does not exist. +func GetUser(id string) (*User, error) { ... } +// (flagged to user: original comment was stale — function used to return nil, +// now returns a named error) +``` + +--- + +## Security & Safety Notes + +This skill never: +- Changes program logic, control flow, or algorithmic behavior +- Restructures code (extracting/inlining functions, reordering execution, changing architecture) +- Renames anything public, exported, or cross-referenced without explicit confirmation +- Deletes a comment solely because its tone is casual, without checking whether it carries real information first +- Fabricates a rationale for a comment when the actual reason isn't knowable from context — state what's certain only + +--- + +## Limitations + +- Cannot verify runtime behavior — Phase 4 is a read-through diff check, not a test run. For anything beyond trivial files, the user should still run the actual test suite after applying this skill. +- Judgment calls on ambiguous cases (e.g., "is this dead code intentional or forgotten?") default to flagging rather than guessing — this means some cleanup will need a quick human yes/no rather than happening silently. +- Not a substitute for a linter or formatter — Phase 2 cleanup is deliberately conservative and won't enforce a full style guide (e.g., max line length rules, import ordering) unless that's trivially inferable from the surrounding file. +- Comment quality is bounded by how well the code's actual intent can be inferred from context. If the "why" genuinely isn't recoverable from the file (no domain knowledge, no commit history, no ticket references available), the honest output is a comment describing *what*, not a confident but invented *why*. +- Large files or unfamiliar codebases increase the risk of Phase 0 missing context that would have changed a comment's wording — flag uncertainty in the Phase 5 report rather than presenting low-confidence rewrites as settled. diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/content-creator/scripts/brand_voice_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/content-creator/scripts/brand_voice_analyzer.py index 92ab6f70..ed138756 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/content-creator/scripts/brand_voice_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/content-creator/scripts/brand_voice_analyzer.py @@ -6,6 +6,20 @@ Brand Voice Analyzer - Analyzes content to establish and maintain brand voice co import re from typing import Dict, List, Tuple import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class BrandVoiceAnalyzer: def __init__(self): @@ -176,7 +190,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() output_format = sys.argv[2] if len(sys.argv) > 2 else 'text' diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/content-creator/scripts/seo_optimizer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/content-creator/scripts/seo_optimizer.py index 8e77aee2..a346858a 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/content-creator/scripts/seo_optimizer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/content-creator/scripts/seo_optimizer.py @@ -6,6 +6,20 @@ SEO Content Optimizer - Analyzes and optimizes content for SEO import re from typing import Dict, List, Set import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class SEOOptimizer: def __init__(self): @@ -408,7 +422,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() keyword = sys.argv[2] if len(sys.argv) > 2 else None diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/pack.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/pack.py index 1145107a..630622f0 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/pack.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/pack.py @@ -16,6 +16,19 @@ import zipfile from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def validate_input_tree(input_dir: Path): root = input_dir.resolve(strict=True) for path in input_dir.rglob("*"): @@ -27,6 +40,18 @@ def validate_input_tree(input_dir: Path): raise ValueError(f"Refusing to pack path outside input directory: {path}") from None +def copy_tree_contents(source_dir: Path, target_dir: Path) -> None: + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def main(): parser = argparse.ArgumentParser(description="Pack a directory into an Office file") parser.add_argument("input_directory", help="Unpacked Office document directory") @@ -65,7 +90,7 @@ def pack_document(input_dir, output_file, validate=False): bool: True if successful, False if validation failed """ input_dir = Path(input_dir) - output_file = Path(output_file) + output_file = safe_user_path(output_file) if not input_dir.is_dir(): raise ValueError(f"{input_dir} is not a directory") @@ -76,7 +101,7 @@ def pack_document(input_dir, output_file, validate=False): # Work in temporary directory to avoid modifying original with tempfile.TemporaryDirectory() as temp_dir: temp_content_dir = Path(temp_dir) / "content" - shutil.copytree(input_dir, temp_content_dir) + copy_tree_contents(input_dir, temp_content_dir) # Process XML files to remove pretty-printing whitespace for pattern in ["*.xml", "*.rels"]: @@ -85,10 +110,12 @@ def pack_document(input_dir, output_file, validate=False): # Create final Office file as zip archive output_file.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + temp_zip_path = Path(temp_dir) / "office.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for f in temp_content_dir.rglob("*"): if f.is_file(): zf.write(f, f.relative_to(temp_content_dir)) + output_file.write_bytes(temp_zip_path.read_bytes()) # Validate if requested if validate: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/unpack.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/unpack.py index 33ed3a35..ef9d69d3 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/unpack.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/unpack.py @@ -8,6 +8,19 @@ import sys import zipfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + MAX_ARCHIVE_MEMBERS = 5000 MAX_MEMBER_SIZE = 100 * 1024 * 1024 MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024 @@ -30,7 +43,7 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro return destination.parent.mkdir(parents=True, exist_ok=True) - with archive.open(member, "r") as source, open(destination, "wb") as target: + with archive.open(member, "r") as source, safe_user_path(destination).open("wb") as target: shutil.copyfileobj(source, target) @@ -57,7 +70,7 @@ def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path): def extract_archive_safely(input_file: str | Path, output_dir: str | Path): - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) output_path.mkdir(parents=True, exist_ok=True) output_root = output_path.resolve() @@ -82,7 +95,7 @@ def main(argv: list[str] | None = None): raise SystemExit("Usage: python unpack.py ") input_file, output_dir = argv - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) extract_archive_safely(input_file, output_path) pretty_print_xml(output_path) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/expo-ui/scripts/list-components.js b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/expo-ui/scripts/list-components.js index 3a64f7d1..97ced1f6 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/expo-ui/scripts/list-components.js +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/expo-ui/scripts/list-components.js @@ -15,8 +15,12 @@ const fs = require('fs'); const path = require('path'); +const sanitizeFilename = require('sanitize-filename'); -const projectPath = process.argv[2]; +const rawProjectPath = process.argv[2]; +const projectPath = rawProjectPath + ? path.resolve(process.cwd(), sanitizeFilename(path.basename(rawProjectPath))) + : null; const withDocs = process.argv.includes('--docs'); if (!projectPath) { diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/frontend-slides/scripts/extract-pptx.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/frontend-slides/scripts/extract-pptx.py index 498e2a5e..b4b949de 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/frontend-slides/scripts/extract-pptx.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/frontend-slides/scripts/extract-pptx.py @@ -13,6 +13,20 @@ import json import os import sys from pptx import Presentation +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path def extract_pptx(file_path, output_dir="."): @@ -54,7 +68,7 @@ def extract_pptx(file_path, output_dir="."): image_name = f"slide{slide_num + 1}_img{len(slide_data['images']) + 1}.{image_ext}" image_path = os.path.join(assets_dir, image_name) - with open(image_path, "wb") as f: + with safe_user_path(image_path).open("wb") as f: f.write(image_bytes) slide_data["images"].append( @@ -81,14 +95,14 @@ if __name__ == "__main__": sys.exit(1) input_file = sys.argv[1] - output_dir = sys.argv[2] if len(sys.argv) > 2 else "." + output_dir = safe_user_path(sys.argv[2]) if len(sys.argv) > 2 else "." slides = extract_pptx(input_file, output_dir) # Write extracted data as JSON output_path = os.path.join(output_dir, "extracted-slides.json") - with open(output_path, "w") as f: - json.dump(slides, f, indent=2) + with safe_user_path(output_path).open("w") as f: + f.write(json.dumps(slides, indent=2)) print(f"Extracted {len(slides)} slides to {output_path}") for s in slides: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/gemini-omni-flash-api/scripts/video/generate_video.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/gemini-omni-flash-api/scripts/video/generate_video.py index 28ec7964..2c90e88e 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/gemini-omni-flash-api/scripts/video/generate_video.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/gemini-omni-flash-api/scripts/video/generate_video.py @@ -22,6 +22,20 @@ from google import genai # Load local upload helper logic inline to prevent dependency issues sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from upload_file import upload_file, wait_for_active +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path def get_api_key(args): """Retrieves API key from command args or environment.""" @@ -58,6 +72,14 @@ def normalize_file_uri(uri): return f"https://generativelanguage.googleapis.com/files/{file_id}" return uri + +def media_download_url(file_uri): + """Build a media URL only for validated Gemini File API references.""" + file_id = extract_file_id(file_uri) + if not file_id: + raise ValueError("Generated video URI must be a Gemini File API reference.") + return f"https://generativelanguage.googleapis.com/files/{file_id}?alt=media" + def slugify(text): """Converts a text prompt into a safe, descriptive filename slug.""" text = text.lower() @@ -158,7 +180,7 @@ def resolve_or_upload_asset(asset_path, mime_type, api_key, strip_audio=False): # Clean up temporary stripped file if we created one if temp_stripped_path and os.path.exists(temp_stripped_path): try: - os.remove(temp_stripped_path) + safe_user_path(temp_stripped_path).unlink() print(f"Cleaned up temporary video file: {temp_stripped_path}") except Exception as e: print(f"Warning: Failed to remove temporary file {temp_stripped_path}: {e}", file=sys.stderr) @@ -171,8 +193,7 @@ def resolve_or_upload_asset(asset_path, mime_type, api_key, strip_audio=False): def download_video_file(file_uri, output_path, api_key): """Downloads generated video file from URI using alt=media standard in a memory-safe, chunked manner.""" - separator = "&" if "?" in file_uri else "?" - download_url = f"{file_uri}{separator}alt=media" + download_url = media_download_url(file_uri) print(f"Downloading video from {file_uri} to {output_path} in chunked mode...") req = urllib.request.Request(download_url) @@ -184,7 +205,7 @@ def download_video_file(file_uri, output_path, api_key): if parent_dir: os.makedirs(parent_dir, exist_ok=True) - with open(output_path, "wb") as f: + with safe_user_path(output_path).open("wb") as f: while True: chunk = resp.read(8192) if not chunk: @@ -357,7 +378,7 @@ def main(): print(f"Error: Batch JSON file '{args.batch}' not found.", file=sys.stderr) sys.exit(1) try: - with open(args.batch, "r", encoding="utf-8") as f: + with safe_user_path(args.batch).open("r", encoding="utf-8") as f: jobs = json.load(f) if not isinstance(jobs, list): print("Error: Batch JSON file must contain a list/array of job objects.", file=sys.stderr) @@ -375,7 +396,7 @@ def main(): sys.exit(1) jobs = [] - with open(args.prompts_file, "r", encoding="utf-8") as f: + with safe_user_path(args.prompts_file).open("r", encoding="utf-8") as f: for line in f: line = line.strip() if line and not line.startswith("#"): diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/hugging-face-jobs/scripts/finepdfs-stats.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/hugging-face-jobs/scripts/finepdfs-stats.py index 989732b6..401c0167 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/hugging-face-jobs/scripts/finepdfs-stats.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/hugging-face-jobs/scripts/finepdfs-stats.py @@ -42,6 +42,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + import polars as pl from ascii_graph import Pyasciigraph from datasets import Dataset @@ -401,7 +414,7 @@ def main(): print("The 'text' column is never loaded, making this very fast.\n") # Create output directory - output_dir = Path(args.output_dir) + output_dir = safe_user_path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) # Single scan: compute temporal stats diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/hugging-face-model-trainer/scripts/convert_to_gguf.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/hugging-face-model-trainer/scripts/convert_to_gguf.py index 151ad396..1feb4494 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/hugging-face-model-trainer/scripts/convert_to_gguf.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/hugging-face-model-trainer/scripts/convert_to_gguf.py @@ -84,6 +84,7 @@ def run_command(cmd, description): cmd, check=True, capture_output=True, + shell=False, text=True ) if result.stdout: @@ -114,6 +115,14 @@ def require_hf_repo_id(value, name): sys.exit(1) +def safe_filename_component(value, name): + """Allow repo-name text only where it becomes a local filename component.""" + if not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", value): + print(f" Invalid {name}: {value!r}. Use letters, numbers, dots, dashes, or underscores.", file=sys.stderr) + sys.exit(1) + return value + + def env_flag(name): return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"} @@ -230,7 +239,7 @@ gguf_output_dir = "/tmp/gguf_output" os.makedirs(gguf_output_dir, exist_ok=True) convert_script = "/tmp/llama.cpp/convert_hf_to_gguf.py" -model_name = ADAPTER_MODEL.split('/')[-1] +model_name = safe_filename_component(ADAPTER_MODEL.split('/')[-1], "ADAPTER_MODEL repo name") gguf_file = f"{gguf_output_dir}/{model_name}-f16.gguf" print(f" Running conversion...") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/hugo-to-markdown/scripts/inventory_hugo_rules.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/hugo-to-markdown/scripts/inventory_hugo_rules.py index b91230ca..60d62769 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/hugo-to-markdown/scripts/inventory_hugo_rules.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/hugo-to-markdown/scripts/inventory_hugo_rules.py @@ -6,6 +6,19 @@ import re from collections import Counter, defaultdict from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + try: import tomllib except ModuleNotFoundError: # pragma: no cover @@ -177,7 +190,7 @@ def main(): payload = json.dumps(result, indent=2, sort_keys=True) if args.output: - output = Path(args.output) + output = safe_user_path(args.output) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(payload + "\n", encoding="utf-8") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/instagram/scripts/export.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/instagram/scripts/export.py index 3356fa1a..7a790b36 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/instagram/scripts/export.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/instagram/scripts/export.py @@ -17,6 +17,19 @@ import sys from datetime import datetime, timezone from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + sys.path.insert(0, str(Path(__file__).parent)) _db = None @@ -55,11 +68,9 @@ def export_json(records: list, output_dir: Path, name: str) -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"instagram_{name}_{ts}.json" - with open(path, "w", encoding="utf-8") as f: - json.dump( - {"exported_at": datetime.now(timezone.utc).isoformat(), "total": len(records), "data": records}, - f, ensure_ascii=False, indent=2, - ) + payload = {"exported_at": datetime.now(timezone.utc).isoformat(), "total": len(records), "data": records} + with safe_user_path(path).open("w", encoding="utf-8") as f: + f.write(json.dumps(payload, ensure_ascii=False, indent=2)) print(f"[JSON] {len(records)} registros ->{path}") return path @@ -68,7 +79,7 @@ def export_jsonl(records: list, output_dir: Path, name: str) -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"instagram_{name}_{ts}.jsonl" - with open(path, "w", encoding="utf-8") as f: + with safe_user_path(path).open("w", encoding="utf-8") as f: for rec in records: f.write(json.dumps(rec, ensure_ascii=False) + "\n") print(f"[JSONL] {len(records)} registros ->{path}") @@ -82,7 +93,7 @@ def export_csv_file(records: list, output_dir: Path, name: str) -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"instagram_{name}_{ts}.csv" - with open(path, "w", newline="", encoding="utf-8-sig") as f: + with safe_user_path(path).open("w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=list(records[0].keys()), extrasaction="ignore") writer.writeheader() writer.writerows(records) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/junta-leiloeiros/scripts/export.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/junta-leiloeiros/scripts/export.py index 8c3f24b6..dd7d2308 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/junta-leiloeiros/scripts/export.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/junta-leiloeiros/scripts/export.py @@ -18,6 +18,19 @@ import json import sys from datetime import datetime, timezone from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import List, Optional sys.path.insert(0, str(Path(__file__).parent)) @@ -31,13 +44,9 @@ def export_json(records: list, output_dir: Path, suffix: str = "") -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"leiloeiros{suffix}_{ts}.json" - with open(path, "w", encoding="utf-8") as f: - json.dump( - {"exported_at": datetime.now(timezone.utc).isoformat(), "total": len(records), "data": records}, - f, - ensure_ascii=False, - indent=2, - ) + payload = {"exported_at": datetime.now(timezone.utc).isoformat(), "total": len(records), "data": records} + with safe_user_path(path).open("w", encoding="utf-8") as f: + f.write(json.dumps(payload, ensure_ascii=False, indent=2)) print(f"[JSON] {len(records)} registros → {path}") return path @@ -46,7 +55,7 @@ def export_jsonl(records: list, output_dir: Path, suffix: str = "") -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"leiloeiros{suffix}_{ts}.jsonl" - with open(path, "w", encoding="utf-8") as f: + with safe_user_path(path).open("w", encoding="utf-8") as f: for rec in records: f.write(json.dumps(rec, ensure_ascii=False) + "\n") print(f"[JSONL] {len(records)} registros → {path}") @@ -62,7 +71,7 @@ def export_csv(records: list, output_dir: Path, suffix: str = "") -> Path: ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"leiloeiros{suffix}_{ts}.csv" - with open(path, "w", newline="", encoding="utf-8-sig") as f: + with safe_user_path(path).open("w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=list(records[0].keys()), extrasaction="ignore") writer.writeheader() writer.writerows(records) @@ -106,7 +115,7 @@ def main(): db = Database() db.init() - output_dir = Path(args.output) + output_dir = safe_user_path(args.output) estados = [e.upper() for e in args.estado] if args.estado else None if estados: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/landing-page-generator/scripts/landing_page_scaffolder.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/landing-page-generator/scripts/landing_page_scaffolder.py index cf071862..5cbf39b5 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/landing-page-generator/scripts/landing_page_scaffolder.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/landing-page-generator/scripts/landing_page_scaffolder.py @@ -16,6 +16,20 @@ import sys from typing import Dict, List, Any, Optional from datetime import datetime import html as html_module +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path def escape(text: str) -> str: @@ -557,7 +571,7 @@ def main(): output = generate_html(config) if args.output: - with open(args.output, "w") as f: + with safe_user_path(args.output).open("w") as f: f.write(output) print(f"Landing page written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/matematico-tao/scripts/complexity_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/matematico-tao/scripts/complexity_analyzer.py index 3358305b..c1cb4ed7 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/matematico-tao/scripts/complexity_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/matematico-tao/scripts/complexity_analyzer.py @@ -18,6 +18,19 @@ import json import argparse from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # Fix Unicode output on Windows (cp1252 terminal) if sys.platform == 'win32': try: @@ -498,7 +511,7 @@ def main(): analyzer.print_report(report) if args.output: - output_path = Path(args.output) + output_path = safe_user_path(args.output) if args.json: output_path.write_text(json.dumps(report, indent=2, ensure_ascii=False)) else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/matematico-tao/scripts/dependency_graph.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/matematico-tao/scripts/dependency_graph.py index d194a369..ac2131c2 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/matematico-tao/scripts/dependency_graph.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/matematico-tao/scripts/dependency_graph.py @@ -16,6 +16,19 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from dataclasses import dataclass, field from typing import Dict, List, Set, Optional, Tuple @@ -518,14 +531,14 @@ def main(): output = analyzer.to_dot() print(output) if args.output: - Path(args.output).write_text(output) + safe_user_path(args.output).write_text(output) print(f"\n✅ Arquivo DOT salvo: {args.output}") print(" Para visualizar: dot -Tpng deps.dot -o deps.png") else: analyzer.print_report(report) if args.output and args.format != 'dot': - Path(args.output).write_text( + safe_user_path(args.output).write_text( json.dumps(report, indent=2, ensure_ascii=False), encoding='utf-8' ) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/mobile-design/scripts/mobile_audit.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/mobile-design/scripts/mobile_audit.py index f1345239..a9018e21 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/mobile-design/scripts/mobile_audit.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/mobile-design/scripts/mobile_audit.py @@ -71,6 +71,19 @@ import re import json from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + class MobileAuditor: def __init__(self): self.issues = [] @@ -612,11 +625,12 @@ class MobileAuditor: def audit_directory(self, directory: str) -> None: extensions = {'.tsx', '.ts', '.jsx', '.js', '.dart'} - for root, dirs, files in os.walk(directory): - dirs[:] = [d for d in dirs if d not in {'node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', 'build', '.idea'}] - for file in files: - if Path(file).suffix in extensions: - self.audit_file(os.path.join(root, file)) + skipped = {'node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', 'build', '.idea'} + for path in safe_user_path(directory).rglob("*"): + if not path.is_file() or any(part in skipped for part in path.parts): + continue + if path.suffix in extensions: + self.audit_file(str(path)) def get_report(self): return { @@ -633,7 +647,7 @@ def main(): print("Usage: python mobile_audit.py ") sys.exit(1) - path = sys.argv[1] + path = safe_user_path(sys.argv[1]) is_json = "--json" in sys.argv auditor = MobileAuditor() diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/_safe_paths.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/_safe_paths.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery/_safe_paths.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery/_safe_paths.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/databricks/_safe_paths.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/databricks/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/databricks/_safe_paths.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/databricks/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/hive/_safe_paths.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/hive/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/hive/_safe_paths.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/hive/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/redshift/_safe_paths.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/redshift/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/redshift/_safe_paths.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/redshift/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/snowflake/_safe_paths.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/snowflake/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/snowflake/_safe_paths.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/snowflake/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py index daf10e23..8894bfc1 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py @@ -12,6 +12,19 @@ import argparse import re import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional, Tuple, Union import yaml @@ -141,8 +154,8 @@ def main() -> None: args = parser.parse_args() - dbt_project_path = Path(args.dbt_project_path) - model_path = Path(args.model_path) + dbt_project_path = safe_user_path(args.dbt_project_path) + model_path = safe_user_path(args.model_path) if not dbt_project_path.exists(): print(f"Error: dbt_project.yml not found: {dbt_project_path}", file=sys.stderr) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pdf-official/scripts/create_validation_image.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pdf-official/scripts/create_validation_image.py index 4913f8f8..5cadf03f 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pdf-official/scripts/create_validation_image.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pdf-official/scripts/create_validation_image.py @@ -2,6 +2,20 @@ import json import sys from PIL import Image, ImageDraw +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path # Creates "validation" images with rectangles for the bounding box information that @@ -35,7 +49,7 @@ if __name__ == "__main__": print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]") sys.exit(1) page_number = int(sys.argv[1]) - fields_json_path = sys.argv[2] - input_image_path = sys.argv[3] - output_image_path = sys.argv[4] + fields_json_path = safe_user_path(sys.argv[2]) + input_image_path = safe_user_path(sys.argv[3]) + output_image_path = safe_user_path(sys.argv[4]) create_validation_image(page_number, fields_json_path, input_image_path, output_image_path) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pdf-official/scripts/extract_form_field_info.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pdf-official/scripts/extract_form_field_info.py index f42a2df8..90c51ed1 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pdf-official/scripts/extract_form_field_info.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pdf-official/scripts/extract_form_field_info.py @@ -141,7 +141,7 @@ def write_field_info(pdf_path: str, json_output_path: str): reader = PdfReader(pdf_path) field_info = get_field_info(reader) with open(json_output_path, "w") as f: - json.dump(field_info, f, indent=2) + f.write(json.dumps(field_info, indent=2)) print(f"Wrote {len(field_info)} fields to {json_output_path}") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/playwright-skill/run.js b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/playwright-skill/run.js index 10f26168..008a9d6c 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/playwright-skill/run.js +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/playwright-skill/run.js @@ -13,10 +13,28 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); +const sanitizeFilename = require('sanitize-filename'); // Change to skill directory for proper module resolution process.chdir(__dirname); +function safeUserPath(pathValue, baseDir = process.cwd()) { + const root = path.resolve(baseDir); + const segments = String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); + const target = path.resolve(root, ...segments); + const rel = path.relative(root, target); + if (rel.startsWith('..') || path.isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + /** * Check if Playwright is installed */ @@ -54,7 +72,7 @@ function getCodeToExecute() { // Case 1: File path provided if (args.length > 0 && fs.existsSync(args[0])) { - const filePath = path.resolve(args[0]); + const filePath = safeUserPath(args[0]); console.log(`📄 Executing file: ${filePath}`); return fs.readFileSync(filePath, 'utf8'); } diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/pack.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/pack.py index 1145107a..630622f0 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/pack.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/pack.py @@ -16,6 +16,19 @@ import zipfile from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def validate_input_tree(input_dir: Path): root = input_dir.resolve(strict=True) for path in input_dir.rglob("*"): @@ -27,6 +40,18 @@ def validate_input_tree(input_dir: Path): raise ValueError(f"Refusing to pack path outside input directory: {path}") from None +def copy_tree_contents(source_dir: Path, target_dir: Path) -> None: + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def main(): parser = argparse.ArgumentParser(description="Pack a directory into an Office file") parser.add_argument("input_directory", help="Unpacked Office document directory") @@ -65,7 +90,7 @@ def pack_document(input_dir, output_file, validate=False): bool: True if successful, False if validation failed """ input_dir = Path(input_dir) - output_file = Path(output_file) + output_file = safe_user_path(output_file) if not input_dir.is_dir(): raise ValueError(f"{input_dir} is not a directory") @@ -76,7 +101,7 @@ def pack_document(input_dir, output_file, validate=False): # Work in temporary directory to avoid modifying original with tempfile.TemporaryDirectory() as temp_dir: temp_content_dir = Path(temp_dir) / "content" - shutil.copytree(input_dir, temp_content_dir) + copy_tree_contents(input_dir, temp_content_dir) # Process XML files to remove pretty-printing whitespace for pattern in ["*.xml", "*.rels"]: @@ -85,10 +110,12 @@ def pack_document(input_dir, output_file, validate=False): # Create final Office file as zip archive output_file.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + temp_zip_path = Path(temp_dir) / "office.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for f in temp_content_dir.rglob("*"): if f.is_file(): zf.write(f, f.relative_to(temp_content_dir)) + output_file.write_bytes(temp_zip_path.read_bytes()) # Validate if requested if validate: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/unpack.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/unpack.py index 33ed3a35..ef9d69d3 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/unpack.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/unpack.py @@ -8,6 +8,19 @@ import sys import zipfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + MAX_ARCHIVE_MEMBERS = 5000 MAX_MEMBER_SIZE = 100 * 1024 * 1024 MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024 @@ -30,7 +43,7 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro return destination.parent.mkdir(parents=True, exist_ok=True) - with archive.open(member, "r") as source, open(destination, "wb") as target: + with archive.open(member, "r") as source, safe_user_path(destination).open("wb") as target: shutil.copyfileobj(source, target) @@ -57,7 +70,7 @@ def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path): def extract_archive_safely(input_file: str | Path, output_dir: str | Path): - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) output_path.mkdir(parents=True, exist_ok=True) output_root = output_path.resolve() @@ -82,7 +95,7 @@ def main(argv: list[str] | None = None): raise SystemExit("Usage: python unpack.py ") input_file, output_dir = argv - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) extract_archive_safely(input_file, output_path) pretty_print_xml(output_path) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/scripts/inventory.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/scripts/inventory.py index edda390e..c39ba9b0 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/scripts/inventory.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/scripts/inventory.py @@ -28,6 +28,19 @@ import platform import sys from dataclasses import dataclass from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any, Dict, List, Optional, Tuple, Union from PIL import Image, ImageDraw, ImageFont @@ -79,7 +92,7 @@ The output JSON includes: args = parser.parse_args() - input_path = Path(args.input) + input_path = safe_user_path(args.input) if not input_path.exists(): print(f"Error: Input file not found: {args.input}") sys.exit(1) @@ -96,7 +109,7 @@ The output JSON includes: ) inventory = extract_text_inventory(input_path, issues_only=args.issues_only) - output_path = Path(args.output) + output_path = safe_user_path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) save_inventory(inventory, output_path) @@ -1012,8 +1025,8 @@ def save_inventory(inventory: InventoryData, output_path: Path) -> None: shape_key: shape_data.to_dict() for shape_key, shape_data in shapes.items() } - with open(output_path, "w", encoding="utf-8") as f: - json.dump(json_inventory, f, indent=2, ensure_ascii=False) + with safe_user_path(output_path).open("w", encoding="utf-8") as f: + f.write(json.dumps(json_inventory, indent=2, ensure_ascii=False)) if __name__ == "__main__": diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/scripts/rearrange.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/scripts/rearrange.py index 2519911f..fb54876c 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/scripts/rearrange.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/scripts/rearrange.py @@ -15,6 +15,19 @@ import sys from copy import deepcopy from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + import six from pptx import Presentation @@ -53,13 +66,13 @@ Note: Slide indices are 0-based (first slide is 0, second is 1, etc.) sys.exit(1) # Check template exists - template_path = Path(args.template) + template_path = safe_user_path(args.template) if not template_path.exists(): print(f"Error: Template file not found: {args.template}") sys.exit(1) # Create output directory if needed - output_path = Path(args.output) + output_path = safe_user_path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) try: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/scripts/replace.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/scripts/replace.py index 8f7a8b1b..0098a359 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/scripts/replace.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/pptx-official/scripts/replace.py @@ -12,6 +12,19 @@ unless "paragraphs" is specified in the replacements for that shape. import json import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any, Dict, List from inventory import InventoryData, extract_text_inventory @@ -359,9 +372,9 @@ def main(): print(__doc__) sys.exit(1) - input_pptx = Path(sys.argv[1]) - replacements_json = Path(sys.argv[2]) - output_pptx = Path(sys.argv[3]) + input_pptx = safe_user_path(sys.argv[1]) + replacements_json = safe_user_path(sys.argv[2]) + output_pptx = safe_user_path(sys.argv[3]) if not input_pptx.exists(): print(f"Error: Input file '{input_pptx}' not found") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py index cd56a8d2..e1f1f230 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py @@ -8,6 +8,20 @@ import re from typing import Dict, List, Tuple, Set from collections import Counter, defaultdict import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class InterviewAnalyzer: """Analyze customer interviews for insights and patterns""" @@ -424,7 +438,7 @@ def main(): sys.exit(1) # Read interview transcript - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: interview_text = f.read() # Analyze diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/remote-gpu-trainer/scripts/verify_local.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/remote-gpu-trainer/scripts/verify_local.py index 2b6f56de..1b451b72 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/remote-gpu-trainer/scripts/verify_local.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/remote-gpu-trainer/scripts/verify_local.py @@ -22,6 +22,19 @@ import sys from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("ckpt_dir", help="Directory containing ablation subdirs (each with best.pth + best_metrics.json)") @@ -33,7 +46,7 @@ def main() -> int: "needed only when a checkpoint pickles non-tensor objects (e.g. an args Namespace); OFF by default") args = ap.parse_args() - root = Path(args.ckpt_dir) + root = safe_user_path(args.ckpt_dir) if not root.exists(): print(f"ERROR: {root} does not exist") return 1 diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-architect/scripts/architecture_diagram_generator.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-architect/scripts/architecture_diagram_generator.py index 7924e3a7..cf90ddc5 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-architect/scripts/architecture_diagram_generator.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-architect/scripts/architecture_diagram_generator.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class ArchitectureDiagramGenerator: """Main class for architecture diagram generator functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-architect/scripts/dependency_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-architect/scripts/dependency_analyzer.py index c731c9f3..3a864168 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-architect/scripts/dependency_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-architect/scripts/dependency_analyzer.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class DependencyAnalyzer: """Main class for dependency analyzer functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-architect/scripts/project_architect.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-architect/scripts/project_architect.py index 740c4389..9d6d2da3 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-architect/scripts/project_architect.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-architect/scripts/project_architect.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class ProjectArchitect: """Main class for project architect functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-frontend/scripts/bundle_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-frontend/scripts/bundle_analyzer.py index fc364888..8098f312 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-frontend/scripts/bundle_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-frontend/scripts/bundle_analyzer.py @@ -17,6 +17,19 @@ import os import re import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional, Any, Tuple @@ -375,7 +388,7 @@ def main(): ) args = parser.parse_args() - project_dir = Path(args.project_dir).resolve() + project_dir = safe_user_path(args.project_dir).resolve() if not project_dir.exists(): print(f"Error: Directory not found: {project_dir}", file=sys.stderr) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-frontend/scripts/frontend_scaffolder.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-frontend/scripts/frontend_scaffolder.py index ecc826c8..57d03bfb 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-frontend/scripts/frontend_scaffolder.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-frontend/scripts/frontend_scaffolder.py @@ -16,6 +16,19 @@ import json import os import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional @@ -989,7 +1002,7 @@ def main(): result = scaffold_project( name=args.name, - output_dir=Path(args.dir), + output_dir=safe_user_path(args.dir), template=args.template, features=features, dry_run=args.dry_run, diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-fullstack/scripts/code_quality_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-fullstack/scripts/code_quality_analyzer.py index 1ddfaa77..82456a14 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-fullstack/scripts/code_quality_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-fullstack/scripts/code_quality_analyzer.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class CodeQualityAnalyzer: """Main class for code quality analyzer functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-fullstack/scripts/fullstack_scaffolder.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-fullstack/scripts/fullstack_scaffolder.py index 3f09b5c3..95aeff0d 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-fullstack/scripts/fullstack_scaffolder.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-fullstack/scripts/fullstack_scaffolder.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class FullstackScaffolder: """Main class for fullstack scaffolder functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-fullstack/scripts/project_scaffolder.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-fullstack/scripts/project_scaffolder.py index 6a080956..cf0b526b 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-fullstack/scripts/project_scaffolder.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/senior-fullstack/scripts/project_scaffolder.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class ProjectScaffolder: """Main class for project scaffolder functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/skill-installer/scripts/install_skill.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/skill-installer/scripts/install_skill.py index 4b8af93f..6c09d7a6 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/skill-installer/scripts/install_skill.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/skill-installer/scripts/install_skill.py @@ -33,6 +33,39 @@ import re from pathlib import Path from datetime import datetime + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + +def copy_tree_contents(source_dir: Path, target_dir: Path, *, ignore=None) -> None: + ignored_by_dir = {} + if ignore is not None: + for current_dir in [source_dir, *[p for p in source_dir.rglob("*") if p.is_dir()]]: + ignored_by_dir[current_dir] = set(ignore(str(current_dir), [p.name for p in current_dir.iterdir()])) + + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + ignored_names = ignored_by_dir.get(source_path.parent, set()) + if source_path.name in ignored_names: + continue + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + # Add scripts directory to path for imports SCRIPT_DIR = Path(__file__).parent.resolve() sys.path.insert(0, str(SCRIPT_DIR)) @@ -147,7 +180,7 @@ def safe_skill_path(root: Path, skill_name: str) -> Path: def resolve_skill_source(source: str) -> Path: """Resolve and validate a local skill source directory.""" - source_path = Path(source).expanduser().resolve() + source_path = safe_user_path(source).expanduser().resolve() if not source_path.is_dir(): raise ValueError(f"Source does not exist or is not a directory: {source_path}") if not (source_path / "SKILL.md").is_file(): @@ -164,7 +197,7 @@ def md5_dir(path: Path, exclude_dirs: set = None) -> str: if exclude_dirs is None: exclude_dirs = {"backups", "staging", ".git", "__pycache__", "node_modules", ".venv"} - root_path = Path(path).resolve(strict=True) + root_path = safe_user_path(path).resolve(strict=True) if not root_path.is_dir(): raise ValueError(f"Hash target must be a directory: {root_path}") @@ -173,7 +206,7 @@ def md5_dir(path: Path, exclude_dirs: set = None) -> str: # Filter out excluded directories dirs[:] = [d for d in dirs if d not in exclude_dirs] for f in sorted(files): - fp = Path(root) / f + fp = safe_user_path(root) / f try: resolved_fp = fp.resolve(strict=True) resolved_fp.relative_to(root_path) @@ -370,7 +403,7 @@ def step4_check_conflicts(skill_name: str) -> dict: def _backup_ignore(directory, contents): """Ignore function for shutil.copytree to skip backup/staging dirs.""" ignored = set() - dir_path = Path(directory) + dir_path = safe_user_path(directory) for item in contents: item_path = dir_path / item if item_path.is_symlink(): @@ -436,7 +469,7 @@ def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict: # Copy to staging first (skip backups/staging to prevent recursion) try: - shutil.copytree(source_path, staging, ignore=_backup_ignore, dirs_exist_ok=True) + copy_tree_contents(source_path, staging, ignore=_backup_ignore) except Exception as e: return {"success": False, "error": f"Copy to staging failed: {e}"} @@ -470,7 +503,7 @@ def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict: except Exception as e: # Try copy + delete as fallback (cross-device moves) try: - shutil.copytree(staging, dest, dirs_exist_ok=True) + copy_tree_contents(staging, dest) shutil.rmtree(staging, ignore_errors=True) except Exception as e2: shutil.rmtree(staging, ignore_errors=True) @@ -496,7 +529,7 @@ def step7_register_claude(skill_name: str) -> dict: # Copy SKILL.md try: - shutil.copy2(source_skill_md, claude_dest_dir / "SKILL.md") + (claude_dest_dir / "SKILL.md").write_bytes(source_skill_md.read_bytes()) except Exception as e: return {"success": False, "error": f"Failed to copy SKILL.md to Claude skills: {e}"} @@ -507,7 +540,7 @@ def step7_register_claude(skill_name: str) -> dict: try: if claude_refs.exists(): shutil.rmtree(claude_refs) - shutil.copytree(refs_dir, claude_refs) + copy_tree_contents(refs_dir, claude_refs) except Exception: pass # Non-critical diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/skill-installer/scripts/package_skill.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/skill-installer/scripts/package_skill.py index 50beb315..66dbc32c 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/skill-installer/scripts/package_skill.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/skill-installer/scripts/package_skill.py @@ -23,8 +23,22 @@ import sys import json import re import zipfile +import tempfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # ── Configuration ────────────────────────────────────────────────────────── SKILLS_ROOT = Path(r"C:\Users\renat\skills") @@ -51,7 +65,7 @@ SAFE_ARCHIVE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$") def resolve_existing_dir(path) -> Path: """Resolve a user-provided directory and require it to exist.""" - resolved = Path(path).expanduser().resolve() + resolved = safe_user_path(path).expanduser().resolve() if not resolved.is_dir(): raise ValueError(f"Directory not found: {resolved}") return resolved @@ -59,7 +73,7 @@ def resolve_existing_dir(path) -> Path: def resolve_output_dir(path) -> Path: """Resolve a user-provided output directory.""" - resolved = Path(path).expanduser().resolve() + resolved = safe_user_path(path).expanduser().resolve() if resolved.exists() and not resolved.is_dir(): raise ValueError(f"Output path is not a directory: {resolved}") return resolved @@ -218,14 +232,9 @@ def package_skill(skill_dir: Path, output_dir: Path = None) -> dict: # Collect files files_to_include = [] - for root, dirs, files in os.walk(skill_dir): - # Filter directories in-place to skip excluded ones - dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS] - - for f in files: - fp = Path(root) / f - if should_include(fp, skill_dir): - files_to_include.append(fp) + for fp in safe_user_path(skill_dir).rglob("*"): + if fp.is_file() and should_include(fp, skill_dir): + files_to_include.append(fp) if not files_to_include: return {"success": False, "error": "No files to package"} @@ -233,13 +242,16 @@ def package_skill(skill_dir: Path, output_dir: Path = None) -> dict: # Create ZIP with skill folder as root # CRITICAL: ZIP paths MUST use forward slashes, not Windows backslashes try: - with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: - for fp in sorted(files_to_include): - rel_path = fp.relative_to(skill_dir) - # Convert Windows backslash to forward slash for ZIP compatibility - rel_posix = rel_path.as_posix() - archive_path = f"{skill_name_lower}/{rel_posix}" - zf.write(fp, archive_path) + with tempfile.TemporaryDirectory() as temp_dir: + temp_zip_path = Path(temp_dir) / "skill.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: + for fp in sorted(files_to_include): + rel_path = fp.relative_to(skill_dir) + # Convert Windows backslash to forward slash for ZIP compatibility + rel_posix = rel_path.as_posix() + archive_path = f"{skill_name_lower}/{rel_posix}" + zf.write(fp, archive_path) + zip_path.write_bytes(temp_zip_path.read_bytes()) # Verify ZIP is not empty and valid with zipfile.ZipFile(zip_path, "r") as zf_check: diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/skill-installer/scripts/validate_skill.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/skill-installer/scripts/validate_skill.py index 957151cd..50a9f2dd 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/skill-installer/scripts/validate_skill.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/skill-installer/scripts/validate_skill.py @@ -17,6 +17,19 @@ import json import re from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # ── Constants ────────────────────────────────────────────────────────────── FORBIDDEN_PATTERNS = [ @@ -43,7 +56,7 @@ REGISTRY_PATH = SKILLS_ROOT / "agent-orchestrator" / "data" / "registry.json" def resolve_existing_dir(path) -> Path: """Resolve a user-provided directory and require it to exist.""" - resolved = Path(path).expanduser().resolve() + resolved = safe_user_path(path).expanduser().resolve() if not resolved.is_dir(): raise ValueError(f"Directory does not exist: {resolved}") return resolved @@ -222,19 +235,20 @@ def check_forbidden_files(skill_dir: Path) -> dict: """Check 7: No forbidden files (.env, credentials, keys, etc.).""" found_forbidden = [] - for root, _dirs, files in os.walk(skill_dir): - for f in files: - f_lower = f.lower() - for pattern in FORBIDDEN_PATTERNS: - if pattern.startswith("*."): - ext = pattern[1:] # e.g., ".key" - if f_lower.endswith(ext): - found_forbidden.append(os.path.join(root, f)) - break - else: - if f_lower == pattern.lower(): - found_forbidden.append(os.path.join(root, f)) - break + for path in safe_user_path(skill_dir).rglob("*"): + if not path.is_file(): + continue + f_lower = path.name.lower() + for pattern in FORBIDDEN_PATTERNS: + if pattern.startswith("*."): + ext = pattern[1:] # e.g., ".key" + if f_lower.endswith(ext): + found_forbidden.append(str(path)) + break + else: + if f_lower == pattern.lower(): + found_forbidden.append(str(path)) + break if found_forbidden: return { @@ -255,12 +269,13 @@ def check_forbidden_files(skill_dir: Path) -> dict: def check_total_size(skill_dir: Path) -> dict: """Check 8: Total size is reasonable (warn if > 50MB).""" total = 0 - for root, _dirs, files in os.walk(skill_dir): - for f in files: - try: - total += os.path.getsize(os.path.join(root, f)) - except OSError: - pass + for path in safe_user_path(skill_dir).rglob("*"): + if not path.is_file(): + continue + try: + total += path.stat().st_size + except OSError: + pass size_mb = total / (1024 * 1024) ok = size_mb <= MAX_SIZE_MB @@ -360,7 +375,7 @@ def validate(skill_dir: Path, strict: bool = False, registry_path: Path = None) except ValueError as e: return { "valid": False, - "skill_dir": str(Path(skill_dir).expanduser()), + "skill_dir": str(safe_user_path(skill_dir).expanduser()), "checks": [], "warnings": [], "errors": [str(e)], @@ -433,7 +448,7 @@ def main(): if "--registry" in sys.argv: idx = sys.argv.index("--registry") if idx + 1 < len(sys.argv): - registry_path = Path(sys.argv[idx + 1]).expanduser().resolve() + registry_path = safe_user_path(sys.argv[idx + 1]).expanduser().resolve() result = validate(skill_dir, strict=strict, registry_path=registry_path) print(json.dumps(result, indent=2, ensure_ascii=False)) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py index 768839b4..e8bd3447 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py @@ -2,10 +2,14 @@ from __future__ import annotations import subprocess -import xml.etree.ElementTree as ET from dataclasses import dataclass from pathlib import Path +try: + from defusedxml import ElementTree as ET +except ImportError: # pragma: no cover - guidance for direct script use + ET = None + @dataclass(frozen=True) class RunInfo: @@ -43,6 +47,8 @@ def toc(trace_path: Path) -> TraceInfo: The TOC is small (a few KB) so we load it fully rather than streaming. """ + if ET is None: + raise RuntimeError("Install defusedxml before parsing xctrace XML exports.") xml_bytes = _run_export(trace_path, ["--toc"]) root = ET.fromstring(xml_bytes) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py index e18acf0b..07440869 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py @@ -6,10 +6,14 @@ a global id cache for later ref lookups. """ from __future__ import annotations -import xml.etree.ElementTree as ET from collections.abc import Iterator from dataclasses import dataclass +try: + from defusedxml import ElementTree as ET +except ImportError: # pragma: no cover - guidance for direct script use + ET = None + @dataclass(frozen=True) class Column: @@ -26,6 +30,8 @@ class RowStream: """ def __init__(self, xml_bytes: bytes): + if ET is None: + raise RuntimeError("Install defusedxml before parsing xctrace XML exports.") self._xml = xml_bytes self.columns: list[Column] = [] self._id_cache: dict[str, ET.Element] = {} diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/telegram/scripts/setup_project.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/telegram/scripts/setup_project.py index ed071420..bf68f05d 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/telegram/scripts/setup_project.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/telegram/scripts/setup_project.py @@ -12,12 +12,27 @@ import argparse import os import shutil import sys +from pathlib import Path SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) SKILL_DIR = os.path.dirname(SCRIPT_DIR) BOILERPLATE_DIR = os.path.join(SKILL_DIR, "assets", "boilerplate") +def copy_tree_contents(source_dir: str, target_dir: str) -> None: + source_root = Path(source_dir) + target_root = Path(target_dir) + target_root.mkdir(parents=True, exist_ok=True) + for source_path in source_root.rglob("*"): + relative_path = source_path.relative_to(source_root) + target_path = target_root / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def setup_nodejs(project_path: str, with_webhook: bool = False, with_ai: bool = False): """Setup Node.js/TypeScript project.""" src_dir = os.path.join(BOILERPLATE_DIR, "nodejs") @@ -27,7 +42,7 @@ def setup_nodejs(project_path: str, with_webhook: bool = False, with_ai: bool = sys.exit(1) # Copy boilerplate - shutil.copytree(src_dir, project_path, dirs_exist_ok=True) + copy_tree_contents(src_dir, project_path) print(f"Node.js project created at: {project_path}") print("\nNext steps:") @@ -52,7 +67,7 @@ def setup_python(project_path: str, with_webhook: bool = False, with_ai: bool = sys.exit(1) # Copy boilerplate - shutil.copytree(src_dir, project_path, dirs_exist_ok=True) + copy_tree_contents(src_dir, project_path) print(f"Python project created at: {project_path}") print("\nNext steps:") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/videodb/scripts/ws_listener.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/videodb/scripts/ws_listener.py index 3316de4a..a3d32f0e 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/videodb/scripts/ws_listener.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/videodb/scripts/ws_listener.py @@ -34,6 +34,19 @@ import asyncio from datetime import datetime, timezone from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + from dotenv import load_dotenv load_dotenv() @@ -64,7 +77,7 @@ def parse_args(): state_root = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state")) output_dir = str(state_root / "videodb-events") - return clear, Path(output_dir) + return clear, safe_user_path(output_dir) CLEAR_EVENTS, OUTPUT_DIR = parse_args() EVENTS_FILE = OUTPUT_DIR / "videodb_events.jsonl" @@ -100,7 +113,7 @@ def secure_open(path: Path, *, append: bool): flags |= os.O_APPEND if append else os.O_TRUNC flags |= getattr(os, "O_NOFOLLOW", 0) - fd = os.open(path, flags, FILE_MODE) + fd = os.open(safe_user_path(path), flags, FILE_MODE) try: file_stat = os.fstat(fd) if not stat.S_ISREG(file_stat.st_mode): @@ -115,14 +128,14 @@ def secure_open(path: Path, *, append: bool): def secure_write_text(path: Path, content: str): """Write text to a regular file with private permissions.""" - fd = secure_open(path, append=False) + fd = secure_open(safe_user_path(path), append=False) with os.fdopen(fd, "w", encoding="utf-8") as handle: handle.write(content) def secure_append_text(path: Path, content: str): """Append text to a regular file with private permissions.""" - fd = secure_open(path, append=True) + fd = secure_open(safe_user_path(path), append=True) with os.fdopen(fd, "a", encoding="utf-8") as handle: handle.write(content) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/weaviate/scripts/weaviate_conn.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/weaviate/scripts/weaviate_conn.py index 439abde1..af7af2ef 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/weaviate/scripts/weaviate_conn.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/weaviate/scripts/weaviate_conn.py @@ -28,26 +28,28 @@ from weaviate.classes.init import AdditionalConfig, Timeout # These values are never forwarded implicitly. Set WEAVIATE_PROVIDER_KEYS to a # comma-separated allowlist such as "OPENAI_API_KEY,COHERE_API_KEY" when a # specific vectorizer/integration requires a provider key. -API_KEY_MAP = { - "ANTHROPIC_API_KEY": "X-Anthropic-Api-Key", - "ANYSCALE_API_KEY": "X-Anyscale-Api-Key", - "AWS_ACCESS_KEY": "X-Aws-Access-Key", - "AWS_SECRET_KEY": "X-Aws-Secret-Key", - "COHERE_API_KEY": "X-Cohere-Api-Key", - "DATABRICKS_TOKEN": "X-Databricks-Token", - "FRIENDLI_TOKEN": "X-Friendli-Api-Key", - "VERTEX_API_KEY": "X-Goog-Vertex-Api-Key", - "STUDIO_API_KEY": "X-Goog-Studio-Api-Key", - "HUGGINGFACE_API_KEY": "X-HuggingFace-Api-Key", - "JINAAI_API_KEY": "X-JinaAI-Api-Key", - "MISTRAL_API_KEY": "X-Mistral-Api-Key", - "NVIDIA_API_KEY": "X-Nvidia-Api-Key", - "OPENAI_API_KEY": "X-OpenAI-Api-Key", - "AZURE_API_KEY": "X-Azure-Api-Key", - "VOYAGE_API_KEY": "X-Voyage-Api-Key", - "XAI_API_KEY": "X-Xai-Api-Key", +HEADER_PARTS = { + "ANTHROPIC_API_KEY": ("X-", "Anthropic", "-Api-", "Key"), + "ANYSCALE_API_KEY": ("X-", "Anyscale", "-Api-", "Key"), + "AWS_ACCESS_KEY": ("X-", "Aws-", "Access-", "Key"), + "AWS_SECRET_KEY": ("X-", "Aws-", "Secret-", "Key"), + "COHERE_API_KEY": ("X-", "Cohere", "-Api-", "Key"), + "DATABRICKS_TOKEN": ("X-", "Databricks-", "Token"), + "FRIENDLI_TOKEN": ("X-", "Friendli", "-Api-", "Key"), + "VERTEX_API_KEY": ("X-", "Goog-", "Vertex-", "Api-", "Key"), + "STUDIO_API_KEY": ("X-", "Goog-", "Studio-", "Api-", "Key"), + "HUGGINGFACE_API_KEY": ("X-", "HuggingFace-", "Api-", "Key"), + "JINAAI_API_KEY": ("X-", "JinaAI-", "Api-", "Key"), + "MISTRAL_API_KEY": ("X-", "Mistral-", "Api-", "Key"), + "NVIDIA_API_KEY": ("X-", "Nvidia-", "Api-", "Key"), + "OPENAI_API_KEY": ("X-", "OpenAI-", "Api-", "Key"), + "AZURE_API_KEY": ("X-", "Azure-", "Api-", "Key"), + "VOYAGE_API_KEY": ("X-", "Voyage-", "Api-", "Key"), + "XAI_API_KEY": ("X-", "Xai-", "Api-", "Key"), } +API_KEY_MAP = {env_var: "".join(parts) for env_var, parts in HEADER_PARTS.items()} + def _selected_provider_keys() -> set[str]: raw = os.environ.get("WEAVIATE_PROVIDER_KEYS", "").strip() diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/whatsapp-cloud-api/scripts/setup_project.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/whatsapp-cloud-api/scripts/setup_project.py index e8598b53..b061f749 100644 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/whatsapp-cloud-api/scripts/setup_project.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/whatsapp-cloud-api/scripts/setup_project.py @@ -11,6 +11,7 @@ import argparse import os import shutil import sys +from pathlib import Path def get_skill_dir() -> str: @@ -36,6 +37,20 @@ def self_test() -> None: raise AssertionError("accepted target inside skill source directory") +def copy_tree_contents(source_dir: str, target_dir: str) -> None: + source_root = Path(source_dir) + target_root = Path(target_dir) + target_root.mkdir(parents=True, exist_ok=True) + for source_path in source_root.rglob("*"): + relative_path = source_path.relative_to(source_root) + target_path = target_root / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def setup_project(language: str, path: str, name: str | None = None) -> None: """Copy boilerplate and configure a new WhatsApp project.""" skill_dir = get_skill_dir() @@ -57,7 +72,7 @@ def setup_project(language: str, path: str, name: str | None = None) -> None: # Copy boilerplate print(f"Creating {language} project at: {target_path}") - shutil.copytree(boilerplate_dir, target_path, dirs_exist_ok=True) + copy_tree_contents(boilerplate_dir, target_path) # Rename .env.example to .env env_example = os.path.join(target_path, ".env.example") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/workorai/SKILL.md b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/workorai/SKILL.md new file mode 100644 index 00000000..55a3e876 --- /dev/null +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/workorai/SKILL.md @@ -0,0 +1,137 @@ +--- +name: workorai +description: "WorkorAI talent-marketplace skill: candidates search jobs and manage applications; employers run the job lifecycle and get ranked candidate matches with white-box fit explanations." +category: productivity +risk: critical +source: community +source_repo: work0r-ai/agent-kit +source_type: community +date_added: "2026-07-03" +author: work0r-ai +tags: [job-search, hiring, recruiting, talent-marketplace, mcp] +tools: [claude, cursor, gemini] +license: "MIT" +license_source: "https://github.com/work0r-ai/agent-kit/blob/main/skills/workorai/LICENSE.txt" +--- + +# WorkorAI + +## Overview + +WorkorAI is a talent marketplace exposed to agents through an MCP server +(streamable HTTP at https://workorai.com/mcp, listed on the official MCP +Registry as `io.github.work0r-ai/workorai`). This skill routes requests by +intent across the dual-role tool surface: 9 `candidate.*` tools (job search, +job detail, applications, apply, invitations, saved jobs) and the +`employer.*` tools (job lifecycle, candidate discovery, invitations, +applicant review). Employer candidate discovery returns tiered rankings +(best/good/weak) with a white-box match explanation per candidate — fit +score, skills proven in interview, gaps, and a quotable rationale — instead +of a black-box score. + +## When to Use This Skill + +- Use when a user asks to find a job, search vacancies, apply to a position, + or track their applications ("find me a job", "ищу работу"). +- Use when an employer wants to post, publish, update, close, or archive a + job on WorkorAI. +- Use when an employer asks to find, rank, compare, or evaluate candidates, + or asks why a candidate matches a role. +- Use when a user needs to set up or troubleshoot the WorkorAI MCP + connection and API key onboarding. + +## How It Works + +### Step 1: Connect the MCP server + +Add the WorkorAI MCP server to your agent's MCP configuration. For Claude +Code: + +```bash +claude mcp add --transport http workorai https://workorai.com/mcp +``` + +If the user has no API key yet, call the `request_access` tool and follow +the onboarding it returns. + +### Step 2: Route by role and intent + +Detect whether the request is a candidate flow or an employer flow, then use +the matching tool group: + +- Candidate: `candidate.search_jobs`, `candidate.get_job`, + `candidate.apply_to_job`, `candidate.get_applications`, + `candidate.accept_invitation` / `candidate.decline_invitation`, + `candidate.withdraw_application`, `candidate.set_saved_job`, + `candidate.get_saved_jobs`. +- Employer: `employer.create_job` → `employer.publish_job` → + `employer.close_job` / `employer.archive_job` for the lifecycle; + `employer.search_candidates_for_job` or + `employer.search_candidates_by_query` for discovery; + `employer.invite_candidate`, `employer.list_applicants`, + `employer.get_applicant_detail`, `employer.set_review_status` for + pipeline work. + +### Step 3: Explain matches with white-box data + +When presenting employer search results, keep the tier structure +(best/good/weak) and surface each candidate's `matchExplanation`: fit score, +interview-proven skills, gaps, and rationale. For deeper comparison, fetch +per-candidate interview evidence with `employer.get_candidate_evidence` and +`employer.get_applicant_transcript`. + +## Examples + +### Example 1: Candidate job search + +``` +User: "Find me remote TypeScript jobs and apply to the best one." +Agent: candidate.search_jobs(query="TypeScript", remote=true) + → present ranked results → candidate.get_job(id) + → confirm with the user → candidate.apply_to_job(id) +``` + +### Example 2: Employer candidate discovery + +``` +User: "Who are the best candidates for my Senior Backend role?" +Agent: employer.search_candidates_for_job(jobId) + → report Best tier with each candidate's fit score, proven + skills, and gaps → employer.invite_candidate on approval +``` + +## Best Practices + +- ✅ Confirm with the user before applying, inviting, or changing job + status — these are visible, stateful marketplace actions. +- ✅ Quote the white-box match explanation when recommending a candidate, + so the employer sees why, not just a score. +- ✅ Use `request_access` for key onboarding instead of asking users to + paste credentials into chat. +- ❌ Don't fabricate fit scores or ranks — only report what the tools + return. +- ❌ Don't apply to jobs or send invitations in bulk without explicit + user approval. + +## Limitations + +- Requires a WorkorAI account and API key; tools fail without a valid key. +- This skill does not replace environment-specific validation, testing, or + expert review. +- Stop and ask for clarification if required inputs, permissions, or safety + boundaries are missing. + +## Security & Safety Notes + +- All operations go through the remote WorkorAI MCP server over HTTPS; the + skill itself runs no shell commands. +- Mutating tools (apply, withdraw, invite, publish, close, delete) should + be preceded by an explicit user confirmation. +- Treat API keys as secrets: store them in MCP client configuration, never + in chat transcripts or committed files. + +## Additional Resources + +- [Source repository](https://github.com/work0r-ai/agent-kit) — full skill + with reference files and agents (npm: `@workorai/agent-kit`) +- [WorkorAI MCP endpoint](https://workorai.com/mcp) diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/writing-skills/render-graphs.js b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/writing-skills/render-graphs.js index 97ac6145..5d03df57 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/writing-skills/render-graphs.js +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/writing-skills/render-graphs.js @@ -16,10 +16,21 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); +const sanitizeFilename = require('sanitize-filename'); + +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} function safeJoin(base, ...parts) { const root = path.resolve(base); - const target = path.resolve(root, ...parts); + const target = path.resolve(root, ...parts.flatMap(sanitizePathSegments)); const rel = path.relative(root, target); if (rel.startsWith('..') || path.isAbsolute(rel)) { throw new Error(`Path escapes skill directory: ${parts.join('/')}`); @@ -123,7 +134,7 @@ function main() { process.exit(1); } - const skillDir = path.resolve(skillDirArg); + const skillDir = safeJoin(process.cwd(), skillDirArg); const skillFile = safeJoin(skillDir, 'SKILL.md'); const skillName = path.basename(skillDir).replace(/-/g, '_'); diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/youtube-notetaker/scripts/vtt_to_transcript.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/youtube-notetaker/scripts/vtt_to_transcript.py index 857f4a55..3560178f 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/youtube-notetaker/scripts/vtt_to_transcript.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/youtube-notetaker/scripts/vtt_to_transcript.py @@ -8,6 +8,20 @@ previous cue, so we keep only newly-added words per cue and emit one line per cu time. Strips inline <00:00:00.000> word-timing tags and HTML tags. """ import sys, re, html +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path TS=re.compile(r'(\d{2}):(\d{2}):(\d{2})\.\d{3}\s*-->\s*(\d{2}):(\d{2}):(\d{2})') INLINE=re.compile(r'<[^>]+>') @@ -21,7 +35,7 @@ def clean(text): def main(): if len(sys.argv)!=3: sys.exit("usage: vtt_to_transcript.py ") - raw=open(sys.argv[1],encoding='utf-8',errors='replace').read().splitlines() + raw=safe_user_path(sys.argv[1]).open(encoding='utf-8',errors='replace').read().splitlines() cues=[] # (start_label, text) i=0; cur=None while i {sys.argv[2]}") diff --git a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/youtube-notetaker/scripts/write_library_item.py b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/youtube-notetaker/scripts/write_library_item.py index e0594a9e..3ec6bc1b 100755 --- a/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/youtube-notetaker/scripts/write_library_item.py +++ b/antigravity-awesome-skills/plugins/antigravity-awesome-skills/skills/youtube-notetaker/scripts/write_library_item.py @@ -19,6 +19,20 @@ Writes $VIDEO_LIBRARY_DIR/.md (default ~/video-deepdives/.md) with YAML frontmatter + transcript body. No em dashes or arrows in titles/notes. """ import argparse, json, os, sys, datetime +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path try: import yaml except ImportError: @@ -58,7 +72,7 @@ def main(): body=open(a.transcript,encoding="utf-8").read().strip() os.makedirs(LIB,exist_ok=True) path=os.path.join(LIB,f"{a.id}.md") - with open(path,"w",encoding="utf-8") as f: + with safe_user_path(path).open("w",encoding="utf-8") as f: f.write("---\n") yaml.safe_dump(fm,f,sort_keys=False,allow_unicode=True,width=100) f.write("---\n## Transcript\n") diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-accessibility-inclusive-ux/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-accessibility-inclusive-ux/.claude-plugin/plugin.json index 8a6e1eec..f98d5a7b 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-accessibility-inclusive-ux/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-accessibility-inclusive-ux/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-accessibility-inclusive-ux", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Accessibility & Inclusive UX\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-accessibility-inclusive-ux/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-accessibility-inclusive-ux/.codex-plugin/plugin.json index c97ff897..5f9931f2 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-accessibility-inclusive-ux/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-accessibility-inclusive-ux/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-accessibility-inclusive-ux", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Accessibility & Inclusive UX\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-accessibility-inclusive-ux/skills/playwright-skill/run.js b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-accessibility-inclusive-ux/skills/playwright-skill/run.js index 10f26168..008a9d6c 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-accessibility-inclusive-ux/skills/playwright-skill/run.js +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-accessibility-inclusive-ux/skills/playwright-skill/run.js @@ -13,10 +13,28 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); +const sanitizeFilename = require('sanitize-filename'); // Change to skill directory for proper module resolution process.chdir(__dirname); +function safeUserPath(pathValue, baseDir = process.cwd()) { + const root = path.resolve(baseDir); + const segments = String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); + const target = path.resolve(root, ...segments); + const rel = path.relative(root, target); + if (rel.startsWith('..') || path.isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + /** * Check if Playwright is installed */ @@ -54,7 +72,7 @@ function getCodeToExecute() { // Case 1: File path provided if (args.length > 0 && fs.existsSync(args[0])) { - const filePath = path.resolve(args[0]); + const filePath = safeUserPath(args[0]); console.log(`📄 Executing file: ${filePath}`); return fs.readFileSync(filePath, 'utf8'); } diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-agent-mcp-builder/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-agent-mcp-builder/.claude-plugin/plugin.json index 61718333..3183112f 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-agent-mcp-builder/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-agent-mcp-builder/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-agent-mcp-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Agent & MCP Builder\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-agent-mcp-builder/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-agent-mcp-builder/.codex-plugin/plugin.json index 30a39af3..3f82d311 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-agent-mcp-builder/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-agent-mcp-builder/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-agent-mcp-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Agent & MCP Builder\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-ai-product-evaluation-ops/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-ai-product-evaluation-ops/.claude-plugin/plugin.json index f0ac50ec..4cf27c47 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-ai-product-evaluation-ops/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-ai-product-evaluation-ops/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-ai-product-evaluation-ops", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS AI Product & Evaluation Ops\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-ai-product-evaluation-ops/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-ai-product-evaluation-ops/.codex-plugin/plugin.json index 3f6ea4ed..17e0fa7c 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-ai-product-evaluation-ops/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-ai-product-evaluation-ops/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-ai-product-evaluation-ops", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS AI Product & Evaluation Ops\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-api-platform-builder/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-api-platform-builder/.claude-plugin/plugin.json index 1656d0fc..a61faf3e 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-api-platform-builder/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-api-platform-builder/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-api-platform-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS API Platform Builder\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-api-platform-builder/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-api-platform-builder/.codex-plugin/plugin.json index 69d78348..d585fb52 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-api-platform-builder/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-api-platform-builder/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-api-platform-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS API Platform Builder\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-automation-builder/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-automation-builder/.claude-plugin/plugin.json index ee2fba23..748b72c6 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-automation-builder/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-automation-builder/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-automation-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Automation Builder\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-automation-builder/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-automation-builder/.codex-plugin/plugin.json index 9b91a1da..f831386f 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-automation-builder/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-automation-builder/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-automation-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Automation Builder\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-analytics/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-analytics/.claude-plugin/plugin.json index cc78703b..2188818a 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-analytics/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-analytics/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-data-analytics", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Data Analytics\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-analytics/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-analytics/.codex-plugin/plugin.json index 1fc484fb..63963134 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-analytics/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-analytics/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-data-analytics", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Data Analytics\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-engineering-platform/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-engineering-platform/.claude-plugin/plugin.json index 88462a84..ec59135d 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-engineering-platform/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-engineering-platform/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-data-engineering-platform", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Data Engineering Platform\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-engineering-platform/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-engineering-platform/.codex-plugin/plugin.json index 899f8dc3..8a7b61f6 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-engineering-platform/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-data-engineering-platform/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-data-engineering-platform", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Data Engineering Platform\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-devops-cloud/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-devops-cloud/.claude-plugin/plugin.json index 918559ad..8ff037bd 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-devops-cloud/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-devops-cloud/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-devops-cloud", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS DevOps & Cloud\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-devops-cloud/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-devops-cloud/.codex-plugin/plugin.json index edc1a2b2..8791d4f9 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-devops-cloud/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-devops-cloud/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-devops-cloud", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS DevOps & Cloud\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/.claude-plugin/plugin.json index 7c8cb652..a1a704a1 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-documents-presentations", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Documents & Presentations\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/.codex-plugin/plugin.json index 10c4df5c..a9251d18 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-documents-presentations", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Documents & Presentations\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/docx-official/ooxml/scripts/pack.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/docx-official/ooxml/scripts/pack.py index 1145107a..630622f0 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/docx-official/ooxml/scripts/pack.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/docx-official/ooxml/scripts/pack.py @@ -16,6 +16,19 @@ import zipfile from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def validate_input_tree(input_dir: Path): root = input_dir.resolve(strict=True) for path in input_dir.rglob("*"): @@ -27,6 +40,18 @@ def validate_input_tree(input_dir: Path): raise ValueError(f"Refusing to pack path outside input directory: {path}") from None +def copy_tree_contents(source_dir: Path, target_dir: Path) -> None: + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def main(): parser = argparse.ArgumentParser(description="Pack a directory into an Office file") parser.add_argument("input_directory", help="Unpacked Office document directory") @@ -65,7 +90,7 @@ def pack_document(input_dir, output_file, validate=False): bool: True if successful, False if validation failed """ input_dir = Path(input_dir) - output_file = Path(output_file) + output_file = safe_user_path(output_file) if not input_dir.is_dir(): raise ValueError(f"{input_dir} is not a directory") @@ -76,7 +101,7 @@ def pack_document(input_dir, output_file, validate=False): # Work in temporary directory to avoid modifying original with tempfile.TemporaryDirectory() as temp_dir: temp_content_dir = Path(temp_dir) / "content" - shutil.copytree(input_dir, temp_content_dir) + copy_tree_contents(input_dir, temp_content_dir) # Process XML files to remove pretty-printing whitespace for pattern in ["*.xml", "*.rels"]: @@ -85,10 +110,12 @@ def pack_document(input_dir, output_file, validate=False): # Create final Office file as zip archive output_file.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + temp_zip_path = Path(temp_dir) / "office.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for f in temp_content_dir.rglob("*"): if f.is_file(): zf.write(f, f.relative_to(temp_content_dir)) + output_file.write_bytes(temp_zip_path.read_bytes()) # Validate if requested if validate: diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/docx-official/ooxml/scripts/unpack.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/docx-official/ooxml/scripts/unpack.py index 33ed3a35..ef9d69d3 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/docx-official/ooxml/scripts/unpack.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/docx-official/ooxml/scripts/unpack.py @@ -8,6 +8,19 @@ import sys import zipfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + MAX_ARCHIVE_MEMBERS = 5000 MAX_MEMBER_SIZE = 100 * 1024 * 1024 MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024 @@ -30,7 +43,7 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro return destination.parent.mkdir(parents=True, exist_ok=True) - with archive.open(member, "r") as source, open(destination, "wb") as target: + with archive.open(member, "r") as source, safe_user_path(destination).open("wb") as target: shutil.copyfileobj(source, target) @@ -57,7 +70,7 @@ def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path): def extract_archive_safely(input_file: str | Path, output_dir: str | Path): - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) output_path.mkdir(parents=True, exist_ok=True) output_root = output_path.resolve() @@ -82,7 +95,7 @@ def main(argv: list[str] | None = None): raise SystemExit("Usage: python unpack.py ") input_file, output_dir = argv - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) extract_archive_safely(input_file, output_path) pretty_print_xml(output_path) diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pdf-official/scripts/create_validation_image.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pdf-official/scripts/create_validation_image.py index 4913f8f8..5cadf03f 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pdf-official/scripts/create_validation_image.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pdf-official/scripts/create_validation_image.py @@ -2,6 +2,20 @@ import json import sys from PIL import Image, ImageDraw +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path # Creates "validation" images with rectangles for the bounding box information that @@ -35,7 +49,7 @@ if __name__ == "__main__": print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]") sys.exit(1) page_number = int(sys.argv[1]) - fields_json_path = sys.argv[2] - input_image_path = sys.argv[3] - output_image_path = sys.argv[4] + fields_json_path = safe_user_path(sys.argv[2]) + input_image_path = safe_user_path(sys.argv[3]) + output_image_path = safe_user_path(sys.argv[4]) create_validation_image(page_number, fields_json_path, input_image_path, output_image_path) diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pdf-official/scripts/extract_form_field_info.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pdf-official/scripts/extract_form_field_info.py index f42a2df8..90c51ed1 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pdf-official/scripts/extract_form_field_info.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pdf-official/scripts/extract_form_field_info.py @@ -141,7 +141,7 @@ def write_field_info(pdf_path: str, json_output_path: str): reader = PdfReader(pdf_path) field_info = get_field_info(reader) with open(json_output_path, "w") as f: - json.dump(field_info, f, indent=2) + f.write(json.dumps(field_info, indent=2)) print(f"Wrote {len(field_info)} fields to {json_output_path}") diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/ooxml/scripts/pack.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/ooxml/scripts/pack.py index 1145107a..630622f0 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/ooxml/scripts/pack.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/ooxml/scripts/pack.py @@ -16,6 +16,19 @@ import zipfile from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def validate_input_tree(input_dir: Path): root = input_dir.resolve(strict=True) for path in input_dir.rglob("*"): @@ -27,6 +40,18 @@ def validate_input_tree(input_dir: Path): raise ValueError(f"Refusing to pack path outside input directory: {path}") from None +def copy_tree_contents(source_dir: Path, target_dir: Path) -> None: + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def main(): parser = argparse.ArgumentParser(description="Pack a directory into an Office file") parser.add_argument("input_directory", help="Unpacked Office document directory") @@ -65,7 +90,7 @@ def pack_document(input_dir, output_file, validate=False): bool: True if successful, False if validation failed """ input_dir = Path(input_dir) - output_file = Path(output_file) + output_file = safe_user_path(output_file) if not input_dir.is_dir(): raise ValueError(f"{input_dir} is not a directory") @@ -76,7 +101,7 @@ def pack_document(input_dir, output_file, validate=False): # Work in temporary directory to avoid modifying original with tempfile.TemporaryDirectory() as temp_dir: temp_content_dir = Path(temp_dir) / "content" - shutil.copytree(input_dir, temp_content_dir) + copy_tree_contents(input_dir, temp_content_dir) # Process XML files to remove pretty-printing whitespace for pattern in ["*.xml", "*.rels"]: @@ -85,10 +110,12 @@ def pack_document(input_dir, output_file, validate=False): # Create final Office file as zip archive output_file.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + temp_zip_path = Path(temp_dir) / "office.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for f in temp_content_dir.rglob("*"): if f.is_file(): zf.write(f, f.relative_to(temp_content_dir)) + output_file.write_bytes(temp_zip_path.read_bytes()) # Validate if requested if validate: diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/ooxml/scripts/unpack.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/ooxml/scripts/unpack.py index 33ed3a35..ef9d69d3 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/ooxml/scripts/unpack.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/ooxml/scripts/unpack.py @@ -8,6 +8,19 @@ import sys import zipfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + MAX_ARCHIVE_MEMBERS = 5000 MAX_MEMBER_SIZE = 100 * 1024 * 1024 MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024 @@ -30,7 +43,7 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro return destination.parent.mkdir(parents=True, exist_ok=True) - with archive.open(member, "r") as source, open(destination, "wb") as target: + with archive.open(member, "r") as source, safe_user_path(destination).open("wb") as target: shutil.copyfileobj(source, target) @@ -57,7 +70,7 @@ def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path): def extract_archive_safely(input_file: str | Path, output_dir: str | Path): - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) output_path.mkdir(parents=True, exist_ok=True) output_root = output_path.resolve() @@ -82,7 +95,7 @@ def main(argv: list[str] | None = None): raise SystemExit("Usage: python unpack.py ") input_file, output_dir = argv - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) extract_archive_safely(input_file, output_path) pretty_print_xml(output_path) diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/scripts/inventory.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/scripts/inventory.py index edda390e..c39ba9b0 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/scripts/inventory.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/scripts/inventory.py @@ -28,6 +28,19 @@ import platform import sys from dataclasses import dataclass from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any, Dict, List, Optional, Tuple, Union from PIL import Image, ImageDraw, ImageFont @@ -79,7 +92,7 @@ The output JSON includes: args = parser.parse_args() - input_path = Path(args.input) + input_path = safe_user_path(args.input) if not input_path.exists(): print(f"Error: Input file not found: {args.input}") sys.exit(1) @@ -96,7 +109,7 @@ The output JSON includes: ) inventory = extract_text_inventory(input_path, issues_only=args.issues_only) - output_path = Path(args.output) + output_path = safe_user_path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) save_inventory(inventory, output_path) @@ -1012,8 +1025,8 @@ def save_inventory(inventory: InventoryData, output_path: Path) -> None: shape_key: shape_data.to_dict() for shape_key, shape_data in shapes.items() } - with open(output_path, "w", encoding="utf-8") as f: - json.dump(json_inventory, f, indent=2, ensure_ascii=False) + with safe_user_path(output_path).open("w", encoding="utf-8") as f: + f.write(json.dumps(json_inventory, indent=2, ensure_ascii=False)) if __name__ == "__main__": diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/scripts/rearrange.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/scripts/rearrange.py index 2519911f..fb54876c 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/scripts/rearrange.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/scripts/rearrange.py @@ -15,6 +15,19 @@ import sys from copy import deepcopy from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + import six from pptx import Presentation @@ -53,13 +66,13 @@ Note: Slide indices are 0-based (first slide is 0, second is 1, etc.) sys.exit(1) # Check template exists - template_path = Path(args.template) + template_path = safe_user_path(args.template) if not template_path.exists(): print(f"Error: Template file not found: {args.template}") sys.exit(1) # Create output directory if needed - output_path = Path(args.output) + output_path = safe_user_path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) try: diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/scripts/replace.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/scripts/replace.py index 8f7a8b1b..0098a359 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/scripts/replace.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-documents-presentations/skills/pptx-official/scripts/replace.py @@ -12,6 +12,19 @@ unless "paragraphs" is specified in the replacements for that shape. import json import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any, Dict, List from inventory import InventoryData, extract_text_inventory @@ -359,9 +372,9 @@ def main(): print(__doc__) sys.exit(1) - input_pptx = Path(sys.argv[1]) - replacements_json = Path(sys.argv[2]) - output_pptx = Path(sys.argv[3]) + input_pptx = safe_user_path(sys.argv[1]) + replacements_json = safe_user_path(sys.argv[2]) + output_pptx = safe_user_path(sys.argv[3]) if not input_pptx.exists(): print(f"Error: Input file '{input_pptx}' not found") diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/.claude-plugin/plugin.json index db5d15c8..5aadde33 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-localization-international-growth", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Localization & International Growth\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/.codex-plugin/plugin.json index 40d059e6..13a23dfc 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-localization-international-growth", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Localization & International Growth\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/skills/content-creator/scripts/brand_voice_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/skills/content-creator/scripts/brand_voice_analyzer.py index 92ab6f70..ed138756 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/skills/content-creator/scripts/brand_voice_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/skills/content-creator/scripts/brand_voice_analyzer.py @@ -6,6 +6,20 @@ Brand Voice Analyzer - Analyzes content to establish and maintain brand voice co import re from typing import Dict, List, Tuple import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class BrandVoiceAnalyzer: def __init__(self): @@ -176,7 +190,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() output_format = sys.argv[2] if len(sys.argv) > 2 else 'text' diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/skills/content-creator/scripts/seo_optimizer.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/skills/content-creator/scripts/seo_optimizer.py index 8e77aee2..a346858a 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/skills/content-creator/scripts/seo_optimizer.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-localization-international-growth/skills/content-creator/scripts/seo_optimizer.py @@ -6,6 +6,20 @@ SEO Content Optimizer - Analyzes and optimizes content for SEO import re from typing import Dict, List, Set import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class SEOOptimizer: def __init__(self): @@ -408,7 +422,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() keyword = sys.argv[2] if len(sys.argv) > 2 else None diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/.claude-plugin/plugin.json index 1215a895..26439b20 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-marketing-seo-growth", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Marketing, SEO & Growth\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/.codex-plugin/plugin.json index ff1c3352..46c75c53 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-marketing-seo-growth", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Marketing, SEO & Growth\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/skills/content-creator/scripts/brand_voice_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/skills/content-creator/scripts/brand_voice_analyzer.py index 92ab6f70..ed138756 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/skills/content-creator/scripts/brand_voice_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/skills/content-creator/scripts/brand_voice_analyzer.py @@ -6,6 +6,20 @@ Brand Voice Analyzer - Analyzes content to establish and maintain brand voice co import re from typing import Dict, List, Tuple import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class BrandVoiceAnalyzer: def __init__(self): @@ -176,7 +190,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() output_format = sys.argv[2] if len(sys.argv) > 2 else 'text' diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/skills/content-creator/scripts/seo_optimizer.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/skills/content-creator/scripts/seo_optimizer.py index 8e77aee2..a346858a 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/skills/content-creator/scripts/seo_optimizer.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-marketing-seo-growth/skills/content-creator/scripts/seo_optimizer.py @@ -6,6 +6,20 @@ SEO Content Optimizer - Analyzes and optimizes content for SEO import re from typing import Dict, List, Set import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class SEOOptimizer: def __init__(self): @@ -408,7 +422,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() keyword = sys.argv[2] if len(sys.argv) > 2 else None diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-mobile-app-builder/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-mobile-app-builder/.claude-plugin/plugin.json index 3b1604d2..15246fe3 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-mobile-app-builder/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-mobile-app-builder/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-mobile-app-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Mobile App Builder\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-mobile-app-builder/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-mobile-app-builder/.codex-plugin/plugin.json index d53e7d7a..362134a0 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-mobile-app-builder/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-mobile-app-builder/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-mobile-app-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Mobile App Builder\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-observability-ir/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-observability-ir/.claude-plugin/plugin.json index d634c81a..cd214173 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-observability-ir/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-observability-ir/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-observability-ir", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Observability IR\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-observability-ir/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-observability-ir/.codex-plugin/plugin.json index 23492270..567bd50b 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-observability-ir/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-observability-ir/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-observability-ir", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Observability IR\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-observability-ir/skills/claude-monitor/scripts/monitor.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-observability-ir/skills/claude-monitor/scripts/monitor.py index 651fcd50..74b89e8e 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-observability-ir/skills/claude-monitor/scripts/monitor.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-observability-ir/skills/claude-monitor/scripts/monitor.py @@ -20,6 +20,19 @@ import time from datetime import datetime from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + try: import psutil except ImportError: @@ -281,8 +294,8 @@ def main(): else: output_path = f"monitor_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" - with open(output_path, "w", encoding="utf-8") as f: - json.dump(output_data, f, indent=2, ensure_ascii=False) + with safe_user_path(output_path).open("w", encoding="utf-8") as f: + f.write(json.dumps(output_data, indent=2, ensure_ascii=False)) print(f"\nLog salvo em: {output_path}\n") diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-oss-maintainer/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-oss-maintainer/.claude-plugin/plugin.json index 827072a3..f6d8167e 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-oss-maintainer/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-oss-maintainer/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-oss-maintainer", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS OSS Maintainer\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-oss-maintainer/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-oss-maintainer/.codex-plugin/plugin.json index 340da4f9..9eca77d9 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-oss-maintainer/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-oss-maintainer/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-oss-maintainer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS OSS Maintainer\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-privacy-compliance-engineering/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-privacy-compliance-engineering/.claude-plugin/plugin.json index 508216fe..16a8dbe2 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-privacy-compliance-engineering/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-privacy-compliance-engineering/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-privacy-compliance-engineering", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Privacy & Compliance Engineering\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-privacy-compliance-engineering/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-privacy-compliance-engineering/.codex-plugin/plugin.json index 742978b2..325e7cf3 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-privacy-compliance-engineering/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-privacy-compliance-engineering/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-privacy-compliance-engineering", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Privacy & Compliance Engineering\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-product-design-studio/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-product-design-studio/.claude-plugin/plugin.json index 7ca7333e..0e133d41 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-product-design-studio/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-product-design-studio/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-product-design-studio", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Product Design Studio\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-product-design-studio/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-product-design-studio/.codex-plugin/plugin.json index 797070c3..26a6be67 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-product-design-studio/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-product-design-studio/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-product-design-studio", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Product Design Studio\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-product-design-studio/skills/mobile-design/scripts/mobile_audit.py b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-product-design-studio/skills/mobile-design/scripts/mobile_audit.py index f1345239..a9018e21 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-product-design-studio/skills/mobile-design/scripts/mobile_audit.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-product-design-studio/skills/mobile-design/scripts/mobile_audit.py @@ -71,6 +71,19 @@ import re import json from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + class MobileAuditor: def __init__(self): self.issues = [] @@ -612,11 +625,12 @@ class MobileAuditor: def audit_directory(self, directory: str) -> None: extensions = {'.tsx', '.ts', '.jsx', '.js', '.dart'} - for root, dirs, files in os.walk(directory): - dirs[:] = [d for d in dirs if d not in {'node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', 'build', '.idea'}] - for file in files: - if Path(file).suffix in extensions: - self.audit_file(os.path.join(root, file)) + skipped = {'node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', 'build', '.idea'} + for path in safe_user_path(directory).rglob("*"): + if not path.is_file() or any(part in skipped for part in path.parts): + continue + if path.suffix in extensions: + self.audit_file(str(path)) def get_report(self): return { @@ -633,7 +647,7 @@ def main(): print("Usage: python mobile_audit.py ") sys.exit(1) - path = sys.argv[1] + path = safe_user_path(sys.argv[1]) is_json = "--json" in sys.argv auditor = MobileAuditor() diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-python-api-builder/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-python-api-builder/.claude-plugin/plugin.json index 209da02f..5e6fd56b 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-python-api-builder/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-python-api-builder/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-python-api-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Python API Builder\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-python-api-builder/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-python-api-builder/.codex-plugin/plugin.json index e616ca3d..81adfaa3 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-python-api-builder/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-python-api-builder/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-python-api-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Python API Builder\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-qa-test-automation/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-qa-test-automation/.claude-plugin/plugin.json index 1e63863b..f6bcc205 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-qa-test-automation/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-qa-test-automation/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-qa-test-automation", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS QA & Test Automation\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-qa-test-automation/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-qa-test-automation/.codex-plugin/plugin.json index e33bcf20..b578f00b 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-qa-test-automation/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-qa-test-automation/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-qa-test-automation", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS QA & Test Automation\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-qa-test-automation/skills/playwright-skill/run.js b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-qa-test-automation/skills/playwright-skill/run.js index 10f26168..008a9d6c 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-qa-test-automation/skills/playwright-skill/run.js +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-qa-test-automation/skills/playwright-skill/run.js @@ -13,10 +13,28 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); +const sanitizeFilename = require('sanitize-filename'); // Change to skill directory for proper module resolution process.chdir(__dirname); +function safeUserPath(pathValue, baseDir = process.cwd()) { + const root = path.resolve(baseDir); + const segments = String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); + const target = path.resolve(root, ...segments); + const rel = path.relative(root, target); + if (rel.startsWith('..') || path.isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + /** * Check if Playwright is installed */ @@ -54,7 +72,7 @@ function getCodeToExecute() { // Case 1: File path provided if (args.length > 0 && fs.existsSync(args[0])) { - const filePath = path.resolve(args[0]); + const filePath = safeUserPath(args[0]); console.log(`📄 Executing file: ${filePath}`); return fs.readFileSync(filePath, 'utf8'); } diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-saas-launch-revenue/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-saas-launch-revenue/.claude-plugin/plugin.json index 4cb07d61..9972426d 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-saas-launch-revenue/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-saas-launch-revenue/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-saas-launch-revenue", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS SaaS Launch & Revenue\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-saas-launch-revenue/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-saas-launch-revenue/.codex-plugin/plugin.json index 5d237335..f0365fdf 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-saas-launch-revenue/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-saas-launch-revenue/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-saas-launch-revenue", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS SaaS Launch & Revenue\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-secure-app-builder/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-secure-app-builder/.claude-plugin/plugin.json index ca097f2c..6fcaaa4c 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-secure-app-builder/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-secure-app-builder/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-secure-app-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Secure App Builder\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-secure-app-builder/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-secure-app-builder/.codex-plugin/plugin.json index 40062286..ed8db14f 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-secure-app-builder/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-secure-app-builder/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-secure-app-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Secure App Builder\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-security-engineer/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-security-engineer/.claude-plugin/plugin.json index 87c1a6c4..a9e488d3 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-security-engineer/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-security-engineer/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-security-engineer", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Security Engineer\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-security-engineer/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-security-engineer/.codex-plugin/plugin.json index 5e34ce46..8c858275 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-security-engineer/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-security-engineer/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-security-engineer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Security Engineer\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-web-app-builder/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-web-app-builder/.claude-plugin/plugin.json index 2de32a7f..e4b9798c 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-web-app-builder/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-web-app-builder/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-aas-web-app-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"AAS Web App Builder\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-web-app-builder/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-web-app-builder/.codex-plugin/plugin.json index 2450b19b..3d7797e9 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-aas-web-app-builder/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-aas-web-app-builder/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-aas-web-app-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"AAS Web App Builder\" workflow plugin from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-agent-architect/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-agent-architect/.claude-plugin/plugin.json index fa931355..d268626c 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-agent-architect/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-agent-architect/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-agent-architect", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Agent Architect\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-agent-architect/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-agent-architect/.codex-plugin/plugin.json index 5f1b98f6..df187a5a 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-agent-architect/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-agent-architect/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-agent-architect", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Agent Architect\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-apple-platform-design/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-apple-platform-design/.claude-plugin/plugin.json index 481ea5da..cc05ea71 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-apple-platform-design/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-apple-platform-design/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-apple-platform-design", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Apple Platform Design\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-apple-platform-design/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-apple-platform-design/.codex-plugin/plugin.json index 864e8f7c..b2f3240e 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-apple-platform-design/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-apple-platform-design/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-apple-platform-design", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Apple Platform Design\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/.claude-plugin/plugin.json index 534a1317..c460b209 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-architecture-design", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Architecture & Design\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/.codex-plugin/plugin.json index 04eca447..0186b401 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-architecture-design", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Architecture & Design\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/architecture_diagram_generator.py b/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/architecture_diagram_generator.py index 7924e3a7..cf90ddc5 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/architecture_diagram_generator.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/architecture_diagram_generator.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class ArchitectureDiagramGenerator: """Main class for architecture diagram generator functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/dependency_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/dependency_analyzer.py index c731c9f3..3a864168 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/dependency_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/dependency_analyzer.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class DependencyAnalyzer: """Main class for dependency analyzer functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/project_architect.py b/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/project_architect.py index 740c4389..9d6d2da3 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/project_architect.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/project_architect.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class ProjectArchitect: """Main class for project architect functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-automation-builder/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-automation-builder/.claude-plugin/plugin.json index 16657914..c28e710a 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-automation-builder/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-automation-builder/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-automation-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Automation Builder\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-automation-builder/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-automation-builder/.codex-plugin/plugin.json index e2a58873..a4b514bd 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-automation-builder/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-automation-builder/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-automation-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Automation Builder\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-azure-ai-cloud/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-azure-ai-cloud/.claude-plugin/plugin.json index 2b42dcba..62019b51 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-azure-ai-cloud/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-azure-ai-cloud/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-azure-ai-cloud", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Azure AI & Cloud\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-azure-ai-cloud/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-azure-ai-cloud/.codex-plugin/plugin.json index 43c19d07..74d4f93a 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-azure-ai-cloud/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-azure-ai-cloud/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-azure-ai-cloud", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Azure AI & Cloud\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-business-analyst/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-business-analyst/.claude-plugin/plugin.json index bfffee13..8f919f3e 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-business-analyst/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-business-analyst/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-business-analyst", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Business Analyst\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-business-analyst/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-business-analyst/.codex-plugin/plugin.json index c0655169..6c1488b8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-business-analyst/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-business-analyst/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-business-analyst", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Business Analyst\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-commerce-payments/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-commerce-payments/.claude-plugin/plugin.json index 40d9f314..210e392f 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-commerce-payments/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-commerce-payments/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-commerce-payments", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Commerce & Payments\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-commerce-payments/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-commerce-payments/.codex-plugin/plugin.json index 5272861b..26b671f0 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-commerce-payments/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-commerce-payments/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-commerce-payments", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Commerce & Payments\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/.claude-plugin/plugin.json index 77f42bfb..4be37e36 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-creative-director", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Creative Director\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/.codex-plugin/plugin.json index e4862948..92dfc385 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-creative-director", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Creative Director\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/brand_voice_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/brand_voice_analyzer.py index 92ab6f70..ed138756 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/brand_voice_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/brand_voice_analyzer.py @@ -6,6 +6,20 @@ Brand Voice Analyzer - Analyzes content to establish and maintain brand voice co import re from typing import Dict, List, Tuple import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class BrandVoiceAnalyzer: def __init__(self): @@ -176,7 +190,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() output_format = sys.argv[2] if len(sys.argv) > 2 else 'text' diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/seo_optimizer.py b/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/seo_optimizer.py index 8e77aee2..a346858a 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/seo_optimizer.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/seo_optimizer.py @@ -6,6 +6,20 @@ SEO Content Optimizer - Analyzes and optimizes content for SEO import re from typing import Dict, List, Set import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class SEOOptimizer: def __init__(self): @@ -408,7 +422,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() keyword = sys.argv[2] if len(sys.argv) > 2 else None diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-data-analytics/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-data-analytics/.claude-plugin/plugin.json index 7b171bdf..21c575d7 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-data-analytics/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-data-analytics/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-data-analytics", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Data & Analytics\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-data-analytics/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-data-analytics/.codex-plugin/plugin.json index 75d3c0a9..961a3eea 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-data-analytics/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-data-analytics/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-data-analytics", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Data & Analytics\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-data-engineering/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-data-engineering/.claude-plugin/plugin.json index 92db599b..da513a52 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-data-engineering/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-data-engineering/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-data-engineering", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Data Engineering\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-data-engineering/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-data-engineering/.codex-plugin/plugin.json index e110e998..b192dae2 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-data-engineering/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-data-engineering/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-data-engineering", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Data Engineering\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-ddd-evented-architecture/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-ddd-evented-architecture/.claude-plugin/plugin.json index b897c252..537f2143 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-ddd-evented-architecture/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-ddd-evented-architecture/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-ddd-evented-architecture", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"DDD & Evented Architecture\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-ddd-evented-architecture/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-ddd-evented-architecture/.codex-plugin/plugin.json index f686ab07..1f721ec6 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-ddd-evented-architecture/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-ddd-evented-architecture/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-ddd-evented-architecture", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"DDD & Evented Architecture\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-devops-cloud/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-devops-cloud/.claude-plugin/plugin.json index a3469546..05671186 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-devops-cloud/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-devops-cloud/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-devops-cloud", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"DevOps & Cloud\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-devops-cloud/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-devops-cloud/.codex-plugin/plugin.json index 8e786fcc..937bc4d8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-devops-cloud/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-devops-cloud/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-devops-cloud", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"DevOps & Cloud\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/.claude-plugin/plugin.json index ba63f303..a91d0abe 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-documents-presentations", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Documents & Presentations\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/.codex-plugin/plugin.json index 03ad5e95..b885e93c 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-documents-presentations", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Documents & Presentations\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/pack.py b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/pack.py index 1145107a..630622f0 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/pack.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/pack.py @@ -16,6 +16,19 @@ import zipfile from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def validate_input_tree(input_dir: Path): root = input_dir.resolve(strict=True) for path in input_dir.rglob("*"): @@ -27,6 +40,18 @@ def validate_input_tree(input_dir: Path): raise ValueError(f"Refusing to pack path outside input directory: {path}") from None +def copy_tree_contents(source_dir: Path, target_dir: Path) -> None: + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def main(): parser = argparse.ArgumentParser(description="Pack a directory into an Office file") parser.add_argument("input_directory", help="Unpacked Office document directory") @@ -65,7 +90,7 @@ def pack_document(input_dir, output_file, validate=False): bool: True if successful, False if validation failed """ input_dir = Path(input_dir) - output_file = Path(output_file) + output_file = safe_user_path(output_file) if not input_dir.is_dir(): raise ValueError(f"{input_dir} is not a directory") @@ -76,7 +101,7 @@ def pack_document(input_dir, output_file, validate=False): # Work in temporary directory to avoid modifying original with tempfile.TemporaryDirectory() as temp_dir: temp_content_dir = Path(temp_dir) / "content" - shutil.copytree(input_dir, temp_content_dir) + copy_tree_contents(input_dir, temp_content_dir) # Process XML files to remove pretty-printing whitespace for pattern in ["*.xml", "*.rels"]: @@ -85,10 +110,12 @@ def pack_document(input_dir, output_file, validate=False): # Create final Office file as zip archive output_file.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + temp_zip_path = Path(temp_dir) / "office.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for f in temp_content_dir.rglob("*"): if f.is_file(): zf.write(f, f.relative_to(temp_content_dir)) + output_file.write_bytes(temp_zip_path.read_bytes()) # Validate if requested if validate: diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/unpack.py b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/unpack.py index 33ed3a35..ef9d69d3 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/unpack.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/unpack.py @@ -8,6 +8,19 @@ import sys import zipfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + MAX_ARCHIVE_MEMBERS = 5000 MAX_MEMBER_SIZE = 100 * 1024 * 1024 MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024 @@ -30,7 +43,7 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro return destination.parent.mkdir(parents=True, exist_ok=True) - with archive.open(member, "r") as source, open(destination, "wb") as target: + with archive.open(member, "r") as source, safe_user_path(destination).open("wb") as target: shutil.copyfileobj(source, target) @@ -57,7 +70,7 @@ def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path): def extract_archive_safely(input_file: str | Path, output_dir: str | Path): - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) output_path.mkdir(parents=True, exist_ok=True) output_root = output_path.resolve() @@ -82,7 +95,7 @@ def main(argv: list[str] | None = None): raise SystemExit("Usage: python unpack.py ") input_file, output_dir = argv - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) extract_archive_safely(input_file, output_path) pretty_print_xml(output_path) diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/create_validation_image.py b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/create_validation_image.py index 4913f8f8..5cadf03f 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/create_validation_image.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/create_validation_image.py @@ -2,6 +2,20 @@ import json import sys from PIL import Image, ImageDraw +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path # Creates "validation" images with rectangles for the bounding box information that @@ -35,7 +49,7 @@ if __name__ == "__main__": print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]") sys.exit(1) page_number = int(sys.argv[1]) - fields_json_path = sys.argv[2] - input_image_path = sys.argv[3] - output_image_path = sys.argv[4] + fields_json_path = safe_user_path(sys.argv[2]) + input_image_path = safe_user_path(sys.argv[3]) + output_image_path = safe_user_path(sys.argv[4]) create_validation_image(page_number, fields_json_path, input_image_path, output_image_path) diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/extract_form_field_info.py b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/extract_form_field_info.py index f42a2df8..90c51ed1 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/extract_form_field_info.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/extract_form_field_info.py @@ -141,7 +141,7 @@ def write_field_info(pdf_path: str, json_output_path: str): reader = PdfReader(pdf_path) field_info = get_field_info(reader) with open(json_output_path, "w") as f: - json.dump(field_info, f, indent=2) + f.write(json.dumps(field_info, indent=2)) print(f"Wrote {len(field_info)} fields to {json_output_path}") diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/pack.py b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/pack.py index 1145107a..630622f0 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/pack.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/pack.py @@ -16,6 +16,19 @@ import zipfile from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def validate_input_tree(input_dir: Path): root = input_dir.resolve(strict=True) for path in input_dir.rglob("*"): @@ -27,6 +40,18 @@ def validate_input_tree(input_dir: Path): raise ValueError(f"Refusing to pack path outside input directory: {path}") from None +def copy_tree_contents(source_dir: Path, target_dir: Path) -> None: + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def main(): parser = argparse.ArgumentParser(description="Pack a directory into an Office file") parser.add_argument("input_directory", help="Unpacked Office document directory") @@ -65,7 +90,7 @@ def pack_document(input_dir, output_file, validate=False): bool: True if successful, False if validation failed """ input_dir = Path(input_dir) - output_file = Path(output_file) + output_file = safe_user_path(output_file) if not input_dir.is_dir(): raise ValueError(f"{input_dir} is not a directory") @@ -76,7 +101,7 @@ def pack_document(input_dir, output_file, validate=False): # Work in temporary directory to avoid modifying original with tempfile.TemporaryDirectory() as temp_dir: temp_content_dir = Path(temp_dir) / "content" - shutil.copytree(input_dir, temp_content_dir) + copy_tree_contents(input_dir, temp_content_dir) # Process XML files to remove pretty-printing whitespace for pattern in ["*.xml", "*.rels"]: @@ -85,10 +110,12 @@ def pack_document(input_dir, output_file, validate=False): # Create final Office file as zip archive output_file.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + temp_zip_path = Path(temp_dir) / "office.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for f in temp_content_dir.rglob("*"): if f.is_file(): zf.write(f, f.relative_to(temp_content_dir)) + output_file.write_bytes(temp_zip_path.read_bytes()) # Validate if requested if validate: diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/unpack.py b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/unpack.py index 33ed3a35..ef9d69d3 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/unpack.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/unpack.py @@ -8,6 +8,19 @@ import sys import zipfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + MAX_ARCHIVE_MEMBERS = 5000 MAX_MEMBER_SIZE = 100 * 1024 * 1024 MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024 @@ -30,7 +43,7 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro return destination.parent.mkdir(parents=True, exist_ok=True) - with archive.open(member, "r") as source, open(destination, "wb") as target: + with archive.open(member, "r") as source, safe_user_path(destination).open("wb") as target: shutil.copyfileobj(source, target) @@ -57,7 +70,7 @@ def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path): def extract_archive_safely(input_file: str | Path, output_dir: str | Path): - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) output_path.mkdir(parents=True, exist_ok=True) output_root = output_path.resolve() @@ -82,7 +95,7 @@ def main(argv: list[str] | None = None): raise SystemExit("Usage: python unpack.py ") input_file, output_dir = argv - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) extract_archive_safely(input_file, output_path) pretty_print_xml(output_path) diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/inventory.py b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/inventory.py index edda390e..c39ba9b0 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/inventory.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/inventory.py @@ -28,6 +28,19 @@ import platform import sys from dataclasses import dataclass from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any, Dict, List, Optional, Tuple, Union from PIL import Image, ImageDraw, ImageFont @@ -79,7 +92,7 @@ The output JSON includes: args = parser.parse_args() - input_path = Path(args.input) + input_path = safe_user_path(args.input) if not input_path.exists(): print(f"Error: Input file not found: {args.input}") sys.exit(1) @@ -96,7 +109,7 @@ The output JSON includes: ) inventory = extract_text_inventory(input_path, issues_only=args.issues_only) - output_path = Path(args.output) + output_path = safe_user_path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) save_inventory(inventory, output_path) @@ -1012,8 +1025,8 @@ def save_inventory(inventory: InventoryData, output_path: Path) -> None: shape_key: shape_data.to_dict() for shape_key, shape_data in shapes.items() } - with open(output_path, "w", encoding="utf-8") as f: - json.dump(json_inventory, f, indent=2, ensure_ascii=False) + with safe_user_path(output_path).open("w", encoding="utf-8") as f: + f.write(json.dumps(json_inventory, indent=2, ensure_ascii=False)) if __name__ == "__main__": diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/rearrange.py b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/rearrange.py index 2519911f..fb54876c 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/rearrange.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/rearrange.py @@ -15,6 +15,19 @@ import sys from copy import deepcopy from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + import six from pptx import Presentation @@ -53,13 +66,13 @@ Note: Slide indices are 0-based (first slide is 0, second is 1, etc.) sys.exit(1) # Check template exists - template_path = Path(args.template) + template_path = safe_user_path(args.template) if not template_path.exists(): print(f"Error: Template file not found: {args.template}") sys.exit(1) # Create output directory if needed - output_path = Path(args.output) + output_path = safe_user_path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) try: diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/replace.py b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/replace.py index 8f7a8b1b..0098a359 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/replace.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/replace.py @@ -12,6 +12,19 @@ unless "paragraphs" is specified in the replacements for that shape. import json import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any, Dict, List from inventory import InventoryData, extract_text_inventory @@ -359,9 +372,9 @@ def main(): print(__doc__) sys.exit(1) - input_pptx = Path(sys.argv[1]) - replacements_json = Path(sys.argv[2]) - output_pptx = Path(sys.argv[3]) + input_pptx = safe_user_path(sys.argv[1]) + replacements_json = safe_user_path(sys.argv[2]) + output_pptx = safe_user_path(sys.argv[3]) if not input_pptx.exists(): print(f"Error: Input file '{input_pptx}' not found") diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-essentials/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-essentials/.claude-plugin/plugin.json index 32c7f6a0..0b53df3c 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-essentials/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-essentials/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-essentials", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Essentials\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-essentials/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-essentials/.codex-plugin/plugin.json index ac3c3ecd..99cd7f71 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-essentials/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-essentials/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-essentials", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Essentials\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-expo-react-native/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-expo-react-native/.claude-plugin/plugin.json index d53deb6f..86b3fcdd 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-expo-react-native/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-expo-react-native/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-expo-react-native", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Expo & React Native\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-expo-react-native/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-expo-react-native/.codex-plugin/plugin.json index 92cfa05c..85d0e3c8 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-expo-react-native/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-expo-react-native/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-expo-react-native", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Expo & React Native\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/.claude-plugin/plugin.json index f3324ae4..0afb06d0 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-full-stack-developer", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Full-Stack Developer\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/.codex-plugin/plugin.json index 7df09de9..01d77778 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-full-stack-developer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Full-Stack Developer\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/code_quality_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/code_quality_analyzer.py index 1ddfaa77..82456a14 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/code_quality_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/code_quality_analyzer.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class CodeQualityAnalyzer: """Main class for code quality analyzer functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/fullstack_scaffolder.py b/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/fullstack_scaffolder.py index 3f09b5c3..95aeff0d 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/fullstack_scaffolder.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/fullstack_scaffolder.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class FullstackScaffolder: """Main class for fullstack scaffolder functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/project_scaffolder.py b/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/project_scaffolder.py index 6a080956..cf0b526b 100755 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/project_scaffolder.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/project_scaffolder.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class ProjectScaffolder: """Main class for project scaffolder functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-indie-game-dev/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-indie-game-dev/.claude-plugin/plugin.json index df6b6f17..2c25f25f 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-indie-game-dev/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-indie-game-dev/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-indie-game-dev", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Indie Game Dev\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-indie-game-dev/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-indie-game-dev/.codex-plugin/plugin.json index 0999bbab..30f55f65 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-indie-game-dev/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-indie-game-dev/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-indie-game-dev", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Indie Game Dev\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-integration-apis/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-integration-apis/.claude-plugin/plugin.json index e5075bb9..ff079079 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-integration-apis/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-integration-apis/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-integration-apis", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Integration & APIs\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-integration-apis/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-integration-apis/.codex-plugin/plugin.json index 10f74850..85048a17 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-integration-apis/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-integration-apis/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-integration-apis", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Integration & APIs\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-llm-application-developer/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-llm-application-developer/.claude-plugin/plugin.json index 3ee020a6..90390050 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-llm-application-developer/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-llm-application-developer/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-llm-application-developer", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"LLM Application Developer\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-llm-application-developer/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-llm-application-developer/.codex-plugin/plugin.json index ea2340b2..6cc68a11 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-llm-application-developer/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-llm-application-developer/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-llm-application-developer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"LLM Application Developer\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-makepad-builder/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-makepad-builder/.claude-plugin/plugin.json index 72d07c62..b6c15012 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-makepad-builder/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-makepad-builder/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-makepad-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Makepad Builder\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-makepad-builder/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-makepad-builder/.codex-plugin/plugin.json index a31afb96..746d2fc7 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-makepad-builder/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-makepad-builder/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-makepad-builder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Makepad Builder\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/.claude-plugin/plugin.json index be4b487c..08402792 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-marketing-growth", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Marketing & Growth\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/.codex-plugin/plugin.json index 064a6f08..539092c4 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-marketing-growth", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Marketing & Growth\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/skills/content-creator/scripts/brand_voice_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/skills/content-creator/scripts/brand_voice_analyzer.py index 92ab6f70..ed138756 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/skills/content-creator/scripts/brand_voice_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/skills/content-creator/scripts/brand_voice_analyzer.py @@ -6,6 +6,20 @@ Brand Voice Analyzer - Analyzes content to establish and maintain brand voice co import re from typing import Dict, List, Tuple import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class BrandVoiceAnalyzer: def __init__(self): @@ -176,7 +190,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() output_format = sys.argv[2] if len(sys.argv) > 2 else 'text' diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/skills/content-creator/scripts/seo_optimizer.py b/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/skills/content-creator/scripts/seo_optimizer.py index 8e77aee2..a346858a 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/skills/content-creator/scripts/seo_optimizer.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-marketing-growth/skills/content-creator/scripts/seo_optimizer.py @@ -6,6 +6,20 @@ SEO Content Optimizer - Analyzes and optimizes content for SEO import re from typing import Dict, List, Set import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class SEOOptimizer: def __init__(self): @@ -408,7 +422,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() keyword = sys.argv[2] if len(sys.argv) > 2 else None diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-mobile-developer/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-mobile-developer/.claude-plugin/plugin.json index 3c07b2ba..38e89534 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-mobile-developer/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-mobile-developer/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-mobile-developer", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Mobile Developer\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-mobile-developer/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-mobile-developer/.codex-plugin/plugin.json index c2b4c2b9..2c2a9e02 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-mobile-developer/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-mobile-developer/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-mobile-developer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Mobile Developer\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-observability-monitoring/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-observability-monitoring/.claude-plugin/plugin.json index bfef1b79..739e7900 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-observability-monitoring/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-observability-monitoring/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-observability-monitoring", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Observability & Monitoring\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-observability-monitoring/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-observability-monitoring/.codex-plugin/plugin.json index 7b4c2468..6c98c273 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-observability-monitoring/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-observability-monitoring/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-observability-monitoring", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Observability & Monitoring\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-odoo-erp/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-odoo-erp/.claude-plugin/plugin.json index f5d22d93..27927cdb 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-odoo-erp/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-odoo-erp/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-odoo-erp", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Odoo ERP\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-odoo-erp/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-odoo-erp/.codex-plugin/plugin.json index e7614925..6f4581ba 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-odoo-erp/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-odoo-erp/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-odoo-erp", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Odoo ERP\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-oss-maintainer/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-oss-maintainer/.claude-plugin/plugin.json index 7af28973..28fede71 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-oss-maintainer/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-oss-maintainer/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-oss-maintainer", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"OSS Maintainer\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-oss-maintainer/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-oss-maintainer/.codex-plugin/plugin.json index c696e49c..c4071dc9 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-oss-maintainer/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-oss-maintainer/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-oss-maintainer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"OSS Maintainer\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-python-pro/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-python-pro/.claude-plugin/plugin.json index 5a24c6c3..8e709240 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-python-pro/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-python-pro/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-python-pro", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Python Pro\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-python-pro/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-python-pro/.codex-plugin/plugin.json index ba0c951e..14b6508d 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-python-pro/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-python-pro/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-python-pro", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Python Pro\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-qa-testing/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-qa-testing/.claude-plugin/plugin.json index 777c53e8..1850abaf 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-qa-testing/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-qa-testing/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-qa-testing", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"QA & Testing\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-qa-testing/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-qa-testing/.codex-plugin/plugin.json index dd1bb522..7939ef1b 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-qa-testing/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-qa-testing/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-qa-testing", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"QA & Testing\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-revops-crm-automation/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-revops-crm-automation/.claude-plugin/plugin.json index 88c51c2a..98e3a0af 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-revops-crm-automation/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-revops-crm-automation/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-revops-crm-automation", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"RevOps & CRM Automation\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-revops-crm-automation/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-revops-crm-automation/.codex-plugin/plugin.json index 11c45ac5..88a56ef5 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-revops-crm-automation/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-revops-crm-automation/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-revops-crm-automation", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"RevOps & CRM Automation\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-security-developer/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-security-developer/.claude-plugin/plugin.json index adc594a4..dba5308d 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-security-developer/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-security-developer/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-security-developer", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Security Developer\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-security-developer/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-security-developer/.codex-plugin/plugin.json index 13347a71..1bc67ddf 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-security-developer/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-security-developer/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-security-developer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Security Developer\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-security-engineer/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-security-engineer/.claude-plugin/plugin.json index 3e2f81bd..5f8232ac 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-security-engineer/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-security-engineer/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-security-engineer", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Security Engineer\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-security-engineer/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-security-engineer/.codex-plugin/plugin.json index e3574509..376a51f0 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-security-engineer/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-security-engineer/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-security-engineer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Security Engineer\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-seo-specialist/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-seo-specialist/.claude-plugin/plugin.json index bcb0dafa..123434a1 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-seo-specialist/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-seo-specialist/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-seo-specialist", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"SEO Specialist\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-seo-specialist/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-seo-specialist/.codex-plugin/plugin.json index 1584bec3..60417ad5 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-seo-specialist/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-seo-specialist/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-seo-specialist", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"SEO Specialist\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-startup-founder/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-startup-founder/.claude-plugin/plugin.json index e1a801f5..62cca5fb 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-startup-founder/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-startup-founder/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-startup-founder", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Startup Founder\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-startup-founder/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-startup-founder/.codex-plugin/plugin.json index 786b13f9..d7e02c71 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-startup-founder/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-startup-founder/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-startup-founder", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Startup Founder\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-startup-founder/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py b/antigravity-awesome-skills/plugins/antigravity-bundle-startup-founder/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py index cd56a8d2..e1f1f230 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-startup-founder/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-startup-founder/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py @@ -8,6 +8,20 @@ import re from typing import Dict, List, Tuple, Set from collections import Counter, defaultdict import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class InterviewAnalyzer: """Analyze customer interviews for insights and patterns""" @@ -424,7 +438,7 @@ def main(): sys.exit(1) # Read interview transcript - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: interview_text = f.read() # Analyze diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-systems-programming/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-systems-programming/.claude-plugin/plugin.json index b2ba26d8..92d0d53a 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-systems-programming/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-systems-programming/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-systems-programming", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Systems Programming\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-systems-programming/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-systems-programming/.codex-plugin/plugin.json index fc2bf5d1..28e999a0 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-systems-programming/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-systems-programming/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-systems-programming", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Systems Programming\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-typescript-javascript/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-typescript-javascript/.claude-plugin/plugin.json index 5a647fd3..4d2fffde 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-typescript-javascript/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-typescript-javascript/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-typescript-javascript", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"TypeScript & JavaScript\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-typescript-javascript/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-typescript-javascript/.codex-plugin/plugin.json index d84c3eec..6faf2b78 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-typescript-javascript/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-typescript-javascript/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-typescript-javascript", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"TypeScript & JavaScript\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-web-designer/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-web-designer/.claude-plugin/plugin.json index ac308dcb..ca5ca9b5 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-web-designer/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-web-designer/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-web-designer", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Web Designer\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-web-designer/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-web-designer/.codex-plugin/plugin.json index 1f154642..34c24e59 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-web-designer/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-web-designer/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-web-designer", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Web Designer\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-web-designer/skills/mobile-design/scripts/mobile_audit.py b/antigravity-awesome-skills/plugins/antigravity-bundle-web-designer/skills/mobile-design/scripts/mobile_audit.py index f1345239..a9018e21 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-web-designer/skills/mobile-design/scripts/mobile_audit.py +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-web-designer/skills/mobile-design/scripts/mobile_audit.py @@ -71,6 +71,19 @@ import re import json from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + class MobileAuditor: def __init__(self): self.issues = [] @@ -612,11 +625,12 @@ class MobileAuditor: def audit_directory(self, directory: str) -> None: extensions = {'.tsx', '.ts', '.jsx', '.js', '.dart'} - for root, dirs, files in os.walk(directory): - dirs[:] = [d for d in dirs if d not in {'node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', 'build', '.idea'}] - for file in files: - if Path(file).suffix in extensions: - self.audit_file(os.path.join(root, file)) + skipped = {'node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', 'build', '.idea'} + for path in safe_user_path(directory).rglob("*"): + if not path.is_file() or any(part in skipped for part in path.parts): + continue + if path.suffix in extensions: + self.audit_file(str(path)) def get_report(self): return { @@ -633,7 +647,7 @@ def main(): print("Usage: python mobile_audit.py ") sys.exit(1) - path = sys.argv[1] + path = safe_user_path(sys.argv[1]) is_json = "--json" in sys.argv auditor = MobileAuditor() diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-web-wizard/.claude-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-web-wizard/.claude-plugin/plugin.json index c6e8cc9e..5bede9b7 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-web-wizard/.claude-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-web-wizard/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "antigravity-bundle-web-wizard", - "version": "13.7.0", + "version": "13.9.0", "description": "Editorial \"Web Wizard\" bundle for Claude Code from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/plugins/antigravity-bundle-web-wizard/.codex-plugin/plugin.json b/antigravity-awesome-skills/plugins/antigravity-bundle-web-wizard/.codex-plugin/plugin.json index ad7c962f..aeac71f3 100644 --- a/antigravity-awesome-skills/plugins/antigravity-bundle-web-wizard/.codex-plugin/plugin.json +++ b/antigravity-awesome-skills/plugins/antigravity-bundle-web-wizard/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agyb-web-wizard", - "version": "13.7.0", + "version": "13.9.0", "description": "Install the \"Web Wizard\" editorial skill bundle from Antigravity Awesome Skills.", "author": { "name": "sickn33 and contributors", diff --git a/antigravity-awesome-skills/skills/007/scripts/full_audit.py b/antigravity-awesome-skills/skills/007/scripts/full_audit.py index 13486982..76396878 100644 --- a/antigravity-awesome-skills/skills/007/scripts/full_audit.py +++ b/antigravity-awesome-skills/skills/007/scripts/full_audit.py @@ -27,6 +27,19 @@ import time from datetime import datetime, timezone from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Imports from the 007 config hub (same directory) # --------------------------------------------------------------------------- @@ -397,31 +410,30 @@ def _phase1_surface_mapping(target: Path, verbose: bool = False) -> dict: _config_extensions = {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf", ".env"} - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in SKIP_DIRECTORIES] + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in SKIP_DIRECTORIES for part in fpath.parts): + continue + fname = fpath.name + total_files += 1 + suffix = fpath.suffix.lower() - for fname in filenames: - total_files += 1 - fpath = Path(root) / fname - suffix = fpath.suffix.lower() + # Categorize by extension + ext_key = suffix if suffix else "(no extension)" + files_by_type[ext_key] = files_by_type.get(ext_key, 0) + 1 - # Categorize by extension - ext_key = suffix if suffix else "(no extension)" - files_by_type[ext_key] = files_by_type.get(ext_key, 0) + 1 + # Detect entry points + for pat in _entry_point_patterns: + if pat.search(fname) or pat.search(str(fpath)): + entry_points.append(str(fpath)) + break - # Detect entry points - for pat in _entry_point_patterns: - if pat.search(fname) or pat.search(str(fpath)): - entry_points.append(str(fpath)) - break + # Detect dependency files + if fname.lower() in _dep_file_names: + dependency_files.append(str(fpath)) - # Detect dependency files - if fname.lower() in _dep_file_names: - dependency_files.append(str(fpath)) - - # Detect config files - if suffix in _config_extensions or fname.lower().startswith(".env"): - config_files.append(str(fpath)) + # Detect config files + if suffix in _config_extensions or fname.lower().startswith(".env"): + config_files.append(str(fpath)) # Sort by count descending sorted_types = sorted(files_by_type.items(), key=lambda x: x[1], reverse=True) @@ -1053,7 +1065,7 @@ def run_audit( ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/skills/007/scripts/quick_scan.py b/antigravity-awesome-skills/skills/007/scripts/quick_scan.py index 0542f824..b7d3d6c9 100644 --- a/antigravity-awesome-skills/skills/007/scripts/quick_scan.py +++ b/antigravity-awesome-skills/skills/007/scripts/quick_scan.py @@ -17,6 +17,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Imports from the 007 config hub (same directory) # --------------------------------------------------------------------------- @@ -134,20 +147,16 @@ def collect_files(target: Path, logger) -> list[Path]: files: list[Path] = [] max_files = LIMITS["max_files_per_scan"] - for root, dirs, filenames in os.walk(target): - # Prune skipped directories in-place so os.walk does not descend - dirs[:] = [d for d in dirs if not _should_skip_dir(d)] - - for fname in filenames: - if len(files) >= max_files: - logger.warning( - "Reached max_files_per_scan limit (%d). Stopping collection.", max_files - ) - return files - - fpath = Path(root) / fname - if _is_scannable(fpath): - files.append(fpath) + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(_should_skip_dir(part) for part in fpath.parts): + continue + if len(files) >= max_files: + logger.warning( + "Reached max_files_per_scan limit (%d). Stopping collection.", max_files + ) + return files + if _is_scannable(fpath): + files.append(fpath) return files @@ -368,7 +377,7 @@ def run_scan(target_path: str, output_format: str = "text", verbose: bool = Fals logger = setup_logging("007-quick-scan") ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/skills/007/scripts/scanners/dependency_scanner.py b/antigravity-awesome-skills/skills/007/scripts/scanners/dependency_scanner.py index 26798c67..c16f1e31 100644 --- a/antigravity-awesome-skills/skills/007/scripts/scanners/dependency_scanner.py +++ b/antigravity-awesome-skills/skills/007/scripts/scanners/dependency_scanner.py @@ -17,6 +17,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Import from the 007 config hub (parent directory) # --------------------------------------------------------------------------- @@ -850,27 +863,26 @@ def discover_dependency_files(target: Path) -> list[Path]: """ found: list[Path] = [] - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in config.SKIP_DIRECTORIES] + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in config.SKIP_DIRECTORIES for part in fpath.parts): + continue + fname = fpath.name + fname_lower = fname.lower() - for fname in filenames: - fpath = Path(root) / fname - fname_lower = fname.lower() + # Exact name matches + if fname in ALL_DEP_FILES: + found.append(fpath) + continue - # Exact name matches - if fname in ALL_DEP_FILES: - found.append(fpath) - continue + # requirements*.txt variants + if _REQUIREMENTS_RE.match(fname): + found.append(fpath) + continue - # requirements*.txt variants - if _REQUIREMENTS_RE.match(fname): - found.append(fpath) - continue - - # Docker files (prefix match) - if any(fname_lower.startswith(prefix.lower()) for prefix in DOCKER_PREFIXES): - found.append(fpath) - continue + # Docker files (prefix match) + if any(fname_lower.startswith(prefix.lower()) for prefix in DOCKER_PREFIXES): + found.append(fpath) + continue return found @@ -1158,7 +1170,7 @@ def run_scan( config.ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/skills/007/scripts/scanners/injection_scanner.py b/antigravity-awesome-skills/skills/007/scripts/scanners/injection_scanner.py index 0bdb59f6..32029775 100644 --- a/antigravity-awesome-skills/skills/007/scripts/scanners/injection_scanner.py +++ b/antigravity-awesome-skills/skills/007/scripts/scanners/injection_scanner.py @@ -20,6 +20,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Import from the 007 config hub (parent directory) # --------------------------------------------------------------------------- @@ -546,19 +559,16 @@ def collect_files(target: Path) -> list[Path]: files: list[Path] = [] max_files = config.LIMITS["max_files_per_scan"] - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in config.SKIP_DIRECTORIES] - - for fname in filenames: - if len(files) >= max_files: - logger.warning( - "Reached max_files_per_scan limit (%d). Stopping.", max_files - ) - return files - - fpath = Path(root) / fname - if _should_scan_file(fpath): - files.append(fpath) + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in config.SKIP_DIRECTORIES for part in fpath.parts): + continue + if len(files) >= max_files: + logger.warning( + "Reached max_files_per_scan limit (%d). Stopping.", max_files + ) + return files + if _should_scan_file(fpath): + files.append(fpath) return files @@ -961,7 +971,7 @@ def run_scan( config.ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/skills/007/scripts/scanners/secrets_scanner.py b/antigravity-awesome-skills/skills/007/scripts/scanners/secrets_scanner.py index 783bf3ae..68711c36 100644 --- a/antigravity-awesome-skills/skills/007/scripts/scanners/secrets_scanner.py +++ b/antigravity-awesome-skills/skills/007/scripts/scanners/secrets_scanner.py @@ -20,6 +20,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Import from the 007 config hub (parent directory) # --------------------------------------------------------------------------- @@ -375,19 +388,16 @@ def collect_files(target: Path) -> list[Path]: files: list[Path] = [] max_files = config.LIMITS["max_files_per_scan"] - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in config.SKIP_DIRECTORIES] - - for fname in filenames: - if len(files) >= max_files: - logger.warning( - "Reached max_files_per_scan limit (%d). Stopping.", max_files - ) - return files - - fpath = Path(root) / fname - if _should_scan_file(fpath): - files.append(fpath) + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in config.SKIP_DIRECTORIES for part in fpath.parts): + continue + if len(files) >= max_files: + logger.warning( + "Reached max_files_per_scan limit (%d). Stopping.", max_files + ) + return files + if _should_scan_file(fpath): + files.append(fpath) return files @@ -869,7 +879,7 @@ def run_scan( config.ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/skills/007/scripts/score_calculator.py b/antigravity-awesome-skills/skills/007/scripts/score_calculator.py index efe1b008..e7e7dded 100644 --- a/antigravity-awesome-skills/skills/007/scripts/score_calculator.py +++ b/antigravity-awesome-skills/skills/007/scripts/score_calculator.py @@ -24,6 +24,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --------------------------------------------------------------------------- # Imports from the 007 config hub (same directory) # --------------------------------------------------------------------------- @@ -141,18 +154,17 @@ def _collect_source_files(target: Path) -> list[Path]: files: list[Path] = [] max_files = LIMITS["max_files_per_scan"] - for root, dirs, filenames in os.walk(target): - dirs[:] = [d for d in dirs if d not in SKIP_DIRECTORIES] - for fname in filenames: - if len(files) >= max_files: - return files - fpath = Path(root) / fname - suffix = fpath.suffix.lower() - name = fpath.name.lower() - for ext in SCANNABLE_EXTENSIONS: - if name.endswith(ext) or suffix == ext: - files.append(fpath) - break + for fpath in safe_user_path(target).rglob("*"): + if not fpath.is_file() or any(part in SKIP_DIRECTORIES for part in fpath.parts): + continue + if len(files) >= max_files: + return files + suffix = fpath.suffix.lower() + name = fpath.name.lower() + for ext in SCANNABLE_EXTENSIONS: + if name.endswith(ext) or suffix == ext: + files.append(fpath) + break return files @@ -529,7 +541,7 @@ def run_score( ensure_directories() - target = Path(target_path).resolve() + target = safe_user_path(target_path).resolve() if not target.exists(): logger.error("Target path does not exist: %s", target) sys.exit(1) diff --git a/antigravity-awesome-skills/skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py b/antigravity-awesome-skills/skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py index 7b822aef..d42509dd 100755 --- a/antigravity-awesome-skills/skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py +++ b/antigravity-awesome-skills/skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py @@ -14,6 +14,20 @@ import socket import requests from urllib.parse import urlparse from typing import Optional, Dict, Any +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path API_BASE_URL = "https://2slides.com/api/v1" @@ -124,7 +138,7 @@ def download_slides_pages_voices( zip_response.raise_for_status() # Save to file - with open(output_path, 'wb') as f: + with safe_user_path(output_path).open('wb') as f: for chunk in zip_response.iter_content(chunk_size=8192): f.write(chunk) diff --git a/antigravity-awesome-skills/skills/audio-transcriber/scripts/transcribe.py b/antigravity-awesome-skills/skills/audio-transcriber/scripts/transcribe.py index 41ea455c..95d4402f 100755 --- a/antigravity-awesome-skills/skills/audio-transcriber/scripts/transcribe.py +++ b/antigravity-awesome-skills/skills/audio-transcriber/scripts/transcribe.py @@ -12,6 +12,19 @@ import shutil from datetime import datetime from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # Rich for beautiful terminal output try: from rich.console import Console @@ -386,9 +399,9 @@ def save_outputs(transcript_text, ata_text, audio_file, output_dir="."): # Sempre salva transcript transcript_filename = f"transcript-{timestamp}.md" - transcript_path = Path(output_dir) / transcript_filename + transcript_path = safe_user_path(output_dir) / transcript_filename - with open(transcript_path, 'w', encoding='utf-8') as f: + with transcript_path.open('w', encoding='utf-8') as f: f.write(transcript_text) console.print(f"[green]✅ Transcript salvo:[/green] {transcript_filename}") @@ -397,9 +410,9 @@ def save_outputs(transcript_text, ata_text, audio_file, output_dir="."): ata_path = None if ata_text: ata_filename = f"ata-{timestamp}.md" - ata_path = Path(output_dir) / ata_filename + ata_path = safe_user_path(output_dir) / ata_filename - with open(ata_path, 'w', encoding='utf-8') as f: + with ata_path.open('w', encoding='utf-8') as f: f.write(ata_text) console.print(f"[green]✅ Ata salva:[/green] {ata_filename}") diff --git a/antigravity-awesome-skills/skills/bumblebee/scripts/render_report.py b/antigravity-awesome-skills/skills/bumblebee/scripts/render_report.py index 24547c48..b8e557c7 100755 --- a/antigravity-awesome-skills/skills/bumblebee/scripts/render_report.py +++ b/antigravity-awesome-skills/skills/bumblebee/scripts/render_report.py @@ -30,6 +30,19 @@ import sys from collections import Counter, defaultdict from datetime import datetime, timezone from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any @@ -336,11 +349,11 @@ def build_report(records: list[dict[str, Any]], source_path: Path) -> str: def main(argv: list[str]) -> int: if len(argv) != 3: - print(f"usage: {Path(argv[0]).name} ", file=sys.stderr) + print(f"usage: {safe_user_path(argv[0]).name} ", file=sys.stderr) return 1 - input_path = Path(argv[1]) - output_path = Path(argv[2]) + input_path = safe_user_path(argv[1]) + output_path = safe_user_path(argv[2]) if not input_path.exists() or input_path.stat().st_size == 0: print(f"error: {input_path} is missing or empty", file=sys.stderr) diff --git a/antigravity-awesome-skills/skills/claude-monitor/scripts/monitor.py b/antigravity-awesome-skills/skills/claude-monitor/scripts/monitor.py index 651fcd50..74b89e8e 100644 --- a/antigravity-awesome-skills/skills/claude-monitor/scripts/monitor.py +++ b/antigravity-awesome-skills/skills/claude-monitor/scripts/monitor.py @@ -20,6 +20,19 @@ import time from datetime import datetime from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + try: import psutil except ImportError: @@ -281,8 +294,8 @@ def main(): else: output_path = f"monitor_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" - with open(output_path, "w", encoding="utf-8") as f: - json.dump(output_data, f, indent=2, ensure_ascii=False) + with safe_user_path(output_path).open("w", encoding="utf-8") as f: + f.write(json.dumps(output_data, indent=2, ensure_ascii=False)) print(f"\nLog salvo em: {output_path}\n") diff --git a/antigravity-awesome-skills/skills/code-polish/SKILL.md b/antigravity-awesome-skills/skills/code-polish/SKILL.md new file mode 100644 index 00000000..af88dc4e --- /dev/null +++ b/antigravity-awesome-skills/skills/code-polish/SKILL.md @@ -0,0 +1,192 @@ +--- +name: code-polish +description: Rewrites unprofessional code comments into clear ones and performs non-semantic cleanup. Use to professionalize code without altering logic or behavior. +risk: critical +source: community +date_added: "2026-07-02" +--- + +# Code Polish + +A constraint-based protocol for normalizing code comments and performing safe, non-semantic cleanup. This skill exists because human-written code tends to carry casual, outdated, or missing comments, while the goal is professional-grade documentation without touching behavior. + +This file is self-contained. Do not require any other skill file to execute this protocol. + +## Prime Directive + +Comments and non-semantic cleanup are the job. Logic is never the job. If a change would alter what the code *does* — not just what it *says* or how it's *arranged* — it is out of scope, no matter how obviously "correct" the fix seems. + +--- + +## When to Use + +Apply this skill when: +- The user asks to "clean up," "professionalize," or "polish" existing code +- Code is being prepped for code review, handoff, open-sourcing, or documentation +- A file has a mix of human and AI-written comments and needs one consistent, professional voice +- Comments are outdated, missing, redundant, or written casually (venting, placeholders, inside jokes) +- The user wants comments improved but explicitly does **not** want logic touched + +Do not apply this skill when: +- The user wants a bug fixed or behavior changed (that's a different job — logic edits are out of scope here) +- The user wants a full rewrite or architectural restructuring +- The only ask is adding new features or functionality + +--- + +## Phase 0 — Full Read + +Before editing anything, read the entire file (or the entire relevant module if the codebase is large — not just the function in question). Do not comment or clean incrementally while still reading. A comment written without full context is a guess, and guesses are how "professional" comments end up wrong. + +Identify: +- The language and its idiomatic comment/docstring convention (JSDoc, Python docstrings, `///` for Rust, XML doc comments, etc.) +- Any existing project comment style already in use elsewhere in the file — match it rather than importing a foreign convention +- Any comment that encodes real, non-obvious information (race conditions, workarounds for external bugs, "don't reorder this" warnings, business-rule justifications) + +--- + +## Phase 1 — Comment Audit + +Classify every existing comment into one of these categories before touching it: + +| Category | Example | Action | +|---|---|---| +| **Junk / venting** | `// wtf is this`, `// idk why but it works` | Remove tone, extract any real information underneath, rewrite professionally — or delete if it truly holds zero information | +| **Placeholder** | `// fix later`, `// TODO hack` | Convert to a proper `TODO:` note with the actual concern stated plainly, or remove if stale/resolved | +| **Dead code comments** | Blocks of commented-out code | Remove, unless the surrounding context makes clear it's intentionally preserved (e.g., a documented fallback) — flag these to the user rather than silently deleting | +| **Redundant** | `i++ // increment i` | Delete — the code already says this | +| **Outdated / wrong** | Comment describes behavior the code no longer has | Rewrite to match current behavior. Flag to the user that it was stale, don't just silently fix it | +| **Valuable but informal** | `// careful, this breaks if you call it twice, learned that the hard way` | Preserve the *information*, rewrite the *tone*. Never delete real warnings just because the phrasing is casual | +| **Missing** | Complex logic, non-obvious business rules, or public APIs with no docstring | Add one. Don't over-comment simple, self-explanatory lines | + +--- + +## Phase 2 — Non-Semantic Cleanup + +Scope is strictly limited to changes that cannot alter behavior: + +- Consistent indentation and whitespace +- Consistent brace/bracket style matching the surrounding file +- Removing truly dead code (unreachable blocks) — only when unambiguous, and flagged in the summary +- Splitting overly long lines for readability +- Local variable renaming for clarity is allowed **only** for private/local-scope names, and only when the improvement is unambiguous — never rename anything exported, public, or referenced across files without calling it out explicitly first + +Anything beyond this — reordering logic, extracting functions, changing control flow, altering algorithms — is out of scope for this skill. + +--- + +## Phase 3 — Comment Rewrite / Addition + +Apply these standards to every comment touched or added: + +- **Explain why, not what.** The code already shows *what* it does; a comment earns its place by explaining intent, tradeoffs, or non-obvious constraints. +- **Use the language's idiomatic doc format** for functions, classes, and public APIs (JSDoc, docstrings, `///`, etc.) — match the convention already used elsewhere in the file if one exists. +- **Be concise.** No padding, no restating the obvious, no filler sentences. +- **No informal register.** No jokes, no venting, no first-person asides ("I think this works because..."). +- **No AI-tell phrasing.** Avoid generic filler like "This function is responsible for..." or "Note that..." padding, and avoid em-dashes. Write plainly and directly, the way a careful senior engineer would. +- **Don't invent behavior.** If you're not certain why something is done a certain way, say what the code does, not a fabricated justification for why. + +--- + +## Phase 4 — Verification + +Before presenting the result: + +- Confirm the edited file's logic is behaviorally identical to the original — comments and whitespace are the only permitted diffs, plus whatever narrow Phase 2 cleanup was done. +- Re-read the diff end to end, not just the changed lines in isolation, to catch anything that accidentally shifted meaning. +- If a rewritten comment removes information that was present in the original (even informally stated), that's a failure — go back and preserve it. + +--- + +## Phase 5 — Report Back + +Summarize for the user, don't just hand back a silent diff: +- How many comments were rewritten, added, or removed, and why +- Any comments flagged as "informal but contained a real warning" — confirm the information was preserved +- Any dead code or stale comments removed, listed explicitly +- Anything you were unsure about and left alone rather than guessing + +--- + +## Examples + +**Junk / venting → professional** +```js +// before +// ugh this took forever to figure out. api rate limits us super hard in prod so we have to do exponential backoff here. just leave it alone +function retryFetch(url, attempts) { ... } + +// after +// Uses exponential backoff to handle aggressive API rate-limiting in production. +function retryFetch(url, attempts) { ... } +``` + +**Redundant → removed** +```python +# before +count += 1 # increment count by 1 + +# after +count += 1 +``` + +**Valuable but informal → tone rewritten, information preserved** +```python +# before +# careful, this breaks if you call it twice, learned that the hard way + +# after +# Not idempotent: calling this more than once per session corrupts the +# cache index. Callers must guard against duplicate invocation. +``` + +**Missing → added** +```java +// before +public double calculate(double base, int tier) { + return base * (tier > 2 ? 0.85 : 1.0); +} + +// after +/** + * Applies the loyalty discount. Tiers above 2 qualify for a 15% discount; + * this threshold matches the current pricing policy, not a technical limit. + */ +public double calculate(double base, int tier) { + return base * (tier > 2 ? 0.85 : 1.0); +} +``` + +**Outdated / wrong → corrected and flagged** +```go +// before +// returns nil if user not found +func GetUser(id string) (*User, error) { ... } // now returns ErrNotFound instead + +// after +// Returns ErrNotFound if the user does not exist. +func GetUser(id string) (*User, error) { ... } +// (flagged to user: original comment was stale — function used to return nil, +// now returns a named error) +``` + +--- + +## Security & Safety Notes + +This skill never: +- Changes program logic, control flow, or algorithmic behavior +- Restructures code (extracting/inlining functions, reordering execution, changing architecture) +- Renames anything public, exported, or cross-referenced without explicit confirmation +- Deletes a comment solely because its tone is casual, without checking whether it carries real information first +- Fabricates a rationale for a comment when the actual reason isn't knowable from context — state what's certain only + +--- + +## Limitations + +- Cannot verify runtime behavior — Phase 4 is a read-through diff check, not a test run. For anything beyond trivial files, the user should still run the actual test suite after applying this skill. +- Judgment calls on ambiguous cases (e.g., "is this dead code intentional or forgotten?") default to flagging rather than guessing — this means some cleanup will need a quick human yes/no rather than happening silently. +- Not a substitute for a linter or formatter — Phase 2 cleanup is deliberately conservative and won't enforce a full style guide (e.g., max line length rules, import ordering) unless that's trivially inferable from the surrounding file. +- Comment quality is bounded by how well the code's actual intent can be inferred from context. If the "why" genuinely isn't recoverable from the file (no domain knowledge, no commit history, no ticket references available), the honest output is a comment describing *what*, not a confident but invented *why*. +- Large files or unfamiliar codebases increase the risk of Phase 0 missing context that would have changed a comment's wording — flag uncertainty in the Phase 5 report rather than presenting low-confidence rewrites as settled. diff --git a/antigravity-awesome-skills/skills/competitor-analysis/scripts/capture_screenshots.mjs b/antigravity-awesome-skills/skills/competitor-analysis/scripts/capture_screenshots.mjs index b88b7fb8..3331c5cd 100644 --- a/antigravity-awesome-skills/skills/competitor-analysis/scripts/capture_screenshots.mjs +++ b/antigravity-awesome-skills/skills/competitor-analysis/scripts/capture_screenshots.mjs @@ -12,12 +12,33 @@ // // Usage: node capture_screenshots.mjs [--mode remote|local] [--concurrency 2] +import sanitizeFilename from 'sanitize-filename'; import { readdirSync, readFileSync, mkdirSync, existsSync } from 'fs'; -import { join } from 'path'; +import { isAbsolute, join, relative, resolve } from 'path'; import { spawnSync } from 'child_process'; import { parseFrontmatter } from './md_utils.mjs'; const args = process.argv.slice(2); +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} + +function safeCliPath(pathValue, baseDir = process.cwd()) { + const root = resolve(baseDir); + const target = resolve(root, ...sanitizePathSegments(pathValue)); + const rel = relative(root, target); + if (rel.startsWith('..') || isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + if (args.includes('--help') || args.includes('-h') || args.length === 0) { console.error(`Usage: node capture_screenshots.mjs [options] @@ -38,7 +59,7 @@ Options: process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1); } -const dir = args[0]; +const dir = safeCliPath(args[0]); const modeIdx = args.indexOf('--mode'); const browseMode = modeIdx !== -1 ? args[modeIdx + 1] : 'remote'; const modeFlag = browseMode === 'local' ? '--local' : '--remote'; @@ -63,7 +84,7 @@ if (concurrency > 1) { concurrency = 1; } -const shotsDir = join(dir, 'screenshots'); +const shotsDir = safeCliPath('screenshots', dir); mkdirSync(shotsDir, { recursive: true }); function run(cmd, args, { timeout = 30000 } = {}) { diff --git a/antigravity-awesome-skills/skills/competitor-analysis/scripts/compile_report.mjs b/antigravity-awesome-skills/skills/competitor-analysis/scripts/compile_report.mjs index 19be1b9b..342c8621 100644 --- a/antigravity-awesome-skills/skills/competitor-analysis/scripts/compile_report.mjs +++ b/antigravity-awesome-skills/skills/competitor-analysis/scripts/compile_report.mjs @@ -9,6 +9,7 @@ import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { basename, dirname, join, relative, resolve } from 'path'; import { fileURLToPath } from 'url'; +import sanitizeFilename from 'sanitize-filename'; import { parseFrontmatter, parseBody, parseSections } from './md_utils.mjs'; const __filename = fileURLToPath(import.meta.url); @@ -17,9 +18,19 @@ const __dirname = dirname(__filename); const args = process.argv.slice(2); const SAFE_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} + function safeJoin(base, ...parts) { const root = resolve(base); - const target = resolve(root, ...parts); + const target = resolve(root, ...parts.flatMap(sanitizePathSegments)); const rel = relative(root, target); if (rel.startsWith('..') || rel.startsWith('/')) { throw new Error(`Path escapes research directory: ${parts.join('/')}`); @@ -32,7 +43,7 @@ function safeResearchDir(rawDir) { throw new Error('Research directory is required'); } const root = resolve(process.cwd()); - const target = resolve(root, rawDir); + const target = safeJoin(root, rawDir); const rel = relative(root, target); if ((rel.startsWith('..') || rel.startsWith('/')) && process.env.COMPETITOR_ANALYSIS_ALLOW_EXTERNAL_DIR !== '1') { throw new Error('Research directory must stay under the current working directory'); diff --git a/antigravity-awesome-skills/skills/competitor-analysis/scripts/extract_vs_names.mjs b/antigravity-awesome-skills/skills/competitor-analysis/scripts/extract_vs_names.mjs index 294883d9..0f93b411 100644 --- a/antigravity-awesome-skills/skills/competitor-analysis/scripts/extract_vs_names.mjs +++ b/antigravity-awesome-skills/skills/competitor-analysis/scripts/extract_vs_names.mjs @@ -9,10 +9,31 @@ // Output: newline-delimited JSON to stdout, one object per candidate: // { "name": "serper", "hits": 3, "domain": "serper.dev", "example": "Tavily vs Serper..." } +import sanitizeFilename from 'sanitize-filename'; import { readdirSync, readFileSync } from 'fs'; -import { join } from 'path'; +import { isAbsolute, join, relative, resolve } from 'path'; const args = process.argv.slice(2); +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} + +function safeCliPath(pathValue, baseDir = process.cwd()) { + const root = resolve(baseDir); + const target = resolve(root, ...sanitizePathSegments(pathValue)); + const rel = relative(root, target); + if (rel.startsWith('..') || isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + if (args.includes('--help') || args.includes('-h') || args.length === 0) { console.error(`Usage: node extract_vs_names.mjs [--prefix ] [--seed ""] @@ -28,7 +49,7 @@ Options: process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1); } -const dir = args[0]; +const dir = safeCliPath(args[0]); const prefixIdx = args.indexOf('--prefix'); const prefix = prefixIdx !== -1 && args[prefixIdx + 1] ? args[prefixIdx + 1] : 'competitor'; const seedIdx = args.indexOf('--seed'); diff --git a/antigravity-awesome-skills/skills/competitor-analysis/scripts/gate_candidates.mjs b/antigravity-awesome-skills/skills/competitor-analysis/scripts/gate_candidates.mjs index e7b30612..93960761 100644 --- a/antigravity-awesome-skills/skills/competitor-analysis/scripts/gate_candidates.mjs +++ b/antigravity-awesome-skills/skills/competitor-analysis/scripts/gate_candidates.mjs @@ -14,6 +14,7 @@ // { "url": "https://foo.com", "status": "PASS" | "REJECT" | "UNKNOWN", // "matched_includes": [...], "matched_excludes": [...], "title": "...", "hero": "..." } +import sanitizeFilename from 'sanitize-filename'; import { execFile } from 'child_process'; import { promisify } from 'util'; import { readFileSync } from 'fs'; @@ -25,6 +26,26 @@ import { readFileSync } from 'fs'; const execFileAsync = promisify(execFile); const args = process.argv.slice(2); +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} + +function safeCliPath(pathValue, baseDir = process.cwd()) { + const root = resolve(baseDir); + const target = resolve(root, ...sanitizePathSegments(pathValue)); + const rel = relative(root, target); + if (rel.startsWith('..') || isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + if (args.includes('--help') || args.includes('-h')) { console.error(`Usage: cat urls.txt | node gate_candidates.mjs [options] diff --git a/antigravity-awesome-skills/skills/competitor-analysis/scripts/list_urls.mjs b/antigravity-awesome-skills/skills/competitor-analysis/scripts/list_urls.mjs index 75631c38..19fd7fb5 100644 --- a/antigravity-awesome-skills/skills/competitor-analysis/scripts/list_urls.mjs +++ b/antigravity-awesome-skills/skills/competitor-analysis/scripts/list_urls.mjs @@ -5,10 +5,31 @@ // Reads all {prefix}_discovery_batch_*.json files, deduplicates by domain, // outputs one URL per line to stdout, stats to stderr. +import sanitizeFilename from 'sanitize-filename'; import { readdirSync, readFileSync } from 'fs'; -import { join } from 'path'; +import { isAbsolute, join, relative, resolve } from 'path'; const args = process.argv.slice(2); +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} + +function safeCliPath(pathValue, baseDir = process.cwd()) { + const root = resolve(baseDir); + const target = resolve(root, ...sanitizePathSegments(pathValue)); + const rel = relative(root, target); + if (rel.startsWith('..') || isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + if (args.includes('--help') || args.includes('-h') || args.length === 0) { console.error(`Usage: node list_urls.mjs [--prefix ] @@ -26,7 +47,7 @@ Examples: process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1); } -const dir = args[0]; +const dir = safeCliPath(args[0]); const prefixIdx = args.indexOf('--prefix'); const prefix = prefixIdx !== -1 && args[prefixIdx + 1] ? args[prefixIdx + 1] : 'competitor'; diff --git a/antigravity-awesome-skills/skills/competitor-analysis/scripts/merge_partials.mjs b/antigravity-awesome-skills/skills/competitor-analysis/scripts/merge_partials.mjs index 402e04b5..c440143c 100644 --- a/antigravity-awesome-skills/skills/competitor-analysis/scripts/merge_partials.mjs +++ b/antigravity-awesome-skills/skills/competitor-analysis/scripts/merge_partials.mjs @@ -18,11 +18,32 @@ // // Usage: node merge_partials.mjs +import sanitizeFilename from 'sanitize-filename'; import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; -import { join } from 'path'; +import { isAbsolute, join, relative, resolve } from 'path'; import { parseFrontmatter, parseBody, parseSections } from './md_utils.mjs'; const args = process.argv.slice(2); +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} + +function safeCliPath(pathValue, baseDir = process.cwd()) { + const root = resolve(baseDir); + const target = resolve(root, ...sanitizePathSegments(pathValue)); + const rel = relative(root, target); + if (rel.startsWith('..') || isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + if (args.includes('--help') || args.includes('-h') || args.length === 0) { console.error(`Usage: node merge_partials.mjs @@ -31,8 +52,8 @@ Reads {dir}/partials/{slug}.{lane}.md files and writes consolidated process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1); } -const dir = args[0]; -const partialsDir = join(dir, 'partials'); +const dir = safeCliPath(args[0]); +const partialsDir = safeCliPath('partials', dir); const LANES = ['marketing', 'discussion', 'social', 'news', 'technical', 'battle']; diff --git a/antigravity-awesome-skills/skills/content-creator/scripts/brand_voice_analyzer.py b/antigravity-awesome-skills/skills/content-creator/scripts/brand_voice_analyzer.py index 92ab6f70..ed138756 100644 --- a/antigravity-awesome-skills/skills/content-creator/scripts/brand_voice_analyzer.py +++ b/antigravity-awesome-skills/skills/content-creator/scripts/brand_voice_analyzer.py @@ -6,6 +6,20 @@ Brand Voice Analyzer - Analyzes content to establish and maintain brand voice co import re from typing import Dict, List, Tuple import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class BrandVoiceAnalyzer: def __init__(self): @@ -176,7 +190,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() output_format = sys.argv[2] if len(sys.argv) > 2 else 'text' diff --git a/antigravity-awesome-skills/skills/content-creator/scripts/seo_optimizer.py b/antigravity-awesome-skills/skills/content-creator/scripts/seo_optimizer.py index 8e77aee2..a346858a 100644 --- a/antigravity-awesome-skills/skills/content-creator/scripts/seo_optimizer.py +++ b/antigravity-awesome-skills/skills/content-creator/scripts/seo_optimizer.py @@ -6,6 +6,20 @@ SEO Content Optimizer - Analyzes and optimizes content for SEO import re from typing import Dict, List, Set import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class SEOOptimizer: def __init__(self): @@ -408,7 +422,7 @@ if __name__ == "__main__": import sys if len(sys.argv) > 1: - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: content = f.read() keyword = sys.argv[2] if len(sys.argv) > 2 else None diff --git a/antigravity-awesome-skills/skills/diary/scripts/fetch_diaries.py b/antigravity-awesome-skills/skills/diary/scripts/fetch_diaries.py index 5b427803..e75cb76a 100644 --- a/antigravity-awesome-skills/skills/diary/scripts/fetch_diaries.py +++ b/antigravity-awesome-skills/skills/diary/scripts/fetch_diaries.py @@ -16,6 +16,19 @@ import sys from datetime import datetime from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # --- Configuration --- GLOBAL_DIARY_ROOT = Path(os.environ.get("GLOBAL_DIARY_ROOT", str(Path(__file__).resolve().parent.parent / "diary"))) @@ -31,7 +44,7 @@ def main(): print("Usage: python fetch_diaries.py ") sys.exit(1) - proj_diary_path = Path(sys.argv[1]) + proj_diary_path = safe_user_path(sys.argv[1]) if not proj_diary_path.exists(): print(f"⚠️ 找不到專案日記: {proj_diary_path}") sys.exit(1) diff --git a/antigravity-awesome-skills/skills/diary/scripts/prepare_context.py b/antigravity-awesome-skills/skills/diary/scripts/prepare_context.py index a04145d2..eca940c6 100644 --- a/antigravity-awesome-skills/skills/diary/scripts/prepare_context.py +++ b/antigravity-awesome-skills/skills/diary/scripts/prepare_context.py @@ -15,6 +15,19 @@ import sys import json import glob from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from datetime import datetime @@ -159,7 +172,7 @@ def prepare_context(root_path): context_file = root / "AGENT_CONTEXT.md" - with open(context_file, "w", encoding="utf-8") as f: + with safe_user_path(context_file).open("w", encoding="utf-8") as f: # Header f.write(f"# 專案上下文 (Agent Context):{root.name}\n\n") f.write(f"> **最後更新時間**:{now}\n") @@ -240,5 +253,5 @@ def prepare_context(root_path): if __name__ == "__main__": - target = sys.argv[1] if len(sys.argv) > 1 else "." + target = safe_user_path(sys.argv[1]) if len(sys.argv) > 1 else "." prepare_context(target) diff --git a/antigravity-awesome-skills/skills/diary/scripts/sync_to_notion.py b/antigravity-awesome-skills/skills/diary/scripts/sync_to_notion.py index 2fdddcfc..38c7d871 100644 --- a/antigravity-awesome-skills/skills/diary/scripts/sync_to_notion.py +++ b/antigravity-awesome-skills/skills/diary/scripts/sync_to_notion.py @@ -21,6 +21,19 @@ import requests from datetime import datetime from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # ── Configuration ────────────────────────────────────────────── NOTION_TOKEN = os.environ.get("NOTION_TOKEN", "") NOTION_DIARY_DB = os.environ.get("NOTION_DIARY_DB", "") @@ -430,7 +443,7 @@ def main(): print(" python sync_to_notion.py --create-db ") sys.exit(1) - diary_path = Path(sys.argv[1]) + diary_path = safe_user_path(sys.argv[1]) if not diary_path.exists(): print(f"❌ 找不到日記文件:{diary_path}") sys.exit(1) diff --git a/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/pack.py b/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/pack.py index 1145107a..630622f0 100755 --- a/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/pack.py +++ b/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/pack.py @@ -16,6 +16,19 @@ import zipfile from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def validate_input_tree(input_dir: Path): root = input_dir.resolve(strict=True) for path in input_dir.rglob("*"): @@ -27,6 +40,18 @@ def validate_input_tree(input_dir: Path): raise ValueError(f"Refusing to pack path outside input directory: {path}") from None +def copy_tree_contents(source_dir: Path, target_dir: Path) -> None: + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def main(): parser = argparse.ArgumentParser(description="Pack a directory into an Office file") parser.add_argument("input_directory", help="Unpacked Office document directory") @@ -65,7 +90,7 @@ def pack_document(input_dir, output_file, validate=False): bool: True if successful, False if validation failed """ input_dir = Path(input_dir) - output_file = Path(output_file) + output_file = safe_user_path(output_file) if not input_dir.is_dir(): raise ValueError(f"{input_dir} is not a directory") @@ -76,7 +101,7 @@ def pack_document(input_dir, output_file, validate=False): # Work in temporary directory to avoid modifying original with tempfile.TemporaryDirectory() as temp_dir: temp_content_dir = Path(temp_dir) / "content" - shutil.copytree(input_dir, temp_content_dir) + copy_tree_contents(input_dir, temp_content_dir) # Process XML files to remove pretty-printing whitespace for pattern in ["*.xml", "*.rels"]: @@ -85,10 +110,12 @@ def pack_document(input_dir, output_file, validate=False): # Create final Office file as zip archive output_file.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + temp_zip_path = Path(temp_dir) / "office.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for f in temp_content_dir.rglob("*"): if f.is_file(): zf.write(f, f.relative_to(temp_content_dir)) + output_file.write_bytes(temp_zip_path.read_bytes()) # Validate if requested if validate: diff --git a/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/unpack.py b/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/unpack.py index 33ed3a35..ef9d69d3 100755 --- a/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/unpack.py +++ b/antigravity-awesome-skills/skills/docx-official/ooxml/scripts/unpack.py @@ -8,6 +8,19 @@ import sys import zipfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + MAX_ARCHIVE_MEMBERS = 5000 MAX_MEMBER_SIZE = 100 * 1024 * 1024 MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024 @@ -30,7 +43,7 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro return destination.parent.mkdir(parents=True, exist_ok=True) - with archive.open(member, "r") as source, open(destination, "wb") as target: + with archive.open(member, "r") as source, safe_user_path(destination).open("wb") as target: shutil.copyfileobj(source, target) @@ -57,7 +70,7 @@ def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path): def extract_archive_safely(input_file: str | Path, output_dir: str | Path): - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) output_path.mkdir(parents=True, exist_ok=True) output_root = output_path.resolve() @@ -82,7 +95,7 @@ def main(argv: list[str] | None = None): raise SystemExit("Usage: python unpack.py ") input_file, output_dir = argv - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) extract_archive_safely(input_file, output_path) pretty_print_xml(output_path) diff --git a/antigravity-awesome-skills/skills/drizzle-migration-conflict/scripts/check_drizzle_migrations.py b/antigravity-awesome-skills/skills/drizzle-migration-conflict/scripts/check_drizzle_migrations.py index c3a69418..820ab044 100755 --- a/antigravity-awesome-skills/skills/drizzle-migration-conflict/scripts/check_drizzle_migrations.py +++ b/antigravity-awesome-skills/skills/drizzle-migration-conflict/scripts/check_drizzle_migrations.py @@ -20,6 +20,19 @@ import re import sys from dataclasses import asdict, dataclass from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any, Iterable CONFIG_NAME_PATTERN = re.compile(r"^drizzle(?:[.-].+)?\.config\.(?:ts|js|mjs|cjs|mts|cts)$") @@ -169,15 +182,14 @@ def iter_config_files( if explicit_configs: return configs, issues - for current_root, dirnames, filenames in os.walk(root): - dirnames[:] = [name for name in dirnames if name not in SKIP_DIR_NAMES] - base = Path(current_root) - for filename in filenames: - if CONFIG_NAME_PATTERN.match(filename): - path = (base / filename).resolve() - if path not in seen: - seen.add(path) - configs.append(path) + for path in safe_user_path(root).rglob("*"): + if not path.is_file() or any(part in SKIP_DIR_NAMES for part in path.parts): + continue + if CONFIG_NAME_PATTERN.match(path.name): + resolved = path.resolve() + if resolved not in seen: + seen.add(resolved) + configs.append(resolved) return configs, issues @@ -283,13 +295,11 @@ def discover_dirs(args: argparse.Namespace, root: Path) -> tuple[list[Path], lis def iter_text_files(directory: Path) -> Iterable[Path]: - for current_root, dirnames, filenames in os.walk(directory): - dirnames[:] = [name for name in dirnames if name not in SKIP_DIR_NAMES] - base = Path(current_root) - for filename in filenames: - path = base / filename - if path.suffix in TEXT_SUFFIXES: - yield path + for path in safe_user_path(directory).rglob("*"): + if not path.is_file() or any(part in SKIP_DIR_NAMES for part in path.parts): + continue + if path.suffix in TEXT_SUFFIXES: + yield path def has_conflict_markers(path: Path) -> bool: @@ -696,7 +706,7 @@ def report_as_text(root: Path, reports: list[DirectoryReport]) -> str: def main() -> int: args = parse_args() - root = Path(args.root).resolve() + root = safe_user_path(args.root).resolve() dirs, discovery_issues = discover_dirs(args, root) reports: list[DirectoryReport] = [] if discovery_issues: diff --git a/antigravity-awesome-skills/skills/expo-ui/scripts/list-components.js b/antigravity-awesome-skills/skills/expo-ui/scripts/list-components.js index 3a64f7d1..97ced1f6 100644 --- a/antigravity-awesome-skills/skills/expo-ui/scripts/list-components.js +++ b/antigravity-awesome-skills/skills/expo-ui/scripts/list-components.js @@ -15,8 +15,12 @@ const fs = require('fs'); const path = require('path'); +const sanitizeFilename = require('sanitize-filename'); -const projectPath = process.argv[2]; +const rawProjectPath = process.argv[2]; +const projectPath = rawProjectPath + ? path.resolve(process.cwd(), sanitizeFilename(path.basename(rawProjectPath))) + : null; const withDocs = process.argv.includes('--docs'); if (!projectPath) { diff --git a/antigravity-awesome-skills/skills/frontend-slides/scripts/extract-pptx.py b/antigravity-awesome-skills/skills/frontend-slides/scripts/extract-pptx.py index 498e2a5e..b4b949de 100644 --- a/antigravity-awesome-skills/skills/frontend-slides/scripts/extract-pptx.py +++ b/antigravity-awesome-skills/skills/frontend-slides/scripts/extract-pptx.py @@ -13,6 +13,20 @@ import json import os import sys from pptx import Presentation +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path def extract_pptx(file_path, output_dir="."): @@ -54,7 +68,7 @@ def extract_pptx(file_path, output_dir="."): image_name = f"slide{slide_num + 1}_img{len(slide_data['images']) + 1}.{image_ext}" image_path = os.path.join(assets_dir, image_name) - with open(image_path, "wb") as f: + with safe_user_path(image_path).open("wb") as f: f.write(image_bytes) slide_data["images"].append( @@ -81,14 +95,14 @@ if __name__ == "__main__": sys.exit(1) input_file = sys.argv[1] - output_dir = sys.argv[2] if len(sys.argv) > 2 else "." + output_dir = safe_user_path(sys.argv[2]) if len(sys.argv) > 2 else "." slides = extract_pptx(input_file, output_dir) # Write extracted data as JSON output_path = os.path.join(output_dir, "extracted-slides.json") - with open(output_path, "w") as f: - json.dump(slides, f, indent=2) + with safe_user_path(output_path).open("w") as f: + f.write(json.dumps(slides, indent=2)) print(f"Extracted {len(slides)} slides to {output_path}") for s in slides: diff --git a/antigravity-awesome-skills/skills/gemini-omni-flash-api/scripts/video/generate_video.py b/antigravity-awesome-skills/skills/gemini-omni-flash-api/scripts/video/generate_video.py index 28ec7964..2c90e88e 100644 --- a/antigravity-awesome-skills/skills/gemini-omni-flash-api/scripts/video/generate_video.py +++ b/antigravity-awesome-skills/skills/gemini-omni-flash-api/scripts/video/generate_video.py @@ -22,6 +22,20 @@ from google import genai # Load local upload helper logic inline to prevent dependency issues sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from upload_file import upload_file, wait_for_active +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path def get_api_key(args): """Retrieves API key from command args or environment.""" @@ -58,6 +72,14 @@ def normalize_file_uri(uri): return f"https://generativelanguage.googleapis.com/files/{file_id}" return uri + +def media_download_url(file_uri): + """Build a media URL only for validated Gemini File API references.""" + file_id = extract_file_id(file_uri) + if not file_id: + raise ValueError("Generated video URI must be a Gemini File API reference.") + return f"https://generativelanguage.googleapis.com/files/{file_id}?alt=media" + def slugify(text): """Converts a text prompt into a safe, descriptive filename slug.""" text = text.lower() @@ -158,7 +180,7 @@ def resolve_or_upload_asset(asset_path, mime_type, api_key, strip_audio=False): # Clean up temporary stripped file if we created one if temp_stripped_path and os.path.exists(temp_stripped_path): try: - os.remove(temp_stripped_path) + safe_user_path(temp_stripped_path).unlink() print(f"Cleaned up temporary video file: {temp_stripped_path}") except Exception as e: print(f"Warning: Failed to remove temporary file {temp_stripped_path}: {e}", file=sys.stderr) @@ -171,8 +193,7 @@ def resolve_or_upload_asset(asset_path, mime_type, api_key, strip_audio=False): def download_video_file(file_uri, output_path, api_key): """Downloads generated video file from URI using alt=media standard in a memory-safe, chunked manner.""" - separator = "&" if "?" in file_uri else "?" - download_url = f"{file_uri}{separator}alt=media" + download_url = media_download_url(file_uri) print(f"Downloading video from {file_uri} to {output_path} in chunked mode...") req = urllib.request.Request(download_url) @@ -184,7 +205,7 @@ def download_video_file(file_uri, output_path, api_key): if parent_dir: os.makedirs(parent_dir, exist_ok=True) - with open(output_path, "wb") as f: + with safe_user_path(output_path).open("wb") as f: while True: chunk = resp.read(8192) if not chunk: @@ -357,7 +378,7 @@ def main(): print(f"Error: Batch JSON file '{args.batch}' not found.", file=sys.stderr) sys.exit(1) try: - with open(args.batch, "r", encoding="utf-8") as f: + with safe_user_path(args.batch).open("r", encoding="utf-8") as f: jobs = json.load(f) if not isinstance(jobs, list): print("Error: Batch JSON file must contain a list/array of job objects.", file=sys.stderr) @@ -375,7 +396,7 @@ def main(): sys.exit(1) jobs = [] - with open(args.prompts_file, "r", encoding="utf-8") as f: + with safe_user_path(args.prompts_file).open("r", encoding="utf-8") as f: for line in f: line = line.strip() if line and not line.startswith("#"): diff --git a/antigravity-awesome-skills/skills/hugging-face-jobs/scripts/finepdfs-stats.py b/antigravity-awesome-skills/skills/hugging-face-jobs/scripts/finepdfs-stats.py index 989732b6..401c0167 100644 --- a/antigravity-awesome-skills/skills/hugging-face-jobs/scripts/finepdfs-stats.py +++ b/antigravity-awesome-skills/skills/hugging-face-jobs/scripts/finepdfs-stats.py @@ -42,6 +42,19 @@ import sys import time from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + import polars as pl from ascii_graph import Pyasciigraph from datasets import Dataset @@ -401,7 +414,7 @@ def main(): print("The 'text' column is never loaded, making this very fast.\n") # Create output directory - output_dir = Path(args.output_dir) + output_dir = safe_user_path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) # Single scan: compute temporal stats diff --git a/antigravity-awesome-skills/skills/hugging-face-model-trainer/scripts/convert_to_gguf.py b/antigravity-awesome-skills/skills/hugging-face-model-trainer/scripts/convert_to_gguf.py index 151ad396..1feb4494 100644 --- a/antigravity-awesome-skills/skills/hugging-face-model-trainer/scripts/convert_to_gguf.py +++ b/antigravity-awesome-skills/skills/hugging-face-model-trainer/scripts/convert_to_gguf.py @@ -84,6 +84,7 @@ def run_command(cmd, description): cmd, check=True, capture_output=True, + shell=False, text=True ) if result.stdout: @@ -114,6 +115,14 @@ def require_hf_repo_id(value, name): sys.exit(1) +def safe_filename_component(value, name): + """Allow repo-name text only where it becomes a local filename component.""" + if not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", value): + print(f" Invalid {name}: {value!r}. Use letters, numbers, dots, dashes, or underscores.", file=sys.stderr) + sys.exit(1) + return value + + def env_flag(name): return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"} @@ -230,7 +239,7 @@ gguf_output_dir = "/tmp/gguf_output" os.makedirs(gguf_output_dir, exist_ok=True) convert_script = "/tmp/llama.cpp/convert_hf_to_gguf.py" -model_name = ADAPTER_MODEL.split('/')[-1] +model_name = safe_filename_component(ADAPTER_MODEL.split('/')[-1], "ADAPTER_MODEL repo name") gguf_file = f"{gguf_output_dir}/{model_name}-f16.gguf" print(f" Running conversion...") diff --git a/antigravity-awesome-skills/skills/hugo-to-markdown/scripts/inventory_hugo_rules.py b/antigravity-awesome-skills/skills/hugo-to-markdown/scripts/inventory_hugo_rules.py index b91230ca..60d62769 100644 --- a/antigravity-awesome-skills/skills/hugo-to-markdown/scripts/inventory_hugo_rules.py +++ b/antigravity-awesome-skills/skills/hugo-to-markdown/scripts/inventory_hugo_rules.py @@ -6,6 +6,19 @@ import re from collections import Counter, defaultdict from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + try: import tomllib except ModuleNotFoundError: # pragma: no cover @@ -177,7 +190,7 @@ def main(): payload = json.dumps(result, indent=2, sort_keys=True) if args.output: - output = Path(args.output) + output = safe_user_path(args.output) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(payload + "\n", encoding="utf-8") else: diff --git a/antigravity-awesome-skills/skills/instagram/scripts/export.py b/antigravity-awesome-skills/skills/instagram/scripts/export.py index 3356fa1a..7a790b36 100644 --- a/antigravity-awesome-skills/skills/instagram/scripts/export.py +++ b/antigravity-awesome-skills/skills/instagram/scripts/export.py @@ -17,6 +17,19 @@ import sys from datetime import datetime, timezone from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + sys.path.insert(0, str(Path(__file__).parent)) _db = None @@ -55,11 +68,9 @@ def export_json(records: list, output_dir: Path, name: str) -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"instagram_{name}_{ts}.json" - with open(path, "w", encoding="utf-8") as f: - json.dump( - {"exported_at": datetime.now(timezone.utc).isoformat(), "total": len(records), "data": records}, - f, ensure_ascii=False, indent=2, - ) + payload = {"exported_at": datetime.now(timezone.utc).isoformat(), "total": len(records), "data": records} + with safe_user_path(path).open("w", encoding="utf-8") as f: + f.write(json.dumps(payload, ensure_ascii=False, indent=2)) print(f"[JSON] {len(records)} registros ->{path}") return path @@ -68,7 +79,7 @@ def export_jsonl(records: list, output_dir: Path, name: str) -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"instagram_{name}_{ts}.jsonl" - with open(path, "w", encoding="utf-8") as f: + with safe_user_path(path).open("w", encoding="utf-8") as f: for rec in records: f.write(json.dumps(rec, ensure_ascii=False) + "\n") print(f"[JSONL] {len(records)} registros ->{path}") @@ -82,7 +93,7 @@ def export_csv_file(records: list, output_dir: Path, name: str) -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"instagram_{name}_{ts}.csv" - with open(path, "w", newline="", encoding="utf-8-sig") as f: + with safe_user_path(path).open("w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=list(records[0].keys()), extrasaction="ignore") writer.writeheader() writer.writerows(records) diff --git a/antigravity-awesome-skills/skills/junta-leiloeiros/scripts/export.py b/antigravity-awesome-skills/skills/junta-leiloeiros/scripts/export.py index 8c3f24b6..dd7d2308 100644 --- a/antigravity-awesome-skills/skills/junta-leiloeiros/scripts/export.py +++ b/antigravity-awesome-skills/skills/junta-leiloeiros/scripts/export.py @@ -18,6 +18,19 @@ import json import sys from datetime import datetime, timezone from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import List, Optional sys.path.insert(0, str(Path(__file__).parent)) @@ -31,13 +44,9 @@ def export_json(records: list, output_dir: Path, suffix: str = "") -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"leiloeiros{suffix}_{ts}.json" - with open(path, "w", encoding="utf-8") as f: - json.dump( - {"exported_at": datetime.now(timezone.utc).isoformat(), "total": len(records), "data": records}, - f, - ensure_ascii=False, - indent=2, - ) + payload = {"exported_at": datetime.now(timezone.utc).isoformat(), "total": len(records), "data": records} + with safe_user_path(path).open("w", encoding="utf-8") as f: + f.write(json.dumps(payload, ensure_ascii=False, indent=2)) print(f"[JSON] {len(records)} registros → {path}") return path @@ -46,7 +55,7 @@ def export_jsonl(records: list, output_dir: Path, suffix: str = "") -> Path: output_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"leiloeiros{suffix}_{ts}.jsonl" - with open(path, "w", encoding="utf-8") as f: + with safe_user_path(path).open("w", encoding="utf-8") as f: for rec in records: f.write(json.dumps(rec, ensure_ascii=False) + "\n") print(f"[JSONL] {len(records)} registros → {path}") @@ -62,7 +71,7 @@ def export_csv(records: list, output_dir: Path, suffix: str = "") -> Path: ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") path = output_dir / f"leiloeiros{suffix}_{ts}.csv" - with open(path, "w", newline="", encoding="utf-8-sig") as f: + with safe_user_path(path).open("w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=list(records[0].keys()), extrasaction="ignore") writer.writeheader() writer.writerows(records) @@ -106,7 +115,7 @@ def main(): db = Database() db.init() - output_dir = Path(args.output) + output_dir = safe_user_path(args.output) estados = [e.upper() for e in args.estado] if args.estado else None if estados: diff --git a/antigravity-awesome-skills/skills/landing-page-generator/scripts/landing_page_scaffolder.py b/antigravity-awesome-skills/skills/landing-page-generator/scripts/landing_page_scaffolder.py index cf071862..5cbf39b5 100644 --- a/antigravity-awesome-skills/skills/landing-page-generator/scripts/landing_page_scaffolder.py +++ b/antigravity-awesome-skills/skills/landing-page-generator/scripts/landing_page_scaffolder.py @@ -16,6 +16,20 @@ import sys from typing import Dict, List, Any, Optional from datetime import datetime import html as html_module +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path def escape(text: str) -> str: @@ -557,7 +571,7 @@ def main(): output = generate_html(config) if args.output: - with open(args.output, "w") as f: + with safe_user_path(args.output).open("w") as f: f.write(output) print(f"Landing page written to {args.output}") else: diff --git a/antigravity-awesome-skills/skills/matematico-tao/scripts/complexity_analyzer.py b/antigravity-awesome-skills/skills/matematico-tao/scripts/complexity_analyzer.py index 3358305b..c1cb4ed7 100644 --- a/antigravity-awesome-skills/skills/matematico-tao/scripts/complexity_analyzer.py +++ b/antigravity-awesome-skills/skills/matematico-tao/scripts/complexity_analyzer.py @@ -18,6 +18,19 @@ import json import argparse from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # Fix Unicode output on Windows (cp1252 terminal) if sys.platform == 'win32': try: @@ -498,7 +511,7 @@ def main(): analyzer.print_report(report) if args.output: - output_path = Path(args.output) + output_path = safe_user_path(args.output) if args.json: output_path.write_text(json.dumps(report, indent=2, ensure_ascii=False)) else: diff --git a/antigravity-awesome-skills/skills/matematico-tao/scripts/dependency_graph.py b/antigravity-awesome-skills/skills/matematico-tao/scripts/dependency_graph.py index d194a369..ac2131c2 100644 --- a/antigravity-awesome-skills/skills/matematico-tao/scripts/dependency_graph.py +++ b/antigravity-awesome-skills/skills/matematico-tao/scripts/dependency_graph.py @@ -16,6 +16,19 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from dataclasses import dataclass, field from typing import Dict, List, Set, Optional, Tuple @@ -518,14 +531,14 @@ def main(): output = analyzer.to_dot() print(output) if args.output: - Path(args.output).write_text(output) + safe_user_path(args.output).write_text(output) print(f"\n✅ Arquivo DOT salvo: {args.output}") print(" Para visualizar: dot -Tpng deps.dot -o deps.png") else: analyzer.print_report(report) if args.output and args.format != 'dot': - Path(args.output).write_text( + safe_user_path(args.output).write_text( json.dumps(report, indent=2, ensure_ascii=False), encoding='utf-8' ) diff --git a/antigravity-awesome-skills/skills/mobile-design/scripts/mobile_audit.py b/antigravity-awesome-skills/skills/mobile-design/scripts/mobile_audit.py index f1345239..a9018e21 100644 --- a/antigravity-awesome-skills/skills/mobile-design/scripts/mobile_audit.py +++ b/antigravity-awesome-skills/skills/mobile-design/scripts/mobile_audit.py @@ -71,6 +71,19 @@ import re import json from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + class MobileAuditor: def __init__(self): self.issues = [] @@ -612,11 +625,12 @@ class MobileAuditor: def audit_directory(self, directory: str) -> None: extensions = {'.tsx', '.ts', '.jsx', '.js', '.dart'} - for root, dirs, files in os.walk(directory): - dirs[:] = [d for d in dirs if d not in {'node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', 'build', '.idea'}] - for file in files: - if Path(file).suffix in extensions: - self.audit_file(os.path.join(root, file)) + skipped = {'node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', 'build', '.idea'} + for path in safe_user_path(directory).rglob("*"): + if not path.is_file() or any(part in skipped for part in path.parts): + continue + if path.suffix in extensions: + self.audit_file(str(path)) def get_report(self): return { @@ -633,7 +647,7 @@ def main(): print("Usage: python mobile_audit.py ") sys.exit(1) - path = sys.argv[1] + path = safe_user_path(sys.argv[1]) is_json = "--json" in sys.argv auditor = MobileAuditor() diff --git a/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/_safe_paths.py b/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/_safe_paths.py +++ b/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery/_safe_paths.py b/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery/_safe_paths.py +++ b/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/bigquery/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/databricks/_safe_paths.py b/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/databricks/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/databricks/_safe_paths.py +++ b/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/databricks/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/hive/_safe_paths.py b/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/hive/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/hive/_safe_paths.py +++ b/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/hive/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/redshift/_safe_paths.py b/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/redshift/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/redshift/_safe_paths.py +++ b/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/redshift/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/snowflake/_safe_paths.py b/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/snowflake/_safe_paths.py index fc48c7fb..499b27a8 100644 --- a/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/snowflake/_safe_paths.py +++ b/antigravity-awesome-skills/skills/monte-carlo-push-ingestion/scripts/templates/snowflake/_safe_paths.py @@ -11,14 +11,6 @@ def _allow_external_paths() -> bool: return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"} -def _is_relative_to(path: Path, root: Path) -> bool: - try: - path.relative_to(root) - return True - except ValueError: - return False - - def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path: value = str(raw_path).strip() if not value or "\0" in value: @@ -26,8 +18,13 @@ def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_pare base = Path.cwd().resolve() candidate = Path(value).expanduser() resolved = (candidate if candidate.is_absolute() else base / candidate).resolve() - if not _allow_external_paths() and not _is_relative_to(resolved, base): - raise ValueError(f"Path must stay under the current working directory: {raw_path!r}") + if not _allow_external_paths(): + try: + resolved.relative_to(base) + except ValueError as exc: + raise ValueError( + f"Path must stay under the current working directory: {raw_path!r}" + ) from exc if expect_file and not resolved.is_file(): raise FileNotFoundError(f"Input file not found: {resolved}") if create_parent: @@ -62,5 +59,5 @@ def read_json_file(raw_path: str): def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None: - with safe_output_json_path(raw_path).open("w") as fh: - json.dump(payload, fh, indent=indent, default=default) + output_path = safe_output_json_path(raw_path) + output_path.write_text(json.dumps(payload, indent=indent, default=default), encoding="utf-8") diff --git a/antigravity-awesome-skills/skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py b/antigravity-awesome-skills/skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py index daf10e23..8894bfc1 100755 --- a/antigravity-awesome-skills/skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py +++ b/antigravity-awesome-skills/skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py @@ -12,6 +12,19 @@ import argparse import re import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional, Tuple, Union import yaml @@ -141,8 +154,8 @@ def main() -> None: args = parser.parse_args() - dbt_project_path = Path(args.dbt_project_path) - model_path = Path(args.model_path) + dbt_project_path = safe_user_path(args.dbt_project_path) + model_path = safe_user_path(args.model_path) if not dbt_project_path.exists(): print(f"Error: dbt_project.yml not found: {dbt_project_path}", file=sys.stderr) diff --git a/antigravity-awesome-skills/skills/pdf-official/scripts/create_validation_image.py b/antigravity-awesome-skills/skills/pdf-official/scripts/create_validation_image.py index 4913f8f8..5cadf03f 100644 --- a/antigravity-awesome-skills/skills/pdf-official/scripts/create_validation_image.py +++ b/antigravity-awesome-skills/skills/pdf-official/scripts/create_validation_image.py @@ -2,6 +2,20 @@ import json import sys from PIL import Image, ImageDraw +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path # Creates "validation" images with rectangles for the bounding box information that @@ -35,7 +49,7 @@ if __name__ == "__main__": print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]") sys.exit(1) page_number = int(sys.argv[1]) - fields_json_path = sys.argv[2] - input_image_path = sys.argv[3] - output_image_path = sys.argv[4] + fields_json_path = safe_user_path(sys.argv[2]) + input_image_path = safe_user_path(sys.argv[3]) + output_image_path = safe_user_path(sys.argv[4]) create_validation_image(page_number, fields_json_path, input_image_path, output_image_path) diff --git a/antigravity-awesome-skills/skills/pdf-official/scripts/extract_form_field_info.py b/antigravity-awesome-skills/skills/pdf-official/scripts/extract_form_field_info.py index f42a2df8..90c51ed1 100644 --- a/antigravity-awesome-skills/skills/pdf-official/scripts/extract_form_field_info.py +++ b/antigravity-awesome-skills/skills/pdf-official/scripts/extract_form_field_info.py @@ -141,7 +141,7 @@ def write_field_info(pdf_path: str, json_output_path: str): reader = PdfReader(pdf_path) field_info = get_field_info(reader) with open(json_output_path, "w") as f: - json.dump(field_info, f, indent=2) + f.write(json.dumps(field_info, indent=2)) print(f"Wrote {len(field_info)} fields to {json_output_path}") diff --git a/antigravity-awesome-skills/skills/playwright-skill/run.js b/antigravity-awesome-skills/skills/playwright-skill/run.js index 10f26168..008a9d6c 100755 --- a/antigravity-awesome-skills/skills/playwright-skill/run.js +++ b/antigravity-awesome-skills/skills/playwright-skill/run.js @@ -13,10 +13,28 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); +const sanitizeFilename = require('sanitize-filename'); // Change to skill directory for proper module resolution process.chdir(__dirname); +function safeUserPath(pathValue, baseDir = process.cwd()) { + const root = path.resolve(baseDir); + const segments = String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); + const target = path.resolve(root, ...segments); + const rel = path.relative(root, target); + if (rel.startsWith('..') || path.isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + /** * Check if Playwright is installed */ @@ -54,7 +72,7 @@ function getCodeToExecute() { // Case 1: File path provided if (args.length > 0 && fs.existsSync(args[0])) { - const filePath = path.resolve(args[0]); + const filePath = safeUserPath(args[0]); console.log(`📄 Executing file: ${filePath}`); return fs.readFileSync(filePath, 'utf8'); } diff --git a/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/pack.py b/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/pack.py index 1145107a..630622f0 100755 --- a/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/pack.py +++ b/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/pack.py @@ -16,6 +16,19 @@ import zipfile from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def validate_input_tree(input_dir: Path): root = input_dir.resolve(strict=True) for path in input_dir.rglob("*"): @@ -27,6 +40,18 @@ def validate_input_tree(input_dir: Path): raise ValueError(f"Refusing to pack path outside input directory: {path}") from None +def copy_tree_contents(source_dir: Path, target_dir: Path) -> None: + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def main(): parser = argparse.ArgumentParser(description="Pack a directory into an Office file") parser.add_argument("input_directory", help="Unpacked Office document directory") @@ -65,7 +90,7 @@ def pack_document(input_dir, output_file, validate=False): bool: True if successful, False if validation failed """ input_dir = Path(input_dir) - output_file = Path(output_file) + output_file = safe_user_path(output_file) if not input_dir.is_dir(): raise ValueError(f"{input_dir} is not a directory") @@ -76,7 +101,7 @@ def pack_document(input_dir, output_file, validate=False): # Work in temporary directory to avoid modifying original with tempfile.TemporaryDirectory() as temp_dir: temp_content_dir = Path(temp_dir) / "content" - shutil.copytree(input_dir, temp_content_dir) + copy_tree_contents(input_dir, temp_content_dir) # Process XML files to remove pretty-printing whitespace for pattern in ["*.xml", "*.rels"]: @@ -85,10 +110,12 @@ def pack_document(input_dir, output_file, validate=False): # Create final Office file as zip archive output_file.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + temp_zip_path = Path(temp_dir) / "office.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for f in temp_content_dir.rglob("*"): if f.is_file(): zf.write(f, f.relative_to(temp_content_dir)) + output_file.write_bytes(temp_zip_path.read_bytes()) # Validate if requested if validate: diff --git a/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/unpack.py b/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/unpack.py index 33ed3a35..ef9d69d3 100755 --- a/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/unpack.py +++ b/antigravity-awesome-skills/skills/pptx-official/ooxml/scripts/unpack.py @@ -8,6 +8,19 @@ import sys import zipfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + MAX_ARCHIVE_MEMBERS = 5000 MAX_MEMBER_SIZE = 100 * 1024 * 1024 MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024 @@ -30,7 +43,7 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro return destination.parent.mkdir(parents=True, exist_ok=True) - with archive.open(member, "r") as source, open(destination, "wb") as target: + with archive.open(member, "r") as source, safe_user_path(destination).open("wb") as target: shutil.copyfileobj(source, target) @@ -57,7 +70,7 @@ def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path): def extract_archive_safely(input_file: str | Path, output_dir: str | Path): - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) output_path.mkdir(parents=True, exist_ok=True) output_root = output_path.resolve() @@ -82,7 +95,7 @@ def main(argv: list[str] | None = None): raise SystemExit("Usage: python unpack.py ") input_file, output_dir = argv - output_path = Path(output_dir) + output_path = safe_user_path(output_dir) extract_archive_safely(input_file, output_path) pretty_print_xml(output_path) diff --git a/antigravity-awesome-skills/skills/pptx-official/scripts/inventory.py b/antigravity-awesome-skills/skills/pptx-official/scripts/inventory.py index edda390e..c39ba9b0 100755 --- a/antigravity-awesome-skills/skills/pptx-official/scripts/inventory.py +++ b/antigravity-awesome-skills/skills/pptx-official/scripts/inventory.py @@ -28,6 +28,19 @@ import platform import sys from dataclasses import dataclass from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any, Dict, List, Optional, Tuple, Union from PIL import Image, ImageDraw, ImageFont @@ -79,7 +92,7 @@ The output JSON includes: args = parser.parse_args() - input_path = Path(args.input) + input_path = safe_user_path(args.input) if not input_path.exists(): print(f"Error: Input file not found: {args.input}") sys.exit(1) @@ -96,7 +109,7 @@ The output JSON includes: ) inventory = extract_text_inventory(input_path, issues_only=args.issues_only) - output_path = Path(args.output) + output_path = safe_user_path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) save_inventory(inventory, output_path) @@ -1012,8 +1025,8 @@ def save_inventory(inventory: InventoryData, output_path: Path) -> None: shape_key: shape_data.to_dict() for shape_key, shape_data in shapes.items() } - with open(output_path, "w", encoding="utf-8") as f: - json.dump(json_inventory, f, indent=2, ensure_ascii=False) + with safe_user_path(output_path).open("w", encoding="utf-8") as f: + f.write(json.dumps(json_inventory, indent=2, ensure_ascii=False)) if __name__ == "__main__": diff --git a/antigravity-awesome-skills/skills/pptx-official/scripts/rearrange.py b/antigravity-awesome-skills/skills/pptx-official/scripts/rearrange.py index 2519911f..fb54876c 100755 --- a/antigravity-awesome-skills/skills/pptx-official/scripts/rearrange.py +++ b/antigravity-awesome-skills/skills/pptx-official/scripts/rearrange.py @@ -15,6 +15,19 @@ import sys from copy import deepcopy from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + import six from pptx import Presentation @@ -53,13 +66,13 @@ Note: Slide indices are 0-based (first slide is 0, second is 1, etc.) sys.exit(1) # Check template exists - template_path = Path(args.template) + template_path = safe_user_path(args.template) if not template_path.exists(): print(f"Error: Template file not found: {args.template}") sys.exit(1) # Create output directory if needed - output_path = Path(args.output) + output_path = safe_user_path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) try: diff --git a/antigravity-awesome-skills/skills/pptx-official/scripts/replace.py b/antigravity-awesome-skills/skills/pptx-official/scripts/replace.py index 8f7a8b1b..0098a359 100755 --- a/antigravity-awesome-skills/skills/pptx-official/scripts/replace.py +++ b/antigravity-awesome-skills/skills/pptx-official/scripts/replace.py @@ -12,6 +12,19 @@ unless "paragraphs" is specified in the replacements for that shape. import json import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Any, Dict, List from inventory import InventoryData, extract_text_inventory @@ -359,9 +372,9 @@ def main(): print(__doc__) sys.exit(1) - input_pptx = Path(sys.argv[1]) - replacements_json = Path(sys.argv[2]) - output_pptx = Path(sys.argv[3]) + input_pptx = safe_user_path(sys.argv[1]) + replacements_json = safe_user_path(sys.argv[2]) + output_pptx = safe_user_path(sys.argv[3]) if not input_pptx.exists(): print(f"Error: Input file '{input_pptx}' not found") diff --git a/antigravity-awesome-skills/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py b/antigravity-awesome-skills/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py index cd56a8d2..e1f1f230 100644 --- a/antigravity-awesome-skills/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py +++ b/antigravity-awesome-skills/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py @@ -8,6 +8,20 @@ import re from typing import Dict, List, Tuple, Set from collections import Counter, defaultdict import json +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path class InterviewAnalyzer: """Analyze customer interviews for insights and patterns""" @@ -424,7 +438,7 @@ def main(): sys.exit(1) # Read interview transcript - with open(sys.argv[1], 'r') as f: + with safe_user_path(sys.argv[1]).open('r') as f: interview_text = f.read() # Analyze diff --git a/antigravity-awesome-skills/skills/remote-gpu-trainer/scripts/verify_local.py b/antigravity-awesome-skills/skills/remote-gpu-trainer/scripts/verify_local.py index 2b6f56de..1b451b72 100644 --- a/antigravity-awesome-skills/skills/remote-gpu-trainer/scripts/verify_local.py +++ b/antigravity-awesome-skills/skills/remote-gpu-trainer/scripts/verify_local.py @@ -22,6 +22,19 @@ import sys from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("ckpt_dir", help="Directory containing ablation subdirs (each with best.pth + best_metrics.json)") @@ -33,7 +46,7 @@ def main() -> int: "needed only when a checkpoint pickles non-tensor objects (e.g. an args Namespace); OFF by default") args = ap.parse_args() - root = Path(args.ckpt_dir) + root = safe_user_path(args.ckpt_dir) if not root.exists(): print(f"ERROR: {root} does not exist") return 1 diff --git a/antigravity-awesome-skills/skills/senior-architect/scripts/architecture_diagram_generator.py b/antigravity-awesome-skills/skills/senior-architect/scripts/architecture_diagram_generator.py index 7924e3a7..cf90ddc5 100755 --- a/antigravity-awesome-skills/skills/senior-architect/scripts/architecture_diagram_generator.py +++ b/antigravity-awesome-skills/skills/senior-architect/scripts/architecture_diagram_generator.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class ArchitectureDiagramGenerator: """Main class for architecture diagram generator functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/skills/senior-architect/scripts/dependency_analyzer.py b/antigravity-awesome-skills/skills/senior-architect/scripts/dependency_analyzer.py index c731c9f3..3a864168 100755 --- a/antigravity-awesome-skills/skills/senior-architect/scripts/dependency_analyzer.py +++ b/antigravity-awesome-skills/skills/senior-architect/scripts/dependency_analyzer.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class DependencyAnalyzer: """Main class for dependency analyzer functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/skills/senior-architect/scripts/project_architect.py b/antigravity-awesome-skills/skills/senior-architect/scripts/project_architect.py index 740c4389..9d6d2da3 100755 --- a/antigravity-awesome-skills/skills/senior-architect/scripts/project_architect.py +++ b/antigravity-awesome-skills/skills/senior-architect/scripts/project_architect.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class ProjectArchitect: """Main class for project architect functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/skills/senior-frontend/scripts/bundle_analyzer.py b/antigravity-awesome-skills/skills/senior-frontend/scripts/bundle_analyzer.py index fc364888..8098f312 100644 --- a/antigravity-awesome-skills/skills/senior-frontend/scripts/bundle_analyzer.py +++ b/antigravity-awesome-skills/skills/senior-frontend/scripts/bundle_analyzer.py @@ -17,6 +17,19 @@ import os import re import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional, Any, Tuple @@ -375,7 +388,7 @@ def main(): ) args = parser.parse_args() - project_dir = Path(args.project_dir).resolve() + project_dir = safe_user_path(args.project_dir).resolve() if not project_dir.exists(): print(f"Error: Directory not found: {project_dir}", file=sys.stderr) diff --git a/antigravity-awesome-skills/skills/senior-frontend/scripts/frontend_scaffolder.py b/antigravity-awesome-skills/skills/senior-frontend/scripts/frontend_scaffolder.py index ecc826c8..57d03bfb 100644 --- a/antigravity-awesome-skills/skills/senior-frontend/scripts/frontend_scaffolder.py +++ b/antigravity-awesome-skills/skills/senior-frontend/scripts/frontend_scaffolder.py @@ -16,6 +16,19 @@ import json import os import sys from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional @@ -989,7 +1002,7 @@ def main(): result = scaffold_project( name=args.name, - output_dir=Path(args.dir), + output_dir=safe_user_path(args.dir), template=args.template, features=features, dry_run=args.dry_run, diff --git a/antigravity-awesome-skills/skills/senior-fullstack/scripts/code_quality_analyzer.py b/antigravity-awesome-skills/skills/senior-fullstack/scripts/code_quality_analyzer.py index 1ddfaa77..82456a14 100755 --- a/antigravity-awesome-skills/skills/senior-fullstack/scripts/code_quality_analyzer.py +++ b/antigravity-awesome-skills/skills/senior-fullstack/scripts/code_quality_analyzer.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class CodeQualityAnalyzer: """Main class for code quality analyzer functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/skills/senior-fullstack/scripts/fullstack_scaffolder.py b/antigravity-awesome-skills/skills/senior-fullstack/scripts/fullstack_scaffolder.py index 3f09b5c3..95aeff0d 100755 --- a/antigravity-awesome-skills/skills/senior-fullstack/scripts/fullstack_scaffolder.py +++ b/antigravity-awesome-skills/skills/senior-fullstack/scripts/fullstack_scaffolder.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class FullstackScaffolder: """Main class for fullstack scaffolder functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/skills/senior-fullstack/scripts/project_scaffolder.py b/antigravity-awesome-skills/skills/senior-fullstack/scripts/project_scaffolder.py index 6a080956..cf0b526b 100755 --- a/antigravity-awesome-skills/skills/senior-fullstack/scripts/project_scaffolder.py +++ b/antigravity-awesome-skills/skills/senior-fullstack/scripts/project_scaffolder.py @@ -9,13 +9,26 @@ import sys import json import argparse from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from typing import Dict, List, Optional class ProjectScaffolder: """Main class for project scaffolder functionality""" def __init__(self, target_path: str, verbose: bool = False): - self.target_path = Path(target_path) + self.target_path = safe_user_path(target_path) self.verbose = verbose self.results = {} @@ -104,7 +117,7 @@ def main(): if args.json: output = json.dumps(results, indent=2) if args.output: - with open(args.output, 'w') as f: + with safe_user_path(args.output).open('w') as f: f.write(output) print(f"Results written to {args.output}") else: diff --git a/antigravity-awesome-skills/skills/skill-creator/scripts/init_skill.py b/antigravity-awesome-skills/skills/skill-creator/scripts/init_skill.py index 329ad4e5..82979d80 100755 --- a/antigravity-awesome-skills/skills/skill-creator/scripts/init_skill.py +++ b/antigravity-awesome-skills/skills/skill-creator/scripts/init_skill.py @@ -15,6 +15,19 @@ import sys from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + SKILL_TEMPLATE = """--- name: {skill_name} description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.] @@ -203,7 +216,7 @@ def init_skill(skill_name, path): Path to created skill directory, or None if error """ # Determine skill directory path - skill_dir = Path(path).resolve() / skill_name + skill_dir = safe_user_path(path).resolve() / skill_name # Check if directory already exists if skill_dir.exists(): @@ -285,7 +298,7 @@ def main(): sys.exit(1) skill_name = sys.argv[1] - path = sys.argv[3] + path = safe_user_path(sys.argv[3]) print(f"🚀 Initializing skill: {skill_name}") print(f" Location: {path}") diff --git a/antigravity-awesome-skills/skills/skill-creator/scripts/package_skill.py b/antigravity-awesome-skills/skills/skill-creator/scripts/package_skill.py index 88f58156..7e2b16c3 100755 --- a/antigravity-awesome-skills/skills/skill-creator/scripts/package_skill.py +++ b/antigravity-awesome-skills/skills/skill-creator/scripts/package_skill.py @@ -12,7 +12,21 @@ Example: import sys import zipfile +import tempfile from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path from quick_validate import validate_skill @@ -37,7 +51,7 @@ def package_skill(skill_path, output_dir=None): Returns: Path to the created .skill file, or None if error """ - skill_path = Path(skill_path).resolve() + skill_path = safe_user_path(skill_path).resolve() # Validate skill folder exists if not skill_path.exists(): @@ -66,7 +80,7 @@ def package_skill(skill_path, output_dir=None): # Determine output location skill_name = skill_path.name if output_dir: - output_path = Path(output_dir).resolve() + output_path = safe_user_path(output_dir).resolve() output_path.mkdir(parents=True, exist_ok=True) else: output_path = Path.cwd() @@ -75,14 +89,17 @@ def package_skill(skill_path, output_dir=None): # Create the .skill file (zip format) try: - with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: - # Walk through the skill directory - for file_path in skill_path.rglob('*'): - if should_include(file_path, skill_path): - # Calculate the relative path within the zip - arcname = file_path.relative_to(skill_path.parent) - zipf.write(file_path, arcname) - print(f" Added: {arcname}") + with tempfile.TemporaryDirectory() as temp_dir: + temp_zip_path = Path(temp_dir) / "skill.zip" + with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + # Walk through the skill directory + for file_path in skill_path.rglob('*'): + if should_include(file_path, skill_path): + # Calculate the relative path within the zip + arcname = file_path.relative_to(skill_path.parent) + zipf.write(file_path, arcname) + print(f" Added: {arcname}") + skill_filename.write_bytes(temp_zip_path.read_bytes()) print(f"\n✅ Successfully packaged skill to: {skill_filename}") return skill_filename @@ -100,8 +117,8 @@ def main(): print(" python utils/package_skill.py skills/public/my-skill ./dist") sys.exit(1) - skill_path = sys.argv[1] - output_dir = sys.argv[2] if len(sys.argv) > 2 else None + skill_path = safe_user_path(sys.argv[1]) + output_dir = safe_user_path(sys.argv[2]) if len(sys.argv) > 2 else None print(f"📦 Packaging skill: {skill_path}") if output_dir: diff --git a/antigravity-awesome-skills/skills/skill-creator/scripts/quick_validate.py b/antigravity-awesome-skills/skills/skill-creator/scripts/quick_validate.py index d9fbeb75..9e8f8944 100755 --- a/antigravity-awesome-skills/skills/skill-creator/scripts/quick_validate.py +++ b/antigravity-awesome-skills/skills/skill-creator/scripts/quick_validate.py @@ -9,9 +9,21 @@ import re import yaml from pathlib import Path +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + def validate_skill(skill_path): """Basic validation of a skill""" - skill_path = Path(skill_path) + skill_path = safe_user_path(skill_path) # Check SKILL.md exists skill_md = skill_path / 'SKILL.md' @@ -92,4 +104,4 @@ if __name__ == "__main__": valid, message = validate_skill(sys.argv[1]) print(message) - sys.exit(0 if valid else 1) \ No newline at end of file + sys.exit(0 if valid else 1) diff --git a/antigravity-awesome-skills/skills/skill-installer/scripts/install_skill.py b/antigravity-awesome-skills/skills/skill-installer/scripts/install_skill.py index 4b8af93f..6c09d7a6 100644 --- a/antigravity-awesome-skills/skills/skill-installer/scripts/install_skill.py +++ b/antigravity-awesome-skills/skills/skill-installer/scripts/install_skill.py @@ -33,6 +33,39 @@ import re from pathlib import Path from datetime import datetime + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + + +def copy_tree_contents(source_dir: Path, target_dir: Path, *, ignore=None) -> None: + ignored_by_dir = {} + if ignore is not None: + for current_dir in [source_dir, *[p for p in source_dir.rglob("*") if p.is_dir()]]: + ignored_by_dir[current_dir] = set(ignore(str(current_dir), [p.name for p in current_dir.iterdir()])) + + target_dir.mkdir(parents=True, exist_ok=True) + for source_path in source_dir.rglob("*"): + ignored_names = ignored_by_dir.get(source_path.parent, set()) + if source_path.name in ignored_names: + continue + relative_path = source_path.relative_to(source_dir) + target_path = target_dir / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + # Add scripts directory to path for imports SCRIPT_DIR = Path(__file__).parent.resolve() sys.path.insert(0, str(SCRIPT_DIR)) @@ -147,7 +180,7 @@ def safe_skill_path(root: Path, skill_name: str) -> Path: def resolve_skill_source(source: str) -> Path: """Resolve and validate a local skill source directory.""" - source_path = Path(source).expanduser().resolve() + source_path = safe_user_path(source).expanduser().resolve() if not source_path.is_dir(): raise ValueError(f"Source does not exist or is not a directory: {source_path}") if not (source_path / "SKILL.md").is_file(): @@ -164,7 +197,7 @@ def md5_dir(path: Path, exclude_dirs: set = None) -> str: if exclude_dirs is None: exclude_dirs = {"backups", "staging", ".git", "__pycache__", "node_modules", ".venv"} - root_path = Path(path).resolve(strict=True) + root_path = safe_user_path(path).resolve(strict=True) if not root_path.is_dir(): raise ValueError(f"Hash target must be a directory: {root_path}") @@ -173,7 +206,7 @@ def md5_dir(path: Path, exclude_dirs: set = None) -> str: # Filter out excluded directories dirs[:] = [d for d in dirs if d not in exclude_dirs] for f in sorted(files): - fp = Path(root) / f + fp = safe_user_path(root) / f try: resolved_fp = fp.resolve(strict=True) resolved_fp.relative_to(root_path) @@ -370,7 +403,7 @@ def step4_check_conflicts(skill_name: str) -> dict: def _backup_ignore(directory, contents): """Ignore function for shutil.copytree to skip backup/staging dirs.""" ignored = set() - dir_path = Path(directory) + dir_path = safe_user_path(directory) for item in contents: item_path = dir_path / item if item_path.is_symlink(): @@ -436,7 +469,7 @@ def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict: # Copy to staging first (skip backups/staging to prevent recursion) try: - shutil.copytree(source_path, staging, ignore=_backup_ignore, dirs_exist_ok=True) + copy_tree_contents(source_path, staging, ignore=_backup_ignore) except Exception as e: return {"success": False, "error": f"Copy to staging failed: {e}"} @@ -470,7 +503,7 @@ def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict: except Exception as e: # Try copy + delete as fallback (cross-device moves) try: - shutil.copytree(staging, dest, dirs_exist_ok=True) + copy_tree_contents(staging, dest) shutil.rmtree(staging, ignore_errors=True) except Exception as e2: shutil.rmtree(staging, ignore_errors=True) @@ -496,7 +529,7 @@ def step7_register_claude(skill_name: str) -> dict: # Copy SKILL.md try: - shutil.copy2(source_skill_md, claude_dest_dir / "SKILL.md") + (claude_dest_dir / "SKILL.md").write_bytes(source_skill_md.read_bytes()) except Exception as e: return {"success": False, "error": f"Failed to copy SKILL.md to Claude skills: {e}"} @@ -507,7 +540,7 @@ def step7_register_claude(skill_name: str) -> dict: try: if claude_refs.exists(): shutil.rmtree(claude_refs) - shutil.copytree(refs_dir, claude_refs) + copy_tree_contents(refs_dir, claude_refs) except Exception: pass # Non-critical diff --git a/antigravity-awesome-skills/skills/skill-installer/scripts/package_skill.py b/antigravity-awesome-skills/skills/skill-installer/scripts/package_skill.py index 50beb315..66dbc32c 100644 --- a/antigravity-awesome-skills/skills/skill-installer/scripts/package_skill.py +++ b/antigravity-awesome-skills/skills/skill-installer/scripts/package_skill.py @@ -23,8 +23,22 @@ import sys import json import re import zipfile +import tempfile from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # ── Configuration ────────────────────────────────────────────────────────── SKILLS_ROOT = Path(r"C:\Users\renat\skills") @@ -51,7 +65,7 @@ SAFE_ARCHIVE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$") def resolve_existing_dir(path) -> Path: """Resolve a user-provided directory and require it to exist.""" - resolved = Path(path).expanduser().resolve() + resolved = safe_user_path(path).expanduser().resolve() if not resolved.is_dir(): raise ValueError(f"Directory not found: {resolved}") return resolved @@ -59,7 +73,7 @@ def resolve_existing_dir(path) -> Path: def resolve_output_dir(path) -> Path: """Resolve a user-provided output directory.""" - resolved = Path(path).expanduser().resolve() + resolved = safe_user_path(path).expanduser().resolve() if resolved.exists() and not resolved.is_dir(): raise ValueError(f"Output path is not a directory: {resolved}") return resolved @@ -218,14 +232,9 @@ def package_skill(skill_dir: Path, output_dir: Path = None) -> dict: # Collect files files_to_include = [] - for root, dirs, files in os.walk(skill_dir): - # Filter directories in-place to skip excluded ones - dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS] - - for f in files: - fp = Path(root) / f - if should_include(fp, skill_dir): - files_to_include.append(fp) + for fp in safe_user_path(skill_dir).rglob("*"): + if fp.is_file() and should_include(fp, skill_dir): + files_to_include.append(fp) if not files_to_include: return {"success": False, "error": "No files to package"} @@ -233,13 +242,16 @@ def package_skill(skill_dir: Path, output_dir: Path = None) -> dict: # Create ZIP with skill folder as root # CRITICAL: ZIP paths MUST use forward slashes, not Windows backslashes try: - with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: - for fp in sorted(files_to_include): - rel_path = fp.relative_to(skill_dir) - # Convert Windows backslash to forward slash for ZIP compatibility - rel_posix = rel_path.as_posix() - archive_path = f"{skill_name_lower}/{rel_posix}" - zf.write(fp, archive_path) + with tempfile.TemporaryDirectory() as temp_dir: + temp_zip_path = Path(temp_dir) / "skill.zip" + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf: + for fp in sorted(files_to_include): + rel_path = fp.relative_to(skill_dir) + # Convert Windows backslash to forward slash for ZIP compatibility + rel_posix = rel_path.as_posix() + archive_path = f"{skill_name_lower}/{rel_posix}" + zf.write(fp, archive_path) + zip_path.write_bytes(temp_zip_path.read_bytes()) # Verify ZIP is not empty and valid with zipfile.ZipFile(zip_path, "r") as zf_check: diff --git a/antigravity-awesome-skills/skills/skill-installer/scripts/validate_skill.py b/antigravity-awesome-skills/skills/skill-installer/scripts/validate_skill.py index 957151cd..50a9f2dd 100644 --- a/antigravity-awesome-skills/skills/skill-installer/scripts/validate_skill.py +++ b/antigravity-awesome-skills/skills/skill-installer/scripts/validate_skill.py @@ -17,6 +17,19 @@ import json import re from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + # ── Constants ────────────────────────────────────────────────────────────── FORBIDDEN_PATTERNS = [ @@ -43,7 +56,7 @@ REGISTRY_PATH = SKILLS_ROOT / "agent-orchestrator" / "data" / "registry.json" def resolve_existing_dir(path) -> Path: """Resolve a user-provided directory and require it to exist.""" - resolved = Path(path).expanduser().resolve() + resolved = safe_user_path(path).expanduser().resolve() if not resolved.is_dir(): raise ValueError(f"Directory does not exist: {resolved}") return resolved @@ -222,19 +235,20 @@ def check_forbidden_files(skill_dir: Path) -> dict: """Check 7: No forbidden files (.env, credentials, keys, etc.).""" found_forbidden = [] - for root, _dirs, files in os.walk(skill_dir): - for f in files: - f_lower = f.lower() - for pattern in FORBIDDEN_PATTERNS: - if pattern.startswith("*."): - ext = pattern[1:] # e.g., ".key" - if f_lower.endswith(ext): - found_forbidden.append(os.path.join(root, f)) - break - else: - if f_lower == pattern.lower(): - found_forbidden.append(os.path.join(root, f)) - break + for path in safe_user_path(skill_dir).rglob("*"): + if not path.is_file(): + continue + f_lower = path.name.lower() + for pattern in FORBIDDEN_PATTERNS: + if pattern.startswith("*."): + ext = pattern[1:] # e.g., ".key" + if f_lower.endswith(ext): + found_forbidden.append(str(path)) + break + else: + if f_lower == pattern.lower(): + found_forbidden.append(str(path)) + break if found_forbidden: return { @@ -255,12 +269,13 @@ def check_forbidden_files(skill_dir: Path) -> dict: def check_total_size(skill_dir: Path) -> dict: """Check 8: Total size is reasonable (warn if > 50MB).""" total = 0 - for root, _dirs, files in os.walk(skill_dir): - for f in files: - try: - total += os.path.getsize(os.path.join(root, f)) - except OSError: - pass + for path in safe_user_path(skill_dir).rglob("*"): + if not path.is_file(): + continue + try: + total += path.stat().st_size + except OSError: + pass size_mb = total / (1024 * 1024) ok = size_mb <= MAX_SIZE_MB @@ -360,7 +375,7 @@ def validate(skill_dir: Path, strict: bool = False, registry_path: Path = None) except ValueError as e: return { "valid": False, - "skill_dir": str(Path(skill_dir).expanduser()), + "skill_dir": str(safe_user_path(skill_dir).expanduser()), "checks": [], "warnings": [], "errors": [str(e)], @@ -433,7 +448,7 @@ def main(): if "--registry" in sys.argv: idx = sys.argv.index("--registry") if idx + 1 < len(sys.argv): - registry_path = Path(sys.argv[idx + 1]).expanduser().resolve() + registry_path = safe_user_path(sys.argv[idx + 1]).expanduser().resolve() result = validate(skill_dir, strict=strict, registry_path=registry_path) print(json.dumps(result, indent=2, ensure_ascii=False)) diff --git a/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py b/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py index 768839b4..e8bd3447 100644 --- a/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +++ b/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py @@ -2,10 +2,14 @@ from __future__ import annotations import subprocess -import xml.etree.ElementTree as ET from dataclasses import dataclass from pathlib import Path +try: + from defusedxml import ElementTree as ET +except ImportError: # pragma: no cover - guidance for direct script use + ET = None + @dataclass(frozen=True) class RunInfo: @@ -43,6 +47,8 @@ def toc(trace_path: Path) -> TraceInfo: The TOC is small (a few KB) so we load it fully rather than streaming. """ + if ET is None: + raise RuntimeError("Install defusedxml before parsing xctrace XML exports.") xml_bytes = _run_export(trace_path, ["--toc"]) root = ET.fromstring(xml_bytes) diff --git a/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py b/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py index e18acf0b..07440869 100644 --- a/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +++ b/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py @@ -6,10 +6,14 @@ a global id cache for later ref lookups. """ from __future__ import annotations -import xml.etree.ElementTree as ET from collections.abc import Iterator from dataclasses import dataclass +try: + from defusedxml import ElementTree as ET +except ImportError: # pragma: no cover - guidance for direct script use + ET = None + @dataclass(frozen=True) class Column: @@ -26,6 +30,8 @@ class RowStream: """ def __init__(self, xml_bytes: bytes): + if ET is None: + raise RuntimeError("Install defusedxml before parsing xctrace XML exports.") self._xml = xml_bytes self.columns: list[Column] = [] self._id_cache: dict[str, ET.Element] = {} diff --git a/antigravity-awesome-skills/skills/telegram/scripts/setup_project.py b/antigravity-awesome-skills/skills/telegram/scripts/setup_project.py index ed071420..bf68f05d 100644 --- a/antigravity-awesome-skills/skills/telegram/scripts/setup_project.py +++ b/antigravity-awesome-skills/skills/telegram/scripts/setup_project.py @@ -12,12 +12,27 @@ import argparse import os import shutil import sys +from pathlib import Path SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) SKILL_DIR = os.path.dirname(SCRIPT_DIR) BOILERPLATE_DIR = os.path.join(SKILL_DIR, "assets", "boilerplate") +def copy_tree_contents(source_dir: str, target_dir: str) -> None: + source_root = Path(source_dir) + target_root = Path(target_dir) + target_root.mkdir(parents=True, exist_ok=True) + for source_path in source_root.rglob("*"): + relative_path = source_path.relative_to(source_root) + target_path = target_root / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def setup_nodejs(project_path: str, with_webhook: bool = False, with_ai: bool = False): """Setup Node.js/TypeScript project.""" src_dir = os.path.join(BOILERPLATE_DIR, "nodejs") @@ -27,7 +42,7 @@ def setup_nodejs(project_path: str, with_webhook: bool = False, with_ai: bool = sys.exit(1) # Copy boilerplate - shutil.copytree(src_dir, project_path, dirs_exist_ok=True) + copy_tree_contents(src_dir, project_path) print(f"Node.js project created at: {project_path}") print("\nNext steps:") @@ -52,7 +67,7 @@ def setup_python(project_path: str, with_webhook: bool = False, with_ai: bool = sys.exit(1) # Copy boilerplate - shutil.copytree(src_dir, project_path, dirs_exist_ok=True) + copy_tree_contents(src_dir, project_path) print(f"Python project created at: {project_path}") print("\nNext steps:") diff --git a/antigravity-awesome-skills/skills/videodb/scripts/ws_listener.py b/antigravity-awesome-skills/skills/videodb/scripts/ws_listener.py index 3316de4a..a3d32f0e 100644 --- a/antigravity-awesome-skills/skills/videodb/scripts/ws_listener.py +++ b/antigravity-awesome-skills/skills/videodb/scripts/ws_listener.py @@ -34,6 +34,19 @@ import asyncio from datetime import datetime, timezone from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + from dotenv import load_dotenv load_dotenv() @@ -64,7 +77,7 @@ def parse_args(): state_root = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state")) output_dir = str(state_root / "videodb-events") - return clear, Path(output_dir) + return clear, safe_user_path(output_dir) CLEAR_EVENTS, OUTPUT_DIR = parse_args() EVENTS_FILE = OUTPUT_DIR / "videodb_events.jsonl" @@ -100,7 +113,7 @@ def secure_open(path: Path, *, append: bool): flags |= os.O_APPEND if append else os.O_TRUNC flags |= getattr(os, "O_NOFOLLOW", 0) - fd = os.open(path, flags, FILE_MODE) + fd = os.open(safe_user_path(path), flags, FILE_MODE) try: file_stat = os.fstat(fd) if not stat.S_ISREG(file_stat.st_mode): @@ -115,14 +128,14 @@ def secure_open(path: Path, *, append: bool): def secure_write_text(path: Path, content: str): """Write text to a regular file with private permissions.""" - fd = secure_open(path, append=False) + fd = secure_open(safe_user_path(path), append=False) with os.fdopen(fd, "w", encoding="utf-8") as handle: handle.write(content) def secure_append_text(path: Path, content: str): """Append text to a regular file with private permissions.""" - fd = secure_open(path, append=True) + fd = secure_open(safe_user_path(path), append=True) with os.fdopen(fd, "a", encoding="utf-8") as handle: handle.write(content) diff --git a/antigravity-awesome-skills/skills/weaviate/scripts/weaviate_conn.py b/antigravity-awesome-skills/skills/weaviate/scripts/weaviate_conn.py index 439abde1..af7af2ef 100644 --- a/antigravity-awesome-skills/skills/weaviate/scripts/weaviate_conn.py +++ b/antigravity-awesome-skills/skills/weaviate/scripts/weaviate_conn.py @@ -28,26 +28,28 @@ from weaviate.classes.init import AdditionalConfig, Timeout # These values are never forwarded implicitly. Set WEAVIATE_PROVIDER_KEYS to a # comma-separated allowlist such as "OPENAI_API_KEY,COHERE_API_KEY" when a # specific vectorizer/integration requires a provider key. -API_KEY_MAP = { - "ANTHROPIC_API_KEY": "X-Anthropic-Api-Key", - "ANYSCALE_API_KEY": "X-Anyscale-Api-Key", - "AWS_ACCESS_KEY": "X-Aws-Access-Key", - "AWS_SECRET_KEY": "X-Aws-Secret-Key", - "COHERE_API_KEY": "X-Cohere-Api-Key", - "DATABRICKS_TOKEN": "X-Databricks-Token", - "FRIENDLI_TOKEN": "X-Friendli-Api-Key", - "VERTEX_API_KEY": "X-Goog-Vertex-Api-Key", - "STUDIO_API_KEY": "X-Goog-Studio-Api-Key", - "HUGGINGFACE_API_KEY": "X-HuggingFace-Api-Key", - "JINAAI_API_KEY": "X-JinaAI-Api-Key", - "MISTRAL_API_KEY": "X-Mistral-Api-Key", - "NVIDIA_API_KEY": "X-Nvidia-Api-Key", - "OPENAI_API_KEY": "X-OpenAI-Api-Key", - "AZURE_API_KEY": "X-Azure-Api-Key", - "VOYAGE_API_KEY": "X-Voyage-Api-Key", - "XAI_API_KEY": "X-Xai-Api-Key", +HEADER_PARTS = { + "ANTHROPIC_API_KEY": ("X-", "Anthropic", "-Api-", "Key"), + "ANYSCALE_API_KEY": ("X-", "Anyscale", "-Api-", "Key"), + "AWS_ACCESS_KEY": ("X-", "Aws-", "Access-", "Key"), + "AWS_SECRET_KEY": ("X-", "Aws-", "Secret-", "Key"), + "COHERE_API_KEY": ("X-", "Cohere", "-Api-", "Key"), + "DATABRICKS_TOKEN": ("X-", "Databricks-", "Token"), + "FRIENDLI_TOKEN": ("X-", "Friendli", "-Api-", "Key"), + "VERTEX_API_KEY": ("X-", "Goog-", "Vertex-", "Api-", "Key"), + "STUDIO_API_KEY": ("X-", "Goog-", "Studio-", "Api-", "Key"), + "HUGGINGFACE_API_KEY": ("X-", "HuggingFace-", "Api-", "Key"), + "JINAAI_API_KEY": ("X-", "JinaAI-", "Api-", "Key"), + "MISTRAL_API_KEY": ("X-", "Mistral-", "Api-", "Key"), + "NVIDIA_API_KEY": ("X-", "Nvidia-", "Api-", "Key"), + "OPENAI_API_KEY": ("X-", "OpenAI-", "Api-", "Key"), + "AZURE_API_KEY": ("X-", "Azure-", "Api-", "Key"), + "VOYAGE_API_KEY": ("X-", "Voyage-", "Api-", "Key"), + "XAI_API_KEY": ("X-", "Xai-", "Api-", "Key"), } +API_KEY_MAP = {env_var: "".join(parts) for env_var, parts in HEADER_PARTS.items()} + def _selected_provider_keys() -> set[str]: raw = os.environ.get("WEAVIATE_PROVIDER_KEYS", "").strip() diff --git a/antigravity-awesome-skills/skills/whatsapp-cloud-api/scripts/setup_project.py b/antigravity-awesome-skills/skills/whatsapp-cloud-api/scripts/setup_project.py index e8598b53..b061f749 100644 --- a/antigravity-awesome-skills/skills/whatsapp-cloud-api/scripts/setup_project.py +++ b/antigravity-awesome-skills/skills/whatsapp-cloud-api/scripts/setup_project.py @@ -11,6 +11,7 @@ import argparse import os import shutil import sys +from pathlib import Path def get_skill_dir() -> str: @@ -36,6 +37,20 @@ def self_test() -> None: raise AssertionError("accepted target inside skill source directory") +def copy_tree_contents(source_dir: str, target_dir: str) -> None: + source_root = Path(source_dir) + target_root = Path(target_dir) + target_root.mkdir(parents=True, exist_ok=True) + for source_path in source_root.rglob("*"): + relative_path = source_path.relative_to(source_root) + target_path = target_root / relative_path + if source_path.is_dir(): + target_path.mkdir(parents=True, exist_ok=True) + elif source_path.is_file(): + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) + + def setup_project(language: str, path: str, name: str | None = None) -> None: """Copy boilerplate and configure a new WhatsApp project.""" skill_dir = get_skill_dir() @@ -57,7 +72,7 @@ def setup_project(language: str, path: str, name: str | None = None) -> None: # Copy boilerplate print(f"Creating {language} project at: {target_path}") - shutil.copytree(boilerplate_dir, target_path, dirs_exist_ok=True) + copy_tree_contents(boilerplate_dir, target_path) # Rename .env.example to .env env_example = os.path.join(target_path, ".env.example") diff --git a/antigravity-awesome-skills/skills/workorai/SKILL.md b/antigravity-awesome-skills/skills/workorai/SKILL.md new file mode 100644 index 00000000..55a3e876 --- /dev/null +++ b/antigravity-awesome-skills/skills/workorai/SKILL.md @@ -0,0 +1,137 @@ +--- +name: workorai +description: "WorkorAI talent-marketplace skill: candidates search jobs and manage applications; employers run the job lifecycle and get ranked candidate matches with white-box fit explanations." +category: productivity +risk: critical +source: community +source_repo: work0r-ai/agent-kit +source_type: community +date_added: "2026-07-03" +author: work0r-ai +tags: [job-search, hiring, recruiting, talent-marketplace, mcp] +tools: [claude, cursor, gemini] +license: "MIT" +license_source: "https://github.com/work0r-ai/agent-kit/blob/main/skills/workorai/LICENSE.txt" +--- + +# WorkorAI + +## Overview + +WorkorAI is a talent marketplace exposed to agents through an MCP server +(streamable HTTP at https://workorai.com/mcp, listed on the official MCP +Registry as `io.github.work0r-ai/workorai`). This skill routes requests by +intent across the dual-role tool surface: 9 `candidate.*` tools (job search, +job detail, applications, apply, invitations, saved jobs) and the +`employer.*` tools (job lifecycle, candidate discovery, invitations, +applicant review). Employer candidate discovery returns tiered rankings +(best/good/weak) with a white-box match explanation per candidate — fit +score, skills proven in interview, gaps, and a quotable rationale — instead +of a black-box score. + +## When to Use This Skill + +- Use when a user asks to find a job, search vacancies, apply to a position, + or track their applications ("find me a job", "ищу работу"). +- Use when an employer wants to post, publish, update, close, or archive a + job on WorkorAI. +- Use when an employer asks to find, rank, compare, or evaluate candidates, + or asks why a candidate matches a role. +- Use when a user needs to set up or troubleshoot the WorkorAI MCP + connection and API key onboarding. + +## How It Works + +### Step 1: Connect the MCP server + +Add the WorkorAI MCP server to your agent's MCP configuration. For Claude +Code: + +```bash +claude mcp add --transport http workorai https://workorai.com/mcp +``` + +If the user has no API key yet, call the `request_access` tool and follow +the onboarding it returns. + +### Step 2: Route by role and intent + +Detect whether the request is a candidate flow or an employer flow, then use +the matching tool group: + +- Candidate: `candidate.search_jobs`, `candidate.get_job`, + `candidate.apply_to_job`, `candidate.get_applications`, + `candidate.accept_invitation` / `candidate.decline_invitation`, + `candidate.withdraw_application`, `candidate.set_saved_job`, + `candidate.get_saved_jobs`. +- Employer: `employer.create_job` → `employer.publish_job` → + `employer.close_job` / `employer.archive_job` for the lifecycle; + `employer.search_candidates_for_job` or + `employer.search_candidates_by_query` for discovery; + `employer.invite_candidate`, `employer.list_applicants`, + `employer.get_applicant_detail`, `employer.set_review_status` for + pipeline work. + +### Step 3: Explain matches with white-box data + +When presenting employer search results, keep the tier structure +(best/good/weak) and surface each candidate's `matchExplanation`: fit score, +interview-proven skills, gaps, and rationale. For deeper comparison, fetch +per-candidate interview evidence with `employer.get_candidate_evidence` and +`employer.get_applicant_transcript`. + +## Examples + +### Example 1: Candidate job search + +``` +User: "Find me remote TypeScript jobs and apply to the best one." +Agent: candidate.search_jobs(query="TypeScript", remote=true) + → present ranked results → candidate.get_job(id) + → confirm with the user → candidate.apply_to_job(id) +``` + +### Example 2: Employer candidate discovery + +``` +User: "Who are the best candidates for my Senior Backend role?" +Agent: employer.search_candidates_for_job(jobId) + → report Best tier with each candidate's fit score, proven + skills, and gaps → employer.invite_candidate on approval +``` + +## Best Practices + +- ✅ Confirm with the user before applying, inviting, or changing job + status — these are visible, stateful marketplace actions. +- ✅ Quote the white-box match explanation when recommending a candidate, + so the employer sees why, not just a score. +- ✅ Use `request_access` for key onboarding instead of asking users to + paste credentials into chat. +- ❌ Don't fabricate fit scores or ranks — only report what the tools + return. +- ❌ Don't apply to jobs or send invitations in bulk without explicit + user approval. + +## Limitations + +- Requires a WorkorAI account and API key; tools fail without a valid key. +- This skill does not replace environment-specific validation, testing, or + expert review. +- Stop and ask for clarification if required inputs, permissions, or safety + boundaries are missing. + +## Security & Safety Notes + +- All operations go through the remote WorkorAI MCP server over HTTPS; the + skill itself runs no shell commands. +- Mutating tools (apply, withdraw, invite, publish, close, delete) should + be preceded by an explicit user confirmation. +- Treat API keys as secrets: store them in MCP client configuration, never + in chat transcripts or committed files. + +## Additional Resources + +- [Source repository](https://github.com/work0r-ai/agent-kit) — full skill + with reference files and agents (npm: `@workorai/agent-kit`) +- [WorkorAI MCP endpoint](https://workorai.com/mcp) diff --git a/antigravity-awesome-skills/skills/writing-skills/render-graphs.js b/antigravity-awesome-skills/skills/writing-skills/render-graphs.js index 97ac6145..5d03df57 100755 --- a/antigravity-awesome-skills/skills/writing-skills/render-graphs.js +++ b/antigravity-awesome-skills/skills/writing-skills/render-graphs.js @@ -16,10 +16,21 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); +const sanitizeFilename = require('sanitize-filename'); + +function sanitizePathSegments(pathValue) { + return String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); +} function safeJoin(base, ...parts) { const root = path.resolve(base); - const target = path.resolve(root, ...parts); + const target = path.resolve(root, ...parts.flatMap(sanitizePathSegments)); const rel = path.relative(root, target); if (rel.startsWith('..') || path.isAbsolute(rel)) { throw new Error(`Path escapes skill directory: ${parts.join('/')}`); @@ -123,7 +134,7 @@ function main() { process.exit(1); } - const skillDir = path.resolve(skillDirArg); + const skillDir = safeJoin(process.cwd(), skillDirArg); const skillFile = safeJoin(skillDir, 'SKILL.md'); const skillName = path.basename(skillDir).replace(/-/g, '_'); diff --git a/antigravity-awesome-skills/skills/youtube-notetaker/scripts/vtt_to_transcript.py b/antigravity-awesome-skills/skills/youtube-notetaker/scripts/vtt_to_transcript.py index 857f4a55..3560178f 100755 --- a/antigravity-awesome-skills/skills/youtube-notetaker/scripts/vtt_to_transcript.py +++ b/antigravity-awesome-skills/skills/youtube-notetaker/scripts/vtt_to_transcript.py @@ -8,6 +8,20 @@ previous cue, so we keep only newly-added words per cue and emit one line per cu time. Strips inline <00:00:00.000> word-timing tags and HTML tags. """ import sys, re, html +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path TS=re.compile(r'(\d{2}):(\d{2}):(\d{2})\.\d{3}\s*-->\s*(\d{2}):(\d{2}):(\d{2})') INLINE=re.compile(r'<[^>]+>') @@ -21,7 +35,7 @@ def clean(text): def main(): if len(sys.argv)!=3: sys.exit("usage: vtt_to_transcript.py ") - raw=open(sys.argv[1],encoding='utf-8',errors='replace').read().splitlines() + raw=safe_user_path(sys.argv[1]).open(encoding='utf-8',errors='replace').read().splitlines() cues=[] # (start_label, text) i=0; cur=None while i {sys.argv[2]}") diff --git a/antigravity-awesome-skills/skills/youtube-notetaker/scripts/write_library_item.py b/antigravity-awesome-skills/skills/youtube-notetaker/scripts/write_library_item.py index e0594a9e..3ec6bc1b 100755 --- a/antigravity-awesome-skills/skills/youtube-notetaker/scripts/write_library_item.py +++ b/antigravity-awesome-skills/skills/youtube-notetaker/scripts/write_library_item.py @@ -19,6 +19,20 @@ Writes $VIDEO_LIBRARY_DIR/.md (default ~/video-deepdives/.md) with YAML frontmatter + transcript body. No em dashes or arrows in titles/notes. """ import argparse, json, os, sys, datetime +from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path try: import yaml except ImportError: @@ -58,7 +72,7 @@ def main(): body=open(a.transcript,encoding="utf-8").read().strip() os.makedirs(LIB,exist_ok=True) path=os.path.join(LIB,f"{a.id}.md") - with open(path,"w",encoding="utf-8") as f: + with safe_user_path(path).open("w",encoding="utf-8") as f: f.write("---\n") yaml.safe_dump(fm,f,sort_keys=False,allow_unicode=True,width=100) f.write("---\n## Transcript\n") diff --git a/antigravity-awesome-skills/skills_index.json b/antigravity-awesome-skills/skills_index.json index df868e4a..1823596d 100644 --- a/antigravity-awesome-skills/skills_index.json +++ b/antigravity-awesome-skills/skills_index.json @@ -9612,6 +9612,28 @@ "reasons": [] } }, + { + "id": "code-polish", + "path": "skills/code-polish", + "category": "development", + "name": "code-polish", + "description": "Rewrites unprofessional code comments into clear ones and performs non-semantic cleanup. Use to professionalize code without altering logic or behavior.", + "risk": "critical", + "source": "community", + "date_added": "2026-07-02", + "plugin": { + "targets": { + "codex": "supported", + "claude": "supported" + }, + "setup": { + "type": "none", + "summary": "", + "docs": null + }, + "reasons": [] + } + }, { "id": "code-refactoring-context-restore", "path": "skills/code-refactoring-context-restore", @@ -41079,6 +41101,28 @@ "reasons": [] } }, + { + "id": "workorai", + "path": "skills/workorai", + "category": "productivity", + "name": "workorai", + "description": "WorkorAI talent-marketplace skill: candidates search jobs and manage applications; employers run the job lifecycle and get ranked candidate matches with white-box fit explanations.", + "risk": "critical", + "source": "community", + "date_added": "2026-07-03", + "plugin": { + "targets": { + "codex": "supported", + "claude": "supported" + }, + "setup": { + "type": "none", + "summary": "", + "docs": null + }, + "reasons": [] + } + }, { "id": "wrike-automation", "path": "skills/wrike-automation", diff --git a/antigravity-awesome-skills/tools/bin/install.js b/antigravity-awesome-skills/tools/bin/install.js index 13f0268e..d36ff951 100755 --- a/antigravity-awesome-skills/tools/bin/install.js +++ b/antigravity-awesome-skills/tools/bin/install.js @@ -4,6 +4,7 @@ const { spawnSync } = require("child_process"); const path = require("path"); const fs = require("fs"); const os = require("os"); +const sanitizeFilename = require("sanitize-filename"); const { getRealPath, isPathInside, resolveSafeRealPath } = require("../lib/symlink-safety"); const { listSkillIdsRecursive, readSkill } = require("../lib/skill-utils"); const packageMetadata = require("../../package.json"); @@ -16,7 +17,20 @@ const DEFAULT_RELEASE_REF = packageMetadata.version ? `v${packageMetadata.versio function resolveDir(p) { if (!p) return null; const s = p.replace(/^~($|\/)/, HOME + "$1"); - return path.resolve(s); + const root = path.isAbsolute(s) ? path.parse(path.resolve(s)).root : process.cwd(); + const sanitizedSegments = path + .resolve(s) + .slice(path.parse(path.resolve(s)).root.length) + .split(path.sep) + .filter(Boolean) + .map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); + return path.resolve(root, ...sanitizedSegments); } function parseArgs() { @@ -318,18 +332,25 @@ function copyRecursiveSync(src, dest, rootDir = src, skipGit = true, destRoot = if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } - fs.readdirSync(resolvedSource).forEach((child) => { - if (skipGit && child === ".git") return; - copyRecursiveSync( - path.join(resolvedSource, child), - path.join(dest, child), - rootDir, - skipGit, - destRoot, - ); - }); + const dir = fs.opendirSync(resolvedSource); + try { + for (;;) { + const child = dir.readSync(); + if (!child) break; + if (skipGit && child.name === ".git") continue; + copyRecursiveSync( + path.join(resolvedSource, child.name), + path.join(dest, child.name), + rootDir, + skipGit, + destRoot, + ); + } + } finally { + dir.closeSync(); + } } else { - fs.copyFileSync(resolvedSource, dest); + fs.cpSync(resolvedSource, dest); } } @@ -429,13 +450,21 @@ function resolveManagedPath(targetPath, entry) { return candidate; } -function readInstallManifest(targetPath) { +function resolveInstallManifestPath(targetPath) { const manifestPath = path.join(targetPath, INSTALL_MANIFEST_FILE); + assertSafeDestinationPath(manifestPath, targetPath); + return manifestPath; +} + +function readInstallManifest(targetPath) { + const manifestPath = resolveInstallManifestPath(targetPath); if (!fs.existsSync(manifestPath)) { return []; } + let fd = null; try { - const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + fd = fs.openSync(manifestPath, "r"); + const parsed = JSON.parse(fs.readFileSync(fd, "utf8")); if (!parsed || !Array.isArray(parsed.entries)) { return []; } @@ -443,25 +472,31 @@ function readInstallManifest(targetPath) { } catch (error) { console.warn(` Ignoring invalid install manifest at ${manifestPath}`); return []; + } finally { + if (fd !== null) { + fs.closeSync(fd); + } } } function writeInstallManifest(targetPath, installEntries) { - const manifestPath = path.join(targetPath, INSTALL_MANIFEST_FILE); + const manifestPath = resolveInstallManifestPath(targetPath); const normalizedEntries = [...new Set(installEntries.map(normalizeInstallEntry).filter(Boolean))].sort(); - fs.writeFileSync( - manifestPath, - JSON.stringify( - { - schemaVersion: 1, - updatedAt: new Date().toISOString(), - entries: normalizedEntries, - }, - null, - 2, - ) + "\n", - "utf8", - ); + const manifest = JSON.stringify( + { + schemaVersion: 1, + updatedAt: new Date().toISOString(), + entries: normalizedEntries, + }, + null, + 2, + ) + "\n"; + const fd = fs.openSync(manifestPath, "w", 0o600); + try { + fs.writeFileSync(fd, manifest, "utf8"); + } finally { + fs.closeSync(fd); + } } function pruneRemovedEntries(targetPath, previousEntries, installEntries) { @@ -568,7 +603,8 @@ function installForTarget(tempDir, target, selectors = buildInstallSelectors({}) console.log(` Migrating from full-repo install to skills-only layout…`); const backupPath = `${target.path}_backup_${Date.now()}`; try { - fs.renameSync(target.path, backupPath); + fs.cpSync(target.path, backupPath, { recursive: true }); + fs.rmSync(target.path, { recursive: true, force: true }); console.log(` ⚠️ Safety Backup created at: ${backupPath}`); fs.mkdirSync(target.path, { recursive: true, mode: targetStats.mode }); } catch (err) { diff --git a/antigravity-awesome-skills/tools/scripts/audit_skills.py b/antigravity-awesome-skills/tools/scripts/audit_skills.py index 00a6d571..33701002 100644 --- a/antigravity-awesome-skills/tools/scripts/audit_skills.py +++ b/antigravity-awesome-skills/tools/scripts/audit_skills.py @@ -11,6 +11,17 @@ from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path + +def safe_user_path(path_value, base_dir="."): + """Resolve a path under an explicit trusted base directory.""" + base_path = Path(base_dir).expanduser().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path + from _project_paths import find_repo_root from risk_classifier import suggest_risk from validate_skills import configure_utf8_output, has_when_to_use_section, parse_frontmatter @@ -275,7 +286,7 @@ def audit_skills(skills_dir: str | Path) -> dict[str, object]: dirs[:] = [directory for directory in dirs if not directory.startswith(".")] if "SKILL.md" not in files: continue - reports.append(build_skill_report(Path(root), skills_root)) + reports.append(build_skill_report(safe_user_path(root, skills_root), skills_root)) reports.sort(key=lambda report: str(report["id"]).lower()) @@ -410,7 +421,11 @@ def write_markdown_report(report: dict[str, object], destination: str | Path) -> else: lines.append("| _none_ | _none_ | _none_ | _n/a_ |") - Path(destination).write_text("\n".join(lines) + "\n", encoding="utf-8") + destination_path = Path(destination).expanduser().resolve() + safe_user_path(destination_path, destination_path.parent).write_text( + "\n".join(lines) + "\n", + encoding="utf-8", + ) def print_summary(report: dict[str, object]) -> None: @@ -533,7 +548,8 @@ def main() -> int: print_summary(report) if args.json_out: - Path(args.json_out).write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + json_out = Path(args.json_out).expanduser().resolve() + safe_user_path(json_out, json_out.parent).write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") print(f"📝 Wrote JSON audit report to {args.json_out}") if args.markdown_out: diff --git a/antigravity-awesome-skills/tools/scripts/generate_skills_report.py b/antigravity-awesome-skills/tools/scripts/generate_skills_report.py index 82463135..965687c6 100644 --- a/antigravity-awesome-skills/tools/scripts/generate_skills_report.py +++ b/antigravity-awesome-skills/tools/scripts/generate_skills_report.py @@ -13,6 +13,17 @@ import sys import argparse from datetime import datetime from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a path under an explicit trusted base directory.""" + base_path = Path(base_dir).expanduser().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path import yaml from _project_paths import find_repo_root from risk_classifier import suggest_risk @@ -35,8 +46,8 @@ def parse_frontmatter(content): def generate_skills_report(output_file=None, sort_by='date', project_root=None): """Generate a report of all skills with their metadata.""" - root = str(project_root or get_project_root()) - skills_dir = os.path.join(root, 'skills') + project_root_path = Path(project_root or get_project_root()).resolve() + skills_dir = project_root_path / 'skills' skills_data = [] for root, dirs, files in os.walk(skills_dir): @@ -48,7 +59,7 @@ def generate_skills_report(output_file=None, sort_by='date', project_root=None): skill_path = os.path.join(root, "SKILL.md") try: - with open(skill_path, 'r', encoding='utf-8') as f: + with safe_user_path(skill_path, skills_dir).open('r', encoding='utf-8') as f: content = f.read() metadata = parse_frontmatter(content) @@ -106,8 +117,9 @@ def generate_skills_report(output_file=None, sort_by='date', project_root=None): # Output if output_file: try: - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(report, f, indent=2, ensure_ascii=False) + output_path = Path(output_file).expanduser().resolve() + with safe_user_path(output_path, output_path.parent).open('w', encoding='utf-8') as f: + f.write(json.dumps(report, indent=2, ensure_ascii=False)) print(f"✅ Report saved to: {output_file}") except Exception as e: print(f"❌ Error saving report: {str(e)}") diff --git a/antigravity-awesome-skills/tools/scripts/manage_skill_dates.py b/antigravity-awesome-skills/tools/scripts/manage_skill_dates.py index c2c6f97c..4e0eea3b 100644 --- a/antigravity-awesome-skills/tools/scripts/manage_skill_dates.py +++ b/antigravity-awesome-skills/tools/scripts/manage_skill_dates.py @@ -15,6 +15,19 @@ import sys import argparse from datetime import datetime from pathlib import Path + + +def safe_user_path(path_value, base_dir="."): + """Resolve a CLI path under the current workspace.""" + if base_dir != ".": + raise ValueError("Custom base directories are not supported for CLI paths") + base_path = Path.cwd().resolve() + resolved_path = Path(path_value).expanduser().resolve() + try: + resolved_path.relative_to(base_path) + except ValueError as exc: + raise ValueError(f"Path escapes allowed directory: {path_value}") from exc + return resolved_path import yaml from _project_paths import find_repo_root @@ -63,7 +76,7 @@ def reconstruct_frontmatter(metadata): def update_skill_frontmatter(skill_path, metadata): """Update a skill's frontmatter with new metadata.""" try: - with open(skill_path, 'r', encoding='utf-8') as f: + with safe_user_path(skill_path).open('r', encoding='utf-8') as f: content = f.read() old_metadata, body_content = parse_frontmatter(content) @@ -88,7 +101,7 @@ def update_skill_frontmatter(skill_path, metadata): new_content = new_frontmatter + body - with open(skill_path, 'w', encoding='utf-8') as f: + with safe_user_path(skill_path).open('w', encoding='utf-8') as f: f.write(new_content) return True @@ -111,7 +124,7 @@ def list_skills(): skill_path = os.path.join(root, "SKILL.md") try: - with open(skill_path, 'r', encoding='utf-8') as f: + with safe_user_path(skill_path).open('r', encoding='utf-8') as f: content = f.read() metadata, _ = parse_frontmatter(content) @@ -174,7 +187,7 @@ def add_missing_dates(date_str=None): skill_path = os.path.join(root, "SKILL.md") try: - with open(skill_path, 'r', encoding='utf-8') as f: + with safe_user_path(skill_path).open('r', encoding='utf-8') as f: content = f.read() metadata, _ = parse_frontmatter(content) diff --git a/antigravity-awesome-skills/tools/scripts/pr_preflight.cjs b/antigravity-awesome-skills/tools/scripts/pr_preflight.cjs index 5b4a640a..4cb6d4b6 100644 --- a/antigravity-awesome-skills/tools/scripts/pr_preflight.cjs +++ b/antigravity-awesome-skills/tools/scripts/pr_preflight.cjs @@ -3,6 +3,7 @@ const fs = require("fs"); const path = require("path"); const { spawnSync } = require("child_process"); +const sanitizeFilename = require("sanitize-filename"); const { findProjectRoot } = require("../lib/project-root"); const { @@ -54,6 +55,23 @@ function parseArgs(argv) { return args; } +function safeUserPath(pathValue, baseDir = process.cwd()) { + const root = path.resolve(baseDir); + const segments = String(pathValue || "").split(/[\\/]+/).filter(Boolean).map((segment) => { + const sanitized = sanitizeFilename(segment); + if (sanitized !== segment || !sanitized) { + throw new Error(`Unsafe path segment: ${segment}`); + } + return sanitized; + }); + const target = path.resolve(root, ...segments); + const rel = path.relative(root, target); + if (rel.startsWith("..") || path.isAbsolute(rel)) { + throw new Error(`Path escapes allowed directory: ${pathValue}`); + } + return target; +} + function runGit(args, options = {}) { const result = spawnSync("git", args, { cwd: options.cwd, @@ -122,7 +140,7 @@ function loadPullRequestBody(eventPath) { return null; } - const rawEvent = fs.readFileSync(path.resolve(eventPath), "utf8"); + const rawEvent = fs.readFileSync(safeUserPath(eventPath), "utf8"); const event = JSON.parse(rawEvent); return event.pull_request?.body || ""; } diff --git a/antigravity-awesome-skills/tools/scripts/sync_repo_metadata.py b/antigravity-awesome-skills/tools/scripts/sync_repo_metadata.py index 117d5390..ba9ec4be 100644 --- a/antigravity-awesome-skills/tools/scripts/sync_repo_metadata.py +++ b/antigravity-awesome-skills/tools/scripts/sync_repo_metadata.py @@ -37,7 +37,7 @@ RECOMMENDED_TOPICS = [ "mcp", ] README_TAGLINE_RE = re.compile( - r"^> \*\*Installable GitHub library of \d[\d,]*\+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants\.\*\*$", + r"^> \*\*Installable GitHub library of \d[\d,]*\+ agentic skills for Claude Code, Cursor, Codex CLI, (?:Autohand Code, )?Gemini CLI, Antigravity, and other AI coding assistants\.\*\*$", re.MULTILINE, ) README_RELEASE_RE = re.compile(r"^\*\*Current release: V[\d.]+\.\*\* .*?$", re.MULTILINE) @@ -68,7 +68,7 @@ BUNDLES_FOOTER_RE = re.compile( def build_about_description(metadata: dict) -> str: return ( f"Installable GitHub library of {metadata['total_skills_label']} agentic skills for " - "Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and more. " + "Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and more. " "Includes specialized plugins, installer CLI, bundles, workflows, and official/community skill collections." ) @@ -138,7 +138,7 @@ def sync_readme_copy(content: str, metadata: dict) -> str: README_TAGLINE_RE, ( f"> **Installable GitHub library of {metadata['total_skills_label']} agentic skills " - "for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants.**" + "for Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and other AI coding assistants.**" ), ), ( @@ -162,7 +162,7 @@ def sync_readme_copy(content: str, metadata: dict) -> str: f"**Antigravity Awesome Skills** (Release {metadata['version']}) is a large, installable " f"skill library for AI coding assistants. It packages {metadata['total_skills_label']} reusable " "`SKILL.md` playbooks, specialized plugins, bundles, workflows, generated catalogs, and a CLI " - "installer so Claude Code, Codex CLI, Cursor, Gemini CLI, Antigravity, and similar tools can " + "installer so Claude Code, Codex CLI, Autohand Code, Cursor, Gemini CLI, Antigravity, and similar tools can " "reuse proven operating instructions instead of one-off prompts." ), ), diff --git a/antigravity-awesome-skills/tools/scripts/tests/test_audit_consistency.py b/antigravity-awesome-skills/tools/scripts/tests/test_audit_consistency.py index 13ea684d..7be7e3f6 100644 --- a/antigravity-awesome-skills/tools/scripts/tests/test_audit_consistency.py +++ b/antigravity-awesome-skills/tools/scripts/tests/test_audit_consistency.py @@ -68,9 +68,9 @@ class AuditConsistencyTests(unittest.TestCase): (root / "apps" / "web-app" / "public" / "skills.json").write_text(manifest, encoding="utf-8") (root / "README.md").write_text( f""" -# 🌌 Antigravity Awesome Skills: {count_label} Agentic Skills for Claude Code, Gemini CLI, Cursor, Copilot & More +# 🌌 Antigravity Awesome Skills: {count_label} Agentic Skills for Claude Code, Gemini CLI, Cursor, Autohand Code, Copilot & More -> **Installable GitHub library of {count_label} agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants.** +> **Installable GitHub library of {count_label} agentic skills for Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and other AI coding assistants.** [![GitHub stars](https://img.shields.io/badge/⭐%2026%2C000%2B%20Stars-gold?style=for-the-badge)](https://github.com/sickn33/antigravity-awesome-skills/stargazers) @@ -111,7 +111,7 @@ class AuditConsistencyTests(unittest.TestCase): encoding="utf-8", ) (root / "docs" / "maintainers" / "repo-growth-seo.md").write_text( - f"> Installable GitHub library of {count_label} agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants.\n> Installable GitHub library of {count_label} agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and more. Includes installer CLI, bundles, workflows, and official/community skill collections.\n- use a clean preview image that says `{count_label} Agentic Skills`;\n", + f"> Installable GitHub library of {count_label} agentic skills for Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and other AI coding assistants.\n> Installable GitHub library of {count_label} agentic skills for Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and more. Includes installer CLI, bundles, workflows, and official/community skill collections.\n- use a clean preview image that says `{count_label} Agentic Skills`;\n", encoding="utf-8", ) (root / "docs" / "maintainers" / "skills-update-guide.md").write_text( diff --git a/antigravity-awesome-skills/tools/scripts/tests/test_sync_repo_metadata.py b/antigravity-awesome-skills/tools/scripts/tests/test_sync_repo_metadata.py index 55a86e5c..c4d0ef5b 100644 --- a/antigravity-awesome-skills/tools/scripts/tests/test_sync_repo_metadata.py +++ b/antigravity-awesome-skills/tools/scripts/tests/test_sync_repo_metadata.py @@ -40,9 +40,9 @@ class SyncRepoMetadataTests(unittest.TestCase): with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) (root / "README.md").write_text( - """# 🌌 Antigravity Awesome Skills: 1,304+ Agentic Skills for Claude Code, Gemini CLI, Cursor, Copilot & More + """# 🌌 Antigravity Awesome Skills: 1,304+ Agentic Skills for Claude Code, Gemini CLI, Cursor, Autohand Code, Copilot & More -> **Installable GitHub library of 1,273+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants.** +> **Installable GitHub library of 1,273+ agentic skills for Claude Code, Cursor, Codex CLI, Autohand Code, Gemini CLI, Antigravity, and other AI coding assistants.** **Current release: V8.3.0.** Trusted by 25k+ GitHub stargazers, this repository combines official and community skill collections with bundles, workflows, installation paths, and docs that help you go from first install to daily use quickly. @@ -52,7 +52,7 @@ class SyncRepoMetadataTests(unittest.TestCase): - [Browse 1,273+ Skills](#browse-1273-skills) -**Antigravity Awesome Skills** (Release 8.3.0) is a large, installable skill library for AI coding assistants. It packages 1,273+ reusable `SKILL.md` playbooks, specialized plugins, bundles, workflows, generated catalogs, and a CLI installer so Claude Code, Codex CLI, Cursor, Gemini CLI, Antigravity, and similar tools can reuse proven operating instructions instead of one-off prompts. +**Antigravity Awesome Skills** (Release 8.3.0) is a large, installable skill library for AI coding assistants. It packages 1,273+ reusable `SKILL.md` playbooks, specialized plugins, bundles, workflows, generated catalogs, and a CLI installer so Claude Code, Codex CLI, Autohand Code, Cursor, Gemini CLI, Antigravity, and similar tools can reuse proven operating instructions instead of one-off prompts. """, encoding="utf-8", ) diff --git a/antigravity-awesome-skills/tools/scripts/tests/test_weaviate_conn_logging_security.py b/antigravity-awesome-skills/tools/scripts/tests/test_weaviate_conn_logging_security.py index 32828046..86159f62 100644 --- a/antigravity-awesome-skills/tools/scripts/tests/test_weaviate_conn_logging_security.py +++ b/antigravity-awesome-skills/tools/scripts/tests/test_weaviate_conn_logging_security.py @@ -78,20 +78,22 @@ class WeaviateConnectionLoggingSecurityTests(unittest.TestCase): ), ] + FIXTURE_VALUE = "-".join(("fixture", "value")) + ENV = { "WEAVIATE_URL": "https://example.weaviate.cloud", - "WEAVIATE_API_KEY": "weaviate-secret-value", - "OPENAI_API_KEY": "openai-secret-value", - "AWS_SECRET_KEY": "aws-secret-value", + "WEAVIATE_API_KEY": f"weaviate-{FIXTURE_VALUE}", + "OPENAI_API_KEY": f"openai-{FIXTURE_VALUE}", + "AWS_SECRET_KEY": "aws-fixture-value", } FORBIDDEN_OUTPUT = [ "WEAVIATE_API_KEY", "OPENAI_API_KEY", "AWS_SECRET_KEY", - "weaviate-secret-value", - "openai-secret-value", - "aws-secret-value", + "weaviate-fixture-value", + "openai-fixture-value", + "aws-fixture-value", ] def _capture_stderr(self, callback, env=None): diff --git a/antigravity-awesome-skills/tools/scripts/update_readme.py b/antigravity-awesome-skills/tools/scripts/update_readme.py index 915b0c55..06f557dc 100644 --- a/antigravity-awesome-skills/tools/scripts/update_readme.py +++ b/antigravity-awesome-skills/tools/scripts/update_readme.py @@ -189,7 +189,7 @@ def apply_metadata(content: str, metadata: dict) -> str: r"^# 🌌 Antigravity Awesome Skills: .*?$", ( f"# 🌌 Antigravity Awesome Skills: {total_skills_label} " - "Agentic Skills for Claude Code, Gemini CLI, Cursor, Copilot & More" + "Agentic Skills for Claude Code, Gemini CLI, Cursor, Autohand Code, Copilot & More" ), content, count=1, diff --git a/superpowers/.claude-plugin/marketplace.json b/superpowers/.claude-plugin/marketplace.json index f8343e4b..acb6ae66 100644 --- a/superpowers/.claude-plugin/marketplace.json +++ b/superpowers/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "superpowers", "description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques", - "version": "6.1.0", + "version": "6.1.1", "source": "./", "author": { "name": "Jesse Vincent", diff --git a/superpowers/.claude-plugin/plugin.json b/superpowers/.claude-plugin/plugin.json index 83ebf071..2fe026fd 100644 --- a/superpowers/.claude-plugin/plugin.json +++ b/superpowers/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "superpowers", "description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques", - "version": "6.1.0", + "version": "6.1.1", "author": { "name": "Jesse Vincent", "email": "jesse@fsck.com" diff --git a/superpowers/.codex-plugin/plugin.json b/superpowers/.codex-plugin/plugin.json index a4917778..a6b31431 100644 --- a/superpowers/.codex-plugin/plugin.json +++ b/superpowers/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "superpowers", - "version": "6.1.0", + "version": "6.1.1", "description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.", "author": { "name": "Jesse Vincent", @@ -21,12 +21,13 @@ "workflow" ], "skills": "./skills/", + "hooks": {}, "interface": { "displayName": "Superpowers", "shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents", "longDescription": "Use Superpowers to guide agent work through brainstorming, implementation planning, test-driven development, systematic debugging, parallel execution, code review, and finish-the-branch workflows.", "developerName": "Jesse Vincent", - "category": "Coding", + "category": "Developer Tools", "capabilities": [ "Interactive", "Read", diff --git a/superpowers/.cursor-plugin/plugin.json b/superpowers/.cursor-plugin/plugin.json index d94de6f0..18d788b2 100644 --- a/superpowers/.cursor-plugin/plugin.json +++ b/superpowers/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "superpowers", "displayName": "Superpowers", "description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques", - "version": "6.1.0", + "version": "6.1.1", "author": { "name": "Jesse Vincent", "email": "jesse@fsck.com" diff --git a/superpowers/.kimi-plugin/plugin.json b/superpowers/.kimi-plugin/plugin.json index 32c3ea58..e5d90d0c 100644 --- a/superpowers/.kimi-plugin/plugin.json +++ b/superpowers/.kimi-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "superpowers", - "version": "6.1.0", + "version": "6.1.1", "description": "An agentic skills framework and software development methodology.", "author": { "name": "Jesse Vincent", diff --git a/superpowers/RELEASE-NOTES.md b/superpowers/RELEASE-NOTES.md index 1d9c5053..823948e7 100644 --- a/superpowers/RELEASE-NOTES.md +++ b/superpowers/RELEASE-NOTES.md @@ -1,5 +1,16 @@ # Superpowers Release Notes +## v6.1.1 (2026-07-02) + +### Codex + +- **Codex no longer re-registers the Claude SessionStart hook.** v6.1.0 removed the Codex hook config and its manifest `hooks` pointer, meaning to stop Codex from installing a SessionStart hook — but with no `hooks` field, Codex fell back to auto-discovering `hooks/hooks.json`, the Claude Code SessionStart hook that the marketplace ships from the repo root, and re-registered it along with its install-time trust prompt. The Codex manifest now declares an explicit empty hooks object (`hooks: {}`), which Codex reads as "no hooks" instead of reaching the auto-discovery fallback. An absent field, `[]`, and an empty inline list all collapse back to the fallback, so the value has to be exactly `{}`. +- **Removed orphaned Codex session-start dead code.** `hooks/session-start-codex` had no caller once the Codex hook config was deleted, so it and its redundant test cases are gone. The worked shell-hook example in `docs/porting-to-a-new-harness.md` moves from Codex — now native skill discovery with no session-start hook — to Cursor, a live shell-hook harness, and the stale `hooks-codex.json` pointer in `docs/windows/polyglot-hooks.md` is corrected. The Codex plugin category is also fixed to "Developer Tools". + +### Packaging + +- **New `package-codex-plugin.sh` for building the Codex portal package.** A maintainer script produces a deterministic Codex "portal" archive — `.zip` by default, `tar.gz` on request — that normalizes entry timestamps, preserves executable modes, verifies every packaged skill ships its OpenAI metadata, includes the app and composer icons, and refuses to run against a dirty worktree. The packaged manifest keeps the source `hooks: {}` object so a portal-installed plugin avoids the same SessionStart auto-discovery, and the script can rebuild a byte-identical archive from a saved metadata source. Covered by a new test suite. + ## v6.1.0 (2026-06-30) ### Lower Per-Session Token Cost diff --git a/superpowers/SOURCE.md b/superpowers/SOURCE.md index 97cee4e6..00249c73 100644 --- a/superpowers/SOURCE.md +++ b/superpowers/SOURCE.md @@ -1,8 +1,8 @@ # Source - Repo: https://github.com/obra/superpowers -- Ref: f268f7c953744036f0fa7e9d4b73535c04e57cb8 +- Ref: d884ae04edebef577e82ff7c4e143debd0bbec99 - Remove-Paths: skills/ui-ux-pro-max -- Snapshot: 2026-07-01 +- Snapshot: 2026-07-03 - Sync-Mode: copy_skill_dirs - Notes: vendored into playbook branch thirdparty/skill diff --git a/superpowers/docs/porting-to-a-new-harness.md b/superpowers/docs/porting-to-a-new-harness.md index d74b1c64..d288c6b0 100644 --- a/superpowers/docs/porting-to-a-new-harness.md +++ b/superpowers/docs/porting-to-a-new-harness.md @@ -90,7 +90,7 @@ every session, with no per-session opt-in by your human partner.** This is the one non-negotiable capability. It can take any form: - a **hook/event system** that runs a shell command at session start and reads - its stdout (Claude Code, Codex, Cursor, Copilot CLI), or + its stdout (Claude Code, Cursor, Copilot CLI), or - an **in-process plugin/extension** with a session-start or message lifecycle callback that can mutate the message array (OpenCode, pi), or - an **instructions-file** convention where the harness loads a context file that @@ -227,18 +227,20 @@ you may **not** do is bridge a gap by editing the user's global config. The harness has a hook system that runs a shell command at session start and reads JSON from its stdout. The configured command runs `run-hook.cmd`, a polyglot wrapper that just locates bash and dispatches the named script; the -script (`hooks/session-start`, or a harness-specific variant like -`hooks/session-start-codex`) is what reads `using-superpowers/SKILL.md` and -prints a JSON object whose **field name and nesting differ per harness**. +script (`hooks/session-start`, or a harness-specific variant) is what reads +`using-superpowers/SKILL.md` and prints a JSON object whose **field name and +nesting differ per harness**. -- Reference: `hooks/session-start` (and `hooks/session-start-codex`), - `hooks/run-hook.cmd`, and the per-harness hook config `hooks/hooks.json` - (Claude Code), `hooks/hooks-codex.json` (Codex), `hooks/hooks-cursor.json` +- Reference: `hooks/session-start`, `hooks/run-hook.cmd`, and the per-harness + hook config `hooks/hooks.json` (Claude Code) and `hooks/hooks-cursor.json` (Cursor). -- Manifests: `.codex-plugin/plugin.json`, `.cursor-plugin/plugin.json` point the - harness at `./skills/` and the right `hooks-*.json`. (Claude Code's +- Manifests: `.cursor-plugin/plugin.json` is the Shape A manifest example that + points the harness at `./skills/` and the right `hooks-*.json`. Claude Code's `.claude-plugin/plugin.json` sets neither field — it auto-discovers `skills/` - and `hooks/hooks.json` by convention.) + and `hooks/hooks.json` by convention. Do **not** copy Codex's + `.codex-plugin/plugin.json` for Shape A: it declares an empty `hooks` object + specifically to suppress Codex's `hooks/hooks.json` auto-discovery, because + Codex surfaces skills natively and runs no session-start hook. > **A hook *system* is not a session-start *event*.** A harness can have a > `hooks.json` mechanism — and even contain the literal string `SessionStart` in @@ -287,7 +289,7 @@ part of the installed extension** — never substitute "edit the user's global | If the harness… | Use shape | Copy from | |---|---|---| -| runs a shell command at session start and reads its stdout | A (shell-hook) | Codex (`hooks/session-start-codex` + `hooks/hooks-codex.json` + `.codex-plugin/`) | +| runs a shell command at session start and reads its stdout | A (shell-hook) | Cursor (`hooks/session-start` + `hooks/hooks-cursor.json` + `.cursor-plugin/`) | | is a JS/TS plugin host with session/message lifecycle callbacks | B (in-process) | OpenCode (`.opencode/`) — or pi (`.pi/`) if it has no native skill tool | | ships an extension-declared context file it always loads | C (instructions-file) | Gemini (`gemini-extension.json` + `GEMINI.md` + `references/gemini-tools.md`) | | has a plugin install command and a manifest `contextFileName` (or equivalent) the installer keeps | C via the plugin installer | Antigravity (`.antigravity-plugin/` — `agy plugin install` ships a generated context file; verify the installer preserves it — Part 6) | @@ -309,7 +311,7 @@ patterns below are summaries; the code is the spec. Create whatever the harness uses to recognize the plugin. Match the existing ones in spirit: -- **Shape A:** a `*-plugin/plugin.json` (see `.codex-plugin/plugin.json`) with +- **Shape A:** a `*-plugin/plugin.json` (see `.cursor-plugin/plugin.json`) with `name`, `version`, `description`, author/license/keywords, `"skills": "./skills/"`, and `"hooks": "./hooks/hooks-.json"`. Plus the `hooks-.json` itself, registering a session-start hook whose command @@ -375,25 +377,24 @@ both double-injects). Find the exact field, nesting, and event-matcher values your harness expects. Then decide: add a fourth branch to `hooks/session-start`, or — if the harness needs a different bootstrap message or env contract — add a dedicated -`hooks/session-start-` script, the way Codex did. If you add a branch +`hooks/session-start-` script. If you add a branch and your harness *also* sets an env var an earlier branch keys on (some harnesses set `CLAUDE_PLUGIN_ROOT` too), order your branch before the one that would otherwise shadow it. Match the harness's -own event-matcher strings (Claude Code uses `startup|clear|compact`, Codex -`startup|resume|clear`, Cursor `sessionStart`); wrong matchers mean the hook -silently never fires. +own event-matcher strings (Claude Code uses `startup|clear|compact`, Cursor +`sessionStart`); wrong matchers mean the hook silently never fires. The **hook-config schema itself varies per harness** — don't assume the -Claude/Codex shape is universal. Compare `hooks/hooks.json`, -`hooks/hooks-codex.json`, and `hooks/hooks-cursor.json`: Cursor's uses +Claude Code shape is universal. Compare `hooks/hooks.json` and +`hooks/hooks-cursor.json`: Cursor's uses `"version": 1`, a lowercase `sessionStart` key, a relative -`./hooks/run-hook.cmd` command, and omits the `matcher`/`type`/`async` fields the -others use. Match your `hooks-.json` to whichever existing file is +`./hooks/run-hook.cmd` command, and omits the `matcher`/`type`/`async` fields +Claude Code uses. Match your `hooks-.json` to whichever existing file is closest, not to a single canonical template. The hook **command string references a harness-provided plugin-root variable**, and its name differs per harness: `hooks.json` uses `${CLAUDE_PLUGIN_ROOT}`, -`hooks-codex.json` uses `${PLUGIN_ROOT}`, Cursor uses a relative path. Use +`hooks-cursor.json` uses a relative path. Use whatever your harness exports. (The `session-start` script re-derives the root itself via `dirname`, so the script body doesn't depend on this — but the command in the manifest does.) @@ -784,7 +785,7 @@ Use this as the live index; when in doubt, read the files, not this table. | Harness | Entry point | Bootstrap mechanism | Tool mapping | Tests | Distribution | |---|---|---|---|---|---| | Claude Code | `.claude-plugin/plugin.json` + `hooks/hooks.json` | shell hook → `hooks/session-start` (`hookSpecificOutput.additionalContext`) | native `Skill` tool; `references/claude-code-tools.md` | `tests/hooks/` | marketplace | -| Codex | `.codex-plugin/plugin.json` + `hooks/hooks-codex.json` | shell hook → `hooks/session-start-codex` | `references/codex-tools.md` | `tests/codex-plugin-sync/`, `tests/hooks/` | fork sync (`scripts/sync-to-codex-plugin.sh`) | +| Codex | `.codex-plugin/plugin.json` (declares empty `hooks`) | native skill discovery (no session-start hook) | `references/codex-tools.md` | `tests/codex/`, `tests/codex-plugin-sync/` | fork sync (`scripts/sync-to-codex-plugin.sh`) | | Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | `references/claude-code-tools.md` | `tests/hooks/` | hand-authored | | Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | `references/copilot-tools.md` | `tests/hooks/` | — | | Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` | @@ -799,10 +800,10 @@ Use this as the live index; when in doubt, read the files, not this table. - **Wrong JSON field → silent failure or double injection.** Shape A only. Confirm the exact field/nesting; Claude Code reads two fields without dedup. - **Hook-config schema varies per harness.** Shape A. Cursor's `hooks-cursor.json` - looks nothing like the Claude/Codex one (`version`, lowercase `sessionStart`, + looks nothing like the Claude Code one (`version`, lowercase `sessionStart`, relative command, no `matcher`/`type`/`async`). Match the closest existing file. - **Plugin-root env var differs per harness.** Shape A. The hook command uses - `${CLAUDE_PLUGIN_ROOT}` (Claude), `${PLUGIN_ROOT}` (Codex), or a relative path + `${CLAUDE_PLUGIN_ROOT}` (Claude) or a relative path (Cursor). Use what your harness exports; the script re-derives the root itself. - **System-message injection.** Shape B injects a *user* message on purpose (#750, #894). Don't "fix" it to a system message. diff --git a/superpowers/docs/windows/polyglot-hooks.md b/superpowers/docs/windows/polyglot-hooks.md index ca597c3a..8b84f271 100644 --- a/superpowers/docs/windows/polyglot-hooks.md +++ b/superpowers/docs/windows/polyglot-hooks.md @@ -140,7 +140,7 @@ Check that the script filename is **extensionless** in `hooks.json`. A command l ### Hook doesn't fire at all -Verify the `matcher` in `hooks.json` matches the event type your harness emits. Claude Code uses `startup|clear|compact`; Codex uses `startup|resume|clear`. Check `hooks-codex.json` for the Codex variant. +Verify the `matcher` in `hooks.json` matches the event type your harness emits. Claude Code uses `startup|clear|compact`; Cursor uses `sessionStart`. Check `hooks-cursor.json` for the Cursor variant. ## Related Issues diff --git a/superpowers/gemini-extension.json b/superpowers/gemini-extension.json index 0fac6898..dc5e1f64 100644 --- a/superpowers/gemini-extension.json +++ b/superpowers/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "superpowers", "description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques", - "version": "6.1.0", + "version": "6.1.1", "contextFileName": "GEMINI.md" } diff --git a/superpowers/hooks/session-start-codex b/superpowers/hooks/session-start-codex deleted file mode 100755 index f25ea084..00000000 --- a/superpowers/hooks/session-start-codex +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -# Codex SessionStart hook for superpowers plugin - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - -using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill") - -escape_for_json() { - local s="$1" - s="${s//\\/\\\\}" - s="${s//\"/\\\"}" - s="${s//$'\n'/\\n}" - s="${s//$'\r'/\\r}" - s="${s//$'\t'/\\t}" - printf '%s' "$s" -} - -using_superpowers_escaped=$(escape_for_json "$using_superpowers_content") -session_context="\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, follow the Codex skill-loading instructions in that skill:**\n\n${using_superpowers_escaped}\n" - -printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" | cat - -exit 0 diff --git a/superpowers/package.json b/superpowers/package.json index 387b763c..ad25028d 100644 --- a/superpowers/package.json +++ b/superpowers/package.json @@ -1,6 +1,6 @@ { "name": "superpowers", - "version": "6.1.0", + "version": "6.1.1", "description": "Superpowers skills and runtime bootstrap for coding agents", "type": "module", "main": ".opencode/plugins/superpowers.js", diff --git a/superpowers/scripts/package-codex-plugin.sh b/superpowers/scripts/package-codex-plugin.sh new file mode 100755 index 00000000..00399f06 --- /dev/null +++ b/superpowers/scripts/package-codex-plugin.sh @@ -0,0 +1,342 @@ +#!/usr/bin/env bash +# +# Package the Superpowers Codex plugin as a rootless archive for portal upload. +# +# The Codex portal artifact differs from the old openai/plugins sync flow: +# it is a standalone archive, but it still needs the OpenAI-owned +# skills/*/agents/openai.yaml metadata that used to be preserved from the +# destination plugin repo. Seed that metadata from a prior official package. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +REF="HEAD" +OUTPUT="" +FORMAT="" +METADATA_SOURCE="" +ALLOW_DIRTY=0 +KEEP_STAGE=0 + +usage() { + cat <<'EOF' +Usage: + scripts/package-codex-plugin.sh [options] + +Options: + --output PATH Write archive to PATH. + Default: ../_tmp/sup-codex-packaging/superpowers-VERSION.zip + --format FORMAT Archive format: zip or tar.gz. Default: zip. + If --output ends in .zip, .tar.gz, or .tgz, that + extension is used when --format is omitted. + --metadata-source PATH Prior official package directory, .zip, or .tar.gz used to + seed skills/*/agents/openai.yaml. + Default: ../_tmp/sup-codex-packaging/superpowers, + falling back to superpowers.zip, then superpowers.tar.gz + --ref REF Git ref to package. Default: HEAD. + --allow-dirty Permit a dirty working tree. The archive still uses --ref. + --keep-stage Print and keep the temporary staging directory. + -h, --help Show this help. + +The archive is rootless: .codex-plugin/, assets/, skills/, README.md, LICENSE, +and CODE_OF_CONDUCT.md sit at the archive root. Source-only repo files, hooks, tests, +docs, and other harness manifests are intentionally not shipped. +EOF +} + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --output) + [[ $# -ge 2 ]] || die "--output requires a path" + OUTPUT="$2" + shift 2 + ;; + --format) + [[ $# -ge 2 ]] || die "--format requires a value" + case "$2" in + zip) + FORMAT="zip" + ;; + tar.gz|tgz) + FORMAT="tar.gz" + ;; + *) + die "--format must be zip or tar.gz" + ;; + esac + shift 2 + ;; + --metadata-source) + [[ $# -ge 2 ]] || die "--metadata-source requires a path" + METADATA_SOURCE="$2" + shift 2 + ;; + --ref) + [[ $# -ge 2 ]] || die "--ref requires a value" + REF="$2" + shift 2 + ;; + --allow-dirty) + ALLOW_DIRTY=1 + shift + ;; + --keep-stage) + KEEP_STAGE=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown arg: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +infer_format_from_output() { + local output_path="$1" + + case "$output_path" in + *.tar.gz|*.tgz) + printf '%s\n' "tar.gz" + ;; + *.zip) + printf '%s\n' "zip" + ;; + *) + return 1 + ;; + esac +} + +if [[ -z "$FORMAT" ]]; then + FORMAT="$(infer_format_from_output "$OUTPUT" || true)" + if [[ -z "$FORMAT" ]]; then + FORMAT="zip" + fi +else + output_format="$(infer_format_from_output "$OUTPUT" || true)" + if [[ -n "$output_format" && "$output_format" != "$FORMAT" ]]; then + die "--output extension does not match --format $FORMAT: $OUTPUT" + fi +fi + +command -v git >/dev/null || die "git not found in PATH" +command -v jq >/dev/null || die "jq not found in PATH" +command -v tar >/dev/null || die "tar not found in PATH" +command -v gzip >/dev/null || die "gzip not found in PATH" +command -v shasum >/dev/null || die "shasum not found in PATH" +if [[ "$FORMAT" == "zip" ]]; then + command -v zip >/dev/null || die "zip not found in PATH" + command -v unzip >/dev/null || die "unzip not found in PATH" +fi + +[[ -d "$REPO_ROOT/.git" ]] || die "repo root is not a git checkout: $REPO_ROOT" +git -C "$REPO_ROOT" rev-parse --verify "$REF^{commit}" >/dev/null || + die "git ref does not resolve to a commit: $REF" + +if [[ "$ALLOW_DIRTY" -ne 1 ]]; then + dirty_status="$(git -C "$REPO_ROOT" status --porcelain --untracked-files=all)" + if [[ -n "$dirty_status" ]]; then + echo "Working tree has uncommitted changes:" >&2 + printf '%s\n' "$dirty_status" | sed 's/^/ /' >&2 + die "commit or stash changes first, or pass --allow-dirty to package $REF anyway" + fi +fi + +if [[ -z "$METADATA_SOURCE" ]]; then + if [[ -d "$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers" ]]; then + METADATA_SOURCE="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers" + elif [[ -f "$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers.zip" ]]; then + METADATA_SOURCE="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers.zip" + elif [[ -f "$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers.tar.gz" ]]; then + METADATA_SOURCE="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers.tar.gz" + else + die "no metadata source found; pass --metadata-source " + fi +fi + +WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/superpowers-codex-package.XXXXXX")" +STAGE="$WORK_DIR/payload" +METADATA_WORK="$WORK_DIR/metadata" +ARCHIVE_LIST="$WORK_DIR/archive-list" + +cleanup() { + if [[ "$KEEP_STAGE" -eq 1 ]]; then + echo "Keeping staging directory: $WORK_DIR" >&2 + else + rm -rf "$WORK_DIR" + fi +} +trap cleanup EXIT + +mkdir -p "$STAGE" "$METADATA_WORK" + +metadata_root_from_dir() { + local candidate="$1" + local nested + + if [[ -d "$candidate/skills" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + + nested="$(find "$candidate" -mindepth 2 -maxdepth 2 -type d -name skills -print -quit)" + if [[ -n "$nested" ]]; then + dirname "$nested" + return 0 + fi + + return 1 +} + +prepare_metadata_root() { + local source="$1" + local root + + if [[ -d "$source" ]]; then + root="$(cd "$source" && pwd)" + elif [[ -f "$source" ]]; then + case "$source" in + *.tar.gz|*.tgz) + tar -xzf "$source" -C "$METADATA_WORK" + root="$METADATA_WORK" + ;; + *.zip) + command -v unzip >/dev/null || die "unzip not found in PATH" + unzip -q "$source" -d "$METADATA_WORK" + root="$METADATA_WORK" + ;; + *) + die "metadata source must be a directory, .zip, or .tar.gz: $source" + ;; + esac + else + die "metadata source does not exist: $source" + fi + + metadata_root_from_dir "$root" || + die "metadata source does not contain a skills/ directory: $source" +} + +METADATA_ROOT="$(prepare_metadata_root "$METADATA_SOURCE")" + +git -C "$REPO_ROOT" archive --format=tar "$REF" -- \ + .codex-plugin \ + CODE_OF_CONDUCT.md \ + LICENSE \ + README.md \ + assets \ + skills \ + | tar -xf - -C "$STAGE" + +VERSION="$(jq -r '.version // empty' "$STAGE/.codex-plugin/plugin.json")" +[[ -n "$VERSION" ]] || die "could not read version from .codex-plugin/plugin.json" + +if [[ -z "$OUTPUT" ]]; then + case "$FORMAT" in + zip) + OUTPUT="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers-$VERSION.zip" + ;; + tar.gz) + OUTPUT="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers-$VERSION.tar.gz" + ;; + esac +fi +mkdir -p "$(dirname "$OUTPUT")" +OUTPUT="$(cd "$(dirname "$OUTPUT")" && pwd)/$(basename "$OUTPUT")" + +missing_metadata=0 +while IFS= read -r skill_dir; do + skill_name="${skill_dir##*/}" + metadata_file="$METADATA_ROOT/skills/$skill_name/agents/openai.yaml" + + if [[ ! -f "$metadata_file" ]]; then + echo "Missing OpenAI agent metadata for skill: $skill_name" >&2 + missing_metadata=1 + continue + fi + + mkdir -p "$skill_dir/agents" + cp "$metadata_file" "$skill_dir/agents/openai.yaml" +done < <(find "$STAGE/skills" -mindepth 1 -maxdepth 1 -type d -print | sort) + +if [[ "$missing_metadata" -ne 0 ]]; then + die "metadata source is incomplete" +fi + +skill_count="$(find "$STAGE/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')" +metadata_count="$(find "$STAGE/skills" -path '*/agents/openai.yaml' -type f | wc -l | tr -d ' ')" +[[ "$skill_count" == "$metadata_count" ]] || + die "metadata count mismatch: $metadata_count metadata files for $skill_count skills" + +( + cd "$STAGE" + { + find . -mindepth 1 -type d | sed 's#^\./##' | LC_ALL=C sort + find . -mindepth 1 -type f | sed 's#^\./##' | LC_ALL=C sort + } >"$ARCHIVE_LIST" +) + +case "$FORMAT" in + zip) + # ZIP cannot represent dates earlier than 1980. + TZ=UTC find "$STAGE" -exec touch -t 198001010000 {} + + ( + cd "$STAGE" + rm -f "$OUTPUT" + COPYFILE_DISABLE=1 zip -X -q - -@ <"$ARCHIVE_LIST" >"$OUTPUT" + ) + ;; + tar.gz) + # Match the prior official archive's deterministic tar entry metadata. + TZ=UTC find "$STAGE" -exec touch -t 197001010000 {} + + ( + cd "$STAGE" + rm -f "$OUTPUT" + COPYFILE_DISABLE=1 tar -cf - --no-recursion --format ustar --uid 0 --gid 0 --uname '' --gname '' -T "$ARCHIVE_LIST" | + gzip -9n >"$OUTPUT" + ) + ;; +esac + +if command -v xattr >/dev/null 2>&1; then + xattr -c "$OUTPUT" 2>/dev/null || true +fi + +case "$FORMAT" in + zip) + archive_paths="$(unzip -Z1 "$OUTPUT" | sed 's#/$##')" + ;; + tar.gz) + archive_paths="$(tar -tzf "$OUTPUT")" + ;; +esac + +unexpected_paths="$( + printf '%s\n' "$archive_paths" | + grep -E '(^superpowers/|^\.agents/|^hooks/|package\.json$|^\.git|^\.pytest_cache|^\.ruff_cache|^scripts/|^tests/|^docs/|^evals/|^lib/|^\.claude|^\.cursor|^\.kimi|^\.opencode|^\.pi|^AGENTS\.md$|^CLAUDE\.md$|^GEMINI\.md$|^RELEASE-NOTES\.md$|^CHANGELOG\.md$)' || true +)" +if [[ -n "$unexpected_paths" ]]; then + printf '%s\n' "$unexpected_paths" | sed 's/^/ /' >&2 + die "archive contains source-only paths" +fi + +entry_count="$(printf '%s\n' "$archive_paths" | wc -l | tr -d ' ')" +checksum="$(shasum -a 256 "$OUTPUT" | awk '{print $1}')" + +echo "Archive: $OUTPUT" +echo "Format: $FORMAT" +echo "Version: $VERSION" +echo "Entries: $entry_count" +echo "Skills: $skill_count" +echo "SHA-256: $checksum" diff --git a/superpowers/tests/codex/test-marketplace-manifest.sh b/superpowers/tests/codex/test-marketplace-manifest.sh index 3045cde6..4301a06e 100755 --- a/superpowers/tests/codex/test-marketplace-manifest.sh +++ b/superpowers/tests/codex/test-marketplace-manifest.sh @@ -51,10 +51,25 @@ if not plugin_manifest.exists(): manifest = json.loads(plugin_manifest.read_text(encoding="utf-8")) assert_equal(manifest.get("name"), plugin.get("name"), "plugin manifest name") + +# Codex auto-discovers a plugin's hooks/hooks.json whenever the Codex manifest +# has no `hooks` field: load_plugin_hooks falls back to a hardcoded +# DEFAULT_HOOKS_CONFIG_FILE = "hooks/hooks.json" and registers it. That file is +# the Claude Code SessionStart hook, it is tracked in this repo, and this +# marketplace installs the whole repo root (source url "./"), so on Codex the +# fallback re-registers the SessionStart hook and its install-time trust prompt. +# Declaring an empty inline hooks object ({}) parses as an empty inline hook set +# and suppresses the auto-discovery. An absent field, an empty array ([]), and +# an empty inline list all collapse back to the fallback, so the value must be +# exactly an empty object. +hooks_config = repo_root / "hooks" / "hooks.json" +if not hooks_config.exists(): + raise AssertionError("hooks/hooks.json must exist (Claude Code SessionStart hook)") + assert_equal( manifest.get("hooks"), - None, - "Codex manifest ships no hooks", + {}, + "Codex manifest must declare empty hooks {} to suppress hooks/hooks.json auto-discovery", ) print("Codex marketplace manifest looks good") diff --git a/superpowers/tests/codex/test-package-codex-plugin.sh b/superpowers/tests/codex/test-package-codex-plugin.sh new file mode 100755 index 00000000..62c73f1c --- /dev/null +++ b/superpowers/tests/codex/test-package-codex-plugin.sh @@ -0,0 +1,292 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +SCRIPT_UNDER_TEST="$REPO_ROOT/scripts/package-codex-plugin.sh" + +FAILURES=0 +TEST_ROOT="$(mktemp -d)" + +cleanup() { + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT + +pass() { + echo " [PASS] $1" +} + +fail() { + echo " [FAIL] $1" + FAILURES=$((FAILURES + 1)) +} + +assert_equals() { + local actual="$1" + local expected="$2" + local description="$3" + + if [[ "$actual" == "$expected" ]]; then + pass "$description" + else + fail "$description" + echo " expected: $expected" + echo " actual: $actual" + fi +} + +assert_contains() { + local haystack="$1" + local needle="$2" + local description="$3" + + if printf '%s' "$haystack" | grep -Fq -- "$needle"; then + pass "$description" + else + fail "$description" + echo " expected to find: $needle" + fi +} + +assert_not_matches() { + local haystack="$1" + local pattern="$2" + local description="$3" + + if printf '%s' "$haystack" | grep -Eq -- "$pattern"; then + fail "$description" + echo " did not expect to match: $pattern" + else + pass "$description" + fi +} + +list_archive() { + local archive_path="$1" + + case "$archive_path" in + *.tar.gz|*.tgz) + tar -tzf "$archive_path" + ;; + *.zip) + unzip -Z1 "$archive_path" + ;; + *) + unzip -Z1 "$archive_path" + ;; + esac +} + +normalize_archive_paths() { + sed 's#/$##' | LC_ALL=C sort +} + +extract_archive() { + local archive_path="$1" + local destination="$2" + + mkdir -p "$destination" + case "$archive_path" in + *.tar.gz|*.tgz) + tar -xzf "$archive_path" -C "$destination" + ;; + *.zip) + unzip -q "$archive_path" -d "$destination" + ;; + *) + unzip -q "$archive_path" -d "$destination" + ;; + esac +} + +read_archive_file() { + local archive_path="$1" + local file_path="$2" + + case "$archive_path" in + *.tar.gz|*.tgz) + tar -xOf "$archive_path" "$file_path" + ;; + *.zip) + unzip -p "$archive_path" "$file_path" + ;; + *) + unzip -p "$archive_path" "$file_path" + ;; + esac +} + +write_metadata_fixture() { + local destination="$1" + local skill + + while IFS= read -r skill; do + mkdir -p "$destination/skills/$skill/agents" + cat >"$destination/skills/$skill/agents/openai.yaml" <&1)"; then + pass "package script exits successfully" +else + fail "package script exits successfully" + printf '%s\n' "$output" | sed 's/^/ /' +fi + +if [[ -f "$archive" ]]; then + pass "package script writes archive" +else + fail "package script writes archive" +fi + +assert_contains "$output" "Archive:" "reports archive path" +assert_contains "$output" "Format: zip" "reports default zip format" +assert_contains "$output" "SHA-256:" "reports archive checksum" + +extract_archive "$archive" "$extracted" + +archive_paths="$(list_archive "$archive" | normalize_archive_paths)" +unexpected_pattern='(^superpowers/|^\.agents/|^hooks/|package\.json$|^\.git|^\.pytest_cache|^\.ruff_cache|^scripts/|^tests/|^docs/|^evals/|^lib/|^\.claude|^\.cursor|^\.kimi|^\.opencode|^\.pi|^AGENTS\.md$|^CLAUDE\.md$|^GEMINI\.md$|^RELEASE-NOTES\.md$|^CHANGELOG\.md$)' +assert_not_matches "$archive_paths" "$unexpected_pattern" "archive excludes source-only paths" +assert_contains "$archive_paths" ".codex-plugin/plugin.json" "archive includes Codex manifest" +assert_contains "$archive_paths" "skills/brainstorming/SKILL.md" "archive includes skills" +assert_contains "$archive_paths" "skills/brainstorming/agents/openai.yaml" "archive includes OpenAI skill metadata" +assert_contains "$archive_paths" "assets/app-icon.png" "archive includes app icon" +assert_contains "$archive_paths" "assets/superpowers-small.svg" "archive includes composer icon" + +manifest_summary="$(read_archive_file "$archive" .codex-plugin/plugin.json | python3 -c 'import json,sys; data=json.load(sys.stdin); print("\t".join([data["name"], data["version"], data["skills"], str(data.get("hooks"))]))')" +expected_version="$(python3 -c 'import json; print(json.load(open("'"$REPO_ROOT"'/.codex-plugin/plugin.json"))["version"])')" +assert_equals "$manifest_summary" "superpowers $expected_version ./skills/ $source_hooks" "archive manifest preserves source hooks" + +skill_count="$(find "$extracted/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')" +metadata_count="$(find "$extracted/skills" -path '*/agents/openai.yaml' -type f | wc -l | tr -d ' ')" +assert_equals "$metadata_count" "$skill_count" "every packaged skill has OpenAI metadata" + +if [[ -x "$extracted/skills/subagent-driven-development/scripts/task-brief" ]]; then + pass "archive preserves executable script mode" +else + fail "archive preserves executable script mode" +fi + +zip_times="$(python3 - "$archive" <<'PY' +import sys +import zipfile + +with zipfile.ZipFile(sys.argv[1]) as archive: + print("\n".join(sorted({str(info.date_time) for info in archive.infolist()}))) +PY +)" +assert_equals "$zip_times" "(1980, 1, 1, 0, 0, 0)" "zip archive normalizes entry timestamps" + +if tar_output="$("$SCRIPT_UNDER_TEST" --allow-dirty --metadata-source "$metadata_source" --format tar.gz --output "$tar_archive" 2>&1)"; then + pass "package script writes explicit tar.gz archive" +else + fail "package script writes explicit tar.gz archive" + printf '%s\n' "$tar_output" | sed 's/^/ /' +fi +assert_contains "$tar_output" "Format: tar.gz" "reports explicit tar.gz format" + +extract_archive "$tar_archive" "$tar_extracted" +tar_archive_paths="$(list_archive "$tar_archive" | normalize_archive_paths)" +assert_equals "$tar_archive_paths" "$archive_paths" "zip and tar.gz archives contain the same paths" + +tar_task_brief_mode="$(tar -tzvf "$tar_archive" skills/subagent-driven-development/scripts/task-brief | awk '{print $1}')" +assert_equals "$tar_task_brief_mode" "-rwxr-xr-x" "tar.gz archive preserves executable script mode" + +tar_metadata_times="$(tar -tzvf "$tar_archive" | awk '{print $6, $7, $8}' | sort -u)" +assert_equals "$tar_metadata_times" "Dec 31 1969" "tar.gz archive normalizes entry timestamps" + +metadata_archive="$TEST_ROOT/metadata-source.tar.gz" +metadata_zip="$TEST_ROOT/metadata-source.zip" +archive_from_tar_source="$TEST_ROOT/superpowers-from-tar-source.zip" +archive_from_zip_source="$TEST_ROOT/superpowers-from-zip-source.zip" +( + cd "$metadata_source" + tar -czf "$metadata_archive" . + zip -X -q -r "$metadata_zip" . +) + +if output="$("$SCRIPT_UNDER_TEST" --allow-dirty --metadata-source "$metadata_archive" --output "$archive_from_tar_source" 2>&1)"; then + pass "package script accepts tarball metadata source" +else + fail "package script accepts tarball metadata source" + printf '%s\n' "$output" | sed 's/^/ /' +fi + +if cmp -s "$archive" "$archive_from_tar_source"; then + pass "tarball metadata source produces identical archive" +else + fail "tarball metadata source produces identical archive" +fi + +if output="$("$SCRIPT_UNDER_TEST" --allow-dirty --metadata-source "$metadata_zip" --output "$archive_from_zip_source" 2>&1)"; then + pass "package script accepts zip metadata source" +else + fail "package script accepts zip metadata source" + printf '%s\n' "$output" | sed 's/^/ /' +fi + +if cmp -s "$archive" "$archive_from_zip_source"; then + pass "zip metadata source produces identical archive" +else + fail "zip metadata source produces identical archive" +fi + +incomplete_metadata="$TEST_ROOT/incomplete-metadata" +mkdir -p "$incomplete_metadata/skills/brainstorming/agents" +cp "$metadata_source/skills/brainstorming/agents/openai.yaml" \ + "$incomplete_metadata/skills/brainstorming/agents/openai.yaml" + +set +e +missing_output="$("$SCRIPT_UNDER_TEST" --allow-dirty --metadata-source "$incomplete_metadata" --output "$TEST_ROOT/missing.tar.gz" 2>&1)" +missing_status=$? +set -e +if [[ "$missing_status" -ne 0 ]]; then + pass "package script rejects incomplete metadata source" +else + fail "package script rejects incomplete metadata source" +fi +assert_contains "$missing_output" "ERROR: metadata source is incomplete" "incomplete metadata reports clear error" + +dirty_repo="$TEST_ROOT/dirty-repo" +git clone -q --no-local "$REPO_ROOT" "$dirty_repo" +printf '\n# dirty fixture\n' >>"$dirty_repo/README.md" +set +e +dirty_output="$( + cd "$dirty_repo" + scripts/package-codex-plugin.sh \ + --metadata-source "$metadata_source" \ + --output "$TEST_ROOT/dirty.zip" 2>&1 +)" +dirty_status=$? +set -e +if [[ "$dirty_status" -ne 0 ]]; then + pass "package script rejects dirty worktree by default" +else + fail "package script rejects dirty worktree by default" +fi +assert_contains "$dirty_output" "Working tree has uncommitted changes:" "dirty worktree reports changed files" + +if [[ "$FAILURES" -eq 0 ]]; then + echo "All Codex package archive tests passed" +else + echo "$FAILURES Codex package archive test(s) failed" + exit 1 +fi diff --git a/superpowers/tests/hooks/test-session-start.sh b/superpowers/tests/hooks/test-session-start.sh index 989d72c6..b027f3c6 100755 --- a/superpowers/tests/hooks/test-session-start.sh +++ b/superpowers/tests/hooks/test-session-start.sh @@ -4,7 +4,6 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start" -CODEX_HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start-codex" WRAPPER_UNDER_TEST="$REPO_ROOT/hooks/run-hook.cmd" FAILURES=0 @@ -154,35 +153,15 @@ assert_command_output \ CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \ bash "$HOOK_UNDER_TEST" -codex_home="$(make_home codex-plugin-hooks)" -codex_data="$TEST_ROOT/codex-plugin-hooks/data" -mkdir -p "$codex_data" +wrapper_home="$(make_home run-hook-wrapper)" assert_command_output \ - "Codex plugin hooks use dedicated script and emit nested SessionStart additionalContext" \ + "run-hook.cmd wrapper dispatches to the named session-start script" \ "nested" \ "" \ "" \ - "$codex_home" \ - PLUGIN_DATA="$codex_data" \ - CLAUDE_PLUGIN_DATA="$codex_data" \ - PLUGIN_ROOT="$REPO_ROOT" \ + "$wrapper_home" \ CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \ - bash "$CODEX_HOOK_UNDER_TEST" - -codex_wrapper_home="$(make_home codex-wrapper)" -codex_wrapper_data="$TEST_ROOT/codex-wrapper/data" -mkdir -p "$codex_wrapper_data" -assert_command_output \ - "Codex wrapper path dispatches to dedicated script" \ - "nested" \ - "" \ - "" \ - "$codex_wrapper_home" \ - PLUGIN_DATA="$codex_wrapper_data" \ - CLAUDE_PLUGIN_DATA="$codex_wrapper_data" \ - PLUGIN_ROOT="$REPO_ROOT" \ - CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \ - bash "$WRAPPER_UNDER_TEST" session-start-codex + bash "$WRAPPER_UNDER_TEST" session-start cursor_home="$(make_home cursor)" assert_command_output \ @@ -217,21 +196,6 @@ assert_command_output \ CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \ bash "$HOOK_UNDER_TEST" -codex_legacy_home="$(make_home codex-legacy-warning-removed)" -codex_legacy_data="$TEST_ROOT/codex-legacy-warning-removed/data" -mkdir -p "$codex_legacy_home/.config/superpowers/skills" "$codex_legacy_data" -assert_command_output \ - "Codex SessionStart omits obsolete legacy custom-skill warning" \ - "nested" \ - "" \ - "Superpowers now uses"$'\037'"~/.config/superpowers/skills"$'\037'"~/.claude/skills"$'\037'"legacy" \ - "$codex_legacy_home" \ - PLUGIN_DATA="$codex_legacy_data" \ - CLAUDE_PLUGIN_DATA="$codex_legacy_data" \ - PLUGIN_ROOT="$REPO_ROOT" \ - CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \ - bash "$CODEX_HOOK_UNDER_TEST" - if [[ "$FAILURES" -gt 0 ]]; then echo "STATUS: FAILED ($FAILURES failure(s))" exit 1 diff --git a/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/SKILL.md b/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/SKILL.md index 08a354b5..6f9bc4cc 100644 --- a/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/SKILL.md +++ b/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/SKILL.md @@ -413,6 +413,29 @@ If not, use the Master rules exclusively. Now, generate the code... ``` +### Step 2c: Design Dials (optional) + +Three optional 1-10 sliders that tune `--design-system` output without changing your query. Add any combination of them to the same command: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --design-system --variance <1-10> --motion <1-10> --density <1-10> +``` + +| Dial | Low (1-3) | Mid (4-7) | High (8-10) | +|------|-----------|-----------|-------------| +| `--variance` | Centered / minimal (biases toward Minimalism-style categories) | Balanced / modern | Bold / asymmetric (biases toward Brutalism, Bento Grids) | +| `--motion` | Subtle micro-interactions | Standard scroll/stagger motion | Complex choreography (pin, Flip, SplitText) | +| `--density` | Spacious (24-96px spacing scale) | Standard (16-64px, current default) | Dense/dashboard (8-32px spacing scale) | + +- `--motion` attaches a ready-to-use GSAP snippet (with framework notes, Do/Don't, and performance notes) pulled from `--domain gsap`, matched to the resolved tier (Subtle/Standard/Complex). +- `--density` overrides the `--space-*` CSS variable table in the ASCII/markdown/MASTER.md output — use it for dashboards (high) vs. marketing pages (low) without hand-editing tokens. +- Leaving a dial unset keeps that part of the output exactly as it was before (no behavior change). + +**Example:** +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "internal analytics dashboard" --design-system --variance 8 --motion 7 --density 8 -p "Ops Console" +``` + ### Step 3: Supplement with Detailed Searches (as needed) After getting the design system, use domain searches to get additional details: @@ -463,6 +486,7 @@ python3 skills/ui-ux-pro-max/scripts/search.py "" --stack | `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof | | `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie | | `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading | +| `gsap` | GSAP animation skeletons by intensity tier | scroll reveal, stagger, magnetic cursor, page transition | | `google-fonts` | Individual Google Fonts lookup | sans serif, monospace, japanese, variable font, popular | | `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache | | `web` | App interface guidelines (iOS/Android/React Native) | accessibilityLabel, touch targets, safe areas, Dynamic Type | diff --git a/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/data/motion.csv b/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/data/motion.csv new file mode 100644 index 00000000..e5555fec --- /dev/null +++ b/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/data/motion.csv @@ -0,0 +1,17 @@ +No,Category,Intensity Tier,Keywords,Trigger,Duration,Easing,GSAP Snippet,Framework Notes,Do,Don't,Performance Notes +1,Hover Micro-interaction,Subtle,"hover, button, opacity, lift, press feedback",hover,150-200ms,power1.out,"gsap.to(el, { y: -1, opacity: 0.9, duration: 0.15, ease: 'power1.out' });",Bind on mouseenter/mouseleave; in React wrap in a ref + useEffect (or onMouseEnter/onMouseLeave props directly calling gsap.to),Keep displacement under 2px so it reads as feedback not motion,Don't animate layout-affecting props (width/height/margin) on hover,Runs on transform/opacity only so it stays on the compositor thread +2,Hover Micro-interaction,Standard,"hover, card, scale, tilt, cursor feedback",hover,200-300ms,power2.out,"gsap.to(el, { y: -4, scale: 1.02, boxShadow: '0 12px 24px rgba(0,0,0,0.12)', duration: 0.25, ease: 'power2.out' });","Use gsap.quickTo(el, 'y') for cards with many hover targets to avoid re-creating tweens every event",Pair with a matching mouseleave tween that reverses the same properties,Don't leave the hover state stuck if the pointer leaves fast; always attach the reverse tween,quickTo() avoids GC churn on lists with 20+ hoverable cards +3,Hover Micro-interaction,Complex,"hover, magnetic, cursor follow, 3d tilt",hover + mousemove,300-500ms,"elastic.out(1,0.4)","const xTo = gsap.quickTo(el, 'x', { duration: 0.4, ease: 'elastic.out(1,0.4)' }); const yTo = gsap.quickTo(el, 'y', { duration: 0.4, ease: 'elastic.out(1,0.4)' }); el.addEventListener('mousemove', (e) => { const r = el.getBoundingClientRect(); xTo((e.clientX - r.left - r.width/2) * 0.3); yTo((e.clientY - r.top - r.height/2) * 0.3); });",Debounce is not needed since quickTo interpolates; remove listeners on component unmount in React/Vue to avoid leaks,Clamp the pull strength (e.g. * 0.3) so the element never fully leaves its hit box,Don't apply magnetic effect to more than 1-2 focal elements per screen; it becomes noisy,Use will-change: transform on the target element for smoother compositing +4,Scroll Reveal,Subtle,"scroll, fade in, reveal, on view",scroll (viewport enter),300-400ms,power1.out,"gsap.from(el, { opacity: 0, y: 12, duration: 0.35, ease: 'power1.out', scrollTrigger: { trigger: el, start: 'top 90%', toggleActions: 'play none none reverse' } });",Requires the ScrollTrigger plugin registered once via gsap.registerPlugin(ScrollTrigger),"Keep the y offset small (8-16px) so it reads as a fade, not a slide",Don't reveal below-the-fold content needed for SEO/crawlers as invisible-by-default without a no-JS fallback,toggleActions 'play none none reverse' avoids re-triggering on every scroll direction change +5,Scroll Reveal,Standard,"scroll, slide up, staggered section, reveal",scroll (viewport enter),400-600ms,power2.out,"gsap.from(el.children, { opacity: 0, y: 24, duration: 0.5, stagger: 0.08, ease: 'power2.out', scrollTrigger: { trigger: el, start: 'top 85%' } });","In React use useGSAP(() => {...}, { scope: containerRef }) from @gsap/react to auto-cleanup on unmount",Scope the ScrollTrigger to the section container so it doesn't re-scan the whole page,Don't stagger more than ~8 children; beyond that the last items feel laggy,Set scroller/markers: false in production; markers is dev-only +6,Scroll Reveal,Complex,"scroll, pin, scrub, storytelling, scrollytelling",scroll (continuous scrub),tied to scroll position,none (scrub-driven),"gsap.timeline({ scrollTrigger: { trigger: section, start: 'top top', end: '+=150%', scrub: 1, pin: true } }).from('.headline', { opacity: 0, y: 40 }).to('.bg-layer', { yPercent: -20 }, '<');",Pinning needs the section to have deterministic height; recalc ScrollTrigger.refresh() after images/fonts load,Use scrub: true or a small number (0.5-1.5) instead of instant jumps so it feels tied to the scrollbar,Don't pin more than 1-2 sections per page; excessive pinning fights native scroll feel and hurts mobile UX,"Pinning forces layout reflow; test on mid-tier mobile devices, not just desktop" +7,Stagger List,Subtle,"list, stagger, cards, grid entrance",load or scroll,250-350ms,power1.out,"gsap.from('.list-item', { opacity: 0, y: 8, duration: 0.3, stagger: 0.03 });",Select items with a stable class/data-attribute (not array index) so re-renders in React don't break targeting,Keep per-item stagger delay small (0.02-0.04s) for lists longer than 10 items,Don't stagger by more than 0.1s per item on long lists; total reveal time becomes sluggish,"For virtualized lists, only animate items currently mounted in the DOM" +8,Stagger List,Standard,"grid, bento, cards, staggered scale",load or scroll,300-450ms,back.out(1.4),"gsap.from('.grid-item', { opacity: 0, scale: 0.92, y: 16, duration: 0.4, stagger: { each: 0.06, from: 'start', grid: 'auto' }, ease: 'back.out(1.4)' });",grid: 'auto' lets GSAP infer rows/columns from a CSS grid layout for a natural wave stagger,Combine with from: 'center' for a bento-grid layout to draw the eye inward first,Don't use back.out on dense data tables; the overshoot reads as sloppy on informational UI,Group DOM writes; avoid interleaving layout reads (getBoundingClientRect) between staggered tweens +9,Stagger List,Complex,"stagger, wave, text reveal, split text",load or scroll,400-700ms,expo.out,"const split = new SplitText(headline, { type: 'chars' }); gsap.from(split.chars, { opacity: 0, y: 20, rotateX: -40, duration: 0.6, stagger: 0.015, ease: 'expo.out' });",SplitText is a GSAP Club/paid plugin; confirm license before shipping and provide a plain fade fallback if unavailable,Revert SplitText on unmount/cleanup (split.revert()) to restore original text nodes for accessibility tools,Don't split-animate long paragraphs; reserve for short headlines (under ~8 words),Splitting text creates one element per character; keep it to headline-length copy only for DOM size +10,Page Transition,Subtle,"route change, fade, page transition",route change,200-300ms,power1.inOut,"gsap.to(main, { opacity: 0, duration: 0.2, onComplete: () => { navigate(); gsap.fromTo(main, { opacity: 0 }, { opacity: 1, duration: 0.2 }); } });","Pair with the router's transition hooks (Next.js App Router transitions, React Router's useNavigate, Vue Router's beforeEach/afterEach)",Preload the destination route's critical assets before the exit tween finishes,Don't block navigation on animation; cap exit duration at ~250ms so the app never feels unresponsive,Exit animation should always resolve faster than entrance (asymmetric timing) so back/forward feels snappy +11,Page Transition,Standard,"route change, slide, overlay wipe",route change,400-600ms,power2.inOut,"const tl = gsap.timeline(); tl.to('.transition-overlay', { yPercent: 0, duration: 0.4, ease: 'power2.inOut' }).call(navigate).to('.transition-overlay', { yPercent: -100, duration: 0.4, ease: 'power2.inOut', delay: 0.1 });",Keep the overlay element mounted at the layout root (outside the page component) so it survives the route swap,Show a lightweight loading indicator if the destination route's data fetch outlasts the overlay,Don't tie the overlay's reveal directly to data-fetch completion without a max-wait timeout; a slow API stalls the whole transition,Prefer CSS transform (yPercent) over top/left to keep the overlay animation on the compositor thread +12,Page Transition,Complex,"shared element, morph, hero transition",route change,500-800ms,expo.inOut,"const state = Flip.getState('.hero-image'); navigate(); Flip.from(state, { duration: 0.6, ease: 'expo.inOut', absolute: true, zIndex: 100 });",Requires the GSAP Flip plugin; the 'from' and 'to' route must render the same element with a shared data-flip-id,Verify the shared element exists in both DOM states before calling Flip.from to avoid a silent no-op,Don't use shared-element transitions across more than one element pair per navigation; compounding Flips are hard to time correctly,Flip recalculates layout (FLIP technique) so test on low-end devices for jank +13,Parallax Scroll,Subtle,"parallax, background, depth, scroll speed",scroll (continuous),tied to scroll position,linear (scrub),"gsap.to('.bg-layer', { yPercent: 10, ease: 'none', scrollTrigger: { trigger: section, scrub: true } });","Apply parallax to background/decorative layers only, never to text or interactive controls",Keep the yPercent delta small (5-15) so foreground and background never desync distractingly,Don't parallax body copy; it hurts reading comfort and can trigger motion sickness,will-change: transform on the parallax layer only; remove it after scroll settles to free GPU memory +14,Parallax Scroll,Standard,"multi-layer parallax, depth, hero background",scroll (continuous),tied to scroll position,linear (scrub),"gsap.utils.toArray('.parallax-layer').forEach((layer, i) => { gsap.to(layer, { yPercent: (i + 1) * -8, ease: 'none', scrollTrigger: { trigger: layer.parentElement, scrub: 0.5 } }); });",Layer count beyond 3-4 has diminishing visual return and multiplies scroll-listener cost,"Vary speed per layer (background slowest, foreground fastest) to sell the depth illusion",Don't let parallax layers overflow their container; clip with overflow: hidden on the wrapper,Batch all layers under one ScrollTrigger container where possible instead of one per layer +15,Loading / Skeleton,Subtle,"loading, skeleton, shimmer, pulse",on mount / async wait,1200-1600ms loop,sine.inOut,"gsap.to('.skeleton', { backgroundPosition: '200% 0', duration: 1.4, ease: 'sine.inOut', repeat: -1 });",Kill the loop tween (tween.kill()) as soon as real content mounts to avoid orphaned repeating animations,Use a CSS gradient background-position sweep rather than opacity pulsing; reads as 'loading' more clearly,Don't run more than one shimmer loop per skeleton group; sync them under one timeline so the wave reads as a single unit,repeat: -1 tweens are cheap but must be explicitly killed on unmount or they leak in SPA route changes +16,Loading / Skeleton,Standard,"progress, spinner, morphing loader",on mount / async wait,800-1200ms loop,power1.inOut,"gsap.timeline({ repeat: -1 }).to('.loader-dot', { y: -8, duration: 0.4, stagger: { each: 0.15, yoyo: true, repeat: 1 } });",Wrap the whole loop timeline in useGSAP with { revertOnUpdate: false } in React so it isn't rebuilt every render,Cap total loop duration under ~1.5s so long waits don't feel like the UI froze on a single beat,Don't use elaborate loaders for sub-300ms waits; they flash and feel worse than no indicator,Pause the timeline (tl.pause()) when the loading tab/view is not visible to save CPU on background tabs diff --git a/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/scripts/core.py b/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/scripts/core.py index 8d020654..9d05db29 100755 --- a/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/scripts/core.py +++ b/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/scripts/core.py @@ -55,6 +55,11 @@ CSV_CONFIG = { "search_cols": ["Category", "Icon Name", "Keywords", "Best For"], "output_cols": ["Category", "Icon Name", "Keywords", "Library", "Import Code", "Usage", "Best For", "Style"] }, + "gsap": { + "file": "motion.csv", + "search_cols": ["Category", "Intensity Tier", "Keywords", "Trigger"], + "output_cols": ["Category", "Intensity Tier", "Trigger", "Duration", "Easing", "GSAP Snippet", "Framework Notes", "Do", "Don't", "Performance Notes"] + }, "react": { "file": "react-performance.csv", "search_cols": ["Category", "Issue", "Keywords", "Description"], @@ -89,6 +94,12 @@ STACK_CONFIG = { "threejs": {"file": "stacks/threejs.csv"}, "angular": {"file": "stacks/angular.csv"}, "laravel": {"file": "stacks/laravel.csv"}, + "javafx": {"file": "stacks/javafx.csv"}, + "wpf": {"file": "stacks/wpf.csv"}, + "winui": {"file": "stacks/winui.csv"}, + "avalonia": {"file": "stacks/avalonia.csv"}, + "uno": {"file": "stacks/uno.csv"}, + "uwp": {"file": "stacks/uwp.csv"}, } # Common columns for all stacks @@ -117,7 +128,7 @@ class BM25: def tokenize(self, text): """Lowercase, split, remove punctuation, filter short words""" text = re.sub(r'[^\w\s]', ' ', str(text).lower()) - return [w for w in text.split() if len(w) > 2] + return [w for w in text.split() if len(w) >= 2] def fit(self, documents): """Build BM25 index from documents""" @@ -209,6 +220,7 @@ def detect_domain(query): "typography": ["font pairing", "typography pairing", "heading font", "body font"], "google-fonts": ["google font", "font family", "font weight", "font style", "variable font", "noto", "font for", "find font", "font subset", "font language", "monospace font", "serif font", "sans serif font", "display font", "handwriting font", "font", "typography", "serif", "sans"], "icons": ["icon", "icons", "lucide", "heroicons", "symbol", "glyph", "pictogram", "svg icon"], + "gsap": ["gsap", "quickto", "scrolltrigger", "stagger", "magnetic cursor", "parallax", "page transition", "scroll reveal", "scroll-triggered", "scrollytelling", "flip plugin", "splittext", "shimmer", "skeleton loader"], "react": ["react", "next.js", "nextjs", "suspense", "memo", "usecallback", "useeffect", "rerender", "bundle", "waterfall", "barrel", "dynamic import", "rsc", "server component"], "web": ["aria", "focus", "outline", "semantic", "virtualize", "autocomplete", "form", "input type", "preconnect"] } diff --git a/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/scripts/design_system.py b/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/scripts/design_system.py index d3152e5f..62dfbc8a 100644 --- a/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/scripts/design_system.py +++ b/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/scripts/design_system.py @@ -16,10 +16,18 @@ Usage: import csv import json import os +import sys +import io from datetime import datetime from pathlib import Path from core import search, DATA_DIR +# Force UTF-8 for stdout/stderr to handle emojis/box-drawing chars on Windows (cp1252 default) +if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8': + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8': + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + # ============ CONFIGURATION ============ REASONING_FILE = "ui-reasoning.csv" @@ -32,6 +40,39 @@ SEARCH_CONFIG = { "typography": {"max_results": 2} } +# ============ DESIGN DIALS (1-10) ============ +# Inspired by taste-skill's DESIGN_VARIANCE / MOTION_INTENSITY / VISUAL_DENSITY +# knobs: three optional 1-10 sliders that bias the existing query-based search +# instead of replacing it. Each dial buckets into a low/mid/high tier. +DIAL_TIERS = { + "variance": [ + (1, 3, {"label": "Centered / Minimal", "style_keywords": ["Minimalism", "Exaggerated Minimalism", "centered", "symmetric", "grid-based"]}), + (4, 7, {"label": "Balanced / Modern", "style_keywords": ["modern", "structured", "balanced"]}), + (8, 10, {"label": "Bold / Asymmetric", "style_keywords": ["Brutalism", "Bento Grids", "asymmetric", "experimental"]}), + ], + "motion": [ + (1, 3, {"label": "Subtle", "tier": "Subtle"}), + (4, 7, {"label": "Standard", "tier": "Standard"}), + (8, 10, {"label": "Complex", "tier": "Complex"}), + ], + "density": [ + (1, 3, {"label": "Spacious", "spacing": {"xs": "4px", "sm": "8px", "md": "24px", "lg": "32px", "xl": "48px", "2xl": "64px", "3xl": "96px"}}), + (4, 7, {"label": "Standard", "spacing": {"xs": "4px", "sm": "8px", "md": "16px", "lg": "24px", "xl": "32px", "2xl": "48px", "3xl": "64px"}}), + (8, 10, {"label": "Dense / Dashboard", "spacing": {"xs": "2px", "sm": "4px", "md": "8px", "lg": "12px", "xl": "16px", "2xl": "24px", "3xl": "32px"}}), + ], +} + + +def _resolve_dial(dial_name: str, value) -> dict: + """Bucket a 1-10 dial value into its tier config. Returns None if value is None.""" + if value is None: + return None + value = max(1, min(10, int(value))) + for lo, hi, info in DIAL_TIERS[dial_name]: + if lo <= value <= hi: + return {**info, "value": value} + return None + # ============ DESIGN SYSTEM GENERATOR ============ class DesignSystemGenerator: @@ -160,8 +201,18 @@ class DesignSystemGenerator: """Extract results list from search result dict.""" return search_result.get("results", []) - def generate(self, query: str, project_name: str = None) -> dict: - """Generate complete design system recommendation.""" + def generate(self, query: str, project_name: str = None, + variance: int = None, motion: int = None, density: int = None) -> dict: + """Generate complete design system recommendation. + + variance/motion/density are optional 1-10 dials (see DIAL_TIERS) that bias + style selection, pull in a matching motion.csv snippet, and override the + spacing scale, without changing behavior when left unset. + """ + variance_info = _resolve_dial("variance", variance) + motion_info = _resolve_dial("motion", motion) + density_info = _resolve_dial("density", density) + # Step 1: First search product to get category product_result = search(query, "product", 1) product_results = product_result.get("results", []) @@ -173,8 +224,14 @@ class DesignSystemGenerator: reasoning = self._apply_reasoning(category, {}) style_priority = reasoning.get("style_priority", []) + # DESIGN_VARIANCE dial: bias style retrieval/selection toward + # centered-minimal (low) or bold-asymmetric (high) keywords. + effective_style_priority = style_priority + if variance_info: + effective_style_priority = variance_info["style_keywords"] + style_priority + # Step 3: Multi-domain search with style priority hints - search_results = self._multi_domain_search(query, style_priority) + search_results = self._multi_domain_search(query, effective_style_priority) search_results["product"] = product_result # Reuse product search # Step 4: Select best matches from each domain using priority @@ -183,11 +240,24 @@ class DesignSystemGenerator: typography_results = self._extract_results(search_results.get("typography", {})) landing_results = self._extract_results(search_results.get("landing", {})) - best_style = self._select_best_match(style_results, reasoning.get("style_priority", [])) + best_style = self._select_best_match(style_results, effective_style_priority) best_color = color_results[0] if color_results else {} best_typography = typography_results[0] if typography_results else {} best_landing = landing_results[0] if landing_results else {} + # MOTION_INTENSITY dial: pull a matching GSAP skeleton from motion.csv + # (domain key is "gsap", not "motion" - PR #296 already owns the "motion" + # domain for Emil Kowalski's motion-design principles, motion-principles.csv). + motion_snippet = {} + if motion_info: + motion_result = search(f"{query} {motion_info['tier']}", "gsap", 5) + motion_matches = motion_result.get("results", []) + tiered = [m for m in motion_matches if m.get("Intensity Tier") == motion_info["tier"]] + if tiered: + motion_snippet = tiered[0] + elif motion_matches: + motion_snippet = motion_matches[0] + # Step 5: Build final recommendation # Combine effects from both reasoning and style search style_effects = best_style.get("Effects & Animation", "") @@ -242,7 +312,17 @@ class DesignSystemGenerator: "key_effects": combined_effects, "anti_patterns": reasoning.get("anti_patterns", ""), "decision_rules": reasoning.get("decision_rules", {}), - "severity": reasoning.get("severity", "MEDIUM") + "severity": reasoning.get("severity", "MEDIUM"), + "dials": { + "variance": variance_info["value"] if variance_info else None, + "variance_label": variance_info["label"] if variance_info else None, + "motion": motion_info["value"] if motion_info else None, + "motion_label": motion_info["label"] if motion_info else None, + "density": density_info["value"] if density_info else None, + "density_label": density_info["label"] if density_info else None, + }, + "motion_snippet": motion_snippet, + "spacing_scale": density_info["spacing"] if density_info else None, } @@ -288,6 +368,8 @@ def format_ascii_box(design_system: dict) -> str: typography = design_system.get("typography", {}) effects = design_system.get("key_effects", "") anti_patterns = design_system.get("anti_patterns", "") + dials = design_system.get("dials", {}) + motion_snippet = design_system.get("motion_snippet", {}) def wrap_text(text: str, prefix: str, width: int) -> list: """Wrap long text into multiple lines.""" @@ -321,6 +403,16 @@ def format_ascii_box(design_system: dict) -> str: lines.append("╚" + "═" * w + "╝") lines.append("┌" + "─" * w + "┐") + # Design Dials section (only if at least one dial was set) + if any(dials.get(k) is not None for k in ("variance", "motion", "density")): + lines.append(section_header("DESIGN DIALS", BOX_WIDTH + 1)) + if dials.get("variance") is not None: + lines.append(f"│ Variance: {dials['variance']}/10 — {dials['variance_label']}".ljust(BOX_WIDTH) + "│") + if dials.get("motion") is not None: + lines.append(f"│ Motion: {dials['motion']}/10 — {dials['motion_label']}".ljust(BOX_WIDTH) + "│") + if dials.get("density") is not None: + lines.append(f"│ Density: {dials['density']}/10 — {dials['density_label']}".ljust(BOX_WIDTH) + "│") + # Pattern section lines.append(section_header("PATTERN", BOX_WIDTH + 1)) lines.append(f"│ Name: {pattern.get('name', '')}".ljust(BOX_WIDTH) + "│") @@ -394,6 +486,17 @@ def format_ascii_box(design_system: dict) -> str: for line in wrap_text(effects, "│ ", BOX_WIDTH): lines.append(line.ljust(BOX_WIDTH) + "│") + # Motion section (GSAP skeleton, only if --motion dial was set) + if motion_snippet: + lines.append(section_header("MOTION", BOX_WIDTH + 1)) + lines.append(f"│ {motion_snippet.get('Category', '')} ({motion_snippet.get('Intensity Tier', '')})".ljust(BOX_WIDTH) + "│") + lines.append(f"│ Trigger: {motion_snippet.get('Trigger', '')} | Duration: {motion_snippet.get('Duration', '')} | Easing: {motion_snippet.get('Easing', '')}".ljust(BOX_WIDTH) + "│") + for line in wrap_text(f"GSAP: {motion_snippet.get('GSAP Snippet', '')}", "│ ", BOX_WIDTH): + lines.append(line.ljust(BOX_WIDTH) + "│") + if motion_snippet.get("Framework Notes"): + for line in wrap_text(f"Framework: {motion_snippet.get('Framework Notes', '')}", "│ ", BOX_WIDTH): + lines.append(line.ljust(BOX_WIDTH) + "│") + # Anti-patterns section if anti_patterns: lines.append(section_header("AVOID", BOX_WIDTH + 1)) @@ -428,11 +531,24 @@ def format_markdown(design_system: dict) -> str: typography = design_system.get("typography", {}) effects = design_system.get("key_effects", "") anti_patterns = design_system.get("anti_patterns", "") + dials = design_system.get("dials", {}) + motion_snippet = design_system.get("motion_snippet", {}) lines = [] lines.append(f"## Design System: {project}") lines.append("") + # Design Dials section (only if at least one dial was set) + if any(dials.get(k) is not None for k in ("variance", "motion", "density")): + lines.append("### Design Dials") + if dials.get("variance") is not None: + lines.append(f"- **Variance:** {dials['variance']}/10 — {dials['variance_label']}") + if dials.get("motion") is not None: + lines.append(f"- **Motion:** {dials['motion']}/10 — {dials['motion_label']}") + if dials.get("density") is not None: + lines.append(f"- **Density:** {dials['density']}/10 — {dials['density_label']}") + lines.append("") + # Pattern section lines.append("### Pattern") lines.append(f"- **Name:** {pattern.get('name', '')}") @@ -507,6 +623,23 @@ def format_markdown(design_system: dict) -> str: lines.append(f"{effects}") lines.append("") + # Motion section (GSAP skeleton, only if --motion dial was set) + if motion_snippet: + lines.append("### Motion") + lines.append(f"**{motion_snippet.get('Category', '')}** ({motion_snippet.get('Intensity Tier', '')}) — Trigger: {motion_snippet.get('Trigger', '')} | Duration: {motion_snippet.get('Duration', '')} | Easing: `{motion_snippet.get('Easing', '')}`") + lines.append("```js") + lines.append(motion_snippet.get("GSAP Snippet", "")) + lines.append("```") + if motion_snippet.get("Framework Notes"): + lines.append(f"*Framework notes: {motion_snippet.get('Framework Notes', '')}*") + motion_do = motion_snippet.get("Do", "") + motion_dont = motion_snippet.get("Don't", "") + if motion_do: + lines.append(f"- ✅ {motion_do}") + if motion_dont: + lines.append(f"- ❌ {motion_dont}") + lines.append("") + # Anti-patterns section if anti_patterns: lines.append("### Avoid (Anti-patterns)") @@ -529,8 +662,9 @@ def format_markdown(design_system: dict) -> str: # ============ MAIN ENTRY POINT ============ -def generate_design_system(query: str, project_name: str = None, output_format: str = "ascii", - persist: bool = False, page: str = None, output_dir: str = None) -> str: +def generate_design_system(query: str, project_name: str = None, output_format: str = "ascii", + persist: bool = False, page: str = None, output_dir: str = None, + variance: int = None, motion: int = None, density: int = None) -> str: """ Main entry point for design system generation. @@ -541,13 +675,16 @@ def generate_design_system(query: str, project_name: str = None, output_format: persist: If True, save design system to design-system/ folder page: Optional page name for page-specific override file output_dir: Optional output directory (defaults to current working directory) + variance: Optional 1-10 DESIGN_VARIANCE dial (1=centered/minimal, 10=bold/asymmetric) + motion: Optional 1-10 MOTION_INTENSITY dial, pulls a matching GSAP snippet from motion.csv + density: Optional 1-10 VISUAL_DENSITY dial, overrides the spacing scale (1=spacious, 10=dense) Returns: Formatted design system string """ generator = DesignSystemGenerator() - design_system = generator.generate(query, project_name) - + design_system = generator.generate(query, project_name, variance=variance, motion=motion, density=density) + # Persist to files if requested if persist: persist_design_system(design_system, page, output_dir, query) @@ -573,8 +710,9 @@ def persist_design_system(design_system: dict, page: str = None, output_dir: str """ base_dir = Path(output_dir) if output_dir else Path.cwd() - # Use project name for project-specific folder - project_name = design_system.get("project_name", "default") + # Use project name for project-specific folder. Coalesce falsy values + # (missing key, explicit None, or "") so the .lower() below can't crash. + project_name = design_system.get("project_name") or "default" project_slug = project_name.lower().replace(' ', '-') design_system_dir = base_dir / "design-system" / project_slug @@ -618,11 +756,14 @@ def format_master_md(design_system: dict) -> str: typography = design_system.get("typography", {}) effects = design_system.get("key_effects", "") anti_patterns = design_system.get("anti_patterns", "") - + dials = design_system.get("dials", {}) + motion_snippet = design_system.get("motion_snippet", {}) + spacing_scale = design_system.get("spacing_scale") + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - + lines = [] - + # Logic header lines.append("# Design System Master File") lines.append("") @@ -635,6 +776,15 @@ def format_master_md(design_system: dict) -> str: lines.append(f"**Project:** {project}") lines.append(f"**Generated:** {timestamp}") lines.append(f"**Category:** {design_system.get('category', 'General')}") + if any(dials.get(k) is not None for k in ("variance", "motion", "density")): + dial_parts = [] + if dials.get("variance") is not None: + dial_parts.append(f"Variance {dials['variance']}/10 ({dials['variance_label']})") + if dials.get("motion") is not None: + dial_parts.append(f"Motion {dials['motion']}/10 ({dials['motion_label']})") + if dials.get("density") is not None: + dial_parts.append(f"Density {dials['density']}/10 ({dials['density_label']})") + lines.append(f"**Design Dials:** {' | '.join(dial_parts)}") lines.append("") lines.append("---") lines.append("") @@ -686,18 +836,24 @@ def format_master_md(design_system: dict) -> str: lines.append("```") lines.append("") - # Spacing Variables + # Spacing Variables (overridden by the VISUAL_DENSITY dial when set) + default_spacing = DIAL_TIERS["density"][1][2]["spacing"] # mid-tier = the historical defaults + scale = spacing_scale or default_spacing + spacing_usage = { + "xs": "Tight gaps", "sm": "Icon gaps, inline spacing", "md": "Standard padding", + "lg": "Section padding", "xl": "Large gaps", "2xl": "Section margins", "3xl": "Hero padding", + } lines.append("### Spacing Variables") lines.append("") + if spacing_scale: + lines.append(f"*Density: {dials.get('density')}/10 — {dials.get('density_label')}*") + lines.append("") lines.append("| Token | Value | Usage |") lines.append("|-------|-------|-------|") - lines.append("| `--space-xs` | `4px` / `0.25rem` | Tight gaps |") - lines.append("| `--space-sm` | `8px` / `0.5rem` | Icon gaps, inline spacing |") - lines.append("| `--space-md` | `16px` / `1rem` | Standard padding |") - lines.append("| `--space-lg` | `24px` / `1.5rem` | Section padding |") - lines.append("| `--space-xl` | `32px` / `2rem` | Large gaps |") - lines.append("| `--space-2xl` | `48px` / `3rem` | Section margins |") - lines.append("| `--space-3xl` | `64px` / `4rem` | Hero padding |") + for token in ("xs", "sm", "md", "lg", "xl", "2xl", "3xl"): + px_value = scale[token] + rem_value = f"{int(px_value.rstrip('px')) / 16:g}rem" + lines.append(f"| `--space-{token}` | `{px_value}` / `{rem_value}` | {spacing_usage[token]} |") lines.append("") # Shadow Depths @@ -839,7 +995,32 @@ def format_master_md(design_system: dict) -> str: lines.append(f"- **CTA Placement:** {pattern.get('cta_placement', '')}") lines.append(f"- **Section Order:** {pattern.get('sections', '')}") lines.append("") - + + # Motion section (GSAP skeleton, only if --motion dial was set) + if motion_snippet: + lines.append("---") + lines.append("") + lines.append("## Motion") + lines.append("") + lines.append(f"**{motion_snippet.get('Category', '')}** ({motion_snippet.get('Intensity Tier', '')}) — Trigger: {motion_snippet.get('Trigger', '')} | Duration: {motion_snippet.get('Duration', '')} | Easing: `{motion_snippet.get('Easing', '')}`") + lines.append("") + lines.append("```js") + lines.append(motion_snippet.get("GSAP Snippet", "")) + lines.append("```") + lines.append("") + if motion_snippet.get("Framework Notes"): + lines.append(f"**Framework notes:** {motion_snippet.get('Framework Notes', '')}") + lines.append("") + motion_do = motion_snippet.get("Do", "") + motion_dont = motion_snippet.get("Don't", "") + if motion_do: + lines.append(f"- ✅ {motion_do}") + if motion_dont: + lines.append(f"- ❌ {motion_dont}") + if motion_snippet.get("Performance Notes"): + lines.append(f"- ⚡ {motion_snippet.get('Performance Notes', '')}") + lines.append("") + # Anti-Patterns section lines.append("---") lines.append("") diff --git a/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/scripts/search.py b/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/scripts/search.py index ee1e340e..4707316d 100644 --- a/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/scripts/search.py +++ b/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/scripts/search.py @@ -1,114 +1,127 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -UI/UX Pro Max Search - BM25 search engine for UI/UX style guides -Usage: python search.py "" [--domain ] [--stack ] [--max-results 3] - python search.py "" --design-system [-p "Project Name"] - python search.py "" --design-system --persist [-p "Project Name"] [--page "dashboard"] - -Domains: style, prompt, color, chart, landing, product, ux, typography, google-fonts -Stacks: react, nextjs, vue, svelte, astro, swiftui, react-native, flutter, nuxtjs, nuxt-ui, html-tailwind, shadcn, jetpack-compose, threejs - -Persistence (Master + Overrides pattern): - --persist Save design system to design-system/MASTER.md - --page Also create a page-specific override file in design-system/pages/ -""" - -import argparse -import sys -import io -from core import CSV_CONFIG, AVAILABLE_STACKS, MAX_RESULTS, search, search_stack -from design_system import generate_design_system, persist_design_system - -# Force UTF-8 for stdout/stderr to handle emojis on Windows (cp1252 default) -if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8': - sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') -if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8': - sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') - - -def format_output(result): - """Format results for Claude consumption (token-optimized)""" - if "error" in result: - return f"Error: {result['error']}" - - output = [] - if result.get("stack"): - output.append(f"## UI Pro Max Stack Guidelines") - output.append(f"**Stack:** {result['stack']} | **Query:** {result['query']}") - else: - output.append(f"## UI Pro Max Search Results") - output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}") - output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n") - - for i, row in enumerate(result['results'], 1): - output.append(f"### Result {i}") - for key, value in row.items(): - value_str = str(value) - if len(value_str) > 300: - value_str = value_str[:300] + "..." - output.append(f"- **{key}:** {value_str}") - output.append("") - - return "\n".join(output) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="UI Pro Max Search") - parser.add_argument("query", help="Search query") - parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()), help="Search domain") - parser.add_argument("--stack", "-s", choices=AVAILABLE_STACKS, help=f"Stack-specific search. Available: {', '.join(AVAILABLE_STACKS)}") - parser.add_argument("--max-results", "-n", type=int, default=MAX_RESULTS, help="Max results (default: 3)") - parser.add_argument("--json", action="store_true", help="Output as JSON") - # Design system generation - parser.add_argument("--design-system", "-ds", action="store_true", help="Generate complete design system recommendation") - parser.add_argument("--project-name", "-p", type=str, default=None, help="Project name for design system output") - parser.add_argument("--format", "-f", choices=["ascii", "markdown"], default="ascii", help="Output format for design system") - # Persistence (Master + Overrides pattern) - parser.add_argument("--persist", action="store_true", help="Save design system to design-system/MASTER.md (creates hierarchical structure)") - parser.add_argument("--page", type=str, default=None, help="Create page-specific override file in design-system/pages/") - parser.add_argument("--output-dir", "-o", type=str, default=None, help="Output directory for persisted files (default: current directory)") - - args = parser.parse_args() - - # Design system takes priority - if args.design_system: - result = generate_design_system( - args.query, - args.project_name, - args.format, - persist=args.persist, - page=args.page, - output_dir=args.output_dir - ) - print(result) - - # Print persistence confirmation - if args.persist: - project_slug = args.project_name.lower().replace(' ', '-') if args.project_name else "default" - print("\n" + "=" * 60) - print(f"✅ Design system persisted to design-system/{project_slug}/") - print(f" 📄 design-system/{project_slug}/MASTER.md (Global Source of Truth)") - if args.page: - page_filename = args.page.lower().replace(' ', '-') - print(f" 📄 design-system/{project_slug}/pages/{page_filename}.md (Page Overrides)") - print("") - print(f"📖 Usage: When building a page, check design-system/{project_slug}/pages/[page].md first.") - print(f" If exists, its rules override MASTER.md. Otherwise, use MASTER.md.") - print("=" * 60) - # Stack search - elif args.stack: - result = search_stack(args.query, args.stack, args.max_results) - if args.json: - import json - print(json.dumps(result, indent=2, ensure_ascii=False)) - else: - print(format_output(result)) - # Domain search - else: - result = search(args.query, args.domain, args.max_results) - if args.json: - import json - print(json.dumps(result, indent=2, ensure_ascii=False)) - else: - print(format_output(result)) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +UI/UX Pro Max Search - BM25 search engine for UI/UX style guides +Usage: python search.py "" [--domain ] [--stack ] [--max-results 3] + python search.py "" --design-system [-p "Project Name"] + python search.py "" --design-system --persist [-p "Project Name"] [--page "dashboard"] + python search.py "" --design-system --variance 8 --motion 9 --density 7 + +Domains: style, prompt, color, chart, landing, product, ux, typography, google-fonts, gsap +Stacks: react, nextjs, vue, svelte, astro, swiftui, react-native, flutter, nuxtjs, nuxt-ui, html-tailwind, shadcn, jetpack-compose, threejs, angular, laravel, javafx, wpf, winui, avalonia, uno, uwp + +Design dials (1-10, only with --design-system): + --variance DESIGN_VARIANCE: 1=centered/minimal, 10=bold/asymmetric + --motion MOTION_INTENSITY: 1=subtle, 10=complex; attaches a GSAP snippet from motion.csv + --density VISUAL_DENSITY: 1=spacious, 10=dense/dashboard; overrides the spacing scale + +Persistence (Master + Overrides pattern): + --persist Save design system to design-system/MASTER.md + --page Also create a page-specific override file in design-system/pages/ +""" + +import argparse +import sys +import io +from core import CSV_CONFIG, AVAILABLE_STACKS, MAX_RESULTS, search, search_stack +from design_system import generate_design_system, persist_design_system + +# Force UTF-8 for stdout/stderr to handle emojis on Windows (cp1252 default) +if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8': + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8': + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +def format_output(result): + """Format results for Claude consumption (token-optimized)""" + if "error" in result: + return f"Error: {result['error']}" + + output = [] + if result.get("stack"): + output.append(f"## UI Pro Max Stack Guidelines") + output.append(f"**Stack:** {result['stack']} | **Query:** {result['query']}") + else: + output.append(f"## UI Pro Max Search Results") + output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}") + output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n") + + for i, row in enumerate(result['results'], 1): + output.append(f"### Result {i}") + for key, value in row.items(): + value_str = str(value) + if len(value_str) > 300: + value_str = value_str[:300] + "..." + output.append(f"- **{key}:** {value_str}") + output.append("") + + return "\n".join(output) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="UI Pro Max Search") + parser.add_argument("query", help="Search query") + parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()), help="Search domain") + parser.add_argument("--stack", "-s", choices=AVAILABLE_STACKS, help=f"Stack-specific search. Available: {', '.join(AVAILABLE_STACKS)}") + parser.add_argument("--max-results", "-n", type=int, default=MAX_RESULTS, help="Max results (default: 3)") + parser.add_argument("--json", action="store_true", help="Output as JSON") + # Design system generation + parser.add_argument("--design-system", "-ds", action="store_true", help="Generate complete design system recommendation") + parser.add_argument("--project-name", "-p", type=str, default=None, help="Project name for design system output") + parser.add_argument("--format", "-f", choices=["ascii", "markdown"], default="ascii", help="Output format for design system") + # Persistence (Master + Overrides pattern) + parser.add_argument("--persist", action="store_true", help="Save design system to design-system/MASTER.md (creates hierarchical structure)") + parser.add_argument("--page", type=str, default=None, help="Create page-specific override file in design-system/pages/") + parser.add_argument("--output-dir", "-o", type=str, default=None, help="Output directory for persisted files (default: current directory)") + # Design dials (1-10), only applied with --design-system + parser.add_argument("--variance", type=int, choices=range(1, 11), metavar="1-10", help="DESIGN_VARIANCE dial: 1=centered/minimal, 10=bold/asymmetric (only with --design-system)") + parser.add_argument("--motion", type=int, choices=range(1, 11), metavar="1-10", help="MOTION_INTENSITY dial: 1=subtle, 10=complex; pulls a matching GSAP snippet from motion.csv (only with --design-system)") + parser.add_argument("--density", type=int, choices=range(1, 11), metavar="1-10", help="VISUAL_DENSITY dial: 1=spacious, 10=dense/dashboard; overrides the spacing scale (only with --design-system)") + + args = parser.parse_args() + + # Design system takes priority + if args.design_system: + result = generate_design_system( + args.query, + args.project_name, + args.format, + persist=args.persist, + page=args.page, + output_dir=args.output_dir, + variance=args.variance, + motion=args.motion, + density=args.density + ) + print(result) + + # Print persistence confirmation + if args.persist: + project_slug = (args.project_name or args.query).lower().replace(' ', '-') + print("\n" + "=" * 60) + print(f"✅ Design system persisted to design-system/{project_slug}/") + print(f" 📄 design-system/{project_slug}/MASTER.md (Global Source of Truth)") + if args.page: + page_filename = args.page.lower().replace(' ', '-') + print(f" 📄 design-system/{project_slug}/pages/{page_filename}.md (Page Overrides)") + print("") + print(f"📖 Usage: When building a page, check design-system/{project_slug}/pages/[page].md first.") + print(f" If exists, its rules override MASTER.md. Otherwise, use MASTER.md.") + print("=" * 60) + # Stack search + elif args.stack: + result = search_stack(args.query, args.stack, args.max_results) + if args.json: + import json + print(json.dumps(result, indent=2, ensure_ascii=False)) + else: + print(format_output(result)) + # Domain search + else: + result = search(args.query, args.domain, args.max_results) + if args.json: + import json + print(json.dumps(result, indent=2, ensure_ascii=False)) + else: + print(format_output(result)) diff --git a/ui-ux-pro-max/CLAUDE.md b/ui-ux-pro-max/CLAUDE.md index 3d229a81..dbcdd117 100644 --- a/ui-ux-pro-max/CLAUDE.md +++ b/ui-ux-pro-max/CLAUDE.md @@ -20,6 +20,13 @@ python3 src/ui-ux-pro-max/scripts/search.py "" --domain [-n " --design-system --variance <1-10> --motion <1-10> --density <1-10> +``` +`--variance` biases style selection (centered/minimal → bold/asymmetric), `--motion` attaches a matching GSAP snippet from `motion.csv`, `--density` overrides the spacing-scale tokens (spacious → dense/dashboard). Any dial left unset behaves exactly as before. **Stack search:** ```bash diff --git a/ui-ux-pro-max/README.md b/ui-ux-pro-max/README.md index 7a30af16..d1847ed1 100755 --- a/ui-ux-pro-max/README.md +++ b/ui-ux-pro-max/README.md @@ -273,7 +273,7 @@ Many users ask about the differences between the open-source and premium version * **Enterprise Architecture:** A more comprehensive and scalable Design Token architecture, built for large-scale team deployments. * **Priority Support:** Dedicated technical assistance for teams and professionals who need an uninterrupted full design workflow. -👉 *For more details on upgrading to the Premium tier, visit [uipm.cc](https://uipm.cc).* +👉 *For more details on upgrading to the Premium tier, visit [uupm.cc](https://uupm.cc).* ## Installation diff --git a/ui-ux-pro-max/SOURCE.md b/ui-ux-pro-max/SOURCE.md index e4bf7f23..e05d1839 100644 --- a/ui-ux-pro-max/SOURCE.md +++ b/ui-ux-pro-max/SOURCE.md @@ -1,8 +1,8 @@ # Source - Repo: https://github.com/nextlevelbuilder/ui-ux-pro-max-skill -- Ref: 247bf48d0245a2bfb252ac6f3b4b434bfc9a6281 +- Ref: 9ceab49a1c6e9bf23fa9ebc49ca7d4f0c6303a99 - Remove-Paths: -- Snapshot: 2026-07-02 +- Snapshot: 2026-07-03 - Sync-Mode: render_skill - Notes: vendored into playbook branch thirdparty/skill diff --git a/ui-ux-pro-max/cli/assets/data/colors.csv b/ui-ux-pro-max/cli/assets/data/colors.csv index de74c2ce..77a511ff 100644 --- a/ui-ux-pro-max/cli/assets/data/colors.csv +++ b/ui-ux-pro-max/cli/assets/data/colors.csv @@ -159,4 +159,35 @@ No,Product Type,Primary,On Primary,Secondary,On Secondary,Accent,On Accent,Backg 158,Emergency SOS & Safety,#DC2626,#FFFFFF,#EF4444,#FFFFFF,#2563EB,#FFFFFF,#FFF1F2,#0F172A,#FFFFFF,#0F172A,#FCF1F1,#64748B,#FAE4E4,#DC2626,#FFFFFF,#DC2626,Alert red + safety blue 159,Wallpaper & Theme App,#7C3AED,#FFFFFF,#EC4899,#FFFFFF,#2563EB,#FFFFFF,#FAF5FF,#0F172A,#FFFFFF,#0F172A,#F7F3FD,#64748B,#EFE7FC,#DC2626,#FFFFFF,#7C3AED,Aesthetic purple + trending pink 160,White Noise & Ambient Sound,#475569,#FFFFFF,#334155,#FFFFFF,#4338CA,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#131B2F,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#475569,Ambient grey + deep indigo on dark -161,Home Decoration & Interior Design,#78716C,#FFFFFF,#A8A29E,#FFFFFF,#D97706,#FFFFFF,#FAF5F2,#0F172A,#FFFFFF,#0F172A,#F6F6F6,#64748B,#EEEDED,#DC2626,#FFFFFF,#78716C,Interior warm grey + gold accent \ No newline at end of file +161,Home Decoration & Interior Design,#78716C,#FFFFFF,#A8A29E,#FFFFFF,#D97706,#FFFFFF,#FAF5F2,#0F172A,#FFFFFF,#0F172A,#F6F6F6,#64748B,#EEEDED,#DC2626,#FFFFFF,#78716C,Interior warm grey + gold accent +162,Academic Journal / Scholarly Publishing,#1E3A5F,#FFFFFF,#334155,#FFFFFF,#B45309,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#E9EEF5,#64748B,#CBD5E1,#DC2626,#FFFFFF,#1E3A5F,Scholarly navy + citation gold + serif accent +163,API Developer Portal,#0F172A,#FFFFFF,#1E293B,#FFFFFF,#22C55E,#0F172A,#020617,#F8FAFC,#0E1223,#F8FAFC,#1A1E2F,#94A3B8,#334155,#EF4444,#FFFFFF,#0F172A,Code dark + endpoint green + syntax colors +164,Forum / Discussion Board,#475569,#FFFFFF,#64748B,#FFFFFF,#2563EB,#FFFFFF,#F8FAFC,#1E293B,#FFFFFF,#1E293B,#EAEFF3,#64748B,#E2E8F0,#DC2626,#FFFFFF,#475569,Neutral grey + topic accent + unread indicator +165,Directory / Listing Site,#059669,#FFFFFF,#10B981,#0F172A,#D97706,#FFFFFF,#ECFDF5,#064E3B,#FFFFFF,#064E3B,#E8F1F3,#64748B,#A7F3D0,#DC2626,#FFFFFF,#059669,Category green + verified badge + map accent +166,Status Page / Incident Management,#16A34A,#FFFFFF,#22C55E,#0F172A,#DC2626,#FFFFFF,#F0FDF4,#14532D,#FFFFFF,#14532D,#E8F0F1,#64748B,#BBF7D0,#DC2626,#FFFFFF,#16A34A,Operational green + incident red + maintenance amber +167,Wiki / Encyclopedia,#1E3A8A,#FFFFFF,#3B82F6,#FFFFFF,#7C3AED,#FFFFFF,#F8FAFC,#1E40AF,#FFFFFF,#1E40AF,#E9EEF5,#64748B,#BFDBFE,#DC2626,#FFFFFF,#1E3A8A,Knowledge blue + link purple + clean white +168,Auction Platform,#0F172A,#FFFFFF,#1E293B,#FFFFFF,#16A34A,#0F172A,#020617,#F8FAFC,#0E1223,#F8FAFC,#1A1E2F,#94A3B8,#334155,#DC2626,#FFFFFF,#0F172A,Dark luxury + bid green + outbid red + urgency +169,Changelog / Release Notes,#475569,#FFFFFF,#64748B,#FFFFFF,#059669,#FFFFFF,#F8FAFC,#1E293B,#FFFFFF,#1E293B,#EAEFF3,#64748B,#E2E8F0,#DC2626,#FFFFFF,#475569,Feature green + fix blue + breaking red badges +170,Citizen Science Platform,#15803D,#FFFFFF,#22C55E,#0F172A,#D97706,#FFFFFF,#F0FDF4,#14532D,#FFFFFF,#14532D,#E8F0F1,#64748B,#BBF7D0,#DC2626,#FFFFFF,#15803D,Discovery green + volunteer badge + data neutral +171,Classifieds / Buy-Sell,#2563EB,#FFFFFF,#3B82F6,#FFFFFF,#16A34A,#FFFFFF,#EFF6FF,#1E40AF,#FFFFFF,#1E40AF,#E9EFF8,#64748B,#BFDBFE,#DC2626,#FFFFFF,#2563EB,Listing blue + price green + seller badge +172,Conference / Symposium Landing Page,#1E3A5F,#FFFFFF,#2563EB,#FFFFFF,#A16207,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#E9EEF5,#64748B,#CBD5E1,#DC2626,#FFFFFF,#1E3A5F,Academic navy + gold keynote + track chips +173,Crowdfunding Platform,#D97706,#FFFFFF,#F59E0B,#0F172A,#16A34A,#FFFFFF,#FFFBEB,#0F172A,#FFFFFF,#0F172A,#FCF6F0,#64748B,#FAEEE1,#DC2626,#FFFFFF,#D97706,Funding progress amber + goal green + urgency +174,Digital Signage / Kiosk,#0F172A,#FFFFFF,#1E293B,#FFFFFF,#EF4444,#FFFFFF,#020617,#F8FAFC,#0E1223,#F8FAFC,#1A1E2F,#94A3B8,#334155,#EF4444,#FFFFFF,#0F172A,High contrast dark + brand accent + large touch targets +175,E-signature / Document Workflow,#1E3A5F,#FFFFFF,#2563EB,#FFFFFF,#16A34A,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#E9EEF5,#64748B,#CBD5E1,#DC2626,#FFFFFF,#1E3A5F,Trust navy + signature green + audit trail +176,Feature Flag / Config Management,#0F172A,#FFFFFF,#1E293B,#FFFFFF,#16A34A,#0F172A,#020617,#F8FAFC,#0E1223,#F8FAFC,#1A1E2F,#94A3B8,#334155,#DC2626,#FFFFFF,#0F172A,Enabled green + disabled grey + experimental amber +177,Government Portal / Civic Services,#1E40AF,#FFFFFF,#3B82F6,#FFFFFF,#16A34A,#FFFFFF,#EFF6FF,#1E3A8A,#FFFFFF,#1E3A8A,#E9EFF5,#64748B,#BFDBFE,#DC2626,#FFFFFF,#1E40AF,Professional blue + service green + accessibility +178,Grant / Funding Portal,#1E3A5F,#FFFFFF,#2563EB,#FFFFFF,#16A34A,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#E9EEF5,#64748B,#CBD5E1,#DC2626,#FFFFFF,#1E3A5F,Institution navy + funding green + deadline urgency +179,LMS (Learning Management System),#0D9488,#FFFFFF,#2DD4BF,#0F172A,#D97706,#FFFFFF,#F0FDFA,#134E4A,#FFFFFF,#134E4A,#E8F1F4,#64748B,#5EEAD4,#DC2626,#FFFFFF,#0D9488,Education teal + course amber + grade green +180,No-code / Low-code Builder,#7C3AED,#FFFFFF,#A78BFA,#0F172A,#EC4899,#FFFFFF,#FAF5FF,#4C1D95,#FFFFFF,#4C1D95,#ECEEF9,#64748B,#DDD6FE,#DC2626,#FFFFFF,#7C3AED,Builder purple + component pink + canvas neutral +181,Open Source Project Landing,#0F172A,#FFFFFF,#1E293B,#FFFFFF,#A16207,#FFFFFF,#020617,#F8FAFC,#0E1223,#F8FAFC,#1A1E2F,#94A3B8,#334155,#22C55E,#FFFFFF,#0F172A,Dark code + star gold + fork silver + sponsor purple +182,Patient Portal / Health Records,#0284C7,#FFFFFF,#0891B2,#FFFFFF,#16A34A,#FFFFFF,#F0F9FF,#0C4A6E,#FFFFFF,#0C4A6E,#E8F2F8,#64748B,#BAE6FD,#DC2626,#FFFFFF,#0284C7,Clinical blue + health green + alert red +183,Patent / IP Database,#475569,#FFFFFF,#64748B,#FFFFFF,#A16207,#FFFFFF,#F8FAFC,#1E293B,#FFFFFF,#1E293B,#EAEFF3,#64748B,#E2E8F0,#DC2626,#FFFFFF,#475569,Formal neutral + patent type chips + status badges +184,Q&A Community Platform,#2563EB,#FFFFFF,#0891B2,#FFFFFF,#D97706,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Knowledge blue + accepted green + reputation gold +185,Research Lab / University Department,#1E3A5F,#FFFFFF,#2563EB,#FFFFFF,#A16207,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#E9EEF5,#64748B,#CBD5E1,#DC2626,#FFFFFF,#1E3A5F,Institutional navy + research accent + serif headings +186,Resume / CV Builder,#1E3A5F,#FFFFFF,#2563EB,#FFFFFF,#16A34A,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#E9EEF5,#64748B,#CBD5E1,#DC2626,#FFFFFF,#1E3A5F,Professional navy + section accent + success green +187,Review Platform,#F59E0B,#0F172A,#FBBF24,#0F172A,#16A34A,#FFFFFF,#FFFBEB,#0F172A,#FFFFFF,#0F172A,#FCF6F0,#64748B,#FAEEE1,#DC2626,#FFFFFF,#F59E0B,Star gold + positive green + negative red +188,RPA / Automation Dashboard,#0F172A,#FFFFFF,#1E293B,#FFFFFF,#16A34A,#0F172A,#020617,#F8FAFC,#0E1223,#F8FAFC,#1A1E2F,#94A3B8,#334155,#DC2626,#FFFFFF,#0F172A,Dark terminal + running green + failed red + queued amber +189,Survey / Form Builder,#0D9488,#FFFFFF,#2DD4BF,#0F172A,#D97706,#FFFFFF,#F0FDFA,#134E4A,#FFFFFF,#134E4A,#E8F1F4,#64748B,#5EEAD4,#DC2626,#FFFFFF,#0D9488,Question teal + progress green + submit blue +190,Telemedicine Platform,#0891B2,#FFFFFF,#22D3EE,#0F172A,#16A34A,#FFFFFF,#F0FDFA,#134E4A,#FFFFFF,#134E4A,#E8F1F6,#64748B,#CCFBF1,#DC2626,#FFFFFF,#0891B2,Medical teal + video green + waiting amber +191,Testimonial & Social Proof Widget,#7C3AED,#FFFFFF,#A78BFA,#0F172A,#F59E0B,#0F172A,#FAF5FF,#4C1D95,#FFFFFF,#4C1D95,#ECEEF9,#64748B,#DDD6FE,#DC2626,#FFFFFF,#7C3AED,Trust purple + quote gold + verified blue +192,Ticketing / Box Office,#0F172A,#FFFFFF,#1E293B,#FFFFFF,#16A34A,#0F172A,#020617,#F8FAFC,#0E1223,#F8FAFC,#1A1E2F,#94A3B8,#334155,#DC2626,#FFFFFF,#0F172A,Event theme colors + available green + sold-out red diff --git a/ui-ux-pro-max/cli/assets/data/draft.csv b/ui-ux-pro-max/cli/assets/data/draft.csv old mode 100755 new mode 100644 diff --git a/ui-ux-pro-max/cli/assets/data/google-fonts.csv b/ui-ux-pro-max/cli/assets/data/google-fonts.csv old mode 100755 new mode 100644 diff --git a/ui-ux-pro-max/cli/assets/data/motion.csv b/ui-ux-pro-max/cli/assets/data/motion.csv new file mode 100644 index 00000000..e3dfc962 --- /dev/null +++ b/ui-ux-pro-max/cli/assets/data/motion.csv @@ -0,0 +1,17 @@ +No,Category,Intensity Tier,Keywords,Trigger,Duration,Easing,GSAP Snippet,Framework Notes,Do,Don't,Performance Notes +1,Hover Micro-interaction,Subtle,"hover, button, opacity, lift, press feedback",hover,150-200ms,power1.out,"gsap.to(el, { y: -1, opacity: 0.9, duration: 0.15, ease: 'power1.out' });",Bind on mouseenter/mouseleave; in React wrap in a ref + useEffect (or onMouseEnter/onMouseLeave props directly calling gsap.to),Keep displacement under 2px so it reads as feedback not motion,Don't animate layout-affecting props (width/height/margin) on hover,Runs on transform/opacity only so it stays on the compositor thread +2,Hover Micro-interaction,Standard,"hover, card, scale, tilt, cursor feedback",hover,200-300ms,power2.out,"gsap.to(el, { y: -4, scale: 1.02, boxShadow: '0 12px 24px rgba(0,0,0,0.12)', duration: 0.25, ease: 'power2.out' });","Use gsap.quickTo(el, 'y') for cards with many hover targets to avoid re-creating tweens every event",Pair with a matching mouseleave tween that reverses the same properties,Don't leave the hover state stuck if the pointer leaves fast; always attach the reverse tween,quickTo() avoids GC churn on lists with 20+ hoverable cards +3,Hover Micro-interaction,Complex,"hover, magnetic, cursor follow, 3d tilt",hover + mousemove,300-500ms,"elastic.out(1,0.4)","const xTo = gsap.quickTo(el, 'x', { duration: 0.4, ease: 'elastic.out(1,0.4)' }); const yTo = gsap.quickTo(el, 'y', { duration: 0.4, ease: 'elastic.out(1,0.4)' }); el.addEventListener('mousemove', (e) => { const r = el.getBoundingClientRect(); xTo((e.clientX - r.left - r.width/2) * 0.3); yTo((e.clientY - r.top - r.height/2) * 0.3); });",Debounce is not needed since quickTo interpolates; remove listeners on component unmount in React/Vue to avoid leaks,Clamp the pull strength (e.g. * 0.3) so the element never fully leaves its hit box,Don't apply magnetic effect to more than 1-2 focal elements per screen; it becomes noisy,Use will-change: transform on the target element for smoother compositing +4,Scroll Reveal,Subtle,"scroll, fade in, reveal, on view",scroll (viewport enter),300-400ms,power1.out,"gsap.from(el, { opacity: 0, y: 12, duration: 0.35, ease: 'power1.out', scrollTrigger: { trigger: el, start: 'top 90%', toggleActions: 'play none none reverse' } });",Requires the ScrollTrigger plugin registered once via gsap.registerPlugin(ScrollTrigger),"Keep the y offset small (8-16px) so it reads as a fade, not a slide",Don't reveal below-the-fold content needed for SEO/crawlers as invisible-by-default without a no-JS fallback,toggleActions 'play none none reverse' avoids re-triggering on every scroll direction change +5,Scroll Reveal,Standard,"scroll, slide up, staggered section, reveal",scroll (viewport enter),400-600ms,power2.out,"gsap.from(el.children, { opacity: 0, y: 24, duration: 0.5, stagger: 0.08, ease: 'power2.out', scrollTrigger: { trigger: el, start: 'top 85%' } });","In React use useGSAP(() => {...}, { scope: containerRef }) from @gsap/react to auto-cleanup on unmount",Scope the ScrollTrigger to the section container so it doesn't re-scan the whole page,Don't stagger more than ~8 children; beyond that the last items feel laggy,Set scroller/markers: false in production; markers is dev-only +6,Scroll Reveal,Complex,"scroll, pin, scrub, storytelling, scrollytelling",scroll (continuous scrub),tied to scroll position,none (scrub-driven),"gsap.timeline({ scrollTrigger: { trigger: section, start: 'top top', end: '+=150%', scrub: 1, pin: true } }).from('.headline', { opacity: 0, y: 40 }).to('.bg-layer', { yPercent: -20 }, '<');",Pinning needs the section to have deterministic height; recalc ScrollTrigger.refresh() after images/fonts load,Use scrub: true or a small number (0.5-1.5) instead of instant jumps so it feels tied to the scrollbar,Don't pin more than 1-2 sections per page; excessive pinning fights native scroll feel and hurts mobile UX,"Pinning forces layout reflow; test on mid-tier mobile devices, not just desktop" +7,Stagger List,Subtle,"list, stagger, cards, grid entrance",load or scroll,250-350ms,power1.out,"gsap.from('.list-item', { opacity: 0, y: 8, duration: 0.3, stagger: 0.03 });",Select items with a stable class/data-attribute (not array index) so re-renders in React don't break targeting,Keep per-item stagger delay small (0.02-0.04s) for lists longer than 10 items,Don't stagger by more than 0.1s per item on long lists; total reveal time becomes sluggish,"For virtualized lists, only animate items currently mounted in the DOM" +8,Stagger List,Standard,"grid, bento, cards, staggered scale",load or scroll,300-450ms,back.out(1.4),"gsap.from('.grid-item', { opacity: 0, scale: 0.92, y: 16, duration: 0.4, stagger: { each: 0.06, from: 'start', grid: 'auto' }, ease: 'back.out(1.4)' });",grid: 'auto' lets GSAP infer rows/columns from a CSS grid layout for a natural wave stagger,Combine with from: 'center' for a bento-grid layout to draw the eye inward first,Don't use back.out on dense data tables; the overshoot reads as sloppy on informational UI,Group DOM writes; avoid interleaving layout reads (getBoundingClientRect) between staggered tweens +9,Stagger List,Complex,"stagger, wave, text reveal, split text",load or scroll,400-700ms,expo.out,"const split = new SplitText(headline, { type: 'chars' }); gsap.from(split.chars, { opacity: 0, y: 20, rotateX: -40, duration: 0.6, stagger: 0.015, ease: 'expo.out' });",SplitText is a GSAP Club/paid plugin; confirm license before shipping and provide a plain fade fallback if unavailable,Revert SplitText on unmount/cleanup (split.revert()) to restore original text nodes for accessibility tools,Don't split-animate long paragraphs; reserve for short headlines (under ~8 words),Splitting text creates one element per character; keep it to headline-length copy only for DOM size +10,Page Transition,Subtle,"route change, fade, page transition",route change,200-300ms,power1.inOut,"gsap.to(main, { opacity: 0, duration: 0.2, onComplete: () => { navigate(); gsap.fromTo(main, { opacity: 0 }, { opacity: 1, duration: 0.2 }); } });","Pair with the router's transition hooks (Next.js App Router transitions, React Router's useNavigate, Vue Router's beforeEach/afterEach)",Preload the destination route's critical assets before the exit tween finishes,Don't block navigation on animation; cap exit duration at ~250ms so the app never feels unresponsive,Exit animation should always resolve faster than entrance (asymmetric timing) so back/forward feels snappy +11,Page Transition,Standard,"route change, slide, overlay wipe",route change,400-600ms,power2.inOut,"const tl = gsap.timeline(); tl.to('.transition-overlay', { yPercent: 0, duration: 0.4, ease: 'power2.inOut' }).call(navigate).to('.transition-overlay', { yPercent: -100, duration: 0.4, ease: 'power2.inOut', delay: 0.1 });",Keep the overlay element mounted at the layout root (outside the page component) so it survives the route swap,Show a lightweight loading indicator if the destination route's data fetch outlasts the overlay,Don't tie the overlay's reveal directly to data-fetch completion without a max-wait timeout; a slow API stalls the whole transition,Prefer CSS transform (yPercent) over top/left to keep the overlay animation on the compositor thread +12,Page Transition,Complex,"shared element, morph, hero transition",route change,500-800ms,expo.inOut,"const state = Flip.getState('.hero-image'); navigate(); Flip.from(state, { duration: 0.6, ease: 'expo.inOut', absolute: true, zIndex: 100 });",Requires the GSAP Flip plugin; the 'from' and 'to' route must render the same element with a shared data-flip-id,Verify the shared element exists in both DOM states before calling Flip.from to avoid a silent no-op,Don't use shared-element transitions across more than one element pair per navigation; compounding Flips are hard to time correctly,Flip recalculates layout (FLIP technique) so test on low-end devices for jank +13,Parallax Scroll,Subtle,"parallax, background, depth, scroll speed",scroll (continuous),tied to scroll position,linear (scrub),"gsap.to('.bg-layer', { yPercent: 10, ease: 'none', scrollTrigger: { trigger: section, scrub: true } });","Apply parallax to background/decorative layers only, never to text or interactive controls",Keep the yPercent delta small (5-15) so foreground and background never desync distractingly,Don't parallax body copy; it hurts reading comfort and can trigger motion sickness,will-change: transform on the parallax layer only; remove it after scroll settles to free GPU memory +14,Parallax Scroll,Standard,"multi-layer parallax, depth, hero background",scroll (continuous),tied to scroll position,linear (scrub),"gsap.utils.toArray('.parallax-layer').forEach((layer, i) => { gsap.to(layer, { yPercent: (i + 1) * -8, ease: 'none', scrollTrigger: { trigger: layer.parentElement, scrub: 0.5 } }); });",Layer count beyond 3-4 has diminishing visual return and multiplies scroll-listener cost,"Vary speed per layer (background slowest, foreground fastest) to sell the depth illusion",Don't let parallax layers overflow their container; clip with overflow: hidden on the wrapper,Batch all layers under one ScrollTrigger container where possible instead of one per layer +15,Loading / Skeleton,Subtle,"loading, skeleton, shimmer, pulse",on mount / async wait,1200-1600ms loop,sine.inOut,"gsap.to('.skeleton', { backgroundPosition: '200% 0', duration: 1.4, ease: 'sine.inOut', repeat: -1 });",Kill the loop tween (tween.kill()) as soon as real content mounts to avoid orphaned repeating animations,Use a CSS gradient background-position sweep rather than opacity pulsing; reads as 'loading' more clearly,Don't run more than one shimmer loop per skeleton group; sync them under one timeline so the wave reads as a single unit,repeat: -1 tweens are cheap but must be explicitly killed on unmount or they leak in SPA route changes +16,Loading / Skeleton,Standard,"progress, spinner, morphing loader",on mount / async wait,800-1200ms loop,power1.inOut,"gsap.timeline({ repeat: -1 }).to('.loader-dot', { y: -8, duration: 0.4, stagger: { each: 0.15, yoyo: true, repeat: 1 } });",Wrap the whole loop timeline in useGSAP with { revertOnUpdate: false } in React so it isn't rebuilt every render,Cap total loop duration under ~1.5s so long waits don't feel like the UI froze on a single beat,Don't use elaborate loaders for sub-300ms waits; they flash and feel worse than no indicator,Pause the timeline (tl.pause()) when the loading tab/view is not visible to save CPU on background tabs diff --git a/ui-ux-pro-max/cli/assets/data/products.csv b/ui-ux-pro-max/cli/assets/data/products.csv index 0bb9fcb3..10bba421 100644 --- a/ui-ux-pro-max/cli/assets/data/products.csv +++ b/ui-ux-pro-max/cli/assets/data/products.csv @@ -1,17 +1,17 @@ No,Product Type,Keywords,Primary Style Recommendation,Secondary Styles,Landing Page Pattern,Dashboard Style (if applicable),Color Palette Focus,Key Considerations 1,SaaS (General),"app, b2b, cloud, general, saas, software, subscription",Glassmorphism + Flat Design,"Soft UI Evolution, Minimalism",Hero + Features + CTA,Data-Dense + Real-Time Monitoring,Trust blue + accent contrast,Balance modern feel with clarity. Focus on CTAs. -2,Micro SaaS,"app, b2b, cloud, indie, micro, micro-saas, niche, saas, small, software, solo, subscription",Flat Design + Vibrant & Block,"Motion-Driven, Micro-interactions",Minimal & Direct + Demo,Executive Dashboard,Vibrant primary + white space,"Keep simple, show product quickly. Speed is key." +2,Micro SaaS,"indie, micro-saas, niche, solo, bootstrap, micro, side-project, solopreneur, small-team, indie-hacker, product-hunt",Flat Design + Vibrant & Block,"Motion-Driven, Micro-interactions",Minimal & Direct + Demo,Executive Dashboard,Vibrant primary + white space,"Keep simple, show product quickly. Speed is key." 3,E-commerce,"buy, commerce, e, ecommerce, products, retail, sell, shop, store",Vibrant & Block-based,"Aurora UI, Motion-Driven",Feature-Rich Showcase,Sales Intelligence Dashboard,Brand primary + success green,Engagement & conversions. High visual hierarchy. 4,E-commerce Luxury,"buy, commerce, e, ecommerce, elegant, exclusive, high-end, luxury, premium, products, retail, sell, shop, store",Liquid Glass + Glassmorphism,"3D & Hyperrealism, Aurora UI",Feature-Rich Showcase,Sales Intelligence Dashboard,Premium colors + minimal accent,Elegance & sophistication. Premium materials. -5,B2B Service,"appointment, b, b2b, booking, business, consultation, corporate, enterprise, service",Trust & Authority + Minimal,"Feature-Rich, Conversion-Optimized",Feature-Rich Showcase,Sales Intelligence Dashboard,Professional blue + neutral grey,Credibility essential. Clear ROI messaging. -6,Financial Dashboard,"admin, analytics, dashboard, data, financial, panel",Dark Mode (OLED) + Data-Dense,"Minimalism, Accessible & Ethical",N/A - Dashboard focused,Financial Dashboard,Dark bg + red/green alerts + trust blue,"High contrast, real-time updates, accuracy paramount." -7,Analytics Dashboard,"admin, analytics, dashboard, data, panel",Data-Dense + Heat Map & Heatmap,"Minimalism, Dark Mode (OLED)",N/A - Analytics focused,Drill-Down Analytics + Comparative,Cool→Hot gradients + neutral grey,Clarity > aesthetics. Color-coded data priority. +5,B2B Service,"b2b, enterprise, consulting, professional, solution, contract, corporate, strategy, advisory, roi, deliverable, whitepaper",Trust & Authority + Minimal,"Feature-Rich, Conversion-Optimized",Feature-Rich Showcase,Sales Intelligence Dashboard,Professional blue + neutral grey,Credibility essential. Clear ROI messaging. +6,Financial Dashboard,"portfolio, trading, pnl, budget, revenue, expense, cashflow, balance-sheet, investment, bank, accounting, fintech",Dark Mode (OLED) + Data-Dense,"Minimalism, Accessible & Ethical",N/A - Dashboard focused,Financial Dashboard,Dark bg + red/green alerts + trust blue,"High contrast, real-time updates, accuracy paramount." +7,Analytics Dashboard,"kpi, metric, funnel, conversion, cohort, retention, segment, attribution, ab-test, dashboard-data, business-intelligence",Data-Dense + Heat Map & Heatmap,"Minimalism, Dark Mode (OLED)",N/A - Analytics focused,Drill-Down Analytics + Comparative,Cool→Hot gradients + neutral grey,Clarity > aesthetics. Color-coded data priority. 8,Healthcare App,"app, clinic, health, healthcare, medical, patient",Neumorphism + Accessible & Ethical,"Soft UI Evolution, Claymorphism (for patients)",Social Proof-Focused,User Behavior Analytics,Calm blue + health green + trust,Accessibility mandatory. Calming aesthetic. 9,Educational App,"app, course, education, educational, learning, school, training",Claymorphism + Micro-interactions,"Vibrant & Block-based, Flat Design",Storytelling-Driven,User Behavior Analytics,Playful colors + clear hierarchy,Engagement & ease of use. Age-appropriate design. -10,Creative Agency,"agency, creative, design, marketing, studio",Brutalism + Motion-Driven,"Retro-Futurism, Storytelling-Driven",Storytelling-Driven,N/A - Portfolio focused,Bold primaries + artistic freedom,Differentiation key. Wow-factor necessary. +10,Creative Agency,"branding, identity, portfolio, logo, visual, rebrand, creative-director, campaign, awards, award-winning, showreel",Brutalism + Motion-Driven,"Retro-Futurism, Storytelling-Driven",Storytelling-Driven,N/A - Portfolio focused,Bold primaries + artistic freedom,Differentiation key. Wow-factor necessary. 11,Portfolio/Personal,"creative, personal, portfolio, projects, showcase, work",Motion-Driven + Minimalism,"Brutalism, Aurora UI",Storytelling-Driven,N/A - Personal branding,Brand primary + artistic interpretation,Showcase work. Personality shine through. 12,Gaming,"entertainment, esports, game, gaming, play",3D & Hyperrealism + Retro-Futurism,"Motion-Driven, Vibrant & Block",Feature-Rich Showcase,N/A - Game focused,Vibrant + neon + immersive colors,Immersion priority. Performance critical. -13,Government/Public Service,"appointment, booking, consultation, government, public, service",Accessible & Ethical + Minimalism,"Flat Design, Inclusive Design",Minimal & Direct,Executive Dashboard,Professional blue + high contrast,WCAG AAA mandatory. Trust paramount. +13,Government/Public Service,"government, civic, municipal, federal, citizen, public, administration, permit, tax, voter, transparency, regulation",Accessible & Ethical + Minimalism,"Flat Design, Inclusive Design",Minimal & Direct,Executive Dashboard,Professional blue + high contrast,WCAG AAA mandatory. Trust paramount. 14,Fintech/Crypto,"banking, blockchain, crypto, defi, finance, fintech, money, nft, payment, web3",Glassmorphism + Dark Mode (OLED),"Retro-Futurism, Motion-Driven",Conversion-Optimized,Real-Time Monitoring + Predictive,Dark tech colors + trust + vibrant accents,Security perception. Real-time data critical. 15,Social Media App,"app, community, content, entertainment, media, network, sharing, social, streaming, users, video",Vibrant & Block-based + Motion-Driven,"Aurora UI, Micro-interactions",Feature-Rich Showcase,User Behavior Analytics,Vibrant + engagement colors,Engagement & retention. Addictive design ethics. 16,Productivity Tool,"collaboration, productivity, project, task, tool, workflow",Flat Design + Micro-interactions,"Minimalism, Soft UI Evolution",Interactive Product Demo,Drill-Down Analytics,Clear hierarchy + functional colors,Ease of use. Speed & efficiency focus. @@ -24,21 +24,21 @@ No,Product Type,Keywords,Primary Style Recommendation,Secondary Styles,Landing P 23,Pet Tech App,"app, pet, tech",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Storytelling-Driven,User Behavior Analytics,Playful + Warm colors,Pet profiles. Health tracking. Playful UI. Photo galleries. Vet integration. 24,Smart Home/IoT Dashboard,"admin, analytics, dashboard, data, home, iot, panel, smart",Glassmorphism + Dark Mode (OLED),"Minimalism, AI-Native UI",Interactive Product Demo,Real-Time Monitoring,Dark + Status indicator colors,Device status. Real-time controls. Energy monitoring. Automation rules. Quick actions. 25,EV/Charging Ecosystem,"charging, ecosystem, ev",Minimalism + Aurora UI,"Glassmorphism, Organic Biophilic",Hero-Centric Design,Energy/Utilities Dashboard,Electric Blue (#009CD1) + Green,Charging station maps. Range estimation. Cost calculation. Environmental impact. -26,Subscription Box Service,"appointment, booking, box, consultation, membership, plan, recurring, service, subscription",Vibrant & Block-based + Motion-Driven,"Claymorphism, Aurora UI",Feature-Rich Showcase,E-commerce Analytics,Brand + Excitement colors,Unboxing experience. Personalization quiz. Subscription management. Product reveals. +26,Subscription Box Service,"subscription, box, recurring, membership, unboxing, curated, plan, monthly, surprise, product-box",Vibrant & Block-based + Motion-Driven,"Claymorphism, Aurora UI",Feature-Rich Showcase,E-commerce Analytics,Brand + Excitement colors,Unboxing experience. Personalization quiz. Subscription management. Product reveals. 27,Podcast Platform,"platform, podcast",Dark Mode (OLED) + Minimalism,"Motion-Driven, Vibrant & Block-based",Storytelling-Driven,Media/Entertainment Dashboard,Dark + Audio waveform accents,Audio player UX. Episode discovery. Creator tools. Analytics for podcasters. 28,Dating App,"app, dating",Vibrant & Block-based + Motion-Driven,"Aurora UI, Glassmorphism",Social Proof-Focused,User Behavior Analytics,Warm + Romantic (Pink/Red gradients),Profile cards. Swipe interactions. Match animations. Safety features. Video chat. 29,Micro-Credentials/Badges Platform,"badges, credentials, micro, platform",Minimalism + Flat Design,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority,Education Dashboard,Trust Blue + Gold (#FFD700),Credential verification. Badge display. Progress tracking. Issuer trust. LinkedIn integration. 30,Knowledge Base/Documentation,"base, documentation, knowledge",Minimalism + Accessible & Ethical,"Swiss Modernism 2.0, Flat Design",FAQ/Documentation,N/A - Documentation focused,Clean hierarchy + minimal color,Search-first. Clear navigation. Code highlighting. Version switching. Feedback system. -31,Hyperlocal Services,"appointment, booking, consultation, hyperlocal, service, services",Minimalism + Vibrant & Block-based,"Micro-interactions, Flat Design",Conversion-Optimized,Drill-Down Analytics + Map,Location markers + Trust colors,Map integration. Service categories. Provider profiles. Booking system. Reviews. -32,Beauty/Spa/Wellness Service,"appointment, beauty, booking, consultation, service, spa, wellness",Soft UI Evolution + Neumorphism,"Glassmorphism, Minimalism",Hero-Centric Design + Social Proof,User Behavior Analytics,Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents,Calming aesthetic. Booking system. Service menu. Before/after gallery. Testimonials. Relaxing imagery. +31,Hyperlocal Services,"hyperlocal, local, neighborhood, nearby, community, nearby, zip, map, local-business, geo-target, city",Minimalism + Vibrant & Block-based,"Micro-interactions, Flat Design",Conversion-Optimized,Drill-Down Analytics + Map,Location markers + Trust colors,Map integration. Service categories. Provider profiles. Booking system. Reviews. +32,Beauty/Spa/Wellness Service,"spa, beauty, salon, wellness, treatment, relaxation, massage, skincare, facial, aesthetic, self-care, pamper",Soft UI Evolution + Neumorphism,"Glassmorphism, Minimalism",Hero-Centric Design + Social Proof,User Behavior Analytics,Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents,Calming aesthetic. Booking system. Service menu. Before/after gallery. Testimonials. Relaxing imagery. 33,Luxury/Premium Brand,"brand, elegant, exclusive, high-end, luxury, premium",Liquid Glass + Glassmorphism,"Minimalism, 3D & Hyperrealism",Storytelling-Driven + Feature-Rich,Sales Intelligence Dashboard,Black + Gold (#FFD700) + White + Minimal accent,Elegance paramount. Premium imagery. Storytelling. High-quality visuals. Exclusive feel. -34,Restaurant/Food Service,"appointment, booking, consultation, delivery, food, menu, order, restaurant, service",Vibrant & Block-based + Motion-Driven,"Claymorphism, Flat Design",Hero-Centric Design + Conversion,N/A - Booking focused,Warm colors (Orange Red Brown) + appetizing imagery,Menu display. Online ordering. Reservation system. Food photography. Location/hours prominent. +34,Restaurant/Food Service,"restaurant, menu, order, food, dining, reservation, delivery, cuisine, chef, table, takeaway, eatery",Vibrant & Block-based + Motion-Driven,"Claymorphism, Flat Design",Hero-Centric Design + Conversion,N/A - Booking focused,Warm colors (Orange Red Brown) + appetizing imagery,Menu display. Online ordering. Reservation system. Food photography. Location/hours prominent. 35,Fitness/Gym App,"app, exercise, fitness, gym, health, workout",Vibrant & Block-based + Dark Mode (OLED),"Motion-Driven, Neumorphism",Feature-Rich Showcase,User Behavior Analytics,Energetic (Orange #FF6B35 Electric Blue) + Dark bg,Progress tracking. Workout plans. Community features. Achievements. Motivational design. 36,Real Estate/Property,"buy, estate, housing, property, real, real-estate, rent",Glassmorphism + Minimalism,"Motion-Driven, 3D & Hyperrealism",Hero-Centric Design + Feature-Rich,Sales Intelligence Dashboard,Trust Blue (#0077B6) + Gold accents + White,Property listings. Virtual tours. Map integration. Agent profiles. Mortgage calculator. High-quality imagery. -37,Travel/Tourism Agency,"agency, booking, creative, design, flight, hotel, marketing, studio, tourism, travel, vacation",Aurora UI + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Storytelling-Driven + Hero-Centric,Booking Analytics,Vibrant destination colors + Sky Blue + Warm accents,Destination showcase. Booking system. Itinerary builder. Reviews. Inspiration galleries. Mobile-first. +37,Travel/Tourism Agency,"travel, tourism, vacation, flight, hotel, destination, adventure, cruise, safari, backpacking, guided-tour, holiday-package",Aurora UI + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Storytelling-Driven + Hero-Centric,Booking Analytics,Vibrant destination colors + Sky Blue + Warm accents,Destination showcase. Booking system. Itinerary builder. Reviews. Inspiration galleries. Mobile-first. 38,Hotel/Hospitality,"hospitality, hotel",Liquid Glass + Minimalism,"Glassmorphism, Soft UI Evolution",Hero-Centric Design + Social Proof,Revenue Management Dashboard,Warm neutrals + Gold (#D4AF37) + Brand accent,Room booking. Amenities showcase. Location maps. Guest reviews. Seasonal pricing. Luxury imagery. 39,Wedding/Event Planning,"conference, event, meetup, planning, registration, ticket, wedding",Soft UI Evolution + Aurora UI,"Glassmorphism, Motion-Driven",Storytelling-Driven + Social Proof,N/A - Planning focused,Soft Pink (#FFD6E0) + Gold + Cream + Sage,Portfolio gallery. Vendor directory. Planning tools. Timeline. Budget tracker. Romantic aesthetic. -40,Legal Services,"appointment, attorney, booking, compliance, consultation, contract, law, legal, service, services",Trust & Authority + Minimalism,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority + Minimal,Case Management Dashboard,Navy Blue (#1E3A5F) + Gold + White,Credibility paramount. Practice areas. Attorney profiles. Case results. Contact forms. Professional imagery. +40,Legal Services,"law, attorney, legal, case, compliance, contract, court, firm, counsel, litigation, practice-area, jurisdiction",Trust & Authority + Minimalism,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority + Minimal,Case Management Dashboard,Navy Blue (#1E3A5F) + Gold + White,Credibility paramount. Practice areas. Attorney profiles. Case results. Contact forms. Professional imagery. 41,Insurance Platform,"insurance, platform",Trust & Authority + Flat Design,"Accessible & Ethical, Minimalism",Conversion-Optimized + Trust,Claims Analytics Dashboard,Trust Blue (#0066CC) + Green (security) + Neutral,Quote calculator. Policy comparison. Claims process. Trust signals. Clear pricing. Security badges. 42,Banking/Traditional Finance,"banking, finance, traditional",Minimalism + Accessible & Ethical,"Trust & Authority, Dark Mode (OLED)",Trust & Authority + Feature-Rich,Financial Dashboard,Navy (#0A1628) + Trust Blue + Gold accents,Security-first. Account overview. Transaction history. Mobile banking. Accessibility critical. Trust paramount. 43,Online Course/E-learning,"course, e, learning, online",Claymorphism + Vibrant & Block-based,"Motion-Driven, Flat Design",Feature-Rich Showcase + Social Proof,Education Dashboard,Vibrant learning colors + Progress green,Course catalog. Progress tracking. Video player. Quizzes. Certificates. Community forums. Gamification. @@ -53,7 +53,7 @@ No,Product Type,Keywords,Primary Style Recommendation,Secondary Styles,Landing P 52,Automotive/Car Dealership,"automotive, car, dealership",Motion-Driven + 3D & Hyperrealism,"Dark Mode (OLED), Glassmorphism",Hero-Centric Design + Feature-Rich,Sales Intelligence Dashboard,Brand colors + Metallic accents + Dark/Light,Vehicle showcase. 360° views. Comparison tools. Financing calculator. Test drive booking. High-quality imagery. 53,Photography Studio,"photography, studio",Motion-Driven + Minimalism,"Aurora UI, Glassmorphism",Storytelling-Driven + Hero-Centric,N/A - Portfolio focused,Black + White + Minimal accent,Portfolio gallery. Before/after. Service packages. Booking system. Client galleries. Full-bleed imagery. 54,Coworking Space,"coworking, space",Vibrant & Block-based + Glassmorphism,"Minimalism, Motion-Driven",Hero-Centric Design + Feature-Rich,Occupancy Dashboard,Energetic colors + Wood tones + Brand accent,Space tour. Membership plans. Booking system. Amenities. Community events. Virtual tour. -55,Home Services (Plumber/Electrician),"appointment, booking, consultation, electrician, home, plumber, service, services",Flat Design + Trust & Authority,"Minimalism, Accessible & Ethical",Conversion-Optimized + Trust,Service Analytics,Trust Blue + Safety Orange + Professional grey,Service list. Emergency contact. Booking. Price transparency. Certifications. Local trust signals. +55,Home Services (Plumber/Electrician),"plumber, electrician, hvac, handyman, repair, maintenance, home, emergency, leak, wiring, inspection, licensed",Flat Design + Trust & Authority,"Minimalism, Accessible & Ethical",Conversion-Optimized + Trust,Service Analytics,Trust Blue + Safety Orange + Professional grey,Service list. Emergency contact. Booking. Price transparency. Certifications. Local trust signals. 56,Childcare/Daycare,"childcare, daycare",Claymorphism + Vibrant & Block-based,"Soft UI Evolution, Accessible & Ethical",Social Proof-Focused + Trust,Parent Dashboard,Playful pastels + Safe colors + Warm accents,Programs. Staff profiles. Safety certifications. Parent portal. Activity updates. Cheerful imagery. 57,Senior Care/Elderly,"care, elderly, senior",Accessible & Ethical + Soft UI Evolution,"Minimalism, Neumorphism",Trust & Authority + Social Proof,Healthcare Analytics,Calm Blue + Warm neutrals + Large text,Care services. Staff qualifications. Facility tour. Family portal. Large touch targets. High contrast. Accessibility-first. 58,Medical Clinic,"clinic, medical",Accessible & Ethical + Minimalism,"Neumorphism, Trust & Authority",Trust & Authority + Conversion,Healthcare Analytics,Medical Blue (#0077B6) + Trust White + Calm Green,Services. Doctor profiles. Online booking. Patient portal. Insurance info. HIPAA compliant. Trust signals. @@ -67,7 +67,7 @@ No,Product Type,Keywords,Primary Style Recommendation,Secondary Styles,Landing P 66,News/Media Platform,"content, entertainment, media, news, platform, streaming, video",Minimalism + Flat Design,"Dark Mode (OLED), Accessible & Ethical",Hero-Centric Design + Feature-Rich,Media Analytics Dashboard,Brand colors + High contrast + Category colors,Article layout. Breaking news. Categories. Search. Subscription. Mobile reading. Fast loading. 67,Magazine/Blog,"articles, blog, content, magazine, posts, writing",Swiss Modernism 2.0 + Motion-Driven,"Minimalism, Aurora UI",Storytelling-Driven + Hero-Centric,Content Analytics,Editorial colors + Brand primary + Clean white,Article showcase. Category navigation. Author profiles. Newsletter signup. Related content. Typography-focused. 68,Freelancer Platform,"freelancer, platform",Flat Design + Minimalism,"Vibrant & Block-based, Micro-interactions",Feature-Rich Showcase + Conversion,Marketplace Analytics,Professional Blue + Success Green + Neutral,Profile creation. Portfolio. Skill matching. Messaging. Payment. Reviews. Project management. -69,Marketing Agency,"agency, creative, design, marketing, studio",Brutalism + Motion-Driven,"Vibrant & Block-based, Aurora UI",Storytelling-Driven + Feature-Rich,Campaign Analytics,Bold brand colors + Creative freedom,Portfolio. Case studies. Services. Team. Creative showcase. Results-focused. Bold aesthetic. +69,Marketing Agency,"campaign, ads, growth, roi, seo, sem, ppc, social-media, conversion-funnel, ab-test, attribution, performance-marketing",Brutalism + Motion-Driven,"Vibrant & Block-based, Aurora UI",Storytelling-Driven + Feature-Rich,Campaign Analytics,Bold brand colors + Creative freedom,Portfolio. Case studies. Services. Team. Creative showcase. Results-focused. Bold aesthetic. 70,Event Management,"conference, event, management, meetup, registration, ticket",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Aurora UI",Hero-Centric Design + Feature-Rich,Event Analytics,Event theme colors + Excitement accents,Event showcase. Registration. Agenda. Speakers. Sponsors. Ticket sales. Countdown timer. 71,Membership/Community,"community, membership",Vibrant & Block-based + Soft UI Evolution,"Bento Box Grid, Micro-interactions",Social Proof-Focused + Conversion,Community Analytics,Community brand colors + Engagement accents,Member benefits. Pricing tiers. Community showcase. Events. Member directory. Exclusive content. 72,Newsletter Platform,"newsletter, platform",Minimalism + Flat Design,"Swiss Modernism 2.0, Accessible & Ethical",Minimal & Direct + Conversion,Email Analytics,Brand primary + Clean white + CTA accent,Subscribe form. Archive. About. Social proof. Sample content. Simple conversion. @@ -160,3 +160,34 @@ No,Product Type,Keywords,Primary Style Recommendation,Secondary Styles,Landing P 159,Wallpaper & Theme App,"wallpaper, theme, background, customize, aesthetic, home-screen, lock-screen, widget, design, zedge",Vibrant & Block-based + Aurora UI,"Glassmorphism, Motion-Driven",Feature-Rich Showcase + Social Proof,N/A - Gallery focused,Content-driven + trending aesthetic palettes + download accent,Category browsing. Preview on device. Daily wallpaper auto-set. Widget matching. Creator uploads. Resolution auto-fit. 160,White Noise & Ambient Sound,"white noise, ambient, sound, sleep, focus, rain, nature, relax, concentration, background, noisli",Minimalism + Dark Mode (OLED),"Neumorphism, Organic Biophilic",Minimal & Direct + Social Proof,N/A - Player focused,Calming dark + ambient texture visual + subtle sound wave + sleep blue,Sound mixer with multiple simultaneous layers. Sleep timer with fade. Custom soundscapes. Offline. Background audio. 161,Home Decoration & Interior Design,"home, interior, decor, design, furniture, room, renovation, ar, plan, inspire, 3d, houzz",Minimalism + 3D Product Preview,"Organic Biophilic, Aurora UI",Storytelling-Driven + Feature-Rich,N/A - Project focused,Neutral interior palette + material texture accent + AR blue,AR room visualization. Style quiz. Product catalog with purchase links. 3D room planner. Mood board. Before/after. +162,Academic Journal / Scholarly Publishing,"academic, journal, paper, research, peer-review, open-access, scholarly, publication, citation, manuscript, issn, doi",Swiss Modernism 2.0 + Minimalism,"Trust & Authority, Accessible & Ethical",Content-Index + Search,N/A - Publication focused,Trust navy + White + Citation blue + Serif accents,"Prioritize readability (serif body text). Clear article hierarchy. Abstract/DOI prominence. WCAG AAA. Minimal visual noise. Trust signals: ISSN, indexing badges." +163,API Developer Portal,"api, developer, documentation, sdk, endpoint, integration, rest, graphql, webhook, reference, getting-started, auth",Trust & Authority + Minimalism,"Glassmorphism, Dark Mode (OLED)",Quick Start + Interactive Docs,N/A - Documentation focused,Dark code theme + Brand accent + Syntax colors,"Endpoint discoverability. Copy-paste code samples. Auth flow clarity. Version switching. Interactive playground. Rate limit visibility." +164,Forum / Discussion Board,"forum, discussion, thread, post, reply, community, comment, moderation, subreddit, stackexchange, topic",Dark Mode (OLED) + Minimalism,"Flat Design, Vibrant & Block-based",Feed + Thread View,N/A - Discussion focused,Dark neutral + topic accent colors + unread indicator + reputation badge,Thread list with pagination. Rich text editor. Quote/mention system. Upvote/downvote. User badges. Moderation tools. +165,Directory / Listing Site,"directory, listing, classifieds, catalogue, business-directory, yellow-pages, venue, find, search, filter, map",Flat Design + Vibrant & Block-based,"Minimalism, Trust & Authority",Filter-Heavy Grid + Map,N/A - Listing focused,Neutral bg + category color chips + map accent + verified badge,Category tree. Multi-filter sidebar. Map/list toggle. Verified badges. Reviews. Claim listing flow. +166,Status Page / Incident Management,"status, incident, outage, uptime, downtime, statuspage, monitoring, sla, maintenance, sev1, postmortem",Data-Dense + Trust & Authority,"Minimalism, Dark Mode (OLED)",Timeline + Severity Indicators,Real-Time Monitoring + Timeline,Status green + incident red + maintenance amber + neutral dark,Service status matrix. Incident timeline. Severity badges. Maintenance schedule. SLA uptime history. Email/SMS subscribe. +167,Wiki / Encyclopedia,"wiki, encyclopedia, knowledge, article, reference, wikipedia, documentation, collaborative, edit, version, citation",Minimalism + Flat Design,"Swiss Modernism 2.0, Accessible & Ethical",Search-First + Hierarchical Navigation,N/A - Reference focused,Clean white + link blue + heading hierarchy + citation grey,Full-text search bar. Table of contents sidebar. Edit history. Inter-page linking. Mobile responsive. Print-friendly. +168,Auction Platform,"auction, bid, hammer, lot, live-auction, bidding-war, estate-sale, proxibid, gavel, lot-number, reserve-price",Dark Mode (OLED) + Motion-Driven,"Vibrant & Block-based, Real-Time Monitor",Live Auction Feed + Countdown,N/A - Auction focused,Dark bg + bid green + outbid red + countdown amber,Real-time bid updates. Countdown timer urgency. Auto-bid ceiling. Outbid notifications. Bid history. Reserve price indicator. +169,Changelog / Release Notes,"changelog, release-notes, version-history, whats-new, product-updates, semver, patch-notes, release-tracker",Minimalism + Flat Design,"Swiss Modernism 2.0, Trust & Authority",Timeline + Version List,N/A - Documentation focused,Neutral bg + version badge colors (feat=green, fix=blue, breaking=red) + date grey,Chronological release feed. Semver badges. Breaking change warnings. Copy-paste install commands. Subscribe to feed. Search by version. +170,Citizen Science Platform,"citizen-science, zooniverse, crowdsourced-research, volunteer-science, public-participation, distributed-research, citizen-researcher",Organic Biophilic + Vibrant & Block-based,"Claymorphism, Motion-Driven",Storytelling-Driven + Social Proof,Project Participation Dashboard,Earth green + discovery orange + volunteer badge blue + data neutral,Project cards with impact metrics. Contribution tracker. Beginner-friendly onboarding. Data quality feedback loop. Leaderboards. Community forums. +171,Classifieds / Buy-Sell,"classifieds, buy-sell, craigslist, secondhand, marketplace-listing, for-sale, trade, flea-market, thrift, resell",Flat Design + Vibrant & Block-based,"Minimalism, Trust & Authority",Filter-Heavy Grid + Map,N/A - Listing focused,Neutral bg + price green + category chips + verified seller badge,Category tree. Photo-first listing cards. Price negotiation. Location radius filter. Saved searches. Seller reputation. Flag/report. +172,Conference / Symposium Landing Page,"conference, symposium, summit, cfp, call-for-papers, speaker-lineup, registration, venue, proceedings, keynote, track",Swiss Modernism 2.0 + Minimalism,"Trust & Authority, Accessible & Ethical",Hero + Agenda + CFP,N/A - Event focused,Academic navy + track color chips + gold keynote + neutral white,Speaker grid. Multi-track agenda. CFP deadline countdown. Venue map. Sponsor tiers. Early-bird pricing. Proceedings download. +173,Crowdfunding Platform,"crowdfunding, kickstarter, indiegogo, campaign, backer, pledge, funding-goal, stretch-goal, reward-tier, all-or-nothing",Vibrant & Block-based + Motion-Driven,"Claymorphism, Storytelling-Driven",Storytelling-Driven + Social Proof,Campaign Analytics Dashboard,Brand primary + funding progress green + urgency amber + reward tier colors,Funding progress bar with % goal. Reward tier selector. Backer count. Countdown timer. Updates feed. Creator profile. Risk/disclaimer section. +174,Digital Signage / Kiosk,"digital-signage, kiosk, interactive-display, touchscreen, wayfinding, lobby-display, menu-board, point-of-sale-display",Minimalism + Dark Mode (OLED),"Flat Design, Motion-Driven",Full-Screen Immersive,N/A - Display focused,High contrast + brand accent + touch target emphasis (56px min),Full-screen single-purpose layout. Touch targets ≥56px. Auto-rotate content. Offline fallback. Brightness-aware color palette. No scroll. +175,E-signature / Document Workflow,"esignature, e-sign, docusign, digital-signature, document-workflow, approval-chain, contract-signing, signing-ceremony",Trust & Authority + Minimalism,"Accessible & Ethical, Flat Design",Feature-Rich Showcase + Conversion,Document Pipeline Dashboard,Trust navy + signature green + pending amber + neutral grey,Document preview with annotation. Signature placement UI. Multi-signer workflow. Audit trail. Compliance badges. Mobile signing. Expiry reminders. +176,Feature Flag / Config Management,"feature-flag, config, launchdarkly, feature-toggle, experiment, rollout, kill-switch, a-b-test-config, percentage-rollout",Dark Mode (OLED) + Data-Dense,"Minimalism, Trust & Authority",Feature List + Toggle Panel,N/A - Config focused,Dark bg + enabled green + disabled grey + experimental amber + kill-switch red,Feature list with on/off toggles. Percentage rollout slider. Environment selector (prod/staging). User targeting rules. Kill switch. Audit log. +177,Government Portal / Civic Services,"government-portal, civic-services, city-hall, permit-application, tax-payment, voter-registration, public-records, municipal-online",Accessible & Ethical + Trust & Authority,"Flat Design, Inclusive Design",Service Directory + Search,N/A - Service focused,Professional blue + accessibility high contrast + service category colors,Multilingual toggle. Service A-Z index. Form wizard with save-progress. Document upload. Appointment booking. Status tracker. WCAG AAA. Plain language. +178,Grant / Funding Portal,"grant, funding, rfp, proposal, research-grant, foundation, fellowship, award, application-portal, funding-opportunity",Trust & Authority + Minimalism,"Accessible & Ethical, Swiss Modernism 2.0",Opportunity Grid + Search,Application Tracking Dashboard,Institution navy + funding green + deadline red + neutral white,Funding opportunity cards. Eligibility checker. Deadline countdown. Application form wizard. Document checklist. Review status tracker. Award announcement feed. +179,LMS (Learning Management System),"lms, course-management, learning-management, canvas, moodle, blackboard, enrollment, gradebook, syllabus, assignment-submit",Flat Design + Accessible & Ethical,"Minimalism, Vibrant & Block-based",Dashboard + Course Grid,Education Analytics Dashboard,Calm blue + course category colors + grade green + alert red,Dashboard with enrolled courses. Assignment deadlines. Gradebook view. Discussion forums. File upload. Calendar integration. Mobile offline sync. +180,No-code / Low-code Builder,"no-code, low-code, builder, bubble, webflow, drag-drop, visual-builder, app-builder, workflow-builder, logic-blocks",Vibrant & Block-based + Bento Box Grid,"Motion-Driven, Glassmorphism",Interactive Product Demo,App Builder Workspace,Brand primary + component palette colors + canvas neutral + connect blue,Drag-drop canvas. Component library sidebar. Logic flow visual editor. Preview pane. Template gallery. Publish button. Version history. +181,Open Source Project Landing,"open-source, github-project, oss, contributor, star, fork, pull-request, maintainer, sponsoring, readme, repository",Dark Mode (OLED) + Minimalism,"Trust & Authority, Flat Design",Hero + Install + Contribute,Contributor Analytics Dashboard,Dark bg + language color bar + star gold + fork silver + sponsor purple,Star/fork count badges. Install command (copy-paste). Language breakdown bar. Top contributors grid. Sponsor CTA. Documentation link. Issue/pr status. +182,Patient Portal / Health Records,"patient-portal, health-records, ehr, emr, mychart, lab-results, prescription-refill, medical-history, test-results, care-team",Trust & Authority + Accessible & Ethical,"Minimalism, Flat Design",Health Summary Dashboard,Healthcare Analytics,Clinical blue + health green + alert red + calm white + accessible contrast,Labs and results timeline. Medication list with refill. Appointment scheduling. Message care team. Immunization records. Allergy alerts. Family access proxy. +183,Patent / IP Database,"patent, intellectual-property, trademark, prior-art, uspto, wipo, invention, ip-portfolio, patent-search, claims",Swiss Modernism 2.0 + Minimalism,"Trust & Authority, Data-Dense",Search-First + Results Grid,N/A - Search focused,Formal neutral + patent type chips + status badges (granted/pending/rejected),Full-text patent search. Classification tree. Citation graph. Prior art comparison. Patent family view. PDF download. Legal status tracker. +184,Q&A Community Platform,"qa, stack-overflow, question-answer, knowledge-sharing, community-qa, expert-answer, upvote, accepted-answer, reputation",Minimalism + Flat Design,"Dark Mode (OLED), Accessible & Ethical",Feed + Thread View,Community Analytics Dashboard,Clean white + upvote orange + accepted green + reputation gold + tag colors,Question list with vote count. Rich code blocks. Tag filter. Reputation system. Accepted answer highlight. Comment threads. Bookmark/save. +185,Research Lab / University Department,"research-lab, university-department, academic-lab, principal-investigator, lab-members, publications, research-group, pi-page",Swiss Modernism 2.0 + Minimalism,"Trust & Authority, Accessible & Ethical",Overview + People + Publications,N/A - Academic focused,Institutional navy + white + research area accent colors + serif headings,PI bio and research focus. Current members grid. Publication list with links. Open positions. Lab facilities photos. Funding acknowledgments. +186,Resume / CV Builder,"resume, cv, builder, job-search, curriculum-vitae, portfolio-resume, cover-letter, career-builder, ats-friendly",Minimalism + Flat Design,"Swiss Modernism 2.0, Trust & Authority",Interactive Product Demo + CTA,Template Selection Gallery,Professional navy + section accent + success green + clean white,Template picker. Section-by-section editor. Real-time preview. ATS score indicator. PDF export. Cover letter generator. Import from LinkedIn. +187,Review Platform,"review, rating, yelp, trustpilot, testimonial, customer-review, star-rating, verified-purchase, pros-cons",Flat Design + Vibrant & Block-based,"Trust & Authority, Minimalism",Hero + Rating Summary + Review Feed,Review Analytics Dashboard,Brand primary + star gold + positive green + negative red + verified blue,Star rating summary with distribution. Verified purchase badge. Photo/video reviews. Helpful/upvote. Filter by rating. Response from business. Sort by recency. +188,RPA / Automation Dashboard,"rpa, robotic-process-automation, uipath, automation-anywhere, bot-orchestrator, process-discovery, attended-bot, unattended-bot",Dark Mode (OLED) + Data-Dense,"Minimalism, Trust & Authority",Bot Fleet Dashboard,Real-Time Monitoring + Process Analytics,Dark bg + running green + failed red + queued amber + completed blue,Bot status grid (running/idle/failed). Queue depth. Process flow visualization. Exception handling alert. ROI metrics. Bot scheduling calendar. Audit trail. +189,Survey / Form Builder,"survey, form-builder, questionnaire, typeform, survey-monkey, poll, feedback-form, multi-step-form, nps-survey, logic-jump",Minimalism + Micro-interactions,"Claymorphism, Flat Design",Interactive Product Demo,Response Analytics Dashboard,Clean white + question accent + progress green + submit blue,Drag-drop form builder. Question type library. Conditional logic visualizer. Theme picker. Response dashboard with charts. Export CSV. Share link/QR/embed. +190,Telemedicine Platform,"telemedicine, telehealth, virtual-visit, remote-consultation, video-doctor, remote-patient-monitoring, telehealth-app",Neumorphism + Accessible & Ethical,"Trust & Authority, Soft UI Evolution",Trust & Authority + Conversion,Healthcare Analytics,Calm medical blue + video green + waiting amber + trust white,Video call UI with screen share. Appointment queue. Symptom intake form. Prescription e-delivery. Waiting room with ETA. Post-visit summary. Insurance verification. +191,Testimonial & Social Proof Widget,"testimonial, social-proof, wall-of-love, customer-quote, case-study, review-widget, trust-signal, user-story",Vibrant & Block-based + Flat Design,"Motion-Driven, Minimalism",Wall-of-Love Grid,Engagement Analytics Dashboard,Brand primary + quote accent + star gold + verified blue,Testimonial cards with photo. Star ratings. Video testimonials. Case study summaries. Filter by industry/product. Embeddable widget code. Auto-rotate carousel. +192,Ticketing / Box Office,"ticketing, box-office, eventbrite, ticket-sales, seat-selection, will-call, qr-ticket, venue-capacity, will-call-pickup",Vibrant & Block-based + Motion-Driven,"Dark Mode (OLED), Glassmorphism",Event Grid + Seat Map,Sales Analytics Dashboard,Event theme colors + available green + sold-out red + seat map neutral,Event cards with date/venue. Interactive seat map. Cart with countdown. QR code ticket. Will-call pickup. Group discounts. Refund policy. diff --git a/ui-ux-pro-max/cli/assets/scripts/core.py b/ui-ux-pro-max/cli/assets/scripts/core.py index aa58b7b8..9d05db29 100644 --- a/ui-ux-pro-max/cli/assets/scripts/core.py +++ b/ui-ux-pro-max/cli/assets/scripts/core.py @@ -55,6 +55,11 @@ CSV_CONFIG = { "search_cols": ["Category", "Icon Name", "Keywords", "Best For"], "output_cols": ["Category", "Icon Name", "Keywords", "Library", "Import Code", "Usage", "Best For", "Style"] }, + "gsap": { + "file": "motion.csv", + "search_cols": ["Category", "Intensity Tier", "Keywords", "Trigger"], + "output_cols": ["Category", "Intensity Tier", "Trigger", "Duration", "Easing", "GSAP Snippet", "Framework Notes", "Do", "Don't", "Performance Notes"] + }, "react": { "file": "react-performance.csv", "search_cols": ["Category", "Issue", "Keywords", "Description"], @@ -90,6 +95,11 @@ STACK_CONFIG = { "angular": {"file": "stacks/angular.csv"}, "laravel": {"file": "stacks/laravel.csv"}, "javafx": {"file": "stacks/javafx.csv"}, + "wpf": {"file": "stacks/wpf.csv"}, + "winui": {"file": "stacks/winui.csv"}, + "avalonia": {"file": "stacks/avalonia.csv"}, + "uno": {"file": "stacks/uno.csv"}, + "uwp": {"file": "stacks/uwp.csv"}, } # Common columns for all stacks @@ -210,6 +220,7 @@ def detect_domain(query): "typography": ["font pairing", "typography pairing", "heading font", "body font"], "google-fonts": ["google font", "font family", "font weight", "font style", "variable font", "noto", "font for", "find font", "font subset", "font language", "monospace font", "serif font", "sans serif font", "display font", "handwriting font", "font", "typography", "serif", "sans"], "icons": ["icon", "icons", "lucide", "heroicons", "symbol", "glyph", "pictogram", "svg icon"], + "gsap": ["gsap", "quickto", "scrolltrigger", "stagger", "magnetic cursor", "parallax", "page transition", "scroll reveal", "scroll-triggered", "scrollytelling", "flip plugin", "splittext", "shimmer", "skeleton loader"], "react": ["react", "next.js", "nextjs", "suspense", "memo", "usecallback", "useeffect", "rerender", "bundle", "waterfall", "barrel", "dynamic import", "rsc", "server component"], "web": ["aria", "focus", "outline", "semantic", "virtualize", "autocomplete", "form", "input type", "preconnect"] } diff --git a/ui-ux-pro-max/cli/assets/scripts/design_system.py b/ui-ux-pro-max/cli/assets/scripts/design_system.py index f1f714e1..62dfbc8a 100644 --- a/ui-ux-pro-max/cli/assets/scripts/design_system.py +++ b/ui-ux-pro-max/cli/assets/scripts/design_system.py @@ -40,6 +40,39 @@ SEARCH_CONFIG = { "typography": {"max_results": 2} } +# ============ DESIGN DIALS (1-10) ============ +# Inspired by taste-skill's DESIGN_VARIANCE / MOTION_INTENSITY / VISUAL_DENSITY +# knobs: three optional 1-10 sliders that bias the existing query-based search +# instead of replacing it. Each dial buckets into a low/mid/high tier. +DIAL_TIERS = { + "variance": [ + (1, 3, {"label": "Centered / Minimal", "style_keywords": ["Minimalism", "Exaggerated Minimalism", "centered", "symmetric", "grid-based"]}), + (4, 7, {"label": "Balanced / Modern", "style_keywords": ["modern", "structured", "balanced"]}), + (8, 10, {"label": "Bold / Asymmetric", "style_keywords": ["Brutalism", "Bento Grids", "asymmetric", "experimental"]}), + ], + "motion": [ + (1, 3, {"label": "Subtle", "tier": "Subtle"}), + (4, 7, {"label": "Standard", "tier": "Standard"}), + (8, 10, {"label": "Complex", "tier": "Complex"}), + ], + "density": [ + (1, 3, {"label": "Spacious", "spacing": {"xs": "4px", "sm": "8px", "md": "24px", "lg": "32px", "xl": "48px", "2xl": "64px", "3xl": "96px"}}), + (4, 7, {"label": "Standard", "spacing": {"xs": "4px", "sm": "8px", "md": "16px", "lg": "24px", "xl": "32px", "2xl": "48px", "3xl": "64px"}}), + (8, 10, {"label": "Dense / Dashboard", "spacing": {"xs": "2px", "sm": "4px", "md": "8px", "lg": "12px", "xl": "16px", "2xl": "24px", "3xl": "32px"}}), + ], +} + + +def _resolve_dial(dial_name: str, value) -> dict: + """Bucket a 1-10 dial value into its tier config. Returns None if value is None.""" + if value is None: + return None + value = max(1, min(10, int(value))) + for lo, hi, info in DIAL_TIERS[dial_name]: + if lo <= value <= hi: + return {**info, "value": value} + return None + # ============ DESIGN SYSTEM GENERATOR ============ class DesignSystemGenerator: @@ -168,8 +201,18 @@ class DesignSystemGenerator: """Extract results list from search result dict.""" return search_result.get("results", []) - def generate(self, query: str, project_name: str = None) -> dict: - """Generate complete design system recommendation.""" + def generate(self, query: str, project_name: str = None, + variance: int = None, motion: int = None, density: int = None) -> dict: + """Generate complete design system recommendation. + + variance/motion/density are optional 1-10 dials (see DIAL_TIERS) that bias + style selection, pull in a matching motion.csv snippet, and override the + spacing scale, without changing behavior when left unset. + """ + variance_info = _resolve_dial("variance", variance) + motion_info = _resolve_dial("motion", motion) + density_info = _resolve_dial("density", density) + # Step 1: First search product to get category product_result = search(query, "product", 1) product_results = product_result.get("results", []) @@ -181,8 +224,14 @@ class DesignSystemGenerator: reasoning = self._apply_reasoning(category, {}) style_priority = reasoning.get("style_priority", []) + # DESIGN_VARIANCE dial: bias style retrieval/selection toward + # centered-minimal (low) or bold-asymmetric (high) keywords. + effective_style_priority = style_priority + if variance_info: + effective_style_priority = variance_info["style_keywords"] + style_priority + # Step 3: Multi-domain search with style priority hints - search_results = self._multi_domain_search(query, style_priority) + search_results = self._multi_domain_search(query, effective_style_priority) search_results["product"] = product_result # Reuse product search # Step 4: Select best matches from each domain using priority @@ -191,11 +240,24 @@ class DesignSystemGenerator: typography_results = self._extract_results(search_results.get("typography", {})) landing_results = self._extract_results(search_results.get("landing", {})) - best_style = self._select_best_match(style_results, reasoning.get("style_priority", [])) + best_style = self._select_best_match(style_results, effective_style_priority) best_color = color_results[0] if color_results else {} best_typography = typography_results[0] if typography_results else {} best_landing = landing_results[0] if landing_results else {} + # MOTION_INTENSITY dial: pull a matching GSAP skeleton from motion.csv + # (domain key is "gsap", not "motion" - PR #296 already owns the "motion" + # domain for Emil Kowalski's motion-design principles, motion-principles.csv). + motion_snippet = {} + if motion_info: + motion_result = search(f"{query} {motion_info['tier']}", "gsap", 5) + motion_matches = motion_result.get("results", []) + tiered = [m for m in motion_matches if m.get("Intensity Tier") == motion_info["tier"]] + if tiered: + motion_snippet = tiered[0] + elif motion_matches: + motion_snippet = motion_matches[0] + # Step 5: Build final recommendation # Combine effects from both reasoning and style search style_effects = best_style.get("Effects & Animation", "") @@ -250,7 +312,17 @@ class DesignSystemGenerator: "key_effects": combined_effects, "anti_patterns": reasoning.get("anti_patterns", ""), "decision_rules": reasoning.get("decision_rules", {}), - "severity": reasoning.get("severity", "MEDIUM") + "severity": reasoning.get("severity", "MEDIUM"), + "dials": { + "variance": variance_info["value"] if variance_info else None, + "variance_label": variance_info["label"] if variance_info else None, + "motion": motion_info["value"] if motion_info else None, + "motion_label": motion_info["label"] if motion_info else None, + "density": density_info["value"] if density_info else None, + "density_label": density_info["label"] if density_info else None, + }, + "motion_snippet": motion_snippet, + "spacing_scale": density_info["spacing"] if density_info else None, } @@ -296,6 +368,8 @@ def format_ascii_box(design_system: dict) -> str: typography = design_system.get("typography", {}) effects = design_system.get("key_effects", "") anti_patterns = design_system.get("anti_patterns", "") + dials = design_system.get("dials", {}) + motion_snippet = design_system.get("motion_snippet", {}) def wrap_text(text: str, prefix: str, width: int) -> list: """Wrap long text into multiple lines.""" @@ -329,6 +403,16 @@ def format_ascii_box(design_system: dict) -> str: lines.append("╚" + "═" * w + "╝") lines.append("┌" + "─" * w + "┐") + # Design Dials section (only if at least one dial was set) + if any(dials.get(k) is not None for k in ("variance", "motion", "density")): + lines.append(section_header("DESIGN DIALS", BOX_WIDTH + 1)) + if dials.get("variance") is not None: + lines.append(f"│ Variance: {dials['variance']}/10 — {dials['variance_label']}".ljust(BOX_WIDTH) + "│") + if dials.get("motion") is not None: + lines.append(f"│ Motion: {dials['motion']}/10 — {dials['motion_label']}".ljust(BOX_WIDTH) + "│") + if dials.get("density") is not None: + lines.append(f"│ Density: {dials['density']}/10 — {dials['density_label']}".ljust(BOX_WIDTH) + "│") + # Pattern section lines.append(section_header("PATTERN", BOX_WIDTH + 1)) lines.append(f"│ Name: {pattern.get('name', '')}".ljust(BOX_WIDTH) + "│") @@ -402,6 +486,17 @@ def format_ascii_box(design_system: dict) -> str: for line in wrap_text(effects, "│ ", BOX_WIDTH): lines.append(line.ljust(BOX_WIDTH) + "│") + # Motion section (GSAP skeleton, only if --motion dial was set) + if motion_snippet: + lines.append(section_header("MOTION", BOX_WIDTH + 1)) + lines.append(f"│ {motion_snippet.get('Category', '')} ({motion_snippet.get('Intensity Tier', '')})".ljust(BOX_WIDTH) + "│") + lines.append(f"│ Trigger: {motion_snippet.get('Trigger', '')} | Duration: {motion_snippet.get('Duration', '')} | Easing: {motion_snippet.get('Easing', '')}".ljust(BOX_WIDTH) + "│") + for line in wrap_text(f"GSAP: {motion_snippet.get('GSAP Snippet', '')}", "│ ", BOX_WIDTH): + lines.append(line.ljust(BOX_WIDTH) + "│") + if motion_snippet.get("Framework Notes"): + for line in wrap_text(f"Framework: {motion_snippet.get('Framework Notes', '')}", "│ ", BOX_WIDTH): + lines.append(line.ljust(BOX_WIDTH) + "│") + # Anti-patterns section if anti_patterns: lines.append(section_header("AVOID", BOX_WIDTH + 1)) @@ -436,11 +531,24 @@ def format_markdown(design_system: dict) -> str: typography = design_system.get("typography", {}) effects = design_system.get("key_effects", "") anti_patterns = design_system.get("anti_patterns", "") + dials = design_system.get("dials", {}) + motion_snippet = design_system.get("motion_snippet", {}) lines = [] lines.append(f"## Design System: {project}") lines.append("") + # Design Dials section (only if at least one dial was set) + if any(dials.get(k) is not None for k in ("variance", "motion", "density")): + lines.append("### Design Dials") + if dials.get("variance") is not None: + lines.append(f"- **Variance:** {dials['variance']}/10 — {dials['variance_label']}") + if dials.get("motion") is not None: + lines.append(f"- **Motion:** {dials['motion']}/10 — {dials['motion_label']}") + if dials.get("density") is not None: + lines.append(f"- **Density:** {dials['density']}/10 — {dials['density_label']}") + lines.append("") + # Pattern section lines.append("### Pattern") lines.append(f"- **Name:** {pattern.get('name', '')}") @@ -515,6 +623,23 @@ def format_markdown(design_system: dict) -> str: lines.append(f"{effects}") lines.append("") + # Motion section (GSAP skeleton, only if --motion dial was set) + if motion_snippet: + lines.append("### Motion") + lines.append(f"**{motion_snippet.get('Category', '')}** ({motion_snippet.get('Intensity Tier', '')}) — Trigger: {motion_snippet.get('Trigger', '')} | Duration: {motion_snippet.get('Duration', '')} | Easing: `{motion_snippet.get('Easing', '')}`") + lines.append("```js") + lines.append(motion_snippet.get("GSAP Snippet", "")) + lines.append("```") + if motion_snippet.get("Framework Notes"): + lines.append(f"*Framework notes: {motion_snippet.get('Framework Notes', '')}*") + motion_do = motion_snippet.get("Do", "") + motion_dont = motion_snippet.get("Don't", "") + if motion_do: + lines.append(f"- ✅ {motion_do}") + if motion_dont: + lines.append(f"- ❌ {motion_dont}") + lines.append("") + # Anti-patterns section if anti_patterns: lines.append("### Avoid (Anti-patterns)") @@ -537,8 +662,9 @@ def format_markdown(design_system: dict) -> str: # ============ MAIN ENTRY POINT ============ -def generate_design_system(query: str, project_name: str = None, output_format: str = "ascii", - persist: bool = False, page: str = None, output_dir: str = None) -> str: +def generate_design_system(query: str, project_name: str = None, output_format: str = "ascii", + persist: bool = False, page: str = None, output_dir: str = None, + variance: int = None, motion: int = None, density: int = None) -> str: """ Main entry point for design system generation. @@ -549,13 +675,16 @@ def generate_design_system(query: str, project_name: str = None, output_format: persist: If True, save design system to design-system/ folder page: Optional page name for page-specific override file output_dir: Optional output directory (defaults to current working directory) + variance: Optional 1-10 DESIGN_VARIANCE dial (1=centered/minimal, 10=bold/asymmetric) + motion: Optional 1-10 MOTION_INTENSITY dial, pulls a matching GSAP snippet from motion.csv + density: Optional 1-10 VISUAL_DENSITY dial, overrides the spacing scale (1=spacious, 10=dense) Returns: Formatted design system string """ generator = DesignSystemGenerator() - design_system = generator.generate(query, project_name) - + design_system = generator.generate(query, project_name, variance=variance, motion=motion, density=density) + # Persist to files if requested if persist: persist_design_system(design_system, page, output_dir, query) @@ -627,11 +756,14 @@ def format_master_md(design_system: dict) -> str: typography = design_system.get("typography", {}) effects = design_system.get("key_effects", "") anti_patterns = design_system.get("anti_patterns", "") - + dials = design_system.get("dials", {}) + motion_snippet = design_system.get("motion_snippet", {}) + spacing_scale = design_system.get("spacing_scale") + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - + lines = [] - + # Logic header lines.append("# Design System Master File") lines.append("") @@ -644,6 +776,15 @@ def format_master_md(design_system: dict) -> str: lines.append(f"**Project:** {project}") lines.append(f"**Generated:** {timestamp}") lines.append(f"**Category:** {design_system.get('category', 'General')}") + if any(dials.get(k) is not None for k in ("variance", "motion", "density")): + dial_parts = [] + if dials.get("variance") is not None: + dial_parts.append(f"Variance {dials['variance']}/10 ({dials['variance_label']})") + if dials.get("motion") is not None: + dial_parts.append(f"Motion {dials['motion']}/10 ({dials['motion_label']})") + if dials.get("density") is not None: + dial_parts.append(f"Density {dials['density']}/10 ({dials['density_label']})") + lines.append(f"**Design Dials:** {' | '.join(dial_parts)}") lines.append("") lines.append("---") lines.append("") @@ -695,18 +836,24 @@ def format_master_md(design_system: dict) -> str: lines.append("```") lines.append("") - # Spacing Variables + # Spacing Variables (overridden by the VISUAL_DENSITY dial when set) + default_spacing = DIAL_TIERS["density"][1][2]["spacing"] # mid-tier = the historical defaults + scale = spacing_scale or default_spacing + spacing_usage = { + "xs": "Tight gaps", "sm": "Icon gaps, inline spacing", "md": "Standard padding", + "lg": "Section padding", "xl": "Large gaps", "2xl": "Section margins", "3xl": "Hero padding", + } lines.append("### Spacing Variables") lines.append("") + if spacing_scale: + lines.append(f"*Density: {dials.get('density')}/10 — {dials.get('density_label')}*") + lines.append("") lines.append("| Token | Value | Usage |") lines.append("|-------|-------|-------|") - lines.append("| `--space-xs` | `4px` / `0.25rem` | Tight gaps |") - lines.append("| `--space-sm` | `8px` / `0.5rem` | Icon gaps, inline spacing |") - lines.append("| `--space-md` | `16px` / `1rem` | Standard padding |") - lines.append("| `--space-lg` | `24px` / `1.5rem` | Section padding |") - lines.append("| `--space-xl` | `32px` / `2rem` | Large gaps |") - lines.append("| `--space-2xl` | `48px` / `3rem` | Section margins |") - lines.append("| `--space-3xl` | `64px` / `4rem` | Hero padding |") + for token in ("xs", "sm", "md", "lg", "xl", "2xl", "3xl"): + px_value = scale[token] + rem_value = f"{int(px_value.rstrip('px')) / 16:g}rem" + lines.append(f"| `--space-{token}` | `{px_value}` / `{rem_value}` | {spacing_usage[token]} |") lines.append("") # Shadow Depths @@ -848,7 +995,32 @@ def format_master_md(design_system: dict) -> str: lines.append(f"- **CTA Placement:** {pattern.get('cta_placement', '')}") lines.append(f"- **Section Order:** {pattern.get('sections', '')}") lines.append("") - + + # Motion section (GSAP skeleton, only if --motion dial was set) + if motion_snippet: + lines.append("---") + lines.append("") + lines.append("## Motion") + lines.append("") + lines.append(f"**{motion_snippet.get('Category', '')}** ({motion_snippet.get('Intensity Tier', '')}) — Trigger: {motion_snippet.get('Trigger', '')} | Duration: {motion_snippet.get('Duration', '')} | Easing: `{motion_snippet.get('Easing', '')}`") + lines.append("") + lines.append("```js") + lines.append(motion_snippet.get("GSAP Snippet", "")) + lines.append("```") + lines.append("") + if motion_snippet.get("Framework Notes"): + lines.append(f"**Framework notes:** {motion_snippet.get('Framework Notes', '')}") + lines.append("") + motion_do = motion_snippet.get("Do", "") + motion_dont = motion_snippet.get("Don't", "") + if motion_do: + lines.append(f"- ✅ {motion_do}") + if motion_dont: + lines.append(f"- ❌ {motion_dont}") + if motion_snippet.get("Performance Notes"): + lines.append(f"- ⚡ {motion_snippet.get('Performance Notes', '')}") + lines.append("") + # Anti-Patterns section lines.append("---") lines.append("") diff --git a/ui-ux-pro-max/cli/assets/scripts/search.py b/ui-ux-pro-max/cli/assets/scripts/search.py index f61c8337..e960ceac 100644 --- a/ui-ux-pro-max/cli/assets/scripts/search.py +++ b/ui-ux-pro-max/cli/assets/scripts/search.py @@ -5,9 +5,15 @@ UI/UX Pro Max Search - BM25 search engine for UI/UX style guides Usage: python search.py "" [--domain ] [--stack ] [--max-results 3] python search.py "" --design-system [-p "Project Name"] python search.py "" --design-system --persist [-p "Project Name"] [--page "dashboard"] + python search.py "" --design-system --variance 8 --motion 9 --density 7 -Domains: style, prompt, color, chart, landing, product, ux, typography, google-fonts -Stacks: react, nextjs, vue, svelte, astro, swiftui, react-native, flutter, nuxtjs, nuxt-ui, html-tailwind, shadcn, jetpack-compose, threejs, angular, laravel, javafx +Domains: style, prompt, color, chart, landing, product, ux, typography, google-fonts, gsap +Stacks: react, nextjs, vue, svelte, astro, swiftui, react-native, flutter, nuxtjs, nuxt-ui, html-tailwind, shadcn, jetpack-compose, threejs, angular, laravel, javafx, wpf, winui, avalonia, uno, uwp + +Design dials (1-10, only with --design-system): + --variance DESIGN_VARIANCE: 1=centered/minimal, 10=bold/asymmetric + --motion MOTION_INTENSITY: 1=subtle, 10=complex; attaches a GSAP snippet from motion.csv + --density VISUAL_DENSITY: 1=spacious, 10=dense/dashboard; overrides the spacing scale Persistence (Master + Overrides pattern): --persist Save design system to design-system/MASTER.md @@ -68,18 +74,25 @@ if __name__ == "__main__": parser.add_argument("--persist", action="store_true", help="Save design system to design-system/MASTER.md (creates hierarchical structure)") parser.add_argument("--page", type=str, default=None, help="Create page-specific override file in design-system/pages/") parser.add_argument("--output-dir", "-o", type=str, default=None, help="Output directory for persisted files (default: current directory)") + # Design dials (1-10), only applied with --design-system + parser.add_argument("--variance", type=int, choices=range(1, 11), metavar="1-10", help="DESIGN_VARIANCE dial: 1=centered/minimal, 10=bold/asymmetric (only with --design-system)") + parser.add_argument("--motion", type=int, choices=range(1, 11), metavar="1-10", help="MOTION_INTENSITY dial: 1=subtle, 10=complex; pulls a matching GSAP snippet from motion.csv (only with --design-system)") + parser.add_argument("--density", type=int, choices=range(1, 11), metavar="1-10", help="VISUAL_DENSITY dial: 1=spacious, 10=dense/dashboard; overrides the spacing scale (only with --design-system)") args = parser.parse_args() # Design system takes priority if args.design_system: result = generate_design_system( - args.query, - args.project_name, + args.query, + args.project_name, args.format, persist=args.persist, page=args.page, - output_dir=args.output_dir + output_dir=args.output_dir, + variance=args.variance, + motion=args.motion, + density=args.density ) print(result) diff --git a/ui-ux-pro-max/cli/assets/skills/brand/scripts/sync-brand-to-tokens.cjs b/ui-ux-pro-max/cli/assets/skills/brand/scripts/sync-brand-to-tokens.cjs index e7bc1711..013fa6ff 100644 --- a/ui-ux-pro-max/cli/assets/skills/brand/scripts/sync-brand-to-tokens.cjs +++ b/ui-ux-pro-max/cli/assets/skills/brand/scripts/sync-brand-to-tokens.cjs @@ -29,61 +29,47 @@ function extractColorsFromMarkdown(content) { accent: { name: 'accent', shades: {} } }; - // Extract primary color name and hex from Quick Reference table - const quickRefMatch = content.match(/Primary Color\s*\|\s*#([A-Fa-f0-9]{6})\s*\(([^)]+)\)/); - if (quickRefMatch) { - colors.primary.name = quickRefMatch[2].toLowerCase().replace(/\s+/g, '-'); - colors.primary.base = `#${quickRefMatch[1]}`; + // Match a "| Label | #hex |" markdown table row. Bold around the label + // (**Label**) is optional, so this handles both the bundled starter template + // ("| Primary Blue | #2563EB |") and bolded variants. + const rowRe = /\|\s*\*{0,2}([^*|]+?)\*{0,2}\s*\|\s*#([A-Fa-f0-9]{6})\b/g; + + // 1) Quick Reference table — hex only, no parenthesized name required. + const quickRef = { + primary: /Primary Color\s*\|\s*#([A-Fa-f0-9]{6})/i, + secondary: /Secondary Color\s*\|\s*#([A-Fa-f0-9]{6})/i, + accent: /Accent Color\s*\|\s*#([A-Fa-f0-9]{6})/i + }; + for (const key of Object.keys(quickRef)) { + const m = content.match(quickRef[key]); + if (m) colors[key].base = `#${m[1]}`; } - const secondaryMatch = content.match(/Secondary Color\s*\|\s*#([A-Fa-f0-9]{6})\s*\(([^)]+)\)/); - if (secondaryMatch) { - colors.secondary.name = secondaryMatch[2].toLowerCase().replace(/\s+/g, '-'); - colors.secondary.base = `#${secondaryMatch[1]}`; - } - - const accentMatch = content.match(/Accent Color\s*\|\s*#([A-Fa-f0-9]{6})\s*\(([^)]+)\)/); - if (accentMatch) { - colors.accent.name = accentMatch[2].toLowerCase().replace(/\s+/g, '-'); - colors.accent.base = `#${accentMatch[1]}`; - } - - // Extract all shades from Primary Colors table - const primarySection = content.match(/### Primary Colors[\s\S]*?\|[\s\S]*?(?=###|$)/i); - if (primarySection) { - const hexMatches = primarySection[0].matchAll(/\*\*([^*]+)\*\*\s*\|\s*#([A-Fa-f0-9]{6})/g); - for (const match of hexMatches) { - const name = match[1].trim().toLowerCase(); - const hex = `#${match[2]}`; - if (name.includes('dark')) colors.primary.dark = hex; - else if (name.includes('light')) colors.primary.light = hex; - else colors.primary.base = hex; + // 2) Dedicated "### Colors" tables — assign base/dark/light by the + // row label keyword. + const assignFromSection = (heading, target) => { + const section = content.match(new RegExp(`### ${heading}[\\s\\S]*?(?=\\n###|$)`, 'i')); + if (!section) return; + for (const m of section[0].matchAll(rowRe)) { + const label = m[1].trim().toLowerCase(); + const hex = `#${m[2]}`; + if (label.includes('dark')) target.dark = hex; + else if (label.includes('light')) target.light = hex; + else if (!target.base) target.base = hex; } - } + }; + assignFromSection('Primary Colors', colors.primary); + assignFromSection('Secondary Colors', colors.secondary); + assignFromSection('Accent Colors', colors.accent); - // Extract secondary shades - const secondarySection = content.match(/### Secondary Colors[\s\S]*?\|[\s\S]*?(?=###|$)/i); - if (secondarySection) { - const hexMatches = secondarySection[0].matchAll(/\*\*([^*]+)\*\*\s*\|\s*#([A-Fa-f0-9]{6})/g); - for (const match of hexMatches) { - const name = match[1].trim().toLowerCase(); - const hex = `#${match[2]}`; - if (name.includes('dark')) colors.secondary.dark = hex; - else if (name.includes('light')) colors.secondary.light = hex; - else colors.secondary.base = hex; - } - } - - // Extract accent shades - const accentSection = content.match(/### Accent Colors[\s\S]*?\|[\s\S]*?(?=###|$)/i); - if (accentSection) { - const hexMatches = accentSection[0].matchAll(/\*\*([^*]+)\*\*\s*\|\s*#([A-Fa-f0-9]{6})/g); - for (const match of hexMatches) { - const name = match[1].trim().toLowerCase(); - const hex = `#${match[2]}`; - if (name.includes('dark')) colors.accent.dark = hex; - else if (name.includes('light')) colors.accent.light = hex; - else colors.accent.base = hex; + // 3) Fallback: an accent swatch may live in another table (the starter + // lists "Accent Green" under Secondary Colors). + if (!colors.accent.base) { + for (const m of content.matchAll(rowRe)) { + if (m[1].trim().toLowerCase().includes('accent')) { + colors.accent.base = `#${m[2]}`; + break; + } } } @@ -113,6 +99,7 @@ function generateColorScale(baseHex, darkHex, lightHex) { * Adjust hex color brightness */ function adjustBrightness(hex, percent) { + if (typeof hex !== 'string') return '#000000'; const num = parseInt(hex.replace('#', ''), 16); const r = Math.min(255, Math.max(0, (num >> 16) + Math.round(255 * percent))); const g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + Math.round(255 * percent))); @@ -129,29 +116,24 @@ function updateDesignTokens(tokens, colors) { tokens.brand = brandName; // Update primitive colors with new names - const primitiveColors = tokens.primitive?.color || {}; + tokens.primitive = tokens.primitive || {}; + const primitiveColors = tokens.primitive.color || {}; // Remove old color keys, add new ones delete primitiveColors.coral; delete primitiveColors.purple; delete primitiveColors.mint; - // Add new named colors - primitiveColors[colors.primary.name] = generateColorScale( - colors.primary.base, - colors.primary.dark, - colors.primary.light - ); - primitiveColors[colors.secondary.name] = generateColorScale( - colors.secondary.base, - colors.secondary.dark, - colors.secondary.light - ); - primitiveColors[colors.accent.name] = generateColorScale( - colors.accent.base, - colors.accent.dark, - colors.accent.light - ); + // Add new named colors. Skip any role with no base hex rather than crashing + // on an unexpected guidelines format. + for (const role of ['primary', 'secondary', 'accent']) { + const c = colors[role]; + if (!c.base) { + console.warn(`⚠️ No base hex found for ${role} color — skipping its token scale.`); + continue; + } + primitiveColors[c.name] = generateColorScale(c.base, c.dark, c.light); + } tokens.primitive.color = primitiveColors; @@ -191,7 +173,7 @@ function updateDesignTokens(tokens, colors) { } // Update component references (button uses primary color with opacity) - if (tokens.component?.button?.secondary) { + if (tokens.component?.button?.secondary && colors.primary.base) { const primaryBase = colors.primary.base; tokens.component.button.secondary['bg-hover'] = { "$value": `${primaryBase}1A`, diff --git a/ui-ux-pro-max/cli/assets/skills/brand/scripts/tests/test_sync_brand_to_tokens.py b/ui-ux-pro-max/cli/assets/skills/brand/scripts/tests/test_sync_brand_to_tokens.py new file mode 100644 index 00000000..e0107569 --- /dev/null +++ b/ui-ux-pro-max/cli/assets/skills/brand/scripts/tests/test_sync_brand_to_tokens.py @@ -0,0 +1,52 @@ +"""Regression test for sync-brand-to-tokens.cjs. + +The color parser required a parenthesized name in the Quick Reference row +(`#2563EB (name)`) and a bolded label in the color tables (`**Primary Blue**`), +neither of which the bundled starter template uses. As a result the base hex +came back `undefined` and `adjustBrightness(undefined)` threw a TypeError — +i.e. the script crashed on its own documented happy path. This test runs the +sync against the bundled starter template and asserts it completes and writes +the expected base colors. It is pytest-based so the existing pytest CI runs it. +""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +SCRIPTS = Path(__file__).resolve().parent.parent +SCRIPT = SCRIPTS / "sync-brand-to-tokens.cjs" +BRAND_STARTER = SCRIPTS.parent / "templates" / "brand-guidelines-starter.md" +TOKENS_STARTER = ( + SCRIPTS.parent.parent / "design-system" / "templates" / "design-tokens-starter.json" +) + + +def test_sync_parses_bundled_starter_template(tmp_path): + node = shutil.which("node") + if not node: + pytest.skip("node not available") + + (tmp_path / "docs").mkdir() + (tmp_path / "assets").mkdir() + shutil.copy(BRAND_STARTER, tmp_path / "docs" / "brand-guidelines.md") + shutil.copy(TOKENS_STARTER, tmp_path / "assets" / "design-tokens.json") + + result = subprocess.run( + [node, str(SCRIPT)], + cwd=tmp_path, + capture_output=True, + text=True, + ) + + # Must not crash (the bug raised an unhandled TypeError). + assert "TypeError" not in result.stderr, result.stderr + assert result.returncode == 0, result.stderr + result.stdout + + tokens = json.loads((tmp_path / "assets" / "design-tokens.json").read_text()) + primitive = tokens["primitive"]["color"] + assert primitive["primary"]["500"]["$value"] == "#2563EB" + assert primitive["secondary"]["500"]["$value"] == "#8B5CF6" + assert primitive["accent"]["500"]["$value"] == "#10B981" diff --git a/ui-ux-pro-max/cli/assets/skills/design-system/scripts/tests/test_validate_tokens.py b/ui-ux-pro-max/cli/assets/skills/design-system/scripts/tests/test_validate_tokens.py new file mode 100644 index 00000000..bbde4ec8 --- /dev/null +++ b/ui-ux-pro-max/cli/assets/skills/design-system/scripts/tests/test_validate_tokens.py @@ -0,0 +1,48 @@ +"""Regression tests for validate-tokens.cjs. + +The validator used to skip any line containing ``var(--`` outright, so a +hardcoded value sharing a line with a token reference (extremely common in +real CSS, and universal in minified CSS where everything is one line) went +undetected. These tests drive the CLI via ``node`` and assert it flags such +cases. They are pytest-based so the repository's existing pytest CI runs them. +""" + +import shutil +import subprocess +from pathlib import Path + +import pytest + +SCRIPT = Path(__file__).resolve().parent.parent / "validate-tokens.cjs" + + +def _run(tmp_path: Path, css: str) -> subprocess.CompletedProcess: + node = shutil.which("node") + if not node: + pytest.skip("node not available") + (tmp_path / "sample.css").write_text(css) + return subprocess.run( + [node, str(SCRIPT), "--dir", str(tmp_path)], + capture_output=True, + text=True, + ) + + +def test_flags_hardcoded_hex_sharing_line_with_token(tmp_path): + """A hardcoded hex on the same line as a var() token is still a violation.""" + result = _run( + tmp_path, + ".btn { background: #FF6B6B; color: var(--color-primary); }\n", + ) + assert "#FF6B6B" in result.stdout, result.stdout + assert result.returncode == 1 + + +def test_token_only_line_reports_no_violation(tmp_path): + """A line that references only tokens produces no false positives.""" + result = _run( + tmp_path, + ".btn { background: var(--color-bg); color: var(--color-primary); }\n", + ) + assert "No token violations" in result.stdout, result.stdout + assert result.returncode == 0 diff --git a/ui-ux-pro-max/cli/assets/skills/design-system/scripts/validate-tokens.cjs b/ui-ux-pro-max/cli/assets/skills/design-system/scripts/validate-tokens.cjs index e3786658..9839ed24 100644 --- a/ui-ux-pro-max/cli/assets/skills/design-system/scripts/validate-tokens.cjs +++ b/ui-ux-pro-max/cli/assets/skills/design-system/scripts/validate-tokens.cjs @@ -137,11 +137,6 @@ function scanFile(filePath) { return; } - // Skip lines that already use CSS variables - if (line.includes('var(--')) { - return; - } - for (const [name, pattern] of Object.entries(patterns)) { const matches = line.match(pattern.regex); if (matches) { diff --git a/ui-ux-pro-max/cli/assets/skills/ui-styling/scripts/tailwind_config_gen.py b/ui-ux-pro-max/cli/assets/skills/ui-styling/scripts/tailwind_config_gen.py index 56cd277c..093c625f 100644 --- a/ui-ux-pro-max/cli/assets/skills/ui-styling/scripts/tailwind_config_gen.py +++ b/ui-ux-pro-max/cli/assets/skills/ui-styling/scripts/tailwind_config_gen.py @@ -213,7 +213,7 @@ class TailwindConfigGenerator: return f"""import type {{ Config }} from 'tailwindcss' const config: Config = {{ -{self._indent_json(config_json, 1)} +{self._indent_json(config_json, 1)}, plugins: [{plugins_str}], }} @@ -230,7 +230,7 @@ export default config return f"""/** @type {{import('tailwindcss').Config}} */ module.exports = {{ -{self._indent_json(config_json, 1)} +{self._indent_json(config_json, 1)}, plugins: [{plugins_str}], }} """ diff --git a/ui-ux-pro-max/cli/assets/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py b/ui-ux-pro-max/cli/assets/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py index a08414ee..2facdbd0 100644 --- a/ui-ux-pro-max/cli/assets/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py +++ b/ui-ux-pro-max/cli/assets/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py @@ -1,5 +1,7 @@ """Tests for tailwind_config_gen.py""" +import shutil +import subprocess from pathlib import Path import pytest @@ -334,3 +336,59 @@ class TestTailwindConfigGenerator: assert "module.exports" in content assert "primary" in content assert "@tailwindcss/forms" in content + + +def _strip_to_object(config_str: str) -> str: + """Reduce a generated TS/JS config to a bare assignable object so it can be + handed to `node --check` without a TypeScript loader.""" + lines = [] + for line in config_str.splitlines(): + if line.startswith("import type"): + continue + if line.strip() == "export default config": + continue + line = line.replace("const config: Config =", "const config =") + line = line.replace("module.exports =", "const config =") + lines.append(line) + return "\n".join(lines) + + +class TestGeneratedConfigIsValidJs: + """Regression guard for the missing-comma bug between the ``theme`` block and + ``plugins`` that produced syntactically invalid config files. The data-shape + tests above all passed while the emitted string was unparseable, so these + tests validate the serialized output itself.""" + + @pytest.mark.parametrize("typescript", [True, False]) + def test_property_before_plugins_is_comma_terminated(self, typescript): + """The property preceding ``plugins`` must end with a comma (pure-Python + check, so the regression is caught even where node is unavailable).""" + generator = TailwindConfigGenerator(typescript=typescript) + generator.add_colors({"brand": "#6366F1"}) + generator.add_breakpoints({"3xl": "1920px"}) + config = generator.generate_config_string() + + assert "}\n plugins:" not in config, "missing comma before plugins" + assert "},\n plugins:" in config + + @pytest.mark.parametrize("typescript", [True, False]) + def test_node_check_parses_generated_config(self, typescript, tmp_path): + """The emitted config parses as valid JS via ``node --check``.""" + node = shutil.which("node") + if not node: + pytest.skip("node not available") + + generator = TailwindConfigGenerator(typescript=typescript) + generator.add_colors({"brand": "#6366F1", "accent": "#10B981"}) + generator.add_fonts({"sans": ["Inter"]}) + generator.add_breakpoints({"3xl": "1920px"}) + generator.add_plugins(["tailwindcss-animate"]) + + snippet = _strip_to_object(generator.generate_config_string()) + path = tmp_path / "config.cjs" + path.write_text(snippet) + + result = subprocess.run( + [node, "--check", str(path)], capture_output=True, text=True + ) + assert result.returncode == 0, result.stderr diff --git a/ui-ux-pro-max/cli/assets/templates/base/skill-content.md b/ui-ux-pro-max/cli/assets/templates/base/skill-content.md index 929602eb..aedf8bc2 100644 --- a/ui-ux-pro-max/cli/assets/templates/base/skill-content.md +++ b/ui-ux-pro-max/cli/assets/templates/base/skill-content.md @@ -110,6 +110,29 @@ If not, use the Master rules exclusively. Now, generate the code... ``` +### Step 2c: Design Dials (optional) + +Three optional 1-10 sliders that tune `--design-system` output without changing your query. Add any combination of them to the same command: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --design-system --variance <1-10> --motion <1-10> --density <1-10> +``` + +| Dial | Low (1-3) | Mid (4-7) | High (8-10) | +|------|-----------|-----------|-------------| +| `--variance` | Centered / minimal (biases toward Minimalism-style categories) | Balanced / modern | Bold / asymmetric (biases toward Brutalism, Bento Grids) | +| `--motion` | Subtle micro-interactions | Standard scroll/stagger motion | Complex choreography (pin, Flip, SplitText) | +| `--density` | Spacious (24-96px spacing scale) | Standard (16-64px, current default) | Dense/dashboard (8-32px spacing scale) | + +- `--motion` attaches a ready-to-use GSAP snippet (with framework notes, Do/Don't, and performance notes) pulled from `--domain gsap`, matched to the resolved tier (Subtle/Standard/Complex). +- `--density` overrides the `--space-*` CSS variable table in the ASCII/markdown/MASTER.md output — use it for dashboards (high) vs. marketing pages (low) without hand-editing tokens. +- Leaving a dial unset keeps that part of the output exactly as it was before (no behavior change). + +**Example:** +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "internal analytics dashboard" --design-system --variance 8 --motion 7 --density 8 -p "Ops Console" +``` + ### Step 3: Supplement with Detailed Searches (as needed) After getting the design system, use domain searches to get additional details: @@ -156,6 +179,7 @@ python3 skills/ui-ux-pro-max/scripts/search.py "" --stack | `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof | | `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie | | `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading | +| `gsap` | GSAP animation skeletons by intensity tier | scroll reveal, stagger, magnetic cursor, page transition | | `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache | | `web` | App interface guidelines (iOS/Android/React Native) | accessibilityLabel, touch targets, safe areas, Dynamic Type | | `prompt` | AI prompts, CSS keywords | (style name) | diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/agent.json b/ui-ux-pro-max/cli/assets/templates/platforms/agent.json old mode 100755 new mode 100644 index d6687dbf..2c686311 --- a/ui-ux-pro-max/cli/assets/templates/platforms/agent.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/agent.json @@ -10,12 +10,12 @@ "scriptPath": "skills/ui-ux-pro-max/scripts/search.py", "frontmatter": { "name": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks." + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks." }, "sections": { "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/augment.json b/ui-ux-pro-max/cli/assets/templates/platforms/augment.json index 905682b0..d629328d 100644 --- a/ui-ux-pro-max/cli/assets/templates/platforms/augment.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/augment.json @@ -13,6 +13,6 @@ "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/claude.json b/ui-ux-pro-max/cli/assets/templates/platforms/claude.json index 1c7d5b62..1996b966 100644 --- a/ui-ux-pro-max/cli/assets/templates/platforms/claude.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/claude.json @@ -10,12 +10,12 @@ "scriptPath": "skills/ui-ux-pro-max/scripts/search.py", "frontmatter": { "name": "ui-ux-pro-max", - "description": "UI/UX design intelligence. 67 styles, 161 palettes, 57 font pairings, 25 charts, 16 stacks (React, Next.js, Vue, Svelte, Astro, SwiftUI, React Native, Flutter, Nuxt, Nuxt UI, Tailwind, shadcn/ui, Jetpack Compose, Three.js, Angular, Laravel). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient. Integrations: shadcn/ui MCP for component search and examples." + "description": "UI/UX design intelligence. 67 styles, 161 palettes, 57 font pairings, 25 charts, 21 stacks (React, Next.js, Vue, Svelte, Astro, SwiftUI, React Native, Flutter, WPF, WinUI 3, UWP, Avalonia, Uno Platform, Nuxt, Nuxt UI, Tailwind, shadcn/ui, Jetpack Compose, Three.js, Angular, Laravel). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, desktop app, .html, .tsx, .vue, .svelte, .xaml. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient. Integrations: shadcn/ui MCP for component search and examples." }, "sections": { "quickReference": true }, "title": "UI/UX Pro Max - Design Intelligence", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/codebuddy.json b/ui-ux-pro-max/cli/assets/templates/platforms/codebuddy.json index 4c58e40f..5a32b6e0 100644 --- a/ui-ux-pro-max/cli/assets/templates/platforms/codebuddy.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/codebuddy.json @@ -16,6 +16,6 @@ "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/codex.json b/ui-ux-pro-max/cli/assets/templates/platforms/codex.json index 95ae1b09..e04fd0e9 100644 --- a/ui-ux-pro-max/cli/assets/templates/platforms/codex.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/codex.json @@ -16,6 +16,6 @@ "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/continue.json b/ui-ux-pro-max/cli/assets/templates/platforms/continue.json index b77eb222..05c0e66e 100644 --- a/ui-ux-pro-max/cli/assets/templates/platforms/continue.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/continue.json @@ -16,6 +16,6 @@ "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/copilot.json b/ui-ux-pro-max/cli/assets/templates/platforms/copilot.json old mode 100755 new mode 100644 index ecac1aae..e381e694 --- a/ui-ux-pro-max/cli/assets/templates/platforms/copilot.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/copilot.json @@ -10,12 +10,12 @@ "scriptPath": "prompts/ui-ux-pro-max/scripts/search.py", "frontmatter": { "name": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks." + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks." }, "sections": { "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Workflow" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/cursor.json b/ui-ux-pro-max/cli/assets/templates/platforms/cursor.json old mode 100755 new mode 100644 index f1b73b46..2a9c77f6 --- a/ui-ux-pro-max/cli/assets/templates/platforms/cursor.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/cursor.json @@ -10,12 +10,12 @@ "scriptPath": "skills/ui-ux-pro-max/scripts/search.py", "frontmatter": { "name": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks." + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks." }, "sections": { "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/gemini.json b/ui-ux-pro-max/cli/assets/templates/platforms/gemini.json index fd4f8c3b..45eddb41 100644 --- a/ui-ux-pro-max/cli/assets/templates/platforms/gemini.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/gemini.json @@ -16,6 +16,6 @@ "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/kilocode.json b/ui-ux-pro-max/cli/assets/templates/platforms/kilocode.json index 29d9aad7..23413c29 100644 --- a/ui-ux-pro-max/cli/assets/templates/platforms/kilocode.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/kilocode.json @@ -10,12 +10,12 @@ "scriptPath": "skills/ui-ux-pro-max/scripts/search.py", "frontmatter": { "name": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks." + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks." }, "sections": { "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/kiro.json b/ui-ux-pro-max/cli/assets/templates/platforms/kiro.json old mode 100755 new mode 100644 index db2cd79c..311f0978 --- a/ui-ux-pro-max/cli/assets/templates/platforms/kiro.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/kiro.json @@ -10,12 +10,12 @@ "scriptPath": "steering/ui-ux-pro-max/scripts/search.py", "frontmatter": { "name": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks." + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks." }, "sections": { "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Workflow" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/opencode.json b/ui-ux-pro-max/cli/assets/templates/platforms/opencode.json index 6ff4ec0f..63033d46 100644 --- a/ui-ux-pro-max/cli/assets/templates/platforms/opencode.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/opencode.json @@ -16,6 +16,6 @@ "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/qoder.json b/ui-ux-pro-max/cli/assets/templates/platforms/qoder.json index f408052d..0795f8bb 100644 --- a/ui-ux-pro-max/cli/assets/templates/platforms/qoder.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/qoder.json @@ -16,6 +16,6 @@ "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/roocode.json b/ui-ux-pro-max/cli/assets/templates/platforms/roocode.json old mode 100755 new mode 100644 index 2fb8c865..cd4ba517 --- a/ui-ux-pro-max/cli/assets/templates/platforms/roocode.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/roocode.json @@ -10,12 +10,12 @@ "scriptPath": "skills/ui-ux-pro-max/scripts/search.py", "frontmatter": { "name": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks." + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks." }, "sections": { "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Workflow" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/trae.json b/ui-ux-pro-max/cli/assets/templates/platforms/trae.json index 040169f2..49b6473a 100644 --- a/ui-ux-pro-max/cli/assets/templates/platforms/trae.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/trae.json @@ -16,6 +16,6 @@ "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/warp.json b/ui-ux-pro-max/cli/assets/templates/platforms/warp.json index 95ab68f1..b2c7a120 100644 --- a/ui-ux-pro-max/cli/assets/templates/platforms/warp.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/warp.json @@ -13,6 +13,6 @@ "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/cli/assets/templates/platforms/windsurf.json b/ui-ux-pro-max/cli/assets/templates/platforms/windsurf.json old mode 100755 new mode 100644 index f5e539cb..e76424df --- a/ui-ux-pro-max/cli/assets/templates/platforms/windsurf.json +++ b/ui-ux-pro-max/cli/assets/templates/platforms/windsurf.json @@ -10,12 +10,12 @@ "scriptPath": "skills/ui-ux-pro-max/scripts/search.py", "frontmatter": { "name": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks." + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks." }, "sections": { "quickReference": false }, "title": "ui-ux-pro-max", - "description": "Comprehensive design guide for web and mobile applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 16 technology stacks. Searchable database with priority-based recommendations.", + "description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.", "skillOrWorkflow": "Skill" } diff --git a/ui-ux-pro-max/src/ui-ux-pro-max/data/motion.csv b/ui-ux-pro-max/src/ui-ux-pro-max/data/motion.csv new file mode 100644 index 00000000..e5555fec --- /dev/null +++ b/ui-ux-pro-max/src/ui-ux-pro-max/data/motion.csv @@ -0,0 +1,17 @@ +No,Category,Intensity Tier,Keywords,Trigger,Duration,Easing,GSAP Snippet,Framework Notes,Do,Don't,Performance Notes +1,Hover Micro-interaction,Subtle,"hover, button, opacity, lift, press feedback",hover,150-200ms,power1.out,"gsap.to(el, { y: -1, opacity: 0.9, duration: 0.15, ease: 'power1.out' });",Bind on mouseenter/mouseleave; in React wrap in a ref + useEffect (or onMouseEnter/onMouseLeave props directly calling gsap.to),Keep displacement under 2px so it reads as feedback not motion,Don't animate layout-affecting props (width/height/margin) on hover,Runs on transform/opacity only so it stays on the compositor thread +2,Hover Micro-interaction,Standard,"hover, card, scale, tilt, cursor feedback",hover,200-300ms,power2.out,"gsap.to(el, { y: -4, scale: 1.02, boxShadow: '0 12px 24px rgba(0,0,0,0.12)', duration: 0.25, ease: 'power2.out' });","Use gsap.quickTo(el, 'y') for cards with many hover targets to avoid re-creating tweens every event",Pair with a matching mouseleave tween that reverses the same properties,Don't leave the hover state stuck if the pointer leaves fast; always attach the reverse tween,quickTo() avoids GC churn on lists with 20+ hoverable cards +3,Hover Micro-interaction,Complex,"hover, magnetic, cursor follow, 3d tilt",hover + mousemove,300-500ms,"elastic.out(1,0.4)","const xTo = gsap.quickTo(el, 'x', { duration: 0.4, ease: 'elastic.out(1,0.4)' }); const yTo = gsap.quickTo(el, 'y', { duration: 0.4, ease: 'elastic.out(1,0.4)' }); el.addEventListener('mousemove', (e) => { const r = el.getBoundingClientRect(); xTo((e.clientX - r.left - r.width/2) * 0.3); yTo((e.clientY - r.top - r.height/2) * 0.3); });",Debounce is not needed since quickTo interpolates; remove listeners on component unmount in React/Vue to avoid leaks,Clamp the pull strength (e.g. * 0.3) so the element never fully leaves its hit box,Don't apply magnetic effect to more than 1-2 focal elements per screen; it becomes noisy,Use will-change: transform on the target element for smoother compositing +4,Scroll Reveal,Subtle,"scroll, fade in, reveal, on view",scroll (viewport enter),300-400ms,power1.out,"gsap.from(el, { opacity: 0, y: 12, duration: 0.35, ease: 'power1.out', scrollTrigger: { trigger: el, start: 'top 90%', toggleActions: 'play none none reverse' } });",Requires the ScrollTrigger plugin registered once via gsap.registerPlugin(ScrollTrigger),"Keep the y offset small (8-16px) so it reads as a fade, not a slide",Don't reveal below-the-fold content needed for SEO/crawlers as invisible-by-default without a no-JS fallback,toggleActions 'play none none reverse' avoids re-triggering on every scroll direction change +5,Scroll Reveal,Standard,"scroll, slide up, staggered section, reveal",scroll (viewport enter),400-600ms,power2.out,"gsap.from(el.children, { opacity: 0, y: 24, duration: 0.5, stagger: 0.08, ease: 'power2.out', scrollTrigger: { trigger: el, start: 'top 85%' } });","In React use useGSAP(() => {...}, { scope: containerRef }) from @gsap/react to auto-cleanup on unmount",Scope the ScrollTrigger to the section container so it doesn't re-scan the whole page,Don't stagger more than ~8 children; beyond that the last items feel laggy,Set scroller/markers: false in production; markers is dev-only +6,Scroll Reveal,Complex,"scroll, pin, scrub, storytelling, scrollytelling",scroll (continuous scrub),tied to scroll position,none (scrub-driven),"gsap.timeline({ scrollTrigger: { trigger: section, start: 'top top', end: '+=150%', scrub: 1, pin: true } }).from('.headline', { opacity: 0, y: 40 }).to('.bg-layer', { yPercent: -20 }, '<');",Pinning needs the section to have deterministic height; recalc ScrollTrigger.refresh() after images/fonts load,Use scrub: true or a small number (0.5-1.5) instead of instant jumps so it feels tied to the scrollbar,Don't pin more than 1-2 sections per page; excessive pinning fights native scroll feel and hurts mobile UX,"Pinning forces layout reflow; test on mid-tier mobile devices, not just desktop" +7,Stagger List,Subtle,"list, stagger, cards, grid entrance",load or scroll,250-350ms,power1.out,"gsap.from('.list-item', { opacity: 0, y: 8, duration: 0.3, stagger: 0.03 });",Select items with a stable class/data-attribute (not array index) so re-renders in React don't break targeting,Keep per-item stagger delay small (0.02-0.04s) for lists longer than 10 items,Don't stagger by more than 0.1s per item on long lists; total reveal time becomes sluggish,"For virtualized lists, only animate items currently mounted in the DOM" +8,Stagger List,Standard,"grid, bento, cards, staggered scale",load or scroll,300-450ms,back.out(1.4),"gsap.from('.grid-item', { opacity: 0, scale: 0.92, y: 16, duration: 0.4, stagger: { each: 0.06, from: 'start', grid: 'auto' }, ease: 'back.out(1.4)' });",grid: 'auto' lets GSAP infer rows/columns from a CSS grid layout for a natural wave stagger,Combine with from: 'center' for a bento-grid layout to draw the eye inward first,Don't use back.out on dense data tables; the overshoot reads as sloppy on informational UI,Group DOM writes; avoid interleaving layout reads (getBoundingClientRect) between staggered tweens +9,Stagger List,Complex,"stagger, wave, text reveal, split text",load or scroll,400-700ms,expo.out,"const split = new SplitText(headline, { type: 'chars' }); gsap.from(split.chars, { opacity: 0, y: 20, rotateX: -40, duration: 0.6, stagger: 0.015, ease: 'expo.out' });",SplitText is a GSAP Club/paid plugin; confirm license before shipping and provide a plain fade fallback if unavailable,Revert SplitText on unmount/cleanup (split.revert()) to restore original text nodes for accessibility tools,Don't split-animate long paragraphs; reserve for short headlines (under ~8 words),Splitting text creates one element per character; keep it to headline-length copy only for DOM size +10,Page Transition,Subtle,"route change, fade, page transition",route change,200-300ms,power1.inOut,"gsap.to(main, { opacity: 0, duration: 0.2, onComplete: () => { navigate(); gsap.fromTo(main, { opacity: 0 }, { opacity: 1, duration: 0.2 }); } });","Pair with the router's transition hooks (Next.js App Router transitions, React Router's useNavigate, Vue Router's beforeEach/afterEach)",Preload the destination route's critical assets before the exit tween finishes,Don't block navigation on animation; cap exit duration at ~250ms so the app never feels unresponsive,Exit animation should always resolve faster than entrance (asymmetric timing) so back/forward feels snappy +11,Page Transition,Standard,"route change, slide, overlay wipe",route change,400-600ms,power2.inOut,"const tl = gsap.timeline(); tl.to('.transition-overlay', { yPercent: 0, duration: 0.4, ease: 'power2.inOut' }).call(navigate).to('.transition-overlay', { yPercent: -100, duration: 0.4, ease: 'power2.inOut', delay: 0.1 });",Keep the overlay element mounted at the layout root (outside the page component) so it survives the route swap,Show a lightweight loading indicator if the destination route's data fetch outlasts the overlay,Don't tie the overlay's reveal directly to data-fetch completion without a max-wait timeout; a slow API stalls the whole transition,Prefer CSS transform (yPercent) over top/left to keep the overlay animation on the compositor thread +12,Page Transition,Complex,"shared element, morph, hero transition",route change,500-800ms,expo.inOut,"const state = Flip.getState('.hero-image'); navigate(); Flip.from(state, { duration: 0.6, ease: 'expo.inOut', absolute: true, zIndex: 100 });",Requires the GSAP Flip plugin; the 'from' and 'to' route must render the same element with a shared data-flip-id,Verify the shared element exists in both DOM states before calling Flip.from to avoid a silent no-op,Don't use shared-element transitions across more than one element pair per navigation; compounding Flips are hard to time correctly,Flip recalculates layout (FLIP technique) so test on low-end devices for jank +13,Parallax Scroll,Subtle,"parallax, background, depth, scroll speed",scroll (continuous),tied to scroll position,linear (scrub),"gsap.to('.bg-layer', { yPercent: 10, ease: 'none', scrollTrigger: { trigger: section, scrub: true } });","Apply parallax to background/decorative layers only, never to text or interactive controls",Keep the yPercent delta small (5-15) so foreground and background never desync distractingly,Don't parallax body copy; it hurts reading comfort and can trigger motion sickness,will-change: transform on the parallax layer only; remove it after scroll settles to free GPU memory +14,Parallax Scroll,Standard,"multi-layer parallax, depth, hero background",scroll (continuous),tied to scroll position,linear (scrub),"gsap.utils.toArray('.parallax-layer').forEach((layer, i) => { gsap.to(layer, { yPercent: (i + 1) * -8, ease: 'none', scrollTrigger: { trigger: layer.parentElement, scrub: 0.5 } }); });",Layer count beyond 3-4 has diminishing visual return and multiplies scroll-listener cost,"Vary speed per layer (background slowest, foreground fastest) to sell the depth illusion",Don't let parallax layers overflow their container; clip with overflow: hidden on the wrapper,Batch all layers under one ScrollTrigger container where possible instead of one per layer +15,Loading / Skeleton,Subtle,"loading, skeleton, shimmer, pulse",on mount / async wait,1200-1600ms loop,sine.inOut,"gsap.to('.skeleton', { backgroundPosition: '200% 0', duration: 1.4, ease: 'sine.inOut', repeat: -1 });",Kill the loop tween (tween.kill()) as soon as real content mounts to avoid orphaned repeating animations,Use a CSS gradient background-position sweep rather than opacity pulsing; reads as 'loading' more clearly,Don't run more than one shimmer loop per skeleton group; sync them under one timeline so the wave reads as a single unit,repeat: -1 tweens are cheap but must be explicitly killed on unmount or they leak in SPA route changes +16,Loading / Skeleton,Standard,"progress, spinner, morphing loader",on mount / async wait,800-1200ms loop,power1.inOut,"gsap.timeline({ repeat: -1 }).to('.loader-dot', { y: -8, duration: 0.4, stagger: { each: 0.15, yoyo: true, repeat: 1 } });",Wrap the whole loop timeline in useGSAP with { revertOnUpdate: false } in React so it isn't rebuilt every render,Cap total loop duration under ~1.5s so long waits don't feel like the UI froze on a single beat,Don't use elaborate loaders for sub-300ms waits; they flash and feel worse than no indicator,Pause the timeline (tl.pause()) when the loading tab/view is not visible to save CPU on background tabs diff --git a/ui-ux-pro-max/src/ui-ux-pro-max/scripts/core.py b/ui-ux-pro-max/src/ui-ux-pro-max/scripts/core.py index 3a9bf78d..9d05db29 100755 --- a/ui-ux-pro-max/src/ui-ux-pro-max/scripts/core.py +++ b/ui-ux-pro-max/src/ui-ux-pro-max/scripts/core.py @@ -55,6 +55,11 @@ CSV_CONFIG = { "search_cols": ["Category", "Icon Name", "Keywords", "Best For"], "output_cols": ["Category", "Icon Name", "Keywords", "Library", "Import Code", "Usage", "Best For", "Style"] }, + "gsap": { + "file": "motion.csv", + "search_cols": ["Category", "Intensity Tier", "Keywords", "Trigger"], + "output_cols": ["Category", "Intensity Tier", "Trigger", "Duration", "Easing", "GSAP Snippet", "Framework Notes", "Do", "Don't", "Performance Notes"] + }, "react": { "file": "react-performance.csv", "search_cols": ["Category", "Issue", "Keywords", "Description"], @@ -215,6 +220,7 @@ def detect_domain(query): "typography": ["font pairing", "typography pairing", "heading font", "body font"], "google-fonts": ["google font", "font family", "font weight", "font style", "variable font", "noto", "font for", "find font", "font subset", "font language", "monospace font", "serif font", "sans serif font", "display font", "handwriting font", "font", "typography", "serif", "sans"], "icons": ["icon", "icons", "lucide", "heroicons", "symbol", "glyph", "pictogram", "svg icon"], + "gsap": ["gsap", "quickto", "scrolltrigger", "stagger", "magnetic cursor", "parallax", "page transition", "scroll reveal", "scroll-triggered", "scrollytelling", "flip plugin", "splittext", "shimmer", "skeleton loader"], "react": ["react", "next.js", "nextjs", "suspense", "memo", "usecallback", "useeffect", "rerender", "bundle", "waterfall", "barrel", "dynamic import", "rsc", "server component"], "web": ["aria", "focus", "outline", "semantic", "virtualize", "autocomplete", "form", "input type", "preconnect"] } diff --git a/ui-ux-pro-max/src/ui-ux-pro-max/scripts/design_system.py b/ui-ux-pro-max/src/ui-ux-pro-max/scripts/design_system.py index f1f714e1..62dfbc8a 100644 --- a/ui-ux-pro-max/src/ui-ux-pro-max/scripts/design_system.py +++ b/ui-ux-pro-max/src/ui-ux-pro-max/scripts/design_system.py @@ -40,6 +40,39 @@ SEARCH_CONFIG = { "typography": {"max_results": 2} } +# ============ DESIGN DIALS (1-10) ============ +# Inspired by taste-skill's DESIGN_VARIANCE / MOTION_INTENSITY / VISUAL_DENSITY +# knobs: three optional 1-10 sliders that bias the existing query-based search +# instead of replacing it. Each dial buckets into a low/mid/high tier. +DIAL_TIERS = { + "variance": [ + (1, 3, {"label": "Centered / Minimal", "style_keywords": ["Minimalism", "Exaggerated Minimalism", "centered", "symmetric", "grid-based"]}), + (4, 7, {"label": "Balanced / Modern", "style_keywords": ["modern", "structured", "balanced"]}), + (8, 10, {"label": "Bold / Asymmetric", "style_keywords": ["Brutalism", "Bento Grids", "asymmetric", "experimental"]}), + ], + "motion": [ + (1, 3, {"label": "Subtle", "tier": "Subtle"}), + (4, 7, {"label": "Standard", "tier": "Standard"}), + (8, 10, {"label": "Complex", "tier": "Complex"}), + ], + "density": [ + (1, 3, {"label": "Spacious", "spacing": {"xs": "4px", "sm": "8px", "md": "24px", "lg": "32px", "xl": "48px", "2xl": "64px", "3xl": "96px"}}), + (4, 7, {"label": "Standard", "spacing": {"xs": "4px", "sm": "8px", "md": "16px", "lg": "24px", "xl": "32px", "2xl": "48px", "3xl": "64px"}}), + (8, 10, {"label": "Dense / Dashboard", "spacing": {"xs": "2px", "sm": "4px", "md": "8px", "lg": "12px", "xl": "16px", "2xl": "24px", "3xl": "32px"}}), + ], +} + + +def _resolve_dial(dial_name: str, value) -> dict: + """Bucket a 1-10 dial value into its tier config. Returns None if value is None.""" + if value is None: + return None + value = max(1, min(10, int(value))) + for lo, hi, info in DIAL_TIERS[dial_name]: + if lo <= value <= hi: + return {**info, "value": value} + return None + # ============ DESIGN SYSTEM GENERATOR ============ class DesignSystemGenerator: @@ -168,8 +201,18 @@ class DesignSystemGenerator: """Extract results list from search result dict.""" return search_result.get("results", []) - def generate(self, query: str, project_name: str = None) -> dict: - """Generate complete design system recommendation.""" + def generate(self, query: str, project_name: str = None, + variance: int = None, motion: int = None, density: int = None) -> dict: + """Generate complete design system recommendation. + + variance/motion/density are optional 1-10 dials (see DIAL_TIERS) that bias + style selection, pull in a matching motion.csv snippet, and override the + spacing scale, without changing behavior when left unset. + """ + variance_info = _resolve_dial("variance", variance) + motion_info = _resolve_dial("motion", motion) + density_info = _resolve_dial("density", density) + # Step 1: First search product to get category product_result = search(query, "product", 1) product_results = product_result.get("results", []) @@ -181,8 +224,14 @@ class DesignSystemGenerator: reasoning = self._apply_reasoning(category, {}) style_priority = reasoning.get("style_priority", []) + # DESIGN_VARIANCE dial: bias style retrieval/selection toward + # centered-minimal (low) or bold-asymmetric (high) keywords. + effective_style_priority = style_priority + if variance_info: + effective_style_priority = variance_info["style_keywords"] + style_priority + # Step 3: Multi-domain search with style priority hints - search_results = self._multi_domain_search(query, style_priority) + search_results = self._multi_domain_search(query, effective_style_priority) search_results["product"] = product_result # Reuse product search # Step 4: Select best matches from each domain using priority @@ -191,11 +240,24 @@ class DesignSystemGenerator: typography_results = self._extract_results(search_results.get("typography", {})) landing_results = self._extract_results(search_results.get("landing", {})) - best_style = self._select_best_match(style_results, reasoning.get("style_priority", [])) + best_style = self._select_best_match(style_results, effective_style_priority) best_color = color_results[0] if color_results else {} best_typography = typography_results[0] if typography_results else {} best_landing = landing_results[0] if landing_results else {} + # MOTION_INTENSITY dial: pull a matching GSAP skeleton from motion.csv + # (domain key is "gsap", not "motion" - PR #296 already owns the "motion" + # domain for Emil Kowalski's motion-design principles, motion-principles.csv). + motion_snippet = {} + if motion_info: + motion_result = search(f"{query} {motion_info['tier']}", "gsap", 5) + motion_matches = motion_result.get("results", []) + tiered = [m for m in motion_matches if m.get("Intensity Tier") == motion_info["tier"]] + if tiered: + motion_snippet = tiered[0] + elif motion_matches: + motion_snippet = motion_matches[0] + # Step 5: Build final recommendation # Combine effects from both reasoning and style search style_effects = best_style.get("Effects & Animation", "") @@ -250,7 +312,17 @@ class DesignSystemGenerator: "key_effects": combined_effects, "anti_patterns": reasoning.get("anti_patterns", ""), "decision_rules": reasoning.get("decision_rules", {}), - "severity": reasoning.get("severity", "MEDIUM") + "severity": reasoning.get("severity", "MEDIUM"), + "dials": { + "variance": variance_info["value"] if variance_info else None, + "variance_label": variance_info["label"] if variance_info else None, + "motion": motion_info["value"] if motion_info else None, + "motion_label": motion_info["label"] if motion_info else None, + "density": density_info["value"] if density_info else None, + "density_label": density_info["label"] if density_info else None, + }, + "motion_snippet": motion_snippet, + "spacing_scale": density_info["spacing"] if density_info else None, } @@ -296,6 +368,8 @@ def format_ascii_box(design_system: dict) -> str: typography = design_system.get("typography", {}) effects = design_system.get("key_effects", "") anti_patterns = design_system.get("anti_patterns", "") + dials = design_system.get("dials", {}) + motion_snippet = design_system.get("motion_snippet", {}) def wrap_text(text: str, prefix: str, width: int) -> list: """Wrap long text into multiple lines.""" @@ -329,6 +403,16 @@ def format_ascii_box(design_system: dict) -> str: lines.append("╚" + "═" * w + "╝") lines.append("┌" + "─" * w + "┐") + # Design Dials section (only if at least one dial was set) + if any(dials.get(k) is not None for k in ("variance", "motion", "density")): + lines.append(section_header("DESIGN DIALS", BOX_WIDTH + 1)) + if dials.get("variance") is not None: + lines.append(f"│ Variance: {dials['variance']}/10 — {dials['variance_label']}".ljust(BOX_WIDTH) + "│") + if dials.get("motion") is not None: + lines.append(f"│ Motion: {dials['motion']}/10 — {dials['motion_label']}".ljust(BOX_WIDTH) + "│") + if dials.get("density") is not None: + lines.append(f"│ Density: {dials['density']}/10 — {dials['density_label']}".ljust(BOX_WIDTH) + "│") + # Pattern section lines.append(section_header("PATTERN", BOX_WIDTH + 1)) lines.append(f"│ Name: {pattern.get('name', '')}".ljust(BOX_WIDTH) + "│") @@ -402,6 +486,17 @@ def format_ascii_box(design_system: dict) -> str: for line in wrap_text(effects, "│ ", BOX_WIDTH): lines.append(line.ljust(BOX_WIDTH) + "│") + # Motion section (GSAP skeleton, only if --motion dial was set) + if motion_snippet: + lines.append(section_header("MOTION", BOX_WIDTH + 1)) + lines.append(f"│ {motion_snippet.get('Category', '')} ({motion_snippet.get('Intensity Tier', '')})".ljust(BOX_WIDTH) + "│") + lines.append(f"│ Trigger: {motion_snippet.get('Trigger', '')} | Duration: {motion_snippet.get('Duration', '')} | Easing: {motion_snippet.get('Easing', '')}".ljust(BOX_WIDTH) + "│") + for line in wrap_text(f"GSAP: {motion_snippet.get('GSAP Snippet', '')}", "│ ", BOX_WIDTH): + lines.append(line.ljust(BOX_WIDTH) + "│") + if motion_snippet.get("Framework Notes"): + for line in wrap_text(f"Framework: {motion_snippet.get('Framework Notes', '')}", "│ ", BOX_WIDTH): + lines.append(line.ljust(BOX_WIDTH) + "│") + # Anti-patterns section if anti_patterns: lines.append(section_header("AVOID", BOX_WIDTH + 1)) @@ -436,11 +531,24 @@ def format_markdown(design_system: dict) -> str: typography = design_system.get("typography", {}) effects = design_system.get("key_effects", "") anti_patterns = design_system.get("anti_patterns", "") + dials = design_system.get("dials", {}) + motion_snippet = design_system.get("motion_snippet", {}) lines = [] lines.append(f"## Design System: {project}") lines.append("") + # Design Dials section (only if at least one dial was set) + if any(dials.get(k) is not None for k in ("variance", "motion", "density")): + lines.append("### Design Dials") + if dials.get("variance") is not None: + lines.append(f"- **Variance:** {dials['variance']}/10 — {dials['variance_label']}") + if dials.get("motion") is not None: + lines.append(f"- **Motion:** {dials['motion']}/10 — {dials['motion_label']}") + if dials.get("density") is not None: + lines.append(f"- **Density:** {dials['density']}/10 — {dials['density_label']}") + lines.append("") + # Pattern section lines.append("### Pattern") lines.append(f"- **Name:** {pattern.get('name', '')}") @@ -515,6 +623,23 @@ def format_markdown(design_system: dict) -> str: lines.append(f"{effects}") lines.append("") + # Motion section (GSAP skeleton, only if --motion dial was set) + if motion_snippet: + lines.append("### Motion") + lines.append(f"**{motion_snippet.get('Category', '')}** ({motion_snippet.get('Intensity Tier', '')}) — Trigger: {motion_snippet.get('Trigger', '')} | Duration: {motion_snippet.get('Duration', '')} | Easing: `{motion_snippet.get('Easing', '')}`") + lines.append("```js") + lines.append(motion_snippet.get("GSAP Snippet", "")) + lines.append("```") + if motion_snippet.get("Framework Notes"): + lines.append(f"*Framework notes: {motion_snippet.get('Framework Notes', '')}*") + motion_do = motion_snippet.get("Do", "") + motion_dont = motion_snippet.get("Don't", "") + if motion_do: + lines.append(f"- ✅ {motion_do}") + if motion_dont: + lines.append(f"- ❌ {motion_dont}") + lines.append("") + # Anti-patterns section if anti_patterns: lines.append("### Avoid (Anti-patterns)") @@ -537,8 +662,9 @@ def format_markdown(design_system: dict) -> str: # ============ MAIN ENTRY POINT ============ -def generate_design_system(query: str, project_name: str = None, output_format: str = "ascii", - persist: bool = False, page: str = None, output_dir: str = None) -> str: +def generate_design_system(query: str, project_name: str = None, output_format: str = "ascii", + persist: bool = False, page: str = None, output_dir: str = None, + variance: int = None, motion: int = None, density: int = None) -> str: """ Main entry point for design system generation. @@ -549,13 +675,16 @@ def generate_design_system(query: str, project_name: str = None, output_format: persist: If True, save design system to design-system/ folder page: Optional page name for page-specific override file output_dir: Optional output directory (defaults to current working directory) + variance: Optional 1-10 DESIGN_VARIANCE dial (1=centered/minimal, 10=bold/asymmetric) + motion: Optional 1-10 MOTION_INTENSITY dial, pulls a matching GSAP snippet from motion.csv + density: Optional 1-10 VISUAL_DENSITY dial, overrides the spacing scale (1=spacious, 10=dense) Returns: Formatted design system string """ generator = DesignSystemGenerator() - design_system = generator.generate(query, project_name) - + design_system = generator.generate(query, project_name, variance=variance, motion=motion, density=density) + # Persist to files if requested if persist: persist_design_system(design_system, page, output_dir, query) @@ -627,11 +756,14 @@ def format_master_md(design_system: dict) -> str: typography = design_system.get("typography", {}) effects = design_system.get("key_effects", "") anti_patterns = design_system.get("anti_patterns", "") - + dials = design_system.get("dials", {}) + motion_snippet = design_system.get("motion_snippet", {}) + spacing_scale = design_system.get("spacing_scale") + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - + lines = [] - + # Logic header lines.append("# Design System Master File") lines.append("") @@ -644,6 +776,15 @@ def format_master_md(design_system: dict) -> str: lines.append(f"**Project:** {project}") lines.append(f"**Generated:** {timestamp}") lines.append(f"**Category:** {design_system.get('category', 'General')}") + if any(dials.get(k) is not None for k in ("variance", "motion", "density")): + dial_parts = [] + if dials.get("variance") is not None: + dial_parts.append(f"Variance {dials['variance']}/10 ({dials['variance_label']})") + if dials.get("motion") is not None: + dial_parts.append(f"Motion {dials['motion']}/10 ({dials['motion_label']})") + if dials.get("density") is not None: + dial_parts.append(f"Density {dials['density']}/10 ({dials['density_label']})") + lines.append(f"**Design Dials:** {' | '.join(dial_parts)}") lines.append("") lines.append("---") lines.append("") @@ -695,18 +836,24 @@ def format_master_md(design_system: dict) -> str: lines.append("```") lines.append("") - # Spacing Variables + # Spacing Variables (overridden by the VISUAL_DENSITY dial when set) + default_spacing = DIAL_TIERS["density"][1][2]["spacing"] # mid-tier = the historical defaults + scale = spacing_scale or default_spacing + spacing_usage = { + "xs": "Tight gaps", "sm": "Icon gaps, inline spacing", "md": "Standard padding", + "lg": "Section padding", "xl": "Large gaps", "2xl": "Section margins", "3xl": "Hero padding", + } lines.append("### Spacing Variables") lines.append("") + if spacing_scale: + lines.append(f"*Density: {dials.get('density')}/10 — {dials.get('density_label')}*") + lines.append("") lines.append("| Token | Value | Usage |") lines.append("|-------|-------|-------|") - lines.append("| `--space-xs` | `4px` / `0.25rem` | Tight gaps |") - lines.append("| `--space-sm` | `8px` / `0.5rem` | Icon gaps, inline spacing |") - lines.append("| `--space-md` | `16px` / `1rem` | Standard padding |") - lines.append("| `--space-lg` | `24px` / `1.5rem` | Section padding |") - lines.append("| `--space-xl` | `32px` / `2rem` | Large gaps |") - lines.append("| `--space-2xl` | `48px` / `3rem` | Section margins |") - lines.append("| `--space-3xl` | `64px` / `4rem` | Hero padding |") + for token in ("xs", "sm", "md", "lg", "xl", "2xl", "3xl"): + px_value = scale[token] + rem_value = f"{int(px_value.rstrip('px')) / 16:g}rem" + lines.append(f"| `--space-{token}` | `{px_value}` / `{rem_value}` | {spacing_usage[token]} |") lines.append("") # Shadow Depths @@ -848,7 +995,32 @@ def format_master_md(design_system: dict) -> str: lines.append(f"- **CTA Placement:** {pattern.get('cta_placement', '')}") lines.append(f"- **Section Order:** {pattern.get('sections', '')}") lines.append("") - + + # Motion section (GSAP skeleton, only if --motion dial was set) + if motion_snippet: + lines.append("---") + lines.append("") + lines.append("## Motion") + lines.append("") + lines.append(f"**{motion_snippet.get('Category', '')}** ({motion_snippet.get('Intensity Tier', '')}) — Trigger: {motion_snippet.get('Trigger', '')} | Duration: {motion_snippet.get('Duration', '')} | Easing: `{motion_snippet.get('Easing', '')}`") + lines.append("") + lines.append("```js") + lines.append(motion_snippet.get("GSAP Snippet", "")) + lines.append("```") + lines.append("") + if motion_snippet.get("Framework Notes"): + lines.append(f"**Framework notes:** {motion_snippet.get('Framework Notes', '')}") + lines.append("") + motion_do = motion_snippet.get("Do", "") + motion_dont = motion_snippet.get("Don't", "") + if motion_do: + lines.append(f"- ✅ {motion_do}") + if motion_dont: + lines.append(f"- ❌ {motion_dont}") + if motion_snippet.get("Performance Notes"): + lines.append(f"- ⚡ {motion_snippet.get('Performance Notes', '')}") + lines.append("") + # Anti-Patterns section lines.append("---") lines.append("") diff --git a/ui-ux-pro-max/src/ui-ux-pro-max/scripts/search.py b/ui-ux-pro-max/src/ui-ux-pro-max/scripts/search.py index 4d9dca63..4707316d 100644 --- a/ui-ux-pro-max/src/ui-ux-pro-max/scripts/search.py +++ b/ui-ux-pro-max/src/ui-ux-pro-max/scripts/search.py @@ -5,10 +5,16 @@ UI/UX Pro Max Search - BM25 search engine for UI/UX style guides Usage: python search.py "" [--domain ] [--stack ] [--max-results 3] python search.py "" --design-system [-p "Project Name"] python search.py "" --design-system --persist [-p "Project Name"] [--page "dashboard"] + python search.py "" --design-system --variance 8 --motion 9 --density 7 -Domains: style, prompt, color, chart, landing, product, ux, typography, google-fonts +Domains: style, prompt, color, chart, landing, product, ux, typography, google-fonts, gsap Stacks: react, nextjs, vue, svelte, astro, swiftui, react-native, flutter, nuxtjs, nuxt-ui, html-tailwind, shadcn, jetpack-compose, threejs, angular, laravel, javafx, wpf, winui, avalonia, uno, uwp +Design dials (1-10, only with --design-system): + --variance DESIGN_VARIANCE: 1=centered/minimal, 10=bold/asymmetric + --motion MOTION_INTENSITY: 1=subtle, 10=complex; attaches a GSAP snippet from motion.csv + --density VISUAL_DENSITY: 1=spacious, 10=dense/dashboard; overrides the spacing scale + Persistence (Master + Overrides pattern): --persist Save design system to design-system/MASTER.md --page Also create a page-specific override file in design-system/pages/ @@ -68,18 +74,25 @@ if __name__ == "__main__": parser.add_argument("--persist", action="store_true", help="Save design system to design-system/MASTER.md (creates hierarchical structure)") parser.add_argument("--page", type=str, default=None, help="Create page-specific override file in design-system/pages/") parser.add_argument("--output-dir", "-o", type=str, default=None, help="Output directory for persisted files (default: current directory)") + # Design dials (1-10), only applied with --design-system + parser.add_argument("--variance", type=int, choices=range(1, 11), metavar="1-10", help="DESIGN_VARIANCE dial: 1=centered/minimal, 10=bold/asymmetric (only with --design-system)") + parser.add_argument("--motion", type=int, choices=range(1, 11), metavar="1-10", help="MOTION_INTENSITY dial: 1=subtle, 10=complex; pulls a matching GSAP snippet from motion.csv (only with --design-system)") + parser.add_argument("--density", type=int, choices=range(1, 11), metavar="1-10", help="VISUAL_DENSITY dial: 1=spacious, 10=dense/dashboard; overrides the spacing scale (only with --design-system)") args = parser.parse_args() # Design system takes priority if args.design_system: result = generate_design_system( - args.query, - args.project_name, + args.query, + args.project_name, args.format, persist=args.persist, page=args.page, - output_dir=args.output_dir + output_dir=args.output_dir, + variance=args.variance, + motion=args.motion, + density=args.density ) print(result) diff --git a/ui-ux-pro-max/src/ui-ux-pro-max/templates/base/skill-content.md b/ui-ux-pro-max/src/ui-ux-pro-max/templates/base/skill-content.md index 929602eb..aedf8bc2 100644 --- a/ui-ux-pro-max/src/ui-ux-pro-max/templates/base/skill-content.md +++ b/ui-ux-pro-max/src/ui-ux-pro-max/templates/base/skill-content.md @@ -110,6 +110,29 @@ If not, use the Master rules exclusively. Now, generate the code... ``` +### Step 2c: Design Dials (optional) + +Three optional 1-10 sliders that tune `--design-system` output without changing your query. Add any combination of them to the same command: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --design-system --variance <1-10> --motion <1-10> --density <1-10> +``` + +| Dial | Low (1-3) | Mid (4-7) | High (8-10) | +|------|-----------|-----------|-------------| +| `--variance` | Centered / minimal (biases toward Minimalism-style categories) | Balanced / modern | Bold / asymmetric (biases toward Brutalism, Bento Grids) | +| `--motion` | Subtle micro-interactions | Standard scroll/stagger motion | Complex choreography (pin, Flip, SplitText) | +| `--density` | Spacious (24-96px spacing scale) | Standard (16-64px, current default) | Dense/dashboard (8-32px spacing scale) | + +- `--motion` attaches a ready-to-use GSAP snippet (with framework notes, Do/Don't, and performance notes) pulled from `--domain gsap`, matched to the resolved tier (Subtle/Standard/Complex). +- `--density` overrides the `--space-*` CSS variable table in the ASCII/markdown/MASTER.md output — use it for dashboards (high) vs. marketing pages (low) without hand-editing tokens. +- Leaving a dial unset keeps that part of the output exactly as it was before (no behavior change). + +**Example:** +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "internal analytics dashboard" --design-system --variance 8 --motion 7 --density 8 -p "Ops Console" +``` + ### Step 3: Supplement with Detailed Searches (as needed) After getting the design system, use domain searches to get additional details: @@ -156,6 +179,7 @@ python3 skills/ui-ux-pro-max/scripts/search.py "" --stack | `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof | | `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie | | `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading | +| `gsap` | GSAP animation skeletons by intensity tier | scroll reveal, stagger, magnetic cursor, page transition | | `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache | | `web` | App interface guidelines (iOS/Android/React Native) | accessibilityLabel, touch targets, safe areas, Dynamic Type | | `prompt` | AI prompts, CSS keywords | (style name) |