diff --git a/codex-rs/core/src/skills/assets/samples/plan/LICENSE.txt b/codex-rs/core/src/skills/assets/samples/plan/LICENSE.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/codex-rs/core/src/skills/assets/samples/plan/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/codex-rs/core/src/skills/assets/samples/skill-creator/SKILL.md b/codex-rs/core/src/skills/assets/samples/skill-creator/SKILL.md index f061c96e3..7b44b52b2 100644 --- a/codex-rs/core/src/skills/assets/samples/skill-creator/SKILL.md +++ b/codex-rs/core/src/skills/assets/samples/skill-creator/SKILL.md @@ -216,7 +216,7 @@ Follow these steps in order, skipping only if there is a clear reason why they a ### Skill Naming - Use lowercase letters, digits, and hyphens only; normalize user-provided titles to hyphen-case (e.g., "Plan Mode" -> `plan-mode`). -- When generating names, generate a name under 30 characters (letters, digits, hyphens). +- When generating names, generate a name under 64 characters (letters, digits, hyphens). - Prefer short, verb-led phrases that describe the action. - Namespace by tool when it improves clarity or triggering (e.g., `gh-address-comments`, `linear-address-issue`). - Name the skill folder exactly after the skill name. diff --git a/codex-rs/core/src/skills/assets/samples/skill-creator/scripts/init_skill.py b/codex-rs/core/src/skills/assets/samples/skill-creator/scripts/init_skill.py index c70271727..8633fe9e3 100644 --- a/codex-rs/core/src/skills/assets/samples/skill-creator/scripts/init_skill.py +++ b/codex-rs/core/src/skills/assets/samples/skill-creator/scripts/init_skill.py @@ -17,7 +17,7 @@ import re import sys from pathlib import Path -MAX_SKILL_NAME_LENGTH = 30 +MAX_SKILL_NAME_LENGTH = 64 ALLOWED_RESOURCES = {"scripts", "references", "assets"} SKILL_TEMPLATE = """--- @@ -37,23 +37,23 @@ description: [TODO: Complete and informative explanation of what the skill does **1. Workflow-Based** (best for sequential processes) - Works well when there are clear step-by-step procedures -- Example: DOCX skill with "Workflow Decision Tree" → "Reading" → "Creating" → "Editing" -- Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2... +- Example: DOCX skill with "Workflow Decision Tree" -> "Reading" -> "Creating" -> "Editing" +- Structure: ## Overview -> ## Workflow Decision Tree -> ## Step 1 -> ## Step 2... **2. Task-Based** (best for tool collections) - Works well when the skill offers different operations/capabilities -- Example: PDF skill with "Quick Start" → "Merge PDFs" → "Split PDFs" → "Extract Text" -- Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2... +- Example: PDF skill with "Quick Start" -> "Merge PDFs" -> "Split PDFs" -> "Extract Text" +- Structure: ## Overview -> ## Quick Start -> ## Task Category 1 -> ## Task Category 2... **3. Reference/Guidelines** (best for standards or specifications) - Works well for brand guidelines, coding standards, or requirements -- Example: Brand styling with "Brand Guidelines" → "Colors" → "Typography" → "Features" -- Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage... +- Example: Brand styling with "Brand Guidelines" -> "Colors" -> "Typography" -> "Features" +- Structure: ## Overview -> ## Guidelines -> ## Specifications -> ## Usage... **4. Capabilities-Based** (best for integrated systems) - Works well when the skill provides multiple interrelated features -- Example: Product Management with "Core Capabilities" → numbered capability list -- Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature... +- Example: Product Management with "Core Capabilities" -> numbered capability list +- Structure: ## Overview -> ## Core Capabilities -> ### 1. Feature -> ### 2. Feature... Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations). @@ -212,7 +212,7 @@ def parse_resources(raw_resources): invalid = sorted({item for item in resources if item not in ALLOWED_RESOURCES}) if invalid: allowed = ", ".join(sorted(ALLOWED_RESOURCES)) - print(f"❌ Error: Unknown resource type(s): {', '.join(invalid)}") + print(f"[ERROR] Unknown resource type(s): {', '.join(invalid)}") print(f" Allowed: {allowed}") sys.exit(1) deduped = [] @@ -233,23 +233,23 @@ def create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_ example_script = resource_dir / "example.py" example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name)) example_script.chmod(0o755) - print("✅ Created scripts/example.py") + print("[OK] Created scripts/example.py") else: - print("✅ Created scripts/") + print("[OK] Created scripts/") elif resource == "references": if include_examples: example_reference = resource_dir / "api_reference.md" example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title)) - print("✅ Created references/api_reference.md") + print("[OK] Created references/api_reference.md") else: - print("✅ Created references/") + print("[OK] Created references/") elif resource == "assets": if include_examples: example_asset = resource_dir / "example_asset.txt" example_asset.write_text(EXAMPLE_ASSET) - print("✅ Created assets/example_asset.txt") + print("[OK] Created assets/example_asset.txt") else: - print("✅ Created assets/") + print("[OK] Created assets/") def init_skill(skill_name, path, resources, include_examples): @@ -270,15 +270,15 @@ def init_skill(skill_name, path, resources, include_examples): # Check if directory already exists if skill_dir.exists(): - print(f"❌ Error: Skill directory already exists: {skill_dir}") + print(f"[ERROR] Skill directory already exists: {skill_dir}") return None # Create skill directory try: skill_dir.mkdir(parents=True, exist_ok=False) - print(f"✅ Created skill directory: {skill_dir}") + print(f"[OK] Created skill directory: {skill_dir}") except Exception as e: - print(f"❌ Error creating directory: {e}") + print(f"[ERROR] Error creating directory: {e}") return None # Create SKILL.md from template @@ -288,9 +288,9 @@ def init_skill(skill_name, path, resources, include_examples): skill_md_path = skill_dir / "SKILL.md" try: skill_md_path.write_text(skill_content) - print("✅ Created SKILL.md") + print("[OK] Created SKILL.md") except Exception as e: - print(f"❌ Error creating SKILL.md: {e}") + print(f"[ERROR] Error creating SKILL.md: {e}") return None # Create resource directories if requested @@ -298,11 +298,11 @@ def init_skill(skill_name, path, resources, include_examples): try: create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples) except Exception as e: - print(f"❌ Error creating resource directories: {e}") + print(f"[ERROR] Error creating resource directories: {e}") return None # Print next steps - print(f"\n✅ Skill '{skill_name}' initialized successfully at {skill_dir}") + print(f"\n[OK] Skill '{skill_name}' initialized successfully at {skill_dir}") print("\nNext steps:") print("1. Edit SKILL.md to complete the TODO items and update the description") if resources: @@ -338,11 +338,11 @@ def main(): raw_skill_name = args.skill_name skill_name = normalize_skill_name(raw_skill_name) if not skill_name: - print("❌ Error: Skill name must include at least one letter or digit.") + print("[ERROR] Skill name must include at least one letter or digit.") sys.exit(1) if len(skill_name) > MAX_SKILL_NAME_LENGTH: print( - f"❌ Error: Skill name '{skill_name}' is too long ({len(skill_name)} characters). " + f"[ERROR] Skill name '{skill_name}' is too long ({len(skill_name)} characters). " f"Maximum is {MAX_SKILL_NAME_LENGTH} characters." ) sys.exit(1) @@ -351,12 +351,12 @@ def main(): resources = parse_resources(args.resources) if args.examples and not resources: - print("❌ Error: --examples requires --resources to be set.") + print("[ERROR] --examples requires --resources to be set.") sys.exit(1) path = args.path - print(f"🚀 Initializing skill: {skill_name}") + print(f"Initializing skill: {skill_name}") print(f" Location: {path}") if resources: print(f" Resources: {', '.join(resources)}") diff --git a/codex-rs/core/src/skills/assets/samples/skill-creator/scripts/package_skill.py b/codex-rs/core/src/skills/assets/samples/skill-creator/scripts/package_skill.py index 4214dc9ac..9a039958b 100644 --- a/codex-rs/core/src/skills/assets/samples/skill-creator/scripts/package_skill.py +++ b/codex-rs/core/src/skills/assets/samples/skill-creator/scripts/package_skill.py @@ -32,27 +32,27 @@ def package_skill(skill_path, output_dir=None): # Validate skill folder exists if not skill_path.exists(): - print(f"❌ Error: Skill folder not found: {skill_path}") + print(f"[ERROR] Skill folder not found: {skill_path}") return None if not skill_path.is_dir(): - print(f"❌ Error: Path is not a directory: {skill_path}") + print(f"[ERROR] Path is not a directory: {skill_path}") return None # Validate SKILL.md exists skill_md = skill_path / "SKILL.md" if not skill_md.exists(): - print(f"❌ Error: SKILL.md not found in {skill_path}") + print(f"[ERROR] SKILL.md not found in {skill_path}") return None # Run validation before packaging - print("🔍 Validating skill...") + print("Validating skill...") valid, message = validate_skill(skill_path) if not valid: - print(f"❌ Validation failed: {message}") + print(f"[ERROR] Validation failed: {message}") print(" Please fix the validation errors before packaging.") return None - print(f"✅ {message}\n") + print(f"[OK] {message}\n") # Determine output location skill_name = skill_path.name @@ -75,11 +75,11 @@ def package_skill(skill_path, output_dir=None): zipf.write(file_path, arcname) print(f" Added: {arcname}") - print(f"\n✅ Successfully packaged skill to: {skill_filename}") + print(f"\n[OK] Successfully packaged skill to: {skill_filename}") return skill_filename except Exception as e: - print(f"❌ Error creating .skill file: {e}") + print(f"[ERROR] Error creating .skill file: {e}") return None @@ -94,7 +94,7 @@ def main(): skill_path = sys.argv[1] output_dir = sys.argv[2] if len(sys.argv) > 2 else None - print(f"📦 Packaging skill: {skill_path}") + print(f"Packaging skill: {skill_path}") if output_dir: print(f" Output directory: {output_dir}") print() diff --git a/codex-rs/core/src/skills/assets/samples/skill-creator/scripts/quick_validate.py b/codex-rs/core/src/skills/assets/samples/skill-creator/scripts/quick_validate.py index 7fca5da5c..0547b4041 100644 --- a/codex-rs/core/src/skills/assets/samples/skill-creator/scripts/quick_validate.py +++ b/codex-rs/core/src/skills/assets/samples/skill-creator/scripts/quick_validate.py @@ -9,7 +9,7 @@ from pathlib import Path import yaml -MAX_SKILL_NAME_LENGTH = 30 +MAX_SKILL_NAME_LENGTH = 64 def validate_skill(skill_path): diff --git a/codex-rs/core/src/skills/assets/samples/skill-installer/LICENSE.txt b/codex-rs/core/src/skills/assets/samples/skill-installer/LICENSE.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/codex-rs/core/src/skills/assets/samples/skill-installer/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/codex-rs/core/src/skills/assets/samples/skill-installer/SKILL.md b/codex-rs/core/src/skills/assets/samples/skill-installer/SKILL.md new file mode 100644 index 000000000..857c32d0f --- /dev/null +++ b/codex-rs/core/src/skills/assets/samples/skill-installer/SKILL.md @@ -0,0 +1,56 @@ +--- +name: skill-installer +description: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). +metadata: + short-description: Install curated skills from openai/skills or other repos +--- + +# Skill Installer + +Helps install skills. By default these are from https://github.com/openai/skills/tree/main/skills/.curated, but users can also provide other locations. + +Use the helper scripts based on the task: +- List curated skills when the user asks what is available, or if the user uses this skill without specifying what to do. +- Install from the curated list when the user provides a skill name. +- Install from another repo when the user provides a GitHub repo/path (including private repos). + +Install skills with the helper scripts. + +## Communication + +When listing curated skills, output approximately as follows, depending on the context of the user's request: +""" +Skills from {repo}: +1. skill-1 +2. skill-2 (already installed) +3. ... +Which ones would you like installed? +""" + +After installing a skill, tell the user: "Restart Codex to pick up new skills." + +## Scripts + +All of these scripts use network, so when running in the sandbox, request escalation when running them. + +- `scripts/list-curated-skills.py` (prints curated list with installed annotations) +- `scripts/list-curated-skills.py --format json` +- `scripts/install-skill-from-github.py --repo / --path [ ...]` +- `scripts/install-skill-from-github.py --url https://github.com///tree//` + +## Behavior and Options + +- Defaults to direct download for public GitHub repos. +- If download fails with auth/permission errors, falls back to git sparse checkout. +- Aborts if the destination skill directory already exists. +- Installs into `$CODEX_HOME/skills/` (defaults to `~/.codex/skills`). +- Multiple `--path` values install multiple skills in one run, each named from the path basename unless `--name` is supplied. +- Options: `--ref ` (default `main`), `--dest `, `--method auto|download|git`. + +## Notes + +- Curated listing is fetched from `https://github.com/openai/skills/tree/main/skills/.curated` via the GitHub API. If it is unavailable, explain the error and exit. +- Private GitHub repos can be accessed via existing git credentials or optional `GITHUB_TOKEN`/`GH_TOKEN` for download. +- Git fallback tries HTTPS first, then SSH. +- The skills at https://github.com/openai/skills/tree/main/skills/.system are preinstalled, so no need to help users install those. If they ask, just explain this. If they insist, you can download and overwrite. +- Installed annotations come from `$CODEX_HOME/skills`. diff --git a/codex-rs/core/src/skills/assets/samples/skill-installer/scripts/github_utils.py b/codex-rs/core/src/skills/assets/samples/skill-installer/scripts/github_utils.py new file mode 100644 index 000000000..711f597e4 --- /dev/null +++ b/codex-rs/core/src/skills/assets/samples/skill-installer/scripts/github_utils.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Shared GitHub helpers for skill install scripts.""" + +from __future__ import annotations + +import os +import urllib.request + + +def github_request(url: str, user_agent: str) -> bytes: + headers = {"User-Agent": user_agent} + token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") + if token: + headers["Authorization"] = f"token {token}" + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req) as resp: + return resp.read() + + +def github_api_contents_url(repo: str, path: str, ref: str) -> str: + return f"https://api.github.com/repos/{repo}/contents/{path}?ref={ref}" diff --git a/codex-rs/core/src/skills/assets/samples/skill-installer/scripts/install-skill-from-github.py b/codex-rs/core/src/skills/assets/samples/skill-installer/scripts/install-skill-from-github.py new file mode 100755 index 000000000..1c8ce89d0 --- /dev/null +++ b/codex-rs/core/src/skills/assets/samples/skill-installer/scripts/install-skill-from-github.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +"""Install a skill from a GitHub repo path into $CODEX_HOME/skills.""" + +from __future__ import annotations + +import argparse +from dataclasses import dataclass +import os +import shutil +import subprocess +import sys +import tempfile +import urllib.error +import urllib.parse +import zipfile + +from github_utils import github_request +DEFAULT_REF = "main" + + +@dataclass +class Args: + url: str | None = None + repo: str | None = None + path: list[str] | None = None + ref: str = DEFAULT_REF + dest: str | None = None + name: str | None = None + method: str = "auto" + + +@dataclass +class Source: + owner: str + repo: str + ref: str + paths: list[str] + repo_url: str | None = None + + +class InstallError(Exception): + pass + + +def _codex_home() -> str: + return os.environ.get("CODEX_HOME", os.path.expanduser("~/.codex")) + + +def _tmp_root() -> str: + base = os.path.join(tempfile.gettempdir(), "codex") + os.makedirs(base, exist_ok=True) + return base + + +def _request(url: str) -> bytes: + return github_request(url, "codex-skill-install") + + +def _parse_github_url(url: str, default_ref: str) -> tuple[str, str, str, str | None]: + parsed = urllib.parse.urlparse(url) + if parsed.netloc != "github.com": + raise InstallError("Only GitHub URLs are supported for download mode.") + parts = [p for p in parsed.path.split("/") if p] + if len(parts) < 2: + raise InstallError("Invalid GitHub URL.") + owner, repo = parts[0], parts[1] + ref = default_ref + subpath = "" + if len(parts) > 2: + if parts[2] in ("tree", "blob"): + if len(parts) < 4: + raise InstallError("GitHub URL missing ref or path.") + ref = parts[3] + subpath = "/".join(parts[4:]) + else: + subpath = "/".join(parts[2:]) + return owner, repo, ref, subpath or None + + +def _download_repo_zip(owner: str, repo: str, ref: str, dest_dir: str) -> str: + zip_url = f"https://codeload.github.com/{owner}/{repo}/zip/{ref}" + zip_path = os.path.join(dest_dir, "repo.zip") + try: + payload = _request(zip_url) + except urllib.error.HTTPError as exc: + raise InstallError(f"Download failed: HTTP {exc.code}") from exc + with open(zip_path, "wb") as file_handle: + file_handle.write(payload) + with zipfile.ZipFile(zip_path, "r") as zip_file: + _safe_extract_zip(zip_file, dest_dir) + top_levels = {name.split("/")[0] for name in zip_file.namelist() if name} + if not top_levels: + raise InstallError("Downloaded archive was empty.") + if len(top_levels) != 1: + raise InstallError("Unexpected archive layout.") + return os.path.join(dest_dir, next(iter(top_levels))) + + +def _run_git(args: list[str]) -> None: + result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if result.returncode != 0: + raise InstallError(result.stderr.strip() or "Git command failed.") + + +def _safe_extract_zip(zip_file: zipfile.ZipFile, dest_dir: str) -> None: + dest_root = os.path.realpath(dest_dir) + for info in zip_file.infolist(): + extracted_path = os.path.realpath(os.path.join(dest_dir, info.filename)) + if extracted_path == dest_root or extracted_path.startswith(dest_root + os.sep): + continue + raise InstallError("Archive contains files outside the destination.") + zip_file.extractall(dest_dir) + + +def _validate_relative_path(path: str) -> None: + if os.path.isabs(path) or os.path.normpath(path).startswith(".."): + raise InstallError("Skill path must be a relative path inside the repo.") + + +def _validate_skill_name(name: str) -> None: + altsep = os.path.altsep + if not name or os.path.sep in name or (altsep and altsep in name): + raise InstallError("Skill name must be a single path segment.") + if name in (".", ".."): + raise InstallError("Invalid skill name.") + + +def _git_sparse_checkout(repo_url: str, ref: str, paths: list[str], dest_dir: str) -> str: + repo_dir = os.path.join(dest_dir, "repo") + clone_cmd = [ + "git", + "clone", + "--filter=blob:none", + "--depth", + "1", + "--sparse", + "--single-branch", + "--branch", + ref, + repo_url, + repo_dir, + ] + try: + _run_git(clone_cmd) + except InstallError: + _run_git( + [ + "git", + "clone", + "--filter=blob:none", + "--depth", + "1", + "--sparse", + "--single-branch", + repo_url, + repo_dir, + ] + ) + _run_git(["git", "-C", repo_dir, "sparse-checkout", "set", *paths]) + _run_git(["git", "-C", repo_dir, "checkout", ref]) + return repo_dir + + +def _validate_skill(path: str) -> None: + if not os.path.isdir(path): + raise InstallError(f"Skill path not found: {path}") + skill_md = os.path.join(path, "SKILL.md") + if not os.path.isfile(skill_md): + raise InstallError("SKILL.md not found in selected skill directory.") + + +def _copy_skill(src: str, dest_dir: str) -> None: + os.makedirs(os.path.dirname(dest_dir), exist_ok=True) + if os.path.exists(dest_dir): + raise InstallError(f"Destination already exists: {dest_dir}") + shutil.copytree(src, dest_dir) + + +def _build_repo_url(owner: str, repo: str) -> str: + return f"https://github.com/{owner}/{repo}.git" + + +def _build_repo_ssh(owner: str, repo: str) -> str: + return f"git@github.com:{owner}/{repo}.git" + + +def _prepare_repo(source: Source, method: str, tmp_dir: str) -> str: + if method in ("download", "auto"): + try: + return _download_repo_zip(source.owner, source.repo, source.ref, tmp_dir) + except InstallError as exc: + if method == "download": + raise + err_msg = str(exc) + if "HTTP 401" in err_msg or "HTTP 403" in err_msg or "HTTP 404" in err_msg: + pass + else: + raise + if method in ("git", "auto"): + repo_url = source.repo_url or _build_repo_url(source.owner, source.repo) + try: + return _git_sparse_checkout(repo_url, source.ref, source.paths, tmp_dir) + except InstallError: + repo_url = _build_repo_ssh(source.owner, source.repo) + return _git_sparse_checkout(repo_url, source.ref, source.paths, tmp_dir) + raise InstallError("Unsupported method.") + + +def _resolve_source(args: Args) -> Source: + if args.url: + owner, repo, ref, url_path = _parse_github_url(args.url, args.ref) + if args.path is not None: + paths = list(args.path) + elif url_path: + paths = [url_path] + else: + paths = [] + if not paths: + raise InstallError("Missing --path for GitHub URL.") + return Source(owner=owner, repo=repo, ref=ref, paths=paths) + + if not args.repo: + raise InstallError("Provide --repo or --url.") + if "://" in args.repo: + return _resolve_source( + Args(url=args.repo, repo=None, path=args.path, ref=args.ref) + ) + + repo_parts = [p for p in args.repo.split("/") if p] + if len(repo_parts) != 2: + raise InstallError("--repo must be in owner/repo format.") + if not args.path: + raise InstallError("Missing --path for --repo.") + paths = list(args.path) + return Source( + owner=repo_parts[0], + repo=repo_parts[1], + ref=args.ref, + paths=paths, + ) + + +def _default_dest() -> str: + return os.path.join(_codex_home(), "skills") + + +def _parse_args(argv: list[str]) -> Args: + parser = argparse.ArgumentParser(description="Install a skill from GitHub.") + parser.add_argument("--repo", help="owner/repo") + parser.add_argument("--url", help="https://github.com/owner/repo[/tree/ref/path]") + parser.add_argument( + "--path", + nargs="+", + help="Path(s) to skill(s) inside repo", + ) + parser.add_argument("--ref", default=DEFAULT_REF) + parser.add_argument("--dest", help="Destination skills directory") + parser.add_argument( + "--name", help="Destination skill name (defaults to basename of path)" + ) + parser.add_argument( + "--method", + choices=["auto", "download", "git"], + default="auto", + ) + return parser.parse_args(argv, namespace=Args()) + + +def main(argv: list[str]) -> int: + args = _parse_args(argv) + try: + source = _resolve_source(args) + source.ref = source.ref or args.ref + if not source.paths: + raise InstallError("No skill paths provided.") + for path in source.paths: + _validate_relative_path(path) + dest_root = args.dest or _default_dest() + tmp_dir = tempfile.mkdtemp(prefix="skill-install-", dir=_tmp_root()) + try: + repo_root = _prepare_repo(source, args.method, tmp_dir) + installed = [] + for path in source.paths: + skill_name = args.name if len(source.paths) == 1 else None + skill_name = skill_name or os.path.basename(path.rstrip("/")) + _validate_skill_name(skill_name) + if not skill_name: + raise InstallError("Unable to derive skill name.") + dest_dir = os.path.join(dest_root, skill_name) + if os.path.exists(dest_dir): + raise InstallError(f"Destination already exists: {dest_dir}") + skill_src = os.path.join(repo_root, path) + _validate_skill(skill_src) + _copy_skill(skill_src, dest_dir) + installed.append((skill_name, dest_dir)) + finally: + if os.path.isdir(tmp_dir): + shutil.rmtree(tmp_dir, ignore_errors=True) + for skill_name, dest_dir in installed: + print(f"Installed {skill_name} to {dest_dir}") + return 0 + except InstallError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/codex-rs/core/src/skills/assets/samples/skill-installer/scripts/list-curated-skills.py b/codex-rs/core/src/skills/assets/samples/skill-installer/scripts/list-curated-skills.py new file mode 100755 index 000000000..08d475c8a --- /dev/null +++ b/codex-rs/core/src/skills/assets/samples/skill-installer/scripts/list-curated-skills.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""List curated skills from a GitHub repo path.""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import urllib.error + +from github_utils import github_api_contents_url, github_request + +DEFAULT_REPO = "openai/skills" +DEFAULT_PATH = "skills/.curated" +DEFAULT_REF = "main" + + +class ListError(Exception): + pass + + +class Args(argparse.Namespace): + repo: str + path: str + ref: str + format: str + + +def _request(url: str) -> bytes: + return github_request(url, "codex-skill-list") + + +def _codex_home() -> str: + return os.environ.get("CODEX_HOME", os.path.expanduser("~/.codex")) + + +def _installed_skills() -> set[str]: + root = os.path.join(_codex_home(), "skills") + if not os.path.isdir(root): + return set() + entries = set() + for name in os.listdir(root): + path = os.path.join(root, name) + if os.path.isdir(path): + entries.add(name) + return entries + + +def _list_curated(repo: str, path: str, ref: str) -> list[str]: + api_url = github_api_contents_url(repo, path, ref) + try: + payload = _request(api_url) + except urllib.error.HTTPError as exc: + if exc.code == 404: + raise ListError( + "Curated skills path not found: " + f"https://github.com/{repo}/tree/{ref}/{path}" + ) from exc + raise ListError(f"Failed to fetch curated skills: HTTP {exc.code}") from exc + data = json.loads(payload.decode("utf-8")) + if not isinstance(data, list): + raise ListError("Unexpected curated listing response.") + skills = [item["name"] for item in data if item.get("type") == "dir"] + return sorted(skills) + + +def _parse_args(argv: list[str]) -> Args: + parser = argparse.ArgumentParser(description="List curated skills.") + parser.add_argument("--repo", default=DEFAULT_REPO) + parser.add_argument("--path", default=DEFAULT_PATH) + parser.add_argument("--ref", default=DEFAULT_REF) + parser.add_argument( + "--format", + choices=["text", "json"], + default="text", + help="Output format", + ) + return parser.parse_args(argv, namespace=Args()) + + +def main(argv: list[str]) -> int: + args = _parse_args(argv) + try: + skills = _list_curated(args.repo, args.path, args.ref) + installed = _installed_skills() + if args.format == "json": + payload = [ + {"name": name, "installed": name in installed} for name in skills + ] + print(json.dumps(payload)) + else: + for idx, name in enumerate(skills, start=1): + suffix = " (already installed)" if name in installed else "" + print(f"{idx}. {name}{suffix}") + return 0 + except ListError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:]))